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.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +337 -0
  4. data/lib/ibex/runtime/ast_data.rb +23 -0
  5. data/lib/ibex/runtime/cst/annotation.rb +16 -0
  6. data/lib/ibex/runtime/cst/cursor.rb +68 -0
  7. data/lib/ibex/runtime/cst/diff.rb +80 -0
  8. data/lib/ibex/runtime/cst/editing.rb +56 -0
  9. data/lib/ibex/runtime/cst/editor.rb +94 -0
  10. data/lib/ibex/runtime/cst/green/builder.rb +211 -0
  11. data/lib/ibex/runtime/cst/green/cache.rb +150 -0
  12. data/lib/ibex/runtime/cst/green/node.rb +78 -0
  13. data/lib/ibex/runtime/cst/green/token.rb +91 -0
  14. data/lib/ibex/runtime/cst/green/trivia.rb +36 -0
  15. data/lib/ibex/runtime/cst/incremental/blender.rb +233 -0
  16. data/lib/ibex/runtime/cst/incremental/lexed_syntax.rb +28 -0
  17. data/lib/ibex/runtime/cst/incremental/parse_memo.rb +95 -0
  18. data/lib/ibex/runtime/cst/incremental/relexer.rb +134 -0
  19. data/lib/ibex/runtime/cst/incremental/session.rb +229 -0
  20. data/lib/ibex/runtime/cst/incremental/token_memo.rb +66 -0
  21. data/lib/ibex/runtime/cst/kind.rb +133 -0
  22. data/lib/ibex/runtime/cst/parse_result.rb +41 -0
  23. data/lib/ibex/runtime/cst/rewriter.rb +75 -0
  24. data/lib/ibex/runtime/cst/serialize.rb +127 -0
  25. data/lib/ibex/runtime/cst/serialized_tree.rb +44 -0
  26. data/lib/ibex/runtime/cst/source_text.rb +112 -0
  27. data/lib/ibex/runtime/cst/syntax_node.rb +432 -0
  28. data/lib/ibex/runtime/cst/syntax_token.rb +160 -0
  29. data/lib/ibex/runtime/cst/text_edit.rb +77 -0
  30. data/lib/ibex/runtime/cst/typed_node.rb +87 -0
  31. data/lib/ibex/runtime/cst/validator.rb +428 -0
  32. data/lib/ibex/runtime/cst.rb +38 -0
  33. data/lib/ibex/runtime/event.rb +44 -0
  34. data/lib/ibex/runtime/event_jsonl_tracer.rb +37 -0
  35. data/lib/ibex/runtime/event_sanitizer.rb +273 -0
  36. data/lib/ibex/runtime/generated_lexer.rb +514 -0
  37. data/lib/ibex/runtime/jsonl_tracer.rb +75 -0
  38. data/lib/ibex/runtime/lexer_input.rb +107 -0
  39. data/lib/ibex/runtime/location_span.rb +114 -0
  40. data/lib/ibex/runtime/observation.rb +82 -0
  41. data/lib/ibex/runtime/parser.rb +3027 -0
  42. data/lib/ibex/runtime/parser_sync_recovery.rb +108 -0
  43. data/lib/ibex/runtime/repair.rb +126 -0
  44. data/lib/ibex/runtime/repair_priority_queue.rb +60 -0
  45. data/lib/ibex/runtime/repair_search.rb +318 -0
  46. data/lib/ibex/runtime/resource_limits.rb +64 -0
  47. data/lib/ibex/runtime/table_format.rb +11 -0
  48. data/lib/ibex/runtime/version.rb +7 -0
  49. data/lib/ibex/runtime.rb +27 -0
  50. data/lib/ibex/tables/compact.rb +186 -0
  51. data/lib/ibex/tables/compact_actions.rb +135 -0
  52. data/lib/ibex/tables/compact_productions.rb +167 -0
  53. data/sig/ibex/runtime/ast_data.rbs +12 -0
  54. data/sig/ibex/runtime/cst/annotation.rbs +13 -0
  55. data/sig/ibex/runtime/cst/cursor.rbs +34 -0
  56. data/sig/ibex/runtime/cst/diff.rbs +29 -0
  57. data/sig/ibex/runtime/cst/editing.rbs +23 -0
  58. data/sig/ibex/runtime/cst/editor.rbs +43 -0
  59. data/sig/ibex/runtime/cst/green/builder.rbs +84 -0
  60. data/sig/ibex/runtime/cst/green/cache.rbs +60 -0
  61. data/sig/ibex/runtime/cst/green/node.rbs +49 -0
  62. data/sig/ibex/runtime/cst/green/token.rbs +59 -0
  63. data/sig/ibex/runtime/cst/green/trivia.rbs +29 -0
  64. data/sig/ibex/runtime/cst/incremental/blender.rbs +114 -0
  65. data/sig/ibex/runtime/cst/incremental/lexed_syntax.rbs +20 -0
  66. data/sig/ibex/runtime/cst/incremental/parse_memo.rbs +59 -0
  67. data/sig/ibex/runtime/cst/incremental/relexer.rbs +54 -0
  68. data/sig/ibex/runtime/cst/incremental/session.rbs +89 -0
  69. data/sig/ibex/runtime/cst/incremental/token_memo.rbs +31 -0
  70. data/sig/ibex/runtime/cst/kind.rbs +93 -0
  71. data/sig/ibex/runtime/cst/parse_result.rbs +35 -0
  72. data/sig/ibex/runtime/cst/rewriter.rbs +31 -0
  73. data/sig/ibex/runtime/cst/serialize.rbs +39 -0
  74. data/sig/ibex/runtime/cst/serialized_tree.rbs +34 -0
  75. data/sig/ibex/runtime/cst/source_text.rbs +51 -0
  76. data/sig/ibex/runtime/cst/syntax_node.rbs +195 -0
  77. data/sig/ibex/runtime/cst/syntax_token.rbs +112 -0
  78. data/sig/ibex/runtime/cst/text_edit.rbs +33 -0
  79. data/sig/ibex/runtime/cst/typed_node.rbs +50 -0
  80. data/sig/ibex/runtime/cst/validator.rbs +108 -0
  81. data/sig/ibex/runtime/cst.rbs +9 -0
  82. data/sig/ibex/runtime/event.rbs +26 -0
  83. data/sig/ibex/runtime/event_jsonl_tracer.rbs +25 -0
  84. data/sig/ibex/runtime/event_sanitizer.rbs +92 -0
  85. data/sig/ibex/runtime/generated_lexer.rbs +169 -0
  86. data/sig/ibex/runtime/jsonl_tracer.rbs +42 -0
  87. data/sig/ibex/runtime/lexer_input.rbs +44 -0
  88. data/sig/ibex/runtime/location_span.rbs +64 -0
  89. data/sig/ibex/runtime/observation.rbs +35 -0
  90. data/sig/ibex/runtime/parser.rbs +926 -0
  91. data/sig/ibex/runtime/parser_sync_recovery.rbs +60 -0
  92. data/sig/ibex/runtime/repair.rbs +92 -0
  93. data/sig/ibex/runtime/repair_priority_queue.rbs +27 -0
  94. data/sig/ibex/runtime/repair_search.rbs +114 -0
  95. data/sig/ibex/runtime/resource_limits.rbs +39 -0
  96. data/sig/ibex/runtime/table_format.rbs +11 -0
  97. data/sig/ibex/runtime/version.rbs +7 -0
  98. data/sig/ibex/runtime.rbs +6 -0
  99. data/sig/ibex/tables/compact.rbs +70 -0
  100. data/sig/ibex/tables/compact_actions.rbs +57 -0
  101. data/sig/ibex/tables/compact_productions.rbs +68 -0
  102. 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