diff --git a/Coder.Graph/AstSchema.cs b/Coder.Graph/AstSchema.cs index 887fd30..8fd5f7d 100644 --- a/Coder.Graph/AstSchema.cs +++ b/Coder.Graph/AstSchema.cs @@ -63,6 +63,16 @@ public static class AstSchema private const string Unnamed = ""; private static readonly AstSlot ArgumentsSlot = new(ArgumentsSlotName, AstSlotCardinality.Many, AstSlotKind.Expression); + + /// + /// A construction's arguments, which take a member initialiser where a call's do not. + /// + /// + /// The same name as , because the editor labels the pin the same way + /// and the attach cases switch on the name; a different kind, because what may stand in it + /// differs. + /// + private static readonly AstSlot ElementsSlot = new(ArgumentsSlotName, AstSlotCardinality.Many, AstSlotKind.Element); private static readonly AstSlot EnumMembersSlot = new("Members", AstSlotCardinality.Many, AstSlotKind.EnumMember); private static readonly AstSlot ReceiverSlot = new(ReceiverSlotName, AstSlotCardinality.One, AstSlotKind.Expression); private static readonly AstSlot ConditionSlot = new(ConditionSlotName, AstSlotCardinality.One, AstSlotKind.Expression); @@ -83,7 +93,7 @@ public static class AstSchema FieldDeclaration => [InitialValueSlot], PropertyDeclaration => [GetterSlot, SetterSlot], MemberInitialiser => [ValueSlot], - ConstructionExpression => [ArgumentsSlot], + ConstructionExpression => [ElementsSlot], CallExpression => [ReceiverSlot, ArgumentsSlot], ConditionalExpression => [ConditionSlot, WhenTrueSlot, WhenFalseSlot], ExpressionStatement => [ExpressionSlot], @@ -625,6 +635,9 @@ public static bool Accepts(AstSlot slot, AstNode candidate) { AstSlotKind.Parameter => candidate is Parameter, AstSlotKind.Expression => IsExpression(candidate), + // A member initialiser is an element and not an expression: it names the member a value + // is for rather than evaluating to one. + AstSlotKind.Element => IsExpression(candidate) || candidate is MemberInitialiser, // 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), diff --git a/Coder.Graph/AstSlot.cs b/Coder.Graph/AstSlot.cs index b796ab1..843a51d 100644 --- a/Coder.Graph/AstSlot.cs +++ b/Coder.Graph/AstSlot.cs @@ -36,6 +36,19 @@ public enum AstSlotKind /// Anything that evaluates to a value, including the legacy leaf nodes. Expression, + /// + /// One element of a braced list: an expression, or a + /// naming the member it is for. + /// + /// + /// Separate from because a member initialiser does not evaluate to a + /// value — it says which member a value is for — and separate from a call's arguments because + /// only a construction has a spelling for one. Every generator reads a construction's arguments + /// for a member initialiser and none reads a call's, so the kinds part exactly where the + /// generators do. + /// + Element, + /// Anything that can stand as a statement in a body. Statement, diff --git a/Coder.Test/Graph/AstGraphTests.cs b/Coder.Test/Graph/AstGraphTests.cs index 4e8834c..de0309c 100644 --- a/Coder.Test/Graph/AstGraphTests.cs +++ b/Coder.Test/Graph/AstGraphTests.cs @@ -379,6 +379,32 @@ public void MoveTo_LeavesTheNodeInTheGraphWhenTheSlotRefusesIt() Assert.IsNotNull(graph.Nodes.Values.SingleOrDefault(n => ReferenceEquals(n, returnStmt))); } + /// + /// Tests that a designated initialiser can be wired into a construction from the editor. + /// + /// + /// The whole chain, not just the schema's answer: the editor drags a link, the graph validates + /// it against the slot, and the AST ends up holding the node. Every generator writes one in this + /// position, and until the slot kind existed this was the one shape the graph could hold, the + /// serializer could persist and the editor could not build. + /// + [TestMethod] + public void Connect_PutsAMemberInitialiserIntoAConstruction() + { + ConstructionExpression construction = new(new TypeReference("Point")); + AstGraph graph = new(construction); + + MemberInitialiser initialiser = new("x") { Value = new LiteralExpression(1) }; + graph.AddDetached(initialiser, Vector2.Zero); + + AstConnectResult result = Connect(graph, initialiser, construction, "Arguments", 0); + + Assert.IsTrue(result.Success, result.Message); + Assert.HasCount(1, construction.Arguments); + Assert.AreSame(initialiser, construction.Arguments[0]); + Assert.IsEmpty(graph.Detached); + } + /// /// Connects a node to a named slot of a parent, looking the pins up the way the editor does. /// diff --git a/Coder.Test/Graph/DeclarationSlotsTests.cs b/Coder.Test/Graph/DeclarationSlotsTests.cs index 859bfc4..a254538 100644 --- a/Coder.Test/Graph/DeclarationSlotsTests.cs +++ b/Coder.Test/Graph/DeclarationSlotsTests.cs @@ -135,6 +135,56 @@ public void MemberInitialiserValue_IsFilledAndEmptied() Assert.IsNull(initialiser.Value); } + /// + /// A construction takes a member initialiser among its arguments, which is what a designated + /// initialiser is. + /// + /// + /// The AST, the serializer and all seven generators handle one in this position — it is a + /// designated initialiser in C++, an object initialiser in C#, a keyword argument in Python and + /// an object literal in JavaScript. The graph is the uniform view of the AST, so a shape every + /// other projection can express has to be one it can express too. + /// + [TestMethod] + public void ConstructionArguments_TakeAMemberInitialiser() + { + ConstructionExpression construction = new(new TypeReference("holo::Kilograms")); + AstSlot arguments = AstSchema.SlotsOf(construction)[0]; + + Assert.IsTrue(AstSchema.Accepts(arguments, new MemberInitialiser("value_"))); + Assert.IsTrue(AstSchema.TryAttach( + construction, + arguments, + new MemberInitialiser("value_") { Value = new LiteralExpression(1.0) })); + + Assert.HasCount(1, AstSchema.ChildrenOf(construction, arguments)); + Assert.IsInstanceOfType(construction.Arguments[0]); + + // And it comes out again, so one loaded from a document can be rewired rather than stranded. + Assert.IsTrue(AstSchema.TryDetachAt(construction, arguments, 0)); + Assert.IsEmpty(construction.Arguments); + } + + /// + /// A call does not take one, because no generator has a spelling for it there. + /// + /// + /// The two share a slot name and a collection type, and the difference is the whole reason the + /// kinds are separate: every generator reads construction.Arguments for a member + /// initialiser and none reads a call's, so one attached to a call would be written as whatever + /// fell out rather than as a named argument. + /// + [TestMethod] + public void CallArguments_DoNotTakeAMemberInitialiser() + { + CallExpression call = new("std::sqrt"); + AstSlot arguments = AstSchema.SlotsOf(call).Single(slot => slot.Name == "Arguments"); + + Assert.IsFalse(AstSchema.Accepts(arguments, new MemberInitialiser("value_"))); + Assert.IsFalse(AstSchema.TryAttach(call, arguments, new MemberInitialiser("value_"))); + Assert.IsEmpty(call.Arguments); + } + /// /// A construction's arguments are a sequence, so they are added, swapped and removed in order. ///