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,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
|