From 2fc6f3e2189cb78ab56cbac6b582afcf795be2ea Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 06:41:43 +0000 Subject: [PATCH] Pass a call's receiver the way C, C++ and Rust can reach it [patch] C passed the address of a receiver that was already a pointer or reference, and took the address of a call's result, so no method chain compiled. C now passes a pointer as it is and puts a call's result in a one-element array compound literal. C++ reaches through a pointer with ->, and Rust dereferences a raw pointer inside unsafe. Rust now scopes the static types of a function's parameters and locals to know which. Fixes #170 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018tZnrna9oooMZHDaLyXumk --- .../CGeneratedSourceCompilesTests.cs | 58 +++++++++++++ .../CppGeneratedSourceCompilesTests.cs | 42 ++++++++++ Coder.Test/Languages/ReceiverExemplar.cs | 80 ++++++++++++++++++ .../RustGeneratedSourceCompilesTests.cs | 41 ++++++++++ Coder/Languages/CGenerator.cs | 82 ++++++++++++++++--- Coder/Languages/CppGenerator.cs | 9 ++ Coder/Languages/LanguageGeneratorBase.cs | 13 ++- Coder/Languages/RustGenerator.cs | 28 +++++++ 8 files changed, 342 insertions(+), 11 deletions(-) create mode 100644 Coder.Test/Languages/ReceiverExemplar.cs diff --git a/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs index 00030eb..fa107ec 100644 --- a/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs @@ -152,6 +152,64 @@ public void ACallWithAReceiver_ReachesTheFunctionItLowersTo() }); } + /// + /// The consumer of the receivers header, which calls each function and exits with zero when each + /// call reached the member function it lowers to. + /// + private const string ReceiverDriver = """ + #include "receivers.h" + + int main(void) + { + Vec2 v = { .x = 0.0 }; + return (int)(through_reference(&v) + through_pointer(&v) + chained(v)) - 3; + } + + """; + + /// + /// Tests that a call on a reference parameter, on a pointer parameter, and on another call's + /// result each pass the pointer C's lowered member function takes (#170). + /// + /// + /// The first two used to take the address of what was already a pointer, which GCC 14 refuses, + /// and the third the address of a call's result, which no C compiler accepts. Warnings are + /// errors here so that the pointer-to-pointer fails on GCC 13 as well. + /// + [TestMethod] + public void CallsOnAReferenceAPointerAndACallResult_CompileAndRun() + { + SourceFile file = ReceiverExemplar.Build(method => $"Vec2_{method}"); + string header = new CGenerator().Generate(file); + + Assert.Contains("return Vec2_length(other);", header, StringComparison.Ordinal, header); + Assert.Contains("return Vec2_length((Vec2[]){ Vec2_twice(&v) });", header, StringComparison.Ordinal, header); + + string? compiler = ToolchainHarness.FindOnPath("--version", Compilers); + if (compiler is null) + { + Assert.Inconclusive("No C compiler on the path, so nothing was compiled."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText(Path.Combine(directory, "receivers.h"), header); + File.WriteAllText(Path.Combine(directory, "driver.c"), ReceiverDriver); + + (int exitCode, string output) = ToolchainHarness.Run( + compiler, + "-std=c11 -Wall -Wextra -Wno-unused-parameter -pedantic -Werror driver.c -o driver", + directory); + + Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated calls:{Environment.NewLine}{output}{Environment.NewLine}{header}"); + + (exitCode, output) = ToolchainHarness.Run(Path.Combine(directory, "driver"), string.Empty, directory); + + Assert.AreEqual(0, exitCode, $"A call did not reach its function:{Environment.NewLine}{output}"); + }); + } + /// /// The consumer of the empty-type header, which builds the one with a constructor. /// diff --git a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs index 683034f..e79ad48 100644 --- a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs @@ -285,6 +285,48 @@ public void ALongConstantStringCompilesWhereverAConstantMayStand() }); } + /// + /// Tests that a call on a reference parameter, on a pointer parameter, and on another call's + /// result compile and run (#170). + /// + /// + /// The pointer was reached with a dot, which C++ refuses: a member call through one is an arrow. + /// + [TestMethod] + public void CallsOnAReferenceAPointerAndACallResult_CompileAndRun() + { + string header = new CppGenerator().Generate(ReceiverExemplar.Build(method => method)); + + Assert.Contains("return other->length();", header, StringComparison.Ordinal, header); + Assert.Contains("return other.length();", header, StringComparison.Ordinal, header); + + string? compiler = ToolchainHarness.FindOnPath("--version", Compilers); + if (compiler is null) + { + Assert.Inconclusive("No C++ compiler on the path, so nothing was compiled."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText(Path.Combine(directory, "receivers.h"), header); + File.WriteAllText( + Path.Combine(directory, "driver.cpp"), + $"#include \"receivers.h\"{Environment.NewLine}{Environment.NewLine}int main() {{ Vec2 v{{}}; return static_cast(through_reference(v) + through_pointer(&v) + chained(v)) - 3; }}{Environment.NewLine}"); + + (int exitCode, string output) = ToolchainHarness.Run( + compiler, + "-std=c++20 -Wall -Wextra -pedantic -Werror driver.cpp -o driver", + directory); + + Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated header:{Environment.NewLine}{output}{Environment.NewLine}{header}"); + + (exitCode, output) = ToolchainHarness.Run(Path.Combine(directory, "driver"), string.Empty, directory); + + Assert.AreEqual(0, exitCode, $"A call did not reach its method:{Environment.NewLine}{output}"); + }); + } + /// /// Builds a header declaring an array in each position one with no bound may stand in. /// diff --git a/Coder.Test/Languages/ReceiverExemplar.cs b/Coder.Test/Languages/ReceiverExemplar.cs new file mode 100644 index 0000000..dcde356 --- /dev/null +++ b/Coder.Test/Languages/ReceiverExemplar.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using ktsu.Coder.Ast; + +/// +/// A type whose methods are called on a reference parameter, on a pointer parameter, and on the +/// result of another call — the three receivers a member call is not always written the same way for +/// (#170). +/// +/// +/// C lowers a member function to a free function taking a pointer to the instance, and the call site +/// has to meet it: it takes the address of a value, passes a pointer or reference as it is, and has +/// nothing to take the address of when the receiver is a call's result. C++ reaches through a pointer +/// with ->, and Rust does not reach through a raw pointer at all without dereferencing it. +/// None of that shows in the text, so the compile tests are what check it. +/// +internal static class ReceiverExemplar +{ + /// + /// Builds the file. + /// + /// Spells a method's name at a call site, which is the caller's business: + /// C names the free function a method lowers to after its type, and the call site has to say so. + /// The file. + internal static SourceFile Build(Func callee) + { + ClassDeclaration vec = new("Vec2") { Kind = TypeDeclarationKind.Struct }; + vec.Members.Add(new VariableDeclaration("x", "double")); + + // double length() const { return 1.0; } + FunctionDeclaration length = new("length") { ReturnType = "double", IsReadOnly = true }; + length.Body.Add(new ReturnStatement(new LiteralExpression(1.0))); + vec.Members.Add(length); + + // Vec2 twice() const { return Vec2{ .x = 2.0 }; } + FunctionDeclaration twice = new("twice") { ReturnType = "Vec2", IsReadOnly = true }; + twice.Body.Add(new ReturnStatement(new ConstructionExpression(new TypeReference("Vec2")) + { + Arguments = { new MemberInitialiser("x", new LiteralExpression(2.0)) }, + })); + vec.Members.Add(twice); + + SourceFile file = new("receivers"); + file.Members.Add(vec); + + // double through_reference(const Vec2& other) { return other.length(); } + file.Members.Add(CallOn("through_reference", "const Vec2&", "other", callee)); + + // double through_pointer(const Vec2* other) { return other->length(); } + file.Members.Add(CallOn("through_pointer", "const Vec2*", "other", callee)); + + // double chained(Vec2 v) { return v.twice().length(); } + FunctionDeclaration chained = new("chained") { ReturnType = "double" }; + chained.Parameters.Add(new Parameter("v", "Vec2")); + chained.Body.Add(new ReturnStatement(new CallExpression( + new CallExpression(new VariableReference("v"), callee("twice")), + callee("length")))); + file.Members.Add(chained); + + return file; + } + + /// + /// Builds a free function returning length() called on its one parameter. + /// + /// The function's name. + /// The parameter's type. + /// The parameter's name. + /// Spells a method's name at a call site. + /// The function. + private static FunctionDeclaration CallOn(string name, string type, string parameter, Func callee) + { + FunctionDeclaration function = new(name) { ReturnType = "double" }; + function.Parameters.Add(new Parameter(parameter, type)); + function.Body.Add(new ReturnStatement(new CallExpression(new VariableReference(parameter), callee("length")))); + return function; + } +} diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs index 47fe63b..e347d15 100644 --- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs @@ -390,6 +390,47 @@ public void CallingOnlyReadOnlyMethodsOnAParameter_BindsNothingMut() }); } + /// + /// Tests that a call on a reference parameter, on a pointer parameter, and on another call's + /// result compile and run (#170). + /// + /// + /// A raw pointer has no methods of its own and is not dereferenced for a call the way a reference + /// is, so other.length() on a *const Vec2 did not compile. + /// + [TestMethod] + public void CallsOnAReferenceAPointerAndACallResult_CompileAndRun() + { + SourceFile file = ReceiverExemplar.Build(method => method); + string source = new RustGenerator().Generate(file); + + Assert.Contains("return unsafe { (*other).length() };", source, StringComparison.Ordinal, source); + + if (ToolchainHarness.FindOnPath("--version", "rustc") is null) + { + Assert.Inconclusive("No Rust compiler on the path, so nothing was compiled."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText( + Path.Combine(directory, "receivers.rs"), + $"{source}{Environment.NewLine}fn main() {{ let v = Vec2 {{ x: 0.0 }}; std::process::exit((through_reference(&v) + through_pointer(&v) + chained(v)) as i32 - 3); }}{Environment.NewLine}"); + + (int exitCode, string output) = ToolchainHarness.Run( + "rustc", + "--edition 2021 -o receivers receivers.rs", + directory); + + Assert.AreEqual(0, exitCode, $"rustc rejected the generated source:{Environment.NewLine}{source}{Environment.NewLine}{output}"); + + (exitCode, output) = ToolchainHarness.Run(Path.Combine(directory, "receivers"), string.Empty, directory); + + Assert.AreEqual(0, exitCode, $"A call did not reach its method:{Environment.NewLine}{output}"); + }); + } + /// /// A struct with a mutating method, a read-only one, and an arithmetic operator that calls the /// mutating one on self. diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs index 98fae99..f78f82d 100644 --- a/Coder/Languages/CGenerator.cs +++ b/Coder/Languages/CGenerator.cs @@ -854,17 +854,12 @@ protected override void GenerateConstructionExpression(ConstructionExpression co /// the declaration — Point_translate(Point* self, …). A call site that kept the dot would /// not reach the function this generator emitted for it. /// - /// Its address is taken, because that self parameter is a pointer. That assumes the - /// receiver is an instance rather than already a pointer to one, which is an assumption rather - /// than a deduction: a knows the receiver's spelling and not its - /// type. It is the assumption worth making, because taking the address is the only one of the two - /// a caller cannot write for itself — the AST has no address-of operator — and because what this - /// generator emits elsewhere is instances. A caller holding a pointer spells the call as a free - /// function and passes the pointer as an ordinary argument. + /// Because that self parameter is a pointer, the receiver has to become one: + /// decides how from what the receiver's type is known to be. /// /// /// What is not done is mangling the name: the declaration's is built from the type it belongs to, - /// and the receiver's type is exactly what is not known here, so + /// and the receiver's type is not always known here, so /// is written verbatim and choosing it stays the caller's — /// which is what the node says it is everywhere else too. /// @@ -879,8 +874,7 @@ protected override void GenerateCallExpression(CallExpression callExpr, CodeBloc if (callExpr.Receiver is not null) { - code.Write("&"); - GenerateInternal(callExpr.Receiver, code); + WriteSelfArgument(callExpr.Receiver, code); if (callExpr.Arguments.Count > 0) { @@ -892,6 +886,74 @@ protected override void GenerateCallExpression(CallExpression callExpr, CodeBloc code.Write(")"); } + /// + /// Writes a call's receiver as the pointer to the instance its lowered function takes first. + /// + /// The receiver the call is made on. + /// The writer to emit into. + /// + /// Three receivers are written three ways (#170). One known to be a pointer or a reference is + /// already the pointer C wants, since this generator spells both with *, so it is passed as + /// it is: taking its address would pass a pointer to the pointer. A variable is an lvalue, so its + /// address is taken. Anything else — a call's result, above all, which is what a chain is made of + /// — has no address, so it is put in a one-element array compound literal of its type, + /// (Vec[]){ Vec_twice(&v) }, which is an lvalue living to the end of the enclosing + /// block and decays to the pointer wanted. An array rather than &(Vec){ … }, because a + /// braced list initialises a struct's members one by one and only an array's element may be + /// initialised from a whole struct. + /// + /// A value whose type the AST does not establish keeps the address-of, which is the assumption + /// the call site made before any of this was known. + /// + /// + private void WriteSelfArgument(Expression receiver, CodeBlocker code) + { + TypeReference? type = StaticType(receiver) ?? (receiver is CallExpression call ? LoweredReturnType(call.Callee) : null); + + if (type is { Indirection: not TypeIndirection.None, IsArray: false }) + { + GenerateInternal(receiver, code); + return; + } + + if (receiver is VariableReference || type is null or { IsArray: true }) + { + code.Write("&"); + GenerateInternal(receiver, code); + return; + } + + code.Write($"({MapToCType(type)}[]){{ "); + GenerateInternal(receiver, code); + code.Write(" }"); + } + + /// + /// Reads the return type of the one member function a call names by the name C gives it. + /// + /// The name the call is written with. + /// The function's return type, or null when no member function or more than one has the name. + /// + /// A call on a receiver names the free function a member function lowers to, + /// Vec_twice, which is not the name the class declares and so not one the shared + /// reading of a call's type can find. + /// + private TypeReference? LoweredReturnType(string callee) + { + if (GenerationRoot is null || string.IsNullOrEmpty(callee)) + { + return null; + } + + FunctionDeclaration[] matches = [.. SelfAndDescendants(GenerationRoot) + .OfType() + .SelectMany(type => type.Members + .OfType() + .Where(function => SpellFunctionName(function, type.Name) == callee)) + .Take(2)]; + return matches.Length == 1 ? matches[0].ReturnType : null; + } + /// /// Writes what a declaration starts at. /// diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs index e03ee4f..0e64d6d 100644 --- a/Coder/Languages/CppGenerator.cs +++ b/Coder/Languages/CppGenerator.cs @@ -150,6 +150,15 @@ protected override string SpellNonFiniteSingle(float value) => : value > 0 ? "std::numeric_limits::infinity()" : "-std::numeric_limits::infinity()"; + /// + /// + /// An arrow when the receiver is known to be a pointer, which a dot cannot reach through (#170). A + /// reference is reached with a dot like the value it refers to, and a receiver whose type the AST + /// does not establish is assumed to be a value. + /// + protected override string SpellMemberAccess(Expression receiver) => + StaticType(receiver) is { Indirection: TypeIndirection.Pointer, IsArray: false } ? "->" : "."; + /// protected override IEnumerable RequiredImports(SourceFile file) { diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index 5357051..cb6d215 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -872,7 +872,7 @@ protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlock code.Write(")"); } - code.Write("."); + code.Write(SpellMemberAccess(callExpr.Receiver)); } code.Write(callExpr.Callee); @@ -881,6 +881,17 @@ protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlock code.Write(")"); } + /// + /// Spells what stands between a call's receiver and its callee. + /// + /// The receiver the call is made on. + /// The separator, which is a dot unless a language reaches through a pointer differently. + /// + /// C++ overrides this: a member call through a pointer is written with ->, and a dot + /// there does not compile (#170). + /// + protected virtual string SpellMemberAccess(Expression receiver) => "."; + /// /// Emits a parenthesised choice between two values. /// diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index 73f5e9f..dc46150 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -1037,6 +1037,7 @@ private void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, st code.Write(" "); using Scope body = new(code); + using IDisposable types = OpenStaticTypeScope(funcDecl); if (funcDecl.Kind == FunctionKind.Constructor) { @@ -1050,6 +1051,32 @@ private void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, st returnType = outerReturnType; } + /// + /// + /// A raw pointer is not dereferenced for a method call the way a reference is, so a call through + /// one is written unsafe { (*p).length() }, which says what the call does: reads through a + /// pointer the compiler cannot vouch for (#170). Every other receiver is written as the base + /// writes it. + /// + protected override void GenerateCallExpression(CallExpression callExpr, CodeBlocker code) + { + Ensure.NotNull(callExpr); + Ensure.NotNull(code); + + if (callExpr.Receiver is not Expression receiver + || StaticType(receiver) is not { Indirection: TypeIndirection.Pointer, IsArray: false }) + { + base.GenerateCallExpression(callExpr, code); + return; + } + + code.Write("unsafe { (*"); + GenerateInternal(receiver, code); + code.Write($").{callExpr.Callee}("); + GenerateArgumentList(callExpr.Arguments, code); + code.Write(") }"); + } + /// /// /// The value is written for the type the function answers, so a string literal returned as an @@ -1571,6 +1598,7 @@ protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, Ensure.NotNull(varDecl); Ensure.NotNull(code); + RegisterStaticType(varDecl); code.Write(varDecl.IsConstant ? "let " : "let mut "); code.Write(varDecl.Name);