Skip to content

Add CallExpression, ExpressionStatement and ConditionalExpression - #57

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/bold-planck-mxarux
Sep 12, 2026
Merged

Add CallExpression, ExpressionStatement and ConditionalExpression#57
matt-edmondson merged 2 commits into
mainfrom
claude/bold-planck-mxarux

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Closes #55.

The gap

The AST could describe a declaration in a language-agnostic way and a body only if that body was an operator applied to operands. There was no way to say a call, and no statement node for an expression evaluated for its effect, so a void call — assert(...), list.clear() — could not be said at all.

Anything beyond a return of a literal, a variable, an operator application or a constructor call had to be handed to VariableReference as target-language text. That text passes through every generator unchanged, so it is right in at most one language — which makes a tree built for one language untranslatable to another, and that is the premise of the library.

What this adds

CallExpression — a callee, an optional receiver, and an argument list. The receiver is modelled separately rather than folded into the callee because the receiver is the part the languages disagree about:

Target CallExpression(receiver: point, callee: "translate", args: [dx, dy])
C#, C++, Python, JavaScript, Rust point.translate(dx, dy)
C translate(&point, dx, dy)

C has no member functions and lowers one to a free function taking the instance, which is the transformation CGenerator already performs on the declaration (Point_translate(Point* self, …)). Modelling the receiver is what lets the call site follow the declaration it was emitted for.

ExpressionStatement — an expression evaluated for its effect. Every generator ends it the way it ends a return or an assignment, so Python's ends at the newline and the other five at a semicolon without either being a special case.

ConditionalExpression — a choice between two values, and three spellings across the six targets:

Target Spelling
C#, C++, C, JavaScript (ready ? go : wait)
Python (go if ready else wait)
Rust (if ready { go } else { wait })

Rust is the strongest argument for this being a node. Python only reorders the operands, so hand-written ?: there is merely unidiomatic — in Rust it does not parse at all, because Rust has no ternary operator and makes if an expression instead.

The callee-naming question, settled

#55 flagged this as needing a decision before the node landed: sqrt is std::sqrt, Math.Sqrt, math.sqrt and f64::sqrt across the targets.

Callee is text and is written verbatim. There is no shared idea underneath those spellings for the AST to hold — the way there is underneath a type, which is why TypeReference is structure. Choosing the name stays the caller's, exactly as SourceFile.Imports and CompileTimeAssertion.Condition already are. What the AST carries is the shape of the call, and the shape is what the generators need in order to disagree about it.

The reasoning is written down on the node itself rather than only here.

One documented limitation

C takes the receiver's address, because the self parameter it generates is a pointer. That assumes the receiver is an instance rather than already a pointer to one — an assumption, not a deduction, since a CallExpression knows the receiver's spelling and not its type.

It is the assumption worth making: taking the address is the only one of the two a caller cannot write for itself (the AST has no address-of operator), and what this generator emits elsewhere is instances. A caller holding a pointer spells the call as a free function and passes the pointer as an ordinary argument. This is stated in CGenerator's remarks rather than left to be discovered.

Coverage

Each node is wired into AstSchema (slots, children, attach, replace, detach, describe), AstFields (a call's callee is editable from the inspector — deliberately not repeating #48), AstNodeCatalog, the YAML serializer and deserializer, and all six generators.

TryAttachAt is split into three along the way — operand slots, statement/declaration slots, sequences — because one switch over every slot the AST has exceeds what CA1502 accepts. That is the same split the file already makes elsewhere for the same reason.

50 new tests. Structure, null rejection, deep clone, generation in all six languages, YAML round-trip, and the graph-side slot/field/palette wiring.

Both risky claims are checked by compiling rather than by pinning a spelling — whether generated code is well formed is a question about types, which is not visible in the text.

C, proving the receiver lowering reaches the member function the same generator emitted:

int Counter_value(const Counter* self) { return 0; }

int Counter_span(void)
{
    Counter here = { .count = 0 };
    Counter_value(&here);
    return Counter_value(&here);
}

under -std=c11 -Wall -Wextra -pedantic. And Rust, proving the void call and the if-expression:

pub fn pick(&mut self) -> i32 {
    self.shift(1);
    return (if (self.x > self.y) { self.x } else { self.y });
}

under rustc --edition 2021.

Merged with main

Main gained a sixth target language (Rust, #56) while this branch was open, which was more than a textual conflict. RustGenerator derives from StandardLanguageGenerator, so it inherited the three new dispatch cases — two of the three defaults were already correct for it, and the third was not:

  • A member call and a statement terminator needed no override; the tests now assert that rather than assume it.
  • A conditional did. The inherited ?: would not have compiled. RustGenerator now writes an if expression, parenthesised because an if at the start of a statement parses as a statement, which would silently discard the branches' values.

The C call test is ported onto ToolchainHarness, which main extracted in the same window. Language counts in the prose are corrected throughout.

Deferred

Throw expressions are left out, as #55 suggested: C++ and C diverge sharply, and there is no consumer for it yet.

Verification

  • dotnet build and dotnet build -c Release — clean, 0 warnings, both target frameworks
  • Full suite on the merged tree: 630 passed, 0 failed
  • Both compile tests ran against real cc and rustc rather than reporting inconclusive
  • No suppressions added

🤖 Generated with Claude Code

https://claude.ai/code/session_01QGCMUrT3jBgmANHNHcPmBf

…sion

The AST could describe a declaration in a language-agnostic way and a body
only if that body was an operator applied to operands. Anything else — a
call, a void call, a choice between two values — had to be handed to a
VariableReference as target-language text, which passes through every
generator unchanged and is therefore right in at most one language. That
made a tree built for one language untranslatable to another, which is the
premise of the library.

CallExpression models the receiver separately from the callee, because the
receiver is the part the languages disagree about: a.b(c) in C#, C++, Python
and JavaScript, and b(&a, c) in C, which has no member functions and lowers
one to a free function taking the instance. That is the same lowering
CGenerator already performs on the declaration, so the call site now follows
the declaration it was emitted for.

The callee itself is text and is written verbatim. A square root is
std::sqrt, Math.Sqrt, math.sqrt and Math.sqrt, and there is no shared idea
underneath those four spellings for the AST to hold, the way there is
underneath a type. Choosing the name stays the caller's, as SourceFile.Imports
and CompileTimeAssertion.Condition already are.

ExpressionStatement is where a call made for its effect stands. The AST could
say what to do with a value but not that a value is beside the point, so a
void call had nowhere to go at all.

ConditionalExpression is one node rather than a branch and a temporary because
four of the targets spell it as an expression. Python only reorders the
operands, which is exactly the kind of difference the AST exists to absorb.

Each node is added to AstSchema, AstFields, AstNodeCatalog, the YAML
serializer and deserializer, and all five generators. TryAttachAt is split in
three along the way, because one switch over every slot is more branches than
CA1502 accepts.

The C receiver lowering is checked by compiling it rather than by pinning its
spelling: whether the call site and the declaration meet is a question about
types, which is not visible in the text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGCMUrT3jBgmANHNHcPmBf
Main gained a sixth target language while this branch was open, which is
more than a textual conflict: RustGenerator derives from
StandardLanguageGenerator, so it inherited the three new dispatch cases and
two of the three defaults were already right for it. The third was not.

Rust has no ternary operator at all, so the inherited `?:` would not have
been a different spelling of ConditionalExpression there — it would not have
parsed. RustGenerator now overrides it with an if-expression, which is the
same idea reached from the other side: the branches yield the value rather
than assigning one. It is parenthesised because an `if` at the start of a
statement is parsed as a statement, so a conditional used for its effect
alone would otherwise have its branches' values silently discarded.

A member call and a statement terminator needed no override: Rust spells
both the way the defaults already do, and the tests now assert that rather
than assuming it.

The Rust exemplar gains a member that calls another for its effect and then
chooses between two values, so both nodes are checked by rustc rather than
by pinning their text — the same standard the C receiver lowering is held
to. The C call test is ported onto ToolchainHarness, which main extracted
while this branch was open.

CLAUDE.md's conflict was two additions to the same bullet, both wanted.
The language counts in the prose are corrected throughout: a member call is
five of six targets rather than four of five, and a conditional is now three
spellings rather than two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGCMUrT3jBgmANHNHcPmBf
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 4f7c21d into main Sep 12, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/bold-planck-mxarux branch September 12, 2026 09:17
matt-edmondson pushed a commit that referenced this pull request Sep 12, 2026
The quality gate passed on #57 and it merged before these landed, so all 24
findings are in code that is now on main. One of them is a real defect rather
than a style note.

S2699, reported as a blocker, was right: WhatThePaletteCreates_IsReadyToConnect
reads every slot of every new node and asserts nothing about what comes back.
It also has no guard against its own filter matching nothing, so it would pass
just as happily if the palette stopped offering these nodes entirely — which is
the regression it exists to catch. It now asserts that all three templates are
found, and that every operand already sitting in a slot is the placeholder the
rest of the library understands rather than a null it does not.

S1192 asked for constants where a literal repeats. AstSchema already keeps one
for the arguments slot's name and the new slots now have theirs; the palette's
category names and the serializer's expectedType key follow the same
convention their own files already set.

The MSTest analyzers asked for the assertions that say what they mean:
HasCount over AreEqual on a count, IsEmpty over AreEqual against zero,
AreSequenceEqual over CollectionAssert, and Contains over IsTrue around a
predicate. All four are already used elsewhere in this suite, so these match
the surrounding code rather than introducing a second style.

MSTEST0046 is left as it was, and that is the one deliberate exception. It
prefers Assert.Contains over StringAssert.Contains, but this suite calls
StringAssert.Contains in 188 places and has no bare Assert.Contains anywhere.
Two call sites written the other way would read as a mistake rather than as an
improvement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGCMUrT3jBgmANHNHcPmBf
matt-edmondson added a commit that referenced this pull request Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The expression vocabulary cannot say a call, so every non-trivial body is smuggled through VariableReference as target-language text

2 participants