Commit c2b4d51749

Andrew Kelley <andrew@ziglang.org>
2021-02-14 05:40:12
astgen: update a handful of expression types to new mem layout
break, continue, blocks, bit_not, negation, identifiers, string literals, integer literals, inline assembly also gave multiline string literals a different node tag from regular string literals, for code clarity and to avoid an unnecessary load from token_tags array.
1 parent 24798b8
Changed files (4)
lib/std/zig/ast.zig
@@ -283,6 +283,7 @@ pub const Tree = struct {
             .undefined_literal,
             .unreachable_literal,
             .string_literal,
+            .multiline_string_literal,
             .grouped_expression,
             .builtin_call_two,
             .builtin_call_two_comma,
@@ -593,7 +594,7 @@ pub const Tree = struct {
             .field_access,
             .unwrap_optional,
             .grouped_expression,
-            .string_literal,
+            .multiline_string_literal,
             .error_set_decl,
             .asm_simple,
             .asm_output,
@@ -614,6 +615,7 @@ pub const Tree = struct {
             .identifier,
             .deref,
             .enum_literal,
+            .string_literal,
             => return main_tokens[n] + end_offset,
 
             .@"return" => if (datas[n].lhs != 0) {
@@ -2826,11 +2828,14 @@ pub const Node = struct {
         identifier,
         /// lhs is the dot token index, rhs unused, main_token is the identifier.
         enum_literal,
+        /// main_token is the string literal token
+        /// Both lhs and rhs unused.
+        string_literal,
         /// main_token is the first token index (redundant with lhs)
         /// lhs is the first token index; rhs is the last token index.
         /// Could be a series of multiline_string_literal_line tokens, or a single
         /// string_literal token.
-        string_literal,
+        multiline_string_literal,
         /// `(lhs)`. main_token is the `(`; rhs is the token index of the `)`.
         grouped_expression,
         /// `@a(lhs, rhs)`. lhs and rhs may be omitted.
lib/std/zig/parse.zig
@@ -2595,8 +2595,8 @@ const Parser = struct {
                     .tag = .string_literal,
                     .main_token = main_token,
                     .data = .{
-                        .lhs = main_token,
-                        .rhs = main_token,
+                        .lhs = undefined,
+                        .rhs = undefined,
                     },
                 });
             },
@@ -2633,7 +2633,7 @@ const Parser = struct {
                     p.tok_i += 1;
                 }
                 return p.addNode(.{
-                    .tag = .string_literal,
+                    .tag = .multiline_string_literal,
                     .main_token = first_line,
                     .data = .{
                         .lhs = first_line,
@@ -3996,8 +3996,8 @@ const Parser = struct {
                     .tag = .string_literal,
                     .main_token = main_token,
                     .data = .{
-                        .lhs = main_token,
-                        .rhs = main_token,
+                        .lhs = undefined,
+                        .rhs = undefined,
                     },
                 });
             },
@@ -4007,7 +4007,7 @@ const Parser = struct {
                     p.tok_i += 1;
                 }
                 return p.addNode(.{
-                    .tag = .string_literal,
+                    .tag = .multiline_string_literal,
                     .main_token = first_line,
                     .data = .{
                         .lhs = first_line,
lib/std/zig/render.zig
@@ -153,29 +153,25 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
         .unreachable_literal,
         .undefined_literal,
         .anyframe_literal,
+        .string_literal,
         => return renderToken(ais, tree, main_tokens[node], space),
 
-        .string_literal => switch (token_tags[main_tokens[node]]) {
-            .string_literal => try renderToken(ais, tree, main_tokens[node], space),
+        .multiline_string_literal => {
+            var locked_indents = ais.lockOneShotIndent();
+            try ais.maybeInsertNewline();
 
-            .multiline_string_literal_line => {
-                var locked_indents = ais.lockOneShotIndent();
-                try ais.maybeInsertNewline();
+            var i = datas[node].lhs;
+            while (i <= datas[node].rhs) : (i += 1) try renderToken(ais, tree, i, .newline);
 
-                var i = datas[node].lhs;
-                while (i <= datas[node].rhs) : (i += 1) try renderToken(ais, tree, i, .newline);
+            while (locked_indents > 0) : (locked_indents -= 1) ais.popIndent();
 
-                while (locked_indents > 0) : (locked_indents -= 1) ais.popIndent();
-
-                switch (space) {
-                    .none => {},
-                    .semicolon => if (token_tags[i] == .semicolon) try renderToken(ais, tree, i, .newline),
-                    .comma => if (token_tags[i] == .comma) try renderToken(ais, tree, i, .newline),
-                    .comma_space => if (token_tags[i] == .comma) try renderToken(ais, tree, i, .space),
-                    else => unreachable,
-                }
-            },
-            else => unreachable,
+            switch (space) {
+                .none => {},
+                .semicolon => if (token_tags[i] == .semicolon) try renderToken(ais, tree, i, .newline),
+                .comma => if (token_tags[i] == .comma) try renderToken(ais, tree, i, .newline),
+                .comma_space => if (token_tags[i] == .comma) try renderToken(ais, tree, i, .space),
+                else => unreachable,
+            }
         },
 
         .error_value => {
@@ -1850,8 +1846,7 @@ fn renderCall(
                 try renderExpression(ais, tree, param_node, Space.none);
 
                 // Unindent the comma for multiline string literals
-                const is_multiline_string = node_tags[param_node] == .string_literal and
-                    token_tags[main_tokens[param_node]] == .multiline_string_literal_line;
+                const is_multiline_string = node_tags[param_node] == .multiline_string_literal;
                 if (is_multiline_string) ais.popIndent();
 
                 const comma = tree.lastToken(param_node) + 1;
src/astgen.zig
@@ -56,7 +56,8 @@ pub const ResultLoc = union(enum) {
 };
 
 pub fn typeExpr(mod: *Module, scope: *Scope, type_node: ast.Node.Index) InnerError!*zir.Inst {
-    const type_src = scope.tree().token_locs[type_node.firstToken()].start;
+    const tree = scope.tree();
+    const type_src = token_starts[tree.firstToken(type_node)];
     const type_type = try addZIRInstConst(mod, scope, type_src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.type_type),
@@ -258,18 +259,23 @@ pub fn expr(mod: *Module, scope: *Scope, rl: ResultLoc, node: ast.Node.Index) In
         .array_cat => return simpleBinOp(mod, scope, rl, node, .array_cat),
         .array_mult => return simpleBinOp(mod, scope, rl, node, .array_mul),
 
-        .bool_and => return boolBinOp(mod, scope, rl, node),
-        .bool_or => return boolBinOp(mod, scope, rl, node),
+        .bool_and => return boolBinOp(mod, scope, rl, node, true),
+        .bool_or => return boolBinOp(mod, scope, rl, node, false),
 
         .bool_not => return rvalue(mod, scope, rl, try boolNot(mod, scope, node)),
         .bit_not => return rvalue(mod, scope, rl, try bitNot(mod, scope, node)),
         .negation => return rvalue(mod, scope, rl, try negation(mod, scope, node, .sub)),
         .negation_wrap => return rvalue(mod, scope, rl, try negation(mod, scope, node, .subwrap)),
 
-        .identifier => return try identifier(mod, scope, rl, node),
-        .@"asm" => return rvalue(mod, scope, rl, try assembly(mod, scope, node)),
-        .string_literal => return rvalue(mod, scope, rl, try stringLiteral(mod, scope, node)),
-        .integer_literal => return rvalue(mod, scope, rl, try integerLiteral(mod, scope, node)),
+        .identifier => return identifier(mod, scope, rl, node),
+
+        .asm_simple => return assembly(mod, scope, rl, tree.asmSimple(node)),
+        .@"asm" => return assembly(mod, scope, rl, tree.asmFull(node)),
+
+        .string_literal => return stringLiteral(mod, scope, rl, node),
+        .multiline_string_literal => return multilineStringLiteral(mod, scope, rl, node),
+
+        .integer_literal => return integerLiteral(mod, scope, rl, node),
         .builtin_call => return builtinCall(mod, scope, rl, node),
         .call => return callExpr(mod, scope, rl, node),
         .@"unreachable" => return unreach(mod, scope, node),
@@ -285,7 +291,22 @@ pub fn expr(mod: *Module, scope: *Scope, rl: ResultLoc, node: ast.Node.Index) In
         .null_literal => return rvalue(mod, scope, rl, try nullLiteral(mod, scope, node)),
         .optional_type => return rvalue(mod, scope, rl, try optionalType(mod, scope, node)),
         .unwrap_optional => return unwrapOptional(mod, scope, rl, node),
-        .block => return rvalueVoid(mod, scope, rl, node, try blockExpr(mod, scope, node)),
+
+        .block_two, .block_two_semicolon => {
+            const statements = [2]ast.Node.Index{ node_datas[node].lhs, node_datas[node].rhs };
+            if (node_datas[node].lhs == 0) {
+                return blockExpr(mod, scope, rl, node, statements[0..0]);
+            } else if (node_datas[node].rhs == 0) {
+                return blockExpr(mod, scope, rl, node, statements[0..1]);
+            } else {
+                return blockExpr(mod, scope, rl, node, statements[0..2]);
+            }
+        },
+        .block, .block_semicolon => {
+            const statements = tree.extra_data[node_datas[node].lhs..node_datas[node].rhs];
+            return blockExpr(mod, scope, rl, node, statements);
+        },
+
         .labeled_block => return labeledBlockExpr(mod, scope, rl, node, .block),
         .@"break" => return rvalue(mod, scope, rl, try breakExpr(mod, scope, node)),
         .@"continue" => return rvalue(mod, scope, rl, try continueExpr(mod, scope, node)),
@@ -293,7 +314,6 @@ pub fn expr(mod: *Module, scope: *Scope, rl: ResultLoc, node: ast.Node.Index) In
         .array_type => return rvalue(mod, scope, rl, try arrayType(mod, scope, node)),
         .array_type_sentinel => return rvalue(mod, scope, rl, try arrayTypeSentinel(mod, scope, node)),
         .enum_literal => return rvalue(mod, scope, rl, try enumLiteral(mod, scope, node)),
-        .MultilineStringLiteral => return rvalue(mod, scope, rl, try multilineStrLiteral(mod, scope, node)),
         .char_literal => return rvalue(mod, scope, rl, try charLiteral(mod, scope, node)),
         .slice_type => return rvalue(mod, scope, rl, try sliceType(mod, scope, node)),
         .error_union => return rvalue(mod, scope, rl, try typeInixOp(mod, scope, node, .error_union_type)),
@@ -343,10 +363,17 @@ pub fn comptimeExpr(
         return expr(mod, parent_scope, rl, node);
     }
 
+    const tree = parent_scope.tree();
+    const main_tokens = tree.nodes.items(.main_token);
+    const token_tags = tree.tokens.items(.tag);
+
     // Optimization for labeled blocks: don't need to have 2 layers of blocks,
     // we can reuse the existing one.
-    if (node.castTag(.labeled_block)) |block_node| {
-        return labeledBlockExpr(mod, parent_scope, rl, block_node, .block_comptime);
+    const lbrace = main_tokens[node];
+    if (token_tags[lbrace - 1] == .colon and
+        token_tags[lbrace - 2] == .identifier)
+    {
+        return labeledBlockExpr(mod, parent_scope, rl, node, .block_comptime);
     }
 
     // Make a scope to collect generated instructions in the sub-expression.
@@ -363,11 +390,7 @@ pub fn comptimeExpr(
     // instruction is the block's result value.
     _ = try expr(mod, &block_scope.base, rl, node);
 
-    const tree = parent_scope.tree();
-    const node_datas = tree.nodes.items(.data);
-    const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[node.firstToken()].start;
-
+    const src = token_starts[tree.firstToken(node)];
     const block = try addZIRInstBlock(mod, parent_scope, src, .block_comptime_flat, .{
         .instructions = try block_scope.arena.dupe(*zir.Inst, block_scope.instructions.items),
     });
@@ -375,15 +398,13 @@ pub fn comptimeExpr(
     return &block.base;
 }
 
-fn breakExpr(
-    mod: *Module,
-    parent_scope: *Scope,
-    node: *ast.Node.ControlFlowExpression,
-) InnerError!*zir.Inst {
+fn breakExpr(mod: *Module, parent_scope: *Scope, node: ast.Node.Index) InnerError!*zir.Inst {
     const tree = parent_scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[node.ltoken].start;
+    const src = token_starts[main_tokens[node]];
+    const break_label = node_datas[node].lhs;
+    const rhs = node_datas[node].rhs;
 
     // Look for the label in the scope.
     var scope = parent_scope;
@@ -393,7 +414,7 @@ fn breakExpr(
                 const gen_zir = scope.cast(Scope.GenZIR).?;
 
                 const block_inst = blk: {
-                    if (node.getLabel()) |break_label| {
+                    if (break_label != 0) {
                         if (gen_zir.label) |*label| {
                             if (try tokenIdentEql(mod, parent_scope, label.token, break_label)) {
                                 label.used = true;
@@ -407,11 +428,11 @@ fn breakExpr(
                     continue;
                 };
 
-                const rhs = node.getRHS() orelse {
+                if (rhs == 0) {
                     return addZirInstTag(mod, parent_scope, src, .break_void, .{
                         .block = block_inst,
                     });
-                };
+                }
                 gen_zir.break_count += 1;
                 const prev_rvalue_rl_count = gen_zir.rvalue_rl_count;
                 const operand = try expr(mod, parent_scope, gen_zir.break_result_loc, rhs);
@@ -435,7 +456,7 @@ fn breakExpr(
             },
             .local_val => scope = scope.cast(Scope.LocalVal).?.parent,
             .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent,
-            else => if (node.getLabel()) |break_label| {
+            else => if (break_label != 0) {
                 const label_name = try mod.identifierTokenString(parent_scope, break_label);
                 return mod.failTok(parent_scope, break_label, "label not found: '{s}'", .{label_name});
             } else {
@@ -445,11 +466,12 @@ fn breakExpr(
     }
 }
 
-fn continueExpr(mod: *Module, parent_scope: *Scope, node: *ast.Node.ControlFlowExpression) InnerError!*zir.Inst {
+fn continueExpr(mod: *Module, parent_scope: *Scope, node: ast.Node.Index) InnerError!*zir.Inst {
     const tree = parent_scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[node.ltoken].start;
+    const src = token_starts[main_tokens[node]];
+    const break_label = node_datas[node].lhs;
 
     // Look for the label in the scope.
     var scope = parent_scope;
@@ -461,7 +483,7 @@ fn continueExpr(mod: *Module, parent_scope: *Scope, node: *ast.Node.ControlFlowE
                     scope = gen_zir.parent;
                     continue;
                 };
-                if (node.getLabel()) |break_label| blk: {
+                if (break_label != 0) blk: {
                     if (gen_zir.label) |*label| {
                         if (try tokenIdentEql(mod, parent_scope, label.token, break_label)) {
                             label.used = true;
@@ -479,7 +501,7 @@ fn continueExpr(mod: *Module, parent_scope: *Scope, node: *ast.Node.ControlFlowE
             },
             .local_val => scope = scope.cast(Scope.LocalVal).?.parent,
             .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent,
-            else => if (node.getLabel()) |break_label| {
+            else => if (break_label != 0) {
                 const label_name = try mod.identifierTokenString(parent_scope, break_label);
                 return mod.failTok(parent_scope, break_label, "label not found: '{s}'", .{label_name});
             } else {
@@ -489,11 +511,18 @@ fn continueExpr(mod: *Module, parent_scope: *Scope, node: *ast.Node.ControlFlowE
     }
 }
 
-pub fn blockExpr(mod: *Module, parent_scope: *Scope, block_node: *ast.Node.block) InnerError!void {
+pub fn blockExpr(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    block_node: ast.Node.Index,
+    statements: []const ast.Node.Index,
+) InnerError!void {
     const tracy = trace(@src());
     defer tracy.end();
 
-    try blockExprStmts(mod, parent_scope, &block_node.base, block_node.statements());
+    try blockExprStmts(mod, scope, &block_node.base, statements);
+    return rvalueVoid(mod, scope, rl, block_node, {});
 }
 
 fn checkLabelRedefinition(mod: *Module, parent_scope: *Scope, label: ast.TokenIndex) !void {
@@ -508,8 +537,8 @@ fn checkLabelRedefinition(mod: *Module, parent_scope: *Scope, label: ast.TokenIn
                         const tree = parent_scope.tree();
                         const node_datas = tree.nodes.items(.data);
                         const main_tokens = tree.nodes.items(.main_token);
-                        const label_src = tree.token_locs[label].start;
-                        const prev_label_src = tree.token_locs[prev_label.token].start;
+                        const label_src = token_starts[label];
+                        const prev_label_src = token_starts[prev_label.token];
 
                         const label_name = try mod.identifierTokenString(parent_scope, label);
                         const msg = msg: {
@@ -556,7 +585,7 @@ fn labeledBlockExpr(
     const tree = parent_scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[block_node.lbrace].start;
+    const src = token_starts[block_node.lbrace];
 
     try checkLabelRedefinition(mod, parent_scope, block_node.label);
 
@@ -595,7 +624,7 @@ fn labeledBlockExpr(
     try blockExprStmts(mod, &block_scope.base, &block_node.base, block_node.statements());
 
     if (!block_scope.label.?.used) {
-        return mod.fail(parent_scope, tree.token_locs[block_node.label].start, "unused block label", .{});
+        return mod.fail(parent_scope, token_starts[block_node.label], "unused block label", .{});
     }
 
     try gen_zir.instructions.append(mod.gpa, &block_inst.base);
@@ -647,7 +676,7 @@ fn blockExprStmts(
 
     var scope = parent_scope;
     for (statements) |statement| {
-        const src = tree.token_locs[statement.firstToken()].start;
+        const src = token_starts[statement.firstToken()];
         _ = try addZIRNoOp(mod, scope, src, .dbg_stmt);
         switch (statement.tag) {
             .var_decl => {
@@ -694,7 +723,7 @@ fn varDecl(
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const name_src = tree.token_locs[node.name_token].start;
+    const name_src = token_starts[node.name_token];
     const ident_name = try mod.identifierTokenString(scope, node.name_token);
 
     // Local variables shadowing detection, including function parameters.
@@ -911,34 +940,46 @@ fn assignOp(
     _ = try addZIRBinOp(mod, scope, src, .store, lhs_ptr, result);
 }
 
-fn boolNot(mod: *Module, scope: *Scope, node: *ast.Node.SimplePrefixOp) InnerError!*zir.Inst {
+fn boolNot(mod: *Module, scope: *Scope, node: ast.Node.Index) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const node_datas = tree.nodes.items(.data);
+    const main_tokens = tree.nodes.items(.main_token);
+
+    const src = token_starts[main_tokens[node]];
     const bool_type = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.bool_type),
     });
-    const operand = try expr(mod, scope, .{ .ty = bool_type }, node.rhs);
+    const operand = try expr(mod, scope, .{ .ty = bool_type }, node_datas[node].lhs);
     return addZIRUnOp(mod, scope, src, .bool_not, operand);
 }
 
-fn bitNot(mod: *Module, scope: *Scope, node: *ast.Node.SimplePrefixOp) InnerError!*zir.Inst {
+fn bitNot(mod: *Module, scope: *Scope, node: ast.Node.Index) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
-    const operand = try expr(mod, scope, .none, node.rhs);
+    const node_datas = tree.nodes.items(.data);
+    const main_tokens = tree.nodes.items(.main_token);
+
+    const src = token_starts[main_tokens[node]];
+    const operand = try expr(mod, scope, .none, node_datas[node].lhs);
     return addZIRUnOp(mod, scope, src, .bit_not, operand);
 }
 
-fn negation(mod: *Module, scope: *Scope, node: *ast.Node.SimplePrefixOp, op_inst_tag: zir.Inst.Tag) InnerError!*zir.Inst {
+fn negation(
+    mod: *Module,
+    scope: *Scope,
+    node: ast.Node.Index,
+    op_inst_tag: zir.Inst.Tag,
+) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const node_datas = tree.nodes.items(.data);
+    const main_tokens = tree.nodes.items(.main_token);
 
+    const src = token_starts[main_tokens[node]];
     const lhs = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.comptime_int),
         .val = Value.initTag(.zero),
     });
-    const rhs = try expr(mod, scope, .none, node.rhs);
-
+    const rhs = try expr(mod, scope, .none, node_datas[node].lhs);
     return addZIRBinOp(mod, scope, src, op_inst_tag, lhs, rhs);
 }
 
@@ -948,20 +989,20 @@ fn addressOf(mod: *Module, scope: *Scope, node: *ast.Node.SimplePrefixOp) InnerE
 
 fn optionalType(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 src = token_starts[node.op_token];
     const operand = try typeExpr(mod, scope, node.rhs);
     return addZIRUnOp(mod, scope, src, .optional_type, operand);
 }
 
 fn sliceType(mod: *Module, scope: *Scope, node: *ast.Node.slice_type) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const src = token_starts[node.op_token];
     return ptrSliceType(mod, scope, src, &node.ptr_info, node.rhs, .Slice);
 }
 
 fn ptrType(mod: *Module, scope: *Scope, node: *ast.Node.PtrType) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const src = token_starts[node.op_token];
     return ptrSliceType(mod, scope, src, &node.ptr_info, node.rhs, switch (tree.token_ids[node.op_token]) {
         .Asterisk, .AsteriskAsterisk => .One,
         // TODO stage1 type inference bug
@@ -1018,7 +1059,7 @@ fn ptrSliceType(mod: *Module, scope: *Scope, src: usize, ptr_info: *ast.PtrInfo,
 
 fn arrayType(mod: *Module, scope: *Scope, node: *ast.Node.array_type) !*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const src = token_starts[node.op_token];
     const usize_type = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.usize_type),
@@ -1033,7 +1074,7 @@ fn arrayType(mod: *Module, scope: *Scope, node: *ast.Node.array_type) !*zir.Inst
 
 fn arrayTypeSentinel(mod: *Module, scope: *Scope, node: *ast.Node.array_type_sentinel) !*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const src = token_starts[node.op_token];
     const usize_type = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.usize_type),
@@ -1054,7 +1095,7 @@ fn arrayTypeSentinel(mod: *Module, scope: *Scope, node: *ast.Node.array_type_sen
 
 fn anyFrameType(mod: *Module, scope: *Scope, node: *ast.Node.anyframe_type) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.anyframe_token].start;
+    const src = token_starts[node.anyframe_token];
     if (node.result) |some| {
         const return_type = try typeExpr(mod, scope, some.return_type);
         return addZIRUnOp(mod, scope, src, .anyframe_type, return_type);
@@ -1068,7 +1109,7 @@ fn anyFrameType(mod: *Module, scope: *Scope, node: *ast.Node.anyframe_type) Inne
 
 fn typeInixOp(mod: *Module, scope: *Scope, node: *ast.Node.SimpleInfixOp, op_inst_tag: zir.Inst.Tag) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const src = token_starts[node.op_token];
     const error_set = try typeExpr(mod, scope, node.lhs);
     const payload = try typeExpr(mod, scope, node.rhs);
     return addZIRBinOp(mod, scope, src, op_inst_tag, error_set, payload);
@@ -1076,7 +1117,7 @@ fn typeInixOp(mod: *Module, scope: *Scope, node: *ast.Node.SimpleInfixOp, op_ins
 
 fn enumLiteral(mod: *Module, scope: *Scope, node: *ast.Node.enum_literal) !*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.name].start;
+    const src = token_starts[node.name];
     const name = try mod.identifierTokenString(scope, node.name);
 
     return addZIRInst(mod, scope, src, zir.Inst.EnumLiteral, .{ .name = name }, .{});
@@ -1084,7 +1125,7 @@ fn enumLiteral(mod: *Module, scope: *Scope, node: *ast.Node.enum_literal) !*zir.
 
 fn unwrapOptional(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.SimpleSuffixOp) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.rtoken].start;
+    const src = token_starts[node.rtoken];
 
     const operand = try expr(mod, scope, rl, node.lhs);
     const op: zir.Inst.Tag = switch (rl) {
@@ -1100,7 +1141,7 @@ fn containerField(
     node: *ast.Node.ContainerField,
 ) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.firstToken()].start;
+    const src = token_starts[tree.firstToken(node)];
     const name = try mod.identifierTokenString(scope, node.name_token);
 
     if (node.comptime_token == null and node.value_expr == null and node.align_expr == null) {
@@ -1133,7 +1174,7 @@ fn containerField(
 
 fn containerDecl(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.ContainerDecl) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.kind_token].start;
+    const src = token_starts[node.kind_token];
 
     var gen_scope: Scope.GenZIR = .{
         .parent = scope,
@@ -1278,7 +1319,7 @@ fn containerDecl(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.Con
 
 fn errorSetDecl(mod: *Module, scope: *Scope, node: *ast.Node.error_set_decl) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.error_token].start;
+    const src = token_starts[node.error_token];
     const decls = node.decls();
     const fields = try scope.arena().alloc([]const u8, decls.len);
 
@@ -1292,7 +1333,7 @@ fn errorSetDecl(mod: *Module, scope: *Scope, node: *ast.Node.error_set_decl) Inn
 
 fn errorType(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.token].start;
+    const src = token_starts[node.token];
     return addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.anyerror_type),
@@ -1370,7 +1411,7 @@ fn orelseCatchExpr(
     payload_node: ?*ast.Node,
 ) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[op_token].start;
+    const src = token_starts[op_token];
 
     var block_scope: Scope.GenZIR = .{
         .parent = scope,
@@ -1544,7 +1585,7 @@ fn tokenIdentEql(mod: *Module, scope: *Scope, token1: ast.TokenIndex, token2: as
 
 pub fn field(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.SimpleInfixOp) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.op_token].start;
+    const src = token_starts[node.op_token];
     // TODO custom AST node for field access so that we don't have to go through a node cast here
     const field_name = try mod.identifierTokenString(scope, node.rhs.castTag(.identifier).?.token);
     if (rl == .ref) {
@@ -1568,7 +1609,7 @@ fn namedField(
     try ensureBuiltinParamCount(mod, scope, call, 2);
 
     const tree = scope.tree();
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
 
     const string_type = try addZIRInstConst(mod, scope, src, .{
@@ -1591,7 +1632,7 @@ fn namedField(
 
 fn arrayAccess(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.array_access) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.rtoken].start;
+    const src = token_starts[node.rtoken];
     const usize_type = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.usize_type),
@@ -1612,7 +1653,7 @@ fn arrayAccess(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.array
 
 fn sliceExpr(mod: *Module, scope: *Scope, node: *ast.Node.slice) InnerError!*zir.Inst {
     const tree = scope.tree();
-    const src = tree.token_locs[node.rtoken].start;
+    const src = token_starts[node.rtoken];
 
     const usize_type = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
@@ -1642,7 +1683,7 @@ fn sliceExpr(mod: *Module, scope: *Scope, node: *ast.Node.slice) InnerError!*zir
 
 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 src = token_starts[node.rtoken];
     const lhs = try expr(mod, scope, .none, node.lhs);
     return addZIRUnOp(mod, scope, src, .deref, lhs);
 }
@@ -1669,13 +1710,14 @@ fn boolBinOp(
     mod: *Module,
     scope: *Scope,
     rl: ResultLoc,
-    infix_node: *ast.Node.SimpleInfixOp,
+    infix_node: ast.Node.Index,
+    is_bool_and: bool,
 ) InnerError!*zir.Inst {
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
 
-    const src = tree.token_locs[infix_node.op_token].start;
+    const src = token_starts[main_tokens[infix_node]];
     const bool_type = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.bool_type),
@@ -1690,7 +1732,7 @@ fn boolBinOp(
     };
     defer block_scope.instructions.deinit(mod.gpa);
 
-    const lhs = try expr(mod, scope, .{ .ty = bool_type }, infix_node.lhs);
+    const lhs = try expr(mod, scope, .{ .ty = bool_type }, node_datas[infix_node].lhs);
     const condbr = try addZIRInstSpecial(mod, &block_scope.base, src, zir.Inst.CondBr, .{
         .condition = lhs,
         .then_body = undefined, // populated below
@@ -1710,7 +1752,7 @@ fn boolBinOp(
     };
     defer rhs_scope.instructions.deinit(mod.gpa);
 
-    const rhs = try expr(mod, &rhs_scope.base, .{ .ty = bool_type }, infix_node.rhs);
+    const rhs = try expr(mod, &rhs_scope.base, .{ .ty = bool_type }, node_datas[infix_node].rhs);
     _ = try addZIRInst(mod, &rhs_scope.base, src, zir.Inst.Break, .{
         .block = block,
         .operand = rhs,
@@ -1725,7 +1767,6 @@ fn boolBinOp(
     };
     defer const_scope.instructions.deinit(mod.gpa);
 
-    const is_bool_and = infix_node.base.tag == .bool_and;
     _ = try addZIRInst(mod, &const_scope.base, src, zir.Inst.Break, .{
         .block = block,
         .operand = try addZIRInstConst(mod, &const_scope.base, src, .{
@@ -1843,7 +1884,7 @@ fn ifExpr(mod: *Module, scope: *Scope, rl: ResultLoc, if_node: *ast.Node.@"if")
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const if_src = tree.token_locs[if_node.if_token].start;
+    const if_src = token_starts[if_node.if_token];
     const cond = try cond_kind.cond(mod, &block_scope, if_src, if_node.condition);
 
     const condbr = try addZIRInstSpecial(mod, &block_scope.base, if_src, zir.Inst.CondBr, .{
@@ -1856,7 +1897,7 @@ fn ifExpr(mod: *Module, scope: *Scope, rl: ResultLoc, if_node: *ast.Node.@"if")
         .instructions = try block_scope.arena.dupe(*zir.Inst, block_scope.instructions.items),
     });
 
-    const then_src = tree.token_locs[if_node.body.lastToken()].start;
+    const then_src = token_starts[if_node.body.lastToken()];
     var then_scope: Scope.GenZIR = .{
         .parent = scope,
         .decl = block_scope.decl,
@@ -1887,14 +1928,14 @@ fn ifExpr(mod: *Module, scope: *Scope, rl: ResultLoc, if_node: *ast.Node.@"if")
     var else_src: usize = undefined;
     var else_sub_scope: *Module.Scope = undefined;
     const else_result: ?*zir.Inst = if (if_node.@"else") |else_node| blk: {
-        else_src = tree.token_locs[else_node.body.lastToken()].start;
+        else_src = token_starts[else_node.body.lastToken()];
         // declare payload to the then_scope
         else_sub_scope = try cond_kind.elseSubScope(mod, &else_scope, else_src, else_node.payload);
 
         block_scope.break_count += 1;
         break :blk try expr(mod, else_sub_scope, block_scope.break_result_loc, else_node.body);
     } else blk: {
-        else_src = tree.token_locs[if_node.lastToken()].start;
+        else_src = token_starts[if_node.lastToken()];
         else_sub_scope = &else_scope.base;
         break :blk null;
     };
@@ -1981,7 +2022,7 @@ fn whileExpr(
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const while_src = tree.token_locs[while_node.while_token].start;
+    const while_src = token_starts[while_node.while_token];
     const void_type = try addZIRInstConst(mod, scope, while_src, .{
         .ty = Type.initTag(.type),
         .val = Value.initTag(.void_type),
@@ -2028,7 +2069,7 @@ fn whileExpr(
         });
     }
 
-    const then_src = tree.token_locs[while_node.body.lastToken()].start;
+    const then_src = token_starts[while_node.body.lastToken()];
     var then_scope: Scope.GenZIR = .{
         .parent = &continue_scope.base,
         .decl = continue_scope.decl,
@@ -2055,19 +2096,19 @@ fn whileExpr(
 
     var else_src: usize = undefined;
     const else_result: ?*zir.Inst = if (while_node.@"else") |else_node| blk: {
-        else_src = tree.token_locs[else_node.body.lastToken()].start;
+        else_src = token_starts[else_node.body.lastToken()];
         // declare payload to the then_scope
         const else_sub_scope = try cond_kind.elseSubScope(mod, &else_scope, else_src, else_node.payload);
 
         loop_scope.break_count += 1;
         break :blk try expr(mod, else_sub_scope, loop_scope.break_result_loc, else_node.body);
     } else blk: {
-        else_src = tree.token_locs[while_node.lastToken()].start;
+        else_src = token_starts[while_node.lastToken()];
         break :blk null;
     };
     if (loop_scope.label) |some| {
         if (!some.used) {
-            return mod.fail(scope, tree.token_locs[some.token].start, "unused while label", .{});
+            return mod.fail(scope, token_starts[some.token], "unused while label", .{});
         }
     }
     return finishThenElseBlock(
@@ -2105,7 +2146,7 @@ fn forExpr(
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const for_src = tree.token_locs[for_node.for_token].start;
+    const for_src = token_starts[for_node.for_token];
     const index_ptr = blk: {
         const usize_type = try addZIRInstConst(mod, scope, for_src, .{
             .ty = Type.initTag(.type),
@@ -2121,7 +2162,7 @@ fn forExpr(
         break :blk index_ptr;
     };
     const array_ptr = try expr(mod, scope, .ref, for_node.array_expr);
-    const cond_src = tree.token_locs[for_node.array_expr.firstToken()].start;
+    const cond_src = token_starts[for_node.array_expr.firstToken()];
     const len = try addZIRUnOp(mod, scope, cond_src, .indexable_ptr_len, array_ptr);
 
     var loop_scope: Scope.GenZIR = .{
@@ -2191,7 +2232,7 @@ fn forExpr(
     }
 
     // while body
-    const then_src = tree.token_locs[for_node.body.lastToken()].start;
+    const then_src = token_starts[for_node.body.lastToken()];
     var then_scope: Scope.GenZIR = .{
         .parent = &cond_scope.base,
         .decl = cond_scope.decl,
@@ -2215,7 +2256,7 @@ fn forExpr(
         const index_symbol_node = payload.index_symbol orelse
             break :blk &then_scope.base;
 
-        const index_name = tree.tokenSlice(index_symbol_node.firstToken());
+        const index_name = tree.tokenSlice(tree.firstToken(index_symbol_node));
         if (mem.eql(u8, index_name, "_")) {
             break :blk &then_scope.base;
         }
@@ -2244,16 +2285,16 @@ fn forExpr(
 
     var else_src: usize = undefined;
     const else_result: ?*zir.Inst = if (for_node.@"else") |else_node| blk: {
-        else_src = tree.token_locs[else_node.body.lastToken()].start;
+        else_src = token_starts[else_node.body.lastToken()];
         loop_scope.break_count += 1;
         break :blk try expr(mod, &else_scope.base, loop_scope.break_result_loc, else_node.body);
     } else blk: {
-        else_src = tree.token_locs[for_node.lastToken()].start;
+        else_src = token_starts[for_node.lastToken()];
         break :blk null;
     };
     if (loop_scope.label) |some| {
         if (!some.used) {
-            return mod.fail(scope, tree.token_locs[some.token].start, "unused for label", .{});
+            return mod.fail(scope, token_starts[some.token], "unused for label", .{});
         }
     }
     return finishThenElseBlock(
@@ -2299,7 +2340,7 @@ fn switchExpr(mod: *Module, scope: *Scope, rl: ResultLoc, switch_node: *ast.Node
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const switch_src = tree.token_locs[switch_node.switch_token].start;
+    const switch_src = token_starts[switch_node.switch_token];
     const use_ref = switchCaseUsesRef(switch_node);
 
     var block_scope: Scope.GenZIR = .{
@@ -2322,7 +2363,7 @@ fn switchExpr(mod: *Module, scope: *Scope, rl: ResultLoc, switch_node: *ast.Node
     var simple_case_count: usize = 0;
     for (switch_node.cases()) |uncasted_case| {
         const case = uncasted_case.castTag(.switch_case).?;
-        const case_src = tree.token_locs[case.firstToken()].start;
+        const case_src = token_starts[case.firstToken()];
         assert(case.items_len != 0);
 
         // Check for else/_ prong, those are handled last.
@@ -2389,7 +2430,7 @@ fn switchExpr(mod: *Module, scope: *Scope, rl: ResultLoc, switch_node: *ast.Node
             if (getRangeNode(item)) |range| {
                 const start = try comptimeExpr(mod, &block_scope.base, .none, range.lhs);
                 const end = try comptimeExpr(mod, &block_scope.base, .none, range.rhs);
-                const range_src = tree.token_locs[range.op_token].start;
+                const range_src = token_starts[range.op_token];
                 const range_inst = try addZIRBinOp(mod, &block_scope.base, range_src, .switch_range, start, end);
                 try items.append(range_inst);
             } else {
@@ -2447,7 +2488,7 @@ fn switchExpr(mod: *Module, scope: *Scope, rl: ResultLoc, switch_node: *ast.Node
     var case_index: usize = 0;
     for (switch_node.cases()) |uncasted_case| {
         const case = uncasted_case.castTag(.switch_case).?;
-        const case_src = tree.token_locs[case.firstToken()].start;
+        const case_src = token_starts[case.firstToken()];
         // reset without freeing to reduce allocations.
         case_scope.instructions.items.len = 0;
 
@@ -2485,7 +2526,7 @@ fn switchExpr(mod: *Module, scope: *Scope, rl: ResultLoc, switch_node: *ast.Node
         var any_ok: ?*zir.Inst = null;
         for (case.items()) |item| {
             if (getRangeNode(item)) |range| {
-                const range_src = tree.token_locs[range.op_token].start;
+                const range_src = token_starts[range.op_token];
                 const range_inst = items.items[items_index].castTag(.switch_range).?;
                 items_index += 1;
 
@@ -2565,7 +2606,7 @@ fn switchCaseExpr(
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const case_src = tree.token_locs[case.firstToken()].start;
+    const case_src = token_starts[case.firstToken()];
     const sub_scope = blk: {
         const uncasted_payload = case.payload orelse break :blk scope;
         const payload = uncasted_payload.castTag(.PointerPayload).?;
@@ -2593,7 +2634,7 @@ fn ret(mod: *Module, scope: *Scope, cfe: *ast.Node.ControlFlowExpression) InnerE
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[cfe.ltoken].start;
+    const src = token_starts[cfe.ltoken];
     if (cfe.getRHS()) |rhs_node| {
         if (nodeMayNeedMemoryLocation(rhs_node, scope)) {
             const ret_ptr = try addZIRNoOp(mod, scope, src, .ret_ptr);
@@ -2609,17 +2650,24 @@ fn ret(mod: *Module, scope: *Scope, cfe: *ast.Node.ControlFlowExpression) InnerE
     }
 }
 
-fn identifier(mod: *Module, scope: *Scope, rl: ResultLoc, ident: *ast.Node.OneToken) InnerError!*zir.Inst {
+fn identifier(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    ident: ast.Node.Index,
+) InnerError!*zir.Inst {
     const tracy = trace(@src());
     defer tracy.end();
 
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const ident_name = try mod.identifierTokenString(scope, ident.token);
-    const src = tree.token_locs[ident.token].start;
+
+    const ident_token = main_tokens[ident];
+    const ident_name = try mod.identifierTokenString(scope, ident_token);
+    const src = token_starts[ident_token];
     if (mem.eql(u8, ident_name, "_")) {
-        return mod.failNode(scope, &ident.base, "TODO implement '_' identifier", .{});
+        return mod.failNode(scope, ident, "TODO implement '_' identifier", .{});
     }
 
     if (getSimplePrimitiveValue(ident_name)) |typed_value| {
@@ -2634,7 +2682,7 @@ fn identifier(mod: *Module, scope: *Scope, rl: ResultLoc, ident: *ast.Node.OneTo
             const bit_count = std.fmt.parseInt(u16, ident_name[1..], 10) catch |err| switch (err) {
                 error.Overflow => return mod.failNode(
                     scope,
-                    &ident.base,
+                    ident,
                     "primitive integer type '{s}' exceeds maximum bit width of 65535",
                     .{ident_name},
                 ),
@@ -2698,64 +2746,91 @@ fn identifier(mod: *Module, scope: *Scope, rl: ResultLoc, ident: *ast.Node.OneTo
         }
     }
 
-    return mod.failNode(scope, &ident.base, "use of undeclared identifier '{s}'", .{ident_name});
+    return mod.failNode(scope, ident, "use of undeclared identifier '{s}'", .{ident_name});
 }
 
-fn stringLiteral(mod: *Module, scope: *Scope, str_lit: *ast.Node.OneToken) InnerError!*zir.Inst {
+fn stringLiteral(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    str_lit: ast.Node.Index,
+) InnerError!*zir.Inst {
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const unparsed_bytes = tree.tokenSlice(str_lit.token);
+
+    const str_lit_token = main_tokens[str_lit];
+    const unparsed_bytes = tree.tokenSlice(str_lit_token);
     const arena = scope.arena();
 
     var bad_index: usize = undefined;
     const bytes = std.zig.parseStringLiteral(arena, unparsed_bytes, &bad_index) catch |err| switch (err) {
         error.InvalidCharacter => {
             const bad_byte = unparsed_bytes[bad_index];
-            const src = tree.token_locs[str_lit.token].start;
+            const src = token_starts[str_lit_token];
             return mod.fail(scope, src + bad_index, "invalid string literal character: '{c}'\n", .{bad_byte});
         },
         else => |e| return e,
     };
 
-    const src = tree.token_locs[str_lit.token].start;
-    return addZIRInst(mod, scope, src, zir.Inst.Str, .{ .bytes = bytes }, .{});
+    const src = token_starts[str_lit_token];
+    const str_inst = try addZIRInst(mod, scope, src, zir.Inst.Str, .{ .bytes = bytes }, .{});
+    return rvalue(mod, scope, rl, str_inst);
 }
 
-fn multilineStrLiteral(mod: *Module, scope: *Scope, node: *ast.Node.MultilineStringLiteral) !*zir.Inst {
+fn multilineStringLiteral(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    str_lit: ast.Node.Index,
+) InnerError!*zir.Inst {
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const lines = node.linesConst();
-    const src = tree.token_locs[lines[0]].start;
 
-    // line lengths and new lines
-    var len = lines.len - 1;
-    for (lines) |line| {
-        // 2 for the '//' + 1 for '\n'
-        len += tree.tokenSlice(line).len - 3;
-    }
+    const start = node_datas[node].lhs;
+    const end = node_datas[node].rhs;
 
-    const bytes = try scope.arena().alloc(u8, len);
-    var i: usize = 0;
-    for (lines) |line, line_i| {
-        if (line_i != 0) {
-            bytes[i] = '\n';
-            i += 1;
+    // Count the number of bytes to allocate.
+    const len: usize = len: {
+        var tok_i = start;
+        var len: usize = 0;
+        while (tok_i <= end) : (tok_i += 1) {
+            // 2 for the '//' + 1 for '\n'
+            len += tree.tokenSlice(tok_i).len - 3;
         }
-        const slice = tree.tokenSlice(line);
-        mem.copy(u8, bytes[i..], slice[2 .. slice.len - 1]);
-        i += slice.len - 3;
-    }
-
-    return addZIRInst(mod, scope, src, zir.Inst.Str, .{ .bytes = bytes }, .{});
+        break :len len;
+    };
+    const bytes = try scope.arena().alloc(u8, len);
+    // First line: do not append a newline.
+    var byte_i: usize = 0;
+    var tok_i = start;
+    {
+        const slice = tree.tokenSlice(tok_i);
+        const line_bytes = slice[2 .. slice.len - 1];
+        mem.copy(u8, bytes[byte_i..], line_bytes);
+        byte_i += line_bytes.len;
+        tok_i += 1;
+    }
+    // Following lines: each line prepends a newline.
+    while (tok_i <= end) : (tok_i += 1) {
+        bytes[byte_i] = '\n';
+        byte_i += 1;
+        const slice = tree.tokenSlice(tok_i);
+        const line_bytes = slice[2 .. slice.len - 1];
+        mem.copy(u8, bytes[byte_i..], line_bytes);
+        byte_i += line_bytes.len;
+    }
+    const src = token_starts[start];
+    const str_inst = try addZIRInst(mod, scope, src, zir.Inst.Str, .{ .bytes = bytes }, .{});
+    return rvalue(mod, scope, rl, str_inst);
 }
 
 fn charLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) !*zir.Inst {
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[node.token].start;
+    const src = token_starts[node.token];
     const slice = tree.tokenSlice(node.token);
 
     var bad_index: usize = undefined;
@@ -2772,13 +2847,19 @@ fn charLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) !*zir.Inst
     });
 }
 
-fn integerLiteral(mod: *Module, scope: *Scope, int_lit: *ast.Node.OneToken) InnerError!*zir.Inst {
+fn integerLiteral(
+    mod: *Module,
+    scope: *Scope,
+    rl: ResultLoc,
+    int_lit: ast.Node.Index,
+) InnerError!*zir.Inst {
     const arena = scope.arena();
     const tree = scope.tree();
-    const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const prefixed_bytes = tree.tokenSlice(int_lit.token);
-    const base = if (mem.startsWith(u8, prefixed_bytes, "0x"))
+
+    const int_token = main_tokens[int_lit];
+    const prefixed_bytes = tree.tokenSlice(int_token);
+    const base: u8 = if (mem.startsWith(u8, prefixed_bytes, "0x"))
         16
     else if (mem.startsWith(u8, prefixed_bytes, "0o"))
         8
@@ -2793,13 +2874,14 @@ fn integerLiteral(mod: *Module, scope: *Scope, int_lit: *ast.Node.OneToken) Inne
         prefixed_bytes[2..];
 
     if (std.fmt.parseInt(u64, bytes, base)) |small_int| {
-        const src = tree.token_locs[int_lit.token].start;
-        return addZIRInstConst(mod, scope, src, .{
+        const src = token_starts[int_token];
+        const result = try addZIRInstConst(mod, scope, src, .{
             .ty = Type.initTag(.comptime_int),
             .val = try Value.Tag.int_u64.create(arena, small_int),
         });
+        return rvalue(mod, scope, rl, result);
     } else |err| {
-        return mod.failTok(scope, int_lit.token, "TODO implement int literals that don't fit in a u64", .{});
+        return mod.failTok(scope, int_token, "TODO implement int literals that don't fit in a u64", .{});
     }
 }
 
@@ -2816,7 +2898,7 @@ fn floatLiteral(mod: *Module, scope: *Scope, float_lit: *ast.Node.OneToken) Inne
     const float_number = std.fmt.parseFloat(f128, bytes) catch |e| switch (e) {
         error.InvalidCharacter => unreachable, // validated by tokenizer
     };
-    const src = tree.token_locs[float_lit.token].start;
+    const src = token_starts[float_lit.token];
     return addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.comptime_float),
         .val = try Value.Tag.float_128.create(arena, float_number),
@@ -2828,7 +2910,7 @@ fn undefLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerErro
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[node.token].start;
+    const src = token_starts[node.token];
     return addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.@"undefined"),
         .val = Value.initTag(.undef),
@@ -2840,7 +2922,7 @@ fn boolLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerError
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[node.token].start;
+    const src = token_starts[node.token];
     return addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.bool),
         .val = switch (tree.token_ids[node.token]) {
@@ -2856,34 +2938,34 @@ fn nullLiteral(mod: *Module, scope: *Scope, node: *ast.Node.OneToken) InnerError
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[node.token].start;
+    const src = token_starts[node.token];
     return addZIRInstConst(mod, 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 {
-    if (asm_node.outputs.len != 0) {
-        return mod.failNode(scope, &asm_node.base, "TODO implement asm with an output", .{});
-    }
+fn assembly(mod: *Module, scope: *Scope, rl: ResultLoc, full: ast.full.Asm) InnerError!*zir.Inst {
     const arena = scope.arena();
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
 
-    const inputs = try arena.alloc(*zir.Inst, asm_node.inputs.len);
-    const args = try arena.alloc(*zir.Inst, asm_node.inputs.len);
+    if (full.outputs.len != 0) {
+        return mod.failTok(scope, full.ast.asm_token, "TODO implement asm with an output", .{});
+    }
 
-    const src = tree.token_locs[asm_node.asm_token].start;
+    const inputs = try arena.alloc(*zir.Inst, full.inputs.len);
+    const args = try arena.alloc(*zir.Inst, full.inputs.len);
 
+    const src = token_starts[full.ast.asm_token];
     const str_type = try addZIRInstConst(mod, 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| {
+    for (full.inputs) |input, i| {
         // TODO semantically analyze constraints
         inputs[i] = try expr(mod, scope, str_type_rl, input.constraint);
         args[i] = try expr(mod, scope, .none, input.expr);
@@ -2894,15 +2976,15 @@ fn assembly(mod: *Module, scope: *Scope, asm_node: *ast.Node.@"asm") InnerError!
         .val = Value.initTag(.void_type),
     });
     const asm_inst = try addZIRInst(mod, scope, src, zir.Inst.@"asm", .{
-        .asm_source = try expr(mod, scope, str_type_rl, asm_node.template),
+        .asm_source = try expr(mod, scope, str_type_rl, full.ast.template),
         .return_type = return_type,
     }, .{
-        .@"volatile" = asm_node.volatile_token != null,
+        .@"volatile" = full.volatile_token != null,
         //.clobbers =  TODO handle clobbers
         .inputs = inputs,
         .args = args,
     });
-    return asm_inst;
+    return rvalue(mod, scope, rl, asm_inst);
 }
 
 fn ensureBuiltinParamCount(mod: *Module, scope: *Scope, call: *ast.Node.builtin_call, count: u32) !void {
@@ -2924,7 +3006,7 @@ fn simpleCast(
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     const dest_type = try typeExpr(mod, scope, params[0]);
     const rhs = try expr(mod, scope, .none, params[1]);
@@ -2938,7 +3020,7 @@ fn ptrToInt(mod: *Module, scope: *Scope, call: *ast.Node.builtin_call) InnerErro
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     return addZIRUnOp(mod, scope, src, .ptrtoint, operand);
 }
 
@@ -2952,7 +3034,7 @@ fn as(
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     const dest_type = try typeExpr(mod, scope, params[0]);
     switch (rl) {
@@ -3028,7 +3110,7 @@ fn bitCast(mod: *Module, scope: *Scope, rl: ResultLoc, call: *ast.Node.builtin_c
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     const dest_type = try typeExpr(mod, scope, params[0]);
     switch (rl) {
@@ -3074,7 +3156,7 @@ fn import(mod: *Module, scope: *Scope, call: *ast.Node.builtin_call) InnerError!
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     const target = try expr(mod, scope, .none, params[0]);
     return addZIRUnOp(mod, scope, src, .import, target);
@@ -3085,7 +3167,7 @@ fn compileError(mod: *Module, scope: *Scope, call: *ast.Node.builtin_call) Inner
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     const target = try expr(mod, scope, .none, params[0]);
     return addZIRUnOp(mod, scope, src, .compile_error, target);
@@ -3096,7 +3178,7 @@ fn setEvalBranchQuota(mod: *Module, scope: *Scope, call: *ast.Node.builtin_call)
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     const u32_type = try addZIRInstConst(mod, scope, src, .{
         .ty = Type.initTag(.type),
@@ -3111,7 +3193,7 @@ fn typeOf(mod: *Module, scope: *Scope, rl: ResultLoc, call: *ast.Node.builtin_ca
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
     const arena = scope.arena();
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     if (params.len < 1) {
         return mod.failTok(scope, call.builtin_token, "expected at least 1 argument, found 0", .{});
@@ -3129,7 +3211,7 @@ fn compileLog(mod: *Module, scope: *Scope, call: *ast.Node.builtin_call) InnerEr
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
     const arena = scope.arena();
-    const src = tree.token_locs[call.builtin_token].start;
+    const src = token_starts[call.builtin_token];
     const params = call.params();
     var targets = try arena.alloc(*zir.Inst, params.len);
     for (params) |param, param_i|
@@ -3161,7 +3243,7 @@ fn builtinCall(mod: *Module, scope: *Scope, rl: ResultLoc, call: *ast.Node.built
     } else if (mem.eql(u8, builtin_name, "@TypeOf")) {
         return typeOf(mod, scope, rl, call);
     } else if (mem.eql(u8, builtin_name, "@breakpoint")) {
-        const src = tree.token_locs[call.builtin_token].start;
+        const src = token_starts[call.builtin_token];
         return rvalue(mod, scope, rl, try addZIRNoOp(mod, scope, src, .breakpoint));
     } else if (mem.eql(u8, builtin_name, "@import")) {
         return rvalue(mod, scope, rl, try import(mod, scope, call));
@@ -3187,7 +3269,7 @@ fn callExpr(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.call) In
     const param_nodes = node.params();
     const args = try scope.getGenZIR().arena.alloc(*zir.Inst, param_nodes.len);
     for (param_nodes) |param_node, i| {
-        const param_src = tree.token_locs[param_node.firstToken()].start;
+        const param_src = token_starts[tree.firstToken(param_node)];
         const param_type = try addZIRInst(mod, scope, param_src, zir.Inst.ParamType, .{
             .func = lhs,
             .arg_index = i,
@@ -3195,7 +3277,7 @@ fn callExpr(mod: *Module, scope: *Scope, rl: ResultLoc, node: *ast.Node.call) In
         args[i] = try expr(mod, scope, .{ .ty = param_type }, param_node);
     }
 
-    const src = tree.token_locs[node.lhs.firstToken()].start;
+    const src = token_starts[node.lhs.firstToken()];
     const result = try addZIRInst(mod, scope, src, zir.Inst.Call, .{
         .func = lhs,
         .args = args,
@@ -3208,7 +3290,7 @@ fn unreach(mod: *Module, scope: *Scope, unreach_node: *ast.Node.OneToken) InnerE
     const tree = scope.tree();
     const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
-    const src = tree.token_locs[unreach_node.token].start;
+    const src = token_starts[unreach_node.token];
     return addZIRNoOp(mod, scope, src, .unreachable_safe);
 }
 
@@ -3528,6 +3610,8 @@ fn rvalue(mod: *Module, scope: *Scope, rl: ResultLoc, result: *zir.Inst) InnerEr
     }
 }
 
+/// TODO when reworking ZIR memory layout, make the void value correspond to a hard coded
+/// index; that way this does not actually need to allocate anything.
 fn rvalueVoid(
     mod: *Module,
     scope: *Scope,