Skip to content

Commit 4e63216

Browse files
author
Mohammad Fawaz
committed
feat(view): add read-only view fn entry points (V15)
Implements V15 `view fn` block: a top-level program component that reads finalize-store state off-consensus and returns plaintext to external callers. Allowed reads cover mappings, vectors, storage, `block.height`, and `network.id`; writes, `async`, `call`, and `block.timestamp` are rejected. View fns are leaves (snarkVM rejects `call` inside a view body) but can be called from `final {}` blocks, `final fn` helpers, and hoisted `Finalize` bodies. Cross-program views go through `is_cross_program_call_target` to survive the inliner and monomorphizer. Interface conformance enforces View↔View and Fn↔(Fn|EntryPoint). Bumps snarkVM to 088684ed (PR #3253 head): finalize-calls-view plus `many0` view bodies, so empty views compile to valid Aleo bytecode with no dummy filler. closes #29419
1 parent 16323d2 commit 4e63216

178 files changed

Lines changed: 3180 additions & 1161 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 396 additions & 376 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ num-traits = "0.2"
5353
once_cell = "1.21"
5454
parking_lot = "0.12"
5555
paste = "1.0"
56-
rand = { version = "0.8", default-features = false }
57-
rand_chacha = { version = "0.3", default-features = false }
56+
rand = { version = "0.10", default-features = false }
57+
rand_chacha = { version = "0.10", default-features = false }
5858
rayon = "1.11"
5959
regex = "1.11"
6060
reqwest = { version = "0.13", features = ["blocking", "json"] }
@@ -68,7 +68,14 @@ serial_test = "3.1"
6868
sha2 = "0.10"
6969
similar = "2.6"
7070
# We enable "test_consensus_heights" to give developers freedom to set custom consensus version upgrade heights and "test_targets" because the devnode's genesis block was created with the same feature flag.
71-
snarkvm = { version = "4.6.0", features = [ "test_consensus_heights", "dev_skip_checks", "test_targets" ] }
71+
# TODO: switch back to a published version once a release is cut after snarkVM PRs #3238 (read-only `view` functions, V15), #3257 (`query` → `view` rename), and #3253 (finalize-calls-view) merge into `staging`.
72+
# `history` is required to compile `Process::evaluate_view_with_history`, the public-facing view entry point.
73+
snarkvm = { git = "https://github.com/ProvableHQ/snarkVM.git", rev = "56c0f46e1f8c41738f6df720b2d1510b2b2a664e", features = [ "test_consensus_heights", "dev_skip_checks", "test_targets", "history" ] }
74+
# Wasm-friendly snarkVM subsets pinned at the same rev as the umbrella crate; used by
75+
# `leo-ast` and `leo-disassembler` on `wasm32-unknown-unknown` where the full `snarkvm`
76+
# crate pulls in native-only ledger/package code.
77+
snarkvm-console = { git = "https://github.com/ProvableHQ/snarkVM.git", rev = "56c0f46e1f8c41738f6df720b2d1510b2b2a664e", default-features = false, features = [ "account", "program", "types", "dev_skip_checks", "test_consensus_heights", "test_targets", "wasm" ] }
78+
snarkvm-synthesizer-program = { git = "https://github.com/ProvableHQ/snarkVM.git", rev = "56c0f46e1f8c41738f6df720b2d1510b2b2a664e", features = [ "wasm" ] }
7279
sys-info = "0.9"
7380
tempfile = "3.20"
7481
thiserror = "2.0"

crates/abi-types/src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ pub struct Program {
5151
/// Public entry points (program functions only, not internal helpers).
5252
/// Compiled to Aleo `transition`s.
5353
pub functions: Vec<Function>,
54+
/// Read-only `view fn` entry points (V15). Compiled to Aleo `view` blocks.
55+
/// Off-consensus, plaintext-only inputs and outputs, no transactions or fees.
56+
/// Defaults to empty for backwards compatibility with pre-V15 ABI consumers.
57+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
58+
pub views: Vec<Function>,
5459
}
5560

5661
/// The ABI for a single Leo interface.
@@ -65,8 +70,11 @@ pub struct Interface {
6570
pub path: Path,
6671
/// Inherited interfaces, by reference. Not flattened.
6772
pub parents: Vec<InterfaceRef>,
68-
/// Locally declared function prototypes.
73+
/// Locally declared function prototypes (`fn`).
6974
pub functions: Vec<Function>,
75+
/// Locally declared view-function prototypes (`view fn`).
76+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
77+
pub views: Vec<Function>,
7078
/// Locally declared record prototypes.
7179
pub records: Vec<Record>,
7280
/// Locally declared mapping prototypes.

crates/abi/src/aleo.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,15 @@ pub fn generate(aleo: &ast::AleoProgram) -> abi::Program {
9191
.map(|(_, f)| convert_function_stub(f, &record_names))
9292
.collect();
9393

94+
let views = aleo
95+
.functions
96+
.iter()
97+
.filter(|(_, f)| f.variant.is_view())
98+
.map(|(_, f)| convert_function_stub(f, &record_names))
99+
.collect();
100+
94101
let mut program =
95-
abi::Program { program, implements: vec![], structs, records, mappings, storage_variables, functions };
102+
abi::Program { program, implements: vec![], structs, records, mappings, storage_variables, functions, views };
96103

97104
// Prune types not used in the public interface.
98105
prune_non_interface_types(&mut program);

crates/abi/src/interfaces.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,20 @@ fn build_interface(
382382
let parents: Vec<abi::InterfaceRef> =
383383
iface.parents.iter().filter_map(|(_, ty)| interface_ref_from_type(ty, &program)).collect();
384384

385-
let functions: Vec<abi::Function> =
386-
iface.functions.iter().map(|(_, proto)| convert_function_prototype(proto, iface, cs)).collect();
385+
// Split prototypes by variant so view fns appear in their own ABI bucket,
386+
// parallel to how `Program.functions` and `Program.views` are split.
387+
let functions: Vec<abi::Function> = iface
388+
.functions
389+
.iter()
390+
.filter(|(_, proto)| !proto.variant.is_view())
391+
.map(|(_, proto)| convert_function_prototype(proto, iface, cs))
392+
.collect();
393+
let views: Vec<abi::Function> = iface
394+
.functions
395+
.iter()
396+
.filter(|(_, proto)| proto.variant.is_view())
397+
.map(|(_, proto)| convert_function_prototype(proto, iface, cs))
398+
.collect();
387399

388400
let records: Vec<abi::Record> = iface.records.iter().map(|(_, proto)| convert_record_prototype(proto)).collect();
389401

@@ -393,9 +405,18 @@ fn build_interface(
393405
iface.storages.iter().map(convert_storage_variable_prototype).collect();
394406

395407
// Collect transitively referenced structs from the composite source.
396-
let structs = collect_interface_structs(&program, &functions, &records, &mappings, &storage_variables, cs);
397-
398-
abi::Interface { name, program, path, parents, functions, records, mappings, storage_variables, structs }
408+
// Pass both `functions` and `views` so that types referenced only by view
409+
// fn signatures are still pulled into the interface's struct set.
410+
let structs = collect_interface_structs(
411+
&program,
412+
functions.iter().chain(views.iter()),
413+
&records,
414+
&mappings,
415+
&storage_variables,
416+
cs,
417+
);
418+
419+
abi::Interface { name, program, path, parents, functions, views, records, mappings, storage_variables, structs }
399420
}
400421

401422
// ------------------------------------------------------------------------- //
@@ -501,17 +522,17 @@ fn convert_function_output(ty: &ast::Type, iface: &ast::Interface, cs: &Composit
501522
///
502523
/// Only includes structs defined in the same program as the interface; external
503524
/// struct references remain as `StructRef` with a program field.
504-
fn collect_interface_structs(
525+
fn collect_interface_structs<'a>(
505526
program_name: &str,
506-
functions: &[abi::Function],
527+
functions: impl IntoIterator<Item = &'a abi::Function>,
507528
records: &[abi::Record],
508529
mappings: &[abi::Mapping],
509530
storage_variables: &[abi::StorageVariable],
510531
cs: &CompositeSource<'_>,
511532
) -> Vec<abi::Struct> {
512533
let mut used_types: HashSet<abi::Path> = HashSet::new();
513534

514-
// Seed from functions.
535+
// Seed from functions (and views — caller chains them in).
515536
for function in functions {
516537
for cp in &function.const_parameters {
517538
collect_from_plaintext(&cp.ty, program_name, &mut used_types);

crates/abi/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,14 @@ pub fn generate(ast: &ast::Program) -> abi::Program {
8080
let functions =
8181
scope.functions.iter().filter(|(_, f)| f.variant.is_entry()).map(|(_, f)| convert_function(f, &ctx)).collect();
8282

83+
let views =
84+
scope.functions.iter().filter(|(_, f)| f.variant.is_view()).map(|(_, f)| convert_function(f, &ctx)).collect();
85+
8386
let implements: Vec<abi::InterfaceRef> =
8487
scope.parents.iter().filter_map(|(_, ty)| interface_ref_from_type(ty, &program)).collect();
8588

86-
let mut program = abi::Program { program, implements, structs, records, mappings, storage_variables, functions };
89+
let mut program =
90+
abi::Program { program, implements, structs, records, mappings, storage_variables, functions, views };
8791

8892
// Prune types not used in the public interface.
8993
prune_non_interface_types(&mut program);
@@ -310,7 +314,7 @@ pub fn prune_non_interface_types(program: &mut abi::Program) {
310314
let program_name = &program.program;
311315

312316
// Phase 1: Collect from interface items
313-
for function in &program.functions {
317+
for function in program.functions.iter().chain(program.views.iter()) {
314318
for input in &function.inputs {
315319
collect_from_function_input(&input.ty, program_name, &mut used_types);
316320
}

crates/ast/Cargo.toml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,5 @@ snarkvm = { workspace = true }
3636

3737
[target.'cfg(target_arch = "wasm32")'.dependencies]
3838
getrandom = { version = "0.2", features = [ "js" ] }
39-
snarkvm-console = { version = "=4.6.1", default-features = false, features = [
40-
"account",
41-
"program",
42-
"types",
43-
"dev_skip_checks",
44-
"test_consensus_heights",
45-
"test_targets",
46-
"wasm",
47-
] }
48-
snarkvm-synthesizer-program = { version = "=4.6.1", features = [ "wasm" ] }
39+
snarkvm-console = { workspace = true }
40+
snarkvm-synthesizer-program = { workspace = true }

crates/ast/src/functions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ impl fmt::Display for Function {
140140
Variant::Fn => write!(f, "fn ")?,
141141
Variant::Finalize => write!(f, "finalize ")?,
142142
Variant::EntryPoint => write!(f, "fn ")?,
143+
Variant::View => write!(f, "view fn ")?,
143144
}
144145

145146
write!(f, "{}", self.identifier)?;

crates/ast/src/functions/variant.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,25 @@ use serde::{Deserialize, Serialize};
1818

1919
use std::fmt;
2020

21-
/// Functions are always one of six variants.
22-
/// A transition function is permitted the ability to manipulate records.
23-
/// An asynchronous transition function is a transition function that calls an asynchronous function.
24-
/// A regular function is not permitted to manipulate records.
25-
/// An asynchronous function contains on-chain operations.
26-
/// An inline function is directly copied at the call site.
21+
/// The kind of a function definition.
22+
///
23+
/// - `Fn`: a regular function, callable from other Leo code.
24+
/// - `FinalFn`: a `final fn`, runs in the on-chain finalize context.
25+
/// - `EntryPoint`: a top-level program function — compiles to an Aleo
26+
/// `transition`. May or may not have a `final {}` block.
27+
/// - `Finalize`: the synthesized `final {}` block of an `EntryPoint`. Created
28+
/// during compilation, not written by the user.
29+
/// - `View`: a read-only `view fn` (V15). Top-level program component that
30+
/// reads finalize-store state and returns plaintext to external callers.
31+
/// Off-consensus, no transitions, no proofs, no state writes.
2732
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
2833
pub enum Variant {
2934
#[default]
3035
Fn,
3136
FinalFn,
3237
EntryPoint,
3338
Finalize,
39+
View,
3440
}
3541

3642
impl Variant {
@@ -46,6 +52,17 @@ impl Variant {
4652
pub fn is_onchain(self) -> bool {
4753
matches!(self, Variant::Finalize | Variant::FinalFn)
4854
}
55+
56+
/// Returns true if the variant is a view function.
57+
pub fn is_view(self) -> bool {
58+
matches!(self, Variant::View)
59+
}
60+
61+
/// Returns true if the variant is callable from outside the program
62+
/// (transition entry point or view).
63+
pub fn is_externally_callable(self) -> bool {
64+
matches!(self, Variant::EntryPoint | Variant::View)
65+
}
4966
}
5067

5168
impl fmt::Display for Variant {
@@ -55,6 +72,7 @@ impl fmt::Display for Variant {
5572
Self::Fn => write!(f, "fn"),
5673
Self::EntryPoint => write!(f, "entry"),
5774
Self::Finalize => write!(f, "finalize"),
75+
Self::View => write!(f, "view fn"),
5876
}
5977
}
6078
}

crates/ast/src/interface/prototypes.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use crate::{
2727
Output,
2828
TupleType,
2929
Type,
30+
Variant,
3031
indent_display::Indent,
3132
};
3233
use itertools::Itertools;
@@ -97,6 +98,9 @@ crate::simple_node_impl!(StorageVariablePrototype);
9798
pub struct FunctionPrototype {
9899
/// Annotations on the function.
99100
pub annotations: Vec<Annotation>,
101+
/// `Variant::Fn` for a plain `fn name(...)` prototype, `Variant::View` for `view fn name(...)`.
102+
/// Other variants are not allowed in interface position.
103+
pub variant: Variant,
100104
/// The function identifier, e.g., `foo` in `function foo(...) { ... }`.
101105
pub identifier: Identifier,
102106
/// The function's const parameters.
@@ -117,6 +121,7 @@ impl FunctionPrototype {
117121
#[allow(clippy::too_many_arguments)]
118122
pub fn new(
119123
annotations: Vec<Annotation>,
124+
variant: Variant,
120125
identifier: Identifier,
121126
const_parameters: Vec<ConstParameter>,
122127
input: Vec<Input>,
@@ -130,7 +135,7 @@ impl FunctionPrototype {
130135
_ => Type::Tuple(TupleType::new(output.iter().map(|o| o.type_.clone()).collect())),
131136
};
132137

133-
Self { annotations, identifier, const_parameters, input, output, output_type, span, id }
138+
Self { annotations, variant, identifier, const_parameters, input, output, output_type, span, id }
134139
}
135140
}
136141

@@ -153,7 +158,10 @@ impl fmt::Display for FunctionPrototype {
153158
for annotation in &self.annotations {
154159
writeln!(f, "{annotation}")?;
155160
}
156-
write!(f, "fn {}", self.identifier)?;
161+
match self.variant {
162+
Variant::View => write!(f, "view fn {}", self.identifier)?,
163+
_ => write!(f, "fn {}", self.identifier)?,
164+
}
157165
if !self.const_parameters.is_empty() {
158166
write!(f, "::[{}]", self.const_parameters.iter().format(", "))?;
159167
}

0 commit comments

Comments
 (0)