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
9 changes: 8 additions & 1 deletion Sources/CLyte/include/lyte.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ============
Expand Down
8 changes: 6 additions & 2 deletions Sources/Lyte/Lyte.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
34 changes: 23 additions & 11 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![];

Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
163 changes: 161 additions & 2 deletions src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Name> {
if self.entry_points.is_empty() {
vec![Name::new("main".into())]
Expand All @@ -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<Name> {
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<Name> {
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);

Expand Down Expand Up @@ -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();
Expand All @@ -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))
}

Expand Down Expand Up @@ -938,17 +983,31 @@ impl Compiler {
/// Run the code using the stack VM interpreter.
pub fn run_stack(&mut self) -> Result<i64, String> {
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))
}

/// Run the code using the VM interpreter.
pub fn run_vm(&mut self) -> Result<i64, String> {
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<CompiledProgram, String> {
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/decl_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &FuncDecl> {
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)) {
Expand Down
Loading
Loading