diff --git a/Coder.Test/Languages/RustGeneratorTests.cs b/Coder.Test/Languages/RustGeneratorTests.cs
index bd2c0f1..ad2d6b8 100644
--- a/Coder.Test/Languages/RustGeneratorTests.cs
+++ b/Coder.Test/Languages/RustGeneratorTests.cs
@@ -841,6 +841,47 @@ public void Construction_IsAStructLiteralACallOrAnArray()
Assert.AreEqual("[ 1 ]", Generate(array));
}
+ ///
+ /// Tests that a field's declared starting value is written down when the type has no constructor
+ /// to apply it, rather than dropped without a word, as Go and C already do.
+ ///
+ [TestMethod]
+ public void FieldInitialValue_WithoutAConstructor_IsANote()
+ {
+ string generated = Generator.Generate(Counter(withConstructor: false));
+
+ StringAssert.Contains(generated, "// count defaults to 0: Rust has no field initialisers, and no constructor sets it", StringComparison.Ordinal);
+ StringAssert.Contains(generated, "// step defaults to 1: Rust has no field initialisers, and no constructor sets it", StringComparison.Ordinal);
+ }
+
+ ///
+ /// Tests that a type with a constructor carries no such note, since the constructor builds a
+ /// field it does not set from its declared value.
+ ///
+ [TestMethod]
+ public void FieldInitialValue_WithAConstructor_IsNotANote()
+ => Assert.IsFalse(Generator.Generate(Counter(withConstructor: true)).Contains("defaults to", StringComparison.Ordinal));
+
+ ///
+ /// Builds the editor's sample type: two fields with declared starting values and one method.
+ ///
+ /// Whether the type also declares a constructor.
+ /// The declaration.
+ private static ClassDeclaration Counter(bool withConstructor)
+ {
+ ClassDeclaration counter = new("Counter");
+ counter.Members.Add(new FieldDeclaration("count", new TypeReference("int")) { InitialValue = Literal.Number(0) });
+ counter.Members.Add(new FieldDeclaration("step", new TypeReference("int")) { InitialValue = Literal.Number(1) });
+ counter.Members.Add(new FunctionDeclaration("increment") { Visibility = Visibility.Public });
+
+ if (withConstructor)
+ {
+ counter.Members.Add(new FunctionDeclaration("Counter") { Kind = FunctionKind.Constructor, Visibility = Visibility.Public });
+ }
+
+ return counter;
+ }
+
///
/// Writes one expression on its own, for the assertions that are about the expression rather than
/// about the declaration holding it.
diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs
index 9a63018..2b415fa 100644
--- a/Coder/Languages/RustGenerator.cs
+++ b/Coder/Languages/RustGenerator.cs
@@ -616,6 +616,7 @@ private void GenerateStruct(ClassDeclaration classDecl, string name, CodeBlocker
case FieldDeclaration field when !IsAssociatedField(field):
WritePreamble(field, code, PreambleSite.Field);
+ WriteUnconstructedDefault(classDecl, field, code);
WriteStructMember(field.Name, field.Type, field.Visibility, code);
break;
@@ -627,6 +628,41 @@ private void GenerateStruct(ClassDeclaration classDecl, string name, CodeBlocker
insideType--;
}
+ ///
+ /// Says what a field starts at when the type has no constructor to start it there.
+ ///
+ /// The type the field belongs to.
+ /// The field being written.
+ /// The writer to emit into.
+ ///
+ /// A constructor builds a field it does not set from its declared value, so a type with one
+ /// needs no note. A type without one has nothing to carry the value, and leaving it out would
+ /// look like the declaration never asked for one — Go and C say the same thing for the same
+ /// reason.
+ ///
+ private void WriteUnconstructedDefault(ClassDeclaration classDecl, FieldDeclaration field, CodeBlocker code)
+ {
+ if (field.InitialValue is not Expression initialValue
+ || classDecl.Members.OfType().Any(function => function.Kind == FunctionKind.Constructor))
+ {
+ return;
+ }
+
+ WriteInexpressible(code, $"{field.Name} defaults to {GenerateExpression(initialValue)}: Rust has no field initialisers, and no constructor sets it");
+ }
+
+ ///
+ /// Writes an expression to a string, for the places a comment has to quote one.
+ ///
+ /// The expression to write.
+ /// Its Rust source.
+ private string GenerateExpression(Expression expression)
+ {
+ using CodeBlocker inline = CodeBlocker.Create(IndentString);
+ GenerateInternal(expression, inline);
+ return inline.ToString().TrimEnd('\r', '\n');
+ }
+
///
/// Reports whether a field belongs to its type rather than to each instance of it.
///
@@ -691,9 +727,9 @@ private void WriteAssociatedField(FieldDeclaration field, CodeBlocker code)
/// What the declaration said about who may see it.
/// The writer to emit into.
///
- /// A field's initial value is not written, and unlike C that is not a gap to apologise for: a
- /// Rust struct is built by naming every field at once, so what a field starts at is the
- /// constructor's business rather than the declaration's.
+ /// A field's initial value is not written here: a Rust struct is built by naming every field at
+ /// once, so what a field starts at is the constructor's business rather than the declaration's.
+ /// Where there is no constructor, says so instead.
///
private static void WriteStructMember(string? name, TypeReference? type, Visibility visibility, CodeBlocker code) =>
code.WriteLine($"{SpellVisibilityKeyword(visibility)}{name}: {SpellType(type ?? new TypeReference(UnknownTypeName))},");