use crate::ast::{BinOp, CfgCondition, Expr, Stmt, UnOp, Visibility};
use crate::builtins;
use crate::env::Env;
use crate::value::{AsyncLambda, Future, Lambda, Value};
use anyhow::Result;
use std::collections::BTreeMap;
fn eval_cfg_condition(condition: &CfgCondition) -> Result<bool> {
match condition {
CfgCondition::Platform(platform) => {
let current_os = std::env::consts::OS;
Ok(match platform.as_str() {
"windows" => current_os == "windows",
"linux" => current_os == "linux",
"macos" => current_os == "macos",
"unix" => current_os != "windows",
other => current_os == other,
})
}
CfgCondition::Feature(feature) => {
let features = std::env::var("IRONSHELL_FEATURES").unwrap_or_default();
Ok(features.split(',').any(|f| f.trim() == feature))
}
CfgCondition::Not(inner) => Ok(!eval_cfg_condition(inner)?),
CfgCondition::All(conditions) => {
for cond in conditions {
if !eval_cfg_condition(cond)? {
return Ok(false);
}
}
Ok(true)
}
CfgCondition::Any(conditions) => {
for cond in conditions {
if eval_cfg_condition(cond)? {
return Ok(true);
}
}
Ok(false)
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Site {
pub line: usize,
pub file: Option<String>,
}
thread_local! {
static STATEMENT: std::cell::RefCell<Option<Site>> = const { std::cell::RefCell::new(None) };
static LAMBDA_SITES: std::cell::RefCell<BTreeMap<String, Site>> =
const { std::cell::RefCell::new(BTreeMap::new()) };
static FAILED_IN: std::cell::RefCell<Option<(String, Site)>> =
const { std::cell::RefCell::new(None) };
}
pub fn set_statement_site(site: Option<Site>) -> Option<Site> {
FAILED_IN.with(|f| *f.borrow_mut() = None);
STATEMENT.with(|s| std::mem::replace(&mut *s.borrow_mut(), site))
}
pub fn clear_failed_lambda() {
FAILED_IN.with(|f| *f.borrow_mut() = None);
}
pub fn take_failed_lambda_note() -> Option<String> {
let (name, site) = FAILED_IN.with(|f| f.borrow_mut().take())?;
Some(match site.file {
None => format!("(in `{name}`, defined at line {})", site.line),
Some(file) => format!("(in `{name}`, defined at {file} line {})", site.line),
})
}
fn note_lambda_binding(stmt: &Stmt) {
let Stmt::Let {
name,
value: Expr::Lambda { .. },
..
} = stmt
else {
return;
};
if let Some(site) = STATEMENT.with(|s| s.borrow().clone()) {
LAMBDA_SITES.with(|m| m.borrow_mut().insert(name.clone(), site));
}
}
fn note_failed_lambda_call(name: &str) {
let Some(site) = LAMBDA_SITES.with(|m| m.borrow().get(name).cloned()) else {
return;
};
let Some(here) = STATEMENT.with(|s| s.borrow().clone()) else {
return;
};
if site == here {
return;
}
FAILED_IN.with(|f| {
let mut f = f.borrow_mut();
if f.is_none() {
*f = Some((name.to_string(), site));
}
});
}
pub fn eval_program(stmts: &[Stmt], env: &mut Env) -> Result<Value> {
let mut last = Value::Null;
for (i, s) in stmts.iter().enumerate() {
last = eval_program_statement(s, i + 1 < stmts.len(), env)?;
}
Ok(last)
}
pub fn eval_program_statement(s: &Stmt, discard: bool, env: &mut Env) -> Result<Value> {
env.set_input(None);
let value = match s {
Stmt::Let {
name,
value,
is_mut,
visibility,
ty,
bare,
} if discard => eval_let(
name,
value,
*is_mut,
*visibility,
ty.as_deref(),
*bare,
env,
false,
)?,
_ => eval_stmt(s, env)?,
};
note_lambda_binding(s);
if let (Stmt::Expr(e), Value::Error(msg)) = (s, &value) {
if !matches!(e, Expr::Ident(_) | Expr::MemberAccess { .. }) {
return Err(anyhow::anyhow!("uncaught error: {msg}"));
}
}
Ok(value)
}
pub fn eval_stream(code: &str, env: &mut Env, on_item: &mut dyn FnMut(Value)) -> Result<usize> {
eval_stream_with_stats(code, env, on_item).map(|s| s.emitted)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StreamStats {
pub emitted: usize,
pub pulled: usize,
pub streamed: bool,
pub short_circuited: bool,
pub barrier_tail: bool,
}
pub fn eval_stream_with_stats(
code: &str,
env: &mut Env,
on_item: &mut dyn FnMut(Value),
) -> Result<StreamStats> {
let stmts = crate::parser::parse_program(code)?;
let (last, init) = match stmts.split_last() {
Some(parts) => parts,
None => return Ok(StreamStats::default()),
};
for s in init {
env.set_input(None);
match s {
Stmt::Let {
name,
value,
is_mut,
visibility,
ty,
bare,
} => {
eval_let(
name,
value,
*is_mut,
*visibility,
ty.as_deref(),
*bare,
env,
false,
)?;
}
_ => {
eval_stmt(s, env)?;
}
}
}
env.set_input(None);
if let Stmt::Expr(expr) = last {
if let Some(stats) = try_stream_pipeline(expr, env, on_item)? {
return Ok(stats);
}
}
let v = eval_stmt(last, env)?;
let emitted = emit_value(v, on_item);
Ok(StreamStats {
emitted,
pulled: emitted,
streamed: false,
short_circuited: false,
barrier_tail: false,
})
}
fn emit_value(v: Value, on_item: &mut dyn FnMut(Value)) -> usize {
match v {
Value::Array(items) => {
let n = items.len();
for it in items {
on_item(it);
}
n
}
Value::Null => 0,
other => {
on_item(other);
1
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StageKind {
Elementwise,
Prefix(i64),
Barrier,
}
fn stage_kind(e: &Expr) -> StageKind {
let Expr::Call { callee, args, .. } = e else {
return StageKind::Barrier;
};
let Expr::Ident(name) = &**callee else {
return StageKind::Barrier;
};
match name.as_str() {
"map" | "where" | "filter" => StageKind::Elementwise,
"take" => match args.first() {
Some(Expr::LitInt(n)) if *n >= 0 => StageKind::Prefix(*n),
_ => StageKind::Barrier,
},
_ => StageKind::Barrier,
}
}
fn is_effect_free(e: &Expr) -> bool {
use crate::safety::{effect_of, Effect};
match e {
Expr::LitInt(_)
| Expr::LitFloat(_)
| Expr::LitStr(_)
| Expr::LitBool(_)
| Expr::Null
| Expr::Ident(_) => true,
Expr::Array(items) => items.iter().all(is_effect_free),
Expr::Record(fields) => fields.iter().all(|(_, v)| is_effect_free(v)),
Expr::Lambda { body, .. } => is_effect_free(body),
Expr::Binary { left, right, .. } => is_effect_free(left) && is_effect_free(right),
Expr::Unary { expr, .. } => is_effect_free(expr),
Expr::MemberAccess { object, .. } => is_effect_free(object),
Expr::Index { object, index } => is_effect_free(object) && is_effect_free(index),
Expr::Pipe { left, right } => is_effect_free(left) && is_effect_free(right),
Expr::Call {
callee,
args,
named,
} => {
let callee_pure = match &**callee {
Expr::Ident(name) => {
crate::builtins::is_dispatched(name) && effect_of(name) == Effect::Pure
}
_ => false,
};
callee_pure
&& args.iter().all(is_effect_free)
&& named.iter().all(|(_, v)| is_effect_free(v))
}
_ => false,
}
}
fn apply_stages(mut batch: Value, stages: &[&Expr], env: &mut Env) -> Result<Value> {
for stage in stages {
let saved = env.take_input();
env.set_input(Some(batch));
let res = eval_expr(stage, env);
match saved {
Some(v) => env.set_input(Some(v)),
None => env.set_input(None),
}
batch = res?;
}
Ok(batch)
}
fn try_stream_pipeline(
expr: &Expr,
env: &mut Env,
on_item: &mut dyn FnMut(Value),
) -> Result<Option<StreamStats>> {
let mut stages: Vec<&Expr> = Vec::new();
let mut cur = expr;
while let Expr::Pipe { left, right } = cur {
stages.push(right);
cur = left;
}
if stages.is_empty() {
return Ok(None); }
stages.reverse();
let all_kinds: Vec<StageKind> = stages.iter().map(|s| stage_kind(s)).collect();
let tail_at = all_kinds
.iter()
.position(|k| *k == StageKind::Barrier)
.unwrap_or(stages.len());
if tail_at == 0 {
return Ok(None);
}
let tail: Vec<&Expr> = stages[tail_at..].to_vec();
let stages: Vec<&Expr> = stages[..tail_at].to_vec();
let kinds: Vec<StageKind> = all_kinds[..tail_at].to_vec();
let src = eval_expr(cur, env)?;
let items = match src {
Value::Array(items) => items,
other => {
let every: Vec<&Expr> = stages.iter().chain(tail.iter()).copied().collect();
let out = apply_stages(other, &every, env)?;
let emitted = emit_value(out, on_item);
return Ok(Some(StreamStats {
emitted,
pulled: 1,
streamed: false,
short_circuited: false,
barrier_tail: !tail.is_empty(),
}));
}
};
let may_abandon: Vec<bool> = kinds
.iter()
.enumerate()
.map(|(i, k)| {
matches!(k, StageKind::Prefix(_)) && stages[..i].iter().all(|s| is_effect_free(s))
})
.collect();
let mut taken = vec![0i64; stages.len()];
let mut stats = StreamStats {
streamed: true,
barrier_tail: !tail.is_empty(),
..Default::default()
};
let mut buffered: Vec<Value> = Vec::new();
for x in items {
let satisfied = kinds
.iter()
.enumerate()
.any(|(i, k)| may_abandon[i] && matches!(k, StageKind::Prefix(n) if taken[i] >= *n));
if satisfied {
stats.short_circuited = true;
break;
}
stats.pulled += 1;
let mut batch = Value::Array(vec![x]);
for (i, stage) in stages.iter().enumerate() {
if let StageKind::Prefix(n) = kinds[i] {
let width = match &batch {
Value::Array(items) => items.len() as i64,
Value::Null => 0,
_ => 1,
};
let room = (n - taken[i]).max(0);
if room == 0 {
batch = Value::Array(Vec::new());
break;
}
taken[i] += width.min(room);
if width > room {
if let Value::Array(items) = batch {
batch = Value::Array(items.into_iter().take(room as usize).collect());
}
}
continue;
}
let saved = env.take_input();
env.set_input(Some(batch));
let res = eval_expr(stage, env);
match saved {
Some(v) => env.set_input(Some(v)),
None => env.set_input(None),
}
batch = res?;
}
let mut deliver = |v: Value| {
if tail.is_empty() {
on_item(v);
stats.emitted += 1;
} else {
buffered.push(v);
}
};
match batch {
Value::Array(out) => {
for it in out {
deliver(it);
}
}
Value::Null => {}
other => deliver(other),
}
}
if !tail.is_empty() {
let out = apply_stages(Value::Array(buffered), &tail, env)?;
stats.emitted += emit_value(out, on_item);
}
Ok(Some(stats))
}
pub fn eval_stmt(stmt: &Stmt, env: &mut Env) -> Result<Value> {
match stmt {
Stmt::Let {
name,
value,
is_mut,
visibility,
ty,
bare,
} => eval_let(
name,
value,
*is_mut,
*visibility,
ty.as_deref(),
*bare,
env,
true,
),
Stmt::Expr(e) => eval_expr(e, env),
Stmt::SetField { name, path, value } => {
let v = eval_expr(value, env)?;
let mut updated = match env.get_var(name) {
Some(root) => root.clone(),
None => return Err(unknown_name_error(name, env)),
};
set_field_path(&mut updated, name, path, v.clone())?;
env.set_var(name, updated).map_err(|e| {
crate::safety::bad_state_with_hint("assign", &e, &env.rebind_hint(name))
})?;
Ok(v)
}
Stmt::Import {
items,
source,
alias,
} => {
crate::module_loader::import(items, source, alias.as_deref(), env)?;
Ok(Value::Null)
}
Stmt::Export { items, from_source } => {
match from_source {
Some(source) => crate::module_loader::reexport(items, source, env)?,
None => crate::module_loader::export_local(items, env)?,
}
Ok(Value::Null)
}
Stmt::Cfg { condition, body } => {
if eval_cfg_condition(condition)? {
eval_stmt(body, env)
} else {
Ok(Value::Null)
}
}
}
}
fn eval_let(
name: &str,
value: &Expr,
is_mut: bool,
visibility: Visibility,
ty: Option<&str>,
bare: bool,
env: &mut Env,
keep: bool,
) -> Result<Value> {
if let Some(right) = in_place_append_operand(name, value, ty, visibility, env) {
let operand = eval_expr(right, env)?;
let operand = match append_in_place(env, name, operand) {
Ok(()) => {
let kept = if keep {
env.get_var(name).cloned().unwrap_or(Value::Null)
} else {
Value::Null
};
return Ok(kept);
}
Err(operand) => operand,
};
let lhs = env.get_var(name).cloned().unwrap_or(Value::Null);
let v = binop(&BinOp::Add, lhs, operand)?;
return bind_value(name, v, is_mut, visibility, bare, env, keep);
}
let v = eval_expr(value, env)?;
if let Some(declared) = ty {
check_declared_type(name, declared, &v)?;
}
bind_value(name, v, is_mut, visibility, bare, env, keep)
}
fn bind_value(
name: &str,
v: Value,
is_mut: bool,
visibility: Visibility,
bare: bool,
env: &mut Env,
keep: bool,
) -> Result<Value> {
let kept = keep.then(|| v.clone());
env.declare_var(name, v, is_mut).map_err(|e| {
if bare {
crate::safety::bad_state_with_hint("assign", &e, &env.reassign_hint(name))
} else {
crate::safety::bad_state_with_hint("let", &e, &env.rebind_hint(name))
}
})?;
if visibility == Visibility::Pub {
env.set_public(name);
}
Ok(kept.unwrap_or(Value::Null))
}
fn in_place_append_operand<'a>(
name: &str,
value: &'a Expr,
ty: Option<&str>,
visibility: Visibility,
env: &Env,
) -> Option<&'a Expr> {
let Expr::Binary {
left,
op: BinOp::Add,
right,
} = value
else {
return None;
};
let Expr::Ident(lhs) = &**left else {
return None;
};
let fits = lhs.as_str() == name
&& ty.is_none()
&& visibility == Visibility::Private
&& env.is_mutable(name)
&& matches!(env.get_var(name), Some(Value::Str(_) | Value::Array(_)));
fits.then_some(&**right)
}
pub(crate) fn eval_body_consuming_params(
body: &Expr,
params: &[String],
env: &mut Env,
) -> Result<Value> {
if let Expr::Binary {
left,
op: BinOp::Add,
right,
} = body
{
if let Expr::Ident(name) = left.as_ref() {
if params.iter().any(|p| p == name) {
let mut mentioned = std::collections::BTreeSet::new();
free_idents(right, &mut Vec::new(), &mut mentioned);
if !mentioned.contains(name.as_str()) {
let rhs = eval_expr(right, env)?;
match (env.get_var_mut(name), rhs) {
(Some(Value::Str(a)), Value::Str(b))
if a.len() + b.len() <= MAX_STRING_BYTES =>
{
let mut out = std::mem::take(a);
out.push_str(&b);
return Ok(Value::Str(out));
}
(Some(Value::Array(a)), Value::Array(b))
if a.len().saturating_add(b.len()) <= MAX_ARRAY_ELEMENTS =>
{
let mut out = std::mem::take(a);
out.extend(b);
return Ok(Value::Array(out));
}
(Some(a), rhs) => return binop(&BinOp::Add, a.clone(), rhs),
(None, _) => {}
}
}
}
}
}
eval_expr(body, env)
}
fn append_in_place(env: &mut Env, name: &str, operand: Value) -> std::result::Result<(), Value> {
match (env.get_var_mut(name), operand) {
(Some(Value::Str(a)), Value::Str(b)) if a.len() + b.len() <= MAX_STRING_BYTES => {
a.push_str(&b);
Ok(())
}
(Some(Value::Array(a)), Value::Array(b))
if a.len().saturating_add(b.len()) <= MAX_ARRAY_ELEMENTS =>
{
a.extend(b);
Ok(())
}
(_, operand) => Err(operand),
}
}
pub fn eval_expr(expr: &Expr, env: &mut Env) -> Result<Value> {
crate::safety::check_deadline()?;
match expr {
Expr::LitInt(n) => Ok(Value::Int(*n)),
Expr::LitFloat(f) => Ok(Value::Float(*f)),
Expr::LitStr(s) => {
if s.contains("${") {
interpolate_string(s, env)
} else {
Ok(Value::Str(s.replace(crate::parser::ESCAPED_DOLLAR, "$")))
}
}
Expr::LitBool(b) => Ok(Value::Bool(*b)),
Expr::Null => Ok(Value::Null),
Expr::Ident(name) => match env.get_var(name) {
Some(value) => Ok(value.clone()),
None if builtins::is_dispatched(name) => Ok(crate::modules::builtin_ref(name)),
None => Err(unknown_name_error(name, env)),
},
Expr::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for it in items {
out.push(eval_expr(it, env)?);
}
Ok(Value::Array(out))
}
Expr::Record(kvs) => {
let mut m = BTreeMap::new();
for (k, v) in kvs {
let key = if k.contains("${") {
match interpolate_string(k, env)? {
Value::Str(s) => s,
other => other.to_display_string(),
}
} else {
k.replace(crate::parser::ESCAPED_DOLLAR, "$")
};
m.insert(key, eval_expr(v, env)?);
}
Ok(Value::Record(m))
}
Expr::Lambda { params, body } => Ok(Value::Lambda(Lambda {
params: params.clone(),
body: body.clone(),
captured: capture_free_vars(params, body, env),
})),
Expr::AsyncLambda { params, body } => Ok(Value::AsyncLambda(AsyncLambda {
params: params.clone(),
body: body.clone(),
captured: capture_free_vars(params, body, env),
})),
Expr::Await(inner) => {
let val = eval_expr(inner, env)?;
match val {
Value::Future(future) => {
let _depth = crate::safety::enter_call()?;
let saved_pipe = env.take_input();
env.set_input(None);
let restore_caps = install_captured(&future.lambda.captured, env);
let mut saved: Vec<(String, Option<Value>)> = Vec::new();
for (param, arg) in future.lambda.params.iter().zip(future.args.iter()) {
saved.push((param.clone(), env.get_var(param).cloned()));
env.set_var_unchecked(param, arg.clone());
}
let out = eval_expr(&future.lambda.body, env);
for (name, old) in saved.into_iter().rev() {
match old {
Some(v) => env.set_var_unchecked(&name, v),
None => env.del_var(&name),
}
}
restore_captured(restore_caps, env);
env.set_input(saved_pipe);
out
}
other => Ok(other),
}
}
Expr::TryCatch {
try_expr,
catch_var,
catch_expr,
} => {
match eval_expr(try_expr, env) {
Ok(Value::Error(msg)) => {
bind_caught(catch_var.as_deref(), Value::Str(msg), catch_expr, env)
}
Ok(val) => {
Ok(val)
}
Err(e) => {
if e.downcast_ref::<crate::safety::SafetyError>()
.is_some_and(|se| se.is_uncatchable_in_agent_mode())
{
return Err(e);
}
clear_failed_lambda();
let caught = match e.downcast_ref::<crate::safety::SafetyError>() {
Some(se) => Value::from_json(&caught_error_record(&se.to_json())),
None => Value::Str(e.to_string()),
};
bind_caught(catch_var.as_deref(), caught, catch_expr, env)
}
}
}
Expr::Throw(inner) => {
let val = eval_expr(inner, env)?;
let msg = match val {
Value::Str(s) => s,
other => format!("{:?}", other),
};
Ok(Value::Error(msg))
}
Expr::Call {
callee,
args,
named: _,
} => {
let pin = env.take_input();
let mut vals = Vec::with_capacity(args.len());
for a in args {
match eval_expr(a, env) {
Ok(v) => vals.push(v),
Err(e) => {
env.set_input(pin);
return Err(e);
}
}
}
env.set_input(pin.clone());
if let Expr::Ident(name) = &**callee {
if let Some(v) = env.get_var(name).cloned() {
match v {
Value::Lambda(_) | Value::AsyncLambda(_) | Value::Builtin(_) => {
return call_value_named(Some(name), v, pin, vals, env)
}
other if !builtins::is_dispatched(name) => {
return Err(crate::safety::bad_arg(
&format!("{name}(...)"),
&format!("a function in `{name}`"),
other.type_name(),
));
}
_ => {
return builtins::call_with_input(name, vals, pin, env);
}
}
} else {
return builtins::call_with_input(name, vals, pin, env);
}
}
if let Expr::MemberAccess { object, field } = &**callee {
if let Some(e) = method_on_non_record(object, field, &vals, env) {
return Err(e);
}
}
let f = eval_expr(callee, env)?;
let called_as = match (&**callee, &f) {
(Expr::MemberAccess { object, field }, Value::Builtin(r)) => match &**object {
Expr::Ident(module) => Some((r.name.clone(), format!("{module}.{field}"))),
_ => None,
},
_ => None,
};
let result = call_value_with_pipe(f, pin, vals, env);
match (result, called_as) {
(Err(e), Some((target, written))) if target != written => {
Err(crate::safety::rename_builtin(e, &target, &written))
}
(result, _) => result,
}
}
Expr::Pipe { left, right } => {
check_pipeline_names(expr, env)?;
let left_val = eval_expr(left, env)?;
if let Some((lambda, path)) = projection_stage(right) {
if env.get_var("map").is_none() && !matches!(left_val, Value::Array(_)) {
if let Value::Record(_) = left_val {
let f = eval_expr(lambda, env)?;
return call_value_with_pipe(f, None, vec![left_val], env);
}
return Err(crate::safety::bad_arg_with_hint(
&path,
"an Array or a Record as the piped subject",
left_val.type_name(),
"a `|.field` stage reads a field of each row of an Array, or of one Record",
));
}
}
if let Expr::Ident(name) = &**right {
if let Some(v) = env.get_var(name).cloned() {
match v {
Value::Lambda(_) | Value::AsyncLambda(_) => {
return call_value_with_pipe(v, None, vec![left_val], env);
}
_ => {
return crate::builtins::call_with_input(
name,
Vec::new(),
Some(left_val),
env,
);
}
}
} else {
return crate::builtins::call_with_input(name, Vec::new(), Some(left_val), env);
}
}
if let Expr::Lambda { .. } = &**right {
let f = eval_expr(right, env)?;
return call_value_with_pipe(f, Some(left_val), Vec::new(), env);
}
let saved = env.take_input();
env.set_input(Some(left_val));
let res = eval_expr(right, env);
match saved {
Some(v) => env.set_input(Some(v)),
None => env.set_input(None),
}
res
}
Expr::Unary { op, expr } => {
let v = eval_expr(expr, env)?;
match (op, v) {
(UnOp::Neg, Value::Int(n)) => Ok(Value::Int(
n.checked_neg()
.ok_or_else(|| crate::safety::integer_overflow("Neg"))?,
)),
(UnOp::Neg, Value::Float(x)) => Ok(Value::Float(-x)),
(UnOp::Not, v) => Ok(Value::Bool(!is_truthy(&v))),
(_, other) => Err(crate::safety::bad_arg(
"unary -",
"an Int or Float",
other.type_name(),
)),
}
}
Expr::Binary { left, op, right } => {
let a = eval_expr(left, env)?;
if matches!(op, BinOp::And | BinOp::Or) {
let x = match a {
Value::Bool(x) => x,
other => return Err(logic_operand_error(op, &other)),
};
match op {
BinOp::And if !x => return Ok(Value::Bool(false)),
BinOp::Or if x => return Ok(Value::Bool(true)),
_ => {}
}
let b = eval_expr(right, env)?;
return match b {
Value::Bool(y) => Ok(Value::Bool(y)),
other => Err(logic_operand_error(op, &other)),
};
}
let b = eval_expr(right, env)?;
binop(op, a, b)
}
Expr::MemberAccess { object, field } => {
if let Some(value) = borrowed_member(expr, env) {
return value.cloned();
}
if let Some(e) = unbound_receiver_error(object, field, env) {
return Err(e);
}
let obj = eval_expr(object, env)?;
match obj {
Value::Record(mut map) => map
.remove(field)
.ok_or_else(|| module_field_error(field, &map, object)),
other => Err(crate::safety::bad_arg(
&match object.as_ref() {
Expr::Ident(name) if name != "__" => format!("{name}.{field}"),
_ => format!(".{field}"),
},
"a Record (or a module)",
other.type_name(),
)),
}
}
Expr::Index { object, index } => {
let idx = eval_expr(index, env)?;
if let Some(target) = borrowed_member(object, env) {
return index_value(target?, &idx, object);
}
let obj = eval_expr(object, env)?;
index_value(&obj, &idx, object)
}
Expr::Match { scrutinee, arms } => {
let value = eval_expr(scrutinee, env)?;
if is_if_desugar(arms) && !matches!(value, Value::Bool(_) | Value::Null) {
return Err(crate::safety::bad_arg_with_hint(
"if",
"a Bool condition",
value.type_name(),
if_condition_hint(&value),
));
}
for arm in arms {
if let Some(bindings) = match_pattern(&arm.pattern, &value) {
if let Some(guard_expr) = &arm.guard {
let mut temp_env = env.clone();
for (name, val) in bindings.iter() {
temp_env.set_var_unchecked(name, val.clone());
}
let guard_result = eval_expr(guard_expr, &mut temp_env)?;
if !is_truthy(&guard_result) {
continue; }
}
for (name, val) in bindings {
env.set_var_unchecked(&name, val);
}
return eval_expr(&arm.body, env);
}
}
Err(crate::safety::bad_arg_in(
"match",
format!("match: no arm matches {}", short_value(&value)),
"add `_ => ...` as the last arm to catch the rest",
))
}
}
}
fn short_value(value: &Value) -> String {
let text = match value {
Value::Str(s) => format!("\"{s}\""),
other => other.to_display_string(),
};
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
if flat.chars().count() <= 40 {
flat
} else {
format!("{}...", flat.chars().take(37).collect::<String>())
}
}
fn unbound_receiver_error(object: &Expr, field: &str, env: &Env) -> Option<anyhow::Error> {
#[cfg(feature = "native")]
{
let Expr::Ident(name) = object else {
return None;
};
if env.get_var(name).is_some()
|| crate::modules::is_module_name(name)
|| builtins::is_dispatched(name)
{
return None;
}
let mapped = builtins::mapped_module_spelling(name, field);
let candidates = if mapped.is_empty() {
builtins::nearest_names(name, crate::modules::all_modules().iter().map(|(m, _)| *m))
} else {
mapped
};
Some(crate::safety::unknown_module(name, candidates))
}
#[cfg(not(feature = "native"))]
{
let _ = (object, field, env);
None
}
}
fn module_field_error(
field: &str,
map: &std::collections::BTreeMap<String, Value>,
object: &Expr,
) -> anyhow::Error {
if let Expr::Ident(name) = object {
if crate::modules::is_module_record(name, map) {
let members = crate::modules::module_member_names(name);
let candidates = builtins::module_member_candidates(name, field, &members);
if builtins::is_dispatched(field) {
let specific = format!(
"`{field}` is a top-level builtin, not a member of {name}: call {field}(s, ...) or s | {field}(...)"
);
return crate::safety::unknown_module_member_hinted(
name,
field,
candidates,
Some(specific),
);
}
return crate::safety::unknown_module_member(name, field, candidates);
}
}
let candidates = crate::builtins::nearest_names(field, map.keys().map(|k| k.as_str()));
let specific = crate::module_loader::unexported_hint(field)
.or_else(|| match object {
Expr::Ident(name) => crate::module_loader::empty_module_hint(name),
_ => None,
})
.or_else(|| {
candidates
.is_empty()
.then(|| crate::safety::missing_field_hint(field, map.keys().map(|k| k.as_str())))
});
crate::safety::unknown_field_hinted(field, candidates, specific)
}
fn unknown_name_error(name: &str, env: &Env) -> anyhow::Error {
let candidates = builtins::did_you_mean_among(name, env.vars().keys().map(|k| k.as_str()));
crate::safety::unknown_name(name, candidates)
}
fn set_field_path(target: &mut Value, label: &str, path: &[String], new: Value) -> Result<()> {
let Some((field, rest)) = path.split_first() else {
*target = new;
return Ok(());
};
let kind = target.type_name();
let Value::Record(map) = target else {
let article = if matches!(kind.chars().next(), Some('A' | 'E' | 'I' | 'O' | 'U')) {
"an"
} else {
"a"
};
let literal = path.iter().rev().fold(value_literal(&new), |inner, field| {
format!("{{{field}: {inner}}}")
});
return Err(crate::safety::bad_arg_with_hint(
label,
"a Record",
kind,
&format!("{label} is {article} {kind}; assign {label} = {literal} to replace it"),
));
};
if rest.is_empty() {
map.insert(field.clone(), new);
return Ok(());
}
let child_label = format!("{label}.{field}");
match map.get_mut(field) {
Some(child) => set_field_path(child, &child_label, rest, new),
None => {
let literal = rest
.iter()
.rev()
.fold(value_literal(&new), |inner, f| format!("{{{f}: {inner}}}"));
Err(crate::safety::bad_arg_in(
label,
format!("{label} has no field {field} to assign into"),
&format!("create it first: {child_label} = {literal}"),
))
}
}
}
fn value_literal(value: &Value) -> String {
match value {
Value::Int(_) | Value::Float(_) | Value::Bool(_) => value.to_display_string(),
Value::Str(s) => format!("\"{s}\""),
_ => "...".to_string(),
}
}
fn borrowed_member<'a>(expr: &Expr, env: &'a Env) -> Option<Result<&'a Value>> {
match expr {
Expr::Ident(name) => {
Some(
crate::safety::check_deadline().and_then(|()| match env.get_var(name) {
Some(value) => Ok(value),
None if builtins::is_dispatched(name) => Ok(&Value::Null),
None => Err(unknown_name_error(name, env)),
}),
)
}
Expr::MemberAccess { object, field } => {
if let Some(e) = unbound_receiver_error(object, field, env) {
return Some(Err(e));
}
let value = borrowed_member(object, env)?;
Some(value.and_then(|value| {
crate::safety::check_deadline()?;
match value {
Value::Record(map) => map
.get(field)
.ok_or_else(|| module_field_error(field, map, object)),
other => Err(crate::safety::bad_arg(
&match object.as_ref() {
Expr::Ident(name) if name != "__" => format!("{name}.{field}"),
_ => format!(".{field}"),
},
"a Record (or a module)",
other.type_name(),
)),
}
}))
}
_ => None,
}
}
fn method_on_non_record(
object: &Expr,
field: &str,
args: &[Value],
env: &Env,
) -> Option<anyhow::Error> {
if !builtins::is_dispatched(field) {
return None;
}
if let Expr::Ident(name) = object {
if name != "__" && env.get_var(name).is_none() && builtins::is_dispatched(name) {
return Some(crate::safety::bad_arg_with_hint(
&format!("{name}.{field}"),
"a Record (or a module)",
"Null",
&format!(
"`{name}` is a builtin, not a module: call {name}() (or {field}(), the builtin itself)"
),
));
}
}
let receiver = match borrowed_member(object, env)? {
Ok(Value::Record(_)) | Ok(Value::Null) | Err(_) => return None,
Ok(value) => value,
};
let recv = match object {
Expr::Ident(name) if name != "__" => name.as_str(),
_ => "value",
};
let ty = receiver.type_name();
let article = if matches!(ty.chars().next(), Some('A' | 'E' | 'I' | 'O' | 'U')) {
"an"
} else {
"a"
};
let rest: Vec<String> = args.iter().map(method_arg_text).collect();
let with_recv = std::iter::once(recv.to_string())
.chain(rest.iter().cloned())
.collect::<Vec<_>>()
.join(", ");
let hint = format!(
"{article} {ty} has no methods; call {field}({with_recv}) or {recv} | {field}({})",
rest.join(", ")
);
Some(crate::safety::bad_arg_with_hint(
&if matches!(object, Expr::Ident(n) if n == "__") {
format!(".{field}")
} else {
format!("{recv}.{field}")
},
"a Record (or a module)",
ty,
&hint,
))
}
fn check_pipeline_names(pipe: &Expr, env: &Env) -> Result<()> {
let mut cur = pipe;
while let Expr::Pipe { left, right } = cur {
if let Some(name) = plain_stage_name(right) {
if env.get_var(name).is_none() && !crate::builtins::is_served(name) {
return Err(crate::safety::unknown_builtin_hinted(
name,
crate::builtins::did_you_mean(name),
crate::module_loader::unexported_hint(name),
));
}
}
cur = &**left;
}
Ok(())
}
fn plain_stage_name(stage: &Expr) -> Option<&str> {
match stage {
Expr::Call { callee, .. } => match &**callee {
Expr::Ident(name) => Some(name.as_str()),
_ => None,
},
Expr::Ident(name) => Some(name.as_str()),
_ => None,
}
}
fn projection_stage(right: &Expr) -> Option<(&Expr, String)> {
let Expr::Call {
callee,
args,
named,
} = right
else {
return None;
};
let (Expr::Ident(name), [lambda], true) = (callee.as_ref(), args.as_slice(), named.is_empty())
else {
return None;
};
let Expr::Lambda { params, body } = lambda else {
return None;
};
let mut fields: Vec<&str> = Vec::new();
let mut root = body.as_ref();
while let Expr::MemberAccess { object, field } = root {
fields.push(field.as_str());
root = object.as_ref();
}
let rooted = !fields.is_empty() && matches!(root, Expr::Ident(n) if n == "__");
if name != "map" || params.len() != 1 || params[0] != "__" || !rooted {
return None;
}
fields.reverse();
Some((lambda, format!(".{}", fields.join("."))))
}
fn method_arg_text(v: &Value) -> String {
match v {
Value::Str(s) => format!("{s:?}"),
other => other.to_display_string(),
}
}
fn index_value(target: &Value, idx: &Value, object: &Expr) -> Result<Value> {
let label = match object {
Expr::Ident(name) => format!("{name}[...]"),
_ => "[...]".to_string(),
};
match (target, idx) {
(Value::Array(items), Value::Int(i)) => {
let from_args = matches!(
object,
Expr::Call { callee, .. }
if matches!(callee.as_ref(), Expr::Ident(name) if name == "args")
);
let pos = resolve_index(*i, items.len(), "Array", from_args)?;
Ok(items[pos].clone())
}
(Value::Str(s), Value::Int(i)) => {
let chars: Vec<char> = s.chars().collect();
let pos = resolve_index(*i, chars.len(), "String", false)?;
Ok(Value::Str(chars[pos].to_string()))
}
(Value::Record(map), Value::Str(key)) => map.get(key).cloned().ok_or_else(|| {
let candidates = crate::builtins::nearest_names(key, map.keys().map(|k| k.as_str()));
let specific = candidates
.is_empty()
.then(|| crate::safety::missing_field_hint(key, map.keys().map(|k| k.as_str())));
crate::safety::unknown_field_hinted(key, candidates, specific)
}),
(Value::Array(_) | Value::Str(_), other) => Err(crate::safety::bad_arg(
&label,
"an Int index",
other.type_name(),
)),
(Value::Record(_), other) => Err(crate::safety::bad_arg(
&label,
"a String key",
other.type_name(),
)),
(other, _) => Err(crate::safety::bad_arg(
&label,
"an Array, String or Record to index",
other.type_name(),
)),
}
}
fn resolve_index(index: i64, len: usize, kind: &str, from_args: bool) -> Result<usize> {
let n = len as i64;
let pos = if index < 0 { n + index } else { index };
if pos < 0 || pos >= n {
return Err(crate::safety::index_out_of_range(
kind, index, len, from_args,
));
}
Ok(pos as usize)
}
use crate::ast::Pattern;
use std::collections::HashMap;
fn is_if_desugar(arms: &[crate::ast::MatchArm]) -> bool {
match arms {
[first, second] => {
first.guard.is_none()
&& second.guard.is_none()
&& matches!(first.pattern, Pattern::LitBool(true))
&& matches!(second.pattern, Pattern::Wildcard)
}
_ => false,
}
}
fn if_condition_hint(value: &Value) -> &'static str {
match value {
Value::Record(_) => "test a field of the record, e.g. exists(p).exists",
Value::Int(_) | Value::Float(_) => "compare it, e.g. n != 0",
Value::Str(_) | Value::Uri(_) => "compare it, e.g. s != \"\"",
Value::Array(_) | Value::Table(_) => "test the length, e.g. len(a) > 0",
_ => "compare it to a value, e.g. x == \"ok\"",
}
}
fn match_pattern(pattern: &Pattern, value: &Value) -> Option<HashMap<String, Value>> {
let mut bindings = HashMap::new();
if match_pattern_impl(pattern, value, &mut bindings) {
Some(bindings)
} else {
None
}
}
fn match_pattern_impl(
pattern: &Pattern,
value: &Value,
bindings: &mut HashMap<String, Value>,
) -> bool {
match pattern {
Pattern::Wildcard => true,
Pattern::Ident(name) => {
bindings.insert(name.clone(), value.clone());
true
}
Pattern::LitInt(n) => value_eq(&Value::Int(*n), value),
Pattern::LitFloat(x) => value_eq(&Value::Float(*x), value),
Pattern::LitStr(s) => matches!(value, Value::Str(v) if v == s),
Pattern::LitBool(b) => matches!(value, Value::Bool(v) if v == b),
Pattern::Null => matches!(value, Value::Null),
Pattern::Constructor { name, args } => {
if let Value::Record(map) = value {
if let Some(Value::Str(tag)) = map.get("_tag") {
if tag == name {
if args.is_empty() {
return true;
} else if args.len() == 1 {
if let Some(inner) = map.get("_value") {
return match_pattern_impl(&args[0], inner, bindings);
}
}
}
}
}
false
}
Pattern::Array(patterns) => {
if let Value::Array(values) = value {
if patterns.len() != values.len() {
return false;
}
for (pat, val) in patterns.iter().zip(values.iter()) {
if !match_pattern_impl(pat, val, bindings) {
return false;
}
}
true
} else {
false
}
}
Pattern::Record(field_patterns) => {
if let Value::Record(map) = value {
for (field_name, field_pattern) in field_patterns {
if let Some(field_value) = map.get(field_name) {
if !match_pattern_impl(field_pattern, field_value, bindings) {
return false;
}
} else {
return false; }
}
true
} else {
false
}
}
}
}
pub(crate) fn call_value_with_pipe(
f: Value,
pin: Option<Value>,
args: Vec<Value>,
env: &mut Env,
) -> Result<Value> {
call_value_named(None, f, pin, args, env)
}
pub(crate) fn call_value_named(
name: Option<&str>,
f: Value,
pin: Option<Value>,
args: Vec<Value>,
env: &mut Env,
) -> Result<Value> {
let is_lambda = matches!(f, Value::Lambda(_));
let result = call_value_named_inner(name, f, pin, args, env);
if let (true, Err(_), Some(name)) = (is_lambda, &result, name) {
note_failed_lambda_call(name);
}
result
}
fn call_value_named_inner(
name: Option<&str>,
f: Value,
pin: Option<Value>,
mut args: Vec<Value>,
env: &mut Env,
) -> Result<Value> {
match f {
Value::Lambda(l) => {
match (pin, l.params.len(), args.len()) {
(_, 0, 0) => call_lambda0(&l, env),
(Some(Value::Array(arr)), 1, 0) => {
let mut out = Vec::with_capacity(arr.len());
for (i, x) in arr.into_iter().enumerate() {
out.push(call_lambda1(&l, x, i, env)?);
}
Ok(Value::Array(out))
}
(Some(v), 1, 0) => call_lambda1(&l, v, 0, env),
(_, 2, 2) if args.len() == 2 => {
let b = args.pop().ok_or_else(|| {
crate::safety::bad_arg("lambda call", "an argument", "nothing")
})?;
let a = args.pop().ok_or_else(|| {
crate::safety::bad_arg("lambda call", "an argument", "nothing")
})?;
call_lambda2(&l, a, b, 0, env)
}
(_, 1, 1) => {
let arg = args.pop().ok_or_else(|| {
crate::safety::bad_arg("lambda call", "an argument", "nothing")
})?;
call_lambda1(&l, arg, 0, env)
}
(_, n, m) if n >= 3 && n == m => call_lambda_n(&l, args, env),
(pin, _, _) => Err(crate::safety::lambda_arity(
name,
&l.params,
args.len() + usize::from(pin.is_some()),
)),
}
}
Value::AsyncLambda(al) => {
let all_args = if let Some(p) = pin {
let mut all = Vec::with_capacity(1 + args.len());
all.push(p);
all.extend(args);
all
} else {
args
};
Ok(Value::Future(Future {
lambda: al,
args: all_args,
}))
}
Value::Builtin(b) => builtins::call_with_input(&b.name, args, pin, env),
Value::Str(name) | Value::Uri(name) => {
if let Some(p) = pin {
let mut all = Vec::with_capacity(1 + args.len());
all.push(p);
all.extend(args);
builtins::call(&name, all, env)
} else {
builtins::call(&name, args, env)
}
}
Value::Null => Err(crate::safety::bad_arg("call", "a function", "Null")),
other => Err(crate::safety::bad_arg(
"call",
"a function",
other.type_name(),
)),
}
}
fn is_zero_divisor(v: &Value) -> bool {
match v {
Value::Int(0) => true,
Value::Float(f) => *f == 0.0, _ => false,
}
}
fn divide_by_zero(what: &str, operator: &str) -> anyhow::Error {
crate::safety::bad_value_in(
operator,
format!(
"{what} by zero: the divisor evaluated to 0. Guard the divisor, or \
filter the empty case before dividing"
),
"check the divisor is not 0 before dividing, or filter zero divisors out first; \
retrying with the same values fails again",
)
}
fn is_truthy(v: &Value) -> bool {
match v {
Value::Null => false,
Value::Bool(b) => *b,
Value::Int(n) => *n != 0,
Value::Float(f) => *f != 0.0,
Value::Str(s) => !s.is_empty(),
Value::Uri(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Record(m) => !m.is_empty(),
Value::Table(t) => !t.rows.is_empty(),
Value::Lambda(_) => true,
Value::AsyncLambda(_) => true,
Value::Future(_) => true,
Value::Error(_) => false, Value::Builtin(_) => true, }
}
const MAX_STRING_BYTES: usize = 8 * 1024 * 1024;
pub(crate) const MAX_ARRAY_ELEMENTS: usize = 10_000_000;
pub(crate) fn check_array_len(len: usize) -> Result<()> {
if len > MAX_ARRAY_ELEMENTS {
return Err(crate::safety::budget_exceeded(
"array",
format!(
"array operation would produce {} elements, over the {} element limit",
len, MAX_ARRAY_ELEMENTS
),
"work on the data in smaller pieces",
));
}
Ok(())
}
fn checked_string(s: String) -> Result<Value> {
if s.len() > MAX_STRING_BYTES {
return Err(crate::safety::budget_exceeded(
"string",
format!(
"string operation would produce {} bytes, over the {} byte limit",
s.len(),
MAX_STRING_BYTES
),
"build the text in smaller pieces, or write it to a file",
));
}
Ok(Value::Str(s))
}
fn bind_caught(
catch_var: Option<&str>,
caught: Value,
catch_expr: &Expr,
env: &mut Env,
) -> Result<Value> {
let Some(name) = catch_var else {
return eval_expr(catch_expr, env);
};
let previous = env.get_var(name).cloned();
env.set_var_unchecked(name, caught);
let out = eval_expr(catch_expr, env);
match previous {
Some(v) => env.set_var_unchecked(name, v),
None => env.del_var(name),
}
out
}
fn free_idents(expr: &Expr, bound: &mut Vec<String>, out: &mut std::collections::BTreeSet<String>) {
match expr {
Expr::LitInt(_) | Expr::LitFloat(_) | Expr::LitStr(_) | Expr::LitBool(_) | Expr::Null => {}
Expr::Ident(name) => {
if !bound.iter().any(|b| b == name) {
out.insert(name.clone());
}
}
Expr::Array(items) => {
for e in items {
free_idents(e, bound, out);
}
}
Expr::Record(fields) => {
for (_, e) in fields {
free_idents(e, bound, out);
}
}
Expr::Lambda { params, body } | Expr::AsyncLambda { params, body } => {
let depth = bound.len();
bound.extend(params.iter().cloned());
free_idents(body, bound, out);
bound.truncate(depth);
}
Expr::Await(inner) | Expr::Throw(inner) => free_idents(inner, bound, out),
Expr::TryCatch {
try_expr,
catch_var,
catch_expr,
} => {
free_idents(try_expr, bound, out);
let depth = bound.len();
if let Some(v) = catch_var {
bound.push(v.clone());
}
free_idents(catch_expr, bound, out);
bound.truncate(depth);
}
Expr::Call {
callee,
args,
named,
} => {
free_idents(callee, bound, out);
for e in args {
free_idents(e, bound, out);
}
for (_, e) in named {
free_idents(e, bound, out);
}
}
Expr::Pipe { left, right } => {
free_idents(left, bound, out);
free_idents(right, bound, out);
}
Expr::Binary { left, right, .. } => {
free_idents(left, bound, out);
free_idents(right, bound, out);
}
Expr::Unary { expr, .. } => free_idents(expr, bound, out),
Expr::MemberAccess { object, .. } => free_idents(object, bound, out),
Expr::Index { object, index } => {
free_idents(object, bound, out);
free_idents(index, bound, out);
}
Expr::Match { scrutinee, arms } => {
free_idents(scrutinee, bound, out);
for arm in arms {
let depth = bound.len();
pattern_bindings(&arm.pattern, bound);
if let Some(g) = &arm.guard {
free_idents(g, bound, out);
}
free_idents(&arm.body, bound, out);
bound.truncate(depth);
}
}
}
}
fn pattern_bindings(p: &Pattern, bound: &mut Vec<String>) {
match p {
Pattern::Wildcard
| Pattern::LitInt(_)
| Pattern::LitFloat(_)
| Pattern::LitStr(_)
| Pattern::LitBool(_)
| Pattern::Null => {}
Pattern::Ident(name) => bound.push(name.clone()),
Pattern::Constructor { args, .. } => {
for a in args {
pattern_bindings(a, bound);
}
}
Pattern::Array(items) => {
for a in items {
pattern_bindings(a, bound);
}
}
Pattern::Record(fields) => {
for (name, sub) in fields {
match sub {
Pattern::Wildcard => bound.push(name.clone()),
other => pattern_bindings(other, bound),
}
}
}
}
}
fn capture_free_vars(
params: &[String],
body: &Expr,
env: &Env,
) -> std::collections::BTreeMap<String, Value> {
let mut bound: Vec<String> = params.to_vec();
let mut free = std::collections::BTreeSet::new();
free_idents(body, &mut bound, &mut free);
let mut captured = std::collections::BTreeMap::new();
for name in free {
#[cfg(feature = "native")]
if crate::modules::is_module_name(&name) {
continue;
}
if env.is_declared_mutable(&name) {
continue;
}
if let Some(v) = env.get_var(&name) {
captured.insert(name, v.clone());
}
}
captured
}
pub(crate) fn complete_module_capture(lam: &mut crate::value::Lambda, module: &Env) {
let mut pending: Vec<String> = lambda_free_names(&lam.params, &lam.body)
.into_iter()
.collect();
let mut seen = std::collections::BTreeSet::new();
while let Some(name) = pending.pop() {
if !seen.insert(name.clone()) {
continue;
}
#[cfg(feature = "native")]
if crate::modules::is_module_name(&name) {
continue;
}
if module.is_declared_mutable(&name) {
continue;
}
let Some(value) = module.get_var(&name) else {
continue;
};
if let Value::Lambda(inner) = value {
pending.extend(lambda_free_names(&inner.params, &inner.body));
}
lam.captured.entry(name).or_insert_with(|| value.clone());
}
}
fn lambda_free_names(params: &[String], body: &Expr) -> std::collections::BTreeSet<String> {
let mut bound: Vec<String> = params.to_vec();
let mut free = std::collections::BTreeSet::new();
free_idents(body, &mut bound, &mut free);
free
}
pub(crate) fn install_captured(
captured: &std::collections::BTreeMap<String, Value>,
env: &mut Env,
) -> Vec<(String, Option<Value>)> {
let mut saved = Vec::with_capacity(captured.len());
for (name, value) in captured {
saved.push((name.clone(), env.get_var(name).cloned()));
env.set_var_unchecked(name, value.clone());
}
saved
}
pub(crate) fn restore_captured(saved: Vec<(String, Option<Value>)>, env: &mut Env) {
for (name, old) in saved.into_iter().rev() {
match old {
Some(v) => env.set_var_unchecked(&name, v),
None => env.del_var(&name),
}
}
}
fn call_lambda0(l: &Lambda, env: &mut Env) -> Result<Value> {
let _depth = crate::safety::enter_call()?;
let restore_caps = install_captured(&l.captured, env);
let saved_pipe = env.take_input();
env.set_input(None);
let out = eval_expr(&l.body, env);
match saved_pipe {
Some(v) => env.set_input(Some(v)),
None => env.set_input(None),
}
restore_captured(restore_caps, env);
out
}
fn call_lambda1(l: &Lambda, x: Value, i: usize, env: &mut Env) -> Result<Value> {
let _depth = crate::safety::enter_call()?;
let restore_caps = install_captured(&l.captured, env);
let p = l
.params
.first()
.ok_or_else(|| crate::safety::arg_err("lambda needs 1 param"))?
.clone();
let saved_pipe = env.take_input();
env.set_input(None);
let old_p = env.get_var(&p).cloned();
env.set_var_unchecked(&p, x);
let mut old_i: Option<Value> = None;
if let Some(ip) = l.params.get(1) {
if ip == "i" {
old_i = env.get_var("i").cloned();
env.set_var_unchecked("i", Value::Int(i as i64));
}
}
let out = eval_expr(&l.body, env);
if let Some(v) = old_i {
env.set_var_unchecked("i", v);
} else if l.params.get(1).map(|s| s.as_str()) == Some("i") {
env.del_var("i");
}
if let Some(v) = old_p {
env.set_var_unchecked(&p, v);
} else {
env.del_var(&p);
}
match saved_pipe {
Some(v) => env.set_input(Some(v)),
None => env.set_input(None),
}
restore_captured(restore_caps, env);
out
}
fn call_lambda_n(l: &Lambda, args: Vec<Value>, env: &mut Env) -> Result<Value> {
let _depth = crate::safety::enter_call()?;
let restore_caps = install_captured(&l.captured, env);
if args.len() != l.params.len() {
return Err(crate::safety::bad_arg(
"lambda call",
&format!("{} argument(s)", l.params.len()),
&format!("{}", args.len()),
));
}
let saved_pipe = env.take_input();
env.set_input(None);
let mut old_bindings: Vec<(String, Option<Value>)> = Vec::with_capacity(l.params.len());
for (param, arg) in l.params.iter().zip(args) {
old_bindings.push((param.clone(), env.get_var(param).cloned()));
env.set_var_unchecked(param, arg);
}
let out = eval_expr(&l.body, env);
for (param, old_val) in old_bindings.into_iter().rev() {
if let Some(v) = old_val {
env.set_var_unchecked(¶m, v);
} else {
env.del_var(¶m);
}
}
match saved_pipe {
Some(v) => env.set_input(Some(v)),
None => env.set_input(None),
}
restore_captured(restore_caps, env);
out
}
fn call_lambda2(l: &Lambda, a: Value, b: Value, i: usize, env: &mut Env) -> Result<Value> {
let _depth = crate::safety::enter_call()?;
let restore_caps = install_captured(&l.captured, env);
let p1 = l
.params
.first()
.ok_or_else(|| crate::safety::arg_err("lambda needs 2 params"))?
.clone();
let p2 = l
.params
.get(1)
.ok_or_else(|| crate::safety::arg_err("lambda needs 2 params"))?
.clone();
let saved_pipe = env.take_input();
env.set_input(None);
let old_p1 = env.get_var(&p1).cloned();
env.set_var_unchecked(&p1, a);
let old_p2 = env.get_var(&p2).cloned();
env.set_var_unchecked(&p2, b);
let mut old_i: Option<Value> = None;
if let Some(ip) = l.params.get(2) {
if ip == "i" {
old_i = env.get_var("i").cloned();
env.set_var_unchecked("i", Value::Int(i as i64));
}
}
let out = eval_expr(&l.body, env);
if let Some(v) = old_i {
env.set_var_unchecked("i", v);
} else if l.params.get(2).map(|s| s.as_str()) == Some("i") {
env.del_var("i");
}
if let Some(v) = old_p2 {
env.set_var_unchecked(&p2, v);
} else {
env.del_var(&p2);
}
if let Some(v) = old_p1 {
env.set_var_unchecked(&p1, v);
} else {
env.del_var(&p1);
}
match saved_pipe {
Some(v) => env.set_input(Some(v)),
None => env.set_input(None),
}
restore_captured(restore_caps, env);
out
}
fn value_eq(a: &Value, b: &Value) -> bool {
use Value::*;
match (a, b) {
(Null, Null) => true,
(Bool(x), Bool(y)) => x == y,
(Int(x), Int(y)) => x == y,
(Float(x), Float(y)) => x == y,
(Int(x), Float(y)) => {
crate::value::cmp_int_float(*x, *y) == Some(std::cmp::Ordering::Equal)
}
(Float(x), Int(y)) => {
crate::value::cmp_int_float(*y, *x) == Some(std::cmp::Ordering::Equal)
}
(Str(x), Str(y)) => x == y,
(Uri(x), Uri(y)) => x == y,
(Array(ax), Array(ay)) => {
ax.len() == ay.len() && ax.iter().zip(ay).all(|(x, y)| value_eq(x, y))
}
(Record(rx), Record(ry)) => {
rx.len() == ry.len()
&& rx
.iter()
.all(|(k, vx)| ry.get(k).is_some_and(|vy| value_eq(vx, vy)))
}
_ => false,
}
}
pub(crate) fn binop(op: &BinOp, a: Value, b: Value) -> Result<Value> {
let result = binop_unchecked(op, a, b)?;
if let Value::Float(f) = &result {
if !f.is_finite()
&& matches!(
op,
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Rem | BinOp::Pow
)
{
let message = if f.is_nan() {
format!("float result in operator {op:?}: the result is not a finite number")
} else {
format!("float overflow in operator {op:?}: the result is not a finite number")
};
return Err(crate::safety::value_not_representable(
&format!("operator {op:?}"),
message,
"the largest Float is about 1.8e308; scale the operands down, or split the computation",
));
}
}
Ok(result)
}
fn binop_unchecked(op: &BinOp, a: Value, b: Value) -> Result<Value> {
use BinOp::*;
Ok(match (op, a, b) {
(Add, Value::Int(x), Value::Int(y)) => Value::Int(checked_int(x.checked_add(y), "Add")?),
(Add, Value::Float(x), Value::Float(y)) => Value::Float(x + y),
(Add, Value::Int(x), Value::Float(y)) => Value::Float((x as f64) + y),
(Add, Value::Float(x), Value::Int(y)) => Value::Float(x + (y as f64)),
(Add, Value::Str(mut x), Value::Str(y)) => {
x.push_str(&y);
checked_string(x)?
}
(Add, Value::Array(mut x), Value::Array(y)) => {
check_array_len(x.len().saturating_add(y.len()))?;
x.extend(y);
Value::Array(x)
}
(Sub, Value::Int(x), Value::Int(y)) => Value::Int(checked_int(x.checked_sub(y), "Sub")?),
(Sub, Value::Float(x), Value::Float(y)) => Value::Float(x - y),
(Sub, Value::Int(x), Value::Float(y)) => Value::Float((x as f64) - y),
(Sub, Value::Float(x), Value::Int(y)) => Value::Float(x - (y as f64)),
(Mul, Value::Int(x), Value::Int(y)) => Value::Int(checked_int(x.checked_mul(y), "Mul")?),
(Mul, Value::Float(x), Value::Float(y)) => Value::Float(x * y),
(Mul, Value::Int(x), Value::Float(y)) => Value::Float((x as f64) * y),
(Mul, Value::Float(x), Value::Int(y)) => Value::Float(x * (y as f64)),
(Div, x, y) if is_zero_divisor(&y) && matches!(x, Value::Int(_) | Value::Float(_)) => {
return Err(divide_by_zero("division", "operator Div"))
}
(Rem, x, y) if is_zero_divisor(&y) && matches!(x, Value::Int(_) | Value::Float(_)) => {
return Err(divide_by_zero("remainder", "operator Rem"))
}
(Div, Value::Int(x), Value::Int(y)) => Value::Float((x as f64) / (y as f64)),
(Div, Value::Float(x), Value::Float(y)) => Value::Float(x / y),
(Div, Value::Int(x), Value::Float(y)) => Value::Float((x as f64) / y),
(Div, Value::Float(x), Value::Int(y)) => Value::Float(x / (y as f64)),
(Rem, Value::Int(x), Value::Int(y)) => Value::Int(x.wrapping_rem(y)),
(Rem, Value::Float(x), Value::Float(y)) => Value::Float(x % y),
(Rem, Value::Int(x), Value::Float(y)) => Value::Float((x as f64) % y),
(Rem, Value::Float(x), Value::Int(y)) => Value::Float(x % (y as f64)),
(Eq, x, y) => Value::Bool(value_eq(&x, &y)),
(Ne, x, y) => Value::Bool(!value_eq(&x, &y)),
(Lt, Value::Int(x), Value::Int(y)) => Value::Bool(x < y),
(Lt, Value::Float(x), Value::Float(y)) => Value::Bool(x < y),
(Lte, Value::Int(x), Value::Int(y)) => Value::Bool(x <= y),
(Lte, Value::Float(x), Value::Float(y)) => Value::Bool(x <= y),
(Gt, Value::Int(x), Value::Int(y)) => Value::Bool(x > y),
(Gt, Value::Float(x), Value::Float(y)) => Value::Bool(x > y),
(Gte, Value::Int(x), Value::Int(y)) => Value::Bool(x >= y),
(Gte, Value::Float(x), Value::Float(y)) => Value::Bool(x >= y),
(Lt, Value::Int(x), Value::Float(y)) => {
Value::Bool(crate::value::cmp_int_float(x, y) == Some(std::cmp::Ordering::Less))
}
(Lt, Value::Float(x), Value::Int(y)) => {
Value::Bool(crate::value::cmp_int_float(y, x) == Some(std::cmp::Ordering::Greater))
}
(Lte, Value::Int(x), Value::Float(y)) => Value::Bool(matches!(
crate::value::cmp_int_float(x, y),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)),
(Lte, Value::Float(x), Value::Int(y)) => Value::Bool(matches!(
crate::value::cmp_int_float(y, x),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)),
(Gt, Value::Int(x), Value::Float(y)) => {
Value::Bool(crate::value::cmp_int_float(x, y) == Some(std::cmp::Ordering::Greater))
}
(Gt, Value::Float(x), Value::Int(y)) => {
Value::Bool(crate::value::cmp_int_float(y, x) == Some(std::cmp::Ordering::Less))
}
(Gte, Value::Int(x), Value::Float(y)) => Value::Bool(matches!(
crate::value::cmp_int_float(x, y),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)),
(Gte, Value::Float(x), Value::Int(y)) => Value::Bool(matches!(
crate::value::cmp_int_float(y, x),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)),
(Lt, Value::Str(x), Value::Str(y)) => Value::Bool(x < y),
(Lte, Value::Str(x), Value::Str(y)) => Value::Bool(x <= y),
(Gt, Value::Str(x), Value::Str(y)) => Value::Bool(x > y),
(Gte, Value::Str(x), Value::Str(y)) => Value::Bool(x >= y),
(And, Value::Bool(x), Value::Bool(y)) => Value::Bool(x && y),
(Or, Value::Bool(x), Value::Bool(y)) => Value::Bool(x || y),
(And | Or, Value::Bool(_), b) => return Err(logic_operand_error(op, &b)),
(And | Or, a, _) => return Err(logic_operand_error(op, &a)),
(Pow, Value::Int(x), Value::Int(y)) => {
if y >= 0 {
let exp = u32::try_from(y).map_err(|_| crate::safety::integer_overflow("Pow"))?;
Value::Int(checked_int(x.checked_pow(exp), "Pow")?)
} else {
if x == 0 {
return Err(divide_by_zero("division", "operator Pow"));
}
Value::Float((x as f64).powf(y as f64))
}
}
(Pow, Value::Float(x), Value::Float(y)) => Value::Float(x.powf(y)),
(Pow, Value::Int(x), Value::Float(y)) => Value::Float((x as f64).powf(y)),
(Pow, Value::Float(x), Value::Int(y)) => Value::Float(x.powf(y as f64)),
(Mul, Value::Str(s), Value::Int(n)) | (Mul, Value::Int(n), Value::Str(s)) => {
if n <= 0 {
Value::Str(String::new())
} else {
let want = s.len().saturating_mul(n as usize);
if want > MAX_STRING_BYTES {
return Err(crate::safety::budget_exceeded(
"string *",
format!(
"string repeat would produce {} bytes, over the {} byte limit",
want, MAX_STRING_BYTES
),
"repeat fewer times",
));
}
Value::Str(s.repeat(n as usize))
}
}
(op, a, b) if is_number_string_mix(&op, &a, &b) => {
return Err(number_string_error(&op, &a, &b))
}
(Add, Value::Str(x), other) => {
checked_string(format!("{}{}", x, other.to_display_string()))?
}
(Add, other, Value::Str(y)) => {
checked_string(format!("{}{}", other.to_display_string(), y))?
}
(op, a, b) => {
let hint = if is_empty_string_number_mix(&a, &b) {
EMPTY_STRING_HINT
} else if is_lambda(&a) || is_lambda(&b) {
PIPE_PRECEDENCE_HINT
} else {
match op {
Lt | Lte | Gt | Gte => {
"compare two numbers or two strings; convert with int(x) or float(x)"
}
_ => "use numbers for arithmetic, or strings for + (concatenation); convert with int(x) or float(x)",
}
};
return Err(crate::safety::bad_arg_with_hint(
&format!("operator {op:?}"),
"operands it applies to",
&format!("{} and {}", a.type_name(), b.type_name()),
hint,
));
}
})
}
fn logic_operand_error(op: &BinOp, got: &Value) -> anyhow::Error {
crate::safety::bad_arg_with_hint(
&format!("operator {op:?}"),
"Bool operands",
got.type_name(),
"compare explicitly, e.g. x != 0, s != \"\", len(a) > 0",
)
}
fn checked_int(result: Option<i64>, operator: &str) -> Result<i64> {
result.ok_or_else(|| crate::safety::integer_overflow(operator))
}
fn is_number_string_mix(op: &BinOp, a: &Value, b: &Value) -> bool {
use BinOp::*;
matches!(op, Add | Sub | Mul | Div | Rem)
&& matches!(
(a, b),
(Value::Str(_), Value::Int(_) | Value::Float(_))
| (Value::Int(_) | Value::Float(_), Value::Str(_))
)
}
fn number_string_error(op: &BinOp, a: &Value, b: &Value) -> anyhow::Error {
let operator = format!("operator {op:?}");
let hint = if is_empty_string_number_mix(a, b) {
EMPTY_STRING_HINT
} else {
"use interpolation \"${x}\", or str(x) / to_string(x) to convert a number to text, or int(x) / float(x) to convert text to a number"
};
crate::safety::bad_arg_in(
&operator,
format!(
"{operator}: cannot combine {} and {}",
a.type_name(),
b.type_name()
),
hint,
)
}
const EMPTY_STRING_HINT: &str =
"the String is empty, often a blank CSV field; filter it out (where(.x != \"\")) or default it";
const PIPE_PRECEDENCE_HINT: &str =
"`|` binds loosest: `a | f > 0` is `a | (f > 0)`; parenthesize the pipe: `(a | f) > 0`";
fn is_lambda(v: &Value) -> bool {
matches!(v, Value::Lambda(_) | Value::AsyncLambda(_))
}
fn is_empty_string_number_mix(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Str(s), Value::Int(_) | Value::Float(_))
| (Value::Int(_) | Value::Float(_), Value::Str(s)) => s.is_empty(),
_ => false,
}
}
pub(crate) fn caught_error_record(wire: &serde_json::Value) -> serde_json::Value {
let inner = wire
.get("error")
.cloned()
.unwrap_or(serde_json::Value::Null);
let mut top = inner.as_object().cloned().unwrap_or_default();
top.insert("error".to_string(), inner);
serde_json::Value::Object(top)
}
fn check_declared_type(name: &str, declared: &str, value: &Value) -> Result<()> {
let expected = match declared {
"Int" | "Float" | "String" | "Bool" | "Array" | "Record" | "Null" => declared,
_ => return Ok(()),
};
let actual = value.type_name();
if actual == expected {
return Ok(());
}
let hint = if expected == "Float" && actual == "Int" {
format!("`{name}` is declared Float: write the literal with a decimal point, such as 1.0")
} else {
format!("`{name}` is declared {expected}: bind a {expected} value, or remove the `: {expected}` annotation")
};
Err(crate::safety::bad_arg_with_hint(
&format!("let {name}"),
expected,
actual,
&hint,
))
}
fn interpolation_name_error(e: anyhow::Error) -> anyhow::Error {
match e.downcast::<crate::safety::SafetyError>() {
Ok(mut se) => {
se.hint = "${...} interpolates in strings; write \\${ for a literal ${".to_string();
anyhow::Error::new(se)
}
Err(other) => other,
}
}
fn unclosed_interpolation_error() -> anyhow::Error {
anyhow::Error::new(crate::parser::ParseError::with_hint(
"unclosed interpolation `${`: no `}` closes it before the string ends",
"close it with }, or write \\${ for a literal ${",
))
}
fn bad_hole_error(expr: &str) -> anyhow::Error {
anyhow::Error::new(crate::parser::ParseError::with_hint(
format!("the interpolation `${{{expr}}}` is not one expression"),
"write one expression inside ${...}, or write \\${ for a literal ${",
))
}
fn interpolate_string(s: &str, env: &mut Env) -> Result<Value> {
let mut result = String::new();
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '$' && chars.get(i + 1) == Some(&'{') {
let body = i + 2;
let expr_str = match crate::parser::interp_hole_end(&chars, body) {
Some(end) => {
i = end;
chars[body..end - 1].iter().collect::<String>()
}
None => return Err(unclosed_interpolation_error()),
};
let stmts = match crate::parser::parse_program(&expr_str) {
Ok(stmts) => stmts,
Err(_) => return Err(bad_hole_error(&expr_str)),
};
let expr = match stmts.as_slice() {
[crate::ast::Stmt::Expr(expr)] => expr,
_ => return Err(bad_hole_error(&expr_str)),
};
match eval_expr(expr, env) {
Ok(val) => {
result.push_str(&val.to_display_string());
if result.len() > MAX_STRING_BYTES {
return checked_string(result);
}
}
Err(e) if crate::safety::code_of(&e) == crate::safety::ErrorCode::UnknownName => {
return Err(interpolation_name_error(e));
}
Err(e) => return Err(e),
}
} else {
result.push(if chars[i] == crate::parser::ESCAPED_DOLLAR {
'$'
} else {
chars[i]
});
i += 1;
}
}
checked_string(result)
}