rbs 4.1.0.pre.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +37 -0
  3. data/.github/workflows/bundle-update.yml +1 -1
  4. data/.github/workflows/c-check.yml +14 -6
  5. data/.github/workflows/comments.yml +2 -2
  6. data/.github/workflows/dependabot.yml +2 -2
  7. data/.github/workflows/jruby.yml +79 -0
  8. data/.github/workflows/milestone.yml +13 -2
  9. data/.github/workflows/ruby.yml +16 -4
  10. data/.github/workflows/rust.yml +13 -8
  11. data/.github/workflows/truffleruby.yml +54 -0
  12. data/.github/workflows/typecheck.yml +5 -2
  13. data/.github/workflows/wasm.yml +55 -0
  14. data/.github/workflows/windows.yml +8 -2
  15. data/.gitignore +8 -0
  16. data/CHANGELOG.md +88 -0
  17. data/Dockerfile.jruby +53 -0
  18. data/README.md +3 -3
  19. data/Rakefile +103 -1
  20. data/Steepfile +9 -0
  21. data/core/array.rbs +243 -180
  22. data/core/builtin.rbs +6 -6
  23. data/core/enumerable.rbs +109 -109
  24. data/core/enumerator/product.rbs +5 -5
  25. data/core/enumerator.rbs +28 -28
  26. data/core/file.rbs +24 -1018
  27. data/core/file_constants.rbs +463 -0
  28. data/core/file_stat.rbs +534 -0
  29. data/core/float.rbs +0 -24
  30. data/core/hash.rbs +117 -101
  31. data/core/integer.rbs +21 -58
  32. data/core/io.rbs +25 -7
  33. data/core/match_data.rbs +1 -1
  34. data/core/module.rbs +88 -74
  35. data/core/numeric.rbs +3 -0
  36. data/core/object_space/weak_key_map.rbs +7 -7
  37. data/core/pathname.rbs +0 -10
  38. data/core/ractor.rbs +0 -10
  39. data/core/range.rbs +23 -23
  40. data/core/rbs/ops.rbs +154 -0
  41. data/core/rbs/unnamed/argf.rbs +3 -3
  42. data/core/rubygems/errors.rbs +4 -1
  43. data/core/rubygems/requirement.rbs +0 -10
  44. data/core/rubygems/rubygems.rbs +4 -1
  45. data/core/rubygems/specification.rbs +8 -0
  46. data/core/rubygems/version.rbs +0 -160
  47. data/core/set.rbs +3 -3
  48. data/core/struct.rbs +16 -16
  49. data/core/thread.rbs +9 -14
  50. data/docs/CONTRIBUTING.md +2 -1
  51. data/docs/inline.md +36 -6
  52. data/docs/rbs_by_example.md +20 -20
  53. data/docs/release.md +69 -0
  54. data/docs/syntax.md +2 -2
  55. data/docs/wasm_serialization.md +80 -0
  56. data/ext/rbs_extension/ast_translation.c +1294 -973
  57. data/ext/rbs_extension/ast_translation.h +4 -0
  58. data/ext/rbs_extension/legacy_location.c +11 -6
  59. data/ext/rbs_extension/main.c +139 -4
  60. data/include/rbs/ast.h +9 -1
  61. data/include/rbs/serialize.h +39 -0
  62. data/lib/rbs/ast/ruby/comment_block.rb +6 -4
  63. data/lib/rbs/ast/ruby/helpers/location_helper.rb +1 -1
  64. data/lib/rbs/ast/ruby/members.rb +12 -1
  65. data/lib/rbs/buffer.rb +48 -11
  66. data/lib/rbs/collection/sources/git.rb +6 -0
  67. data/lib/rbs/definition_builder/method_builder.rb +12 -6
  68. data/lib/rbs/environment.rb +4 -3
  69. data/lib/rbs/inline_parser.rb +5 -3
  70. data/lib/rbs/namespace.rb +47 -11
  71. data/lib/rbs/parser_aux.rb +4 -2
  72. data/lib/rbs/prototype/rbi.rb +193 -25
  73. data/lib/rbs/prototype/runtime.rb +2 -0
  74. data/lib/rbs/resolver/type_name_resolver.rb +12 -14
  75. data/lib/rbs/type_name.rb +33 -13
  76. data/lib/rbs/unit_test/type_assertions.rb +9 -0
  77. data/lib/rbs/version.rb +1 -1
  78. data/lib/rbs/wasm/deserializer.rb +213 -0
  79. data/lib/rbs/wasm/location.rb +61 -0
  80. data/lib/rbs/wasm/parser.rb +137 -0
  81. data/lib/rbs/wasm/runtime.rb +196 -0
  82. data/lib/rbs/wasm/serialization_schema.rb +110 -0
  83. data/lib/rbs.rb +13 -2
  84. data/lib/rbs_jars.rb +39 -0
  85. data/lib/rdoc_plugin/parser.rb +5 -0
  86. data/rbs.gemspec +32 -2
  87. data/sig/ast/ruby/members.rbs +6 -1
  88. data/sig/buffer.rbs +19 -1
  89. data/sig/namespace.rbs +20 -0
  90. data/sig/parser.rbs +10 -0
  91. data/sig/prototype/rbi.rbs +33 -4
  92. data/sig/resolver/type_name_resolver.rbs +2 -4
  93. data/sig/typename.rbs +15 -0
  94. data/sig/unit_test/type_assertions.rbs +4 -0
  95. data/sig/wasm/deserializer.rbs +66 -0
  96. data/sig/wasm/serialization_schema.rbs +13 -0
  97. data/src/ast.c +78 -78
  98. data/src/lexer.c +97 -93
  99. data/src/lexer.re +1 -1
  100. data/src/lexstate.c +9 -1
  101. data/src/serialize.c +958 -0
  102. data/src/util/rbs_allocator.c +14 -8
  103. data/stdlib/abbrev/0/array.rbs +1 -1
  104. data/stdlib/csv/0/csv.rbs +5 -5
  105. data/stdlib/delegate/0/delegator.rbs +2 -1
  106. data/stdlib/digest/0/digest.rbs +11 -5
  107. data/stdlib/erb/0/erb.rbs +1 -1
  108. data/stdlib/etc/0/etc.rbs +18 -4
  109. data/stdlib/fileutils/0/fileutils.rbs +21 -21
  110. data/stdlib/ipaddr/0/ipaddr.rbs +0 -5
  111. data/stdlib/json/0/json.rbs +6 -6
  112. data/stdlib/monitor/0/monitor.rbs +2 -2
  113. data/stdlib/openssl/0/openssl.rbs +44 -38
  114. data/stdlib/resolv/0/resolv.rbs +1 -1
  115. data/stdlib/shellwords/0/shellwords.rbs +1 -1
  116. data/stdlib/stringio/0/stringio.rbs +32 -10
  117. data/stdlib/strscan/0/string_scanner.rbs +74 -55
  118. data/stdlib/tempfile/0/manifest.yaml +3 -0
  119. data/stdlib/timeout/0/timeout.rbs +0 -5
  120. data/stdlib/tsort/0/cyclic.rbs +1 -1
  121. data/stdlib/tsort/0/interfaces.rbs +8 -8
  122. data/stdlib/tsort/0/tsort.rbs +9 -9
  123. data/stdlib/uri/0/generic.rbs +0 -5
  124. data/stdlib/zlib/0/gzip_reader.rbs +2 -2
  125. data/stdlib/zlib/0/zstream.rbs +0 -1
  126. data/wasm/README.md +60 -0
  127. data/wasm/rbs_wasm.c +423 -0
  128. metadata +26 -5
  129. data/.vscode/extensions.json +0 -5
  130. data/.vscode/settings.json +0 -19
@@ -323,6 +323,15 @@ module RBS
323
323
  assert typecheck.value(constant, definition_type), "`#{constant_name}` (#{constant.inspect}) must be compatible with RBS type definition `#{definition_type}`"
324
324
  end
325
325
 
326
+ def assert_visibility(visibility, method)
327
+ _, definition = target
328
+ method_entry = definition.methods[method]
329
+
330
+ assert method_entry, "Method `#{method}` not found in RBS definition"
331
+ assert visibility == method_entry.accessibility,
332
+ "Expected `#{method}` to be #{visibility}, but was #{method_entry.accessibility}"
333
+ end
334
+
326
335
  def assert_type(type, value)
327
336
  typecheck = RBS::Test::TypeCheck.new(
328
337
  self_class: value.class,
data/lib/rbs/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RBS
4
- VERSION = "4.1.0.pre.1"
4
+ VERSION = "4.1.0"
5
5
  end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "serialization_schema"
4
+
5
+ module RBS
6
+ module WASM
7
+ # Rebuilds RBS::AST objects from the binary buffer produced by
8
+ # `rbs_serialize_node` (src/serialize.c), driven by the generated
9
+ # SerializationSchema. This is the pure-Ruby counterpart of the C extension's
10
+ # ast_translation.c, used when the parser runs inside WebAssembly (JRuby).
11
+ #
12
+ # All locations are reconstructed through the public RBS::Location API, so the
13
+ # same code works whether RBS::Location is backed by the C extension (CRuby)
14
+ # or by a pure-Ruby implementation (JRuby).
15
+ class Deserializer
16
+ # Deserialize a buffer produced for a whole signature, returning
17
+ # `[directives, declarations]` to match RBS::Parser._parse_signature.
18
+ def self.deserialize(bytes, buffer)
19
+ new(bytes, buffer).read_node
20
+ end
21
+
22
+ # Deserialize a bare node list (rbs_serialize_node_list), e.g. the result
23
+ # of RBS::Parser._parse_type_params.
24
+ def self.deserialize_node_list(bytes, buffer)
25
+ new(bytes, buffer).read_node_list
26
+ end
27
+
28
+ # Deserialize the token stream produced by rbs_wasm_lex into the
29
+ # [type, location] pairs RBS::Parser._lex returns.
30
+ def self.deserialize_tokens(bytes, buffer)
31
+ new(bytes, buffer).read_tokens
32
+ end
33
+
34
+ def initialize(bytes, buffer)
35
+ @bytes = bytes
36
+ @buffer = buffer
37
+ # Symbols and rbs_string fields (comments, annotations) inherit the
38
+ # source encoding, matching ast_translation.c. String/Integer literal
39
+ # nodes are always UTF-8 (see read_node).
40
+ @encoding = buffer.content.encoding
41
+ @pos = 0
42
+ @class_cache = {} #: Hash[String, untyped]
43
+ end
44
+
45
+ def read_node
46
+ tag = read_u8
47
+ return nil if tag == 0
48
+ return read_string(@encoding).to_sym if tag == SerializationSchema::SYMBOL_TAG
49
+
50
+ entry = SerializationSchema::SCHEMA[tag] or raise "Unknown node tag: #{tag}"
51
+
52
+ case entry[0]
53
+ when :node then read_struct(entry)
54
+ when :bool then read_u8 != 0
55
+ when :integer then read_string(Encoding::UTF_8).to_i
56
+ when :string_value then read_string(Encoding::UTF_8)
57
+ when :record_field then [read_node, read_u8 != 0]
58
+ when :signature then [read_node_list, read_node_list]
59
+ when :namespace then RBS::Namespace[read_node_list, read_u8 != 0]
60
+ when :type_name then RBS::TypeName[read_node, read_node]
61
+ else raise "Unknown schema entry kind: #{entry[0].inspect}"
62
+ end
63
+ end
64
+
65
+ def read_node_list
66
+ Array.new(read_count) { read_node }
67
+ end
68
+
69
+ # The lex stream has no leading count: read records until the buffer is
70
+ # exhausted. Each is a token type name followed by its character range.
71
+ def read_tokens
72
+ tokens = [] #: Array[[ Symbol, Location ]]
73
+ until @pos >= @bytes.bytesize
74
+ type = read_string(Encoding::UTF_8).to_sym
75
+ start_char = read_i32
76
+ end_char = read_i32
77
+ tokens << [type, RBS::Location.new(@buffer, start_char, end_char)]
78
+ end
79
+ tokens
80
+ end
81
+
82
+ private
83
+
84
+ def read_struct(entry)
85
+ _, class_name, expose_location, loc_children, fields, resolve_type_params = entry
86
+
87
+ location = read_location(loc_children) if expose_location
88
+
89
+ kwargs = {} #: Hash[Symbol, untyped]
90
+ (fields || []).each do |name, reader|
91
+ kwargs[name] = read_field(reader)
92
+ end
93
+
94
+ RBS::AST::TypeParam.resolve_variables(kwargs[:type_params]) if resolve_type_params
95
+
96
+ klass = class_for(class_name)
97
+ if expose_location
98
+ klass.new(location: location, **kwargs)
99
+ else
100
+ klass.new(**kwargs)
101
+ end
102
+ end
103
+
104
+ def read_field(reader)
105
+ case reader
106
+ when :node then read_node
107
+ when :node_list then read_node_list
108
+ when :hash then read_hash
109
+ when :string then read_string(@encoding)
110
+ when :bool then read_u8 != 0
111
+ when :location_range then read_location_value
112
+ when :location_range_list then read_location_value_list
113
+ when :attr_ivar_name then read_attr_ivar_name
114
+ else # [:enum, [value_or_nil, ...]]
115
+ reader[1][read_u8]
116
+ end
117
+ end
118
+
119
+ def read_hash
120
+ hash = {} #: Hash[untyped, untyped]
121
+ read_count.times do
122
+ key = read_node
123
+ hash[key] = read_node
124
+ end
125
+ hash
126
+ end
127
+
128
+ # A count of nested items. Each item is at least one byte, so a count that
129
+ # exceeds the bytes remaining signals the cursor has drifted out of sync.
130
+ def read_count
131
+ count = read_u32
132
+ if count > @bytes.bytesize - @pos
133
+ raise "Corrupt buffer: count #{count} exceeds #{@bytes.bytesize - @pos} remaining bytes at offset #{@pos}"
134
+ end
135
+ count
136
+ end
137
+
138
+ # The base location of a node, followed by its named child ranges.
139
+ def read_location(loc_children)
140
+ base = read_range
141
+ children = (loc_children || []).map { |name, required| [name, required, read_range] }
142
+
143
+ return nil unless base
144
+
145
+ location = RBS::Location.new(@buffer, base[0], base[1])
146
+ children.each do |name, required, range|
147
+ if required
148
+ location.add_required_child(name, range[0]...range[1]) if range
149
+ else
150
+ location.add_optional_child(name, range ? (range[0]...range[1]) : nil)
151
+ end
152
+ end
153
+ location
154
+ end
155
+
156
+ # A standalone location range field: nil or an RBS::Location without children.
157
+ def read_location_value
158
+ range = read_range
159
+ range && RBS::Location.new(@buffer, range[0], range[1])
160
+ end
161
+
162
+ def read_location_value_list
163
+ Array.new(read_count) { read_location_value }
164
+ end
165
+
166
+ def read_attr_ivar_name
167
+ case read_u8
168
+ when 0 then nil # inferred instance variable
169
+ when 1 then false # no instance variable
170
+ else read_string(@encoding).to_sym
171
+ end
172
+ end
173
+
174
+ # Reads a presence byte and, when present, the start/end character positions.
175
+ def read_range
176
+ return nil if read_u8 == 0
177
+
178
+ start_char = read_i32
179
+ end_char = read_i32
180
+ [start_char, end_char]
181
+ end
182
+
183
+ def read_u8
184
+ byte = @bytes.getbyte(@pos) or raise "Unexpected end of buffer"
185
+ @pos += 1
186
+ byte
187
+ end
188
+
189
+ def read_u32
190
+ value = @bytes.unpack1("L<", offset: @pos) #: Integer
191
+ @pos += 4
192
+ value
193
+ end
194
+
195
+ def read_i32
196
+ value = @bytes.unpack1("l<", offset: @pos) #: Integer
197
+ @pos += 4
198
+ value
199
+ end
200
+
201
+ def read_string(encoding)
202
+ length = read_u32
203
+ string = @bytes.byteslice(@pos, length) or raise "Unexpected end of buffer"
204
+ @pos += length
205
+ string.force_encoding(encoding)
206
+ end
207
+
208
+ def class_for(name)
209
+ @class_cache[name] ||= Object.const_get(name)
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBS
4
+ # Pure-Ruby implementation of the primitives that back RBS::Location.
5
+ #
6
+ # On CRuby these come from the C extension (ext/rbs_extension/legacy_location.c).
7
+ # JRuby loads this instead, before rbs/location_aux.rb layers the public API on
8
+ # top, so RBS::Location behaves identically without the native extension.
9
+ class Location
10
+ attr_reader :buffer
11
+
12
+ def initialize(buffer, start_pos, end_pos)
13
+ @buffer = buffer
14
+ @start_pos = start_pos
15
+ @end_pos = end_pos
16
+ @required_children = {} #: Hash[Symbol, [ Integer, Integer ]]
17
+ @optional_children = {} #: Hash[Symbol, [ Integer, Integer ]?]
18
+ end
19
+
20
+ def _start_pos
21
+ @start_pos
22
+ end
23
+
24
+ def _end_pos
25
+ @end_pos
26
+ end
27
+
28
+ def _add_required_child(name, start_pos, end_pos)
29
+ @required_children[name] = [start_pos, end_pos]
30
+ end
31
+
32
+ def _add_optional_child(name, start_pos, end_pos)
33
+ @optional_children[name] = [start_pos, end_pos]
34
+ end
35
+
36
+ def _add_optional_no_child(name)
37
+ @optional_children[name] = nil
38
+ end
39
+
40
+ def _required_keys
41
+ @required_children.keys
42
+ end
43
+
44
+ def _optional_keys
45
+ @optional_children.keys
46
+ end
47
+
48
+ def [](name)
49
+ if (range = @required_children[name])
50
+ return Location.new(@buffer, range[0], range[1])
51
+ end
52
+
53
+ if @optional_children.key?(name)
54
+ range = @optional_children[name]
55
+ return range && Location.new(@buffer, range[0], range[1])
56
+ end
57
+
58
+ raise "Unknown child name given: #{name}"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "runtime"
4
+ require_relative "deserializer"
5
+
6
+ module RBS
7
+ # WebAssembly-backed implementation of the parser primitives.
8
+ #
9
+ # On CRuby these come from the C extension (ext/rbs_extension/main.c). JRuby
10
+ # loads this instead: it runs the parser inside WebAssembly, then rebuilds the
11
+ # AST with RBS::WASM::Deserializer. rbs/parser_aux.rb layers the public
12
+ # RBS::Parser API on top, exactly as it does for the C extension.
13
+ class Parser
14
+ class << self
15
+ def _parse_signature(buffer, start_pos, end_pos)
16
+ validate_position_range(start_pos, end_pos)
17
+ encoding = buffer.content.encoding.name
18
+ success, bytes = WASM::Runtime.instance.parse_signature(buffer.content, encoding, start_pos, end_pos)
19
+ raise_parsing_error(buffer, bytes) unless success
20
+
21
+ WASM::Deserializer.deserialize(bytes, buffer)
22
+ end
23
+
24
+ def _parse_type(buffer, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed)
25
+ validate_position_range(start_pos, end_pos)
26
+ validate_variables(variables)
27
+ encoding = buffer.content.encoding.name
28
+ success, bytes = WASM::Runtime.instance.parse_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed)
29
+ raise_parsing_error(buffer, bytes) unless success
30
+
31
+ deserialize_or_nil(bytes, buffer)
32
+ end
33
+
34
+ def _parse_method_type(buffer, start_pos, end_pos, variables, require_eof)
35
+ validate_position_range(start_pos, end_pos)
36
+ validate_variables(variables)
37
+ encoding = buffer.content.encoding.name
38
+ success, bytes = WASM::Runtime.instance.parse_method_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof)
39
+ raise_parsing_error(buffer, bytes) unless success
40
+
41
+ deserialize_or_nil(bytes, buffer)
42
+ end
43
+
44
+ def _parse_type_params(buffer, start_pos, end_pos, module_type_params)
45
+ validate_position_range(start_pos, end_pos)
46
+ encoding = buffer.content.encoding.name
47
+ success, bytes = WASM::Runtime.instance.parse_type_params(buffer.content, encoding, start_pos, end_pos, module_type_params)
48
+ raise_parsing_error(buffer, bytes) unless success
49
+
50
+ bytes.empty? ? nil : WASM::Deserializer.deserialize_node_list(bytes, buffer)
51
+ end
52
+
53
+ def _lex(buffer, end_pos)
54
+ encoding = buffer.content.encoding.name
55
+ _success, bytes = WASM::Runtime.instance.lex(buffer.content, encoding, end_pos)
56
+
57
+ WASM::Deserializer.deserialize_tokens(bytes, buffer)
58
+ end
59
+
60
+ def _parse_inline_leading_annotation(buffer, start_pos, end_pos, variables)
61
+ validate_position_range(start_pos, end_pos)
62
+ validate_variables(variables)
63
+ encoding = buffer.content.encoding.name
64
+ success, bytes = WASM::Runtime.instance.parse_inline_leading_annotation(buffer.content, encoding, start_pos, end_pos, variables)
65
+ raise_parsing_error(buffer, bytes) unless success
66
+
67
+ deserialize_or_nil(bytes, buffer)
68
+ end
69
+
70
+ def _parse_inline_trailing_annotation(buffer, start_pos, end_pos, variables)
71
+ validate_position_range(start_pos, end_pos)
72
+ validate_variables(variables)
73
+ encoding = buffer.content.encoding.name
74
+ success, bytes = WASM::Runtime.instance.parse_inline_trailing_annotation(buffer.content, encoding, start_pos, end_pos, variables)
75
+ raise_parsing_error(buffer, bytes) unless success
76
+
77
+ deserialize_or_nil(bytes, buffer)
78
+ end
79
+
80
+ private
81
+
82
+ # Reject negative or reversed ranges before handing them to the parser,
83
+ # matching validate_position_range in the C extension (main.c). A reversed
84
+ # range would otherwise make the lexer loop forever inside WebAssembly.
85
+ def validate_position_range(start_pos, end_pos)
86
+ if start_pos < 0 || end_pos < 0
87
+ raise ArgumentError, "negative position range: #{start_pos}...#{end_pos}"
88
+ end
89
+ if start_pos > end_pos
90
+ raise ArgumentError, "invalid position range: #{start_pos}...#{end_pos}"
91
+ end
92
+ end
93
+
94
+ # Reject anything that is not nil or an Array of Symbols, matching
95
+ # declare_type_variables in the C extension (main.c).
96
+ def validate_variables(variables)
97
+ return if variables.nil?
98
+
99
+ unless variables.is_a?(Array)
100
+ raise TypeError, "wrong argument type #{variables.class} (must be an Array of Symbols or nil)"
101
+ end
102
+
103
+ variables.each do |variable|
104
+ unless variable.is_a?(Symbol)
105
+ raise TypeError, "Type variables Array contains invalid value #{variable.inspect} of type #{variable.class} (must be an Array of Symbols or nil)"
106
+ end
107
+ end
108
+ end
109
+
110
+ # An empty result means the parser reached EOF immediately (`nil`).
111
+ def deserialize_or_nil(bytes, buffer)
112
+ bytes.empty? ? nil : WASM::Deserializer.deserialize(bytes, buffer)
113
+ end
114
+
115
+ # Decodes the error blob written by set_error_result (rbs_wasm.c) and raises
116
+ # the same error the C extension would (see raise_error in main.c).
117
+ def raise_parsing_error(buffer, blob)
118
+ start_char, end_char, syntax_error = blob.unpack("l<l<C")
119
+
120
+ raise "Unexpected error" if syntax_error.zero?
121
+
122
+ offset = 9
123
+ token_type_length = blob.unpack1("L<", offset: offset)
124
+ offset += 4
125
+ token_type = blob.byteslice(offset, token_type_length).to_s.force_encoding(Encoding::UTF_8)
126
+ offset += token_type_length
127
+
128
+ message_length = blob.unpack1("L<", offset: offset)
129
+ offset += 4
130
+ message = blob.byteslice(offset, message_length).to_s.force_encoding(Encoding::UTF_8)
131
+
132
+ location = Location.new(buffer, start_char, end_char)
133
+ raise ParsingError.new(location, message, token_type)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "java"
4
+ require "monitor"
5
+
6
+ module RBS
7
+ module WASM
8
+ # Loads rbs_parser.wasm into a JVM WebAssembly runtime (Chicory) and drives
9
+ # it. This is the JRuby counterpart of the C extension's main.c: it copies a
10
+ # source string into the module's linear memory, runs the parser, and returns
11
+ # the serialized result for RBS::WASM::Deserializer to rebuild.
12
+ #
13
+ # Chicory is a pure-Java runtime, so there is no native dependency. The
14
+ # `.wasm` ships in the gem; the Chicory jars are fetched from Maven by
15
+ # jar-dependencies (see lib/rbs_jars.rb and rbs.gemspec).
16
+ class Runtime
17
+ include MonitorMixin
18
+
19
+ class << self
20
+ def instance
21
+ @instance ||= new
22
+ end
23
+
24
+ def wasm_path
25
+ ENV["RBS_WASM_PARSER"] || File.expand_path("rbs_parser.wasm", __dir__)
26
+ end
27
+ end
28
+
29
+ def initialize
30
+ super()
31
+ # rbs_jars.rb require_jars the Chicory/ASM jars from the local Maven
32
+ # repository (~/.m2), where jar-dependencies puts them at gem install (or
33
+ # `rake wasm:install_jars` when running from source).
34
+ require "rbs_jars"
35
+ @wasm = build_instance
36
+ @memory = @wasm.memory
37
+ @alloc = @wasm.export("rbs_wasm_alloc")
38
+ @free = @wasm.export("rbs_wasm_free")
39
+ @result_ptr = @wasm.export("rbs_wasm_result_ptr")
40
+ @result_len = @wasm.export("rbs_wasm_result_len")
41
+ @parse_signature = @wasm.export("rbs_wasm_parse_signature")
42
+ @parse_type = @wasm.export("rbs_wasm_parse_type")
43
+ @parse_method_type = @wasm.export("rbs_wasm_parse_method_type")
44
+ @parse_type_params = @wasm.export("rbs_wasm_parse_type_params")
45
+ @parse_inline_leading_annotation = @wasm.export("rbs_wasm_parse_inline_leading_annotation")
46
+ @parse_inline_trailing_annotation = @wasm.export("rbs_wasm_parse_inline_trailing_annotation")
47
+ @lex = @wasm.export("rbs_wasm_lex")
48
+ end
49
+
50
+ # `content` is the whole buffer; `start_pos`/`end_pos` are the character
51
+ # range within it to parse. Each method returns [success, bytes]: on success
52
+ # `bytes` is the serialized AST, otherwise it is the error blob (see
53
+ # set_error_result in rbs_wasm.c).
54
+
55
+ def parse_signature(content, encoding, start_pos, end_pos)
56
+ run(content, encoding) { |ptr, len, enc_ptr, enc_len| @parse_signature.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos)[0] }
57
+ end
58
+
59
+ def parse_type(content, encoding, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed)
60
+ with_variables(variables) do |vars_ptr, vars_len|
61
+ run(content, encoding) do |ptr, len, enc_ptr, enc_len|
62
+ @parse_type.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos, vars_ptr, vars_len, bool(require_eof), bool(void_allowed), bool(self_allowed), bool(classish_allowed))[0]
63
+ end
64
+ end
65
+ end
66
+
67
+ def parse_method_type(content, encoding, start_pos, end_pos, variables, require_eof)
68
+ with_variables(variables) do |vars_ptr, vars_len|
69
+ run(content, encoding) do |ptr, len, enc_ptr, enc_len|
70
+ @parse_method_type.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos, vars_ptr, vars_len, bool(require_eof))[0]
71
+ end
72
+ end
73
+ end
74
+
75
+ def parse_type_params(content, encoding, start_pos, end_pos, module_type_params)
76
+ run(content, encoding) do |ptr, len, enc_ptr, enc_len|
77
+ @parse_type_params.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos, bool(module_type_params))[0]
78
+ end
79
+ end
80
+
81
+ def parse_inline_leading_annotation(content, encoding, start_pos, end_pos, variables)
82
+ with_variables(variables) do |vars_ptr, vars_len|
83
+ run(content, encoding) do |ptr, len, enc_ptr, enc_len|
84
+ @parse_inline_leading_annotation.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos, vars_ptr, vars_len)[0]
85
+ end
86
+ end
87
+ end
88
+
89
+ def parse_inline_trailing_annotation(content, encoding, start_pos, end_pos, variables)
90
+ with_variables(variables) do |vars_ptr, vars_len|
91
+ run(content, encoding) do |ptr, len, enc_ptr, enc_len|
92
+ @parse_inline_trailing_annotation.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos, vars_ptr, vars_len)[0]
93
+ end
94
+ end
95
+ end
96
+
97
+ def lex(content, encoding, end_pos)
98
+ run(content, encoding) do |ptr, len, enc_ptr, enc_len|
99
+ @lex.apply(ptr, len, enc_ptr, enc_len, end_pos)[0]
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # Copies `source` and its encoding name into linear memory, yields their
106
+ # pointers/lengths to the block (which invokes the parser and returns its
107
+ # status), then reads the result back out. Serialized through the monitor
108
+ # because the module keeps its result in a single shared location.
109
+ def run(source, encoding)
110
+ synchronize do
111
+ bytes = source.b
112
+ length = bytes.bytesize
113
+ name = encoding.to_s.b
114
+ name_length = name.bytesize
115
+ source_ptr = @alloc.apply(length)[0]
116
+ name_ptr = @alloc.apply(name_length)[0]
117
+ begin
118
+ @memory.write(source_ptr, bytes.to_java_bytes)
119
+ @memory.write(name_ptr, name.to_java_bytes) unless name_length.zero?
120
+ status = yield(source_ptr, length, name_ptr, name_length)
121
+ [status == 1, read_result]
122
+ ensure
123
+ @free.apply(source_ptr)
124
+ @free.apply(name_ptr)
125
+ end
126
+ end
127
+ end
128
+
129
+ def read_result
130
+ pointer = @result_ptr.apply[0]
131
+ length = @result_len.apply[0]
132
+ return "".b if length.zero?
133
+
134
+ String.from_java_bytes(@memory.read_bytes(pointer, length)).b
135
+ end
136
+
137
+ # Allocates a buffer of newline-separated variable names and yields its
138
+ # pointer/length. A nil `variables` is passed as length -1 ("no variables").
139
+ def with_variables(variables)
140
+ names = variables&.map(&:to_s)&.join("\n")
141
+
142
+ if names.nil? || names.empty?
143
+ return yield(0, variables.nil? ? -1 : 0)
144
+ end
145
+
146
+ bytes = names.b
147
+ length = bytes.bytesize
148
+ synchronize do
149
+ pointer = @alloc.apply(length)[0]
150
+ begin
151
+ @memory.write(pointer, bytes.to_java_bytes)
152
+ yield(pointer, length)
153
+ ensure
154
+ @free.apply(pointer)
155
+ end
156
+ end
157
+ end
158
+
159
+ def bool(value)
160
+ value ? 1 : 0
161
+ end
162
+
163
+ def build_instance
164
+ parser = Java::ComDylibsoChicoryWasm::Parser
165
+ instance_class = Java::ComDylibsoChicoryRuntime::Instance
166
+ import_values = Java::ComDylibsoChicoryRuntime::ImportValues
167
+ wasi_preview1 = Java::ComDylibsoChicoryWasi::WasiPreview1
168
+ wasi_options = Java::ComDylibsoChicoryWasi::WasiOptions
169
+
170
+ wasm_module = parser.parse(java.io.File.new(self.class.wasm_path))
171
+ wasi = wasi_preview1.builder.with_options(wasi_options.builder.build).build
172
+ imports = import_values.builder.add_function(wasi.to_host_functions).build
173
+
174
+ builder = instance_class.builder(wasm_module).with_import_values(imports)
175
+ if (factory = machine_factory(wasm_module))
176
+ builder = builder.with_machine_factory(factory)
177
+ end
178
+
179
+ wasm = builder.build
180
+ wasm.export("_initialize").apply
181
+ wasm
182
+ end
183
+
184
+ # Chicory's AOT compiler when its jars are present and usable, otherwise nil
185
+ # (the builder then uses the interpreter). NameError covers a missing
186
+ # compiler class; LinkageError covers an incompatible/missing ASM (so a bad
187
+ # jar set degrades to the interpreter instead of crashing).
188
+ def machine_factory(wasm_module)
189
+ Java::ComDylibsoChicoryCompiler::MachineFactoryCompiler.compile(wasm_module)
190
+ rescue NameError, Java::JavaLang::LinkageError
191
+ nil
192
+ end
193
+
194
+ end
195
+ end
196
+ end