flexr 1.0.0 → 1.1.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 +4 -4
- data/Rakefile +74 -28
- data/benchmark/golden/calculator_lexer.sha256 +1 -1
- data/benchmark/golden/json_lexer.sha256 +1 -1
- data/benchmark/golden/regexp_tokenizer.sha256 +1 -1
- data/benchmark/golden/ruby_subset_lexer.sha256 +1 -1
- data/benchmark/golden/toy_lang_lexer.sha256 +1 -1
- data/benchmark/golden/with_lrama_lexer.sha256 +1 -1
- data/benchmark/golden/with_racc_lexer.sha256 +1 -1
- data/docs/README.md +2 -1
- data/docs/explanation/backends.md +13 -10
- data/docs/explanation/matching-semantics.md +4 -2
- data/docs/explanation/security-model.md +10 -3
- data/docs/explanation/unicode-and-encoding.md +3 -1
- data/docs/how-to/deploy-a-standalone-lexer.md +5 -3
- data/docs/how-to/generate-a-lexer.md +3 -3
- data/docs/how-to/handle-errors.md +4 -1
- data/docs/how-to/run-a-lexer-at-runtime.md +5 -0
- data/docs/how-to/track-token-locations.md +2 -1
- data/docs/how-to/tune-performance.md +3 -3
- data/docs/perf-log.md +16 -0
- data/docs/reference/actions.md +5 -2
- data/docs/reference/diagnostics.md +8 -0
- data/docs/reference/dsl.md +19 -5
- data/docs/reference/errors.md +6 -3
- data/docs/reference/generated-artifacts.md +29 -2
- data/docs/reference/public-api.md +7 -2
- data/docs/reference/regexp.md +8 -6
- data/docs/reference/runtime.md +20 -3
- data/docs/reference/tokens-and-locations.md +6 -4
- data/docs/releases/v1.1.0.md +44 -0
- data/lib/flexr/action_resolver.rb +15 -0
- data/lib/flexr/artifact_writer.rb +61 -0
- data/lib/flexr/automaton/accel.rb +9 -4
- data/lib/flexr/automaton/analysis.rb +47 -6
- data/lib/flexr/automaton/backend_cost_model.rb +40 -0
- data/lib/flexr/automaton/compiler.rb +50 -71
- data/lib/flexr/automaton/dfa.rb +106 -19
- data/lib/flexr/automaton/minimizer.rb +79 -31
- data/lib/flexr/automaton/nfa.rb +45 -13
- data/lib/flexr/automaton/types.rb +14 -0
- data/lib/flexr/cli.rb +11 -5
- data/lib/flexr/codegen/direct.rb +5 -41
- data/lib/flexr/codegen/table.rb +175 -69
- data/lib/flexr/codegen.rb +8 -0
- data/lib/flexr/configuration.rb +37 -0
- data/lib/flexr/dsl.rb +105 -39
- data/lib/flexr/errors.rb +1 -0
- data/lib/flexr/generated.rb +61 -35
- data/lib/flexr/generator.rb +47 -82
- data/lib/flexr/importer.rb +4 -40
- data/lib/flexr/options.rb +5 -3
- data/lib/flexr/rake_task.rb +6 -1
- data/lib/flexr/regexp/ast.rb +5 -2
- data/lib/flexr/regexp/normalizer.rb +62 -16
- data/lib/flexr/regexp/parser.rb +121 -46
- data/lib/flexr/regexp/tokenizer.rb +192 -67
- data/lib/flexr/runtime/buffer.rb +115 -9
- data/lib/flexr/runtime/core.rb +214 -47
- data/lib/flexr/runtime/errors.rb +64 -4
- data/lib/flexr/runtime/interpreter.rb +178 -71
- data/lib/flexr/runtime.rb +70 -0
- data/lib/flexr/source/passthrough.rb +62 -9
- data/lib/flexr/source/prism_reader.rb +175 -46
- data/lib/flexr/source/static_eval.rb +40 -4
- data/lib/flexr/source.rb +6 -0
- data/lib/flexr/unicode/data/properties.rb +1 -1
- data/lib/flexr/unicode/data.rb +11 -0
- data/lib/flexr/unicode/property.rb +3 -5
- data/lib/flexr/unicode/reference_regexp.rb +4 -0
- data/lib/flexr/unicode/version.rb +7 -0
- data/lib/flexr/version.rb +1 -1
- data/lib/flexr.rb +9 -76
- data/tools/coverage.rb +6 -1
- metadata +12 -1
|
@@ -4,12 +4,12 @@ module Flexr
|
|
|
4
4
|
module Source
|
|
5
5
|
RuleDefinition = Struct.new(
|
|
6
6
|
:index, :patterns, :trailing, :action_source, :action, :states, :bol_only, :end_anchor,
|
|
7
|
-
:pattern_conditions,
|
|
7
|
+
:pattern_conditions, :bind_action,
|
|
8
8
|
:span, keyword_init: true
|
|
9
9
|
)
|
|
10
10
|
SpecSource = Struct.new(
|
|
11
11
|
:source, :path, :class_name, :rules, :config, :dsl_spans, :first_dsl_offset, :constants,
|
|
12
|
-
:states, keyword_init: true
|
|
12
|
+
:states, :flexr_require_spans, :dsl_edits, keyword_init: true
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
class PrismReader
|
|
@@ -19,9 +19,11 @@ module Flexr
|
|
|
19
19
|
@source = source
|
|
20
20
|
@path = path
|
|
21
21
|
@constants = {}
|
|
22
|
-
@constant_definitions =
|
|
22
|
+
@constant_definitions = []
|
|
23
|
+
@resolved_constant_definitions = {}
|
|
23
24
|
@rules = []
|
|
24
25
|
@spans = []
|
|
26
|
+
@edits = []
|
|
25
27
|
@states = { initial: { inclusive: true } }
|
|
26
28
|
@eof_rules = {}
|
|
27
29
|
@config = { backend: :table, token_kind: :array, encoding: Encoding::UTF_8, declared_tokens: [] }
|
|
@@ -47,20 +49,24 @@ module Flexr
|
|
|
47
49
|
|
|
48
50
|
@lexer_scope = class_name.to_s.delete_prefix("::").split("::").reject(&:empty?)
|
|
49
51
|
collect_constants(prism.value)
|
|
50
|
-
resolve_constants
|
|
51
52
|
collect_body(klass.body, [:initial])
|
|
53
|
+
require_spans = flexr_require_spans(prism.value)
|
|
52
54
|
first_dsl_offset = @spans.map(&:first).min || class_body_offset(klass)
|
|
53
55
|
@config[:eof_rules] = @eof_rules
|
|
54
56
|
SpecSource.new(source: @source, path: @path, class_name: class_name, rules: @rules,
|
|
55
57
|
config: @config, dsl_spans: @spans.uniq(&:first),
|
|
56
|
-
first_dsl_offset: first_dsl_offset, constants: @constants, states: @states
|
|
58
|
+
first_dsl_offset: first_dsl_offset, constants: @constants, states: @states,
|
|
59
|
+
flexr_require_spans: require_spans, dsl_edits: @edits)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
private
|
|
60
63
|
|
|
61
64
|
def find_lexer_class(node, namespace = [])
|
|
62
65
|
candidates = lexer_class_candidates(node, namespace)
|
|
63
|
-
candidates.
|
|
66
|
+
dsl_candidates = candidates.select { |candidate, _name| lexer_has_dsl?(candidate.body) }
|
|
67
|
+
unsupported_static!("multiple lexer classes require an explicit selection") if dsl_candidates.length > 1
|
|
68
|
+
|
|
69
|
+
dsl_candidates.first || candidates.first
|
|
64
70
|
end
|
|
65
71
|
|
|
66
72
|
def lexer_class_candidates(node, namespace = [])
|
|
@@ -99,6 +105,11 @@ module Flexr
|
|
|
99
105
|
return unless node
|
|
100
106
|
|
|
101
107
|
kind = node.class.name.split("::").last
|
|
108
|
+
if kind == "ProgramNode"
|
|
109
|
+
collect_constants(node.statements, scope: scope)
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
102
113
|
if %w[ClassNode ModuleNode].include?(kind)
|
|
103
114
|
nested_scope = scope + namespace_parts(node.constant_path)
|
|
104
115
|
collect_constants(node.body, scope: nested_scope)
|
|
@@ -107,28 +118,30 @@ module Flexr
|
|
|
107
118
|
|
|
108
119
|
if kind.end_with?("ConstantWriteNode")
|
|
109
120
|
name = qualify_constant(scope, node.name)
|
|
110
|
-
@constant_definitions[name
|
|
121
|
+
@constant_definitions << [name, node.value, scope.dup, node.location.start_offset]
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if kind == "StatementsNode"
|
|
126
|
+
node.body.each { |child| collect_constants(child, scope: scope) }
|
|
127
|
+
return
|
|
111
128
|
end
|
|
112
129
|
|
|
113
|
-
node.
|
|
130
|
+
return unless kind == "CallNode" && node.receiver.nil? && %i[state all_states].include?(node.name.to_sym)
|
|
131
|
+
|
|
132
|
+
collect_constants(node.block&.body, scope: scope)
|
|
114
133
|
end
|
|
115
134
|
|
|
116
|
-
def resolve_constants
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
resolved = true
|
|
127
|
-
rescue StaticResolutionError
|
|
128
|
-
# Keep unresolved definitions out of the table. If a DSL expression
|
|
129
|
-
# refers to one later, StaticEval emits the normal FLEXR-E017.
|
|
130
|
-
end
|
|
131
|
-
break unless resolved
|
|
135
|
+
def resolve_constants(before_offset:)
|
|
136
|
+
@constant_definitions.sort_by(&:last).each do |name, value_node, scope, offset|
|
|
137
|
+
next if offset >= before_offset || @resolved_constant_definitions[offset]
|
|
138
|
+
|
|
139
|
+
@resolved_constant_definitions[offset] = true
|
|
140
|
+
value = StaticEval.new(@source, constants: @constants, scope: scope).call(value_node)
|
|
141
|
+
@constants[name] = value
|
|
142
|
+
rescue StaticResolutionError
|
|
143
|
+
# Preserve dynamic constants as ordinary Ruby. A later DSL reference
|
|
144
|
+
# still receives FLEXR-E017 instead of seeing a forward value.
|
|
132
145
|
end
|
|
133
146
|
end
|
|
134
147
|
|
|
@@ -143,6 +156,7 @@ module Flexr
|
|
|
143
156
|
def collect_body(node, states)
|
|
144
157
|
body = node.respond_to?(:body) ? node.body : []
|
|
145
158
|
Array(body).each do |child|
|
|
159
|
+
resolve_constants(before_offset: child.location.start_offset)
|
|
146
160
|
collect_call(child, states)
|
|
147
161
|
end
|
|
148
162
|
end
|
|
@@ -150,19 +164,24 @@ module Flexr
|
|
|
150
164
|
def collect_call(node, states)
|
|
151
165
|
return unless node
|
|
152
166
|
return collect_body(node, states) if node.class.name.end_with?("StatementsNode")
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
unless node.class.name.end_with?("CallNode") && node.receiver.nil?
|
|
168
|
+
reject_unhandled_dsl!(node)
|
|
169
|
+
return
|
|
170
|
+
end
|
|
155
171
|
|
|
156
172
|
name = node.name.to_sym
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
unless DSL_NAMES.include?(name)
|
|
174
|
+
reject_unhandled_dsl!(node)
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
record_deletion(node) unless %i[rule state all_states].include?(name)
|
|
159
178
|
case name
|
|
160
179
|
when :rule
|
|
161
180
|
collect_rule(node, states)
|
|
162
181
|
when :state
|
|
163
182
|
collect_state(node, states)
|
|
164
183
|
when :all_states
|
|
165
|
-
|
|
184
|
+
collect_all_states(node, states)
|
|
166
185
|
when :on_eof
|
|
167
186
|
action_source = node.block && source_slice(node.block)
|
|
168
187
|
action = if action_source
|
|
@@ -173,28 +192,50 @@ module Flexr
|
|
|
173
192
|
values = positional(node).map { |item| static(item) }.compact.flatten
|
|
174
193
|
@config[:declared_tokens].concat(values.map(&:to_sym))
|
|
175
194
|
when :backend
|
|
176
|
-
@config[:backend] = static(positional(node).first)
|
|
195
|
+
@config[:backend] = Configuration.backend!(static(positional(node).first))
|
|
177
196
|
when :token_kind
|
|
178
|
-
@config[:token_kind] = static(positional(node).first)
|
|
197
|
+
@config[:token_kind] = Configuration.token_kind!(static(positional(node).first))
|
|
179
198
|
when :encoding
|
|
180
199
|
@config[:encoding] = static(positional(node).first)
|
|
181
200
|
when :option
|
|
182
201
|
@config[:options] ||= {}
|
|
183
|
-
positional(node).each { |item| @config[:options][static(item)
|
|
202
|
+
positional(node).each { |item| @config[:options][Configuration.option!(static(item))] = true }
|
|
184
203
|
when :accel
|
|
185
204
|
@config[:options] ||= {}
|
|
186
|
-
@config[:options][:accel] = static(positional(node).first)
|
|
205
|
+
@config[:options][:accel] = Configuration.accelerator!(static(positional(node).first))
|
|
187
206
|
end
|
|
188
207
|
end
|
|
189
208
|
|
|
190
209
|
def collect_state(node, parent_states)
|
|
210
|
+
block = node.block
|
|
211
|
+
raise ArgumentError, "state requires a block" unless block
|
|
212
|
+
|
|
191
213
|
args = positional(node)
|
|
192
214
|
names = args.map { |argument| static(argument, scope: @lexer_scope).to_sym }
|
|
193
215
|
names = parent_states if node.name.to_sym == :all_states
|
|
216
|
+
raise ArgumentError, "state requires a name" if names.empty?
|
|
217
|
+
|
|
218
|
+
validate_keywords!(node, %i[inclusive])
|
|
194
219
|
inclusive = keyword(node, :inclusive) ? static(keyword(node, :inclusive), scope: @lexer_scope) : false
|
|
195
|
-
names.each
|
|
220
|
+
names.each do |name|
|
|
221
|
+
existing = @states[name]
|
|
222
|
+
raise ArgumentError, "state #{name.inspect} already declared with inclusive: #{existing[:inclusive]}" if
|
|
223
|
+
existing && existing[:inclusive] != inclusive
|
|
224
|
+
@states[name] ||= { inclusive: inclusive }
|
|
225
|
+
end
|
|
226
|
+
record_state_wrapper(node, block)
|
|
227
|
+
active_states = parent_states == [:initial] ? names : (parent_states + names).uniq
|
|
228
|
+
collect_body(block.body, active_states)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def collect_all_states(node, parent_states)
|
|
196
232
|
block = node.block
|
|
197
|
-
|
|
233
|
+
raise ArgumentError, "all_states requires a block" unless block
|
|
234
|
+
|
|
235
|
+
validate_keywords!(node, [])
|
|
236
|
+
unsupported_static!("all_states does not accept arguments") unless positional(node).empty?
|
|
237
|
+
record_state_wrapper(node, block)
|
|
238
|
+
collect_body(block.body, (parent_states + @states.keys).uniq)
|
|
198
239
|
end
|
|
199
240
|
|
|
200
241
|
def collect_rule(node, states)
|
|
@@ -206,6 +247,7 @@ module Flexr
|
|
|
206
247
|
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
207
248
|
end
|
|
208
249
|
options = keywords(node)
|
|
250
|
+
validate_keywords!(node, %i[skip emit followed_by])
|
|
209
251
|
action_source = node.block && source_slice(node.block)
|
|
210
252
|
if action_source&.match?(/\breject\b/)
|
|
211
253
|
diagnostic = Diagnostics.error(
|
|
@@ -214,25 +256,26 @@ module Flexr
|
|
|
214
256
|
)
|
|
215
257
|
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
216
258
|
end
|
|
217
|
-
|
|
259
|
+
block_action = if action_source
|
|
218
260
|
action_source.start_with?("do") ? "proc #{action_source}" : "proc#{action_source}"
|
|
219
261
|
end
|
|
262
|
+
skip = options.key?(:skip) ? static(options[:skip], scope: @lexer_scope) : false
|
|
263
|
+
emit = options.key?(:emit) ? static(options[:emit], scope: @lexer_scope) : nil
|
|
264
|
+
action = ActionResolver.resolve(
|
|
265
|
+
skip: skip, emit: emit, block: block_action, default: "proc { emit(nil, text) }"
|
|
266
|
+
)
|
|
267
|
+
bind_action = !block_action.nil? && action.equal?(block_action)
|
|
268
|
+
rule_index = @rules.length
|
|
269
|
+
record_rule_edit(node, rule_index, bind_action ? block_action : nil)
|
|
220
270
|
@rules << RuleDefinition.new(
|
|
221
|
-
index:
|
|
271
|
+
index: rule_index, patterns: patterns,
|
|
222
272
|
trailing: options[:followed_by] && static(options[:followed_by], scope: @lexer_scope),
|
|
223
|
-
action_source: action_source, action: action
|
|
273
|
+
action_source: action_source, action: action, states: states.dup,
|
|
224
274
|
bol_only: false, end_anchor: nil,
|
|
225
|
-
span: [node.location.start_offset, node.location.end_offset]
|
|
275
|
+
span: [node.location.start_offset, node.location.end_offset], bind_action: bind_action
|
|
226
276
|
)
|
|
227
277
|
end
|
|
228
278
|
|
|
229
|
-
def action_for(options)
|
|
230
|
-
return :skip if options[:skip] && static(options[:skip])
|
|
231
|
-
return [:emit, static(options[:emit]).to_sym] if options[:emit]
|
|
232
|
-
|
|
233
|
-
"proc { emit(nil, text) }"
|
|
234
|
-
end
|
|
235
|
-
|
|
236
279
|
def static(node, scope: @lexer_scope)
|
|
237
280
|
StaticEval.new(@source, constants: @constants, scope: scope).call(node)
|
|
238
281
|
rescue StaticResolutionError
|
|
@@ -254,6 +297,68 @@ module Flexr
|
|
|
254
297
|
keywords(node)[key]
|
|
255
298
|
end
|
|
256
299
|
|
|
300
|
+
def validate_keywords!(node, allowed)
|
|
301
|
+
unknown = keywords(node).keys - allowed
|
|
302
|
+
unsupported_static!("unknown keyword #{unknown.first.inspect} for #{node.name}") unless unknown.empty?
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def record_deletion(node)
|
|
306
|
+
span = [node.location.start_offset, node.location.end_offset]
|
|
307
|
+
@spans << span
|
|
308
|
+
@edits << [*span, ""]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def record_rule_edit(node, rule_index, block_action)
|
|
312
|
+
span = [node.location.start_offset, node.location.end_offset]
|
|
313
|
+
replacement = block_action ? "__flexr_bind_generated_action(#{rule_index}, #{block_action})" : ""
|
|
314
|
+
@spans << span
|
|
315
|
+
@edits << [*span, replacement]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def record_state_wrapper(node, block)
|
|
319
|
+
body = block.body
|
|
320
|
+
return record_deletion(node) unless body
|
|
321
|
+
|
|
322
|
+
body_start = Source::Passthrough.line_start_offset(@source, body.location.start_offset)
|
|
323
|
+
opening = [node.location.start_offset, body_start]
|
|
324
|
+
closing = [body.location.end_offset, node.location.end_offset]
|
|
325
|
+
opening_source = @source.byteslice(block.location.start_offset...body_start)
|
|
326
|
+
closing_source = @source.byteslice(body.location.end_offset...block.location.end_offset)
|
|
327
|
+
@spans.push(opening, closing)
|
|
328
|
+
@edits << [*opening, "(::Kernel.proc #{opening_source}"]
|
|
329
|
+
@edits << [*closing, "#{closing_source}).call"]
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def reject_unhandled_dsl!(node)
|
|
333
|
+
each_node_including_self(node) do |child|
|
|
334
|
+
next unless child.class.name.end_with?("CallNode")
|
|
335
|
+
if %i[send public_send].include?(child.name.to_sym)
|
|
336
|
+
dynamic_name = dynamically_dispatched_dsl(child)
|
|
337
|
+
unsupported_static!("dynamic dispatch of #{dynamic_name} is not statically supported") if dynamic_name
|
|
338
|
+
end
|
|
339
|
+
next unless DSL_NAMES.include?(child.name.to_sym)
|
|
340
|
+
next unless child.receiver.nil? || child.receiver.class.name.end_with?("SelfNode")
|
|
341
|
+
|
|
342
|
+
unsupported_static!("#{child.name} must be a direct receiverless statement in a lexer or state body")
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def dynamically_dispatched_dsl(node)
|
|
347
|
+
first_argument = Array(node.arguments&.arguments).first
|
|
348
|
+
return unless first_argument && first_argument.class.name.end_with?("SymbolNode")
|
|
349
|
+
|
|
350
|
+
name = first_argument.unescaped.to_sym
|
|
351
|
+
name if DSL_NAMES.include?(name)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def unsupported_static!(reason)
|
|
355
|
+
diagnostic = Diagnostics.error(
|
|
356
|
+
"FLEXR-E017", "DSL structure is not statically supported",
|
|
357
|
+
help: "move the DSL call to the lexer or state body, or use --eval", note: reason
|
|
358
|
+
)
|
|
359
|
+
raise StaticResolutionError.new(diagnostic.message, diagnostic: diagnostic)
|
|
360
|
+
end
|
|
361
|
+
|
|
257
362
|
def each_node(node, &block)
|
|
258
363
|
return unless node
|
|
259
364
|
|
|
@@ -263,6 +368,30 @@ module Flexr
|
|
|
263
368
|
end
|
|
264
369
|
end
|
|
265
370
|
|
|
371
|
+
def flexr_require_spans(node)
|
|
372
|
+
spans = []
|
|
373
|
+
each_node_including_self(node) do |child|
|
|
374
|
+
next unless child.class.name.end_with?("CallNode")
|
|
375
|
+
next unless child.receiver.nil? && child.name.to_sym == :require
|
|
376
|
+
|
|
377
|
+
arguments = Array(child.arguments&.arguments)
|
|
378
|
+
next unless arguments.one?
|
|
379
|
+
|
|
380
|
+
argument = arguments.first
|
|
381
|
+
next unless argument.class.name.end_with?("StringNode") && argument.unescaped == "flexr"
|
|
382
|
+
|
|
383
|
+
spans << [child.location.start_offset, child.location.end_offset]
|
|
384
|
+
end
|
|
385
|
+
spans
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def each_node_including_self(node, &block)
|
|
389
|
+
return unless node
|
|
390
|
+
|
|
391
|
+
block.call(node)
|
|
392
|
+
node.child_nodes.compact.each { |child| each_node_including_self(child, &block) }
|
|
393
|
+
end
|
|
394
|
+
|
|
266
395
|
def source_slice(node)
|
|
267
396
|
return "" unless node
|
|
268
397
|
|
|
@@ -45,7 +45,15 @@ module Flexr
|
|
|
45
45
|
when "NilNode"
|
|
46
46
|
nil
|
|
47
47
|
when "ArrayNode"
|
|
48
|
-
node.elements.
|
|
48
|
+
node.elements.flat_map do |element|
|
|
49
|
+
if element.class.name.end_with?("SplatNode")
|
|
50
|
+
Array(evaluate(element.expression))
|
|
51
|
+
else
|
|
52
|
+
[evaluate(element)]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
when "HashNode"
|
|
56
|
+
evaluate_hash(node)
|
|
49
57
|
when "ConstantReadNode"
|
|
50
58
|
evaluate_constant([node.name.to_s], absolute: false)
|
|
51
59
|
when "ConstantPathNode"
|
|
@@ -66,8 +74,10 @@ module Flexr
|
|
|
66
74
|
kind = part.class.name.split("::").last
|
|
67
75
|
if kind == "EmbeddedStatementsNode"
|
|
68
76
|
body = part.statements&.body || []
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
raise_resolution("interpolation must contain exactly one expression") unless body.one?
|
|
78
|
+
|
|
79
|
+
value = evaluate(body.first)
|
|
80
|
+
value.is_a?(::Regexp) ? regexp_fragment(value) : value.to_s
|
|
71
81
|
else
|
|
72
82
|
evaluate(part).to_s
|
|
73
83
|
end
|
|
@@ -95,7 +105,7 @@ module Flexr
|
|
|
95
105
|
return value unless value.equal?(MISSING)
|
|
96
106
|
end
|
|
97
107
|
|
|
98
|
-
return ::Encoding.const_get(parts.last) if
|
|
108
|
+
return ::Encoding.const_get(parts.last) if parts.length == 2 && parts.first == "Encoding"
|
|
99
109
|
|
|
100
110
|
raise_resolution("constant #{parts.join('::')} is not statically known")
|
|
101
111
|
rescue NameError
|
|
@@ -122,6 +132,32 @@ module Flexr
|
|
|
122
132
|
raise_resolution("method call #{node.name} is not statically supported")
|
|
123
133
|
end
|
|
124
134
|
|
|
135
|
+
def evaluate_hash(node)
|
|
136
|
+
node.elements.each_with_object({}) do |element, result|
|
|
137
|
+
kind = element.class.name.split("::").last
|
|
138
|
+
if kind == "AssocNode"
|
|
139
|
+
result[evaluate(element.key)] = evaluate(element.value)
|
|
140
|
+
elsif kind == "AssocSplatNode"
|
|
141
|
+
value = evaluate(element.value)
|
|
142
|
+
raise_resolution("hash splat value must be a Hash") unless value.is_a?(Hash)
|
|
143
|
+
|
|
144
|
+
result.merge!(value)
|
|
145
|
+
else
|
|
146
|
+
raise_resolution("hash element #{kind} is not statically supported")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def regexp_fragment(regexp)
|
|
152
|
+
flags = {
|
|
153
|
+
"i" => ::Regexp::IGNORECASE,
|
|
154
|
+
"m" => ::Regexp::MULTILINE,
|
|
155
|
+
"x" => ::Regexp::EXTENDED
|
|
156
|
+
}
|
|
157
|
+
enabled, disabled = flags.partition { |_name, bit| regexp.options.anybits?(bit) }
|
|
158
|
+
"(?#{enabled.to_h.keys.join}-#{disabled.to_h.keys.join}:#{regexp.source})"
|
|
159
|
+
end
|
|
160
|
+
|
|
125
161
|
def positional_arguments(node)
|
|
126
162
|
Array(node.arguments&.arguments).reject { |argument| argument.class.name.end_with?("KeywordHashNode") }
|
|
127
163
|
end
|