ibex-runtime 0.2.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +337 -0
- data/lib/ibex/runtime/ast_data.rb +23 -0
- data/lib/ibex/runtime/cst/annotation.rb +16 -0
- data/lib/ibex/runtime/cst/cursor.rb +68 -0
- data/lib/ibex/runtime/cst/diff.rb +80 -0
- data/lib/ibex/runtime/cst/editing.rb +56 -0
- data/lib/ibex/runtime/cst/editor.rb +94 -0
- data/lib/ibex/runtime/cst/green/builder.rb +211 -0
- data/lib/ibex/runtime/cst/green/cache.rb +150 -0
- data/lib/ibex/runtime/cst/green/node.rb +78 -0
- data/lib/ibex/runtime/cst/green/token.rb +91 -0
- data/lib/ibex/runtime/cst/green/trivia.rb +36 -0
- data/lib/ibex/runtime/cst/incremental/blender.rb +233 -0
- data/lib/ibex/runtime/cst/incremental/lexed_syntax.rb +28 -0
- data/lib/ibex/runtime/cst/incremental/parse_memo.rb +95 -0
- data/lib/ibex/runtime/cst/incremental/relexer.rb +134 -0
- data/lib/ibex/runtime/cst/incremental/session.rb +229 -0
- data/lib/ibex/runtime/cst/incremental/token_memo.rb +66 -0
- data/lib/ibex/runtime/cst/kind.rb +133 -0
- data/lib/ibex/runtime/cst/parse_result.rb +41 -0
- data/lib/ibex/runtime/cst/rewriter.rb +75 -0
- data/lib/ibex/runtime/cst/serialize.rb +127 -0
- data/lib/ibex/runtime/cst/serialized_tree.rb +44 -0
- data/lib/ibex/runtime/cst/source_text.rb +112 -0
- data/lib/ibex/runtime/cst/syntax_node.rb +432 -0
- data/lib/ibex/runtime/cst/syntax_token.rb +160 -0
- data/lib/ibex/runtime/cst/text_edit.rb +77 -0
- data/lib/ibex/runtime/cst/typed_node.rb +87 -0
- data/lib/ibex/runtime/cst/validator.rb +428 -0
- data/lib/ibex/runtime/cst.rb +38 -0
- data/lib/ibex/runtime/event.rb +44 -0
- data/lib/ibex/runtime/event_jsonl_tracer.rb +37 -0
- data/lib/ibex/runtime/event_sanitizer.rb +273 -0
- data/lib/ibex/runtime/generated_lexer.rb +514 -0
- data/lib/ibex/runtime/jsonl_tracer.rb +75 -0
- data/lib/ibex/runtime/lexer_input.rb +107 -0
- data/lib/ibex/runtime/location_span.rb +114 -0
- data/lib/ibex/runtime/observation.rb +82 -0
- data/lib/ibex/runtime/parser.rb +3027 -0
- data/lib/ibex/runtime/parser_sync_recovery.rb +108 -0
- data/lib/ibex/runtime/repair.rb +126 -0
- data/lib/ibex/runtime/repair_priority_queue.rb +60 -0
- data/lib/ibex/runtime/repair_search.rb +318 -0
- data/lib/ibex/runtime/resource_limits.rb +64 -0
- data/lib/ibex/runtime/table_format.rb +11 -0
- data/lib/ibex/runtime/version.rb +7 -0
- data/lib/ibex/runtime.rb +27 -0
- data/lib/ibex/tables/compact.rb +186 -0
- data/lib/ibex/tables/compact_actions.rb +135 -0
- data/lib/ibex/tables/compact_productions.rb +167 -0
- data/sig/ibex/runtime/ast_data.rbs +12 -0
- data/sig/ibex/runtime/cst/annotation.rbs +13 -0
- data/sig/ibex/runtime/cst/cursor.rbs +34 -0
- data/sig/ibex/runtime/cst/diff.rbs +29 -0
- data/sig/ibex/runtime/cst/editing.rbs +23 -0
- data/sig/ibex/runtime/cst/editor.rbs +43 -0
- data/sig/ibex/runtime/cst/green/builder.rbs +84 -0
- data/sig/ibex/runtime/cst/green/cache.rbs +60 -0
- data/sig/ibex/runtime/cst/green/node.rbs +49 -0
- data/sig/ibex/runtime/cst/green/token.rbs +59 -0
- data/sig/ibex/runtime/cst/green/trivia.rbs +29 -0
- data/sig/ibex/runtime/cst/incremental/blender.rbs +114 -0
- data/sig/ibex/runtime/cst/incremental/lexed_syntax.rbs +20 -0
- data/sig/ibex/runtime/cst/incremental/parse_memo.rbs +59 -0
- data/sig/ibex/runtime/cst/incremental/relexer.rbs +54 -0
- data/sig/ibex/runtime/cst/incremental/session.rbs +89 -0
- data/sig/ibex/runtime/cst/incremental/token_memo.rbs +31 -0
- data/sig/ibex/runtime/cst/kind.rbs +93 -0
- data/sig/ibex/runtime/cst/parse_result.rbs +35 -0
- data/sig/ibex/runtime/cst/rewriter.rbs +31 -0
- data/sig/ibex/runtime/cst/serialize.rbs +39 -0
- data/sig/ibex/runtime/cst/serialized_tree.rbs +34 -0
- data/sig/ibex/runtime/cst/source_text.rbs +51 -0
- data/sig/ibex/runtime/cst/syntax_node.rbs +195 -0
- data/sig/ibex/runtime/cst/syntax_token.rbs +112 -0
- data/sig/ibex/runtime/cst/text_edit.rbs +33 -0
- data/sig/ibex/runtime/cst/typed_node.rbs +50 -0
- data/sig/ibex/runtime/cst/validator.rbs +108 -0
- data/sig/ibex/runtime/cst.rbs +9 -0
- data/sig/ibex/runtime/event.rbs +26 -0
- data/sig/ibex/runtime/event_jsonl_tracer.rbs +25 -0
- data/sig/ibex/runtime/event_sanitizer.rbs +92 -0
- data/sig/ibex/runtime/generated_lexer.rbs +169 -0
- data/sig/ibex/runtime/jsonl_tracer.rbs +42 -0
- data/sig/ibex/runtime/lexer_input.rbs +44 -0
- data/sig/ibex/runtime/location_span.rbs +64 -0
- data/sig/ibex/runtime/observation.rbs +35 -0
- data/sig/ibex/runtime/parser.rbs +926 -0
- data/sig/ibex/runtime/parser_sync_recovery.rbs +60 -0
- data/sig/ibex/runtime/repair.rbs +92 -0
- data/sig/ibex/runtime/repair_priority_queue.rbs +27 -0
- data/sig/ibex/runtime/repair_search.rbs +114 -0
- data/sig/ibex/runtime/resource_limits.rbs +39 -0
- data/sig/ibex/runtime/table_format.rbs +11 -0
- data/sig/ibex/runtime/version.rbs +7 -0
- data/sig/ibex/runtime.rbs +6 -0
- data/sig/ibex/tables/compact.rbs +70 -0
- data/sig/ibex/tables/compact_actions.rbs +57 -0
- data/sig/ibex/tables/compact_productions.rbs +68 -0
- metadata +144 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative "lexer_input" unless defined?(Ibex::Runtime::LexerInput)
|
|
5
|
+
|
|
6
|
+
module Ibex
|
|
7
|
+
module Runtime
|
|
8
|
+
# Runtime contract mixed into parser classes that declare a lexer.
|
|
9
|
+
# rubocop:disable Metrics/ModuleLength -- matching, actions, state, and positions share one session invariant.
|
|
10
|
+
# @rbs module-self Parser
|
|
11
|
+
module GeneratedLexer
|
|
12
|
+
NO_EMISSION = Object.new.freeze #: Object
|
|
13
|
+
empty_green_trivia = [] # @type var empty_green_trivia: Array[CST::GreenTrivia]
|
|
14
|
+
EMPTY_GREEN_TRIVIA = empty_green_trivia.freeze #: Array[CST::GreenTrivia]
|
|
15
|
+
|
|
16
|
+
# @rbs @lexer_input: LexerInput?
|
|
17
|
+
# @rbs @lexer_file: String
|
|
18
|
+
# @rbs @lexer_states: Array[String]
|
|
19
|
+
# @rbs @lexer_line: Integer
|
|
20
|
+
# @rbs @lexer_column: Integer
|
|
21
|
+
# @rbs @lexer_byte_column: Integer
|
|
22
|
+
# @rbs @lexer_byte_offset: Integer
|
|
23
|
+
# @rbs @lexer_lexeme: String?
|
|
24
|
+
# @rbs @lexer_emission: Object | Array[untyped]
|
|
25
|
+
# @rbs @lexer_skip_requested: bool
|
|
26
|
+
# @rbs @lexer_pending_green_trivia: Array[CST::GreenTrivia]
|
|
27
|
+
# @rbs @lexer_cst_trivia_policy: Symbol
|
|
28
|
+
# @rbs @lexer_cst_trivia_kinds: Hash[String, Integer]?
|
|
29
|
+
# @rbs @lexer_has_token: bool
|
|
30
|
+
|
|
31
|
+
# Reset the generated lexer to the beginning of an input.
|
|
32
|
+
# @rbs (String | IO | Fiber source, ?file: String) -> self
|
|
33
|
+
def lex(source, file: "(input)")
|
|
34
|
+
@lexer_input = LexerInput.new(source)
|
|
35
|
+
@lexer_file = file
|
|
36
|
+
@lexer_states = ["INITIAL"]
|
|
37
|
+
@lexer_line = 1
|
|
38
|
+
@lexer_column = 1
|
|
39
|
+
@lexer_byte_column = 1
|
|
40
|
+
@lexer_byte_offset = 0
|
|
41
|
+
@lexer_lexeme = nil
|
|
42
|
+
@lexer_emission = NO_EMISSION
|
|
43
|
+
@lexer_skip_requested = false
|
|
44
|
+
@lexer_pending_green_trivia = []
|
|
45
|
+
configure_lexer_cst
|
|
46
|
+
@lexer_has_token = false
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Lex and parse one String, IO, or Fiber source.
|
|
51
|
+
# @rbs (String | IO | Fiber source, ?file: String) -> untyped
|
|
52
|
+
def parse(source, file: "(input)")
|
|
53
|
+
lex(source, file: file)
|
|
54
|
+
parser = self #: Parser
|
|
55
|
+
parser.do_parse
|
|
56
|
+
rescue ParseError => e
|
|
57
|
+
raise unless parser_tables[:cst].is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
parser = self #: Parser
|
|
60
|
+
parser.__send__(:cst_lexical_failure, e)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Parse one generated-lexer source and return its semantic and syntax results.
|
|
64
|
+
# @rbs (String | IO | Fiber source, ?file: String) -> CST::ParseResult
|
|
65
|
+
def parse_with_syntax(source, file: "(input)")
|
|
66
|
+
lex(source, file: file)
|
|
67
|
+
parser = self #: Parser
|
|
68
|
+
value = parser.do_parse
|
|
69
|
+
parser.__send__(:syntax_parse_result, value)
|
|
70
|
+
rescue ParseError => e
|
|
71
|
+
raise unless parser_tables[:cst].is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
parser = self #: Parser
|
|
74
|
+
value = parser.__send__(:cst_lexical_failure, e)
|
|
75
|
+
parser.__send__(:syntax_parse_result, value)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parse without executing parser production actions.
|
|
79
|
+
# Lexer actions still run because they define tokenization and lexer state.
|
|
80
|
+
# @rbs (String source, ?file: String) -> CST::SyntaxResult
|
|
81
|
+
def parse_syntax(source, file: "(input)")
|
|
82
|
+
parse_syntax_with_cache(CST::SourceText.new(source, file: file), CST::NodeCache.new)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Return the current named lexer state.
|
|
86
|
+
# @rbs () -> Symbol
|
|
87
|
+
def lexer_state
|
|
88
|
+
(@lexer_states || ["INITIAL"]).last.to_sym
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Replace the current named lexer state.
|
|
92
|
+
# @rbs (Symbol | String state) -> Symbol
|
|
93
|
+
def lexer_state=(state)
|
|
94
|
+
name = validate_lexer_state(state)
|
|
95
|
+
states = @lexer_states ||= ["INITIAL"]
|
|
96
|
+
states[-1] = name
|
|
97
|
+
name.to_sym
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Pull one token using longest match and declaration-order tie breaking.
|
|
101
|
+
# @rbs () -> [untyped, untyped, Hash[Symbol, untyped]]
|
|
102
|
+
def next_token
|
|
103
|
+
input = @lexer_input
|
|
104
|
+
raise ParseError, "(lexer):1:1: call parse or lex before next_token" unless input
|
|
105
|
+
|
|
106
|
+
loop do
|
|
107
|
+
unless ensure_lexer_data?(input)
|
|
108
|
+
location = lexer_zero_width_location
|
|
109
|
+
location[:ibex_lexer_start_state] = lexer_state
|
|
110
|
+
location = attach_cst_trivia(location) if cst_enabled?
|
|
111
|
+
return [nil, nil, location.freeze]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
rule, lexeme = select_lexer_match(input)
|
|
115
|
+
raise_lexer_no_match(input) unless rule && lexeme
|
|
116
|
+
|
|
117
|
+
location = consume_lexer_match(input, lexeme)
|
|
118
|
+
location[:ibex_lexer_start_state] = lexer_state
|
|
119
|
+
emitted = apply_lexer_rule(rule, lexeme, location)
|
|
120
|
+
return emitted if emitted
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# @rbs (CST::SourceText source_text, CST::NodeCache cache) -> CST::SyntaxResult
|
|
127
|
+
def parse_syntax_with_cache(source_text, cache)
|
|
128
|
+
parser = self #: Parser
|
|
129
|
+
parser.__send__(:with_syntax_only, cache) do
|
|
130
|
+
lex(source_text.text, file: source_text.file || "(input)")
|
|
131
|
+
value = begin
|
|
132
|
+
parser.do_parse
|
|
133
|
+
rescue ParseError => e
|
|
134
|
+
raise unless parser_tables[:cst].is_a?(Hash)
|
|
135
|
+
|
|
136
|
+
parser.__send__(:cst_lexical_failure, e)
|
|
137
|
+
end
|
|
138
|
+
parsed = parser.__send__(:syntax_parse_result, value)
|
|
139
|
+
CST::SyntaxResult.new(
|
|
140
|
+
syntax_root: parsed.syntax_root,
|
|
141
|
+
diagnostics: parsed.diagnostics,
|
|
142
|
+
reused_ratio: 0.0
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Run the generated lexer once and materialize its exact Green token stream.
|
|
148
|
+
# @rbs (CST::SourceText source_text, CST::NodeCache cache) -> CST::LexedSyntax
|
|
149
|
+
def scan_syntax_with_cache(source_text, cache)
|
|
150
|
+
lex(source_text.text, file: source_text.file || "(input)")
|
|
151
|
+
raw_tokens = [] #: Array[Array[untyped]]
|
|
152
|
+
green_tokens = [] #: Array[CST::GreenToken]
|
|
153
|
+
states = [] #: Array[Symbol]
|
|
154
|
+
loop do
|
|
155
|
+
external, value, location = next_token
|
|
156
|
+
replace_previous_scanned_trailing(green_tokens, location, cache)
|
|
157
|
+
token_id = external.nil? || external == false ? Parser::EOF_TOKEN : __send__(:internal_token_id, external)
|
|
158
|
+
green = scanned_green_token(token_id, value, location, cache)
|
|
159
|
+
raw_tokens << [external, value, location]
|
|
160
|
+
green_tokens << green
|
|
161
|
+
state = location[:ibex_lexer_start_state] if location.is_a?(Hash)
|
|
162
|
+
states << (state ? state.to_sym : :INITIAL)
|
|
163
|
+
break if token_id == Parser::EOF_TOKEN
|
|
164
|
+
end
|
|
165
|
+
offsets = [] #: Array[Integer]
|
|
166
|
+
offset = 0
|
|
167
|
+
green_tokens.each do |token|
|
|
168
|
+
offsets << offset
|
|
169
|
+
offset += token.full_width
|
|
170
|
+
end
|
|
171
|
+
memo = CST::TokenMemo.new(tokens: green_tokens, offsets: offsets, states: states)
|
|
172
|
+
CST::LexedSyntax.new(raw_tokens: raw_tokens, memo: memo)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @rbs (Integer token_id, untyped value, untyped location, CST::NodeCache cache) -> CST::GreenToken
|
|
176
|
+
def scanned_green_token(token_id, value, location, cache)
|
|
177
|
+
leading = scanned_green_trivia(location, :leading_trivia)
|
|
178
|
+
text = if location.is_a?(Hash) && location[:ibex_cst_text].is_a?(String)
|
|
179
|
+
location.fetch(:ibex_cst_text)
|
|
180
|
+
elsif value.is_a?(String)
|
|
181
|
+
value
|
|
182
|
+
else
|
|
183
|
+
""
|
|
184
|
+
end
|
|
185
|
+
text = "" if token_id == Parser::EOF_TOKEN
|
|
186
|
+
cache.intern_token_fields(kind: token_id, text: text, leading: leading)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @rbs (Array[CST::GreenToken] tokens, untyped location, CST::NodeCache cache) -> void
|
|
190
|
+
def replace_previous_scanned_trailing(tokens, location, cache)
|
|
191
|
+
trailing = scanned_green_trivia(location, :cst_previous_trailing)
|
|
192
|
+
previous = tokens.last
|
|
193
|
+
return if trailing.empty? || !previous
|
|
194
|
+
|
|
195
|
+
tokens[-1] = cache.intern_token_fields(
|
|
196
|
+
kind: previous.kind,
|
|
197
|
+
text: previous.text,
|
|
198
|
+
leading: previous.leading,
|
|
199
|
+
trailing: previous.trailing + trailing,
|
|
200
|
+
flags: previous.flags,
|
|
201
|
+
expected_kind: previous.expected_kind
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# @rbs (untyped location, Symbol key) -> Array[CST::GreenTrivia]
|
|
206
|
+
def scanned_green_trivia(location, key)
|
|
207
|
+
return [] unless location.is_a?(Hash)
|
|
208
|
+
|
|
209
|
+
value = location[key]
|
|
210
|
+
value.is_a?(Array) ? value.grep(CST::GreenTrivia) : []
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @rbs (LexerInput input) -> bool
|
|
214
|
+
def ensure_lexer_data?(input)
|
|
215
|
+
input.read_more? while input.buffer.empty? && !input.eof?
|
|
216
|
+
!input.buffer.empty?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @rbs (LexerInput input) -> [Hash[Symbol, untyped]?, String?]
|
|
220
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
221
|
+
def select_lexer_match(input)
|
|
222
|
+
loop do
|
|
223
|
+
matches = lexer_rules.filter_map do |rule|
|
|
224
|
+
match = rule.fetch(:regexp).match(input.buffer)
|
|
225
|
+
[rule, match[0]] if match&.begin(0)&.zero?
|
|
226
|
+
end #: Array[[Hash[Symbol, untyped], String]]
|
|
227
|
+
boundary_match = matches.any? { |_rule, lexeme| lexeme.length == input.buffer.length }
|
|
228
|
+
if !input.eof? && (matches.empty? || boundary_match)
|
|
229
|
+
input.read_more?
|
|
230
|
+
next
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
selected = matches.max_by { |rule, lexeme| [lexeme.bytesize, -rule.fetch(:id)] }
|
|
234
|
+
return [nil, nil] unless selected
|
|
235
|
+
|
|
236
|
+
return selected
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
240
|
+
|
|
241
|
+
# @rbs () -> Array[Hash[Symbol, untyped]]
|
|
242
|
+
def lexer_rules
|
|
243
|
+
tables = self.class.const_get(:LEXER_RULES_BY_STATE)
|
|
244
|
+
rules = tables[lexer_state]
|
|
245
|
+
return rules if rules
|
|
246
|
+
|
|
247
|
+
raise ParseError, "(lexer):1:1: missing rules for lexer state #{lexer_state}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @rbs (LexerInput input) -> bot
|
|
251
|
+
def raise_lexer_no_match(input)
|
|
252
|
+
location = lexer_zero_width_location
|
|
253
|
+
location[:ibex_cst_unmatched_text] = input.buffer.dup.freeze
|
|
254
|
+
location.freeze
|
|
255
|
+
excerpt = input.buffer.slice(0, 16)
|
|
256
|
+
raise ParseError.new(
|
|
257
|
+
token_name: "lexer input", token_value: excerpt, location: location,
|
|
258
|
+
detail: "no lexer rule matches #{excerpt.inspect} in state #{lexer_state}"
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# @rbs (LexerInput input, String lexeme) -> Hash[Symbol, untyped]
|
|
263
|
+
def consume_lexer_match(input, lexeme)
|
|
264
|
+
start = lexer_position
|
|
265
|
+
input.consume(lexeme)
|
|
266
|
+
advance_lexer_position(lexeme)
|
|
267
|
+
finish = lexer_position
|
|
268
|
+
{
|
|
269
|
+
file: @lexer_file, line: start.fetch(:line), column: start.fetch(:column),
|
|
270
|
+
grapheme_column: start.fetch(:column), byte_column: start.fetch(:byte_column),
|
|
271
|
+
end_line: finish.fetch(:line), end_column: finish.fetch(:column),
|
|
272
|
+
end_grapheme_column: finish.fetch(:column), end_byte_column: finish.fetch(:byte_column),
|
|
273
|
+
start_byte: start.fetch(:byte), end_byte: finish.fetch(:byte),
|
|
274
|
+
source_line: input.source_line(start.fetch(:line))
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# @rbs (Hash[Symbol, untyped] rule, String lexeme, Hash[Symbol, untyped] location) ->
|
|
279
|
+
# [untyped, untyped, Hash[Symbol, untyped]]?
|
|
280
|
+
def apply_lexer_rule(rule, lexeme, location)
|
|
281
|
+
@lexer_lexeme = lexeme
|
|
282
|
+
@lexer_emission = NO_EMISSION
|
|
283
|
+
@lexer_skip_requested = rule.fetch(:kind) == :skip
|
|
284
|
+
action = rule[:action]
|
|
285
|
+
value = action ? __send__(action, lexeme) : lexeme
|
|
286
|
+
emission = @lexer_emission
|
|
287
|
+
if emission.is_a?(Array)
|
|
288
|
+
location[:ibex_cst_text] = lexeme
|
|
289
|
+
attached = attach_cst_trivia(location)
|
|
290
|
+
@lexer_has_token = true
|
|
291
|
+
return [emission.fetch(0), emission.fetch(1), attached.freeze]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if @lexer_skip_requested
|
|
295
|
+
retain_cst_trivia(lexeme, location.freeze)
|
|
296
|
+
return nil
|
|
297
|
+
end
|
|
298
|
+
if rule.fetch(:kind) == :token
|
|
299
|
+
location[:ibex_cst_text] = lexeme
|
|
300
|
+
attached = attach_cst_trivia(location)
|
|
301
|
+
@lexer_has_token = true
|
|
302
|
+
return [rule.fetch(:token), value, attached.freeze]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
location.freeze
|
|
306
|
+
raise ParseError, "#{location.fetch(:file)}:#{location.fetch(:line)}:" \
|
|
307
|
+
"#{location.fetch(:column)}: on lexer rule did not emit or skip"
|
|
308
|
+
ensure
|
|
309
|
+
@lexer_lexeme = nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Emit a token from a lexer action.
|
|
313
|
+
# @rbs (untyped token, ?untyped value) -> untyped
|
|
314
|
+
def emit(token, value = NO_EMISSION)
|
|
315
|
+
actual = value.equal?(NO_EMISSION) ? @lexer_lexeme : value
|
|
316
|
+
@lexer_emission = [token, actual]
|
|
317
|
+
actual
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Suppress the current match from a lexer action.
|
|
321
|
+
# @rbs () -> nil
|
|
322
|
+
def skip
|
|
323
|
+
@lexer_skip_requested = true
|
|
324
|
+
nil
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Return the text consumed by the active lexer action.
|
|
328
|
+
# @rbs () -> String?
|
|
329
|
+
def lexeme
|
|
330
|
+
@lexer_lexeme
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# @rbs (String text, Hash[Symbol, untyped] location) -> void
|
|
334
|
+
def retain_cst_trivia(text, _location)
|
|
335
|
+
return if cst_trivia_policy == :drop
|
|
336
|
+
|
|
337
|
+
kind = cst_trivia_kind(text)
|
|
338
|
+
cache = @green_cache
|
|
339
|
+
value = if cache
|
|
340
|
+
cache.intern_trivia_fields(kind: kind, text: text)
|
|
341
|
+
else
|
|
342
|
+
CST::GreenTrivia.new(kind: kind, text: text)
|
|
343
|
+
end
|
|
344
|
+
@lexer_pending_green_trivia << value
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# @rbs (Hash[Symbol, untyped] location) -> Hash[Symbol, untyped]
|
|
348
|
+
def attach_cst_trivia(location)
|
|
349
|
+
policy = cst_trivia_policy
|
|
350
|
+
trivia = @lexer_pending_green_trivia
|
|
351
|
+
return location if trivia.empty? || policy == :drop
|
|
352
|
+
|
|
353
|
+
@lexer_pending_green_trivia = []
|
|
354
|
+
if policy == :balanced && @lexer_has_token
|
|
355
|
+
trailing, leading = split_balanced_trivia(trivia)
|
|
356
|
+
else
|
|
357
|
+
leading = trivia.freeze
|
|
358
|
+
trailing = EMPTY_GREEN_TRIVIA
|
|
359
|
+
end
|
|
360
|
+
location[:leading_trivia] = leading.freeze
|
|
361
|
+
location[:cst_previous_trailing] = trailing.freeze
|
|
362
|
+
location
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# @rbs () -> Symbol
|
|
366
|
+
def cst_trivia_policy
|
|
367
|
+
configure_lexer_cst unless defined?(@lexer_cst_trivia_policy)
|
|
368
|
+
@lexer_cst_trivia_policy
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# @rbs (String text) -> Integer
|
|
372
|
+
def cst_trivia_kind(text)
|
|
373
|
+
kinds = @lexer_cst_trivia_kinds || raise(ParseError, "(lexer):1:1: CST trivia kinds are unavailable")
|
|
374
|
+
name = if text.include?("\n")
|
|
375
|
+
"newline"
|
|
376
|
+
elsif text.match?(/\A[[:space:]]+\z/)
|
|
377
|
+
"whitespace"
|
|
378
|
+
elsif text.start_with?("//", "#")
|
|
379
|
+
"line_comment"
|
|
380
|
+
elsif text.start_with?("/*")
|
|
381
|
+
"block_comment"
|
|
382
|
+
else
|
|
383
|
+
"custom_skip"
|
|
384
|
+
end
|
|
385
|
+
kinds.fetch(name)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# @rbs () -> void
|
|
389
|
+
def configure_lexer_cst
|
|
390
|
+
tables = parser_tables
|
|
391
|
+
config = tables[:cst]
|
|
392
|
+
if config.is_a?(Hash)
|
|
393
|
+
@lexer_cst_trivia_policy = config.fetch(:trivia_policy)
|
|
394
|
+
@lexer_cst_trivia_kinds = config.fetch(:kinds).fetch(:trivia)
|
|
395
|
+
return
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
@lexer_cst_trivia_policy = :drop
|
|
399
|
+
@lexer_cst_trivia_kinds = nil
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# @rbs (Array[CST::GreenTrivia] trivia) -> [Array[CST::GreenTrivia], Array[CST::GreenTrivia]]
|
|
403
|
+
def split_balanced_trivia(trivia)
|
|
404
|
+
return [trivia.freeze, EMPTY_GREEN_TRIVIA] unless trivia.any? { |item| item.text.include?("\n".b) }
|
|
405
|
+
|
|
406
|
+
before = [] #: Array[CST::GreenTrivia]
|
|
407
|
+
after = [] #: Array[CST::GreenTrivia]
|
|
408
|
+
found = false
|
|
409
|
+
trivia.each do |item|
|
|
410
|
+
if found
|
|
411
|
+
after << item
|
|
412
|
+
next
|
|
413
|
+
end
|
|
414
|
+
newline = item.text.index("\n".b)
|
|
415
|
+
unless newline
|
|
416
|
+
before << item
|
|
417
|
+
next
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
boundary = newline + 1
|
|
421
|
+
before_text = item.text.byteslice(0, boundary) || "".b
|
|
422
|
+
after_text = item.text.byteslice(boundary, item.text.bytesize - boundary) || "".b
|
|
423
|
+
before << CST::GreenTrivia.new(kind: cst_trivia_kind(before_text), text: before_text)
|
|
424
|
+
after << CST::GreenTrivia.new(kind: cst_trivia_kind(after_text), text: after_text) unless after_text.empty?
|
|
425
|
+
found = true
|
|
426
|
+
end
|
|
427
|
+
[before, after]
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# @rbs () -> CST::SourceText
|
|
431
|
+
def cst_source_text
|
|
432
|
+
input = @lexer_input || raise(ParseError, "(lexer):1:1: lexer input is unavailable")
|
|
433
|
+
CST::SourceText.new(input.source_bytes.byteslice(0, @lexer_byte_offset || 0) || "".b, file: @lexer_file)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# @rbs () -> Array[CST::GreenTrivia]
|
|
437
|
+
def take_cst_pending_green_trivia
|
|
438
|
+
values = @lexer_pending_green_trivia
|
|
439
|
+
@lexer_pending_green_trivia = []
|
|
440
|
+
values.freeze
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# @rbs (Array[CST::GreenTrivia] values) -> Array[CST::GreenTrivia]
|
|
444
|
+
def green_trivia_values(values)
|
|
445
|
+
result = [] #: Array[CST::GreenTrivia]
|
|
446
|
+
values.each { |item| result << item if item.is_a?(CST::GreenTrivia) }
|
|
447
|
+
result
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Push a named lexer state.
|
|
451
|
+
# @rbs (Symbol | String state) -> Symbol
|
|
452
|
+
def push_state(state)
|
|
453
|
+
name = validate_lexer_state(state)
|
|
454
|
+
(@lexer_states ||= ["INITIAL"]) << name
|
|
455
|
+
name.to_sym
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Pop the current named lexer state.
|
|
459
|
+
# @rbs () -> Symbol
|
|
460
|
+
def pop_state
|
|
461
|
+
states = @lexer_states ||= ["INITIAL"]
|
|
462
|
+
raise ParseError, "(lexer):1:1: cannot pop the INITIAL lexer state" if states.one?
|
|
463
|
+
|
|
464
|
+
(states.pop || "INITIAL").to_sym
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# @rbs (Symbol | String state) -> String
|
|
468
|
+
def validate_lexer_state(state)
|
|
469
|
+
name = state.to_s
|
|
470
|
+
known = self.class.const_get(:LEXER_STATES)
|
|
471
|
+
raise ArgumentError, "unknown lexer state #{state.inspect}" unless known.include?(name)
|
|
472
|
+
|
|
473
|
+
name
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# @rbs () -> Hash[Symbol, Integer]
|
|
477
|
+
def lexer_position
|
|
478
|
+
{
|
|
479
|
+
line: @lexer_line || 1, column: @lexer_column || 1,
|
|
480
|
+
byte_column: @lexer_byte_column || 1, byte: @lexer_byte_offset || 0
|
|
481
|
+
}
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
485
|
+
def lexer_zero_width_location
|
|
486
|
+
position = lexer_position
|
|
487
|
+
{
|
|
488
|
+
file: @lexer_file || "(input)", line: position.fetch(:line), column: position.fetch(:column),
|
|
489
|
+
grapheme_column: position.fetch(:column), byte_column: position.fetch(:byte_column),
|
|
490
|
+
end_line: position.fetch(:line), end_column: position.fetch(:column),
|
|
491
|
+
end_grapheme_column: position.fetch(:column), end_byte_column: position.fetch(:byte_column),
|
|
492
|
+
start_byte: position.fetch(:byte), end_byte: position.fetch(:byte)
|
|
493
|
+
}
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# @rbs (String lexeme) -> void
|
|
497
|
+
def advance_lexer_position(lexeme)
|
|
498
|
+
lexeme.scan(/\X/).each do |value|
|
|
499
|
+
grapheme = value.is_a?(String) ? value : value.join
|
|
500
|
+
@lexer_byte_offset += grapheme.bytesize
|
|
501
|
+
if grapheme.include?("\n")
|
|
502
|
+
@lexer_line += 1
|
|
503
|
+
@lexer_column = 1
|
|
504
|
+
@lexer_byte_column = 1
|
|
505
|
+
else
|
|
506
|
+
@lexer_column += 1
|
|
507
|
+
@lexer_byte_column += grapheme.bytesize
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
# rubocop:enable Metrics/ModuleLength
|
|
513
|
+
end
|
|
514
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Ibex
|
|
7
|
+
module Runtime
|
|
8
|
+
# Emits committed parser observer events as JSON Lines.
|
|
9
|
+
module JSONLTracer
|
|
10
|
+
# @rbs!
|
|
11
|
+
# interface _TraceOutput
|
|
12
|
+
# def puts: (String) -> untyped
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
# Hook layer installed on an individual parser's singleton class.
|
|
16
|
+
module Hooks
|
|
17
|
+
# @rbs!
|
|
18
|
+
# def token_to_str: (Integer) -> String
|
|
19
|
+
|
|
20
|
+
# @rbs @ibex_jsonl_trace_output: _TraceOutput?
|
|
21
|
+
|
|
22
|
+
# @rbs (Integer token_id, untyped value, Integer state) -> void
|
|
23
|
+
def on_shift(token_id, value, state)
|
|
24
|
+
write_trace(event: "shift", token_id: token_id, token: token_to_str(token_id),
|
|
25
|
+
value: safe_inspect(value), state: state)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @rbs (Integer production_id, Array[untyped] values, untyped result) -> void
|
|
30
|
+
def on_reduce(production_id, values, result)
|
|
31
|
+
write_trace(event: "reduce", production_id: production_id, values: values.map { |value| safe_inspect(value) },
|
|
32
|
+
result: safe_inspect(result))
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs (Integer token_id, untyped value, Array[untyped] value_stack) -> void
|
|
37
|
+
def on_error_recover(token_id, value, value_stack)
|
|
38
|
+
write_trace(event: "error_recover", token_id: token_id, token: token_to_str(token_id),
|
|
39
|
+
value: safe_inspect(value), value_stack: value_stack.map { |item| safe_inspect(item) })
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# @rbs (_TraceOutput output) -> void
|
|
46
|
+
def ibex_jsonl_trace_output=(output)
|
|
47
|
+
@ibex_jsonl_trace_output = output
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @rbs (**untyped event) -> void
|
|
51
|
+
def write_trace(**event)
|
|
52
|
+
@ibex_jsonl_trace_output&.puts(JSON.generate(event))
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @rbs (untyped value) -> String
|
|
58
|
+
def safe_inspect(value)
|
|
59
|
+
value.inspect.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
"<inspect failed>"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @rbs [P < Parser] (P parser, io: _TraceOutput) -> P
|
|
66
|
+
def self.attach(parser, io:)
|
|
67
|
+
singleton = parser.singleton_class
|
|
68
|
+
singleton.prepend(Hooks) unless singleton.ancestors.include?(Hooks)
|
|
69
|
+
Object.instance_method(:instance_variable_set).bind_call(parser, :@runtime_fast_path, false)
|
|
70
|
+
parser.__send__(:ibex_jsonl_trace_output=, io)
|
|
71
|
+
parser
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ibex
|
|
5
|
+
module Runtime
|
|
6
|
+
# Incremental String, IO, or Fiber input used by generated lexers.
|
|
7
|
+
class LexerInput
|
|
8
|
+
DEFAULT_CHUNK_SIZE = 16 * 1024 #: Integer
|
|
9
|
+
|
|
10
|
+
attr_reader :buffer #: String
|
|
11
|
+
|
|
12
|
+
# @rbs (String | IO | Fiber source, ?chunk_size: Integer) -> void
|
|
13
|
+
def initialize(source, chunk_size: DEFAULT_CHUNK_SIZE)
|
|
14
|
+
raise ArgumentError, "lexer chunk_size must be positive" unless chunk_size.positive?
|
|
15
|
+
unless source.is_a?(String) || source.respond_to?(:read) || source.is_a?(Fiber)
|
|
16
|
+
raise ArgumentError, "lexer input must be a String, IO, or Fiber"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@source = source
|
|
20
|
+
@chunk_size = chunk_size
|
|
21
|
+
@buffer = +""
|
|
22
|
+
@archive = +""
|
|
23
|
+
@eof = false
|
|
24
|
+
return unless source.is_a?(String)
|
|
25
|
+
|
|
26
|
+
append(source)
|
|
27
|
+
@eof = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs () -> bool
|
|
31
|
+
def eof? = @eof
|
|
32
|
+
|
|
33
|
+
# Return every source byte read so far, including the unconsumed buffer.
|
|
34
|
+
# @rbs () -> String
|
|
35
|
+
def source_bytes = @archive.dup
|
|
36
|
+
|
|
37
|
+
# Read until at least one byte is added or EOF is observed.
|
|
38
|
+
# @rbs () -> bool
|
|
39
|
+
def read_more?
|
|
40
|
+
before = @buffer.bytesize
|
|
41
|
+
read_chunk until @eof || @buffer.bytesize > before
|
|
42
|
+
@buffer.bytesize > before
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs (String prefix) -> void
|
|
46
|
+
def consume(prefix)
|
|
47
|
+
raise ArgumentError, "lexer input prefix does not match the buffer" unless @buffer.start_with?(prefix)
|
|
48
|
+
|
|
49
|
+
@buffer.slice!(0, prefix.length)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return one source line, reading ahead only to its line boundary.
|
|
53
|
+
# @rbs (Integer line) -> String
|
|
54
|
+
def source_line(line)
|
|
55
|
+
raise ArgumentError, "source line must be positive" unless line.positive?
|
|
56
|
+
|
|
57
|
+
read_more? while !@eof && @archive.count("\n") < line
|
|
58
|
+
(@archive.lines[line - 1] || "").delete_suffix("\n").delete_suffix("\r")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# @rbs () -> void
|
|
64
|
+
def read_chunk
|
|
65
|
+
source = @source
|
|
66
|
+
chunk = if source.is_a?(Fiber)
|
|
67
|
+
resume_fiber(source)
|
|
68
|
+
elsif source.respond_to?(:read)
|
|
69
|
+
source.read(@chunk_size)
|
|
70
|
+
end
|
|
71
|
+
if chunk.nil? || chunk == ""
|
|
72
|
+
@eof = true
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
raise TypeError, "lexer input chunks must be Strings" unless chunk.is_a?(String)
|
|
76
|
+
|
|
77
|
+
text = chunk #: String
|
|
78
|
+
append(text)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @rbs (Fiber source) -> untyped
|
|
82
|
+
def resume_fiber(source)
|
|
83
|
+
supports_alive = source.respond_to?(:alive?)
|
|
84
|
+
if supports_alive && !source.alive?
|
|
85
|
+
@eof = true
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
value = source.resume
|
|
90
|
+
@eof = true if supports_alive && !source.alive?
|
|
91
|
+
value
|
|
92
|
+
rescue FiberError => e
|
|
93
|
+
terminated = /\A(?:dead fiber called|attempt to resume a terminated fiber)\z/.match?(e.message)
|
|
94
|
+
raise unless !supports_alive && terminated
|
|
95
|
+
|
|
96
|
+
@eof = true
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @rbs (String chunk) -> void
|
|
101
|
+
def append(chunk)
|
|
102
|
+
@buffer << chunk
|
|
103
|
+
@archive << chunk
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|