Skip to main content

crisp_resolve/
resolve.rs

1use crate::error::ResolveError;
2use crate::module::{ModuleGraph, load_module_graph};
3use crate::prelude::prelude_symbols;
4use crate::stdlib::stdlib_symbols;
5use crate::symbols::{Symbol, SymbolKey, SymbolKind, Visibility, collect_module_symbols};
6use crate::warning::ResolveWarning;
7use crisp_ast::Span;
8use crisp_ast::expr::{Block, Expr, ExprKind, Stmt};
9use crisp_ast::ident::Ident;
10use crisp_ast::item::{Item, SourceFile, UseDecl};
11use crisp_ast::pat::{Pat, PatKind};
12use crisp_ast::ty::{Type, TypeBound, TypeKind};
13use crisp_manifest::{read_manifest, resolve_dependencies};
14use std::collections::{BTreeMap, HashMap, HashSet};
15use std::path::Path;
16
17#[derive(Debug, Clone)]
18pub struct ResolvedBinding {
19    pub local_name: String,
20    pub symbol: SymbolKey,
21}
22
23/// A Rust-crate import binding (spec §14.2), for later typeck/emit.
24///
25/// Primary surface: `use serde_json { from_str }`. Compat: `use rust.serde_json { … }`.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ResolvedRustImport {
28    pub crisp_module: String,
29    pub crate_name: String,
30    pub item: String,
31    pub local_name: String,
32}
33
34#[derive(Debug, Clone)]
35pub struct ResolvedModule {
36    pub module_path: String,
37    pub file: String,
38    pub imports: Vec<ResolvedBinding>,
39    pub scope: Vec<String>,
40}
41
42#[derive(Debug, Clone)]
43pub struct ResolvedCrate {
44    pub crate_root: String,
45    pub modules: Vec<ResolvedModule>,
46    pub symbol_count: usize,
47    pub rust_imports: Vec<ResolvedRustImport>,
48    pub warnings: Vec<ResolveWarning>,
49}
50
51pub struct Resolver {
52    graph: ModuleGraph,
53    global: BTreeMap<SymbolKey, Symbol>,
54    /// Crate names from `crisp.toml` with `rust = true` (plus auto tokio when applicable).
55    rust_deps: HashSet<String>,
56    /// Deps present but not marked `rust = true` (for E0045).
57    unmarked_deps: HashSet<String>,
58    rust_imports: Vec<ResolvedRustImport>,
59    warnings: Vec<ResolveWarning>,
60}
61
62impl Resolver {
63    pub fn resolve_crate(crate_root: &Path) -> Result<ResolvedCrate, ResolveError> {
64        let graph = load_module_graph(crate_root)?;
65        let (rust_deps, unmarked_deps) = load_dep_sets(crate_root)?;
66        let mut resolver = Self::new(graph, rust_deps, unmarked_deps)?;
67        resolver.run()
68    }
69
70    fn new(
71        graph: ModuleGraph,
72        rust_deps: HashSet<String>,
73        unmarked_deps: HashSet<String>,
74    ) -> Result<Self, ResolveError> {
75        let mut global = BTreeMap::new();
76        for sym in prelude_symbols().into_iter().chain(stdlib_symbols()) {
77            global.insert(sym.key.clone(), sym);
78        }
79        for (module_path, node) in &graph.modules {
80            for sym in collect_module_symbols(module_path, &node.ast.items) {
81                if let Some(prev) = global.get(&sym.key) {
82                    return Err(ResolveError::DuplicateDef {
83                        name: sym.key.name.clone(),
84                        module: sym.key.module.clone(),
85                        span: sym.span.merge(prev.span),
86                    });
87                }
88                global.insert(sym.key.clone(), sym);
89            }
90        }
91        Ok(Self {
92            graph,
93            global,
94            rust_deps,
95            unmarked_deps,
96            rust_imports: Vec::new(),
97            warnings: Vec::new(),
98        })
99    }
100
101    fn run(&mut self) -> Result<ResolvedCrate, ResolveError> {
102        let mut resolved_modules = Vec::new();
103        for (module_path, node) in &self.graph.modules.clone() {
104            let imports = self.resolve_module_imports(module_path, &node.ast)?;
105            let scope: Vec<String> = imports.iter().map(|b| b.local_name.clone()).collect();
106            self.check_module_references(module_path, &node.ast, &imports)?;
107            resolved_modules.push(ResolvedModule {
108                module_path: module_path.clone(),
109                file: node.path.display().to_string(),
110                imports,
111                scope,
112            });
113        }
114        Ok(ResolvedCrate {
115            crate_root: self.graph.crate_root.display().to_string(),
116            modules: resolved_modules,
117            symbol_count: self.global.len(),
118            rust_imports: self.rust_imports.clone(),
119            warnings: self.warnings.clone(),
120        })
121    }
122
123    fn resolve_module_imports(
124        &mut self,
125        current: &str,
126        file: &SourceFile,
127    ) -> Result<Vec<ResolvedBinding>, ResolveError> {
128        let mut scope: HashMap<String, SymbolKey> = HashMap::new();
129        let mut bindings = Vec::new();
130
131        for sym in prelude_symbols().into_iter().chain(stdlib_symbols()) {
132            scope.insert(sym.key.name.clone(), sym.key.clone());
133            bindings.push(ResolvedBinding {
134                local_name: sym.key.name.clone(),
135                symbol: sym.key.clone(),
136            });
137        }
138
139        for sym in collect_module_symbols(current, &file.items) {
140            scope.insert(sym.key.name.clone(), sym.key.clone());
141            if !bindings.iter().any(|b| b.local_name == sym.key.name) {
142                bindings.push(ResolvedBinding {
143                    local_name: sym.key.name.clone(),
144                    symbol: sym.key.clone(),
145                });
146            }
147        }
148
149        for item in &file.items {
150            let Item::Use(use_decl) = item else {
151                continue;
152            };
153            self.apply_use(current, use_decl, &mut scope, &mut bindings)?;
154        }
155
156        Ok(bindings)
157    }
158
159    fn apply_use(
160        &mut self,
161        current: &str,
162        decl: &UseDecl,
163        scope: &mut HashMap<String, SymbolKey>,
164        bindings: &mut Vec<ResolvedBinding>,
165    ) -> Result<(), ResolveError> {
166        // Compat alias: `use rust.<crate> { … }` / `use rust::<crate> { … }`.
167        if decl.path.first().is_some_and(|p| p.name == "rust") {
168            let path_str = decl
169                .path
170                .iter()
171                .map(|p| p.name.as_str())
172                .collect::<Vec<_>>()
173                .join(".");
174            if decl.path.len() != 2 {
175                return Err(ResolveError::RustUsePathInvalid {
176                    path: path_str,
177                    span: decl.span,
178                });
179            }
180            let crate_name = decl.path[1].name.clone();
181            return self.bind_rust_crate(current, &crate_name, decl, scope, bindings);
182        }
183
184        // Crisp module wins when present (TS-like bare crate path otherwise).
185        if let Some(target_module) = self.lookup_crisp_module(current, &decl.path) {
186            let joined = decl
187                .path
188                .iter()
189                .map(|p| p.name.as_str())
190                .collect::<Vec<_>>()
191                .join(".");
192            if self.rust_deps.contains(&joined) {
193                self.warnings.push(ResolveWarning::ModuleShadowsRustDep {
194                    name: joined.clone(),
195                    span: decl.span,
196                });
197            }
198            return self.bind_crisp_use(target_module, decl, scope, bindings);
199        }
200
201        // Bare `use serde_json { … }` when it is (or claims to be) a Cargo dependency.
202        if decl.path.len() == 1 {
203            let crate_name = decl.path[0].name.clone();
204            if self.rust_deps.contains(&crate_name) || self.unmarked_deps.contains(&crate_name) {
205                return self.bind_rust_crate(current, &crate_name, decl, scope, bindings);
206            }
207        }
208
209        Err(ResolveError::ModuleNotFound {
210            path: decl
211                .path
212                .iter()
213                .map(|p| p.name.as_str())
214                .collect::<Vec<_>>()
215                .join("."),
216        })
217    }
218
219    fn bind_crisp_use(
220        &self,
221        target_module: String,
222        decl: &UseDecl,
223        scope: &mut HashMap<String, SymbolKey>,
224        bindings: &mut Vec<ResolvedBinding>,
225    ) -> Result<(), ResolveError> {
226        let span = decl.span;
227        if let Some(imports) = &decl.imports {
228            for imp in imports {
229                let sym = self.lookup_export(&target_module, &imp.name.name)?;
230                let local = imp
231                    .alias
232                    .as_ref()
233                    .map(|a| a.name.clone())
234                    .unwrap_or_else(|| imp.name.name.clone());
235                self.insert_binding(&local, sym.key.clone(), scope, bindings, span)?;
236            }
237        } else {
238            let exported: Vec<_> = self
239                .global
240                .values()
241                .filter(|s| s.key.module == target_module && s.is_exported() && !s.from_prelude)
242                .cloned()
243                .collect();
244            for sym in exported {
245                self.insert_binding(&sym.key.name, sym.key.clone(), scope, bindings, span)?;
246            }
247        }
248        let _ = decl.is_pub;
249        Ok(())
250    }
251
252    fn bind_rust_crate(
253        &mut self,
254        current: &str,
255        crate_name: &str,
256        decl: &UseDecl,
257        scope: &mut HashMap<String, SymbolKey>,
258        bindings: &mut Vec<ResolvedBinding>,
259    ) -> Result<(), ResolveError> {
260        if self.unmarked_deps.contains(crate_name) && !self.rust_deps.contains(crate_name) {
261            return Err(ResolveError::RustCrateNotMarked {
262                name: crate_name.to_string(),
263                span: decl.span,
264            });
265        }
266        if !self.rust_deps.contains(crate_name) {
267            return Err(ResolveError::RustCrateNotFound {
268                name: crate_name.to_string(),
269                span: decl.span,
270            });
271        }
272        let Some(imports) = &decl.imports else {
273            return Err(ResolveError::RustImportNeedsList {
274                name: crate_name.to_string(),
275                span: decl.span,
276            });
277        };
278
279        let module = format!("rust.{crate_name}");
280        for imp in imports {
281            let key = SymbolKey {
282                module: module.clone(),
283                name: imp.name.name.clone(),
284            };
285            self.global.entry(key.clone()).or_insert_with(|| Symbol {
286                key: key.clone(),
287                kind: SymbolKind::RustFn,
288                visibility: Visibility::Public,
289                span: imp.span,
290                from_prelude: false,
291            });
292            let local = imp
293                .alias
294                .as_ref()
295                .map(|a| a.name.clone())
296                .unwrap_or_else(|| imp.name.name.clone());
297            self.insert_binding(&local, key, scope, bindings, decl.span)?;
298            self.rust_imports.push(ResolvedRustImport {
299                crisp_module: current.to_string(),
300                crate_name: crate_name.to_string(),
301                item: imp.name.name.clone(),
302                local_name: local,
303            });
304        }
305        Ok(())
306    }
307
308    /// Crisp module path for a `use`, if one exists (does not consider Rust deps).
309    fn lookup_crisp_module(&self, current: &str, path: &[Ident]) -> Option<String> {
310        self.resolve_use_path(current, path).ok()
311    }
312
313    fn insert_binding(
314        &self,
315        local: &str,
316        key: SymbolKey,
317        scope: &mut HashMap<String, SymbolKey>,
318        bindings: &mut Vec<ResolvedBinding>,
319        span: Span,
320    ) -> Result<(), ResolveError> {
321        if let Some(prev) = scope.get(local) {
322            if prev != &key {
323                return Err(ResolveError::AmbiguousImport {
324                    name: local.to_string(),
325                    span,
326                });
327            }
328            return Ok(());
329        }
330        scope.insert(local.to_string(), key.clone());
331        bindings.push(ResolvedBinding {
332            local_name: local.to_string(),
333            symbol: key,
334        });
335        Ok(())
336    }
337
338    fn resolve_use_path(&self, current: &str, path: &[Ident]) -> Result<String, ResolveError> {
339        if path.is_empty() {
340            return Err(ResolveError::ModuleNotFound {
341                path: "(empty)".to_string(),
342            });
343        }
344        let joined = path
345            .iter()
346            .map(|p| p.name.as_str())
347            .collect::<Vec<_>>()
348            .join(".");
349        if path.first().map(|p| p.name.as_str()) == Some("std") {
350            return Ok(joined);
351        }
352        if self.graph.modules.contains_key(&joined) {
353            return Ok(joined);
354        }
355        // sibling import: `use config` from `main` -> `config`
356        if self.graph.modules.contains_key(&joined) {
357            return Ok(joined);
358        }
359        // relative to current module's directory prefix
360        let current_dir = current.rsplit_once('.').map(|(d, _)| d).unwrap_or("");
361        let candidate = if current_dir.is_empty() {
362            joined.clone()
363        } else {
364            format!("{current_dir}.{joined}")
365        };
366        if self.graph.modules.contains_key(&candidate) {
367            return Ok(candidate);
368        }
369        Err(ResolveError::ModuleNotFound { path: joined })
370    }
371
372    fn lookup_export(&self, module: &str, name: &str) -> Result<&Symbol, ResolveError> {
373        let key = SymbolKey {
374            module: module.to_string(),
375            name: name.to_string(),
376        };
377        let sym = self
378            .global
379            .get(&key)
380            .ok_or_else(|| ResolveError::NotExported {
381                name: name.to_string(),
382                module: module.to_string(),
383                span: Span::default(),
384            })?;
385        if !sym.is_exported() {
386            return Err(ResolveError::PrivateImport {
387                name: name.to_string(),
388                module: module.to_string(),
389                span: sym.span,
390            });
391        }
392        Ok(sym)
393    }
394
395    fn check_module_references(
396        &self,
397        current: &str,
398        file: &SourceFile,
399        imports: &[ResolvedBinding],
400    ) -> Result<(), ResolveError> {
401        let scope: HashMap<String, SymbolKey> = imports
402            .iter()
403            .map(|b| (b.local_name.clone(), b.symbol.clone()))
404            .collect();
405        for item in &file.items {
406            match item {
407                Item::Function(f) => {
408                    let mut local = scope_with_generics(&scope, &f.generics);
409                    for p in &f.params {
410                        local.insert(
411                            p.name.name.clone(),
412                            SymbolKey {
413                                module: "_param".to_string(),
414                                name: p.name.name.clone(),
415                            },
416                        );
417                        if let Some(ty) = &p.ty {
418                            self.check_type(&local, ty)?;
419                        }
420                    }
421                    if let Some(ty) = &f.ret_type {
422                        self.check_type(&local, ty)?;
423                    }
424                    self.check_expr(&local, &f.body)?;
425                }
426                Item::TypeDef(t) => {
427                    let local = scope_with_generics(&scope, &t.generics);
428                    self.check_type_def(&local, t)?;
429                }
430                Item::Const(c) => self.check_expr(&scope, &c.value)?,
431                Item::Test(t) => self.check_block(&scope, &t.body)?,
432                Item::TestCompileFail(_) => {}
433                Item::Impl(i) => {
434                    if let Some(tn) = &i.trait_name {
435                        self.check_name(&scope, &tn.name, tn.span)?;
436                    }
437                    for arg in &i.trait_args {
438                        self.check_type(&scope, arg)?;
439                    }
440                    self.check_type(&scope, &i.ty)?;
441                    for f in &i.items {
442                        let mut local = scope_with_generics(&scope, &f.generics);
443                        for p in &f.params {
444                            local.insert(
445                                p.name.name.clone(),
446                                SymbolKey {
447                                    module: "_param".to_string(),
448                                    name: p.name.name.clone(),
449                                },
450                            );
451                            if let Some(ty) = &p.ty {
452                                self.check_type(&local, ty)?;
453                            }
454                        }
455                        if let Some(ty) = &f.ret_type {
456                            self.check_type(&local, ty)?;
457                        }
458                        self.check_expr(&local, &f.body)?;
459                    }
460                }
461                Item::TraitDef(t) => {
462                    let trait_scope = scope_with_generics(&scope, &t.generics);
463                    for item in &t.items {
464                        let mut local = trait_scope.clone();
465                        for p in &item.params {
466                            local.insert(
467                                p.name.name.clone(),
468                                SymbolKey {
469                                    module: "_param".to_string(),
470                                    name: p.name.name.clone(),
471                                },
472                            );
473                            if let Some(ty) = &p.ty {
474                                self.check_type(&local, ty)?;
475                            }
476                        }
477                        if let Some(ty) = &item.ret_type {
478                            self.check_type(&local, ty)?;
479                        }
480                        if let Some(body) = &item.default_body {
481                            self.check_expr(&local, body)?;
482                        }
483                    }
484                }
485                Item::ShapeDef(s) => {
486                    let local = scope_with_generics(&scope, &s.generics);
487                    for f in &s.fields {
488                        match f {
489                            crisp_ast::item::ShapeField::Data { ty, .. } => {
490                                self.check_type(&local, ty)?;
491                            }
492                            crisp_ast::item::ShapeField::Method {
493                                params, ret_type, ..
494                            } => {
495                                for p in params {
496                                    if let Some(ty) = &p.ty {
497                                        self.check_type(&local, ty)?;
498                                    }
499                                }
500                                self.check_type(&local, ret_type)?;
501                            }
502                        }
503                    }
504                }
505                Item::Use(_) | Item::Extern(_) => {}
506            }
507        }
508        let _ = current;
509        Ok(())
510    }
511
512    fn check_type_def(
513        &self,
514        scope: &HashMap<String, SymbolKey>,
515        t: &crisp_ast::item::TypeDef,
516    ) -> Result<(), ResolveError> {
517        use crisp_ast::item::TypeBody;
518        match &t.body {
519            TypeBody::Struct(fields) => {
520                for f in fields {
521                    self.check_type(scope, &f.ty)?;
522                    if let Some(def) = &f.default {
523                        self.check_expr(scope, def)?;
524                    }
525                }
526            }
527            TypeBody::Enum(variants) => {
528                for v in variants {
529                    for ty in &v.fields {
530                        self.check_type(scope, ty)?;
531                    }
532                }
533            }
534            TypeBody::Alias(ty) => self.check_type(scope, ty)?,
535        }
536        Ok(())
537    }
538
539    fn check_type(
540        &self,
541        scope: &HashMap<String, SymbolKey>,
542        ty: &Type,
543    ) -> Result<(), ResolveError> {
544        match &ty.kind {
545            TypeKind::Named(id) => {
546                self.check_name(scope, &id.name, id.span)?;
547                self.reject_shape_type(scope, &id.name, id.span)
548            }
549            TypeKind::Option(inner) | TypeKind::Slice(inner) | TypeKind::Ref { inner, .. } => {
550                self.check_type(scope, inner)
551            }
552            TypeKind::Tuple(types) => {
553                for t in types {
554                    self.check_type(scope, t)?;
555                }
556                Ok(())
557            }
558            TypeKind::Array { elem, .. } => self.check_type(scope, elem),
559            TypeKind::Fn { params, ret } => {
560                for p in params {
561                    self.check_type(scope, p)?;
562                }
563                self.check_type(scope, ret)
564            }
565            TypeKind::Constrained { inner, bounds } => {
566                for b in bounds {
567                    match b {
568                        TypeBound::Shape(id) => {
569                            self.check_name(scope, &id.name, id.span)?;
570                            // Ensure the name is a shape (not a random type).
571                            if let Some(key) = scope.get(&id.name)
572                                && let Some(sym) = self.global.get(key)
573                                && sym.kind != SymbolKind::Shape
574                            {
575                                return Err(ResolveError::UnresolvedName {
576                                    name: id.name.clone(),
577                                    span: id.span,
578                                    message: format!(
579                                        "[E0035] `{name}` is not a shape",
580                                        name = id.name
581                                    ),
582                                    hint: Some("shape bounds require a `shape` definition".into()),
583                                });
584                            }
585                        }
586                        TypeBound::Trait(id) => {
587                            self.check_name(scope, &id.name, id.span)?;
588                        }
589                    }
590                }
591                self.check_type(scope, inner)
592            }
593            TypeKind::Never | TypeKind::Unit => Ok(()),
594            TypeKind::Generic { base, args } => {
595                self.check_type(scope, base)?;
596                for a in args {
597                    self.check_type(scope, a)?;
598                }
599                Ok(())
600            }
601        }
602    }
603
604    fn check_block(
605        &self,
606        scope: &HashMap<String, SymbolKey>,
607        block: &Block,
608    ) -> Result<(), ResolveError> {
609        let mut local = scope.clone();
610        for stmt in &block.stmts {
611            match stmt {
612                Stmt::Bind { pat, value, .. } => {
613                    self.check_expr(&local, value)?;
614                    self.bind_pat(&mut local, pat)?;
615                }
616                Stmt::Assign { target, value } => {
617                    self.check_name(&local, &target.name, target.span)?;
618                    self.check_expr(&local, value)?;
619                }
620                Stmt::Expr(e) => self.check_expr(&local, e)?,
621            }
622        }
623        if let Some(tail) = &block.tail {
624            self.check_expr(&local, tail)?;
625        }
626        Ok(())
627    }
628
629    fn bind_pat(
630        &self,
631        scope: &mut HashMap<String, SymbolKey>,
632        pat: &Pat,
633    ) -> Result<(), ResolveError> {
634        match &pat.kind {
635            PatKind::Ident(id) => {
636                scope.insert(
637                    id.name.clone(),
638                    SymbolKey {
639                        module: "_local".to_string(),
640                        name: id.name.clone(),
641                    },
642                );
643            }
644            PatKind::Wildcard => {}
645            PatKind::Tuple(pats) => {
646                for p in pats {
647                    self.bind_pat(scope, p)?;
648                }
649            }
650            PatKind::Slice { prefix, rest } => {
651                for p in prefix {
652                    self.bind_pat(scope, p)?;
653                }
654                if let Some(id) = rest {
655                    scope.insert(
656                        id.name.clone(),
657                        SymbolKey {
658                            module: "_local".to_string(),
659                            name: id.name.clone(),
660                        },
661                    );
662                }
663            }
664            PatKind::Struct { fields, .. } => {
665                for f in fields {
666                    if let Some(p) = &f.pat {
667                        self.bind_pat(scope, p)?;
668                    }
669                }
670            }
671            PatKind::Enum { args, .. } => {
672                for p in args {
673                    self.bind_pat(scope, p)?;
674                }
675            }
676            PatKind::Literal(_) => {}
677            PatKind::Type { inner, .. } => self.bind_pat(scope, inner)?,
678        }
679        Ok(())
680    }
681
682    fn check_expr(
683        &self,
684        scope: &HashMap<String, SymbolKey>,
685        expr: &Expr,
686    ) -> Result<(), ResolveError> {
687        match &expr.kind {
688            ExprKind::Ident(id) if crisp_ast::is_hole_ident(&id.name) => Ok(()),
689            ExprKind::Ident(id) => self.check_name(scope, &id.name, id.span),
690            ExprKind::Block(b) => self.check_block(scope, b),
691            ExprKind::If {
692                cond,
693                then_branch,
694                else_branch,
695            } => {
696                self.check_expr(scope, cond)?;
697                self.check_expr(scope, then_branch)?;
698                if let Some(e) = else_branch {
699                    self.check_expr(scope, e)?;
700                }
701                Ok(())
702            }
703            ExprKind::Match { scrutinee, arms } => {
704                self.check_expr(scope, scrutinee)?;
705                let mut local = scope.clone();
706                for arm in arms {
707                    self.bind_pat(&mut local, &arm.pat)?;
708                    if let Some(g) = &arm.guard {
709                        self.check_expr(&local, g)?;
710                    }
711                    self.check_expr(&local, &arm.body)?;
712                }
713                Ok(())
714            }
715            ExprKind::For { pat, iter, body } => {
716                self.check_expr(scope, iter)?;
717                let mut local = scope.clone();
718                self.bind_pat(&mut local, pat)?;
719                self.check_expr(&local, body)
720            }
721            ExprKind::While { cond, body } => {
722                self.check_expr(scope, cond)?;
723                self.check_expr(scope, body)
724            }
725            ExprKind::Loop(body)
726            | ExprKind::Async(body)
727            | ExprKind::Await(body)
728            | ExprKind::Spawn(body)
729            | ExprKind::Unsafe(body)
730            | ExprKind::Try(body) => self.check_expr(scope, body),
731            ExprKind::Break(Some(v)) => self.check_expr(scope, v),
732            ExprKind::Lambda { params, body } => {
733                let mut local = scope.clone();
734                for p in params {
735                    local.insert(
736                        p.name.name.clone(),
737                        SymbolKey {
738                            module: "_local".to_string(),
739                            name: p.name.name.clone(),
740                        },
741                    );
742                    if let Some(ty) = &p.ty {
743                        self.check_type(&local, ty)?;
744                    }
745                }
746                self.check_expr(&local, body)
747            }
748            ExprKind::Call { func, args } => {
749                self.check_expr(scope, func)?;
750                for a in args {
751                    self.check_expr(scope, a)?;
752                }
753                Ok(())
754            }
755            ExprKind::MethodCall { receiver, args, .. } => {
756                self.check_expr(scope, receiver)?;
757                for a in args {
758                    self.check_expr(scope, a)?;
759                }
760                Ok(())
761            }
762            ExprKind::Field { base, .. } => self.check_expr(scope, base),
763            ExprKind::Index { base, index } => {
764                self.check_expr(scope, base)?;
765                self.check_expr(scope, index)
766            }
767            ExprKind::Unary { expr, .. } | ExprKind::Throw(expr) | ExprKind::Return(Some(expr)) => {
768                self.check_expr(scope, expr)
769            }
770            ExprKind::Binary { left, right, .. } | ExprKind::Pipe { left, right, .. } => {
771                self.check_expr(scope, left)?;
772                self.check_expr(scope, right)
773            }
774            ExprKind::Assign { target, value } => {
775                self.check_name(scope, &target.name, target.span)?;
776                self.check_expr(scope, value)
777            }
778            ExprKind::Bind { pat, value, .. } => {
779                self.check_expr(scope, value)?;
780                let mut local = scope.clone();
781                self.bind_pat(&mut local, pat)
782            }
783            ExprKind::StructLit { name, fields } => {
784                self.check_name(scope, &name.name, name.span)?;
785                for f in fields {
786                    self.check_expr(scope, &f.value)?;
787                }
788                Ok(())
789            }
790            ExprKind::Str(parts) => {
791                for part in &parts.0 {
792                    if let crisp_ast::expr::StringPart::Expr(e) = part {
793                        self.check_expr(scope, e)?;
794                    }
795                }
796                Ok(())
797            }
798            ExprKind::Catch { body, arms } => {
799                self.check_expr(scope, body)?;
800                let mut local = scope.clone();
801                for arm in arms {
802                    self.bind_pat(&mut local, &arm.pat)?;
803                    self.check_expr(&local, &arm.body)?;
804                }
805                Ok(())
806            }
807            ExprKind::Int(_)
808            | ExprKind::Float(_)
809            | ExprKind::Bool(_)
810            | ExprKind::Char(_)
811            | ExprKind::Unit
812            | ExprKind::Break(None)
813            | ExprKind::Continue
814            | ExprKind::Return(None) => Ok(()),
815        }
816    }
817
818    fn check_name(
819        &self,
820        scope: &HashMap<String, SymbolKey>,
821        name: &str,
822        span: Span,
823    ) -> Result<(), ResolveError> {
824        if scope.contains_key(name) {
825            return Ok(());
826        }
827        if name == "Self" || name.starts_with('_') {
828            return Ok(());
829        }
830        let hint = self.unresolved_hint(scope, name);
831        let message = match &hint {
832            Some(h) => format!("[E0035] unresolved name `{name}`\nhelp: {h}"),
833            None => format!("[E0035] unresolved name `{name}`"),
834        };
835        Err(ResolveError::UnresolvedName {
836            name: name.to_string(),
837            span,
838            message,
839            hint,
840        })
841    }
842
843    fn unresolved_hint(&self, scope: &HashMap<String, SymbolKey>, name: &str) -> Option<String> {
844        let _ = scope;
845        let mut modules: Vec<&str> = self
846            .global
847            .values()
848            .filter(|s| s.key.name == name && !s.from_prelude)
849            .map(|s| s.key.module.as_str())
850            .collect();
851        modules.sort_unstable();
852        modules.dedup();
853        if modules.is_empty() {
854            return None;
855        }
856        let module = modules[0];
857        Some(format!(
858            "`{name}` is defined in module `{module}`; add `use {module} {{ {name} }}` \
859(sibling modules are not visible by filename order alone)"
860        ))
861    }
862
863    fn reject_shape_type(
864        &self,
865        _scope: &HashMap<String, SymbolKey>,
866        _name: &str,
867        _span: Span,
868    ) -> Result<(), ResolveError> {
869        // Named shapes are supported as types (v1.5 / #61).
870        Ok(())
871    }
872}
873
874fn scope_with_generics(
875    scope: &HashMap<String, SymbolKey>,
876    generics: &[Ident],
877) -> HashMap<String, SymbolKey> {
878    let mut local = scope.clone();
879    for g in generics {
880        local.insert(
881            g.name.clone(),
882            SymbolKey {
883                module: "_generic".to_string(),
884                name: g.name.clone(),
885            },
886        );
887    }
888    local
889}
890
891fn load_dep_sets(crate_root: &Path) -> Result<(HashSet<String>, HashSet<String>), ResolveError> {
892    let manifest = read_manifest(crate_root).map_err(|e| ResolveError::Manifest {
893        root: crate_root.display().to_string(),
894        message: e.to_string(),
895    })?;
896    let deps = resolve_dependencies(&manifest);
897    let mut rust_deps = HashSet::new();
898    let mut unmarked_deps = HashSet::new();
899    for dep in deps {
900        if dep.rust {
901            rust_deps.insert(dep.name);
902        } else {
903            unmarked_deps.insert(dep.name);
904        }
905    }
906    for (name, spec) in &manifest.dependencies {
907        use crisp_manifest::DependencySpec;
908        match spec {
909            DependencySpec::Version(_) => {
910                unmarked_deps.insert(name.clone());
911            }
912            DependencySpec::Detailed { rust, .. } if !*rust => {
913                unmarked_deps.insert(name.clone());
914            }
915            _ => {}
916        }
917    }
918    Ok((rust_deps, unmarked_deps))
919}