Commit 75acfcf0ea

Veikka Tuominen <git@vexu.eu>
2021-02-01 14:45:11
stage2: reimplement switch
1 parent 3ec5c9a
Changed files (4)
src/codegen/c.zig
@@ -414,11 +414,13 @@ pub fn genBody(o: *Object, body: ir.Body) error{ AnalysisFail, OutOfMemory }!voi
             .loop => try genLoop(o, inst.castTag(.loop).?),
             .condbr => try genCondBr(o, inst.castTag(.condbr).?),
             .br => try genBr(o, inst.castTag(.br).?),
-            .brvoid => try genBrVoid(o, inst.castTag(.brvoid).?.block),
+            .br_void => try genBrVoid(o, inst.castTag(.br_void).?.block),
             .switchbr => try genSwitchBr(o, inst.castTag(.switchbr).?),
-            // booland and boolor are non-short-circuit operations
-            .booland, .bitand => try genBinOp(o, inst.castTag(.booland).?, " & "),
-            .boolor, .bitor => try genBinOp(o, inst.castTag(.boolor).?, " | "),
+            // bool_and and bool_or are non-short-circuit operations
+            .bool_and => try genBinOp(o, inst.castTag(.bool_and).?, " & "),
+            .bool_or => try genBinOp(o, inst.castTag(.bool_or).?, " | "),
+            .bit_and => try genBinOp(o, inst.castTag(.bit_and).?, " & "),
+            .bit_or => try genBinOp(o, inst.castTag(.bit_or).?, " | "),
             .xor => try genBinOp(o, inst.castTag(.xor).?, " ^ "),
             .not => try genUnOp(o, inst.castTag(.not).?, "!"),
             else => |e| return o.dg.fail(o.dg.decl.src(), "TODO: C backend: implement codegen for {}", .{e}),
src/astgen.zig
@@ -309,7 +309,7 @@ pub fn expr(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node) InnerEr
         .Catch => return catchExpr(mod, scope, rl, node.castTag(.Catch).?),
         .Comptime => return comptimeKeyword(mod, scope, rl, node.castTag(.Comptime).?),
         .OrElse => return orelseExpr(mod, scope, rl, node.castTag(.OrElse).?),
-        .Switch => return mod.failNode(scope, node, "TODO implement astgen.expr for .Switch", .{}),
+        .Switch => return switchExpr(mod, scope, rl, node.castTag(.Switch).?),
         .ContainerDecl => return containerDecl(mod, scope, rl, node.castTag(.ContainerDecl).?),
 
         .Defer => return mod.failNode(scope, node, "TODO implement astgen.expr for .Defer", .{}),
@@ -2246,6 +2246,317 @@ fn forExpr(
     );
 }
 
+fn switchCaseUsesRef(node: *ast.Node.Switch) bool {
+    for (node.cases()) |uncasted_case| {
+        const case = uncasted_case.castTag(.SwitchCase).?;
+        const uncasted_payload = case.payload orelse continue;
+        const payload = uncasted_payload.castTag(.PointerPayload).?;
+        if (payload.ptr_token) |_| return true;
+    }
+    return false;
+}
+
+fn getRangeNode(node: *ast.Node) ?*ast.Node.SimpleInfixOp {
+    var cur = node;
+    while (true) {
+        switch (cur.tag) {
+            .Range => return @fieldParentPtr(ast.Node.SimpleInfixOp, "base", cur),
+            .GroupedExpression => cur = @fieldParentPtr(ast.Node.GroupedExpression, "base", cur).expr,
+            else => return null,
+        }
+    }
+}
+
+fn switchExpr(mod: *Module, scope: *Scope, rl: ResultLoc, switch_node: *ast.Node.Switch) InnerError!*zir.Inst {
+    const tree = scope.tree();
+    const switch_src = tree.token_locs[switch_node.switch_token].start;
+    const use_ref = switchCaseUsesRef(switch_node);
+
+    var block_scope: Scope.GenZIR = .{
+        .parent = scope,
+        .decl = scope.ownerDecl().?,
+        .arena = scope.arena(),
+        .force_comptime = scope.isComptime(),
+        .instructions = .{},
+    };
+    setBlockResultLoc(&block_scope, rl);
+    defer block_scope.instructions.deinit(mod.gpa);
+
+    var items = std.ArrayList(*zir.Inst).init(mod.gpa);
+    defer items.deinit();
+
+    // first we gather all the switch items and check else/'_' prongs
+    var else_src: ?usize = null;
+    var underscore_src: ?usize = null;
+    var first_range: ?*zir.Inst = null;
+    var simple_case_count: usize = 0;
+    for (switch_node.cases()) |uncasted_case| {
+        const case = uncasted_case.castTag(.SwitchCase).?;
+        const case_src = tree.token_locs[case.firstToken()].start;
+        assert(case.items_len != 0);
+
+        // Check for else/_ prong, those are handled last.
+        if (case.items_len == 1 and case.items()[0].tag == .SwitchElse) {
+            if (else_src) |src| {
+                const msg = msg: {
+                    const msg = try mod.errMsg(
+                        scope,
+                        case_src,
+                        "multiple else prongs in switch expression",
+                        .{},
+                    );
+                    errdefer msg.destroy(mod.gpa);
+                    try mod.errNote(scope, src, msg, "previous else prong is here", .{});
+                    break :msg msg;
+                };
+                return mod.failWithOwnedErrorMsg(scope, msg);
+            }
+            else_src = case_src;
+            continue;
+        } else if (case.items_len == 1 and case.items()[0].tag == .Identifier and
+            mem.eql(u8, tree.tokenSlice(case.items()[0].firstToken()), "_"))
+        {
+            if (underscore_src) |src| {
+                const msg = msg: {
+                    const msg = try mod.errMsg(
+                        scope,
+                        case_src,
+                        "multiple '_' prongs in switch expression",
+                        .{},
+                    );
+                    errdefer msg.destroy(mod.gpa);
+                    try mod.errNote(scope, src, msg, "previous '_' prong is here", .{});
+                    break :msg msg;
+                };
+                return mod.failWithOwnedErrorMsg(scope, msg);
+            }
+            underscore_src = case_src;
+            continue;
+        }
+
+        if (else_src) |some_else| {
+            if (underscore_src) |some_underscore| {
+                const msg = msg: {
+                    const msg = try mod.errMsg(
+                        scope,
+                        switch_src,
+                        "else and '_' prong in switch expression",
+                        .{},
+                    );
+                    errdefer msg.destroy(mod.gpa);
+                    try mod.errNote(scope, some_else, msg, "else prong is here", .{});
+                    try mod.errNote(scope, some_underscore, msg, "'_' prong is here", .{});
+                    break :msg msg;
+                };
+                return mod.failWithOwnedErrorMsg(scope, msg);
+            }
+        }
+
+        if (case.items_len == 1 and getRangeNode(case.items()[0]) == null) simple_case_count += 1;
+
+        // generate all the switch items as comptime expressions
+        for (case.items()) |item| {
+            if (getRangeNode(item)) |range| {
+                const start = try comptimeExpr(mod, &block_scope.base, .none, range.lhs);
+                const end = try comptimeExpr(mod, &block_scope.base, .none, range.rhs);
+                const range_src = tree.token_locs[range.op_token].start;
+                const range_inst = try addZIRBinOp(mod, &block_scope.base, range_src, .switch_range, start, end);
+                try items.append(range_inst);
+            } else {
+                const item_inst = try comptimeExpr(mod, &block_scope.base, .none, item);
+                try items.append(item_inst);
+            }
+        }
+    }
+
+    var special_prong: zir.Inst.SwitchBr.SpecialProng = .none;
+    if (else_src != null) special_prong = .@"else";
+    if (underscore_src != null) special_prong = .underscore;
+    var cases = try block_scope.arena.alloc(zir.Inst.SwitchBr.Case, simple_case_count);
+
+    const target_ptr = if (use_ref) try expr(mod, &block_scope.base, .ref, switch_node.expr) else null;
+    const target = if (target_ptr) |some|
+        try addZIRUnOp(mod, &block_scope.base, some.src, .deref, some)
+    else
+        try expr(mod, &block_scope.base, .none, switch_node.expr);
+    const switch_inst = try addZIRInst(mod, &block_scope.base, switch_src, zir.Inst.SwitchBr, .{
+        .target = target,
+        .cases = cases,
+        .items = try block_scope.arena.dupe(*zir.Inst, items.items),
+        .else_body = undefined, // populated below
+    }, .{
+        .range = first_range,
+        .special_prong = special_prong,
+    });
+
+    const block = try addZIRInstBlock(mod, scope, switch_src, .block, .{
+        .instructions = try block_scope.arena.dupe(*zir.Inst, block_scope.instructions.items),
+    });
+
+    var case_scope: Scope.GenZIR = .{
+        .parent = scope,
+        .decl = block_scope.decl,
+        .arena = block_scope.arena,
+        .force_comptime = block_scope.force_comptime,
+        .instructions = .{},
+    };
+    defer case_scope.instructions.deinit(mod.gpa);
+
+    var else_scope: Scope.GenZIR = .{
+        .parent = scope,
+        .decl = case_scope.decl,
+        .arena = case_scope.arena,
+        .force_comptime = case_scope.force_comptime,
+        .instructions = .{},
+    };
+    defer else_scope.instructions.deinit(mod.gpa);
+
+    // Now generate all but the special cases
+    var special_case: ?*ast.Node.SwitchCase = null;
+    var items_index: usize = 0;
+    var case_index: usize = 0;
+    for (switch_node.cases()) |uncasted_case| {
+        const case = uncasted_case.castTag(.SwitchCase).?;
+        const case_src = tree.token_locs[case.firstToken()].start;
+        // reset without freeing to reduce allocations.
+        case_scope.instructions.items.len = 0;
+
+        // Check for else/_ prong, those are handled last.
+        if (case.items_len == 1 and case.items()[0].tag == .SwitchElse) {
+            special_case = case;
+            continue;
+        } else if (case.items_len == 1 and case.items()[0].tag == .Identifier and
+            mem.eql(u8, tree.tokenSlice(case.items()[0].firstToken()), "_"))
+        {
+            special_case = case;
+            continue;
+        }
+
+        // If this is a simple one item prong then it is handled by the switchbr.
+        if (case.items_len == 1 and getRangeNode(case.items()[0]) == null) {
+            const item = items.items[items_index];
+            items_index += 1;
+            try switchCaseExpr(mod, &case_scope.base, block_scope.break_result_loc, block, case, target, target_ptr);
+
+            cases[case_index] = .{
+                .item = item,
+                .body = .{ .instructions = try scope.arena().dupe(*zir.Inst, case_scope.instructions.items) },
+            };
+            case_index += 1;
+            continue;
+        }
+
+        // TODO if the case has few items and no ranges it might be better
+        // to just handle them as switch prongs.
+
+        // Check if the target matches any of the items.
+        // 1, 2, 3..6 will result in
+        // target == 1 or target == 2 or (target >= 3 and target <= 6)
+        var any_ok: ?*zir.Inst = null;
+        for (case.items()) |item| {
+            if (getRangeNode(item)) |range| {
+                const range_src = tree.token_locs[range.op_token].start;
+                const range_inst = items.items[items_index].castTag(.switch_range).?;
+                items_index += 1;
+
+                // target >= start and target <= end
+                const range_start_ok = try addZIRBinOp(mod, &else_scope.base, range_src, .cmp_gte, target, range_inst.positionals.lhs);
+                const range_end_ok = try addZIRBinOp(mod, &else_scope.base, range_src, .cmp_lte, target, range_inst.positionals.rhs);
+                const range_ok = try addZIRBinOp(mod, &else_scope.base, range_src, .bool_and, range_start_ok, range_end_ok);
+
+                if (any_ok) |some| {
+                    any_ok = try addZIRBinOp(mod, &else_scope.base, range_src, .bool_or, some, range_ok);
+                } else {
+                    any_ok = range_ok;
+                }
+                continue;
+            }
+
+            const item_inst = items.items[items_index];
+            items_index += 1;
+            const cpm_ok = try addZIRBinOp(mod, &else_scope.base, item_inst.src, .cmp_eq, target, item_inst);
+
+            if (any_ok) |some| {
+                any_ok = try addZIRBinOp(mod, &else_scope.base, item_inst.src, .bool_or, some, cpm_ok);
+            } else {
+                any_ok = cpm_ok;
+            }
+        }
+
+        const condbr = try addZIRInstSpecial(mod, &case_scope.base, case_src, zir.Inst.CondBr, .{
+            .condition = any_ok.?,
+            .then_body = undefined, // populated below
+            .else_body = undefined, // populated below
+        }, .{});
+        const cond_block = try addZIRInstBlock(mod, &else_scope.base, case_src, .block, .{
+            .instructions = try scope.arena().dupe(*zir.Inst, case_scope.instructions.items),
+        });
+
+        // reset cond_scope for then_body
+        case_scope.instructions.items.len = 0;
+        try switchCaseExpr(mod, &case_scope.base, block_scope.break_result_loc, block, case, target, target_ptr);
+        condbr.positionals.then_body = .{
+            .instructions = try scope.arena().dupe(*zir.Inst, case_scope.instructions.items),
+        };
+
+        // reset cond_scope for else_body
+        case_scope.instructions.items.len = 0;
+        _ = try addZIRInst(mod, &case_scope.base, case_src, zir.Inst.BreakVoid, .{
+            .block = cond_block,
+        }, .{});
+        condbr.positionals.else_body = .{
+            .instructions = try scope.arena().dupe(*zir.Inst, case_scope.instructions.items),
+        };
+    }
+
+    // Finally generate else block or a break.
+    if (special_case) |case| {
+        try switchCaseExpr(mod, &else_scope.base, block_scope.break_result_loc, block, case, target, target_ptr);
+    } else {
+        // Not handling all possible cases is a compile error.
+        _ = try addZIRNoOp(mod, &else_scope.base, switch_src, .unreachable_unsafe);
+    }
+    switch_inst.castTag(.switchbr).?.positionals.else_body = .{
+        .instructions = try block_scope.arena.dupe(*zir.Inst, else_scope.instructions.items),
+    };
+
+    return &block.base;
+}
+
+fn switchCaseExpr(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    block: *zir.Inst.Block,
+    case: *ast.Node.SwitchCase,
+    target: *zir.Inst,
+    target_ptr: ?*zir.Inst,
+) !void {
+    const tree = scope.tree();
+    const case_src = tree.token_locs[case.firstToken()].start;
+    const sub_scope = blk: {
+        const uncasted_payload = case.payload orelse break :blk scope;
+        const payload = uncasted_payload.castTag(.PointerPayload).?;
+        const is_ptr = payload.ptr_token != null;
+        const value_name = tree.tokenSlice(payload.value_symbol.firstToken());
+        if (mem.eql(u8, value_name, "_")) {
+            if (is_ptr) {
+                return mod.failTok(scope, payload.ptr_token.?, "pointer modifier invalid on discard", .{});
+            }
+            break :blk scope;
+        }
+        return mod.failNode(scope, payload.value_symbol, "TODO implement switch value payload", .{});
+    };
+
+    const case_body = try expr(mod, sub_scope, rl, case.expr);
+    if (!case_body.tag.isNoReturn()) {
+        _ = try addZIRInst(mod, sub_scope, case_src, zir.Inst.Break, .{
+            .block = block,
+            .operand = case_body,
+        }, .{});
+    }
+}
+
 fn ret(mod: *Module, scope: *Scope, cfe: *ast.Node.ControlFlowExpression) InnerError!*zir.Inst {
     const tree = scope.tree();
     const src = tree.token_locs[cfe.ltoken].start;
src/zir.zig
@@ -338,6 +338,12 @@ pub const Inst = struct {
         enum_type,
         /// Does nothing; returns a void value.
         void_value,
+        /// A switch expression.
+        switchbr,
+        /// A range in a switch case, `lhs...rhs`.
+        /// Only checks that `lhs >= rhs` if they are ints, everything else is
+        /// validated by the .switch instruction.
+        switch_range,
 
         pub fn Type(tag: Tag) type {
             return switch (tag) {
@@ -435,6 +441,7 @@ pub const Inst = struct {
                 .error_union_type,
                 .merge_error_sets,
                 .slice_start,
+                .switch_range,
                 => BinOp,
 
                 .block,
@@ -478,6 +485,7 @@ pub const Inst = struct {
                 .enum_type => EnumType,
                 .union_type => UnionType,
                 .struct_type => StructType,
+                .switchbr => SwitchBr,
             };
         }
 
@@ -605,6 +613,8 @@ pub const Inst = struct {
                 .union_type,
                 .struct_type,
                 .void_value,
+                .switch_range,
+                .switchbr,
                 => false,
 
                 .@"break",
@@ -1171,6 +1181,36 @@ pub const Inst = struct {
             none,
         };
     };
+
+    pub const SwitchBr = struct {
+        pub const base_tag = Tag.switchbr;
+        base: Inst,
+
+        positionals: struct {
+            target: *Inst,
+            /// List of all individual items and ranges
+            items: []*Inst,
+            cases: []Case,
+            else_body: Body,
+        },
+        kw_args: struct {
+            /// Pointer to first range if such exists.
+            range: ?*Inst = null,
+            special_prong: SpecialProng = .none,
+        },
+
+        // Not anonymous due to stage1 limitations
+        pub const SpecialProng = enum {
+            none,
+            @"else",
+            underscore,
+        };
+
+        pub const Case = struct {
+            item: *Inst,
+            body: Body,
+        };
+    };
 };
 
 pub const ErrorMsg = struct {
@@ -1431,6 +1471,26 @@ const Writer = struct {
                 }
                 try stream.writeByte(']');
             },
+            []Inst.SwitchBr.Case => {
+                if (param.len == 0) {
+                    return stream.writeAll("{}");
+                }
+                try stream.writeAll("{\n");
+                for (param) |*case, i| {
+                    if (i != 0) {
+                        try stream.writeAll(",\n");
+                    }
+                    try stream.writeByteNTimes(' ', self.indent);
+                    self.indent += 2;
+                    try self.writeParamToStream(stream, &case.item);
+                    try stream.writeAll(" => ");
+                    try self.writeParamToStream(stream, &case.body);
+                    self.indent -= 2;
+                }
+                try stream.writeByte('\n');
+                try stream.writeByteNTimes(' ', self.indent - 2);
+                try stream.writeByte('}');
+            },
             else => |T| @compileError("unimplemented: rendering parameter of type " ++ @typeName(T)),
         }
     }
src/zir_sema.zig
@@ -154,6 +154,8 @@ pub fn analyzeInst(mod: *Module, scope: *Scope, old_inst: *zir.Inst) InnerError!
         .bool_and => return zirBoolOp(mod, scope, old_inst.castTag(.bool_and).?),
         .bool_or => return zirBoolOp(mod, scope, old_inst.castTag(.bool_or).?),
         .void_value => return mod.constVoid(scope, old_inst.src),
+        .switchbr => return zirSwitchBr(mod, scope, old_inst.castTag(.switchbr).?),
+        .switch_range => return zirSwitchRange(mod, scope, old_inst.castTag(.switch_range).?),
 
         .container_field_named,
         .container_field_typed,
@@ -1535,6 +1537,232 @@ fn zirSliceStart(mod: *Module, scope: *Scope, inst: *zir.Inst.BinOp) InnerError!
     return mod.analyzeSlice(scope, inst.base.src, array_ptr, start, null, null);
 }
 
+fn zirSwitchRange(mod: *Module, scope: *Scope, inst: *zir.Inst.BinOp) InnerError!*Inst {
+    const tracy = trace(@src());
+    defer tracy.end();
+    const start = try resolveInst(mod, scope, inst.positionals.lhs);
+    const end = try resolveInst(mod, scope, inst.positionals.rhs);
+
+    switch (start.ty.zigTypeTag()) {
+        .Int, .ComptimeInt => {},
+        else => return mod.constVoid(scope, inst.base.src),
+    }
+    switch (end.ty.zigTypeTag()) {
+        .Int, .ComptimeInt => {},
+        else => return mod.constVoid(scope, inst.base.src),
+    }
+    // .switch_range must be inside a comptime scope
+    const start_val = start.value().?;
+    const end_val = end.value().?;
+    if (start_val.compare(.gte, end_val)) {
+        return mod.fail(scope, inst.base.src, "range start value must be smaller than the end value", .{});
+    }
+    return mod.constVoid(scope, inst.base.src);
+}
+
+fn zirSwitchBr(mod: *Module, scope: *Scope, inst: *zir.Inst.SwitchBr) InnerError!*Inst {
+    const tracy = trace(@src());
+    defer tracy.end();
+    const target = try resolveInst(mod, scope, inst.positionals.target);
+    try validateSwitch(mod, scope, target, inst);
+
+    if (try mod.resolveDefinedValue(scope, target)) |target_val| {
+        for (inst.positionals.cases) |case| {
+            const resolved = try resolveInst(mod, scope, case.item);
+            const casted = try mod.coerce(scope, target.ty, resolved);
+            const item = try mod.resolveConstValue(scope, casted);
+
+            if (target_val.eql(item)) {
+                try analyzeBody(mod, scope.cast(Scope.Block).?, case.body);
+                return mod.constNoReturn(scope, inst.base.src);
+            }
+        }
+        try analyzeBody(mod, scope.cast(Scope.Block).?, inst.positionals.else_body);
+        return mod.constNoReturn(scope, inst.base.src);
+    }
+
+    if (inst.positionals.cases.len == 0) {
+        // no cases just analyze else_branch
+        try analyzeBody(mod, scope.cast(Scope.Block).?, inst.positionals.else_body);
+        return mod.constNoReturn(scope, inst.base.src);
+    }
+
+    const parent_block = try mod.requireRuntimeBlock(scope, inst.base.src);
+    const cases = try parent_block.arena.alloc(Inst.SwitchBr.Case, inst.positionals.cases.len);
+
+    var case_block: Scope.Block = .{
+        .parent = parent_block,
+        .inst_table = parent_block.inst_table,
+        .func = parent_block.func,
+        .owner_decl = parent_block.owner_decl,
+        .src_decl = parent_block.src_decl,
+        .instructions = .{},
+        .arena = parent_block.arena,
+        .inlining = parent_block.inlining,
+        .is_comptime = parent_block.is_comptime,
+        .branch_quota = parent_block.branch_quota,
+    };
+    defer case_block.instructions.deinit(mod.gpa);
+
+    for (inst.positionals.cases) |case, i| {
+        // Reset without freeing.
+        case_block.instructions.items.len = 0;
+
+        const resolved = try resolveInst(mod, scope, case.item);
+        const casted = try mod.coerce(scope, target.ty, resolved);
+        const item = try mod.resolveConstValue(scope, casted);
+
+        try analyzeBody(mod, &case_block, case.body);
+
+        cases[i] = .{
+            .item = item,
+            .body = .{ .instructions = try parent_block.arena.dupe(*Inst, case_block.instructions.items) },
+        };
+    }
+
+    case_block.instructions.items.len = 0;
+    try analyzeBody(mod, &case_block, inst.positionals.else_body);
+
+    const else_body: ir.Body = .{
+        .instructions = try parent_block.arena.dupe(*Inst, case_block.instructions.items),
+    };
+
+    return mod.addSwitchBr(parent_block, inst.base.src, target, cases, else_body);
+}
+
+fn validateSwitch(mod: *Module, scope: *Scope, target: *Inst, inst: *zir.Inst.SwitchBr) InnerError!void {
+    // validate usage of '_' prongs
+    if (inst.kw_args.special_prong == .underscore and target.ty.zigTypeTag() != .Enum) {
+        return mod.fail(scope, inst.base.src, "'_' prong only allowed when switching on non-exhaustive enums", .{});
+        // TODO notes "'_' prong here" inst.positionals.cases[last].src
+    }
+
+    // check that target type supports ranges
+    if (inst.kw_args.range) |range_inst| {
+        switch (target.ty.zigTypeTag()) {
+            .Int, .ComptimeInt => {},
+            else => {
+                return mod.fail(scope, target.src, "ranges not allowed when switching on type {}", .{target.ty});
+                // TODO notes "range used here" range_inst.src
+            },
+        }
+    }
+
+    // validate for duplicate items/missing else prong
+    switch (target.ty.zigTypeTag()) {
+        .Enum => return mod.fail(scope, inst.base.src, "TODO validateSwitch .Enum", .{}),
+        .ErrorSet => return mod.fail(scope, inst.base.src, "TODO validateSwitch .ErrorSet", .{}),
+        .Union => return mod.fail(scope, inst.base.src, "TODO validateSwitch .Union", .{}),
+        .Int, .ComptimeInt => {
+            var range_set = @import("RangeSet.zig").init(mod.gpa);
+            defer range_set.deinit();
+
+            for (inst.positionals.items) |item| {
+                const maybe_src = if (item.castTag(.switch_range)) |range| blk: {
+                    const start_resolved = try resolveInst(mod, scope, range.positionals.lhs);
+                    const start_casted = try mod.coerce(scope, target.ty, start_resolved);
+                    const end_resolved = try resolveInst(mod, scope, range.positionals.rhs);
+                    const end_casted = try mod.coerce(scope, target.ty, end_resolved);
+
+                    break :blk try range_set.add(
+                        try mod.resolveConstValue(scope, start_casted),
+                        try mod.resolveConstValue(scope, end_casted),
+                        item.src,
+                    );
+                } else blk: {
+                    const resolved = try resolveInst(mod, scope, item);
+                    const casted = try mod.coerce(scope, target.ty, resolved);
+                    const value = try mod.resolveConstValue(scope, casted);
+                    break :blk try range_set.add(value, value, item.src);
+                };
+
+                if (maybe_src) |previous_src| {
+                    return mod.fail(scope, item.src, "duplicate switch value", .{});
+                    // TODO notes "previous value is here" previous_src
+                }
+            }
+
+            if (target.ty.zigTypeTag() == .Int) {
+                var arena = std.heap.ArenaAllocator.init(mod.gpa);
+                defer arena.deinit();
+
+                const start = try target.ty.minInt(&arena, mod.getTarget());
+                const end = try target.ty.maxInt(&arena, mod.getTarget());
+                if (try range_set.spans(start, end)) {
+                    if (inst.kw_args.special_prong == .@"else") {
+                        return mod.fail(scope, inst.base.src, "unreachable else prong, all cases already handled", .{});
+                    }
+                    return;
+                }
+            }
+
+            if (inst.kw_args.special_prong != .@"else") {
+                return mod.fail(scope, inst.base.src, "switch must handle all possibilities", .{});
+            }
+        },
+        .Bool => {
+            var true_count: u8 = 0;
+            var false_count: u8 = 0;
+            for (inst.positionals.items) |item| {
+                const resolved = try resolveInst(mod, scope, item);
+                const casted = try mod.coerce(scope, Type.initTag(.bool), resolved);
+                if ((try mod.resolveConstValue(scope, casted)).toBool()) {
+                    true_count += 1;
+                } else {
+                    false_count += 1;
+                }
+
+                if (true_count + false_count > 2) {
+                    return mod.fail(scope, item.src, "duplicate switch value", .{});
+                }
+            }
+            if ((true_count + false_count < 2) and inst.kw_args.special_prong != .@"else") {
+                return mod.fail(scope, inst.base.src, "switch must handle all possibilities", .{});
+            }
+            if ((true_count + false_count == 2) and inst.kw_args.special_prong == .@"else") {
+                return mod.fail(scope, inst.base.src, "unreachable else prong, all cases already handled", .{});
+            }
+        },
+        .EnumLiteral, .Void, .Fn, .Pointer, .Type => {
+            if (inst.kw_args.special_prong != .@"else") {
+                return mod.fail(scope, inst.base.src, "else prong required when switching on type '{}'", .{target.ty});
+            }
+
+            var seen_values = std.HashMap(Value, usize, Value.hash, Value.eql, std.hash_map.DefaultMaxLoadPercentage).init(mod.gpa);
+            defer seen_values.deinit();
+
+            for (inst.positionals.items) |item| {
+                const resolved = try resolveInst(mod, scope, item);
+                const casted = try mod.coerce(scope, target.ty, resolved);
+                const val = try mod.resolveConstValue(scope, casted);
+
+                if (try seen_values.fetchPut(val, item.src)) |prev| {
+                    return mod.fail(scope, item.src, "duplicate switch value", .{});
+                    // TODO notes "previous value here" prev.value
+                }
+            }
+        },
+
+        .ErrorUnion,
+        .NoReturn,
+        .Array,
+        .Struct,
+        .Undefined,
+        .Null,
+        .Optional,
+        .BoundFn,
+        .Opaque,
+        .Vector,
+        .Frame,
+        .AnyFrame,
+        .ComptimeFloat,
+        .Float,
+        => {
+            return mod.fail(scope, target.src, "invalid switch target type '{}'", .{target.ty});
+        },
+    }
+}
+
 fn zirImport(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError!*Inst {
     const tracy = trace(@src());
     defer tracy.end();