use std::collections::{BTreeMap, BTreeSet};
use brink_format::DefinitionId;
use brink_ir::hir::expr_span;
use brink_ir::hir::visit::{self, ContentContext, HirVisitor};
use brink_ir::{
Choice, ConstDecl, Content, Diagnostic, DiagnosticCode, Expr, FileId, HirFile, InfixOp, Knot,
ResolutionMap, Stitch, Stmt, SymbolIndex, SymbolKind, VarDecl,
};
use rowan::TextRange;
use crate::annotations;
use crate::infer::{self, CoalesceError, InferenceResult, InferredSig, Ty};
use crate::structs::{self, MistypeCtx};
use crate::ufcs::{NodeKey, SideTable};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoalesceShape {
PreserveOption,
Collapse,
RuntimeCheck,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoalesceStep {
pub lhs: Ty,
pub rhs: Ty,
pub result: Ty,
pub shape: CoalesceShape,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoalesceChain {
pub steps: Vec<CoalesceStep>,
}
pub type CoalesceTable = SideTable<CoalesceChain>;
#[must_use]
pub fn to_lir_lookup(table: &CoalesceTable) -> brink_ir::lir::CoalesceLookup {
let entries = table
.iter()
.map(|(key, chain)| {
let range = TextRange::new(key.range.0.into(), key.range.1.into());
let shapes = chain
.steps
.iter()
.map(|step| match step.shape {
CoalesceShape::PreserveOption => brink_ir::lir::CoalesceShape::PreserveOption,
CoalesceShape::Collapse => brink_ir::lir::CoalesceShape::Collapse,
CoalesceShape::RuntimeCheck => brink_ir::lir::CoalesceShape::RuntimeCheck,
})
.collect();
(key.file, range, shapes)
})
.collect();
brink_ir::lir::CoalesceLookup::from_entries(entries)
}
#[must_use]
pub fn project_has_coalesce(hir: &HirFile) -> bool {
struct Scan {
found: bool,
}
impl HirVisitor for Scan {
fn visit_exprs(&self) -> bool {
true
}
fn enter_expr(&mut self, expr: &Expr) {
if coalesce_operands(expr).is_some() {
self.found = true;
}
}
}
let mut scan = Scan { found: false };
visit::visit_with_decl_initializers(hir, &mut scan);
scan.found
}
#[must_use]
pub fn resolve(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
inference: &InferenceResult,
resolutions: &ResolutionMap,
) -> (CoalesceTable, Vec<Diagnostic>) {
let globals = crate::infer::collect_globals(files, index, None);
let mut out = Vec::new();
let mut table = CoalesceTable::new();
for &(file, hir) in files {
let resolution_by_range = resolution_index(resolutions, file);
let mut v = CoalesceVisitor {
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,
fallback: TextRange::new(0.into(), 0.into()),
spine: BTreeSet::new(),
table: &mut table,
lambda_locals: Vec::new(),
diagnostics: &mut out,
};
visit::visit_with_decl_initializers(hir, &mut v);
}
(table, out)
}
#[must_use]
pub fn check(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
inference: &InferenceResult,
resolutions: &ResolutionMap,
) -> Vec<Diagnostic> {
resolve(files, index, inference, resolutions).1
}
struct CoalesceVisitor<'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>>,
fallback: TextRange,
spine: BTreeSet<usize>,
table: &'a mut CoalesceTable,
lambda_locals: Vec<BTreeMap<String, Ty>>,
diagnostics: &'a mut Vec<Diagnostic>,
}
impl CoalesceVisitor<'_> {
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 CoalesceVisitor<'_> {
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_var_decl(&mut self, var: &VarDecl) {
self.fallback = var.ptr.text_range();
self.current_knot_name = None;
self.knot_locals = None;
self.stitch_locals = None;
}
fn enter_const_decl(&mut self, konst: &ConstDecl) {
self.fallback = konst.ptr.text_range();
self.current_knot_name = None;
self.knot_locals = None;
self.stitch_locals = None;
}
fn enter_stmt(&mut self, stmt: &Stmt) {
if let Some(range) = stmt_anchor(stmt) {
self.fallback = range;
}
}
fn enter_content(&mut self, content: &Content, _ctx: ContentContext) {
if let Some(ptr) = content.ptr {
self.fallback = ptr.text_range();
}
}
fn enter_choice(&mut self, choice: &Choice) {
self.fallback = choice.ptr.text_range();
}
fn enter_expr(&mut self, expr: &Expr) {
if self.spine.remove(&std::ptr::from_ref(expr).addr()) {
return;
}
if coalesce_operands(expr).is_none() {
return;
}
for node in chain_spine(expr).iter().skip(1) {
self.spine.insert(std::ptr::from_ref(*node).addr());
}
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)),
};
analyze_chain(
expr,
self.fallback,
self.file,
&ctx,
self.table,
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 coalesce_operands(expr: &Expr) -> Option<(&Expr, &Expr)> {
match expr {
Expr::Infix(ie) if ie.op == InfixOp::Coalesce => Some((&ie.lhs, &ie.rhs)),
_ => None,
}
}
fn chain_spine(root: &Expr) -> Vec<&Expr> {
let mut spine = Vec::new();
let mut cursor = root;
while let Some((lhs, _)) = coalesce_operands(cursor) {
spine.push(cursor);
cursor = lhs;
}
spine
}
fn analyze_chain(
root: &Expr,
fallback: TextRange,
file: FileId,
ctx: &MistypeCtx<'_>,
table: &mut CoalesceTable,
out: &mut Vec<Diagnostic>,
) {
let spine = chain_spine(root);
let mut steps = Vec::with_capacity(spine.len());
let mut carried: Option<Ty> = None;
for node in spine.iter().rev() {
let Some((lhs_expr, rhs_expr)) = coalesce_operands(node) else {
return;
};
let lhs = match carried.take() {
Some(ty) => ty,
None => classify_coalesce_operand(lhs_expr, ctx).unwrap_or(Ty::Unknown),
};
let Some(rhs) = classify_coalesce_operand(rhs_expr, ctx) else {
return;
};
match infer::coalesce(&lhs, &rhs) {
Ok(result) => {
let shape = step_shape(&lhs, &rhs);
carried = Some(result.clone());
steps.push(CoalesceStep {
lhs,
rhs,
result,
shape,
});
}
Err(err) => {
let range = expr_anchor(lhs_expr)
.or_else(|| expr_anchor(rhs_expr))
.unwrap_or(fallback);
out.push(Diagnostic {
file,
range,
message: coalesce_error_message(&err),
code: DiagnosticCode::E066,
});
return;
}
}
}
if steps.is_empty() {
return;
}
let Some(range) = expr_span(root) else {
return;
};
table.insert(NodeKey::new(file, range), CoalesceChain { steps });
}
fn step_shape(lhs: &Ty, rhs: &Ty) -> CoalesceShape {
if matches!(lhs, Ty::Unknown | Ty::Conflicted) {
return CoalesceShape::RuntimeCheck;
}
if matches!(rhs, Ty::Option(_)) {
CoalesceShape::PreserveOption
} else {
CoalesceShape::Collapse
}
}
fn coalesce_error_message(err: &CoalesceError) -> String {
match err {
CoalesceError::LeftNotOption(ty) => format!(
"{}: `or`-coalescing requires an `Option[T]` left-hand side (docs/stdlib-spec.md \
§1.6a) — found `{}`",
DiagnosticCode::E066.title(),
ty.display(),
),
CoalesceError::Mismatch { element, fallback } => format!(
"{}: `or`-coalescing's fallback type disagrees with the `Option`'s element type \
(docs/stdlib-spec.md §1.6a) — `{}` vs `{}`",
DiagnosticCode::E066.title(),
element.display(),
fallback.display(),
),
}
}
fn classify_coalesce_operand(expr: &Expr, ctx: &MistypeCtx<'_>) -> Option<Ty> {
match expr {
Expr::Call(path, args) => {
if let [seg] = path.segments.as_slice()
&& !ctx.resolution_by_range.contains_key(&range_key(path.range))
{
if seg.text == "some" {
let elem = args
.first()
.and_then(|a| classify_coalesce_operand(a, ctx))
.unwrap_or(Ty::Unknown);
return Some(Ty::Option(Box::new(elem)));
}
if crate::infer::intrinsic_returns_option(&seg.text) {
return Some(Ty::Option(Box::new(Ty::Unknown)));
}
}
structs::classify_expr_ty(expr, ctx)
}
Expr::Path(p) => {
if let [seg] = p.segments.as_slice()
&& seg.text == "none"
&& !ctx.resolution_by_range.contains_key(&range_key(p.range))
{
return Some(Ty::Option(Box::new(Ty::Unknown)));
}
structs::classify_expr_ty(expr, ctx)
}
_ => structs::classify_expr_ty(expr, ctx),
}
}
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),
Expr::Infix(ie) => expr_anchor(&ie.lhs).or_else(|| expr_anchor(&ie.rhs)),
_ => None,
}
}
fn stmt_anchor(stmt: &Stmt) -> Option<TextRange> {
match stmt {
Stmt::Content(c) => c.ptr.map(|p| p.text_range()),
Stmt::Divert(d) => d.ptr.map(|p| p.text_range()),
Stmt::TunnelCall(t) => Some(t.ptr.text_range()),
Stmt::ThreadStart(t) => Some(t.ptr.text_range()),
Stmt::TempDecl(t) => Some(t.ptr.text_range()),
Stmt::Assignment(a) => Some(a.ptr.text_range()),
Stmt::Return(r) => r.ptr.map(|p| p.text_range()),
Stmt::Conditional(c) => Some(c.ptr.text_range()),
Stmt::Sequence(s) => Some(s.ptr.text_range()),
Stmt::LogicBlock(lb) => Some(lb.ptr.text_range()),
Stmt::Await(a) => Some(a.ptr.text_range()),
Stmt::ChoiceSet(_)
| Stmt::LabeledBlock(_)
| Stmt::ExprStmt(_)
| Stmt::EndOfLine
| Stmt::AttachElement(_)
| Stmt::EndElementRun => 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)]
#[expect(
clippy::panic,
reason = "test-only assertions; see sibling test modules"
)]
mod tests {
use super::*;
use brink_ir::{FileId as HirFileId, SymbolIndex};
fn build_native(src: &str) -> (HirFile, SymbolIndex, ResolutionMap, InferenceResult) {
let parse = brink_syntax_native::parse(src);
assert!(
parse.errors().is_empty(),
"fixture must parse cleanly: {:?}",
parse.errors()
);
let tree = parse.tree();
let (hir, manifest, _diag) = brink_ir::hir::lower_native::lower(HirFileId(0), &tree);
let (index, _diag) = crate::symbol_index(&[(HirFileId(0), &manifest)]);
let (resolutions, _diag) = crate::resolve(
HirFileId(0),
&manifest,
&index,
&crate::ImportScope::default(),
);
let inference = crate::infer_project(
&[(HirFileId(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_native(src);
check(&[(HirFileId(0), &hir)], &index, &inference, &resolutions)
}
#[test]
fn a_bad_chain_in_a_lambda_statement_of_a_var_initializer_is_e066() {
let diags = check_all("var f = ||: int {\n let x = 5 or 9;\n 0\n};\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E066);
}
#[test]
fn a_chain_in_a_lambda_statement_of_a_var_initializer_trips_the_project_gate() {
let (hir, _index, _res, _inf) =
build_native("var f = ||: int {\n let x = some(1) or 2;\n 0\n};\n");
assert!(project_has_coalesce(&hir));
}
#[test]
fn a_chain_in_a_lambda_statement_of_a_var_initializer_is_recorded_in_the_table() {
let chain = only_chain("var f = ||: int {\n let x = some(1) or 2;\n 0\n};\n");
assert_eq!(chain.steps.len(), 1, "{chain:?}");
assert_eq!(chain.steps[0].rhs, Ty::Int);
}
#[test]
fn annotated_fn_param_non_option_lhs_of_or_is_not_visible_to_e066() {
let src = "fn build(x: int) {\n let y = x or 5;\n}\n";
let (hir, index, resolutions, inference) = build_native(src);
let (table, diags) = resolve(&[(HirFileId(0), &hir)], &index, &inference, &resolutions);
assert!(
diags.is_empty(),
"documents the gap: no E066 fires despite `x: int` disagreeing \
with `or`'s Option requirement: {diags:?}"
);
let (_key, chain) = table.iter().next().expect("one recorded chain");
assert_eq!(
chain.steps[0].lhs,
Ty::Option(Box::new(Ty::Int)),
"the coalesce arm's own forced back-propagation, not `x`'s real \
`int` annotation, is what the recorded shape reflects: {chain:?}"
);
let mismatch_diags =
annotations::mismatches(&[(HirFileId(0), &hir)], &index, &inference, None);
assert_eq!(mismatch_diags.len(), 1, "{mismatch_diags:?}");
assert_eq!(mismatch_diags[0].code, DiagnosticCode::E063);
assert_eq!(
mismatch_diags[0].message,
"annotated type `int` disagrees with the type inferred from usage (`Option<int>`)",
"{mismatch_diags:?}"
);
}
#[test]
fn non_option_left_hand_side_is_e066() {
let diags = check_all("flow main() {\n {5 or 9}\n -> END\n}\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E066);
}
#[test]
fn mismatched_fallback_type_is_e066() {
let diags = check_all("flow main() {\n {some(1) or \"text\"}\n -> END\n}\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E066);
}
#[test]
fn collapse_form_with_agreeing_types_is_clean() {
let diags = check_all("flow main() {\n {some(1) or 2}\n -> END\n}\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn two_option_form_with_agreeing_types_is_clean() {
let diags = check_all("flow main() {\n {some(1) or none}\n -> END\n}\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unclassifiable_operand_stays_silently_unchecked() {
let diags = check_all("flow main(x) {\n {x or 9}\n -> END\n}\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unpinned_left_hand_side_records_a_runtime_check_step() {
let src = concat!(
"fn f(x) {\n return !x or 9;\n}\n",
"flow main() {\n -> END\n}\n",
);
let chain = only_chain(src);
assert_eq!(chain.steps.len(), 1, "{chain:?}");
let step = &chain.steps[0];
assert_eq!(step.lhs, Ty::Unknown);
assert_eq!(step.rhs, Ty::Int);
assert_eq!(step.result, Ty::Unknown);
assert_eq!(step.shape, CoalesceShape::RuntimeCheck);
}
fn table_of(src: &str) -> CoalesceTable {
let (hir, index, resolutions, inference) = build_native(src);
resolve(&[(HirFileId(0), &hir)], &index, &inference, &resolutions).0
}
fn only_chain(src: &str) -> CoalesceChain {
let table = table_of(src);
assert_eq!(table.len(), 1, "expected exactly one chain: {table:?}");
let (_key, chain) = table.iter().next().expect("one entry");
chain.clone()
}
fn opt(inner: Ty) -> Ty {
Ty::Option(Box::new(inner))
}
#[test]
fn collapse_form_records_the_collapsed_value_type() {
let chain = only_chain("flow main() {\n {some(1) or 2}\n -> END\n}\n");
assert_eq!(chain.steps.len(), 1);
let step = &chain.steps[0];
assert_eq!(step.lhs, opt(Ty::Int));
assert_eq!(step.rhs, Ty::Int);
assert_eq!(step.result, Ty::Int);
assert_eq!(step.shape, CoalesceShape::Collapse);
}
#[test]
fn two_option_form_records_preserved_optionality() {
let chain = only_chain("flow main() {\n {some(1) or none}\n -> END\n}\n");
assert_eq!(chain.steps.len(), 1);
let step = &chain.steps[0];
assert_eq!(step.shape, CoalesceShape::PreserveOption);
assert!(
matches!(step.result, Ty::Option(_)),
"optionality survives: {:?}",
step.result
);
}
#[test]
fn a_call_fallback_is_typed_from_its_return_type_not_its_syntax() {
let src = concat!(
"fn maybe() {\n return some(7);\n}\n",
"flow main() {\n {some(1) or maybe()}\n -> END\n}\n",
);
let chain = only_chain(src);
assert_eq!(chain.steps.len(), 1);
assert_eq!(chain.steps[0].rhs, opt(Ty::Int));
assert_eq!(chain.steps[0].shape, CoalesceShape::PreserveOption);
}
#[test]
fn a_chain_records_every_step_innermost_first() {
let src = concat!(
"fn maybe() {\n return some(7);\n}\n",
"flow main() {\n {some(1) or maybe() or 99}\n -> END\n}\n",
);
let chain = only_chain(src);
assert_eq!(chain.steps.len(), 2, "{chain:?}");
assert_eq!(chain.steps[0].shape, CoalesceShape::PreserveOption);
assert_eq!(chain.steps[0].result, opt(Ty::Int));
assert_eq!(chain.steps[1].lhs, opt(Ty::Int));
assert_eq!(chain.steps[1].rhs, Ty::Int);
assert_eq!(chain.steps[1].result, Ty::Int);
assert_eq!(chain.steps[1].shape, CoalesceShape::Collapse);
}
#[test]
fn an_option_typed_path_fallback_preserves_optionality() {
let src = concat!(
"fn pick() {\n",
" let fallback = some(3);\n",
" return some(1) or fallback;\n",
"}\n",
"flow main() {\n -> END\n}\n",
);
let chain = only_chain(src);
assert_eq!(chain.steps.len(), 1, "{chain:?}");
assert_eq!(chain.steps[0].rhs, opt(Ty::Int));
assert_eq!(chain.steps[0].shape, CoalesceShape::PreserveOption);
}
#[test]
fn a_mismatch_at_a_later_chain_step_is_now_e066() {
let diags = check_all("flow main() {\n {some(1) or none or \"text\"}\n -> END\n}\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E066);
}
#[test]
fn an_ill_typed_chain_records_no_verdict() {
let table = table_of("flow main() {\n {some(1) or \"text\"}\n -> END\n}\n");
assert!(table.is_empty(), "{table:?}");
}
#[test]
fn an_all_literal_chain_is_keyable_but_still_ill_typed() {
let src = "flow main() {\n {5 or 9}\n -> END\n}\n";
let (hir, ..) = build_native(src);
let root = first_coalesce_root(&hir).expect("one chain");
assert!(expr_span(root).is_some(), "the operation is keyable now");
assert!(table_of(src).is_empty(), "but it is ill-typed");
}
#[test]
fn a_chain_root_and_its_left_spine_derive_different_keys() {
let src = concat!(
"fn maybe() {\n return some(7);\n}\n",
"flow main() {\n {some(1) or maybe() or 99}\n -> END\n}\n",
);
let table = table_of(src);
assert_eq!(table.len(), 1, "{table:?}");
let (key, _) = table.iter().next().expect("one entry");
let start = usize::try_from(key.range.0).unwrap();
let end = usize::try_from(key.range.1).unwrap();
assert_eq!(&src[start..end], "some(1) or maybe() or 99");
let (hir, ..) = build_native(src);
let root_expr = first_coalesce_root(&hir).expect("one chain");
let Expr::Infix(root) = root_expr else {
panic!("expected a left-associative chain, got {root_expr:?}");
};
let spine_range = expr_span(&root.lhs).expect("the left spine is an infix too");
assert_ne!(
spine_range,
TextRange::new(key.range.0.into(), key.range.1.into())
);
assert!(
table.at(HirFileId(0), spine_range).is_none(),
"a spine node must miss, never inherit the root's verdict: {table:?}"
);
}
fn first_coalesce_root(hir: &HirFile) -> Option<&Expr> {
for knot in &hir.knots {
for stmt in &knot.body.stmts {
if let Stmt::Content(c) = stmt {
for part in &c.parts {
if let brink_ir::ContentPart::Interpolation(e) = part
&& coalesce_operands(e).is_some()
{
return Some(e);
}
}
}
}
}
None
}
#[test]
fn each_chain_is_recorded_once_at_its_root() {
let src = concat!(
"fn maybe() {\n return some(7);\n}\n",
"flow main() {\n {some(1) or maybe() or 99}\n {some(2) or 3}\n -> END\n}\n",
);
let table = table_of(src);
assert_eq!(table.len(), 2, "one entry per chain root: {table:?}");
}
#[test]
fn sibling_chains_keep_distinct_keys() {
let src = "flow main() {\n {some(1) or 2}\n {some(1) or 2}\n -> END\n}\n";
let table = table_of(src);
assert_eq!(table.len(), 2, "{table:?}");
}
#[test]
fn a_var_initializer_chain_is_recorded_too() {
let src = "var v = some(1) or 2\nflow main() {\n -> END\n}\n";
let chain = only_chain(src);
assert_eq!(chain.steps.len(), 1);
assert_eq!(chain.steps[0].shape, CoalesceShape::Collapse);
}
#[test]
fn a_bare_literal_chain_in_a_var_initializer_anchors_on_the_declaration() {
let src = "var v = 5 or 9\nflow main() {\n -> END\n}\n";
let (hir, index, resolutions, inference) = build_native(src);
let diags = check(&[(HirFileId(0), &hir)], &index, &inference, &resolutions);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E066);
assert_eq!(
diags[0].range,
hir.variables[0].ptr.text_range(),
"a bare-literal chain's fallback anchor must be the VAR's own \
range when nothing narrower is available"
);
}
#[test]
fn an_annotated_shadowing_lambda_param_keeps_its_preserve_option_shape() {
let chain = only_chain(
"fn build() {\n let x = some(1);\n let f = |x: Option<int>| x or none;\n}\n",
);
assert_eq!(chain.steps.len(), 1, "{chain:?}");
let step = &chain.steps[0];
assert_eq!(step.lhs, opt(Ty::Int));
assert_eq!(step.shape, CoalesceShape::PreserveOption);
}
#[test]
fn an_unannotated_shadowing_lambda_param_flips_the_step_to_runtime_check() {
let chain = only_chain("fn build() {\n let x = some(1);\n let f = |x| x or none;\n}\n");
assert_eq!(chain.steps.len(), 1, "{chain:?}");
let step = &chain.steps[0];
assert_eq!(
step.lhs,
Ty::Unknown,
"the lambda's own unannotated `x` must not inherit the outer \
`Option<int>`: {step:?}"
);
assert_eq!(step.shape, CoalesceShape::RuntimeCheck);
}
}