#![allow(
clippy::unwrap_used,
clippy::panic,
clippy::print_stderr,
clippy::wildcard_enum_match_arm,
clippy::match_same_arms
)]
use std::collections::HashMap;
use std::path::Path;
use brink_runtime::{DotNetRng, Step, Story};
fn compile_mem(
entry: &str,
files: &HashMap<&str, &str>,
) -> Result<brink_format::StoryData, brink_compiler::CompileError> {
brink_compiler::compile(entry, |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}"),
)
})
})
.map(|output| output.data)
}
#[test]
fn compile_minimal_story() {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", "Hello, world!\n")]);
let story = compile_mem("main.ink", &files).unwrap();
assert!(
!story.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_story_with_knots() {
let files: HashMap<&str, &str> = HashMap::from([(
"main.ink",
"\
Hello!
-> greet
== greet ==
Welcome to the story.
-> END
",
)]);
let story = compile_mem("main.ink", &files).unwrap();
assert!(
!story.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_follows_includes() {
let files: HashMap<&str, &str> = HashMap::from([
("main.ink", "INCLUDE helpers.ink\nHello!\n-> greet\n"),
("helpers.ink", "== greet ==\nWelcome.\n-> END\n"),
]);
let story = compile_mem("main.ink", &files).unwrap();
assert!(
!story.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_nested_includes() {
let files: HashMap<&str, &str> = HashMap::from([
("main.ink", "INCLUDE a.ink\nMain content.\n"),
("a.ink", "INCLUDE b.ink\n"),
("b.ink", "VAR x = 5\n== knot_b ==\nHello from b.\n-> END\n"),
]);
let story = compile_mem("main.ink", &files).unwrap();
assert!(
!story.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_circular_includes_detected() {
let files: HashMap<&str, &str> = HashMap::from([
("a.ink", "INCLUDE b.ink\nContent A.\n"),
("b.ink", "INCLUDE a.ink\nContent B.\n"),
]);
let err = compile_mem("a.ink", &files).unwrap_err();
assert!(
matches!(err, brink_compiler::CompileError::CircularInclude(_)),
"expected CircularInclude variant, got: {err}"
);
}
#[test]
fn compile_resolves_relative_include_paths() {
let files: HashMap<&str, &str> = HashMap::from([
("src/main.ink", "INCLUDE utils/helpers.ink\nHello!\n"),
("src/utils/helpers.ink", "== greet ==\nHi.\n-> END\n"),
]);
let story = compile_mem("src/main.ink", &files).unwrap();
assert!(
!story.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_missing_entry_file() {
let files: HashMap<&str, &str> = HashMap::new();
let err = compile_mem("nonexistent.ink", &files).unwrap_err();
assert!(
matches!(err, brink_compiler::CompileError::Io(_)),
"expected I/O error for missing entry file, got: {err}"
);
}
#[test]
fn compile_missing_included_file() {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", "INCLUDE missing.ink\nHello!\n")]);
let err = compile_mem("main.ink", &files).unwrap_err();
assert!(
matches!(err, brink_compiler::CompileError::Io(_)),
"expected I/O error for missing included file, got: {err}"
);
}
#[test]
fn compile_bare_include_reports_e037_not_io_error() {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", "INCLUDE\nHello!\n")]);
let err = compile_mem("main.ink", &files).unwrap_err();
assert!(
matches!(err, brink_compiler::CompileError::Diagnostics(_)),
"expected a Diagnostics(E037) compile error, got: {err}"
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E037"),
"expected E037 (expected file path) among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_path_native_single_file_no_brink_toml() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-single-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"flow main() {\n Hello. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let output = result.expect("single-file native project should compile");
assert!(
!output.data.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_path_native_multi_file_no_brink_toml() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-multi-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("story")).unwrap();
std::fs::write(
dir.join("story/main.brink"),
"flow main() {\n Hello. -> END\n}\n",
)
.unwrap();
std::fs::write(
dir.join("story/other.brink"),
"flow other() {\n Hi. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("story/main.brink"));
std::fs::remove_dir_all(&dir).ok();
let output = result.expect("multi-file native project should compile");
assert!(
!output.data.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_path_native_lambda_lifts_to_a_callable_function_value() {
let output = compile_and_run_native(
"lambda-lift",
"fn tally(n: int): int {\n let add = |x| x + 1;\n return add(n);\n}\n\n\
flow main() {\n Tally: {tally(41)} -> END\n}\n",
);
assert!(
output.contains("Tally: 42"),
"the lambda must be lifted to a real function value and invoked, got: {output:?}"
);
}
#[test]
fn compile_path_native_lambda_captures_by_value_at_creation() {
let output = compile_and_run_native(
"lambda-capture",
"fn shifted(): int {\n let step = 1;\n let bump = |x| x + step;\n \
step = 100;\n return bump(5);\n}\n\n\
flow main() {\n Shifted: {shifted()} -> END\n}\n",
);
assert!(
output.contains("Shifted: 6"),
"a capture is a creation-site snapshot, not a live read, got: {output:?}"
);
}
#[test]
fn compile_path_native_lambda_tailless_body_and_transitive_capture() {
let output = compile_and_run_native(
"lambda-edges",
"fn tailless() {\n let f = |x| { return x + 1; };\n return f(41);\n}\n\n\
fn nested() {\n let outer = 10;\n \
let make = |y| { let inner = |z| z + outer; inner(y) };\n return make(5);\n}\n\n\
flow main() {\n Tailless: {tailless()}\n Nested: {nested()} -> END\n}\n",
);
assert!(
output.contains("Tailless: 42"),
"an explicit `return` must leave the lambda, not the enclosing fn, got: {output:?}"
);
assert!(
output.contains("Nested: 15"),
"a nested lambda's read of a two-levels-out local must capture transitively, \
got: {output:?}"
);
}
#[test]
fn compile_path_native_lambda_is_a_legal_verb_callback() {
let output = compile_and_run_native(
"lambda-verb-callback",
"fn doubled() {\n return map([1, 2, 3], |x| x * 2);\n}\n\n\
flow main() {\n Doubled: {doubled()} -> END\n}\n",
);
assert!(
output.contains("Doubled: [2, 4, 6]"),
"a lambda literal must be a legal `map` callback, got: {output:?}"
);
}
#[test]
fn compile_path_native_lambda_self_reference_is_e158() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-lambda-self-ref-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"fn a() {\n let f = |x| {\n if x <= 0 { return 0; }\n \
return f(x - 1) + 1;\n };\n return f(3);\n}\n\n\
flow main() {\n Out: {a()} -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let err = result.expect_err(
"a lambda reading its own not-yet-bound `let` name (recursion) must refuse to \
compile, not silently target the wrong container and fault at runtime",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E158"),
"expected E158 (unliftable lambda capture) among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_path_native_const_lambda_literal_decl_default_compiles() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-lambda-decl-default-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"const twice = |x| x * 2\n\nflow main() {\n Hi. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let output = result.expect("a file-scope const lambda literal must compile (E083 lifted)");
let global = output
.data
.variables
.iter()
.find(|v| output.data.name_table[v.name.0 as usize] == "twice")
.expect("global `twice` must be present in the compiled StoryData");
assert!(
!global.mutable,
"a `const` global stays immutable regardless of its default's kind"
);
let brink_format::Value::FnRef(target) = global.default_value else {
panic!(
"a file-scope lambda has no enclosing frame to capture from, so it \
must fold to a bare FnRef (no bound environment), got {:?}",
global.default_value
);
};
let lifted = output
.data
.containers
.iter()
.find(|c| c.id == target)
.unwrap_or_else(|| {
panic!(
"no compiled container has id {target:?} — the FnRef target \
does not resolve to a real container in StoryData"
)
});
assert_eq!(
lifted.param_count, 1,
"expected `twice`'s one `x` param, got param_count {}",
lifted.param_count
);
assert_eq!(
lifted.params.len(),
1,
"expected `twice`'s one `x` ParamMeta entry, got {} entries",
lifted.params.len()
);
assert_eq!(
output.data.name_table[lifted.params[0].name.0 as usize], "x",
"expected the lifted container's param to be named `x`"
);
}
#[test]
fn compile_path_native_var_lambda_literal_decl_default_compiles() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-lambda-decl-default-var-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"var addOne = |x| x + 1\n\nflow main() {\n Hi. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let output = result.expect("a file-scope var lambda literal must compile (E083 lifted)");
let global = output
.data
.variables
.iter()
.find(|v| output.data.name_table[v.name.0 as usize] == "addOne")
.expect("global `addOne` must be present in the compiled StoryData");
assert!(global.mutable, "a `var` global stays mutable");
assert!(matches!(
global.default_value,
brink_format::Value::FnRef(_)
));
}
#[test]
fn compile_path_native_lambda_valued_global_call_site_resolves() {
let output = compile_and_run_native(
"lambda-decl-default-call-site",
"const twice = |x| x * 2\n\nflow main() {\n Result: {twice(21)} -> END\n}\n",
);
assert!(
output.contains("Result: 42"),
"calling a fn-valued CONST global from flow main should work now that \
issue #2083 is fixed, got: {output:?}"
);
}
#[test]
fn compile_path_native_bare_name_fn_valued_const_global_call_site_resolves() {
let output = compile_and_run_native(
"bare-name-const-call-site",
"fn double(n: int): int {\n return n * 2;\n}\n\nconst twice = double\n\n\
flow main() {\n Result: {twice(21)} -> END\n}\n",
);
assert!(
output.contains("Result: 42"),
"calling a bare-name fn-valued CONST global from flow main should \
work now that issue #2083 is fixed, got: {output:?}"
);
}
#[test]
fn compile_path_native_bare_name_fn_valued_var_global_call_site_resolves() {
let output = compile_and_run_native(
"bare-name-var-call-site",
"fn double(n: int): int {\n return n * 2;\n}\n\nvar twice = double\n\n\
flow main() {\n Result: {twice(21)} -> END\n}\n",
);
assert!(
output.contains("Result: 42"),
"calling a bare-name fn-valued VAR global from flow main should \
keep working, got: {output:?}"
);
}
#[test]
fn compile_path_native_call_site_local_shadows_same_named_const_global() {
let output = compile_and_run_native(
"call-site-local-shadows-const",
"fn double(n: int): int {\n return n * 2;\n}\n\n\
fn triple(n: int): int {\n return n * 3;\n}\n\n\
const twice = double\n\n\
flow main() {\n ~ let twice = triple\n Result: {twice(21)} -> END\n}\n",
);
assert!(
output.contains("Result: 63"),
"the local `let twice = triple` must shadow the global \
`const twice = double` at the call site (63, not 42), got: {output:?}"
);
}
#[test]
fn compile_path_native_call_site_local_shadows_same_named_var_global() {
let output = compile_and_run_native(
"call-site-local-shadows-var",
"fn double(n: int): int {\n return n * 2;\n}\n\n\
fn triple(n: int): int {\n return n * 3;\n}\n\n\
var twice = double\n\n\
flow main() {\n ~ let twice = triple\n Result: {twice(21)} -> END\n}\n",
);
assert!(
output.contains("Result: 63"),
"the local `let twice = triple` must shadow the global \
`var twice = double` at the call site (63, not 42), got: {output:?}"
);
}
#[test]
#[ignore = "pre-existing resolver limitation: a global const-bound lambda cannot reference its own name recursively (E025) — not introduced or fixed by #1774, narrower than and adjacent to #2083, not filed separately"]
fn compile_path_native_const_lambda_decl_default_self_recursion_works() {
let output = compile_and_run_native(
"lambda-decl-default-self-recursion",
"const fact = |n| {\n if n <= 1 { return 1; }\n return n * fact(n - 1);\n}\n\n\
flow main() {\n Result: {fact(5)} -> END\n}\n",
);
assert!(
output.contains("Result: 120"),
"a global const-bound lambda should be able to call itself \
recursively once the resolver limitation is fixed, got: {output:?}"
);
}
#[test]
fn compile_path_native_lambda_valued_var_default_compiles_with_map_keys_warning_alongside() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-lambda-var-default-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"var f = ||: int {\n let m = Map { 3.5: 1 };\n 0\n}\n\n\
flow main() {\n Hello. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let output = result.expect(
"a lambda-valued VAR default now compiles (E083 lifted, RULED 2026-08-01, issue #1774)",
);
let codes: Vec<&str> = output.warnings.iter().map(|d| d.code.as_str()).collect();
assert!(
codes.contains(&"E106"),
"expected E106 (bad map key inside the lambda's own body) to still fire \
as a warning now that the outer VAR compiles, got: {codes:?}"
);
}
#[test]
fn compile_path_native_ufcs_call_in_lambda_decl_default_is_e142_unannotated_receiver() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-ufcs-lambda-decl-default-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"struct Guest {\n name: string\n}\n\n\
fn greet(g, loudness) {\n return loudness;\n}\n\n\
const callGreet = |g| g.greet(3)\n\n\
flow main() {\n Hi. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let err = result.expect_err(
"an unannotated UFCS receiver inside a decl-default lambda body must still \
refuse to compile (D3: the type is genuinely undecidable here) — but now with \
the real diagnostic naming that cause, not a structural never-visited refusal",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E142"),
"expected E142 (D3: annotate the receiver) among diagnostics, got: {codes:?}"
);
assert!(
!codes.contains(&"E144"),
"must not fall through to the old defensive never-visited refusal any more, \
got: {codes:?}"
);
}
#[test]
fn compile_path_native_ufcs_call_in_lambda_decl_default_resolves_and_runs() {
let output = compile_and_run_native(
"ufcs-lambda-decl-default-resolves",
"struct Guest {\n hp: int\n}\n\n\
fn greet(g, loudness) {\n return loudness;\n}\n\n\
const callGreet = |g: Guest| g.greet(3)\n\n\
flow main() {\n Result: {callGreet(Guest { hp: 1 })} -> END\n}\n",
);
assert!(
output.contains("Result: 3"),
"the UFCS call inside the decl-default lambda body must actually run and \
produce `greet`'s own return value (loudness=3), not just compile, got: {output:?}"
);
}
fn compile_native_brink_dialect(
dir_suffix: &str,
source: &str,
) -> Result<brink_compiler::CompileOutput, brink_compiler::CompileError> {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-comparator-contract-{dir_suffix}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.brink"), source).unwrap();
let options = brink_compiler::AnalysisOptions {
dialect: brink_compiler::Dialect::Brink,
..brink_compiler::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 compile_path_native_comparator_contract_call_in_lambda_decl_default_is_e119() {
let source = "var seen = 0\n\n\
fn spy(n) {\n seen = seen + n;\n return n;\n}\n\n\
const doIt = || map([1, 2], spy)\n\n\
flow main() {\n Hi. -> END\n}\n";
let err = compile_native_brink_dialect("lambda-decl-default", source).expect_err(
"an impure named callback of a pure-callback verb, called inside a decl-default \
lambda's own body, must be refused by E119 — not compile clean because the \
analyzer never visited the initializer",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E119"),
"expected E119 among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_path_native_comparator_contract_pure_call_in_lambda_decl_default_compiles() {
let source = "fn double(n) {\n return n * 2;\n}\n\n\
const doIt = || map([1, 2], double)\n\n\
flow main() {\n Hi. -> END\n}\n";
compile_native_brink_dialect("lambda-decl-default-pure", source).expect(
"a pure named callback inside a decl-default lambda body must compile clean \
(E119 is exceedance-only, not a blanket refusal of the new initializer reach)",
);
}
#[test]
fn compile_path_native_comparator_contract_call_directly_in_var_initializer_is_e119() {
let source = "var seen = 0\n\n\
fn spy(x, y) {\n seen = seen + 1;\n return x - y;\n}\n\n\
var sorted = sort_by([2, 1], spy)\n\n\
flow main() {\n Hi. -> END\n}\n";
let err = compile_native_brink_dialect("var-initializer-direct", source).expect_err(
"an impure named comparator called directly in a VAR initializer (no lambda \
involved) must be refused by E119 — issue #1769's own gap",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E119"),
"expected E119 among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_path_native_contains_domain_call_in_lambda_decl_default_is_e152() {
let source = "const doIt = || {\n let hit = contains(Map { 1: \"a\" }, 3.5);\n 0\n}\n\n\
flow main() {\n Hi. -> END\n}\n";
let result = compile_native_brink_dialect("contains-domain-lambda-decl-default", source);
let out = result.expect(
"a decl-default lambda body's contains() misuse compiles clean (E152 is a warning), \
with E152 firing alongside it",
);
let codes: Vec<&str> = out.warnings.iter().map(|d| d.code.as_str()).collect();
assert!(
codes.contains(&"E152"),
"expected E152 among warnings, got: {codes:?}"
);
}
#[test]
fn compile_path_native_conversions_call_in_lambda_decl_default_is_e078() {
let source = "const doIt = || {\n let x = int(Map { 1: 2 });\n 0\n}\n\n\
flow main() {\n Hi. -> END\n}\n";
let err = compile_native_brink_dialect("conversions-lambda-decl-default", source).expect_err(
"a bad int() conversion inside a decl-default lambda body must be refused by E078",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E078"),
"expected E078 among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_path_native_structs_literal_in_lambda_decl_default_is_e071() {
let source = "struct Point {\n x: float\n}\n\n\
const doIt = || Point { x: \"hi\" }\n\n\
flow main() {\n Hi. -> END\n}\n";
let err = compile_native_brink_dialect("structs-lambda-decl-default", source).expect_err(
"a struct literal with a field type mismatch inside a decl-default lambda body \
must be refused by E071",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E071"),
"expected E071 among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_ink_brink_range_refinement_direct_var_initializer_is_e117() {
let files: std::collections::HashMap<&str, &str> =
std::collections::HashMap::from([("main.ink", "VAR bad = int(0..0)\n-> END\n")]);
let options = brink_compiler::AnalysisOptions {
dialect: brink_compiler::Dialect::Brink,
..brink_compiler::AnalysisOptions::default()
};
let result = brink_compiler::compile_with_options(
"main.ink",
|p| {
files.get(p).map(|s| (*s).to_string()).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, format!("not found: {p}"))
})
},
options,
);
let err = result.expect_err(
"a provably-empty range literal in a plain VAR initializer must be refused by E117",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E117"),
"expected E117 among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_path_native_ignores_unparseable_file_under_target() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-target-junk-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("target")).unwrap();
std::fs::write(
dir.join("main.brink"),
"flow main() {\n Hello. -> END\n}\n",
)
.unwrap();
std::fs::write(dir.join("target/junk.brink"), "{{{ not brink source at all").unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let output = result.expect(
"an unparseable .brink file under target/ must not be discovered, so the entry still compiles",
);
assert!(
!output.data.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_path_native_walks_up_to_brink_toml() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-walkup-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("story")).unwrap();
std::fs::write(dir.join("brink.toml"), "[project]\ndialect = \"brink\"\n").unwrap();
std::fs::write(
dir.join("story/main.brink"),
"flow main() {\n Hello. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("story/main.brink"));
std::fs::remove_dir_all(&dir).ok();
let output =
result.expect("native project with an ancestor brink.toml should compile via walk-up");
assert!(
!output.data.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_path_native_with_explicit_gradual_types_is_e137() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-gradual-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"flow main() {\n Hello. -> END\n}\n",
)
.unwrap();
let options = brink_compiler::AnalysisOptions {
types: Some(brink_compiler::TypePolicy::Gradual),
..Default::default()
};
let result = brink_compiler::compile_path_with_options(&dir.join("main.brink"), options);
std::fs::remove_dir_all(&dir).ok();
let err = result.expect_err("a gradual-knob .brink compile must be a hard error");
assert!(
matches!(err, brink_compiler::CompileError::Diagnostics(_)),
"expected a Diagnostics(E137) compile error, got: {err}"
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E137"),
"expected E137 (native strict-only) among diagnostics, got: {codes:?}"
);
}
#[test]
fn compile_path_native_with_explicit_strict_types_compiles() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-strict-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"flow main() {\n Hello. -> END\n}\n",
)
.unwrap();
let options = brink_compiler::AnalysisOptions {
types: Some(brink_compiler::TypePolicy::Strict),
..Default::default()
};
let result = brink_compiler::compile_path_with_options(&dir.join("main.brink"), options);
std::fs::remove_dir_all(&dir).ok();
let output = result.expect("types = strict native compile should succeed with no dialect set");
assert!(
!output.data.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_path_native_struct_decl_under_default_options_has_no_dialect_gate_e051() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-struct-dialect-gate-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"struct Item {\n name: string,\n weight: int\n}\n\nflow main() {\n Hello. -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path_with_options(
&dir.join("main.brink"),
brink_compiler::AnalysisOptions::default(),
);
std::fs::remove_dir_all(&dir).ok();
let output = result
.expect("a native STRUCT declaration must never trip the ink-only dialect gate (E051)");
assert!(
!output.data.containers.is_empty(),
"expected non-empty containers"
);
}
fn compile_and_run_native(dir_suffix: &str, source: &str) -> String {
try_compile_and_run_native(dir_suffix, source)
.unwrap_or_else(|err| panic!("fixture must run cleanly, got a runtime fault: {err:?}"))
}
fn try_compile_and_run_native(
dir_suffix: &str,
source: &str,
) -> Result<String, brink_runtime::RuntimeError> {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-b1-{dir_suffix}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.brink"), source).unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let data = result.unwrap().data;
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let lines = story.continue_maximally()?;
let mut output = String::new();
for line in &lines {
output.push_str(line.text());
}
Ok(output)
}
#[test]
fn native_or_coalescing_collapse_form_unwraps_some_and_falls_back_on_none() {
let output = compile_and_run_native(
"collapse",
"flow main() {\n Some case: {some(5) or 99}\n None case: {none or 99} -> END\n}\n",
);
assert!(
output.contains("Some case: 5"),
"expected the unwrapped `some(5)`, got: {output:?}"
);
assert!(
output.contains("None case: 99"),
"expected the `none` fallback, got: {output:?}"
);
}
#[test]
fn native_or_coalescing_chain_falls_through_to_final_fallback() {
let output = compile_and_run_native(
"chain",
"flow main() {\n Chained: {none or none or 7} -> END\n}\n",
);
assert!(
output.contains("Chained: 7"),
"expected the chain to fall through both `none`s to the final fallback, got: {output:?}"
);
}
const OR_COALESCING_SHORT_CIRCUIT_SRC: &str = "var counter = 0\n\
fn bump() {\n counter = counter + 1;\n return 99;\n}\n\
flow main() {\n Value: {some(5) or bump()}\n Counter: {counter} -> END\n}\n";
#[test]
fn native_or_coalescing_short_circuits_rhs_on_some_lhs() {
let output = compile_and_run_native("shortcircuit", OR_COALESCING_SHORT_CIRCUIT_SRC);
assert!(
output.contains("Value: 5"),
"the collapse form must still unwrap `some(5)`, got: {output:?}"
);
assert!(
output.contains("Counter: 0"),
"expected `bump()` to never run since `lhs` is `some(_)` \
(short-circuit), got: {output:?}"
);
}
#[test]
fn native_or_coalescing_still_evaluates_rhs_when_lhs_is_none() {
let output = compile_and_run_native(
"shortcircuit-none",
"var counter = 0\n\
fn bump() {\n counter = counter + 1;\n return 99;\n}\n\
flow main() {\n Value: {none or bump()}\n Counter: {counter} -> END\n}\n",
);
assert!(
output.contains("Value: 99"),
"the `none` lhs must fall through to `bump()`'s return value, got: {output:?}"
);
assert!(
output.contains("Counter: 1"),
"expected `bump()` to have run exactly once for a `none` lhs, got: {output:?}"
);
}
#[test]
fn native_or_coalescing_chain_preserves_optionality_through_intermediate_some() {
let output = compile_and_run_native(
"chain-preserve",
"flow main() {\n Chained: {some(5) or none or 99} -> END\n}\n",
);
assert!(
output.contains("Chained: 5"),
"expected the leading `some(5)` to win, unwrapped only at the final \
non-Option fallback, got: {output:?}"
);
}
const OR_COALESCING_CHAIN_WITH_CALL_SRC: &str = "fn maybe() {\n return none;\n}\n\
flow main() {\n Chained: {some(5) or maybe() or 99} -> END\n}\n";
#[test]
fn native_or_coalescing_chain_with_intermediate_call_yields_the_leading_some() {
let output = compile_and_run_native("chain-call", OR_COALESCING_CHAIN_WITH_CALL_SRC);
assert!(
output.contains("Chained: 5"),
"expected the leading `some(5)` to win through an Option-returning \
call fallback, got: {output:?}"
);
}
#[test]
fn native_or_coalescing_rhs_visit_count_reference_is_tracked() {
let output = compile_and_run_native(
"visit-count",
"flow main() {\n -> other\n}\n\
flow other() {\n V: {none or other} -> END\n}\n",
);
assert!(
output.contains("V: 1"),
"expected `other`'s visit count to be tracked through the coalesce \
operand, got: {output:?}"
);
}
const OR_COALESCING_UNPINNED_LHS_FAULT_SRC: &str = "fn pick(x) {\n return x or 99;\n}\n\
flow main() {\n Value: {pick(1)} -> END\n}\n";
#[test]
fn native_or_coalescing_unpinned_lhs_faults_on_a_plain_value() {
let err =
try_compile_and_run_native("runtime-check-fault", OR_COALESCING_UNPINNED_LHS_FAULT_SRC)
.expect_err("a plain `Int` left-hand side must fault at runtime");
assert!(
matches!(&err, brink_runtime::RuntimeError::TypeError(msg)
if msg.contains("or-coalescing requires an Option left-hand side")),
"expected the or-coalescing TypeError, got: {err:?}"
);
}
#[test]
fn native_or_coalescing_unpinned_lhs_coalesces_an_option() {
let output = compile_and_run_native(
"runtime-check-ok",
"fn pick(x) {\n return x or 99;\n}\n\
flow main() {\n Some: {pick(some(5))}\n None: {pick(none)} -> END\n}\n",
);
assert!(
output.contains("Some: 5"),
"an unpinned `lhs` holding `some(5)` must unwrap, got: {output:?}"
);
assert!(
output.contains("None: 99"),
"an unpinned `lhs` holding `none` must fall through, got: {output:?}"
);
}
#[test]
fn native_or_coalescing_falls_through_to_an_option_returning_call() {
let output = compile_and_run_native(
"call-fallthrough",
"fn maybe() {\n return some(7);\n}\n\
flow main() {\n Chained: {none or maybe() or 99} -> END\n}\n",
);
assert!(
output.contains("Chained: 7"),
"expected `maybe()`'s `some(7)` to win, unwrapped at the final \
non-Option fallback, got: {output:?}"
);
}
#[test]
fn native_as_binding_statement_form_binds_payload_and_falls_to_else() {
let output = compile_and_run_native(
"as-if",
"fn present() {\n if some(41) as n {\n return n + 1;\n }\n return 0;\n}\n\
fn absent() {\n if none as n {\n return n;\n }\n return -7;\n}\n\
flow main() {\n Present: {present()}\n Absent: {absent()} -> END\n}\n",
);
assert!(
output.contains("Present: 42"),
"expected `n` to be the UNWRAPPED 41 (42 after +1), got: {output:?}"
);
assert!(
output.contains("Absent: -7"),
"expected the `none` condition to skip the arm entirely, got: {output:?}"
);
}
const AS_BINDING_WHILE_REBIND_SRC: &str = "var counter = 3\n\
fn next_ticket() {\n\
\x20 if counter > 0 {\n\
\x20 counter = counter - 1;\n\
\x20 return some(counter);\n\
\x20 }\n\
\x20 return none;\n}\n\
fn drain() {\n\
\x20 let sum = 0;\n\
\x20 while next_ticket() as t {\n\
\x20 sum = sum + t;\n\
\x20 }\n\
\x20 return sum;\n}\n\
flow main() {\n Sum: {drain()} -> END\n}\n";
#[test]
fn native_as_binding_while_form_rebinds_each_iteration() {
let output = compile_and_run_native("as-while", AS_BINDING_WHILE_REBIND_SRC);
assert!(
output.contains("Sum: 3"),
"expected 2+1+0 = 3 from per-iteration rebinding, got: {output:?}"
);
}
#[test]
fn native_as_binding_template_form_binds_inside_the_success_arm() {
let output = compile_and_run_native(
"as-template",
"flow main() {\n\
\x20 Leader: {if some(9) as l: number {l} else: nobody}\n\
\x20 Empty: {if none as l: number {l} else: nobody} -> END\n}\n",
);
assert!(
output.contains("Leader: number 9"),
"expected the template arm to see the unwrapped 9, got: {output:?}"
);
assert!(
output.contains("Empty: nobody"),
"expected the `else` arm on `none`, got: {output:?}"
);
}
#[test]
fn native_as_binding_scope_ends_at_the_arm() {
let output = compile_and_run_native(
"as-scope",
"fn probe() {\n\
\x20 let n = 100;\n\
\x20 let inner = 0;\n\
\x20 if some(1) as n {\n\
\x20 inner = n;\n\
\x20 }\n\
\x20 return inner * 1000 + n;\n}\n\
flow main() {\n Probe: {probe()} -> END\n}\n",
);
assert!(
output.contains("Probe: 1100"),
"expected inner = 1 (the binding) and n = 100 (the outer local, \
restored after the arm), got: {output:?}"
);
}
fn compile_native_linked(
dir_suffix: &str,
source: &str,
) -> (
std::sync::Arc<brink_runtime::Program>,
Vec<Vec<brink_format::LineEntry>>,
) {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-choice-as-{dir_suffix}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.brink"), source).unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let data = result
.unwrap_or_else(|err| panic!("choice-guard `as` fixture must compile cleanly: {err:?}"))
.data;
let (program, line_tables) = brink_runtime::link(&data).unwrap();
(std::sync::Arc::new(program), line_tables)
}
fn compile_native_to_story(dir_suffix: &str, source: &str) -> Story<DotNetRng> {
let (program, line_tables) = compile_native_linked(dir_suffix, source);
Story::<DotNetRng>::new(program, line_tables)
}
const CHOICE_GUARD_AS_SRC: &str = "var stash: Option<int> = none\n\
flow main() {\n\
\x20 ~ stash = some(41)\n\
\x20 {?\n\
\x20 * {if stash as n} [pick it] {\n\
\x20 You have {n}.\n\
\x20 -> DONE\n\
\x20 }\n\
\x20 }\n}\n";
#[test]
fn native_choice_guard_as_binds_and_captures_at_presentation() {
let mut story = compile_native_to_story("guard-capture", CHOICE_GUARD_AS_SRC);
story.set_visibility_enforcement(false);
let lines = story.continue_maximally().expect("continue to the choice");
let Some(Step::Choices(choices)) = lines.last() else {
panic!("expected the guard-gated choice to be presented, got: {lines:?}");
};
assert_eq!(choices.len(), 1, "expected exactly one choice: {choices:?}");
assert_eq!(choices[0].text, "pick it");
let mutated = story.set_variable(
"stash",
brink_format::Value::some(brink_format::Value::Int(999)),
);
assert!(mutated, "`stash` must be a real declared global");
story.choose(0).expect("choose the only choice");
let lines = story.continue_maximally().expect("continue after choosing");
let output: String = lines.iter().map(Step::text).collect();
assert!(
output.contains("You have 41."),
"expected the captured (pre-mutation) value 41, got: {output:?}"
);
assert!(
!output.contains("999"),
"the post-presentation mutation to 999 must never reach the picked \
body — capture-at-presentation, by-value COW: {output:?}"
);
}
#[test]
fn native_choice_guard_as_captured_value_survives_a_story_snapshot_round_trip() {
let (program, line_tables) = compile_native_linked("guard-snapshot", CHOICE_GUARD_AS_SRC);
let mut story = Story::<DotNetRng>::new(std::sync::Arc::clone(&program), line_tables);
let lines = story.continue_maximally().expect("continue to the choice");
assert!(
matches!(lines.last(), Some(Step::Choices(cs)) if cs.len() == 1),
"expected the guard-gated choice to be presented: {lines:?}"
);
let (snapshot, line_tables) = story.into_snapshot();
let mut story = Story::<DotNetRng>::from_snapshot(program, snapshot, line_tables);
story.choose(0).expect("choose the only choice");
let lines = story.continue_maximally().expect("continue after choosing");
let output: String = lines.iter().map(Step::text).collect();
assert!(
output.contains("You have 41."),
"expected the captured value to survive the snapshot round trip, got: {output:?}"
);
}
#[test]
fn native_choice_guard_as_false_condition_hides_the_choice() {
let source = "flow main() {\n\
\x20 {?\n\
\x20 * {if none as n} [pick it] {\n\
\x20 You have {n}.\n\
\x20 -> DONE\n\
\x20 }\n\
\x20 else {\n\
\x20 Nothing to grab.\n\
\x20 -> DONE\n\
\x20 }\n\
\x20 }\n}\n";
let mut story = compile_native_to_story("guard-false", source);
let lines = story
.continue_maximally()
.expect("continue past the hidden choice");
let output: String = lines.iter().map(Step::text).collect();
assert!(
output.contains("Nothing to grab."),
"expected the fallback to run since the guard is `none`, got: {output:?}"
);
assert!(
!output.contains("pick it") && !output.contains("You have"),
"the guarded choice must never appear when its condition is `none`, \
got: {output:?}"
);
}
#[test]
fn native_choice_guard_as_start_content_reads_the_binding() {
let source = "var stash: Option<int> = none\n\
flow main() {\n\
\x20 ~ stash = some(41)\n\
\x20 {?\n\
\x20 * {if stash as n} You have {n}. [pick it] {\n\
\x20 -> DONE\n\
\x20 }\n\
\x20 }\n}\n";
let mut story = compile_native_to_story("guard-start-content", source);
story.set_visibility_enforcement(false);
let lines = story
.continue_maximally()
.expect("start-content `{n}` read must resolve through the guard binding, not fault");
let Some(Step::Choices(choices)) = lines.last() else {
panic!("expected the guard-gated choice to be presented, got: {lines:?}");
};
assert_eq!(choices.len(), 1, "expected exactly one choice: {choices:?}");
}
#[test]
fn native_choice_guard_as_inner_content_reads_the_binding() {
let source = "var stash: Option<int> = none\n\
flow main() {\n\
\x20 ~ stash = some(41)\n\
\x20 {?\n\
\x20 * {if stash as n} [pick it] You have {n}. {\n\
\x20 -> DONE\n\
\x20 }\n\
\x20 }\n}\n";
let mut story = compile_native_to_story("guard-inner-content", source);
story.set_visibility_enforcement(false);
let lines = story.continue_maximally().expect("continue to the choice");
assert!(
matches!(lines.last(), Some(Step::Choices(cs)) if cs.len() == 1),
"expected the guard-gated choice to be presented: {lines:?}"
);
story.choose(0).expect("choose the only choice");
let lines = story.continue_maximally().expect(
"inner-content `{n}` read must resolve through the guard binding after picking, not fault",
);
let output: String = lines.iter().map(Step::text).collect();
assert!(
output.contains("You have 41."),
"expected the choice's own inner-content to read the captured binding, got: {output:?}"
);
}
#[test]
fn native_choice_guard_as_two_choices_binding_the_same_name_both_compile_clean() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-choice-as-shared-name-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"var stash: Option<int> = none\n\
flow main() {\n\
\x20 ~ stash = some(41)\n\
\x20 {?\n\
\x20 * {if stash as n} A [a] {\n\
\x20 -> DONE\n\
\x20 }\n\
\x20 * {if stash as n} B {n} [b] {\n\
\x20 -> DONE\n\
\x20 }\n\
\x20 }\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
result.unwrap_or_else(|err| {
panic!(
"a second choice binding the same guard name must compile clean — \
no `~ {{ … }}` block exists in this source, so E082 must never \
fire: {err:?}"
)
});
}
#[test]
fn ink_bare_function_name_is_still_a_visit_count() {
let source = "Count: {f}\n\
-> END\n\n\
=== function f ===\n\
~ return 1\n";
let output = compile_and_run(source, &[]);
assert!(
output.contains("Count: 0"),
"an ink bare function-knot name must stay a visit count (0, never entered), \
got: {output:?}"
);
}
#[test]
fn native_bare_name_fn_value_with_a_ref_param_is_e080() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-fnvalue-ref-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"fn heal(ref amount) {\n\
\x20 amount = amount + 1;\n}\n\
fn used() {\n\
\x20 let f = heal;\n\
\x20 return 0;\n}\n\
flow main() {\n Used: {used()} -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let err = result.expect_err("a ref-param target may not be referenced by bare name");
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E080"),
"expected E080 at the bare-name reference, got: {codes:?}"
);
}
#[test]
fn native_bare_name_fn_value_in_decl_initializer_with_a_ref_param_is_e080() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-fnvalue-decl-ref-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"fn heal(ref amount) {\n\
\x20 amount = amount + 1;\n}\n\
var f = heal\n\
flow main() {\n Used: {0} -> END\n}\n",
)
.unwrap();
let result = brink_compiler::compile_path(&dir.join("main.brink"));
std::fs::remove_dir_all(&dir).ok();
let err = result.expect_err("a ref-param target may not be referenced by bare name");
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E080"),
"expected E080 at the decl-initializer bare-name reference, got: {codes:?}"
);
}
#[test]
fn native_bare_name_fn_value_without_ref_params_compiles_and_runs() {
let output = compile_and_run_native(
"fnvalue-plain",
"fn double(x) {\n\
\x20 return x * 2;\n}\n\
fn apply(g, v) {\n\
\x20 return g(v);\n}\n\
flow main() {\n Applied: {apply(double, 21)} -> END\n}\n",
);
assert!(
output.contains("Applied: 42"),
"expected the bare name to reach `apply` as a callable fn value, got: {output:?}"
);
}
fn compile_native_strict(
dir_suffix: &str,
source: &str,
) -> Result<brink_compiler::CompileOutput, brink_compiler::CompileError> {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-1876-{dir_suffix}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.brink"), source).unwrap();
let result = brink_compiler::compile_path_with_options(
&dir.join("main.brink"),
brink_compiler::AnalysisOptions {
dialect: brink_compiler::Dialect::Brink,
types: Some(brink_compiler::TypePolicy::Strict),
..brink_compiler::AnalysisOptions::default()
},
);
std::fs::remove_dir_all(&dir).ok();
result
}
#[test]
fn native_bare_name_fn_value_passed_where_an_int_is_expected_is_e063() {
let err = compile_native_strict(
"mismatch",
"fn double(x: int): int {\n\
\x20 return x * 2;\n}\n\
fn total(n: int): int {\n\
\x20 return n + 1;\n}\n\
flow main() {\n Bad: {total(double)} -> END\n}\n",
)
.expect_err("passing a bare-name fn value where an `int` is declared must fail compilation");
let brink_compiler::CompileError::Diagnostics(diags) = &err else {
panic!("expected a Diagnostics compile error, got: {err:?}");
};
assert_eq!(
diags.iter().map(|d| d.code).collect::<Vec<_>>(),
vec![brink_ir::DiagnosticCode::E063],
"expected E063 alone, got: {diags:?}"
);
assert_eq!(
diags[0].message,
"argument 1 of call to `total` has type `fn(int): int` but its known type expects `int`",
"expected the `check_direct_call_args` message shape, got: {:?}",
diags[0].message
);
}
#[test]
fn native_bare_name_fn_value_satisfies_a_declared_fn_parameter() {
let data = compile_native_strict(
"annotated-param",
"fn double(x: int): int {\n\
\x20 return x * 2;\n}\n\
fn apply(g: fn(int): int, v: int): int {\n\
\x20 return g(v);\n}\n\
flow main() {\n Applied: {apply(double, 21)} -> END\n}\n",
)
.unwrap_or_else(|err| {
panic!(
"a bare name must satisfy a declared `fn(int): int` parameter: {:?}",
diagnostic_codes(&err)
)
})
.data;
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let output: String = story
.continue_maximally()
.unwrap()
.iter()
.map(Step::text)
.collect();
assert!(
output.contains("Applied: 42"),
"the fn value must still reach `apply` and be callable there, got: {output:?}"
);
}
#[test]
fn native_bare_name_fn_value_decl_initializer_call_is_not_e065() {
let data = compile_native_strict(
"decl-init-call",
"fn double(x: int): int {\n\
\x20 return x * 2;\n}\n\
var f = double\n\
flow main() {\n Doubled: {f(3)} -> END\n}\n",
)
.unwrap_or_else(|err| {
panic!(
"calling a global initialized to a native bare-name fn value must compile: {:?}",
diagnostic_codes(&err)
)
})
.data;
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let output: String = story
.continue_maximally()
.unwrap()
.iter()
.map(Step::text)
.collect();
assert!(
output.contains("Doubled: 6"),
"the global must still hold a callable fn value at runtime, got: {output:?}"
);
}
#[test]
fn native_bare_name_fn_value_decl_initializer_type_is_the_targets_signature() {
let err = compile_native_strict(
"decl-init-mismatch",
"fn double(x: int): int {\n\
\x20 return x * 2;\n}\n\
fn total(n: int): int {\n\
\x20 return n + 1;\n}\n\
var f = double\n\
flow main() {\n Bad: {total(f)} -> END\n}\n",
)
.expect_err("passing the fn-valued global where an `int` is declared must fail compilation");
let brink_compiler::CompileError::Diagnostics(diags) = &err else {
panic!("expected a Diagnostics compile error, got: {err:?}");
};
assert_eq!(
diags.iter().map(|d| d.code).collect::<Vec<_>>(),
vec![brink_ir::DiagnosticCode::E063],
"expected E063 alone, got: {diags:?}"
);
assert_eq!(
diags[0].message,
"argument 1 of call to `total` has type `fn(int): int` but its known type expects `int`",
"expected the `check_direct_call_args` message shape, got: {:?}",
diags[0].message
);
}
#[test]
fn native_bare_name_shadowed_by_a_same_named_global_is_not_typed_as_a_fn_value() {
let data = compile_native_strict(
"decl-init-shadowed",
"fn double(x: int): int {\n\
\x20 return x * 2;\n}\n\
fn total(n: int): int {\n\
\x20 return n + 1;\n}\n\
const double = 5\n\
var alias = double\n\
flow main() {\n Val: {total(alias)} -> END\n}\n",
)
.unwrap_or_else(|err| {
panic!(
"a bare name shadowed by a same-named global is a constant read, not a fn value: {:?}",
diagnostic_codes(&err)
)
})
.data;
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let output: String = story
.continue_maximally()
.unwrap()
.iter()
.map(Step::text)
.collect();
assert!(
output.contains("Val: 6"),
"the shadowed bare name must fold to the constant's value, got: {output:?}"
);
}
#[test]
fn native_bare_name_shadowed_by_a_same_named_list_item_is_not_typed_as_a_fn_value() {
let data = compile_native_strict(
"decl-init-shadowed-by-list-item",
"flags Palette = double, other\n\
fn double(x: int): int {\n\
\x20 return x * 2;\n}\n\
fn total(n: int): int {\n\
\x20 return n + 1;\n}\n\
var alias = double\n\
flow main() {\n Val: {total(alias)} -> END\n}\n",
)
.unwrap_or_else(|err| {
panic!(
"a bare name shadowed by a same-named list item is a list-item read, not a fn value: {:?}",
diagnostic_codes(&err)
)
})
.data;
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let output: String = story
.continue_maximally()
.unwrap()
.iter()
.map(Step::text)
.collect();
assert!(
output.contains("Val:"),
"the shadowed bare name must fold to the list item's value, got: {output:?}"
);
}
#[test]
fn ink_var_initialized_to_a_function_name_is_not_typed_as_a_fn_value() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-ink-1895-decl-init-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("main.ink");
std::fs::write(
&path,
"VAR g = f\n\
=== function f(x: int): int ===\n\
~ return x\n\n\
=== function total(n: int): int ===\n\
~ return n + 1\n\n\
=== main ===\n\
~ total(g)\n\
-> DONE\n",
)
.unwrap();
let result = brink_compiler::compile_path_with_options(
&path,
brink_compiler::AnalysisOptions {
dialect: brink_compiler::Dialect::Brink,
types: Some(brink_compiler::TypePolicy::Strict),
..brink_compiler::AnalysisOptions::default()
},
);
std::fs::remove_dir_all(&dir).ok();
result.unwrap_or_else(|err| {
panic!(
"an ink bare function-knot name must stay Unknown and compile \
clean under strict typing, got: {:?}",
diagnostic_codes(&err)
)
});
}
fn strict_findings(
fixture_name: &str,
result: Result<brink_compiler::CompileOutput, brink_compiler::CompileError>,
) -> Vec<(String, String, String)> {
let mut findings = Vec::new();
let diagnostics = match result {
Ok(output) => output.warnings,
Err(brink_compiler::CompileError::Diagnostics(ds)) => ds,
Err(e) => panic!("{fixture_name}: unexpected compile failure: {e}"),
};
for d in diagnostics {
if matches!(
d.code,
brink_ir::DiagnosticCode::E063
| brink_ir::DiagnosticCode::E065
| brink_ir::DiagnosticCode::E066
) {
findings.push((
fixture_name.to_string(),
d.code.as_str().to_string(),
d.message,
));
}
}
findings
}
const BASELINE: &[(&str, &str, &str)] = &[
(
"or-coalescing-chain-call",
"E065",
"`maybe`'s return type escapes strict inference as Unknown — annotate or restructure",
),
(
"or-coalescing-unpinned-lhs-fault",
"E063",
"argument 1 of call to `pick` has type `int` but its known type expects `Option<int>`",
),
(
"or-coalescing-unpinned-lhs-fault",
"E065",
"`pick`'s return type escapes strict inference as Unknown — annotate or restructure",
),
];
#[test]
fn native_or_coalescing_strict_findings_match_baseline() {
let mut actual = Vec::new();
let result = compile_native_strict("or-coalesce-chain-call", OR_COALESCING_CHAIN_WITH_CALL_SRC);
actual.extend(strict_findings("or-coalescing-chain-call", result));
let result = compile_native_strict(
"or-coalesce-unpinned-lhs-fault",
OR_COALESCING_UNPINNED_LHS_FAULT_SRC,
);
actual.extend(strict_findings("or-coalescing-unpinned-lhs-fault", result));
actual.sort();
let expected: Vec<(String, String, String)> = BASELINE
.iter()
.map(|(f, c, m)| ((*f).to_string(), (*c).to_string(), (*m).to_string()))
.collect();
assert_eq!(
actual, expected,
"swept driver.rs native fixtures' strict findings drifted from baseline.\n\
Do NOT edit the fixtures to make this pass — triage each finding and either \
fix the checker or update BASELINE with a classification and tracking issue."
);
}
#[test]
fn the_sweep_actually_runs_under_strict() {
let result = compile_native_strict(
"guard-check",
"fn f(x) { return x; }\nflow main() { -> END }\n",
);
let findings = strict_findings("guard", result);
assert!(
!findings.is_empty(),
"the strict pass produced no findings at all — it is almost certainly \
not running (issue #1916's original bug)"
);
}
#[test]
fn compile_path_reads_from_disk() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/tier1/basics/I001-minimal-story/story.ink");
let story = brink_compiler::compile_path(&path).unwrap();
assert!(
!story.data.containers.is_empty(),
"expected non-empty containers"
);
}
#[test]
fn compile_path_nested_includes_from_disk() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/tier3/misc/I025-nested-includes/story.ink");
let story = brink_compiler::compile_path(&path).unwrap();
assert!(
!story.data.containers.is_empty(),
"expected non-empty containers"
);
}
fn compile_and_run(source: &str, inputs: &[usize]) -> String {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let data = compile_mem("main.ink", &files).unwrap();
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let mut output = String::new();
let mut input_idx = 0;
loop {
let lines = story.continue_maximally().unwrap();
let last = lines.last().unwrap();
match last {
Step::Line(_) | Step::Done | Step::End | Step::Suspended => {
for line in &lines {
output.push_str(line.text());
}
break;
}
Step::Choices(choices) => {
for line in &lines {
output.push_str(line.text());
}
let idx = if input_idx < inputs.len() {
let c = inputs[input_idx];
input_idx += 1;
c
} else {
0
};
assert!(
idx < choices.len(),
"choice index {idx} out of range (only {} choices available)",
choices.len()
);
story.choose(idx).unwrap();
}
}
}
output
}
#[test]
fn choices_after_tunnel_call_are_yielded() {
let source = "\
-> main
=== function is_alive ===
~ return true
=== check ===
{ is_alive():
->->
}
-> END
=== main ===
Before choices.
-> check ->
* [Option A]
Chose A.
* [Option B]
Chose B.
- -> END
";
let result = compile_and_run(source, &[0]);
assert!(
result.contains("Chose A"),
"expected 'Chose A' after tunnel return, got: {result:?}"
);
}
#[test]
fn choices_after_tunnel_call_with_args_are_yielded() {
let source = "\
VAR hp = 2
-> main
=== function is_alive ===
~ return hp > 0
=== get_hit(x) ===
~ hp = hp - x
{ is_alive():
->->
}
-> END
=== main ===
Start.
-> get_hit(1) ->
* [Fight]
You fight.
* [Flee]
You flee.
- -> END
";
let result = compile_and_run(source, &[0]);
assert!(
result.contains("You fight"),
"expected 'You fight' after tunnel return, got: {result:?}"
);
}
#[test]
fn nested_choices_after_tunnel_in_stitch() {
let source = "\
VAR hp = 2
-> main
=== function is_alive ===
~ return hp > 0
=== get_hit(x) ===
~ hp = hp - x
{ is_alive():
->->
}
-> END
=== main ===
Choose:
* [Yes]
You chose yes.
-> END
* [No]
You chose no.
-> get_hit(1) ->
** [Fight]
You fight.
** [Flee]
You flee.
- -> END
";
let result = compile_and_run(source, &[1, 0]);
assert!(
result.contains("You fight"),
"expected inner choice after tunnel return, got: {result:?}"
);
}
#[test]
fn list_items_display_without_origin_prefix() {
let source = "\
LIST colors = (red), green, (blue)
{colors}
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "red, blue\n");
}
#[test]
fn multi_list_display_without_origin_prefix() {
let source = "\
LIST a = (x), y
LIST b = (p), q
{a + b}
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "x, p\n");
}
#[test]
fn external_function_uses_ink_fallback() {
let source = "\
EXTERNAL greet()
The value is {greet()}.
-> END
=== function greet() ===
~ return \"hello\"
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "The value is hello.\n");
}
#[test]
fn external_function_fallback_with_args() {
let source = "\
EXTERNAL add(x, y)
The value is {add(3, 4)}.
-> END
=== function add(x, y) ===
~ return x + y
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "The value is 7.\n");
}
#[test]
fn include_content_appears_before_main() {
let files: HashMap<&str, &str> = HashMap::from([
("main.ink", "INCLUDE a.ink\nINCLUDE b.ink\nThis is main.\n"),
("a.ink", "This is A.\n"),
("b.ink", "This is B.\n"),
]);
let data = compile_mem("main.ink", &files).unwrap();
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let lines = story.continue_maximally().unwrap();
let result: String = lines.iter().map(Step::text).collect();
assert_eq!(
result, "This is A.\nThis is B.\nThis is main.\n",
"included file content must appear before main file content"
);
}
#[test]
fn divert_to_standalone_labeled_gather() {
let source = "\
-> knot
=== knot ===
-> knot.gather
- (gather) g
-> DONE
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "g\n");
}
#[test]
fn divert_target_with_parameter() {
let source = "\
VAR x = ->place
->x (5)
== place (a) ==
{a}
-> DONE
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "5\n");
}
#[test]
fn tunnel_onwards_with_arg() {
let source = "\
-> a ->
=== a ===
->-> b (5 + 3)
=== b (x) ===
{x}
-> END
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "8\n");
}
#[test]
fn tunnel_onwards_with_param_default_choice() {
let source = "\
-> tunnel ->
== tunnel ==
* ->-> elsewhere (8)
== elsewhere (x) ==
{x}
-> END
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "8\n");
}
#[test]
fn variable_tunnel_call() {
let source = "\
-> one_then_tother(-> tunnel)
=== one_then_tother(-> x) ===
-> x -> end
=== tunnel ===
STUFF
->->
=== end ===
-> END
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "STUFF\n");
}
#[test]
fn tunnel_return_at_gather_with_thread() {
let source = "\
-> knot
=== knot
<- threadA
When should this get printed?
-> DONE
=== threadA
-> tunnel ->
Finishing thread.
-> DONE
=== tunnel
- I'm in a tunnel
* I'm an option
- ->->
";
let result = compile_and_run(source, &[0]);
assert_eq!(
result,
"I'm in a tunnel\nWhen should this get printed?\nI'm an option\nFinishing thread.\n"
);
}
#[test]
fn gather_bare_tunnel_return() {
let source = "\
-> start
== start ==
-> tun ->
After tunnel.
-> END
== tun ==
- Gathered.
* Pick me
- ->->
";
let result = compile_and_run(source, &[0]);
assert_eq!(result, "Gathered.\nPick me\nAfter tunnel.\n");
}
#[test]
fn gather_tunnel_return_with_override() {
let source = "\
-> start
== start ==
-> tun ->
Should not print.
-> END
== tun ==
- In tunnel.
* Pick me
- ->-> destination
== destination ==
Overridden.
-> END
";
let result = compile_and_run(source, &[0]);
assert_eq!(result, "In tunnel.\nPick me\nOverridden.\n");
}
#[test]
fn gather_tunnel_call() {
let source = "\
-> start
== start ==
* Pick me
- -> inner_tunnel ->
After inner tunnel.
-> END
== inner_tunnel ==
Inside inner tunnel.
->->
";
let result = compile_and_run(source, &[0]);
assert_eq!(
result,
"Pick me\nInside inner tunnel.\nAfter inner tunnel.\n"
);
}
#[test]
fn gather_thread_start() {
let source = "\
-> start
== start ==
* Pick me
- <- bg_thread
+ Next
-
Done.
-> END
== bg_thread ==
* Background option
- -> DONE
";
let result = compile_and_run(source, &[0, 0]);
assert!(
result.contains("Background option"),
"expected thread's choice from gather `<- bg_thread` to be available, got: {result:?}"
);
}
#[test]
fn gather_tunnel_return_emits_tunnel_return_opcode() {
let source = "\
-> start
== start ==
-> tun ->
After.
-> END
== tun ==
- Top.
* Option
- ->->
";
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let data = compile_mem("main.ink", &files).unwrap();
let mut buf = String::new();
brink_format::write_inkt(&data, &mut buf).unwrap();
assert!(
buf.contains("tunnel_return"),
"expected tunnel_return in bytecode for gather `->->`, got:\n{buf}"
);
}
#[test]
#[ignore = "thread completion doesn't resume main flow — runtime thread merging bug"]
fn tunnel_and_thread_choices_merge() {
let source = "\
-> knot_with_options ->
Finished tunnel.
Starting thread.
<- thread_with_options
* E
-
Done.
== knot_with_options ==
* A
* B
-
->->
== thread_with_options ==
* C
* D
- -> DONE
";
let result = compile_and_run(source, &[0, 0]);
assert_eq!(result, "A\nFinished tunnel.\nStarting thread.\nC\nDone.\n");
}
#[test]
fn thread_choices_merge_with_tunnel() {
let source = "\
-> knot
=== knot
<- threadB
-> tunnel ->
THE END
-> END
=== tunnel
- blah blah
* wigwag
- ->->
=== threadB
* option
- something
-> DONE
";
let result = compile_and_run(source, &[0]);
assert_eq!(result, "blah blah\noption\nsomething\n");
}
#[test]
fn multiple_thread_choices_merge() {
let source = "\
-> start
== start ==
-> tunnel ->
The end
-> END
== tunnel ==
<- place1
<- place2
-> DONE
== place1 ==
This is place 1.
* choice in place 1
- ->->
== place2 ==
This is place 2.
* choice in place 2
- ->->
";
let result = compile_and_run(source, &[0]);
assert!(
result.contains("choice in place 1"),
"expected first thread's choice to be available, got: {result:?}"
);
}
#[test]
fn thread_choice_loop_with_variable_divert() {
let source = "\
-> start
=== start ===
Here is some gold. Do you want it?
- (top)
<- choices(-> top)
+ Yes
You win!
-> END
=== choices(-> goback) ===
+ No
Try again!
-> goback
";
let result = compile_and_run(source, &[1, 1, 0]);
assert!(
result.contains("You win!"),
"expected loop with thread choices, got: {result:?}"
);
}
#[test]
fn choice_set_does_not_emit_begin_choice_set() {
let source = "\
-> start
== start ==
* Choice A
* Choice B
- Done.
";
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let data = compile_mem("main.ink", &files).unwrap();
let mut buf = String::new();
brink_format::write_inkt(&data, &mut buf).unwrap();
assert!(
!buf.contains("begin_choice_set"),
"begin_choice_set should not appear in compiled output:\n{buf}"
);
assert!(
!buf.contains("end_choice_set"),
"end_choice_set should not appear in compiled output:\n{buf}"
);
}
#[test]
fn three_threads_all_choices_merge() {
let source = "\
-> start
== start ==
<- t1
<- t2
<- t3
* local choice
- Done.
== t1 ==
* thread 1 choice
- -> DONE
== t2 ==
* thread 2 choice
- -> DONE
== t3 ==
* thread 3 choice
- -> DONE
";
let result = compile_and_run(source, &[0]);
assert!(
result.contains("Done.") || result.contains("choice"),
"expected all thread choices to be available, got: {result:?}"
);
}
#[test]
fn thread_choice_with_once_only_filtering() {
let source = "\
-> start
== start ==
<- thread_opts
+ [sticky] Sticky text
- -> END
== thread_opts ==
* once only
-> start
- -> DONE
";
let result = compile_and_run(source, &[0, 0]);
assert!(
result.contains("once only") || result.contains("Sticky text"),
"expected both choices to be available initially, got: {result:?}"
);
}
#[test]
fn nested_thread_in_tunnel_choices_merge() {
let source = "\
-> start
== start ==
-> tun ->
* caller choice
- The end.
== tun ==
<- inner_thread
* tunnel choice
- ->->
== inner_thread ==
* thread choice
- -> DONE
";
let result = compile_and_run(source, &[0]);
assert!(
result.contains("The end.") || result.contains("choice"),
"expected thread+tunnel+caller choices to merge, got: {result:?}"
);
}
#[test]
fn nested_gather_three_levels() {
let source = "\
* A
* * B
* * * C
- - - Inner gather.
- - Middle gather.
- Outer gather.
-> END
";
let result = compile_and_run(source, &[0, 0, 0]);
assert_eq!(
result,
"A\nB\nC\nInner gather.\nMiddle gather.\nOuter gather.\n"
);
}
#[test]
fn nested_gather_with_second_choice_round() {
let source = "\
* First
* * Second
* * Third
- - Between.
* * Fourth
- - After fourth.
- Final.
-> END
";
let result = compile_and_run(source, &[0, 0, 0]);
assert_eq!(
result,
"First\nSecond\nBetween.\nFourth\nAfter fourth.\nFinal.\n"
);
}
#[test]
fn nested_gather_with_glue_continuation() {
let source = "\
* Outer choice
* * Deep choice
- - After deep, <>
- outer end.
-> END
";
let result = compile_and_run(source, &[0, 0]);
assert_eq!(
result,
"Outer choice\nDeep choice\nAfter deep, outer end.\n"
);
}
#[test]
fn stitch_params_by_value() {
let source = "\
-> greet.say(\"Hello\", \"world\")
== greet ==
= say(greeting, who)
{greeting}, {who}!
-> END
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Hello, world!\n");
}
#[test]
fn ref_param_global_var() {
let source = "\
VAR x = 1
~ bump(x)
{x}
-> END
=== function bump(ref target) ===
~ target = target + 1
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "2\n");
}
#[test]
fn ref_param_function_two_refs() {
let source = "\
VAR a = 10
VAR b = 0
~ swap(a, b)
a={a} b={b}
-> END
=== function swap(ref x, ref y) ===
~ temp t = x
~ x = y
~ y = t
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "a=0 b=10\n");
}
#[test]
fn tower_of_hanoi_mini() {
let source = "\
LIST Discs = one, two, three
VAR post1 = ()
VAR post2 = ()
VAR post3 = ()
~ post1 = LIST_ALL(Discs)
-> gameloop
=== function can_move(from_list, to_list) ===
{
- LIST_COUNT(from_list) == 0:
~ return false
- LIST_COUNT(to_list) > 0 && LIST_MIN(from_list) > LIST_MIN(to_list):
~ return false
- else:
~ return true
}
=== function move_ring( ref from, ref to ) ===
~ temp whichRingToMove = LIST_MIN(from)
~ from -= whichRingToMove
~ to += whichRingToMove
=== gameloop
Start.
- (top)
+ [ Regard]
Regarded.
<- move_post(1, 2, post1, post2)
-> DONE
= move_post(from_post_num, to_post_num, ref from_post_list, ref to_post_list)
+ { can_move(from_post_list, to_post_list) }
[ Move ]
{ move_ring(from_post_list, to_post_list) }
Moved.
-> top
";
let result = compile_and_run(source, &[0, 0]);
assert!(
result.contains("Moved") || result.contains("Regarded"),
"expected tower-of-hanoi mini to produce output, got: {result:?}"
);
}
#[test]
fn ref_param_list_move_ring() {
let source = "\
LIST Discs = one, two, three
VAR post1 = ()
VAR post2 = ()
~ post1 = LIST_ALL(Discs)
~ move_ring(post1, post2)
{post1}
{post2}
-> END
=== function move_ring( ref from, ref to ) ===
~ temp whichRingToMove = LIST_MIN(from)
~ from -= whichRingToMove
~ to += whichRingToMove
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "two, three\none\n");
}
#[test]
#[ignore = "visit count for gather labels not incremented on re-entry"]
fn space_between_interpolations_preserved() {
let source = "\
VAR gatherCount = 0
- (loop)
~ gatherCount++
{gatherCount} {loop}
{gatherCount<3:->loop}
-> DONE
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "1 1\n2 2\n3 3\n");
}
#[test]
fn conditional_divert_basic() {
let source = "\
VAR x = 1
{x == 1:->yes}
Nope.
-> END
== yes ==
Yes!
-> END
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Yes!\n");
}
#[test]
fn conditional_divert_loop() {
let source = "\
VAR i = 0
- (loop)
~ i++
{i}
{i < 3:->loop}
-> DONE
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "1\n2\n3\n");
}
#[test]
fn conditional_text_then_divert() {
let source = "\
VAR x = 1
{x == 1: Going there! ->yes}
Nope.
-> END
== yes ==
Arrived.
-> END
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Going there! Arrived.\n");
}
#[test]
fn conditional_divert_false_branch() {
let source = "\
VAR x = 0
{x == 1:->yes}
Fallthrough.
-> END
== yes ==
Yes!
-> END
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Fallthrough.\n");
}
#[test]
fn ref_parameter_modifies_caller_variable() {
let source = "\
VAR x = 0
~ bump(x)
{x}
-> DONE
=== function bump(ref n) ===
~ n++
";
let result = compile_and_run(source, &[]);
assert_eq!(result, "1\n");
}
#[test]
#[ignore = "runtime thread merging infinite loop with multiple conditional-choice threads"]
fn tower_of_hanoi_6threads() {
let source = "\
LIST Discs = one, two, three
VAR post1 = ()
VAR post2 = ()
VAR post3 = ()
~ post1 = LIST_ALL(Discs)
-> gameloop
=== function can_move(from_list, to_list) ===
{
- LIST_COUNT(from_list) == 0:
~ return false
- LIST_COUNT(to_list) > 0 && LIST_MIN(from_list) > LIST_MIN(to_list):
~ return false
- else:
~ return true
}
=== function move_ring( ref from, ref to ) ===
~ temp whichRingToMove = LIST_MIN(from)
~ from -= whichRingToMove
~ to += whichRingToMove
=== gameloop
Start.
- (top)
+ [ Regard]
Regarded.
<- move_post(1, 2, post1, post2)
<- move_post(2, 1, post2, post1)
<- move_post(1, 3, post1, post3)
<- move_post(3, 1, post3, post1)
<- move_post(3, 2, post3, post2)
<- move_post(2, 3, post2, post3)
-> DONE
= move_post(from_post_num, to_post_num, ref from_post_list, ref to_post_list)
+ { can_move(from_post_list, to_post_list) }
[ Move {from_post_num} to {to_post_num} ]
{ move_ring(from_post_list, to_post_list) }
Moved.
-> top
";
let result = compile_and_run(source, &[0, 0]);
assert!(
result.contains("Moved") || result.contains("Regarded"),
"expected output, got: {result:?}"
);
}
fn diagnostic_codes(err: &brink_compiler::CompileError) -> Vec<&'static str> {
match err {
brink_compiler::CompileError::Diagnostics(diags) => {
diags.iter().map(|d| d.code.as_str()).collect()
}
_ => vec![],
}
}
#[test]
fn compile_error_nested_choice_in_conditional() {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", "{ true:\n * choice\n}\n")]);
let result = compile_mem("main.ink", &files);
let err = result.expect_err(
"choice inside inline conditional should be a compile error, \
but compilation succeeded",
);
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E029"),
"expected E029 (choice in conditional must explicitly divert), got: {codes:?}"
);
}
#[test]
fn choice_in_conditional_with_divert_is_valid() {
let source = "=== play_game ===\n{ true:\n + [Burn] -> play_game\n}\n-> END\n";
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let result = compile_mem("main.ink", &files);
assert!(
result.is_ok(),
"choice with divert in conditional should compile: {result:?}"
);
}
#[test]
fn choice_in_conditional_with_gather_continuation_is_valid() {
let source =
"=== play_game ===\n{ true:\n + (burny) [Burn]\n Hello\n}\n- -> burny\n-> END\n";
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let result = compile_mem("main.ink", &files);
assert!(
result.is_ok(),
"choice in conditional with gather continuation should compile: {result:?}"
);
}
#[test]
fn compile_error_disallow_empty_diverts() {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", "->\n")]);
let result = compile_mem("main.ink", &files);
let err = result.expect_err("bare `->` should be a compile error, but compilation succeeded");
let codes = diagnostic_codes(&err);
assert!(
codes.contains(&"E012"),
"expected E012 (divert is missing a target), got: {codes:?}"
);
}
#[test]
fn unresolved_function_call_is_compile_error() {
let files: HashMap<&str, &str> = HashMap::from([(
"main.ink",
"\
~ temp x = DOES_NOT_EXIST()
{x}
-> END
",
)]);
let result = compile_mem("main.ink", &files);
assert!(
result.is_err(),
"calling a nonexistent function should produce a compile error, not succeed silently"
);
}
#[test]
fn turns_builtin_compiles_and_runs() {
let output = compile_and_run(
"\
~ temp t = TURNS()
turn is {t}
-> END
",
&[],
);
assert_eq!(output.trim(), "turn is 0");
}
#[test]
fn turns_builtin_increments_across_choices() {
let output = compile_and_run(
"\
turn {TURNS()}
+ [continue]
-
turn {TURNS()}
-> END
",
&[0],
);
assert_eq!(output.trim(), "turn 0\nturn 1");
}
fn compile_and_run_steps(source: &str, inputs: &[usize]) -> Vec<(String, Option<usize>)> {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let data = compile_mem("main.ink", &files).unwrap();
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let mut steps = Vec::new();
let mut input_idx = 0;
let mut guard = 0;
loop {
guard += 1;
assert!(guard < 100, "infinite loop detected");
let lines = story.continue_maximally().unwrap();
let combined_text: String = lines.iter().map(Step::text).collect();
let last = lines.last().unwrap();
match last {
Step::Line(_) | Step::Done | Step::End | Step::Suspended => {
steps.push((combined_text, None));
break;
}
Step::Choices(choices) => {
let count = choices.len();
steps.push((combined_text.clone(), Some(count)));
let idx = if input_idx < inputs.len() {
let c = inputs[input_idx];
input_idx += 1;
c
} else {
0
};
assert!(
idx < count,
"choice index {idx} out of range (only {count} choices), text so far: {combined_text:?}"
);
story.choose(idx).unwrap();
}
}
}
steps
}
#[test]
fn sequence_branch_starts_with_newline() {
let source = "\
-> test
=== test ===
{ stopping:
- Branch one.
- Branch two.
}
* [Again] Prefix. -> test
- -> END
";
let steps = compile_and_run_steps(source, &[0]);
assert!(
steps.len() >= 2,
"expected at least 2 steps, got {}",
steps.len()
);
let text = &steps[1].0;
assert!(
text.contains("Prefix.") && text.contains("Branch two."),
"expected both 'Prefix.' and 'Branch two.' in output, got: {text:?}"
);
assert!(
!text.contains("Prefix. Branch two.") && !text.contains("Prefix.Branch two."),
"expected newline between 'Prefix.' and 'Branch two.', got: {text:?}"
);
}
#[test]
fn choices_inside_sequence_branch_accumulate_with_parent() {
let source = "\
-> test
=== test ===
{ stopping:
- At the table, I drew a card. Ace of Hearts.
- 2 of Diamonds.
\"Should I hit you again,\" the croupier asks.
* [No.] I left the table. -> END
- King of Spades.
\"You lose,\" he crowed.
-> END
}
+ [Draw a card] I drew a card. -> test
";
let steps = compile_and_run_steps(source, &[0, 0]);
let second_choice_count = steps[1].1;
assert_eq!(
second_choice_count,
Some(2),
"expected 2 choices (No. + Draw a card) on second visit, got: {second_choice_count:?}"
);
}
#[test]
fn content_after_multiline_conditional_preserved() {
let source = "\
{true:
a
} <> b
";
let result = compile_and_run(source, &[]);
assert_eq!(
result, "a b\n",
"glue + text after conditional must be preserved"
);
}
#[test]
fn content_after_multiline_conditional_with_nested_conditional() {
let source = "\
{true:
a
} <> { true:
b
}
";
let result = compile_and_run(source, &[]);
assert_eq!(
result, "a b\n",
"glue + conditional after conditional must be preserved"
);
}
#[test]
fn shuffle_once_exhausts_after_all_branches_visited() {
let source = "\
~ SEED_RANDOM(1)
one: {f()}
two: {f()}
three: {f()}
four: {f()}
== function f ==
{shuffle once:
- A
- B
}
";
let result = compile_and_run(source, &[]);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 4, "expected 4 output lines, got: {result:?}");
let first_two_content: Vec<&str> = lines[0..2]
.iter()
.map(|l| l.split(": ").nth(1).unwrap_or("").trim())
.collect();
let mut sorted = first_two_content.clone();
sorted.sort_unstable();
assert_eq!(
sorted,
vec!["A", "B"],
"first two calls should produce A and B (in any order), got: {first_two_content:?}"
);
for (i, line) in lines[2..].iter().enumerate() {
let after_colon = line.split(": ").nth(1).unwrap_or("").trim();
assert!(
after_colon.is_empty(),
"call {} (line {:?}) should produce no text after exhaustion, got: {after_colon:?}",
i + 3,
line,
);
}
}
#[test]
fn shuffle_stopping_pins_to_last_branch() {
let source = "\
~ SEED_RANDOM(1)
one: {f()}
two: {f()}
three: {f()}
four: {f()}
five: {f()}
== function f ==
{stopping shuffle:
- A
- B
- final
}
";
let result = compile_and_run(source, &[]);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 5, "expected 5 output lines, got: {result:?}");
let first_three_content: Vec<String> = lines[0..3]
.iter()
.map(|l| l.split(": ").nth(1).unwrap_or("").trim().to_string())
.collect();
let mut sorted: Vec<&str> = first_three_content.iter().map(String::as_str).collect();
sorted.sort_unstable();
assert_eq!(
sorted,
vec!["A", "B", "final"],
"first three calls should produce A, B, final (in any order), got: {first_three_content:?}"
);
for (i, line) in lines[3..].iter().enumerate() {
let after_colon = line.split(": ").nth(1).unwrap_or("").trim();
assert_eq!(
after_colon,
"final",
"call {} should pin to 'final' after exhaustion, got: {after_colon:?}",
i + 4,
);
}
}
#[test]
fn shuffle_once_codegen_emits_min_opcode() {
use brink_format::Opcode;
let source = "\
{shuffle once:
- A
- B
}
";
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let data = compile_mem("main.ink", &files).unwrap();
let seq_container = data
.containers
.iter()
.find(|c| {
let mut offset = 0;
let mut has_sequence = false;
while offset < c.bytecode.len() {
if let Ok(op) = Opcode::decode(&c.bytecode, &mut offset) {
if matches!(op, Opcode::Sequence(..)) {
has_sequence = true;
}
} else {
break;
}
}
has_sequence
})
.expect("should find a container with a Sequence opcode");
let mut offset = 0;
let mut has_min = false;
while offset < seq_container.bytecode.len() {
if let Ok(op) = Opcode::decode(&seq_container.bytecode, &mut offset) {
if matches!(op, Opcode::Min) {
has_min = true;
}
} else {
break;
}
}
assert!(
has_min,
"shuffle once container must emit Min opcode for exhaustion clamping"
);
}
#[test]
fn keyword_once_as_knot_name_and_divert_target() {
let source = "\
-> once
== once ==
Hello from once.
-> END
";
let result = compile_and_run(source, &[]);
assert!(
result.contains("Hello from once"),
"knot named 'once' should work, got: {result:?}"
);
}
#[test]
fn thread_in_logic_compiles_and_runs() {
let source = "\
-> once ->
-> once ->
== once ==
{<- content|}
->->
== content ==
Content
-> DONE
";
let result = compile_and_run(source, &[]);
assert!(
result.contains("Content"),
"thread-in-logic should produce 'Content', got: {result:?}"
);
}
#[test]
fn template_single_variable() {
let source = "VAR name = \"World\"\nHello, {name}!\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Hello, World!\n");
}
#[test]
fn template_multiple_interpolations() {
let source = "VAR a = \"one\"\nVAR b = \"two\"\n{a} and {b}\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "one and two\n");
}
#[test]
fn template_expression_interpolation() {
let source = "VAR n = 3\nResult: {n * 2}\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Result: 6\n");
}
#[test]
fn template_interpolation_at_start() {
let source = "VAR x = \"Hello\"\n{x} world\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Hello world\n");
}
#[test]
fn template_interpolation_at_end() {
let source = "VAR x = \"world\"\nHello {x}\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Hello world\n");
}
#[test]
fn plain_text_regression() {
let source = "Just plain text.\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Just plain text.\n");
}
#[test]
fn template_integer_interpolation() {
let source = "VAR count = 42\nThere are {count} items.\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "There are 42 items.\n");
}
#[test]
fn template_float_interpolation() {
let source = "VAR pi = 3.14\nPi is {pi}.\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Pi is 3.14.\n");
}
#[test]
fn template_bool_interpolation() {
let source = "VAR flag = true\nFlag: {flag}\n";
let result = compile_and_run(source, &[]);
assert_eq!(result, "Flag: true\n");
}
fn compile_mem_with_warnings(
entry: &str,
files: &HashMap<&str, &str>,
) -> Result<brink_compiler::CompileOutput, brink_compiler::CompileError> {
brink_compiler::compile(entry, |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}"),
)
})
})
}
#[test]
fn warnings_surfaced_alongside_successful_compilation() {
let files: HashMap<&str, &str> = HashMap::from([(
"main.ink",
"VAR name = \"world\"\nCONST greeting = \"hi {name}\"\n{greeting}\n",
)]);
let output = compile_mem_with_warnings("main.ink", &files).unwrap();
assert!(
!output.data.containers.is_empty(),
"compilation should succeed"
);
assert!(
output.warnings.iter().any(|w| w.code.as_str() == "E030"),
"expected E030 warning, got: {:?}",
output
.warnings
.iter()
.map(|w| w.code.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn warning_from_included_file_carries_its_path() {
let files: HashMap<&str, &str> = HashMap::from([
("main.ink", "INCLUDE phone.ink\n-> reveal\n"),
("phone.ink", "=== reveal ===\n-> END\nAnd we're off.\n"),
]);
let output = compile_mem_with_warnings("main.ink", &files).unwrap();
let e033 = output
.warnings
.iter()
.find(|w| w.code.as_str() == "E033")
.expect("expected an E033 warning from the unreachable line in phone.ink");
assert_eq!(
e033.path, "phone.ink",
"E033 from phone.ink must be attributed to phone.ink, not the entry"
);
}
#[test]
fn clean_compilation_has_no_warnings() {
let files: HashMap<&str, &str> = HashMap::from([("main.ink", "Hello, world!\n-> END\n")]);
let output = compile_mem_with_warnings("main.ink", &files).unwrap();
assert!(
output.warnings.is_empty(),
"expected no warnings for clean source, got: {:?}",
output
.warnings
.iter()
.map(|w| format!("[{}] {}", w.code.as_str(), w.message))
.collect::<Vec<_>>()
);
}
#[test]
fn glue_in_choice_body_emits_glue_opcode() {
let source = "\
-> knot
=== knot
* [Yes]
Yes considered. <>
* [No]
No way. <>
- He seemed to know.
-> END
";
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let data = compile_mem("main.ink", &files).unwrap();
let mut buf = String::new();
brink_format::write_inkt(&data, &mut buf).unwrap();
eprintln!("{buf}");
assert!(
buf.contains("glue"),
"expected glue opcode in bytecode, got:\n{buf}"
);
}
#[test]
fn glue_in_choice_body_runtime_joins_text() {
let source = "\
-> knot
=== knot
* [Yes]
Yes considered. <>
* [No]
No way. <>
- He seemed to know.
-> END
";
let files: HashMap<&str, &str> = HashMap::from([("main.ink", source)]);
let data = compile_mem("main.ink", &files).unwrap();
let (program, line_tables) = brink_runtime::link(&data).unwrap();
let mut story = Story::<DotNetRng>::new(std::sync::Arc::new(program), line_tables);
let line = story.continue_single().unwrap();
match &line {
Step::Choices(choices) => {
assert_eq!(choices.len(), 2);
story.choose(0).unwrap(); }
other => panic!("expected Choices, got: {other:?}"),
}
let line = story.continue_single().unwrap();
let text = match &line {
Step::Line(line) => line.text.clone(),
other => panic!("expected text output, got: {other:?}"),
};
eprintln!("got text: {text:?}");
assert!(
text.contains("Yes considered. He seemed to know."),
"expected glue to join choice text with gather text, got: {text:?}"
);
}
#[test]
fn compile_error_inline_conditional_with_logic() {
let files: HashMap<&str, &str> = HashMap::from([(
"main.ink",
"VAR x = 0\n{ true: ~ x = 2 }\nValue {x}.\n-> END\n",
)]);
let err = compile_mem("main.ink", &files).expect_err(
"an inline conditional containing a `~` logic statement is invalid ink \
(logic belongs in a multiline block) and should be a compile error",
);
let codes = diagnostic_codes(&err);
assert!(!codes.is_empty(), "expected a diagnostic, got: {codes:?}");
}
#[test]
fn compile_error_inline_multi_branch_conditional() {
let files: HashMap<&str, &str> = HashMap::from([(
"main.ink",
"VAR n = 5\nIt is {n > 8: big|n > 4: medium|small}.\n-> END\n",
)]);
let err = compile_mem("main.ink", &files).expect_err(
"an inline conditional with conditions on each branch is invalid ink \
(multi-branch switches require the multiline block form) and should be \
a compile error",
);
let codes = diagnostic_codes(&err);
assert!(!codes.is_empty(), "expected a diagnostic, got: {codes:?}");
}
#[test]
fn local_directive_reaches_story_data() {
let files: HashMap<&str, &str> = HashMap::from([(
"main.ink",
"\
#@local
VAR mood = 0
VAR shared = 1
-> guard
== guard ==
#@local
Halt! # spoken
-> END
== plaza ==
Busy.
-> END
",
)]);
let story = compile_mem("main.ink", &files).unwrap();
let name = |id: brink_format::NameId| story.name_table[id.0 as usize].as_str();
let mood = story
.variables
.iter()
.find(|v| name(v.name) == "mood")
.unwrap();
let shared = story
.variables
.iter()
.find(|v| name(v.name) == "shared")
.unwrap();
assert!(mood.local, "#@local VAR carries the scope bit");
assert!(!shared.local, "unmarked VAR stays World");
let guard = story
.containers
.iter()
.find(|c| c.name.is_some_and(|n| name(n) == "guard"))
.unwrap();
let plaza = story
.containers
.iter()
.find(|c| c.name.is_some_and(|n| name(n) == "plaza"))
.unwrap();
assert!(guard.local, "#@local knot carries the scope bit");
assert!(!plaza.local, "unmarked knot stays World");
let all_lines = format!("{:?}", story.line_tables);
assert!(
!all_lines.contains("@local"),
"directives never reach runtime content"
);
assert!(all_lines.contains("spoken"), "plain tags survive");
}
#[test]
fn local_directive_implies_visits_counting() {
let files: HashMap<&str, &str> = HashMap::from([(
"main.ink",
"\
-> guard
== guard ==
#@local
Halt!
-> inner
= inner
Deeper.
-> END
== plaza ==
Busy.
-> nook
= nook
#@local
Quiet.
-> END
",
)]);
let story = compile_mem("main.ink", &files).unwrap();
let name = |id: brink_format::NameId| story.name_table[id.0 as usize].as_str();
let container = |wanted: &str| {
story
.containers
.iter()
.find(|c| c.name.is_some_and(|n| name(n) == wanted))
.unwrap_or_else(|| {
let names: Vec<_> = story
.containers
.iter()
.filter_map(|c| c.name.map(name))
.collect();
panic!("container {wanted:?} not found; named containers: {names:?}")
})
};
let visits = |wanted: &str| {
container(wanted)
.counting_flags
.contains(brink_format::CountingFlags::VISITS)
};
assert!(visits("guard"), "#@local knot implies VISITS");
assert!(
visits("guard.inner"),
"stitch under a #@local knot implies VISITS"
);
assert!(visits("plaza.nook"), "#@local stitch implies VISITS");
assert!(
!visits("plaza"),
"unmarked, unread knot keeps counting compiled out"
);
}
#[test]
fn native_cross_file_global_shadow_of_a_fn_value_reference_fails_to_compile() {
let dir = std::env::temp_dir().join(format!(
"brink-compiler-native-1901-cross-file-shadow-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.brink"),
"fn double(x: int): int {\n\
\x20 return x * 2;\n}\n\
fn total(n: int): int {\n\
\x20 return n + 1;\n}\n\
var alias = double\n\
flow main() {\n Val: {total(alias)} -> END\n}\n",
)
.unwrap();
std::fs::write(
dir.join("unrelated.brink"),
"const double = \"unrelated shadow\"\n",
)
.unwrap();
let result = brink_compiler::compile_path_with_options(
&dir.join("main.brink"),
brink_compiler::AnalysisOptions {
dialect: brink_compiler::Dialect::Brink,
types: Some(brink_compiler::TypePolicy::Strict),
..brink_compiler::AnalysisOptions::default()
},
);
std::fs::remove_dir_all(&dir).ok();
let err = result.expect_err(
"an unrelated file's same-named global must never be a legitimate reference target",
);
let brink_compiler::CompileError::Diagnostics(diags) = &err else {
panic!("expected a Diagnostics compile error, got: {err:?}");
};
assert_eq!(
diags.iter().map(|d| d.code).collect::<Vec<_>>(),
vec![brink_ir::DiagnosticCode::E087],
"expected the cross-module privacy gate alone, got: {diags:?}"
);
}