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,41 @@
1
+ # Diagnostics catalog
2
+
3
+ Diagnostics have a code, severity, message, optional location, help, and note.
4
+ Use `flexr check SPEC --format json` to consume the structured form.
5
+
6
+ ## Errors
7
+
8
+ | Code | Meaning | First fix to try |
9
+ |---|---|---|
10
+ | `FLEXR-E001` | Regexp syntax is malformed | Correct the regexp syntax |
11
+ | `FLEXR-E003` | A state name is undefined | Declare the state before entering it |
12
+ | `FLEXR-E005` | A rule can match an empty string | Make it consuming or opt into `allow_empty_match` |
13
+ | `FLEXR-E006` | DFA state limit exceeded | Raise `max_dfa_states` or split rules |
14
+ | `FLEXR-E007` | Bounded repetition exceeds 1000 | Use a smaller bound or split the rule |
15
+ | `FLEXR-E009` | Anchor is not at an outermost boundary | Move `^` / `$` or split alternatives |
16
+ | `FLEXR-E010` | Prism reported source syntax errors | Fix Ruby syntax in the specification |
17
+ | `FLEXR-E011` | Encoding is not UTF-8 or binary | Change `encoding` |
18
+ | `FLEXR-E012` | Runtime token exceeded the size limit | Raise `max_token_size` or split the token |
19
+ | `FLEXR-E013` | `reject` is not supported | Use states and `less` |
20
+ | `FLEXR-E014` | Regexp construct is not DFA-compatible | Follow the diagnostic alternative |
21
+ | `FLEXR-E017` | Pattern is not statically resolvable | Rewrite as a static expression or use trusted `--eval` |
22
+ | `FLEXR-E018` | Rule pattern or trailing context has an invalid type | Use a regexp, string, or valid array |
23
+ | `FLEXR-E019` | Prism is unavailable for generation | Install the generator dependencies |
24
+
25
+ ## Warnings
26
+
27
+ | Code | Meaning | First fix to try |
28
+ |---|---|---|
29
+ | `FLEXR-W001` | Rule is unreachable or shadowed | Remove, reorder, or distinguish it |
30
+ | `FLEXR-W002` | Named state has no rules | Add rules or remove the state |
31
+ | `FLEXR-W003` | Trailing context has variable length | Make both parts fixed length |
32
+ | `FLEXR-W010` | `firstmatch` overlap may change semantics | Use the stable table backend |
33
+ | `FLEXR-W011` | Transition table is large | Use direct backend, compression, or split rules |
34
+ | `FLEXR-W012` | Trailing context cannot use region acceleration | Remove context or set `accel :none` |
35
+ | `FLEXR-W013` | Capturing group is treated as non-capturing | Use `(?:...)` |
36
+ | `FLEXR-W014` | Emitted token is not declared | Add it to `emits` or accept the warning |
37
+ | `FLEXR-W016` | DFA construction took more than 0.5 seconds | Use generated mode for startup |
38
+
39
+ Warnings are reported by `check` according to `--warn`. The default omits the
40
+ wall-clock `FLEXR-W016`; `--warn all` includes it, and `--warn none` suppresses
41
+ warnings. `--warn-as-error` turns selected warnings into a failed command.
@@ -0,0 +1,81 @@
1
+ # DSL reference
2
+
3
+ The DSL is extended onto a subclass of `Flexr::Lexer`. Class bodies are normal
4
+ Ruby and may contain constants, methods, requires, comments, and ordinary
5
+ expressions.
6
+
7
+ ## `rule(pattern, skip: false, emit: nil, followed_by: nil, &action)`
8
+
9
+ Registers one rule. `pattern` is a `Regexp`, `String`, or an array containing
10
+ those values. A string is matched literally. Array members are alternatives of
11
+ one rule and share its action.
12
+
13
+ `skip: true` discards the match and takes precedence over `emit` and the block.
14
+ `emit: :TOKEN` installs a simple action that emits the token with the matched
15
+ text as its value. Without either option, the block runs in the lexer instance;
16
+ if no block is given, the default action emits `[nil, text]`.
17
+
18
+ `followed_by:` accepts a `Regexp` or `String`. It participates in rule
19
+ selection but is not consumed. The method returns `nil`. Rules are numbered in
20
+ source order starting at zero.
21
+
22
+ ## `state(*names, inclusive: false, &block)`
23
+
24
+ Creates one or more named states and registers rules in the block for those
25
+ states. The block is required. Named states are exclusive by default. An
26
+ inclusive state also sees rules registered for `:initial`.
27
+
28
+ ## `all_states(&block)`
29
+
30
+ Registers the block for every state known when the call is evaluated, including
31
+ `:initial`. It has the same rule-registration behavior as `state`.
32
+
33
+ ## `on_eof(&action)`
34
+
35
+ Installs an action for EOF in the current state. If called outside a state block,
36
+ the action belongs to `:initial`. The action can emit a final token, change
37
+ state, or do nothing. It runs at most once per state for a lexer instance.
38
+
39
+ ## `emits(*tokens)`
40
+
41
+ Declares token names for diagnostics and parser integration. Arguments are
42
+ flattened and converted to symbols. Duplicate names are removed. The method
43
+ does not restrict `emit` at runtime.
44
+
45
+ ## `backend(name)`
46
+
47
+ Selects `:table`, `:direct`, `:auto`, or the experimental `:firstmatch`
48
+ backend. `:table`, `:direct`, and `:auto` preserve the stable longest-match
49
+ contract. `:firstmatch` requires `option :experimental` and can change the
50
+ meaning of overlapping rules.
51
+
52
+ ## `token_kind(name)`
53
+
54
+ Selects `:array`, `:struct`, or `:yield`. The default is `:array`. See the
55
+ [token contract](tokens-and-locations.md) for the exact shapes.
56
+
57
+ ## `encoding(value)`
58
+
59
+ Accepts `Encoding::UTF_8`, `Encoding::BINARY`, or a name resolving to one of
60
+ them. Other encodings raise `FLEXR-E011`. The regexp encoding can override the
61
+ specification encoding for an individual pattern only when the resulting byte
62
+ model is valid.
63
+
64
+ ## `option(*values)`
65
+
66
+ Adds boolean options to the specification. Stable options include
67
+ `: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.
70
+
71
+ ## `accel(value)`
72
+
73
+ Selects `:auto`, `:strscan`, `:regexp`, or `:none` for runtime region
74
+ acceleration. Acceleration is an optimization; the DFA remains the semantic
75
+ source of truth. Affected trailing-context rules cannot use region acceleration.
76
+
77
+ ## Compilation helpers
78
+
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).
@@ -0,0 +1,27 @@
1
+ # Errors and recovery
2
+
3
+ ## Specification errors
4
+
5
+ Compile-time failures raise `Flexr::CompileError` or its
6
+ `Flexr::UnsupportedRegexpError` subclass. Both expose a structured
7
+ `diagnostic`. Static generation can additionally raise
8
+ `Flexr::StaticResolutionError` with `FLEXR-E017`.
9
+
10
+ ## Input errors
11
+
12
+ `Flexr::LexError` records the message, filename, byte position, line, matched
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`.
16
+
17
+ The default `error_mode: :raise` raises the lexical error. `:token` queues
18
+ `[:error, text]`; `:panic` discards the unmatched byte and continues. An
19
+ `on_error` callback can return `:skip`, `:raise`, `:token`, or `:halt` for
20
+ instance-specific control.
21
+
22
+ ## Import failures
23
+
24
+ `flexr import` reports untranslated actions as `FLEXR-TODO`, returns status 1,
25
+ and never writes incomplete output. This is intentionally stricter than a
26
+ best-effort migration because a partially translated lexer can silently change
27
+ language behavior.
@@ -0,0 +1,50 @@
1
+ # Generated artifacts
2
+
3
+ ## Dependency matrix
4
+
5
+ | Use | Build-time requirement | Runtime requirement |
6
+ |---|---|---|
7
+ | Runtime mode | None beyond the flexr gem | `flexr` gem |
8
+ | Normal generated mode | flexr gem and Prism on Ruby 3.3+ | `flexr` gem |
9
+ | Standalone generated mode | flexr gem and Prism on Ruby 3.3+ | Generated file and Ruby standard library |
10
+
11
+ The runtime gem supports Ruby 3.1 and newer. The generator's Prism path is
12
+ supported on Ruby 3.3 and newer. A generated file can be loaded by a compatible
13
+ runtime without the generator installed.
14
+
15
+ ## Static evaluation boundary
16
+
17
+ The default generator resolves:
18
+
19
+ - regexp, string, numeric, symbol, and range literals;
20
+ - arrays and hashes made from supported values;
21
+ - constants in lexical and qualified scope;
22
+ - interpolation whose expressions resolve statically;
23
+ - `.freeze` on a static value; and
24
+ - `Regexp.union` with static arguments.
25
+
26
+ Methods with runtime-dependent results, `Time`, IO, environment reads, and
27
+ arbitrary dynamic calls produce `FLEXR-E017`. `--eval` is the explicit escape
28
+ hatch and executes the complete specification.
29
+
30
+ ## Source transformation
31
+
32
+ The generator removes DSL call spans, inserts `Flexr::Generated.install_compiled!`,
33
+ and adds generated scanner methods. User actions remain Ruby source. A generated
34
+ header records:
35
+
36
+ - source path;
37
+ - SHA-256 digest of the generated payload;
38
+ - vendored Unicode version;
39
+ - effective backend;
40
+ - whether compilation used eval; and
41
+ - whether the runtime is standalone.
42
+
43
+ Do not edit generated files by hand. Regenerate them when the specification,
44
+ generator, backend, table format, or Unicode snapshot changes.
45
+
46
+ ## Table formats
47
+
48
+ `--table-compression none|rows|full` controls row packing. `--table-format
49
+ literal|packed` controls whether packed arrays are emitted as Ruby literals or
50
+ Base64. These affect artifact size and loading cost, not matching semantics.
@@ -0,0 +1,42 @@
1
+ # Public API and stability
2
+
3
+ Compatibility guarantees apply only to the stable surface below. Everything
4
+ else may change between releases without a deprecation cycle.
5
+
6
+ ## Public and stable
7
+
8
+ - `Flexr::Lexer` and the DSL methods in [dsl.md](dsl.md).
9
+ - Runtime consumption: `initialize`, `next_token`, `tokens`, `each_token`, and
10
+ `racc_next_token`.
11
+ - Action context methods in [actions.md](actions.md).
12
+ - `Flexr::Runtime::Token`, `Flexr::Runtime::Location`, `Flexr::LexError`,
13
+ `Flexr::CompileError`, `Flexr::UnsupportedRegexpError`, and
14
+ `Flexr::Runtime::TokenTooLargeError`.
15
+ - `Flexr::Diagnostic`, `Flexr::DiagnosticSet`, and `Flexr::Diagnostics`.
16
+ - `Flexr::Generator` for source generation, `Flexr::RakeTask` for Rake builds,
17
+ and `Flexr::CLI.run` for programmatic CLI invocation.
18
+ - `Flexr.compile_pattern` and `Flexr.parse_pattern` for regexp tooling.
19
+
20
+ Method arguments, return shapes, and diagnostics for this surface are described
21
+ in the reference pages and are covered by the test suite.
22
+
23
+ ## Experimental
24
+
25
+ - `backend :firstmatch` and `--backend firstmatch`.
26
+ - `option :experimental` as the opt-in for experimental behavior.
27
+ - Any future backend or option explicitly marked experimental in its reference.
28
+
29
+ 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.
32
+
33
+ ## Internal; compatibility is not guaranteed
34
+
35
+ - `Flexr::IR`, `Flexr::Automaton`, `Flexr::Codegen`, and their members.
36
+ - `Flexr::Runtime::Buffer` and `Flexr::Runtime::Interpreter` internals.
37
+ - `Flexr::Generated` installation helpers.
38
+ - `__flexr_*` methods and instance variables.
39
+ - `Flexr::Lexer.compile!` and `.dfa` as implementation inspection hooks.
40
+
41
+ Use stable token and diagnostic APIs instead of depending on internal compiled
42
+ tables or generated installation methods.
@@ -0,0 +1,39 @@
1
+ # Regexp compatibility
2
+
3
+ flexr accepts the regular-expression subset that can be represented by its
4
+ byte-oriented automaton. All matches start at the current lexer position.
5
+
6
+ | Ruby construct | Status | Notes and alternative |
7
+ |---|---|---|
8
+ | Literals, concatenation, alternation | Supported | |
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 |
11
+ | Unicode `\p{...}` / `\P{...}` | Supported | Uses the vendored UCD snapshot |
12
+ | `*`, `+`, `?` | Supported | Greedy forms only |
13
+ | `{n}`, `{n,m}` | Supported | The upper bound is 1000 |
14
+ | `{n,}` | Unsupported | Split the rule or use states |
15
+ | `^` / `$` | Restricted | Only at outermost pattern boundaries; otherwise `FLEXR-E009` |
16
+ | `/i`, `/m`, `/x`, `/n` | Supported | Inline forms are supported where Ruby permits them |
17
+ | `\d`, `\w`, `\s` | Supported | Unicode-aware with `option :unicode` in UTF-8 |
18
+ | Lookahead / lookbehind | Unsupported | Use `followed_by:` or a state; `FLEXR-E014` |
19
+ | Backreferences | Unsupported | Split the language into states or actions |
20
+ | `\b`, `\B`, `\A`, `\z`, `\G`, `\K` | Unsupported | Use rule boundaries or state/action logic |
21
+ | Lazy or possessive quantifiers | Unsupported | Use a negated character class or separate rule |
22
+ | Conditional, atomic, and capture-dependent groups | Unsupported | Rewrite as DFA-compatible rules |
23
+
24
+ Malformed syntax produces `FLEXR-E001`. Unsupported constructs produce
25
+ `FLEXR-E014`; the diagnostic help normally names the supported replacement.
26
+ Empty matches are rejected with `FLEXR-E005` unless `option :allow_empty_match`
27
+ is explicitly enabled, in which case flexr advances to guarantee progress.
28
+
29
+ ## Bounded repetition
30
+
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.
34
+
35
+ ## Captures
36
+
37
+ Capturing parentheses do not expose match groups to actions. They are treated
38
+ as grouping syntax and produce `FLEXR-W013`; use `(?:...)` and inspect `text`
39
+ in the action instead.
@@ -0,0 +1,49 @@
1
+ # Runtime reference
2
+
3
+ `Flexr::Lexer` includes the runtime methods below. Runtime mode and generated
4
+ mode expose the same consumer-facing methods.
5
+
6
+ ## Initialization
7
+
8
+ ```ruby
9
+ Lexer.new(input,
10
+ filename: nil,
11
+ error_mode: :raise,
12
+ max_token_size: 16 * 1024 * 1024,
13
+ max_state_stack: 1024,
14
+ chunk_size: 64 * 1024)
15
+ ```
16
+
17
+ `input` must be a `String` or an object responding to `read`. `filename` is
18
+ copied into locations and lexical errors. `error_mode` is `:raise`, `:token`,
19
+ or `:panic`; unknown values fall back to raising when an unmatched byte is
20
+ handled. Size limits are non-negative, and `chunk_size` must be positive.
21
+
22
+ ## Consumption
23
+
24
+ | Method | Result |
25
+ |---|---|
26
+ | `next_token` | One token, or `nil` at EOF |
27
+ | `tokens` | Array containing every token until EOF |
28
+ | `each_token` | Enumerator without a block; otherwise yields tokens and returns `self` |
29
+ | `racc_next_token` | `[type, value]`, or `[false, "$end"]` at EOF |
30
+ | `input` | The buffered input string |
31
+ | `buffer` | Internal buffered input object; use only for integrations that need it |
32
+ | `filename` | The configured filename |
33
+ | `error_mode` | The configured default input error mode |
34
+ | `max_token_size` | The configured token-size limit |
35
+
36
+ `each_token` yields two arguments instead of one when `token_kind :yield` is
37
+ selected. `tokens` always returns an array of token values.
38
+
39
+ ## State and action context
40
+
41
+ The public action methods are collected in the [actions reference](actions.md).
42
+ State transitions are instance-local; changing one lexer does not change the
43
+ class or another lexer instance.
44
+
45
+ ## Streaming input
46
+
47
+ IO input is read lazily in chunks. UTF-8 input is checked at codepoint
48
+ boundaries, while binary input is consumed byte by byte. Invalid UTF-8 becomes
49
+ an unmatched-byte error rather than being silently normalized.
@@ -0,0 +1,33 @@
1
+ # Tokens and locations
2
+
3
+ ## Token kinds
4
+
5
+ | `token_kind` | `next_token` / `tokens` | `each_token` |
6
+ |---|---|---|
7
+ | `:array` (default) | `[type, value]` | Yields one array |
8
+ | `:struct` | `Flexr::Runtime::Token` | Yields one `Token` |
9
+ | `:yield` | `[type, value]` | Yields `type, value` as separate arguments |
10
+
11
+ For `:struct`, `Token` has `type`, `value`, and `location` members. `emit(nil,
12
+ value)` is valid and produces a token whose type is `nil`. `skip` produces no
13
+ token.
14
+
15
+ ## Locations
16
+
17
+ `Flexr::Runtime::Location` has these public members:
18
+
19
+ | Member | Meaning |
20
+ |---|---|
21
+ | `filename` | The `filename:` value passed to the lexer |
22
+ | `byte_begin` | Inclusive byte offset of the token |
23
+ | `byte_end` | Exclusive byte offset of the token |
24
+ | `line_begin` / `line_end` | One-based line range |
25
+ | `column_begin` / `column_end` | One-based character columns for UTF-8, byte columns for binary input |
26
+
27
+ Columns are computed lazily by default. `option :eager_columns` fills them while
28
+ the location is created. The end line and column identify the position after
29
+ the token's last byte, so a token ending at the start of the next line may have
30
+ an end column of 1.
31
+
32
+ `more` extends the location from the first match to the final match. Trailing
33
+ context is not part of the location because it is not consumed.
@@ -0,0 +1,96 @@
1
+ # Build a calculator lexer
2
+
3
+ This tutorial takes one specification through runtime execution, diagnostics,
4
+ generation, and generated execution. The complete executable source is
5
+ [`examples/calculator/lexer.flexr.rb`](../../examples/calculator/lexer.flexr.rb).
6
+
7
+ ## 1. Install flexr
8
+
9
+ ```sh
10
+ gem install flexr
11
+ ```
12
+
13
+ For a checkout, use the bundle and add `lib` to Ruby's load path:
14
+
15
+ ```sh
16
+ bundle install
17
+ bundle exec ruby -Ilib examples/calculator/lexer.flexr.rb
18
+ ```
19
+
20
+ ## 2. Read the specification
21
+
22
+ ```ruby
23
+ class Lexer < Flexr::Lexer
24
+ emits :EQ, :ASSIGN, :IF, :IDENT, :INTEGER, :PLUS
25
+
26
+ rule(/[ \t\r\n]+/, skip: true)
27
+ rule(/==/) { emit :EQ }
28
+ rule(/=/) { emit :ASSIGN }
29
+ rule(/if/) { emit :IF }
30
+ rule(/[a-z_][a-z0-9_]*/) { emit :IDENT }
31
+ rule(/[0-9]+/) { emit :INTEGER, text.to_i }
32
+ rule(/\+/) { emit :PLUS }
33
+ end
34
+ ```
35
+
36
+ `emits` is a declaration used by diagnostics and parser integration. It does
37
+ not create constants and is not required for `emit` to work.
38
+
39
+ The `==` rule is longer than `=`, so it wins even though both rules are active.
40
+ The input `if` matches both the keyword and identifier rules with equal length;
41
+ the keyword rule appears first, so it wins. The input `ifx` is longer under the
42
+ identifier rule and therefore becomes one identifier token.
43
+
44
+ ## 3. Run runtime mode
45
+
46
+ ```sh
47
+ ruby -Ilib -e 'require "json"; load "examples/calculator/lexer.flexr.rb"; puts JSON.generate(CalculatorExample::Lexer.new("if ifx == = 12 + 3").tokens)'
48
+ ```
49
+
50
+ Expected output:
51
+
52
+ ```json
53
+ [["IF","if"],["IDENT","ifx"],["EQ","=="],["ASSIGN","="],["INTEGER",12],["PLUS","+"],["INTEGER",3]]
54
+ ```
55
+
56
+ Runtime mode evaluates the Ruby specification normally. The DFA is compiled
57
+ when the lexer class is first used, and actions run in the lexer instance.
58
+
59
+ ## 4. Validate the specification
60
+
61
+ ```sh
62
+ flexr check examples/calculator/lexer.flexr.rb --format json
63
+ ```
64
+
65
+ An empty JSON array means that the specification produced no errors or selected
66
+ warnings. Use `--warn all` to include the full warning catalog.
67
+
68
+ ## 5. Generate Ruby
69
+
70
+ ```sh
71
+ flexr examples/calculator/lexer.flexr.rb -o /tmp/calculator_lexer.rb
72
+ ```
73
+
74
+ The default generator statically resolves patterns and constants with Prism.
75
+ The generated file keeps ordinary Ruby surrounding the DSL, embeds the
76
+ compiled tables, and keeps actions as Ruby code.
77
+
78
+ ## 6. Run the generated lexer
79
+
80
+ ```sh
81
+ ruby -Ilib -e 'require "json"; load ARGV.fetch(0); puts JSON.generate(CalculatorExample::Lexer.new("if ifx == = 12 + 3").tokens)' /tmp/calculator_lexer.rb
82
+ ```
83
+
84
+ The output must match runtime mode. `rake modes:equivalence` checks this
85
+ relationship for every committed example.
86
+
87
+ ## 7. Inspect the contract
88
+
89
+ ```sh
90
+ flexr tokens examples/calculator/lexer.flexr.rb
91
+ flexr stats examples/calculator/lexer.flexr.rb
92
+ flexr explain examples/calculator/lexer.flexr.rb --rule 1
93
+ ```
94
+
95
+ Use the [DSL reference](../reference/dsl.md) for method arguments and the
96
+ [CLI reference](../reference/cli.md) for output and exit-status behavior.
@@ -0,0 +1,27 @@
1
+ # Calculator lexer example
2
+
3
+ This is the executable source for the first-time user tutorial. It keeps the
4
+ rules deliberately small while showing two important matching contracts:
5
+
6
+ - `==` wins over `=` because the longest match wins.
7
+ - `if` wins over the identifier rule for the input `if` because equal-length
8
+ matches use source order.
9
+
10
+ Run it directly:
11
+
12
+ ```sh
13
+ ruby -Ilib -e 'require "json"; load "examples/calculator/lexer.flexr.rb"; puts JSON.generate(CalculatorExample::Lexer.new("if ifx == = 12 + 3").tokens)'
14
+ ```
15
+
16
+ Expected output:
17
+
18
+ ```json
19
+ [["IF","if"],["IDENT","ifx"],["EQ","=="],["ASSIGN","="],["INTEGER",12],["PLUS","+"],["INTEGER",3]]
20
+ ```
21
+
22
+ Validate and generate it with:
23
+
24
+ ```sh
25
+ flexr check examples/calculator/lexer.flexr.rb --format json
26
+ flexr examples/calculator/lexer.flexr.rb -o /tmp/calculator_lexer.rb
27
+ ```
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flexr"
4
+
5
+ module CalculatorExample
6
+ class Lexer < Flexr::Lexer
7
+ emits :EQ, :ASSIGN, :IF, :IDENT, :INTEGER, :PLUS
8
+
9
+ rule(/[ \t\r\n]+/, skip: true)
10
+ rule(/==/) { emit :EQ }
11
+ rule(/=/) { emit :ASSIGN }
12
+ rule(/if/) { emit :IF }
13
+ rule(/[a-z_][a-z0-9_]*/) { emit :IDENT }
14
+ rule(/[0-9]+/) { emit :INTEGER, text.to_i }
15
+ rule(/\+/) { emit :PLUS }
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # JSON lexer example
2
+
3
+ ## What this example demonstrates
4
+
5
+ - a complete JSON token set with value conversion;
6
+ - the stable `direct` backend; and
7
+ - runtime/generated equivalence for a small practical lexer.
8
+
9
+ ## Run in runtime mode
10
+
11
+ ```sh
12
+ ruby -Ilib -e 'require "json"; load "examples/json/lexer.flexr.rb"; puts JSON.generate(JsonExample::Lexer.new(%q({"answer":42})).tokens)'
13
+ ```
14
+
15
+ Expected output:
16
+
17
+ ```json
18
+ [["LBRACE","{"],["STRING","answer"],["COLON",":"],["NUMBER",42.0],["RBRACE","}"]]
19
+ ```
20
+
21
+ ## Validate and generate
22
+
23
+ ```sh
24
+ flexr check examples/json/lexer.flexr.rb --format json
25
+ flexr examples/json/lexer.flexr.rb -o /tmp/json_lexer.rb
26
+ ruby -Ilib -e 'require "json"; load ARGV.fetch(0); puts JSON.generate(JsonExample::Lexer.new(%q({"answer":42})).tokens)' /tmp/json_lexer.rb
27
+ ```
28
+
29
+ The generated output should produce the same JSON token array. Use
30
+ `--standalone` when the runtime artifact must not require the flexr gem.
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flexr"
4
+
5
+ module JsonExample
6
+ class Lexer < Flexr::Lexer
7
+ backend :direct
8
+ emits :LBRACE, :RBRACE, :LBRACKET, :RBRACKET, :COLON, :COMMA,
9
+ :STRING, :NUMBER, :TRUE, :FALSE, :NULL
10
+
11
+ rule(/[ \t\r\n]+/, skip: true)
12
+ rule(/\{/) { emit :LBRACE }
13
+ rule(/\}/) { emit :RBRACE }
14
+ rule(/\[/) { emit :LBRACKET }
15
+ rule(/\]/) { emit :RBRACKET }
16
+ rule(/:/) { emit :COLON }
17
+ rule(/,/) { emit :COMMA }
18
+ rule(/true/) { emit :TRUE, true }
19
+ rule(/false/) { emit :FALSE, false }
20
+ rule(/null/) { emit :NULL, nil }
21
+ rule(/-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/) { emit :NUMBER, text.to_f }
22
+ rule(/"(?:\\.|[^"\\])*"/) { emit :STRING, text.byteslice(1...-1) }
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ # Ruby subset lexer
2
+
3
+ ## What this example demonstrates
4
+
5
+ The specification contains constants, a `%w` array, a heredoc inside an action,
6
+ and ordinary Ruby interpolation. The generator removes only DSL calls and
7
+ preserves the surrounding Ruby.
8
+
9
+ ## Run and validate
10
+
11
+ ```sh
12
+ ruby -Ilib -e 'require "json"; load "examples/ruby_subset/lexer.flexr.rb"; puts JSON.generate(RubySubset::Lexer.new(%q(class Foo "ok" end)).tokens)'
13
+ flexr check examples/ruby_subset/lexer.flexr.rb --format json
14
+ flexr examples/ruby_subset/lexer.flexr.rb -o /tmp/ruby_subset_lexer.rb
15
+ ```
16
+
17
+ Inspect the generated file to see that the constants and action body remain.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flexr"
4
+
5
+ module RubySubset
6
+ class Lexer < Flexr::Lexer
7
+ KEYWORDS = %w[class def end].freeze
8
+ IDENT = /[A-Za-z_][A-Za-z_0-9]*/
9
+
10
+ emits :KEYWORD, :IDENT, :STRING
11
+
12
+ rule(/[ \t\r\n]+/, skip: true)
13
+ rule(KEYWORDS) { emit :KEYWORD, text.to_sym }
14
+ rule(IDENT) { emit :IDENT, "#{text}:identifier" }
15
+ rule(/"(?:\\.|[^"\\])*"/) do
16
+ value = <<~TOKEN
17
+ #{text[1...-1]}
18
+ TOKEN
19
+ emit :STRING, value.chomp
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # Toy language lexer
2
+
3
+ ## What this example demonstrates
4
+
5
+ This lexer keeps `DIGIT` and `IDENT` as ordinary Ruby constants and interpolates
6
+ them into regexp literals. It therefore exercises static constant resolution
7
+ without introducing a second lexer language.
8
+
9
+ ## Run and validate
10
+
11
+ ```sh
12
+ ruby -Ilib -e 'require "json"; load "examples/toy_lang/lexer.flexr.rb"; puts JSON.generate(ToyLang::Lexer.new("answer + 12").tokens)'
13
+ flexr check examples/toy_lang/lexer.flexr.rb --format json
14
+ flexr examples/toy_lang/lexer.flexr.rb -o /tmp/toy_lang_lexer.rb
15
+ ```
16
+
17
+ The generated file preserves the constants and produces the same token stream.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flexr"
4
+
5
+ module ToyLang
6
+ class Lexer < Flexr::Lexer
7
+ DIGIT = /[0-9]/
8
+ IDENT = /[A-Za-z_]/
9
+
10
+ emits :INT, :IDENT, :PLUS, :STRING
11
+
12
+ rule(/[ \t\r\n]+/, skip: true)
13
+ rule(/#{DIGIT}+/) { emit :INT, text.to_i }
14
+ rule(/#{IDENT}[A-Za-z_0-9]*/) { emit :IDENT, text }
15
+ rule(/\+/) { emit :PLUS }
16
+ rule(/"(?:\\.|[^"\\])*"/) { emit :STRING, text[1...-1] }
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # flexr with Lrama
2
+
3
+ ## What this example demonstrates
4
+
5
+ Lrama consumes the same `racc_next_token` protocol as Racc. The lexer declares
6
+ grammar-facing names with `emits`, and the dependency on Lrama remains optional.
7
+
8
+ ## Run and validate
9
+
10
+ ```sh
11
+ ruby -Ilib -e 'load "examples/with_lrama/lexer.flexr.rb"; lexer = WithLrama::LramaLexer.new("12 - 3"); p lexer.tokens'
12
+ flexr tokens examples/with_lrama/lexer.flexr.rb
13
+ flexr check examples/with_lrama/lexer.flexr.rb --format json
14
+ ```
15
+
16
+ See the [Lrama integration guide](../../docs/how-to/integrate-with-lrama.md)
17
+ for the EOF protocol and build flow.