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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion Coder.Graph/AstSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
/// </remarks>
public static class AstSchema
{
private static readonly AstSlot ExpressionSlot = new("Expression", AstSlotCardinality.One, AstSlotKind.Expression);

Check warning on line 28 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Expression' 8 times.

Check warning on line 28 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Expression' 8 times.
private static readonly AstSlot LeftSlot = new("Left", AstSlotCardinality.One, AstSlotKind.Expression);
private static readonly AstSlot RightSlot = new("Right", AstSlotCardinality.One, AstSlotKind.Expression);

Check warning on line 30 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Right' 4 times.

Check warning on line 30 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Right' 4 times.
private static readonly AstSlot OperandSlot = new("Operand", AstSlotCardinality.One, AstSlotKind.Expression);

Check warning on line 31 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Operand' 4 times.

Check warning on line 31 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Operand' 4 times.
private static readonly AstSlot InitialValueSlot = new("InitialValue", AstSlotCardinality.One, AstSlotKind.Expression);

Check warning on line 32 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'InitialValue' 7 times.

Check warning on line 32 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'InitialValue' 7 times.
private static readonly AstSlot TargetSlot = new("Target", AstSlotCardinality.One, AstSlotKind.Expression);

Check warning on line 33 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Target' 4 times.

Check warning on line 33 in Coder.Graph/AstSchema.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Target' 4 times.
private static readonly AstSlot ValueSlot = new("Value", AstSlotCardinality.One, AstSlotKind.Expression);
private static readonly AstSlot ParametersSlot = new("Parameters", AstSlotCardinality.Many, AstSlotKind.Parameter);
private static readonly AstSlot BodySlot = new("Body", AstSlotCardinality.Many, AstSlotKind.Statement);
Expand Down Expand Up @@ -63,6 +63,16 @@
private const string Unnamed = "<unnamed>";

private static readonly AstSlot ArgumentsSlot = new(ArgumentsSlotName, AstSlotCardinality.Many, AstSlotKind.Expression);

/// <summary>
/// A construction's arguments, which take a member initialiser where a call's do not.
/// </summary>
/// <remarks>
/// The same name as <see cref="ArgumentsSlot"/>, 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.
/// </remarks>
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);
Expand All @@ -83,7 +93,7 @@
FieldDeclaration => [InitialValueSlot],
PropertyDeclaration => [GetterSlot, SetterSlot],
MemberInitialiser => [ValueSlot],
ConstructionExpression => [ArgumentsSlot],
ConstructionExpression => [ElementsSlot],
CallExpression => [ReceiverSlot, ArgumentsSlot],
ConditionalExpression => [ConditionSlot, WhenTrueSlot, WhenFalseSlot],
ExpressionStatement => [ExpressionSlot],
Expand Down Expand Up @@ -625,6 +635,9 @@
{
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),
Expand Down
13 changes: 13 additions & 0 deletions Coder.Graph/AstSlot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ public enum AstSlotKind
/// <summary>Anything that evaluates to a value, including the legacy leaf nodes.</summary>
Expression,

/// <summary>
/// One element of a braced list: an expression, or a
/// <see cref="ktsu.Coder.Ast.MemberInitialiser"/> naming the member it is for.
/// </summary>
/// <remarks>
/// Separate from <see cref="Expression"/> 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.
/// </remarks>
Element,

/// <summary>Anything that can stand as a statement in a body.</summary>
Statement,

Expand Down
26 changes: 26 additions & 0 deletions Coder.Test/Graph/AstGraphTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,32 @@ public void MoveTo_LeavesTheNodeInTheGraphWhenTheSlotRefusesIt()
Assert.IsNotNull(graph.Nodes.Values.SingleOrDefault(n => ReferenceEquals(n, returnStmt)));
}

/// <summary>
/// Tests that a designated initialiser can be wired into a construction from the editor.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[TestMethod]
public void Connect_PutsAMemberInitialiserIntoAConstruction()
{
ConstructionExpression construction = new(new TypeReference("Point"));
AstGraph graph = new(construction);

MemberInitialiser initialiser = new("x") { Value = new LiteralExpression<int>(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);
}

/// <summary>
/// Connects a node to a named slot of a parent, looking the pins up the way the editor does.
/// </summary>
Expand Down
50 changes: 50 additions & 0 deletions Coder.Test/Graph/DeclarationSlotsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,56 @@ public void MemberInitialiserValue_IsFilledAndEmptied()
Assert.IsNull(initialiser.Value);
}

/// <summary>
/// A construction takes a member initialiser among its arguments, which is what a designated
/// initialiser is.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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<double>(1.0) }));

Assert.HasCount(1, AstSchema.ChildrenOf(construction, arguments));
Assert.IsInstanceOfType<MemberInitialiser>(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);
}

/// <summary>
/// A call does not take one, because no generator has a spelling for it there.
/// </summary>
/// <remarks>
/// The two share a slot name and a collection type, and the difference is the whole reason the
/// kinds are separate: every generator reads <c>construction.Arguments</c> 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.
/// </remarks>
[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);
}

/// <summary>
/// A construction's arguments are a sequence, so they are added, swapped and removed in order.
/// </summary>
Expand Down