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;
}