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/docs/reference/runtime.md
CHANGED
|
@@ -10,14 +10,22 @@ Lexer.new(input,
|
|
|
10
10
|
filename: nil,
|
|
11
11
|
error_mode: :raise,
|
|
12
12
|
max_token_size: 16 * 1024 * 1024,
|
|
13
|
+
max_lookahead_size: nil,
|
|
14
|
+
max_buffer_size: 64 * 1024 * 1024,
|
|
13
15
|
max_state_stack: 1024,
|
|
16
|
+
max_steps: nil,
|
|
17
|
+
cancellation: nil,
|
|
18
|
+
retain_input: true,
|
|
14
19
|
chunk_size: 64 * 1024)
|
|
15
20
|
```
|
|
16
21
|
|
|
17
22
|
`input` must be a `String` or an object responding to `read`. `filename` is
|
|
18
23
|
copied into locations and lexical errors. `error_mode` is `:raise`, `:token`,
|
|
19
24
|
or `:panic`; unknown values fall back to raising when an unmatched byte is
|
|
20
|
-
handled.
|
|
25
|
+
handled. `max_lookahead_size` defaults to `max_token_size`. `max_steps: nil`
|
|
26
|
+
leaves the step count unbounded; `cancellation` may be a callable accepting
|
|
27
|
+
zero arguments or the lexer. Size limits are non-negative, and `chunk_size`
|
|
28
|
+
must be positive.
|
|
21
29
|
|
|
22
30
|
## Consumption
|
|
23
31
|
|
|
@@ -27,11 +35,13 @@ handled. Size limits are non-negative, and `chunk_size` must be positive.
|
|
|
27
35
|
| `tokens` | Array containing every token until EOF |
|
|
28
36
|
| `each_token` | Enumerator without a block; otherwise yields tokens and returns `self` |
|
|
29
37
|
| `racc_next_token` | `[type, value]`, or `[false, "$end"]` at EOF |
|
|
30
|
-
| `input` | The
|
|
38
|
+
| `input` | The currently retained input string |
|
|
31
39
|
| `buffer` | Internal buffered input object; use only for integrations that need it |
|
|
32
40
|
| `filename` | The configured filename |
|
|
33
41
|
| `error_mode` | The configured default input error mode |
|
|
34
42
|
| `max_token_size` | The configured token-size limit |
|
|
43
|
+
| `max_lookahead_size` | The configured trailing-context limit |
|
|
44
|
+
| `steps` | Work units consumed by scanning |
|
|
35
45
|
|
|
36
46
|
`each_token` yields two arguments instead of one when `token_kind :yield` is
|
|
37
47
|
selected. `tokens` always returns an array of token values.
|
|
@@ -46,4 +56,11 @@ class or another lexer instance.
|
|
|
46
56
|
|
|
47
57
|
IO input is read lazily in chunks. UTF-8 input is checked at codepoint
|
|
48
58
|
boundaries, while binary input is consumed byte by byte. Invalid UTF-8 becomes
|
|
49
|
-
an unmatched-byte error rather than being silently normalized.
|
|
59
|
+
an unmatched-byte error rather than being silently normalized. With
|
|
60
|
+
`retain_input: false`, bytes before the current token are released after each
|
|
61
|
+
match; `input` and `binary_input` then expose only the retained window.
|
|
62
|
+
|
|
63
|
+
`max_token_size` is checked before an action can shorten or materialize an
|
|
64
|
+
oversized match. `max_lookahead_size` separately bounds trailing context, and
|
|
65
|
+
`max_buffer_size` bounds bytes retained at one time. `max_steps` and
|
|
66
|
+
`cancellation` bound CPU work cooperatively.
|
|
@@ -24,10 +24,12 @@ token.
|
|
|
24
24
|
| `line_begin` / `line_end` | One-based line range |
|
|
25
25
|
| `column_begin` / `column_end` | One-based character columns for UTF-8, byte columns for binary input |
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
Line and column positions are tracked incrementally while input is consumed,
|
|
28
|
+
so creating locations does not rescan the input prefix. Column members are
|
|
29
|
+
filled lazily by default; `option :eager_columns` fills them while the location
|
|
30
|
+
is created. The end line and column identify the position after the token's
|
|
31
|
+
last byte, so a token ending at the start of the next line may have an end
|
|
32
|
+
column of 1.
|
|
31
33
|
|
|
32
34
|
`more` extends the location from the first match to the final match. Trailing
|
|
33
35
|
context is not part of the location because it is not consumed.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# flexr v1.1.0
|
|
2
|
+
|
|
3
|
+
Release date: 2026-08-13
|
|
4
|
+
|
|
5
|
+
v1.1.0 strengthens the runtime, generated, and standalone lexer contract while
|
|
6
|
+
keeping the stable public API focused on deterministic, byte-oriented lexing.
|
|
7
|
+
|
|
8
|
+
## Highlights
|
|
9
|
+
|
|
10
|
+
- Runtime, generated, and standalone modes now share action precedence,
|
|
11
|
+
nested-state behavior, static-evaluation rules, immutable compiled
|
|
12
|
+
specifications, and structured error outcomes.
|
|
13
|
+
- Runtime resource usage is bounded with token, lookahead, buffer, step, state
|
|
14
|
+
stack, cancellation, and non-progress guards. Streaming input supports
|
|
15
|
+
sliding-buffer retention and incremental location tracking.
|
|
16
|
+
- Unicode properties and POSIX classes use the vendored Unicode 15.1.0 data
|
|
17
|
+
through the DFA path, including property-aware longest-match behavior and
|
|
18
|
+
Unicode-safe empty-match progress.
|
|
19
|
+
- Regexp parsing and normalization now fail closed for unsupported syntax and
|
|
20
|
+
cover empty-language AST nodes, symbolic repetition, class intersections,
|
|
21
|
+
trailing hyphens, shorthand classes, and exact first-match counterexamples.
|
|
22
|
+
- Compiler and generated-runtime paths use indexed construction, Hopcroft
|
|
23
|
+
minimization, lazy transition representations, backend cost selection, and
|
|
24
|
+
state-specific scanner fast paths.
|
|
25
|
+
- Generated artifact writes are collision-safe, atomic, no-op aware, and carry
|
|
26
|
+
schema, runtime ABI, compiler, and Unicode metadata.
|
|
27
|
+
|
|
28
|
+
## Compatibility notes
|
|
29
|
+
|
|
30
|
+
- Stable backends remain `table`, `direct`, and `auto`.
|
|
31
|
+
- `firstmatch` remains experimental and intentionally does not provide the
|
|
32
|
+
longest-match guarantee.
|
|
33
|
+
- Generated files should be regenerated with flexr v1.1.0 when upgrading from
|
|
34
|
+
v1.0.x. Older generated artifacts remain loadable when their artifact
|
|
35
|
+
metadata is compatible.
|
|
36
|
+
- Unicode behavior is pinned to the vendored Unicode 15.1.0 snapshot rather
|
|
37
|
+
than the host Ruby version.
|
|
38
|
+
|
|
39
|
+
## Verification
|
|
40
|
+
|
|
41
|
+
The release candidate passed the repository's RSpec, RuboCop, documentation,
|
|
42
|
+
generation, parity, differential, fuzz, Unicode, coverage, benchmark, and gem
|
|
43
|
+
build gates on the release environment. See [the release procedure](../RELEASING.md)
|
|
44
|
+
for the commands to repeat before tagging and publishing.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module ActionResolver
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def resolve(skip:, emit:, block:, default:)
|
|
8
|
+
return :skip if skip
|
|
9
|
+
return [:emit, emit.to_sym] if emit
|
|
10
|
+
return block if block
|
|
11
|
+
|
|
12
|
+
default
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
module Flexr
|
|
6
|
+
module ArtifactWriter
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def default_generated_path(source_path)
|
|
10
|
+
return source_path.sub(/\.flexr\.rb\z/, ".rb") if source_path.end_with?(".flexr.rb")
|
|
11
|
+
return source_path.sub(/\.rb\z/, ".generated.rb") if source_path.end_with?(".rb")
|
|
12
|
+
|
|
13
|
+
"#{source_path}.generated.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write!(path, content, source_path: nil)
|
|
17
|
+
ensure_distinct!(source_path, path) if source_path
|
|
18
|
+
return :unchanged if File.file?(path) && File.binread(path) == content.b
|
|
19
|
+
|
|
20
|
+
atomic_replace(path, content)
|
|
21
|
+
:written
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ensure_distinct!(source_path, output_path)
|
|
25
|
+
same_path = File.expand_path(source_path) == File.expand_path(output_path)
|
|
26
|
+
same_file = File.exist?(source_path) && File.exist?(output_path) && File.identical?(source_path, output_path)
|
|
27
|
+
return unless same_path || same_file
|
|
28
|
+
|
|
29
|
+
raise ArgumentError, "refusing to overwrite input file: #{source_path}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def atomic_replace(path, content)
|
|
33
|
+
expanded = File.expand_path(path)
|
|
34
|
+
directory = File.dirname(expanded)
|
|
35
|
+
basename = File.basename(expanded)
|
|
36
|
+
mode = File.exist?(expanded) ? File.stat(expanded).mode & 0o7777 : 0o666 & ~File.umask
|
|
37
|
+
temporary_path = nil
|
|
38
|
+
|
|
39
|
+
Tempfile.create([".#{basename}.", ".tmp"], directory, binmode: true) do |temporary|
|
|
40
|
+
temporary_path = temporary.path
|
|
41
|
+
temporary.write(content)
|
|
42
|
+
temporary.flush
|
|
43
|
+
temporary.fsync
|
|
44
|
+
temporary.chmod(mode)
|
|
45
|
+
temporary.close
|
|
46
|
+
File.rename(temporary_path, expanded)
|
|
47
|
+
temporary_path = nil
|
|
48
|
+
end
|
|
49
|
+
sync_directory(directory)
|
|
50
|
+
ensure
|
|
51
|
+
File.unlink(temporary_path) if temporary_path && File.exist?(temporary_path)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sync_directory(directory)
|
|
55
|
+
File.open(directory, File::RDONLY, &:fsync)
|
|
56
|
+
rescue SystemCallError, IOError
|
|
57
|
+
# Some supported filesystems do not allow fsync on directories. The file
|
|
58
|
+
# itself has already been synced before the atomic rename.
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Flexr
|
|
4
4
|
module Automaton
|
|
5
|
-
Region = Struct.new(:state, :bytes, :regexp, keyword_init: true)
|
|
5
|
+
Region = Struct.new(:state, :bytes, :regexp, :utf8_regexp, keyword_init: true)
|
|
6
6
|
|
|
7
7
|
module Accel
|
|
8
8
|
module_function
|
|
@@ -12,13 +12,18 @@ module Flexr
|
|
|
12
12
|
bytes = Analysis.self_loop_set(dfa, state)
|
|
13
13
|
next if bytes.empty?
|
|
14
14
|
|
|
15
|
-
Region.new(
|
|
15
|
+
Region.new(
|
|
16
|
+
state: state, bytes: bytes.freeze,
|
|
17
|
+
regexp: regexp_for(bytes, binary: true),
|
|
18
|
+
utf8_regexp: bytes.all? { |byte| byte < 128 } ? regexp_for(bytes, binary: false) : nil
|
|
19
|
+
)
|
|
16
20
|
end
|
|
17
21
|
end
|
|
18
22
|
|
|
19
|
-
def regexp_for(bytes)
|
|
23
|
+
def regexp_for(bytes, binary: true)
|
|
20
24
|
source = bytes_to_source(bytes)
|
|
21
|
-
|
|
25
|
+
options = binary ? ::Regexp::NOENCODING : 0
|
|
26
|
+
::Regexp.new("(?:[#{source}])+", options)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def bytes_to_source(bytes)
|
|
@@ -19,13 +19,13 @@ module Flexr
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self_loop_set(dfa, state)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
end
|
|
22
|
+
representatives = Array.new(dfa.class_count)
|
|
23
|
+
dfa.ec.each_with_index { |class_id, byte| representatives[class_id] ||= byte }
|
|
24
|
+
self_loops = representatives.each_index.map do |class_id|
|
|
25
|
+
byte = representatives.fetch(class_id)
|
|
26
|
+
byte && dfa.transition(state, byte) == state
|
|
28
27
|
end
|
|
28
|
+
dfa.ec.each_index.select { |byte| self_loops.fetch(dfa.ec.fetch(byte)) }
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def dead_states(dfa)
|
|
@@ -33,6 +33,47 @@ module Flexr
|
|
|
33
33
|
dfa.accepts[state].empty? && dfa.transitions[state].compact.all? { |destination| destination == state }
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
def firstmatch_counterexample(first, second)
|
|
38
|
+
queue = [[first.start, second.start, false, +"".b]]
|
|
39
|
+
visited = { [first.start, second.start, false] => true }
|
|
40
|
+
bytes = joint_byte_representatives(first, second)
|
|
41
|
+
cursor = 0
|
|
42
|
+
|
|
43
|
+
while cursor < queue.length
|
|
44
|
+
first_state, second_state, first_seen, input = queue.fetch(cursor)
|
|
45
|
+
cursor += 1
|
|
46
|
+
bytes.each do |byte|
|
|
47
|
+
next_first = first_state && first.transition(first_state, byte)
|
|
48
|
+
next_second = second_state && second.transition(second_state, byte)
|
|
49
|
+
next unless next_second
|
|
50
|
+
|
|
51
|
+
next_input = input + byte.chr(Encoding::BINARY)
|
|
52
|
+
next_first_seen = first_seen || accepting?(first, next_first)
|
|
53
|
+
return next_input if next_first_seen && accepting?(second, next_second) && !accepting?(first, next_first)
|
|
54
|
+
|
|
55
|
+
key = [next_first, next_second, next_first_seen]
|
|
56
|
+
next if visited[key]
|
|
57
|
+
|
|
58
|
+
visited[key] = true
|
|
59
|
+
queue << [next_first, next_second, next_first_seen, next_input]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def joint_byte_representatives(first, second)
|
|
66
|
+
representatives = {}
|
|
67
|
+
256.times do |byte|
|
|
68
|
+
key = [first.ec[byte], second.ec[byte]]
|
|
69
|
+
representatives[key] ||= byte
|
|
70
|
+
end
|
|
71
|
+
representatives.values.sort_by { |byte| [byte.between?(32, 126) ? 0 : 1, byte] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def accepting?(dfa, state)
|
|
75
|
+
state && !dfa.accepts.fetch(state).empty?
|
|
76
|
+
end
|
|
36
77
|
end
|
|
37
78
|
end
|
|
38
79
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Automaton
|
|
5
|
+
module BackendCostModel
|
|
6
|
+
Metrics = Struct.new(:dense_bytes, :packed_bytes, :lookup_samples, keyword_init: true) do
|
|
7
|
+
def direct_score
|
|
8
|
+
dense_bytes + lookup_samples
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def table_score
|
|
12
|
+
packed_bytes + (lookup_samples * 3)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def choose(compiled)
|
|
19
|
+
metrics = compiled.machines.values.map { |machine| metrics_for(machine.dfa) }
|
|
20
|
+
direct_score = metrics.sum(&:direct_score)
|
|
21
|
+
table_score = metrics.sum(&:table_score)
|
|
22
|
+
direct_score < table_score ? :direct : :table
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def metrics_for(dfa)
|
|
26
|
+
cells = dfa.states * dfa.class_count
|
|
27
|
+
overrides = dfa.transitions.sum do |row|
|
|
28
|
+
default = row.tally.max_by { |_value, count| count }&.first
|
|
29
|
+
row.count { |value| value != default }
|
|
30
|
+
end
|
|
31
|
+
packed_integers = (overrides * 2) + (dfa.states * 2)
|
|
32
|
+
Metrics.new(
|
|
33
|
+
dense_bytes: cells * 4,
|
|
34
|
+
packed_bytes: packed_integers * 4,
|
|
35
|
+
lookup_samples: dfa.states * [dfa.class_count, 16].min
|
|
36
|
+
).freeze
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Flexr
|
|
4
4
|
module Automaton
|
|
5
|
-
CompiledSpec = Struct.new(:machines, :rules, :states, :stats, :diagnostics, keyword_init: true)
|
|
6
|
-
Machine = Struct.new(:dfa, :state_name, keyword_init: true)
|
|
7
|
-
|
|
8
5
|
class Compiler
|
|
9
6
|
def initialize(spec)
|
|
10
7
|
@spec = spec
|
|
@@ -48,11 +45,6 @@ module Flexr
|
|
|
48
45
|
normalized = []
|
|
49
46
|
rules.each do |rule|
|
|
50
47
|
rule.pattern_conditions = []
|
|
51
|
-
if reference_rule?(rule)
|
|
52
|
-
validate_reference_patterns(rule)
|
|
53
|
-
next
|
|
54
|
-
end
|
|
55
|
-
|
|
56
48
|
rule.patterns.each_with_index do |pattern, pattern_index|
|
|
57
49
|
regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(::Regexp.escape(pattern.to_s))
|
|
58
50
|
encoding = regexp.encoding == Encoding::BINARY ? Encoding::BINARY : @spec.encoding
|
|
@@ -78,32 +70,6 @@ module Flexr
|
|
|
78
70
|
DFA.new(transitions: [[nil]], accepts: [[]], ec: Array.new(256, 0), class_count: 1, start: 0, rule_ids: [])
|
|
79
71
|
end
|
|
80
72
|
|
|
81
|
-
def reference_rule?(rule)
|
|
82
|
-
rule.patterns.any? { |pattern| reference_pattern?(pattern) }
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def reference_pattern?(pattern)
|
|
86
|
-
return false unless pattern.is_a?(::Regexp)
|
|
87
|
-
return true if pattern.source.match?(/\\[pP]\{/) || pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
|
|
88
|
-
|
|
89
|
-
@spec.options[:unicode] == true && @spec.encoding != Encoding::BINARY &&
|
|
90
|
-
pattern.source.match?(/\\[dDwWsS]/)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def validate_reference_patterns(rule)
|
|
94
|
-
rule.patterns.each_with_index do |pattern, pattern_index|
|
|
95
|
-
regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(::Regexp.escape(pattern.to_s))
|
|
96
|
-
encoding = regexp.encoding == Encoding::BINARY ? Encoding::BINARY : @spec.encoding
|
|
97
|
-
parser = Regexp::Parser.new(regexp.source, options: regexp.options, encoding: encoding,
|
|
98
|
-
unicode: @spec.options[:unicode] == true)
|
|
99
|
-
ast = parser.parse
|
|
100
|
-
_body, bol_only, end_anchor = strip_anchors(ast)
|
|
101
|
-
rule.pattern_conditions[pattern_index] = Acceptance.new(
|
|
102
|
-
rule_index: rule.index, pattern_index: pattern_index, bol_only: bol_only, end_anchor: end_anchor
|
|
103
|
-
)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
73
|
def strip_anchors(ast)
|
|
108
74
|
children = ast.is_a?(Regexp::AST::Seq) ? ast.children.dup : [ast]
|
|
109
75
|
children.shift while children.first.is_a?(Regexp::AST::Empty)
|
|
@@ -131,35 +97,38 @@ module Flexr
|
|
|
131
97
|
|
|
132
98
|
def contains_anchor?(node)
|
|
133
99
|
return true if node.is_a?(Regexp::AST::Anchor)
|
|
100
|
+
return contains_anchor?(node.child) if node.is_a?(Regexp::AST::Repeat) || node.is_a?(Regexp::AST::Star)
|
|
134
101
|
return false unless node.respond_to?(:children)
|
|
135
102
|
|
|
136
103
|
node.children.any? { |child| contains_anchor?(child) }
|
|
137
104
|
end
|
|
138
105
|
|
|
139
106
|
def subset_construction(nfa, ec, class_count)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
107
|
+
closures = {}
|
|
108
|
+
closure_for = lambda do |state|
|
|
109
|
+
closures[state] ||= epsilon_closure_for(nfa, state)
|
|
110
|
+
end
|
|
111
|
+
transition_closures = indexed_transition_closures(nfa, ec, closure_for)
|
|
112
|
+
start_set = closure_for.call(nfa.start)
|
|
143
113
|
sets = [start_set]
|
|
144
114
|
ids = { start_set => 0 }
|
|
145
115
|
transitions = []
|
|
146
116
|
accepts = []
|
|
147
117
|
queue = [start_set]
|
|
118
|
+
queue_index = 0
|
|
119
|
+
limit = @spec.options.fetch(:max_dfa_states, 100_000)
|
|
120
|
+
limit = 100_000 unless limit.is_a?(Integer) && limit.positive?
|
|
148
121
|
|
|
149
|
-
|
|
150
|
-
set = queue.
|
|
122
|
+
while queue_index < queue.length
|
|
123
|
+
set = queue.fetch(queue_index)
|
|
124
|
+
queue_index += 1
|
|
151
125
|
state_id = ids.fetch(set)
|
|
152
126
|
transitions[state_id] ||= Array.new(class_count)
|
|
153
127
|
accepts[state_id] = accepting_rules(nfa, set)
|
|
154
|
-
|
|
155
|
-
moved = move(nfa, set, representatives[class_id])
|
|
156
|
-
next if moved.zero?
|
|
157
|
-
closure = epsilon_closure(nfa, moved)
|
|
128
|
+
move_closures(set, transition_closures).each do |class_id, closure|
|
|
158
129
|
destination = ids[closure]
|
|
159
130
|
unless destination
|
|
160
131
|
destination = sets.length
|
|
161
|
-
limit = @spec.options.fetch(:max_dfa_states, 100_000)
|
|
162
|
-
limit = 100_000 unless limit.is_a?(Integer) && limit.positive?
|
|
163
132
|
if destination >= limit
|
|
164
133
|
message = "DFA state limit exceeded while compiling rules #{@active_rule_ids.join(', ')}"
|
|
165
134
|
diagnostic = Diagnostics.error("FLEXR-E006", message,
|
|
@@ -180,13 +149,12 @@ module Flexr
|
|
|
180
149
|
Minimizer.minimize(dfa)
|
|
181
150
|
end
|
|
182
151
|
|
|
183
|
-
def
|
|
184
|
-
closure =
|
|
185
|
-
stack = []
|
|
186
|
-
nfa.states.each_index { |id| stack << id if set.anybits?(1 << id) }
|
|
152
|
+
def epsilon_closure_for(nfa, state)
|
|
153
|
+
closure = 1 << state
|
|
154
|
+
stack = [state]
|
|
187
155
|
until stack.empty?
|
|
188
|
-
|
|
189
|
-
nfa.states[
|
|
156
|
+
current = stack.pop
|
|
157
|
+
nfa.states[current].epsilon.each do |target|
|
|
190
158
|
next if closure.anybits?(1 << target)
|
|
191
159
|
|
|
192
160
|
closure |= 1 << target
|
|
@@ -196,35 +164,52 @@ module Flexr
|
|
|
196
164
|
closure
|
|
197
165
|
end
|
|
198
166
|
|
|
199
|
-
def
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
167
|
+
def indexed_transition_closures(nfa, ec, closure_for)
|
|
168
|
+
nfa.states.map do |state|
|
|
169
|
+
raw = {}
|
|
170
|
+
state.transitions.each do |transition|
|
|
171
|
+
transition.lo.upto(transition.hi).map { |byte| ec.fetch(byte) }.uniq.each do |class_id|
|
|
172
|
+
raw[class_id] = raw.fetch(class_id, 0) | (1 << transition.to)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
raw.transform_values do |targets|
|
|
176
|
+
each_state_id(targets).reduce(0) { |closure, target| closure | closure_for.call(target) }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
206
180
|
|
|
207
|
-
|
|
181
|
+
def move_closures(set, transition_closures)
|
|
182
|
+
each_state_id(set).with_object({}) do |state, moves|
|
|
183
|
+
transition_closures.fetch(state).each do |class_id, closure|
|
|
184
|
+
moves[class_id] = moves.fetch(class_id, 0) | closure
|
|
208
185
|
end
|
|
209
186
|
end
|
|
210
|
-
moved
|
|
211
187
|
end
|
|
212
188
|
|
|
213
189
|
def accepting_rules(nfa, set)
|
|
214
|
-
rules = []
|
|
215
|
-
|
|
216
|
-
next if set.nobits?(1 << state)
|
|
217
|
-
|
|
218
|
-
rules.concat(nfa.states[state].accepts)
|
|
190
|
+
rules = each_state_id(set).with_object([]) do |state, result|
|
|
191
|
+
result.concat(nfa.states[state].accepts)
|
|
219
192
|
end
|
|
220
193
|
rules.uniq.sort_by { |acceptance| [acceptance.rule_index, acceptance.pattern_index] }
|
|
221
194
|
end
|
|
222
195
|
|
|
196
|
+
def each_state_id(set)
|
|
197
|
+
return enum_for(__method__, set) unless block_given?
|
|
198
|
+
|
|
199
|
+
remaining = set
|
|
200
|
+
until remaining.zero?
|
|
201
|
+
bit = remaining & -remaining
|
|
202
|
+
yield bit.bit_length - 1
|
|
203
|
+
remaining ^= bit
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
223
207
|
def validate_rules
|
|
224
208
|
raise CompileError, "firstmatch requires option :experimental" if @spec.backend == :firstmatch && !@spec.options[:experimental]
|
|
225
209
|
|
|
226
210
|
@spec.rules.each do |rule|
|
|
227
211
|
raise CompileError, "rule #{rule.index} has no pattern" if rule.patterns.empty?
|
|
212
|
+
parse_regexp(rule.trailing) if rule.trailing.is_a?(::Regexp)
|
|
228
213
|
next if @spec.options[:allow_empty_match]
|
|
229
214
|
|
|
230
215
|
rule.patterns.each do |pattern|
|
|
@@ -249,8 +234,6 @@ module Flexr
|
|
|
249
234
|
present = compiled.machines.values.flat_map do |machine|
|
|
250
235
|
machine.dfa.accepts.filter_map { |acceptances| acceptances.min_by(&:rule_index)&.rule_index }
|
|
251
236
|
end
|
|
252
|
-
reference_rules = @spec.rules.select { |rule| reference_rule?(rule) && rule_active_anywhere?(rule) }
|
|
253
|
-
present.concat(reference_rules.map(&:index))
|
|
254
237
|
shadowers = shadowed_rules(compiled)
|
|
255
238
|
diagnostics.concat(@spec.rules.reject { |rule| present.include?(rule.index) }.map do |rule|
|
|
256
239
|
winners = shadowers.fetch(rule.index, []).uniq.sort
|
|
@@ -344,10 +327,6 @@ module Flexr
|
|
|
344
327
|
rules.combination(2).to_a
|
|
345
328
|
end
|
|
346
329
|
|
|
347
|
-
def rule_active_anywhere?(rule)
|
|
348
|
-
@spec.states.keys.any? { |state_name| rules_for(state_name).include?(rule) }
|
|
349
|
-
end
|
|
350
|
-
|
|
351
330
|
def capture_rules
|
|
352
331
|
@spec.rules.select do |rule|
|
|
353
332
|
rule.patterns.any? do |pattern|
|