@@ -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+
509527fn 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+
31443192test "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