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
data/lib/flexr/generated.rb
CHANGED
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module Flexr
|
|
4
4
|
module Generated
|
|
5
|
+
ARTIFACT_SCHEMA_VERSION = 1
|
|
6
|
+
RUNTIME_ABI_VERSION = 1
|
|
7
|
+
|
|
5
8
|
module_function
|
|
6
9
|
|
|
10
|
+
def artifact_metadata
|
|
11
|
+
{
|
|
12
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
13
|
+
compiler_version: VERSION,
|
|
14
|
+
runtime_abi_version: RUNTIME_ABI_VERSION,
|
|
15
|
+
unicode_version: Unicode::VERSION
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
7
19
|
def install!(klass, payload)
|
|
8
20
|
rules = payload.fetch(:rules)
|
|
9
21
|
config = payload
|
|
@@ -34,20 +46,21 @@ module Flexr
|
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def install_compiled!(klass, payload)
|
|
49
|
+
validate_artifact!(payload[:artifact])
|
|
37
50
|
install!(klass, payload)
|
|
38
51
|
machines = payload.fetch(:compiled).fetch(:machines).transform_values do |machine|
|
|
39
52
|
dfa_data = machine.fetch(:dfa)
|
|
40
53
|
packed = decode_packed(dfa_data[:packed])
|
|
41
54
|
dfa = Automaton::DFA.new(
|
|
42
|
-
transitions: dfa_data[:transitions]
|
|
43
|
-
dfa_data.fetch(:class_count)),
|
|
55
|
+
transitions: dfa_data[:transitions],
|
|
44
56
|
accepts: dfa_data.fetch(:accepts),
|
|
45
57
|
ec: dfa_data.fetch(:ec),
|
|
46
58
|
class_count: dfa_data.fetch(:class_count),
|
|
47
59
|
start: dfa_data.fetch(:start),
|
|
48
60
|
rule_ids: dfa_data.fetch(:rule_ids),
|
|
49
61
|
packed: packed,
|
|
50
|
-
direct: dfa_data[:direct]
|
|
62
|
+
direct: dfa_data[:direct],
|
|
63
|
+
state_count: dfa_data.fetch(:state_count)
|
|
51
64
|
)
|
|
52
65
|
Automaton::Machine.new(dfa: dfa, state_name: machine.fetch(:state_name).to_sym)
|
|
53
66
|
end
|
|
@@ -65,6 +78,35 @@ module Flexr
|
|
|
65
78
|
klass
|
|
66
79
|
end
|
|
67
80
|
|
|
81
|
+
def validate_artifact!(artifact)
|
|
82
|
+
return incompatible_artifact!("artifact metadata is missing") unless artifact.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
expected = {
|
|
85
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
86
|
+
runtime_abi_version: RUNTIME_ABI_VERSION,
|
|
87
|
+
unicode_version: Unicode::VERSION
|
|
88
|
+
}
|
|
89
|
+
mismatch = expected.find { |name, value| artifact[name] != value }
|
|
90
|
+
compiler_version = artifact[:compiler_version]
|
|
91
|
+
return if mismatch.nil? && compiler_version.is_a?(String) && !compiler_version.empty?
|
|
92
|
+
|
|
93
|
+
detail = if mismatch
|
|
94
|
+
name, value = mismatch
|
|
95
|
+
"#{name}=#{artifact[name].inspect} (expected #{value.inspect})"
|
|
96
|
+
else
|
|
97
|
+
"compiler_version=#{compiler_version.inspect}"
|
|
98
|
+
end
|
|
99
|
+
incompatible_artifact!(detail)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def incompatible_artifact!(detail)
|
|
103
|
+
diagnostic = Diagnostics.error(
|
|
104
|
+
"FLEXR-E020", "generated artifact is incompatible with this runtime",
|
|
105
|
+
help: "regenerate the lexer with the installed flexr version", note: detail
|
|
106
|
+
)
|
|
107
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
108
|
+
end
|
|
109
|
+
|
|
68
110
|
def decode_packed(packed)
|
|
69
111
|
return packed unless packed.is_a?(Hash) && packed[:encoding]&.to_sym == :base64
|
|
70
112
|
|
|
@@ -84,42 +126,26 @@ module Flexr
|
|
|
84
126
|
values.map { |value| value == nil_value ? nil : value }
|
|
85
127
|
end
|
|
86
128
|
|
|
87
|
-
def inflate_packed(packed, state_count, class_count)
|
|
88
|
-
return nil unless packed
|
|
89
|
-
|
|
90
|
-
Array.new(state_count) do |state|
|
|
91
|
-
Array.new(class_count) do |class_id|
|
|
92
|
-
cursor = state
|
|
93
|
-
loop do
|
|
94
|
-
index = packed.fetch(:base).fetch(cursor) + class_id
|
|
95
|
-
break packed.fetch(:next).fetch(index) if packed.fetch(:check)[index] == cursor
|
|
96
|
-
|
|
97
|
-
fallback = packed[:fallback]&.fetch(cursor)
|
|
98
|
-
break packed.fetch(:default).fetch(cursor) unless fallback
|
|
99
|
-
|
|
100
|
-
cursor = fallback
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
129
|
end
|
|
106
130
|
|
|
107
131
|
module DSL
|
|
108
132
|
def __flexr_add_generated_rule(definition)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
__flexr_mutate! do
|
|
134
|
+
action = definition.fetch(:action)
|
|
135
|
+
@__flexr_rules << IR::Rule.new(
|
|
136
|
+
index: definition.fetch(:index), patterns: Array(definition.fetch(:patterns)),
|
|
137
|
+
trailing: normalize_trailing(definition[:trailing]), action: action,
|
|
138
|
+
states: Array(definition.fetch(:states)).map(&:to_sym),
|
|
139
|
+
bol_only: definition.fetch(:bol_only, false), end_anchor: definition[:end_anchor],
|
|
140
|
+
location: definition[:span] || definition[:location],
|
|
141
|
+
pattern_conditions: Array(definition[:pattern_conditions]).map do |condition|
|
|
142
|
+
next unless condition
|
|
143
|
+
|
|
144
|
+
Automaton::Acceptance.new(rule_index: condition[0], pattern_index: condition[1],
|
|
145
|
+
bol_only: condition[2], end_anchor: condition[3])
|
|
146
|
+
end
|
|
147
|
+
)
|
|
148
|
+
end
|
|
123
149
|
end
|
|
124
150
|
end
|
|
125
151
|
end
|
data/lib/flexr/generator.rb
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "runtime" unless defined?(Flexr::Lexer)
|
|
5
|
+
require_relative "artifact_writer"
|
|
6
|
+
require_relative "codegen"
|
|
7
|
+
require_relative "source"
|
|
8
|
+
|
|
3
9
|
module Flexr
|
|
4
10
|
class Generator
|
|
5
11
|
attr_reader :diagnostics
|
|
@@ -8,15 +14,18 @@ module Flexr
|
|
|
8
14
|
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
15
|
|
|
10
16
|
RUNTIME_SOURCES = %w[
|
|
11
|
-
version.rb errors.rb diagnostics.rb ir.rb
|
|
12
|
-
|
|
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
|
|
17
|
+
version.rb errors.rb diagnostics.rb action_resolver.rb configuration.rb ir.rb
|
|
18
|
+
unicode/version.rb automaton/types.rb automaton/dfa.rb automaton/analysis.rb automaton/accel.rb
|
|
16
19
|
runtime/location.rb runtime/token.rb runtime/buffer.rb runtime/errors.rb
|
|
17
20
|
runtime/interpreter.rb runtime/core.rb dsl.rb lexer.rb generated.rb
|
|
18
21
|
].freeze
|
|
19
22
|
|
|
23
|
+
MATCHER_RUNTIME_SOURCES = %w[
|
|
24
|
+
regexp/ast.rb regexp/parser.rb regexp/unsupported.rb
|
|
25
|
+
unicode/utf8_splitter.rb unicode/data/properties.rb unicode/data/case_folding.rb
|
|
26
|
+
unicode/property.rb unicode/reference_regexp.rb unicode/case_fold.rb
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
20
29
|
def initialize(path, output: nil, eval_mode: false, options: {})
|
|
21
30
|
@path = path
|
|
22
31
|
@output = output
|
|
@@ -26,6 +35,7 @@ module Flexr
|
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
def generate
|
|
38
|
+
ArtifactWriter.ensure_distinct!(@path, @output) if @output
|
|
29
39
|
# Keep generated source stable when a checkout presents the spec with
|
|
30
40
|
# Windows line endings. All offsets consumed by the parser and
|
|
31
41
|
# passthrough writer are relative to this normalized source.
|
|
@@ -36,7 +46,7 @@ module Flexr
|
|
|
36
46
|
parsed = Source::PrismReader.new(source, path: @path).read
|
|
37
47
|
generate_static(parsed)
|
|
38
48
|
end
|
|
39
|
-
|
|
49
|
+
ArtifactWriter.write!(@output, result, source_path: @path) if @output
|
|
40
50
|
result
|
|
41
51
|
end
|
|
42
52
|
|
|
@@ -51,24 +61,26 @@ module Flexr
|
|
|
51
61
|
install = "Flexr::Generated.install_compiled!(self, #{payload})\n"
|
|
52
62
|
install = "#{install}#{Codegen::Table.new(compiled).source(indent: indent)}"
|
|
53
63
|
install = "#{install}#{generated_action_source(parsed, indent)}"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
result = Source::Passthrough.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
digest = Digest::SHA256.hexdigest(
|
|
64
|
+
edits = parsed.dsl_edits.dup
|
|
65
|
+
edits.concat(parsed.flexr_require_spans.map { |span| [*span, ""] }) if standalone?(parsed)
|
|
66
|
+
result = Source::Passthrough.rewrite(
|
|
67
|
+
parsed.source, edits, insertion: parsed.first_dsl_offset, payload: install
|
|
68
|
+
)
|
|
69
|
+
digest = Digest::SHA256.hexdigest(result)
|
|
60
70
|
header = [
|
|
61
71
|
"# Generated by flexr. DO NOT EDIT.",
|
|
62
72
|
"# source: #{parsed.path}",
|
|
63
73
|
"# spec-digest: sha256:#{digest}",
|
|
74
|
+
"# artifact-schema: #{Generated::ARTIFACT_SCHEMA_VERSION}",
|
|
75
|
+
"# runtime-abi: #{Generated::RUNTIME_ABI_VERSION}",
|
|
64
76
|
"# unicode: #{Unicode::VERSION}",
|
|
65
77
|
"# backend: #{effective_backend(parsed)}",
|
|
66
78
|
"# compiled: true",
|
|
67
79
|
"# eval: #{@eval_mode}",
|
|
68
80
|
"# standalone: #{standalone?(parsed)}"
|
|
69
81
|
].join("\n")
|
|
70
|
-
prefix = standalone?(parsed) ? "require \"json\"\n#{embedded_runtime}\n" : ""
|
|
71
|
-
|
|
82
|
+
prefix = standalone?(parsed) ? "require \"json\"\nrequire \"monitor\"\n#{embedded_runtime(parsed)}\n" : ""
|
|
83
|
+
Source::Passthrough.insert_after_preamble(result, "#{header}\n#{prefix}")
|
|
72
84
|
end
|
|
73
85
|
|
|
74
86
|
def generated_payload(parsed, compiled)
|
|
@@ -80,7 +92,7 @@ module Flexr
|
|
|
80
92
|
"action: #{action_expression(rule.action)}, states: #{rule.states.inspect}, " \
|
|
81
93
|
"bol_only: #{ruby_literal(rule.bol_only)}, end_anchor: #{ruby_literal(rule.end_anchor)} }"
|
|
82
94
|
end
|
|
83
|
-
"{ rules: [#{definitions.join(', ')}], " \
|
|
95
|
+
"{ artifact: #{ruby_literal(artifact_metadata)}, rules: [#{definitions.join(', ')}], " \
|
|
84
96
|
"backend: #{ruby_literal(effective_backend(parsed))}, " \
|
|
85
97
|
"token_kind: #{ruby_literal(@options.fetch(:token_kind, parsed.config[:token_kind]))}, " \
|
|
86
98
|
"encoding: #{encoding_expression(parsed.config[:encoding])}, " \
|
|
@@ -93,6 +105,10 @@ module Flexr
|
|
|
93
105
|
backend: effective_backend(parsed))} }"
|
|
94
106
|
end
|
|
95
107
|
|
|
108
|
+
def artifact_metadata
|
|
109
|
+
Generated.artifact_metadata
|
|
110
|
+
end
|
|
111
|
+
|
|
96
112
|
def compile_parsed(parsed)
|
|
97
113
|
states = parsed.states.each_with_index.to_h do |(name, value), index|
|
|
98
114
|
[name.to_sym, IR::State.new(name: name.to_sym, inclusive: value[:inclusive], id: index)]
|
|
@@ -111,7 +127,6 @@ module Flexr
|
|
|
111
127
|
eof_rules: parsed.config[:eof_rules] || {}, verbatim: parsed.source
|
|
112
128
|
)
|
|
113
129
|
compiled = Automaton::Compiler.new(spec).compile
|
|
114
|
-
validate_firstmatch_equivalence!(spec, compiled) if spec.backend == :firstmatch
|
|
115
130
|
@resolved_backend = resolve_backend(spec.backend, compiled)
|
|
116
131
|
parsed.rules.each do |rule|
|
|
117
132
|
compiled_rule = spec.rules.fetch(rule.index)
|
|
@@ -128,8 +143,7 @@ module Flexr
|
|
|
128
143
|
return requested unless requested == :auto
|
|
129
144
|
return :table unless compiled
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
cells > DSL::AUTO_DIRECT_CELL_THRESHOLD ? :direct : :table
|
|
146
|
+
Automaton::BackendCostModel.choose(compiled)
|
|
133
147
|
end
|
|
134
148
|
|
|
135
149
|
def normalize_trailing(value)
|
|
@@ -144,66 +158,6 @@ module Flexr
|
|
|
144
158
|
end
|
|
145
159
|
end
|
|
146
160
|
|
|
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
161
|
def compiled_expression(compiled, compression: :none, backend: :table)
|
|
208
162
|
machines = compiled.machines.map do |name, machine|
|
|
209
163
|
dfa = machine.dfa
|
|
@@ -216,7 +170,9 @@ module Flexr
|
|
|
216
170
|
rule_ids: dfa.rule_ids
|
|
217
171
|
}
|
|
218
172
|
pack_tables = %i[rows full].include?(compression) || @options.fetch(:table_format, :literal).to_sym == :packed
|
|
219
|
-
if
|
|
173
|
+
if backend == :direct
|
|
174
|
+
data[:direct] = Codegen::Direct.new(compiled).generate(state: name)
|
|
175
|
+
elsif pack_tables
|
|
220
176
|
packed = Codegen::TablePacker.pack(dfa.transitions, compression: compression)
|
|
221
177
|
data[:packed] = if @options.fetch(:table_format, :literal).to_sym == :packed
|
|
222
178
|
Codegen::TablePacker.encode(packed)
|
|
@@ -226,7 +182,6 @@ module Flexr
|
|
|
226
182
|
else
|
|
227
183
|
data[:transitions] = dfa.transitions
|
|
228
184
|
end
|
|
229
|
-
data[:direct] = Codegen::Direct.new(compiled).generate(state: name) if backend == :direct
|
|
230
185
|
"#{ruby_literal(name)} => { state_name: #{ruby_literal(machine.state_name)}, dfa: #{ruby_literal(data)} }"
|
|
231
186
|
end
|
|
232
187
|
# W016 is derived from wall-clock compilation time and must not make
|
|
@@ -253,12 +208,18 @@ module Flexr
|
|
|
253
208
|
@options.fetch(:table_compression, parsed.config[:options]&.fetch(:table_compression, :rows) || :rows).to_sym
|
|
254
209
|
end
|
|
255
210
|
|
|
256
|
-
def embedded_runtime
|
|
257
|
-
RUNTIME_SOURCES.
|
|
211
|
+
def embedded_runtime(parsed)
|
|
212
|
+
sources = RUNTIME_SOURCES.dup
|
|
213
|
+
sources.insert(6, *MATCHER_RUNTIME_SOURCES) if matcher_runtime?(parsed)
|
|
214
|
+
sources.map do |relative|
|
|
258
215
|
File.binread(File.expand_path(relative, __dir__))
|
|
259
216
|
end.join("\n")
|
|
260
217
|
end
|
|
261
218
|
|
|
219
|
+
def matcher_runtime?(parsed)
|
|
220
|
+
effective_backend(parsed) == :firstmatch || parsed.rules.any?(&:trailing)
|
|
221
|
+
end
|
|
222
|
+
|
|
262
223
|
def encoding_expression(encoding)
|
|
263
224
|
encoding == Encoding::BINARY ? "Encoding::BINARY" : "Encoding::UTF_8"
|
|
264
225
|
end
|
|
@@ -279,6 +240,10 @@ module Flexr
|
|
|
279
240
|
lines = ["#{indent}def __flexr_generated_execute(rule)", "#{indent} case rule.index"]
|
|
280
241
|
parsed.rules.each do |rule|
|
|
281
242
|
lines << "#{indent} when #{rule.index}"
|
|
243
|
+
if rule.bind_action
|
|
244
|
+
lines << "#{indent} instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))"
|
|
245
|
+
next
|
|
246
|
+
end
|
|
282
247
|
body = inline_action_body(rule)
|
|
283
248
|
if body.nil?
|
|
284
249
|
lines << "#{indent} instance_exec(&rule.action)"
|
data/lib/flexr/importer.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "runtime" unless defined?(Flexr::Lexer)
|
|
4
|
+
|
|
3
5
|
module Flexr
|
|
4
6
|
class Importer
|
|
5
7
|
Result = Struct.new(:source, :warnings, :complete?, keyword_init: true)
|
|
@@ -387,46 +389,8 @@ module Flexr
|
|
|
387
389
|
|
|
388
390
|
def rexical_dfa_counterexample(regexps, automata)
|
|
389
391
|
first, second = automata
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
bytes = rexical_byte_representatives(first, second)
|
|
393
|
-
|
|
394
|
-
until queue.empty?
|
|
395
|
-
first_state, second_state, first_seen, input = queue.shift
|
|
396
|
-
bytes.each do |byte|
|
|
397
|
-
next_first = first_state && first.transition(first_state, byte)
|
|
398
|
-
next_second = second.transition(second_state, byte)
|
|
399
|
-
next unless next_second
|
|
400
|
-
|
|
401
|
-
next_input = input + byte.chr(Encoding::BINARY)
|
|
402
|
-
next_first_seen = first_seen || rexical_accepting?(first, next_first)
|
|
403
|
-
if next_first_seen && rexical_accepting?(second, next_second) &&
|
|
404
|
-
!rexical_accepting?(first, next_first)
|
|
405
|
-
lengths = rexical_match_lengths(regexps, next_input)
|
|
406
|
-
return lengths if lengths
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
key = [next_first, next_second, next_first_seen]
|
|
410
|
-
next if visited[key]
|
|
411
|
-
|
|
412
|
-
visited[key] = true
|
|
413
|
-
queue << [next_first, next_second, next_first_seen, next_input]
|
|
414
|
-
end
|
|
415
|
-
end
|
|
416
|
-
nil
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def rexical_byte_representatives(first, second)
|
|
420
|
-
representatives = {}
|
|
421
|
-
256.times do |byte|
|
|
422
|
-
key = [first.ec[byte], second.ec[byte]]
|
|
423
|
-
representatives[key] ||= byte
|
|
424
|
-
end
|
|
425
|
-
representatives.values.sort_by { |byte| [byte.between?(32, 126) ? 0 : 1, byte] }
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
def rexical_accepting?(dfa, state)
|
|
429
|
-
state && !dfa.accepts.fetch(state).empty?
|
|
392
|
+
input = Automaton::Analysis.firstmatch_counterexample(first, second)
|
|
393
|
+
input && rexical_match_lengths(regexps, input)
|
|
430
394
|
end
|
|
431
395
|
|
|
432
396
|
def rexical_match_lengths(regexps, input)
|
data/lib/flexr/options.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "runtime" unless defined?(Flexr::Lexer)
|
|
4
|
+
|
|
3
5
|
module Flexr
|
|
4
6
|
Options = Struct.new(
|
|
5
7
|
:backend, :token_kind, :accel, :standalone, :eval_mode, :table_compression,
|
|
@@ -20,9 +22,9 @@ module Flexr
|
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def validate!
|
|
23
|
-
validate_value!(backend,
|
|
24
|
-
validate_value!(token_kind,
|
|
25
|
-
validate_value!(accel,
|
|
25
|
+
validate_value!(backend, Configuration::BACKENDS, :backend)
|
|
26
|
+
validate_value!(token_kind, Configuration::TOKEN_KINDS, :token_kind)
|
|
27
|
+
validate_value!(accel, Configuration::ACCELERATORS, :accel)
|
|
26
28
|
validate_value!(table_compression, %i[none rows full], :table_compression)
|
|
27
29
|
validate_value!(table_format, %i[literal packed], :table_format)
|
|
28
30
|
validate_value!(warn_level, %i[all default none], :warn_level)
|
data/lib/flexr/rake_task.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "runtime" unless defined?(Flexr::Lexer)
|
|
4
|
+
require_relative "artifact_writer"
|
|
5
|
+
require_relative "generator"
|
|
6
|
+
|
|
3
7
|
module Flexr
|
|
4
8
|
class RakeTask
|
|
5
9
|
attr_accessor :spec, :output, :warn_as_error
|
|
@@ -17,7 +21,8 @@ module Flexr
|
|
|
17
21
|
require "rake"
|
|
18
22
|
raise ArgumentError, "spec is required" unless @spec
|
|
19
23
|
|
|
20
|
-
output = @output || @spec
|
|
24
|
+
output = @output || ArtifactWriter.default_generated_path(@spec)
|
|
25
|
+
ArtifactWriter.ensure_distinct!(@spec, output)
|
|
21
26
|
Rake::FileTask.define_task(output => @spec) do
|
|
22
27
|
Generator.new(@spec, output: output, options: { warn_as_error: @warn_as_error }).generate
|
|
23
28
|
end
|
data/lib/flexr/regexp/ast.rb
CHANGED
|
@@ -4,11 +4,13 @@ module Flexr
|
|
|
4
4
|
module Regexp
|
|
5
5
|
module AST
|
|
6
6
|
Empty = Struct.new(:loc, keyword_init: true)
|
|
7
|
+
Fail = Struct.new(:loc, keyword_init: true)
|
|
7
8
|
ByteRange = Struct.new(:lo, :hi, :loc, keyword_init: true)
|
|
8
9
|
CodepointRange = Struct.new(:lo, :hi, :loc, keyword_init: true)
|
|
9
10
|
Seq = Struct.new(:children, :loc, keyword_init: true)
|
|
10
11
|
Alt = Struct.new(:children, :loc, keyword_init: true)
|
|
11
12
|
Star = Struct.new(:child, :loc, keyword_init: true)
|
|
13
|
+
Repeat = Struct.new(:child, :minimum, :maximum, :loc, keyword_init: true)
|
|
12
14
|
Anchor = Struct.new(:kind, :loc, keyword_init: true)
|
|
13
15
|
TrailMark = Struct.new(:rule_id, :loc, keyword_init: true)
|
|
14
16
|
CharClass = Struct.new(:ranges, :negated, :loc, keyword_init: true)
|
|
@@ -18,18 +20,19 @@ module Flexr
|
|
|
18
20
|
def to_s
|
|
19
21
|
name = self.class.name.split("::").last
|
|
20
22
|
case name
|
|
21
|
-
when "Empty" then
|
|
23
|
+
when "Empty", "Fail" then name
|
|
22
24
|
when "ByteRange" then "ByteRange(#{lo}..#{hi})"
|
|
23
25
|
when "CodepointRange" then "CodepointRange(#{lo}..#{hi})"
|
|
24
26
|
when "Anchor" then "Anchor(#{kind})"
|
|
25
27
|
when "Star" then "Star(#{child})"
|
|
28
|
+
when "Repeat" then "Repeat(#{child}, #{minimum}, #{maximum || '∞'})"
|
|
26
29
|
when "Seq", "Alt" then "#{name}(#{children.join(', ')})"
|
|
27
30
|
else super
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
32
|
-
[Empty, ByteRange, CodepointRange, Seq, Alt, Star, Anchor, TrailMark, CharClass].each do |node_class|
|
|
35
|
+
[Empty, Fail, ByteRange, CodepointRange, Seq, Alt, Star, Repeat, Anchor, TrailMark, CharClass].each do |node_class|
|
|
33
36
|
node_class.include(Formatting)
|
|
34
37
|
end
|
|
35
38
|
|
|
@@ -18,7 +18,7 @@ module Flexr
|
|
|
18
18
|
|
|
19
19
|
def normalize_node(node)
|
|
20
20
|
case node
|
|
21
|
-
when AST::Empty, AST::ByteRange, AST::CodepointRange, AST::Anchor
|
|
21
|
+
when AST::Empty, AST::Fail, AST::ByteRange, AST::CodepointRange, AST::Anchor
|
|
22
22
|
normalize_leaf(node)
|
|
23
23
|
when AST::CharClass
|
|
24
24
|
char_class(node)
|
|
@@ -27,7 +27,10 @@ module Flexr
|
|
|
27
27
|
when AST::Alt
|
|
28
28
|
alternatives(node.children.map { |child| normalize_node(child) })
|
|
29
29
|
when AST::Star
|
|
30
|
-
|
|
30
|
+
child = normalize_node(node.child)
|
|
31
|
+
child.is_a?(AST::Fail) ? AST::Empty.new(loc: node.loc) : AST::Star.new(child: child, loc: node.loc)
|
|
32
|
+
when AST::Repeat
|
|
33
|
+
repeat(node)
|
|
31
34
|
else
|
|
32
35
|
raise CompileError, "unknown regexp AST node: #{node.class}"
|
|
33
36
|
end
|
|
@@ -36,7 +39,7 @@ module Flexr
|
|
|
36
39
|
def normalize_leaf(node)
|
|
37
40
|
case node
|
|
38
41
|
when AST::CodepointRange
|
|
39
|
-
byte_sequences_for_ranges(casefold_ranges(node.lo, node.hi))
|
|
42
|
+
byte_sequences_for_ranges(casefold_ranges(node.lo, node.hi), loc: node.loc)
|
|
40
43
|
else
|
|
41
44
|
node
|
|
42
45
|
end
|
|
@@ -45,44 +48,76 @@ module Flexr
|
|
|
45
48
|
def char_class(node)
|
|
46
49
|
ranges = node.ranges.flat_map do |range|
|
|
47
50
|
if range.first == AST::Property
|
|
48
|
-
Unicode::Property.ranges(range.last
|
|
51
|
+
property_ranges = Unicode::Property.ranges(range.last)
|
|
52
|
+
property_ranges = property_ranges.flat_map { |lo, hi| casefold_ranges(lo, hi) }
|
|
53
|
+
range[1] ? complement(property_ranges) : property_ranges
|
|
49
54
|
else
|
|
50
55
|
casefold_ranges(*range)
|
|
51
56
|
end
|
|
52
57
|
end
|
|
53
58
|
ranges = complement(ranges) if node.negated
|
|
54
|
-
byte_sequences_for_ranges(ranges)
|
|
59
|
+
byte_sequences_for_ranges(ranges, loc: node.loc)
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
def byte_sequences_for_ranges(ranges)
|
|
62
|
+
def byte_sequences_for_ranges(ranges, loc:)
|
|
58
63
|
if @byte_mode
|
|
59
64
|
byte_ranges = ranges.filter_map do |lo, hi|
|
|
60
65
|
next if lo > 255
|
|
61
66
|
|
|
62
67
|
[lo, [hi, 255].min]
|
|
63
68
|
end
|
|
64
|
-
return AST::
|
|
69
|
+
return AST::Fail.new(loc: loc) if byte_ranges.empty?
|
|
65
70
|
|
|
66
|
-
return alternatives(byte_ranges.map { |lo, hi| AST::ByteRange.new(lo: lo, hi: hi, loc:
|
|
71
|
+
return alternatives(byte_ranges.map { |lo, hi| AST::ByteRange.new(lo: lo, hi: hi, loc: loc) })
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
sequences = ranges.flat_map { |lo, hi| Unicode::Utf8Splitter.split(lo, hi) }
|
|
75
|
+
@trie_cache = {}
|
|
70
76
|
trie(sequences, 0)
|
|
71
77
|
end
|
|
72
78
|
|
|
73
79
|
def trie(sequences, index)
|
|
74
|
-
return AST::
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
return AST::Fail.new(loc: nil) if sequences.empty?
|
|
81
|
+
|
|
82
|
+
finished, continuing = sequences.partition { |sequence| sequence.length == index }
|
|
83
|
+
nodes = []
|
|
84
|
+
nodes << AST::Empty.new(loc: nil) unless finished.empty?
|
|
85
|
+
return nodes.first if continuing.empty?
|
|
86
|
+
|
|
87
|
+
key = [index, continuing]
|
|
88
|
+
return alternatives(nodes + [@trie_cache[key]]) if @trie_cache.key?(key)
|
|
89
|
+
|
|
90
|
+
endpoints = continuing.flat_map do |sequence|
|
|
91
|
+
lo, hi = sequence.fetch(index)
|
|
92
|
+
[lo, hi + 1]
|
|
93
|
+
end.uniq.sort
|
|
94
|
+
partitions = endpoints.each_cons(2).filter_map do |lo, after_hi|
|
|
95
|
+
covered = continuing.select do |sequence|
|
|
96
|
+
range = sequence.fetch(index)
|
|
97
|
+
range.first <= lo && range.last >= after_hi - 1
|
|
98
|
+
end
|
|
99
|
+
[lo, after_hi - 1, covered] unless covered.empty?
|
|
100
|
+
end
|
|
101
|
+
branches = partitions.map { |lo, hi, covered| [lo, hi, trie(covered, index + 1)] }
|
|
102
|
+
merged = branches.each_with_object([]) do |(lo, hi, child), result|
|
|
103
|
+
if !result.empty? && result.last[1] + 1 == lo && result.last[2] == child
|
|
104
|
+
result.last[1] = hi
|
|
105
|
+
else
|
|
106
|
+
result << [lo, hi, child]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
tree = alternatives(merged.map do |lo, hi, child|
|
|
110
|
+
sequence([AST::ByteRange.new(lo: lo, hi: hi, loc: nil), child])
|
|
81
111
|
end)
|
|
112
|
+
@trie_cache[key] = tree
|
|
113
|
+
alternatives(nodes + [tree])
|
|
82
114
|
end
|
|
83
115
|
|
|
84
116
|
def sequence(children)
|
|
85
117
|
flattened = children.flat_map { |child| child.is_a?(AST::Seq) ? child.children : [child] }
|
|
118
|
+
return AST::Fail.new(loc: nil) if flattened.any?(AST::Fail)
|
|
119
|
+
|
|
120
|
+
flattened.reject! { |child| child.is_a?(AST::Empty) }
|
|
86
121
|
return AST::Empty.new(loc: nil) if flattened.empty?
|
|
87
122
|
return flattened.first if flattened.length == 1
|
|
88
123
|
|
|
@@ -91,12 +126,23 @@ module Flexr
|
|
|
91
126
|
|
|
92
127
|
def alternatives(children)
|
|
93
128
|
flattened = children.flat_map { |child| child.is_a?(AST::Alt) ? child.children : [child] }
|
|
94
|
-
|
|
129
|
+
flattened.reject! { |child| child.is_a?(AST::Fail) }
|
|
130
|
+
flattened.uniq!
|
|
131
|
+
return AST::Fail.new(loc: nil) if flattened.empty?
|
|
95
132
|
return flattened.first if flattened.length == 1
|
|
96
133
|
|
|
97
134
|
AST::Alt.new(children: flattened, loc: nil)
|
|
98
135
|
end
|
|
99
136
|
|
|
137
|
+
def repeat(node)
|
|
138
|
+
child = normalize_node(node.child)
|
|
139
|
+
return AST::Empty.new(loc: node.loc) if node.maximum&.zero? || child.is_a?(AST::Empty)
|
|
140
|
+
return node.minimum.zero? ? AST::Empty.new(loc: node.loc) : AST::Fail.new(loc: node.loc) if
|
|
141
|
+
child.is_a?(AST::Fail)
|
|
142
|
+
|
|
143
|
+
AST::Repeat.new(child: child, minimum: node.minimum, maximum: node.maximum, loc: node.loc)
|
|
144
|
+
end
|
|
145
|
+
|
|
100
146
|
def complement(ranges)
|
|
101
147
|
result = []
|
|
102
148
|
cursor = 0
|