Accounts cursor runtime design draft
- Status: Implemented foundation
- Related issue: #143
- Related review: Anchor
lang-v2review
This document sketches a concrete path for reworking Pina’s #[derive(Accounts)] runtime around a cursor-based loader, informed by solana-foundation/anchor anchor-next/lang-v2 but adapted to Pina’s explicit-validation and no_std constraints.
Why change the current model?
Today #[derive(Accounts)] mainly expands to direct slice destructuring over &mut [AccountView].
That is simple and easy to audit, but it starts to strain when Pina needs richer account-loading behavior such as:
- nested account groups
- explicit remaining-account cursors
- duplicate mutable alias diagnostics
- future optional/composite account ergonomics
- shared loader logic between entrypoint dispatch and future CPI/codegen paths
Anchor lang-v2 demonstrates that a reusable cursor/loader runtime can support these needs cleanly.
Constraints Pina must preserve
Any redesign must keep the following Pina invariants intact:
- no heap allocation in core on-chain account parsing
no_stdcompatibility- explicit validation order in user code
- discriminator-first fixed layouts
- no weakening of duplicate mutable alias guarantees
- proc-macro output should remain understandable and reviewable
Proposed runtime model
1. Introduce an AccountsCursor<'a>
A lightweight cursor owns:
- the original
&'a mut [AccountView] - the current index
- duplicate-account bookkeeping state
Core operations:
peek()next()remaining()finish_exact()or equivalent exactness check
The cursor should be allocator-free and work entirely with indices and borrowed references.
2. Split parsing from validation
Derive-generated code should stop directly destructuring account slices.
Instead it should:
- parse structural account positions through the cursor
- produce a typed accounts struct of borrowed
&AccountView/&mut AccountView - leave semantic validation in user-authored
process(...)methods
This keeps Pina’s existing style intact:
- parsing answers “which account is where?”
- user code answers “is this account valid for my program logic?”
3. Make duplicate mutable checks first-class in the runtime
The cursor should explicitly track whether a parsed writable account aliases a prior writable account reference.
That allows:
- earlier and clearer failures
- shared duplicate-account logic across all derived structs
- future nested-account support without re-implementing alias checks in generated code paths
4. Model remaining accounts as a cursor view, not only a trailing slice
Current #[pina(remaining)] gives raw trailing access.
The next step should preserve that capability but add a more structured runtime concept:
- either a
RemainingAccounts<'a>wrapper - or a borrow of the cursor with restricted operations
That would make future nested parsing and optional account loading safer and more composable.
Suggested traits
A possible shape is:
pub struct AccountsCursor<'a> {
// borrowed account slice, index, duplicate tracking
}
pub trait ParseAccounts<'a>: Sized {
fn parse_accounts(cursor: &mut AccountsCursor<'a>) -> Result<Self, ProgramError>;
}
Then #[derive(Accounts)] generates ParseAccounts and keeps TryFromAccountInfos as a compatibility layer that delegates to the cursor runtime.
Nested account groups use the same trait: a non-reference field in a derived accounts struct is parsed with <FieldTy as ParseAccounts>::parse_accounts(cursor). That means parent and child loaders share one cursor, preserve left-to-right account order, and run one exactness check only at the outer TryFromAccountInfos boundary. Raw trailing accounts still use #[pina(remaining)], which consumes the cursor tail and intentionally leaves those accounts for user code to inspect.
That gives Pina an incremental migration path:
- existing public trait stays usable
- internal runtime becomes richer
- follow-up derive features have a stable foundation
Migration plan
Phase 1 — Internal cursor introduction
- add
AccountsCursor<'a>behind the current public API - reimplement
TryFromAccountInfosderive output in terms of the cursor - preserve current exact/remaining behavior byte-for-byte where possible
Phase 2 — Explicit duplicate-account runtime checks
- move duplicate mutable alias detection into cursor state
- add adversarial regression tests for nested and repeated-account cases
Phase 3 — Structured remaining accounts
- add a typed remaining-accounts wrapper
- update derive code and docs
- preserve a simple slice-based escape hatch when needed
Phase 4 — Nested/composite account loaders
- allow derive-generated account structs to contain parsed sub-groups
- keep final semantic validation explicit in user code
Testing requirements
This redesign should land with dedicated tests for:
- exact account count handling
- remaining-account passthrough
- duplicate mutable alias rejection
- nested loader ordering
- compatibility with current
ProcessAccountInfosflows - compile-fail coverage for unsupported account-struct shapes
Non-goals
This draft does not propose:
- hiding validation logic behind Anchor-style constraint attributes
- adding allocator-backed parsing helpers to on-chain paths
- making
#[derive(Accounts)]opaque or difficult to inspect - changing the guard-backed typed account loader model
Relationship to the CPI-handle prototype
The cursor-runtime work complements the typed CPI-handle prototype from #142.
Together they point toward a more coherent runtime story:
- cursor-based typed account parsing on entry
- guard-backed typed data access in handlers
- typed CPI-handle composition for outbound invocations
That combination is the main architectural lesson Pina can take from Anchor lang-v2 without copying its heap-backed or asm-oriented design choices.