From aedc678e252b978306a3bc73fb3518567235db82 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 10:37:38 +0000 Subject: [PATCH] Apply declared field values in Rust and C constructors [patch] A Rust or C constructor that did not set a field with an InitialValue left it out: Rust's struct literal failed with E0063 (or fell back to Self::default(), losing the value), and C started the member at 0 despite its "defaults to" note. Both now add each such field to the literal or designated initialiser, as Go does since #187, including when the constructor has no initialisers. In Rust, a constructor with no initialisers that still leaves a field undeclared fills the rest with ..Self::default(), as it relied on Default before. Fixes #193 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012KjGQ13RexM4WYjuEaBtE6 --- .../Languages/ConstructorFieldDefaultTests.cs | 157 ++++++++++++++++++ Coder/Languages/CGenerator.cs | 40 ++++- Coder/Languages/RustGenerator.cs | 64 ++++++- 3 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 Coder.Test/Languages/ConstructorFieldDefaultTests.cs 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. ///