Skip to content

Add a Rust language generator - #56

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/practical-mendel-gg7bjv
Sep 12, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/practical-mendel-gg7bjv

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Adds RustGenerator, a sixth target alongside C#, C++, C, Python and JavaScript.

C was interesting because it had almost nothing to map onto. Rust is interesting for the opposite reason: it has a real feature for nearly everything the AST says, so the question is which one each part of a declaration became rather than what to write in its place.

The AST says Rust writes
ClassDeclaration a struct for the data and an impl block for the behaviour — the split Rust insists on
a member function &self or &mut self depending on IsReadOnly, which is the promise C++ spells as a trailing const, stated more strongly
a constructor pub fn new(…) -> Self, built from the initialiser list as the tail expression, with the field-init shorthand where a field takes a variable of its own name
a destructor impl Drop — the one member whose name Rust fixes, and which cannot live in the inherent block
an interface a trait; BaseType on one is a supertrait
an operator its std::ops trait (impl Add for Point, naming the Output), or PartialEq for ==
a conversion impl From<Point> for f64 — implementing one gives Into for free
a specialisation impl Describe for Point — the one place a target answers C++'s template<> struct Describe<T> exactly, since attaching facts to a type without touching the type is what both are for
an enumeration scoped variants, so nothing needs prefixing (the thing C could not do), and #[repr(i32)] for a fixed underlying type
IsPure / MustUseResult #[must_use], and nothing on a function that answers nothing
IsCompileTimeEvaluable const fn
CompileTimeAssertion const _: () = assert!(…), which needs no macro crate
a namespace nested pub mod

What is left over. Inheritance, which Rust does not have — a base type on a struct becomes a field with a note. And the operators Rust supplies from another one and will not let a type define by itself: != comes from PartialEq, the four orderings from PartialOrd::partial_cmp, and &&/|| short-circuit and are not overloadable at all. Each is written as a note naming the trait to implement instead, rather than as an implementation the language would reject.

Two rules are the opposite of every other target, so they are written out rather than inherited. A let binds immutably, so an ordinary declaration is let mut and only a constant one is plain let — the warning an unneeded mut earns beats the error a missing one causes. And an inferred declaration writes no type at all, which is the one thing on the list Rust does better than the language IsTypeInferred was modelled on: no auto, no var.

Testing

  • RustGeneratorTests — 36 tests pinning which feature each part of a declaration became.
  • RustGeneratedSourceCompilesTests — compiles the output with rustc. Rust earns this more than C did: a receiver that should have been &mut self, an associated item carrying a visibility Rust forbids, a trait implementation missing its Output — each is a spelling a test can pin and only a compiler notices. I verified it has teeth by removing the type Output line from generated output and watching rustc reject it. Inconclusive where no toolchain is on the path.
  • CompiledExemplarone AST, two real compilers. The C and Rust compile tests are now checked against the same declarations rather than parallel fixtures, which is a stronger claim: that the same AST comes out as valid source in each target. Each test still adds what only its language has to answer.
  • Full suite: 579 passed, 0 failed.

Changes outside the new generator

  • The braced-list writer moves from CFamilyGenerator up to StandardLanguageGenerator, parameterised by its delimiters and by how a language says which member an element is for. Rust's struct literal and array literal share it instead of copying it; C and C++ output is unchanged.
  • CGenerator now writes a note for a specialisation instead of dropping it silently — one line, and what every other target already did. It was a pre-existing gap, and I hit it because the documentation sentence about specialisations needed rewriting either way.
  • Coder.Editor registers a Rust definition with the syntax highlighter. It ships fifteen languages and Rust is not among them, so the preview pane would have drawn generated Rust as plain text — the existing EveryGenerator_ProducesSourceTheHighlighterClassifies test caught exactly that.

README, CLAUDE.md and docs/design.md are updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7


Generated by Claude Code

Rust is the sixth target and the first with a real answer to most of what
the AST says rather than a convention standing in for one:

- data goes in a `struct` and behaviour in an `impl` block
- an interface is a `trait`, and a base type on one is a supertrait
- a destructor is `impl Drop`, an operator is its `std::ops` trait, a
  conversion is `impl From`, and a specialisation is `impl Trait for Type`
  - the one place a target answers C++'s explicit specialisation exactly
- an enumeration's variants are scoped by it, so nothing needs prefixing,
  and a fixed underlying type is `#[repr]`
- a pure function is `#[must_use]`, one fixed at build time is a `const fn`,
  and an assertion is `const _: () = assert!(...)`
- `IsReadOnly` decides the receiver: `&self` against `&mut self`, which is
  the promise C++ spells as a trailing `const`, stated more strongly

What is left over is inheritance, which Rust does not have and which
becomes a field with a note, and the operators Rust supplies from another
one and will not let a type define by itself - `!=`, the orderings, and the
short-circuiting pair - each written as a note naming the trait to
implement instead.

Two rules are the opposite of every other target here and so are written
out: a `let` binds immutably, so an ordinary declaration is `let mut`; and
an inferred declaration writes no type at all, which needs no `auto` and no
`var`.

`RustGeneratedSourceCompilesTests` compiles what it writes with rustc.
`CompiledExemplar` is the AST it and the C test are both checked against -
one AST, two real compilers, which says more than two parallel fixtures
could.

The braced-list writer moves from `CFamilyGenerator` up to
`StandardLanguageGenerator`, parameterised by its delimiters and by how a
language says which member an element is for, so Rust's struct literal and
array literal share it rather than copying it. `CGenerator` now says
something about a specialisation instead of dropping it silently, which is
what the other targets already did. `Coder.Editor` registers a Rust
definition with the syntax highlighter, which ships fifteen languages and
has no Rust among them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
SonarCloud's S1192: the sentence naming PartialOrd was written four times,
once per ordering, and the one naming nothing twice. They are four and two
spellings of one decision, so they are one constant each - and the wording
now has one place to change rather than six.

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

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 04e96fd into main Sep 12, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/practical-mendel-gg7bjv branch September 12, 2026 08:50
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.

2 participants