#![allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
use std::collections::HashMap;
use std::sync::Arc;
use brink_compiler::{AnalysisOptions, Dialect, TypePolicy};
use brink_ir::DiagnosticCode;
use brink_runtime::{DotNetRng, Step, Story};
fn compile_brink(
source: &str,
types: Option<TypePolicy>,
) -> Result<brink_compiler::CompileOutput, brink_compiler::CompileError> {
compile_in(source, Dialect::Brink, types)
}
fn compile_in(
source: &str,
dialect: Dialect,
types: Option<TypePolicy>,
) -> Result<brink_compiler::CompileOutput, brink_compiler::CompileError> {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let options = AnalysisOptions {
dialect,
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,
)
}
fn diagnostics_of(err: brink_compiler::CompileError) -> Vec<brink_compiler::ResolvedDiagnostic> {
match err {
brink_compiler::CompileError::Diagnostics(diags) => diags,
other => panic!("expected Diagnostics error, got {other:?}"),
}
}
fn has_code(diags: &[brink_compiler::ResolvedDiagnostic], code: DiagnosticCode) -> bool {
diags.iter().any(|d| d.code == code)
}
fn run(source: &str) -> String {
let output = compile_brink(source, Some(TypePolicy::Gradual)).expect("compile");
let (program, line_tables) = brink_runtime::link(&output.data).expect("link");
let mut story = Story::<DotNetRng>::new(Arc::new(program), line_tables);
let mut out = String::new();
loop {
match story.continue_single().expect("no runtime fault") {
Step::Line(line) => out.push_str(&line.text),
Step::Choices(_) => panic!("these programs are choice-free"),
Step::Done | Step::End | Step::Suspended => break,
}
}
out
}
fn run_expecting_fault(source: &str) -> String {
let output = compile_brink(source, Some(TypePolicy::Gradual)).expect("compile");
let (program, line_tables) = brink_runtime::link(&output.data).expect("link");
let mut story = Story::<DotNetRng>::new(Arc::new(program), line_tables);
loop {
match story.continue_single() {
Ok(Step::Line(_)) => {}
Ok(other) => panic!("expected a runtime fault, story finished: {other:?}"),
Err(e) => return e.to_string(),
}
}
}
fn arity_warning(source: &str) -> String {
let out = compile_brink(source, Some(TypePolicy::Gradual))
.expect("wrong arity is a warning, not a compile error");
let Some(d) = out.warnings.iter().find(|d| d.code == DiagnosticCode::E031) else {
panic!("expected E031, got {:?}", out.warnings)
};
d.message.clone()
}
#[test]
fn map_arity_mismatch_is_e031() {
let message = arity_warning("~ temp a = #[1]\n~ temp b = map(a)\n{b}\n-> END\n");
assert!(message.contains("`map` expects 2 argument(s)"), "{message}");
}
#[test]
fn filter_arity_mismatch_is_e031() {
let message = arity_warning("~ temp a = #[1]\n~ temp b = filter(a)\n{b}\n-> END\n");
assert!(
message.contains("`filter` expects 2 argument(s)"),
"{message}"
);
}
#[test]
fn fold_arity_mismatch_is_e031() {
let message = arity_warning(
"~ temp a = #[1]\n~ temp b = fold(a, 0)\n{b}\n-> END\n\n=== function add(x, y) ===\n~ return x + y\n",
);
assert!(
message.contains("`fold` expects 3 argument(s)"),
"{message}"
);
}
#[test]
fn filter_map_arity_mismatch_is_e031() {
let message = arity_warning("~ temp a = #[1]\n~ temp b = filter_map(a)\n{b}\n-> END\n");
assert!(
message.contains("`filter_map` expects 2 argument(s)"),
"{message}"
);
}
#[test]
fn each_arity_mismatch_is_e031() {
let message = arity_warning("~ temp a = #[1]\n~ each(a)\n-> END\n");
assert!(
message.contains("`each` expects 2 argument(s)"),
"{message}"
);
}
#[test]
fn map_each_arity_mismatch_is_e031() {
let message = arity_warning("~ temp a = #[1]\n~ temp b = map_each(a)\n{b}\n-> END\n");
assert!(
message.contains("`map_each` expects 2 argument(s)"),
"{message}"
);
}
#[test]
fn writing_map_callback_is_e119() {
let source = "VAR seen = 0\n~ temp a = #[1]\n~ temp b = map(a, #fn(spy))\n{b}\n-> END\n\n=== function spy(n: int): int ===\n~ seen = seen + 1\n~ return n\n";
let diags = diagnostics_of(compile_brink(source, Some(TypePolicy::Gradual)).unwrap_err());
assert!(has_code(&diags, DiagnosticCode::E119), "{diags:?}");
}
#[test]
fn emitting_filter_callback_is_e119() {
let source = "~ temp a = #[1]\n~ temp b = filter(a, #fn(loud))\n{b}\n-> END\n\n=== function loud(n: int): bool ===\nchecking!\n~ return true\n";
let diags = diagnostics_of(compile_brink(source, Some(TypePolicy::Gradual)).unwrap_err());
assert!(has_code(&diags, DiagnosticCode::E119), "{diags:?}");
}
#[test]
fn writing_fold_callback_is_e119() {
let source = "VAR seen = 0\n~ temp a = #[1]\n~ temp b = fold(a, 0, #fn(spy))\n{b}\n-> END\n\n=== function spy(acc: int, n: int): int ===\n~ seen = seen + 1\n~ return acc + n\n";
let diags = diagnostics_of(compile_brink(source, Some(TypePolicy::Gradual)).unwrap_err());
assert!(has_code(&diags, DiagnosticCode::E119), "{diags:?}");
}
#[test]
fn writing_filter_map_callback_is_e119() {
let source = "VAR seen = 0\n~ temp a = #[1]\n~ temp b = filter_map(a, #fn(spy))\n{b}\n-> END\n\n=== function spy(n: int) ===\n~ seen = seen + 1\n~ return some(n)\n";
let diags = diagnostics_of(compile_brink(source, Some(TypePolicy::Gradual)).unwrap_err());
assert!(has_code(&diags, DiagnosticCode::E119), "{diags:?}");
}
#[test]
fn trio_e119_message_names_the_callback_not_a_comparator() {
let source = "VAR seen = 0\n~ temp a = #[1]\n~ temp b = map(a, #fn(spy))\n{b}\n-> END\n\n=== function spy(n: int): int ===\n~ seen = seen + 1\n~ return n\n";
let diags = diagnostics_of(compile_brink(source, Some(TypePolicy::Gradual)).unwrap_err());
let message = diags
.iter()
.find(|d| d.code == DiagnosticCode::E119)
.map(|d| d.message.clone())
.expect("an E119");
assert!(message.contains("`map`'s callback"), "{message}");
assert!(!message.contains("comparator"), "{message}");
}
#[test]
fn e119_message_points_at_the_shipped_effectful_exit() {
let source = "VAR seen = 0\n~ temp a = #[1]\n~ temp b = map(a, #fn(spy))\n{b}\n-> END\n\n=== function spy(n: int): int ===\n~ seen = seen + 1\n~ return n\n";
let diags = diagnostics_of(compile_brink(source, Some(TypePolicy::Gradual)).unwrap_err());
let message = diags
.iter()
.find(|d| d.code == DiagnosticCode::E119)
.map(|d| d.message.clone())
.expect("an E119");
assert!(message.contains("each"), "{message}");
assert!(message.contains("map_each"), "{message}");
assert!(!message.contains("not shipped yet"), "{message}");
}
#[test]
fn each_and_map_each_are_not_e119_gated() {
let source = "VAR seen = 0\n~ temp a = #[1, 2]\n~ each(a, #fn(spy))\n{seen}\n-> END\n\n=== function spy(n) ===\n~ seen = seen + n\n~ return n\n";
compile_brink(source, Some(TypePolicy::Gradual))
.expect("each's callback must not be E119-gated even though it writes a global");
let source = "VAR seen = 0\n~ temp a = #[1, 2]\n~ temp b = map_each(a, #fn(spy))\n{seen}\n-> END\n\n=== function spy(n) ===\n~ seen = seen + n\n~ return n * 10\n";
compile_brink(source, Some(TypePolicy::Gradual))
.expect("map_each's callback must not be E119-gated either");
}
#[test]
fn pure_callbacks_pass_the_gate() {
let source = "~ temp a = #[1, 2]\n{map(a, #fn(double))}\n{filter(a, #fn(is_even))}\n{fold(a, 0, #fn(add))}\n{filter_map(a, #fn(keep_all))}\n-> END\n\n=== function double(n: int): int ===\n~ return n * 2\n\n=== function is_even(n: int): bool ===\n~ return n % 2 == 0\n\n=== function add(acc: int, n: int): int ===\n~ return acc + n\n\n=== function keep_all(n: int) ===\n~ return some(n)\n";
compile_brink(source, None).expect("pure callbacks must pass E119 under the strict default");
}
#[test]
fn opaque_callback_is_not_proven_and_passes() {
let source = "VAR seen = 0\n~ temp a = #[1]\n~ temp f = #fn(spy)\n~ temp b = map(a, f)\n{b}\n-> END\n\n=== function spy(n: int): int ===\n~ seen = seen + 1\n~ return n\n";
compile_brink(source, Some(TypePolicy::Gradual))
.expect("an opaque callback is not provably in violation");
}
#[test]
fn writing_map_callback_directly_in_var_initializer_is_e119() {
let source = "VAR seen = 0\nVAR bad = map(#[1], #fn(spy))\n{bad}\n-> END\n\n=== function spy(n: int): int ===\n~ seen = seen + 1\n~ return n\n";
let diags = diagnostics_of(compile_brink(source, Some(TypePolicy::Gradual)).unwrap_err());
assert!(has_code(&diags, DiagnosticCode::E119), "{diags:?}");
}
fn compile_native(
dir_suffix: &str,
source: &str,
) -> Result<brink_compiler::CompileOutput, brink_compiler::CompileError> {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-seq-verbs-native-{dir_suffix}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp dir");
std::fs::write(dir.join("main.brink"), source).expect("write main.brink");
let options = AnalysisOptions {
dialect: Dialect::Brink,
..AnalysisOptions::default()
};
let result = brink_compiler::compile_path_with_options(&dir.join("main.brink"), options);
std::fs::remove_dir_all(&dir).ok();
result
}
#[test]
fn native_bare_name_writing_map_callback_is_e119() {
let source = "\
var seen = 0
fn spy(n) {
seen = seen + n;
return n;
}
flow main() {
Result: {map([1, 2], spy)} -> END
}
";
let diags = diagnostics_of(compile_native("write", source).unwrap_err());
assert!(has_code(&diags, DiagnosticCode::E119), "{diags:?}");
}
#[test]
fn native_bare_name_pure_callback_passes() {
let source = "\
fn double(n) {
return n * 2;
}
flow main() {
Result: {map([1, 2], double)} -> END
}
";
compile_native("pure", source).expect("a pure native bare-name callback must pass E119");
}
#[test]
fn native_bare_name_opaque_callback_is_not_proven_and_passes() {
let source = "\
var seen = 0
fn spy(n) {
seen = seen + n;
return n;
}
fn indirectly(f: fn(int): int): Array<int> {
return map([1, 2], f);
}
flow main() {
Result: {indirectly(spy)} -> END
}
";
compile_native("opaque", source)
.expect("an opaque callback reached through a param is not provably in violation");
}
#[test]
fn the_trio_runs_end_to_end() {
let source = "~ temp a = #[1, 2, 3]\nmap={map(a, #fn(double))} filter={filter(a, #fn(is_even))} fold={fold(a, 0, #fn(add))}\n-> END\n\n=== function double(n: int): int ===\n~ return n * 2\n\n=== function is_even(n: int): bool ===\n~ return n % 2 == 0\n\n=== function add(acc: int, n: int): int ===\n~ return acc + n\n";
assert_eq!(run(source), "map=[2, 4, 6] filter=[2] fold=6\n");
}
#[test]
fn filter_map_runs_end_to_end() {
let source = "~ temp a = #[1, 2, 3, 4, 5]\n{filter_map(a, #fn(keep_even_doubled))}\n-> END\n\n=== function keep_even_doubled(n: int) ===\n~ {\n if n % 2 == 0 {\n return some(n * 10)\n }\n return none\n}\n";
assert_eq!(run(source), "[20, 40]\n");
}
#[test]
fn each_runs_a_callback_with_legal_world_writes() {
let source = "VAR seen = 0\n~ temp a = #[1, 2, 3]\n~ each(a, #fn(bump))\n{seen}\n-> END\n\n=== function bump(n) ===\n~ seen = seen + n\n";
assert_eq!(run(source), "6\n");
}
#[test]
fn map_each_runs_a_callback_with_legal_world_writes() {
let source = "VAR seen = 0\n~ temp a = #[1, 2, 3]\n~ temp b = map_each(a, #fn(tally))\n{b} seen={seen}\n-> END\n\n=== function tally(n: int): int ===\n~ seen = seen + n\n~ return n * 10\n";
assert_eq!(run(source), "[10, 20, 30] seen=6\n");
}
#[test]
fn each_callback_output_reaches_the_transcript_in_statement_position() {
let source = "~ temp a = #[1, 2, 3]\n~ each(a, #fn(shout))\n-> END\n\n=== function shout(n) ===\nLoud {n}!\n~ return n\n";
assert_eq!(run(source), "Loud 1!Loud 2!Loud 3!\n");
}
#[test]
fn map_each_callback_output_reaches_the_transcript_inside_an_interpolation() {
let source = "~ temp a = #[1, 2]\nresult: {map_each(a, #fn(shout))}\n-> END\n\n=== function shout(n) ===\nLoud {n}!\n~ return n * 10\n";
assert_eq!(run(source), "Loud 1!Loud 2!result: [10, 20]\n");
}
#[test]
fn map_over_a_non_array_faults() {
let source = "~ temp a = 1\n~ temp b = map(a, #fn(double))\n{b}\n-> END\n\n=== function double(n) ===\n~ return n * 2\n";
let message = run_expecting_fault(source);
assert!(message.contains("`map`"), "{message}");
assert!(message.contains("an array"), "{message}");
}
#[test]
fn map_with_a_non_function_callback_faults() {
let source = "~ temp a = #[1]\n~ temp b = map(a, 7)\n{b}\n-> END\n";
let message = run_expecting_fault(source);
assert!(message.contains("`map` callback"), "{message}");
assert!(message.contains("`fn(T): U`"), "{message}");
}
#[test]
fn filter_with_a_non_function_callback_faults() {
let source = "~ temp a = #[1]\n~ temp b = filter(a, 7)\n{b}\n-> END\n";
let message = run_expecting_fault(source);
assert!(message.contains("`filter` callback"), "{message}");
assert!(message.contains("`fn(T): bool`"), "{message}");
}
#[test]
fn fold_with_a_non_function_callback_faults() {
let source = "~ temp a = #[1]\n~ temp b = fold(a, 0, 7)\n{b}\n-> END\n";
let message = run_expecting_fault(source);
assert!(message.contains("`fold` callback"), "{message}");
assert!(message.contains("`fn(U, T): U`"), "{message}");
}
#[test]
fn filter_predicate_returning_a_non_bool_faults() {
let source = "~ temp a = #[1]\n~ temp b = filter(a, #fn(nonsense))\n{b}\n-> END\n\n=== function nonsense(n) ===\n~ return 7\n";
let message = run_expecting_fault(source);
assert!(
message.contains("`filter` callback must return a bool"),
"{message}"
);
}
#[test]
fn dev_mode_world_write_inside_a_map_callback_names_map() {
let source = "VAR seen = 0\n~ temp a = #[1]\n~ temp f = #fn(spy)\n~ temp b = map(a, f)\n{b}\n-> END\n\n=== function spy(n) ===\n~ seen = seen + 1\n~ return n\n";
let message = run_expecting_fault(source);
assert!(message.contains("`map`"), "{message}");
assert!(!message.contains("sort_by"), "{message}");
assert!(!message.contains("comparator"), "{message}");
}
#[test]
fn filter_map_over_a_non_array_faults() {
let source = "~ temp a = 1\n~ temp b = filter_map(a, #fn(keep))\n{b}\n-> END\n\n=== function keep(n) ===\n~ return some(n)\n";
let message = run_expecting_fault(source);
assert!(message.contains("`filter_map`"), "{message}");
assert!(message.contains("an array"), "{message}");
}
#[test]
fn filter_map_with_a_non_function_callback_faults() {
let source = "~ temp a = #[1]\n~ temp b = filter_map(a, 7)\n{b}\n-> END\n";
let message = run_expecting_fault(source);
assert!(message.contains("`filter_map` callback"), "{message}");
assert!(message.contains("`fn(T): Option[U]`"), "{message}");
}
#[test]
fn filter_map_callback_returning_a_non_option_faults() {
let source = "~ temp a = #[1]\n~ temp b = filter_map(a, #fn(nonsense))\n{b}\n-> END\n\n=== function nonsense(n) ===\n~ return 7\n";
let message = run_expecting_fault(source);
assert!(
message.contains("`filter_map` callback must return an Option"),
"{message}"
);
}
#[test]
fn each_over_a_non_array_faults() {
let source =
"~ temp a = 1\n~ each(a, #fn(bump))\n-> END\n\n=== function bump(n) ===\n~ return n\n";
let message = run_expecting_fault(source);
assert!(message.contains("`each`"), "{message}");
assert!(message.contains("an array"), "{message}");
}
#[test]
fn each_with_a_non_function_callback_faults() {
let source = "~ temp a = #[1]\n~ each(a, 7)\n-> END\n";
let message = run_expecting_fault(source);
assert!(message.contains("`each` callback"), "{message}");
assert!(message.contains("`fn(T)`"), "{message}");
}
#[test]
fn map_each_over_a_non_array_faults() {
let source = "~ temp a = 1\n~ temp b = map_each(a, #fn(bump))\n{b}\n-> END\n\n=== function bump(n) ===\n~ return n\n";
let message = run_expecting_fault(source);
assert!(message.contains("`map_each`"), "{message}");
assert!(message.contains("an array"), "{message}");
}
#[test]
fn map_each_with_a_non_function_callback_faults() {
let source = "~ temp a = #[1]\n~ temp b = map_each(a, 7)\n{b}\n-> END\n";
let message = run_expecting_fault(source);
assert!(message.contains("`map_each` callback"), "{message}");
assert!(message.contains("`fn(T): U`"), "{message}");
}
#[test]
fn each_callback_reaching_end_still_escapes() {
let source =
"~ temp a = #[1]\n~ each(a, #fn(leaves))\n-> END\n\n=== function leaves(n) ===\n-> END\n";
let message = run_expecting_fault(source);
assert!(message.contains("`each`"), "{message}");
assert!(message.contains("DONE"), "{message}");
}
#[test]
fn strict_ink_never_reaches_the_fn_value_verbs() {
let source = "~ temp a = #[1]\n~ temp b = map(a, #fn(double))\n{b}\n-> END\n\n=== function double(n) ===\n~ return n * 2\n";
let diags = diagnostics_of(
compile_in(source, Dialect::StrictInk, None)
.expect_err("strict-ink must reject the brink verb surface"),
);
assert!(
diags
.iter()
.any(|d| d.message.contains("`map` stdlib function")),
"expected a dialect-gate diagnostic naming the `map` call, got {diags:?}"
);
}
#[test]
fn call_site_bare_list_item_collides_with_stdlib_verb_name() {
let source =
"VAR arr = #[]\nLIST Verbs = push, pull\n~ push(arr, 5)\nlen is {len(arr)}\n-> END\n";
assert_eq!(run(source), "len is 1\n");
}