use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use brink_format::{DefinitionId, NameId, ProjSegment, ShapeId, Value};
use crate::collection_ops;
use crate::error::RuntimeError;
use crate::program::Program;
use crate::state::ContextAccess;
pub(crate) fn read<C: ContextAccess + ?Sized>(
program: &Program,
context: &C,
cell: DefinitionId,
segments: &[ProjSegment],
) -> Result<Value, RuntimeError> {
let idx = resolve_root(program, cell)?;
let mut current = context.global(idx);
for seg in segments {
current = step_read(current, seg, program)?;
}
Ok(current.clone())
}
pub(crate) fn write<C: ContextAccess + ?Sized>(
program: &Program,
context: &mut C,
cell: DefinitionId,
segments: &[ProjSegment],
value: Value,
) -> Result<(), RuntimeError> {
let idx = resolve_root(program, cell)?;
let mut root = context.take_global(idx);
write_recursive(&mut root, segments, value, program)?;
context.set_global(idx, root);
Ok(())
}
pub(crate) fn take<C: ContextAccess + ?Sized>(
program: &Program,
context: &mut C,
cell: DefinitionId,
segments: &[ProjSegment],
) -> Result<Value, RuntimeError> {
let taken = read(program, context, cell, segments)?;
write(program, context, cell, segments, Value::Null)?;
Ok(taken)
}
fn resolve_root(program: &Program, cell: DefinitionId) -> Result<u32, RuntimeError> {
program
.resolve_global(cell)
.ok_or_else(|| RuntimeError::ProjectionInvalidated(format!("root cell {cell} unresolved")))
}
fn seg_as_value(seg: &ProjSegment) -> Value {
match seg {
ProjSegment::Index(n) => Value::Int(*n),
ProjSegment::Key(v) => v.clone(),
}
}
fn seg_as_field_name(seg: &ProjSegment) -> Option<&str> {
match seg {
ProjSegment::Key(Value::String(s)) => Some(s),
_ => None,
}
}
fn resolve_field_offset(program: &Program, shape: ShapeId, name: &str) -> Option<usize> {
let entry = program.struct_shape(shape)?;
entry
.fields
.iter()
.position(|&id: &NameId| program.name_checked(id) == Some(name))
}
fn invalidated(e: &RuntimeError) -> RuntimeError {
RuntimeError::ProjectionInvalidated(alloc::string::ToString::to_string(e))
}
fn field_not_found_fault(name: &str) -> RuntimeError {
RuntimeError::ProjectionInvalidated(format!("struct has no field {name:?}"))
}
fn not_a_struct_fault(seg: &ProjSegment) -> RuntimeError {
RuntimeError::ProjectionInvalidated(format!(
"field-access segment against a non-struct value: {}",
describe_segment(seg)
))
}
fn describe_segment(seg: &ProjSegment) -> String {
match seg {
ProjSegment::Index(n) => format!("[{n}]"),
ProjSegment::Key(v) => format!("[{v:?}]"),
}
}
fn step_read<'v>(
current: &'v Value,
seg: &ProjSegment,
program: &Program,
) -> Result<&'v Value, RuntimeError> {
match current {
Value::Record { shape, fields } => {
let name = seg_as_field_name(seg).ok_or_else(|| not_a_struct_fault(seg))?;
let offset = resolve_field_offset(program, *shape, name)
.ok_or_else(|| field_not_found_fault(name))?;
fields
.get(offset)
.ok_or_else(|| field_not_found_fault(name))
}
Value::Array(_) | Value::Map(_) => {
collection_ops::read_index(current, &seg_as_value(seg)).map_err(|e| invalidated(&e))
}
other => Err(RuntimeError::ProjectionInvalidated(format!(
"cannot index into a {} value",
value_kind(other)
))),
}
}
fn write_recursive(
current: &mut Value,
segments: &[ProjSegment],
value: Value,
program: &Program,
) -> Result<(), RuntimeError> {
let Some((seg, rest)) = segments.split_first() else {
*current = value;
return Ok(());
};
if rest.is_empty() {
return set_segment(current, seg, value, program);
}
match current {
Value::Record { shape, .. } => {
let name = seg_as_field_name(seg)
.ok_or_else(|| not_a_struct_fault(seg))?
.to_owned();
let offset = resolve_field_offset(program, *shape, &name)
.ok_or_else(|| field_not_found_fault(&name))?;
let fields = current
.record_make_mut()
.ok_or_else(|| field_not_found_fault(&name))?;
let elem = fields
.get_mut(offset)
.ok_or_else(|| field_not_found_fault(&name))?;
write_recursive(elem, rest, value, program)
}
Value::Array(_) => {
let len = current.as_array().map_or(0, |items| items.len());
let idx = array_index_of(seg, len)?;
let items = current
.array_make_mut()
.ok_or_else(|| RuntimeError::ProjectionInvalidated("not an array".into()))?;
let elem = items.get_mut(idx).ok_or_else(|| {
RuntimeError::ProjectionInvalidated("array index vanished".into())
})?;
write_recursive(elem, rest, value, program)
}
Value::Map(_) => {
let key = brink_format::MapKey::from_value(&seg_as_value(seg)).ok_or_else(|| {
RuntimeError::ProjectionInvalidated("map key is not int/string/bool".into())
})?;
let has_key = current.as_map().is_some_and(|m| m.contains_key(&key));
if !has_key {
return Err(RuntimeError::ProjectionInvalidated(format!(
"map has no key {key:?}"
)));
}
let map = current
.map_make_mut()
.ok_or_else(|| RuntimeError::ProjectionInvalidated("not a map".into()))?;
let elem = map
.get_mut(&key)
.ok_or_else(|| RuntimeError::ProjectionInvalidated("map key vanished".into()))?;
write_recursive(elem, rest, value, program)
}
other => Err(RuntimeError::ProjectionInvalidated(format!(
"cannot index into a {} value",
value_kind(other)
))),
}
}
fn set_segment(
current: &mut Value,
seg: &ProjSegment,
value: Value,
program: &Program,
) -> Result<(), RuntimeError> {
match current {
Value::Record { shape, .. } => {
let name = seg_as_field_name(seg)
.ok_or_else(|| not_a_struct_fault(seg))?
.to_owned();
let offset = resolve_field_offset(program, *shape, &name)
.ok_or_else(|| field_not_found_fault(&name))?;
let fields = current
.record_make_mut()
.ok_or_else(|| field_not_found_fault(&name))?;
let slot = fields
.get_mut(offset)
.ok_or_else(|| field_not_found_fault(&name))?;
*slot = value;
Ok(())
}
Value::Array(_) | Value::Map(_) => {
collection_ops::write_index(current, &seg_as_value(seg), value)
.map_err(|e| invalidated(&e))
}
other => Err(RuntimeError::ProjectionInvalidated(format!(
"cannot index into a {} value",
value_kind(other)
))),
}
}
fn array_index_of(seg: &ProjSegment, len: usize) -> Result<usize, RuntimeError> {
let ProjSegment::Index(i) = seg else {
return Err(RuntimeError::ProjectionInvalidated(format!(
"array index segment must be an int, got {}",
describe_segment(seg)
)));
};
#[expect(clippy::cast_sign_loss)]
if *i < 0 || *i as usize >= len {
Err(RuntimeError::ProjectionInvalidated(format!(
"array index {i} out of bounds (len {len})"
)))
} else {
Ok(*i as usize)
}
}
fn value_kind(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",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::program::{GlobalSlot, LinkedContainer, StructShapeEntry};
use crate::world::{ResolvedPolicy, World};
use alloc::vec;
use brink_format::{CountingFlags, DefinitionTag};
use std::collections::HashMap;
fn global_id(n: u64) -> DefinitionId {
DefinitionId::new(DefinitionTag::GlobalVar, n)
}
fn test_program() -> Program {
let npc_id = global_id(1);
let gold_id = global_id(2);
let mut global_map = HashMap::new();
global_map.insert(npc_id, 0);
global_map.insert(gold_id, 1);
Program {
link: crate::program::LinkTables::default(),
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,
scope_id: DefinitionId::new(DefinitionTag::Address, 0),
}],
address_map: HashMap::new(),
scope_ids: vec![DefinitionId::new(DefinitionTag::Address, 0)],
source_checksum: 0,
globals: vec![
GlobalSlot {
id: npc_id,
name: NameId(0),
default: Value::record(ShapeId(0), vec![Value::Int(10), Value::Null]),
local: false,
},
GlobalSlot {
id: gold_id,
name: NameId(3),
default: Value::Int(5),
local: false,
},
],
global_map,
name_table: vec![
"npc".to_string(),
"hp".to_string(),
"name".to_string(),
"gold".to_string(),
],
address_by_path: HashMap::new(),
container_paths: 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(1), NameId(2)],
}],
private_defs: Vec::new(),
alias_table: Vec::new(),
debug_info: None,
}
}
fn world_with_defaults(program: &Program) -> World {
World::from_globals(program.global_defaults(), ResolvedPolicy::all_world())
}
#[test]
fn read_walks_struct_field() {
let program = test_program();
let world = world_with_defaults(&program);
let cell = global_id(1);
let segments = vec![ProjSegment::Key(Value::String("hp".into()))];
let v = read(&program, &world, cell, &segments).expect("read");
assert_eq!(v, Value::Int(10));
}
#[test]
fn write_updates_struct_field_root_cell() {
let program = test_program();
let mut world = world_with_defaults(&program);
let cell = global_id(1);
let segments = vec![ProjSegment::Key(Value::String("hp".into()))];
write(&program, &mut world, cell, &segments, Value::Int(42)).expect("write");
let v = read(&program, &world, cell, &segments).expect("read back");
assert_eq!(v, Value::Int(42));
}
#[test]
fn write_missing_field_faults_projection_invalidated() {
let program = test_program();
let mut world = world_with_defaults(&program);
let cell = global_id(1);
let segments = vec![ProjSegment::Key(Value::String("mana".into()))];
let err = write(&program, &mut world, cell, &segments, Value::Int(1)).unwrap_err();
assert!(matches!(err, RuntimeError::ProjectionInvalidated(_)));
}
#[test]
fn read_array_index_out_of_bounds_faults() {
let program = test_program();
let mut world = world_with_defaults(&program);
world.set_global(0, Value::array(vec![Value::Int(1), Value::Int(2)]));
let cell = global_id(1);
let segments = vec![ProjSegment::Index(5)];
let err = read(&program, &world, cell, &segments).unwrap_err();
assert!(matches!(err, RuntimeError::ProjectionInvalidated(_)));
}
#[test]
fn take_reads_then_leaves_null() {
let program = test_program();
let mut world = world_with_defaults(&program);
let cell = global_id(2);
let segments: Vec<ProjSegment> = Vec::new();
let taken = take(&program, &mut world, cell, &segments).expect("take");
assert_eq!(taken, Value::Int(5));
assert_eq!(*world.global(1), Value::Null);
}
#[test]
fn overlapping_projections_write_through_immediately() {
let program = test_program();
let mut world = world_with_defaults(&program);
let cell = global_id(1);
let a = vec![ProjSegment::Key(Value::String("hp".into()))];
let b = vec![ProjSegment::Key(Value::String("hp".into()))];
write(&program, &mut world, cell, &a, Value::Int(99)).expect("write via a");
let via_b = read(&program, &world, cell, &b).expect("read via b");
assert_eq!(via_b, Value::Int(99));
}
}