use brink_format::DefinitionId;
use brink_ir::{
Diagnostic, DiagnosticCode, FileId, RefKind, ResolutionMap, ResolvedRef, SymbolIndex,
SymbolKind, SymbolManifest,
};
pub fn resolve_refs(
index: &SymbolIndex,
files: &[(FileId, &SymbolManifest)],
) -> (ResolutionMap, Vec<Diagnostic>) {
let mut map = ResolutionMap::new();
let mut diagnostics = Vec::new();
for &(file_id, manifest) in files {
for uref in &manifest.unresolved {
match uref.kind {
RefKind::Divert => {
resolve_divert(index, file_id, uref, &mut map, &mut diagnostics);
}
RefKind::Variable => {
resolve_variable(index, file_id, uref, &mut map, &mut diagnostics);
}
RefKind::Function => {
resolve_function(index, file_id, uref, &mut map, &mut diagnostics);
}
RefKind::List => {
resolve_list_ref(index, file_id, uref, &mut map, &mut diagnostics);
}
}
}
}
(map, diagnostics)
}
fn resolve_divert(
index: &SymbolIndex,
file_id: FileId,
uref: &brink_ir::UnresolvedRef,
map: &mut ResolutionMap,
diagnostics: &mut Vec<Diagnostic>,
) {
if let Some(id) = lookup_divert(index, uref) {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
} else {
diagnostics.push(unresolved_diag(
file_id,
uref.range,
&uref.path,
DiagnosticCode::E024,
));
}
}
fn lookup_divert(index: &SymbolIndex, uref: &brink_ir::UnresolvedRef) -> Option<DefinitionId> {
let path = &uref.path;
if path.contains('.') {
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Stitch, SymbolKind::Label]) {
return Some(id);
}
if let Some(knot) = &uref.scope.knot {
let qualified = format!("{knot}.{path}");
if let Some(id) =
lookup_by_name(index, &qualified, &[SymbolKind::Stitch, SymbolKind::Label])
{
return Some(id);
}
}
return None;
}
if let Some(knot) = &uref.scope.knot {
let qualified = format!("{knot}.{path}");
if let Some(id) =
lookup_by_name(index, &qualified, &[SymbolKind::Stitch, SymbolKind::Label])
{
return Some(id);
}
if let Some(stitch) = &uref.scope.stitch
&& let Some(id) = lookup_by_name(
index,
&format!("{knot}.{stitch}.{path}"),
&[SymbolKind::Label],
)
{
return Some(id);
}
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Knot]) {
return Some(id);
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Stitch]) {
return Some(id);
}
if let Some(knot) = &uref.scope.knot
&& let Some(id) = lookup_label_in_knot(index, knot, path)
{
return Some(id);
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Label]) {
return Some(id);
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Variable]) {
return Some(id);
}
lookup_local_in_scope(index, path, &uref.scope)
}
fn resolve_variable(
index: &SymbolIndex,
file_id: FileId,
uref: &brink_ir::UnresolvedRef,
map: &mut ResolutionMap,
diagnostics: &mut Vec<Diagnostic>,
) {
let path = &uref.path;
if is_builtin_function(path) {
return;
}
match lookup_variable(index, uref) {
VarResult::Found(id) => {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
}
VarResult::Ambiguous => {
diagnostics.push(ambiguous_diag(file_id, uref.range, path));
}
VarResult::NotFound => {
diagnostics.push(unresolved_diag(
file_id,
uref.range,
path,
DiagnosticCode::E025,
));
}
}
}
enum VarResult {
Found(DefinitionId),
Ambiguous,
NotFound,
}
fn lookup_variable(index: &SymbolIndex, uref: &brink_ir::UnresolvedRef) -> VarResult {
let path = &uref.path;
if let Some(id) = lookup_local_in_scope(index, path, &uref.scope) {
return VarResult::Found(id);
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Variable, SymbolKind::Constant]) {
return VarResult::Found(id);
}
match lookup_list_item_bare(index, path) {
BareItemResult::Unique(id) => return VarResult::Found(id),
BareItemResult::Ambiguous => return VarResult::Ambiguous,
BareItemResult::NotFound => {}
}
if path.contains('.')
&& let Some(id) = lookup_by_name(index, path, &[SymbolKind::ListItem])
{
return VarResult::Found(id);
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::List]) {
return VarResult::Found(id);
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Knot, SymbolKind::Stitch]) {
return VarResult::Found(id);
}
if let Some(knot) = &uref.scope.knot
&& let Some(id) = lookup_by_name(index, &format!("{knot}.{path}"), &[SymbolKind::Stitch])
{
return VarResult::Found(id);
}
if path.contains('.') {
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Stitch, SymbolKind::Label]) {
return VarResult::Found(id);
}
if let Some((knot, label)) = path.split_once('.')
&& !label.contains('.')
&& let Some(id) = lookup_label_in_knot(index, knot, label)
{
return VarResult::Found(id);
}
}
if let Some(knot) = &uref.scope.knot
&& let Some(id) = lookup_label_in_knot(index, knot, path)
{
return VarResult::Found(id);
}
if uref.scope.knot.is_none()
&& let Some(id) = lookup_by_name(index, path, &[SymbolKind::Label])
{
return VarResult::Found(id);
}
VarResult::NotFound
}
fn resolve_function(
index: &SymbolIndex,
file_id: FileId,
uref: &brink_ir::UnresolvedRef,
map: &mut ResolutionMap,
diagnostics: &mut Vec<Diagnostic>,
) {
let path = &uref.path;
if is_builtin_function(path) {
return;
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::External]) {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
check_arity(index, file_id, uref, id, diagnostics);
return;
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Knot]) {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
check_arity(index, file_id, uref, id, diagnostics);
return;
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::List]) {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
return;
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::Variable]) {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
return;
}
if let Some(id) = lookup_local_in_scope(index, path, &uref.scope) {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
return;
}
diagnostics.push(unresolved_diag(
file_id,
uref.range,
path,
DiagnosticCode::E025,
));
}
fn check_arity(
index: &SymbolIndex,
file_id: FileId,
uref: &brink_ir::UnresolvedRef,
target: DefinitionId,
diagnostics: &mut Vec<Diagnostic>,
) {
let Some(call_arg_count) = uref.arg_count else {
return;
};
let Some(info) = index.symbols.get(&target) else {
return;
};
let expected = info.params.len();
if call_arg_count != expected {
diagnostics.push(Diagnostic {
file: file_id,
range: uref.range,
message: format!(
"{}: `{}` expects {} argument(s), got {}",
DiagnosticCode::E031.title(),
uref.path,
expected,
call_arg_count,
),
code: DiagnosticCode::E031,
});
}
}
fn resolve_list_ref(
index: &SymbolIndex,
file_id: FileId,
uref: &brink_ir::UnresolvedRef,
map: &mut ResolutionMap,
diagnostics: &mut Vec<Diagnostic>,
) {
let path = &uref.path;
if path.contains('.')
&& let Some(id) = lookup_by_name(index, path, &[SymbolKind::ListItem])
{
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
return;
}
match lookup_list_item_bare(index, path) {
BareItemResult::Unique(id) => {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
return;
}
BareItemResult::Ambiguous => {
diagnostics.push(ambiguous_diag(file_id, uref.range, path));
return;
}
BareItemResult::NotFound => {}
}
if let Some(id) = lookup_by_name(index, path, &[SymbolKind::List]) {
map.push(ResolvedRef {
file: file_id,
range: uref.range,
target: id,
});
return;
}
diagnostics.push(unresolved_diag(
file_id,
uref.range,
path,
DiagnosticCode::E025,
));
}
fn lookup_local_in_scope(
index: &SymbolIndex,
bare_name: &str,
scope: &brink_ir::Scope,
) -> Option<DefinitionId> {
let ids = index.by_name.get(bare_name)?;
let mut best: Option<(DefinitionId, rowan::TextRange)> = None;
for &id in ids {
let info = index.symbols.get(&id)?;
if !matches!(info.kind, SymbolKind::Param | SymbolKind::Temp) {
continue;
}
let Some(sym_scope) = &info.scope else {
continue;
};
if sym_scope.knot != scope.knot {
continue;
}
if sym_scope.stitch.is_some() && sym_scope.stitch != scope.stitch {
continue;
}
match &best {
Some((_, prev_range)) if info.range.start() > prev_range.start() => {
best = Some((id, info.range));
}
None => {
best = Some((id, info.range));
}
_ => {}
}
}
best.map(|(id, _)| id)
}
pub(crate) fn is_builtin_function(name: &str) -> bool {
matches!(
name,
"TURNS_SINCE"
| "CHOICE_COUNT"
| "RANDOM"
| "SEED_RANDOM"
| "INT"
| "FLOAT"
| "FLOOR"
| "CEILING"
| "POW"
| "MIN"
| "MAX"
| "LIST_COUNT"
| "LIST_MIN"
| "LIST_MAX"
| "LIST_ALL"
| "LIST_INVERT"
| "LIST_RANGE"
| "LIST_RANDOM"
| "LIST_VALUE"
| "LIST_FROM_INT"
| "READ_COUNT"
| "TURNS"
)
}
fn lookup_by_name(index: &SymbolIndex, name: &str, kinds: &[SymbolKind]) -> Option<DefinitionId> {
let ids = index.by_name.get(name)?;
for id in ids {
if let Some(info) = index.symbols.get(id)
&& kinds.contains(&info.kind)
{
return Some(*id);
}
}
None
}
enum BareItemResult {
Unique(DefinitionId),
Ambiguous,
NotFound,
}
fn lookup_list_item_bare(index: &SymbolIndex, bare_name: &str) -> BareItemResult {
let suffix = format!(".{bare_name}");
let mut found: Option<DefinitionId> = None;
for (name, ids) in &index.by_name {
if name.ends_with(&suffix) {
for id in ids {
if let Some(info) = index.symbols.get(id)
&& info.kind == SymbolKind::ListItem
{
if found.is_some() {
return BareItemResult::Ambiguous;
}
found = Some(*id);
}
}
}
}
match found {
Some(id) => BareItemResult::Unique(id),
None => BareItemResult::NotFound,
}
}
fn lookup_label_in_knot(index: &SymbolIndex, knot: &str, label: &str) -> Option<DefinitionId> {
let direct = format!("{knot}.{label}");
if let Some(id) = lookup_by_name(index, &direct, &[SymbolKind::Label]) {
return Some(id);
}
let suffix = format!(".{label}");
let prefix = format!("{knot}.");
let mut best: Option<DefinitionId> = None;
for (name, ids) in &index.by_name {
if name.starts_with(&prefix) && name.ends_with(&suffix) && name.matches('.').count() == 2 {
for id in ids {
if let Some(info) = index.symbols.get(id)
&& info.kind == SymbolKind::Label
{
best = Some(match best {
Some(prev) if prev.to_raw() <= id.to_raw() => prev,
_ => *id,
});
}
}
}
}
best
}
fn ambiguous_diag(file: FileId, range: rowan::TextRange, path: &str) -> Diagnostic {
Diagnostic {
file,
range,
message: format!(
"{}: `{path}` — qualify with the list name (e.g., `ListName.{path}`)",
DiagnosticCode::E027.title(),
),
code: DiagnosticCode::E027,
}
}
fn unresolved_diag(
file: FileId,
range: rowan::TextRange,
path: &str,
code: DiagnosticCode,
) -> Diagnostic {
Diagnostic {
file,
range,
message: format!("{}: `{path}`", code.title()),
code,
}
}
#[cfg(test)]
#[expect(clippy::cast_possible_truncation, reason = "test helper ranges")]
mod tests {
use brink_ir::{DeclaredSymbol, Scope, UnresolvedRef};
use rowan::TextRange;
use rowan::TextSize;
use super::*;
use crate::manifest::merge_manifests;
fn range(offset: u32, len: u32) -> TextRange {
TextRange::new(TextSize::new(offset), TextSize::new(offset + len))
}
fn make_manifest(
knots: &[&str],
stitches: &[&str],
variables: &[&str],
lists: &[(&str, &[&str])],
externals: &[&str],
labels: &[&str],
unresolved: Vec<UnresolvedRef>,
) -> SymbolManifest {
let mut manifest = SymbolManifest::default();
let mut offset = 0u32;
for &name in knots {
let r = range(offset, name.len() as u32);
manifest.knots.push(DeclaredSymbol {
name: name.to_string(),
range: r,
params: Vec::new(),
detail: None,
});
offset += name.len() as u32 + 1;
}
for &name in stitches {
let r = range(offset, name.len() as u32);
manifest.stitches.push(DeclaredSymbol {
name: name.to_string(),
range: r,
params: Vec::new(),
detail: None,
});
offset += name.len() as u32 + 1;
}
for &name in variables {
let r = range(offset, name.len() as u32);
manifest.variables.push(DeclaredSymbol {
name: name.to_string(),
range: r,
params: Vec::new(),
detail: None,
});
offset += name.len() as u32 + 1;
}
for &(list_name, items) in lists {
let r = range(offset, list_name.len() as u32);
manifest.lists.push(DeclaredSymbol {
name: list_name.to_string(),
range: r,
params: Vec::new(),
detail: None,
});
offset += list_name.len() as u32 + 1;
for &item in items {
let qualified = format!("{list_name}.{item}");
let r = range(offset, item.len() as u32);
manifest.list_items.push(DeclaredSymbol {
name: qualified,
range: r,
params: Vec::new(),
detail: None,
});
offset += item.len() as u32 + 1;
}
}
for &name in externals {
let r = range(offset, name.len() as u32);
manifest.externals.push(DeclaredSymbol {
name: name.to_string(),
range: r,
params: Vec::new(),
detail: None,
});
offset += name.len() as u32 + 1;
}
for &name in labels {
let r = range(offset, name.len() as u32);
manifest.labels.push(DeclaredSymbol {
name: name.to_string(),
range: r,
params: Vec::new(),
detail: None,
});
offset += name.len() as u32 + 1;
}
manifest.unresolved = unresolved;
manifest
}
fn uref(path: &str, kind: RefKind, knot: Option<&str>, stitch: Option<&str>) -> UnresolvedRef {
uref_with_args(path, kind, knot, stitch, None)
}
fn uref_with_args(
path: &str,
kind: RefKind,
knot: Option<&str>,
stitch: Option<&str>,
arg_count: Option<usize>,
) -> UnresolvedRef {
UnresolvedRef {
path: path.to_string(),
range: range(900, path.len() as u32),
kind,
scope: Scope {
knot: knot.map(String::from),
stitch: stitch.map(String::from),
},
arg_count,
}
}
#[test]
fn single_knot_divert_resolves() {
let manifest = make_manifest(
&["start"],
&[],
&[],
&[],
&[],
&[],
vec![uref("start", RefKind::Divert, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, merge_diags) = merge_manifests(&files);
let (resolutions, resolve_diags) = resolve_refs(&index, &files);
assert!(merge_diags.is_empty());
assert!(resolve_diags.is_empty());
assert_eq!(resolutions.len(), 1);
assert_eq!(resolutions[0].file, FileId(0));
}
#[test]
fn qualified_knot_stitch_divert_resolves() {
let manifest = make_manifest(
&["kitchen"],
&["kitchen.look_around"],
&[],
&[],
&[],
&[],
vec![uref("kitchen.look_around", RefKind::Divert, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert_eq!(resolutions.len(), 1);
}
#[test]
fn stitch_local_divert_prefers_local_stitch() {
let manifest = make_manifest(
&["bedroom", "kitchen"],
&["bedroom.look", "kitchen.look"],
&[],
&[],
&[],
&[],
vec![uref("look", RefKind::Divert, Some("bedroom"), None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert_eq!(resolutions.len(), 1);
let info = index.symbols.get(&resolutions[0].target).unwrap();
assert_eq!(info.name, "bedroom.look");
}
#[test]
fn unresolved_divert_emits_diagnostic() {
let manifest = make_manifest(
&["start"],
&[],
&[],
&[],
&[],
&[],
vec![uref("nonexistent", RefKind::Divert, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(resolutions.is_empty());
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E024);
}
#[test]
fn duplicate_knot_emits_warning() {
let mut m1 = make_manifest(&["start"], &[], &[], &[], &[], &[], vec![]);
let m2 = make_manifest(&["start"], &[], &[], &[], &[], &[], vec![]);
m1.knots[0].range = range(0, 5);
let files = vec![(FileId(0), &m1), (FileId(1), &m2)];
let (_index, diags) = merge_manifests(&files);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E022);
}
#[test]
fn list_item_bare_name_resolves() {
let manifest = make_manifest(
&[],
&[],
&[],
&[("Colors", &["red", "green", "blue"])],
&[],
&[],
vec![uref("red", RefKind::Variable, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert_eq!(resolutions.len(), 1);
let info = index.symbols.get(&resolutions[0].target).unwrap();
assert_eq!(info.name, "Colors.red");
}
#[test]
fn end_done_not_in_unresolved() {
let manifest = make_manifest(
&["start"],
&[],
&[],
&[],
&[],
&[],
vec![], );
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert!(resolutions.is_empty());
}
#[test]
fn label_in_knot_resolves() {
let manifest = make_manifest(
&["meeting"],
&[],
&[],
&[],
&[],
&["meeting.greet"],
vec![uref("greet", RefKind::Divert, Some("meeting"), None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert_eq!(resolutions.len(), 1);
let info = index.symbols.get(&resolutions[0].target).unwrap();
assert_eq!(info.name, "meeting.greet");
}
#[test]
fn external_function_resolves() {
let manifest = make_manifest(
&[],
&[],
&[],
&[],
&["print_debug"],
&[],
vec![uref("print_debug", RefKind::Function, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert_eq!(resolutions.len(), 1);
}
#[test]
fn global_variable_resolves() {
let manifest = make_manifest(
&[],
&[],
&["player_name"],
&[],
&[],
&[],
vec![uref("player_name", RefKind::Variable, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert_eq!(resolutions.len(), 1);
}
#[test]
fn ambiguous_bare_list_item_emits_diagnostic() {
let manifest = make_manifest(
&[],
&[],
&[],
&[("Fruit", &["red"]), ("Color", &["red"])],
&[],
&[],
vec![uref("red", RefKind::Variable, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(resolutions.is_empty());
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E027);
}
#[test]
fn qualified_list_item_resolves_despite_ambiguity() {
let manifest = make_manifest(
&[],
&[],
&[],
&[("Fruit", &["red"]), ("Color", &["red"])],
&[],
&[],
vec![uref("Color.red", RefKind::Variable, None, None)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert!(diags.is_empty());
assert_eq!(resolutions.len(), 1);
let info = index.symbols.get(&resolutions[0].target).unwrap();
assert_eq!(info.name, "Color.red");
}
fn make_manifest_with_params(
knot_name: &str,
param_count: usize,
unresolved: Vec<UnresolvedRef>,
) -> SymbolManifest {
let mut manifest = SymbolManifest::default();
let r = range(0, knot_name.len() as u32);
let params: Vec<brink_ir::ParamInfo> = (0..param_count)
.map(|i| brink_ir::ParamInfo {
name: format!("p{i}"),
is_ref: false,
is_divert: false,
})
.collect();
manifest.knots.push(DeclaredSymbol {
name: knot_name.to_string(),
range: r,
params,
detail: Some("function".to_string()),
});
manifest.unresolved = unresolved;
manifest
}
#[test]
fn arity_match_no_warning() {
let manifest = make_manifest_with_params(
"greet",
1,
vec![uref_with_args(
"greet",
RefKind::Function,
None,
None,
Some(1),
)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert_eq!(resolutions.len(), 1);
assert!(
diags.is_empty(),
"expected no diagnostics for matching arity, got: {diags:?}"
);
}
#[test]
fn arity_mismatch_emits_e031() {
let manifest = make_manifest_with_params(
"greet",
1,
vec![uref_with_args(
"greet",
RefKind::Function,
None,
None,
Some(2),
)],
);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert_eq!(
resolutions.len(),
1,
"should still resolve despite arity mismatch"
);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E031);
assert!(diags[0].message.contains("expects 1"));
assert!(diags[0].message.contains("got 2"));
}
#[test]
fn arity_check_no_arg_count_no_warning() {
let manifest =
make_manifest_with_params("greet", 1, vec![uref("greet", RefKind::Divert, None, None)]);
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (_resolutions, diags) = resolve_refs(&index, &files);
assert!(
diags.is_empty(),
"divert should not trigger arity check: {diags:?}"
);
}
#[test]
fn arity_mismatch_external() {
let mut manifest = SymbolManifest::default();
let r = range(0, 5);
manifest.externals.push(DeclaredSymbol {
name: "print".to_string(),
range: r,
params: vec![brink_ir::ParamInfo {
name: "msg".into(),
is_ref: false,
is_divert: false,
}],
detail: None,
});
manifest.unresolved.push(uref_with_args(
"print",
RefKind::Function,
None,
None,
Some(3),
));
let files = vec![(FileId(0), &manifest)];
let (index, _) = merge_manifests(&files);
let (resolutions, diags) = resolve_refs(&index, &files);
assert_eq!(resolutions.len(), 1);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E031);
}
}