diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs index b72621e..6838f1c 100644 --- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs @@ -144,12 +144,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 ee8e798..2ffdeb6 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 a parameter the body assigns to is declared mut, and only that one, since /// Rust binds a parameter immutably. diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index 5a54e44..8ea4673 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -416,13 +416,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) @@ -553,7 +561,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; @@ -566,6 +574,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. ///