use alloc::string::ToString;
use alloc::vec::Vec;
use brink_format::{MapKey, OrderedMap, Value, ValueType};
use crate::error::RuntimeError;
use crate::program::Program;
use crate::story::{ExecMode, Flow};
pub(crate) fn array_new(flow: &mut Flow, n: u32) -> Result<(), RuntimeError> {
let mut items = Vec::with_capacity(n as usize);
for _ in 0..n {
items.push(flow.pop_value()?);
}
items.reverse();
flow.value_stack.push(Value::array(items));
Ok(())
}
pub(crate) fn map_new(flow: &mut Flow, n: u32) -> Result<(), RuntimeError> {
let mut pairs = Vec::with_capacity(n as usize);
for _ in 0..n {
let value = flow.pop_value()?;
let key_value = flow.pop_value()?;
let key = to_map_key(&key_value)?;
pairs.push((key, value));
}
pairs.reverse();
let mut map = OrderedMap::with_capacity(pairs.len());
for (k, v) in pairs {
map.insert(k, v);
}
flow.value_stack.push(Value::map(map));
Ok(())
}
pub(crate) fn index_get(flow: &mut Flow) -> Result<(), RuntimeError> {
let index = flow.pop_value()?;
let container = flow.pop_value()?;
if let Value::Range { .. } = &container {
let result = range_element(&container, &index)?;
flow.value_stack.push(result);
return Ok(());
}
let result = read_index(&container, &index)?.clone();
flow.value_stack.push(result);
Ok(())
}
fn range_element(range: &Value, index: &Value) -> Result<Value, RuntimeError> {
let Value::Int(i) = index else {
return Err(RuntimeError::InvalidArrayIndex(type_name(index)));
};
let (Some((start, _, _)), Some(len)) = (range.as_range(), range.range_len()) else {
return Err(RuntimeError::NotIndexable(type_name(range)));
};
if i64::from(*i) < 0 || i64::from(*i) >= len {
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
return Err(RuntimeError::IndexOutOfBounds {
index: *i,
len: len.min(i64::from(u32::MAX)) as usize,
});
}
#[expect(
clippy::cast_possible_truncation,
reason = "start + i is an element of the range by construction, so it fits i32"
)]
Ok(Value::Int((i64::from(start) + i64::from(*i)) as i32))
}
pub(crate) fn index_set(flow: &mut Flow) -> Result<(), RuntimeError> {
let value = flow.pop_value()?;
let index = flow.pop_value()?;
let mut container = flow.pop_value()?;
write_index_upsert(&mut container, &index, value)?;
flow.value_stack.push(container);
Ok(())
}
pub(crate) fn collection_len(flow: &mut Flow) -> Result<(), RuntimeError> {
let container = flow.pop_value()?;
#[expect(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let len = match &container {
Value::Array(items) => items.len() as i32,
Value::Map(map) => map.len() as i32,
Value::Range { .. } => container.range_len().unwrap_or(0).min(i64::from(i32::MAX)) as i32,
Value::String(s) => s.chars().count() as i32,
other => return Err(RuntimeError::NotIndexable(type_name(other))),
};
flow.value_stack.push(Value::Int(len));
Ok(())
}
pub(crate) fn collection_keys(flow: &mut Flow) -> Result<(), RuntimeError> {
let container = flow.pop_value()?;
if matches!(container, Value::Range { .. }) {
flow.value_stack.push(container);
return Ok(());
}
let result = Value::Array(iteration_sequence(container)?);
flow.value_stack.push(result);
Ok(())
}
pub(crate) fn iteration_sequence(
container: Value,
) -> Result<alloc::sync::Arc<Vec<Value>>, RuntimeError> {
match container {
Value::Array(items) => Ok(items),
Value::Map(map) => {
let keys: Vec<Value> = map.keys().map(map_key_to_value).collect();
Ok(alloc::sync::Arc::new(keys))
}
other => Err(RuntimeError::NotIndexable(type_name(&other))),
}
}
pub(crate) fn collection_values(flow: &mut Flow) -> Result<(), RuntimeError> {
let container = flow.pop_value()?;
let Value::Map(map) = &container else {
return Err(RuntimeError::NotIndexable(type_name(&container)));
};
let values: Vec<Value> = map.values().cloned().collect();
flow.value_stack.push(Value::array(values));
Ok(())
}
pub(crate) fn map_get(flow: &mut Flow) -> Result<(), RuntimeError> {
let key = flow.pop_value()?;
let container = flow.pop_value()?;
let Value::Map(map) = &container else {
return Err(RuntimeError::NotIndexable(type_name(&container)));
};
let map_key = to_map_key(&key)?;
let value = map
.get(&map_key)
.cloned()
.ok_or_else(|| RuntimeError::MapKeyNotFound {
key: map_key_display(&map_key),
})?;
flow.value_stack.push(value);
Ok(())
}
pub(crate) fn map_insert(flow: &mut Flow) -> Result<(), RuntimeError> {
let value = flow.pop_value()?;
let key = flow.pop_value()?;
let mut container = flow.pop_value()?;
match container.value_type() {
ValueType::Map => {
let map_key = to_map_key(&key)?;
note_map_mutation(&container);
let Some(map) = container.map_make_mut() else {
return Err(RuntimeError::NotIndexable(type_name(&container)));
};
map.insert(map_key, value);
}
ValueType::Array => {
let len = container.as_array().map_or(0, |items| items.len());
let idx = insert_index(&key, len)?;
note_array_mutation(&container);
let Some(items) = container.array_make_mut() else {
return Err(RuntimeError::NotIndexable(type_name(&container)));
};
items.insert(idx, value);
}
_ => return Err(RuntimeError::NotIndexable(type_name(&container))),
}
flow.value_stack.push(container);
Ok(())
}
pub(crate) fn map_remove(flow: &mut Flow) -> Result<(), RuntimeError> {
let key = flow.pop_value()?;
let mut container = flow.pop_value()?;
match container.value_type() {
ValueType::Map => {
let map_key = to_map_key(&key)?;
note_map_mutation(&container);
let Some(map) = container.map_make_mut() else {
return Err(RuntimeError::NotIndexable(type_name(&container)));
};
map.remove(&map_key);
}
_ => return Err(RuntimeError::NotIndexable(type_name(&container))),
}
flow.value_stack.push(container);
Ok(())
}
pub(crate) fn seq_remove_at(flow: &mut Flow) -> Result<(), RuntimeError> {
let index = flow.pop_value()?;
let mut container = flow.pop_value()?;
match container.value_type() {
ValueType::Array => {
let len = container.as_array().map_or(0, |items| items.len());
let idx = array_index(&index, len)?;
note_array_mutation(&container);
let Some(items) = container.array_make_mut() else {
return Err(RuntimeError::NotIndexable(type_name(&container)));
};
items.remove(idx);
}
_ => return Err(RuntimeError::NotIndexable(type_name(&container))),
}
flow.value_stack.push(container);
Ok(())
}
pub(crate) fn map_contains(flow: &mut Flow) -> Result<(), RuntimeError> {
let needle = flow.pop_value()?;
let container = flow.pop_value()?;
let found = match &container {
Value::Map(map) => MapKey::from_value(&needle).is_some_and(|k| map.contains_key(&k)),
Value::Array(items) => items.iter().any(|item| item == &needle),
other => return Err(RuntimeError::NotIndexable(type_name(other))),
};
flow.value_stack.push(Value::Bool(found));
Ok(())
}
pub(crate) fn seq_index_of(flow: &mut Flow) -> Result<(), RuntimeError> {
let needle = flow.pop_value()?;
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb: "index_of",
expected: "an array",
found: type_name(&container),
});
};
#[expect(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let result = items
.iter()
.position(|item| item == &needle)
.map_or_else(Value::none, |i| Value::some(Value::Int(i as i32)));
flow.value_stack.push(result);
Ok(())
}
pub(crate) fn seq_first(flow: &mut Flow) -> Result<(), RuntimeError> {
seq_edge(flow, "first", <[Value]>::first)
}
pub(crate) fn seq_last(flow: &mut Flow) -> Result<(), RuntimeError> {
seq_edge(flow, "last", <[Value]>::last)
}
fn seq_edge(
flow: &mut Flow,
verb: &'static str,
pick: impl Fn(&[Value]) -> Option<&Value>,
) -> Result<(), RuntimeError> {
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb,
expected: "an array",
found: type_name(&container),
});
};
let result = pick(items).map_or_else(Value::none, |v| Value::some(v.clone()));
flow.value_stack.push(result);
Ok(())
}
pub(crate) fn seq_min(flow: &mut Flow) -> Result<(), RuntimeError> {
seq_extremum(flow, "min", core::cmp::Ordering::Less)
}
pub(crate) fn seq_max(flow: &mut Flow) -> Result<(), RuntimeError> {
seq_extremum(flow, "max", core::cmp::Ordering::Greater)
}
fn seq_extremum(
flow: &mut Flow,
verb: &'static str,
keep_when: core::cmp::Ordering,
) -> Result<(), RuntimeError> {
let mode = flow.exec_mode;
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb,
expected: "an array",
found: type_name(&container),
});
};
if mode == ExecMode::Dev {
nan_scan(verb, items, 0)?;
}
let mut best: Option<&Value> = None;
for item in items.iter() {
best = Some(match best {
None => item,
Some(b) if total_order_cmp(verb, item, b)? == keep_when => item,
Some(b) => b,
});
}
let result = best.map_or_else(Value::none, |v| Value::some(v.clone()));
flow.value_stack.push(result);
Ok(())
}
const ORDERING_NEST_LIMIT: u32 = 64;
fn total_order_cmp(
verb: &'static str,
a: &Value,
b: &Value,
) -> Result<core::cmp::Ordering, RuntimeError> {
total_order_cmp_at(verb, a, b, 0)
}
fn total_order_cmp_at(
verb: &'static str,
a: &Value,
b: &Value,
depth: u32,
) -> Result<core::cmp::Ordering, RuntimeError> {
let not_orderable = |v: &Value| RuntimeError::NotOrderable {
verb,
found: type_name(v),
};
match (a, b) {
(Value::Int(x), Value::Int(y)) => Ok(x.cmp(y)),
(Value::Float(_) | Value::Int(_), Value::Float(_) | Value::Int(_)) => {
let (Some(x), Some(y)) = (a.as_float(), b.as_float()) else {
return Err(not_orderable(a));
};
Ok(pinned_float_cmp(x, y))
}
(Value::Bool(x), Value::Bool(y)) => Ok(x.cmp(y)),
(Value::String(x), Value::String(y)) => Ok(x.as_ref().cmp(y.as_ref())),
(Value::Array(xs), Value::Array(ys)) => {
if depth >= ORDERING_NEST_LIMIT {
return Err(RuntimeError::NotOrderable {
verb,
found: "an array nested past the ordering depth limit",
});
}
for (x, y) in xs.iter().zip(ys.iter()) {
let ord = total_order_cmp_at(verb, x, y, depth + 1)?;
if ord != core::cmp::Ordering::Equal {
return Ok(ord);
}
}
Ok(xs.len().cmp(&ys.len()))
}
_ => {
let orderable = |v: &Value| {
matches!(
v,
Value::Int(_)
| Value::Float(_)
| Value::Bool(_)
| Value::String(_)
| Value::Array(_)
)
};
if orderable(a) && !orderable(b) {
Err(not_orderable(b))
} else {
Err(not_orderable(a))
}
}
}
}
fn nan_scan(verb: &'static str, items: &[Value], depth: u32) -> Result<(), RuntimeError> {
if depth >= ORDERING_NEST_LIMIT {
return Err(RuntimeError::NotOrderable {
verb,
found: "an array nested past the ordering depth limit",
});
}
for item in items {
match item {
Value::Float(f) if f.is_nan() => {
return Err(RuntimeError::UnorderedComparand { verb });
}
Value::Array(inner) => nan_scan(verb, inner, depth + 1)?,
_ => {}
}
}
Ok(())
}
pub(crate) fn fallible_stable_sort<F>(items: &mut [Value], cmp: &mut F) -> Result<(), RuntimeError>
where
F: FnMut(&Value, &Value) -> Result<core::cmp::Ordering, RuntimeError>,
{
let n = items.len();
if n <= 1 {
return Ok(());
}
let mut src: Vec<Value> = items.to_vec();
let mut dst: Vec<Value> = items.to_vec();
let mut width = 1usize;
while width < n {
let mut start = 0usize;
while start < n {
let mid = usize::min(start + width, n);
let end = usize::min(start + 2 * width, n);
let (mut l, mut r, mut o) = (start, mid, start);
while l < mid && r < end {
if cmp(&src[l], &src[r])? == core::cmp::Ordering::Greater {
dst[o] = src[r].clone();
r += 1;
} else {
dst[o] = src[l].clone();
l += 1;
}
o += 1;
}
while l < mid {
dst[o] = src[l].clone();
l += 1;
o += 1;
}
while r < end {
dst[o] = src[r].clone();
r += 1;
o += 1;
}
start = end;
}
core::mem::swap(&mut src, &mut dst);
width *= 2;
}
items.clone_from_slice(&src);
Ok(())
}
pub(crate) fn seq_sorted(flow: &mut Flow) -> Result<(), RuntimeError> {
let mode = flow.exec_mode;
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb: "sort",
expected: "an array",
found: type_name(&container),
});
};
if mode == ExecMode::Dev {
nan_scan("sort", items, 0)?;
}
let mut sorted: Vec<Value> = items.as_ref().clone();
fallible_stable_sort(&mut sorted, &mut |a, b| total_order_cmp("sort", a, b))?;
flow.value_stack.push(Value::array(sorted));
Ok(())
}
fn pinned_float_cmp(x: f32, y: f32) -> core::cmp::Ordering {
use core::cmp::Ordering;
match x.partial_cmp(&y) {
Some(ord) => ord,
None => match (x.is_nan(), y.is_nan()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, _) => Ordering::Less,
},
}
}
pub(crate) fn seq_pop(flow: &mut Flow) -> Result<(), RuntimeError> {
let mut container = flow.pop_value()?;
if !matches!(container, Value::Array(_)) {
return Err(RuntimeError::StdlibWrongType {
verb: "pop",
expected: "an array",
found: type_name(&container),
});
}
note_array_mutation(&container);
let popped = container
.array_make_mut()
.and_then(Vec::pop)
.map_or_else(Value::none, Value::some);
flow.value_stack.push(popped);
flow.value_stack.push(container);
Ok(())
}
pub(crate) fn map_get_opt(flow: &mut Flow) -> Result<(), RuntimeError> {
let key = flow.pop_value()?;
let container = flow.pop_value()?;
let Value::Map(map) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb: "get",
expected: "a map",
found: type_name(&container),
});
};
let map_key = to_map_key(&key)?;
let result = map
.get(&map_key)
.map_or_else(Value::none, |v| Value::some(v.clone()));
flow.value_stack.push(result);
Ok(())
}
pub(crate) fn map_contains_value(flow: &mut Flow) -> Result<(), RuntimeError> {
let needle = flow.pop_value()?;
let container = flow.pop_value()?;
let Value::Map(map) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb: "contains_value",
expected: "a map",
found: type_name(&container),
});
};
let found = map.values().any(|v| v == &needle);
flow.value_stack.push(Value::Bool(found));
Ok(())
}
pub(crate) fn map_clear(flow: &mut Flow) -> Result<(), RuntimeError> {
let container = flow.pop_value()?;
if !matches!(container, Value::Map(_)) {
return Err(RuntimeError::StdlibWrongType {
verb: "clear",
expected: "a map",
found: type_name(&container),
});
}
flow.value_stack.push(Value::map(OrderedMap::new()));
Ok(())
}
pub(crate) fn weighted_new(flow: &mut Flow) -> Result<(), RuntimeError> {
let row = flow.pop_value()?;
let Value::Array(items) = &row else {
return Err(RuntimeError::StdlibWrongType {
verb: "weighted",
expected: "a flattened weight/value pair row",
found: type_name(&row),
});
};
if items.is_empty() {
return Err(RuntimeError::WeightedMalformedTable {
detail: "an empty table",
});
}
if items.len() % 2 != 0 {
return Err(RuntimeError::WeightedMalformedTable {
detail: "an odd flattened pair row",
});
}
let mut entries = Vec::with_capacity(items.len() / 2);
let (pairs, _) = items.as_chunks::<2>();
for pair in pairs {
let weight = match &pair[0] {
Value::Int(w) if *w >= 1 => *w,
Value::Int(w) => {
return Err(RuntimeError::WeightedBadWeight {
found: w.to_string(),
});
}
Value::Float(f) => {
return Err(RuntimeError::WeightedBadWeight {
found: f.to_string(),
});
}
other => {
return Err(RuntimeError::WeightedBadWeight {
found: type_name(other).to_string(),
});
}
};
entries.push((weight, pair[1].clone()));
}
flow.value_stack.push(Value::weighted(entries));
Ok(())
}
pub(crate) fn heap_push(flow: &mut Flow) -> Result<(), RuntimeError> {
let mode = flow.exec_mode;
let element = flow.pop_value()?;
let mut container = flow.pop_value()?;
if !matches!(container, Value::Array(_)) {
return Err(RuntimeError::StdlibWrongType {
verb: "heap_push",
expected: "an array",
found: type_name(&container),
});
}
if mode == ExecMode::Dev {
nan_scan("heap_push", core::slice::from_ref(&element), 0)?;
}
note_array_mutation(&container);
if let Some(items) = container.array_make_mut() {
items.push(element);
let mut i = items.len() - 1;
while i > 0 {
let parent = (i - 1) / 2;
if total_order_cmp("heap_push", &items[i], &items[parent])? == core::cmp::Ordering::Less
{
items.swap(i, parent);
i = parent;
} else {
break;
}
}
}
flow.value_stack.push(container);
Ok(())
}
pub(crate) fn heap_pop(flow: &mut Flow) -> Result<(), RuntimeError> {
let mut container = flow.pop_value()?;
if !matches!(container, Value::Array(_)) {
return Err(RuntimeError::StdlibWrongType {
verb: "heap_pop",
expected: "an array",
found: type_name(&container),
});
}
note_array_mutation(&container);
let mut popped = Value::none();
if let Some(items) = container.array_make_mut()
&& !items.is_empty()
{
let last = items.len() - 1;
items.swap(0, last);
if let Some(min) = items.pop() {
popped = Value::some(min);
}
let n = items.len();
let mut i = 0usize;
loop {
let (l, r) = (2 * i + 1, 2 * i + 2);
let mut smallest = i;
if l < n
&& total_order_cmp("heap_pop", &items[l], &items[smallest])?
== core::cmp::Ordering::Less
{
smallest = l;
}
if r < n
&& total_order_cmp("heap_pop", &items[r], &items[smallest])?
== core::cmp::Ordering::Less
{
smallest = r;
}
if smallest == i {
break;
}
items.swap(i, smallest);
i = smallest;
}
}
flow.value_stack.push(popped);
flow.value_stack.push(container);
Ok(())
}
pub(crate) fn heap_peek(flow: &mut Flow) -> Result<(), RuntimeError> {
let container = flow.pop_value()?;
let Value::Array(items) = &container else {
return Err(RuntimeError::StdlibWrongType {
verb: "heap_peek",
expected: "an array",
found: type_name(&container),
});
};
let result = items
.first()
.map_or_else(Value::none, |v| Value::some(v.clone()));
flow.value_stack.push(result);
Ok(())
}
pub(crate) fn push_literal(
flow: &mut Flow,
program: &Program,
idx: u32,
) -> Result<(), RuntimeError> {
let value = program
.literal_pool_entry(idx)
.cloned()
.ok_or_else(|| RuntimeError::InvalidLiteralIndex(idx))?;
flow.value_stack.push(value);
Ok(())
}
#[cfg(feature = "bench-counters")]
#[inline]
fn note_array_mutation(container: &Value) {
if let Value::Array(items) = container
&& alloc::sync::Arc::strong_count(items) > 1
{
crate::bench_counters::record_cow_copy();
}
}
#[cfg(not(feature = "bench-counters"))]
#[inline(always)]
fn note_array_mutation(_container: &Value) {}
#[cfg(feature = "bench-counters")]
#[inline]
fn note_map_mutation(container: &Value) {
if let Value::Map(map) = container
&& alloc::sync::Arc::strong_count(map) > 1
{
crate::bench_counters::record_cow_copy();
}
}
#[cfg(not(feature = "bench-counters"))]
#[inline(always)]
fn note_map_mutation(_container: &Value) {}
pub(crate) fn type_name(v: &Value) -> &'static str {
match v {
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::Bool(_) => "bool",
Value::String(_) => "string",
Value::List(_) => "list",
Value::DivertTarget(_) => "divert_target",
Value::VariablePointer(_) => "var_pointer",
Value::TempPointer { .. } => "temp_pointer",
Value::Null => "null",
Value::FragmentRef(_) => "fragment_ref",
Value::Array(_) => "array",
Value::Map(_) => "map",
Value::Record { .. } => "record",
Value::FnRef(_) | Value::Closure(_) => "fn",
Value::Handle { .. } => "handle",
Value::Projection(_) => "projection",
Value::OptionVal(_) => "option",
Value::Range { .. } => "range",
Value::Vec2(_) => "vec2",
Value::Vec3(_) => "vec3",
Value::Vec4(_) => "vec4",
Value::Quat(_) => "quat",
Value::Mat2(_) => "mat2",
Value::Mat3(_) => "mat3",
Value::Mat4(_) => "mat4",
Value::Weighted(_) => "weighted",
}
}
fn to_map_key(v: &Value) -> Result<MapKey, RuntimeError> {
MapKey::from_value(v).ok_or_else(|| RuntimeError::InvalidMapKeyType(type_name(v)))
}
fn map_key_to_value(k: &MapKey) -> Value {
match k {
MapKey::Int(n) => Value::Int(*n),
MapKey::Str(s) => Value::String(alloc::sync::Arc::clone(s)),
MapKey::Bool(b) => Value::Bool(*b),
}
}
fn map_key_display(k: &MapKey) -> alloc::string::String {
match k {
MapKey::Int(n) => n.to_string(),
MapKey::Str(s) => s.to_string(),
MapKey::Bool(b) => b.to_string(),
}
}
pub(crate) fn read_index<'a>(
container: &'a Value,
index: &Value,
) -> Result<&'a Value, RuntimeError> {
match container {
Value::Array(items) => {
let i = array_index(index, items.len())?;
#[expect(clippy::indexing_slicing, reason = "bounds validated above")]
Ok(&items[i])
}
Value::Map(map) => {
let key = to_map_key(index)?;
map.get(&key).ok_or_else(|| RuntimeError::MapKeyNotFound {
key: map_key_display(&key),
})
}
other => Err(RuntimeError::NotIndexable(type_name(other))),
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MissingMapKey {
Fault,
Insert,
}
pub(crate) fn write_index(
container: &mut Value,
index: &Value,
value: Value,
) -> Result<(), RuntimeError> {
write_index_impl(container, index, value, MissingMapKey::Fault)
}
pub(crate) fn write_index_upsert(
container: &mut Value,
index: &Value,
value: Value,
) -> Result<(), RuntimeError> {
write_index_impl(container, index, value, MissingMapKey::Insert)
}
fn write_index_impl(
container: &mut Value,
index: &Value,
value: Value,
on_missing: MissingMapKey,
) -> Result<(), RuntimeError> {
match container {
Value::Array(_) => {
let len = container.as_array().map_or(0, |items| items.len());
let i = array_index(index, len)?;
note_array_mutation(container);
let Some(items) = container.array_make_mut() else {
return Err(RuntimeError::NotIndexable(type_name(container)));
};
#[expect(clippy::indexing_slicing, reason = "bounds validated above")]
{
items[i] = value;
}
Ok(())
}
Value::Map(_) => {
let key = to_map_key(index)?;
if on_missing == MissingMapKey::Fault {
let has_key = container.as_map().is_some_and(|map| map.contains_key(&key));
if !has_key {
return Err(RuntimeError::MapKeyNotFound {
key: map_key_display(&key),
});
}
}
note_map_mutation(container);
let Some(map) = container.map_make_mut() else {
return Err(RuntimeError::NotIndexable(type_name(container)));
};
map.insert(key, value);
Ok(())
}
other => Err(RuntimeError::NotIndexable(type_name(other))),
}
}
fn array_index(index: &Value, len: usize) -> Result<usize, RuntimeError> {
let Value::Int(i) = index else {
return Err(RuntimeError::InvalidArrayIndex(type_name(index)));
};
#[expect(clippy::cast_sign_loss)]
if *i < 0 || *i as usize >= len {
Err(RuntimeError::IndexOutOfBounds { index: *i, len })
} else {
Ok(*i as usize)
}
}
fn insert_index(index: &Value, len: usize) -> Result<usize, RuntimeError> {
let Value::Int(i) = index else {
return Err(RuntimeError::InvalidArrayIndex(type_name(index)));
};
#[expect(clippy::cast_sign_loss)]
if *i < 0 || *i as usize > len {
Err(RuntimeError::IndexOutOfBounds { index: *i, len })
} else {
Ok(*i as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::OutputBuffer;
use crate::story::{Flow, PendingTerminal};
use alloc::sync::Arc;
fn test_flow() -> Flow {
Flow {
threads: Vec::new(),
value_stack: Vec::new(),
output: OutputBuffer::new(),
pending_choices: Vec::new(),
current_tags: Vec::new(),
in_tag: false,
skipping_choice: false,
did_safe_exit: false,
did_unsafe_yield: false,
ran_out_of_content_cause: crate::RanOutOfContentCause::default(),
line_delivered_this_turn: false,
exec_mode: crate::story::ExecMode::default(),
pure_callback: crate::story::PureCallbackState::default(),
next_block_id: 0,
pending_terminal: PendingTerminal::default(),
warnings: Vec::new(),
}
}
fn arr(items: Vec<Value>) -> Value {
Value::array(items)
}
fn push_args(flow: &mut Flow, args: Vec<Value>) {
for v in args {
flow.value_stack.push(v);
}
}
#[test]
fn map_insert_array_appends_at_len_index() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![
arr(vec![Value::Int(1), Value::Int(2)]),
Value::Int(2),
Value::Int(3),
],
);
map_insert(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
assert_eq!(
result,
arr(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
);
}
#[test]
fn map_insert_array_shifts_elements_right_at_interior_index() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![
arr(vec![Value::Int(1), Value::Int(3)]),
Value::Int(1),
Value::Int(2),
],
);
map_insert(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
assert_eq!(
result,
arr(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
);
}
#[test]
fn map_insert_array_index_past_len_faults() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![Value::Int(1)]), Value::Int(5), Value::Int(9)],
);
let err = map_insert(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::IndexOutOfBounds { index: 5, len: 1 });
}
#[test]
fn map_insert_array_negative_index_faults() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![Value::Int(1)]), Value::Int(-1), Value::Int(9)],
);
let err = map_insert(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::IndexOutOfBounds { index: -1, len: 1 });
}
#[test]
fn map_insert_array_non_int_index_faults() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![Value::Int(1)]), Value::from("nope"), Value::Int(9)],
);
let err = map_insert(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::InvalidArrayIndex("string"));
}
#[test]
fn map_insert_map_still_insert_or_overwrite_by_key() {
let mut map = OrderedMap::new();
map.insert(MapKey::Str(Arc::from("a")), Value::Int(1));
let mut flow = test_flow();
push_args(
&mut flow,
vec![Value::map(map), Value::from("b"), Value::Int(2)],
);
map_insert(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
let Value::Map(m) = result else {
unreachable!("map_insert on a map must return a map")
};
assert_eq!(m.len(), 2);
assert_eq!(m.get(&MapKey::Str(Arc::from("b"))), Some(&Value::Int(2)));
}
#[test]
fn collection_len_string_counts_chars_not_bytes() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::from("café")]);
collection_len(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
assert_eq!(result, Value::Int(4));
}
#[test]
fn collection_len_string_ascii() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::from("cider")]);
collection_len(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
assert_eq!(result, Value::Int(5));
}
#[test]
fn collection_len_empty_string_is_zero() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::from("")]);
collection_len(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
assert_eq!(result, Value::Int(0));
}
#[test]
fn index_set_map_fresh_key_inserts() {
let mut map = OrderedMap::new();
map.insert(MapKey::Str(Arc::from("a")), Value::Int(1));
let mut flow = test_flow();
push_args(
&mut flow,
vec![Value::map(map), Value::from("fresh"), Value::Int(99)],
);
index_set(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
let Value::Map(m) = result else {
unreachable!("index_set on a map must return a map")
};
assert_eq!(m.len(), 2);
assert_eq!(
m.get(&MapKey::Str(Arc::from("fresh"))),
Some(&Value::Int(99))
);
assert_eq!(m.get(&MapKey::Str(Arc::from("a"))), Some(&Value::Int(1)));
}
#[test]
fn index_set_map_existing_key_overwrites() {
let mut map = OrderedMap::new();
map.insert(MapKey::Str(Arc::from("a")), Value::Int(1));
let mut flow = test_flow();
push_args(
&mut flow,
vec![Value::map(map), Value::from("a"), Value::Int(42)],
);
index_set(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
let Value::Map(m) = result else {
unreachable!("index_set on a map must return a map")
};
assert_eq!(m.len(), 1, "overwrite must not grow the map");
assert_eq!(m.get(&MapKey::Str(Arc::from("a"))), Some(&Value::Int(42)));
}
#[test]
fn index_set_map_invalid_key_type_still_faults() {
let mut map = OrderedMap::new();
map.insert(MapKey::Int(1), Value::Int(1));
let mut flow = test_flow();
push_args(
&mut flow,
vec![Value::map(map), Value::Float(3.5), Value::Int(9)],
);
let err = index_set(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::InvalidMapKeyType("float"));
}
#[test]
fn index_set_array_out_of_bounds_still_faults() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![
arr(vec![Value::Int(1), Value::Int(2)]),
Value::Int(5),
Value::Int(9),
],
);
let err = index_set(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::IndexOutOfBounds { index: 5, len: 2 });
}
#[test]
fn write_index_strict_still_faults_on_missing_key() {
let mut container = Value::map(OrderedMap::new());
let err = write_index(&mut container, &Value::from("k"), Value::Int(1)).unwrap_err();
assert_eq!(
err,
RuntimeError::MapKeyNotFound {
key: "k".to_string()
}
);
}
#[test]
fn seq_remove_at_array_shifts_elements_left() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![
arr(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
Value::Int(1),
],
);
seq_remove_at(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
assert_eq!(result, arr(vec![Value::Int(1), Value::Int(3)]));
}
#[test]
fn seq_remove_at_index_equal_to_len_faults() {
let mut flow = test_flow();
push_args(&mut flow, vec![arr(vec![Value::Int(1)]), Value::Int(1)]);
let err = seq_remove_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::IndexOutOfBounds { index: 1, len: 1 });
}
#[test]
fn seq_remove_at_on_a_map_faults() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![Value::map(OrderedMap::new()), Value::Int(0)],
);
let err = seq_remove_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::NotIndexable("map"));
}
#[test]
fn map_remove_map_no_op_when_key_absent() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![Value::map(OrderedMap::new()), Value::from("missing")],
);
map_remove(&mut flow).unwrap();
let result = flow.pop_value().unwrap();
let Value::Map(m) = result else {
unreachable!("map_remove on a map must return a map")
};
assert_eq!(m.len(), 0);
}
#[test]
fn map_remove_on_an_array_faults() {
let mut flow = test_flow();
push_args(&mut flow, vec![arr(vec![Value::Int(1)]), Value::Int(0)]);
let err = map_remove(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::NotIndexable("array"));
}
#[test]
fn map_contains_array_element_present() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![
arr(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
Value::Int(2),
],
);
map_contains(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(true));
}
#[test]
fn map_contains_array_element_absent() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![Value::Int(1), Value::Int(2)]), Value::Int(9)],
);
map_contains(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(false));
}
#[test]
fn map_contains_array_non_scalar_needle() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![
arr(vec![arr(vec![Value::Int(1)]), arr(vec![Value::Int(2)])]),
arr(vec![Value::Int(2)]),
],
);
map_contains(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(true));
}
#[test]
fn map_contains_map_key_containment_unchanged() {
let mut map = OrderedMap::new();
map.insert(MapKey::Str(Arc::from("k")), Value::Int(1));
let mut flow = test_flow();
push_args(&mut flow, vec![Value::map(map), Value::from("k")]);
map_contains(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(true));
}
#[test]
fn map_contains_map_non_key_domain_float_needle_returns_false() {
let mut map = OrderedMap::new();
map.insert(MapKey::Int(1), Value::Int(1));
let mut flow = test_flow();
push_args(&mut flow, vec![Value::map(map), Value::Float(1.0)]);
map_contains(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(false));
}
#[test]
fn map_contains_map_collection_needle_returns_false() {
let mut map = OrderedMap::new();
map.insert(MapKey::Str(Arc::from("k")), Value::Int(1));
let mut flow = test_flow();
push_args(&mut flow, vec![Value::map(map), arr(vec![Value::Int(1)])]);
map_contains(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(false));
}
#[test]
fn map_contains_non_collection_faults() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::Int(5), Value::Int(5)]);
let err = map_contains(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::NotIndexable("int"));
}
#[test]
fn map_insert_array_cows_when_shared() {
let original = arr(vec![Value::Int(1)]);
let snapshot = original.clone();
let mut flow = test_flow();
push_args(&mut flow, vec![original, Value::Int(1), Value::Int(2)]);
map_insert(&mut flow).unwrap();
let mutated = flow.pop_value().unwrap();
assert_eq!(snapshot, arr(vec![Value::Int(1)]), "snapshot unmutated");
assert_eq!(mutated, arr(vec![Value::Int(1), Value::Int(2)]));
}
#[test]
fn seq_remove_at_cows_when_shared() {
let original = arr(vec![Value::Int(1), Value::Int(2)]);
let snapshot = original.clone();
let mut flow = test_flow();
push_args(&mut flow, vec![original, Value::Int(0)]);
seq_remove_at(&mut flow).unwrap();
let mutated = flow.pop_value().unwrap();
assert_eq!(
snapshot,
arr(vec![Value::Int(1), Value::Int(2)]),
"snapshot unmutated"
);
assert_eq!(mutated, arr(vec![Value::Int(2)]));
}
fn ints(ns: &[i32]) -> Value {
arr(ns.iter().map(|n| Value::Int(*n)).collect())
}
#[test]
fn seq_index_of_finds_first_occurrence_and_none_when_absent() {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[7, 8, 7]), Value::Int(7)]);
seq_index_of(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(0)));
push_args(&mut flow, vec![ints(&[7, 8]), Value::Int(9)]);
seq_index_of(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::none());
}
#[test]
fn seq_index_of_uses_structural_equality() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![ints(&[1, 2]), ints(&[3])]), ints(&[3])],
);
seq_index_of(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(1)));
}
#[test]
fn seq_index_of_on_non_array_faults() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::Int(1), Value::Int(1)]);
let err = seq_index_of(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "index_of",
expected: "an array",
found: "int",
}
);
}
#[test]
fn seq_first_last_on_empty_are_none() {
for op in [seq_first, seq_last] {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[])]);
op(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::none());
}
}
#[test]
fn seq_first_last_pick_the_edges() {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[4, 5, 6])]);
seq_first(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(4)));
push_args(&mut flow, vec![ints(&[4, 5, 6])]);
seq_last(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(6)));
}
#[test]
fn seq_min_max_over_ints_and_empty() {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[3, 1, 2])]);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(1)));
push_args(&mut flow, vec![ints(&[3, 1, 2])]);
seq_max(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(3)));
push_args(&mut flow, vec![ints(&[])]);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::none());
}
#[test]
fn seq_min_max_promote_mixed_numerics_and_return_the_element() {
let mixed = || arr(vec![Value::Int(2), Value::Float(1.5)]);
let mut flow = test_flow();
push_args(&mut flow, vec![mixed()]);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Float(1.5)));
push_args(&mut flow, vec![mixed()]);
seq_max(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(2)));
}
#[test]
fn seq_min_max_order_strings_and_bools() {
let strs = arr(vec![Value::from("pear"), Value::from("apple")]);
let mut flow = test_flow();
push_args(&mut flow, vec![strs]);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::from("apple")));
let bools = arr(vec![Value::Bool(true), Value::Bool(false)]);
push_args(&mut flow, vec![bools]);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Bool(false)));
}
#[test]
fn seq_min_ties_keep_the_first_occurrence() {
let a = arr(vec![Value::Float(-0.0), Value::Float(0.0)]);
let mut flow = test_flow();
push_args(&mut flow, vec![a]);
seq_min(&mut flow).unwrap();
let Value::OptionVal(Some(v)) = flow.pop_value().unwrap() else {
unreachable!("min of a non-empty float array is some");
};
let Value::Float(f) = *v else {
unreachable!("element is a float");
};
assert!(f.is_sign_negative(), "first (-0.0) kept on tie");
}
#[test]
fn seq_max_places_nan_greatest_per_the_pinned_prod_order() {
let a = arr(vec![Value::Float(1.0), Value::Float(f32::NAN)]);
let mut flow = test_flow();
flow.exec_mode = ExecMode::Prod;
push_args(&mut flow, vec![a.clone()]);
seq_max(&mut flow).unwrap();
let Value::OptionVal(Some(v)) = flow.pop_value().unwrap() else {
unreachable!("max of a non-empty float array is some");
};
let Value::Float(f) = *v else {
unreachable!("element is a float");
};
assert!(f.is_nan(), "NaN sorts greatest");
push_args(&mut flow, vec![a]);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Float(1.0)));
}
#[test]
fn seq_extremum_dev_mode_faults_on_nan_comparand() {
let mut flow = test_flow();
assert_eq!(flow.exec_mode, ExecMode::Dev, "dev is the default");
push_args(&mut flow, vec![arr(vec![Value::Float(f32::NAN)])]);
let err = seq_min(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::UnorderedComparand { verb: "min" });
push_args(
&mut flow,
vec![arr(vec![arr(vec![Value::Float(f32::NAN)]), ints(&[1])])],
);
let err = seq_max(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::UnorderedComparand { verb: "max" });
push_args(
&mut flow,
vec![arr(vec![Value::Float(2.0), Value::Float(-1.0)])],
);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Float(-1.0)));
}
#[test]
fn seq_sorted_orders_ints_stably_ascending() {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[3, 1, 2, 1])]);
seq_sorted(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), ints(&[1, 1, 2, 3]));
}
#[test]
fn seq_sorted_orders_strings_and_mixed_numerics() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![
Value::from("pear"),
Value::from("apple"),
Value::from("fig"),
])],
);
seq_sorted(&mut flow).unwrap();
assert_eq!(
flow.pop_value().unwrap(),
arr(vec![
Value::from("apple"),
Value::from("fig"),
Value::from("pear"),
])
);
push_args(
&mut flow,
vec![arr(vec![Value::Int(2), Value::Float(1.5), Value::Int(1)])],
);
seq_sorted(&mut flow).unwrap();
assert_eq!(
flow.pop_value().unwrap(),
arr(vec![Value::Int(1), Value::Float(1.5), Value::Int(2)])
);
}
#[test]
fn seq_sorted_is_stable_across_pinned_ties() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![
Value::Float(0.0),
Value::Float(-0.0),
Value::Float(-1.0),
])],
);
seq_sorted(&mut flow).unwrap();
let Value::Array(items) = flow.pop_value().unwrap() else {
unreachable!("sorted returns an array");
};
let signs: Vec<bool> = items
.iter()
.map(|v| {
let Value::Float(f) = v else {
unreachable!("floats in, floats out")
};
f.is_sign_negative()
})
.collect();
assert_eq!(signs, vec![true, false, true]);
}
#[test]
fn seq_sorted_orders_arrays_lexicographically() {
let mut flow = test_flow();
push_args(
&mut flow,
vec![arr(vec![ints(&[2]), ints(&[1, 5]), ints(&[1])])],
);
seq_sorted(&mut flow).unwrap();
assert_eq!(
flow.pop_value().unwrap(),
arr(vec![ints(&[1]), ints(&[1, 5]), ints(&[2])])
);
}
#[test]
fn seq_sorted_dev_faults_prod_places_nan() {
let a = || {
arr(vec![
Value::Float(f32::NAN),
Value::Float(1.0),
Value::Float(-1.0),
])
};
let mut flow = test_flow();
push_args(&mut flow, vec![a()]);
let err = seq_sorted(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::UnorderedComparand { verb: "sort" });
flow.exec_mode = ExecMode::Prod;
push_args(&mut flow, vec![a()]);
seq_sorted(&mut flow).unwrap();
let Value::Array(items) = flow.pop_value().unwrap() else {
unreachable!("sorted returns an array");
};
assert_eq!(items[0], Value::Float(-1.0));
assert_eq!(items[1], Value::Float(1.0));
let Value::Float(last) = items[2] else {
unreachable!("floats in, floats out")
};
assert!(last.is_nan(), "prod places NaN greatest");
}
#[test]
fn seq_sorted_faults_on_non_array_and_unorderable_elements() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::Int(3)]);
let err = seq_sorted(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "sort",
expected: "an array",
found: "int",
}
);
push_args(&mut flow, vec![arr(vec![Value::Int(1), Value::from("x")])]);
let err = seq_sorted(&mut flow).unwrap_err();
assert!(matches!(
err,
RuntimeError::NotOrderable { verb: "sort", .. }
));
}
#[test]
fn seq_sorted_edge_shapes() {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[])]);
seq_sorted(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), ints(&[]));
push_args(&mut flow, vec![arr(vec![Value::Float(f32::NAN)])]);
let err = seq_sorted(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::UnorderedComparand { verb: "sort" });
}
#[test]
fn seq_min_over_records_faults_not_orderable() {
let p = Value::record(brink_format::ShapeId(0), vec![Value::Int(1)]);
let q = Value::record(brink_format::ShapeId(0), vec![Value::Int(2)]);
let a = arr(vec![p, q]);
let mut flow = test_flow();
push_args(&mut flow, vec![a]);
let err = seq_min(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::NotOrderable {
verb: "min",
found: "record",
}
);
}
#[test]
fn seq_min_cross_type_elements_fault_not_orderable() {
let a = arr(vec![Value::Int(1), Value::from("x")]);
let mut flow = test_flow();
push_args(&mut flow, vec![a]);
let err = seq_min(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::NotOrderable {
verb: "min",
found: "string",
}
);
}
#[test]
fn seq_min_unorderable_element_type_faults() {
let a = arr(vec![
Value::Map(Arc::new(OrderedMap::new())),
Value::Map(Arc::new(OrderedMap::new())),
]);
let mut flow = test_flow();
push_args(&mut flow, vec![a]);
let err = seq_min(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::NotOrderable {
verb: "min",
found: "map",
}
);
}
#[test]
fn seq_min_orders_arrays_lexicographically() {
let a = arr(vec![ints(&[1, 2]), ints(&[1])]);
let mut flow = test_flow();
push_args(&mut flow, vec![a]);
seq_min(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(ints(&[1])));
}
#[test]
fn seq_pop_pushes_option_then_shrunk_array() {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[1, 2])]);
seq_pop(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), ints(&[1]));
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(2)));
}
#[test]
fn seq_pop_on_empty_is_none_and_keeps_the_empty_array() {
let mut flow = test_flow();
push_args(&mut flow, vec![ints(&[])]);
seq_pop(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), ints(&[]));
assert_eq!(flow.pop_value().unwrap(), Value::none());
}
#[test]
fn seq_pop_on_non_array_faults() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::from("nope")]);
let err = seq_pop(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "pop",
expected: "an array",
found: "string",
}
);
}
#[test]
fn seq_pop_cows_when_shared() {
let original = ints(&[1, 2]);
let snapshot = original.clone();
let mut flow = test_flow();
push_args(&mut flow, vec![original]);
seq_pop(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), ints(&[1]));
assert_eq!(snapshot, ints(&[1, 2]), "snapshot unmutated");
}
fn simple_map() -> Value {
let mut m = OrderedMap::new();
m.insert(MapKey::from("hp"), Value::Int(10));
m.insert(MapKey::from("name"), Value::from("gob"));
Value::map(m)
}
#[test]
fn map_get_opt_present_absent_and_wrong_container() {
let mut flow = test_flow();
push_args(&mut flow, vec![simple_map(), Value::from("hp")]);
map_get_opt(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(10)));
push_args(&mut flow, vec![simple_map(), Value::from("mp")]);
map_get_opt(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::none());
push_args(&mut flow, vec![ints(&[1]), Value::Int(0)]);
let err = map_get_opt(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "get",
expected: "a map",
found: "array",
}
);
}
#[test]
fn map_get_opt_non_scalar_key_is_a_malformed_question_fault() {
let mut flow = test_flow();
push_args(&mut flow, vec![simple_map(), ints(&[1])]);
let err = map_get_opt(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::InvalidMapKeyType("array"));
}
#[test]
fn map_contains_value_scans_content_equality() {
let mut flow = test_flow();
push_args(&mut flow, vec![simple_map(), Value::Int(10)]);
map_contains_value(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(true));
push_args(&mut flow, vec![simple_map(), Value::Int(11)]);
map_contains_value(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Bool(false));
push_args(&mut flow, vec![ints(&[10]), Value::Int(10)]);
let err = map_contains_value(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "contains_value",
expected: "a map",
found: "array",
}
);
}
#[test]
fn map_clear_empties_and_faults_on_non_map() {
let mut flow = test_flow();
push_args(&mut flow, vec![simple_map()]);
map_clear(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::map(OrderedMap::new()));
push_args(&mut flow, vec![ints(&[1])]);
let err = map_clear(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "clear",
expected: "a map",
found: "array",
}
);
}
fn weighted_from(flow: &mut Flow, row: Vec<Value>) -> Result<Value, RuntimeError> {
push_args(flow, vec![arr(row)]);
weighted_new(flow)?;
flow.pop_value()
}
#[test]
fn weighted_new_builds_multiset_in_construction_order() {
let mut flow = test_flow();
let w = weighted_from(
&mut flow,
vec![
Value::Int(3),
Value::String("sword".into()),
Value::Int(3),
Value::String("shield".into()),
],
)
.unwrap();
let Value::Weighted(table) = &w else {
unreachable!("expected a Weighted, got {w:?}");
};
assert_eq!(table.entries.len(), 2);
assert_eq!(table.entries[0], (3, Value::String("sword".into())));
assert_eq!(table.entries[1], (3, Value::String("shield".into())));
assert_eq!(table.total_weight(), 6);
}
#[test]
fn weighted_equality_is_multiset_content_not_order() {
let a = Value::weighted(vec![
(3, Value::String("a".into())),
(1, Value::String("b".into())),
]);
let b = Value::weighted(vec![
(1, Value::String("b".into())),
(3, Value::String("a".into())),
]);
let c = Value::weighted(vec![
(3, Value::String("a".into())),
(3, Value::String("a".into())),
]);
assert_eq!(a, b, "order-insensitive");
assert_ne!(a, c, "multiplicity-sensitive");
}
#[test]
fn weighted_new_refuses_bad_computed_weights() {
let mut flow = test_flow();
let err = weighted_from(&mut flow, vec![Value::Int(0), Value::Int(1)]).unwrap_err();
assert_eq!(
err,
RuntimeError::WeightedBadWeight {
found: "0".to_string()
}
);
let err = weighted_from(&mut flow, vec![Value::Int(-3), Value::Int(1)]).unwrap_err();
assert_eq!(
err,
RuntimeError::WeightedBadWeight {
found: "-3".to_string()
}
);
let err = weighted_from(&mut flow, vec![Value::Float(1.5), Value::Int(1)]).unwrap_err();
assert_eq!(
err,
RuntimeError::WeightedBadWeight {
found: "1.5".to_string()
}
);
let err =
weighted_from(&mut flow, vec![Value::String("w".into()), Value::Int(1)]).unwrap_err();
assert_eq!(
err,
RuntimeError::WeightedBadWeight {
found: "string".to_string()
}
);
}
#[test]
fn weighted_new_guards_malformed_pair_rows() {
let mut flow = test_flow();
let err = weighted_from(&mut flow, vec![]).unwrap_err();
assert_eq!(
err,
RuntimeError::WeightedMalformedTable {
detail: "an empty table"
}
);
let err = weighted_from(&mut flow, vec![Value::Int(1)]).unwrap_err();
assert_eq!(
err,
RuntimeError::WeightedMalformedTable {
detail: "an odd flattened pair row"
}
);
}
fn push_heap(flow: &mut Flow, heap: Value, x: Value) -> Result<Value, RuntimeError> {
push_args(flow, vec![heap, x]);
heap_push(flow)?;
flow.pop_value()
}
fn pop_heap(flow: &mut Flow, heap: Value) -> Result<(Value, Value), RuntimeError> {
push_args(flow, vec![heap]);
heap_pop(flow)?;
let shrunk = flow.pop_value()?;
let popped = flow.pop_value()?;
Ok((popped, shrunk))
}
#[test]
fn heap_property_push_n_pop_all_drains_ascending() {
let mut flow = test_flow();
let values = [7, 3, 11, 3, -2, 0, 42, 5, 5, -100, 19, 1];
let mut heap = arr(vec![]);
for v in values {
heap = push_heap(&mut flow, heap, Value::Int(v)).unwrap();
}
let mut drained = Vec::new();
loop {
let (popped, shrunk) = pop_heap(&mut flow, heap).unwrap();
heap = shrunk;
match popped {
Value::OptionVal(None) => break,
Value::OptionVal(Some(v)) => match v.as_ref() {
Value::Int(n) => drained.push(*n),
other => unreachable!("unexpected pop payload {other:?}"),
},
other => unreachable!("heap_pop must produce an Option, got {other:?}"),
}
}
let mut expected = values.to_vec();
expected.sort_unstable();
assert_eq!(drained, expected, "min-heap drains ascending");
assert_eq!(heap, arr(vec![]), "drained heap is empty");
}
#[test]
fn heap_peek_reads_min_without_extraction() {
let mut flow = test_flow();
let mut heap = arr(vec![]);
for v in [5, 2, 9] {
heap = push_heap(&mut flow, heap, Value::Int(v)).unwrap();
}
push_args(&mut flow, vec![heap.clone()]);
heap_peek(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(2)));
push_args(&mut flow, vec![arr(vec![])]);
heap_peek(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::none());
}
#[test]
fn heap_pop_on_empty_is_none_not_fault() {
let mut flow = test_flow();
let (popped, shrunk) = pop_heap(&mut flow, arr(vec![])).unwrap();
assert_eq!(popped, Value::none());
assert_eq!(shrunk, arr(vec![]));
}
#[test]
fn heap_push_dev_mode_faults_on_nan_entry() {
let mut flow = test_flow();
assert_eq!(flow.exec_mode, ExecMode::Dev, "dev is the default");
let err = push_heap(&mut flow, arr(vec![]), Value::Float(f32::NAN)).unwrap_err();
assert_eq!(err, RuntimeError::UnorderedComparand { verb: "heap_push" });
let err = push_heap(&mut flow, arr(vec![]), arr(vec![Value::Float(f32::NAN)])).unwrap_err();
assert_eq!(err, RuntimeError::UnorderedComparand { verb: "heap_push" });
let heap = push_heap(&mut flow, arr(vec![]), Value::Float(1.5)).unwrap();
assert_eq!(heap, arr(vec![Value::Float(1.5)]));
}
#[test]
fn heap_push_prod_mode_places_nan_by_the_pinned_order() {
let mut flow = test_flow();
flow.exec_mode = ExecMode::Prod;
let mut heap = arr(vec![]);
for v in [Value::Float(2.0), Value::Float(f32::NAN), Value::Float(1.0)] {
heap = push_heap(&mut flow, heap, v).unwrap();
}
let (popped, shrunk) = pop_heap(&mut flow, heap).unwrap();
assert_eq!(popped, Value::some(Value::Float(1.0)));
let (popped, shrunk) = pop_heap(&mut flow, shrunk).unwrap();
assert_eq!(popped, Value::some(Value::Float(2.0)));
let (popped, shrunk) = pop_heap(&mut flow, shrunk).unwrap();
let Value::OptionVal(Some(v)) = popped else {
unreachable!("expected some(NaN)");
};
let Value::Float(f) = v.as_ref() else {
unreachable!("expected a float");
};
assert!(f.is_nan(), "NaN pops last (greatest), never dropped");
assert_eq!(shrunk, arr(vec![]));
}
#[test]
fn heap_verbs_fault_on_unorderable_elements_and_non_arrays() {
let mut flow = test_flow();
let heap = arr(vec![Value::Int(1)]);
let err = push_heap(&mut flow, heap, simple_map()).unwrap_err();
assert_eq!(
err,
RuntimeError::NotOrderable {
verb: "heap_push",
found: "map",
}
);
let err = push_heap(&mut flow, Value::Int(1), Value::Int(2)).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "heap_push",
expected: "an array",
found: "int",
}
);
push_args(&mut flow, vec![Value::Int(1)]);
let err = heap_pop(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "heap_pop",
expected: "an array",
found: "int",
}
);
push_args(&mut flow, vec![Value::Int(1)]);
let err = heap_peek(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "heap_peek",
expected: "an array",
found: "int",
}
);
}
#[test]
fn heap_push_cows_when_shared() {
let original = ints(&[1, 3]);
let snapshot = original.clone();
let mut flow = test_flow();
let mutated = push_heap(&mut flow, original, Value::Int(0)).unwrap();
assert_eq!(snapshot, ints(&[1, 3]), "snapshot unmutated");
assert_eq!(mutated, ints(&[0, 3, 1]), "sifted to the root");
}
}