Skip to main content

mimium_lang/compiler/mirgen/
convert_qualified_names.rs

1//! AST transformation pass for resolving qualified names.
2//!
3//! This pass converts `Expr::QualifiedVar` to `Expr::Var` with mangled names,
4//! and resolves `use` aliases and wildcard imports. It runs before type checking,
5//! allowing subsequent compiler phases to work with simple variable references.
6//!
7//! The resolution uses a 2-pass approach:
8//! 1. First pass: Collect all defined names from the AST (Let/LetRec bindings)
9//! 2. Second pass: Transform the AST, resolving qualified names, aliases, and wildcards
10//!
11//! Error handling:
12//! - Private member access: Reports an error but allows type checking to proceed
13//! - Unresolved names: Kept as-is for the type checker to report
14
15use std::collections::HashSet;
16use std::path::PathBuf;
17
18use thiserror::Error;
19
20use crate::ast::Expr;
21use crate::ast::program::{ModuleInfo, resolve_qualified_path};
22use crate::compiler::intrinsics;
23use crate::interner::{ExprNodeId, Symbol, ToSymbol};
24use crate::pattern::Pattern;
25use crate::utils::error::ReportableError;
26use crate::utils::metadata::Location;
27
28/// Error types specific to qualified name resolution.
29#[derive(Debug, Clone, Error)]
30#[error("Private member access")]
31pub enum Error {
32    /// Attempted to access a private module member
33    PrivateMemberAccess {
34        module_path: Vec<Symbol>,
35        member: Symbol,
36        location: Location,
37    },
38}
39
40impl ReportableError for Error {
41    fn get_message(&self) -> String {
42        match self {
43            Error::PrivateMemberAccess {
44                module_path,
45                member,
46                ..
47            } => {
48                let path_str = module_path
49                    .iter()
50                    .map(|s| s.to_string())
51                    .collect::<Vec<_>>()
52                    .join("::");
53                format!("Member \"{member}\" in module \"{path_str}\" is private")
54            }
55        }
56    }
57
58    fn get_labels(&self) -> Vec<(Location, String)> {
59        match self {
60            Error::PrivateMemberAccess { location, .. } => {
61                vec![(location.clone(), "private member accessed here".to_string())]
62            }
63        }
64    }
65}
66
67// ============================================================================
68// Pass 1: Collect all defined names from the AST
69// ============================================================================
70
71/// Collect all names defined in the AST (from Let and LetRec bindings).
72fn collect_defined_names(expr: ExprNodeId, names: &mut HashSet<Symbol>) {
73    match expr.to_expr() {
74        Expr::Let(typed_pat, body, then) => {
75            // Collect names from the pattern
76            collect_names_from_pattern(&typed_pat.pat, names);
77            collect_defined_names(body, names);
78            if let Some(t) = then {
79                collect_defined_names(t, names);
80            }
81        }
82        Expr::LetRec(typed_id, body, then) => {
83            names.insert(typed_id.id);
84            collect_defined_names(body, names);
85            if let Some(t) = then {
86                collect_defined_names(t, names);
87            }
88        }
89        Expr::Lambda(params, _, body) => {
90            for param in params {
91                names.insert(param.id);
92            }
93            collect_defined_names(body, names);
94        }
95        Expr::Tuple(v) => {
96            for e in v {
97                collect_defined_names(e, names);
98            }
99        }
100        Expr::Proj(e, _) => collect_defined_names(e, names),
101        Expr::Apply(fun, args) => {
102            collect_defined_names(fun, names);
103            for arg in args {
104                collect_defined_names(arg, names);
105            }
106        }
107        Expr::BinOp(lhs, _, rhs) => {
108            collect_defined_names(lhs, names);
109            collect_defined_names(rhs, names);
110        }
111        Expr::UniOp(_, e) => collect_defined_names(e, names),
112        Expr::MacroExpand(fun, args) => {
113            collect_defined_names(fun, names);
114            for arg in args {
115                collect_defined_names(arg, names);
116            }
117        }
118        Expr::If(cond, then, opt_else) => {
119            collect_defined_names(cond, names);
120            collect_defined_names(then, names);
121            if let Some(e) = opt_else {
122                collect_defined_names(e, names);
123            }
124        }
125        Expr::Block(body) => {
126            if let Some(b) = body {
127                collect_defined_names(b, names);
128            }
129        }
130        Expr::Escape(e) | Expr::Bracket(e) | Expr::Paren(e) | Expr::Feed(_, e) => {
131            collect_defined_names(e, names);
132        }
133        Expr::FieldAccess(record, _) => collect_defined_names(record, names),
134        Expr::ArrayAccess(array, index) => {
135            collect_defined_names(array, names);
136            collect_defined_names(index, names);
137        }
138        Expr::ArrayLiteral(elems) => {
139            for e in elems {
140                collect_defined_names(e, names);
141            }
142        }
143        Expr::RecordLiteral(fields) | Expr::ImcompleteRecord(fields) => {
144            for f in fields {
145                collect_defined_names(f.expr, names);
146            }
147        }
148        Expr::RecordUpdate(record, fields) => {
149            collect_defined_names(record, names);
150            for f in fields {
151                collect_defined_names(f.expr, names);
152            }
153        }
154        Expr::Assign(target, value) => {
155            collect_defined_names(target, names);
156            collect_defined_names(value, names);
157        }
158        Expr::Then(e, then) => {
159            collect_defined_names(e, names);
160            if let Some(t) = then {
161                collect_defined_names(t, names);
162            }
163        }
164        Expr::Match(scrutinee, arms) => {
165            collect_defined_names(scrutinee, names);
166            for arm in arms {
167                collect_defined_names(arm.body, names);
168            }
169        }
170        // Leaf nodes - no names to collect
171        Expr::Var(_) | Expr::QualifiedVar(_) | Expr::Literal(_) | Expr::Error => {}
172    }
173}
174
175/// Extract all names bound by a pattern.
176fn collect_names_from_pattern(pat: &Pattern, names: &mut HashSet<Symbol>) {
177    match pat {
178        Pattern::Single(name) => {
179            names.insert(*name);
180        }
181        Pattern::Tuple(pats) => {
182            for p in pats {
183                collect_names_from_pattern(p, names);
184            }
185        }
186        Pattern::Record(fields) => {
187            for (_, p) in fields {
188                collect_names_from_pattern(p, names);
189            }
190        }
191        Pattern::Placeholder | Pattern::Error => {}
192    }
193}
194
195// ============================================================================
196// Pass 2: Transform the AST
197// ============================================================================
198
199/// Context for qualified name resolution.
200struct ResolveContext<'a> {
201    /// Module information (visibility, aliases, wildcards)
202    module_info: &'a ModuleInfo,
203    /// Set of all known names (from AST definitions + builtins)
204    known_names: &'a HashSet<Symbol>,
205    /// Current module context for relative path resolution
206    current_module_context: Vec<Symbol>,
207    /// File path for error reporting
208    file_path: PathBuf,
209    /// Accumulated errors
210    errors: Vec<Error>,
211    /// Lexically scoped local bindings (innermost scope at the end)
212    local_bindings: Vec<HashSet<Symbol>>,
213}
214
215impl<'a> ResolveContext<'a> {
216    fn new(
217        module_info: &'a ModuleInfo,
218        known_names: &'a HashSet<Symbol>,
219        file_path: PathBuf,
220    ) -> Self {
221        Self {
222            module_info,
223            known_names,
224            current_module_context: Vec::new(),
225            file_path,
226            errors: Vec::new(),
227            local_bindings: Vec::new(),
228        }
229    }
230
231    /// Check if a name exists in the known names set.
232    fn name_exists(&self, name: &Symbol) -> bool {
233        self.known_names.contains(name)
234    }
235
236    fn push_scope(&mut self) {
237        self.local_bindings.push(HashSet::new());
238    }
239
240    fn pop_scope(&mut self) {
241        let _ = self.local_bindings.pop();
242    }
243
244    fn bind_local(&mut self, symbol: Symbol) {
245        if let Some(scope) = self.local_bindings.last_mut() {
246            scope.insert(symbol);
247        }
248    }
249
250    fn bind_pattern_locals(&mut self, pat: &Pattern) {
251        collect_names_from_pattern(
252            pat,
253            self.local_bindings.last_mut().expect("scope must exist"),
254        );
255    }
256
257    fn is_locally_bound(&self, name: Symbol) -> bool {
258        self.local_bindings
259            .iter()
260            .rev()
261            .any(|scope| scope.contains(&name))
262    }
263
264    /// Try to resolve a simple variable name through wildcard imports.
265    /// Returns the first matching mangled name if found.
266    fn resolve_through_wildcards(&self, name: Symbol) -> Option<Symbol> {
267        for base in &self.module_info.wildcard_imports {
268            let mangled = if base.as_str().is_empty() {
269                name
270            } else {
271                format!("{}${}", base.as_str(), name.as_str()).to_symbol()
272            };
273
274            if self.name_exists(&mangled) {
275                // Check visibility - only allow public members through wildcards
276                if let Some(&is_public) = self.module_info.visibility_map.get(&mangled) {
277                    if is_public {
278                        return Some(mangled);
279                    }
280                } else {
281                    // If not in visibility map, treat as accessible
282                    return Some(mangled);
283                }
284            }
285        }
286        None
287    }
288
289    /// Check if the resolved path is within the same module hierarchy as current context.
290    fn is_within_module_hierarchy(&self, resolved_path: &[Symbol]) -> bool {
291        if self.current_module_context.is_empty() || resolved_path.len() < 2 {
292            return false;
293        }
294        let target_module = &resolved_path[..resolved_path.len() - 1];
295        self.current_module_context.starts_with(target_module)
296    }
297
298    fn make_location(&self, e_id: ExprNodeId) -> Location {
299        Location {
300            span: e_id.to_span().clone(),
301            path: self.file_path.clone(),
302        }
303    }
304}
305
306/// Convert qualified names in an AST, returning the transformed AST and any errors.
307///
308/// This function performs the following transformations:
309/// 1. Converts `QualifiedVar` to `Var` with mangled names (e.g., `foo::bar` → `foo$bar`)
310/// 2. Resolves explicit `use` aliases for simple `Var` references
311/// 3. Resolves wildcard imports (`use foo::*`) by checking the collected environment
312/// 4. Resolves relative paths within modules
313/// 5. Reports errors for private member access (but allows type checking to proceed)
314///
315/// The `builtin_names` parameter should contain names from builtin functions and external functions.
316pub fn convert_qualified_names(
317    expr: ExprNodeId,
318    module_info: &ModuleInfo,
319    builtin_names: &[Symbol],
320    file_path: PathBuf,
321) -> (ExprNodeId, Vec<Error>) {
322    // Pass 1: Collect all defined names from the AST
323    let mut known_names: HashSet<Symbol> = builtin_names.iter().copied().collect();
324    collect_defined_names(expr, &mut known_names);
325
326    // Pass 2: Transform the AST
327    let mut ctx = ResolveContext::new(module_info, &known_names, file_path);
328    let result = convert_expr(&mut ctx, expr);
329    (result, ctx.errors)
330}
331
332fn convert_expr(ctx: &mut ResolveContext, e_id: ExprNodeId) -> ExprNodeId {
333    let loc = ctx.make_location(e_id);
334
335    match e_id.to_expr().clone() {
336        Expr::Var(name) => convert_var(ctx, name, loc),
337        Expr::QualifiedVar(path) => convert_qualified_var(ctx, &path.segments, loc),
338
339        // Update module context when entering a LetRec (function definition)
340        Expr::LetRec(id, body, then) => {
341            let name = id.id;
342            // Save current context
343            let prev_context = std::mem::take(&mut ctx.current_module_context);
344
345            ctx.push_scope();
346            ctx.bind_local(name);
347
348            // Update context if this function has a module context
349            if let Some(new_context) = ctx.module_info.module_context_map.get(&name) {
350                ctx.current_module_context = new_context.clone();
351            }
352
353            let new_body = convert_expr(ctx, body);
354
355            // Restore context
356            ctx.current_module_context = prev_context;
357
358            let new_then = then.map(|t| convert_expr(ctx, t));
359            ctx.pop_scope();
360            Expr::LetRec(id, new_body, new_then).into_id(loc)
361        }
362
363        // Recursively process other expression types
364        Expr::Tuple(v) => {
365            let new_v: Vec<_> = v.into_iter().map(|e| convert_expr(ctx, e)).collect();
366            Expr::Tuple(new_v).into_id(loc)
367        }
368        Expr::Proj(e, idx) => {
369            let new_e = convert_expr(ctx, e);
370            Expr::Proj(new_e, idx).into_id(loc)
371        }
372        Expr::Let(pat, body, then) => {
373            let prev_context = std::mem::take(&mut ctx.current_module_context);
374            if let Some(module_context) = find_pattern_module_context(ctx, &pat.pat) {
375                ctx.current_module_context = module_context;
376            } else {
377                ctx.current_module_context = prev_context.clone();
378            }
379
380            let new_body = convert_expr(ctx, body);
381            let new_then = then.map(|t| {
382                ctx.push_scope();
383                ctx.bind_pattern_locals(&pat.pat);
384                let converted = convert_expr(ctx, t);
385                ctx.pop_scope();
386                converted
387            });
388
389            ctx.current_module_context = prev_context;
390            Expr::Let(pat, new_body, new_then).into_id(loc)
391        }
392        Expr::Lambda(params, r_type, body) => {
393            ctx.push_scope();
394            for param in &params {
395                ctx.bind_local(param.id);
396            }
397            let new_body = convert_expr(ctx, body);
398            ctx.pop_scope();
399            Expr::Lambda(params, r_type, new_body).into_id(loc)
400        }
401        Expr::Apply(fun, args) => {
402            let new_fun = convert_expr(ctx, fun);
403            let new_args: Vec<_> = args.into_iter().map(|e| convert_expr(ctx, e)).collect();
404            Expr::Apply(new_fun, new_args).into_id(loc)
405        }
406        Expr::BinOp(lhs, op, rhs) => {
407            let new_lhs = convert_expr(ctx, lhs);
408            let new_rhs = convert_expr(ctx, rhs);
409            Expr::BinOp(new_lhs, op, new_rhs).into_id(loc)
410        }
411        Expr::UniOp(op, expr) => {
412            let new_expr = convert_expr(ctx, expr);
413            Expr::UniOp(op, new_expr).into_id(loc)
414        }
415        Expr::MacroExpand(fun, args) => {
416            let new_fun = convert_expr(ctx, fun);
417            let new_args: Vec<_> = args.into_iter().map(|e| convert_expr(ctx, e)).collect();
418            Expr::MacroExpand(new_fun, new_args).into_id(loc)
419        }
420        Expr::If(cond, then, opt_else) => {
421            let new_cond = convert_expr(ctx, cond);
422            let new_then = convert_expr(ctx, then);
423            let new_else = opt_else.map(|e| convert_expr(ctx, e));
424            Expr::If(new_cond, new_then, new_else).into_id(loc)
425        }
426        Expr::Block(body) => {
427            let new_body = body.map(|e| convert_expr(ctx, e));
428            Expr::Block(new_body).into_id(loc)
429        }
430        Expr::Escape(e) => {
431            let new_e = convert_expr(ctx, e);
432            Expr::Escape(new_e).into_id(loc)
433        }
434        Expr::Bracket(e) => {
435            let new_e = convert_expr(ctx, e);
436            Expr::Bracket(new_e).into_id(loc)
437        }
438        Expr::FieldAccess(record, field) => {
439            let new_record = convert_expr(ctx, record);
440            Expr::FieldAccess(new_record, field).into_id(loc)
441        }
442        Expr::ArrayAccess(array, index) => {
443            let new_array = convert_expr(ctx, array);
444            let new_index = convert_expr(ctx, index);
445            Expr::ArrayAccess(new_array, new_index).into_id(loc)
446        }
447        Expr::ArrayLiteral(elems) => {
448            let new_elems: Vec<_> = elems.into_iter().map(|e| convert_expr(ctx, e)).collect();
449            Expr::ArrayLiteral(new_elems).into_id(loc)
450        }
451        Expr::RecordLiteral(fields) => {
452            let new_fields = fields
453                .into_iter()
454                .map(|f| crate::ast::RecordField {
455                    name: f.name,
456                    expr: convert_expr(ctx, f.expr),
457                })
458                .collect();
459            Expr::RecordLiteral(new_fields).into_id(loc)
460        }
461        Expr::ImcompleteRecord(fields) => {
462            let new_fields = fields
463                .into_iter()
464                .map(|f| crate::ast::RecordField {
465                    name: f.name,
466                    expr: convert_expr(ctx, f.expr),
467                })
468                .collect();
469            Expr::ImcompleteRecord(new_fields).into_id(loc)
470        }
471        Expr::RecordUpdate(record, fields) => {
472            let new_record = convert_expr(ctx, record);
473            let new_fields = fields
474                .into_iter()
475                .map(|f| crate::ast::RecordField {
476                    name: f.name,
477                    expr: convert_expr(ctx, f.expr),
478                })
479                .collect();
480            Expr::RecordUpdate(new_record, new_fields).into_id(loc)
481        }
482        Expr::Assign(target, value) => {
483            let new_target = convert_expr(ctx, target);
484            let new_value = convert_expr(ctx, value);
485            Expr::Assign(new_target, new_value).into_id(loc)
486        }
487        Expr::Then(e, then) => {
488            let new_e = convert_expr(ctx, e);
489            let new_then = then.map(|t| convert_expr(ctx, t));
490            Expr::Then(new_e, new_then).into_id(loc)
491        }
492        Expr::Feed(sym, e) => {
493            let new_e = convert_expr(ctx, e);
494            Expr::Feed(sym, new_e).into_id(loc)
495        }
496        Expr::Paren(e) => {
497            // Unwrap parenthesized expressions
498            convert_expr(ctx, e)
499        }
500        Expr::Match(scrutinee, arms) => {
501            let new_scrutinee = convert_expr(ctx, scrutinee);
502            let new_arms = arms
503                .into_iter()
504                .map(|arm| {
505                    ctx.push_scope();
506                    bind_match_pattern_locals(ctx, &arm.pattern);
507                    let body = convert_expr(ctx, arm.body);
508                    ctx.pop_scope();
509                    crate::ast::MatchArm {
510                        pattern: arm.pattern,
511                        body,
512                    }
513                })
514                .collect();
515            Expr::Match(new_scrutinee, new_arms).into_id(loc)
516        }
517
518        // Leaf nodes that don't need transformation
519        Expr::Literal(_) | Expr::Error => e_id,
520    }
521}
522
523fn find_pattern_module_context(
524    ctx: &ResolveContext,
525    pat: &crate::pattern::Pattern,
526) -> Option<Vec<Symbol>> {
527    match pat {
528        Pattern::Single(name) => ctx.module_info.module_context_map.get(name).cloned(),
529        Pattern::Tuple(items) => items
530            .iter()
531            .find_map(|item| find_pattern_module_context(ctx, item)),
532        Pattern::Record(fields) => fields
533            .iter()
534            .find_map(|(_, item)| find_pattern_module_context(ctx, item)),
535        Pattern::Placeholder | Pattern::Error => None,
536    }
537}
538
539fn bind_match_pattern_locals(ctx: &mut ResolveContext, pat: &crate::ast::MatchPattern) {
540    use crate::ast::MatchPattern;
541    match pat {
542        MatchPattern::Variable(id) => {
543            ctx.bind_local(*id);
544        }
545        MatchPattern::Tuple(items) => {
546            items
547                .iter()
548                .for_each(|item| bind_match_pattern_locals(ctx, item));
549        }
550        MatchPattern::Constructor(_, inner) => {
551            inner
552                .as_ref()
553                .iter()
554                .for_each(|inner_pat| bind_match_pattern_locals(ctx, inner_pat));
555        }
556        MatchPattern::Wildcard | MatchPattern::Literal(_) => {}
557    }
558}
559
560fn resolve_alias_chain(module_info: &ModuleInfo, symbol: Symbol) -> Symbol {
561    let mut current = symbol;
562    let mut visited = HashSet::new();
563    while visited.insert(current) {
564        match module_info.use_alias_map.get(&current).copied() {
565            Some(next) if next != current => current = next,
566            _ => break,
567        }
568    }
569    current
570}
571
572fn is_core_intrinsic_name(name: Symbol) -> bool {
573    matches!(
574        name.as_str(),
575        intrinsics::NEG
576            | intrinsics::ADD
577            | intrinsics::SUB
578            | intrinsics::MULT
579            | intrinsics::DIV
580            | intrinsics::EQ
581            | intrinsics::NE
582            | intrinsics::LE
583            | intrinsics::LT
584            | intrinsics::GE
585            | intrinsics::GT
586            | intrinsics::MODULO
587            | intrinsics::POW
588            | intrinsics::AND
589            | intrinsics::OR
590            | intrinsics::TOFLOAT
591    )
592}
593
594fn is_operator_lowered_builtin_name(name: Symbol) -> bool {
595    is_core_intrinsic_name(name) || name.as_str() == "_mimium_schedule_at"
596}
597
598/// Convert a simple variable reference, resolving explicit `use` aliases and wildcards.
599fn convert_var(ctx: &mut ResolveContext, name: Symbol, loc: Location) -> ExprNodeId {
600    // Lexical local bindings must take precedence over imported aliases or wildcards.
601    if ctx.is_locally_bound(name) {
602        return Expr::Var(name).into_id(loc);
603    }
604
605    // Resolve within current module hierarchy first. Imported names must not shadow
606    // internal symbols defined in the current/parent module chain.
607    if !ctx.current_module_context.is_empty()
608        && let Some(relative_mangled) = (1..=ctx.current_module_context.len())
609            .rev()
610            .map(|prefix_len| {
611                let mut relative_path = ctx.current_module_context[..prefix_len].to_vec();
612                relative_path.push(name);
613                relative_path
614                    .iter()
615                    .map(|s| s.as_str())
616                    .collect::<Vec<_>>()
617                    .join("$")
618                    .to_symbol()
619            })
620            .find(|relative_mangled| ctx.name_exists(relative_mangled))
621    {
622        return Expr::Var(relative_mangled).into_id(loc);
623    }
624
625    // Check if this is a use alias (explicit `use foo::bar` or `use foo::bar as alias`)
626    if ctx.module_info.use_alias_map.contains_key(&name) {
627        let mangled_name = resolve_alias_chain(ctx.module_info, name);
628        // Check visibility
629        if let Some(&is_public) = ctx.module_info.visibility_map.get(&mangled_name)
630            && !is_public
631            && !ctx.is_within_module_hierarchy(&extract_path_from_mangled(mangled_name))
632        {
633            let parts: Vec<&str> = mangled_name.as_str().split('$').collect();
634            let module_path: Vec<Symbol> = parts[..parts.len() - 1]
635                .iter()
636                .map(|s| s.to_symbol())
637                .collect();
638            let member = parts.last().unwrap().to_symbol();
639            ctx.errors.push(Error::PrivateMemberAccess {
640                module_path,
641                member,
642                location: loc.clone(),
643            });
644            // Continue with the resolved name despite the error
645        }
646        return Expr::Var(mangled_name).into_id(loc);
647    }
648
649    // Try wildcard resolution
650    if let Some(mangled) = ctx.resolve_through_wildcards(name) {
651        return Expr::Var(mangled).into_id(loc);
652    }
653
654    // Keep as-is - will be resolved by type checker (local variable or error)
655    Expr::Var(name).into_id(loc)
656}
657
658/// Convert a qualified variable reference (e.g., `module::func`).
659fn convert_qualified_var(
660    ctx: &mut ResolveContext,
661    segments: &[Symbol],
662    loc: Location,
663) -> ExprNodeId {
664    if let [ns, name] = segments
665        && ns.as_str() == intrinsics::OP_INTRINSIC_MARKER_NS
666        && is_operator_lowered_builtin_name(*name)
667    {
668        return Expr::Var(*name).into_id(loc);
669    }
670
671    // Build mangled name for absolute path
672    let mangled_name = if segments.len() == 1 {
673        segments[0]
674    } else {
675        segments
676            .iter()
677            .map(|s| s.as_str())
678            .collect::<Vec<_>>()
679            .join("$")
680            .to_symbol()
681    };
682
683    // Try to resolve the path with relative fallback
684    let (resolved_name, resolved_path) = resolve_qualified_path(
685        segments,
686        mangled_name,
687        &ctx.current_module_context,
688        |name| ctx.name_exists(name),
689    );
690
691    // Check if it's a re-exported alias
692    let lookup_name = resolve_alias_chain(ctx.module_info, resolved_name);
693
694    // Check visibility for module members
695    if resolved_path.len() > 1
696        && let Some(&is_public) = ctx.module_info.visibility_map.get(&resolved_name)
697    {
698        let is_same_module = ctx.is_within_module_hierarchy(&resolved_path);
699        if !is_public && !is_same_module {
700            ctx.errors.push(Error::PrivateMemberAccess {
701                module_path: resolved_path[..resolved_path.len() - 1].to_vec(),
702                member: *resolved_path.last().unwrap(),
703                location: loc.clone(),
704            });
705            // Continue with the resolved name despite the error
706        }
707    }
708
709    Expr::Var(lookup_name).into_id(loc)
710}
711
712/// Extract path segments from a mangled name (e.g., "foo$bar$baz" -> ["foo", "bar", "baz"])
713fn extract_path_from_mangled(mangled: Symbol) -> Vec<Symbol> {
714    mangled.as_str().split('$').map(|s| s.to_symbol()).collect()
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use crate::ast::program::QualifiedPath;
721    use crate::pattern::TypedPattern;
722    use crate::types::Type;
723
724    fn make_loc() -> Location {
725        Location {
726            span: 0..1,
727            path: PathBuf::from("/test"),
728        }
729    }
730
731    #[test]
732    fn test_qualified_var_to_mangled() {
733        let loc = make_loc();
734        let mut module_info = ModuleInfo::default();
735        module_info
736            .visibility_map
737            .insert("foo$bar".to_symbol(), true);
738
739        let expr = Expr::QualifiedVar(QualifiedPath {
740            segments: vec!["foo".to_symbol(), "bar".to_symbol()],
741        })
742        .into_id(loc.clone());
743
744        // Add foo$bar to builtin_names so it's recognized
745        let builtin_names = vec!["foo$bar".to_symbol()];
746        let (result, errors) =
747            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
748
749        assert!(errors.is_empty());
750        assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "foo$bar"));
751    }
752
753    #[test]
754    fn test_private_member_access_reports_error_but_continues() {
755        let loc = make_loc();
756        let mut module_info = ModuleInfo::default();
757        module_info
758            .visibility_map
759            .insert("foo$secret".to_symbol(), false);
760
761        let expr = Expr::QualifiedVar(QualifiedPath {
762            segments: vec!["foo".to_symbol(), "secret".to_symbol()],
763        })
764        .into_id(loc.clone());
765
766        let builtin_names = vec!["foo$secret".to_symbol()];
767        let (result, errors) =
768            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
769
770        // Error should be reported
771        assert_eq!(errors.len(), 1);
772        assert!(matches!(&errors[0], Error::PrivateMemberAccess { .. }));
773        // But the expression should still be converted
774        assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "foo$secret"));
775    }
776
777    #[test]
778    fn test_use_alias_resolution() {
779        let loc = make_loc();
780        let mut module_info = ModuleInfo::default();
781        module_info
782            .use_alias_map
783            .insert("func".to_symbol(), "module$func".to_symbol());
784        module_info
785            .visibility_map
786            .insert("module$func".to_symbol(), true);
787
788        let expr = Expr::Var("func".to_symbol()).into_id(loc.clone());
789
790        let builtin_names = vec!["module$func".to_symbol()];
791        let (result, errors) =
792            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
793
794        assert!(errors.is_empty());
795        assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "module$func"));
796    }
797
798    #[test]
799    fn test_wildcard_resolution() {
800        let loc = make_loc();
801        let mut module_info = ModuleInfo::default();
802        module_info.wildcard_imports.push("mymod".to_symbol());
803        module_info
804            .visibility_map
805            .insert("mymod$helper".to_symbol(), true);
806
807        let expr = Expr::Var("helper".to_symbol()).into_id(loc.clone());
808
809        // mymod$helper is in the "environment" (builtin_names here for test)
810        let builtin_names = vec!["mymod$helper".to_symbol()];
811        let (result, errors) =
812            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
813
814        assert!(errors.is_empty());
815        // Now the variable SHOULD be resolved through wildcard
816        assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "mymod$helper"));
817    }
818
819    #[test]
820    fn test_local_binding_shadows_wildcard_import() {
821        let loc = make_loc();
822        let mut module_info = ModuleInfo::default();
823        module_info.wildcard_imports.push("core".to_symbol());
824        module_info
825            .visibility_map
826            .insert("core$mix".to_symbol(), true);
827
828        // let mix = 0.5; mix
829        let unknownty = Type::Unknown.into_id_with_location(loc.clone());
830        let expr = Expr::Let(
831            TypedPattern {
832                pat: Pattern::Single("mix".to_symbol()),
833                ty: unknownty,
834                default_value: None,
835            },
836            Expr::Literal(crate::ast::Literal::Float("0.5".to_symbol())).into_id(loc.clone()),
837            Some(Expr::Var("mix".to_symbol()).into_id(loc.clone())),
838        )
839        .into_id(loc.clone());
840
841        let builtin_names = vec!["core$mix".to_symbol()];
842        let (result, errors) =
843            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
844
845        assert!(errors.is_empty());
846        match result.to_expr() {
847            Expr::Let(_, _, Some(then)) => {
848                assert!(matches!(then.to_expr(), Expr::Var(name) if name.as_str() == "mix"));
849            }
850            _ => panic!("expected let expression with then branch"),
851        }
852    }
853
854    #[test]
855    fn test_parent_module_symbol_shadows_wildcard_import() {
856        let loc = make_loc();
857        let mut module_info = ModuleInfo::default();
858        module_info.wildcard_imports.push("filter".to_symbol());
859        module_info
860            .visibility_map
861            .insert("filter$allpass".to_symbol(), true);
862
863        let wet_name = "reverb$freeverb$freeverb_mono_wet".to_symbol();
864        module_info.module_context_map.insert(
865            wet_name,
866            vec![
867                "reverb".to_symbol(),
868                "freeverb".to_symbol(),
869                "freeverb_mono_wet".to_symbol(),
870            ],
871        );
872
873        let unknownty = Type::Unknown.into_id_with_location(loc.clone());
874        let expr = Expr::LetRec(
875            crate::pattern::TypedId::new(wet_name, unknownty),
876            Expr::Var("allpass".to_symbol()).into_id(loc.clone()),
877            None,
878        )
879        .into_id(loc.clone());
880
881        // Both names exist, but the relative parent module symbol must win.
882        let builtin_names = vec!["filter$allpass".to_symbol(), "reverb$allpass".to_symbol()];
883        let (result, errors) =
884            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
885
886        assert!(errors.is_empty());
887        match result.to_expr() {
888            Expr::LetRec(_, body, _) => {
889                assert!(
890                    matches!(body.to_expr(), Expr::Var(name) if name.as_str() == "reverb$allpass")
891                );
892            }
893            _ => panic!("expected letrec expression"),
894        }
895    }
896
897    #[test]
898    fn test_wildcard_not_resolved_when_name_not_exists() {
899        let loc = make_loc();
900        let mut module_info = ModuleInfo::default();
901        module_info.wildcard_imports.push("mymod".to_symbol());
902        // mymod$helper is NOT in visibility_map and NOT in known_names
903
904        let expr = Expr::Var("helper".to_symbol()).into_id(loc.clone());
905
906        let builtin_names = vec![]; // Empty - no names known
907        let (result, errors) =
908            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
909
910        assert!(errors.is_empty());
911        // The variable should NOT be resolved - remains as "helper"
912        assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "helper"));
913    }
914
915    #[test]
916    fn test_simple_var_unchanged() {
917        let loc = make_loc();
918        let module_info = ModuleInfo::default();
919
920        let expr = Expr::Var("local_var".to_symbol()).into_id(loc.clone());
921
922        let builtin_names = vec![];
923        let (result, errors) =
924            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
925
926        assert!(errors.is_empty());
927        assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "local_var"));
928    }
929
930    #[test]
931    fn test_names_collected_from_let() {
932        // Test that names defined in Let are collected and can be used
933        let loc = make_loc();
934        let module_info = ModuleInfo::default();
935
936        let unknownty = Type::Unknown.into_id_with_location(loc.clone());
937        // let x = 1; x
938        let expr = Expr::Let(
939            TypedPattern {
940                pat: Pattern::Single("x".to_symbol()),
941                ty: unknownty,
942                default_value: None,
943            },
944            Expr::Literal(crate::ast::Literal::Float("1.0".to_symbol())).into_id(loc.clone()),
945            Some(Expr::Var("x".to_symbol()).into_id(loc.clone())),
946        )
947        .into_id(loc.clone());
948
949        let builtin_names = vec![];
950        let (result, errors) =
951            convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
952
953        assert!(errors.is_empty());
954        // The structure should be preserved
955        assert!(matches!(result.to_expr(), Expr::Let(..)));
956    }
957}