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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 54c55f186a1f4aa048a7a46ca86cae52eb5f9566888fc9cdc09f527d4b6b341f
|
|
4
|
+
data.tar.gz: 7a5e9329e688889cfe665f3a4499deea7ca2e9de684f4fd5ad10847bd631fb4e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7399ef9f672ff36623a8d54a6eed882ee43fed2c7e9d59b417038bd0d8a9a32b892a8556774531aceb41b0e1c3729a169a003b190b8f2a330c4b825960da32b2
|
|
7
|
+
data.tar.gz: 4ecdca40a21a0ff5af1146f20912297c96542bd2ec66412f3726461472c45e10ebc7e6d81a17ef942f3d0bfa036c8701f123fc15be72408dd62c5673c5636154
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
4
|
+
Exclude:
|
|
5
|
+
- "lib/flexr/codegen/**/*"
|
|
6
|
+
- "lib/flexr/unicode/data/**/*"
|
|
7
|
+
- "lib/flexr/regexp/tokenizer.rb"
|
|
8
|
+
- "spec/fixtures/generated.rb"
|
|
9
|
+
- "vendor/**/*"
|
|
10
|
+
|
|
11
|
+
Metrics/MethodLength:
|
|
12
|
+
Max: 80
|
|
13
|
+
|
|
14
|
+
Metrics/ClassLength:
|
|
15
|
+
Max: 250
|
|
16
|
+
|
|
17
|
+
Style/Documentation:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
Style/StringLiterals:
|
|
21
|
+
Enabled: false
|
|
22
|
+
|
|
23
|
+
Metrics:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
Layout:
|
|
27
|
+
Enabled: false
|
|
28
|
+
|
|
29
|
+
Naming/MethodParameterName:
|
|
30
|
+
Enabled: false
|
|
31
|
+
|
|
32
|
+
Lint/ScriptPermission:
|
|
33
|
+
Enabled: false
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bundle install
|
|
7
|
+
bundle exec rake test
|
|
8
|
+
bundle exec rubocop
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The runtime supports Ruby 3.1 and newer. Generator changes need Ruby 3.3 or
|
|
12
|
+
newer because Prism is used for source analysis.
|
|
13
|
+
|
|
14
|
+
## Validation layers
|
|
15
|
+
|
|
16
|
+
Run the focused test first, then the relevant contract checks:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bundle exec rspec spec/cli_spec.rb
|
|
20
|
+
bundle exec rake docs:verify
|
|
21
|
+
bundle exec rake modes:equivalence generated:verify golden:verify
|
|
22
|
+
bundle exec rake test:differential fuzz
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The full CI workflow also checks Unicode invariants, acceleration equivalence,
|
|
26
|
+
generated-only loading, Graphviz output, coverage, and benchmark regressions.
|
|
27
|
+
Do not update a generated golden file to hide a semantic change; inspect the
|
|
28
|
+
generated diff and update the compatibility documentation when the change is
|
|
29
|
+
intentional.
|
|
30
|
+
|
|
31
|
+
## Documentation changes
|
|
32
|
+
|
|
33
|
+
README is an entry point. Put task-oriented instructions in `docs/how-to/`,
|
|
34
|
+
exact contracts in `docs/reference/`, and design rationale in
|
|
35
|
+
`docs/explanation/`. Keep executable specifications in `examples/` and link to
|
|
36
|
+
them instead of duplicating large source blocks. `rake docs:verify` checks local
|
|
37
|
+
links, the tutorial example, CLI help, and stable DSL coverage.
|
|
38
|
+
|
|
39
|
+
All documentation in this repository is written in English.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# flexr
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ydah/flexr/actions/workflows/main.yml)
|
|
4
|
+
[](LICENSE.txt)
|
|
5
|
+
|
|
6
|
+
[Product site](https://ydah.github.io/flexr/) · [Playground](https://ydah.github.io/flexr/playground/) · [Documentation](docs/README.md)
|
|
7
|
+
|
|
8
|
+
Ruby-native lexer generator for parser authors who want ordinary Ruby
|
|
9
|
+
specifications and deterministic generated output.
|
|
10
|
+
|
|
11
|
+
Write one specification, run it directly while developing, or generate Ruby
|
|
12
|
+
for deployment when startup cost and build reproducibility matter. Both modes
|
|
13
|
+
use the same rules and actions.
|
|
14
|
+
|
|
15
|
+
## Why flexr?
|
|
16
|
+
|
|
17
|
+
- Ordinary Ruby DSL; no separate lexer language is required.
|
|
18
|
+
- Leftmost-longest matching: the longest rule wins, and source order breaks
|
|
19
|
+
equal-length ties.
|
|
20
|
+
- Runtime/generated parity with diagnostics for unsupported or risky designs.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
gem install flexr
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# lexer.flexr.rb
|
|
30
|
+
require "flexr"
|
|
31
|
+
|
|
32
|
+
class Lexer < Flexr::Lexer
|
|
33
|
+
emits :INTEGER, :PLUS
|
|
34
|
+
|
|
35
|
+
rule(/[ \t\r\n]+/, skip: true)
|
|
36
|
+
rule(/[0-9]+/) { emit :INTEGER, text.to_i }
|
|
37
|
+
rule(/\+/) { emit :PLUS }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Lexer.new("12 + 3").tokens
|
|
41
|
+
# => [[:INTEGER, 12], [:PLUS, "+"], [:INTEGER, 3]]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Follow the [calculator tutorial](docs/tutorial/build-a-calculator-lexer.md)
|
|
45
|
+
for validation, generation, and runtime/generated comparison.
|
|
46
|
+
|
|
47
|
+
## How matching works
|
|
48
|
+
|
|
49
|
+
At each input position flexr considers every active rule and chooses the
|
|
50
|
+
longest match. If multiple rules consume the same number of bytes, the rule
|
|
51
|
+
defined first wins. A rule can use `followed_by:` to inspect trailing context
|
|
52
|
+
without consuming it.
|
|
53
|
+
|
|
54
|
+
The regexp engine is a DFA-oriented subset of Ruby regexp syntax. See the
|
|
55
|
+
[regexp reference](docs/reference/regexp.md) before relying on look-around,
|
|
56
|
+
backreferences, or other non-regular constructs.
|
|
57
|
+
|
|
58
|
+
## Runtime or generated?
|
|
59
|
+
|
|
60
|
+
| Mode | Build requirement | Runtime requirement | Best for |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| Runtime | `flexr` gem | `flexr` gem | Development, tests, and dynamic Ruby specs |
|
|
63
|
+
| Generated | `flexr` plus Prism on Ruby 3.3+ | `flexr` gem | Reproducible deployment artifacts |
|
|
64
|
+
| Standalone generated | `flexr` plus Prism on Ruby 3.3+ | Generated file and Ruby standard library | Distribution without the gem |
|
|
65
|
+
|
|
66
|
+
Static generation is the default. Use `--eval` only for trusted specifications;
|
|
67
|
+
it executes the specification during the build. See the
|
|
68
|
+
[generation guide](docs/how-to/generate-a-lexer.md) and
|
|
69
|
+
[standalone deployment guide](docs/how-to/deploy-a-standalone-lexer.md).
|
|
70
|
+
|
|
71
|
+
## Is flexr right for you?
|
|
72
|
+
|
|
73
|
+
flexr fits projects that want a Ruby-native lexer, deterministic longest-match
|
|
74
|
+
semantics, Unicode-aware byte-level matching, and parser integration. It is not
|
|
75
|
+
a drop-in replacement for a first-match lexer, and it does not accept regexp
|
|
76
|
+
features that require backtracking or capture-dependent matching.
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
Start with the [documentation map](docs/README.md), then choose the path that
|
|
81
|
+
matches your task:
|
|
82
|
+
|
|
83
|
+
- [Tutorial](docs/tutorial/build-a-calculator-lexer.md) — build one lexer from
|
|
84
|
+
source to generated artifact.
|
|
85
|
+
- [How-to guides](docs/how-to/) — solve one focused integration or runtime
|
|
86
|
+
problem.
|
|
87
|
+
- [Reference](docs/reference/README.md) — look up APIs, CLI options, regexp
|
|
88
|
+
support, diagnostics, and compatibility.
|
|
89
|
+
- [Explanation](docs/explanation/) — understand matching, backends, Unicode,
|
|
90
|
+
generation, and security decisions.
|
|
91
|
+
- [Examples](examples/) — executable specifications and parser integrations.
|
|
92
|
+
|
|
93
|
+
## Compatibility and stability
|
|
94
|
+
|
|
95
|
+
Runtime Ruby support starts at 3.1. The generator requires Ruby 3.3 or newer
|
|
96
|
+
because it uses Prism. Stable and experimental APIs are listed in the
|
|
97
|
+
[public API contract](docs/reference/public-api.md). The vendored Unicode
|
|
98
|
+
snapshot and generated-artifact policy are described in the
|
|
99
|
+
[compatibility reference](docs/reference/compatibility.md).
|
|
100
|
+
|
|
101
|
+
## Security
|
|
102
|
+
|
|
103
|
+
Lexer actions are Ruby code and remain Ruby code in generated output. Static
|
|
104
|
+
generation parses the specification, while `--eval` executes it. Treat source
|
|
105
|
+
specifications and generated files as trusted build inputs; never process an
|
|
106
|
+
untrusted specification with `--eval`.
|
|
107
|
+
|
|
108
|
+
## Contributing
|
|
109
|
+
|
|
110
|
+
Run `bundle exec rake docs:verify` together with the normal test and generated
|
|
111
|
+
artifact checks before submitting changes. See
|
|
112
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) and [RELEASING.md](docs/RELEASING.md).
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "json"
|
|
7
|
+
require "open3"
|
|
8
|
+
require "rbconfig"
|
|
9
|
+
require "rspec/core/rake_task"
|
|
10
|
+
require "tmpdir"
|
|
11
|
+
require "flexr"
|
|
12
|
+
require_relative "tools/regexp_tokenizer_reference"
|
|
13
|
+
|
|
14
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
15
|
+
task test: :spec
|
|
16
|
+
|
|
17
|
+
module FlexrVerification
|
|
18
|
+
ROOT = File.expand_path(__dir__)
|
|
19
|
+
EXAMPLES = Dir[File.join(ROOT, "examples/**/*.flexr.rb")].freeze
|
|
20
|
+
TOKENIZER_SPEC = File.join(ROOT, "lib/flexr/regexp/tokenizer.flexr.rb").freeze
|
|
21
|
+
TOKENIZER_GENERATED = File.join(ROOT, "lib/flexr/regexp/tokenizer.rb").freeze
|
|
22
|
+
VERIFICATION_SPECS = (EXAMPLES + [TOKENIZER_SPEC]).freeze
|
|
23
|
+
EXPECTED_INPUTS = {
|
|
24
|
+
%r{/examples/calculator/} => "if ifx == = 12 + 3",
|
|
25
|
+
%r{/examples/json/} => '{"answer": 42}',
|
|
26
|
+
%r{/examples/toy_lang/} => "answer + 12",
|
|
27
|
+
%r{/examples/ruby_subset/} => 'class Foo "ok" end',
|
|
28
|
+
%r{/examples/with_racc/} => "12 + 3",
|
|
29
|
+
%r{/examples/with_lrama/} => "12 - 3",
|
|
30
|
+
%r{/lib/flexr/regexp/tokenizer\.flexr\.rb\z} => "a|[a-z]+\\p{L}?"
|
|
31
|
+
}.freeze
|
|
32
|
+
TOKENIZER_REFERENCE_INPUTS = [
|
|
33
|
+
"a|[a-z]+\\p{L}?",
|
|
34
|
+
"あa",
|
|
35
|
+
"aあ",
|
|
36
|
+
"é|あ",
|
|
37
|
+
"\\xffa".b
|
|
38
|
+
].freeze
|
|
39
|
+
RUNTIME_CLASS_NAMES = {
|
|
40
|
+
%r{/examples/calculator/} => "CalculatorExample::Lexer",
|
|
41
|
+
%r{/examples/json/} => "JsonExample::Lexer",
|
|
42
|
+
%r{/examples/toy_lang/} => "ToyLang::Lexer",
|
|
43
|
+
%r{/examples/ruby_subset/} => "RubySubset::Lexer",
|
|
44
|
+
%r{/examples/with_racc/} => "WithRacc::RaccLexer",
|
|
45
|
+
%r{/examples/with_lrama/} => "WithLrama::LramaLexer",
|
|
46
|
+
%r{/lib/flexr/regexp/tokenizer\.flexr\.rb\z} => "Flexr::Regexp::SourceLexer"
|
|
47
|
+
}.freeze
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
def input_for(spec)
|
|
51
|
+
normalized = normalized_path(spec)
|
|
52
|
+
EXPECTED_INPUTS.find { |pattern, _| normalized.match?(pattern) }&.last || raise("no verification input for #{spec}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def golden_path(spec)
|
|
56
|
+
name = "#{File.basename(File.dirname(spec))}_#{File.basename(spec, '.flexr.rb')}.sha256"
|
|
57
|
+
File.join(ROOT, "benchmark/golden", name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def generated_source(spec)
|
|
61
|
+
Flexr::Generator.new(relative_spec(spec)).generate
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def relative_spec(spec)
|
|
65
|
+
normalized_spec = normalized_path(spec)
|
|
66
|
+
normalized_root = normalized_path(ROOT)
|
|
67
|
+
relative = normalized_spec.delete_prefix("#{normalized_root}/")
|
|
68
|
+
relative == normalized_spec ? normalized_spec.sub(%r{\A.*?(?=(?:examples|lib/flexr)/)}, "") : relative
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalized_path(path)
|
|
72
|
+
path.to_s.tr("\\", "/")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def normalized_source(source)
|
|
76
|
+
source.gsub(/\r\n?/, "\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def random_unicode_string(random, max_codepoints: 8)
|
|
80
|
+
codepoints = Array.new(random.rand(max_codepoints + 1)) do
|
|
81
|
+
loop do
|
|
82
|
+
codepoint = random.rand(0x11_0000)
|
|
83
|
+
break codepoint unless codepoint.between?(0xd800, 0xdfff)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
codepoints.pack("U*")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def load_runtime(spec)
|
|
90
|
+
class_name = runtime_class_name(spec)
|
|
91
|
+
existing = constantize(class_name)
|
|
92
|
+
return existing if runtime_lexer?(existing)
|
|
93
|
+
remove_constant(class_name) if existing
|
|
94
|
+
|
|
95
|
+
before = ObjectSpace.each_object(Class).to_a
|
|
96
|
+
load spec
|
|
97
|
+
loaded = (ObjectSpace.each_object(Class).to_a - before).find { |klass| runtime_lexer?(klass) }
|
|
98
|
+
return loaded if loaded
|
|
99
|
+
|
|
100
|
+
resolved = constantize(class_name)
|
|
101
|
+
return resolved if runtime_lexer?(resolved)
|
|
102
|
+
|
|
103
|
+
raise "no lexer class loaded from #{spec}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def runtime_class_name(spec)
|
|
107
|
+
normalized = normalized_path(spec)
|
|
108
|
+
RUNTIME_CLASS_NAMES.find { |pattern, _class_name| normalized.match?(pattern) }&.last
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def constantize(class_name)
|
|
112
|
+
return unless class_name
|
|
113
|
+
|
|
114
|
+
class_name.split("::").reject(&:empty?).reduce(Object) { |parent, name| parent.const_get(name) }
|
|
115
|
+
rescue NameError
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def remove_constant(class_name)
|
|
120
|
+
return unless class_name
|
|
121
|
+
|
|
122
|
+
parts = class_name.split("::").reject(&:empty?)
|
|
123
|
+
parent = constantize(parts[0...-1].join("::"))
|
|
124
|
+
parent.send(:remove_const, parts.last) if parent&.const_defined?(parts.last, false)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def runtime_lexer?(klass)
|
|
128
|
+
klass.is_a?(Class) && klass != Flexr::Lexer && klass.respond_to?(:__flexr_spec) &&
|
|
129
|
+
(!klass.respond_to?(:__flexr_generated?) || !klass.__flexr_generated?)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def verify_acceleration(spec)
|
|
133
|
+
lexer = load_runtime(spec)
|
|
134
|
+
random = Random.new(17)
|
|
135
|
+
inputs = [input_for(spec)] + Array.new(32) do
|
|
136
|
+
Array.new(random.rand(48..96)) { random.rand(32..126) }.pack("C*")
|
|
137
|
+
end
|
|
138
|
+
options = lexer.__flexr_config.options
|
|
139
|
+
original_accel = options.fetch(:accel, :auto)
|
|
140
|
+
accelerated = inputs.map { |input| lexer.new(input, error_mode: :panic).tokens }
|
|
141
|
+
options[:accel] = :none
|
|
142
|
+
reference = inputs.map { |input| lexer.new(input, error_mode: :panic).tokens }
|
|
143
|
+
raise "acceleration token mismatch in #{spec}" unless accelerated == reference
|
|
144
|
+
|
|
145
|
+
lexer.compile!.machines.each_value do |machine|
|
|
146
|
+
Flexr::Automaton::Accel.extract(machine.dfa).each do |region|
|
|
147
|
+
256.times do |byte|
|
|
148
|
+
expected = region.bytes.include?(byte)
|
|
149
|
+
actual = region.regexp.match?(byte.chr(Encoding::BINARY))
|
|
150
|
+
next if expected == actual
|
|
151
|
+
|
|
152
|
+
raise "acceleration mismatch in #{spec}: state=#{region.state}, byte=#{byte}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
ensure
|
|
157
|
+
options[:accel] = original_accel if options && original_accel
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def verify_dogfood(spec)
|
|
161
|
+
generated_path = File.join(Dir.tmpdir, "flexr-dogfood-#{Process.pid}-#{File.basename(spec)}")
|
|
162
|
+
source = generated_source(spec)
|
|
163
|
+
File.binwrite(generated_path, source)
|
|
164
|
+
_, stderr, status = Open3.capture3(RbConfig.ruby, "-c", generated_path)
|
|
165
|
+
raise "dogfood syntax check failed for #{spec}: #{stderr}" unless status.success?
|
|
166
|
+
raise "generated lexer missing compiled payload: #{spec}" unless source.include?("install_compiled!")
|
|
167
|
+
raise "generated lexer contains an unfinished TODO: #{spec}" if source.include?("FLEXR-TODO")
|
|
168
|
+
|
|
169
|
+
script = <<~RUBY
|
|
170
|
+
require "json"
|
|
171
|
+
load ARGV.fetch(0)
|
|
172
|
+
lexer = ObjectSpace.each_object(Class).find { |klass| klass.respond_to?(:__flexr_spec) && klass != Flexr::Lexer }
|
|
173
|
+
abort "no generated lexer" unless lexer
|
|
174
|
+
puts JSON.generate(lexer.new(ARGV.fetch(1)).tokens)
|
|
175
|
+
RUBY
|
|
176
|
+
runtime_output, runtime_error, runtime_status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", script,
|
|
177
|
+
spec, input_for(spec))
|
|
178
|
+
generated_output, generated_error, generated_status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", script,
|
|
179
|
+
generated_path, input_for(spec))
|
|
180
|
+
raise "dogfood token mismatch for #{spec}: #{runtime_error}#{generated_error}" unless
|
|
181
|
+
runtime_status.success? && generated_status.success? && runtime_output == generated_output
|
|
182
|
+
|
|
183
|
+
return unless spec == TOKENIZER_SPEC
|
|
184
|
+
|
|
185
|
+
reference_script = <<~RUBY
|
|
186
|
+
require "json"
|
|
187
|
+
require "regexp_tokenizer_reference"
|
|
188
|
+
puts JSON.generate(FlexrVerification::RegexpTokenizerReference.tokens(ARGV.fetch(0)))
|
|
189
|
+
RUBY
|
|
190
|
+
reference_output, reference_error, reference_status = Open3.capture3(
|
|
191
|
+
RbConfig.ruby, "-Itools", "-e", reference_script, input_for(spec)
|
|
192
|
+
)
|
|
193
|
+
raise "tokenizer reference failed: #{reference_error}" unless reference_status.success?
|
|
194
|
+
raise "tokenizer reference mismatch for #{spec}" unless runtime_output == reference_output
|
|
195
|
+
verify_tokenizer_reference
|
|
196
|
+
ensure
|
|
197
|
+
FileUtils.rm_f(generated_path) if generated_path
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def verify_tokenizer_reference
|
|
201
|
+
runtime_lexer = load_runtime(TOKENIZER_SPEC)
|
|
202
|
+
generated_path = File.join(Dir.tmpdir, "flexr-tokenizer-reference-#{Process.pid}.rb")
|
|
203
|
+
remove_constant("Flexr::Regexp::SourceLexer")
|
|
204
|
+
File.binwrite(generated_path, generated_source(TOKENIZER_SPEC))
|
|
205
|
+
load generated_path
|
|
206
|
+
generated_lexer = Flexr::Regexp.const_get(:SourceLexer, false)
|
|
207
|
+
|
|
208
|
+
TOKENIZER_REFERENCE_INPUTS.each do |input|
|
|
209
|
+
expected = RegexpTokenizerReference.tokens(input)
|
|
210
|
+
runtime = runtime_lexer.new(input).tokens
|
|
211
|
+
generated = generated_lexer.new(input).tokens
|
|
212
|
+
next if runtime == expected && generated == expected
|
|
213
|
+
|
|
214
|
+
raise "tokenizer reference mismatch for #{input.inspect}: " \
|
|
215
|
+
"runtime=#{runtime.inspect}, generated=#{generated.inspect}, reference=#{expected.inspect}"
|
|
216
|
+
end
|
|
217
|
+
ensure
|
|
218
|
+
FileUtils.rm_f(generated_path) if generated_path
|
|
219
|
+
remove_constant("Flexr::Regexp::SourceLexer")
|
|
220
|
+
Flexr::Regexp.const_set(:SourceLexer, runtime_lexer) if runtime_lexer
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
namespace :modes do
|
|
226
|
+
task :equivalence do
|
|
227
|
+
FlexrVerification::VERIFICATION_SPECS.each do |spec|
|
|
228
|
+
generated_path = File.join(Dir.tmpdir, "flexr-mode-#{Process.pid}-#{File.basename(spec)}")
|
|
229
|
+
File.binwrite(generated_path, FlexrVerification.generated_source(spec))
|
|
230
|
+
script = <<~RUBY
|
|
231
|
+
load ARGV.fetch(0)
|
|
232
|
+
lexer = ObjectSpace.each_object(Class).find { |klass| klass.respond_to?(:__flexr_spec) && klass != Flexr::Lexer }
|
|
233
|
+
abort "no lexer found" unless lexer
|
|
234
|
+
p lexer.new(ARGV.fetch(1)).tokens
|
|
235
|
+
RUBY
|
|
236
|
+
runtime_output, runtime_error, runtime_status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", script,
|
|
237
|
+
spec, FlexrVerification.input_for(spec))
|
|
238
|
+
generated_output, generated_error, generated_status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", script,
|
|
239
|
+
generated_path, FlexrVerification.input_for(spec))
|
|
240
|
+
abort "mode mismatch: #{spec}\n#{runtime_error}#{generated_error}" unless runtime_status.success? && generated_status.success? && runtime_output == generated_output
|
|
241
|
+
ensure
|
|
242
|
+
FileUtils.rm_f(generated_path) if generated_path
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
task "golden:verify" do
|
|
248
|
+
FlexrVerification::VERIFICATION_SPECS.each do |spec|
|
|
249
|
+
golden = FlexrVerification.golden_path(spec)
|
|
250
|
+
abort "missing golden file: #{golden}" unless File.file?(golden)
|
|
251
|
+
|
|
252
|
+
expected = File.read(golden).strip
|
|
253
|
+
actual = Digest::SHA256.hexdigest(FlexrVerification.generated_source(spec))
|
|
254
|
+
abort "golden mismatch: #{spec} (expected #{expected}, got #{actual})" unless expected == actual
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
task "accel:equivalence" do
|
|
259
|
+
FlexrVerification::VERIFICATION_SPECS.each { |spec| FlexrVerification.verify_acceleration(spec) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
task "dogfood:verify" do
|
|
263
|
+
FlexrVerification::VERIFICATION_SPECS.each { |spec| FlexrVerification.verify_dogfood(spec) }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
task "generated:verify" do
|
|
267
|
+
abort "missing committed tokenizer generated file: #{FlexrVerification::TOKENIZER_GENERATED}" unless
|
|
268
|
+
File.file?(FlexrVerification::TOKENIZER_GENERATED)
|
|
269
|
+
|
|
270
|
+
expected = FlexrVerification.normalized_source(File.binread(FlexrVerification::TOKENIZER_GENERATED))
|
|
271
|
+
actual = FlexrVerification.normalized_source(FlexrVerification.generated_source(FlexrVerification::TOKENIZER_SPEC))
|
|
272
|
+
abort "committed tokenizer generated file is stale" unless expected == actual
|
|
273
|
+
|
|
274
|
+
puts "generated: committed tokenizer is reproducible"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
task "dot:verify" do
|
|
278
|
+
spec = File.join(FlexrVerification::ROOT, "examples/json/lexer.flexr.rb")
|
|
279
|
+
dot_source, dot_error, dot_status = Open3.capture3(
|
|
280
|
+
RbConfig.ruby, "-Ilib", "exe/flexr", "dot", spec, chdir: FlexrVerification::ROOT
|
|
281
|
+
)
|
|
282
|
+
abort "flexr dot failed: #{dot_error}" unless dot_status.success?
|
|
283
|
+
|
|
284
|
+
svg, svg_error, svg_status = Open3.capture3("dot", "-Tsvg", stdin_data: dot_source)
|
|
285
|
+
abort "dot -Tsvg failed: #{svg_error}" unless svg_status.success? && svg.include?("<svg")
|
|
286
|
+
|
|
287
|
+
puts "dot: parsed #{spec} as SVG"
|
|
288
|
+
rescue Errno::ENOENT => e
|
|
289
|
+
abort "dot executable is required for dot:verify: #{e.message}"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
task "direct:verify" do
|
|
293
|
+
unless defined?(RubyVM::InstructionSequence)
|
|
294
|
+
puts "direct: disassembly unavailable on #{RUBY_ENGINE}; skipped"
|
|
295
|
+
next
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
spec = File.join(FlexrVerification::ROOT, "examples/json/lexer.flexr.rb")
|
|
299
|
+
disassembly = RubyVM::InstructionSequence.compile(FlexrVerification.generated_source(spec)).disasm
|
|
300
|
+
abort "direct dispatch did not compile to opt_case_dispatch" unless disassembly.include?("opt_case_dispatch")
|
|
301
|
+
|
|
302
|
+
puts "direct: opt_case_dispatch present"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
task "unicode:verify" do
|
|
306
|
+
splitter = Flexr::Unicode::Utf8Splitter
|
|
307
|
+
properties = Flexr::Unicode::Data::PROPERTIES
|
|
308
|
+
abort "unexpected vendored Unicode version" unless Flexr::Unicode::VERSION == "15.1.0"
|
|
309
|
+
properties.each do |name, ranges|
|
|
310
|
+
previous = -1
|
|
311
|
+
ranges.each do |lo, hi|
|
|
312
|
+
abort "invalid #{name} Unicode range #{lo.inspect}..#{hi.inspect}" unless
|
|
313
|
+
lo.is_a?(Integer) && hi.is_a?(Integer) && lo <= hi && lo > previous && hi <= 0x10_ffff
|
|
314
|
+
|
|
315
|
+
previous = hi
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
scalar_count = 0
|
|
319
|
+
(0..0x10_ffff).each do |codepoint|
|
|
320
|
+
next if codepoint.between?(0xd800, 0xdfff)
|
|
321
|
+
|
|
322
|
+
expected = [codepoint].pack("U").bytes.map { |byte| [byte, byte] }
|
|
323
|
+
actual = splitter.split(codepoint, codepoint)
|
|
324
|
+
abort "Unicode singleton mismatch at U+#{codepoint.to_s(16)}" unless actual == [expected]
|
|
325
|
+
scalar_count += 1
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
random = Random.new(0xF1E2)
|
|
329
|
+
100_000.times do
|
|
330
|
+
loop do
|
|
331
|
+
lo = random.rand(0x11_0000)
|
|
332
|
+
hi = [lo + random.rand(17), 0x10_ffff].min
|
|
333
|
+
next if lo <= 0xdfff && hi >= 0xd800
|
|
334
|
+
|
|
335
|
+
sequences = splitter.split(lo, hi)
|
|
336
|
+
abort "Unicode range split was empty for U+#{lo.to_s(16)}..U+#{hi.to_s(16)}" if sequences.empty?
|
|
337
|
+
(lo..hi).each do |codepoint|
|
|
338
|
+
bytes = [codepoint].pack("U").bytes
|
|
339
|
+
included = sequences.any? do |sequence|
|
|
340
|
+
sequence.length == bytes.length && sequence.zip(bytes).all? { |range, byte| byte.between?(*range) }
|
|
341
|
+
end
|
|
342
|
+
abort "Unicode range omitted U+#{codepoint.to_s(16)}" unless included
|
|
343
|
+
end
|
|
344
|
+
break
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
puts "unicode: UCD #{Flexr::Unicode::VERSION}, #{properties.length} properties, " \
|
|
348
|
+
"#{scalar_count} singleton and 100000 random range cases passed"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
task "bench:regression" do
|
|
352
|
+
baseline = ENV.fetch("FLEXR_BENCHMARK_BASELINE", File.join(FlexrVerification::ROOT, "benchmark/baselines/json.json"))
|
|
353
|
+
command = [RbConfig.ruby, "-Ilib", "benchmark/run.rb", "--baseline", baseline, "--json"]
|
|
354
|
+
stdout, stderr, status = Open3.capture3(*command, chdir: FlexrVerification::ROOT)
|
|
355
|
+
abort "benchmark regression failed (#{status.exitstatus}): #{stderr}#{stdout}" unless status.success?
|
|
356
|
+
|
|
357
|
+
puts stdout
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
namespace :test do
|
|
361
|
+
task :differential do
|
|
362
|
+
patterns = [/[a-z]+/, /a(?:b|c)?/, /[0-9]{1,3}/, /[^\n]+/, /foo/,
|
|
363
|
+
/[[:alpha:]]+/, /[[:alnum:]]+/, /\p{L}+/, /\p{Nd}+/]
|
|
364
|
+
cases = Integer(ENV.fetch("FLEXR_DIFFERENTIAL_CASES", "1000000"), 10)
|
|
365
|
+
random = Random.new(Integer(ENV.fetch("FLEXR_SEED", "17"), 10))
|
|
366
|
+
unicode_inputs = ["", "a", "あ", "é", "ß", "Ω", "١", " ", "aあ", "éΩ", [0x18db8].pack("U")].freeze
|
|
367
|
+
compiled = {}
|
|
368
|
+
cases.times do
|
|
369
|
+
pattern = patterns[random.rand(patterns.length)]
|
|
370
|
+
input = if random.rand(3).zero?
|
|
371
|
+
random.rand(2).zero? ? unicode_inputs.sample(random: random) : FlexrVerification.random_unicode_string(random)
|
|
372
|
+
else
|
|
373
|
+
Array.new(random.rand(10)) { random.rand(32..126) }.pack("C*")
|
|
374
|
+
end
|
|
375
|
+
expected = if Flexr.reference_pattern?(pattern)
|
|
376
|
+
reference = Flexr::Unicode::ReferenceRegexp.compiled(
|
|
377
|
+
pattern, encoding: pattern.encoding, options: pattern.options, unicode: false
|
|
378
|
+
)
|
|
379
|
+
match = reference.match(input, 0)
|
|
380
|
+
if match
|
|
381
|
+
match.begin(0).zero? && match[0].bytesize == input.bytesize
|
|
382
|
+
else
|
|
383
|
+
false
|
|
384
|
+
end
|
|
385
|
+
else
|
|
386
|
+
Regexp.new("\\A(?:#{pattern.source})\\z", pattern.options).match?(input)
|
|
387
|
+
end
|
|
388
|
+
key = [pattern.source, pattern.options]
|
|
389
|
+
actual = (compiled[key] ||= Flexr.compile_pattern(pattern)).accept?(input)
|
|
390
|
+
next if expected == actual
|
|
391
|
+
|
|
392
|
+
abort "differential mismatch: #{pattern.inspect} #{input.inspect} expected=#{expected} actual=#{actual}"
|
|
393
|
+
end
|
|
394
|
+
puts "differential: #{cases} cases passed"
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
task :fuzz do
|
|
399
|
+
cases = Integer(ENV.fetch("FLEXR_FUZZ_CASES", "10000"), 10)
|
|
400
|
+
random = Random.new(Integer(ENV.fetch("FLEXR_SEED", "17"), 10))
|
|
401
|
+
FlexrVerification::VERIFICATION_SPECS.each do |spec|
|
|
402
|
+
runtime_lexer = FlexrVerification.load_runtime(spec)
|
|
403
|
+
parts = runtime_lexer.name.split("::")
|
|
404
|
+
parent = Object
|
|
405
|
+
parts[0...-1].each { |part| parent = parent.const_get(part) }
|
|
406
|
+
parent.send(:remove_const, parts.last) if parent.const_defined?(parts.last, false)
|
|
407
|
+
generated_path = File.join(Dir.tmpdir, "flexr-fuzz-#{Process.pid}-#{File.basename(spec)}")
|
|
408
|
+
before = ObjectSpace.each_object(Class).to_a
|
|
409
|
+
File.binwrite(generated_path, FlexrVerification.generated_source(spec))
|
|
410
|
+
load generated_path
|
|
411
|
+
generated_lexer = (ObjectSpace.each_object(Class).to_a - before).find do |klass|
|
|
412
|
+
klass.respond_to?(:__flexr_spec)
|
|
413
|
+
end
|
|
414
|
+
raise "no generated lexer found for #{spec}" unless generated_lexer
|
|
415
|
+
|
|
416
|
+
cases.times do
|
|
417
|
+
input = case random.rand(4)
|
|
418
|
+
when 0
|
|
419
|
+
Array.new(random.rand(128)) { random.rand(0..127) }.pack("C*").force_encoding(Encoding::UTF_8)
|
|
420
|
+
when 1
|
|
421
|
+
FlexrVerification.random_unicode_string(random, max_codepoints: 32)
|
|
422
|
+
when 2
|
|
423
|
+
Array.new(random.rand(128)) { random.rand(0..255) }.pack("C*").force_encoding(Encoding::UTF_8)
|
|
424
|
+
else
|
|
425
|
+
(FlexrVerification.random_unicode_string(random, max_codepoints: 16) +
|
|
426
|
+
Array.new(random.rand(64)) { random.rand(32..126) }.pack("C*")).force_encoding(Encoding::UTF_8)
|
|
427
|
+
end
|
|
428
|
+
runtime_tokens = runtime_lexer.new(input, error_mode: :panic).tokens
|
|
429
|
+
generated_tokens = generated_lexer.new(input, error_mode: :panic).tokens
|
|
430
|
+
next if runtime_tokens == generated_tokens
|
|
431
|
+
|
|
432
|
+
abort "fuzz mode mismatch: #{spec} input=#{input.inspect} runtime=#{runtime_tokens.inspect} generated=#{generated_tokens.inspect}"
|
|
433
|
+
rescue Flexr::LexError, ArgumentError, EncodingError
|
|
434
|
+
# Invalid input and user-defined error actions are expected fuzz outcomes.
|
|
435
|
+
end
|
|
436
|
+
ensure
|
|
437
|
+
FileUtils.rm_f(generated_path) if generated_path
|
|
438
|
+
end
|
|
439
|
+
puts "fuzz: #{cases} inputs per example passed"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
namespace :examples do
|
|
443
|
+
task :check do
|
|
444
|
+
FlexrVerification::EXAMPLES.each do |spec|
|
|
445
|
+
stdout, stderr, status = Open3.capture3(
|
|
446
|
+
RbConfig.ruby, "-Ilib", "exe/flexr", "check", spec, "--format", "json", chdir: FlexrVerification::ROOT
|
|
447
|
+
)
|
|
448
|
+
abort "example diagnostics failed for #{spec}: #{stderr}#{stdout}" unless status.success?
|
|
449
|
+
diagnostics = JSON.parse(stdout)
|
|
450
|
+
abort "example diagnostics are not empty for #{spec}: #{diagnostics.inspect}" unless diagnostics.empty?
|
|
451
|
+
rescue JSON::ParserError => e
|
|
452
|
+
abort "example diagnostics were not JSON for #{spec}: #{e.message}\n#{stdout}#{stderr}"
|
|
453
|
+
end
|
|
454
|
+
puts "examples: all checks passed"
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
task :coverage do
|
|
459
|
+
sh RbConfig.ruby, "-Ilib", "tools/coverage.rb"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
namespace :docs do
|
|
463
|
+
task :verify do
|
|
464
|
+
ruby "tools/docs_verify.rb"
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
task default: :spec
|