Skip to content

Commit e5c20ae

Browse files
authored
Merge pull request #3085 from ruby/claude/jruby-pr-3083-null-issue-0s30d5
Handle the NULL parser in the WebAssembly shim
2 parents 0870d21 + 5bffc46 commit e5c20ae

4 files changed

Lines changed: 176 additions & 68 deletions

File tree

lib/rbs/wasm/parser.rb

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,82 +13,90 @@ module RBS
1313
class Parser
1414
class << self
1515
def _parse_signature(buffer, start_pos, end_pos)
16-
validate_position_range(start_pos, end_pos)
16+
validate_position_range(buffer, start_pos, end_pos)
1717
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
18+
status, bytes = WASM::Runtime.instance.parse_signature(buffer.content, encoding, start_pos, end_pos)
19+
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK
2020

2121
WASM::Deserializer.deserialize(bytes, buffer)
2222
end
2323

2424
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)
25+
validate_position_range(buffer, start_pos, end_pos)
2626
validate_variables(variables)
2727
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
28+
status, bytes = WASM::Runtime.instance.parse_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed)
29+
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK
3030

3131
deserialize_or_nil(bytes, buffer)
3232
end
3333

3434
def _parse_method_type(buffer, start_pos, end_pos, variables, require_eof)
35-
validate_position_range(start_pos, end_pos)
35+
validate_position_range(buffer, start_pos, end_pos)
3636
validate_variables(variables)
3737
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
38+
status, bytes = WASM::Runtime.instance.parse_method_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof)
39+
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK
4040

4141
deserialize_or_nil(bytes, buffer)
4242
end
4343

4444
def _parse_type_params(buffer, start_pos, end_pos, module_type_params)
45-
validate_position_range(start_pos, end_pos)
45+
validate_position_range(buffer, start_pos, end_pos)
4646
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
47+
status, bytes = WASM::Runtime.instance.parse_type_params(buffer.content, encoding, start_pos, end_pos, module_type_params)
48+
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK
4949

5050
bytes.empty? ? nil : WASM::Deserializer.deserialize_node_list(bytes, buffer)
5151
end
5252

5353
def _lex(buffer, end_pos)
5454
encoding = buffer.content.encoding.name
55-
_success, bytes = WASM::Runtime.instance.lex(buffer.content, encoding, end_pos)
55+
_status, bytes = WASM::Runtime.instance.lex(buffer.content, encoding, end_pos)
5656

5757
WASM::Deserializer.deserialize_tokens(bytes, buffer)
5858
end
5959

6060
def _parse_inline_leading_annotation(buffer, start_pos, end_pos, variables)
61-
validate_position_range(start_pos, end_pos)
61+
validate_position_range(buffer, start_pos, end_pos)
6262
validate_variables(variables)
6363
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
64+
status, bytes = WASM::Runtime.instance.parse_inline_leading_annotation(buffer.content, encoding, start_pos, end_pos, variables)
65+
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK
6666

6767
deserialize_or_nil(bytes, buffer)
6868
end
6969

7070
def _parse_inline_trailing_annotation(buffer, start_pos, end_pos, variables)
71-
validate_position_range(start_pos, end_pos)
71+
validate_position_range(buffer, start_pos, end_pos)
7272
validate_variables(variables)
7373
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
74+
status, bytes = WASM::Runtime.instance.parse_inline_trailing_annotation(buffer.content, encoding, start_pos, end_pos, variables)
75+
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK
7676

7777
deserialize_or_nil(bytes, buffer)
7878
end
7979

8080
private
8181

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)
82+
# Reject the position ranges the parser cannot take, matching
83+
# validate_position_range in the C extension (main.c).
84+
#
85+
# `end_pos` past the end of the buffer is fine: clamping with a large
86+
# number instead of measuring the buffer is ordinary, and the lexer stops
87+
# at the end of the input on its own.
88+
def validate_position_range(buffer, start_pos, end_pos)
8689
if start_pos < 0 || end_pos < 0
8790
raise ArgumentError, "negative position range: #{start_pos}...#{end_pos}"
8891
end
8992
if start_pos > end_pos
9093
raise ArgumentError, "invalid position range: #{start_pos}...#{end_pos}"
9194
end
95+
96+
size = buffer.content.bytesize
97+
if start_pos > size
98+
raise ArgumentError, "position range starts past the end of the buffer: #{start_pos}...#{end_pos}, buffer is #{size} bytes"
99+
end
92100
end
93101

94102
# Reject anything that is not nil or an Array of Symbols, matching
@@ -112,6 +120,24 @@ def deserialize_or_nil(bytes, buffer)
112120
bytes.empty? ? nil : WASM::Deserializer.deserialize(bytes, buffer)
113121
end
114122

123+
# Raise for a status other than OK (see rbs_wasm.c).
124+
#
125+
# A negative status is about the range rather than the source text, so it
126+
# comes with an empty result and an ArgumentError, as in the C extension
127+
# (main.c). Starting past the end of the buffer is plain from the
128+
# buffer's size and rejected above, so a start position that comes back
129+
# rejected can only be one inside a character.
130+
def raise_parse_failure(buffer, status, bytes, start_pos, end_pos)
131+
case status
132+
when WASM::Runtime::INVALID_START_POS
133+
raise ArgumentError, "position range starts inside a character: #{start_pos}...#{end_pos}"
134+
when WASM::Runtime::INVALID_RANGE
135+
raise ArgumentError, "invalid position range: #{start_pos}...#{end_pos}"
136+
else
137+
raise_parsing_error(buffer, bytes)
138+
end
139+
end
140+
115141
# Decodes the error blob written by set_error_result (rbs_wasm.c) and raises
116142
# the same error the C extension would (see raise_error in main.c).
117143
def raise_parsing_error(buffer, blob)

lib/rbs/wasm/runtime.rb

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ module WASM
1616
class Runtime
1717
include MonitorMixin
1818

19+
# Statuses the parse entry points return (see rbs_wasm.c). A negative one
20+
# is about the range the caller asked for rather than the source text,
21+
# and comes with an empty result.
22+
INVALID_START_POS = -2
23+
INVALID_RANGE = -1
24+
PARSE_ERROR = 0
25+
OK = 1
26+
1927
class << self
2028
def instance
2129
@instance ||= new
@@ -48,9 +56,9 @@ def initialize
4856
end
4957

5058
# `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).
59+
# range within it to parse. Each method returns [status, bytes]: with OK
60+
# `bytes` is the serialized AST, with PARSE_ERROR it is the error blob (see
61+
# set_error_result in rbs_wasm.c), and with a negative status it is empty.
5462

5563
def parse_signature(content, encoding, start_pos, end_pos)
5664
run(content, encoding) { |ptr, len, enc_ptr, enc_len| @parse_signature.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos)[0] }
@@ -118,7 +126,7 @@ def run(source, encoding)
118126
@memory.write(source_ptr, bytes.to_java_bytes)
119127
@memory.write(name_ptr, name.to_java_bytes) unless name_length.zero?
120128
status = yield(source_ptr, length, name_ptr, name_length)
121-
[status == 1, read_result]
129+
[i32(status), read_result]
122130
ensure
123131
@free.apply(source_ptr)
124132
@free.apply(name_ptr)
@@ -156,6 +164,13 @@ def with_variables(variables)
156164
end
157165
end
158166

167+
# A WebAssembly i32 comes back in a JVM long, so read the low 32 bits as
168+
# signed: the negative statuses have to stay negative on this side.
169+
def i32(value)
170+
value &= 0xFFFF_FFFF
171+
value >= 0x8000_0000 ? value - 0x1_0000_0000 : value
172+
end
173+
159174
def bool(value)
160175
value ? 1 : 0
161176
end

wasm/README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,21 @@ Memory management and results:
8181

8282
Parsing — each takes the whole buffer (`ptr`/`len`), its Ruby encoding name
8383
(`enc`/`enc_len`, e.g. `"UTF-8"` or `"EUC-JP"`; falls back to UTF-8 when empty or
84-
unknown), and the character range to parse (`start`/`end`). Each returns `1` on
85-
success or `0` on a parse error. On success the result is the serialized AST; on
86-
error it is an error blob (start/end positions, syntax flag, token type,
87-
message). Type/method-type parsing also takes a buffer of newline-separated
84+
unknown), and the character range to parse (`start`/`end`). Each returns:
85+
86+
| Status | Meaning | Result |
87+
| --- | --- | --- |
88+
| `1` | Parsed. | The serialized AST. |
89+
| `0` | Parse error. | An error blob (start/end positions, syntax flag, token type, message). |
90+
| `-1` | Negative or reversed range. | Empty. |
91+
| `-2` | `start` is a byte position no character starts at — inside a character, or past the end of the buffer. | Empty. |
92+
93+
An `end` past the end of the buffer is not an error: it is clamped to the
94+
buffer, which is where lexing stops anyway. The two negative statuses are about
95+
the range the caller asked for rather than the source text, and `RBS::Parser`
96+
turns both into an `ArgumentError`, as the C extension does.
97+
98+
Type/method-type parsing also takes a buffer of newline-separated
8899
type-variable names (`vars`/`vars_len`, with `vars_len < 0` meaning "none"):
89100

90101
| Export | Signature |

0 commit comments

Comments
 (0)