Skip to content

Commit aa74eaf

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 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 aa74eaf

File tree

1 file changed

+44
-0
lines changed

1 file changed

+44
-0
lines changed

src/Sema.zig

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9651,6 +9651,50 @@ fn analyzeAs(
96519651
) CompileError!Air.Inst.Ref {
96529652
const pt = sema.pt;
96539653
const zcu = pt.zcu;
9654+
9655+
// Optimize nested cast builtins to prevent redundant coercion and circular dependencies.
9656+
if (zir_operand.toIndex()) |operand_index| {
9657+
const operand_tag = sema.code.instructions.items(.tag)[@intFromEnum(operand_index)];
9658+
switch (operand_tag) {
9659+
// These builtins perform their own type-directed casting
9660+
.int_cast, .float_cast, .ptr_cast, .truncate => {
9661+
// Resolve dest_ty first so inner cast can use it as type context
9662+
const dest_ty = try sema.resolveTypeOrPoison(block, src, zir_dest_type) orelse {
9663+
return sema.resolveInst(zir_operand);
9664+
};
9665+
9666+
// Validate dest_ty (same checks as below)
9667+
switch (dest_ty.zigTypeTag(zcu)) {
9668+
.@"opaque" => return sema.fail(block, src, "cannot cast to opaque type '{f}'", .{dest_ty.fmt(pt)}),
9669+
.noreturn => return sema.fail(block, src, "cannot cast to noreturn", .{}),
9670+
else => {},
9671+
}
9672+
9673+
// Now analyze inner cast with dest_ty already resolved
9674+
const operand = try sema.resolveInst(zir_operand);
9675+
9676+
// If inner cast already produced the correct type, skip redundant coercion
9677+
if (sema.typeOf(operand).eql(dest_ty, zcu)) {
9678+
return operand;
9679+
}
9680+
9681+
// Otherwise perform outer coercion (handles vectors/arrays and other edge cases)
9682+
const is_ret = if (zir_dest_type.toIndex()) |ptr_index|
9683+
sema.code.instructions.items(.tag)[@intFromEnum(ptr_index)] == .ret_type
9684+
else
9685+
false;
9686+
return sema.coerceExtra(block, dest_ty, operand, src, .{
9687+
.is_ret = is_ret,
9688+
.no_cast_to_comptime_int = no_cast_to_comptime_int
9689+
}) catch |err| switch (err) {
9690+
error.NotCoercible => unreachable,
9691+
else => |e| return e,
9692+
};
9693+
},
9694+
else => {},
9695+
}
9696+
}
9697+
96549698
const operand = try sema.resolveInst(zir_operand);
96559699
const dest_ty = try sema.resolveTypeOrPoison(block, src, zir_dest_type) orelse return operand;
96569700
switch (dest_ty.zigTypeTag(zcu)) {

0 commit comments

Comments
 (0)