#![allow(clippy::panic, clippy::unwrap_used)]
use std::collections::HashMap;
use brink_compiler::{AnalysisOptions, Dialect, TypePolicy};
use brink_format::{Opcode, StoryData, Value};
use brink_runtime::{DotNetRng, RuntimeError, Story};
fn compile_mem(
source: &str,
dialect: Dialect,
types: TypePolicy,
) -> Result<StoryData, brink_compiler::CompileError> {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let options = AnalysisOptions {
dialect,
types: Some(types),
..AnalysisOptions::default()
};
brink_compiler::compile_with_options(
"main.ink",
|path| {
files.get(path).map(|s| (*s).to_string()).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("file not found: {path}"),
)
})
},
options,
)
.map(|output| output.data)
}
fn story_for(data: &StoryData) -> Story<DotNetRng> {
let (program, line_tables) = brink_runtime::link(data).unwrap();
Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables)
}
fn run_to_text(story: &mut Story<DotNetRng>) -> String {
let lines = story.continue_maximally().unwrap();
let mut out = String::new();
for line in &lines {
out.push_str(line.text());
}
out
}
fn all_opcodes(data: &StoryData) -> Vec<Opcode> {
let mut ops = Vec::new();
for c in &data.containers {
let mut offset = 0;
while offset < c.bytecode.len() {
match Opcode::decode(&c.bytecode, &mut offset) {
Ok(op) => ops.push(op),
Err(_) => break,
}
}
}
ops
}
const POINT_SRC: &str = "STRUCT Point = #{x: float, y: float}\n";
#[test]
fn construction_read_and_write_run_end_to_end() {
let src = format!(
"{POINT_SRC}VAR p = 0\n~ {{\n p = Point#{{x: 1.0, y: 2.0}}\n p.x = 9.0\n}}\n\
{{p.x}} {{p.y}}\n-> DONE\n"
);
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let text = run_to_text(&mut story);
assert_eq!(text.trim(), "9 2");
}
#[test]
fn struct_shapes_table_is_populated_and_ordered_by_declaration() {
let src = "STRUCT A = #{v: int}\nSTRUCT B = #{v: int}\nSTRUCT C = #{v: int}\n\
~ temp a = A#{v: 1}\nHello.\n-> DONE\n";
let data = compile_mem(src, Dialect::Brink, TypePolicy::Gradual).unwrap();
assert_eq!(data.struct_shapes.len(), 3);
let names: Vec<&str> = {
let mut by_id: Vec<_> = data.struct_shapes.iter().collect();
by_id.sort_by_key(|s| s.id.0);
by_id
.iter()
.map(|s| data.name_table[s.name.0 as usize].as_str())
.collect()
};
assert_eq!(names, ["A", "B", "C"], "shape ids follow declaration order");
}
#[test]
fn nested_struct_in_array_reads_back_correctly() {
let src = format!(
"{POINT_SRC}VAR pts = 0\n~ pts = #[Point#{{x: 1.0, y: 2.0}}, Point#{{x: 3.0, y: 4.0}}]\n\
{{pts[1].x}}\n-> DONE\n"
);
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let text = run_to_text(&mut story);
assert_eq!(text.trim(), "3");
}
#[test]
fn cow_sharing_law_temp_copy_does_not_mutate_source() {
let src = format!(
"{POINT_SRC}VAR p = 0\n~ p = Point#{{x: 1.0, y: 2.0}}\n~ temp q = p\n~ q.x = 9.0\n\
{{p.x}} {{q.x}}\n-> DONE\n"
);
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let text = run_to_text(&mut story);
assert_eq!(
text.trim(),
"1 9",
"mutating q must never affect p (value semantics)"
);
}
#[test]
fn strict_and_gradual_produce_equivalent_output_for_well_formed_program() {
let src = format!(
"{POINT_SRC}VAR p: Point = Point#{{x: 0.0, y: 0.0}}\n~ p = Point#{{x: 1.0, y: 2.0}}\n\
~ p.y = 5.0\n{{p.x}} {{p.y}}\n-> DONE\n"
);
let gradual = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let strict = compile_mem(&src, Dialect::Brink, TypePolicy::Strict).unwrap();
let gradual_text = run_to_text(&mut story_for(&gradual));
let strict_text = run_to_text(&mut story_for(&strict));
assert_eq!(gradual_text, strict_text);
assert_eq!(gradual_text.trim(), "1 5");
let gradual_ops = all_opcodes(&gradual);
let strict_ops = all_opcodes(&strict);
assert!(
gradual_ops
.iter()
.any(|op| matches!(op, Opcode::RecordGetDyn(_) | Opcode::RecordSetDyn(_))),
"gradual should use by-name field ops"
);
assert!(
!gradual_ops
.iter()
.any(|op| matches!(op, Opcode::RecordGet(_) | Opcode::RecordSet(_))),
"gradual must never emit static-offset ops, even with an annotation"
);
assert!(
strict_ops
.iter()
.any(|op| matches!(op, Opcode::RecordGet(_) | Opcode::RecordSet(_))),
"strict + known shape should use static-offset field ops"
);
}
#[test]
fn gradual_construction_field_mismatch_faults_at_runtime() {
let src = format!("{POINT_SRC}~ temp p = Point#{{x: 1.0}}\nHello.\n-> DONE\n");
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let err = story.continue_maximally().unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidShapeId(_)),
"expected a turn-terminating construction fault, got {err:?}"
);
}
#[test]
fn strict_construction_field_mismatch_is_a_compile_error() {
let src = format!("{POINT_SRC}~ temp p = Point#{{x: 1.0}}\nHello.\n-> DONE\n");
let result = compile_mem(&src, Dialect::Brink, TypePolicy::Strict);
assert!(
result.is_err(),
"missing field under strict must be a compile error (E069)"
);
}
#[test]
fn gradual_construction_extra_field_faults_at_runtime() {
let src = format!("{POINT_SRC}~ temp p = Point#{{x: 1.0, y: 2.0, z: 3.0}}\nHello.\n-> DONE\n");
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let err = story.continue_maximally().unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidShapeId(_)),
"expected a turn-terminating construction fault, got {err:?}"
);
}
#[test]
fn strict_construction_extra_field_is_a_compile_error() {
let src = format!("{POINT_SRC}~ temp p = Point#{{x: 1.0, y: 2.0, z: 3.0}}\nHello.\n-> DONE\n");
let result = compile_mem(&src, Dialect::Brink, TypePolicy::Strict);
assert!(
result.is_err(),
"extra field under strict must be a compile error (E070)"
);
}
#[test]
fn save_state_round_trips_a_struct_valued_global() {
let src = format!("{POINT_SRC}VAR p = 0\n~ p = Point#{{x: 1.0, y: 2.0}}\nHello.\n-> DONE\n");
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let _ = story.continue_maximally().unwrap();
let save = story.save_state();
let record = save
.globals
.get("p")
.expect("the struct-valued global `p` should be in the save");
assert!(matches!(record, Value::Record { .. }));
let json = serde_json::to_string(&save).unwrap();
let restored: brink_format::SaveState = serde_json::from_str(&json).unwrap();
assert_eq!(restored, save);
let mut story2 = story_for(&data);
let report = story2.load_state(&restored);
assert!(report.unknown_globals.is_empty());
assert_eq!(story2.variable("p"), Some(record));
}
fn rmw_sugar_output(new_y: f64) -> String {
let src = format!(
"{POINT_SRC}VAR p = 0\n~ p = Point#{{x: 1.0, y: 2.0}}\n~ p.y = {new_y}\n{{p.x}} {{p.y}}\n-> DONE\n"
);
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
run_to_text(&mut story_for(&data))
}
fn rmw_manual_output(new_y: f64) -> String {
let src = format!(
"{POINT_SRC}VAR p = 0\n~ p = Point#{{x: 1.0, y: 2.0}}\n\
~ temp old_x = p.x\n~ p = Point#{{x: old_x, y: {new_y}}}\n{{p.x}} {{p.y}}\n-> DONE\n"
);
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
run_to_text(&mut story_for(&data))
}
#[test]
fn single_level_field_write_is_observably_equivalent_to_manual_read_modify_write() {
for new_y in [0.0, 1.0, -3.5, 42.0, 100.25] {
let sugar = rmw_sugar_output(new_y);
let manual = rmw_manual_output(new_y);
assert_eq!(
sugar, manual,
"p.y = {new_y} must match the manual take/reconstruct/write-back reference"
);
}
}
#[test]
fn construction_literal_initializers_evaluate_in_source_order() {
let src = format!(
"VAR log = \"\"\n{POINT_SRC}\
~ temp p = Point#{{y: mark_y(), x: mark_x()}}\n\
{{log}}\n-> DONE\n\n\
=== function mark_y() ===\n~ log = log + \"y\"\n~ return 2.0\n\n\
=== function mark_x() ===\n~ log = log + \"x\"\n~ return 1.0\n"
);
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let text = run_to_text(&mut story);
assert_eq!(
text.trim(),
"yx",
"y is written first in source, so mark_y() must fire before mark_x() \
even though x is declared first in Point's shape"
);
}
#[test]
fn construction_literal_values_land_at_the_right_field_despite_source_reordering() {
let src = format!("{POINT_SRC}~ temp p = Point#{{y: 2.0, x: 1.0}}\n{{p.x}} {{p.y}}\n-> DONE\n");
let data = compile_mem(&src, Dialect::Brink, TypePolicy::Gradual).unwrap();
let mut story = story_for(&data);
let text = run_to_text(&mut story);
assert_eq!(text.trim(), "1 2");
}