Skip to content

Commit 27cdd72

Browse files
wthollidayclaude
andcommitted
Surface stack interp trap reasons through the globals header
The iOS C stack interp only distinguished watchdog cancellation structurally; assertion-failed and call-stack-overflow traps were stringly set on ctx->error and printed to stderr. Host callers had no way to branch on the trap kind via read_trap_reason() the way they can on the JIT/LLVM paths. Add a numeric trap_reason to the C Ctx set alongside each ctx->error assignment (and on cancel), mirror it in the Rust bridge, and reserve CANCEL_FLAG_RESERVED bytes at the start of stack_codegen globals so the FFI iOS path can write the reason into TRAP_REASON_OFFSET. The entry-point call now returns false for any trap, matching the LLVM setjmp semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cadd8d0 commit 27cdd72

6 files changed

Lines changed: 86 additions & 4 deletions

File tree

src/compiler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1749,7 +1749,8 @@ mod tests {
17491749
assert!(compiler.check(), "type check failed");
17501750
compiler.specialize().expect("specialize failed");
17511751

1752-
let globals_info = compiler.globals_info_with_offset(0);
1752+
let globals_info =
1753+
compiler.globals_info_with_offset(crate::cancel::CANCEL_FLAG_RESERVED as usize);
17531754
let input_offset = globals_info
17541755
.iter()
17551756
.find(|g| g.0 == "input")

src/ffi.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ pub unsafe extern "C" fn lyte_compiler_compile(ptr: *mut LyteCompiler) -> *mut L
395395
match c.compiler.compile_stack() {
396396
Ok(stack_program) => {
397397
let globals_size = stack_program.globals_size;
398-
let globals_info = make_globals_info(0);
398+
let globals_info =
399+
make_globals_info(crate::cancel::CANCEL_FLAG_RESERVED as usize);
399400

400401
let mut entry_points = Vec::new();
401402
for ep_name in &entry_point_names {
@@ -639,8 +640,18 @@ pub unsafe extern "C" fn lyte_entry_point_call(
639640
program
640641
.backend
641642
.set_cancel_callback(program.cancel_callback, program.cancel_userdata);
643+
// Clear any prior trap reason so read_trap_reason() reflects only
644+
// this invocation even if the caller reuses the globals buffer.
645+
*(globals.add(crate::cancel::TRAP_REASON_OFFSET as usize) as *mut u32) =
646+
crate::cancel::TRAP_NONE;
642647
program.backend.call_entry(ep.func_idx, globals);
643-
result = !program.backend.cancelled();
648+
let reason = program.backend.trap_reason();
649+
// Mirror the structural trap reason into the globals header so
650+
// read_trap_reason() works uniformly across JIT/LLVM/VM/stack.
651+
*(globals.add(crate::cancel::TRAP_REASON_OFFSET as usize) as *mut u32) = reason;
652+
// Any trap (cancelled / call-stack-overflow / assertion-failed)
653+
// surfaces as a failed call, matching the LLVM setjmp path.
654+
result = reason == crate::cancel::TRAP_NONE;
644655
}
645656

646657
// Clear print callback.

src/stack_codegen.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,15 @@ impl StackCodegen {
9191
}
9292

9393
/// Collect global variables and compute their offsets.
94+
///
95+
/// Reserves `CANCEL_FLAG_RESERVED` bytes at the start of the globals
96+
/// buffer for the cancel/trap header (cancel counter, callback,
97+
/// trap_reason, jmp_buf). The header layout matches the one used by
98+
/// the JIT/LLVM backends, which lets the FFI layer write the stack
99+
/// interp's structural trap reason to `TRAP_REASON_OFFSET` so hosts
100+
/// can call `read_trap_reason(globals)` uniformly across backends.
94101
fn declare_globals(&mut self, decls: &DeclTable) {
95-
let mut offset: i32 = 0;
102+
let mut offset: i32 = crate::cancel::CANCEL_FLAG_RESERVED;
96103
for decl in &decls.decls {
97104
match decl {
98105
Decl::Global {

src/stack_interp.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ static int enter_function(Ctx* ctx, uint32_t func_idx, uint64_t** out_locals) {
3535
size_t fs_needed = fs_base + total_slots;
3636
if (fs_needed > ctx->frame_stack_cap) {
3737
ctx->error = "stack overflow";
38+
ctx->trap_reason = STACK_TRAP_CALL_STACK_OVERFLOW;
3839
ctx->done = 1;
3940
*out_locals = NULL;
4041
return 0;
@@ -178,6 +179,7 @@ static int64_t ipow(int64_t base, uint32_t exp) {
178179
ctx->cancel_counter = CANCEL_CHECK_INTERVAL; \
179180
if (ctx->cancel_callback && ctx->cancel_callback(ctx->cancel_userdata)) { \
180181
ctx->cancelled = true; \
182+
ctx->trap_reason = STACK_TRAP_CANCELLED; \
181183
ctx->done = 1; \
182184
return; \
183185
} \
@@ -676,6 +678,7 @@ HANDLER(op_call) {
676678

677679
if (ctx->call_depth >= ctx->call_stack_cap) {
678680
ctx->error = "call stack overflow";
681+
ctx->trap_reason = STACK_TRAP_CALL_STACK_OVERFLOW;
679682
ctx->done = 1;
680683
return;
681684
}
@@ -776,6 +779,7 @@ HANDLER(op_call_closure) {
776779

777780
if (ctx->call_depth >= ctx->call_stack_cap) {
778781
ctx->error = "call stack overflow";
782+
ctx->trap_reason = STACK_TRAP_CALL_STACK_OVERFLOW;
779783
ctx->done = 1;
780784
return;
781785
}
@@ -851,6 +855,7 @@ HANDLER(op_call_indirect) {
851855

852856
if (ctx->call_depth >= ctx->call_stack_cap) {
853857
ctx->error = "call stack overflow";
858+
ctx->trap_reason = STACK_TRAP_CALL_STACK_OVERFLOW;
854859
ctx->done = 1;
855860
return;
856861
}
@@ -1033,6 +1038,7 @@ HANDLER(op_assert) {
10331038
fflush(stdout);
10341039
if (val == 0) {
10351040
ctx->error = "assertion failed";
1041+
ctx->trap_reason = STACK_TRAP_ASSERTION_FAILED;
10361042
ctx->done = 1;
10371043
return; // Break the tail-call chain back to stack_interp_run.
10381044
}
@@ -1787,6 +1793,7 @@ int64_t stack_interp_run(Ctx* ctx, uint32_t entry_func) {
17871793
ctx->done = 0;
17881794
ctx->result = 0;
17891795
ctx->error = NULL;
1796+
ctx->trap_reason = STACK_TRAP_NONE;
17901797
ctx->frame_stack_size = 0;
17911798
ctx->call_depth = 0;
17921799
ctx->cancelled = false;

src/stack_interp.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
// CancelCallback in src/cancel.rs.
1818
typedef bool (*CancelCallback)(void* user_data);
1919

20+
// Trap reason codes. Must stay in sync with TRAP_* in src/cancel.rs so
21+
// the host can read ctx->trap_reason uniformly across backends.
22+
#define STACK_TRAP_NONE 0u
23+
#define STACK_TRAP_CANCELLED 1u
24+
#define STACK_TRAP_CALL_STACK_OVERFLOW 2u
25+
#define STACK_TRAP_ASSERTION_FAILED 3u
26+
2027
// Instruction encoding: 32 bytes (4 x u64).
2128
// [0] handler function pointer
2229
// [1] imm0 (local index, constant, jump offset, etc.)
@@ -104,6 +111,13 @@ typedef struct Ctx {
104111
// Set to true if the callback returned true. The host distinguishes
105112
// a clean exit from cancellation by reading this after run() returns.
106113
bool cancelled;
114+
115+
// Structural trap reason set alongside `error` at each trap site (and
116+
// on cancellation). Lets the host distinguish assertion-failed vs
117+
// call-stack-overflow vs cancelled without string matching. Matches
118+
// the TRAP_* codes in src/cancel.rs so read_trap_reason() in the
119+
// globals buffer can surface the same value across backends.
120+
uint32_t trap_reason;
107121
} Ctx;
108122

109123
// Handler function signature.

src/stack_interp_bridge.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct Ctx {
5050
cancel_userdata: *mut u8,
5151
cancel_counter: i32,
5252
cancelled: bool,
53+
trap_reason: u32,
5354
}
5455

5556
extern "C" {
@@ -803,6 +804,7 @@ impl StackBackend {
803804
cancel_userdata: std::ptr::null_mut(),
804805
cancel_counter: crate::cancel::CANCEL_CHECK_INTERVAL,
805806
cancelled: false,
807+
trap_reason: crate::cancel::TRAP_NONE,
806808
};
807809

808810
Self {
@@ -832,6 +834,13 @@ impl StackBackend {
832834
self.ctx.cancelled
833835
}
834836

837+
/// Structural trap reason from the most recent `call_entry`. Matches
838+
/// the TRAP_* constants in `crate::cancel` so the host can handle
839+
/// this value the same way as `read_trap_reason()` on JIT/LLVM.
840+
pub fn trap_reason(&self) -> u32 {
841+
self.ctx.trap_reason
842+
}
843+
835844
/// Run `entry_func` with the given external globals buffer.
836845
/// The globals buffer must remain valid for the duration of the call.
837846
pub fn call_entry(&mut self, entry_func: u32, external_globals: *mut u8) -> i64 {
@@ -882,5 +891,38 @@ mod tests {
882891
backend.cancelled(),
883892
"expected the C interp's infinite loop to be cancelled"
884893
);
894+
assert_eq!(
895+
backend.trap_reason(),
896+
crate::cancel::TRAP_CANCELLED,
897+
"cancel callback should surface as TRAP_CANCELLED"
898+
);
899+
}
900+
901+
#[test]
902+
fn test_assert_failure_surfaces_trap_reason() {
903+
// `0 debug.assert` fires the assertion trap. The bridge should
904+
// expose it as TRAP_ASSERTION_FAILED instead of only printing to
905+
// stderr — this is the iOS gap vs. the LLVM/JIT path.
906+
let mut func = StackFunction::new("entry");
907+
func.emit(StackOp::I64Const(0));
908+
func.emit(StackOp::Assert);
909+
func.local_count = 3; // min scalar slots used by enter_function
910+
911+
let mut program = StackProgram::new();
912+
program.entry = program.add_function(func);
913+
914+
let mut backend = StackBackend::new(&program);
915+
let mut globals: Vec<u8> = vec![0u8; program.globals_size.max(1)];
916+
backend.call_entry(program.entry, globals.as_mut_ptr());
917+
918+
assert!(
919+
!backend.cancelled(),
920+
"assertion trap is not a cancellation"
921+
);
922+
assert_eq!(
923+
backend.trap_reason(),
924+
crate::cancel::TRAP_ASSERTION_FAILED,
925+
"failed debug.assert should surface as TRAP_ASSERTION_FAILED"
926+
);
885927
}
886928
}

0 commit comments

Comments
 (0)