Skip to content
ry

Development

ry is a Rust workspace whose crates live under crates/. Structure maps every source file, and Architecture explains how the analysis fits together. The editor extensions live in editors/code (VS Code) and editors/zed (Zed).

The workspace builds on the dated nightly that rust-toolchain.toml pins, because std’s allocator API and other unstable features are worth having before they stabilize (the policy is in .agents/memory/decisions.md, “Data-oriented design and explicit allocators”). With rustup, cargo downloads that toolchain, with clippy and rustfmt, on the first build. The Zed extension is the exception: editors/zed/rust-toolchain.toml pins a stable toolchain, because the nightly’s wasm32-wasip2 std imports a WASI function that Zed’s host does not provide yet. The nix dev shell reads the same file. Nightly rustfmt also applies the import settings in rustfmt.toml, so cargo fmt keeps a single use { ... } tree per file.

The justfile at the repository root names every day-to-day action, and just with no arguments lists them all. The common ones:

Terminal window
just ry check # build ry and its runtime if needed, then run `ry check`
just build # every binary and ry's runtime (`just build --release`)
just try # build ry and walk through every command in a demo project
just try-editor # build ry and print how to point an editor at it
just gate # tests, clippy -D warnings, and fmt --check
just battery # all workspace tests, excluding zed_ry
just fixture <group__case> # one fixture case
just bless -p semantics --test ... # rewrite fixture expectations from current output
just docs-test [page] # run the docs' marked examples through the real binary
just stats <path> # phase timings and memory for one workspace
just perf # corpus perf and memory budgets (needs corpus/)
just repl-e2e # drive `ry R` and `ry run` through a pty
just release-dry-run # package and smoke-test this machine's release archive
just docs-deploy <branch> # build the docs site and deploy it (main is production)

The simulators, fuzzers and long oracle runs have their own recipes, which Robustness lists.

just fixture runs a case from the test_typing_fixtures target in semantics unless you pass a different target and package as its second and third arguments. Review the diff after every bless: bless records whatever the code produces, right or wrong.

Without just:

Terminal window
cargo build # the ry binary (the workspace default member)
cargo test # the ry crate's tests
cargo test --workspace --exclude zed_ry # every crate
cargo test -p semantics # the analysis core, the usual loop while iterating

Most behavior is pinned by fixture tests, plain-text .test files whose rendered output is compared to an expected block. Read Testing before you add or change one. Two environment variables come up every day:

  • RY_BLESS=1 rewrites the expected #++++ blocks in place.
  • FIXTURE_FILTER=group__case runs a single case.

Some suites and every performance instrument read a corpus of real-world R code. Fetch it with cargo -Zscript scripts/fetch-corpus.rs, which writes into the gitignored corpus/ directory. The resolved inventory is committed as scripts/corpus-manifest.txt.

The repl crate loads R’s shared library at runtime instead of linking against it, so the whole workspace builds and its tests run on a machine with no R. Only running the console needs R. The end-to-end tests skip cleanly where R is missing; run just repl-e2e before you touch the console. REPL design records how the embedding works.

ry run and ry repl --runtime ry load ry’s own runtime, libR.so, from the directory of the ry executable. It is a separate crate (libr), so cargo build -p ry-lang does not rebuild it; just ry and just build do. To keep a leftover library from running old code, ry compares a hash of the runtime’s sources with the one stored in libR.so right after loading it, and refuses a mismatch with exit status 2:

x ry's runtime (target/debug/libR.so) is out of date: it has build 8fcdf6d9f4862983, but this ry is build
| 2588183553632a45; rebuild it with `just build` or `cargo build -p libr` (same profile as ry)

Build libr with the same profile as ry and the error goes away. The hash covers the sources of libr and the crates it links, so editing the formatter or the IDE layer never triggers it.

  • ry server --debug, or RY_DEBUG=1 in the server’s environment, adds a “Debug” section to every hover with the Lowering, Naming, and Parsing views of the expression under the cursor. It is deliberately not a ry.toml key, because the configuration file is a user-facing contract.

  • ry debug ast <file> prints a file’s syntax tree.

  • ry debug erase <file>... rewrites files in place as their erasure, the plain R that ry R runs.

  • ry debug analysis-stats [path] (or just stats) runs the full pipeline over a workspace through the language server’s queries, with [check] typing forced on, and prints:

    • the wall time and resident-memory growth of each phase, and the slowest files to type-check;
    • keystroke latency and rechecks per keystroke, from an incremental probe;
    • how often hover, goto-definition and signature help answer at sampled identifiers, by role (call head, variable, argument name, $ field, pkg::name);
    • how often completion of a truncated identifier puts the original name in the top five, with latency percentiles;
    • whether find-references on sampled package bindings agrees with an independent lexical resolver (crates/ry/src/scope_oracle.rs), and whether renaming a binding and back restores the text.

    Build with --release when absolute numbers matter; a debug build gives honest ratios.

The sidebar (docs/astro.config.ts) has four sections: Introduction, Tutorial (one page per adoption step), Reference and Development. A moved or deleted page gets an entry in the redirects map there. docs/src/components/Sidebar.astro keeps the sidebar’s scroll position across page loads, because the theme scrolls .sidebar-content rather than the element Starlight’s own persister restores.

Typed examples are tab groups inside <Tabs syncKey="annotation-style">, so a reader’s choice follows them across pages: Untyped (the plain R a reader starts from), then Comments and Inline, with the form the page teaches second: Comments on the tutorial’s types page and Inline elsewhere. An example drops Untyped when its plain R would not be R (records, tags, match, Arrays, DataFrame, the standard library, nominal types) or when its subject is an annotation itself. Pages with tabs are .mdx. Use one tab group per idea, not one per snippet.

Every block of ry output on a page is a test that just docs-test runs through the real binary; Docs examples has the marker format and the rules for tabs.

Agents read the site as Markdown. Every page is also served at its URL plus .md (for example /tutorial/types.md), with the frontmatter, imports and components removed and each tab group turned into one subheading per tab. /llms.txt lists the pages with their descriptions in sidebar order, and /llms-full.txt concatenates them all. Each page’s head links its Markdown version with <link rel="alternate" type="text/markdown">, and the breadcrumb row has a “View as Markdown” link. The code is docs/src/lib/markdown.ts, the three endpoints in docs/src/pages/, and docs/src/routeData.ts. A new MDX component must be handled in bodyToMarkdown, or it leaks into the Markdown as raw tags. A description containing # must be quoted, because YAML reads the rest of the line as a comment.

Run just docs for a live preview, and cd docs && bun run build (or pnpm build) for a full build, which needs Node 22.23.4 or newer. The live site changes only when someone deploys it with just docs-deploy or the Docs workflow (Releasing).

Formatting rules is generated, so do not edit it by hand. Edit crates/format/tests/formatter.template.md and regenerate the page with just format-docs.

  1. Run cd editors/code && bun install.
  2. Open the repository in VS Code and press Ctrl+Shift+B to compile the client in watch mode.
  3. In the Run and Debug view (Ctrl+Shift+D), select Launch Client and press F5.
  4. Open any .R file in the Extension Development Host window that appears.

A comment in R can sit between any two tokens: inside an if header, between a call and its argument list, after an operator. A declarative “format each field” style silently drops such a comment, so the formatter walks the concrete children token by token and decides placement at the token level. The parser attaches a trailing comment inside an expression node, so a decision about where a closing token goes has to look at tokens, never at whole elements. See crates/format/src/format.rs and the fixtures under crates/format/tests/format/.