From b1943d1a4f9eb798af30bdc8929dece4d378f8c3 Mon Sep 17 00:00:00 2001 From: jess Date: Mon, 8 Jun 2026 17:13:32 -0700 Subject: [PATCH] closure closure --- compile/src/lib.rs | 265 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 234 insertions(+), 31 deletions(-) diff --git a/compile/src/lib.rs b/compile/src/lib.rs index f8c168a..4396ac5 100644 --- a/compile/src/lib.rs +++ b/compile/src/lib.rs @@ -8,7 +8,7 @@ use acord_core::interp::{parse_program, read_module_source, Expr, Op, Stmt}; /// decomposition context threaded through every emit step. struct Ctx<'a> { hook: &'a dyn DecomposeHook, - /// user-defined function/solve names — these shadow same-named builtins. + /// names of user functions and solve-defs, shadowing same-named builtins. user_fns: HashSet, } @@ -135,7 +135,7 @@ fn emit_program(out: &mut String, stmts: &[Stmt], deps: &mut Vec, ho } } - // names that resolve to user code first, so a same-named builtin is suppressed. + // collects user function and solve names. let mut user_fns = HashSet::new(); for s in stmts { match s { @@ -301,8 +301,7 @@ fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec { let Some(root) = segments.first() else { return Ok(()); }; - // `use spice` / `use ring` are language directives, not importable modules — - // spice literals and ring values are baked into the runtime, so emit nothing. + // skips spice and ring directives, leaving no module import. if is_builtin_use(root) { return Ok(()); } deps.push(Dependency { segments: segments.clone(), wildcard: *wildcard }); let mod_ident = root.replace('-', "_"); @@ -362,7 +361,7 @@ fn collect_lvalue_path(expr: &Expr, steps: &mut Vec, cx: &Ctx) -> Result } } -/// value builtins routed through the runtime `v_builtin_call` dispatcher. +/// builtin names lowered to a single runtime dispatch call. const VALUE_BUILTINS: &[&str] = &[ "sin", "cos", "tan", "asin", "acos", "atan", "sqrt", "abs", "ln", "log", "floor", "ceil", "round", "rand", "seed", @@ -370,9 +369,10 @@ const VALUE_BUILTINS: &[&str] = &[ "len", "range", "push", "take", "skip", "drop", "chunk", "window", "zip", "flatten", "distinct", "unique", "delta", "sort", "hooks", "module_paths", "module_subpaths", + "ring", "peek", "history", ]; -/// maps a higher-order builtin name to its runtime fn, or None if not a HOF. +/// maps a higher-order builtin name to the runtime function, None when not higher-order. fn hof_vname(lname: &str) -> Option<&'static str> { Some(match lname { "map" => "v_map", @@ -393,19 +393,47 @@ fn hof_vname(lname: &str) -> Option<&'static str> { }) } -/// emits a HOF call: leading value args (first by ref), trailing function name as a fn pointer. -fn emit_hof_core(vname: &str, val_args: &[Expr], fn_ident: &str, cx: &Ctx) -> Result { +/// true for the higher-order builtins taking a two-argument accumulator function. +fn hof_is_binary(lname: &str) -> bool { matches!(lname, "fold" | "reduce" | "scan") } + +/// resolves a higher-order function argument, wrapping builtins in a closure and passing user functions directly. +fn hof_fn_arg(name: &str, binary: bool, cx: &Ctx) -> String { + if cx.user_fns.contains(name) { + return ident(name); + } + let lname = name.to_ascii_lowercase(); + if VALUE_BUILTINS.contains(&lname.as_str()) { + return if binary { + format!("(|__a: V, __b: V| v_builtin_call({:?}, &[__a, __b]).unwrap_or(V::Void))", lname) + } else { + format!("(|__v: V| v_builtin_call({:?}, &[__v]).unwrap_or(V::Void))", lname) + }; + } + ident(name) +} + +/// emits a higher-order call, first value arg by reference, function argument last. +fn emit_hof_core(vname: &str, val_args: &[Expr], fn_arg: &str, cx: &Ctx) -> Result { let mut parts = Vec::with_capacity(val_args.len() + 1); for (i, a) in val_args.iter().enumerate() { let s = emit_expr(a, cx)?; parts.push(if i == 0 { format!("&{}", s) } else { s }); } - parts.push(fn_ident.to_string()); + parts.push(fn_arg.to_string()); Ok(format!("{}({})", vname, parts.join(", "))) } -/// HOF in free-call position — the last argument must be a bare function name. +/// lowers a free-call higher-order builtin to the runtime function. fn try_hof_call(lname: &str, args: &[Expr], cx: &Ctx) -> Result, String> { + // ring iter takes the function as the middle of three arguments. + if lname == "iter" && args.len() == 3 { + let Expr::Ident(fname) = &args[1] else { + return Err("iter() expects a function name as its second argument".into()); + }; + let arr = emit_expr(&args[0], cx)?; + let ring = emit_expr(&args[2], cx)?; + return Ok(Some(format!("v_ring_iter(&{}, {}, &{})", arr, hof_fn_arg(fname, false, cx), ring))); + } let Some(vname) = hof_vname(lname) else { return Ok(None); }; let Some((fn_arg, val_args)) = args.split_last() else { return Err(format!("{}() requires arguments", lname)); @@ -413,11 +441,11 @@ fn try_hof_call(lname: &str, args: &[Expr], cx: &Ctx) -> Result, let Expr::Ident(fname) = fn_arg else { return Err(format!("{}() expects a function name as its last argument", lname)); }; - Ok(Some(emit_hof_core(vname, val_args, &ident(fname), cx)?)) + let resolved = hof_fn_arg(fname, hof_is_binary(lname), cx); + Ok(Some(emit_hof_core(vname, val_args, &resolved, cx)?)) } -/// HOF in method position — only fires when the trailing arg names a user function, -/// so `obj.find(some_var)` on a struct still routes as a normal method call. +/// lowers a method-call higher-order builtin only when the trailing argument names a user function. fn try_hof_method(lname: &str, recv: &Expr, args: &[Expr], cx: &Ctx) -> Result, String> { let Some(vname) = hof_vname(lname) else { return Ok(None); }; let Some((fn_arg, mid)) = args.split_last() else { return Ok(None); }; @@ -425,7 +453,8 @@ fn try_hof_method(lname: &str, recv: &Expr, args: &[Expr], cx: &Ctx) -> Result Result { @@ -434,10 +463,10 @@ fn emit_expr(expr: &Expr, cx: &Ctx) -> Result { } Ok(match expr { Expr::Num(n) => format!("V::Num({})", fmt_num(*n)), - Expr::Spice(n, unit) => format!("V::Array(vec![V::Num({}), V::Str({:?}.into())])", fmt_num(*n), unit), + Expr::Spice(n, unit) => format!("V::Spice({}, {:?}.into())", fmt_num(*n), unit), Expr::Bool(b) => format!("V::Bool({})", b), Expr::Str(s) => format!("V::Str({:?}.into())", s), - // `pi` is the one builtin constant; everything else is a variable read. + // builtin constant pi. Expr::Ident(name) if name == "pi" => "V::Num(std::f64::consts::PI)".to_string(), Expr::Ident(name) => format!("{}.clone()", ident(name)), Expr::UnaryOp(op, inner) => { @@ -475,7 +504,7 @@ fn emit_expr(expr: &Expr, cx: &Ctx) -> Result { if let Some(custom) = cx.hook.call(name, args) { custom } else if cx.user_fns.contains(name) { - // user-defined function wins over any same-named builtin. + // user function shadows the same-named builtin. let arg_list = emit_args(args, cx)?; format!("{}({})", ident(name), arg_list.join(", ")) } else { @@ -543,7 +572,7 @@ fn emit_expr(expr: &Expr, cx: &Ctx) -> Result { } Expr::MethodCall(recv, method, args) => { let lname = method.to_ascii_lowercase(); - // `arr.map(f)` style — only when `f` is a known user function (see try_hof_method). + // method-call higher-order builtins. if let Some(s) = try_hof_method(&lname, recv, args, cx)? { s } else { @@ -563,12 +592,12 @@ fn emit_expr(expr: &Expr, cx: &Ctx) -> Result { }) } -/// emits each argument expression, in order. +/// emits each argument expression. fn emit_args(args: &[Expr], cx: &Ctx) -> Result, String> { args.iter().map(|a| emit_expr(a, cx)).collect() } -/// `spice` and `ring` are built-in language directives, not resolvable modules. +/// recognizes spice and ring as language directives rather than resolvable modules. fn is_builtin_use(root: &str) -> bool { root == "spice" || root == "ring" } @@ -603,6 +632,8 @@ const RESERVED: &[&str] = &[ const PRELUDE_HEADER: &str = r#"#![allow(unused_variables, unused_mut, dead_code, unused_imports, non_snake_case)] use std::collections::BTreeMap; +use std::rc::Rc; +use std::cell::RefCell; #[derive(Clone, Debug, PartialEq)] pub enum V { @@ -611,6 +642,10 @@ pub enum V { Bool(bool), Array(Vec), Struct(BTreeMap), + /// scalar carrying a SPICE unit. + Spice(f64, String), + /// shared fixed-capacity ring buffer. + Ring(Rc>), Void, } @@ -636,16 +671,66 @@ impl std::fmt::Display for V { } write!(f, "}}") } + V::Spice(n, u) => write!(f, "{}", format_spice(*n, u)), + V::Ring(r) => write!(f, "{}", r.borrow().display()), V::Void => write!(f, "()"), } } } +/// shared mutable ring buffer with FIFO eviction. +#[derive(Debug, PartialEq)] +pub struct RingBuf { + pub volume: usize, + pub shape: Option<(usize, usize)>, + pub slots: Vec>, + pub head: usize, + pub len: usize, +} + +impl RingBuf { + pub fn flat(volume: usize) -> Self { + RingBuf { volume, shape: None, slots: vec![None; volume], head: 0, len: 0 } + } + pub fn rect(length: usize, width: usize) -> Self { + let v = length.saturating_mul(width); + RingBuf { volume: v, shape: Some((length, width)), slots: vec![None; v], head: 0, len: 0 } + } + /// pushes a run, overwriting oldest entries once full. + pub fn push_run(&mut self, incoming: &[V]) { + if self.volume == 0 { return; } + for v in incoming { + self.slots[self.head] = Some(v.clone()); + self.head = (self.head + 1) % self.volume; + if self.len < self.volume { self.len += 1; } + } + } + /// cloned chronological snapshot of filled slots. + pub fn snapshot(&self) -> Vec { + if self.len == 0 { return Vec::new(); } + let start = (self.head + self.volume - self.len) % self.volume; + (0..self.len).map(|i| self.slots[(start + i) % self.volume].clone().unwrap()).collect() + } + pub fn display(&self) -> String { + let parts: Vec = self.snapshot().into_iter().map(|v| match v { + V::Str(s) => format!("\"{}\"", s), + V::Void => "null".to_string(), + other => format!("{}", other), + }).collect(); + let shape = match self.shape { + Some((l, w)) => format!(" [{}x{}]", l, w), + None => String::new(), + }; + format!("ring{}({})", shape, parts.join(", ")) + } +} + enum Step { Field(String), Index(i64) } "#; -const PRELUDE_FNS: &str = r#"fn v_num(v: &V) -> f64 { match v { V::Num(n) => *n, V::Bool(b) => if *b { 1.0 } else { 0.0 }, _ => 0.0 } } -fn v_truthy(v: &V) -> bool { match v { V::Bool(b) => *b, V::Num(n) => *n != 0.0, V::Str(s) => !s.is_empty(), V::Array(a) => !a.is_empty(), V::Struct(m) => !m.is_empty(), _ => false } } +const PRELUDE_FNS: &str = r#"fn v_num(v: &V) -> f64 { match v { V::Num(n) => *n, V::Spice(n, _) => *n, V::Bool(b) => if *b { 1.0 } else { 0.0 }, _ => 0.0 } } +fn v_truthy(v: &V) -> bool { match v { V::Bool(b) => *b, V::Num(n) => *n != 0.0, V::Spice(n, _) => *n != 0.0, V::Str(s) => !s.is_empty(), V::Array(a) => !a.is_empty(), V::Struct(m) => !m.is_empty(), V::Ring(r) => r.borrow().len > 0, _ => false } } +fn v_is_spice(v: &V) -> bool { matches!(v, V::Spice(_, _)) } fn v_struct_type(v: &V) -> Option { if let V::Struct(m) = v { @@ -667,15 +752,15 @@ fn v_neg(a: &V) -> V { if let Some(r) = v_unop_overload("neg", a) { return r; } fn v_not(a: &V) -> V { if let Some(r) = v_unop_overload("not", a) { return r; } V::Bool(!v_truthy(a)) } fn v_strip(a: &V) -> V { if let Some(r) = v_unop_overload("strip", a) { return r; } match a { V::Str(s) => V::Str(s.trim().into()), V::Bool(b) => V::Num(if *b { 1.0 } else { 0.0 }), _ => a.clone() } } -fn v_add(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("add", a, b) { return r; } match (a, b) { (V::Str(s1), V::Str(s2)) => V::Str(format!("{}{}", s1, s2)), (V::Str(s), o) => V::Str(format!("{}{}", s, o)), (o, V::Str(s)) => V::Str(format!("{}{}", o, s)), (V::Array(a1), V::Array(a2)) => V::Array([a1.as_slice(), a2.as_slice()].concat()), (V::Array(a1), o) => { let mut v = a1.clone(); v.push(o.clone()); V::Array(v) }, (o, V::Array(a2)) => { let mut v = vec![o.clone()]; v.extend(a2.iter().cloned()); V::Array(v) }, _ => V::Num(v_num(a) + v_num(b)) } } -fn v_sub(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("sub", a, b) { return r; } V::Num(v_num(a) - v_num(b)) } -fn v_mul(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("mul", a, b) { return r; } V::Num(v_num(a) * v_num(b)) } -fn v_div(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("div", a, b) { return r; } let d = v_num(b); V::Num(if d == 0.0 { f64::NAN } else { v_num(a) / d }) } -fn v_rem(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("rem", a, b) { return r; } V::Num(v_num(a) % v_num(b)) } -fn v_pow(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("pow", a, b) { return r; } V::Num(v_num(a).powf(v_num(b))) } +fn v_add(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("add", a, b) { return r; } match (a, b) { (V::Str(s1), V::Str(s2)) => V::Str(format!("{}{}", s1, s2)), (V::Str(s), o) => V::Str(format!("{}{}", s, o)), (o, V::Str(s)) => V::Str(format!("{}{}", o, s)), (V::Array(a1), V::Array(a2)) => V::Array([a1.as_slice(), a2.as_slice()].concat()), (V::Array(a1), o) => { let mut v = a1.clone(); v.push(o.clone()); V::Array(v) }, (o, V::Array(a2)) => { let mut v = vec![o.clone()]; v.extend(a2.iter().cloned()); V::Array(v) }, _ => v_spice_arith("add", a, b) } } +fn v_sub(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("sub", a, b) { return r; } v_spice_arith("sub", a, b) } +fn v_mul(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("mul", a, b) { return r; } v_spice_arith("mul", a, b) } +fn v_div(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("div", a, b) { return r; } v_spice_arith("div", a, b) } +fn v_rem(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("rem", a, b) { return r; } v_spice_arith("rem", a, b) } +fn v_pow(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("pow", a, b) { return r; } v_spice_arith("pow", a, b) } -fn v_eq(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("eq", a, b) { return r; } V::Bool(a == b) } -fn v_neq(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("ne", a, b) { return r; } V::Bool(a != b) } +fn v_eq(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("eq", a, b) { return r; } if v_is_spice(a) || v_is_spice(b) { return V::Bool(v_num(a) == v_num(b)); } V::Bool(a == b) } +fn v_neq(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("ne", a, b) { return r; } if v_is_spice(a) || v_is_spice(b) { return V::Bool(v_num(a) != v_num(b)); } V::Bool(a != b) } fn v_lt(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("lt", a, b) { return r; } V::Bool(v_num(a) < v_num(b)) } fn v_gt(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("gt", a, b) { return r; } V::Bool(v_num(a) > v_num(b)) } fn v_lte(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("le", a, b) { return r; } V::Bool(v_num(a) <= v_num(b)) } @@ -766,7 +851,7 @@ fn v_method_call(recv: &V, method: &str, args: &[V]) -> V { if let Some(t) = v_struct_type(recv) { if let Some(r) = v_dispatch(&t, method, &full) { return r; } } - // UFCS: a non-method call routes to the same builtins as a free call. + // non-struct method calls fall back to the free-call builtins. if let Some(r) = v_builtin_call(method, &full) { return r; } V::Void } @@ -799,6 +884,68 @@ fn v_sort_by(a: &V, f: fn(V) -> V) -> V { match a { V::Array(x) => { let mut key fn v_reduce(a: &V, f: fn(V, V) -> V) -> V { match a { V::Array(x) => { let mut it = x.iter().cloned(); let mut acc = match it.next() { Some(v) => v, None => return V::Void }; for v in it { acc = f(acc, v); } acc }, _ => V::Void } } fn v_fold(a: &V, seed: V, f: fn(V, V) -> V) -> V { let mut acc = seed; if let V::Array(x) = a { for v in x { acc = f(acc, v.clone()); } } acc } fn v_scan(a: &V, seed: V, f: fn(V, V) -> V) -> V { let mut acc = seed; let mut out = Vec::new(); if let V::Array(x) = a { for v in x { acc = f(acc, v.clone()); out.push(acc.clone()); } } V::Array(out) } +fn v_unwrap_spice(v: &V) -> (f64, Option) { match v { V::Spice(n, u) => (*n, Some(u.clone())), _ => (v_num(v), None) } } +fn v_retag_spice(n: f64, unit: Option) -> V { match unit { Some(u) => V::Spice(n, u), None => V::Num(n) } } +fn v_unit_mul(a: &str, b: &str) -> Option { match (a.is_empty(), b.is_empty()) { (true, true) => None, (true, false) => Some(b.to_string()), (false, true) => Some(a.to_string()), (false, false) if a == b => Some(format!("{}\u{b2}", a)), (false, false) => Some(format!("{}\u{b7}{}", a, b)) } } +fn v_unit_div(a: &str, b: &str) -> Option { if a == b { return None; } if b.is_empty() { return if a.is_empty() { None } else { Some(a.to_string()) }; } if a.is_empty() { return Some(format!("1/{}", b)); } Some(format!("{}/{}", a, b)) } +fn v_unit_pow(a: &str, exp: f64) -> Option { if a.is_empty() { return None; } if exp == 1.0 { return Some(a.to_string()); } if exp == 2.0 { return Some(format!("{}\u{b2}", a)); } if exp == 3.0 { return Some(format!("{}\u{b3}", a)); } if exp == 0.5 { return Some(format!("\u{221a}{}", a)); } if exp == exp.trunc() && exp.abs() < 1e9 { return Some(format!("{}^{}", a, exp as i64)); } Some(format!("{}^{}", a, exp)) } +fn v_unit_additive(a: &str, b: &str) -> Option { if a == b { if a.is_empty() { None } else { Some(a.to_string()) } } else if a.is_empty() { Some(b.to_string()) } else if b.is_empty() { Some(a.to_string()) } else { None } } +fn v_spice_arith(op: &str, a: &V, b: &V) -> V { + let (an, au) = v_unwrap_spice(a); + let (bn, bu) = v_unwrap_spice(b); + let had = au.is_some() || bu.is_some(); + let la = au.unwrap_or_default(); + let ra = bu.unwrap_or_default(); + let unit_after = if !had { None } else { match op { + "add" | "sub" | "rem" => v_unit_additive(&la, &ra), + "mul" => v_unit_mul(&la, &ra), + "div" => v_unit_div(&la, &ra), + "pow" => v_unit_pow(&la, bn), + _ => None, + } }; + let res = match op { "add" => an + bn, "sub" => an - bn, "mul" => an * bn, "div" => if bn == 0.0 { f64::NAN } else { an / bn }, "rem" => an % bn, "pow" => an.powf(bn), _ => 0.0 }; + v_retag_spice(res, unit_after) +} +fn format_spice(n: f64, unit: &str) -> String { + if n == 0.0 { return format!("0{}", unit); } + if !n.is_finite() { return format!("{}{}", n, unit); } + let abs_n = n.abs(); + let (prefix, scale): (&str, f64) = if abs_n >= 1.0 { ("", 1.0) } else if abs_n >= 1e-3 { ("m", 1e-3) } else if abs_n >= 1e-6 { ("U", 1e-6) } else if abs_n >= 1e-9 { ("N", 1e-9) } else { ("p", 1e-12) }; + let mantissa = n / scale; + let mag = mantissa.abs().log10().floor() as i32; + let decimals = (2 - mag).max(0) as usize; + let raw = format!("{:.*}", decimals, mantissa); + let trimmed: &str = if raw.contains('.') { raw.trim_end_matches('0').trim_end_matches('.') } else { raw.as_str() }; + let compound = unit.chars().any(|c| !c.is_ascii_alphabetic()); + let sep = if compound && !unit.is_empty() { " " } else { "" }; + format!("{}{}{}{}", trimmed, prefix, sep, unit) +} +fn v_ring_new(a: &[V]) -> V { + match a.len() { + 1 => match v_arg(a, 0) { + V::Num(n) if n >= 0.0 && n.is_finite() && n == n.trunc() => V::Ring(Rc::new(RefCell::new(RingBuf::flat(n as usize)))), + V::Array(items) if items.len() == 2 => { let l = v_num(&items[0]).max(0.0) as usize; let w = v_num(&items[1]).max(0.0) as usize; V::Ring(Rc::new(RefCell::new(RingBuf::rect(l, w)))) } + _ => V::Void, + }, + 2 => match (v_arg(a, 0), v_arg(a, 1)) { (V::Bool(false), V::Num(n)) if n >= 0.0 && n.is_finite() && n == n.trunc() => V::Ring(Rc::new(RefCell::new(RingBuf::flat(n as usize)))), _ => V::Void }, + _ => V::Void, + } +} +fn v_ring_peek(a: &[V]) -> V { + let r = match v_arg(a, 0) { V::Ring(r) => r, _ => return V::Void }; + let snapshot = r.borrow().snapshot(); + if a.len() >= 2 { let n = v_num(&v_arg(a, 1)).max(0.0) as usize; let take = n.min(snapshot.len()); V::Array(snapshot[snapshot.len() - take..].to_vec()) } else { V::Array(snapshot) } +} +fn v_ring_iter(arr: &V, f: fn(V) -> V, ring: &V) -> V { + let r = match ring { V::Ring(r) => r.clone(), _ => return V::Void }; + if let V::Array(items) = arr { + for v in items { + let pushed = match f(v.clone()) { V::Array(a) => a, V::Void => Vec::new(), other => vec![other] }; + r.borrow_mut().push_run(&pushed); + } + } + V::Ring(r) +} fn v_builtin_call(name: &str, a: &[V]) -> Option { let r = match name { "sin" => V::Num(v_num(&v_arg(a, 0)).sin()), @@ -827,6 +974,8 @@ fn v_builtin_call(name: &str, a: &[V]) -> Option { "distinct" | "unique" => v_distinct(&v_arg(a, 0)), "delta" => v_delta(&v_arg(a, 0)), "sort" => { let mut x = match v_arg(a, 0) { V::Array(x) => x, _ => return Some(V::Void) }; v_sort_vals(&mut x); V::Array(x) } + "ring" => v_ring_new(a), + "peek" | "history" => v_ring_peek(a), "hooks" | "module_paths" | "module_subpaths" => V::Array(vec![]), _ => return None, }; @@ -1056,6 +1205,60 @@ fn main() { std::fs::remove_dir_all(&dir).ok(); } + #[test] + fn spice_ring_and_hof_builtins_compile_and_run() { + let src = "\ +use spice +use ring +fn sq(x) { + return x * x +} +fn voltage() { + let v = 5V + let i = 2A + return v * i +} +fn ringo() { + let r = ring(3) + let filled = iter([1, 2, 3, 4], sq, r) + return peek(filled) +} +fn mapped() { + return map([1, 4, 9], sqrt) +} +"; + let mut code = decompose(src).unwrap().code; + assert!(!code.contains("mod spice"), "use spice must not emit a module"); + assert!(!code.contains("mod ring"), "use ring must not emit a module"); + code.push_str(r#" +fn main() { + assert_eq!(voltage(), V::Spice(10.0, "V\u{b7}A".into())); + assert_eq!(ringo(), V::Array(vec![V::Num(4.0), V::Num(9.0), V::Num(16.0)])); + assert_eq!(mapped(), V::Array(vec![V::Num(1.0), V::Num(2.0), V::Num(3.0)])); + println!("OK"); +} +"#); + let dir = std::env::temp_dir().join(format!("acord-dec-sr-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let src_path = dir.join("prog.rs"); + let bin_path = dir.join("prog"); + std::fs::write(&src_path, &code).unwrap(); + + let compile = std::process::Command::new("rustc") + .arg("--edition").arg("2021").arg("-O") + .arg(&src_path).arg("-o").arg(&bin_path) + .output(); + let compile = match compile { + Ok(c) => c, + Err(_) => { std::fs::remove_dir_all(&dir).ok(); return; } + }; + assert!(compile.status.success(), "rustc failed:\n{}", String::from_utf8_lossy(&compile.stderr)); + let run = std::process::Command::new(&bin_path).output().unwrap(); + assert_eq!(String::from_utf8_lossy(&run.stdout).trim(), "OK"); + std::fs::remove_dir_all(&dir).ok(); + } + #[test] fn overridden_prelude_compiles_and_runs() { let mut code = decompose_with("let x = 1", &FieldOverride).unwrap().code;