#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use brink_analyzer::{AnalysisOptions, AnalysisResult, ModuleMap, ResolvedModule, UfcsVerdict};
use brink_ir::hir::lower_native;
use brink_ir::{Diagnostic, DiagnosticCode, FileId, HirFile, Name, SymbolManifest, hir::visit};
fn analyze_with_map(
files: &[(FileId, &HirFile, &SymbolManifest)],
modules: &ModuleMap,
opts: &AnalysisOptions,
is_native: bool,
) -> AnalysisResult {
let manifest_inputs: Vec<(FileId, &SymbolManifest)> =
files.iter().map(|&(id, _hir, m)| (id, m)).collect();
let (index, mut diagnostics) = brink_analyzer::symbol_index_with_modules(
&manifest_inputs,
modules,
opts.dialect,
is_native,
);
let mut resolutions = brink_ir::ResolutionMap::new();
let mut scopes = std::collections::BTreeMap::new();
for &(file_id, hir, manifest) in files {
let declared_module = match modules.get(&file_id) {
Some(resolved) => resolved.declared.then(|| resolved.name.clone()),
None => hir.module.as_ref().map(|m| m.name.clone()),
};
let scope = brink_analyzer::ImportScope::new(declared_module, &hir.imports);
let (file_map, file_diags) = brink_analyzer::resolve(file_id, manifest, &index, &scope);
resolutions.extend(std::sync::Arc::unwrap_or_clone(file_map));
diagnostics.extend(file_diags);
scopes.insert(file_id, scope);
}
brink_analyzer::finish_analysis(
files,
index,
resolutions,
diagnostics,
opts,
is_native,
None,
&scopes,
)
}
fn lower(src: &str) -> (HirFile, SymbolManifest) {
let parse = brink_syntax_native::parse(src);
assert!(
parse.errors().is_empty(),
"fixture must parse cleanly: {:?}",
parse.errors()
);
let (hir, manifest, diags) = lower_native::lower(FileId(0), &parse.tree());
assert!(diags.is_empty(), "lowering diagnostics: {diags:?}");
(hir, manifest)
}
fn diagnostics(hir: &HirFile, manifest: &SymbolManifest) -> Vec<Diagnostic> {
let files = vec![(FileId(0), hir, manifest)];
let analysis = brink_analyzer::analyze(&files);
let (diags, _meta) = brink_analyzer::whole_project_diagnostics(
&files,
&analysis.index,
&analysis.resolutions,
&AnalysisOptions::default(),
false,
None,
);
diags
}
fn verdicts(hir: &HirFile, manifest: &SymbolManifest) -> Vec<UfcsVerdict> {
let files = vec![(FileId(0), hir, manifest)];
let analysis = brink_analyzer::analyze(&files);
let hir_inputs = vec![(FileId(0), hir)];
let manifest_inputs = vec![(FileId(0), manifest)];
let inline_docs = brink_analyzer::project_inline_docs(&manifest_inputs);
let inference = brink_analyzer::infer_project(
&hir_inputs,
&analysis.index,
&analysis.resolutions,
None,
&inline_docs,
);
let (table, _diags) = brink_analyzer::ufcs_resolution(
&hir_inputs,
&analysis.index,
&analysis.resolutions,
&inference,
);
table.iter().map(|(_key, v)| v.clone()).collect()
}
fn codes(diags: &[Diagnostic]) -> Vec<DiagnosticCode> {
diags.iter().map(|d| d.code).collect()
}
fn only(diags: &[Diagnostic], code: DiagnosticCode) -> &Diagnostic {
let hits: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == code).collect();
assert_eq!(hits.len(), 1, "expected exactly one {code:?}: {diags:?}");
hits[0]
}
const FREE_FN: &str = "\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
fn main() {
let g = Guest { name: \"ada\" };
let n = g.greet(3);
}
";
#[test]
fn a_free_function_in_scope_resolves_and_is_recorded_as_a_desugar() {
let (hir, manifest) = lower(FREE_FN);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FreeFnDesugar { receiver, name, .. } = &verdicts[0] else {
panic!("expected a free-fn desugar verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "greet");
assert_eq!(*receiver, brink_analyzer::Ty::Struct("Guest".into()));
}
#[test]
fn a_resolved_method_call_raises_no_diagnostic_at_all() {
let (hir, manifest) = lower(FREE_FN);
let diags = diagnostics(&hir, &manifest);
let relevant: Vec<&Diagnostic> = diags
.iter()
.filter(|d| {
matches!(
d.code,
DiagnosticCode::E025
| DiagnosticCode::E140
| DiagnosticCode::E141
| DiagnosticCode::E142
| DiagnosticCode::E143
)
})
.collect();
assert!(relevant.is_empty(), "expected none, got {relevant:?}");
}
#[test]
fn a_free_fn_desugar_with_the_wrong_arity_is_still_reported() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
fn main() {
let g = Guest { name: \"ada\" };
let n = g.greet(1, 2, 3);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
assert!(matches!(verdicts[0], UfcsVerdict::FreeFnDesugar { .. }));
let diags = diagnostics(&hir, &manifest);
let e031 = only(&diags, DiagnosticCode::E031);
assert!(
e031.message.contains("greet"),
"E031 must name the function: {}",
e031.message
);
}
fn array_receiver_fixture(call_expr: &str) -> (HirFile, SymbolManifest) {
lower(&format!(
"\
struct Bag {{
items: Array<int>
}}
fn main() {{
let b = Bag {{ items: 0 }};
let n = {call_expr};
}}
"
))
}
#[test]
fn a_prelude_verb_resolves_and_is_recorded_as_a_prelude_desugar() {
let (hir, manifest) = array_receiver_fixture("b.items.len()");
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E141),
"a prelude verb must not read as \"no function in scope\": {diags:?}"
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::PreludeDesugar { receiver, name, .. } = &verdicts[0] else {
panic!("expected a prelude-desugar verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "len");
assert_eq!(
*receiver,
brink_analyzer::Ty::Array(Box::new(brink_analyzer::Ty::Int))
);
}
#[test]
fn a_mutating_prelude_verb_also_resolves_as_a_prelude_desugar() {
let (hir, manifest) = array_receiver_fixture("b.items.push(3)");
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E141),
"a prelude verb must not read as \"no function in scope\": {diags:?}"
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::PreludeDesugar { name, .. } = &verdicts[0] else {
panic!("expected a prelude-desugar verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "push");
}
#[test]
fn a_multi_file_global_receiver_is_typed_from_the_resolved_definition() {
let (hir_a, manifest_a) = lower("var g: int = 10\n");
let (hir_b, manifest_b) = lower(
"\
fn greet(g, loudness) {
return loudness;
}
fn main() {
let n = g.greet(3);
}
",
);
let files = vec![
(FileId(0), &hir_a, &manifest_a),
(FileId(1), &hir_b, &manifest_b),
];
let analysis = brink_analyzer::analyze(&files);
let (diags, _meta) = brink_analyzer::whole_project_diagnostics(
&files,
&analysis.index,
&analysis.resolutions,
&AnalysisOptions::default(),
false,
None,
);
assert!(
!diags.iter().any(|d| d.code == DiagnosticCode::E142),
"a global receiver declared in another file must not read as unknown: {diags:?}"
);
let hir_inputs = vec![(FileId(0), &hir_a), (FileId(1), &hir_b)];
let manifest_inputs = vec![(FileId(0), &manifest_a), (FileId(1), &manifest_b)];
let inline_docs = brink_analyzer::project_inline_docs(&manifest_inputs);
let inference = brink_analyzer::infer_project(
&hir_inputs,
&analysis.index,
&analysis.resolutions,
None,
&inline_docs,
);
let (table, _diags) = brink_analyzer::ufcs_resolution(
&hir_inputs,
&analysis.index,
&analysis.resolutions,
&inference,
);
let verdicts: Vec<_> = table.iter().map(|(_key, v)| v.clone()).collect();
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FreeFnDesugar { receiver, name, .. } = &verdicts[0] else {
panic!("expected a free-fn desugar verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "greet");
assert_eq!(*receiver, brink_analyzer::Ty::Int);
}
#[test]
fn a_matching_non_callable_field_is_a_hard_error_and_never_falls_through() {
let (hir, manifest) = lower(
"\
struct Guest {
greet: string
}
fn greet(g, loudness) {
return loudness;
}
fn main() {
let g = Guest { greet: \"hi\" };
let n = g.greet(3);
}
",
);
assert!(verdicts(&hir, &manifest).is_empty());
let diags = diagnostics(&hir, &manifest);
let e140 = only(&diags, DiagnosticCode::E140);
assert!(
e140.message.contains("greet") && e140.message.contains("Guest"),
"E140 must name the field and its shape: {}",
e140.message
);
assert!(
!codes(&diags).contains(&DiagnosticCode::E141),
"the free `greet` must not be reported as a second attempt: {diags:?}"
);
}
#[test]
fn a_function_typed_field_wins_and_is_recorded_as_a_field_call() {
let (hir, manifest) = lower(
"\
struct Guest {
greet: fn(int): int
}
fn main() {
let g = Guest { greet: \"hi\" };
let n = g.greet(3);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FieldCall {
receiver,
field,
field_ty,
arity_mismatch,
arg_mismatches,
} = &verdicts[0]
else {
panic!("expected a field-call verdict, got {:?}", verdicts[0]);
};
assert_eq!(*receiver, brink_analyzer::Ty::Struct("Guest".into()));
assert_eq!(field, "greet");
assert!(matches!(field_ty, brink_analyzer::Ty::Fn(..)));
assert_eq!(*arity_mismatch, None, "correct arity must stay clean");
assert!(
arg_mismatches.is_empty(),
"correct argument types must stay clean: {arg_mismatches:?}"
);
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E140),
"a callable field is not an E140: {diags:?}"
);
}
#[test]
fn a_shadowed_free_function_still_gets_a_call_edge_despite_the_field_winning() {
let source = "\
struct Guest {
greet: fn(int): int
}
fn greet(g, loudness) {
return loudness;
}
fn caller() {
let g = Guest { greet: \"hi\" };
return g.greet(3);
}
";
let (hir, manifest) = lower(source);
let vs = verdicts(&hir, &manifest);
assert_eq!(vs.len(), 1, "one UFCS site: {vs:?}");
assert!(
matches!(vs[0], UfcsVerdict::FieldCall { .. }),
"field access must win over the same-named free function: {:?}",
vs[0]
);
let files = vec![(FileId(0), &hir, &manifest)];
let analysis = brink_analyzer::analyze(&files);
let hir_inputs = vec![(FileId(0), &hir)];
let manifest_inputs = vec![(FileId(0), &manifest)];
let inline_docs = brink_analyzer::project_inline_docs(&manifest_inputs);
let inference = brink_analyzer::infer_project(
&hir_inputs,
&analysis.index,
&analysis.resolutions,
None,
&inline_docs,
);
let caller_id = analysis
.index
.by_name
.get("caller")
.and_then(|ids| ids.first())
.copied()
.expect("caller");
let greet_fn_id = analysis
.index
.by_name
.get("greet")
.and_then(|ids| ids.first())
.copied()
.expect("free fn greet");
let caller_body = inference.bodies.get(&caller_id).expect("caller body");
assert_eq!(
caller_body.return_ty,
brink_analyzer::Ty::Unknown,
"the struct-receiver gate must decline the result type"
);
let inferable = brink_analyzer::inferable_defs_from_index(&analysis.index);
let edges = brink_analyzer::call_edges(
caller_id,
&hir_inputs,
&analysis.index,
&analysis.resolutions,
&inferable,
None,
);
assert!(
edges.contains(&greet_fn_id),
"the edge to the shadowed free function must still be recorded: {edges:?}"
);
}
#[test]
fn neither_a_field_nor_a_free_function_is_one_diagnostic_naming_both() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn main() {
let g = Guest { name: \"ada\" };
let n = g.nope(3);
}
",
);
assert!(verdicts(&hir, &manifest).is_empty());
let diags = diagnostics(&hir, &manifest);
let e141 = only(&diags, DiagnosticCode::E141);
assert!(
e141.message.contains("no field `nope`"),
"must name the field attempt: {}",
e141.message
);
assert!(
e141.message.contains("no function `nope` is in scope"),
"must name the free-function attempt: {}",
e141.message
);
assert!(
e141.message.contains("Guest"),
"must name the receiver's type: {}",
e141.message
);
assert!(
!codes(&diags).contains(&DiagnosticCode::E025),
"the pre-#1482 unresolved-reference error is replaced, not doubled: {diags:?}"
);
}
#[test]
fn an_unknown_receiver_type_demands_an_annotation() {
let (hir, manifest) = lower(
"\
fn greet(g, loudness) {
return loudness;
}
fn main(guest) {
let n = guest.greet(3);
}
",
);
assert!(verdicts(&hir, &manifest).is_empty());
let diags = diagnostics(&hir, &manifest);
let e142 = only(&diags, DiagnosticCode::E142);
assert!(
e142.message.contains("annotate"),
"E142 must demand an annotation: {}",
e142.message
);
assert!(
!codes(&diags).contains(&DiagnosticCode::E141),
"an unknown receiver is not a both-attempts-failed site: {diags:?}"
);
}
#[test]
fn an_annotated_fn_param_receiver_resolves_from_its_own_annotation() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
fn main(g: Guest) {
let n = g.greet(3);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FreeFnDesugar { receiver, name, .. } = &verdicts[0] else {
panic!("expected a free-fn desugar verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "greet");
assert_eq!(*receiver, brink_analyzer::Ty::Struct("Guest".into()));
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E142),
"the ordinary fn param's own `: Guest` annotation must resolve the \
receiver without demanding a further one: {diags:?}"
);
}
#[test]
fn an_unannotated_shadowing_lambda_param_receiver_demands_an_annotation() {
let (hir, manifest) = lower(
"\
fn shout(s: string, times: int) {
return times;
}
fn f(s: string) {
let g = |s| s.shout(2);
}
",
);
let diags = diagnostics(&hir, &manifest);
let e142 = only(&diags, DiagnosticCode::E142);
assert!(
e142.message.contains("annotate"),
"E142 must demand an annotation: {}",
e142.message
);
}
#[test]
fn an_annotated_shadowing_lambda_param_receiver_resolves_from_its_own_annotation() {
let (hir, manifest) = lower(
"\
fn shout(s: string, times: int) {
return times;
}
fn f(s: int) {
let g = |s: string| s.shout(2);
}
",
);
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E142),
"the lambda's own `: string` annotation must resolve the receiver: {diags:?}"
);
}
#[test]
fn a_ref_first_param_auto_refs_a_frame_local_receiver() {
let (hir, manifest) = lower(
"\
fn bump(ref n, amount) {
n = n + amount;
}
fn main() {
let g = 1;
g.bump(5);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FreeFnAutoRef { receiver, name, .. } = &verdicts[0] else {
panic!("expected an auto-ref verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "bump");
assert_eq!(*receiver, brink_analyzer::Ty::Int);
assert!(
!codes(&diagnostics(&hir, &manifest)).contains(&DiagnosticCode::E143),
"a writable receiver is not refused"
);
}
#[test]
fn a_ref_first_param_auto_refs_a_global_var_receiver() {
let (hir, manifest) = lower(
"\
var hp: int = 1
fn bump(ref n, amount) {
n = n + amount;
}
fn main() {
hp.bump(5);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
assert!(matches!(verdicts[0], UfcsVerdict::FreeFnAutoRef { .. }));
}
#[test]
fn an_auto_ref_desugar_with_the_wrong_arity_is_still_reported() {
let (hir, manifest) = lower(
"\
fn bump(ref n, amount) {
n = n + amount;
}
fn main() {
let g = 1;
g.bump(1, 2, 3);
}
",
);
let diags = diagnostics(&hir, &manifest);
let e031 = only(&diags, DiagnosticCode::E031);
assert!(
e031.message.contains("bump"),
"E031 must name the function: {}",
e031.message
);
}
#[test]
fn a_const_receiver_under_a_ref_first_param_is_refused() {
let (hir, manifest) = lower(
"\
const START: int = 1
fn bump(ref n, amount) {
n = n + amount;
}
fn main() {
START.bump(5);
}
",
);
assert!(
verdicts(&hir, &manifest).is_empty(),
"an unwritable receiver must never be desugared"
);
let diags = diagnostics(&hir, &manifest);
let e143 = only(&diags, DiagnosticCode::E143);
assert!(
e143.message.contains("cannot mutate") && e143.message.contains("CONST"),
"E143 must name the cause: {}",
e143.message
);
}
#[test]
fn a_projection_off_a_frame_local_under_a_ref_first_param_is_accepted() {
let (hir, manifest) = lower(
"\
struct Guest {
hp: int
}
fn heal(ref h, amount) {
h = h + amount;
}
fn main() {
let g = Guest { hp: 1 };
g.hp.heal(5);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
assert!(matches!(verdicts[0], UfcsVerdict::FreeFnAutoRef { .. }));
assert!(
!codes(&diagnostics(&hir, &manifest)).contains(&DiagnosticCode::E143),
"a single-field-deep frame-local projection is a legal receiver"
);
}
#[test]
fn a_two_field_deep_projection_off_a_frame_local_under_a_ref_first_param_is_refused() {
let (hir, manifest) = lower(
"\
struct Hp {
current: int
}
struct Guest {
hp: Hp
}
fn heal(ref h, amount) {
h = h + amount;
}
fn main() {
let g = Guest { hp: Hp { current: 1 } };
g.hp.current.heal(5);
}
",
);
assert!(verdicts(&hir, &manifest).is_empty());
let diags = diagnostics(&hir, &manifest);
let e143 = only(&diags, DiagnosticCode::E143);
assert!(
e143.message.contains("cannot mutate") && e143.message.contains("one field"),
"E143 must name the field-depth rule: {}",
e143.message
);
}
#[test]
fn a_non_ref_first_param_accepts_an_immutable_receiver() {
let (hir, manifest) = lower(
"\
const START: int = 1
fn plus(n, amount) {
return n + amount;
}
fn main() {
let n = START.plus(5);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
assert!(matches!(verdicts[0], UfcsVerdict::FreeFnDesugar { .. }));
assert!(
!codes(&diagnostics(&hir, &manifest)).contains(&DiagnosticCode::E143),
"the by-value desugar has no lvalue requirement"
);
}
#[test]
fn each_call_in_a_chain_resolves_independently() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
fn main() {
let g = Guest { name: \"ada\" };
let a = g.greet(1);
let b = g.nope(2);
}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "only the resolvable call: {verdicts:?}");
assert!(matches!(verdicts[0], UfcsVerdict::FreeFnDesugar { .. }));
let diags = diagnostics(&hir, &manifest);
only(&diags, DiagnosticCode::E141);
}
#[test]
fn a_bare_dotted_read_is_not_a_ufcs_site() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn main() {
let g = Guest { name: \"ada\" };
let n = g.name;
}
",
);
assert!(verdicts(&hir, &manifest).is_empty());
let diags = diagnostics(&hir, &manifest);
for code in [
DiagnosticCode::E140,
DiagnosticCode::E141,
DiagnosticCode::E142,
DiagnosticCode::E143,
] {
assert!(
!codes(&diags).contains(&code),
"{code:?} must not fire on a bare field read: {diags:?}"
);
}
}
#[test]
fn the_explicit_free_call_spelling_is_untouched() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
fn main() {
let g = Guest { name: \"ada\" };
let n = greet(g, 3);
}
",
);
assert!(verdicts(&hir, &manifest).is_empty());
}
#[derive(Default)]
struct MultiSegmentCalleeScan {
found: Vec<String>,
}
impl visit::HirVisitor for MultiSegmentCalleeScan {
fn visit_exprs(&self) -> bool {
true
}
fn enter_expr(&mut self, expr: &brink_ir::Expr) {
if let brink_ir::Expr::Call(path, _) = expr
&& path.segments.len() > 1
{
self.found.push(
path.segments
.iter()
.map(|s: &Name| s.text.clone())
.collect::<Vec<_>>()
.join("."),
);
}
}
}
#[test]
fn ink_never_produces_a_multi_segment_callee_path() {
let parse = brink_syntax::parse(
"== start ==\n~ temp v = f(1)\n~ temp w = knot_a.stitch_b\n-> END\n\n== knot_a ==\n= stitch_b\n-> END\n",
);
assert!(parse.errors().is_empty(), "{:?}", parse.errors());
let (hir, _manifest, _diags) = brink_ir::hir::lower(FileId(0), &parse.tree());
let mut scan = MultiSegmentCalleeScan::default();
visit::visit(&hir, &mut scan);
assert!(
scan.found.is_empty(),
"ink lowering produced a multi-segment callee path: {:?}",
scan.found
);
}
#[test]
fn a_dotted_fn_literal_target_is_not_claimed_as_a_ufcs_callee() {
let parse = brink_syntax::parse("VAR g = 0\n\n== start ==\n~ temp f = #fn(g.greet)\n-> END\n");
assert!(parse.errors().is_empty(), "{:?}", parse.errors());
let (hir, manifest, _diags) = brink_ir::hir::lower(FileId(0), &parse.tree());
let files = vec![(FileId(0), &hir, &manifest)];
let analysis = brink_analyzer::analyze(&files);
assert!(
analysis
.diagnostics
.iter()
.any(|d| d.code == DiagnosticCode::E025),
"expected the unresolved-reference error to survive: {:?}",
analysis.diagnostics
);
}
#[test]
fn ufcs_field_call_resolves_the_referrers_own_shape_when_std_and_project_share_a_name() {
let project_src = "\
struct Guest {
greet: fn(int): int
}
fn main() {
let g = Guest { greet: \"hi\" };
let n = g.greet(3);
}
";
let std_src = "\
struct Guest {
greet: string
}
fn noop() {}
";
let (project_hir, project_manifest) = lower(project_src);
let (std_hir, std_manifest) = lower(std_src);
let project_file = FileId(0);
let std_file = FileId(1);
let mut modules = ModuleMap::new();
modules.insert(
project_file,
ResolvedModule {
name: "story::main".to_string(),
declared: true,
was: None,
},
);
modules.insert(
std_file,
ResolvedModule {
name: "std::conventions::screenplay".to_string(),
declared: true,
was: None,
},
);
let files = vec![
(project_file, &project_hir, &project_manifest),
(std_file, &std_hir, &std_manifest),
];
let analysis = analyze_with_map(
&files,
&modules,
&AnalysisOptions::default(),
true,
);
assert!(
!analysis
.diagnostics
.iter()
.any(|d| d.code == DiagnosticCode::E023),
"cross-declared-module `Guest`s must coexist with no duplicate-declaration \
diagnostic: {:?}",
analysis.diagnostics
);
let hir_inputs = vec![(project_file, &project_hir), (std_file, &std_hir)];
let manifest_inputs = vec![(project_file, &project_manifest), (std_file, &std_manifest)];
let inline_docs = brink_analyzer::project_inline_docs(&manifest_inputs);
let inference = brink_analyzer::infer_project(
&hir_inputs,
&analysis.index,
&analysis.resolutions,
None,
&inline_docs,
);
let (table, ufcs_diags) = brink_analyzer::ufcs_resolution(
&hir_inputs,
&analysis.index,
&analysis.resolutions,
&inference,
);
assert!(
!ufcs_diags.iter().any(|d| d.code == DiagnosticCode::E140),
"the project's own callable `greet` field must resolve a clean field-call verdict, \
not std's non-callable one: {ufcs_diags:?}"
);
let verdicts: Vec<_> = table.iter().map(|(_key, v)| v.clone()).collect();
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FieldCall {
receiver,
field,
field_ty,
..
} = &verdicts[0]
else {
panic!("expected a field-call verdict, got {:?}", verdicts[0]);
};
assert_eq!(*receiver, brink_analyzer::Ty::Struct("Guest".into()));
assert_eq!(field, "greet");
assert!(
matches!(field_ty, brink_analyzer::Ty::Fn(..)),
"resolved against std's non-callable `greet: string` instead of the project's own \
`greet: fn(int): int`: {field_ty:?}"
);
}
#[test]
fn project_has_ufcs_call_sees_a_call_inside_a_decl_default_lambda_body() {
let (hir, _manifest) = lower(
"\
fn greet(g, loudness) {
return loudness;
}
const callGreet = |g: int| g.greet(3)
fn main() {}
",
);
assert!(
brink_analyzer::project_has_ufcs_call(&hir),
"a UFCS-shaped call inside a CONST decl-default's own lambda body must trip the \
laziness gate, or the whole pass never runs for this project"
);
}
#[test]
fn a_ufcs_call_inside_a_const_decl_default_lambda_body_resolves() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
const callGreet = |g: Guest| g.greet(3)
fn main() {}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FreeFnDesugar { receiver, name, .. } = &verdicts[0] else {
panic!("expected a free-fn desugar verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "greet");
assert_eq!(*receiver, brink_analyzer::Ty::Struct("Guest".into()));
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E144),
"must never fall through to the old defensive never-visited refusal: {diags:?}"
);
}
#[test]
fn a_ufcs_call_inside_a_var_decl_default_lambda_body_resolves() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
var callGreet = |g: Guest| g.greet(3)
fn main() {}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
assert!(matches!(verdicts[0], UfcsVerdict::FreeFnDesugar { .. }));
}
#[test]
fn an_unannotated_decl_default_lambda_receiver_demands_an_annotation() {
let (hir, manifest) = lower(
"\
fn greet(g, loudness) {
return loudness;
}
const callGreet = |g| g.greet(3)
fn main() {}
",
);
assert!(
verdicts(&hir, &manifest).is_empty(),
"an undecidable receiver must record no verdict"
);
let diags = diagnostics(&hir, &manifest);
let e142 = only(&diags, DiagnosticCode::E142);
assert!(
e142.message.contains("annotate"),
"E142 must demand an annotation: {}",
e142.message
);
assert!(
!codes(&diags).contains(&DiagnosticCode::E144),
"must never fall through to the old defensive never-visited refusal: {diags:?}"
);
}
#[test]
fn a_const_valued_global_receiver_inside_a_decl_default_lambda_resolves() {
let (hir, manifest) = lower(
"\
struct Guest {
name: string
}
fn greet(g, loudness) {
return loudness;
}
const guest: Guest = Guest{name: \"Ava\"}
const callGreet = || guest.greet(3)
fn main() {}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
let UfcsVerdict::FreeFnDesugar { receiver, name, .. } = &verdicts[0] else {
panic!("expected a free-fn desugar verdict, got {:?}", verdicts[0]);
};
assert_eq!(name, "greet");
assert_eq!(*receiver, brink_analyzer::Ty::Struct("Guest".into()));
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E144)
&& !codes(&diags).contains(&DiagnosticCode::E142),
"a const-valued global receiver must resolve, not demand annotation: {diags:?}"
);
}
fn strict_diagnostics(hir: &HirFile, manifest: &SymbolManifest) -> Vec<Diagnostic> {
let files = vec![(FileId(0), hir, manifest)];
let analysis = brink_analyzer::analyze(&files);
let opts = AnalysisOptions {
dialect: brink_analyzer::Dialect::Brink,
types: Some(brink_analyzer::TypePolicy::Strict),
..Default::default()
};
let (diags, _meta) = brink_analyzer::whole_project_diagnostics(
&files,
&analysis.index,
&analysis.resolutions,
&opts,
true,
None,
);
diags
}
fn field_call_fixture(call_expr: &str) -> (HirFile, SymbolManifest) {
lower(&format!(
"\
struct Guest {{
greet: fn(int): int
}}
fn main() {{
let g = Guest {{ greet: \"hi\" }};
let n = {call_expr};
}}
"
))
}
#[test]
fn a_correctly_typed_field_call_reports_no_e063_under_strict() {
let (hir, manifest) = field_call_fixture("g.greet(3)");
let diags = strict_diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E063),
"a correctly-typed, correct-arity field call must stay clean: {diags:?}"
);
}
#[test]
fn a_field_call_with_a_mistyped_argument_is_reported_under_strict() {
let (hir, manifest) = field_call_fixture("g.greet(\"nope\")");
let diags = strict_diagnostics(&hir, &manifest);
let e063 = only(&diags, DiagnosticCode::E063);
assert!(
e063.message
.contains("argument 1 of call through `greet` has type `string`"),
"E063 must use the T1c call-through phrasing, name the field, the mismatched type, and \
the written-args-only argument number: {}",
e063.message
);
}
#[test]
fn a_field_call_with_a_mistyped_argument_is_not_reported_under_gradual() {
let (hir, manifest) = field_call_fixture("g.greet(\"nope\")");
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E063),
"gradual mode must never run the strict field-call argument check: {diags:?}"
);
}
#[test]
fn a_field_call_with_the_wrong_arity_is_reported_under_strict() {
let (hir, manifest) = field_call_fixture("g.greet()");
let diags = strict_diagnostics(&hir, &manifest);
let e063 = only(&diags, DiagnosticCode::E063);
assert!(
e063.message.contains("greet") && e063.message.contains('1') && e063.message.contains('0'),
"E063 must name the field and the expected/got counts: {}",
e063.message
);
}
#[test]
fn a_field_call_with_the_wrong_arity_is_not_reported_under_gradual() {
let (hir, manifest) = field_call_fixture("g.greet()");
let diags = diagnostics(&hir, &manifest);
assert!(
!codes(&diags).contains(&DiagnosticCode::E063),
"gradual mode must never run the strict field-call arity check: {diags:?}"
);
}
#[test]
fn a_field_call_inside_a_decl_default_lambda_body_still_reports_wrong_arity_under_strict() {
let (hir, manifest) = lower(
"\
struct Guest {
greet: fn(int): int
}
const callGreet = |g: Guest| g.greet()
fn main() {}
",
);
let verdicts = verdicts(&hir, &manifest);
assert_eq!(verdicts.len(), 1, "one UFCS site: {verdicts:?}");
assert!(
matches!(verdicts[0], UfcsVerdict::FieldCall { .. }),
"expected a field-call verdict, got {:?}",
verdicts[0]
);
let diags = strict_diagnostics(&hir, &manifest);
let e063 = only(&diags, DiagnosticCode::E063);
assert!(
e063.message.contains("greet"),
"arity checking must still reach a field call inside a decl-default lambda body: {}",
e063.message
);
}