Skip to main content

stoffel/
program.rs

1//! Compiled program metadata and bytecode helpers.
2//!
3//! `Program` wraps `stoffel-vm-types` bytecode and exposes SDK-friendly
4//! metadata for functions, ClientStore schemas, and CLI-compatible bytecode
5//! round trips.
6
7use std::fmt;
8use std::io::Cursor;
9use std::path::Path;
10
11use serde::{Deserialize, Serialize};
12use stoffel_vm_types::compiled_binary::{
13    utils, ClientIoManifest, ClientIoSchema, CompiledBinary, CompiledFunction, CompiledInstruction,
14    FunctionType,
15};
16use stoffel_vm_types::core_types::{ShareType, Value as VmValue};
17use stoffel_vm_types::registers::DEFAULT_SECRET_REGISTER_START;
18
19use crate::compiler;
20use crate::error::{Error, Result};
21use crate::types::Value;
22
23#[derive(Debug, Clone)]
24pub struct Program {
25    binary: CompiledBinary,
26}
27
28#[derive(Debug, Clone, PartialEq)]
29pub(crate) enum LocalInputShape {
30    Clear(Value),
31    Share(ShareType),
32    List(Vec<LocalInputShape>),
33    Object(Vec<(String, LocalInputShape)>),
34}
35
36impl LocalInputShape {
37    pub(crate) fn secret_from_value(value: &Value) -> Self {
38        match value {
39            Value::List(values) => Self::List(
40                values
41                    .iter()
42                    .map(Self::secret_from_value)
43                    .collect::<Vec<_>>(),
44            ),
45            Value::Object(fields) => Self::Object(
46                fields
47                    .iter()
48                    .map(|(name, value)| (name.clone(), Self::secret_from_value(value)))
49                    .collect(),
50            ),
51            Value::I64(_)
52            | Value::U64(_)
53            | Value::Float(_)
54            | Value::String(_)
55            | Value::Bytes(_)
56            | Value::Unit => Self::Share(ShareType::default_secret_int()),
57            Value::Bool(_) => Self::Share(ShareType::SecretInt { bit_length: 1 }),
58        }
59    }
60
61    pub(crate) fn clear_from_value(value: &Value) -> Self {
62        match value {
63            Value::List(values) => Self::List(
64                values
65                    .iter()
66                    .map(Self::clear_from_value)
67                    .collect::<Vec<_>>(),
68            ),
69            Value::Object(fields) => Self::Object(
70                fields
71                    .iter()
72                    .map(|(name, value)| (name.clone(), Self::clear_from_value(value)))
73                    .collect(),
74            ),
75            value => Self::Clear(value.clone()),
76        }
77    }
78
79    fn share_count(&self) -> usize {
80        match self {
81            Self::Clear(_) => 0,
82            Self::Share(_) => 1,
83            Self::List(items) => items.iter().map(Self::share_count).sum(),
84            Self::Object(fields) => fields.iter().map(|(_, shape)| shape.share_count()).sum(),
85        }
86    }
87
88    fn share_types(&self, out: &mut Vec<ShareType>) {
89        match self {
90            Self::Clear(_) => {}
91            Self::Share(ty) => out.push(*ty),
92            Self::List(items) => {
93                for item in items {
94                    item.share_types(out);
95                }
96            }
97            Self::Object(fields) => {
98                for (_, shape) in fields {
99                    shape.share_types(out);
100                }
101            }
102        }
103    }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct ProgramSummary {
108    pub function_count: usize,
109    pub function_names: Vec<String>,
110    pub functions: Vec<FunctionSummary>,
111    pub total_instruction_count: usize,
112    pub total_register_count: usize,
113    pub bytecode_backend: String,
114    pub bytecode_curve: String,
115    pub client_count: usize,
116    pub client_slots: Vec<u64>,
117    pub clients: Vec<ClientMetadataSummary>,
118    pub total_client_input_count: usize,
119    pub total_client_output_count: usize,
120    pub minimum_expected_clients: usize,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct FunctionSummary {
125    pub name: String,
126    pub arg_count: usize,
127    pub parameters: Vec<String>,
128    pub parameter_types: Vec<FunctionType>,
129    pub return_type: FunctionType,
130    pub register_count: usize,
131    pub instruction_count: usize,
132    pub upvalue_count: usize,
133    pub parent: Option<String>,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct ClientMetadataSummary {
138    pub client_slot: u64,
139    pub input_count: usize,
140    pub output_count: usize,
141    pub inputs: Vec<ShareType>,
142    pub outputs: Vec<ShareType>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct BytecodeSummary {
147    pub byte_len: usize,
148    pub program: ProgramSummary,
149}
150
151impl Program {
152    pub fn new(binary: CompiledBinary) -> Self {
153        Self { binary }
154    }
155
156    pub fn from_bytecode(bytecode: &[u8]) -> Result<Self> {
157        compiler::load_bytecode(bytecode)
158    }
159
160    pub fn from_bytecode_file(path: impl AsRef<Path>) -> Result<Self> {
161        let bytecode = std::fs::read(path)?;
162        Self::from_bytecode(&bytecode)
163    }
164
165    pub fn to_bytecode(&self) -> Result<Vec<u8>> {
166        let mut bytes = Vec::new();
167        self.binary
168            .serialize(&mut Cursor::new(&mut bytes))
169            .map_err(|error| Error::Bytecode(format!("{error:?}")))?;
170        Ok(bytes)
171    }
172
173    pub fn save_bytecode(&self, path: impl AsRef<Path>) -> Result<()> {
174        utils::save_to_file(&self.binary, path)
175            .map_err(|error| Error::Bytecode(format!("{error:?}")))
176    }
177
178    pub fn bytecode_summary(&self) -> Result<BytecodeSummary> {
179        Ok(BytecodeSummary {
180            byte_len: self.to_bytecode()?.len(),
181            program: self.summary(),
182        })
183    }
184
185    pub fn disassemble(&self) -> String {
186        stoffellang::binary_converter::disassemble(&self.binary)
187    }
188
189    pub fn function_count(&self) -> usize {
190        self.binary.functions.len()
191    }
192
193    pub fn function_names(&self) -> impl Iterator<Item = &str> {
194        self.binary
195            .functions
196            .iter()
197            .map(|function| function.name.as_str())
198    }
199
200    pub fn total_instruction_count(&self) -> usize {
201        self.binary
202            .functions
203            .iter()
204            .map(|function| function.instructions.len())
205            .sum()
206    }
207
208    pub fn total_register_count(&self) -> usize {
209        self.binary
210            .functions
211            .iter()
212            .map(|function| function.register_count)
213            .sum()
214    }
215
216    pub fn is_empty(&self) -> bool {
217        self.binary.functions.is_empty()
218    }
219
220    pub fn summary(&self) -> ProgramSummary {
221        ProgramSummary {
222            function_count: self.function_count(),
223            function_names: self.function_names().map(ToOwned::to_owned).collect(),
224            functions: self
225                .functions()
226                .map(|function| function.summary())
227                .collect(),
228            total_instruction_count: self.total_instruction_count(),
229            total_register_count: self.total_register_count(),
230            bytecode_backend: bytecode_backend_name(self.bytecode_backend()).to_owned(),
231            bytecode_curve: bytecode_curve_name(self.bytecode_curve()).to_owned(),
232            client_count: self.client_count(),
233            client_slots: self.client_slots().collect(),
234            clients: self.clients().map(|client| client.summary()).collect(),
235            total_client_input_count: self.total_client_input_count(),
236            total_client_output_count: self.total_client_output_count(),
237            minimum_expected_clients: self.minimum_expected_clients(),
238        }
239    }
240
241    pub fn client_io_manifest(&self) -> &ClientIoManifest {
242        &self.binary.client_io_manifest
243    }
244
245    pub fn client_count(&self) -> usize {
246        self.binary.client_io_manifest.clients.len()
247    }
248
249    pub fn client_slots(&self) -> impl Iterator<Item = u64> + '_ {
250        self.binary
251            .client_io_manifest
252            .clients
253            .iter()
254            .map(|client| client.client_slot)
255    }
256
257    pub fn total_client_input_count(&self) -> usize {
258        self.binary
259            .client_io_manifest
260            .clients
261            .iter()
262            .map(|client| client.inputs.len())
263            .sum()
264    }
265
266    pub fn total_client_output_count(&self) -> usize {
267        self.binary
268            .client_io_manifest
269            .clients
270            .iter()
271            .map(|client| client.outputs.len())
272            .sum()
273    }
274
275    pub fn minimum_expected_clients(&self) -> usize {
276        self.binary
277            .client_io_manifest
278            .clients
279            .iter()
280            .map(|client| usize::try_from(client.client_slot).unwrap_or(usize::MAX))
281            .map(|client_slot| client_slot.saturating_add(1))
282            .max()
283            .unwrap_or(0)
284    }
285
286    pub fn has_client_io(&self) -> bool {
287        !self.binary.client_io_manifest.clients.is_empty()
288    }
289
290    /// Whether the program reads client INPUTS via `ClientStore` (as opposed to
291    /// only delivering client OUTPUTS via `send_to_client`). The local
292    /// named-input adapter synthesizes the input wrapper itself, so it can only
293    /// wrap programs that take their inputs as function parameters — i.e. those
294    /// with no client input metadata. Output-only client IO is fine and is
295    /// preserved through wrapping.
296    pub fn has_client_input_metadata(&self) -> bool {
297        self.binary
298            .client_io_manifest
299            .clients
300            .iter()
301            .any(|client| !client.inputs.is_empty())
302    }
303
304    pub fn validate_expected_clients(&self, expected_clients: usize) -> Result<()> {
305        let minimum_expected_clients = self.minimum_expected_clients();
306        if minimum_expected_clients > expected_clients {
307            return Err(Error::Configuration(format!(
308                "program declares ClientStore slot(s) requiring expected_clients >= {minimum_expected_clients}, but expected_clients is {expected_clients}"
309            )));
310        }
311        Ok(())
312    }
313
314    pub fn validate_client_inputs(&self, inputs: &[(u64, &[Value])]) -> Result<()> {
315        let has_declared_clients = self.has_client_io();
316        if !has_declared_clients && inputs.is_empty() {
317            return Ok(());
318        }
319        if has_declared_clients
320            && inputs.is_empty()
321            && self.clients().any(|metadata| metadata.input_count() > 0)
322        {
323            return Err(Error::Configuration(
324                "program declares ClientStore input metadata; provide local client inputs"
325                    .to_owned(),
326            ));
327        }
328        if !has_declared_clients {
329            let mut seen_slots = std::collections::HashSet::with_capacity(inputs.len());
330            for (client_slot, _values) in inputs {
331                if !seen_slots.insert(*client_slot) {
332                    return Err(Error::Configuration(format!(
333                        "client slot {client_slot} was provided more than once"
334                    )));
335                }
336            }
337            return Ok(());
338        }
339
340        let mut seen_slots = std::collections::HashSet::with_capacity(inputs.len());
341        for (client_slot, values) in inputs {
342            if !seen_slots.insert(*client_slot) {
343                return Err(Error::Configuration(format!(
344                    "client slot {client_slot} was provided more than once"
345                )));
346            }
347            let Some(metadata) = self.client(*client_slot) else {
348                return Err(Error::Configuration(format!(
349                    "client slot {client_slot} is not declared in the program client IO manifest"
350                )));
351            };
352            if metadata.input_count() != values.len() {
353                return Err(Error::Configuration(format!(
354                    "client slot {client_slot} expects {} inputs, got {}",
355                    metadata.input_count(),
356                    values.len()
357                )));
358            }
359        }
360
361        for metadata in self.clients() {
362            if metadata.input_count() > 0 && !seen_slots.contains(&metadata.client_slot()) {
363                return Err(Error::Configuration(format!(
364                    "client slot {} is declared in the program client IO manifest but no input was provided",
365                    metadata.client_slot()
366                )));
367            }
368        }
369
370        Ok(())
371    }
372
373    pub(crate) fn validate_owned_client_inputs(&self, inputs: &[(u64, Vec<Value>)]) -> Result<()> {
374        let borrowed = inputs
375            .iter()
376            .map(|(client_slot, values)| (*client_slot, values.as_slice()))
377            .collect::<Vec<_>>();
378        self.validate_client_inputs(&borrowed)
379    }
380
381    pub fn clients(&self) -> impl Iterator<Item = ClientMetadata<'_>> {
382        self.binary
383            .client_io_manifest
384            .clients
385            .iter()
386            .map(ClientMetadata)
387    }
388
389    pub fn client(&self, client_slot: u64) -> Option<ClientMetadata<'_>> {
390        self.binary
391            .client_io_manifest
392            .clients
393            .iter()
394            .find(|client| client.client_slot == client_slot)
395            .map(ClientMetadata)
396    }
397
398    pub fn bytecode_backend(&self) -> stoffel_vm_types::compiled_binary::MpcBackend {
399        self.binary.client_io_manifest.mpc_backend
400    }
401
402    pub fn bytecode_curve(&self) -> stoffel_vm_types::compiled_binary::MpcCurve {
403        self.binary.client_io_manifest.mpc_curve
404    }
405
406    pub(crate) fn with_local_client_input_wrapper(
407        &self,
408        call_name: &str,
409        entry_name: &str,
410        input_shapes: &[LocalInputShape],
411    ) -> Result<Self> {
412        if input_shapes.is_empty() {
413            return Err(Error::Configuration(
414                "local client input wrapper requires at least one input".to_owned(),
415            ));
416        }
417
418        let mut binary = self.binary.clone();
419        if binary
420            .functions
421            .iter()
422            .any(|function| function.name == entry_name)
423        {
424            return Err(Error::Configuration(format!(
425                "local client input wrapper entry '{entry_name}' already exists"
426            )));
427        }
428        let Some(_target_function) = binary
429            .functions
430            .iter()
431            .find(|function| function.name == call_name)
432        else {
433            return Err(Error::FunctionNotFound(call_name.to_owned()));
434        };
435        let input_count = input_shapes
436            .iter()
437            .map(LocalInputShape::share_count)
438            .sum::<usize>();
439        let mut input_types = Vec::with_capacity(input_count);
440        for shape in input_shapes {
441            shape.share_types(&mut input_types);
442        }
443
444        let mut instructions = Vec::with_capacity(input_count * 8 + input_shapes.len() + 2);
445        let mut registers = WrapperRegisters::new();
446        let mut next_share_index = 0usize;
447        let mut arg_registers = Vec::with_capacity(input_shapes.len());
448        for shape in input_shapes {
449            let register = emit_local_input_shape(
450                shape,
451                &mut binary,
452                &mut instructions,
453                &mut next_share_index,
454                &mut registers,
455            )?;
456            arg_registers.push(register);
457        }
458        for register in arg_registers {
459            instructions.push(CompiledInstruction::PUSHARG(register));
460        }
461        instructions.push(CompiledInstruction::CALL(call_name.to_owned()));
462        instructions.push(CompiledInstruction::RET(0));
463
464        let register_count = registers.register_count();
465        binary.functions.push(CompiledFunction {
466            name: entry_name.to_owned(),
467            register_count,
468            parameters: Vec::new(),
469            parameter_types: Vec::new(),
470            return_type: FunctionType::Unknown,
471            upvalues: Vec::new(),
472            parent: None,
473            labels: Default::default(),
474            instructions,
475        });
476
477        // The wrapper synthesizes all client inputs (clear constants + secret
478        // `take_share*` loads) on client slot 0, so it owns the input schema.
479        // Any client OUTPUT schema the program declares (e.g. `send_to_client`)
480        // is orthogonal and must be preserved, so a wrapped program can still
481        // deliver results to its clients.
482        if input_count > 0 {
483            if let Some(client) = binary
484                .client_io_manifest
485                .clients
486                .iter_mut()
487                .find(|client| client.client_slot == 0)
488            {
489                client.inputs = input_types;
490            } else {
491                binary.client_io_manifest.clients.push(ClientIoSchema {
492                    client_slot: 0,
493                    inputs: input_types,
494                    outputs: Vec::new(),
495                });
496            }
497        }
498
499        Ok(Self::new(binary))
500    }
501
502    pub fn functions(&self) -> impl Iterator<Item = FunctionMetadata<'_>> {
503        self.binary.functions.iter().map(FunctionMetadata)
504    }
505
506    pub fn function(&self, name: &str) -> Option<FunctionMetadata<'_>> {
507        self.binary
508            .functions
509            .iter()
510            .find(|function| function.name == name)
511            .map(FunctionMetadata)
512    }
513
514    pub fn main(&self) -> Option<FunctionMetadata<'_>> {
515        self.function("main")
516    }
517
518    pub fn binary(&self) -> &CompiledBinary {
519        &self.binary
520    }
521
522    pub fn validate_function_inputs(
523        &self,
524        function_name: &str,
525        inputs: &[(String, Value)],
526    ) -> Result<()> {
527        let function = self
528            .function(function_name)
529            .ok_or_else(|| Error::FunctionNotFound(function_name.to_owned()))?;
530        function.validate_inputs(inputs)
531    }
532
533    pub fn validate_function_args(&self, function_name: &str, args: &[Value]) -> Result<()> {
534        let function = self
535            .function(function_name)
536            .ok_or_else(|| Error::FunctionNotFound(function_name.to_owned()))?;
537        function.validate_args(args)
538    }
539}
540
541#[derive(Debug, Clone, Copy)]
542pub struct ClientMetadata<'a>(&'a ClientIoSchema);
543
544impl<'a> ClientMetadata<'a> {
545    pub fn client_slot(&self) -> u64 {
546        self.0.client_slot
547    }
548
549    pub fn input_count(&self) -> usize {
550        self.0.inputs.len()
551    }
552
553    pub fn output_count(&self) -> usize {
554        self.0.outputs.len()
555    }
556
557    pub fn inputs(&self) -> &'a [ShareType] {
558        &self.0.inputs
559    }
560
561    pub fn outputs(&self) -> &'a [ShareType] {
562        &self.0.outputs
563    }
564
565    pub fn summary(&self) -> ClientMetadataSummary {
566        ClientMetadataSummary {
567            client_slot: self.client_slot(),
568            input_count: self.input_count(),
569            output_count: self.output_count(),
570            inputs: self.inputs().to_vec(),
571            outputs: self.outputs().to_vec(),
572        }
573    }
574}
575
576impl fmt::Display for ClientMetadata<'_> {
577    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578        write!(
579            f,
580            "client {}: {} input(s), {} output(s)",
581            self.client_slot(),
582            self.input_count(),
583            self.output_count()
584        )
585    }
586}
587
588#[derive(Debug, Clone, Copy)]
589pub struct FunctionMetadata<'a>(&'a CompiledFunction);
590
591impl<'a> FunctionMetadata<'a> {
592    pub fn name(&self) -> &str {
593        &self.0.name
594    }
595
596    pub fn arg_count(&self) -> usize {
597        self.0.parameters.len()
598    }
599
600    pub fn parameters(&self) -> &[String] {
601        &self.0.parameters
602    }
603
604    pub fn parameter_names(&self) -> impl Iterator<Item = &str> {
605        self.0.parameters.iter().map(String::as_str)
606    }
607
608    pub fn parameter_types(&self) -> &[FunctionType] {
609        &self.0.parameter_types
610    }
611
612    pub fn return_type(&self) -> &FunctionType {
613        &self.0.return_type
614    }
615
616    pub fn register_count(&self) -> usize {
617        self.0.register_count
618    }
619
620    pub fn instruction_count(&self) -> usize {
621        self.0.instructions.len()
622    }
623
624    pub fn upvalue_count(&self) -> usize {
625        self.0.upvalues.len()
626    }
627
628    pub fn parent(&self) -> Option<&str> {
629        self.0.parent.as_deref()
630    }
631
632    pub fn summary(&self) -> FunctionSummary {
633        FunctionSummary {
634            name: self.name().to_owned(),
635            arg_count: self.arg_count(),
636            parameters: self.parameters().to_vec(),
637            parameter_types: self.parameter_types().to_vec(),
638            return_type: self.return_type().clone(),
639            register_count: self.register_count(),
640            instruction_count: self.instruction_count(),
641            upvalue_count: self.upvalue_count(),
642            parent: self.parent().map(ToOwned::to_owned),
643        }
644    }
645
646    pub fn validate_inputs(&self, inputs: &[(String, Value)]) -> Result<()> {
647        for (name, _) in inputs {
648            if !self.0.parameters.iter().any(|parameter| parameter == name) {
649                return Err(Error::InvalidInput(format!(
650                    "unexpected input '{name}' for function '{}'",
651                    self.name()
652                )));
653            }
654        }
655
656        for parameter in &self.0.parameters {
657            let mut matches = inputs.iter().filter(|(name, _)| name == parameter);
658            let Some((_, value)) = matches.next() else {
659                return Err(Error::InvalidInput(format!(
660                    "missing input '{parameter}' for function '{}'",
661                    self.name()
662                )));
663            };
664            if matches.next().is_some() {
665                return Err(Error::InvalidInput(format!(
666                    "duplicate input '{parameter}' for function '{}'",
667                    self.name()
668                )));
669            }
670            let ty = self
671                .0
672                .parameter_types
673                .get(
674                    self.0
675                        .parameters
676                        .iter()
677                        .position(|name| name == parameter)
678                        .unwrap_or(usize::MAX),
679                )
680                .unwrap_or(&FunctionType::Unknown);
681            validate_value_against_function_type(parameter, value, ty)?;
682        }
683
684        Ok(())
685    }
686
687    pub fn validate_args(&self, args: &[Value]) -> Result<()> {
688        let expected = self.arg_count();
689        let actual = args.len();
690        if actual != expected {
691            return Err(Error::InvalidInput(format!(
692                "function '{}' expects {expected} argument(s), got {actual}",
693                self.name()
694            )));
695        }
696
697        for (index, value) in args.iter().enumerate() {
698            let parameter = self
699                .0
700                .parameters
701                .get(index)
702                .map(String::as_str)
703                .unwrap_or("argument");
704            let ty = self
705                .0
706                .parameter_types
707                .get(index)
708                .unwrap_or(&FunctionType::Unknown);
709            validate_value_against_function_type(parameter, value, ty)?;
710        }
711
712        Ok(())
713    }
714}
715
716impl fmt::Display for FunctionMetadata<'_> {
717    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
718        write!(f, "{}(", self.name())?;
719        for (index, parameter) in self.parameter_names().enumerate() {
720            if index > 0 {
721                f.write_str(", ")?;
722            }
723            f.write_str(parameter)?;
724        }
725        write!(f, ")")
726    }
727}
728
729fn validate_value_against_function_type(
730    path: &str,
731    value: &Value,
732    ty: &FunctionType,
733) -> Result<()> {
734    let expected = ty.underlying_type();
735    if expected.is_unknown_like() {
736        return Ok(());
737    }
738
739    match expected {
740        FunctionType::Int { signed, bits } => validate_integer(path, value, *signed, *bits),
741        FunctionType::Float | FunctionType::Fixed { .. } => match value {
742            Value::Float(_) | Value::I64(_) | Value::U64(_) => Ok(()),
743            _ => invalid_type(path, expected, value),
744        },
745        FunctionType::String => match value {
746            Value::String(_) => Ok(()),
747            _ => invalid_type(path, expected, value),
748        },
749        FunctionType::Bool => match value {
750            Value::Bool(_) => Ok(()),
751            _ => invalid_type(path, expected, value),
752        },
753        FunctionType::Nil | FunctionType::Void => match value {
754            Value::Unit => Ok(()),
755            _ => invalid_type(path, expected, value),
756        },
757        FunctionType::List(element_type) => match value {
758            Value::List(values) => values.iter().enumerate().try_for_each(|(index, item)| {
759                validate_value_against_function_type(&format!("{path}[{index}]"), item, element_type)
760            }),
761            _ => invalid_type(path, expected, value),
762        },
763        FunctionType::Dict(key_type, value_type) => match value {
764            Value::Object(fields) => fields.iter().try_for_each(|(key, item)| {
765                if !key_type.is_unknown_like() && !matches!(key_type.underlying_type(), FunctionType::String) {
766                    return Err(Error::InvalidInput(format!(
767                        "input '{path}' expects {expected}, but SDK object inputs only provide string keys"
768                    )));
769                }
770                validate_value_against_function_type(&format!("{path}.{key}"), item, value_type)
771            }),
772            _ => invalid_type(path, expected, value),
773        },
774        FunctionType::Object(_) => match value {
775            Value::Object(_) => Ok(()),
776            _ => invalid_type(path, expected, value),
777        },
778        FunctionType::Secret(_)
779        | FunctionType::Generic(_, _)
780        | FunctionType::TypeVar(_)
781        | FunctionType::Unknown => Ok(()),
782    }
783}
784
785fn validate_integer(path: &str, value: &Value, signed: bool, bits: u8) -> Result<()> {
786    let max_unsigned = if bits >= 64 {
787        u64::MAX
788    } else {
789        (1u64 << bits) - 1
790    };
791    if signed {
792        let max = if bits >= 64 {
793            i64::MAX
794        } else {
795            (1i64 << (bits - 1)) - 1
796        };
797        let min = if bits >= 64 {
798            i64::MIN
799        } else {
800            -(1i64 << (bits - 1))
801        };
802        match value {
803            Value::I64(value) if *value >= min && *value <= max => Ok(()),
804            Value::U64(value) if *value <= max as u64 => Ok(()),
805            _ => invalid_type(path, &FunctionType::Int { signed, bits }, value),
806        }
807    } else {
808        match value {
809            Value::U64(value) if *value <= max_unsigned => Ok(()),
810            Value::I64(value) if *value >= 0 && (*value as u64) <= max_unsigned => Ok(()),
811            _ => invalid_type(path, &FunctionType::Int { signed, bits }, value),
812        }
813    }
814}
815
816fn invalid_type(path: &str, expected: &FunctionType, value: &Value) -> Result<()> {
817    Err(Error::InvalidInput(format!(
818        "input '{path}' expects {expected}, got {}",
819        value.kind()
820    )))
821}
822
823fn bytecode_backend_name(backend: stoffel_vm_types::compiled_binary::MpcBackend) -> &'static str {
824    match backend {
825        stoffel_vm_types::compiled_binary::MpcBackend::HoneyBadger => "honeybadger",
826        stoffel_vm_types::compiled_binary::MpcBackend::Avss => "avss",
827    }
828}
829
830fn bytecode_curve_name(curve: stoffel_vm_types::compiled_binary::MpcCurve) -> &'static str {
831    match curve {
832        stoffel_vm_types::compiled_binary::MpcCurve::Bls12_381 => "bls12_381",
833        stoffel_vm_types::compiled_binary::MpcCurve::Bn254 => "bn254",
834        stoffel_vm_types::compiled_binary::MpcCurve::Curve25519 => "curve25519",
835        stoffel_vm_types::compiled_binary::MpcCurve::Ed25519 => "ed25519",
836        stoffel_vm_types::compiled_binary::MpcCurve::Secp256k1 => "secp256k1",
837        stoffel_vm_types::compiled_binary::MpcCurve::Secp256r1 => "p-256",
838    }
839}
840
841/// Register-frame bookkeeping for the local input wrapper.
842///
843/// The VM splits the register frame into a clear bank (indices below
844/// [`DEFAULT_SECRET_REGISTER_START`]) and a secret bank (at or above it), so a
845/// clear value must live in a low index and a secret share in a high one. The
846/// wrapper builds aggregate (list/object) inputs by materializing each element
847/// into a temporary register and immediately consuming it with `array_push`/
848/// `set_field`; those element registers are dead afterwards, so we reuse them
849/// instead of allocating one per element (a 128-element list would otherwise
850/// overflow the 14-slot clear bank and corrupt the secret bank). `next_*` is the
851/// reusable cursor and `high_*` is the high-water mark used to size the frame.
852struct WrapperRegisters {
853    next_clear: usize,
854    next_secret: usize,
855    high_clear: usize,
856    high_secret: usize,
857    used_secret: bool,
858}
859
860impl WrapperRegisters {
861    /// Registers 0 and 1 are scratch for the builtin call ABI (return value and
862    /// argument staging), so clear allocation starts at 2.
863    const FIRST_CLEAR_REGISTER: usize = 2;
864
865    fn new() -> Self {
866        Self {
867            next_clear: Self::FIRST_CLEAR_REGISTER,
868            next_secret: DEFAULT_SECRET_REGISTER_START,
869            high_clear: Self::FIRST_CLEAR_REGISTER,
870            high_secret: DEFAULT_SECRET_REGISTER_START,
871            used_secret: false,
872        }
873    }
874
875    fn allocate_clear(&mut self) -> usize {
876        let register = self.next_clear;
877        self.next_clear += 1;
878        self.high_clear = self.high_clear.max(self.next_clear);
879        register
880    }
881
882    fn allocate_secret(&mut self) -> usize {
883        let register = self.next_secret;
884        self.next_secret += 1;
885        self.high_secret = self.high_secret.max(self.next_secret);
886        self.used_secret = true;
887        register
888    }
889
890    fn register_count(&self) -> usize {
891        if self.used_secret {
892            self.high_clear.max(self.high_secret)
893        } else {
894            self.high_clear
895        }
896    }
897}
898
899fn emit_local_input_shape(
900    shape: &LocalInputShape,
901    binary: &mut CompiledBinary,
902    instructions: &mut Vec<CompiledInstruction>,
903    next_share_index: &mut usize,
904    registers: &mut WrapperRegisters,
905) -> Result<usize> {
906    match shape {
907        LocalInputShape::Clear(value) => {
908            let dest = registers.allocate_clear();
909            let const_index = binary.constants.len();
910            binary
911                .constants
912                .push(clear_sdk_value_to_vm_constant(value)?);
913            instructions.push(CompiledInstruction::LDI(dest, const_index));
914            Ok(dest)
915        }
916        LocalInputShape::Share(share_type) => {
917            let dest = registers.allocate_secret();
918            let client_index_const = binary.constants.len();
919            binary.constants.push(VmValue::U64(0));
920            let share_index_const = binary.constants.len();
921            binary
922                .constants
923                .push(VmValue::U64(*next_share_index as u64));
924            *next_share_index += 1;
925            instructions.push(CompiledInstruction::LDI(0, client_index_const));
926            instructions.push(CompiledInstruction::LDI(1, share_index_const));
927            instructions.push(CompiledInstruction::PUSHARG(0));
928            instructions.push(CompiledInstruction::PUSHARG(1));
929            // Pick the ClientStore loader that tags the share with the declared
930            // type. The boolean gates (NOT/AND/XOR) reject the default 64-bit
931            // integer share, so a 1-bit secret must be loaded via
932            // `take_share_bool`; fixed-point likewise needs `take_share_fixed`.
933            let take_method = match share_type {
934                ShareType::SecretInt { bit_length: 1 } => "ClientStore.take_share_bool",
935                ShareType::SecretFixedPoint { .. } => "ClientStore.take_share_fixed",
936                _ => "ClientStore.take_share",
937            };
938            instructions.push(CompiledInstruction::CALL(take_method.to_owned()));
939            instructions.push(CompiledInstruction::MOV(dest, 0));
940            Ok(dest)
941        }
942        LocalInputShape::List(items) => {
943            let dest = registers.allocate_clear();
944            instructions.push(CompiledInstruction::CALL("create_array".to_owned()));
945            instructions.push(CompiledInstruction::MOV(dest, 0));
946            for item in items {
947                // Each element is consumed immediately by `array_push`, so its
948                // register is dead afterwards: snapshot the cursors, emit, push,
949                // then rewind to reuse the registers for the next element. The
950                // array handle in `dest` was allocated before the snapshot, so
951                // it survives.
952                let saved_clear = registers.next_clear;
953                let saved_secret = registers.next_secret;
954                let item_register = emit_local_input_shape(
955                    item,
956                    binary,
957                    instructions,
958                    next_share_index,
959                    registers,
960                )?;
961                instructions.push(CompiledInstruction::PUSHARG(dest));
962                instructions.push(CompiledInstruction::PUSHARG(item_register));
963                instructions.push(CompiledInstruction::CALL("array_push".to_owned()));
964                registers.next_clear = saved_clear;
965                registers.next_secret = saved_secret;
966            }
967            Ok(dest)
968        }
969        LocalInputShape::Object(fields) => {
970            let dest = registers.allocate_clear();
971            instructions.push(CompiledInstruction::CALL("create_object".to_owned()));
972            instructions.push(CompiledInstruction::MOV(dest, 0));
973            for (field_name, field_shape) in fields {
974                // Key and value are both live until `set_field`, then dead;
975                // rewind afterwards so the next field reuses their registers.
976                let saved_clear = registers.next_clear;
977                let saved_secret = registers.next_secret;
978                let key_register = registers.allocate_clear();
979                let key_const = binary.constants.len();
980                binary.constants.push(VmValue::String(field_name.clone()));
981                instructions.push(CompiledInstruction::LDI(key_register, key_const));
982                let value_register = emit_local_input_shape(
983                    field_shape,
984                    binary,
985                    instructions,
986                    next_share_index,
987                    registers,
988                )?;
989                instructions.push(CompiledInstruction::PUSHARG(dest));
990                instructions.push(CompiledInstruction::PUSHARG(key_register));
991                instructions.push(CompiledInstruction::PUSHARG(value_register));
992                instructions.push(CompiledInstruction::CALL("set_field".to_owned()));
993                registers.next_clear = saved_clear;
994                registers.next_secret = saved_secret;
995            }
996            Ok(dest)
997        }
998    }
999}
1000
1001fn clear_sdk_value_to_vm_constant(value: &Value) -> Result<VmValue> {
1002    match value {
1003        Value::I64(value) => Ok(VmValue::I64(*value)),
1004        Value::U64(value) => Ok(VmValue::U64(*value)),
1005        Value::Bool(value) => Ok(VmValue::Bool(*value)),
1006        Value::Float(value) => Ok(VmValue::Float((*value).into())),
1007        Value::String(value) => Ok(VmValue::String(value.clone())),
1008        Value::Unit => Ok(VmValue::Unit),
1009        Value::Bytes(_) => Err(Error::InvalidInput(
1010            "clear byte values cannot be embedded in local named-input wrappers".to_owned(),
1011        )),
1012        Value::List(_) | Value::Object(_) => Err(Error::InvalidInput(
1013            "internal error: structured clear value was not lowered recursively".to_owned(),
1014        )),
1015    }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021    use crate::backend::MpcBackend;
1022
1023    #[test]
1024    fn local_client_input_wrapper_places_secret_inputs_in_secret_registers() -> Result<()> {
1025        let program = crate::compiler::compile_source(
1026            "def main(a: secret int64, b: secret int64) -> secret int64:\n  return a + b",
1027            "test.stfl",
1028            MpcBackend::HoneyBadger,
1029        )?;
1030
1031        let wrapped = program.with_local_client_input_wrapper(
1032            "main",
1033            "__stoffel_sdk_local_entry",
1034            &[
1035                LocalInputShape::Share(ShareType::default_secret_int()),
1036                LocalInputShape::Share(ShareType::default_secret_int()),
1037            ],
1038        )?;
1039        let wrapper = wrapped
1040            .binary
1041            .functions
1042            .iter()
1043            .find(|function| function.name == "__stoffel_sdk_local_entry")
1044            .expect("wrapper function should be present");
1045
1046        assert!(wrapper.register_count >= DEFAULT_SECRET_REGISTER_START + 2);
1047        assert!(
1048            wrapper
1049                .instructions
1050                .contains(&CompiledInstruction::MOV(DEFAULT_SECRET_REGISTER_START, 0)),
1051            "first ClientStore share must be moved into a secret register"
1052        );
1053        assert!(
1054            wrapper.instructions.contains(&CompiledInstruction::MOV(
1055                DEFAULT_SECRET_REGISTER_START + 1,
1056                0
1057            )),
1058            "second ClientStore share must be moved into a secret register"
1059        );
1060
1061        let main_call = wrapper
1062            .instructions
1063            .iter()
1064            .position(|instruction| {
1065                matches!(instruction, CompiledInstruction::CALL(name) if name == "main")
1066            })
1067            .expect("wrapper should call main");
1068        assert_eq!(
1069            &wrapper.instructions[main_call - 2..main_call],
1070            &[
1071                CompiledInstruction::PUSHARG(DEFAULT_SECRET_REGISTER_START),
1072                CompiledInstruction::PUSHARG(DEFAULT_SECRET_REGISTER_START + 1),
1073            ]
1074        );
1075        assert!(
1076            !wrapper.instructions.iter().any(
1077                |instruction| matches!(instruction, CompiledInstruction::CALL(name) if name == "Share.open")
1078            ),
1079            "the local wrapper should return the VM value and leave share reveals to the runner"
1080        );
1081
1082        Ok(())
1083    }
1084
1085    #[test]
1086    fn local_client_input_wrapper_preserves_secret_bool_share_type() -> Result<()> {
1087        let program = crate::compiler::compile_source(
1088            "def main(a: list[secret bool]) -> bool:\n  return a[0].reveal()",
1089            "test.stfl",
1090            MpcBackend::HoneyBadger,
1091        )?;
1092
1093        let wrapped = program.with_local_client_input_wrapper(
1094            "main",
1095            "__stoffel_sdk_local_entry",
1096            &[LocalInputShape::List(vec![
1097                LocalInputShape::Share(ShareType::SecretInt { bit_length: 1 }),
1098                LocalInputShape::Share(ShareType::SecretInt { bit_length: 1 }),
1099            ])],
1100        )?;
1101
1102        assert_eq!(
1103            wrapped.binary.client_io_manifest.clients[0].inputs,
1104            vec![
1105                ShareType::SecretInt { bit_length: 1 },
1106                ShareType::SecretInt { bit_length: 1 },
1107            ]
1108        );
1109        Ok(())
1110    }
1111
1112    #[test]
1113    fn local_client_input_wrapper_keeps_clear_only_inputs_in_clear_registers() -> Result<()> {
1114        let program = crate::compiler::compile_source(
1115            "def main(a: int64, b: int64) -> int64:\n  return a + b",
1116            "test.stfl",
1117            MpcBackend::HoneyBadger,
1118        )?;
1119
1120        let wrapped = program.with_local_client_input_wrapper(
1121            "main",
1122            "__stoffel_sdk_local_entry",
1123            &[
1124                LocalInputShape::Clear(Value::I64(1)),
1125                LocalInputShape::Clear(Value::I64(2)),
1126            ],
1127        )?;
1128        let wrapper = wrapped
1129            .binary
1130            .functions
1131            .iter()
1132            .find(|function| function.name == "__stoffel_sdk_local_entry")
1133            .expect("wrapper function should be present");
1134
1135        assert!(wrapper.register_count < DEFAULT_SECRET_REGISTER_START);
1136        assert!(
1137            wrapped.binary.client_io_manifest.clients.is_empty(),
1138            "clear-only named input wrappers must not declare client input metadata"
1139        );
1140
1141        Ok(())
1142    }
1143}