diff --git a/Coder.Test/Languages/ConstructorFieldDefaultTests.cs b/Coder.Test/Languages/ConstructorFieldDefaultTests.cs
new file mode 100644
index 0000000..f541479
--- /dev/null
+++ b/Coder.Test/Languages/ConstructorFieldDefaultTests.cs
@@ -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;
+
+///
+/// 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.
+///
+///
+/// The tests are inconclusive rather than failing where no compiler is on the path, which is the
+/// honest result: nothing was checked.
+///
+[TestClass]
+public class ConstructorFieldDefaultTests
+{
+ private static readonly string[] CCompilers = ["cc", "gcc", "clang"];
+
+ ///
+ /// The Rust driver. Mixed has a field with no declared value, which its constructor
+ /// leaves to Default; the driver supplies that, as a user of the type would.
+ ///
+ 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); }
+ }
+
+ """;
+
+ ///
+ /// The C driver: each constructor's members read back what was declared or passed.
+ ///
+ 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;
+ }
+
+ """;
+
+ ///
+ /// 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 Default.
+ ///
+ [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}");
+ });
+ }
+
+ ///
+ /// 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 defaults to note holds.
+ ///
+ [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}");
+ });
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The file holding them.
+ 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;
+ }
+}
diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs
index d2c061b..e623c1a 100644
--- a/Coder/Languages/CGenerator.cs
+++ b/Coder/Languages/CGenerator.cs
@@ -275,8 +275,8 @@ protected override void GenerateFunction(FunctionDeclaration funcDecl, CodeBlock
/// 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 {0}, 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 {0}, so every member of the returned value has a value whether or not anyone named it.
///
private void GenerateConstructorBody(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType)
{
@@ -284,7 +284,15 @@ private void GenerateConstructorBody(FunctionDeclaration funcDecl, CodeBlocker c
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 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.
@@ -298,6 +306,11 @@ private void GenerateConstructorBody(FunctionDeclaration funcDecl, CodeBlocker c
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(";");
}
@@ -634,13 +647,28 @@ .. classDecl.Members.Where(member =>
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;
}
}
+ ///
+ /// The struct whose functions are being written, which a constructor reads the declared starting
+ /// values of its members from.
+ ///
+ private ClassDeclaration? constructedType;
+
///
/// Reports whether a member declares a type rather than data or behaviour.
///
diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs
index 7ae9f84..1b50e3d 100644
--- a/Coder/Languages/RustGenerator.cs
+++ b/Coder/Languages/RustGenerator.cs
@@ -475,16 +475,25 @@ .. functions.Where(member => member.Kind is FunctionKind.Method or FunctionKind.
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;
}
}
@@ -629,6 +638,12 @@ private void GenerateStruct(ClassDeclaration classDecl, string name, CodeBlocker
///
private static bool IsAssociatedField(FieldDeclaration field) => field.IsStatic || field.IsConstant;
+ ///
+ /// The struct whose inherent functions are being written, which a constructor reads the declared
+ /// starting values of its fields from.
+ ///
+ private ClassDeclaration? constructedType;
+
///
/// Writes a field that belongs to its type into the type's inherent impl block.
///
@@ -1076,7 +1091,15 @@ private void WriteConstructorBody(FunctionDeclaration funcDecl, CodeBlocker code
{
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 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.
@@ -1092,10 +1115,35 @@ private void WriteConstructorBody(FunctionDeclaration funcDecl, CodeBlocker code
: 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();
}
+ ///
+ /// Reports whether a struct literal names every field of the struct it builds.
+ ///
+ /// The struct being built.
+ /// The literal building it.
+ /// True when no field is left for struct update syntax to fill in.
+ private static bool NamesEveryField(ClassDeclaration classDecl, ConstructionExpression value)
+ {
+ HashSet named = [.. value.Arguments.OfType().Select(initialiser => initialiser.Name)];
+ return classDecl.BaseType is null
+ && classDecl.Members.OfType().Where(field => !IsAssociatedField(field)).All(field => named.Contains(field.Name));
+ }
+
///
/// Writes the instance a member function acts on, when it acts on one.
///