@@ -59,19 +59,39 @@ const Kind = enum(u8) { unknown, undefined, null, boolean, number };
5959/// fact, because repeated Number arithmetic can overflow to infinity.
6060const IntegerFact = enum (u8 ) { unknown , signed , nonnegative , positive };
6161
62- const StackValue = struct { kind : Kind , integer : IntegerFact };
62+ const max_safe_integer : i64 = 9_007_199_254_740_991 ;
63+
64+ const IntegerRange = struct {
65+ min : i64 ,
66+ max : i64 ,
67+
68+ const unknown : IntegerRange = .{ .min = 1 , .max = 0 };
69+
70+ fn isKnown (self : IntegerRange ) bool {
71+ return self .min <= self .max ;
72+ }
73+ };
74+
75+ const StackValue = struct {
76+ kind : Kind ,
77+ integer : IntegerFact ,
78+ range : IntegerRange ,
79+ };
6380
6481const State = struct {
6582 locals : [max_slots ]Kind = @splat (.undefined ),
6683 local_integers : [max_slots ]IntegerFact = @splat (.unknown ),
84+ local_ranges : [max_slots ]IntegerRange = @splat (.unknown ),
6785 stack : [jit .numeric_scratch_capacity ]Kind = @splat (.undefined ),
6886 stack_integers : [jit .numeric_scratch_capacity ]IntegerFact = @splat (.unknown ),
87+ stack_ranges : [jit .numeric_scratch_capacity ]IntegerRange = @splat (.unknown ),
6988 depth : u8 = 0 ,
7089
71- fn push (self : * State , kind : Kind , integer : IntegerFact ) error {UnsupportedChunk }! void {
90+ fn push (self : * State , kind : Kind , integer : IntegerFact , range : IntegerRange ) error {UnsupportedChunk }! void {
7291 if (self .depth == jit .numeric_scratch_capacity ) return error .UnsupportedChunk ;
7392 self .stack [self .depth ] = kind ;
7493 self .stack_integers [self .depth ] = integer ;
94+ self .stack_ranges [self .depth ] = range ;
7595 self .depth += 1 ;
7696 }
7797
@@ -80,9 +100,11 @@ const State = struct {
80100 self .depth -= 1 ;
81101 const kind = self .stack [self .depth ];
82102 const integer = self .stack_integers [self .depth ];
103+ const range = self .stack_ranges [self .depth ];
83104 self .stack [self .depth ] = .undefined ;
84105 self .stack_integers [self .depth ] = .unknown ;
85- return .{ .kind = kind , .integer = integer };
106+ self .stack_ranges [self .depth ] = .unknown ;
107+ return .{ .kind = kind , .integer = integer , .range = range };
86108 }
87109};
88110
@@ -226,19 +248,35 @@ fn classifyInteger(v: Value) IntegerFact {
226248 return .signed ;
227249}
228250
251+ fn classifyIntegerRange (v : Value ) IntegerRange {
252+ if (classifyInteger (v ) == .unknown ) return .unknown ;
253+ const number = v .asNum ();
254+ if (number < - @as (f64 , @floatFromInt (max_safe_integer )) or number > @as (f64 , @floatFromInt (max_safe_integer )))
255+ return .unknown ;
256+ const integer : i64 = @intFromFloat (number );
257+ return .{ .min = integer , .max = integer };
258+ }
259+
229260fn joinIntegerFact (left : IntegerFact , right : IntegerFact ) IntegerFact {
230261 if (left == right ) return left ;
231262 if (left == .unknown or right == .unknown ) return .unknown ;
232263 if (left == .signed or right == .signed ) return .signed ;
233264 return .nonnegative ;
234265}
235266
267+ fn joinIntegerRange (left : IntegerRange , right : IntegerRange , widen : bool ) IntegerRange {
268+ if (! left .isKnown () or ! right .isKnown ()) return .unknown ;
269+ const joined : IntegerRange = .{ .min = @min (left .min , right .min ), .max = @max (left .max , right .max ) };
270+ if (widen and ! std .meta .eql (left , joined )) return .unknown ;
271+ return joined ;
272+ }
273+
236274fn joinKind (left : Kind , right : Kind ) Kind {
237275 if (left == right ) return left ;
238276 return .unknown ;
239277}
240278
241- fn mergeState (existing : * State , incoming : State ) error {UnsupportedChunk }! bool {
279+ fn mergeState (existing : * State , incoming : State , widen_ranges : bool ) error {UnsupportedChunk }! bool {
242280 if (existing .depth != incoming .depth ) return error .UnsupportedChunk ;
243281 for (existing .stack [0.. existing .depth ], incoming .stack [0.. incoming .depth ]) | current , next |
244282 if (current != next ) return error .UnsupportedChunk ;
@@ -258,13 +296,27 @@ fn mergeState(existing: *State, incoming: State) error{UnsupportedChunk}!bool {
258296 changed = true ;
259297 }
260298 }
299+ for (& existing .local_ranges , incoming .local_ranges ) | * current , next | {
300+ const merged = joinIntegerRange (current .* , next , widen_ranges );
301+ if (! std .meta .eql (merged , current .* )) {
302+ current .* = merged ;
303+ changed = true ;
304+ }
305+ }
261306 for (existing .stack_integers [0.. existing .depth ], incoming .stack_integers [0.. incoming .depth ]) | * current , next | {
262307 const merged = joinIntegerFact (current .* , next );
263308 if (merged != current .* ) {
264309 current .* = merged ;
265310 changed = true ;
266311 }
267312 }
313+ for (existing .stack_ranges [0.. existing .depth ], incoming .stack_ranges [0.. incoming .depth ]) | * current , next | {
314+ const merged = joinIntegerRange (current .* , next , widen_ranges );
315+ if (! std .meta .eql (merged , current .* )) {
316+ current .* = merged ;
317+ changed = true ;
318+ }
319+ }
268320 return changed ;
269321}
270322
@@ -279,17 +331,58 @@ fn arithmeticIntegerFact(op: bc.Op, lhs: IntegerFact, rhs: IntegerFact) IntegerF
279331 };
280332}
281333
334+ fn boundedIntegerRange (min : i128 , max : i128 ) IntegerRange {
335+ if (min < - @as (i128 , max_safe_integer ) or max > max_safe_integer ) return .unknown ;
336+ return .{ .min = @intCast (min ), .max = @intCast (max ) };
337+ }
338+
339+ fn arithmeticIntegerRange (op : bc.Op , lhs : StackValue , rhs : StackValue ) IntegerRange {
340+ if (op == .mod and rhs .range .isKnown () and rhs .range .min > 0 ) {
341+ const remainder_bound = rhs .range .max - 1 ;
342+ if (lhs .integer == .nonnegative or lhs .integer == .positive ) {
343+ const upper = if (lhs .range .isKnown ()) @min (lhs .range .max , remainder_bound ) else remainder_bound ;
344+ return .{ .min = 0 , .max = upper };
345+ }
346+ if (lhs .integer == .signed ) {
347+ const lower = if (lhs .range .isKnown ()) @max (lhs .range .min , - remainder_bound ) else - remainder_bound ;
348+ const upper = if (lhs .range .isKnown ()) @min (lhs .range .max , remainder_bound ) else remainder_bound ;
349+ return .{ .min = lower , .max = upper };
350+ }
351+ }
352+ if (! lhs .range .isKnown () or ! rhs .range .isKnown ()) return .unknown ;
353+ const lhs_min : i128 = lhs .range .min ;
354+ const lhs_max : i128 = lhs .range .max ;
355+ const rhs_min : i128 = rhs .range .min ;
356+ const rhs_max : i128 = rhs .range .max ;
357+ return switch (op ) {
358+ .add = > boundedIntegerRange (lhs_min + rhs_min , lhs_max + rhs_max ),
359+ .sub = > boundedIntegerRange (lhs_min - rhs_max , lhs_max - rhs_min ),
360+ .mul = > blk : {
361+ const products = [4 ]i128 {
362+ lhs_min * rhs_min ,
363+ lhs_min * rhs_max ,
364+ lhs_max * rhs_min ,
365+ lhs_max * rhs_max ,
366+ };
367+ break :blk boundedIntegerRange (std .mem .min (i128 , & products ), std .mem .max (i128 , & products ));
368+ },
369+ .div , .mod = > .unknown ,
370+ else = > unreachable ,
371+ };
372+ }
373+
282374fn enqueueState (
283375 states : []? State ,
284376 worklist : * std .ArrayListUnmanaged (u32 ),
285377 allocator : std.mem.Allocator ,
286378 target : u32 ,
287379 state : State ,
380+ widen_ranges : bool ,
288381) ! void {
289382 if (target >= states .len ) return error .UnsupportedChunk ;
290383 if (states [target ]) | existing_state | {
291384 var existing = existing_state ;
292- if (try mergeState (& existing , state )) {
385+ if (try mergeState (& existing , state , widen_ranges )) {
293386 states [target ] = existing ;
294387 try worklist .append (allocator , target );
295388 }
@@ -311,7 +404,10 @@ fn analyzeNumeric(chunk: *const Chunk, integer_parameters: bool) !Analysis {
311404 var initial = State {};
312405 for (0.. chunk .param_count ) | slot | {
313406 initial .locals [slot ] = .number ;
314- if (integer_parameters ) initial .local_integers [slot ] = .nonnegative ;
407+ if (integer_parameters ) {
408+ initial .local_integers [slot ] = .nonnegative ;
409+ initial .local_ranges [slot ] = .{ .min = 0 , .max = std .math .maxInt (u32 ) };
410+ }
315411 }
316412 states [0 ] = initial ;
317413
@@ -331,42 +427,51 @@ fn analyzeNumeric(chunk: *const Chunk, integer_parameters: bool) !Analysis {
331427 .load_const = > {
332428 if (inst .a >= chunk .consts .items .len ) return error .UnsupportedChunk ;
333429 const constant = chunk .consts .items [inst .a ];
334- try state .push (classify (constant ) orelse return error .UnsupportedChunk , classifyInteger (constant ));
430+ try state .push (
431+ classify (constant ) orelse return error .UnsupportedChunk ,
432+ classifyInteger (constant ),
433+ classifyIntegerRange (constant ),
434+ );
335435 },
336- .load_undefined = > try state .push (.undefined , .unknown ),
337- .load_null = > try state .push (.null , .unknown ),
338- .load_true , .load_false = > try state .push (.boolean , .unknown ),
436+ .load_undefined = > try state .push (.undefined , .unknown , .unknown ),
437+ .load_null = > try state .push (.null , .unknown , .unknown ),
438+ .load_true , .load_false = > try state .push (.boolean , .unknown , .unknown ),
339439 .pop = > _ = try state .pop (),
340440 .load_local = > {
341441 if (inst .a >= chunk .local_count ) return error .UnsupportedChunk ;
342442 // A local whose representation differs across predecessors is
343443 // safe only after an unconditional overwrite. Reconciliation
344444 // at a live mixed-kind merge is outside this numeric tier.
345445 if (state .locals [inst .a ] == .unknown ) return error .UnsupportedChunk ;
346- try state .push (state .locals [inst .a ], state .local_integers [inst .a ]);
446+ try state .push (state .locals [inst .a ], state .local_integers [inst .a ], state . local_ranges [ inst . a ] );
347447 },
348448 .store_local = > {
349449 if (inst .a >= chunk .local_count or state .depth == 0 ) return error .UnsupportedChunk ;
350450 state .locals [inst .a ] = state .stack [state .depth - 1 ];
351451 state .local_integers [inst .a ] = state .stack_integers [state .depth - 1 ];
452+ state .local_ranges [inst .a ] = state .stack_ranges [state .depth - 1 ];
352453 },
353454 .add , .sub , .mul , .div , .mod = > {
354455 const rhs = try state .pop ();
355456 const lhs = try state .pop ();
356457 if (rhs .kind != .number or lhs .kind != .number ) return error .UnsupportedChunk ;
357- try state .push (.number , arithmeticIntegerFact (inst .op , lhs .integer , rhs .integer ));
458+ try state .push (
459+ .number ,
460+ arithmeticIntegerFact (inst .op , lhs .integer , rhs .integer ),
461+ arithmeticIntegerRange (inst .op , lhs , rhs ),
462+ );
358463 },
359464 .lt , .le , .gt , .ge , .eq , .neq , .eq_strict , .neq_strict = > {
360465 if ((try state .pop ()).kind != .number or (try state .pop ()).kind != .number ) return error .UnsupportedChunk ;
361- try state .push (.boolean , .unknown );
466+ try state .push (.boolean , .unknown , .unknown );
362467 },
363468 .jump = > {
364- try enqueueState (states , & worklist , allocator , inst .a , state );
469+ try enqueueState (states , & worklist , allocator , inst .a , state , inst . a <= ip );
365470 fallthrough = false ;
366471 },
367472 .jump_if_false = > {
368473 if ((try state .pop ()).kind != .boolean ) return error .UnsupportedChunk ;
369- try enqueueState (states , & worklist , allocator , inst .a , state );
474+ try enqueueState (states , & worklist , allocator , inst .a , state , inst . a <= ip );
370475 },
371476 .ret = > {
372477 _ = try state .pop ();
@@ -385,7 +490,7 @@ fn analyzeNumeric(chunk: *const Chunk, integer_parameters: bool) !Analysis {
385490 max_stack_depth = @max (max_stack_depth , state .depth );
386491 if (fallthrough ) {
387492 const next = std .math .cast (u32 , ip + 1 ) orelse return error .UnsupportedChunk ;
388- try enqueueState (states , & worklist , allocator , next , state );
493+ try enqueueState (states , & worklist , allocator , next , state , false );
389494 }
390495 }
391496
@@ -970,6 +1075,9 @@ test "integer provenance converges through benchmark-shaped loops" {
9701075 try std .testing .expect (state .depth >= 2 );
9711076 try std .testing .expectEqual (IntegerFact .nonnegative , state .stack_integers [state .depth - 2 ]);
9721077 try std .testing .expectEqual (IntegerFact .positive , state .stack_integers [state .depth - 1 ]);
1078+ try std .testing .expectEqual (IntegerRange { .min = 1_000_003 , .max = 1_000_003 }, state .stack_ranges [state .depth - 1 ]);
1079+ const result_state = analysis .states [ip + 1 ].? ;
1080+ try std .testing .expectEqual (IntegerRange { .min = 0 , .max = 1_000_002 }, result_state .stack_ranges [result_state .depth - 1 ]);
9731081 saw_remainder = true ;
9741082 },
9751083 .ret = > {
@@ -984,6 +1092,28 @@ test "integer provenance converges through benchmark-shaped loops" {
9841092 try std .testing .expect (saw_return );
9851093}
9861094
1095+ test "integer ranges bound guarded straight-line remainder" {
1096+ const Parser = @import ("../parser.zig" ).Parser ;
1097+ const Compiler = @import ("../compiler.zig" ).Compiler ;
1098+
1099+ var arena = std .heap .ArenaAllocator .init (std .testing .allocator );
1100+ defer arena .deinit ();
1101+ const allocator = arena .allocator ();
1102+ var parser = try Parser .init (allocator , "function bounded(n) { return (n + 5) % 1000; }" );
1103+ const program = try parser .parseProgram ();
1104+ const root = try Compiler .compileProgram (allocator , program );
1105+ const chunk = root .fns .items [0 ].chunk .? ;
1106+ var analysis = try analyzeNumeric (chunk , true );
1107+ defer analysis .deinit ();
1108+
1109+ for (chunk .code .items , 0.. ) | inst , ip | if (inst .op == .ret ) {
1110+ const state = analysis .states [ip ].? ;
1111+ try std .testing .expectEqual (IntegerRange { .min = 0 , .max = 999 }, state .stack_ranges [state .depth - 1 ]);
1112+ return ;
1113+ };
1114+ return error .TestExpectedEqual ;
1115+ }
1116+
9871117test "integer provenance rejects live mixed-kind locals" {
9881118 const Parser = @import ("../parser.zig" ).Parser ;
9891119 const Compiler = @import ("../compiler.zig" ).Compiler ;
0 commit comments