Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Coder.Test/Languages/CGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,64 @@ public void ACallWithAReceiver_ReachesTheFunctionItLowersTo()
});
}

/// <summary>
/// The consumer of the receivers header, which calls each function and exits with zero when each
/// call reached the member function it lowers to.
/// </summary>
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;
}

""";

/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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}");
});
}

/// <summary>
/// The consumer of the empty-type header, which builds the one with a constructor.
/// </summary>
Expand Down
42 changes: 42 additions & 0 deletions Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,48 @@ public void ALongConstantStringCompilesWhereverAConstantMayStand()
});
}

/// <summary>
/// Tests that a call on a reference parameter, on a pointer parameter, and on another call's
/// result compile and run (#170).
/// </summary>
/// <remarks>
/// The pointer was reached with a dot, which C++ refuses: a member call through one is an arrow.
/// </remarks>
[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<int>(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}");
});
}

/// <summary>
/// Builds a header declaring an array in each position one with no bound may stand in.
/// </summary>
Expand Down
80 changes: 80 additions & 0 deletions Coder.Test/Languages/ReceiverExemplar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

using ktsu.Coder.Ast;

/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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 <c>-&gt;</c>, 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.
/// </remarks>
internal static class ReceiverExemplar
{
/// <summary>
/// Builds the file.
/// </summary>
/// <param name="callee">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.</param>
/// <returns>The file.</returns>
internal static SourceFile Build(Func<string, string> 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<double>(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<double>(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;
}

/// <summary>
/// Builds a free function returning <c>length()</c> called on its one parameter.
/// </summary>
/// <param name="name">The function's name.</param>
/// <param name="type">The parameter's type.</param>
/// <param name="parameter">The parameter's name.</param>
/// <param name="callee">Spells a method's name at a call site.</param>
/// <returns>The function.</returns>
private static FunctionDeclaration CallOn(string name, string type, string parameter, Func<string, string> 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;
}
}
41 changes: 41 additions & 0 deletions Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,47 @@ public void CallingOnlyReadOnlyMethodsOnAParameter_BindsNothingMut()
});
}

/// <summary>
/// Tests that a call on a reference parameter, on a pointer parameter, and on another call's
/// result compile and run (#170).
/// </summary>
/// <remarks>
/// A raw pointer has no methods of its own and is not dereferenced for a call the way a reference
/// is, so <c>other.length()</c> on a <c>*const Vec2</c> did not compile.
/// </remarks>
[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}");
});
}

/// <summary>
/// A struct with a mutating method, a read-only one, and an arithmetic operator that calls the
/// mutating one on <c>self</c>.
Expand Down
82 changes: 72 additions & 10 deletions Coder/Languages/CGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
/// The <c>&lt;math.h&gt;</c> macros, which is why a file holding one includes it.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "NAN" : value > 0 ? "INFINITY" : "-INFINITY";

Check warning on line 180 in Coder/Languages/CGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
protected override IEnumerable<string> RequiredImports(SourceFile file)
Expand Down Expand Up @@ -860,17 +860,12 @@
/// the declaration — <c>Point_translate(Point* self, …)</c>. A call site that kept the dot would
/// not reach the function this generator emitted for it.
/// <para>
/// Its address is taken, because that <c>self</c> 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 <see cref="CallExpression"/> 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 <c>self</c> parameter is a pointer, the receiver has to become one:
/// <see cref="WriteSelfArgument"/> decides how from what the receiver's type is known to be.
/// </para>
/// <para>
/// 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
/// <see cref="CallExpression.Callee"/> is written verbatim and choosing it stays the caller's —
/// which is what the node says it is everywhere else too.
/// </para>
Expand All @@ -885,8 +880,7 @@

if (callExpr.Receiver is not null)
{
code.Write("&");
GenerateInternal(callExpr.Receiver, code);
WriteSelfArgument(callExpr.Receiver, code);

if (callExpr.Arguments.Count > 0)
{
Expand All @@ -898,6 +892,74 @@
code.Write(")");
}

/// <summary>
/// Writes a call's receiver as the pointer to the instance its lowered function takes first.
/// </summary>
/// <param name="receiver">The receiver the call is made on.</param>
/// <param name="code">The writer to emit into.</param>
/// <remarks>
/// 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 <c>*</c>, 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,
/// <c>(Vec[]){ Vec_twice(&amp;v) }</c>, which is an lvalue living to the end of the enclosing
/// block and decays to the pointer wanted. An array rather than <c>&amp;(Vec){ … }</c>, 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.
/// <para>
/// 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.
/// </para>
/// </remarks>
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(" }");
}

/// <summary>
/// Reads the return type of the one member function a call names by the name C gives it.
/// </summary>
/// <param name="callee">The name the call is written with.</param>
/// <returns>The function's return type, or null when no member function or more than one has the name.</returns>
/// <remarks>
/// A call on a receiver names the free function a member function lowers to,
/// <c>Vec_twice</c>, which is not the name the class declares and so not one the shared
/// reading of a call's type can find.
/// </remarks>
private TypeReference? LoweredReturnType(string callee)
{
if (GenerationRoot is null || string.IsNullOrEmpty(callee))
{
return null;
}

FunctionDeclaration[] matches = [.. SelfAndDescendants(GenerationRoot)
.OfType<ClassDeclaration>()
.SelectMany(type => type.Members
.OfType<FunctionDeclaration>()
.Where(function => SpellFunctionName(function, type.Name) == callee))
.Take(2)];
return matches.Length == 1 ? matches[0].ReturnType : null;
}

/// <summary>
/// Writes what a declaration starts at.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions Coder/Languages/CppGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ protected override string SpellNonFiniteSingle(float value) =>
: value > 0 ? "std::numeric_limits<float>::infinity()"
: "-std::numeric_limits<float>::infinity()";

/// <inheritdoc/>
/// <remarks>
/// 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.
/// </remarks>
protected override string SpellMemberAccess(Expression receiver) =>
StaticType(receiver) is { Indirection: TypeIndirection.Pointer, IsArray: false } ? "->" : ".";

/// <inheritdoc/>
protected override IEnumerable<string> RequiredImports(SourceFile file)
{
Expand Down
13 changes: 12 additions & 1 deletion Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@
}
}

foreach (object item in expected)

Check warning on line 384 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!emittedPreambleItems!.Contains(item))
{
Expand All @@ -399,7 +399,7 @@
/// Read off the nodes' public properties rather than a list of node types, so a node added to the
/// AST is walked without anybody remembering to add it here.
/// </remarks>
protected static IEnumerable<AstNode> SelfAndDescendants(AstNode root)

Check warning on line 402 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.
{
HashSet<AstNode> visited = new(ReferenceEqualityComparer.Instance);
Stack<AstNode> pending = new();
Expand Down Expand Up @@ -872,7 +872,7 @@
code.Write(")");
}

code.Write(".");
code.Write(SpellMemberAccess(callExpr.Receiver));
}

code.Write(callExpr.Callee);
Expand All @@ -890,6 +890,17 @@
code.Write(")");
}

/// <summary>
/// Spells what stands between a call's receiver and its callee.
/// </summary>
/// <param name="receiver">The receiver the call is made on.</param>
/// <returns>The separator, which is a dot unless a language reaches through a pointer differently.</returns>
/// <remarks>
/// C++ overrides this: a member call through a pointer is written with <c>-&gt;</c>, and a dot
/// there does not compile (#170).
/// </remarks>
protected virtual string SpellMemberAccess(Expression receiver) => ".";

/// <summary>
/// Emits one argument of a call.
/// </summary>
Expand Down Expand Up @@ -1235,7 +1246,7 @@
/// the generator adds through <see cref="RequiredImports"/>.
/// </remarks>
protected virtual string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity";

Check warning on line 1249 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Spells a float that is NaN or an infinity.
Expand Down Expand Up @@ -1346,7 +1357,7 @@
string? origin = null;
long offset = 0;

foreach (EnumMember member in enumDecl.Members)

Check warning on line 1360 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)
{
if (member.Value is not null)
{
Expand Down
Loading
Loading