From 8a4888c3df81b89ea0c21088554854645b47a53b Mon Sep 17 00:00:00 2001 From: Nicholas Bishop Date: Mon, 9 Mar 2026 11:31:16 -0400 Subject: [PATCH] Support assigning to a struct field through a macro (#6843) Example: ```carbon import Cpp inline ''' struct B { int c; }; struct A { B b; }; A a; #define m a.b.c '''; fn F() { Cpp.m = 2; } ``` --- toolchain/check/cpp/macros.cpp | 65 ++++++++- .../check/testdata/interop/cpp/macros.carbon | 131 ++++++++++++++++-- 2 files changed, 182 insertions(+), 14 deletions(-) diff --git a/toolchain/check/cpp/macros.cpp b/toolchain/check/cpp/macros.cpp index 0e9e525a5450..129bece9f1d8 100644 --- a/toolchain/check/cpp/macros.cpp +++ b/toolchain/check/cpp/macros.cpp @@ -12,6 +12,8 @@ #include "toolchain/check/cpp/constant.h" #include "toolchain/check/cpp/import.h" #include "toolchain/check/literal.h" +#include "toolchain/check/member_access.h" +#include "toolchain/check/type_completion.h" namespace Carbon::Check { @@ -113,18 +115,69 @@ auto TryEvaluateMacro(Context& context, SemIR::LocId loc_id, const auto* value_decl = ap_value.getLValueBase().get(); + if (!ap_value.hasLValuePath()) { + context.TODO(loc_id, "Macro expanded to lvalue with no path"); + return SemIR::ErrorInst::InstId; + } + + if (ap_value.isLValueOnePastTheEnd()) { + context.TODO(loc_id, "Macro expanded to a one-past-the-end lvalue"); + return SemIR::ErrorInst::InstId; + } + auto key = SemIR::ClangDeclKey::ForNonFunctionDecl( // TODO: can this const_cast be avoided? const_cast(value_decl)); - if (ap_value.hasLValuePath() && ap_value.getLValuePath().size() > 0) { - context.TODO(loc_id, "Macro evaluated to an lvalue with a path: " + - ap_value.getAsString(context.ast_context(), - result_expr->getType())); - return SemIR::ErrorInst::InstId; + auto inst_id = ImportCppDecl(context, loc_id, key); + if (ap_value.getLValuePath().size() == 0) { + return inst_id; } - return ImportCppDecl(context, loc_id, key); + // Import the base type so that its fields can be accessed. + auto var_storage = context.insts().GetAs(inst_id); + // TODO: currently an error isn't reachable here because incomplete + // array types can't be imported. Once that changes, switch to + // `RequireCompleteType` and handle the error. + CompleteTypeOrCheckFail(context, var_storage.type_id); + + clang::QualType qual_type = ap_value.getLValueBase().getType(); + for (const auto& entry : ap_value.getLValuePath()) { + if (qual_type->isArrayType()) { + context.TODO(loc_id, "Macro expanded to array type"); + } else { + const auto* decl = + cast(entry.getAsBaseOrMember().getPointer()); + + const auto* field_decl = dyn_cast(decl); + if (!field_decl) { + context.TODO(loc_id, "Macro expanded to a base class subobject"); + return SemIR::ErrorInst::InstId; + } + + auto field_inst_id = + ImportCppDecl(context, loc_id, + SemIR::ClangDeclKey::ForNonFunctionDecl( + const_cast(field_decl))); + + if (field_inst_id == SemIR::ErrorInst::InstId) { + context.TODO(loc_id, + "Unsupported field in macro expansion: " + + ap_value.getAsString(context.ast_context(), + result_expr->getType())); + return SemIR::ErrorInst::InstId; + } + + const SemIR::FieldDecl& field_decl_inst = + context.insts().GetAs(field_inst_id); + + qual_type = field_decl->getType(); + inst_id = PerformMemberAccess(context, loc_id, inst_id, + field_decl_inst.name_id); + } + } + + return inst_id; } else { auto const_id = MapAPValueToConstant(context, loc_id, ap_value, result_expr->getType()); diff --git a/toolchain/check/testdata/interop/cpp/macros.carbon b/toolchain/check/testdata/interop/cpp/macros.carbon index 61f9aeb9ec90..148944719ec7 100644 --- a/toolchain/check/testdata/interop/cpp/macros.carbon +++ b/toolchain/check/testdata/interop/cpp/macros.carbon @@ -891,27 +891,82 @@ fn F() { (*Cpp.m) = 2; } -// --- fail_todo_assign_subobject.carbon +// --- assign_subobject.carbon + +library "[[@TEST_NAME]]"; + +import Cpp inline ''' +struct B { + int c; +}; +struct A { + B b; +}; +A a; +#define m a.b.c +'''; + +fn F() { + //@dump-sem-ir-begin + Cpp.m = 2; + //@dump-sem-ir-end +} + +// --- fail_todo_assign_array.carbon library "[[@TEST_NAME]]"; import Cpp inline ''' struct Struct { - int field; + // CHECK:STDERR: fail_todo_assign_array.carbon:[[@LINE+3]]:7: error: semantics TODO: `Unsupported: field declaration has unhandled type or kind` [SemanticsTodo] + // CHECK:STDERR: int arr[3]; + // CHECK:STDERR: ^ + int arr[3]; }; Struct s; -#define m s.field +#define m s.arr[1] '''; fn F() { - // CHECK:STDERR: fail_todo_assign_subobject.carbon:[[@LINE+11]]:3: error: semantics TODO: `Macro evaluated to an lvalue with a path: &s.field` [SemanticsTodo] - // CHECK:STDERR: Cpp.m = 2; - // CHECK:STDERR: ^~~~~ - // CHECK:STDERR: fail_todo_assign_subobject.carbon:[[@LINE+8]]:3: note: in `Cpp` name lookup for `m` [InCppNameLookup] + // CHECK:STDERR: fail_todo_assign_array.carbon:[[@LINE+15]]:3: note: in `Cpp` name lookup for `m` [InCppNameLookup] // CHECK:STDERR: Cpp.m = 2; // CHECK:STDERR: ^~~~~ // CHECK:STDERR: - // CHECK:STDERR: fail_todo_assign_subobject.carbon:[[@LINE+4]]:3: error: member name `m` not found in `Cpp` [MemberNameNotFoundInInstScope] + // CHECK:STDERR: fail_todo_assign_array.carbon:[[@LINE+11]]:3: error: semantics TODO: `Unsupported field in macro expansion: &s.arr[1]` [SemanticsTodo] + // CHECK:STDERR: Cpp.m = 2; + // CHECK:STDERR: ^~~~~ + // CHECK:STDERR: fail_todo_assign_array.carbon:[[@LINE+8]]:3: note: in `Cpp` name lookup for `m` [InCppNameLookup] + // CHECK:STDERR: Cpp.m = 2; + // CHECK:STDERR: ^~~~~ + // CHECK:STDERR: + // CHECK:STDERR: fail_todo_assign_array.carbon:[[@LINE+4]]:3: error: member name `m` not found in `Cpp` [MemberNameNotFoundInInstScope] + // CHECK:STDERR: Cpp.m = 2; + // CHECK:STDERR: ^~~~~ + // CHECK:STDERR: + Cpp.m = 2; +} + +// --- fail_todo_assign_base_class_member.carbon + +library "[[@TEST_NAME]]"; + +import Cpp inline ''' +struct A { + int n; +}; +struct B : A {} b; +#define m b.n +'''; + +fn F() { + // CHECK:STDERR: fail_todo_assign_base_class_member.carbon:[[@LINE+11]]:3: error: semantics TODO: `Macro expanded to a base class subobject` [SemanticsTodo] + // CHECK:STDERR: Cpp.m = 2; + // CHECK:STDERR: ^~~~~ + // CHECK:STDERR: fail_todo_assign_base_class_member.carbon:[[@LINE+8]]:3: note: in `Cpp` name lookup for `m` [InCppNameLookup] + // CHECK:STDERR: Cpp.m = 2; + // CHECK:STDERR: ^~~~~ + // CHECK:STDERR: + // CHECK:STDERR: fail_todo_assign_base_class_member.carbon:[[@LINE+4]]:3: error: member name `m` not found in `Cpp` [MemberNameNotFoundInInstScope] // CHECK:STDERR: Cpp.m = 2; // CHECK:STDERR: ^~~~~ // CHECK:STDERR: @@ -2015,3 +2070,63 @@ fn F() { // CHECK:STDOUT: // CHECK:STDOUT: } // CHECK:STDOUT: +// CHECK:STDOUT: --- assign_subobject.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %A: type = class_type @A [concrete] +// CHECK:STDOUT: %pattern_type.9de: type = pattern_type %A [concrete] +// CHECK:STDOUT: %B: type = class_type @B [concrete] +// CHECK:STDOUT: %A.elem: type = unbound_element_type %A, %B [concrete] +// CHECK:STDOUT: %int_32: Core.IntLiteral = int_value 32 [concrete] +// CHECK:STDOUT: %i32: type = class_type @Int, @Int(%int_32) [concrete] +// CHECK:STDOUT: %B.elem: type = unbound_element_type %B, %i32 [concrete] +// CHECK:STDOUT: %.1b7: ref %B = class_element_access imports.%a.var, element0 [concrete] +// CHECK:STDOUT: %.59e: ref %i32 = class_element_access %.1b7, element0 [concrete] +// CHECK:STDOUT: %int_2.ecc: Core.IntLiteral = int_value 2 [concrete] +// CHECK:STDOUT: %ImplicitAs.type.e8c: type = facet_type <@ImplicitAs, @ImplicitAs(%i32)> [concrete] +// CHECK:STDOUT: %To: Core.IntLiteral = symbolic_binding To, 0 [symbolic] +// CHECK:STDOUT: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%To) [symbolic] +// CHECK:STDOUT: %Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6 = struct_value () [symbolic] +// CHECK:STDOUT: %ImplicitAs.impl_witness.6bc: = impl_witness imports.%ImplicitAs.impl_witness_table.74f, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete] +// CHECK:STDOUT: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete] +// CHECK:STDOUT: %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d = struct_value () [concrete] +// CHECK:STDOUT: %ImplicitAs.facet: %ImplicitAs.type.e8c = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness.6bc) [concrete] +// CHECK:STDOUT: %ImplicitAs.WithSelf.Convert.type.b37: type = fn_type @ImplicitAs.WithSelf.Convert, @ImplicitAs.WithSelf(%i32, %ImplicitAs.facet) [concrete] +// CHECK:STDOUT: %.545: type = fn_type_with_self_type %ImplicitAs.WithSelf.Convert.type.b37, %ImplicitAs.facet [concrete] +// CHECK:STDOUT: %Core.IntLiteral.as.ImplicitAs.impl.Convert.bound: = bound_method %int_2.ecc, %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5 [concrete] +// CHECK:STDOUT: %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5, @Core.IntLiteral.as.ImplicitAs.impl.Convert(%int_32) [concrete] +// CHECK:STDOUT: %bound_method: = bound_method %int_2.ecc, %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn [concrete] +// CHECK:STDOUT: %int_2.ef8: %i32 = int_value 2 [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: imports { +// CHECK:STDOUT: %Cpp: = namespace file.%Cpp.import_cpp, [concrete] { +// CHECK:STDOUT: .m = @F.%.loc17_6.2 +// CHECK:STDOUT: import Cpp//... +// CHECK:STDOUT: } +// CHECK:STDOUT: %a.patt: %pattern_type.9de = ref_binding_pattern a [concrete] +// CHECK:STDOUT: %a.var_patt: %pattern_type.9de = var_pattern %a.patt [concrete] +// CHECK:STDOUT: %a.var: ref %A = var %a.var_patt [concrete] +// CHECK:STDOUT: %Core.import_ref.42d: @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert.type (%Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6) = import_ref Core//prelude/types/int, loc{{\d+_\d+}}, loaded [symbolic = @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert (constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2)] +// CHECK:STDOUT: %ImplicitAs.impl_witness_table.74f = impl_witness_table (%Core.import_ref.42d), @Core.IntLiteral.as.ImplicitAs.impl [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F() { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: %Cpp.ref: = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp] +// CHECK:STDOUT: %b.ref: %A.elem = name_ref b, @A.%.1 [concrete = @A.%.1] +// CHECK:STDOUT: %.loc17_6.1: ref %B = class_element_access imports.%a.var, element0 [concrete = constants.%.1b7] +// CHECK:STDOUT: %c.ref: %B.elem = name_ref c, @B.%.1 [concrete = @B.%.1] +// CHECK:STDOUT: %.loc17_6.2: ref %i32 = class_element_access %.loc17_6.1, element0 [concrete = constants.%.59e] +// CHECK:STDOUT: %m.ref: ref %i32 = name_ref m, %.loc17_6.2 [concrete = constants.%.59e] +// CHECK:STDOUT: %int_2: Core.IntLiteral = int_value 2 [concrete = constants.%int_2.ecc] +// CHECK:STDOUT: %impl.elem0: %.545 = impl_witness_access constants.%ImplicitAs.impl_witness.6bc, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5] +// CHECK:STDOUT: %bound_method.loc17_9.1: = bound_method %int_2, %impl.elem0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.bound] +// CHECK:STDOUT: %specific_fn: = specific_function %impl.elem0, @Core.IntLiteral.as.ImplicitAs.impl.Convert(constants.%int_32) [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn] +// CHECK:STDOUT: %bound_method.loc17_9.2: = bound_method %int_2, %specific_fn [concrete = constants.%bound_method] +// CHECK:STDOUT: %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc17_9.2(%int_2) [concrete = constants.%int_2.ef8] +// CHECK:STDOUT: %.loc17_9: init %i32 = converted %int_2, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_2.ef8] +// CHECK:STDOUT: assign %m.ref, %.loc17_9 +// CHECK:STDOUT: +// CHECK:STDOUT: } +// CHECK:STDOUT: