diff --git a/CLAUDE.md b/CLAUDE.md index 4530385..38efd33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,11 @@ source in four target languages. The solution uses: keyed child dictionary; `Expression` marks the nodes that evaluate to a value. `Visibility` is an enumeration rather than the modifier's text, because each generator spells it differently — or, in Python's case, not at all — and `IHasVisibility` is how a generator reads it off a member - without switching on which kind of member it is. + without switching on which kind of member it is. `TypeReference` is structure rather than the + type's text for the same reason, and a stronger one: a generator handed `std::span` + as a string can only paste it, so the caller would have to spell the target language itself. + `Parse` and `ToString` are inverses and text the grammar cannot read becomes a name holding it + verbatim, so a property that used to hold a string still takes and gives one. - `Coder/Languages/LanguageGeneratorBase.cs` — the emitters every generator shares. - `Coder/Languages/StandardLanguageGenerator.cs` — owns the node dispatch, so a derived generator supplies only the syntax its language does not share. `CSharpGenerator` deliberately diff --git a/Coder.Graph/AstFields.cs b/Coder.Graph/AstFields.cs index b01ce67..dce3658 100644 --- a/Coder.Graph/AstFields.cs +++ b/Coder.Graph/AstFields.cs @@ -110,15 +110,17 @@ public static IReadOnlyList Of(AstNode node) ClassDeclaration classDecl => [ new("Name", AstFieldKind.Text, classDecl.Name ?? string.Empty), - new("BaseType", AstFieldKind.Text, classDecl.BaseType ?? string.Empty), + new("BaseType", AstFieldKind.Text, classDecl.BaseType?.ToString() ?? string.Empty), new("Visibility", AstFieldKind.Choice, classDecl.Visibility.ToString(), Visibilities), ], FunctionDeclaration function => [ new("Name", AstFieldKind.Text, function.Name ?? string.Empty), - new("ReturnType", AstFieldKind.Text, function.ReturnType ?? string.Empty), + new("ReturnType", AstFieldKind.Text, function.ReturnType?.ToString() ?? string.Empty), new("Visibility", AstFieldKind.Choice, function.Visibility.ToString(), Visibilities), + new("Static", AstFieldKind.Flag, Spell(function.IsStatic)), + new("Pure", AstFieldKind.Flag, Spell(function.IsPure)), ], EntryPoint entryPoint => @@ -130,7 +132,7 @@ public static IReadOnlyList Of(AstNode node) Parameter parameter => [ new("Name", AstFieldKind.Text, parameter.Name ?? string.Empty), - new("Type", AstFieldKind.Text, parameter.Type ?? string.Empty), + new("Type", AstFieldKind.Text, parameter.Type?.ToString() ?? string.Empty), new("Optional", AstFieldKind.Flag, Spell(parameter.IsOptional)), new("Default", AstFieldKind.Text, parameter.DefaultValue ?? string.Empty), ], @@ -138,7 +140,7 @@ public static IReadOnlyList Of(AstNode node) VariableDeclaration varDecl => [ new("Name", AstFieldKind.Text, varDecl.Name), - new("Type", AstFieldKind.Text, varDecl.Type ?? string.Empty), + new("Type", AstFieldKind.Text, varDecl.Type?.ToString() ?? string.Empty), new("Constant", AstFieldKind.Flag, Spell(varDecl.IsConstant)), new("Inferred", AstFieldKind.Flag, Spell(varDecl.IsTypeInferred)), new("Visibility", AstFieldKind.Choice, varDecl.Visibility.ToString(), Visibilities), @@ -225,6 +227,10 @@ public static bool TryWrite(AstNode node, string fieldName, string value) (FunctionDeclaration function, "ReturnType") => Assign(() => function.ReturnType = OrNull(value)), (FunctionDeclaration function, "Visibility") => TryParseVisibility(value, out Visibility functionVisibility) && Assign(() => function.Visibility = functionVisibility), + (FunctionDeclaration function, "Static") => + TryParseBool(value, out bool isStatic) && Assign(() => function.IsStatic = isStatic), + (FunctionDeclaration function, "Pure") => + TryParseBool(value, out bool isPure) && Assign(() => function.IsPure = isPure), (EntryPoint entryPoint, "Arguments") => TryParseBool(value, out bool acceptsArguments) && Assign(() => entryPoint.AcceptsArguments = acceptsArguments), diff --git a/Coder.Test/Ast/FunctionModifierTests.cs b/Coder.Test/Ast/FunctionModifierTests.cs new file mode 100644 index 0000000..dc18595 --- /dev/null +++ b/Coder.Test/Ast/FunctionModifierTests.cs @@ -0,0 +1,244 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Ast; + +using ktsu.Coder.Ast; +using ktsu.Coder.Graph; +using ktsu.Coder.Languages; +using ktsu.Coder.Serialization; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for and : +/// what each language spells them as, and that they survive a round trip through YAML. +/// +/// +/// Static is the more interesting of the two, because Python spells it by changing the signature +/// rather than by decorating it: a static method has no self. Purity has a spelling in only +/// two of the four languages, which is the ordinary case for something the AST models — the AST says +/// what is true and a generator says as much of it as its language can. +/// +[TestClass] +public class FunctionModifierTests +{ + /// + /// Builds a class with one static pure method, which is the shape most of these use. + /// + /// The class. + private static ClassDeclaration SampleClass() + { + ClassDeclaration declaration = new("Points"); + + FunctionDeclaration distance = new("distance") + { + ReturnType = "double", + IsStatic = true, + IsPure = true, + }; + distance.Parameters.Add(new Parameter("scale", "double")); + distance.Body.Add(new ReturnStatement(new VariableReference("scale"))); + + declaration.Members.Add(distance); + return declaration; + } + + /// + /// Both are off unless asked for, so an existing function is unaffected by their existence. + /// + [TestMethod] + public void Neither_IsSetByDefault() + { + FunctionDeclaration function = new("area"); + + Assert.IsFalse(function.IsStatic); + Assert.IsFalse(function.IsPure); + } + + /// + /// C# writes static in the signature and [Pure] above it. + /// + [TestMethod] + public void CSharp_WritesStaticAndThePureAttribute() + { + string code = new CSharpGenerator().Generate(SampleClass()); + + Assert.Contains("[System.Diagnostics.Contracts.Pure]", code, StringComparison.Ordinal); + Assert.Contains("public static double distance(", code, StringComparison.Ordinal); + } + + /// + /// C++ writes static, and spells purity as the one thing the standard can say about it. + /// + [TestMethod] + public void Cpp_WritesStaticAndNodiscard() + { + FunctionDeclaration function = new("distance") + { + ReturnType = "double", + IsStatic = true, + IsPure = true, + }; + + Assert.Contains( + "[[nodiscard]] static double distance(", + new CppGenerator().Generate(function), + StringComparison.Ordinal); + } + + /// + /// Python spells static by dropping the receiver, which is the whole reason this modifier is not + /// just a keyword the generators paste in. + /// + [TestMethod] + public void Python_DropsSelfFromAStaticMethod() + { + string code = new PythonGenerator().Generate(SampleClass()); + + Assert.Contains("@staticmethod", code, StringComparison.Ordinal); + Assert.Contains("def distance(scale", code, StringComparison.Ordinal); + Assert.DoesNotContain("self", code, StringComparison.Ordinal); + } + + /// + /// An ordinary method still takes its receiver, and still separates it from the first parameter. + /// + [TestMethod] + public void Python_KeepsSelfOnAnInstanceMethod() + { + ClassDeclaration declaration = SampleClass(); + ((FunctionDeclaration)declaration.Members[0]).IsStatic = false; + + Assert.Contains( + "def distance(self, scale", + new PythonGenerator().Generate(declaration), + StringComparison.Ordinal); + } + + /// + /// Purity has no spelling in Python, so nothing is emitted for it rather than something invented. + /// + [TestMethod] + public void Python_SaysNothingAboutPurity() + { + FunctionDeclaration function = new("distance") { ReturnType = "double", IsPure = true }; + + Assert.AreEqual( + new PythonGenerator().Generate(new FunctionDeclaration("distance") { ReturnType = "double" }), + new PythonGenerator().Generate(function)); + } + + /// + /// JavaScript spells static on a class member and has nothing to say about purity. + /// + [TestMethod] + public void JavaScript_WritesStaticOnAMethod() + { + string code = new JavaScriptGenerator().Generate(SampleClass()); + + Assert.Contains("static distance(scale)", code, StringComparison.Ordinal); + } + + /// + /// A static private member keeps both spellings, which are separate parts of the same declaration. + /// + [TestMethod] + public void JavaScript_WritesStaticAlongsideThePrivatePrefix() + { + ClassDeclaration declaration = SampleClass(); + ((FunctionDeclaration)declaration.Members[0]).Visibility = Visibility.Private; + + Assert.Contains( + "static #distance(scale)", + new JavaScriptGenerator().Generate(declaration), + StringComparison.Ordinal); + } + + /// + /// Both survive a round trip through YAML. + /// + [TestMethod] + public void Yaml_RoundTripsBothModifiers() + { + FunctionDeclaration original = new("distance") + { + ReturnType = "double", + IsStatic = true, + IsPure = true, + }; + + string yaml = new YamlSerializer().Serialize(original); + FunctionDeclaration restored = (FunctionDeclaration)new YamlDeserializer().Deserialize(yaml)!; + + Assert.IsTrue(restored.IsStatic); + Assert.IsTrue(restored.IsPure); + } + + /// + /// A function that asked for neither writes neither key, so the document says nothing rather than + /// saying false twice on every function ever written. + /// + [TestMethod] + public void Yaml_WritesNothingForAModifierNobodyAskedFor() + { + string yaml = new YamlSerializer().Serialize(new FunctionDeclaration("distance")); + + Assert.DoesNotContain("isStatic", yaml, StringComparison.Ordinal); + Assert.DoesNotContain("isPure", yaml, StringComparison.Ordinal); + } + + /// + /// A document written before these existed still opens, with both off. + /// + [TestMethod] + public void Yaml_ReadsADocumentWrittenBeforeTheseExisted() + { + const string yaml = """ + functionDeclaration: + name: distance + returnType: double + """; + + FunctionDeclaration restored = (FunctionDeclaration)new YamlDeserializer().Deserialize(yaml)!; + + Assert.IsFalse(restored.IsStatic); + Assert.IsFalse(restored.IsPure); + } + + /// + /// A clone carries both, so copying a function does not quietly drop what it was marked as. + /// + [TestMethod] + public void Clone_CarriesBothModifiers() + { + FunctionDeclaration clone = (FunctionDeclaration)new FunctionDeclaration("distance") + { + IsStatic = true, + IsPure = true, + }.Clone(); + + Assert.IsTrue(clone.IsStatic); + Assert.IsTrue(clone.IsPure); + } + + /// + /// The inspector offers both as flags and writes what the user ticked. + /// + [TestMethod] + public void Fields_OfferBothAsFlags() + { + FunctionDeclaration function = new("distance") { ReturnType = "double" }; + + Assert.Contains( + field => field.Name == "Static" && field.Kind == AstFieldKind.Flag, + AstFields.Of(function)); + Assert.Contains( + field => field.Name == "Pure" && field.Kind == AstFieldKind.Flag, + AstFields.Of(function)); + + Assert.IsTrue(AstFields.TryWrite(function, "Static", "true")); + Assert.IsTrue(AstFields.TryWrite(function, "Pure", "true")); + + Assert.IsTrue(function.IsStatic); + Assert.IsTrue(function.IsPure); + } +} diff --git a/Coder.Test/Ast/TypeReferenceTests.cs b/Coder.Test/Ast/TypeReferenceTests.cs new file mode 100644 index 0000000..0cdb0c9 --- /dev/null +++ b/Coder.Test/Ast/TypeReferenceTests.cs @@ -0,0 +1,238 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Ast; + +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for : what it reads, what it writes back, and that each +/// generator spells the same type its own language's way. +/// +/// +/// A type used to be a bare string, which every generator could only paste. These are the tests that +/// say it is no longer one — the last of them is the case that motivated the change, a parameter no +/// string-shaped property could describe without the caller writing C++ by hand. +/// +[TestClass] +public class TypeReferenceTests +{ + /// + /// A name with nothing around it is a name. + /// + [TestMethod] + public void Parse_ReadsAPlainName() + { + TypeReference type = TypeReference.Parse("int"); + + Assert.AreEqual("int", type.Name); + Assert.IsEmpty(type.TypeArguments); + Assert.IsFalse(type.IsReadOnly); + Assert.AreEqual(TypeIndirection.None, type.Indirection); + } + + /// + /// A qualified name is one name. Which separator a language uses is the generator's business, so + /// the AST does not split it into parts it would only have to join again. + /// + [TestMethod] + public void Parse_KeepsAQualifiedNameWhole() + { + Assert.AreEqual("std::vector", TypeReference.Parse("std::vector").Name); + Assert.AreEqual("System.Collections.Generic.List", TypeReference.Parse("System.Collections.Generic.List").Name); + } + + /// + /// The argument list is a list, which is the whole difference from a string. + /// + [TestMethod] + public void Parse_ReadsArgumentsInOrder() + { + TypeReference type = TypeReference.Parse("Dictionary>"); + + Assert.AreEqual("Dictionary", type.Name); + Assert.HasCount(2, type.TypeArguments); + Assert.AreEqual("string", type.TypeArguments[0].Name); + Assert.AreEqual("List", type.TypeArguments[1].Name); + Assert.AreEqual("int", type.TypeArguments[1].TypeArguments[0].Name); + } + + /// + /// Read-only-ness and indirection are properties of the type, including of a type nested inside + /// another one's argument list. + /// + [TestMethod] + public void Parse_ReadsQualifiersAtEveryDepth() + { + TypeReference outer = TypeReference.Parse("const RigidBody&"); + + Assert.IsTrue(outer.IsReadOnly); + Assert.AreEqual(TypeIndirection.Reference, outer.Indirection); + + TypeReference element = TypeReference.Parse("std::span").TypeArguments[0]; + + Assert.IsTrue(element.IsReadOnly); + Assert.AreEqual("Velocity", element.Name); + } + + /// + /// readonly is accepted for the same thing, because the AST names the property for what it + /// means rather than for C++'s spelling of it. + /// + [TestMethod] + public void Parse_AcceptsEitherQualifierKeyword() + { + Assert.AreEqual(TypeReference.Parse("const Body"), TypeReference.Parse("readonly Body")); + } + + /// + /// A name that merely starts with a keyword is a name. + /// + [TestMethod] + public void Parse_DoesNotMistakeANameForAQualifier() + { + TypeReference type = TypeReference.Parse("constant"); + + Assert.AreEqual("constant", type.Name); + Assert.IsFalse(type.IsReadOnly); + } + + /// + /// Whatever the grammar can read, it writes back the same. + /// + [TestMethod] + [DataRow("int")] + [DataRow("std::vector")] + [DataRow("List")] + [DataRow("Dictionary>")] + [DataRow("const RigidBody&")] + [DataRow("std::span")] + [DataRow("Body*")] + public void ToString_IsTheInverseOfParse(string text) => + Assert.AreEqual(text, TypeReference.Parse(text).ToString()); + + /// + /// Text the grammar cannot read survives intact rather than being dropped or half-read. + /// + /// + /// This is what lets the string-shaped API keep working: a type nobody anticipated is wrong-but + /// -lossless, and a half-read one would be neither. + /// + [TestMethod] + [DataRow("List")] + [DataRow("a b c")] + public void Parse_KeepsTextItCannotRead(string text) + { + TypeReference type = TypeReference.Parse(text); + + Assert.AreEqual(text, type.Name); + Assert.AreEqual(text, type.ToString()); + } + + /// + /// Two types describing the same thing are equal, and equal types hash together. + /// + [TestMethod] + public void Equality_ComparesTheWholeType() + { + TypeReference span = TypeReference.Parse("std::span"); + + Assert.AreEqual(span, TypeReference.Parse("std::span")); + Assert.AreEqual(span.GetHashCode(), TypeReference.Parse("std::span").GetHashCode()); + Assert.AreNotEqual(span, TypeReference.Parse("std::span")); + Assert.AreNotEqual(span, TypeReference.Parse("std::span")); + } + + /// + /// A clone shares no argument with its original, so editing one does not edit the other. + /// + [TestMethod] + public void Clone_CopiesTheArgumentsRatherThanSharingThem() + { + TypeReference original = TypeReference.Parse("List"); + TypeReference clone = original.Clone(); + + Assert.AreEqual(original, clone); + Assert.AreNotSame(original.TypeArguments[0], clone.TypeArguments[0]); + + clone.TypeArguments[0].Name = "double"; + + Assert.AreEqual("int", original.TypeArguments[0].Name); + } + + /// + /// A name may be written where a type is wanted, which is what keeps the common case short. + /// + [TestMethod] + public void StringConversion_ReadsTheText() + { + Parameter parameter = new("scale") { Type = "List" }; + + Assert.AreEqual("List", parameter.Type?.Name); + Assert.HasCount(1, parameter.Type!.TypeArguments); + + parameter.Type = null; + + Assert.IsNull(parameter.Type); + } + + /// + /// A node keeps its own copy of a type, so cloning it does not share one. + /// + [TestMethod] + public void Clone_OfANodeCopiesItsType() + { + FunctionDeclaration original = new("count") { ReturnType = "List" }; + FunctionDeclaration clone = (FunctionDeclaration)original.Clone(); + + Assert.AreEqual(original.ReturnType, clone.ReturnType); + Assert.AreNotSame(original.ReturnType, clone.ReturnType); + } + + /// + /// The case the change exists for: a borrowed sequence of read-only elements, which no bare + /// string could describe without the caller spelling C++ themselves. + /// + [TestMethod] + public void Cpp_SpellsABorrowedSequenceOfReadOnlyElements() + { + FunctionDeclaration integrate = new("integrate") { ReturnType = "void" }; + integrate.Parameters.Add(new Parameter("velocities") + { + Type = new TypeReference("std::span") + { + TypeArguments = { new TypeReference("Velocity") { IsReadOnly = true } }, + }, + }); + integrate.Parameters.Add(new Parameter("positions") + { + Type = new TypeReference("std::span") { TypeArguments = { new TypeReference("Position") } }, + }); + + string code = new CppGenerator().Generate(integrate); + + Assert.Contains("std::span velocities", code, StringComparison.Ordinal); + Assert.Contains("std::span positions", code, StringComparison.Ordinal); + } + + /// + /// Every language spells the same type its own way, which is the point of holding it as structure + /// rather than as text. + /// + /// + /// C++ maps the container's name and keeps the argument, which is what a name-to-name mapping + /// buys: the table used to hold whole types, so the name and its arguments could not both be + /// honoured and a list<int> came out as neither. + /// + [TestMethod] + public void EveryGenerator_SpellsAParameterisedTypeItsOwnWay() + { + FunctionDeclaration function = new("items") { ReturnType = "List" }; + + Assert.Contains("List items", new CSharpGenerator().Generate(function), StringComparison.Ordinal); + Assert.Contains("std::vector items", new CppGenerator().Generate(function), StringComparison.Ordinal); + Assert.Contains("-> List[int]", new PythonGenerator().Generate(function), StringComparison.Ordinal); + } +} diff --git a/Coder/Ast/ClassDeclaration.cs b/Coder/Ast/ClassDeclaration.cs index def148a..dea80ec 100644 --- a/Coder/Ast/ClassDeclaration.cs +++ b/Coder/Ast/ClassDeclaration.cs @@ -39,7 +39,7 @@ public ClassDeclaration() /// /// Gets or sets the type this class derives from, or null when it derives from nothing. /// - public string? BaseType { get; set; } + public TypeReference? BaseType { get; set; } /// /// Gets or sets how widely the class is visible. @@ -66,7 +66,7 @@ public override AstNode Clone() ClassDeclaration clone = new() { Name = Name, - BaseType = BaseType, + BaseType = BaseType?.Clone(), Visibility = Visibility }; diff --git a/Coder/Ast/FunctionDeclaration.cs b/Coder/Ast/FunctionDeclaration.cs index b66b916..fac57ac 100644 --- a/Coder/Ast/FunctionDeclaration.cs +++ b/Coder/Ast/FunctionDeclaration.cs @@ -30,13 +30,40 @@ public FunctionDeclaration() /// /// Gets or sets the return type of the function. /// - public string? ReturnType { get; set; } + public TypeReference? ReturnType { get; set; } /// /// Gets or sets how widely the function is visible. /// public Visibility Visibility { get; set; } + /// + /// Gets or sets a value indicating whether the function is called without a receiver. + /// + /// + /// C++ and C# spell this static, JavaScript spells it static on a class member and + /// nothing at module scope, and Python spells it @staticmethod — which also decides whether + /// the method takes self, so this is the one modifier that changes a signature rather than + /// only decorating it. + /// + public bool IsStatic { get; set; } + + /// + /// Gets or sets a value indicating whether the function's result depends only on its arguments + /// and calling it changes nothing. + /// + /// + /// Not to be confused with C++'s pure virtual, which means a declaration has no + /// definition. That is a different property and the AST does not model it yet. + /// + /// Only two languages can say anything: C++ gets [[nodiscard]] and C# gets + /// [Pure]. Both are the same observation — discarding the result of a call that does + /// nothing else is always a mistake — rather than a promise to the optimiser, which is what + /// __attribute__((pure)) would be and which the AST is in no position to make. + /// + /// + public bool IsPure { get; set; } + /// /// Gets or sets a list of parameters for the function. /// @@ -62,8 +89,10 @@ public override AstNode Clone() FunctionDeclaration clone = new() { Name = Name, - ReturnType = ReturnType, - Visibility = Visibility + ReturnType = ReturnType?.Clone(), + Visibility = Visibility, + IsStatic = IsStatic, + IsPure = IsPure }; // Copy metadata diff --git a/Coder/Ast/Parameter.cs b/Coder/Ast/Parameter.cs index c61e666..013354b 100644 --- a/Coder/Ast/Parameter.cs +++ b/Coder/Ast/Parameter.cs @@ -39,7 +39,7 @@ public Parameter(string name, string type) /// /// Gets or sets the type of the parameter. /// - public string? Type { get; set; } + public TypeReference? Type { get; set; } /// /// Gets or sets a value indicating whether the parameter is optional. @@ -66,7 +66,7 @@ public override AstNode Clone() Parameter clone = new() { Name = Name, - Type = Type, + Type = Type?.Clone(), IsOptional = IsOptional, DefaultValue = DefaultValue }; diff --git a/Coder/Ast/TypeIndirection.cs b/Coder/Ast/TypeIndirection.cs new file mode 100644 index 0000000..91e06f2 --- /dev/null +++ b/Coder/Ast/TypeIndirection.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Ast; + +using System.Diagnostics.CodeAnalysis; + +/// +/// How a value of a type is reached. +/// +/// +/// Only the distinctions the generators can act on are modelled. A language that has no indirection +/// of its own — Python, JavaScript — emits and the same +/// way it emits , because the distinction has no spelling there rather than because +/// it was lost. +/// +[SuppressMessage("Naming", "CA1720:Identifier contains type name", + Justification = "A pointer is what this member means. Naming it around the rule would leave the enum " + + "unable to say the one thing it exists to distinguish.")] +public enum TypeIndirection +{ + /// The value itself. + None, + + /// A reference to the value, which is always bound. C++ spells this &. + Reference, + + /// A pointer to the value, which may be null. C++ spells this *. + Pointer, +} diff --git a/Coder/Ast/TypeReference.cs b/Coder/Ast/TypeReference.cs new file mode 100644 index 0000000..f03ee4f --- /dev/null +++ b/Coder/Ast/TypeReference.cs @@ -0,0 +1,371 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Ast; + +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +/// +/// A type as the AST understands it: a name, the types it is parameterised by, and how the value is +/// reached. +/// +/// +/// A type used to be a bare string on every node that carried one, which is enough for Foo and +/// hopeless for span<const Velocity> — a generator handed that string can only paste it, +/// so whoever built the node had to spell the target language themselves. Structure is what lets a +/// generator decide the spelling. +/// +/// and are inverses over everything +/// understands, and text it does not understand becomes a +/// holding that text verbatim — so a string that means nothing to this grammar +/// still survives a round trip intact rather than being dropped or half-read. That is what lets the +/// string-shaped properties on the nodes stay as they are while carrying structure underneath. +/// +/// +public sealed class TypeReference : IEquatable +{ + /// The token writes for . + private const string ConstKeyword = "const"; + + /// The other token accepts for . + private const string ReadOnlyKeyword = "readonly"; + + /// + /// Initializes a new instance of the class. + /// + public TypeReference() + { + } + + /// + /// Initializes a new instance of the class with a name. + /// + /// The type's name, unparameterised. + public TypeReference(string name) => Name = name; + + /// + /// Gets or sets the type's name, without its type arguments or qualifiers. + /// + /// + /// A qualified name — std::vector, System.Collections.Generic.List — is one name, + /// not a path the AST walks. Which separator a language uses is the generator's business. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets the types this one is parameterised by, in declaration order. + /// + public Collection TypeArguments { get; init; } = []; + + /// + /// Gets or sets a value indicating whether the value may not be written through. + /// + /// + /// Named for what it means rather than for how any one language spells it: C++ writes + /// const, C# writes in on a parameter, and Python writes nothing. + /// + public bool IsReadOnly { get; set; } + + /// + /// Gets or sets how the value is reached. + /// + public TypeIndirection Indirection { get; set; } + + /// + /// Reads a type from its text form. + /// + /// The text to read. + /// + /// The type the text describes, or one whose is the text verbatim when the + /// text is not something this grammar can read. + /// + /// + /// The grammar is deliberately small: an optional const or readonly, a name, an + /// optional angle-bracketed argument list, and any number of & or * suffixes. + /// It exists to read what the string-shaped properties already hold, not to parse a language. + /// + public static TypeReference Parse(string text) + { + Ensure.NotNull(text); + + int position = 0; + TypeReference? parsed = TryRead(text, ref position); + SkipWhitespace(text, ref position); + + // Anything left over means the grammar read only part of the text, which would silently + // discard the rest. The whole text as a name is wrong-but-lossless; a half-read type is not. + return parsed is not null && position == text.Length + ? parsed + : new TypeReference(text); + } + + /// + /// Reads a type from its text form, so that a plain name can be written where a type is wanted. + /// + /// The text to read, or for no type. + /// The type the text describes, or . + /// + /// Implicit because a name is the overwhelmingly common case and requiring + /// at every one of them would be noise. The named alternate is + /// ; the inverse is . + /// + [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", + Justification = "Parse is the named alternate for this direction and ToString for the other.")] + public static implicit operator TypeReference?(string? text) => text is null ? null : Parse(text); + + /// + /// Writes this type in the AST's own text form. + /// + /// The text form, which reads back to an equal type. + /// + /// This is the AST's spelling, not any target language's. A generator that cares about the + /// difference reads the structure instead — which is the whole reason the structure exists. + /// + public override string ToString() + { + StringBuilder text = new(); + + if (IsReadOnly) + { + text.Append(ConstKeyword).Append(' '); + } + + text.Append(Name); + + if (TypeArguments.Count > 0) + { + text.Append('<'); + for (int index = 0; index < TypeArguments.Count; index++) + { + if (index > 0) + { + text.Append(", "); + } + + text.Append(TypeArguments[index].ToString()); + } + + text.Append('>'); + } + + return text.Append(Indirection switch + { + TypeIndirection.Reference => "&", + TypeIndirection.Pointer => "*", + _ => string.Empty, + }).ToString(); + } + + /// + /// Creates a deep copy of this type. + /// + /// A new type equal to this one, sharing none of its argument instances. + public TypeReference Clone() + { + TypeReference clone = new() + { + Name = Name, + IsReadOnly = IsReadOnly, + Indirection = Indirection, + }; + + foreach (TypeReference argument in TypeArguments) + { + clone.TypeArguments.Add(argument.Clone()); + } + + return clone; + } + + /// + public bool Equals(TypeReference? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return string.Equals(Name, other.Name, StringComparison.Ordinal) + && IsReadOnly == other.IsReadOnly + && Indirection == other.Indirection + && TypeArguments.SequenceEqual(other.TypeArguments); + } + + /// + public override bool Equals(object? obj) => Equals(obj as TypeReference); + + /// + public override int GetHashCode() + { + HashCode hash = new(); + hash.Add(Name, StringComparer.Ordinal); + hash.Add(IsReadOnly); + hash.Add(Indirection); + foreach (TypeReference argument in TypeArguments) + { + hash.Add(argument); + } + + return hash.ToHashCode(); + } + + /// Compares two types for equality. + /// The first type. + /// The second type. + /// when both describe the same type. + public static bool operator ==(TypeReference? left, TypeReference? right) => + left is null ? right is null : left.Equals(right); + + /// Compares two types for inequality. + /// The first type. + /// The second type. + /// when they describe different types. + public static bool operator !=(TypeReference? left, TypeReference? right) => !(left == right); + + /// + /// Reads one type starting at , leaving it after the last character + /// consumed. + /// + /// The text being read. + /// Where to start, updated to where reading stopped. + /// The type read, or when the text at that point is not one. + private static TypeReference? TryRead(string text, ref int position) + { + SkipWhitespace(text, ref position); + + bool isReadOnly = TryReadQualifier(text, ref position); + string name = ReadName(text, ref position); + if (name.Length == 0) + { + return null; + } + + TypeReference type = new(name) { IsReadOnly = isReadOnly }; + + SkipWhitespace(text, ref position); + if (position < text.Length && text[position] == '<' && !TryReadArguments(text, ref position, type)) + { + return null; + } + + SkipWhitespace(text, ref position); + if (position < text.Length && (text[position] == '&' || text[position] == '*')) + { + type.Indirection = text[position] == '&' ? TypeIndirection.Reference : TypeIndirection.Pointer; + position++; + } + + return type; + } + + /// + /// Reads the angle-bracketed argument list into . + /// + /// The text being read. + /// The position of the opening bracket, updated past the closing one. + /// The type to add the arguments to. + /// when a well-formed list was read. + private static bool TryReadArguments(string text, ref int position, TypeReference type) + { + position++; + + while (true) + { + TypeReference? argument = TryRead(text, ref position); + if (argument is null) + { + return false; + } + + type.TypeArguments.Add(argument); + + SkipWhitespace(text, ref position); + if (position >= text.Length) + { + return false; + } + + if (text[position] == ',') + { + position++; + continue; + } + + if (text[position] == '>') + { + position++; + return true; + } + + return false; + } + } + + /// + /// Reads a leading const or readonly, if one is there. + /// + /// The text being read. + /// Where to start, updated past the keyword when one was read. + /// when a qualifier was read. + private static bool TryReadQualifier(string text, ref int position) + { + foreach (string keyword in new[] { ConstKeyword, ReadOnlyKeyword }) + { + // The space matters: `constant` starts with `const` and is a name, not a qualified one. + if (position + keyword.Length < text.Length + && string.CompareOrdinal(text, position, keyword, 0, keyword.Length) == 0 + && char.IsWhiteSpace(text[position + keyword.Length])) + { + position += keyword.Length; + SkipWhitespace(text, ref position); + return true; + } + } + + return false; + } + + /// + /// Reads a name: everything up to a character the grammar gives its own meaning. + /// + /// The text being read. + /// Where to start, updated past the name. + /// The name read, which is empty when there is none. + private static string ReadName(string text, ref int position) + { + int start = position; + while (position < text.Length && !IsPunctuation(text[position]) && !char.IsWhiteSpace(text[position])) + { + position++; + } + + return text[start..position]; + } + + /// + /// Reports whether a character is one the grammar reads rather than one a name may contain. + /// + /// The character to test. + /// when the character ends a name. + private static bool IsPunctuation(char character) => + character is '<' or '>' or ',' or '&' or '*'; + + /// + /// Advances past any whitespace. + /// + /// The text being read. + /// Where to start, updated past the whitespace. + private static void SkipWhitespace(string text, ref int position) + { + while (position < text.Length && char.IsWhiteSpace(text[position])) + { + position++; + } + } +} diff --git a/Coder/Ast/VariableDeclaration.cs b/Coder/Ast/VariableDeclaration.cs index 06ee23f..5f11391 100644 --- a/Coder/Ast/VariableDeclaration.cs +++ b/Coder/Ast/VariableDeclaration.cs @@ -17,7 +17,7 @@ public class VariableDeclaration : AstNode, IHasVisibility /// Gets or sets the declared type of the variable. /// Can be null for type-inferred declarations. /// - public string? Type { get; set; } + public TypeReference? Type { get; set; } /// /// Gets or sets the initial value expression. @@ -82,7 +82,7 @@ public override AstNode Clone() VariableDeclaration clone = new() { Name = Name, - Type = Type, + Type = Type?.Clone(), InitialValue = (Expression?)InitialValue?.DeepClone(), IsConstant = IsConstant, IsTypeInferred = IsTypeInferred, @@ -104,7 +104,7 @@ public override AstNode Clone() /// String representation. public override string ToString() { - string typeInfo = IsTypeInferred ? "var" : Type ?? "?"; + string typeInfo = IsTypeInferred ? "var" : Type?.ToString() ?? "?"; string valueInfo = InitialValue != null ? $" = {InitialValue}" : ""; string modifiers = Visibility != Visibility.Unspecified ? $"{Visibility.ToString().ToLowerInvariant()} " : ""; modifiers += IsConstant ? "const " : ""; diff --git a/Coder/Languages/CSharpGenerator.cs b/Coder/Languages/CSharpGenerator.cs index e4d82b6..ba679eb 100644 --- a/Coder/Languages/CSharpGenerator.cs +++ b/Coder/Languages/CSharpGenerator.cs @@ -4,6 +4,7 @@ namespace ktsu.Coder.Languages; using System.Collections.Generic; using System.Globalization; +using System.Linq; using ktsu.Coder.Ast; using ktsu.CodeBlocker; @@ -113,9 +114,9 @@ private void GenerateClass(ClassDeclaration classDecl, CodeBlocker code) { code.Write($"{SpellVisibility(classDecl.Visibility) ?? "public"} class {classDecl.Name ?? "UnnamedClass"}"); - if (!string.IsNullOrEmpty(classDecl.BaseType)) + if (classDecl.BaseType is TypeReference baseType) { - code.Write($" : {MapToCSType(classDecl.BaseType!)}"); + code.Write($" : {MapToCSType(baseType)}"); } // The line is ended before the scope opens, so C#'s brace lands on its own line. @@ -128,11 +129,33 @@ private void GenerateClass(ClassDeclaration classDecl, CodeBlocker code) } } + /// + /// Emits a function. + /// + /// The function to emit. + /// The writer to emit into. + /// + /// A pure function carries [Pure], written out in full because the AST has no import to + /// hang a using on: a generated file is a fragment, and a short name in it would be one + /// the reader has to arrange for. + /// private void GenerateFunction(FunctionDeclaration function, CodeBlocker code) { + if (function.IsPure) + { + code.WriteLine("[System.Diagnostics.Contracts.Pure]"); + } + // Build method signature. A function nobody has given a visibility to is public: an // inaccessible method is not what someone who wrote no modifier meant. - code.Write($"{SpellVisibility(function.Visibility) ?? "public"} {MapToCSType(function.ReturnType ?? "void")} {function.Name}("); + code.Write($"{SpellVisibility(function.Visibility) ?? "public"} "); + + if (function.IsStatic) + { + code.Write("static "); + } + + code.Write($"{MapToCSType(function.ReturnType ?? new TypeReference("void"))} {function.Name}("); // Add parameters for (int i = 0; i < function.Parameters.Count; i++) @@ -158,7 +181,7 @@ private void GenerateFunction(FunctionDeclaration function, CodeBlocker code) private static void GenerateParameter(Parameter parameter, CodeBlocker code) { - code.Write($"{MapToCSType(parameter.Type ?? "object")} {parameter.Name}"); + code.Write($"{MapToCSType(parameter.Type ?? new TypeReference("object"))} {parameter.Name}"); if (parameter.IsOptional && !string.IsNullOrEmpty(parameter.DefaultValue)) { @@ -173,20 +196,71 @@ private static void GenerateParameter(Parameter parameter, CodeBlocker code) { "float", "float" }, { "double", "double" }, { "bool", "bool" }, - { "list", "List" }, - { "dict", "Dictionary" }, + { "list", "List" }, + { "dict", "Dictionary" }, { "void", "void" } }; - private static string MapToCSType(string pythonType) => - TypeMappings.TryGetValue(pythonType, out string? mapped) - ? mapped - : string.Equals(pythonType, "void", StringComparison.OrdinalIgnoreCase) ? "void" : pythonType; + /// + /// What a container named without arguments is a container of. + /// + /// + /// Keyed by the name as written rather than by the mapped one, because that is what the schema + /// said. A list<int> is a List<int> and never reaches here. + /// + private static readonly Dictionary DefaultTypeArguments = new(StringComparer.OrdinalIgnoreCase) + { + { "list", "" }, + { "dict", "" } + }; + + /// + /// Spells a type in C#. + /// + /// The type to spell. + /// The C# source for it. + /// + /// C# has no const on a type and no pointer outside unsafe code, so + /// and have no + /// spelling here. Read-only-ness of an argument is in, which belongs on the parameter + /// rather than on the type, and is not something the AST can say yet. + /// + private static string MapToCSType(TypeReference type) + { + string name = MapTypeName(type.Name); + + if (type.TypeArguments.Count > 0) + { + return $"{name}<{string.Join(", ", type.TypeArguments.Select(MapToCSType))}>"; + } + + return DefaultTypeArguments.TryGetValue(type.Name, out string? fallback) ? $"{name}{fallback}" : name; + } + + /// + /// Spells a type's name in C#, leaving any arguments to the caller. + /// + /// The name as the AST holds it. + /// The C# name. + /// + /// void is matched case-insensitively where the rest of the table is not, because it is + /// the one name a caller reaches for without knowing which language's casing the AST was written + /// in — a return type left unset is spelled void by the generator itself. + /// + private static string MapTypeName(string name) + { + if (TypeMappings.TryGetValue(name, out string? mapped)) + { + return mapped; + } + + return string.Equals(name, "void", StringComparison.OrdinalIgnoreCase) ? "void" : name; + } private void GenerateVariableDeclaration(VariableDeclaration varDecl, CodeBlocker code) { // Use type or var for type inference - string type = varDecl.IsTypeInferred || string.IsNullOrEmpty(varDecl.Type) + string type = varDecl.IsTypeInferred || varDecl.Type is null ? "var" : MapToCSType(varDecl.Type); diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs index 48b03dc..432bc27 100644 --- a/Coder/Languages/CppGenerator.cs +++ b/Coder/Languages/CppGenerator.cs @@ -4,6 +4,7 @@ namespace ktsu.Coder.Languages; using System; using System.Collections.Generic; +using System.Linq; using ktsu.Coder.Ast; using ktsu.CodeBlocker; @@ -30,8 +31,8 @@ public class CppGenerator : StandardLanguageGenerator { "float", "float" }, { "double", "double" }, { "bool", "bool" }, - { "list", "std::vector" }, - { "dict", "std::map" }, + { "list", "std::vector" }, + { "dict", "std::map" }, { "void", "void" }, { "object", "std::any" } }; @@ -52,12 +53,28 @@ public class CppGenerator : StandardLanguageGenerator public override string FileExtension => "cpp"; /// + /// + /// A pure function is written [[nodiscard]]: discarding the result of a call that does + /// nothing else is always a mistake, and that is the whole of what the standard can say. The + /// compiler-specific __attribute__((pure)) asserts to the optimiser that the call may be + /// elided or duplicated, which is a stronger promise than the AST is in a position to make. + /// protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl, CodeBlocker code) { Ensure.NotNull(funcDecl); Ensure.NotNull(code); - code.Write($"{MapToCppType(funcDecl.ReturnType ?? "void")} {funcDecl.Name ?? "unnamedFunction"}("); + if (funcDecl.IsPure) + { + code.Write("[[nodiscard]] "); + } + + if (funcDecl.IsStatic) + { + code.Write("static "); + } + + code.Write($"{MapToCppType(funcDecl.ReturnType ?? new TypeReference("void"))} {funcDecl.Name ?? "unnamedFunction"}("); GenerateParameterList(funcDecl.Parameters, code); // The line is ended before the scope opens, so C++'s brace lands on its own line. @@ -88,9 +105,9 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod code.Write($"class {classDecl.Name ?? "UnnamedClass"}"); - if (!string.IsNullOrEmpty(classDecl.BaseType)) + if (classDecl.BaseType is TypeReference baseType) { - code.Write($" : public {MapToCppType(classDecl.BaseType!)}"); + code.Write($" : public {MapToCppType(baseType)}"); } code.WriteLine(); @@ -200,7 +217,7 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, Ensure.NotNull(parameter); Ensure.NotNull(code); - code.Write($"{MapToCppType(parameter.Type ?? "object")} {parameter.Name ?? $"param{position}"}"); + code.Write($"{MapToCppType(parameter.Type ?? new TypeReference("object"))} {parameter.Name ?? $"param{position}"}"); AppendDefaultValue(parameter, code); } @@ -237,14 +254,66 @@ protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, /// private static string GetDeclaredType(VariableDeclaration varDecl) { - if (!varDecl.IsTypeInferred && !string.IsNullOrEmpty(varDecl.Type)) + if (!varDecl.IsTypeInferred && varDecl.Type is TypeReference declared) { - return MapToCppType(varDecl.Type!); + return MapToCppType(declared); } return varDecl.InitialValue is not null ? "auto" : "std::any"; } - private static string MapToCppType(string type) => - TypeMappings.TryGetValue(type, out string? mapped) ? mapped : type; + /// + /// What a container named without arguments is a container of. + /// + /// + /// list comes from languages that do not say what is in one, and C++ insists. These are + /// keyed by the name as written rather than by the mapped one, because that is what the schema + /// said. A list<int> is a std::vector<int> and never reaches here. + /// + private static readonly Dictionary DefaultTypeArguments = new(StringComparer.OrdinalIgnoreCase) + { + { "list", "" }, + { "dict", "" } + }; + + /// + /// Spells a type in C++. + /// + /// The type to spell. + /// The C++ source for it. + /// + /// Only the name is mapped; the shape around it — arguments, const, & and + /// * — is C++'s own spelling of what the type says, which is what the string form could + /// not express. + /// + private static string MapToCppType(TypeReference type) + { + string name = TypeMappings.TryGetValue(type.Name, out string? mapped) ? mapped : type.Name; + + string arguments = SpellTypeArguments(type); + + string indirection = type.Indirection switch + { + TypeIndirection.Reference => "&", + TypeIndirection.Pointer => "*", + _ => string.Empty, + }; + + return $"{(type.IsReadOnly ? "const " : string.Empty)}{name}{arguments}{indirection}"; + } + + /// + /// Spells a type's argument list, supplying the one a bare container does not name. + /// + /// The type whose arguments to spell. + /// The angle-bracketed list, or nothing when the type takes no arguments. + private static string SpellTypeArguments(TypeReference type) + { + if (type.TypeArguments.Count > 0) + { + return $"<{string.Join(", ", type.TypeArguments.Select(MapToCppType))}>"; + } + + return DefaultTypeArguments.TryGetValue(type.Name, out string? fallback) ? fallback : string.Empty; + } } diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index d494d03..768d9d2 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -69,9 +69,9 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod code.Write($"class {classDecl.Name ?? "UnnamedClass"}"); - if (!string.IsNullOrEmpty(classDecl.BaseType)) + if (classDecl.BaseType is TypeReference baseType) { - code.Write($" extends {classDecl.BaseType}"); + code.Write($" extends {baseType.Name}"); } // The line is left open, so the scope's brace lands on it: JavaScript braces hang. @@ -132,6 +132,11 @@ private void GenerateField(VariableDeclaration field, CodeBlocker code) /// The writer to emit into. private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) { + if (method.IsStatic) + { + code.Write("static "); + } + code.Write($"{MemberName(method.Name ?? "unnamedMethod", method.Visibility)}("); GenerateParameterList(method.Parameters, code); code.Write(") "); diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs index ee45a4f..ef5be25 100644 --- a/Coder/Languages/PythonGenerator.cs +++ b/Coder/Languages/PythonGenerator.cs @@ -102,9 +102,9 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod code.Write($"class {classDecl.Name ?? "UnnamedClass"}"); - if (!string.IsNullOrEmpty(classDecl.BaseType)) + if (classDecl.BaseType is TypeReference baseType) { - code.Write($"({classDecl.BaseType})"); + code.Write($"({PythonTypeFromGenericType(baseType)})"); } code.WriteLine(":"); @@ -144,15 +144,36 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod /// self is Python's spelling of the receiver a method is called on. It is not carried in /// the AST — no other target language has it — so it is supplied here rather than being something /// the user has to remember to add as a parameter and then remove for every other language. + /// + /// A static method is the one case where that receiver is not supplied, because + /// @staticmethod is what says there is none. Every other language spells static + /// alongside the signature; Python spells it by changing the signature. + /// /// private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) { - code.Write($"def {method.Name ?? "unnamed_method"}(self"); + if (method.IsStatic) + { + code.WriteLine("@staticmethod"); + } - foreach (Parameter parameter in method.Parameters) + code.Write($"def {method.Name ?? "unnamed_method"}("); + + bool needsSeparator = !method.IsStatic; + if (needsSeparator) { - code.Write(", "); - GenerateParameter(parameter, code, method.Parameters.IndexOf(parameter)); + code.Write("self"); + } + + for (int index = 0; index < method.Parameters.Count; index++) + { + if (needsSeparator) + { + code.Write(", "); + } + + GenerateParameter(method.Parameters[index], code, index); + needsSeparator = true; } code.Write(")"); @@ -253,9 +274,19 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, AppendDefaultValue(parameter, code); } - private static string PythonTypeFromGenericType(string genericType) + /// + /// Spells a type in Python. + /// + /// The type to spell. + /// The Python source for it. + /// + /// Python parameterises a type with brackets rather than angle brackets, which is only spellable + /// now that the arguments are a list rather than part of a name. Read-only-ness and indirection + /// have no spelling in Python at all, so neither is emitted. + /// + private static string PythonTypeFromGenericType(TypeReference type) { - return genericType.ToLowerInvariant() switch + string name = type.Name.ToLowerInvariant() switch { "int" => "int", "string" => "str", @@ -263,8 +294,12 @@ private static string PythonTypeFromGenericType(string genericType) "float" => "float", "double" => "float", "void" => "None", - _ => genericType + _ => type.Name }; + + return type.TypeArguments.Count == 0 + ? name + : $"{name}[{string.Join(", ", type.TypeArguments.Select(PythonTypeFromGenericType))}]"; } /// diff --git a/Coder/Serialization/YamlDeserializer.cs b/Coder/Serialization/YamlDeserializer.cs index 41d2e51..1a20d2e 100644 --- a/Coder/Serialization/YamlDeserializer.cs +++ b/Coder/Serialization/YamlDeserializer.cs @@ -125,6 +125,18 @@ private static void DeserializeFunctionBasicProperties(FunctionDeclaration funcD funcDecl.ReturnType = returnTypeObj?.ToString(); } + if (dict.TryGetValue("isStatic", out object? staticObj) && + bool.TryParse(staticObj?.ToString(), out bool isStatic)) + { + funcDecl.IsStatic = isStatic; + } + + if (dict.TryGetValue("isPure", out object? pureObj) && + bool.TryParse(pureObj?.ToString(), out bool isPure)) + { + funcDecl.IsPure = isPure; + } + DeserializeVisibility(funcDecl, dict); } diff --git a/Coder/Serialization/YamlSerializer.cs b/Coder/Serialization/YamlSerializer.cs index a62da27..990658c 100644 --- a/Coder/Serialization/YamlSerializer.cs +++ b/Coder/Serialization/YamlSerializer.cs @@ -150,11 +150,23 @@ private static void SerializeFunctionDeclaration(FunctionDeclaration funcDecl, D if (funcDecl.ReturnType != null) { - nodeData["returnType"] = funcDecl.ReturnType; + nodeData["returnType"] = funcDecl.ReturnType.ToString(); } SerializeVisibility(funcDecl, nodeData); + // Written only when true: a modifier nobody asked for should not appear in the document, the + // same way an unspecified visibility does not. + if (funcDecl.IsStatic) + { + nodeData["isStatic"] = funcDecl.IsStatic; + } + + if (funcDecl.IsPure) + { + nodeData["isPure"] = funcDecl.IsPure; + } + if (funcDecl.Parameters.Count > 0) { nodeData["parameters"] = SerializeParameters(funcDecl.Parameters); @@ -212,7 +224,7 @@ private static void SerializeClassDeclaration(ClassDeclaration classDecl, Dictio if (classDecl.BaseType != null) { - nodeData["baseType"] = classDecl.BaseType; + nodeData["baseType"] = classDecl.BaseType.ToString(); } SerializeVisibility(classDecl, nodeData); @@ -259,7 +271,7 @@ private static void SerializeParameter(Parameter param, Dictionary