Commit aac6e8c418

Andrew Kelley <andrew@ziglang.org>
2020-07-24 08:05:26
self-hosted: AST flattening, astgen improvements, result locations, and more
* AST: flatten ControlFlowExpression into Continue, Break, and Return. * AST: unify identifiers and literals into the same AST type: OneToken * AST: ControlFlowExpression uses TrailerFlags to optimize storage space. * astgen: support `var` as well as `const` locals, and support explicitly typed locals. Corresponding Module and codegen code is not implemented yet. * astgen: support result locations. * ZIR: add the following instructions (see the corresponding doc comments for explanations of semantics): - alloc - alloc_inferred - bitcast_result_ptr - coerce_result_block_ptr - coerce_result_ptr - coerce_to_ptr_elem - ensure_result_used - ensure_result_non_error - ret_ptr - ret_type - store - param_type * the skeleton structure for result locations is set up. It's looking pretty clean so far. * add compile error for unused result and compile error for discarding errors. * astgen: split builtin calls up to implemented manually, and implement `@as`, `@bitCast` (and others) with respect to result locations. * add CLI support for hex and raw object formats. They are not supported by the self-hosted compiler yet, and emit errors. * rename `--c` CLI to `-ofmt=[objectformat]` which can be any of the object formats. Only ELF and C are supported so far. Also added missing help to the help text. * Remove hard tabs from C backend test cases. Shame on you Noam, you are grounded, you should know better, etc. Bad boy. * Delete C backend code and test case that relied on comptime_int incorrectly making it all the way to codegen.
1 parent b4d383a
lib/std/zig/ast.zig
@@ -495,8 +495,10 @@ pub const Node = struct {
         While,
         For,
         If,
-        ControlFlowExpression,
         Suspend,
+        Continue,
+        Break,
+        Return,
 
         // Type expressions
         AnyType,
@@ -601,6 +603,24 @@ pub const Node = struct {
                 .Try,
                 => SimplePrefixOp,
 
+                .Identifier,
+                .BoolLiteral,
+                .NullLiteral,
+                .UndefinedLiteral,
+                .Unreachable,
+                .AnyType,
+                .ErrorType,
+                .IntegerLiteral,
+                .FloatLiteral,
+                .StringLiteral,
+                .CharLiteral,
+                => OneToken,
+
+                .Continue,
+                .Break,
+                .Return,
+                => ControlFlowExpression,
+
                 .ArrayType => ArrayType,
                 .ArrayTypeSentinel => ArrayTypeSentinel,
 
@@ -621,23 +641,11 @@ pub const Node = struct {
                 .While => While,
                 .For => For,
                 .If => If,
-                .ControlFlowExpression => ControlFlowExpression,
                 .Suspend => Suspend,
-                .AnyType => AnyType,
-                .ErrorType => ErrorType,
                 .FnProto => FnProto,
                 .AnyFrameType => AnyFrameType,
-                .IntegerLiteral => IntegerLiteral,
-                .FloatLiteral => FloatLiteral,
                 .EnumLiteral => EnumLiteral,
-                .StringLiteral => StringLiteral,
                 .MultilineStringLiteral => MultilineStringLiteral,
-                .CharLiteral => CharLiteral,
-                .BoolLiteral => BoolLiteral,
-                .NullLiteral => NullLiteral,
-                .UndefinedLiteral => UndefinedLiteral,
-                .Unreachable => Unreachable,
-                .Identifier => Identifier,
                 .GroupedExpression => GroupedExpression,
                 .BuiltinCall => BuiltinCall,
                 .ErrorSetDecl => ErrorSetDecl,
@@ -1182,19 +1190,19 @@ pub const Node = struct {
         }
     };
 
-    pub const Identifier = struct {
-        base: Node = Node{ .tag = .Identifier },
+    pub const OneToken = struct {
+        base: Node,
         token: TokenIndex,
 
-        pub fn iterate(self: *const Identifier, index: usize) ?*Node {
+        pub fn iterate(self: *const OneToken, index: usize) ?*Node {
             return null;
         }
 
-        pub fn firstToken(self: *const Identifier) TokenIndex {
+        pub fn firstToken(self: *const OneToken) TokenIndex {
             return self.token;
         }
 
-        pub fn lastToken(self: *const Identifier) TokenIndex {
+        pub fn lastToken(self: *const OneToken) TokenIndex {
             return self.token;
         }
     };
@@ -2569,34 +2577,65 @@ pub const Node = struct {
         }
     };
 
-    /// TODO break this into separate Break, Continue, Return AST Nodes to save memory.
-    /// Could be further broken into LabeledBreak, LabeledContinue, and ReturnVoid to save even more.
+    /// Trailed in memory by possibly many things, with each optional thing
+    /// determined by a bit in `trailer_flags`.
+    /// Can be: return, break, continue
     pub const ControlFlowExpression = struct {
-        base: Node = Node{ .tag = .ControlFlowExpression },
+        base: Node,
+        trailer_flags: TrailerFlags,
         ltoken: TokenIndex,
-        kind: Kind,
-        rhs: ?*Node,
 
-        pub const Kind = union(enum) {
-            Break: ?*Node,
-            Continue: ?*Node,
-            Return,
+        pub const TrailerFlags = std.meta.TrailerFlags(struct {
+            rhs: *Node,
+            label: TokenIndex,
+        });
+
+        pub const RequiredFields = struct {
+            tag: Tag,
+            ltoken: TokenIndex,
         };
 
+        pub fn getRHS(self: *const ControlFlowExpression) ?*Node {
+            return self.getTrailer("rhs");
+        }
+
+        pub fn getLabel(self: *const ControlFlowExpression) ?TokenIndex {
+            return self.getTrailer("label");
+        }
+
+        pub fn getTrailer(self: *const ControlFlowExpression, comptime name: []const u8) ?TrailerFlags.Field(name) {
+            const trailers_start = @ptrCast([*]const u8, self) + @sizeOf(ControlFlowExpression);
+            return self.trailer_flags.get(trailers_start, name);
+        }
+
+        pub fn setTrailer(self: *ControlFlowExpression, comptime name: []const u8, value: TrailerFlags.Field(name)) void {
+            const trailers_start = @ptrCast([*]u8, self) + @sizeOf(ControlFlowExpression);
+            self.trailer_flags.set(trailers_start, name, value);
+        }
+
+        pub fn create(allocator: *mem.Allocator, required: RequiredFields, trailers: anytype) !*ControlFlowExpression {
+            const trailer_flags = TrailerFlags.init(trailers);
+            const bytes = try allocator.alignedAlloc(u8, @alignOf(ControlFlowExpression), sizeInBytes(trailer_flags));
+            const ctrl_flow_expr = @ptrCast(*ControlFlowExpression, bytes.ptr);
+            ctrl_flow_expr.* = .{
+                .base = .{ .tag = required.tag },
+                .trailer_flags = trailer_flags,
+                .ltoken = required.ltoken,
+            };
+            const trailers_start = bytes.ptr + @sizeOf(ControlFlowExpression);
+            trailer_flags.setMany(trailers_start, trailers);
+            return ctrl_flow_expr;
+        }
+
+        pub fn destroy(self: *ControlFlowExpression, allocator: *mem.Allocator) void {
+            const bytes = @ptrCast([*]u8, self)[0..sizeInBytes(self.trailer_flags)];
+            allocator.free(bytes);
+        }
+
         pub fn iterate(self: *const ControlFlowExpression, index: usize) ?*Node {
             var i = index;
 
-            switch (self.kind) {
-                .Break, .Continue => |maybe_label| {
-                    if (maybe_label) |label| {
-                        if (i < 1) return label;
-                        i -= 1;
-                    }
-                },
-                .Return => {},
-            }
-
-            if (self.rhs) |rhs| {
+            if (self.getRHS()) |rhs| {
                 if (i < 1) return rhs;
                 i -= 1;
             }
@@ -2609,21 +2648,20 @@ pub const Node = struct {
         }
 
         pub fn lastToken(self: *const ControlFlowExpression) TokenIndex {
-            if (self.rhs) |rhs| {
+            if (self.getRHS()) |rhs| {
                 return rhs.lastToken();
             }
 
-            switch (self.kind) {
-                .Break, .Continue => |maybe_label| {
-                    if (maybe_label) |label| {
-                        return label.lastToken();
-                    }
-                },
-                .Return => return self.ltoken,
+            if (self.getLabel()) |label| {
+                return label;
             }
 
             return self.ltoken;
         }
+
+        fn sizeInBytes(trailer_flags: TrailerFlags) usize {
+            return @sizeOf(ControlFlowExpression) + trailer_flags.sizeInBytes();
+        }
     };
 
     pub const Suspend = struct {
@@ -2655,23 +2693,6 @@ pub const Node = struct {
         }
     };
 
-    pub const IntegerLiteral = struct {
-        base: Node = Node{ .tag = .IntegerLiteral },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const IntegerLiteral, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const IntegerLiteral) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const IntegerLiteral) TokenIndex {
-            return self.token;
-        }
-    };
-
     pub const EnumLiteral = struct {
         base: Node = Node{ .tag = .EnumLiteral },
         dot: TokenIndex,
@@ -2690,23 +2711,6 @@ pub const Node = struct {
         }
     };
 
-    pub const FloatLiteral = struct {
-        base: Node = Node{ .tag = .FloatLiteral },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const FloatLiteral, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const FloatLiteral) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const FloatLiteral) TokenIndex {
-            return self.token;
-        }
-    };
-
     /// Parameters are in memory following BuiltinCall.
     pub const BuiltinCall = struct {
         base: Node = Node{ .tag = .BuiltinCall },
@@ -2757,23 +2761,6 @@ pub const Node = struct {
         }
     };
 
-    pub const StringLiteral = struct {
-        base: Node = Node{ .tag = .StringLiteral },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const StringLiteral, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const StringLiteral) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const StringLiteral) TokenIndex {
-            return self.token;
-        }
-    };
-
     /// The string literal tokens appear directly in memory after MultilineStringLiteral.
     pub const MultilineStringLiteral = struct {
         base: Node = Node{ .tag = .MultilineStringLiteral },
@@ -2817,74 +2804,6 @@ pub const Node = struct {
         }
     };
 
-    pub const CharLiteral = struct {
-        base: Node = Node{ .tag = .CharLiteral },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const CharLiteral, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const CharLiteral) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const CharLiteral) TokenIndex {
-            return self.token;
-        }
-    };
-
-    pub const BoolLiteral = struct {
-        base: Node = Node{ .tag = .BoolLiteral },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const BoolLiteral, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const BoolLiteral) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const BoolLiteral) TokenIndex {
-            return self.token;
-        }
-    };
-
-    pub const NullLiteral = struct {
-        base: Node = Node{ .tag = .NullLiteral },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const NullLiteral, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const NullLiteral) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const NullLiteral) TokenIndex {
-            return self.token;
-        }
-    };
-
-    pub const UndefinedLiteral = struct {
-        base: Node = Node{ .tag = .UndefinedLiteral },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const UndefinedLiteral, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const UndefinedLiteral) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const UndefinedLiteral) TokenIndex {
-            return self.token;
-        }
-    };
-
     pub const Asm = struct {
         base: Node = Node{ .tag = .Asm },
         asm_token: TokenIndex,
@@ -2904,7 +2823,7 @@ pub const Node = struct {
             rparen: TokenIndex,
 
             pub const Kind = union(enum) {
-                Variable: *Identifier,
+                Variable: *OneToken,
                 Return: *Node,
             };
 
@@ -3005,57 +2924,6 @@ pub const Node = struct {
         }
     };
 
-    pub const Unreachable = struct {
-        base: Node = Node{ .tag = .Unreachable },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const Unreachable, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const Unreachable) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const Unreachable) TokenIndex {
-            return self.token;
-        }
-    };
-
-    pub const ErrorType = struct {
-        base: Node = Node{ .tag = .ErrorType },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const ErrorType, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const ErrorType) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const ErrorType) TokenIndex {
-            return self.token;
-        }
-    };
-
-    pub const AnyType = struct {
-        base: Node = Node{ .tag = .AnyType },
-        token: TokenIndex,
-
-        pub fn iterate(self: *const AnyType, index: usize) ?*Node {
-            return null;
-        }
-
-        pub fn firstToken(self: *const AnyType) TokenIndex {
-            return self.token;
-        }
-
-        pub fn lastToken(self: *const AnyType) TokenIndex {
-            return self.token;
-        }
-    };
-
     /// TODO remove from the Node base struct
     /// TODO actually maybe remove entirely in favor of iterating backward from Node.firstToken()
     /// and forwards to find same-line doc comments.
lib/std/zig/parse.zig
@@ -628,8 +628,11 @@ const Parser = struct {
         var type_expr: ?*Node = null;
         if (p.eatToken(.Colon)) |_| {
             if (p.eatToken(.Keyword_anytype) orelse p.eatToken(.Keyword_var)) |anytype_tok| {
-                const node = try p.arena.allocator.create(Node.AnyType);
-                node.* = .{ .token = anytype_tok };
+                const node = try p.arena.allocator.create(Node.OneToken);
+                node.* = .{
+                    .base = .{ .tag = .AnyType },
+                    .token = anytype_tok,
+                };
                 type_expr = &node.base;
             } else {
                 type_expr = try p.expectNode(parseTypeExpr, .{
@@ -1079,12 +1082,13 @@ const Parser = struct {
         if (p.eatToken(.Keyword_break)) |token| {
             const label = try p.parseBreakLabel();
             const expr_node = try p.parseExpr();
-            const node = try p.arena.allocator.create(Node.ControlFlowExpression);
-            node.* = .{
+            const node = try Node.ControlFlowExpression.create(&p.arena.allocator, .{
+                .tag = .Break,
                 .ltoken = token,
-                .kind = .{ .Break = label },
+            }, .{
+                .label = label,
                 .rhs = expr_node,
-            };
+            });
             return &node.base;
         }
 
@@ -1115,12 +1119,13 @@ const Parser = struct {
 
         if (p.eatToken(.Keyword_continue)) |token| {
             const label = try p.parseBreakLabel();
-            const node = try p.arena.allocator.create(Node.ControlFlowExpression);
-            node.* = .{
+            const node = try Node.ControlFlowExpression.create(&p.arena.allocator, .{
+                .tag = .Continue,
                 .ltoken = token,
-                .kind = .{ .Continue = label },
+            }, .{
+                .label = label,
                 .rhs = null,
-            };
+            });
             return &node.base;
         }
 
@@ -1139,12 +1144,12 @@ const Parser = struct {
 
         if (p.eatToken(.Keyword_return)) |token| {
             const expr_node = try p.parseExpr();
-            const node = try p.arena.allocator.create(Node.ControlFlowExpression);
-            node.* = .{
+            const node = try Node.ControlFlowExpression.create(&p.arena.allocator, .{
+                .tag = .Return,
                 .ltoken = token,
-                .kind = .Return,
+            }, .{
                 .rhs = expr_node,
-            };
+            });
             return &node.base;
         }
 
@@ -1516,8 +1521,9 @@ const Parser = struct {
     fn parsePrimaryTypeExpr(p: *Parser) !?*Node {
         if (try p.parseBuiltinCall()) |node| return node;
         if (p.eatToken(.CharLiteral)) |token| {
-            const node = try p.arena.allocator.create(Node.CharLiteral);
+            const node = try p.arena.allocator.create(Node.OneToken);
             node.* = .{
+                .base = .{ .tag = .CharLiteral },
                 .token = token,
             };
             return &node.base;
@@ -1547,7 +1553,7 @@ const Parser = struct {
             const identifier = try p.expectNodeRecoverable(parseIdentifier, .{
                 .ExpectedIdentifier = .{ .token = p.tok_i },
             });
-            const global_error_set = try p.createLiteral(Node.ErrorType, token);
+            const global_error_set = try p.createLiteral(.ErrorType, token);
             if (period == null or identifier == null) return global_error_set;
 
             const node = try p.arena.allocator.create(Node.SimpleInfixOp);
@@ -1559,8 +1565,8 @@ const Parser = struct {
             };
             return &node.base;
         }
-        if (p.eatToken(.Keyword_false)) |token| return p.createLiteral(Node.BoolLiteral, token);
-        if (p.eatToken(.Keyword_null)) |token| return p.createLiteral(Node.NullLiteral, token);
+        if (p.eatToken(.Keyword_false)) |token| return p.createLiteral(.BoolLiteral, token);
+        if (p.eatToken(.Keyword_null)) |token| return p.createLiteral(.NullLiteral, token);
         if (p.eatToken(.Keyword_anyframe)) |token| {
             const node = try p.arena.allocator.create(Node.AnyFrameType);
             node.* = .{
@@ -1569,9 +1575,9 @@ const Parser = struct {
             };
             return &node.base;
         }
-        if (p.eatToken(.Keyword_true)) |token| return p.createLiteral(Node.BoolLiteral, token);
-        if (p.eatToken(.Keyword_undefined)) |token| return p.createLiteral(Node.UndefinedLiteral, token);
-        if (p.eatToken(.Keyword_unreachable)) |token| return p.createLiteral(Node.Unreachable, token);
+        if (p.eatToken(.Keyword_true)) |token| return p.createLiteral(.BoolLiteral, token);
+        if (p.eatToken(.Keyword_undefined)) |token| return p.createLiteral(.UndefinedLiteral, token);
+        if (p.eatToken(.Keyword_unreachable)) |token| return p.createLiteral(.Unreachable, token);
         if (try p.parseStringLiteral()) |node| return node;
         if (try p.parseSwitchExpr()) |node| return node;
 
@@ -1865,7 +1871,7 @@ const Parser = struct {
             const variable = try p.expectNode(parseIdentifier, .{
                 .ExpectedIdentifier = .{ .token = p.tok_i },
             });
-            break :blk .{ .Variable = variable.cast(Node.Identifier).? };
+            break :blk .{ .Variable = variable.castTag(.Identifier).? };
         };
         const rparen = try p.expectToken(.RParen);
 
@@ -1906,11 +1912,10 @@ const Parser = struct {
     }
 
     /// BreakLabel <- COLON IDENTIFIER
-    fn parseBreakLabel(p: *Parser) !?*Node {
+    fn parseBreakLabel(p: *Parser) !?TokenIndex {
         _ = p.eatToken(.Colon) orelse return null;
-        return try p.expectNode(parseIdentifier, .{
-            .ExpectedIdentifier = .{ .token = p.tok_i },
-        });
+        const ident = try p.expectToken(.Identifier);
+        return ident;
     }
 
     /// BlockLabel <- IDENTIFIER COLON
@@ -3022,8 +3027,9 @@ const Parser = struct {
             });
 
             // lets pretend this was an identifier so we can continue parsing
-            const node = try p.arena.allocator.create(Node.Identifier);
+            const node = try p.arena.allocator.create(Node.OneToken);
             node.* = .{
+                .base = .{ .tag = .Identifier },
                 .token = token,
             };
             return &node.base;
@@ -3054,8 +3060,9 @@ const Parser = struct {
 
     fn parseIdentifier(p: *Parser) !?*Node {
         const token = p.eatToken(.Identifier) orelse return null;
-        const node = try p.arena.allocator.create(Node.Identifier);
+        const node = try p.arena.allocator.create(Node.OneToken);
         node.* = .{
+            .base = .{ .tag = .Identifier },
             .token = token,
         };
         return &node.base;
@@ -3064,16 +3071,18 @@ const Parser = struct {
     fn parseAnyType(p: *Parser) !?*Node {
         const token = p.eatToken(.Keyword_anytype) orelse
             p.eatToken(.Keyword_var) orelse return null; // TODO remove in next release cycle
-        const node = try p.arena.allocator.create(Node.AnyType);
+        const node = try p.arena.allocator.create(Node.OneToken);
         node.* = .{
+            .base = .{ .tag = .AnyType },
             .token = token,
         };
         return &node.base;
     }
 
-    fn createLiteral(p: *Parser, comptime T: type, token: TokenIndex) !*Node {
-        const result = try p.arena.allocator.create(T);
-        result.* = T{
+    fn createLiteral(p: *Parser, tag: ast.Node.Tag, token: TokenIndex) !*Node {
+        const result = try p.arena.allocator.create(Node.OneToken);
+        result.* = .{
+            .base = .{ .tag = tag },
             .token = token,
         };
         return &result.base;
@@ -3081,8 +3090,9 @@ const Parser = struct {
 
     fn parseStringLiteralSingle(p: *Parser) !?*Node {
         if (p.eatToken(.StringLiteral)) |token| {
-            const node = try p.arena.allocator.create(Node.StringLiteral);
+            const node = try p.arena.allocator.create(Node.OneToken);
             node.* = .{
+                .base = .{ .tag = .StringLiteral },
                 .token = token,
             };
             return &node.base;
@@ -3131,8 +3141,9 @@ const Parser = struct {
 
     fn parseIntegerLiteral(p: *Parser) !?*Node {
         const token = p.eatToken(.IntegerLiteral) orelse return null;
-        const node = try p.arena.allocator.create(Node.IntegerLiteral);
+        const node = try p.arena.allocator.create(Node.OneToken);
         node.* = .{
+            .base = .{ .tag = .IntegerLiteral },
             .token = token,
         };
         return &node.base;
@@ -3140,8 +3151,9 @@ const Parser = struct {
 
     fn parseFloatLiteral(p: *Parser) !?*Node {
         const token = p.eatToken(.FloatLiteral) orelse return null;
-        const node = try p.arena.allocator.create(Node.FloatLiteral);
+        const node = try p.arena.allocator.create(Node.OneToken);
         node.* = .{
+            .base = .{ .tag = .FloatLiteral },
             .token = token,
         };
         return &node.base;
lib/std/zig/render.zig
@@ -366,10 +366,32 @@ fn renderExpression(
     space: Space,
 ) (@TypeOf(stream).Error || Error)!void {
     switch (base.tag) {
-        .Identifier => {
-            const identifier = @fieldParentPtr(ast.Node.Identifier, "base", base);
-            return renderToken(tree, stream, identifier.token, indent, start_col, space);
+        .Identifier,
+        .IntegerLiteral,
+        .FloatLiteral,
+        .StringLiteral,
+        .CharLiteral,
+        .BoolLiteral,
+        .NullLiteral,
+        .Unreachable,
+        .ErrorType,
+        .UndefinedLiteral,
+        => {
+            const casted_node = base.cast(ast.Node.OneToken).?;
+            return renderToken(tree, stream, casted_node.token, indent, start_col, space);
         },
+
+        .AnyType => {
+            const any_type = base.castTag(.AnyType).?;
+            if (mem.eql(u8, tree.tokenSlice(any_type.token), "var")) {
+                // TODO remove in next release cycle
+                try stream.writeAll("anytype");
+                if (space == .Comma) try stream.writeAll(",\n");
+                return;
+            }
+            return renderToken(tree, stream, any_type.token, indent, start_col, space);
+        },
+
         .Block => {
             const block = @fieldParentPtr(ast.Node.Block, "base", base);
 
@@ -399,6 +421,7 @@ fn renderExpression(
                 return renderToken(tree, stream, block.rbrace, indent, start_col, space);
             }
         },
+
         .Defer => {
             const defer_node = @fieldParentPtr(ast.Node.Defer, "base", base);
 
@@ -1107,50 +1130,48 @@ fn renderExpression(
             return renderToken(tree, stream, suffix_op.rtoken, indent, start_col, space); // ?
         },
 
-        .ControlFlowExpression => {
-            const flow_expr = @fieldParentPtr(ast.Node.ControlFlowExpression, "base", base);
+        .Break => {
+            const flow_expr = base.castTag(.Break).?;
+            const maybe_rhs = flow_expr.getRHS();
+            const maybe_label = flow_expr.getLabel();
 
-            switch (flow_expr.kind) {
-                .Break => |maybe_label| {
-                    if (maybe_label == null and flow_expr.rhs == null) {
-                        return renderToken(tree, stream, flow_expr.ltoken, indent, start_col, space); // break
-                    }
-
-                    try renderToken(tree, stream, flow_expr.ltoken, indent, start_col, Space.Space); // break
-                    if (maybe_label) |label| {
-                        const colon = tree.nextToken(flow_expr.ltoken);
-                        try renderToken(tree, stream, colon, indent, start_col, Space.None); // :
-
-                        if (flow_expr.rhs == null) {
-                            return renderExpression(allocator, stream, tree, indent, start_col, label, space); // label
-                        }
-                        try renderExpression(allocator, stream, tree, indent, start_col, label, Space.Space); // label
-                    }
-                },
-                .Continue => |maybe_label| {
-                    assert(flow_expr.rhs == null);
+            if (maybe_label == null and maybe_rhs == null) {
+                return renderToken(tree, stream, flow_expr.ltoken, indent, start_col, space); // break
+            }
 
-                    if (maybe_label == null and flow_expr.rhs == null) {
-                        return renderToken(tree, stream, flow_expr.ltoken, indent, start_col, space); // continue
-                    }
+            try renderToken(tree, stream, flow_expr.ltoken, indent, start_col, Space.Space); // break
+            if (maybe_label) |label| {
+                const colon = tree.nextToken(flow_expr.ltoken);
+                try renderToken(tree, stream, colon, indent, start_col, Space.None); // :
 
-                    try renderToken(tree, stream, flow_expr.ltoken, indent, start_col, Space.Space); // continue
-                    if (maybe_label) |label| {
-                        const colon = tree.nextToken(flow_expr.ltoken);
-                        try renderToken(tree, stream, colon, indent, start_col, Space.None); // :
+                if (maybe_rhs == null) {
+                    return renderToken(tree, stream, label, indent, start_col, space); // label
+                }
+                try renderToken(tree, stream, label, indent, start_col, Space.Space); // label
+            }
+            return renderExpression(allocator, stream, tree, indent, start_col, maybe_rhs.?, space);
+        },
 
-                        return renderExpression(allocator, stream, tree, indent, start_col, label, space);
-                    }
-                },
-                .Return => {
-                    if (flow_expr.rhs == null) {
-                        return renderToken(tree, stream, flow_expr.ltoken, indent, start_col, space);
-                    }
-                    try renderToken(tree, stream, flow_expr.ltoken, indent, start_col, Space.Space);
-                },
+        .Continue => {
+            const flow_expr = base.castTag(.Continue).?;
+            if (flow_expr.getLabel()) |label| {
+                try renderToken(tree, stream, flow_expr.ltoken, indent, start_col, Space.Space); // continue
+                const colon = tree.nextToken(flow_expr.ltoken);
+                try renderToken(tree, stream, colon, indent, start_col, Space.None); // :
+                return renderToken(tree, stream, label, indent, start_col, space); // label
+            } else {
+                return renderToken(tree, stream, flow_expr.ltoken, indent, start_col, space); // continue
             }
+        },
 
-            return renderExpression(allocator, stream, tree, indent, start_col, flow_expr.rhs.?, space);
+        .Return => {
+            const flow_expr = base.castTag(.Return).?;
+            if (flow_expr.getRHS()) |rhs| {
+                try renderToken(tree, stream, flow_expr.ltoken, indent, start_col, Space.Space);
+                return renderExpression(allocator, stream, tree, indent, start_col, rhs, space);
+            } else {
+                return renderToken(tree, stream, flow_expr.ltoken, indent, start_col, space);
+            }
         },
 
         .Payload => {
@@ -1208,48 +1229,6 @@ fn renderExpression(
             return renderExpression(allocator, stream, tree, indent, start_col, field_init.expr, space);
         },
 
-        .IntegerLiteral => {
-            const integer_literal = @fieldParentPtr(ast.Node.IntegerLiteral, "base", base);
-            return renderToken(tree, stream, integer_literal.token, indent, start_col, space);
-        },
-        .FloatLiteral => {
-            const float_literal = @fieldParentPtr(ast.Node.FloatLiteral, "base", base);
-            return renderToken(tree, stream, float_literal.token, indent, start_col, space);
-        },
-        .StringLiteral => {
-            const string_literal = @fieldParentPtr(ast.Node.StringLiteral, "base", base);
-            return renderToken(tree, stream, string_literal.token, indent, start_col, space);
-        },
-        .CharLiteral => {
-            const char_literal = @fieldParentPtr(ast.Node.CharLiteral, "base", base);
-            return renderToken(tree, stream, char_literal.token, indent, start_col, space);
-        },
-        .BoolLiteral => {
-            const bool_literal = @fieldParentPtr(ast.Node.CharLiteral, "base", base);
-            return renderToken(tree, stream, bool_literal.token, indent, start_col, space);
-        },
-        .NullLiteral => {
-            const null_literal = @fieldParentPtr(ast.Node.NullLiteral, "base", base);
-            return renderToken(tree, stream, null_literal.token, indent, start_col, space);
-        },
-        .Unreachable => {
-            const unreachable_node = @fieldParentPtr(ast.Node.Unreachable, "base", base);
-            return renderToken(tree, stream, unreachable_node.token, indent, start_col, space);
-        },
-        .ErrorType => {
-            const error_type = @fieldParentPtr(ast.Node.ErrorType, "base", base);
-            return renderToken(tree, stream, error_type.token, indent, start_col, space);
-        },
-        .AnyType => {
-            const any_type = @fieldParentPtr(ast.Node.AnyType, "base", base);
-            if (mem.eql(u8, tree.tokenSlice(any_type.token), "var")) {
-                // TODO remove in next release cycle
-                try stream.writeAll("anytype");
-                if (space == .Comma) try stream.writeAll(",\n");
-                return;
-            }
-            return renderToken(tree, stream, any_type.token, indent, start_col, space);
-        },
         .ContainerDecl => {
             const container_decl = @fieldParentPtr(ast.Node.ContainerDecl, "base", base);
 
@@ -1468,10 +1447,6 @@ fn renderExpression(
             }
             try stream.writeByteNTimes(' ', indent);
         },
-        .UndefinedLiteral => {
-            const undefined_literal = @fieldParentPtr(ast.Node.UndefinedLiteral, "base", base);
-            return renderToken(tree, stream, undefined_literal.token, indent, start_col, space);
-        },
 
         .BuiltinCall => {
             const builtin_call = @fieldParentPtr(ast.Node.BuiltinCall, "base", base);
lib/std/target.zig
@@ -436,6 +436,8 @@ pub const Target = struct {
         macho,
         wasm,
         c,
+        hex,
+        raw,
     };
 
     pub const SubSystem = enum {
src-self-hosted/codegen/c.zig
@@ -90,7 +90,7 @@ fn genFn(file: *C, decl: *Decl) !void {
     const instructions = func.analysis.success.instructions;
     if (instructions.len > 0) {
         for (instructions) |inst| {
-            try writer.writeAll("\n\t");
+            try writer.writeAll("\n    ");
             switch (inst.tag) {
                 .assembly => try genAsm(file, inst.castTag(.assembly).?, decl),
                 .call => try genCall(file, inst.castTag(.call).?, decl),
@@ -106,21 +106,7 @@ fn genFn(file: *C, decl: *Decl) !void {
 }
 
 fn genRet(file: *C, inst: *Inst.UnOp, decl: *Decl, expected_return_type: Type) !void {
-    const writer = file.main.writer();
-    const ret_value = inst.operand;
-    const value = ret_value.value().?;
-    if (expected_return_type.eql(ret_value.ty))
-        return file.fail(decl.src(), "TODO return {}", .{expected_return_type})
-    else if (expected_return_type.isInt() and ret_value.ty.tag() == .comptime_int)
-        if (value.intFitsInType(expected_return_type, file.options.target))
-            if (expected_return_type.intInfo(file.options.target).bits <= 64)
-                try writer.print("return {};", .{value.toUnsignedInt()})
-            else
-                return file.fail(decl.src(), "TODO return ints > 64 bits", .{})
-        else
-            return file.fail(decl.src(), "comptime int {} does not fit in {}", .{ value.toUnsignedInt(), expected_return_type })
-    else
-        return file.fail(decl.src(), "return type mismatch: expected {}, found {}", .{ expected_return_type, ret_value.ty });
+    return file.fail(decl.src(), "TODO return {}", .{expected_return_type});
 }
 
 fn genCall(file: *C, inst: *Inst.Call, decl: *Decl) !void {
@@ -162,7 +148,7 @@ fn genAsm(file: *C, as: *Inst.Assembly, decl: *Decl) !void {
                 if (c.val.tag() == .int_u64) {
                     try writer.writeAll("register ");
                     try renderType(file, writer, arg.ty, decl.src());
-                    try writer.print(" {}_constant __asm__(\"{}\") = {};\n\t", .{ reg, reg, c.val.toUnsignedInt() });
+                    try writer.print(" {}_constant __asm__(\"{}\") = {};\n    ", .{ reg, reg, c.val.toUnsignedInt() });
                 } else {
                     return file.fail(decl.src(), "TODO inline asm {} args", .{c.val.tag()});
                 }
src-self-hosted/astgen.zig
@@ -1,5 +1,6 @@
 const std = @import("std");
 const mem = std.mem;
+const Allocator = std.mem.Allocator;
 const Value = @import("value.zig").Value;
 const Type = @import("type.zig").Type;
 const TypedValue = @import("TypedValue.zig");
@@ -11,35 +12,67 @@ const trace = @import("tracy.zig").trace;
 const Scope = Module.Scope;
 const InnerError = Module.InnerError;
 
+pub const ResultLoc = union(enum) {
+    /// The expression is the right-hand side of assignment to `_`.
+    discard,
+    /// The expression has an inferred type, and it will be evaluated as an rvalue.
+    none,
+    /// The expression will be type coerced into this type, but it will be evaluated as an rvalue.
+    ty: *zir.Inst,
+    /// The expression must store its result into this typed pointer.
+    ptr: *zir.Inst,
+    /// The expression must store its result into this allocation, which has an inferred type.
+    inferred_ptr: *zir.Inst.Tag.alloc_inferred.Type(),
+    /// The expression must store its result into this pointer, which is a typed pointer that
+    /// has been bitcasted to whatever the expression's type is.
+    bitcasted_ptr: *zir.Inst.UnOp,
+    /// There is a pointer for the expression to store its result into, however, its type
+    /// is inferred based on peer type resolution for a `zir.Inst.Block`.
+    block_ptr: *zir.Inst.Block,
+};
+
+pub fn typeExpr(mod: *Module, scope: *Scope, type_node: *ast.Node) InnerError!*zir.Inst {
+    const type_src = scope.tree().token_locs[type_node.firstToken()].start;
+    const type_type = try mod.addZIRInstConst(scope, type_src, .{
+        .ty = Type.initTag(.type),
+        .val = Value.initTag(.type_type),
+    });
+    const type_rl: ResultLoc = .{ .ty = type_type };
+    return expr(mod, scope, type_rl, type_node);
+}
+
 /// Turn Zig AST into untyped ZIR istructions.
-pub fn expr(mod: *Module, scope: *Scope, node: *ast.Node) InnerError!*zir.Inst {
+pub fn expr(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node) InnerError!*zir.Inst {
     switch (node.tag) {
         .VarDecl => unreachable, // Handled in `blockExpr`.
-
-        .Add => return simpleInfixOp(mod, scope, node.castTag(.Add).?, .add),
-        .Sub => return simpleInfixOp(mod, scope, node.castTag(.Sub).?, .sub),
-        .BangEqual => return simpleInfixOp(mod, scope, node.castTag(.BangEqual).?, .cmp_neq),
-        .EqualEqual => return simpleInfixOp(mod, scope, node.castTag(.EqualEqual).?, .cmp_eq),
-        .GreaterThan => return simpleInfixOp(mod, scope, node.castTag(.GreaterThan).?, .cmp_gt),
-        .GreaterOrEqual => return simpleInfixOp(mod, scope, node.castTag(.GreaterOrEqual).?, .cmp_gte),
-        .LessThan => return simpleInfixOp(mod, scope, node.castTag(.LessThan).?, .cmp_lt),
-        .LessOrEqual => return simpleInfixOp(mod, scope, node.castTag(.LessOrEqual).?, .cmp_lte),
-
-        .Identifier => return identifier(mod, scope, node.castTag(.Identifier).?),
-        .Asm => return assembly(mod, scope, node.castTag(.Asm).?),
-        .StringLiteral => return stringLiteral(mod, scope, node.castTag(.StringLiteral).?),
-        .IntegerLiteral => return integerLiteral(mod, scope, node.castTag(.IntegerLiteral).?),
-        .BuiltinCall => return builtinCall(mod, scope, node.castTag(.BuiltinCall).?),
-        .Call => return callExpr(mod, scope, node.castTag(.Call).?),
+        .Assign => unreachable, // Handled in `blockExpr`.
+
+        .Add => return arithmetic(mod, scope, rl, node.castTag(.Add).?, .add),
+        .Sub => return arithmetic(mod, scope, rl, node.castTag(.Sub).?, .sub),
+
+        .BangEqual => return cmp(mod, scope, rl, node.castTag(.BangEqual).?, .cmp_neq),
+        .EqualEqual => return cmp(mod, scope, rl, node.castTag(.EqualEqual).?, .cmp_eq),
+        .GreaterThan => return cmp(mod, scope, rl, node.castTag(.GreaterThan).?, .cmp_gt),
+        .GreaterOrEqual => return cmp(mod, scope, rl, node.castTag(.GreaterOrEqual).?, .cmp_gte),
+        .LessThan => return cmp(mod, scope, rl, node.castTag(.LessThan).?, .cmp_lt),
+        .LessOrEqual => return cmp(mod, scope, rl, node.castTag(.LessOrEqual).?, .cmp_lte),
+
+        .Identifier => return rlWrap(mod, scope, rl, try identifier(mod, scope, node.castTag(.Identifier).?)),
+        .Asm => return rlWrap(mod, scope, rl, try assembly(mod, scope, node.castTag(.Asm).?)),
+        .StringLiteral => return rlWrap(mod, scope, rl, try stringLiteral(mod, scope, node.castTag(.StringLiteral).?)),
+        .IntegerLiteral => return rlWrap(mod, scope, rl, try integerLiteral(mod, scope, node.castTag(.IntegerLiteral).?)),
+        .BuiltinCall => return builtinCall(mod, scope, rl, node.castTag(.BuiltinCall).?),
+        .Call => return callExpr(mod, scope, rl, node.castTag(.Call).?),
         .Unreachable => return unreach(mod, scope, node.castTag(.Unreachable).?),
-        .ControlFlowExpression => return controlFlowExpr(mod, scope, node.castTag(.ControlFlowExpression).?),
-        .If => return ifExpr(mod, scope, node.castTag(.If).?),
-        .Assign => return assign(mod, scope, node.castTag(.Assign).?),
-        .Period => return field(mod, scope, node.castTag(.Period).?),
-        .Deref => return deref(mod, scope, node.castTag(.Deref).?),
-        .BoolNot => return boolNot(mod, scope, node.castTag(.BoolNot).?),
-        .FloatLiteral => return floatLiteral(mod, scope, node.castTag(.FloatLiteral).?),
-        .UndefinedLiteral, .BoolLiteral, .NullLiteral => return primitiveLiteral(mod, scope, node),
+        .Return => return ret(mod, scope, node.castTag(.Return).?),
+        .If => return ifExpr(mod, scope, rl, node.castTag(.If).?),
+        .Period => return rlWrap(mod, scope, rl, try field(mod, scope, node.castTag(.Period).?)),
+        .Deref => return rlWrap(mod, scope, rl, try deref(mod, scope, node.castTag(.Deref).?)),
+        .BoolNot => return rlWrap(mod, scope, rl, try boolNot(mod, scope, node.castTag(.BoolNot).?)),
+        .FloatLiteral => return rlWrap(mod, scope, rl, try floatLiteral(mod, scope, node.castTag(.FloatLiteral).?)),
+        .UndefinedLiteral => return rlWrap(mod, scope, rl, try undefLiteral(mod, scope, node.castTag(.UndefinedLiteral).?)),
+        .BoolLiteral => return rlWrap(mod, scope, rl, try boolLiteral(mod, scope, node.castTag(.BoolLiteral).?)),
+        .NullLiteral => return rlWrap(mod, scope, rl, try nullLiteral(mod, scope, node.castTag(.NullLiteral).?)),
         else => return mod.failNode(scope, node, "TODO implement astgen.Expr for {}", .{@tagName(node.tag)}),
     }
 }
@@ -59,17 +92,28 @@ pub fn blockExpr(mod: *Module, parent_scope: *Scope, block_node: *ast.Node.Block
     for (block_node.statements()) |statement| {
         switch (statement.tag) {
             .VarDecl => {
-                const sub_scope = try block_arena.allocator.create(Scope.LocalVar);
-                const var_decl_node = @fieldParentPtr(ast.Node.VarDecl, "base", statement);
-                sub_scope.* = try varDecl(mod, scope, var_decl_node);
-                scope = &sub_scope.base;
+                const var_decl_node = statement.castTag(.VarDecl).?;
+                scope = try varDecl(mod, scope, var_decl_node, &block_arena.allocator);
+            },
+            .Assign => {
+                const ass = statement.castTag(.Assign).?;
+                try assign(mod, scope, ass);
+            },
+            else => {
+                const possibly_unused_result = try expr(mod, scope, .none, statement);
+                const src = scope.tree().token_locs[statement.firstToken()].start;
+                _ = try mod.addZIRUnOp(scope, src, .ensure_result_used, possibly_unused_result);
             },
-            else => _ = try expr(mod, scope, statement),
         }
     }
 }
 
-fn varDecl(mod: *Module, scope: *Scope, node: *ast.Node.VarDecl) InnerError!Scope.LocalVar {
+fn varDecl(
+    mod: *Module,
+    scope: *Scope,
+    node: *ast.Node.VarDecl,
+    block_arena: *Allocator,
+) InnerError!*Scope {
     // TODO implement detection of shadowing
     if (node.getTrailer("comptime_token")) |comptime_token| {
         return mod.failTok(scope, comptime_token, "TODO implement comptime locals", .{});
@@ -78,48 +122,96 @@ fn varDecl(mod: *Module, scope: *Scope, node: *ast.Node.VarDecl) InnerError!Scop
         return mod.failNode(scope, align_node, "TODO implement alignment on locals", .{});
     }
     const tree = scope.tree();
+    const name_src = tree.token_locs[node.name_token].start;
+    const ident_name = try identifierTokenString(mod, scope, node.name_token);
+    const init_node = node.getTrailer("init_node").?;
     switch (tree.token_ids[node.mut_token]) {
         .Keyword_const => {
-            if (node.getTrailer("type_node")) |type_node| {
-                return mod.failNode(scope, type_node, "TODO implement typed const locals", .{});
-            }
             // Depending on the type of AST the initialization expression is, we may need an lvalue
             // or an rvalue as a result location. If it is an rvalue, we can use the instruction as
             // the variable, no memory location needed.
-            const init_node = node.getTrailer("init_node").?;
             if (nodeMayNeedMemoryLocation(init_node)) {
-                return mod.failNode(scope, init_node, "TODO implement result locations", .{});
+                if (node.getTrailer("type_node")) |type_node| {
+                    const type_inst = try typeExpr(mod, scope, type_node);
+                    const alloc = try mod.addZIRUnOp(scope, name_src, .alloc, type_inst);
+                    const result_loc: ResultLoc = .{ .ptr = alloc };
+                    const init_inst = try expr(mod, scope, result_loc, init_node);
+                    const sub_scope = try block_arena.create(Scope.LocalVal);
+                    sub_scope.* = .{
+                        .parent = scope,
+                        .gen_zir = scope.getGenZIR(),
+                        .name = ident_name,
+                        .inst = init_inst,
+                    };
+                    return &sub_scope.base;
+                } else {
+                    const alloc = try mod.addZIRNoOpT(scope, name_src, .alloc_inferred);
+                    const result_loc: ResultLoc = .{ .inferred_ptr = alloc };
+                    const init_inst = try expr(mod, scope, result_loc, init_node);
+                    const sub_scope = try block_arena.create(Scope.LocalVal);
+                    sub_scope.* = .{
+                        .parent = scope,
+                        .gen_zir = scope.getGenZIR(),
+                        .name = ident_name,
+                        .inst = init_inst,
+                    };
+                    return &sub_scope.base;
+                }
+            } else {
+                const result_loc: ResultLoc = if (node.getTrailer("type_node")) |type_node|
+                    .{ .ty = try typeExpr(mod, scope, type_node) }
+                else
+                    .none;
+                const init_inst = try expr(mod, scope, result_loc, init_node);
+                const sub_scope = try block_arena.create(Scope.LocalVal);
+                sub_scope.* = .{
+                    .parent = scope,
+                    .gen_zir = scope.getGenZIR(),
+                    .name = ident_name,
+                    .inst = init_inst,
+                };
+                return &sub_scope.base;
             }
-            const init_inst = try expr(mod, scope, init_node);
-            const ident_name = try identifierTokenString(mod, scope, node.name_token);
-            return Scope.LocalVar{
-                .parent = scope,
-                .gen_zir = scope.getGenZIR(),
-                .name = ident_name,
-                .inst = init_inst,
-            };
         },
         .Keyword_var => {
-            return mod.failNode(scope, &node.base, "TODO implement local vars", .{});
+            if (node.getTrailer("type_node")) |type_node| {
+                const type_inst = try typeExpr(mod, scope, type_node);
+                const alloc = try mod.addZIRUnOp(scope, name_src, .alloc, type_inst);
+                const result_loc: ResultLoc = .{ .ptr = alloc };
+                const init_inst = try expr(mod, scope, result_loc, init_node);
+                const sub_scope = try block_arena.create(Scope.LocalPtr);
+                sub_scope.* = .{
+                    .parent = scope,
+                    .gen_zir = scope.getGenZIR(),
+                    .name = ident_name,
+                    .ptr = alloc,
+                };
+                return &sub_scope.base;
+            } else {
+                const alloc = try mod.addZIRNoOp(scope, name_src, .alloc_inferred);
+                const result_loc = .{ .inferred_ptr = alloc.castTag(.alloc_inferred).? };
+                const init_inst = try expr(mod, scope, result_loc, init_node);
+                const sub_scope = try block_arena.create(Scope.LocalPtr);
+                sub_scope.* = .{
+                    .parent = scope,
+                    .gen_zir = scope.getGenZIR(),
+                    .name = ident_name,
+                    .ptr = alloc,
+                };
+                return &sub_scope.base;
+            }
         },
         else => unreachable,
     }
 }
 
-fn boolNot(mod: *Module, scope: *Scope, node: *ast.Node.SimplePrefixOp) InnerError!*zir.Inst {
-    const operand = try expr(mod, scope, node.rhs);
-    const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
-    return mod.addZIRUnOp(scope, src, .boolnot, operand);
-}
-
-fn assign(mod: *Module, scope: *Scope, infix_node: *ast.Node.SimpleInfixOp) InnerError!*zir.Inst {
-    if (infix_node.lhs.tag == .Identifier) {
-        const ident = @fieldParentPtr(ast.Node.Identifier, "base", infix_node.lhs);
+fn assign(mod: *Module, scope: *Scope, infix_node: *ast.Node.SimpleInfixOp) InnerError!void {
+    if (infix_node.lhs.castTag(.Identifier)) |ident| {
         const tree = scope.tree();
         const ident_name = try identifierTokenString(mod, scope, ident.token);
         if (std.mem.eql(u8, ident_name, "_")) {
-            return expr(mod, scope, infix_node.rhs);
+            _ = try expr(mod, scope, .discard, infix_node.rhs);
+            return;
         } else {
             return mod.failNode(scope, &infix_node.base, "TODO implement infix operator assign", .{});
         }
@@ -128,6 +220,17 @@ fn assign(mod: *Module, scope: *Scope, infix_node: *ast.Node.SimpleInfixOp) Inne
     }
 }
 
+fn boolNot(mod: *Module, scope: *Scope, node: *ast.Node.SimplePrefixOp) InnerError!*zir.Inst {
+    const tree = scope.tree();
+    const src = tree.token_locs[node.op_token].start;
+    const bool_type = try mod.addZIRInstConst(scope, src, .{
+        .ty = Type.initTag(.type),
+        .val = Value.initTag(.bool_type),
+    });
+    const operand = try expr(mod, scope, .{ .ty = bool_type }, node.rhs);
+    return mod.addZIRUnOp(scope, src, .boolnot, operand);
+}
+
 /// Identifier token -> String (allocated in scope.arena())
 pub fn identifierTokenString(mod: *Module, scope: *Scope, token: ast.TokenIndex) InnerError![]const u8 {
     const tree = scope.tree();
@@ -148,7 +251,7 @@ pub fn identifierTokenString(mod: *Module, scope: *Scope, token: ast.TokenIndex)
     return ident_name;
 }
 
-pub fn identifierStringInst(mod: *Module, scope: *Scope, node: *ast.Node.Identifier) InnerError!*zir.Inst {
+pub fn identifierStringInst(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerError!*zir.Inst {
     const tree = scope.tree();
     const src = tree.token_locs[node.token].start;
 
@@ -158,10 +261,11 @@ pub fn identifierStringInst(mod: *Module, scope: *Scope, node: *ast.Node.Identif
 }
 
 fn field(mod: *Module, scope: *Scope, node: *ast.Node.SimpleInfixOp) InnerError!*zir.Inst {
+    // TODO introduce lvalues
     const tree = scope.tree();
     const src = tree.token_locs[node.op_token].start;
 
-    const lhs = try expr(mod, scope, node.lhs);
+    const lhs = try expr(mod, scope, .none, node.lhs);
     const field_name = try identifierStringInst(mod, scope, node.rhs.castTag(.Identifier).?);
 
     const pointer = try mod.addZIRInst(scope, src, zir.Inst.FieldPtr, .{ .object_ptr = lhs, .field_name = field_name }, .{});
@@ -171,26 +275,44 @@ fn field(mod: *Module, scope: *Scope, node: *ast.Node.SimpleInfixOp) InnerError!
 fn deref(mod: *Module, scope: *Scope, node: *ast.Node.SimpleSuffixOp) InnerError!*zir.Inst {
     const tree = scope.tree();
     const src = tree.token_locs[node.rtoken].start;
-    const lhs = try expr(mod, scope, node.lhs);
+    const lhs = try expr(mod, scope, .none, node.lhs);
     return mod.addZIRUnOp(scope, src, .deref, lhs);
 }
 
-fn simpleInfixOp(
+fn cmp(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    infix_node: *ast.Node.SimpleInfixOp,
+    cmp_inst_tag: zir.Inst.Tag,
+) InnerError!*zir.Inst {
+    const tree = scope.tree();
+    const src = tree.token_locs[infix_node.op_token].start;
+
+    const lhs = try expr(mod, scope, .none, infix_node.lhs);
+    const rhs = try expr(mod, scope, .none, infix_node.rhs);
+    const result = try mod.addZIRBinOp(scope, src, cmp_inst_tag, lhs, rhs);
+    return rlWrap(mod, scope, rl, result);
+}
+
+fn arithmetic(
     mod: *Module,
     scope: *Scope,
+    rl: ResultLoc,
     infix_node: *ast.Node.SimpleInfixOp,
     op_inst_tag: zir.Inst.Tag,
 ) InnerError!*zir.Inst {
-    const lhs = try expr(mod, scope, infix_node.lhs);
-    const rhs = try expr(mod, scope, infix_node.rhs);
+    const lhs = try expr(mod, scope, .none, infix_node.lhs);
+    const rhs = try expr(mod, scope, .none, infix_node.rhs);
 
     const tree = scope.tree();
     const src = tree.token_locs[infix_node.op_token].start;
 
-    return mod.addZIRBinOp(scope, src, op_inst_tag, lhs, rhs);
+    const result = try mod.addZIRBinOp(scope, src, op_inst_tag, lhs, rhs);
+    return rlWrap(mod, scope, rl, result);
 }
 
-fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.Inst {
+fn ifExpr(mod: *Module, scope: *Scope, rl: ResultLoc, if_node: *ast.Node.If) InnerError!*zir.Inst {
     if (if_node.payload) |payload| {
         return mod.failNode(scope, payload, "TODO implement astgen.IfExpr for optionals", .{});
     }
@@ -207,10 +329,14 @@ fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.In
     };
     defer block_scope.instructions.deinit(mod.gpa);
 
-    const cond = try expr(mod, &block_scope.base, if_node.condition);
-
     const tree = scope.tree();
     const if_src = tree.token_locs[if_node.if_token].start;
+    const bool_type = try mod.addZIRInstConst(scope, if_src, .{
+        .ty = Type.initTag(.type),
+        .val = Value.initTag(.bool_type),
+    });
+    const cond = try expr(mod, &block_scope.base, .{ .ty = bool_type }, if_node.condition);
+
     const condbr = try mod.addZIRInstSpecial(&block_scope.base, if_src, zir.Inst.CondBr, .{
         .condition = cond,
         .then_body = undefined, // populated below
@@ -228,7 +354,16 @@ fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.In
     };
     defer then_scope.instructions.deinit(mod.gpa);
 
-    const then_result = try expr(mod, &then_scope.base, if_node.body);
+    // Most result location types can be forwarded directly; however
+    // if we need to write to a pointer which has an inferred type,
+    // proper type inference requires peer type resolution on the if's
+    // branches.
+    const branch_rl: ResultLoc = switch (rl) {
+        .discard, .none, .ty, .ptr => rl,
+        .inferred_ptr, .bitcasted_ptr, .block_ptr => .{ .block_ptr = block },
+    };
+
+    const then_result = try expr(mod, &then_scope.base, branch_rl, if_node.body);
     if (!then_result.tag.isNoReturn()) {
         const then_src = tree.token_locs[if_node.body.lastToken()].start;
         _ = try mod.addZIRInst(&then_scope.base, then_src, zir.Inst.Break, .{
@@ -249,7 +384,7 @@ fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.In
     defer else_scope.instructions.deinit(mod.gpa);
 
     if (if_node.@"else") |else_node| {
-        const else_result = try expr(mod, &else_scope.base, else_node.body);
+        const else_result = try expr(mod, &else_scope.base, branch_rl, else_node.body);
         if (!else_result.tag.isNoReturn()) {
             const else_src = tree.token_locs[else_node.body.lastToken()].start;
             _ = try mod.addZIRInst(&else_scope.base, else_src, zir.Inst.Break, .{
@@ -272,27 +407,25 @@ fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.In
     return &block.base;
 }
 
-fn controlFlowExpr(
-    mod: *Module,
-    scope: *Scope,
-    cfe: *ast.Node.ControlFlowExpression,
-) InnerError!*zir.Inst {
-    switch (cfe.kind) {
-        .Break => return mod.failNode(scope, &cfe.base, "TODO implement astgen.Expr for Break", .{}),
-        .Continue => return mod.failNode(scope, &cfe.base, "TODO implement astgen.Expr for Continue", .{}),
-        .Return => {},
-    }
+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;
-    if (cfe.rhs) |rhs_node| {
-        const operand = try expr(mod, scope, rhs_node);
-        return mod.addZIRUnOp(scope, src, .@"return", operand);
+    if (cfe.getRHS()) |rhs_node| {
+        if (nodeMayNeedMemoryLocation(rhs_node)) {
+            const ret_ptr = try mod.addZIRNoOp(scope, src, .ret_ptr);
+            const operand = try expr(mod, scope, .{ .ptr = ret_ptr }, rhs_node);
+            return mod.addZIRUnOp(scope, src, .@"return", operand);
+        } else {
+            const fn_ret_ty = try mod.addZIRNoOp(scope, src, .ret_type);
+            const operand = try expr(mod, scope, .{ .ty = fn_ret_ty }, rhs_node);
+            return mod.addZIRUnOp(scope, src, .@"return", operand);
+        }
     } else {
         return mod.addZIRNoOp(scope, src, .returnvoid);
     }
 }
 
-fn identifier(mod: *Module, scope: *Scope, ident: *ast.Node.Identifier) InnerError!*zir.Inst {
+fn identifier(mod: *Module, scope: *Scope, ident: *ast.Node.OneToken) InnerError!*zir.Inst {
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -345,12 +478,19 @@ fn identifier(mod: *Module, scope: *Scope, ident: *ast.Node.Identifier) InnerErr
     {
         var s = scope;
         while (true) switch (s.tag) {
-            .local_var => {
-                const local_var = s.cast(Scope.LocalVar).?;
-                if (mem.eql(u8, local_var.name, ident_name)) {
-                    return local_var.inst;
+            .local_val => {
+                const local_val = s.cast(Scope.LocalVal).?;
+                if (mem.eql(u8, local_val.name, ident_name)) {
+                    return local_val.inst;
+                }
+                s = local_val.parent;
+            },
+            .local_ptr => {
+                const local_ptr = s.cast(Scope.LocalPtr).?;
+                if (mem.eql(u8, local_ptr.name, ident_name)) {
+                    return try mod.addZIRUnOp(scope, src, .deref, local_ptr.ptr);
                 }
-                s = local_var.parent;
+                s = local_ptr.parent;
             },
             .gen_zir => s = s.cast(Scope.GenZIR).?.parent,
             else => break,
@@ -364,7 +504,7 @@ fn identifier(mod: *Module, scope: *Scope, ident: *ast.Node.Identifier) InnerErr
     return mod.failNode(scope, &ident.base, "use of undeclared identifier '{}'", .{ident_name});
 }
 
-fn stringLiteral(mod: *Module, scope: *Scope, str_lit: *ast.Node.StringLiteral) InnerError!*zir.Inst {
+fn stringLiteral(mod: *Module, scope: *Scope, str_lit: *ast.Node.OneToken) InnerError!*zir.Inst {
     const tree = scope.tree();
     const unparsed_bytes = tree.tokenSlice(str_lit.token);
     const arena = scope.arena();
@@ -383,7 +523,7 @@ fn stringLiteral(mod: *Module, scope: *Scope, str_lit: *ast.Node.StringLiteral)
     return mod.addZIRInst(scope, src, zir.Inst.Str, .{ .bytes = bytes }, .{});
 }
 
-fn integerLiteral(mod: *Module, scope: *Scope, int_lit: *ast.Node.IntegerLiteral) InnerError!*zir.Inst {
+fn integerLiteral(mod: *Module, scope: *Scope, int_lit: *ast.Node.OneToken) InnerError!*zir.Inst {
     const arena = scope.arena();
     const tree = scope.tree();
     const prefixed_bytes = tree.tokenSlice(int_lit.token);
@@ -414,7 +554,7 @@ fn integerLiteral(mod: *Module, scope: *Scope, int_lit: *ast.Node.IntegerLiteral
     }
 }
 
-fn floatLiteral(mod: *Module, scope: *Scope, float_lit: *ast.Node.FloatLiteral) InnerError!*zir.Inst {
+fn floatLiteral(mod: *Module, scope: *Scope, float_lit: *ast.Node.OneToken) InnerError!*zir.Inst {
     const arena = scope.arena();
     const tree = scope.tree();
     const bytes = tree.tokenSlice(float_lit.token);
@@ -434,30 +574,38 @@ fn floatLiteral(mod: *Module, scope: *Scope, float_lit: *ast.Node.FloatLiteral)
     });
 }
 
-fn primitiveLiteral(mod: *Module, scope: *Scope, node: *ast.Node) InnerError!*zir.Inst {
+fn undefLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerError!*zir.Inst {
     const arena = scope.arena();
     const tree = scope.tree();
-    const src = tree.token_locs[node.firstToken()].start;
+    const src = tree.token_locs[node.token].start;
+    return mod.addZIRInstConst(scope, src, .{
+        .ty = Type.initTag(.@"undefined"),
+        .val = Value.initTag(.undef),
+    });
+}
 
-    if (node.cast(ast.Node.BoolLiteral)) |bool_node| {
-        return mod.addZIRInstConst(scope, src, .{
-            .ty = Type.initTag(.bool),
-            .val = if (tree.token_ids[bool_node.token] == .Keyword_true)
-                Value.initTag(.bool_true)
-            else
-                Value.initTag(.bool_false),
-        });
-    } else if (node.tag == .UndefinedLiteral) {
-        return mod.addZIRInstConst(scope, src, .{
-            .ty = Type.initTag(.@"undefined"),
-            .val = Value.initTag(.undef),
-        });
-    } else if (node.tag == .NullLiteral) {
-        return mod.addZIRInstConst(scope, src, .{
-            .ty = Type.initTag(.@"null"),
-            .val = Value.initTag(.null_value),
-        });
-    } else unreachable;
+fn boolLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerError!*zir.Inst {
+    const arena = scope.arena();
+    const tree = scope.tree();
+    const src = tree.token_locs[node.token].start;
+    return mod.addZIRInstConst(scope, src, .{
+        .ty = Type.initTag(.bool),
+        .val = switch (tree.token_ids[node.token]) {
+            .Keyword_true => Value.initTag(.bool_true),
+            .Keyword_false => Value.initTag(.bool_false),
+            else => unreachable,
+        },
+    });
+}
+
+fn nullLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerError!*zir.Inst {
+    const arena = scope.arena();
+    const tree = scope.tree();
+    const src = tree.token_locs[node.token].start;
+    return mod.addZIRInstConst(scope, src, .{
+        .ty = Type.initTag(.@"null"),
+        .val = Value.initTag(.null_value),
+    });
 }
 
 fn assembly(mod: *Module, scope: *Scope, asm_node: *ast.Node.Asm) InnerError!*zir.Inst {
@@ -470,19 +618,26 @@ fn assembly(mod: *Module, scope: *Scope, asm_node: *ast.Node.Asm) InnerError!*zi
     const inputs = try arena.alloc(*zir.Inst, asm_node.inputs.len);
     const args = try arena.alloc(*zir.Inst, asm_node.inputs.len);
 
+    const src = tree.token_locs[asm_node.asm_token].start;
+
+    const str_type = try mod.addZIRInstConst(scope, src, .{
+        .ty = Type.initTag(.type),
+        .val = Value.initTag(.const_slice_u8_type),
+    });
+    const str_type_rl: ResultLoc = .{ .ty = str_type };
+
     for (asm_node.inputs) |input, i| {
         // TODO semantically analyze constraints
-        inputs[i] = try expr(mod, scope, input.constraint);
-        args[i] = try expr(mod, scope, input.expr);
+        inputs[i] = try expr(mod, scope, str_type_rl, input.constraint);
+        args[i] = try expr(mod, scope, .none, input.expr);
     }
 
-    const src = tree.token_locs[asm_node.asm_token].start;
     const return_type = try mod.addZIRInstConst(scope, src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.void_type),
     });
     const asm_inst = try mod.addZIRInst(scope, src, zir.Inst.Asm, .{
-        .asm_source = try expr(mod, scope, asm_node.template),
+        .asm_source = try expr(mod, scope, str_type_rl, asm_node.template),
         .return_type = return_type,
     }, .{
         .@"volatile" = asm_node.volatile_token != null,
@@ -493,63 +648,174 @@ fn assembly(mod: *Module, scope: *Scope, asm_node: *ast.Node.Asm) InnerError!*zi
     return asm_inst;
 }
 
-fn builtinCall(mod: *Module, scope: *Scope, call: *ast.Node.BuiltinCall) InnerError!*zir.Inst {
+fn ensureBuiltinParamCount(mod: *Module, scope: *Scope, call: *ast.Node.BuiltinCall, count: u32) !void {
+    if (call.params_len == count)
+        return;
+
+    const s = if (count == 1) "" else "s";
+    return mod.failTok(scope, call.builtin_token, "expected {} parameter{}, found {}", .{ count, s, call.params_len });
+}
+
+fn simpleCast(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    call: *ast.Node.BuiltinCall,
+    inst_tag: zir.Inst.Tag,
+) InnerError!*zir.Inst {
+    try ensureBuiltinParamCount(mod, scope, call, 2);
     const tree = scope.tree();
-    const builtin_name = tree.tokenSlice(call.builtin_token);
     const src = tree.token_locs[call.builtin_token].start;
+    const type_type = try mod.addZIRInstConst(scope, src, .{
+        .ty = Type.initTag(.type),
+        .val = Value.initTag(.type_type),
+    });
+    const params = call.params();
+    const dest_type = try expr(mod, scope, .{ .ty = type_type }, params[0]);
+    const rhs = try expr(mod, scope, .none, params[1]);
+    const result = try mod.addZIRBinOp(scope, src, inst_tag, dest_type, rhs);
+    return rlWrap(mod, scope, rl, result);
+}
 
-    inline for (std.meta.declarations(zir.Inst)) |inst| {
-        if (inst.data != .Type) continue;
-        const T = inst.data.Type;
-        if (!@hasDecl(T, "builtin_name")) continue;
-        if (std.mem.eql(u8, builtin_name, T.builtin_name)) {
-            var value: T = undefined;
-            const positionals = @typeInfo(std.meta.fieldInfo(T, "positionals").field_type).Struct;
-            if (positionals.fields.len == 0) {
-                return mod.addZIRInst(scope, src, T, value.positionals, value.kw_args);
-            }
-            const arg_count: ?usize = if (positionals.fields[0].field_type == []*zir.Inst) null else positionals.fields.len;
-            if (arg_count) |some| {
-                if (call.params_len != some) {
-                    return mod.failTok(
-                        scope,
-                        call.builtin_token,
-                        "expected {} parameter{}, found {}",
-                        .{ some, if (some == 1) "" else "s", call.params_len },
-                    );
-                }
-                const params = call.params();
-                inline for (positionals.fields) |p, i| {
-                    @field(value.positionals, p.name) = try expr(mod, scope, params[i]);
-                }
-            } else {
-                return mod.failTok(scope, call.builtin_token, "TODO var args builtin '{}'", .{builtin_name});
-            }
+fn ptrToInt(mod: *Module, scope: *Scope, call: *ast.Node.BuiltinCall) InnerError!*zir.Inst {
+    try ensureBuiltinParamCount(mod, scope, call, 1);
+    const operand = try expr(mod, scope, .none, call.params()[0]);
+    const tree = scope.tree();
+    const src = tree.token_locs[call.builtin_token].start;
+    return mod.addZIRUnOp(scope, src, .ptrtoint, operand);
+}
 
-            return mod.addZIRInst(scope, src, T, value.positionals, .{});
-        }
+fn as(mod: *Module, scope: *Scope, rl: ResultLoc, call: *ast.Node.BuiltinCall) InnerError!*zir.Inst {
+    try ensureBuiltinParamCount(mod, scope, call, 2);
+    const tree = scope.tree();
+    const src = tree.token_locs[call.builtin_token].start;
+    const params = call.params();
+    const dest_type = try typeExpr(mod, scope, params[0]);
+    switch (rl) {
+        .none => return try expr(mod, scope, .{ .ty = dest_type }, params[1]),
+        .discard => {
+            const result = try expr(mod, scope, .{ .ty = dest_type }, params[1]);
+            _ = try mod.addZIRUnOp(scope, result.src, .ensure_result_non_error, result);
+            return result;
+        },
+        .ty => |result_ty| {
+            const result = try expr(mod, scope, .{ .ty = dest_type }, params[1]);
+            return mod.addZIRBinOp(scope, src, .as, result_ty, result);
+        },
+        .ptr => |result_ptr| {
+            const casted_result_ptr = try mod.addZIRBinOp(scope, src, .coerce_result_ptr, dest_type, result_ptr);
+            return expr(mod, scope, .{ .ptr = casted_result_ptr }, params[1]);
+        },
+        .bitcasted_ptr => |bitcasted_ptr| {
+            // TODO here we should be able to resolve the inference; we now have a type for the result.
+            return mod.failTok(scope, call.builtin_token, "TODO implement @as with result location @bitCast", .{});
+        },
+        .inferred_ptr => |result_alloc| {
+            // TODO here we should be able to resolve the inference; we now have a type for the result.
+            return mod.failTok(scope, call.builtin_token, "TODO implement @as with inferred-type result location pointer", .{});
+        },
+        .block_ptr => |block_ptr| {
+            const casted_block_ptr = try mod.addZIRInst(scope, src, zir.Inst.CoerceResultBlockPtr, .{
+                .dest_type = dest_type,
+                .block = block_ptr,
+            }, .{});
+            return expr(mod, scope, .{ .ptr = casted_block_ptr }, params[1]);
+        },
+    }
+}
+
+fn bitCast(mod: *Module, scope: *Scope, rl: ResultLoc, call: *ast.Node.BuiltinCall) InnerError!*zir.Inst {
+    try ensureBuiltinParamCount(mod, scope, call, 2);
+    const tree = scope.tree();
+    const src = tree.token_locs[call.builtin_token].start;
+    const type_type = try mod.addZIRInstConst(scope, src, .{
+        .ty = Type.initTag(.type),
+        .val = Value.initTag(.type_type),
+    });
+    const params = call.params();
+    const dest_type = try expr(mod, scope, .{ .ty = type_type }, params[0]);
+    switch (rl) {
+        .none => {
+            const operand = try expr(mod, scope, .none, params[1]);
+            return mod.addZIRBinOp(scope, src, .bitcast, dest_type, operand);
+        },
+        .discard => {
+            const operand = try expr(mod, scope, .none, params[1]);
+            const result = try mod.addZIRBinOp(scope, src, .bitcast, dest_type, operand);
+            _ = try mod.addZIRUnOp(scope, result.src, .ensure_result_non_error, result);
+            return result;
+        },
+        .ty => |result_ty| {
+            const result = try expr(mod, scope, .none, params[1]);
+            const bitcasted = try mod.addZIRBinOp(scope, src, .bitcast, dest_type, result);
+            return mod.addZIRBinOp(scope, src, .as, result_ty, bitcasted);
+        },
+        .ptr => |result_ptr| {
+            const casted_result_ptr = try mod.addZIRUnOp(scope, src, .bitcast_result_ptr, result_ptr);
+            return expr(mod, scope, .{ .bitcasted_ptr = casted_result_ptr.castTag(.bitcast_result_ptr).? }, params[1]);
+        },
+        .bitcasted_ptr => |bitcasted_ptr| {
+            return mod.failTok(scope, call.builtin_token, "TODO implement @bitCast with result location another @bitCast", .{});
+        },
+        .block_ptr => |block_ptr| {
+            return mod.failTok(scope, call.builtin_token, "TODO implement @bitCast with result location inferred peer types", .{});
+        },
+        .inferred_ptr => |result_alloc| {
+            // TODO here we should be able to resolve the inference; we now have a type for the result.
+            return mod.failTok(scope, call.builtin_token, "TODO implement @bitCast with inferred-type result location pointer", .{});
+        },
     }
-    return mod.failTok(scope, call.builtin_token, "TODO implement builtin call for '{}'", .{builtin_name});
 }
 
-fn callExpr(mod: *Module, scope: *Scope, node: *ast.Node.Call) InnerError!*zir.Inst {
+fn builtinCall(mod: *Module, scope: *Scope, rl: ResultLoc, call: *ast.Node.BuiltinCall) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const lhs = try expr(mod, scope, node.lhs);
+    const builtin_name = tree.tokenSlice(call.builtin_token);
+
+    // We handle the different builtins manually because they have different semantics depending
+    // on the function. For example, `@as` and others participate in result location semantics,
+    // and `@cImport` creates a special scope that collects a .c source code text buffer.
+    // Also, some builtins have a variable number of parameters.
+
+    if (mem.eql(u8, builtin_name, "@ptrToInt")) {
+        return rlWrap(mod, scope, rl, try ptrToInt(mod, scope, call));
+    } else if (mem.eql(u8, builtin_name, "@as")) {
+        return as(mod, scope, rl, call);
+    } else if (mem.eql(u8, builtin_name, "@floatCast")) {
+        return simpleCast(mod, scope, rl, call, .floatcast);
+    } else if (mem.eql(u8, builtin_name, "@intCast")) {
+        return simpleCast(mod, scope, rl, call, .intcast);
+    } else if (mem.eql(u8, builtin_name, "@bitCast")) {
+        return bitCast(mod, scope, rl, call);
+    } else {
+        return mod.failTok(scope, call.builtin_token, "invalid builtin function: '{}'", .{builtin_name});
+    }
+}
+
+fn callExpr(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.Call) InnerError!*zir.Inst {
+    const tree = scope.tree();
+    const lhs = try expr(mod, scope, .none, node.lhs);
 
     const param_nodes = node.params();
     const args = try scope.getGenZIR().arena.alloc(*zir.Inst, param_nodes.len);
     for (param_nodes) |param_node, i| {
-        args[i] = try expr(mod, scope, param_node);
+        const param_src = tree.token_locs[param_node.firstToken()].start;
+        const param_type = try mod.addZIRInst(scope, param_src, zir.Inst.ParamType, .{
+            .func = lhs,
+            .arg_index = i,
+        }, .{});
+        args[i] = try expr(mod, scope, .{ .ty = param_type }, param_node);
     }
 
     const src = tree.token_locs[node.lhs.firstToken()].start;
-    return mod.addZIRInst(scope, src, zir.Inst.Call, .{
+    const result = try mod.addZIRInst(scope, src, zir.Inst.Call, .{
         .func = lhs,
         .args = args,
     }, .{});
+    // TODO function call with result location
+    return rlWrap(mod, scope, rl, result);
 }
 
-fn unreach(mod: *Module, scope: *Scope, unreach_node: *ast.Node.Unreachable) InnerError!*zir.Inst {
+fn unreach(mod: *Module, scope: *Scope, unreach_node: *ast.Node.OneToken) InnerError!*zir.Inst {
     const tree = scope.tree();
     const src = tree.token_locs[unreach_node.token].start;
     return mod.addZIRNoOp(scope, src, .@"unreachable");
@@ -611,7 +877,9 @@ fn nodeMayNeedMemoryLocation(start_node: *ast.Node) bool {
             .FieldInitializer,
             => unreachable,
 
-            .ControlFlowExpression,
+            .Return,
+            .Break,
+            .Continue,
             .BitNot,
             .BoolNot,
             .VarDecl,
@@ -722,3 +990,39 @@ fn nodeMayNeedMemoryLocation(start_node: *ast.Node) bool {
         }
     }
 }
+
+/// Applies `rl` semantics to `inst`. Expressions which do not do their own handling of
+/// result locations must call this function on their result.
+/// As an example, if the `ResultLoc` is `ptr`, it will write the result to the pointer.
+/// If the `ResultLoc` is `ty`, it will coerce the result to the type.
+fn rlWrap(mod: *Module, scope: *Scope, rl: ResultLoc, result: *zir.Inst) InnerError!*zir.Inst {
+    switch (rl) {
+        .none => return result,
+        .discard => {
+            // Emit a compile error for discarding error values.
+            _ = try mod.addZIRUnOp(scope, result.src, .ensure_result_non_error, result);
+            return result;
+        },
+        .ty => |ty_inst| return mod.addZIRBinOp(scope, result.src, .as, ty_inst, result),
+        .ptr => |ptr_inst| {
+            const casted_result = try mod.addZIRInst(scope, result.src, zir.Inst.CoerceToPtrElem, .{
+                .ptr = ptr_inst,
+                .value = result,
+            }, .{});
+            _ = try mod.addZIRInst(scope, result.src, zir.Inst.Store, .{
+                .ptr = ptr_inst,
+                .value = casted_result,
+            }, .{});
+            return casted_result;
+        },
+        .bitcasted_ptr => |bitcasted_ptr| {
+            return mod.fail(scope, result.src, "TODO implement rlWrap .bitcasted_ptr", .{});
+        },
+        .inferred_ptr => |alloc| {
+            return mod.fail(scope, result.src, "TODO implement rlWrap .inferred_ptr", .{});
+        },
+        .block_ptr => |block_ptr| {
+            return mod.fail(scope, result.src, "TODO implement rlWrap .block_ptr", .{});
+        },
+    }
+}
src-self-hosted/link.zig
@@ -1579,6 +1579,8 @@ pub fn createElfFile(allocator: *Allocator, file: fs.File, options: Options) !Fi
         .elf => {},
         .macho => return error.TODOImplementWritingMachO,
         .wasm => return error.TODOImplementWritingWasmObjects,
+        .hex => return error.TODOImplementWritingHex,
+        .raw => return error.TODOImplementWritingRaw,
     }
 
     var self: File.Elf = .{
@@ -1638,6 +1640,8 @@ fn openBinFileInner(allocator: *Allocator, file: fs.File, options: Options) !Fil
         .elf => {},
         .macho => return error.IncrFailed,
         .wasm => return error.IncrFailed,
+        .hex => return error.IncrFailed,
+        .raw => return error.IncrFailed,
     }
     var self: File.Elf = .{
         .allocator = allocator,
src-self-hosted/main.zig
@@ -141,11 +141,19 @@ const usage_build_generic =
     \\  --name [name]             Override output name
     \\  --mode [mode]             Set the build mode
     \\    Debug                   (default) optimizations off, safety on
-    \\    ReleaseFast             optimizations on, safety off
-    \\    ReleaseSafe             optimizations on, safety on
-    \\    ReleaseSmall            optimize for small binary, safety off
+    \\    ReleaseFast             Optimizations on, safety off
+    \\    ReleaseSafe             Optimizations on, safety on
+    \\    ReleaseSmall            Optimize for small binary, safety off
     \\  --dynamic                 Force output to be dynamically linked
     \\  --strip                   Exclude debug symbols
+    \\  -ofmt=[mode]              Override target object format
+    \\    elf                     Executable and Linking Format
+    \\    c                       Compile to C source code
+    \\    coff   (planned)        Common Object File Format (Windows)
+    \\    pe     (planned)        Portable Executable (Windows)
+    \\    macho  (planned)        macOS relocatables
+    \\    hex    (planned)        Intel IHEX
+    \\    raw    (planned)        Dump machine code directly
     \\
     \\Link Options:
     \\  -l[lib], --library [lib]  Link against system library
@@ -195,7 +203,7 @@ fn buildOutputType(
     var target_arch_os_abi: []const u8 = "native";
     var target_mcpu: ?[]const u8 = null;
     var target_dynamic_linker: ?[]const u8 = null;
-    var object_format: ?std.builtin.ObjectFormat = null;
+    var target_ofmt: ?[]const u8 = null;
 
     var system_libs = std.ArrayList([]const u8).init(gpa);
     defer system_libs.deinit();
@@ -282,12 +290,8 @@ fn buildOutputType(
                     }
                     i += 1;
                     target_mcpu = args[i];
-                } else if (mem.eql(u8, arg, "--c")) {
-                    if (object_format) |old| {
-                        std.debug.print("attempted to override object format {} with C\n", .{old});
-                        process.exit(1);
-                    }
-                    object_format = .c;
+                } else if (mem.startsWith(u8, arg, "-ofmt=")) {
+                    target_ofmt = arg["-ofmt=".len..];
                 } else if (mem.startsWith(u8, arg, "-mcpu=")) {
                     target_mcpu = arg["-mcpu=".len..];
                 } else if (mem.eql(u8, arg, "--dynamic-linker")) {
@@ -434,6 +438,30 @@ fn buildOutputType(
         process.exit(1);
     };
 
+    const object_format: ?std.Target.ObjectFormat = blk: {
+        const ofmt = target_ofmt orelse break :blk null;
+        if (mem.eql(u8, ofmt, "elf")) {
+            break :blk .elf;
+        } else if (mem.eql(u8, ofmt, "c")) {
+            break :blk .c;
+        } else if (mem.eql(u8, ofmt, "coff")) {
+            break :blk .coff;
+        } else if (mem.eql(u8, ofmt, "pe")) {
+            break :blk .coff;
+        } else if (mem.eql(u8, ofmt, "macho")) {
+            break :blk .macho;
+        } else if (mem.eql(u8, ofmt, "wasm")) {
+            break :blk .wasm;
+        } else if (mem.eql(u8, ofmt, "hex")) {
+            break :blk .hex;
+        } else if (mem.eql(u8, ofmt, "raw")) {
+            break :blk .raw;
+        } else {
+            std.debug.print("unsupported object format: {}", .{ofmt});
+            process.exit(1);
+        }
+    };
+
     const bin_path = switch (emit_bin) {
         .no => {
             std.debug.print("-fno-emit-bin not supported yet", .{});
src-self-hosted/Module.zig
@@ -212,7 +212,8 @@ pub const Decl = struct {
             },
             .block => unreachable,
             .gen_zir => unreachable,
-            .local_var => unreachable,
+            .local_val => unreachable,
+            .local_ptr => unreachable,
             .decl => unreachable,
         }
     }
@@ -308,7 +309,8 @@ pub const Scope = struct {
             .block => return self.cast(Block).?.arena,
             .decl => return &self.cast(DeclAnalysis).?.arena.allocator,
             .gen_zir => return self.cast(GenZIR).?.arena,
-            .local_var => return self.cast(LocalVar).?.gen_zir.arena,
+            .local_val => return self.cast(LocalVal).?.gen_zir.arena,
+            .local_ptr => return self.cast(LocalPtr).?.gen_zir.arena,
             .zir_module => return &self.cast(ZIRModule).?.contents.module.arena.allocator,
             .file => unreachable,
         }
@@ -320,7 +322,8 @@ pub const Scope = struct {
         return switch (self.tag) {
             .block => self.cast(Block).?.decl,
             .gen_zir => self.cast(GenZIR).?.decl,
-            .local_var => return self.cast(LocalVar).?.gen_zir.decl,
+            .local_val => return self.cast(LocalVal).?.gen_zir.decl,
+            .local_ptr => return self.cast(LocalPtr).?.gen_zir.decl,
             .decl => self.cast(DeclAnalysis).?.decl,
             .zir_module => null,
             .file => null,
@@ -333,7 +336,8 @@ pub const Scope = struct {
         switch (self.tag) {
             .block => return self.cast(Block).?.decl.scope,
             .gen_zir => return self.cast(GenZIR).?.decl.scope,
-            .local_var => return self.cast(LocalVar).?.gen_zir.decl.scope,
+            .local_val => return self.cast(LocalVal).?.gen_zir.decl.scope,
+            .local_ptr => return self.cast(LocalPtr).?.gen_zir.decl.scope,
             .decl => return self.cast(DeclAnalysis).?.decl.scope,
             .zir_module, .file => return self,
         }
@@ -346,7 +350,8 @@ pub const Scope = struct {
         switch (self.tag) {
             .block => unreachable,
             .gen_zir => unreachable,
-            .local_var => unreachable,
+            .local_val => unreachable,
+            .local_ptr => unreachable,
             .decl => unreachable,
             .zir_module => return self.cast(ZIRModule).?.fullyQualifiedNameHash(name),
             .file => return self.cast(File).?.fullyQualifiedNameHash(name),
@@ -361,7 +366,8 @@ pub const Scope = struct {
             .decl => return self.cast(DeclAnalysis).?.decl.scope.cast(File).?.contents.tree,
             .block => return self.cast(Block).?.decl.scope.cast(File).?.contents.tree,
             .gen_zir => return self.cast(GenZIR).?.decl.scope.cast(File).?.contents.tree,
-            .local_var => return self.cast(LocalVar).?.gen_zir.decl.scope.cast(File).?.contents.tree,
+            .local_val => return self.cast(LocalVal).?.gen_zir.decl.scope.cast(File).?.contents.tree,
+            .local_ptr => return self.cast(LocalPtr).?.gen_zir.decl.scope.cast(File).?.contents.tree,
         }
     }
 
@@ -370,7 +376,8 @@ pub const Scope = struct {
         return switch (self.tag) {
             .block => unreachable,
             .gen_zir => self.cast(GenZIR).?,
-            .local_var => return self.cast(LocalVar).?.gen_zir,
+            .local_val => return self.cast(LocalVal).?.gen_zir,
+            .local_ptr => return self.cast(LocalPtr).?.gen_zir,
             .decl => unreachable,
             .zir_module => unreachable,
             .file => unreachable,
@@ -397,7 +404,8 @@ pub const Scope = struct {
             .zir_module => return @fieldParentPtr(ZIRModule, "base", base).sub_file_path,
             .block => unreachable,
             .gen_zir => unreachable,
-            .local_var => unreachable,
+            .local_val => unreachable,
+            .local_ptr => unreachable,
             .decl => unreachable,
         }
     }
@@ -408,7 +416,8 @@ pub const Scope = struct {
             .zir_module => return @fieldParentPtr(ZIRModule, "base", base).unload(gpa),
             .block => unreachable,
             .gen_zir => unreachable,
-            .local_var => unreachable,
+            .local_val => unreachable,
+            .local_ptr => unreachable,
             .decl => unreachable,
         }
     }
@@ -418,7 +427,8 @@ pub const Scope = struct {
             .file => return @fieldParentPtr(File, "base", base).getSource(module),
             .zir_module => return @fieldParentPtr(ZIRModule, "base", base).getSource(module),
             .gen_zir => unreachable,
-            .local_var => unreachable,
+            .local_val => unreachable,
+            .local_ptr => unreachable,
             .block => unreachable,
             .decl => unreachable,
         }
@@ -431,7 +441,8 @@ pub const Scope = struct {
             .zir_module => return @fieldParentPtr(ZIRModule, "base", base).removeDecl(child),
             .block => unreachable,
             .gen_zir => unreachable,
-            .local_var => unreachable,
+            .local_val => unreachable,
+            .local_ptr => unreachable,
             .decl => unreachable,
         }
     }
@@ -451,7 +462,8 @@ pub const Scope = struct {
             },
             .block => unreachable,
             .gen_zir => unreachable,
-            .local_var => unreachable,
+            .local_val => unreachable,
+            .local_ptr => unreachable,
             .decl => unreachable,
         }
     }
@@ -472,7 +484,8 @@ pub const Scope = struct {
         block,
         decl,
         gen_zir,
-        local_var,
+        local_val,
+        local_ptr,
     };
 
     pub const File = struct {
@@ -708,17 +721,31 @@ pub const Scope = struct {
         instructions: std.ArrayListUnmanaged(*zir.Inst) = .{},
     };
 
+    /// This is always a `const` local and importantly the `inst` is a value type, not a pointer.
     /// This structure lives as long as the AST generation of the Block
     /// node that contains the variable.
-    pub const LocalVar = struct {
-        pub const base_tag: Tag = .local_var;
+    pub const LocalVal = struct {
+        pub const base_tag: Tag = .local_val;
         base: Scope = Scope{ .tag = base_tag },
-        /// Parents can be: `LocalVar`, `GenZIR`.
+        /// Parents can be: `LocalVal`, `LocalPtr`, `GenZIR`.
         parent: *Scope,
         gen_zir: *GenZIR,
         name: []const u8,
         inst: *zir.Inst,
     };
+
+    /// This could be a `const` or `var` local. It has a pointer instead of a value.
+    /// This structure lives as long as the AST generation of the Block
+    /// node that contains the variable.
+    pub const LocalPtr = struct {
+        pub const base_tag: Tag = .local_ptr;
+        base: Scope = Scope{ .tag = base_tag },
+        /// Parents can be: `LocalVal`, `LocalPtr`, `GenZIR`.
+        parent: *Scope,
+        gen_zir: *GenZIR,
+        name: []const u8,
+        ptr: *zir.Inst,
+    };
 };
 
 pub const AllErrors = struct {
@@ -1176,12 +1203,19 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool {
 
             const param_decls = fn_proto.params();
             const param_types = try fn_type_scope.arena.alloc(*zir.Inst, param_decls.len);
+
+            const fn_src = tree.token_locs[fn_proto.fn_token].start;
+            const type_type = try self.addZIRInstConst(&fn_type_scope.base, fn_src, .{
+                .ty = Type.initTag(.type),
+                .val = Value.initTag(.type_type),
+            });
+            const type_type_rl: astgen.ResultLoc = .{ .ty = type_type };
             for (param_decls) |param_decl, i| {
                 const param_type_node = switch (param_decl.param_type) {
                     .any_type => |node| return self.failNode(&fn_type_scope.base, node, "TODO implement anytype parameter", .{}),
                     .type_expr => |node| node,
                 };
-                param_types[i] = try astgen.expr(self, &fn_type_scope.base, param_type_node);
+                param_types[i] = try astgen.expr(self, &fn_type_scope.base, type_type_rl, param_type_node);
             }
             if (fn_proto.getTrailer("var_args_token")) |var_args_token| {
                 return self.failTok(&fn_type_scope.base, var_args_token, "TODO implement var args", .{});
@@ -1209,8 +1243,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool {
                 .Invalid => |tok| return self.failTok(&fn_type_scope.base, tok, "unable to parse return type", .{}),
             };
 
-            const return_type_inst = try astgen.expr(self, &fn_type_scope.base, return_type_expr);
-            const fn_src = tree.token_locs[fn_proto.fn_token].start;
+            const return_type_inst = try astgen.expr(self, &fn_type_scope.base, type_type_rl, return_type_expr);
             const fn_type_inst = try self.addZIRInst(&fn_type_scope.base, fn_src, zir.Inst.FnType, .{
                 .return_type = return_type_inst,
                 .param_types = param_types,
@@ -1266,7 +1299,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool {
                         .kw_args = .{},
                     };
                     gen_scope.instructions.items[i] = &arg.base;
-                    const sub_scope = try gen_scope_arena.allocator.create(Scope.LocalVar);
+                    const sub_scope = try gen_scope_arena.allocator.create(Scope.LocalVal);
                     sub_scope.* = .{
                         .parent = params_scope,
                         .gen_zir = &gen_scope,
@@ -1829,6 +1862,7 @@ fn resolveInst(self: *Module, scope: *Scope, old_inst: *zir.Inst) InnerError!*In
     return self.analyzeDeref(scope, old_inst.src, decl_ref, old_inst.src);
 }
 
+/// TODO split this into `requireRuntimeBlock` and `requireFunctionBlock` and audit callsites.
 fn requireRuntimeBlock(self: *Module, scope: *Scope, src: usize) !*Scope.Block {
     return scope.cast(Scope.Block) orelse
         return self.fail(scope, src, "instruction illegal outside function body", .{});
@@ -2098,12 +2132,7 @@ pub fn addZIRInstSpecial(
     return inst;
 }
 
-pub fn addZIRNoOp(
-    self: *Module,
-    scope: *Scope,
-    src: usize,
-    tag: zir.Inst.Tag,
-) !*zir.Inst {
+pub fn addZIRNoOpT(self: *Module, scope: *Scope, src: usize, tag: zir.Inst.Tag) !*zir.Inst.NoOp {
     const gen_zir = scope.getGenZIR();
     try gen_zir.instructions.ensureCapacity(self.gpa, gen_zir.instructions.items.len + 1);
     const inst = try gen_zir.arena.create(zir.Inst.NoOp);
@@ -2116,6 +2145,11 @@ pub fn addZIRNoOp(
         .kw_args = .{},
     };
     gen_zir.instructions.appendAssumeCapacity(&inst.base);
+    return inst;
+}
+
+pub fn addZIRNoOp(self: *Module, scope: *Scope, src: usize, tag: zir.Inst.Tag) !*zir.Inst {
+    const inst = try self.addZIRNoOpT(scope, src, tag);
     return &inst.base;
 }
 
@@ -2320,24 +2354,36 @@ fn analyzeInstConst(self: *Module, scope: *Scope, const_inst: *zir.Inst.Const) I
 
 fn analyzeInst(self: *Module, scope: *Scope, old_inst: *zir.Inst) InnerError!*Inst {
     switch (old_inst.tag) {
+        .alloc => return self.analyzeInstAlloc(scope, old_inst.castTag(.alloc).?),
+        .alloc_inferred => return self.analyzeInstAllocInferred(scope, old_inst.castTag(.alloc_inferred).?),
         .arg => return self.analyzeInstArg(scope, old_inst.castTag(.arg).?),
+        .bitcast_result_ptr => return self.analyzeInstBitCastResultPtr(scope, old_inst.castTag(.bitcast_result_ptr).?),
         .block => return self.analyzeInstBlock(scope, old_inst.castTag(.block).?),
         .@"break" => return self.analyzeInstBreak(scope, old_inst.castTag(.@"break").?),
         .breakpoint => return self.analyzeInstBreakpoint(scope, old_inst.castTag(.breakpoint).?),
         .breakvoid => return self.analyzeInstBreakVoid(scope, old_inst.castTag(.breakvoid).?),
         .call => return self.analyzeInstCall(scope, old_inst.castTag(.call).?),
+        .coerce_result_block_ptr => return self.analyzeInstCoerceResultBlockPtr(scope, old_inst.castTag(.coerce_result_block_ptr).?),
+        .coerce_result_ptr => return self.analyzeInstCoerceResultPtr(scope, old_inst.castTag(.coerce_result_ptr).?),
+        .coerce_to_ptr_elem => return self.analyzeInstCoerceToPtrElem(scope, old_inst.castTag(.coerce_to_ptr_elem).?),
         .compileerror => return self.analyzeInstCompileError(scope, old_inst.castTag(.compileerror).?),
         .@"const" => return self.analyzeInstConst(scope, old_inst.castTag(.@"const").?),
         .declref => return self.analyzeInstDeclRef(scope, old_inst.castTag(.declref).?),
         .declref_str => return self.analyzeInstDeclRefStr(scope, old_inst.castTag(.declref_str).?),
         .declval => return self.analyzeInstDeclVal(scope, old_inst.castTag(.declval).?),
         .declval_in_module => return self.analyzeInstDeclValInModule(scope, old_inst.castTag(.declval_in_module).?),
+        .ensure_result_used => return self.analyzeInstEnsureResultUsed(scope, old_inst.castTag(.ensure_result_used).?),
+        .ensure_result_non_error => return self.analyzeInstEnsureResultNonError(scope, old_inst.castTag(.ensure_result_non_error).?),
+        .ret_ptr => return self.analyzeInstRetPtr(scope, old_inst.castTag(.ret_ptr).?),
+        .ret_type => return self.analyzeInstRetType(scope, old_inst.castTag(.ret_type).?),
+        .store => return self.analyzeInstStore(scope, old_inst.castTag(.store).?),
         .str => return self.analyzeInstStr(scope, old_inst.castTag(.str).?),
         .int => {
             const big_int = old_inst.castTag(.int).?.positionals.int;
             return self.constIntBig(scope, old_inst.src, Type.initTag(.comptime_int), big_int);
         },
         .inttype => return self.analyzeInstIntType(scope, old_inst.castTag(.inttype).?),
+        .param_type => return self.analyzeInstParamType(scope, old_inst.castTag(.param_type).?),
         .ptrtoint => return self.analyzeInstPtrToInt(scope, old_inst.castTag(.ptrtoint).?),
         .fieldptr => return self.analyzeInstFieldPtr(scope, old_inst.castTag(.fieldptr).?),
         .deref => return self.analyzeInstDeref(scope, old_inst.castTag(.deref).?),
@@ -2369,6 +2415,94 @@ fn analyzeInst(self: *Module, scope: *Scope, old_inst: *zir.Inst) InnerError!*In
     }
 }
 
+fn analyzeInstCoerceResultBlockPtr(
+    self: *Module,
+    scope: *Scope,
+    inst: *zir.Inst.CoerceResultBlockPtr,
+) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstCoerceResultBlockPtr", .{});
+}
+
+fn analyzeInstBitCastResultPtr(self: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstBitCastResultPtr", .{});
+}
+
+fn analyzeInstCoerceResultPtr(self: *Module, scope: *Scope, inst: *zir.Inst.BinOp) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstCoerceResultPtr", .{});
+}
+
+fn analyzeInstCoerceToPtrElem(self: *Module, scope: *Scope, inst: *zir.Inst.CoerceToPtrElem) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstCoerceToPtrElem", .{});
+}
+
+fn analyzeInstRetPtr(self: *Module, scope: *Scope, inst: *zir.Inst.NoOp) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstRetPtr", .{});
+}
+
+fn analyzeInstRetType(self: *Module, scope: *Scope, inst: *zir.Inst.NoOp) InnerError!*Inst {
+    const b = try self.requireRuntimeBlock(scope, inst.base.src);
+    const fn_ty = b.func.?.owner_decl.typed_value.most_recent.typed_value.ty;
+    const ret_type = fn_ty.fnReturnType();
+    return self.constType(scope, inst.base.src, ret_type);
+}
+
+fn analyzeInstEnsureResultUsed(self: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError!*Inst {
+    const operand = try self.resolveInst(scope, inst.positionals.operand);
+    switch (operand.ty.zigTypeTag()) {
+        .Void, .NoReturn => return self.constVoid(scope, operand.src),
+        else => return self.fail(scope, operand.src, "expression value is ignored", .{}),
+    }
+}
+
+fn analyzeInstEnsureResultNonError(self: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError!*Inst {
+    const operand = try self.resolveInst(scope, inst.positionals.operand);
+    switch (operand.ty.zigTypeTag()) {
+        .ErrorSet, .ErrorUnion => return self.fail(scope, operand.src, "error is discarded", .{}),
+        else => return self.constVoid(scope, operand.src),
+    }
+}
+
+fn analyzeInstAlloc(self: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstAlloc", .{});
+}
+
+fn analyzeInstAllocInferred(self: *Module, scope: *Scope, inst: *zir.Inst.NoOp) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstAllocInferred", .{});
+}
+
+fn analyzeInstStore(self: *Module, scope: *Scope, inst: *zir.Inst.Store) InnerError!*Inst {
+    return self.fail(scope, inst.base.src, "TODO implement analyzeInstStore", .{});
+}
+
+fn analyzeInstParamType(self: *Module, scope: *Scope, inst: *zir.Inst.ParamType) InnerError!*Inst {
+    const fn_inst = try self.resolveInst(scope, inst.positionals.func);
+    const arg_index = inst.positionals.arg_index;
+
+    const fn_ty: Type = switch (fn_inst.ty.zigTypeTag()) {
+        .Fn => fn_inst.ty,
+        .BoundFn => {
+            return self.fail(scope, fn_inst.src, "TODO implement analyzeInstParamType for method call syntax", .{});
+        },
+        else => {
+            return self.fail(scope, fn_inst.src, "expected function, found '{}'", .{fn_inst.ty});
+        },
+    };
+
+    // TODO support C-style var args
+    const param_count = fn_ty.fnParamLen();
+    if (arg_index >= param_count) {
+        return self.fail(scope, inst.base.src, "arg index {} out of bounds; '{}' has {} arguments", .{
+            arg_index,
+            fn_ty,
+            param_count,
+        });
+    }
+
+    // TODO support generic functions
+    const param_type = fn_ty.fnParamType(arg_index);
+    return self.constType(scope, inst.base.src, param_type);
+}
+
 fn analyzeInstStr(self: *Module, scope: *Scope, str_inst: *zir.Inst.Str) InnerError!*Inst {
     // The bytes references memory inside the ZIR module, which can get deallocated
     // after semantic analysis is complete. We need the memory to be in the new anonymous Decl's arena.
@@ -2746,13 +2880,13 @@ fn analyzeInstPrimitive(self: *Module, scope: *Scope, primitive: *zir.Inst.Primi
     return self.constInst(scope, primitive.base.src, primitive.positionals.tag.toTypedValue());
 }
 
-fn analyzeInstAs(self: *Module, scope: *Scope, as: *zir.Inst.As) InnerError!*Inst {
-    const dest_type = try self.resolveType(scope, as.positionals.dest_type);
-    const new_inst = try self.resolveInst(scope, as.positionals.value);
+fn analyzeInstAs(self: *Module, scope: *Scope, as: *zir.Inst.BinOp) InnerError!*Inst {
+    const dest_type = try self.resolveType(scope, as.positionals.lhs);
+    const new_inst = try self.resolveInst(scope, as.positionals.rhs);
     return self.coerce(scope, dest_type, new_inst);
 }
 
-fn analyzeInstPtrToInt(self: *Module, scope: *Scope, ptrtoint: *zir.Inst.PtrToInt) InnerError!*Inst {
+fn analyzeInstPtrToInt(self: *Module, scope: *Scope, ptrtoint: *zir.Inst.UnOp) InnerError!*Inst {
     const ptr = try self.resolveInst(scope, ptrtoint.positionals.operand);
     if (ptr.ty.zigTypeTag() != .Pointer) {
         return self.fail(scope, ptrtoint.positionals.operand.src, "expected pointer, found '{}'", .{ptr.ty});
@@ -2797,16 +2931,16 @@ fn analyzeInstFieldPtr(self: *Module, scope: *Scope, fieldptr: *zir.Inst.FieldPt
     }
 }
 
-fn analyzeInstIntCast(self: *Module, scope: *Scope, inst: *zir.Inst.IntCast) InnerError!*Inst {
-    const dest_type = try self.resolveType(scope, inst.positionals.dest_type);
-    const operand = try self.resolveInst(scope, inst.positionals.operand);
+fn analyzeInstIntCast(self: *Module, scope: *Scope, inst: *zir.Inst.BinOp) InnerError!*Inst {
+    const dest_type = try self.resolveType(scope, inst.positionals.lhs);
+    const operand = try self.resolveInst(scope, inst.positionals.rhs);
 
     const dest_is_comptime_int = switch (dest_type.zigTypeTag()) {
         .ComptimeInt => true,
         .Int => false,
         else => return self.fail(
             scope,
-            inst.positionals.dest_type.src,
+            inst.positionals.lhs.src,
             "expected integer type, found '{}'",
             .{
                 dest_type,
@@ -2818,7 +2952,7 @@ fn analyzeInstIntCast(self: *Module, scope: *Scope, inst: *zir.Inst.IntCast) Inn
         .ComptimeInt, .Int => {},
         else => return self.fail(
             scope,
-            inst.positionals.operand.src,
+            inst.positionals.rhs.src,
             "expected integer type, found '{}'",
             .{operand.ty},
         ),
@@ -2833,22 +2967,22 @@ fn analyzeInstIntCast(self: *Module, scope: *Scope, inst: *zir.Inst.IntCast) Inn
     return self.fail(scope, inst.base.src, "TODO implement analyze widen or shorten int", .{});
 }
 
-fn analyzeInstBitCast(self: *Module, scope: *Scope, inst: *zir.Inst.BitCast) InnerError!*Inst {
-    const dest_type = try self.resolveType(scope, inst.positionals.dest_type);
-    const operand = try self.resolveInst(scope, inst.positionals.operand);
+fn analyzeInstBitCast(self: *Module, scope: *Scope, inst: *zir.Inst.BinOp) InnerError!*Inst {
+    const dest_type = try self.resolveType(scope, inst.positionals.lhs);
+    const operand = try self.resolveInst(scope, inst.positionals.rhs);
     return self.bitcast(scope, dest_type, operand);
 }
 
-fn analyzeInstFloatCast(self: *Module, scope: *Scope, inst: *zir.Inst.FloatCast) InnerError!*Inst {
-    const dest_type = try self.resolveType(scope, inst.positionals.dest_type);
-    const operand = try self.resolveInst(scope, inst.positionals.operand);
+fn analyzeInstFloatCast(self: *Module, scope: *Scope, inst: *zir.Inst.BinOp) InnerError!*Inst {
+    const dest_type = try self.resolveType(scope, inst.positionals.lhs);
+    const operand = try self.resolveInst(scope, inst.positionals.rhs);
 
     const dest_is_comptime_float = switch (dest_type.zigTypeTag()) {
         .ComptimeFloat => true,
         .Float => false,
         else => return self.fail(
             scope,
-            inst.positionals.dest_type.src,
+            inst.positionals.lhs.src,
             "expected float type, found '{}'",
             .{
                 dest_type,
@@ -2860,7 +2994,7 @@ fn analyzeInstFloatCast(self: *Module, scope: *Scope, inst: *zir.Inst.FloatCast)
         .ComptimeFloat, .Float, .ComptimeInt => {},
         else => return self.fail(
             scope,
-            inst.positionals.operand.src,
+            inst.positionals.rhs.src,
             "expected float type, found '{}'",
             .{operand.ty},
         ),
@@ -3560,8 +3694,14 @@ fn failWithOwnedErrorMsg(self: *Module, scope: *Scope, src: usize, err_msg: *Err
             gen_zir.decl.generation = self.generation;
             self.failed_decls.putAssumeCapacityNoClobber(gen_zir.decl, err_msg);
         },
-        .local_var => {
-            const gen_zir = scope.cast(Scope.LocalVar).?.gen_zir;
+        .local_val => {
+            const gen_zir = scope.cast(Scope.LocalVal).?.gen_zir;
+            gen_zir.decl.analysis = .sema_failure;
+            gen_zir.decl.generation = self.generation;
+            self.failed_decls.putAssumeCapacityNoClobber(gen_zir.decl, err_msg);
+        },
+        .local_ptr => {
+            const gen_zir = scope.cast(Scope.LocalPtr).?.gen_zir;
             gen_zir.decl.analysis = .sema_failure;
             gen_zir.decl.generation = self.generation;
             self.failed_decls.putAssumeCapacityNoClobber(gen_zir.decl, err_msg);
src-self-hosted/translate_c.zig
@@ -1308,8 +1308,7 @@ fn transBinaryOperator(
             const rhs = try transExpr(rp, &block_scope.base, ZigClangBinaryOperator_getRHS(stmt), .used, .r_value);
             if (expr) {
                 _ = try appendToken(rp.c, .Semicolon, ";");
-                const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label);
-                break_node.rhs = rhs;
+                const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label, rhs);
                 try block_scope.statements.append(&break_node.base);
                 const block_node = try block_scope.complete(rp.c);
                 const rparen = try appendToken(rp.c, .RParen, ")");
@@ -1881,12 +1880,19 @@ fn transReturnStmt(
     scope: *Scope,
     expr: *const ZigClangReturnStmt,
 ) TransError!*ast.Node {
-    const node = try transCreateNodeReturnExpr(rp.c);
-    if (ZigClangReturnStmt_getRetValue(expr)) |val_expr| {
-        node.rhs = try transExprCoercing(rp, scope, val_expr, .used, .r_value);
-    }
+    const return_kw = try appendToken(rp.c, .Keyword_return, "return");
+    const rhs: ?*ast.Node = if (ZigClangReturnStmt_getRetValue(expr)) |val_expr|
+        try transExprCoercing(rp, scope, val_expr, .used, .r_value)
+    else
+        null;
+    const return_expr = try ast.Node.ControlFlowExpression.create(rp.c.arena, .{
+        .ltoken = return_kw,
+        .tag = .Return,
+    }, .{
+        .rhs = rhs,
+    });
     _ = try appendToken(rp.c, .Semicolon, ";");
-    return &node.base;
+    return &return_expr.base;
 }
 
 fn transStringLiteral(
@@ -1912,8 +1918,9 @@ fn transStringLiteral(
             buf[buf.len - 1] = '"';
 
             const token = try appendToken(rp.c, .StringLiteral, buf);
-            const node = try rp.c.arena.create(ast.Node.StringLiteral);
+            const node = try rp.c.arena.create(ast.Node.OneToken);
             node.* = .{
+                .base = .{ .tag = .StringLiteral },
                 .token = token,
             };
             return maybeSuppressResult(rp, scope, result_used, &node.base);
@@ -2518,7 +2525,7 @@ fn transDoWhileLoop(
     prefix_op.rhs = try transBoolExpr(rp, &cond_scope.base, @ptrCast(*const ZigClangExpr, ZigClangDoStmt_getCond(stmt)), .used, .r_value, true);
     _ = try appendToken(rp.c, .RParen, ")");
     if_node.condition = &prefix_op.base;
-    if_node.body = &(try transCreateNodeBreak(rp.c, null)).base;
+    if_node.body = &(try transCreateNodeBreak(rp.c, null, null)).base;
     _ = try appendToken(rp.c, .Semicolon, ";");
 
     const body_node = if (ZigClangStmt_getStmtClass(ZigClangDoStmt_getBody(stmt)) == .CompoundStmtClass) blk: {
@@ -2688,7 +2695,7 @@ fn transSwitch(
     _ = try appendToken(rp.c, .Colon, ":");
     if (!switch_scope.has_default) {
         const else_prong = try transCreateNodeSwitchCase(rp.c, try transCreateNodeSwitchElse(rp.c));
-        else_prong.expr = &(try transCreateNodeBreak(rp.c, "__switch")).base;
+        else_prong.expr = &(try transCreateNodeBreak(rp.c, "__switch", null)).base;
         _ = try appendToken(rp.c, .Comma, ",");
 
         if (switch_scope.case_index >= switch_scope.cases.len)
@@ -2732,7 +2739,7 @@ fn transCase(
         try transExpr(rp, scope, ZigClangCaseStmt_getLHS(stmt), .used, .r_value);
 
     const switch_prong = try transCreateNodeSwitchCase(rp.c, expr);
-    switch_prong.expr = &(try transCreateNodeBreak(rp.c, label)).base;
+    switch_prong.expr = &(try transCreateNodeBreak(rp.c, label, null)).base;
     _ = try appendToken(rp.c, .Comma, ",");
 
     if (switch_scope.case_index >= switch_scope.cases.len)
@@ -2768,7 +2775,7 @@ fn transDefault(
     _ = try appendToken(rp.c, .Semicolon, ";");
 
     const else_prong = try transCreateNodeSwitchCase(rp.c, try transCreateNodeSwitchElse(rp.c));
-    else_prong.expr = &(try transCreateNodeBreak(rp.c, label)).base;
+    else_prong.expr = &(try transCreateNodeBreak(rp.c, label, null)).base;
     _ = try appendToken(rp.c, .Comma, ",");
 
     if (switch_scope.case_index >= switch_scope.cases.len)
@@ -2843,8 +2850,9 @@ fn transCharLiteral(
             }
             var char_buf: [4]u8 = undefined;
             const token = try appendTokenFmt(rp.c, .CharLiteral, "'{}'", .{escapeChar(@intCast(u8, val), &char_buf)});
-            const node = try rp.c.arena.create(ast.Node.CharLiteral);
+            const node = try rp.c.arena.create(ast.Node.OneToken);
             node.* = .{
+                .base = .{ .tag = .CharLiteral },
                 .token = token,
             };
             break :blk &node.base;
@@ -2889,8 +2897,11 @@ fn transStmtExpr(rp: RestorePoint, scope: *Scope, stmt: *const ZigClangStmtExpr,
         const result = try transStmt(rp, &block_scope.base, it[0], .unused, .r_value);
         try block_scope.statements.append(result);
     }
-    const break_node = try transCreateNodeBreak(rp.c, "blk");
-    break_node.rhs = try transStmt(rp, &block_scope.base, it[0], .used, .r_value);
+    const break_node = blk: {
+        var tmp = try CtrlFlow.init(rp.c, .Break, "blk");
+        const rhs = try transStmt(rp, &block_scope.base, it[0], .used, .r_value);
+        break :blk try tmp.finish(rhs);
+    };
     _ = try appendToken(rp.c, .Semicolon, ";");
     try block_scope.statements.append(&break_node.base);
     const block_node = try block_scope.complete(rp.c);
@@ -3205,8 +3216,7 @@ fn transCreatePreCrement(
     const assign = try transCreateNodeInfixOp(rp, scope, ref_node, op, token, one, .used, false);
     try block_scope.statements.append(assign);
 
-    const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label);
-    break_node.rhs = ref_node;
+    const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label, ref_node);
     try block_scope.statements.append(&break_node.base);
     const block_node = try block_scope.complete(rp.c);
     // semicolon must immediately follow rbrace because it is the last token in a block
@@ -3297,8 +3307,11 @@ fn transCreatePostCrement(
     const assign = try transCreateNodeInfixOp(rp, scope, ref_node, op, token, one, .used, false);
     try block_scope.statements.append(assign);
 
-    const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label);
-    break_node.rhs = try transCreateNodeIdentifier(rp.c, tmp);
+    const break_node = blk: {
+        var tmp_ctrl_flow = try CtrlFlow.initToken(rp.c, .Break, block_scope.label);
+        const rhs = try transCreateNodeIdentifier(rp.c, tmp);
+        break :blk try tmp_ctrl_flow.finish(rhs);
+    };
     try block_scope.statements.append(&break_node.base);
     _ = try appendToken(rp.c, .Semicolon, ";");
     const block_node = try block_scope.complete(rp.c);
@@ -3490,8 +3503,7 @@ fn transCreateCompoundAssign(
         try block_scope.statements.append(assign);
     }
 
-    const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label);
-    break_node.rhs = ref_node;
+    const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label, ref_node);
     try block_scope.statements.append(&break_node.base);
     const block_node = try block_scope.complete(rp.c);
     const grouped_expr = try rp.c.arena.create(ast.Node.GroupedExpression);
@@ -3567,10 +3579,8 @@ fn transCPtrCast(
 
 fn transBreak(rp: RestorePoint, scope: *Scope) TransError!*ast.Node {
     const break_scope = scope.getBreakableScope();
-    const br = try transCreateNodeBreak(rp.c, if (break_scope.id == .Switch)
-        "__switch"
-    else
-        null);
+    const label_text: ?[]const u8 = if (break_scope.id == .Switch) "__switch" else null;
+    const br = try transCreateNodeBreak(rp.c, label_text, null);
     _ = try appendToken(rp.c, .Semicolon, ";");
     return &br.base;
 }
@@ -3578,8 +3588,9 @@ fn transBreak(rp: RestorePoint, scope: *Scope) TransError!*ast.Node {
 fn transFloatingLiteral(rp: RestorePoint, scope: *Scope, stmt: *const ZigClangFloatingLiteral, used: ResultUsed) TransError!*ast.Node {
     // TODO use something more accurate
     const dbl = ZigClangAPFloat_getValueAsApproximateDouble(stmt);
-    const node = try rp.c.arena.create(ast.Node.FloatLiteral);
+    const node = try rp.c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .FloatLiteral },
         .token = try appendTokenFmt(rp.c, .FloatLiteral, "{d}", .{dbl}),
     };
     return maybeSuppressResult(rp, scope, used, &node.base);
@@ -3619,7 +3630,7 @@ fn transBinaryConditionalOperator(rp: RestorePoint, scope: *Scope, stmt: *const
     });
     try block_scope.statements.append(&tmp_var.base);
 
-    const break_node = try transCreateNodeBreakToken(rp.c, block_scope.label);
+    var break_node_tmp = try CtrlFlow.initToken(rp.c, .Break, block_scope.label);
 
     const if_node = try transCreateNodeIf(rp.c);
     var cond_scope = Scope.Condition{
@@ -3641,7 +3652,7 @@ fn transBinaryConditionalOperator(rp: RestorePoint, scope: *Scope, stmt: *const
     if_node.@"else".?.body = try transExpr(rp, &block_scope.base, false_expr, .used, .r_value);
     _ = try appendToken(rp.c, .Semicolon, ";");
 
-    break_node.rhs = &if_node.base;
+    const break_node = try break_node_tmp.finish(&if_node.base);
     _ = try appendToken(rp.c, .Semicolon, ";");
     try block_scope.statements.append(&break_node.base);
     const block_node = try block_scope.complete(rp.c);
@@ -3822,8 +3833,9 @@ fn qualTypeToLog2IntRef(rp: RestorePoint, qt: ZigClangQualType, source_loc: ZigC
     if (int_bit_width != 0) {
         // we can perform the log2 now.
         const cast_bit_width = math.log2_int(u64, int_bit_width);
-        const node = try rp.c.arena.create(ast.Node.IntegerLiteral);
+        const node = try rp.c.arena.create(ast.Node.OneToken);
         node.* = .{
+            .base = .{ .tag = .IntegerLiteral },
             .token = try appendTokenFmt(rp.c, .Identifier, "u{}", .{cast_bit_width}),
         };
         return &node.base;
@@ -3845,8 +3857,9 @@ fn qualTypeToLog2IntRef(rp: RestorePoint, qt: ZigClangQualType, source_loc: ZigC
 
     const import_fn_call = try rp.c.createBuiltinCall("@import", 1);
     const std_token = try appendToken(rp.c, .StringLiteral, "\"std\"");
-    const std_node = try rp.c.arena.create(ast.Node.StringLiteral);
+    const std_node = try rp.c.arena.create(ast.Node.OneToken);
     std_node.* = .{
+        .base = .{ .tag = .StringLiteral },
         .token = std_token,
     };
     import_fn_call.params()[0] = &std_node.base;
@@ -4081,8 +4094,11 @@ fn transCreateNodeAssign(
     const assign = try transCreateNodeInfixOp(rp, &block_scope.base, lhs_node, .Assign, lhs_eq_token, ident, .used, false);
     try block_scope.statements.append(assign);
 
-    const break_node = try transCreateNodeBreak(rp.c, label_name);
-    break_node.rhs = try transCreateNodeIdentifier(rp.c, tmp);
+    const break_node = blk: {
+        var tmp_ctrl_flow = try CtrlFlow.init(rp.c, .Break, label_name);
+        const rhs_expr = try transCreateNodeIdentifier(rp.c, tmp);
+        break :blk try tmp_ctrl_flow.finish(rhs_expr);
+    };
     _ = try appendToken(rp.c, .Semicolon, ";");
     try block_scope.statements.append(&break_node.base);
     const block_node = try block_scope.complete(rp.c);
@@ -4255,28 +4271,19 @@ fn transCreateNodeAPInt(c: *Context, int: *const ZigClangAPSInt) !*ast.Node {
     };
     defer c.arena.free(str);
     const token = try appendToken(c, .IntegerLiteral, str);
-    const node = try c.arena.create(ast.Node.IntegerLiteral);
+    const node = try c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .IntegerLiteral },
         .token = token,
     };
     return &node.base;
 }
 
-fn transCreateNodeReturnExpr(c: *Context) !*ast.Node.ControlFlowExpression {
-    const ltoken = try appendToken(c, .Keyword_return, "return");
-    const node = try c.arena.create(ast.Node.ControlFlowExpression);
-    node.* = .{
-        .ltoken = ltoken,
-        .kind = .Return,
-        .rhs = null,
-    };
-    return node;
-}
-
 fn transCreateNodeUndefinedLiteral(c: *Context) !*ast.Node {
     const token = try appendToken(c, .Keyword_undefined, "undefined");
-    const node = try c.arena.create(ast.Node.UndefinedLiteral);
+    const node = try c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .UndefinedLiteral },
         .token = token,
     };
     return &node.base;
@@ -4284,8 +4291,9 @@ fn transCreateNodeUndefinedLiteral(c: *Context) !*ast.Node {
 
 fn transCreateNodeNullLiteral(c: *Context) !*ast.Node {
     const token = try appendToken(c, .Keyword_null, "null");
-    const node = try c.arena.create(ast.Node.NullLiteral);
+    const node = try c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .NullLiteral },
         .token = token,
     };
     return &node.base;
@@ -4296,8 +4304,9 @@ fn transCreateNodeBoolLiteral(c: *Context, value: bool) !*ast.Node {
         try appendToken(c, .Keyword_true, "true")
     else
         try appendToken(c, .Keyword_false, "false");
-    const node = try c.arena.create(ast.Node.BoolLiteral);
+    const node = try c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .BoolLiteral },
         .token = token,
     };
     return &node.base;
@@ -4305,8 +4314,9 @@ fn transCreateNodeBoolLiteral(c: *Context, value: bool) !*ast.Node {
 
 fn transCreateNodeInt(c: *Context, int: anytype) !*ast.Node {
     const token = try appendTokenFmt(c, .IntegerLiteral, "{}", .{int});
-    const node = try c.arena.create(ast.Node.IntegerLiteral);
+    const node = try c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .IntegerLiteral },
         .token = token,
     };
     return &node.base;
@@ -4314,8 +4324,9 @@ fn transCreateNodeInt(c: *Context, int: anytype) !*ast.Node {
 
 fn transCreateNodeFloat(c: *Context, int: anytype) !*ast.Node {
     const token = try appendTokenFmt(c, .FloatLiteral, "{}", .{int});
-    const node = try c.arena.create(ast.Node.FloatLiteral);
+    const node = try c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .FloatLiteral },
         .token = token,
     };
     return &node.base;
@@ -4362,7 +4373,7 @@ fn transCreateNodeMacroFn(c: *Context, name: []const u8, ref: *ast.Node, proto_a
 
     const block_lbrace = try appendToken(c, .LBrace, "{");
 
-    const return_expr = try transCreateNodeReturnExpr(c);
+    const return_kw = try appendToken(c, .Keyword_return, "return");
     const unwrap_expr = try transCreateNodeUnwrapNull(c, ref.cast(ast.Node.VarDecl).?.getTrailer("init_node").?);
 
     const call_expr = try c.createCall(unwrap_expr, fn_params.items.len);
@@ -4376,7 +4387,12 @@ fn transCreateNodeMacroFn(c: *Context, name: []const u8, ref: *ast.Node, proto_a
     }
     call_expr.rtoken = try appendToken(c, .RParen, ")");
 
-    return_expr.rhs = &call_expr.base;
+    const return_expr = try ast.Node.ControlFlowExpression.create(c.arena, .{
+        .ltoken = return_kw,
+        .tag = .Return,
+    }, .{
+        .rhs = &call_expr.base,
+    });
     _ = try appendToken(c, .Semicolon, ";");
 
     const block = try ast.Node.Block.alloc(c.arena, 1);
@@ -4424,8 +4440,9 @@ fn transCreateNodeEnumLiteral(c: *Context, name: []const u8) !*ast.Node {
 }
 
 fn transCreateNodeStringLiteral(c: *Context, str: []const u8) !*ast.Node {
-    const node = try c.arena.create(ast.Node.StringLiteral);
+    const node = try c.arena.create(ast.Node.OneToken);
     node.* = .{
+        .base = .{ .tag = .StringLiteral },
         .token = try appendToken(c, .StringLiteral, str),
     };
     return &node.base;
@@ -4455,28 +4472,77 @@ fn transCreateNodeElse(c: *Context) !*ast.Node.Else {
     return node;
 }
 
-fn transCreateNodeBreakToken(c: *Context, label: ?ast.TokenIndex) !*ast.Node.ControlFlowExpression {
-    const other_token = label orelse return transCreateNodeBreak(c, null);
+fn transCreateNodeBreakToken(
+    c: *Context,
+    label: ?ast.TokenIndex,
+    rhs: ?*ast.Node,
+) !*ast.Node.ControlFlowExpression {
+    const other_token = label orelse return transCreateNodeBreak(c, null, rhs);
     const loc = c.token_locs.items[other_token];
     const label_name = c.source_buffer.items[loc.start..loc.end];
-    return transCreateNodeBreak(c, label_name);
+    return transCreateNodeBreak(c, label_name, rhs);
 }
 
-fn transCreateNodeBreak(c: *Context, label: ?[]const u8) !*ast.Node.ControlFlowExpression {
-    const ltoken = try appendToken(c, .Keyword_break, "break");
-    const label_node = if (label) |l| blk: {
-        _ = try appendToken(c, .Colon, ":");
-        break :blk try transCreateNodeIdentifier(c, l);
-    } else null;
-    const node = try c.arena.create(ast.Node.ControlFlowExpression);
-    node.* = .{
-        .ltoken = ltoken,
-        .kind = .{ .Break = label_node },
-        .rhs = null,
-    };
-    return node;
+fn transCreateNodeBreak(
+    c: *Context,
+    label: ?[]const u8,
+    rhs: ?*ast.Node,
+) !*ast.Node.ControlFlowExpression {
+    var ctrl_flow = try CtrlFlow.init(c, .Break, label);
+    return ctrl_flow.finish(rhs);
 }
 
+const CtrlFlow = struct {
+    c: *Context,
+    ltoken: ast.TokenIndex,
+    label_token: ?ast.TokenIndex,
+    tag: ast.Node.Tag,
+
+    /// Does everything except the RHS.
+    fn init(c: *Context, tag: ast.Node.Tag, label: ?[]const u8) !CtrlFlow {
+        const kw: Token.Id = switch (tag) {
+            .Break => .Keyword_break,
+            .Continue => .Keyword_continue,
+            .Return => .Keyword_return,
+            else => unreachable,
+        };
+        const kw_text = switch (tag) {
+            .Break => "break",
+            .Continue => "continue",
+            .Return => "return",
+            else => unreachable,
+        };
+        const ltoken = try appendToken(c, kw, kw_text);
+        const label_token = if (label) |l| blk: {
+            _ = try appendToken(c, .Colon, ":");
+            break :blk try appendToken(c, .Identifier, l);
+        } else null;
+        return CtrlFlow{
+            .c = c,
+            .ltoken = ltoken,
+            .label_token = label_token,
+            .tag = tag,
+        };
+    }
+
+    fn initToken(c: *Context, tag: ast.Node.Tag, label: ?ast.TokenIndex) !CtrlFlow {
+        const other_token = label orelse return init(c, tag, null);
+        const loc = c.token_locs.items[other_token];
+        const label_name = c.source_buffer.items[loc.start..loc.end];
+        return init(c, tag, label_name);
+    }
+
+    fn finish(self: *CtrlFlow, rhs: ?*ast.Node) !*ast.Node.ControlFlowExpression {
+        return ast.Node.ControlFlowExpression.create(self.c.arena, .{
+            .ltoken = self.ltoken,
+            .tag = self.tag,
+        }, .{
+            .label = self.label_token,
+            .rhs = rhs,
+        });
+    }
+};
+
 fn transCreateNodeWhile(c: *Context) !*ast.Node.While {
     const while_tok = try appendToken(c, .Keyword_while, "while");
     _ = try appendToken(c, .LParen, "(");
@@ -4497,12 +4563,10 @@ fn transCreateNodeWhile(c: *Context) !*ast.Node.While {
 
 fn transCreateNodeContinue(c: *Context) !*ast.Node {
     const ltoken = try appendToken(c, .Keyword_continue, "continue");
-    const node = try c.arena.create(ast.Node.ControlFlowExpression);
-    node.* = .{
+    const node = try ast.Node.ControlFlowExpression.create(c.arena, .{
         .ltoken = ltoken,
-        .kind = .{ .Continue = null },
-        .rhs = null,
-    };
+        .tag = .Continue,
+    }, .{});
     _ = try appendToken(c, .Semicolon, ";");
     return &node.base;
 }
@@ -5006,8 +5070,9 @@ pub fn failDecl(c: *Context, loc: ZigClangSourceLocation, name: []const u8, comp
     const semi_tok = try appendToken(c, .Semicolon, ";");
     _ = try appendTokenFmt(c, .LineComment, "// {}", .{c.locStr(loc)});
 
-    const msg_node = try c.arena.create(ast.Node.StringLiteral);
+    const msg_node = try c.arena.create(ast.Node.OneToken);
     msg_node.* = .{
+        .base = .{ .tag = .StringLiteral },
         .token = msg_tok,
     };
 
@@ -5110,8 +5175,9 @@ fn appendIdentifier(c: *Context, name: []const u8) !ast.TokenIndex {
 
 fn transCreateNodeIdentifier(c: *Context, name: []const u8) !*ast.Node {
     const token_index = try appendIdentifier(c, name);
-    const identifier = try c.arena.create(ast.Node.Identifier);
+    const identifier = try c.arena.create(ast.Node.OneToken);
     identifier.* = .{
+        .base = .{ .tag = .Identifier },
         .token = token_index,
     };
     return &identifier.base;
@@ -5119,8 +5185,9 @@ fn transCreateNodeIdentifier(c: *Context, name: []const u8) !*ast.Node {
 
 fn transCreateNodeIdentifierUnchecked(c: *Context, name: []const u8) !*ast.Node {
     const token_index = try appendTokenFmt(c, .Identifier, "{}", .{name});
-    const identifier = try c.arena.create(ast.Node.Identifier);
+    const identifier = try c.arena.create(ast.Node.OneToken);
     identifier.* = .{
+        .base = .{ .tag = .Identifier },
         .token = token_index,
     };
     return &identifier.base;
@@ -5289,8 +5356,9 @@ fn transMacroFnDefine(c: *Context, it: *CTokenList.Iterator, source: []const u8,
         const param_name_tok = try appendIdentifier(c, mangled_name);
         _ = try appendToken(c, .Colon, ":");
 
-        const any_type = try c.arena.create(ast.Node.AnyType);
+        const any_type = try c.arena.create(ast.Node.OneToken);
         any_type.* = .{
+            .base = .{ .tag = .AnyType },
             .token = try appendToken(c, .Keyword_anytype, "anytype"),
         };
 
@@ -5322,7 +5390,7 @@ fn transMacroFnDefine(c: *Context, it: *CTokenList.Iterator, source: []const u8,
 
     const type_of = try c.createBuiltinCall("@TypeOf", 1);
 
-    const return_expr = try transCreateNodeReturnExpr(c);
+    const return_kw = try appendToken(c, .Keyword_return, "return");
     const expr = try parseCExpr(c, it, source, source_loc, scope);
     const last = it.next().?;
     if (last.id != .Eof and last.id != .Nl)
@@ -5337,13 +5405,17 @@ fn transMacroFnDefine(c: *Context, it: *CTokenList.Iterator, source: []const u8,
     const type_of_arg = if (expr.tag != .Block) expr else blk: {
         const blk = @fieldParentPtr(ast.Node.Block, "base", expr);
         const blk_last = blk.statements()[blk.statements_len - 1];
-        std.debug.assert(blk_last.tag == .ControlFlowExpression);
-        const br = @fieldParentPtr(ast.Node.ControlFlowExpression, "base", blk_last);
-        break :blk br.rhs.?;
+        const br = blk_last.cast(ast.Node.ControlFlowExpression).?;
+        break :blk br.getRHS().?;
     };
     type_of.params()[0] = type_of_arg;
     type_of.rparen_token = try appendToken(c, .RParen, ")");
-    return_expr.rhs = expr;
+    const return_expr = try ast.Node.ControlFlowExpression.create(c.arena, .{
+        .ltoken = return_kw,
+        .tag = .Return,
+    }, .{
+        .rhs = expr,
+    });
 
     try block_scope.statements.append(&return_expr.base);
     const block_node = try block_scope.complete(c);
@@ -5416,8 +5488,7 @@ fn parseCExpr(c: *Context, it: *CTokenList.Iterator, source: []const u8, source_
                 }
             }
 
-            const break_node = try transCreateNodeBreak(c, label_name);
-            break_node.rhs = last;
+            const break_node = try transCreateNodeBreak(c, label_name, last);
             try block_scope.statements.append(&break_node.base);
             const block_node = try block_scope.complete(c);
             return &block_node.base;
@@ -5656,15 +5727,17 @@ fn parseCPrimaryExpr(c: *Context, it: *CTokenList.Iterator, source: []const u8,
             const first_tok = it.list.at(0);
             if (source[tok.start] != '\'' or source[tok.start + 1] == '\\' or tok.end - tok.start == 3) {
                 const token = try appendToken(c, .CharLiteral, try zigifyEscapeSequences(c, source[tok.start..tok.end], source[first_tok.start..first_tok.end], source_loc));
-                const node = try c.arena.create(ast.Node.CharLiteral);
+                const node = try c.arena.create(ast.Node.OneToken);
                 node.* = .{
+                    .base = .{ .tag = .CharLiteral },
                     .token = token,
                 };
                 return &node.base;
             } else {
                 const token = try appendTokenFmt(c, .IntegerLiteral, "0x{x}", .{source[tok.start + 1 .. tok.end - 1]});
-                const node = try c.arena.create(ast.Node.IntegerLiteral);
+                const node = try c.arena.create(ast.Node.OneToken);
                 node.* = .{
+                    .base = .{ .tag = .IntegerLiteral },
                     .token = token,
                 };
                 return &node.base;
@@ -5673,8 +5746,9 @@ fn parseCPrimaryExpr(c: *Context, it: *CTokenList.Iterator, source: []const u8,
         .StringLiteral => {
             const first_tok = it.list.at(0);
             const token = try appendToken(c, .StringLiteral, try zigifyEscapeSequences(c, source[tok.start..tok.end], source[first_tok.start..first_tok.end], source_loc));
-            const node = try c.arena.create(ast.Node.StringLiteral);
+            const node = try c.arena.create(ast.Node.OneToken);
             node.* = .{
+                .base = .{ .tag = .StringLiteral },
                 .token = token,
             };
             return &node.base;
@@ -6224,7 +6298,7 @@ fn getContainer(c: *Context, node: *ast.Node) ?*ast.Node {
         => return node,
 
         .Identifier => {
-            const ident = node.cast(ast.Node.Identifier).?;
+            const ident = node.castTag(.Identifier).?;
             if (c.global_scope.sym_table.get(tokenSlice(c, ident.token))) |value| {
                 if (value.cast(ast.Node.VarDecl)) |var_decl|
                     return getContainer(c, var_decl.getTrailer("init_node").?);
@@ -6238,7 +6312,7 @@ fn getContainer(c: *Context, node: *ast.Node) ?*ast.Node {
                 if (ty_node.cast(ast.Node.ContainerDecl)) |container| {
                     for (container.fieldsAndDecls()) |field_ref| {
                         const field = field_ref.cast(ast.Node.ContainerField).?;
-                        const ident = infix.rhs.cast(ast.Node.Identifier).?;
+                        const ident = infix.rhs.castTag(.Identifier).?;
                         if (mem.eql(u8, tokenSlice(c, field.name_token), tokenSlice(c, ident.token))) {
                             return getContainer(c, field.type_expr.?);
                         }
@@ -6253,7 +6327,7 @@ fn getContainer(c: *Context, node: *ast.Node) ?*ast.Node {
 }
 
 fn getContainerTypeOf(c: *Context, ref: *ast.Node) ?*ast.Node {
-    if (ref.cast(ast.Node.Identifier)) |ident| {
+    if (ref.castTag(.Identifier)) |ident| {
         if (c.global_scope.sym_table.get(tokenSlice(c, ident.token))) |value| {
             if (value.cast(ast.Node.VarDecl)) |var_decl| {
                 if (var_decl.getTrailer("type_node")) |ty|
@@ -6265,7 +6339,7 @@ fn getContainerTypeOf(c: *Context, ref: *ast.Node) ?*ast.Node {
             if (ty_node.cast(ast.Node.ContainerDecl)) |container| {
                 for (container.fieldsAndDecls()) |field_ref| {
                     const field = field_ref.cast(ast.Node.ContainerField).?;
-                    const ident = infix.rhs.cast(ast.Node.Identifier).?;
+                    const ident = infix.rhs.castTag(.Identifier).?;
                     if (mem.eql(u8, tokenSlice(c, field.name_token), tokenSlice(c, ident.token))) {
                         return getContainer(c, field.type_expr.?);
                     }
src-self-hosted/zir.zig
@@ -34,9 +34,17 @@ pub const Inst = struct {
 
     /// These names are used directly as the instruction names in the text format.
     pub const Tag = enum {
+        /// Allocates stack local memory. Its lifetime ends when the block ends that contains
+        /// this instruction.
+        alloc,
+        /// Same as `alloc` except the type is inferred.
+        alloc_inferred,
         /// Function parameter value. These must be first in a function's main block,
         /// in respective order with the parameters.
         arg,
+        /// A typed result location pointer is bitcasted to a new result location pointer.
+        /// The new result location pointer has an inferred type.
+        bitcast_result_ptr,
         /// A labeled block of code, which can return a value.
         block,
         /// Return a value from a `Block`.
@@ -45,6 +53,17 @@ pub const Inst = struct {
         /// Same as `break` but without an operand; the operand is assumed to be the void value.
         breakvoid,
         call,
+        /// Coerces a result location pointer to a new element type. It is evaluated "backwards"-
+        /// as type coercion from the new element type to the old element type.
+        /// LHS is destination element type, RHS is result pointer.
+        coerce_result_ptr,
+        /// This instruction does a `coerce_result_ptr` operation on a `Block`'s
+        /// result location pointer, whose type is inferred by peer type resolution on the
+        /// `Block`'s corresponding `break` instructions.
+        coerce_result_block_ptr,
+        /// Equivalent to `as(ptr_child_type(typeof(ptr)), value)`.
+        coerce_to_ptr_elem,
+        /// Emit an error message and fail compilation.
         compileerror,
         /// Special case, has no textual representation.
         @"const",
@@ -57,7 +76,17 @@ pub const Inst = struct {
         declval,
         /// Same as declval but the parameter is a `*Module.Decl` rather than a name.
         declval_in_module,
+        /// Emits a compile error if the operand is not `void`.
+        ensure_result_used,
+        /// Emits a compile error if an error is ignored.
+        ensure_result_non_error,
         boolnot,
+        /// Obtains a pointer to the return value.
+        ret_ptr,
+        /// Obtains the return type of the in-scope function.
+        ret_type,
+        /// Write a value to a pointer.
+        store,
         /// String Literal. Makes an anonymous Decl and then takes a pointer to it.
         str,
         int,
@@ -73,6 +102,9 @@ pub const Inst = struct {
         @"fn",
         fntype,
         @"export",
+        /// Given a reference to a function and a parameter index, returns the
+        /// type of the parameter. TODO what happens when the parameter is `anytype`?
+        param_type,
         primitive,
         intcast,
         bitcast,
@@ -96,6 +128,9 @@ pub const Inst = struct {
                 .breakpoint,
                 .@"unreachable",
                 .returnvoid,
+                .alloc_inferred,
+                .ret_ptr,
+                .ret_type,
                 => NoOp,
 
                 .boolnot,
@@ -103,6 +138,11 @@ pub const Inst = struct {
                 .@"return",
                 .isnull,
                 .isnonnull,
+                .ptrtoint,
+                .alloc,
+                .ensure_result_used,
+                .ensure_result_non_error,
+                .bitcast_result_ptr,
                 => UnOp,
 
                 .add,
@@ -113,32 +153,36 @@ pub const Inst = struct {
                 .cmp_gte,
                 .cmp_gt,
                 .cmp_neq,
+                .as,
+                .floatcast,
+                .intcast,
+                .bitcast,
+                .coerce_result_ptr,
                 => BinOp,
 
                 .block => Block,
                 .@"break" => Break,
                 .breakvoid => BreakVoid,
                 .call => Call,
+                .coerce_to_ptr_elem => CoerceToPtrElem,
                 .declref => DeclRef,
                 .declref_str => DeclRefStr,
                 .declval => DeclVal,
                 .declval_in_module => DeclValInModule,
+                .coerce_result_block_ptr => CoerceResultBlockPtr,
                 .compileerror => CompileError,
                 .@"const" => Const,
+                .store => Store,
                 .str => Str,
                 .int => Int,
                 .inttype => IntType,
-                .ptrtoint => PtrToInt,
                 .fieldptr => FieldPtr,
-                .as => As,
                 .@"asm" => Asm,
                 .@"fn" => Fn,
                 .@"export" => Export,
+                .param_type => ParamType,
                 .primitive => Primitive,
                 .fntype => FnType,
-                .intcast => IntCast,
-                .bitcast => BitCast,
-                .floatcast => FloatCast,
                 .elemptr => ElemPtr,
                 .condbr => CondBr,
             };
@@ -148,15 +192,26 @@ pub const Inst = struct {
         /// Function calls do not count.
         pub fn isNoReturn(tag: Tag) bool {
             return switch (tag) {
+                .alloc,
+                .alloc_inferred,
                 .arg,
+                .bitcast_result_ptr,
                 .block,
                 .breakpoint,
                 .call,
+                .coerce_result_ptr,
+                .coerce_result_block_ptr,
+                .coerce_to_ptr_elem,
                 .@"const",
                 .declref,
                 .declref_str,
                 .declval,
                 .declval_in_module,
+                .ensure_result_used,
+                .ensure_result_non_error,
+                .ret_ptr,
+                .ret_type,
+                .store,
                 .str,
                 .int,
                 .inttype,
@@ -168,6 +223,7 @@ pub const Inst = struct {
                 .@"fn",
                 .fntype,
                 .@"export",
+                .param_type,
                 .primitive,
                 .intcast,
                 .bitcast,
@@ -292,6 +348,17 @@ pub const Inst = struct {
         },
     };
 
+    pub const CoerceToPtrElem = struct {
+        pub const base_tag = Tag.coerce_to_ptr_elem;
+        base: Inst,
+
+        positionals: struct {
+            ptr: *Inst,
+            value: *Inst,
+        },
+        kw_args: struct {},
+    };
+
     pub const DeclRef = struct {
         pub const base_tag = Tag.declref;
         base: Inst,
@@ -332,6 +399,17 @@ pub const Inst = struct {
         kw_args: struct {},
     };
 
+    pub const CoerceResultBlockPtr = struct {
+        pub const base_tag = Tag.coerce_result_block_ptr;
+        base: Inst,
+
+        positionals: struct {
+            dest_type: *Inst,
+            block: *Block,
+        },
+        kw_args: struct {},
+    };
+
     pub const CompileError = struct {
         pub const base_tag = Tag.compileerror;
         base: Inst,
@@ -352,33 +430,33 @@ pub const Inst = struct {
         kw_args: struct {},
     };
 
-    pub const Str = struct {
-        pub const base_tag = Tag.str;
+    pub const Store = struct {
+        pub const base_tag = Tag.store;
         base: Inst,
 
         positionals: struct {
-            bytes: []const u8,
+            ptr: *Inst,
+            value: *Inst,
         },
         kw_args: struct {},
     };
 
-    pub const Int = struct {
-        pub const base_tag = Tag.int;
+    pub const Str = struct {
+        pub const base_tag = Tag.str;
         base: Inst,
 
         positionals: struct {
-            int: BigIntConst,
+            bytes: []const u8,
         },
         kw_args: struct {},
     };
 
-    pub const PtrToInt = struct {
-        pub const builtin_name = "@ptrToInt";
-        pub const base_tag = Tag.ptrtoint;
+    pub const Int = struct {
+        pub const base_tag = Tag.int;
         base: Inst,
 
         positionals: struct {
-            operand: *Inst,
+            int: BigIntConst,
         },
         kw_args: struct {},
     };
@@ -394,18 +472,6 @@ pub const Inst = struct {
         kw_args: struct {},
     };
 
-    pub const As = struct {
-        pub const base_tag = Tag.as;
-        pub const builtin_name = "@as";
-        base: Inst,
-
-        positionals: struct {
-            dest_type: *Inst,
-            value: *Inst,
-        },
-        kw_args: struct {},
-    };
-
     pub const Asm = struct {
         pub const base_tag = Tag.@"asm";
         base: Inst,
@@ -469,6 +535,17 @@ pub const Inst = struct {
         kw_args: struct {},
     };
 
+    pub const ParamType = struct {
+        pub const base_tag = Tag.param_type;
+        base: Inst,
+
+        positionals: struct {
+            func: *Inst,
+            arg_index: usize,
+        },
+        kw_args: struct {},
+    };
+
     pub const Primitive = struct {
         pub const base_tag = Tag.primitive;
         base: Inst,
@@ -559,42 +636,6 @@ pub const Inst = struct {
         };
     };
 
-    pub const FloatCast = struct {
-        pub const base_tag = Tag.floatcast;
-        pub const builtin_name = "@floatCast";
-        base: Inst,
-
-        positionals: struct {
-            dest_type: *Inst,
-            operand: *Inst,
-        },
-        kw_args: struct {},
-    };
-
-    pub const IntCast = struct {
-        pub const base_tag = Tag.intcast;
-        pub const builtin_name = "@intCast";
-        base: Inst,
-
-        positionals: struct {
-            dest_type: *Inst,
-            operand: *Inst,
-        },
-        kw_args: struct {},
-    };
-
-    pub const BitCast = struct {
-        pub const base_tag = Tag.bitcast;
-        pub const builtin_name = "@bitCast";
-        base: Inst,
-
-        positionals: struct {
-            dest_type: *Inst,
-            operand: *Inst,
-        },
-        kw_args: struct {},
-    };
-
     pub const ElemPtr = struct {
         pub const base_tag = Tag.elemptr;
         base: Inst,
@@ -1467,15 +1508,15 @@ const EmitZIR = struct {
             },
             .ComptimeInt => return self.emitComptimeIntVal(src, typed_value.val),
             .Int => {
-                const as_inst = try self.arena.allocator.create(Inst.As);
+                const as_inst = try self.arena.allocator.create(Inst.BinOp);
                 as_inst.* = .{
                     .base = .{
+                        .tag = .as,
                         .src = src,
-                        .tag = Inst.As.base_tag,
                     },
                     .positionals = .{
-                        .dest_type = (try self.emitType(src, typed_value.ty)).inst,
-                        .value = (try self.emitComptimeIntVal(src, typed_value.val)).inst,
+                        .lhs = (try self.emitType(src, typed_value.ty)).inst,
+                        .rhs = (try self.emitComptimeIntVal(src, typed_value.val)).inst,
                     },
                     .kw_args = .{},
                 };
@@ -1640,17 +1681,17 @@ const EmitZIR = struct {
         src: usize,
         new_body: ZirBody,
         old_inst: *ir.Inst.UnOp,
-        comptime I: type,
+        tag: Inst.Tag,
     ) Allocator.Error!*Inst {
-        const new_inst = try self.arena.allocator.create(I);
+        const new_inst = try self.arena.allocator.create(Inst.BinOp);
         new_inst.* = .{
             .base = .{
                 .src = src,
-                .tag = I.base_tag,
+                .tag = tag,
             },
             .positionals = .{
-                .dest_type = (try self.emitType(src, old_inst.base.ty)).inst,
-                .operand = try self.resolveInst(new_body, old_inst.operand),
+                .lhs = (try self.emitType(old_inst.base.src, old_inst.base.ty)).inst,
+                .rhs = try self.resolveInst(new_body, old_inst.operand),
             },
             .kw_args = .{},
         };
@@ -1691,9 +1732,9 @@ const EmitZIR = struct {
                 .cmp_gt => try self.emitBinOp(inst.src, new_body, inst.castTag(.cmp_gt).?, .cmp_gt),
                 .cmp_neq => try self.emitBinOp(inst.src, new_body, inst.castTag(.cmp_neq).?, .cmp_neq),
 
-                .bitcast => try self.emitCast(inst.src, new_body, inst.castTag(.bitcast).?, Inst.BitCast),
-                .intcast => try self.emitCast(inst.src, new_body, inst.castTag(.intcast).?, Inst.IntCast),
-                .floatcast => try self.emitCast(inst.src, new_body, inst.castTag(.floatcast).?, Inst.FloatCast),
+                .bitcast => try self.emitCast(inst.src, new_body, inst.castTag(.bitcast).?, .bitcast),
+                .intcast => try self.emitCast(inst.src, new_body, inst.castTag(.intcast).?, .intcast),
+                .floatcast => try self.emitCast(inst.src, new_body, inst.castTag(.floatcast).?, .floatcast),
 
                 .block => blk: {
                     const old_inst = inst.castTag(.block).?;
test/stage2/cbe.zig
@@ -19,13 +19,13 @@ pub fn addCases(ctx: *TestContext) !void {
         \\fn main() noreturn {}
         \\
         \\export fn _start() noreturn {
-        \\	main();
+        \\    main();
         \\}
     ,
         \\noreturn void main(void);
         \\
         \\noreturn void _start(void) {
-        \\	main();
+        \\    main();
         \\}
         \\
         \\noreturn void main(void) {}
@@ -35,15 +35,15 @@ pub fn addCases(ctx: *TestContext) !void {
     // TODO: figure out a way to prevent asm constants from being generated
     ctx.c("inline asm", linux_x64,
         \\fn exitGood() void {
-        \\	asm volatile ("syscall"
-        \\ 		:
-        \\		: [number] "{rax}" (231),
-        \\		  [arg1] "{rdi}" (0)
-        \\	);
+        \\    asm volatile ("syscall"
+        \\        :
+        \\        : [number] "{rax}" (231),
+        \\          [arg1] "{rdi}" (0)
+        \\    );
         \\}
         \\
         \\export fn _start() noreturn {
-        \\	exitGood();
+        \\    exitGood();
         \\}
     ,
         \\#include <stddef.h>
@@ -55,36 +55,14 @@ pub fn addCases(ctx: *TestContext) !void {
         \\const char *const exitGood__anon_2 = "syscall";
         \\
         \\noreturn void _start(void) {
-        \\	exitGood();
+        \\    exitGood();
         \\}
         \\
         \\void exitGood(void) {
-        \\	register size_t rax_constant __asm__("rax") = 231;
-        \\	register size_t rdi_constant __asm__("rdi") = 0;
-        \\	__asm volatile ("syscall" :: ""(rax_constant), ""(rdi_constant));
-        \\	return;
-        \\}
-        \\
-    );
-    ctx.c("basic return", linux_x64,
-        \\fn main() u8 {
-        \\	return 103;
-        \\}
-        \\
-        \\export fn _start() noreturn {
-        \\	_ = main();
-        \\}
-    ,
-        \\#include <stdint.h>
-        \\
-        \\uint8_t main(void);
-        \\
-        \\noreturn void _start(void) {
-        \\	(void)main();
-        \\}
-        \\
-        \\uint8_t main(void) {
-        \\	return 103;
+        \\    register size_t rax_constant __asm__("rax") = 231;
+        \\    register size_t rdi_constant __asm__("rdi") = 0;
+        \\    __asm volatile ("syscall" :: ""(rax_constant), ""(rdi_constant));
+        \\    return;
         \\}
         \\
     );