diff --git a/CLAUDE.md b/CLAUDE.md index 8665918..1851d71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,10 @@ source in four target languages. The solution uses: ### Key Files - `Coder/Ast/*.cs` — one file per node type. `AstNode` is the base; `AstCompositeNode` adds a - keyed child dictionary; `Expression` marks the nodes that evaluate to a value. + 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. - `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 4887caf..b01ce67 100644 --- a/Coder.Graph/AstFields.cs +++ b/Coder.Graph/AstFields.cs @@ -82,14 +82,18 @@ public AstField(string name, AstFieldKind kind, string value) public static class AstFields { /// - /// The access modifiers a class declaration offers. + /// The visibilities a declaration offers. /// - private static readonly IReadOnlyList AccessModifiers = + /// + /// Built from the enumeration so a visibility the AST gains appears in the inspector without + /// anyone remembering to list it here. is labelled for what + /// it means rather than by its name: it is not a fifth modifier, it is the absence of one. + /// + private static readonly IReadOnlyList Visibilities = [ - new("public", "public"), - new("internal", "internal"), - new("protected", "protected"), - new("private", "private"), + .. Enum.GetValues().Select(visibility => new AstFieldChoice( + visibility.ToString(), + visibility == Visibility.Unspecified ? "(language default)" : visibility.ToString().ToLowerInvariant())), ]; /// @@ -107,13 +111,20 @@ public static IReadOnlyList Of(AstNode node) [ new("Name", AstFieldKind.Text, classDecl.Name ?? string.Empty), new("BaseType", AstFieldKind.Text, classDecl.BaseType ?? string.Empty), - new("Access", AstFieldKind.Choice, classDecl.AccessModifier ?? "public", AccessModifiers), + 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("Visibility", AstFieldKind.Choice, function.Visibility.ToString(), Visibilities), + ], + + EntryPoint entryPoint => + [ + new("Arguments", AstFieldKind.Flag, Spell(entryPoint.AcceptsArguments)), + new("ExitCode", AstFieldKind.Flag, Spell(entryPoint.ReturnsExitCode)), ], Parameter parameter => @@ -130,7 +141,7 @@ public static IReadOnlyList Of(AstNode node) new("Type", AstFieldKind.Text, varDecl.Type ?? string.Empty), new("Constant", AstFieldKind.Flag, Spell(varDecl.IsConstant)), new("Inferred", AstFieldKind.Flag, Spell(varDecl.IsTypeInferred)), - new("Access", AstFieldKind.Text, varDecl.AccessModifier ?? string.Empty), + new("Visibility", AstFieldKind.Choice, varDecl.Visibility.ToString(), Visibilities), ], VariableReference varRef => @@ -207,10 +218,18 @@ public static bool TryWrite(AstNode node, string fieldName, string value) { (ClassDeclaration classDecl, "Name") => Assign(() => classDecl.Name = OrNull(value)), (ClassDeclaration classDecl, "BaseType") => Assign(() => classDecl.BaseType = OrNull(value)), - (ClassDeclaration classDecl, "Access") => Assign(() => classDecl.AccessModifier = OrNull(value)), + (ClassDeclaration classDecl, "Visibility") => + TryParseVisibility(value, out Visibility classVisibility) && Assign(() => classDecl.Visibility = classVisibility), (FunctionDeclaration function, "Name") => Assign(() => function.Name = OrNull(value)), (FunctionDeclaration function, "ReturnType") => Assign(() => function.ReturnType = OrNull(value)), + (FunctionDeclaration function, "Visibility") => + TryParseVisibility(value, out Visibility functionVisibility) && Assign(() => function.Visibility = functionVisibility), + + (EntryPoint entryPoint, "Arguments") => + TryParseBool(value, out bool acceptsArguments) && Assign(() => entryPoint.AcceptsArguments = acceptsArguments), + (EntryPoint entryPoint, "ExitCode") => + TryParseBool(value, out bool returnsExitCode) && Assign(() => entryPoint.ReturnsExitCode = returnsExitCode), (Parameter parameter, "Name") => Assign(() => parameter.Name = OrNull(value)), (Parameter parameter, "Type") => Assign(() => parameter.Type = OrNull(value)), @@ -221,7 +240,8 @@ public static bool TryWrite(AstNode node, string fieldName, string value) (VariableDeclaration varDecl, "Type") => Assign(() => varDecl.Type = OrNull(value)), (VariableDeclaration varDecl, "Constant") => TryParseBool(value, out bool constant) && Assign(() => varDecl.IsConstant = constant), (VariableDeclaration varDecl, "Inferred") => TryParseBool(value, out bool inferred) && Assign(() => varDecl.IsTypeInferred = inferred), - (VariableDeclaration varDecl, "Access") => Assign(() => varDecl.AccessModifier = OrNull(value)), + (VariableDeclaration varDecl, "Visibility") => + TryParseVisibility(value, out Visibility varVisibility) && Assign(() => varDecl.Visibility = varVisibility), (VariableReference varRef, "Name") => value.Length > 0 && Assign(() => varRef.Name = value), @@ -316,6 +336,11 @@ private static bool TryParseDouble(string value, out double result) => private static bool TryParseBool(string value, out bool result) => bool.TryParse(value, out result); + // Case-insensitively, so a document hand-edited with "public" reads back the same as the + // inspector's own "Public". + private static bool TryParseVisibility(string value, out Visibility result) => + Enum.TryParse(value, ignoreCase: true, out result); + private static string Spell(bool value) => value ? "true" : "false"; private static string Spell(int value) => value.ToString(CultureInfo.InvariantCulture); diff --git a/Coder.Graph/AstNodeCatalog.cs b/Coder.Graph/AstNodeCatalog.cs index 76fcf94..e7f5747 100644 --- a/Coder.Graph/AstNodeCatalog.cs +++ b/Coder.Graph/AstNodeCatalog.cs @@ -48,6 +48,8 @@ public static class AstNodeCatalog new("Declarations", "Function", () => new FunctionDeclaration("newFunction") { ReturnType = "void" }), new("Declarations", "Parameter", () => new Parameter("value", "int")), new("Declarations", "Variable", () => new VariableDeclaration("value", "int")), + new("Declarations", "Constant", () => new VariableDeclaration("VALUE", "int", Literal.Number(0)) { IsConstant = true }), + new("Declarations", "Entry point", () => new EntryPoint()), new("Statements", "Return", () => new ReturnStatement()), .. Enum.GetValues().Select(op => new AstNodeTemplate( diff --git a/Coder.Graph/AstSchema.cs b/Coder.Graph/AstSchema.cs index 1db3b8e..8a411d8 100644 --- a/Coder.Graph/AstSchema.cs +++ b/Coder.Graph/AstSchema.cs @@ -45,6 +45,7 @@ public static class AstSchema { ClassDeclaration => [MembersSlot], FunctionDeclaration => [ParametersSlot, BodySlot], + EntryPoint => [BodySlot], ReturnStatement => [ExpressionSlot], BinaryExpression => [LeftSlot, RightSlot], UnaryExpression => [OperandSlot], @@ -87,6 +88,7 @@ public static IReadOnlyList ChildrenOf(AstNode node, AstSlot slot) (ClassDeclaration classDecl, "Members") => [.. classDecl.Members], (FunctionDeclaration function, "Parameters") => [.. function.Parameters], (FunctionDeclaration function, "Body") => [.. function.Body], + (EntryPoint entryPoint, "Body") => [.. entryPoint.Body], _ when SlotsOf(node).Contains(slot) => [], _ => throw new ArgumentException($"{node.GetNodeTypeName()} has no slot named '{slot.Name}'.", nameof(slot)), }; @@ -161,6 +163,10 @@ public static bool TryAttachAt(AstNode parent, AstSlot slot, int index, AstNode function.Body.Add(child); return true; + case (EntryPoint entryPoint, "Body"): + entryPoint.Body.Add(child); + return true; + case (ClassDeclaration classDecl, "Members"): classDecl.Members.Add(child); return true; @@ -190,6 +196,10 @@ private static bool TryReplaceAt(AstNode parent, AstSlot slot, int index, AstNod function.Body[index] = child; return true; + case (EntryPoint entryPoint, "Body"): + entryPoint.Body[index] = child; + return true; + case (ClassDeclaration classDecl, "Members"): classDecl.Members[index] = child; return true; @@ -266,6 +276,10 @@ public static bool TryDetachAt(AstNode parent, AstSlot slot, int index) function.Body.RemoveAt(index); return true; + case (EntryPoint entryPoint, "Body") when index < entryPoint.Body.Count: + entryPoint.Body.RemoveAt(index); + return true; + case (ClassDeclaration classDecl, "Members") when index < classDecl.Members.Count: classDecl.Members.RemoveAt(index); return true; @@ -332,8 +346,10 @@ public static bool Accepts(AstSlot slot, AstNode candidate) { AstSlotKind.Parameter => candidate is Parameter, AstSlotKind.Expression => IsExpression(candidate), - AstSlotKind.Statement => candidate is not Parameter, - AstSlotKind.Member => candidate is FunctionDeclaration or VariableDeclaration or ClassDeclaration, + // A parameter is not a statement, and neither is an entry point: a program starts + // running at one, so it belongs to a class or to the document rather than inside a body. + AstSlotKind.Statement => candidate is not (Parameter or EntryPoint), + AstSlotKind.Member => candidate is FunctionDeclaration or VariableDeclaration or ClassDeclaration or EntryPoint, _ => false, }; } @@ -367,6 +383,7 @@ public static string Describe(AstNode node) { ClassDeclaration classDecl => $"class {classDecl.Name ?? ""}", FunctionDeclaration function => $"function {function.Name ?? ""}", + EntryPoint => "entry point", Parameter parameter => $"param {parameter.Name ?? ""}", ReturnStatement => "return", BinaryExpression binary => $"binary {SpellOrName(binary.Operator)}", diff --git a/Coder.Test/Ast/ClassDeclarationTests.cs b/Coder.Test/Ast/ClassDeclarationTests.cs index 62043aa..7c7a102 100644 --- a/Coder.Test/Ast/ClassDeclarationTests.cs +++ b/Coder.Test/Ast/ClassDeclarationTests.cs @@ -137,13 +137,13 @@ public void CSharp_GeneratesAClass() } /// - /// Tests that the access modifier a class carries is the one emitted, and that a class without one + /// Tests that the visibility a class carries is the one emitted, and that a class without one /// is public rather than nothing at all. /// [TestMethod] - public void CSharp_UsesTheDeclaredAccessModifier() + public void CSharp_UsesTheDeclaredVisibility() { - ClassDeclaration declaration = new("Point") { AccessModifier = "internal" }; + ClassDeclaration declaration = new("Point") { Visibility = Visibility.Internal }; StringAssert.StartsWith(new CSharpGenerator().Generate(declaration), "internal class Point", StringComparison.Ordinal); StringAssert.StartsWith(new CSharpGenerator().Generate(new ClassDeclaration("Point")), "public class Point", StringComparison.Ordinal); @@ -252,13 +252,13 @@ public void Yaml_RoundTripsAClass() public void Yaml_RoundTripsANestedClass() { ClassDeclaration outer = new("Outer"); - outer.Members.Add(new ClassDeclaration("Inner") { AccessModifier = "private" }); + outer.Members.Add(new ClassDeclaration("Inner") { Visibility = Visibility.Private }); string yaml = new YamlSerializer().Serialize(outer); ClassDeclaration restored = (ClassDeclaration)new YamlDeserializer().Deserialize(yaml)!; Assert.IsInstanceOfType(restored.Members.Single()); Assert.AreEqual("Inner", ((ClassDeclaration)restored.Members[0]).Name); - Assert.AreEqual("private", ((ClassDeclaration)restored.Members[0]).AccessModifier); + Assert.AreEqual(Visibility.Private, ((ClassDeclaration)restored.Members[0]).Visibility); } } diff --git a/Coder.Test/Ast/ConstantDeclarationTests.cs b/Coder.Test/Ast/ConstantDeclarationTests.cs new file mode 100644 index 0000000..b024ee7 --- /dev/null +++ b/Coder.Test/Ast/ConstantDeclarationTests.cs @@ -0,0 +1,175 @@ +// 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 a constant: a marked +/// and initialised from a literal. +/// +/// +/// A constant is spelled two ways in each language that has one — as a local and as a class member — +/// and they are not the same spelling, so both are covered here rather than only the local one. +/// +[TestClass] +public class ConstantDeclarationTests +{ + /// + /// Builds a constant with a literal value. + /// + /// The declaration. + private static VariableDeclaration SampleConstant() => + new("MAX", "int", Literal.Number(10)) { IsConstant = true }; + + /// + /// Builds a class holding one constant member. + /// + /// The class. + private static ClassDeclaration SampleClass() + { + ClassDeclaration declaration = new("Limits"); + declaration.Members.Add(SampleConstant()); + return declaration; + } + + /// + /// Tests that a clone stays constant, since a copy that quietly became writable would be a copy + /// of something else. + /// + [TestMethod] + public void Clone_StaysConstant() + { + VariableDeclaration clone = (VariableDeclaration)SampleConstant().Clone(); + + Assert.IsTrue(clone.IsConstant); + Assert.AreEqual(10, ((LiteralExpression)clone.InitialValue!).Value); + } + + /// + /// Tests that C# writes const in front of the declaration, with the type a C# constant has + /// to name. + /// + [TestMethod] + public void CSharp_WritesConst() + { + StringAssert.Contains(new CSharpGenerator().Generate(SampleConstant()), "const int MAX = 10;", StringComparison.Ordinal); + StringAssert.Contains(new CSharpGenerator().Generate(SampleClass()), "const int MAX = 10;", StringComparison.Ordinal); + } + + /// + /// Tests that C++ writes const for a local and static constexpr for a class member, + /// which is the spelling that gives the class one compile-time value rather than one per object. + /// + [TestMethod] + public void Cpp_WritesConstexprForAMember() + { + StringAssert.Contains(new CppGenerator().Generate(SampleConstant()), "const int MAX = 10;", StringComparison.Ordinal); + StringAssert.Contains(new CppGenerator().Generate(SampleClass()), "static constexpr int MAX = 10;", StringComparison.Ordinal); + } + + /// + /// Tests that a constant member with nothing to initialise it stays a plain const, since + /// constexpr without an initialiser does not compile. + /// + [TestMethod] + public void Cpp_LeavesAnUninitialisedMemberAsConst() + { + ClassDeclaration declaration = new("Limits"); + declaration.Members.Add(new VariableDeclaration("MAX", "int") { IsConstant = true }); + + string code = new CppGenerator().Generate(declaration); + + StringAssert.Contains(code, "const int MAX;", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("constexpr", StringComparison.Ordinal)); + } + + /// + /// Tests that JavaScript writes const for a local and static for a class member, + /// since const declares a binding in a scope and a class body is not one. + /// + [TestMethod] + public void JavaScript_WritesStaticForAMember() + { + StringAssert.Contains(new JavaScriptGenerator().Generate(SampleConstant()), "const MAX = 10;", StringComparison.Ordinal); + StringAssert.Contains(new JavaScriptGenerator().Generate(SampleClass()), "static MAX = 10;", StringComparison.Ordinal); + } + + /// + /// Tests that a private constant member is spelled with both, since the two are independent. + /// + [TestMethod] + public void JavaScript_CombinesStaticWithAPrivateName() + { + ClassDeclaration declaration = new("Limits"); + declaration.Members.Add(new VariableDeclaration("MAX", "int", Literal.Number(10)) + { + IsConstant = true, + Visibility = Visibility.Private, + }); + + StringAssert.Contains(new JavaScriptGenerator().Generate(declaration), "static #MAX = 10;", StringComparison.Ordinal); + } + + /// + /// Tests that Python emits an ordinary assignment, since it has no constant declaration and the + /// upper-case naming that stands in for one is a convention about the identifier rather than + /// something the declaration can say. + /// + [TestMethod] + public void Python_EmitsAnOrdinaryAssignment() + { + string code = new PythonGenerator().Generate(SampleConstant()); + + StringAssert.Contains(code, "MAX = 10", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("const", StringComparison.Ordinal)); + } + + /// + /// Tests that a constant survives a round trip through YAML, value and all. + /// + [TestMethod] + public void Yaml_RoundTripsAConstant() + { + string yaml = new YamlSerializer().Serialize(SampleConstant()); + VariableDeclaration restored = (VariableDeclaration)new YamlDeserializer().Deserialize(yaml)!; + + Assert.IsTrue(restored.IsConstant); + Assert.AreEqual("MAX", restored.Name); + Assert.AreEqual(10, ((LiteralExpression)restored.InitialValue!).Value); + } + + /// + /// Tests that the palette offers a constant already holding a literal, so it is a node the user + /// edits rather than one they have to wire a value into first. + /// + [TestMethod] + public void Catalog_OffersAConstantHoldingALiteral() + { + AstNode created = AstNodeCatalog.Templates + .Single(template => string.Equals(template.Label, "Constant", StringComparison.Ordinal)) + .Create(); + + VariableDeclaration constant = (VariableDeclaration)created; + + Assert.IsTrue(constant.IsConstant); + Assert.IsInstanceOfType>(constant.InitialValue); + } + + /// + /// Tests that a constant with nothing in it is reported rather than generated, since a constant + /// is the one declaration whose value is not optional. + /// + [TestMethod] + public void Graph_ReportsAConstantWithNoValue() + { + VariableDeclaration constant = new("MAX", "int") { IsConstant = true }; + AstGraph graph = new(constant); + + Assert.IsTrue(graph.Validate().Any(problem => ReferenceEquals(problem.Node, constant))); + } +} diff --git a/Coder.Test/Ast/EntryPointTests.cs b/Coder.Test/Ast/EntryPointTests.cs new file mode 100644 index 0000000..9ff5e0d --- /dev/null +++ b/Coder.Test/Ast/EntryPointTests.cs @@ -0,0 +1,241 @@ +// 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 : what it holds, how each language spells the place a program +/// starts running, and that it survives a round trip through YAML. +/// +/// +/// The whole reason the entry point is a node of its own is that no two languages spell it the same +/// way, so each generator is checked for its own spelling rather than for a shared one. +/// +[TestClass] +public class EntryPointTests +{ + /// + /// Builds an entry point that takes arguments, returns an exit code and has a statement in it. + /// + /// The entry point. + private static EntryPoint SampleEntryPoint() + { + EntryPoint entryPoint = new() { AcceptsArguments = true, ReturnsExitCode = true }; + entryPoint.Body.Add(new VariableDeclaration("count", "int", Literal.Number(0))); + entryPoint.Body.Add(new ReturnStatement(Literal.Number(0))); + return entryPoint; + } + + /// + /// Tests that a clone copies the body rather than sharing it, so editing one program does not + /// edit the other. + /// + [TestMethod] + public void Clone_CopiesTheBodyRatherThanSharingIt() + { + EntryPoint original = SampleEntryPoint(); + + EntryPoint clone = (EntryPoint)original.Clone(); + + Assert.IsTrue(clone.AcceptsArguments); + Assert.IsTrue(clone.ReturnsExitCode); + Assert.AreEqual(original.Body.Count, clone.Body.Count); + Assert.AreNotSame(original.Body[0], clone.Body[0]); + + ((VariableDeclaration)clone.Body[0]).Name = "total"; + Assert.AreEqual("count", ((VariableDeclaration)original.Body[0]).Name); + } + + /// + /// Tests that the node reports the type name the serializer writes and reads it back under. + /// + [TestMethod] + public void NodeTypeName_IsEntryPoint() => Assert.AreEqual("EntryPoint", new EntryPoint().GetNodeTypeName()); + + /// + /// Tests that the editor sees an entry point as one slot holding its statements. + /// + [TestMethod] + public void Schema_ExposesTheBodyAsOneSlot() + { + EntryPoint entryPoint = SampleEntryPoint(); + AstSlot body = AstSchema.SlotsOf(entryPoint).Single(); + + Assert.AreEqual("Body", body.Name); + Assert.AreEqual(AstSlotCardinality.Many, body.Cardinality); + Assert.AreEqual(2, AstSchema.ChildrenOf(entryPoint, body).Count); + + Assert.IsTrue(AstSchema.TryAttach(entryPoint, body, new ReturnStatement())); + Assert.AreEqual(3, AstSchema.ChildrenOf(entryPoint, body).Count); + + Assert.IsTrue(AstSchema.TryDetachAt(entryPoint, body, 0)); + Assert.AreEqual(2, AstSchema.ChildrenOf(entryPoint, body).Count); + } + + /// + /// Tests that an entry point can be a class member — which is where C# puts Main — but not + /// a statement inside a body, since a program does not start running part-way through a function. + /// + [TestMethod] + public void Schema_TakesAnEntryPointAsAMemberButNotAsAStatement() + { + AstSlot members = AstSchema.SlotsOf(new ClassDeclaration("Program")).Single(); + AstSlot body = AstSchema.SlotsOf(new FunctionDeclaration("run")).Single(slot => slot.Name == "Body"); + + Assert.IsTrue(AstSchema.Accepts(members, new EntryPoint())); + Assert.IsFalse(AstSchema.Accepts(body, new EntryPoint())); + } + + /// + /// Tests that the editor has a caption for an entry point, since a node with no caption is a blank + /// title bar on screen. + /// + [TestMethod] + public void Schema_DescribesAnEntryPoint() => Assert.AreEqual("entry point", AstSchema.Describe(new EntryPoint())); + + /// + /// Tests that C# emits the static Main it looks for, with the argument array and the exit + /// code the node asked for. + /// + [TestMethod] + public void CSharp_GeneratesMain() + { + string code = new CSharpGenerator().Generate(SampleEntryPoint()); + + StringAssert.Contains(code, "public static int Main(string[] args)", StringComparison.Ordinal); + StringAssert.Contains(code, "int count = 0;", StringComparison.Ordinal); + } + + /// + /// Tests that an entry point that takes nothing and returns nothing is emitted as a bare void + /// Main, rather than one with an unused parameter. + /// + [TestMethod] + public void CSharp_GeneratesABareMainWhenNothingIsAskedFor() + { + string code = new CSharpGenerator().Generate(new EntryPoint()); + + StringAssert.Contains(code, "public static void Main()", StringComparison.Ordinal); + } + + /// + /// Tests that C++ emits the free int main the standard names, with argc and + /// argv when the program reads its arguments. + /// + [TestMethod] + public void Cpp_GeneratesMain() + { + StringAssert.Contains(new CppGenerator().Generate(SampleEntryPoint()), "int main(int argc, char* argv[])", StringComparison.Ordinal); + + // C++'s main returns int whether or not the program hands back an exit code. + StringAssert.Contains(new CppGenerator().Generate(new EntryPoint()), "int main()", StringComparison.Ordinal); + } + + /// + /// Tests that Python emits the function along with the __main__ guard that runs it, and the + /// import the arguments and exit code need. + /// + [TestMethod] + public void Python_GeneratesMainAndItsGuard() + { + string code = new PythonGenerator().Generate(SampleEntryPoint()); + + StringAssert.Contains(code, "import sys", StringComparison.Ordinal); + StringAssert.Contains(code, "def main(args):", StringComparison.Ordinal); + StringAssert.Contains(code, "if __name__ == \"__main__\":", StringComparison.Ordinal); + StringAssert.Contains(code, "sys.exit(main(sys.argv[1:]))", StringComparison.Ordinal); + } + + /// + /// Tests that an entry point needing neither arguments nor an exit code imports nothing, since an + /// unused import is something a linter will complain about. + /// + [TestMethod] + public void Python_ImportsNothingWhenSysIsNotNeeded() + { + string code = new PythonGenerator().Generate(new EntryPoint()); + + Assert.IsFalse(code.Contains("import sys", StringComparison.Ordinal)); + StringAssert.Contains(code, "def main():", StringComparison.Ordinal); + StringAssert.Contains(code, "pass", StringComparison.Ordinal); + StringAssert.Contains(code, "main()", StringComparison.Ordinal); + } + + /// + /// Tests that JavaScript emits the function along with the call that runs it, since a module that + /// only defines main does nothing when it is run. + /// + [TestMethod] + public void JavaScript_GeneratesMainAndCallsIt() + { + string code = new JavaScriptGenerator().Generate(SampleEntryPoint()); + + StringAssert.Contains(code, "function main(args) {", StringComparison.Ordinal); + StringAssert.Contains(code, "process.exit(main(process.argv.slice(2)));", StringComparison.Ordinal); + + StringAssert.Contains(new JavaScriptGenerator().Generate(new EntryPoint()), "main();", StringComparison.Ordinal); + } + + /// + /// Tests that every generator accepts an entry point, so a document rooted at one can be generated + /// rather than refused. + /// + [TestMethod] + public void EveryGenerator_AcceptsAnEntryPoint() + { + foreach (ILanguageGenerator generator in new ILanguageGenerator[] + { new CSharpGenerator(), new CppGenerator(), new PythonGenerator(), new JavaScriptGenerator() }) + { + Assert.IsTrue(generator.CanGenerate(new EntryPoint()), generator.DisplayName); + } + } + + /// + /// Tests that an entry point survives a round trip through YAML, which is how the editor saves. + /// + [TestMethod] + public void Yaml_RoundTripsAnEntryPoint() + { + EntryPoint original = SampleEntryPoint(); + + string yaml = new YamlSerializer().Serialize(original); + EntryPoint restored = (EntryPoint)new YamlDeserializer().Deserialize(yaml)!; + + Assert.IsTrue(restored.AcceptsArguments); + Assert.IsTrue(restored.ReturnsExitCode); + Assert.AreEqual(2, restored.Body.Count); + Assert.AreEqual(new CSharpGenerator().Generate(original), new CSharpGenerator().Generate(restored)); + } + + /// + /// Tests that the inspector edits the two things that vary about an entry point. + /// + [TestMethod] + public void Fields_EditArgumentsAndExitCode() + { + EntryPoint entryPoint = new(); + + Assert.IsTrue(AstFields.TryWrite(entryPoint, "Arguments", "true")); + Assert.IsTrue(AstFields.TryWrite(entryPoint, "ExitCode", "true")); + + Assert.IsTrue(entryPoint.AcceptsArguments); + Assert.IsTrue(entryPoint.ReturnsExitCode); + + // A value that is not a flag leaves the document alone. + Assert.IsFalse(AstFields.TryWrite(entryPoint, "Arguments", "maybe")); + Assert.IsTrue(entryPoint.AcceptsArguments); + } + + /// + /// Tests that the palette offers an entry point, since a program with no way to start is not one + /// a user can build from the menu. + /// + [TestMethod] + public void Catalog_OffersAnEntryPoint() => + Assert.IsTrue(AstNodeCatalog.Templates.Any(template => template.Create() is EntryPoint)); +} diff --git a/Coder.Test/Ast/VisibilityTests.cs b/Coder.Test/Ast/VisibilityTests.cs new file mode 100644 index 0000000..f06a037 --- /dev/null +++ b/Coder.Test/Ast/VisibilityTests.cs @@ -0,0 +1,286 @@ +// 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 : what each declaration carries, how each language spells it, +/// and that it survives a round trip through YAML. +/// +/// +/// Visibility is the first thing the AST models that no two languages spell the same way — a C# +/// keyword, a C++ access label, a JavaScript name prefix, and nothing at all in Python — so these +/// cover each spelling rather than asserting one shape four times. +/// +[TestClass] +public class VisibilityTests +{ + /// + /// Builds a class whose three members ask for three different visibilities. + /// + /// The class. + private static ClassDeclaration SampleClass() + { + ClassDeclaration declaration = new("Point"); + declaration.Members.Add(new VariableDeclaration("origin", "int", Literal.Number(0)) { Visibility = Visibility.Public }); + declaration.Members.Add(new VariableDeclaration("x", "int", Literal.Number(0)) { Visibility = Visibility.Private }); + + FunctionDeclaration area = new("area") { ReturnType = "int", Visibility = Visibility.Protected }; + area.Body.Add(new ReturnStatement(new VariableReference("x"))); + declaration.Members.Add(area); + + return declaration; + } + + /// + /// Tests that a declaration nobody has given a visibility to carries none, rather than silently + /// acquiring one the author never asked for. + /// + [TestMethod] + public void Unspecified_IsWhatADeclarationStartsWith() + { + Assert.AreEqual(Visibility.Unspecified, new ClassDeclaration("Point").Visibility); + Assert.AreEqual(Visibility.Unspecified, new FunctionDeclaration("area").Visibility); + Assert.AreEqual(Visibility.Unspecified, new VariableDeclaration("x", "int").Visibility); + } + + /// + /// Tests that a clone carries the visibility over, since a copied declaration that quietly became + /// public would be a copy of something else. + /// + [TestMethod] + public void Clone_CarriesTheVisibility() + { + ClassDeclaration classClone = (ClassDeclaration)new ClassDeclaration("Point") { Visibility = Visibility.Internal }.Clone(); + FunctionDeclaration functionClone = (FunctionDeclaration)new FunctionDeclaration("area") { Visibility = Visibility.Protected }.Clone(); + VariableDeclaration variableClone = (VariableDeclaration)new VariableDeclaration("x", "int") { Visibility = Visibility.Private }.Clone(); + + Assert.AreEqual(Visibility.Internal, classClone.Visibility); + Assert.AreEqual(Visibility.Protected, functionClone.Visibility); + Assert.AreEqual(Visibility.Private, variableClone.Visibility); + } + + /// + /// Tests that C# writes the keyword for each visibility, and that a member without one is public + /// rather than carrying no modifier at all. + /// + [TestMethod] + public void CSharp_WritesTheKeyword() + { + string code = new CSharpGenerator().Generate(SampleClass()); + + StringAssert.Contains(code, "public int origin = 0;", StringComparison.Ordinal); + StringAssert.Contains(code, "private int x = 0;", StringComparison.Ordinal); + StringAssert.Contains(code, "protected int area()", StringComparison.Ordinal); + StringAssert.Contains(new CSharpGenerator().Generate(new FunctionDeclaration("area") { ReturnType = "int" }), "public int area()", StringComparison.Ordinal); + } + + /// + /// Tests that a local variable — one with no visibility — is emitted without a modifier, since a + /// modifier on a local does not compile. + /// + [TestMethod] + public void CSharp_LeavesALocalUnmodified() + { + FunctionDeclaration function = new("run") { ReturnType = "void" }; + function.Body.Add(new VariableDeclaration("total", "int", Literal.Number(0))); + + StringAssert.Contains(new CSharpGenerator().Generate(function), "int total = 0;", StringComparison.Ordinal); + Assert.IsFalse(new CSharpGenerator().Generate(function).Contains("public int total", StringComparison.Ordinal)); + } + + /// + /// Tests that C++ groups the members under the access label each one asks for, which is how C++ + /// spells visibility. + /// + [TestMethod] + public void Cpp_GroupsMembersUnderAccessLabels() + { + string code = new CppGenerator().Generate(SampleClass()); + + StringAssert.Contains(code, "public:", StringComparison.Ordinal); + StringAssert.Contains(code, "private:", StringComparison.Ordinal); + StringAssert.Contains(code, "protected:", StringComparison.Ordinal); + + // The label has to come before the member it governs, not after it. + Assert.IsTrue(code.IndexOf("private:", StringComparison.Ordinal) < code.IndexOf("int x = 0;", StringComparison.Ordinal)); + Assert.IsTrue(code.IndexOf("protected:", StringComparison.Ordinal) < code.IndexOf("int area()", StringComparison.Ordinal)); + } + + /// + /// Tests that a member with no visibility of its own lands under public:, since an + /// unlabelled C++ class body is private and nothing outside the class could reach it. + /// + [TestMethod] + public void Cpp_PutsAnUnspecifiedMemberUnderPublic() + { + ClassDeclaration declaration = new("Point"); + declaration.Members.Add(new VariableDeclaration("x", "int", Literal.Number(0))); + + string code = new CppGenerator().Generate(declaration); + + Assert.IsTrue(code.IndexOf("public:", StringComparison.Ordinal) < code.IndexOf("int x = 0;", StringComparison.Ordinal)); + } + + /// + /// Tests that C++ puts an internal member under public:, since C++ has no assembly for a + /// declaration to be internal to. + /// + [TestMethod] + public void Cpp_TreatsInternalAsPublic() + { + ClassDeclaration declaration = new("Point"); + declaration.Members.Add(new VariableDeclaration("x", "int", Literal.Number(0)) { Visibility = Visibility.Internal }); + + string code = new CppGenerator().Generate(declaration); + + StringAssert.Contains(code, "public:", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("internal", StringComparison.Ordinal)); + } + + /// + /// Tests that JavaScript spells a private member with the # prefix its own private syntax + /// uses, and leaves the others as ordinary members. + /// + [TestMethod] + public void JavaScript_SpellsAPrivateMemberWithAHash() + { + string code = new JavaScriptGenerator().Generate(SampleClass()); + + StringAssert.Contains(code, "#x = 0;", StringComparison.Ordinal); + StringAssert.Contains(code, "origin = 0;", StringComparison.Ordinal); + + // protected has no JavaScript spelling, so the method is an ordinary one. + StringAssert.Contains(code, "area() {", StringComparison.Ordinal); + } + + /// + /// Tests that a private method is spelled with the prefix too, since JavaScript's # applies + /// to any class member rather than to fields alone. + /// + [TestMethod] + public void JavaScript_SpellsAPrivateMethodWithAHash() + { + ClassDeclaration declaration = new("Point"); + declaration.Members.Add(new FunctionDeclaration("recompute") { ReturnType = "void", Visibility = Visibility.Private }); + + StringAssert.Contains(new JavaScriptGenerator().Generate(declaration), "#recompute() {", StringComparison.Ordinal); + } + + /// + /// Tests that Python drops visibility rather than renaming the declaration, since its convention + /// for a non-public member is a spelling of the identifier and renaming one here would leave every + /// reference to it naming something that no longer exists. + /// + [TestMethod] + public void Python_DropsVisibilityRatherThanRenaming() + { + string code = new PythonGenerator().Generate(SampleClass()); + + StringAssert.Contains(code, "x = 0", StringComparison.Ordinal); + StringAssert.Contains(code, "def area(self) -> int:", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("_x", StringComparison.Ordinal)); + Assert.IsFalse(code.Contains("private", StringComparison.Ordinal)); + } + + /// + /// Tests that visibility survives a round trip through YAML, on each of the declarations that can + /// carry one. + /// + [TestMethod] + public void Yaml_RoundTripsVisibility() + { + ClassDeclaration original = SampleClass(); + original.Visibility = Visibility.Internal; + + string yaml = new YamlSerializer().Serialize(original); + ClassDeclaration restored = (ClassDeclaration)new YamlDeserializer().Deserialize(yaml)!; + + Assert.AreEqual(Visibility.Internal, restored.Visibility); + Assert.AreEqual(Visibility.Public, ((VariableDeclaration)restored.Members[0]).Visibility); + Assert.AreEqual(Visibility.Private, ((VariableDeclaration)restored.Members[1]).Visibility); + Assert.AreEqual(Visibility.Protected, ((FunctionDeclaration)restored.Members[2]).Visibility); + } + + /// + /// Tests that a declaration with no visibility writes no key at all, so the document says nothing + /// rather than saying "unspecified". + /// + [TestMethod] + public void Yaml_WritesNothingForAnUnspecifiedVisibility() + { + string yaml = new YamlSerializer().Serialize(new ClassDeclaration("Point")); + + Assert.IsFalse(yaml.Contains("visibility", StringComparison.Ordinal)); + } + + /// + /// Tests that a document written before visibility became an enumeration still opens, since a + /// saved document should not stop loading because the library changed how it models the same idea. + /// + [TestMethod] + public void Yaml_ReadsTheOlderAccessModifierSpelling() + { + const string yaml = """ + classDeclaration: + name: Point + accessModifier: internal + """; + + ClassDeclaration restored = (ClassDeclaration)new YamlDeserializer().Deserialize(yaml)!; + + Assert.AreEqual(Visibility.Internal, restored.Visibility); + } + + /// + /// Tests that the inspector offers visibility on every declaration that can carry one, and writes + /// the choice the user picked. + /// + [TestMethod] + public void Fields_OfferVisibilityOnEveryDeclaration() + { + ClassDeclaration classDecl = new("Point"); + FunctionDeclaration function = new("area") { ReturnType = "int" }; + VariableDeclaration variable = new("x", "int"); + + Assert.IsTrue(AstFields.TryWrite(classDecl, "Visibility", "Internal")); + Assert.IsTrue(AstFields.TryWrite(function, "Visibility", "Protected")); + Assert.IsTrue(AstFields.TryWrite(variable, "Visibility", "Private")); + + Assert.AreEqual(Visibility.Internal, classDecl.Visibility); + Assert.AreEqual(Visibility.Protected, function.Visibility); + Assert.AreEqual(Visibility.Private, variable.Visibility); + } + + /// + /// Tests that the inspector offers each visibility as a choice, so it is picked from a menu rather + /// than typed and misspelled. + /// + [TestMethod] + public void Fields_ListEveryVisibilityAsAChoice() + { + AstField field = AstFields.Of(new ClassDeclaration("Point")).Single(f => f.Name == "Visibility"); + + Assert.AreEqual(AstFieldKind.Choice, field.Kind); + Assert.AreEqual(Enum.GetValues().Length, field.Choices.Count); + Assert.IsTrue(field.Choices.Any(choice => choice.Value == nameof(Visibility.Internal))); + } + + /// + /// Tests that a value that names no visibility is refused, leaving the document alone rather than + /// writing a default over what the user had. + /// + [TestMethod] + public void Fields_RefuseAValueThatNamesNoVisibility() + { + ClassDeclaration classDecl = new("Point") { Visibility = Visibility.Internal }; + + Assert.IsFalse(AstFields.TryWrite(classDecl, "Visibility", "friendly")); + Assert.AreEqual(Visibility.Internal, classDecl.Visibility); + } +} diff --git a/Coder.Test/Graph/AstFieldsTests.cs b/Coder.Test/Graph/AstFieldsTests.cs index afb79d7..f425718 100644 --- a/Coder.Test/Graph/AstFieldsTests.cs +++ b/Coder.Test/Graph/AstFieldsTests.cs @@ -167,7 +167,7 @@ public void TryWrite_EditsDeclarations() Assert.IsTrue(AstFields.TryWrite(parameter, "Optional", "true")); Assert.IsTrue(AstFields.TryWrite(variable, "Constant", "true")); Assert.IsTrue(AstFields.TryWrite(classDecl, "BaseType", "Shape")); - Assert.IsTrue(AstFields.TryWrite(classDecl, "Access", "internal")); + Assert.IsTrue(AstFields.TryWrite(classDecl, "Visibility", "Internal")); Assert.AreEqual("total", function.Name); Assert.AreEqual("int", function.ReturnType); @@ -176,7 +176,7 @@ public void TryWrite_EditsDeclarations() Assert.IsTrue(parameter.IsOptional); Assert.IsTrue(variable.IsConstant); Assert.AreEqual("Shape", classDecl.BaseType); - Assert.AreEqual("internal", classDecl.AccessModifier); + Assert.AreEqual(Visibility.Internal, classDecl.Visibility); } /// diff --git a/Coder/Ast/ClassDeclaration.cs b/Coder/Ast/ClassDeclaration.cs index 52a1d01..def148a 100644 --- a/Coder/Ast/ClassDeclaration.cs +++ b/Coder/Ast/ClassDeclaration.cs @@ -16,7 +16,7 @@ namespace ktsu.Coder.Ast; /// its declaration syntax, and interfaces are a language feature the AST does not model yet. /// /// -public class ClassDeclaration : AstCompositeNode +public class ClassDeclaration : AstCompositeNode, IHasVisibility { /// /// Initializes a new instance of the class. @@ -42,9 +42,9 @@ public ClassDeclaration() public string? BaseType { get; set; } /// - /// Gets or sets the visibility/access modifier (public, internal, …), or null for the language's default. + /// Gets or sets how widely the class is visible. /// - public string? AccessModifier { get; set; } + public Visibility Visibility { get; set; } /// /// Gets the members the class declares, in the order they should be emitted. @@ -67,7 +67,7 @@ public override AstNode Clone() { Name = Name, BaseType = BaseType, - AccessModifier = AccessModifier + Visibility = Visibility }; foreach ((string key, object? value) in Metadata) diff --git a/Coder/Ast/EntryPoint.cs b/Coder/Ast/EntryPoint.cs new file mode 100644 index 0000000..896b129 --- /dev/null +++ b/Coder/Ast/EntryPoint.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Ast; + +using System.Collections.ObjectModel; + +/// +/// Represents the point a program starts running at: main, or whatever the target language +/// calls it. +/// +/// +/// A node of its own rather than a function named "main", because every language spells its entry +/// point differently and only some of them spell it as a function at all. C# wants a static +/// Main returning void or int, C++ wants a free int main taking +/// argc and argv, Python wants a function plus the __main__ guard that calls +/// it, and JavaScript wants a function plus the call. Naming the intent lets each generator write +/// its own spelling; a called "main" would only be right for +/// whichever language it was written for. +/// +/// and are the only two things that +/// vary about an entry point across those languages, so they are what the node carries. The name, +/// the parameter spelling and the wiring that runs it are the generator's business. +/// +/// +public class EntryPoint : AstCompositeNode +{ + /// + /// Initializes a new instance of the class. + /// + public EntryPoint() + { + } + + /// + /// Gets or sets a value indicating whether the program reads the command line arguments. + /// + public bool AcceptsArguments { get; set; } + + /// + /// Gets or sets a value indicating whether the program returns an exit code to its caller. + /// + public bool ReturnsExitCode { get; set; } + + /// + /// Gets the statements the program runs, in order. + /// + public Collection Body { get; init; } = []; + + /// + /// Gets the type name of this node for serialization purposes. + /// + /// The name of the node type. + public override string GetNodeTypeName() => "EntryPoint"; + + /// + /// Creates a deep clone of this entry point. + /// + /// A new instance with the same properties and a cloned body. + public override AstNode Clone() + { + EntryPoint clone = new() + { + AcceptsArguments = AcceptsArguments, + ReturnsExitCode = ReturnsExitCode + }; + + foreach ((string key, object? value) in Metadata) + { + clone.Metadata[key] = value; + } + + foreach (AstNode statement in Body) + { + clone.Body.Add(statement.Clone()); + } + + foreach ((string key, AstNode child) in Children) + { + clone.Children[key] = child.Clone(); + } + + return clone; + } +} diff --git a/Coder/Ast/FunctionDeclaration.cs b/Coder/Ast/FunctionDeclaration.cs index b1c9fa3..b66b916 100644 --- a/Coder/Ast/FunctionDeclaration.cs +++ b/Coder/Ast/FunctionDeclaration.cs @@ -7,7 +7,7 @@ namespace ktsu.Coder.Ast; /// /// Represents a function declaration in the abstract syntax tree. /// -public class FunctionDeclaration : AstCompositeNode +public class FunctionDeclaration : AstCompositeNode, IHasVisibility { /// /// Initializes a new instance of the class. @@ -32,6 +32,11 @@ public FunctionDeclaration() /// public string? ReturnType { get; set; } + /// + /// Gets or sets how widely the function is visible. + /// + public Visibility Visibility { get; set; } + /// /// Gets or sets a list of parameters for the function. /// @@ -57,7 +62,8 @@ public override AstNode Clone() FunctionDeclaration clone = new() { Name = Name, - ReturnType = ReturnType + ReturnType = ReturnType, + Visibility = Visibility }; // Copy metadata diff --git a/Coder/Ast/VariableDeclaration.cs b/Coder/Ast/VariableDeclaration.cs index 90e9b92..06ee23f 100644 --- a/Coder/Ast/VariableDeclaration.cs +++ b/Coder/Ast/VariableDeclaration.cs @@ -6,7 +6,7 @@ namespace ktsu.Coder.Ast; /// Represents a variable declaration statement. /// Examples: int x; string name = "hello"; var result = 42; /// -public class VariableDeclaration : AstNode +public class VariableDeclaration : AstNode, IHasVisibility { /// /// Gets or sets the name of the variable. @@ -36,9 +36,14 @@ public class VariableDeclaration : AstNode public bool IsTypeInferred { get; set; } /// - /// Gets or sets the visibility/access modifier (public, private, etc.). + /// Gets or sets how widely the declaration is visible. /// - public string? AccessModifier { get; set; } + /// + /// A local variable has no visibility of its own, so one left + /// is emitted without a modifier — which is what a local needs and what a field in a language + /// with a sensible default wants too. + /// + public Visibility Visibility { get; set; } /// /// Initializes a new instance of the class. @@ -81,7 +86,7 @@ public override AstNode Clone() InitialValue = (Expression?)InitialValue?.DeepClone(), IsConstant = IsConstant, IsTypeInferred = IsTypeInferred, - AccessModifier = AccessModifier + Visibility = Visibility }; // Copy metadata @@ -101,7 +106,7 @@ public override string ToString() { string typeInfo = IsTypeInferred ? "var" : Type ?? "?"; string valueInfo = InitialValue != null ? $" = {InitialValue}" : ""; - string modifiers = AccessModifier != null ? $"{AccessModifier} " : ""; + string modifiers = Visibility != Visibility.Unspecified ? $"{Visibility.ToString().ToLowerInvariant()} " : ""; modifiers += IsConstant ? "const " : ""; return $"{modifiers}{typeInfo} {Name}{valueInfo}"; diff --git a/Coder/Ast/Visibility.cs b/Coder/Ast/Visibility.cs new file mode 100644 index 0000000..fa2563d --- /dev/null +++ b/Coder/Ast/Visibility.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Ast; + +/// +/// How widely a declaration is visible: the language-agnostic form of public, +/// protected, internal and private. +/// +/// +/// An enumeration rather than the modifier's text, because no two target languages spell visibility +/// the same way — C# writes a keyword, C++ groups members under an access label, JavaScript has one +/// real modifier and Python has none at all. A string would only be right for whichever language it +/// was typed for. +/// +/// is the default so that a declaration nobody has given a visibility to +/// comes out the way that language would write it anyway, rather than acquiring a modifier the +/// author never asked for. +/// +/// +public enum Visibility +{ + /// The language's own default, which is what a declaration carries until it is told otherwise. + Unspecified, + + /// Visible to everything. + Public, + + /// Visible to the declaring type and the types deriving from it. + Protected, + + /// Visible within the assembly, module or package the declaration belongs to. + Internal, + + /// Visible only within the declaring type. + Private, +} + +/// +/// Implemented by the declarations that can carry a . +/// +/// +/// A generator needs a member's visibility without caring which kind of member it is — C++ has to +/// group whatever a class holds under access labels — so the property is reachable through this +/// rather than through a switch repeated in every generator. +/// +public interface IHasVisibility +{ + /// + /// Gets or sets how widely the declaration is visible. + /// + public Visibility Visibility { get; set; } +} diff --git a/Coder/Languages/CSharpGenerator.cs b/Coder/Languages/CSharpGenerator.cs index 89e8424..e4d82b6 100644 --- a/Coder/Languages/CSharpGenerator.cs +++ b/Coder/Languages/CSharpGenerator.cs @@ -45,6 +45,9 @@ protected override void GenerateInternal(AstNode node, CodeBlocker code) case FunctionDeclaration function: GenerateFunction(function, code); break; + case EntryPoint entryPoint: + GenerateEntryPoint(entryPoint, code); + break; case Parameter parameter: GenerateParameter(parameter, code); break; @@ -108,7 +111,7 @@ protected override void GenerateInternal(AstNode node, CodeBlocker code) /// private void GenerateClass(ClassDeclaration classDecl, CodeBlocker code) { - code.Write($"{classDecl.AccessModifier ?? "public"} class {classDecl.Name ?? "UnnamedClass"}"); + code.Write($"{SpellVisibility(classDecl.Visibility) ?? "public"} class {classDecl.Name ?? "UnnamedClass"}"); if (!string.IsNullOrEmpty(classDecl.BaseType)) { @@ -127,8 +130,9 @@ private void GenerateClass(ClassDeclaration classDecl, CodeBlocker code) private void GenerateFunction(FunctionDeclaration function, CodeBlocker code) { - // Build method signature - code.Write($"public {MapToCSType(function.ReturnType ?? "void")} {function.Name}("); + // 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}("); // Add parameters for (int i = 0; i < function.Parameters.Count; i++) @@ -188,9 +192,9 @@ private void GenerateVariableDeclaration(VariableDeclaration varDecl, CodeBlocke // A local has no access modifier and a field usually does, and the AST distinguishes the two // by whether one was set: emitting it only when present keeps both correct. - if (!string.IsNullOrEmpty(varDecl.AccessModifier)) + if (SpellVisibility(varDecl.Visibility) is string modifier) { - code.Write($"{varDecl.AccessModifier} "); + code.Write($"{modifier} "); } // A C# constant must name its type, so an inferred one stays a plain declaration rather than @@ -210,4 +214,33 @@ private void GenerateVariableDeclaration(VariableDeclaration varDecl, CodeBlocke code.WriteLine(";"); } + + /// + /// Emits the program's entry point as C#'s Main method. + /// + /// The entry point to emit. + /// The writer to emit into. + /// + /// Always static, and named with the capital C# gives it. The method is emitted on its own + /// rather than wrapped in a class, because a C# entry point is an ordinary member of whatever + /// class the document puts it in — including one this generator emits around it. + /// + private void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker code) + { + code.Write($"public static {(entryPoint.ReturnsExitCode ? "int" : "void")} Main("); + + if (entryPoint.AcceptsArguments) + { + code.Write("string[] args"); + } + + // The line is ended before the scope opens, so C#'s brace lands on its own line. + code.WriteLine(")"); + + using Scope body = new(code); + foreach (AstNode statement in entryPoint.Body) + { + GenerateInternal(statement, code); + } + } } diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs index 348a621..48b03dc 100644 --- a/Coder/Languages/CppGenerator.cs +++ b/Coder/Languages/CppGenerator.cs @@ -72,9 +72,14 @@ protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl /// /// - /// Every member is public. The AST carries no per-member visibility, and a C++ class defaults to - /// private, so a generated class with no access specifier would compile to something nothing - /// outside it could use. + /// Members are grouped under the access label each one asks for, and a member with no visibility + /// of its own lands under public:. A C++ class defaults to private, so a generated class + /// with no access specifier at all would compile to something nothing outside it could use. + /// + /// becomes public:: C++ has no assembly to be internal + /// to, and the nearest thing — a friend declaration — names the code it trusts rather than + /// describing a scope. + /// /// protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code) { @@ -92,8 +97,101 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod // A C++ class declaration is a statement, so its closing brace takes a semicolon. using ScopeWithTrailingSemicolon body = new(code); - code.WriteLine("public:"); - GenerateClassMembers(classDecl, code); + + // Unspecified rather than Public, so the first member always writes its label: an unlabelled + // C++ class body is private, which is the one thing the label has to rule out. + Visibility current = Visibility.Unspecified; + foreach (AstNode member in classDecl.Members) + { + Visibility access = AccessOf(member); + if (access != current) + { + code.WriteLine($"{SpellVisibility(access)}:"); + current = access; + } + + if (member is VariableDeclaration field) + { + GenerateField(field, code); + } + else + { + GenerateInternal(member, code); + } + } + } + + /// + /// Maps a member's visibility onto the access label C++ would put it under. + /// + /// The member to place. + /// The visibility whose label the member belongs beneath. + private static Visibility AccessOf(AstNode member) => VisibilityOf(member) switch + { + Visibility.Protected => Visibility.Protected, + Visibility.Private => Visibility.Private, + _ => Visibility.Public, + }; + + /// + /// Emits a variable declaration as a class member. + /// + /// The declaration to emit. + /// The writer to emit into. + /// + /// A constant member is emitted as static constexpr. A plain const member is a + /// per-instance value initialised once per object, which is not what a constant means; the + /// static constexpr spelling is the one that gives the class a single compile-time value, + /// and it is available because a constant declared here is initialised from a literal. + /// + private void GenerateField(VariableDeclaration field, CodeBlocker code) + { + if (field.IsConstant && field.InitialValue is not null) + { + code.Write("static constexpr "); + } + else if (field.IsConstant) + { + code.Write("const "); + } + + code.Write($"{GetDeclaredType(field)} {field.Name}"); + + if (field.InitialValue is not null) + { + code.Write(" = "); + GenerateInternal(field.InitialValue, code); + } + + EndStatement(code); + } + + /// + /// + /// C++'s main returns int whether or not the program means to hand back an exit + /// code, so changes nothing in the signature — a program + /// that returns nothing exits with zero, which the standard supplies by falling off the end. + /// + protected override void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker code) + { + Ensure.NotNull(entryPoint); + Ensure.NotNull(code); + + code.Write("int main("); + + if (entryPoint.AcceptsArguments) + { + code.Write("int argc, char* argv[]"); + } + + // The line is ended before the scope opens, so C++'s brace lands on its own line. + code.WriteLine(")"); + + using Scope body = new(code); + foreach (AstNode statement in entryPoint.Body) + { + GenerateInternal(statement, code); + } } /// diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index baefd2f..d494d03 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -56,6 +56,11 @@ protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl /// A function inside a class body is written as a method — name(args) { } — because /// JavaScript's function keyword is a syntax error there. That is why the members are /// emitted here rather than through . + /// + /// A private member is spelled with the # prefix, which is JavaScript's own private syntax + /// and enforced by the runtime. The other three visibilities have no spelling: JavaScript draws + /// the line at private, and a protected or internal member is an ordinary one. + /// /// protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code) { @@ -101,7 +106,15 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod /// The writer to emit into. private void GenerateField(VariableDeclaration field, CodeBlocker code) { - code.Write(field.Name); + // `static` is how a class holds one value rather than one per instance, which is what a + // constant member means. JavaScript has no `const` for a field: the keyword declares a + // binding in a scope, and a class body is not one. + if (field.IsConstant) + { + code.Write("static "); + } + + code.Write(MemberName(field.Name, field.Visibility)); if (field.InitialValue is not null) { @@ -119,7 +132,7 @@ private void GenerateField(VariableDeclaration field, CodeBlocker code) /// The writer to emit into. private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) { - code.Write($"{method.Name ?? "unnamedMethod"}("); + code.Write($"{MemberName(method.Name ?? "unnamedMethod", method.Visibility)}("); GenerateParameterList(method.Parameters, code); code.Write(") "); @@ -159,6 +172,58 @@ protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, EndStatement(code); } + /// + /// Spells a class member's name for its visibility. + /// + /// The member's name in the AST. + /// The visibility it was declared with. + /// The name as the class body should spell it. + /// + /// # is a part of the name in JavaScript rather than a modifier in front of it, so private + /// members are spelled here rather than by writing a keyword before the declaration. + /// + private static string MemberName(string name, Visibility visibility) => + visibility == Visibility.Private ? $"#{name}" : name; + + /// + /// + /// JavaScript has no entry point of its own — a module runs top to bottom — so the function is + /// emitted along with the call that runs it. The arguments and the exit code are Node's + /// (process.argv, process.exit); a browser has neither, and there is nothing more + /// portable to reach for. + /// + protected override void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker code) + { + Ensure.NotNull(entryPoint); + Ensure.NotNull(code); + + code.Write("function main("); + + if (entryPoint.AcceptsArguments) + { + code.Write("args"); + } + + // The line is left open, so the scope's brace lands on it: JavaScript braces hang. + code.Write(") "); + + using (Scope body = new(code)) + { + foreach (AstNode statement in entryPoint.Body) + { + GenerateInternal(statement, code); + } + } + + code.WriteLine(); + + // The call is what makes the file a program rather than a definition of one. + string arguments = entryPoint.AcceptsArguments ? "process.argv.slice(2)" : string.Empty; + code.WriteLine(entryPoint.ReturnsExitCode + ? $"process.exit(main({arguments}));" + : $"main({arguments});"); + } + /// /// Maps a binary operator to its JavaScript spelling. /// diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index 31b7c75..06afe9e 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -264,6 +264,7 @@ protected static bool CanGenerateStandardNodes(AstNode astNode) // No null check: a type pattern never matches null. return astNode is FunctionDeclaration or ClassDeclaration + or EntryPoint or Parameter or ReturnStatement or BinaryExpression @@ -280,6 +281,38 @@ or AstLeafNode or AstLeafNode; } + /// + /// Spells a visibility as the keyword a C-family language writes in front of a declaration. + /// + /// The visibility to spell. + /// The keyword, or null when nothing should be written for it. + /// + /// Null rather than an empty string, so a caller writes the modifier and the space after it + /// together or writes neither, instead of leaving a stray space in front of a declaration that + /// carries no visibility. A language that spells one of them differently — or does not spell them + /// at all — does not call this. + /// + protected static string? SpellVisibility(Visibility visibility) => visibility switch + { + Visibility.Public => "public", + Visibility.Protected => "protected", + Visibility.Internal => "internal", + Visibility.Private => "private", + _ => null, + }; + + /// + /// Reads the visibility a declaration was given. + /// + /// The node to read. + /// Its visibility, or for a node that cannot carry one. + /// + /// Reached through so a generator grouping a class's members by + /// visibility does not need a switch over which kind of member each one is. + /// + protected static Visibility VisibilityOf(AstNode node) => + node is IHasVisibility declaration ? declaration.Visibility : Visibility.Unspecified; + /// /// Escapes a string literal's contents for a target language using C-style backslash escapes. /// diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs index 2284857..ee45a4f 100644 --- a/Coder/Languages/PythonGenerator.cs +++ b/Coder/Languages/PythonGenerator.cs @@ -8,6 +8,14 @@ namespace ktsu.Coder.Languages; /// /// Generates Python code from AST nodes. /// +/// +/// Python has neither access modifiers nor constants, so and +/// are deliberately dropped, the same way JavaScript +/// drops the types the AST carries. The conventions Python does have for both — a leading underscore +/// for a non-public member, an upper-case name for a constant — are spellings of the identifier +/// rather than modifiers on the declaration, and renaming a declaration here would leave every +/// to it naming something that no longer exists. +/// public class PythonGenerator : StandardLanguageGenerator { /// @@ -170,6 +178,64 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) } } + /// + /// + /// The function is emitted with the __main__ guard that runs it, which is how a Python + /// file is both a script and an importable module. Arguments and the exit code go through + /// sys, so the import it needs is emitted with it — the entry point is the top of a file, + /// which is the one place a generator can put an import without knowing what else the document + /// holds. + /// + protected override void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker code) + { + Ensure.NotNull(entryPoint); + Ensure.NotNull(code); + + bool needsSys = entryPoint.AcceptsArguments || entryPoint.ReturnsExitCode; + if (needsSys) + { + code.WriteLine("import sys"); + code.WriteLine(); + code.WriteLine(); + } + + code.Write("def main("); + + if (entryPoint.AcceptsArguments) + { + code.Write("args"); + } + + code.WriteLine("):"); + + // Python's body is delimited by indentation alone, so there is no brace scope to open. + using (IndentScope body = new(code)) + { + if (entryPoint.Body.Count == 0) + { + code.WriteLine("pass"); + } + else + { + // EndStatement writes nothing for Python, so the line break is this loop's to write. + foreach (AstNode statement in entryPoint.Body) + { + GenerateInternal(statement, code); + code.WriteLine(); + } + } + } + + // PEP 8 puts two blank lines between a top-level definition and what follows it. + code.WriteLine(); + code.WriteLine(); + code.WriteLine("if __name__ == \"__main__\":"); + + using IndentScope guard = new(code); + string call = entryPoint.AcceptsArguments ? "main(sys.argv[1:])" : "main()"; + code.WriteLine(entryPoint.ReturnsExitCode ? $"sys.exit({call})" : call); + } + /// protected override void GenerateParameter(Parameter parameter, CodeBlocker code, int position) { @@ -202,6 +268,11 @@ private static string PythonTypeFromGenericType(string genericType) } /// + /// + /// A constant is emitted as an ordinary assignment: Python has no constant declaration, and the + /// upper-case naming that stands in for one is a convention about the identifier rather than + /// something the declaration can say. + /// protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, CodeBlocker code) { Ensure.NotNull(varDecl); diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs index 442c8ca..96bab06 100644 --- a/Coder/Languages/StandardLanguageGenerator.cs +++ b/Coder/Languages/StandardLanguageGenerator.cs @@ -55,6 +55,10 @@ protected sealed override void GenerateInternal(AstNode node, CodeBlocker code) GenerateFunctionDeclaration(funcDecl, code); break; + case EntryPoint entryPoint: + GenerateEntryPoint(entryPoint, code); + break; + case Parameter parameter: GenerateParameter(parameter, code, 0); break; @@ -103,6 +107,13 @@ protected sealed override void GenerateInternal(AstNode node, CodeBlocker code) /// The writer to emit into. protected abstract void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code); + /// + /// Emits the program's entry point, and whatever else the language needs in order to run it. + /// + /// The entry point to emit. + /// The writer to emit into. + protected abstract void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker code); + /// /// Emits the members of a class, one after another. /// diff --git a/Coder/Serialization/YamlDeserializer.cs b/Coder/Serialization/YamlDeserializer.cs index 044251c..41d2e51 100644 --- a/Coder/Serialization/YamlDeserializer.cs +++ b/Coder/Serialization/YamlDeserializer.cs @@ -55,6 +55,8 @@ public YamlDeserializer() "ClassDeclaration" => DeserializeClassDeclaration(nodeData), "functionDeclaration" => DeserializeFunctionDeclaration(nodeData), "FunctionDeclaration" => DeserializeFunctionDeclaration(nodeData), + "entryPoint" => DeserializeEntryPoint(nodeData), + "EntryPoint" => DeserializeEntryPoint(nodeData), "parameter" => DeserializeParameter(nodeData), "Parameter" => DeserializeParameter(nodeData), "returnStatement" => DeserializeReturnStatement(nodeData ?? new object()), @@ -122,6 +124,73 @@ private static void DeserializeFunctionBasicProperties(FunctionDeclaration funcD { funcDecl.ReturnType = returnTypeObj?.ToString(); } + + DeserializeVisibility(funcDecl, dict); + } + + /// + /// Reads a declaration's visibility, leaving it when the + /// document does not say. + /// + /// The declaration being read into. + /// The mapping the node was written as. + /// + /// accessModifier is read as well as visibility: documents written before visibility + /// became an enumeration spell it that way, and a saved document should not stop opening because + /// the library changed how it models the same idea. + /// + private static void DeserializeVisibility(IHasVisibility declaration, Dictionary dict) + { + if ((dict.TryGetValue("visibility", out object? visibilityObj) || dict.TryGetValue("accessModifier", out visibilityObj)) + && Enum.TryParse(visibilityObj?.ToString(), ignoreCase: true, out Visibility visibility)) + { + declaration.Visibility = visibility; + } + } + + private EntryPoint DeserializeEntryPoint(object? nodeData) + { + EntryPoint entryPoint = new(); + if (nodeData is not Dictionary dict) + { + return entryPoint; + } + + if (dict.TryGetValue("acceptsArguments", out object? argumentsObj) + && bool.TryParse(argumentsObj?.ToString(), out bool acceptsArguments)) + { + entryPoint.AcceptsArguments = acceptsArguments; + } + + if (dict.TryGetValue("returnsExitCode", out object? exitCodeObj) + && bool.TryParse(exitCodeObj?.ToString(), out bool returnsExitCode)) + { + entryPoint.ReturnsExitCode = returnsExitCode; + } + + if (dict.TryGetValue("body", out object? bodyObj) && bodyObj is List bodyList) + { + foreach (object statementObj in bodyList) + { + if (statementObj is not Dictionary statementDict) + { + continue; + } + + foreach ((object statementType, object statementData) in statementDict) + { + AstNode? statement = DeserializeNode(statementType.ToString() ?? string.Empty, statementData); + if (statement != null) + { + entryPoint.Body.Add(statement); + } + } + } + } + + DeserializeMetadata(entryPoint, dict); + + return entryPoint; } private static void DeserializeFunctionParameters(FunctionDeclaration funcDecl, Dictionary dict) @@ -185,10 +254,7 @@ private ClassDeclaration DeserializeClassDeclaration(object? nodeData) classDecl.BaseType = baseTypeObj?.ToString(); } - if (dict.TryGetValue("accessModifier", out object? accessObj)) - { - classDecl.AccessModifier = accessObj?.ToString(); - } + DeserializeVisibility(classDecl, dict); DeserializeClassMembers(classDecl, dict); DeserializeMetadata(classDecl, dict); @@ -237,7 +303,7 @@ private static void DeserializeMetadata(AstNode node, Dictionary private void DeserializeFunctionChildren(FunctionDeclaration funcDecl, Dictionary dict) { - HashSet knownKeys = ["name", "returnType", "parameters", "body", "metadata"]; + HashSet knownKeys = ["name", "returnType", "visibility", "accessModifier", "parameters", "body", "metadata"]; foreach ((object key, object value) in dict) { @@ -498,10 +564,7 @@ private VariableDeclaration DeserializeVariableDeclaration(object? nodeData) varDecl.IsTypeInferred = isInferred; } - if (dict.TryGetValue("accessModifier", out object? modifierObj)) - { - varDecl.AccessModifier = modifierObj.ToString(); - } + DeserializeVisibility(varDecl, dict); DeserializeMetadata(varDecl, dict); } diff --git a/Coder/Serialization/YamlSerializer.cs b/Coder/Serialization/YamlSerializer.cs index da4d6d0..a62da27 100644 --- a/Coder/Serialization/YamlSerializer.cs +++ b/Coder/Serialization/YamlSerializer.cs @@ -87,6 +87,9 @@ private static void SerializeCompositeNode(AstNode node, Dictionary 0) { nodeData["parameters"] = SerializeParameters(funcDecl.Parameters); @@ -161,6 +166,43 @@ private static void SerializeFunctionDeclaration(FunctionDeclaration funcDecl, D } } + /// + /// Writes a declaration's visibility, when it has one. + /// + /// The declaration being serialized. + /// The mapping to write into. + /// + /// Written in lower case, which is how every target language spells the modifier and how someone + /// hand-editing the YAML would expect to type it. is written + /// as nothing at all: it means the declaration carries no visibility, and a key saying so would + /// only be noise in the diff. + /// + private static void SerializeVisibility(IHasVisibility declaration, Dictionary nodeData) + { + if (declaration.Visibility != Visibility.Unspecified) + { + nodeData["visibility"] = declaration.Visibility.ToString().ToLowerInvariant(); + } + } + + private static void SerializeEntryPoint(EntryPoint entryPoint, Dictionary nodeData) + { + if (entryPoint.AcceptsArguments) + { + nodeData["acceptsArguments"] = entryPoint.AcceptsArguments; + } + + if (entryPoint.ReturnsExitCode) + { + nodeData["returnsExitCode"] = entryPoint.ReturnsExitCode; + } + + if (entryPoint.Body.Count > 0) + { + nodeData["body"] = SerializeBodyStatements(entryPoint.Body); + } + } + private static void SerializeClassDeclaration(ClassDeclaration classDecl, Dictionary nodeData) { if (classDecl.Name != null) @@ -173,10 +215,7 @@ private static void SerializeClassDeclaration(ClassDeclaration classDecl, Dictio nodeData["baseType"] = classDecl.BaseType; } - if (classDecl.AccessModifier != null) - { - nodeData["accessModifier"] = classDecl.AccessModifier; - } + SerializeVisibility(classDecl, nodeData); if (classDecl.Members.Count > 0) { @@ -338,10 +377,7 @@ private static void SerializeVariableDeclaration(VariableDeclaration varDecl, Di nodeData["isTypeInferred"] = varDecl.IsTypeInferred; } - if (varDecl.AccessModifier != null) - { - nodeData["accessModifier"] = varDecl.AccessModifier; - } + SerializeVisibility(varDecl, nodeData); } private static void SerializeAssignmentStatement(AssignmentStatement assignment, Dictionary nodeData) diff --git a/README.md b/README.md index 6069228..f5a7330 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ This makes it ideal for code generation tools, transpilers, and any application ### Supported AST Node Types - **ClassDeclaration**: A named class with an optional base type, holding methods, fields and nested classes +- **EntryPoint**: Where the program starts running, optionally reading arguments and returning an exit code - **FunctionDeclaration**: Represents function/method declarations with parameters and body - **Parameter**: Function parameters with optional default values - **ReturnStatement**: Return statements with optional expressions @@ -53,6 +54,32 @@ Increment and decrement are deliberately absent from `UnaryOperator`: Python has them, and an `AssignmentStatement` with `AssignmentOperator.AddAssign` expresses the same effect in every target language. +### Visibility + +A `ClassDeclaration`, a `FunctionDeclaration` and a `VariableDeclaration` each carry a `Visibility`: +`Public`, `Protected`, `Internal`, `Private`, or `Unspecified` — the default, meaning the +declaration is written the way the target language would write it anyway. It is an enumeration +rather than the modifier's text because no two languages spell visibility the same way: + +| Language | How it is spelled | +|---|---| +| C# | The keyword, in front of the declaration; a class or function with none is `public` | +| C++ | An access label (`public:`, `protected:`, `private:`) the members are grouped under; `Internal` becomes `public:` | +| JavaScript | A private class member takes the `#` prefix, which is JavaScript's own private syntax; nothing for the rest | +| Python | Nothing — Python has no access modifiers, and its leading-underscore convention renames the declaration rather than modifying it | + +### Constants and entry points + +A `VariableDeclaration` marked `IsConstant` with a literal `InitialValue` is a constant. C# writes +`const`, C++ writes `const` for a local and `static constexpr` for a class member, JavaScript writes +`const` for a local and `static` for a class member, and Python writes a plain assignment, having no +constant declaration to spell. + +An `EntryPoint` holds the statements a program runs. Each generator writes the spelling its language +looks for: C#'s `static Main`, C++'s free `int main`, Python's `main` with the `__main__` guard that +calls it (and the `import sys` its arguments and exit code need), and JavaScript's `main` with the +call that runs it. + ### Visual graph editor `ktsu.Coder.Graph` renders an AST as an editable node graph, built on @@ -139,10 +166,10 @@ directly. | `LanguageId` | Generator | Extension | Notes | |---|---|---|---| -| `python` | `PythonGenerator` | `py` | Type hints, `None` for void, `pass` for an empty body or class; `self` on methods | -| `csharp` | `CSharpGenerator` | `cs` | Mapped type names, `var` for inferred declarations, declared access modifiers | -| `javascript` | `JavaScriptGenerator` | `js` | Untyped; `const`/`let`; strict `===` and `!==`; method and field syntax inside a class | -| `cpp` | `CppGenerator` | `cpp` | Mapped type spellings (`str` → `std::string`); `auto` for inferred declarations; `public:` and a terminating `;` on a class | +| `python` | `PythonGenerator` | `py` | Type hints, `None` for void, `pass` for an empty body or class; `self` on methods; `main` with its `__main__` guard | +| `csharp` | `CSharpGenerator` | `cs` | Mapped type names, `var` for inferred declarations, visibility keywords, `const`, `static Main` | +| `javascript` | `JavaScriptGenerator` | `js` | Untyped; `const`/`let`; strict `===` and `!==`; method, `static` and `#private` syntax inside a class | +| `cpp` | `CppGenerator` | `cpp` | Mapped type spellings (`str` → `std::string`); `auto` for inferred declarations; access labels, `static constexpr` members and a terminating `;` on a class | ## Installation