arpaka 0.1.1 → 2.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 +4 -4
- data/CHANGELOG.md +46 -2
- data/README.md +118 -7
- data/Rakefile +15 -1
- data/docs/architecture.dot +30 -0
- data/docs/architecture.md +96 -0
- data/docs/architecture.webp +0 -0
- data/docs/contributes.md +115 -0
- data/docs/usage.md +497 -0
- data/lib/arpaka/cli.rb +5 -50
- data/lib/arpaka/plugins/cli.rb +61 -0
- data/lib/arpaka/plugins/sample.rb +14 -0
- data/lib/arpaka/plugins/sample_runner.rb +21 -0
- data/lib/arpaka/{actions.rb → ruby/actions.rb} +230 -151
- data/lib/arpaka/ruby/class_name_parser.rb +97 -0
- data/lib/arpaka/ruby/cli.rb +64 -0
- data/lib/arpaka/ruby/frontend_emitter.rb +59 -0
- data/lib/arpaka/ruby/generator.rb +41 -0
- data/lib/arpaka/{ruby_grammar.rb → ruby/grammar.rb} +53 -59
- data/lib/arpaka/ruby/grammar_emitter.rb +71 -0
- data/lib/arpaka/ruby/profile.rb +129 -0
- data/lib/arpaka/ruby/profiles/ruby_3_3.rb +800 -0
- data/lib/arpaka/ruby/profiles/ruby_3_4.rb +802 -0
- data/lib/arpaka/ruby/runtime/detailed/ast.rb.erb +92 -0
- data/lib/arpaka/ruby/runtime/detailed/builder.rb.erb +1177 -0
- data/lib/arpaka/{runtime → ruby/runtime}/frontend.rb.erb +17 -4
- data/lib/arpaka/{runtime → ruby/runtime}/lexer.rb.erb +583 -99
- data/lib/arpaka/ruby/runtime/structured.rb.erb +111 -0
- data/lib/arpaka/ruby.rb +23 -0
- data/lib/arpaka.rb +6 -9
- data/lib/lrama/ruby/backend.rb +4 -9
- data/lib/lrama/ruby/parser.rb.erb +35 -9
- data/lib/lrama/ruby/parser_emitter.rb +41 -0
- data/lib/lrama/ruby/table_codec.rb +50 -0
- data/lib/lrama/ruby/version.rb +1 -1
- data/lib/lrama/ruby.rb +1 -0
- data/sig/arpaka.rbs +8 -3
- metadata +46 -13
- data/lib/arpaka/generator.rb +0 -43
- data/lib/arpaka/runtime/ast.rb.erb +0 -35
- data/lib/arpaka/runtime/builder.rb.erb +0 -247
- /data/lib/arpaka/{BSDL → ruby/BSDL} +0 -0
- /data/lib/arpaka/{COPYING → ruby/COPYING} +0 -0
- /data/lib/arpaka/{source.json → ruby/source.json} +0 -0
- /data/lib/arpaka/{upstream_rules.json → ruby/upstream_rules.json} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93edcf594153e47990a52fa6b5645d19af6bea08b8eb1dba90c165737293561b
|
|
4
|
+
data.tar.gz: 7c02b70e4a01d155542c1d9db2302498c1d43b65a073ad9e8363bede2ebfe2f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0b845cc98cca04321ba644e8bb3b4bfe1974a0979faec672590110a63f8b825da0e34244fceb911dcd11027c57938a6dd9ad6e775ceb6826968c34d2bd7e2ab
|
|
7
|
+
data.tar.gz: 6e4b87c52c412c8dff7de7fe727d1ece2b4b831e07036c9e2619d48b4c66e59955d419122996ca491e5cca0eb5a32dd5434982c983ef24e3154da7a2bae01a74
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [2.0.0]
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
- Added optional structured AST output for lexical class, module, instance
|
|
6
|
+
method and singleton method definitions, with source-order nesting and
|
|
7
|
+
separate occurrences for reopened classes and modules.
|
|
8
|
+
- Supported namespaced generated frontend class names through the API and CLI.
|
|
9
|
+
- Improved Ruby AST compatibility for method parameters, local scopes, pattern
|
|
10
|
+
matching, splats, interpolation, control flow and command calls.
|
|
11
|
+
- Added a usage guide and an AST comparison benchmark that separates parse
|
|
12
|
+
success from AST agreement and records unsupported comparisons explicitly.
|
|
13
|
+
Added optional Ractor checks and a `--disable-gc` switch for experiments.
|
|
14
|
+
- Reduced the pinned grammar's generated standalone frontend from about 450 KB
|
|
15
|
+
to about 190 KB by compressing parser tables and omitting unused rule data.
|
|
16
|
+
- Refactored Ruby parser and frontend generation into focused internal
|
|
17
|
+
components without changing the default detailed AST output.
|
|
18
|
+
- Preserved Ruby 3.3 unsupported-rule metadata in generated frontends.
|
|
19
|
+
- Fixed quote-specific escapes, escaped word boundaries and percent-regexp
|
|
20
|
+
delimiter escapes in generated Ruby frontends. Interpolated `%W` and `%I`
|
|
21
|
+
now fail explicitly instead of silently losing their interpolation.
|
|
22
|
+
- Temporarily represent keyword arguments with Hash-shaped AST nodes while
|
|
23
|
+
RubyVM::AbstractSyntaxTree is used as the comparison oracle. This
|
|
24
|
+
representation may later be split into dedicated keyword-argument nodes as
|
|
25
|
+
Arpaka's semantic AST becomes the canonical model.
|
|
26
|
+
|
|
27
|
+
## [1.0.0] - 2026-09-19
|
|
28
|
+
|
|
29
|
+
- Added frontend generation directly from a Ruby `parse.y` file.
|
|
30
|
+
- Added source profiles for the Ruby 3.3 and Ruby 3.4 grammars.
|
|
31
|
+
- Loaded Ruby's canonical token definitions from `defs/id.def` when generating
|
|
32
|
+
frontends from a Ruby source tree.
|
|
33
|
+
- Made generated runtime extensions depend on the selected source profile.
|
|
34
|
+
|
|
35
|
+
## [0.1.1] - 2026-09-15
|
|
36
|
+
|
|
37
|
+
- Changed Ruby source profile mismatches and unknown Action IDs from generation
|
|
38
|
+
errors to warnings.
|
|
39
|
+
|
|
40
|
+
## [0.1.0] - 2026-09-15
|
|
41
|
+
|
|
42
|
+
- Initial release of the `Lrama::Ruby` backend for generating standalone Ruby
|
|
43
|
+
parsers from Lrama grammars.
|
|
44
|
+
- Added `Arpaka` frontend generation from a pinned Ruby source tree, including
|
|
45
|
+
its standalone lexer, AST, builder and Action mappings.
|
|
46
|
+
- Added parser compilation in isolated Ruby Boxes, recognizer generation and
|
|
47
|
+
support for retaining error productions.
|
|
48
|
+
- Added a Ruby action scanner and compatibility coverage using Rails and Ruby
|
|
49
|
+
Trick programs.
|
data/README.md
CHANGED
|
@@ -21,6 +21,13 @@ location and regenerate when updating the grammar or Actions.
|
|
|
21
21
|
The Ruby frontend is experimental and partial. The gem contains generation
|
|
22
22
|
support, not a bundled `parse.y` or a pre-generated Ruby frontend.
|
|
23
23
|
|
|
24
|
+
Successful parsing does not guarantee a complete or correct AST. Known losses
|
|
25
|
+
include block parameters and qualified constant paths. `%W` and `%I` also have
|
|
26
|
+
incorrect Action mappings; their interpolated forms currently raise a lexer
|
|
27
|
+
error instead of returning a flattened AST. Do not rely on these forms for AST
|
|
28
|
+
analysis. The AST comparison task below records agreement separately from parse
|
|
29
|
+
success.
|
|
30
|
+
|
|
24
31
|
## Requirements
|
|
25
32
|
|
|
26
33
|
Ruby 4.0 or later. Start the generator with `RUBY_BOX=1`: Ruby Box isolates the
|
|
@@ -46,6 +53,11 @@ gem "arpaka", git: "https://github.com/katsyoshi/arpaka"
|
|
|
46
53
|
|
|
47
54
|
## Usage
|
|
48
55
|
|
|
56
|
+
For a detailed, task-oriented guide to the `Arpaka.generate` and
|
|
57
|
+
`Arpaka.compile` APIs, the CLI, standalone frontend runtime, limitations, and
|
|
58
|
+
the AST compatibility benchmark, see [docs/usage.md](docs/usage.md). The
|
|
59
|
+
summary below keeps the main README focused on the supported workflow.
|
|
60
|
+
|
|
49
61
|
Write a Yacc-style grammar with Ruby actions, such as
|
|
50
62
|
[examples/calculator.y](examples/calculator.y):
|
|
51
63
|
|
|
@@ -145,10 +157,36 @@ RUBY_BOX=1 bundle exec arpaka generate \
|
|
|
145
157
|
```
|
|
146
158
|
|
|
147
159
|
The output directory must already exist. `--class-name` defaults to
|
|
148
|
-
`RubyParser` and
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
`RubyParser` and accepts a constant name or a namespace path such as
|
|
161
|
+
`MyLibrary::RubyParser`; each component must be a valid Ruby constant name.
|
|
162
|
+
Existing output files are preserved unless `--force` is supplied. A failed
|
|
163
|
+
generation leaves the previous file intact. The output includes its source
|
|
164
|
+
profile, generator version and copyright/license notices.
|
|
165
|
+
|
|
166
|
+
When only `parse.y` is available, Arpaka can replace its `RUBY_TOKEN(NAME)`
|
|
167
|
+
declarations with token names and generate a frontend without the rest of the
|
|
168
|
+
Ruby source tree:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
RUBY_BOX=1 bundle exec arpaka generate \
|
|
172
|
+
--parse-y /path/to/parse.y \
|
|
173
|
+
--class-name MyRubyParser \
|
|
174
|
+
--output lib/my_library/ruby_parser.rb
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This mode embeds Arpaka's own license notices. The `--ruby-source` mode remains
|
|
178
|
+
the compatibility-preserving path when the Ruby source tree is available.
|
|
179
|
+
|
|
180
|
+
The experimental sample plugin demonstrates the plugin command shape. It is
|
|
181
|
+
selected by the first command argument and runs in its own Ruby Box; the host
|
|
182
|
+
process writes the generated source:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
RUBY_BOX=1 bundle exec arpaka sample generate \
|
|
186
|
+
-y /path/to/parser.y \
|
|
187
|
+
-c MyParser \
|
|
188
|
+
-o lib/my_library/parser.rb
|
|
189
|
+
```
|
|
152
190
|
|
|
153
191
|
The consumer only needs the generated file and Ruby 4.0 or later:
|
|
154
192
|
|
|
@@ -164,6 +202,14 @@ internal; callers do not need to supply tokens. Each call has fresh parsing and
|
|
|
164
202
|
local-variable state. No Arpaka/Lrama gem, Ruby Box, grammar files, network access
|
|
165
203
|
or filesystem writes are needed at runtime.
|
|
166
204
|
|
|
205
|
+
For a lightweight tree of lexical definitions, add `--ast-type structured` to
|
|
206
|
+
`arpaka generate`, or pass `ast_type: :structured` to `Arpaka.generate` or
|
|
207
|
+
`Arpaka.compile`. The generated class name stays the same. Its `parse` method
|
|
208
|
+
returns `AST::Source` with a filename and children for class, module,
|
|
209
|
+
instance method, and singleton method declarations. Reopened classes and modules
|
|
210
|
+
remain separate occurrences. See [the usage guide](docs/usage.md#structured-ast-output)
|
|
211
|
+
for the node fields and limitations.
|
|
212
|
+
|
|
167
213
|
For generation from Ruby, `Arpaka.generate` returns that same standalone source.
|
|
168
214
|
To try a frontend in memory, use `Arpaka.compile`; both require `RUBY_BOX=1`:
|
|
169
215
|
|
|
@@ -259,10 +305,10 @@ requires an explicit Ruby source tree. `ruby:check` verifies the source profile,
|
|
|
259
305
|
Action inventory, grammar transformation and generated frontend without editing
|
|
260
306
|
artifacts. `ruby:generate` writes the standalone frontend to `OUTPUT`.
|
|
261
307
|
|
|
262
|
-
`lib/arpaka/actions.rb` holds the Ruby Action mappings;
|
|
263
|
-
`lib/arpaka/upstream_rules.json` records the expected expanded upstream rules.
|
|
308
|
+
`lib/arpaka/ruby/actions.rb` holds the Ruby Action mappings;
|
|
309
|
+
`lib/arpaka/ruby/upstream_rules.json` records the expected expanded upstream rules.
|
|
264
310
|
Update these and the source checksums together when adding support for a changed
|
|
265
|
-
Ruby grammar. Runtime templates live under `lib/arpaka/runtime/`.
|
|
311
|
+
Ruby grammar. Runtime templates live under `lib/arpaka/ruby/runtime/`.
|
|
266
312
|
|
|
267
313
|
Tests exercise the generated frontend and the generic backend. Package checks
|
|
268
314
|
install the gem, generate from external Ruby sources, and execute the resulting
|
|
@@ -297,6 +343,71 @@ Generation is a one-time build step per regeneration, not a cost of parsing an
|
|
|
297
343
|
input file. The generated frontend is loaded and measured with Ruby Box disabled.
|
|
298
344
|
Results vary with input and environment.
|
|
299
345
|
|
|
346
|
+
### AST compatibility evidence
|
|
347
|
+
|
|
348
|
+
The correctness benchmark compares the generated frontend's AST with
|
|
349
|
+
`RubyVM::AbstractSyntaxTree` for every `.rb` file in explicitly supplied local
|
|
350
|
+
source trees. It does not execute those files. Generate once with Ruby Box
|
|
351
|
+
enabled; the comparison runs in a separate process with Ruby Box disabled:
|
|
352
|
+
|
|
353
|
+
```sh
|
|
354
|
+
RUBY_BOX=1 bundle exec ruby -Ilib benchmark/compatibility.rb \
|
|
355
|
+
--frontend local/tmp/compatibility-frontend.rb \
|
|
356
|
+
--frontend-class CompatibilityFrontend \
|
|
357
|
+
--corpus rails=/path/to/rails \
|
|
358
|
+
--corpus trick13=/path/to/trick13 \
|
|
359
|
+
--report local/tmp/arpaka-compatibility.jsonl
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
For a generated frontend named `A::B::Parser`, pass
|
|
363
|
+
`--frontend-class A::B::Parser`. The default `CompatibilityFrontend` remains
|
|
364
|
+
available for existing top-level frontends.
|
|
365
|
+
|
|
366
|
+
The Rake entry point accepts the same corpus selections through `CORPORA`,
|
|
367
|
+
with optional `REPORT` and `RUBY_SOURCE`:
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
RUBY_BOX=1 CORPORA='rails=/path/to/rails trick13=/path/to/trick13' \
|
|
371
|
+
REPORT=/tmp/arpaka-compatibility.jsonl bundle exec rake benchmark:compatibility
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
AST comparison currently covers a basic syntax subset. A file is a match only
|
|
375
|
+
when both entire ASTs can be normalized and agree. Differences are mismatches,
|
|
376
|
+
except that a call's explicit keyword arguments are reported as
|
|
377
|
+
`accepted_mismatch`: Arpaka retains them as `KeywordArguments`, while
|
|
378
|
+
`RubyVM::AbstractSyntaxTree` represents them as `HASH`. This exception applies
|
|
379
|
+
only at a call argument position; Hash literals and `Pair` nodes are compared
|
|
380
|
+
normally. Unknown or ambiguous nodes make the file unverified, including
|
|
381
|
+
unknown nodes nested inside otherwise supported constructs. Parsing failures
|
|
382
|
+
and per-parser 10-second timeouts are reported separately. A successful parse
|
|
383
|
+
by itself never counts as AST agreement. Missing semantic information is not
|
|
384
|
+
reconstructed from the input or the reference parser. When absent metadata
|
|
385
|
+
would describe an empty parameter list or a regexp without flags, the result is
|
|
386
|
+
unverified rather than assuming agreement or a defect.
|
|
387
|
+
|
|
388
|
+
The JSONL report records runtime and revision information, source and generated
|
|
389
|
+
frontend hashes, file-level outcomes, comparison diagnostics, and totals. These
|
|
390
|
+
identify the exact inputs and distinguish complete matches, accepted
|
|
391
|
+
representation differences and comparisons that remain unsupported. Internal
|
|
392
|
+
comparison errors are also distinct from parser failures. The timeout includes
|
|
393
|
+
worker startup and AST normalization. Exit status is 0 when every file is a
|
|
394
|
+
match or accepted mismatch, 1 for any other mismatch, unverified file, parsing
|
|
395
|
+
failure, timeout or internal error, and 2 for invalid configuration. The
|
|
396
|
+
current frontend has known semantic losses, so a corpus run can legitimately
|
|
397
|
+
exit with status 1. Reports are local artifacts; external
|
|
398
|
+
corpora are not downloaded or included in the gem.
|
|
399
|
+
|
|
400
|
+
The normal test suite checks the comparison machinery and known semantic-loss
|
|
401
|
+
examples. External corpus comparison is an explicit task, separate from the
|
|
402
|
+
speed benchmark and the default CI checks.
|
|
403
|
+
|
|
404
|
+
The normal compatibility tests run the Thread path. Ruby Box/Ractor
|
|
405
|
+
compatibility tests are currently omitted by default because of Ruby 4.0's
|
|
406
|
+
Ruby Box/Ractor constraints; opt into those tests with
|
|
407
|
+
`ARPAKA_RUN_RACTOR_TESTS=1`. This test-suite setting is separate from the
|
|
408
|
+
compatibility runner's `--parallel ractor` mode, which remains available as an
|
|
409
|
+
experimental, explicitly selected benchmark mode.
|
|
410
|
+
|
|
300
411
|
## License
|
|
301
412
|
|
|
302
413
|
[MIT](LICENSE.txt).
|
data/Rakefile
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rake/testtask"
|
|
5
|
+
require "shellwords"
|
|
5
6
|
|
|
6
7
|
Rake::TestTask.new(:test) do |t|
|
|
7
8
|
t.libs << "test"
|
|
8
9
|
t.libs << "lib"
|
|
9
|
-
|
|
10
|
+
# The compatibility check exercises Ruby::Box and Ractor in a dedicated
|
|
11
|
+
# process. Keep it out of the regular suite so Box state cannot leak into
|
|
12
|
+
# the other tests.
|
|
13
|
+
t.test_files = FileList["test/**/*_test.rb"].exclude("test/benchmark_compatibility_test.rb")
|
|
10
14
|
end
|
|
11
15
|
|
|
12
16
|
namespace :ruby do
|
|
@@ -36,4 +40,14 @@ namespace :benchmark do
|
|
|
36
40
|
task :parse do
|
|
37
41
|
ruby "benchmark/parse.rb"
|
|
38
42
|
end
|
|
43
|
+
|
|
44
|
+
desc "Compare generated Ruby ASTs with RubyVM (requires CORPORA; optional REPORT and RUBY_SOURCE)"
|
|
45
|
+
task :compatibility do
|
|
46
|
+
arguments = Shellwords.split(ENV.fetch("CORPORA", "")).flat_map do |corpus|
|
|
47
|
+
["--corpus", corpus]
|
|
48
|
+
end
|
|
49
|
+
arguments += ["--report", ENV.fetch("REPORT")] if ENV["REPORT"]
|
|
50
|
+
arguments += ["--ruby-source", ENV.fetch("RUBY_SOURCE")] if ENV["RUBY_SOURCE"]
|
|
51
|
+
ruby "benchmark/compatibility.rb", *arguments
|
|
52
|
+
end
|
|
39
53
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
digraph arpaka_architecture {
|
|
2
|
+
graph [bgcolor="white", rankdir=TB, pad="0.25", nodesep="0.45", ranksep="0.65"];
|
|
3
|
+
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=12,
|
|
4
|
+
color="#334155", fillcolor="#f8fafc", margin="0.16,0.10"];
|
|
5
|
+
edge [color="#64748b", penwidth=1.2, arrowsize=0.75, fontname="Helvetica", fontsize=10];
|
|
6
|
+
|
|
7
|
+
parse_y [label="Ruby parse.y\n(Yacc/Bison grammar)", fillcolor="#e0f2fe"];
|
|
8
|
+
lrama_parser [label="Lrama::Parser\nread grammar"];
|
|
9
|
+
grammar [label="Grammar model\nYaccRule / Actions"];
|
|
10
|
+
ruby_backend [label="Lrama::Ruby backend\ngenerate Ruby Parser", fillcolor="#dcfce7"];
|
|
11
|
+
generated_parser [label="Generated Ruby Parser\nparse table + reduce actions", fillcolor="#dcfce7"];
|
|
12
|
+
|
|
13
|
+
ruby_source [label="Ruby source", fillcolor="#fef3c7"];
|
|
14
|
+
lexer [label="Arpaka Lexer\nsource → tokens", fillcolor="#fef3c7"];
|
|
15
|
+
builder [label="Arpaka Builder\nsemantic values → AST", fillcolor="#fce7f3"];
|
|
16
|
+
ast [label="Arpaka AST\nindependent representation", fillcolor="#fce7f3"];
|
|
17
|
+
frontend [label="Generated Ruby entry file\nLexer + Parser + Builder", fillcolor="#ede9fe"];
|
|
18
|
+
|
|
19
|
+
oracle [label="RubyVM::AST\nvalidation oracle", shape=note, fillcolor="#f1f5f9"];
|
|
20
|
+
comparator [label="Compatibility comparator\nmatch / accepted / mismatch / unverified", shape=note, fillcolor="#f1f5f9"];
|
|
21
|
+
|
|
22
|
+
parse_y -> lrama_parser -> grammar -> ruby_backend -> generated_parser;
|
|
23
|
+
ruby_source -> lexer -> generated_parser;
|
|
24
|
+
generated_parser -> builder -> ast;
|
|
25
|
+
generated_parser -> frontend [style=dashed, label="embedded in"];
|
|
26
|
+
lexer -> frontend [style=dashed, label="embedded in"];
|
|
27
|
+
builder -> frontend [style=dashed, label="embedded in"];
|
|
28
|
+
ast -> comparator [style=dashed, label="compare"];
|
|
29
|
+
oracle -> comparator [style=dashed, label="reference"];
|
|
30
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Arpaka architecture
|
|
2
|
+
|
|
3
|
+
Arpaka uses Lrama to generate a Ruby parser from Ruby's `parse.y`. The generated
|
|
4
|
+
frontend combines a Lexer, the generated Parser, and a Builder, then returns an
|
|
5
|
+
Arpaka-specific AST.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Generation pipeline
|
|
10
|
+
|
|
11
|
+
The generation pipeline starts with the grammar definition rather than with a
|
|
12
|
+
parser implementation written by hand:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
parse.y / Yacc grammar
|
|
16
|
+
↓
|
|
17
|
+
Lrama::Parser
|
|
18
|
+
↓
|
|
19
|
+
Grammar model / YaccRule
|
|
20
|
+
↓
|
|
21
|
+
Lrama::Ruby backend
|
|
22
|
+
↓
|
|
23
|
+
Generated Ruby Parser
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Lrama owns grammar parsing, rule preparation, parser tables, and shift/reduce
|
|
27
|
+
execution. The Ruby backend turns those structures into standalone Ruby source.
|
|
28
|
+
Grammar actions are embedded as Ruby reduce actions; v1 does not introduce a
|
|
29
|
+
language-independent semantic-action IR.
|
|
30
|
+
|
|
31
|
+
## Runtime pipeline
|
|
32
|
+
|
|
33
|
+
The generated Parser consumes tokens produced by the Arpaka Lexer. The Builder
|
|
34
|
+
turns semantic values produced by reductions into Arpaka AST nodes.
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
Ruby source → Lexer → Parser → Builder → Arpaka AST
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The generated Ruby entry file packages these components together. It exposes a
|
|
41
|
+
Ruby API for parsing source and does not require the Lrama or Arpaka gems at
|
|
42
|
+
runtime.
|
|
43
|
+
|
|
44
|
+
The Lexer is a separate component because Lrama generates parsers, not Ruby
|
|
45
|
+
lexers. It handles Ruby's lexical rules, including strings, interpolation,
|
|
46
|
+
regular expressions, percent literals, heredocs, escapes, and context-sensitive
|
|
47
|
+
token boundaries.
|
|
48
|
+
|
|
49
|
+
## Responsibilities
|
|
50
|
+
|
|
51
|
+
- **Lrama** reads Yacc/Bison grammar, builds the grammar model, computes LALR or
|
|
52
|
+
IELR tables, and executes grammar actions through the generated parser.
|
|
53
|
+
- **The Ruby backend** emits the parser table, parser runtime, and Ruby grammar
|
|
54
|
+
actions as Ruby source.
|
|
55
|
+
- **The Arpaka Lexer** converts Ruby source into the token stream expected by
|
|
56
|
+
`parse.y`.
|
|
57
|
+
- **The Builder** resolves scopes and semantic values and constructs AST nodes.
|
|
58
|
+
- **The Arpaka AST** represents Ruby syntax and the semantic information needed
|
|
59
|
+
by later consumers. It is an independent representation, not a copy of
|
|
60
|
+
RubyVM's internal node layout.
|
|
61
|
+
- **The compatibility comparator** uses RubyVM::AST as an Oracle for finding
|
|
62
|
+
missing or incorrectly interpreted information. It allows representation
|
|
63
|
+
differences when the meaning is equivalent.
|
|
64
|
+
|
|
65
|
+
## Profiles and Actions
|
|
66
|
+
|
|
67
|
+
Ruby's grammar changes across versions. Profiles select the source revision,
|
|
68
|
+
runtime features, and Action mappings for a particular grammar. Rule signatures
|
|
69
|
+
are used where numeric rule IDs are unstable; legacy profiles retain mappings
|
|
70
|
+
for older Ruby grammars.
|
|
71
|
+
|
|
72
|
+
An Action in v1 means the semantic action attached to a Yacc grammar rule. The
|
|
73
|
+
profile maps that action to Ruby Builder code. A common semantic-action adapter
|
|
74
|
+
between grammar actions and language-specific Builders is a possible future
|
|
75
|
+
extension, but it is not required by the v1 architecture.
|
|
76
|
+
|
|
77
|
+
## AST and validation
|
|
78
|
+
|
|
79
|
+
Arpaka's AST is designed for a Ruby frontend, so node names and wrappers do not
|
|
80
|
+
need to match RubyVM::AST exactly. RubyVM::AST provides an Oracle for checking
|
|
81
|
+
whether the frontend preserves relevant syntax and meaning. The comparator
|
|
82
|
+
classifies results as `match`, `accepted_mismatch`, `mismatch`, or `unverified`.
|
|
83
|
+
|
|
84
|
+
Some RubyVM metadata is intentionally outside the v1 AST contract. Examples
|
|
85
|
+
include information whose meaning is tied to RubyVM internals or cases where
|
|
86
|
+
the reference AST does not expose enough information to distinguish two
|
|
87
|
+
representations. Those cases remain explicitly classified instead of being
|
|
88
|
+
silently accepted.
|
|
89
|
+
|
|
90
|
+
## Scope of v1
|
|
91
|
+
|
|
92
|
+
v1 targets Ruby 4.0's `parse.y` and provides a generated Ruby frontend with a
|
|
93
|
+
Lexer, Parser, Builder, and independent AST. Ruby 3.3 and 3.4 profiles are
|
|
94
|
+
additional compatibility targets. Broader version support, language-neutral
|
|
95
|
+
semantic actions, and other-language backends can be introduced later without
|
|
96
|
+
making them prerequisites for the v1 frontend.
|
|
Binary file
|
data/docs/contributes.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Compatibility checks
|
|
2
|
+
|
|
3
|
+
This repository has two kinds of compatibility checks. They serve different
|
|
4
|
+
purposes and should not be treated as interchangeable.
|
|
5
|
+
|
|
6
|
+
## Pull request CI
|
|
7
|
+
|
|
8
|
+
Pull request CI is the fast, required check for the repository itself. It
|
|
9
|
+
verifies that the generated frontend and the test suite still work on the
|
|
10
|
+
supported Ruby versions.
|
|
11
|
+
|
|
12
|
+
The compatibility runner's Ractor mode is experimental. It is separate from
|
|
13
|
+
the repository test suite: ordinary tests execute the Thread compatibility
|
|
14
|
+
path, while Ruby Box/Ractor compatibility test cases are currently omitted by
|
|
15
|
+
default. To opt into those test cases, set
|
|
16
|
+
`ARPAKA_RUN_RACTOR_TESTS=1`. Generate a frontend first in a Ruby Box, then run
|
|
17
|
+
the runner without a Ruby Box:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
RUBY_BOX=1 OUTPUT=/tmp/arpaka-frontend.rb bundle exec rake ruby:generate
|
|
21
|
+
|
|
22
|
+
bundle exec ruby benchmark/compatibility.rb \
|
|
23
|
+
--ruby-source vendor/ruby \
|
|
24
|
+
--corpus fixtures=benchmark/fixtures \
|
|
25
|
+
--frontend /tmp/arpaka-frontend.rb \
|
|
26
|
+
--reference-cache /path/to/reference.jsonl \
|
|
27
|
+
--threads 2 \
|
|
28
|
+
--parallel ractor
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The runner's default mode remains Thread-based. Ractor checks should not make
|
|
32
|
+
ordinary pull request validation depend on large external repositories, and
|
|
33
|
+
the test-suite omission is not a change to the runner's `--parallel ractor`
|
|
34
|
+
option.
|
|
35
|
+
|
|
36
|
+
## Weekly compatibility inspection
|
|
37
|
+
|
|
38
|
+
The weekly inspection is intended for larger real-world inputs, especially the
|
|
39
|
+
Rails repository and the TRICK programs. It is a diagnostic and compatibility
|
|
40
|
+
inspection, not a replacement for pull request CI.
|
|
41
|
+
|
|
42
|
+
The external repositories should be checked out by the CI job at the commit
|
|
43
|
+
being inspected. The RubyVM Oracle JSONL should then be generated in the same
|
|
44
|
+
job and with the same Ruby runtime used for the comparison. The Oracle is
|
|
45
|
+
temporary job data; it should not be committed to this repository.
|
|
46
|
+
|
|
47
|
+
The inspection should record the external repository commits, Ruby runtime,
|
|
48
|
+
Ractor count, GC setting and result summary. The comparison report should be
|
|
49
|
+
uploaded as an Artifact. The much larger Oracle should normally remain in the
|
|
50
|
+
job workspace and only be uploaded for a failed or explicitly requested run.
|
|
51
|
+
|
|
52
|
+
### Reference benchmark
|
|
53
|
+
|
|
54
|
+
The following measurement was made with Ruby 4.0.7 against the Rails
|
|
55
|
+
repository and TRICK 2013, 2015, 2018, 2022 and 2025 sources. The corpus
|
|
56
|
+
contained 3,525 Ruby files and both modes used `--threads 32`.
|
|
57
|
+
|
|
58
|
+
| Mode | Elapsed time | Heap allocated pages | Heap live slots | Minor GC | Major GC |
|
|
59
|
+
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
60
|
+
| Thread | 6.85 s | 1,483 | 131,390 | 5 | 3 |
|
|
61
|
+
| Thread + `--disable-gc` | 6.62 s | 1,484 | 131,433 | 5 | 3 |
|
|
62
|
+
| Ractor | 15.66 s | 4,795 | 4,445,551 | 128 | 10 |
|
|
63
|
+
| Ractor + `--disable-gc` | 10.42 s | 101,705 | 131,481,029 | 2 | 1 |
|
|
64
|
+
|
|
65
|
+
In this run, Ractor mode was about 2.3 times slower than Thread mode with GC
|
|
66
|
+
enabled. Disabling GC reduced the Ractor time by about 33%, but increased heap
|
|
67
|
+
usage substantially. These numbers are a reference point rather than a
|
|
68
|
+
performance guarantee; Ruby versions, CPU count, corpus revisions and worker
|
|
69
|
+
counts can change the result.
|
|
70
|
+
|
|
71
|
+
## Ractor mode
|
|
72
|
+
|
|
73
|
+
Ractor workers share the generated frontend class loaded by the parent Ractor.
|
|
74
|
+
The frontend is loaded once per run so that each corpus does not redefine its
|
|
75
|
+
constants.
|
|
76
|
+
|
|
77
|
+
Use `--threads` to control the number of concurrent workers. Ractor mode has
|
|
78
|
+
additional object-transfer and heap costs, so the best worker count may differ
|
|
79
|
+
from the Thread mode.
|
|
80
|
+
|
|
81
|
+
## Disabling GC
|
|
82
|
+
|
|
83
|
+
`--disable-gc` is an opt-in performance experiment. It applies to both Thread
|
|
84
|
+
and Ractor workers; GC remains enabled by default.
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
bundle exec ruby benchmark/compatibility.rb \
|
|
88
|
+
--corpus fixtures=benchmark/fixtures \
|
|
89
|
+
--frontend /tmp/arpaka-frontend.rb \
|
|
90
|
+
--reference-cache /path/to/reference.jsonl \
|
|
91
|
+
--parallel ractor \
|
|
92
|
+
--threads 32 \
|
|
93
|
+
--disable-gc
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Disabling GC can improve throughput substantially, but it can also grow the
|
|
97
|
+
Ruby heap dramatically. Use it only on machines with ample memory and inspect
|
|
98
|
+
the `GC.stat` values in the report. It should not be enabled by default in CI.
|
|
99
|
+
|
|
100
|
+
## Reading results
|
|
101
|
+
|
|
102
|
+
The report distinguishes parser outcomes from tree comparison results:
|
|
103
|
+
|
|
104
|
+
- `match`: normalized trees are equal.
|
|
105
|
+
- `accepted_mismatch`: a known, explicitly accepted representation difference.
|
|
106
|
+
- `mismatch`: normalized trees differ.
|
|
107
|
+
- `unverified`: one side uses a construct outside the comparison model.
|
|
108
|
+
- `parse_failure`: parsing failed.
|
|
109
|
+
- `timeout`: a worker exceeded its time limit.
|
|
110
|
+
- `error`: an unexpected worker or input error occurred.
|
|
111
|
+
|
|
112
|
+
Existing `mismatch` and `unverified` results are not automatically evidence of
|
|
113
|
+
a Ractor failure. Compare Thread and Ractor runs using the same corpus,
|
|
114
|
+
reference cache and Ruby runtime; Ractor-specific failures are parse failures,
|
|
115
|
+
timeouts, worker errors, or result differences that appear only in Ractor mode.
|