closure closure

This commit is contained in:
jess 2026-06-08 17:13:32 -07:00
parent 4e6141cbd2
commit b1943d1a4f
1 changed files with 234 additions and 31 deletions

View File

@ -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<String>,
}
@ -135,7 +135,7 @@ fn emit_program(out: &mut String, stmts: &[Stmt], deps: &mut Vec<Dependency>, 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<Depende
}
Stmt::Use(segments, wildcard) => {
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<String>, 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<String, String> {
/// 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<String, String> {
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<Option<String>, 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<Option<String>,
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<Option<String>, 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<O
if !cx.user_fns.contains(fname) { return Ok(None); }
let mut val_args = vec![recv.clone()];
val_args.extend(mid.iter().cloned());
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)?))
}
fn emit_expr(expr: &Expr, cx: &Ctx) -> Result<String, String> {
@ -434,10 +463,10 @@ fn emit_expr(expr: &Expr, cx: &Ctx) -> Result<String, String> {
}
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<String, String> {
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<String, String> {
}
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<String, String> {
})
}
/// emits each argument expression, in order.
/// emits each argument expression.
fn emit_args(args: &[Expr], cx: &Ctx) -> Result<Vec<String>, 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<V>),
Struct(BTreeMap<String, V>),
/// scalar carrying a SPICE unit.
Spice(f64, String),
/// shared fixed-capacity ring buffer.
Ring(Rc<RefCell<RingBuf>>),
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<Option<V>>,
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<V> {
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<String> = 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<String> {
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<String>) { match v { V::Spice(n, u) => (*n, Some(u.clone())), _ => (v_num(v), None) } }
fn v_retag_spice(n: f64, unit: Option<String>) -> V { match unit { Some(u) => V::Spice(n, u), None => V::Num(n) } }
fn v_unit_mul(a: &str, b: &str) -> Option<String> { 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<String> { 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<String> { 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<String> { 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<V> {
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<V> {
"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;