@@ -269,6 +269,11 @@ const a64 = struct {
269269 return 0xD1000000 | (@as (u32 , imm12 ) << 10 ) | (@as (u32 , rn ) << 5 ) | rd ;
270270 }
271271
272+ /// SUBS Xd, Xn, #imm12 (64-bit, sets flags)
273+ fn subsImm64 (rd : u5 , rn : u5 , imm12 : u12 ) u32 {
274+ return 0xF1000000 | (@as (u32 , imm12 ) << 10 ) | (@as (u32 , rn ) << 5 ) | rd ;
275+ }
276+
272277 /// SUB Wd, Wn, #imm12 (32-bit)
273278 fn subImm32 (rd : u5 , rn : u5 , imm12 : u12 ) u32 {
274279 return 0x51000000 | (@as (u32 , imm12 ) << 10 ) | (@as (u32 , rn ) << 5 ) | rd ;
@@ -887,6 +892,8 @@ pub const Compiler = struct {
887892 patches : std .ArrayList (Patch ),
888893 /// Error stubs: branch-to-error sites to be patched at end of function.
889894 error_stubs : std .ArrayList (ErrorStub ),
895+ /// Branch indices from fuel checks — all patched to a single shared fuel exit.
896+ fuel_check_branches : std .ArrayList (u32 ),
890897 alloc : Allocator ,
891898 reg_count : u16 ,
892899 local_count : u16 ,
@@ -916,6 +923,7 @@ pub const Compiler = struct {
916923 param_count : u16 ,
917924 result_count : u16 ,
918925 reg_ptr_offset : u32 ,
926+ jit_fuel_offset : u32 ,
919927 min_memory_bytes : u32 ,
920928 /// Bitmask of vregs that need loading in the prologue.
921929 /// Bit N = 1 means vreg N is read before written and must be loaded.
@@ -993,6 +1001,7 @@ pub const Compiler = struct {
9931001 .pc_map = .empty ,
9941002 .patches = .empty ,
9951003 .error_stubs = .empty ,
1004+ .fuel_check_branches = .empty ,
9961005 .alloc = alloc ,
9971006 .reg_count = 0 ,
9981007 .local_count = 0 ,
@@ -1016,6 +1025,7 @@ pub const Compiler = struct {
10161025 .param_count = 0 ,
10171026 .result_count = 0 ,
10181027 .reg_ptr_offset = 0 ,
1028+ .jit_fuel_offset = 0 ,
10191029 .min_memory_bytes = 0 ,
10201030 .prologue_load_mask = 0xFFFFF , // default: load all (20 vregs)
10211031 .known_consts = .{null } ** 128 ,
@@ -1041,6 +1051,7 @@ pub const Compiler = struct {
10411051 self .pc_map .deinit (self .alloc );
10421052 self .patches .deinit (self .alloc );
10431053 self .error_stubs .deinit (self .alloc );
1054+ self .fuel_check_branches .deinit (self .alloc );
10441055 }
10451056
10461057 fn emit (self : * Compiler , inst : u32 ) void {
@@ -2381,6 +2392,8 @@ pub const Compiler = struct {
23812392 regalloc_mod .OP_BR = > {
23822393 self .fpCacheEvictAll ();
23832394 const target = instr .operand ;
2395+ // Fuel check at back-edges (target <= current pc)
2396+ if (target <= pc .* - 1 ) self .emitFuelCheck ();
23842397 const arm_idx = self .currentIdx ();
23852398 self .emit (a64 .b (0 )); // placeholder
23862399 self .patches .append (self .alloc , .{
@@ -2391,6 +2404,8 @@ pub const Compiler = struct {
23912404 },
23922405 regalloc_mod .OP_BR_IF = > {
23932406 self .fpCacheEvictAll ();
2407+ // Fuel check at back-edges (before the conditional branch)
2408+ if (instr .operand <= pc .* - 1 ) self .emitFuelCheck ();
23942409 // Branch if rd != 0
23952410 const cond_reg = self .getOrLoad (instr .rd , SCRATCH );
23962411 const arm_idx = self .currentIdx ();
@@ -2403,6 +2418,8 @@ pub const Compiler = struct {
24032418 },
24042419 regalloc_mod .OP_BR_IF_NOT = > {
24052420 self .fpCacheEvictAll ();
2421+ // Fuel check at back-edges
2422+ if (instr .operand <= pc .* - 1 ) self .emitFuelCheck ();
24062423 const cond_reg = self .getOrLoad (instr .rd , SCRATCH );
24072424 const arm_idx = self .currentIdx ();
24082425 self .emit (a64 .cbz32 (cond_reg , 0 )); // placeholder
@@ -3307,6 +3324,40 @@ pub const Compiler = struct {
33073324 self .storeVreg (instr .rd , d );
33083325 }
33093326
3327+ /// Emit fuel check at a loop back-edge.
3328+ /// Decrements jit_fuel in the VM struct; if negative, branches to a shared
3329+ /// fuel-exhausted exit (emitted once at end of function by emitErrorStubs).
3330+ fn emitFuelCheck (self : * Compiler ) void {
3331+ // Skip when offset is 0 (unit tests without Vm struct)
3332+ if (self .jit_fuel_offset == 0 ) return ;
3333+ // Load vm_ptr → SCRATCH (x8)
3334+ self .emitLoadVmPtr (SCRATCH );
3335+ const fuel_reg : u5 = 0 ; // x0: safe — not mapped to any vreg
3336+ // Compute address of jit_fuel: ADD SCRATCH, SCRATCH, #offset
3337+ // jit_fuel offset exceeds u16 (Vm struct is >500KB), so use ADD imm.
3338+ const fuel_off = self .jit_fuel_offset ;
3339+ // ADD can encode up to 12-bit immediate (4095). If larger, use two-step.
3340+ if (fuel_off <= 4095 ) {
3341+ self .emit (a64 .addImm64 (SCRATCH , SCRATCH , @intCast (fuel_off )));
3342+ } else {
3343+ // MOVZ x0, #(fuel_off & 0xFFFF), LSL#0
3344+ self .emit (a64 .movz64 (fuel_reg , @truncate (fuel_off ), 0 ));
3345+ if (fuel_off > 0xFFFF ) {
3346+ self .emit (a64 .movk64 (fuel_reg , @truncate (fuel_off >> 16 ), 1 ));
3347+ }
3348+ self .emit (a64 .add64 (SCRATCH , SCRATCH , fuel_reg ));
3349+ }
3350+ // Now SCRATCH points to &vm.jit_fuel
3351+ self .emit (a64 .ldr64 (fuel_reg , SCRATCH , 0 ));
3352+ self .emit (a64 .subsImm64 (fuel_reg , fuel_reg , 1 ));
3353+ self .emit (a64 .str64 (fuel_reg , SCRATCH , 0 ));
3354+ self .scratch_vreg = null ;
3355+ // Branch to shared fuel exit (patched in emitErrorStubs)
3356+ const branch_idx = self .currentIdx ();
3357+ self .emit (a64 .bCond (.mi , 0 )); // placeholder
3358+ self .fuel_check_branches .append (self .alloc , branch_idx ) catch {};
3359+ }
3360+
33103361 /// Emit conditional error: if condition is true, branch to shared error stub.
33113362 /// Error stubs are emitted at function end by emitErrorStubs().
33123363 fn emitCondError (self : * Compiler , cond : a64.Cond , error_code : u16 ) void {
@@ -4652,7 +4703,7 @@ pub const Compiler = struct {
46524703 /// Emit error stubs and shared error epilogue at end of function.
46534704 /// Each error site branches to a stub that sets x0 and jumps to shared exit.
46544705 fn emitErrorStubs (self : * Compiler ) void {
4655- if (self .error_stubs .items .len == 0 and ! self .use_guard_pages ) return ;
4706+ if (self .error_stubs .items .len == 0 and self . fuel_check_branches . items . len == 0 and ! self .use_guard_pages ) return ;
46564707
46574708 // Shared error epilogue: restore callee-saved regs and return (x0 has error code)
46584709 const shared_exit = self .currentIdx ();
@@ -4690,6 +4741,22 @@ pub const Compiler = struct {
46904741 }
46914742 }
46924743 }
4744+
4745+ // Emit shared fuel-exhausted exit and patch all fuel check branches.
4746+ // Single stub: MOVZ x0, #9 + B shared_exit. All back-edge checks branch here.
4747+ if (self .fuel_check_branches .items .len > 0 ) {
4748+ const fuel_stub_idx = self .currentIdx ();
4749+ self .emit (a64 .movz64 (0 , 9 , 0 )); // x0 = 9 (FuelExhausted)
4750+ const exit_offset : i26 = @intCast (@as (i32 , @intCast (shared_exit )) - @as (i32 , @intCast (self .currentIdx ())));
4751+ self .emit (a64 .b (exit_offset ));
4752+
4753+ // Patch all fuel check B.MI branches to point to this stub
4754+ for (self .fuel_check_branches .items ) | branch_idx | {
4755+ const offset : i32 = @as (i32 , @intCast (fuel_stub_idx )) - @as (i32 , @intCast (branch_idx ));
4756+ const imm : u19 = @bitCast (@as (i19 , @intCast (offset )));
4757+ self .code .items [branch_idx ] = 0x54000000 | (@as (u32 , imm ) << 5 ) | @as (u32 , @intFromEnum (a64 .Cond .mi ));
4758+ }
4759+ }
46934760 }
46944761
46954762 // --- Branch patching ---
@@ -5199,6 +5266,7 @@ pub fn compileFunction(
51995266 compiler .use_guard_pages = use_guard_pages ;
52005267 compiler .osr_target_pc = osr_target_pc ;
52015268 compiler .gc_trampoline_addr = gc_trampoline_addr ;
5269+ compiler .jit_fuel_offset = @intCast (@offsetOf (vm_mod .Vm , "jit_fuel" ));
52025270
52035271 // Dump JIT code before deinit (pc_map still alive, one-shot)
52045272 if (trace ) | tc | {
0 commit comments