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.
- checksums.yaml +7 -0
- data/.rubocop.yml +33 -0
- data/CONTRIBUTING.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +468 -0
- data/benchmark/baselines/json.json +34 -0
- data/benchmark/baselines/json_handwritten.rb +43 -0
- data/benchmark/baselines/json_rexical.rex +25 -0
- data/benchmark/corpora/README.md +11 -0
- data/benchmark/corpora/generate_json.rb +26 -0
- data/benchmark/golden/calculator_lexer.sha256 +1 -0
- data/benchmark/golden/json_lexer.sha256 +1 -0
- data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
- data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
- data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
- data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
- data/benchmark/golden/with_racc_lexer.sha256 +1 -0
- data/benchmark/run.rb +254 -0
- data/docs/README.md +64 -0
- data/docs/RELEASING.md +30 -0
- data/docs/adr/0001-byte-level-dfa.md +5 -0
- data/docs/adr/0003-leftmost-longest.md +4 -0
- data/docs/adr/0006-accel-not-scanner.md +4 -0
- data/docs/adr/0008-what-pure-ruby-means.md +5 -0
- data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
- data/docs/adr/0017-static-analysis-by-default.md +5 -0
- data/docs/adr/0018-prism-for-generator-only.md +4 -0
- data/docs/adr/0019-measured-performance-floor.md +26 -0
- data/docs/adr/0020-vendored-unicode-contract.md +21 -0
- data/docs/explanation/backends.md +33 -0
- data/docs/explanation/matching-semantics.md +20 -0
- data/docs/explanation/runtime-vs-generated.md +22 -0
- data/docs/explanation/security-model.md +18 -0
- data/docs/explanation/unicode-and-encoding.md +20 -0
- data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
- data/docs/how-to/generate-a-lexer.md +39 -0
- data/docs/how-to/handle-errors.md +32 -0
- data/docs/how-to/integrate-with-lrama.md +21 -0
- data/docs/how-to/integrate-with-racc.md +25 -0
- data/docs/how-to/migrate-from-flex.md +21 -0
- data/docs/how-to/migrate-from-rexical.md +23 -0
- data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
- data/docs/how-to/track-token-locations.md +27 -0
- data/docs/how-to/tune-performance.md +23 -0
- data/docs/how-to/use-states.md +36 -0
- data/docs/how-to/use-trailing-context.md +22 -0
- data/docs/internals/README.md +14 -0
- data/docs/perf-log.md +56 -0
- data/docs/reference/README.md +23 -0
- data/docs/reference/actions.md +47 -0
- data/docs/reference/cli.md +80 -0
- data/docs/reference/compatibility.md +38 -0
- data/docs/reference/diagnostics.md +41 -0
- data/docs/reference/dsl.md +81 -0
- data/docs/reference/errors.md +27 -0
- data/docs/reference/generated-artifacts.md +50 -0
- data/docs/reference/public-api.md +42 -0
- data/docs/reference/regexp.md +39 -0
- data/docs/reference/runtime.md +49 -0
- data/docs/reference/tokens-and-locations.md +33 -0
- data/docs/tutorial/build-a-calculator-lexer.md +96 -0
- data/examples/calculator/README.md +27 -0
- data/examples/calculator/lexer.flexr.rb +17 -0
- data/examples/json/README.md +30 -0
- data/examples/json/lexer.flexr.rb +24 -0
- data/examples/ruby_subset/README.md +17 -0
- data/examples/ruby_subset/lexer.flexr.rb +22 -0
- data/examples/toy_lang/README.md +17 -0
- data/examples/toy_lang/lexer.flexr.rb +18 -0
- data/examples/with_lrama/README.md +17 -0
- data/examples/with_lrama/lexer.flexr.rb +13 -0
- data/examples/with_racc/README.md +17 -0
- data/examples/with_racc/lexer.flexr.rb +13 -0
- data/exe/flexr +7 -0
- data/lib/flexr/automaton/accel.rb +39 -0
- data/lib/flexr/automaton/analysis.rb +38 -0
- data/lib/flexr/automaton/byte_class_set.rb +29 -0
- data/lib/flexr/automaton/compiler.rb +413 -0
- data/lib/flexr/automaton/dfa.rb +103 -0
- data/lib/flexr/automaton/minimizer.rb +70 -0
- data/lib/flexr/automaton/nfa.rb +92 -0
- data/lib/flexr/cli.rb +342 -0
- data/lib/flexr/codegen/base.rb +17 -0
- data/lib/flexr/codegen/direct.rb +52 -0
- data/lib/flexr/codegen/firstmatch.rb +17 -0
- data/lib/flexr/codegen/table.rb +158 -0
- data/lib/flexr/codegen/table_packer.rb +61 -0
- data/lib/flexr/diagnostics.rb +94 -0
- data/lib/flexr/dsl.rb +182 -0
- data/lib/flexr/errors.rb +28 -0
- data/lib/flexr/generated.rb +125 -0
- data/lib/flexr/generator.rb +400 -0
- data/lib/flexr/importer.rb +560 -0
- data/lib/flexr/ir.rb +36 -0
- data/lib/flexr/lexer.rb +10 -0
- data/lib/flexr/options.rb +47 -0
- data/lib/flexr/rake_task.rb +27 -0
- data/lib/flexr/regexp/ast.rb +45 -0
- data/lib/flexr/regexp/char_class.rb +7 -0
- data/lib/flexr/regexp/normalizer.rb +117 -0
- data/lib/flexr/regexp/parser.rb +517 -0
- data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
- data/lib/flexr/regexp/tokenizer.rb +168 -0
- data/lib/flexr/regexp/unsupported.rb +7 -0
- data/lib/flexr/runtime/buffer.rb +112 -0
- data/lib/flexr/runtime/core.rb +388 -0
- data/lib/flexr/runtime/errors.rb +22 -0
- data/lib/flexr/runtime/interpreter.rb +505 -0
- data/lib/flexr/runtime/location.rb +26 -0
- data/lib/flexr/runtime/token.rb +7 -0
- data/lib/flexr/source/passthrough.rb +31 -0
- data/lib/flexr/source/prism_reader.rb +283 -0
- data/lib/flexr/source/static_eval.rb +145 -0
- data/lib/flexr/unicode/case_fold.rb +45 -0
- data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
- data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
- data/lib/flexr/unicode/data/case_folding.rb +9 -0
- data/lib/flexr/unicode/data/properties.rb +10 -0
- data/lib/flexr/unicode/property.rb +107 -0
- data/lib/flexr/unicode/reference_regexp.rb +102 -0
- data/lib/flexr/unicode/utf8_splitter.rb +109 -0
- data/lib/flexr/version.rb +5 -0
- data/lib/flexr.rb +81 -0
- data/site/README.md +22 -0
- data/site/astro.config.mjs +57 -0
- data/site/package.json +19 -0
- data/site/pnpm-lock.yaml +5029 -0
- data/site/pnpm-workspace.yaml +6 -0
- data/site/public/playground.js +189 -0
- data/site/scripts/verify-site.mjs +42 -0
- data/site/src/content/docs/benchmarks.md +8 -0
- data/site/src/content/docs/concepts/matching-semantics.md +15 -0
- data/site/src/content/docs/concepts/regexp-model.md +18 -0
- data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
- data/site/src/content/docs/concepts/security-model.md +15 -0
- data/site/src/content/docs/examples.md +17 -0
- data/site/src/content/docs/learn/generation.md +29 -0
- data/site/src/content/docs/learn/getting-started.md +56 -0
- data/site/src/content/docs/learn/parser-integration.md +27 -0
- data/site/src/content/docs/learn/runtime-mode.md +32 -0
- data/site/src/content/docs/reference/action-context.md +20 -0
- data/site/src/content/docs/reference/cli.md +22 -0
- data/site/src/content/docs/reference/diagnostics.md +16 -0
- data/site/src/content/docs/reference/dsl.md +19 -0
- data/site/src/content/docs/reference/public-api.md +18 -0
- data/site/src/content/docs/reference/regexp.md +16 -0
- data/site/src/content/docs/reference/runtime.md +16 -0
- data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
- data/site/src/content.config.ts +12 -0
- data/site/src/env.d.ts +1 -0
- data/site/src/layouts/SiteLayout.astro +39 -0
- data/site/src/pages/index.astro +174 -0
- data/site/src/pages/playground.astro +64 -0
- data/site/src/styles/custom.css +711 -0
- data/site/tsconfig.json +5 -0
- data/tools/coverage.rb +32 -0
- data/tools/docs_verify.rb +116 -0
- data/tools/gen_unicode_tables.rb +202 -0
- data/tools/regexp_tokenizer_reference.rb +60 -0
- 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.
|