Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 35 additions & 19 deletions crates/loon-lang/src/eir/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,21 +346,20 @@ impl<'a> Lower<'a> {
let Some(modpath) = items[1].as_dotted_path() else {
continue;
};
// Alias: `[use a/b as c]` → c, else the last path segment.
let alias = if items.len() >= 4 {
if let (ExprKind::Symbol(kw), ExprKind::Symbol(a)) =
(&items[2].kind, &items[3].kind)
{
if kw == "as" {
a.clone()
} else {
modpath.rsplit('/').next().unwrap_or(&modpath).to_string()
}
} else {
modpath.rsplit('/').next().unwrap_or(&modpath).to_string()
// Alias: `[use a.b :as c]` → c, else the full dotted path (the
// interpreter qualifies as `a.b.name`, so we match that).
let alias = if items.len() >= 3 {
match (&items[2].kind, items.get(3).map(|e| &e.kind)) {
(ExprKind::Keyword(kw), Some(ExprKind::Symbol(a))) if kw == "as" => a.clone(),
// Lenient fallback: accept a bare-symbol `as` too. The
// canonical syntax is the keyword `:as` (what the
// interpreter matches); a bare `as` would otherwise be
// silently ignored here and fail later as NotCallable.
(ExprKind::Symbol(kw), Some(ExprKind::Symbol(a))) if kw == "as" => a.clone(),
_ => modpath.clone(),
}
} else {
modpath.rsplit('/').next().unwrap_or(&modpath).to_string()
modpath.clone()
};

let file = crate::module::ModuleCache::resolve_path(&modpath, base);
Expand All @@ -384,29 +383,46 @@ impl<'a> Lower<'a> {
let module_forms = sub.expanded_program.clone();
// Recurse first so transitive imports land before this module.
self.collect_imports(&module_forms, &dir, visited, imported, qualified);
// Export rule matches the interpreter (module.rs): `pub fn` names
// if any are declared, otherwise every top-level fn. `every_fn`
// intentionally includes pub fns too — it is the complete set,
// used only as the fallback when the module declares no `pub`.
let mut pub_fns: Vec<String> = Vec::new();
let mut every_fn: Vec<String> = Vec::new();
for mf in &module_forms {
if let ExprKind::List(mitems) = &mf.kind {
match mitems.first().map(|e| &e.kind) {
// Skip the module's own `use` lines (handled by recursion).
Some(ExprKind::Symbol(s)) if s == "use" => continue,
_ => {}
}
// Record qualified names for `pub fn` exports.
if let Some(ExprKind::Symbol(s)) = mitems.first().map(|e| &e.kind) {
if s == "pub" && mitems.len() >= 3 {
Some(ExprKind::Symbol(s)) if s == "fn" && mitems.len() >= 3 => {
if let ExprKind::Symbol(name) = &mitems[1].kind {
every_fn.push(name.clone());
}
}
Some(ExprKind::Symbol(s)) if s == "pub" && mitems.len() >= 4 => {
if let (Some(ExprKind::Symbol(inner)), Some(ExprKind::Symbol(name))) = (
mitems.get(1).map(|e| &e.kind),
mitems.get(2).map(|e| &e.kind),
) {
if inner == "fn" {
qualified.push((alias.clone(), name.clone()));
pub_fns.push(name.clone());
every_fn.push(name.clone());
}
}
}
Comment thread
ecto marked this conversation as resolved.
_ => {}
}
}
imported.push(mf.clone());
}
let exported = if pub_fns.is_empty() {
every_fn
} else {
pub_fns
};
for name in exported {
qualified.push((alias.clone(), name));
}
}
}

Expand Down
116 changes: 112 additions & 4 deletions crates/loon-lang/src/eir/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3035,6 +3035,9 @@ pub enum VmErrorKind {
StackOverflow,
/// `assert-eq` failed with mismatched values.
AssertFailed(String, String),
/// A `[use ...]` failed at compile time: unresolved module, private
/// symbol, or circular dependency (checker E05xx diagnostics).
ModuleError(String),
/// A replayed program requested a different effect op than the trace
/// recorded (or ran past the end of the trace).
ReplayDivergence(ReplayDivergence),
Expand Down Expand Up @@ -3075,6 +3078,7 @@ impl VmErrorKind {
VmErrorKind::Trap => "trap",
VmErrorKind::StackOverflow => "stack-overflow",
VmErrorKind::AssertFailed(..) => "assert-failed",
VmErrorKind::ModuleError(_) => "module-error",
VmErrorKind::ReplayDivergence(_) => "replay-divergence",
VmErrorKind::UnhandledEffect(_) => "unhandled-effect",
VmErrorKind::DivideByZero(_) => "divide-by-zero",
Expand Down Expand Up @@ -3108,6 +3112,7 @@ impl std::fmt::Display for VmError {
VmErrorKind::AssertFailed(actual, expected) => {
write!(f, "assertion failed: {actual} != {expected}")
}
VmErrorKind::ModuleError(msg) => write!(f, "{msg}"),
VmErrorKind::ReplayDivergence(d) => {
write!(f, "replay diverged {}", d.message)
}
Expand All @@ -3128,6 +3133,22 @@ impl std::fmt::Display for VmError {
}
}

/// Surface the first module diagnostic (E05xx: unresolved module, private
/// symbol, circular dependency) from a check pass as a `VmError`. Other
/// checker diagnostics stay non-fatal on the EIR path for now, but a broken
/// `[use ...]` must not fall through to a confusing "value is not callable"
/// at the first qualified call site.
fn module_error(errors: &[crate::errors::LoonDiagnostic]) -> Option<VmError> {
errors
.iter()
.find(|d| d.code.category() == "module")
.map(|d| VmError {
kind: VmErrorKind::ModuleError(d.what.clone()),
span: d.labels.first().map(|l| l.span),
context: None,
})
}

// ─── Public API ────────────────────────────────────────────────────────────

/// Run a Loon program through the EIR pipeline: parse → check → lower → VM.
Expand All @@ -3146,7 +3167,10 @@ fn eval_eir_impl(src: &str, mut checker: crate::check::Checker) -> Result<VmResu
span: Some(e.span),
context: Some(format!("parse error: {}", e.message)),
})?;
let _errors = checker.check_program(&exprs);
let errors = checker.check_program(&exprs);
if let Some(e) = module_error(&errors) {
return Err(e);
}
let module = crate::eir::lower::lower(&checker);
let mut vm = Vm::new(module);
vm.run()
Expand All @@ -3165,7 +3189,10 @@ pub fn eval_eir_recorded(
span: Some(e.span),
context: Some(format!("parse error: {}", e.message)),
})?;
let _errors = checker.check_program(&exprs);
let errors = checker.check_program(&exprs);
if let Some(e) = module_error(&errors) {
return Err(e);
}
let module = crate::eir::lower::lower(&checker);
let mut vm = Vm::new(module);
vm.set_recorder(recorder);
Expand All @@ -3187,7 +3214,10 @@ pub fn eval_eir_replayed(
span: Some(e.span),
context: Some(format!("parse error: {}", e.message)),
})?;
let _errors = checker.check_program(&exprs);
let errors = checker.check_program(&exprs);
if let Some(e) = module_error(&errors) {
return Err(e);
}
let module = crate::eir::lower::lower(&checker);
let mut vm = Vm::new(module);
vm.set_replay(entries);
Expand Down Expand Up @@ -3218,7 +3248,10 @@ pub fn eval_eir_verified(
)
}
};
let _errors = checker.check_program(&exprs);
let errors = checker.check_program(&exprs);
if let Some(e) = module_error(&errors) {
return (Err(e), 0);
}
let module = crate::eir::lower::lower(&checker);
let mut vm = Vm::new(module);
vm.set_replay(entries);
Expand Down Expand Up @@ -3489,6 +3522,81 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn vm_use_non_pub_exports_all() {
// A module with no `pub` declarations exports every top-level fn,
// matching the interpreter's export rule (module.rs).
let dir = std::env::temp_dir().join(format!("loon_use_nopub_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::fs::write(dir.join("lib.oo"), "[fn triple [x] [* x 3]]\n").unwrap();
let r =
eval_eir_with_base_dir("[use lib] [println [lib.triple 5]]", &dir).expect("vm error");
assert_eq!(r.output, vec!["15".to_string()]);
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn vm_use_alias() {
// `[use mod :as m]` binds exports under the alias, like the interpreter.
let dir = std::env::temp_dir().join(format!("loon_use_alias_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::fs::write(dir.join("mymath.oo"), "[pub fn add [a b] [+ a b]]\n").unwrap();
let r = eval_eir_with_base_dir("[use mymath :as m] [println [m.add 40 2]]", &dir)
.expect("vm error");
assert_eq!(r.output, vec!["42".to_string()]);
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn vm_use_nested_modules() {
// A module that itself uses another module: transitive imports are
// inlined first, so both bare and qualified references resolve.
let dir = std::env::temp_dir().join(format!("loon_use_nested_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::fs::write(dir.join("inner.oo"), "[pub fn double [x] [* x 2]]\n").unwrap();
std::fs::write(
dir.join("outer.oo"),
"[use inner]\n[pub fn quad [x] [inner.double [inner.double x]]]\n",
)
.unwrap();
let r =
eval_eir_with_base_dir("[use outer] [println [outer.quad 3]]", &dir).expect("vm error");
assert_eq!(r.output, vec!["12".to_string()]);
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn vm_use_missing_module_errors() {
// A `use` of a nonexistent module fails with the checker's E0500
// diagnostic instead of a confusing NotCallable at the call site.
let dir = std::env::temp_dir().join(format!("loon_use_missing_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let err = eval_eir_with_base_dir("[use nosuch] [println 1]", &dir)
.expect_err("missing module should error");
assert!(
matches!(err.kind, VmErrorKind::ModuleError(ref m) if m.contains("cannot read module 'nosuch'")),
"got: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn vm_use_circular_import_errors() {
// Mutually-recursive modules surface the checker's E0502 circular
// dependency diagnostic on the EIR path.
let dir = std::env::temp_dir().join(format!("loon_use_cycle_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::fs::write(dir.join("ca.oo"), "[use cb]\n[fn afn [x] [+ x 1]]\n").unwrap();
std::fs::write(dir.join("cb.oo"), "[use ca]\n[fn bfn [x] [afn x]]\n").unwrap();
let err = eval_eir_with_base_dir("[use ca] [println [ca.afn 1]]", &dir)
.expect_err("circular import should error");
assert!(
matches!(err.kind, VmErrorKind::ModuleError(ref m) if m.contains("circular")),
"got: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn vm_host_effects() {
// Host effects are wired into the EIR VM (LIM-4): unhandled IO.now /
Expand Down
Loading