use alloc::format;
use alloc::vec::Vec;
use brink_format::{NameId, ShapeId, Value};
use crate::error::RuntimeError;
use crate::program::Program;
use crate::story::Flow;
pub(crate) fn record_new(
flow: &mut Flow,
program: &Program,
shape_id: u32,
) -> Result<(), RuntimeError> {
let shape = ShapeId(shape_id);
let entry = program
.struct_shape(shape)
.ok_or(RuntimeError::InvalidShapeId(shape_id))?;
let field_count = entry.fields.len();
let mut fields = Vec::with_capacity(field_count);
for _ in 0..field_count {
fields.push(flow.pop_value()?);
}
fields.reverse();
flow.value_stack.push(Value::record(shape, fields));
Ok(())
}
pub(crate) fn record_get_dyn(
flow: &mut Flow,
program: &Program,
name_id: u16,
) -> Result<(), RuntimeError> {
let record = flow.pop_value()?;
if crate::value_ops::is_tower(&record) {
let name = program
.name_checked(NameId(name_id))
.ok_or(RuntimeError::InvalidNameId(name_id))?;
let Some(component) = crate::tower_ops::tower_component(&record, name) else {
return Err(RuntimeError::TypeError(alloc::format!(
"`{}` has no component `{name}`",
type_name(&record)
)));
};
flow.value_stack.push(component);
return Ok(());
}
let value = read_field(program, &record, NameId(name_id))?.clone();
flow.value_stack.push(value);
Ok(())
}
pub(crate) fn record_set_dyn(
flow: &mut Flow,
program: &Program,
name_id: u16,
) -> Result<(), RuntimeError> {
let value = flow.pop_value()?;
let mut record = flow.pop_value()?;
if crate::value_ops::is_tower(&record) {
return Err(RuntimeError::TypeError(alloc::format!(
"`{}` components are read-only — construct a new value instead",
type_name(&record)
)));
}
let idx = field_index(program, &record, NameId(name_id))?;
note_record_mutation(&record);
let Some(fields) = record.record_make_mut() else {
return Err(RuntimeError::NotARecord(type_name(&record)));
};
#[expect(
clippy::indexing_slicing,
reason = "index validated by field_index above"
)]
{
fields[idx] = value;
}
flow.value_stack.push(record);
Ok(())
}
pub(crate) fn record_get(flow: &mut Flow, offset: u16) -> Result<(), RuntimeError> {
let record = flow.pop_value()?;
let Some((_, fields)) = record.as_record() else {
return Err(RuntimeError::NotARecord(type_name(&record)));
};
let Some(value) = fields.get(offset as usize).cloned() else {
return Err(RuntimeError::RecordFieldOffsetOutOfRange {
offset,
len: fields.len(),
});
};
flow.value_stack.push(value);
Ok(())
}
pub(crate) fn record_set(flow: &mut Flow, offset: u16) -> Result<(), RuntimeError> {
let value = flow.pop_value()?;
let mut record = flow.pop_value()?;
let len = record
.as_record()
.map(|(_, fields)| fields.len())
.ok_or_else(|| RuntimeError::NotARecord(type_name(&record)))?;
if offset as usize >= len {
return Err(RuntimeError::RecordFieldOffsetOutOfRange { offset, len });
}
note_record_mutation(&record);
let Some(fields) = record.record_make_mut() else {
return Err(RuntimeError::NotARecord(type_name(&record)));
};
#[expect(
clippy::indexing_slicing,
reason = "offset validated against len above"
)]
{
fields[offset as usize] = value;
}
flow.value_stack.push(record);
Ok(())
}
#[cfg(feature = "bench-counters")]
#[inline]
fn note_record_mutation(record: &Value) {
if let Value::Record { fields, .. } = record
&& alloc::sync::Arc::strong_count(fields) > 1
{
crate::bench_counters::record_cow_copy();
}
}
#[cfg(not(feature = "bench-counters"))]
#[inline(always)]
fn note_record_mutation(_record: &Value) {}
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 field_index(program: &Program, record: &Value, name: NameId) -> Result<usize, RuntimeError> {
let Some((shape, _)) = record.as_record() else {
return Err(RuntimeError::NotARecord(type_name(record)));
};
let entry = program
.struct_shape(shape)
.ok_or(RuntimeError::InvalidShapeId(shape.0))?;
entry.fields.iter().position(|&f| f == name).ok_or_else(|| {
let display = program
.name_table
.get(name.0 as usize)
.cloned()
.unwrap_or_else(|| format!("<name#{}>", name.0));
RuntimeError::RecordFieldNotFound(display)
})
}
fn read_field<'a>(
program: &Program,
record: &'a Value,
name: NameId,
) -> Result<&'a Value, RuntimeError> {
let idx = field_index(program, record, name)?;
let Some((_, fields)) = record.as_record() else {
return Err(RuntimeError::NotARecord(type_name(record)));
};
#[expect(
clippy::indexing_slicing,
reason = "index validated by field_index above"
)]
Ok(&fields[idx])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::OutputBuffer;
use crate::program::{LinkedContainer, StructShapeEntry};
use crate::story::PendingTerminal;
use brink_format::{CountingFlags, DefinitionId, DefinitionTag};
use std::collections::HashMap;
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(),
exec_mode: crate::story::ExecMode::default(),
pure_callback: crate::story::PureCallbackState::default(),
next_block_id: 0,
pending_terminal: PendingTerminal::default(),
}
}
fn point_program() -> Program {
Program {
containers: vec![LinkedContainer {
id: DefinitionId::new(DefinitionTag::Address, 0),
bytecode: vec![],
counting_flags: CountingFlags::empty(),
path_hash: 0,
param_count: 0,
params: Vec::new(),
scope_table_idx: 0,
}],
address_map: HashMap::new(),
scope_ids: vec![DefinitionId::new(DefinitionTag::Address, 0)],
source_checksum: 0,
globals: vec![],
global_map: HashMap::new(),
name_table: vec!["x".to_string(), "y".to_string()],
address_by_path: HashMap::new(),
root_idx: 0,
list_literals: vec![],
literal_pool: vec![],
list_item_map: HashMap::new(),
list_defs: vec![],
list_def_map: HashMap::new(),
external_fns: HashMap::new(),
local_scope_defaults: Vec::new(),
struct_shapes: vec![StructShapeEntry {
name: NameId(0),
fields: vec![NameId(0), NameId(1)],
}],
private_defs: Vec::new(),
alias_table: Vec::new(),
}
}
fn push_args(flow: &mut Flow, args: Vec<Value>) {
for v in args {
flow.value_stack.push(v);
}
}
#[test]
fn record_new_constructs_in_shape_order() {
let program = point_program();
let mut flow = test_flow();
push_args(&mut flow, vec![Value::Float(1.0), Value::Float(2.0)]);
record_new(&mut flow, &program, 0).unwrap();
let result = flow.pop_value().unwrap();
let (shape, fields) = result.as_record().unwrap();
assert_eq!(shape, ShapeId(0));
assert_eq!(fields.as_slice(), &[Value::Float(1.0), Value::Float(2.0)]);
}
#[test]
fn record_new_unknown_shape_faults() {
let program = point_program();
let mut flow = test_flow();
let err = record_new(&mut flow, &program, 99).unwrap_err();
assert_eq!(err, RuntimeError::InvalidShapeId(99));
}
#[test]
fn record_get_dyn_reads_field_by_name() {
let program = point_program();
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record]);
record_get_dyn(&mut flow, &program, 1).unwrap(); assert_eq!(flow.pop_value().unwrap(), Value::Float(2.0));
}
#[test]
fn record_get_dyn_missing_field_faults() {
let program = point_program();
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record]);
let err = record_get_dyn(&mut flow, &program, 5).unwrap_err();
assert!(matches!(err, RuntimeError::RecordFieldNotFound(_)));
}
#[test]
fn record_get_dyn_non_record_faults() {
let program = point_program();
let mut flow = test_flow();
push_args(&mut flow, vec![Value::Int(1)]);
let err = record_get_dyn(&mut flow, &program, 0).unwrap_err();
assert_eq!(err, RuntimeError::NotARecord("int"));
}
#[test]
fn record_set_dyn_writes_field_by_name() {
let program = point_program();
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record, Value::Float(9.0)]);
record_set_dyn(&mut flow, &program, 1).unwrap(); let result = flow.pop_value().unwrap();
let (_, fields) = result.as_record().unwrap();
assert_eq!(fields.as_slice(), &[Value::Float(1.0), Value::Float(9.0)]);
}
#[test]
fn record_set_dyn_missing_field_faults() {
let program = point_program();
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record, Value::Float(9.0)]);
let err = record_set_dyn(&mut flow, &program, 7).unwrap_err();
assert!(matches!(err, RuntimeError::RecordFieldNotFound(_)));
}
#[test]
fn record_get_reads_field_by_offset() {
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record]);
record_get(&mut flow, 1).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::Float(2.0));
}
#[test]
fn record_get_out_of_range_offset_faults() {
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record]);
let err = record_get(&mut flow, 5).unwrap_err();
assert_eq!(
err,
RuntimeError::RecordFieldOffsetOutOfRange { offset: 5, len: 2 }
);
}
#[test]
fn record_get_non_record_faults() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::Int(1)]);
let err = record_get(&mut flow, 0).unwrap_err();
assert_eq!(err, RuntimeError::NotARecord("int"));
}
#[test]
fn record_set_writes_field_by_offset() {
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record, Value::Float(9.0)]);
record_set(&mut flow, 1).unwrap();
let result = flow.pop_value().unwrap();
let (_, fields) = result.as_record().unwrap();
assert_eq!(fields.as_slice(), &[Value::Float(1.0), Value::Float(9.0)]);
}
#[test]
fn record_set_out_of_range_offset_faults() {
let mut flow = test_flow();
let record = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
push_args(&mut flow, vec![record, Value::Float(9.0)]);
let err = record_set(&mut flow, 7).unwrap_err();
assert_eq!(
err,
RuntimeError::RecordFieldOffsetOutOfRange { offset: 7, len: 2 }
);
}
#[test]
fn record_set_non_record_faults() {
let mut flow = test_flow();
push_args(&mut flow, vec![Value::Int(1), Value::Float(9.0)]);
let err = record_set(&mut flow, 0).unwrap_err();
assert_eq!(err, RuntimeError::NotARecord("int"));
}
#[test]
fn record_set_cows_when_shared() {
let original = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
let snapshot = original.clone();
let mut flow = test_flow();
push_args(&mut flow, vec![original, Value::Float(9.0)]);
record_set(&mut flow, 0).unwrap();
let mutated = flow.pop_value().unwrap();
assert_eq!(
snapshot,
Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]),
"snapshot unmutated"
);
assert_eq!(
mutated,
Value::record(ShapeId(0), vec![Value::Float(9.0), Value::Float(2.0)])
);
}
#[test]
fn record_set_dyn_cows_when_shared() {
let program = point_program();
let original = Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]);
let snapshot = original.clone();
let mut flow = test_flow();
push_args(&mut flow, vec![original, Value::Float(9.0)]);
record_set_dyn(&mut flow, &program, 0).unwrap(); let mutated = flow.pop_value().unwrap();
assert_eq!(
snapshot,
Value::record(ShapeId(0), vec![Value::Float(1.0), Value::Float(2.0)]),
"snapshot unmutated"
);
assert_eq!(
mutated,
Value::record(ShapeId(0), vec![Value::Float(9.0), Value::Float(2.0)])
);
}
#[test]
fn record_equality_requires_matching_shape() {
let a = Value::record(ShapeId(0), vec![Value::Int(1)]);
let b = Value::record(ShapeId(1), vec![Value::Int(1)]);
assert_ne!(a, b, "same fields, different shape must not be equal");
let c = Value::record(ShapeId(0), vec![Value::Int(1)]);
assert_eq!(a, c);
}
}