Skip to content

Update Rust parser to RBS 4.1.2 - #90

Closed
soutaro wants to merge 253 commits into
masterfrom
codex/update-rust-parser-v4.1.2
Closed

Update Rust parser to RBS 4.1.2#90
soutaro wants to merge 253 commits into
masterfrom
codex/update-rust-parser-v4.1.2

Conversation

@soutaro

@soutaro soutaro commented Aug 10, 2026

Copy link
Copy Markdown

Updates the RBS C parser pinned by the Rust crates from v4.0.2 to v4.1.2.

v4.1 adds ModuleSelfAnnotation, so this also adds its node-kind label for Rust diagnostic paths.

sampersand and others added 30 commits March 17, 2026 18:15
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Parser.parse_inline_leading_annotation and parse_inline_trailing_annotation
were passing character offsets directly to the C parser, which expects byte
offsets since ruby#2863. With ASCII-only input the two coincide so the bug was
hidden, but multibyte content (e.g. a Japanese comment preceding the
annotation) caused the C parser to start at an invalid byte position and
emit a parsing error.

Apply the existing byte_range helper to convert character offsets to byte
offsets, matching the pattern used by parse_type, parse_method_type, and
other parse_* methods.

The existing tests for parse_inline_*_annotation used ASCII-only input
which would not catch this class of bug, so multibyte regression tests are
added.
- Remove outdated method limitations (singleton methods, splat/block params)
- Add note that singleton methods (`def self.NAME`) are supported
- Document trailing `#:` annotation for method return types
- Fix constant type inference: Symbol literals infer to `Symbol`, not the
  singleton symbol type; add `nil` to the list
- Add Skip Annotation section for `@rbs skip`
- Add limitations: `class << self`, top-level def, non-self receiver,
  top-level attr_*
- Fix typos: "hyphones" -> "hyphens", "infered" -> "inferred"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update docs/inline.md to match current inline parser behavior
Pins the default GITHUB_TOKEN to contents: read on the 6 workflows in
.github/workflows/ that don't actually need any write scope:

- c-check.yml: C-extension build check.
- milestone.yml: PR milestone enforcer. The github-script step only
  reads context.payload.pull_request and calls core.setFailed /
  core.info; no github.rest.* API call.
- ruby.yml: Ruby test matrix.
- rust.yml: Rust parser build.
- typecheck.yml: steep typecheck.
- windows.yml: Windows test job.

bundle-update.yml and comments.yml are intentionally left implicit:

- bundle-update.yml does git push, gh pr create, gh pr merge --auto.
- comments.yml needs comment-write scope.

Motivation: CVE-2025-30066 (March 2025 tj-actions/changed-files
compromise) exfiltrated GITHUB_TOKEN from workflow logs. Per-workflow
caps bound runtime authority irrespective of repo or org default,
give drift protection if the default ever widens, and are credited
per-file by the OpenSSF Scorecard Token-Permissions check.

YAML validated locally with yaml.safe_load.

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
Cache canonical TypeName / Namespace instances behind two
positional factory methods, and route every allocation site —
Ruby helpers, the C parser, the resolver, and the environment
— through them. Repeated construction of structurally equal
values returns the same object instead of allocating fresh.

  Namespace[path, absolute]
  TypeName[namespace, name]

Namespace interns into a per-`absolute` trie of nested Hashes
keyed on path Symbols. TypeName interns into a two-level Hash
keyed by canonical Namespace identity and name Symbol. Both
fast paths are lock-free; cache misses take a mutex.

The C parser calls `Namespace[]` / `TypeName[]` via
`rb_funcallv`; an in-C trie walk that read intern internals
through `rb_hash_lookup` was identical in wall time on Ruby 4.0+
and was dropped to avoid coupling the parser to the cache layout.

Measured against the kaigionrails/conference-app project's RBS
collection (3,903 type names), 1 warmup + 5 runs, median:

  Ruby 3.4.9   master 1.247s wall / 0.63s user
               branch 1.158s wall / 0.57s user   (-7% wall, -10% user)
  Ruby 4.0.4   master 1.175s wall / 0.57s user
               branch 1.146s wall / 0.55s user   (-2.5% wall, -4% user)

Live RBS::Namespace 51,436 → 6,904; RBS::TypeName 78,493 → 22,819
after `rbs list` on Ruby 4.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-perms-readonly

ci: declare workflow-level `contents: read` on 6 test/lint workflows
soutaro and others added 29 commits July 30, 2026 12:29
A backport is a cherry-pick, so the only pull request its commit is
associated with is the one that carried the backport. That pull request
says nothing about the change, and every commit it brought over resolves
to it, so a release branch's changelog credits all of its entries to one
number -- 4.0.3 lists three, all of them ruby#3005.

`git cherry-pick -x` records the commit a cherry-pick was made from, so
follow it: look the recorded origin up instead of the commit, and the
changelog names the pull request the change was written and reviewed in.
A commit backported twice carries one line per hop, so the first one,
where the change started, is the one used.

An origin that leads nowhere -- a commit cherry-picked from a fork, or
one that reached the default branch without a pull request -- falls back
to the commit in this history, which is at least the backport that
brought it here. Cherry-picks made without `-x` record nothing to follow
and behave as before.

The commit messages are read with `%B`, so the output is forced to UTF-8:
unlike the SHAs read until now, a message can hold anything, and
splitting one that is not ASCII fails under `LANG=C`, as in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvSmN7ats6HGEyeT7VEbau
…-picks

Attribute a backported commit to the pull request it came from
Pushing a tag needs credentials that a cloud development environment does
not have, so the tag is created by `Release gems` rather than by hand
before it.

The workflow now takes the commit being released and the version that
commit declares. Everything is built from the commit, so the run no
longer depends on where `master` is when it starts, and the version is
the same fact stated a second time: the run stops before anything is
built unless it matches `RBS::VERSION` at that commit and the section
CHANGELOG.md starts with. Releasing the wrong commit, or the right one
under the wrong name, is a failed run rather than a gem to yank.

The tag is created once both gems are known to build and run, and before
anything is published, so the reversible step still comes before the
irreversible one. What it names is decided by the checkout, so nothing
rests on it existing first, which is what let it move after the build.

`github.ref_type == 'tag'` used to be what separated a release from a
build, and a branch dispatch was the dry run. The ref no longer decides
anything, so `dry_run` is an explicit input: it builds and checks both
gems and stops before the tag.

`rake gem:check_release[X.Y.Z]` and `rake gem:tag` are the two steps the
workflow runs, so both are also available locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvSmN7ats6HGEyeT7VEbau
A patch release can be cut from a release branch, which is not an
ancestor of the default branch, so requiring one rejected a release that
is perfectly normal.

What is left is that some branch contains the commit -- a commit no
branch leads to is one that cannot be found again -- and the branches
that do are printed, so the log says where the release came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvSmN7ats6HGEyeT7VEbau
Create the release tag in the workflow, from a commit and a version
A dev release cut from `master` after 4.1.1, for people who need one of the
changes since it early. `.dev.N` is a prerelease, and it is not written up in
CHANGELOG.md nor announced with a GitHub release, so it does not disturb the
4.1.2 cycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhJnDiMKwunqnLpuXzRY7v
rdoc 8 needs `rbs >= 4.0.0`, which resolved to the C-extension gem when the pin went in.
`gem install rdoc` now picks up `rbs-4.1.1-java`, so the pin is no longer needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhJnDiMKwunqnLpuXzRY7v
A bare `X.Y.0.dev` means `X.Y.0` is being developed; any other version means the one after it is.
So a release needs no follow-up bump, and starting a new minor is the only version change made by
hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhJnDiMKwunqnLpuXzRY7v
`rake release` published from a working copy and built no `-java` gem, and `release:note` drafted
the GitHub release that `gem:gh_release` now publishes. README pointed at both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhJnDiMKwunqnLpuXzRY7v
`Dockerfile.jruby` was not mentioned in the wasm README. Chicory and ASM are pinned once, in
`rbs.gemspec`, not in the Dockerfile as docs/release.md claimed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhJnDiMKwunqnLpuXzRY7v
Update the release docs, and remove the release path they replaced
`GC.enable` returns whether GC was *disabled* before the call (true when
the call actually re-enabled GC), not whether it was enabled. The ensure
clause `GC.disable unless was_enabled` therefore inverted the restore:
in the common case where GC is enabled when the test starts, GC.enable
returns false and the ensure clause disables GC permanently.

Every test that runs after GC_test then executes with GC turned off, so
the whole stdlib test process accumulates garbage until exit. On the
ruby/ruby Windows CI runners this reaches the process memory limit and
the suite dies with NoMemoryError (deterministically), and on all
platforms it silently changes what later tests exercise.

Rename the variable to what the value actually means and restore the
previous state with `GC.disable if was_disabled`, mirroring test_disable.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fix GC_test#test_enable leaving GC disabled for the rest of the suite
`GC.stress = true` runs a full GC on every allocation, and each
`assert_send_type` allocates heavily while parsing the method type and type
checking the call trace. Since ruby#3059 stopped `test_enable` from leaking a
disabled GC into the rest of the suite, stress mode is finally live during this
test, and the five assertions between `GC.stress = 0` and `GC.stress = false`
turn the whole stdlib suite from 193s into 827s -- 647s of it inside this one
method. The cost scales with heap size, so it is far worse in the full suite
than when the file runs alone.

The assertions only check the types of the return values, so no GC needs to
actually run. Disable GC for the duration and restore both the stress mode and
the previous enabled state afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtDw9t8cMiaqT2s8wwSsDt
…nce-424sfy

Keep GC disabled during GC_test#test_stress_and_stress=
The 4.1.2 release proper, cut from `master` after 4.1.1 and the
`4.1.2.dev.1` release in between. `Gemfile.lock` is regenerated with the
bump, and CHANGELOG.md gains the 4.1.2 section.

The changelog covers the whole cycle since 4.1.1, the `.dev.1` release
included, which is how a release proper is written up. Both entries are
the stdlib test suite, which is excluded from the gem's files, so nothing
in this release reaches the gem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QUAgW2m9wsYE1cLM1fFbw
`gem:changelog` and `gem:changelog:json` go through `gh`, which a Claude
Code on the web session cannot reach: `api.github.com` is blocked at the
agent proxy for anything the shell does, so `gh` is absent, installing it
does not help, and rewriting the tasks against REST or Net::HTTP would be
refused the same way. The rest of the release is unaffected -- the other
tasks read git, or run on a runner.

Document the route that does work there, the GitHub MCP server, as the
three steps the task itself takes: resolve the base tag, list the
commits, then match the pull requests by `head.sha`. That intersection
is what the GraphQL `associatedPullRequests` query answers.

The trap it warns about is the obvious shortcut of reading the numbers
out of `Merge pull request #N` subjects, which looks like it works and
loses pull requests silently: the path filter the task uses drops the
merge commits while keeping the commits they merged, and five of the
eight numbers of the 4.1.2 cycle went with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QUAgW2m9wsYE1cLM1fFbw
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QHHu1wodDoUBPi2Cs7yhCn
Say how the changelog is assembled without `gh`
`NODISCARD` is a generic name that can easily collide with macros defined
by other headers embedding the RBS parser. Prefix it with `RBS_` to match
the other public macros in `include/rbs/defines.h`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SesqemYtV3tnKoNGYuP1JW
…tzvbf7

Rename NODISCARD macro to RBS_NODISCARD
`rake wasm:build` compiled the parser without `NDEBUG`, so every `RBS_ASSERT`
survived into `rbs_parser.wasm`. The MRI extension already drops them
(ext/rbs_extension/extconf.rb), and the assertions that matter sit on the
hottest paths there are: two per character in `rbs_encoding.c`, one per
character in `rbs_skip`, and five around the constant pool. Each one is an
out-of-line varargs call to `rbs_assert_impl`, which the compiler cannot inline
away, so the cost is paid on every character lexed.

Parsing core/ + stdlib/ (250 files, 4.3MB) through a WASI host, best of 10:

    assert build  75.7 ms
    NDEBUG build  61.9 ms   (-18%)

Measured the same on wasmtime and on V8. The serialized ASTs for all 250 files
are byte-identical between the two builds.

`DEBUG=1 rake wasm:build` keeps the assertions, matching the DEBUG convention
extconf.rb and the prepare_bench / prepare_profiling tasks already use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uu4EqN6azBeVjM9F71TUp
@soutaro soutaro closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.