use std::collections::BTreeMap;
use brink_format::DefinitionId;
use brink_ir::{
Block, BlockStmt, Choice, ChoiceSet, CondKind, Conditional, Content, ContentPart, Diagnostic,
DiagnosticCode, ElseBranch, Expr, FileId, HirFile, IfStmt, LambdaBody, PrefixOp, ResolutionMap,
Stmt, StringPart, SymbolIndex, SymbolKind,
};
use rowan::TextRange;
use crate::annotations;
use crate::infer::{InferenceResult, Ty};
use crate::structs::{self, MistypeCtx};
#[must_use]
pub(crate) 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 file_scope_ctx = MistypeCtx {
index,
globals: &globals,
signatures: &inference.signatures,
resolution_by_range: &resolution_by_range,
locals: None,
};
for var in &hir.variables {
walk_expr_for_lambdas(&var.value, file, &file_scope_ctx, &mut out);
}
for cst in &hir.constants {
walk_expr_for_lambdas(&cst.value, file, &file_scope_ctx, &mut out);
}
let root_locals = if hir.root_content.stmts.is_empty() {
None
} else {
let synthetic_id = crate::infer::root_content_def_id(file);
inference.bodies.get(&synthetic_id).map(|b| &b.locals)
};
let root_ctx = MistypeCtx {
index,
globals: &globals,
signatures: &inference.signatures,
resolution_by_range: &resolution_by_range,
locals: root_locals,
};
check_block(&hir.root_content, file, &root_ctx, &mut out);
for knot in &hir.knots {
let kind = knot.symbol_kind();
let knot_locals = annotations::def_id_for(index, file, kind, &knot.name.text)
.and_then(|id| inference.bodies.get(&id))
.map(|b| &b.locals);
let ctx = MistypeCtx {
index,
globals: &globals,
signatures: &inference.signatures,
resolution_by_range: &resolution_by_range,
locals: knot_locals,
};
check_block(&knot.body, file, &ctx, &mut out);
for stitch in &knot.stitches {
let qualified = format!("{}.{}", knot.name.text, stitch.name.text);
let stitch_locals =
annotations::def_id_for(index, file, SymbolKind::Stitch, &qualified)
.and_then(|id| inference.bodies.get(&id))
.map(|b| &b.locals);
let ctx = MistypeCtx {
index,
globals: &globals,
signatures: &inference.signatures,
resolution_by_range: &resolution_by_range,
locals: stitch_locals.or(knot_locals),
};
check_block(&stitch.body, file, &ctx, &mut out);
}
}
}
out
}
fn check_block(block: &Block, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
for stmt in &block.stmts {
check_stmt(stmt, file, ctx, out);
}
}
fn check_stmt(stmt: &Stmt, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
match stmt {
Stmt::Content(c) => check_content(c, file, ctx, out),
Stmt::Conditional(c) => check_conditional(c, file, ctx, out),
Stmt::ChoiceSet(cs) => check_choice_set(cs, file, ctx, out),
Stmt::LabeledBlock(b) => check_block(b, file, ctx, out),
Stmt::Sequence(s) => {
for branch in &s.branches {
check_block(&branch.body, file, ctx, out);
}
}
Stmt::LogicBlock(lb) => {
for bs in &lb.stmts {
check_block_stmt(bs, file, ctx, out);
}
}
Stmt::Await(a) => {
if let Some(cond) = &a.condition {
walk_expr_for_lambdas(cond, file, ctx, out);
check_condition(cond, a.ptr.text_range(), file, ctx, out);
}
}
Stmt::Divert(d) => {
for arg in &d.target.args {
walk_expr_for_lambdas(arg, file, ctx, out);
}
}
Stmt::TunnelCall(tc) => {
for target in &tc.targets {
for arg in &target.args {
walk_expr_for_lambdas(arg, file, ctx, out);
}
}
}
Stmt::ThreadStart(ts) => {
for arg in &ts.target.args {
walk_expr_for_lambdas(arg, file, ctx, out);
}
}
Stmt::TempDecl(t) => {
if let Some(v) = &t.value {
walk_expr_for_lambdas(v, file, ctx, out);
}
}
Stmt::Assignment(a) => {
walk_expr_for_lambdas(&a.target, file, ctx, out);
walk_expr_for_lambdas(&a.value, file, ctx, out);
}
Stmt::Return(r) => {
if let Some(v) = &r.value {
walk_expr_for_lambdas(v, file, ctx, out);
}
for arg in &r.onwards_args {
walk_expr_for_lambdas(arg, file, ctx, out);
}
}
Stmt::ExprStmt(e) | Stmt::AttachElement(e) => walk_expr_for_lambdas(e, file, ctx, out),
Stmt::EndOfLine | Stmt::EndElementRun => {}
}
}
fn check_content(c: &Content, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
for part in &c.parts {
check_content_part(part, file, ctx, out);
}
}
fn check_content_part(
part: &ContentPart,
file: FileId,
ctx: &MistypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
match part {
ContentPart::InlineConditional(cond) => check_conditional(cond, file, ctx, out),
ContentPart::InlineSequence(s) => {
for branch in &s.branches {
check_block(&branch.body, file, ctx, out);
}
}
ContentPart::Span(span) => {
for child in &span.children {
check_content_part(child, file, ctx, out);
}
}
ContentPart::Interpolation(e) => walk_expr_for_lambdas(e, file, ctx, out),
ContentPart::Text(_) | ContentPart::Glue | ContentPart::Spring => {}
}
}
fn check_conditional(
c: &Conditional,
file: FileId,
ctx: &MistypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
let conditions_are_truthiness = !matches!(c.kind, CondKind::Switch(_));
if let CondKind::Switch(scrutinee) = &c.kind {
walk_expr_for_lambdas(scrutinee, file, ctx, out);
}
for branch in &c.branches {
if conditions_are_truthiness && let Some(cond) = &branch.condition {
check_condition_or_binding(
cond,
branch.binding.as_ref(),
c.ptr.text_range(),
file,
ctx,
out,
);
} else if let Some(case_value) = &branch.condition {
walk_expr_for_lambdas(case_value, file, ctx, out);
}
check_block(&branch.body, file, ctx, out);
}
}
fn check_choice_set(cs: &ChoiceSet, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
for choice in &cs.choices {
check_choice(choice, file, ctx, out);
}
check_block(&cs.continuation, file, ctx, out);
}
fn check_choice(choice: &Choice, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
if let Some(cond) = &choice.condition {
check_condition_or_binding(
cond,
choice.binding.as_ref(),
choice.ptr.text_range(),
file,
ctx,
out,
);
}
if let Some(c) = &choice.start_content {
check_content(c, file, ctx, out);
}
if let Some(c) = &choice.bracket_content {
check_content(c, file, ctx, out);
}
if let Some(c) = &choice.inner_content {
check_content(c, file, ctx, out);
}
check_block(&choice.body, file, ctx, out);
}
fn check_block_stmt(bs: &BlockStmt, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
match bs {
BlockStmt::If(i) => check_if(i, file, ctx, out),
BlockStmt::While(w) => {
check_condition_or_binding(
&w.condition,
w.binding.as_ref(),
w.ptr.text_range(),
file,
ctx,
out,
);
for s in &w.body {
check_block_stmt(s, file, ctx, out);
}
}
BlockStmt::For(f) => {
walk_expr_for_lambdas(&f.iterable, file, ctx, out);
for s in &f.body {
check_block_stmt(s, file, ctx, out);
}
}
BlockStmt::Await(a) => {
if let Some(cond) = &a.condition {
walk_expr_for_lambdas(cond, file, ctx, out);
check_condition(cond, a.ptr.text_range(), file, ctx, out);
}
}
BlockStmt::TempDecl(t) => {
if let Some(v) = &t.value {
walk_expr_for_lambdas(v, file, ctx, out);
}
}
BlockStmt::Assignment(a) => {
walk_expr_for_lambdas(&a.target, file, ctx, out);
walk_expr_for_lambdas(&a.value, file, ctx, out);
}
BlockStmt::Return(r) => {
if let Some(v) = &r.value {
walk_expr_for_lambdas(v, file, ctx, out);
}
for arg in &r.onwards_args {
walk_expr_for_lambdas(arg, file, ctx, out);
}
}
BlockStmt::ExprStmt(e) => walk_expr_for_lambdas(e, file, ctx, out),
BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
}
}
fn walk_expr_for_lambdas(
expr: &Expr,
file: FileId,
ctx: &MistypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
match expr {
Expr::Lambda(l) => match &l.body {
LambdaBody::Block { stmts, tail } => {
let pruned_locals = structs::pruned_locals_for_lambda(l, ctx.index, ctx.locals);
let pruned_ctx = MistypeCtx {
index: ctx.index,
globals: ctx.globals,
signatures: ctx.signatures,
resolution_by_range: ctx.resolution_by_range,
locals: Some(&pruned_locals),
};
for bs in stmts {
check_block_stmt(bs, file, &pruned_ctx, out);
}
if let Some(t) = tail {
walk_expr_for_lambdas(t, file, &pruned_ctx, out);
}
}
LambdaBody::Expr(e) => walk_expr_for_lambdas(e, file, ctx, out),
},
Expr::Call(_path, args) => {
for arg in args {
walk_expr_for_lambdas(arg, file, ctx, out);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => {
walk_expr_for_lambdas(inner, file, ctx, out);
}
Expr::Infix(ie) => {
walk_expr_for_lambdas(&ie.lhs, file, ctx, out);
walk_expr_for_lambdas(&ie.rhs, file, ctx, out);
}
Expr::String(s) => {
for part in &s.parts {
if let StringPart::Interpolation(e) = part {
walk_expr_for_lambdas(e, file, ctx, out);
}
}
}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
walk_expr_for_lambdas(e, file, ctx, out);
}
}
Expr::MapLiteral(m) => {
for (k, v) in &m.entries {
walk_expr_for_lambdas(k, file, ctx, out);
walk_expr_for_lambdas(v, file, ctx, out);
}
}
Expr::Index(idx) => {
walk_expr_for_lambdas(&idx.base, file, ctx, out);
walk_expr_for_lambdas(&idx.index, file, ctx, out);
}
Expr::StructLiteral(sl) => {
for (_name, val) in &sl.fields {
walk_expr_for_lambdas(val, file, ctx, out);
}
}
Expr::FieldAccess(fa) => walk_expr_for_lambdas(&fa.base, file, ctx, out),
Expr::FnLiteral(fl) => {
for arg in &fl.args {
walk_expr_for_lambdas(arg, file, ctx, out);
}
}
Expr::RefArg(ra) => walk_expr_for_lambdas(&ra.operand, file, ctx, out),
Expr::Range(r) => {
walk_expr_for_lambdas(&r.start, file, ctx, out);
walk_expr_for_lambdas(&r.end, file, ctx, out);
}
Expr::Fragment(stmts) => {
for s in stmts {
check_stmt(s, file, ctx, out);
}
}
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::Null
| Expr::Path(_)
| Expr::DivertTarget(_)
| Expr::ListLiteral(_) => {}
}
}
fn check_if(i: &IfStmt, file: FileId, ctx: &MistypeCtx<'_>, out: &mut Vec<Diagnostic>) {
check_condition_or_binding(
&i.condition,
i.binding.as_ref(),
i.ptr.text_range(),
file,
ctx,
out,
);
for s in &i.body {
check_block_stmt(s, file, ctx, out);
}
match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => check_if(inner, file, ctx, out),
Some(ElseBranch::Else(stmts)) => {
for s in stmts {
check_block_stmt(s, file, ctx, out);
}
}
None => {}
}
}
fn check_condition_or_binding(
cond: &Expr,
binding: Option<&brink_ir::Name>,
fallback_range: TextRange,
file: FileId,
ctx: &MistypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
walk_expr_for_lambdas(cond, file, ctx, out);
match binding {
Some(_) => check_binding_condition(cond, fallback_range, file, ctx, out),
None => check_condition(cond, fallback_range, file, ctx, out),
}
}
fn check_binding_condition(
cond: &Expr,
fallback_range: TextRange,
file: FileId,
ctx: &MistypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
if condition_is_option(cond, ctx) {
return;
}
let Some(ty) = structs::classify_expr_ty(cond, ctx) else {
return;
};
if matches!(ty, Ty::Unknown | Ty::Conflicted) {
return;
}
out.push(Diagnostic {
file,
range: expr_anchor(cond).unwrap_or(fallback_range),
message: format!(
"{}: the `as` binding unwraps an `Option[T]`, but this condition is `{}`",
DiagnosticCode::E147.title(),
ty.display(),
),
code: DiagnosticCode::E147,
});
}
fn check_condition(
cond: &Expr,
fallback_range: TextRange,
file: FileId,
ctx: &MistypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
if let Expr::Prefix(PrefixOp::Not, inner) = cond {
check_condition(inner, fallback_range, file, ctx, out);
return;
}
if !condition_is_option(cond, ctx) {
return;
}
out.push(Diagnostic {
file,
range: expr_anchor(cond).unwrap_or(fallback_range),
message: format!(
"{} (F27, docs/stdlib-spec.md §1.6)",
DiagnosticCode::E116.title()
),
code: DiagnosticCode::E116,
});
}
fn condition_is_option(cond: &Expr, ctx: &MistypeCtx<'_>) -> bool {
match cond {
Expr::Call(path, _) => {
if let [seg] = path.segments.as_slice()
&& !ctx.resolution_by_range.contains_key(&range_key(path.range))
{
return crate::infer::intrinsic_returns_option(&seg.text);
}
matches!(structs::classify_expr_ty(cond, ctx), Some(Ty::Option(_)))
}
Expr::Path(p) => {
if let [seg] = p.segments.as_slice()
&& seg.text == "none"
&& !ctx.resolution_by_range.contains_key(&range_key(p.range))
{
return true;
}
matches!(structs::classify_expr_ty(cond, ctx), Some(Ty::Option(_)))
}
_ => matches!(structs::classify_expr_ty(cond, ctx), Some(Ty::Option(_))),
}
}
fn expr_anchor(expr: &Expr) -> Option<TextRange> {
match expr {
Expr::Path(p) => Some(p.range),
Expr::Call(path, _) => Some(path.range),
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => expr_anchor(inner),
Expr::Index(idx) => expr_anchor(&idx.base),
Expr::FieldAccess(fa) => expr_anchor(&fa.base),
_ => 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;
fn check_all(src: &str) -> Vec<Diagnostic> {
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(),
);
check(&[(FileId(0), &hir)], &index, &inference, &resolutions)
}
#[test]
fn option_temp_in_inline_conditional_guard_is_e116() {
let diags =
check_all("=== main ===\n~ temp r = find(\"ab\", \"b\")\n{r: found.}\n-> END\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn e116_message_is_not_doubled() {
let diags =
check_all("=== main ===\n~ temp r = find(\"ab\", \"b\")\n{r: found.}\n-> END\n");
assert_eq!(diags.len(), 1, "{diags:?}");
let message = &diags[0].message;
assert_eq!(
message.matches("has no truthiness").count(),
1,
"E116 message repeats its core sentence: {message:?}"
);
assert_eq!(
message.matches("== none").count(),
1,
"E116 message repeats the `== none` idiom: {message:?}"
);
}
#[test]
fn direct_option_intrinsic_call_in_condition_is_e116() {
let diags = check_all("=== main ===\n{find(\"ab\", \"b\"): found.}\n-> END\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn root_content_condition_on_option_is_e116() {
let diags =
check_all("~ temp r = find(\"ab\", \"b\")\n{r: found.}\n=== main ===\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn root_content_non_option_condition_stays_clean() {
let diags = check_all("~ temp n = 3\n{n: nonzero.}\n=== main ===\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn root_content_global_option_condition_is_e116() {
let diags =
check_all("VAR opt: Option<int> = none\n{opt: has value.}\n=== main ===\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn option_temp_in_choice_guard_is_e116() {
let diags =
check_all("=== main ===\n~ temp r = find(\"ab\", \"b\")\n* {r} [go] Went.\n- -> END\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn option_temp_in_block_if_condition_is_e116() {
let diags = check_all(
"=== main ===\n~ {\n temp r = find(\"ab\", \"b\")\n if r {\n \
return\n }\n}\nHi.\n-> END\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn negated_option_condition_is_e116() {
let diags =
check_all("=== main ===\n~ temp r = find(\"ab\", \"b\")\n{not r: absent.}\n-> END\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn explicit_none_comparison_is_clean() {
let diags = check_all(
"=== main ===\n~ temp r = find(\"ab\", \"b\")\n{r == none: absent.}\n\
{r == some(1): at one.}\n-> END\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn int_truthiness_idiom_stays_clean() {
let diags = check_all("=== main ===\n~ temp n = 3\n{n: nonzero.}\n-> END\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unknown_typed_condition_stays_silent() {
let diags = check_all("=== main(r) ===\n{r: yes.}\n-> END\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn switch_case_values_are_not_condition_positions() {
let diags = check_all(
"=== main ===\n~ temp n = 2\n{n:\n- 1: one\n- 2: two\n- else: other\n}\n-> END\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn option_returning_user_function_call_in_condition_is_e116() {
let diags = check_all(
"=== function probe() ===\n~ return find(\"ab\", \"b\")\n\
=== main ===\n{probe(): found.}\n-> END\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn await_condition_of_option_type_is_e116() {
let diags = check_all("=== main ===\n~ temp r = find(\"ab\", \"b\")\n~ await r\n-> END\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
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 top_level_condition_on_option_is_e116_native() {
let diags = check_all_native(
"fn heal(x) {\n if find(\"ab\", \"b\") {\n return 0;\n } else {\n return 1;\n }\n}\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn lambda_body_condition_on_option_is_e116() {
let diags = check_all_native(
"fn heal(x) {\n let f = |y| {\n if find(\"ab\", \"b\") {\n return 0;\n } else {\n return 1;\n }\n };\n return 0;\n}\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn var_lambda_body_condition_on_option_is_e116() {
let diags = check_all_native(
"var f = |y| {\n if find(\"ab\", \"b\") {\n 0\n } else {\n 1\n }\n};\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn lambda_body_non_option_condition_stays_clean() {
let diags = check_all_native(
"fn heal(x) {\n let f = |y| {\n if y {\n return 0;\n } else {\n return 1;\n }\n };\n return 0;\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn lambda_param_shadowing_outer_option_stays_clean() {
let diags = check_all_native(
"fn heal(x) {\n let r = some(3);\n let f = |r| {\n if r {\n return 0;\n } else {\n return 1;\n }\n };\n return 0;\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn lambda_own_temp_shadowing_outer_option_stays_clean() {
let diags = check_all_native(
"fn heal(x) {\n let r = some(3);\n let f = |q| {\n let r = 5;\n if r {\n return 0;\n } else {\n return 1;\n }\n };\n return 0;\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
fn option_lambda_in_condition_position() -> Expr {
let range = TextRange::new(0.into(), 1.into());
let find_call = Expr::Call(
brink_ir::Path {
segments: vec![brink_ir::Name {
text: "find".to_string(),
range,
}],
range,
crosses_module_wall: false,
},
Vec::new(),
);
let if_stmt = IfStmt {
ptr: brink_ir::Provenance::synthetic(brink_ir::NodeClass::If, range),
condition: find_call,
binding: None,
body: vec![BlockStmt::Return(brink_ir::Return {
ptr: None,
kind: brink_ir::ReturnKind::Explicit,
value: None,
onwards_args: Vec::new(),
})],
else_branch: None,
};
Expr::Lambda(Box::new(brink_ir::LambdaExpr {
ptr: brink_ir::Provenance::synthetic(brink_ir::NodeClass::Lambda, range),
params: Vec::new(),
return_type: None,
body: LambdaBody::Block {
stmts: vec![BlockStmt::If(if_stmt)],
tail: None,
},
container_id: None,
}))
}
struct EmptyCtxParts {
index: std::sync::Arc<SymbolIndex>,
globals: BTreeMap<DefinitionId, Ty>,
signatures: BTreeMap<DefinitionId, crate::infer::InferredSig>,
resolution_by_range: BTreeMap<(u32, u32), DefinitionId>,
}
fn empty_ctx() -> EmptyCtxParts {
let (index, _diag) = crate::symbol_index(&[]);
EmptyCtxParts {
index,
globals: BTreeMap::new(),
signatures: BTreeMap::new(),
resolution_by_range: BTreeMap::new(),
}
}
#[test]
fn switch_scrutinee_lambda_condition_is_e116() {
let parts = empty_ctx();
let ctx = MistypeCtx {
index: parts.index.as_ref(),
globals: &parts.globals,
signatures: &parts.signatures,
resolution_by_range: &parts.resolution_by_range,
locals: None,
};
let range = TextRange::new(0.into(), 1.into());
let conditional = Conditional {
ptr: brink_ir::Provenance::synthetic(brink_ir::NodeClass::Conditional, range),
kind: CondKind::Switch(option_lambda_in_condition_position()),
branches: Vec::new(),
};
let mut out = Vec::new();
check_conditional(&conditional, FileId(0), &ctx, &mut out);
assert_eq!(out.len(), 1, "{out:?}");
assert_eq!(out[0].code, DiagnosticCode::E116);
}
#[test]
fn switch_case_value_lambda_condition_is_e116() {
let parts = empty_ctx();
let ctx = MistypeCtx {
index: parts.index.as_ref(),
globals: &parts.globals,
signatures: &parts.signatures,
resolution_by_range: &parts.resolution_by_range,
locals: None,
};
let range = TextRange::new(0.into(), 1.into());
let branch = brink_ir::CondBranch {
ptr: brink_ir::Provenance::synthetic(brink_ir::NodeClass::Conditional, range),
condition: Some(option_lambda_in_condition_position()),
binding: None,
body: Block::from_stmts(Vec::new()),
container_id: None,
};
let conditional = Conditional {
ptr: brink_ir::Provenance::synthetic(brink_ir::NodeClass::Conditional, range),
kind: CondKind::Switch(Expr::Int(0)),
branches: vec![branch],
};
let mut out = Vec::new();
check_conditional(&conditional, FileId(0), &ctx, &mut out);
assert_eq!(out.len(), 1, "{out:?}");
assert_eq!(out[0].code, DiagnosticCode::E116);
}
#[test]
fn annotated_fn_param_option_condition_is_e116() {
let diags = check_all_native(
"fn heal(x: Option<int>): int {\n if x {\n return 0;\n } else {\n return 1;\n }\n}\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn annotated_fn_param_non_option_condition_stays_clean() {
let diags = check_all_native(
"fn heal(x: int): int {\n if x {\n return 0;\n } else {\n return 1;\n }\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn fn_param_own_annotation_re_bound_by_body_stays_clean() {
let diags = check_all_native(
"fn heal(x: Option<int>, y: int): int {\n let x = y;\n if x {\n return 0;\n } else {\n return 1;\n }\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn annotated_lambda_param_option_condition_is_e116() {
let diags = check_all_native(
"fn heal(n: int): int {\n let f = |x: Option<int>| {\n if x {\n return 0;\n } else {\n return 1;\n }\n };\n return n;\n}\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn annotated_lambda_param_non_option_condition_stays_clean() {
let diags = check_all_native(
"fn heal(n: int): int {\n let f = |x: int| {\n if x {\n return 0;\n } else {\n return 1;\n }\n };\n return n;\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn lambda_param_own_annotation_shadowing_outer_non_option_local_is_e116() {
let diags = check_all_native(
"fn heal(x) {\n let r = 5;\n let f = |r: Option<int>| {\n if r {\n return 0;\n } else {\n return 1;\n }\n };\n return 0;\n}\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn lambda_param_own_annotation_re_bound_by_body_stays_clean() {
let diags = check_all_native(
"fn heal(x) {\n let f = |r: Option<int>| {\n let r = 5;\n if r {\n return 0;\n } else {\n return 1;\n }\n };\n return 0;\n}\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn var_lambda_own_annotation_option_condition_is_e116() {
let diags = check_all_native(
"var f = |x: Option<int>| {\n if x {\n 0\n } else {\n 1\n }\n};\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn lambda_captured_outer_option_condition_is_e116() {
let diags = check_all_native(
"fn heal(x) {\n let r = some(3);\n let f = |q| {\n if r {\n return 0;\n } else {\n return 1;\n }\n };\n return 0;\n}\n",
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E116);
}
#[test]
fn hir_file_condition_bearing_fields_stay_in_sync_with_the_e116_walk() {
let parsed = brink_syntax::parse("=== main ===\nHi.\n-> DONE\n");
let (hir, _manifest, diags) = lower(FileId(0), &parsed.tree());
assert!(diags.is_empty(), "fixture must lower cleanly: {diags:?}");
let HirFile {
root_content: _,
knots: _,
variables: _,
constants: _,
lists: _,
structs: _,
externals: _,
includes: _,
module: _,
imports: _,
visibility: _,
was_directives: _,
allow_scopes: _,
element_matches: _,
cue_names: _,
native: _,
claim_handlers: _,
dispatch_handlers: _,
} = hir;
}
}