From e7491a33c447890e3a7bb04460bd362e80f7e16d Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 7 Jan 2026 10:31:01 -0300 Subject: [PATCH 01/18] add IO::args --- cli/src/common.rs | 3 +- cli/src/ivy_cli.rs | 2 +- cli/src/vine_cli.rs | 3 +- ivm/src/ext.rs | 50 ++++++++++++++++++- ivy/src/host/ext.rs | 47 +++++++++++++++--- ivy/src/optimize/pre_reduce.rs | 2 +- root/IO.vi | 89 ++++++++++++++++++++++++++++++++++ 7 files changed, 185 insertions(+), 11 deletions(-) diff --git a/cli/src/common.rs b/cli/src/common.rs index e3c7c289b..b5266c72f 100644 --- a/cli/src/common.rs +++ b/cli/src/common.rs @@ -34,6 +34,7 @@ pub struct RunArgs { breadth_first: bool, #[arg(long, short = 'H', value_parser = parse_size)] heap: Option, + pub args: Vec, } impl RunArgs { @@ -45,7 +46,7 @@ impl RunArgs { }; let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics); + host.register_default_extrinsics(&mut extrinsics, self.args); host.insert_nets(&nets); let main = host.get("::").expect("missing main"); diff --git a/cli/src/ivy_cli.rs b/cli/src/ivy_cli.rs index c728e22a9..cce5f18bf 100644 --- a/cli/src/ivy_cli.rs +++ b/cli/src/ivy_cli.rs @@ -83,7 +83,7 @@ impl IvyReplCommand { let heap = Heap::new(); let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics); + host.register_default_extrinsics(&mut extrinsics, self.run_args.args); host.insert_nets(&nets); let mut ivm = IVM::new(&heap, &extrinsics); diff --git a/cli/src/vine_cli.rs b/cli/src/vine_cli.rs index aac501e81..f049dfae6 100644 --- a/cli/src/vine_cli.rs +++ b/cli/src/vine_cli.rs @@ -320,6 +320,7 @@ pub struct VineReplCommand { echo: bool, #[arg(long)] no_debug: bool, + args: Vec, } impl VineReplCommand { @@ -331,7 +332,7 @@ impl VineReplCommand { let host = &mut Host::default(); let heap = Heap::new(); let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics); + host.register_default_extrinsics(&mut extrinsics, self.args); let mut ivm = IVM::new(&heap, &extrinsics); let config = Config::new(!self.no_debug, false); diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index af4984099..e8749a631 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -183,9 +183,16 @@ impl<'ivm> Debug for ExtVal<'ivm> { } } -#[derive(Clone, Copy)] pub struct ExtTy<'ivm, T>(ExtTyId<'ivm>, PhantomData T>); +impl<'ivm, T> Clone for ExtTy<'ivm, T> { + fn clone(&self) -> Self { + *self + } +} + +impl<'ivm, T> Copy for ExtTy<'ivm, T> {} + impl<'ivm, T> ExtTy<'ivm, T> { pub fn new_unchecked(ty_id: ExtTyId<'ivm>) -> Self { Self(ty_id, PhantomData) @@ -287,6 +294,47 @@ impl<'ivm> ExtTyCast<'ivm> for f64 { } } +pub struct ExtIter { + elements: Vec, + idx: usize, +} + +impl ExtIter { + pub fn new(elements: Vec) -> Self { + Self { elements, idx: 0 } + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.elements.len() + } + + pub fn current(&self) -> Option<&I> { + self.elements.get(self.idx) + } + + pub fn advance(self) -> Self { + Self { elements: self.elements, idx: self.idx + 1 } + } +} + +impl<'ivm, I> ExtTyCast<'ivm> for ExtIter { + const COPY: bool = false; + + #[inline(always)] + fn into_payload(self) -> Word { + let pointer = Box::into_raw(Box::new(Aligned(self))); + Word::from_ptr(pointer.cast()) + } + + #[inline(always)] + unsafe fn from_payload(payload: Word) -> Self { + let ptr = payload.ptr().cast_mut().cast(); + let Aligned(iter) = unsafe { *Box::from_raw(ptr) }; + iter + } +} + /// Used for the `IO` extrinsic type. impl<'ivm> ExtTyCast<'ivm> for () { const COPY: bool = false; diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index 557749174..fc4e67eb7 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -4,7 +4,7 @@ use std::{ }; use ivm::{ - ext::{ExtFn, ExtTy, ExtTyId, Extrinsics}, + ext::{ExtFn, ExtIter, ExtTy, ExtTyId, Extrinsics}, port::Port, }; @@ -121,10 +121,34 @@ impl<'ivm> Host<'ivm> { f64_ty } - pub fn register_default_extrinsics(&mut self, extrinsics: &mut Extrinsics<'ivm>) { + fn register_strs_ext_ty( + &mut self, + extrinsics: &mut Extrinsics<'ivm>, + ) -> ExtTy<'ivm, ExtIter> { + let strs_ty = extrinsics.new_ext_ty(); + self.register_ext_ty_id("STRS".into(), strs_ty.ty_id()); + strs_ty + } + + fn register_str_ext_ty( + &mut self, + extrinsics: &mut Extrinsics<'ivm>, + ) -> ExtTy<'ivm, ExtIter> { + let str_ty = extrinsics.new_ext_ty(); + self.register_ext_ty_id("STR".into(), str_ty.ty_id()); + str_ty + } + + pub fn register_default_extrinsics( + &mut self, + extrinsics: &mut Extrinsics<'ivm>, + args: Vec, + ) { let n32 = self.register_n32_ext_ty(extrinsics); let f32 = self.register_f32_ext_ty(extrinsics); let f64 = self.register_f64_ext_ty(extrinsics); + let str = self.register_str_ext_ty(extrinsics); + let strs = self.register_strs_ext_ty(extrinsics); let io = self.register_io_ext_ty(extrinsics); // u64 to/from (lo: u32, hi: u32) halves @@ -177,10 +201,6 @@ impl<'ivm> Host<'ivm> { "i32_lt" => |a: n32, b: n32| -> n32 { ((a as i32) < (b as i32)) as u32 }, "i32_le" => |a: n32, b: n32| -> n32 { ((a as i32) <= (b as i32)) as u32 }, - "io_join" => |_io_a: io, _io_b: io| -> io {}, - "io_split" => |_io: io| -> (io, io) { ((), ()) }, - "io_ready" => |_io: io| -> (n32, io) { (1, ()) }, - "f64_fork" => |f: f64| -> (f64, f64) { (f, f) }, "f64_drop" => |f: f64| {}, @@ -204,6 +224,21 @@ impl<'ivm> Host<'ivm> { "f64_to_bits" => |f: f64| -> (n32, n32) { u64_to_parts(f.to_bits()) }, "f64_from_bits" => |lo: n32, hi: n32| -> f64 { f64::from_bits(u64_from_parts(lo, hi)) }, + "str_len" => |s: str| -> (n32, str) { (s.len() as u32, s) }, + "str_next" => |s: str| -> (n32, str) { (*s.current()? as u32, s.advance()) }, + "str_drop" => |s: str| {}, + + "io_join" => |_io_a: io, _io_b: io| -> io {}, + "io_split" => |_io: io| -> (io, io) { ((), ()) }, + "io_ready" => |_io: io| -> (n32, io) { (1, ()) }, + + "io_args" => |io0: io| -> (strs, io) { (ExtIter::new(args.clone()), io0) }, + "args_len" => |args: strs| -> (n32, strs) { (args.len() as u32, args) }, + "args_next" => |args: strs| -> (str, strs) { + (ExtIter::new(args.current()?.chars().collect()), args.advance()) + }, + "args_drop" => |args: strs| {}, + "io_print_char" => |_io: io, b: n32| -> io { print!("{}", char::try_from(b).unwrap()); }, diff --git a/ivy/src/optimize/pre_reduce.rs b/ivy/src/optimize/pre_reduce.rs index 47b948e28..bdf4d402f 100644 --- a/ivy/src/optimize/pre_reduce.rs +++ b/ivy/src/optimize/pre_reduce.rs @@ -18,7 +18,7 @@ pub fn pre_reduce(nets: &mut Nets) { let heap = &Heap::new(); let mut host = &mut Host::default(); let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics); + host.register_default_extrinsics(&mut extrinsics, Vec::new()); host._insert_nets(nets, true); for (name, net) in nets.iter_mut() { diff --git a/root/IO.vi b/root/IO.vi index d23edad76..031a839f9 100644 --- a/root/IO.vi +++ b/root/IO.vi @@ -1,4 +1,6 @@ +use #root::ops::Cast; + /// A special primitive type used to interact with the outside world. /// Values of this type cannot be explicitly constructed; instead, /// an IO handle is passed in to `main` at the start of the program. @@ -137,4 +139,91 @@ pub mod IO { io0 = @io_ready(signal io1) } } + + type Args; + + mod Args { + pub fn new(&io: &IO) -> Args { + inline_ivy! (io0 <- io, io1 -> io) -> Args { + args + io0 = @io_args(args io1) + } + } + + pub fn .len(&self: &Args) -> N32 { + inline_ivy! (args0 <- self, args1 -> self) -> N32 { + len + args0 = @args_len(len args1) + } + } + + pub fn .next(&self: &Args) -> String { + inline_ivy! (args0 <- self, args1 -> self) -> ExtString { + str + args0 = @args_next(str args1) + } as String + } + + pub impl drop: Drop[Args] { + fn drop(self: Args) { + inline_ivy! (args <- self) -> () { + _ + args = @args_drop(_ _) + } + } + } + } + + type ExtString; + + mod ExtString { + pub fn .len(&self: &ExtString) -> N32 { + inline_ivy! (str0 <- self, str1 -> self) -> N32 { + len + str0 = @str_len(len str1) + } + } + + pub fn .next(&self: &ExtString) -> Char { + inline_ivy! (str0 <- self, str1 -> self) -> Char { + char + str0 = @str_next(char str1) + } + } + + pub impl drop: Drop[ExtString] { + fn drop(self: ExtString) { + inline_ivy! (str <- self) -> () { + _ + str = @str_drop(_ _) + } + } + } + + pub impl to_string: Cast[ExtString, String] { + fn cast(self: ExtString) -> String { + String(List::from_fn(self.len(), fn* () { self.next() })) + } + } + } + + /// Returns the command-line arguments that vine was executed with. + /// + /// Unlike most other programming languages, the first argument _is not_ a + /// path to the binary being executed, it is the first argument provided to + /// the vine binary. + /// ```vi + /// // example.vi + /// pub fn main(&io: &IO) { + /// let args = io.args(); + /// io.println("args: {args.show()}"); + /// } + /// + /// // > vine run example.vi -- some cli args + /// // ["some", "cli", "args"] + /// ``` + pub fn .args(&io: &IO) -> List[String] { + let args = Args::new(&io); + List::from_fn(args.len(), fn* () { args.next() }) + } } From 61a7b64e23675489bdccb333290a2495f0bd49c3 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 7 Jan 2026 10:31:06 -0300 Subject: [PATCH 02/18] add IO::args tests --- tests/programs/cli_args.vi | 5 + tests/snaps/vine/cli_args/output.txt | 1 + tests/snaps/vine/cli_args/stats.txt | 15 +++ tests/tests.rs | 150 ++++++++++++++------------- 4 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 tests/programs/cli_args.vi create mode 100644 tests/snaps/vine/cli_args/output.txt create mode 100644 tests/snaps/vine/cli_args/stats.txt diff --git a/tests/programs/cli_args.vi b/tests/programs/cli_args.vi new file mode 100644 index 000000000..2e8a8a7d6 --- /dev/null +++ b/tests/programs/cli_args.vi @@ -0,0 +1,5 @@ + +pub fn main(&io: &IO) { + let args = io.args(); + io.println("{args.show()}"); +} diff --git a/tests/snaps/vine/cli_args/output.txt b/tests/snaps/vine/cli_args/output.txt new file mode 100644 index 000000000..2ef2020a2 --- /dev/null +++ b/tests/snaps/vine/cli_args/output.txt @@ -0,0 +1 @@ +["some", "cli", "args"] diff --git a/tests/snaps/vine/cli_args/stats.txt b/tests/snaps/vine/cli_args/stats.txt new file mode 100644 index 000000000..2fbec7aea --- /dev/null +++ b/tests/snaps/vine/cli_args/stats.txt @@ -0,0 +1,15 @@ + +Interactions + Total 2_177 + Annihilate 1_063 + Commute 17 + Copy 216 + Erase 308 + Expand 196 + Call 269 + Branch 108 + +Memory + Heap 2_832 B + Allocated 44_880 B + Freed 44_880 B diff --git a/tests/tests.rs b/tests/tests.rs index 3faa70ac0..835d26593 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -33,80 +33,81 @@ fn tests(t: &mut DynTester) { }); t.group("vine", |t| { - test_vi(t, "vine/examples/cat.vi", cat_input, ".txt", true, false, false); - test_vi(t, "vine/examples/fib_repl.vi", fib_repl_input_vi, ".txt", true, false, false); - test_vi(t, "vine/examples/fib.vi", b"", ".txt", true, false, false); - test_vi(t, "vine/examples/fizzbuzz.vi", b"", ".txt", true, false, false); - test_vi(t, "vine/examples/guessing_game.vi", guessing_game_input, ".txt", true, false, false); - test_vi(t, "vine/examples/hello_world.vi", b"", ".txt", true, false, false); - test_vi(t, "vine/examples/mandelbrot_sixel.vi", b"", ".sixel", true, false, false); - test_vi(t, "vine/examples/mandelbrot_tga.vi", b"", ".tga", false, false, false); - test_vi(t, "vine/examples/mandelbrot.vi", b"", ".txt", true, false, false); - test_vi(t, "vine/examples/primeness.vi", b"", ".txt", true, false, false); - test_vi(t, "vine/examples/stream_primes.vi", b"", ".txt", true, false, false); - test_vi(t, "vine/examples/sub_min.vi", b"", ".txt", true, false, false); - test_vi(t, "vine/examples/sum_divisors.vi", b"", ".txt", true, false, false); - - test_vi(t, "tests/programs/array_from_list.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/array_order.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/array_smoothsort.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/array_to_list.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/basic_diverge.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/brainfuck.vi", brainfuck_input, ".txt", true, false, false); - test_vi(t, "tests/programs/break_result.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/centimanes.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/classify_primes.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/cond_diverge.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/cubes.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/cyclist.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/div_by_zero.vi", b"", ".txt", false, true, true); - test_vi(t, "tests/programs/empty_loop.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/f32_roundabout.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/final_countdown.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/find_primes.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/heap.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/int_edges.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/invalid_extrinsics.vi", b"", ".txt", true, false, true); - test_vi(t, "tests/programs/inverse.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/iterator_party.vi", b"", ".txt", false, true, false); - test_vi(t, "tests/programs/lambda.vi", lambda_input, ".txt", true, false, false); - test_vi(t, "tests/programs/lcs.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/life.vi", life_input, ".txt", true, false, false); - test_vi(t, "tests/programs/log_brute.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/logic.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/loop_break_continue.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/loop_vi_loop.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/main.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/mandelbrot_f64.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/map_test.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/map_ops.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/maybe_set.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/n64_div_rem.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/nat_div.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/nat_edges.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/nothing_lasts_forever.vi", b"", ".txt", true, false, true); - test_vi(t, "tests/programs/no_return.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/option_party.vi", b"", ".txt", false, true, true); - test_vi(t, "tests/programs/par.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/pretty_div.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/primenesses.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/quine.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/segmented_sieve.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/sieve.vi", b"", ".txt", false, false, false); - test_vi(t, "tests/programs/sort.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/so_random.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/specializations.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/square_case.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/the_greatest_show.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/tiny_f64.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/verbose_add.vi", b"", ".txt", true, false, false); - test_vi(t, "tests/programs/when_break_continue.vi", b"", ".txt", true, false, false); + test_vi(t, "vine/examples/cat.vi", cat_input, ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/fib_repl.vi", fib_repl_input_vi, ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/fib.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/fizzbuzz.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/guessing_game.vi", guessing_game_input, ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/hello_world.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/mandelbrot_sixel.vi", b"", ".sixel", true, false, false, &[]); + test_vi(t, "vine/examples/mandelbrot_tga.vi", b"", ".tga", false, false, false, &[]); + test_vi(t, "vine/examples/mandelbrot.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/primeness.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/stream_primes.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/sub_min.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "vine/examples/sum_divisors.vi", b"", ".txt", true, false, false, &[]); + + test_vi(t, "tests/programs/array_from_list.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/array_order.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/array_smoothsort.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/array_to_list.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/basic_diverge.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/brainfuck.vi", brainfuck_input, ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/break_result.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/centimanes.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/classify_primes.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/cli_args.vi", b"", ".txt", false, false, false, &["some", "cli", "args"]); + test_vi(t, "tests/programs/cond_diverge.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/cubes.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/cyclist.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/div_by_zero.vi", b"", ".txt", false, true, true, &[]); + test_vi(t, "tests/programs/empty_loop.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/f32_roundabout.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/final_countdown.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/find_primes.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/heap.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/int_edges.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/invalid_extrinsics.vi", b"", ".txt", true, false, true, &[]); + test_vi(t, "tests/programs/inverse.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/iterator_party.vi", b"", ".txt", false, true, false, &[]); + test_vi(t, "tests/programs/lambda.vi", lambda_input, ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/lcs.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/life.vi", life_input, ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/log_brute.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/logic.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/loop_break_continue.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/loop_vi_loop.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/main.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/mandelbrot_f64.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/map_test.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/map_ops.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/maybe_set.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/n64_div_rem.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/nat_div.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/nat_edges.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/nothing_lasts_forever.vi", b"", ".txt", true, false, true, &[]); + test_vi(t, "tests/programs/no_return.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/option_party.vi", b"", ".txt", false, true, true, &[]); + test_vi(t, "tests/programs/par.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/pretty_div.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/primenesses.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/quine.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/segmented_sieve.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/sieve.vi", b"", ".txt", false, false, false, &[]); + test_vi(t, "tests/programs/sort.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/so_random.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/specializations.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/square_case.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/the_greatest_show.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/tiny_f64.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/verbose_add.vi", b"", ".txt", true, false, false, &[]); + test_vi(t, "tests/programs/when_break_continue.vi", b"", ".txt", true, false, false, &[]); for (name, _) in t.glob_in("programs/aoc_2024/", "*.vi") { let name: String = name.into(); let input = fs::read(format!("tests/programs/aoc_2024/input/{name}")).unwrap(); let path = format!("tests/programs/aoc_2024/{name}.vi"); - test_vi(t, leak(path), leak(input), ".txt", true, false, false); + test_vi(t, leak(path), leak(input), ".txt", true, false, false, &[]); } t.group("test", |t| { @@ -163,6 +164,7 @@ fn tests(t: &mut DynTester) { const VINE: &[&str] = &["vine", "--release"]; const IVY: &[&str] = &["ivy", "--release"]; +#[allow(clippy::too_many_arguments)] fn test_vi( t: &mut DynTester, path: &'static str, @@ -171,6 +173,7 @@ fn test_vi( breadth_first: bool, debug: bool, error: bool, + cli_args: &'static [&'static str], ) { let name = path.strip_prefix("tests/programs/").or(path.strip_prefix("vine/examples/")).unwrap_or(path); @@ -190,7 +193,7 @@ fn test_vi( t.test("run", move || { let path = receiver.recv().unwrap(); let path = path.as_os_str().to_str().unwrap(); - run_iv("vine", name, path, input, output_ext, breadth_first, error); + run_iv("vine", name, path, input, output_ext, breadth_first, error, cli_args); }); }); } @@ -247,10 +250,11 @@ fn test_vi_fmt(t: &mut DynTester, path: &'static str) { fn test_iv(t: &mut DynTester, path: &'static str, input: &'static [u8], output_ext: &'static str) { let name = Path::file_stem(path.as_ref()).unwrap().to_str().unwrap(); t.test(name, || { - run_iv("ivy", name, path, input, output_ext, false, false); + run_iv("ivy", name, path, input, output_ext, false, false, &[]); }); } +#[allow(clippy::too_many_arguments)] fn run_iv( group: &str, name: &str, @@ -259,11 +263,15 @@ fn run_iv( output_ext: &str, breadth_first: bool, error: bool, + extra_args: &'static [&'static str], ) { let mut args = vec!["run", path]; if breadth_first { args.push("--breadth-first"); } + if !extra_args.is_empty() { + args.extend(extra_args.iter()); + } let (stdout, stderr) = exec(IVY, &args, input, !error); test_snapshot(&[group, name, &format!("output{output_ext}")], &stdout); let full_stats = String::from_utf8(stderr).unwrap(); From b3743881b51803b0bee1879586c576df0c677254 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 7 Jan 2026 12:54:28 -0300 Subject: [PATCH 03/18] fmt --- tests/tests.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 835d26593..b527ab795 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -37,7 +37,16 @@ fn tests(t: &mut DynTester) { test_vi(t, "vine/examples/fib_repl.vi", fib_repl_input_vi, ".txt", true, false, false, &[]); test_vi(t, "vine/examples/fib.vi", b"", ".txt", true, false, false, &[]); test_vi(t, "vine/examples/fizzbuzz.vi", b"", ".txt", true, false, false, &[]); - test_vi(t, "vine/examples/guessing_game.vi", guessing_game_input, ".txt", true, false, false, &[]); + test_vi( + t, + "vine/examples/guessing_game.vi", + guessing_game_input, + ".txt", + true, + false, + false, + &[], + ); test_vi(t, "vine/examples/hello_world.vi", b"", ".txt", true, false, false, &[]); test_vi(t, "vine/examples/mandelbrot_sixel.vi", b"", ".sixel", true, false, false, &[]); test_vi(t, "vine/examples/mandelbrot_tga.vi", b"", ".tga", false, false, false, &[]); @@ -56,7 +65,16 @@ fn tests(t: &mut DynTester) { test_vi(t, "tests/programs/break_result.vi", b"", ".txt", true, false, false, &[]); test_vi(t, "tests/programs/centimanes.vi", b"", ".txt", true, false, false, &[]); test_vi(t, "tests/programs/classify_primes.vi", b"", ".txt", true, false, false, &[]); - test_vi(t, "tests/programs/cli_args.vi", b"", ".txt", false, false, false, &["some", "cli", "args"]); + test_vi( + t, + "tests/programs/cli_args.vi", + b"", + ".txt", + false, + false, + false, + &["some", "cli", "args"], + ); test_vi(t, "tests/programs/cond_diverge.vi", b"", ".txt", true, false, false, &[]); test_vi(t, "tests/programs/cubes.vi", b"", ".txt", true, false, false, &[]); test_vi(t, "tests/programs/cyclist.vi", b"", ".txt", true, false, false, &[]); From 56718fe2b4bc350eb1733cea0ad76e7a193d8161 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Mon, 12 Jan 2026 13:40:15 +0100 Subject: [PATCH 04/18] single register_ext_ty fn --- ivy/src/host/ext.rs | 50 +++++++++++---------------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index fc4e67eb7..fb9ebb7f2 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -4,7 +4,7 @@ use std::{ }; use ivm::{ - ext::{ExtFn, ExtIter, ExtTy, ExtTyId, Extrinsics}, + ext::{ExtFn, ExtIter, ExtTy, ExtTyCast, ExtTyId, Extrinsics}, port::Port, }; @@ -103,40 +103,14 @@ impl<'ivm> Host<'ivm> { n32_ty } - fn register_f32_ext_ty(&mut self, extrinsics: &mut Extrinsics<'ivm>) -> ExtTy<'ivm, f32> { - let f32_ty = extrinsics.new_ext_ty(); - self.register_ext_ty_id("F32".into(), f32_ty.ty_id()); - f32_ty - } - - fn register_io_ext_ty(&mut self, extrinsics: &mut Extrinsics<'ivm>) -> ExtTy<'ivm, ()> { - let io_ty = extrinsics.new_ext_ty(); - self.register_ext_ty_id("IO".into(), io_ty.ty_id()); - io_ty - } - - fn register_f64_ext_ty(&mut self, extrinsics: &mut Extrinsics<'ivm>) -> ExtTy<'ivm, f64> { - let f64_ty = extrinsics.new_ext_ty(); - self.register_ext_ty_id("F64".into(), f64_ty.ty_id()); - f64_ty - } - - fn register_strs_ext_ty( - &mut self, - extrinsics: &mut Extrinsics<'ivm>, - ) -> ExtTy<'ivm, ExtIter> { - let strs_ty = extrinsics.new_ext_ty(); - self.register_ext_ty_id("STRS".into(), strs_ty.ty_id()); - strs_ty - } - - fn register_str_ext_ty( + fn register_ext_ty>( &mut self, + name: &'static str, extrinsics: &mut Extrinsics<'ivm>, - ) -> ExtTy<'ivm, ExtIter> { - let str_ty = extrinsics.new_ext_ty(); - self.register_ext_ty_id("STR".into(), str_ty.ty_id()); - str_ty + ) -> ExtTy<'ivm, T> { + let ty = extrinsics.new_ext_ty(); + self.register_ext_ty_id(name.into(), ty.ty_id()); + ty } pub fn register_default_extrinsics( @@ -145,11 +119,11 @@ impl<'ivm> Host<'ivm> { args: Vec, ) { let n32 = self.register_n32_ext_ty(extrinsics); - let f32 = self.register_f32_ext_ty(extrinsics); - let f64 = self.register_f64_ext_ty(extrinsics); - let str = self.register_str_ext_ty(extrinsics); - let strs = self.register_strs_ext_ty(extrinsics); - let io = self.register_io_ext_ty(extrinsics); + let f32 = self.register_ext_ty::("F32", extrinsics); + let f64 = self.register_ext_ty::("F64", extrinsics); + let str = self.register_ext_ty::>("STR", extrinsics); + let strs = self.register_ext_ty::>("STRS", extrinsics); + let io = self.register_ext_ty::<()>("IO", extrinsics); // u64 to/from (lo: u32, hi: u32) halves let u64_to_parts = |x: u64| (x as u32, (x >> 32) as u32); From d1acd7495ed8eb733856d2f1e67c7fbafb4a6e89 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Mon, 12 Jan 2026 13:54:52 +0100 Subject: [PATCH 05/18] Box>> --- ivm/src/ext.rs | 40 ++++++++++++++++++++++------------------ ivy/src/host/ext.rs | 10 +++++----- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index e8749a631..fa98d46a5 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -6,10 +6,13 @@ use core::{ fmt::{self, Debug}, marker::PhantomData, + ops::{Deref, DerefMut}, }; use crate::{ivm::IVM, port::Tag, wire::Wire, word::Word}; +use std::vec::IntoIter; + macro_rules! trait_alias { ($($(#[$attr:meta])* $vis:vis trait $name:ident = ($($trait:tt)*);)*) => {$( $(#[$attr])* @@ -294,44 +297,45 @@ impl<'ivm> ExtTyCast<'ivm> for f64 { } } -pub struct ExtIter { - elements: Vec, - idx: usize, +#[repr(transparent)] +pub struct ExtIter { + iter: Box>>, } -impl ExtIter { - pub fn new(elements: Vec) -> Self { - Self { elements, idx: 0 } +impl ExtIter { + pub fn new(elements: Vec) -> Self { + Self { iter: Box::new(Aligned(elements.into_iter())) } } +} - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.elements.len() - } +impl Deref for ExtIter { + type Target = IntoIter; - pub fn current(&self) -> Option<&I> { - self.elements.get(self.idx) + fn deref(&self) -> &Self::Target { + &self.iter.0 } +} - pub fn advance(self) -> Self { - Self { elements: self.elements, idx: self.idx + 1 } +impl DerefMut for ExtIter { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.iter.0 } } -impl<'ivm, I> ExtTyCast<'ivm> for ExtIter { +impl<'ivm, T> ExtTyCast<'ivm> for ExtIter { const COPY: bool = false; #[inline(always)] fn into_payload(self) -> Word { - let pointer = Box::into_raw(Box::new(Aligned(self))); + let pointer = Box::into_raw(self.iter); Word::from_ptr(pointer.cast()) } #[inline(always)] unsafe fn from_payload(payload: Word) -> Self { let ptr = payload.ptr().cast_mut().cast(); - let Aligned(iter) = unsafe { *Box::from_raw(ptr) }; - iter + let iter = unsafe { Box::from_raw(ptr) }; + Self { iter } } } diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index fb9ebb7f2..0d7344f53 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -70,7 +70,8 @@ macro_rules! register_ext_fns { (@impl $ext:ident, |$a:ident : $a_ty:ident| -> ($b_ty:ident, $c_ty:ident) $body:block) => { $ext.new_split_ext_fn(move |ivm, $a, out0, out1| { let (val0, val1) = (|| { - let $a = $a_ty.unwrap_ext_val($a)?; + #[allow(unused_mut)] + let mut $a = $a_ty.unwrap_ext_val($a)?; let (b, c) = $body; let b = $b_ty.wrap_ext_val(b); let c = $c_ty.wrap_ext_val(c); @@ -199,7 +200,7 @@ impl<'ivm> Host<'ivm> { "f64_from_bits" => |lo: n32, hi: n32| -> f64 { f64::from_bits(u64_from_parts(lo, hi)) }, "str_len" => |s: str| -> (n32, str) { (s.len() as u32, s) }, - "str_next" => |s: str| -> (n32, str) { (*s.current()? as u32, s.advance()) }, + "str_next" => |s: str| -> (n32, str) { (s.next()? as u32, s) }, "str_drop" => |s: str| {}, "io_join" => |_io_a: io, _io_b: io| -> io {}, @@ -208,9 +209,8 @@ impl<'ivm> Host<'ivm> { "io_args" => |io0: io| -> (strs, io) { (ExtIter::new(args.clone()), io0) }, "args_len" => |args: strs| -> (n32, strs) { (args.len() as u32, args) }, - "args_next" => |args: strs| -> (str, strs) { - (ExtIter::new(args.current()?.chars().collect()), args.advance()) - }, + "args_next" => + |args: strs| -> (str, strs) { (ExtIter::new(args.next()?.chars().collect()), args) }, "args_drop" => |args: strs| {}, "io_print_char" => |_io: io, b: n32| -> io { From 1af8a82fcb7efed13dba27b7dcc2fab3a4144820 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Tue, 13 Jan 2026 10:32:37 +0100 Subject: [PATCH 06/18] remove #[repr(transparent)] --- ivm/src/ext.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index fa98d46a5..87b5d3ac5 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -297,7 +297,6 @@ impl<'ivm> ExtTyCast<'ivm> for f64 { } } -#[repr(transparent)] pub struct ExtIter { iter: Box>>, } From eb8e348f7e0a4df6eae482cfc4102120ffc09fae Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Tue, 13 Jan 2026 15:19:40 +0100 Subject: [PATCH 07/18] add ext.vi, move ExtString to ext.vi --- root/IO.vi | 35 +---------------------------------- root/ext.vi | 36 ++++++++++++++++++++++++++++++++++++ root/root.vi | 1 + 3 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 root/ext.vi diff --git a/root/IO.vi b/root/IO.vi index 031a839f9..1652e9d13 100644 --- a/root/IO.vi +++ b/root/IO.vi @@ -1,5 +1,5 @@ -use #root::ops::Cast; +use #root::ext::ExtString; /// A special primitive type used to interact with the outside world. /// Values of this type cannot be explicitly constructed; instead, @@ -174,39 +174,6 @@ pub mod IO { } } - type ExtString; - - mod ExtString { - pub fn .len(&self: &ExtString) -> N32 { - inline_ivy! (str0 <- self, str1 -> self) -> N32 { - len - str0 = @str_len(len str1) - } - } - - pub fn .next(&self: &ExtString) -> Char { - inline_ivy! (str0 <- self, str1 -> self) -> Char { - char - str0 = @str_next(char str1) - } - } - - pub impl drop: Drop[ExtString] { - fn drop(self: ExtString) { - inline_ivy! (str <- self) -> () { - _ - str = @str_drop(_ _) - } - } - } - - pub impl to_string: Cast[ExtString, String] { - fn cast(self: ExtString) -> String { - String(List::from_fn(self.len(), fn* () { self.next() })) - } - } - } - /// Returns the command-line arguments that vine was executed with. /// /// Unlike most other programming languages, the first argument _is not_ a diff --git a/root/ext.vi b/root/ext.vi new file mode 100644 index 000000000..85336de7c --- /dev/null +++ b/root/ext.vi @@ -0,0 +1,36 @@ + +use #root::ops::Cast; + +pub type ExtString; + +pub mod ExtString { + pub fn .len(&self: &ExtString) -> N32 { + inline_ivy! (str0 <- self, str1 -> self) -> N32 { + len + str0 = @str_len(len str1) + } + } + + pub fn .next(&self: &ExtString) -> Char { + inline_ivy! (str0 <- self, str1 -> self) -> Char { + char + str0 = @str_next(char str1) + } + } + + pub impl drop: Drop[ExtString] { + fn drop(self: ExtString) { + inline_ivy! (str <- self) -> () { + _ + str = @str_drop(_ _) + } + } + } + + pub impl to_string: Cast[ExtString, String] { + fn cast(self: ExtString) -> String { + String(List::from_fn(self.len(), fn* () { self.next() })) + } + } +} + diff --git a/root/root.vi b/root/root.vi index 9a6727ee1..337026d68 100644 --- a/root/root.vi +++ b/root/root.vi @@ -5,6 +5,7 @@ mod; pub mod data; pub mod debug; pub mod derive; +pub mod ext; pub mod logical; pub mod numeric; pub mod ops; From 2ccc320356b8165aa383fea50423cad8c40116c7 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 14 Jan 2026 12:13:09 +0100 Subject: [PATCH 08/18] add ExtTuple<'ivm>, remove ExtIter --- ivm/src/ext.rs | 76 +++++++++++++++++++----- ivy/src/host/ext.rs | 56 +++++++++++++++--- root/Ext.vi | 130 +++++++++++++++++++++++++++++++++++++++++ root/IO.vi | 14 +++-- root/ext.vi | 36 ------------ root/root.vi | 2 +- root/unicode/String.vi | 13 ++++- 7 files changed, 260 insertions(+), 67 deletions(-) create mode 100644 root/Ext.vi delete mode 100644 root/ext.vi diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index 87b5d3ac5..61e9d07bf 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -11,8 +11,6 @@ use core::{ use crate::{ivm::IVM, port::Tag, wire::Wire, word::Word}; -use std::vec::IntoIter; - macro_rules! trait_alias { ($($(#[$attr:meta])* $vis:vis trait $name:ident = ($($trait:tt)*);)*) => {$( $(#[$attr])* @@ -277,6 +275,7 @@ impl<'ivm> ExtTyCast<'ivm> for f32 { } } +#[derive(Default)] #[repr(align(8))] pub struct Aligned(T); @@ -297,44 +296,89 @@ impl<'ivm> ExtTyCast<'ivm> for f64 { } } -pub struct ExtIter { - iter: Box>>, +pub struct ExtList { + vec: Box>>, +} + +impl ExtList { + pub fn new() -> Self { + Self { vec: Default::default() } + } +} + +impl From> for ExtList { + fn from(vec: Vec) -> Self { + Self { vec: Box::new(Aligned(vec)) } + } } -impl ExtIter { - pub fn new(elements: Vec) -> Self { - Self { iter: Box::new(Aligned(elements.into_iter())) } +impl From<&str> for ExtList { + fn from(s: &str) -> Self { + Self { vec: Box::new(Aligned(s.chars().collect())) } } } -impl Deref for ExtIter { - type Target = IntoIter; +impl Deref for ExtList { + type Target = Vec; fn deref(&self) -> &Self::Target { - &self.iter.0 + &self.vec.0 } } -impl DerefMut for ExtIter { +impl DerefMut for ExtList { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.iter.0 + &mut self.vec.0 + } +} + +impl<'ivm, T> ExtTyCast<'ivm> for ExtList { + const COPY: bool = false; + + #[inline(always)] + fn into_payload(self) -> Word { + let pointer = Box::into_raw(self.vec); + Word::from_ptr(pointer.cast()) + } + + #[inline(always)] + unsafe fn from_payload(payload: Word) -> Self { + let ptr = payload.ptr().cast_mut().cast(); + let vec = unsafe { Box::from_raw(ptr) }; + Self { vec } + } +} + +/// A tuple of two extrinsic values of unknown type. +pub struct ExtTuple<'ivm>(Box, ExtVal<'ivm>)>>); + +impl<'ivm> From<(ExtVal<'ivm>, ExtVal<'ivm>)> for ExtTuple<'ivm> { + fn from(tup: (ExtVal<'ivm>, ExtVal<'ivm>)) -> Self { + Self(Box::new(Aligned(tup))) } } -impl<'ivm, T> ExtTyCast<'ivm> for ExtIter { +impl<'ivm> From> for (ExtVal<'ivm>, ExtVal<'ivm>) { + fn from(tup: ExtTuple<'ivm>) -> Self { + (*tup.0).0 + } +} + +/// Used for the `IO` extrinsic type. +impl<'ivm> ExtTyCast<'ivm> for ExtTuple<'ivm> { const COPY: bool = false; #[inline(always)] fn into_payload(self) -> Word { - let pointer = Box::into_raw(self.iter); + let pointer = Box::into_raw(self.0); Word::from_ptr(pointer.cast()) } #[inline(always)] unsafe fn from_payload(payload: Word) -> Self { let ptr = payload.ptr().cast_mut().cast(); - let iter = unsafe { Box::from_raw(ptr) }; - Self { iter } + let tuple = unsafe { Box::from_raw(ptr) }; + Self(tuple) } } diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index 0d7344f53..4c1ff34a9 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -4,7 +4,7 @@ use std::{ }; use ivm::{ - ext::{ExtFn, ExtIter, ExtTy, ExtTyCast, ExtTyId, Extrinsics}, + ext::{ExtFn, ExtList, ExtTuple, ExtTy, ExtTyCast, ExtTyId, Extrinsics}, port::Port, }; @@ -54,7 +54,8 @@ macro_rules! register_ext_fns { (@impl $ext:ident, |$a:ident : $a_ty:ident, $b:ident : $b_ty:ident| -> $c_ty:ident $body:block) => { $ext.new_merge_ext_fn(move |ivm, $a, $b, out| { let val = (|| { - let $a = $a_ty.unwrap_ext_val($a)?; + #[allow(unused_mut)] + let mut $a = $a_ty.unwrap_ext_val($a)?; let $b = $b_ty.unwrap_ext_val($b)?; let res = $c_ty.wrap_ext_val($body); Some(Port::new_ext_val(res)) @@ -122,14 +123,39 @@ impl<'ivm> Host<'ivm> { let n32 = self.register_n32_ext_ty(extrinsics); let f32 = self.register_ext_ty::("F32", extrinsics); let f64 = self.register_ext_ty::("F64", extrinsics); - let str = self.register_ext_ty::>("STR", extrinsics); - let strs = self.register_ext_ty::>("STRS", extrinsics); + let str = self.register_ext_ty::>("STR", extrinsics); + let strs = self.register_ext_ty::>("STRS", extrinsics); + let tup = self.register_ext_ty::>("TUP", extrinsics); let io = self.register_ext_ty::<()>("IO", extrinsics); // u64 to/from (lo: u32, hi: u32) halves let u64_to_parts = |x: u64| (x as u32, (x >> 32) as u32); let u64_from_parts = |lo: u32, hi: u32| ((hi as u64) << 32) | (lo as u64); + self.register_ext_fn( + "merge".into(), + extrinsics.new_merge_ext_fn(move |ivm, a, b, out| { + let tup = tup.wrap_ext_val((a, b).into()); + ivm.link_wire(out, Port::new_ext_val(tup)); + }), + ); + + self.register_ext_fn( + "split".into(), + extrinsics.new_split_ext_fn(move |ivm, val, out0, out1| { + let (a, b) = match tup.unwrap_ext_val(val).map(<(_, _)>::from) { + Some((a, b)) => (Port::new_ext_val(a), Port::new_ext_val(b)), + None => { + ivm.flags.ext_generic = true; + (Port::ERASE, Port::ERASE) + } + }; + + ivm.link_wire(out0, a); + ivm.link_wire(out1, b); + }), + ); + register_ext_fns!(match (self, extrinsics) { "n32_add" => |a: n32, b: n32| -> n32 { a.wrapping_add(b) }, "n32_sub" => |a: n32, b: n32| -> n32 { a.wrapping_sub(b) }, @@ -199,18 +225,32 @@ impl<'ivm> Host<'ivm> { "f64_to_bits" => |f: f64| -> (n32, n32) { u64_to_parts(f.to_bits()) }, "f64_from_bits" => |lo: n32, hi: n32| -> f64 { f64::from_bits(u64_from_parts(lo, hi)) }, + "str_new" => |_unused: n32| -> str { ExtList::::new() }, "str_len" => |s: str| -> (n32, str) { (s.len() as u32, s) }, - "str_next" => |s: str| -> (n32, str) { (s.next()? as u32, s) }, + "str_get" => |params: tup| -> (n32, str) { + let (s, i) = params.into(); + let s = str.unwrap_ext_val(s)?; + let i = n32.unwrap_ext_val(i)?; + (*s.get(i as usize)? as u32, s) + }, + "str_push" => |s: str, c: n32| -> str { + s.push(char::from_u32(c)?); + s + }, "str_drop" => |s: str| {}, "io_join" => |_io_a: io, _io_b: io| -> io {}, "io_split" => |_io: io| -> (io, io) { ((), ()) }, "io_ready" => |_io: io| -> (n32, io) { (1, ()) }, - "io_args" => |io0: io| -> (strs, io) { (ExtIter::new(args.clone()), io0) }, + "io_args" => |io0: io| -> (strs, io) { (args.clone().into(), io0) }, "args_len" => |args: strs| -> (n32, strs) { (args.len() as u32, args) }, - "args_next" => - |args: strs| -> (str, strs) { (ExtIter::new(args.next()?.chars().collect()), args) }, + "args_get" => |params: tup| -> (str, strs) { + let (args, i) = params.into(); + let args = strs.unwrap_ext_val(args)?; + let i = n32.unwrap_ext_val(i)?; + (args.get(i as usize)?.as_str().into(), args) + }, "args_drop" => |args: strs| {}, "io_print_char" => |_io: io, b: n32| -> io { diff --git a/root/Ext.vi b/root/Ext.vi new file mode 100644 index 000000000..7b3a376e6 --- /dev/null +++ b/root/Ext.vi @@ -0,0 +1,130 @@ + +use #root::{derive::Tuple, ops::Cast, unsafe::transmute}; + +/// An extrinsic value logically representing a Vine value of type `T`. +/// The author of `T` is allowed to decide the representation of this type. +pub type Ext[T]; + +/// Conversion between `T` and `Ext[T]`. +pub trait Ext[T] { + fn encode(value: T) -> Ext[T]; + fn .decode(value: Ext[T]) -> T; +} + +pub mod Ext { + #[manual] + pub impl identity[T]: Ext[T] { + fn encode(value: T) -> Ext[T] { + transmute(value) + } + + fn decode(value: Ext[T]) -> T { + transmute(value) + } + } + + pub impl encode[T; Ext[T]]: Cast[T, Ext[T]] { + fn cast(value: T) -> Ext[T] { + Ext::encode(value) + } + } + + pub impl decode[T; Ext[T]]: Cast[Ext[T], T] { + fn cast(value: Ext[T]) -> T { + Ext::decode(value) + } + } + + pub impl n32: Ext[N32] = Ext::identity; + pub impl f32: Ext[F32] = Ext::identity; + pub impl f64: Ext[F64] = Ext::identity; + pub impl io: Ext[IO] = Ext::identity; + pub impl ext[T]: Ext[Ext[T]] = Ext::identity; + + pub impl unary[T; Ext[T]]: Ext[(T,)] { + fn encode((value: T,)) -> Ext[(T,)] { + transmute[Ext[T], Ext[(T,)]](value as Ext) + } + + fn decode(value: Ext[(T,)]) -> (T,) { + (transmute[Ext[(T,)], Ext[T]](value) as T,) + } + } + + pub impl tuple[T, I, R; Tuple[T, I, R], Ext[I], Ext[R]]: Ext[T] { + fn encode(tuple: T) -> Ext[T] { + let (init, rest) = tuple as (I, R); + let init = init as Ext; + let rest = rest as Ext; + + inline_ivy! (init <- init, rest <- rest) -> Ext[T] { + tup + init = @merge(rest tup) + } + } + + fn decode(tuple: Ext[T]) -> T { + let (init: Ext[I], rest: Ext[R]); + inline_ivy! (tuple <- tuple, init -> init, rest -> rest) -> () { + _ + tuple = @split(init rest) + }; + + (init as I, rest as R) as T + } + } +} + +pub mod Ext[String] { + pub fn new_string() -> Ext[String] { + inline_ivy! () -> Ext[String] { + str + 0 = @str_new(_ str) + } + } + + pub fn .len(&self: &Ext[String]) -> N32 { + inline_ivy! (str0 <- self, str1 -> self) -> N32 { + len + str0 = @str_len(len str1) + } + } + + // TODO: mark unsafe + pub fn .at(&self: &Ext[String], i: N32) -> Char { + let params = (self, i) as Ext[(Ext[String], N32)]; + inline_ivy! (params <- params, str1 -> self) -> Char { + char + params = @str_get(char str1) + } + } + + // TODO: mark unsafe + pub fn .push(&self: &Ext[String], c: Char) { + inline_ivy! (str0 <- self, c <- c, str1 -> self) -> () { + _ + str0 = @str_push(c str1) + }; + } + + pub impl drop: Drop[Ext[String]] { + fn drop(self: Ext[String]) { + inline_ivy! (str <- self) -> () { + _ + str = @str_drop(_ _) + } + } + } + + pub impl ext_string: Ext[String] { + fn encode(s: String) -> Ext[String] { + let ext = Ext::new_string(); + s!.iter().for_each(fn* (c) { ext.push(c) }); + ext + } + + fn decode(ext: Ext[String]) -> String { + (0..ext.len()).map(fn* (i) { ext.at(i) }).collect[String, _, _]() + } + } +} diff --git a/root/IO.vi b/root/IO.vi index 1652e9d13..b00bc69a0 100644 --- a/root/IO.vi +++ b/root/IO.vi @@ -1,5 +1,5 @@ -use #root::ext::ExtString; +use #root::Ext; /// A special primitive type used to interact with the outside world. /// Values of this type cannot be explicitly constructed; instead, @@ -143,6 +143,8 @@ pub mod IO { type Args; mod Args { + pub impl : Ext[Args] = Ext::identity; + pub fn new(&io: &IO) -> Args { inline_ivy! (io0 <- io, io1 -> io) -> Args { args @@ -157,10 +159,12 @@ pub mod IO { } } - pub fn .next(&self: &Args) -> String { - inline_ivy! (args0 <- self, args1 -> self) -> ExtString { + // TODO: mark unsafe + pub fn .at(&self: &Args, i: N32) -> String { + let params = (self, i) as Ext[(Args, N32)]; + inline_ivy! (params <- params, args1 -> self) -> Ext[String] { str - args0 = @args_next(str args1) + params = @args_get(str args1) } as String } @@ -191,6 +195,6 @@ pub mod IO { /// ``` pub fn .args(&io: &IO) -> List[String] { let args = Args::new(&io); - List::from_fn(args.len(), fn* () { args.next() }) + (0..args.len()).map(fn* (i) { args.at(i) }).collect[List[String], _, _]() } } diff --git a/root/ext.vi b/root/ext.vi deleted file mode 100644 index 85336de7c..000000000 --- a/root/ext.vi +++ /dev/null @@ -1,36 +0,0 @@ - -use #root::ops::Cast; - -pub type ExtString; - -pub mod ExtString { - pub fn .len(&self: &ExtString) -> N32 { - inline_ivy! (str0 <- self, str1 -> self) -> N32 { - len - str0 = @str_len(len str1) - } - } - - pub fn .next(&self: &ExtString) -> Char { - inline_ivy! (str0 <- self, str1 -> self) -> Char { - char - str0 = @str_next(char str1) - } - } - - pub impl drop: Drop[ExtString] { - fn drop(self: ExtString) { - inline_ivy! (str <- self) -> () { - _ - str = @str_drop(_ _) - } - } - } - - pub impl to_string: Cast[ExtString, String] { - fn cast(self: ExtString) -> String { - String(List::from_fn(self.len(), fn* () { self.next() })) - } - } -} - diff --git a/root/root.vi b/root/root.vi index 337026d68..af56a2062 100644 --- a/root/root.vi +++ b/root/root.vi @@ -5,7 +5,7 @@ mod; pub mod data; pub mod debug; pub mod derive; -pub mod ext; +pub mod Ext; pub mod logical; pub mod numeric; pub mod ops; diff --git a/root/unicode/String.vi b/root/unicode/String.vi index 4ecedfd2c..e922faaae 100644 --- a/root/unicode/String.vi +++ b/root/unicode/String.vi @@ -1,5 +1,5 @@ -use ops::{Cast, Concat, comparison::{Eq, Ord}}; +use #root::{data::Iterator::{Iterator, Collect}, ops::{Cast, Concat, comparison::{Eq, Ord}}}; /// A Unicode string, represented as a list of characters. /// ```vi @@ -194,6 +194,17 @@ pub mod String { } } + /// Collect an iterator of `Char` into a `String`. + /// ```vi + /// > (48..91).map(fn* (n: N32) { n as Char }).collect[String, _, _]() + /// // "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ" + /// ``` + pub impl : Collect[String, Char] { + fn collect[I; Iterator[I, Char]](iter_ref: I) -> String { + String(iter_ref.collect[List[Char], _, _]()) + } + } + pub impl : Show[String] { fn show(&self: &String) -> Show { let escaped = ""; From a5bf2bbd3cb64d492151bf5818df62ea7d574611 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 14 Jan 2026 12:13:31 +0100 Subject: [PATCH 09/18] add tests/programs/repl/extrinsics.vi --- tests/programs/repl/extrinsics.vi | 5 +++++ tests/snaps/vine/repl/extrinsics.repl.vi | 21 +++++++++++++++++++++ tests/tests.rs | 1 + 3 files changed, 27 insertions(+) create mode 100644 tests/programs/repl/extrinsics.vi create mode 100644 tests/snaps/vine/repl/extrinsics.repl.vi diff --git a/tests/programs/repl/extrinsics.vi b/tests/programs/repl/extrinsics.vi new file mode 100644 index 000000000..028d575ba --- /dev/null +++ b/tests/programs/repl/extrinsics.vi @@ -0,0 +1,5 @@ +use #root::Ext +12345[N32] as Ext[N32] as N32 +1.234[F32] as Ext[F32] as F32 +1.234[F64] as Ext[F64] as F64 +"abcd" as Ext[String] as String diff --git a/tests/snaps/vine/repl/extrinsics.repl.vi b/tests/snaps/vine/repl/extrinsics.repl.vi new file mode 100644 index 000000000..9d7979bd9 --- /dev/null +++ b/tests/snaps/vine/repl/extrinsics.repl.vi @@ -0,0 +1,21 @@ + +let io: IO = ; +> use #root::Ext + +let io: IO = ; +> 12345[N32] as Ext[N32] as N32 +12345 + +let io: IO = ; +> 1.234[F32] as Ext[F32] as F32 +1.23399997 + +let io: IO = ; +> 1.234[F64] as Ext[F64] as F64 +≈1.23399997 + +let io: IO = ; +> "abcd" as Ext[String] as String +"abcd" + +let io: IO = ; diff --git a/tests/tests.rs b/tests/tests.rs index b527ab795..4a4e64a30 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -156,6 +156,7 @@ fn tests(t: &mut DynTester) { test_vi_repl(t, "tests/programs/repl/advanced_repl.vi"); test_vi_repl(t, "tests/programs/repl/basic_repl.vi"); test_vi_repl(t, "tests/programs/repl/elementwise.vi"); + test_vi_repl(t, "tests/programs/repl/extrinsics.vi"); test_vi_repl(t, "tests/programs/repl/f32_to_string.vi"); test_vi_repl(t, "tests/programs/repl/F64.vi"); test_vi_repl(t, "tests/programs/repl/heap.vi"); From 59f05152cbe47e42443cefae0ccdd95c139913a2 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 14 Jan 2026 12:17:35 +0100 Subject: [PATCH 10/18] ExtList::new -> ExtList::default --- ivm/src/ext.rs | 4 ++-- ivy/src/host/ext.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index 61e9d07bf..9208e87a4 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -300,8 +300,8 @@ pub struct ExtList { vec: Box>>, } -impl ExtList { - pub fn new() -> Self { +impl Default for ExtList { + fn default() -> Self { Self { vec: Default::default() } } } diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index 4c1ff34a9..964ed38d3 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -225,7 +225,7 @@ impl<'ivm> Host<'ivm> { "f64_to_bits" => |f: f64| -> (n32, n32) { u64_to_parts(f.to_bits()) }, "f64_from_bits" => |lo: n32, hi: n32| -> f64 { f64::from_bits(u64_from_parts(lo, hi)) }, - "str_new" => |_unused: n32| -> str { ExtList::::new() }, + "str_new" => |_unused: n32| -> str { ExtList::::default() }, "str_len" => |s: str| -> (n32, str) { (s.len() as u32, s) }, "str_get" => |params: tup| -> (n32, str) { let (s, i) = params.into(); From 0e3f9843dafe936f4af12469a9fa83b9df0baf09 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 14 Jan 2026 12:18:49 +0100 Subject: [PATCH 11/18] update snapshot --- tests/snaps/vine/cli_args/stats.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/snaps/vine/cli_args/stats.txt b/tests/snaps/vine/cli_args/stats.txt index 2fbec7aea..94408f7f4 100644 --- a/tests/snaps/vine/cli_args/stats.txt +++ b/tests/snaps/vine/cli_args/stats.txt @@ -1,15 +1,15 @@ Interactions - Total 2_177 - Annihilate 1_063 + Total 2_298 + Annihilate 1_144 Commute 17 Copy 216 - Erase 308 - Expand 196 - Call 269 + Erase 314 + Expand 216 + Call 283 Branch 108 Memory Heap 2_832 B - Allocated 44_880 B - Freed 44_880 B + Allocated 47_920 B + Freed 47_920 B From 059220df33b76fa86074aa29642bc94cd245e5ec Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 15 Jan 2026 14:06:33 +0100 Subject: [PATCH 12/18] make ExtList hold ExtVal<'ivm>, non-generic --- ivm/src/ext.rs | 79 +++++++-------------------- ivy/src/host/ext.rs | 94 ++++++++++++++++++-------------- root/Ext.vi | 127 ++++++++++++++++++++++++++++---------------- root/IO.vi | 21 +++++--- 4 files changed, 167 insertions(+), 154 deletions(-) diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index 9208e87a4..391fa99bc 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -6,7 +6,6 @@ use core::{ fmt::{self, Debug}, marker::PhantomData, - ops::{Deref, DerefMut}, }; use crate::{ivm::IVM, port::Tag, wire::Wire, word::Word}; @@ -296,89 +295,49 @@ impl<'ivm> ExtTyCast<'ivm> for f64 { } } -pub struct ExtList { - vec: Box>>, -} - -impl Default for ExtList { - fn default() -> Self { - Self { vec: Default::default() } - } -} - -impl From> for ExtList { - fn from(vec: Vec) -> Self { - Self { vec: Box::new(Aligned(vec)) } - } -} - -impl From<&str> for ExtList { - fn from(s: &str) -> Self { - Self { vec: Box::new(Aligned(s.chars().collect())) } - } -} - -impl Deref for ExtList { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.vec.0 - } +#[derive(Default)] +pub struct ExtList<'ivm> { + values: Box>>>, } -impl DerefMut for ExtList { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.vec.0 +impl<'ivm> ExtList<'ivm> { + pub fn is_empty(&self) -> bool { + self.len() == 0 } -} - -impl<'ivm, T> ExtTyCast<'ivm> for ExtList { - const COPY: bool = false; - #[inline(always)] - fn into_payload(self) -> Word { - let pointer = Box::into_raw(self.vec); - Word::from_ptr(pointer.cast()) + pub fn len(&self) -> usize { + self.values.0.len() } - #[inline(always)] - unsafe fn from_payload(payload: Word) -> Self { - let ptr = payload.ptr().cast_mut().cast(); - let vec = unsafe { Box::from_raw(ptr) }; - Self { vec } + pub fn push(&mut self, value: ExtVal<'ivm>) { + self.values.0.push(value); } -} - -/// A tuple of two extrinsic values of unknown type. -pub struct ExtTuple<'ivm>(Box, ExtVal<'ivm>)>>); -impl<'ivm> From<(ExtVal<'ivm>, ExtVal<'ivm>)> for ExtTuple<'ivm> { - fn from(tup: (ExtVal<'ivm>, ExtVal<'ivm>)) -> Self { - Self(Box::new(Aligned(tup))) + pub fn pop(&mut self) -> Option> { + self.values.0.pop() } } -impl<'ivm> From> for (ExtVal<'ivm>, ExtVal<'ivm>) { - fn from(tup: ExtTuple<'ivm>) -> Self { - (*tup.0).0 +impl<'ivm> From>> for ExtList<'ivm> { + fn from(values: Vec>) -> Self { + Self { values: Box::new(Aligned(values)) } } } -/// Used for the `IO` extrinsic type. -impl<'ivm> ExtTyCast<'ivm> for ExtTuple<'ivm> { +impl<'ivm> ExtTyCast<'ivm> for ExtList<'ivm> { const COPY: bool = false; #[inline(always)] fn into_payload(self) -> Word { - let pointer = Box::into_raw(self.0); + let pointer = Box::into_raw(self.values); Word::from_ptr(pointer.cast()) } #[inline(always)] unsafe fn from_payload(payload: Word) -> Self { let ptr = payload.ptr().cast_mut().cast(); - let tuple = unsafe { Box::from_raw(ptr) }; - Self(tuple) + let values = unsafe { Box::from_raw(ptr) }; + Self { values } } } diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index 964ed38d3..f30f7f6fd 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -4,7 +4,7 @@ use std::{ }; use ivm::{ - ext::{ExtFn, ExtList, ExtTuple, ExtTy, ExtTyCast, ExtTyId, Extrinsics}, + ext::{ExtFn, ExtList, ExtTy, ExtTyCast, ExtTyId, ExtVal, Extrinsics}, port::Port, }; @@ -14,7 +14,7 @@ macro_rules! register_ext_fns { (match ($self:ident, $ext:ident) { $( $name:expr => |$($param_name:ident : $param_ty:ident),*| $(-> $out:tt)? $body:block, )* }) => { - $($self.register_ext_fn($name.into(), + $($self.register_ext_fn($name, register_ext_fns!(@impl $ext, |$($param_name : $param_ty),*| $(-> $out)? $body) );)* }; @@ -89,7 +89,8 @@ macro_rules! register_ext_fns { } impl<'ivm> Host<'ivm> { - pub fn register_ext_fn(&mut self, name: String, f: ExtFn<'ivm>) { + pub fn register_ext_fn(&mut self, name: impl Into, f: ExtFn<'ivm>) { + let name = name.into(); self.ext_fns.insert(name.clone(), f); self.reverse_ext_fns.insert(f, name); } @@ -123,9 +124,7 @@ impl<'ivm> Host<'ivm> { let n32 = self.register_n32_ext_ty(extrinsics); let f32 = self.register_ext_ty::("F32", extrinsics); let f64 = self.register_ext_ty::("F64", extrinsics); - let str = self.register_ext_ty::>("STR", extrinsics); - let strs = self.register_ext_ty::>("STRS", extrinsics); - let tup = self.register_ext_ty::>("TUP", extrinsics); + let list = self.register_ext_ty::>("LIST", extrinsics); let io = self.register_ext_ty::<()>("IO", extrinsics); // u64 to/from (lo: u32, hi: u32) halves @@ -133,26 +132,48 @@ impl<'ivm> Host<'ivm> { let u64_from_parts = |lo: u32, hi: u32| ((hi as u64) << 32) | (lo as u64); self.register_ext_fn( - "merge".into(), - extrinsics.new_merge_ext_fn(move |ivm, a, b, out| { - let tup = tup.wrap_ext_val((a, b).into()); - ivm.link_wire(out, Port::new_ext_val(tup)); + "list_push", + extrinsics.new_merge_ext_fn(move |ivm, l, el, out| { + let Some(mut l) = list.unwrap_ext_val(l) else { + ivm.flags.ext_generic = true; + return; + }; + + l.push(el); + + ivm.link_wire(out, Port::new_ext_val(list.wrap_ext_val(l))); }), ); self.register_ext_fn( - "split".into(), - extrinsics.new_split_ext_fn(move |ivm, val, out0, out1| { - let (a, b) = match tup.unwrap_ext_val(val).map(<(_, _)>::from) { - Some((a, b)) => (Port::new_ext_val(a), Port::new_ext_val(b)), - None => { - ivm.flags.ext_generic = true; - (Port::ERASE, Port::ERASE) - } + "list_pop", + extrinsics.new_split_ext_fn(move |ivm, l, out0, out1| { + let Some(mut l) = list.unwrap_ext_val(l) else { + ivm.flags.ext_generic = true; + return; }; - ivm.link_wire(out0, a); - ivm.link_wire(out1, b); + let Some(el) = l.pop() else { + ivm.flags.ext_generic = true; + return; + }; + + ivm.link_wire(out0, Port::new_ext_val(el)); + ivm.link_wire(out1, Port::new_ext_val(list.wrap_ext_val(l))); + }), + ); + + self.register_ext_fn( + "list_drop", + extrinsics.new_split_ext_fn(move |ivm, l, out0, out1| { + match list.unwrap_ext_val(l) { + Some(l) if !l.is_empty() => ivm.flags.ext_erase = true, + None => ivm.flags.ext_generic = true, + _ => {} + } + + ivm.link_wire(out0, Port::ERASE); + ivm.link_wire(out1, Port::ERASE); }), ); @@ -225,33 +246,24 @@ impl<'ivm> Host<'ivm> { "f64_to_bits" => |f: f64| -> (n32, n32) { u64_to_parts(f.to_bits()) }, "f64_from_bits" => |lo: n32, hi: n32| -> f64 { f64::from_bits(u64_from_parts(lo, hi)) }, - "str_new" => |_unused: n32| -> str { ExtList::::default() }, - "str_len" => |s: str| -> (n32, str) { (s.len() as u32, s) }, - "str_get" => |params: tup| -> (n32, str) { - let (s, i) = params.into(); - let s = str.unwrap_ext_val(s)?; - let i = n32.unwrap_ext_val(i)?; - (*s.get(i as usize)? as u32, s) - }, - "str_push" => |s: str, c: n32| -> str { - s.push(char::from_u32(c)?); - s - }, - "str_drop" => |s: str| {}, + "list_new" => |_unused: n32| -> list { ExtList::default() }, + "list_len" => |l: list| -> (n32, list) { (l.len() as u32, l) }, "io_join" => |_io_a: io, _io_b: io| -> io {}, "io_split" => |_io: io| -> (io, io) { ((), ()) }, "io_ready" => |_io: io| -> (n32, io) { (1, ()) }, - "io_args" => |io0: io| -> (strs, io) { (args.clone().into(), io0) }, - "args_len" => |args: strs| -> (n32, strs) { (args.len() as u32, args) }, - "args_get" => |params: tup| -> (str, strs) { - let (args, i) = params.into(); - let args = strs.unwrap_ext_val(args)?; - let i = n32.unwrap_ext_val(i)?; - (args.get(i as usize)?.as_str().into(), args) + "io_args" => |io0: io| -> (list, io) { + let args = args + .iter() + .map(|s| { + let chars = s.chars().map(|c| n32.wrap_ext_val(c as u32)).collect::>(); + list.wrap_ext_val(ExtList::from(chars)) + }) + .collect::>() + .into(); + (args, io0) }, - "args_drop" => |args: strs| {}, "io_print_char" => |_io: io, b: n32| -> io { print!("{}", char::try_from(b).unwrap()); diff --git a/root/Ext.vi b/root/Ext.vi index 7b3a376e6..42bbcf093 100644 --- a/root/Ext.vi +++ b/root/Ext.vi @@ -39,15 +39,14 @@ pub mod Ext { pub impl f32: Ext[F32] = Ext::identity; pub impl f64: Ext[F64] = Ext::identity; pub impl io: Ext[IO] = Ext::identity; - pub impl ext[T]: Ext[Ext[T]] = Ext::identity; - pub impl unary[T; Ext[T]]: Ext[(T,)] { - fn encode((value: T,)) -> Ext[(T,)] { - transmute[Ext[T], Ext[(T,)]](value as Ext) + pub impl nil: Ext[()] { + fn encode((): ()) -> Ext[()] { + transmute(Ext::new_list()) } - fn decode(value: Ext[(T,)]) -> (T,) { - (transmute[Ext[(T,)], Ext[T]](value) as T,) + fn decode(tup: Ext[()]) -> () { + Ext::drop_list(transmute(tup)) } } @@ -55,76 +54,114 @@ pub mod Ext { fn encode(tuple: T) -> Ext[T] { let (init, rest) = tuple as (I, R); let init = init as Ext; - let rest = rest as Ext; + let rest = transmute[_, Ext[List[Ext[I]]]](rest as Ext); + rest.push(init); - inline_ivy! (init <- init, rest <- rest) -> Ext[T] { - tup - init = @merge(rest tup) - } + transmute(rest) } fn decode(tuple: Ext[T]) -> T { - let (init: Ext[I], rest: Ext[R]); - inline_ivy! (tuple <- tuple, init -> init, rest -> rest) -> () { - _ - tuple = @split(init rest) - }; + let rest = transmute[_, Ext[List[Ext[I]]]](tuple); + let init = rest.pop(); - (init as I, rest as R) as T + (init as I, transmute[_, Ext[R]](rest) as R) as T + } + } + + pub impl string: Ext[String] { + fn encode(s: String) -> Ext[String] { + let ext = Ext::new_string(); + for c in s! { + ext.push_char(c); + } + + ext + } + + fn decode(ext: Ext[String]) -> String { + let str = ""; + for _ in 0..ext.len_str() { + str!.push_front(ext.pop_char()); + } + Ext::drop_str(ext) + + str } } -} -pub mod Ext[String] { pub fn new_string() -> Ext[String] { inline_ivy! () -> Ext[String] { str - 0 = @str_new(_ str) + 0 = @list_new(_ str) + } + } + + pub fn new_list[T]() -> Ext[List[T]] { + inline_ivy! () -> Ext[List[T]] { + list + 0 = @list_new(_ list) + } + } + + // TODO: mark unsafe + pub fn drop_list[T](list: Ext[List[T]]) { + inline_ivy! (list <- list) -> () { + _ + list = @list_drop(_ _) + } + } + + // TODO: mark unsafe + pub fn drop_str(str: Ext[String]) { + inline_ivy! (str <- str) -> () { + _ + str = @list_drop(_ _) } } - pub fn .len(&self: &Ext[String]) -> N32 { + pub fn .len_str(&self: &Ext[String]) -> N32 { inline_ivy! (str0 <- self, str1 -> self) -> N32 { len - str0 = @str_len(len str1) + str0 = @list_len(len str1) } } - // TODO: mark unsafe - pub fn .at(&self: &Ext[String], i: N32) -> Char { - let params = (self, i) as Ext[(Ext[String], N32)]; - inline_ivy! (params <- params, str1 -> self) -> Char { - char - params = @str_get(char str1) + pub fn .len_list[T](&self: &Ext[List[T]]) -> N32 { + inline_ivy! (str0 <- self, str1 -> self) -> N32 { + len + str0 = @list_len(len str1) } } // TODO: mark unsafe - pub fn .push(&self: &Ext[String], c: Char) { - inline_ivy! (str0 <- self, c <- c, str1 -> self) -> () { + pub fn .push_char(&self: &Ext[String], char: Char) { + inline_ivy! (str0 <- self, char <- char, str1 -> self) -> () { _ - str0 = @str_push(c str1) + str0 = @list_push(char str1) }; } - pub impl drop: Drop[Ext[String]] { - fn drop(self: Ext[String]) { - inline_ivy! (str <- self) -> () { - _ - str = @str_drop(_ _) - } + // TODO: mark unsafe + pub fn .pop_char(&self: &Ext[String]) -> Char { + inline_ivy! (str0 <- self, str1 -> self) -> Char { + char + str0 = @list_pop(char str1) } } - pub impl ext_string: Ext[String] { - fn encode(s: String) -> Ext[String] { - let ext = Ext::new_string(); - s!.iter().for_each(fn* (c) { ext.push(c) }); - ext - } + // TODO: mark unsafe + pub fn .push[T](&self: &Ext[List[T]], t: T) { + inline_ivy! (list0 <- self, t <- t, list1 -> self) -> () { + _ + list0 = @list_push(t list1) + }; + } - fn decode(ext: Ext[String]) -> String { - (0..ext.len()).map(fn* (i) { ext.at(i) }).collect[String, _, _]() + // TODO: mark unsafe + pub fn .pop[T](&self: &Ext[List[T]]) -> T { + inline_ivy! (list0 <- self, list1 -> self) -> T { + t + list0 = @list_pop(t list1) } } } diff --git a/root/IO.vi b/root/IO.vi index b00bc69a0..53d97ded5 100644 --- a/root/IO.vi +++ b/root/IO.vi @@ -155,24 +155,24 @@ pub mod IO { pub fn .len(&self: &Args) -> N32 { inline_ivy! (args0 <- self, args1 -> self) -> N32 { len - args0 = @args_len(len args1) + args0 = @list_len(len args1) } } // TODO: mark unsafe - pub fn .at(&self: &Args, i: N32) -> String { - let params = (self, i) as Ext[(Args, N32)]; - inline_ivy! (params <- params, args1 -> self) -> Ext[String] { + pub fn .pop(&self: &Args) -> String { + inline_ivy! (args0 <- self, args1 -> self) -> Ext[String] { str - params = @args_get(str args1) + args0 = @list_pop(str args1) } as String } + // TODO: only safe is args is empty pub impl drop: Drop[Args] { fn drop(self: Args) { inline_ivy! (args <- self) -> () { _ - args = @args_drop(_ _) + args = @list_drop(_ _) } } } @@ -194,7 +194,12 @@ pub mod IO { /// // ["some", "cli", "args"] /// ``` pub fn .args(&io: &IO) -> List[String] { - let args = Args::new(&io); - (0..args.len()).map(fn* (i) { args.at(i) }).collect[List[String], _, _]() + let args_ext = Args::new(&io); + let args = []; + for _ in 0..args_ext.len() { + args.push_front(args_ext.pop()); + } + + args } } From 93c6599f3e966a658a6238b47e758d6630bce40f Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 15 Jan 2026 14:06:52 +0100 Subject: [PATCH 13/18] add test to repl/extrinsics.vi, update snapshots --- tests/programs/repl/extrinsics.vi | 1 + tests/snaps/vine/cli_args/stats.txt | 14 +++++++------- tests/snaps/vine/repl/extrinsics.repl.vi | 4 ++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/programs/repl/extrinsics.vi b/tests/programs/repl/extrinsics.vi index 028d575ba..dcde25ae8 100644 --- a/tests/programs/repl/extrinsics.vi +++ b/tests/programs/repl/extrinsics.vi @@ -3,3 +3,4 @@ use #root::Ext 1.234[F32] as Ext[F32] as F32 1.234[F64] as Ext[F64] as F64 "abcd" as Ext[String] as String +(123, 1.23[F32], 4.56[F64], "hello") as Ext as (_, _, _, _) diff --git a/tests/snaps/vine/cli_args/stats.txt b/tests/snaps/vine/cli_args/stats.txt index 94408f7f4..f403b0810 100644 --- a/tests/snaps/vine/cli_args/stats.txt +++ b/tests/snaps/vine/cli_args/stats.txt @@ -1,15 +1,15 @@ Interactions - Total 2_298 - Annihilate 1_144 + Total 2_163 + Annihilate 1_054 Commute 17 Copy 216 - Erase 314 - Expand 216 - Call 283 + Erase 305 + Expand 194 + Call 269 Branch 108 Memory Heap 2_832 B - Allocated 47_920 B - Freed 47_920 B + Allocated 44_592 B + Freed 44_592 B diff --git a/tests/snaps/vine/repl/extrinsics.repl.vi b/tests/snaps/vine/repl/extrinsics.repl.vi index 9d7979bd9..aa060227f 100644 --- a/tests/snaps/vine/repl/extrinsics.repl.vi +++ b/tests/snaps/vine/repl/extrinsics.repl.vi @@ -19,3 +19,7 @@ let io: IO = ; "abcd" let io: IO = ; +> (123, 1.23[F32], 4.56[F64], "hello") as Ext as (_, _, _, _) +(123, 1.23000002, ≈4.55999994, "hello") + +let io: IO = ; From d69bb59c77b58d92d2c451493ecc20db42021245 Mon Sep 17 00:00:00 2001 From: T6 Date: Thu, 15 Jan 2026 08:19:20 -0500 Subject: [PATCH 14/18] tweaks --- root/Ext.vi | 106 +++++++++++++++++++++------------------------------- 1 file changed, 42 insertions(+), 64 deletions(-) diff --git a/root/Ext.vi b/root/Ext.vi index 42bbcf093..3a052b542 100644 --- a/root/Ext.vi +++ b/root/Ext.vi @@ -1,5 +1,5 @@ -use #root::{derive::Tuple, ops::Cast, unsafe::transmute}; +use #root::{derive::{Composite, Struct}, ops::Cast, unsafe::transmute}; /// An extrinsic value logically representing a Vine value of type `T`. /// The author of `T` is allowed to decide the representation of this type. @@ -38,6 +38,7 @@ pub mod Ext { pub impl n32: Ext[N32] = Ext::identity; pub impl f32: Ext[F32] = Ext::identity; pub impl f64: Ext[F64] = Ext::identity; + pub impl char: Ext[Char] = Ext::identity; pub impl io: Ext[IO] = Ext::identity; pub impl nil: Ext[()] { @@ -50,107 +51,84 @@ pub mod Ext { } } - pub impl tuple[T, I, R; Tuple[T, I, R], Ext[I], Ext[R]]: Ext[T] { - fn encode(tuple: T) -> Ext[T] { - let (init, rest) = tuple as (I, R); + pub impl composite[C, K, I, R; Composite[C, K, I, R], Ext[I], Ext[R]]: Ext[C] { + fn encode(composite: C) -> Ext[C] { + let (init, rest) = composite as (I, R); let init = init as Ext; - let rest = transmute[_, Ext[List[Ext[I]]]](rest as Ext); + let rest = transmute[Ext[R], Ext[List[I]]](rest as Ext); rest.push(init); transmute(rest) } - fn decode(tuple: Ext[T]) -> T { - let rest = transmute[_, Ext[List[Ext[I]]]](tuple); + fn decode(composite: Ext[C]) -> C { + let rest = transmute[Ext[C], Ext[List[I]]](composite); let init = rest.pop(); - (init as I, transmute[_, Ext[R]](rest) as R) as T + (init as I, transmute[Ext[List[I]], Ext[R]](rest) as R) as C } } - pub impl string: Ext[String] { - fn encode(s: String) -> Ext[String] { - let ext = Ext::new_string(); - for c in s! { - ext.push_char(c); + pub impl list[T; Ext[T]]: Ext[List[T]] { + fn encode(list: List[T]) -> Ext[List[T]] { + let ext = Ext::new_list(); + + for value in list { + ext.push(value as Ext); } ext } - fn decode(ext: Ext[String]) -> String { - let str = ""; - for _ in 0..ext.len_str() { - str!.push_front(ext.pop_char()); - } - Ext::drop_str(ext) + fn decode(ext: Ext[List[T]]) -> List[T] { + let list = []; - str - } - } + for _ in 0..ext.len() { + list.push_front(ext.pop() as T); + } - pub fn new_string() -> Ext[String] { - inline_ivy! () -> Ext[String] { - str - 0 = @list_new(_ str) - } - } + drop_list(ext); - pub fn new_list[T]() -> Ext[List[T]] { - inline_ivy! () -> Ext[List[T]] { list - 0 = @list_new(_ list) } } - // TODO: mark unsafe - pub fn drop_list[T](list: Ext[List[T]]) { - inline_ivy! (list <- list) -> () { - _ - list = @list_drop(_ _) + #[basic] + pub impl struct_[S, C; Struct[S, C], Ext[C]]: Ext[S] { + fn encode(s: S) -> Ext[S] { + transmute[Ext[C], Ext[S]](s as C as Ext) } - } - // TODO: mark unsafe - pub fn drop_str(str: Ext[String]) { - inline_ivy! (str <- str) -> () { - _ - str = @list_drop(_ _) + fn decode(ext: Ext[S]) -> S { + transmute[Ext[S], Ext[C]](ext) as C as S } } - pub fn .len_str(&self: &Ext[String]) -> N32 { - inline_ivy! (str0 <- self, str1 -> self) -> N32 { - len - str0 = @list_len(len str1) + pub impl string: Ext[String]; + + pub fn new_list[T]() -> Ext[List[T]] { + inline_ivy! () -> Ext[List[T]] { + list + 0 = @list_new(_ list) } } - pub fn .len_list[T](&self: &Ext[List[T]]) -> N32 { - inline_ivy! (str0 <- self, str1 -> self) -> N32 { + pub fn .len[T](&list: &Ext[List[T]]) -> N32 { + inline_ivy! (list0 <- list, list1 -> list) -> N32 { len - str0 = @list_len(len str1) + list0 = @list_len(len list1) } } // TODO: mark unsafe - pub fn .push_char(&self: &Ext[String], char: Char) { - inline_ivy! (str0 <- self, char <- char, str1 -> self) -> () { + pub fn drop_list[T](list: Ext[List[T]]) { + inline_ivy! (list <- list) -> () { _ - str0 = @list_push(char str1) - }; - } - - // TODO: mark unsafe - pub fn .pop_char(&self: &Ext[String]) -> Char { - inline_ivy! (str0 <- self, str1 -> self) -> Char { - char - str0 = @list_pop(char str1) + list = @list_drop(_ _) } } - // TODO: mark unsafe - pub fn .push[T](&self: &Ext[List[T]], t: T) { + pub fn .push[T](&self: &Ext[List[T]], t: Ext[T]) { inline_ivy! (list0 <- self, t <- t, list1 -> self) -> () { _ list0 = @list_push(t list1) @@ -158,8 +136,8 @@ pub mod Ext { } // TODO: mark unsafe - pub fn .pop[T](&self: &Ext[List[T]]) -> T { - inline_ivy! (list0 <- self, list1 -> self) -> T { + pub fn .pop[T](&self: &Ext[List[T]]) -> Ext[T] { + inline_ivy! (list0 <- self, list1 -> self) -> Ext[T] { t list0 = @list_pop(t list1) } From 054d3c4f15d6139e6a1a937a759e20a62c175668 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Mon, 19 Jan 2026 10:02:21 +0100 Subject: [PATCH 15/18] split extrinsic registration into default & runtime --- cli/src/common.rs | 3 +- cli/src/ivy_cli.rs | 3 +- cli/src/vine_cli.rs | 3 +- ivy/src/host/ext.rs | 112 +++++++++++++++++---------------- ivy/src/optimize/pre_reduce.rs | 2 +- 5 files changed, 66 insertions(+), 57 deletions(-) diff --git a/cli/src/common.rs b/cli/src/common.rs index b5266c72f..9ca33ef48 100644 --- a/cli/src/common.rs +++ b/cli/src/common.rs @@ -46,7 +46,8 @@ impl RunArgs { }; let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics, self.args); + host.register_default_extrinsics(&mut extrinsics); + host.register_runtime_extrinsics(&mut extrinsics, self.args); host.insert_nets(&nets); let main = host.get("::").expect("missing main"); diff --git a/cli/src/ivy_cli.rs b/cli/src/ivy_cli.rs index cce5f18bf..bde6df469 100644 --- a/cli/src/ivy_cli.rs +++ b/cli/src/ivy_cli.rs @@ -83,7 +83,8 @@ impl IvyReplCommand { let heap = Heap::new(); let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics, self.run_args.args); + host.register_default_extrinsics(&mut extrinsics); + host.register_runtime_extrinsics(&mut extrinsics, self.run_args.args); host.insert_nets(&nets); let mut ivm = IVM::new(&heap, &extrinsics); diff --git a/cli/src/vine_cli.rs b/cli/src/vine_cli.rs index f049dfae6..f06649010 100644 --- a/cli/src/vine_cli.rs +++ b/cli/src/vine_cli.rs @@ -332,7 +332,8 @@ impl VineReplCommand { let host = &mut Host::default(); let heap = Heap::new(); let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics, self.args); + host.register_default_extrinsics(&mut extrinsics); + host.register_runtime_extrinsics(&mut extrinsics, self.args); let mut ivm = IVM::new(&heap, &extrinsics); let config = Config::new(!self.no_debug, false); diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index f30f7f6fd..42b0d0f90 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -116,67 +116,15 @@ impl<'ivm> Host<'ivm> { ty } - pub fn register_default_extrinsics( - &mut self, - extrinsics: &mut Extrinsics<'ivm>, - args: Vec, - ) { + pub fn register_default_extrinsics(&mut self, extrinsics: &mut Extrinsics<'ivm>) { let n32 = self.register_n32_ext_ty(extrinsics); let f32 = self.register_ext_ty::("F32", extrinsics); let f64 = self.register_ext_ty::("F64", extrinsics); - let list = self.register_ext_ty::>("LIST", extrinsics); - let io = self.register_ext_ty::<()>("IO", extrinsics); // u64 to/from (lo: u32, hi: u32) halves let u64_to_parts = |x: u64| (x as u32, (x >> 32) as u32); let u64_from_parts = |lo: u32, hi: u32| ((hi as u64) << 32) | (lo as u64); - self.register_ext_fn( - "list_push", - extrinsics.new_merge_ext_fn(move |ivm, l, el, out| { - let Some(mut l) = list.unwrap_ext_val(l) else { - ivm.flags.ext_generic = true; - return; - }; - - l.push(el); - - ivm.link_wire(out, Port::new_ext_val(list.wrap_ext_val(l))); - }), - ); - - self.register_ext_fn( - "list_pop", - extrinsics.new_split_ext_fn(move |ivm, l, out0, out1| { - let Some(mut l) = list.unwrap_ext_val(l) else { - ivm.flags.ext_generic = true; - return; - }; - - let Some(el) = l.pop() else { - ivm.flags.ext_generic = true; - return; - }; - - ivm.link_wire(out0, Port::new_ext_val(el)); - ivm.link_wire(out1, Port::new_ext_val(list.wrap_ext_val(l))); - }), - ); - - self.register_ext_fn( - "list_drop", - extrinsics.new_split_ext_fn(move |ivm, l, out0, out1| { - match list.unwrap_ext_val(l) { - Some(l) if !l.is_empty() => ivm.flags.ext_erase = true, - None => ivm.flags.ext_generic = true, - _ => {} - } - - ivm.link_wire(out0, Port::ERASE); - ivm.link_wire(out1, Port::ERASE); - }), - ); - register_ext_fns!(match (self, extrinsics) { "n32_add" => |a: n32, b: n32| -> n32 { a.wrapping_add(b) }, "n32_sub" => |a: n32, b: n32| -> n32 { a.wrapping_sub(b) }, @@ -245,7 +193,65 @@ impl<'ivm> Host<'ivm> { "f64_to_n64" => |f: f64| -> (n32, n32) { u64_to_parts(f as u64) }, "f64_to_bits" => |f: f64| -> (n32, n32) { u64_to_parts(f.to_bits()) }, "f64_from_bits" => |lo: n32, hi: n32| -> f64 { f64::from_bits(u64_from_parts(lo, hi)) }, + }); + } + pub fn register_runtime_extrinsics( + &mut self, + extrinsics: &mut Extrinsics<'ivm>, + args: Vec, + ) { + let n32 = extrinsics.n32_ext_ty(); + let list = self.register_ext_ty::>("LIST", extrinsics); + let io = self.register_ext_ty::<()>("IO", extrinsics); + + self.register_ext_fn( + "list_push", + extrinsics.new_merge_ext_fn(move |ivm, l, el, out| { + let Some(mut l) = list.unwrap_ext_val(l) else { + ivm.flags.ext_generic = true; + return; + }; + + l.push(el); + + ivm.link_wire(out, Port::new_ext_val(list.wrap_ext_val(l))); + }), + ); + + self.register_ext_fn( + "list_pop", + extrinsics.new_split_ext_fn(move |ivm, l, out0, out1| { + let Some(mut l) = list.unwrap_ext_val(l) else { + ivm.flags.ext_generic = true; + return; + }; + + let Some(el) = l.pop() else { + ivm.flags.ext_generic = true; + return; + }; + + ivm.link_wire(out0, Port::new_ext_val(el)); + ivm.link_wire(out1, Port::new_ext_val(list.wrap_ext_val(l))); + }), + ); + + self.register_ext_fn( + "list_drop", + extrinsics.new_split_ext_fn(move |ivm, l, out0, out1| { + match list.unwrap_ext_val(l) { + Some(l) if !l.is_empty() => ivm.flags.ext_erase = true, + None => ivm.flags.ext_generic = true, + _ => {} + } + + ivm.link_wire(out0, Port::ERASE); + ivm.link_wire(out1, Port::ERASE); + }), + ); + + register_ext_fns!(match (self, extrinsics) { "list_new" => |_unused: n32| -> list { ExtList::default() }, "list_len" => |l: list| -> (n32, list) { (l.len() as u32, l) }, diff --git a/ivy/src/optimize/pre_reduce.rs b/ivy/src/optimize/pre_reduce.rs index bdf4d402f..47b948e28 100644 --- a/ivy/src/optimize/pre_reduce.rs +++ b/ivy/src/optimize/pre_reduce.rs @@ -18,7 +18,7 @@ pub fn pre_reduce(nets: &mut Nets) { let heap = &Heap::new(); let mut host = &mut Host::default(); let mut extrinsics = Extrinsics::default(); - host.register_default_extrinsics(&mut extrinsics, Vec::new()); + host.register_default_extrinsics(&mut extrinsics); host._insert_nets(nets, true); for (name, net) in nets.iter_mut() { From eba617a00e48d42ace830052358185b11eecc098 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Mon, 12 Jan 2026 18:32:39 +0100 Subject: [PATCH 16/18] root/IO.vi -> root/IO/IO.vi --- root/{ => IO}/IO.vi | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename root/{ => IO}/IO.vi (100%) diff --git a/root/IO.vi b/root/IO/IO.vi similarity index 100% rename from root/IO.vi rename to root/IO/IO.vi From da78e970fe4870ef28449d4ca4308fa5ddd1d3e3 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Mon, 19 Jan 2026 16:33:24 +0100 Subject: [PATCH 17/18] add ExtFile, and File.vi with basic operations --- ivm/src/ext.rs | 46 +++++++++++++++++++++ ivy/src/host/ext.rs | 75 +++++++++++++++++++++++++++++++--- root/Ext.vi | 19 +++++++++ root/IO/File.vi | 99 +++++++++++++++++++++++++++++++++++++++++++++ root/IO/IO.vi | 2 + 5 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 root/IO/File.vi diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index 391fa99bc..f4f62aa57 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -6,8 +6,11 @@ use core::{ fmt::{self, Debug}, marker::PhantomData, + ops::{Deref, DerefMut}, }; +use std::fs::File; + use crate::{ivm::IVM, port::Tag, wire::Wire, word::Word}; macro_rules! trait_alias { @@ -316,6 +319,10 @@ impl<'ivm> ExtList<'ivm> { pub fn pop(&mut self) -> Option> { self.values.0.pop() } + + pub fn into_inner(self) -> Vec> { + self.values.0 + } } impl<'ivm> From>> for ExtList<'ivm> { @@ -341,6 +348,45 @@ impl<'ivm> ExtTyCast<'ivm> for ExtList<'ivm> { } } +pub struct ExtFile(Box>); + +impl From for ExtFile { + fn from(file: File) -> Self { + Self(Box::new(Aligned(file))) + } +} + +impl Deref for ExtFile { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.0.0 + } +} + +impl DerefMut for ExtFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0.0 + } +} + +impl<'ivm> ExtTyCast<'ivm> for ExtFile { + const COPY: bool = false; + + #[inline(always)] + fn into_payload(self) -> Word { + let pointer = Box::into_raw(self.0); + Word::from_ptr(pointer.cast()) + } + + #[inline(always)] + unsafe fn from_payload(payload: Word) -> Self { + let ptr = payload.ptr().cast_mut().cast(); + let file = unsafe { Box::from_raw(ptr) }; + Self(file) + } +} + /// Used for the `IO` extrinsic type. impl<'ivm> ExtTyCast<'ivm> for () { const COPY: bool = false; diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index 42b0d0f90..d367c1928 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -1,10 +1,11 @@ use std::{ + fs::OpenOptions, io::{self, Read, Write}, ops::{Add, Div, Mul, Sub}, }; use ivm::{ - ext::{ExtFn, ExtList, ExtTy, ExtTyCast, ExtTyId, ExtVal, Extrinsics}, + ext::{ExtFile, ExtFn, ExtList, ExtTy, ExtTyCast, ExtTyId, ExtVal, Extrinsics}, port::Port, }; @@ -38,7 +39,8 @@ macro_rules! register_ext_fns { (@impl $ext:ident, |$a:ident : $a_ty:ident| -> $c_ty:ident $body:block) => { $ext.new_split_ext_fn(move |ivm, $a, out0, out1| { let (val0, val1) = (|| { - let $a = $a_ty.unwrap_ext_val($a)?; + #[allow(unused_mut)] + let mut $a = $a_ty.unwrap_ext_val($a)?; let res = $c_ty.wrap_ext_val($body); Some((Port::ERASE, Port::new_ext_val(res))) })().unwrap_or_else(|| { @@ -53,10 +55,10 @@ macro_rules! register_ext_fns { (@impl $ext:ident, |$a:ident : $a_ty:ident, $b:ident : $b_ty:ident| -> $c_ty:ident $body:block) => { $ext.new_merge_ext_fn(move |ivm, $a, $b, out| { + #[allow(unused_mut)] let val = (|| { - #[allow(unused_mut)] let mut $a = $a_ty.unwrap_ext_val($a)?; - let $b = $b_ty.unwrap_ext_val($b)?; + let mut $b = $b_ty.unwrap_ext_val($b)?; let res = $c_ty.wrap_ext_val($body); Some(Port::new_ext_val(res)) })().unwrap_or_else(|| { @@ -125,6 +127,16 @@ impl<'ivm> Host<'ivm> { let u64_to_parts = |x: u64| (x as u32, (x >> 32) as u32); let u64_from_parts = |lo: u32, hi: u32| ((hi as u64) << 32) | (lo as u64); + self.register_ext_fn( + "is_n32", + extrinsics.new_split_ext_fn(move |ivm, unknown, is_n32_out, unknown_out| { + let is_n32 = unknown.ty_id() == n32.ty_id(); + + ivm.link_wire(is_n32_out, Port::new_ext_val(n32.wrap_ext_val(is_n32 as u32))); + ivm.link_wire(unknown_out, Port::new_ext_val(unknown)); + }), + ); + register_ext_fns!(match (self, extrinsics) { "n32_add" => |a: n32, b: n32| -> n32 { a.wrapping_add(b) }, "n32_sub" => |a: n32, b: n32| -> n32 { a.wrapping_sub(b) }, @@ -204,6 +216,7 @@ impl<'ivm> Host<'ivm> { let n32 = extrinsics.n32_ext_ty(); let list = self.register_ext_ty::>("LIST", extrinsics); let io = self.register_ext_ty::<()>("IO", extrinsics); + let file = self.register_ext_ty::("FILE", extrinsics); self.register_ext_fn( "list_push", @@ -300,6 +313,50 @@ impl<'ivm> Host<'ivm> { (c.unwrap_or(char::REPLACEMENT_CHARACTER as u32), ()) }, + + "io_open_file" => |_io: io, params: list| -> list { + let options = n32.unwrap_ext_val(params.pop()?)?; + let path = list.unwrap_ext_val(params.pop()?).and_then(|path| unwrap_ext_str(n32, path))?; + + let mut open_options = OpenOptions::new(); + let open_options = open_options + .append(options & 0x1 != 0) + .create(options & 0x10 != 0) + .read(options & 0x100 != 0); + + let mut res = ExtList::default(); + match open_options.open(path) { + Ok(f) => { + res.push(file.wrap_ext_val(f.into())); + } + Err(err) => { + // TODO: put an accurate error code + res.push(n32.wrap_ext_val(1)); + } + } + res.push(io.wrap_ext_val(())); + + res + }, + + "io_read_byte_file" => |params: list| -> list { + let mut f = file.unwrap_ext_val(params.pop()?)?; + + let () = io.unwrap_ext_val(params.pop()?)?; + + let mut buf = [0u8]; + let bytes = f.read(&mut buf).unwrap(); + let mut res = ExtList::default(); + + res.push(io.wrap_ext_val(())); + res.push(file.wrap_ext_val(f)); + res.push(n32.wrap_ext_val((bytes == 0) as u32)); + res.push(n32.wrap_ext_val(buf[0] as u32)); + + res + }, + + "io_close_file" => |_io: io, _f: file| -> io {}, }); } } @@ -313,7 +370,7 @@ fn read_bytes_into_utf8_u32(first_byte: u8, n: usize) -> Option { assert!((1..=3).contains(&n)); let buf = &mut [0; 4][..(n + 1)]; - let count = io::stdin().read(&mut buf[1..]).unwrap(); + let count = io::stdin().read(&mut buf[1..]).ok()?; if count != n { return None; } @@ -322,3 +379,11 @@ fn read_bytes_into_utf8_u32(first_byte: u8, n: usize) -> Option { Some(str::from_utf8(buf).ok()?.chars().next()? as u32) } + +fn unwrap_ext_str<'ivm>(n32: ExtTy<'ivm, u32>, chars: ExtList<'ivm>) -> Option { + chars + .into_inner() + .into_iter() + .map(|c| n32.unwrap_ext_val(c).map(char::try_from)?.ok()) + .collect::>() +} diff --git a/root/Ext.vi b/root/Ext.vi index 3a052b542..65d4e40b7 100644 --- a/root/Ext.vi +++ b/root/Ext.vi @@ -35,11 +35,13 @@ pub mod Ext { } } + pub impl bool: Ext[Bool] = Ext::identity; pub impl n32: Ext[N32] = Ext::identity; pub impl f32: Ext[F32] = Ext::identity; pub impl f64: Ext[F64] = Ext::identity; pub impl char: Ext[Char] = Ext::identity; pub impl io: Ext[IO] = Ext::identity; + pub impl ext[T]: Ext[Ext[T]] = Ext::identity; pub impl nil: Ext[()] { fn encode((): ()) -> Ext[()] { @@ -142,4 +144,21 @@ pub mod Ext { list0 = @list_pop(t list1) } } + + /// Used to mark extrinsic values of an unknown type. + pub type Unknown; + + /// Interprets `self` to an `Ext[N32]`, returning `Err(self)` on failure. + pub fn .as_n32(self: Ext[Unknown]) -> Result[N32, Ext[Unknown]] { + let is_n32 = inline_ivy! (ref0 <- self, ref1 -> self) -> Bool { + is_n32 + ref0 = @is_n32(is_n32 ref1) + }; + + if is_n32 { + Ok(transmute[Ext[Unknown], Ext[N32]](self) as N32) + } else { + Err(self) + } + } } diff --git a/root/IO/File.vi b/root/IO/File.vi new file mode 100644 index 000000000..83ce16220 --- /dev/null +++ b/root/IO/File.vi @@ -0,0 +1,99 @@ + +use #root::Ext; +use #root::Ext::Unknown; + +pub type File; + +pub mod File { + /// Opens a file in read-only mode. + /// + /// If you need to write or create a file, see [`Options`]. + pub fn open(&io: &IO, path: String) -> Result[File, N32] { + Options::new().read().open(&io, path) + } + + /// Reads all remaining bytes in the file to a list. + pub fn .read_bytes(&self: &File, &io: &IO) -> List[N32] { + let bytes = []; + + while self.read_byte(&io) is Some(char) { + bytes.push_back(char); + } + + bytes + } + + /// Reads the next byte in the file, returning `None()` on EOF. + pub fn .read_byte(&self: &File, &io: &IO) -> Option[N32] { + let byte: N32; + let is_eof: Bool; + let params = (self, io) as Ext; + (byte, is_eof, self, io) = inline_ivy! (params <- params) -> Ext[(N32, Bool, File, IO)] { + res + params = @io_read_byte_file(_ res) + } as ( + _, + _, + _, + _, + ); + + if is_eof { + None() + } else { + Some(byte) + } + } + + pub fn .close(self: File, &io: &IO) { + inline_ivy! (self <- self, io0 <- io, io1 -> io) -> () { + _ + io0 = @io_close_file(self io1) + } + } + + pub impl : Ext[File] = Ext::identity; + + /// Options which control how a file is opened, stored as a bit set. + pub struct* Options(N32); + + mod Options { + pub impl : Ext[Options]; + + pub fn new() -> Options { + Options(0) + } + + pub fn .append(self: Options) -> Options { + self! |= 0x1; + self + } + + pub fn .create(self: Options) -> Options { + self! |= 0x10; + self + } + + pub fn .read(self: Options) -> Options { + self! |= 0x100; + self + } + + pub fn .open(&self: &Options, &io: &IO, path: String) -> Result[File, N32] { + let params = (self, path) as Ext; + let file: Ext[Unknown]; + (io, file) = inline_ivy! (params <- params, io <- io) -> Ext[(IO, Ext[Unknown])] { + res + io = @io_open_file(params res) + } as ( + _, + _, + ); + + match file.as_n32() { + Ok(error) { Err(error) } + Err(file) { Ok(unsafe::transmute[Ext[Unknown], Ext[File]](file) as File) } + } + } + } +} diff --git a/root/IO/IO.vi b/root/IO/IO.vi index 53d97ded5..52fde2ea1 100644 --- a/root/IO/IO.vi +++ b/root/IO/IO.vi @@ -1,4 +1,6 @@ +pub mod File; + use #root::Ext; /// A special primitive type used to interact with the outside world. From b0c6c06173111071d70151db39806954b43c4556 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Tue, 20 Jan 2026 11:53:16 +0100 Subject: [PATCH 18/18] add Error --- ivm/src/ext.rs | 41 ++++++++++++++++- ivy/src/host/ext.rs | 22 +++++++-- root/IO/File.vi | 106 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 150 insertions(+), 19 deletions(-) diff --git a/ivm/src/ext.rs b/ivm/src/ext.rs index f4f62aa57..dad081674 100644 --- a/ivm/src/ext.rs +++ b/ivm/src/ext.rs @@ -9,7 +9,7 @@ use core::{ ops::{Deref, DerefMut}, }; -use std::fs::File; +use std::{fs::File, io}; use crate::{ivm::IVM, port::Tag, wire::Wire, word::Word}; @@ -387,6 +387,45 @@ impl<'ivm> ExtTyCast<'ivm> for ExtFile { } } +pub struct ExtIoError(Box>); + +impl From for ExtIoError { + fn from(error: io::Error) -> Self { + Self(Box::new(Aligned(error))) + } +} + +impl Deref for ExtIoError { + type Target = io::Error; + + fn deref(&self) -> &Self::Target { + &self.0.0 + } +} + +impl DerefMut for ExtIoError { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0.0 + } +} + +impl<'ivm> ExtTyCast<'ivm> for ExtIoError { + const COPY: bool = false; + + #[inline(always)] + fn into_payload(self) -> Word { + let pointer = Box::into_raw(self.0); + Word::from_ptr(pointer.cast()) + } + + #[inline(always)] + unsafe fn from_payload(payload: Word) -> Self { + let ptr = payload.ptr().cast_mut().cast(); + let error = unsafe { Box::from_raw(ptr) }; + Self(error) + } +} + /// Used for the `IO` extrinsic type. impl<'ivm> ExtTyCast<'ivm> for () { const COPY: bool = false; diff --git a/ivy/src/host/ext.rs b/ivy/src/host/ext.rs index d367c1928..ed7bf256b 100644 --- a/ivy/src/host/ext.rs +++ b/ivy/src/host/ext.rs @@ -1,11 +1,11 @@ use std::{ fs::OpenOptions, - io::{self, Read, Write}, + io::{self, ErrorKind, Read, Write}, ops::{Add, Div, Mul, Sub}, }; use ivm::{ - ext::{ExtFile, ExtFn, ExtList, ExtTy, ExtTyCast, ExtTyId, ExtVal, Extrinsics}, + ext::{ExtFile, ExtFn, ExtIoError, ExtList, ExtTy, ExtTyCast, ExtTyId, ExtVal, Extrinsics}, port::Port, }; @@ -217,6 +217,7 @@ impl<'ivm> Host<'ivm> { let list = self.register_ext_ty::>("LIST", extrinsics); let io = self.register_ext_ty::<()>("IO", extrinsics); let file = self.register_ext_ty::("FILE", extrinsics); + let io_err = self.register_ext_ty::("IO_ERR", extrinsics); self.register_ext_fn( "list_push", @@ -327,11 +328,12 @@ impl<'ivm> Host<'ivm> { let mut res = ExtList::default(); match open_options.open(path) { Ok(f) => { + res.push(n32.wrap_ext_val(0)); res.push(file.wrap_ext_val(f.into())); } Err(err) => { - // TODO: put an accurate error code res.push(n32.wrap_ext_val(1)); + res.push(io_err.wrap_ext_val(err.into())); } } res.push(io.wrap_ext_val(())); @@ -339,6 +341,20 @@ impl<'ivm> Host<'ivm> { res }, + "io_error_code" => |error: io_err| -> (n32, io_err) { + match error.kind() { + ErrorKind::NotFound => (1, error), + ErrorKind::PermissionDenied => (2, error), + ErrorKind::AlreadyExists => (3, error), + ErrorKind::NotADirectory => (4, error), + ErrorKind::IsADirectory => (5, error), + ErrorKind::ReadOnlyFilesystem => (6, error), + _ => (0, error), + } + }, + + "io_error_drop" => |_error: io_err| {}, + "io_read_byte_file" => |params: list| -> list { let mut f = file.unwrap_ext_val(params.pop()?)?; diff --git a/root/IO/File.vi b/root/IO/File.vi index 83ce16220..7438d79c8 100644 --- a/root/IO/File.vi +++ b/root/IO/File.vi @@ -1,14 +1,22 @@ -use #root::Ext; +use #root::{Ext, ops::Cast}; use #root::Ext::Unknown; pub type File; pub mod File { + pub impl : Show[File] { + fn show(&_self: &File) -> Show { + Show::Literal("") + } + } + + pub impl : Ext[File] = Ext::identity; + /// Opens a file in read-only mode. /// /// If you need to write or create a file, see [`Options`]. - pub fn open(&io: &IO, path: String) -> Result[File, N32] { + pub fn open(&io: &IO, path: String) -> Result[File, Error] { Options::new().read().open(&io, path) } @@ -52,12 +60,10 @@ pub mod File { } } - pub impl : Ext[File] = Ext::identity; - /// Options which control how a file is opened, stored as a bit set. pub struct* Options(N32); - mod Options { + pub mod Options { pub impl : Ext[Options]; pub fn new() -> Options { @@ -79,20 +85,90 @@ pub mod File { self } - pub fn .open(&self: &Options, &io: &IO, path: String) -> Result[File, N32] { + pub fn .open(&self: &Options, &io: &IO, path: String) -> Result[File, Error] { let params = (self, path) as Ext; let file: Ext[Unknown]; - (io, file) = inline_ivy! (params <- params, io <- io) -> Ext[(IO, Ext[Unknown])] { + let is_error: Bool; + (io, file, is_error) = inline_ivy! (params <- params, io <- io) -> Ext[ + (IO, Ext[Unknown], Bool); + ] { res io = @io_open_file(params res) - } as ( - _, - _, - ); - - match file.as_n32() { - Ok(error) { Err(error) } - Err(file) { Ok(unsafe::transmute[Ext[Unknown], Ext[File]](file) as File) } + } as (_, _, _); + + if is_error { + Err(unsafe::transmute[Ext[Unknown], ExtError](file) as Error) + } else { + Ok(unsafe::transmute[Ext[Unknown], Ext[File]](file) as File) + } + } + } + + pub enum? Error { + NotFound(), + PermissionDenied(), + AlreadyExists(), + NotADirectory(), + IsADirectory(), + ReadOnlyFilesystem(), + Other(ExtError), + } + + pub mod Error { + pub impl : Show[Error]; + + pub impl : Cast[ExtError, Error] { + fn cast(error: ExtError) -> Error { + let code = error.code(); + + if code == 1 { + return Error::NotFound(); + } + if code == 2 { + return Error::PermissionDenied(); + } + if code == 3 { + return Error::AlreadyExists(); + } + if code == 4 { + return Error::NotADirectory(); + } + if code == 5 { + return Error::IsADirectory(); + } + if code == 6 { + return Error::ReadOnlyFilesystem(); + } + + return Error::Other(error); + } + } + } + + pub type ExtError; + + pub mod ExtError { + pub impl : Ext[ExtError] = Ext::identity; + + pub fn .code(&self: &ExtError) -> N32 { + inline_ivy! (self0 <- self, self1 -> self) -> N32 { + code + self0 = @io_error_code(code self1) + } + } + + pub impl : Drop[ExtError] { + fn drop(self: ExtError) { + inline_ivy! (self <- self) -> () { + _ + self = @io_error_drop(_ _) + } + } + } + + pub impl : Show[ExtError] { + fn show(&self: &ExtError) -> Show { + self.code().show() } } }