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: 1009,
};
const E_EFFECT_OP_UNAVAILABLE: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 6005,
};
const E_MODULE_NOT_FOUND: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 1010,
};
const E_SYMBOL_NOT_FOUND: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 1011,
};
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 effects_declaring_op(&self, op_name: &str) -> Vec<String> {
let mut effects: Vec<String> = self
.effect_info
.iter()
.filter(|(_, info)| info.operations.iter().any(|(name, _, _)| name == op_name))
.map(|(effect_name, _)| effect_name.clone())
.collect();
effects.sort();
effects
}
#[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>,
current_fn: Option<String>,
}
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,
current_fn: 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);
self.inject_module_handle_operations(&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,
)
.note(format!(
"declare `{name}` as `public` in module `{module_id}` to export it"
));
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) {
let prev_fn = self.current_fn.replace(d.name.name.clone());
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();
self.current_fn = prev_fn;
}
fn inject_effect_operations(&mut self, effect_clause: &[bock_ast::TypePath]) {
let mut visited = HashSet::new();
for effect_path in effect_clause {
if let Some(first) = effect_path.segments.first() {
self.symbols.mark_used(&first.name);
}
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_module_handle_operations(&mut self, items: &[Item]) {
let mut visited = HashSet::new();
for item in items {
if let Item::ModuleHandle(d) = item {
let effect_name = d
.effect
.segments
.iter()
.map(|seg| seg.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) {
if let Some(trait_path) = &d.trait_path {
if let Some(first) = trait_path.segments.first() {
self.symbols.mark_used(&first.name);
}
}
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.symbols.push_scope();
let mut visited = HashSet::new();
for pair in &h.handlers {
if let Some(first) = pair.effect.segments.first() {
self.symbols.mark_used(&first.name);
}
let effect_name = pair
.effect
.segments
.iter()
.map(|seg| seg.name.as_str())
.collect::<Vec<_>>()
.join(".");
self.inject_ops_for_effect(&effect_name, &mut visited);
}
self.resolve_block(&h.body);
self.symbols.pop_scope();
}
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 owning_effects = self.symbols.effects_declaring_op(&name.name);
if !owning_effects.is_empty() {
let effect_list = owning_effects
.iter()
.map(|e| format!("`{e}`"))
.collect::<Vec<_>>()
.join(", ");
let effect = &owning_effects[0];
let fix_target = match &self.current_fn {
Some(fn_name) => {
format!("the signature of the enclosing function `{fn_name}`")
}
None => "the enclosing function's signature".to_string(),
};
self.diag
.error(
E_EFFECT_OP_UNAVAILABLE,
format!(
"`{}` is an operation of effect {effect_list}, but the effect is neither declared by the enclosing function nor handled here",
name.name
),
name.span,
)
.note(format!(
"add `with {effect}` to {fix_target}, or wrap the call in a handler: `handling ({effect} with <handler>) {{ ... }}`"
));
return;
}
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")));
}
fn effect_log_item(id: NodeId) -> Item {
Item::Effect(bock_ast::EffectDecl {
id,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
name: ident("Log"),
generic_params: vec![],
components: vec![],
operations: vec![FnDecl {
id: id + 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("log"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: None,
}],
})
}
fn main_calling(id: NodeId, callee: &str) -> Item {
Item::Fn(FnDecl {
id,
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: id + 100,
span: sp(),
stmts: vec![],
tail: Some(Box::new(Expr::Identifier {
id: id + 101,
span: sp(),
name: ident(callee),
})),
}),
})
}
#[test]
fn undeclared_effect_op_reports_e6005_with_fix() {
let module = simple_module(vec![], vec![effect_log_item(1), main_calling(10, "log")]);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(diag.has_errors());
let d = diag
.iter()
.find(|d| d.severity == bock_errors::Severity::Error)
.expect("an error diagnostic");
assert_eq!(d.code.to_string(), "E6005");
assert!(
d.message.contains("`log`") && d.message.contains("`Log`"),
"message must name the op and the effect: {}",
d.message
);
assert!(
d.notes
.iter()
.any(|n| n.contains("with Log") && n.contains("`main`") && n.contains("handling")),
"note must state the concrete fix: {:?}",
d.notes
);
assert!(
!d.notes.iter().any(|n| n.contains("did you mean")),
"must not suggest a rename for an effect violation: {:?}",
d.notes
);
assert_eq!(diag.error_count(), 1);
}
#[test]
fn non_effect_op_typo_still_reports_e1009() {
let module = simple_module(
vec![],
vec![effect_log_item(1), main_calling(10, "totally_unknown")],
);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
let d = diag
.iter()
.find(|d| d.severity == bock_errors::Severity::Error)
.expect("an error diagnostic");
assert_eq!(d.code.to_string(), "E1009");
}
#[test]
fn declared_effect_op_resolves_without_error() {
let mut main_fn = main_calling(10, "log");
if let Item::Fn(f) = &mut main_fn {
f.effect_clause = vec![bock_ast::TypePath {
segments: vec![ident("Log")],
span: sp(),
}];
}
let module = simple_module(vec![], vec![effect_log_item(1), main_fn]);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(
!diag.has_errors(),
"declared effect op must resolve: {:?}",
diag.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn effects_declaring_op_is_sorted_and_filtered() {
let mut st = SymbolTable::new();
for effect in ["Zeta", "Alpha"] {
st.effect_info.insert(
effect.to_string(),
EffectInfo {
operations: vec![("log".to_string(), 1, sp())],
components: vec![],
},
);
}
st.effect_info.insert(
"Clock".to_string(),
EffectInfo {
operations: vec![("now".to_string(), 2, sp())],
components: vec![],
},
);
assert_eq!(st.effects_declaring_op("log"), vec!["Alpha", "Zeta"]);
assert_eq!(st.effects_declaring_op("now"), vec!["Clock"]);
assert!(st.effects_declaring_op("missing").is_empty());
}
#[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");
}
fn named_import(id: NodeId, name: &str) -> ImportDecl {
ImportDecl {
id,
span: sp(),
visibility: Visibility::Private,
path: mpath(&["core"]),
items: ImportItems::Named(vec![ImportedName {
span: sp(),
name: ident(name),
alias: None,
}]),
}
}
fn tpath(name: &str) -> bock_ast::TypePath {
bock_ast::TypePath {
segments: vec![ident(name)],
span: sp(),
}
}
fn unused_import_warnings(diag: &DiagnosticBag) -> Vec<String> {
diag.iter()
.filter(|d| d.severity == bock_errors::Severity::Warning)
.filter(|d| d.message.contains("unused import"))
.map(|d| d.message.clone())
.collect()
}
#[test]
fn effect_used_in_with_clause_is_not_unused_import() {
let import = named_import(10, "Log");
let func = Item::Fn(FnDecl {
id: 1,
span: sp(),
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("doWork"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![tpath("Log")],
where_clause: vec![],
body: Some(empty_block(100)),
});
let module = simple_module(vec![import], vec![func]);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(
unused_import_warnings(&diag).is_empty(),
"effect used in `with` clause must not warn W1001: {:?}",
unused_import_warnings(&diag)
);
}
#[test]
fn effect_used_in_impl_for_is_not_unused_import() {
let import = named_import(10, "Log");
let impl_block = Item::Impl(bock_ast::ImplBlock {
id: 2,
span: sp(),
annotations: vec![],
generic_params: vec![],
trait_path: Some(tpath("Log")),
trait_args: vec![],
target: bock_ast::TypeExpr::Named {
id: 3,
span: sp(),
path: tpath("Silent"),
args: vec![],
},
where_clause: vec![],
type_assignments: vec![],
methods: vec![],
});
let module = simple_module(vec![import], vec![impl_block]);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
assert!(
unused_import_warnings(&diag).is_empty(),
"effect used in `impl ... for` must not warn W1001: {:?}",
unused_import_warnings(&diag)
);
}
#[test]
fn genuinely_unused_import_still_warns() {
let import = named_import(10, "ReallyUnused");
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names(&module, &mut st);
let warns = unused_import_warnings(&diag);
assert!(
warns.iter().any(|w| w.contains("ReallyUnused")),
"a genuinely unused import must still warn W1001: {warns:?}"
);
}
#[test]
fn private_symbol_import_error_suggests_public() {
use crate::registry::{ExportDetail, ExportKind, ExportedSymbol, ModuleExports};
use crate::stubs::TypeRef;
let mut registry = ModuleRegistry::new();
let mut exports = ModuleExports::new("core", "core.bock");
exports.add_symbol(
"secret",
ExportedSymbol {
kind: ExportKind::Function,
visibility: Visibility::Private,
ty: TypeRef("Fn() -> Void".to_string()),
detail: ExportDetail::None,
},
);
registry.register(exports);
let import = named_import(10, "secret");
let module = simple_module(vec![import], vec![]);
let mut st = SymbolTable::new();
let diag = resolve_names_with_registry(&module, &mut st, ®istry);
let e1007: Vec<_> = diag
.iter()
.filter(|d| d.code.to_string() == "E1007")
.collect();
assert_eq!(e1007.len(), 1, "expected one E1007, got {}", e1007.len());
let d = e1007[0];
assert!(
d.message.contains("secret") && d.message.contains("private"),
"message should name the symbol and the rule: {:?}",
d.message
);
assert!(
d.notes
.iter()
.any(|n| n.contains("public") && n.contains("secret")),
"E1007 must suggest declaring the symbol `public`: {:?}",
d.notes
);
}
#[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);
}
}