use std::sync::Arc;
use brink_format::{ListValue, Value};
use crate::error::RuntimeError;
use crate::program::Program;
pub(crate) fn is_truthy(v: &Value) -> bool {
match v {
Value::Bool(b) => *b,
Value::Int(n) => *n != 0,
Value::Float(n) => *n != 0.0,
Value::String(s) => !s.is_empty(),
Value::Null => false,
Value::DivertTarget(_)
| Value::VariablePointer(_)
| Value::TempPointer { .. }
| Value::FragmentRef(_) => true,
Value::List(lv) => !lv.items.is_empty(),
}
}
pub(crate) fn stringify(v: &Value, program: &Program) -> String {
match v {
Value::Int(n) => n.to_string(),
Value::Float(n) => format!("{n}"),
Value::Bool(b) => if *b { "true" } else { "false" }.to_owned(),
Value::String(s) => s.to_string(),
Value::Null => String::new(),
Value::List(lv) => stringify_list(lv, program),
Value::DivertTarget(id) | Value::VariablePointer(id) => format!("{id}"),
Value::TempPointer { slot, frame_depth } => {
format!("TempPointer({slot}@{frame_depth})")
}
Value::FragmentRef(idx) => format!("<fragment:{idx}>"),
}
}
fn stringify_list(lv: &ListValue, program: &Program) -> String {
let mut entries: Vec<(i32, &str, &str)> = lv
.items
.iter()
.filter_map(|&id| {
program.list_item(id).map(|entry| {
let origin_name = program
.list_def(entry.origin)
.map_or("", |def| program.name(def.name));
let full_name = program.name(entry.name);
let display_name = full_name
.split_once('.')
.map_or(full_name, |(_, item)| item);
(entry.ordinal, origin_name, display_name)
})
})
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)));
let names: Vec<&str> = entries.iter().map(|&(_, _, name)| name).collect();
names.join(", ")
}
pub(crate) fn binary_op(
op: BinaryOp,
left: &Value,
right: &Value,
program: &Program,
) -> Result<Value, RuntimeError> {
match (left, right) {
(Value::List(a), Value::List(b)) => list_binary_op(op, a, b, program),
(Value::List(a), Value::Int(b)) if op == BinaryOp::Add || op == BinaryOp::Subtract => {
let shift = if op == BinaryOp::Add { *b } else { -*b };
Ok(Value::List(Arc::new(list_ordinal_shift(a, shift, program))))
}
(Value::Int(a), Value::Int(b)) => int_op(op, *a, *b),
(Value::Float(a), Value::Float(b)) => Ok(float_op(op, *a, *b)),
#[expect(clippy::cast_precision_loss)]
(Value::Int(a), Value::Float(b)) => Ok(float_op(op, *a as f32, *b)),
#[expect(clippy::cast_precision_loss)]
(Value::Float(a), Value::Int(b)) => Ok(float_op(op, *a, *b as f32)),
(Value::String(a), Value::String(b)) => string_op(op, a, b),
(Value::String(a), Value::Int(b)) if op == BinaryOp::Add => {
Ok(Value::String(format!("{a}{b}").into()))
}
(Value::Int(a), Value::String(b)) if op == BinaryOp::Add => {
Ok(Value::String(format!("{a}{b}").into()))
}
(Value::String(a), Value::Float(b)) if op == BinaryOp::Add => {
Ok(Value::String(format!("{a}{b}").into()))
}
(Value::Float(a), Value::String(b)) if op == BinaryOp::Add => {
Ok(Value::String(format!("{a}{b}").into()))
}
(Value::String(a), Value::Int(b)) if op == BinaryOp::Equal || op == BinaryOp::NotEqual => {
string_op(op, a, &b.to_string())
}
(Value::Int(a), Value::String(b)) if op == BinaryOp::Equal || op == BinaryOp::NotEqual => {
string_op(op, &a.to_string(), b)
}
(Value::String(a), Value::Float(b))
if op == BinaryOp::Equal || op == BinaryOp::NotEqual =>
{
string_op(op, a, &format!("{b}"))
}
(Value::Float(a), Value::String(b))
if op == BinaryOp::Equal || op == BinaryOp::NotEqual =>
{
string_op(op, &format!("{a}"), b)
}
(Value::Bool(a), Value::Bool(b)) => bool_op(op, *a, *b),
(Value::Bool(a), Value::Int(b)) => int_op(op, i32::from(*a), *b),
(Value::Int(a), Value::Bool(b)) => int_op(op, *a, i32::from(*b)),
(Value::Bool(a), Value::Float(b)) => Ok(float_op(op, if *a { 1.0 } else { 0.0 }, *b)),
(Value::Float(a), Value::Bool(b)) => Ok(float_op(op, *a, if *b { 1.0 } else { 0.0 })),
(Value::DivertTarget(a), Value::DivertTarget(b)) if op == BinaryOp::Equal => {
Ok(Value::Bool(a == b))
}
(Value::DivertTarget(a), Value::DivertTarget(b)) if op == BinaryOp::NotEqual => {
Ok(Value::Bool(a != b))
}
(Value::Null, Value::Null) if op == BinaryOp::Equal => Ok(Value::Bool(true)),
(Value::Null, Value::Null) if op == BinaryOp::NotEqual => Ok(Value::Bool(false)),
(Value::Null, _) | (_, Value::Null) if op == BinaryOp::Equal => Ok(Value::Bool(false)),
(Value::Null, _) | (_, Value::Null) if op == BinaryOp::NotEqual => Ok(Value::Bool(true)),
_ => Err(RuntimeError::TypeError(format!(
"cannot apply {op:?} to {:?} and {:?}",
left.value_type(),
right.value_type()
))),
}
}
fn list_min_ordinal(lv: &ListValue, program: &Program) -> Option<i32> {
lv.items
.iter()
.filter_map(|&id| program.list_item(id).map(|e| e.ordinal))
.min()
}
fn list_max_ordinal(lv: &ListValue, program: &Program) -> Option<i32> {
lv.items
.iter()
.filter_map(|&id| program.list_item(id).map(|e| e.ordinal))
.max()
}
fn list_compare(op: BinaryOp, a: &ListValue, b: &ListValue, program: &Program) -> bool {
match op {
BinaryOp::Greater => {
if a.items.is_empty() {
return false;
}
if b.items.is_empty() {
return true;
}
matches!(
(list_min_ordinal(a, program), list_max_ordinal(b, program)),
(Some(a_min), Some(b_max)) if a_min > b_max
)
}
BinaryOp::GreaterOrEqual => {
if a.items.is_empty() {
return b.items.is_empty();
}
if b.items.is_empty() {
return true;
}
matches!(
(list_min_ordinal(a, program), list_min_ordinal(b, program),
list_max_ordinal(a, program), list_max_ordinal(b, program)),
(Some(a_min), Some(b_min), Some(a_max), Some(b_max))
if a_min >= b_min && a_max >= b_max
)
}
BinaryOp::Less => {
if b.items.is_empty() {
return false;
}
if a.items.is_empty() {
return true;
}
matches!(
(list_max_ordinal(a, program), list_min_ordinal(b, program)),
(Some(a_max), Some(b_min)) if a_max < b_min
)
}
BinaryOp::LessOrEqual => {
if b.items.is_empty() {
return a.items.is_empty();
}
if a.items.is_empty() {
return true;
}
matches!(
(list_max_ordinal(a, program), list_max_ordinal(b, program),
list_min_ordinal(a, program), list_min_ordinal(b, program)),
(Some(a_max), Some(b_max), Some(a_min), Some(b_min))
if a_max <= b_max && a_min <= b_min
)
}
_ => false,
}
}
fn list_binary_op(
op: BinaryOp,
a: &ListValue,
b: &ListValue,
program: &Program,
) -> Result<Value, RuntimeError> {
match op {
BinaryOp::Add => {
let mut items = a.items.clone();
for &id in &b.items {
if !items.contains(&id) {
items.push(id);
}
}
let mut origins = a.origins.clone();
for &id in &b.origins {
if !origins.contains(&id) {
origins.push(id);
}
}
Ok(Value::List(Arc::new(ListValue { items, origins })))
}
BinaryOp::Subtract => {
let items: Vec<_> = a
.items
.iter()
.filter(|id| !b.items.contains(id))
.copied()
.collect();
Ok(Value::List(Arc::new(ListValue {
items,
origins: a.origins.clone(),
})))
}
BinaryOp::Equal => {
let eq =
a.items.len() == b.items.len() && a.items.iter().all(|id| b.items.contains(id));
Ok(Value::Bool(eq))
}
BinaryOp::NotEqual => {
let eq =
a.items.len() == b.items.len() && a.items.iter().all(|id| b.items.contains(id));
Ok(Value::Bool(!eq))
}
BinaryOp::Greater | BinaryOp::GreaterOrEqual | BinaryOp::Less | BinaryOp::LessOrEqual => {
Ok(Value::Bool(list_compare(op, a, b, program)))
}
BinaryOp::And => Ok(Value::Bool(!a.items.is_empty() && !b.items.is_empty())),
BinaryOp::Or => Ok(Value::Bool(!a.items.is_empty() || !b.items.is_empty())),
_ => Err(RuntimeError::TypeError(format!(
"cannot apply {op:?} to lists"
))),
}
}
fn list_ordinal_shift(lv: &ListValue, shift: i32, program: &Program) -> ListValue {
let mut items = Vec::with_capacity(lv.items.len());
for &item_id in &lv.items {
if let Some(entry) = program.list_item(item_id) {
let target_ordinal = entry.ordinal + shift;
if let Some(def) = program.list_def(entry.origin) {
for &candidate_id in &def.items {
if let Some(candidate) = program.list_item(candidate_id)
&& candidate.ordinal == target_ordinal
{
items.push(candidate_id);
break;
}
}
}
}
}
ListValue {
items,
origins: lv.origins.clone(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BinaryOp {
Add,
Subtract,
Multiply,
Divide,
Modulo,
Equal,
NotEqual,
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
And,
Or,
Min,
Max,
Pow,
}
fn int_op(op: BinaryOp, a: i32, b: i32) -> Result<Value, RuntimeError> {
Ok(match op {
BinaryOp::Add => Value::Int(a.wrapping_add(b)),
BinaryOp::Subtract => Value::Int(a.wrapping_sub(b)),
BinaryOp::Multiply => Value::Int(a.wrapping_mul(b)),
BinaryOp::Divide => {
if b == 0 {
return Err(RuntimeError::DivisionByZero);
}
Value::Int(a.wrapping_div(b))
}
BinaryOp::Modulo => {
if b == 0 {
return Err(RuntimeError::DivisionByZero);
}
Value::Int(a.wrapping_rem(b))
}
BinaryOp::Equal => Value::Bool(a == b),
BinaryOp::NotEqual => Value::Bool(a != b),
BinaryOp::Greater => Value::Bool(a > b),
BinaryOp::GreaterOrEqual => Value::Bool(a >= b),
BinaryOp::Less => Value::Bool(a < b),
BinaryOp::LessOrEqual => Value::Bool(a <= b),
BinaryOp::And => Value::Bool(a != 0 && b != 0),
BinaryOp::Or => Value::Bool(a != 0 || b != 0),
BinaryOp::Min => Value::Int(a.min(b)),
BinaryOp::Max => Value::Int(a.max(b)),
#[expect(clippy::cast_precision_loss)]
BinaryOp::Pow => float_op(op, a as f32, b as f32),
})
}
fn float_op(op: BinaryOp, a: f32, b: f32) -> Value {
match op {
BinaryOp::Add => Value::Float(a + b),
BinaryOp::Subtract => Value::Float(a - b),
BinaryOp::Multiply => Value::Float(a * b),
BinaryOp::Divide => Value::Float(a / b),
BinaryOp::Modulo => Value::Float(a % b),
BinaryOp::Equal => Value::Bool((a - b).abs() < f32::EPSILON),
BinaryOp::NotEqual => Value::Bool((a - b).abs() >= f32::EPSILON),
BinaryOp::Greater => Value::Bool(a > b),
BinaryOp::GreaterOrEqual => Value::Bool(a >= b),
BinaryOp::Less => Value::Bool(a < b),
BinaryOp::LessOrEqual => Value::Bool(a <= b),
BinaryOp::And => Value::Bool(a != 0.0 && b != 0.0),
BinaryOp::Or => Value::Bool(a != 0.0 || b != 0.0),
BinaryOp::Min => Value::Float(a.min(b)),
BinaryOp::Max => Value::Float(a.max(b)),
BinaryOp::Pow => Value::Float(a.powf(b)),
}
}
fn string_op(op: BinaryOp, a: &str, b: &str) -> Result<Value, RuntimeError> {
Ok(match op {
BinaryOp::Add => Value::String(format!("{a}{b}").into()),
BinaryOp::Equal => Value::Bool(a == b),
BinaryOp::NotEqual => Value::Bool(a != b),
_ => {
return Err(RuntimeError::TypeError(format!(
"cannot apply {op:?} to strings"
)));
}
})
}
fn bool_op(op: BinaryOp, a: bool, b: bool) -> Result<Value, RuntimeError> {
Ok(match op {
BinaryOp::Equal => Value::Bool(a == b),
BinaryOp::NotEqual => Value::Bool(a != b),
BinaryOp::And => Value::Bool(a && b),
BinaryOp::Or => Value::Bool(a || b),
_ => int_op(op, i32::from(a), i32::from(b))?,
})
}
pub(crate) fn cast_to_int(v: &Value) -> Value {
match v {
Value::Int(_) => v.clone(),
#[expect(clippy::cast_possible_truncation)]
Value::Float(f) => Value::Int(*f as i32),
Value::Bool(b) => Value::Int(i32::from(*b)),
Value::String(s) => Value::Int(s.parse::<i32>().unwrap_or(0)),
_ => Value::Int(0),
}
}
pub(crate) fn cast_to_float(v: &Value) -> Value {
match v {
Value::Float(_) => v.clone(),
#[expect(clippy::cast_precision_loss)]
Value::Int(n) => Value::Float(*n as f32),
Value::Bool(b) => Value::Float(if *b { 1.0 } else { 0.0 }),
Value::String(s) => Value::Float(s.parse::<f32>().unwrap_or(0.0)),
_ => Value::Float(0.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::program::{LinkedContainer, ListDefEntry, ListItemEntry};
use brink_format::{DefinitionId, DefinitionTag, NameId};
use std::collections::HashMap;
fn dummy_program() -> Program {
Program {
containers: vec![LinkedContainer {
id: DefinitionId::new(DefinitionTag::Address, 0),
bytecode: vec![],
counting_flags: brink_format::CountingFlags::empty(),
path_hash: 0,
scope_table_idx: 0,
}],
address_map: {
let mut m = HashMap::new();
m.insert(DefinitionId::new(DefinitionTag::Address, 0), (0u32, 0usize));
m
},
scope_ids: vec![DefinitionId::new(DefinitionTag::Address, 0)],
source_checksum: 0,
globals: vec![],
global_map: HashMap::new(),
name_table: vec![],
address_by_path: HashMap::new(),
root_idx: 0,
list_literals: vec![],
list_item_map: HashMap::new(),
list_defs: vec![],
list_def_map: HashMap::new(),
external_fns: HashMap::new(),
}
}
#[test]
fn truthiness() {
assert!(is_truthy(&Value::Bool(true)));
assert!(!is_truthy(&Value::Bool(false)));
assert!(is_truthy(&Value::Int(1)));
assert!(!is_truthy(&Value::Int(0)));
assert!(is_truthy(&Value::Float(0.1)));
assert!(!is_truthy(&Value::Float(0.0)));
assert!(is_truthy(&Value::String("hi".into())));
assert!(!is_truthy(&Value::String("".into())));
assert!(!is_truthy(&Value::Null));
}
#[test]
fn int_arithmetic() {
let p = dummy_program();
let r = binary_op(BinaryOp::Add, &Value::Int(2), &Value::Int(3), &p).unwrap();
assert_eq!(r, Value::Int(5));
}
#[test]
fn int_float_promotion() {
let p = dummy_program();
let r = binary_op(BinaryOp::Add, &Value::Int(2), &Value::Float(1.5), &p).unwrap();
assert_eq!(r, Value::Float(3.5));
}
#[test]
fn string_concat() {
let p = dummy_program();
let r = binary_op(
BinaryOp::Add,
&Value::String("a".into()),
&Value::String("b".into()),
&p,
)
.unwrap();
assert_eq!(r, Value::String("ab".into()));
}
#[test]
fn stringify_values() {
let p = dummy_program();
assert_eq!(stringify(&Value::Int(42), &p), "42");
assert_eq!(stringify(&Value::Bool(true), &p), "true");
assert_eq!(stringify(&Value::Null, &p), "");
}
fn program_with_rank_list() -> (Program, DefinitionId, DefinitionId, DefinitionId) {
let list_def_id = DefinitionId::new(DefinitionTag::ListDef, 100);
let low_id = DefinitionId::new(DefinitionTag::ListItem, 1);
let mid_id = DefinitionId::new(DefinitionTag::ListItem, 2);
let high_id = DefinitionId::new(DefinitionTag::ListItem, 3);
let mut p = dummy_program();
p.name_table = vec![
"low".to_string(),
"mid".to_string(),
"high".to_string(),
"Rank".to_string(),
];
p.list_item_map.insert(
low_id,
ListItemEntry {
name: NameId(0),
ordinal: 1,
origin: list_def_id,
},
);
p.list_item_map.insert(
mid_id,
ListItemEntry {
name: NameId(1),
ordinal: 2,
origin: list_def_id,
},
);
p.list_item_map.insert(
high_id,
ListItemEntry {
name: NameId(2),
ordinal: 3,
origin: list_def_id,
},
);
p.list_defs.push(ListDefEntry {
name: NameId(3),
items: vec![low_id, mid_id, high_id],
});
p.list_def_map.insert(list_def_id, 0);
(p, low_id, mid_id, high_id)
}
#[test]
fn list_comparison_ordinal_semantics() {
let (p, low_id, mid_id, high_id) = program_with_rank_list();
let list_def_id = DefinitionId::new(DefinitionTag::ListDef, 100);
let low = Value::List(Arc::new(ListValue {
items: vec![low_id],
origins: vec![list_def_id],
}));
let mid = Value::List(Arc::new(ListValue {
items: vec![mid_id],
origins: vec![list_def_id],
}));
let high = Value::List(Arc::new(ListValue {
items: vec![high_id],
origins: vec![list_def_id],
}));
let mid_high = Value::List(Arc::new(ListValue {
items: vec![mid_id, high_id],
origins: vec![list_def_id],
}));
assert_eq!(
binary_op(BinaryOp::Greater, &mid, &low, &p).unwrap(),
Value::Bool(true)
);
assert_eq!(
binary_op(BinaryOp::Greater, &high, &mid_high, &p).unwrap(),
Value::Bool(false)
);
assert_eq!(
binary_op(BinaryOp::Less, &low, &mid, &p).unwrap(),
Value::Bool(true)
);
assert_eq!(
binary_op(BinaryOp::GreaterOrEqual, &mid_high, &mid, &p).unwrap(),
Value::Bool(true)
);
assert_eq!(
binary_op(BinaryOp::LessOrEqual, &mid, &mid_high, &p).unwrap(),
Value::Bool(true)
);
assert_eq!(
binary_op(BinaryOp::GreaterOrEqual, &low, &mid, &p).unwrap(),
Value::Bool(false)
);
}
#[test]
fn list_comparison_empty() {
let (p, low_id, _, _) = program_with_rank_list();
let list_def_id = DefinitionId::new(DefinitionTag::ListDef, 100);
let empty = Value::List(Arc::new(ListValue {
items: vec![],
origins: vec![list_def_id],
}));
let low = Value::List(Arc::new(ListValue {
items: vec![low_id],
origins: vec![list_def_id],
}));
assert_eq!(
binary_op(BinaryOp::Greater, &low, &empty, &p).unwrap(),
Value::Bool(true)
);
assert_eq!(
binary_op(BinaryOp::Greater, &empty, &low, &p).unwrap(),
Value::Bool(false)
);
assert_eq!(
binary_op(BinaryOp::Less, &empty, &low, &p).unwrap(),
Value::Bool(true)
);
assert_eq!(
binary_op(BinaryOp::GreaterOrEqual, &empty, &empty, &p).unwrap(),
Value::Bool(true)
);
assert_eq!(
binary_op(BinaryOp::LessOrEqual, &empty, &empty, &p).unwrap(),
Value::Bool(true)
);
}
#[test]
fn string_int_equality_coercion() {
let p = dummy_program();
let r = binary_op(
BinaryOp::Equal,
&Value::String("5".into()),
&Value::Int(5),
&p,
)
.unwrap();
assert_eq!(r, Value::Bool(true));
let r = binary_op(
BinaryOp::Equal,
&Value::String("blah".into()),
&Value::Int(5),
&p,
)
.unwrap();
assert_eq!(r, Value::Bool(false));
let r = binary_op(
BinaryOp::Equal,
&Value::Int(5),
&Value::String("5".into()),
&p,
)
.unwrap();
assert_eq!(r, Value::Bool(true));
}
#[test]
fn stringify_list_strips_origin_prefix() {
let list_def_id = DefinitionId::new(DefinitionTag::ListDef, 200);
let a_id = DefinitionId::new(DefinitionTag::ListItem, 10);
let b_id = DefinitionId::new(DefinitionTag::ListItem, 11);
let mut p = dummy_program();
p.name_table = vec![
"Colors.red".to_string(),
"Colors.blue".to_string(),
"Colors".to_string(),
];
p.list_item_map.insert(
a_id,
ListItemEntry {
name: NameId(0),
ordinal: 1,
origin: list_def_id,
},
);
p.list_item_map.insert(
b_id,
ListItemEntry {
name: NameId(1),
ordinal: 2,
origin: list_def_id,
},
);
p.list_defs.push(ListDefEntry {
name: NameId(2),
items: vec![a_id, b_id],
});
p.list_def_map.insert(list_def_id, 0);
let lv = ListValue {
items: vec![a_id, b_id],
origins: vec![list_def_id],
};
assert_eq!(stringify(&Value::List(Arc::new(lv)), &p), "red, blue");
}
#[test]
fn stringify_list_unqualified_names_unchanged() {
let (p, low_id, mid_id, _) = program_with_rank_list();
let list_def_id = DefinitionId::new(DefinitionTag::ListDef, 100);
let lv = ListValue {
items: vec![low_id, mid_id],
origins: vec![list_def_id],
};
assert_eq!(stringify(&Value::List(Arc::new(lv)), &p), "low, mid");
}
}