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
157 changes: 157 additions & 0 deletions Coder.Test/Languages/ConstructorFieldDefaultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

using ktsu.Coder.Ast;
using ktsu.Coder.Languages;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Compiles and runs what the Rust and C generators write for a constructor that leaves a field
/// with a declared starting value unset. Neither language has field initialisers, so the
/// constructor is the only place the declared value can be applied; every other target applies it.
/// </summary>
/// <remarks>
/// The tests are inconclusive rather than failing where no compiler is on the path, which is the
/// honest result: nothing was checked.
/// </remarks>
[TestClass]
public class ConstructorFieldDefaultTests
{
private static readonly string[] CCompilers = ["cc", "gcc", "clang"];

/// <summary>
/// The Rust driver. <c>Mixed</c> has a field with no declared value, which its constructor
/// leaves to <c>Default</c>; the driver supplies that, as a user of the type would.
/// </summary>
private const string RustDriver = """

impl Default for Mixed {
fn default() -> Self {
Mixed { count: 0, step: 7 }
}
}

fn main() {
let counter = Counter::new(3);
if counter.count != 5 || counter.step != 3 { std::process::exit(1); }
let settings = Settings::new();
if settings.count != 5 || settings.step != 1 { std::process::exit(2); }
let mixed = Mixed::new();
if mixed.count != 5 || mixed.step != 7 { std::process::exit(3); }
}

""";

/// <summary>
/// The C driver: each constructor's members read back what was declared or passed.
/// </summary>
private const string CDriver = """
#include "defaults.h"

int main(void)
{
Counter counter = Counter_create(3);
if (counter.count != 5 || counter.step != 3) { return 1; }
Settings settings = Settings_create();
if (settings.count != 5 || settings.step != 1) { return 2; }
Mixed mixed = Mixed_create();
if (mixed.count != 5 || mixed.step != 0) { return 3; }
return 0;
}

""";

/// <summary>
/// Tests that a Rust constructor builds a field it does not set from its declared value, with or
/// without initialisers of its own, and leaves only the undeclared rest to <c>Default</c>.
/// </summary>
[TestMethod]
public void Rust_ConstructorAppliesDeclaredFieldValues()
{
if (ToolchainHarness.FindOnPath("--version", "rustc") is null)
{
Assert.Inconclusive("No Rust compiler on the path, so nothing was compiled.");
return;
}

ToolchainHarness.InTemporaryDirectory(directory =>
{
string program = OperatingSystem.IsWindows() ? "defaults.exe" : "defaults";
string source = new RustGenerator().Generate(Types()) + RustDriver;
File.WriteAllText(Path.Combine(directory, "defaults.rs"), source);

(int built, string output) = ToolchainHarness.Run("rustc", $"--edition 2021 -o {program} defaults.rs", directory);
Assert.AreEqual(0, built, $"rustc rejected the generated source:{Environment.NewLine}{output}{Environment.NewLine}{source}");

(int ran, _) = ToolchainHarness.Run(Path.Combine(directory, program), string.Empty, directory);
Assert.AreEqual(0, ran, $"a field did not start at its declared value (driver check {ran}):{Environment.NewLine}{source}");
});
}

/// <summary>
/// Tests that a C constructor builds a member it does not set from its declared value, with or
/// without initialisers of its own, so the struct's <c>defaults to</c> note holds.
/// </summary>
[TestMethod]
public void C_ConstructorAppliesDeclaredFieldValues()
{
string? compiler = ToolchainHarness.FindOnPath("--version", CCompilers);
if (compiler is null)
{
Assert.Inconclusive("No C compiler on the path, so nothing was compiled.");
return;
}

ToolchainHarness.InTemporaryDirectory(directory =>
{
string program = OperatingSystem.IsWindows() ? "defaults.exe" : "defaults";
string header = new CGenerator().Generate(Types());
File.WriteAllText(Path.Combine(directory, "defaults.h"), header);
File.WriteAllText(Path.Combine(directory, "driver.c"), CDriver);

(int built, string output) = ToolchainHarness.Run(
compiler,
$"-std=c11 -Wall -Wextra -pedantic -o {program} driver.c",
directory);
Assert.AreEqual(0, built, $"{compiler} rejected the generated header:{Environment.NewLine}{output}{Environment.NewLine}{header}");

(int ran, _) = ToolchainHarness.Run(Path.Combine(directory, program), string.Empty, directory);
Assert.AreEqual(0, ran, $"a member did not start at its declared value (driver check {ran}):{Environment.NewLine}{header}");
});
}

/// <summary>
/// Builds three types whose constructors leave a declared field unset: one alongside an
/// initialiser, one with no initialisers and every field declared, and one with no initialisers
/// and a field that declares nothing.
/// </summary>
/// <returns>The file holding them.</returns>
private static SourceFile Types()
{
SourceFile file = new("defaults");

ClassDeclaration counter = new("Counter") { Kind = TypeDeclarationKind.Struct };
counter.Members.Add(new FieldDeclaration("count", new TypeReference("int")) { Visibility = Visibility.Public, InitialValue = Literal.Number(5) });
counter.Members.Add(new FieldDeclaration("step", new TypeReference("int")) { Visibility = Visibility.Public });
FunctionDeclaration counterNew = new("Counter") { Kind = FunctionKind.Constructor, Visibility = Visibility.Public };
counterNew.Parameters.Add(new Parameter("s", "int"));
counterNew.Initialisers.Add(new MemberInitialiser("step", new VariableReference("s")));
counter.Members.Add(counterNew);
file.Members.Add(counter);

ClassDeclaration settings = new("Settings") { Kind = TypeDeclarationKind.Struct };
settings.Members.Add(new FieldDeclaration("count", new TypeReference("int")) { Visibility = Visibility.Public, InitialValue = Literal.Number(5) });
settings.Members.Add(new FieldDeclaration("step", new TypeReference("int")) { Visibility = Visibility.Public, InitialValue = Literal.Number(1) });
settings.Members.Add(new FunctionDeclaration("Settings") { Kind = FunctionKind.Constructor, Visibility = Visibility.Public });
file.Members.Add(settings);

ClassDeclaration mixed = new("Mixed") { Kind = TypeDeclarationKind.Struct };
mixed.Members.Add(new FieldDeclaration("count", new TypeReference("int")) { Visibility = Visibility.Public, InitialValue = Literal.Number(5) });
mixed.Members.Add(new FieldDeclaration("step", new TypeReference("int")) { Visibility = Visibility.Public });
mixed.Members.Add(new FunctionDeclaration("Mixed") { Kind = FunctionKind.Constructor, Visibility = Visibility.Public });
file.Members.Add(mixed);

return file;
}
}
40 changes: 34 additions & 6 deletions Coder/Languages/CGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,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 174 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.

Check warning on line 174 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 @@ -275,16 +275,24 @@
/// C constructs nothing on its own, so a constructor is a function that returns a value it built.
/// The initialiser list becomes the designated initialiser it already is — which is the one place
/// C is the better fit, since C's designators need not be in declaration order and C++20's do —
/// and a constructor with no initialisers starts from <c>{0}</c>, so every member of the returned
/// value has a value whether or not anyone named it.
/// and a constructor with no initialisers, and no member with a declared starting value, starts
/// from <c>{0}</c>, so every member of the returned value has a value whether or not anyone named it.
/// </remarks>
private void GenerateConstructorBody(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType)
{
string typeName = enclosingType ?? funcDecl.Name ?? "UnnamedType";

code.Write($"{typeName} {SelfParameterName} = ");

if (funcDecl.Initialisers.Count == 0)
// A member the declaration gave a starting value, and this constructor did not set, starts
// at that value, which is what the struct's `defaults to` note promises. A member named by
// neither is zeroed, as a designated initialiser leaves every member it does not name.
List<FieldDeclaration> defaulted = constructedType is null
? []
: [.. InstanceInitialised(constructedType, _ => true)
.Where(field => !funcDecl.Initialisers.Any(initialiser => initialiser.Name == field.Name))];

if (funcDecl.Initialisers.Count == 0 && defaulted.Count == 0)
{
// Not `{}`, which C only allows from C23. `{0}` is the spelling that zeroes a whole
// object in every dialect, whatever the first member's type is.
Expand All @@ -298,6 +306,11 @@
initialiser.Arguments.Add(member);
}

foreach (FieldDeclaration field in defaulted)
{
initialiser.Arguments.Add(new MemberInitialiser(field.Name ?? string.Empty, (Expression)field.InitialValue!.DeepClone()));
}

WriteInitialiser(initialiser, code);
code.WriteLine(";");
}
Expand Down Expand Up @@ -634,13 +647,28 @@
code.Outdent();
code.WriteLine($"}} {name};");

foreach (FunctionDeclaration function in functions)
ClassDeclaration? outerConstructed = constructedType;
constructedType = classDecl;
try
{
code.NewLine();
GenerateFunction(function, code, name);
foreach (FunctionDeclaration function in functions)
{
code.NewLine();
GenerateFunction(function, code, name);
}
}
finally
{
constructedType = outerConstructed;
}
}

/// <summary>
/// The struct whose functions are being written, which a constructor reads the declared starting
/// values of its members from.
/// </summary>
private ClassDeclaration? constructedType;

/// <summary>
/// Reports whether a member declares a type rather than data or behaviour.
/// </summary>
Expand Down
64 changes: 56 additions & 8 deletions Coder/Languages/RustGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@
/// The associated constants of <c>f64</c>, which need nothing imported.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "f64::NAN" : value > 0 ? "f64::INFINITY" : "f64::NEG_INFINITY";

Check warning on line 344 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 344 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
/// <remarks>
Expand All @@ -349,7 +349,7 @@
/// <c>f32</c> implicitly.
/// </remarks>
protected override string SpellNonFiniteSingle(float value) =>
float.IsNaN(value) ? "f32::NAN" : value > 0 ? "f32::INFINITY" : "f32::NEG_INFINITY";

Check warning on line 352 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 352 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
/// <remarks>
Expand Down Expand Up @@ -475,16 +475,25 @@
WriteAssociatedField(field, code);
}

bool first = associated.Count == 0;
foreach (FunctionDeclaration function in inherent)
ClassDeclaration? outerConstructed = constructedType;
constructedType = classDecl;
try
{
if (!first)
bool first = associated.Count == 0;
foreach (FunctionDeclaration function in inherent)
{
code.NewLine();
}
if (!first)
{
code.NewLine();
}

first = false;
GenerateFunction(function, code, name);
first = false;
GenerateFunction(function, code, name);
}
}
finally
{
constructedType = outerConstructed;
}
}

Expand Down Expand Up @@ -629,6 +638,12 @@
/// </remarks>
private static bool IsAssociatedField(FieldDeclaration field) => field.IsStatic || field.IsConstant;

/// <summary>
/// The struct whose inherent functions are being written, which a constructor reads the declared
/// starting values of its fields from.
/// </summary>
private ClassDeclaration? constructedType;

/// <summary>
/// Writes a field that belongs to its type into the type's inherent impl block.
/// </summary>
Expand Down Expand Up @@ -790,7 +805,7 @@
HashSet<string> reassigned = op.ReturnsBool
? []
: ReassignedNames(funcDecl.Body, [SelfName, .. funcDecl.Parameters.Select(OperandName)]);
string self = op.ReturnsBool ? "&self" : reassigned.Contains(SelfName) ? $"mut {SelfName}" : SelfName;

Check warning on line 808 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 808 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.
code.Write($"fn {op.Method}({self}");

foreach (Parameter parameter in funcDecl.Parameters)
Expand Down Expand Up @@ -1076,7 +1091,15 @@
{
WriteBody(funcDecl.Body, code);

if (funcDecl.Initialisers.Count == 0)
// A field the declaration gave a starting value, and this constructor did not set, starts
// at that value, as it does in every other target. Rust has no field initialisers, so the
// constructor's literal is the only place left to say it.
List<FieldDeclaration> defaulted = constructedType is null
? []
: [.. InstanceInitialised(constructedType, field => !IsAssociatedField(field))
.Where(field => !funcDecl.Initialisers.Any(initialiser => initialiser.Name == field.Name))];

if (funcDecl.Initialisers.Count == 0 && defaulted.Count == 0)
{
// There is nothing to build from, and Rust has no zero value to fall back on. Default is
// the one thing a type can ask for without naming its fields.
Expand All @@ -1092,10 +1115,35 @@
: initialiser);
}

foreach (FieldDeclaration field in defaulted)
{
value.Arguments.Add(new MemberInitialiser(field.Name ?? string.Empty, (Expression)field.InitialValue!.DeepClone()));
}

// With no initialisers, the fields nobody gave a value still come from Default, as they
// did before any had a declared one: struct update syntax fills in the rest.
if (funcDecl.Initialisers.Count == 0 && !NamesEveryField(constructedType!, value))
{
value.Arguments.Add(new CallExpression("..Self::default"));
}

GenerateConstructionExpression(value, code);
code.WriteLine();
}

/// <summary>
/// Reports whether a struct literal names every field of the struct it builds.
/// </summary>
/// <param name="classDecl">The struct being built.</param>
/// <param name="value">The literal building it.</param>
/// <returns>True when no field is left for struct update syntax to fill in.</returns>
private static bool NamesEveryField(ClassDeclaration classDecl, ConstructionExpression value)
{
HashSet<string?> named = [.. value.Arguments.OfType<MemberInitialiser>().Select(initialiser => initialiser.Name)];
return classDecl.BaseType is null
&& classDecl.Members.OfType<FieldDeclaration>().Where(field => !IsAssociatedField(field)).All(field => named.Contains(field.Name));
}

/// <summary>
/// Writes the instance a member function acts on, when it acts on one.
/// </summary>
Expand Down Expand Up @@ -1254,7 +1302,7 @@

bool inferred = varDecl.IsTypeInferred || varDecl.Type is null;
TypeReference? annotation = !inferred ? varDecl.Type
: varDecl.InitialValue is null ? new TypeReference(UnknownTypeName)

Check warning on line 1305 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 1305 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.
: KeptWidth(varDecl.InitialValue);

if (annotation is not null)
Expand Down
Loading