Skip to content

Commit 6b2e01d

Browse files
committed
Sema: Fix compilation timeout in analyzeAs with nested cast builtins
Resolves a circular dependency issue where `@as` coercion with nested cast builtins (`@intCast`, `@floatCast`, `@ptrCast`, `@truncate`) would cause infinite recursion in complex control flow contexts. The bug occurs when the pattern `@as(DestType, @intcast(value))` appears in code with: - Loop constructs - Short-circuit boolean operations (OR/AND) - Optional unwrapping Example problematic pattern: ```zig while (condition) { if (opt == null or (opt.?)[@as(usize, @intcast(pid))] == false) { break; } } ``` Root cause: The original code would resolve the operand before the destination type, causing the inner cast builtin to recursively analyze without type context, leading to circular dependencies in the type resolution system. Fix: When the operand is a cast builtin, resolve the destination type FIRST, then analyze the inner cast with proper type context. This breaks the circular dependency while maintaining correct type coercion semantics. The fix adds an optimization path that: 1. Detects when operand is a type-directed cast builtin 2. Resolves destination type before analyzing the operand 3. Skips redundant outer coercion if types already match 4. Preserves existing behavior for non-cast operands A helper function `validateCastDestType` was extracted to eliminate code duplication and improve maintainability. Tested with Bun codebase which previously timed out during compilation. The pattern appears in src/install/updatePackageJSONAndInstall.zig:722. Related: oven-sh/bun
1 parent feb05a7 commit 6b2e01d

File tree

2 files changed

+73
-5
lines changed

2 files changed

+73
-5
lines changed

src/Sema.zig

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9641,6 +9641,21 @@ fn zirAsShiftOperand(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileEr
96419641
return sema.analyzeAs(block, src, extra.dest_type, extra.operand, true);
96429642
}
96439643

9644+
fn validateCastDestType(
9645+
sema: *Sema,
9646+
block: *Block,
9647+
dest_ty: Type,
9648+
src: LazySrcLoc,
9649+
) CompileError!void {
9650+
const pt = sema.pt;
9651+
const zcu = pt.zcu;
9652+
switch (dest_ty.zigTypeTag(zcu)) {
9653+
.@"opaque" => return sema.fail(block, src, "cannot cast to opaque type '{f}'", .{dest_ty.fmt(pt)}),
9654+
.noreturn => return sema.fail(block, src, "cannot cast to noreturn", .{}),
9655+
else => {},
9656+
}
9657+
}
9658+
96449659
fn analyzeAs(
96459660
sema: *Sema,
96469661
block: *Block,
@@ -9651,13 +9666,48 @@ fn analyzeAs(
96519666
) CompileError!Air.Inst.Ref {
96529667
const pt = sema.pt;
96539668
const zcu = pt.zcu;
9669+
9670+
// Optimize nested cast builtins to prevent redundant coercion and circular dependencies.
9671+
if (zir_operand.toIndex()) |operand_index| {
9672+
const operand_tag = sema.code.instructions.items(.tag)[@intFromEnum(operand_index)];
9673+
switch (operand_tag) {
9674+
// These builtins perform their own type-directed casting
9675+
.int_cast, .float_cast, .ptr_cast, .truncate => {
9676+
// Resolve dest_ty first so inner cast can use it as type context
9677+
const dest_ty = try sema.resolveTypeOrPoison(block, src, zir_dest_type) orelse {
9678+
return sema.resolveInst(zir_operand);
9679+
};
9680+
9681+
try sema.validateCastDestType(block, dest_ty, src);
9682+
9683+
// Now analyze inner cast with dest_ty already resolved
9684+
const operand = try sema.resolveInst(zir_operand);
9685+
9686+
// If inner cast already produced the correct type, skip redundant coercion
9687+
if (sema.typeOf(operand).eql(dest_ty, zcu)) {
9688+
return operand;
9689+
}
9690+
9691+
// Otherwise perform outer coercion (handles vectors/arrays and other edge cases)
9692+
const is_ret = if (zir_dest_type.toIndex()) |ptr_index|
9693+
sema.code.instructions.items(.tag)[@intFromEnum(ptr_index)] == .ret_type
9694+
else
9695+
false;
9696+
return sema.coerceExtra(block, dest_ty, operand, src, .{
9697+
.is_ret = is_ret,
9698+
.no_cast_to_comptime_int = no_cast_to_comptime_int
9699+
}) catch |err| switch (err) {
9700+
error.NotCoercible => unreachable,
9701+
else => |e| return e,
9702+
};
9703+
},
9704+
else => {},
9705+
}
9706+
}
9707+
96549708
const operand = try sema.resolveInst(zir_operand);
96559709
const dest_ty = try sema.resolveTypeOrPoison(block, src, zir_dest_type) orelse return operand;
9656-
switch (dest_ty.zigTypeTag(zcu)) {
9657-
.@"opaque" => return sema.fail(block, src, "cannot cast to opaque type '{f}'", .{dest_ty.fmt(pt)}),
9658-
.noreturn => return sema.fail(block, src, "cannot cast to noreturn", .{}),
9659-
else => {},
9660-
}
9710+
try sema.validateCastDestType(block, dest_ty, src);
96619711

96629712
const is_ret = if (zir_dest_type.toIndex()) |ptr_index|
96639713
sema.code.instructions.items(.tag)[@intFromEnum(ptr_index)] == .ret_type

test/behavior/cast_int.zig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,21 @@ test "load non byte-sized value in union" {
251251
try expect(pieces[1].type == .PAWN);
252252
try expect(pieces[1].color == .BLACK);
253253
}
254+
test "@as with nested @intCast in loop with optional" {
255+
// Regression test for circular dependency bug in Sema.analyzeAs
256+
// where @as(DestType, @intCast(value)) would cause compilation timeout
257+
// in complex control flow contexts (loop + short-circuit OR + optional unwrap)
258+
const arr = [_]bool{ true, false, true };
259+
var opt: ?[]const bool = &arr;
260+
var pid: u32 = 1;
261+
_ = .{ &opt, &pid };
262+
263+
var i: usize = 0;
264+
while (i < 3) : (i += 1) {
265+
// This pattern previously caused infinite recursion during compilation
266+
if (opt == null or (opt.?)[@as(usize, @intCast(pid))] == false) {
267+
break;
268+
}
269+
}
270+
try expect(i == 0); // Should break immediately since arr[1] == false
271+
}

0 commit comments

Comments
 (0)