Skip to content

feat(view): allow finalize bodies to call view functions#3253

Open
mohammadfawaz wants to merge 11 commits into
stagingfrom
mohammadfawaz/finalize_calls_query
Open

feat(view): allow finalize bodies to call view functions#3253
mohammadfawaz wants to merge 11 commits into
stagingfrom
mohammadfawaz/finalize_calls_query

Conversation

@mohammadfawaz
Copy link
Copy Markdown
Collaborator

@mohammadfawaz mohammadfawaz commented May 11, 2026

Closes #3252. Builds on #3257 (queryview rename) and #3238 (the read-only feature).

A function's finalize body can now call a view function — same-program or imported. Views remain leaves (cannot call other views).

finalize compute_total:
    input r0 as address.public;
    call my_token.aleo/get_balance r0 into r1;  // call a view
    add r1 100u64 into r2;
    set r2 into total[r0];

Views may also declare zero outputs and serve as cross-program preconditions (Aleo analogue of Solidity's function require_member(address) external view { require(...); }):

view require_member:
    input r0 as address.public;
    get.or_use members[r0] false into r1;
    assert.eq r1 true;

finalize transfer:
    input r0 as address.public;
    call vw_acl.aleo/require_member r0;   // no `into`; aborts the tx if the guard fails
    ...

What's enforced

  • Construction: Finalize::add_command permits Call; call.dynamic rejected via new Command::is_dynamic_call. Constructors still forbid call entirely. View inputs / commands / outputs are all many0 (matches function); the body constraints — no record-touching ops, no state writes, no async/await/call/rand.chacha — are enforced by ViewCore::add_command, not by arity.
  • Type-check: Opcode::Call(_) allowed only if the target resolves to a view (functions / closures → bail); Call::output_types_for_view additionally checks each operand's type against the view's declared input type (cross-program structs resolved via qualify) and verifies that the destination count matches the view's output count (so call vw/guard r0 into r1 against a zero-output view is rejected at deploy); CallDynamic is explicitly bailed in the finalize type-checker (was previously only blocked upstream by Finalize::add_command).
  • Runtime: finalize_command_except_await special-cases Callevaluate_call_to_view (loads inputs from caller registers, runs view body against live store with caller's FinalizeGlobalState, 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 rollup: cost_per_command folds the callee's view_cost_for_single_view into the caller's finalize cost, so TRANSACTION_SPEND_LIMIT bounds the combined cost.
  • V15 gating: Program::contains_v15_syntax flags any call in a finalize body (pre-V15 finalize forbade call). The arity relaxations ride inside the same V15 gate.

Notes

  • View types cached on Stack (mirror of existing finalize_types) so evaluate_call_to_view has cheap structural access.
  • The synthesizer/process/src/view.rs module is now unconditionally compiled; only the external evaluate_view_at_height API + HistoricFinalizeStore stay gated on --features history.
  • In-block calls avoid the 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.
  • The post-view-body finalize_operations.is_empty() invariant is now ensure! (release-time) instead of debug_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.dynamic rejected at parse, view body with call rejected 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 r1 against zero-output view rejected at deploy, zero-command passthrough view parses, fully-empty view parses.

@mohammadfawaz mohammadfawaz marked this pull request as draft May 11, 2026 21:00
@mohammadfawaz mohammadfawaz self-assigned this May 11, 2026
@mohammadfawaz mohammadfawaz force-pushed the mohammadfawaz/finalize_calls_query branch from a3f40b7 to cf6b381 Compare May 13, 2026 13:57
@mohammadfawaz mohammadfawaz changed the title feat(query): allow finalize bodies to call query functions feat(view): allow finalize bodies to call view functions May 13, 2026
@mohammadfawaz mohammadfawaz changed the base branch from staging to mohammadfawaz/rename_query_to_view May 13, 2026 13:57
Base automatically changed from mohammadfawaz/rename_query_to_view to staging May 13, 2026 17:17
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.
@mohammadfawaz mohammadfawaz force-pushed the mohammadfawaz/finalize_calls_query branch from cf6b381 to 1e6645e Compare May 13, 2026 17:24
Comment thread synthesizer/process/src/lib.rs Outdated
Comment thread synthesizer/program/src/finalize/mod.rs Outdated
// 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) {
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 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?

Copy link
Copy Markdown
Collaborator Author

@mohammadfawaz mohammadfawaz May 14, 2026

Choose a reason for hiding this comment

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

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 🤔

Mohammad Fawaz 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.
@mohammadfawaz mohammadfawaz marked this pull request as ready for review May 14, 2026 18:38
@mohammadfawaz mohammadfawaz requested a review from vicsn May 14, 2026 18:38
@mohammadfawaz mohammadfawaz changed the title feat(view): allow finalize bodies to call view functions feat(view): allow finalize bodies to call view functions May 14, 2026
Mohammad Fawaz 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.
@mohammadfawaz mohammadfawaz force-pushed the mohammadfawaz/finalize_calls_query branch from 22250b3 to 0afb918 Compare May 14, 2026 19:35
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.
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.

[Feature] Allow query to be called from function or finalize

2 participants