Skip to content

Generate Rust topLevel nodes (account, constant, error, event, instruction*, pda, program, root, …)#102

Merged
lorisleiva merged 1 commit into
mainfrom
loris/spec-gen-top-level
Jun 10, 2026
Merged

Generate Rust topLevel nodes (account, constant, error, event, instruction*, pda, program, root, …)#102
lorisleiva merged 1 commit into
mainfrom
loris/spec-gen-top-level

Conversation

@lorisleiva

Copy link
Copy Markdown
Member

This PR teaches the generator to emit the 14 topLevel nodes (plus 2 inline category unions and a CODAMA_VERSION constant) directly to the root of codama-nodes/src/generated/. It is the largest of the node-generation PRs because topLevel nodes are direct Node variants (not wrapped through a category union), the spec's literal and previously-anonymous programNode.origin are first wired through to real Rust types, and the optional-scalar / Option<T> reconciliation cascades across codama-nodes, codama-attributes, and codama-korok-visitors.

The generator gains four small features. First, CategoryRouting is a discriminated union with wrapped and direct modes; the direct mode (used by topLevel) deliberately emits no impl From<Self> for crate::Node, because the Node enum's #[derive(From)] auto-generates the impl and an explicit one would conflict. Second, literal TypeExpr renders as String; the literal value lives in the hand-written Default for that node. Third, the Default-derive heuristic now fires for any struct whose fields are all unconditionally Default-able — Option/Vec/Docs/scalars/String/CamelCaseString-from-stringIdentifier/Box<Option<_>>. The rule is sound by construction: required union/enumeration/node/nestedUnion/literalUnion fields and FIELD_TYPE_OVERRIDES targets are all treated as opaque and disqualify the struct. As a side effect, 28 previously-no-Default nodes across link/count/discriminator/value/contextualValue also gain a derived Default — pure addition, no consumer breakage. Fourth, a new generated/codama_version.rs page emits pub const CODAMA_VERSION: &str = "<spec.version>"; pinned to the spec version at generation time, mirroring the JS CODAMA_VERSION constant used by rootNode(); RootNode::default() now uses this constant instead of the stale "1.0.0" placeholder, bringing the serialized form in line with JS codama.

The reconciliation is the bulk of the diff. Spec-driven type changes: errorNode.code: u32 (spec width, was usize), accountNode.size: Option<u64>, programNode.origin: Option<ProgramOrigin> (the enum generated in PR #9). The locked optional-scalar rule (optional scalar → Option<T> uniformly) flips ~48 read/write sites: is_optional, is_writable (remaining-accounts only), is_signer (remaining-accounts only, optional), subtract, message, and optional_account_strategy. The box-all-union rule wraps every direct union field, and the corresponding constructors gain Box::new(...) while tests deref through *node.field. The InstructionByteDeltaValue inline-union variants rename to {AccountLink, ArgumentValue, NumberValue, ResolverValue} (the common PascalCase suffix across its leaves is just Node); InstructionRemainingAccountsValue already matched the derived rule.

Two nodes keep a hand-written Default for structural reasons: InstructionStatusNode has a required lifecycle: InstructionLifecycle (opaque enum), and RootNode pins both standard: "codama" and version: CODAMA_VERSION. Both are marked #[allow(clippy::derivable_impls)]. A new self-import filter in nodeStructFragment.ts strips the redundant crate::Self import that self-referential nodes (e.g. instructionNode.subInstructions: Vec<InstructionNode>) would otherwise accumulate. ProgramNode::new() defaults the program's own version to "0.0.0" to match the JS factory; ProgramNode::default() stays at "" so SetProgramMetadataVisitor's Cargo.toml-override path (if program.version.is_empty()) is unaffected.

Workspace-wide: 1008 cargo tests pass unchanged (incl. the two golden-IDL tests updated for the new "version": "1.6.0" line), fmt and clippy stay clean, and pnpm test grew by 10 to 140 (new topLevel + Default-heuristic + literal + CODAMA_VERSION coverage). pnpm generate round-trips deterministically. The override surface is unchanged (3 maps, FIELD_TYPE_OVERRIDES still has 1 entry).

lorisleiva commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

Merge activity

  • Jun 10, 2:13 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 10, 2:33 PM UTC: Graphite rebased this pull request as part of a merge.
  • Jun 10, 2:33 PM UTC: @lorisleiva merged this pull request with Graphite.

@lorisleiva lorisleiva changed the base branch from loris/spec-gen-literal-unions to graphite-base/102 June 10, 2026 14:30
@lorisleiva lorisleiva changed the base branch from graphite-base/102 to main June 10, 2026 14:31
…ction*, pda, program, root, …)

This PR teaches the generator to emit the 14 `topLevel` nodes (plus 2 inline category unions and a `CODAMA_VERSION` constant) directly to the root of `codama-nodes/src/generated/`. It is the largest of the node-generation PRs because topLevel nodes are direct `Node` variants (not wrapped through a category union), the spec's `literal` and previously-anonymous `programNode.origin` are first wired through to real Rust types, and the optional-scalar / `Option<T>` reconciliation cascades across `codama-nodes`, `codama-attributes`, and `codama-korok-visitors`.

The generator gains four small features. First, `CategoryRouting` is a discriminated union with `wrapped` and `direct` modes; the `direct` mode (used by `topLevel`) deliberately emits no `impl From<Self> for crate::Node`, because the `Node` enum's `#[derive(From)]` auto-generates the impl and an explicit one would conflict. Second, `literal` TypeExpr renders as `String`; the literal value lives in the hand-written `Default` for that node. Third, the Default-derive heuristic now fires for any struct whose fields are all unconditionally Default-able — `Option`/`Vec`/`Docs`/scalars/`String`/`CamelCaseString`-from-`stringIdentifier`/`Box<Option<_>>`. The rule is sound by construction: required `union`/`enumeration`/`node`/`nestedUnion`/`literalUnion` fields and `FIELD_TYPE_OVERRIDES` targets are all treated as opaque and disqualify the struct. As a side effect, 28 previously-no-Default nodes across `link`/`count`/`discriminator`/`value`/`contextualValue` also gain a derived `Default` — pure addition, no consumer breakage. Fourth, a new `generated/codama_version.rs` page emits `pub const CODAMA_VERSION: &str = "<spec.version>";` pinned to the spec version at generation time, mirroring the JS `CODAMA_VERSION` constant used by `rootNode()`; `RootNode::default()` now uses this constant instead of the stale `"1.0.0"` placeholder, bringing the serialized form in line with JS codama.

The reconciliation is the bulk of the diff. Spec-driven type changes: `errorNode.code: u32` (spec width, was `usize`), `accountNode.size: Option<u64>`, `programNode.origin: Option<ProgramOrigin>` (the enum generated in PR #9). The locked optional-scalar rule (optional scalar → `Option<T>` uniformly) flips ~48 read/write sites: `is_optional`, `is_writable` (remaining-accounts only), `is_signer` (remaining-accounts only, optional), `subtract`, `message`, and `optional_account_strategy`. The box-all-union rule wraps every direct union field, and the corresponding constructors gain `Box::new(...)` while tests deref through `*node.field`. The `InstructionByteDeltaValue` inline-union variants rename to `{AccountLink, ArgumentValue, NumberValue, ResolverValue}` (the common PascalCase suffix across its leaves is just `Node`); `InstructionRemainingAccountsValue` already matched the derived rule.

Two nodes keep a hand-written `Default` for structural reasons: `InstructionStatusNode` has a required `lifecycle: InstructionLifecycle` (opaque enum), and `RootNode` pins both `standard: "codama"` and `version: CODAMA_VERSION`. Both are marked `#[allow(clippy::derivable_impls)]`. A new self-import filter in `nodeStructFragment.ts` strips the redundant `crate::Self` import that self-referential nodes (e.g. `instructionNode.subInstructions: Vec<InstructionNode>`) would otherwise accumulate. `ProgramNode::new()` defaults the program's own version to `"0.0.0"` to match the JS factory; `ProgramNode::default()` stays at `""` so `SetProgramMetadataVisitor`'s Cargo.toml-override path (`if program.version.is_empty()`) is unaffected.

Workspace-wide: 1008 cargo tests pass unchanged (incl. the two golden-IDL tests updated for the new `"version": "1.6.0"` line), fmt and clippy stay clean, and `pnpm test` grew by 10 to 140 (new topLevel + Default-heuristic + literal + CODAMA_VERSION coverage). `pnpm generate` round-trips deterministically. The override surface is unchanged (3 maps, `FIELD_TYPE_OVERRIDES` still has 1 entry).
@lorisleiva lorisleiva force-pushed the loris/spec-gen-top-level branch from d17789f to 4906665 Compare June 10, 2026 14:32
@lorisleiva lorisleiva merged commit a922a04 into main Jun 10, 2026
4 checks passed
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.

1 participant