use std::collections::HashSet;
use std::path::PathBuf;
use thiserror::Error;
use crate::ast::Expr;
use crate::ast::program::{ModuleInfo, resolve_qualified_path};
use crate::compiler::intrinsics;
use crate::interner::{ExprNodeId, Symbol, ToSymbol};
use crate::pattern::Pattern;
use crate::utils::error::ReportableError;
use crate::utils::metadata::Location;
#[derive(Debug, Clone, Error)]
#[error("Private member access")]
pub enum Error {
PrivateMemberAccess {
module_path: Vec<Symbol>,
member: Symbol,
location: Location,
},
}
impl ReportableError for Error {
fn get_message(&self) -> String {
match self {
Error::PrivateMemberAccess {
module_path,
member,
..
} => {
let path_str = module_path
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join("::");
format!("Member \"{member}\" in module \"{path_str}\" is private")
}
}
}
fn get_labels(&self) -> Vec<(Location, String)> {
match self {
Error::PrivateMemberAccess { location, .. } => {
vec![(location.clone(), "private member accessed here".to_string())]
}
}
}
}
fn collect_defined_names(expr: ExprNodeId, names: &mut HashSet<Symbol>) {
match expr.to_expr() {
Expr::Let(typed_pat, body, then) => {
collect_names_from_pattern(&typed_pat.pat, names);
collect_defined_names(body, names);
if let Some(t) = then {
collect_defined_names(t, names);
}
}
Expr::LetRec(typed_id, body, then) => {
names.insert(typed_id.id);
collect_defined_names(body, names);
if let Some(t) = then {
collect_defined_names(t, names);
}
}
Expr::Lambda(params, _, body) => {
for param in params {
names.insert(param.id);
}
collect_defined_names(body, names);
}
Expr::Tuple(v) => {
for e in v {
collect_defined_names(e, names);
}
}
Expr::Proj(e, _) => collect_defined_names(e, names),
Expr::Apply(fun, args) => {
collect_defined_names(fun, names);
for arg in args {
collect_defined_names(arg, names);
}
}
Expr::BinOp(lhs, _, rhs) => {
collect_defined_names(lhs, names);
collect_defined_names(rhs, names);
}
Expr::UniOp(_, e) => collect_defined_names(e, names),
Expr::MacroExpand(fun, args) => {
collect_defined_names(fun, names);
for arg in args {
collect_defined_names(arg, names);
}
}
Expr::If(cond, then, opt_else) => {
collect_defined_names(cond, names);
collect_defined_names(then, names);
if let Some(e) = opt_else {
collect_defined_names(e, names);
}
}
Expr::Block(body) => {
if let Some(b) = body {
collect_defined_names(b, names);
}
}
Expr::Escape(e) | Expr::Bracket(e) | Expr::Paren(e) | Expr::Feed(_, e) => {
collect_defined_names(e, names);
}
Expr::FieldAccess(record, _) => collect_defined_names(record, names),
Expr::ArrayAccess(array, index) => {
collect_defined_names(array, names);
collect_defined_names(index, names);
}
Expr::ArrayLiteral(elems) => {
for e in elems {
collect_defined_names(e, names);
}
}
Expr::RecordLiteral(fields) | Expr::ImcompleteRecord(fields) => {
for f in fields {
collect_defined_names(f.expr, names);
}
}
Expr::RecordUpdate(record, fields) => {
collect_defined_names(record, names);
for f in fields {
collect_defined_names(f.expr, names);
}
}
Expr::Assign(target, value) => {
collect_defined_names(target, names);
collect_defined_names(value, names);
}
Expr::Then(e, then) => {
collect_defined_names(e, names);
if let Some(t) = then {
collect_defined_names(t, names);
}
}
Expr::Match(scrutinee, arms) => {
collect_defined_names(scrutinee, names);
for arm in arms {
collect_defined_names(arm.body, names);
}
}
Expr::Var(_) | Expr::QualifiedVar(_) | Expr::Literal(_) | Expr::Error => {}
}
}
fn collect_names_from_pattern(pat: &Pattern, names: &mut HashSet<Symbol>) {
match pat {
Pattern::Single(name) => {
names.insert(*name);
}
Pattern::Tuple(pats) => {
for p in pats {
collect_names_from_pattern(p, names);
}
}
Pattern::Record(fields) => {
for (_, p) in fields {
collect_names_from_pattern(p, names);
}
}
Pattern::Placeholder | Pattern::Error => {}
}
}
struct ResolveContext<'a> {
module_info: &'a ModuleInfo,
known_names: &'a HashSet<Symbol>,
current_module_context: Vec<Symbol>,
file_path: PathBuf,
errors: Vec<Error>,
local_bindings: Vec<HashSet<Symbol>>,
}
impl<'a> ResolveContext<'a> {
fn new(
module_info: &'a ModuleInfo,
known_names: &'a HashSet<Symbol>,
file_path: PathBuf,
) -> Self {
Self {
module_info,
known_names,
current_module_context: Vec::new(),
file_path,
errors: Vec::new(),
local_bindings: Vec::new(),
}
}
fn name_exists(&self, name: &Symbol) -> bool {
self.known_names.contains(name)
}
fn push_scope(&mut self) {
self.local_bindings.push(HashSet::new());
}
fn pop_scope(&mut self) {
let _ = self.local_bindings.pop();
}
fn bind_local(&mut self, symbol: Symbol) {
if let Some(scope) = self.local_bindings.last_mut() {
scope.insert(symbol);
}
}
fn bind_pattern_locals(&mut self, pat: &Pattern) {
collect_names_from_pattern(
pat,
self.local_bindings.last_mut().expect("scope must exist"),
);
}
fn is_locally_bound(&self, name: Symbol) -> bool {
self.local_bindings
.iter()
.rev()
.any(|scope| scope.contains(&name))
}
fn resolve_through_wildcards(&self, name: Symbol) -> Option<Symbol> {
for base in &self.module_info.wildcard_imports {
let mangled = if base.as_str().is_empty() {
name
} else {
format!("{}${}", base.as_str(), name.as_str()).to_symbol()
};
if self.name_exists(&mangled) {
if let Some(&is_public) = self.module_info.visibility_map.get(&mangled) {
if is_public {
return Some(mangled);
}
} else {
return Some(mangled);
}
}
}
None
}
fn is_within_module_hierarchy(&self, resolved_path: &[Symbol]) -> bool {
if self.current_module_context.is_empty() || resolved_path.len() < 2 {
return false;
}
let target_module = &resolved_path[..resolved_path.len() - 1];
self.current_module_context.starts_with(target_module)
}
fn make_location(&self, e_id: ExprNodeId) -> Location {
Location {
span: e_id.to_span().clone(),
path: self.file_path.clone(),
}
}
}
pub fn convert_qualified_names(
expr: ExprNodeId,
module_info: &ModuleInfo,
builtin_names: &[Symbol],
file_path: PathBuf,
) -> (ExprNodeId, Vec<Error>) {
let mut known_names: HashSet<Symbol> = builtin_names.iter().copied().collect();
collect_defined_names(expr, &mut known_names);
let mut ctx = ResolveContext::new(module_info, &known_names, file_path);
let result = convert_expr(&mut ctx, expr);
(result, ctx.errors)
}
fn convert_expr(ctx: &mut ResolveContext, e_id: ExprNodeId) -> ExprNodeId {
let loc = ctx.make_location(e_id);
match e_id.to_expr().clone() {
Expr::Var(name) => convert_var(ctx, name, loc),
Expr::QualifiedVar(path) => convert_qualified_var(ctx, &path.segments, loc),
Expr::LetRec(id, body, then) => {
let name = id.id;
let prev_context = std::mem::take(&mut ctx.current_module_context);
ctx.push_scope();
ctx.bind_local(name);
if let Some(new_context) = ctx.module_info.module_context_map.get(&name) {
ctx.current_module_context = new_context.clone();
}
let new_body = convert_expr(ctx, body);
ctx.current_module_context = prev_context;
let new_then = then.map(|t| convert_expr(ctx, t));
ctx.pop_scope();
Expr::LetRec(id, new_body, new_then).into_id(loc)
}
Expr::Tuple(v) => {
let new_v: Vec<_> = v.into_iter().map(|e| convert_expr(ctx, e)).collect();
Expr::Tuple(new_v).into_id(loc)
}
Expr::Proj(e, idx) => {
let new_e = convert_expr(ctx, e);
Expr::Proj(new_e, idx).into_id(loc)
}
Expr::Let(pat, body, then) => {
let prev_context = std::mem::take(&mut ctx.current_module_context);
if let Some(module_context) = find_pattern_module_context(ctx, &pat.pat) {
ctx.current_module_context = module_context;
} else {
ctx.current_module_context = prev_context.clone();
}
let new_body = convert_expr(ctx, body);
let new_then = then.map(|t| {
ctx.push_scope();
ctx.bind_pattern_locals(&pat.pat);
let converted = convert_expr(ctx, t);
ctx.pop_scope();
converted
});
ctx.current_module_context = prev_context;
Expr::Let(pat, new_body, new_then).into_id(loc)
}
Expr::Lambda(params, r_type, body) => {
ctx.push_scope();
for param in ¶ms {
ctx.bind_local(param.id);
}
let new_body = convert_expr(ctx, body);
ctx.pop_scope();
Expr::Lambda(params, r_type, new_body).into_id(loc)
}
Expr::Apply(fun, args) => {
let new_fun = convert_expr(ctx, fun);
let new_args: Vec<_> = args.into_iter().map(|e| convert_expr(ctx, e)).collect();
Expr::Apply(new_fun, new_args).into_id(loc)
}
Expr::BinOp(lhs, op, rhs) => {
let new_lhs = convert_expr(ctx, lhs);
let new_rhs = convert_expr(ctx, rhs);
Expr::BinOp(new_lhs, op, new_rhs).into_id(loc)
}
Expr::UniOp(op, expr) => {
let new_expr = convert_expr(ctx, expr);
Expr::UniOp(op, new_expr).into_id(loc)
}
Expr::MacroExpand(fun, args) => {
let new_fun = convert_expr(ctx, fun);
let new_args: Vec<_> = args.into_iter().map(|e| convert_expr(ctx, e)).collect();
Expr::MacroExpand(new_fun, new_args).into_id(loc)
}
Expr::If(cond, then, opt_else) => {
let new_cond = convert_expr(ctx, cond);
let new_then = convert_expr(ctx, then);
let new_else = opt_else.map(|e| convert_expr(ctx, e));
Expr::If(new_cond, new_then, new_else).into_id(loc)
}
Expr::Block(body) => {
let new_body = body.map(|e| convert_expr(ctx, e));
Expr::Block(new_body).into_id(loc)
}
Expr::Escape(e) => {
let new_e = convert_expr(ctx, e);
Expr::Escape(new_e).into_id(loc)
}
Expr::Bracket(e) => {
let new_e = convert_expr(ctx, e);
Expr::Bracket(new_e).into_id(loc)
}
Expr::FieldAccess(record, field) => {
let new_record = convert_expr(ctx, record);
Expr::FieldAccess(new_record, field).into_id(loc)
}
Expr::ArrayAccess(array, index) => {
let new_array = convert_expr(ctx, array);
let new_index = convert_expr(ctx, index);
Expr::ArrayAccess(new_array, new_index).into_id(loc)
}
Expr::ArrayLiteral(elems) => {
let new_elems: Vec<_> = elems.into_iter().map(|e| convert_expr(ctx, e)).collect();
Expr::ArrayLiteral(new_elems).into_id(loc)
}
Expr::RecordLiteral(fields) => {
let new_fields = fields
.into_iter()
.map(|f| crate::ast::RecordField {
name: f.name,
expr: convert_expr(ctx, f.expr),
})
.collect();
Expr::RecordLiteral(new_fields).into_id(loc)
}
Expr::ImcompleteRecord(fields) => {
let new_fields = fields
.into_iter()
.map(|f| crate::ast::RecordField {
name: f.name,
expr: convert_expr(ctx, f.expr),
})
.collect();
Expr::ImcompleteRecord(new_fields).into_id(loc)
}
Expr::RecordUpdate(record, fields) => {
let new_record = convert_expr(ctx, record);
let new_fields = fields
.into_iter()
.map(|f| crate::ast::RecordField {
name: f.name,
expr: convert_expr(ctx, f.expr),
})
.collect();
Expr::RecordUpdate(new_record, new_fields).into_id(loc)
}
Expr::Assign(target, value) => {
let new_target = convert_expr(ctx, target);
let new_value = convert_expr(ctx, value);
Expr::Assign(new_target, new_value).into_id(loc)
}
Expr::Then(e, then) => {
let new_e = convert_expr(ctx, e);
let new_then = then.map(|t| convert_expr(ctx, t));
Expr::Then(new_e, new_then).into_id(loc)
}
Expr::Feed(sym, e) => {
let new_e = convert_expr(ctx, e);
Expr::Feed(sym, new_e).into_id(loc)
}
Expr::Paren(e) => {
convert_expr(ctx, e)
}
Expr::Match(scrutinee, arms) => {
let new_scrutinee = convert_expr(ctx, scrutinee);
let new_arms = arms
.into_iter()
.map(|arm| {
ctx.push_scope();
bind_match_pattern_locals(ctx, &arm.pattern);
let body = convert_expr(ctx, arm.body);
ctx.pop_scope();
crate::ast::MatchArm {
pattern: arm.pattern,
body,
}
})
.collect();
Expr::Match(new_scrutinee, new_arms).into_id(loc)
}
Expr::Literal(_) | Expr::Error => e_id,
}
}
fn find_pattern_module_context(
ctx: &ResolveContext,
pat: &crate::pattern::Pattern,
) -> Option<Vec<Symbol>> {
match pat {
Pattern::Single(name) => ctx.module_info.module_context_map.get(name).cloned(),
Pattern::Tuple(items) => items
.iter()
.find_map(|item| find_pattern_module_context(ctx, item)),
Pattern::Record(fields) => fields
.iter()
.find_map(|(_, item)| find_pattern_module_context(ctx, item)),
Pattern::Placeholder | Pattern::Error => None,
}
}
fn bind_match_pattern_locals(ctx: &mut ResolveContext, pat: &crate::ast::MatchPattern) {
use crate::ast::MatchPattern;
match pat {
MatchPattern::Variable(id) => {
ctx.bind_local(*id);
}
MatchPattern::Tuple(items) => {
items
.iter()
.for_each(|item| bind_match_pattern_locals(ctx, item));
}
MatchPattern::Constructor(_, inner) => {
inner
.as_ref()
.iter()
.for_each(|inner_pat| bind_match_pattern_locals(ctx, inner_pat));
}
MatchPattern::Wildcard | MatchPattern::Literal(_) => {}
}
}
fn resolve_alias_chain(module_info: &ModuleInfo, symbol: Symbol) -> Symbol {
let mut current = symbol;
let mut visited = HashSet::new();
while visited.insert(current) {
match module_info.use_alias_map.get(¤t).copied() {
Some(next) if next != current => current = next,
_ => break,
}
}
current
}
fn is_core_intrinsic_name(name: Symbol) -> bool {
matches!(
name.as_str(),
intrinsics::NEG
| intrinsics::ADD
| intrinsics::SUB
| intrinsics::MULT
| intrinsics::DIV
| intrinsics::EQ
| intrinsics::NE
| intrinsics::LE
| intrinsics::LT
| intrinsics::GE
| intrinsics::GT
| intrinsics::MODULO
| intrinsics::POW
| intrinsics::AND
| intrinsics::OR
| intrinsics::TOFLOAT
)
}
fn is_operator_lowered_builtin_name(name: Symbol) -> bool {
is_core_intrinsic_name(name) || name.as_str() == "_mimium_schedule_at"
}
fn convert_var(ctx: &mut ResolveContext, name: Symbol, loc: Location) -> ExprNodeId {
if ctx.is_locally_bound(name) {
return Expr::Var(name).into_id(loc);
}
if !ctx.current_module_context.is_empty()
&& let Some(relative_mangled) = (1..=ctx.current_module_context.len())
.rev()
.map(|prefix_len| {
let mut relative_path = ctx.current_module_context[..prefix_len].to_vec();
relative_path.push(name);
relative_path
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("$")
.to_symbol()
})
.find(|relative_mangled| ctx.name_exists(relative_mangled))
{
return Expr::Var(relative_mangled).into_id(loc);
}
if ctx.module_info.use_alias_map.contains_key(&name) {
let mangled_name = resolve_alias_chain(ctx.module_info, name);
if let Some(&is_public) = ctx.module_info.visibility_map.get(&mangled_name)
&& !is_public
&& !ctx.is_within_module_hierarchy(&extract_path_from_mangled(mangled_name))
{
let parts: Vec<&str> = mangled_name.as_str().split('$').collect();
let module_path: Vec<Symbol> = parts[..parts.len() - 1]
.iter()
.map(|s| s.to_symbol())
.collect();
let member = parts.last().unwrap().to_symbol();
ctx.errors.push(Error::PrivateMemberAccess {
module_path,
member,
location: loc.clone(),
});
}
return Expr::Var(mangled_name).into_id(loc);
}
if let Some(mangled) = ctx.resolve_through_wildcards(name) {
return Expr::Var(mangled).into_id(loc);
}
Expr::Var(name).into_id(loc)
}
fn convert_qualified_var(
ctx: &mut ResolveContext,
segments: &[Symbol],
loc: Location,
) -> ExprNodeId {
if let [ns, name] = segments
&& ns.as_str() == intrinsics::OP_INTRINSIC_MARKER_NS
&& is_operator_lowered_builtin_name(*name)
{
return Expr::Var(*name).into_id(loc);
}
let mangled_name = if segments.len() == 1 {
segments[0]
} else {
segments
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("$")
.to_symbol()
};
let (resolved_name, resolved_path) = resolve_qualified_path(
segments,
mangled_name,
&ctx.current_module_context,
|name| ctx.name_exists(name),
);
let lookup_name = resolve_alias_chain(ctx.module_info, resolved_name);
if resolved_path.len() > 1
&& let Some(&is_public) = ctx.module_info.visibility_map.get(&resolved_name)
{
let is_same_module = ctx.is_within_module_hierarchy(&resolved_path);
if !is_public && !is_same_module {
ctx.errors.push(Error::PrivateMemberAccess {
module_path: resolved_path[..resolved_path.len() - 1].to_vec(),
member: *resolved_path.last().unwrap(),
location: loc.clone(),
});
}
}
Expr::Var(lookup_name).into_id(loc)
}
fn extract_path_from_mangled(mangled: Symbol) -> Vec<Symbol> {
mangled.as_str().split('$').map(|s| s.to_symbol()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::program::QualifiedPath;
use crate::pattern::TypedPattern;
use crate::types::Type;
fn make_loc() -> Location {
Location {
span: 0..1,
path: PathBuf::from("/test"),
}
}
#[test]
fn test_qualified_var_to_mangled() {
let loc = make_loc();
let mut module_info = ModuleInfo::default();
module_info
.visibility_map
.insert("foo$bar".to_symbol(), true);
let expr = Expr::QualifiedVar(QualifiedPath {
segments: vec!["foo".to_symbol(), "bar".to_symbol()],
})
.into_id(loc.clone());
let builtin_names = vec!["foo$bar".to_symbol()];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "foo$bar"));
}
#[test]
fn test_private_member_access_reports_error_but_continues() {
let loc = make_loc();
let mut module_info = ModuleInfo::default();
module_info
.visibility_map
.insert("foo$secret".to_symbol(), false);
let expr = Expr::QualifiedVar(QualifiedPath {
segments: vec!["foo".to_symbol(), "secret".to_symbol()],
})
.into_id(loc.clone());
let builtin_names = vec!["foo$secret".to_symbol()];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert_eq!(errors.len(), 1);
assert!(matches!(&errors[0], Error::PrivateMemberAccess { .. }));
assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "foo$secret"));
}
#[test]
fn test_use_alias_resolution() {
let loc = make_loc();
let mut module_info = ModuleInfo::default();
module_info
.use_alias_map
.insert("func".to_symbol(), "module$func".to_symbol());
module_info
.visibility_map
.insert("module$func".to_symbol(), true);
let expr = Expr::Var("func".to_symbol()).into_id(loc.clone());
let builtin_names = vec!["module$func".to_symbol()];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "module$func"));
}
#[test]
fn test_wildcard_resolution() {
let loc = make_loc();
let mut module_info = ModuleInfo::default();
module_info.wildcard_imports.push("mymod".to_symbol());
module_info
.visibility_map
.insert("mymod$helper".to_symbol(), true);
let expr = Expr::Var("helper".to_symbol()).into_id(loc.clone());
let builtin_names = vec!["mymod$helper".to_symbol()];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "mymod$helper"));
}
#[test]
fn test_local_binding_shadows_wildcard_import() {
let loc = make_loc();
let mut module_info = ModuleInfo::default();
module_info.wildcard_imports.push("core".to_symbol());
module_info
.visibility_map
.insert("core$mix".to_symbol(), true);
let unknownty = Type::Unknown.into_id_with_location(loc.clone());
let expr = Expr::Let(
TypedPattern {
pat: Pattern::Single("mix".to_symbol()),
ty: unknownty,
default_value: None,
},
Expr::Literal(crate::ast::Literal::Float("0.5".to_symbol())).into_id(loc.clone()),
Some(Expr::Var("mix".to_symbol()).into_id(loc.clone())),
)
.into_id(loc.clone());
let builtin_names = vec!["core$mix".to_symbol()];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
match result.to_expr() {
Expr::Let(_, _, Some(then)) => {
assert!(matches!(then.to_expr(), Expr::Var(name) if name.as_str() == "mix"));
}
_ => panic!("expected let expression with then branch"),
}
}
#[test]
fn test_parent_module_symbol_shadows_wildcard_import() {
let loc = make_loc();
let mut module_info = ModuleInfo::default();
module_info.wildcard_imports.push("filter".to_symbol());
module_info
.visibility_map
.insert("filter$allpass".to_symbol(), true);
let wet_name = "reverb$freeverb$freeverb_mono_wet".to_symbol();
module_info.module_context_map.insert(
wet_name,
vec![
"reverb".to_symbol(),
"freeverb".to_symbol(),
"freeverb_mono_wet".to_symbol(),
],
);
let unknownty = Type::Unknown.into_id_with_location(loc.clone());
let expr = Expr::LetRec(
crate::pattern::TypedId::new(wet_name, unknownty),
Expr::Var("allpass".to_symbol()).into_id(loc.clone()),
None,
)
.into_id(loc.clone());
let builtin_names = vec!["filter$allpass".to_symbol(), "reverb$allpass".to_symbol()];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
match result.to_expr() {
Expr::LetRec(_, body, _) => {
assert!(
matches!(body.to_expr(), Expr::Var(name) if name.as_str() == "reverb$allpass")
);
}
_ => panic!("expected letrec expression"),
}
}
#[test]
fn test_wildcard_not_resolved_when_name_not_exists() {
let loc = make_loc();
let mut module_info = ModuleInfo::default();
module_info.wildcard_imports.push("mymod".to_symbol());
let expr = Expr::Var("helper".to_symbol()).into_id(loc.clone());
let builtin_names = vec![]; let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "helper"));
}
#[test]
fn test_simple_var_unchanged() {
let loc = make_loc();
let module_info = ModuleInfo::default();
let expr = Expr::Var("local_var".to_symbol()).into_id(loc.clone());
let builtin_names = vec![];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
assert!(matches!(result.to_expr(), Expr::Var(name) if name.as_str() == "local_var"));
}
#[test]
fn test_names_collected_from_let() {
let loc = make_loc();
let module_info = ModuleInfo::default();
let unknownty = Type::Unknown.into_id_with_location(loc.clone());
let expr = Expr::Let(
TypedPattern {
pat: Pattern::Single("x".to_symbol()),
ty: unknownty,
default_value: None,
},
Expr::Literal(crate::ast::Literal::Float("1.0".to_symbol())).into_id(loc.clone()),
Some(Expr::Var("x".to_symbol()).into_id(loc.clone())),
)
.into_id(loc.clone());
let builtin_names = vec![];
let (result, errors) =
convert_qualified_names(expr, &module_info, &builtin_names, PathBuf::from("/test"));
assert!(errors.is_empty());
assert!(matches!(result.to_expr(), Expr::Let(..)));
}
}