use crate::ast::*;
use crate::builtins::{Builtin, CharAtResult, char_at_signed};
use crate::caps::Caps;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
pub mod http_wasm;
pub mod json;
#[derive(Debug)]
pub struct TraceEvent {
pub line: usize,
pub stmt: String,
pub bindings: Vec<(String, Value)>,
pub result: Value,
}
#[derive(Debug)]
pub struct ExprTraceEvent {
pub line: usize,
pub expr: String,
pub refs: Vec<String>,
pub result: Value,
}
std::thread_local! {
#[allow(clippy::type_complexity)]
pub(crate) static TRACE_HOOK: std::cell::RefCell<Option<Box<dyn FnMut(TraceEvent)>>> =
const { std::cell::RefCell::new(None) };
pub(crate) static TRACE_SOURCE: std::cell::RefCell<Option<String>> =
const { std::cell::RefCell::new(None) };
#[allow(clippy::type_complexity)]
static EXPR_TRACE_HOOK: std::cell::RefCell<Option<Box<dyn FnMut(ExprTraceEvent)>>> =
const { std::cell::RefCell::new(None) };
static CURRENT_STMT_SPAN: std::cell::RefCell<Span> =
const { std::cell::RefCell::new(Span { start: 0, end: 0 }) };
}
#[inline]
pub(crate) fn trace_hook_active() -> bool {
TRACE_HOOK.with(|h| h.borrow().is_some())
}
#[inline]
pub(crate) fn fire_trace_hook(ev: TraceEvent) {
TRACE_HOOK.with(|h| {
if let Some(ref mut hook) = *h.borrow_mut() {
hook(ev);
}
});
}
pub fn run_with_trace<F>(
program: &Program,
func_name: Option<&str>,
args: Vec<Value>,
on_event: F,
) -> Result<Value>
where
F: FnMut(TraceEvent) + 'static,
{
run_with_trace_opts(
program,
func_name,
args,
on_event,
None::<fn(ExprTraceEvent)>,
)
}
pub fn run_with_trace_opts<F, G>(
program: &Program,
func_name: Option<&str>,
args: Vec<Value>,
on_stmt: F,
on_expr: Option<G>,
) -> Result<Value>
where
F: FnMut(TraceEvent) + 'static,
G: FnMut(ExprTraceEvent) + 'static,
{
TRACE_HOOK.with(|h| {
*h.borrow_mut() = Some(Box::new(on_stmt));
});
TRACE_SOURCE.with(|s| {
*s.borrow_mut() = program.source.clone();
});
if let Some(expr_hook) = on_expr {
EXPR_TRACE_HOOK.with(|h| {
*h.borrow_mut() = Some(Box::new(expr_hook));
});
}
let result = run_with_env(program, func_name, args, Env::new());
TRACE_HOOK.with(|h| {
*h.borrow_mut() = None;
});
TRACE_SOURCE.with(|s| {
*s.borrow_mut() = None;
});
EXPR_TRACE_HOOK.with(|h| {
*h.borrow_mut() = None;
});
result
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum MapKey {
Text(String),
Int(i64),
}
impl MapKey {
pub fn as_text(&self) -> Option<&str> {
match self {
MapKey::Text(s) => Some(s.as_str()),
_ => None,
}
}
pub fn as_int(&self) -> Option<i64> {
match self {
MapKey::Int(n) => Some(*n),
_ => None,
}
}
pub fn to_display_string(&self) -> String {
match self {
MapKey::Text(s) => s.clone(),
MapKey::Int(n) => n.to_string(),
}
}
pub fn from_value(v: &Value, op_name: &str) -> std::result::Result<Self, RuntimeError> {
match v {
Value::Text(s) => Ok(MapKey::Text((**s).clone())),
Value::Number(n) => {
if !n.is_finite() {
return Err(RuntimeError::new(
"ILO-R009",
format!("{op_name}: numeric key must be finite, got {n}"),
));
}
Ok(MapKey::Int(n.floor() as i64))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("{op_name}: key must be text or number, got {other:?}"),
)),
}
}
}
impl std::fmt::Display for MapKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MapKey::Text(s) => write!(f, "{s}"),
MapKey::Int(n) => write!(f, "{n}"),
}
}
}
impl Ord for MapKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(MapKey::Int(a), MapKey::Int(b)) => a.cmp(b),
(MapKey::Text(a), MapKey::Text(b)) => a.cmp(b),
(MapKey::Int(_), MapKey::Text(_)) => std::cmp::Ordering::Less,
(MapKey::Text(_), MapKey::Int(_)) => std::cmp::Ordering::Greater,
}
}
}
impl PartialOrd for MapKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub fn map_key_to_value(k: &MapKey) -> Value {
match k {
MapKey::Text(s) => Value::Text(Arc::new(s.clone())),
MapKey::Int(n) => Value::Number(*n as f64),
}
}
type StdinLinesInner =
Arc<Mutex<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>>>;
#[allow(clippy::type_complexity)]
pub struct StdinLinesHandle {
inner: StdinLinesInner,
}
impl std::fmt::Debug for StdinLinesHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<stdin-lines>")
}
}
impl Clone for StdinLinesHandle {
fn clone(&self) -> Self {
StdinLinesHandle {
inner: Arc::clone(&self.inner),
}
}
}
impl PartialEq for StdinLinesHandle {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
}
#[cfg(not(target_family = "wasm"))]
impl Default for StdinLinesHandle {
fn default() -> Self {
Self::new()
}
}
impl StdinLinesHandle {
#[cfg(not(target_family = "wasm"))]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(std::io::stdin());
let stdin_box: Box<
dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send,
> = Box::new(reader.lines());
StdinLinesHandle {
inner: Arc::new(Mutex::new(stdin_box)),
}
}
pub fn next_line(&self) -> Option<std::result::Result<String, std::io::Error>> {
self.inner
.lock()
.expect("StdinLinesHandle lock poisoned")
.next()
}
}
type HttpLinesInner =
Arc<Mutex<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>>>;
#[allow(clippy::type_complexity)]
pub struct HttpLinesHandle {
inner: HttpLinesInner,
}
impl std::fmt::Debug for HttpLinesHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<http-lines>")
}
}
impl Clone for HttpLinesHandle {
fn clone(&self) -> Self {
HttpLinesHandle {
inner: Arc::clone(&self.inner),
}
}
}
impl PartialEq for HttpLinesHandle {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
}
impl HttpLinesHandle {
pub fn from_lines(
it: Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>,
) -> Self {
HttpLinesHandle {
inner: Arc::new(Mutex::new(it)),
}
}
pub fn next_line(&self) -> Option<std::result::Result<String, std::io::Error>> {
self.inner
.lock()
.expect("HttpLinesHandle lock poisoned")
.next()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Number(f64),
Text(Arc<String>),
Bool(bool),
Nil,
List(Arc<Vec<Value>>),
Map(Arc<HashMap<MapKey, Value>>),
Record {
type_name: String,
fields: HashMap<String, Value>,
},
Ok(Box<Value>),
Err(Box<Value>),
World {
net: bool,
read: bool,
write: bool,
run: bool,
},
FnRef(String),
Closure {
fn_name: String,
captures: Vec<Value>,
},
Variant {
type_name: String,
tag: String,
payload: Option<Box<Value>>,
},
LazyStdinLines(StdinLinesHandle),
LazyHttpLines(HttpLinesHandle),
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Number(n) => {
if *n == (*n as i64) as f64 {
write!(f, "{}", *n as i64)
} else {
write!(f, "{}", n)
}
}
Value::Text(s) => write!(f, "{}", s),
Value::Bool(b) => write!(f, "{}", b),
Value::Nil => write!(f, "nil"),
Value::List(items) => {
write!(f, "[")?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", item)?;
}
write!(f, "]")
}
Value::Record { type_name, fields } => {
write!(f, "{} {{", type_name)?;
let mut keys: Vec<&String> = fields.keys().collect();
keys.sort();
for (i, k) in keys.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {}", k, fields[*k])?;
}
write!(f, "}}")
}
Value::Map(m) => {
write!(f, "{{")?;
let mut keys: Vec<&MapKey> = m.keys().collect();
keys.sort();
for (i, k) in keys.iter().enumerate() {
if i > 0 {
write!(f, "; ")?;
}
write!(f, "{}: {}", k, m[*k])?;
}
write!(f, "}}")
}
Value::Ok(v) => write!(f, "~{}", v),
Value::Err(v) => write!(f, "^{}", v),
Value::World {
net,
read,
write,
run,
} => {
write!(
f,
"World {{net: {net}, read: {read}, write: {write}, run: {run}}}"
)
}
Value::FnRef(name) => write!(f, "<fn:{}>", name),
Value::Closure { fn_name, captures } => {
write!(f, "<closure:{}[", fn_name)?;
for (i, c) in captures.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", c)?;
}
write!(f, "]>")
}
Value::Variant { tag, payload, .. } => match payload {
Some(p) => write!(f, "{tag}({p})"),
None => write!(f, "{tag}"),
},
Value::LazyStdinLines(_) => write!(f, "<stdin-lines>"),
Value::LazyHttpLines(_) => write!(f, "<http-lines>"),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("Runtime error: {message}")]
pub struct RuntimeError {
pub code: &'static str,
pub message: String,
pub span: Option<crate::ast::Span>,
pub call_stack: Vec<String>,
pub propagate_value: Option<Box<Value>>,
}
impl RuntimeError {
fn new(code: &'static str, msg: impl Into<String>) -> Self {
RuntimeError {
code,
message: msg.into(),
span: None,
call_stack: Vec::new(),
propagate_value: None,
}
}
}
type Result<T> = std::result::Result<T, RuntimeError>;
struct Env {
vars: Vec<(String, Value)>,
scope_marks: Vec<usize>,
functions: HashMap<String, Decl>,
sum_variants: HashMap<String, (String, bool)>,
call_stack: Vec<String>,
tool_provider: Option<std::sync::Arc<dyn crate::tools::ToolProvider>>,
#[cfg(feature = "tools")]
tokio_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
caps: Arc<Caps>,
defer_stack: Vec<(crate::ast::Expr, crate::ast::DeferKind)>,
}
impl Env {
fn new() -> Self {
Env {
vars: Vec::new(),
scope_marks: vec![0],
functions: HashMap::new(),
sum_variants: HashMap::new(),
call_stack: Vec::new(),
tool_provider: None,
#[cfg(feature = "tools")]
tokio_runtime: None,
caps: Arc::new(Caps::default()),
defer_stack: Vec::new(),
}
}
fn with_caps(caps: Arc<Caps>) -> Self {
Env {
vars: Vec::new(),
scope_marks: vec![0],
functions: HashMap::new(),
sum_variants: HashMap::new(),
call_stack: Vec::new(),
tool_provider: None,
#[cfg(feature = "tools")]
tokio_runtime: None,
caps,
defer_stack: Vec::new(),
}
}
fn with_tools(
provider: std::sync::Arc<dyn crate::tools::ToolProvider>,
#[cfg(feature = "tools")] runtime: std::sync::Arc<tokio::runtime::Runtime>,
) -> Self {
Env {
vars: Vec::new(),
scope_marks: vec![0],
functions: HashMap::new(),
sum_variants: HashMap::new(),
call_stack: Vec::new(),
tool_provider: Some(provider),
#[cfg(feature = "tools")]
tokio_runtime: Some(runtime),
caps: Arc::new(Caps::default()),
defer_stack: Vec::new(),
}
}
fn with_tools_and_caps(
provider: std::sync::Arc<dyn crate::tools::ToolProvider>,
#[cfg(feature = "tools")] runtime: std::sync::Arc<tokio::runtime::Runtime>,
caps: Arc<Caps>,
) -> Self {
Env {
vars: Vec::new(),
scope_marks: vec![0],
functions: HashMap::new(),
sum_variants: HashMap::new(),
call_stack: Vec::new(),
tool_provider: Some(provider),
#[cfg(feature = "tools")]
tokio_runtime: Some(runtime),
caps,
defer_stack: Vec::new(),
}
}
fn push_scope(&mut self) {
self.scope_marks.push(self.vars.len());
}
fn pop_scope(&mut self) {
let mark = self
.scope_marks
.pop()
.expect("unbalanced push_scope/pop_scope");
self.vars.truncate(mark);
}
fn set(&mut self, name: &str, value: Value) {
for entry in self.vars.iter_mut().rev() {
if entry.0 == name {
entry.1 = value;
return;
}
}
self.vars.push((name.to_string(), value));
}
fn take(&mut self, name: &str) -> Option<Value> {
for entry in self.vars.iter_mut().rev() {
if entry.0 == name {
return Some(std::mem::replace(&mut entry.1, Value::Nil));
}
}
None
}
fn define(&mut self, name: &str, value: Value) {
self.vars.push((name.to_string(), value));
}
fn get(&self, name: &str) -> Result<Value> {
for (k, v) in self.vars.iter().rev() {
if k == name {
return Ok(v.clone());
}
}
if self.functions.contains_key(name) {
return Ok(Value::FnRef(name.to_string()));
}
if let Some((type_name, has_payload)) = self.sum_variants.get(name) {
if *has_payload {
return Ok(Value::FnRef(name.to_string()));
} else {
return Ok(Value::Variant {
type_name: type_name.clone(),
tag: name.to_string(),
payload: None,
});
}
}
if Builtin::is_builtin(name) {
return Ok(Value::FnRef(name.to_string()));
}
if name == "nil" {
return Ok(Value::Nil);
}
Err(RuntimeError::new(
"ILO-R001",
format!("undefined variable: {}", name),
))
}
fn function(&self, name: &str) -> Result<Decl> {
self.functions
.get(name)
.cloned()
.ok_or_else(|| RuntimeError::new("ILO-R002", format!("undefined function: {}", name)))
}
}
enum BodyResult {
Value(Value),
Return(Value),
Break(Value),
Continue,
TailCall { callee: String, args: Vec<Value> },
}
pub fn run(program: &Program, func_name: Option<&str>, args: Vec<Value>) -> Result<Value> {
run_with_env(program, func_name, args, Env::new())
}
pub fn run_with_caps(
program: &Program,
func_name: Option<&str>,
args: Vec<Value>,
caps: Arc<Caps>,
) -> Result<Value> {
run_with_env(program, func_name, args, Env::with_caps(caps))
}
pub fn call_builtin_for_bridge(name: &str, args: Vec<Value>) -> Result<Value> {
let mut env = Env::new();
call_function(&mut env, name, args)
}
pub fn call_builtin_for_bridge_with_program(
name: &str,
args: Vec<Value>,
program: &Program,
) -> Result<Value> {
let mut env = Env::new();
for decl in &program.declarations {
match decl {
Decl::Function { name, .. } | Decl::Tool { name, .. } => {
env.functions.insert(name.clone(), decl.clone());
}
Decl::SumType { name, variants, .. } => {
for v in variants {
env.sum_variants
.insert(v.name.clone(), (name.clone(), v.payload.is_some()));
}
}
Decl::TypeDef { .. }
| Decl::Alias { .. }
| Decl::Use { .. }
| Decl::VersionPragma { .. }
| Decl::Error { .. } => {}
}
}
call_function(&mut env, name, args)
}
pub fn run_with_tools(
program: &Program,
func_name: Option<&str>,
args: Vec<Value>,
provider: std::sync::Arc<dyn crate::tools::ToolProvider>,
#[cfg(feature = "tools")] runtime: std::sync::Arc<tokio::runtime::Runtime>,
) -> Result<Value> {
let env = Env::with_tools(
provider,
#[cfg(feature = "tools")]
runtime,
);
run_with_env(program, func_name, args, env)
}
pub fn run_with_tools_and_caps(
program: &Program,
func_name: Option<&str>,
args: Vec<Value>,
provider: std::sync::Arc<dyn crate::tools::ToolProvider>,
#[cfg(feature = "tools")] runtime: std::sync::Arc<tokio::runtime::Runtime>,
caps: Arc<Caps>,
) -> Result<Value> {
let env = Env::with_tools_and_caps(
provider,
#[cfg(feature = "tools")]
runtime,
caps,
);
run_with_env(program, func_name, args, env)
}
fn run_with_env(
program: &Program,
func_name: Option<&str>,
args: Vec<Value>,
mut env: Env,
) -> Result<Value> {
for decl in &program.declarations {
match decl {
Decl::Function { name, .. } | Decl::Tool { name, .. } => {
env.functions.insert(name.clone(), decl.clone());
}
Decl::SumType { name, variants, .. } => {
for v in variants {
env.sum_variants
.insert(v.name.clone(), (name.clone(), v.payload.is_some()));
}
}
Decl::TypeDef { .. }
| Decl::Alias { .. }
| Decl::Use { .. }
| Decl::VersionPragma { .. }
| Decl::Error { .. } => {}
}
}
let target = match func_name {
Some(name) => name.to_string(),
None => {
program
.declarations
.iter()
.find_map(|d| match d {
Decl::Function { name, .. } => Some(name.clone()),
_ => None,
})
.ok_or_else(|| RuntimeError::new("ILO-R012", "no functions defined"))?
}
};
call_function(&mut env, &target, args)
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn box_muller_normal(mu: f64, sigma: f64) -> f64 {
crate::rng::normal(mu, sigma)
}
#[inline]
fn b64url_no_pad_encode(bytes: &[u8]) -> String {
const ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let n = bytes.len();
let cap = n.div_ceil(3) * 4;
let mut out = Vec::with_capacity(cap);
let mut chunks = bytes.chunks_exact(3);
for chunk in chunks.by_ref() {
let b0 = chunk[0];
let b1 = chunk[1];
let b2 = chunk[2];
out.push(ALPHA[(b0 >> 2) as usize]);
out.push(ALPHA[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize]);
out.push(ALPHA[(((b1 & 0b1111) << 2) | (b2 >> 6)) as usize]);
out.push(ALPHA[(b2 & 0b111111) as usize]);
}
let rem = chunks.remainder();
match rem.len() {
1 => {
let b0 = rem[0];
out.push(ALPHA[(b0 >> 2) as usize]);
out.push(ALPHA[((b0 & 0b11) << 4) as usize]);
}
2 => {
let b0 = rem[0];
let b1 = rem[1];
out.push(ALPHA[(b0 >> 2) as usize]);
out.push(ALPHA[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize]);
out.push(ALPHA[((b1 & 0b1111) << 2) as usize]);
}
_ => {}
}
debug_assert!(out.iter().all(|b| b.is_ascii()));
String::from_utf8(out).expect("base64url alphabet is ASCII-only")
}
#[inline(never)]
pub(crate) fn eval_rand_bytes(arg: &Value) -> Result<Value> {
let n_f = match arg {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rand-bytes requires a number, got {other:?}"),
));
}
};
if !n_f.is_finite() {
return Err(RuntimeError::new(
"ILO-R009",
format!("rand-bytes: n is not finite ({n_f})"),
));
}
if n_f < 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("rand-bytes: n must be non-negative, got {n_f}"),
));
}
const MAX_BYTES: f64 = 1024.0 * 1024.0;
if n_f > MAX_BYTES {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rand-bytes: n={n_f} exceeds 1 MiB cap; if you really need this much CSPRNG output, call rand-bytes in a loop"
),
));
}
let n = n_f as usize;
if n == 0 {
return Ok(Value::Text(Arc::new(String::new())));
}
let mut buf = vec![0u8; n];
if let Err(e) = getrandom::getrandom(&mut buf) {
return Err(RuntimeError::new(
"ILO-R009",
format!("rand-bytes: CSPRNG read failed: {e}"),
));
}
Ok(Value::Text(Arc::new(b64url_no_pad_encode(&buf))))
}
#[inline(never)]
pub(crate) fn ewm_compute(xs: &[f64], a: f64) -> Vec<f64> {
if xs.is_empty() {
return Vec::new();
}
let one_minus_a = 1.0 - a;
let mut out: Vec<f64> = Vec::with_capacity(xs.len());
let mut prev = xs[0];
out.push(prev);
for &x in &xs[1..] {
prev = a * x + one_minus_a * prev;
out.push(prev);
}
out
}
#[inline(never)]
pub(crate) fn rsum_compute(n: usize, xs: &[f64]) -> Vec<f64> {
let len = xs.len();
if n == 0 || n > len {
return Vec::new();
}
let out_len = len - n + 1;
let mut out: Vec<f64> = Vec::with_capacity(out_len);
let mut s: f64 = xs[..n].iter().sum();
out.push(s);
for i in n..len {
s += xs[i];
s -= xs[i - n];
out.push(s);
}
out
}
#[inline(never)]
pub(crate) fn ravg_compute(n: usize, xs: &[f64]) -> Vec<f64> {
let sums = rsum_compute(n, xs);
if sums.is_empty() {
return sums;
}
let denom = n as f64;
sums.into_iter().map(|s| s / denom).collect()
}
#[inline(never)]
pub(crate) fn rmin_compute(n: usize, xs: &[f64]) -> Vec<f64> {
let len = xs.len();
if n == 0 || n > len {
return Vec::new();
}
let out_len = len - n + 1;
let mut out: Vec<f64> = Vec::with_capacity(out_len);
let mut dq: std::collections::VecDeque<usize> = std::collections::VecDeque::with_capacity(n);
for i in 0..len {
while let Some(&front) = dq.front() {
if front + n <= i {
dq.pop_front();
} else {
break;
}
}
while let Some(&back) = dq.back() {
let cmp = xs[back]
.partial_cmp(&xs[i])
.unwrap_or(std::cmp::Ordering::Greater);
if cmp != std::cmp::Ordering::Less {
dq.pop_back();
} else {
break;
}
}
dq.push_back(i);
if i + 1 >= n {
let &front = dq.front().expect("deque non-empty after push");
out.push(xs[front]);
}
}
out
}
pub(crate) fn dirname_posix(p: &str) -> String {
if p.is_empty() {
return String::new();
}
if p == "/" {
return "/".to_string();
}
let trimmed = if p.ends_with('/') && p.len() > 1 {
&p[..p.len() - 1]
} else {
p
};
match trimmed.rfind('/') {
None => String::new(),
Some(0) => "/".to_string(),
Some(i) => trimmed[..i].to_string(),
}
}
pub(crate) fn basename_posix(p: &str) -> String {
if p.is_empty() {
return String::new();
}
if p == "/" {
return "/".to_string();
}
let trimmed = if p.ends_with('/') && p.len() > 1 {
&p[..p.len() - 1]
} else {
p
};
match trimmed.rfind('/') {
None => trimmed.to_string(),
Some(i) => trimmed[i + 1..].to_string(),
}
}
pub(crate) fn pathjoin_posix(parts: &[&str]) -> String {
let mut out = String::new();
let mut first = true;
for (idx, seg) in parts.iter().enumerate() {
let s = if idx == 0 {
seg.trim_end_matches('/')
} else {
seg.trim_start_matches('/').trim_end_matches('/')
};
let s = if idx == 0 && !seg.is_empty() && s.is_empty() && seg.starts_with('/') {
"/"
} else {
s
};
if s.is_empty() {
continue;
}
if first {
out.push_str(s);
first = false;
} else {
if !out.ends_with('/') {
out.push('/');
}
out.push_str(s);
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FmtSpec {
Bare,
Precision(usize),
WidthRight(usize),
IntWidth(usize),
WidthLeft(usize),
}
pub(crate) fn parse_fmt_spec(spec: &str) -> Option<FmtSpec> {
let inner = spec.strip_prefix('{')?.strip_suffix('}')?;
if inner.is_empty() {
return Some(FmtSpec::Bare);
}
if let Some(rest) = inner.strip_prefix('.')
&& let Some(digits) = rest.strip_suffix('f')
&& !digits.is_empty()
&& digits.chars().all(|c| c.is_ascii_digit())
{
return digits.parse::<usize>().ok().map(FmtSpec::Precision);
}
let body = inner.strip_prefix(':')?;
if let Some(rest) = body.strip_prefix('.')
&& let Some(digits) = rest.strip_suffix('f')
&& !digits.is_empty()
&& digits.chars().all(|c| c.is_ascii_digit())
{
return digits.parse::<usize>().ok().map(FmtSpec::Precision);
}
let is_plain_width = |s: &str| {
!s.is_empty()
&& s.chars().all(|c| c.is_ascii_digit())
&& !(s.len() > 1 && s.starts_with('0'))
};
if let Some(digits) = body.strip_prefix('<')
&& is_plain_width(digits)
{
return digits.parse::<usize>().ok().map(FmtSpec::WidthLeft);
}
if let Some(digits) = body.strip_suffix('d')
&& is_plain_width(digits)
{
return digits.parse::<usize>().ok().map(FmtSpec::IntWidth);
}
if is_plain_width(body) {
return body.parse::<usize>().ok().map(FmtSpec::WidthRight);
}
None
}
pub(crate) fn apply_fmt_spec(spec: &FmtSpec, arg: &Value) -> std::result::Result<String, String> {
match spec {
FmtSpec::Bare => Ok(format!("{}", arg)),
FmtSpec::Precision(n) => match arg {
Value::Number(x) => Ok(format!("{:.*}", *n, x)),
other => Err(format!(
"decimal-precision spec requires a number, got {:?}",
other
)),
},
FmtSpec::IntWidth(w) => match arg {
Value::Number(x) => {
let n = *x as i64;
let s = n.to_string();
Ok(pad_left(&s, *w))
}
other => Err(format!("`d` width spec requires a number, got {:?}", other)),
},
FmtSpec::WidthRight(w) => {
let s = format!("{}", arg);
Ok(pad_left(&s, *w))
}
FmtSpec::WidthLeft(w) => {
let s = format!("{}", arg);
Ok(pad_right(&s, *w))
}
}
}
fn pad_left(s: &str, width: usize) -> String {
let n = s.chars().count();
if n >= width {
s.to_string()
} else {
let pad = " ".repeat(width - n);
format!("{pad}{s}")
}
}
fn pad_right(s: &str, width: usize) -> String {
let n = s.chars().count();
if n >= width {
s.to_string()
} else {
let pad = " ".repeat(width - n);
format!("{s}{pad}")
}
}
pub(crate) fn dur_parse(s: &str) -> std::result::Result<f64, String> {
let s = s.trim();
if s.is_empty() {
return Err("dur-parse: empty input".to_string());
}
let mut total = 0.0_f64;
let mut found_any = false;
let mut sign = 1.0_f64;
let mut rest = s;
while !rest.is_empty() {
let trimmed = rest.trim_start_matches(|c: char| c.is_ascii_whitespace() || c == ',');
if trimmed.is_empty() {
break;
}
rest = trimmed;
if let Some(r) = rest.strip_prefix('-') {
sign = -1.0_f64;
rest = r;
} else if let Some(r) = rest.strip_prefix('+') {
sign = 1.0_f64;
rest = r;
}
let num_end = rest
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(rest.len());
if num_end == 0 {
let skip = rest.chars().next().map(|c| c.len_utf8()).unwrap_or(1);
rest = &rest[skip..];
continue;
}
let num_str = &rest[..num_end];
let num: f64 = match num_str.parse() {
Ok(n) => n,
Err(_) => {
rest = &rest[num_end..];
continue;
}
};
rest = &rest[num_end..];
rest = rest.trim_start_matches(|c: char| c.is_ascii_whitespace());
let unit_end = rest
.find(|c: char| !c.is_ascii_alphabetic())
.unwrap_or(rest.len());
if unit_end == 0 {
continue;
}
let unit = &rest[..unit_end];
rest = &rest[unit_end..];
let multiplier: f64 = match unit.to_ascii_lowercase().as_str() {
"w" | "week" | "weeks" => 604_800.0,
"d" | "day" | "days" => 86_400.0,
"h" | "hr" | "hrs" | "hour" | "hours" => 3_600.0,
"m" | "min" | "mins" | "minute" | "minutes" => 60.0,
"s" | "sec" | "secs" | "second" | "seconds" => 1.0,
_ => {
continue;
}
};
total += sign * num * multiplier;
found_any = true;
}
if !found_any {
return Err(format!("dur-parse: no recognised unit in {:?}", s));
}
Ok(total)
}
pub(crate) fn dur_fmt(secs: f64) -> String {
if !secs.is_finite() {
return format!("{secs}");
}
let negative = secs < 0.0;
let total_secs = secs.abs();
let whole = total_secs.trunc() as u64;
let frac = total_secs - whole as f64;
let weeks = whole / 604_800;
let rem = whole % 604_800;
let days = rem / 86_400;
let rem = rem % 86_400;
let hours = rem / 3_600;
let rem = rem % 3_600;
let minutes = rem / 60;
let seconds = rem % 60;
let mut parts: Vec<String> = Vec::with_capacity(5);
if weeks > 0 {
parts.push(if weeks == 1 {
"1 week".to_string()
} else {
format!("{weeks} weeks")
});
}
if days > 0 {
parts.push(if days == 1 {
"1 day".to_string()
} else {
format!("{days} days")
});
}
if hours > 0 {
parts.push(if hours == 1 {
"1h".to_string()
} else {
format!("{hours}h")
});
}
if minutes > 0 {
parts.push(format!("{minutes}m"));
}
let has_frac = frac > 1e-9;
if seconds > 0 || has_frac {
if has_frac {
let total_s = seconds as f64 + frac;
let formatted = format!("{:.3}", total_s);
let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
parts.push(format!("{formatted}s"));
} else {
parts.push(format!("{seconds}s"));
}
}
if parts.is_empty() {
parts.push("0s".to_string());
}
let joined = parts.join(" ");
if negative {
format!("-{joined}")
} else {
joined
}
}
fn walk_collect(root: &std::path::Path) -> std::result::Result<Vec<String>, String> {
let mut out: Vec<String> = Vec::new();
let root_rd = std::fs::read_dir(root).map_err(|e| e.to_string())?;
let mut stack: Vec<(std::path::PathBuf, std::fs::ReadDir)> = Vec::new();
stack.push((root.to_path_buf(), root_rd));
while let Some((_cur, rd)) = stack.pop() {
for entry in rd {
let ent = match entry {
Ok(e) => e,
Err(_) => continue,
};
let path = ent.path();
let rel = path.strip_prefix(root).unwrap_or(&path);
let rel_str = rel
.to_string_lossy()
.into_owned()
.replace(std::path::MAIN_SEPARATOR, "/");
out.push(rel_str);
let is_dir = match ent.file_type() {
Ok(ft) => ft.is_dir(),
Err(_) => false,
};
if is_dir {
if let Ok(child_rd) = std::fs::read_dir(&path) {
stack.push((path, child_rd));
}
}
}
}
out.sort();
Ok(out)
}
fn glob_match(pat: &str, text: &str) -> bool {
glob_match_bytes(pat.as_bytes(), text.as_bytes())
}
fn glob_match_bytes(pat: &[u8], text: &[u8]) -> bool {
let (mut pi, mut ti) = (0usize, 0usize);
while pi < pat.len() {
match pat[pi] {
b'*' => {
let is_double = pi + 1 < pat.len() && pat[pi + 1] == b'*';
let left_boundary = pi == 0 || pat[pi - 1] == b'/';
let right_boundary_at = pi + 2;
let right_boundary =
is_double && (right_boundary_at == pat.len() || pat[right_boundary_at] == b'/');
if is_double && left_boundary && right_boundary {
let rest_start = if right_boundary_at < pat.len() {
right_boundary_at + 1
} else {
right_boundary_at
};
let rest = &pat[rest_start..];
if rest.is_empty() {
return true;
}
let mut probe = ti;
loop {
if glob_match_bytes(rest, &text[probe..]) {
return true;
}
if probe == text.len() {
return false;
}
probe += 1;
}
}
let rest = &pat[pi + 1..];
let mut probe = ti;
loop {
if glob_match_bytes(rest, &text[probe..]) {
return true;
}
if probe == text.len() || text[probe] == b'/' {
return false;
}
probe += 1;
}
}
b'?' => {
if ti >= text.len() || text[ti] == b'/' {
return false;
}
pi += 1;
ti += 1;
}
b'[' => {
if ti >= text.len() || text[ti] == b'/' {
return false;
}
let mut j = pi + 1;
let mut negate = false;
if j < pat.len() && (pat[j] == b'!' || pat[j] == b'^') {
negate = true;
j += 1;
}
let mut matched = false;
let mut found_close = false;
while j < pat.len() {
if pat[j] == b']' && j > pi + 1 + (negate as usize) {
found_close = true;
break;
}
if j + 2 < pat.len() && pat[j + 1] == b'-' && pat[j + 2] != b']' {
if text[ti] >= pat[j] && text[ti] <= pat[j + 2] {
matched = true;
}
j += 3;
} else {
if text[ti] == pat[j] {
matched = true;
}
j += 1;
}
}
if !found_close {
if text[ti] != b'[' {
return false;
}
pi += 1;
ti += 1;
continue;
}
if matched == negate {
return false;
}
pi = j + 1;
ti += 1;
}
c => {
if ti >= text.len() || text[ti] != c {
return false;
}
pi += 1;
ti += 1;
}
}
}
ti == text.len()
}
fn parse_format(fmt: &str, content: &str) -> std::result::Result<Value, String> {
match fmt {
"csv" | "tsv" => {
let sep = if fmt == "tsv" { '\t' } else { ',' };
let rows: Vec<Value> = parse_csv_content(content, sep)
.into_iter()
.map(|row| {
let fields: Vec<Value> =
row.into_iter().map(|s| Value::Text(Arc::new(s))).collect();
Value::List(Arc::new(fields))
})
.collect();
Ok(Value::List(Arc::new(rows)))
}
"json" => serde_json::from_str::<serde_json::Value>(content)
.map(serde_json_to_value)
.map_err(|e| e.to_string()),
_ => Ok(Value::Text(Arc::new(content.to_string()))),
}
}
fn parse_csv_content(content: &str, sep: char) -> Vec<Vec<String>> {
let mut rows: Vec<Vec<String>> = Vec::new();
let mut row: Vec<String> = Vec::new();
let mut field = String::new();
let mut in_quotes = false;
let mut chars = content.chars().peekable();
while let Some(c) = chars.next() {
if in_quotes {
if c == '"' {
if chars.peek() == Some(&'"') {
chars.next();
field.push('"');
} else {
in_quotes = false;
}
} else {
field.push(c);
}
} else if c == '"' {
in_quotes = true;
} else if c == sep {
row.push(std::mem::take(&mut field));
} else if c == '\n' {
row.push(std::mem::take(&mut field));
rows.push(std::mem::take(&mut row));
} else if c == '\r' {
if chars.peek() == Some(&'\n') {
chars.next();
}
row.push(std::mem::take(&mut field));
rows.push(std::mem::take(&mut row));
} else {
field.push(c);
}
}
if !field.is_empty() || !row.is_empty() || in_quotes {
row.push(field);
rows.push(row);
}
rows
}
#[inline(never)]
fn matvec_run(xm_val: &Value, ys_val: &Value) -> Result<Value> {
let xm = matrix_from_value(xm_val, "matvec")?;
let ys = vec_from_value(ys_val, "matvec")?;
let n_rows = xm.len();
if n_rows == 0 {
return Err(RuntimeError::new(
"ILO-R009",
"matvec: empty matrix".to_string(),
));
}
let n_cols = xm[0].len();
if n_cols == 0 {
return Err(RuntimeError::new(
"ILO-R009",
"matvec: matrix has zero columns".to_string(),
));
}
for row in &xm {
if row.len() != n_cols {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"matvec: ragged rows (expected {n_cols} cols, got {})",
row.len()
),
));
}
}
if ys.len() != n_cols {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"matvec: dim mismatch (matrix has {n_cols} cols, ys has {})",
ys.len()
),
));
}
let mut out: Vec<Value> = Vec::with_capacity(n_rows);
for row in &xm {
let mut s = 0.0_f64;
for (k, &v) in row.iter().enumerate() {
s += v * ys[k];
}
out.push(Value::Number(s));
}
Ok(Value::List(Arc::new(out)))
}
fn matrix_from_value(v: &Value, name: &str) -> Result<Vec<Vec<f64>>> {
let rows = match v {
Value::List(rs) => rs,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{}: expected a list of lists, got {:?}", name, other),
));
}
};
let mut mat: Vec<Vec<f64>> = Vec::with_capacity(rows.len());
for row in rows.iter() {
let cells = match row {
Value::List(cs) => cs,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{}: each row must be a list, got {:?}", name, other),
));
}
};
let mut r: Vec<f64> = Vec::with_capacity(cells.len());
for c in cells.iter() {
match c {
Value::Number(n) => r.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{}: matrix cells must be numbers, got {:?}", name, other),
));
}
}
}
mat.push(r);
}
Ok(mat)
}
fn vec_from_value(v: &Value, name: &str) -> Result<Vec<f64>> {
let items = match v {
Value::List(xs) => xs,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{}: expected a list of numbers, got {:?}", name, other),
));
}
};
let mut out: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => out.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{}: vector items must be numbers, got {:?}", name, other),
));
}
}
}
Ok(out)
}
fn fmt_csv_field(v: &Value, sep: char) -> String {
let raw = match v {
Value::Text(s) => (**s).clone(),
Value::Number(n) => {
if *n == (*n as i64) as f64 {
format!("{}", *n as i64)
} else {
format!("{n}")
}
}
Value::Bool(b) => format!("{b}"),
Value::Nil => String::new(),
other => format!("{other}"),
};
if raw.contains(sep) || raw.contains('"') || raw.contains('\n') {
format!("\"{}\"", raw.replace('"', "\"\""))
} else {
raw
}
}
pub(crate) fn write_csv_tsv(rows: &[Value], sep: char) -> Result<String> {
let mut out = String::new();
let first = match rows.first() {
Some(r) => r,
None => return Ok(out),
};
let (header, use_keys): (Option<Vec<String>>, bool) = match first {
Value::List(_) => (None, false),
Value::Record { fields, .. } => {
let mut keys: Vec<String> = fields.keys().cloned().collect();
keys.sort();
(Some(keys), true)
}
Value::Map(m) => {
let mut keys: Vec<MapKey> = m.keys().cloned().collect();
keys.sort();
(
Some(keys.iter().map(|k| k.to_display_string()).collect()),
true,
)
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"wr: each row must be a list, record, or map, got {:?}",
other
),
));
}
};
if let Some(ref keys) = header {
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(sep);
}
out.push_str(&fmt_csv_field(&Value::Text(Arc::new(k.clone())), sep));
}
out.push('\n');
}
for row in rows {
match (row, use_keys, header.as_ref()) {
(Value::List(fields), false, _) => {
for (i, f) in fields.iter().enumerate() {
if i > 0 {
out.push(sep);
}
out.push_str(&fmt_csv_field(f, sep));
}
out.push('\n');
}
(Value::Record { fields, .. }, true, Some(keys)) => {
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(sep);
}
let v = fields.get(k).cloned().unwrap_or(Value::Nil);
out.push_str(&fmt_csv_field(&v, sep));
}
out.push('\n');
}
(Value::Map(m), true, Some(keys)) => {
let str_view: HashMap<String, &Value> =
m.iter().map(|(k, v)| (k.to_display_string(), v)).collect();
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(sep);
}
let v = str_view
.get(k.as_str())
.copied()
.cloned()
.unwrap_or(Value::Nil);
out.push_str(&fmt_csv_field(&v, sep));
}
out.push('\n');
}
(other, _, _) => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"wr: row shape mismatch (expected {} rows), got {:?}",
if use_keys { "record/map" } else { "list" },
other
),
));
}
}
}
Ok(out)
}
#[allow(clippy::needless_range_loop)]
pub(crate) fn lu_decompose(mut a: Vec<Vec<f64>>) -> (Vec<Vec<f64>>, Vec<usize>, f64, bool) {
let n = a.len();
let mut piv: Vec<usize> = (0..n).collect();
let mut det_sign = 1.0_f64;
let mut singular = false;
for k in 0..n {
let mut max_val = a[k][k].abs();
let mut max_row = k;
for i in (k + 1)..n {
let v = a[i][k].abs();
if v > max_val {
max_val = v;
max_row = i;
}
}
if max_val < 1e-12 {
singular = true;
continue;
}
if max_row != k {
a.swap(k, max_row);
piv.swap(k, max_row);
det_sign = -det_sign;
}
let pivot = a[k][k];
for i in (k + 1)..n {
a[i][k] /= pivot;
let factor = a[i][k];
for j in (k + 1)..n {
a[i][j] -= factor * a[k][j];
}
}
}
let mut det = det_sign;
for (i, row) in a.iter().enumerate().take(n) {
det *= row[i];
}
if singular {
det = 0.0;
}
(a, piv, det, singular)
}
pub(crate) fn lu_solve(lu: &[Vec<f64>], piv: &[usize], b: &[f64]) -> Vec<f64> {
let n = lu.len();
let mut x: Vec<f64> = (0..n).map(|i| b[piv[i]]).collect();
for i in 0..n {
for j in 0..i {
let lij = lu[i][j];
x[i] -= lij * x[j];
}
}
for i in (0..n).rev() {
for j in (i + 1)..n {
let uij = lu[i][j];
x[i] -= uij * x[j];
}
x[i] /= lu[i][i];
}
x
}
#[inline(never)]
fn lstsq_run(xm_val: &Value, ys_val: &Value) -> Result<Value> {
let xm = matrix_from_value(xm_val, "lstsq")?;
let ys = vec_from_value(ys_val, "lstsq")?;
let n_rows = xm.len();
if n_rows == 0 {
return Err(RuntimeError::new(
"ILO-R009",
"lstsq: empty design matrix".to_string(),
));
}
let n_cols = xm[0].len();
if n_cols == 0 {
return Err(RuntimeError::new(
"ILO-R009",
"lstsq: design matrix has zero columns".to_string(),
));
}
for row in &xm {
if row.len() != n_cols {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"lstsq: ragged design matrix (expected {n_cols} cols, got {})",
row.len()
),
));
}
}
if ys.len() != n_rows {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"lstsq: ys length {} must match design matrix row count {n_rows}",
ys.len()
),
));
}
if n_cols > n_rows {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"lstsq: underdetermined system ({n_cols} columns > {n_rows} rows); normal-equation OLS requires rows >= columns"
),
));
}
let mut xt: Vec<Vec<f64>> = vec![vec![0.0; n_rows]; n_cols];
for (i, row) in xm.iter().enumerate() {
for (j, &v) in row.iter().enumerate() {
xt[j][i] = v;
}
}
let mut xtx: Vec<Vec<f64>> = vec![vec![0.0; n_cols]; n_cols];
for i in 0..n_cols {
for j in 0..n_cols {
let mut s = 0.0;
for k in 0..n_rows {
s += xt[i][k] * xm[k][j];
}
xtx[i][j] = s;
}
}
let mut xty: Vec<f64> = vec![0.0; n_cols];
for i in 0..n_cols {
let mut s = 0.0;
for k in 0..n_rows {
s += xt[i][k] * ys[k];
}
xty[i] = s;
}
let (lu, piv, _det, singular) = lu_decompose(xtx);
if singular {
return Err(RuntimeError::new(
"ILO-R009",
"lstsq: normal-equation matrix XᵀX is singular (rank-deficient design)".to_string(),
));
}
let x = lu_solve(&lu, &piv, &xty);
Ok(Value::List(Arc::new(
x.into_iter().map(Value::Number).collect(),
)))
}
#[inline(never)]
fn urlenc_impl(arg: &Value) -> Result<Value> {
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
const UNRESERVED_PUNCT: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~');
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("urlenc requires text, got {:?}", other),
));
}
};
let encoded: String = utf8_percent_encode(s.as_str(), UNRESERVED_PUNCT).collect();
Ok(Value::Text(Arc::new(encoded)))
}
#[inline(never)]
fn urldec_impl(arg: &Value) -> Result<Value> {
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("urldec requires text, got {:?}", other),
));
}
};
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len()
|| !bytes[i + 1].is_ascii_hexdigit()
|| !bytes[i + 2].is_ascii_hexdigit()
{
return Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"urldec: invalid percent escape at byte {}",
i
))))));
}
i += 3;
} else {
i += 1;
}
}
match percent_encoding::percent_decode_str(s.as_str()).decode_utf8() {
Ok(cow) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(cow.into_owned()))))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"urldec: invalid UTF-8 in decoded bytes: {}",
e
)))))),
}
}
#[inline(never)]
fn idxof_impl(s_arg: &Value, sub_arg: &Value) -> Result<Value> {
let s = match s_arg {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("idxof: first arg must be text, got {:?}", other),
));
}
};
let sub = match sub_arg {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("idxof: second arg must be text, got {:?}", other),
));
}
};
if sub.is_empty() {
return Ok(Value::Number(0.0));
}
match s.find(sub) {
None => Ok(Value::Nil),
Some(byte_offset) => {
let char_idx = s[..byte_offset].chars().count();
Ok(Value::Number(char_idx as f64))
}
}
}
#[inline(never)]
fn b64u_impl(arg: &Value) -> Result<Value> {
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("b64u requires text, got {:?}", other),
));
}
};
Ok(Value::Text(Arc::new(URL_SAFE_NO_PAD.encode(s.as_bytes()))))
}
#[inline(never)]
fn b64u_dec_impl(arg: &Value) -> Result<Value> {
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("b64u-dec requires text, got {:?}", other),
));
}
};
match URL_SAFE_NO_PAD.decode(s.as_bytes()) {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"b64u-dec: invalid base64url input: {}",
e
)))))),
Ok(bytes) => match String::from_utf8(bytes) {
Ok(text) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(text))))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"b64u-dec: decoded bytes are not valid UTF-8: {}",
e
)))))),
},
}
}
#[inline(never)]
fn sha256_impl(arg: &Value) -> Result<Value> {
use sha2::{Digest, Sha256};
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("sha256 requires text, got {:?}", other),
));
}
};
let mut h = Sha256::new();
h.update(s.as_bytes());
let digest = h.finalize();
Ok(Value::Text(Arc::new(hex::encode(digest))))
}
fn hex_decode_arg(arg: &Value, caller: &str) -> Result<Vec<u8>> {
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{caller} requires text, got {:?}", other),
));
}
};
if s.len() % 2 != 0 {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"{caller}: hex input must have even length, got {} chars",
s.len()
),
));
}
hex::decode(s.as_ref())
.map_err(|e| RuntimeError::new("ILO-R009", format!("{caller}: invalid hex input: {e}")))
}
#[inline(never)]
fn sha256_hex_impl(arg: &Value) -> Result<Value> {
use sha2::{Digest, Sha256};
let bytes = hex_decode_arg(arg, "sha256-hex")?;
let mut h = Sha256::new();
h.update(&bytes);
Ok(Value::Text(Arc::new(hex::encode(h.finalize()))))
}
#[inline(never)]
fn sha256d_impl(arg: &Value) -> Result<Value> {
use sha2::{Digest, Sha256};
let bytes = hex_decode_arg(arg, "sha256d")?;
let inner = Sha256::digest(&bytes);
let outer = Sha256::digest(inner);
Ok(Value::Text(Arc::new(hex::encode(outer))))
}
#[inline(never)]
fn hmac_sha256_impl(key_arg: &Value, msg_arg: &Value) -> Result<Value> {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let key = match key_arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("hmac-sha256: key must be text, got {:?}", other),
));
}
};
let msg = match msg_arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("hmac-sha256: msg must be text, got {:?}", other),
));
}
};
let mut mac =
<Hmac<Sha256>>::new_from_slice(key.as_bytes()).expect("HMAC-SHA256 accepts any key length");
mac.update(msg.as_bytes());
let tag = mac.finalize().into_bytes();
Ok(Value::Text(Arc::new(hex::encode(tag))))
}
#[inline(never)]
fn tokcount_impl(arg: &Value) -> Result<Value> {
match arg {
Value::Text(s) => {
let bytes = s.len() as f64;
let count = (bytes / 3.4_f64).ceil();
Ok(Value::Number(count))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("tokcount requires text, got {:?}", other),
)),
}
}
#[inline(never)]
fn b64_impl(arg: &Value) -> Result<Value> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("b64 requires text, got {:?}", other),
));
}
};
Ok(Value::Text(Arc::new(STANDARD.encode(s.as_bytes()))))
}
#[inline(never)]
fn b64_dec_impl(arg: &Value) -> Result<Value> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("b64-dec requires text, got {:?}", other),
));
}
};
match STANDARD.decode(s.as_bytes()) {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"b64-dec: invalid base64 input: {}",
e
)))))),
Ok(bytes) => match String::from_utf8(bytes) {
Ok(text) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(text))))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"b64-dec: decoded bytes are not valid UTF-8: {}",
e
)))))),
},
}
}
#[inline(never)]
fn hex_impl(arg: &Value) -> Result<Value> {
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("hex requires text, got {:?}", other),
));
}
};
Ok(Value::Text(Arc::new(hex::encode(s.as_bytes()))))
}
#[inline(never)]
fn ct_eq_impl(a_arg: &Value, b_arg: &Value) -> Result<Value> {
use subtle::ConstantTimeEq;
let a = match a_arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ct-eq: first arg must be text, got {:?}", other),
));
}
};
let b = match b_arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ct-eq: second arg must be text, got {:?}", other),
));
}
};
if a.len() != b.len() {
return Ok(Value::Bool(false));
}
let eq: bool = a.as_bytes().ct_eq(b.as_bytes()).into();
Ok(Value::Bool(eq))
}
#[inline(never)]
fn hex_rev_impl(arg: &Value) -> Result<Value> {
let s = match arg {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("hex-rev requires text, got {:?}", other),
));
}
};
if s.len() % 2 != 0 {
return Err(RuntimeError::new(
"ILO-T013",
format!(
"hex-rev: input length {} is odd — hex strings must encode whole bytes (2 chars \
per byte); hint: pad to even length first (e.g. prepend \"0\")",
s.len()
),
));
}
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = s.len();
while i >= 2 {
i -= 2;
out.push(bytes[i] as char);
out.push(bytes[i + 1] as char);
}
Ok(Value::Text(Arc::new(out)))
}
#[inline(never)]
fn add_mo_impl(epoch_arg: &Value, months_arg: &Value) -> Result<Value> {
use chrono::{Datelike, NaiveDate, TimeZone, Utc};
let epoch = match epoch_arg {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"add-mo: first arg must be a number (epoch), got {:?}",
other
),
));
}
};
let months = match months_arg {
Value::Number(n) => *n as i32,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"add-mo: second arg must be a number (months), got {:?}",
other
),
));
}
};
let secs = epoch as i64;
let dt = match Utc.timestamp_opt(secs, 0).single() {
Some(d) => d,
None => {
return Err(RuntimeError::new(
"ILO-R009",
format!("add-mo: epoch out of range: {epoch}"),
));
}
};
let date = dt.date_naive();
fn add_months_snap(date: NaiveDate, months: i32) -> Option<NaiveDate> {
let total: i64 = (date.year() as i64) * 12 + (date.month() as i64 - 1) + (months as i64);
let y_i64 = total.div_euclid(12);
let m = (total.rem_euclid(12) + 1) as u32;
let y: i32 = i32::try_from(y_i64).ok()?;
let max_day = {
let next = if m == 12 {
NaiveDate::from_ymd_opt(y.checked_add(1)?, 1, 1)
} else {
NaiveDate::from_ymd_opt(y, m + 1, 1)
};
(next? - NaiveDate::from_ymd_opt(y, m, 1)?).num_days() as u32
};
NaiveDate::from_ymd_opt(y, m, date.day().min(max_day))
}
match add_months_snap(date, months) {
Some(d) => {
let ts = d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
Ok(Value::Number(ts as f64))
}
None => Err(RuntimeError::new(
"ILO-R009",
"add-mo: result out of calendar range".to_string(),
)),
}
}
#[inline(never)]
fn last_dom_impl(arg: &Value) -> Result<Value> {
use chrono::{Datelike, NaiveDate, TimeZone, Utc};
let epoch = match arg {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("last-dom: arg must be a number (epoch), got {:?}", other),
));
}
};
let secs = epoch as i64;
let dt = match Utc.timestamp_opt(secs, 0).single() {
Some(d) => d,
None => {
return Err(RuntimeError::new(
"ILO-R009",
format!("last-dom: epoch out of range: {epoch}"),
));
}
};
let date = dt.date_naive();
let y = date.year();
let m = date.month();
let first_next = if m == 12 {
NaiveDate::from_ymd_opt(y + 1, 1, 1)
} else {
NaiveDate::from_ymd_opt(y, m + 1, 1)
};
match first_next {
Some(next) => {
let last = next.pred_opt().unwrap();
let ts = last.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
Ok(Value::Number(ts as f64))
}
None => Err(RuntimeError::new(
"ILO-R009",
"last-dom: month arithmetic out of range".to_string(),
)),
}
}
#[inline(never)]
fn next_business_day_impl(arg: &Value) -> Result<Value> {
use chrono::{Datelike, Duration, NaiveDate, TimeZone, Utc, Weekday};
let epoch = match arg {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"next-business-day: arg must be a number (epoch), got {:?}",
other
),
));
}
};
let secs = epoch as i64;
let dt = match Utc.timestamp_opt(secs, 0).single() {
Some(d) => d,
None => {
return Err(RuntimeError::new(
"ILO-R009",
format!("next-business-day: epoch out of range: {epoch}"),
));
}
};
let date: NaiveDate = dt.date_naive();
let days_ahead: i64 = match date.weekday() {
Weekday::Fri => 3,
Weekday::Sat => 2,
_ => 1,
};
let next = date + Duration::days(days_ahead);
let ts = next.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
Ok(Value::Number(ts as f64))
}
#[inline(never)]
fn day_of_week_impl(arg: &Value) -> Result<Value> {
use chrono::{Datelike, TimeZone, Utc, Weekday};
let epoch = match arg {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("day-of-week: arg must be a number (epoch), got {:?}", other),
));
}
};
let secs = epoch as i64;
let dt = match Utc.timestamp_opt(secs, 0).single() {
Some(d) => d,
None => {
return Err(RuntimeError::new(
"ILO-R009",
format!("day-of-week: epoch out of range: {epoch}"),
));
}
};
let dow: u32 = match dt.date_naive().weekday() {
Weekday::Sun => 0,
Weekday::Mon => 1,
Weekday::Tue => 2,
Weekday::Wed => 3,
Weekday::Thu => 4,
Weekday::Fri => 5,
Weekday::Sat => 6,
};
Ok(Value::Number(dow as f64))
}
#[inline(never)]
fn run_bitwise(b: crate::builtins::Builtin, args: &[Value]) -> Result<Value> {
use crate::builtins::Builtin;
fn to_u32(v: &Value, pos: &str, name: &str) -> Result<u32> {
match v {
Value::Number(n) => {
if !n.is_finite() {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: {pos} must be a finite number, got {n}"),
));
}
Ok((*n as i64) as u32)
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("{name}: {pos} must be a number, got {other:?}"),
)),
}
}
let name = b.name();
let result: u32 = match b {
Builtin::Band => {
to_u32(&args[0], "first arg", name)? & to_u32(&args[1], "second arg", name)?
}
Builtin::Bor => {
to_u32(&args[0], "first arg", name)? | to_u32(&args[1], "second arg", name)?
}
Builtin::Bxor => {
to_u32(&args[0], "first arg", name)? ^ to_u32(&args[1], "second arg", name)?
}
Builtin::Bnot => !to_u32(&args[0], "arg", name)?,
Builtin::Bshl => {
let x = to_u32(&args[0], "first arg", name)?;
let n = to_u32(&args[1], "second arg", name)? % 32;
x << n
}
Builtin::Bshr => {
let x = to_u32(&args[0], "first arg", name)?;
let n = to_u32(&args[1], "second arg", name)? % 32;
x >> n
}
Builtin::Brot => {
let x = to_u32(&args[0], "first arg", name)?;
let n = to_u32(&args[1], "second arg", name)? % 32;
x.rotate_left(n)
}
_ => unreachable!(),
};
Ok(Value::Number(result as f64))
}
#[inline(never)]
fn run_bitwise_64(b: crate::builtins::Builtin, args: &[Value]) -> Result<Value> {
use crate::builtins::Builtin;
fn to_u64(v: &Value, pos: &str, name: &str) -> Result<u64> {
match v {
Value::Number(n) => {
if !n.is_finite() {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: {pos} must be a finite number, got {n}"),
));
}
let result = if *n < 0.0 {
(*n as i64) as u64
} else {
*n as u64
};
Ok(result)
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("{name}: {pos} must be a number, got {other:?}"),
)),
}
}
let name = b.name();
let result: u64 = match b {
Builtin::Band64 => {
to_u64(&args[0], "first arg", name)? & to_u64(&args[1], "second arg", name)?
}
Builtin::Bor64 => {
to_u64(&args[0], "first arg", name)? | to_u64(&args[1], "second arg", name)?
}
Builtin::Bxor64 => {
to_u64(&args[0], "first arg", name)? ^ to_u64(&args[1], "second arg", name)?
}
Builtin::Bnot64 => !to_u64(&args[0], "arg", name)?,
Builtin::Bshl64 => {
let x = to_u64(&args[0], "first arg", name)?;
let n = to_u64(&args[1], "second arg", name)? % 64;
x << n
}
Builtin::Bshr64 => {
let x = to_u64(&args[0], "first arg", name)?;
let n = to_u64(&args[1], "second arg", name)? % 64;
x >> n
}
Builtin::Brot64 => {
let x = to_u64(&args[0], "first arg", name)?;
let n = (to_u64(&args[1], "second arg", name)? % 64) as u32;
x.rotate_left(n)
}
_ => unreachable!(),
};
Ok(Value::Number(result as f64))
}
#[inline(never)]
fn run_bisect(list_arg: &Value, target_arg: &Value) -> Result<Value> {
let items = match list_arg {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("bisect: first arg must be a list, got {:?}", other),
));
}
};
let target = match target_arg {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("bisect: target must be a number, got {:?}", other),
));
}
};
if target.is_nan() {
return Ok(Value::Number(f64::NAN));
}
if items.is_empty() {
return Ok(Value::Number(0.0));
}
for item in items.iter() {
if !matches!(item, Value::Number(_)) {
return Err(RuntimeError::new(
"ILO-R009",
format!("bisect: list elements must be numbers, got {:?}", item),
));
}
}
let mut lo: usize = 0;
let mut hi: usize = items.len();
while lo < hi {
let mid = lo + (hi - lo) / 2;
let Value::Number(m) = items[mid] else {
unreachable!("validated above")
};
if m < target {
lo = mid + 1;
} else {
hi = mid;
}
}
Ok(Value::Number(lo as f64))
}
#[inline(never)]
fn median_run(items: &[Value]) -> Result<Value> {
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"median: cannot take median of an empty list".to_string(),
));
}
let mut nums: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("median: list elements must be numbers, got {:?}", other),
));
}
}
}
if nums.iter().any(|x| x.is_nan()) {
return Ok(Value::Number(f64::NAN));
}
nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = nums.len();
let m = if n % 2 == 1 {
nums[n / 2]
} else {
(nums[n / 2 - 1] + nums[n / 2]) / 2.0
};
Ok(Value::Number(m))
}
#[inline(never)]
fn quantile_run(items: &[Value], p: f64) -> Result<Value> {
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"quantile: cannot take quantile of an empty list".to_string(),
));
}
let mut nums: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("quantile: list elements must be numbers, got {:?}", other),
));
}
}
}
if nums.iter().any(|x| x.is_nan()) {
return Ok(Value::Number(f64::NAN));
}
nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let p = p.clamp(0.0, 1.0);
let n = nums.len();
if n == 1 {
return Ok(Value::Number(nums[0]));
}
let pos = p * (n - 1) as f64;
let lo = pos.floor() as usize;
let hi = pos.ceil() as usize;
let frac = pos - lo as f64;
let q = nums[lo] + frac * (nums[hi] - nums[lo]);
Ok(Value::Number(q))
}
#[inline(never)]
fn variance_run(items: &[Value]) -> Result<Value> {
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"variance: cannot take variance of an empty list".to_string(),
));
}
let mut nums: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("variance: list elements must be numbers, got {:?}", other),
));
}
}
}
let n = nums.len();
if n == 1 {
return Err(RuntimeError::new(
"ILO-R009",
"variance: at least 2 samples required".to_string(),
));
}
if nums.iter().any(|x| x.is_nan()) {
return Ok(Value::Number(f64::NAN));
}
let mean = nums.iter().sum::<f64>() / n as f64;
let sse: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
Ok(Value::Number(sse / (n - 1) as f64))
}
#[inline(never)]
fn stdev_run(items: &[Value]) -> Result<Value> {
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"stdev: cannot take stdev of an empty list".to_string(),
));
}
let mut nums: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("stdev: list elements must be numbers, got {:?}", other),
));
}
}
}
let n = nums.len();
if n == 1 {
return Err(RuntimeError::new(
"ILO-R009",
"stdev: at least 2 samples required".to_string(),
));
}
if nums.iter().any(|x| x.is_nan()) {
return Ok(Value::Number(f64::NAN));
}
let mean = nums.iter().sum::<f64>() / n as f64;
let sse: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
Ok(Value::Number((sse / (n - 1) as f64).sqrt()))
}
#[inline(never)]
fn rgx_run(pattern: &str, input: &str) -> Result<Value> {
let re = regex::Regex::new(pattern)
.map_err(|e| RuntimeError::new("ILO-R009", format!("rgx: invalid regex pattern: {e}")))?;
let result: Vec<Value> = if re.captures_len() > 1 {
re.captures(input)
.map(|caps| {
(1..caps.len())
.filter_map(|i| {
caps.get(i)
.map(|m| Value::Text(Arc::new(m.as_str().to_string())))
})
.collect()
})
.unwrap_or_default()
} else {
re.find_iter(input)
.map(|m| Value::Text(Arc::new(m.as_str().to_string())))
.collect()
};
Ok(Value::List(Arc::new(result)))
}
#[inline(never)]
fn rgxall_run(pattern: &str, input: &str) -> Result<Value> {
let re = regex::Regex::new(pattern).map_err(|e| {
RuntimeError::new("ILO-R009", format!("rgxall: invalid regex pattern: {e}"))
})?;
let result: Vec<Value> = if re.captures_len() > 1 {
re.captures_iter(input)
.map(|caps| {
let groups: Vec<Value> = (1..caps.len())
.filter_map(|i| {
caps.get(i)
.map(|m| Value::Text(Arc::new(m.as_str().to_string())))
})
.collect();
Value::List(Arc::new(groups))
})
.collect()
} else {
re.find_iter(input)
.map(|m| {
Value::List(Arc::new(vec![Value::Text(Arc::new(
m.as_str().to_string(),
))]))
})
.collect()
};
Ok(Value::List(Arc::new(result)))
}
#[inline(never)]
fn rgxall1_run(pattern: &str, input: &str) -> Result<Value> {
let re = regex::Regex::new(pattern).map_err(|e| {
RuntimeError::new("ILO-R009", format!("rgxall1: invalid regex pattern: {e}"))
})?;
let group_count = re.captures_len().saturating_sub(1);
if group_count >= 2 {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxall1: pattern has {group_count} capture groups; rgxall1 only supports 0 or 1. Use rgxall for L (L t) with every group preserved."
),
));
}
let result: Vec<Value> = if group_count == 1 {
re.captures_iter(input)
.filter_map(|caps| {
caps.get(1)
.map(|m| Value::Text(Arc::new(m.as_str().to_string())))
})
.collect()
} else {
re.find_iter(input)
.map(|m| Value::Text(Arc::new(m.as_str().to_string())))
.collect()
};
Ok(Value::List(Arc::new(result)))
}
#[inline(never)]
fn rgxall_multi_run(pats: &Arc<Vec<Value>>, input: &Arc<String>) -> Result<Value> {
let mut result: Vec<Value> = Vec::new();
for (i, pat_val) in pats.iter().enumerate() {
let pattern = match pat_val {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxall-multi: pats[{i}] must be a string pattern, got {:?}",
other
),
));
}
};
let re = regex::Regex::new(pattern).map_err(|e| {
RuntimeError::new(
"ILO-R009",
format!("rgxall-multi: invalid regex pattern at index {i}: {e}"),
)
})?;
let group_count = re.captures_len().saturating_sub(1);
if group_count >= 2 {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxall-multi: pattern at index {i} has {group_count} capture groups; rgxall-multi only supports 0 or 1 per pattern. Use rgxall for L (L t) with every group preserved."
),
));
}
if group_count == 1 {
re.captures_iter(input.as_str())
.filter_map(|caps| {
caps.get(1)
.map(|m| Value::Text(Arc::new(m.as_str().to_string())))
})
.for_each(|v| result.push(v));
} else {
re.find_iter(input.as_str())
.map(|m| Value::Text(Arc::new(m.as_str().to_string())))
.for_each(|v| result.push(v));
}
}
Ok(Value::List(Arc::new(result)))
}
#[inline(never)]
fn rgxsub_run(pattern: &str, replacement: &str, subject: &str) -> Result<Value> {
let re = regex::Regex::new(pattern).map_err(|e| {
RuntimeError::new("ILO-R009", format!("rgxsub: invalid regex pattern: {e}"))
})?;
Ok(Value::Text(Arc::new(
re.replace_all(subject, replacement).into_owned(),
)))
}
#[inline(never)]
fn matmul_run(a_rows: &[Value], b_rows: &[Value]) -> Result<Value> {
let mut a: Vec<Vec<f64>> = Vec::with_capacity(a_rows.len());
let mut a_cols: Option<usize> = None;
for row in a_rows.iter() {
match row {
Value::List(r) => {
match a_cols {
None => a_cols = Some(r.len()),
Some(n) if n != r.len() => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"matmul: ragged rows in first arg (expected {n} cols, got {})",
r.len()
),
));
}
_ => {}
}
let mut nums = Vec::with_capacity(r.len());
for v in r.iter() {
match v {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("matmul: elements must be numbers, got {:?}", other),
));
}
}
}
a.push(nums);
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("matmul: rows must be lists, got {:?}", other),
));
}
}
}
let mut b: Vec<Vec<f64>> = Vec::with_capacity(b_rows.len());
let mut b_cols: Option<usize> = None;
for row in b_rows.iter() {
match row {
Value::List(r) => {
match b_cols {
None => b_cols = Some(r.len()),
Some(n) if n != r.len() => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"matmul: ragged rows in second arg (expected {n} cols, got {})",
r.len()
),
));
}
_ => {}
}
let mut nums = Vec::with_capacity(r.len());
for v in r.iter() {
match v {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("matmul: elements must be numbers, got {:?}", other),
));
}
}
}
b.push(nums);
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("matmul: rows must be lists, got {:?}", other),
));
}
}
}
let a_rows_n = a.len();
let a_cols_n = a_cols.unwrap_or(0);
let b_rows_n = b.len();
let b_cols_n = b_cols.unwrap_or(0);
if a_cols_n != b_rows_n {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"matmul: shape mismatch (a is {a_rows_n}x{a_cols_n}, b is {b_rows_n}x{b_cols_n})"
),
));
}
let mut out: Vec<Value> = Vec::with_capacity(a_rows_n);
#[allow(clippy::needless_range_loop)]
for i in 0..a_rows_n {
let mut row: Vec<Value> = Vec::with_capacity(b_cols_n);
for j in 0..b_cols_n {
let mut s = 0.0_f64;
for k in 0..a_cols_n {
s += a[i][k] * b[k][j];
}
row.push(Value::Number(s));
}
out.push(Value::List(Arc::new(row)));
}
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
fn ifft_run(items: &[Value]) -> Result<Value> {
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"ifft: input list must not be empty".to_string(),
));
}
let mut re: Vec<f64> = Vec::with_capacity(items.len());
let mut im: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::List(pair) if pair.len() == 2 => {
let r = match &pair[0] {
Value::Number(n) => *n,
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"ifft: pair elements must be numbers".to_string(),
));
}
};
let i = match &pair[1] {
Value::Number(n) => *n,
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"ifft: pair elements must be numbers".to_string(),
));
}
};
re.push(r);
im.push(i);
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"ifft: each element must be a [real, imag] pair, got {:?}",
other
),
));
}
}
}
let n = next_pow2(re.len());
re.resize(n, 0.0);
im.resize(n, 0.0);
cooley_tukey(&mut re, &mut im, true);
let result: Vec<Value> = re.into_iter().map(Value::Number).collect();
Ok(Value::List(Arc::new(result)))
}
#[inline(never)]
fn where_run(cond: &[Value], xs: &[Value], ys: &[Value]) -> Result<Value> {
if cond.len() != xs.len() || cond.len() != ys.len() {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"where: length mismatch — cond={}, xs={}, ys={}; all three lists must be the same length",
cond.len(),
xs.len(),
ys.len()
),
));
}
let mut out = Vec::with_capacity(cond.len());
for (i, c) in cond.iter().enumerate() {
match c {
Value::Bool(true) => out.push(xs[i].clone()),
Value::Bool(false) => out.push(ys[i].clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"where: cond element at index {} must be a bool, got {:?}",
i, other
),
));
}
}
}
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
fn clamp_run(x: f64, lo: f64, hi: f64) -> Value {
Value::Number(x.min(hi).max(lo))
}
#[inline(never)]
fn chunks_run(n_raw: f64, list_arg: &Value) -> Result<Value> {
if n_raw.fract() != 0.0 || n_raw <= 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("chunks: size must be a positive integer, got {n_raw}"),
));
}
let n = n_raw as usize;
let xs = match list_arg {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("chunks: requires a list, got {:?}", other),
));
}
};
let mut out: Vec<Value> = Vec::with_capacity(xs.len().div_ceil(n));
for chunk in xs.chunks(n) {
out.push(Value::List(Arc::new(chunk.to_vec())));
}
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
fn ewm_run(list_arg: &Value, a: f64) -> Result<Value> {
let items = match list_arg {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ewm: first arg must be a list, got {:?}", other),
));
}
};
if !(0.0..=1.0).contains(&a) {
return Err(RuntimeError::new(
"ILO-R009",
format!("ewm: smoothing factor a must be in [0, 1], got {}", a),
));
}
let mut nums: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ewm: list elements must be numbers, got {:?}", other),
));
}
}
}
let out: Vec<Value> = ewm_compute(&nums, a)
.into_iter()
.map(Value::Number)
.collect();
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
fn cap_run(arg: &Value) -> Result<Value> {
match arg {
Value::Text(s) => {
let mut chars = s.chars();
let out = match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
};
Ok(Value::Text(Arc::new(out)))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("cap requires text, got {:?}", other),
)),
}
}
#[inline(never)]
fn padl_padr_run(is_left: bool, args: &[Value]) -> Result<Value> {
let name = if is_left { "padl" } else { "padr" };
let s = match &args[0] {
Value::Text(t) => t.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} arg 1 requires text, got {:?}", other),
));
}
};
let w = match &args[1] {
Value::Number(n) => {
if !n.is_finite() || n.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} width must be a non-negative integer, got {n}"),
));
}
if *n < 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} width must be non-negative, got {n}"),
));
}
*n as usize
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} arg 2 requires number, got {:?}", other),
));
}
};
let pad_char: char = if args.len() == 3 {
match &args[2] {
Value::Text(t) => {
let mut iter = t.chars();
match (iter.next(), iter.next()) {
(Some(c), None) => c,
_ => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"{name} pad char must be a 1-character string, got {:?}",
t.as_str()
),
));
}
}
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"{name} pad char must be a 1-character string, got {:?}",
other
),
));
}
}
} else {
' '
};
let char_count = s.chars().count();
if char_count >= w {
return Ok(Value::Text(s));
}
let pad: String = std::iter::repeat_n(pad_char, w - char_count).collect();
let out = if is_left {
format!("{pad}{s}")
} else {
format!("{s}{pad}")
};
Ok(Value::Text(Arc::new(out)))
}
#[inline(never)]
fn wr_run(env: &mut Env, args: Vec<Value>) -> Result<Value> {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wr: first arg must be a text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_write(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let content = if args.len() == 3 {
let fmt = match &args[2] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wr: format arg must be text, got {:?}", other),
));
}
};
match fmt.as_str() {
"csv" | "tsv" => {
let sep = if fmt.as_str() == "csv" { ',' } else { '\t' };
let rows = match &args[1] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wr: data for {fmt} must be a list of rows, got {:?}", other),
));
}
};
write_csv_tsv(rows, sep)?
}
"json" => {
fn value_to_json_local(v: &Value) -> serde_json::Value {
match v {
Value::Number(n) => serde_json::Value::from(*n),
Value::Text(s) => serde_json::Value::from(s.as_str()),
Value::Bool(b) => serde_json::Value::from(*b),
Value::List(l) => {
serde_json::Value::Array(l.iter().map(value_to_json_local).collect())
}
Value::Map(m) => {
let obj: serde_json::Map<String, serde_json::Value> = m
.iter()
.map(|(k, v)| (k.to_display_string(), value_to_json_local(v)))
.collect();
serde_json::Value::Object(obj)
}
Value::Nil => serde_json::Value::Null,
other => serde_json::Value::from(format!("{other}")),
}
}
serde_json::to_string_pretty(&value_to_json_local(&args[1]))
.unwrap_or_else(|e| format!("json error: {e}"))
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wr: unknown format '{other}', expected csv, tsv, or json"),
));
}
}
} else {
match &args[1] {
Value::Text(s) => (**s).clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wr: second arg must be text content, got {:?}", other),
));
}
}
};
match std::fs::write(path.as_str(), &content) {
Ok(()) => Ok(Value::Ok(Box::new(Value::Text(path)))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[inline(never)]
fn fmt_run(args: &[Value]) -> Result<Value> {
let template = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("fmt first arg must be text template, got {:?}", other),
));
}
};
let mut result = String::new();
let mut arg_idx = 1;
let mut chars = template.chars().peekable();
while let Some(c) = chars.next() {
if c == '{'
&& (chars.peek() == Some(&'}')
|| chars.peek() == Some(&':')
|| chars.peek() == Some(&'.'))
{
let mut spec = String::from("{");
let mut terminated = false;
for sc in chars.by_ref() {
spec.push(sc);
if sc == '}' {
terminated = true;
break;
}
}
if !terminated {
result.push_str(&spec);
continue;
}
match parse_fmt_spec(&spec) {
Some(FmtSpec::Bare) => {
if arg_idx < args.len() {
result.push_str(&format!("{}", args[arg_idx]));
arg_idx += 1;
} else {
result.push_str("{}");
}
}
Some(spec_kind) => {
if arg_idx >= args.len() {
return Err(RuntimeError::new(
"ILO-R009",
format!("fmt template spec `{spec}` has no matching value arg"),
));
}
let rendered = apply_fmt_spec(&spec_kind, &args[arg_idx]).map_err(|e| {
RuntimeError::new("ILO-R009", format!("fmt spec `{spec}`: {e}"))
})?;
result.push_str(&rendered);
arg_idx += 1;
}
None => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"fmt: unsupported placeholder spec `{spec}`. \
Supported: `{{}}`, `{{.Nf}}` / `{{:.Nf}}` (decimal places), \
`{{:N}}` (right-align width), `{{:Nd}}` (integer width), \
`{{:<N}}` (left-align width). Zero-padded widths and hex/sign \
are out of scope; compose via `fmt2` / `padl` / `padr`."
),
));
}
}
} else {
result.push(c);
}
}
Ok(Value::Text(Arc::new(result)))
}
#[inline(never)]
fn flt_run(
env: &mut Env,
fn_name: &str,
captures: Vec<Value>,
ctx: Option<Value>,
items: Arc<Vec<Value>>,
) -> Result<Value> {
let mut keep: Vec<bool> = Vec::with_capacity(items.len());
for item in items.iter() {
let mut call_args = match &ctx {
Some(c) => vec![item.clone(), c.clone()],
None => vec![item.clone()],
};
call_args.extend(captures.iter().cloned());
match call_function(env, fn_name, call_args)? {
Value::Bool(true) => keep.push(true),
Value::Bool(false) => keep.push(false),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("flt: predicate must return bool, got {:?}", other),
));
}
}
}
let mut items = items;
if Arc::strong_count(&items) == 1 {
let inner = Arc::make_mut(&mut items);
let mut idx = 0usize;
inner.retain(|_| {
let k = keep[idx];
idx += 1;
k
});
Ok(Value::List(items))
} else {
let mut result = Vec::with_capacity(keep.iter().filter(|k| **k).count());
for (item, &k) in items.iter().zip(keep.iter()) {
if k {
result.push(item.clone());
}
}
Ok(Value::List(Arc::new(result)))
}
}
#[inline(never)]
fn grp_run(
env: &mut Env,
fn_name: &str,
captures: Vec<Value>,
items: Arc<Vec<Value>>,
) -> Result<Value> {
let mut groups: std::collections::HashMap<MapKey, Vec<Value>> =
std::collections::HashMap::new();
for item in items.iter() {
let mut call_args = vec![item.clone()];
call_args.extend(captures.iter().cloned());
let key = call_function(env, fn_name, call_args)?;
let map_key = match &key {
Value::Text(s) => MapKey::Text((**s).clone()),
Value::Number(n) => {
if !n.is_finite() {
return Err(RuntimeError::new(
"ILO-R009",
format!("grp: numeric key must be finite, got {n}"),
));
}
MapKey::Int(n.floor() as i64)
}
Value::Bool(b) => MapKey::Text(format!("{b}")),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"grp: key function must return a string, number, or bool, got {:?}",
other
),
));
}
};
groups.entry(map_key).or_default().push(item.clone());
}
let map: HashMap<MapKey, Value> = groups
.into_iter()
.map(|(k, v)| (k, Value::List(Arc::new(v))))
.collect();
Ok(Value::Map(Arc::new(map)))
}
#[inline(never)]
fn uniqby_run(
env: &mut Env,
fn_name: &str,
captures: Vec<Value>,
items: Arc<Vec<Value>>,
) -> Result<Value> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut out: Vec<Value> = Vec::new();
for item in items.iter() {
let mut call_args = vec![item.clone()];
call_args.extend(captures.iter().cloned());
let key = call_function(env, fn_name, call_args)?;
let key_str = match &key {
Value::Text(s) => format!("t:{s}"),
Value::Number(n) => {
if *n == (*n as i64) as f64 {
format!("n:{}", *n as i64)
} else {
format!("n:{n}")
}
}
Value::Bool(b) => format!("b:{b}"),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"uniqby: key function must return a string, number, or bool, got {:?}",
other
),
));
}
};
if seen.insert(key_str) {
out.push(item.clone());
}
}
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
#[allow(dead_code)]
fn post_run(env: &mut Env, args: Vec<Value>) -> Result<Value> {
let (url, body) = match (&args[0], &args[1]) {
(Value::Text(u), Value::Text(b)) => (u.clone(), b.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pst requires (t, t), got ({:?}, {:?})", args[0], args[1]),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let headers = if args.len() == 3 {
match &args[2] {
Value::Map(m) => m
.iter()
.map(|(k, v)| {
let vs: String = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect::<Vec<_>>(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pst headers must be M t t, got {:?}", other),
));
}
}
} else {
vec![]
};
#[cfg(feature = "http")]
{
let mut req = minreq::post(url.as_str()).with_body(body.as_str());
for (k, v) in &headers {
req = req.with_header(k.as_str(), v.as_str());
}
match req.send() {
Ok(resp) => match resp.as_str() {
Ok(b) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(b.to_string()))))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"response is not valid UTF-8: {e}"
)))))),
},
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[cfg(not(feature = "http"))]
{
let _ = (url, body, headers);
Ok(Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
))))
}
}
#[inline(never)]
fn put_pat_run(env: &mut Env, builtin: Option<Builtin>, args: Vec<Value>) -> Result<Value> {
let name = builtin.unwrap().name();
let (url, body) = match (&args[0], &args[1]) {
(Value::Text(u), Value::Text(b)) => (u.clone(), b.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} requires (t, t), got ({:?}, {:?})", args[0], args[1]),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let headers = if args.len() == 3 {
match &args[2] {
Value::Map(m) => m
.iter()
.map(|(k, v)| {
let vs: String = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect::<Vec<_>>(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} headers must be M t t, got {:?}", other),
));
}
}
} else {
vec![]
};
#[cfg(feature = "http")]
{
let mut req = match builtin {
Some(Builtin::Put) => minreq::put(url.as_str()),
Some(Builtin::Pat) => minreq::patch(url.as_str()),
_ => unreachable!(),
}
.with_body(body.as_str());
for (k, v) in &headers {
req = req.with_header(k.as_str(), v.as_str());
}
match req.send() {
Ok(resp) => match resp.as_str() {
Ok(b) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(b.to_string()))))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"response is not valid UTF-8: {e}"
)))))),
},
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[cfg(not(feature = "http"))]
{
let _ = (url, body, headers);
Ok(Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
))))
}
}
#[inline(never)]
fn del_hed_opt_run(env: &mut Env, builtin: Option<Builtin>, args: Vec<Value>) -> Result<Value> {
let name = builtin.unwrap().name();
let url = match &args[0] {
Value::Text(u) => u.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} requires text (url), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let headers = if args.len() == 2 {
match &args[1] {
Value::Map(m) => m
.iter()
.map(|(k, v)| {
let vs: String = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect::<Vec<_>>(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name} headers must be M t t, got {:?}", other),
));
}
}
} else {
vec![]
};
#[cfg(feature = "http")]
{
let mut req = match builtin {
Some(Builtin::Del) => minreq::delete(url.as_str()),
Some(Builtin::Hed) => minreq::head(url.as_str()),
Some(Builtin::Opt) => minreq::options(url.as_str()),
_ => unreachable!(),
};
for (k, v) in &headers {
req = req.with_header(k.as_str(), v.as_str());
}
match req.send() {
Ok(resp) => match resp.as_str() {
Ok(body) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(body.to_string()))))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"response is not valid UTF-8: {e}"
)))))),
},
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[cfg(not(feature = "http"))]
{
let _ = (url, headers);
Ok(Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
))))
}
}
#[inline(never)]
fn rolling_window_run(env_name: &str, b: Builtin, n_f: f64, list_arg: &Value) -> Result<Value> {
let name = env_name;
if !n_f.is_finite() || n_f.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"{name}: window size n must be a non-negative integer, got {}",
n_f
),
));
}
if n_f <= 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: window size n must be >= 1, got {}", n_f),
));
}
let n = n_f as usize;
let items = match list_arg {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: second arg must be a list, got {:?}", other),
));
}
};
let mut nums: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(v) => nums.push(*v),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: list elements must be numbers, got {:?}", other),
));
}
}
}
let computed = match b {
Builtin::Rsum => rsum_compute(n, &nums),
Builtin::Ravg => ravg_compute(n, &nums),
Builtin::Rmin => rmin_compute(n, &nums),
_ => unreachable!(),
};
let out: Vec<Value> = computed.into_iter().map(Value::Number).collect();
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
fn argmax_argmin_run(is_min: bool, name: &str, arg: &Value) -> Result<Value> {
let items = match arg {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: arg must be a list, got {:?}", other),
));
}
};
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: cannot take {name} of an empty list"),
));
}
let mut best_idx: usize = 0;
let mut best_val: Option<f64> = None;
for (i, item) in items.iter().enumerate() {
match item {
Value::Number(n) => {
if n.is_nan() {
return Ok(Value::Number(f64::NAN));
}
match best_val {
None => {
best_val = Some(*n);
best_idx = i;
}
Some(cur) => {
let better = if is_min { *n < cur } else { *n > cur };
if better {
best_val = Some(*n);
best_idx = i;
}
}
}
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: list elements must be numbers, got {:?}", other),
));
}
}
}
Ok(Value::Number(best_idx as f64))
}
#[inline(never)]
fn setops_run(builtin: Option<Builtin>, xs_arg: &Value, ys_arg: &Value) -> Result<Value> {
let op_name = match builtin {
Some(Builtin::Setunion) => "setunion",
Some(Builtin::Setinter) => "setinter",
Some(Builtin::Setdiff) => "setdiff",
_ => unreachable!(),
};
let xs = match xs_arg {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{op_name} arg 1 requires a list, got {:?}", other),
));
}
};
let ys = match ys_arg {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{op_name} arg 2 requires a list, got {:?}", other),
));
}
};
fn key_for(v: &Value, op_name: &str) -> std::result::Result<String, RuntimeError> {
match v {
Value::Text(s) => Ok(format!("t:{s}")),
Value::Number(n) => {
if *n == (*n as i64) as f64 {
Ok(format!("n:{}", *n as i64))
} else {
Ok(format!("n:{n}"))
}
}
Value::Bool(b) => Ok(format!("b:{b}")),
other => Err(RuntimeError::new(
"ILO-R009",
format!(
"{op_name}: elements must be text, number, or bool, got {:?}",
other
),
)),
}
}
use std::collections::{HashMap, HashSet};
let mut set_a: HashSet<String> = HashSet::new();
let mut a_first: HashMap<String, Value> = HashMap::new();
for v in xs.iter() {
let k = key_for(v, op_name)?;
if set_a.insert(k.clone()) {
a_first.insert(k, v.clone());
}
}
let mut set_b: HashSet<String> = HashSet::new();
let mut b_first: HashMap<String, Value> = HashMap::new();
for v in ys.iter() {
let k = key_for(v, op_name)?;
if set_b.insert(k.clone()) {
b_first.insert(k, v.clone());
}
}
let (result_keys, value_lookup): (Vec<String>, &HashMap<String, Value>) = match builtin {
Some(Builtin::Setunion) => {
let mut keys: Vec<String> = set_a.union(&set_b).cloned().collect();
let mut merged = a_first;
for (k, v) in &b_first {
merged.entry(k.clone()).or_insert_with(|| v.clone());
}
keys.sort();
let mut out: Vec<Value> = Vec::with_capacity(keys.len());
for k in &keys {
if let Some(v) = merged.get(k) {
out.push(v.clone());
}
}
return Ok(Value::List(Arc::new(out)));
}
Some(Builtin::Setinter) => (
set_a.intersection(&set_b).cloned().collect::<Vec<_>>(),
&a_first,
),
Some(Builtin::Setdiff) => (
set_a.difference(&set_b).cloned().collect::<Vec<_>>(),
&a_first,
),
_ => unreachable!(),
};
let mut keys = result_keys;
keys.sort();
let mut out: Vec<Value> = Vec::with_capacity(keys.len());
for k in &keys {
if let Some(v) = value_lookup.get(k) {
out.push(v.clone());
}
}
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
fn run_convolve(xs_val: &Value, ys_val: &Value) -> Result<Value> {
let xs = match xs_val {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("convolve: first arg must be a list, got {:?}", other),
));
}
};
let ys = match ys_val {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("convolve: second arg must be a list, got {:?}", other),
));
}
};
if xs.is_empty() || ys.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"convolve: both input lists must be non-empty".to_string(),
));
}
let mut a = Vec::with_capacity(xs.len());
for item in xs.iter() {
match item {
Value::Number(n) => a.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"convolve: first list elements must be numbers, got {:?}",
other
),
));
}
}
}
let mut b = Vec::with_capacity(ys.len());
for item in ys.iter() {
match item {
Value::Number(n) => b.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"convolve: second list elements must be numbers, got {:?}",
other
),
));
}
}
}
convolve_compute(&a, &b)
.map(|v| Value::List(Arc::new(v.into_iter().map(Value::Number).collect())))
}
#[inline(never)]
fn convolve_compute(a: &[f64], b: &[f64]) -> Result<Vec<f64>> {
let n = a.len() + b.len() - 1;
let mut out = vec![0.0_f64; n];
for (i, &ai) in a.iter().enumerate() {
for (j, &bj) in b.iter().enumerate() {
out[i + j] += ai * bj;
}
}
Ok(out)
}
#[inline(never)]
fn run_searchsorted(xs_val: &Value, targets_val: &Value) -> Result<Value> {
let xs = match xs_val {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("searchsorted: first arg must be a list, got {:?}", other),
));
}
};
let targets = match targets_val {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"searchsorted: second arg must be a list of targets, got {:?}",
other
),
));
}
};
let mut nums: Vec<f64> = Vec::with_capacity(xs.len());
for item in xs.iter() {
match item {
Value::Number(n) => nums.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"searchsorted: sorted-list elements must be numbers, got {:?}",
other
),
));
}
}
}
let mut out = Vec::with_capacity(targets.len());
for t_val in targets.iter() {
let t = match t_val {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"searchsorted: target elements must be numbers, got {:?}",
other
),
));
}
};
if t.is_nan() {
out.push(Value::Number(f64::NAN));
continue;
}
let mut lo = 0usize;
let mut hi = nums.len();
while lo < hi {
let mid = lo + (hi - lo) / 2;
if nums[mid] < t {
lo = mid + 1;
} else {
hi = mid;
}
}
out.push(Value::Number(lo as f64));
}
Ok(Value::List(Arc::new(out)))
}
#[inline(never)]
fn run_cabs(pair_val: &Value) -> Result<Value> {
let pair = match pair_val {
Value::List(l) if l.len() == 2 => l,
Value::List(l) => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"cabs: expected [re, im] pair (length 2), got length {}",
l.len()
),
));
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cabs: arg must be a [re, im] list, got {:?}", other),
));
}
};
let re = match &pair[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cabs: re must be a number, got {:?}", other),
));
}
};
let im = match &pair[1] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cabs: im must be a number, got {:?}", other),
));
}
};
Ok(Value::Number((re * re + im * im).sqrt()))
}
#[inline(never)]
fn run_cmul(a_val: &Value, b_val: &Value) -> Result<Value> {
fn extract_pair(v: &Value, label: &str) -> Result<(f64, f64)> {
let pair = match v {
Value::List(l) if l.len() == 2 => l,
Value::List(l) => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"cmul: {label} must be a [re, im] pair (length 2), got length {}",
l.len()
),
));
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cmul: {label} must be a [re, im] list, got {:?}", other),
));
}
};
let re = match &pair[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cmul: {label} re must be a number, got {:?}", other),
));
}
};
let im = match &pair[1] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cmul: {label} im must be a number, got {:?}", other),
));
}
};
Ok((re, im))
}
let (re_a, im_a) = extract_pair(a_val, "a")?;
let (re_b, im_b) = extract_pair(b_val, "b")?;
let re_out = re_a * re_b - im_a * im_b;
let im_out = re_a * im_b + im_a * re_b;
Ok(Value::List(Arc::new(vec![
Value::Number(re_out),
Value::Number(im_out),
])))
}
#[inline(never)]
fn run_pdist2(xs_val: &Value, ys_val: &Value) -> Result<Value> {
let xs = match xs_val {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pdist2: first arg must be a list, got {:?}", other),
));
}
};
let ys = match ys_val {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pdist2: second arg must be a list, got {:?}", other),
));
}
};
if xs.len() != ys.len() {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"pdist2: lists must have the same length, got {} and {}",
xs.len(),
ys.len()
),
));
}
let mut out = Vec::with_capacity(xs.len());
for (x_val, y_val) in xs.iter().zip(ys.iter()) {
let x = match x_val {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"pdist2: first list elements must be numbers, got {:?}",
other
),
));
}
};
let y = match y_val {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"pdist2: second list elements must be numbers, got {:?}",
other
),
));
}
};
let d = x - y;
out.push(Value::Number(d * d));
}
Ok(Value::List(Arc::new(out)))
}
fn call_function(env: &mut Env, name: &str, args: Vec<Value>) -> Result<Value> {
let builtin = Builtin::from_name(name);
if builtin == Some(Builtin::Len) {
if args.len() != 1 {
return Err(RuntimeError::new(
"ILO-R009",
format!("len: expected 1 arg, got {}", args.len()),
));
}
return match &args[0] {
Value::Text(s) => Ok(Value::Number(s.len() as f64)),
Value::List(l) => Ok(Value::Number(l.len() as f64)),
Value::Map(m) => Ok(Value::Number(m.len() as f64)),
other => Err(RuntimeError::new(
"ILO-R009",
format!("len requires string, list, or map, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Mmap) && args.is_empty() {
return Ok(Value::Map(Arc::new(HashMap::new())));
}
if builtin == Some(Builtin::Mget) && args.len() == 2 {
return match &args[0] {
Value::Map(m) => {
let key = MapKey::from_value(&args[1], "mget")?;
Ok(m.get(&key).cloned().unwrap_or(Value::Nil))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mget: expects map and key".to_string(),
)),
};
}
if builtin == Some(Builtin::Mset) && args.len() == 3 {
let mut it = args.into_iter();
let map_val = it.next().unwrap();
let key_val = it.next().unwrap();
let value = it.next().unwrap();
return match map_val {
Value::Map(mut m) => {
let key = MapKey::from_value(&key_val, "mset")?;
let inner = Arc::make_mut(&mut m);
inner.insert(key, value);
Ok(Value::Map(m))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mset: expects map, key, and value".to_string(),
)),
};
}
if builtin == Some(Builtin::MgetOr) && args.len() == 3 {
return match &args[0] {
Value::Map(m) => {
let key = MapKey::from_value(&args[1], "mget-or")?;
Ok(m.get(&key).cloned().unwrap_or_else(|| args[2].clone()))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mget-or: expects map, key, and default".to_string(),
)),
};
}
if builtin == Some(Builtin::Mhas) && args.len() == 2 {
return match &args[0] {
Value::Map(m) => {
let key = MapKey::from_value(&args[1], "mhas")?;
Ok(Value::Bool(m.contains_key(&key)))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mhas: expects map and key".to_string(),
)),
};
}
if builtin == Some(Builtin::Mkeys) && args.len() == 1 {
return match &args[0] {
Value::Map(m) => {
let mut keys: Vec<&MapKey> = m.keys().collect();
keys.sort();
Ok(Value::List(Arc::new(
keys.into_iter().map(map_key_to_value).collect(),
)))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mkeys: expects a map".to_string(),
)),
};
}
if builtin == Some(Builtin::Mvals) && args.len() == 1 {
return match &args[0] {
Value::Map(m) => {
let mut pairs: Vec<(&MapKey, &Value)> = m.iter().collect();
pairs.sort_by_key(|(k, _)| (*k).clone());
Ok(Value::List(Arc::new(
pairs.into_iter().map(|(_, v)| v.clone()).collect(),
)))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mvals: expects a map".to_string(),
)),
};
}
if builtin == Some(Builtin::Mpairs) && args.len() == 1 {
return match &args[0] {
Value::Map(m) => {
let mut pairs: Vec<(&MapKey, &Value)> = m.iter().collect();
pairs.sort_by_key(|(k, _)| (*k).clone());
let out: Vec<Value> = pairs
.into_iter()
.map(|(k, v)| Value::List(Arc::new(vec![map_key_to_value(k), v.clone()])))
.collect();
Ok(Value::List(Arc::new(out)))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mpairs: expects a map".to_string(),
)),
};
}
if builtin == Some(Builtin::Mdel) && args.len() == 2 {
let mut it = args.into_iter();
let map_val = it.next().unwrap();
let key_val = it.next().unwrap();
return match map_val {
Value::Map(mut m) => {
let key = MapKey::from_value(&key_val, "mdel")?;
let inner = Arc::make_mut(&mut m);
inner.remove(&key);
Ok(Value::Map(m))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mdel: expects map and key".to_string(),
)),
};
}
if builtin == Some(Builtin::Det) && args.len() == 1 {
let mat = matrix_from_value(&args[0], "det")?;
let n = mat.len();
if n == 0 {
return Err(RuntimeError::new(
"ILO-R009",
"det: empty matrix".to_string(),
));
}
for row in &mat {
if row.len() != n {
return Err(RuntimeError::new(
"ILO-R009",
"det: matrix must be square".to_string(),
));
}
}
let (_lu, _piv, det, _) = lu_decompose(mat);
return Ok(Value::Number(det));
}
if builtin == Some(Builtin::Inv) && args.len() == 1 {
let mat = matrix_from_value(&args[0], "inv")?;
let n = mat.len();
if n == 0 {
return Err(RuntimeError::new(
"ILO-R009",
"inv: empty matrix".to_string(),
));
}
for row in &mat {
if row.len() != n {
return Err(RuntimeError::new(
"ILO-R009",
"inv: matrix must be square".to_string(),
));
}
}
let (lu, piv, _det, singular) = lu_decompose(mat);
if singular {
return Err(RuntimeError::new(
"ILO-R009",
"inv: matrix is singular".to_string(),
));
}
let mut cols: Vec<Vec<f64>> = Vec::with_capacity(n);
for j in 0..n {
let mut e = vec![0.0; n];
e[j] = 1.0;
cols.push(lu_solve(&lu, &piv, &e));
}
let rows: Vec<Value> = (0..n)
.map(|i| {
Value::List(Arc::new(
(0..n)
.map(|j| Value::Number(cols[j][i]))
.collect::<Vec<_>>(),
))
})
.collect();
return Ok(Value::List(Arc::new(rows)));
}
if builtin == Some(Builtin::Solve) && args.len() == 2 {
let mat = matrix_from_value(&args[0], "solve")?;
let b = vec_from_value(&args[1], "solve")?;
let n = mat.len();
if n == 0 {
return Err(RuntimeError::new(
"ILO-R009",
"solve: empty matrix".to_string(),
));
}
for row in &mat {
if row.len() != n {
return Err(RuntimeError::new(
"ILO-R009",
"solve: matrix must be square".to_string(),
));
}
}
if b.len() != n {
return Err(RuntimeError::new(
"ILO-R009",
"solve: vector length must match matrix size".to_string(),
));
}
let (lu, piv, _det, singular) = lu_decompose(mat);
if singular {
return Err(RuntimeError::new(
"ILO-R009",
"solve: matrix is singular".to_string(),
));
}
let x = lu_solve(&lu, &piv, &b);
return Ok(Value::List(Arc::new(
x.into_iter().map(Value::Number).collect(),
)));
}
if builtin == Some(Builtin::Lstsq) && args.len() == 2 {
return lstsq_run(&args[0], &args[1]);
}
if builtin == Some(Builtin::Str) {
if args.len() != 1 {
return Err(RuntimeError::new(
"ILO-R009",
format!("str: expected 1 arg, got {}", args.len()),
));
}
return match &args[0] {
Value::Number(n) => {
let s = if n.fract() == 0.0 && n.abs() < 1e15 {
format!("{}", *n as i64)
} else {
format!("{}", n)
};
Ok(Value::Text(Arc::new(s)))
}
Value::Text(_) => Ok(args[0].clone()),
other => Err(RuntimeError::new(
"ILO-R009",
format!("str requires a number or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Num) {
if args.len() != 1 {
return Err(RuntimeError::new(
"ILO-R009",
format!("num: expected 1 arg, got {}", args.len()),
));
}
return match &args[0] {
Value::Text(s) => {
let trimmed = s.trim_matches(|c: char| c.is_ascii_whitespace());
match trimmed.parse::<f64>() {
Ok(n) => Ok(Value::Ok(Box::new(Value::Number(n)))),
Err(_) => Ok(Value::Err(Box::new(Value::Text(s.clone())))),
}
}
Value::Number(n) => Ok(Value::Ok(Box::new(Value::Number(*n)))),
other => Err(RuntimeError::new(
"ILO-R009",
format!("num requires text or number, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Abs) {
if args.len() != 1 {
return Err(RuntimeError::new(
"ILO-R009",
format!("abs: expected 1 arg, got {}", args.len()),
));
}
return match &args[0] {
Value::Number(n) => Ok(Value::Number(n.abs())),
other => Err(RuntimeError::new(
"ILO-R009",
format!("abs requires a number, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Mod) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => {
if *b == 0.0 {
Err(RuntimeError::new("ILO-R003", "modulo by zero".to_string()))
} else {
Ok(Value::Number(a % b))
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"mod requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Fmod) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => {
if *b == 0.0 {
Err(RuntimeError::new(
"ILO-R003",
"fmod: modulo by zero".to_string(),
))
} else {
let r = a % b;
Ok(Value::Number(if r != 0.0 && r.signum() != b.signum() {
r + b
} else {
r
}))
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"fmod requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Clamp) && args.len() == 3 {
return match (&args[0], &args[1], &args[2]) {
(Value::Number(x), Value::Number(lo), Value::Number(hi)) => Ok(clamp_run(*x, *lo, *hi)),
_ => Err(RuntimeError::new(
"ILO-R009",
"clamp requires three numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Min) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a.min(*b))),
_ => Err(RuntimeError::new(
"ILO-R009",
"min requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Max) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a.max(*b))),
_ => Err(RuntimeError::new(
"ILO-R009",
"max requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Argmax) && args.len() == 1 {
return argmax_argmin_run(false, "argmax", &args[0]);
}
if builtin == Some(Builtin::Argmin) && args.len() == 1 {
return argmax_argmin_run(true, "argmin", &args[0]);
}
if builtin == Some(Builtin::Argsort) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("argsort: arg must be a list, got {:?}", other),
));
}
};
if items.is_empty() {
return Ok(Value::List(Arc::new(Vec::new())));
}
for item in items.iter() {
if !matches!(item, Value::Number(_)) {
return Err(RuntimeError::new(
"ILO-R009",
format!("argsort: list elements must be numbers, got {:?}", item),
));
}
}
let mut idxs: Vec<usize> = (0..items.len()).collect();
idxs.sort_by(|&a, &b| {
let (Value::Number(x), Value::Number(y)) = (&items[a], &items[b]) else {
unreachable!()
};
x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
});
let out: Vec<Value> = idxs.into_iter().map(|i| Value::Number(i as f64)).collect();
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Bisect) && args.len() == 2 {
return run_bisect(&args[0], &args[1]);
}
if matches!(
builtin,
Some(
Builtin::Band
| Builtin::Bor
| Builtin::Bxor
| Builtin::Bnot
| Builtin::Bshl
| Builtin::Bshr
| Builtin::Brot
)
) {
let b = builtin.unwrap();
let expected_argc = if b == Builtin::Bnot { 1 } else { 2 };
if args.len() == expected_argc {
return run_bitwise(b, &args);
}
}
if matches!(
builtin,
Some(
Builtin::Band64
| Builtin::Bor64
| Builtin::Bxor64
| Builtin::Bnot64
| Builtin::Bshl64
| Builtin::Bshr64
| Builtin::Brot64
)
) {
let b = builtin.unwrap();
let expected_argc = if b == Builtin::Bnot64 { 1 } else { 2 };
if args.len() == expected_argc {
return run_bitwise_64(b, &args);
}
}
if matches!(builtin, Some(Builtin::Min | Builtin::Max)) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: arg must be a list, got {:?}", other),
));
}
};
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: cannot take {name} of an empty list"),
));
}
let is_min = builtin == Some(Builtin::Min);
let mut best: Option<f64> = None;
for item in items.iter() {
match item {
Value::Number(n) => {
if n.is_nan() {
return Ok(Value::Number(f64::NAN));
}
best = Some(match best {
None => *n,
Some(cur) => {
if is_min {
cur.min(*n)
} else {
cur.max(*n)
}
}
});
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: list elements must be numbers, got {:?}", other),
));
}
}
}
return Ok(Value::Number(best.unwrap()));
}
if matches!(builtin, Some(Builtin::Flr | Builtin::Cel | Builtin::Rou)) && args.len() == 1 {
return match &args[0] {
Value::Number(n) => {
let result = match builtin {
Some(Builtin::Flr) => n.floor(),
Some(Builtin::Cel) => n.ceil(),
_ => n.round(),
};
Ok(Value::Number(result))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("{} requires a number, got {:?}", name, other),
)),
};
}
if matches!(
builtin,
Some(
Builtin::Sqrt
| Builtin::Log
| Builtin::Exp
| Builtin::Sin
| Builtin::Cos
| Builtin::Tan
| Builtin::Log10
| Builtin::Log2
| Builtin::Asin
| Builtin::Acos
| Builtin::Atan
)
) && args.len() == 1
{
return match &args[0] {
Value::Number(n) => {
let result = match builtin {
Some(Builtin::Sqrt) => n.sqrt(),
Some(Builtin::Log) => n.ln(),
Some(Builtin::Exp) => n.exp(),
Some(Builtin::Sin) => n.sin(),
Some(Builtin::Cos) => n.cos(),
Some(Builtin::Tan) => n.tan(),
Some(Builtin::Log10) => n.log10(),
Some(Builtin::Log2) => n.log2(),
Some(Builtin::Asin) => n.asin(),
Some(Builtin::Acos) => n.acos(),
_ => n.atan(),
};
Ok(Value::Number(result))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("{} requires a number, got {:?}", name, other),
)),
};
}
if builtin == Some(Builtin::Pow) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a.powf(*b))),
_ => Err(RuntimeError::new(
"ILO-R009",
"pow requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Atan2) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(y), Value::Number(x)) => Ok(Value::Number(y.atan2(*x))),
_ => Err(RuntimeError::new(
"ILO-R009",
"atan2 requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Pi) && args.is_empty() {
return Ok(Value::Number(std::f64::consts::PI));
}
if builtin == Some(Builtin::Tau) && args.is_empty() {
return Ok(Value::Number(std::f64::consts::TAU));
}
if builtin == Some(Builtin::Eu) && args.is_empty() {
return Ok(Value::Number(std::f64::consts::E));
}
if builtin == Some(Builtin::Now) && args.is_empty() {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64();
return Ok(Value::Number(ts));
}
if builtin == Some(Builtin::NowMs) && args.is_empty() {
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as f64;
return Ok(Value::Number(ms));
}
if builtin == Some(Builtin::Sleep) && args.len() == 1 {
return match &args[0] {
Value::Number(ms) => {
let clamped = if ms.is_finite() && *ms > 0.0 {
ms.min(u64::MAX as f64) as u64
} else {
0
};
if clamped > 0 {
std::thread::sleep(std::time::Duration::from_millis(clamped));
}
Ok(Value::Nil)
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("sleep requires a number of milliseconds, got {other:?}"),
)),
};
}
if builtin == Some(Builtin::Spawn) && !args.is_empty() {
let fn_name = match &args[0] {
Value::FnRef(n) => n.clone(),
Value::Text(n) => (**n).clone(),
Value::Closure { fn_name, .. } => fn_name.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"spawn: first arg must be a function reference, got {:?}",
other
),
));
}
};
let captures: Vec<Value> = match &args[0] {
Value::Closure { captures, .. } => captures.clone(),
_ => Vec::new(),
};
let forwarded: Vec<Value> = args.iter().skip(1).cloned().collect();
let fns_snapshot = env.functions.clone();
let sum_variants_snapshot = env.sum_variants.clone();
let caps_snapshot = Arc::clone(&env.caps);
let fn_name_for_thread = fn_name.clone();
std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut worker_env = Env::with_caps(caps_snapshot);
worker_env.functions = fns_snapshot;
worker_env.sum_variants = sum_variants_snapshot;
let mut call_args = forwarded;
call_args.extend(captures);
call_function(&mut worker_env, &fn_name_for_thread, call_args)
}));
match result {
Ok(Ok(_)) => {}
Ok(Err(e)) => {
eprintln!("spawn: thread '{}' errored: {}", fn_name, e.message);
}
Err(panic) => {
let msg = if let Some(s) = panic.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
};
eprintln!("spawn: thread '{}' panicked: {}", fn_name, msg);
}
}
});
return Ok(Value::Nil);
}
if builtin == Some(Builtin::Rndn) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(mu), Value::Number(sigma)) => {
Ok(Value::Number(crate::rng::normal(*mu, *sigma)))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"rndn requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Dtfmt) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(epoch), Value::Text(fmt_str)) => {
if !epoch.is_finite() {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"dtfmt: epoch is not finite ({epoch})"
))))));
}
if *epoch < i64::MIN as f64 || *epoch > i64::MAX as f64 {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"dtfmt: epoch out of range ({epoch})"
))))));
}
let secs = *epoch as i64;
match chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0) {
Some(dt) => {
let formatted = dt.format(fmt_str.as_str()).to_string();
Ok(Value::Ok(Box::new(Value::Text(Arc::new(formatted)))))
}
None => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"dtfmt: timestamp out of range ({secs})"
)))))),
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"dtfmt requires a number (epoch) and text (format)".to_string(),
)),
};
}
if builtin == Some(Builtin::Dtparse) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Text(text), Value::Text(fmt_str)) => {
let parsed = chrono::NaiveDateTime::parse_from_str(text, fmt_str)
.map(|ndt| ndt.and_utc().timestamp() as f64)
.or_else(|_| {
chrono::NaiveDate::parse_from_str(text, fmt_str)
.map(|nd| nd.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp() as f64)
});
match parsed {
Ok(n) => Ok(Value::Ok(Box::new(Value::Number(n)))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"dtparse: {e}"
)))))),
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"dtparse requires two text args".to_string(),
)),
};
}
if builtin == Some(Builtin::DtparseRel) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Text(phrase), Value::Number(now_epoch)) => {
Ok(dtparse_rel(phrase.as_str(), *now_epoch))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"dtparse-rel requires text phrase and number epoch".to_string(),
)),
};
}
if builtin == Some(Builtin::Rnd) {
if args.is_empty() {
return Ok(Value::Number(crate::rng::f64()));
}
if args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => {
let lo = *a as i64;
let hi = *b as i64;
if lo > hi {
return Err(RuntimeError::new(
"ILO-R009",
format!("rnd: lower bound {} > upper bound {}", lo, hi),
));
}
Ok(Value::Number(crate::rng::i64_range(lo, hi) as f64))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"rnd requires two numbers".to_string(),
)),
};
}
}
if builtin == Some(Builtin::RandBytes) && args.len() == 1 {
return eval_rand_bytes(&args[0]);
}
if builtin == Some(Builtin::Seed) && args.len() == 1 {
return match &args[0] {
Value::Number(n) => {
crate::rng::seed(*n as u64);
Ok(Value::Nil)
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("seed requires a number, got {other:?}"),
)),
};
}
if builtin == Some(Builtin::Spl) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Text(s), Value::Text(sep)) => {
let parts: Vec<Value> = s
.split(sep.as_str())
.map(|p| Value::Text(Arc::new(p.to_string())))
.collect();
Ok(Value::List(Arc::new(parts)))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"spl requires two text args".to_string(),
)),
};
}
if builtin == Some(Builtin::Cat) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::List(items), Value::Text(sep)) => {
let mut parts: Vec<String> = Vec::new();
for item in items.iter() {
match item {
Value::Text(s) => parts.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cat: list items must be text, got {:?}", other),
));
}
}
}
Ok(Value::Text(Arc::new(parts.join(sep.as_str()))))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"cat requires a list and text separator".to_string(),
)),
};
}
if builtin == Some(Builtin::Has) && args.len() == 2 {
return match &args[0] {
Value::List(items) => Ok(Value::Bool(items.contains(&args[1]))),
Value::Text(s) => match &args[1] {
Value::Text(needle) => Ok(Value::Bool(s.contains(needle.as_str()))),
other => Err(RuntimeError::new(
"ILO-R009",
format!("has: text search requires text needle, got {:?}", other),
)),
},
other => Err(RuntimeError::new(
"ILO-R009",
format!("has requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Hd) && args.len() == 1 {
return match &args[0] {
Value::List(items) => {
if items.is_empty() {
Err(RuntimeError::new("ILO-R009", "hd: empty list".to_string()))
} else {
Ok(items[0].clone())
}
}
Value::Text(s) => {
if s.is_empty() {
Err(RuntimeError::new("ILO-R009", "hd: empty text".to_string()))
} else {
Ok(Value::Text(Arc::new(s.chars().next().unwrap().to_string())))
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("hd requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::At) && args.len() == 2 {
let i = match &args[1] {
Value::Number(n) => n.floor() as i64,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("at: index must be a number, got {:?}", other),
));
}
};
return match &args[0] {
Value::List(items) => {
let len = items.len() as i64;
let adjusted = if i < 0 { i + len } else { i };
if adjusted < 0 || adjusted >= len {
Err(RuntimeError::new(
"ILO-R009",
format!(
"at: index {i} out of range for list of length {}",
items.len()
),
))
} else {
Ok(items[adjusted as usize].clone())
}
}
Value::Text(s) => match char_at_signed(s, i) {
CharAtResult::Found(c) => Ok(Value::Text(Arc::new(c.to_string()))),
CharAtResult::OutOfRange { len } => Err(RuntimeError::new(
"ILO-R009",
format!("at: index {i} out of range for text of length {len}"),
)),
},
other => Err(RuntimeError::new(
"ILO-R009",
format!("at requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::LgetOr) && args.len() == 3 {
let i = match &args[1] {
Value::Number(n) => n.floor() as i64,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("lget-or: index must be a number, got {:?}", other),
));
}
};
return match &args[0] {
Value::List(items) => {
let len = items.len() as i64;
let adjusted = if i < 0 { i + len } else { i };
if adjusted < 0 || adjusted >= len {
Ok(args[2].clone())
} else {
Ok(items[adjusted as usize].clone())
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"lget-or: expects a list".to_string(),
)),
};
}
if builtin == Some(Builtin::DefaultOnErr) && args.len() == 2 {
return match &args[0] {
Value::Ok(inner) => Ok(*inner.clone()),
Value::Err(_) => Ok(args[1].clone()),
other => Err(RuntimeError::new(
"ILO-R009",
format!(
"default-on-err: first argument must be R T E (Ok or Err), got {:?}",
other
),
)),
};
}
if builtin == Some(Builtin::Urlenc) && args.len() == 1 {
return urlenc_impl(&args[0]);
}
if builtin == Some(Builtin::Urldec) && args.len() == 1 {
return urldec_impl(&args[0]);
}
if builtin == Some(Builtin::Idxof) && args.len() == 2 {
return idxof_impl(&args[0], &args[1]);
}
if builtin == Some(Builtin::B64u) && args.len() == 1 {
return b64u_impl(&args[0]);
}
if builtin == Some(Builtin::B64uDec) && args.len() == 1 {
return b64u_dec_impl(&args[0]);
}
if builtin == Some(Builtin::Sha256) && args.len() == 1 {
return sha256_impl(&args[0]);
}
if builtin == Some(Builtin::HmacSha256) && args.len() == 2 {
return hmac_sha256_impl(&args[0], &args[1]);
}
if builtin == Some(Builtin::B64) && args.len() == 1 {
return b64_impl(&args[0]);
}
if builtin == Some(Builtin::B64Dec) && args.len() == 1 {
return b64_dec_impl(&args[0]);
}
if builtin == Some(Builtin::HexEnc) && args.len() == 1 {
return hex_impl(&args[0]);
}
if builtin == Some(Builtin::CtEq) && args.len() == 2 {
return ct_eq_impl(&args[0], &args[1]);
}
if builtin == Some(Builtin::Sha256Hex) && args.len() == 1 {
return sha256_hex_impl(&args[0]);
}
if builtin == Some(Builtin::Sha256d) && args.len() == 1 {
return sha256d_impl(&args[0]);
}
if builtin == Some(Builtin::HexRev) && args.len() == 1 {
return hex_rev_impl(&args[0]);
}
if builtin == Some(Builtin::Tokcount) && args.len() == 1 {
return tokcount_impl(&args[0]);
}
if builtin == Some(Builtin::Lst) && args.len() == 3 {
let idx = match &args[1] {
Value::Number(n) => {
if *n < 0.0 || n.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
"lst: index must be a non-negative integer".to_string(),
));
}
*n as usize
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("lst: index must be a number, got {:?}", other),
));
}
};
return match &args[0] {
Value::List(items) => {
if idx >= items.len() {
Err(RuntimeError::new(
"ILO-R009",
format!(
"lst: index {idx} out of range for list of length {}",
items.len()
),
))
} else {
let mut new_items = (**items).clone();
new_items[idx] = args[2].clone();
Ok(Value::List(Arc::new(new_items)))
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("lst requires a list, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Window) && args.len() == 2 {
let n = match &args[0] {
Value::Number(v) => {
if !v.is_finite() || *v <= 0.0 || v.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("window: size must be a positive integer, got {}", v),
));
}
*v as usize
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("window: size must be a number, got {:?}", other),
));
}
};
let xs = match &args[1] {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("window arg 2 requires a list, got {:?}", other),
));
}
};
if n > xs.len() {
return Ok(Value::List(Arc::new(vec![])));
}
let mut out = Vec::with_capacity(xs.len() - n + 1);
for w in xs.windows(n) {
out.push(Value::List(Arc::new(w.to_vec())));
}
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Zip) && args.len() == 2 {
let xs = match &args[0] {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("zip arg 1 requires a list, got {:?}", other),
));
}
};
let ys = match &args[1] {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("zip arg 2 requires a list, got {:?}", other),
));
}
};
let n = xs.len().min(ys.len());
let mut out = Vec::with_capacity(n);
for i in 0..n {
out.push(Value::List(Arc::new(vec![xs[i].clone(), ys[i].clone()])));
}
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Enumerate) && args.len() == 1 {
let xs = match &args[0] {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("enumerate requires a list, got {:?}", other),
));
}
};
let mut out = Vec::with_capacity(xs.len());
for (i, v) in xs.iter().enumerate() {
out.push(Value::List(Arc::new(vec![
Value::Number(i as f64),
v.clone(),
])));
}
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Range) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => {
if a.fract() != 0.0 || b.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
"range: bounds must be integers".to_string(),
));
}
let start = *a as i64;
let end = *b as i64;
if start >= end {
return Ok(Value::List(Arc::new(Vec::new())));
}
let len = (end - start) as u64;
if len > 1_000_000 {
return Err(RuntimeError::new(
"ILO-R009",
format!("range too large: {len} elements (max 1000000)"),
));
}
let mut out = Vec::with_capacity(len as usize);
for i in start..end {
out.push(Value::Number(i as f64));
}
Ok(Value::List(Arc::new(out)))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"range requires two numbers".to_string(),
)),
};
}
if builtin == Some(Builtin::Linspace) && args.len() == 3 {
let (a, b, n_raw) = match (&args[0], &args[1], &args[2]) {
(Value::Number(a), Value::Number(b), Value::Number(n)) => (*a, *b, *n),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"linspace requires three numbers (a b n)".to_string(),
));
}
};
if n_raw.fract() != 0.0 || n_raw < 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("linspace: n must be a non-negative integer, got {n_raw}"),
));
}
let n = n_raw as u64;
if n > 1_000_000 {
return Err(RuntimeError::new(
"ILO-R009",
format!("linspace too large: {n} elements (max 1000000)"),
));
}
if n == 0 {
return Ok(Value::List(Arc::new(Vec::new())));
}
if n == 1 {
return Ok(Value::List(Arc::new(vec![Value::Number(a)])));
}
let mut out = Vec::with_capacity(n as usize);
let step = (b - a) / ((n - 1) as f64);
for i in 0..n {
out.push(Value::Number(a + step * (i as f64)));
}
if let Some(last) = out.last_mut() {
*last = Value::Number(b);
}
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Ones) && args.len() == 1 {
let n_raw = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ones: count must be a number, got {:?}", other),
));
}
};
if n_raw.fract() != 0.0 || n_raw < 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("ones: count must be a non-negative integer, got {n_raw}"),
));
}
let n = n_raw as u64;
if n > 1_000_000 {
return Err(RuntimeError::new(
"ILO-R009",
format!("ones too large: {n} elements (max 1000000)"),
));
}
let out = vec![Value::Number(1.0); n as usize];
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Rep) && args.len() == 2 {
let n_raw = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rep: count must be a number, got {:?}", other),
));
}
};
if n_raw.fract() != 0.0 || n_raw < 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("rep: count must be a non-negative integer, got {n_raw}"),
));
}
let n = n_raw as u64;
if n > 1_000_000 {
return Err(RuntimeError::new(
"ILO-R009",
format!("rep too large: {n} elements (max 1000000)"),
));
}
let v = &args[1];
let out = vec![v.clone(); n as usize];
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Zeros) && args.len() == 1 {
let n_raw = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("zeros: count must be a number, got {:?}", other),
));
}
};
if n_raw.fract() != 0.0 || n_raw < 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("zeros: count must be a non-negative integer, got {n_raw}"),
));
}
let n = n_raw as u64;
if n > 1_000_000 {
return Err(RuntimeError::new(
"ILO-R009",
format!("zeros too large: {n} elements (max 1000000)"),
));
}
let out = vec![Value::Number(0.0); n as usize];
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Arange) && args.len() == 3 {
let (start, stop, step) = match (&args[0], &args[1], &args[2]) {
(Value::Number(a), Value::Number(b), Value::Number(s)) => (*a, *b, *s),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"arange requires three numbers (start stop step)".to_string(),
));
}
};
if step <= 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("arange: step must be positive, got {step}"),
));
}
if start >= stop {
return Ok(Value::List(Arc::new(Vec::new())));
}
let n = ((stop - start) / step).ceil() as u64;
if n > 1_000_000 {
return Err(RuntimeError::new(
"ILO-R009",
format!("arange too large: {n} elements (max 1000000)"),
));
}
let mut out = Vec::with_capacity(n as usize);
let mut i = 0u64;
loop {
let v = start + step * (i as f64);
if v >= stop {
break;
}
out.push(Value::Number(v));
i += 1;
if i > 1_000_000 {
break;
}
}
return Ok(Value::List(Arc::new(out)));
}
#[inline(never)]
fn vstack_run(matrices_val: &Value) -> Result<Value> {
let matrices = match matrices_val {
Value::List(xs) => xs.clone(),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"vstack: argument must be a list of matrices".to_string(),
));
}
};
let mut out: Vec<Value> = Vec::new();
for item in matrices.iter() {
match item {
Value::List(rows) => {
for row in rows.iter() {
out.push(row.clone());
}
}
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"vstack: each element must be a list (matrix or vector)".to_string(),
));
}
}
}
Ok(Value::List(Arc::new(out)))
}
if builtin == Some(Builtin::Vstack) && args.len() == 1 {
return vstack_run(&args[0]);
}
#[inline(never)]
fn hstack_run(matrices_val: &Value) -> Result<Value> {
let matrices = match matrices_val {
Value::List(xs) => xs.clone(),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"hstack: argument must be a list of matrices".to_string(),
));
}
};
if matrices.is_empty() {
return Ok(Value::List(Arc::new(Vec::new())));
}
let mut mats: Vec<Arc<Vec<Value>>> = Vec::with_capacity(matrices.len());
for item in matrices.iter() {
match item {
Value::List(rows) => mats.push(rows.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"hstack: each element must be a list (matrix)".to_string(),
));
}
}
}
let n_rows = mats[0].len();
for m in &mats {
if m.len() != n_rows {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"hstack: all matrices must have the same number of rows; expected {n_rows}, got {}",
m.len()
),
));
}
}
let mut out = Vec::with_capacity(n_rows);
for r in 0..n_rows {
let mut row: Vec<Value> = Vec::new();
for m in &mats {
match &m[r] {
Value::List(cols) => {
for col in cols.iter() {
row.push(col.clone());
}
}
other => row.push(other.clone()),
}
}
out.push(Value::List(Arc::new(row)));
}
Ok(Value::List(Arc::new(out)))
}
if builtin == Some(Builtin::Hstack) && args.len() == 1 {
return hstack_run(&args[0]);
}
#[inline(never)]
fn column_stack_run(vecs_val: &Value) -> Result<Value> {
let vecs = match vecs_val {
Value::List(xs) => xs.clone(),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"column-stack: argument must be a list of vectors".to_string(),
));
}
};
if vecs.is_empty() {
return Ok(Value::List(Arc::new(Vec::new())));
}
let mut cols: Vec<Arc<Vec<Value>>> = Vec::with_capacity(vecs.len());
for item in vecs.iter() {
match item {
Value::List(col) => cols.push(col.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"column-stack: each element must be a list (vector)".to_string(),
));
}
}
}
let n_rows = cols[0].len();
for col in &cols {
if col.len() != n_rows {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"column-stack: all vectors must have the same length; expected {n_rows}, got {}",
col.len()
),
));
}
}
let mut out = Vec::with_capacity(n_rows);
for r in 0..n_rows {
let row: Vec<Value> = cols.iter().map(|col| col[r].clone()).collect();
out.push(Value::List(Arc::new(row)));
}
Ok(Value::List(Arc::new(out)))
}
if builtin == Some(Builtin::ColumnStack) && args.len() == 1 {
return column_stack_run(&args[0]);
}
#[inline(never)]
fn hist_run(xs_val: &Value, n_bins_val: &Value) -> Result<Value> {
let xs = match xs_val {
Value::List(xs) => xs.clone(),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"hist: first argument must be a numeric list".to_string(),
));
}
};
let n_bins_raw = match n_bins_val {
Value::Number(n) => *n,
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"hist: n_bins must be a number".to_string(),
));
}
};
if n_bins_raw.fract() != 0.0 || n_bins_raw <= 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
format!("hist: n_bins must be a positive integer, got {n_bins_raw}"),
));
}
let n_bins = n_bins_raw as usize;
if n_bins > 1_000_000 {
return Err(RuntimeError::new(
"ILO-R009",
format!("hist: n_bins too large: {n_bins} (max 1000000)"),
));
}
let mut counts = vec![Value::Number(0.0); n_bins];
if xs.is_empty() {
return Ok(Value::List(Arc::new(counts)));
}
let mut vals: Vec<f64> = Vec::with_capacity(xs.len());
for v in xs.iter() {
match v {
Value::Number(n) => vals.push(*n),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"hist: list elements must all be numbers".to_string(),
));
}
}
}
let mn = vals.iter().cloned().fold(f64::INFINITY, f64::min);
let mx = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let range = mx - mn;
for &v in &vals {
let bin = if range == 0.0 {
0
} else {
let b = ((v - mn) / range * (n_bins as f64)).floor() as usize;
b.min(n_bins - 1)
};
if let Value::Number(ref mut c) = counts[bin] {
*c += 1.0;
}
}
Ok(Value::List(Arc::new(counts)))
}
if builtin == Some(Builtin::Hist) && args.len() == 2 {
return hist_run(&args[0], &args[1]);
}
if builtin == Some(Builtin::Chunks) && args.len() == 2 {
let n_raw = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("chunks: size must be a number, got {:?}", other),
));
}
};
return chunks_run(n_raw, &args[1]);
}
if builtin == Some(Builtin::Setunion) && args.len() == 2 {
return setops_run(builtin, &args[0], &args[1]);
}
if builtin == Some(Builtin::Setinter) && args.len() == 2 {
return setops_run(builtin, &args[0], &args[1]);
}
if builtin == Some(Builtin::Setdiff) && args.len() == 2 {
return setops_run(builtin, &args[0], &args[1]);
}
if builtin == Some(Builtin::Tl) && args.len() == 1 {
return match &args[0] {
Value::List(items) => {
if items.is_empty() {
Err(RuntimeError::new("ILO-R009", "tl: empty list".to_string()))
} else {
Ok(Value::List(Arc::new(items[1..].to_vec())))
}
}
Value::Text(s) => {
if s.is_empty() {
Err(RuntimeError::new("ILO-R009", "tl: empty text".to_string()))
} else {
let mut chars = s.chars();
chars.next();
Ok(Value::Text(Arc::new(chars.collect())))
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("tl requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Rev) && args.len() == 1 {
let mut it = args.into_iter();
let v = it.next().unwrap();
return match v {
Value::List(mut items) => {
let inner = Arc::make_mut(&mut items);
inner.reverse();
Ok(Value::List(items))
}
Value::Text(mut s) => {
let inner = Arc::make_mut(&mut s);
let reversed: String = inner.chars().rev().collect();
*inner = reversed;
Ok(Value::Text(s))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("rev requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Srt) && args.len() == 1 {
let mut it = args.into_iter();
let v = it.next().unwrap();
return match v {
Value::List(mut items) => {
if items.is_empty() {
return Ok(Value::List(items));
}
let all_numbers = items.iter().all(|v| matches!(v, Value::Number(_)));
let all_text = items.iter().all(|v| matches!(v, Value::Text(_)));
if all_numbers {
let inner = Arc::make_mut(&mut items);
inner.sort_by(|a, b| {
if let (Value::Number(x), Value::Number(y)) = (a, b) {
x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
} else {
unreachable!()
}
});
Ok(Value::List(items))
} else if all_text {
let inner = Arc::make_mut(&mut items);
inner.sort_by(|a, b| {
if let (Value::Text(x), Value::Text(y)) = (a, b) {
x.cmp(y)
} else {
unreachable!()
}
});
Ok(Value::List(items))
} else {
Err(RuntimeError::new(
"ILO-R009",
"srt: list must contain all numbers or all text".to_string(),
))
}
}
Value::Text(mut s) => {
let inner = Arc::make_mut(&mut s);
let mut chars: Vec<char> = inner.chars().collect();
chars.sort();
*inner = chars.into_iter().collect();
Ok(Value::Text(s))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("srt requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Srt) && (args.len() == 2 || args.len() == 3) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"srt: key arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let (ctx, list_arg) = if args.len() == 3 {
(Some(args[1].clone()), &args[2])
} else {
(None, &args[1])
};
let items = match list_arg {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("srt: list arg must be a list, got {:?}", other),
));
}
};
let owned_items: Vec<Value> = Arc::try_unwrap(items).unwrap_or_else(|arc| (*arc).clone());
let mut keyed: Vec<(Value, Value)> = owned_items
.into_iter()
.map(|item| {
let mut call_args = match &ctx {
Some(c) => vec![item.clone(), c.clone()],
None => vec![item.clone()],
};
call_args.extend(captures.iter().cloned());
let key = call_function(env, &fn_name, call_args)?;
Ok((key, item))
})
.collect::<Result<_>>()?;
keyed.sort_by(|(ka, _), (kb, _)| match (ka, kb) {
(Value::Number(a), Value::Number(b)) => {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
}
(Value::Text(a), Value::Text(b)) => a.cmp(b),
_ => std::cmp::Ordering::Equal,
});
return Ok(Value::List(Arc::new(
keyed.into_iter().map(|(_, v)| v).collect(),
)));
}
if builtin == Some(Builtin::Rsrt) && args.len() == 1 {
return match &args[0] {
Value::List(items) => {
if items.is_empty() {
return Ok(Value::List(Arc::new(vec![])));
}
let all_numbers = items.iter().all(|v| matches!(v, Value::Number(_)));
let all_text = items.iter().all(|v| matches!(v, Value::Text(_)));
if all_numbers {
let mut sorted: Vec<Value> = (**items).clone();
sorted.sort_by(|a, b| {
if let (Value::Number(x), Value::Number(y)) = (a, b) {
y.partial_cmp(x).unwrap_or(std::cmp::Ordering::Equal)
} else {
unreachable!()
}
});
Ok(Value::List(Arc::new(sorted)))
} else if all_text {
let mut sorted: Vec<Value> = (**items).clone();
sorted.sort_by(|a, b| {
if let (Value::Text(x), Value::Text(y)) = (a, b) {
y.cmp(x)
} else {
unreachable!()
}
});
Ok(Value::List(Arc::new(sorted)))
} else {
Err(RuntimeError::new(
"ILO-R009",
"rsrt: list must contain all numbers or all text".to_string(),
))
}
}
Value::Text(s) => {
let mut chars: Vec<char> = s.chars().collect();
chars.sort_by(|a, b| b.cmp(a));
Ok(Value::Text(Arc::new(chars.into_iter().collect())))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("rsrt requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Rsrt) && (args.len() == 2 || args.len() == 3) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"rsrt: key arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let (ctx, list_arg) = if args.len() == 3 {
(Some(args[1].clone()), &args[2])
} else {
(None, &args[1])
};
let items = match list_arg {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rsrt: list arg must be a list, got {:?}", other),
));
}
};
let owned_items: Vec<Value> = Arc::try_unwrap(items).unwrap_or_else(|arc| (*arc).clone());
let mut keyed: Vec<(Value, Value)> = owned_items
.into_iter()
.map(|item| {
let mut call_args = match &ctx {
Some(c) => vec![item.clone(), c.clone()],
None => vec![item.clone()],
};
call_args.extend(captures.iter().cloned());
let key = call_function(env, &fn_name, call_args)?;
Ok((key, item))
})
.collect::<Result<_>>()?;
keyed.sort_by(|(ka, _), (kb, _)| match (ka, kb) {
(Value::Number(a), Value::Number(b)) => {
b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
}
(Value::Text(a), Value::Text(b)) => b.cmp(a),
_ => std::cmp::Ordering::Equal,
});
return Ok(Value::List(Arc::new(
keyed.into_iter().map(|(_, v)| v).collect(),
)));
}
if builtin == Some(Builtin::Slc) && args.len() == 3 {
let start_raw = match &args[1] {
Value::Number(n) => {
if n.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
"slc: start index must be an integer".to_string(),
));
}
*n as i64
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("slc: start index must be a number, got {:?}", other),
));
}
};
let end_raw = match &args[2] {
Value::Number(n) => {
if n.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
"slc: end index must be an integer".to_string(),
));
}
*n as i64
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("slc: end index must be a number, got {:?}", other),
));
}
};
return match &args[0] {
Value::List(items) => {
let len = items.len();
let end = crate::builtins::resolve_slc_end(start_raw, end_raw, len);
let start = crate::builtins::resolve_slice_bound(start_raw, len).min(end);
Ok(Value::List(Arc::new(items[start..end].to_vec())))
}
Value::Text(s) => {
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let end = crate::builtins::resolve_slc_end(start_raw, end_raw, len);
let start = crate::builtins::resolve_slice_bound(start_raw, len).min(end);
Ok(Value::Text(Arc::new(chars[start..end].iter().collect())))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("slc requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Take) && args.len() == 2 {
let n = match &args[0] {
Value::Number(n) => {
if n.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
"take: count must be an integer".to_string(),
));
}
*n as i64
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("take: count must be a number, got {:?}", other),
));
}
};
return match &args[1] {
Value::List(items) => {
let end = crate::builtins::resolve_take_count(n, items.len());
Ok(Value::List(Arc::new(items[..end].to_vec())))
}
Value::Text(s) => {
let chars: Vec<char> = s.chars().collect();
let end = crate::builtins::resolve_take_count(n, chars.len());
Ok(Value::Text(Arc::new(chars[..end].iter().collect())))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("take requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Drop) && args.len() == 2 {
let n = match &args[0] {
Value::Number(n) => {
if n.fract() != 0.0 {
return Err(RuntimeError::new(
"ILO-R009",
"drop: count must be an integer".to_string(),
));
}
*n as i64
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("drop: count must be a number, got {:?}", other),
));
}
};
return match &args[1] {
Value::List(items) => {
let start = crate::builtins::resolve_drop_count(n, items.len());
Ok(Value::List(Arc::new(items[start..].to_vec())))
}
Value::Text(s) => {
let chars: Vec<char> = s.chars().collect();
let start = crate::builtins::resolve_drop_count(n, chars.len());
Ok(Value::Text(Arc::new(chars[start..].iter().collect())))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("drop requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Get) && (args.len() == 1 || args.len() == 2) {
let url = match &args[0] {
Value::Text(u) => u.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("get requires text (url), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let headers = if args.len() == 2 {
match &args[1] {
Value::Map(m) => m
.iter()
.map(|(k, v)| {
let vs: String = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect::<Vec<_>>(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("get headers must be M t t, got {:?}", other),
));
}
}
} else {
vec![]
};
return {
let backend = http_wasm::default_backend();
let result = backend.get(url.as_str(), &headers);
Ok(http_wasm::result_to_value(result))
};
}
if builtin == Some(Builtin::GetMany) && args.len() == 1 {
let urls: Vec<String> = match &args[0] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"get-many requires L t (list of urls); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("get-many requires L t (list of urls), got {:?}", other),
));
}
};
for url in &urls {
if let Err(msg) = env.caps.check_net(url) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
}
return Ok(Value::List(Arc::new(get_many_fetch(&urls))));
}
if builtin == Some(Builtin::Post) && (args.len() == 2 || args.len() == 3) {
let (url, body) = match (&args[0], &args[1]) {
(Value::Text(u), Value::Text(b)) => (u.clone(), b.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pst requires (t, t), got ({:?}, {:?})", args[0], args[1]),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let headers = if args.len() == 3 {
match &args[2] {
Value::Map(m) => m
.iter()
.map(|(k, v)| {
let vs: String = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect::<Vec<_>>(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pst headers must be M t t, got {:?}", other),
));
}
}
} else {
vec![]
};
return {
let backend = http_wasm::default_backend();
let result = backend.post(url.as_str(), body.as_str(), &headers);
Ok(http_wasm::result_to_value(result))
};
}
if builtin == Some(Builtin::GetStream) && args.len() == 1 {
return http_stream_get_dispatch(env, &args[0], None);
}
if builtin == Some(Builtin::GetStreamH) && args.len() == 2 {
return http_stream_get_dispatch(env, &args[0], Some(&args[1]));
}
if builtin == Some(Builtin::PostStream) && args.len() == 2 {
return http_stream_post_dispatch(env, &args[0], &args[1], None);
}
if builtin == Some(Builtin::PostStreamH) && args.len() == 3 {
return http_stream_post_dispatch(env, &args[0], &args[1], Some(&args[2]));
}
if builtin == Some(Builtin::Put) && (args.len() == 2 || args.len() == 3) {
return put_pat_run(env, builtin, args);
}
if builtin == Some(Builtin::Pat) && (args.len() == 2 || args.len() == 3) {
return put_pat_run(env, builtin, args);
}
if builtin == Some(Builtin::Del) && (args.len() == 1 || args.len() == 2) {
return del_hed_opt_run(env, builtin, args);
}
if builtin == Some(Builtin::Hed) && (args.len() == 1 || args.len() == 2) {
return del_hed_opt_run(env, builtin, args);
}
if builtin == Some(Builtin::Opt) && (args.len() == 1 || args.len() == 2) {
return del_hed_opt_run(env, builtin, args);
}
if builtin == Some(Builtin::GetTo) && args.len() == 2 {
let url = match &args[0] {
Value::Text(u) => u.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("get-to requires text (url), got {:?}", other),
));
}
};
let timeout_ms = match &args[1] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("get-to requires n (timeout-ms), got {:?}", other),
));
}
};
let timeout_secs = ((timeout_ms / 1000.0).ceil() as u64).max(1);
return {
#[cfg(feature = "http")]
{
let req = minreq::get(url.as_str()).with_timeout(timeout_secs);
match req.send() {
Ok(resp) => match resp.as_str() {
Ok(body) => {
Ok(Value::Ok(Box::new(Value::Text(Arc::new(body.to_string())))))
}
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"response is not valid UTF-8: {e}"
)))))),
},
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[cfg(not(feature = "http"))]
{
let _ = (url, timeout_secs);
Ok(Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
))))
}
};
}
if builtin == Some(Builtin::PstTo) && args.len() == 3 {
let (url, body) = match (&args[0], &args[1]) {
(Value::Text(u), Value::Text(b)) => (u.clone(), b.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"pst-to requires (t, t, n), got ({:?}, {:?}, {:?})",
args[0], args[1], args[2]
),
));
}
};
let timeout_ms = match &args[2] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pst-to requires n (timeout-ms), got {:?}", other),
));
}
};
let timeout_secs = ((timeout_ms / 1000.0).ceil() as u64).max(1);
return {
#[cfg(feature = "http")]
{
let req = minreq::post(url.as_str())
.with_body(body.as_str())
.with_timeout(timeout_secs);
match req.send() {
Ok(resp) => match resp.as_str() {
Ok(b) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(b.to_string()))))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"response is not valid UTF-8: {e}"
)))))),
},
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[cfg(not(feature = "http"))]
{
let _ = (url, body, timeout_secs);
Ok(Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
))))
}
};
}
if builtin == Some(Builtin::Getx) && (args.len() == 1 || args.len() == 2) {
let url = match &args[0] {
Value::Text(u) => u.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("getx requires text (url), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let headers = if args.len() == 2 {
match &args[1] {
Value::Map(m) => m
.iter()
.map(|(k, v)| {
let vs: String = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect::<Vec<_>>(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("getx headers must be M t t, got {:?}", other),
));
}
}
} else {
vec![]
};
return {
#[cfg(feature = "http")]
{
let mut req = minreq::get(url.as_str());
for (k, v) in &headers {
req = req.with_header(k.as_str(), v.as_str());
}
match req.send() {
Ok(resp) => Ok(http_response_to_ok_map(&resp)),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[cfg(not(feature = "http"))]
{
let _ = (url, headers);
Ok(Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
))))
}
};
}
if builtin == Some(Builtin::Pstx) && (args.len() == 2 || args.len() == 3) {
let (url, body) = match (&args[0], &args[1]) {
(Value::Text(u), Value::Text(b)) => (u.clone(), b.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pstx requires (t, t), got ({:?}, {:?})", args[0], args[1]),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let headers = if args.len() == 3 {
match &args[2] {
Value::Map(m) => m
.iter()
.map(|(k, v)| {
let vs: String = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect::<Vec<_>>(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pstx headers must be M t t, got {:?}", other),
));
}
}
} else {
vec![]
};
return {
#[cfg(feature = "http")]
{
let mut req = minreq::post(url.as_str()).with_body(body.as_str());
for (k, v) in &headers {
req = req.with_header(k.as_str(), v.as_str());
}
match req.send() {
Ok(resp) => Ok(http_response_to_ok_map(&resp)),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
#[cfg(not(feature = "http"))]
{
let _ = (url, body, headers);
Ok(Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
))))
}
};
}
if builtin == Some(Builtin::Run) && args.len() == 2 {
let cmd = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run requires text (cmd), got {:?}", other),
));
}
};
let argv: Vec<String> = match &args[1] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run argv must be L t (text list); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run argv must be L t (text list), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_run(cmd.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return Ok(run_spawn(cmd.as_str(), &argv));
}
if builtin == Some(Builtin::Run2) && args.len() == 2 {
let cmd = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run2 requires text (cmd), got {:?}", other),
));
}
};
let argv: Vec<String> = match &args[1] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run2 argv must be L t (text list); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run2 argv must be L t (text list), got {:?}", other),
));
}
};
return Ok(run_spawn_structured(cmd.as_str(), &argv));
}
if builtin == Some(Builtin::Run) && args.len() == 3 {
let cmd = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run requires text (cmd), got {:?}", other),
));
}
};
let argv: Vec<String> = match &args[1] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run argv must be L t (text list); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run argv must be L t (text list), got {:?}", other),
));
}
};
let stdin_text = match &args[2] {
Value::Text(s) => (**s).clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run stdin arg must be t (text), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_run(cmd.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return Ok(run_spawn_with_stdin(cmd.as_str(), &argv, &stdin_text));
}
if builtin == Some(Builtin::Run2) && args.len() == 3 {
let cmd = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run2 requires text (cmd), got {:?}", other),
));
}
};
let argv: Vec<String> = match &args[1] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run2 argv must be L t (text list); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run2 argv must be L t (text list), got {:?}", other),
));
}
};
let stdin_text = match &args[2] {
Value::Text(s) => (**s).clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run2 stdin arg must be t (text), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_run(cmd.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return Ok(run_spawn_structured_with_stdin(
cmd.as_str(),
&argv,
&stdin_text,
));
}
if builtin == Some(Builtin::RunBg) && args.len() == 2 {
let cmd = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run-bg requires text (cmd), got {:?}", other),
));
}
};
let argv: Vec<String> = match &args[1] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run-bg argv must be L t (text list); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run-bg argv must be L t (text list), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_run(cmd.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return Ok(run_spawn_bg(cmd.as_str(), &argv));
}
if builtin == Some(Builtin::RunFullEnv) && args.len() == 2 {
let cmd = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run-full-env requires text (cmd), got {:?}", other),
));
}
};
let argv: Vec<String> = match &args[1] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run-full-env argv must be L t (text list); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run-full-env argv must be L t (text list), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_run(cmd.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return Ok(run_spawn_full_env(cmd.as_str(), &argv));
}
if builtin == Some(Builtin::Run2FullEnv) && args.len() == 2 {
let cmd = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("run2-full-env requires text (cmd), got {:?}", other),
));
}
};
let argv: Vec<String> = match &args[1] {
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, v) in items.iter().enumerate() {
match v {
Value::Text(s) => out.push((**s).clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run2-full-env argv must be L t (text list); element {i} is {:?}",
other
),
));
}
}
}
out
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"run2-full-env argv must be L t (text list), got {:?}",
other
),
));
}
};
if let Err(msg) = env.caps.check_run(cmd.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return Ok(run_spawn_structured_full_env(cmd.as_str(), &argv));
}
if builtin == Some(Builtin::Trm) && args.len() == 1 {
return match &args[0] {
Value::Text(s) => Ok(Value::Text(Arc::new(s.trim().to_string()))),
other => Err(RuntimeError::new(
"ILO-R009",
format!("trm requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Upr) && args.len() == 1 {
return match &args[0] {
Value::Text(s) => Ok(Value::Text(Arc::new(s.to_uppercase()))),
other => Err(RuntimeError::new(
"ILO-R009",
format!("upr requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Lwr) && args.len() == 1 {
return match &args[0] {
Value::Text(s) => Ok(Value::Text(Arc::new(s.to_lowercase()))),
other => Err(RuntimeError::new(
"ILO-R009",
format!("lwr requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Cap) && args.len() == 1 {
return cap_run(&args[0]);
}
if builtin == Some(Builtin::Padl) && (args.len() == 2 || args.len() == 3) {
return padl_padr_run(true, &args);
}
if builtin == Some(Builtin::Padr) && (args.len() == 2 || args.len() == 3) {
return padl_padr_run(false, &args);
}
if builtin == Some(Builtin::Ord) && args.len() == 1 {
return match &args[0] {
Value::Text(s) => match s.chars().next() {
Some(c) => Ok(Value::Number(c as u32 as f64)),
None => Err(RuntimeError::new(
"ILO-R009",
"ord requires a non-empty string".to_string(),
)),
},
other => Err(RuntimeError::new(
"ILO-R009",
format!("ord requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Chr) && args.len() == 1 {
return match &args[0] {
Value::Number(n) => {
if !n.is_finite() || n.fract() != 0.0 || *n < 0.0 || *n > u32::MAX as f64 {
return Err(RuntimeError::new(
"ILO-R009",
format!("chr requires a non-negative integer codepoint, got {n}"),
));
}
let cp = *n as u32;
match char::from_u32(cp) {
Some(c) => Ok(Value::Text(Arc::new(c.to_string()))),
None => Err(RuntimeError::new(
"ILO-R009",
format!("chr: {cp} is not a valid Unicode codepoint"),
)),
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("chr requires number, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Chars) && args.len() == 1 {
return match &args[0] {
Value::Text(s) => Ok(Value::List(Arc::new(
s.chars()
.map(|c| Value::Text(Arc::new(c.to_string())))
.collect(),
))),
other => Err(RuntimeError::new(
"ILO-R009",
format!("chars requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Unq) && args.len() == 1 {
return match &args[0] {
Value::List(xs) => {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for v in xs.iter() {
let key = format!("{v:?}");
if seen.insert(key) {
out.push(v.clone());
}
}
Ok(Value::List(Arc::new(out)))
}
Value::Text(s) => {
let mut seen = std::collections::HashSet::new();
let deduped: String = s.chars().filter(|c| seen.insert(*c)).collect();
Ok(Value::Text(Arc::new(deduped)))
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("unq requires a list or text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Fmt2) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(x), Value::Number(d)) => {
let digits = if !d.is_finite() || *d <= 0.0 {
0usize
} else {
(*d as usize).min(20)
};
Ok(Value::Text(Arc::new(format!("{:.*}", digits, x))))
}
_ => Err(RuntimeError::new(
"ILO-R009",
"fmt2 requires two numbers (x, digits)".to_string(),
)),
};
}
if builtin == Some(Builtin::Fmt) && !args.is_empty() {
return fmt_run(&args);
}
if builtin == Some(Builtin::Ls) && args.len() == 1 {
let dir = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ls requires text path, got {:?}", other),
));
}
};
return match std::fs::read_dir(dir.as_str()) {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
Ok(rd) => {
let mut names: Vec<String> = Vec::new();
for entry in rd {
match entry {
Ok(ent) => {
names.push(ent.file_name().to_string_lossy().into_owned());
}
Err(e) => {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string())))));
}
}
}
names.sort();
let items: Vec<Value> = names
.into_iter()
.map(|n| Value::Text(Arc::new(n)))
.collect();
Ok(Value::Ok(Box::new(Value::List(Arc::new(items)))))
}
};
}
if builtin == Some(Builtin::Walk) && args.len() == 1 {
let dir = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("walk requires text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_read(dir.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let root = std::path::PathBuf::from(dir.as_str());
match walk_collect(&root) {
Ok(out) => {
let items: Vec<Value> = out.into_iter().map(|n| Value::Text(Arc::new(n))).collect();
return Ok(Value::Ok(Box::new(Value::List(Arc::new(items)))));
}
Err(e) => {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(e)))));
}
}
}
if builtin == Some(Builtin::Glob) && args.len() == 2 {
let dir = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("glob requires text path, got {:?}", other),
));
}
};
let pat = match &args[1] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("glob pattern must be text, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_read(dir.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let root = std::path::PathBuf::from(dir.as_str());
match walk_collect(&root) {
Ok(all) => {
let items: Vec<Value> = all
.into_iter()
.filter(|p| glob_match(pat.as_str(), p))
.map(|n| Value::Text(Arc::new(n)))
.collect();
return Ok(Value::Ok(Box::new(Value::List(Arc::new(items)))));
}
Err(e) => {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(e)))));
}
}
}
if builtin == Some(Builtin::Dirname) && args.len() == 1 {
let p = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("dirname requires text path, got {:?}", other),
));
}
};
return Ok(Value::Text(Arc::new(dirname_posix(p.as_str()))));
}
if builtin == Some(Builtin::Basename) && args.len() == 1 {
let p = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("basename requires text path, got {:?}", other),
));
}
};
return Ok(Value::Text(Arc::new(basename_posix(p.as_str()))));
}
if builtin == Some(Builtin::Pathjoin) && args.len() == 1 {
let parts = match &args[0] {
Value::List(xs) => xs.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pathjoin requires list of text, got {:?}", other),
));
}
};
let mut segs: Vec<&str> = Vec::with_capacity(parts.len());
for p in parts.iter() {
match p {
Value::Text(s) => segs.push(s.as_str()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pathjoin requires list of text, got element {:?}", other),
));
}
}
}
return Ok(Value::Text(Arc::new(pathjoin_posix(&segs))));
}
if builtin == Some(Builtin::DurParse) && args.len() == 1 {
let s = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("dur-parse requires text, got {:?}", other),
));
}
};
match dur_parse(s.as_str()) {
Ok(secs) => return Ok(Value::Ok(Box::new(Value::Number(secs)))),
Err(msg) => return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg.to_string()))))),
}
}
if builtin == Some(Builtin::DurFmt) && args.len() == 1 {
let secs = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("dur-fmt requires a number (seconds), got {:?}", other),
));
}
};
return Ok(Value::Text(Arc::new(dur_fmt(secs))));
}
if builtin == Some(Builtin::AddMo) && args.len() == 2 {
return add_mo_impl(&args[0], &args[1]);
}
if builtin == Some(Builtin::LastDom) && args.len() == 1 {
return last_dom_impl(&args[0]);
}
if builtin == Some(Builtin::NextBusinessDay) && args.len() == 1 {
return next_business_day_impl(&args[0]);
}
if builtin == Some(Builtin::DayOfWeek) && args.len() == 1 {
return day_of_week_impl(&args[0]);
}
if builtin == Some(Builtin::Fsize) && args.len() == 1 {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("fsize requires text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_read(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return match std::fs::metadata(path.as_str()) {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
Ok(md) if md.is_dir() => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"{}: is a directory",
path
)))))),
Ok(md) => Ok(Value::Ok(Box::new(Value::Number(md.len() as f64)))),
};
}
if builtin == Some(Builtin::Mtime) && args.len() == 1 {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("mtime requires text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_read(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return match std::fs::metadata(path.as_str()) {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
Ok(md) => match md.modified() {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
Ok(t) => match t.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => Ok(Value::Ok(Box::new(Value::Number(d.as_secs_f64())))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
},
},
};
}
if builtin == Some(Builtin::Isfile) && args.len() == 1 {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("isfile requires text path, got {:?}", other),
));
}
};
if env.caps.check_read(path.as_str()).is_err() {
return Ok(Value::Bool(false));
}
let is = std::fs::metadata(path.as_str())
.map(|m| m.is_file())
.unwrap_or(false);
return Ok(Value::Bool(is));
}
if builtin == Some(Builtin::Isdir) && args.len() == 1 {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("isdir requires text path, got {:?}", other),
));
}
};
if env.caps.check_read(path.as_str()).is_err() {
return Ok(Value::Bool(false));
}
let is = std::fs::metadata(path.as_str())
.map(|m| m.is_dir())
.unwrap_or(false);
return Ok(Value::Bool(is));
}
if builtin == Some(Builtin::TzOffset) && args.len() == 2 {
let tz_name = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("tz-offset: first arg must be text tz name, got {:?}", other),
));
}
};
let epoch = match &args[1] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"tz-offset: second arg must be number epoch, got {:?}",
other
),
));
}
};
let tz: chrono_tz::Tz = match tz_name.parse() {
Ok(t) => t,
Err(_) => {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"tz-offset: unknown timezone {:?}",
tz_name.as_str()
))))));
}
};
let secs = epoch as i64;
let utc_dt = chrono::DateTime::from_timestamp(secs, 0).unwrap_or_default();
let local_dt = utc_dt.with_timezone(&tz);
use chrono::offset::Offset as _;
let offset_secs = local_dt.offset().fix().local_minus_utc() as f64;
return Ok(Value::Ok(Box::new(Value::Number(offset_secs))));
}
if builtin == Some(Builtin::Rd) && (args.len() == 1 || args.len() == 2) {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rd requires text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_read(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let fmt = if args.len() == 2 {
match &args[1] {
Value::Text(s) => s.as_str().to_owned(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rd format must be text, got {:?}", other),
));
}
}
} else {
"raw".to_owned()
};
return match std::fs::read_to_string(path.as_str()) {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
Ok(content) => match parse_format(&fmt, &content) {
Ok(v) => Ok(Value::Ok(Box::new(v))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e))))),
},
};
}
if builtin == Some(Builtin::RdJson) && args.len() == 1 {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rd-json requires text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_read(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
return match std::fs::read_to_string(path.as_str()) {
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
Ok(content) => match parse_format("json", &content) {
Ok(v) => Ok(Value::Ok(Box::new(v))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e))))),
},
};
}
if builtin == Some(Builtin::Rdb) && args.len() == 2 {
let s = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rdb requires text string, got {:?}", other),
));
}
};
let fmt = match &args[1] {
Value::Text(f) => f.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rdb format must be text, got {:?}", other),
));
}
};
return match parse_format(&fmt, &s) {
Ok(v) => Ok(Value::Ok(Box::new(v))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e))))),
};
}
if builtin == Some(Builtin::Rdl) && args.len() == 1 {
return match &args[0] {
Value::Text(path) => {
if let Err(msg) = env.caps.check_read(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
match std::fs::read_to_string(path.as_str()) {
Ok(content) => {
let lines: Vec<Value> = content
.lines()
.map(|l| Value::Text(Arc::new(l.to_string())))
.collect();
Ok(Value::Ok(Box::new(Value::List(Arc::new(lines)))))
}
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("rdl requires text path, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Rdin) && args.is_empty() {
return rdin_impl();
}
if builtin == Some(Builtin::Rdinl) && args.is_empty() {
return rdinl_impl();
}
if builtin == Some(Builtin::ForLine) && args.len() == 1 {
return for_line_impl(&args[0]);
}
if builtin == Some(Builtin::Wr) && (args.len() == 2 || args.len() == 3) {
return wr_run(env, args);
}
if builtin == Some(Builtin::Wra) && args.len() == 2 {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wra: first arg must be a text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_write(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let content = match &args[1] {
Value::Text(s) => (**s).clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wra: second arg must be text content, got {:?}", other),
));
}
};
use std::io::Write as _;
return match std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(path.as_str())
{
Ok(mut f) => match f.write_all(content.as_bytes()) {
Ok(()) => Ok(Value::Ok(Box::new(Value::Text(path)))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
},
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
};
}
if builtin == Some(Builtin::Wro) && args.len() == 2 {
let path = match &args[0] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wro: first arg must be a text path, got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_write(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let content = match &args[1] {
Value::Text(s) => (**s).clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wro: second arg must be text content, got {:?}", other),
));
}
};
return match std::fs::write(path.as_str(), content.as_bytes()) {
Ok(()) => Ok(Value::Ok(Box::new(Value::Text(path)))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
};
}
if builtin == Some(Builtin::Wrl) && args.len() == 2 {
if let Value::Text(path) = &args[0] {
if let Err(msg) = env.caps.check_write(path.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
}
return match (&args[0], &args[1]) {
(Value::Text(path), Value::List(lines)) => {
let mut content = String::new();
for line in lines.iter() {
match line {
Value::Text(s) => {
content.push_str(s);
content.push('\n');
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("wrl list must contain text, got {:?}", other),
));
}
}
}
match std::fs::write(path.as_str(), &content) {
Ok(()) => Ok(Value::Ok(Box::new(Value::Text(path.clone())))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("wrl requires text path and list of text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Jpth) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Text(json_str), Value::Text(path)) => {
if let Some(msg) = crate::builtins::jpth_jsonpath_diagnostic(path) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
match serde_json::from_str::<serde_json::Value>(json_str) {
Ok(parsed) => {
let mut current = &parsed;
for key in path.split('.') {
if let Ok(idx) = key.parse::<usize>() {
if let Some(v) = current.as_array().and_then(|a| a.get(idx)) {
current = v;
} else {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(
format!("key not found: {key}"),
)))));
}
} else if let Some(v) = current.get(key) {
current = v;
} else {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"key not found: {key}"
))))));
}
}
let typed = serde_json_to_value(current.clone());
Ok(Value::Ok(Box::new(typed)))
}
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"jpth requires two text args".to_string(),
)),
};
}
if builtin == Some(Builtin::Jkeys) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Text(json_str), Value::Text(path)) => {
if let Some(msg) = crate::builtins::jpth_jsonpath_diagnostic(path) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
match serde_json::from_str::<serde_json::Value>(json_str) {
Ok(parsed) => {
let mut current = &parsed;
if !path.is_empty() {
for key in path.split('.') {
if let Ok(idx) = key.parse::<usize>() {
if let Some(v) = current.as_array().and_then(|a| a.get(idx)) {
current = v;
} else {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(
format!("key not found: {key}"),
)))));
}
} else if let Some(v) = current.get(key) {
current = v;
} else {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(
format!("key not found: {key}"),
)))));
}
}
}
match current.as_object() {
Some(obj) => {
let mut keys: Vec<String> = obj.keys().cloned().collect();
keys.sort();
let items: Vec<Value> =
keys.into_iter().map(|k| Value::Text(Arc::new(k))).collect();
Ok(Value::Ok(Box::new(Value::List(Arc::new(items)))))
}
None => Ok(Value::Err(Box::new(Value::Text(Arc::new(
"jkeys: value at path is not a JSON object".to_string(),
))))),
}
}
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"jkeys requires two text args".to_string(),
)),
};
}
if builtin == Some(Builtin::Prnt) && args.len() == 1 {
let v = args
.into_iter()
.next()
.expect("prnt: arity=1 guaranteed by caller");
let s = format!("{v}");
crate::runtime_guard::record_output(s.len() + 1);
println!("{s}");
return Ok(v);
}
if builtin == Some(Builtin::Jdmp) && args.len() == 1 {
let json_val = value_to_json(&args[0]);
return Ok(Value::Text(Arc::new(json_val.to_string())));
}
if builtin == Some(Builtin::Jpar) && args.len() == 1 {
return match &args[0] {
Value::Text(s) => match serde_json::from_str::<serde_json::Value>(s) {
Ok(v) => Ok(Value::Ok(Box::new(serde_json_to_value(v)))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
},
other => Err(RuntimeError::new(
"ILO-R009",
format!("jpar requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::JparList) && args.len() == 1 {
return match &args[0] {
Value::Text(s) => match serde_json::from_str::<serde_json::Value>(s) {
Ok(serde_json::Value::Array(arr)) => {
let items: Vec<Value> = arr.into_iter().map(serde_json_to_value).collect();
Ok(Value::Ok(Box::new(Value::List(Arc::new(items)))))
}
Ok(other) => {
let kind = match &other {
serde_json::Value::Object(_) => "object",
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => unreachable!(),
};
Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"jpar-list: expected JSON array, got {kind}"
))))))
}
Err(e) => Ok(Value::Err(Box::new(Value::Text(Arc::new(e.to_string()))))),
},
other => Err(RuntimeError::new(
"ILO-R009",
format!("jpar-list requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Rdjl) && args.len() == 1 {
return match &args[0] {
Value::Text(path) => match std::fs::read_to_string(path.as_str()) {
Ok(content) => {
let mut items: Vec<Value> = Vec::new();
for line in content.split('\n') {
if line.is_empty() {
continue;
}
let parsed = match serde_json::from_str::<serde_json::Value>(line) {
Ok(v) => Value::Ok(Box::new(serde_json_to_value(v))),
Err(e) => Value::Err(Box::new(Value::Text(Arc::new(e.to_string())))),
};
items.push(parsed);
}
Ok(Value::List(Arc::new(items)))
}
Err(e) => Err(RuntimeError::new(
"ILO-R009",
format!("rdjl failed to read '{}': {}", path, e),
)),
},
other => Err(RuntimeError::new(
"ILO-R009",
format!("rdjl requires text path, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::Env) && args.len() == 1 {
return match &args[0] {
Value::Text(key) => {
if let Err(msg) = env.caps.check_env(key.as_str()) {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
match std::env::var(key.as_str()) {
Ok(val) => Ok(Value::Ok(Box::new(Value::Text(Arc::new(val))))),
Err(_) => Ok(Value::Err(Box::new(Value::Text(Arc::new(format!(
"env var '{}' not set",
key
)))))),
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!("env requires text, got {:?}", other),
)),
};
}
if builtin == Some(Builtin::WorldCap) && args.is_empty() {
let (net, read, write, run) = match env.caps.as_ref() {
crate::caps::Caps::Permissive => (true, true, true, true),
crate::caps::Caps::Restricted {
net,
read,
write,
run,
..
} => {
let cap_allowed = |p: &crate::caps::Policy| {
matches!(p, crate::caps::Policy::All)
|| matches!(p, crate::caps::Policy::List(v) if !v.is_empty())
};
(
cap_allowed(net),
cap_allowed(read),
cap_allowed(write),
cap_allowed(run),
)
}
};
return Ok(Value::World {
net,
read,
write,
run,
});
}
if builtin == Some(Builtin::WorldNoNet) && args.is_empty() {
let (_net, read, write, run) = match env.caps.as_ref() {
crate::caps::Caps::Permissive => (true, true, true, true),
crate::caps::Caps::Restricted {
net,
read,
write,
run,
..
} => {
let cap_allowed = |p: &crate::caps::Policy| {
matches!(p, crate::caps::Policy::All)
|| matches!(p, crate::caps::Policy::List(v) if !v.is_empty())
};
(
cap_allowed(net),
cap_allowed(read),
cap_allowed(write),
cap_allowed(run),
)
}
};
return Ok(Value::World {
net: false, read,
write,
run,
});
}
if builtin == Some(Builtin::EnvAll) && args.is_empty() {
if let Err(msg) = env.caps.check_env("*") {
return Ok(Value::Err(Box::new(Value::Text(Arc::new(msg)))));
}
let map: std::collections::HashMap<MapKey, Value> = std::env::vars()
.map(|(k, v)| (MapKey::Text(k), Value::Text(Arc::new(v))))
.collect();
return Ok(Value::Ok(Box::new(Value::Map(Arc::new(map)))));
}
fn resolve_fn_ref(val: &Value) -> Option<String> {
match val {
Value::FnRef(n) => Some(n.clone()),
Value::Text(n) => Some((**n).clone()),
Value::Closure { fn_name, .. } => Some(fn_name.clone()),
_ => None,
}
}
fn closure_captures(val: &Value) -> Vec<Value> {
match val {
Value::Closure { captures, .. } => captures.clone(),
_ => Vec::new(),
}
}
if builtin == Some(Builtin::Map) && (args.len() == 2 || args.len() == 3) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"map: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let (ctx, list_arg) = if args.len() == 3 {
(Some(args[1].clone()), &args[2])
} else {
(None, &args[1])
};
let items = match list_arg {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("map: list arg must be a list, got {:?}", other),
));
}
};
let mut result = Vec::with_capacity(items.len());
for item in items.iter().cloned() {
let mut call_args = match &ctx {
Some(c) => vec![item, c.clone()],
None => vec![item],
};
call_args.extend(captures.iter().cloned());
result.push(call_function(env, &fn_name, call_args)?);
}
return Ok(Value::List(Arc::new(result)));
}
if builtin == Some(Builtin::ParMap) && (args.len() == 2 || args.len() == 3) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"par-map: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("par-map: second arg must be a list, got {:?}", other),
));
}
};
let concurrency: usize = if args.len() == 3 {
match &args[2] {
Value::Number(n) => {
let n = *n as usize;
if n == 0 {
par_map_default_concurrency()
} else {
n
}
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"par-map: third arg must be a number (concurrency), got {:?}",
other
),
));
}
}
} else {
par_map_default_concurrency()
};
let fns_snapshot = env.functions.clone();
let caps_snapshot = env.caps.clone();
return Ok(Value::List(Arc::new(par_map_run(
&fn_name,
captures,
&items,
concurrency,
fns_snapshot,
caps_snapshot,
))));
}
if builtin == Some(Builtin::Mapr) && args.len() == 2 {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"mapr: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("mapr: list arg must be a list, got {:?}", other),
));
}
};
let mut result = Vec::with_capacity(items.len());
for item in items.iter().cloned() {
let mut call_args = vec![item];
call_args.extend(captures.iter().cloned());
match call_function(env, &fn_name, call_args)? {
Value::Ok(inner) => result.push(*inner),
Value::Err(e) => return Ok(Value::Err(e)),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("mapr: fn must return a Result (~v or ^e), got {:?}", other),
));
}
}
}
return Ok(Value::Ok(Box::new(Value::List(Arc::new(result)))));
}
if builtin == Some(Builtin::Flt) && (args.len() == 2 || args.len() == 3) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"flt: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let mut args_iter = args.into_iter();
let _ = args_iter.next(); let (ctx, list_arg) = if args_iter.len() == 2 {
let c = args_iter.next().unwrap();
let l = args_iter.next().unwrap();
(Some(c), l)
} else {
(None, args_iter.next().unwrap())
};
let items = match list_arg {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("flt: list arg must be a list, got {:?}", other),
));
}
};
return flt_run(env, &fn_name, captures, ctx, items);
}
if builtin == Some(Builtin::Ct) && (args.len() == 2 || args.len() == 3) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"ct: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let (ctx, list_arg) = if args.len() == 3 {
(Some(args[1].clone()), &args[2])
} else {
(None, &args[1])
};
let items = match list_arg {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ct: list arg must be a list, got {:?}", other),
));
}
};
let mut count: i64 = 0;
for item in items.iter() {
let mut call_args = match &ctx {
Some(c) => vec![item.clone(), c.clone()],
None => vec![item.clone()],
};
call_args.extend(captures.iter().cloned());
match call_function(env, &fn_name, call_args)? {
Value::Bool(true) => count += 1,
Value::Bool(false) => {}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ct: predicate must return bool, got {:?}", other),
));
}
}
}
return Ok(Value::Number(count as f64));
}
if builtin == Some(Builtin::Fld) && (args.len() == 3 || args.len() == 4) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"fld: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let (ctx, list_arg, init) = if args.len() == 4 {
(Some(args[1].clone()), &args[2], args[3].clone())
} else {
(None, &args[1], args[2].clone())
};
let items = match list_arg {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("fld: list arg must be a list, got {:?}", other),
));
}
};
let mut acc = init;
for item in items.iter() {
let mut call_args = match &ctx {
Some(c) => vec![acc, item.clone(), c.clone()],
None => vec![acc, item.clone()],
};
call_args.extend(captures.iter().cloned());
acc = call_function(env, &fn_name, call_args)?;
}
return Ok(acc);
}
if builtin == Some(Builtin::Partition) && args.len() == 2 {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"partition: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("partition: second arg must be a list, got {:?}", other),
));
}
};
let mut pass: Vec<Value> = Vec::new();
let mut fail: Vec<Value> = Vec::new();
for item in items.iter() {
let mut call_args = vec![item.clone()];
call_args.extend(captures.iter().cloned());
match call_function(env, &fn_name, call_args)? {
Value::Bool(true) => pass.push(item.clone()),
Value::Bool(false) => fail.push(item.clone()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("partition: predicate must return bool, got {:?}", other),
));
}
}
}
return Ok(Value::List(Arc::new(vec![
Value::List(Arc::new(pass)),
Value::List(Arc::new(fail)),
])));
}
if builtin == Some(Builtin::Flatmap) && args.len() == 2 {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"flatmap: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("flatmap: second arg must be a list, got {:?}", other),
));
}
};
let mut result: Vec<Value> = Vec::new();
for item in items.iter().cloned() {
let mut call_args = vec![item];
call_args.extend(captures.iter().cloned());
match call_function(env, &fn_name, call_args)? {
Value::List(inner) => result.extend(inner.iter().cloned()),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("flatmap: function must return a list, got {:?}", other),
));
}
}
}
return Ok(Value::List(Arc::new(result)));
}
if builtin == Some(Builtin::Uniqby) && args.len() == 2 {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"uniqby: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("uniqby: second arg must be a list, got {:?}", other),
));
}
};
return uniqby_run(env, &fn_name, captures, items);
}
if builtin == Some(Builtin::Grp) && args.len() == 2 {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"grp: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("grp: second arg must be a list, got {:?}", other),
));
}
};
return grp_run(env, &fn_name, captures, items);
}
if builtin == Some(Builtin::Frq) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("frq: arg must be a list, got {:?}", other),
));
}
};
let mut counts: std::collections::HashMap<MapKey, usize> = std::collections::HashMap::new();
for item in items.iter() {
let map_key = match item {
Value::Text(s) => MapKey::Text((**s).clone()),
Value::Number(n) => {
if !n.is_finite() {
return Err(RuntimeError::new(
"ILO-R009",
format!("frq: numeric element must be finite, got {n}"),
));
}
MapKey::Int(n.floor() as i64)
}
Value::Bool(b) => MapKey::Text(format!("{b}")),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"frq: list elements must be text, number, or bool, got {:?}",
other
),
));
}
};
*counts.entry(map_key).or_insert(0) += 1;
}
let map: HashMap<MapKey, Value> = counts
.into_iter()
.map(|(k, v)| (k, Value::Number(v as f64)))
.collect();
return Ok(Value::Map(Arc::new(map)));
}
if builtin == Some(Builtin::Transpose) && args.len() == 1 {
let rows = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("transpose: arg must be a list of lists, got {:?}", other),
));
}
};
if rows.is_empty() {
return Ok(Value::List(Arc::new(vec![])));
}
let mut row_data: Vec<&Vec<Value>> = Vec::with_capacity(rows.len());
let mut ncols: Option<usize> = None;
for row in rows.iter() {
match row {
Value::List(r) => {
match ncols {
None => ncols = Some(r.len()),
Some(n) if n != r.len() => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"transpose: ragged rows (expected {n} cols, got {})",
r.len()
),
));
}
_ => {}
}
row_data.push(r);
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("transpose: rows must be lists, got {:?}", other),
));
}
}
}
let ncols = ncols.unwrap_or(0);
let mut result: Vec<Value> = Vec::with_capacity(ncols);
for j in 0..ncols {
let mut col: Vec<Value> = Vec::with_capacity(row_data.len());
for r in &row_data {
col.push(r[j].clone());
}
result.push(Value::List(Arc::new(col)));
}
return Ok(Value::List(Arc::new(result)));
}
if builtin == Some(Builtin::Matmul) && args.len() == 2 {
let a_rows = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("matmul: first arg must be a list of lists, got {:?}", other),
));
}
};
let b_rows = match &args[1] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"matmul: second arg must be a list of lists, got {:?}",
other
),
));
}
};
return matmul_run(a_rows, b_rows);
}
if builtin == Some(Builtin::Matvec) && args.len() == 2 {
return matvec_run(&args[0], &args[1]);
}
if builtin == Some(Builtin::Dot) && args.len() == 2 {
let xs = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("dot: first arg must be a list, got {:?}", other),
));
}
};
let ys = match &args[1] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("dot: second arg must be a list, got {:?}", other),
));
}
};
if xs.len() != ys.len() {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"dot: length mismatch (xs has {}, ys has {})",
xs.len(),
ys.len()
),
));
}
let mut total = 0.0_f64;
for (x, y) in xs.iter().zip(ys.iter()) {
match (x, y) {
(Value::Number(a), Value::Number(b)) => total += a * b,
_ => {
return Err(RuntimeError::new(
"ILO-R009",
"dot: list elements must be numbers".to_string(),
));
}
}
}
return Ok(Value::Number(total));
}
if builtin == Some(Builtin::Sum) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("sum: arg must be a list, got {:?}", other),
));
}
};
let mut total = 0.0_f64;
for item in items.iter() {
match item {
Value::Number(n) => total += n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("sum: list elements must be numbers, got {:?}", other),
));
}
}
}
return Ok(Value::Number(total));
}
if builtin == Some(Builtin::Prod) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("prod: arg must be a list, got {:?}", other),
));
}
};
let mut total = 1.0_f64;
for item in items.iter() {
match item {
Value::Number(n) => total *= n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("prod: list elements must be numbers, got {:?}", other),
));
}
}
}
return Ok(Value::Number(total));
}
if builtin == Some(Builtin::Cumsum) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cumsum: arg must be a list, got {:?}", other),
));
}
};
let mut total = 0.0_f64;
let mut out: Vec<Value> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => {
total += n;
out.push(Value::Number(total));
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cumsum: list elements must be numbers, got {:?}", other),
));
}
}
}
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Cprod) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cprod: arg must be a list, got {:?}", other),
));
}
};
let mut total = 1.0_f64;
let mut out: Vec<Value> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => {
total *= n;
out.push(Value::Number(total));
}
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("cprod: list elements must be numbers, got {:?}", other),
));
}
}
}
return Ok(Value::List(Arc::new(out)));
}
if builtin == Some(Builtin::Ewm) && args.len() == 2 {
let a = match &args[1] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ewm: second arg a must be a number, got {:?}", other),
));
}
};
return ewm_run(&args[0], a);
}
if builtin == Some(Builtin::Rsum) && args.len() == 2 {
let n_f = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rsum: first arg n must be a number, got {:?}", other),
));
}
};
return rolling_window_run("rsum", Builtin::Rsum, n_f, &args[1]);
}
if builtin == Some(Builtin::Ravg) && args.len() == 2 {
let n_f = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ravg: first arg n must be a number, got {:?}", other),
));
}
};
return rolling_window_run("ravg", Builtin::Ravg, n_f, &args[1]);
}
if builtin == Some(Builtin::Rmin) && args.len() == 2 {
let n_f = match &args[0] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rmin: first arg n must be a number, got {:?}", other),
));
}
};
return rolling_window_run("rmin", Builtin::Rmin, n_f, &args[1]);
}
if builtin == Some(Builtin::Where) && args.len() == 3 {
let cond = match &args[0] {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("where: first arg (cond) must be a list, got {:?}", other),
));
}
};
let xs = match &args[1] {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("where: second arg (xs) must be a list, got {:?}", other),
));
}
};
let ys = match &args[2] {
Value::List(items) => items,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("where: third arg (ys) must be a list, got {:?}", other),
));
}
};
return where_run(cond, xs, ys);
}
if builtin == Some(Builtin::Avg) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("avg: arg must be a list, got {:?}", other),
));
}
};
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"avg: cannot average an empty list".to_string(),
));
}
let mut total = 0.0_f64;
for item in items.iter() {
match item {
Value::Number(n) => total += n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("avg: list elements must be numbers, got {:?}", other),
));
}
}
}
return Ok(Value::Number(total / items.len() as f64));
}
if builtin == Some(Builtin::Median) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("median: arg must be a list, got {:?}", other),
));
}
};
return median_run(items);
}
if builtin == Some(Builtin::Quantile) && args.len() == 2 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("quantile: first arg must be a list, got {:?}", other),
));
}
};
let p = match &args[1] {
Value::Number(n) => *n,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("quantile: second arg p must be a number, got {:?}", other),
));
}
};
return quantile_run(items, p);
}
if builtin == Some(Builtin::Variance) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("variance: arg must be a list, got {:?}", other),
));
}
};
return variance_run(items);
}
if builtin == Some(Builtin::Stdev) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("stdev: arg must be a list, got {:?}", other),
));
}
};
return stdev_run(items);
}
if builtin == Some(Builtin::Rgx) && args.len() == 2 {
let pattern = match &args[0] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rgx: first arg must be a string pattern, got {:?}", other),
));
}
};
let input = match &args[1] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rgx: second arg must be a string, got {:?}", other),
));
}
};
return rgx_run(pattern, input);
}
if builtin == Some(Builtin::Rgxall) && args.len() == 2 {
let pattern = match &args[0] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxall: first arg must be a string pattern, got {:?}",
other
),
));
}
};
let input = match &args[1] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rgxall: second arg must be a string, got {:?}", other),
));
}
};
return rgxall_run(pattern, input);
}
if builtin == Some(Builtin::Rgxall1) && args.len() == 2 {
let pattern = match &args[0] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxall1: first arg must be a string pattern, got {:?}",
other
),
));
}
};
let input = match &args[1] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rgxall1: second arg must be a string, got {:?}", other),
));
}
};
return rgxall1_run(pattern, input);
}
if builtin == Some(Builtin::RgxallMulti) && args.len() == 2 {
let pats = match &args[0] {
Value::List(xs) => xs.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxall-multi: first arg must be a list of patterns, got {:?}",
other
),
));
}
};
let input = match &args[1] {
Value::Text(s) => s.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("rgxall-multi: second arg must be a string, got {:?}", other),
));
}
};
return rgxall_multi_run(&pats, &input);
}
if builtin == Some(Builtin::Rgxsub) && args.len() == 3 {
let pattern = match &args[0] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxsub: first arg must be a string pattern, got {:?}",
other
),
));
}
};
let replacement = match &args[1] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxsub: second arg must be a string replacement, got {:?}",
other
),
));
}
};
let subject = match &args[2] {
Value::Text(s) => s.as_str(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!(
"rgxsub: third arg must be a string subject, got {:?}",
other
),
));
}
};
return rgxsub_run(pattern, replacement, subject);
}
if builtin == Some(Builtin::Flat) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("flat: arg must be a list, got {:?}", other),
));
}
};
let mut result: Vec<Value> = Vec::new();
for item in items.iter() {
match item {
Value::List(inner) => result.extend(inner.iter().cloned()),
other => result.push(other.clone()),
}
}
return Ok(Value::List(Arc::new(result)));
}
if builtin == Some(Builtin::Fft) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("fft: arg must be a list of numbers, got {:?}", other),
));
}
};
if items.is_empty() {
return Err(RuntimeError::new(
"ILO-R009",
"fft: input list must not be empty".to_string(),
));
}
let mut reals: Vec<f64> = Vec::with_capacity(items.len());
for item in items.iter() {
match item {
Value::Number(n) => reals.push(*n),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("fft: list elements must be numbers, got {:?}", other),
));
}
}
}
let n = next_pow2(reals.len());
let mut re = reals;
re.resize(n, 0.0);
let mut im = vec![0.0_f64; n];
cooley_tukey(&mut re, &mut im, false);
let result: Vec<Value> = re
.into_iter()
.zip(im)
.map(|(r, i)| Value::List(Arc::new(vec![Value::Number(r), Value::Number(i)])))
.collect();
return Ok(Value::List(Arc::new(result)));
}
if builtin == Some(Builtin::Ifft) && args.len() == 1 {
let items = match &args[0] {
Value::List(l) => l,
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("ifft: arg must be a list of pairs, got {:?}", other),
));
}
};
return ifft_run(items);
}
if let Some((type_name, has_payload)) = env.sum_variants.get(name).cloned() {
return if has_payload {
if args.len() != 1 {
return Err(RuntimeError::new(
"ILO-R004",
format!(
"{name}: variant constructor expects 1 argument, got {}",
args.len()
),
));
}
Ok(Value::Variant {
type_name,
tag: name.to_string(),
payload: Some(Box::new(args.into_iter().next().unwrap())),
})
} else {
if !args.is_empty() {
return Err(RuntimeError::new(
"ILO-R004",
format!(
"{name}: variant constructor takes no arguments, got {}",
args.len()
),
));
}
Ok(Value::Variant {
type_name,
tag: name.to_string(),
payload: None,
})
};
}
if builtin == Some(Builtin::Convolve) && args.len() == 2 {
return run_convolve(&args[0], &args[1]);
}
if builtin == Some(Builtin::Searchsorted) && args.len() == 2 {
return run_searchsorted(&args[0], &args[1]);
}
if builtin == Some(Builtin::Cabs) && args.len() == 1 {
return run_cabs(&args[0]);
}
if builtin == Some(Builtin::Cmul) && args.len() == 2 {
return run_cmul(&args[0], &args[1]);
}
if builtin == Some(Builtin::Pairwise) && args.len() == 2 {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"pairwise: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("pairwise: second arg must be a list, got {:?}", other),
));
}
};
if items.len() < 2 {
return Ok(Value::List(Arc::new(vec![])));
}
let mut result = Vec::with_capacity(items.len() - 1);
for i in 0..items.len() - 1 {
let mut call_args = vec![items[i].clone(), items[i + 1].clone()];
call_args.extend(captures.iter().cloned());
result.push(call_function(env, &fn_name, call_args)?);
}
return Ok(Value::List(Arc::new(result)));
}
if builtin == Some(Builtin::Pdist2) && args.len() == 2 {
return run_pdist2(&args[0], &args[1]);
}
let decl = env.function(name)?;
match decl {
Decl::Function {
params,
body,
name: func_name,
..
} => {
if args.len() != params.len() {
return Err(RuntimeError::new(
"ILO-R004",
format!(
"{}: expected {} args, got {}",
name,
params.len(),
args.len()
),
));
}
let saved_vars = std::mem::take(&mut env.vars);
let saved_marks = std::mem::replace(&mut env.scope_marks, vec![0]);
let saved_defers = std::mem::take(&mut env.defer_stack);
let mut cur_params = params;
let mut cur_body = body;
let mut cur_args = args;
let mut cur_func_name = func_name;
let body_result = loop {
env.vars.clear();
env.scope_marks.clear();
env.scope_marks.push(0);
for (param, arg) in cur_params.iter().zip(cur_args) {
env.define(¶m.name, arg);
}
env.call_stack.push(cur_func_name.clone());
let result = eval_body(env, &cur_body, true);
env.call_stack.pop();
match result {
Err(e) => break Err(e),
Ok(BodyResult::Value(v))
| Ok(BodyResult::Return(v))
| Ok(BodyResult::Break(v)) => break Ok(v),
Ok(BodyResult::Continue) => break Ok(Value::Nil),
Ok(BodyResult::TailCall {
callee,
args: ta_args,
}) => match env.function(&callee) {
Ok(Decl::Function {
params: np,
body: nb,
name: nn,
..
}) => {
if ta_args.len() != np.len() {
break Err(RuntimeError::new(
"ILO-R004",
format!(
"{}: expected {} args, got {}",
callee,
np.len(),
ta_args.len()
),
));
}
cur_params = np;
cur_body = nb;
cur_args = ta_args;
cur_func_name = nn;
continue;
}
_ => {
break call_function(env, &callee, ta_args);
}
},
}
};
let is_error = matches!(&body_result, Err(_) | Ok(Value::Err(_)));
let frame_defers = std::mem::take(&mut env.defer_stack);
for (defer_expr, defer_kind) in frame_defers.into_iter().rev() {
let should_run = match defer_kind {
crate::ast::DeferKind::Always => true,
crate::ast::DeferKind::OnError => is_error,
};
if should_run {
let _ = eval_expr(env, &defer_expr);
}
}
env.vars = saved_vars;
env.scope_marks = saved_marks;
env.defer_stack = saved_defers;
body_result
}
Decl::Tool { name, .. } => {
if let Some(ref _provider) = env.tool_provider {
#[cfg(feature = "tools")]
{
if let Some(ref rt) = env.tokio_runtime {
return rt
.block_on(_provider.call(&name, args))
.map_err(|e| RuntimeError::new("ILO-R099", e.to_string()));
}
}
let args_str: Vec<String> = args.iter().map(|a| format!("{a}")).collect();
eprintln!("tool call (no runtime): {}({})", name, args_str.join(", "));
Ok(Value::Ok(Box::new(Value::Nil)))
} else {
let args_str: Vec<String> = args.iter().map(|a| format!("{a}")).collect();
eprintln!("tool call: {}({})", name, args_str.join(", "));
Ok(Value::Ok(Box::new(Value::Nil)))
}
}
Decl::TypeDef { .. } => Err(RuntimeError::new(
"ILO-R004",
format!("{} is a type, not callable", name),
)),
Decl::Alias { .. } => Err(RuntimeError::new(
"ILO-R004",
format!("{} is a type alias, not callable", name),
)),
Decl::Use { .. } => Err(RuntimeError::new(
"ILO-R002",
format!("{} is an unresolved import", name),
)),
Decl::Error { .. } => Err(RuntimeError::new(
"ILO-R002",
format!("{} failed to parse", name),
)),
Decl::SumType { .. } => Err(RuntimeError::new(
"ILO-R002",
format!("{} is a sum type, not a callable function", name),
)),
Decl::VersionPragma { .. } => Err(RuntimeError::new(
"ILO-R002",
format!("{} is a version pragma, not a callable function", name),
)),
}
}
fn value_to_json(val: &Value) -> serde_json::Value {
match val {
Value::Number(n) => {
if n.fract() == 0.0 && n.abs() < 1e15 {
serde_json::Value::Number(serde_json::Number::from(*n as i64))
} else {
serde_json::Number::from_f64(*n)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)
}
}
Value::Text(s) => serde_json::Value::String((**s).clone()),
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Nil => serde_json::Value::Null,
Value::List(items) => serde_json::Value::Array(items.iter().map(value_to_json).collect()),
Value::Record { fields, .. } => {
let map: serde_json::Map<String, serde_json::Value> = fields
.iter()
.map(|(k, v)| (k.clone(), value_to_json(v)))
.collect();
serde_json::Value::Object(map)
}
Value::Map(m) => {
let map: serde_json::Map<String, serde_json::Value> = m
.iter()
.map(|(k, v)| (k.to_display_string(), value_to_json(v)))
.collect();
serde_json::Value::Object(map)
}
Value::Ok(inner) => value_to_json(inner),
Value::Err(inner) => value_to_json(inner),
Value::FnRef(name) => serde_json::Value::String(format!("<fn:{}>", name)),
Value::Closure { fn_name, .. } => {
serde_json::Value::String(format!("<closure:{}>", fn_name))
}
Value::Variant { tag, payload, .. } => {
let mut map = serde_json::Map::new();
map.insert("tag".to_string(), serde_json::Value::String(tag.clone()));
if let Some(p) = payload {
map.insert("payload".to_string(), value_to_json(p));
}
serde_json::Value::Object(map)
}
Value::LazyStdinLines(_) => serde_json::Value::String("<stdin-lines>".to_string()),
Value::LazyHttpLines(_) => serde_json::Value::String("<http-lines>".to_string()),
Value::World {
net,
read,
write,
run,
} => {
let mut map = serde_json::Map::with_capacity(4);
map.insert("net".to_string(), serde_json::Value::Bool(*net));
map.insert("read".to_string(), serde_json::Value::Bool(*read));
map.insert("write".to_string(), serde_json::Value::Bool(*write));
map.insert("run".to_string(), serde_json::Value::Bool(*run));
serde_json::Value::Object(map)
}
}
}
fn serde_json_to_value(root: serde_json::Value) -> Value {
enum Task {
Convert(serde_json::Value),
BuildArray(usize),
BuildObject(Vec<String>), }
let mut work: Vec<Task> = vec![Task::Convert(root)];
let mut out: Vec<Value> = Vec::new();
while let Some(task) = work.pop() {
match task {
Task::Convert(v) => match v {
serde_json::Value::String(s) => out.push(Value::Text(Arc::new(s))),
serde_json::Value::Number(n) => out.push(Value::Number(n.as_f64().unwrap_or(0.0))),
serde_json::Value::Bool(b) => out.push(Value::Bool(b)),
serde_json::Value::Null => out.push(Value::Nil),
serde_json::Value::Array(arr) => {
let len = arr.len();
work.push(Task::BuildArray(len));
for elem in arr.into_iter().rev() {
work.push(Task::Convert(elem));
}
}
serde_json::Value::Object(map) => {
let keys: Vec<String> = map.keys().cloned().collect();
let len = keys.len();
work.push(Task::BuildObject(keys.clone()));
let mut pairs: Vec<(String, serde_json::Value)> = map.into_iter().collect();
pairs.sort_by(|a, b| {
keys.iter()
.position(|k| k == &a.0)
.cmp(&keys.iter().position(|k| k == &b.0))
});
let _ = len; for (_, v) in pairs.into_iter().rev() {
work.push(Task::Convert(v));
}
}
},
Task::BuildArray(len) => {
let start = out.len() - len;
let items: Vec<Value> = out.drain(start..).collect();
out.push(Value::List(Arc::new(items)));
}
Task::BuildObject(keys) => {
let len = keys.len();
let start = out.len() - len;
let vals: Vec<Value> = out.drain(start..).collect();
let fields: HashMap<String, Value> = keys.into_iter().zip(vals).collect();
out.push(Value::Record {
type_name: "json".to_string(),
fields,
});
}
}
}
out.pop().expect("serde_json_to_value: output stack empty")
}
#[inline(never)]
fn try_synthesize_tail_call(env: &mut Env, expr: &Expr) -> Option<Result<(String, Vec<Value>)>> {
let Expr::Call {
function,
args,
unwrap,
} = expr
else {
return None;
};
if unwrap.is_any() {
return None;
}
if env.vars.iter().rev().any(|(k, _)| k == function.as_str()) {
return None;
}
let decl = env.functions.get(function.as_str())?;
if !matches!(decl, Decl::Function { .. }) {
return None;
}
let mut arg_vals = Vec::with_capacity(args.len());
for arg in args {
match eval_expr(env, arg) {
Ok(v) => arg_vals.push(v),
Err(e) => return Some(Err(e)),
}
}
Some(Ok((function.clone(), arg_vals)))
}
fn eval_body(env: &mut Env, stmts: &[Spanned<Stmt>], is_tail: bool) -> Result<BodyResult> {
let mut last = Value::Nil;
let n = stmts.len();
for (i, spanned) in stmts.iter().enumerate() {
let stmt_is_tail = is_tail && i + 1 == n;
CURRENT_STMT_SPAN.with(|s| *s.borrow_mut() = spanned.span);
match eval_stmt(env, &spanned.node, stmt_is_tail) {
Ok(Some(BodyResult::Return(v))) => {
fire_trace_event(env, spanned, v.clone());
return Ok(BodyResult::Return(v));
}
Ok(Some(BodyResult::Break(v))) => {
fire_trace_event(env, spanned, v.clone());
return Ok(BodyResult::Break(v));
}
Ok(Some(BodyResult::Continue)) => {
fire_trace_event(env, spanned, Value::Nil);
return Ok(BodyResult::Continue);
}
Ok(Some(BodyResult::TailCall { callee, args })) => {
fire_trace_event(env, spanned, Value::Nil);
return Ok(BodyResult::TailCall { callee, args });
}
Ok(Some(BodyResult::Value(v))) => {
fire_trace_event(env, spanned, v.clone());
last = v;
}
Ok(None) => {
let result = if let Stmt::Let { name, .. } = &spanned.node {
env.vars
.iter()
.rev()
.find(|(k, _)| k == name)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil)
} else {
Value::Nil
};
fire_trace_event(env, spanned, result);
}
Err(mut e) => {
if let Some(val) = e.propagate_value.take() {
return Ok(BodyResult::Return(*val));
}
if e.span.is_none() {
e.span = Some(spanned.span);
}
if e.call_stack.is_empty() {
e.call_stack = env.call_stack.clone();
}
return Err(e);
}
}
}
Ok(BodyResult::Value(last))
}
#[inline]
fn fire_trace_event(env: &Env, spanned: &Spanned<Stmt>, result: Value) {
let has_hook = TRACE_HOOK.with(|h| h.borrow().is_some());
if !has_hook {
return;
}
let span = spanned.span;
let (line, stmt_text) = TRACE_SOURCE.with(|src| {
if let Some(ref source) = *src.borrow() {
let sm = crate::ast::SourceMap::new(source);
let (line, _col) = sm.lookup(span.start);
let text = sm.line_text(source, line).trim().to_string();
(line, text)
} else {
(0, String::new())
}
});
let bindings: Vec<(String, Value)> = env
.vars
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
TRACE_HOOK.with(|h| {
if let Some(ref mut hook) = *h.borrow_mut() {
hook(TraceEvent {
line,
stmt: stmt_text,
bindings,
result,
});
}
});
}
#[inline]
fn fire_expr_trace_event(expr: &Expr, span: Span, result: &Value) {
let has_hook = EXPR_TRACE_HOOK.with(|h| h.borrow().is_some());
if !has_hook {
return;
}
let refs = collect_refs(expr);
let (line, expr_text) = TRACE_SOURCE.with(|src| {
if let Some(ref source) = *src.borrow() {
let sm = crate::ast::SourceMap::new(source);
let (line, _col) = sm.lookup(span.start);
let text = source
.get(span.start..span.end)
.map(|s| s.trim().to_string())
.unwrap_or_else(|| sm.line_text(source, line).trim().to_string());
(line, text)
} else {
(0, String::new())
}
});
EXPR_TRACE_HOOK.with(|h| {
if let Some(ref mut hook) = *h.borrow_mut() {
hook(ExprTraceEvent {
line,
expr: expr_text,
refs,
result: result.clone(),
});
}
});
}
fn collect_refs(expr: &Expr) -> Vec<String> {
let mut out = Vec::new();
collect_refs_inner(expr, &mut out);
out
}
fn collect_refs_inner(expr: &Expr, out: &mut Vec<String>) {
match expr {
Expr::Ref(name) => out.push(name.clone()),
Expr::Field { object, .. } => collect_refs_inner(object, out),
Expr::Index { object, .. } => collect_refs_inner(object, out),
Expr::Call { args, .. } => {
for a in args {
collect_refs_inner(a, out);
}
}
Expr::BinOp { left, right, .. } => {
collect_refs_inner(left, out);
collect_refs_inner(right, out);
}
Expr::UnaryOp { operand, .. } => collect_refs_inner(operand, out),
_ => {}
}
}
fn match_self_rebind_mset<'a>(name: &str, value: &'a Expr) -> Option<(&'a Expr, &'a Expr)> {
if let Expr::Call {
function,
args,
unwrap,
} = value
&& !unwrap.is_any()
&& function == "mset"
&& args.len() == 3
&& let Expr::Ref(arg_name) = &args[0]
&& arg_name == name
{
return Some((&args[1], &args[2]));
}
None
}
fn eval_self_rebind_mset(
env: &mut Env,
key_expr: &Expr,
val_expr: &Expr,
prev: Value,
) -> Result<Value> {
let key_val = eval_expr(env, key_expr)?;
let val_val = eval_expr(env, val_expr)?;
let args = vec![prev, key_val, val_val];
call_function(env, "mset", args)
}
fn expr_refers_to(name: &str, expr: &Expr) -> bool {
match expr {
Expr::Ref(n) => n == name,
Expr::Field { object, .. } => expr_refers_to(name, object),
Expr::Index { object, .. } => expr_refers_to(name, object),
Expr::Call { args, .. } => args.iter().any(|a| expr_refers_to(name, a)),
Expr::BinOp { left, right, .. } => {
expr_refers_to(name, left) || expr_refers_to(name, right)
}
Expr::UnaryOp { operand, .. } => expr_refers_to(name, operand),
Expr::Ok(inner) | Expr::Err(inner) => expr_refers_to(name, inner),
Expr::List(items) => items.iter().any(|e| expr_refers_to(name, e)),
Expr::Record { fields, .. } | Expr::AnonRecord { fields } => {
fields.iter().any(|(_, e)| expr_refers_to(name, e))
}
Expr::Match { .. } => true,
Expr::NilCoalesce { value, default } => {
expr_refers_to(name, value) || expr_refers_to(name, default)
}
Expr::With { object, updates } => {
expr_refers_to(name, object) || updates.iter().any(|(_, e)| expr_refers_to(name, e))
}
Expr::Ternary {
condition,
then_expr,
else_expr,
} => {
expr_refers_to(name, condition)
|| expr_refers_to(name, then_expr)
|| expr_refers_to(name, else_expr)
}
Expr::MakeClosure { captures, .. } => captures.iter().any(|c| expr_refers_to(name, c)),
Expr::Todo(inner) | Expr::Panic(inner) => expr_refers_to(name, inner),
Expr::Literal(_) => false,
}
}
fn match_self_rebind_append<'a>(name: &str, value: &'a Expr) -> Option<&'a Expr> {
if let Expr::BinOp { op, left, right } = value
&& matches!(op, BinOp::Append)
&& let Expr::Ref(left_name) = left.as_ref()
&& left_name == name
&& !expr_refers_to(name, right)
{
return Some(right.as_ref());
}
None
}
fn eval_self_rebind_append(env: &mut Env, val_expr: &Expr, prev: Value) -> Result<Value> {
let val_val = eval_expr(env, val_expr)?;
match prev {
Value::List(mut items) => {
let inner = Arc::make_mut(&mut items);
inner.push(val_val);
Ok(Value::List(items))
}
other => Err(RuntimeError::new(
"ILO-R004",
format!(
"unsupported operation: {:?} on {:?} and {:?}",
BinOp::Append,
other,
val_val
),
)),
}
}
fn match_self_rebind_concat<'a>(name: &str, value: &'a Expr) -> Option<&'a Expr> {
if let Expr::BinOp { op, left, right } = value
&& matches!(op, BinOp::Add)
&& let Expr::Ref(left_name) = left.as_ref()
&& left_name == name
&& !expr_refers_to(name, right)
{
return Some(right.as_ref());
}
None
}
fn eval_self_rebind_concat(env: &mut Env, rhs_expr: &Expr, prev: Value) -> Result<Value> {
let rhs = eval_expr(env, rhs_expr)?;
match (prev, rhs) {
(Value::List(mut items), Value::List(other)) => {
let inner = Arc::make_mut(&mut items);
inner.extend(other.iter().cloned());
Ok(Value::List(items))
}
(Value::Text(mut s), Value::Text(other)) => {
let inner = Arc::make_mut(&mut s);
inner.push_str(&other);
Ok(Value::Text(s))
}
(prev, rhs) => eval_binop(&BinOp::Add, &prev, &rhs),
}
}
fn run_block_defers(env: &mut Env, saved_len: usize, is_error: bool) {
let block_defers: Vec<_> = env.defer_stack.drain(saved_len..).rev().collect();
for (defer_expr, defer_kind) in block_defers {
let should_run = match defer_kind {
crate::ast::DeferKind::Always => true,
crate::ast::DeferKind::OnError => is_error,
};
if should_run {
let _ = eval_expr(env, &defer_expr);
}
}
}
fn eval_stmt(env: &mut Env, stmt: &Stmt, is_tail: bool) -> Result<Option<BodyResult>> {
match stmt {
Stmt::Let { name, value } => {
if let Some((key_expr, val_expr)) = match_self_rebind_mset(name, value)
&& let Some(prev) = env.take(name)
{
let val = eval_self_rebind_mset(env, key_expr, val_expr, prev)?;
env.set(name, val);
return Ok(None);
}
if let Some(val_expr) = match_self_rebind_append(name, value)
&& let Some(prev) = env.take(name)
{
let val = eval_self_rebind_append(env, val_expr, prev)?;
env.set(name, val);
return Ok(None);
}
if let Some(rhs_expr) = match_self_rebind_concat(name, value)
&& let Some(prev) = env.take(name)
{
let val = eval_self_rebind_concat(env, rhs_expr, prev)?;
env.set(name, val);
return Ok(None);
}
if name == "_" {
eval_expr(env, value)?;
return Ok(None);
}
let val = eval_expr(env, value)?;
env.set(name, val);
Ok(None)
}
Stmt::Destructure { bindings, value } => {
let val = eval_expr(env, value)?;
match val {
Value::Record { fields, .. } => {
for binding in bindings {
let field_val = fields.get(binding).cloned().ok_or_else(|| {
RuntimeError::new(
"ILO-R005",
format!("no field '{}' on record", binding),
)
})?;
env.set(binding, field_val);
}
Ok(None)
}
_ => Err(RuntimeError::new(
"ILO-R005",
"destructure requires a record".to_string(),
)),
}
}
Stmt::Guard {
condition,
negated,
body,
else_body,
braceless,
} => {
let cond = eval_expr(env, condition)?;
let truth = is_truthy(&cond);
let should_run = if *negated { !truth } else { truth };
if let Some(else_b) = else_body {
let chosen = if should_run { body } else { else_b };
env.push_scope();
let defer_mark = env.defer_stack.len();
let result = eval_body(env, chosen, is_tail);
let is_err = matches!(result, Err(_) | Ok(BodyResult::Return(_)));
run_block_defers(env, defer_mark, is_err);
env.pop_scope();
match result? {
BodyResult::Break(v) => Ok(Some(BodyResult::Break(v))),
BodyResult::Continue => Ok(Some(BodyResult::Continue)),
BodyResult::TailCall { callee, args } => {
Ok(Some(BodyResult::TailCall { callee, args }))
}
BodyResult::Value(v) | BodyResult::Return(v) => Ok(Some(BodyResult::Value(v))),
}
} else if should_run && *braceless {
env.push_scope();
let defer_mark = env.defer_stack.len();
let result = eval_body(env, body, true);
let is_err = matches!(result, Err(_) | Ok(BodyResult::Return(_)));
run_block_defers(env, defer_mark, is_err);
env.pop_scope();
match result? {
BodyResult::Break(v) => Ok(Some(BodyResult::Break(v))),
BodyResult::Continue => Ok(Some(BodyResult::Continue)),
BodyResult::TailCall { callee, args } => {
Ok(Some(BodyResult::TailCall { callee, args }))
}
BodyResult::Value(v) | BodyResult::Return(v) => Ok(Some(BodyResult::Return(v))),
}
} else if should_run {
env.push_scope();
let defer_mark = env.defer_stack.len();
let result = eval_body(env, body, is_tail);
let is_err = matches!(result, Err(_) | Ok(BodyResult::Return(_)));
run_block_defers(env, defer_mark, is_err);
env.pop_scope();
match result? {
BodyResult::Break(v) => Ok(Some(BodyResult::Break(v))),
BodyResult::Continue => Ok(Some(BodyResult::Continue)),
BodyResult::TailCall { callee, args } => {
Ok(Some(BodyResult::TailCall { callee, args }))
}
BodyResult::Return(v) => Ok(Some(BodyResult::Return(v))),
BodyResult::Value(v) => Ok(Some(BodyResult::Value(v))),
}
} else {
Ok(None)
}
}
Stmt::Match { subject, arms } => {
let subj = match subject {
Some(e) => eval_expr(env, e)?,
None => Value::Nil,
};
for arm in arms {
if let Some(bindings) = match_pattern(&arm.pattern, &subj) {
env.push_scope();
for (name, val) in bindings {
env.define(&name, val);
}
let defer_mark = env.defer_stack.len();
let result = eval_body(env, &arm.body, is_tail);
let is_err = matches!(result, Err(_) | Ok(BodyResult::Return(_)));
run_block_defers(env, defer_mark, is_err);
env.pop_scope();
match result? {
BodyResult::Return(v) => return Ok(Some(BodyResult::Return(v))),
BodyResult::Break(v) => return Ok(Some(BodyResult::Break(v))),
BodyResult::Continue => return Ok(Some(BodyResult::Continue)),
BodyResult::TailCall { callee, args } => {
return Ok(Some(BodyResult::TailCall { callee, args }));
}
BodyResult::Value(v) => return Ok(Some(BodyResult::Value(v))),
}
}
}
Ok(None)
}
Stmt::ForEach {
binding,
collection,
body,
} => {
let coll = eval_expr(env, collection)?;
match coll {
Value::List(items) => {
let mut last = Value::Nil;
for item in items.iter().cloned() {
env.push_scope();
env.define(binding, item);
let defer_mark = env.defer_stack.len();
let result = eval_body(env, body, false);
let is_err = matches!(result, Err(_) | Ok(BodyResult::Return(_)));
run_block_defers(env, defer_mark, is_err);
env.pop_scope();
match result? {
BodyResult::Return(v) => {
return Ok(Some(BodyResult::Return(v)));
}
BodyResult::Break(v) => {
last = v;
break;
}
BodyResult::Continue => continue,
BodyResult::TailCall { .. } => {
unreachable!("TailCall escaping non-tail loop body");
}
BodyResult::Value(v) => last = v,
}
}
Ok(Some(BodyResult::Value(last)))
}
Value::LazyStdinLines(handle) => {
let mut last = Value::Nil;
loop {
let line = handle.next_line();
match line {
None => break,
Some(Err(e)) => {
return Err(RuntimeError::new(
"ILO-R012",
format!("for-line: stdin read error: {}", e),
));
}
Some(Ok(s)) => {
env.push_scope();
env.define(binding, Value::Text(Arc::new(s)));
let result = eval_body(env, body, false);
env.pop_scope();
match result? {
BodyResult::Return(v) => {
return Ok(Some(BodyResult::Return(v)));
}
BodyResult::Break(v) => {
last = v;
break;
}
BodyResult::Continue => continue,
BodyResult::TailCall { .. } => {
unreachable!("TailCall escaping non-tail loop body");
}
BodyResult::Value(v) => last = v,
}
}
}
}
Ok(Some(BodyResult::Value(last)))
}
Value::LazyHttpLines(handle) => {
let mut last = Value::Nil;
loop {
let line = handle.next_line();
match line {
None => break,
Some(Err(e)) => {
return Err(RuntimeError::new(
"ILO-R009",
format!("http-stream read error: {}", e),
));
}
Some(Ok(s)) => {
env.push_scope();
env.define(binding, Value::Text(Arc::new(s)));
let result = eval_body(env, body, false);
env.pop_scope();
match result? {
BodyResult::Return(v) => {
return Ok(Some(BodyResult::Return(v)));
}
BodyResult::Break(v) => {
last = v;
break;
}
BodyResult::Continue => continue,
BodyResult::TailCall { .. } => {
unreachable!("TailCall escaping non-tail loop body");
}
BodyResult::Value(v) => last = v,
}
}
}
}
Ok(Some(BodyResult::Value(last)))
}
_ => Err(RuntimeError::new("ILO-R007", "foreach requires a list")),
}
}
Stmt::ForRange {
binding,
start,
end,
step,
body,
} => {
let start_val = eval_expr(env, start)?;
let end_val = eval_expr(env, end)?;
let s = match start_val {
Value::Number(n) => n as i64,
_ => {
return Err(RuntimeError::new(
"ILO-R007",
"range start must be a number",
));
}
};
let e = match end_val {
Value::Number(n) => n as i64,
_ => return Err(RuntimeError::new("ILO-R007", "range end must be a number")),
};
let st: i64 = if let Some(step_expr) = step {
match eval_expr(env, step_expr)? {
Value::Number(n) => n as i64,
_ => return Err(RuntimeError::new("ILO-R007", "range step must be a number")),
}
} else {
1
};
let mut last = Value::Nil;
let mut i = s;
while i < e {
env.push_scope();
env.define(binding, Value::Number(i as f64));
let defer_mark = env.defer_stack.len();
let result = eval_body(env, body, false);
let is_err = matches!(result, Err(_) | Ok(BodyResult::Return(_)));
run_block_defers(env, defer_mark, is_err);
env.pop_scope();
match result? {
BodyResult::Return(v) => {
return Ok(Some(BodyResult::Return(v)));
}
BodyResult::Break(v) => {
last = v;
break;
}
BodyResult::Continue => {
i += st;
continue;
}
BodyResult::TailCall { .. } => {
unreachable!("TailCall escaping non-tail range body");
}
BodyResult::Value(v) => last = v,
}
i += st;
}
Ok(Some(BodyResult::Value(last)))
}
Stmt::While { condition, body } => {
let mut last = Value::Nil;
loop {
let cond = eval_expr(env, condition)?;
if !is_truthy(&cond) {
break;
}
let defer_mark = env.defer_stack.len();
let result = eval_body(env, body, false);
let is_err = matches!(result, Err(_) | Ok(BodyResult::Return(_)));
run_block_defers(env, defer_mark, is_err);
match result? {
BodyResult::Return(v) => {
return Ok(Some(BodyResult::Return(v)));
}
BodyResult::Break(v) => {
last = v;
break;
}
BodyResult::Continue => continue,
BodyResult::TailCall { .. } => {
unreachable!("TailCall escaping non-tail while body");
}
BodyResult::Value(v) => last = v,
}
}
Ok(Some(BodyResult::Value(last)))
}
Stmt::Return(expr) => eval_return_stmt(env, expr),
Stmt::Break(expr) => {
let val = match expr {
Some(e) => eval_expr(env, e)?,
None => Value::Nil,
};
Ok(Some(BodyResult::Break(val)))
}
Stmt::Continue => Ok(Some(BodyResult::Continue)),
Stmt::Defer { expr, kind } => {
env.defer_stack.push((expr.clone(), *kind));
Ok(None)
}
Stmt::Expr(expr) => {
if is_tail {
eval_tail_expr_stmt(env, expr)
} else {
let val = eval_expr(env, expr)?;
Ok(Some(BodyResult::Value(val)))
}
}
}
}
#[inline(never)]
fn eval_return_stmt(env: &mut Env, expr: &Expr) -> Result<Option<BodyResult>> {
if let Some(result) = try_synthesize_tail_call(env, expr) {
let (callee, args) = result?;
return Ok(Some(BodyResult::TailCall { callee, args }));
}
let val = eval_expr(env, expr)?;
Ok(Some(BodyResult::Return(val)))
}
#[inline(never)]
fn eval_tail_expr_stmt(env: &mut Env, expr: &Expr) -> Result<Option<BodyResult>> {
if let Some(result) = try_synthesize_tail_call(env, expr) {
let (callee, args) = result?;
return Ok(Some(BodyResult::TailCall { callee, args }));
}
let val = eval_expr(env, expr)?;
Ok(Some(BodyResult::Value(val)))
}
fn eval_expr(env: &mut Env, expr: &Expr) -> Result<Value> {
match expr {
Expr::Literal(lit) => Ok(eval_literal(lit)),
Expr::Ref(name) => env.get(name),
Expr::Field {
object,
field,
safe,
} => {
let obj = eval_expr(env, object)?;
if *safe && matches!(obj, Value::Nil) {
return Ok(Value::Nil);
}
match obj {
Value::Record { fields, .. } => match fields.get(field).cloned() {
Some(v) => Ok(v),
None if *safe => Ok(Value::Nil),
None => Err(RuntimeError::new(
"ILO-R005",
format!("no field '{}' on record", field),
)),
},
Value::World {
net,
read,
write,
run,
} => {
let v = match field.as_str() {
"net" => Value::Bool(net),
"read" => Value::Bool(read),
"write" => Value::Bool(write),
"run" => Value::Bool(run),
_other if *safe => Value::Nil,
other => {
return Err(RuntimeError::new(
"ILO-R005",
format!(
"no field '{other}' on World (known: net, read, write, run)"
),
));
}
};
Ok(v)
}
_ if *safe => Ok(Value::Nil),
_ => Err(RuntimeError::new(
"ILO-R005",
format!("cannot access field '{}' on non-record", field),
)),
}
}
Expr::Index {
object,
index,
safe,
} => {
let obj = eval_expr(env, object)?;
if *safe && matches!(obj, Value::Nil) {
return Ok(Value::Nil);
}
match obj {
Value::List(items) => items.get(*index).cloned().ok_or_else(|| {
RuntimeError::new(
"ILO-R006",
format!("list index {} out of bounds (len {})", index, items.len()),
)
}),
_ => Err(RuntimeError::new("ILO-R006", "index access on non-list")),
}
}
Expr::Call {
function,
args,
unwrap,
} => {
let mut arg_vals = Vec::new();
for arg in args {
arg_vals.push(eval_expr(env, arg)?);
}
let callee_from_scope = env
.vars
.iter()
.rev()
.find(|(k, _)| k == function.as_str())
.map(|(_, v)| v.clone());
let (callee, extra_captures) = match callee_from_scope {
Some(Value::FnRef(name)) => (name, Vec::new()),
Some(Value::Text(name)) if env.functions.contains_key(name.as_str()) => {
((*name).clone(), Vec::new())
}
Some(Value::Closure { fn_name, captures }) => (fn_name, captures),
_ => (function.clone(), Vec::new()),
};
arg_vals.extend(extra_captures);
let result = call_function(env, &callee, arg_vals)?;
{
let span = CURRENT_STMT_SPAN.with(|s| *s.borrow());
fire_expr_trace_event(expr, span, &result);
}
match *unwrap {
UnwrapMode::None => Ok(result),
UnwrapMode::Propagate => match result {
Value::Ok(v) => Ok(*v),
Value::Err(e) => Err(RuntimeError {
propagate_value: Some(Box::new(Value::Err(e))),
..RuntimeError::new("ILO-R014", "auto-unwrap propagating Err")
}),
Value::Nil => Err(RuntimeError {
propagate_value: Some(Box::new(Value::Nil)),
..RuntimeError::new("ILO-R014", "auto-unwrap propagating nil")
}),
other => Ok(other), },
UnwrapMode::Panic => match result {
Value::Ok(v) => Ok(*v),
Value::Err(e) => Err(RuntimeError::new(
"ILO-R026",
format!("panic-unwrap: {}", *e),
)),
Value::Nil => Err(RuntimeError::new(
"ILO-R026",
"panic-unwrap: expected value, got nil".to_string(),
)),
other => Ok(other), },
}
}
Expr::BinOp { op, left, right } => {
if *op == BinOp::And {
let l = eval_expr(env, left)?;
return if !is_truthy(&l) {
Ok(l)
} else {
eval_expr(env, right)
};
}
if *op == BinOp::Or {
let l = eval_expr(env, left)?;
return if is_truthy(&l) {
Ok(l)
} else {
eval_expr(env, right)
};
}
let l = eval_expr(env, left)?;
let r = eval_expr(env, right)?;
let result = eval_binop(op, &l, &r)?;
{
let span = CURRENT_STMT_SPAN.with(|s| *s.borrow());
fire_expr_trace_event(expr, span, &result);
}
Ok(result)
}
Expr::UnaryOp { op, operand } => {
let val = eval_expr(env, operand)?;
match op {
UnaryOp::Not => Ok(Value::Bool(!is_truthy(&val))),
UnaryOp::Negate => match val {
Value::Number(n) => Ok(Value::Number(-n)),
_ => Err(RuntimeError::new("ILO-R004", "cannot negate non-number")),
},
}
}
Expr::Ok(inner) => {
let val = eval_expr(env, inner)?;
Ok(Value::Ok(Box::new(val)))
}
Expr::Err(inner) => {
let val = eval_expr(env, inner)?;
Ok(Value::Err(Box::new(val)))
}
Expr::List(items) => {
let mut vals = Vec::new();
for item in items {
vals.push(eval_expr(env, item)?);
}
Ok(Value::List(Arc::new(vals)))
}
Expr::AnonRecord { fields } => {
let mut field_map = HashMap::new();
for (name, val_expr) in fields {
field_map.insert(name.clone(), eval_expr(env, val_expr)?);
}
Ok(Value::Record {
type_name: "__anon".to_string(),
fields: field_map,
})
}
Expr::Record { type_name, fields } => {
let mut field_map = HashMap::new();
for (name, val_expr) in fields {
field_map.insert(name.clone(), eval_expr(env, val_expr)?);
}
Ok(Value::Record {
type_name: type_name.clone(),
fields: field_map,
})
}
Expr::Match { subject, arms } => {
let subj = match subject {
Some(e) => eval_expr(env, e)?,
None => Value::Nil,
};
for arm in arms {
if let Some(bindings) = match_pattern(&arm.pattern, &subj) {
env.push_scope();
for (name, val) in bindings {
env.define(&name, val);
}
let result = eval_body(env, &arm.body, false);
env.pop_scope();
return match result? {
BodyResult::Value(v) | BodyResult::Return(v) | BodyResult::Break(v) => {
Ok(v)
}
BodyResult::Continue => Ok(Value::Nil),
BodyResult::TailCall { .. } => {
unreachable!("TailCall escaping value-producing Match arm")
}
};
}
}
Ok(Value::Nil)
}
Expr::NilCoalesce { value, default } => {
let val = eval_expr(env, value)?;
if matches!(val, Value::Nil) {
eval_expr(env, default)
} else {
Ok(val)
}
}
Expr::Ternary {
condition,
then_expr,
else_expr,
} => {
let cond = eval_expr(env, condition)?;
if is_truthy(&cond) {
eval_expr(env, then_expr)
} else {
eval_expr(env, else_expr)
}
}
Expr::With { object, updates } => {
let obj = eval_expr(env, object)?;
match obj {
Value::Record {
type_name,
mut fields,
} => {
for (name, val_expr) in updates {
fields.insert(name.clone(), eval_expr(env, val_expr)?);
}
Ok(Value::Record { type_name, fields })
}
_ => Err(RuntimeError::new("ILO-R008", "'with' requires a record")),
}
}
Expr::MakeClosure { fn_name, captures } => {
let mut cap_vals = Vec::with_capacity(captures.len());
for c in captures {
cap_vals.push(eval_expr(env, c)?);
}
Ok(Value::Closure {
fn_name: fn_name.clone(),
captures: cap_vals,
})
}
Expr::Todo(reason) => {
let msg = match eval_expr(env, reason)? {
Value::Text(s) => s.to_string(),
v => format!("{v}"),
};
Err(RuntimeError::new("ILO-R020", format!("todo: {msg}")))
}
Expr::Panic(reason) => {
let msg = match eval_expr(env, reason)? {
Value::Text(s) => s.to_string(),
v => format!("{v}"),
};
Err(RuntimeError::new("ILO-R021", format!("panic: {msg}")))
}
}
}
fn eval_literal(lit: &Literal) -> Value {
match lit {
Literal::Number(n) => Value::Number(*n),
Literal::Text(s) => Value::Text(Arc::new(s.clone())),
Literal::Bool(b) => Value::Bool(*b),
Literal::Nil => Value::Nil,
}
}
fn eval_binop(op: &BinOp, left: &Value, right: &Value) -> Result<Value> {
match (op, left, right) {
(BinOp::Add, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
(BinOp::Subtract, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a - b)),
(BinOp::Multiply, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)),
(BinOp::Divide, Value::Number(a), Value::Number(b)) => {
if *b == 0.0 {
Err(RuntimeError::new("ILO-R003", "division by zero"))
} else {
Ok(Value::Number(a / b))
}
}
(BinOp::Add, Value::Text(a), Value::Text(b)) => {
let mut out = String::with_capacity(a.len() + b.len());
out.push_str(a);
out.push_str(b);
Ok(Value::Text(Arc::new(out)))
}
(BinOp::Add, Value::List(a), Value::List(b)) => {
let mut out = Vec::with_capacity(a.len() + b.len());
out.extend_from_slice(a);
out.extend_from_slice(b);
Ok(Value::List(Arc::new(out)))
}
(BinOp::GreaterThan, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a > b)),
(BinOp::LessThan, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a < b)),
(BinOp::GreaterOrEqual, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a >= b)),
(BinOp::LessOrEqual, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a <= b)),
(BinOp::GreaterThan, Value::Text(a), Value::Text(b)) => Ok(Value::Bool(a > b)),
(BinOp::LessThan, Value::Text(a), Value::Text(b)) => Ok(Value::Bool(a < b)),
(BinOp::GreaterOrEqual, Value::Text(a), Value::Text(b)) => Ok(Value::Bool(a >= b)),
(BinOp::LessOrEqual, Value::Text(a), Value::Text(b)) => Ok(Value::Bool(a <= b)),
(BinOp::Append, Value::List(items), val) => {
let mut new_items = (**items).clone();
new_items.push(val.clone());
Ok(Value::List(Arc::new(new_items)))
}
(BinOp::Equals, a, b) => Ok(Value::Bool(values_equal(a, b))),
(BinOp::NotEquals, a, b) => Ok(Value::Bool(!values_equal(a, b))),
_ => Err(RuntimeError::new(
"ILO-R004",
format!(
"unsupported operation: {:?} on {:?} and {:?}",
op, left, right
),
)),
}
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON,
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Nil, Value::Nil) => true,
_ => false,
}
}
fn is_truthy(val: &Value) -> bool {
match val {
Value::Bool(b) => *b,
Value::Nil => false,
Value::Number(n) => *n != 0.0,
Value::Text(s) => !s.is_empty(),
Value::List(l) => !l.is_empty(),
_ => true,
}
}
fn match_pattern(pattern: &Pattern, value: &Value) -> Option<Vec<(String, Value)>> {
match pattern {
Pattern::Wildcard => Some(vec![("_".to_string(), value.clone())]),
Pattern::Ok(binding) => {
if let Value::Ok(inner) = value {
Some(vec![(binding.clone(), *inner.clone())])
} else {
None
}
}
Pattern::Err(binding) => {
if let Value::Err(inner) = value {
Some(vec![(binding.clone(), *inner.clone())])
} else {
None
}
}
Pattern::Literal(lit) => {
let expected = eval_literal(lit);
if values_equal(&expected, value) {
Some(vec![])
} else {
None
}
}
Pattern::TypeIs { ty, binding } => {
let matches = match ty {
Type::Number => matches!(value, Value::Number(_)),
Type::Text => matches!(value, Value::Text(_)),
Type::Bool => matches!(value, Value::Bool(_)),
Type::List(_) => matches!(value, Value::List(_)),
_ => false,
};
if matches {
Some(vec![(binding.clone(), value.clone())])
} else {
None
}
}
Pattern::Variant { tag, binding } => {
if tag == "nil" && matches!(value, Value::Nil) {
return Some(vec![]);
}
if let Value::Variant {
tag: vtag, payload, ..
} = value
{
if vtag == tag {
let mut bindings = vec![];
if let Some(b) = binding {
if b != "_" {
let pval = payload.as_deref().cloned().unwrap_or(Value::Nil);
bindings.push((b.clone(), pval));
}
}
Some(bindings)
} else {
None
}
} else {
None
}
}
Pattern::Or(alts) => {
for alt in alts {
if let Some(bindings) = match_pattern(alt, value) {
return Some(bindings);
}
}
None
}
}
}
fn next_pow2(n: usize) -> usize {
if n <= 1 {
return 1;
}
let mut p = 1usize;
while p < n {
p <<= 1;
}
p
}
pub(crate) fn cooley_tukey(re: &mut [f64], im: &mut [f64], inverse: bool) {
let n = re.len();
debug_assert_eq!(n, im.len());
if n <= 1 {
return;
}
debug_assert!(n.is_power_of_two());
let mut j = 0usize;
for i in 1..n {
let mut bit = n >> 1;
while j & bit != 0 {
j ^= bit;
bit >>= 1;
}
j ^= bit;
if i < j {
re.swap(i, j);
im.swap(i, j);
}
}
let sign: f64 = if inverse { 1.0 } else { -1.0 };
let mut len = 2usize;
while len <= n {
let half = len / 2;
let theta = sign * 2.0 * std::f64::consts::PI / (len as f64);
let w_re = theta.cos();
let w_im = theta.sin();
let mut i = 0usize;
while i < n {
let mut cur_re = 1.0_f64;
let mut cur_im = 0.0_f64;
for k in 0..half {
let a_re = re[i + k];
let a_im = im[i + k];
let b_re = re[i + k + half] * cur_re - im[i + k + half] * cur_im;
let b_im = re[i + k + half] * cur_im + im[i + k + half] * cur_re;
re[i + k] = a_re + b_re;
im[i + k] = a_im + b_im;
re[i + k + half] = a_re - b_re;
im[i + k + half] = a_im - b_im;
let new_re = cur_re * w_re - cur_im * w_im;
let new_im = cur_re * w_im + cur_im * w_re;
cur_re = new_re;
cur_im = new_im;
}
i += len;
}
len <<= 1;
}
if inverse {
let scale = 1.0 / (n as f64);
for x in re.iter_mut() {
*x *= scale;
}
for x in im.iter_mut() {
*x *= scale;
}
}
}
pub(crate) const GET_MANY_MAX_CONCURRENCY: usize = 10;
pub(crate) const RUN_OUTPUT_CAP: usize = 10 * 1024 * 1024;
fn rdin_impl() -> Result<Value> {
#[cfg(target_family = "wasm")]
{
return Ok(Value::Err(Box::new(Value::Text(Arc::new(
"rdin: stdin not available on wasm".to_string(),
)))));
}
#[cfg(not(target_family = "wasm"))]
{
use std::io::Read;
let mut buf = String::new();
Ok(match std::io::stdin().read_to_string(&mut buf) {
Ok(_) => Value::Ok(Box::new(Value::Text(Arc::new(buf)))),
Err(e) => Value::Err(Box::new(Value::Text(Arc::new(e.to_string())))),
})
}
}
fn rdinl_impl() -> Result<Value> {
#[cfg(target_family = "wasm")]
{
return Ok(Value::Err(Box::new(Value::Text(Arc::new(
"rdinl: stdin not available on wasm".to_string(),
)))));
}
#[cfg(not(target_family = "wasm"))]
{
use std::io::BufRead;
let stdin = std::io::stdin();
let lines: std::result::Result<Vec<String>, std::io::Error> =
stdin.lock().lines().collect();
Ok(match lines {
Ok(ls) => {
let items: Vec<Value> = ls.into_iter().map(|l| Value::Text(Arc::new(l))).collect();
Value::Ok(Box::new(Value::List(Arc::new(items))))
}
Err(e) => Value::Err(Box::new(Value::Text(Arc::new(e.to_string())))),
})
}
}
#[cfg(not(target_family = "wasm"))]
fn is_secret_env_var(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
upper.starts_with("ANTHROPIC_")
|| upper.starts_with("CLAUDE_")
|| upper == "GITHUB_TOKEN"
|| upper == "GITHUB_PAT"
|| upper.ends_with("_TOKEN")
|| upper.ends_with("_KEY")
|| upper.ends_with("_SECRET")
|| upper.ends_with("_PASSWORD")
|| upper.ends_with("_PASSWD")
|| upper.ends_with("_CREDENTIAL")
|| upper.ends_with("_CREDENTIALS")
}
fn http_stream_headers(name: &str, v: &Value) -> Result<Vec<(String, String)>> {
match v {
Value::Map(m) => Ok(m
.iter()
.map(|(k, val)| {
let vs: String = match val {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(k.to_display_string(), vs)
})
.collect()),
other => Err(RuntimeError::new(
"ILO-R009",
format!("{name}: headers must be M t t, got {:?}", other),
)),
}
}
fn http_stream_get_dispatch(
env: &mut Env,
url_v: &Value,
headers_v: Option<&Value>,
) -> Result<Value> {
let name = if headers_v.is_some() {
"get-stream-h"
} else {
"get-stream"
};
let url = match url_v {
Value::Text(u) => u.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: first arg must be text (url), got {:?}", other),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::LazyHttpLines(HttpLinesHandle::from_lines(
errored_lines(msg),
)));
}
let headers = match headers_v {
Some(v) => http_stream_headers(name, v)?,
None => vec![],
};
let backend = http_wasm::default_backend();
match backend.get_stream(url.as_str(), &headers) {
Ok(iter) => Ok(Value::LazyHttpLines(HttpLinesHandle::from_lines(iter))),
Err(msg) => Ok(Value::LazyHttpLines(HttpLinesHandle::from_lines(
errored_lines(msg),
))),
}
}
fn errored_lines(
msg: String,
) -> Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send> {
Box::new(std::iter::once(Err(std::io::Error::other(msg))))
}
fn http_stream_post_dispatch(
env: &mut Env,
url_v: &Value,
body_v: &Value,
headers_v: Option<&Value>,
) -> Result<Value> {
let name = if headers_v.is_some() {
"pst-stream-h"
} else {
"pst-stream"
};
let (url, body) = match (url_v, body_v) {
(Value::Text(u), Value::Text(b)) => (u.clone(), b.clone()),
_ => {
return Err(RuntimeError::new(
"ILO-R009",
format!("{name}: requires (t, t), got ({:?}, {:?})", url_v, body_v),
));
}
};
if let Err(msg) = env.caps.check_net(url.as_str()) {
return Ok(Value::LazyHttpLines(HttpLinesHandle::from_lines(
errored_lines(msg),
)));
}
let headers = match headers_v {
Some(v) => http_stream_headers(name, v)?,
None => vec![],
};
let backend = http_wasm::default_backend();
match backend.post_stream(url.as_str(), body.as_str(), &headers) {
Ok(iter) => Ok(Value::LazyHttpLines(HttpLinesHandle::from_lines(iter))),
Err(msg) => Ok(Value::LazyHttpLines(HttpLinesHandle::from_lines(
errored_lines(msg),
))),
}
}
fn for_line_impl(source: &Value) -> Result<Value> {
match source {
Value::Text(s) if s.as_str() == "stdin" => {
#[cfg(target_family = "wasm")]
{
return Ok(Value::Err(Box::new(Value::Text(Arc::new(
"for-line: stdin not available on wasm".to_string(),
)))));
}
#[cfg(not(target_family = "wasm"))]
{
Ok(Value::LazyStdinLines(StdinLinesHandle::new()))
}
}
other => Err(RuntimeError::new(
"ILO-R009",
format!(
"for-line: argument must be the text \"stdin\", got {:?}",
other
),
)),
}
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn run_spawn(cmd: &str, argv: &[String]) -> Value {
run_spawn_inner(cmd, argv, false)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn run_spawn_full_env(cmd: &str, argv: &[String]) -> Value {
run_spawn_inner(cmd, argv, true)
}
#[cfg(not(target_family = "wasm"))]
fn run_spawn_inner(cmd: &str, argv: &[String], inherit_full_env: bool) -> Value {
use std::process::{Command, Stdio};
let mut command = Command::new(cmd);
command
.args(argv)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if !inherit_full_env {
for (key, _) in std::env::vars_os() {
if let Some(k) = key.to_str() {
if is_secret_env_var(k) {
command.env_remove(k);
}
}
}
}
let mut child = match command.spawn() {
Ok(c) => c,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: failed to spawn {cmd:?}: {e}"
)))));
}
};
let mut stdout_pipe = child.stdout.take();
let mut stderr_pipe = child.stderr.take();
let (stdout_res, stderr_res) = std::thread::scope(|s| {
let so = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stdout_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let se = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stderr_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let so = so
.join()
.unwrap_or_else(|_| Err("stdout reader panicked".to_string()));
let se = se
.join()
.unwrap_or_else(|_| Err("stderr reader panicked".to_string()));
(so, se)
});
let stdout_buf = match stdout_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: stdout capture failed: {e}"
)))));
}
};
let stderr_buf = match stderr_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: stderr capture failed: {e}"
)))));
}
};
let status = match child.wait() {
Ok(s) => s,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: wait failed: {e}"
)))));
}
};
let stdout = String::from_utf8_lossy(&stdout_buf).into_owned();
let stderr = String::from_utf8_lossy(&stderr_buf).into_owned();
let code = status.code().map(|c| c.to_string()).unwrap_or_else(|| {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = status.signal() {
return format!("signal:{sig}");
}
}
"unknown".to_string()
});
let mut m: HashMap<MapKey, Value> = HashMap::with_capacity(3);
m.insert(
MapKey::Text("stdout".to_string()),
Value::Text(Arc::new(stdout)),
);
m.insert(
MapKey::Text("stderr".to_string()),
Value::Text(Arc::new(stderr)),
);
m.insert(
MapKey::Text("code".to_string()),
Value::Text(Arc::new(code)),
);
Value::Ok(Box::new(Value::Map(Arc::new(m))))
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn run_spawn_structured(cmd: &str, argv: &[String]) -> Value {
run_spawn_structured_inner(cmd, argv, false)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn run_spawn_structured_full_env(cmd: &str, argv: &[String]) -> Value {
run_spawn_structured_inner(cmd, argv, true)
}
#[cfg(not(target_family = "wasm"))]
fn run_spawn_structured_inner(cmd: &str, argv: &[String], inherit_full_env: bool) -> Value {
use std::process::{Command, Stdio};
let mut command = Command::new(cmd);
command
.args(argv)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if !inherit_full_env {
for (key, _) in std::env::vars_os() {
if let Some(k) = key.to_str() {
if is_secret_env_var(k) {
command.env_remove(k);
}
}
}
}
let mut child = match command.spawn() {
Ok(c) => c,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: failed to spawn {cmd:?}: {e}"
)))));
}
};
let mut stdout_pipe = child.stdout.take();
let mut stderr_pipe = child.stderr.take();
let (stdout_res, stderr_res) = std::thread::scope(|s| {
let so = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stdout_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let se = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stderr_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let so = so
.join()
.unwrap_or_else(|_| Err("stdout reader panicked".to_string()));
let se = se
.join()
.unwrap_or_else(|_| Err("stderr reader panicked".to_string()));
(so, se)
});
let stdout_buf = match stdout_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: stdout capture failed: {e}"
)))));
}
};
let stderr_buf = match stderr_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: stderr capture failed: {e}"
)))));
}
};
let status = match child.wait() {
Ok(s) => s,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: wait failed: {e}"
)))));
}
};
let stdout = String::from_utf8_lossy(&stdout_buf).into_owned();
let stderr = String::from_utf8_lossy(&stderr_buf).into_owned();
let exit_code: f64 = status.code().map(|c| c as f64).unwrap_or_else(|| {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if status.signal().is_some() {
return -1.0;
}
}
-1.0
});
let mut fields = HashMap::with_capacity(3);
fields.insert("stdout".to_string(), Value::Text(Arc::new(stdout)));
fields.insert("stderr".to_string(), Value::Text(Arc::new(stderr)));
fields.insert("exit".to_string(), Value::Number(exit_code));
Value::Ok(Box::new(Value::Record {
type_name: "RunResult".to_string(),
fields,
}))
}
#[cfg(target_family = "wasm")]
pub(crate) fn run_spawn(_cmd: &str, _argv: &[String]) -> Value {
Value::Err(Box::new(Value::Text(Arc::new(
"run: process spawn not available on wasm".to_string(),
))))
}
#[cfg(target_family = "wasm")]
pub(crate) fn run_spawn_structured(_cmd: &str, _argv: &[String]) -> Value {
Value::Err(Box::new(Value::Text(Arc::new(
"run2: process spawn not available on wasm".to_string(),
))))
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn run_spawn_with_stdin(cmd: &str, argv: &[String], stdin_text: &str) -> Value {
use std::io::Write;
use std::process::{Command, Stdio};
let mut command = Command::new(cmd);
command
.args(argv)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = match command.spawn() {
Ok(c) => c,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: failed to spawn {cmd:?}: {e}"
)))));
}
};
if let Some(mut stdin_pipe) = child.stdin.take() {
if let Err(e) = stdin_pipe.write_all(stdin_text.as_bytes()) {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: failed to write stdin: {e}"
)))));
}
}
let mut stdout_pipe = child.stdout.take();
let mut stderr_pipe = child.stderr.take();
let (stdout_res, stderr_res) = std::thread::scope(|s| {
let so = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stdout_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let se = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stderr_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let so = so
.join()
.unwrap_or_else(|_| Err("stdout reader panicked".to_string()));
let se = se
.join()
.unwrap_or_else(|_| Err("stderr reader panicked".to_string()));
(so, se)
});
let stdout_buf = match stdout_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: stdout capture failed: {e}"
)))));
}
};
let stderr_buf = match stderr_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: stderr capture failed: {e}"
)))));
}
};
let status = match child.wait() {
Ok(s) => s,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run: wait failed: {e}"
)))));
}
};
let stdout = String::from_utf8_lossy(&stdout_buf).into_owned();
let stderr = String::from_utf8_lossy(&stderr_buf).into_owned();
let code = status.code().map(|c| c.to_string()).unwrap_or_else(|| {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = status.signal() {
return format!("signal:{sig}");
}
}
"unknown".to_string()
});
let mut m: HashMap<MapKey, Value> = HashMap::with_capacity(3);
m.insert(
MapKey::Text("stdout".to_string()),
Value::Text(Arc::new(stdout)),
);
m.insert(
MapKey::Text("stderr".to_string()),
Value::Text(Arc::new(stderr)),
);
m.insert(
MapKey::Text("code".to_string()),
Value::Text(Arc::new(code)),
);
Value::Ok(Box::new(Value::Map(Arc::new(m))))
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn run_spawn_structured_with_stdin(
cmd: &str,
argv: &[String],
stdin_text: &str,
) -> Value {
use std::io::Write;
use std::process::{Command, Stdio};
let mut command = Command::new(cmd);
command
.args(argv)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = match command.spawn() {
Ok(c) => c,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: failed to spawn {cmd:?}: {e}"
)))));
}
};
if let Some(mut stdin_pipe) = child.stdin.take() {
if let Err(e) = stdin_pipe.write_all(stdin_text.as_bytes()) {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: failed to write stdin: {e}"
)))));
}
}
let mut stdout_pipe = child.stdout.take();
let mut stderr_pipe = child.stderr.take();
let (stdout_res, stderr_res) = std::thread::scope(|s| {
let so = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stdout_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let se = s.spawn(|| -> std::result::Result<Vec<u8>, String> {
let mut buf = Vec::new();
if let Some(p) = stderr_pipe.as_mut() {
read_capped(p, &mut buf, RUN_OUTPUT_CAP)?;
}
Ok(buf)
});
let so = so
.join()
.unwrap_or_else(|_| Err("stdout reader panicked".to_string()));
let se = se
.join()
.unwrap_or_else(|_| Err("stderr reader panicked".to_string()));
(so, se)
});
let stdout_buf = match stdout_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: stdout capture failed: {e}"
)))));
}
};
let stderr_buf = match stderr_res {
Ok(b) => b,
Err(e) => {
let _ = child.kill();
let _ = child.wait();
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: stderr capture failed: {e}"
)))));
}
};
let status = match child.wait() {
Ok(s) => s,
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"run2: wait failed: {e}"
)))));
}
};
let stdout = String::from_utf8_lossy(&stdout_buf).into_owned();
let stderr = String::from_utf8_lossy(&stderr_buf).into_owned();
let exit_code: f64 = status.code().map(|c| c as f64).unwrap_or_else(|| {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if status.signal().is_some() {
return -1.0;
}
}
-1.0
});
let mut fields = HashMap::with_capacity(3);
fields.insert("stdout".to_string(), Value::Text(Arc::new(stdout)));
fields.insert("stderr".to_string(), Value::Text(Arc::new(stderr)));
fields.insert("exit".to_string(), Value::Number(exit_code));
Value::Ok(Box::new(Value::Record {
type_name: "RunResult".to_string(),
fields,
}))
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn run_spawn_bg(cmd: &str, argv: &[String]) -> Value {
use std::process::{Command, Stdio};
let mut command = Command::new(cmd);
command
.args(argv)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match command.spawn() {
Ok(child) => {
let pid = child.id() as f64;
Value::Ok(Box::new(Value::Number(pid)))
}
Err(e) => Value::Err(Box::new(Value::Text(Arc::new(format!(
"run-bg: failed to spawn {cmd:?}: {e}"
))))),
}
}
#[cfg(target_family = "wasm")]
pub(crate) fn run_spawn_with_stdin(_cmd: &str, _argv: &[String], _stdin: &str) -> Value {
Value::Err(Box::new(Value::Text(Arc::new(
"run: process spawn not available on wasm".to_string(),
))))
}
#[cfg(target_family = "wasm")]
pub(crate) fn run_spawn_structured_with_stdin(_cmd: &str, _argv: &[String], _stdin: &str) -> Value {
Value::Err(Box::new(Value::Text(Arc::new(
"run2: process spawn not available on wasm".to_string(),
))))
}
#[cfg(target_family = "wasm")]
pub(crate) fn run_spawn_bg(_cmd: &str, _argv: &[String]) -> Value {
Value::Err(Box::new(Value::Text(Arc::new(
"run-bg: process spawn not available on wasm".to_string(),
))))
}
#[cfg(target_family = "wasm")]
pub(crate) fn run_spawn_full_env(_cmd: &str, _argv: &[String]) -> Value {
Value::Err(Box::new(Value::Text(Arc::new(
"run-full-env: process spawn not available on wasm".to_string(),
))))
}
#[cfg(target_family = "wasm")]
pub(crate) fn run_spawn_structured_full_env(_cmd: &str, _argv: &[String]) -> Value {
Value::Err(Box::new(Value::Text(Arc::new(
"run2-full-env: process spawn not available on wasm".to_string(),
))))
}
#[cfg(not(target_family = "wasm"))]
fn read_capped<R: std::io::Read>(
reader: &mut R,
buf: &mut Vec<u8>,
cap: usize,
) -> std::result::Result<(), String> {
let mut chunk = [0u8; 8192];
loop {
match reader.read(&mut chunk) {
Ok(0) => return Ok(()),
Ok(n) => {
if buf.len() + n > cap {
return Err(format!(
"captured output exceeded cap ({cap} bytes); kill child + abort"
));
}
buf.extend_from_slice(&chunk[..n]);
}
Err(e) => return Err(format!("read error: {e}")),
}
}
}
#[cfg(feature = "http")]
pub(crate) fn http_response_to_ok_map(resp: &minreq::Response) -> Value {
let body = match resp.as_str() {
Ok(b) => b.to_string(),
Err(e) => {
return Value::Err(Box::new(Value::Text(Arc::new(format!(
"response is not valid UTF-8: {e}"
)))));
}
};
let mut headers_map: HashMap<MapKey, Value> = HashMap::with_capacity(resp.headers.len());
for (k, v) in resp.headers.iter() {
headers_map.insert(MapKey::Text(k.clone()), Value::Text(Arc::new(v.clone())));
}
let mut m: HashMap<MapKey, Value> = HashMap::with_capacity(3);
m.insert(
MapKey::Text("status".to_string()),
Value::Number(resp.status_code as f64),
);
m.insert(
MapKey::Text("headers".to_string()),
Value::Map(Arc::new(headers_map)),
);
m.insert(
MapKey::Text("body".to_string()),
Value::Text(Arc::new(body)),
);
Value::Ok(Box::new(Value::Map(Arc::new(m))))
}
pub(crate) fn get_many_fetch(urls: &[String]) -> Vec<Value> {
if urls.is_empty() {
return Vec::new();
}
let mut results: Vec<Value> = (0..urls.len()).map(|_| Value::Nil).collect();
#[cfg(feature = "http")]
{
let chunks: Vec<(usize, &[String])> = urls
.chunks(GET_MANY_MAX_CONCURRENCY)
.enumerate()
.map(|(i, c)| (i * GET_MANY_MAX_CONCURRENCY, c))
.collect();
for (base, chunk) in chunks {
std::thread::scope(|s| {
let mut handles = Vec::with_capacity(chunk.len());
for url in chunk.iter() {
let u = url.clone();
handles.push(s.spawn(move || match minreq::get(u.as_str()).send() {
Ok(resp) => match resp.as_str() {
Ok(body) => {
Value::Ok(Box::new(Value::Text(Arc::new(body.to_string()))))
}
Err(e) => Value::Err(Box::new(Value::Text(Arc::new(format!(
"response is not valid UTF-8: {e}"
))))),
},
Err(e) => Value::Err(Box::new(Value::Text(Arc::new(e.to_string())))),
}));
}
for (i, h) in handles.into_iter().enumerate() {
let v = h.join().unwrap_or_else(|_| {
Value::Err(Box::new(Value::Text(Arc::new(
"worker thread panicked".to_string(),
))))
});
results[base + i] = v;
}
});
}
}
#[cfg(not(feature = "http"))]
{
for slot in results.iter_mut() {
*slot = Value::Err(Box::new(Value::Text(
"http feature not enabled".to_string().into(),
)));
}
}
results
}
fn par_map_default_concurrency() -> usize {
if let Ok(s) = std::env::var("ILO_PAR_MAP_CONCURRENCY") {
if let Ok(n) = s.trim().parse::<usize>() {
if n > 0 {
return n;
}
}
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
}
fn par_map_chunk_size(n_items: usize, n_threads: usize) -> usize {
let t = n_threads.max(1);
n_items.div_ceil(t)
}
#[inline(never)]
fn par_map_run(
fn_name: &str,
captures: Vec<Value>,
items: &[Value],
concurrency: usize,
fns: HashMap<String, Decl>,
caps: Arc<Caps>,
) -> Vec<Value> {
use std::sync::atomic::{AtomicBool, Ordering};
if items.is_empty() {
return Vec::new();
}
let n_threads = concurrency.max(1);
let chunk_size = par_map_chunk_size(items.len(), n_threads);
let mut results: Vec<Value> = vec![Value::Nil; items.len()];
let cancelled = Arc::new(AtomicBool::new(false));
std::thread::scope(|s| {
let mut handles = Vec::new();
for (chunk_idx, chunk) in items.chunks(chunk_size).enumerate() {
let base = chunk_idx * chunk_size;
let chunk_items: Vec<Value> = chunk.to_vec();
let fn_name_owned = fn_name.to_string();
let captures = captures.clone();
let fns = fns.clone();
let caps = caps.clone();
let cancelled = cancelled.clone();
handles.push((
base,
chunk_items.len(),
s.spawn(move || {
let mut worker_env = Env::with_caps(caps);
worker_env.functions = fns;
let mut local: Vec<Value> = Vec::with_capacity(chunk_items.len());
for item in chunk_items {
if cancelled.load(Ordering::Relaxed) {
local.push(Value::Err(Box::new(Value::Text(Arc::new(
"par-map: cancelled due to earlier error".to_string(),
)))));
continue;
}
let mut call_args = vec![item];
call_args.extend(captures.iter().cloned());
let result = match call_function(&mut worker_env, &fn_name_owned, call_args)
{
Ok(v) => Value::Ok(Box::new(v)),
Err(e) => {
cancelled.store(true, Ordering::Relaxed);
Value::Err(Box::new(Value::Text(Arc::new(e.message.clone()))))
}
};
local.push(result);
}
local
}),
));
}
for (base, len, handle) in handles {
let local = handle.join().unwrap_or_else(|_| {
vec![
Value::Err(Box::new(Value::Text(Arc::new(
"par-map worker thread panicked".to_string(),
))));
len
]
});
for (i, v) in local.into_iter().enumerate() {
results[base + i] = v;
}
}
});
results
}
fn dtparse_rel(phrase: &str, now_epoch: f64) -> Value {
use chrono::{Datelike, Duration, NaiveDate, TimeZone, Utc, Weekday};
let make_ok = |epoch: i64| Value::Ok(Box::new(Value::Number(epoch as f64)));
let make_err = |msg: String| Value::Err(Box::new(Value::Text(Arc::new(msg))));
let now_secs = if now_epoch.is_finite() {
now_epoch as i64
} else {
return make_err(format!(
"dtparse-rel: now epoch is not finite ({now_epoch})"
));
};
let now_dt = match Utc.timestamp_opt(now_secs, 0).single() {
Some(dt) => dt,
None => return make_err(format!("dtparse-rel: now epoch out of range ({now_secs})")),
};
let today: NaiveDate = now_dt.date_naive();
let s = phrase.trim().to_ascii_lowercase();
if s == "today" {
let epoch = today.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
return make_ok(epoch);
}
if s == "yesterday" {
let d = today - Duration::days(1);
return make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp());
}
if s == "tomorrow" {
let d = today + Duration::days(1);
return make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp());
}
fn parse_weekday(name: &str) -> Option<Weekday> {
match name {
"monday" | "mon" => Some(Weekday::Mon),
"tuesday" | "tue" => Some(Weekday::Tue),
"wednesday" | "wed" => Some(Weekday::Wed),
"thursday" | "thu" => Some(Weekday::Thu),
"friday" | "fri" => Some(Weekday::Fri),
"saturday" | "sat" => Some(Weekday::Sat),
"sunday" | "sun" => Some(Weekday::Sun),
_ => None,
}
}
if let Some(day_name) = s.strip_prefix("last ") {
return match parse_weekday(day_name.trim()) {
None => make_err(format!("dtparse-rel: unknown weekday '{day_name}'")),
Some(target) => {
let today_num = today.weekday().num_days_from_monday(); let target_num = target.num_days_from_monday();
let diff = (today_num + 7 - target_num) % 7;
let diff = if diff == 0 { 7 } else { diff };
let d = today - Duration::days(diff as i64);
make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp())
}
};
}
if let Some(day_name) = s.strip_prefix("next ") {
return match parse_weekday(day_name.trim()) {
None => make_err(format!("dtparse-rel: unknown weekday '{day_name}'")),
Some(target) => {
let today_num = today.weekday().num_days_from_monday();
let target_num = target.num_days_from_monday();
let diff = (target_num + 7 - today_num) % 7;
let diff = if diff == 0 { 7 } else { diff };
let d = today + Duration::days(diff as i64);
make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp())
}
};
}
if let Some(day_name) = s.strip_prefix("this ") {
return match parse_weekday(day_name.trim()) {
None => make_err(format!("dtparse-rel: unknown weekday '{day_name}'")),
Some(target) => {
let today_num = today.weekday().num_days_from_monday() as i64;
let target_num = target.num_days_from_monday() as i64;
let d = today + Duration::days(target_num - today_num);
make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp())
}
};
}
fn parse_unsigned_count(rest: &str) -> Option<i64> {
let t = rest.trim();
if t.is_empty() || !t.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
t.parse::<i64>().ok()
}
let ago_days = s
.strip_suffix(" days ago")
.or_else(|| s.strip_suffix(" day ago"));
if let Some(rest) = ago_days
&& let Some(n) = parse_unsigned_count(rest)
{
let d = today - Duration::days(n);
return make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp());
}
let in_days = s
.strip_prefix("in ")
.and_then(|r| r.strip_suffix(" days").or_else(|| r.strip_suffix(" day")));
if let Some(rest) = in_days
&& let Some(n) = parse_unsigned_count(rest)
{
let d = today + Duration::days(n);
return make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp());
}
let ago_weeks = s
.strip_suffix(" weeks ago")
.or_else(|| s.strip_suffix(" week ago"));
if let Some(rest) = ago_weeks
&& let Some(n) = parse_unsigned_count(rest)
{
let d = today - Duration::weeks(n);
return make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp());
}
let in_weeks = s
.strip_prefix("in ")
.and_then(|r| r.strip_suffix(" weeks").or_else(|| r.strip_suffix(" week")));
if let Some(rest) = in_weeks
&& let Some(n) = parse_unsigned_count(rest)
{
let d = today + Duration::weeks(n);
return make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp());
}
fn add_months(date: NaiveDate, months: i32) -> Option<NaiveDate> {
let total_months = date.year() * 12 + (date.month() as i32 - 1) + months;
let y = total_months.div_euclid(12);
let m = (total_months.rem_euclid(12) + 1) as u32;
let max_day = days_in_month(y, m);
let d = date.day().min(max_day);
NaiveDate::from_ymd_opt(y, m, d)
}
fn days_in_month(year: i32, month: u32) -> u32 {
let next = if month == 12 {
NaiveDate::from_ymd_opt(year + 1, 1, 1)
} else {
NaiveDate::from_ymd_opt(year, month + 1, 1)
};
(next.unwrap() - NaiveDate::from_ymd_opt(year, month, 1).unwrap()).num_days() as u32
}
let ago_months = s
.strip_suffix(" months ago")
.or_else(|| s.strip_suffix(" month ago"));
if let Some(rest) = ago_months
&& let Some(n) = parse_unsigned_count(rest)
{
let n = n as i32;
return match add_months(today, -n) {
Some(d) => make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp()),
None => make_err(format!(
"dtparse-rel: month arithmetic out of range in '{phrase}'"
)),
};
}
let in_months = s.strip_prefix("in ").and_then(|r| {
r.strip_suffix(" months")
.or_else(|| r.strip_suffix(" month"))
});
if let Some(rest) = in_months
&& let Some(n) = parse_unsigned_count(rest)
{
let n = n as i32;
return match add_months(today, n) {
Some(d) => make_ok(d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp()),
None => make_err(format!(
"dtparse-rel: month arithmetic out of range in '{phrase}'"
)),
};
}
if let Ok(nd) = chrono::NaiveDate::parse_from_str(phrase.trim(), "%Y-%m-%d") {
let epoch = nd.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
return make_ok(epoch);
}
make_err(format!(
"dtparse-rel: unrecognised phrase '{phrase}' — expected: today/yesterday/tomorrow, \
N days/weeks/months ago, in N days/weeks/months, last/next/this <weekday>, or YYYY-MM-DD"
))
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
use crate::lexer;
use crate::parser;
static ENV_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn parse_program(source: &str) -> Program {
let tokens = lexer::lex(source).unwrap();
let token_spans: Vec<(crate::lexer::Token, crate::ast::Span)> = tokens
.into_iter()
.map(|(t, r)| {
(
t,
crate::ast::Span {
start: r.start,
end: r.end,
},
)
})
.collect();
let (prog, errors) = parser::parse(token_spans);
assert!(errors.is_empty(), "parse errors: {:?}", errors);
prog
}
fn run_str(source: &str, func: Option<&str>, args: Vec<Value>) -> Value {
let prog = parse_program(source);
run(&prog, func, args).unwrap()
}
#[test]
fn interpret_tot() {
let source = std::fs::read_to_string("examples/01-simple-function.ilo").unwrap();
let result = run_str(
&source,
Some("tot"),
vec![
Value::Number(10.0),
Value::Number(20.0),
Value::Number(30.0),
],
);
assert_eq!(result, Value::Number(6200.0));
}
#[test]
fn interpret_tot_different_args() {
let source = "tot p:n q:n r:n>n;s=*p q;t=*s r;+s t";
let result = run_str(
source,
Some("tot"),
vec![Value::Number(2.0), Value::Number(3.0), Value::Number(4.0)],
);
assert_eq!(result, Value::Number(30.0));
}
#[test]
fn interpret_cls_gold() {
let source = r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#;
let result = run_str(source, Some("cls"), vec![Value::Number(1000.0)]);
assert_eq!(result, Value::Text(Arc::new("gold".to_string())));
}
#[test]
fn interpret_cls_silver() {
let source = r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#;
let result = run_str(source, Some("cls"), vec![Value::Number(500.0)]);
assert_eq!(result, Value::Text(Arc::new("silver".to_string())));
}
#[test]
fn interpret_cls_bronze() {
let source = r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#;
let result = run_str(source, Some("cls"), vec![Value::Number(100.0)]);
assert_eq!(result, Value::Text(Arc::new("bronze".to_string())));
}
#[test]
fn interpret_match_stmt() {
let source = r#"f x:t>n;?x{"a":1;"b":2;_:0}"#;
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("a".to_string()))]
),
Value::Number(1.0)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("b".to_string()))]
),
Value::Number(2.0)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("z".to_string()))]
),
Value::Number(0.0)
);
}
#[test]
fn interpret_ok_err() {
let source = "f x:n>R n t;~x";
let result = run_str(source, Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Ok(Box::new(Value::Number(42.0))));
}
#[test]
fn interpret_err_constructor() {
let source = r#"f x:n>R n t;^"bad""#;
let result = run_str(source, Some("f"), vec![Value::Number(0.0)]);
assert_eq!(
result,
Value::Err(Box::new(Value::Text(Arc::new("bad".to_string()))))
);
}
#[test]
fn interpret_match_ok_err_patterns() {
let source = r#"f x:R n t>n;?x{^er:0;~v:v}"#;
let ok_result = run_str(
source,
Some("f"),
vec![Value::Ok(Box::new(Value::Number(42.0)))],
);
assert_eq!(ok_result, Value::Number(42.0));
let err_result = run_str(
source,
Some("f"),
vec![Value::Err(Box::new(Value::Text(Arc::new(
"oops".to_string(),
))))],
);
assert_eq!(err_result, Value::Number(0.0));
}
#[test]
fn interpret_negated_guard() {
let source = r#"f x:b>t;!x{"nope"}{"yes"}"#;
assert_eq!(
run_str(source, Some("f"), vec![Value::Bool(false)]),
Value::Text(Arc::new("nope".to_string()))
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Bool(true)]),
Value::Text(Arc::new("yes".to_string()))
);
}
#[test]
fn interpret_logical_not() {
let source = "f x:b>b;!x";
assert_eq!(
run_str(source, Some("f"), vec![Value::Bool(true)]),
Value::Bool(false)
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Bool(false)]),
Value::Bool(true)
);
}
#[test]
fn interpret_record_and_field() {
let source = "f x:n>n;r=point x:x y:10;r.y";
let result = run_str(source, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn interpret_with_expr() {
let source = "f>n;r=point x:1 y:2;r2=r with y:10;r2.y";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn interpret_string_concat() {
let source = r#"f a:t b:t>t;+a b"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new("hello ".to_string())),
Value::Text(Arc::new("world".to_string())),
],
);
assert_eq!(result, Value::Text(Arc::new("hello world".to_string())));
}
#[test]
fn interpret_string_comparison() {
let gt = r#"f a:t b:t>b;>a b"#;
assert_eq!(
run_str(
gt,
Some("f"),
vec![
Value::Text(Arc::new("banana".to_string())),
Value::Text(Arc::new("apple".to_string()))
]
),
Value::Bool(true)
);
assert_eq!(
run_str(
gt,
Some("f"),
vec![
Value::Text(Arc::new("apple".to_string())),
Value::Text(Arc::new("banana".to_string()))
]
),
Value::Bool(false)
);
let lt = r#"f a:t b:t>b;<a b"#;
assert_eq!(
run_str(
lt,
Some("f"),
vec![
Value::Text(Arc::new("apple".to_string())),
Value::Text(Arc::new("banana".to_string()))
]
),
Value::Bool(true)
);
let ge = r#"f a:t b:t>b;>=a b"#;
assert_eq!(
run_str(
ge,
Some("f"),
vec![
Value::Text(Arc::new("apple".to_string())),
Value::Text(Arc::new("apple".to_string()))
]
),
Value::Bool(true)
);
let le = r#"f a:t b:t>b;<=a b"#;
assert_eq!(
run_str(
le,
Some("f"),
vec![
Value::Text(Arc::new("zebra".to_string())),
Value::Text(Arc::new("banana".to_string()))
]
),
Value::Bool(false)
);
}
#[test]
fn interpret_match_expr_in_let() {
let source = r#"f x:t>n;y=?x{"a":1;"b":2;_:0};y"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("b".to_string()))],
);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn interpret_default_first_function() {
let source = "f>n;42";
let result = run_str(source, None, vec![]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn interpret_division_by_zero() {
let source = "f x:n>n;/x 0";
let prog = parse_program(source);
let result = run(&prog, Some("f"), vec![Value::Number(10.0)]);
assert!(result.is_err());
}
#[test]
fn interpret_sqrt_non_number_errors() {
let source = "f x:t>n;sqrt x";
let prog = parse_program(source);
let err = run(
&prog,
Some("f"),
vec![Value::Text(Arc::new("nope".to_string()))],
)
.unwrap_err();
assert!(
err.to_string().contains("sqrt") && err.to_string().contains("requires a number"),
"unexpected error: {err}"
);
}
#[test]
fn interpret_log_non_number_errors() {
let prog = parse_program("f x:t>n;log x");
let err = run(
&prog,
Some("f"),
vec![Value::Text(Arc::new("nope".to_string()))],
)
.unwrap_err();
assert!(err.to_string().contains("log"), "unexpected error: {err}");
}
#[test]
fn interpret_exp_non_number_errors() {
let prog = parse_program("f x:t>n;exp x");
let err = run(
&prog,
Some("f"),
vec![Value::Text(Arc::new("nope".to_string()))],
)
.unwrap_err();
assert!(err.to_string().contains("exp"), "unexpected error: {err}");
}
#[test]
fn interpret_sin_non_number_errors() {
let prog = parse_program("f x:t>n;sin x");
let err = run(
&prog,
Some("f"),
vec![Value::Text(Arc::new("nope".to_string()))],
)
.unwrap_err();
assert!(err.to_string().contains("sin"), "unexpected error: {err}");
}
#[test]
fn interpret_cos_non_number_errors() {
let prog = parse_program("f x:t>n;cos x");
let err = run(
&prog,
Some("f"),
vec![Value::Text(Arc::new("nope".to_string()))],
)
.unwrap_err();
assert!(err.to_string().contains("cos"), "unexpected error: {err}");
}
#[test]
fn interpret_pow_non_number_errors() {
let prog = parse_program("f x:t y:t>n;pow x y");
let err = run(
&prog,
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
],
)
.unwrap_err();
assert!(
err.to_string().contains("pow") && err.to_string().contains("two numbers"),
"unexpected error: {err}"
);
}
#[test]
fn interpret_logical_and() {
let source = "f a:b b:b>b;&a b";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Bool(true), Value::Bool(true)]
),
Value::Bool(true)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Bool(true), Value::Bool(false)]
),
Value::Bool(false)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Bool(false), Value::Bool(true)]
),
Value::Bool(false)
);
}
#[test]
fn interpret_logical_or() {
let source = "f a:b b:b>b;|a b";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Bool(false), Value::Bool(false)]
),
Value::Bool(false)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Bool(true), Value::Bool(false)]
),
Value::Bool(true)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Bool(false), Value::Bool(true)]
),
Value::Bool(true)
);
}
#[test]
fn interpret_len_string() {
let source = r#"f s:t>n;len s"#;
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("hello".to_string()))]
),
Value::Number(5.0)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("".to_string()))]
),
Value::Number(0.0)
);
}
#[test]
fn interpret_len_list() {
let source = "f>n;xs=[1, 2, 3];len xs";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_list_append() {
let source = "f>L n;xs=[1, 2];+=xs 3";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0)
]))
);
}
#[test]
fn interpret_list_append_empty() {
let source = "f>L n;xs=[];+=xs 42";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![Value::Number(42.0)]))
);
}
#[test]
fn interpret_list_concat() {
let source = "f>L n;a=[1, 2];b=[3, 4];+a b";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0),
Value::Number(4.0)
]))
);
}
#[test]
fn interpret_str_integer() {
let source = "f>t;str 42";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Text(Arc::new("42".to_string()))
);
}
#[test]
fn interpret_str_float() {
let source = "f>t;str 3.14";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Text(Arc::new("3.14".to_string()))
);
}
#[test]
fn interpret_num_ok() {
let source = "f>R n t;num \"42\"";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Ok(Box::new(Value::Number(42.0)))
);
}
#[test]
fn interpret_num_err() {
let source = "f>R n t;num \"abc\"";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Err(Box::new(Value::Text(Arc::new("abc".to_string()))))
);
}
#[test]
fn interpret_abs() {
let source = "f>n;abs -7";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn interpret_min() {
let source = "f>n;min 3 7";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_max() {
let source = "f>n;max 3 7";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn interpret_flr() {
let source = "f>n;flr 3.7";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_cel() {
let source = "f>n;cel 3.2";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(4.0));
}
#[test]
fn interpret_index_access() {
let source = "f>n;xs=[10, 20, 30];xs.1";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(20.0));
}
#[test]
fn interpret_index_access_string() {
let source = "f>t;xs=[\"hello\", \"world\"];xs.0";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Text(Arc::new("hello".to_string()))
);
}
#[test]
fn interpret_multi_function() {
let source = "double x:n>n;*x 2\nf x:n>n;double x";
let result = run_str(source, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn interpret_nested_multiply_add() {
let source = "f a:n b:n c:n>n;+*a b c";
let result = run_str(
source,
Some("f"),
vec![Value::Number(2.0), Value::Number(3.0), Value::Number(4.0)],
);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn interpret_nested_compare() {
let source = "f x:n y:n>b;>=+x y 100";
let result = run_str(
source,
Some("f"),
vec![Value::Number(60.0), Value::Number(50.0)],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpret_not_as_and_operand() {
let source = "f x:b y:b>b;&!x y";
let result = run_str(
source,
Some("f"),
vec![Value::Bool(false), Value::Bool(true)],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpret_negate_product() {
let source = "f a:n b:n>n;-*a b";
let result = run_str(
source,
Some("f"),
vec![Value::Number(3.0), Value::Number(4.0)],
);
assert_eq!(result, Value::Number(-12.0));
}
fn run_str_err(source: &str, func: Option<&str>, args: Vec<Value>) -> String {
let prog = parse_program(source);
run(&prog, func, args).unwrap_err().to_string()
}
#[test]
fn display_float() {
assert_eq!(format!("{}", Value::Number(3.14)), "3.14");
}
#[test]
fn display_integer_number() {
assert_eq!(format!("{}", Value::Number(42.0)), "42");
}
#[test]
fn display_text() {
assert_eq!(
format!("{}", Value::Text(Arc::new("hello".to_string()))),
"hello"
);
}
#[test]
fn display_bool() {
assert_eq!(format!("{}", Value::Bool(true)), "true");
assert_eq!(format!("{}", Value::Bool(false)), "false");
}
#[test]
fn display_nil() {
assert_eq!(format!("{}", Value::Nil), "nil");
}
#[test]
fn display_list() {
let list = Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0),
]));
assert_eq!(format!("{}", list), "[1, 2, 3]");
}
#[test]
fn display_list_empty() {
assert_eq!(format!("{}", Value::List(Arc::new(vec![]))), "[]");
}
#[test]
fn display_record() {
let mut fields = HashMap::new();
fields.insert("x".to_string(), Value::Number(1.0));
let rec = Value::Record {
type_name: "point".into(),
fields,
};
assert_eq!(format!("{}", rec), "point {x: 1}");
}
#[test]
fn display_record_multiple_fields() {
let mut fields = HashMap::new();
fields.insert("a".to_string(), Value::Number(1.0));
fields.insert("b".to_string(), Value::Number(2.0));
let rec = Value::Record {
type_name: "pair".into(),
fields,
};
let s = format!("{}", rec);
assert!(s.starts_with("pair {"));
assert!(s.contains("a: 1"));
assert!(s.contains("b: 2"));
assert!(s.ends_with("}"));
}
#[test]
fn display_ok() {
assert_eq!(
format!("{}", Value::Ok(Box::new(Value::Number(42.0)))),
"~42"
);
}
#[test]
fn display_err() {
assert_eq!(
format!(
"{}",
Value::Err(Box::new(Value::Text(Arc::new("bad".to_string()))))
),
"^bad"
);
}
#[test]
fn err_undefined_variable() {
let err = run_str_err("f>n;x", Some("f"), vec![]);
assert!(err.contains("undefined variable"));
}
#[test]
fn err_undefined_function() {
let err = run_str_err("f>n;nope 1", Some("f"), vec![]);
assert!(err.contains("undefined function"));
}
#[test]
fn err_wrong_arity() {
let err = run_str_err("f x:n>n;x", Some("f"), vec![]);
assert!(err.contains("expected 1 args, got 0"));
}
#[test]
fn err_len_wrong_arg_count() {
let err = run_str_err("f>n;len 1 2", Some("f"), vec![]);
assert!(err.contains("len: expected 1 arg"));
}
#[test]
fn err_len_wrong_type() {
let err = run_str_err("f x:n>n;len x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("len requires string, list, or map"));
}
#[test]
fn err_str_wrong_arg_count() {
let err = run_str_err("f>t;str 1 2", Some("f"), vec![]);
assert!(err.contains("str: expected 1 arg"));
}
#[test]
fn err_str_wrong_type() {
let err = run_str_err(r#"f x:_ >t;str x"#, Some("f"), vec![Value::Bool(true)]);
assert!(err.contains("str requires"));
}
#[test]
fn err_num_wrong_arg_count() {
let err = run_str_err(r#"f>R n t;num "1" "2""#, Some("f"), vec![]);
assert!(err.contains("num: expected 1 arg"));
}
#[test]
fn err_num_wrong_type() {
let err = run_str_err("f x:b>R n t;num x", Some("f"), vec![Value::Bool(true)]);
assert!(
err.contains("num requires text or number"),
"expected polymorphic num error, got: {err}"
);
}
#[test]
fn num_on_number_is_identity() {
let result = run_str("f x:n>R n t;num x", Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Ok(Box::new(Value::Number(42.0))));
}
#[test]
fn err_abs_wrong_arg_count() {
let err = run_str_err("f>n;abs 1 2", Some("f"), vec![]);
assert!(err.contains("abs: expected 1 arg"));
}
#[test]
fn num_trims_leading_whitespace() {
let result = run_str(r#"f>R n t;num " 77516""#, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Number(77516.0))));
}
#[test]
fn num_trims_trailing_whitespace() {
let result = run_str(r#"f>R n t;num "77516 ""#, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Number(77516.0))));
}
#[test]
fn num_trims_both_sides_signed_float() {
let result = run_str(r#"f>R n t;num " -3.14 ""#, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Number(-3.14))));
}
#[test]
fn num_trims_scientific_notation() {
let result = run_str(r#"f>R n t;num " 1e10 ""#, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Number(1e10))));
}
#[test]
fn num_internal_whitespace_still_errors() {
let result = run_str(r#"f>R n t;num "1 2""#, Some("f"), vec![]);
match result {
Value::Err(_) => {}
other => panic!("expected Err for internal whitespace, got {:?}", other),
}
}
#[test]
fn num_empty_string_errors() {
let result = run_str(r#"f>R n t;num """#, Some("f"), vec![]);
match result {
Value::Err(_) => {}
other => panic!("expected Err for empty string, got {:?}", other),
}
}
#[test]
fn num_whitespace_only_errors() {
let result = run_str(r#"f>R n t;num " ""#, Some("f"), vec![]);
match result {
Value::Err(_) => {}
other => panic!("expected Err for whitespace-only, got {:?}", other),
}
}
#[test]
fn err_abs_wrong_type() {
let err = run_str_err(
r#"f x:t>n;abs x"#,
Some("f"),
vec![Value::Text(Arc::new("hi".to_string()))],
);
assert!(err.contains("abs requires a number"));
}
#[test]
fn err_min_non_number() {
let err = run_str_err(
r#"f a:t b:t>n;min a b"#,
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
],
);
assert!(err.contains("min requires two numbers"));
}
#[test]
fn err_max_non_number() {
let err = run_str_err(
r#"f a:t b:t>n;max a b"#,
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
],
);
assert!(err.contains("max requires two numbers"));
}
#[test]
fn err_flr_non_number() {
let err = run_str_err(
r#"f x:t>n;flr x"#,
Some("f"),
vec![Value::Text(Arc::new("a".to_string()))],
);
assert!(err.contains("flr requires a number"));
}
#[test]
fn err_cel_non_number() {
let err = run_str_err(
r#"f x:t>n;cel x"#,
Some("f"),
vec![Value::Text(Arc::new("a".to_string()))],
);
assert!(err.contains("cel requires a number"));
}
#[test]
fn err_field_not_found_on_record() {
let err = run_str_err("f>n;r=point x:1 y:2;r.z", Some("f"), vec![]);
assert!(err.contains("no field 'z' on record"));
}
#[test]
fn err_field_access_on_non_record() {
let err = run_str_err("f x:n>n;x.y", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("cannot access field"));
}
#[test]
fn err_index_out_of_bounds() {
let err = run_str_err("f>n;xs=[1, 2];xs.5", Some("f"), vec![]);
assert!(err.contains("out of bounds"));
}
#[test]
fn err_index_on_non_list() {
let err = run_str_err("f x:n>n;x.0", Some("f"), vec![Value::Number(1.0)]);
assert!(
err.contains("index access on non-list") || err.contains("cannot access field"),
"got: {}",
err
);
}
#[test]
fn err_negate_non_number() {
let err = run_str_err(r#"f>n;-"hello""#, Some("f"), vec![]);
assert!(err.contains("cannot negate non-number"));
}
#[test]
fn err_with_on_non_record() {
let err = run_str_err("f x:n>n;x with y:1", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("'with' requires a record"));
}
#[test]
fn interpret_foreach() {
let source = "f>n;s=0;@x [1, 2, 3]{+s x}";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn interpret_subtract() {
let source = "f a:n b:n>n;-a b";
let result = run_str(
source,
Some("f"),
vec![Value::Number(10.0), Value::Number(3.0)],
);
assert_eq!(result, Value::Number(7.0));
}
#[test]
fn interpret_divide() {
let source = "f a:n b:n>n;/a b";
let result = run_str(
source,
Some("f"),
vec![Value::Number(10.0), Value::Number(4.0)],
);
assert_eq!(result, Value::Number(2.5));
}
#[test]
fn interpret_equals() {
let source = "f a:n b:n>b;=a b";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Number(1.0), Value::Number(1.0)]
),
Value::Bool(true)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Number(1.0), Value::Number(2.0)]
),
Value::Bool(false)
);
}
#[test]
fn interpret_not_equals() {
let source = "f a:n b:n>b;!=a b";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Number(1.0), Value::Number(2.0)]
),
Value::Bool(true)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Number(1.0), Value::Number(1.0)]
),
Value::Bool(false)
);
}
#[test]
fn values_equal_numbers() {
assert!(values_equal(&Value::Number(1.0), &Value::Number(1.0)));
assert!(!values_equal(&Value::Number(1.0), &Value::Number(2.0)));
}
#[test]
fn values_equal_bools() {
assert!(values_equal(&Value::Bool(true), &Value::Bool(true)));
assert!(!values_equal(&Value::Bool(true), &Value::Bool(false)));
}
#[test]
fn values_equal_nil() {
assert!(values_equal(&Value::Nil, &Value::Nil));
}
#[test]
fn values_equal_mismatched() {
assert!(!values_equal(
&Value::Number(1.0),
&Value::Text(Arc::new("1".to_string()))
));
assert!(!values_equal(&Value::Nil, &Value::Bool(false)));
}
#[test]
fn is_truthy_nil() {
assert!(!is_truthy(&Value::Nil));
}
#[test]
fn is_truthy_number_zero() {
assert!(!is_truthy(&Value::Number(0.0)));
}
#[test]
fn is_truthy_number_nonzero() {
assert!(is_truthy(&Value::Number(1.0)));
assert!(is_truthy(&Value::Number(-5.0)));
}
#[test]
fn is_truthy_text() {
assert!(!is_truthy(&Value::Text(Arc::new("".to_string()))));
assert!(is_truthy(&Value::Text(Arc::new("hello".to_string()))));
}
#[test]
fn is_truthy_list() {
assert!(!is_truthy(&Value::List(Arc::new(vec![]))));
assert!(is_truthy(&Value::List(Arc::new(vec![Value::Number(1.0)]))));
}
#[test]
fn is_truthy_other() {
assert!(is_truthy(&Value::Ok(Box::new(Value::Nil))));
assert!(is_truthy(&Value::Err(Box::new(Value::Nil))));
}
#[test]
fn interpret_literal_bool() {
let source = "f>b;true";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Bool(true));
let source2 = "f>b;false";
assert_eq!(run_str(source2, Some("f"), vec![]), Value::Bool(false));
}
#[test]
fn interpret_match_no_subject() {
let source = r#"f>n;?{_:42}"#;
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn interpret_match_expr_with_bindings() {
let source = "f x:R n t>n;y=?x{~v:v;_:0};y";
let result = run_str(
source,
Some("f"),
vec![Value::Ok(Box::new(Value::Number(99.0)))],
);
assert_eq!(result, Value::Number(99.0));
}
#[test]
fn interpret_match_expr_no_arm_matches() {
let source = r#"f>n;y=?1{2:99};y"#;
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
fn interpret_typedef_in_declarations() {
let source = "type point{x:n;y:n}\nf>n;42";
let result = run_str(source, None, vec![]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn interpret_pattern_literal_no_match() {
let source = r#"f x:n>n;?x{1:10;2:20;_:0}"#;
let result = run_str(source, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(0.0));
}
#[test]
fn interpret_foreach_on_non_list() {
let err = run_str_err("f x:n>n;@i x{i}", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("foreach requires a list"));
}
#[test]
fn interpret_tool_call() {
let source =
"tool fetch\"HTTP GET\" url:t>R _ t timeout:30\nf>R _ t;fetch \"http://example.com\"";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Nil)));
}
#[test]
fn interpret_typedef_not_callable() {
let source = "type point{x:n;y:n}\nf>n;point 1 2";
let err = run_str_err(source, Some("f"), vec![]);
assert!(
err.contains("undefined function")
|| err.contains("type")
|| err.contains("not callable"),
"unexpected error: {}",
err
);
}
#[test]
fn interpret_greater_than() {
let source = "f a:n b:n>b;>a b";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Number(5.0), Value::Number(3.0)]
),
Value::Bool(true)
);
}
#[test]
fn interpret_less_than() {
let source = "f a:n b:n>b;<a b";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Number(3.0), Value::Number(5.0)]
),
Value::Bool(true)
);
}
#[test]
fn interpret_less_or_equal() {
let source = "f a:n b:n>b;<=a b";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Number(3.0), Value::Number(3.0)]
),
Value::Bool(true)
);
}
#[test]
fn interpret_unsupported_binop() {
let source = "f a:b b:b>b;-a b";
let err = run_str_err(
source,
Some("f"),
vec![Value::Bool(true), Value::Bool(false)],
);
assert!(
err.contains("unsupported operation"),
"unexpected error: {}",
err
);
}
#[test]
fn interpret_foreach_early_return() {
let source = "f xs:L n>n;@x xs{>=x 3{ret x}};0";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(5.0),
Value::Number(2.0),
]))],
);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn interpret_match_not_last_stmt() {
let source = "f x:n>n;?x{0:x;_:x};+x 1";
let result = run_str(source, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(6.0));
}
#[test]
fn interpret_match_expr_no_subject() {
let source = r#"f>t;x=?{_:"always"};x"#;
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("always".to_string())));
}
#[test]
fn interpret_pattern_ok_no_match() {
let source = r#"f>t;x=^"err";?x{~v:v;_:"default"}"#;
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("default".to_string())));
}
#[test]
fn interpret_match_stmt_no_arm_matches() {
let source = "f x:n>n;?x{1:99};0";
let result = run_str(source, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(0.0));
}
#[test]
fn interpret_match_arm_body_with_guard_return() {
let source = "f x:n>n;y=0;?x{1:>=x 0{42};_:0}";
let result = run_str(source, Some("f"), vec![Value::Number(1.0)]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn call_typedef_as_function() {
let mut env = Env::new();
env.functions.insert(
"point".to_string(),
Decl::TypeDef {
name: "point".to_string(),
fields: vec![],
span: Span::UNKNOWN,
},
);
let result = call_function(&mut env, "point", vec![]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("is a type, not callable"),
"got: {}",
err
);
}
#[test]
fn call_error_decl_as_function() {
let mut env = Env::new();
env.functions.insert(
"broken".to_string(),
Decl::Error {
span: Span::UNKNOWN,
},
);
let result = call_function(&mut env, "broken", vec![]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("failed to parse"), "got: {}", err);
}
fn make_result_program(inner_body: Vec<Spanned<Stmt>>) -> Program {
Program {
declarations: vec![
Decl::Function {
type_params: vec![],
name: "inner".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: Type::Number,
}],
return_type: Type::Result(Box::new(Type::Number), Box::new(Type::Text)),
effect_set: None,
body: inner_body,
span: Span::UNKNOWN,
},
Decl::Function {
type_params: vec![],
name: "outer".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: Type::Number,
}],
return_type: Type::Result(Box::new(Type::Number), Box::new(Type::Text)),
effect_set: None,
body: vec![
Spanned::unknown(Stmt::Let {
name: "d".to_string(),
value: Expr::Call {
function: "inner".to_string(),
args: vec![Expr::Ref("x".to_string())],
unwrap: UnwrapMode::Propagate,
},
}),
Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(Expr::Ref(
"d".to_string(),
))))),
],
span: Span::UNKNOWN,
},
],
source: None,
parse_failed_fns: Default::default(),
glued_eq_binding_sites: Default::default(),
h_keyword_simple_ref_sites: Vec::new(),
}
}
#[test]
fn unwrap_ok_path() {
let prog = make_result_program(vec![Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(
Expr::Ref("x".to_string()),
))))]);
let result = run(&prog, Some("outer"), vec![Value::Number(42.0)]).unwrap();
assert_eq!(result, Value::Ok(Box::new(Value::Number(42.0))));
}
#[test]
fn unwrap_err_path() {
let prog = make_result_program(vec![Spanned::unknown(Stmt::Expr(Expr::Err(Box::new(
Expr::Literal(Literal::Text("fail".to_string())),
))))]);
let result = run(&prog, Some("outer"), vec![Value::Number(42.0)]).unwrap();
assert_eq!(
result,
Value::Err(Box::new(Value::Text(Arc::new("fail".to_string()))))
);
}
#[test]
fn unwrap_nested_propagation() {
let unwrap_body = |callee: &str| {
vec![
Spanned::unknown(Stmt::Let {
name: "d".to_string(),
value: Expr::Call {
function: callee.to_string(),
args: vec![Expr::Ref("x".to_string())],
unwrap: UnwrapMode::Propagate,
},
}),
Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(Expr::Ref("d".to_string()))))),
]
};
let rnt = Type::Result(Box::new(Type::Number), Box::new(Type::Text));
let prog = Program {
declarations: vec![
Decl::Function {
type_params: vec![],
name: "c".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: Type::Number,
}],
return_type: rnt.clone(),
effect_set: None,
body: vec![Spanned::unknown(Stmt::Expr(Expr::Err(Box::new(
Expr::Literal(Literal::Text("deep".to_string())),
))))],
span: Span::UNKNOWN,
},
Decl::Function {
type_params: vec![],
name: "b".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: Type::Number,
}],
return_type: rnt.clone(),
effect_set: None,
body: unwrap_body("c"),
span: Span::UNKNOWN,
},
Decl::Function {
type_params: vec![],
name: "a".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: Type::Number,
}],
return_type: rnt,
effect_set: None,
body: unwrap_body("b"),
span: Span::UNKNOWN,
},
],
source: None,
parse_failed_fns: Default::default(),
glued_eq_binding_sites: Default::default(),
h_keyword_simple_ref_sites: Vec::new(),
};
let result = run(&prog, Some("a"), vec![Value::Number(1.0)]).unwrap();
assert_eq!(
result,
Value::Err(Box::new(Value::Text(Arc::new("deep".to_string()))))
);
}
#[test]
fn interpret_braceless_guard() {
let source = r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#;
assert_eq!(
run_str(source, Some("cls"), vec![Value::Number(1500.0)]),
Value::Text(Arc::new("gold".to_string()))
);
assert_eq!(
run_str(source, Some("cls"), vec![Value::Number(750.0)]),
Value::Text(Arc::new("silver".to_string()))
);
assert_eq!(
run_str(source, Some("cls"), vec![Value::Number(100.0)]),
Value::Text(Arc::new("bronze".to_string()))
);
}
#[test]
fn interpret_braceless_guard_factorial() {
let source = "fac n:n>n;<=n 1 1;r=fac -n 1;*n r";
assert_eq!(
run_str(source, Some("fac"), vec![Value::Number(5.0)]),
Value::Number(120.0)
);
}
#[test]
fn interpret_braceless_guard_fibonacci() {
std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let source = "fib n:n>n;<=n 1 n;a=fib -n 1;b=fib -n 2;+a b";
assert_eq!(
run_str(source, Some("fib"), vec![Value::Number(10.0)]),
Value::Number(55.0)
);
})
.expect("spawn test thread")
.join()
.expect("thread panicked");
}
#[test]
fn interpret_spl_basic() {
let source = r#"f>L t;spl "a,b,c" ",""#;
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
Value::Text(Arc::new("c".to_string())),
]))
);
}
#[test]
fn interpret_spl_empty() {
let source = r#"f>L t;spl "" ",""#;
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![Value::Text(Arc::new("".to_string()))]))
);
}
#[test]
fn interpret_cat_basic() {
let source = "f items:L t>t;cat items \",\"";
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
Value::Text(Arc::new("c".to_string())),
]))]
),
Value::Text(Arc::new("a,b,c".to_string()))
);
}
#[test]
fn interpret_cat_empty_list() {
let source = "f items:L t>t;cat items \"-\"";
assert_eq!(
run_str(source, Some("f"), vec![Value::List(Arc::new(vec![]))]),
Value::Text(Arc::new("".to_string()))
);
}
#[test]
fn interpret_has_list() {
let source = "f xs:L n x:n>b;has xs x";
assert_eq!(
run_str(
source,
Some("f"),
vec![
Value::List(Arc::new(vec![Value::Number(1.0), Value::Number(2.0)])),
Value::Number(2.0)
]
),
Value::Bool(true)
);
assert_eq!(
run_str(
source,
Some("f"),
vec![
Value::List(Arc::new(vec![Value::Number(1.0)])),
Value::Number(5.0)
]
),
Value::Bool(false)
);
}
#[test]
fn interpret_has_text() {
let source = r#"f s:t needle:t>b;has s needle"#;
assert_eq!(
run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new("hello world".to_string())),
Value::Text(Arc::new("world".to_string()))
]
),
Value::Bool(true)
);
}
#[test]
fn interpret_hd_list() {
let source = "f>n;xs=[10, 20, 30];hd xs";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(10.0));
}
#[test]
fn interpret_tl_list() {
let source = "f>L n;xs=[10, 20, 30];tl xs";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![Value::Number(20.0), Value::Number(30.0)]))
);
}
#[test]
fn interpret_hd_text() {
let source = r#"f s:t>t;hd s"#;
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("hello".to_string()))]
),
Value::Text(Arc::new("h".to_string()))
);
}
#[test]
fn interpret_tl_text() {
let source = r#"f s:t>t;tl s"#;
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("hello".to_string()))]
),
Value::Text(Arc::new("ello".to_string()))
);
}
#[test]
fn interpret_rev_list() {
let source = "f>L n;rev [1, 2, 3]";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![
Value::Number(3.0),
Value::Number(2.0),
Value::Number(1.0)
]))
);
}
#[test]
fn interpret_rev_text() {
let source = r#"f>t;rev "abc""#;
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Text(Arc::new("cba".to_string()))
);
}
#[test]
fn interpret_srt_numbers() {
let source = "f>L n;srt [3, 1, 2]";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0)
]))
);
}
#[test]
fn interpret_srt_text_list() {
let source = r#"f>L t;srt ["c", "a", "b"]"#;
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
Value::Text(Arc::new("c".to_string()))
]))
);
}
#[test]
fn interpret_srt_text_string() {
let source = r#"f>t;srt "cab""#;
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Text(Arc::new("abc".to_string()))
);
}
#[test]
fn interpret_slc_list() {
let source = "f>L n;slc [1, 2, 3, 4, 5] 1 3";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![Value::Number(2.0), Value::Number(3.0)]))
);
}
#[test]
fn interpret_slc_text() {
let source = r#"f>t;slc "hello" 1 4"#;
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Text(Arc::new("ell".to_string()))
);
}
#[test]
fn interpret_slc_clamped() {
let source = "f>L n;slc [1, 2, 3] 1 100";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![Value::Number(2.0), Value::Number(3.0)]))
);
}
#[test]
fn interpret_ternary_true() {
let source = r#"f x:n>t;=x 1{"yes"}{"no"}"#;
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(1.0)]),
Value::Text(Arc::new("yes".to_string()))
);
}
#[test]
fn interpret_ternary_false() {
let source = r#"f x:n>t;=x 1{"yes"}{"no"}"#;
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(2.0)]),
Value::Text(Arc::new("no".to_string()))
);
}
#[test]
fn interpret_ternary_no_early_return() {
let source = r#"f x:n>n;=x 0{10}{20};+x 1"#;
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(0.0)]),
Value::Number(1.0)
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(6.0)
);
}
#[test]
fn interpret_braced_guard_no_early_return() {
let source = "f x:n>n;=x 0{99};+x 1";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(0.0)]),
Value::Number(1.0)
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(6.0)
);
}
#[test]
fn interpret_braceless_guard_still_returns_early() {
let source = "f x:n>n;=x 0 99;+x 1";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(0.0)]),
Value::Number(99.0)
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(6.0)
);
}
#[test]
fn interpret_braced_guard_in_loop_no_early_return() {
let source = "mx xs:L n>n;m=xs.0;@x xs{>x m{m=x}};+m 0";
let result = run_str(
source,
Some("mx"),
vec![Value::List(Arc::new(vec![
Value::Number(3.0),
Value::Number(1.0),
Value::Number(5.0),
]))],
);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn interpret_braceless_guard_early_return_factorial() {
let source = "f x:n>n;<=x 1 1;r=f -x 1;*x r";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(120.0)
);
}
#[test]
fn interpret_ternary_let_binding() {
let source = "f x:n>n;v=<x 0{- 0 x}{x};v";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(-3.0)]),
Value::Number(3.0)
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(7.0)]),
Value::Number(7.0)
);
}
#[test]
fn interpret_ternary_negated() {
let source = r#"f x:n>t;!=x 1{"not one"}{"one"}"#;
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(1.0)]),
Value::Text(Arc::new("one".to_string()))
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(2.0)]),
Value::Text(Arc::new("not one".to_string()))
);
}
#[test]
fn interpret_ret_early_return() {
let source = r#"f x:n>n;>x 0{ret x};0"#;
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(5.0)
);
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(-1.0)]),
Value::Number(0.0)
);
}
#[test]
fn interpret_pipe_simple() {
let source = "f x:n>n;str x>>len";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(42.0)]),
Value::Number(2.0)
);
}
#[test]
fn interpret_pipe_chain() {
let source = "dbl x:n>n;*x 2\nadd1 x:n>n;+x 1\nf x:n>n;dbl x>>add1";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(11.0)
);
}
#[test]
fn interpret_pipe_with_extra_args() {
let source = "add a:n b:n>n;+a b\nf x:n>n;add x 1>>add 2";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(8.0)
);
}
#[test]
fn interpret_ret_in_foreach() {
let source = "f xs:L n>n;@x xs{>=x 10{ret x}};0";
let list = Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(15.0),
Value::Number(3.0),
]));
assert_eq!(run_str(source, Some("f"), vec![list]), Value::Number(15.0));
}
#[test]
fn interpret_while_basic() {
let source = "f>n;i=0;s=0;wh <i 5{i=+i 1;s=+s i};s";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(15.0));
}
#[test]
fn interpret_while_zero_iterations() {
let source = "f>n;wh false{42};0";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(0.0));
}
#[test]
fn interpret_nil_coalesce_nil() {
let source = "mk x:n>n;>=x 1{x}\nf>n;x=mk 0;x??42";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(42.0));
}
#[test]
fn interpret_nil_coalesce_non_nil() {
let source = "f>n;x=10;x??42";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(10.0));
}
#[test]
fn interpret_nil_coalesce_chain() {
let source = "mk x:n>n;>=x 1{x}\nf>n;a=mk 0;b=mk 0;a??b??99";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn interpret_safe_field_on_nil() {
let source = "mk x:n>n;>=x 1{x}\nf>n;v=mk 0;v.?name??99";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn interpret_safe_field_on_value() {
let source = "f>n;p=pt x:5;p.?x\ntype pt{x:n}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn interpret_safe_field_chained() {
let source = "mk x:n>n;>=x 1{x}\nf>n;v=mk 0;v.?a.?b??77";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(77.0));
}
#[test]
fn interpret_while_with_ret() {
let source = "f>n;i=0;wh true{i=+i 1;>=i 3{ret i}};0";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_while_brk() {
let source = "f>n;i=0;wh true{i=+i 1;>=i 3{brk}};i";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_while_brk_value() {
let source = "f>n;i=0;wh true{i=+i 1;>=i 3{brk 99}};i";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_while_cnt() {
let source = "f>n;i=0;s=0;wh <i 5{i=+i 1;>=i 3{cnt};s=+s i};s";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_foreach_brk() {
let source = "f>n;@x [1,2,3,4,5]{>=x 3{brk x};x}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_foreach_cnt() {
let source = "f>n;@x [1,2,3,4,5]{>=x 3{cnt};*x 2}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(4.0));
}
#[test]
fn interpret_rnd_no_args() {
let source = "f>n;rnd";
let result = run_str(source, Some("f"), vec![]);
let Value::Number(n) = result else {
panic!("expected Number")
};
assert!((0.0..1.0).contains(&n), "rnd should be in [0,1), got {n}");
}
#[test]
fn interpret_rnd_two_args() {
let source = "f>n;rnd 1 10";
let result = run_str(source, Some("f"), vec![]);
let Value::Number(n) = result else {
panic!("expected Number")
};
assert!(
(1.0..=10.0).contains(&n),
"rnd 1 10 should be in [1,10], got {n}"
);
assert_eq!(n, n.floor(), "rnd with two args should return integer");
}
#[test]
fn interpret_rnd_same_bounds() {
let source = "f>n;rnd 5 5";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn interpret_now() {
let source = "f>n;now";
let result = run_str(source, Some("f"), vec![]);
let Value::Number(n) = result else {
panic!("expected Number")
};
assert!(
n > 1_000_000_000.0,
"now should be a reasonable unix timestamp, got {n}"
);
}
#[test]
fn interpret_env_existing_var() {
let _guard = ENV_TEST_MUTEX.lock().unwrap();
unsafe {
std::env::set_var("ILO_TEST_VAR", "hello");
}
let source = r#"f k:t>R t t;env k"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("ILO_TEST_VAR".to_string()))],
);
assert_eq!(
result,
Value::Ok(Box::new(Value::Text(Arc::new("hello".to_string()))))
);
unsafe {
std::env::remove_var("ILO_TEST_VAR");
}
}
#[test]
fn interpret_env_missing_var() {
let _guard = ENV_TEST_MUTEX.lock().unwrap();
let source = r#"f k:t>R t t;env k"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("ILO_NONEXISTENT_12345".to_string()))],
);
let Value::Err(inner) = result else {
panic!("expected Err")
};
let Value::Text(s) = *inner else {
panic!("expected Text")
};
assert!(s.contains("not set"), "got: {s}");
}
#[test]
fn interpret_env_unwrap() {
let _guard = ENV_TEST_MUTEX.lock().unwrap();
unsafe {
std::env::set_var("ILO_TEST_UNWRAP", "world");
}
let source = r#"f k:t>R t t;~(env! k)"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("ILO_TEST_UNWRAP".to_string()))],
);
assert_eq!(
result,
Value::Ok(Box::new(Value::Text(Arc::new("world".to_string()))))
);
unsafe {
std::env::remove_var("ILO_TEST_UNWRAP");
}
}
#[test]
fn interpret_range_basic() {
let source = "f>n;@i 0..3{i}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(2.0));
}
#[test]
fn interpret_range_accumulate() {
let source = "f>n;@i 0..3{+i 1}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_range_empty() {
let source = "f>n;@i 5..3{99}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Nil);
}
#[test]
fn interpret_range_dynamic_end() {
let source = "f n:n>n;@i 0..n{i}";
assert_eq!(
run_str(source, Some("f"), vec![Value::Number(4.0)]),
Value::Number(3.0)
);
}
#[test]
fn interpret_range_brk() {
let source = "f>n;@i 0..10{>=i 3{brk i};i}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn interpret_range_cnt() {
let source = "f>n;@i 0..5{=i 2{cnt};*i 10}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(40.0));
}
#[test]
fn interpret_range_as_index() {
let source = "f>n;@i 0..3{*i i}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(4.0));
}
#[test]
fn err_spl_non_text_first() {
let err = run_str_err(
"f x:n y:t>L t;spl x y",
Some("f"),
vec![Value::Number(1.0), Value::Text(Arc::new("a".to_string()))],
);
assert!(err.contains("spl requires two text args"), "got: {err}");
}
#[test]
fn err_spl_non_text_second() {
let err = run_str_err(
"f x:t y:n>L t;spl x y",
Some("f"),
vec![Value::Text(Arc::new("a-b".to_string())), Value::Number(1.0)],
);
assert!(err.contains("spl requires two text args"), "got: {err}");
}
#[test]
fn err_cat_non_text_items() {
let err = run_str_err("f>t;cat [1,2,3] \",\"", Some("f"), vec![]);
assert!(err.contains("cat: list items must be text"), "got: {err}");
}
#[test]
fn err_cat_wrong_arg_types() {
let err = run_str_err(
"f x:n y:n>t;cat x y",
Some("f"),
vec![Value::Number(1.0), Value::Number(2.0)],
);
assert!(
err.contains("cat requires a list and text separator"),
"got: {err}"
);
}
#[test]
fn err_has_text_non_text_needle() {
let err = run_str_err(
"f x:t y:n>b;has x y",
Some("f"),
vec![
Value::Text(Arc::new("hello".to_string())),
Value::Number(1.0),
],
);
assert!(
err.contains("text search requires text needle"),
"got: {err}"
);
}
#[test]
fn err_has_wrong_first_arg() {
let err = run_str_err(
"f x:n y:n>b;has x y",
Some("f"),
vec![Value::Number(1.0), Value::Number(2.0)],
);
assert!(err.contains("has requires a list or text"), "got: {err}");
}
#[test]
fn err_hd_empty_list() {
let err = run_str_err("f>n;hd []", Some("f"), vec![]);
assert!(err.contains("hd: empty list"), "got: {err}");
}
#[test]
fn err_hd_empty_text() {
let err = run_str_err("f>t;hd \"\"", Some("f"), vec![]);
assert!(err.contains("hd: empty text"), "got: {err}");
}
#[test]
fn err_hd_wrong_type() {
let err = run_str_err("f x:n>n;hd x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("hd requires a list or text"), "got: {err}");
}
#[test]
fn err_tl_empty_list() {
let err = run_str_err("f>L n;tl []", Some("f"), vec![]);
assert!(err.contains("tl: empty list"), "got: {err}");
}
#[test]
fn err_tl_empty_text() {
let err = run_str_err("f>t;tl \"\"", Some("f"), vec![]);
assert!(err.contains("tl: empty text"), "got: {err}");
}
#[test]
fn err_tl_wrong_type() {
let err = run_str_err("f x:n>n;tl x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("tl requires a list or text"), "got: {err}");
}
#[test]
fn err_rev_wrong_type() {
let err = run_str_err("f x:n>n;rev x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("rev requires a list or text"), "got: {err}");
}
#[test]
fn err_srt_mixed_types() {
let err = run_str_err("f>L n;srt [1,\"a\"]", Some("f"), vec![]);
assert!(
err.contains("srt: list must contain all numbers or all text"),
"got: {err}"
);
}
#[test]
fn err_srt_wrong_type() {
let err = run_str_err("f x:n>n;srt x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("srt requires a list or text"), "got: {err}");
}
#[test]
fn err_slc_wrong_first_arg() {
let err = run_str_err("f x:n>n;slc x 0 1", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("slc requires a list or text"), "got: {err}");
}
#[test]
fn err_slc_non_number_start() {
let err = run_str_err(
"f x:t y:t>t;slc x y 1",
Some("f"),
vec![
Value::Text(Arc::new("hi".to_string())),
Value::Text(Arc::new("a".to_string())),
],
);
assert!(
err.contains("slc: start index must be a number"),
"got: {err}"
);
}
#[test]
fn err_slc_non_number_end() {
let err = run_str_err(
"f x:t y:t>t;slc x 0 y",
Some("f"),
vec![
Value::Text(Arc::new("hi".to_string())),
Value::Text(Arc::new("a".to_string())),
],
);
assert!(
err.contains("slc: end index must be a number"),
"got: {err}"
);
}
#[test]
fn err_rnd_lower_gt_upper() {
let err = run_str_err("f>n;rnd 10 1", Some("f"), vec![]);
assert!(err.contains("rnd: lower bound"), "got: {err}");
assert!(err.contains("upper bound"), "got: {err}");
}
#[test]
fn err_rnd_wrong_arg_types() {
let err = run_str_err(
"f x:t y:t>n;rnd x y",
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
],
);
assert!(err.contains("rnd requires two numbers"), "got: {err}");
}
#[test]
fn err_get_non_text_arg() {
let err = run_str_err("f x:n>R t t;get x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("get requires text"), "got: {err}");
}
#[test]
fn ok_srt_empty_list() {
let source = "f>L n;srt []";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::List(Arc::new(vec![]))
);
}
#[test]
fn destructure_basic() {
let source = "type pt{x:n;y:n} f>n;p=pt x:3 y:4;{x;y}=p;+x y";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn destructure_single_field() {
let source = "type pt{x:n;y:n} f>n;p=pt x:10 y:20;{x}=p;x";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(10.0));
}
#[test]
fn destructure_with_text_fields() {
let source =
"type usr{name:t;email:t} f>t;u=usr name:\"alice\" email:\"a@b\";{name;email}=u;name";
assert_eq!(
run_str(source, Some("f"), vec![]),
Value::Text(Arc::new("alice".to_string()))
);
}
#[test]
fn destructure_in_loop() {
let source = "type pt{x:n;y:n} f>n;ps=[pt x:1 y:2,pt x:3 y:4];@p ps{{x;y}=p;+x y}";
assert_eq!(run_str(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn destructure_non_record_error() {
let err = run_str_err("f x:n>n;{a}=x;a", Some("f"), vec![Value::Number(5.0)]);
assert!(
err.contains("destructure requires a record"),
"got: {}",
err
);
}
#[test]
fn destructure_missing_field_error() {
let source = "type pt{x:n;y:n} f>n;p=pt x:3 y:4;{x;z}=p;x";
let err = run_str_err(source, Some("f"), vec![]);
assert!(err.contains("no field 'z'"), "got: {}", err);
}
#[test]
fn interp_jp_object() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new(r#"{"name":"alice"}"#.to_string())),
Value::Text(Arc::new("name".to_string())),
],
);
assert_eq!(
result,
Value::Ok(Box::new(Value::Text(Arc::new("alice".to_string()))))
);
}
#[test]
fn interp_jp_nested() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new(r#"{"user":{"name":"bob"}}"#.to_string())),
Value::Text(Arc::new("user.name".to_string())),
],
);
assert_eq!(
result,
Value::Ok(Box::new(Value::Text(Arc::new("bob".to_string()))))
);
}
#[test]
fn interp_jp_array_index() {
let source = r#"f j:t p:t>R _ t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new(r#"{"items":[10,20,30]}"#.to_string())),
Value::Text(Arc::new("items.1".to_string())),
],
);
assert_eq!(result, Value::Ok(Box::new(Value::Number(20.0))));
}
#[test]
fn interp_jp_missing_key() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new(r#"{"a":1}"#.to_string())),
Value::Text(Arc::new("b".to_string())),
],
);
let Value::Err(e) = result else {
panic!("expected Err")
};
assert!(e.to_string().contains("key not found"), "got: {}", e);
}
#[test]
fn interp_jp_invalid_json() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new("not json".to_string())),
Value::Text(Arc::new("x".to_string())),
],
);
assert!(matches!(result, Value::Err(_)));
}
#[test]
fn interp_jp_unwrap() {
let source = r#"f j:t p:t>t;jpth! j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new(r#"{"x":"hello"}"#.to_string())),
Value::Text(Arc::new("x".to_string())),
],
);
assert_eq!(result, Value::Text(Arc::new("hello".to_string())));
}
#[test]
fn interp_jd_number() {
let source = "f x:n>t;jdmp x";
let result = run_str(source, Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Text(Arc::new("42".to_string())));
}
#[test]
fn interp_jd_text() {
let source = r#"f x:t>t;jdmp x"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("hello".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new(r#""hello""#.to_string())));
}
#[test]
fn interp_jd_list() {
let source = "f>t;xs=[1, 2, 3];jdmp xs";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("[1,2,3]".to_string())));
}
#[test]
fn interp_jd_record() {
let source = "type pt{x:n;y:n} f>t;p=pt x:1 y:2;jdmp p";
let result = run_str(source, Some("f"), vec![]);
let Value::Text(ref s) = result else {
panic!("expected text")
};
let text = s.clone();
let parsed: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(parsed["x"], 1);
assert_eq!(parsed["y"], 2);
}
#[test]
fn interp_jparse_object() {
let source = r#"f j:t>R t t;jpar j"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new(r#"{"a":1,"b":"two"}"#.to_string()))],
);
let Value::Ok(inner) = result else {
panic!("expected Ok")
};
let Value::Record { type_name, fields } = *inner else {
panic!("expected record")
};
assert_eq!(type_name, "json");
assert_eq!(fields.get("a"), Some(&Value::Number(1.0)));
assert_eq!(
fields.get("b"),
Some(&Value::Text(Arc::new("two".to_string())))
);
}
#[test]
fn interp_jparse_array() {
let source = r#"f j:t>R t t;jpar j"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("[1,2,3]".to_string()))],
);
let Value::Ok(inner) = result else {
panic!("expected Ok")
};
assert_eq!(
*inner,
Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0)
]))
);
}
#[test]
fn interp_jparse_scalar() {
let source = r#"f j:t>R t t;jpar j"#;
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("42".to_string()))]
),
Value::Ok(Box::new(Value::Number(42.0)))
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("true".to_string()))]
),
Value::Ok(Box::new(Value::Bool(true)))
);
assert_eq!(
run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("null".to_string()))]
),
Value::Ok(Box::new(Value::Nil))
);
}
#[test]
fn interp_jparse_invalid() {
let source = r#"f j:t>R t t;jpar j"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("not json".to_string()))],
);
assert!(matches!(result, Value::Err(_)));
}
#[test]
fn interp_jparse_unwrap() {
let source = r#"f j:t>t;jpar! j"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new(r#"{"x":1}"#.to_string()))],
);
let Value::Record { type_name, fields } = result else {
panic!("expected record")
};
assert_eq!(type_name, "json");
assert_eq!(fields.get("x"), Some(&Value::Number(1.0)));
}
#[test]
fn interp_jparse_then_field_access() {
let source = r#"f j:t>n;r=jpar! j;r.x"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new(r#"{"x":42}"#.to_string()))],
);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn interp_map_squares() {
let source = "sq x:n>n;*x x main xs:L n>L n;map sq xs";
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(
vec![1.0, 2.0, 3.0, 4.0, 5.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
assert_eq!(
result,
Value::List(Arc::new(
vec![1.0, 4.0, 9.0, 16.0, 25.0]
.into_iter()
.map(Value::Number)
.collect()
))
);
}
#[test]
fn interp_flt_positive() {
let source = "pos x:n>b;>x 0 main xs:L n>L n;flt pos xs";
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(
vec![-3.0, -1.0, 0.0, 2.0, 4.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
assert_eq!(
result,
Value::List(Arc::new(
vec![2.0, 4.0].into_iter().map(Value::Number).collect()
))
);
}
#[test]
fn interp_fld_sum() {
let source = "add a:n b:n>n;+a b main xs:L n>n;fld add xs 0";
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(
vec![1.0, 2.0, 3.0, 4.0, 5.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
assert_eq!(result, Value::Number(15.0));
}
#[test]
fn interp_grp_by_string_key() {
let source = r#"cl x:n>t;>x 5{"big"}{"small"} main xs:L n>M t L n;grp cl xs"#;
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(
vec![1.0, 8.0, 3.0, 9.0, 2.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
let Value::Map(m) = result else {
panic!("expected Map")
};
assert_eq!(
m.get(&MapKey::Text("small".to_string())).unwrap(),
&Value::List(Arc::new(
vec![1.0, 3.0, 2.0].into_iter().map(Value::Number).collect()
))
);
assert_eq!(
m.get(&MapKey::Text("big".to_string())).unwrap(),
&Value::List(Arc::new(
vec![8.0, 9.0].into_iter().map(Value::Number).collect()
))
);
}
#[test]
fn interp_grp_by_numeric_key() {
let source = "key x:n>t;str x main xs:L n>M t L n;grp key xs";
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(
vec![1.0, 2.0, 1.0, 3.0, 2.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
let Value::Map(m) = result else {
panic!("expected Map")
};
assert_eq!(
m.get(&MapKey::Text("1".to_string())).unwrap(),
&Value::List(Arc::new(
vec![1.0, 1.0].into_iter().map(Value::Number).collect()
))
);
assert_eq!(
m.get(&MapKey::Text("2".to_string())).unwrap(),
&Value::List(Arc::new(
vec![2.0, 2.0].into_iter().map(Value::Number).collect()
))
);
assert_eq!(
m.get(&MapKey::Text("3".to_string())).unwrap(),
&Value::List(Arc::new(vec![3.0].into_iter().map(Value::Number).collect()))
);
}
#[test]
fn interp_grp_empty_list() {
let source = "id x:n>t;str x main xs:L n>M t L n;grp id xs";
let result = run_str(source, Some("main"), vec![Value::List(Arc::new(vec![]))]);
assert_eq!(
result,
Value::Map(Arc::new(std::collections::HashMap::new()))
);
}
#[test]
fn interp_grp_wrong_fn_arg() {
let err = run_str_err("f>t;grp 42 [1, 2, 3]", Some("f"), vec![]);
assert!(err.contains("grp"), "got: {err}");
}
#[test]
fn interp_grp_wrong_list_arg() {
let err = run_str_err("id x:n>n;x f>t;grp id 42", Some("f"), vec![]);
assert!(err.contains("grp"), "got: {err}");
}
#[test]
fn interp_sum_basic() {
let source = "f xs:L n>n;sum xs";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(
vec![1.0, 2.0, 3.0, 4.0, 5.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
assert_eq!(result, Value::Number(15.0));
}
#[test]
fn interp_sum_empty() {
let source = "f xs:L n>n;sum xs";
let result = run_str(source, Some("f"), vec![Value::List(Arc::new(vec![]))]);
assert_eq!(result, Value::Number(0.0));
}
#[test]
fn interp_sum_wrong_arg() {
let err = run_str_err("f>n;sum 42", Some("f"), vec![]);
assert!(err.contains("sum"), "got: {err}");
}
#[test]
fn interp_sum_non_numeric_element() {
let err = run_str_err(r#"f>n;sum ["a", "b"]"#, Some("f"), vec![]);
assert!(err.contains("sum"), "got: {err}");
}
#[test]
fn interp_avg_basic() {
let source = "f xs:L n>n;avg xs";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(
vec![2.0, 4.0, 6.0].into_iter().map(Value::Number).collect(),
))],
);
assert_eq!(result, Value::Number(4.0));
}
#[test]
fn interp_avg_empty_error() {
let err = run_str_err("f>n;avg []", Some("f"), vec![]);
assert!(err.contains("avg"), "got: {err}");
}
#[test]
fn interp_avg_wrong_arg() {
let err = run_str_err("f>n;avg 42", Some("f"), vec![]);
assert!(err.contains("avg"), "got: {err}");
}
#[test]
fn interp_min_lst_basic() {
let source = "f xs:L n>n;min xs";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(
vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
assert_eq!(result, Value::Number(1.0));
}
#[test]
fn interp_max_lst_basic() {
let source = "f xs:L n>n;max xs";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(
vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0]
.into_iter()
.map(Value::Number)
.collect(),
))],
);
assert_eq!(result, Value::Number(9.0));
}
#[test]
fn interp_min_lst_empty_errors() {
let err = run_str_err("f>n;min []", Some("f"), vec![]);
assert!(err.contains("min") && err.contains("empty"), "got: {err}");
}
#[test]
fn interp_max_lst_empty_errors() {
let err = run_str_err("f>n;max []", Some("f"), vec![]);
assert!(err.contains("max") && err.contains("empty"), "got: {err}");
}
#[test]
fn interp_min_lst_non_list_arg() {
let err = run_str_err("f x:_>n;min x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("min"), "got: {err}");
}
#[test]
fn interp_max_lst_non_list_arg() {
let err = run_str_err("f x:_>n;max x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("max"), "got: {err}");
}
#[test]
fn interp_min_lst_non_number_element() {
let err = run_str_err(
"f xs:L n>n;min xs",
Some("f"),
vec![Value::List(Arc::new(vec![Value::Text(Arc::new(
"x".to_string(),
))]))],
);
assert!(err.contains("min"), "got: {err}");
}
#[test]
fn interp_max_lst_non_number_element() {
let err = run_str_err(
"f xs:L n>n;max xs",
Some("f"),
vec![Value::List(Arc::new(vec![Value::Text(Arc::new(
"x".to_string(),
))]))],
);
assert!(err.contains("max"), "got: {err}");
}
#[test]
fn interp_min_lst_nan_propagates() {
let result = run_str(
"f xs:L n>n;min xs",
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(f64::NAN),
Value::Number(4.0),
]))],
);
match result {
Value::Number(n) => assert!(n.is_nan(), "expected NaN, got {n}"),
other => panic!("expected Number(NaN), got {other:?}"),
}
}
#[test]
fn interp_max_lst_nan_propagates() {
let result = run_str(
"f xs:L n>n;max xs",
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(f64::NAN),
Value::Number(4.0),
]))],
);
match result {
Value::Number(n) => assert!(n.is_nan(), "expected NaN, got {n}"),
other => panic!("expected Number(NaN), got {other:?}"),
}
}
#[test]
fn interp_wr_csv_output() {
let dir = std::env::temp_dir();
let path = dir.join("ilo_test_wr_csv.csv");
let path_str = path.to_str().unwrap();
let source = format!(
r#"f>R t t;wr "{}" [["name", "age"], ["alice", 30], ["bob", 25]] "csv""#,
path_str.replace('\\', "\\\\")
);
let result = run_str(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "name,age\nalice,30\nbob,25\n");
let _ = std::fs::remove_file(&path);
}
#[test]
fn interp_wr_csv_quoted_fields() {
let dir = std::env::temp_dir();
let path = dir.join("ilo_test_wr_csv_quoted.csv");
let path_str = path.to_str().unwrap();
let source = format!(
r#"f>R t t;wr "{}" [["a,b", "c\"d"]] "csv""#,
path_str.replace('\\', "\\\\")
);
let result = run_str(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "\"a,b\",\"c\"\"d\"\n");
let _ = std::fs::remove_file(&path);
}
#[test]
fn interp_wr_json_output() {
let dir = std::env::temp_dir();
let path = dir.join("ilo_test_wr_json.json");
let path_str = path.to_str().unwrap();
let source = format!(
r#"f>R t t;wr "{}" [1, 2, 3] "json""#,
path_str.replace('\\', "\\\\")
);
let result = run_str(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(&path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed, serde_json::json!([1.0, 2.0, 3.0]));
let _ = std::fs::remove_file(&path);
}
#[test]
fn interp_wr_unknown_format() {
let err = run_str_err(r#"f>R t t;wr "/tmp/x" "data" "xml""#, Some("f"), vec![]);
assert!(err.contains("unknown format"), "got: {err}");
}
#[test]
fn interp_rgx_find_all() {
let source = r#"f s:t>L t;rgx "\d+" s"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("abc 123 def 456".to_string()))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Text(Arc::new("123".to_string())),
Value::Text(Arc::new("456".to_string())),
]))
);
}
#[test]
fn interp_rgx_capture_groups() {
let source = r#"f s:t>L t;rgx "(\w+)=(\w+)" s"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("name=alice age=30".to_string()))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Text(Arc::new("name".to_string())),
Value::Text(Arc::new("alice".to_string())),
]))
);
}
#[test]
fn interp_rgx_no_match() {
let source = r#"f s:t>L t;rgx "\d+" s"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("no numbers here".to_string()))],
);
assert_eq!(result, Value::List(Arc::new(vec![])));
}
#[test]
fn interp_rgx_invalid_pattern() {
let err = run_str_err(r#"f>L t;rgx "[invalid" "test""#, Some("f"), vec![]);
assert!(err.contains("rgx"), "got: {err}");
}
#[test]
fn interp_rgx_wrong_arg_types() {
let err = run_str_err(r#"f>L t;rgx 42 "test""#, Some("f"), vec![]);
assert!(err.contains("rgx"), "got: {err}");
}
#[test]
fn interp_flat_nested() {
let source = "f>L n;flat [[1, 2], [3], [4, 5]]";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(
result,
Value::List(Arc::new(
vec![1.0, 2.0, 3.0, 4.0, 5.0]
.into_iter()
.map(Value::Number)
.collect()
))
);
}
#[test]
fn interp_flat_mixed() {
let source = "f>L n;flat [[1, 2], 3]";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(
result,
Value::List(Arc::new(
vec![1.0, 2.0, 3.0].into_iter().map(Value::Number).collect()
))
);
}
#[test]
fn interp_flat_empty() {
let source = "f>L n;flat []";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::List(Arc::new(vec![])));
}
#[test]
fn interp_flat_wrong_arg() {
let err = run_str_err("f>L n;flat 42", Some("f"), vec![]);
assert!(err.contains("flat"), "got: {err}");
}
#[test]
fn interp_user_hof_fn_type() {
let source = "sq x:n>n;*x x apl f:F n n x:n>n;f x";
let result = run_str(
source,
Some("apl"),
vec![Value::FnRef("sq".to_string()), Value::Number(7.0)],
);
assert_eq!(result, Value::Number(49.0));
}
#[test]
fn interp_fn_ref_via_ref_expr() {
let source = "dbl x:n>n;*x 2 main>n;f=dbl;f 10";
let result = run_str(source, Some("main"), vec![]);
assert_eq!(result, Value::Number(20.0));
}
#[test]
fn interpret_trm_basic() {
let result = run_str(
"f s:t>t;trm s",
Some("f"),
vec![Value::Text(Arc::new(" hello ".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("hello".to_string())));
}
#[test]
fn interpret_trm_no_whitespace() {
let result = run_str(
"f s:t>t;trm s",
Some("f"),
vec![Value::Text(Arc::new("hi".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("hi".to_string())));
}
#[test]
fn interpret_trm_only_whitespace() {
let result = run_str(
"f s:t>t;trm s",
Some("f"),
vec![Value::Text(Arc::new(" ".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("".to_string())));
}
#[test]
fn err_trm_wrong_type() {
let err = run_str_err("f x:n>t;trm x", Some("f"), vec![Value::Number(1.0)]);
assert!(
err.contains("trm requires text"),
"expected trm type error, got: {err}"
);
}
#[test]
fn interpret_unq_list_numbers() {
let result = run_str(
"f xs:L n>L n;unq xs",
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(1.0),
Value::Number(3.0),
Value::Number(2.0),
]))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0)
]))
);
}
#[test]
fn interpret_unq_list_strings() {
let result = run_str(
"f xs:L t>L t;unq xs",
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
Value::Text(Arc::new("a".to_string())),
]))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string()))
]))
);
}
#[test]
fn interpret_unq_text_chars() {
let result = run_str(
"f s:t>t;unq s",
Some("f"),
vec![Value::Text(Arc::new("aabbc".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("abc".to_string())));
}
#[test]
fn interpret_unq_empty_list() {
let result = run_str(
"f xs:L n>L n;unq xs",
Some("f"),
vec![Value::List(Arc::new(vec![]))],
);
assert_eq!(result, Value::List(Arc::new(vec![])));
}
#[test]
fn interpret_unq_preserves_order() {
let result = run_str(
"f xs:L n>L n;unq xs",
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(3.0),
Value::Number(1.0),
Value::Number(2.0),
Value::Number(1.0),
Value::Number(3.0),
]))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Number(3.0),
Value::Number(1.0),
Value::Number(2.0)
]))
);
}
#[test]
fn interpret_fmt_basic() {
let result = run_str(
r#"f a:t b:t>t;fmt "{} + {}" a b"#,
Some("f"),
vec![
Value::Text(Arc::new("1".to_string())),
Value::Text(Arc::new("2".to_string())),
],
);
assert_eq!(result, Value::Text(Arc::new("1 + 2".to_string())));
}
#[test]
fn interpret_fmt_template_only() {
let result = run_str(r#"f>t;fmt "hello""#, Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("hello".to_string())));
}
#[test]
fn interpret_fmt_fewer_args_than_slots() {
let result = run_str(
r#"f a:t>t;fmt "{} and {}" a"#,
Some("f"),
vec![Value::Text(Arc::new("x".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("x and {}".to_string())));
}
#[test]
fn interpret_fmt_number_arg() {
let result = run_str(
r#"f n:n>t;fmt "value: {}" n"#,
Some("f"),
vec![Value::Number(42.0)],
);
assert_eq!(result, Value::Text(Arc::new("value: 42".to_string())));
}
#[test]
fn interpret_srt_fn_by_length() {
let source = "ln s:t>n;len s main xs:L t>L t;srt ln xs";
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(vec![
Value::Text(Arc::new("banana".to_string())),
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("cc".to_string())),
]))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("cc".to_string())),
Value::Text(Arc::new("banana".to_string())),
]))
);
}
#[test]
fn interpret_srt_fn_numeric_key() {
let source = "neg x:n>n;-x main xs:L n>L n;srt neg xs";
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(3.0),
Value::Number(2.0),
]))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Number(3.0),
Value::Number(2.0),
Value::Number(1.0)
]))
);
}
#[test]
fn interpret_prnt_returns_value() {
let result = run_str("f x:n>n;prnt x", Some("f"), vec![Value::Number(7.0)]);
assert_eq!(result, Value::Number(7.0));
}
#[test]
fn interpret_prnt_text_passthrough() {
let result = run_str(
"f s:t>t;prnt s",
Some("f"),
vec![Value::Text(Arc::new("hi".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("hi".to_string())));
}
#[test]
fn interpret_rdb_csv() {
let result = run_str(
r#"f s:t>t;rdb s "csv""#,
Some("f"),
vec![Value::Text(Arc::new("a,b\n1,2".to_string()))],
);
let Value::Ok(inner) = result else {
panic!("expected Ok")
};
let Value::List(rows) = *inner else {
panic!("expected list")
};
assert_eq!(rows.len(), 2);
assert!(matches!(&rows[0], Value::List(_)));
}
#[test]
fn interpret_rdb_json() {
let result = run_str(
r#"f s:t>t;rdb s "json""#,
Some("f"),
vec![Value::Text(Arc::new(r#"{"x":1}"#.to_string()))],
);
assert!(
matches!(result, Value::Ok(_)),
"expected Ok, got {:?}",
result
);
}
#[test]
fn interpret_rdb_invalid_json_is_err() {
let result = run_str(
r#"f s:t>t;rdb s "json""#,
Some("f"),
vec![Value::Text(Arc::new("not json".to_string()))],
);
assert!(
matches!(result, Value::Err(_)),
"expected Err, got {:?}",
result
);
}
#[test]
fn interpret_rdb_raw_passthrough() {
let result = run_str(
r#"f s:t>t;rdb s "raw""#,
Some("f"),
vec![Value::Text(Arc::new("hello".to_string()))],
);
assert_eq!(
result,
Value::Ok(Box::new(Value::Text(Arc::new("hello".to_string()))))
);
}
#[test]
fn interpret_rd_file_not_found() {
let result = run_str(
"f p:t>t;rd p",
Some("f"),
vec![Value::Text(Arc::new(
"/nonexistent/ilo_test_file.txt".to_string(),
))],
);
assert!(
matches!(result, Value::Err(_)),
"expected Err, got {:?}",
result
);
}
#[test]
fn interpret_type_is_number_match() {
let result = run_str(
r#"f x:n>t;?x{n v:"num";_:"other"}"#,
Some("f"),
vec![Value::Number(42.0)],
);
assert_eq!(result, Value::Text(Arc::new("num".to_string())));
}
#[test]
fn interpret_type_is_text_match() {
let result = run_str(
r#"f x:t>t;?x{t v:v;_:"other"}"#,
Some("f"),
vec![Value::Text(Arc::new("hello".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("hello".to_string())));
}
#[test]
fn interpret_type_is_bool_match() {
let result = run_str(
r#"f x:b>t;?x{b v:"bool";_:"other"}"#,
Some("f"),
vec![Value::Bool(true)],
);
assert_eq!(result, Value::Text(Arc::new("bool".to_string())));
}
#[test]
fn interpret_type_is_no_match_falls_through() {
let result = run_str(
r#"f x:n>t;?x{t v:"text";_:"other"}"#,
Some("f"),
vec![Value::Number(1.0)],
);
assert_eq!(result, Value::Text(Arc::new("other".to_string())));
}
#[test]
fn interpret_type_is_wildcard_binding() {
let result = run_str(
r#"f x:n>t;?x{n _:"matched";_:"other"}"#,
Some("f"),
vec![Value::Number(5.0)],
);
assert_eq!(result, Value::Text(Arc::new("matched".to_string())));
}
#[test]
fn interpret_text_greater_than() {
let result = run_str(
"f a:t b:t>b;>a b",
Some("f"),
vec![
Value::Text(Arc::new("b".to_string())),
Value::Text(Arc::new("a".to_string())),
],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpret_text_less_than() {
let result = run_str(
"f a:t b:t>b;<a b",
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpret_text_greater_or_equal() {
let result = run_str(
"f a:t b:t>b;>=a b",
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("a".to_string())),
],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpret_text_less_or_equal() {
let result = run_str(
"f a:t b:t>b;<=a b",
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpret_destructure_non_record_error() {
let prog = parse_program("type pt{x:n;y:n} f p:pt>n;{x;y}=p;+x y");
let result = run(&prog, Some("f"), vec![Value::Number(42.0)]);
assert!(
result.is_err(),
"expected error for destructure on non-record"
);
}
#[test]
fn interpret_safe_field_on_nil_returns_nil() {
let result = run_str("f>n;x=mget mmap \"key\";x.?field", Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
fn interpret_safe_index_on_nil_returns_nil() {
let result = run_str("f>n;xs=mget mmap \"key\";xs.?0", Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
fn values_equal_texts() {
assert!(values_equal(
&Value::Text(Arc::new("a".to_string())),
&Value::Text(Arc::new("a".to_string()))
));
assert!(!values_equal(
&Value::Text(Arc::new("a".to_string())),
&Value::Text(Arc::new("b".to_string()))
));
}
#[test]
fn display_fnref() {
assert_eq!(format!("{}", Value::FnRef("add".into())), "<fn:add>");
}
#[test]
fn parse_csv_content_single_row_escaped_quote() {
let rows = parse_csv_content(r#""he said ""hello""","world""#, ',');
assert_eq!(rows.len(), 1);
assert_eq!(rows[0], vec![r#"he said "hello""#, "world"]);
}
#[test]
fn parse_csv_content_single_row_simple_quoted() {
let rows = parse_csv_content(r#""hello","world""#, ',');
assert_eq!(rows.len(), 1);
assert_eq!(rows[0], vec!["hello", "world"]);
}
#[test]
fn parse_csv_content_multiline_quoted_field() {
let input = "name,note,n\nplain,\"line\nbreak\",2\n";
let rows = parse_csv_content(input, ',');
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], vec!["name", "note", "n"]);
assert_eq!(rows[1], vec!["plain", "line\nbreak", "2"]);
}
#[test]
fn parse_csv_content_basic_no_trailing_newline() {
let rows = parse_csv_content("a,b\nc,d", ',');
assert_eq!(rows, vec![vec!["a", "b"], vec!["c", "d"]]);
}
#[test]
fn parse_csv_content_basic_trailing_newline_no_phantom_row() {
let rows = parse_csv_content("a,b\nc,d\n", ',');
assert_eq!(rows, vec![vec!["a", "b"], vec!["c", "d"]]);
}
#[test]
fn parse_csv_content_crlf_line_endings() {
let rows = parse_csv_content("a,b\r\nc,d\r\n", ',');
assert_eq!(rows, vec![vec!["a", "b"], vec!["c", "d"]]);
}
#[test]
fn parse_csv_content_crlf_inside_quoted_field_preserved() {
let rows = parse_csv_content("a,\"x\r\ny\"\n", ',');
assert_eq!(rows, vec![vec!["a".to_string(), "x\r\ny".to_string()]]);
}
#[test]
fn parse_csv_content_escaped_quote_inside_multiline_field() {
let input = "a,\"he said \"\"hi\"\"\nfoo\",b\n";
let rows = parse_csv_content(input, ',');
assert_eq!(rows.len(), 1);
assert_eq!(rows[0], vec!["a", "he said \"hi\"\nfoo", "b"]);
}
#[test]
fn parse_csv_content_tsv_separator() {
let rows = parse_csv_content("a\tb\nc\td\n", '\t');
assert_eq!(rows, vec![vec!["a", "b"], vec!["c", "d"]]);
}
#[test]
fn parse_csv_content_empty_input() {
let rows = parse_csv_content("", ',');
assert!(rows.is_empty());
}
#[test]
fn parse_csv_content_embedded_comma_in_quoted_field() {
let rows = parse_csv_content("a,\"x,y\",b\n", ',');
assert_eq!(rows, vec![vec!["a", "x,y", "b"]]);
}
#[test]
fn parse_csv_content_mixed_quoted_and_unquoted_in_same_row() {
let rows = parse_csv_content("alice,\"engineer, sr.\",30,\"London\"\n", ',');
assert_eq!(rows, vec![vec!["alice", "engineer, sr.", "30", "London"]]);
}
#[test]
fn parse_csv_content_empty_trailing_field() {
let rows = parse_csv_content("a,b,\nc,d,\n", ',');
assert_eq!(rows, vec![vec!["a", "b", ""], vec!["c", "d", ""]]);
}
#[test]
fn parse_csv_content_empty_field_in_middle() {
let rows = parse_csv_content("a,,b\n", ',');
assert_eq!(rows, vec![vec!["a", "", "b"]]);
}
#[test]
fn parse_csv_content_empty_quoted_field() {
let rows = parse_csv_content("a,\"\",b\n", ',');
assert_eq!(rows, vec![vec!["a", "", "b"]]);
}
#[test]
fn parse_csv_content_utf8_bom_preserved_in_first_cell() {
let rows = parse_csv_content("\u{feff}name,age\nalice,30\n", ',');
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], vec!["\u{feff}name", "age"]);
assert_eq!(rows[1], vec!["alice", "30"]);
}
#[test]
fn parse_csv_content_single_unterminated_quoted_field() {
let rows = parse_csv_content("a,\"oops\n", ',');
assert_eq!(rows.len(), 1);
assert_eq!(rows[0], vec!["a", "oops\n"]);
}
#[test]
fn parse_csv_content_round_trip_via_write_csv_tsv() {
let original = vec![
Value::List(Arc::new(vec![
Value::Text(Arc::new("name".to_string())),
Value::Text(Arc::new("note".to_string())),
Value::Text(Arc::new("n".to_string())),
])),
Value::List(Arc::new(vec![
Value::Text(Arc::new("plain".to_string())),
Value::Text(Arc::new("line\nbreak".to_string())),
Value::Number(2.0),
])),
];
let serialised = write_csv_tsv(&original, ',').expect("write_csv_tsv failed");
let rows = parse_csv_content(&serialised, ',');
assert_eq!(rows.len(), 2, "round-trip produced wrong row count");
assert_eq!(rows[0], vec!["name", "note", "n"]);
assert_eq!(rows[1], vec!["plain", "line\nbreak", "2"]);
}
#[test]
fn interpret_len_map() {
let result = run_str(
r#"f>n;m=mset (mset mmap "a" 1) "b" 2;len m"#,
Some("f"),
vec![],
);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn interpret_mget_wrong_args() {
let err = run_str_err("f>n;mget 42 \"key\"", Some("f"), vec![]);
assert!(err.contains("mget"), "got: {err}");
}
#[test]
fn interpret_mset_wrong_args() {
let err = run_str_err("f>n;mset 42 \"key\" 1", Some("f"), vec![]);
assert!(err.contains("mset"), "got: {err}");
}
#[test]
fn interpret_mhas_wrong_args() {
let err = run_str_err("f>n;mhas 42 \"key\"", Some("f"), vec![]);
assert!(err.contains("mhas"), "got: {err}");
}
#[test]
fn interpret_mkeys_wrong_args() {
let err = run_str_err("f>n;mkeys 42", Some("f"), vec![]);
assert!(err.contains("mkeys"), "got: {err}");
}
#[test]
fn interpret_mvals_wrong_args() {
let err = run_str_err("f>n;mvals 42", Some("f"), vec![]);
assert!(err.contains("mvals"), "got: {err}");
}
#[test]
fn interpret_mdel_wrong_args() {
let err = run_str_err("f>n;mdel 42 \"key\"", Some("f"), vec![]);
assert!(err.contains("mdel"), "got: {err}");
}
#[test]
fn interpret_rnd_wrong_types() {
let err = run_str_err(r#"f>n;rnd "a" "b""#, Some("f"), vec![]);
assert!(err.contains("rnd"), "got: {err}");
}
#[test]
fn interpret_srt_key_fn_wrong_second_arg() {
let source = "sq x:n>n;*x x f>n;srt sq 42";
let err = run_str_err(source, Some("f"), vec![]);
assert!(err.contains("srt"), "got: {err}");
}
#[test]
fn interpret_srt_key_fn_text_keys() {
let source = "id x:t>t;x main xs:L t>L t;srt id xs";
let result = run_str(
source,
Some("main"),
vec![Value::List(Arc::new(vec![
Value::Text(Arc::new("banana".to_string())),
Value::Text(Arc::new("apple".to_string())),
Value::Text(Arc::new("cherry".to_string())),
]))],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Text(Arc::new("apple".to_string())),
Value::Text(Arc::new("banana".to_string())),
Value::Text(Arc::new("cherry".to_string())),
]))
);
}
#[test]
fn interpret_get_invalid_headers() {
let err = run_str_err(r#"f>t;get "http://x" 42"#, Some("f"), vec![]);
assert!(
err.contains("headers") || err.contains("M t t"),
"got: {err}"
);
}
#[test]
fn interpret_pst_wrong_arg_types() {
let err = run_str_err(r#"f>t;pst 42 "body""#, Some("f"), vec![]);
assert!(err.contains("pst"), "got: {err}");
}
#[test]
fn interpret_pst_invalid_headers() {
let err = run_str_err(r#"f>t;pst "http://x" "body" 42"#, Some("f"), vec![]);
assert!(err.contains("headers") || err.contains("pst"), "got: {err}");
}
#[test]
fn interpret_unq_wrong_type() {
let err = run_str_err("f>n;unq 42", Some("f"), vec![]);
assert!(err.contains("unq"), "got: {err}");
}
#[test]
fn interpret_fmt_wrong_first_arg() {
let err = run_str_err("f>n;fmt 42", Some("f"), vec![]);
assert!(err.contains("fmt"), "got: {err}");
}
#[test]
fn interpret_rd_wrong_arg_type() {
let err = run_str_err("f>t;rd 42", Some("f"), vec![]);
assert!(err.contains("rd"), "got: {err}");
}
#[test]
fn interpret_rd_with_wrong_format_type() {
let err = run_str_err("f>t;rd \"/tmp\" 42", Some("f"), vec![]);
assert!(err.contains("rd") || err.contains("format"), "got: {err}");
}
#[test]
fn interpret_rd_json_path_returns_raw_text() {
let mut path = std::env::temp_dir();
path.push("ilo_interp_rd_json_raw.json");
std::fs::write(&path, r#"{"key":"value"}"#).unwrap();
let path_str = path.to_str().unwrap().to_string();
let result = run_str(
"f p:t>R t t;rd p",
Some("f"),
vec![Value::Text(Arc::new(path_str))],
);
std::fs::remove_file(&path).ok();
match &result {
Value::Ok(inner) => assert!(
matches!(inner.as_ref(), Value::Text(_)),
"rd on .json must return raw text, not {:?}",
inner
),
other => panic!("expected Ok(text), got {other:?}"),
}
}
#[test]
fn interpret_rd_json_builtin_parses_json() {
let mut path = std::env::temp_dir();
path.push("ilo_interp_rd_json_builtin.json");
std::fs::write(&path, r#"{"key":"value"}"#).unwrap();
let path_str = path.to_str().unwrap().to_string();
let result = run_str(
"f p:t>R _ t;rd-json p",
Some("f"),
vec![Value::Text(Arc::new(path_str))],
);
std::fs::remove_file(&path).ok();
match &result {
Value::Ok(inner) => assert!(
!matches!(inner.as_ref(), Value::Text(_)),
"rd-json must return parsed value, not raw text"
),
other => panic!("expected Ok(parsed), got {other:?}"),
}
}
#[test]
fn interpret_rd_json_not_found() {
let result = run_str(
"f p:t>R _ t;rd-json p",
Some("f"),
vec![Value::Text(Arc::new(
"/nonexistent/ilo_rd_json_test.json".to_string(),
))],
);
assert!(
matches!(result, Value::Err(_)),
"expected Err for missing file, got {:?}",
result
);
}
#[test]
fn interpret_rdb_wrong_first_arg() {
let err = run_str_err(r#"f>t;rdb 42 "raw""#, Some("f"), vec![]);
assert!(err.contains("rdb"), "got: {err}");
}
#[test]
fn interpret_rdb_wrong_format_arg() {
let err = run_str_err(r#"f>t;rdb "hello" 42"#, Some("f"), vec![]);
assert!(err.contains("rdb") || err.contains("format"), "got: {err}");
}
#[test]
fn interpret_rdl_basic() {
let mut path = std::env::temp_dir();
path.push("ilo_interp_rdl_test.txt");
std::fs::write(&path, "line1\nline2\nline3").unwrap();
let path_str = path.to_str().unwrap().to_string();
let result = run_str(
"f p:t>t;rdl p",
Some("f"),
vec![Value::Text(Arc::new(path_str))],
);
std::fs::remove_file(&path).ok();
let Value::Ok(inner) = result else {
panic!("expected Ok")
};
let Value::List(lines) = *inner else {
panic!("expected list")
};
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], Value::Text(Arc::new("line1".to_string())));
}
#[test]
fn interpret_rdl_not_found() {
let result = run_str(
"f p:t>t;rdl p",
Some("f"),
vec![Value::Text(Arc::new(
"/nonexistent/ilo_rdl_test.txt".to_string(),
))],
);
assert!(
matches!(result, Value::Err(_)),
"expected Err, got {:?}",
result
);
}
#[test]
fn interpret_rdl_wrong_arg() {
let err = run_str_err("f>t;rdl 42", Some("f"), vec![]);
assert!(err.contains("rdl"), "got: {err}");
}
#[test]
fn interpret_wr_basic() {
let mut path = std::env::temp_dir();
path.push("ilo_interp_wr_test.txt");
let path_str = path.to_str().unwrap().to_string();
let result = run_str(
"f p:t>t;wr p \"hello\"",
Some("f"),
vec![Value::Text(Arc::new(path_str.clone()))],
);
std::fs::remove_file(&path).ok();
assert!(
matches!(result, Value::Ok(_)),
"expected Ok, got {:?}",
result
);
}
#[test]
fn interpret_wr_wrong_args() {
let err = run_str_err("f>t;wr 42 \"hello\"", Some("f"), vec![]);
assert!(err.contains("wr"), "got: {err}");
}
#[test]
fn interpret_wrl_basic() {
let mut path = std::env::temp_dir();
path.push("ilo_interp_wrl_test.txt");
let path_str = path.to_str().unwrap().to_string();
let result = run_str(
"f p:t>t;wrl p [\"a\", \"b\", \"c\"]",
Some("f"),
vec![Value::Text(Arc::new(path_str.clone()))],
);
std::fs::remove_file(&path).ok();
assert!(
matches!(result, Value::Ok(_)),
"expected Ok, got {:?}",
result
);
}
#[test]
fn interpret_wrl_non_text_item() {
let mut path = std::env::temp_dir();
path.push("ilo_interp_wrl_nontxt_test.txt");
let path_str = path.to_str().unwrap().to_string();
let mut env = Env::new();
let result = call_function(
&mut env,
"wrl",
vec![
Value::Text(Arc::new(path_str.clone())),
Value::List(Arc::new(vec![
Value::Text(Arc::new("ok".to_string())),
Value::Number(99.0),
])),
],
);
std::fs::remove_file(&path).ok();
assert!(result.is_err(), "expected error for non-text wrl item");
let err = result.unwrap_err().to_string();
assert!(err.contains("wrl"), "got: {err}");
}
#[test]
fn interpret_wrl_wrong_args() {
let err = run_str_err("f>t;wrl 42 [\"a\"]", Some("f"), vec![]);
assert!(err.contains("wrl"), "got: {err}");
}
#[test]
fn interpret_jpth_array_index() {
let source = r#"f j:t p:t>R _ t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new(r#"[10,20,30]"#.to_string())),
Value::Text(Arc::new("1".to_string())),
],
);
assert_eq!(result, Value::Ok(Box::new(Value::Number(20.0))));
}
#[test]
fn interpret_jpth_wrong_args() {
let err = run_str_err(r#"f>t;jpth 42 "path""#, Some("f"), vec![]);
assert!(err.contains("jpth"), "got: {err}");
}
#[test]
fn interp_jdmp_ok_value() {
let result = run_str("f>t;jdmp ~42", Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("42".to_string())));
}
#[test]
fn interp_jdmp_fnref() {
let source = "sq x:n>n;*x x f>t;r=sq;jdmp r";
let result = run_str(source, Some("f"), vec![]);
let Value::Text(s) = result else {
panic!("expected Text")
};
assert!(s.contains("fn:sq") || s.contains("sq"), "got: {s}");
}
#[test]
fn interp_jpar_wrong_arg_type() {
let err = run_str_err("f>t;jpar 42", Some("f"), vec![]);
assert!(err.contains("jpar"), "got: {err}");
}
#[test]
fn interpret_env_wrong_arg_type() {
let err = run_str_err("f>t;env 42", Some("f"), vec![]);
assert!(err.contains("env"), "got: {err}");
}
#[test]
fn interpret_map_wrong_fn_arg() {
let err = run_str_err("f>t;map 42 [1, 2]", Some("f"), vec![]);
assert!(err.contains("map"), "got: {err}");
}
#[test]
fn interpret_map_wrong_list_arg() {
let source = "sq x:n>n;*x x f>t;map sq 42";
let err = run_str_err(source, Some("f"), vec![]);
assert!(err.contains("map"), "got: {err}");
}
#[test]
fn interpret_flt_predicate_returns_non_bool() {
let source = "id x:n>n;x f xs:L n>L n;flt id xs";
let err = run_str_err(
source,
Some("f"),
vec![Value::List(Arc::new(vec![Value::Number(1.0)]))],
);
assert!(err.contains("flt") || err.contains("bool"), "got: {err}");
}
#[test]
fn interpret_flt_wrong_list_arg() {
let source = "pos x:n>b;>x 0 f>t;flt pos 42";
let err = run_str_err(source, Some("f"), vec![]);
assert!(err.contains("flt"), "got: {err}");
}
#[test]
fn interpret_fld_wrong_list_arg() {
let source = "add a:n b:n>n;+a b f>n;fld add 42 0";
let err = run_str_err(source, Some("f"), vec![]);
assert!(err.contains("fld"), "got: {err}");
}
#[test]
fn interpret_fld_wrong_fn_arg() {
let err = run_str_err("f>n;fld 42 [1, 2] 0", Some("f"), vec![]);
assert!(err.contains("fld"), "got: {err}");
}
#[test]
fn interpret_call_use_decl_errors() {
use crate::ast::{Decl, Span};
let mut env = Env::new();
env.functions.insert(
"fake_use".to_string(),
Decl::Use {
path: "x.ilo".to_string(),
only: None,
alias: None,
predicate: None,
alt_path: None,
reexport: false,
lazy: false,
span: Span { start: 0, end: 0 },
},
);
let result = call_function(&mut env, "fake_use", vec![]);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("unresolved import")
);
}
#[test]
fn interpret_call_alias_decl_errors() {
use crate::ast::{Decl, Span, Type};
let mut env = Env::new();
env.functions.insert(
"myalias".to_string(),
Decl::Alias {
name: "myalias".to_string(),
target: Type::Number,
span: Span { start: 0, end: 0 },
},
);
let result = call_function(&mut env, "myalias", vec![]);
assert!(result.is_err());
}
#[test]
fn interpret_call_error_decl_errors() {
use crate::ast::{Decl, Span};
let mut env = Env::new();
env.functions.insert(
"bad_decl".to_string(),
Decl::Error {
span: Span { start: 0, end: 0 },
},
);
let result = call_function(&mut env, "bad_decl", vec![]);
assert!(result.is_err());
}
#[test]
fn interpret_match_continue_arm_returns_nil() {
let source = "f xs:L n>n;@x xs{?x{1:cnt;_:x}}";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
]))],
);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn interpret_guard_ternary_in_foreach() {
let source = "f xs:L n>n;@x xs{=x 0{10}{20}}";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(0.0),
Value::Number(1.0),
]))],
);
assert_eq!(result, Value::Number(20.0));
}
#[test]
fn interpret_match_stmt_continue_propagates() {
let source = "f xs:L n>n;@x xs{?x{1:cnt;_:x}}";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(5.0),
]))],
);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn interpret_foreach_return_from_nested_match() {
let source = "f xs:L n>n;@x xs{?x{5:x;_:0}}";
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(5.0),
Value::Number(9.0),
]))],
);
assert_eq!(result, Value::Number(0.0));
}
#[test]
fn interpret_range_end_not_number() {
let source = "f s:n en:n>n;@i s..en{i}";
let result = run_str(
source,
Some("f"),
vec![Value::Number(0.0), Value::Number(3.0)],
);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn interp_jdmp_large_float() {
let source = "f x:n>t;jdmp x";
let result = run_str(source, Some("f"), vec![Value::Number(1.23456789e20)]);
assert!(matches!(result, Value::Text(_)));
}
#[test]
fn interp_jdmp_err_value() {
let result = run_str("f>t;jdmp ^42", Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("42".to_string())));
}
#[test]
fn interp_jdmp_map_value() {
let result = run_str(r#"f>t;m=mset mmap "k" 1;jdmp m"#, Some("f"), vec![]);
let Value::Text(s) = result else {
panic!("expected text")
};
assert!(s.contains("k"), "got: {s}");
}
#[test]
fn interpret_type_is_list_match() {
let source = r#"f x:L n>t;?x{l v:"list";_:"other"}"#;
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![Value::Number(1.0)]))],
);
assert_eq!(result, Value::Text(Arc::new("list".to_string())));
}
#[test]
fn interpret_rdb_csv_single_row() {
let result = run_str(
r#"f s:t>t;rdb s "csv""#,
Some("f"),
vec![Value::Text(Arc::new("a,b,c".to_string()))],
);
let Value::Ok(inner) = result else {
panic!("expected Ok")
};
let Value::List(rows) = *inner else {
panic!("expected list")
};
assert_eq!(rows.len(), 1);
}
#[test]
fn interpret_mhas_found() {
let result = run_str(r#"f>b;m=mset mmap "x" 1;mhas m "x""#, Some("f"), vec![]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpret_mhas_not_found() {
let result = run_str(r#"f>b;m=mset mmap "x" 1;mhas m "y""#, Some("f"), vec![]);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn interpret_mkeys_happy_path() {
let result = run_str(
r#"f>L t;m=mset (mset mmap "b" 2) "a" 1;mkeys m"#,
Some("f"),
vec![],
);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string()))
]))
);
}
#[test]
fn interpret_mvals_happy_path() {
let result = run_str(
r#"f>L n;m=mset (mset mmap "b" 2) "a" 1;mvals m"#,
Some("f"),
vec![],
);
assert_eq!(
result,
Value::List(Arc::new(vec![Value::Number(1.0), Value::Number(2.0)]))
);
}
#[test]
fn interpret_mdel_happy_path() {
let result = run_str(
r#"f>n;m=mset (mset mmap "a" 1) "b" 2;m2=mdel m "a";len m2"#,
Some("f"),
vec![],
);
assert_eq!(result, Value::Number(1.0));
}
#[test]
fn interpret_srt_key_not_fn_ref() {
let err = run_str_err(
"f xs:L n>L n;srt 42 xs",
Some("f"),
vec![Value::List(Arc::new(vec![Value::Number(1.0)]))],
);
assert!(err.contains("srt"), "got: {err}");
}
#[test]
fn interpret_flt_key_not_fn_ref() {
let err = run_str_err(
"f xs:L n>L n;flt 42 xs",
Some("f"),
vec![Value::List(Arc::new(vec![Value::Number(1.0)]))],
);
assert!(err.contains("flt"), "got: {err}");
}
#[test]
fn interpret_map_with_text_fn_name() {
let source = "sq x:n>n;*x x f cb:t xs:L n>L n;map cb xs";
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new("sq".to_string())),
Value::List(Arc::new(vec![Value::Number(3.0)])),
],
);
assert_eq!(result, Value::List(Arc::new(vec![Value::Number(9.0)])));
}
#[test]
fn interpret_rd_explicit_raw_format() {
let path = "/tmp/ilo_test_rd_explicit.txt";
std::fs::write(path, "hello").unwrap();
let source = format!(r#"f>R t t;rd "{path}" "raw""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(inner) = result else {
panic!("expected Ok")
};
assert_eq!(*inner, Value::Text(Arc::new("hello".to_string())));
}
#[test]
fn interpret_rd_explicit_format_parse_error() {
let path = "/tmp/ilo_test_rd_badjson.txt";
std::fs::write(path, "not json at all!!!").unwrap();
let source = format!(r#"f>R t t;rd "{path}" "json""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Err(_) = result else {
panic!("expected Err")
};
}
#[test]
fn interpret_wr_csv_format() {
let path = "/tmp/ilo_test_wr.csv";
let source = format!(r#"f>R t t;wr "{path}" [[1,2],[3,4]] "csv""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(_) = result else {
panic!("expected Ok")
};
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("1,2"));
}
#[test]
fn interpret_wr_csv_bool_field() {
let path = "/tmp/ilo_test_wr_bool.csv";
let source = format!(r#"f>R t t;wr "{path}" [[true,false]] "csv""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(_) = result else {
panic!("expected Ok")
};
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("true"));
}
#[test]
fn interpret_wr_json_format() {
let path = "/tmp/ilo_test_wr.json";
let source = format!(r#"f>R t t;wr "{path}" [1,2,3] "json""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(_) = result else {
panic!("expected Ok")
};
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("1"));
}
#[test]
fn interpret_grp_number_key() {
let source = "id x:n>n;x g xs:L n>_;grp id xs";
let result = run_str(
source,
Some("g"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(1.0),
]))],
);
let Value::Map(m) = result else {
panic!("expected map")
};
assert_eq!(m.len(), 2);
}
#[test]
fn interpret_grp_bool_key() {
let source = "pos x:n>b;>x 0 g xs:L n>_;grp pos xs";
let result = run_str(
source,
Some("g"),
vec![Value::List(Arc::new(vec![
Value::Number(-1.0),
Value::Number(1.0),
Value::Number(2.0),
]))],
);
let Value::Map(m) = result else {
panic!("expected map")
};
assert!(m.contains_key(&MapKey::Text("true".to_string())));
assert!(m.contains_key(&MapKey::Text("false".to_string())));
}
#[test]
fn interpret_avg_non_number_element() {
let err = run_str_err(
"f xs:L n>n;avg xs",
Some("f"),
vec![Value::List(Arc::new(vec![Value::Text(Arc::new(
"x".to_string(),
))]))],
);
assert!(err.contains("avg"), "got: {err}");
}
#[test]
fn interpret_rgx_non_text_second_arg() {
let err = run_str_err(r#"f>L t;rgx "." 42"#, Some("f"), vec![]);
assert!(err.contains("rgx"), "got: {err}");
}
#[test]
fn interpret_jdmp_bool_value() {
let result = run_str("f>t;jdmp true", Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("true".to_string())));
}
#[test]
fn interpret_jdmp_nil_value() {
let result = run_str(r#"f>t;jdmp (mget mmap "k")"#, Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("null".to_string())));
}
#[test]
fn interpret_wr_json_text_value() {
let path = "/tmp/ilo_test_wr_json_text.json";
let source = format!(r#"f>R t t;wr "{path}" "hello world" "json""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(_) = result else {
panic!("expected Ok")
};
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("hello world"));
}
#[test]
fn interpret_wr_json_bool_value() {
let path = "/tmp/ilo_test_wr_json_bool.json";
let source = format!(r#"f>R t t;wr "{path}" true "json""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(_) = result else {
panic!("expected Ok")
};
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("true"));
}
#[test]
fn interpret_wr_json_map_value() {
let path = "/tmp/ilo_test_wr_json_map.json";
let source = format!(r#"f>R t t;m=mset mmap "k" 42;wr "{path}" m "json""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(_) = result else {
panic!("expected Ok")
};
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("\"k\""));
assert!(content.contains("42"));
}
#[test]
fn interpret_wr_json_nil_value() {
let path = "/tmp/ilo_test_wr_json_nil.json";
let source = format!(r#"f>R t t;v=mget mmap "x";wr "{path}" v "json""#);
let result = run_str(&source, Some("f"), vec![]);
let Value::Ok(_) = result else {
panic!("expected Ok")
};
let content = std::fs::read_to_string(path).unwrap();
assert_eq!(content.trim(), "null");
}
#[test]
fn interpret_wr_non_text_format_arg_errors() {
let path = "/tmp/ilo_test_wr_fmt_err.csv";
let source = format!(r#"f>R t t;wr "{path}" [1] 42"#);
let err = run_str_err(&source, Some("f"), vec![]);
assert!(err.contains("wr"), "got: {err}");
}
#[test]
fn interpret_wr_csv_non_list_data_errors() {
let path = "/tmp/ilo_test_wr_csv_nonlist.csv";
let source = format!(r#"f>R t t;wr "{path}" 42 "csv""#);
let err = run_str_err(&source, Some("f"), vec![]);
assert!(err.contains("wr"), "got: {err}");
}
#[test]
fn interpret_wr_csv_row_not_a_list_errors() {
let path = "/tmp/ilo_test_wr_csv_row_err.csv";
let source = format!(r#"f>R t t;wr "{path}" [42] "csv""#);
let err = run_str_err(&source, Some("f"), vec![]);
assert!(err.contains("wr"), "got: {err}");
}
#[test]
fn interpret_grp_float_key() {
let source = "half x:n>n;/x 2 g xs:L n>_;grp half xs";
let result = run_str(
source,
Some("g"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0),
]))],
);
let Value::Map(m) = result else {
panic!("expected Map")
};
assert_eq!(m.len(), 2);
assert!(m.contains_key(&MapKey::Int(0)));
assert!(m.contains_key(&MapKey::Int(1)));
}
#[test]
fn interpret_for_range_early_return_via_guard() {
let result = run_str("f>n;@i 0..5{>=i 3{ret i};i}", Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn interpret_wr_csv_nil_field() {
let path = "/tmp/ilo_test_wr_nil.csv";
let source = format!(r#"f x:z>R t t;wr "{path}" [[x,1]] "csv""#);
let result = run_str(&source, Some("f"), vec![Value::Nil]);
let Value::Ok(_) = result else {
panic!("expected Ok, got {:?}", result)
};
let content = std::fs::read_to_string(path).unwrap();
assert!(!content.is_empty());
}
#[test]
fn interpret_wr_json_with_ok_value() {
let path = "/tmp/ilo_test_wr_ok.json";
let source = format!(r#"f x:z>R t t;wr "{path}" x "json""#);
let result = run_str(
&source,
Some("f"),
vec![Value::Ok(Box::new(Value::Number(1.0)))],
);
let Value::Ok(_) = result else {
panic!("expected Ok, got {:?}", result)
};
}
#[test]
fn interpret_wr_two_arg_non_text_content_error() {
let err = run_str_err(
r#"f>R t t;wr "/tmp/ilo_test_bad_wr.txt" 42"#,
Some("f"),
vec![],
);
assert!(
err.contains("wr") || err.contains("text") || err.contains("content"),
"got: {err}"
);
}
#[test]
fn interpret_wr_write_failure_returns_err() {
let source = r#"f>R t t;wr "/no/such/dir/ilo_test.txt" "hello""#;
let result = run_str(source, Some("f"), vec![]);
let Value::Err(_) = result else {
panic!("expected Err for bad path, got {:?}", result)
};
}
#[test]
fn interpret_wrl_write_failure_returns_err() {
let source = r#"f>R t t;wrl "/no/such/dir/ilo_test.txt" ["a","b"]"#;
let result = run_str(source, Some("f"), vec![]);
let Value::Err(_) = result else {
panic!("expected Err for bad path, got {:?}", result)
};
}
#[test]
fn interpret_jpth_array_index_out_of_bounds() {
let source = r#"f>R t t;jpth "[1,2,3]" "5""#;
let result = run_str(source, Some("f"), vec![]);
let Value::Err(inner) = result else {
panic!("expected Err, got {:?}", result)
};
let s = inner.to_string();
assert!(s.contains("not found") || s.contains("5"), "got: {s}");
}
#[test]
fn interpret_jpth_nested_array_no_stack_overflow() {
let json = r#"{"errors":[{"path":["user","name"]}]}"#;
let source = r#"f j:t p:t>R _ t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![
Value::Text(Arc::new(json.to_string())),
Value::Text(Arc::new("errors.0.path".to_string())),
],
);
let Value::Ok(inner) = result else {
panic!("expected Ok, got {:?}", result)
};
let Value::List(items) = *inner else {
panic!("expected List, got {:?}", inner)
};
assert_eq!(items.len(), 2);
assert_eq!(items[0], Value::Text(Arc::new("user".to_string())));
assert_eq!(items[1], Value::Text(Arc::new("name".to_string())));
}
#[test]
fn interpret_jpth_deeply_nested_no_stack_overflow() {
let mut s = "\"leaf\"".to_string();
for _ in 0..50 {
s = format!("[{}]", s);
}
let path = (0..50).map(|_| "0").collect::<Vec<_>>().join(".");
let source = r#"f j:t p:t>R _ t;jpth j p"#;
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new(s)), Value::Text(Arc::new(path))],
);
let Value::Ok(inner) = result else {
panic!("expected Ok, got {:?}", result)
};
assert_eq!(*inner, Value::Text(Arc::new("leaf".to_string())));
}
#[test]
fn interpret_grp_key_returns_list_error() {
let source = "mk x:n>L n;[x] g xs:L n>_;grp mk xs";
let err = run_str_err(
source,
Some("g"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
]))],
);
assert!(
err.contains("grp") || err.contains("key") || err.contains("string"),
"got: {err}"
);
}
#[test]
fn interpret_for_range_non_number_start_error() {
let err = run_str_err(
"f s:t>n;@i s..3{i}",
Some("f"),
vec![Value::Text(Arc::new("a".to_string()))],
);
assert!(
err.contains("range") || err.contains("number") || err.contains("start"),
"got: {err}"
);
}
#[test]
fn interpret_for_range_non_number_end_error() {
let err = run_str_err(
"f en:t>n;@i 0..en{i}",
Some("f"),
vec![Value::Text(Arc::new("b".to_string()))],
);
assert!(
err.contains("range") || err.contains("number") || err.contains("end"),
"got: {err}"
);
}
#[test]
fn interpret_fnref_callee_from_scope() {
let source = "sq x:n>n;*x x f cb:z>n;cb 3";
let result = run_str(source, Some("f"), vec![Value::FnRef("sq".into())]);
assert_eq!(result, Value::Number(9.0));
}
#[test]
fn interpret_bang_on_non_result_passes_through() {
let source = "id x:n>z;x f>z;id! 42";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn interpret_typeis_pattern_non_basic_type_no_match() {
let source = "f x:z>b;?x{n _:true;_:false}";
let result = run_str(
source,
Some("f"),
vec![Value::Record {
type_name: "pt".into(),
fields: std::collections::HashMap::new(),
}],
);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn interpret_brk_inside_match_arm_propagates() {
let src = "f>n;@x [1,2,3]{?x{2:brk x;_:x};x}";
let result = run_str(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn interpret_text_callee_from_scope() {
let source = "sq x:n>n;*x x f cb:z>n;cb 3";
let result = run_str(
source,
Some("f"),
vec![Value::Text(Arc::new("sq".to_string()))],
);
assert_eq!(result, Value::Number(9.0));
}
#[test]
fn interpret_srt_bool_key_equal_ordering() {
let source = "pos x:n>b;> x 0 f>L n;srt pos [3,-1,2,-2]";
let result = run_str(source, Some("f"), vec![]);
let Value::List(items) = result else {
panic!("expected List, got {:?}", result)
};
assert_eq!(items.len(), 4);
}
#[test]
fn interpret_brk_inside_guard_body_propagates() {
let src = "f>n;@x [1,2,3,4]{>x 2{brk x};x}";
let result = run_str(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn interpret_cnt_inside_guard_body_propagates() {
let src = "f>n;@x [1,2,3]{=x 1{cnt};x}";
let result = run_str(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn interpret_brk_inside_ternary_body_propagates() {
let src = "f>n;@x [1,2,3]{=x 2{brk x}{0};0}";
let result = run_str(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn interpret_cnt_inside_ternary_body_propagates() {
let src = "f>n;@x [1,2,3]{=x 1{cnt}{0};x}";
let result = run_str(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn interpret_cnt_in_match_expr_arm_returns_nil() {
let src = "f>n;@x [1,2,3]{r=?x{1:cnt;_:x};r}";
let result = run_str(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn interpret_continue_in_function_body_returns_nil() {
let result = run_str("f>_;cnt", Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
fn interpret_mod_normal() {
let result = run_str(
"f a:n b:n>n;mod a b",
Some("f"),
vec![Value::Number(10.0), Value::Number(3.0)],
);
assert_eq!(result, Value::Number(1.0));
}
#[test]
fn interpret_mod_by_zero() {
let prog = parse_program("f a:n b:n>n;mod a b");
let err = run(
&prog,
Some("f"),
vec![Value::Number(10.0), Value::Number(0.0)],
)
.unwrap_err();
assert!(err.to_string().contains("modulo by zero"), "got: {err}");
}
#[test]
fn interpret_mod_non_numbers() {
let prog = parse_program(r#"f a:t b:t>_;mod a b"#);
let err = run(
&prog,
Some("f"),
vec![
Value::Text(Arc::new("a".to_string())),
Value::Text(Arc::new("b".to_string())),
],
)
.unwrap_err();
assert!(
err.to_string().contains("mod requires two numbers"),
"got: {err}"
);
}
#[test]
fn interpret_round() {
let result = run_str("f x:n>n;rou x", Some("f"), vec![Value::Number(3.7)]);
assert_eq!(result, Value::Number(4.0));
let result2 = run_str("f x:n>n;rou x", Some("f"), vec![Value::Number(3.2)]);
assert_eq!(result2, Value::Number(3.0));
}
#[test]
fn interpret_ternary_then() {
let result = run_str("f x:n>n;?=x 0 10 20", Some("f"), vec![Value::Number(0.0)]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn interpret_ternary_else() {
let result = run_str("f x:n>n;?=x 0 10 20", Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(20.0));
}
#[test]
fn interpret_literal_nil() {
let result = run_str("f>O n;nil", Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
fn interpret_type_is_no_match() {
let result = run_str(
r#"f x:n>t;?x{t v:"text";_:"other"}"#,
Some("f"),
vec![Value::Number(42.0)],
);
assert_eq!(result, Value::Text(Arc::new("other".to_string())));
}
#[test]
fn interpret_tool_call_with_provider_no_runtime() {
let source = r#"tool greet"say hello" name:t>R _ t timeout:5"#;
let prog = parse_program(source);
let provider = std::sync::Arc::new(crate::tools::StubProvider);
let result = run_with_tools(
&prog,
Some("greet"),
vec![Value::Text(Arc::new("world".to_string()))],
provider,
)
.unwrap();
assert_eq!(result, Value::Ok(Box::new(Value::Nil)));
}
#[test]
fn interp_rnd_valid_bounds() {
let result = run_str("f>n;rnd 1 10", None, vec![]);
match result {
Value::Number(n) => assert!((1.0..=10.0).contains(&n)),
_ => panic!("expected number"),
}
}
#[test]
fn interp_type_is_pattern_number() {
let result = run_str(
r#"f x:n>t;?x{n v:"num";_:"other"}"#,
None,
vec![Value::Number(5.0)],
);
assert_eq!(result, Value::Text(Arc::new("num".to_string())));
}
#[test]
fn interp_type_is_pattern_text() {
let result = run_str(
r#"f x:t>t;?x{t v:v;_:"other"}"#,
None,
vec![Value::Text(Arc::new("hi".to_string()))],
);
assert_eq!(result, Value::Text(Arc::new("hi".to_string())));
}
#[test]
fn interp_type_is_pattern_bool() {
let result = run_str(
r#"f x:b>t;?x{b v:"matched";_:"other"}"#,
None,
vec![Value::Bool(true)],
);
assert_eq!(result, Value::Text(Arc::new("matched".to_string())));
}
#[test]
fn interp_type_is_list_match_with_binding() {
let source = r#"f x:L n>t;?x{l v:"list";_:"other"}"#;
let result = run_str(
source,
Some("f"),
vec![Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(2.0),
]))],
);
assert_eq!(result, Value::Text(Arc::new("list".to_string())));
}
#[test]
fn interp_type_is_list_no_match() {
let source = r#"f x:n>t;?x{l _:"list";_:"other"}"#;
let result = run_str(source, Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Text(Arc::new("other".to_string())));
}
#[test]
fn interp_type_is_map_falls_through() {
let source = r#"f x:M t n>t;?x{n _:"num";_:"other"}"#;
let result = run_str(
source,
Some("f"),
vec![Value::Map(Arc::new(std::collections::HashMap::from([(
MapKey::Text("a".to_string()),
Value::Number(1.0),
)])))],
);
assert_eq!(result, Value::Text(Arc::new("other".to_string())));
}
#[test]
fn interp_type_is_nil_falls_through() {
let source = r#"f x:O n>t;?x{n _:"num";_:"nil"}"#;
let result = run_str(source, Some("f"), vec![Value::Nil]);
assert_eq!(result, Value::Text(Arc::new("nil".to_string())));
}
#[test]
fn interp_type_is_nil_value_against_text() {
let source = r#"f x:O t>t;?x{t v:v;_:"none"}"#;
let result = run_str(source, Some("f"), vec![Value::Nil]);
assert_eq!(result, Value::Text(Arc::new("none".to_string())));
}
#[test]
fn interp_mget_bang_missing_propagates_nil() {
let source = r#"f>O n;m=mmap;v=mget! m "missing";+v 99"#;
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
fn interp_mget_bang_present_returns_inner() {
let source = r#"f>O n;m=mset mmap "k" 5;v=mget! m "k";v"#;
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn interp_at_list_negative_last() {
let source = "f>n;xs=[10,20,30];at xs -1";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(30.0));
}
#[test]
fn interp_at_list_negative_first() {
let source = "f>n;xs=[10,20,30];at xs -3";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn interp_at_text_negative_last() {
let source = r#"f>t;at "abc" -1"#;
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Text(Arc::new("c".to_string())));
}
#[test]
fn interp_at_list_negative_out_of_range() {
let prog = parse_program("f>n;xs=[10,20,30];at xs -4");
let err = run(&prog, Some("f"), vec![]).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("out of range"), "got {msg}");
}
#[test]
fn interp_at_text_negative_out_of_range() {
let prog = parse_program(r#"f>t;at "ab" -3"#);
let err = run(&prog, Some("f"), vec![]).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("out of range"), "got {msg}");
}
#[test]
fn interp_at_fractional_index_floors() {
let prog = parse_program("f>n;xs=[10,20,30];at xs 1.5");
let v = run(&prog, Some("f"), vec![]).unwrap();
assert_eq!(v, Value::Number(20.0));
}
#[test]
fn interp_at_fractional_negative_index_floors() {
let prog = parse_program("f>n;xs=[10,20,30];at xs -0.5");
let v = run(&prog, Some("f"), vec![]).unwrap();
assert_eq!(v, Value::Number(30.0));
}
#[test]
fn interp_at_non_numeric_index_errors() {
let prog = parse_program("f>n;xs=[10,20,30];at xs \"a\"");
let err = run(&prog, Some("f"), vec![]).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("number") || msg.contains("at"), "got {msg}");
}
#[test]
fn interp_lst_happy() {
let source = "f>L n;lst [10,20,30] 1 99";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Number(10.0),
Value::Number(99.0),
Value::Number(30.0),
]))
);
}
#[test]
fn interp_lst_first_index() {
let source = "f>L n;lst [10,20,30] 0 7";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Number(7.0),
Value::Number(20.0),
Value::Number(30.0),
]))
);
}
#[test]
fn interp_lst_out_of_range_errors() {
let prog = parse_program("f>L n;lst [1,2,3] 5 0");
let err = run(&prog, Some("f"), vec![]).unwrap_err();
assert!(format!("{err:?}").contains("out of range"));
}
#[test]
fn interp_lst_negative_index_errors() {
let prog = parse_program("f>L n;lst [1,2,3] -1 0");
let err = run(&prog, Some("f"), vec![]).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("non-negative integer"), "got {msg}");
}
#[test]
fn interp_lst_fractional_index_errors() {
let prog = parse_program("f>L n;lst [1,2,3] 1.5 0");
let err = run(&prog, Some("f"), vec![]).unwrap_err();
assert!(format!("{err:?}").contains("non-negative integer"));
}
#[test]
fn interp_fmt2_rejects_non_number_args() {
let mut env = Env::new();
let result = call_function(
&mut env,
"fmt2",
vec![Value::Text(Arc::new("hi".to_string())), Value::Number(2.0)],
);
let err = result.unwrap_err();
assert_eq!(err.code, "ILO-R009");
assert!(
err.message.contains("fmt2 requires two numbers"),
"got: {}",
err.message
);
}
#[test]
fn box_muller_sigma_zero_returns_mu() {
assert_eq!(box_muller_normal(5.0, 0.0), 5.0);
assert_eq!(box_muller_normal(-1.25, 0.0), -1.25);
assert_eq!(box_muller_normal(0.0, 0.0), 0.0);
}
#[test]
fn box_muller_finite_for_nonzero_sigma() {
crate::rng::seed(42);
for _ in 0..200 {
let v = box_muller_normal(0.0, 1.0);
assert!(v.is_finite(), "got non-finite {v}");
}
}
#[test]
fn interp_rndn_sigma_zero_returns_mu() {
let source = "f>n;rndn 7 0";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(7.0));
}
#[test]
fn interp_rndn_negative_mu_sigma_zero() {
let source = "f>n;rndn -3 0";
let result = run_str(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(-3.0));
}
#[test]
fn dur_parse_basic_abbreviations() {
assert_eq!(super::dur_parse("3h 30m"), Ok(12_600.0));
assert_eq!(super::dur_parse("1d"), Ok(86_400.0));
assert_eq!(super::dur_parse("2w"), Ok(1_209_600.0));
}
#[test]
fn dur_parse_full_unit_names() {
assert_eq!(super::dur_parse("1 week 2 days"), Ok(777_600.0));
assert_eq!(super::dur_parse("1 hour"), Ok(3_600.0));
assert_eq!(super::dur_parse("30 seconds"), Ok(30.0));
assert_eq!(super::dur_parse("5 minutes"), Ok(300.0));
}
#[test]
fn dur_parse_decimal_quantity() {
assert_eq!(super::dur_parse("1.5 hours"), Ok(5_400.0));
assert_eq!(super::dur_parse("0.5s"), Ok(0.5));
}
#[test]
fn dur_parse_negative_first_token_is_sticky() {
assert_eq!(super::dur_parse("-1m 30s"), Ok(-90.0));
assert_eq!(super::dur_parse("-1h 30m"), Ok(-5_400.0));
}
#[test]
fn dur_parse_explicit_sign_resets_sticky() {
assert_eq!(super::dur_parse("-1m +30s"), Ok(-30.0));
assert_eq!(super::dur_parse("+1h -10m"), Ok(3_000.0));
}
#[test]
fn dur_parse_months_rejected() {
assert!(super::dur_parse("3mo").is_err());
assert!(super::dur_parse("3 months").is_err());
assert!(super::dur_parse("3 month").is_err());
}
#[test]
fn dur_parse_unknown_unit_skipped() {
assert!(super::dur_parse("3xyz").is_err());
assert_eq!(super::dur_parse("3xyz 5s"), Ok(5.0));
}
#[test]
fn dur_parse_empty_and_whitespace() {
assert!(super::dur_parse("").is_err());
assert!(super::dur_parse(" ").is_err());
}
#[test]
fn dur_parse_no_recognised_unit() {
assert!(super::dur_parse("hello").is_err());
assert!(super::dur_parse("42").is_err());
}
#[test]
fn dur_fmt_basic() {
assert_eq!(super::dur_fmt(0.0), "0s");
assert_eq!(super::dur_fmt(90.0), "1m 30s");
assert_eq!(super::dur_fmt(9_720.0), "2h 42m");
assert_eq!(super::dur_fmt(86_400.0), "1 day");
assert_eq!(super::dur_fmt(604_800.0), "1 week");
}
#[test]
fn dur_fmt_preserves_fractional_seconds() {
assert_eq!(super::dur_fmt(0.5), "0.5s");
assert_eq!(super::dur_fmt(90.5), "1m 30.5s");
assert_eq!(super::dur_fmt(1.75), "1.75s");
}
#[test]
fn dur_fmt_negative_round_trips() {
assert_eq!(super::dur_fmt(-90.0), "-1m 30s");
assert_eq!(super::dur_parse(&super::dur_fmt(-90.0)), Ok(-90.0));
assert_eq!(super::dur_parse(&super::dur_fmt(-5_400.0)), Ok(-5_400.0));
}
#[test]
fn dur_fmt_non_finite_passthrough() {
assert_eq!(super::dur_fmt(f64::INFINITY), "inf");
assert_eq!(super::dur_fmt(f64::NEG_INFINITY), "-inf");
assert_eq!(super::dur_fmt(f64::NAN), "NaN");
}
#[test]
fn dur_round_trip_examples() {
for &secs in &[
0.0_f64, 1.0, 30.0, 90.0, 3_600.0, 9_720.0, 86_400.0, 604_800.0,
] {
let s = super::dur_fmt(secs);
assert_eq!(
super::dur_parse(&s),
Ok(secs),
"round-trip failed for {secs}"
);
}
}
#[test]
fn b64url_no_pad_empty() {
assert_eq!(super::b64url_no_pad_encode(b""), "");
}
#[test]
fn b64url_no_pad_one_byte() {
assert_eq!(super::b64url_no_pad_encode(b"f"), "Zg");
}
#[test]
fn b64url_no_pad_two_bytes() {
assert_eq!(super::b64url_no_pad_encode(b"fo"), "Zm8");
}
#[test]
fn b64url_no_pad_three_bytes() {
assert_eq!(super::b64url_no_pad_encode(b"foo"), "Zm9v");
}
#[test]
fn b64url_no_pad_canonical_hello() {
assert_eq!(super::b64url_no_pad_encode(b"hello"), "aGVsbG8");
}
#[test]
fn b64url_no_pad_url_safe_chars() {
assert_eq!(super::b64url_no_pad_encode(&[0xfb, 0xff, 0xbf]), "-_-_");
}
#[test]
fn b64url_no_pad_length_formula() {
for n in 0..=64 {
let bytes = vec![0xa5u8; n];
let encoded = super::b64url_no_pad_encode(&bytes);
let expected = if n == 0 {
0
} else {
let pad = match n % 3 {
1 => 2,
2 => 1,
_ => 0,
};
n.div_ceil(3) * 4 - pad
};
assert_eq!(encoded.len(), expected, "n={n}: encoded={encoded:?}");
}
}
#[test]
fn rand_bytes_negative_returns_err() {
let r = super::eval_rand_bytes(&Value::Number(-1.0));
assert!(matches!(&r, Err(e) if e.code == "ILO-R009"), "got {r:?}");
}
#[test]
fn rand_bytes_non_finite_returns_err() {
for n in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let r = super::eval_rand_bytes(&Value::Number(n));
assert!(
matches!(&r, Err(e) if e.code == "ILO-R009"),
"n={n} got {r:?}"
);
}
}
#[test]
fn rand_bytes_over_cap_returns_err() {
let r = super::eval_rand_bytes(&Value::Number(2.0 * 1024.0 * 1024.0));
assert!(matches!(&r, Err(e) if e.code == "ILO-R009"), "got {r:?}");
}
#[test]
fn rand_bytes_zero_returns_empty_text() {
let r = super::eval_rand_bytes(&Value::Number(0.0)).expect("Ok");
match r {
Value::Text(s) => assert_eq!(s.as_str(), ""),
other => panic!("expected Text(\"\"), got {other:?}"),
}
}
#[test]
fn rand_bytes_16_returns_22_char_text() {
let r = super::eval_rand_bytes(&Value::Number(16.0)).expect("Ok");
match r {
Value::Text(s) => {
assert_eq!(s.len(), 22, "got {s:?}");
assert!(
s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
"non-b64url char in {s:?}"
);
}
other => panic!("expected Text, got {other:?}"),
}
}
#[test]
#[cfg(not(target_family = "wasm"))]
fn run2_echo_stdout_is_record() {
let src = r#"f>_;r=run2!! "echo" ["hello"];r"#;
let v = run_str(src, Some("f"), vec![]);
match v {
Value::Record {
ref type_name,
ref fields,
} => {
assert_eq!(type_name, "RunResult");
assert_eq!(
fields.get("stdout"),
Some(&Value::Text(Arc::new("hello\n".to_string())))
);
assert_eq!(
fields.get("stderr"),
Some(&Value::Text(Arc::new(String::new())))
);
assert_eq!(fields.get("exit"), Some(&Value::Number(0.0)));
}
other => panic!("expected RunResult record, got {:?}", other),
}
}
#[test]
#[cfg(not(target_family = "wasm"))]
fn run2_false_exit_nonzero() {
let src = r#"f>n;r=run2!! "false" [];r.exit"#;
assert_eq!(run_str(src, Some("f"), vec![]), Value::Number(1.0));
}
#[test]
#[cfg(not(target_family = "wasm"))]
fn run2_true_exit_zero() {
let src = r#"f>n;r=run2!! "true" [];r.exit"#;
assert_eq!(run_str(src, Some("f"), vec![]), Value::Number(0.0));
}
#[test]
#[cfg(not(target_family = "wasm"))]
fn run2_nonexistent_is_err() {
let src = r#"f>b;r=run2 "no-such-command-xyz-run2" [];?r{~v:false;^e:true}"#;
assert_eq!(run_str(src, Some("f"), vec![]), Value::Bool(true));
}
#[test]
#[cfg(not(target_family = "wasm"))]
fn run2_stderr_captured() {
let src = r#"f>n;r=run2!! "echo" ["hello"];len r.stdout"#;
let v = run_str(src, Some("f"), vec![]);
assert_eq!(v, Value::Number(6.0));
}
#[test]
#[cfg(not(target_family = "wasm"))]
fn run2_exit_is_number_not_text() {
let src = r#"f>b;r=run2!! "true" [];?r.exit{0:true;_:false}"#;
assert_eq!(run_str(src, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn sum_type_payload_less_variant_returns_variant_value() {
let src = r#"type color = red | green | blue
f>t;c=red;?c{red:"r";green:"g";blue:"b"}"#;
assert_eq!(
run_str(src, Some("f"), vec![]),
Value::Text(Arc::new("r".to_string()))
);
}
#[test]
fn sum_type_payload_variant_carries_value() {
let src = r#"type shape = circle(n) | point
f>n;s=circle 5;?s{circle(r):r;point:0}"#;
assert_eq!(run_str(src, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn sum_type_wildcard_arm_catches_remaining() {
let src = r#"type shape = circle(n) | square(n) | point
f>t;s=point;?s{circle(r):"c";_:"other"}"#;
assert_eq!(
run_str(src, Some("f"), vec![]),
Value::Text(Arc::new("other".to_string()))
);
}
#[test]
fn sum_type_multiple_payload_variants_inline() {
let src = r#"type shape = circle(n) | square(n) | point
area s:shape>n;?s{circle(r):*3 r;square(side):*side side;point:0}
f>n;+area(circle 2) area(square 3)"#;
assert_eq!(run_str(src, Some("f"), vec![]), Value::Number(6.0 + 9.0));
}
#[test]
fn par_map_applies_fn_to_each_element_in_order() {
let src = r#"dbl x:n>n;*x 2 main>L n;xs=[1 2 3];ys=par-map dbl xs 2;map (y:_>n;?y{~v:v;^_:0}) ys"#;
let result = run_str(src, Some("main"), vec![]);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Number(2.0),
Value::Number(4.0),
Value::Number(6.0),
]))
);
}
#[test]
fn par_map_empty_list_returns_empty() {
let src = r#"dbl x:n>n;*x 2 main>L n;par-map dbl [] 4"#;
let result = run_str(src, Some("main"), vec![]);
assert_eq!(result, Value::List(Arc::new(vec![])));
}
#[test]
fn par_map_default_concurrency_two_arg_form() {
let src =
r#"sq x:n>n;*x x main>L n;xs=[1 2 3 4];ys=par-map sq xs;map (y:_>n;?y{~v:v;^_:0}) ys"#;
let result = run_str(src, Some("main"), vec![]);
assert_eq!(
result,
Value::List(Arc::new(vec![
Value::Number(1.0),
Value::Number(4.0),
Value::Number(9.0),
Value::Number(16.0),
]))
);
}
#[test]
fn par_map_large_list_chunking() {
let src = r#"dbl x:n>n;*x 2 main>L n;xs=range 0 100;ys=par-map dbl xs;map (y:_>n;?y{~v:v;^_:0}) ys"#;
let result = run_str(src, Some("main"), vec![]);
if let Value::List(list) = result {
assert_eq!(list.len(), 100);
for (i, v) in list.iter().enumerate() {
assert_eq!(*v, Value::Number((i * 2) as f64), "mismatch at index {i}");
}
} else {
panic!("expected a list");
}
}
#[test]
fn par_map_error_cancels_remaining_workers() {
let src = r#"boom x:n>n;=x 5{at [] 0};x main>L n;xs=range 0 10;par-map boom xs 1"#;
let result = run_str(src, Some("main"), vec![]);
if let Value::List(list) = result {
assert_eq!(list.len(), 10);
for i in 0..5 {
assert!(
matches!(&list[i], Value::Ok(_)),
"expected Ok at index {i}, got {:?}",
list[i]
);
}
assert!(
matches!(&list[5], Value::Err(_)),
"expected Err at index 5, got {:?}",
list[5]
);
for i in 6..10 {
assert!(
matches!(&list[i], Value::Err(_)),
"expected cancelled Err at index {i}, got {:?}",
list[i]
);
}
} else {
panic!("expected a list");
}
}
#[test]
fn par_map_chunk_size_formula() {
use super::par_map_chunk_size;
assert_eq!(par_map_chunk_size(100, 4), 25);
assert_eq!(par_map_chunk_size(101, 4), 26); assert_eq!(par_map_chunk_size(1, 8), 1);
assert_eq!(par_map_chunk_size(0, 4), 0);
assert_eq!(par_map_chunk_size(10, 0), 10); }
}