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 1bfd005..5629817 100644
--- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
+++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
@@ -444,6 +444,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 ec21c86..3b93587 100644
--- a/Coder/Languages/CGenerator.cs
+++ b/Coder/Languages/CGenerator.cs
@@ -860,17 +860,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.
///
@@ -885,8 +880,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)
{
@@ -898,6 +892,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 642e730..36ea05c 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 adb90b7..6a317bc 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);
@@ -890,6 +890,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 one argument of a call.
///
diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs
index 83cde19..f1705a8 100644
--- a/Coder/Languages/RustGenerator.cs
+++ b/Coder/Languages/RustGenerator.cs
@@ -1072,6 +1072,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