feat(view): allow finalize bodies to call view functions#3253
Open
mohammadfawaz wants to merge 11 commits into
Open
feat(view): allow finalize bodies to call view functions#3253mohammadfawaz wants to merge 11 commits into
view functions#3253mohammadfawaz wants to merge 11 commits into
Conversation
a3f40b7 to
cf6b381
Compare
Adds Eth-`view`-style invocation: a function's `finalize` body can call a view function (same-program or imported cross-program). Views themselves remain leaves — they still reject `is_call` at construction, so no recursion. Changes: - Cache view `FinalizeTypes` on `Stack` (previously recomputed per call). - Loosen `Finalize::add_command` to permit `call`; keep `call.dynamic` rejected. New `Command::is_dynamic_call` helper. - Type-check (`check_instruction_opcode`) resolves the target and allows `Opcode::Call` only when it lands on a view. `Call::output_types` gains a view branch alongside closure / function. - Runtime dispatch: `finalize_command_except_await` special-cases `Command::Instruction(Instruction::Call(_))` and defers to a new `evaluate_call_to_view` helper that loads inputs from the caller's registers, runs the view body against the live store with the caller's inherited `FinalizeGlobalState`, and writes outputs to the caller's destination registers. - Cost rollup: `cost_per_command` now folds the called view's `view_cost_for_single_view` into the caller's finalize cost, so the per-function `TRANSACTION_SPEND_LIMIT` check covers callee compute too. - V15 syntax gate: `Program::contains_v15_syntax` flags any `call` inside a finalize body (pre-V15 finalize forbade `call` entirely). - The view module is now unconditionally compiled (the in-block call path doesn't need the `history` feature); only the `evaluate_view_at_height` public API and `HistoricFinalizeStore` remain gated.
…output, struct return)
cf6b381 to
1e6645e
Compare
vicsn
requested changes
May 13, 2026
| // Views are externally-callable and declared as `FinalizeType::Plaintext(_)` for both | ||
| // inputs and outputs (enforced by `ViewCore::add_input`/`add_output`); we therefore | ||
| // pattern-match `FinalizeType::Plaintext` directly when constructing the `RegisterType`. | ||
| if let Ok(view) = program.get_view_ref(name) { |
Collaborator
There was a problem hiding this comment.
Could it be worth creating a new FinalizeCall operation, giving us extra clarity and assurance that old edge case Call behaviour doesn't leak into the finalize-version of doing calls? Or would it just be a bunch of code duplication for little benefit?
Collaborator
Author
There was a problem hiding this comment.
Addressed in 0593427 — moved the view-target handling into a dedicated Call::output_types_for_view so the transition-side Call::output_types never sees views. Creating a separate FinalizeCall doesn't feel worth it 🤔
added 3 commits
May 14, 2026 13:36
Drop the verbose explanation on the unconditional 'view' module declaration and correct the rationale for forbidding 'call.dynamic' in finalize: the real blocker is that we have not yet designed dynamic spend / gas tracking for runtime-resolved targets.
Restore Call::output_types to its original transition-only behavior and introduce Call::output_types_for_view for the finalize-side view-call type-check. Finalize-types initialize dispatches Instruction::Call to the new helper, making the API surface self-document the split and removing the risk of a future transition-path caller accidentally accepting a view target through the generic output_types entry.
view functions
added 2 commits
May 14, 2026 15:34
Bail explicitly on `CallDynamic` in the finalize type-checker, add a per-operand input-type check in `Call::output_types_for_view`, and promote the post-view `finalize_operations.is_empty()` invariant to a release-time `ensure!`. Tests cover destination-count and input-type mismatches at deploy, runtime view failure, and branch interactions.
Relax `many1` → `many0` on view outputs in the parser, `FromBytes`/`ToBytes`, and `Program::add_view`. Enables Solidity-style cross-program precondition guards from finalize; rides the existing V15 gate.
22250b3 to
0afb918
Compare
Relax `many1` → `many0` on view commands in the parser, `FromBytes`/`ToBytes`, and `Program::add_view`. Brings view arity in line with `function` (all-`many0`) and removes an arbitrary restriction; no use case in mind, just consistency. Rides the existing V15 gate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3252. Builds on #3257 (
query→viewrename) and #3238 (the read-only feature).A function's
finalizebody can nowcalla view function — same-program or imported. Views remain leaves (cannot call other views).Views may also declare zero outputs and serve as cross-program preconditions (Aleo analogue of Solidity's
function require_member(address) external view { require(...); }):What's enforced
Finalize::add_commandpermitsCall;call.dynamicrejected via newCommand::is_dynamic_call. Constructors still forbidcallentirely. View inputs / commands / outputs are allmany0(matchesfunction); the body constraints — no record-touching ops, no state writes, noasync/await/call/rand.chacha— are enforced byViewCore::add_command, not by arity.Opcode::Call(_)allowed only if the target resolves to a view (functions / closures → bail);Call::output_types_for_viewadditionally checks each operand's type against the view's declared input type (cross-program structs resolved viaqualify) and verifies that the destination count matches the view's output count (socall vw/guard r0 into r1against a zero-output view is rejected at deploy);CallDynamicis explicitly bailed in the finalize type-checker (was previously only blocked upstream byFinalize::add_command).finalize_command_except_awaitspecial-casesCall→evaluate_call_to_view(loads inputs from caller registers, runs view body against live store with caller'sFinalizeGlobalState, writes outputs to caller destinations). Zero-output views run their body to completion, write nothing back; an assertion failure inside the body propagates as a finalize rejection.cost_per_commandfolds the callee'sview_cost_for_single_viewinto the caller's finalize cost, soTRANSACTION_SPEND_LIMITbounds the combined cost.Program::contains_v15_syntaxflags anycallin a finalize body (pre-V15 finalize forbadecall). The arity relaxations ride inside the same V15 gate.Notes
Stack(mirror of existingfinalize_types) soevaluate_call_to_viewhas cheap structural access.synthesizer/process/src/view.rsmodule is now unconditionally compiled; only the externalevaluate_view_at_heightAPI +HistoricFinalizeStorestay gated on--features history.Stack-snapshot caveat from docs/test(query): qualify Stack snapshot claim + add name-collision tests #3250 — the caller's finalize is already mid-atomic-batch.finalize_operations.is_empty()invariant is nowensure!(release-time) instead ofdebug_assert!, so a regression that lets a write through the view type-check fails closed.Tests (
test_v15/views.rs+view/parse.rs)Same-program and cross-program lifecycle (incl. repeated cross-program calls), non-view target rejected at deploy, arity / destination-count / input-type / destination-type mismatches rejected at deploy, runtime view failure inside a finalize call rejects the tx with batch rollback, multiple-call + interleaved write read-write-read, multi-output + struct return, single-call-only finalize body, zero-input view from finalize, view-call inside
branch.eq(skip + not-skip paths), V15 bidirectional gate,call.dynamicrejected at parse, view body withcallrejected at parse, zero-output guard view (assertion pass: caller's finalize proceeds; assertion fail: tx finalize-rejected, no state change),call vw/guard r0 into r1against zero-output view rejected at deploy, zero-command passthrough view parses, fully-empty view parses.