Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 49 additions & 23 deletions lib/rbs/wasm/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,82 +13,90 @@ module RBS
class Parser
class << self
def _parse_signature(buffer, start_pos, end_pos)
validate_position_range(start_pos, end_pos)
validate_position_range(buffer, start_pos, end_pos)
encoding = buffer.content.encoding.name
success, bytes = WASM::Runtime.instance.parse_signature(buffer.content, encoding, start_pos, end_pos)
raise_parsing_error(buffer, bytes) unless success
status, bytes = WASM::Runtime.instance.parse_signature(buffer.content, encoding, start_pos, end_pos)
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK

WASM::Deserializer.deserialize(bytes, buffer)
end

def _parse_type(buffer, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed)
validate_position_range(start_pos, end_pos)
validate_position_range(buffer, start_pos, end_pos)
validate_variables(variables)
encoding = buffer.content.encoding.name
success, bytes = WASM::Runtime.instance.parse_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed)
raise_parsing_error(buffer, bytes) unless success
status, bytes = WASM::Runtime.instance.parse_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed)
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK

deserialize_or_nil(bytes, buffer)
end

def _parse_method_type(buffer, start_pos, end_pos, variables, require_eof)
validate_position_range(start_pos, end_pos)
validate_position_range(buffer, start_pos, end_pos)
validate_variables(variables)
encoding = buffer.content.encoding.name
success, bytes = WASM::Runtime.instance.parse_method_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof)
raise_parsing_error(buffer, bytes) unless success
status, bytes = WASM::Runtime.instance.parse_method_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof)
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK

deserialize_or_nil(bytes, buffer)
end

def _parse_type_params(buffer, start_pos, end_pos, module_type_params)
validate_position_range(start_pos, end_pos)
validate_position_range(buffer, start_pos, end_pos)
encoding = buffer.content.encoding.name
success, bytes = WASM::Runtime.instance.parse_type_params(buffer.content, encoding, start_pos, end_pos, module_type_params)
raise_parsing_error(buffer, bytes) unless success
status, bytes = WASM::Runtime.instance.parse_type_params(buffer.content, encoding, start_pos, end_pos, module_type_params)
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK

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

def _lex(buffer, end_pos)
encoding = buffer.content.encoding.name
_success, bytes = WASM::Runtime.instance.lex(buffer.content, encoding, end_pos)
_status, bytes = WASM::Runtime.instance.lex(buffer.content, encoding, end_pos)

WASM::Deserializer.deserialize_tokens(bytes, buffer)
end

def _parse_inline_leading_annotation(buffer, start_pos, end_pos, variables)
validate_position_range(start_pos, end_pos)
validate_position_range(buffer, start_pos, end_pos)
validate_variables(variables)
encoding = buffer.content.encoding.name
success, bytes = WASM::Runtime.instance.parse_inline_leading_annotation(buffer.content, encoding, start_pos, end_pos, variables)
raise_parsing_error(buffer, bytes) unless success
status, bytes = WASM::Runtime.instance.parse_inline_leading_annotation(buffer.content, encoding, start_pos, end_pos, variables)
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK

deserialize_or_nil(bytes, buffer)
end

def _parse_inline_trailing_annotation(buffer, start_pos, end_pos, variables)
validate_position_range(start_pos, end_pos)
validate_position_range(buffer, start_pos, end_pos)
validate_variables(variables)
encoding = buffer.content.encoding.name
success, bytes = WASM::Runtime.instance.parse_inline_trailing_annotation(buffer.content, encoding, start_pos, end_pos, variables)
raise_parsing_error(buffer, bytes) unless success
status, bytes = WASM::Runtime.instance.parse_inline_trailing_annotation(buffer.content, encoding, start_pos, end_pos, variables)
raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK

deserialize_or_nil(bytes, buffer)
end

private

# Reject negative or reversed ranges before handing them to the parser,
# matching validate_position_range in the C extension (main.c). A reversed
# range would otherwise make the lexer loop forever inside WebAssembly.
def validate_position_range(start_pos, end_pos)
# Reject the position ranges the parser cannot take, matching
# validate_position_range in the C extension (main.c).
#
# `end_pos` past the end of the buffer is fine: clamping with a large
# number instead of measuring the buffer is ordinary, and the lexer stops
# at the end of the input on its own.
def validate_position_range(buffer, start_pos, end_pos)
if start_pos < 0 || end_pos < 0
raise ArgumentError, "negative position range: #{start_pos}...#{end_pos}"
end
if start_pos > end_pos
raise ArgumentError, "invalid position range: #{start_pos}...#{end_pos}"
end

size = buffer.content.bytesize
if start_pos > size
raise ArgumentError, "position range starts past the end of the buffer: #{start_pos}...#{end_pos}, buffer is #{size} bytes"
end
end

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

# Raise for a status other than OK (see rbs_wasm.c).
#
# A negative status is about the range rather than the source text, so it
# comes with an empty result and an ArgumentError, as in the C extension
# (main.c). Starting past the end of the buffer is plain from the
# buffer's size and rejected above, so a start position that comes back
# rejected can only be one inside a character.
def raise_parse_failure(buffer, status, bytes, start_pos, end_pos)
case status
when WASM::Runtime::INVALID_START_POS
raise ArgumentError, "position range starts inside a character: #{start_pos}...#{end_pos}"
when WASM::Runtime::INVALID_RANGE
raise ArgumentError, "invalid position range: #{start_pos}...#{end_pos}"
else
raise_parsing_error(buffer, bytes)
end
end

# Decodes the error blob written by set_error_result (rbs_wasm.c) and raises
# the same error the C extension would (see raise_error in main.c).
def raise_parsing_error(buffer, blob)
Expand Down
23 changes: 19 additions & 4 deletions lib/rbs/wasm/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ module WASM
class Runtime
include MonitorMixin

# Statuses the parse entry points return (see rbs_wasm.c). A negative one
# is about the range the caller asked for rather than the source text,
# and comes with an empty result.
INVALID_START_POS = -2
INVALID_RANGE = -1
PARSE_ERROR = 0
OK = 1

class << self
def instance
@instance ||= new
Expand Down Expand Up @@ -48,9 +56,9 @@ def initialize
end

# `content` is the whole buffer; `start_pos`/`end_pos` are the character
# range within it to parse. Each method returns [success, bytes]: on success
# `bytes` is the serialized AST, otherwise it is the error blob (see
# set_error_result in rbs_wasm.c).
# range within it to parse. Each method returns [status, bytes]: with OK
# `bytes` is the serialized AST, with PARSE_ERROR it is the error blob (see
# set_error_result in rbs_wasm.c), and with a negative status it is empty.

def parse_signature(content, encoding, start_pos, end_pos)
run(content, encoding) { |ptr, len, enc_ptr, enc_len| @parse_signature.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos)[0] }
Expand Down Expand Up @@ -118,7 +126,7 @@ def run(source, encoding)
@memory.write(source_ptr, bytes.to_java_bytes)
@memory.write(name_ptr, name.to_java_bytes) unless name_length.zero?
status = yield(source_ptr, length, name_ptr, name_length)
[status == 1, read_result]
[i32(status), read_result]
ensure
@free.apply(source_ptr)
@free.apply(name_ptr)
Expand Down Expand Up @@ -156,6 +164,13 @@ def with_variables(variables)
end
end

# A WebAssembly i32 comes back in a JVM long, so read the low 32 bits as
# signed: the negative statuses have to stay negative on this side.
def i32(value)
value &= 0xFFFF_FFFF
value >= 0x8000_0000 ? value - 0x1_0000_0000 : value
end

def bool(value)
value ? 1 : 0
end
Expand Down
19 changes: 15 additions & 4 deletions wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,21 @@ Memory management and results:

Parsing — each takes the whole buffer (`ptr`/`len`), its Ruby encoding name
(`enc`/`enc_len`, e.g. `"UTF-8"` or `"EUC-JP"`; falls back to UTF-8 when empty or
unknown), and the character range to parse (`start`/`end`). Each returns `1` on
success or `0` on a parse error. On success the result is the serialized AST; on
error it is an error blob (start/end positions, syntax flag, token type,
message). Type/method-type parsing also takes a buffer of newline-separated
unknown), and the character range to parse (`start`/`end`). Each returns:

| Status | Meaning | Result |
| --- | --- | --- |
| `1` | Parsed. | The serialized AST. |
| `0` | Parse error. | An error blob (start/end positions, syntax flag, token type, message). |
| `-1` | Negative or reversed range. | Empty. |
| `-2` | `start` is a byte position no character starts at — inside a character, or past the end of the buffer. | Empty. |

An `end` past the end of the buffer is not an error: it is clamped to the
buffer, which is where lexing stops anyway. The two negative statuses are about
the range the caller asked for rather than the source text, and `RBS::Parser`
turns both into an `ArgumentError`, as the C extension does.

Type/method-type parsing also takes a buffer of newline-separated
type-variable names (`vars`/`vars_len`, with `vars_len < 0` meaning "none"):

| Export | Signature |
Expand Down
Loading
Loading