Commit af12596e8d

Andrew Kelley <andrew@ziglang.org>
2020-07-16 04:39:18
stage2: breaking AST memory layout modifications
InfixOp is flattened out so that each operator is an independent AST node tag. The two kinds of structs are now Catch and SimpleInfixOp. Beginning implementation of supporting codegen for const locals.
1 parent f119092
Changed files (6)
lib/std/zig/ast.zig
@@ -408,8 +408,54 @@ pub const Node = struct {
         VarDecl,
         Defer,
 
-        // Operators
-        InfixOp,
+        // Infix operators
+        Catch,
+
+        // SimpleInfixOp
+        Add,
+        AddWrap,
+        ArrayCat,
+        ArrayMult,
+        Assign,
+        AssignBitAnd,
+        AssignBitOr,
+        AssignBitShiftLeft,
+        AssignBitShiftRight,
+        AssignBitXor,
+        AssignDiv,
+        AssignSub,
+        AssignSubWrap,
+        AssignMod,
+        AssignAdd,
+        AssignAddWrap,
+        AssignMul,
+        AssignMulWrap,
+        BangEqual,
+        BitAnd,
+        BitOr,
+        BitShiftLeft,
+        BitShiftRight,
+        BitXor,
+        BoolAnd,
+        BoolOr,
+        Div,
+        EqualEqual,
+        ErrorUnion,
+        GreaterOrEqual,
+        GreaterThan,
+        LessOrEqual,
+        LessThan,
+        MergeErrorSets,
+        Mod,
+        Mul,
+        MulWrap,
+        Period,
+        Range,
+        Sub,
+        SubWrap,
+        UnwrapOptional,
+
+        // SimplePrefixOp
         AddressOf,
         Await,
         BitNot,
@@ -419,6 +465,7 @@ pub const Node = struct {
         NegationWrap,
         Resume,
         Try,
+
         ArrayType,
         /// ArrayType but has a sentinel node.
         ArrayTypeSentinel,
@@ -492,7 +539,51 @@ pub const Node = struct {
                 .TestDecl => TestDecl,
                 .VarDecl => VarDecl,
                 .Defer => Defer,
-                .InfixOp => InfixOp,
+                .Catch => Catch,
+
+                .Add,
+                .AddWrap,
+                .ArrayCat,
+                .ArrayMult,
+                .Assign,
+                .AssignBitAnd,
+                .AssignBitOr,
+                .AssignBitShiftLeft,
+                .AssignBitShiftRight,
+                .AssignBitXor,
+                .AssignDiv,
+                .AssignSub,
+                .AssignSubWrap,
+                .AssignMod,
+                .AssignAdd,
+                .AssignAddWrap,
+                .AssignMul,
+                .AssignMulWrap,
+                .BangEqual,
+                .BitAnd,
+                .BitOr,
+                .BitShiftLeft,
+                .BitShiftRight,
+                .BitXor,
+                .BoolAnd,
+                .BoolOr,
+                .Div,
+                .EqualEqual,
+                .ErrorUnion,
+                .GreaterOrEqual,
+                .GreaterThan,
+                .LessOrEqual,
+                .LessThan,
+                .MergeErrorSets,
+                .Mod,
+                .Mul,
+                .MulWrap,
+                .Period,
+                .Range,
+                .Sub,
+                .SubWrap,
+                .UnwrapOptional,
+                => SimpleInfixOp,
 
                 .AddressOf,
                 .Await,
@@ -507,13 +598,17 @@ pub const Node = struct {
 
                 .ArrayType => ArrayType,
                 .ArrayTypeSentinel => ArrayTypeSentinel,
+
                 .PtrType => PtrType,
                 .SliceType => SliceType,
                 .SuffixOp => SuffixOp,
+
                 .ArrayInitializer => ArrayInitializer,
                 .ArrayInitializerDot => ArrayInitializerDot,
+
                 .StructInitializer => StructInitializer,
                 .StructInitializerDot => StructInitializerDot,
+
                 .Call => Call,
                 .Switch => Switch,
                 .While => While,
@@ -1859,117 +1954,22 @@ pub const Node = struct {
         }
     };
 
-    /// TODO split up and make every op its own AST Node tag
-    pub const InfixOp = struct {
-        base: Node = Node{ .tag = .InfixOp },
+    pub const Catch = struct {
+        base: Node = Node{ .tag = .Catch },
         op_token: TokenIndex,
         lhs: *Node,
-        op: Op,
         rhs: *Node,
+        payload: ?*Node,
 
-        pub const Op = union(enum) {
-            Add,
-            AddWrap,
-            ArrayCat,
-            ArrayMult,
-            Assign,
-            AssignBitAnd,
-            AssignBitOr,
-            AssignBitShiftLeft,
-            AssignBitShiftRight,
-            AssignBitXor,
-            AssignDiv,
-            AssignSub,
-            AssignSubWrap,
-            AssignMod,
-            AssignAdd,
-            AssignAddWrap,
-            AssignMul,
-            AssignMulWrap,
-            BangEqual,
-            BitAnd,
-            BitOr,
-            BitShiftLeft,
-            BitShiftRight,
-            BitXor,
-            BoolAnd,
-            BoolOr,
-            Catch: ?*Node,
-            Div,
-            EqualEqual,
-            ErrorUnion,
-            GreaterOrEqual,
-            GreaterThan,
-            LessOrEqual,
-            LessThan,
-            MergeErrorSets,
-            Mod,
-            Mul,
-            MulWrap,
-            Period,
-            Range,
-            Sub,
-            SubWrap,
-            UnwrapOptional,
-        };
-
-        pub fn iterate(self: *const InfixOp, index: usize) ?*Node {
+        pub fn iterate(self: *const Catch, index: usize) ?*Node {
             var i = index;
 
             if (i < 1) return self.lhs;
             i -= 1;
 
-            switch (self.op) {
-                .Catch => |maybe_payload| {
-                    if (maybe_payload) |payload| {
-                        if (i < 1) return payload;
-                        i -= 1;
-                    }
-                },
-
-                .Add,
-                .AddWrap,
-                .ArrayCat,
-                .ArrayMult,
-                .Assign,
-                .AssignBitAnd,
-                .AssignBitOr,
-                .AssignBitShiftLeft,
-                .AssignBitShiftRight,
-                .AssignBitXor,
-                .AssignDiv,
-                .AssignSub,
-                .AssignSubWrap,
-                .AssignMod,
-                .AssignAdd,
-                .AssignAddWrap,
-                .AssignMul,
-                .AssignMulWrap,
-                .BangEqual,
-                .BitAnd,
-                .BitOr,
-                .BitShiftLeft,
-                .BitShiftRight,
-                .BitXor,
-                .BoolAnd,
-                .BoolOr,
-                .Div,
-                .EqualEqual,
-                .ErrorUnion,
-                .GreaterOrEqual,
-                .GreaterThan,
-                .LessOrEqual,
-                .LessThan,
-                .MergeErrorSets,
-                .Mod,
-                .Mul,
-                .MulWrap,
-                .Period,
-                .Range,
-                .Sub,
-                .SubWrap,
-                .UnwrapOptional,
-                => {},
+            if (self.payload) |payload| {
+                if (i < 1) return payload;
+                i -= 1;
             }
 
             if (i < 1) return self.rhs;
@@ -1978,11 +1978,38 @@ pub const Node = struct {
             return null;
         }
 
-        pub fn firstToken(self: *const InfixOp) TokenIndex {
+        pub fn firstToken(self: *const Catch) TokenIndex {
+            return self.lhs.firstToken();
+        }
+
+        pub fn lastToken(self: *const Catch) TokenIndex {
+            return self.rhs.lastToken();
+        }
+    };
+
+    pub const SimpleInfixOp = struct {
+        base: Node,
+        op_token: TokenIndex,
+        lhs: *Node,
+        rhs: *Node,
+
+        pub fn iterate(self: *const SimpleInfixOp, index: usize) ?*Node {
+            var i = index;
+
+            if (i < 1) return self.lhs;
+            i -= 1;
+
+            if (i < 1) return self.rhs;
+            i -= 1;
+
+            return null;
+        }
+
+        pub fn firstToken(self: *const SimpleInfixOp) TokenIndex {
             return self.lhs.firstToken();
         }
 
-        pub fn lastToken(self: *const InfixOp) TokenIndex {
+        pub fn lastToken(self: *const SimpleInfixOp) TokenIndex {
             return self.rhs.lastToken();
         }
     };
lib/std/zig/parse.zig
@@ -1015,7 +1015,7 @@ const Parser = struct {
     /// BoolOrExpr <- BoolAndExpr (KEYWORD_or BoolAndExpr)*
     fn parseBoolOrExpr(p: *Parser) !?*Node {
         return p.parseBinOpExpr(
-            SimpleBinOpParseFn(.Keyword_or, Node.InfixOp.Op.BoolOr),
+            SimpleBinOpParseFn(.Keyword_or, .BoolOr),
             parseBoolAndExpr,
             .Infinitely,
         );
@@ -1405,8 +1405,8 @@ const Parser = struct {
     fn parseErrorUnionExpr(p: *Parser) !?*Node {
         const suffix_expr = (try p.parseSuffixExpr()) orelse return null;
 
-        if (try SimpleBinOpParseFn(.Bang, Node.InfixOp.Op.ErrorUnion)(p)) |node| {
-            const error_union = node.cast(Node.InfixOp).?;
+        if (try SimpleBinOpParseFn(.Bang, .ErrorUnion)(p)) |node| {
+            const error_union = node.castTag(.ErrorUnion).?;
             const type_expr = try p.expectNode(parseTypeExpr, .{
                 .ExpectedTypeExpr = .{ .token = p.tok_i },
             });
@@ -1439,10 +1439,56 @@ const Parser = struct {
                 .ExpectedPrimaryTypeExpr = .{ .token = p.tok_i },
             });
 
+            // TODO pass `res` into `parseSuffixOp` rather than patching it up afterwards.
             while (try p.parseSuffixOp()) |node| {
                 switch (node.tag) {
                     .SuffixOp => node.cast(Node.SuffixOp).?.lhs = res,
-                    .InfixOp => node.cast(Node.InfixOp).?.lhs = res,
+                    .Catch => node.castTag(.Catch).?.lhs = res,
+
+                    .Add,
+                    .AddWrap,
+                    .ArrayCat,
+                    .ArrayMult,
+                    .Assign,
+                    .AssignBitAnd,
+                    .AssignBitOr,
+                    .AssignBitShiftLeft,
+                    .AssignBitShiftRight,
+                    .AssignBitXor,
+                    .AssignDiv,
+                    .AssignSub,
+                    .AssignSubWrap,
+                    .AssignMod,
+                    .AssignAdd,
+                    .AssignAddWrap,
+                    .AssignMul,
+                    .AssignMulWrap,
+                    .BangEqual,
+                    .BitAnd,
+                    .BitOr,
+                    .BitShiftLeft,
+                    .BitShiftRight,
+                    .BitXor,
+                    .BoolAnd,
+                    .BoolOr,
+                    .Div,
+                    .EqualEqual,
+                    .ErrorUnion,
+                    .GreaterOrEqual,
+                    .GreaterThan,
+                    .LessOrEqual,
+                    .LessThan,
+                    .MergeErrorSets,
+                    .Mod,
+                    .Mul,
+                    .MulWrap,
+                    .Period,
+                    .Range,
+                    .Sub,
+                    .SubWrap,
+                    .UnwrapOptional,
+                    => node.cast(Node.SimpleInfixOp).?.lhs = res,
+
                     else => unreachable,
                 }
                 res = node;
@@ -1470,10 +1516,55 @@ const Parser = struct {
             var res = expr;
 
             while (true) {
+                // TODO pass `res` into `parseSuffixOp` rather than patching it up afterwards.
                 if (try p.parseSuffixOp()) |node| {
                     switch (node.tag) {
                         .SuffixOp => node.cast(Node.SuffixOp).?.lhs = res,
-                        .InfixOp => node.cast(Node.InfixOp).?.lhs = res,
+                        .Catch => node.castTag(.Catch).?.lhs = res,
+
+                        .Add,
+                        .AddWrap,
+                        .ArrayCat,
+                        .ArrayMult,
+                        .Assign,
+                        .AssignBitAnd,
+                        .AssignBitOr,
+                        .AssignBitShiftLeft,
+                        .AssignBitShiftRight,
+                        .AssignBitXor,
+                        .AssignDiv,
+                        .AssignSub,
+                        .AssignSubWrap,
+                        .AssignMod,
+                        .AssignAdd,
+                        .AssignAddWrap,
+                        .AssignMul,
+                        .AssignMulWrap,
+                        .BangEqual,
+                        .BitAnd,
+                        .BitOr,
+                        .BitShiftLeft,
+                        .BitShiftRight,
+                        .BitXor,
+                        .BoolAnd,
+                        .BoolOr,
+                        .Div,
+                        .EqualEqual,
+                        .ErrorUnion,
+                        .GreaterOrEqual,
+                        .GreaterThan,
+                        .LessOrEqual,
+                        .LessThan,
+                        .MergeErrorSets,
+                        .Mod,
+                        .Mul,
+                        .MulWrap,
+                        .Period,
+                        .Range,
+                        .Sub,
+                        .SubWrap,
+                        .UnwrapOptional,
+                        => node.cast(Node.SimpleInfixOp).?.lhs = res,
                         else => unreachable,
                     }
                     res = node;
@@ -1560,11 +1651,11 @@ const Parser = struct {
             const global_error_set = try p.createLiteral(Node.ErrorType, token);
             if (period == null or identifier == null) return global_error_set;
 
-            const node = try p.arena.allocator.create(Node.InfixOp);
+            const node = try p.arena.allocator.create(Node.SimpleInfixOp);
             node.* = .{
+                .base = Node{ .tag = .Period },
                 .op_token = period.?,
                 .lhs = global_error_set,
-                .op = .Period,
                 .rhs = identifier.?,
             };
             return &node.base;
@@ -2237,11 +2328,11 @@ const Parser = struct {
                 .ExpectedExpr = .{ .token = p.tok_i },
             });
 
-            const node = try p.arena.allocator.create(Node.InfixOp);
+            const node = try p.arena.allocator.create(Node.SimpleInfixOp);
             node.* = .{
+                .base = Node{ .tag = .Range },
                 .op_token = token,
                 .lhs = expr,
-                .op = .Range,
                 .rhs = range_end,
             };
             return &node.base;
@@ -2266,7 +2357,7 @@ const Parser = struct {
     ///      / EQUAL
     fn parseAssignOp(p: *Parser) !?*Node {
         const token = p.nextToken();
-        const op: Node.InfixOp.Op = switch (p.token_ids[token]) {
+        const op: Node.Tag = switch (p.token_ids[token]) {
             .AsteriskEqual => .AssignMul,
             .SlashEqual => .AssignDiv,
             .PercentEqual => .AssignMod,
@@ -2287,11 +2378,11 @@ const Parser = struct {
             },
         };
 
-        const node = try p.arena.allocator.create(Node.InfixOp);
+        const node = try p.arena.allocator.create(Node.SimpleInfixOp);
         node.* = .{
+            .base = .{ .tag = op },
             .op_token = token,
             .lhs = undefined, // set by caller
-            .op = op,
             .rhs = undefined, // set by caller
         };
         return &node.base;
@@ -2306,7 +2397,7 @@ const Parser = struct {
     ///      / RARROWEQUAL
     fn parseCompareOp(p: *Parser) !?*Node {
         const token = p.nextToken();
-        const op: Node.InfixOp.Op = switch (p.token_ids[token]) {
+        const op: Node.Tag = switch (p.token_ids[token]) {
             .EqualEqual => .EqualEqual,
             .BangEqual => .BangEqual,
             .AngleBracketLeft => .LessThan,
@@ -2330,12 +2421,22 @@ const Parser = struct {
     ///      / KEYWORD_catch Payload?
     fn parseBitwiseOp(p: *Parser) !?*Node {
         const token = p.nextToken();
-        const op: Node.InfixOp.Op = switch (p.token_ids[token]) {
+        const op: Node.Tag = switch (p.token_ids[token]) {
             .Ampersand => .BitAnd,
             .Caret => .BitXor,
             .Pipe => .BitOr,
             .Keyword_orelse => .UnwrapOptional,
-            .Keyword_catch => .{ .Catch = try p.parsePayload() },
+            .Keyword_catch => {
+                const payload = try p.parsePayload();
+                const node = try p.arena.allocator.create(Node.Catch);
+                node.* = .{
+                    .op_token = token,
+                    .lhs = undefined, // set by caller
+                    .rhs = undefined, // set by caller
+                    .payload = payload,
+                };
+                return &node.base;
+            },
             else => {
                 p.putBackToken(token);
                 return null;
@@ -2350,7 +2451,7 @@ const Parser = struct {
     ///      / RARROW2
     fn parseBitShiftOp(p: *Parser) !?*Node {
         const token = p.nextToken();
-        const op: Node.InfixOp.Op = switch (p.token_ids[token]) {
+        const op: Node.Tag = switch (p.token_ids[token]) {
             .AngleBracketAngleBracketLeft => .BitShiftLeft,
             .AngleBracketAngleBracketRight => .BitShiftRight,
             else => {
@@ -2370,7 +2471,7 @@ const Parser = struct {
     ///      / MINUSPERCENT
     fn parseAdditionOp(p: *Parser) !?*Node {
         const token = p.nextToken();
-        const op: Node.InfixOp.Op = switch (p.token_ids[token]) {
+        const op: Node.Tag = switch (p.token_ids[token]) {
             .Plus => .Add,
             .Minus => .Sub,
             .PlusPlus => .ArrayCat,
@@ -2394,7 +2495,7 @@ const Parser = struct {
     ///      / ASTERISKPERCENT
     fn parseMultiplyOp(p: *Parser) !?*Node {
         const token = p.nextToken();
-        const op: Node.InfixOp.Op = switch (p.token_ids[token]) {
+        const op: Node.Tag = switch (p.token_ids[token]) {
             .PipePipe => .MergeErrorSets,
             .Asterisk => .Mul,
             .Slash => .Div,
@@ -2673,14 +2774,14 @@ const Parser = struct {
 
             if (p.eatToken(.Period)) |period| {
                 if (try p.parseIdentifier()) |identifier| {
-                    // TODO: It's a bit weird to return an InfixOp from the SuffixOp parser.
+                    // TODO: It's a bit weird to return a SimpleInfixOp from the SuffixOp parser.
                     // Should there be an Node.SuffixOp.FieldAccess variant? Or should
                     // this grammar rule be altered?
-                    const node = try p.arena.allocator.create(Node.InfixOp);
+                    const node = try p.arena.allocator.create(Node.SimpleInfixOp);
                     node.* = .{
+                        .base = Node{ .tag = .Period },
                         .op_token = period,
                         .lhs = undefined, // set by caller
-                        .op = .Period,
                         .rhs = identifier,
                     };
                     return &node.base;
@@ -2987,7 +3088,7 @@ const Parser = struct {
         }.parse;
     }
 
-    fn SimpleBinOpParseFn(comptime token: Token.Id, comptime op: Node.InfixOp.Op) NodeParseFn {
+    fn SimpleBinOpParseFn(comptime token: Token.Id, comptime op: Node.Tag) NodeParseFn {
         return struct {
             pub fn parse(p: *Parser) Error!?*Node {
                 const op_token = if (token == .Keyword_and) switch (p.token_ids[p.tok_i]) {
@@ -3001,11 +3102,11 @@ const Parser = struct {
                     else => return null,
                 } else p.eatToken(token) orelse return null;
 
-                const node = try p.arena.allocator.create(Node.InfixOp);
+                const node = try p.arena.allocator.create(Node.SimpleInfixOp);
                 node.* = .{
+                    .base = .{ .tag = op },
                     .op_token = op_token,
                     .lhs = undefined, // set by caller
-                    .op = op,
                     .rhs = undefined, // set by caller
                 };
                 return &node.base;
@@ -3350,9 +3451,13 @@ const Parser = struct {
             const left = res;
             res = node;
 
-            const op = node.cast(Node.InfixOp).?;
-            op.*.lhs = left;
-            op.*.rhs = right;
+            if (node.castTag(.Catch)) |op| {
+                op.lhs = left;
+                op.rhs = right;
+            } else if (node.cast(Node.SimpleInfixOp)) |op| {
+                op.lhs = left;
+                op.rhs = right;
+            }
 
             switch (chain) {
                 .Once => break,
@@ -3363,12 +3468,12 @@ const Parser = struct {
         return res;
     }
 
-    fn createInfixOp(p: *Parser, index: TokenIndex, op: Node.InfixOp.Op) !*Node {
-        const node = try p.arena.allocator.create(Node.InfixOp);
+    fn createInfixOp(p: *Parser, op_token: TokenIndex, tag: Node.Tag) !*Node {
+        const node = try p.arena.allocator.create(Node.SimpleInfixOp);
         node.* = .{
-            .op_token = index,
+            .base = Node{ .tag = tag },
+            .op_token = op_token,
             .lhs = undefined, // set by caller
-            .op = op,
             .rhs = undefined, // set by caller
         };
         return &node.base;
lib/std/zig/render.zig
@@ -436,13 +436,10 @@ fn renderExpression(
             }
         },
 
-        .InfixOp => {
-            const infix_op_node = @fieldParentPtr(ast.Node.InfixOp, "base", base);
+        .Catch => {
+            const infix_op_node = @fieldParentPtr(ast.Node.Catch, "base", base);
 
-            const op_space = switch (infix_op_node.op) {
-                ast.Node.InfixOp.Op.Period, ast.Node.InfixOp.Op.ErrorUnion, ast.Node.InfixOp.Op.Range => Space.None,
-                else => Space.Space,
-            };
+            const op_space = Space.Space;
             try renderExpression(allocator, stream, tree, indent, start_col, infix_op_node.lhs, op_space);
 
             const after_op_space = blk: {
@@ -458,11 +455,75 @@ fn renderExpression(
                 start_col.* = indent + indent_delta;
             }
 
-            switch (infix_op_node.op) {
-                ast.Node.InfixOp.Op.Catch => |maybe_payload| if (maybe_payload) |payload| {
-                    try renderExpression(allocator, stream, tree, indent, start_col, payload, Space.Space);
-                },
-                else => {},
+            if (infix_op_node.payload) |payload| {
+                try renderExpression(allocator, stream, tree, indent, start_col, payload, Space.Space);
+            }
+
+            return renderExpression(allocator, stream, tree, indent, start_col, infix_op_node.rhs, space);
+        },
+
+        .Add,
+        .AddWrap,
+        .ArrayCat,
+        .ArrayMult,
+        .Assign,
+        .AssignBitAnd,
+        .AssignBitOr,
+        .AssignBitShiftLeft,
+        .AssignBitShiftRight,
+        .AssignBitXor,
+        .AssignDiv,
+        .AssignSub,
+        .AssignSubWrap,
+        .AssignMod,
+        .AssignAdd,
+        .AssignAddWrap,
+        .AssignMul,
+        .AssignMulWrap,
+        .BangEqual,
+        .BitAnd,
+        .BitOr,
+        .BitShiftLeft,
+        .BitShiftRight,
+        .BitXor,
+        .BoolAnd,
+        .BoolOr,
+        .Div,
+        .EqualEqual,
+        .ErrorUnion,
+        .GreaterOrEqual,
+        .GreaterThan,
+        .LessOrEqual,
+        .LessThan,
+        .MergeErrorSets,
+        .Mod,
+        .Mul,
+        .MulWrap,
+        .Period,
+        .Range,
+        .Sub,
+        .SubWrap,
+        .UnwrapOptional,
+        => {
+            const infix_op_node = @fieldParentPtr(ast.Node.SimpleInfixOp, "base", base);
+
+            const op_space = switch (base.tag) {
+                .Period, .ErrorUnion, .Range => Space.None,
+                else => Space.Space,
+            };
+            try renderExpression(allocator, stream, tree, indent, start_col, infix_op_node.lhs, op_space);
+
+            const after_op_space = blk: {
+                const loc = tree.tokenLocation(tree.token_locs[infix_op_node.op_token].end, tree.nextToken(infix_op_node.op_token));
+                break :blk if (loc.line == 0) op_space else Space.Newline;
+            };
+
+            try renderToken(tree, stream, infix_op_node.op_token, indent, start_col, after_op_space);
+            if (after_op_space == Space.Newline and
+                tree.token_ids[tree.nextToken(infix_op_node.op_token)] != .MultilineStringLiteralLine)
+            {
+                try stream.writeByteNTimes(' ', indent + indent_delta);
+                start_col.* = indent + indent_delta;
             }
 
             return renderExpression(allocator, stream, tree, indent, start_col, infix_op_node.rhs, space);
@@ -2553,10 +2614,52 @@ fn nodeIsBlock(base: *const ast.Node) bool {
 }
 
 fn nodeCausesSliceOpSpace(base: *ast.Node) bool {
-    const infix_op = base.cast(ast.Node.InfixOp) orelse return false;
-    return switch (infix_op.op) {
-        ast.Node.InfixOp.Op.Period => false,
-        else => true,
+    return switch (base.tag) {
+        .Catch,
+        .Add,
+        .AddWrap,
+        .ArrayCat,
+        .ArrayMult,
+        .Assign,
+        .AssignBitAnd,
+        .AssignBitOr,
+        .AssignBitShiftLeft,
+        .AssignBitShiftRight,
+        .AssignBitXor,
+        .AssignDiv,
+        .AssignSub,
+        .AssignSubWrap,
+        .AssignMod,
+        .AssignAdd,
+        .AssignAddWrap,
+        .AssignMul,
+        .AssignMulWrap,
+        .BangEqual,
+        .BitAnd,
+        .BitOr,
+        .BitShiftLeft,
+        .BitShiftRight,
+        .BitXor,
+        .BoolAnd,
+        .BoolOr,
+        .Div,
+        .EqualEqual,
+        .ErrorUnion,
+        .GreaterOrEqual,
+        .GreaterThan,
+        .LessOrEqual,
+        .LessThan,
+        .MergeErrorSets,
+        .Mod,
+        .Mul,
+        .MulWrap,
+        .Range,
+        .Sub,
+        .SubWrap,
+        .UnwrapOptional,
+        => true,
+
+        else => false,
     };
 }
 
src-self-hosted/astgen.zig
@@ -12,22 +12,29 @@ const Scope = Module.Scope;
 const InnerError = Module.InnerError;
 
 /// Turn Zig AST into untyped ZIR istructions.
-pub fn expr(mod: *Module, scope: *Scope, ast_node: *ast.Node) InnerError!*zir.Inst {
-    switch (ast_node.tag) {
+pub fn expr(mod: *Module, scope: *Scope, node: *ast.Node) InnerError!*zir.Inst {
+    switch (node.tag) {
         .VarDecl => unreachable, // Handled in `blockExpr`.
 
-        .Identifier => return identifier(mod, scope, @fieldParentPtr(ast.Node.Identifier, "base", ast_node)),
-        .Asm => return assembly(mod, scope, @fieldParentPtr(ast.Node.Asm, "base", ast_node)),
-        .StringLiteral => return stringLiteral(mod, scope, @fieldParentPtr(ast.Node.StringLiteral, "base", ast_node)),
-        .IntegerLiteral => return integerLiteral(mod, scope, @fieldParentPtr(ast.Node.IntegerLiteral, "base", ast_node)),
-        .BuiltinCall => return builtinCall(mod, scope, @fieldParentPtr(ast.Node.BuiltinCall, "base", ast_node)),
-        .Call => return callExpr(mod, scope, @fieldParentPtr(ast.Node.Call, "base", ast_node)),
-        .Unreachable => return unreach(mod, scope, @fieldParentPtr(ast.Node.Unreachable, "base", ast_node)),
-        .ControlFlowExpression => return controlFlowExpr(mod, scope, @fieldParentPtr(ast.Node.ControlFlowExpression, "base", ast_node)),
-        .If => return ifExpr(mod, scope, @fieldParentPtr(ast.Node.If, "base", ast_node)),
-        .InfixOp => return infixOp(mod, scope, @fieldParentPtr(ast.Node.InfixOp, "base", ast_node)),
-        .BoolNot => return boolNot(mod, scope, @fieldParentPtr(ast.Node.SimplePrefixOp, "base", ast_node)),
-        else => return mod.failNode(scope, ast_node, "TODO implement astgen.Expr for {}", .{@tagName(ast_node.tag)}),
+        .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).?),
+        .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).?),
+        .Add => return add(mod, scope, node.castTag(.Add).?),
+        .BangEqual => return cmp(mod, scope, node.castTag(.BangEqual).?, .neq),
+        .EqualEqual => return cmp(mod, scope, node.castTag(.EqualEqual).?, .eq),
+        .GreaterThan => return cmp(mod, scope, node.castTag(.GreaterThan).?, .gt),
+        .GreaterOrEqual => return cmp(mod, scope, node.castTag(.GreaterOrEqual).?, .gte),
+        .LessThan => return cmp(mod, scope, node.castTag(.LessThan).?, .lt),
+        .LessOrEqual => return cmp(mod, scope, node.castTag(.LessOrEqual).?, .lte),
+        .BoolNot => return boolNot(mod, scope, node.castTag(.BoolNot).?),
+        else => return mod.failNode(scope, node, "TODO implement astgen.Expr for {}", .{@tagName(node.tag)}),
     }
 }
 
@@ -57,6 +64,7 @@ pub fn blockExpr(mod: *Module, parent_scope: *Scope, block_node: *ast.Node.Block
 }
 
 fn varDecl(mod: *Module, scope: *Scope, node: *ast.Node.VarDecl) InnerError!Scope.LocalVar {
+    // TODO implement detection of shadowing
     if (node.getTrailer("comptime_token")) |comptime_token| {
         return mod.failTok(scope, comptime_token, "TODO implement comptime locals", .{});
     }
@@ -98,64 +106,48 @@ fn boolNot(mod: *Module, scope: *Scope, node: *ast.Node.SimplePrefixOp) InnerErr
     return mod.addZIRInst(scope, src, zir.Inst.BoolNot, .{ .operand = operand }, .{});
 }
 
-fn infixOp(mod: *Module, scope: *Scope, infix_node: *ast.Node.InfixOp) InnerError!*zir.Inst {
-    switch (infix_node.op) {
-        .Assign => {
-            if (infix_node.lhs.tag == .Identifier) {
-                const ident = @fieldParentPtr(ast.Node.Identifier, "base", infix_node.lhs);
-                const tree = scope.tree();
-                const ident_name = tree.tokenSlice(ident.token);
-                if (std.mem.eql(u8, ident_name, "_")) {
-                    return expr(mod, scope, infix_node.rhs);
-                } else {
-                    return mod.failNode(scope, &infix_node.base, "TODO implement infix operator assign", .{});
-                }
-            } else {
-                return mod.failNode(scope, &infix_node.base, "TODO implement infix operator assign", .{});
-            }
-        },
-        .Add => {
-            const lhs = try expr(mod, scope, infix_node.lhs);
-            const rhs = try expr(mod, scope, infix_node.rhs);
+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);
+        const tree = scope.tree();
+        const ident_name = tree.tokenSlice(ident.token);
+        if (std.mem.eql(u8, ident_name, "_")) {
+            return expr(mod, scope, infix_node.rhs);
+        } else {
+            return mod.failNode(scope, &infix_node.base, "TODO implement infix operator assign", .{});
+        }
+    } else {
+        return mod.failNode(scope, &infix_node.base, "TODO implement infix operator assign", .{});
+    }
+}
 
-            const tree = scope.tree();
-            const src = tree.token_locs[infix_node.op_token].start;
+fn add(mod: *Module, scope: *Scope, infix_node: *ast.Node.SimpleInfixOp) InnerError!*zir.Inst {
+    const lhs = try expr(mod, scope, infix_node.lhs);
+    const rhs = try expr(mod, scope, infix_node.rhs);
 
-            return mod.addZIRInst(scope, src, zir.Inst.Add, .{ .lhs = lhs, .rhs = rhs }, .{});
-        },
-        .BangEqual,
-        .EqualEqual,
-        .GreaterThan,
-        .GreaterOrEqual,
-        .LessThan,
-        .LessOrEqual,
-        => {
-            const lhs = try expr(mod, scope, infix_node.lhs);
-            const rhs = try expr(mod, scope, infix_node.rhs);
-
-            const tree = scope.tree();
-            const src = tree.token_locs[infix_node.op_token].start;
-
-            const op: std.math.CompareOperator = switch (infix_node.op) {
-                .BangEqual => .neq,
-                .EqualEqual => .eq,
-                .GreaterThan => .gt,
-                .GreaterOrEqual => .gte,
-                .LessThan => .lt,
-                .LessOrEqual => .lte,
-                else => unreachable,
-            };
+    const tree = scope.tree();
+    const src = tree.token_locs[infix_node.op_token].start;
 
-            return mod.addZIRInst(scope, src, zir.Inst.Cmp, .{
-                .lhs = lhs,
-                .op = op,
-                .rhs = rhs,
-            }, .{});
-        },
-        else => |op| {
-            return mod.failNode(scope, &infix_node.base, "TODO implement infix operator {}", .{op});
-        },
-    }
+    return mod.addZIRInst(scope, src, zir.Inst.Add, .{ .lhs = lhs, .rhs = rhs }, .{});
+}
+
+fn cmp(
+    mod: *Module,
+    scope: *Scope,
+    infix_node: *ast.Node.SimpleInfixOp,
+    op: std.math.CompareOperator,
+) InnerError!*zir.Inst {
+    const lhs = try expr(mod, scope, infix_node.lhs);
+    const rhs = try expr(mod, scope, infix_node.rhs);
+
+    const tree = scope.tree();
+    const src = tree.token_locs[infix_node.op_token].start;
+
+    return mod.addZIRInst(scope, src, zir.Inst.Cmp, .{
+        .lhs = lhs,
+        .op = op,
+        .rhs = rhs,
+    }, .{});
 }
 
 fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.Inst {
@@ -571,6 +563,47 @@ fn nodeNeedsMemoryLocation(node: *ast.Node) bool {
         .ErrorSetDecl,
         .ContainerDecl,
         .Asm,
+        .Add,
+        .AddWrap,
+        .ArrayCat,
+        .ArrayMult,
+        .Assign,
+        .AssignBitAnd,
+        .AssignBitOr,
+        .AssignBitShiftLeft,
+        .AssignBitShiftRight,
+        .AssignBitXor,
+        .AssignDiv,
+        .AssignSub,
+        .AssignSubWrap,
+        .AssignMod,
+        .AssignAdd,
+        .AssignAddWrap,
+        .AssignMul,
+        .AssignMulWrap,
+        .BangEqual,
+        .BitAnd,
+        .BitOr,
+        .BitShiftLeft,
+        .BitShiftRight,
+        .BitXor,
+        .BoolAnd,
+        .BoolOr,
+        .Div,
+        .EqualEqual,
+        .ErrorUnion,
+        .GreaterOrEqual,
+        .GreaterThan,
+        .LessOrEqual,
+        .LessThan,
+        .MergeErrorSets,
+        .Mod,
+        .Mul,
+        .MulWrap,
+        .Range,
+        .Period,
+        .Sub,
+        .SubWrap,
         => false,
 
         .ArrayInitializer,
@@ -579,9 +612,10 @@ fn nodeNeedsMemoryLocation(node: *ast.Node) bool {
         .StructInitializerDot,
         => true,
 
-        .GroupedExpression => nodeNeedsMemoryLocation(node.cast(ast.Node.GroupedExpression).?.expr),
+        .GroupedExpression => nodeNeedsMemoryLocation(node.castTag(.GroupedExpression).?.expr),
 
-        .InfixOp => @panic("TODO nodeNeedsMemoryLocation for InfixOp"),
+        .UnwrapOptional => @panic("TODO nodeNeedsMemoryLocation for UnwrapOptional"),
+        .Catch => @panic("TODO nodeNeedsMemoryLocation for Catch"),
         .Await => @panic("TODO nodeNeedsMemoryLocation for Await"),
         .Try => @panic("TODO nodeNeedsMemoryLocation for Try"),
         .If => @panic("TODO nodeNeedsMemoryLocation for If"),
src-self-hosted/Module.zig
@@ -1954,7 +1954,7 @@ pub fn addZIRInstSpecial(
     positionals: std.meta.fieldInfo(T, "positionals").field_type,
     kw_args: std.meta.fieldInfo(T, "kw_args").field_type,
 ) !*T {
-    const gen_zir = scope.cast(Scope.GenZIR).?;
+    const gen_zir = scope.getGenZIR();
     try gen_zir.instructions.ensureCapacity(self.gpa, gen_zir.instructions.items.len + 1);
     const inst = try newZIRInst(gen_zir.arena, src, T, positionals, kw_args);
     gen_zir.instructions.appendAssumeCapacity(&inst.base);
src-self-hosted/translate_c.zig
@@ -1103,11 +1103,11 @@ fn transEnumDecl(c: *Context, enum_decl: *const ZigClangEnumDecl) Error!?*ast.No
             const enum_ident = try transCreateNodeIdentifier(c, name);
             const period_tok = try appendToken(c, .Period, ".");
             const field_ident = try transCreateNodeIdentifier(c, field_name);
-            const field_access_node = try c.arena.create(ast.Node.InfixOp);
+            const field_access_node = try c.arena.create(ast.Node.SimpleInfixOp);
             field_access_node.* = .{
+                .base = .{ .tag = .Period },
                 .op_token = period_tok,
                 .lhs = enum_ident,
-                .op = .Period,
                 .rhs = field_ident,
             };
             cast_node.params()[0] = &field_access_node.base;
@@ -1294,7 +1294,7 @@ fn transBinaryOperator(
     const op = ZigClangBinaryOperator_getOpcode(stmt);
     const qt = ZigClangBinaryOperator_getType(stmt);
     var op_token: ast.TokenIndex = undefined;
-    var op_id: ast.Node.InfixOp.Op = undefined;
+    var op_id: ast.Node.Tag = undefined;
     switch (op) {
         .Assign => return try transCreateNodeAssign(rp, scope, result_used, ZigClangBinaryOperator_getLHS(stmt), ZigClangBinaryOperator_getRHS(stmt)),
         .Comma => {
@@ -1737,25 +1737,22 @@ fn exprIsStringLiteral(expr: *const ZigClangExpr) bool {
 
 fn isBoolRes(res: *ast.Node) bool {
     switch (res.tag) {
-        .InfixOp => switch (@fieldParentPtr(ast.Node.InfixOp, "base", res).op) {
-            .BoolOr,
-            .BoolAnd,
-            .EqualEqual,
-            .BangEqual,
-            .LessThan,
-            .GreaterThan,
-            .LessOrEqual,
-            .GreaterOrEqual,
-            => return true,
+        .BoolOr,
+        .BoolAnd,
+        .EqualEqual,
+        .BangEqual,
+        .LessThan,
+        .GreaterThan,
+        .LessOrEqual,
+        .GreaterOrEqual,
+        .BoolNot,
+        .BoolLiteral,
+        => return true,
 
-            else => {},
-        },
-        .BoolNot => return true,
-        .BoolLiteral => return true,
         .GroupedExpression => return isBoolRes(@fieldParentPtr(ast.Node.GroupedExpression, "base", res).expr),
-        else => {},
+
+        else => return false,
     }
-    return false;
 }
 
 fn finishBoolExpr(
@@ -2312,11 +2309,11 @@ fn transInitListExprArray(
         &filler_init_node.base
     else blk: {
         const mul_tok = try appendToken(rp.c, .AsteriskAsterisk, "**");
-        const mul_node = try rp.c.arena.create(ast.Node.InfixOp);
+        const mul_node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
         mul_node.* = .{
+            .base = .{ .tag = .ArrayMult },
             .op_token = mul_tok,
             .lhs = &filler_init_node.base,
-            .op = .ArrayMult,
             .rhs = try transCreateNodeInt(rp.c, leftover_count),
         };
         break :blk &mul_node.base;
@@ -2326,11 +2323,11 @@ fn transInitListExprArray(
         return rhs_node;
     }
 
-    const cat_node = try rp.c.arena.create(ast.Node.InfixOp);
+    const cat_node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
     cat_node.* = .{
+        .base = .{ .tag = .ArrayCat },
         .op_token = cat_tok,
         .lhs = &init_node.base,
-        .op = .ArrayCat,
         .rhs = rhs_node,
     };
     return &cat_node.base;
@@ -2723,11 +2720,11 @@ fn transCase(
         const ellips = try appendToken(rp.c, .Ellipsis3, "...");
         const rhs_node = try transExpr(rp, scope, rhs, .used, .r_value);
 
-        const node = try rp.c.arena.create(ast.Node.InfixOp);
+        const node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
         node.* = .{
+            .base = .{ .tag = .Range },
             .op_token = ellips,
             .lhs = lhs_node,
-            .op = .Range,
             .rhs = rhs_node,
         };
         break :blk &node.base;
@@ -3153,7 +3150,7 @@ fn transCreatePreCrement(
     rp: RestorePoint,
     scope: *Scope,
     stmt: *const ZigClangUnaryOperator,
-    op: ast.Node.InfixOp.Op,
+    op: ast.Node.Tag,
     op_tok_id: std.zig.Token.Id,
     bytes: []const u8,
     used: ResultUsed,
@@ -3227,7 +3224,7 @@ fn transCreatePostCrement(
     rp: RestorePoint,
     scope: *Scope,
     stmt: *const ZigClangUnaryOperator,
-    op: ast.Node.InfixOp.Op,
+    op: ast.Node.Tag,
     op_tok_id: std.zig.Token.Id,
     bytes: []const u8,
     used: ResultUsed,
@@ -3349,10 +3346,10 @@ fn transCreateCompoundAssign(
     rp: RestorePoint,
     scope: *Scope,
     stmt: *const ZigClangCompoundAssignOperator,
-    assign_op: ast.Node.InfixOp.Op,
+    assign_op: ast.Node.Tag,
     assign_tok_id: std.zig.Token.Id,
     assign_bytes: []const u8,
-    bin_op: ast.Node.InfixOp.Op,
+    bin_op: ast.Node.Tag,
     bin_tok_id: std.zig.Token.Id,
     bin_bytes: []const u8,
     used: ResultUsed,
@@ -3377,7 +3374,7 @@ fn transCreateCompoundAssign(
         // zig: lhs += rhs
         if ((is_mod or is_div) and is_signed) {
             const op_token = try appendToken(rp.c, .Equal, "=");
-            const op_node = try rp.c.arena.create(ast.Node.InfixOp);
+            const op_node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
             const builtin = if (is_mod) "@rem" else "@divTrunc";
             const builtin_node = try rp.c.createBuiltinCall(builtin, 2);
             const lhs_node = try transExpr(rp, scope, lhs, .used, .l_value);
@@ -3386,9 +3383,9 @@ fn transCreateCompoundAssign(
             builtin_node.params()[1] = try transExpr(rp, scope, rhs, .used, .r_value);
             builtin_node.rparen_token = try appendToken(rp.c, .RParen, ")");
             op_node.* = .{
+                .base = .{ .tag = .Assign },
                 .op_token = op_token,
                 .lhs = lhs_node,
-                .op = .Assign,
                 .rhs = &builtin_node.base,
             };
             _ = try appendToken(rp.c, .Semicolon, ";");
@@ -3452,7 +3449,7 @@ fn transCreateCompoundAssign(
 
     if ((is_mod or is_div) and is_signed) {
         const op_token = try appendToken(rp.c, .Equal, "=");
-        const op_node = try rp.c.arena.create(ast.Node.InfixOp);
+        const op_node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
         const builtin = if (is_mod) "@rem" else "@divTrunc";
         const builtin_node = try rp.c.createBuiltinCall(builtin, 2);
         builtin_node.params()[0] = try transCreateNodePtrDeref(rp.c, lhs_node);
@@ -3461,9 +3458,9 @@ fn transCreateCompoundAssign(
         builtin_node.rparen_token = try appendToken(rp.c, .RParen, ")");
         _ = try appendToken(rp.c, .Semicolon, ";");
         op_node.* = .{
+            .base = .{ .tag = .Assign },
             .op_token = op_token,
             .lhs = ref_node,
-            .op = .Assign,
             .rhs = &builtin_node.base,
         };
         _ = try appendToken(rp.c, .Semicolon, ";");
@@ -3716,11 +3713,11 @@ fn maybeSuppressResult(
     }
     const lhs = try transCreateNodeIdentifier(rp.c, "_");
     const op_token = try appendToken(rp.c, .Equal, "=");
-    const op_node = try rp.c.arena.create(ast.Node.InfixOp);
+    const op_node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
     op_node.* = .{
+        .base = .{ .tag = .Assign },
         .op_token = op_token,
         .lhs = lhs,
-        .op = .Assign,
         .rhs = result,
     };
     return &op_node.base;
@@ -4095,11 +4092,11 @@ fn transCreateNodeAssign(
 }
 
 fn transCreateNodeFieldAccess(c: *Context, container: *ast.Node, field_name: []const u8) !*ast.Node {
-    const field_access_node = try c.arena.create(ast.Node.InfixOp);
+    const field_access_node = try c.arena.create(ast.Node.SimpleInfixOp);
     field_access_node.* = .{
+        .base = .{ .tag = .Period },
         .op_token = try appendToken(c, .Period, "."),
         .lhs = container,
-        .op = .Period,
         .rhs = try transCreateNodeIdentifier(c, field_name),
     };
     return &field_access_node.base;
@@ -4124,7 +4121,7 @@ fn transCreateNodeInfixOp(
     rp: RestorePoint,
     scope: *Scope,
     lhs_node: *ast.Node,
-    op: ast.Node.InfixOp.Op,
+    op: ast.Node.Tag,
     op_token: ast.TokenIndex,
     rhs_node: *ast.Node,
     used: ResultUsed,
@@ -4134,11 +4131,11 @@ fn transCreateNodeInfixOp(
         try appendToken(rp.c, .LParen, "(")
     else
         null;
-    const node = try rp.c.arena.create(ast.Node.InfixOp);
+    const node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
     node.* = .{
+        .base = .{ .tag = op },
         .op_token = op_token,
         .lhs = lhs_node,
-        .op = op,
         .rhs = rhs_node,
     };
     if (!grouped) return maybeSuppressResult(rp, scope, used, &node.base);
@@ -4156,7 +4153,7 @@ fn transCreateNodeBoolInfixOp(
     rp: RestorePoint,
     scope: *Scope,
     stmt: *const ZigClangBinaryOperator,
-    op: ast.Node.InfixOp.Op,
+    op: ast.Node.Tag,
     used: ResultUsed,
     grouped: bool,
 ) !*ast.Node {
@@ -4536,7 +4533,7 @@ fn transCreateNodeShiftOp(
     rp: RestorePoint,
     scope: *Scope,
     stmt: *const ZigClangBinaryOperator,
-    op: ast.Node.InfixOp.Op,
+    op: ast.Node.Tag,
     op_tok_id: std.zig.Token.Id,
     bytes: []const u8,
 ) !*ast.Node {
@@ -4558,11 +4555,11 @@ fn transCreateNodeShiftOp(
     cast_node.params()[1] = rhs;
     cast_node.rparen_token = try appendToken(rp.c, .RParen, ")");
 
-    const node = try rp.c.arena.create(ast.Node.InfixOp);
+    const node = try rp.c.arena.create(ast.Node.SimpleInfixOp);
     node.* = .{
+        .base = .{ .tag = op },
         .op_token = op_token,
         .lhs = lhs,
-        .op = op,
         .rhs = &cast_node.base,
     };
 
@@ -5404,11 +5401,11 @@ fn parseCExpr(c: *Context, it: *CTokenList.Iterator, source: []const u8, source_
                 // suppress result
                 const lhs = try transCreateNodeIdentifier(c, "_");
                 const op_token = try appendToken(c, .Equal, "=");
-                const op_node = try c.arena.create(ast.Node.InfixOp);
+                const op_node = try c.arena.create(ast.Node.SimpleInfixOp);
                 op_node.* = .{
+                    .base = .{ .tag = .Assign },
                     .op_token = op_token,
                     .lhs = lhs,
-                    .op = .Assign,
                     .rhs = last,
                 };
                 try block_scope.statements.append(&op_node.base);
@@ -5787,9 +5784,60 @@ fn parseCPrimaryExpr(c: *Context, it: *CTokenList.Iterator, source: []const u8,
     }
 }
 
+fn nodeIsInfixOp(tag: ast.Node.Tag) bool {
+    return switch (tag) {
+        .Add,
+        .AddWrap,
+        .ArrayCat,
+        .ArrayMult,
+        .Assign,
+        .AssignBitAnd,
+        .AssignBitOr,
+        .AssignBitShiftLeft,
+        .AssignBitShiftRight,
+        .AssignBitXor,
+        .AssignDiv,
+        .AssignSub,
+        .AssignSubWrap,
+        .AssignMod,
+        .AssignAdd,
+        .AssignAddWrap,
+        .AssignMul,
+        .AssignMulWrap,
+        .BangEqual,
+        .BitAnd,
+        .BitOr,
+        .BitShiftLeft,
+        .BitShiftRight,
+        .BitXor,
+        .BoolAnd,
+        .BoolOr,
+        .Div,
+        .EqualEqual,
+        .ErrorUnion,
+        .GreaterOrEqual,
+        .GreaterThan,
+        .LessOrEqual,
+        .LessThan,
+        .MergeErrorSets,
+        .Mod,
+        .Mul,
+        .MulWrap,
+        .Period,
+        .Range,
+        .Sub,
+        .SubWrap,
+        .UnwrapOptional,
+        .Catch,
+        => true,
+
+        else => false,
+    };
+}
+
 fn macroBoolToInt(c: *Context, node: *ast.Node) !*ast.Node {
     if (!isBoolRes(node)) {
-        if (node.tag != .InfixOp) return node;
+        if (!nodeIsInfixOp(node.tag)) return node;
 
         const group_node = try c.arena.create(ast.Node.GroupedExpression);
         group_node.* = .{
@@ -5808,7 +5856,7 @@ fn macroBoolToInt(c: *Context, node: *ast.Node) !*ast.Node {
 
 fn macroIntToBool(c: *Context, node: *ast.Node) !*ast.Node {
     if (isBoolRes(node)) {
-        if (node.tag != .InfixOp) return node;
+        if (!nodeIsInfixOp(node.tag)) return node;
 
         const group_node = try c.arena.create(ast.Node.GroupedExpression);
         group_node.* = .{
@@ -5821,11 +5869,11 @@ fn macroIntToBool(c: *Context, node: *ast.Node) !*ast.Node {
 
     const op_token = try appendToken(c, .BangEqual, "!=");
     const zero = try transCreateNodeInt(c, 0);
-    const res = try c.arena.create(ast.Node.InfixOp);
+    const res = try c.arena.create(ast.Node.SimpleInfixOp);
     res.* = .{
+        .base = .{ .tag = .BangEqual },
         .op_token = op_token,
         .lhs = node,
-        .op = .BangEqual,
         .rhs = zero,
     };
     const group_node = try c.arena.create(ast.Node.GroupedExpression);
@@ -5842,7 +5890,7 @@ fn parseCSuffixOpExpr(c: *Context, it: *CTokenList.Iterator, source: []const u8,
     while (true) {
         const tok = it.next().?;
         var op_token: ast.TokenIndex = undefined;
-        var op_id: ast.Node.InfixOp.Op = undefined;
+        var op_id: ast.Node.Tag = undefined;
         var bool_op = false;
         switch (tok.id) {
             .Period => {
@@ -6049,11 +6097,11 @@ fn parseCSuffixOpExpr(c: *Context, it: *CTokenList.Iterator, source: []const u8,
         const cast_fn = if (bool_op) macroIntToBool else macroBoolToInt;
         const lhs_node = try cast_fn(c, node);
         const rhs_node = try parseCPrefixOpExpr(c, it, source, source_loc, scope);
-        const op_node = try c.arena.create(ast.Node.InfixOp);
+        const op_node = try c.arena.create(ast.Node.SimpleInfixOp);
         op_node.* = .{
+            .base = .{ .tag = op_id },
             .op_token = op_token,
             .lhs = lhs_node,
-            .op = op_id,
             .rhs = try cast_fn(c, rhs_node),
         };
         node = &op_node.base;
@@ -6131,10 +6179,9 @@ fn getContainer(c: *Context, node: *ast.Node) ?*ast.Node {
             }
         },
 
-        .InfixOp => {
-            const infix = node.cast(ast.Node.InfixOp).?;
-            if (infix.op != .Period)
-                return null;
+        .Period => {
+            const infix = node.castTag(.Period).?;
+
             if (getContainerTypeOf(c, infix.lhs)) |ty_node| {
                 if (ty_node.cast(ast.Node.ContainerDecl)) |container| {
                     for (container.fieldsAndDecls()) |field_ref| {
@@ -6161,9 +6208,7 @@ fn getContainerTypeOf(c: *Context, ref: *ast.Node) ?*ast.Node {
                     return getContainer(c, ty);
             }
         }
-    } else if (ref.cast(ast.Node.InfixOp)) |infix| {
-        if (infix.op != .Period)
-            return null;
+    } else if (ref.castTag(.Period)) |infix| {
         if (getContainerTypeOf(c, infix.lhs)) |ty_node| {
             if (ty_node.cast(ast.Node.ContainerDecl)) |container| {
                 for (container.fieldsAndDecls()) |field_ref| {