zig icon indicating copy to clipboard operation
zig copied to clipboard

Discarding first inline else capture on tagged union switch when using labeled switch fails to compile

Open Cassunshine opened this issue 2 months ago • 4 comments

Zig Version

0.15.2

Steps to Reproduce and Observed Behavior

Sorry for the bulky issue name :< The bug is just kinda that specific.


const TaggedUnion = union(enum) {
    a: u32,
    b: i32,
    c: void,
};

// In some function...
    const tu: TaggedUnion = .{ .a = 15 };
    sw: switch (tu) {
        inline else => |_, tag| {
            // Code body doesn't matter, but presumably you're using tag somewhere.
            _ = tag;
            continue :sw next; // Any continue value still fails
        },
    }

The code above will fail to compile with no sensible error (only returning error code 1), despite the fact that this should be valid as far as I can tell.

Replacing inline else => |_, tag| with inline else => |value, tag| and then discarding value using _ = value compiles just fine.

I don't personally see any use cases where this would actually matter, but it definitely shouldn't just crash without explanation like it does.

Expected Behavior

The given code should compile, or if it's not valid for some reason, it should return an actual compiler error instead of simply crashing the compiler.

Cassunshine avatar Nov 24 '25 16:11 Cassunshine

Same issue as #24789

This is indeed a compiler bug your code should be valid :)

error trace etc
Compiler crash context:
Analyzing 'build/repro.zig'
      %15 = dbg_stmt(2, 5)
      %16 = block_comptime(reason=type, {
        %17 = decl_val("TaggedUnion") token_offset:307:15 to :307:26
        %18 = break_inline(%16, %17)
      }) node_offset:307:15 to :307:26
      %19 = alloc(%16) node_offset:307:5 to :307:41
      %20 = opt_eu_base_ptr_init(%19) node_offset:307:29 to :307:41
      %21 = struct_init_field_ptr(%20, "a") node_offset:307:37 to :307:39
      %22 = int(15)
      %23 = store_node(%21, %22) node_offset:307:37 to :307:39
      %24 = validate_ptr_struct_init({
        %21 = struct_init_field_ptr(%20, "a") node_offset:307:37 to :307:39
      }) node_offset:307:29 to :307:41
      %25 = make_ptr_const(%19) node_offset:307:5 to :307:41
      %26 = dbg_var_ptr(%25, "tu")
      %27 = dbg_stmt(3, 5)
      %28 = load(%25) node_offset:308:17 to :308:19
      %29 = typeof(%28) node_offset:308:17 to :308:19
      %30 = dbg_stmt(3, 17)
    > %31 = switch_block(%28, tag_capture=%32,
        by_val inline else => {
          %33 = dbg_var_val(%32, "tag")
          %34 = dbg_stmt(7, 13)
          %35 = ensure_result_non_error(%32) node_offset:312:17 to :312:20
          %36 = dbg_stmt(8, 13)
          %37 = validate_struct_init_result_ty(%29) node_offset:313:26 to :313:40
          %38 = struct_init_field_type(%29, a) node_offset:313:34 to :313:38
          %39 = struct_init([%38, %14]) node_offset:313:26 to :313:40
          %40 = restore_err_ret_index_unconditional(%31) node_offset:313:13 to :313:40
          %41 = switch_continue(%31, %39)
          %42 = break(%31, @void_value)
        }) node_offset:308:5 to :308:7
      %43 = ensure_result_used(%31) node_offset:308:5 to :308:7
      %44 = restore_err_ret_index_unconditional(.none) node_offset:306:1 to :306:10
      %45 = ret_implicit(@void_value) token_offset:316:1 to :316:1
    For full context, use the command
      build/stage4-master/bin/zig ast-check -t build/repro.zig


thread 278351901 panic: reached unreachable code
zig/src/InternPool.zig:10549:17: 0x107212a4b in childType (zig)
        else => unreachable,
                ^
zig/src/Type.zig:1946:42: 0x106fa09e7 in childTypeIp (zig)
    return Type.fromInterned(ip.childType(ty.toIntern()));
                                         ^
zig/src/Type.zig:1942:23: 0x106e7e4e3 in childType (zig)
    return childTypeIp(ty, &zcu.intern_pool);
                      ^
zig/src/Sema.zig:10586:55: 0x108b0b803 in analyzeTagCapture (zig)
                const alloc_child = alloc_ty.childType(zcu);
                                                      ^
zig/src/Sema.zig:10540:54: 0x108b12a33 in analyzeProngRuntime (zig)
            const tag_ref = try spa.analyzeTagCapture(case_block, capture_src, inline_case_capture);

[...]

Justus2308 avatar Nov 24 '25 17:11 Justus2308

😔 i swear i searched for an existing issue, dangit

well good to know i'm not crazy!

Cassunshine avatar Nov 24 '25 17:11 Cassunshine

Can you please explain the compiler's error trace? I'm not familiar enough with the language to understand it. I'm new to the zig world,, hello!

ProCoder445 avatar Nov 24 '25 19:11 ProCoder445

Sure! First off, to even get this kind of error trace you need a debug build of the compiler, further details are in the build from source section of the README.

Everything below 'Compiler crash context:' (all of the % stuff) is ZIR (zig intermediate representation), it's basically zig code that's already been parsed and turned into a series of instructions, but has not yet been semantically analyzed (basically type resolution/checking and everything comptime related). For more details on how ZIR works and how it's semantically analyzed have a look at this great post by mlugg.

The little > points out where something went wrong. In this case it's pointed at a switch_block instruction, so the compiler crashed while trying to semantically analyze a switch statement.

From there on it's just a normal stack trace. I shortened it a bit because the rest isn't really relevant, the issue is already known anyway. What's happening is that analyzeTagCapture is accessing an undefined value (see #24792 for more details):

    fn analyzeTagCapture(
        spa: SwitchProngAnalysis,
        block: *Block,
        capture_src: LazySrcLoc,
        inline_case_capture: Air.Inst.Ref,
    ) CompileError!Air.Inst.Ref {
        const sema = spa.sema;
        const pt = sema.pt;
        const zcu = pt.zcu;
        const operand_ty = switch (spa.operand) {
            .simple => |s| sema.typeOf(s.by_val),
            .loop => |l| ty: {
                const alloc_ty = sema.typeOf(l.operand_alloc); // <- operand_alloc is undefined at this point, so alloc_ty is also a garbage value
                const alloc_child = alloc_ty.childType(zcu); // <- the crash happens here, might as well have happened a line earlier
                if (l.operand_is_ref) break :ty alloc_child.childType(zcu);
                break :ty alloc_child;
            },
        };

[...]

https://github.com/ziglang/zig/blob/3c647ca6bb987c496652fd6bf279ef2625f41821/src/Sema.zig#L10562-L10579

Welcome to the zig world! If you have more questions I'd recommend joining a zig community

Justus2308 avatar Nov 24 '25 19:11 Justus2308