From 6b2e01d01980243592df1cdd4321762051f0b72e Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 24 Oct 2025 19:47:12 +0900 Subject: [PATCH] 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 --- src/Sema.zig | 60 ++++++++++++++++++++++++++++++++++---- test/behavior/cast_int.zig | 18 ++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index b94e172b45ac..b180656a9b9c 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -9641,6 +9641,21 @@ fn zirAsShiftOperand(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileEr return sema.analyzeAs(block, src, extra.dest_type, extra.operand, true); } +fn validateCastDestType( + sema: *Sema, + block: *Block, + dest_ty: Type, + src: LazySrcLoc, +) CompileError!void { + const pt = sema.pt; + const zcu = pt.zcu; + switch (dest_ty.zigTypeTag(zcu)) { + .@"opaque" => return sema.fail(block, src, "cannot cast to opaque type '{f}'", .{dest_ty.fmt(pt)}), + .noreturn => return sema.fail(block, src, "cannot cast to noreturn", .{}), + else => {}, + } +} + fn analyzeAs( sema: *Sema, block: *Block, @@ -9651,13 +9666,48 @@ fn analyzeAs( ) CompileError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; + + // Optimize nested cast builtins to prevent redundant coercion and circular dependencies. + if (zir_operand.toIndex()) |operand_index| { + const operand_tag = sema.code.instructions.items(.tag)[@intFromEnum(operand_index)]; + switch (operand_tag) { + // These builtins perform their own type-directed casting + .int_cast, .float_cast, .ptr_cast, .truncate => { + // Resolve dest_ty first so inner cast can use it as type context + const dest_ty = try sema.resolveTypeOrPoison(block, src, zir_dest_type) orelse { + return sema.resolveInst(zir_operand); + }; + + try sema.validateCastDestType(block, dest_ty, src); + + // Now analyze inner cast with dest_ty already resolved + const operand = try sema.resolveInst(zir_operand); + + // If inner cast already produced the correct type, skip redundant coercion + if (sema.typeOf(operand).eql(dest_ty, zcu)) { + return operand; + } + + // Otherwise perform outer coercion (handles vectors/arrays and other edge cases) + const is_ret = if (zir_dest_type.toIndex()) |ptr_index| + sema.code.instructions.items(.tag)[@intFromEnum(ptr_index)] == .ret_type + else + false; + return sema.coerceExtra(block, dest_ty, operand, src, .{ + .is_ret = is_ret, + .no_cast_to_comptime_int = no_cast_to_comptime_int + }) catch |err| switch (err) { + error.NotCoercible => unreachable, + else => |e| return e, + }; + }, + else => {}, + } + } + const operand = try sema.resolveInst(zir_operand); const dest_ty = try sema.resolveTypeOrPoison(block, src, zir_dest_type) orelse return operand; - switch (dest_ty.zigTypeTag(zcu)) { - .@"opaque" => return sema.fail(block, src, "cannot cast to opaque type '{f}'", .{dest_ty.fmt(pt)}), - .noreturn => return sema.fail(block, src, "cannot cast to noreturn", .{}), - else => {}, - } + try sema.validateCastDestType(block, dest_ty, src); const is_ret = if (zir_dest_type.toIndex()) |ptr_index| sema.code.instructions.items(.tag)[@intFromEnum(ptr_index)] == .ret_type diff --git a/test/behavior/cast_int.zig b/test/behavior/cast_int.zig index 8a1fcf79b2f0..f300cd754f86 100644 --- a/test/behavior/cast_int.zig +++ b/test/behavior/cast_int.zig @@ -251,3 +251,21 @@ test "load non byte-sized value in union" { try expect(pieces[1].type == .PAWN); try expect(pieces[1].color == .BLACK); } +test "@as with nested @intCast in loop with optional" { + // Regression test for circular dependency bug in Sema.analyzeAs + // where @as(DestType, @intCast(value)) would cause compilation timeout + // in complex control flow contexts (loop + short-circuit OR + optional unwrap) + const arr = [_]bool{ true, false, true }; + var opt: ?[]const bool = &arr; + var pid: u32 = 1; + _ = .{ &opt, &pid }; + + var i: usize = 0; + while (i < 3) : (i += 1) { + // This pattern previously caused infinite recursion during compilation + if (opt == null or (opt.?)[@as(usize, @intCast(pid))] == false) { + break; + } + } + try expect(i == 0); // Should break immediately since arr[1] == false +}