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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +74 -28
  3. data/benchmark/golden/calculator_lexer.sha256 +1 -1
  4. data/benchmark/golden/json_lexer.sha256 +1 -1
  5. data/benchmark/golden/regexp_tokenizer.sha256 +1 -1
  6. data/benchmark/golden/ruby_subset_lexer.sha256 +1 -1
  7. data/benchmark/golden/toy_lang_lexer.sha256 +1 -1
  8. data/benchmark/golden/with_lrama_lexer.sha256 +1 -1
  9. data/benchmark/golden/with_racc_lexer.sha256 +1 -1
  10. data/docs/README.md +2 -1
  11. data/docs/explanation/backends.md +13 -10
  12. data/docs/explanation/matching-semantics.md +4 -2
  13. data/docs/explanation/security-model.md +10 -3
  14. data/docs/explanation/unicode-and-encoding.md +3 -1
  15. data/docs/how-to/deploy-a-standalone-lexer.md +5 -3
  16. data/docs/how-to/generate-a-lexer.md +3 -3
  17. data/docs/how-to/handle-errors.md +4 -1
  18. data/docs/how-to/run-a-lexer-at-runtime.md +5 -0
  19. data/docs/how-to/track-token-locations.md +2 -1
  20. data/docs/how-to/tune-performance.md +3 -3
  21. data/docs/perf-log.md +16 -0
  22. data/docs/reference/actions.md +5 -2
  23. data/docs/reference/diagnostics.md +8 -0
  24. data/docs/reference/dsl.md +19 -5
  25. data/docs/reference/errors.md +6 -3
  26. data/docs/reference/generated-artifacts.md +29 -2
  27. data/docs/reference/public-api.md +7 -2
  28. data/docs/reference/regexp.md +8 -6
  29. data/docs/reference/runtime.md +20 -3
  30. data/docs/reference/tokens-and-locations.md +6 -4
  31. data/docs/releases/v1.1.0.md +44 -0
  32. data/lib/flexr/action_resolver.rb +15 -0
  33. data/lib/flexr/artifact_writer.rb +61 -0
  34. data/lib/flexr/automaton/accel.rb +9 -4
  35. data/lib/flexr/automaton/analysis.rb +47 -6
  36. data/lib/flexr/automaton/backend_cost_model.rb +40 -0
  37. data/lib/flexr/automaton/compiler.rb +50 -71
  38. data/lib/flexr/automaton/dfa.rb +106 -19
  39. data/lib/flexr/automaton/minimizer.rb +79 -31
  40. data/lib/flexr/automaton/nfa.rb +45 -13
  41. data/lib/flexr/automaton/types.rb +14 -0
  42. data/lib/flexr/cli.rb +11 -5
  43. data/lib/flexr/codegen/direct.rb +5 -41
  44. data/lib/flexr/codegen/table.rb +175 -69
  45. data/lib/flexr/codegen.rb +8 -0
  46. data/lib/flexr/configuration.rb +37 -0
  47. data/lib/flexr/dsl.rb +105 -39
  48. data/lib/flexr/errors.rb +1 -0
  49. data/lib/flexr/generated.rb +61 -35
  50. data/lib/flexr/generator.rb +47 -82
  51. data/lib/flexr/importer.rb +4 -40
  52. data/lib/flexr/options.rb +5 -3
  53. data/lib/flexr/rake_task.rb +6 -1
  54. data/lib/flexr/regexp/ast.rb +5 -2
  55. data/lib/flexr/regexp/normalizer.rb +62 -16
  56. data/lib/flexr/regexp/parser.rb +121 -46
  57. data/lib/flexr/regexp/tokenizer.rb +192 -67
  58. data/lib/flexr/runtime/buffer.rb +115 -9
  59. data/lib/flexr/runtime/core.rb +214 -47
  60. data/lib/flexr/runtime/errors.rb +64 -4
  61. data/lib/flexr/runtime/interpreter.rb +178 -71
  62. data/lib/flexr/runtime.rb +70 -0
  63. data/lib/flexr/source/passthrough.rb +62 -9
  64. data/lib/flexr/source/prism_reader.rb +175 -46
  65. data/lib/flexr/source/static_eval.rb +40 -4
  66. data/lib/flexr/source.rb +6 -0
  67. data/lib/flexr/unicode/data/properties.rb +1 -1
  68. data/lib/flexr/unicode/data.rb +11 -0
  69. data/lib/flexr/unicode/property.rb +3 -5
  70. data/lib/flexr/unicode/reference_regexp.rb +4 -0
  71. data/lib/flexr/unicode/version.rb +7 -0
  72. data/lib/flexr/version.rb +1 -1
  73. data/lib/flexr.rb +9 -76
  74. data/tools/coverage.rb +6 -1
  75. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54c55f186a1f4aa048a7a46ca86cae52eb5f9566888fc9cdc09f527d4b6b341f
4
- data.tar.gz: 7a5e9329e688889cfe665f3a4499deea7ca2e9de684f4fd5ad10847bd631fb4e
3
+ metadata.gz: edb2c916db82e07c56290ab6365d50bf9a0a148278ca811a100063031315bbf5
4
+ data.tar.gz: 13c08660e91681ab2143766d58072c4c4e3d081b5899a18568a772bfd80810b9
5
5
  SHA512:
6
- metadata.gz: 7399ef9f672ff36623a8d54a6eed882ee43fed2c7e9d59b417038bd0d8a9a32b892a8556774531aceb41b0e1c3729a169a003b190b8f2a330c4b825960da32b2
7
- data.tar.gz: 4ecdca40a21a0ff5af1146f20912297c96542bd2ec66412f3726461472c45e10ebc7e6d81a17ef942f3d0bfa036c8701f123fc15be72408dd62c5673c5636154
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
- options[:accel] = :none
142
- reference = inputs.map { |input| lexer.new(input, error_mode: :panic).tokens }
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
- disassembly = RubyVM::InstructionSequence.compile(FlexrVerification.generated_source(spec)).disasm
300
- abort "direct dispatch did not compile to opt_case_dispatch" unless disassembly.include?("opt_case_dispatch")
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: opt_case_dispatch present"
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
- expected = if Flexr.reference_pattern?(pattern)
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
- else
386
- Regexp.new("\\A(?:#{pattern.source})\\z", pattern.options).match?(input)
426
+ unless vendored_expected == host_expected
427
+ version_mismatches += 1
428
+ next
429
+ end
387
430
  end
388
- key = [pattern.source, pattern.options]
431
+ independent_cases += 1
389
432
  actual = (compiled[key] ||= Flexr.compile_pattern(pattern)).accept?(input)
390
- next if expected == actual
433
+ next if host_expected == actual
391
434
 
392
- abort "differential mismatch: #{pattern.inspect} #{input.inspect} expected=#{expected} actual=#{actual}"
435
+ abort "differential mismatch: #{pattern.inspect} #{input.inspect} " \
436
+ "expected=#{host_expected} actual=#{actual}"
393
437
  end
394
- puts "differential: #{cases} cases passed"
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
- runtime_tokens = runtime_lexer.new(input, error_mode: :panic).tokens
429
- generated_tokens = generated_lexer.new(input, error_mode: :panic).tokens
430
- next if runtime_tokens == generated_tokens
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} runtime=#{runtime_tokens.inspect} generated=#{generated_tokens.inspect}"
433
- rescue Flexr::LexError, ArgumentError, EncodingError
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
- 6359976d9cdc72772eff8c72cf421d965e913febad3dbc0ca0f2057a0faea3b4
1
+ d01c0b69f4f2fedd572e90000f4fc670524b01089a6942b94b1fc1c1cd27f95e
@@ -1 +1 @@
1
- 913cf85ba7e1a3128eea0a4b06077486fead2d56357b669685ae6e26bdd55715
1
+ f6d8418031b657a328d3ad62fcd780a43bbf4731e9d0e390120dd219894a515e
@@ -1 +1 @@
1
- 7f20a1024eea1d674aee73a2baf7a32c52fa178f2d158d24e49f9c9b77a63960
1
+ 1f53aa34bf24a92fd03096648b3290bbfda76222078c0c58966e259fd3a13aca
@@ -1 +1 @@
1
- 0d99b92c658d56cbef7d9c33ac3e5770d83e379b94cf412f8b028f59d44910b8
1
+ 5bb597bfacaf3217d2852185a9c4e90833a3cb6da02e114817d00d00dd3a3aec
@@ -1 +1 @@
1
- 16943c2d8a86999148dd6bcd2ed644671032d1e6b4acbe891d180ff0810cd97a
1
+ 0285b7b1ceff88a7eddcd1f7d3ac87b0a4bce1e4162f5e64205549aacd576dce
@@ -1 +1 @@
1
- 7627b0fafbfdbdea850d02e67c3e9c24eab8512cdc74571a986323d397848071
1
+ 6abde13a982ee9f529cc2182dad6cefd1f8608af295981fdfe8671e6b46c7f99
@@ -1 +1 @@
1
- 037fa90c634117522c1e882d08c437c7b79653f59ca94e6cf6c115cd7897610f
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 a flattened dispatch structure for
12
- large machines and can reduce lookup overhead at the cost of artifact size.
13
- It preserves the table backend's longest-match behavior.
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 chooses direct dispatch when the
18
- largest state-by-byte-class table exceeds the configured threshold, otherwise
19
- it uses table dispatch. The effective backend is recorded in generated headers.
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. Generation
25
- compares a deterministic input corpus with the table matcher and aborts if the
26
- results differ.
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
- they do not change token streams.
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. The runtime and generated scanner share the compiled model;
15
- acceleration may skip self-loop bytes but cannot change the winner.
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` and `max_state_stack` limits provide resource
17
- guards for input processing. They are operational controls, not a security
18
- sandbox and do not protect against arbitrary Ruby actions.
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. Host Ruby Unicode tables are not used as a cross-version oracle.
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 sources
10
- needed to execute the compiled tables. It can be loaded by a Ruby process that
11
- does not have the flexr gem installed:
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 supported
17
- values, constants, ranges, interpolation whose values are static, `.freeze`,
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` when exceeded.
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` chooses `:direct` for large transition
13
- tables and `:table` otherwise; `:table` and `:direct` preserve the same
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.
@@ -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. `more` is finalized after
47
- the action; call `emit` only after the complete text has been assembled.
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
 
@@ -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`. Unknown
69
- options are retained in generated configuration but may have no effect.
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. They are
80
- not required for normal use and are not compatibility-stable; use the lexer
81
- constructor and token methods instead. See [public API](public-api.md).
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.
@@ -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`. `StateStackOverflowError` is raised when a
15
- `push` would exceed `max_state_stack`.
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. User actions remain Ruby source. A generated
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. These affect artifact size and loading cost, not matching semantics.
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 generation performs a deterministic
31
- differential check against the table matcher.
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
 
@@ -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 are supported; see encoding notes |
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 | The upper bound is 1000 |
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
- `{n,m}` expands into a finite automaton. `m` must be at least `n` and no greater
32
- than 1000. Open repetition is rejected because it is easy to express with `+`
33
- or a separate rule and otherwise gives a misleading compatibility promise.
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