diff --git a/compiler/llvm_backend/src/codegen.rs b/compiler/llvm_backend/src/codegen.rs index 90fd5916..26655b60 100644 --- a/compiler/llvm_backend/src/codegen.rs +++ b/compiler/llvm_backend/src/codegen.rs @@ -124,8 +124,7 @@ impl<'a> CodegenEngine<'a> { let dest_block = self.ctx.get_block(*dest).unwrap(); LLVMBuildBr(self.ctx.builder, dest_block); } - TerminatorKind::Throw(_) => { - return Err("Exception handling (throw) is not yet implemented in the LLVM backend".to_string()); +< } TerminatorKind::ConditionalJump { cond, @@ -1057,7 +1056,113 @@ impl<'a> CodegenEngine<'a> { CString::new("cast").unwrap().as_ptr(), ) } - Op::Try { .. } | Op::EndTry | Op::MakeDslBlock { .. } | Op::NoOp => return Ok(()), + Op::Try { catch_block, catch_var } => { + let i8_ptr_ty = LLVMPointerType(LLVMInt8TypeInContext(context), 0); + let fn_push = self.get_or_declare_runtime_fn("ts_try_push", i8_ptr_ty, &[]); + let buf_ptr = LLVMBuildCall2( + self.ctx.builder, + LLVMTypeOf(fn_push), + fn_push, + [].as_mut_ptr(), + 0, + CString::new("jmp_buf").unwrap().as_ptr(), + ); + + let setjmp_fn = self.get_or_declare_runtime_fn( + #[cfg(target_os = "windows")] + "_setjmp", + #[cfg(not(target_os = "windows"))] + "setjmp", + i32_ty, + &[i8_ptr_ty], + ); + + let res = LLVMBuildCall2( + self.ctx.builder, + LLVMTypeOf(setjmp_fn), + setjmp_fn, + [buf_ptr].as_mut_ptr(), + 1, + CString::new("setjmp_res").unwrap().as_ptr(), + ); + + let zero = LLVMConstInt(i32_ty, 0, 0); + let is_exception = LLVMBuildICmp( + self.ctx.builder, + llvm_sys::LLVMIntPredicate::LLVMIntNE, + res, + zero, + CString::new("is_exception").unwrap().as_ptr(), + ); + + // We need to split the block here because setjmp acts as a conditional branch point + let current_block = LLVMGetInsertBlock(self.ctx.builder); + let func = LLVMGetBasicBlockParent(current_block); + + let cont_block = LLVMAppendBasicBlockInContext( + context, + func, + CString::new("try_continue").unwrap().as_ptr(), + ); + + let catch_target = self.ctx.get_block(*catch_block).unwrap(); + + // We create a dispatch block for the exception path + let dispatch_block = LLVMAppendBasicBlockInContext( + context, + func, + CString::new("try_dispatch").unwrap().as_ptr(), + ); + + LLVMBuildCondBr(self.ctx.builder, is_exception, dispatch_block, cont_block); + + // exception path + LLVMPositionBuilderAtEnd(self.ctx.builder, dispatch_block); + + // Clean up the jmp_buf since we arrived here via longjmp and ts_try_pop wasn't called + let fn_free_buf = self.get_or_declare_runtime_fn("ts_try_free", LLVMVoidTypeInContext(context), &[i8_ptr_ty]); + LLVMBuildCall2( + self.ctx.builder, + LLVMTypeOf(fn_free_buf), + fn_free_buf, + [buf_ptr].as_mut_ptr(), + 1, + CString::new("").unwrap().as_ptr(), + ); + + let fn_get_ex = self.get_or_declare_runtime_fn("ts_get_exception", i8_ptr_ty, &[]); + let ex_val = LLVMBuildCall2( + self.ctx.builder, + LLVMTypeOf(fn_get_ex), + fn_get_ex, + [].as_mut_ptr(), + 0, + CString::new("ex_val").unwrap().as_ptr(), + ); + + if let Some(target) = self.ctx.get_local(*catch_var) { + LLVMBuildStore(self.ctx.builder, ex_val, target); + } + LLVMBuildBr(self.ctx.builder, catch_target); + + // continue path + LLVMPositionBuilderAtEnd(self.ctx.builder, cont_block); + + return Ok(()); + } + Op::EndTry => { + let fn_pop = self.get_or_declare_runtime_fn("ts_try_pop", LLVMVoidTypeInContext(context), &[]); + LLVMBuildCall2( + self.ctx.builder, + LLVMTypeOf(fn_pop), + fn_pop, + [].as_mut_ptr(), + 0, + CString::new("").unwrap().as_ptr(), + ); + return Ok(()); + } + Op::MakeDslBlock { .. } | Op::NoOp => return Ok(()), }; if let Some(res_id) = inst.result { diff --git a/runtime/native_runtime/src/lib.rs b/runtime/native_runtime/src/lib.rs index 3d829fcc..735bdca7 100644 --- a/runtime/native_runtime/src/lib.rs +++ b/runtime/native_runtime/src/lib.rs @@ -1018,3 +1018,109 @@ pub unsafe extern "C" fn ts_await(val: *mut TsValue) -> *mut TsValue { } val } + +use std::cell::RefCell; +use std::ffi::c_int; + +// `libc::jmp_buf` is not exposed in the `libc` crate consistently across platforms, +// so we define a generic opaque struct that is large enough and aligned. +// A size of 2048 bytes (256 * 8) is vastly larger than needed by any OS for jmp_buf. +#[repr(C, align(16))] +pub struct JmpBuf { + data: [u64; 256], +} + +extern "C" { + #[cfg_attr(target_os = "windows", link_name = "_setjmp")] + pub fn setjmp(env: *mut JmpBuf) -> c_int; + pub fn longjmp(env: *mut JmpBuf, val: c_int) -> !; +} + +thread_local! { + static TRY_STACK: RefCell> = RefCell::new(Vec::new()); + static PENDING_EXCEPTION: RefCell<*mut TsValue> = RefCell::new(std::ptr::null_mut()); +} + +#[no_mangle] +pub extern "C" fn ts_try_push() -> *mut JmpBuf { + // We allocate a jmp_buf on the heap and keep it on the stack. + let layout = std::alloc::Layout::new::(); + let ptr = unsafe { std::alloc::alloc_zeroed(layout) as *mut JmpBuf }; + TRY_STACK.with(|stack| { + stack.borrow_mut().push(ptr); + }); + ptr +} + +#[no_mangle] +pub extern "C" fn ts_try_pop() { + let ptr = TRY_STACK.with(|stack| stack.borrow_mut().pop()); + if let Some(p) = ptr { + let layout = std::alloc::Layout::new::(); + unsafe { + std::alloc::dealloc(p as *mut u8, layout); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn ts_throw(val: *mut TsValue) -> ! { + PENDING_EXCEPTION.with(|exc| { + *exc.borrow_mut() = val; + }); + + let buf_ptr = TRY_STACK.with(|stack| { + let mut s = stack.borrow_mut(); + if let Some(buf) = s.pop() { + buf + } else { + std::ptr::null_mut() + } + }); + + if buf_ptr.is_null() { + // Uncaught exception! We should abort or panic. + let msg = if val.is_null() { + "null".to_string() + } else { + let v = &*val; + if v.tag == TsTag::String as u32 { + let s = &*(v.data.pointer as *const String); + s.clone() + } else { + format!("exception tag {}", v.tag) + } + }; + eprintln!("Uncaught Exception: {}", msg); + std::process::abort(); + } + + // Call longjmp (memory leak of the jmp buf happens here if we don't deallocate first! + // But since longjmp doesn't return, we can't deallocate it after. + // It's safe to deallocate before calling longjmp since setjmp execution is fully complete and we just need the values out of the struct. Wait, longjmp uses the buf ptr so we shouldn't dealloc before longjmp. We'll leave it allocated, but wait! longjmp jumps to the setjmp frame, which can deallocate the buffer. So we should NOT deallocate in try_pop if we arrived via longjmp, OR we deallocate the buffer in the Catch block via LLVM IR calling ts_free_buf, and remove ts_try_pop. + // Instead of leaking, let's keep it simple: ts_try_pop() frees it, so if an exception is thrown, ts_try_pop is skipped (because the instruction is after the Try block), so the Catch block needs to call ts_try_pop or equivalent? + // Let's create a dedicated function to clean it up after an exception. + + // Call longjmp + longjmp(buf_ptr, 1); +} + +#[no_mangle] +pub extern "C" fn ts_try_free(buf_ptr: *mut JmpBuf) { + if !buf_ptr.is_null() { + let layout = std::alloc::Layout::new::(); + unsafe { + std::alloc::dealloc(buf_ptr as *mut u8, layout); + } + } +} + +#[no_mangle] +pub extern "C" fn ts_get_exception() -> *mut TsValue { + PENDING_EXCEPTION.with(|exc| { + let mut e = exc.borrow_mut(); + let val = *e; + *e = std::ptr::null_mut(); + val + }) +} diff --git a/test_libc b/test_libc new file mode 100755 index 00000000..8b3d1f5b Binary files /dev/null and b/test_libc differ diff --git a/test_libc.rs b/test_libc.rs new file mode 100644 index 00000000..0470b5e4 --- /dev/null +++ b/test_libc.rs @@ -0,0 +1,4 @@ +extern crate libc; +fn main() { + println!("{}", std::mem::size_of::()); +}