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,94 @@
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
+ class EditConflictError < StandardError; end
10
+
11
+ # Applies multiple occurrence-addressed replacements in one path-copying pass.
12
+ class SyntaxEditor
13
+ # @rbs! type element = SyntaxNode | SyntaxToken
14
+ # @rbs! type green = GreenNode | GreenToken
15
+ # @rbs! type edit = [element, green]
16
+
17
+ # @rbs @root: SyntaxNode
18
+ # @rbs @edits: Array[edit]
19
+
20
+ # @rbs (SyntaxNode root) -> void
21
+ def initialize(root)
22
+ @root = root
23
+ @edits = []
24
+ end
25
+
26
+ # @rbs (element target, green | element replacement) -> self
27
+ def replace(target, replacement)
28
+ raise ArgumentError, "edit target belongs to a different root" unless target.root.equal?(@root)
29
+
30
+ green = Editing.green_element(replacement)
31
+ duplicate = @edits.find { |existing, _value| same_occurrence?(existing, target) }
32
+ if duplicate
33
+ raise EditConflictError, "the same syntax occurrence has conflicting replacements" unless
34
+ duplicate.fetch(1).equal?(green)
35
+
36
+ return self
37
+ end
38
+ return self if @edits.any? { |existing, _value| ancestor?(existing, target) }
39
+
40
+ @edits.reject! { |existing, _value| ancestor?(target, existing) }
41
+ @edits << [target, green]
42
+ self
43
+ end
44
+
45
+ # @rbs () -> SyntaxNode
46
+ def apply
47
+ green = apply_element(@root)
48
+ return @root if green.equal?(@root.green)
49
+ raise TypeError, "a syntax root must remain a GreenNode" unless green.is_a?(GreenNode)
50
+
51
+ Editing.red_root(@root, green)
52
+ end
53
+
54
+ private
55
+
56
+ # @rbs (element element) -> green
57
+ def apply_element(element)
58
+ replacement = @edits.find { |target, _green| same_occurrence?(target, element) }&.fetch(1)
59
+ return replacement if replacement
60
+ return element.green if element.is_a?(SyntaxToken)
61
+
62
+ changed = false
63
+ children = element.children.map do |child|
64
+ green = apply_element(child)
65
+ changed ||= !green.equal?(child.green)
66
+ green
67
+ end
68
+ return element.green unless changed
69
+
70
+ GreenNode.new(
71
+ kind: element.green.kind, children: children,
72
+ flags: element.green.intrinsic_flags, annotations: element.green.annotations
73
+ )
74
+ end
75
+
76
+ # @rbs (element possible_ancestor, element element) -> bool
77
+ def ancestor?(possible_ancestor, element)
78
+ parent = element.parent
79
+ while parent
80
+ return true if same_occurrence?(parent, possible_ancestor)
81
+
82
+ parent = parent.parent
83
+ end
84
+ false
85
+ end
86
+
87
+ # @rbs (element left, element right) -> bool
88
+ def same_occurrence?(left, right)
89
+ left.root.equal?(right.root) && left.green.equal?(right.green) && left.offset == right.offset
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "cache" unless defined?(Ibex::Runtime::CST::NodeCache)
5
+
6
+ module Ibex
7
+ module Runtime
8
+ module CST
9
+ # LR bottom-up builder backed by a Green element stack.
10
+ # rubocop:disable Naming/PredicateMethod -- mutation methods report whether they found a target.
11
+ class GreenBuilder
12
+ # @rbs! type child = GreenNode | GreenToken
13
+ empty_trivia = [] # @type var empty_trivia: Array[GreenTrivia]
14
+ EMPTY_TRIVIA = empty_trivia.freeze #: Array[GreenTrivia]
15
+
16
+ # @rbs @kinds: Kind
17
+ # @rbs @cache: NodeCache
18
+ # @rbs @stack: Array[child]
19
+
20
+ # @rbs (kinds: Kind, ?cache: NodeCache) -> void
21
+ def initialize(kinds:, cache: NodeCache.new)
22
+ @kinds = kinds
23
+ @cache = cache
24
+ @stack = []
25
+ end
26
+
27
+ # @rbs (Integer kind, String text, ?leading: Array[GreenTrivia], ?trailing: Array[GreenTrivia],
28
+ # ?flags: Integer) -> GreenToken
29
+ def token(kind, text, leading: EMPTY_TRIVIA, trailing: EMPTY_TRIVIA, flags: 0)
30
+ value = make_token(kind, text, leading: leading, trailing: trailing, flags: flags)
31
+ @stack << value
32
+ value
33
+ end
34
+
35
+ # Construct an interned token without changing the builder stack.
36
+ # @rbs (Integer kind, String text, ?leading: Array[GreenTrivia], ?trailing: Array[GreenTrivia],
37
+ # ?flags: Integer) -> GreenToken
38
+ def make_token(kind, text, leading: EMPTY_TRIVIA, trailing: EMPTY_TRIVIA, flags: 0)
39
+ @cache.intern_token_fields(
40
+ kind: kind, text: text, leading: leading, trailing: trailing, flags: flags
41
+ )
42
+ end
43
+
44
+ # @rbs (Integer kind, Integer arity, ?flags: Integer) -> GreenNode
45
+ def node(kind, arity, flags: 0)
46
+ raise ArgumentError, "arity must be non-negative" if arity.negative?
47
+ raise ArgumentError, "green stack underflow" if arity > @stack.length
48
+
49
+ empty = [] #: Array[child]
50
+ children = arity.zero? ? empty : @stack.pop(arity)
51
+ value = @cache.intern_node(GreenNode.new(kind: kind, children: children, flags: flags))
52
+ @stack << value
53
+ value
54
+ end
55
+
56
+ # Push one already validated nonterminal without rebuilding its descendants.
57
+ # @rbs (GreenNode value) -> GreenNode
58
+ def subtree(value)
59
+ @stack << value
60
+ value
61
+ end
62
+
63
+ # @rbs (Integer expected_kind) -> GreenToken
64
+ def missing(expected_kind)
65
+ value = @cache.intern_token_fields(
66
+ kind: @kinds.fetch(:missing_token), text: "", expected_kind: expected_kind,
67
+ flags: Flags::CONTAINS_MISSING | Flags::SYNTHETIC
68
+ )
69
+ @stack << value
70
+ value
71
+ end
72
+
73
+ # @rbs (String text, ?leading: Array[GreenTrivia]) -> GreenToken
74
+ def lexical_error(text, leading: EMPTY_TRIVIA)
75
+ value = @cache.intern_token_fields(
76
+ kind: @kinds.fetch(:lexical_error_token), text: text, leading: leading,
77
+ flags: Flags::CONTAINS_ERROR
78
+ )
79
+ @stack << value
80
+ value
81
+ end
82
+
83
+ # Preserve popped and discarded Green elements in one error node.
84
+ # @rbs (Integer pop_count, ?skipped: Array[child]) -> GreenNode
85
+ def absorb_into_error(pop_count, skipped: [])
86
+ raise ArgumentError, "pop_count must be non-negative" if pop_count.negative?
87
+ raise ArgumentError, "green stack underflow" if pop_count > @stack.length
88
+
89
+ empty = [] #: Array[child]
90
+ popped = pop_count.zero? ? empty : @stack.pop(pop_count)
91
+ value = @cache.intern_node(
92
+ GreenNode.new(
93
+ kind: @kinds.fetch(:error_node), children: popped + skipped,
94
+ flags: Flags::CONTAINS_ERROR
95
+ )
96
+ )
97
+ @stack << value
98
+ value
99
+ end
100
+
101
+ # Wrap the completed start node and explicit EOF token.
102
+ # @rbs (GreenToken eof_token, ?incomplete: bool) -> GreenNode
103
+ def finish_source_file(eof_token, incomplete: false)
104
+ raise ArgumentError, "expected one completed start node" unless @stack.one?
105
+
106
+ flags = Flags::SYNTHETIC
107
+ flags |= Flags::INCOMPLETE_INPUT if incomplete
108
+ @cache.intern_node(
109
+ GreenNode.new(
110
+ kind: @kinds.fetch(:source_file), children: [@stack.fetch(0), eof_token],
111
+ flags: flags
112
+ )
113
+ )
114
+ end
115
+
116
+ # Preserve every available fragment when parsing terminates without a start node.
117
+ # @rbs (?Array[child] trailing, ?incomplete: bool) -> GreenNode
118
+ def finish_synthetic_root(trailing = [], incomplete: false)
119
+ flags = Flags::SYNTHETIC | Flags::CONTAINS_ERROR
120
+ flags |= Flags::INCOMPLETE_INPUT if incomplete
121
+ @cache.intern_node(
122
+ GreenNode.new(
123
+ kind: @kinds.fetch(:synthetic_root), children: @stack + trailing,
124
+ flags: flags
125
+ )
126
+ )
127
+ end
128
+
129
+ # Append discarded input to the most recent error node on the stack.
130
+ # @rbs (child skipped) -> bool
131
+ def append_to_last_error(skipped)
132
+ index = @stack.rindex { |element| element.is_a?(GreenNode) && element.kind == @kinds.fetch(:error_node) }
133
+ return false unless index
134
+
135
+ error = @stack.fetch(index)
136
+ return false unless error.is_a?(GreenNode)
137
+
138
+ @stack[index] = @cache.intern_node(
139
+ GreenNode.new(
140
+ kind: error.kind, children: error.children + [skipped],
141
+ flags: error.intrinsic_flags | Flags::CONTAINS_ERROR, annotations: error.annotations
142
+ )
143
+ )
144
+ true
145
+ end
146
+
147
+ # Path-copy the right edge to attach balanced trailing trivia.
148
+ # @rbs (Array[GreenTrivia] trivia) -> bool
149
+ def append_trailing_to_last_token(trivia)
150
+ return false if trivia.empty?
151
+
152
+ index = @stack.length - 1
153
+ while index >= 0
154
+ replacement = with_rightmost_trailing(@stack.fetch(index), trivia)
155
+ if replacement
156
+ @stack[index] = replacement
157
+ return true
158
+ end
159
+ index -= 1
160
+ end
161
+ false
162
+ end
163
+
164
+ # @rbs () -> Array[child]
165
+ def snapshot = @stack.dup
166
+
167
+ # @rbs (Array[child] elements) -> void
168
+ def restore(elements)
169
+ @stack = elements.dup
170
+ end
171
+
172
+ # @rbs () -> Integer
173
+ def size = @stack.length
174
+
175
+ # @rbs () -> Array[child]
176
+ def elements = @stack.dup.freeze
177
+
178
+ private
179
+
180
+ # @rbs (child element, Array[GreenTrivia] trivia) -> child?
181
+ def with_rightmost_trailing(element, trivia)
182
+ if element.is_a?(GreenToken)
183
+ combined_trailing = element.trailing.empty? ? trivia : element.trailing + trivia
184
+ return @cache.intern_token_fields(
185
+ kind: element.kind, text: element.text, leading: element.leading,
186
+ trailing: combined_trailing, flags: element.flags,
187
+ expected_kind: element.expected_kind
188
+ )
189
+ end
190
+ return if element.children.empty?
191
+
192
+ child = element.children.last
193
+ return unless child
194
+
195
+ replacement = with_rightmost_trailing(child, trivia)
196
+ return unless replacement
197
+
198
+ children = element.children.dup
199
+ children[-1] = replacement
200
+ @cache.intern_node(
201
+ GreenNode.new(
202
+ kind: element.kind, children: children, flags: element.intrinsic_flags,
203
+ annotations: element.annotations
204
+ )
205
+ )
206
+ end
207
+ end
208
+ # rubocop:enable Naming/PredicateMethod
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "node" unless defined?(Ibex::Runtime::CST::GreenNode)
5
+
6
+ module Ibex
7
+ module Runtime
8
+ module CST
9
+ # Session-owned hash-consing cache for immutable Green elements.
10
+ class NodeCache
11
+ DEFAULT_NODE_ARITY_LIMIT = 3 #: Integer
12
+ DEFAULT_NODE_DESCENDANT_LIMIT = 32 #: Integer
13
+ empty_trivia = [] # @type var empty_trivia: Array[GreenTrivia]
14
+ EMPTY_TRIVIA = empty_trivia.freeze #: Array[GreenTrivia]
15
+
16
+ # @rbs @enabled: bool
17
+ # @rbs @node_arity_limit: Integer
18
+ # @rbs @node_descendant_limit: Integer
19
+ # @rbs @trivia: Hash[Integer, Array[GreenTrivia]]
20
+ # @rbs @tokens: Hash[Integer, Array[GreenToken]]
21
+ # @rbs @nodes: Hash[GreenNode, GreenNode]
22
+
23
+ # @rbs (?enabled: bool, ?node_arity_limit: Integer, ?node_descendant_limit: Integer) -> void
24
+ def initialize(
25
+ enabled: true, node_arity_limit: DEFAULT_NODE_ARITY_LIMIT,
26
+ node_descendant_limit: DEFAULT_NODE_DESCENDANT_LIMIT
27
+ )
28
+ raise ArgumentError, "node_arity_limit must be non-negative" if node_arity_limit.negative?
29
+ raise ArgumentError, "node_descendant_limit must be positive" unless node_descendant_limit.positive?
30
+
31
+ @enabled = enabled
32
+ @node_arity_limit = node_arity_limit
33
+ @node_descendant_limit = node_descendant_limit
34
+ @trivia = {}
35
+ @tokens = {}
36
+ @nodes = {}
37
+ end
38
+
39
+ # Intern trivia before constructing it, using byte-equivalent text within this session.
40
+ # @rbs (kind: Integer, text: String) -> GreenTrivia
41
+ def intern_trivia_fields(kind:, text:)
42
+ return GreenTrivia.new(kind: kind, text: text) unless @enabled
43
+
44
+ comparable_text = if text.encoding == Encoding::BINARY || text.ascii_only?
45
+ text
46
+ else
47
+ text.b.freeze
48
+ end
49
+ signature = kind.hash ^ comparable_text.hash
50
+ bucket = @trivia[signature]
51
+ existing = bucket&.find { |trivia| trivia.kind == kind && trivia.text == comparable_text }
52
+ return existing if existing
53
+
54
+ trivia = GreenTrivia.new(kind: kind, text: comparable_text)
55
+ (@trivia[signature] ||= []) << trivia
56
+ trivia
57
+ end
58
+
59
+ # @rbs (GreenToken token) -> GreenToken
60
+ def intern_token(token)
61
+ return token unless @enabled
62
+ return token unless token.flags.nobits?(Flags::HAS_ANNOTATION)
63
+
64
+ signature = token_signature(
65
+ token.kind, token.text, token.leading, token.trailing, token.flags, token.expected_kind
66
+ )
67
+ bucket = @tokens[signature]
68
+ existing = find_token(
69
+ bucket, token.kind, token.text, token.leading, token.trailing, token.flags, token.expected_kind
70
+ )
71
+ return existing if existing
72
+
73
+ (@tokens[signature] ||= []) << token
74
+ token
75
+ end
76
+
77
+ # Intern a token before constructing it, avoiding discarded duplicate immutable values.
78
+ # @rbs (kind: Integer, text: String, ?leading: Array[GreenTrivia], ?trailing: Array[GreenTrivia],
79
+ # ?flags: Integer, ?expected_kind: Integer?) -> GreenToken
80
+ def intern_token_fields(
81
+ kind:, text:, leading: EMPTY_TRIVIA, trailing: EMPTY_TRIVIA, flags: 0, expected_kind: nil
82
+ )
83
+ unless @enabled && flags.nobits?(Flags::HAS_ANNOTATION)
84
+ return GreenToken.new(
85
+ kind: kind, text: text, leading: leading, trailing: trailing,
86
+ flags: flags, expected_kind: expected_kind
87
+ )
88
+ end
89
+
90
+ comparable_text = if text.encoding == Encoding::BINARY || text.ascii_only?
91
+ text
92
+ else
93
+ text.b.freeze
94
+ end
95
+ signature = token_signature(kind, comparable_text, leading, trailing, flags, expected_kind)
96
+ bucket = @tokens[signature]
97
+ existing = find_token(bucket, kind, comparable_text, leading, trailing, flags, expected_kind)
98
+ return existing if existing
99
+
100
+ token = GreenToken.new(
101
+ kind: kind, text: comparable_text, leading: leading, trailing: trailing,
102
+ flags: flags, expected_kind: expected_kind
103
+ )
104
+ (@tokens[signature] ||= []) << token
105
+ token
106
+ end
107
+
108
+ # @rbs (GreenNode node) -> GreenNode
109
+ def intern_node(node)
110
+ return node unless @enabled
111
+ return node if node.children.length > @node_arity_limit
112
+ return node if node.descendant_count > @node_descendant_limit
113
+ return node unless node.flags.nobits?(Flags::HAS_ANNOTATION)
114
+
115
+ @nodes[node] ||= node
116
+ end
117
+
118
+ # @rbs () -> void
119
+ def clear
120
+ @trivia.clear
121
+ @tokens.clear
122
+ @nodes.clear
123
+ end
124
+
125
+ private
126
+
127
+ # @rbs (Integer kind, String text, Array[GreenTrivia] leading, Array[GreenTrivia] trailing,
128
+ # Integer flags, Integer? expected_kind) -> Integer
129
+ def token_signature(kind, text, leading, trailing, flags, expected_kind)
130
+ kind.hash ^ text.hash ^ leading.hash ^ trailing.hash ^ flags.hash ^ expected_kind.hash
131
+ end
132
+
133
+ # @rbs (Array[GreenToken]? bucket, Integer kind, String text, Array[GreenTrivia] leading,
134
+ # Array[GreenTrivia] trailing, Integer flags, Integer? expected_kind) -> GreenToken?
135
+ def find_token(bucket, kind, text, leading, trailing, flags, expected_kind)
136
+ return unless bucket
137
+
138
+ bucket.find do |token|
139
+ token.kind == kind &&
140
+ token.text == text &&
141
+ token.leading == leading &&
142
+ token.trailing == trailing &&
143
+ token.flags == flags &&
144
+ token.expected_kind == expected_kind
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "token" unless defined?(Ibex::Runtime::CST::GreenToken)
5
+
6
+ module Ibex
7
+ module Runtime
8
+ module CST
9
+ # Immutable position-independent syntax node.
10
+ class GreenNode
11
+ # @rbs! type child = GreenNode | GreenToken
12
+
13
+ attr_reader :kind #: Integer
14
+ attr_reader :children #: Array[child]
15
+ attr_reader :flags #: Integer
16
+ attr_reader :intrinsic_flags #: Integer
17
+ attr_reader :annotations #: Array[SyntaxAnnotation]
18
+ attr_reader :full_width #: Integer
19
+ attr_reader :leading_width #: Integer
20
+ attr_reader :trailing_width #: Integer
21
+ attr_reader :descendant_count #: Integer
22
+
23
+ # @rbs (kind: Integer, children: Array[child], ?flags: Integer, ?annotations: Array[SyntaxAnnotation]) -> void
24
+ def initialize(kind:, children:, flags: 0, annotations: [])
25
+ @kind = kind
26
+ @children = children.dup.freeze
27
+ @annotations = annotations.dup.freeze
28
+ @intrinsic_flags = flags
29
+ @intrinsic_flags |= Flags::HAS_ANNOTATION unless @annotations.empty?
30
+ @flags = @children.reduce(@intrinsic_flags) { |value, child| value | child.flags }
31
+ @full_width = @children.sum(&:full_width)
32
+ @leading_width = edge_width(@children, :leading_width)
33
+ @trailing_width = edge_width(@children.reverse_each, :trailing_width)
34
+ @descendant_count = 1 + @children.sum(&:descendant_count)
35
+ freeze
36
+ end
37
+
38
+ # @rbs () -> String
39
+ def to_source
40
+ source = String.new(encoding: Encoding::BINARY)
41
+ pending = @children.reverse
42
+ until pending.empty?
43
+ child = pending.pop || raise("green source traversal underflow")
44
+ if child.is_a?(GreenNode)
45
+ child.children.reverse_each { |nested| pending << nested }
46
+ next
47
+ end
48
+
49
+ child.leading.each { |trivia| source << trivia.text }
50
+ source << child.text
51
+ child.trailing.each { |trivia| source << trivia.text }
52
+ end
53
+ source
54
+ end
55
+
56
+ # @rbs (untyped other) -> bool
57
+ def ==(other)
58
+ other.is_a?(GreenNode) && @kind == other.kind && @intrinsic_flags == other.intrinsic_flags &&
59
+ @annotations == other.annotations && @children == other.children
60
+ end
61
+ alias eql? ==
62
+
63
+ # @rbs () -> Integer
64
+ def hash = [@kind, @children, @intrinsic_flags, @annotations].hash
65
+
66
+ private
67
+
68
+ # @rbs (Enumerable[child] children, Symbol reader) -> Integer
69
+ def edge_width(children, reader)
70
+ child = children.find { |candidate| candidate.full_width.positive? }
71
+ return 0 unless child
72
+
73
+ reader == :leading_width ? child.leading_width : child.trailing_width
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "../kind" unless defined?(Ibex::Runtime::CST::Flags)
5
+ require_relative "trivia" unless defined?(Ibex::Runtime::CST::GreenTrivia)
6
+
7
+ module Ibex
8
+ module Runtime
9
+ module CST
10
+ # Immutable position-independent terminal occurrence.
11
+ class GreenToken
12
+ empty_trivia = [] # @type var empty_trivia: Array[GreenTrivia]
13
+ EMPTY_TRIVIA = empty_trivia.freeze #: Array[GreenTrivia]
14
+
15
+ attr_reader :kind #: Integer
16
+ attr_reader :text #: String
17
+ attr_reader :leading #: Array[GreenTrivia]
18
+ attr_reader :trailing #: Array[GreenTrivia]
19
+ attr_reader :flags #: Integer
20
+ attr_reader :full_width #: Integer
21
+ attr_reader :leading_width #: Integer
22
+ attr_reader :trailing_width #: Integer
23
+ attr_reader :expected_kind #: Integer?
24
+ attr_reader :hash #: Integer
25
+
26
+ # @rbs (kind: Integer, text: String, ?leading: Array[GreenTrivia], ?trailing: Array[GreenTrivia],
27
+ # ?flags: Integer, ?expected_kind: Integer?) -> void
28
+ def initialize(
29
+ kind:, text:, leading: EMPTY_TRIVIA, trailing: EMPTY_TRIVIA, flags: 0, expected_kind: nil
30
+ )
31
+ @kind = kind
32
+ @text = text.encoding == Encoding::BINARY && text.frozen? ? text : text.b.freeze
33
+ @leading = leading.frozen? ? leading : leading.dup.freeze
34
+ @trailing = trailing.frozen? ? trailing : trailing.dup.freeze
35
+ @flags = flags
36
+ @expected_kind = expected_kind
37
+ @leading_width = trivia_width(@leading)
38
+ @trailing_width = trivia_width(@trailing)
39
+ @full_width = @leading_width + @text.bytesize + @trailing_width
40
+ @hash = @kind.hash ^ @text.hash ^ @leading.hash ^ @trailing.hash ^ @flags.hash ^ @expected_kind.hash
41
+ freeze
42
+ end
43
+
44
+ # @rbs (kind: Integer, expected_kind: Integer) -> GreenToken
45
+ def self.missing(kind:, expected_kind:)
46
+ new(
47
+ kind: kind, text: "", expected_kind: expected_kind,
48
+ flags: Flags::CONTAINS_MISSING | Flags::SYNTHETIC
49
+ )
50
+ end
51
+
52
+ # @rbs (kind: Integer, text: String, ?leading: Array[GreenTrivia]) -> GreenToken
53
+ def self.lexical_error(kind:, text:, leading: [])
54
+ new(kind: kind, text: text, leading: leading, flags: Flags::CONTAINS_ERROR)
55
+ end
56
+
57
+ # @rbs () -> Integer
58
+ def descendant_count = 1
59
+
60
+ # @rbs () -> String
61
+ def to_source
62
+ source = String.new(encoding: Encoding::BINARY)
63
+ @leading.each { |trivia| source << trivia.text }
64
+ source << @text
65
+ @trailing.each { |trivia| source << trivia.text }
66
+ source
67
+ end
68
+
69
+ # @rbs (untyped other) -> bool
70
+ def ==(other)
71
+ other.is_a?(GreenToken) &&
72
+ @kind == other.kind &&
73
+ @text == other.text &&
74
+ @leading == other.leading &&
75
+ @trailing == other.trailing &&
76
+ @flags == other.flags &&
77
+ @expected_kind == other.expected_kind
78
+ end
79
+ alias eql? ==
80
+
81
+ # @rbs () -> Integer
82
+ private
83
+
84
+ # @rbs (Array[GreenTrivia] trivia) -> Integer
85
+ def trivia_width(trivia)
86
+ trivia.sum(&:full_width)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ module CST
7
+ # Immutable source text ignored by the grammar but owned by a token.
8
+ class GreenTrivia
9
+ attr_reader :kind #: Integer
10
+ attr_reader :text #: String
11
+ attr_reader :full_width #: Integer
12
+ attr_reader :hash #: Integer
13
+
14
+ # @rbs (kind: Integer, text: String) -> void
15
+ def initialize(kind:, text:)
16
+ @kind = kind
17
+ @text = text.b.freeze
18
+ @full_width = @text.bytesize
19
+ @hash = [@kind, @text].hash
20
+ freeze
21
+ end
22
+
23
+ # @rbs () -> String
24
+ def to_source = @text
25
+
26
+ # @rbs (untyped other) -> bool
27
+ def ==(other)
28
+ other.is_a?(GreenTrivia) && @kind == other.kind && @text == other.text
29
+ end
30
+ alias eql? ==
31
+
32
+ # @rbs () -> Integer
33
+ end
34
+ end
35
+ end
36
+ end