Skip to content

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

Description

@Cassunshine

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.

Activity

  1. added
    bugObserved behavior contradicts documented or intended behavior
    on Nov 24, 2025
  2. Justus2308 commented on Nov 24, 2025

    @Justus2308
    Member

    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);
    
    [...]
    
  3. Cassunshine commented on Nov 24, 2025

    @Cassunshine
    Author

    😔 i swear i searched for an existing issue, dangit

    well good to know i'm not crazy!

  4. ProCoder445 commented on Nov 24, 2025

    @ProCoder445

    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!

  5. Justus2308 commented on Nov 24, 2025

    @Justus2308
    Member

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugObserved behavior contradicts documented or intended behavior

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions