Commit 38b83d9d93

mlugg <mlugg@mlugg.co.uk>
2023-05-13 18:10:05
Zir: eliminate `field_call_bind` and `field_call_bind_named`
This commit removes the `field_call_bind` and `field_call_bind_named` ZIR instructions, replacing them with a `field_call` instruction which does the bind and call in one. `field_call_bind` is an unfortunate instruction. It's tied into one very specific usage pattern - its result can only be used as a callee. This means that it creates a value of a "pseudo-type" of sorts, `bound_fn` - this type used to exist in Zig, but now we just hide it from the user and have AstGen ensure it's only used in one way. This is quite silly - `Type` and `Value` should, as much as possible, reflect real Zig types and values. It makes sense to instead encode the `a.b()` syntax as its own ZIR instruction, so that's what we do here. This commit introduces a new instruction, `field_call`. It's like `call`, but rather than a callee ref, it contains a ref to the object pointer (`&a` in `a.b()`) and the string field name (`b`). This eliminates `bound_fn` from the language, and slightly decreases the size of generated ZIR - stats below. This commit does remove a few usages which used to be allowed: - `@field(a, "b")()` - `@call(.auto, a.b, .{})` - `@call(.auto, @field(a, "b"), .{})` These forms used to work just like `a.b()`, but are no longer allowed. I believe this is the correct choice for a few reasons: - `a.b()` is a purely *syntactic* form; for instance, `(a.b)()` is not valid. This means it is *not* inconsistent to not allow it in these cases; the special case here isn't "a field access as a callee", but rather this exact syntactic form. - The second argument to `@call` looks much more visually distinct from the callee in standard call syntax. To me, this makes it seem strange for that argument to not work like a normal expression in this context. - A more practical argument: it's confusing! `@field` and `@call` are used in very different contexts to standard function calls: the former normally hints at some comptime machinery, and the latter that you want more precise control over parts of a function call. In these contexts, you don't want implicit arguments adding extra confusion: you want to be very explicit about what you're doing. Lastly, some stats. I mentioned before that this change slightly reduces the size of ZIR - this is due to two instructions (`field_call_bind` then `call`) being replaced with one (`field_call`). Here are some numbers: +--------------+----------+----------+--------+ | File | Before | After | Change | +--------------+----------+----------+--------+ | Sema.zig | 4.72M | 4.53M | -4% | | AstGen.zig | 1.52M | 1.48M | -3% | | hash_map.zig | 283.9K | 276.2K | -3% | | math.zig | 312.6K | 305.3K | -2% | +--------------+----------+----------+--------+
1 parent 7077e90
lib/std/crypto/siphash.zig
@@ -167,8 +167,8 @@ fn SipHashStateless(comptime T: type, comptime c_rounds: usize, comptime d_round
         pub fn hash(msg: []const u8, key: *const [key_length]u8) T {
             const aligned_len = msg.len - (msg.len % 8);
             var c = Self.init(key);
-            @call(.always_inline, c.update, .{msg[0..aligned_len]});
-            return @call(.always_inline, c.final, .{msg[aligned_len..]});
+            @call(.always_inline, update, .{ &c, msg[0..aligned_len] });
+            return @call(.always_inline, final, .{ &c, msg[aligned_len..] });
         }
     };
 }
lib/std/hash/auto_hash.zig
@@ -64,9 +64,13 @@ pub fn hashArray(hasher: anytype, key: anytype, comptime strat: HashStrategy) vo
 /// Strategy is provided to determine if pointers should be followed or not.
 pub fn hash(hasher: anytype, key: anytype, comptime strat: HashStrategy) void {
     const Key = @TypeOf(key);
+    const Hasher = switch (@typeInfo(@TypeOf(hasher))) {
+        .Pointer => |ptr| ptr.child,
+        else => @TypeOf(hasher),
+    };
 
     if (strat == .Shallow and comptime meta.trait.hasUniqueRepresentation(Key)) {
-        @call(.always_inline, hasher.update, .{mem.asBytes(&key)});
+        @call(.always_inline, Hasher.update, .{ hasher, mem.asBytes(&key) });
         return;
     }
 
@@ -89,12 +93,12 @@ pub fn hash(hasher: anytype, key: anytype, comptime strat: HashStrategy) void {
         // TODO Check if the situation is better after #561 is resolved.
         .Int => {
             if (comptime meta.trait.hasUniqueRepresentation(Key)) {
-                @call(.always_inline, hasher.update, .{std.mem.asBytes(&key)});
+                @call(.always_inline, Hasher.update, .{ hasher, std.mem.asBytes(&key) });
             } else {
                 // Take only the part containing the key value, the remaining
                 // bytes are undefined and must not be hashed!
                 const byte_size = comptime std.math.divCeil(comptime_int, @bitSizeOf(Key), 8) catch unreachable;
-                @call(.always_inline, hasher.update, .{std.mem.asBytes(&key)[0..byte_size]});
+                @call(.always_inline, Hasher.update, .{ hasher, std.mem.asBytes(&key)[0..byte_size] });
             }
         },
 
lib/std/hash/wyhash.zig
@@ -65,7 +65,7 @@ const WyhashStateless = struct {
 
         var off: usize = 0;
         while (off < b.len) : (off += 32) {
-            @call(.always_inline, self.round, .{b[off..][0..32]});
+            @call(.always_inline, round, .{ self, b[off..][0..32] });
         }
 
         self.msg_len += b.len;
@@ -121,8 +121,8 @@ const WyhashStateless = struct {
         const aligned_len = input.len - (input.len % 32);
 
         var c = WyhashStateless.init(seed);
-        @call(.always_inline, c.update, .{input[0..aligned_len]});
-        return @call(.always_inline, c.final, .{input[aligned_len..]});
+        @call(.always_inline, update, .{ &c, input[0..aligned_len] });
+        return @call(.always_inline, final, .{ &c, input[aligned_len..] });
     }
 };
 
lib/std/Thread/Mutex.zig
@@ -169,7 +169,7 @@ const FutexImpl = struct {
         }
     }
 
-    inline fn lockFast(self: *@This(), comptime casFn: []const u8) bool {
+    inline fn lockFast(self: *@This(), comptime cas_fn_name: []const u8) bool {
         // On x86, use `lock bts` instead of `lock cmpxchg` as:
         // - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048
         // - `lock bts` is smaller instruction-wise which makes it better for inlining
@@ -180,7 +180,8 @@ const FutexImpl = struct {
 
         // Acquire barrier ensures grabbing the lock happens before the critical section
         // and that the previous lock holder's critical section happens before we grab the lock.
-        return @field(self.state, casFn)(unlocked, locked, .Acquire, .Monotonic) == null;
+        const casFn = @field(@TypeOf(self.state), cas_fn_name);
+        return casFn(&self.state, unlocked, locked, .Acquire, .Monotonic) == null;
     }
 
     fn lockSlow(self: *@This()) void {
src/AstGen.zig
@@ -2482,7 +2482,7 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
         switch (zir_tags[inst]) {
             // For some instructions, modify the zir data
             // so we can avoid a separate ensure_result_used instruction.
-            .call => {
+            .call, .field_call => {
                 const extra_index = gz.astgen.instructions.items(.data)[inst].pl_node.payload_index;
                 const slot = &gz.astgen.extra.items[extra_index];
                 var flags = @bitCast(Zir.Inst.Call.Flags, slot.*);
@@ -2557,7 +2557,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
             .field_ptr,
             .field_ptr_init,
             .field_val,
-            .field_call_bind,
             .field_ptr_named,
             .field_val_named,
             .func,
@@ -8516,7 +8515,7 @@ fn builtinCall(
         },
         .call => {
             const modifier = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .modifier_type } }, params[0]);
-            const callee = try calleeExpr(gz, scope, params[1]);
+            const callee = try expr(gz, scope, .{ .rl = .none }, params[1]);
             const args = try expr(gz, scope, .{ .rl = .none }, params[2]);
             const result = try gz.addPlNode(.builtin_call, node, Zir.Inst.BuiltinCall{
                 .modifier = modifier,
@@ -8976,7 +8975,10 @@ fn callExpr(
         } });
     }
 
-    assert(callee != .none);
+    switch (callee) {
+        .direct => |obj| assert(obj != .none),
+        .field => |field| assert(field.obj_ptr != .none),
+    }
     assert(node != 0);
 
     const call_index = @intCast(Zir.Inst.Index, astgen.instructions.len);
@@ -9015,89 +9017,98 @@ fn callExpr(
         else => false,
     };
 
-    const payload_index = try addExtra(astgen, Zir.Inst.Call{
-        .callee = callee,
-        .flags = .{
-            .pop_error_return_trace = !propagate_error_trace,
-            .packed_modifier = @intCast(Zir.Inst.Call.Flags.PackedModifier, @enumToInt(modifier)),
-            .args_len = @intCast(Zir.Inst.Call.Flags.PackedArgsLen, call.ast.params.len),
+    switch (callee) {
+        .direct => |callee_obj| {
+            const payload_index = try addExtra(astgen, Zir.Inst.Call{
+                .callee = callee_obj,
+                .flags = .{
+                    .pop_error_return_trace = !propagate_error_trace,
+                    .packed_modifier = @intCast(Zir.Inst.Call.Flags.PackedModifier, @enumToInt(modifier)),
+                    .args_len = @intCast(Zir.Inst.Call.Flags.PackedArgsLen, call.ast.params.len),
+                },
+            });
+            if (call.ast.params.len != 0) {
+                try astgen.extra.appendSlice(astgen.gpa, astgen.scratch.items[scratch_top..]);
+            }
+            gz.astgen.instructions.set(call_index, .{
+                .tag = .call,
+                .data = .{ .pl_node = .{
+                    .src_node = gz.nodeIndexToRelative(node),
+                    .payload_index = payload_index,
+                } },
+            });
+        },
+        .field => |callee_field| {
+            const payload_index = try addExtra(astgen, Zir.Inst.FieldCall{
+                .obj_ptr = callee_field.obj_ptr,
+                .field_name_start = callee_field.field_name_start,
+                .flags = .{
+                    .pop_error_return_trace = !propagate_error_trace,
+                    .packed_modifier = @intCast(Zir.Inst.Call.Flags.PackedModifier, @enumToInt(modifier)),
+                    .args_len = @intCast(Zir.Inst.Call.Flags.PackedArgsLen, call.ast.params.len),
+                },
+            });
+            if (call.ast.params.len != 0) {
+                try astgen.extra.appendSlice(astgen.gpa, astgen.scratch.items[scratch_top..]);
+            }
+            gz.astgen.instructions.set(call_index, .{
+                .tag = .field_call,
+                .data = .{ .pl_node = .{
+                    .src_node = gz.nodeIndexToRelative(node),
+                    .payload_index = payload_index,
+                } },
+            });
         },
-    });
-    if (call.ast.params.len != 0) {
-        try astgen.extra.appendSlice(astgen.gpa, astgen.scratch.items[scratch_top..]);
     }
-    gz.astgen.instructions.set(call_index, .{
-        .tag = .call,
-        .data = .{ .pl_node = .{
-            .src_node = gz.nodeIndexToRelative(node),
-            .payload_index = payload_index,
-        } },
-    });
     return rvalue(gz, ri, call_inst, node); // TODO function call with result location
 }
 
-/// calleeExpr generates the function part of a call expression (f in f(x)), or the
-/// callee argument to the @call() builtin. If the lhs is a field access or the
-/// @field() builtin, we need to generate a special field_call_bind instruction
-/// instead of the normal field_val or field_ptr.  If this is a inst.func() call,
-/// this instruction will capture the value of the first argument before evaluating
-/// the other arguments. We need to use .ref here to guarantee we will be able to
-/// promote an lvalue to an address if the first parameter requires it.  This
-/// unfortunately also means we need to take a reference to any types on the lhs.
+const Callee = union(enum) {
+    field: struct {
+        /// A *pointer* to the object the field is fetched on, so that we can
+        /// promote the lvalue to an address if the first parameter requires it.
+        obj_ptr: Zir.Inst.Ref,
+        /// Offset into `string_bytes`.
+        field_name_start: u32,
+    },
+    direct: Zir.Inst.Ref,
+};
+
+/// calleeExpr generates the function part of a call expression (f in f(x)), but
+/// *not* the callee argument to the @call() builtin. Its purpose is to
+/// distinguish between standard calls and method call syntax `a.b()`. Thus, if
+/// the lhs is a field access, we return using the `field` union field;
+/// otherwise, we use the `direct` union field.
 fn calleeExpr(
     gz: *GenZir,
     scope: *Scope,
     node: Ast.Node.Index,
-) InnerError!Zir.Inst.Ref {
+) InnerError!Callee {
     const astgen = gz.astgen;
     const tree = astgen.tree;
 
     const tag = tree.nodes.items(.tag)[node];
     switch (tag) {
-        .field_access => return addFieldAccess(.field_call_bind, gz, scope, .{ .rl = .ref }, node),
-
-        .builtin_call_two,
-        .builtin_call_two_comma,
-        .builtin_call,
-        .builtin_call_comma,
-        => {
-            const node_datas = tree.nodes.items(.data);
+        .field_access => {
             const main_tokens = tree.nodes.items(.main_token);
-            const builtin_token = main_tokens[node];
-            const builtin_name = tree.tokenSlice(builtin_token);
-
-            var inline_params: [2]Ast.Node.Index = undefined;
-            var params: []Ast.Node.Index = switch (tag) {
-                .builtin_call,
-                .builtin_call_comma,
-                => tree.extra_data[node_datas[node].lhs..node_datas[node].rhs],
-
-                .builtin_call_two,
-                .builtin_call_two_comma,
-                => blk: {
-                    inline_params = .{ node_datas[node].lhs, node_datas[node].rhs };
-                    const len: usize = if (inline_params[0] == 0) @as(usize, 0) else if (inline_params[1] == 0) @as(usize, 1) else @as(usize, 2);
-                    break :blk inline_params[0..len];
-                },
-
-                else => unreachable,
-            };
+            const node_datas = tree.nodes.items(.data);
+            const object_node = node_datas[node].lhs;
+            const dot_token = main_tokens[node];
+            const field_ident = dot_token + 1;
+            const str_index = try astgen.identAsString(field_ident);
+            // Capture the object by reference so we can promote it to an
+            // address in Sema if needed.
+            const lhs = try expr(gz, scope, .{ .rl = .ref }, object_node);
 
-            // If anything is wrong, fall back to builtinCall.
-            // It will emit any necessary compile errors and notes.
-            if (std.mem.eql(u8, builtin_name, "@field") and params.len == 2) {
-                const lhs = try expr(gz, scope, .{ .rl = .ref }, params[0]);
-                const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .ty = .const_slice_u8_type } }, params[1]);
-                return gz.addExtendedPayload(.field_call_bind_named, Zir.Inst.FieldNamedNode{
-                    .node = gz.nodeIndexToRelative(node),
-                    .lhs = lhs,
-                    .field_name = field_name,
-                });
-            }
+            const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
+            try emitDbgStmt(gz, cursor);
 
-            return builtinCall(gz, scope, .{ .rl = .none }, node, params);
+            return .{ .field = .{
+                .obj_ptr = lhs,
+                .field_name_start = str_index,
+            } };
         },
-        else => return expr(gz, scope, .{ .rl = .none }, node),
+        else => return .{ .direct = try expr(gz, scope, .{ .rl = .none }, node) },
     }
 }
 
src/Autodoc.zig
@@ -2141,7 +2141,7 @@ fn walkInstruction(
                 .expr = .{ .declRef = decl_status },
             };
         },
-        .field_val, .field_call_bind, .field_ptr, .field_type => {
+        .field_val, .field_ptr, .field_type => {
             // TODO: field type uses Zir.Inst.FieldType, it just happens to have the
             // same layout as Zir.Inst.Field :^)
             const pl_node = data[inst_index].pl_node;
@@ -2163,7 +2163,6 @@ fn walkInstruction(
 
                     const lhs = @enumToInt(lhs_extra.data.lhs) - Ref.typed_value_map.len;
                     if (tags[lhs] != .field_val and
-                        tags[lhs] != .field_call_bind and
                         tags[lhs] != .field_ptr and
                         tags[lhs] != .field_type) break :blk lhs_extra.data.lhs;
 
@@ -2191,7 +2190,7 @@ fn walkInstruction(
             const wr = blk: {
                 if (@enumToInt(lhs_ref) >= Ref.typed_value_map.len) {
                     const lhs_inst = @enumToInt(lhs_ref) - Ref.typed_value_map.len;
-                    if (tags[lhs_inst] == .call) {
+                    if (tags[lhs_inst] == .call or tags[lhs_inst] == .field_call) {
                         break :blk DocData.WalkResult{
                             .expr = .{
                                 .comptimeExpr = 0,
src/Module.zig
@@ -2489,8 +2489,21 @@ pub const SrcLoc = struct {
                 const node_datas = tree.nodes.items(.data);
                 const node_tags = tree.nodes.items(.tag);
                 const node = src_loc.declRelativeToNodeIndex(node_off);
+                var buf: [1]Ast.Node.Index = undefined;
                 const tok_index = switch (node_tags[node]) {
                     .field_access => node_datas[node].rhs,
+                    .call_one,
+                    .call_one_comma,
+                    .async_call_one,
+                    .async_call_one_comma,
+                    .call,
+                    .call_comma,
+                    .async_call,
+                    .async_call_comma,
+                    => blk: {
+                        const full = tree.fullCall(&buf, node).?;
+                        break :blk tree.lastToken(full.ast.fn_expr);
+                    },
                     else => tree.firstToken(node) - 2,
                 };
                 const start = tree.tokens.items(.start)[tok_index];
@@ -3083,7 +3096,8 @@ pub const LazySrcLoc = union(enum) {
     /// The payload is offset from the containing Decl AST node.
     /// The source location points to the field name of:
     ///  * a field access expression (`a.b`), or
-    ///  * the operand ("b" node) of a field initialization expression (`.a = b`)
+    ///  * the callee of a method call (`a.b()`), or
+    ///  * the operand ("b" node) of a field initialization expression (`.a = b`), or
     /// The Decl is determined contextually.
     node_offset_field_name: i32,
     /// The source location points to the pointer of a pointer deref expression,
src/print_air.zig
@@ -369,7 +369,6 @@ const Writer = struct {
             .inferred_alloc_const => try s.writeAll("(inferred_alloc_const)"),
             .inferred_alloc_mut => try s.writeAll("(inferred_alloc_mut)"),
             .generic_poison => try s.writeAll("(generic_poison)"),
-            .bound_fn => try s.writeAll("(bound_fn)"),
             else => try ty.print(s, w.module),
         }
     }
src/print_zir.zig
@@ -362,7 +362,8 @@ const Writer = struct {
             .@"export" => try self.writePlNodeExport(stream, inst),
             .export_value => try self.writePlNodeExportValue(stream, inst),
 
-            .call => try self.writeCall(stream, inst),
+            .call => try self.writeCall(stream, inst, .direct),
+            .field_call => try self.writeCall(stream, inst, .field),
 
             .block,
             .block_comptime,
@@ -392,7 +393,6 @@ const Writer = struct {
             .field_ptr,
             .field_ptr_init,
             .field_val,
-            .field_call_bind,
             => try self.writePlNodeField(stream, inst),
 
             .field_ptr_named,
@@ -543,15 +543,6 @@ const Writer = struct {
                 try self.writeSrc(stream, src);
             },
 
-            .field_call_bind_named => {
-                const extra = self.code.extraData(Zir.Inst.FieldNamedNode, extended.operand).data;
-                const src = LazySrcLoc.nodeOffset(extra.node);
-                try self.writeInstRef(stream, extra.lhs);
-                try stream.writeAll(", ");
-                try self.writeInstRef(stream, extra.field_name);
-                try stream.writeAll(") ");
-                try self.writeSrc(stream, src);
-            },
             .builtin_async_call => try self.writeBuiltinAsyncCall(stream, extended),
             .cmpxchg => try self.writeCmpxchg(stream, extended),
         }
@@ -1176,9 +1167,18 @@ const Writer = struct {
         try self.writeSrc(stream, src);
     }
 
-    fn writeCall(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void {
+    fn writeCall(
+        self: *Writer,
+        stream: anytype,
+        inst: Zir.Inst.Index,
+        comptime kind: enum { direct, field },
+    ) !void {
         const inst_data = self.code.instructions.items(.data)[inst].pl_node;
-        const extra = self.code.extraData(Zir.Inst.Call, inst_data.payload_index);
+        const ExtraType = switch (kind) {
+            .direct => Zir.Inst.Call,
+            .field => Zir.Inst.FieldCall,
+        };
+        const extra = self.code.extraData(ExtraType, inst_data.payload_index);
         const args_len = extra.data.flags.args_len;
         const body = self.code.extra[extra.end..];
 
@@ -1186,7 +1186,14 @@ const Writer = struct {
             try stream.writeAll("nodiscard ");
         }
         try stream.print(".{s}, ", .{@tagName(@intToEnum(std.builtin.CallModifier, extra.data.flags.packed_modifier))});
-        try self.writeInstRef(stream, extra.data.callee);
+        switch (kind) {
+            .direct => try self.writeInstRef(stream, extra.data.callee),
+            .field => {
+                const field_name = self.code.nullTerminatedString(extra.data.field_name_start);
+                try self.writeInstRef(stream, extra.data.obj_ptr);
+                try stream.print(", {}", .{std.zig.fmtId(field_name)});
+            },
+        }
         try stream.writeAll(", [");
 
         self.indent += 2;
src/Sema.zig
@@ -920,7 +920,8 @@ fn analyzeBodyInner(
             .bool_br_and                  => try sema.zirBoolBr(block, inst, false),
             .bool_br_or                   => try sema.zirBoolBr(block, inst, true),
             .c_import                     => try sema.zirCImport(block, inst),
-            .call                         => try sema.zirCall(block, inst),
+            .call                         => try sema.zirCall(block, inst, .direct),
+            .field_call                   => try sema.zirCall(block, inst, .field),
             .closure_get                  => try sema.zirClosureGet(block, inst),
             .cmp_lt                       => try sema.zirCmp(block, inst, .lt),
             .cmp_lte                      => try sema.zirCmp(block, inst, .lte),
@@ -952,7 +953,6 @@ fn analyzeBodyInner(
             .field_ptr_named              => try sema.zirFieldPtrNamed(block, inst),
             .field_val                    => try sema.zirFieldVal(block, inst),
             .field_val_named              => try sema.zirFieldValNamed(block, inst),
-            .field_call_bind              => try sema.zirFieldCallBind(block, inst),
             .func                         => try sema.zirFunc(block, inst, false),
             .func_inferred                => try sema.zirFunc(block, inst, true),
             .func_fancy                   => try sema.zirFuncFancy(block, inst),
@@ -1149,7 +1149,6 @@ fn analyzeBodyInner(
                     .wasm_memory_size      => try sema.zirWasmMemorySize(    block, extended),
                     .wasm_memory_grow      => try sema.zirWasmMemoryGrow(    block, extended),
                     .prefetch              => try sema.zirPrefetch(          block, extended),
-                    .field_call_bind_named => try sema.zirFieldCallBindNamed(block, extended),
                     .err_set_cast          => try sema.zirErrSetCast(        block, extended),
                     .await_nosuspend       => try sema.zirAwaitNosuspend(    block, extended),
                     .select                => try sema.zirSelect(            block, extended),
@@ -6262,38 +6261,50 @@ fn zirCall(
     sema: *Sema,
     block: *Block,
     inst: Zir.Inst.Index,
+    comptime kind: enum { direct, field },
 ) CompileError!Air.Inst.Ref {
     const tracy = trace(@src());
     defer tracy.end();
 
     const inst_data = sema.code.instructions.items(.data)[inst].pl_node;
-    const func_src: LazySrcLoc = .{ .node_offset_call_func = inst_data.src_node };
+    const callee_src: LazySrcLoc = .{ .node_offset_call_func = inst_data.src_node };
     const call_src = inst_data.src();
-    const extra = sema.code.extraData(Zir.Inst.Call, inst_data.payload_index);
+    const ExtraType = switch (kind) {
+        .direct => Zir.Inst.Call,
+        .field => Zir.Inst.FieldCall,
+    };
+    const extra = sema.code.extraData(ExtraType, inst_data.payload_index);
     const args_len = extra.data.flags.args_len;
 
     const modifier = @intToEnum(std.builtin.CallModifier, extra.data.flags.packed_modifier);
     const ensure_result_used = extra.data.flags.ensure_result_used;
     const pop_error_return_trace = extra.data.flags.pop_error_return_trace;
 
-    var func = try sema.resolveInst(extra.data.callee);
+    const callee: ResolvedFieldCallee = switch (kind) {
+        .direct => .{ .direct = try sema.resolveInst(extra.data.callee) },
+        .field => blk: {
+            const object_ptr = try sema.resolveInst(extra.data.obj_ptr);
+            const field_name = sema.code.nullTerminatedString(extra.data.field_name_start);
+            const field_name_src: LazySrcLoc = .{ .node_offset_field_name = inst_data.src_node };
+            break :blk try sema.fieldCallBind(block, callee_src, object_ptr, field_name, field_name_src);
+        },
+    };
     var resolved_args: []Air.Inst.Ref = undefined;
-    var arg_index: u32 = 0;
-
-    const func_type = sema.typeOf(func);
-
-    // Desugar bound functions here
     var bound_arg_src: ?LazySrcLoc = null;
-    if (func_type.tag() == .bound_fn) {
-        bound_arg_src = func_src;
-        const bound_func = try sema.resolveValue(block, .unneeded, func, "");
-        const bound_data = &bound_func.cast(Value.Payload.BoundFn).?.data;
-        func = bound_data.func_inst;
-        resolved_args = try sema.arena.alloc(Air.Inst.Ref, args_len + 1);
-        resolved_args[arg_index] = bound_data.arg0_inst;
-        arg_index += 1;
-    } else {
-        resolved_args = try sema.arena.alloc(Air.Inst.Ref, args_len);
+    var func: Air.Inst.Ref = undefined;
+    var arg_index: u32 = 0;
+    switch (callee) {
+        .direct => |func_inst| {
+            resolved_args = try sema.arena.alloc(Air.Inst.Ref, args_len);
+            func = func_inst;
+        },
+        .method => |method| {
+            resolved_args = try sema.arena.alloc(Air.Inst.Ref, args_len + 1);
+            func = method.func_inst;
+            resolved_args[0] = method.arg0_inst;
+            arg_index += 1;
+            bound_arg_src = callee_src;
+        },
     }
 
     const callee_ty = sema.typeOf(func);
@@ -6308,10 +6319,11 @@ fn zirCall(
             },
             else => {},
         }
-        return sema.fail(block, func_src, "type '{}' not a function", .{callee_ty.fmt(sema.mod)});
+        return sema.fail(block, callee_src, "type '{}' not a function", .{callee_ty.fmt(sema.mod)});
     };
+
     const total_args = args_len + @boolToInt(bound_arg_src != null);
-    try sema.checkCallArgumentCount(block, func, func_src, func_ty, total_args, bound_arg_src != null);
+    try sema.checkCallArgumentCount(block, func, callee_src, func_ty, total_args, bound_arg_src != null);
 
     const args_body = sema.code.extra[extra.end..];
 
@@ -6369,7 +6381,7 @@ fn zirCall(
         !block.is_comptime and !block.is_typeof and (input_is_error or pop_error_return_trace))
     {
         const call_inst: Air.Inst.Ref = if (modifier == .always_tail) undefined else b: {
-            break :b try sema.analyzeCall(block, func, func_ty, func_src, call_src, modifier, ensure_result_used, resolved_args, bound_arg_src, call_dbg_node);
+            break :b try sema.analyzeCall(block, func, func_ty, callee_src, call_src, modifier, ensure_result_used, resolved_args, bound_arg_src, call_dbg_node);
         };
 
         const return_ty = sema.typeOf(call_inst);
@@ -6398,11 +6410,11 @@ fn zirCall(
         }
 
         if (modifier == .always_tail) // Perform the call *after* the restore, so that a tail call is possible.
-            return sema.analyzeCall(block, func, func_ty, func_src, call_src, modifier, ensure_result_used, resolved_args, bound_arg_src, call_dbg_node);
+            return sema.analyzeCall(block, func, func_ty, callee_src, call_src, modifier, ensure_result_used, resolved_args, bound_arg_src, call_dbg_node);
 
         return call_inst;
     } else {
-        return sema.analyzeCall(block, func, func_ty, func_src, call_src, modifier, ensure_result_used, resolved_args, bound_arg_src, call_dbg_node);
+        return sema.analyzeCall(block, func, func_ty, callee_src, call_src, modifier, ensure_result_used, resolved_args, bound_arg_src, call_dbg_node);
     }
 }
 
@@ -9467,19 +9479,6 @@ fn zirFieldPtr(sema: *Sema, block: *Block, inst: Zir.Inst.Index, initializing: b
     return sema.fieldPtr(block, src, object_ptr, field_name, field_name_src, initializing);
 }
 
-fn zirFieldCallBind(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const inst_data = sema.code.instructions.items(.data)[inst].pl_node;
-    const src = inst_data.src();
-    const field_name_src: LazySrcLoc = .{ .node_offset_field_name = inst_data.src_node };
-    const extra = sema.code.extraData(Zir.Inst.Field, inst_data.payload_index).data;
-    const field_name = sema.code.nullTerminatedString(extra.field_name_start);
-    const object_ptr = try sema.resolveInst(extra.lhs);
-    return sema.fieldCallBind(block, src, object_ptr, field_name, field_name_src);
-}
-
 fn zirFieldValNamed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
     const tracy = trace(@src());
     defer tracy.end();
@@ -9506,18 +9505,6 @@ fn zirFieldPtrNamed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErr
     return sema.fieldPtr(block, src, object_ptr, field_name, field_name_src, false);
 }
 
-fn zirFieldCallBindNamed(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!Air.Inst.Ref {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const extra = sema.code.extraData(Zir.Inst.FieldNamedNode, extended.operand).data;
-    const src = LazySrcLoc.nodeOffset(extra.node);
-    const field_name_src: LazySrcLoc = .{ .node_offset_builtin_call_arg1 = extra.node };
-    const object_ptr = try sema.resolveInst(extra.lhs);
-    const field_name = try sema.resolveConstString(block, field_name_src, extra.field_name, "field name must be comptime-known");
-    return sema.fieldCallBind(block, src, object_ptr, field_name, field_name_src);
-}
-
 fn zirIntCast(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
     const tracy = trace(@src());
     defer tracy.end();
@@ -21673,25 +21660,9 @@ fn zirBuiltinCall(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
         return sema.fail(block, args_src, "expected a tuple, found '{}'", .{args_ty.fmt(sema.mod)});
     }
 
-    var resolved_args: []Air.Inst.Ref = undefined;
-
-    // Desugar bound functions here
-    var bound_arg_src: ?LazySrcLoc = null;
-    if (sema.typeOf(func).tag() == .bound_fn) {
-        bound_arg_src = func_src;
-        const bound_func = try sema.resolveValue(block, .unneeded, func, "");
-        const bound_data = &bound_func.cast(Value.Payload.BoundFn).?.data;
-        func = bound_data.func_inst;
-        resolved_args = try sema.arena.alloc(Air.Inst.Ref, args_ty.structFieldCount() + 1);
-        resolved_args[0] = bound_data.arg0_inst;
-        for (resolved_args[1..], 0..) |*resolved, i| {
-            resolved.* = try sema.tupleFieldValByIndex(block, args_src, args, @intCast(u32, i), args_ty);
-        }
-    } else {
-        resolved_args = try sema.arena.alloc(Air.Inst.Ref, args_ty.structFieldCount());
-        for (resolved_args, 0..) |*resolved, i| {
-            resolved.* = try sema.tupleFieldValByIndex(block, args_src, args, @intCast(u32, i), args_ty);
-        }
+    var resolved_args: []Air.Inst.Ref = try sema.arena.alloc(Air.Inst.Ref, args_ty.structFieldCount());
+    for (resolved_args, 0..) |*resolved, i| {
+        resolved.* = try sema.tupleFieldValByIndex(block, args_src, args, @intCast(u32, i), args_ty);
     }
 
     const callee_ty = sema.typeOf(func);
@@ -21708,10 +21679,10 @@ fn zirBuiltinCall(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
         }
         return sema.fail(block, func_src, "type '{}' not a function", .{callee_ty.fmt(sema.mod)});
     };
-    try sema.checkCallArgumentCount(block, func, func_src, func_ty, resolved_args.len, bound_arg_src != null);
+    try sema.checkCallArgumentCount(block, func, func_src, func_ty, resolved_args.len, false);
 
     const ensure_result_used = extra.flags.ensure_result_used;
-    return sema.analyzeCall(block, func, func_ty, func_src, call_src, modifier, ensure_result_used, resolved_args, bound_arg_src, null);
+    return sema.analyzeCall(block, func, func_ty, func_src, call_src, modifier, ensure_result_used, resolved_args, null, null);
 }
 
 fn zirFieldParentPtr(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -24175,6 +24146,16 @@ fn fieldPtr(
     return sema.failWithInvalidFieldAccess(block, src, object_ty, field_name);
 }
 
+const ResolvedFieldCallee = union(enum) {
+    /// The LHS of the call was an actual field with this value.
+    direct: Air.Inst.Ref,
+    /// This is a method call, with the function and first argument given.
+    method: struct {
+        func_inst: Air.Inst.Ref,
+        arg0_inst: Air.Inst.Ref,
+    },
+};
+
 fn fieldCallBind(
     sema: *Sema,
     block: *Block,
@@ -24182,7 +24163,7 @@ fn fieldCallBind(
     raw_ptr: Air.Inst.Ref,
     field_name: []const u8,
     field_name_src: LazySrcLoc,
-) CompileError!Air.Inst.Ref {
+) CompileError!ResolvedFieldCallee {
     // When editing this function, note that there is corresponding logic to be edited
     // in `fieldVal`. This function takes a pointer and returns a pointer.
 
@@ -24202,7 +24183,6 @@ fn fieldCallBind(
     else
         raw_ptr;
 
-    const arena = sema.arena;
     find_field: {
         switch (concrete_ty.zigTypeTag()) {
             .Struct => {
@@ -24216,7 +24196,7 @@ fn fieldCallBind(
                     return sema.finishFieldCallBind(block, src, ptr_ty, field.ty, field_index, object_ptr);
                 } else if (struct_ty.isTuple()) {
                     if (mem.eql(u8, field_name, "len")) {
-                        return sema.addIntUnsigned(Type.usize, struct_ty.structFieldCount());
+                        return .{ .direct = try sema.addIntUnsigned(Type.usize, struct_ty.structFieldCount()) };
                     }
                     if (std.fmt.parseUnsigned(u32, field_name, 10)) |field_index| {
                         if (field_index >= struct_ty.structFieldCount()) break :find_field;
@@ -24243,7 +24223,7 @@ fn fieldCallBind(
             },
             .Type => {
                 const namespace = try sema.analyzeLoad(block, src, object_ptr, src);
-                return sema.fieldVal(block, src, namespace, field_name, field_name_src);
+                return .{ .direct = try sema.fieldVal(block, src, namespace, field_name, field_name_src) };
             },
             else => {},
         }
@@ -24272,54 +24252,47 @@ fn fieldCallBind(
                                 first_param_type.childType().eql(concrete_ty, sema.mod)))
                         {
                         // zig fmt: on
+                            // Note that if the param type is generic poison, we know that it must
+                            // specifically be `anytype` since it's the first parameter, meaning we
+                            // can safely assume it can be a pointer.
                             // TODO: bound fn calls on rvalues should probably
                             // generate a by-value argument somehow.
-                            const ty = Type.Tag.bound_fn.init();
-                            const value = try Value.Tag.bound_fn.create(arena, .{
+                            return .{ .method = .{
                                 .func_inst = decl_val,
                                 .arg0_inst = object_ptr,
-                            });
-                            return sema.addConstant(ty, value);
+                            } };
                         } else if (first_param_type.eql(concrete_ty, sema.mod)) {
                             const deref = try sema.analyzeLoad(block, src, object_ptr, src);
-                            const ty = Type.Tag.bound_fn.init();
-                            const value = try Value.Tag.bound_fn.create(arena, .{
+                            return .{ .method = .{
                                 .func_inst = decl_val,
                                 .arg0_inst = deref,
-                            });
-                            return sema.addConstant(ty, value);
+                            } };
                         } else if (first_param_type.zigTypeTag() == .Optional) {
                             var opt_buf: Type.Payload.ElemType = undefined;
                             const child = first_param_type.optionalChild(&opt_buf);
                             if (child.eql(concrete_ty, sema.mod)) {
                                 const deref = try sema.analyzeLoad(block, src, object_ptr, src);
-                                const ty = Type.Tag.bound_fn.init();
-                                const value = try Value.Tag.bound_fn.create(arena, .{
+                                return .{ .method = .{
                                     .func_inst = decl_val,
                                     .arg0_inst = deref,
-                                });
-                                return sema.addConstant(ty, value);
+                                } };
                             } else if (child.zigTypeTag() == .Pointer and
                                 child.ptrSize() == .One and
                                 child.childType().eql(concrete_ty, sema.mod))
                             {
-                                const ty = Type.Tag.bound_fn.init();
-                                const value = try Value.Tag.bound_fn.create(arena, .{
+                                return .{ .method = .{
                                     .func_inst = decl_val,
                                     .arg0_inst = object_ptr,
-                                });
-                                return sema.addConstant(ty, value);
+                                } };
                             }
                         } else if (first_param_type.zigTypeTag() == .ErrorUnion and
                             first_param_type.errorUnionPayload().eql(concrete_ty, sema.mod))
                         {
                             const deref = try sema.analyzeLoad(block, src, object_ptr, src);
-                            const ty = Type.Tag.bound_fn.init();
-                            const value = try Value.Tag.bound_fn.create(arena, .{
+                            return .{ .method = .{
                                 .func_inst = decl_val,
                                 .arg0_inst = deref,
-                            });
-                            return sema.addConstant(ty, value);
+                            } };
                         }
                     }
                     break :found_decl decl_idx;
@@ -24351,7 +24324,7 @@ fn finishFieldCallBind(
     field_ty: Type,
     field_index: u32,
     object_ptr: Air.Inst.Ref,
-) CompileError!Air.Inst.Ref {
+) CompileError!ResolvedFieldCallee {
     const arena = sema.arena;
     const ptr_field_ty = try Type.ptr(arena, sema.mod, .{
         .pointee_type = field_ty,
@@ -24362,7 +24335,7 @@ fn finishFieldCallBind(
     const container_ty = ptr_ty.childType();
     if (container_ty.zigTypeTag() == .Struct) {
         if (container_ty.structFieldValueComptime(field_index)) |default_val| {
-            return sema.addConstant(field_ty, default_val);
+            return .{ .direct = try sema.addConstant(field_ty, default_val) };
         }
     }
 
@@ -24375,12 +24348,12 @@ fn finishFieldCallBind(
                 .field_index = field_index,
             }),
         );
-        return sema.analyzeLoad(block, src, pointer, src);
+        return .{ .direct = try sema.analyzeLoad(block, src, pointer, src) };
     }
 
     try sema.requireRuntimeBlock(block, src, null);
     const ptr_inst = try block.addStructFieldPtr(object_ptr, field_index, ptr_field_ty);
-    return sema.analyzeLoad(block, src, ptr_inst, src);
+    return .{ .direct = try sema.analyzeLoad(block, src, ptr_inst, src) };
 }
 
 fn namespaceLookup(
@@ -31281,7 +31254,6 @@ pub fn resolveTypeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
 
         .inferred_alloc_mut => unreachable,
         .inferred_alloc_const => unreachable,
-        .bound_fn => unreachable,
 
         .array,
         .array_sentinel,
@@ -32666,7 +32638,6 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
         .single_const_pointer,
         .single_mut_pointer,
         .pointer,
-        .bound_fn,
         => return null,
 
         .optional => {
@@ -33308,7 +33279,6 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
 
         .inferred_alloc_mut => unreachable,
         .inferred_alloc_const => unreachable,
-        .bound_fn => unreachable,
 
         .array,
         .array_sentinel,
src/type.zig
@@ -156,8 +156,6 @@ pub const Type = extern union {
             .union_tagged,
             .type_info,
             => return .Union,
-
-            .bound_fn => unreachable,
         }
     }
 
@@ -933,7 +931,6 @@ pub const Type = extern union {
             // for example, a was resolved into .union_tagged but b was one of these tags.
             .type_info => unreachable, // needed to resolve the type before now
 
-            .bound_fn => unreachable,
         }
     }
 
@@ -1242,7 +1239,6 @@ pub const Type = extern union {
             // we can't hash these based on tags because they wouldn't match the expanded version.
             .type_info => unreachable, // needed to resolve the type before now
 
-            .bound_fn => unreachable,
         }
     }
 
@@ -1349,7 +1345,6 @@ pub const Type = extern union {
             .type_info,
             .@"anyframe",
             .generic_poison,
-            .bound_fn,
             => unreachable,
 
             .array_u8,
@@ -1613,7 +1608,6 @@ pub const Type = extern union {
                 .comptime_int,
                 .comptime_float,
                 .noreturn,
-                .bound_fn,
                 => return writer.writeAll(@tagName(t)),
 
                 .enum_literal => return writer.writeAll("@Type(.EnumLiteral)"),
@@ -1949,7 +1943,6 @@ pub const Type = extern union {
             .inferred_alloc_const => unreachable,
             .inferred_alloc_mut => unreachable,
             .generic_poison => unreachable,
-            .bound_fn => unreachable,
 
             // TODO get rid of these Type.Tag values.
             .atomic_order => unreachable,
@@ -2468,7 +2461,6 @@ pub const Type = extern union {
             .enum_literal,
             .empty_struct,
             .empty_struct_literal,
-            .bound_fn,
             // These are function *bodies*, not pointers.
             // Special exceptions have to be made when emitting functions due to
             // this returning false.
@@ -2703,7 +2695,6 @@ pub const Type = extern union {
 
             .inferred_alloc_mut => unreachable,
             .inferred_alloc_const => unreachable,
-            .bound_fn => unreachable,
 
             .array,
             .array_sentinel,
@@ -3182,7 +3173,6 @@ pub const Type = extern union {
             .noreturn,
             .inferred_alloc_const,
             .inferred_alloc_mut,
-            .bound_fn,
             => unreachable,
 
             .generic_poison => unreachable,
@@ -3282,7 +3272,6 @@ pub const Type = extern union {
             .fn_ccc_void_no_args => unreachable, // represents machine code; not a pointer
             .function => unreachable, // represents machine code; not a pointer
             .@"opaque" => unreachable, // no size available
-            .bound_fn => unreachable,
             .noreturn => unreachable,
             .inferred_alloc_const => unreachable,
             .inferred_alloc_mut => unreachable,
@@ -3630,7 +3619,6 @@ pub const Type = extern union {
             .inferred_alloc_mut => unreachable,
             .@"opaque" => unreachable,
             .generic_poison => unreachable,
-            .bound_fn => unreachable,
 
             .void => return 0,
             .bool, .u1 => return 1,
@@ -5042,7 +5030,6 @@ pub const Type = extern union {
             .single_const_pointer,
             .single_mut_pointer,
             .pointer,
-            .bound_fn,
             => return null,
 
             .optional => {
@@ -5245,7 +5232,6 @@ pub const Type = extern union {
 
             .inferred_alloc_mut => unreachable,
             .inferred_alloc_const => unreachable,
-            .bound_fn => unreachable,
 
             .array,
             .array_sentinel,
@@ -6081,7 +6067,6 @@ pub const Type = extern union {
         inferred_alloc_mut,
         /// Same as `inferred_alloc_mut` but the local is `var` not `const`.
         inferred_alloc_const, // See last_no_payload_tag below.
-        bound_fn,
         // After this, the tag requires a payload.
 
         array_u8,
@@ -6126,7 +6111,7 @@ pub const Type = extern union {
         enum_full,
         enum_nonexhaustive,
 
-        pub const last_no_payload_tag = Tag.bound_fn;
+        pub const last_no_payload_tag = Tag.inferred_alloc_const;
         pub const no_payload_count = @enumToInt(last_no_payload_tag) + 1;
 
         pub fn Type(comptime t: Tag) type {
@@ -6199,7 +6184,6 @@ pub const Type = extern union {
                 .extern_options,
                 .type_info,
                 .@"anyframe",
-                .bound_fn,
                 => @compileError("Type Tag " ++ @tagName(t) ++ " has no payload"),
 
                 .array_u8,
src/TypedValue.zig
@@ -499,10 +499,6 @@ pub fn print(
         // TODO these should not appear in this function
         .inferred_alloc => return writer.writeAll("(inferred allocation value)"),
         .inferred_alloc_comptime => return writer.writeAll("(inferred comptime allocation value)"),
-        .bound_fn => {
-            const bound_func = val.castTag(.bound_fn).?.data;
-            return writer.print("(bound_fn %{}(%{})", .{ bound_func.func_inst, bound_func.arg0_inst });
-        },
         .generic_poison_type => return writer.writeAll("(generic poison type)"),
         .generic_poison => return writer.writeAll("(generic poison)"),
         .runtime_value => return writer.writeAll("[runtime value]"),
src/value.zig
@@ -183,10 +183,6 @@ pub const Value = extern union {
         /// Used to coordinate alloc_inferred, store_to_inferred_ptr, and resolve_inferred_alloc
         /// instructions for comptime code.
         inferred_alloc_comptime,
-        /// Used sometimes as the result of field_call_bind.  This value is always temporary,
-        /// and refers directly to the air.  It will never be referenced by the air itself.
-        /// TODO: This is probably a bad encoding, maybe put temp data in the sema instead.
-        bound_fn,
         /// The ABI alignment of the payload type.
         lazy_align,
         /// The ABI size of the payload type.
@@ -326,7 +322,6 @@ pub const Value = extern union {
                 .inferred_alloc_comptime => Payload.InferredAllocComptime,
                 .aggregate => Payload.Aggregate,
                 .@"union" => Payload.Union,
-                .bound_fn => Payload.BoundFn,
                 .comptime_field_ptr => Payload.ComptimeFieldPtr,
             };
         }
@@ -477,7 +472,6 @@ pub const Value = extern union {
             .extern_options_type,
             .type_info_type,
             .generic_poison,
-            .bound_fn,
             => unreachable,
 
             .ty, .lazy_align, .lazy_size => {
@@ -837,10 +831,6 @@ pub const Value = extern union {
                 try out_stream.writeAll("(opt_payload_ptr)");
                 val = val.castTag(.opt_payload_ptr).?.data.container_ptr;
             },
-            .bound_fn => {
-                const bound_func = val.castTag(.bound_fn).?.data;
-                return out_stream.print("(bound_fn %{}(%{})", .{ bound_func.func_inst, bound_func.arg0_inst });
-            },
         };
     }
 
@@ -5657,16 +5647,6 @@ pub const Value = extern union {
                 val: Value,
             },
         };
-
-        pub const BoundFn = struct {
-            pub const base_tag = Tag.bound_fn;
-
-            base: Payload = Payload{ .tag = base_tag },
-            data: struct {
-                func_inst: Air.Inst.Ref,
-                arg0_inst: Air.Inst.Ref,
-            },
-        };
     };
 
     /// Big enough to fit any non-BigInt value
src/Zir.zig
@@ -297,6 +297,14 @@ pub const Inst = struct {
         /// Uses the `pl_node` union field with payload `Call`.
         /// AST node is the function call.
         call,
+        /// Function call using `a.b()` syntax.
+        /// Uses the named field as the callee. If there is no such field, searches in the type for
+        /// a decl matching the field name. The decl is resolved and we ensure that it's a function
+        /// which can accept the object as the first parameter, with one pointer fixup. This
+        /// function is then used as the callee, with the object as an implicit first parameter.
+        /// Uses the `pl_node` union field with payload `FieldCall`.
+        /// AST node is the function call.
+        field_call,
         /// Implements the `@call` builtin.
         /// Uses the `pl_node` union field with payload `BuiltinCall`.
         /// AST node is the builtin call.
@@ -432,15 +440,6 @@ pub const Inst = struct {
         /// This instruction also accepts a pointer.
         /// Uses `pl_node` field. The AST node is the a.b syntax. Payload is Field.
         field_val,
-        /// Given a pointer to a struct or object that contains virtual fields, returns the
-        /// named field.  If there is no named field, searches in the type for a decl that
-        /// matches the field name.  The decl is resolved and we ensure that it's a function
-        /// which can accept the object as the first parameter, with one pointer fixup.  If
-        /// all of that works, this instruction produces a special "bound function" value
-        /// which contains both the function and the saved first parameter value.
-        /// Bound functions may only be used as the function parameter to a `call` or
-        /// `builtin_call` instruction.  Any other use is invalid zir and may crash the compiler.
-        field_call_bind,
         /// Given a pointer to a struct or object that contains virtual fields, returns a pointer
         /// to the named field. The field name is a comptime instruction. Used by @field.
         /// Uses `pl_node` field. The AST node is the builtin call. Payload is FieldNamed.
@@ -1051,6 +1050,7 @@ pub const Inst = struct {
                 .bool_br_or,
                 .bool_not,
                 .call,
+                .field_call,
                 .cmp_lt,
                 .cmp_lte,
                 .cmp_eq,
@@ -1083,7 +1083,6 @@ pub const Inst = struct {
                 .field_ptr,
                 .field_ptr_init,
                 .field_val,
-                .field_call_bind,
                 .field_ptr_named,
                 .field_val_named,
                 .func,
@@ -1361,6 +1360,7 @@ pub const Inst = struct {
                 .bool_br_or,
                 .bool_not,
                 .call,
+                .field_call,
                 .cmp_lt,
                 .cmp_lte,
                 .cmp_eq,
@@ -1383,7 +1383,6 @@ pub const Inst = struct {
                 .field_ptr,
                 .field_ptr_init,
                 .field_val,
-                .field_call_bind,
                 .field_ptr_named,
                 .field_val_named,
                 .func,
@@ -1601,6 +1600,7 @@ pub const Inst = struct {
                 .check_comptime_control_flow = .un_node,
                 .for_len = .pl_node,
                 .call = .pl_node,
+                .field_call = .pl_node,
                 .cmp_lt = .pl_node,
                 .cmp_lte = .pl_node,
                 .cmp_eq = .pl_node,
@@ -1641,7 +1641,6 @@ pub const Inst = struct {
                 .field_val = .pl_node,
                 .field_ptr_named = .pl_node,
                 .field_val_named = .pl_node,
-                .field_call_bind = .pl_node,
                 .func = .pl_node,
                 .func_inferred = .pl_node,
                 .func_fancy = .pl_node,
@@ -1955,16 +1954,6 @@ pub const Inst = struct {
         /// The `@prefetch` builtin.
         /// `operand` is payload index to `BinNode`.
         prefetch,
-        /// Given a pointer to a struct or object that contains virtual fields, returns the
-        /// named field.  If there is no named field, searches in the type for a decl that
-        /// matches the field name.  The decl is resolved and we ensure that it's a function
-        /// which can accept the object as the first parameter, with one pointer fixup.  If
-        /// all of that works, this instruction produces a special "bound function" value
-        /// which contains both the function and the saved first parameter value.
-        /// Bound functions may only be used as the function parameter to a `call` or
-        /// `builtin_call` instruction.  Any other use is invalid zir and may crash the compiler.
-        /// Uses `pl_node` field. The AST node is the `@field` builtin. Payload is FieldNamedNode.
-        field_call_bind_named,
         /// Implements the `@fence` builtin.
         /// `operand` is payload index to `UnNode`.
         fence,
@@ -2913,6 +2902,19 @@ pub const Inst = struct {
         };
     };
 
+    /// Stored inside extra, with trailing arguments according to `args_len`.
+    /// Implicit 0. arg_0_start: u32, // always same as `args_len`
+    /// 1. arg_end: u32, // for each `args_len`
+    /// arg_N_start is the same as arg_N-1_end
+    pub const FieldCall = struct {
+        // Note: Flags *must* come first so that unusedResultExpr
+        // can find it when it goes to modify them.
+        flags: Call.Flags,
+        obj_ptr: Ref,
+        /// Offset into `string_bytes`.
+        field_name_start: u32,
+    };
+
     pub const TypeOfPeer = struct {
         src_node: i32,
         body_len: u32,
@@ -3187,12 +3189,6 @@ pub const Inst = struct {
         field_name: Ref,
     };
 
-    pub const FieldNamedNode = struct {
-        node: i32,
-        lhs: Ref,
-        field_name: Ref,
-    };
-
     pub const As = struct {
         dest_type: Ref,
         operand: Ref,
test/behavior/member_func.zig
@@ -86,18 +86,6 @@ test "@field field calls" {
     const pv = &v;
     const pcv: *const HasFuncs = pv;
 
-    try expect(@field(v, "get")() == 0);
-    @field(v, "inc")();
-    try expect(v.state == 1);
-    try expect(@field(v, "get")() == 1);
-
-    @field(pv, "inc")();
-    try expect(v.state == 2);
-    try expect(@field(pv, "get")() == 2);
-    try expect(@field(v, "getPtr")().* == 2);
-    try expect(@field(pcv, "get")() == 2);
-    try expect(@field(pcv, "getPtr")().* == 2);
-
     v.func_field = HasFuncs.one;
     try expect(@field(v, "func_field")(0) == 1);
     try expect(@field(pv, "func_field")(0) == 1);
test/cases/compile_errors/member_function_arg_mismatch.zig
@@ -6,10 +6,6 @@ pub export fn entry() void {
     var s: S = undefined;
     s.foo(true);
 }
-pub export fn entry2() void {
-    var s: S = undefined;
-    @call(.auto, s.foo, .{true});
-}
 
 // error
 // backend=stage2
@@ -17,5 +13,3 @@ pub export fn entry2() void {
 //
 // :7:6: error: member function expected 2 argument(s), found 1
 // :3:5: note: function declared here
-// :11:19: error: member function expected 2 argument(s), found 1
-// :3:5: note: function declared here