flexr 1.0.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/.rubocop.yml +33 -0
- data/CONTRIBUTING.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +468 -0
- data/benchmark/baselines/json.json +34 -0
- data/benchmark/baselines/json_handwritten.rb +43 -0
- data/benchmark/baselines/json_rexical.rex +25 -0
- data/benchmark/corpora/README.md +11 -0
- data/benchmark/corpora/generate_json.rb +26 -0
- data/benchmark/golden/calculator_lexer.sha256 +1 -0
- data/benchmark/golden/json_lexer.sha256 +1 -0
- data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
- data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
- data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
- data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
- data/benchmark/golden/with_racc_lexer.sha256 +1 -0
- data/benchmark/run.rb +254 -0
- data/docs/README.md +64 -0
- data/docs/RELEASING.md +30 -0
- data/docs/adr/0001-byte-level-dfa.md +5 -0
- data/docs/adr/0003-leftmost-longest.md +4 -0
- data/docs/adr/0006-accel-not-scanner.md +4 -0
- data/docs/adr/0008-what-pure-ruby-means.md +5 -0
- data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
- data/docs/adr/0017-static-analysis-by-default.md +5 -0
- data/docs/adr/0018-prism-for-generator-only.md +4 -0
- data/docs/adr/0019-measured-performance-floor.md +26 -0
- data/docs/adr/0020-vendored-unicode-contract.md +21 -0
- data/docs/explanation/backends.md +33 -0
- data/docs/explanation/matching-semantics.md +20 -0
- data/docs/explanation/runtime-vs-generated.md +22 -0
- data/docs/explanation/security-model.md +18 -0
- data/docs/explanation/unicode-and-encoding.md +20 -0
- data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
- data/docs/how-to/generate-a-lexer.md +39 -0
- data/docs/how-to/handle-errors.md +32 -0
- data/docs/how-to/integrate-with-lrama.md +21 -0
- data/docs/how-to/integrate-with-racc.md +25 -0
- data/docs/how-to/migrate-from-flex.md +21 -0
- data/docs/how-to/migrate-from-rexical.md +23 -0
- data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
- data/docs/how-to/track-token-locations.md +27 -0
- data/docs/how-to/tune-performance.md +23 -0
- data/docs/how-to/use-states.md +36 -0
- data/docs/how-to/use-trailing-context.md +22 -0
- data/docs/internals/README.md +14 -0
- data/docs/perf-log.md +56 -0
- data/docs/reference/README.md +23 -0
- data/docs/reference/actions.md +47 -0
- data/docs/reference/cli.md +80 -0
- data/docs/reference/compatibility.md +38 -0
- data/docs/reference/diagnostics.md +41 -0
- data/docs/reference/dsl.md +81 -0
- data/docs/reference/errors.md +27 -0
- data/docs/reference/generated-artifacts.md +50 -0
- data/docs/reference/public-api.md +42 -0
- data/docs/reference/regexp.md +39 -0
- data/docs/reference/runtime.md +49 -0
- data/docs/reference/tokens-and-locations.md +33 -0
- data/docs/tutorial/build-a-calculator-lexer.md +96 -0
- data/examples/calculator/README.md +27 -0
- data/examples/calculator/lexer.flexr.rb +17 -0
- data/examples/json/README.md +30 -0
- data/examples/json/lexer.flexr.rb +24 -0
- data/examples/ruby_subset/README.md +17 -0
- data/examples/ruby_subset/lexer.flexr.rb +22 -0
- data/examples/toy_lang/README.md +17 -0
- data/examples/toy_lang/lexer.flexr.rb +18 -0
- data/examples/with_lrama/README.md +17 -0
- data/examples/with_lrama/lexer.flexr.rb +13 -0
- data/examples/with_racc/README.md +17 -0
- data/examples/with_racc/lexer.flexr.rb +13 -0
- data/exe/flexr +7 -0
- data/lib/flexr/automaton/accel.rb +39 -0
- data/lib/flexr/automaton/analysis.rb +38 -0
- data/lib/flexr/automaton/byte_class_set.rb +29 -0
- data/lib/flexr/automaton/compiler.rb +413 -0
- data/lib/flexr/automaton/dfa.rb +103 -0
- data/lib/flexr/automaton/minimizer.rb +70 -0
- data/lib/flexr/automaton/nfa.rb +92 -0
- data/lib/flexr/cli.rb +342 -0
- data/lib/flexr/codegen/base.rb +17 -0
- data/lib/flexr/codegen/direct.rb +52 -0
- data/lib/flexr/codegen/firstmatch.rb +17 -0
- data/lib/flexr/codegen/table.rb +158 -0
- data/lib/flexr/codegen/table_packer.rb +61 -0
- data/lib/flexr/diagnostics.rb +94 -0
- data/lib/flexr/dsl.rb +182 -0
- data/lib/flexr/errors.rb +28 -0
- data/lib/flexr/generated.rb +125 -0
- data/lib/flexr/generator.rb +400 -0
- data/lib/flexr/importer.rb +560 -0
- data/lib/flexr/ir.rb +36 -0
- data/lib/flexr/lexer.rb +10 -0
- data/lib/flexr/options.rb +47 -0
- data/lib/flexr/rake_task.rb +27 -0
- data/lib/flexr/regexp/ast.rb +45 -0
- data/lib/flexr/regexp/char_class.rb +7 -0
- data/lib/flexr/regexp/normalizer.rb +117 -0
- data/lib/flexr/regexp/parser.rb +517 -0
- data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
- data/lib/flexr/regexp/tokenizer.rb +168 -0
- data/lib/flexr/regexp/unsupported.rb +7 -0
- data/lib/flexr/runtime/buffer.rb +112 -0
- data/lib/flexr/runtime/core.rb +388 -0
- data/lib/flexr/runtime/errors.rb +22 -0
- data/lib/flexr/runtime/interpreter.rb +505 -0
- data/lib/flexr/runtime/location.rb +26 -0
- data/lib/flexr/runtime/token.rb +7 -0
- data/lib/flexr/source/passthrough.rb +31 -0
- data/lib/flexr/source/prism_reader.rb +283 -0
- data/lib/flexr/source/static_eval.rb +145 -0
- data/lib/flexr/unicode/case_fold.rb +45 -0
- data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
- data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
- data/lib/flexr/unicode/data/case_folding.rb +9 -0
- data/lib/flexr/unicode/data/properties.rb +10 -0
- data/lib/flexr/unicode/property.rb +107 -0
- data/lib/flexr/unicode/reference_regexp.rb +102 -0
- data/lib/flexr/unicode/utf8_splitter.rb +109 -0
- data/lib/flexr/version.rb +5 -0
- data/lib/flexr.rb +81 -0
- data/site/README.md +22 -0
- data/site/astro.config.mjs +57 -0
- data/site/package.json +19 -0
- data/site/pnpm-lock.yaml +5029 -0
- data/site/pnpm-workspace.yaml +6 -0
- data/site/public/playground.js +189 -0
- data/site/scripts/verify-site.mjs +42 -0
- data/site/src/content/docs/benchmarks.md +8 -0
- data/site/src/content/docs/concepts/matching-semantics.md +15 -0
- data/site/src/content/docs/concepts/regexp-model.md +18 -0
- data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
- data/site/src/content/docs/concepts/security-model.md +15 -0
- data/site/src/content/docs/examples.md +17 -0
- data/site/src/content/docs/learn/generation.md +29 -0
- data/site/src/content/docs/learn/getting-started.md +56 -0
- data/site/src/content/docs/learn/parser-integration.md +27 -0
- data/site/src/content/docs/learn/runtime-mode.md +32 -0
- data/site/src/content/docs/reference/action-context.md +20 -0
- data/site/src/content/docs/reference/cli.md +22 -0
- data/site/src/content/docs/reference/diagnostics.md +16 -0
- data/site/src/content/docs/reference/dsl.md +19 -0
- data/site/src/content/docs/reference/public-api.md +18 -0
- data/site/src/content/docs/reference/regexp.md +16 -0
- data/site/src/content/docs/reference/runtime.md +16 -0
- data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
- data/site/src/content.config.ts +12 -0
- data/site/src/env.d.ts +1 -0
- data/site/src/layouts/SiteLayout.astro +39 -0
- data/site/src/pages/index.astro +174 -0
- data/site/src/pages/playground.astro +64 -0
- data/site/src/styles/custom.css +711 -0
- data/site/tsconfig.json +5 -0
- data/tools/coverage.rb +32 -0
- data/tools/docs_verify.rb +116 -0
- data/tools/gen_unicode_tables.rb +202 -0
- data/tools/regexp_tokenizer_reference.rb +60 -0
- metadata +205 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
class Generator
|
|
5
|
+
attr_reader :diagnostics
|
|
6
|
+
|
|
7
|
+
INLINE_EMIT = /\Aemit(?:\s+(.+?))?\z/
|
|
8
|
+
INLINE_EMIT_ARGUMENTS = /\A(?::[A-Za-z_]\w*|true|false|nil|-?\d+(?:\.\d+)?|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|text(?:\.(?:to_f|to_i|bytesize|byteslice\([^()\n]+\)))?|lineno|line)(?:\s*,\s*(?::[A-Za-z_]\w*|true|false|nil|-?\d+(?:\.\d+)?|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|text(?:\.(?:to_f|to_i|bytesize|byteslice\([^()\n]+\)))?|lineno|line))*\z/
|
|
9
|
+
|
|
10
|
+
RUNTIME_SOURCES = %w[
|
|
11
|
+
version.rb errors.rb diagnostics.rb ir.rb
|
|
12
|
+
regexp/ast.rb regexp/parser.rb regexp/normalizer.rb regexp/unsupported.rb regexp/char_class.rb
|
|
13
|
+
unicode/utf8_splitter.rb unicode/data/properties.rb unicode/data/case_folding.rb unicode/property.rb unicode/reference_regexp.rb unicode/case_fold.rb
|
|
14
|
+
automaton/byte_class_set.rb automaton/nfa.rb automaton/dfa.rb automaton/compiler.rb
|
|
15
|
+
automaton/analysis.rb automaton/minimizer.rb automaton/accel.rb
|
|
16
|
+
runtime/location.rb runtime/token.rb runtime/buffer.rb runtime/errors.rb
|
|
17
|
+
runtime/interpreter.rb runtime/core.rb dsl.rb lexer.rb generated.rb
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(path, output: nil, eval_mode: false, options: {})
|
|
21
|
+
@path = path
|
|
22
|
+
@output = output
|
|
23
|
+
@eval_mode = eval_mode
|
|
24
|
+
@options = options
|
|
25
|
+
@diagnostics = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate
|
|
29
|
+
# Keep generated source stable when a checkout presents the spec with
|
|
30
|
+
# Windows line endings. All offsets consumed by the parser and
|
|
31
|
+
# passthrough writer are relative to this normalized source.
|
|
32
|
+
source = File.binread(@path).gsub(/\r\n?/, "\n").force_encoding(Encoding::UTF_8)
|
|
33
|
+
result = if @eval_mode
|
|
34
|
+
generate_from_runtime(source)
|
|
35
|
+
else
|
|
36
|
+
parsed = Source::PrismReader.new(source, path: @path).read
|
|
37
|
+
generate_static(parsed)
|
|
38
|
+
end
|
|
39
|
+
File.binwrite(@output, result) if @output
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def generate_static(parsed)
|
|
46
|
+
compiled = compile_parsed(parsed)
|
|
47
|
+
@diagnostics = Array(compiled.diagnostics)
|
|
48
|
+
validate_diagnostics!(compiled)
|
|
49
|
+
payload = generated_payload(parsed, compiled)
|
|
50
|
+
indent = Source::Passthrough.indentation(parsed.source, parsed.first_dsl_offset)
|
|
51
|
+
install = "Flexr::Generated.install_compiled!(self, #{payload})\n"
|
|
52
|
+
install = "#{install}#{Codegen::Table.new(compiled).source(indent: indent)}"
|
|
53
|
+
install = "#{install}#{generated_action_source(parsed, indent)}"
|
|
54
|
+
install = "#{install}#{Codegen::Direct.new(compiled).source(indent: indent)}#{indent}" if
|
|
55
|
+
effective_backend(parsed) == :direct
|
|
56
|
+
result = Source::Passthrough.remove_spans(parsed.source, parsed.dsl_spans, insertion: parsed.first_dsl_offset, payload: install)
|
|
57
|
+
result = result.gsub(/^[ \t]+(?=\n)/, "")
|
|
58
|
+
result = result.gsub(/^\s*require ["']flexr["']\s*\n/, "") if standalone?(parsed)
|
|
59
|
+
digest = Digest::SHA256.hexdigest(payload)
|
|
60
|
+
header = [
|
|
61
|
+
"# Generated by flexr. DO NOT EDIT.",
|
|
62
|
+
"# source: #{parsed.path}",
|
|
63
|
+
"# spec-digest: sha256:#{digest}",
|
|
64
|
+
"# unicode: #{Unicode::VERSION}",
|
|
65
|
+
"# backend: #{effective_backend(parsed)}",
|
|
66
|
+
"# compiled: true",
|
|
67
|
+
"# eval: #{@eval_mode}",
|
|
68
|
+
"# standalone: #{standalone?(parsed)}"
|
|
69
|
+
].join("\n")
|
|
70
|
+
prefix = standalone?(parsed) ? "require \"json\"\n#{embedded_runtime}\n" : ""
|
|
71
|
+
result.sub(/\A(# frozen_string_literal: true\n)/) { "#{::Regexp.last_match(1)}#{header}\n#{prefix}" }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def generated_payload(parsed, compiled)
|
|
75
|
+
definitions = parsed.rules.map do |rule|
|
|
76
|
+
conditions = Array(rule.pattern_conditions).map do |condition|
|
|
77
|
+
[condition.rule_index, condition.pattern_index, condition.bol_only, condition.end_anchor]
|
|
78
|
+
end
|
|
79
|
+
"{ index: #{rule.index}, patterns: #{ruby_literal(rule.patterns)}, pattern_conditions: #{ruby_literal(conditions)}, trailing: #{ruby_literal(rule.trailing)}, " \
|
|
80
|
+
"action: #{action_expression(rule.action)}, states: #{rule.states.inspect}, " \
|
|
81
|
+
"bol_only: #{ruby_literal(rule.bol_only)}, end_anchor: #{ruby_literal(rule.end_anchor)} }"
|
|
82
|
+
end
|
|
83
|
+
"{ rules: [#{definitions.join(', ')}], " \
|
|
84
|
+
"backend: #{ruby_literal(effective_backend(parsed))}, " \
|
|
85
|
+
"token_kind: #{ruby_literal(@options.fetch(:token_kind, parsed.config[:token_kind]))}, " \
|
|
86
|
+
"encoding: #{encoding_expression(parsed.config[:encoding])}, " \
|
|
87
|
+
"declared_tokens: #{ruby_literal(parsed.config[:declared_tokens])}, " \
|
|
88
|
+
"options: #{ruby_literal(effective_options(parsed))}, " \
|
|
89
|
+
"eof_rules: #{eof_rules_expression(parsed.config[:eof_rules] || {})}, " \
|
|
90
|
+
"states: #{ruby_literal(parsed.states.keys.reject { |name| name == :initial })}, " \
|
|
91
|
+
"inclusive_states: #{ruby_literal(parsed.states.transform_values { |value| value[:inclusive] })}, " \
|
|
92
|
+
"compiled: #{compiled_expression(compiled, compression: table_compression(parsed),
|
|
93
|
+
backend: effective_backend(parsed))} }"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def compile_parsed(parsed)
|
|
97
|
+
states = parsed.states.each_with_index.to_h do |(name, value), index|
|
|
98
|
+
[name.to_sym, IR::State.new(name: name.to_sym, inclusive: value[:inclusive], id: index)]
|
|
99
|
+
end
|
|
100
|
+
rules = parsed.rules.map do |rule|
|
|
101
|
+
IR::Rule.new(index: rule.index, patterns: rule.patterns, trailing: normalize_trailing(rule.trailing),
|
|
102
|
+
action: rule.action, states: rule.states, bol_only: rule.bol_only,
|
|
103
|
+
end_anchor: rule.end_anchor, location: rule.span)
|
|
104
|
+
end
|
|
105
|
+
spec = IR::Spec.new(
|
|
106
|
+
class_name: parsed.class_name, superclass: "Flexr::Lexer",
|
|
107
|
+
backend: @options.fetch(:backend, parsed.config[:backend]),
|
|
108
|
+
token_kind: @options.fetch(:token_kind, parsed.config[:token_kind]),
|
|
109
|
+
encoding: parsed.config[:encoding], options: effective_options(parsed),
|
|
110
|
+
declared_tokens: parsed.config[:declared_tokens], states: states, rules: rules,
|
|
111
|
+
eof_rules: parsed.config[:eof_rules] || {}, verbatim: parsed.source
|
|
112
|
+
)
|
|
113
|
+
compiled = Automaton::Compiler.new(spec).compile
|
|
114
|
+
validate_firstmatch_equivalence!(spec, compiled) if spec.backend == :firstmatch
|
|
115
|
+
@resolved_backend = resolve_backend(spec.backend, compiled)
|
|
116
|
+
parsed.rules.each do |rule|
|
|
117
|
+
compiled_rule = spec.rules.fetch(rule.index)
|
|
118
|
+
rule.pattern_conditions = compiled_rule.pattern_conditions
|
|
119
|
+
end
|
|
120
|
+
compiled
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def effective_backend(parsed)
|
|
124
|
+
@resolved_backend || resolve_backend(@options.fetch(:backend, parsed.config[:backend]).to_sym, nil)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resolve_backend(requested, compiled)
|
|
128
|
+
return requested unless requested == :auto
|
|
129
|
+
return :table unless compiled
|
|
130
|
+
|
|
131
|
+
cells = compiled.stats.values.map { |stats| stats[:states] * stats[:classes] }.max.to_i
|
|
132
|
+
cells > DSL::AUTO_DIRECT_CELL_THRESHOLD ? :direct : :table
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def normalize_trailing(value)
|
|
136
|
+
case value
|
|
137
|
+
when nil, ::Regexp
|
|
138
|
+
value
|
|
139
|
+
when String
|
|
140
|
+
::Regexp.new(::Regexp.escape(value))
|
|
141
|
+
else
|
|
142
|
+
diagnostic = Diagnostics.error("FLEXR-E018", "followed_by must be a Regexp or String")
|
|
143
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def validate_firstmatch_equivalence!(spec, compiled)
|
|
148
|
+
return if spec.rules.any? do |rule|
|
|
149
|
+
rule.trailing || rule.patterns.any? { |pattern| reference_pattern?(pattern, unicode: spec.options[:unicode] == true) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
machine = compiled.machines.fetch(:initial).dfa
|
|
153
|
+
random = Random.new(17)
|
|
154
|
+
inputs = ["", "a", "aa", "aaa", "ba", "aab"]
|
|
155
|
+
inputs.concat(Array.new(9_994) do
|
|
156
|
+
Array.new(random.rand(10)) { random.rand(32..126) }.pack("C*")
|
|
157
|
+
end)
|
|
158
|
+
inputs.each do |input|
|
|
159
|
+
table = table_match(machine, input)
|
|
160
|
+
firstmatch = firstmatch_match(spec.rules, input)
|
|
161
|
+
next if table == firstmatch
|
|
162
|
+
|
|
163
|
+
raise CompileError, "firstmatch differs from table for #{input.inspect}: #{firstmatch.inspect} vs #{table.inspect}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def reference_pattern?(pattern, unicode: false)
|
|
168
|
+
return false unless pattern.is_a?(::Regexp)
|
|
169
|
+
return true if pattern.source.match?(/\\[pP]\{/) || pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
|
|
170
|
+
|
|
171
|
+
unicode && pattern.encoding != Encoding::BINARY && pattern.source.match?(/\\[dDwWsS]/)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def table_match(dfa, input)
|
|
175
|
+
state = dfa.start
|
|
176
|
+
best = nil
|
|
177
|
+
input.each_byte.with_index do |byte, index|
|
|
178
|
+
state = dfa.transition(state, byte)
|
|
179
|
+
break unless state
|
|
180
|
+
|
|
181
|
+
acceptance = dfa.accepts[state].select do |candidate|
|
|
182
|
+
!candidate.end_anchor || index + 1 == input.bytesize || input.getbyte(index + 1) == 0x0a
|
|
183
|
+
end.min_by(&:rule_index)
|
|
184
|
+
next unless acceptance
|
|
185
|
+
|
|
186
|
+
candidate = [acceptance.rule_index, index + 1]
|
|
187
|
+
best = candidate if best.nil? || candidate[1] > best[1] ||
|
|
188
|
+
(candidate[1] == best[1] && candidate[0] < best[0])
|
|
189
|
+
end
|
|
190
|
+
best
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def firstmatch_match(rules, input)
|
|
194
|
+
rules.sort_by(&:index).each do |rule|
|
|
195
|
+
lengths = rule.patterns.filter_map do |pattern|
|
|
196
|
+
regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(::Regexp.escape(pattern.to_s))
|
|
197
|
+
match = regexp.match(input, 0)
|
|
198
|
+
match&.begin(0)&.zero? ? match[0].bytesize : nil
|
|
199
|
+
rescue ArgumentError, RegexpError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
return [rule.index, lengths.max] unless lengths.empty?
|
|
203
|
+
end
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def compiled_expression(compiled, compression: :none, backend: :table)
|
|
208
|
+
machines = compiled.machines.map do |name, machine|
|
|
209
|
+
dfa = machine.dfa
|
|
210
|
+
data = {
|
|
211
|
+
accepts: dfa.accepts,
|
|
212
|
+
ec: dfa.ec,
|
|
213
|
+
class_count: dfa.class_count,
|
|
214
|
+
state_count: dfa.states,
|
|
215
|
+
start: dfa.start,
|
|
216
|
+
rule_ids: dfa.rule_ids
|
|
217
|
+
}
|
|
218
|
+
pack_tables = %i[rows full].include?(compression) || @options.fetch(:table_format, :literal).to_sym == :packed
|
|
219
|
+
if pack_tables
|
|
220
|
+
packed = Codegen::TablePacker.pack(dfa.transitions, compression: compression)
|
|
221
|
+
data[:packed] = if @options.fetch(:table_format, :literal).to_sym == :packed
|
|
222
|
+
Codegen::TablePacker.encode(packed)
|
|
223
|
+
else
|
|
224
|
+
packed
|
|
225
|
+
end
|
|
226
|
+
else
|
|
227
|
+
data[:transitions] = dfa.transitions
|
|
228
|
+
end
|
|
229
|
+
data[:direct] = Codegen::Direct.new(compiled).generate(state: name) if backend == :direct
|
|
230
|
+
"#{ruby_literal(name)} => { state_name: #{ruby_literal(machine.state_name)}, dfa: #{ruby_literal(data)} }"
|
|
231
|
+
end
|
|
232
|
+
# W016 is derived from wall-clock compilation time and must not make
|
|
233
|
+
# generated source (or its golden digest) vary between runs.
|
|
234
|
+
diagnostics = compiled.diagnostics.reject { |diagnostic| diagnostic.code == "FLEXR-W016" }.map(&:to_h)
|
|
235
|
+
"{ machines: { #{machines.join(', ')} }, states: #{ruby_literal(compiled.states)}, " \
|
|
236
|
+
"stats: #{ruby_literal(compiled.stats)}, diagnostics: #{ruby_literal(diagnostics)} }"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def standalone?(parsed)
|
|
240
|
+
@options.fetch(:standalone, parsed.config[:options]&.key?(:standalone) || false)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def effective_options(parsed)
|
|
244
|
+
options = (parsed.config[:options] || {}).dup
|
|
245
|
+
%i[accel max_dfa_states table_compression table_format].each do |name|
|
|
246
|
+
options[name] = @options[name] if @options.key?(name)
|
|
247
|
+
end
|
|
248
|
+
options[:standalone] = true if standalone?(parsed)
|
|
249
|
+
options
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def table_compression(parsed)
|
|
253
|
+
@options.fetch(:table_compression, parsed.config[:options]&.fetch(:table_compression, :rows) || :rows).to_sym
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def embedded_runtime
|
|
257
|
+
RUNTIME_SOURCES.map do |relative|
|
|
258
|
+
File.binread(File.expand_path(relative, __dir__))
|
|
259
|
+
end.join("\n")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def encoding_expression(encoding)
|
|
263
|
+
encoding == Encoding::BINARY ? "Encoding::BINARY" : "Encoding::UTF_8"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def eof_rules_expression(rules)
|
|
267
|
+
pairs = rules.map { |state, action| "#{ruby_literal(state)} => #{action_expression(action)}" }
|
|
268
|
+
"{ #{pairs.join(', ')} }"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def action_expression(action)
|
|
272
|
+
return action if action.is_a?(String) && action.start_with?("proc")
|
|
273
|
+
return ruby_literal(action) unless action.is_a?(Proc)
|
|
274
|
+
|
|
275
|
+
"proc { emit(nil, text) }"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def generated_action_source(parsed, indent)
|
|
279
|
+
lines = ["#{indent}def __flexr_generated_execute(rule)", "#{indent} case rule.index"]
|
|
280
|
+
parsed.rules.each do |rule|
|
|
281
|
+
lines << "#{indent} when #{rule.index}"
|
|
282
|
+
body = inline_action_body(rule)
|
|
283
|
+
if body.nil?
|
|
284
|
+
lines << "#{indent} instance_exec(&rule.action)"
|
|
285
|
+
else
|
|
286
|
+
body.lines.each { |line| lines << "#{indent} #{line.rstrip}" }
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
lines << "#{indent} else"
|
|
290
|
+
lines << "#{indent} instance_exec(&rule.action)"
|
|
291
|
+
lines << "#{indent} end"
|
|
292
|
+
lines << "#{indent}end\n"
|
|
293
|
+
lines.join("\n")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def inline_action_body(rule)
|
|
297
|
+
action = rule.action
|
|
298
|
+
case action
|
|
299
|
+
when :skip
|
|
300
|
+
""
|
|
301
|
+
when Array
|
|
302
|
+
return "emit(#{ruby_literal(action.fetch(1))}, text)" if action.first == :emit
|
|
303
|
+
|
|
304
|
+
nil
|
|
305
|
+
when String
|
|
306
|
+
return inline_simple_action(rule.action_source) if rule.action_source
|
|
307
|
+
return unless action.start_with?("proc")
|
|
308
|
+
|
|
309
|
+
body = action.delete_prefix("proc").strip
|
|
310
|
+
if body.start_with?("{") && body.end_with?("}")
|
|
311
|
+
body = body[1...-1]
|
|
312
|
+
return nil if body.lstrip.start_with?("|")
|
|
313
|
+
return body.strip
|
|
314
|
+
end
|
|
315
|
+
return nil unless body.start_with?("do") && body.end_with?("end")
|
|
316
|
+
|
|
317
|
+
body.delete_prefix("do")[0...-3].strip
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Hash#inspect changed from hash rockets to keyword-style labels in Ruby
|
|
322
|
+
# 4. Generated source and its golden digests must be identical on every
|
|
323
|
+
# supported Ruby, so serialize the small set of Ruby literals used in the
|
|
324
|
+
# generated payload ourselves.
|
|
325
|
+
def ruby_literal(value)
|
|
326
|
+
case value
|
|
327
|
+
when Hash
|
|
328
|
+
return "{}" if value.empty?
|
|
329
|
+
|
|
330
|
+
entries = value.map do |key, nested|
|
|
331
|
+
if key.is_a?(Symbol) && key.to_s.match?(/\A[a-zA-Z_]\w*\z/)
|
|
332
|
+
"#{key}: #{ruby_literal(nested)}"
|
|
333
|
+
else
|
|
334
|
+
"#{ruby_literal(key)} => #{ruby_literal(nested)}"
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
"{#{entries.join(', ')}}"
|
|
338
|
+
when Array
|
|
339
|
+
"[#{value.map { |item| ruby_literal(item) }.join(', ')}]"
|
|
340
|
+
else
|
|
341
|
+
value.inspect
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def inline_simple_action(source)
|
|
346
|
+
body = source.to_s.strip
|
|
347
|
+
body = body[1...-1].strip if body.start_with?("{") && body.end_with?("}")
|
|
348
|
+
match = INLINE_EMIT.match(body)
|
|
349
|
+
return unless match
|
|
350
|
+
|
|
351
|
+
arguments = match[1].to_s.strip
|
|
352
|
+
return unless arguments.empty? || INLINE_EMIT_ARGUMENTS.match?(arguments)
|
|
353
|
+
|
|
354
|
+
arguments.empty? ? "emit" : "emit #{arguments}"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def validate_diagnostics!(compiled)
|
|
358
|
+
return unless @options[:warn_as_error]
|
|
359
|
+
|
|
360
|
+
diagnostic = Array(compiled.diagnostics).find do |item|
|
|
361
|
+
item.warning? && (@options.fetch(:warn_level, :default) == :all || item.code != "FLEXR-W016")
|
|
362
|
+
end
|
|
363
|
+
return unless diagnostic
|
|
364
|
+
|
|
365
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def generate_from_runtime(source)
|
|
369
|
+
loader = Object.new
|
|
370
|
+
loader.define_singleton_method(:load_spec) do
|
|
371
|
+
# --eval is opt-in and intentionally executes trusted specification code.
|
|
372
|
+
# rubocop:disable Security/Eval
|
|
373
|
+
eval(source, TOPLEVEL_BINDING, @path, 1)
|
|
374
|
+
# rubocop:enable Security/Eval
|
|
375
|
+
end
|
|
376
|
+
loader.instance_variable_set(:@path, @path)
|
|
377
|
+
loader.load_spec
|
|
378
|
+
passthrough = Source::PrismReader.new(source, path: @path).read(allow_dynamic: true)
|
|
379
|
+
classes = ObjectSpace.each_object(Class).select { |klass| klass.respond_to?(:__flexr_spec) }
|
|
380
|
+
classes.select! do |klass|
|
|
381
|
+
name = klass.name.to_s
|
|
382
|
+
(passthrough.class_name.nil? || name == passthrough.class_name || name.end_with?("::#{passthrough.class_name}")) &&
|
|
383
|
+
klass != Flexr::Lexer && klass.__flexr_rules.any?
|
|
384
|
+
end
|
|
385
|
+
klass = classes.last
|
|
386
|
+
raise CompileError, "--eval could not find a Flexr::Lexer class" unless klass
|
|
387
|
+
|
|
388
|
+
# --eval is intentionally explicit; the generated file still contains
|
|
389
|
+
# executable action source, but its metadata is deterministic for this run.
|
|
390
|
+
parsed = klass.__flexr_spec
|
|
391
|
+
runtime_rules = parsed.rules.to_h { |rule| [rule.index, rule] }
|
|
392
|
+
passthrough.rules.each do |rule|
|
|
393
|
+
runtime_rule = runtime_rules.fetch(rule.index)
|
|
394
|
+
rule.patterns = runtime_rule.patterns
|
|
395
|
+
rule.trailing = runtime_rule.trailing
|
|
396
|
+
end
|
|
397
|
+
generate_static(passthrough)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|