Skip to content

Commit 69dfc78

Browse files
committed
feat(jit): add unsigned entry guards (#56)
1 parent 281193d commit 69dfc78

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

‎src/jit.zig‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ pub const CompiledCode = struct {
105105
manages_steps: bool = false,
106106
frame_slots: u32 = 0,
107107
required_numeric_slots: u64 = 0,
108+
/// Parameter slots that must contain canonical non-negative u32 Numbers.
109+
/// The VM checks this before native step accounting or slot mutation, so a
110+
/// failed speculative integer entry restarts safely at bytecode IP zero.
111+
required_u32_slots: u64 = 0,
108112
max_stack_depth: u8 = 0,
109113

110114
pub fn deinit(self: *CompiledCode) void {

‎src/vm.zig‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,24 @@ fn nativeRemainder(a: f64, b: f64) callconv(.c) f64 {
506506
return numberRemainder(a, b);
507507
}
508508

509+
fn isExactUnsigned32(value_: Value) bool {
510+
if (!value_.isNumber()) return false;
511+
const number = value_.asNum();
512+
if (!std.math.isFinite(number) or number < 0 or number > @as(f64, @floatFromInt(std.math.maxInt(u32)))) return false;
513+
if (number == 0 and std.math.signbit(number)) return false;
514+
return @trunc(number) == number;
515+
}
516+
517+
fn unsigned32GuardsPass(slots: []const Value, required_mask: u64) bool {
518+
var required = required_mask;
519+
while (required != 0) {
520+
const slot: u6 = @intCast(@ctz(required));
521+
if (slot >= slots.len or !isExactUnsigned32(slots[slot])) return false;
522+
required &= required - 1;
523+
}
524+
return true;
525+
}
526+
509527
fn nativeCheckpoint(frame: *jit.NativeFrame) callconv(.c) u32 {
510528
const vm: *Interpreter = @ptrCast(@alignCast(frame.runtime_context orelse return @intFromEnum(jit.ExitStatus.stop)));
511529
const steps = (frame.steps orelse return @intFromEnum(jit.ExitStatus.stop)).*;
@@ -577,6 +595,10 @@ fn tryRunNative(vm: *Interpreter, exec: *Exec, chunk: *Chunk, frame: ?*Frame, ge
577595
required &= required - 1;
578596
}
579597
}
598+
if (native.required_u32_slots != 0) {
599+
const cf = current_frame orelse return null;
600+
if (!unsigned32GuardsPass(cf.slots, native.required_u32_slots)) return null;
601+
}
580602

581603
var scratch: [jit.numeric_scratch_capacity]u64 = undefined;
582604
var native_frame = jit.NativeFrame{
@@ -3141,6 +3163,32 @@ test "vm: hot primitive constant function tiers through native entry" {
31413163
try std.testing.expectEqual(@as(u64, 1024), machine.steps);
31423164
}
31433165

3166+
test "vm: speculative unsigned parameter guards are exact" {
3167+
var slots = [_]Value{
3168+
Value.num(0),
3169+
Value.num(42),
3170+
Value.num(@floatFromInt(std.math.maxInt(u32))),
3171+
};
3172+
try std.testing.expect(unsigned32GuardsPass(&slots, 0b111));
3173+
try std.testing.expect(unsigned32GuardsPass(&slots, 0));
3174+
3175+
const rejected = [_]f64{
3176+
-0.0,
3177+
-1,
3178+
1.5,
3179+
std.math.nan(f64),
3180+
std.math.inf(f64),
3181+
@as(f64, @floatFromInt(@as(u64, std.math.maxInt(u32)) + 1)),
3182+
};
3183+
for (rejected) |number| {
3184+
slots[1] = Value.num(number);
3185+
try std.testing.expect(!unsigned32GuardsPass(&slots, 0b010));
3186+
}
3187+
slots[1] = Value.str("not a number");
3188+
try std.testing.expect(!unsigned32GuardsPass(&slots, 0b010));
3189+
try std.testing.expect(!unsigned32GuardsPass(&slots, @as(u64, 1) << 7));
3190+
}
3191+
31443192
test "vm: numeric baseline tier preserves steps and non-number fallback" {
31453193
if (!jit.supported or @import("builtin").cpu.arch != .aarch64) return error.SkipZigTest;
31463194

0 commit comments

Comments
 (0)