diff --git a/Coder.Editor/CoderEditorApp.cs b/Coder.Editor/CoderEditorApp.cs index 7550844..e8c49cd 100644 --- a/Coder.Editor/CoderEditorApp.cs +++ b/Coder.Editor/CoderEditorApp.cs @@ -130,17 +130,35 @@ private void RememberWindow() /// /// Builds the document a fresh editor opens with. /// - /// A function with one parameter and an empty body. + /// A small class with two fields and two methods that do something with them. /// - /// An empty function rather than an empty graph: the AST has no node that means "nothing yet", - /// and a user who has just opened the editor is better served by something to attach to than by - /// a blank canvas and no way to start. + /// Something to read rather than something to start from: the AST has no node meaning "nothing + /// yet", and a canvas holding one empty function shows neither what the node kinds are nor how + /// they connect. This one puts a field, a parameter, an assignment, a binary expression, a local + /// and a return on screen at once, so the shape of the graph is legible before anything is added. /// - public static FunctionDeclaration NewDocument() + public static ClassDeclaration NewDocument() { - FunctionDeclaration function = new("newFunction") { ReturnType = "void" }; - function.Parameters.Add(new Parameter("value", "int")); - return function; + ClassDeclaration declaration = new("Counter"); + + declaration.Members.Add(new VariableDeclaration("count", "int", new LiteralExpression(0))); + declaration.Members.Add(new VariableDeclaration("step", "int", new LiteralExpression(1))); + + FunctionDeclaration add = new("Add") { ReturnType = "int" }; + add.Parameters.Add(new Parameter("amount", "int")); + add.Body.Add(new AssignmentStatement( + new VariableReference("count"), + new BinaryExpression(new VariableReference("count"), BinaryOperator.Add, new VariableReference("amount")))); + add.Body.Add(new ReturnStatement(new VariableReference("count"))); + declaration.Members.Add(add); + + FunctionDeclaration next = new("Next") { ReturnType = "int" }; + next.Body.Add(new VariableDeclaration("result", "int", + new BinaryExpression(new VariableReference("count"), BinaryOperator.Add, new VariableReference("step")))); + next.Body.Add(new ReturnStatement(new VariableReference("result"))); + declaration.Members.Add(next); + + return declaration; } /// diff --git a/Coder.Test/Editor/CoderEditorAppTests.cs b/Coder.Test/Editor/CoderEditorAppTests.cs index f200ab4..76dffcc 100644 --- a/Coder.Test/Editor/CoderEditorAppTests.cs +++ b/Coder.Test/Editor/CoderEditorAppTests.cs @@ -65,15 +65,56 @@ private static CoderEditorApp NewApp(DocumentStore store, EditorSettings? settin settings ?? new EditorSettings()); /// - /// Tests that a fresh editor opens with something to attach to rather than a blank canvas. + /// Tests that a document containing an assignment survives being written and read back. + /// + /// + /// AssignmentStatement's deserialization constructor built its placeholder target through the + /// VariableReference overload that rejects an empty name, so every load of a document holding one + /// threw before it could be overwritten. The default document has an assignment in it, so this is + /// the path a user takes by saving and reopening what the editor gave them. + /// + [TestMethod] + public void Document_WithAnAssignment_RoundTripsThroughTheFileSystem() + { + DocumentStore store = NewStore(); + CoderEditorApp app = NewApp(store); + + string path = PathIn("assigning"); + Assert.IsTrue(app.Save(path), app.Status); + + CoderEditorApp reopened = NewApp(store); + Assert.IsTrue(reopened.Open(path), reopened.Status); + + ClassDeclaration reopenedRoot = (ClassDeclaration)reopened.Editor.Graph.Root; + Assert.IsTrue( + reopenedRoot.Members.OfType().SelectMany(m => m.Body).OfType().Any(), + "the reopened document should still hold its assignment"); + } + + /// + /// Tests that a fresh editor opens with something to read rather than a blank canvas. /// [TestMethod] - public void NewDocument_IsAFunctionWithSomethingToAttachTo() + public void NewDocument_IsAClassWithFieldsAndMethodsThatDoSomething() { - FunctionDeclaration document = CoderEditorApp.NewDocument(); + ClassDeclaration document = CoderEditorApp.NewDocument(); + + Assert.AreEqual("Counter", document.Name); + + List fields = [.. document.Members.OfType()]; + Assert.AreEqual(2, fields.Count, "the class should carry a couple of fields"); + Assert.IsTrue(fields.TrueForAll(f => f.InitialValue is not null), "each field should be initialised"); - Assert.AreEqual("newFunction", document.Name); - Assert.AreEqual(1, document.Parameters.Count); + List methods = [.. document.Members.OfType()]; + Assert.AreEqual(2, methods.Count, "the class should carry a couple of methods"); + Assert.IsTrue(methods.TrueForAll(m => m.Body.Count > 0), "each method should have a body"); + + Assert.IsTrue( + methods.SelectMany(m => m.Body).OfType().Any(a => a.Value is BinaryExpression), + "a method should assign the result of an expression"); + Assert.IsTrue( + methods.SelectMany(m => m.Body).OfType().Any(v => v.InitialValue is BinaryExpression), + "a method should declare a local from an expression"); } /// @@ -92,8 +133,8 @@ public void Document_RoundTripsThroughTheFileSystem() CoderEditorApp reopened = NewApp(store); Assert.IsTrue(reopened.Open(path), reopened.Status); - Assert.IsInstanceOfType(reopened.Editor.Graph.Root); - Assert.AreEqual("newFunction", ((FunctionDeclaration)reopened.Editor.Graph.Root).Name); + Assert.IsInstanceOfType(reopened.Editor.Graph.Root); + Assert.AreEqual("Counter", ((ClassDeclaration)reopened.Editor.Graph.Root).Name); Assert.AreEqual(path, reopened.DocumentPath); } @@ -259,11 +300,11 @@ public void Preview_GeneratesInTheSelectedLanguage() CoderEditorApp app = NewApp(store, settings); app.Regenerate(); - StringAssert.Contains(app.GeneratedCode, "public void newFunction", StringComparison.Ordinal); + StringAssert.Contains(app.GeneratedCode, "public class Counter", StringComparison.Ordinal); settings.PreviewLanguageId = "python"; app.Regenerate(); - StringAssert.Contains(app.GeneratedCode, "def newFunction", StringComparison.Ordinal); + StringAssert.Contains(app.GeneratedCode, "def Add(self, amount: int)", StringComparison.Ordinal); } /// @@ -367,7 +408,7 @@ public void Export_WritesTheGeneratedCodeBesideTheDocument() Assert.IsNotNull(written); Assert.AreEqual(Path.Combine(root, "greeting.cs"), written); - StringAssert.Contains(File.ReadAllText(written), "public void newFunction(int value)", StringComparison.Ordinal); + StringAssert.Contains(File.ReadAllText(written), "public int Add(int amount)", StringComparison.Ordinal); StringAssert.Contains(app.Status, "Wrote", StringComparison.Ordinal); } @@ -521,7 +562,7 @@ public void CodePane_ListsProblemsInsteadOfGeneratingWhileIncomplete() CoderEditorApp app = NewApp(store); // An operand nobody has filled in yet, which is what Validate reports. - FunctionDeclaration document = CoderEditorApp.NewDocument(); + FunctionDeclaration document = new("incomplete") { ReturnType = "int" }; document.Body.Add(new ReturnStatement( new BinaryExpression(AstSchema.Unfilled(), BinaryOperator.Add, AstSchema.Unfilled()))); Assert.IsTrue(app.Open(WriteDocument(store, document, PathIn("incomplete"))), app.Status); diff --git a/Coder/Ast/AssignmentStatement.cs b/Coder/Ast/AssignmentStatement.cs index 8b8b2de..8c963b3 100644 --- a/Coder/Ast/AssignmentStatement.cs +++ b/Coder/Ast/AssignmentStatement.cs @@ -44,7 +44,9 @@ public AssignmentStatement(Expression target, Expression value, AssignmentOperat /// public AssignmentStatement() { - Target = new VariableReference(""); + // VariableReference(string) rejects an empty name, so the placeholder a deserializer overwrites + // has to come from the parameterless constructor instead. + Target = new VariableReference(); Value = new LiteralExpression(""); Operator = AssignmentOperator.Assign; }