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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edb2c916db82e07c56290ab6365d50bf9a0a148278ca811a100063031315bbf5
|
|
4
|
+
data.tar.gz: 13c08660e91681ab2143766d58072c4c4e3d081b5899a18568a772bfd80810b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aef18f76825b6544c03a3115a1dfeb8cd469560fb4bff23b2fa3e57cc9504e780a318ebfcc06d1ff7c8807c217b813a6a2768d154a8596cf7103347fd97e399e
|
|
7
|
+
data.tar.gz: 0355b004bc8ff95a00fd4be5fd4644f4cce1a0f2feaad49da48ef023fc594453de208600d542a26a6bc6fcb4a645864770816ed8d4b4411e8607860801d1c43c
|
data/Rakefile
CHANGED
|
@@ -86,6 +86,36 @@ module FlexrVerification
|
|
|
86
86
|
codepoints.pack("U*")
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
def merge_ranges(ranges)
|
|
90
|
+
ranges.sort_by(&:first).each_with_object([]) do |range, merged|
|
|
91
|
+
if merged.empty? || range.first > merged.last.last + 1
|
|
92
|
+
merged << range.dup
|
|
93
|
+
else
|
|
94
|
+
merged.last[1] = [merged.last.last, range.last].max
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def lexer_outcome(lexer_class, input, **options)
|
|
100
|
+
{ kind: :tokens, tokens: lexer_class.new(input.dup, **options).tokens }
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
{
|
|
103
|
+
kind: :error,
|
|
104
|
+
class: e.class.name,
|
|
105
|
+
message: e.message,
|
|
106
|
+
code: if e.respond_to?(:code)
|
|
107
|
+
e.code
|
|
108
|
+
elsif e.respond_to?(:diagnostic)
|
|
109
|
+
e.diagnostic&.code
|
|
110
|
+
end,
|
|
111
|
+
filename: e.respond_to?(:filename) ? e.filename : nil,
|
|
112
|
+
byte_pos: e.respond_to?(:byte_pos) ? e.byte_pos : nil,
|
|
113
|
+
line: e.respond_to?(:line) ? e.line : nil,
|
|
114
|
+
rule: e.respond_to?(:rule) ? e.rule : nil,
|
|
115
|
+
text: e.respond_to?(:text) ? e.text : nil
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
89
119
|
def load_runtime(spec)
|
|
90
120
|
class_name = runtime_class_name(spec)
|
|
91
121
|
existing = constantize(class_name)
|
|
@@ -135,11 +165,11 @@ module FlexrVerification
|
|
|
135
165
|
inputs = [input_for(spec)] + Array.new(32) do
|
|
136
166
|
Array.new(random.rand(48..96)) { random.rand(32..126) }.pack("C*")
|
|
137
167
|
end
|
|
138
|
-
options = lexer.__flexr_config.options
|
|
139
|
-
original_accel = options.fetch(:accel, :auto)
|
|
140
168
|
accelerated = inputs.map { |input| lexer.new(input, error_mode: :panic).tokens }
|
|
141
|
-
|
|
142
|
-
|
|
169
|
+
remove_constant(runtime_class_name(spec))
|
|
170
|
+
reference_lexer = load_runtime(spec)
|
|
171
|
+
reference_lexer.accel(:none)
|
|
172
|
+
reference = inputs.map { |input| reference_lexer.new(input, error_mode: :panic).tokens }
|
|
143
173
|
raise "acceleration token mismatch in #{spec}" unless accelerated == reference
|
|
144
174
|
|
|
145
175
|
lexer.compile!.machines.each_value do |machine|
|
|
@@ -153,8 +183,6 @@ module FlexrVerification
|
|
|
153
183
|
end
|
|
154
184
|
end
|
|
155
185
|
end
|
|
156
|
-
ensure
|
|
157
|
-
options[:accel] = original_accel if options && original_accel
|
|
158
186
|
end
|
|
159
187
|
|
|
160
188
|
def verify_dogfood(spec)
|
|
@@ -290,16 +318,12 @@ rescue Errno::ENOENT => e
|
|
|
290
318
|
end
|
|
291
319
|
|
|
292
320
|
task "direct:verify" do
|
|
293
|
-
unless defined?(RubyVM::InstructionSequence)
|
|
294
|
-
puts "direct: disassembly unavailable on #{RUBY_ENGINE}; skipped"
|
|
295
|
-
next
|
|
296
|
-
end
|
|
297
|
-
|
|
298
321
|
spec = File.join(FlexrVerification::ROOT, "examples/json/lexer.flexr.rb")
|
|
299
|
-
|
|
300
|
-
abort "direct
|
|
322
|
+
source = Flexr::Generator.new(spec, options: { backend: :direct }).generate
|
|
323
|
+
abort "direct payload is missing its flattened transition representation" unless
|
|
324
|
+
source.include?("direct: {nxt:") && !source.include?("__flexr_generated_direct_transition")
|
|
301
325
|
|
|
302
|
-
puts "direct:
|
|
326
|
+
puts "direct: single flattened representation present"
|
|
303
327
|
end
|
|
304
328
|
|
|
305
329
|
task "unicode:verify" do
|
|
@@ -315,6 +339,14 @@ task "unicode:verify" do
|
|
|
315
339
|
previous = hi
|
|
316
340
|
end
|
|
317
341
|
end
|
|
342
|
+
expected_alnum = FlexrVerification.merge_ranges(
|
|
343
|
+
properties.fetch("L") + properties.fetch("N") + properties.fetch("Other_Alphabetic")
|
|
344
|
+
)
|
|
345
|
+
abort "vendored Alnum table is stale" unless properties.fetch("Alnum") == expected_alnum
|
|
346
|
+
|
|
347
|
+
expected_word = FlexrVerification.merge_ranges(expected_alnum + [[0x5f, 0x5f]])
|
|
348
|
+
abort "vendored Word table is stale" unless properties.fetch("Word") == expected_word
|
|
349
|
+
|
|
318
350
|
scalar_count = 0
|
|
319
351
|
(0..0x10_ffff).each do |codepoint|
|
|
320
352
|
next if codepoint.between?(0xd800, 0xdfff)
|
|
@@ -365,6 +397,9 @@ namespace :test do
|
|
|
365
397
|
random = Random.new(Integer(ENV.fetch("FLEXR_SEED", "17"), 10))
|
|
366
398
|
unicode_inputs = ["", "a", "あ", "é", "ß", "Ω", "١", " ", "aあ", "éΩ", [0x18db8].pack("U")].freeze
|
|
367
399
|
compiled = {}
|
|
400
|
+
host_patterns = {}
|
|
401
|
+
independent_cases = 0
|
|
402
|
+
version_mismatches = 0
|
|
368
403
|
cases.times do
|
|
369
404
|
pattern = patterns[random.rand(patterns.length)]
|
|
370
405
|
input = if random.rand(3).zero?
|
|
@@ -372,26 +407,38 @@ namespace :test do
|
|
|
372
407
|
else
|
|
373
408
|
Array.new(random.rand(10)) { random.rand(32..126) }.pack("C*")
|
|
374
409
|
end
|
|
375
|
-
|
|
410
|
+
key = [pattern.source, pattern.options]
|
|
411
|
+
host_expected = begin
|
|
412
|
+
(host_patterns[key] ||= Regexp.new("\\A(?:#{pattern.source})\\z", pattern.options)).match?(input)
|
|
413
|
+
rescue RegexpError, ArgumentError, EncodingError
|
|
414
|
+
false
|
|
415
|
+
end
|
|
416
|
+
if Flexr.reference_pattern?(pattern)
|
|
376
417
|
reference = Flexr::Unicode::ReferenceRegexp.compiled(
|
|
377
418
|
pattern, encoding: pattern.encoding, options: pattern.options, unicode: false
|
|
378
419
|
)
|
|
379
420
|
match = reference.match(input, 0)
|
|
380
|
-
if match
|
|
421
|
+
vendored_expected = if match
|
|
381
422
|
match.begin(0).zero? && match[0].bytesize == input.bytesize
|
|
382
423
|
else
|
|
383
424
|
false
|
|
384
425
|
end
|
|
385
|
-
|
|
386
|
-
|
|
426
|
+
unless vendored_expected == host_expected
|
|
427
|
+
version_mismatches += 1
|
|
428
|
+
next
|
|
429
|
+
end
|
|
387
430
|
end
|
|
388
|
-
|
|
431
|
+
independent_cases += 1
|
|
389
432
|
actual = (compiled[key] ||= Flexr.compile_pattern(pattern)).accept?(input)
|
|
390
|
-
next if
|
|
433
|
+
next if host_expected == actual
|
|
391
434
|
|
|
392
|
-
abort "differential mismatch: #{pattern.inspect} #{input.inspect}
|
|
435
|
+
abort "differential mismatch: #{pattern.inspect} #{input.inspect} " \
|
|
436
|
+
"expected=#{host_expected} actual=#{actual}"
|
|
393
437
|
end
|
|
394
|
-
|
|
438
|
+
abort "differential did not exercise an independent host oracle" if independent_cases.zero?
|
|
439
|
+
|
|
440
|
+
puts "differential: #{independent_cases} independent cases passed " \
|
|
441
|
+
"(#{version_mismatches} host/vendored Unicode differences skipped)"
|
|
395
442
|
end
|
|
396
443
|
end
|
|
397
444
|
|
|
@@ -425,13 +472,12 @@ task :fuzz do
|
|
|
425
472
|
(FlexrVerification.random_unicode_string(random, max_codepoints: 16) +
|
|
426
473
|
Array.new(random.rand(64)) { random.rand(32..126) }.pack("C*")).force_encoding(Encoding::UTF_8)
|
|
427
474
|
end
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
next if
|
|
475
|
+
runtime_outcome = FlexrVerification.lexer_outcome(runtime_lexer, input, error_mode: :panic)
|
|
476
|
+
generated_outcome = FlexrVerification.lexer_outcome(generated_lexer, input, error_mode: :panic)
|
|
477
|
+
next if runtime_outcome == generated_outcome
|
|
431
478
|
|
|
432
|
-
abort "fuzz mode mismatch: #{spec} input=#{input.inspect}
|
|
433
|
-
|
|
434
|
-
# Invalid input and user-defined error actions are expected fuzz outcomes.
|
|
479
|
+
abort "fuzz mode mismatch: #{spec} input=#{input.inspect} " \
|
|
480
|
+
"runtime=#{runtime_outcome.inspect} generated=#{generated_outcome.inspect}"
|
|
435
481
|
end
|
|
436
482
|
ensure
|
|
437
483
|
FileUtils.rm_f(generated_path) if generated_path
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d01c0b69f4f2fedd572e90000f4fc670524b01089a6942b94b1fc1c1cd27f95e
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
f6d8418031b657a328d3ad62fcd780a43bbf4731e9d0e390120dd219894a515e
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1f53aa34bf24a92fd03096648b3290bbfda76222078c0c58966e259fd3a13aca
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
5bb597bfacaf3217d2852185a9c4e90833a3cb6da02e114817d00d00dd3a3aec
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0285b7b1ceff88a7eddcd1f7d3ac87b0a4bce1e4162f5e64205549aacd576dce
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6abde13a982ee9f529cc2182dad6cefd1f8608af295981fdfe8671e6b46c7f99
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
cb3836416ff7db7877efa68dc4f921755af9ebfb1e03aaa440892e12b6b7ac35
|
data/docs/README.md
CHANGED
|
@@ -61,4 +61,5 @@ pages explain how to use those specifications.
|
|
|
61
61
|
|
|
62
62
|
The [performance log](perf-log.md), [architecture decisions](adr/),
|
|
63
63
|
[internals notes](internals/README.md), and [release procedure](RELEASING.md)
|
|
64
|
-
are maintainer-facing documents.
|
|
64
|
+
are maintainer-facing documents. Release notes are kept under
|
|
65
|
+
[releases](releases/v1.1.0.md).
|
|
@@ -8,26 +8,29 @@ target for `firstmatch`.
|
|
|
8
8
|
|
|
9
9
|
## `direct`
|
|
10
10
|
|
|
11
|
-
The stable dense-dispatch backend. It emits
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
The stable dense-dispatch backend. It emits one flattened transition structure
|
|
12
|
+
and avoids retaining a second packed or row table. It can reduce lookup
|
|
13
|
+
overhead at the cost of artifact size and preserves the table backend's
|
|
14
|
+
leftmost-longest behavior.
|
|
14
15
|
|
|
15
16
|
## `auto`
|
|
16
17
|
|
|
17
|
-
The stable selection policy. At compile time it
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
The stable selection policy. At compile time it compares estimated dense bytes,
|
|
19
|
+
packed bytes, and lookup cost for each lexical-state machine. The effective
|
|
20
|
+
backend is recorded in generated headers.
|
|
20
21
|
|
|
21
22
|
## `firstmatch`
|
|
22
23
|
|
|
23
24
|
Experimental compatibility behavior. It requires `option :experimental` and
|
|
24
|
-
can return a shorter match merely because its rule appears earlier.
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
can return a shorter match merely because its rule appears earlier. Runtime and
|
|
26
|
+
generated artifacts preserve that same behavior. Potential differences from
|
|
27
|
+
the stable longest-match backends are reported with `FLEXR-W010`.
|
|
27
28
|
|
|
28
29
|
## Acceleration and packing
|
|
29
30
|
|
|
30
31
|
Region acceleration and packed tables are representations, not semantic
|
|
31
32
|
backends. `accel` can be disabled for debugging or equivalence tests. Table
|
|
32
33
|
compression and packed Base64 output trade source size against loading cost;
|
|
33
|
-
|
|
34
|
+
packed transitions are queried without creating a second dense table, and a
|
|
35
|
+
dense row is materialized lazily only for inspection. These choices do not
|
|
36
|
+
change token streams.
|
|
@@ -11,8 +11,10 @@ extends the selection end without extending the consumed token end.
|
|
|
11
11
|
|
|
12
12
|
The matcher is byte-oriented so binary input and invalid UTF-8 have deterministic
|
|
13
13
|
behavior. UTF-8 patterns still require valid codepoint boundaries before a
|
|
14
|
-
match is accepted.
|
|
15
|
-
|
|
14
|
+
match is accepted. Unicode properties and POSIX classes are expanded from the
|
|
15
|
+
vendored UCD into this same model rather than matched by a separate host-regexp
|
|
16
|
+
path. The runtime and generated scanner share the compiled model; acceleration
|
|
17
|
+
may skip self-loop bytes but cannot change the winner.
|
|
16
18
|
|
|
17
19
|
`firstmatch` is intentionally separate. It picks the first rule that has a
|
|
18
20
|
matching alternative and therefore is useful only for compatibility migrations
|
|
@@ -13,6 +13,13 @@ to discover dynamic patterns. Use it only with trusted files and a trusted
|
|
|
13
13
|
environment. The same rule applies to generated output: review or sign the
|
|
14
14
|
artifact before loading it in a process with sensitive access.
|
|
15
15
|
|
|
16
|
-
The runtime's `max_token_size`
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
The runtime's `max_token_size`, `max_lookahead_size`, `max_buffer_size`,
|
|
17
|
+
`max_state_stack`, and optional `max_steps` limits provide resource guards for
|
|
18
|
+
input processing. A `cancellation` callback can stop concurrent work
|
|
19
|
+
cooperatively. Limits are checked while scanning, before an oversized token's
|
|
20
|
+
action runs. They are operational controls, not a security sandbox and do not
|
|
21
|
+
protect against arbitrary Ruby actions.
|
|
22
|
+
|
|
23
|
+
Use `retain_input: false` for long-lived streaming lexers that do not need the
|
|
24
|
+
complete input after consumption. This makes the buffer a sliding window;
|
|
25
|
+
`input` and `binary_input` no longer contain discarded prefixes.
|
|
@@ -8,7 +8,9 @@ allowing Unicode-aware patterns.
|
|
|
8
8
|
The vendored Unicode Character Database snapshot, currently 15.1.0, is the
|
|
9
9
|
compatibility oracle for `\p{...}`, POSIX properties where a Unicode property
|
|
10
10
|
is defined, Unicode shorthand behavior with `option :unicode`, and simple case
|
|
11
|
-
folding.
|
|
11
|
+
folding. Property ranges are compiled into the same DFA as ordinary rules, so
|
|
12
|
+
they preserve leftmost-longest matching and streaming behavior. Host Ruby
|
|
13
|
+
Unicode tables are not used as a cross-version oracle.
|
|
12
14
|
|
|
13
15
|
Use `encoding Encoding::BINARY` for protocol or byte-oriented formats. Use the
|
|
14
16
|
default UTF-8 encoding for text. A pattern with invalid UTF-8 or an input that
|
|
@@ -6,9 +6,11 @@ Use `--standalone` to embed the flexr runtime into the generated file:
|
|
|
6
6
|
flexr lexer.flexr.rb --standalone -o lexer_standalone.rb
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
The generated file removes `require "flexr"` and includes the runtime
|
|
10
|
-
needed to execute
|
|
11
|
-
|
|
9
|
+
The generated file removes `require "flexr"` and includes only the runtime
|
|
10
|
+
sources needed to execute its compiled tables. Compiler, generator, and
|
|
11
|
+
minimizer sources are omitted. The regexp reference matcher and Unicode tables
|
|
12
|
+
are included only when the selected backend or trailing context needs them. It
|
|
13
|
+
can be loaded by a Ruby process that does not have the flexr gem installed:
|
|
12
14
|
|
|
13
15
|
```sh
|
|
14
16
|
ruby -e 'load ARGV.fetch(0); p Lexer.new("12").tokens' lexer_standalone.rb
|
|
@@ -13,9 +13,9 @@ Ruby constants, comments, requires, and non-DSL code remain in the file.
|
|
|
13
13
|
|
|
14
14
|
## Static expressions
|
|
15
15
|
|
|
16
|
-
The default path accepts regexp and string literals, arrays of
|
|
17
|
-
values, constants, ranges, interpolation whose values are static,
|
|
18
|
-
and `Regexp.union`. A call whose value depends on runtime state raises
|
|
16
|
+
The default path accepts regexp and string literals, arrays and hashes of
|
|
17
|
+
supported values, constants, ranges, interpolation whose values are static,
|
|
18
|
+
`.freeze`, and `Regexp.union`. Constants follow Ruby source order. A call whose value depends on runtime state raises
|
|
19
19
|
`FLEXR-E017`. See the [static-expression reference](../reference/generated-artifacts.md)
|
|
20
20
|
for the exact boundary.
|
|
21
21
|
|
|
@@ -29,4 +29,7 @@ instance with `lexer.on_error = ->(error) { :skip }`, `:raise`, `:token`, or
|
|
|
29
29
|
byte position, line, and text.
|
|
30
30
|
|
|
31
31
|
`error!` creates the same error from an action. `max_token_size` protects the
|
|
32
|
-
runtime from unexpectedly large tokens and raises `FLEXR-E012`
|
|
32
|
+
runtime from unexpectedly large tokens and raises `FLEXR-E012` before the
|
|
33
|
+
action runs. Lookahead, buffer, step, state-stack, cancellation, and progress
|
|
34
|
+
guards likewise raise structured runtime errors; their stable codes are listed
|
|
35
|
+
in the [diagnostics catalog](../reference/diagnostics.md).
|
|
@@ -20,6 +20,11 @@ in chunks; `chunk_size:` controls the buffer size. `next_token` returns one
|
|
|
20
20
|
token or `nil` at EOF, `tokens` drains the lexer into an array, and
|
|
21
21
|
`each_token` returns an enumerator when no block is given.
|
|
22
22
|
|
|
23
|
+
For a long-lived stream that does not need old input, pass
|
|
24
|
+
`retain_input: false`. Combine it with `max_buffer_size:`, `max_token_size:`,
|
|
25
|
+
and `max_lookahead_size:` to keep memory bounded. Use `max_steps:` or a
|
|
26
|
+
`cancellation:` callback when request-level work must also be bounded.
|
|
27
|
+
|
|
23
28
|
Use runtime mode when the specification deliberately depends on ordinary Ruby
|
|
24
29
|
execution. Static generation is a better fit when startup cost, deployment
|
|
25
30
|
reproducibility, or a generator-free runtime matters.
|
|
@@ -21,7 +21,8 @@ Locations use byte offsets for `byte_begin` and `byte_end`, with an exclusive
|
|
|
21
21
|
end offset. Lines and columns are one-based. UTF-8 columns count characters;
|
|
22
22
|
binary columns count bytes. Column values are computed lazily unless
|
|
23
23
|
`option :eager_columns` is set. `filename:` on the lexer is copied into each
|
|
24
|
-
location and lexical error.
|
|
24
|
+
location and lexical error. The lexer tracks line and column state
|
|
25
|
+
incrementally; locations do not rescan all previously consumed input.
|
|
25
26
|
|
|
26
27
|
`more` joins successive matches into one `text` value, so `last_location` spans
|
|
27
28
|
the complete assembled token. See the [token and location contract](../reference/tokens-and-locations.md).
|
|
@@ -9,9 +9,9 @@ bundle exec rake bench:regression
|
|
|
9
9
|
|
|
10
10
|
Use `token_kind :yield` when the consumer streams tokens and does not need an
|
|
11
11
|
intermediate token array. Use generated mode to move DFA construction out of
|
|
12
|
-
application startup. `backend :auto`
|
|
13
|
-
|
|
14
|
-
longest-match semantics.
|
|
12
|
+
application startup. `backend :auto` compares dense memory, packed memory, and
|
|
13
|
+
lookup costs rather than using a fixed table-size threshold; `:table` and
|
|
14
|
+
`:direct` preserve the same longest-match semantics.
|
|
15
15
|
|
|
16
16
|
Acceleration is an optimization over the DFA, not a second matcher. `accel
|
|
17
17
|
:auto` selects an available safe path, while `:strscan`, `:regexp`, and `:none`
|
data/docs/perf-log.md
CHANGED
|
@@ -54,3 +54,19 @@ the following local estimates (one iteration, `backend :direct`):
|
|
|
54
54
|
|
|
55
55
|
The padded corpus can be reproduced with
|
|
56
56
|
`FLEXR_JSON_PADDING_BYTES=100 ruby benchmark/corpora/generate_json.rb`.
|
|
57
|
+
|
|
58
|
+
## 2026-08-12
|
|
59
|
+
|
|
60
|
+
After adding bounded scanning and the compiler/generated-runtime
|
|
61
|
+
optimizations, the same Ruby 4.0.0 host produced this three-iteration sample:
|
|
62
|
+
|
|
63
|
+
| mode | MB/s | tokens/s | allocations/token | handwritten ratio |
|
|
64
|
+
|---|---:|---:|---:|---:|
|
|
65
|
+
| runtime | 1.603 | 572,508 | 5.2 | 0.203x |
|
|
66
|
+
| generated | 1.607 | 573,857 | 5.21 | 0.203x |
|
|
67
|
+
| handwritten | 7.902 | 2,822,254 | 3.4 | 1.000x |
|
|
68
|
+
|
|
69
|
+
Both the portable baseline comparison and the ordinary 0.18x performance
|
|
70
|
+
floor pass in this sample. `auto` acceleration now stops trying a region after
|
|
71
|
+
repeated short runs, while explicit `strscan` and `regexp` modes remain fixed
|
|
72
|
+
choices for workloads that have measured long runs.
|
data/docs/reference/actions.md
CHANGED
|
@@ -43,5 +43,8 @@ instance as `self`.
|
|
|
43
43
|
| `less(count)` | Keep only the first `count` bytes of the current match |
|
|
44
44
|
| `more` | Join the next match into the current token text |
|
|
45
45
|
|
|
46
|
-
`less` cannot exceed the current matched byte count.
|
|
47
|
-
|
|
46
|
+
`less` cannot exceed the current matched byte count. In UTF-8 mode its result
|
|
47
|
+
must also be a codepoint boundary. Returning to the same byte and state is a
|
|
48
|
+
non-progress error; state-changing rescan cycles are detected as well. `more`
|
|
49
|
+
is finalized after the action; call `emit` only after the complete text has
|
|
50
|
+
been assembled.
|
|
@@ -21,6 +21,14 @@ Use `flexr check SPEC --format json` to consume the structured form.
|
|
|
21
21
|
| `FLEXR-E017` | Pattern is not statically resolvable | Rewrite as a static expression or use trusted `--eval` |
|
|
22
22
|
| `FLEXR-E018` | Rule pattern or trailing context has an invalid type | Use a regexp, string, or valid array |
|
|
23
23
|
| `FLEXR-E019` | Prism is unavailable for generation | Install the generator dependencies |
|
|
24
|
+
| `FLEXR-E020` | Generated artifact metadata is missing or incompatible | Regenerate with the installed flexr version |
|
|
25
|
+
| `FLEXR-E021` | An action or state cycle made no input progress | Consume input or break the state cycle |
|
|
26
|
+
| `FLEXR-E022` | The retained streaming buffer is too large | Shorten tokens, raise the limit, or disable retention |
|
|
27
|
+
| `FLEXR-E023` | Runtime work exceeded `max_steps` | Raise the limit or simplify the specification |
|
|
28
|
+
| `FLEXR-E024` | Trailing context exceeded its lookahead limit | Bound the context or raise the limit |
|
|
29
|
+
| `FLEXR-E025` | The lexical state stack overflowed | Remove recursive pushes or raise the limit |
|
|
30
|
+
| `FLEXR-E026` | A cancellation callback stopped lexing | Resume with a new lexer when ready |
|
|
31
|
+
| `FLEXR-E027` | An error callback returned an unknown action | Return `:skip`, `:raise`, `:token`, or `:halt` |
|
|
24
32
|
|
|
25
33
|
## Warnings
|
|
26
34
|
|
data/docs/reference/dsl.md
CHANGED
|
@@ -25,6 +25,11 @@ Creates one or more named states and registers rules in the block for those
|
|
|
25
25
|
states. The block is required. Named states are exclusive by default. An
|
|
26
26
|
inclusive state also sees rules registered for `:initial`.
|
|
27
27
|
|
|
28
|
+
Nested state blocks register their rules for every enclosing state, matching
|
|
29
|
+
Ruby runtime evaluation. Repeating a state declaration with a different
|
|
30
|
+
`inclusive:` value raises immediately instead of silently retaining or replacing
|
|
31
|
+
one declaration.
|
|
32
|
+
|
|
28
33
|
## `all_states(&block)`
|
|
29
34
|
|
|
30
35
|
Registers the block for every state known when the call is evaluated, including
|
|
@@ -65,8 +70,9 @@ model is valid.
|
|
|
65
70
|
|
|
66
71
|
Adds boolean options to the specification. Stable options include
|
|
67
72
|
`:unicode`, `:eager_columns`, and `:allow_empty_match`. `:experimental` is a
|
|
68
|
-
capability opt-in for experimental behavior such as `:firstmatch`.
|
|
69
|
-
|
|
73
|
+
capability opt-in for experimental behavior such as `:firstmatch`.
|
|
74
|
+
`:standalone` is build metadata used by standalone generation. Unknown option
|
|
75
|
+
names raise immediately so misspellings cannot silently change behavior.
|
|
70
76
|
|
|
71
77
|
## `accel(value)`
|
|
72
78
|
|
|
@@ -76,6 +82,14 @@ source of truth. Affected trailing-context rules cannot use region acceleration.
|
|
|
76
82
|
|
|
77
83
|
## Compilation helpers
|
|
78
84
|
|
|
79
|
-
`compile!` and `dfa` exist for diagnostics and integration internals.
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
`compile!` and `dfa` exist for diagnostics and integration internals. The first
|
|
86
|
+
compilation makes the specification immutable; later calls to `rule`, `state`,
|
|
87
|
+
`backend`, `option`, and other declaration methods raise
|
|
88
|
+
`Flexr::FrozenSpecificationError`. This keeps the compiled DFA and its DSL
|
|
89
|
+
definition from diverging. These helpers are not required for normal use and
|
|
90
|
+
are not compatibility-stable; use the lexer constructor and token methods
|
|
91
|
+
instead. See [public API](public-api.md).
|
|
92
|
+
|
|
93
|
+
Subclassing a custom lexer class starts a new, empty specification; rules and
|
|
94
|
+
states are not inherited. This avoids silently changing rule order or state
|
|
95
|
+
membership in the child class.
|
data/docs/reference/errors.md
CHANGED
|
@@ -11,13 +11,16 @@ Compile-time failures raise `Flexr::CompileError` or its
|
|
|
11
11
|
|
|
12
12
|
`Flexr::LexError` records the message, filename, byte position, line, matched
|
|
13
13
|
text, and optional diagnostic. `Flexr::Runtime::TokenTooLargeError` is a
|
|
14
|
-
`LexError` with code `FLEXR-E012`.
|
|
15
|
-
`
|
|
14
|
+
`LexError` with code `FLEXR-E012`. Other runtime guard failures use stable codes
|
|
15
|
+
`FLEXR-E021` through `FLEXR-E027`; see the
|
|
16
|
+
[diagnostics catalog](diagnostics.md). `StateStackOverflowError` is raised when
|
|
17
|
+
a `push` would exceed `max_state_stack`.
|
|
16
18
|
|
|
17
19
|
The default `error_mode: :raise` raises the lexical error. `:token` queues
|
|
18
20
|
`[:error, text]`; `:panic` discards the unmatched byte and continues. An
|
|
19
21
|
`on_error` callback can return `:skip`, `:raise`, `:token`, or `:halt` for
|
|
20
|
-
instance-specific control.
|
|
22
|
+
instance-specific control. Any other result raises
|
|
23
|
+
`InvalidRecoveryActionError` instead of silently applying the default policy.
|
|
21
24
|
|
|
22
25
|
## Import failures
|
|
23
26
|
|
|
@@ -27,14 +27,24 @@ Methods with runtime-dependent results, `Time`, IO, environment reads, and
|
|
|
27
27
|
arbitrary dynamic calls produce `FLEXR-E017`. `--eval` is the explicit escape
|
|
28
28
|
hatch and executes the complete specification.
|
|
29
29
|
|
|
30
|
+
Constants are resolved in source order, array splats are flattened with Ruby
|
|
31
|
+
semantics, and regexp interpolation retains the embedded regexp's options.
|
|
32
|
+
Receiver-qualified DSL calls, DSL hidden in conditionals or other unsupported
|
|
33
|
+
control flow, unknown DSL keywords, and files containing multiple candidate
|
|
34
|
+
lexer classes fail closed with `FLEXR-E017`. A file intended for static
|
|
35
|
+
generation should contain one lexer class and place DSL statements directly in
|
|
36
|
+
that class or a `state`/`all_states` body.
|
|
37
|
+
|
|
30
38
|
## Source transformation
|
|
31
39
|
|
|
32
40
|
The generator removes DSL call spans, inserts `Flexr::Generated.install_compiled!`,
|
|
33
|
-
and adds generated scanner methods.
|
|
41
|
+
and adds generated scanner methods. Source edits use Prism spans, so heredocs and
|
|
42
|
+
the `__END__` data section are not rewritten. User actions remain Ruby source. A generated
|
|
34
43
|
header records:
|
|
35
44
|
|
|
36
45
|
- source path;
|
|
37
46
|
- SHA-256 digest of the generated payload;
|
|
47
|
+
- artifact schema and runtime ABI versions;
|
|
38
48
|
- vendored Unicode version;
|
|
39
49
|
- effective backend;
|
|
40
50
|
- whether compilation used eval; and
|
|
@@ -43,8 +53,25 @@ header records:
|
|
|
43
53
|
Do not edit generated files by hand. Regenerate them when the specification,
|
|
44
54
|
generator, backend, table format, or Unicode snapshot changes.
|
|
45
55
|
|
|
56
|
+
Compiled payloads also record the artifact schema, compiler version, runtime ABI,
|
|
57
|
+
and Unicode version. Loading fails with `FLEXR-E020` when those contracts are
|
|
58
|
+
missing or incompatible. This prevents a stale artifact from silently running
|
|
59
|
+
against a runtime with different table or Unicode semantics.
|
|
60
|
+
|
|
61
|
+
Generation refuses to write through the input path, including aliases made with
|
|
62
|
+
symbolic or hard links. Outputs are written to a file in the destination directory,
|
|
63
|
+
synced, and atomically renamed. An unchanged output is left untouched so build
|
|
64
|
+
timestamps remain stable. For input names other than `*.flexr.rb`, the default
|
|
65
|
+
output is `*.generated.rb`.
|
|
66
|
+
|
|
46
67
|
## Table formats
|
|
47
68
|
|
|
48
69
|
`--table-compression none|rows|full` controls row packing. `--table-format
|
|
49
70
|
literal|packed` controls whether packed arrays are emitted as Ruby literals or
|
|
50
|
-
Base64.
|
|
71
|
+
Base64. Packed artifacts retain and query their packed representation; the
|
|
72
|
+
dense row view is populated lazily only when an inspection API requests a row.
|
|
73
|
+
These settings affect artifact size and loading cost, not matching semantics.
|
|
74
|
+
|
|
75
|
+
Standalone output embeds a runtime core, not the compiler or generator. The
|
|
76
|
+
regexp reference matcher and vendored Unicode tables are added only for a
|
|
77
|
+
`firstmatch` backend or trailing-context rules that can use that matcher.
|
|
@@ -20,6 +20,10 @@ else may change between releases without a deprecation cycle.
|
|
|
20
20
|
Method arguments, return shapes, and diagnostics for this surface are described
|
|
21
21
|
in the reference pages and are covered by the test suite.
|
|
22
22
|
|
|
23
|
+
`require "flexr"` keeps the build tools lazy while preserving all public
|
|
24
|
+
constants through autoload. Applications that want an explicit load boundary
|
|
25
|
+
can require `flexr/runtime`, `flexr/generator`, or `flexr/cli` directly.
|
|
26
|
+
|
|
23
27
|
## Experimental
|
|
24
28
|
|
|
25
29
|
- `backend :firstmatch` and `--backend firstmatch`.
|
|
@@ -27,8 +31,9 @@ in the reference pages and are covered by the test suite.
|
|
|
27
31
|
- Any future backend or option explicitly marked experimental in its reference.
|
|
28
32
|
|
|
29
33
|
Experimental behavior may change semantics or output between minor releases.
|
|
30
|
-
`firstmatch` requires the opt-in and
|
|
31
|
-
|
|
34
|
+
`firstmatch` requires the opt-in. Runtime and generated `firstmatch` modes use
|
|
35
|
+
the same rule-order semantics; overlap with longest-match behavior is reported
|
|
36
|
+
as `FLEXR-W010` rather than making generation fail.
|
|
32
37
|
|
|
33
38
|
## Internal; compatibility is not guaranteed
|
|
34
39
|
|
data/docs/reference/regexp.md
CHANGED
|
@@ -7,14 +7,14 @@ byte-oriented automaton. All matches start at the current lexer position.
|
|
|
7
7
|
|---|---|---|
|
|
8
8
|
| Literals, concatenation, alternation | Supported | |
|
|
9
9
|
| Groups and `(?:...)` | Supported | Captures are treated as non-capturing and warn |
|
|
10
|
-
| Character classes and ranges | Supported | POSIX classes
|
|
10
|
+
| Character classes and ranges | Supported | Includes trailing `-`, POSIX classes, and `&&` intersection |
|
|
11
11
|
| Unicode `\p{...}` / `\P{...}` | Supported | Uses the vendored UCD snapshot |
|
|
12
12
|
| `*`, `+`, `?` | Supported | Greedy forms only |
|
|
13
|
-
| `{n}`, `{n,m}` | Supported |
|
|
14
|
-
| `{n,}` | Unsupported | Split the rule or use states |
|
|
13
|
+
| `{n}`, `{n,m}`, `{n,}` | Supported | Explicit bounds may not exceed 1000 |
|
|
15
14
|
| `^` / `$` | Restricted | Only at outermost pattern boundaries; otherwise `FLEXR-E009` |
|
|
16
15
|
| `/i`, `/m`, `/x`, `/n` | Supported | Inline forms are supported where Ruby permits them |
|
|
17
16
|
| `\d`, `\w`, `\s` | Supported | Unicode-aware with `option :unicode` in UTF-8 |
|
|
17
|
+
| `\h`, `\H` | Supported | Hexadecimal digits and their complement |
|
|
18
18
|
| Lookahead / lookbehind | Unsupported | Use `followed_by:` or a state; `FLEXR-E014` |
|
|
19
19
|
| Backreferences | Unsupported | Split the language into states or actions |
|
|
20
20
|
| `\b`, `\B`, `\A`, `\z`, `\G`, `\K` | Unsupported | Use rule boundaries or state/action logic |
|
|
@@ -23,14 +23,16 @@ byte-oriented automaton. All matches start at the current lexer position.
|
|
|
23
23
|
|
|
24
24
|
Malformed syntax produces `FLEXR-E001`. Unsupported constructs produce
|
|
25
25
|
`FLEXR-E014`; the diagnostic help normally names the supported replacement.
|
|
26
|
+
Unknown alphabetic escapes are rejected rather than silently treated as
|
|
27
|
+
literals; escaped regexp punctuation remains supported.
|
|
26
28
|
Empty matches are rejected with `FLEXR-E005` unless `option :allow_empty_match`
|
|
27
29
|
is explicitly enabled, in which case flexr advances to guarantee progress.
|
|
28
30
|
|
|
29
31
|
## Bounded repetition
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
Repetition remains a symbolic AST node until NFA construction, avoiding large
|
|
34
|
+
intermediate trees. In `{n,m}`, `m` must be at least `n`; explicit bounds may
|
|
35
|
+
not exceed 1000. `{n,}` uses an unbounded loop after the required copies.
|
|
34
36
|
|
|
35
37
|
## Captures
|
|
36
38
|
|