Skip to content

Commit 61a9d70

Browse files
committed
Add WASM backend for Ruby API
1 parent aa6bce6 commit 61a9d70

1 file changed

Lines changed: 376 additions & 0 deletions

File tree

lib/prism/wasm.rb

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
# frozen_string_literal: true
2+
# :markup: markdown
3+
# typed: ignore
4+
5+
# This file is responsible for mirroring the API provided by the C extension by
6+
# using FFI to call into the shared library.
7+
8+
require "rbconfig"
9+
require "ffi"
10+
11+
# We want to eagerly load this file if there are Ractors so that it does not get
12+
# autoloaded from within a non-main Ractor.
13+
require "prism/serialize" if defined?(Ractor)
14+
15+
# Load the prism-parser-wasm jar
16+
require 'jar-dependencies'
17+
require_jar('org.ruby-lang', 'prism-parser-wasm', '0.0.1-SNAPSHOT')
18+
require_jar('com.dylibso.chicory', 'runtime', '1.6.1')
19+
require_jar('com.dylibso.chicory', 'wasi', '1.6.1')
20+
require_jar('com.dylibso.chicory', 'wasm', '1.6.1')
21+
require_jar('com.dylibso.chicory', 'log', '1.6.1')
22+
23+
module Prism # :nodoc:
24+
module WASM
25+
java_import org.ruby_lang.prism.wasm.Prism
26+
27+
PRISM = org.ruby_lang.prism.wasm.Prism.new
28+
end
29+
private_constant :WASM
30+
31+
# The version constant is set by reading the result of calling pm_version.
32+
VERSION = WASM::PRISM.version
33+
34+
class << self
35+
# Mirror the Prism.dump API by using the serialization API.
36+
def dump(source, **options)
37+
parsed = WASM::PRISM.parse(source.to_java_bytes, dump_options(options).to_java_bytes)
38+
String.from_java_bytes(parsed)
39+
end
40+
41+
# Mirror the Prism.dump_file API by using the serialization API.
42+
def dump_file(filepath, **options)
43+
dump_file(File.read(filepath), filepath: filepath, **options)
44+
end
45+
46+
# Mirror the Prism.lex API by using the serialization API.
47+
def lex(source, **options)
48+
lexed = WASM::PRISM.lex(source.to_java_bytes, dump_options(options).to_java_bytes)
49+
Serialize.load_lex(source, lexed, options.fetch(:freeze, false))
50+
end
51+
52+
# Mirror the Prism.lex_file API by using the serialization API.
53+
def lex_file(filepath, **options)
54+
lex_file(File.read(filepath), filepath: filepath, **options)
55+
end
56+
57+
# Mirror the Prism.parse API by using the serialization API.
58+
def parse(source, **options)
59+
serialized = dump(source, **options)
60+
Serialize.load_parse(source, serialized, options.fetch(:freeze, false))
61+
end
62+
63+
# Mirror the Prism.parse_file API by using the serialization API. This uses
64+
# native strings instead of Ruby strings because it allows us to use mmap
65+
# when it is available.
66+
def parse_file(filepath, **options)
67+
parse(File.read(filepath), filepath: filepath, **options)
68+
end
69+
70+
# Mirror the Prism.parse_stream API by using the serialization API.
71+
def parse_stream(stream, **options)
72+
LibRubyParser::PrismBuffer.with do |buffer|
73+
source = +""
74+
callback = -> (string, size, _) {
75+
raise "Expected size to be >= 0, got: #{size}" if size <= 0
76+
77+
if !(line = stream.gets(size - 1)).nil?
78+
source << line
79+
string.write_string("#{line}\x00", line.bytesize + 1)
80+
end
81+
}
82+
83+
eof_callback = -> (_) { stream.eof? }
84+
85+
# In the pm_serialize_parse_stream function it accepts a pointer to the
86+
# IO object as a void* and then passes it through to the callback as the
87+
# third argument, but it never touches it itself. As such, since we have
88+
# access to the IO object already through the closure of the lambda, we
89+
# can pass a null pointer here and not worry.
90+
LibRubyParser.pm_serialize_parse_stream(buffer.pointer, nil, callback, eof_callback, dump_options(options))
91+
Prism.load(source, buffer.read, options.fetch(:freeze, false))
92+
end
93+
end
94+
95+
# Mirror the Prism.parse_comments API by using the serialization API.
96+
def parse_comments(code, **options)
97+
LibRubyParser::PrismString.with_string(code) { |string| parse_comments_common(string, code, options) }
98+
end
99+
100+
# Mirror the Prism.parse_file_comments API by using the serialization
101+
# API. This uses native strings instead of Ruby strings because it allows us
102+
# to use mmap when it is available.
103+
def parse_file_comments(filepath, **options)
104+
options[:filepath] = filepath
105+
LibRubyParser::PrismString.with_file(filepath) { |string| parse_comments_common(string, string.read, options) }
106+
end
107+
108+
# Mirror the Prism.parse_lex API by using the serialization API.
109+
def parse_lex(code, **options)
110+
LibRubyParser::PrismString.with_string(code) { |string| parse_lex_common(string, code, options) }
111+
end
112+
113+
# Mirror the Prism.parse_lex_file API by using the serialization API.
114+
def parse_lex_file(filepath, **options)
115+
options[:filepath] = filepath
116+
LibRubyParser::PrismString.with_file(filepath) { |string| parse_lex_common(string, string.read, options) }
117+
end
118+
119+
# Mirror the Prism.parse_success? API by using the serialization API.
120+
def parse_success?(code, **options)
121+
LibRubyParser::PrismString.with_string(code) { |string| parse_file_success_common(string, options) }
122+
end
123+
124+
# Mirror the Prism.parse_failure? API by using the serialization API.
125+
def parse_failure?(code, **options)
126+
!parse_success?(code, **options)
127+
end
128+
129+
# Mirror the Prism.parse_file_success? API by using the serialization API.
130+
def parse_file_success?(filepath, **options)
131+
options[:filepath] = filepath
132+
LibRubyParser::PrismString.with_file(filepath) { |string| parse_file_success_common(string, options) }
133+
end
134+
135+
# Mirror the Prism.parse_file_failure? API by using the serialization API.
136+
def parse_file_failure?(filepath, **options)
137+
!parse_file_success?(filepath, **options)
138+
end
139+
140+
# Mirror the Prism.profile API by using the serialization API.
141+
def profile(source, **options)
142+
LibRubyParser::PrismString.with_string(source) do |string|
143+
LibRubyParser::PrismBuffer.with do |buffer|
144+
LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
145+
nil
146+
end
147+
end
148+
end
149+
150+
# Mirror the Prism.profile_file API by using the serialization API.
151+
def profile_file(filepath, **options)
152+
LibRubyParser::PrismString.with_file(filepath) do |string|
153+
LibRubyParser::PrismBuffer.with do |buffer|
154+
options[:filepath] = filepath
155+
LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
156+
nil
157+
end
158+
end
159+
end
160+
161+
private
162+
163+
def lex_common(string, code, options) # :nodoc:
164+
LibRubyParser::PrismBuffer.with do |buffer|
165+
LibRubyParser.pm_serialize_lex(buffer.pointer, string.pointer, string.length, dump_options(options))
166+
Serialize.load_lex(code, buffer.read, options.fetch(:freeze, false))
167+
end
168+
end
169+
170+
def parse_common(string, code, options) # :nodoc:
171+
serialized = dump_common(string, options)
172+
Serialize.load_parse(code, serialized, options.fetch(:freeze, false))
173+
end
174+
175+
def parse_comments_common(string, code, options) # :nodoc:
176+
LibRubyParser::PrismBuffer.with do |buffer|
177+
LibRubyParser.pm_serialize_parse_comments(buffer.pointer, string.pointer, string.length, dump_options(options))
178+
Serialize.load_parse_comments(code, buffer.read, options.fetch(:freeze, false))
179+
end
180+
end
181+
182+
def parse_lex_common(string, code, options) # :nodoc:
183+
LibRubyParser::PrismBuffer.with do |buffer|
184+
LibRubyParser.pm_serialize_parse_lex(buffer.pointer, string.pointer, string.length, dump_options(options))
185+
Serialize.load_parse_lex(code, buffer.read, options.fetch(:freeze, false))
186+
end
187+
end
188+
189+
def parse_file_success_common(string, options) # :nodoc:
190+
LibRubyParser.pm_parse_success_p(string.pointer, string.length, dump_options(options))
191+
end
192+
193+
# Return the value that should be dumped for the command_line option.
194+
def dump_options_command_line(options)
195+
command_line = options.fetch(:command_line, "")
196+
raise ArgumentError, "command_line must be a string" unless command_line.is_a?(String)
197+
198+
command_line.each_char.inject(0) do |value, char|
199+
case char
200+
when "a" then value | 0b000001
201+
when "e" then value | 0b000010
202+
when "l" then value | 0b000100
203+
when "n" then value | 0b001000
204+
when "p" then value | 0b010000
205+
when "x" then value | 0b100000
206+
else raise ArgumentError, "invalid command_line option: #{char}"
207+
end
208+
end
209+
end
210+
211+
# Return the value that should be dumped for the version option.
212+
def dump_options_version(version)
213+
case version
214+
when "current"
215+
version_string_to_number(RUBY_VERSION) || raise(CurrentVersionError, RUBY_VERSION)
216+
when "latest", nil
217+
0 # Handled in pm_parser_init
218+
when "nearest"
219+
dump = version_string_to_number(RUBY_VERSION)
220+
return dump if dump
221+
if RUBY_VERSION < "3.3"
222+
version_string_to_number("3.3")
223+
else
224+
0 # Handled in pm_parser_init
225+
end
226+
else
227+
version_string_to_number(version) || raise(ArgumentError, "invalid version: #{version}")
228+
end
229+
end
230+
231+
# Converts a version string like "4.0.0" or "4.0" into a number.
232+
# Returns nil if the version is unknown.
233+
def version_string_to_number(version)
234+
case version
235+
when /\A3\.3(\.\d+)?\z/
236+
1
237+
when /\A3\.4(\.\d+)?\z/
238+
2
239+
when /\A3\.5(\.\d+)?\z/, /\A4\.0(\.\d+)?\z/
240+
3
241+
when /\A4\.1(\.\d+)?\z/
242+
4
243+
end
244+
end
245+
246+
# Convert the given options into a serialized options string.
247+
def dump_options(options)
248+
template = +""
249+
values = []
250+
251+
template << "L"
252+
if (filepath = options[:filepath])
253+
values.push(filepath.bytesize, filepath.b)
254+
template << "A*"
255+
else
256+
values << 0
257+
end
258+
259+
template << "l"
260+
values << options.fetch(:line, 1)
261+
262+
template << "L"
263+
if (encoding = options[:encoding])
264+
name = encoding.is_a?(Encoding) ? encoding.name : encoding
265+
values.push(name.bytesize, name.b)
266+
template << "A*"
267+
else
268+
values << 0
269+
end
270+
271+
template << "C"
272+
values << (options.fetch(:frozen_string_literal, false) ? 1 : 0)
273+
274+
template << "C"
275+
values << dump_options_command_line(options)
276+
277+
template << "C"
278+
values << dump_options_version(options[:version])
279+
280+
template << "C"
281+
values << (options[:encoding] == false ? 1 : 0)
282+
283+
template << "C"
284+
values << (options.fetch(:main_script, false) ? 1 : 0)
285+
286+
template << "C"
287+
values << (options.fetch(:partial_script, false) ? 1 : 0)
288+
289+
template << "C"
290+
values << (options.fetch(:freeze, false) ? 1 : 0)
291+
292+
template << "L"
293+
if (scopes = options[:scopes])
294+
values << scopes.length
295+
296+
scopes.each do |scope|
297+
locals = nil
298+
forwarding = 0
299+
300+
case scope
301+
when Array
302+
locals = scope
303+
when Scope
304+
locals = scope.locals
305+
306+
scope.forwarding.each do |forward|
307+
case forward
308+
when :* then forwarding |= 0x1
309+
when :** then forwarding |= 0x2
310+
when :& then forwarding |= 0x4
311+
when :"..." then forwarding |= 0x8
312+
else raise ArgumentError, "invalid forwarding value: #{forward}"
313+
end
314+
end
315+
else
316+
raise TypeError, "wrong argument type #{scope.class.inspect} (expected Array or Prism::Scope)"
317+
end
318+
319+
template << "L"
320+
values << locals.length
321+
322+
template << "C"
323+
values << forwarding
324+
325+
locals.each do |local|
326+
name = local.name
327+
template << "L"
328+
values << name.bytesize
329+
330+
template << "A*"
331+
values << name.b
332+
end
333+
end
334+
else
335+
values << 0
336+
end
337+
338+
values.pack(template)
339+
end
340+
end
341+
342+
# Here we are going to patch StringQuery to put in the class-level methods so
343+
# that it can maintain a consistent interface
344+
class StringQuery # :nodoc:
345+
class << self
346+
# Mirrors the C extension's StringQuery::local? method.
347+
def local?(string)
348+
query(LibRubyParser.pm_string_query_local(string, string.bytesize, string.encoding.name))
349+
end
350+
351+
# Mirrors the C extension's StringQuery::constant? method.
352+
def constant?(string)
353+
query(LibRubyParser.pm_string_query_constant(string, string.bytesize, string.encoding.name))
354+
end
355+
356+
# Mirrors the C extension's StringQuery::method_name? method.
357+
def method_name?(string)
358+
query(LibRubyParser.pm_string_query_method_name(string, string.bytesize, string.encoding.name))
359+
end
360+
361+
private
362+
363+
# Parse the enum result and return an appropriate boolean.
364+
def query(result)
365+
case result
366+
when :PM_STRING_QUERY_ERROR
367+
raise ArgumentError, "Invalid or non ascii-compatible encoding"
368+
when :PM_STRING_QUERY_FALSE
369+
false
370+
when :PM_STRING_QUERY_TRUE
371+
true
372+
end
373+
end
374+
end
375+
end
376+
end

0 commit comments

Comments
 (0)