use std::collections::BTreeMap;
use brink_format::DefinitionId;
use brink_ir::hir::visit::{self, HirVisitor};
use brink_ir::{
Diagnostic, DiagnosticCode, Expr, FileId, HirFile, Knot, ResolutionMap, Stitch, SymbolIndex,
SymbolKind,
};
use rowan::TextRange;
use crate::annotations;
use crate::infer::{InferenceResult, InferredSig, Ty};
use crate::structs::{self, MistypeCtx};
fn domain_checked_name(name: &str) -> bool {
matches!(name, "int" | "float")
}
#[must_use]
pub fn check(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
inference: &InferenceResult,
resolutions: &ResolutionMap,
) -> Vec<Diagnostic> {
let globals = crate::infer::collect_globals(files, index, None);
let mut out = Vec::new();
for &(file, hir) in files {
let resolution_by_range = resolution_index(resolutions, file);
let mut v = ConversionVisitor {
file,
index,
globals: &globals,
signatures: &inference.signatures,
bodies: &inference.bodies,
resolution_by_range: &resolution_by_range,
current_knot_name: None,
knot_locals: None,
stitch_locals: None,
lambda_locals: Vec::new(),
diagnostics: &mut out,
};
visit::visit_with_decl_initializers(hir, &mut v);
}
out
}
struct ConversionVisitor<'a> {
file: FileId,
index: &'a SymbolIndex,
globals: &'a BTreeMap<DefinitionId, Ty>,
signatures: &'a BTreeMap<DefinitionId, InferredSig>,
bodies: &'a BTreeMap<DefinitionId, crate::infer::BodyTypes>,
resolution_by_range: &'a BTreeMap<(u32, u32), DefinitionId>,
current_knot_name: Option<String>,
knot_locals: Option<&'a BTreeMap<String, Ty>>,
stitch_locals: Option<&'a BTreeMap<String, Ty>>,
lambda_locals: Vec<BTreeMap<String, Ty>>,
diagnostics: &'a mut Vec<Diagnostic>,
}
impl ConversionVisitor<'_> {
fn current_locals(&self) -> Option<&BTreeMap<String, Ty>> {
self.lambda_locals
.last()
.or_else(|| self.stitch_locals.or(self.knot_locals))
}
fn knot_def_id(&self, knot: &Knot) -> Option<DefinitionId> {
let kind = knot.symbol_kind();
annotations::def_id_for(self.index, self.file, kind, &knot.name.text)
}
}
impl HirVisitor for ConversionVisitor<'_> {
fn visit_exprs(&self) -> bool {
true
}
fn enter_knot(&mut self, knot: &Knot) {
self.current_knot_name = Some(knot.name.text.clone());
self.knot_locals = self
.knot_def_id(knot)
.and_then(|id| self.bodies.get(&id))
.map(|b| &b.locals);
}
fn exit_knot(&mut self, _knot: &Knot) {
self.current_knot_name = None;
self.knot_locals = None;
}
fn enter_stitch(&mut self, stitch: &Stitch) {
self.stitch_locals = self.current_knot_name.as_ref().and_then(|knot_name| {
let qualified = format!("{knot_name}.{}", stitch.name.text);
annotations::def_id_for(self.index, self.file, SymbolKind::Stitch, &qualified)
.and_then(|id| self.bodies.get(&id))
.map(|b| &b.locals)
});
}
fn exit_stitch(&mut self, _stitch: &Stitch) {
self.stitch_locals = None;
}
fn enter_expr(&mut self, expr: &Expr) {
let ctx = MistypeCtx {
index: self.index,
globals: self.globals,
signatures: self.signatures,
resolution_by_range: self.resolution_by_range,
locals: self
.lambda_locals
.last()
.or_else(|| self.stitch_locals.or(self.knot_locals)),
};
check_call(expr, self.file, &ctx, self.diagnostics);
}
fn enter_lambda(&mut self, l: &brink_ir::LambdaExpr) {
let pruned = structs::pruned_locals_for_lambda(l, self.index, self.current_locals());
self.lambda_locals.push(pruned);
}
fn exit_lambda(&mut self, _l: &brink_ir::LambdaExpr) {
self.lambda_locals.pop();
}
}
fn check_call(expr: &Expr, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
let Expr::Call(path, args) = expr else {
return;
};
let [seg] = path.segments.as_slice() else {
return;
};
if !domain_checked_name(&seg.text) {
return;
}
if ctx.resolution_by_range.contains_key(&range_key(path.range)) {
return; }
let [arg] = args.as_slice() else {
return; };
let Some(kind) = classify_out_of_domain(arg, ctx) else {
return;
};
out.push(Diagnostic {
file,
range: path.range,
message: format!(
"{}: `{}(x)` cannot accept a {kind} value under `types = strict` — permitted \
domain: int, float, string, bool (docs/typed-mode-spec.md §4)",
DiagnosticCode::E078.title(),
seg.text,
),
code: DiagnosticCode::E078,
});
}
fn classify_out_of_domain(expr: &Expr, ctx: &MistypeCtx<'_>) -> Option<&'static str> {
if let Some(kind) = literal_out_of_domain_kind(expr) {
return Some(kind);
}
let ty = structs::classify_expr_ty(expr, ctx)?;
domain_kind_for_ty(&ty)
}
fn literal_out_of_domain_kind(expr: &Expr) -> Option<&'static str> {
match expr {
Expr::DivertTarget(_) => Some("divert"),
Expr::ListLiteral(_) => Some("list"),
Expr::ArrayLiteral(_) => Some("array"),
Expr::MapLiteral(_) => Some("map"),
Expr::StructLiteral(_) => Some("struct"),
_ => None,
}
}
fn domain_kind_for_ty(ty: &Ty) -> Option<&'static str> {
match ty {
Ty::Divert => Some("divert"),
Ty::List(_) => Some("list"),
Ty::Array(_) => Some("array"),
Ty::Map(_, _) => Some("map"),
Ty::Struct(_) => Some("struct"),
_ => None,
}
}
fn range_key(range: TextRange) -> (u32, u32) {
(range.start().into(), range.end().into())
}
fn resolution_index(
resolutions: &ResolutionMap,
file: FileId,
) -> BTreeMap<(u32, u32), DefinitionId> {
resolutions
.iter()
.filter(|r| r.file == file)
.map(|r| (range_key(r.range), r.target))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use brink_ir::hir::lower;
use brink_ir::{FileId, SymbolIndex};
fn build_with_inference(src: &str) -> (HirFile, SymbolIndex, ResolutionMap, InferenceResult) {
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());
let inference = crate::infer_project(
&[(FileId(0), &hir)],
&index,
&resolutions,
None,
&BTreeMap::new(),
);
(hir, (*index).clone(), (*resolutions).clone(), inference)
}
fn check_all(src: &str) -> Vec<Diagnostic> {
let (hir, index, resolutions, inference) = build_with_inference(src);
check(&[(FileId(0), &hir)], &index, &inference, &resolutions)
}
fn check_all_native(src: &str) -> Vec<Diagnostic> {
let parsed = brink_syntax_native::parse(src);
assert!(parsed.errors().is_empty(), "{:?}", parsed.errors());
let (hir, manifest, _diag) = brink_ir::hir::lower_native::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());
let inference = crate::infer_project(
&[(FileId(0), &hir)],
&index,
&resolutions,
None,
&BTreeMap::new(),
);
check(&[(FileId(0), &hir)], &index, &inference, &resolutions)
}
#[test]
fn a_bad_conversion_in_a_lambda_statement_of_a_var_initializer_is_e078() {
let diags = check_all_native("var f = ||: int {\n let x = int(Map { 1: 2 });\n 0\n};\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("map"), "{:?}", diags[0].message);
}
#[test]
fn a_bad_conversion_in_a_lambda_tail_of_a_var_initializer_is_still_e078() {
let diags = check_all_native("var f = ||: int {\n let a = 1;\n int(Map { 1: 2 })\n};\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
}
#[test]
fn lambda_param_shadowing_outer_array_local_is_not_misclassified_as_out_of_domain() {
let diags = check_all_native(
"fn build() {\n let x = [1, 2, 3];\n let f = |x: int| {\n int(x)\n };\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn lambda_param_own_annotation_still_flags_a_genuine_out_of_domain_conversion() {
let diags = check_all_native(
"fn build() {\n let x = [1, 2, 3];\n let f = |x: Map<int, int>| {\n int(x)\n };\n}\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
}
#[test]
fn lambda_expr_body_param_shadowing_outer_array_local_is_not_misclassified() {
let diags =
check_all_native("fn build() {\n let x = [1, 2, 3];\n let f = |x: int| int(x);\n}\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn annotated_fn_param_out_of_domain_conversion_is_e078() {
let diags = check_all_native("fn build(x: Map<int, int>) {\n let y = int(x);\n}\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("map"), "{:?}", diags[0].message);
}
#[test]
fn annotated_fn_param_in_domain_conversion_stays_clean() {
let diags = check_all_native("fn build(x: float) {\n let y = int(x);\n}\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn int_of_a_divert_target_literal_is_e078() {
let diags =
check_all("=== knot ===\nHello.\n-> DONE\n=== main ===\n~ x = int(-> knot)\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("divert"));
}
#[test]
fn float_of_an_array_literal_is_e078() {
let diags = check_all("=== main ===\n~ x = float(#[1, 2])\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("array"));
}
#[test]
fn int_of_a_map_literal_is_e078() {
let diags = check_all("=== main ===\n~ x = int(#{\"a\": 1})\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert!(diags[0].message.contains("map"));
}
#[test]
fn int_of_a_struct_literal_is_e078() {
let diags = check_all(
"STRUCT Point = #{x: float}\n=== main ===\n~ y = int(Point#{x: 1.0})\n-> DONE\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert!(diags[0].message.contains("struct"));
}
#[test]
fn int_of_a_list_literal_is_e078() {
let diags = check_all("LIST Colors = red, blue\n=== main ===\n~ x = int((red))\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert!(diags[0].message.contains("list"));
}
#[test]
fn int_of_a_numeric_literal_is_clean() {
let diags = check_all("=== main ===\n~ x = int(2.9)\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn int_of_a_string_literal_is_clean() {
let diags = check_all("=== main ===\n~ x = int(\"42\")\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn int_of_an_in_domain_variable_is_clean() {
let diags = check_all("VAR gold = 5\n=== main ===\n~ x = int(gold)\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn string_of_a_divert_target_is_never_checked() {
let diags = check_all(
"=== knot ===\nHello.\n-> DONE\n=== main ===\n~ x = string(-> knot)\n-> DONE\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn author_defined_int_shadowing_the_builtin_is_never_checked() {
let diags = check_all(
"=== function int(x) ===\n~ return 0\n=== main ===\n~ y = int(-> main)\n-> DONE\n",
);
assert!(
diags.is_empty(),
"a resolved call to the author's own `int` must never be flagged: {diags:?}"
);
}
#[test]
fn wrong_arity_int_call_is_not_flagged_here() {
let diags = check_all("=== main ===\n~ x = int(1, 2)\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn global_variable_valued_argument_fires_when_provably_mistyped() {
let diags = check_all(
"=== knot ===\nHello.\n-> DONE\nVAR v = -> knot\n=== main ===\n~ x = int(v)\n-> DONE\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(
diags[0].message.contains("divert"),
"{:?}",
diags[0].message
);
}
#[test]
fn global_array_valued_argument_fires_since_the_value_ty_widening() {
let diags = check_all("VAR xs = #[1, 2]\n=== main ===\n~ x = int(xs)\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("array"), "{:?}", diags[0].message);
}
#[test]
fn global_variable_valued_argument_of_in_domain_type_is_clean() {
let diags = check_all("VAR v = 1.0\n=== main ===\n~ x = int(v)\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unused_param_variable_valued_argument_stays_silent_when_unknown() {
let diags = check_all("=== main(n) ===\n~ x = int(n)\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn temp_variable_valued_argument_fires_when_provably_mistyped() {
let diags = check_all("=== main ===\n~ temp xs = #{\"a\": 1}\n~ x = int(xs)\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("map"), "{:?}", diags[0].message);
}
#[test]
fn call_valued_argument_fires_when_provably_mistyped() {
let diags = check_all(
"STRUCT Point = #{x: float}\n\
=== function shape() ===\n~ return Point#{x: 1.0}\n\
=== main ===\n~ y = int(shape())\n-> DONE\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(
diags[0].message.contains("struct"),
"{:?}",
diags[0].message
);
}
#[test]
fn call_valued_argument_of_in_domain_return_type_is_clean() {
let diags = check_all(
"=== function label() ===\n~ return \"a\"\n\
=== main ===\n~ x = int(label())\n-> DONE\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn index_valued_argument_fires_when_provably_mistyped() {
let diags = check_all(
"=== main ===\n\
~ temp xs = #[#[1, 2], #[3, 4]]\n~ x = int(xs[0])\n-> DONE\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("array"), "{:?}", diags[0].message);
}
#[test]
fn index_valued_argument_of_in_domain_result_is_clean() {
let diags = check_all(
"=== main ===\n\
~ temp xs = #[1.0, 2.0]\n~ x = int(xs[0])\n-> DONE\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn index_valued_argument_stays_silent_when_unknown() {
let diags = check_all("=== main(xs) ===\n~ x = int(xs[0])\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn stitch_local_variable_valued_argument_fires_when_provably_mistyped() {
let diags =
check_all("=== room ===\n= inside\n~ temp t = #[1, 2]\n~ x = int(t)\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("array"), "{:?}", diags[0].message);
}
#[test]
fn variable_valued_argument_inside_var_initializer_uses_global_scope_only() {
let diags = check_all("LIST Colors = red, blue\nVAR other = (red)\nVAR x = int(other)\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E078);
assert!(diags[0].message.contains("list"), "{:?}", diags[0].message);
}
#[test]
fn mistyped_variable_argument_diagnostic_is_order_independent() {
let forward = "=== knot ===\nHello.\n-> DONE\nVAR v = -> knot\n\
=== main ===\n~ x = int(v)\n~ y = int(2.9)\n-> DONE\n";
let reversed = "=== knot ===\nHello.\n-> DONE\nVAR v = -> knot\n\
=== main ===\n~ y = int(2.9)\n~ x = int(v)\n-> DONE\n";
let diags_f = check_all(forward);
let diags_r = check_all(reversed);
assert_eq!(diags_f.len(), 1, "{diags_f:?}");
assert_eq!(diags_f[0].code, DiagnosticCode::E078);
assert!(
diags_f[0].message.contains("divert"),
"{:?}",
diags_f[0].message
);
assert_eq!(diags_r.len(), 1, "{diags_r:?}");
assert_eq!(diags_r[0].code, DiagnosticCode::E078);
assert!(
diags_r[0].message.contains("divert"),
"{:?}",
diags_r[0].message
);
}
}