Skip to content

Commit 1fc7e9c

Browse files
committed
feat(jit): analyze bounded integer ranges (#56)
1 parent 6011cc1 commit 1fc7e9c

1 file changed

Lines changed: 146 additions & 16 deletions

File tree

‎src/jit/compiler.zig‎

Lines changed: 146 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,39 @@ const Kind = enum(u8) { unknown, undefined, null, boolean, number };
5959
/// fact, because repeated Number arithmetic can overflow to infinity.
6060
const 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

6481
const 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+
229260
fn 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+
236274
fn 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+
282374
fn 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+
9871117
test "integer provenance rejects live mixed-kind locals" {
9881118
const Parser = @import("../parser.zig").Parser;
9891119
const Compiler = @import("../compiler.zig").Compiler;

0 commit comments

Comments
 (0)