diff --git a/Sources/CLyte/include/lyte.h b/Sources/CLyte/include/lyte.h index 430f4b44..4f564c45 100644 --- a/Sources/CLyte/include/lyte.h +++ b/Sources/CLyte/include/lyte.h @@ -100,10 +100,17 @@ void lyte_program_set_print_callback(LyteProgram* program, lyte_print_fn callbac // ============ Entry point invocation ============ +/// Returns true if the entry point at the given index was defined in the +/// compiled source. Entry points are optional: a requested name that the +/// source doesn't define is simply not compiled, and it's up to the caller +/// to decide whether that's an error. +bool lyte_program_has_entry_point(const LyteProgram* program, size_t entry_point); + /// Call an entry point by index with an external globals buffer. /// The index corresponds to the order of entry points passed to lyte_compiler_new. /// The buffer must be at least lyte_program_get_globals_size() bytes. -/// Returns true on success, false if cancelled, error, or invalid index. +/// Returns true on success, false if cancelled, error, invalid index, or if +/// the entry point wasn't defined in the source. bool lyte_entry_point_call(LyteProgram* program, size_t entry_point, uint8_t* globals); // ============ Globals helpers ============ diff --git a/Sources/Lyte/Lyte.swift b/Sources/Lyte/Lyte.swift index ae834223..398eeaa6 100644 --- a/Sources/Lyte/Lyte.swift +++ b/Sources/Lyte/Lyte.swift @@ -112,15 +112,19 @@ public final class Program { globals.first { $0.name == name } } - /// Look up an entry point by name. Returns nil if not found. + /// Look up an entry point by name. Returns nil if it wasn't requested, or + /// if the source doesn't define it — entry points are optional, so it's up + /// to the caller to decide whether a missing one is an error. public func entryPoint(named name: String) -> EntryPoint? { guard let index = entryPointNames.firstIndex(of: name) else { return nil } - return EntryPoint(program: self, index: index) + return entryPoint(at: index) } /// Get an entry point by index (matching the order passed to LyteCompiler.init). + /// Returns nil if the source doesn't define it. public func entryPoint(at index: Int) -> EntryPoint? { guard index >= 0 && index < entryPointNames.count else { return nil } + guard lyte_program_has_entry_point(handle, index) else { return nil } return EntryPoint(program: self, index: index) } diff --git a/cli/src/main.rs b/cli/src/main.rs index 96a5a9bb..cacca7ed 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -73,6 +73,17 @@ struct Args { target: String, } +/// The compiler treats entry points as optional (undefined ones are skipped), +/// so requiring them is the client's job. The CLI needs every requested entry +/// point to exist before it compiles or runs anything. +fn require_entry_points(compiler: &lyte::Compiler) -> bool { + let missing = compiler.missing_entry_points(); + for name in &missing { + eprintln!("entry point function '{}' not found", name); + } + missing.is_empty() +} + fn run(args: Args) -> i32 { let mut paths = vec![]; @@ -167,18 +178,12 @@ fn run(args: Args) -> i32 { // For `--check`, also run monomorphization so the post-monomorph safety // check (which catches bounds violations in `[T; N]` function bodies) - // fires here too. The actual compiled code is discarded. Skip if no - // entry point is defined — many test snippets are entry-point-less. + // fires here too. The actual compiled code is discarded. Undefined entry + // points are simply skipped — many test snippets are entry-point-less. if args.check && compiler.has_decls() { - let has_entry = compiler - .effective_entry_points() - .iter() - .any(|name| !compiler.decls().find(*name).is_empty()); - if has_entry { - if let Err(e) = compiler.specialize() { - eprintln!("{}", e); - return 1; - } + if let Err(e) = compiler.specialize() { + eprintln!("{}", e); + return 1; } } @@ -188,6 +193,9 @@ fn run(args: Args) -> i32 { #[cfg(feature = "llvm")] { if let Some(out) = args.aot.as_deref() { + if !require_entry_points(&compiler) { + return 1; + } return run_aot( &mut compiler, out, @@ -250,6 +258,10 @@ fn run(args: Args) -> i32 { return 1; } + if !require_entry_points(&compiler) { + return 1; + } + let compile_start = Instant::now(); if let Err(e) = compiler.specialize() { eprintln!("{}", e); diff --git a/src/compiler.rs b/src/compiler.rs index ff97305d..fe5fe831 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -471,6 +471,10 @@ impl Compiler { } /// Returns the effective entry points (defaults to ["main"] if none set). + /// + /// These are the *requested* entry points. Entry points are optional: the + /// backends skip any that aren't defined, so it's up to the client to + /// decide whether a missing one is an error (see `missing_entry_points`). pub fn effective_entry_points(&self) -> Vec { if self.entry_points.is_empty() { vec![Name::new("main".into())] @@ -479,6 +483,38 @@ impl Compiler { } } + /// True if `name` is declared as a top level function in the parsed source. + /// + /// Answered from the AST rather than the decl table so it is valid as soon + /// as the source is parsed. `check()` copies every tree decl into + /// `self.decls`, so the two agree once it has run; before that `self.decls` + /// is empty and every entry point would look missing. + fn entry_point_is_defined(&self, name: Name) -> bool { + self.ast.iter().any(|tree| { + tree.decls + .iter() + .any(|d| matches!(d, Decl::Func(f) if f.name == name)) + }) + } + + /// The effective entry points that are defined as functions. + pub fn found_entry_points(&self) -> Vec { + self.effective_entry_points() + .into_iter() + .filter(|name| self.entry_point_is_defined(*name)) + .collect() + } + + /// The effective entry points that are *not* defined as functions. + /// Clients that require an entry point should check this and report an + /// error; compilation itself just skips them. + pub fn missing_entry_points(&self) -> Vec { + self.effective_entry_points() + .into_iter() + .filter(|name| !self.entry_point_is_defined(*name)) + .collect() + } + pub fn parse_file(&mut self, path: &str) { let contents = fs::read_to_string(path); @@ -824,7 +860,8 @@ impl Compiler { } } - /// Compile to native code via Cranelift JIT. + /// Compile to native code via Cranelift JIT. Returns the code pointer of + /// the first entry point that's defined. #[cfg(feature = "cranelift")] pub fn jit(&self) -> Result<(*const u8, usize, JIT), String> { let mut jit = JIT::default(); @@ -833,7 +870,15 @@ impl Compiler { if self.decls.decls.is_empty() { return Err(String::from("No declarations to compile")); } - let (code_ptr, globals_size) = jit.compile(&self.decls)?; + let entry_points = self.effective_entry_points(); + let (map, globals_size) = jit.compile_multi(&self.decls, &entry_points)?; + let code_ptr = entry_points + .iter() + .find_map(|name| map.get(name).copied()) + .ok_or_else(|| match entry_points.first() { + Some(name) => format!("entry point function '{}' not found", name), + None => "no entry point to run".to_string(), + })?; Ok((code_ptr, globals_size, jit)) } @@ -938,6 +983,9 @@ impl Compiler { /// Run the code using the stack VM interpreter. pub fn run_stack(&mut self) -> Result { let program = self.compile_stack()?; + if program.entry_points.is_empty() { + return Err(self.no_entry_point_error()); + } let mut vm = crate::stack_vm::StackVM::new(); Ok(vm.run(&program)) } @@ -945,10 +993,21 @@ impl Compiler { /// Run the code using the VM interpreter. pub fn run_vm(&mut self) -> Result { let program = self.compile_vm()?; + if program.entry_points.is_empty() { + return Err(self.no_entry_point_error()); + } let mut vm = VM::new(); Ok(vm.run(&program)) } + /// Running requires an entry point, even though compiling doesn't. + fn no_entry_point_error(&self) -> String { + match self.effective_entry_points().first() { + Some(name) => format!("entry point function '{}' not found", name), + None => "no entry point to run".to_string(), + } + } + /// Compile to a backend-agnostic CompiledProgram. /// Auto-selects LLVM JIT (when available) or VM. pub fn compile_program(&self) -> Result { @@ -1506,6 +1565,106 @@ mod tests { jit.free_memory(); } + #[test] + fn test_missing_entry_point_is_skipped_vm() { + // Only "init" is defined. The missing "process" entry point is not an + // error — it's just absent from the compiled program. + let code = r#" + var counter: i32 + + init { + counter = 10 + } + "#; + + let mut compiler = Compiler::new(); + compiler.parse(code, "."); + compiler.set_entry_points(&["init", "process"]); + assert!(compiler.check()); + + assert_eq!(compiler.found_entry_points(), vec![Name::str("init")]); + assert_eq!(compiler.missing_entry_points(), vec![Name::str("process")]); + + compiler.specialize().unwrap(); + let program = compiler.compile_vm().unwrap(); + + assert!(program.entry_points.contains_key(&Name::str("init"))); + assert!(!program.entry_points.contains_key(&Name::str("process"))); + + let mut vm = crate::vm::VM::new(); + vm.call(&program, Name::str("init"), &[]).unwrap(); + assert!(vm.call(&program, Name::str("process"), &[]).is_err()); + } + + #[cfg(feature = "cranelift")] + #[test] + fn test_missing_entry_point_is_skipped_jit() { + let code = r#" + var counter: i32 + + init { + counter = 10 + } + "#; + + let mut compiler = Compiler::new(); + compiler.parse(code, "."); + compiler.set_entry_points(&["init", "process"]); + assert!(compiler.check()); + compiler.specialize().unwrap(); + + let (map, _globals_size, jit) = compiler.jit_multi().unwrap(); + assert!(map.contains_key(&Name::str("init"))); + assert!(!map.contains_key(&Name::str("process"))); + jit.free_memory(); + } + + #[test] + fn test_entry_points_reported_before_check() { + // found/missing_entry_points read the AST, so an embedder that asks + // before check() gets the real answer rather than "everything missing". + let code = r#" + init { + } + "#; + + let mut compiler = Compiler::new(); + compiler.parse(code, "."); + compiler.set_entry_points(&["init", "process"]); + + assert_eq!(compiler.found_entry_points(), vec![Name::str("init")]); + assert_eq!(compiler.missing_entry_points(), vec![Name::str("process")]); + + // And the answer doesn't change once check() has populated decls. + assert!(compiler.check()); + assert_eq!(compiler.found_entry_points(), vec![Name::str("init")]); + assert_eq!(compiler.missing_entry_points(), vec![Name::str("process")]); + } + + #[test] + fn test_no_entry_points_found_compiles_but_does_not_run() { + // A library with no entry point at all still compiles cleanly; only + // running requires one. + let code = r#" + helper(x: i32) -> i32 { + x * 2 + } + "#; + + let mut compiler = Compiler::new(); + compiler.parse(code, "."); + assert!(compiler.check()); + assert!(compiler.found_entry_points().is_empty()); + assert_eq!(compiler.missing_entry_points(), vec![Name::str("main")]); + + compiler.specialize().unwrap(); + let program = compiler.compile_vm().unwrap(); + assert!(program.entry_points.is_empty()); + + let err = compiler.run_vm().unwrap_err(); + assert!(err.contains("not found"), "{}", err); + } + #[test] fn test_multi_entry_shared_function() { // Both entry points call the same helper function diff --git a/src/decl_table.rs b/src/decl_table.rs index d86abcb8..2a885294 100644 --- a/src/decl_table.rs +++ b/src/decl_table.rs @@ -46,6 +46,28 @@ impl DeclTable { &self.decls[range] } + /// Looks up an entry point function by name. + /// + /// Entry points are optional: a name that doesn't resolve to a function + /// yields None so backends can skip it. Deciding whether a missing entry + /// point is an error is up to the client. + /// + /// Non-function decls sharing the name (a global, struct, etc.) are skipped + /// rather than shadowing the function, since decls with equal names are + /// ordered by source position. + pub fn find_entry_point(&self, name: Name) -> Option<&FuncDecl> { + self.entry_point_overloads(name).next() + } + + /// Returns every function declared with the given name, ignoring non-function + /// decls that happen to share it. + pub fn entry_point_overloads(&self, name: Name) -> impl Iterator { + self.find(name).iter().filter_map(|d| match d { + Decl::Func(d) => Some(d), + _ => None, + }) + } + /// Calls f for every enum containing a case named name. /// This is for resolving .enum_case expressions. pub fn find_enum(&self, name: Name, f: &mut impl FnMut(Name)) { diff --git a/src/ffi.rs b/src/ffi.rs index 6fa36bf4..c379dcc3 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -21,6 +21,8 @@ struct GlobalInfo { /// An entry point: backend-specific data. /// Index matches the order of entry points passed to lyte_compiler_new. +/// Entry points are optional — a requested name that isn't defined in the +/// source keeps its slot (as None) so the indices stay stable. struct EntryPointInfo { #[cfg(feature = "llvm")] fn_addr: usize, @@ -233,7 +235,7 @@ pub struct LyteProgram { inner: crate::llvm_jit::LLVMCompiledProgram, globals_size: usize, globals_info: Vec, - entry_points: Vec, + entry_points: Vec>, cancel_callback: Option bool>, cancel_userdata: *mut u8, print_callback: Option, @@ -249,7 +251,7 @@ pub struct LyteProgram { vm: VM, globals_size: usize, globals_info: Vec, - entry_points: Vec, + entry_points: Vec>, cancel_callback: Option bool>, cancel_userdata: *mut u8, print_callback: Option, @@ -266,7 +268,7 @@ pub struct LyteProgram { backend: StackBackend, globals_size: usize, globals_info: Vec, - entry_points: Vec, + entry_points: Vec>, cancel_callback: Option bool>, cancel_userdata: *mut u8, print_callback: Option, @@ -325,12 +327,15 @@ pub unsafe extern "C" fn lyte_compiler_compile(ptr: *mut LyteCompiler) -> *mut L _ => unreachable!(), }; - let mut entry_points = Vec::new(); - for ep_name in &entry_point_names { - if let Some(&fn_addr) = inner.entry_points.get(ep_name) { - entry_points.push(EntryPointInfo { fn_addr }); - } - } + let entry_points: Vec> = entry_point_names + .iter() + .map(|ep_name| { + inner + .entry_points + .get(ep_name) + .map(|&fn_addr| EntryPointInfo { fn_addr }) + }) + .collect(); let program = Box::new(LyteProgram { inner, @@ -361,12 +366,15 @@ pub unsafe extern "C" fn lyte_compiler_compile(ptr: *mut LyteCompiler) -> *mut L let linked = LinkedProgram::from_program(&vm_program); let vm = VM::new(); - let mut entry_points = Vec::new(); - for ep_name in &entry_point_names { - if let Some(&func_idx) = vm_program.entry_points.get(ep_name) { - entry_points.push(EntryPointInfo { func_idx }); - } - } + let entry_points: Vec> = entry_point_names + .iter() + .map(|ep_name| { + vm_program + .entry_points + .get(ep_name) + .map(|&func_idx| EntryPointInfo { func_idx }) + }) + .collect(); let program = Box::new(LyteProgram { vm_program, @@ -398,12 +406,15 @@ pub unsafe extern "C" fn lyte_compiler_compile(ptr: *mut LyteCompiler) -> *mut L let globals_info = make_globals_info(crate::cancel::CANCEL_FLAG_RESERVED as usize); - let mut entry_points = Vec::new(); - for ep_name in &entry_point_names { - if let Some(&func_idx) = stack_program.entry_points.get(ep_name) { - entry_points.push(EntryPointInfo { func_idx }); - } - } + let entry_points: Vec> = entry_point_names + .iter() + .map(|ep_name| { + stack_program + .entry_points + .get(ep_name) + .map(|&func_idx| EntryPointInfo { func_idx }) + }) + .collect(); let backend = StackBackend::new(&stack_program); @@ -559,9 +570,26 @@ pub unsafe extern "C" fn lyte_globals_bind_extern( ptr::write_unaligned(globals.add(offset + 8) as *mut usize, context as usize); } +/// Returns true if the entry point at the given index was defined in the +/// compiled source. Entry points are optional: a requested name that isn't +/// defined simply isn't compiled, and it's up to the caller to decide whether +/// that's an error. +#[no_mangle] +pub unsafe extern "C" fn lyte_program_has_entry_point( + ptr: *const LyteProgram, + entry_point: usize, +) -> bool { + if ptr.is_null() { + return false; + } + let program = &*ptr; + matches!(program.entry_points.get(entry_point), Some(Some(_))) +} + /// Call an entry point by index with an external globals buffer. /// The index corresponds to the order of entry points passed to lyte_compiler_new. -/// Returns true on success, false if cancelled or invalid index. +/// Returns true on success, false if cancelled, if the index is invalid, or if +/// that entry point wasn't defined in the source. #[no_mangle] pub unsafe extern "C" fn lyte_entry_point_call( ptr: *mut LyteProgram, @@ -573,8 +601,8 @@ pub unsafe extern "C" fn lyte_entry_point_call( } let program = &mut *ptr; let ep = match program.entry_points.get(entry_point) { - Some(ep) => ep, - None => return false, + Some(Some(ep)) => ep, + _ => return false, }; // Install print callback for the duration of this call. diff --git a/src/jit.rs b/src/jit.rs index a90057cf..225d08f6 100644 --- a/src/jit.rs +++ b/src/jit.rs @@ -166,12 +166,15 @@ impl JIT { let code_ptr = map .get(&main_name) .copied() - .ok_or_else(|| "main function not found".to_string())?; + .ok_or_else(|| "entry point function 'main' not found".to_string())?; Ok((code_ptr, globals_size)) } /// Compile multiple entry points into native code. /// Returns (name→code_ptr map, globals_size). + /// + /// Entry points that aren't defined are skipped, so the returned map only + /// contains the ones that were found. pub fn compile_multi( &mut self, decls: &DeclTable, @@ -181,14 +184,8 @@ impl JIT { let mut func_ids = Vec::new(); for &ep_name in entry_points { - let ep_decls = decls.find(ep_name); - if ep_decls.is_empty() { - return Err(format!("entry point function '{}' not found", ep_name)); - } - let ep_decl = if let Decl::Func(d) = &ep_decls[0] { - d - } else { - return Err(format!("'{}' is not a function", ep_name)); + let Some(ep_decl) = decls.find_entry_point(ep_name) else { + continue; }; let id = self.compile_function(decls, ep_decl)?; func_ids.push((ep_name, id)); diff --git a/src/llvm_aot.rs b/src/llvm_aot.rs index 195dba96..6429a27b 100644 --- a/src/llvm_aot.rs +++ b/src/llvm_aot.rs @@ -235,16 +235,29 @@ pub fn compile_aot( Ok(()) } +/// Collect signature info for the requested entry points. +/// +/// Unlike the JIT and VM backends, AOT treats an undefined entry point as an +/// error rather than skipping it: the entry point list is the object's export +/// list, so skipping one produces an .o + .h silently missing that symbol, and +/// the mistake only surfaces as an undefined-symbol error in the host's link. fn collect_entries(decls: &DeclTable, entry_points: &[Name]) -> Result, String> { + let missing: Vec = entry_points + .iter() + .filter(|n| decls.find_entry_point(**n).is_none()) + .map(|n| format!("'{}'", n)) + .collect(); + if !missing.is_empty() { + return Err(format!( + "AOT entry point function(s) not found: {}", + missing.join(", ") + )); + } + let mut out = Vec::with_capacity(entry_points.len()); for &ep_name in entry_points { - let found = decls.find(ep_name); - if found.is_empty() { - return Err(format!("entry point '{}' not found", ep_name)); - } - let f = match &found[0] { - Decl::Func(d) => d, - _ => return Err(format!("'{}' is not a function", ep_name)), + let Some(f) = decls.find_entry_point(ep_name) else { + unreachable!("checked above"); }; let mut params = Vec::new(); for p in &f.params { @@ -833,3 +846,25 @@ fn c_string_escape(s: &str) -> String { } out } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Compiler; + + #[test] + fn undefined_entry_point_is_an_error() { + // AOT exports exactly the requested entry points, so an undefined one + // must fail here rather than yield an .o + .h missing that symbol. + let mut compiler = Compiler::new(); + compiler.parse("var counter: i32\ninit { counter = 10 }\n", "."); + assert!(compiler.check()); + let decls = compiler.decls(); + + assert!(collect_entries(decls, &[Name::str("init")]).is_ok()); + + let err = collect_entries(decls, &[Name::str("init"), Name::str("process")]).unwrap_err(); + assert!(err.contains("'process'"), "{}", err); + assert!(!err.contains("'init'"), "{}", err); + } +} diff --git a/src/llvm_jit.rs b/src/llvm_jit.rs index dd86900e..9ed5fa3f 100644 --- a/src/llvm_jit.rs +++ b/src/llvm_jit.rs @@ -483,15 +483,11 @@ pub(crate) fn build_module<'ctx>( state.declare_globals(decls); + // Entry points that aren't defined are skipped — the client decides + // whether a missing entry point is an error. for &ep_name in entry_points { - let ep_decls = decls.find(ep_name); - if ep_decls.is_empty() { - return Err(format!("entry point function '{}' not found", ep_name)); - } - let ep_decl = if let Decl::Func(d) = &ep_decls[0] { - d - } else { - return Err(format!("'{}' is not a function", ep_name)); + let Some(ep_decl) = decls.find_entry_point(ep_name) else { + continue; }; state.compile_function(decls, ep_decl)?; } @@ -579,8 +575,17 @@ fn compile_and_run_with_context( } } - // Look up the first entry point for execution. - let run_name = &*entry_points[0]; + // Look up the first entry point that exists for execution. Running does + // require one, so this is where a missing entry point becomes an error. + let run_ep = entry_points + .iter() + .copied() + .find(|n| decls.find_entry_point(*n).is_some()) + .ok_or_else(|| match entry_points.first() { + Some(n) => format!("entry point function '{}' not found", n), + None => "no entry point to run".to_string(), + })?; + let run_name = &*run_ep; let fn_addr = ee .get_function_address(run_name) .map_err(|e| format!("function '{}' not found in JIT: {:?}", run_name, e))?; @@ -678,9 +683,12 @@ impl LLVMJIT { } } - // Look up all entry point addresses. + // Look up the addresses of the entry points that were found. let mut ep_map = HashMap::new(); for &ep_name in entry_points { + if decls.find_entry_point(ep_name).is_none() { + continue; + } let fn_addr = ee .get_function_address(&*ep_name) .map_err(|e| format!("function '{}' not found in JIT: {:?}", ep_name, e))?; diff --git a/src/monomorph_pass.rs b/src/monomorph_pass.rs index 6ca31b59..ca5c9c73 100644 --- a/src/monomorph_pass.rs +++ b/src/monomorph_pass.rs @@ -45,33 +45,31 @@ impl MonomorphPass { /// /// Each entry point is processed as a root. The `processed_non_generic` /// set prevents reprocessing shared functions reached from multiple roots. + /// + /// Entry points that aren't defined are skipped — whether a missing entry + /// point is an error is up to the client. pub fn monomorphize_multi( &mut self, decls: &DeclTable, entry_points: &[Name], ) -> Result, String> { for &entry_point in entry_points { - let func_decls = decls.find(entry_point); - if func_decls.is_empty() { - return Err(format!("entry point function '{}' not found", entry_point)); - } - - if func_decls.len() > 1 { + if decls.entry_point_overloads(entry_point).count() > 1 { return Err(format!( "Multiple overloads found for entry point function '{}'", entry_point )); } - if let Decl::Func(fdecl) = &func_decls[0] { - if !self.processed_non_generic.contains(&fdecl.name) { - self.processed_non_generic.insert(fdecl.name); - let mut fdecl = fdecl.clone(); - self.process_function(&mut fdecl, decls)?; - self.out_decls.push(Decl::Func(fdecl)); - } - } else { - return Err(format!("Entry point '{}' is not a function", entry_point)); + let Some(fdecl) = decls.find_entry_point(entry_point) else { + continue; + }; + + if !self.processed_non_generic.contains(&fdecl.name) { + self.processed_non_generic.insert(fdecl.name); + let mut fdecl = fdecl.clone(); + self.process_function(&mut fdecl, decls)?; + self.out_decls.push(Decl::Func(fdecl)); } } @@ -997,10 +995,9 @@ mod tests { let mut pass = MonomorphPass::new(); let decls = DeclTable::new(vec![]); + // A missing entry point is not an error — it's simply skipped. let result = pass.monomorphize(&decls, Name::str("main")); - // Should fail because the entry point doesn't exist - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not found")); + assert_eq!(result.unwrap().len(), 0); } #[test] diff --git a/src/stack_codegen.rs b/src/stack_codegen.rs index f3e4bf36..7f0e82ae 100644 --- a/src/stack_codegen.rs +++ b/src/stack_codegen.rs @@ -130,6 +130,9 @@ impl StackCodegen { } /// Compile multiple entry points into a StackProgram. + /// + /// Entry points that aren't defined are skipped: only the ones that were + /// found show up in `program.entry_points`. pub fn compile_multi( &mut self, decls: &DeclTable, @@ -141,13 +144,8 @@ impl StackCodegen { if self.compiled_functions.contains(&ep_name) { continue; } - let ep_decls = decls.find(ep_name); - if ep_decls.is_empty() { - return Err(format!("entry point function '{}' not found", ep_name)); - } - let ep_decl = match &ep_decls[0] { - Decl::Func(d) => d, - _ => return Err(format!("'{}' is not a function", ep_name)), + let Some(ep_decl) = decls.find_entry_point(ep_name) else { + continue; }; self.compile_function(ep_decl, decls)?; @@ -165,16 +163,17 @@ impl StackCodegen { } } - // Set entry point. - self.program.entry = *self - .func_indices - .get(&entry_points[0]) - .ok_or_else(|| format!("entry point '{}' not found", entry_points[0]))?; - - // Populate entry_points map. + // Populate entry_points map, and set program.entry to the first entry + // point that was actually found. If none were found, program.entry + // stays at its default and the map is empty. + let mut entry_set = false; for &ep_name in entry_points { if let Some(&idx) = self.func_indices.get(&ep_name) { self.program.entry_points.insert(ep_name, idx); + if !entry_set { + self.program.entry = idx; + entry_set = true; + } } } diff --git a/src/stack_interp_bridge.rs b/src/stack_interp_bridge.rs index c87972f3..726441e5 100644 --- a/src/stack_interp_bridge.rs +++ b/src/stack_interp_bridge.rs @@ -917,6 +917,11 @@ fn encode_imm(op: &StackOp, func_idx: u32) -> [u64; 3] { /// Convert a StackProgram to C instruction format and run it. pub fn run(program: &StackProgram) -> i64 { + // Nothing to run: no entry point was resolved at compile time, so + // program.entry is a meaningless default. + if !program.has_entry() { + return 0; + } let mut backend = StackBackend::new(program); let mut owned_globals: Vec = vec![0u8; program.globals_size]; backend.call_entry(program.entry, owned_globals.as_mut_ptr()) diff --git a/src/stack_ir.rs b/src/stack_ir.rs index 5e0d06d6..7081566e 100644 --- a/src/stack_ir.rs +++ b/src/stack_ir.rs @@ -675,6 +675,13 @@ impl StackProgram { self.functions.push(func); idx } + + /// True if `entry` names a real function. Entry points are optional, and + /// `entry` defaults to 0, so a program compiled with none resolved would + /// otherwise run function 0 (or index out of bounds). + pub fn has_entry(&self) -> bool { + (self.entry as usize) < self.functions.len() + } } /// Display support for stack IR disassembly. diff --git a/src/stack_vm.rs b/src/stack_vm.rs index 2af0b43d..1c42be0d 100644 --- a/src/stack_vm.rs +++ b/src/stack_vm.rs @@ -126,6 +126,12 @@ impl StackVM { self.globals.resize(program.globals_size, 0); self.cancelled = false; + // Nothing to run: no entry point was resolved at compile time, so + // program.entry is a meaningless default. + if !program.has_entry() { + return 0; + } + let mut func_idx = program.entry; let (mut locals, mut lm_base) = self.enter_function(program, func_idx, &[]); let mut ip: usize = 0; diff --git a/src/vm.rs b/src/vm.rs index 4a67ab74..b8fae918 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1062,6 +1062,13 @@ impl VMProgram { self.functions.push(func); idx } + + /// True if `entry` names a real function. Entry points are optional, and + /// `entry` defaults to 0, so a program compiled with none resolved would + /// otherwise run function 0 (or index out of bounds). + pub fn has_entry(&self) -> bool { + (self.entry as usize) < self.functions.len() + } } /// Call frame for function execution @@ -1284,6 +1291,14 @@ impl VM { pub fn run(&mut self, program: &VMProgram) -> i64 { // Always reinitialize globals for run(). self.globals = vec![0u8; program.globals_size]; + self.cancelled = false; + self.trap = None; + + // Nothing to run: no entry point was resolved at compile time, so + // program.entry is a meaningless default. + if !program.has_entry() { + return 0; + } self.run_inner(program, program.entry, &[]) } @@ -3335,4 +3350,39 @@ mod tests { vm.run(&program); assert!(vm.cancelled, "expected the infinite loop to be cancelled"); } + + #[test] + fn test_no_entry_point_does_not_run() { + // Entry points are optional, so `entry` can be left at its 0 default + // with nothing to run. Neither shape may execute a function. + let empty = VMProgram::new(); + assert!(!empty.has_entry()); + assert_eq!(VM::new().run(&empty), 0); + + // A stale entry index pointing past the function table (e.g. a program + // rebuilt from a codegen that kept indices from a previous compile). + let mut func = VMFunction::new("test"); + func.emit(Opcode::LoadImm { dst: 0, value: 42 }); + func.emit(Opcode::Return); + + let mut program = VMProgram::new(); + program.add_function(func); + program.entry = 7; + assert!(!program.has_entry()); + assert_eq!(VM::new().run(&program), 0); + } + + #[test] + fn test_run_rezeroes_globals_without_entry_point() { + // run() documents that globals are always re-zeroed; the early return + // for a missing entry point must not skip that. + let mut vm = VM::new(); + vm.globals = vec![0xffu8; 8]; + + let mut program = VMProgram::new(); + program.globals_size = 4; + + assert_eq!(vm.run(&program), 0); + assert_eq!(vm.globals, vec![0u8; 4]); + } } diff --git a/src/vm_arm64.rs b/src/vm_arm64.rs index 74f340aa..8c394ddb 100644 --- a/src/vm_arm64.rs +++ b/src/vm_arm64.rs @@ -230,6 +230,12 @@ impl VM { self.cancelled = false; self.trap = None; + // Nothing to run: no entry point was resolved at compile time, so + // program.entry is a meaningless default. + if !program.has_entry() { + return 0; + } + // Pre-allocate call stack let mut call_stack = Vec::with_capacity(MAX_CALL_DEPTH); call_stack.resize( diff --git a/src/vm_codegen.rs b/src/vm_codegen.rs index 23e173a7..8363f266 100644 --- a/src/vm_codegen.rs +++ b/src/vm_codegen.rs @@ -114,6 +114,9 @@ impl VMCodegen { } /// Compile multiple entry points into a VMProgram. + /// + /// Entry points that aren't defined are skipped: only the ones that were + /// found show up in `program.entry_points`. pub fn compile_multi( &mut self, decls: &DeclTable, @@ -127,13 +130,8 @@ impl VMCodegen { if self.compiled_functions.contains(&ep_name) { continue; } - let ep_decls = decls.find(ep_name); - if ep_decls.is_empty() { - return Err(format!("entry point function '{}' not found", ep_name)); - } - let ep_decl = match &ep_decls[0] { - Decl::Func(d) => d, - _ => return Err(format!("'{}' is not a function", ep_name)), + let Some(ep_decl) = decls.find_entry_point(ep_name) else { + continue; }; self.compile_function(ep_decl, decls)?; @@ -152,16 +150,17 @@ impl VMCodegen { } } - // Set entry point to first entry point for backward compat. - self.program.entry = *self - .func_indices - .get(&entry_points[0]) - .ok_or_else(|| format!("entry point '{}' not found", entry_points[0]))?; - - // Populate entry_points map. + // Populate entry_points map, and set program.entry to the first entry + // point that was actually found (for backward compat). If none were + // found, program.entry stays at its default and the map is empty. + let mut entry_set = false; for &ep_name in entry_points { if let Some(&idx) = self.func_indices.get(&ep_name) { self.program.entry_points.insert(ep_name, idx); + if !entry_set { + self.program.entry = idx; + entry_set = true; + } } } diff --git a/tests/cases/custom_entry.lyte b/tests/cases/custom_entry.lyte new file mode 100644 index 00000000..0b3ffe87 --- /dev/null +++ b/tests/cases/custom_entry.lyte @@ -0,0 +1,13 @@ +// Entry points are optional and need not be named "main": the client picks +// them, and here we ask for "init". +// args: --entry init +// expected stdout: +// compilation successful +// assert(true) + +var counter: i32 + +init { + counter = 10 + assert(counter == 10) +} diff --git a/tests/cases/globals/global_named_like_entry_point.lyte b/tests/cases/globals/global_named_like_entry_point.lyte new file mode 100644 index 00000000..f8d63624 --- /dev/null +++ b/tests/cases/globals/global_named_like_entry_point.lyte @@ -0,0 +1,14 @@ +// A global sharing the entry point's name must not shadow the entry point +// function. Decls with equal names are ordered by source position, so looking +// at only the first one would report 'main' as missing. + +// expected stdout: +// compilation successful +// assert(true) + +var main: i32 + +main { + main = 3 + assert(main == 3) +}