1use 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#[derive(Debug, Clone, Error)]
30#[error("Private member access")]
31pub enum Error {
32 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
67fn 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_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 Expr::Var(_) | Expr::QualifiedVar(_) | Expr::Literal(_) | Expr::Error => {}
172 }
173}
174
175fn 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
195struct ResolveContext<'a> {
201 module_info: &'a ModuleInfo,
203 known_names: &'a HashSet<Symbol>,
205 current_module_context: Vec<Symbol>,
207 file_path: PathBuf,
209 errors: Vec<Error>,
211 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 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 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 if let Some(&is_public) = self.module_info.visibility_map.get(&mangled) {
277 if is_public {
278 return Some(mangled);
279 }
280 } else {
281 return Some(mangled);
283 }
284 }
285 }
286 None
287 }
288
289 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
306pub fn convert_qualified_names(
317 expr: ExprNodeId,
318 module_info: &ModuleInfo,
319 builtin_names: &[Symbol],
320 file_path: PathBuf,
321) -> (ExprNodeId, Vec<Error>) {
322 let mut known_names: HashSet<Symbol> = builtin_names.iter().copied().collect();
324 collect_defined_names(expr, &mut known_names);
325
326 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 Expr::LetRec(id, body, then) => {
341 let name = id.id;
342 let prev_context = std::mem::take(&mut ctx.current_module_context);
344
345 ctx.push_scope();
346 ctx.bind_local(name);
347
348 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 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 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 ¶ms {
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 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 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(¤t).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
598fn convert_var(ctx: &mut ResolveContext, name: Symbol, loc: Location) -> ExprNodeId {
600 if ctx.is_locally_bound(name) {
602 return Expr::Var(name).into_id(loc);
603 }
604
605 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 if ctx.module_info.use_alias_map.contains_key(&name) {
627 let mangled_name = resolve_alias_chain(ctx.module_info, name);
628 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 }
646 return Expr::Var(mangled_name).into_id(loc);
647 }
648
649 if let Some(mangled) = ctx.resolve_through_wildcards(name) {
651 return Expr::Var(mangled).into_id(loc);
652 }
653
654 Expr::Var(name).into_id(loc)
656}
657
658fn 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 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 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 let lookup_name = resolve_alias_chain(ctx.module_info, resolved_name);
693
694 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 }
707 }
708
709 Expr::Var(lookup_name).into_id(loc)
710}
711
712fn 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 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 assert_eq!(errors.len(), 1);
772 assert!(matches!(&errors[0], Error::PrivateMemberAccess { .. }));
773 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 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 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 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 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 let expr = Expr::Var("helper".to_symbol()).into_id(loc.clone());
905
906 let builtin_names = vec![]; let (result, errors) =
908 convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
909
910 assert!(errors.is_empty());
911 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 let loc = make_loc();
934 let module_info = ModuleInfo::default();
935
936 let unknownty = Type::Unknown.into_id_with_location(loc.clone());
937 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 assert!(matches!(result.to_expr(), Expr::Let(..)));
956 }
957}