use std::collections::BTreeMap;
use brink_format::DefinitionId;
use brink_ir::hir::visit::{self, HirVisitor};
use brink_ir::{
Diagnostic, DiagnosticCode, Expr, FileId, FnLiteral, HirFile, ResolutionMap, SymbolIndex,
SymbolKind,
};
use rowan::TextRange;
fn range_key(range: TextRange) -> (u32, u32) {
(range.start().into(), range.end().into())
}
#[must_use]
pub fn check(
files: &[(FileId, &HirFile)],
file_resolutions: &ResolutionMap,
index: &SymbolIndex,
) -> Vec<Diagnostic> {
let mut out = Vec::new();
for &(file, hir) in files {
let by_range: BTreeMap<(u32, u32), DefinitionId> = file_resolutions
.iter()
.filter(|r| r.file == file)
.map(|r| (range_key(r.range), r.target))
.collect();
let mut v = FnValueVisitor {
file,
by_range: &by_range,
index,
diagnostics: &mut out,
};
visit::visit(hir, &mut v);
}
out
}
#[must_use]
pub fn check_native_bare_refs(
files: &[(FileId, &HirFile)],
file_resolutions: &ResolutionMap,
index: &SymbolIndex,
) -> Vec<Diagnostic> {
let mut out = Vec::new();
for &(file, hir) in files {
if !hir.native {
continue;
}
let by_range: BTreeMap<(u32, u32), DefinitionId> = file_resolutions
.iter()
.filter(|r| r.file == file)
.map(|r| (range_key(r.range), r.target))
.collect();
let mut v = NativeFnRefVisitor {
file,
by_range: &by_range,
index,
diagnostics: &mut out,
};
visit::visit_with_decl_initializers(hir, &mut v);
}
out
}
struct NativeFnRefVisitor<'a> {
file: FileId,
by_range: &'a BTreeMap<(u32, u32), DefinitionId>,
index: &'a SymbolIndex,
diagnostics: &'a mut Vec<Diagnostic>,
}
impl HirVisitor for NativeFnRefVisitor<'_> {
fn visit_exprs(&self) -> bool {
true
}
fn enter_expr(&mut self, expr: &Expr) {
let Expr::Path(path) = expr else { return };
let Some(&def) = self.by_range.get(&range_key(path.range)) else {
return;
};
let Some(info) = self.index.symbols.get(&def) else {
return;
};
if !info.is_function_definition() {
return;
}
let target_name = path
.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join("::");
for param in &info.params {
if !param.is_ref {
continue;
}
self.diagnostics.push(Diagnostic {
file: self.file,
range: path.range,
message: format!(
"ref parameter `{}` of `{target_name}` must be bound at creation, but a \
bare-name fn value binds no arguments — the binding form has no native \
spelling (docs/t1c-spec.md §2a)",
param.name,
),
code: DiagnosticCode::E080,
});
}
}
}
struct FnValueVisitor<'a> {
file: FileId,
by_range: &'a BTreeMap<(u32, u32), DefinitionId>,
index: &'a SymbolIndex,
diagnostics: &'a mut Vec<Diagnostic>,
}
impl HirVisitor for FnValueVisitor<'_> {
fn visit_exprs(&self) -> bool {
true
}
fn enter_expr(&mut self, expr: &Expr) {
if let Expr::FnLiteral(fl) = expr {
self.check_fn_literal(fl);
}
}
}
impl FnValueVisitor<'_> {
fn push(&mut self, range: TextRange, message: String, code: DiagnosticCode) {
self.diagnostics.push(Diagnostic {
file: self.file,
range,
message,
code,
});
}
fn check_fn_literal(&mut self, fl: &FnLiteral) {
let target_name = fl
.target
.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join(".");
let Some(&def) = self.by_range.get(&range_key(fl.target.range)) else {
let is_intrinsic = fl.target.segments.len() == 1
&& (crate::resolve::is_builtin_function(&target_name)
|| crate::resolve::is_t1b_stdlib_name(&target_name));
if is_intrinsic {
self.push(
fl.target.range,
format!(
"`#fn` target `{target_name}` is a builtin, not a function \
definition — only a statically-named `=== function ===` can \
become a function value (docs/t1c-spec.md §2)"
),
DiagnosticCode::E079,
);
}
return;
};
let Some(info) = self.index.symbols.get(&def) else {
if def.tag() == brink_format::DefinitionTag::LocalVar {
self.push(
fl.target.range,
format!(
"`#fn` target `{target_name}` does not resolve to a \
statically-named function definition (resolved to a local \
temp/param) — declare the target as `=== function \
{target_name} ===` (docs/t1c-spec.md §2)"
),
DiagnosticCode::E079,
);
}
return;
};
if !info.is_function_definition() {
self.push(
fl.target.range,
format!(
"`#fn` target `{target_name}` does not resolve to a statically-named \
function definition (resolved to a {}) — declare the target as \
`=== function {target_name} ===` (docs/t1c-spec.md §2)",
kind_label(info.kind),
),
DiagnosticCode::E079,
);
return;
}
if fl.args.len() > info.params.len() {
self.push(
fl.ptr.text_range(),
format!(
"`#fn` binds {} argument(s) but `{target_name}` declares only {} \
parameter(s) — bound args are a prefix of the declared row \
(docs/t1c-spec.md §2)",
fl.args.len(),
info.params.len(),
),
DiagnosticCode::E081,
);
}
for (i, param) in info.params.iter().enumerate() {
if !param.is_ref {
continue;
}
match fl.args.get(i) {
None => {
self.push(
fl.ptr.text_range(),
format!(
"ref parameter `{}` of `{target_name}` must be bound at \
creation — all ref params bind in the `#fn` prefix \
(docs/t1c-spec.md §2)",
param.name,
),
DiagnosticCode::E080,
);
}
Some(arg) => self.check_ref_arg(fl, &target_name, ¶m.name, arg),
}
}
}
fn check_ref_arg(&mut self, fl: &FnLiteral, target_name: &str, param_name: &str, arg: &Expr) {
if matches!(arg, Expr::RefArg(_)) {
return;
}
let reject = |cause: &str| {
format!(
"ref parameter `{param_name}` of `{target_name}` must capture a durable \
cell (a VAR, including `#@local` flow-locals) — {cause} \
(docs/t1c-spec.md §2)"
)
};
let Expr::Path(p) = arg else {
let msg = reject("this argument is not an lvalue");
self.push(fl.ptr.text_range(), msg, DiagnosticCode::E080);
return;
};
let Some(&arg_def) = self.by_range.get(&range_key(p.range)) else {
return;
};
let Some(info) = self.index.symbols.get(&arg_def) else {
if arg_def.tag() == brink_format::DefinitionTag::LocalVar {
let msg = reject("a temp/param dies with its frame (value-model §11)");
self.push(p.range, msg, DiagnosticCode::E080);
}
return;
};
if p.segments.len() > 1 {
let msg = reject("a field projection is a heap location, not a cell");
self.push(p.range, msg, DiagnosticCode::E080);
return;
}
match info.kind {
SymbolKind::Variable => {}
SymbolKind::Constant => {
let msg = reject("a CONST is not a mutable cell");
self.push(p.range, msg, DiagnosticCode::E080);
}
SymbolKind::Param | SymbolKind::Temp => {
let msg = reject("a temp/param dies with its frame (value-model §11)");
self.push(p.range, msg, DiagnosticCode::E080);
}
_ => {
let msg = reject(&format!("a {} is not a cell", kind_label(info.kind)));
self.push(p.range, msg, DiagnosticCode::E080);
}
}
}
}
fn kind_label(kind: SymbolKind) -> &'static str {
match kind {
SymbolKind::Knot => "knot",
SymbolKind::Stitch => "stitch",
SymbolKind::Variable => "variable",
SymbolKind::Constant => "constant",
SymbolKind::List => "LIST",
SymbolKind::ListItem => "list item",
SymbolKind::External => "external function",
SymbolKind::Label => "label",
SymbolKind::Param => "parameter",
SymbolKind::Temp => "temp",
SymbolKind::Struct => "STRUCT",
}
}
#[cfg(test)]
mod tests {
use super::*;
use brink_ir::hir::lower;
fn build(src: &str) -> (HirFile, SymbolIndex, ResolutionMap) {
let parsed = brink_syntax::parse(src);
let (hir, manifest, _diag) = lower(FileId(0), &parsed.tree());
let (index, _diag) = crate::symbol_index(&[(FileId(0), &manifest)]);
let (resolutions, _diag) =
crate::resolve(FileId(0), &manifest, &index, &crate::ImportScope::default());
(hir, (*index).clone(), (*resolutions).clone())
}
fn check_src(src: &str) -> Vec<Diagnostic> {
let (hir, index, res) = build(src);
check(&[(FileId(0), &hir)], &res, &index)
}
const HEAL: &str = "=== function heal(ref hp, amount) ===\n~ hp = hp + amount\n~ return hp\n\n";
const PURE: &str = "=== function double(x) ===\n~ return x + x\n\n";
#[test]
fn function_knot_target_is_clean() {
let src = format!("{PURE}VAR v = 0\n=== main ===\n~ temp f = #fn(double, 1)\n-> DONE\n");
let diags = check_src(&src);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn variable_target_is_e079() {
let src = "VAR gold = 5\n=== main ===\n~ temp f = #fn(gold)\n-> DONE\n";
let diags = check_src(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E079);
assert!(diags[0].message.contains("variable"), "{diags:?}");
}
#[test]
fn non_function_knot_target_is_e079() {
let src = "=== plain_knot ===\nHello.\n-> DONE\n=== main ===\n~ temp f = #fn(plain_knot)\n-> DONE\n";
let diags = check_src(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E079);
assert!(diags[0].message.contains("knot"), "{diags:?}");
}
#[test]
fn stdlib_intrinsic_target_is_e079() {
let src = "=== main ===\n~ temp f = #fn(len)\n-> DONE\n";
let diags = check_src(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E079);
assert!(diags[0].message.contains("builtin"), "{diags:?}");
}
#[test]
fn uppercase_builtin_target_is_e079() {
let src = "=== main ===\n~ temp f = #fn(RANDOM)\n-> DONE\n";
let diags = check_src(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E079);
}
#[test]
fn unknown_target_is_left_to_resolutions_e025_not_double_reported() {
let src = "=== main ===\n~ temp f = #fn(nowhere)\n-> DONE\n";
let (hir, index, res) = build(src);
let diags = check(&[(FileId(0), &hir)], &res, &index);
assert!(diags.is_empty(), "E025 owns unknown names: {diags:?}");
}
#[test]
fn external_target_is_e079() {
let src = "EXTERNAL beep(x)\n=== main ===\n~ temp f = #fn(beep)\n-> DONE\n";
let diags = check_src(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E079);
assert!(diags[0].message.contains("external"), "{diags:?}");
}
#[test]
fn ref_param_bound_to_var_is_clean() {
let src = format!(
"{HEAL}VAR player_hp = 10\n=== main ===\n~ temp f = #fn(heal, player_hp)\n-> DONE\n"
);
let diags = check_src(&src);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unbound_ref_param_is_e080() {
let src = format!("{HEAL}=== main ===\n~ temp f = #fn(heal)\n-> DONE\n");
let diags = check_src(&src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E080);
assert!(diags[0].message.contains("must be bound"), "{diags:?}");
}
#[test]
fn ref_param_bound_to_temp_is_e080() {
let src = format!(
"{HEAL}=== main ===\n~ temp local_hp = 10\n~ temp f = #fn(heal, local_hp)\n-> DONE\n"
);
let diags = check_src(&src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E080);
assert!(diags[0].message.contains("frame"), "{diags:?}");
}
#[test]
fn ref_param_bound_to_rvalue_is_e080() {
let src = format!("{HEAL}=== main ===\n~ temp f = #fn(heal, 5 + 1)\n-> DONE\n");
let diags = check_src(&src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E080);
assert!(diags[0].message.contains("lvalue"), "{diags:?}");
}
#[test]
fn ref_param_bound_to_const_is_e080() {
let src = format!(
"CONST LIMIT = 100\n{HEAL}=== main ===\n~ temp f = #fn(heal, LIMIT)\n-> DONE\n"
);
let diags = check_src(&src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E080);
assert!(diags[0].message.contains("CONST"), "{diags:?}");
}
#[test]
fn val_params_never_require_binding() {
let src = format!(
"{HEAL}VAR player_hp = 10\n=== main ===\n~ temp f = #fn(heal, player_hp)\n-> DONE\n"
);
let diags = check_src(&src);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn zero_arg_creation_over_a_ref_free_target_is_clean() {
let src = format!("{PURE}=== main ===\n~ temp f = #fn(double)\n-> DONE\n");
let diags = check_src(&src);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn binding_more_args_than_declared_is_e081() {
let src = format!("{PURE}=== main ===\n~ temp f = #fn(double, 1, 2)\n-> DONE\n");
let diags = check_src(&src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E081);
assert!(diags[0].message.contains("2 argument"), "{diags:?}");
}
#[test]
fn binding_exactly_the_declared_row_is_clean() {
let src = format!("{PURE}=== main ===\n~ temp f = #fn(double, 1)\n-> DONE\n");
let diags = check_src(&src);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn nested_fn_literal_inside_a_call_argument_is_checked() {
let src = "VAR gold = 5\n=== main ===\n~ temp x = double(#fn(gold))\n-> DONE\n\
=== function double(x) ===\n~ return x + x\n";
let diags = check_src(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E079);
}
}