arpaka 1.0.0 → 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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -2
  3. data/README.md +104 -7
  4. data/Rakefile +15 -1
  5. data/docs/architecture.dot +30 -0
  6. data/docs/architecture.md +96 -0
  7. data/docs/architecture.webp +0 -0
  8. data/docs/contributes.md +115 -0
  9. data/docs/usage.md +497 -0
  10. data/lib/arpaka/cli.rb +5 -53
  11. data/lib/arpaka/plugins/cli.rb +61 -0
  12. data/lib/arpaka/plugins/sample.rb +14 -0
  13. data/lib/arpaka/plugins/sample_runner.rb +21 -0
  14. data/lib/arpaka/{actions.rb → ruby/actions.rb} +230 -151
  15. data/lib/arpaka/ruby/class_name_parser.rb +97 -0
  16. data/lib/arpaka/ruby/cli.rb +64 -0
  17. data/lib/arpaka/ruby/frontend_emitter.rb +59 -0
  18. data/lib/arpaka/ruby/generator.rb +41 -0
  19. data/lib/arpaka/{ruby_grammar.rb → ruby/grammar.rb} +5 -56
  20. data/lib/arpaka/ruby/grammar_emitter.rb +71 -0
  21. data/lib/arpaka/ruby/profile.rb +129 -0
  22. data/lib/arpaka/{profile → ruby/profiles}/ruby_3_3.rb +21 -21
  23. data/lib/arpaka/{profile → ruby/profiles}/ruby_3_4.rb +21 -21
  24. data/lib/arpaka/{runtime → ruby/runtime/detailed}/ast.rb.erb +41 -5
  25. data/lib/arpaka/ruby/runtime/detailed/builder.rb.erb +1177 -0
  26. data/lib/arpaka/{runtime → ruby/runtime}/frontend.rb.erb +17 -4
  27. data/lib/arpaka/{runtime → ruby/runtime}/lexer.rb.erb +576 -99
  28. data/lib/arpaka/ruby/runtime/structured.rb.erb +111 -0
  29. data/lib/arpaka/ruby.rb +23 -0
  30. data/lib/arpaka.rb +6 -9
  31. data/lib/lrama/ruby/backend.rb +4 -9
  32. data/lib/lrama/ruby/parser.rb.erb +35 -9
  33. data/lib/lrama/ruby/parser_emitter.rb +41 -0
  34. data/lib/lrama/ruby/table_codec.rb +50 -0
  35. data/lib/lrama/ruby/version.rb +1 -1
  36. data/lib/lrama/ruby.rb +1 -0
  37. data/sig/arpaka.rbs +8 -3
  38. metadata +32 -16
  39. data/lib/arpaka/generator.rb +0 -51
  40. data/lib/arpaka/profile/ruby.rb +0 -49
  41. data/lib/arpaka/runtime/builder.rb.erb +0 -323
  42. /data/lib/arpaka/{BSDL → ruby/BSDL} +0 -0
  43. /data/lib/arpaka/{COPYING → ruby/COPYING} +0 -0
  44. /data/lib/arpaka/{source.json → ruby/source.json} +0 -0
  45. /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: 5a9e8ba3375a56453551778e540a98c57737d596ddd67d1121b040011d356c5c
4
- data.tar.gz: af951a8d45255b4adb3531ccde464d0a20427fc58067bae9d9c2fb88595c0067
3
+ metadata.gz: 93edcf594153e47990a52fa6b5645d19af6bea08b8eb1dba90c165737293561b
4
+ data.tar.gz: 7c02b70e4a01d155542c1d9db2302498c1d43b65a073ad9e8363bede2ebfe2f2
5
5
  SHA512:
6
- metadata.gz: 84c9d1d486ce1dc6352c47f191396bd7d9b93d23c4938a0d62bdd0080583102ab13e2d7c363c99b72043607ef813c94a81fee842ea255c2ed6f2dc02f594e8ae
7
- data.tar.gz: 86586ad0c9d3bba004a276c0691c4bb64c6d479f4a9330e7a1cd6bdf4bed17d7a545716fc5caf217e2497070ad6f7d0f5ecb4b23a9aa1cae88bcdbcd9d234939
6
+ metadata.gz: e0b845cc98cca04321ba644e8bb3b4bfe1974a0979faec672590110a63f8b825da0e34244fceb911dcd11027c57938a6dd9ad6e775ceb6826968c34d2bd7e2ab
7
+ data.tar.gz: 6e4b87c52c412c8dff7de7fe727d1ece2b4b831e07036c9e2619d48b4c66e59955d419122996ca491e5cca0eb5a32dd5434982c983ef24e3154da7a2bae01a74
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2026-09-07
3
+ ## [2.0.0]
4
4
 
5
- - Initial release
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,11 @@ 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 must be a single Ruby constant name. Existing output files are
149
- preserved unless `--force` is supplied. A failed generation leaves the previous
150
- file intact. The output includes its source profile, generator version and
151
- copyright/license notices.
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.
152
165
 
153
166
  When only `parse.y` is available, Arpaka can replace its `RUBY_TOKEN(NAME)`
154
167
  declarations with token names and generate a frontend without the rest of the
@@ -164,6 +177,17 @@ RUBY_BOX=1 bundle exec arpaka generate \
164
177
  This mode embeds Arpaka's own license notices. The `--ruby-source` mode remains
165
178
  the compatibility-preserving path when the Ruby source tree is available.
166
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
+ ```
190
+
167
191
  The consumer only needs the generated file and Ruby 4.0 or later:
168
192
 
169
193
  ```ruby
@@ -178,6 +202,14 @@ internal; callers do not need to supply tokens. Each call has fresh parsing and
178
202
  local-variable state. No Arpaka/Lrama gem, Ruby Box, grammar files, network access
179
203
  or filesystem writes are needed at runtime.
180
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
+
181
213
  For generation from Ruby, `Arpaka.generate` returns that same standalone source.
182
214
  To try a frontend in memory, use `Arpaka.compile`; both require `RUBY_BOX=1`:
183
215
 
@@ -273,10 +305,10 @@ requires an explicit Ruby source tree. `ruby:check` verifies the source profile,
273
305
  Action inventory, grammar transformation and generated frontend without editing
274
306
  artifacts. `ruby:generate` writes the standalone frontend to `OUTPUT`.
275
307
 
276
- `lib/arpaka/actions.rb` holds the Ruby Action mappings;
277
- `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.
278
310
  Update these and the source checksums together when adding support for a changed
279
- Ruby grammar. Runtime templates live under `lib/arpaka/runtime/`.
311
+ Ruby grammar. Runtime templates live under `lib/arpaka/ruby/runtime/`.
280
312
 
281
313
  Tests exercise the generated frontend and the generic backend. Package checks
282
314
  install the gem, generate from external Ruby sources, and execute the resulting
@@ -311,6 +343,71 @@ Generation is a one-time build step per regeneration, not a cost of parsing an
311
343
  input file. The generated frontend is loaded and measured with Ruby Box disabled.
312
344
  Results vary with input and environment.
313
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
+
314
411
  ## License
315
412
 
316
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
- t.test_files = FileList["test/**/*_test.rb"]
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
+ ![Parser architecture](architecture.webp)
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
@@ -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.