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,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ibex
|
|
5
|
+
module Runtime
|
|
6
|
+
module CST
|
|
7
|
+
# Raised when incremental parsing cannot preserve its syntax-only contract.
|
|
8
|
+
class IncrementalUnsupportedError < ArgumentError; end
|
|
9
|
+
|
|
10
|
+
# A mutable, single-owner session around immutable source and syntax results.
|
|
11
|
+
class IncrementalParseSession
|
|
12
|
+
attr_reader :source_text #: SourceText
|
|
13
|
+
attr_reader :result #: SyntaxResult
|
|
14
|
+
attr_reader :token_memo #: TokenMemo
|
|
15
|
+
attr_reader :parse_memo #: ParseMemo
|
|
16
|
+
attr_reader :last_relex_result #: RelexResult?
|
|
17
|
+
attr_reader :last_blender #: Blender?
|
|
18
|
+
|
|
19
|
+
# @rbs @resource_limits: ResourceLimits
|
|
20
|
+
# @rbs @parser: Parser
|
|
21
|
+
# @rbs @cache: NodeCache
|
|
22
|
+
# @rbs @blender_enabled: bool
|
|
23
|
+
# @rbs @mutex: Mutex
|
|
24
|
+
|
|
25
|
+
# @rbs (Class parser_class, SourceText source_text, ?resource_limits: ResourceLimits?, ?blender: bool) -> void
|
|
26
|
+
def initialize(parser_class, source_text, resource_limits: nil, blender: true)
|
|
27
|
+
unless source_text.is_a?(SourceText)
|
|
28
|
+
raise ArgumentError, "incremental_session requires an Ibex::Runtime::CST::SourceText"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@resource_limits = resource_limits || ResourceLimits.new #: ResourceLimits
|
|
32
|
+
@parser = parser_class.__send__(:new, resource_limits: @resource_limits) #: Parser
|
|
33
|
+
validate_parser!
|
|
34
|
+
@cache = NodeCache.new #: NodeCache
|
|
35
|
+
@source_text = source_text
|
|
36
|
+
@last_relex_result = nil #: RelexResult?
|
|
37
|
+
@last_blender = nil #: Blender?
|
|
38
|
+
@blender_enabled = blender
|
|
39
|
+
@mutex = Mutex.new
|
|
40
|
+
@result, @token_memo, @parse_memo = parse_current(0.0)
|
|
41
|
+
enforce_memo_budget!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Apply edits expressed against the current source and return syntax only.
|
|
45
|
+
# @rbs (Array[TextEdit] edits) -> SyntaxResult
|
|
46
|
+
def edit(edits)
|
|
47
|
+
@mutex.synchronize { edit_locked(TextEdit.normalize(edits)) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Observe parser and incremental runtime events.
|
|
51
|
+
# @rbs () { (Event) -> void } -> Observation::Subscription
|
|
52
|
+
def observe(&observer)
|
|
53
|
+
@parser.observe(&observer)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @rbs (Observation::Subscription subscription) -> bool
|
|
57
|
+
def unobserve(subscription)
|
|
58
|
+
@parser.unobserve(subscription)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# @rbs (Array[TextEdit] edits) -> SyntaxResult
|
|
64
|
+
def edit_locked(edits)
|
|
65
|
+
return @result if edits.empty?
|
|
66
|
+
|
|
67
|
+
previous_memo = @token_memo
|
|
68
|
+
previous_root = @result.syntax_root
|
|
69
|
+
previous_parse_memo = @parse_memo
|
|
70
|
+
@source_text = @source_text.apply(edits)
|
|
71
|
+
lexed = scan_current
|
|
72
|
+
return finish_full_fallback(previous_memo, edits, :lexical_error) unless lexed
|
|
73
|
+
|
|
74
|
+
relexed = Relexer.reconcile(previous_memo, lexed.memo, edits)
|
|
75
|
+
blender = build_blender(previous_root, previous_parse_memo, lexed.with_memo(relexed.memo), edits)
|
|
76
|
+
finish_blended_edit(relexed, blender)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @rbs () -> LexedSyntax?
|
|
80
|
+
def scan_current
|
|
81
|
+
@parser.__send__(:scan_syntax_with_cache, @source_text, @cache)
|
|
82
|
+
rescue ParseError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @rbs (SyntaxNode old_root, ParseMemo old_memo, LexedSyntax lexed, Array[TextEdit] edits) -> Blender
|
|
87
|
+
def build_blender(old_root, old_memo, lexed, edits)
|
|
88
|
+
Blender.new(
|
|
89
|
+
old_root: old_root,
|
|
90
|
+
parse_memo: old_memo,
|
|
91
|
+
lexed: lexed,
|
|
92
|
+
edits: edits,
|
|
93
|
+
max_decomposed_nodes: @resource_limits.max_incremental_decomposed_nodes,
|
|
94
|
+
enabled: @blender_enabled
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @rbs (RelexResult relexed, Blender blender) -> SyntaxResult
|
|
99
|
+
def finish_blended_edit(relexed, blender)
|
|
100
|
+
@last_relex_result = relexed
|
|
101
|
+
@last_blender = blender
|
|
102
|
+
parsed = @parser.__send__(:parse_syntax_token_source, blender, @cache)
|
|
103
|
+
reused_ratio = blender_reused_ratio(blender, parsed.syntax_root.green, relexed)
|
|
104
|
+
@token_memo = relexed.memo
|
|
105
|
+
@parse_memo = @parser.__send__(:syntax_parse_memo) || empty_parse_memo(parsed.syntax_root.green)
|
|
106
|
+
@result = SyntaxResult.new(
|
|
107
|
+
syntax_root: parsed.syntax_root,
|
|
108
|
+
diagnostics: parsed.diagnostics,
|
|
109
|
+
reused_ratio: reused_ratio
|
|
110
|
+
)
|
|
111
|
+
report_blender_fallback(blender)
|
|
112
|
+
enforce_memo_budget!
|
|
113
|
+
emit_reuse_event(blender, relexed, reused_ratio)
|
|
114
|
+
@result
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @rbs (Blender blender, GreenNode root, RelexResult relexed) -> Float
|
|
118
|
+
def blender_reused_ratio(blender, root, relexed)
|
|
119
|
+
return relexed.reused_ratio unless @blender_enabled
|
|
120
|
+
|
|
121
|
+
blender.reused_descendants.fdiv(root.descendant_count)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @rbs (Blender blender, RelexResult relexed, Float reused_ratio) -> void
|
|
125
|
+
def emit_reuse_event(blender, relexed, reused_ratio)
|
|
126
|
+
@parser.__send__(
|
|
127
|
+
:emit_incremental_event,
|
|
128
|
+
:cst_reuse,
|
|
129
|
+
{
|
|
130
|
+
"stage" => blender.reused_descendants.positive? ? "subtree" : "lexical",
|
|
131
|
+
"reused_tokens" => relexed.reused_count,
|
|
132
|
+
"token_count" => relexed.memo.tokens.length,
|
|
133
|
+
"reused_ratio" => reused_ratio
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @rbs () -> void
|
|
139
|
+
def validate_parser!
|
|
140
|
+
unless @parser.is_a?(GeneratedLexer)
|
|
141
|
+
raise IncrementalUnsupportedError, "incremental parsing requires a generated lexer"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
tables = @parser.__send__(:parser_tables) #: Hash[Symbol, untyped]
|
|
145
|
+
config = tables[:cst]
|
|
146
|
+
unless tables.fetch(:format_version) >= 6 && config.is_a?(Hash)
|
|
147
|
+
raise IncrementalUnsupportedError, "incremental parsing requires a format-v6 Red/Green CST parser"
|
|
148
|
+
end
|
|
149
|
+
return unless config.fetch(:trivia_policy) == :drop
|
|
150
|
+
|
|
151
|
+
raise IncrementalUnsupportedError, "incremental parsing does not support drop trivia policy"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @rbs (Float reused_ratio) -> [SyntaxResult, TokenMemo, ParseMemo]
|
|
155
|
+
def parse_current(reused_ratio)
|
|
156
|
+
parsed = @parser.__send__(:parse_syntax_with_cache, @source_text, @cache)
|
|
157
|
+
result = SyntaxResult.new(
|
|
158
|
+
syntax_root: parsed.syntax_root,
|
|
159
|
+
diagnostics: parsed.diagnostics,
|
|
160
|
+
reused_ratio: reused_ratio
|
|
161
|
+
)
|
|
162
|
+
states = @parser.__send__(:syntax_token_states)
|
|
163
|
+
parse_memo = @parser.__send__(:syntax_parse_memo) || empty_parse_memo(result.syntax_root.green)
|
|
164
|
+
|
|
165
|
+
[result, TokenMemo.from_root(result.syntax_root, states: states), parse_memo]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Error trees cannot be reused, but retain position-aligned disposable memo storage.
|
|
169
|
+
# @rbs (GreenNode root) -> ParseMemo
|
|
170
|
+
def empty_parse_memo(root)
|
|
171
|
+
tables = @parser.__send__(:parser_tables) #: Hash[Symbol, untyped]
|
|
172
|
+
ParseMemo.new(
|
|
173
|
+
left_states: Array.new(root.descendant_count),
|
|
174
|
+
grammar_digest: tables.fetch(:grammar_digest),
|
|
175
|
+
state_count: tables.fetch(:state_count),
|
|
176
|
+
production_count: tables.fetch(:production_count)
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @rbs (Blender blender) -> void
|
|
181
|
+
def report_blender_fallback(blender)
|
|
182
|
+
return unless blender.fallback_reason
|
|
183
|
+
|
|
184
|
+
@parser.__send__(
|
|
185
|
+
:emit_incremental_event,
|
|
186
|
+
:cst_fallback,
|
|
187
|
+
{
|
|
188
|
+
"reason" => blender.fallback_reason.to_s,
|
|
189
|
+
"limit" => @resource_limits.max_incremental_decomposed_nodes,
|
|
190
|
+
"observed" => blender.decomposed_nodes
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @rbs (TokenMemo previous_memo, Array[TextEdit] edits, Symbol reason) -> SyntaxResult
|
|
196
|
+
def finish_full_fallback(previous_memo, edits, reason)
|
|
197
|
+
fresh_result, fresh_memo, fresh_parse_memo = parse_current(0.0)
|
|
198
|
+
@last_relex_result = Relexer.reconcile(previous_memo, fresh_memo, edits)
|
|
199
|
+
@last_blender = nil
|
|
200
|
+
@token_memo = fresh_memo
|
|
201
|
+
@parse_memo = fresh_parse_memo
|
|
202
|
+
@result = fresh_result
|
|
203
|
+
@parser.__send__(
|
|
204
|
+
:emit_incremental_event,
|
|
205
|
+
:cst_fallback,
|
|
206
|
+
{ "reason" => reason.to_s, "limit" => 0, "observed" => 0 }
|
|
207
|
+
)
|
|
208
|
+
enforce_memo_budget!
|
|
209
|
+
@result
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @rbs () -> void
|
|
213
|
+
def enforce_memo_budget!
|
|
214
|
+
observed = @token_memo.estimated_bytes + @parse_memo.estimated_bytes
|
|
215
|
+
limit = @resource_limits.max_session_memo_bytes
|
|
216
|
+
return unless observed > limit
|
|
217
|
+
|
|
218
|
+
@parser.__send__(
|
|
219
|
+
:emit_incremental_event,
|
|
220
|
+
:cst_fallback,
|
|
221
|
+
{ "reason" => "memo_budget", "limit" => limit, "observed" => observed }
|
|
222
|
+
)
|
|
223
|
+
@cache.clear
|
|
224
|
+
@token_memo = TokenMemo.from_root(@result.syntax_root)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ibex
|
|
5
|
+
module Runtime
|
|
6
|
+
module CST
|
|
7
|
+
# Session-local token metadata used to validate lexical resynchronization.
|
|
8
|
+
class TokenMemo
|
|
9
|
+
ENTRY_BYTES = 32 #: Integer
|
|
10
|
+
|
|
11
|
+
attr_reader :tokens #: Array[GreenToken]
|
|
12
|
+
attr_reader :offsets #: Array[Integer]
|
|
13
|
+
attr_reader :states #: Array[Symbol]
|
|
14
|
+
|
|
15
|
+
# @rbs (tokens: Array[GreenToken], offsets: Array[Integer], states: Array[Symbol]) -> void
|
|
16
|
+
def initialize(tokens:, offsets:, states:)
|
|
17
|
+
unless tokens.length == offsets.length && offsets.length == states.length
|
|
18
|
+
raise ArgumentError, "token memo arrays must have equal lengths"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
ordered = (0...[offsets.length - 1, 0].max).all? do |index|
|
|
22
|
+
offsets.fetch(index) <= offsets.fetch(index + 1)
|
|
23
|
+
end
|
|
24
|
+
raise ArgumentError, "token memo offsets must be ordered" unless ordered
|
|
25
|
+
|
|
26
|
+
@tokens = tokens.dup.freeze
|
|
27
|
+
@offsets = offsets.dup.freeze
|
|
28
|
+
@states = states.dup.freeze
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @rbs (SyntaxNode root, ?states: Array[Symbol]) -> TokenMemo
|
|
33
|
+
def self.from_root(root, states: [])
|
|
34
|
+
tokens = root.tokens.map(&:green)
|
|
35
|
+
offsets = [] #: Array[Integer]
|
|
36
|
+
offset = 0
|
|
37
|
+
tokens.each do |token|
|
|
38
|
+
offsets << offset
|
|
39
|
+
offset += token.full_width
|
|
40
|
+
end
|
|
41
|
+
actual_states = Array.new(tokens.length) { |index| states.fetch(index, :INITIAL) }
|
|
42
|
+
new(tokens: tokens, offsets: offsets, states: actual_states)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The first token whose owned full span contains or follows an edit.
|
|
46
|
+
# @rbs (Array[TextEdit] edits) -> Integer?
|
|
47
|
+
def damage_index(edits)
|
|
48
|
+
normalized = TextEdit.normalize(edits)
|
|
49
|
+
edit = normalized.first
|
|
50
|
+
return unless edit
|
|
51
|
+
|
|
52
|
+
point = edit.start
|
|
53
|
+
@tokens.each_index do |index|
|
|
54
|
+
start = @offsets.fetch(index)
|
|
55
|
+
finish = start + @tokens.fetch(index).full_width
|
|
56
|
+
return index if point < finish || (start == point && finish == point)
|
|
57
|
+
end
|
|
58
|
+
@tokens.empty? ? nil : @tokens.length - 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @rbs () -> Integer
|
|
62
|
+
def estimated_bytes = @tokens.length * ENTRY_BYTES
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ibex
|
|
5
|
+
module Runtime
|
|
6
|
+
module CST
|
|
7
|
+
# Flags cached on Green elements and propagated by Green nodes.
|
|
8
|
+
module Flags
|
|
9
|
+
CONTAINS_ERROR = 1 << 0 #: Integer
|
|
10
|
+
CONTAINS_MISSING = 1 << 1 #: Integer
|
|
11
|
+
CONTAINS_SKIPPED = 1 << 2 #: Integer
|
|
12
|
+
HAS_ANNOTATION = 1 << 3 #: Integer
|
|
13
|
+
SYNTHETIC = 1 << 4 #: Integer
|
|
14
|
+
INCOMPLETE_INPUT = 1 << 5 #: Integer
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Grammar-specific integer kind names and interval predicates.
|
|
18
|
+
class Kind
|
|
19
|
+
# @rbs!
|
|
20
|
+
# type kind_map = Hash[String, Integer]
|
|
21
|
+
# type field_slot = Integer | { index: Integer, extraction: Symbol }
|
|
22
|
+
# type slot = { node_kind: Integer, node_name: String, fields: Hash[String, field_slot] }
|
|
23
|
+
# type metadata = {
|
|
24
|
+
# names: Array[String],
|
|
25
|
+
# terminal_range: Array[Integer],
|
|
26
|
+
# nonterminal_range: Array[Integer],
|
|
27
|
+
# named: kind_map,
|
|
28
|
+
# named_nonterminals: Hash[Integer, Integer],
|
|
29
|
+
# trivia: kind_map,
|
|
30
|
+
# synthetic: kind_map
|
|
31
|
+
# }
|
|
32
|
+
|
|
33
|
+
# @rbs @names: Array[String]
|
|
34
|
+
# @rbs @terminal_range: Array[Integer]
|
|
35
|
+
# @rbs @nonterminal_range: Array[Integer]
|
|
36
|
+
# @rbs @named: Hash[String, Integer]
|
|
37
|
+
# @rbs @named_nonterminals: Hash[Integer, Integer]
|
|
38
|
+
# @rbs @trivia: Hash[String, Integer]
|
|
39
|
+
# @rbs @synthetic: Hash[String, Integer]
|
|
40
|
+
# @rbs @field_slots: Hash[Integer, Hash[String, field_slot]]
|
|
41
|
+
|
|
42
|
+
# @rbs (metadata metadata, ?slots: Hash[Integer, slot]) -> void
|
|
43
|
+
def initialize(metadata, slots: {})
|
|
44
|
+
@names = metadata.fetch(:names).dup.freeze
|
|
45
|
+
@terminal_range = metadata.fetch(:terminal_range).dup.freeze
|
|
46
|
+
@nonterminal_range = metadata.fetch(:nonterminal_range).dup.freeze
|
|
47
|
+
@named = metadata.fetch(:named).dup.freeze
|
|
48
|
+
@named_nonterminals = metadata.fetch(:named_nonterminals).dup.freeze
|
|
49
|
+
@trivia = metadata.fetch(:trivia).dup.freeze
|
|
50
|
+
@synthetic = metadata.fetch(:synthetic).dup.freeze
|
|
51
|
+
@field_slots = build_field_slots(slots)
|
|
52
|
+
freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @rbs (Integer kind) -> String
|
|
56
|
+
def name(kind)
|
|
57
|
+
@names.fetch(kind)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @rbs (String | Symbol name) -> Integer
|
|
61
|
+
def fetch(name)
|
|
62
|
+
key = name.to_s
|
|
63
|
+
@named[key] || @trivia[key] || @synthetic.fetch(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @rbs (Integer kind) -> bool
|
|
67
|
+
def terminal?(kind) = within?(@terminal_range, kind)
|
|
68
|
+
|
|
69
|
+
# @rbs (Integer kind) -> bool
|
|
70
|
+
def nonterminal?(kind)
|
|
71
|
+
within?(@nonterminal_range, kind) || @named_nonterminals.key?(kind)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @rbs (Integer kind) -> bool
|
|
75
|
+
def trivia?(kind) = @trivia.value?(kind)
|
|
76
|
+
|
|
77
|
+
# @rbs (Integer kind) -> bool
|
|
78
|
+
def error?(kind)
|
|
79
|
+
kind == @synthetic["lexical_error_token"] || kind == @synthetic["error_node"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Map a named node kind to its physical nonterminal kind.
|
|
83
|
+
# @rbs (Integer kind) -> Integer
|
|
84
|
+
def nonterminal_of(kind)
|
|
85
|
+
@named_nonterminals.fetch(kind, kind)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Return the named field-to-physical-slot mapping for a kind.
|
|
89
|
+
# @rbs (Integer kind) -> Hash[String, field_slot]
|
|
90
|
+
def fields(kind) = @field_slots.fetch(kind, EMPTY_FIELDS)
|
|
91
|
+
|
|
92
|
+
# Return the serialization-safe kind metadata.
|
|
93
|
+
# @rbs () -> metadata
|
|
94
|
+
def to_h
|
|
95
|
+
value = {
|
|
96
|
+
names: @names, terminal_range: @terminal_range, nonterminal_range: @nonterminal_range,
|
|
97
|
+
named: @named, named_nonterminals: @named_nonterminals,
|
|
98
|
+
trivia: @trivia, synthetic: @synthetic
|
|
99
|
+
} #: metadata
|
|
100
|
+
value.freeze
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
empty_fields = {} #: Hash[String, field_slot]
|
|
106
|
+
EMPTY_FIELDS = empty_fields.freeze
|
|
107
|
+
private_constant :EMPTY_FIELDS
|
|
108
|
+
|
|
109
|
+
# @rbs (Hash[Integer, slot] slots) -> Hash[Integer, Hash[String, field_slot]]
|
|
110
|
+
def build_field_slots(slots)
|
|
111
|
+
values = {} #: Hash[Integer, Hash[String, field_slot]]
|
|
112
|
+
slots.each_value do |slot|
|
|
113
|
+
kind = slot.fetch(:node_kind)
|
|
114
|
+
fields = slot.fetch(:fields).to_h do |name, field_slot|
|
|
115
|
+
index = field_slot.is_a?(Hash) ? field_slot.fetch(:index) : field_slot
|
|
116
|
+
[name, index]
|
|
117
|
+
end.freeze
|
|
118
|
+
previous = values[kind]
|
|
119
|
+
raise ArgumentError, "conflicting CST fields for kind #{kind}" if previous && previous != fields
|
|
120
|
+
|
|
121
|
+
values[kind] = fields
|
|
122
|
+
end
|
|
123
|
+
values.freeze
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @rbs (Array[Integer] range, Integer kind) -> bool
|
|
127
|
+
def within?(range, kind)
|
|
128
|
+
kind >= range.fetch(0) && kind < range.fetch(1)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ibex
|
|
5
|
+
module Runtime
|
|
6
|
+
module CST
|
|
7
|
+
# Immutable result returned by syntax-aware parser entry points.
|
|
8
|
+
class ParseResult
|
|
9
|
+
attr_reader :value #: untyped
|
|
10
|
+
attr_reader :syntax_root #: SyntaxNode
|
|
11
|
+
attr_reader :diagnostics #: Array[untyped]
|
|
12
|
+
|
|
13
|
+
# @rbs (value: untyped, syntax_root: SyntaxNode, diagnostics: Array[untyped]) -> void
|
|
14
|
+
def initialize(value:, syntax_root:, diagnostics:)
|
|
15
|
+
@value = value
|
|
16
|
+
@syntax_root = syntax_root
|
|
17
|
+
@diagnostics = diagnostics.dup.freeze
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Syntax-only result used by incremental sessions.
|
|
23
|
+
class SyntaxResult
|
|
24
|
+
attr_reader :syntax_root #: SyntaxNode
|
|
25
|
+
attr_reader :diagnostics #: Array[untyped]
|
|
26
|
+
attr_reader :reused_ratio #: Float
|
|
27
|
+
|
|
28
|
+
# @rbs (syntax_root: SyntaxNode, diagnostics: Array[untyped], reused_ratio: Float) -> void
|
|
29
|
+
def initialize(syntax_root:, diagnostics:, reused_ratio:)
|
|
30
|
+
@syntax_root = syntax_root
|
|
31
|
+
@diagnostics = diagnostics.dup.freeze
|
|
32
|
+
@reused_ratio = reused_ratio
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ParseResult = CST::ParseResult
|
|
39
|
+
SyntaxResult = CST::SyntaxResult
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative "editing" unless defined?(Ibex::Runtime::CST::Editing)
|
|
5
|
+
|
|
6
|
+
module Ibex
|
|
7
|
+
module Runtime
|
|
8
|
+
module CST
|
|
9
|
+
# Bottom-up syntax rewriter with kind-name dispatch.
|
|
10
|
+
class SyntaxRewriter
|
|
11
|
+
# @rbs! type element = SyntaxNode | SyntaxToken
|
|
12
|
+
# @rbs! type green = GreenNode | GreenToken
|
|
13
|
+
|
|
14
|
+
# @rbs (SyntaxNode root) -> SyntaxNode
|
|
15
|
+
def rewrite(root)
|
|
16
|
+
green = visit(root)
|
|
17
|
+
return root if green.equal?(root.green)
|
|
18
|
+
raise TypeError, "a syntax root must remain a GreenNode" unless green.is_a?(GreenNode)
|
|
19
|
+
|
|
20
|
+
Editing.red_root(root, green)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs (element element) -> green
|
|
24
|
+
def visit(element)
|
|
25
|
+
rewritten = element.is_a?(SyntaxNode) ? visit_children(element) : element.green
|
|
26
|
+
method = :"visit_#{method_name(element.kind_name)}"
|
|
27
|
+
return rewritten unless respond_to?(method, true)
|
|
28
|
+
|
|
29
|
+
Editing.green_element(__send__(method, rewritten_view(element, rewritten)))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# @rbs (SyntaxNode node) -> GreenNode
|
|
35
|
+
def visit_children(node)
|
|
36
|
+
changed = false
|
|
37
|
+
children = node.children.map do |child|
|
|
38
|
+
green = visit(child)
|
|
39
|
+
changed ||= !green.equal?(child.green)
|
|
40
|
+
green
|
|
41
|
+
end
|
|
42
|
+
return node.green unless changed
|
|
43
|
+
|
|
44
|
+
GreenNode.new(
|
|
45
|
+
kind: node.green.kind, children: children,
|
|
46
|
+
flags: node.green.intrinsic_flags, annotations: node.green.annotations
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @rbs (element original, green rewritten) -> element
|
|
51
|
+
def rewritten_view(original, rewritten)
|
|
52
|
+
if rewritten.is_a?(GreenNode)
|
|
53
|
+
raise TypeError, "a node rewrite requires its original node" unless original.is_a?(SyntaxNode)
|
|
54
|
+
|
|
55
|
+
source = SourceText.new(rewritten.to_source, file: original.root.source_text.file)
|
|
56
|
+
return SyntaxNode.new(
|
|
57
|
+
green: rewritten, kinds: original.kinds,
|
|
58
|
+
trivia_policy: original.root.trivia_policy, source_text: source
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
parent = original.parent
|
|
63
|
+
raise TypeError, "a token rewrite requires its original parent" unless parent
|
|
64
|
+
|
|
65
|
+
SyntaxToken.new(green: rewritten, parent: parent, index: original.index, offset: original.offset)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @rbs (String name) -> String
|
|
69
|
+
def method_name(name)
|
|
70
|
+
name.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Ibex
|
|
7
|
+
module Runtime
|
|
8
|
+
module CST
|
|
9
|
+
class SerializationError < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Stable JSON serialization for `ibex_cst` schema version 1.
|
|
12
|
+
module Serialize
|
|
13
|
+
SCHEMA_VERSION = 1 #: Integer
|
|
14
|
+
|
|
15
|
+
# @rbs (SyntaxNode | SerializedTree value, ?grammar_digest: String?, ?table_format: Integer?,
|
|
16
|
+
# ?state_count: Integer?, ?production_count: Integer?, ?memo: ParseMemo?) -> String
|
|
17
|
+
def dump(value, grammar_digest: nil, table_format: nil, state_count: nil, production_count: nil, memo: nil)
|
|
18
|
+
tree = serialization_tree(
|
|
19
|
+
value, grammar_digest: grammar_digest, table_format: table_format,
|
|
20
|
+
state_count: state_count, production_count: production_count, memo: memo
|
|
21
|
+
)
|
|
22
|
+
document = {
|
|
23
|
+
"ibex_ir" => "cst",
|
|
24
|
+
"schema_version" => SCHEMA_VERSION,
|
|
25
|
+
"grammar_digest" => tree.grammar_digest,
|
|
26
|
+
"table_format" => tree.table_format,
|
|
27
|
+
"state_count" => tree.state_count,
|
|
28
|
+
"production_count" => tree.production_count,
|
|
29
|
+
"trivia_policy" => tree.trivia_policy.to_s,
|
|
30
|
+
"kinds" => kinds_document(tree.kinds),
|
|
31
|
+
"root" => element_document(tree.green_root),
|
|
32
|
+
"memo" => tree.memo&.to_h
|
|
33
|
+
}
|
|
34
|
+
"#{JSON.pretty_generate(document)}\n"
|
|
35
|
+
end
|
|
36
|
+
module_function :dump
|
|
37
|
+
|
|
38
|
+
# @rbs (String source, ?grammar_digest: String?, ?state_count: Integer?,
|
|
39
|
+
# ?production_count: Integer?) -> SerializedTree
|
|
40
|
+
def load(source, grammar_digest: nil, state_count: nil, production_count: nil)
|
|
41
|
+
Validator.validate(
|
|
42
|
+
source,
|
|
43
|
+
grammar_digest: grammar_digest,
|
|
44
|
+
state_count: state_count,
|
|
45
|
+
production_count: production_count
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
module_function :load
|
|
49
|
+
|
|
50
|
+
# @rbs (SyntaxNode | SerializedTree value, grammar_digest: String?, table_format: Integer?,
|
|
51
|
+
# state_count: Integer?, production_count: Integer?, memo: ParseMemo?) -> SerializedTree
|
|
52
|
+
def serialization_tree(value, grammar_digest:, table_format:, state_count:, production_count:, memo:)
|
|
53
|
+
return value if value.is_a?(SerializedTree)
|
|
54
|
+
|
|
55
|
+
missing = {
|
|
56
|
+
grammar_digest: grammar_digest, table_format: table_format,
|
|
57
|
+
state_count: state_count, production_count: production_count
|
|
58
|
+
}.select { |_name, item| item.nil? }.keys
|
|
59
|
+
raise ArgumentError, "missing CST serialization metadata: #{missing.join(', ')}" unless missing.empty?
|
|
60
|
+
|
|
61
|
+
SerializedTree.new(
|
|
62
|
+
grammar_digest: grammar_digest || "", table_format: table_format || 0,
|
|
63
|
+
state_count: state_count || 0, production_count: production_count || 0,
|
|
64
|
+
trivia_policy: value.trivia_policy, kinds: value.kinds, green_root: value.green, memo: memo
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
module_function :serialization_tree
|
|
68
|
+
private_class_method :serialization_tree
|
|
69
|
+
|
|
70
|
+
# @rbs (Kind kinds) -> Hash[String, untyped]
|
|
71
|
+
def kinds_document(kinds)
|
|
72
|
+
metadata = kinds.to_h
|
|
73
|
+
{
|
|
74
|
+
"names" => metadata.fetch(:names),
|
|
75
|
+
"terminal_range" => metadata.fetch(:terminal_range),
|
|
76
|
+
"nonterminal_range" => metadata.fetch(:nonterminal_range),
|
|
77
|
+
"named" => metadata.fetch(:named),
|
|
78
|
+
"named_nonterminals" => metadata.fetch(:named_nonterminals).to_h { |key, value| [key.to_s, value] },
|
|
79
|
+
"trivia" => metadata.fetch(:trivia),
|
|
80
|
+
"synthetic" => metadata.fetch(:synthetic)
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
module_function :kinds_document
|
|
84
|
+
private_class_method :kinds_document
|
|
85
|
+
|
|
86
|
+
# @rbs (GreenNode | GreenToken element) -> Hash[String, untyped]
|
|
87
|
+
def element_document(element)
|
|
88
|
+
if element.is_a?(GreenNode)
|
|
89
|
+
raise SerializationError, "annotated Green nodes cannot be serialized" unless element.annotations.empty?
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"k" => element.kind,
|
|
93
|
+
"f" => element.intrinsic_flags,
|
|
94
|
+
"c" => element.children.map { |child| element_document(child) }
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
"k" => element.kind,
|
|
100
|
+
"f" => element.flags,
|
|
101
|
+
"e" => element.expected_kind,
|
|
102
|
+
"t" => encode_text(element.text),
|
|
103
|
+
"l" => element.leading.map { |trivia| trivia_document(trivia) },
|
|
104
|
+
"r" => element.trailing.map { |trivia| trivia_document(trivia) }
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
module_function :element_document
|
|
108
|
+
private_class_method :element_document
|
|
109
|
+
|
|
110
|
+
# @rbs (GreenTrivia trivia) -> Array[untyped]
|
|
111
|
+
def trivia_document(trivia) = [trivia.kind, encode_text(trivia.text)]
|
|
112
|
+
module_function :trivia_document
|
|
113
|
+
private_class_method :trivia_document
|
|
114
|
+
|
|
115
|
+
# @rbs (String text) -> (String | Hash[String, String])
|
|
116
|
+
def encode_text(text)
|
|
117
|
+
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
118
|
+
return utf8 if utf8.valid_encoding?
|
|
119
|
+
|
|
120
|
+
{ "b64" => [text.b].pack("m0") }
|
|
121
|
+
end
|
|
122
|
+
module_function :encode_text
|
|
123
|
+
private_class_method :encode_text
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|