use std::cell::RefCell;
use std::fs;
use std::process::Command;
use std::rc::Rc;
use inkwell::context::Context;
use inkwell::module::Module;
use inkwell::targets::{CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetMachine, TargetTriple};
use inkwell::values::{CallSiteValue, InstructionOpcode, Operand};
use inkwell::AddressSpace;
use inkwell::OptimizationLevel;
use crate::check::core;
use crate::compile::symbols::CompiledItem;
use crate::{Checker, Heap, Interp, Path, Reader, TopLevelForm, Value};
const ENTRY_POINT_NAME: &str = "main";
fn collect_aot_item(
heap: &mut Heap,
interp: &mut Interp,
tl: TopLevelForm,
items: &mut Vec<CompiledItem>,
defvar_inits: &mut Vec<(Path, Value)>,
ffi_decls: &mut Vec<typelisp_front::eval::interp::FfiDecl>,
entry_path: &Path,
) -> Result<(), String> {
let tag = core::op(heap, tl).map(str::to_string).unwrap_or_default();
if tag == "module" {
let body = core::fields(heap, tl).map_err(|e| e.to_string())?;
for item in body.into_iter().skip(1) {
collect_aot_item(heap, interp, item, items, defvar_inits, ffi_decls, entry_path)?;
}
return Ok(());
}
let defvar_meta = match tag.as_str() {
"defun" => {
let path = core::path_field(heap, tl, 0).ok_or_else(|| "compile-file: defun without a name".to_string())?;
if path.last_segment() == ENTRY_POINT_NAME && &path != entry_path {
return Err(format!(
"compile-file: `{}` is named `{}`, but only the entry file's top-level \
`defun main` (`{}`) is the entry point — rename this one (a `defun main` \
nested in a module, or in a `use`d file, is never called)",
path, ENTRY_POINT_NAME, entry_path
));
}
items.push(CompiledItem::Fn(path));
None
}
"defmethod" => {
let type_path = core::path_field(heap, tl, 0).ok_or_else(|| "compile-file: defmethod without a type".to_string())?;
let method = match core::field(heap, tl, 1) {
Some(Value::Symbol(id)) => heap.symbol_name(id).to_string(),
_ => return Err("compile-file: defmethod without a name".to_string()),
};
items.push(CompiledItem::Method(type_path, method));
None
}
"defvar" => {
let name = core::path_field(heap, tl, 0).ok_or_else(|| "compile-file: defvar without a name".to_string())?;
Some((name, tl))
}
"defenum" | "defstruct" => None,
"defffi" => {
ffi_decls.push(typelisp_front::eval::interp::read_ffi_decl(heap, tl).map_err(|e| e.to_string())?);
None
}
"use" => None,
"expr" if is_entry_call(heap, tl, entry_path) => return Ok(()),
other => {
return Err(format!(
"compile-file only supports top-level `defun`/`defmethod`/`defvar`/`defconstant`/`defstruct`/`defenum`/`defffi`/`use`/`module`/`impl`, found `{}`",
other
))
}
};
interp.exec(heap, tl).map_err(|e| e.to_string())?;
if let Some((name, form)) = defvar_meta {
interp.promote_global(heap, &name).map_err(|e| e.to_string())?;
defvar_inits.push((name, form));
}
Ok(())
}
pub(crate) fn is_trailing_main(heap: &Heap, tl: Value, entry_path: &Path) -> bool {
core::op(heap, tl) == Some("expr") && is_entry_call(heap, tl, entry_path)
}
pub(crate) fn entry_point_path(segs: &[String]) -> Path {
let mut segs = segs.to_vec();
segs.push(ENTRY_POINT_NAME.to_string());
Path::from_segments(segs)
}
fn is_entry_call(heap: &Heap, tl: Value, entry_path: &Path) -> bool {
let Some(form) = core::field(heap, tl, 0) else { return false };
if core::op(heap, form) != Some("call") {
return false;
}
let is_main = core::path_field(heap, form, 2).is_some_and(|p| &p == entry_path);
is_main && core::fields(heap, form).map(|f| f.len() == 4).unwrap_or(false)
}
pub fn compile_file(source_path: &str, output_path: &str) -> Result<(), String> {
crate::compile::driver::emitted_layout_is_runnable()?;
let source =
fs::read_to_string(source_path).map_err(|e| format!("failed to read \"{}\": {}", source_path, e))?;
let mut heap = Heap::with_capacity(1 << 16);
let mut chk = Checker::new();
let mut interp = Interp::new();
let prelude = crate::compile::prelude_bootstrap::load_for_aot(&mut heap, &mut chk, &mut interp)?;
crate::load_compiler(&mut heap, &mut chk, &mut interp);
let entry_path_on_disk = std::path::Path::new(source_path);
let entry_dir = entry_path_on_disk
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|| std::path::PathBuf::from("."));
let src_root = crate::project::find_src_root(&entry_dir).unwrap_or(entry_dir);
let entry_segs = crate::project::module_segs_for(entry_path_on_disk, &src_root).map_err(|e| e.to_string())?;
let mut loader = crate::project::Loader::new(src_root.clone());
let reader = Reader::new();
let mut forms = reader.forms_within(source_path, &source, typelisp_front::mem::symbols::ns_of(&entry_segs));
chk.enter_file_module(&entry_segs);
let mut items: Vec<CompiledItem> = Vec::new();
let mut defvar_inits: Vec<(Path, Value)> = Vec::new();
let mut ffi_decls: Vec<typelisp_front::eval::interp::FfiDecl> = Vec::new();
let entry_path = entry_point_path(&entry_segs);
let mut entry_forms: Vec<TopLevelForm> = Vec::new();
loop {
let next = {
let hook = typelisp_front::read::DriverReadEval::new(&mut chk, &interp);
forms.next_form_with(&mut heap, Some(&hook)).map_err(|e| e.to_string())?
};
let Some((v, loc)) = next else { break };
loader.load_uses_in(&mut heap, &reader, &mut chk, &mut interp, std::slice::from_ref(&v)).map_err(|e| e.to_string())?;
let tl = chk.check_form_at(&mut heap, &interp, v, Some(loc)).map_err(|e| e.to_string())?;
heap.push_root(tl);
entry_forms.push(tl);
}
chk.exit_file_module(entry_segs.len());
for w in chk.take_warnings() {
eprintln!("{}", w);
}
for tl in loader.take_pending() {
collect_aot_item(&mut heap, &mut interp, tl, &mut items, &mut defvar_inits, &mut ffi_decls, &entry_path)?;
}
for tl in entry_forms {
collect_aot_item(&mut heap, &mut interp, tl, &mut items, &mut defvar_inits, &mut ffi_decls, &entry_path)?;
}
if !items.iter().any(|i| matches!(i, CompiledItem::Fn(p) if p == &entry_path)) {
return Err(format!(
"no zero-argument `{}` defun found (required as the entry point)",
ENTRY_POINT_NAME
));
}
let link_libraries: Vec<String> = ffi_decls
.iter()
.filter_map(|d| d.library.clone())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
let ctx = crate::compile::llvm_context();
let module = {
let _guard = crate::compile::COMPILE_LOCK.lock().unwrap();
let module = ctx.create_module("compiled_file");
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let fn_ty = ctx.i64_type().fn_type(&[ptr_ty.into(), ctx.i32_type().into()], false);
for (name, _) in crate::compile::externs::rt_extern_functions() {
module.add_function(name, fn_ty, None);
}
for decl in &ffi_decls {
crate::compile::ffi::emit_thunk(&module, decl)?;
}
let buffer =
inkwell::memory_buffer::MemoryBuffer::create_from_memory_range_copy(prelude.bitcode, "prelude");
let prelude_module = Module::parse_bitcode_from_buffer(&buffer, ctx)
.map_err(|e| format!("the committed prelude bitcode failed to parse: {}", e))?;
module
.link_in_module(prelude_module)
.map_err(|e| format!("linking the prelude into the compiled file failed: {}", e))?;
Rc::new(RefCell::new(module))
};
{
let _guard = crate::compile::COMPILE_LOCK.lock().unwrap();
let m = module.borrow();
let lisp_fn_ty = if crate::compile::EMITTED_BODY_ABI == typelisp_abi::BODY_ABI_COROUTINE {
crate::compile::llvm_builtins::coroutine_fn_type()
} else {
let ptr_ty = ctx.ptr_type(AddressSpace::default());
ctx.i64_type().fn_type(&[ptr_ty.into(), ctx.i32_type().into()], false)
};
for item in &items {
let symbol = item.symbol_name();
if m.get_function(&symbol).is_none() {
m.add_function(&symbol, lisp_fn_ty, None);
}
}
}
for item in &items {
crate::compile::driver::add_compiled_function(&interp, &mut heap, module.clone(), &item.node_name(), &item.symbol_name()).map_err(|e| e.to_string())?;
}
let all_inits: Vec<(Path, Value)> =
prelude.global_inits.iter().chain(defvar_inits.iter()).cloned().collect();
let mut global_init_names: Vec<String> = Vec::with_capacity(all_inits.len());
for (i, (_, form)) in all_inits.iter().enumerate() {
let internal_name = format!("$global_init${}", i);
crate::compile::driver::add_compiled_global_init(&interp, &mut heap, module.clone(), &internal_name, *form).map_err(|e| e.to_string())?;
global_init_names.push(internal_name);
}
let mut print_objects: Vec<(String, String)> = items
.iter()
.filter_map(|item| Some((print_object_key(&item.node_name())?, item.symbol_name())))
.collect();
{
let m = module.borrow();
let mut prelude_methods: Vec<(String, String)> = m
.get_functions()
.filter(|f| f.count_basic_blocks() > 0)
.filter_map(|f| {
let symbol = f.get_name().to_str().ok()?.to_string();
let node = symbol.strip_prefix(crate::compile::USER_SYMBOL_PREFIX)?;
Some((print_object_key(node)?, symbol))
})
.filter(|(key, _)| !print_objects.iter().any(|(k, _)| k == key))
.collect();
prelude_methods.sort();
print_objects.extend(prelude_methods);
}
let format_calls: Vec<(String, String, String)> = chk
.format_call_methods()
.into_iter()
.map(|(path, method)| {
let node = format!("{}::{}", path, method);
let symbol = items
.iter()
.find(|item| item.node_name() == node)
.map(|item| item.symbol_name())
.ok_or_else(|| format!(
"compile-file: `~/{}/` names `{}`, which was not compiled into this file",
method, node
))?;
Ok((crate::type_key::type_key_of(&path).into_owned(), method, symbol))
})
.collect::<Result<_, String>>()?;
let eval_globals: Vec<(String, usize)> = all_inits
.iter()
.map(|(path, _)| {
let id = interp
.compiled_global_id(path)
.ok_or_else(|| format!("internal error: global `{}` was collected but never promoted", path))?;
Ok((path.to_string(), id))
})
.collect::<Result<_, String>>()?;
let printer_globals: Vec<(&str, usize)> = typelisp_print::runtime::PRINTER_GLOBALS
.iter()
.map(|name| {
let id = interp
.compiled_global_id(&Path::root(name))
.ok_or_else(|| format!("internal error: printer variable {} was never promoted", name))?;
Ok((*name, id))
})
.collect::<Result<_, String>>()?;
let calls_eval = {
let _guard = crate::compile::COMPILE_LOCK.lock().unwrap();
let m = module.borrow();
module_calls_any(&m, &EVAL_SHIMS)
};
let eval_env: Option<Vec<u8>> = if calls_eval {
let mut env_heap = Heap::with_capacity(EVAL_HEAP_CAPACITY);
Some(crate::compile::dump::capture_program_dump(&mut env_heap, &source, &src_root, &entry_segs, &eval_globals)?)
} else {
None
};
let main_returns_int = chk
.registry()
.fn_sig(&entry_path)
.is_some_and(|sig| sig.ret == crate::Type::Int);
let eval_ns = entry_segs.join("::");
let _guard = crate::compile::COMPILE_LOCK.lock().unwrap();
let result = {
let m = module.borrow();
let mut callback_entries = Vec::new();
for (i, key) in chk.ffi_callback_keys().into_iter().enumerate() {
let sig = typelisp_front::ffi_callback::CallbackSig::parse(&key)?;
let entry = crate::compile::ffi::emit_callback_entry_in_module(&m, &format!("tl_ffi_callback_entry${}", i), &sig)?;
callback_entries.push((key, entry));
}
build_main_wrapper(
ctx,
&m,
&CompiledItem::Fn(entry_path.clone()).symbol_name(),
&global_init_names,
&interp.vtable_descriptors(),
&interp.upcast_descriptors(),
&interp.enum_variant_descriptors(),
&interp.field_template_descriptors(),
&interp.field_name_descriptors(),
&print_objects,
&format_calls,
&printer_globals,
&callback_entries,
eval_env.as_deref().map(|bytes| (bytes, eval_ns.as_str())),
main_returns_int,
)
.and_then(|()| m.verify().map_err(|e| format!("module failed verification: {}", e)))
.and_then(|()| write_executable(&m, output_path, &link_libraries))
};
drop(module);
result
}
fn print_object_key(node: &str) -> Option<String> {
let (type_name, targs) = match node.rsplit_once("::print-object") {
Some((ty, "")) => (ty, None),
Some((ty, rest)) => {
let args = rest.strip_prefix(" <")?.strip_suffix('>')?;
(ty, Some(args))
}
None => return None,
};
let path = Path::from_segments(type_name.split("::").map(str::to_string).collect());
let base = crate::type_key::type_key_of(&path).into_owned();
Some(match targs {
Some(args) => format!("{}<{}>", base, args),
None => base,
})
}
fn build_main_wrapper(
ctx: &'static Context,
module: &Module<'static>,
entry_symbol: &str,
global_init_names: &[String],
vtables: &[(u32, Vec<(Path, String)>)],
upcasts: &[(u32, u32, u32)],
enum_variants: &[(String, usize, String)],
field_templates: &[(String, i64, i64, String)],
field_names: &[(String, i64, String)],
print_objects: &[(String, String)],
format_calls: &[(String, String, String)],
printer_globals: &[(&str, usize)],
callback_entries: &[(String, inkwell::values::FunctionValue<'static>)],
eval_env: Option<(&[u8], &str)>,
main_returns_int: bool,
) -> Result<(), String> {
let tl_main = module
.get_function(entry_symbol)
.ok_or_else(|| "internal error: compiled entry point not found in module".to_string())?;
let i32_type = ctx.i32_type();
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let fn_ty = ctx.i64_type().fn_type(&[ptr_ty.into(), ctx.i32_type().into()], false);
let rt_heap_init = module.add_function("rt_heap_init", fn_ty, None);
let main_fn = module.add_function(ENTRY_POINT_NAME, i32_type.fn_type(&[], false), None);
let entry_block = ctx.append_basic_block(main_fn, "entry");
let builder = ctx.create_builder();
builder.position_at_end(entry_block);
let null_args = ctx.ptr_type(AddressSpace::default()).const_null();
let argc_zero = ctx.i32_type().const_int(0, false);
if !vtables.is_empty() {
let i64_ty = ctx.i64_type();
let rt_vtable_set = module
.get_function("rt_vtable_set")
.ok_or_else(|| "internal error: rt_vtable_set not declared in module".to_string())?;
let args_ptr = builder
.build_alloca(i64_ty.array_type(4), "vtable_set_args")
.map_err(|e| format!("failed to alloca vtable-set args: {}", e))?;
for (id, slots) in vtables {
for (slot, (type_name, method)) in slots.iter().enumerate() {
let symbol = crate::compile::symbols::user_method_symbol_name(type_name, method);
let target = module.get_function(&symbol).ok_or_else(|| {
format!(
"compile-file: dyn dispatch target `{}::{}` was not compiled into this file",
type_name, method
)
})?;
let fn_ptr = target.as_global_value().as_pointer_value().const_to_int(i64_ty);
let body_abi = if target.get_type() == crate::compile::llvm_builtins::coroutine_fn_type() {
typelisp_abi::BODY_ABI_COROUTINE
} else {
typelisp_abi::BODY_ABI_CLASSIC
};
let set_args = [
i64_ty.const_int(*id as u64, false),
i64_ty.const_int(slot as u64, false),
fn_ptr,
i64_ty.const_int(u64::from(body_abi), false),
];
for (i, v) in set_args.iter().enumerate() {
let v = *v;
let p = unsafe {
builder
.build_gep(i64_ty, args_ptr, &[i64_ty.const_int(i as u64, false)], "vtable_set_arg_ptr")
.map_err(|e| format!("failed to build vtable-set gep: {}", e))?
};
builder.build_store(p, v).map_err(|e| format!("failed to store vtable-set arg: {}", e))?;
}
builder
.build_call(rt_vtable_set, &[args_ptr.into(), ctx.i32_type().const_int(4, false).into()], "vtable_set_result")
.map_err(|e| format!("failed to build rt_vtable_set call: {}", e))?;
}
}
}
if !upcasts.is_empty() {
let i64_ty = ctx.i64_type();
let rt_upcast_set = module
.get_function("rt_upcast_set")
.ok_or_else(|| "internal error: rt_upcast_set not declared in module".to_string())?;
let args_ptr = builder
.build_alloca(i64_ty.array_type(3), "upcast_set_args")
.map_err(|e| format!("failed to alloca upcast-set args: {}", e))?;
for (from, trait_id, to) in upcasts {
let set_args = [
i64_ty.const_int(*from as u64, false),
i64_ty.const_int(*trait_id as u64, false),
i64_ty.const_int(*to as u64, false),
];
for (i, v) in set_args.iter().enumerate() {
let v = *v;
let p = unsafe {
builder
.build_gep(i64_ty, args_ptr, &[i64_ty.const_int(i as u64, false)], "upcast_set_arg_ptr")
.map_err(|e| format!("failed to build upcast-set gep: {}", e))?
};
builder.build_store(p, v).map_err(|e| format!("failed to store upcast-set arg: {}", e))?;
}
builder
.build_call(rt_upcast_set, &[args_ptr.into(), ctx.i32_type().const_int(3, false).into()], "upcast_set_result")
.map_err(|e| format!("failed to build rt_upcast_set call: {}", e))?;
}
}
for (i, (key, entry)) in callback_entries.iter().enumerate() {
let i64_ty = ctx.i64_type();
let g = builder
.build_global_string_ptr(key, &format!("ffi_callback_key${}", i))
.map_err(|e| format!("failed to build a callback key: {}", e))?;
let words = [
g.as_pointer_value().const_to_int(i64_ty),
i64_ty.const_int(key.len() as u64, false),
entry.as_global_value().as_pointer_value().const_to_int(i64_ty),
];
call_shim(ctx, module, &builder, "rt_ffi_callback_register", &words)?;
}
if module_calls_any(module, &PRINT_SHIMS) {
let i64_ty = ctx.i64_type();
let literal = |builder: &inkwell::builder::Builder<'static>, text: &str| -> Result<(inkwell::values::IntValue<'static>, inkwell::values::IntValue<'static>), String> {
let g = builder
.build_global_string_ptr(text, "print_reg_str")
.map_err(|e| format!("failed to build a printer-registration string: {}", e))?;
Ok((
g.as_pointer_value().const_to_int(i64_ty),
i64_ty.const_int(text.len() as u64, false),
))
};
let call = |builder: &inkwell::builder::Builder<'static>, name: &str, words: &[inkwell::values::IntValue<'static>]| -> Result<(), String> {
let f = module
.get_function(name)
.ok_or_else(|| format!("internal error: {} not declared in module", name))?;
let args_ptr = builder
.build_alloca(i64_ty.array_type(words.len() as u32), "print_reg_args")
.map_err(|e| format!("failed to alloca printer-registration args: {}", e))?;
for (i, w) in words.iter().enumerate() {
let p = unsafe {
builder
.build_gep(i64_ty, args_ptr, &[i64_ty.const_int(i as u64, false)], "print_reg_arg_ptr")
.map_err(|e| format!("failed to build printer-registration gep: {}", e))?
};
builder.build_store(p, *w).map_err(|e| format!("failed to store printer-registration arg: {}", e))?;
}
builder
.build_call(f, &[args_ptr.into(), ctx.i32_type().const_int(words.len() as u32 as u64, false).into()], "print_reg_result")
.map_err(|e| format!("failed to build {} call: {}", name, e))?;
Ok(())
};
for (key, variant, name) in enum_variants {
let (key_ptr, key_len) = literal(&builder, key)?;
let (name_ptr, name_len) = literal(&builder, name)?;
call(
&builder,
"rt_print_enum_variant",
&[key_ptr, key_len, i64_ty.const_int(*variant as u64, false), name_ptr, name_len],
)?;
}
for (key, variant, index, template) in field_templates {
let (key_ptr, key_len) = literal(&builder, key)?;
let (t_ptr, t_len) = literal(&builder, template)?;
call(
&builder,
"rt_print_field_template",
&[key_ptr, key_len, i64_ty.const_int(*variant as u64, true), i64_ty.const_int(*index as u64, true), t_ptr, t_len],
)?;
}
for (key, index, name) in field_names {
let (key_ptr, key_len) = literal(&builder, key)?;
let (name_ptr, name_len) = literal(&builder, name)?;
call(&builder, "rt_print_field_name", &[key_ptr, key_len, i64_ty.const_int(*index as u64, true), name_ptr, name_len])?;
}
for (key, symbol) in print_objects {
let target = module.get_function(symbol).ok_or_else(|| {
format!("compile-file: `print-object` implementation `{}` was not compiled into this file", symbol)
})?;
let (key_ptr, key_len) = literal(&builder, key)?;
let fn_ptr = classic_door(ctx, module, target)?;
call(&builder, "rt_print_object_method", &[key_ptr, key_len, fn_ptr])?;
}
for (key, method, symbol) in format_calls {
let target = module.get_function(symbol).ok_or_else(|| {
format!("compile-file: `~/{}/` names `{}`, which was not compiled into this file", method, symbol)
})?;
let (key_ptr, key_len) = literal(&builder, key)?;
let (name_ptr, name_len) = literal(&builder, method)?;
let fn_ptr = classic_door(ctx, module, target)?;
call(&builder, "rt_format_call_method", &[key_ptr, key_len, name_ptr, name_len, fn_ptr])?;
}
for (name, id) in printer_globals {
let (name_ptr, name_len) = literal(&builder, name)?;
call(&builder, "rt_print_global", &[name_ptr, name_len, i64_ty.const_int(*id as u64, false)])?;
}
}
if let Some((bytes, ns)) = eval_env {
let i64_ty = ctx.i64_type();
let blob = module.add_global(ctx.i8_type().array_type(bytes.len() as u32), None, "typelisp_eval_env");
blob.set_initializer(&ctx.const_string(bytes, false));
blob.set_constant(true);
let ns_str = builder
.build_global_string_ptr(ns, "typelisp_eval_ns")
.map_err(|e| format!("failed to build the eval namespace string: {}", e))?;
call_shim(
ctx,
module,
&builder,
"rt_eval_state",
&[
blob.as_pointer_value().const_to_int(i64_ty),
i64_ty.const_int(bytes.len() as u64, false),
ns_str.as_pointer_value().const_to_int(i64_ty),
i64_ty.const_int(ns.len() as u64, false),
],
)?;
}
match eval_env {
None => {
builder
.build_call(rt_heap_init, &[null_args.into(), argc_zero.into()], "heap_init_result")
.map_err(|e| format!("failed to build rt_heap_init call: {}", e))?;
}
Some(_) => {
call_shim(ctx, module, &builder, "rt_heap_init", &[ctx.i64_type().const_int(EVAL_HEAP_CAPACITY as u64, false)])?;
}
}
if eval_env.is_some() {
call_shim(ctx, module, &builder, "rt_eval_init", &[])?;
}
let coroutine_fn_ty = crate::compile::llvm_builtins::coroutine_fn_type();
let i64_ty = ctx.i64_type();
let mut init_addrs: Vec<inkwell::values::IntValue<'static>> = Vec::with_capacity(global_init_names.len());
for name in global_init_names {
let f = module
.get_function(name)
.ok_or_else(|| format!("internal error: global-init function \"{}\" not found in module", name))?;
if f.get_type() != coroutine_fn_ty {
return Err(format!(
"internal error: global initialiser \"{}\" is not a coroutine-ABI body, and only those run under the scheduler",
name
));
}
init_addrs.push(f.as_global_value().as_pointer_value().const_to_int(i64_ty));
}
let entry_addr = tl_main.as_global_value().as_pointer_value().const_to_int(i64_ty);
let call: CallSiteValue = if tl_main.get_type() == coroutine_fn_ty {
let entry_shim = match (eval_env.is_some(), main_returns_int) {
(true, true) => "rt_run_program_interp_int",
(true, false) => "rt_run_program_interp",
(false, true) => "rt_run_program_int",
(false, false) => "rt_run_program",
};
let rt_run_program = module.add_function(entry_shim, fn_ty, None);
let inits = module.add_global(i64_ty.array_type(init_addrs.len() as u32), None, "tl_global_inits");
inits.set_initializer(&i64_ty.const_array(&init_addrs));
inits.set_constant(true);
let inits_addr = inits.as_pointer_value().const_to_int(i64_ty);
let words = [inits_addr, i64_ty.const_int(init_addrs.len() as u64, false), entry_addr];
let args_ptr = builder
.build_alloca(i64_ty.array_type(words.len() as u32), "program_args")
.map_err(|e| format!("failed to alloca the program arguments: {}", e))?;
for (i, w) in words.iter().enumerate() {
let slot = unsafe {
builder
.build_gep(i64_ty, args_ptr, &[i64_ty.const_int(i as u64, false)], "program_arg_ptr")
.map_err(|e| format!("failed to build the program argument gep: {}", e))?
};
builder.build_store(slot, *w).map_err(|e| format!("failed to store a program argument: {}", e))?;
}
builder
.build_call(rt_run_program, &[args_ptr.into(), ctx.i32_type().const_int(words.len() as u64, false).into()], "tl_main_result")
.map_err(|e| format!("failed to build entry-point call: {}", e))?
} else {
if main_returns_int {
return Err("an `int`-returning main under the classic ABI is not supported".to_string());
}
if !init_addrs.is_empty() {
return Err("internal error: a classic-ABI entry point cannot run global initialisers (no scheduler)".to_string());
}
let rt_run_entry = module.add_function("rt_run_entry", i64_ty.fn_type(&[i64_ty.into()], false), None);
builder
.build_call(rt_run_entry, &[entry_addr.into()], "tl_main_result")
.map_err(|e| format!("failed to build entry-point call: {}", e))?
};
let result = match call.try_as_basic_value() {
inkwell::values::ValueKind::Basic(v) => v.into_int_value(),
inkwell::values::ValueKind::Instruction(_) => {
return Err("internal error: entry point produced no value".to_string())
}
};
let exit_code = builder
.build_int_truncate(result, i32_type, "exit_code")
.map_err(|e| format!("failed to truncate exit code: {}", e))?;
builder.build_return(Some(&exit_code)).map_err(|e| format!("failed to build entry-point return: {}", e))?;
Ok(())
}
const EVAL_HEAP_CAPACITY: usize = 1 << 18;
const EVAL_SHIMS: [&str; 3] = ["rt_eval", "rt_set_macro_character", "rt_set_dispatch_macro_character"];
fn call_shim(
ctx: &'static Context,
module: &Module<'static>,
builder: &inkwell::builder::Builder<'static>,
name: &str,
words: &[inkwell::values::IntValue<'static>],
) -> Result<(), String> {
let i64_ty = ctx.i64_type();
let f = module
.get_function(name)
.ok_or_else(|| format!("internal error: {} not declared in module", name))?;
let args_ptr = if words.is_empty() {
ctx.ptr_type(AddressSpace::default()).const_null()
} else {
let p = builder
.build_alloca(i64_ty.array_type(words.len() as u32), "shim_args")
.map_err(|e| format!("failed to alloca {} args: {}", name, e))?;
for (i, w) in words.iter().enumerate() {
let slot = unsafe {
builder
.build_gep(i64_ty, p, &[i64_ty.const_int(i as u64, false)], "shim_arg_ptr")
.map_err(|e| format!("failed to build {} gep: {}", name, e))?
};
builder.build_store(slot, *w).map_err(|e| format!("failed to store {} arg: {}", name, e))?;
}
p
};
builder
.build_call(f, &[args_ptr.into(), ctx.i32_type().const_int(words.len() as u64, false).into()], "shim_result")
.map_err(|e| format!("failed to build {} call: {}", name, e))?;
Ok(())
}
const PRINT_SHIMS: [&str; 11] = [
"rt_format",
"rt_print",
"rt_println",
"rt_pprint",
"rt_pprint_block_start",
"rt_pprint_block_end",
"rt_pprint_newline",
"rt_pprint_indent",
"rt_pprint_tab",
"rt_pprint_pop",
"rt_pprint_list_exhausted",
];
fn module_calls_any(module: &Module<'static>, names: &[&str]) -> bool {
let mut f = module.get_first_function();
while let Some(func) = f {
for block in func.get_basic_blocks() {
let mut instr = block.get_first_instruction();
while let Some(i) = instr {
if i.get_opcode() == InstructionOpcode::Call {
let n = i.get_num_operands();
if let Some(Operand::Value(v)) = n.checked_sub(1).and_then(|last| i.get_operand(last)) {
if v.is_pointer_value() {
let name = v.into_pointer_value().get_name().to_string_lossy().into_owned();
if names.contains(&name.as_str()) {
return true;
}
}
}
}
instr = i.get_next_instruction();
}
}
f = func.get_next_function();
}
false
}
fn link_archive() -> Result<std::path::PathBuf, String> {
if let Some(dir) = LIB_DIR.get() {
return Ok(dir.join(STATICLIB_NAME));
}
default_archive()
}
#[cfg(typelisp_bundled_runtime)]
static BUNDLED_STATICLIB: &[u8] = include_bytes!(env!("TYPELISP_BUNDLED_STATICLIB"));
#[cfg(typelisp_bundled_runtime)]
fn default_archive() -> Result<std::path::PathBuf, String> {
write_archive_once(&bundled_archive_dir()?, BUNDLED_STATICLIB)
}
#[cfg(not(typelisp_bundled_runtime))]
fn default_archive() -> Result<std::path::PathBuf, String> {
Ok(std::path::PathBuf::from(env!("TYPELISP_PROFILE_DIR")).join(STATICLIB_NAME))
}
#[cfg(typelisp_bundled_runtime)]
fn bundled_archive_dir() -> Result<std::path::PathBuf, String> {
Ok(typelisp_home()?.join("lib").join(env!("TYPELISP_BUNDLED_STATICLIB_ID")))
}
fn typelisp_home() -> Result<std::path::PathBuf, String> {
if let Some(home) = std::env::var_os("TYPELISP_HOME").filter(|h| !h.is_empty()) {
return Ok(std::path::PathBuf::from(home));
}
match std::env::var_os("HOME").filter(|h| !h.is_empty()) {
Some(home) => Ok(std::path::PathBuf::from(home).join(".typelisp")),
None => Err("neither TYPELISP_HOME nor HOME is set, so there is nowhere to put the library \
compiled executables link; set TYPELISP_HOME or pass --lib-dir"
.to_string()),
}
}
#[cfg(any(typelisp_bundled_runtime, test))]
fn write_archive_once(dir: &std::path::Path, bytes: &[u8]) -> Result<std::path::PathBuf, String> {
let path = dir.join(STATICLIB_NAME);
if path.is_file() {
return Ok(path);
}
fs::create_dir_all(dir).map_err(|e| format!("failed to create {}: {}", dir.display(), e))?;
static WRITES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let partial = dir.join(format!(
".{}.{}.{}",
STATICLIB_NAME,
std::process::id(),
WRITES.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
if let Err(e) = fs::write(&partial, bytes) {
let _ = fs::remove_file(&partial);
return Err(format!("failed to write {}: {}", partial.display(), e));
}
fs::rename(&partial, &path).map_err(|e| {
let _ = fs::remove_file(&partial);
format!("failed to move {} to {}: {}", partial.display(), path.display(), e)
})?;
Ok(path)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RemoveLib {
This,
Others,
All,
}
pub fn remove_written_archives(which: RemoveLib) -> Result<Vec<std::path::PathBuf>, String> {
let own = own_build_id();
if own.is_none() && which != RemoveLib::All {
return Err("this typl is a debug build: it links the library in the tree it was built in, \
has written none and has no build id of its own; --all removes every build's"
.to_string());
}
remove_archives_under(&typelisp_home()?.join("lib"), own, which)
}
fn remove_archives_under(
root: &std::path::Path,
own: Option<&str>,
which: RemoveLib,
) -> Result<Vec<std::path::PathBuf>, String> {
let entries = match fs::read_dir(root) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(format!("failed to read {}: {}", root.display(), e)),
};
let mut removed = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("failed to read {}: {}", root.display(), e))?;
let name = entry.file_name();
let Some(name) = name.to_str().filter(|n| is_build_id(n)) else { continue };
let is_own = own == Some(name);
let chosen = match which {
RemoveLib::This => is_own,
RemoveLib::Others => !is_own,
RemoveLib::All => true,
};
let path = entry.path();
if !chosen || !path.is_dir() {
continue;
}
fs::remove_dir_all(&path).map_err(|e| format!("failed to remove {}: {}", path.display(), e))?;
removed.push(path);
}
removed.sort();
Ok(removed)
}
#[cfg(typelisp_bundled_runtime)]
fn own_build_id() -> Option<&'static str> {
Some(env!("TYPELISP_BUNDLED_STATICLIB_ID"))
}
#[cfg(not(typelisp_bundled_runtime))]
fn own_build_id() -> Option<&'static str> {
None
}
fn is_build_id(name: &str) -> bool {
name.len() == 16 && name.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
pub const STATICLIB_NAME: &str = "libtypelisp_front.a";
static LIB_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
pub fn set_lib_dir(dir: std::path::PathBuf) -> Result<(), String> {
if !dir.join(STATICLIB_NAME).is_file() {
return Err(format!("{} has no {}", dir.display(), STATICLIB_NAME));
}
LIB_DIR.set(dir).map_err(|_| "the library folder is already set".to_string())
}
fn host_target_machine() -> Result<TargetMachine, String> {
Target::initialize_native(&InitializationConfig::default())
.map_err(|e| format!("failed to initialize native target: {}", e))?;
let triple = host_triple()?;
let target = Target::from_triple(&triple).map_err(|e| e.to_string())?;
target
.create_target_machine(
&triple,
&TargetMachine::get_host_cpu_name().to_string(),
&TargetMachine::get_host_cpu_features().to_string(),
OptimizationLevel::None,
reloc_mode(),
CodeModel::Default,
)
.ok_or_else(|| "failed to create a target machine for the host triple".to_string())
}
#[cfg(target_os = "macos")]
fn host_triple() -> Result<TargetTriple, String> {
let default = TargetMachine::get_default_triple();
let default = default.as_str().to_string_lossy();
let arch = default
.split_once("-apple-")
.map(|(arch, _)| arch)
.ok_or_else(|| format!("internal error: the host triple `{}` is not an Apple one", default))?;
match macos_version_min() {
Some(version) => Ok(TargetTriple::create(&format!("{}-apple-macosx{}", arch, version))),
None => Ok(TargetMachine::get_default_triple()),
}
}
#[cfg(not(target_os = "macos"))]
fn host_triple() -> Result<TargetTriple, String> {
Ok(TargetMachine::get_default_triple())
}
#[cfg(target_os = "linux")]
fn reloc_mode() -> RelocMode {
RelocMode::PIC
}
#[cfg(not(target_os = "linux"))]
fn reloc_mode() -> RelocMode {
RelocMode::Default
}
#[cfg(target_os = "linux")]
const SYSTEM_LIBRARIES: &[&str] = &["-lgcc_s", "-lutil", "-lrt", "-lpthread", "-lm", "-ldl"];
#[cfg(not(target_os = "linux"))]
const SYSTEM_LIBRARIES: &[&str] = &[];
#[cfg(target_os = "macos")]
fn macos_version_min() -> Option<&'static str> {
option_env!("TYPELISP_MACOSX_DEPLOYMENT_TARGET")
}
pub(crate) fn assembly_of(module: &Module<'static>) -> Result<String, String> {
let tm = host_target_machine()?;
let buf = tm
.write_to_memory_buffer(module, FileType::Assembly)
.map_err(|e| format!("failed to emit assembly: {}", e))?;
String::from_utf8(buf.as_slice().to_vec()).map_err(|e| format!("the assembler emitted invalid UTF-8: {}", e))
}
fn classic_door(
ctx: &'static Context,
module: &Module<'static>,
target: inkwell::values::FunctionValue<'static>,
) -> Result<inkwell::values::IntValue<'static>, String> {
let i64_ty = ctx.i64_type();
if target.get_type() != crate::compile::llvm_builtins::coroutine_fn_type() {
return Ok(target.as_global_value().as_pointer_value().const_to_int(i64_ty));
}
let name = format!("{}$classic", target.get_name().to_string_lossy());
if let Some(existing) = module.get_function(&name) {
return Ok(existing.as_global_value().as_pointer_value().const_to_int(i64_ty));
}
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let classic_ty = i64_ty.fn_type(&[ptr_ty.into(), ctx.i32_type().into()], false);
let door = module.add_function(&name, classic_ty, None);
let drive = match module.get_function("rt_drive_body") {
Some(f) => f,
None => module.add_function("rt_drive_body", classic_ty, None),
};
let builder = ctx.create_builder();
builder.position_at_end(ctx.append_basic_block(door, "entry"));
let args = builder
.build_alloca(i64_ty.array_type(3), "drive_args")
.map_err(|e| format!("failed to alloca the driver arguments: {}", e))?;
let words = [
target.as_global_value().as_pointer_value().const_to_int(i64_ty),
builder
.build_ptr_to_int(
door.get_nth_param(0).expect("the classic signature has two parameters").into_pointer_value(),
i64_ty,
"callee_args",
)
.map_err(|e| format!("failed to take the argument array's address: {}", e))?,
builder
.build_int_z_extend(
door.get_nth_param(1).expect("the classic signature has two parameters").into_int_value(),
i64_ty,
"callee_argc",
)
.map_err(|e| format!("failed to widen the argument count: {}", e))?,
];
for (i, w) in words.iter().enumerate() {
let p = unsafe {
builder
.build_gep(i64_ty, args, &[i64_ty.const_int(i as u64, false)], "drive_arg_ptr")
.map_err(|e| format!("failed to index the driver arguments: {}", e))?
};
builder.build_store(p, *w).map_err(|e| format!("failed to store a driver argument: {}", e))?;
}
let out = builder
.build_call(drive, &[args.into(), ctx.i32_type().const_int(3, false).into()], "drive_result")
.map_err(|e| format!("failed to build the rt_drive_body call: {}", e))?;
let out = match out.try_as_basic_value() {
inkwell::values::ValueKind::Basic(v) => v.into_int_value(),
inkwell::values::ValueKind::Instruction(_) => {
return Err("internal error: rt_drive_body produced no value".to_string())
}
};
builder.build_return(Some(&out)).map_err(|e| format!("failed to build the door's return: {}", e))?;
Ok(door.as_global_value().as_pointer_value().const_to_int(i64_ty))
}
fn write_executable(module: &Module<'static>, output_path: &str, libraries: &[String]) -> Result<(), String> {
if let Ok(path) = std::env::var("TYPELISP_AOT_IR") {
std::fs::write(&path, module.print_to_string().to_string())
.map_err(|e| format!("failed to write {}: {}", path, e))?;
}
let target_machine = host_target_machine()?;
let object_path = format!("{}.o", output_path);
target_machine
.write_to_file(module, FileType::Object, std::path::Path::new(&object_path))
.map_err(|e| format!("failed to emit object file: {}", e))?;
let archive = match link_archive() {
Ok(archive) => archive,
Err(e) => {
let _ = fs::remove_file(&object_path);
return Err(e);
}
};
let mut link = Command::new("cc");
#[cfg(target_os = "macos")]
if let Some(version) = macos_version_min() {
link.arg(format!("-mmacosx-version-min={}", version));
}
let status = link
.arg(&object_path)
.arg(&archive)
.args(libraries.iter().map(|l| format!("-l{}", l)))
.args(SYSTEM_LIBRARIES)
.arg("-o")
.arg(output_path)
.status()
.map_err(|e| format!("failed to invoke the system linker (`cc`): {}", e));
let _ = fs::remove_file(&object_path);
let status = status?;
if !status.success() {
return Err(format!("linker failed with status {}", status));
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::process::Command;
use inkwell::AddressSpace;
use super::{build_main_wrapper, remove_archives_under, write_archive_once, write_executable, RemoveLib, STATICLIB_NAME};
use crate::compile::{llvm_context, COMPILE_LOCK};
fn tmp_path(name: &str) -> PathBuf {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("aot-test-tmp");
std::fs::create_dir_all(&dir).expect("failed to create the AOT test scratch dir");
dir.join(name)
}
#[test]
fn remove_lib_removes_the_build_ids_it_names_and_nothing_else() {
let root = tmp_path("remove-lib");
let (own, old, older) = ("00000000000000aa", "00000000000000bb", "00000000000000cc");
let fill = || {
let _ = std::fs::remove_dir_all(&root);
for id in [own, old, older, "not-a-build-id"] {
write_archive_once(&root.join(id), b"archive").expect("the archive is written");
}
};
let left = || {
let mut names: Vec<String> =
std::fs::read_dir(&root).expect("the folder is there").map(|e| e.expect("an entry").file_name().into_string().expect("utf-8")).collect();
names.sort();
names
};
let removed = |which| -> Vec<String> {
remove_archives_under(&root, Some(own), which)
.expect("removal succeeds")
.iter()
.map(|p| p.file_name().expect("a name").to_str().expect("utf-8").to_string())
.collect()
};
fill();
assert_eq!(removed(RemoveLib::This), vec![own]);
assert_eq!(left(), vec![old, older, "not-a-build-id"]);
assert_eq!(removed(RemoveLib::This), Vec::<String>::new());
fill();
assert_eq!(removed(RemoveLib::Others), vec![old, older]);
assert_eq!(left(), vec![own, "not-a-build-id"]);
fill();
assert_eq!(removed(RemoveLib::All), vec![own, old, older]);
assert_eq!(left(), vec!["not-a-build-id"]);
std::fs::remove_dir_all(&root).expect("the folder can be removed");
assert_eq!(removed(RemoveLib::All), Vec::<String>::new());
}
#[test]
fn the_archive_is_written_once_and_left_in_place() {
let dir = tmp_path("archive-once");
let _ = std::fs::remove_dir_all(&dir);
let first = write_archive_once(&dir, b"first").expect("the first write succeeds");
assert_eq!(first, dir.join(STATICLIB_NAME));
let second = write_archive_once(&dir, b"second").expect("the second call succeeds");
assert_eq!(second, first);
assert_eq!(std::fs::read(&first).expect("the archive is there"), b"first");
let names: Vec<_> = std::fs::read_dir(&dir).expect("the folder is there").map(|e| e.expect("an entry").file_name()).collect();
assert_eq!(names, vec![std::ffi::OsString::from(STATICLIB_NAME)]);
std::fs::remove_dir_all(&dir).expect("the folder can be removed");
}
#[test]
fn aot_output_can_call_an_rt_extern_function() {
let ctx = llvm_context();
let _guard = COMPILE_LOCK.lock().unwrap();
let module = ctx.create_module("rt_ping_test");
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let fn_ty = ctx.i64_type().fn_type(&[ptr_ty.into(), ctx.i32_type().into()], false);
let rt_ping = module.add_function("rt_ping", fn_ty, None);
let tl_main = module.add_function("tl_main", fn_ty, None);
let builder = ctx.create_builder();
let entry = ctx.append_basic_block(tl_main, "entry");
builder.position_at_end(entry);
let one_slot = builder.build_alloca(ctx.i64_type(), "one_slot").unwrap();
builder.build_store(one_slot, ctx.i64_type().const_int(41, false)).unwrap();
let argc_one = ctx.i32_type().const_int(1, false);
let call = builder.build_call(rt_ping, &[one_slot.into(), argc_one.into()], "rt_ping_result").unwrap();
let result = match call.try_as_basic_value() {
inkwell::values::ValueKind::Basic(v) => v.into_int_value(),
inkwell::values::ValueKind::Instruction(_) => panic!("rt_ping call produced no value"),
};
let exit_ty = ctx.void_type().fn_type(&[ctx.i32_type().into()], false);
let exit = module.add_function("exit", exit_ty, None);
let code = builder.build_int_truncate(result, ctx.i32_type(), "code").unwrap();
builder.build_call(exit, &[code.into()], "").unwrap();
builder.build_unreachable().unwrap();
build_main_wrapper(ctx, &module, "tl_main", &[], &[], &[], &[], &[], &[], &[], &[], &[], &[], None, false).expect("build_main_wrapper failed");
module.verify().expect("module failed verification");
let out_path = tmp_path("rt_ping_test");
write_executable(&module, out_path.to_str().unwrap(), &[]).expect("write_executable failed");
let status = Command::new(&out_path).status().expect("failed to run the compiled executable");
assert_eq!(status.code(), Some(42));
}
#[test]
fn aot_main_wrapper_initializes_a_heap_before_tl_main_runs() {
let ctx = llvm_context();
let _guard = COMPILE_LOCK.lock().unwrap();
let module = ctx.create_module("rt_heap_init_test");
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let fn_ty = ctx.i64_type().fn_type(&[ptr_ty.into(), ctx.i32_type().into()], false);
let rt_heap_live_count = module.add_function("rt_heap_live_count", fn_ty, None);
let tl_main = module.add_function("tl_main", fn_ty, None);
let builder = ctx.create_builder();
let entry = ctx.append_basic_block(tl_main, "entry");
builder.position_at_end(entry);
let null_args = ptr_ty.const_null();
let argc_zero = ctx.i32_type().const_int(0, false);
let call = builder.build_call(rt_heap_live_count, &[null_args.into(), argc_zero.into()], "live_count").unwrap();
let result = match call.try_as_basic_value() {
inkwell::values::ValueKind::Basic(v) => v.into_int_value(),
inkwell::values::ValueKind::Instruction(_) => panic!("rt_heap_live_count call produced no value"),
};
builder.build_return(Some(&result)).unwrap();
build_main_wrapper(ctx, &module, "tl_main", &[], &[], &[], &[], &[], &[], &[], &[], &[], &[], None, false).expect("build_main_wrapper failed");
module.verify().expect("module failed verification");
let out_path = tmp_path("rt_heap_init_test");
write_executable(&module, out_path.to_str().unwrap(), &[]).expect("write_executable failed");
let status = Command::new(&out_path).status().expect("failed to run the compiled executable");
assert_eq!(status.code(), Some(0));
}
}