From da56a810fe7d83a849753f158c17509c1d06b353 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 04:36:42 +0000 Subject: [PATCH] Write a Rust type's constant fields as associated constants Inside a type the Rust generator wrote every field as a struct member, so a static constant became a per-instance field every constructor had to supply, and its value was silently dropped. Static and constant fields are now kept out of the struct: a constant is an associated const in the inherent impl block, and a mutable static, which Rust has no associated form for, is written down as a note instead of disappearing. Fixes ktsu-dev/Coder#81 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NtawxkUssqE3PHcqRCW1Ag --- .../RustGeneratedSourceCompilesTests.cs | 12 ++++ Coder.Test/Languages/RustGeneratorTests.cs | 45 +++++++++++++ Coder/Languages/RustGenerator.cs | 64 ++++++++++++++++++- 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs index 8e0f69a..7c97cd0 100644 --- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs @@ -126,12 +126,24 @@ private static ClassDeclaration Boxed() boxed.Documentation.Add("Holds one of whatever it was given."); boxed.TypeParameters.Add(TypeParameter.Parse("T : Clone")); boxed.Members.Add(new FieldDeclaration("held", "T")); + boxed.Members.Add(new FieldDeclaration("CAPACITY", "int") + { + IsStatic = true, + IsConstant = true, + InitialValue = new LiteralExpression(8), + }); FunctionDeclaration copy = new("copy") { ReturnType = "T", IsReadOnly = true }; copy.Body.Add(new ReturnStatement( new CallExpression(new VariableReference("self.held"), "clone"))); boxed.Members.Add(copy); + // Named through the type, which only compiles if the constant is an associated item of it + // rather than a field of each instance. + FunctionDeclaration capacity = new("capacity") { ReturnType = "int", IsReadOnly = true }; + capacity.Body.Add(new ReturnStatement(new VariableReference("Self::CAPACITY"))); + boxed.Members.Add(capacity); + return boxed; } diff --git a/Coder.Test/Languages/RustGeneratorTests.cs b/Coder.Test/Languages/RustGeneratorTests.cs index 6500be3..69a9eaf 100644 --- a/Coder.Test/Languages/RustGeneratorTests.cs +++ b/Coder.Test/Languages/RustGeneratorTests.cs @@ -61,6 +61,51 @@ public void Types_AreMappedToRustSpellings() StringComparison.Ordinal); } + /// + /// Tests that a constant field of a type is an associated constant in the inherent impl block, + /// keeping its value, rather than a field every instance holds. + /// + [TestMethod] + public void ConstantField_IsAnAssociatedConstant() + { + ClassDeclaration cfg = new("Cfg") { Kind = TypeDeclarationKind.Struct }; + cfg.Members.Add(new FieldDeclaration("width", "int")); + cfg.Members.Add(new FieldDeclaration("Limit", "int") + { + IsStatic = true, + IsConstant = true, + InitialValue = new LiteralExpression(8), + }); + + string code = Generator.Generate(cfg); + string structBody = code[..code.IndexOf("impl", StringComparison.Ordinal)]; + + StringAssert.Contains(structBody, "width: i32,", StringComparison.Ordinal); + Assert.DoesNotContain("Limit", structBody, StringComparison.Ordinal, code); + StringAssert.Contains(code, "impl Cfg {", StringComparison.Ordinal); + StringAssert.Contains(code, "const Limit: i32 = 8;", StringComparison.Ordinal); + } + + /// + /// Tests that a mutable static field of a type, which Rust has no associated form for, is + /// written down as a note rather than becoming a per-instance field. + /// + [TestMethod] + public void MutableStaticField_IsNotedRatherThanAStructField() + { + ClassDeclaration counter = new("Counter") { Kind = TypeDeclarationKind.Struct }; + counter.Members.Add(new FieldDeclaration("Count", "int") + { + IsStatic = true, + InitialValue = new LiteralExpression(0), + }); + + string code = Generator.Generate(counter); + + Assert.DoesNotContain("Count: i32,", code, StringComparison.Ordinal, code); + StringAssert.Contains(code, "// Count: Rust has no associated statics", StringComparison.Ordinal); + } + /// /// Tests that an unrecognized type name is passed through, so a caller can name a real Rust type. /// diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index 6892a04..2bcbbf1 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -405,13 +405,21 @@ .. classDecl.Members.OfType().Where(member => member.Kind i .. functions.Where(member => member.Kind is FunctionKind.Method or FunctionKind.Constructor), ]; - if (inherent.Count > 0) + List associated = [.. classDecl.Members.OfType().Where(IsAssociatedField)]; + + if (inherent.Count > 0 || associated.Count > 0) { code.NewLine(); code.Write($"impl{implBounds} {applied} "); using Scope block = new(code); - bool first = true; + + foreach (FieldDeclaration field in associated) + { + WriteAssociatedField(field, code); + } + + bool first = associated.Count == 0; foreach (FunctionDeclaration function in inherent) { if (!first) @@ -542,7 +550,7 @@ private void GenerateStruct(ClassDeclaration classDecl, string name, CodeBlocker WriteStructMember(field.Name, field.Type, VisibilityOf(field), code); break; - case FieldDeclaration field: + case FieldDeclaration field when !IsAssociatedField(field): GenerateDocumentation(field, code); WriteStructMember(field.Name, field.Type, field.Visibility, code); break; @@ -555,6 +563,56 @@ private void GenerateStruct(ClassDeclaration classDecl, string name, CodeBlocker insideType--; } + /// + /// Reports whether a field belongs to its type rather than to each instance of it. + /// + /// The field to test. + /// True when the field is static or constant. + /// + /// A constant is static whether or not the declaration says so. Either way it has no place in + /// the struct: a struct's fields are what every instance holds and every constructor supplies. + /// + private static bool IsAssociatedField(FieldDeclaration field) => field.IsStatic || field.IsConstant; + + /// + /// Writes a field that belongs to its type into the type's inherent impl block. + /// + /// The field to write. + /// The writer to emit into. + /// + /// A constant is an associated const, named through the type as Cfg::Limit, which + /// is what a C++ static constexpr member is. A mutable static has no such form: Rust has + /// associated constants but no associated statics, so it is written down rather than dropped. + /// + private void WriteAssociatedField(FieldDeclaration field, CodeBlocker code) + { + GenerateDocumentation(field, code); + + if (!field.IsConstant) + { + WriteInexpressible(code, $"{field.Name}: Rust has no associated statics, so a mutable static belongs to no type"); + return; + } + + if (field.InitialValue is null) + { + WriteInexpressible(code, $"{field.Name}: Rust gives a const no value of its own"); + return; + } + + TypeReference type = field.Type ?? new TypeReference(UnknownTypeName); + + code.Write($"{SpellVisibilityKeyword(field.Visibility)}const {field.Name}: {SpellStorageType(type)} = "); + + if (type.IsArray) + { + code.Write("&"); + } + + GenerateInternal(field.InitialValue, code); + EndStatement(code); + } + /// /// Writes one field of a struct. ///