Skip to content

[1/3] FPI#1113

Merged
bitwalker merged 62 commits into
nextfrom
i697-fpi-codex
Jun 12, 2026
Merged

[1/3] FPI#1113
bitwalker merged 62 commits into
nextfrom
i697-fpi-codex

Conversation

@greenhat

@greenhat greenhat commented May 6, 2026

Copy link
Copy Markdown
Contributor

Ref #697

Adds end-to-end Foreign Procedure Invocation support for SDK account components and note scripts. The new #[foreign_account(package_name1, package_name2)] SDK macro now discover Miden account dependencies from package metadata, synthesize caller-only FPI imports, and generate methods for the struct that let callers invoke dependency methods through execute_foreign_procedure with normal Rust signatures.

  • During binding generation, the macro loads the selected WIT world and injects a synthetic fpi-<method> import for each freestanding method exported by a Miden dependency interface. Each synthetic import prepends the foreign account id prefix, account id suffix, and procedure root word to the original method parameters while preserving the original result type. wit-bindgen then renders these imports as Rust fpi_<method> free functions.
  • After wit-bindgen emits bindings, the macro scans dependency import modules for generated fpi_* functions, loads the dependency .masp package, resolves exported procedure roots, and appends methods to the defined struct. The public wrapper methods remove the internal prefix/suffix/root parameters from the user-facing signature and call the generated fpi_* function with self.0.prefix, self.0.suffix, and the resolved root.
  • The Wasm frontend recognizes imports whose WIT function name starts with fpi- and routes them through dedicated FPI lowering instead of generic component import lowering. For direct canonical ABI calls, it rewrites the inner import call into an HIR exec of miden::protocol::tx::execute_foreign_procedure, reordering the account id to the protocol ABI order, expanding the procedure root word, forwarding flattened procedure inputs, validating the 16-input/16-result limits, and storing multi-felt results through the canonical output pointer when needed.
    For FPI calls whose flattened parameters exceed the direct canonical ABI limit, the canonical ABI passes a single argument tuple pointer, and the generated wrapper function reloads every flattened value from that tuple in HIR (sign-extending narrow signed integers), so the indirect shape never leaves the frontend. From there, every FPI call — direct, indirect, and raw — lowers to the same dedicated hir.exec_fpi op: its operands are the up-to-16 flattened procedure input felts, while the 6-felt executor prefix (account id suffix and prefix, already reordered, plus the procedure root word) is stored into felt function locals referenced by the op. MASM codegen then pads the scheduled inputs with zeros to the fixed 16-felt executor input width, loads the prefix locals on top of the stack, and invokes the real execute_foreign_procedure — no compiler-internal marker callee, no layout attributes, and no stack shuffling beyond the addressable 16-element window.

The note to account example:

...
#[foreign_account(mixed_scalar_struct_account)]
struct CounterContract;
...
#[note]
impl CounterCaller {
    /// Checks that a mixed scalar record crosses the FPI boundary in both directions.
    #[note_script]
    pub fn run(self, _arg: Word) {
        let count_acc = CounterContract::from_account(self.counter_account_id);
        let result = count_acc.echo_mixed_scalar_record(MixedScalarRecord {
            first_u64: FIRST_U64,
            second_u64: SECOND_U64,
            felt_value: felt!(77),
            u32_value: U32_VALUE,
            u8_value: U8_VALUE,
        });

See tests/integration-network/src/mockchain/fpi/note/mixed_scalar_struct.rs for the complete example.

The account to account example:

#[foreign_account(account_to_account_callee_account)]
struct CalleeAccount;

/// Account component which forwards reads to another account through FPI.
#[component]
struct CallerAccount;

#[component]
impl CallerAccount {
    /// Reads a counter value from the provided foreign account.
    pub fn read_foreign_count(&self, callee_account_id: AccountId) -> Felt {
        let callee = CalleeAccount::from_account(callee_account_id);
        let key = Word::new([felt!(13), felt!(21), felt!(34), felt!(55)]);
        callee.get_count(key)
    }
}

See tests/integration-network/src/mockchain/fpi/account/account_to_account.rs for the complete example.

The note to account example with multiple packages:

#[foreign_account(multiple_packages_first_account, multiple_packages_second_account)]
struct ForeignCounters;

/// Note script input containing the foreign account id.
#[note]
struct CounterCaller {
    /// Account id with both counter components deployed.
    foreign_account_id: AccountId,
}

#[note]
impl CounterCaller {
    /// Checks that a multi-package foreign account binding exposes both component methods.
    #[note_script]
    pub fn run(self, _arg: Word) {
        let counters = ForeignCounters::from_account(self.foreign_account_id);

        let first = counters.get_first_count();
        let second = counters.get_second_count();
...

This PR also adds raw bindings for the kernel execute_foreign_procedure procedure so that the MASM account methods can be called via FPI:

...
let outputs = tx::execute_foreign_procedure(
    self.foreign_account_id,
    procedure_root,
    inputs,
);
...

@greenhat greenhat force-pushed the i697-fpi-codex branch 7 times, most recently from e843cf7 to c30223a Compare May 14, 2026 08:03
@greenhat greenhat changed the base branch from next to cse-non-determinism-fix May 14, 2026 08:04
@greenhat greenhat marked this pull request as ready for review May 15, 2026 10:57
@greenhat greenhat marked this pull request as draft May 15, 2026 10:57
greenhat added 15 commits May 15, 2026 16:24
Account components now emit fpi-prefixed WIT methods, and note bindings generate typed dependency wrappers that embed exported procedure roots from dependency packages.

Lower FPI imports through the transaction kernel execute_foreign_procedure ABI, with MASM lowering adding the unused zero slots after scheduling the real operands so the generic operand scheduler avoids a 22-input call. Add the counter-caller note example and mockchain coverage that deploys a counter contract, invokes it through FPI, and consumes the note.

Updated affected integration snapshots for the expanded counter and wallet artifacts. Verified with cargo make test, cargo make clippy, and cargo make format-rust.
FPI helper functions should not be part of a component's exported WIT surface because that forces the callee bindings to implement extra guest trait methods.

Keep component WIT exports to the user-defined methods and synthesize the fpi-prefixed functions only for imported Miden dependency interfaces before wit-bindgen emits caller bindings. This preserves the CounterContract caller wrapper while keeping counter-contract bindings free of FPI exports.

Verified with cargo check -p miden-base-macros, the counter_caller_note_reads_counter_through_fpi integration test, cargo make format-rust, and git diff --check.
The SDK macro code used separate local literals for the WIT fpi- prefix and the Rust fpi_ identifier prefix.

Move those prefixes into a shared FPI module and use the constants at the import injection and wrapper collection call sites. This keeps the naming contract in one place before extracting the rest of the FPI binding code.

Verified with cargo check -p miden-base-macros, cargo make format-rust, and git diff --check.
The generate! macro file had accumulated the FPI-specific import synthesis, package artifact lookup, procedure-root extraction, and typed wrapper generation code.

Move that behavior into the SDK macro FPI module and leave generate.rs responsible for generic wit-bindgen setup and account wrapper augmentation. The public surface between the modules is limited to injecting FPI imports, augmenting bindings, and identifying generated FPI functions.

Verified with cargo check -p miden-base-macros, the counter_caller_note_reads_counter_through_fpi integration test, cargo make format-rust, and git diff --check.
Removing generated FPI methods from callee exports shrinks the affected account packages and cycle counts back down to their caller-only values.

Update the expect-test baselines produced by the targeted UPDATE_EXPECT runs so the integration tests reflect the reviewed binding generation shape.
The FPI mockchain test only needs a no-authentication account component for the deployed counter account. Compiling the local auth-component-no-auth example adds unnecessary setup and keeps the test tied to that fixture.

Build the counter account with miden_standards::account::auth::NoAuth directly, while reusing the standard BasicWallet component import for both accounts. This keeps the test focused on FPI behavior and avoids the extra auth component package build.
The FPI integration test depended on the checked-in counter-caller example, which shared generated WIT and Miden package outputs with other tests compiling the counter contract. Under parallel test execution that can leave the caller with a foreign procedure root from a different counter package build.

Generate simplified counter contract and caller note projects inside the test using the target-backed cargo project helper, so their artifacts live under unique target paths. Remove the standalone counter-caller example now that the integration test owns the fixture source directly.
The FPI mock-chain tests are about to grow beyond the single no-argument caller case, so keeping the existing fixture in the module root would make later variants harder to organize.

Move the existing no-argument counter caller test and its generated-project fixture helpers into fpi/no_arg.rs, leaving fpi.rs as the FPI module root. The test behavior is unchanged; this is only a module layout split for upcoming FPI coverage.
The FPI mock-chain coverage only exercised a no-argument account method, so it did not verify that generated foreign procedure wrappers forward structured user arguments correctly.

Move the shared generated-project and mock-chain setup into an FPI-local fixture module, keep the existing no-arg case intact, and add a Word-to-Felt counter lookup through get_count_by_key using a non-zero storage key. This gives the caller note an end-to-end check that a Word argument reaches the foreign account method.
FPI methods returning composite values lower through the canonical ABI with an output pointer rather than direct core return values. The previous validation and executor wiring treated that pointer as a procedure input, which made Word-returning calls fail before execution.

Detect the canonical FPI output pointer from the lowered component signature, keep it out of the executor inputs, and store the returned Felts back through the pointer. Add a mock-chain test that calls a Word-to-Word foreign procedure to cover this lowering path.
FPI coverage now includes Word arguments and Word results, but not a custom record result that has to be flattened through the canonical ABI output path.

Add a mock-chain test that generates an isolated counter contract with a two-Word argument method returning an exported WordPair record. The caller note invokes the generated FPI wrapper and checks both returned words, exercising record result lowering without reusing the scalar counter helper.
The FPI integration tests generate temporary account and note crates, and the previous names were long enough to make target paths and binding aliases noisy.

Derive the generated crate, component package, and dependency names from each one-test module instead. Because storage slot names include the component package namespace, pass the generated account storage slot through the shared helpers so each renamed account still initializes and verifies the right storage map.

Verified with cargo test -p midenc-integration-network-tests mockchain::fpi -- --nocapture, cargo make test, cargo make clippy, and cargo make format-rust.
The two-word FPI coverage still used two separate Word parameters, so it did not exercise record arguments containing multiple words.

Change the generated counter account fixture to export a KeyPair record and accept that single value when reading the two storage keys. The caller note now constructs the generated KeyPair binding and passes it through FPI while keeping the WordPair return assertion intact.

Verified with cargo test -p midenc-integration-network-tests mockchain::fpi::two_words_struct::two_words_struct -- --nocapture, cargo make test, cargo make clippy, and cargo make format-rust.
FPI imports with more than 16 flattened parameters are lowered by the canonical ABI to an argument pointer, which means the backend cannot infer the original tuple length from the call signature alone.

Carry the flattened argument count on the generated hir.exec op and use a stable internal indirect executor path. The MASM lowering reads that per-call attribute, expands the canonical ABI tuple pointer into the protocol execute_foreign_procedure stack shape, and preserves the account suffix/prefix ordering.

Add a three-word struct FPI integration test to exercise the arg-pointer path. Verified with cargo make test, cargo make clippy, and cargo make format-rust.
greenhat added 5 commits May 21, 2026 09:14
Plain generate! expansion only needs dependency WIT metadata when Miden dependencies are present. Loading the full component manifest forced crates without miden-project.toml, or executable-only projects, through library-target validation before FPI was relevant.

Introduce lightweight project package metadata for binding generation and WIT path resolution. Missing project manifests now behave as empty dependency metadata, while real project manifests still load through miden-project so dependency WIT imports are preserved.
Typed FPI validation allowed values such as wide integers, f64, function types, unknown/never, and non-C-like enums to reach canonical flattening. Those paths could panic or produce generic lowering errors before users saw an FPI-specific diagnostic.

Tighten the typed signature check to accept only values that the felt ABI can encode, and add tests for direct parameters, indirect parameters, enum payloads, and results. Unsupported signatures now fail before flattening with an explicit typed FPI error.
FPI bindings were previously emitted implicitly from the component and note macros, which made dependency bindings appear as a side effect of unrelated macro expansion. This introduces the explicit #[foreign_account(...)] macro on an empty struct and moves selected dependency exports onto that struct instead.

The macro resolves the requested packages from miden-project metadata, generates the hidden binding world for those imports, maps dependency-owned WIT types back to the normal crate bindings, and loads procedure roots from the dependency package for wrapper generation. Component and note macros now only emit their own bindings, and the FPI integration fixtures opt in through the new macro.

Dependency WIT metadata is now parsed with wit-parser rather than line scanning. That keeps package/interface/type resolution aligned with WIT semantics, preloads the bundled SDK WIT for core type imports, and filters out use-imported core types so only dependency-owned types participate in with mappings.
The explicit foreign_account macro supports listing more than one dependency package, but the mock-chain coverage only exercised a single generated package at a time.

Add a note-to-account fixture that builds two account-component packages, deploys both components onto the same foreign account, and calls methods from both packages through one foreign account binding. The shared FPI test helper now has multi-dependency manifest generation so the fixture mirrors the macro shape it is validating.
The foreign_account macro accepted only dependency identifiers, but the empty-attribute error and docs did not clearly describe that those identifiers are Rust-style Miden package names.

Validate the dependency list before struct shape checks so foreign_account() reports the missing dependency directly. Expand the empty-attribute and non-empty-struct diagnostics, and document the dashed package to Rust identifier mapping at the macro entry points.
@greenhat greenhat marked this pull request as ready for review May 21, 2026 12:38
@greenhat greenhat requested a review from bitwalker May 21, 2026 12:38
@greenhat

greenhat commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

@bitwalker @bobbinth I finished the FPI implementation with the #[foreign_account(package_name1, package_name2)] attribute macro we discussed in #697 (comment). Check out the PR description and tests for examples.

EDIT: We support account-to-account, note-to-account, and direct execute_foreign_procedure calls (for MASM accounts).

@greenhat

greenhat commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Following our discussion to go with the 2.1 #[account(...)] macro for both current and foreign accounts, I renamed the #[foreign_account(...)] macro to #[account(...)] in 9966e55

@bitwalker bitwalker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still working my way through this, but I think some additional documentation in the codegen backend would be helpful here, as there are a few aspects that aren't super clear to me that I suspect documentation would fix.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the module doc here elaborate a bit more on how FPI is handled during lowering? In particular, the aspects related to ABI handling, but I would also document any assumptions/expectations on code that triggers the FPI handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d9c4587

Clarify how FPI lowering maps typed component imports onto the transaction kernel executor ABI.

Document the fixed-width executor operands, the direct and indirect call shapes, and why the compiler-internal indirect marker exists when canonical ABI lowers more than sixteen flattened parameters through memory while FPI itself remains capped at sixteen procedure input felts.
Comment thread codegen/masm/src/lower/fpi.rs Outdated
//! operands directly on the `hir.exec`. The frontend omits unused trailing procedure input slots
//! so the generic operand scheduler only solves for the real operands. After scheduling those
//! operands, this module pads the stack with zero felts until it reaches the 22-felt executor
//! width. The 16-operand boundary needs one backend scratch local because MASM stack shuffling can

@bitwalker bitwalker Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be simpler if we arranged to store the first 6 operands (account id, procedure root, FPI context) in locals, and then load them on to the operand stack manually in the lowering of an hir.exec that targets execute_foreign_procedure after padding the stack out to 16 elements, thereby getting all 22 operands on the operand stack with minimal fuss.

With only a single scratch local, it takes a lot more shuffling to get things into place.

This also seems to me to be a good fit for a special instruction specifically for FPI, that largely looks like hir.exec, but rather than the first 6 operands (3 values) being SSA values, they would be references to 3 local variables that are stored to prior to the FPI instruction. Lowering for the new instruction would then be able to guarantee that up to 16 operands would be on the operand stack on entry to the lowering of that op, and the lowering could manually load the values of the 3 local variables on top of the operand stack in the correct order. The only special handling that would be needed in the lowering code would be the padding aspect.

What do you think? It's a bit of an adjustment, but I think it would make lowering more straightforward, more efficient, and a bit easier to reason about (not to mention it would avoid complicating the lowering for hir.exec.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! Thank you! Implemented in the last two commits (6119c12 and 39dafb8)

@greenhat greenhat marked this pull request as draft June 11, 2026 05:53
greenhat added 2 commits June 11, 2026 11:16
Direct FPI lowering previously carried all 22 executor operands through the operand stack, so the 16-operand boundary case needed a reserved backend scratch slot and ~100 instructions of movup shuffling, while the indirect shape leaked into the backend as a compiler-internal marker symbol with layout attributes.

Introduce hir.exec_fpi, which takes only the flattened procedure input felts as operands and references six felt function locals holding the executor prefix (account id suffix, account id prefix, procedure root). The generated wrapper stores the prefix into those locals for every FPI shape: the direct path uses its flattened arguments, while the indirect and raw paths reload the canonical ABI tuple in HIR. Lowering then pads the scheduled inputs to the fixed 16-felt width with one push/movdn pair per slot and loads the prefix on top, so no call shape needs shuffling past the addressable stack window. The marker symbol, the layout attributes, the scratch-slot machinery, and the FPI special-casing in hir.exec are all removed, and local2reg learns to pin locals that exec_fpi reads through its attribute so their stores are not treated as dead.

Replace the former direct-path felt-count limit with a check of the real constraint it was masking: the wrapper invocation itself cannot pass more than 16 operand stack felts, including the canonical ABI output pointer. This also turns a latent spill-analysis panic for 16-felt signatures with an output pointer into a proper diagnostic; the six-u64 record rejection test now asserts the new message.
FPI validation was split awkwardly across the lowering path: the wrapper stack-width check ran inline while arguments were being lowered, and the remaining protocol checks ran only after the conversion ops had already been emitted, against the produced SSA values.

Introduce a pure FpiCallShape planning step that computes the direct/indirect decision and the flattened felt counts from types alone, and runs every validation up front before any IR is built. Argument emission now just consumes the validated shape, with a single consistency check that the produced felt count matches the plan. This also makes the validation unit-testable without constructing IR values, and adds direct coverage for the wrapper stack-window rejection that previously only an integration test exercised.

Also tighten the exec_fpi builder to take exactly six prefix locals as a fixed-size array instead of an arbitrary iterator, and move store_fpi_prefix_locals into a shared crate-level fpi module so the raw FPI linker stub transform no longer reaches into the component import lowering module.
@greenhat greenhat marked this pull request as ready for review June 11, 2026 12:50
@greenhat greenhat requested a review from bitwalker June 11, 2026 12:50
@greenhat greenhat changed the title [1/2] FPI [1/3] FPI Jun 11, 2026
implements(InferTypeOpInterface, MemoryEffectOpInterface, OpPrinter)
)]
#[effects(MemoryEffect(MemoryEffect::Read, MemoryEffect::Write))]
pub struct ExecFpi {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't help but feel it would be better if we could somehow use the "real" signature of the foreign procedure somehow, probably by storing the type signature in an attribute and making the operands/results AnyType - but I see that more as a future improvement - this will do for now I think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I made #1176.

@bitwalker bitwalker merged commit f966c05 into next Jun 12, 2026
18 checks passed
@bitwalker bitwalker deleted the i697-fpi-codex branch June 12, 2026 03:15
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.

Foreign Procedure Invocation (FPI) support

2 participants