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
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ source in six target languages. The solution uses:
own condition — and Rust, whose `const _: () = assert!(…)` needs no macro crate because a constant
nobody names still has to be evaluated for the program to build; the others write a comment,
because a file that quietly loses a guarantee looks like one that still makes it.
- `Coder/Ast/CallExpression.cs`, `ExpressionStatement.cs`, `ConditionalExpression.cs` — what a
function *body* is made of beyond an operator applied to operands. `CallExpression`'s `Callee` is
text and written verbatim, for the reason `SourceFile.Imports` and `CompileTimeAssertion.Condition`
are: a square root is `std::sqrt`, `Math.Sqrt`, `math.sqrt` and `f64::sqrt`, and there is no shared
idea underneath those to model. Its `Receiver` *is* modelled, because that is the part the
languages disagree about — `a.b(c)` in five of them and `b(&a, c)` in C, which is the same lowering
`CGenerator` already performs on the declaration, so the call site follows the declaration.
`ExpressionStatement` is where a call made for its effect stands: without it a `void` call has
nowhere to go, since the AST could say what to do with a value but not that a value is beside the
point. `ConditionalExpression` is a choice between two values rather than between two statements,
which every target can express and each spells differently: `?:` in the four C-family ones,
`go if ready else wait` in Python, and `if ready { go } else { wait }` in Rust, which has no
ternary operator at all and makes `if` an expression instead.
- `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
Expand Down
10 changes: 10 additions & 0 deletions Coder.Graph/AstFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ public static class AstFields
/// <summary>The name of the field every node holding one value exposes.</summary>
private const string ValueField = "Value";

/// <summary>The name of the field a call exposes its callee through.</summary>
private const string CalleeField = "Callee";

private static readonly IReadOnlyList<AstFieldChoice> FunctionKinds =
[
.. Enum.GetValues<FunctionKind>().Select(kind => new AstFieldChoice(kind.ToString(), kind.ToString())),
Expand Down Expand Up @@ -272,6 +275,11 @@ private static IReadOnlyList<AstField> OfExpression(AstNode node)
new("Name", AstFieldKind.Text, varRef.Name),
],

CallExpression callExpr =>
[
new(CalleeField, AstFieldKind.Text, callExpr.Callee),
],

BinaryExpression binary =>
[
new("Operator", AstFieldKind.Choice, binary.Operator.ToString(), OperatorChoices<BinaryOperator>()),
Expand Down Expand Up @@ -463,6 +471,8 @@ private static bool TryWriteExpression(AstNode node, string fieldName, string va
{
(VariableReference varRef, "Name") => value.Length > 0 && Assign(() => varRef.Name = value),

(CallExpression callExpr, CalleeField) => value.Length > 0 && Assign(() => callExpr.Callee = value),

(BinaryExpression binary, "Operator") =>
Enum.TryParse(value, out BinaryOperator binaryOp) && Assign(() => binary.Operator = binaryOp),
(UnaryExpression unary, "Operator") =>
Expand Down
6 changes: 6 additions & 0 deletions Coder.Graph/AstNodeCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
new("Declarations", "Entry point", () => new EntryPoint()),

new("Statements", "Return", () => new ReturnStatement()),
new("Statements", "Expression", () => new ExpressionStatement()),
.. Enum.GetValues<AssignmentOperator>().Select(op => new AstNodeTemplate(
"Statements",
Spell(op),
Expand All @@ -59,7 +60,7 @@
"Assignment")),

.. Enum.GetValues<BinaryOperator>().Select(op => new AstNodeTemplate(
"Expressions",

Check warning on line 63 in Coder.Graph/AstNodeCatalog.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using this literal 'Expressions' 5 times.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaCU5_wHnwuLUAdKBd2i&open=AaCU5_wHnwuLUAdKBd2i&pullRequest=57
Spell(op),
() => new BinaryExpression(AstSchema.Unfilled(), op, AstSchema.Unfilled()),
"Binary")),
Expand All @@ -69,6 +70,11 @@
() => new UnaryExpression(op, AstSchema.Unfilled()),
"Unary")),
new("Expressions", "Variable reference", () => new VariableReference("value")),
new("Expressions", "Call", () => new CallExpression("function")),
new("Expressions", "Conditional", () => new ConditionalExpression(
AstSchema.Unfilled(),
AstSchema.Unfilled(),
AstSchema.Unfilled())),

new("Literals", "Text", () => Literal.Text("text")),
new("Literals", "Number", () => Literal.Number(0)),
Expand Down
140 changes: 132 additions & 8 deletions Coder.Graph/AstSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

private static readonly AstSlot ArgumentsSlot = new(ArgumentsSlotName, AstSlotCardinality.Many, AstSlotKind.Expression);
private static readonly AstSlot EnumMembersSlot = new("Members", AstSlotCardinality.Many, AstSlotKind.EnumMember);
private static readonly AstSlot ReceiverSlot = new("Receiver", AstSlotCardinality.One, AstSlotKind.Expression);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaCU5_vcnwuLUAdKBd2d&open=AaCU5_vcnwuLUAdKBd2d&pullRequest=57
private static readonly AstSlot ConditionSlot = new("Condition", AstSlotCardinality.One, AstSlotKind.Expression);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaCU5_vcnwuLUAdKBd2e&open=AaCU5_vcnwuLUAdKBd2e&pullRequest=57
private static readonly AstSlot WhenTrueSlot = new("WhenTrue", AstSlotCardinality.One, AstSlotKind.Expression);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaCU5_vcnwuLUAdKBd2f&open=AaCU5_vcnwuLUAdKBd2f&pullRequest=57
private static readonly AstSlot WhenFalseSlot = new("WhenFalse", AstSlotCardinality.One, AstSlotKind.Expression);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaCU5_vcnwuLUAdKBd2g&open=AaCU5_vcnwuLUAdKBd2g&pullRequest=57

/// <summary>
/// Lists the slots a node exposes, in the order the editor should draw them.
Expand All @@ -55,6 +59,9 @@
FieldDeclaration => [InitialValueSlot],
MemberInitialiser => [ValueSlot],
ConstructionExpression => [ArgumentsSlot],
CallExpression => [ReceiverSlot, ArgumentsSlot],
ConditionalExpression => [ConditionSlot, WhenTrueSlot, WhenFalseSlot],
ExpressionStatement => [ExpressionSlot],
FunctionDeclaration => [ParametersSlot, BodySlot],
EntryPoint => [BodySlot],
ReturnStatement => [ExpressionSlot],
Expand Down Expand Up @@ -88,6 +95,11 @@
(MemberInitialiser initialiser, "Value") => initialiser.Value,
(AssignmentStatement assignment, "Target") => assignment.Target,
(AssignmentStatement assignment, "Value") => assignment.Value,
(CallExpression callExpr, "Receiver") => callExpr.Receiver,
(ConditionalExpression conditional, "Condition") => conditional.Condition,
(ConditionalExpression conditional, "WhenTrue") => conditional.WhenTrue,
(ConditionalExpression conditional, "WhenFalse") => conditional.WhenFalse,
(ExpressionStatement statement, "Expression") => statement.Expression,
_ => null,
};

Expand All @@ -103,6 +115,7 @@
(ClassDeclaration classDecl, "Members") => [.. classDecl.Members],
(EnumDeclaration enumDecl, "Members") => [.. enumDecl.Members],
(ConstructionExpression construction, ArgumentsSlotName) => [.. construction.Arguments],
(CallExpression callExpr, ArgumentsSlotName) => [.. callExpr.Arguments],
(FunctionDeclaration function, "Parameters") => [.. function.Parameters],
(FunctionDeclaration function, "Body") => [.. function.Body],
(EntryPoint entryPoint, "Body") => [.. entryPoint.Body],
Expand Down Expand Up @@ -142,12 +155,27 @@
return TryReplaceAt(parent, slot, index, child);
}

return TryAttachOperand(parent, slot, child)
|| TryAttachStatementOperand(parent, slot, child)
|| TryAttachSequence(parent, slot, child);
}

/// <summary>
/// Fills one of an expression's own operand slots.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="slot">The slot to fill.</param>
/// <param name="child">The node to attach.</param>
/// <returns>True if the child was attached; false if this is not one of these slots.</returns>
/// <remarks>
/// Split from the slots a statement or a declaration owns, and from the sequences, only because
/// one switch over every slot the AST has is more branches than the analyzer accepts. The three
/// are tried in turn and a slot belongs to exactly one of them.
/// </remarks>
private static bool TryAttachOperand(AstNode parent, AstSlot slot, AstNode child)
{
switch (parent, slot.Name)
{
case (ReturnStatement returnStmt, "Expression"):
returnStmt.SetExpression(child);
return true;

case (BinaryExpression binary, "Left") when child is Expression leftExpr:
binary.Left = leftExpr;
return true;
Expand All @@ -160,6 +188,47 @@
unary.Operand = operandExpr;
return true;

case (CallExpression callExpr, "Receiver") when child is Expression receiverExpr:
callExpr.Receiver = receiverExpr;
return true;

case (ConditionalExpression conditional, "Condition") when child is Expression conditionExpr:
conditional.Condition = conditionExpr;
return true;

case (ConditionalExpression conditional, "WhenTrue") when child is Expression whenTrueExpr:
conditional.WhenTrue = whenTrueExpr;
return true;

case (ConditionalExpression conditional, "WhenFalse") when child is Expression whenFalseExpr:
conditional.WhenFalse = whenFalseExpr;
return true;

default:
return false;
}
}

/// <summary>
/// Fills the single-valued slot a statement or a declaration holds an expression in.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="slot">The slot to fill.</param>
/// <param name="child">The node to attach.</param>
/// <returns>True if the child was attached; false if this is not one of these slots.</returns>
/// <inheritdoc cref="TryAttachOperand" path="/remarks"/>
private static bool TryAttachStatementOperand(AstNode parent, AstSlot slot, AstNode child)
{
switch (parent, slot.Name)
{
case (ReturnStatement returnStmt, "Expression"):
returnStmt.SetExpression(child);
return true;

case (ExpressionStatement statement, "Expression") when child is Expression statementExpr:
statement.Expression = statementExpr;
return true;

case (VariableDeclaration varDecl, "InitialValue") when child is Expression initialExpr:
varDecl.InitialValue = initialExpr;
return true;
Expand All @@ -172,10 +241,6 @@
initialiser.Value = initialiserExpr;
return true;

case (ConstructionExpression construction, ArgumentsSlotName):
construction.Arguments.Add(child);
return true;

case (AssignmentStatement assignment, "Target") when child is Expression targetExpr:
assignment.Target = targetExpr;
return true;
Expand All @@ -184,6 +249,31 @@
assignment.Value = valueExpr;
return true;

default:
return false;
}
}

/// <summary>
/// Appends a child to one of the slots that hold several.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="slot">The slot to append to.</param>
/// <param name="child">The node to attach.</param>
/// <returns>True if the child was attached; false if this is not one of these slots.</returns>
/// <inheritdoc cref="TryAttachOperand" path="/remarks"/>
private static bool TryAttachSequence(AstNode parent, AstSlot slot, AstNode child)
{
switch (parent, slot.Name)
{
case (ConstructionExpression construction, ArgumentsSlotName):
construction.Arguments.Add(child);
return true;

case (CallExpression callExpr, ArgumentsSlotName):
callExpr.Arguments.Add(child);
return true;

case (FunctionDeclaration function, "Parameters") when child is Parameter parameter:
function.Parameters.Add(parameter);
return true;
Expand Down Expand Up @@ -261,6 +351,10 @@
construction.Arguments[index] = child;
return true;

case (CallExpression callExpr, ArgumentsSlotName):
callExpr.Arguments[index] = child;
return true;

default:
return false;
}
Expand Down Expand Up @@ -367,6 +461,33 @@
construction.Arguments.RemoveAt(index);
return true;

case (CallExpression callExpr, ArgumentsSlotName) when index < callExpr.Arguments.Count:
callExpr.Arguments.RemoveAt(index);
return true;

// A receiver is genuinely optional -- a call with none is a free function rather than an
// unfinished member call -- so detaching one clears it instead of leaving a placeholder.
case (CallExpression callExpr, "Receiver"):
bool hadReceiver = callExpr.Receiver is not null;
callExpr.Receiver = null;
return hadReceiver;

case (ConditionalExpression conditional, "Condition"):
conditional.Condition = Unfilled();
return true;

case (ConditionalExpression conditional, "WhenTrue"):
conditional.WhenTrue = Unfilled();
return true;

case (ConditionalExpression conditional, "WhenFalse"):
conditional.WhenFalse = Unfilled();
return true;

case (ExpressionStatement statement, "Expression"):
statement.Expression = Unfilled();
return true;

default:
return false;
}
Expand Down Expand Up @@ -468,11 +589,14 @@

return node switch
{
ClassDeclaration classDecl => $"class {classDecl.Name ?? "<unnamed>"}",

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using this literal '<unnamed>' 4 times.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaCU5_vcnwuLUAdKBd2h&open=AaCU5_vcnwuLUAdKBd2h&pullRequest=57
FunctionDeclaration function => $"function {function.Name ?? "<unnamed>"}",
EntryPoint => "entry point",
Parameter parameter => $"param {parameter.Name ?? "<unnamed>"}",
ReturnStatement => "return",
CallExpression callExpr => $"call {(callExpr.Callee.Length == 0 ? "<unnamed>" : callExpr.Callee)}",
ConditionalExpression => "conditional",
ExpressionStatement => "expression",
BinaryExpression binary => $"binary {SpellOrName(binary.Operator)}",
UnaryExpression unary => $"unary {SpellOrName(unary.Operator)}",
AssignmentStatement assignment => $"assign {SpellOrName(assignment.Operator)}",
Expand Down
Loading
Loading