Skip to main content

snarkvm_synthesizer_program/view/
mod.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::Command;
17
18mod input;
19pub use input::*;
20
21mod output;
22pub use output::*;
23
24mod bytes;
25mod parse;
26
27use console::{
28    network::{error, prelude::*},
29    program::{FinalizeType, Identifier, Register},
30};
31
32use indexmap::IndexSet;
33use std::collections::HashMap;
34
35/// A view function: a top-level, externally-callable, read-only block that returns typed values.
36///
37/// Views share the finalize command set (so they can `get`, `get.or_use`, `contains` against
38/// mappings), but cannot mutate state, schedule futures, or call other functions.
39#[derive(Clone, PartialEq, Eq)]
40pub struct ViewCore<N: Network> {
41    /// The name of the view function.
42    name: Identifier<N>,
43    /// The input statements, added in order of the input registers.
44    inputs: IndexSet<Input<N>>,
45    /// The commands, in order of execution.
46    commands: Vec<Command<N>>,
47    /// The output statements, in order of the desired output.
48    outputs: IndexSet<Output<N>>,
49    /// A mapping from `Position`s to their index in `commands`.
50    positions: HashMap<Identifier<N>, usize>,
51}
52
53impl<N: Network> ViewCore<N> {
54    /// Initializes a new view function with the given name.
55    pub fn new(name: Identifier<N>) -> Self {
56        Self {
57            name,
58            inputs: IndexSet::new(),
59            commands: Vec::new(),
60            outputs: IndexSet::new(),
61            positions: HashMap::new(),
62        }
63    }
64
65    /// Returns the name of the view function.
66    pub const fn name(&self) -> &Identifier<N> {
67        &self.name
68    }
69
70    /// Returns the checksum of the view.
71    ///
72    /// The checksum is a 32-byte hash of the view's source code in string format.
73    /// This ensures a strict definition of view equivalence, useful for program upgradability.
74    pub fn to_checksum(&self) -> [console::types::U8<N>; 32] {
75        crate::to_checksum::source_code_checksum(&self.to_string())
76    }
77
78    /// Returns the view inputs.
79    pub const fn inputs(&self) -> &IndexSet<Input<N>> {
80        &self.inputs
81    }
82
83    /// Returns the view input types.
84    pub fn input_types(&self) -> Vec<FinalizeType<N>> {
85        self.inputs.iter().map(|input| input.finalize_type()).cloned().collect()
86    }
87
88    /// Returns the view commands.
89    pub fn commands(&self) -> &[Command<N>] {
90        &self.commands
91    }
92
93    /// Returns the view outputs.
94    pub const fn outputs(&self) -> &IndexSet<Output<N>> {
95        &self.outputs
96    }
97
98    /// Returns the view output types.
99    pub fn output_types(&self) -> Vec<FinalizeType<N>> {
100        self.outputs.iter().map(|output| output.finalize_type()).cloned().collect()
101    }
102
103    /// Returns the mapping of `Position`s to their index in `commands`.
104    pub const fn positions(&self) -> &HashMap<Identifier<N>, usize> {
105        &self.positions
106    }
107
108    /// Returns `true` if the view contains an array type with a size that exceeds the given maximum.
109    /// Mirrors `Finalize::exceeds_max_array_size` and additionally walks `outputs`, since views
110    /// declare typed outputs.
111    pub fn exceeds_max_array_size(&self, max_array_size: u32) -> bool {
112        self.inputs.iter().any(|input| {
113            matches!(input.finalize_type(), FinalizeType::Plaintext(p) if p.exceeds_max_array_size(max_array_size))
114        }) || self.commands.iter().any(|command| command.exceeds_max_array_size(max_array_size))
115            || self.outputs.iter().any(|output| {
116                matches!(output.finalize_type(), FinalizeType::Plaintext(p) if p.exceeds_max_array_size(max_array_size))
117            })
118    }
119
120    /// Returns `true` if the view refers to an external struct in its inputs, body, or outputs.
121    pub fn contains_external_struct(&self) -> bool {
122        self.inputs
123            .iter()
124            .any(|input| matches!(input.finalize_type(), FinalizeType::Plaintext(p) if p.contains_external_struct()))
125            || self
126                .commands
127                .iter()
128                .any(|command| matches!(command, Command::Instruction(inst) if inst.contains_external_struct()))
129            || self.outputs.iter().any(
130                |output| matches!(output.finalize_type(), FinalizeType::Plaintext(p) if p.contains_external_struct()),
131            )
132    }
133
134    /// Returns `true` if the view contains a string type. Mirrors `Finalize::contains_string_type`
135    /// and additionally walks `outputs`.
136    pub fn contains_string_type(&self) -> bool {
137        self.inputs
138            .iter()
139            .any(|input| matches!(input.finalize_type(), FinalizeType::Plaintext(p) if p.contains_string_type()))
140            || self.commands.iter().any(|command| command.contains_string_type())
141            || self
142                .outputs
143                .iter()
144                .any(|output| matches!(output.finalize_type(), FinalizeType::Plaintext(p) if p.contains_string_type()))
145    }
146}
147
148impl<N: Network> ViewCore<N> {
149    /// Adds the input statement to the view.
150    #[inline]
151    fn add_input(&mut self, input: Input<N>) -> Result<()> {
152        // Ensure there are no commands or outputs in memory.
153        ensure!(self.commands.is_empty(), "Cannot add inputs after commands have been added");
154        ensure!(self.outputs.is_empty(), "Cannot add inputs after outputs have been added");
155
156        // Ensure the maximum number of inputs has not been exceeded.
157        ensure!(self.inputs.len() < N::MAX_INPUTS, "Cannot add more than {} inputs", N::MAX_INPUTS);
158        // Ensure the input statement was not previously added.
159        ensure!(!self.inputs.contains(&input), "Cannot add duplicate input statement");
160
161        // Views are externally-callable; futures and dynamic futures are not meaningful here.
162        ensure!(
163            matches!(input.finalize_type(), FinalizeType::Plaintext(..)),
164            "View inputs must be plaintext (futures are forbidden)"
165        );
166
167        // Ensure the input register is a locator.
168        ensure!(matches!(input.register(), Register::Locator(..)), "Input register must be a locator");
169
170        self.inputs.insert(input);
171        Ok(())
172    }
173
174    /// Adds the given command to the view.
175    #[inline]
176    pub fn add_command(&mut self, command: Command<N>) -> Result<()> {
177        // Ensure there are no outputs already.
178        ensure!(self.outputs.is_empty(), "Cannot add commands after outputs have been added");
179
180        // Ensure the maximum number of commands has not been exceeded.
181        ensure!(self.commands.len() < N::MAX_COMMANDS, "Cannot add more than {} commands", N::MAX_COMMANDS);
182
183        // Reject any state-mutating or non-deterministic command.
184        ensure!(!command.is_write(), "Forbidden operation: view functions cannot use 'set' or 'remove'");
185        ensure!(!command.is_async(), "Forbidden operation: view functions cannot invoke an 'async' instruction");
186        ensure!(!command.is_await(), "Forbidden operation: view functions cannot 'await' a future");
187        ensure!(!command.is_call(), "Forbidden operation: view functions cannot 'call' another function");
188        ensure!(!command.is_instruction_for_record(), "Forbidden operation: view functions cannot operate on records");
189        // `rand.chacha` is only meaningful with finalize global state.
190        ensure!(!command.is_rand_chacha(), "Forbidden operation: view functions cannot use 'rand.chacha'");
191
192        // Check the destination registers.
193        for register in command.destinations() {
194            ensure!(matches!(register, Register::Locator(..)), "Destination register must be a locator");
195        }
196
197        // Branch target validation.
198        if let Some(position) = command.branch_to() {
199            ensure!(!self.positions.contains_key(position), "Cannot branch to an earlier position '{position}'");
200        }
201
202        if let Some(position) = command.position() {
203            ensure!(!self.positions.contains_key(position), "Cannot redefine position '{position}'");
204            ensure!(self.positions.len() < N::MAX_POSITIONS, "Cannot add more than {} positions", N::MAX_POSITIONS);
205            self.positions.insert(*position, self.commands.len());
206        }
207
208        self.commands.push(command);
209        Ok(())
210    }
211
212    /// Adds the output statement to the view.
213    #[inline]
214    fn add_output(&mut self, output: Output<N>) -> Result<()> {
215        // Ensure the maximum number of outputs has not been exceeded.
216        ensure!(self.outputs.len() < N::MAX_OUTPUTS, "Cannot add more than {} outputs", N::MAX_OUTPUTS);
217
218        // Views return plaintext only.
219        ensure!(
220            matches!(output.finalize_type(), FinalizeType::Plaintext(..)),
221            "View outputs must be plaintext (futures are forbidden)"
222        );
223
224        self.outputs.insert(output);
225        Ok(())
226    }
227}
228
229impl<N: Network> TypeName for ViewCore<N> {
230    #[inline]
231    fn type_name() -> &'static str {
232        "view"
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    type CurrentNetwork = console::network::MainnetV0;
241
242    #[test]
243    fn test_add_input() {
244        let name = Identifier::from_str("view_core_test").unwrap();
245        let mut view = ViewCore::<CurrentNetwork>::new(name);
246
247        let input = Input::<CurrentNetwork>::from_str("input r0 as field.public;").unwrap();
248        assert!(view.add_input(input.clone()).is_ok());
249        assert!(view.add_input(input).is_err());
250    }
251
252    #[test]
253    fn test_reject_set_command() {
254        let name = Identifier::from_str("view_core_test").unwrap();
255        let mut view = ViewCore::<CurrentNetwork>::new(name);
256
257        let cmd = Command::<CurrentNetwork>::from_str("set 1u64 into balances[0u64];").unwrap();
258        let err = view.add_command(cmd).unwrap_err();
259        assert!(err.to_string().contains("'set' or 'remove'"));
260    }
261
262    #[test]
263    fn test_reject_remove_command() {
264        let name = Identifier::from_str("view_core_test").unwrap();
265        let mut view = ViewCore::<CurrentNetwork>::new(name);
266
267        let cmd = Command::<CurrentNetwork>::from_str("remove balances[0u64];").unwrap();
268        let err = view.add_command(cmd).unwrap_err();
269        assert!(err.to_string().contains("'set' or 'remove'"));
270    }
271
272    #[test]
273    fn test_reject_rand_chacha() {
274        let name = Identifier::from_str("view_core_test").unwrap();
275        let mut view = ViewCore::<CurrentNetwork>::new(name);
276
277        let cmd = Command::<CurrentNetwork>::from_str("rand.chacha into r0 as u64;").unwrap();
278        let err = view.add_command(cmd).unwrap_err();
279        assert!(err.to_string().contains("rand.chacha"));
280    }
281
282    #[test]
283    fn test_reject_await_command() {
284        let name = Identifier::from_str("view_core_test").unwrap();
285        let mut view = ViewCore::<CurrentNetwork>::new(name);
286
287        let cmd = Command::<CurrentNetwork>::from_str("await r0;").unwrap();
288        let err = view.add_command(cmd).unwrap_err();
289        assert!(err.to_string().contains("'await'"));
290    }
291
292    #[test]
293    fn test_reject_call_instruction() {
294        let name = Identifier::from_str("view_core_test").unwrap();
295        let mut view = ViewCore::<CurrentNetwork>::new(name);
296
297        let cmd = Command::<CurrentNetwork>::from_str("call foo r0 into r1;").unwrap();
298        let err = view.add_command(cmd).unwrap_err();
299        assert!(err.to_string().contains("'call'"));
300    }
301
302    #[test]
303    fn test_reject_cast_to_record() {
304        let name = Identifier::from_str("view_core_test").unwrap();
305        let mut view = ViewCore::<CurrentNetwork>::new(name);
306
307        let cmd =
308            Command::<CurrentNetwork>::from_str("cast r0.owner r0.token_amount into r1 as token.record;").unwrap();
309        let err = view.add_command(cmd).unwrap_err();
310        assert!(err.to_string().contains("operate on records"));
311    }
312
313    #[test]
314    fn test_reject_cast_to_dynamic_record() {
315        let name = Identifier::from_str("view_core_test").unwrap();
316        let mut view = ViewCore::<CurrentNetwork>::new(name);
317
318        let cmd = Command::<CurrentNetwork>::from_str("cast r0 into r1 as dynamic.record;").unwrap();
319        let err = view.add_command(cmd).unwrap_err();
320        assert!(err.to_string().contains("operate on records"));
321    }
322
323    #[test]
324    fn test_reject_get_record_dynamic() {
325        let name = Identifier::from_str("view_core_test").unwrap();
326        let mut view = ViewCore::<CurrentNetwork>::new(name);
327
328        let cmd = Command::<CurrentNetwork>::from_str("get.record.dynamic r0.x into r1 as bool;").unwrap();
329        let err = view.add_command(cmd).unwrap_err();
330        assert!(err.to_string().contains("operate on records"));
331    }
332
333    #[test]
334    fn test_reject_async_instruction() {
335        let name = Identifier::from_str("view_core_test").unwrap();
336        let mut view = ViewCore::<CurrentNetwork>::new(name);
337
338        let cmd = Command::<CurrentNetwork>::from_str("async foo r0 r1 into r3;").unwrap();
339        let err = view.add_command(cmd).unwrap_err();
340        assert!(err.to_string().contains("'async'"));
341    }
342}