From d37c4d991f7a0155ecd1e0f475dd1a801737fcdb Mon Sep 17 00:00:00 2001 From: Jake Miller Date: Mon, 9 Sep 2024 13:12:24 -0700 Subject: [PATCH] serialization: consolidate cue functionality, docs (#266) * serialization: consolidate cue functionality, docs - Merged cue_bitslice into a single cue function - Handles both atom/byteslice inputs. - Improved comments for clarity on jam/cue algo. - Added a bunch of tests. - Fix parse tests - Size check - Trying to cue with too small of a stack can cause a panic * add the hoon sources for reference * remove nonsense, fix cue_pill * formatting --- Cargo.lock | 3 + rust/sword/Cargo.toml | 1 + rust/sword/benches/cue_pill.rs | 2 +- rust/sword/src/jets/parse.rs | 8 +- rust/sword/src/serialization.rs | 444 ++++++++++++++++++++++++++++---- 5 files changed, 404 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03ae137..6a8994c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "addr2line" version = "0.22.0" @@ -1257,6 +1259,7 @@ dependencies = [ "murmur3", "num-derive", "num-traits", + "rand 0.8.5", "signal-hook", "static_assertions", "sword_crypto", diff --git a/rust/sword/Cargo.toml b/rust/sword/Cargo.toml index cbe7fe4..e79b07c 100644 --- a/rust/sword/Cargo.toml +++ b/rust/sword/Cargo.toml @@ -26,6 +26,7 @@ libc = "0.2.126" memmap = "0.7.0" num-derive = "0.3" num-traits = "0.2" +rand = "0.8.5" signal-hook = "0.3" static_assertions = "1.1.0" diff --git a/rust/sword/benches/cue_pill.rs b/rust/sword/benches/cue_pill.rs index 0a02824..5fbba35 100644 --- a/rust/sword/benches/cue_pill.rs +++ b/rust/sword/benches/cue_pill.rs @@ -33,7 +33,7 @@ fn main() -> io::Result<()> { break; }; i += 1; - input = cue(&mut stack, jammed_input); + input = cue(&mut stack, jammed_input).unwrap(); } match now.elapsed() { diff --git a/rust/sword/src/jets/parse.rs b/rust/sword/src/jets/parse.rs index 9538574..625a654 100644 --- a/rust/sword/src/jets/parse.rs +++ b/rust/sword/src/jets/parse.rs @@ -695,21 +695,21 @@ mod tests { let ans_jam = A(&mut c.stack, &ubig!(1720922644868600060465749189)) .as_atom() .unwrap(); - let ans = cue(&mut c.stack, ans_jam); + let ans = cue(&mut c.stack, ans_jam).unwrap(); let ctx = T(&mut c.stack, &[D(0), D(97), D(0)]); - assert_jet_door(c, jet_easy, sam, ctx, ans); + assert_jet_door(c, jet_easy, sam.unwrap(), ctx, ans); // ((easy %foo) [[1 1] "abc"]) // [[1 1] "abc"] let sam_jam = A(&mut c.stack, &ubig!(3205468216717221061)) .as_atom() .unwrap(); - let sam = cue(&mut c.stack, sam_jam); + let sam = cue(&mut c.stack, sam_jam).unwrap(); // [p=[p=1 q=1] q=[~ [p=%foo q=[p=[p=1 q=1] q="abc"]]]] let ans_jam = A(&mut c.stack, &ubig!(3609036366588910247778413036281029)) .as_atom() .unwrap(); - let ans = cue(&mut c.stack, ans_jam); + let ans = cue(&mut c.stack, ans_jam).unwrap(); let ctx = T(&mut c.stack, &[D(0), D(0x6f6f66), D(0)]); assert_jet_door(c, jet_easy, sam, ctx, ans); } diff --git a/rust/sword/src/serialization.rs b/rust/sword/src/serialization.rs index f594bfe..11b229a 100644 --- a/rust/sword/src/serialization.rs +++ b/rust/sword/src/serialization.rs @@ -1,29 +1,26 @@ use crate::hamt::MutHamt; -use crate::interpreter::Error::{self,*}; +use crate::interpreter::Error::{self, *}; use crate::interpreter::Mote::*; use crate::mem::NockStack; -use crate::noun::{Atom, Cell, D, DirectAtom, IndirectAtom, Noun}; +use crate::noun::{Atom, Cell, DirectAtom, IndirectAtom, Noun, D}; use bitvec::prelude::{BitSlice, Lsb0}; use either::Either::{Left, Right}; crate::gdb!(); +/// Calculate the number of bits needed to represent an atom pub fn met0_usize(atom: Atom) -> usize { let atom_bitslice = atom.as_bitslice(); - match atom_bitslice.last_one() { - Some(last_one) => last_one + 1, - None => 0, - } + atom_bitslice.last_one().map_or(0, |last_one| last_one + 1) } +/// Calculate the number of bits needed to represent a u64 as a usize pub fn met0_u64_to_usize(x: u64) -> usize { let usize_bitslice = BitSlice::::from_element(&x); - match usize_bitslice.last_one() { - Some(last_one) => last_one + 1, - None => 0, - } + usize_bitslice.last_one().map_or(0, |last_one| last_one + 1) } +/// Read the next bit from the bitslice and advance the cursor pub fn next_bit(cursor: &mut usize, slice: &BitSlice) -> bool { if (*slice).len() > *cursor { let res = slice[*cursor]; @@ -34,19 +31,24 @@ pub fn next_bit(cursor: &mut usize, slice: &BitSlice) -> bool { } } -pub fn next_n_bits<'a>(cursor: &mut usize, slice: &'a BitSlice, n: usize) -> &'a BitSlice { - let res = - if (slice).len() >= *cursor + n { - &slice[*cursor..*cursor + n] - } else if slice.len() > *cursor { - &slice[*cursor..] - } else { - BitSlice::::empty() - }; +/// Read the next n bits from the bitslice and advance the cursor +pub fn next_n_bits<'a>( + cursor: &mut usize, + slice: &'a BitSlice, + n: usize, +) -> &'a BitSlice { + let res = if (slice).len() >= *cursor + n { + &slice[*cursor..*cursor + n] + } else if slice.len() > *cursor { + &slice[*cursor..] + } else { + BitSlice::::empty() + }; *cursor += n; res } +/// Get the remaining bits from the cursor position pub fn rest_bits(cursor: usize, slice: &BitSlice) -> &BitSlice { if slice.len() > cursor { &slice[cursor..] @@ -55,28 +57,78 @@ pub fn rest_bits(cursor: usize, slice: &BitSlice) -> &BitSlice) -> Result { let backref_map = MutHamt::::new(stack); let mut result = D(0); let mut cursor = 0; + + let stack_size = stack.size(); + let input_size = buffer.len(); + if stack_size < input_size { + eprintln!("stack too small: {} < {}", stack_size, input_size); + return Err(Error::NonDeterministic(Fail, D(0))); + } + unsafe { stack.with_frame(0, |stack: &mut NockStack| { - // TODO: Pushing initial noun onto the stack to be used as a destination pointer? Why? *(stack.push::<*mut Noun>()) = &mut result as *mut Noun; loop { if stack.stack_is_empty() { break Ok(result); - }; - // We capture the destination pointer and then pop it off the stack. + } + // Capture the destination pointer and pop it off the stack let dest_ptr: *mut Noun = *(stack.top::<*mut Noun>()); stack.pop::<*mut Noun>(); - if next_bit(&mut cursor, buffer) { // 1 bit - if next_bit(&mut cursor, buffer) { // 11 tag: backref - let mut backref_noun = Atom::new(stack, rub_backref(&mut cursor, buffer)?).as_noun(); - *dest_ptr = backref_map.lookup(stack, &mut backref_noun).ok_or(Deterministic(Exit, D(0)))?; - } else { // 10 tag: cell + // 1 bit + if next_bit(&mut cursor, buffer) { + // 11 tag: backref + if next_bit(&mut cursor, buffer) { + let mut backref_noun = + Atom::new(stack, rub_backref(&mut cursor, buffer)?).as_noun(); + *dest_ptr = backref_map + .lookup(stack, &mut backref_noun) + .ok_or(Deterministic(Exit, D(0)))?; + } else { + // 10 tag: cell let (cell, cell_mem_ptr) = Cell::new_raw_mut(stack); *dest_ptr = cell.as_noun(); let mut backref_atom = Atom::new(stack, (cursor - 2) as u64).as_noun(); @@ -84,7 +136,8 @@ pub fn cue_bitslice(stack: &mut NockStack, buffer: &BitSlice) -> Resu *(stack.push()) = &mut (*cell_mem_ptr).tail; *(stack.push()) = &mut (*cell_mem_ptr).head; } - } else { // 0 tag: atom + } else { + // 0 tag: atom let backref: u64 = (cursor - 1) as u64; *dest_ptr = rub_atom(stack, &mut cursor, buffer)?.as_noun(); let mut backref_atom = Atom::new(stack, backref).as_noun(); @@ -95,12 +148,23 @@ pub fn cue_bitslice(stack: &mut NockStack, buffer: &BitSlice) -> Resu } } -pub fn cue(stack: &mut NockStack, buffer: Atom) -> Result { +/// Deserialize a noun from an Atom +/// +/// This function is a wrapper around cue_bitslice that takes an Atom as input. +/// +/// # Arguments +/// * `stack` - A mutable reference to the NockStack +/// * `buffer` - An Atom containing the serialized noun +/// +/// # Returns +/// A Result containing either the deserialized Noun or an Error +pub fn cue(stack: &mut NockStack, buffer: Atom) -> Result { let buffer_bitslice = buffer.as_bitslice(); cue_bitslice(stack, buffer_bitslice) } -// TODO: use first_zero() on a slice of the buffer +/// Get the size in bits of an encoded atom or backref +/// TODO: use first_zero() on a slice of the buffer fn get_size(cursor: &mut usize, buffer: &BitSlice) -> Result { let buff_at_cursor = rest_bits(*cursor, buffer); let bitsize = buff_at_cursor @@ -113,35 +177,57 @@ fn get_size(cursor: &mut usize, buffer: &BitSlice) -> Result) -> Result { +/// Length-decode an atom from the buffer +/// +/// Corresponds to `++rub` in the hoon stdlib. +/// +/// ```hoon +/// ++ rub :: length-decode +/// ~/ %rub +/// |= [a=@ b=@] +/// ^- [p=@ q=@] +/// =+ ^= c +/// =+ [c=0 m=(met 0 b)] +/// |- ?< (gth c m) +/// ?. =(0 (cut 0 [(add a c) 1] b)) +/// c +/// $(c +(c)) +/// ?: =(0 c) +/// [1 0] +/// =+ d=(add a +(c)) +/// =+ e=(add (bex (dec c)) (cut 0 [d (dec c)] b)) +/// [(add (add c c) e) (cut 0 [(add d (dec c)) e] b)] +/// ``` +fn rub_atom( + stack: &mut NockStack, + cursor: &mut usize, + buffer: &BitSlice, +) -> Result { let size = get_size(cursor, buffer)?; let bits = next_n_bits(cursor, buffer, size); if size == 0 { unsafe { Ok(DirectAtom::new_unchecked(0).as_atom()) } } else if size < 64 { - // fits in a direct atom + // Fits in a direct atom let mut direct_raw = 0; - BitSlice::from_element_mut(&mut direct_raw)[0..bits.len()] - .copy_from_bitslice(bits); + BitSlice::from_element_mut(&mut direct_raw)[0..bits.len()].copy_from_bitslice(bits); unsafe { Ok(DirectAtom::new_unchecked(direct_raw).as_atom()) } } else { - // need an indirect atom + // Need an indirect atom let wordsize = (size + 63) >> 6; - let (mut atom, slice) = unsafe { IndirectAtom::new_raw_mut_bitslice(stack, wordsize) }; // fast round to wordsize + let (mut atom, slice) = unsafe { IndirectAtom::new_raw_mut_bitslice(stack, wordsize) }; slice[0..bits.len()].copy_from_bitslice(bits); debug_assert!(atom.size() > 0); unsafe { Ok(atom.normalize_as_atom()) } } } -// TODO: rub_backref needs explanation. It's not clear what it's doing. It seems to be deserializing a backreference from a buffer. +/// Deserialize a backreference from the buffer fn rub_backref(cursor: &mut usize, buffer: &BitSlice) -> Result { // TODO: What's size here usually? let size = get_size(cursor, buffer)?; @@ -166,6 +252,11 @@ struct JamState<'a> { slice: &'a mut BitSlice, } +/// Serialize a noun into an atom +/// +/// Corresponds to ++jam in the hoon stdlib. +/// +/// Implements a compact encoding scheme for nouns, with backreferences for shared structures. pub fn jam(stack: &mut NockStack, noun: Noun) -> Atom { let backref_map = MutHamt::new(stack); let size = 8; @@ -234,6 +325,7 @@ pub fn jam(stack: &mut NockStack, noun: Noun) -> Atom { } } +/// Serialize an atom into the jam state fn jam_atom(traversal: &mut NockStack, state: &mut JamState, atom: Atom) { loop { if state.cursor + 1 > state.slice.len() { @@ -242,7 +334,7 @@ fn jam_atom(traversal: &mut NockStack, state: &mut JamState, atom: Atom) { break; } } - state.slice.set(state.cursor, false); + state.slice.set(state.cursor, false); // 0 tag for atom state.cursor += 1; loop { if let Ok(()) = mat(traversal, state, atom) { @@ -253,6 +345,7 @@ fn jam_atom(traversal: &mut NockStack, state: &mut JamState, atom: Atom) { } } +/// Serialize a cell into the jam state fn jam_cell(traversal: &mut NockStack, state: &mut JamState) { loop { if state.cursor + 2 > state.slice.len() { @@ -261,11 +354,12 @@ fn jam_cell(traversal: &mut NockStack, state: &mut JamState) { break; } } - state.slice.set(state.cursor, true); - state.slice.set(state.cursor + 1, false); + state.slice.set(state.cursor, true); // 1 bit + state.slice.set(state.cursor + 1, false); // 0 bit, forming 10 tag for cell state.cursor += 2; } +/// Serialize a backreference into the jam state fn jam_backref(traversal: &mut NockStack, state: &mut JamState, backref: u64) { loop { if state.cursor + 2 > state.slice.len() { @@ -274,8 +368,8 @@ fn jam_backref(traversal: &mut NockStack, state: &mut JamState, backref: u64) { break; } } - state.slice.set(state.cursor, true); - state.slice.set(state.cursor + 1, true); + state.slice.set(state.cursor, true); // 1 bit + state.slice.set(state.cursor + 1, true); // 1 bit, forming 11 tag for backref state.cursor += 2; let backref_atom = Atom::new(traversal, backref); loop { @@ -287,6 +381,7 @@ fn jam_backref(traversal: &mut NockStack, state: &mut JamState, backref: u64) { } } +/// Double the size of the atom in the jam state fn double_atom_size(traversal: &mut NockStack, state: &mut JamState) { let new_size = state.size + state.size; let (new_atom, new_slice) = unsafe { IndirectAtom::new_raw_mut_bitslice(traversal, new_size) }; @@ -296,7 +391,9 @@ fn double_atom_size(traversal: &mut NockStack, state: &mut JamState) { state.slice = new_slice; } -// INVARIANT: mat must not modify state.cursor unless it will also return `Ok(())` +/// Encode an atom's size and value into the jam state +/// +/// INVARIANT: mat must not modify state.cursor unless it will also return `Ok(())` fn mat(traversal: &mut NockStack, state: &mut JamState, atom: Atom) -> Result<(), ()> { let b_atom_size = met0_usize(atom); let b_atom_size_atom = Atom::new(traversal, b_atom_size as u64); @@ -319,9 +416,258 @@ fn mat(traversal: &mut NockStack, state: &mut JamState, atom: Atom) -> Result<() .copy_from_bitslice(&b_atom_size_atom.as_bitslice()[0..c_b_size - 1]); // the atom size excepting the most significant 1 (since we know where that is from the size-of-the-size) state.slice[state.cursor + c_b_size + c_b_size ..state.cursor + c_b_size + c_b_size + b_atom_size] - .copy_from_bitslice(&atom.as_bitslice()[0..b_atom_size]); // the atom itself + .copy_from_bitslice(&atom.as_bitslice()[0..b_atom_size]); state.cursor += c_b_size + c_b_size + b_atom_size; Ok(()) } } } + +#[cfg(test)] +mod tests { + + use rand::prelude::*; + + use super::*; + use crate::jets::util::test::assert_noun_eq; + use crate::mem::NockStack; + use crate::noun::{Atom, Cell, Noun}; + use crate::persist::Persist; + fn setup_stack() -> NockStack { + NockStack::new(1 << 30, 0) + } + + #[test] + fn test_jam_cue_atom() { + let mut stack = setup_stack(); + let atom = Atom::new(&mut stack, 42); + let jammed = jam(&mut stack, atom.as_noun()); + let cued = cue(&mut stack, jammed).unwrap(); + assert_noun_eq(&mut stack, cued, atom.as_noun()); + } + + #[test] + fn test_jam_cue_cell() { + let mut stack = setup_stack(); + let n1 = Atom::new(&mut stack, 1).as_noun(); + let n2 = Atom::new(&mut stack, 2).as_noun(); + let cell = Cell::new(&mut stack, n1, n2).as_noun(); + let jammed = jam(&mut stack, cell); + let cued = cue(&mut stack, jammed).unwrap(); + assert_noun_eq(&mut stack, cued, cell); + } + + #[test] + fn test_jam_cue_nested_cell() { + let mut stack = setup_stack(); + let n3 = Atom::new(&mut stack, 3).as_noun(); + let n4 = Atom::new(&mut stack, 4).as_noun(); + let inner_cell = Cell::new(&mut stack, n3, n4); + let n1 = Atom::new(&mut stack, 1).as_noun(); + let outer_cell = Cell::new(&mut stack, n1, inner_cell.as_noun()); + let jammed = jam(&mut stack, outer_cell.as_noun()); + let cued = cue(&mut stack, jammed).unwrap(); + assert_noun_eq(&mut stack, cued, outer_cell.as_noun()); + } + + #[test] + fn test_jam_cue_shared_structure() { + let mut stack = setup_stack(); + let shared_atom = Atom::new(&mut stack, 42); + let cell = Cell::new(&mut stack, shared_atom.as_noun(), shared_atom.as_noun()); + let jammed = jam(&mut stack, cell.as_noun()); + let cued = cue(&mut stack, jammed).unwrap(); + assert_noun_eq(&mut stack, cued, cell.as_noun()); + } + + #[test] + fn test_jam_cue_large_atom() { + let mut stack = setup_stack(); + let large_atom = Atom::new(&mut stack, u64::MAX); + let jammed = jam(&mut stack, large_atom.as_noun()); + let cued = cue(&mut stack, jammed).unwrap(); + assert_noun_eq(&mut stack, cued, large_atom.as_noun()); + } + + #[test] + fn test_jam_cue_empty_atom() { + let mut stack = setup_stack(); + let empty_atom = Atom::new(&mut stack, 0); + let jammed = jam(&mut stack, empty_atom.as_noun()); + let cued = cue(&mut stack, jammed).unwrap(); + assert_noun_eq(&mut stack, cued, empty_atom.as_noun()); + } + + #[test] + fn test_jam_cue_complex_structure() { + let mut stack = setup_stack(); + let atom1 = Atom::new(&mut stack, 1); + let atom2 = Atom::new(&mut stack, 2); + let cell1 = Cell::new(&mut stack, atom1.as_noun(), atom2.as_noun()); + let cell2 = Cell::new(&mut stack, cell1.as_noun(), atom2.as_noun()); + let cell3 = Cell::new(&mut stack, cell2.as_noun(), cell1.as_noun()); + let jammed = jam(&mut stack, cell3.as_noun()); + let cued = cue(&mut stack, jammed).unwrap(); + assert_noun_eq(&mut stack, cued, cell3.as_noun()); + } + + #[test] + fn test_cue_invalid_input() { + let mut stack = setup_stack(); + let invalid_atom = Atom::new(&mut stack, 0b11); // Invalid tag + let result = cue(&mut stack, invalid_atom); + assert!(result.is_err()); + } + + #[test] + fn test_jam_cue_roundtrip_property() { + let rng = StdRng::seed_from_u64(1); + let depth = 9; + println!("Testing noun with depth: {}", depth); + + let mut stack = setup_stack(); + let mut rng_clone = rng.clone(); + let (original, total_size) = generate_deeply_nested_noun(&mut stack, depth, &mut rng_clone); + + println!( + "Total size of all generated nouns: {:.2} KB", + total_size as f64 / 1024.0 + ); + println!("Original size: {:.2} KB", original.mass() as f64 / 1024.0); + let jammed = jam(&mut stack, original.clone()); + println!( + "Jammed size: {:.2} KB", + jammed.as_noun().mass() as f64 / 1024.0 + ); + let cued = cue(&mut stack, jammed).unwrap(); + println!("Cued size: {:.2} KB", cued.mass() as f64 / 1024.0); + + assert_noun_eq(&mut stack, cued, original); + } + + fn generate_random_noun(stack: &mut NockStack, bits: usize, rng: &mut StdRng) -> (Noun, usize) { + const MAX_DEPTH: usize = 100; // Adjust this value as needed + fn inner( + stack: &mut NockStack, + bits: usize, + rng: &mut StdRng, + depth: usize, + accumulated_size: usize, + ) -> (Noun, usize) { + let mut done = false; + if depth >= MAX_DEPTH || stack.size() < 1024 || accumulated_size > stack.size() - 1024 { + // println!("Done at depth and size: {} {:.2} KB", depth, accumulated_size as f64 / 1024.0); + done = true; + } + + let mut result = if rng.gen_bool(0.5) || done { + let value = rng.gen::(); + let atom = Atom::new(stack, value); + let noun = atom.as_noun(); + (noun, accumulated_size + noun.mass()) + } else { + let (left, left_size) = inner(stack, bits / 2, rng, depth + 1, accumulated_size); + let (right, _) = inner(stack, bits / 2, rng, depth + 1, left_size); + + let cell = Cell::new(stack, left, right); + let noun = cell.as_noun(); + (noun, noun.mass()) + }; + + if unsafe { result.0.space_needed(stack) } > stack.size() { + eprintln!( + "Stack size exceeded with noun size {:.2} KB", + result.0.mass() as f64 / 1024.0 + ); + unsafe { + let top_noun = *stack.top::(); + (top_noun, result.1) + } + } else { + result + } + } + + inner(stack, bits, rng, 0, 0) + } + + fn generate_deeply_nested_noun( + stack: &mut NockStack, + depth: usize, + rng: &mut StdRng, + ) -> (Noun, usize) { + if depth == 0 { + let (noun, size) = generate_random_noun(stack, 100, rng); + (noun, size) + } else { + let (left, left_size) = generate_deeply_nested_noun(stack, depth - 1, rng); + let (right, right_size) = generate_deeply_nested_noun(stack, depth - 1, rng); + let cell = Cell::new(stack, left, right); + let mut noun = cell.as_noun(); + let total_size = left_size + right_size + noun.mass(); + + if unsafe { noun.space_needed(stack) } > stack.size() { + eprintln!( + "Stack size exceeded at depth {} with noun size {:.2} KB", + depth, + noun.mass() as f64 / 1024.0 + ); + unsafe { + let top_noun = *stack.top::(); + (top_noun, total_size) + } + } else { + // println!("Size: {:.2} KB, depth: {}", noun.mass() as f64 / 1024.0, depth); + (noun, total_size) + } + } + } + + #[test] + fn test_cue_invalid_backreference() { + std::env::set_var("RUST_BACKTRACE", "full"); + + let mut stack = setup_stack(); + let invalid_atom = Atom::new(&mut stack, 0b11); // Invalid atom representation + let result = cue(&mut stack, invalid_atom); + + assert!(result.is_err()); + if let Err(e) = result { + println!("Error: {:?}", e); + assert!(matches!(e, Error::Deterministic(_, _))); + } + } + #[test] + fn test_cue_nondeterministic_error() { + let mut big_stack = NockStack::new(1 << 30, 0); + + let mut rng = StdRng::seed_from_u64(1); + + // Create an atom with a very large value to potentially cause overflow + let (large_atom, _) = generate_deeply_nested_noun(&mut big_stack, 5, &mut rng); + + // Attempt to jam and then cue the large atom in the big stack + let jammed = jam(&mut big_stack, large_atom); + + // make a smaller stack to try to cause a nondeterministic error + // NOTE: if the stack is big enough to fit the jammed atom, cue panics + let mut stack = NockStack::new(jammed.as_noun().mass() / 2 as usize, 0); + + // Attempt to cue the jammed noun with limited stack space + let result: Result<_, Error> = match cue(&mut stack, jammed) { + Ok(_res) => { + assert!(false, "Unexpected success: cue operation did not fail"); + Ok(()) + } + Err(e) => Err(e), + }; + + // Check if we got a nondeterministic error + println!("Result: {:?}", result); + assert!(result.is_err()); + if let Err(e) = result { + assert!(matches!(e, Error::NonDeterministic(_, _))); + println!("got expected error: {:?}", e); + } + } +}