flexr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +33 -0
  3. data/CONTRIBUTING.md +39 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +116 -0
  6. data/Rakefile +468 -0
  7. data/benchmark/baselines/json.json +34 -0
  8. data/benchmark/baselines/json_handwritten.rb +43 -0
  9. data/benchmark/baselines/json_rexical.rex +25 -0
  10. data/benchmark/corpora/README.md +11 -0
  11. data/benchmark/corpora/generate_json.rb +26 -0
  12. data/benchmark/golden/calculator_lexer.sha256 +1 -0
  13. data/benchmark/golden/json_lexer.sha256 +1 -0
  14. data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
  15. data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
  16. data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
  17. data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
  18. data/benchmark/golden/with_racc_lexer.sha256 +1 -0
  19. data/benchmark/run.rb +254 -0
  20. data/docs/README.md +64 -0
  21. data/docs/RELEASING.md +30 -0
  22. data/docs/adr/0001-byte-level-dfa.md +5 -0
  23. data/docs/adr/0003-leftmost-longest.md +4 -0
  24. data/docs/adr/0006-accel-not-scanner.md +4 -0
  25. data/docs/adr/0008-what-pure-ruby-means.md +5 -0
  26. data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
  27. data/docs/adr/0017-static-analysis-by-default.md +5 -0
  28. data/docs/adr/0018-prism-for-generator-only.md +4 -0
  29. data/docs/adr/0019-measured-performance-floor.md +26 -0
  30. data/docs/adr/0020-vendored-unicode-contract.md +21 -0
  31. data/docs/explanation/backends.md +33 -0
  32. data/docs/explanation/matching-semantics.md +20 -0
  33. data/docs/explanation/runtime-vs-generated.md +22 -0
  34. data/docs/explanation/security-model.md +18 -0
  35. data/docs/explanation/unicode-and-encoding.md +20 -0
  36. data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
  37. data/docs/how-to/generate-a-lexer.md +39 -0
  38. data/docs/how-to/handle-errors.md +32 -0
  39. data/docs/how-to/integrate-with-lrama.md +21 -0
  40. data/docs/how-to/integrate-with-racc.md +25 -0
  41. data/docs/how-to/migrate-from-flex.md +21 -0
  42. data/docs/how-to/migrate-from-rexical.md +23 -0
  43. data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
  44. data/docs/how-to/track-token-locations.md +27 -0
  45. data/docs/how-to/tune-performance.md +23 -0
  46. data/docs/how-to/use-states.md +36 -0
  47. data/docs/how-to/use-trailing-context.md +22 -0
  48. data/docs/internals/README.md +14 -0
  49. data/docs/perf-log.md +56 -0
  50. data/docs/reference/README.md +23 -0
  51. data/docs/reference/actions.md +47 -0
  52. data/docs/reference/cli.md +80 -0
  53. data/docs/reference/compatibility.md +38 -0
  54. data/docs/reference/diagnostics.md +41 -0
  55. data/docs/reference/dsl.md +81 -0
  56. data/docs/reference/errors.md +27 -0
  57. data/docs/reference/generated-artifacts.md +50 -0
  58. data/docs/reference/public-api.md +42 -0
  59. data/docs/reference/regexp.md +39 -0
  60. data/docs/reference/runtime.md +49 -0
  61. data/docs/reference/tokens-and-locations.md +33 -0
  62. data/docs/tutorial/build-a-calculator-lexer.md +96 -0
  63. data/examples/calculator/README.md +27 -0
  64. data/examples/calculator/lexer.flexr.rb +17 -0
  65. data/examples/json/README.md +30 -0
  66. data/examples/json/lexer.flexr.rb +24 -0
  67. data/examples/ruby_subset/README.md +17 -0
  68. data/examples/ruby_subset/lexer.flexr.rb +22 -0
  69. data/examples/toy_lang/README.md +17 -0
  70. data/examples/toy_lang/lexer.flexr.rb +18 -0
  71. data/examples/with_lrama/README.md +17 -0
  72. data/examples/with_lrama/lexer.flexr.rb +13 -0
  73. data/examples/with_racc/README.md +17 -0
  74. data/examples/with_racc/lexer.flexr.rb +13 -0
  75. data/exe/flexr +7 -0
  76. data/lib/flexr/automaton/accel.rb +39 -0
  77. data/lib/flexr/automaton/analysis.rb +38 -0
  78. data/lib/flexr/automaton/byte_class_set.rb +29 -0
  79. data/lib/flexr/automaton/compiler.rb +413 -0
  80. data/lib/flexr/automaton/dfa.rb +103 -0
  81. data/lib/flexr/automaton/minimizer.rb +70 -0
  82. data/lib/flexr/automaton/nfa.rb +92 -0
  83. data/lib/flexr/cli.rb +342 -0
  84. data/lib/flexr/codegen/base.rb +17 -0
  85. data/lib/flexr/codegen/direct.rb +52 -0
  86. data/lib/flexr/codegen/firstmatch.rb +17 -0
  87. data/lib/flexr/codegen/table.rb +158 -0
  88. data/lib/flexr/codegen/table_packer.rb +61 -0
  89. data/lib/flexr/diagnostics.rb +94 -0
  90. data/lib/flexr/dsl.rb +182 -0
  91. data/lib/flexr/errors.rb +28 -0
  92. data/lib/flexr/generated.rb +125 -0
  93. data/lib/flexr/generator.rb +400 -0
  94. data/lib/flexr/importer.rb +560 -0
  95. data/lib/flexr/ir.rb +36 -0
  96. data/lib/flexr/lexer.rb +10 -0
  97. data/lib/flexr/options.rb +47 -0
  98. data/lib/flexr/rake_task.rb +27 -0
  99. data/lib/flexr/regexp/ast.rb +45 -0
  100. data/lib/flexr/regexp/char_class.rb +7 -0
  101. data/lib/flexr/regexp/normalizer.rb +117 -0
  102. data/lib/flexr/regexp/parser.rb +517 -0
  103. data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
  104. data/lib/flexr/regexp/tokenizer.rb +168 -0
  105. data/lib/flexr/regexp/unsupported.rb +7 -0
  106. data/lib/flexr/runtime/buffer.rb +112 -0
  107. data/lib/flexr/runtime/core.rb +388 -0
  108. data/lib/flexr/runtime/errors.rb +22 -0
  109. data/lib/flexr/runtime/interpreter.rb +505 -0
  110. data/lib/flexr/runtime/location.rb +26 -0
  111. data/lib/flexr/runtime/token.rb +7 -0
  112. data/lib/flexr/source/passthrough.rb +31 -0
  113. data/lib/flexr/source/prism_reader.rb +283 -0
  114. data/lib/flexr/source/static_eval.rb +145 -0
  115. data/lib/flexr/unicode/case_fold.rb +45 -0
  116. data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
  117. data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
  118. data/lib/flexr/unicode/data/case_folding.rb +9 -0
  119. data/lib/flexr/unicode/data/properties.rb +10 -0
  120. data/lib/flexr/unicode/property.rb +107 -0
  121. data/lib/flexr/unicode/reference_regexp.rb +102 -0
  122. data/lib/flexr/unicode/utf8_splitter.rb +109 -0
  123. data/lib/flexr/version.rb +5 -0
  124. data/lib/flexr.rb +81 -0
  125. data/site/README.md +22 -0
  126. data/site/astro.config.mjs +57 -0
  127. data/site/package.json +19 -0
  128. data/site/pnpm-lock.yaml +5029 -0
  129. data/site/pnpm-workspace.yaml +6 -0
  130. data/site/public/playground.js +189 -0
  131. data/site/scripts/verify-site.mjs +42 -0
  132. data/site/src/content/docs/benchmarks.md +8 -0
  133. data/site/src/content/docs/concepts/matching-semantics.md +15 -0
  134. data/site/src/content/docs/concepts/regexp-model.md +18 -0
  135. data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
  136. data/site/src/content/docs/concepts/security-model.md +15 -0
  137. data/site/src/content/docs/examples.md +17 -0
  138. data/site/src/content/docs/learn/generation.md +29 -0
  139. data/site/src/content/docs/learn/getting-started.md +56 -0
  140. data/site/src/content/docs/learn/parser-integration.md +27 -0
  141. data/site/src/content/docs/learn/runtime-mode.md +32 -0
  142. data/site/src/content/docs/reference/action-context.md +20 -0
  143. data/site/src/content/docs/reference/cli.md +22 -0
  144. data/site/src/content/docs/reference/diagnostics.md +16 -0
  145. data/site/src/content/docs/reference/dsl.md +19 -0
  146. data/site/src/content/docs/reference/public-api.md +18 -0
  147. data/site/src/content/docs/reference/regexp.md +16 -0
  148. data/site/src/content/docs/reference/runtime.md +16 -0
  149. data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
  150. data/site/src/content.config.ts +12 -0
  151. data/site/src/env.d.ts +1 -0
  152. data/site/src/layouts/SiteLayout.astro +39 -0
  153. data/site/src/pages/index.astro +174 -0
  154. data/site/src/pages/playground.astro +64 -0
  155. data/site/src/styles/custom.css +711 -0
  156. data/site/tsconfig.json +5 -0
  157. data/tools/coverage.rb +32 -0
  158. data/tools/docs_verify.rb +116 -0
  159. data/tools/gen_unicode_tables.rb +202 -0
  160. data/tools/regexp_tokenizer_reference.rb +60 -0
  161. metadata +205 -0
@@ -0,0 +1,22 @@
1
+ # Runtime versus generated
2
+
3
+ Both modes begin with the same DSL concepts and compile to the same IR-shaped
4
+ rule and state model, but they serve different operational needs.
5
+
6
+ | Concern | Runtime | Generated |
7
+ |---|---|---|
8
+ | Specification | Evaluated by Ruby | Read statically with Prism by default |
9
+ | DFA construction | First class use | Build time |
10
+ | Dynamic pattern methods | Available | Requires trusted `--eval` |
11
+ | Startup cost | Includes compilation | Loads compiled payload |
12
+ | Action code | Ruby block | Ruby source preserved in artifact |
13
+ | Prism at runtime | Not needed | Not needed after generation |
14
+
15
+ Keeping actions as Ruby is deliberate: semantic work belongs in the user's
16
+ action, while flexr owns rule selection and token boundaries. It also means
17
+ that generated files are executable code and must be treated as trusted build
18
+ artifacts.
19
+
20
+ The generated header and payload digest make artifact provenance visible. The
21
+ repository's mode-equivalence checks execute each example in both modes and
22
+ compare token streams.
@@ -0,0 +1,18 @@
1
+ # Security model
2
+
3
+ flexr is a code generator, not a sandbox. A specification is ordinary Ruby and
4
+ its actions can perform any operation available to the Ruby process. Generated
5
+ files retain those actions as Ruby code.
6
+
7
+ Static generation parses the specification and evaluates only a constrained set
8
+ of literal and constant forms. This reduces accidental build-time execution but
9
+ does not make the generated artifact safe to load if its source is untrusted.
10
+
11
+ `--eval` is an explicit execution boundary: it evaluates the full specification
12
+ to discover dynamic patterns. Use it only with trusted files and a trusted
13
+ environment. The same rule applies to generated output: review or sign the
14
+ artifact before loading it in a process with sensitive access.
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.
@@ -0,0 +1,20 @@
1
+ # Unicode and encoding
2
+
3
+ The automaton consumes bytes, but UTF-8 patterns are expanded into byte ranges
4
+ that respect codepoint boundaries. Binary specifications operate directly on
5
+ all 256 byte values. This separation keeps the hot path predictable while
6
+ allowing Unicode-aware patterns.
7
+
8
+ The vendored Unicode Character Database snapshot, currently 15.1.0, is the
9
+ compatibility oracle for `\p{...}`, POSIX properties where a Unicode property
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.
12
+
13
+ Use `encoding Encoding::BINARY` for protocol or byte-oriented formats. Use the
14
+ default UTF-8 encoding for text. A pattern with invalid UTF-8 or an input that
15
+ breaks a required boundary is rejected or reported as an unmatched byte rather
16
+ than normalized.
17
+
18
+ `rake unicode:verify` checks range ordering, scalar expansions, and deterministic
19
+ random range cases. Updating Unicode data requires regenerating generated golden
20
+ files and is documented as a minor-version compatibility change.
@@ -0,0 +1,23 @@
1
+ # Deploy a standalone lexer
2
+
3
+ Use `--standalone` to embed the flexr runtime into the generated file:
4
+
5
+ ```sh
6
+ flexr lexer.flexr.rb --standalone -o lexer_standalone.rb
7
+ ```
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:
12
+
13
+ ```sh
14
+ ruby -e 'load ARGV.fetch(0); p Lexer.new("12").tokens' lexer_standalone.rb
15
+ ```
16
+
17
+ Generation still needs the flexr gem and Prism on Ruby 3.3 or newer. Runtime
18
+ execution needs the generated file and Ruby's standard library only.
19
+
20
+ Standalone output is larger and duplicates the runtime for every artifact.
21
+ Prefer ordinary generated output when several lexers share one application and
22
+ can depend on the flexr gem. The standalone behavior is covered by the CLI
23
+ end-to-end test.
@@ -0,0 +1,39 @@
1
+ # Generate a lexer
2
+
3
+ Generate a Ruby artifact from a `.flexr.rb` specification:
4
+
5
+ ```sh
6
+ flexr path/to/lexer.flexr.rb -o path/to/lexer.rb
7
+ ```
8
+
9
+ Generation is static by default. Prism reads the source, resolves supported
10
+ constant and expression forms, compiles the same DFA model used by runtime
11
+ mode, and replaces DSL calls with a compiled installation payload. Ordinary
12
+ Ruby constants, comments, requires, and non-DSL code remain in the file.
13
+
14
+ ## Static expressions
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
19
+ `FLEXR-E017`. See the [static-expression reference](../reference/generated-artifacts.md)
20
+ for the exact boundary.
21
+
22
+ ## Dynamic specifications
23
+
24
+ ```sh
25
+ flexr path/to/lexer.flexr.rb --eval -o path/to/lexer.rb
26
+ ```
27
+
28
+ `--eval` executes the specification and is therefore limited to trusted build
29
+ inputs. It is useful when a pattern is assembled by a method or external
30
+ configuration, but it removes the static-analysis guarantee and can make the
31
+ artifact depend on the build environment.
32
+
33
+ ## Reproducible builds
34
+
35
+ Commit generated files when downstream consumers should not need Prism, or
36
+ generate them in the build when the source is the only artifact you distribute.
37
+ The generated header records the source path, payload digest, Unicode snapshot,
38
+ backend, compilation mode, and standalone setting. Use the digest and
39
+ `generated:verify` in CI to detect stale committed output.
@@ -0,0 +1,32 @@
1
+ # Handle errors
2
+
3
+ There are two error layers: specification diagnostics and input-time lexical
4
+ errors.
5
+
6
+ ## Validate the specification
7
+
8
+ ```sh
9
+ flexr check lexer.flexr.rb --format json --warn all
10
+ ```
11
+
12
+ The command returns status 0 when the specification compiles, even when it has
13
+ warnings. Add `--warn-as-error` in CI to make warnings fail the check. Syntax,
14
+ unsupported regexp, and compile-limit failures return status 1; malformed CLI
15
+ usage returns status 2.
16
+
17
+ ## Choose input error behavior
18
+
19
+ ```ruby
20
+ Lexer.new(input, error_mode: :raise)
21
+ Lexer.new(input, error_mode: :token)
22
+ Lexer.new(input, error_mode: :panic)
23
+ ```
24
+
25
+ `:raise` raises `Flexr::LexError`, `:token` emits `[:error, text]`, and `:panic`
26
+ skips the unmatched byte and continues. You can override the decision for one
27
+ instance with `lexer.on_error = ->(error) { :skip }`, `:raise`, `:token`, or
28
+ `:halt`. The callback receives the `LexError` and may inspect its filename,
29
+ byte position, line, and text.
30
+
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.
@@ -0,0 +1,21 @@
1
+ # Integrate with Lrama
2
+
3
+ Lrama-generated parsers use the same two-element token protocol as Racc.
4
+ Implement the lexer with `racc_next_token`, declare the grammar tokens with
5
+ `emits`, and generate the lexer as part of the parser build.
6
+
7
+ ```ruby
8
+ class Lexer < Flexr::Lexer
9
+ emits :INTEGER, :MINUS
10
+ rule(/[ \t\r\n]+/, skip: true)
11
+ rule(/[0-9]+/, emit: :INTEGER)
12
+ rule(/-/, emit: :MINUS)
13
+ end
14
+ ```
15
+
16
+ `racc_next_token` returns `[type, value]` for a token and `[false, "$end"]` at
17
+ EOF. `emits` is a documentation and diagnostic contract; it does not alter
18
+ the value returned by the lexer.
19
+
20
+ See the runnable [Lrama example](../../examples/with_lrama/README.md) and the
21
+ [token contract](../reference/tokens-and-locations.md).
@@ -0,0 +1,25 @@
1
+ # Integrate with Racc
2
+
3
+ flexr exposes the conventional `racc_next_token` method, so a parser can use a
4
+ lexer without an adapter:
5
+
6
+ ```ruby
7
+ class Lexer < Flexr::Lexer
8
+ emits :INTEGER, :PLUS
9
+ rule(/[ \t\r\n]+/, skip: true)
10
+ rule(/[0-9]+/, emit: :INTEGER)
11
+ rule(/\+/, emit: :PLUS)
12
+ end
13
+
14
+ lexer = Lexer.new("12 + 3")
15
+ lexer.racc_next_token # => [:INTEGER, "12"]
16
+ ```
17
+
18
+ At EOF the method returns `[false, "$end"]`. Keep the names in `emits` aligned
19
+ with the grammar's `%token` declarations. `flexr tokens SPEC.flexr.rb` prints
20
+ the declared names for a build-time comparison.
21
+
22
+ The complete executable lexer is in
23
+ [examples/with_racc](../../examples/with_racc/README.md). Generate the lexer
24
+ before running a parser build if the parser should consume a committed Ruby
25
+ artifact.
@@ -0,0 +1,21 @@
1
+ # Migrate from flex
2
+
3
+ Use the importer to translate a flex `.l` file into a Ruby specification:
4
+
5
+ ```sh
6
+ flexr import scanner.l -o lexer.flexr.rb
7
+ ```
8
+
9
+ Common rules and actions are translated, including `return`, `BEGIN`,
10
+ `yyless`, `yymore`, and `ECHO`. The importer preserves header and footer text as
11
+ Ruby comments where possible. Run the result through `flexr check` before
12
+ loading it.
13
+
14
+ Untranslated C actions are emitted as `FLEXR-TODO`; the command returns status 1
15
+ and does not create or overwrite the requested output file. Complete the Ruby
16
+ action manually and repeat the check. Also review rule overlaps: flexr uses
17
+ longest-match semantics, which can differ from a flex specification that relies
18
+ on rule order.
19
+
20
+ The [CLI reference](../reference/cli.md) documents importer exit statuses and
21
+ the [errors reference](../reference/errors.md) documents `FLEXR-TODO` behavior.
@@ -0,0 +1,23 @@
1
+ # Migrate from Rexical
2
+
3
+ Import a Rexical specification with:
4
+
5
+ ```sh
6
+ flexr import scanner.rex -o lexer.flexr.rb
7
+ ```
8
+
9
+ Rexical uses first-match semantics while flexr uses longest-match semantics.
10
+ The importer reports that semantic boundary and searches for concrete
11
+ counterexamples when it finds overlapping rules. Treat every reported witness
12
+ as a migration review item.
13
+
14
+ After rewriting actions and reviewing overlaps:
15
+
16
+ ```sh
17
+ flexr check lexer.flexr.rb --warn all
18
+ flexr lexer.flexr.rb -o lexer.rb
19
+ ```
20
+
21
+ Do not enable `backend :firstmatch` merely to hide a migration difference. It
22
+ is experimental, requires `option :experimental`, and generation performs a
23
+ differential check against the table matcher.
@@ -0,0 +1,29 @@
1
+ # Run a lexer at runtime
2
+
3
+ Runtime mode loads the Ruby specification and builds its automaton when the
4
+ lexer class is first used. It is the simplest mode for development and tests.
5
+
6
+ ```ruby
7
+ require "flexr"
8
+
9
+ class Lexer < Flexr::Lexer
10
+ rule(/[ \t\n]+/, skip: true)
11
+ rule(/[0-9]+/) { emit :INTEGER, text.to_i }
12
+ end
13
+
14
+ lexer = Lexer.new(StringIO.new("12 34"))
15
+ lexer.each_token { |token| p token }
16
+ ```
17
+
18
+ The input may be a `String` or an object responding to `read`. IO input is read
19
+ in chunks; `chunk_size:` controls the buffer size. `next_token` returns one
20
+ token or `nil` at EOF, `tokens` drains the lexer into an array, and
21
+ `each_token` returns an enumerator when no block is given.
22
+
23
+ Use runtime mode when the specification deliberately depends on ordinary Ruby
24
+ execution. Static generation is a better fit when startup cost, deployment
25
+ reproducibility, or a generator-free runtime matters.
26
+
27
+ See the [runtime reference](../reference/runtime.md) for initialization options
28
+ and the [action reference](../reference/actions.md) for methods available in a
29
+ rule block.
@@ -0,0 +1,27 @@
1
+ # Track token locations
2
+
3
+ Use `token_kind :struct` when consumers need a value object with a location:
4
+
5
+ ```ruby
6
+ class Lexer < Flexr::Lexer
7
+ token_kind :struct
8
+ option :eager_columns
9
+
10
+ rule(/[0-9]+/) { emit :INTEGER, text.to_i }
11
+ end
12
+
13
+ token = Lexer.new("12\n34").next_token
14
+ token.type
15
+ token.value
16
+ token.location.line_begin
17
+ token.location.column_begin
18
+ ```
19
+
20
+ Locations use byte offsets for `byte_begin` and `byte_end`, with an exclusive
21
+ end offset. Lines and columns are one-based. UTF-8 columns count characters;
22
+ binary columns count bytes. Column values are computed lazily unless
23
+ `option :eager_columns` is set. `filename:` on the lexer is copied into each
24
+ location and lexical error.
25
+
26
+ `more` joins successive matches into one `text` value, so `last_location` spans
27
+ the complete assembled token. See the [token and location contract](../reference/tokens-and-locations.md).
@@ -0,0 +1,23 @@
1
+ # Tune performance
2
+
3
+ Measure a representative input before changing the backend:
4
+
5
+ ```sh
6
+ ruby -Ilib benchmark/run.rb --spec examples/json/lexer.flexr.rb
7
+ bundle exec rake bench:regression
8
+ ```
9
+
10
+ Use `token_kind :yield` when the consumer streams tokens and does not need an
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.
15
+
16
+ Acceleration is an optimization over the DFA, not a second matcher. `accel
17
+ :auto` selects an available safe path, while `:strscan`, `:regexp`, and `:none`
18
+ make the choice explicit. Trailing context disables region acceleration for the
19
+ affected rule and emits `FLEXR-W012` unless acceleration is disabled.
20
+
21
+ The [performance log](../perf-log.md) records reproducible local observations,
22
+ and the [backend explanation](../explanation/backends.md) describes the memory
23
+ and compatibility trade-offs.
@@ -0,0 +1,36 @@
1
+ # Use states
2
+
3
+ States model lexical contexts such as strings, comments, or interpolation.
4
+ The initial state is inclusive; a named state is exclusive by default.
5
+
6
+ ```ruby
7
+ class Lexer < Flexr::Lexer
8
+ emits :TEXT, :STRING
9
+
10
+ rule(/[^"<]+/) { emit :TEXT }
11
+ rule(/"/) { push :string; more; skip }
12
+
13
+ state :string do
14
+ rule(/[^"\\]+/) { more; skip }
15
+ rule(/"/) { pop; emit :STRING }
16
+ end
17
+ end
18
+ ```
19
+
20
+ `push(name)` saves the current state, `pop` restores it, and `begin_state(name)`
21
+ switches without changing the stack. `push_state`, `pop_state`, and `state=`
22
+ are aliases. An undefined state raises `FLEXR-E003`; exceeding
23
+ `max_state_stack` raises `StateStackOverflowError`.
24
+
25
+ Use `inclusive: true` when a state should also use rules declared for the
26
+ initial state:
27
+
28
+ ```ruby
29
+ state :interpolation, inclusive: true do
30
+ rule(/\}/) { pop; skip }
31
+ end
32
+ ```
33
+
34
+ `all_states` applies a rule block to every state known at that point. Keep
35
+ state transitions in actions so the rule set remains easy to inspect with
36
+ `flexr explain`.
@@ -0,0 +1,22 @@
1
+ # Use trailing context
2
+
3
+ Use `followed_by:` when a rule should be selected only if another expression
4
+ starts immediately after it, but that expression must remain in the input for
5
+ the next token:
6
+
7
+ ```ruby
8
+ class Lexer < Flexr::Lexer
9
+ rule(/name/, followed_by: /\(/) { emit :CALL }
10
+ rule(/[a-z_][a-z0-9_]*/) { emit :IDENT }
11
+ rule(/\(/) { emit :LPAREN }
12
+ end
13
+ ```
14
+
15
+ The input `name(` produces `CALL` for `name`, then `LPAREN` for `(`. A string
16
+ can be used for literal trailing context. The body and trailing expression
17
+ should be fixed length when possible; variable-length context produces
18
+ `FLEXR-W003` and can prevent region acceleration (`FLEXR-W012`).
19
+
20
+ Trailing context is not the same as regexp lookahead. Lookahead syntax is
21
+ rejected with `FLEXR-E014`; `followed_by:` is the supported replacement.
22
+ Disable acceleration with `accel :none` only when the trade-off is understood.
@@ -0,0 +1,14 @@
1
+ # Internals
2
+
3
+ This is maintainer-facing material. The public contract is in
4
+ [reference/public-api.md](../reference/public-api.md); implementation choices
5
+ are explained in the [architecture decisions](../adr/) and
6
+ [explanation pages](../explanation/).
7
+
8
+ Both runtime and generated modes compile the same byte-oriented model: regexp
9
+ AST, Thompson NFA, byte-class DFA, deterministic minimization, and last-accept
10
+ tracking. Generated output embeds compiled tables and generated dispatch code;
11
+ runtime mode uses the interpreter and optional acceleration paths.
12
+
13
+ The internal names `Flexr::IR`, `Flexr::Automaton`, `Flexr::Codegen`, and
14
+ `__flexr_*` are not compatibility-stable.
data/docs/perf-log.md ADDED
@@ -0,0 +1,56 @@
1
+ # Performance log
2
+
3
+ The benchmark harness reports bytes/s, tokens/s, and allocations per token.
4
+ Numbers are intentionally recorded only after running the harness on the
5
+ target Ruby and hardware; no unmeasured figures are presented as guarantees.
6
+
7
+ Run:
8
+
9
+ ```sh
10
+ ruby -Ilib benchmark/run.rb
11
+ ```
12
+
13
+ The JSON result includes `mb_per_s`, `tokens_per_s`, and cumulative Ruby
14
+ `allocations_per_token` for runtime, generated, and handwritten modes. JSON
15
+ also records each Flexr mode as a ratio against the handwritten baseline.
16
+ The large deterministic corpus can be materialized when needed:
17
+
18
+ ```sh
19
+ ruby benchmark/corpora/generate_json.rb > benchmark/corpora/json_10mb.json
20
+ ruby -Ilib benchmark/run.rb --spec examples/json/lexer.flexr.rb \
21
+ --input-file benchmark/corpora/json_10mb.json
22
+ ```
23
+
24
+ ## 2026-08-10
25
+
26
+ On the local Ruby 4.0.0 environment, the default 140,000-byte JSON input and
27
+ three measured iterations produced the following sample. These are recorded
28
+ observations, not cross-machine guarantees.
29
+
30
+ | mode | MB/s | tokens/s | allocations/token | handwritten ratio |
31
+ |---|---:|---:|---:|---:|
32
+ | runtime | 1.671 | 596,789 | 5.201 | 0.194x |
33
+ | generated | 1.604 | 572,954 | 4.201 | 0.187x |
34
+ | handwritten | 8.597 | 3,070,499 | 3.4 | 1.000x |
35
+
36
+ This sample is above the v1.0 measured regression floor of 0.18x, but below the
37
+ 0.7x handwritten stretch target. The measured ratios are recorded explicitly
38
+ rather than presented as a claim that the stretch target has been reached.
39
+ Runtime is 1.042x generated in this sample, above the 0.2x runtime-mode risk
40
+ threshold. See ADR 0019 for the target revision.
41
+
42
+ ### Byte/token cost estimate
43
+
44
+ The same 10 MB JSON corpus was measured with compact records and with 100
45
+ padding spaces after each numeric value. The compact sample had 3,448,271
46
+ tokens; the padded sample had 775,191 tokens. Solving
47
+ `elapsed = c_byte * bytes + c_token * tokens` from the two measurements gives
48
+ the following local estimates (one iteration, `backend :direct`):
49
+
50
+ | mode | c_byte | c_token |
51
+ |---|---:|---:|
52
+ | runtime | 619 ns/byte | 1.090 us/token |
53
+ | generated | 611 ns/byte | 0.983 us/token |
54
+
55
+ The padded corpus can be reproduced with
56
+ `FLEXR_JSON_PADDING_BYTES=100 ruby benchmark/corpora/generate_json.rb`.
@@ -0,0 +1,23 @@
1
+ # Reference
2
+
3
+ This section describes behavior rather than teaching a sequence. Start with
4
+ the [DSL](dsl.md) when writing a specification, the [runtime](runtime.md) when
5
+ consuming tokens, or the [CLI](cli.md) when validating and generating files.
6
+
7
+ ## Reference pages
8
+
9
+ - [DSL](dsl.md) — class-level specification methods and defaults.
10
+ - [Runtime](runtime.md) — initialization, token iteration, input sources, and
11
+ limits.
12
+ - [Actions](actions.md) — methods available inside rule and EOF actions.
13
+ - [Tokens and locations](tokens-and-locations.md) — exact return shapes and
14
+ offsets.
15
+ - [Errors](errors.md) — exception and input-recovery behavior.
16
+ - [Regexp compatibility](regexp.md) — supported syntax and alternatives.
17
+ - [CLI](cli.md) — commands, options, output, and exit statuses.
18
+ - [Diagnostics](diagnostics.md) — diagnostic code catalog.
19
+ - [Generated artifacts](generated-artifacts.md) — static evaluation, output
20
+ headers, dependencies, and standalone files.
21
+ - [Public API](public-api.md) — stable, experimental, and internal boundaries.
22
+ - [Compatibility](compatibility.md) — Ruby, Unicode, backend, and artifact
23
+ compatibility commitments.
@@ -0,0 +1,47 @@
1
+ # Action context reference
2
+
3
+ Rule blocks, `on_eof` blocks, and generated action bodies run with the lexer
4
+ instance as `self`.
5
+
6
+ ## Token methods
7
+
8
+ | Method | Contract |
9
+ |---|---|
10
+ | `emit(type, value = text)` | Queue one token; `type` may be `nil` |
11
+ | `skip` | Queue no token for the current match |
12
+ | `echo` | Equivalent to `emit(nil, text)` |
13
+ | `error!(message)` | Apply the configured input error policy to a `LexError` |
14
+
15
+ ## Match and position methods
16
+
17
+ | Method | Contract |
18
+ |---|---|
19
+ | `text` | Matched text, including a `more` prefix when present |
20
+ | `text_bytesize` | Byte size of `text` |
21
+ | `last_location` | A `Runtime::Location` for the current text span |
22
+ | `lineno` / `line` | One-based current line number |
23
+ | `byte_pos` | Current byte offset |
24
+ | `beginning_of_line?` | Whether the current position is at line start |
25
+ | `state` | Current state symbol |
26
+ | `binary_input` | Input as a binary string |
27
+
28
+ `text` is memoized for the current action. `last_location` uses the same span;
29
+ `more` makes the span include all joined matches.
30
+
31
+ ## State methods
32
+
33
+ | Method | Contract |
34
+ |---|---|
35
+ | `push(name)` / `push_state(name)` | Save the current state and enter `name` |
36
+ | `pop` / `pop_state` | Restore the previous state, or `:initial` when the stack is empty |
37
+ | `begin_state(name)` / `state=` | Enter `name` without changing the stack |
38
+
39
+ ## Flex-compatible methods
40
+
41
+ | Method | Contract |
42
+ |---|---|
43
+ | `less(count)` | Keep only the first `count` bytes of the current match |
44
+ | `more` | Join the next match into the current token text |
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.
@@ -0,0 +1,80 @@
1
+ # CLI reference
2
+
3
+ The executable is `flexr`, with usage:
4
+
5
+ ```text
6
+ flexr [COMMAND] SPEC.rb [options]
7
+ ```
8
+
9
+ Without a command, `flexr SPEC.rb` generates Ruby. `SPEC.rb` is normally a
10
+ `.flexr.rb` file, except for `import`, which accepts flex `.l` and Rexical
11
+ `.rex` input.
12
+
13
+ ## Commands
14
+
15
+ | Command | Purpose | Main output |
16
+ |---|---|---|
17
+ | `check SPEC` | Compile and render diagnostics | Human or JSON diagnostics |
18
+ | `stats SPEC` | Report DFA states, byte classes, and table sizes | JSON |
19
+ | `tokens SPEC` | Print names declared by `emits` | Space-separated names |
20
+ | `dot SPEC` | Render the DFA as Graphviz DOT | DOT on stdout |
21
+ | `trace SPEC` | Print DFA states, accepts, and transitions | Text on stdout |
22
+ | `explain SPEC` | List parsed rules and patterns | Text on stdout |
23
+ | `bench SPEC` | Run the benchmark harness | Benchmark output |
24
+ | `import INPUT` | Translate flex/Rexical input | Ruby on stdout or `--output` |
25
+
26
+ `trace` describes the automaton; it does not trace a particular input string.
27
+ `dot` marks accepting states and accelerated regions for visual inspection.
28
+
29
+ ## Options
30
+
31
+ | Option | Values / argument | Applies to |
32
+ |---|---|---|
33
+ | `-o, --output PATH` | File path | Generation and import |
34
+ | `-b, --backend NAME` | `table`, `direct`, `firstmatch`, `auto` | Compilation |
35
+ | `--token-kind KIND` | `array`, `struct`, `yield` | Generated/runtime contract |
36
+ | `--accel MODE` | `auto`, `strscan`, `regexp`, `none` | Runtime optimization |
37
+ | `--standalone` | Flag | Generation |
38
+ | `--eval` | Flag | Generation and check |
39
+ | `--table-compression VALUE` | `none`, `rows`, `full` | Generation |
40
+ | `--table-format VALUE` | `literal`, `packed` | Generation |
41
+ | `--max-dfa-states N` | Positive integer | Compilation |
42
+ | `-W, --warn LEVEL` | `all`, `default`, `none` | Check |
43
+ | `--warn-as-error` | Flag | Check and generation |
44
+ | `--color WHEN` | `auto`, `always`, `never` | Human diagnostics |
45
+ | `--format FMT` | `human`, `json` | Check and stats |
46
+ | `--rule N` | Non-negative integer | Explain |
47
+
48
+ The complete help output is kept as a checked snapshot:
49
+
50
+ <!-- flexr-help:start -->
51
+ Usage: flexr [COMMAND] SPEC.rb [options]
52
+
53
+ Commands: check, stats, tokens, dot, explain, trace, bench, import
54
+ Options:
55
+ -o, --output PATH generated output path
56
+ -b, --backend NAME table | direct | firstmatch | auto
57
+ --token-kind KIND array | struct | yield
58
+ --accel MODE auto | strscan | regexp | none
59
+ --standalone
60
+ --eval
61
+ --table-compression VALUE none | rows | full
62
+ --table-format VALUE literal | packed
63
+ --max-dfa-states N
64
+ -W, --warn LEVEL all | default | none
65
+ --warn-as-error
66
+ --color WHEN auto | always | never
67
+ --format FMT human | json
68
+ <!-- flexr-help:end -->
69
+
70
+ ## Exit statuses
71
+
72
+ | Status | Meaning |
73
+ |---:|---|
74
+ | `0` | Command completed; warnings do not fail by default |
75
+ | `1` | Specification, input, import, or generation failure |
76
+ | `2` | Invalid command-line usage |
77
+
78
+ `check --format json` writes one JSON array to stdout. Human diagnostics go to
79
+ stdout for `check` and errors go to stderr for command failures. Use
80
+ `--warn-as-error` when a warning should fail CI.
@@ -0,0 +1,38 @@
1
+ # Compatibility
2
+
3
+ ## Ruby and dependencies
4
+
5
+ | Surface | Requirement |
6
+ |---|---|
7
+ | Runtime gem | Ruby 3.1+ |
8
+ | Generator | Ruby 3.3+ with Prism available |
9
+ | Standalone runtime | Compatible Ruby plus the generated file |
10
+ | Optional acceleration | Ruby `strscan` when available; regexp fallback remains valid |
11
+
12
+ Runtime mode does not require Prism. Generated mode uses Prism only while
13
+ reading the source; normal generated output does not need the generator.
14
+
15
+ ## Matching and encoding
16
+
17
+ UTF-8 and binary are the supported specification encodings. The automaton is
18
+ byte-oriented, but UTF-8 boundaries and Unicode properties are validated using
19
+ the vendored data snapshot. Invalid UTF-8 is not silently repaired.
20
+
21
+ ## Unicode snapshot
22
+
23
+ The current Unicode Character Database snapshot is 15.1.0. It supplies Unicode
24
+ properties, POSIX behavior where applicable, Unicode shorthand behavior under
25
+ `option :unicode`, and case folding. Updating the snapshot can change generated
26
+ output and is therefore a minor-version compatibility change. Run
27
+ `bundle exec rake unicode:verify` and regenerate golden artifacts after an
28
+ update.
29
+
30
+ ## Generated files and SemVer
31
+
32
+ Runtime and generated-file compatibility is preserved within a major version
33
+ unless a migration is explicitly documented. Generated artifacts include a
34
+ payload digest and Unicode version so stale or cross-version output can be
35
+ detected. Do not assume that internal table layouts are stable.
36
+
37
+ Stable backends are `table`, `direct`, and `auto`. `firstmatch` is experimental
38
+ and does not share the longest-match promise.