use crate::value::{deep_equals, Value};
use serde_json::Value as J;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExprFailureCode {
IntOverflow,
NanOrInf,
ModZero,
PrecisionLoss,
TypeMismatch,
NullRef,
MissingProp,
UnknownBinding,
UnknownOp,
InvalidNode,
InvalidLiteral,
}
impl ExprFailureCode {
pub fn as_str(self) -> &'static str {
match self {
ExprFailureCode::IntOverflow => "INT_OVERFLOW",
ExprFailureCode::NanOrInf => "NAN_OR_INF",
ExprFailureCode::ModZero => "MOD_ZERO",
ExprFailureCode::PrecisionLoss => "PRECISION_LOSS",
ExprFailureCode::TypeMismatch => "TYPE_MISMATCH",
ExprFailureCode::NullRef => "NULL_REF",
ExprFailureCode::MissingProp => "MISSING_PROP",
ExprFailureCode::UnknownBinding => "UNKNOWN_BINDING",
ExprFailureCode::UnknownOp => "UNKNOWN_OP",
ExprFailureCode::InvalidNode => "INVALID_NODE",
ExprFailureCode::InvalidLiteral => "INVALID_LITERAL",
}
}
}
#[derive(Debug, Clone)]
pub struct ExprFailure {
pub code: ExprFailureCode,
pub message: String,
}
impl std::fmt::Display for ExprFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code.as_str(), self.message)
}
}
impl std::error::Error for ExprFailure {}
type R = Result<Value, ExprFailure>;
fn fail<T>(code: ExprFailureCode, message: impl Into<String>) -> Result<T, ExprFailure> {
Err(ExprFailure {
code,
message: message.into(),
})
}
const WIDEN_EXACT: i64 = 1 << 53;
fn check_finite(v: f64) -> R {
if v.is_finite() {
Ok(Value::Float(v))
} else {
fail(ExprFailureCode::NanOrInf, format!("non-finite float: {v}"))
}
}
fn widen_to_float(v: &Value) -> Result<f64, ExprFailure> {
match v {
Value::Float(f) => Ok(*f),
Value::Int(i) => {
if *i > WIDEN_EXACT || *i < -WIDEN_EXACT {
fail(
ExprFailureCode::PrecisionLoss,
format!("int {i} exceeds exact float range (±2^53)"),
)
} else {
Ok(*i as f64)
}
}
other => fail(
ExprFailureCode::TypeMismatch,
format!("numeric operand expected, got {}", other.type_name()),
),
}
}
pub fn cmp_code_points(a: &str, b: &str) -> std::cmp::Ordering {
a.cmp(b)
}
fn require_bool(v: &Value, ctx: &str) -> Result<bool, ExprFailure> {
match v {
Value::Bool(b) => Ok(*b),
other => fail(
ExprFailureCode::TypeMismatch,
format!(
"{ctx}: bool expected, got {} (no truthiness)",
other.type_name()
),
),
}
}
pub fn evaluate(node: &J, scope: &[(String, Value)]) -> R {
match node {
J::Null => Ok(Value::Null),
J::Bool(b) => Ok(Value::Bool(*b)),
J::String(s) => Ok(Value::Str(s.clone())),
J::Number(n) => {
if n.is_i64() {
let i = n.as_i64().unwrap();
const SAFE: i64 = 9_007_199_254_740_991;
if !(-SAFE..=SAFE).contains(&i) {
return fail(
ExprFailureCode::InvalidLiteral,
format!("integral literal {i} exceeds safe range; use {{int:\"…\"}}"),
);
}
Ok(Value::Int(i))
} else if n.is_u64() {
fail(
ExprFailureCode::InvalidLiteral,
format!("integral literal {n} exceeds safe range; use {{int:\"…\"}}"),
)
} else {
let f = n.as_f64().ok_or(ExprFailure {
code: ExprFailureCode::InvalidLiteral,
message: format!("bad number literal {n}"),
})?;
check_finite(f)
}
}
J::Array(_) => fail(
ExprFailureCode::InvalidNode,
"bare array is not an expression (use {arr:[...]})",
),
J::Object(map) => {
if map.len() != 1 {
let keys: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
return fail(
ExprFailureCode::InvalidNode,
format!(
"operator node must have exactly one key, got [{}]",
keys.join(", ")
),
);
}
let (op, arg) = map.iter().next().unwrap();
eval_op(op, arg, scope)
}
}
}
fn eval_op(op: &str, arg: &J, scope: &[(String, Value)]) -> R {
match op {
"int" => {
let s = arg.as_str().ok_or_else(|| ExprFailure {
code: ExprFailureCode::InvalidNode,
message: "{int:…} expects a string".into(),
})?;
match s.parse::<i64>() {
Ok(v) => Ok(Value::Int(v)),
Err(_) => {
if s.trim_start_matches('-')
.chars()
.all(|c| c.is_ascii_digit())
&& !s.is_empty()
&& s != "-"
{
fail(ExprFailureCode::IntOverflow, format!("i64 overflow: {s}"))
} else {
fail(
ExprFailureCode::InvalidLiteral,
format!("invalid int literal: {s}"),
)
}
}
}
}
"float" => {
let n = arg.as_f64().ok_or_else(|| ExprFailure {
code: ExprFailureCode::InvalidNode,
message: "{float:…} expects a number".into(),
})?;
check_finite(n)
}
"ref" | "refOpt" => eval_ref(op, arg, scope),
"obj" => {
let m = arg.as_object().ok_or_else(|| ExprFailure {
code: ExprFailureCode::InvalidNode,
message: "{obj:…} expects an object".into(),
})?;
let mut out = Vec::with_capacity(m.len());
for (k, v) in m {
out.push((k.clone(), evaluate(v, scope)?));
}
Ok(Value::Obj(out))
}
"arr" => {
let a = arg_array(op, arg)?;
let mut out = Vec::with_capacity(a.len());
for e in a {
out.push(evaluate(e, scope)?);
}
Ok(Value::Arr(out))
}
"add" | "sub" | "mul" => {
let (a, b) = eval_binary(op, arg, scope)?;
match (&a, &b) {
(Value::Int(x), Value::Int(y)) => {
let r = match op {
"add" => x.checked_add(*y),
"sub" => x.checked_sub(*y),
_ => x.checked_mul(*y),
};
match r {
Some(v) => Ok(Value::Int(v)),
None => fail(
ExprFailureCode::IntOverflow,
format!("i64 overflow in {op}"),
),
}
}
(Value::Float(x), Value::Float(y)) => {
let r = match op {
"add" => x + y,
"sub" => x - y,
_ => x * y,
};
check_finite(r)
}
_ => fail(
ExprFailureCode::TypeMismatch,
format!(
"{op}: int×int or float×float (got {}×{})",
a.type_name(),
b.type_name()
),
),
}
}
"neg" => {
let a = evaluate(arg_unary(op, arg)?, scope)?;
match a {
Value::Int(i) => match i.checked_neg() {
Some(v) => Ok(Value::Int(v)),
None => fail(ExprFailureCode::IntOverflow, "i64 overflow in neg"),
},
Value::Float(f) => check_finite(-f),
other => fail(
ExprFailureCode::TypeMismatch,
format!("neg: numeric expected, got {}", other.type_name()),
),
}
}
"div" => {
let (a, b) = eval_binary(op, arg, scope)?;
let fa = widen_to_float(&a)?;
let fb = widen_to_float(&b)?;
check_finite(fa / fb)
}
"mod" => {
let (a, b) = eval_binary(op, arg, scope)?;
match (&a, &b) {
(Value::Int(x), Value::Int(y)) => {
if *y == 0 {
return fail(ExprFailureCode::ModZero, "int mod by zero");
}
match x.checked_rem(*y) {
Some(v) => Ok(Value::Int(v)),
None => fail(ExprFailureCode::IntOverflow, "i64 overflow in mod"),
}
}
(Value::Float(x), Value::Float(y)) => check_finite(x % y),
_ => fail(
ExprFailureCode::TypeMismatch,
format!(
"mod: int×int or float×float (got {}×{})",
a.type_name(),
b.type_name()
),
),
}
}
"concat" => {
let a = arg_array(op, arg)?;
let mut s = String::new();
for e in a {
match evaluate(e, scope)? {
Value::Str(p) => s.push_str(&p),
other => {
return fail(
ExprFailureCode::TypeMismatch,
format!(
"concat: strings only (got {}; no implicit toString)",
other.type_name()
),
)
}
}
}
Ok(Value::Str(s))
}
"eq" | "ne" => {
let (a, b) = eval_binary(op, arg, scope)?;
let equal = value_equals(&a, &b)?;
Ok(Value::Bool(if op == "eq" { equal } else { !equal }))
}
"lt" | "le" | "gt" | "ge" => {
use std::cmp::Ordering;
let (a, b) = eval_binary(op, arg, scope)?;
let c: Ordering = match (&a, &b) {
(Value::Int(x), Value::Int(y)) => x.cmp(y),
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
(Value::Str(x), Value::Str(y)) => cmp_code_points(x, y),
_ => {
return fail(
ExprFailureCode::TypeMismatch,
format!(
"{op}: same-typed int/float/string only (got {}×{})",
a.type_name(),
b.type_name()
),
)
}
};
let res = match op {
"lt" => c == Ordering::Less,
"le" => c != Ordering::Greater,
"gt" => c == Ordering::Greater,
_ => c != Ordering::Less,
};
Ok(Value::Bool(res))
}
"and" | "or" => {
let (ea, eb) = raw_binary(op, arg)?;
let a = require_bool(&evaluate(ea, scope)?, op)?;
if op == "and" && !a {
return Ok(Value::Bool(false));
}
if op == "or" && a {
return Ok(Value::Bool(true));
}
Ok(Value::Bool(require_bool(&evaluate(eb, scope)?, op)?))
}
"not" => {
let a = require_bool(&evaluate(arg_unary(op, arg)?, scope)?, "not")?;
Ok(Value::Bool(!a))
}
"coalesce" => {
let (ea, eb) = raw_binary(op, arg)?;
let a = evaluate(ea, scope)?;
match a {
Value::Null => evaluate(eb, scope),
other => Ok(other),
}
}
"cond" => {
let a = arg
.as_array()
.filter(|a| a.len() == 3)
.ok_or_else(|| ExprFailure {
code: ExprFailureCode::InvalidNode,
message: "cond expects [c, t, e]".into(),
})?;
let c = require_bool(&evaluate(&a[0], scope)?, "cond")?;
evaluate(if c { &a[1] } else { &a[2] }, scope)
}
"len" => {
let a = evaluate(arg_unary(op, arg)?, scope)?;
match a {
Value::Arr(v) => Ok(Value::Int(v.len() as i64)),
other => fail(
ExprFailureCode::TypeMismatch,
format!(
"len: arrays only (string length is not v1; got {})",
other.type_name()
),
),
}
}
_ => fail(
ExprFailureCode::UnknownOp,
format!("unknown operator: {op} (fail-closed)"),
),
}
}
fn eval_ref(op: &str, arg: &J, scope: &[(String, Value)]) -> R {
let path = arg_array(op, arg)?;
if path.is_empty() || !path.iter().all(|p| p.is_string()) {
return fail(
ExprFailureCode::InvalidNode,
format!("{op} expects a non-empty string path"),
);
}
let head = path[0].as_str().unwrap();
let mut cur: Value = match scope.iter().find(|(k, _)| k == head) {
Some((_, v)) => v.clone(),
None => {
return fail(
ExprFailureCode::UnknownBinding,
format!("unknown binding: {head}"),
)
}
};
for seg_node in &path[1..] {
let seg = seg_node.as_str().unwrap();
match cur {
Value::Null => {
if op == "refOpt" {
return Ok(Value::Null);
}
return fail(
ExprFailureCode::NullRef,
format!("null intermediate at .{seg} (use ?.)"),
);
}
Value::Obj(ref pairs) => match pairs.iter().find(|(k, _)| k == seg) {
Some((_, v)) => {
let next = v.clone();
cur = next;
}
None => {
return fail(
ExprFailureCode::MissingProp,
format!("missing property .{seg}"),
)
}
},
ref other => {
return fail(
ExprFailureCode::TypeMismatch,
format!("cannot access .{seg} on {}", other.type_name()),
)
}
}
}
Ok(cur)
}
fn value_equals(a: &Value, b: &Value) -> Result<bool, ExprFailure> {
if matches!(a, Value::Null) || matches!(b, Value::Null) {
return Ok(matches!(a, Value::Null) && matches!(b, Value::Null));
}
let ta = a.type_name();
let tb = b.type_name();
if ta != tb {
return fail(
ExprFailureCode::TypeMismatch,
format!("eq/ne: same type only (got {ta}×{tb})"),
);
}
if ta == "arr" || ta == "obj" {
return fail(
ExprFailureCode::TypeMismatch,
"eq/ne: obj/arr equality is undefined in v1",
);
}
Ok(deep_equals(a, b))
}
fn arg_array<'a>(op: &str, arg: &'a J) -> Result<&'a Vec<J>, ExprFailure> {
arg.as_array().ok_or_else(|| ExprFailure {
code: ExprFailureCode::InvalidNode,
message: format!("{op} expects an args array"),
})
}
fn arg_unary<'a>(op: &str, arg: &'a J) -> Result<&'a J, ExprFailure> {
let a = arg_array(op, arg)?;
if a.len() != 1 {
return Err(ExprFailure {
code: ExprFailureCode::InvalidNode,
message: format!("{op} expects 1 arg"),
});
}
Ok(&a[0])
}
fn raw_binary<'a>(op: &str, arg: &'a J) -> Result<(&'a J, &'a J), ExprFailure> {
let a = arg_array(op, arg)?;
if a.len() != 2 {
return Err(ExprFailure {
code: ExprFailureCode::InvalidNode,
message: format!("{op} expects 2 args"),
});
}
Ok((&a[0], &a[1]))
}
fn eval_binary(
op: &str,
arg: &J,
scope: &[(String, Value)],
) -> Result<(Value, Value), ExprFailure> {
let (ea, eb) = raw_binary(op, arg)?;
Ok((evaluate(ea, scope)?, evaluate(eb, scope)?))
}