1use std::collections::HashSet;
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10
11use crate::config::MpcBackend;
12use crate::error::{Error, Result};
13use crate::runtime::StoffelRuntime;
14use crate::types::Value;
15use stoffel_vm_types::core_types::{ShareType, TableRef, Value as VmValue};
16
17pub fn execute_clear(runtime: &StoffelRuntime, function_name: &str) -> Result<Vec<Value>> {
18 let args = runtime.input_values_for_function(function_name)?;
19 execute_clear_with_sdk_args(runtime, function_name, &args)
20}
21
22pub(crate) fn execute_clear_with_sdk_args(
23 runtime: &StoffelRuntime,
24 function_name: &str,
25 args: &[Value],
26) -> Result<Vec<Value>> {
27 runtime
28 .program()
29 .validate_function_args(function_name, args)?;
30
31 let mut vm = stoffel_vm::core_vm::VirtualMachine::try_new()
32 .map_err(|error| Error::Computation(error.to_string()))?;
33 for function in runtime
34 .program()
35 .binary()
36 .try_to_vm_functions()
37 .map_err(|error| Error::Bytecode(format!("{error:?}")))?
38 {
39 vm.try_register_function(function)
40 .map_err(|error| Error::Computation(error.to_string()))?;
41 }
42
43 let vm_args = args
44 .iter()
45 .map(|value| sdk_value_to_vm_value(&mut vm, value))
46 .collect::<Result<Vec<_>>>()?;
47 let value = vm
48 .execute_with_args(function_name, &vm_args)
49 .map_err(|error| Error::Computation(error.to_string()))?;
50 let value = sdk_value_from_vm_value(&mut vm, value, &mut HashSet::new(), 0)?;
51 match value {
52 Value::List(values) => Ok(values),
53 value => Ok(vec![value]),
54 }
55}
56
57pub fn execute_clear_with_args(
58 runtime: &StoffelRuntime,
59 function_name: &str,
60 args: &[stoffel_vm_types::core_types::Value],
61) -> Result<Vec<Value>> {
62 let sdk_args = args
63 .iter()
64 .filter_map(|value| Value::from_vm_value(value.clone()))
65 .collect::<Vec<_>>();
66 if sdk_args.len() == args.len() {
67 runtime
68 .program()
69 .validate_function_args(function_name, &sdk_args)?;
70 } else if runtime.program().function(function_name).is_none() {
71 return Err(Error::FunctionNotFound(function_name.to_owned()));
72 }
73
74 let mut vm = stoffel_vm::core_vm::VirtualMachine::try_new()
75 .map_err(|error| Error::Computation(error.to_string()))?;
76 for function in runtime
77 .program()
78 .binary()
79 .try_to_vm_functions()
80 .map_err(|error| Error::Bytecode(format!("{error:?}")))?
81 {
82 vm.try_register_function(function)
83 .map_err(|error| Error::Computation(error.to_string()))?;
84 }
85
86 let value = vm
87 .execute_with_args(function_name, args)
88 .map_err(|error| Error::Computation(error.to_string()))?;
89 let value = sdk_value_from_vm_value(&mut vm, value, &mut HashSet::new(), 0)?;
90 match value {
91 Value::List(values) => Ok(values),
92 value => Ok(vec![value]),
93 }
94}
95
96pub async fn execute_local(runtime: &StoffelRuntime, function_name: &str) -> Result<Vec<Value>> {
97 execute_local_with_options(runtime, function_name, LocalExecutionOptions::default()).await
98}
99
100#[derive(Debug, Clone, Default)]
101pub(crate) struct LocalExecutionOptions {
102 pub(crate) runner_path: Option<PathBuf>,
103 pub(crate) timeout: Option<Duration>,
104}
105
106#[derive(Debug, Clone, PartialEq)]
118pub struct LocalClientOutput {
119 pub client_slot: u64,
120 pub values: Vec<Value>,
121 pub raw: Vec<u64>,
122}
123
124impl LocalClientOutput {
125 pub fn bytes(&self) -> Vec<u8> {
134 let mut out = vec![0u8; self.values.len().div_ceil(8)];
135 for (index, value) in self.values.iter().enumerate() {
136 if value_is_set(value) {
137 out[index / 8] |= 1 << (index % 8);
138 }
139 }
140 out
141 }
142
143 pub fn bools(&self) -> Vec<bool> {
145 self.values.iter().map(value_is_set).collect()
146 }
147}
148
149fn value_is_set(value: &Value) -> bool {
150 match value {
151 Value::Bool(bit) => *bit,
152 Value::I64(value) => *value != 0,
153 Value::U64(value) => *value != 0,
154 _ => false,
155 }
156}
157
158fn decode_client_output_value(raw: u64, share_type: ShareType) -> Value {
161 match share_type {
162 ShareType::SecretInt { bit_length: 1 } => Value::Bool(raw != 0),
163 ShareType::SecretInt { .. } => Value::I64(raw as i64),
164 ShareType::SecretUInt { .. } => Value::U64(raw),
165 ShareType::SecretFixedPoint { precision } => {
166 let scale = (1u128 << precision.fractional_bits()) as f64;
167 Value::Float((raw as i64) as f64 / scale)
168 }
169 }
170}
171
172pub(crate) async fn execute_local_with_options(
173 runtime: &StoffelRuntime,
174 function_name: &str,
175 options: LocalExecutionOptions,
176) -> Result<Vec<Value>> {
177 let (returned, _program_output, _client_outputs) =
178 execute_local_capturing_with_options(runtime, function_name, options).await?;
179 Ok(returned)
180}
181
182pub(crate) async fn execute_local_capturing_with_options(
187 runtime: &StoffelRuntime,
188 function_name: &str,
189 options: LocalExecutionOptions,
190) -> Result<(Vec<Value>, String, Vec<LocalClientOutput>)> {
191 if runtime.program().function(function_name).is_none() {
192 return Err(Error::FunctionNotFound(function_name.to_owned()));
193 }
194 let vm_inputs = runtime.input_values_for_function(function_name)?;
195 if !vm_inputs.is_empty() {
196 return Err(Error::Unsupported(
197 "SDK local coordinator execution does not support direct function parameters; use a no-argument entrypoint and `with_client_input` for ClientStore values"
198 .to_owned(),
199 ));
200 }
201
202 let mpc_config = runtime
203 .mpc_config()
204 .ok_or_else(|| Error::Configuration("MPC configuration is required".to_owned()))?;
205 let flattened_client_inputs = flatten_local_client_inputs(runtime.client_inputs())?;
206 validate_flattened_local_client_inputs(runtime, &flattened_client_inputs)?;
207 if matches!(
208 mpc_config.backend,
209 MpcBackend::Avss {
210 curve: crate::config::Curve::Bn254
211 | crate::config::Curve::Curve25519
212 | crate::config::Curve::Ed25519
213 }
214 ) && !runtime.client_inputs().is_empty()
215 {
216 return Err(Error::Unsupported(
217 "SDK local coordinator execution currently supports AVSS local client inputs only for bls12_381"
218 .to_owned(),
219 ));
220 }
221 let local_client_inputs = flattened_client_inputs
222 .iter()
223 .map(|(client_slot, values)| {
224 Ok(stoffel_vm_runner::LocalClientInput::raw(
225 *client_slot,
226 values
227 .iter()
228 .map(local_client_input_value)
229 .collect::<Result<Vec<_>>>()?,
230 ))
231 })
232 .collect::<Result<Vec<_>>>()?;
233 let runner_path = resolve_stoffel_run_binary(
234 options
235 .runner_path
236 .as_deref()
237 .or_else(|| runtime.local_runner_binary_path()),
238 )?;
239 eprintln!(
240 "[stoffel] using stoffel-run binary: {}",
241 runner_path.display()
242 );
243
244 let mut runner = stoffel_vm_runner::LocalCoordinatorRunner::builder(
245 runner_path,
246 runtime.program().binary().clone(),
247 )
248 .entry(function_name)
249 .backend(local_runner_backend(mpc_config.backend))
250 .curve(local_runner_curve(mpc_config.backend))
251 .parties(mpc_config.parties)
252 .threshold(mpc_config.threshold);
253 if let Some(timeout) = options.timeout {
254 runner = runner.timeout(timeout);
255 }
256 if let Some(expected_clients) = runtime.configured_expected_clients() {
257 runner = runner.expected_output_clients(expected_clients);
258 }
259 for (client_slot, count) in runtime.client_output_counts() {
260 runner = runner.client_output_count(*client_slot, *count);
261 }
262 runner = runner.client_inputs(local_client_inputs);
263
264 let output = runner
265 .build()
266 .map_err(|error| Error::Configuration(error.to_string()))?
267 .run()
268 .await
269 .map_err(|error| Error::Computation(error.to_string()))?;
270
271 let program_output = local_program_output(&output);
272 print!("{program_output}");
273
274 let returned = output.consistent_returned_values().map_err(|error| {
275 Error::Computation(format!(
276 "local coordinator run did not produce consistent VM return values: {error}\noutput:\n{}",
277 output.combined_output
278 ))
279 })?;
280 let values = returned
281 .iter()
282 .map(|value| parse_runner_return_value(value))
283 .collect::<Result<Vec<_>>>()?;
284 let manifest = runtime.program().client_io_manifest();
285 let client_outputs = output
286 .client_outputs
287 .iter()
288 .map(|record| {
289 let output_types = manifest
290 .clients
291 .iter()
292 .find(|client| client.client_slot == record.client_slot)
293 .map(|client| client.outputs.as_slice())
294 .unwrap_or(&[]);
295 let typed = record
296 .values
297 .iter()
298 .enumerate()
299 .map(|(index, &raw)| match output_types.get(index) {
300 Some(share_type) => decode_client_output_value(raw, *share_type),
301 None => Value::U64(raw),
302 })
303 .collect();
304 LocalClientOutput {
305 client_slot: record.client_slot,
306 values: typed,
307 raw: record.values.clone(),
308 }
309 })
310 .collect();
311 Ok((values, program_output, client_outputs))
312}
313
314fn local_program_output(output: &stoffel_vm_runner::LocalCoordinatorRunOutput) -> String {
317 let Some(first_party) = output.party_outputs.first() else {
318 return String::new();
319 };
320 local_program_output_without_return_markers(&first_party.stdout)
321}
322
323fn local_program_output_without_return_markers(stdout: &str) -> String {
324 let mut output = String::new();
325 for line in stdout.lines() {
326 if line.trim().starts_with("Program returned: ") {
327 continue;
328 }
329 output.push_str(line);
330 output.push('\n');
331 }
332 output
333}
334
335fn local_runner_backend(backend: MpcBackend) -> stoffel_vm::net::MpcBackendKind {
336 match backend {
337 MpcBackend::HoneyBadger => stoffel_vm::net::MpcBackendKind::HoneyBadger,
338 MpcBackend::Avss { .. } => stoffel_vm::net::MpcBackendKind::Avss,
339 }
340}
341
342fn local_runner_curve(backend: MpcBackend) -> stoffel_vm::net::MpcCurveConfig {
343 match backend.curve() {
344 None | Some(crate::config::Curve::Bls12_381) => stoffel_vm::net::MpcCurveConfig::Bls12_381,
345 Some(crate::config::Curve::Bn254) => stoffel_vm::net::MpcCurveConfig::Bn254,
346 Some(crate::config::Curve::Curve25519) => stoffel_vm::net::MpcCurveConfig::Curve25519,
347 Some(crate::config::Curve::Ed25519) => stoffel_vm::net::MpcCurveConfig::Ed25519,
348 Some(crate::config::Curve::Secp256k1) => stoffel_vm::net::MpcCurveConfig::Secp256k1,
349 Some(crate::config::Curve::Secp256r1) => stoffel_vm::net::MpcCurveConfig::Secp256r1,
350 }
351}
352
353fn validate_flattened_local_client_inputs(
354 runtime: &StoffelRuntime,
355 flattened_client_inputs: &[(u64, Vec<Value>)],
356) -> Result<()> {
357 runtime
358 .program()
359 .validate_owned_client_inputs(flattened_client_inputs)
360}
361
362fn flatten_local_client_inputs(inputs: &[(u64, Vec<Value>)]) -> Result<Vec<(u64, Vec<Value>)>> {
363 inputs
364 .iter()
365 .map(|(client_slot, values)| {
366 let mut flattened = Vec::new();
367 for value in values {
368 flatten_local_client_input_value(value, &mut flattened)?;
369 }
370 Ok((*client_slot, flattened))
371 })
372 .collect()
373}
374
375fn flatten_local_client_input_value(value: &Value, out: &mut Vec<Value>) -> Result<()> {
376 match value {
377 Value::List(values) => {
378 for value in values {
379 flatten_local_client_input_value(value, out)?;
380 }
381 Ok(())
382 }
383 Value::Object(_) => Err(Error::InvalidInput(
384 "local coordinator client inputs cannot directly encode objects; pass their scalar secret fields or use typed lowering"
385 .to_owned(),
386 )),
387 Value::I64(_)
388 | Value::U64(_)
389 | Value::Bool(_)
390 | Value::Bytes(_)
391 | Value::Float(_)
392 | Value::String(_)
393 | Value::Unit => {
394 out.push(value.clone());
395 Ok(())
396 }
397 }
398}
399
400fn local_client_input_value(value: &Value) -> Result<String> {
401 match value {
402 Value::I64(value) => Ok(value.to_string()),
403 Value::U64(value) if i64::try_from(*value).is_ok() => Ok(value.to_string()),
404 Value::U64(value) => Ok(format!("0x{value:x}")),
405 Value::Bool(value) => Ok(if *value { "1" } else { "0" }.to_owned()),
406 Value::Bytes(value) => Ok(format!("0x{}", hex_encode(value))),
407 Value::Float(_) | Value::String(_) | Value::List(_) | Value::Object(_) | Value::Unit => {
408 Err(Error::InvalidInput(
409 "local coordinator client inputs support integers, booleans, 0x-prefixed hex bytes, and lists of those values"
410 .to_owned(),
411 ))
412 }
413 }
414}
415
416fn resolve_stoffel_run_binary(explicit_path: Option<&Path>) -> Result<PathBuf> {
417 if let Some(path) = explicit_path {
418 return resolve_existing_runner_path(path).ok_or_else(|| {
419 Error::Unsupported(format!(
420 "SDK local coordinator execution requires an existing stoffel-run binary; configured path does not exist: {}",
421 path.display()
422 ))
423 });
424 }
425
426 if let Some(path) = std::env::var_os("STOFFEL_RUN_BIN").map(PathBuf::from) {
427 return resolve_existing_runner_path(&path).ok_or_else(|| {
428 Error::Unsupported(format!(
429 "SDK local coordinator execution requires an existing stoffel-run binary; STOFFEL_RUN_BIN points to a missing path: {}",
430 path.display()
431 ))
432 });
433 }
434
435 if let Some(path) = built_workspace_runner() {
436 return Ok(path);
437 }
438
439 if let Some(path) = sibling_runner() {
440 return Ok(path);
441 }
442
443 if !running_from_workspace_target() {
444 if let Some(path) = path_runner() {
445 return Ok(path);
446 }
447 }
448
449 if let Some(workspace_root) = workspace_root() {
450 return Err(Error::Unsupported(format!(
451 "SDK local coordinator execution requires a built workspace stoffel-run binary at {}; build it with `cargo build -p stoffel-vm-runner --bin stoffel-run`, set STOFFEL_RUN_BIN, or call `local_runner_path`",
452 workspace_root
453 .join("target")
454 .join(if cfg!(debug_assertions) { "debug" } else { "release" })
455 .join(format!("stoffel-run{}", std::env::consts::EXE_SUFFIX))
456 .display()
457 )));
458 }
459
460 if let Some(path) = path_runner() {
461 return Ok(path);
462 }
463
464 Err(Error::Unsupported(
465 "SDK local coordinator execution requires a stoffel-run binary; install it with `cargo install --path crates/stoffel-vm-runner` (or `cargo install stoffel-vm-runner`) so it lands on your PATH, set STOFFEL_RUN_BIN, call `local_runner_path`, or build `cargo build -p stoffel-vm-runner --bin stoffel-run`"
466 .to_owned(),
467 ))
468}
469
470fn sibling_runner() -> Option<PathBuf> {
474 let exe = std::env::current_exe().ok()?;
475 let candidate = exe.with_file_name("stoffel-run");
476 candidate.exists().then_some(candidate)
477}
478
479fn path_runner() -> Option<PathBuf> {
481 find_binary_on_path("stoffel-run")
482}
483
484fn built_workspace_runner() -> Option<PathBuf> {
485 let workspace_root = workspace_root()?;
486 let mut candidates = Vec::new();
487
488 if let Some(profile_dir) = current_target_profile_dir() {
489 candidates.push(profile_dir.join(format!("stoffel-run{}", std::env::consts::EXE_SUFFIX)));
490 }
491
492 candidates.push(
493 workspace_root
494 .join("target")
495 .join(if cfg!(debug_assertions) {
496 "debug"
497 } else {
498 "release"
499 })
500 .join(format!("stoffel-run{}", std::env::consts::EXE_SUFFIX)),
501 );
502
503 candidates.push(
504 workspace_root
505 .join("target")
506 .join("debug")
507 .join(format!("stoffel-run{}", std::env::consts::EXE_SUFFIX)),
508 );
509
510 candidates
511 .into_iter()
512 .find(|candidate| is_executable_file(candidate))
513}
514
515fn current_target_profile_dir() -> Option<PathBuf> {
516 let exe = std::env::current_exe().ok()?;
517 target_profile_dir_from_exe(&exe)
518}
519
520fn running_from_workspace_target() -> bool {
521 let (Some(workspace_root), Ok(exe)) = (workspace_root(), std::env::current_exe()) else {
522 return false;
523 };
524 path_is_under(&exe, &workspace_root.join("target"))
525}
526
527fn path_is_under(path: &Path, ancestor: &Path) -> bool {
528 path.ancestors().any(|candidate| candidate == ancestor)
529}
530
531fn target_profile_dir_from_exe(exe: &Path) -> Option<PathBuf> {
532 let parent = exe.parent()?;
533 let profile_dir = if parent.file_name().is_some_and(|name| name == "deps") {
534 parent.parent()?
535 } else {
536 parent
537 };
538 profile_dir
539 .parent()
540 .and_then(Path::file_name)
541 .is_some_and(|name| name == "target")
542 .then(|| profile_dir.to_path_buf())
543}
544
545fn find_binary_on_path(binary_name: &str) -> Option<PathBuf> {
546 let path = std::env::var_os("PATH")?;
547
548 find_binary_in_path(binary_name, &path)
549}
550
551fn find_binary_in_path(binary_name: &str, path: &std::ffi::OsStr) -> Option<PathBuf> {
552 let binary_name = format!("{binary_name}{}", std::env::consts::EXE_SUFFIX);
553
554 std::env::split_paths(&path)
555 .map(|dir| dir.join(&binary_name))
556 .find(|candidate| is_executable_file(candidate))
557}
558
559#[cfg(unix)]
560fn is_executable_file(path: &Path) -> bool {
561 use std::os::unix::fs::PermissionsExt;
562
563 path.is_file()
564 && path
565 .metadata()
566 .map(|metadata| metadata.permissions().mode() & 0o111 != 0)
567 .unwrap_or(false)
568}
569
570#[cfg(not(unix))]
571fn is_executable_file(path: &Path) -> bool {
572 path.is_file()
573}
574
575fn resolve_existing_runner_path(path: &Path) -> Option<PathBuf> {
576 if path.exists() {
577 return Some(path.to_path_buf());
578 }
579 if path.is_absolute() {
580 return None;
581 }
582 workspace_root()
583 .map(|root| root.join(path))
584 .filter(|candidate| candidate.exists())
585}
586
587fn workspace_root() -> Option<PathBuf> {
588 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
589 .parent()
590 .and_then(|path| path.parent())
591 .map(Path::to_path_buf)
592}
593
594fn parse_runner_return_value(value: &str) -> Result<Value> {
595 let value = value.trim();
596 if value == "true" {
597 return Ok(Value::Bool(true));
598 }
599 if value == "false" {
600 return Ok(Value::Bool(false));
601 }
602 if value == "()" || value == "Unit" {
603 return Ok(Value::Unit);
604 }
605 if let Ok(value) = value.parse::<i64>() {
606 return Ok(Value::I64(value));
607 }
608 if let Ok(value) = value.parse::<u64>() {
609 return Ok(Value::U64(value));
610 }
611 if let Ok(value) = value.parse::<f64>() {
612 return Ok(Value::Float(value));
613 }
614 Ok(Value::String(value.to_owned()))
615}
616
617fn hex_encode(bytes: &[u8]) -> String {
618 const HEX: &[u8; 16] = b"0123456789abcdef";
619 let mut out = String::with_capacity(bytes.len() * 2);
620 for byte in bytes {
621 out.push(HEX[(byte >> 4) as usize] as char);
622 out.push(HEX[(byte & 0x0f) as usize] as char);
623 }
624 out
625}
626
627fn sdk_value_to_vm_value(
628 vm: &mut stoffel_vm::core_vm::VirtualMachine,
629 value: &Value,
630) -> Result<VmValue> {
631 match value {
632 Value::I64(value) => Ok(VmValue::I64(*value)),
633 Value::U64(value) => Ok(VmValue::U64(*value)),
634 Value::Bool(value) => Ok(VmValue::Bool(*value)),
635 Value::Float(value) => Ok(VmValue::Float((*value).into())),
636 Value::String(value) => Ok(VmValue::String(value.clone())),
637 Value::Bytes(_) => Err(Error::InvalidInput(
638 "byte inputs are only supported for local coordinator client inputs".to_owned(),
639 )),
640 Value::List(values) => {
641 let array_ref = vm
642 .create_array_ref(values.len())
643 .map_err(|error| Error::Computation(error.to_string()))?;
644 let values = values
645 .iter()
646 .map(|value| sdk_value_to_vm_value(vm, value))
647 .collect::<Result<Vec<_>>>()?;
648 vm.push_array_ref_values(array_ref, &values)
649 .map_err(|error| Error::Computation(error.to_string()))?;
650 Ok(VmValue::from(array_ref))
651 }
652 Value::Object(fields) => {
653 let object_ref = vm
654 .create_object_ref()
655 .map_err(|error| Error::Computation(error.to_string()))?;
656 let table_ref = TableRef::from(object_ref);
657 for (name, field_value) in fields {
658 let field_value = sdk_value_to_vm_value(vm, field_value)?;
659 vm.set_table_field(table_ref, VmValue::String(name.clone()), field_value)
660 .map_err(|error| Error::Computation(error.to_string()))?;
661 }
662 Ok(VmValue::from(object_ref))
663 }
664 Value::Unit => Ok(VmValue::Unit),
665 }
666}
667
668fn sdk_value_from_vm_value(
669 vm: &mut stoffel_vm::core_vm::VirtualMachine,
670 value: VmValue,
671 active_tables: &mut HashSet<TableRef>,
672 depth: usize,
673) -> Result<Value> {
674 const MAX_TABLE_DEPTH: usize = 32;
675
676 match value {
677 VmValue::Array(array_ref) => {
678 if depth >= MAX_TABLE_DEPTH {
679 return Err(Error::Computation(format!(
680 "VM array output exceeds maximum SDK conversion depth of {MAX_TABLE_DEPTH}"
681 )));
682 }
683 let table_ref = TableRef::from(array_ref);
684 if !active_tables.insert(table_ref) {
685 return Err(Error::Computation(format!(
686 "VM array output contains a cycle at array ref {}",
687 array_ref.id()
688 )));
689 }
690 let len = vm
691 .read_array_ref_len(array_ref)
692 .map_err(|error| Error::Computation(error.to_string()))?;
693 let mut values = Vec::with_capacity(len);
694 for index in 0..len {
695 let index = i64::try_from(index).map_err(|_| {
696 Error::Computation("VM array index cannot be represented as int64".to_owned())
697 })?;
698 let item = vm
699 .read_table_field(TableRef::from(array_ref), &VmValue::I64(index))
700 .map_err(|error| Error::Computation(error.to_string()))?
701 .ok_or_else(|| {
702 Error::Computation(format!("VM array is missing element at index {index}"))
703 })?;
704 values.push(sdk_value_from_vm_value(vm, item, active_tables, depth + 1)?);
705 }
706 active_tables.remove(&table_ref);
707 Ok(Value::List(values))
708 }
709 VmValue::Object(object_ref) => {
710 if depth >= MAX_TABLE_DEPTH {
711 return Err(Error::Computation(format!(
712 "VM object output exceeds maximum SDK conversion depth of {MAX_TABLE_DEPTH}"
713 )));
714 }
715 let table_ref = TableRef::from(object_ref);
716 if !active_tables.insert(table_ref) {
717 return Err(Error::Computation(format!(
718 "VM object output contains a cycle at object ref {}",
719 object_ref.id()
720 )));
721 }
722 let len = vm
723 .read_object_ref_len(object_ref)
724 .map_err(|error| Error::Computation(error.to_string()))?;
725 let entries = vm
726 .read_object_ref_entries(object_ref, len)
727 .map_err(|error| Error::Computation(error.to_string()))?;
728 let mut fields = std::collections::BTreeMap::new();
729 for (key, value) in entries {
730 let VmValue::String(key) = key else {
731 return Err(Error::Computation(format!(
732 "VM object output contains a non-string field key: {key:?}"
733 )));
734 };
735 fields.insert(
736 key,
737 sdk_value_from_vm_value(vm, value, active_tables, depth + 1)?,
738 );
739 }
740 active_tables.remove(&table_ref);
741 Ok(Value::Object(fields))
742 }
743 value => Value::from_vm_value(value).ok_or_else(|| {
744 Error::Computation(
745 "VM returned a value that cannot be represented by the public SDK Value type"
746 .to_owned(),
747 )
748 }),
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::{
755 find_binary_in_path, local_program_output_without_return_markers, path_is_under,
756 target_profile_dir_from_exe,
757 };
758 use std::path::PathBuf;
759
760 #[test]
761 fn local_program_output_filter_removes_runner_return_markers() {
762 let stdout = "polynomial p\nProgram returned: ()\n";
763
764 assert_eq!(
765 local_program_output_without_return_markers(stdout),
766 "polynomial p\n"
767 );
768 }
769
770 #[test]
771 fn find_binary_in_path_finds_executable_file() {
772 let missing_dir = tempfile::tempdir().unwrap();
773 let bin_dir = tempfile::tempdir().unwrap();
774 let binary = bin_dir
775 .path()
776 .join(format!("stoffel-run{}", std::env::consts::EXE_SUFFIX));
777 std::fs::write(&binary, b"#!/bin/sh\n").unwrap();
778
779 #[cfg(unix)]
780 {
781 use std::os::unix::fs::PermissionsExt;
782
783 let mut permissions = std::fs::metadata(&binary).unwrap().permissions();
784 permissions.set_mode(0o755);
785 std::fs::set_permissions(&binary, permissions).unwrap();
786 }
787
788 let path = std::env::join_paths([missing_dir.path(), bin_dir.path()]).unwrap();
789
790 assert_eq!(find_binary_in_path("stoffel-run", &path), Some(binary));
791 }
792
793 #[test]
794 fn target_profile_dir_from_test_exe_uses_parent_of_deps_dir() {
795 let exe: PathBuf = ["workspace", "target", "debug", "deps", "sdk_usage-abc"]
796 .iter()
797 .collect();
798
799 assert_eq!(
800 target_profile_dir_from_exe(&exe),
801 Some(["workspace", "target", "debug"].iter().collect())
802 );
803 }
804
805 #[test]
806 fn path_is_under_detects_workspace_target_executables() {
807 let exe: PathBuf = ["workspace", "target", "debug", "deps", "sdk_usage-abc"]
808 .iter()
809 .collect();
810 let target: PathBuf = ["workspace", "target"].iter().collect();
811 let other: PathBuf = ["workspace", "other-target"].iter().collect();
812
813 assert!(path_is_under(&exe, &target));
814 assert!(!path_is_under(&exe, &other));
815 }
816}