Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<const Velocity>`
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
Expand Down
14 changes: 10 additions & 4 deletions Coder.Graph/AstFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,17 @@ public static IReadOnlyList<AstField> 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 =>
Expand All @@ -130,15 +132,15 @@ public static IReadOnlyList<AstField> 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),
],

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),
Expand Down Expand Up @@ -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),
Expand Down
244 changes: 244 additions & 0 deletions Coder.Test/Ast/FunctionModifierTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests for <see cref="FunctionDeclaration.IsStatic"/> and <see cref="FunctionDeclaration.IsPure"/>:
/// what each language spells them as, and that they survive a round trip through YAML.
/// </summary>
/// <remarks>
/// 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 <c>self</c>. 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.
/// </remarks>
[TestClass]
public class FunctionModifierTests
{
/// <summary>
/// Builds a class with one static pure method, which is the shape most of these use.
/// </summary>
/// <returns>The class.</returns>
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;
}

/// <summary>
/// Both are off unless asked for, so an existing function is unaffected by their existence.
/// </summary>
[TestMethod]
public void Neither_IsSetByDefault()
{
FunctionDeclaration function = new("area");

Assert.IsFalse(function.IsStatic);
Assert.IsFalse(function.IsPure);
}

/// <summary>
/// C# writes <c>static</c> in the signature and <c>[Pure]</c> above it.
/// </summary>
[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);
}

/// <summary>
/// C++ writes <c>static</c>, and spells purity as the one thing the standard can say about it.
/// </summary>
[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);
}

/// <summary>
/// Python spells static by dropping the receiver, which is the whole reason this modifier is not
/// just a keyword the generators paste in.
/// </summary>
[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);
}

/// <summary>
/// An ordinary method still takes its receiver, and still separates it from the first parameter.
/// </summary>
[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);
}

/// <summary>
/// Purity has no spelling in Python, so nothing is emitted for it rather than something invented.
/// </summary>
[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));
}

/// <summary>
/// JavaScript spells static on a class member and has nothing to say about purity.
/// </summary>
[TestMethod]
public void JavaScript_WritesStaticOnAMethod()
{
string code = new JavaScriptGenerator().Generate(SampleClass());

Assert.Contains("static distance(scale)", code, StringComparison.Ordinal);
}

/// <summary>
/// A static private member keeps both spellings, which are separate parts of the same declaration.
/// </summary>
[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);
}

/// <summary>
/// Both survive a round trip through YAML.
/// </summary>
[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);
}

/// <summary>
/// A function that asked for neither writes neither key, so the document says nothing rather than
/// saying false twice on every function ever written.
/// </summary>
[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);
}

/// <summary>
/// A document written before these existed still opens, with both off.
/// </summary>
[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);
}

/// <summary>
/// A clone carries both, so copying a function does not quietly drop what it was marked as.
/// </summary>
[TestMethod]
public void Clone_CarriesBothModifiers()
{
FunctionDeclaration clone = (FunctionDeclaration)new FunctionDeclaration("distance")
{
IsStatic = true,
IsPure = true,
}.Clone();

Assert.IsTrue(clone.IsStatic);
Assert.IsTrue(clone.IsPure);
}

/// <summary>
/// The inspector offers both as flags and writes what the user ticked.
/// </summary>
[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);
}
}
Loading