use std::collections::{HashMap, HashSet};
use bock_ast::{
Block, EnumVariant, Expr, FnDecl, ForLoop, GuardStmt, HandlingBlock, ImplBlock, ImportItems,
InterpolationPart, Item, LetStmt, LoopStmt, MatchArm, Module, ModulePath, NodeId, Param,
Pattern, Stmt, Visibility, WhileLoop,
};
use bock_errors::{DiagnosticBag, DiagnosticCode, Span};
use crate::registry::{ExportDetail, ExportKind, ExportedSymbol, ModuleRegistry, RegistryError};
const E_UNDEFINED: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 1001,
};
const E_MODULE_NOT_FOUND: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 1005,
};
const E_SYMBOL_NOT_FOUND: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 1006,
};
const E_NOT_VISIBLE: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 1007,
};
const W_UNUSED_IMPORT: DiagnosticCode = DiagnosticCode {
prefix: 'W',
number: 1001,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NameKind {
Variable,
Function,
Type,
Trait,
Effect,
Module,
Builtin,
Unresolved,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedName {
pub def_id: NodeId,
pub kind: NameKind,
}
#[derive(Debug, Clone)]
pub struct Binding {
pub name: String,
pub resolved: ResolvedName,
pub visibility: Visibility,
pub span: Span,
pub used: bool,
pub is_import: bool,
}
#[derive(Debug, Clone, Default)]
pub struct EffectInfo {
pub operations: Vec<(String, NodeId, Span)>,
pub components: Vec<String>,
}
#[derive(Debug, Default)]
pub struct Scope {
pub bindings: HashMap<String, Binding>,
}
impl Scope {
fn new() -> Self {
Self::default()
}
}
pub struct SymbolTable {
scopes: Vec<Scope>,
pub resolutions: HashMap<NodeId, ResolvedName>,
pub effect_info: HashMap<String, EffectInfo>,
variant_parent: HashMap<String, String>,
}
impl Default for SymbolTable {
fn default() -> Self {
Self::new()
}
}
impl SymbolTable {
#[must_use]
pub fn new() -> Self {
Self {
scopes: vec![Scope::new()],
resolutions: HashMap::new(),
effect_info: HashMap::new(),
variant_parent: HashMap::new(),
}
}
fn seed_prelude(&mut self) {
const PRELUDE_BASE_ID: NodeId = u32::MAX / 4;
use crate::prelude_vocab::{
PRELUDE_CONSTRUCTORS, PRELUDE_FUNCTIONS as PRELUDE_FNS, PRELUDE_TRAITS, PRELUDE_TYPES,
};
let mut id = PRELUDE_BASE_ID;
let dummy_span = Span {
file: bock_errors::FileId(0),
start: 0,
end: 0,
};
for &name in PRELUDE_FNS {
self.define(
name.to_string(),
Binding {
name: name.to_string(),
resolved: ResolvedName {
def_id: id,
kind: NameKind::Builtin,
},
visibility: Visibility::Public,
span: dummy_span,
used: true, is_import: false,
},
);
id += 1;
}
for &name in PRELUDE_TYPES {
self.define(
name.to_string(),
Binding {
name: name.to_string(),
resolved: ResolvedName {
def_id: id,
kind: NameKind::Builtin,
},
visibility: Visibility::Public,
span: dummy_span,
used: true,
is_import: false,
},
);
id += 1;
}
for &name in PRELUDE_CONSTRUCTORS {
self.define(
name.to_string(),
Binding {
name: name.to_string(),
resolved: ResolvedName {
def_id: id,
kind: NameKind::Builtin,
},
visibility: Visibility::Public,
span: dummy_span,
used: true,
is_import: false,
},
);
id += 1;
}
for &name in PRELUDE_TRAITS {
self.define(
name.to_string(),
Binding {
name: name.to_string(),
resolved: ResolvedName {
def_id: id,
kind: NameKind::Builtin,
},
visibility: Visibility::Public,
span: dummy_span,
used: true,
is_import: false,
},
);
id += 1;
}
}
pub fn define(&mut self, name: String, binding: Binding) {
if let Some(scope) = self.scopes.last_mut() {
scope.bindings.insert(name, binding);
}
}
pub fn push_scope(&mut self) {
self.scopes.push(Scope::new());
}
pub fn pop_scope(&mut self) -> Option<Scope> {
if self.scopes.len() > 1 {
self.scopes.pop()
} else {
None
}
}
pub fn lookup(&mut self, name: &str) -> Option<ResolvedName> {
for scope in self.scopes.iter_mut().rev() {
if let Some(binding) = scope.bindings.get_mut(name) {
binding.used = true;
return Some(binding.resolved.clone());
}
}
None
}
pub fn mark_used(&mut self, name: &str) {
for scope in self.scopes.iter_mut().rev() {
if let Some(binding) = scope.bindings.get_mut(name) {
binding.used = true;
return;
}
}
}
#[must_use]
pub fn lookup_peek(&self, name: &str) -> Option<&Binding> {
for scope in self.scopes.iter().rev() {
if let Some(b) = scope.bindings.get(name) {
return Some(b);
}
}
None
}
pub fn record_resolution(&mut self, use_id: NodeId, resolved: ResolvedName) {
self.resolutions.insert(use_id, resolved);
}
#[must_use]
pub fn visible_names(&self) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::new();
let mut out = Vec::new();
for scope in self.scopes.iter().rev() {
for name in scope.bindings.keys() {
if seen.insert(name.clone()) {
out.push(name.clone());
}
}
}
out
}
#[must_use]
pub fn has_wildcard_import(&self) -> bool {
self.scopes
.first()
.map(|s| {
s.bindings
.values()
.any(|b| b.is_import && b.name.ends_with(".*"))
})
.unwrap_or(false)
}
#[must_use]
pub fn unused_imports(&self) -> Vec<&Binding> {
self.scopes
.first()
.map(|s| {
s.bindings
.values()
.filter(|b| b.is_import && !b.used)
.collect()
})
.unwrap_or_default()
}
}
struct Resolver<'a> {
symbols: &'a mut SymbolTable,
diag: &'a mut DiagnosticBag,
synthetic_id: NodeId,
registry: Option<&'a ModuleRegistry>,
}
impl<'a> Resolver<'a> {
fn new(symbols: &'a mut SymbolTable, diag: &'a mut DiagnosticBag) -> Self {
Self {
symbols,
diag,
synthetic_id: u32::MAX / 2,
registry: None,
}
}
fn next_synthetic_id(&mut self) -> NodeId {
let id = self.synthetic_id;
self.synthetic_id += 1;
id
}
fn resolve_module(&mut self, module: &Module) {
self.symbols.seed_prelude();
self.collect_imports(module);
self.collect_items(&module.items);
for item in &module.items {
self.resolve_item(item);
}
self.check_unused_imports();
}
fn collect_imports(&mut self, module: &Module) {
for import in &module.imports {
let module_id = module_path_str(&import.path);
match &import.items {
ImportItems::Module => {
let name = import
.path
.segments
.last()
.map(|s| s.name.clone())
.unwrap_or_default();
self.symbols.define(
name.clone(),
Binding {
name,
resolved: ResolvedName {
def_id: import.id,
kind: NameKind::Module,
},
visibility: Visibility::Private,
span: import.span,
used: false,
is_import: true,
},
);
}
ImportItems::Named(names) => {
for imported in names {
let local = imported.alias.as_ref().unwrap_or(&imported.name);
let kind = if let Some(registry) = self.registry {
match registry.resolve_symbol(&module_id, &imported.name.name) {
Ok(sym) => {
if sym.kind == ExportKind::Effect {
seed_effect_info_from_registry(
self.symbols,
&local.name,
sym,
import.id,
import.span,
);
}
if sym.kind == ExportKind::Enum {
seed_enum_variants_from_registry(
self.symbols,
&local.name,
sym,
import.id,
import.span,
);
}
export_kind_to_name_kind(sym.kind)
}
Err(RegistryError::ModuleNotFound { .. }) => {
self.diag.error(
E_MODULE_NOT_FOUND,
format!("module `{module_id}` not found"),
import.span,
);
NameKind::Unresolved
}
Err(RegistryError::SymbolNotFound { name, .. }) => {
self.diag.error(
E_SYMBOL_NOT_FOUND,
format!(
"`{name}` is not exported by module `{module_id}`"
),
imported.span,
);
NameKind::Unresolved
}
Err(RegistryError::NotVisible { name, .. }) => {
self.diag.error(
E_NOT_VISIBLE,
format!(
"`{name}` in module `{module_id}` is private"
),
imported.span,
);
NameKind::Unresolved
}
}
} else {
NameKind::Unresolved
};
self.symbols.define(
local.name.clone(),
Binding {
name: local.name.clone(),
resolved: ResolvedName {
def_id: import.id,
kind,
},
visibility: Visibility::Private,
span: imported.span,
used: false,
is_import: true,
},
);
}
}
ImportItems::Glob => {
if let Some(registry) = self.registry {
match registry.resolve_glob(&module_id) {
Ok(exports) => {
for (name, sym) in exports {
if sym.kind == ExportKind::Effect {
seed_effect_info_from_registry(
self.symbols,
name,
sym,
import.id,
import.span,
);
}
if sym.kind == ExportKind::Enum {
seed_enum_variants_from_registry(
self.symbols,
name,
sym,
import.id,
import.span,
);
}
self.symbols.define(
name.to_string(),
Binding {
name: name.to_string(),
resolved: ResolvedName {
def_id: import.id,
kind: export_kind_to_name_kind(sym.kind),
},
visibility: Visibility::Private,
span: import.span,
used: false,
is_import: true,
},
);
}
}
Err(RegistryError::ModuleNotFound { .. }) => {
self.diag.error(
E_MODULE_NOT_FOUND,
format!("module `{module_id}` not found"),
import.span,
);
}
Err(e) => {
self.diag.error(
E_SYMBOL_NOT_FOUND,
format!("{e}"),
import.span,
);
}
}
}
let sentinel = format!("{}.*", module_id);
self.symbols.define(
sentinel.clone(),
Binding {
name: sentinel,
resolved: ResolvedName {
def_id: import.id,
kind: NameKind::Module,
},
visibility: Visibility::Private,
span: import.span,
used: true, is_import: true,
},
);
}
}
}
}
fn collect_items(&mut self, items: &[Item]) {
for item in items {
match item {
Item::Fn(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Function,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
}
Item::Record(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Type,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
}
Item::Enum(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Type,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
for variant in &d.variants {
let (vname, vid, vspan) = match variant {
EnumVariant::Unit { name, id, span } => (name, id, span),
EnumVariant::Struct { name, id, span, .. } => (name, id, span),
EnumVariant::Tuple { name, id, span, .. } => (name, id, span),
};
self.symbols.define(
vname.name.clone(),
Binding {
name: vname.name.clone(),
resolved: ResolvedName {
def_id: *vid,
kind: NameKind::Function,
},
visibility: d.visibility,
span: *vspan,
used: false,
is_import: false,
},
);
}
}
Item::Class(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Type,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
}
Item::Trait(d) | Item::PlatformTrait(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Trait,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
}
Item::Effect(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Effect,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
let ops: Vec<(String, NodeId, Span)> = d
.operations
.iter()
.map(|op| (op.name.name.clone(), op.id, op.span))
.collect();
let components: Vec<String> = d
.components
.iter()
.map(|tp| {
tp.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".")
})
.collect();
self.symbols.effect_info.insert(
d.name.name.clone(),
EffectInfo {
operations: ops,
components,
},
);
}
Item::TypeAlias(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Type,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
}
Item::Const(d) => {
self.symbols.define(
d.name.name.clone(),
Binding {
name: d.name.name.clone(),
resolved: ResolvedName {
def_id: d.id,
kind: NameKind::Variable,
},
visibility: d.visibility,
span: d.span,
used: false,
is_import: false,
},
);
}
Item::Impl(_)
| Item::ModuleHandle(_)
| Item::PropertyTest(_)
| Item::Error { .. } => {}
}
}
}
fn resolve_item(&mut self, item: &Item) {
match item {
Item::Fn(d) => self.resolve_fn(d),
Item::Impl(d) => self.resolve_impl(d),
Item::Class(d) => {
for m in &d.methods {
self.resolve_fn(m);
}
}
Item::Trait(d) | Item::PlatformTrait(d) => {
for m in &d.methods {
self.resolve_fn(m);
}
}
Item::Effect(d) => {
for op in &d.operations {
self.resolve_fn(op);
}
}
Item::Const(d) => self.resolve_expr(&d.value),
Item::ModuleHandle(d) => self.resolve_expr(&d.handler),
Item::PropertyTest(d) => self.resolve_block(&d.body),
Item::Record(_) | Item::Enum(_) | Item::TypeAlias(_) | Item::Error { .. } => {}
}
}
fn resolve_fn(&mut self, d: &FnDecl) {
self.symbols.push_scope();
for param in &d.params {
self.resolve_param(param);
}
if let Some(ret) = &d.return_type {
self.resolve_type_expr(ret);
}
self.inject_effect_operations(&d.effect_clause);
if let Some(ref body) = d.body {
self.resolve_block_body(body);
}
self.symbols.pop_scope();
}
fn inject_effect_operations(&mut self, effect_clause: &[bock_ast::TypePath]) {
let mut visited = HashSet::new();
for effect_path in effect_clause {
let effect_name = effect_path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
self.inject_ops_for_effect(&effect_name, &mut visited);
}
}
fn inject_ops_for_effect(&mut self, effect_name: &str, visited: &mut HashSet<String>) {
if !visited.insert(effect_name.to_string()) {
return; }
let info = self.symbols.effect_info.get(effect_name).cloned();
if let Some(info) = info {
for (op_name, op_id, op_span) in &info.operations {
self.symbols.define(
op_name.clone(),
Binding {
name: op_name.clone(),
resolved: ResolvedName {
def_id: *op_id,
kind: NameKind::Function,
},
visibility: Visibility::Public,
span: *op_span,
used: true, is_import: false,
},
);
}
let components = info.components.clone();
for component in &components {
self.inject_ops_for_effect(component, visited);
}
}
}
fn resolve_param(&mut self, param: &Param) {
self.collect_pattern_bindings(¶m.pattern, NameKind::Variable, Visibility::Private);
if let Some(ty) = ¶m.ty {
self.resolve_type_expr(ty);
}
if let Some(default) = ¶m.default {
self.resolve_expr(default);
}
}
fn resolve_impl(&mut self, d: &ImplBlock) {
for m in &d.methods {
let has_self = m.params.iter().any(|p| {
matches!(&p.pattern, Pattern::Bind { name, .. } if name.name == "self")
});
if has_self || d.trait_path.is_none() {
self.resolve_fn(m);
} else {
self.symbols.push_scope();
let syn_id = self.next_synthetic_id();
self.symbols.define(
"self".to_string(),
Binding {
name: "self".to_string(),
resolved: ResolvedName {
def_id: syn_id,
kind: NameKind::Variable,
},
visibility: Visibility::Private,
span: m.span,
used: false,
is_import: false,
},
);
for param in &m.params {
self.resolve_param(param);
}
self.inject_effect_operations(&m.effect_clause);
if let Some(ref body) = m.body {
self.resolve_block_body(body);
}
self.symbols.pop_scope();
}
}
}
fn resolve_block(&mut self, block: &Block) {
self.symbols.push_scope();
self.resolve_block_body(block);
self.symbols.pop_scope();
}
fn resolve_block_body(&mut self, block: &Block) {
for stmt in &block.stmts {
self.resolve_stmt(stmt);
}
if let Some(tail) = &block.tail {
self.resolve_expr(tail);
}
}
fn resolve_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Let(s) => self.resolve_let(s),
Stmt::Expr(e) => self.resolve_expr(e),
Stmt::For(f) => self.resolve_for(f),
Stmt::While(w) => self.resolve_while(w),
Stmt::Loop(l) => self.resolve_loop(l),
Stmt::Guard(g) => self.resolve_guard(g),
Stmt::Handling(h) => self.resolve_handling(h),
Stmt::Empty => {}
}
}
fn resolve_let(&mut self, s: &LetStmt) {
self.resolve_expr(&s.value);
if let Some(ty) = &s.ty {
self.resolve_type_expr(ty);
}
self.collect_pattern_bindings(&s.pattern, NameKind::Variable, Visibility::Private);
}
fn resolve_for(&mut self, f: &ForLoop) {
self.resolve_expr(&f.iterable);
self.symbols.push_scope();
self.collect_pattern_bindings(&f.pattern, NameKind::Variable, Visibility::Private);
self.resolve_block_body(&f.body);
self.symbols.pop_scope();
}
fn resolve_while(&mut self, w: &WhileLoop) {
self.resolve_expr(&w.condition);
self.resolve_block(&w.body);
}
fn resolve_loop(&mut self, l: &LoopStmt) {
self.resolve_block(&l.body);
}
fn resolve_guard(&mut self, g: &GuardStmt) {
self.resolve_expr(&g.condition);
if let Some(pat) = &g.let_pattern {
self.collect_pattern_bindings(pat, NameKind::Variable, Visibility::Private);
}
self.resolve_block(&g.else_block);
}
fn resolve_handling(&mut self, h: &HandlingBlock) {
for pair in &h.handlers {
self.resolve_expr(&pair.handler);
}
self.resolve_block(&h.body);
}
fn resolve_expr(&mut self, expr: &Expr) {
match expr {
Expr::Identifier { id, name, .. } => {
if let Some(resolved) = self.symbols.lookup(&name.name) {
self.symbols.record_resolution(*id, resolved);
} else if !self.symbols.has_wildcard_import() {
let visible = self.symbols.visible_names();
let diag = self.diag.error(
E_UNDEFINED,
format!("undefined name `{}`", name.name),
name.span,
);
if let Some(hint) = keyword_hint(&name.name) {
diag.note(hint);
} else if let Some(suggestion) =
bock_errors::suggest_similar(&name.name, visible, 2)
{
diag.note(format!("did you mean `{suggestion}`?"));
}
}
}
Expr::Literal { .. }
| Expr::Continue { .. }
| Expr::Unreachable { .. }
| Expr::Placeholder { .. } => {}
Expr::Binary { left, right, .. } => {
self.resolve_expr(left);
self.resolve_expr(right);
}
Expr::Unary { operand, .. } => self.resolve_expr(operand),
Expr::Assign { target, value, .. } => {
self.resolve_expr(target);
self.resolve_expr(value);
}
Expr::Call { callee, args, .. } => {
self.resolve_expr(callee);
for arg in args {
self.resolve_expr(&arg.value);
}
}
Expr::MethodCall { receiver, args, .. } => {
self.resolve_expr(receiver);
for arg in args {
self.resolve_expr(&arg.value);
}
}
Expr::FieldAccess { object, .. } => self.resolve_expr(object),
Expr::Index { object, index, .. } => {
self.resolve_expr(object);
self.resolve_expr(index);
}
Expr::Try { expr, .. } => self.resolve_expr(expr),
Expr::Lambda { params, body, .. } => {
self.symbols.push_scope();
for p in params {
self.resolve_param(p);
}
self.resolve_expr(body);
self.symbols.pop_scope();
}
Expr::Pipe { left, right, .. } | Expr::Compose { left, right, .. } => {
self.resolve_expr(left);
self.resolve_expr(right);
}
Expr::If {
condition,
let_pattern,
then_block,
else_block,
..
} => {
self.resolve_expr(condition);
self.symbols.push_scope();
if let Some(pat) = let_pattern {
self.collect_pattern_bindings(pat, NameKind::Variable, Visibility::Private);
}
self.resolve_block_body(then_block);
self.symbols.pop_scope();
if let Some(eb) = else_block {
self.resolve_expr(eb);
}
}
Expr::Match {
scrutinee, arms, ..
} => {
self.resolve_expr(scrutinee);
for arm in arms {
self.resolve_match_arm(arm);
}
}
Expr::Loop { body, .. } => self.resolve_block(body),
Expr::Block { block, .. } => self.resolve_block(block),
Expr::RecordConstruct {
path,
fields,
spread,
..
} => {
if let Some(first) = path.segments.first() {
self.symbols.mark_used(&first.name);
}
for f in fields {
if let Some(v) = &f.value {
self.resolve_expr(v);
}
}
if let Some(s) = spread {
self.resolve_expr(&s.expr);
}
}
Expr::ListLiteral { elems, .. }
| Expr::SetLiteral { elems, .. }
| Expr::TupleLiteral { elems, .. } => {
for e in elems {
self.resolve_expr(e);
}
}
Expr::MapLiteral { entries, .. } => {
for (k, v) in entries {
self.resolve_expr(k);
self.resolve_expr(v);
}
}
Expr::Range { lo, hi, .. } => {
self.resolve_expr(lo);
self.resolve_expr(hi);
}
Expr::Await { expr, .. } => self.resolve_expr(expr),
Expr::Return { value, .. } | Expr::Break { value, .. } => {
if let Some(v) = value {
self.resolve_expr(v);
}
}
Expr::Interpolation { parts, .. } => {
for part in parts {
if let InterpolationPart::Expr(e) = part {
self.resolve_expr(e);
}
}
}
Expr::Is { expr, .. } => {
self.resolve_expr(expr);
}
}
}
fn resolve_type_expr(&mut self, ty: &bock_ast::TypeExpr) {
match ty {
bock_ast::TypeExpr::Named { path, args, .. } => {
if let Some(first) = path.segments.first() {
self.symbols.mark_used(&first.name);
}
for arg in args {
self.resolve_type_expr(arg);
}
}
bock_ast::TypeExpr::Tuple { elems, .. } => {
for e in elems {
self.resolve_type_expr(e);
}
}
bock_ast::TypeExpr::Function {
params, ret, effects, ..
} => {
for p in params {
self.resolve_type_expr(p);
}
self.resolve_type_expr(ret);
for eff in effects {
if let Some(first) = eff.segments.first() {
self.symbols.mark_used(&first.name);
}
}
}
bock_ast::TypeExpr::Optional { inner, .. } => {
self.resolve_type_expr(inner);
}
bock_ast::TypeExpr::SelfType { .. } => {}
}
}
fn resolve_match_arm(&mut self, arm: &MatchArm) {
self.symbols.push_scope();
self.collect_pattern_bindings(&arm.pattern, NameKind::Variable, Visibility::Private);
if let Some(g) = &arm.guard {
self.resolve_expr(g);
}
self.resolve_expr(&arm.body);
self.symbols.pop_scope();
}
fn collect_pattern_bindings(
&mut self,
pattern: &Pattern,
kind: NameKind,
visibility: Visibility,
) {
match pattern {
Pattern::Wildcard { .. } | Pattern::Literal { .. } | Pattern::Rest { .. } => {}
Pattern::Bind { id, span, name } | Pattern::MutBind { id, span, name } => {
self.symbols.define(
name.name.clone(),
Binding {
name: name.name.clone(),
resolved: ResolvedName { def_id: *id, kind },
visibility,
span: *span,
used: false,
is_import: false,
},
);
}
Pattern::Constructor { path, fields, .. } => {
if let Some(first) = path.segments.first() {
self.symbols.mark_used(&first.name);
}
for f in fields {
self.collect_pattern_bindings(f, kind, visibility);
}
}
Pattern::Tuple { elems, .. } => {
for e in elems {
self.collect_pattern_bindings(e, kind, visibility);
}
}
Pattern::Record { path, fields, .. } => {
if let Some(first) = path.segments.first() {
self.symbols.mark_used(&first.name);
}
for f in fields {
if let Some(p) = &f.pattern {
self.collect_pattern_bindings(p, kind, visibility);
} else {
let syn_id = self.next_synthetic_id();
self.symbols.define(
f.name.name.clone(),
Binding {
name: f.name.name.clone(),
resolved: ResolvedName {
def_id: syn_id,
kind,
},
visibility,
span: f.span,
used: false,
is_import: false,
},
);
}
}
}
Pattern::List { elems, rest, .. } => {
for e in elems {
self.collect_pattern_bindings(e, kind, visibility);
}
if let Some(r) = rest {
self.collect_pattern_bindings(r, kind, visibility);
}
}
Pattern::Or { alternatives, .. } => {
if let Some(first) = alternatives.first() {
self.collect_pattern_bindings(first, kind, visibility);
}
}
Pattern::Range { lo, hi, .. } => {
self.collect_pattern_bindings(lo, kind, visibility);
self.collect_pattern_bindings(hi, kind, visibility);
}
}
}
fn check_unused_imports(&mut self) {
if let Some(scope) = self.symbols.scopes.first() {
let used_parents: Vec<String> = self
.symbols
.variant_parent
.iter()
.filter(|(variant, _)| {
scope
.bindings
.get(variant.as_str())
.is_some_and(|b| b.used)
})
.map(|(_, parent)| parent.clone())
.collect();
for parent in used_parents {
self.symbols.mark_used(&parent);
}
}
let unused: Vec<(String, Span)> = self
.symbols
.scopes
.first()
.map(|s| {
s.bindings
.values()
.filter(|b| b.is_import && !b.used)
.map(|b| (b.name.clone(), b.span))
.collect()
})
.unwrap_or_default();
for (name, span) in unused {
self.diag
.warning(W_UNUSED_IMPORT, format!("unused import `{name}`"), span);
}
}
}
fn module_path_str(path: &ModulePath) -> String {
path.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".")
}
fn export_kind_to_name_kind(kind: ExportKind) -> NameKind {
match kind {
ExportKind::Function => NameKind::Function,
ExportKind::Record | ExportKind::Enum | ExportKind::TypeAlias => NameKind::Type,
ExportKind::Trait => NameKind::Trait,
ExportKind::Effect => NameKind::Effect,
ExportKind::Constant => NameKind::Variable,
}
}
fn seed_effect_info_from_registry(
symbols: &mut SymbolTable,
local_name: &str,
sym: &ExportedSymbol,
import_id: NodeId,
import_span: Span,
) {
if let ExportDetail::Effect {
operations,
components,
} = &sym.detail
{
let ops: Vec<(String, NodeId, Span)> = operations
.iter()
.map(|(name, _type_ref)| (name.clone(), import_id, import_span))
.collect();
symbols.effect_info.insert(
local_name.to_string(),
EffectInfo {
operations: ops,
components: components.clone(),
},
);
}
}
fn seed_enum_variants_from_registry(
symbols: &mut SymbolTable,
enum_name: &str,
sym: &ExportedSymbol,
import_id: NodeId,
import_span: Span,
) {
if let ExportDetail::Enum { variants, .. } = &sym.detail {
for variant in variants {
symbols.variant_parent.insert(
variant.name.clone(),
enum_name.to_string(),
);
symbols.define(
variant.name.clone(),
Binding {
name: variant.name.clone(),
resolved: ResolvedName {
def_id: import_id,
kind: NameKind::Function,
},
visibility: Visibility::Private,
span: import_span,
used: false,
is_import: false,
},
);
}
}
}
fn keyword_hint(name: &str) -> Option<&'static str> {
match name {
"pub" => Some("Bock uses `public` for visibility, not `pub`"),
"var" => Some("Bock uses `let mut` for mutable bindings, not `var`"),
"func" | "def" => Some("Bock uses `fn` to declare functions"),
"interface" => Some("Bock uses `trait` for interfaces"),
"struct" => Some("Bock uses `record` for value types"),
"class" => Some("Bock uses `record` for data and `trait` for behavior — there is no `class`"),
"None_" | "nil" | "null" | "undefined" => {
Some("Bock uses `None` (from `Optional[T]`) to represent absent values")
}
"true_" | "false_" => Some("Bock boolean literals are `true` and `false`"),
_ => None,
}
}
pub fn resolve_names(ast: &Module, symbols: &mut SymbolTable) -> DiagnosticBag {
let mut diag = DiagnosticBag::new();
let mut resolver = Resolver::new(symbols, &mut diag);
resolver.resolve_module(ast);
diag
}
pub fn resolve_names_with_registry(
ast: &Module,
symbols: &mut SymbolTable,
registry: &ModuleRegistry,
) -> DiagnosticBag {
let mut diag = DiagnosticBag::new();
let mut resolver = Resolver::new(symbols, &mut diag);
resolver.registry = Some(registry);
resolver.resolve_module(ast);
diag
}
#[cfg(test)]
mod tests {
use super::*;
use bock_ast::{
Block, FnDecl, Ident, ImportDecl, ImportItems, ImportedName, Item, Literal, Module,
ModulePath, Param, Pattern, Stmt, Visibility,
};
use bock_errors::{FileId, Span};
fn sp() -> Span {
Span {
file: FileId(0),
start: 0,
end: 1,
}
}
fn ident(name: &str) -> Ident {
Ident {
name: name.to_string(),
span: sp(),
}
}
fn mpath(segments: &[&str]) -> ModulePath {
ModulePath {
segments: segments.iter().map(|s| ident(s)).collect(),
span: sp(),
}
}
fn empty_block(id: NodeId) -> Block {
Block {
id,
span: sp(),
stmts: vec![],
tail: None,
}
}
fn simple_module(imports: Vec<ImportDecl>, items: Vec<Item>) -> Module {
Module {
id: 0,
span: sp(),
doc: vec![],
path: None,
imports,
items,
}
}
fn fn_item(id: NodeId, name: &str, vis: Visibility) -> Item {
Item::Fn(FnDecl {
id,
span: sp(),
annotations: vec![],
visibility: vis,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(empty_block(id + 100)),
})
}
#[test]
fn symbol_table_define_and_lookup() {
let mut st = SymbolTable::new();
st.define(
"foo".into(),
Binding {
name: "foo".into(),
resolved: ResolvedName {
def_id: 1,
kind: NameKind::Function,
},
visibility: Visibility::Public,
span: sp(),
used: false,
is_import: false,
},
);
let r = st.lookup("foo").unwrap();
assert_eq!(r.def_id, 1);
assert_eq!(r.kind, NameKind::Function);
}
#[test]
fn symbol_table_lookup_marks_used() {
let mut st = SymbolTable::new();
st.define(
"x".into(),
Binding {
name: "x".into(),
resolved: ResolvedName {
def_id: 5,
kind: NameKind::Variable,
},
visibility: Visibility::Private,
span: sp(),
used: false,
is_import: false,
},
);
st.lookup("x");
assert!(st.lookup_peek("x").unwrap().used);
}
#[test]
fn symbol_table_inner_scope_shadows_outer() {
let mut st = SymbolTable::new();
st.define(
"x".into(),
Binding {
name: "x".into(),
resolved: ResolvedName {
def_id: 1,
kind: NameKind::Variable,
},
visibility: Visibility::Private,
span: sp(),
used: false,
is_import: false,
},
);
st.push_scope();
st.define(
"x".into(),
Binding {
name: "x".into(),
resolved: ResolvedName {
def_id: 2,
kind: NameKind::Variable,
},
visibility: Visibility::Private,
span: sp(),
used: false,
is_import: false,
},
);
assert_eq!(st.lookup("x").unwrap().def_id, 2);
st.pop_scope();
assert_eq!(st.lookup("x").unwrap().def_id, 1);
}
#[test]
fn symbol_table_lookup_unknown_returns_none() {
let mut st = SymbolTable::new();
assert!(st.lookup("unknown").is_none());
}
#[test]
fn symbol_table_module_scope_never_popped() {
let mut st = SymbolTable::new();
assert!(st.pop_scope().is_none()); }
#[test]
fn resolve_defined_identifier() {
let module = simple_module(
vec![],
vec![
fn_item(1, "foo", Visibility::Private),
Item::Fn(FnDecl {
id: 2,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("bar"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 200,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Identifier {
id: 99,
span: sp(),
name: ident("foo"),
})),
}),
}),
],
);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(
!diag.has_errors(),
"unexpected errors: {:?}",
diag.iter().collect::<Vec<_>>()
);
let resolved = st
.resolutions
.get(&99)
.expect("identifier should be resolved");
assert_eq!(resolved.def_id, 1);
assert_eq!(resolved.kind, NameKind::Function);
}
#[test]
fn resolve_undefined_identifier_produces_error() {
let module = simple_module(
vec![],
vec![Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("bar"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 100,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Identifier {
id: 42,
span: sp(),
name: ident("undefined_thing"),
})),
}),
})],
);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(diag.has_errors());
let msgs: Vec<_> = diag.iter().map(|d| d.message.as_str()).collect();
assert!(msgs.iter().any(|m| m.contains("undefined_thing")));
}
#[test]
fn named_import_creates_binding() {
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["core", "collections"]),
items: ImportItems::Named(vec![
ImportedName {
span: sp(),
name: ident("List"),
alias: None,
},
ImportedName {
span: sp(),
name: ident("Map"),
alias: None,
},
]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
resolve_names(&module, &mut st);
assert!(st.lookup_peek("List").is_some());
assert!(st.lookup_peek("Map").is_some());
}
#[test]
fn named_import_with_alias() {
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["core"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("FooBar"),
alias: Some(ident("FB")),
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
resolve_names(&module, &mut st);
assert!(st.lookup_peek("FB").is_some());
assert!(st.lookup_peek("FooBar").is_none());
}
#[test]
fn module_import_creates_binding() {
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Module,
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
resolve_names(&module, &mut st);
let b = st.lookup_peek("models").expect("models should be bound");
assert_eq!(b.resolved.kind, NameKind::Module);
}
#[test]
fn wildcard_import_suppresses_undefined_errors() {
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["some", "module"]),
items: ImportItems::Glob,
};
let module = simple_module(
vec![import],
vec![Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 100,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Identifier {
id: 99,
span: sp(),
name: ident("SomethingFromWildcard"),
})),
}),
})],
);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(
!diag.has_errors(),
"wildcard import should suppress undefined errors"
);
}
#[test]
fn unused_named_import_produces_warning() {
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["core"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("Unused"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(!diag.has_errors());
let warnings: Vec<_> = diag
.iter()
.filter(|d| d.severity == bock_errors::Severity::Warning)
.collect();
assert!(!warnings.is_empty(), "expected unused import warning");
assert!(warnings.iter().any(|w| w.message.contains("Unused")));
}
#[test]
fn used_import_no_warning() {
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["core"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("Used"),
alias: None,
}]),
};
let module = simple_module(
vec![import],
vec![Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 100,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Identifier {
id: 99,
span: sp(),
name: ident("Used"),
})),
}),
})],
);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(!diag.has_errors());
let warnings: Vec<_> = diag
.iter()
.filter(|d| d.severity == bock_errors::Severity::Warning)
.collect();
assert!(warnings.is_empty(), "no warning expected for used import");
}
#[test]
fn let_binding_shadows_outer() {
use bock_ast::LetStmt;
let outer_let = Stmt::Let(LetStmt {
id: 10,
span: sp(),
pattern: Pattern::Bind {
id: 10,
span: sp(),
name: ident("x"),
},
ty: None,
value: Expr::Literal {
id: 11,
span: sp(),
lit: Literal::Int("1".into()),
},
});
let inner_let = Stmt::Let(LetStmt {
id: 20,
span: sp(),
pattern: Pattern::Bind {
id: 20,
span: sp(),
name: ident("x"),
},
ty: None,
value: Expr::Literal {
id: 21,
span: sp(),
lit: Literal::Int("2".into()),
},
});
let use_x = Expr::Identifier {
id: 99,
span: sp(),
name: ident("x"),
};
let module = simple_module(
vec![],
vec![Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 100,
span: sp(),
stmts: vec![outer_let, inner_let],
tail: Some(Box::new(use_x)),
}),
})],
);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(!diag.has_errors());
let resolved = st.resolutions.get(&99).expect("x should be resolved");
assert_eq!(resolved.def_id, 20);
}
#[test]
fn function_param_is_in_scope() {
let module = simple_module(
vec![],
vec![Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("id"),
generic_params: vec![],
params: vec![Param {
id: 5,
span: sp(),
pattern: Pattern::Bind {
id: 5,
span: sp(),
name: ident("n"),
},
ty: None,
default: None,
}],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 100,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Identifier {
id: 99,
span: sp(),
name: ident("n"),
})),
}),
})],
);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(!diag.has_errors());
let resolved = st.resolutions.get(&99).expect("param n should resolve");
assert_eq!(resolved.def_id, 5);
assert_eq!(resolved.kind, NameKind::Variable);
}
#[test]
fn visibility_is_stored_in_binding() {
let module = simple_module(vec![], vec![fn_item(1, "pub_fn", Visibility::Public)]);
let mut st = SymbolTable::new();
resolve_names(&module, &mut st);
let b = st.lookup_peek("pub_fn").expect("pub_fn should be bound");
assert_eq!(b.visibility, Visibility::Public);
}
use crate::registry::{
EnumVariantExport, ExportDetail, ExportKind, ExportedSymbol, ModuleExports,
ModuleRegistry,
};
use crate::stubs::TypeRef;
fn sample_registry() -> ModuleRegistry {
let mut reg = ModuleRegistry::new();
let mut exports = ModuleExports::new("app.models", "src/app/models.bock");
exports.add_symbol(
"User",
ExportedSymbol {
kind: ExportKind::Record,
visibility: Visibility::Public,
ty: TypeRef("User".to_string()),
detail: ExportDetail::Record {
fields: vec![
("name".to_string(), TypeRef("String".to_string())),
("age".to_string(), TypeRef("Int".to_string())),
],
generic_params: vec![],
methods: HashMap::new(),
},
},
);
exports.add_symbol(
"Role",
ExportedSymbol {
kind: ExportKind::Enum,
visibility: Visibility::Public,
ty: TypeRef("Role".to_string()),
detail: ExportDetail::Enum {
variants: vec![],
generic_params: vec![],
},
},
);
exports.add_symbol(
"default_user",
ExportedSymbol {
kind: ExportKind::Function,
visibility: Visibility::Public,
ty: TypeRef("Fn() -> User".to_string()),
detail: ExportDetail::None,
},
);
exports.add_symbol(
"internal_helper",
ExportedSymbol {
kind: ExportKind::Function,
visibility: Visibility::Internal,
ty: TypeRef("Fn() -> Void".to_string()),
detail: ExportDetail::None,
},
);
exports.add_symbol(
"private_secret",
ExportedSymbol {
kind: ExportKind::Function,
visibility: Visibility::Private,
ty: TypeRef("Fn() -> Void".to_string()),
detail: ExportDetail::None,
},
);
reg.register(exports);
reg
}
#[test]
fn registry_named_import_resolves_kind() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Named(vec![
ImportedName {
span: sp(),
name: ident("User"),
alias: None,
},
ImportedName {
span: sp(),
name: ident("default_user"),
alias: None,
},
]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(
!diag.has_errors(),
"unexpected errors: {:?}",
diag.iter().collect::<Vec<_>>()
);
let user = st.lookup_peek("User").expect("User should be bound");
assert_eq!(user.resolved.kind, NameKind::Type);
assert!(user.is_import);
let dfn = st
.lookup_peek("default_user")
.expect("default_user should be bound");
assert_eq!(dfn.resolved.kind, NameKind::Function);
}
#[test]
fn registry_named_import_with_alias_resolves_kind() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("User"),
alias: Some(ident("AppUser")),
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(!diag.has_errors());
assert!(st.lookup_peek("AppUser").is_some());
assert!(st.lookup_peek("User").is_none());
assert_eq!(
st.lookup_peek("AppUser").unwrap().resolved.kind,
NameKind::Type
);
}
#[test]
fn registry_named_import_missing_symbol_produces_error() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("NonExistent"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(diag.has_errors());
let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
assert!(msgs.iter().any(|m| m.contains("NonExistent")));
assert!(msgs.iter().any(|m| m.contains("not exported")));
}
#[test]
fn registry_named_import_private_symbol_produces_error() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("private_secret"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(diag.has_errors());
let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
assert!(msgs.iter().any(|m| m.contains("private")));
}
#[test]
fn registry_named_import_module_not_found_produces_error() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["no", "such", "module"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("Foo"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(diag.has_errors());
let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
assert!(msgs.iter().any(|m| m.contains("not found")));
}
#[test]
fn registry_glob_import_defines_all_public_names() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Glob,
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(
!diag.has_errors(),
"unexpected errors: {:?}",
diag.iter().collect::<Vec<_>>()
);
assert!(st.lookup_peek("User").is_some());
assert!(st.lookup_peek("Role").is_some());
assert!(st.lookup_peek("default_user").is_some());
assert!(st.lookup_peek("internal_helper").is_some());
assert!(
st.lookup_peek("private_secret").is_none()
|| st.lookup_peek("private_secret").unwrap().resolved.kind == NameKind::Builtin
);
assert!(st.has_wildcard_import());
}
#[test]
fn registry_glob_import_module_not_found_produces_error() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["no", "such", "module"]),
items: ImportItems::Glob,
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(diag.has_errors());
let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
assert!(msgs.iter().any(|m| m.contains("not found")));
}
#[test]
fn registry_resolved_import_used_in_body_no_errors() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Named(vec![
ImportedName {
span: sp(),
name: ident("User"),
alias: None,
},
ImportedName {
span: sp(),
name: ident("default_user"),
alias: None,
},
]),
};
let module = simple_module(
vec![import],
vec![Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("main"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 100,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Call {
id: 50,
span: sp(),
callee: Box::new(Expr::Identifier {
id: 51,
span: sp(),
name: ident("default_user"),
}),
type_args: vec![],
args: vec![],
})),
}),
})],
);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(
!diag.has_errors(),
"unexpected errors: {:?}",
diag.iter().collect::<Vec<_>>()
);
let resolved = st
.resolutions
.get(&51)
.expect("default_user should resolve");
assert_eq!(resolved.kind, NameKind::Function);
let warnings: Vec<_> = diag
.iter()
.filter(|d| d.severity == bock_errors::Severity::Warning)
.collect();
assert!(
warnings.iter().any(|w| w.message.contains("User")),
"expected unused import warning for User"
);
}
#[test]
fn empty_registry_behaves_like_single_file() {
let registry = ModuleRegistry::new();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["unknown", "module"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("Thing"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(diag.has_errors());
let b = st.lookup_peek("Thing").expect("Thing should still be bound");
assert_eq!(b.resolved.kind, NameKind::Unresolved);
}
#[test]
fn no_registry_leaves_imports_unresolved() {
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("User"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(!diag.has_errors());
let b = st.lookup_peek("User").expect("User should be bound");
assert_eq!(b.resolved.kind, NameKind::Unresolved);
}
#[test]
fn registry_internal_symbol_is_importable() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("internal_helper"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(
!diag.has_errors(),
"internal symbols should be importable: {:?}",
diag.iter().collect::<Vec<_>>()
);
let b = st
.lookup_peek("internal_helper")
.expect("internal_helper should be bound");
assert_eq!(b.resolved.kind, NameKind::Function);
}
#[test]
fn registry_named_enum_import_seeds_variant_constructors() {
let mut reg = ModuleRegistry::new();
let mut exports = ModuleExports::new("colors", "colors.bock");
exports.add_symbol(
"Color",
ExportedSymbol {
kind: ExportKind::Enum,
visibility: Visibility::Public,
ty: TypeRef("Color".to_string()),
detail: ExportDetail::Enum {
variants: vec![
EnumVariantExport {
name: "Red".to_string(),
constructor_type: None,
fields: None,
},
EnumVariantExport {
name: "Green".to_string(),
constructor_type: None,
fields: None,
},
EnumVariantExport {
name: "Blue".to_string(),
constructor_type: None,
fields: None,
},
],
generic_params: vec![],
},
},
);
reg.register(exports);
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["colors"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident("Color"),
alias: None,
}]),
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®);
assert!(
!diag.has_errors(),
"unexpected errors: {:?}",
diag.iter().collect::<Vec<_>>()
);
assert!(st.lookup_peek("Color").is_some());
let red = st.lookup_peek("Red").expect("Red should be in scope");
assert_eq!(red.resolved.kind, NameKind::Function);
assert!(!red.is_import);
assert!(st.lookup_peek("Green").is_some());
assert!(st.lookup_peek("Blue").is_some());
}
#[test]
fn registry_glob_enum_import_seeds_variant_constructors() {
let mut reg = ModuleRegistry::new();
let mut exports = ModuleExports::new("colors", "colors.bock");
exports.add_symbol(
"Color",
ExportedSymbol {
kind: ExportKind::Enum,
visibility: Visibility::Public,
ty: TypeRef("Color".to_string()),
detail: ExportDetail::Enum {
variants: vec![
EnumVariantExport {
name: "Red".to_string(),
constructor_type: None,
fields: None,
},
],
generic_params: vec![],
},
},
);
reg.register(exports);
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["colors"]),
items: ImportItems::Glob,
};
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®);
assert!(
!diag.has_errors(),
"unexpected errors: {:?}",
diag.iter().collect::<Vec<_>>()
);
assert!(st.lookup_peek("Color").is_some());
let red = st.lookup_peek("Red").expect("Red should be in scope via glob");
assert_eq!(red.resolved.kind, NameKind::Function);
assert!(!red.is_import);
}
#[test]
fn registry_glob_with_body_resolves_names() {
let registry = sample_registry();
let import = ImportDecl {
id: 10,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["app", "models"]),
items: ImportItems::Glob,
};
let module = simple_module(
vec![import],
vec![Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Some(Block {
id: 100,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Identifier {
id: 99,
span: sp(),
name: ident("User"),
})),
}),
})],
);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
assert!(
!diag.has_errors(),
"unexpected errors: {:?}",
diag.iter().collect::<Vec<_>>()
);
let resolved = st
.resolutions
.get(&99)
.expect("User should resolve from glob import");
assert_eq!(resolved.kind, NameKind::Type);
}
}