Add CallExpression, ExpressionStatement and ConditionalExpression - #57
Merged
Conversation
…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
|
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
Address the SonarCloud findings from #57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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
voidcall —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
VariableReferenceas 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:CallExpression(receiver: point, callee: "translate", args: [dx, dy])point.translate(dx, dy)translate(&point, dx, dy)C has no member functions and lowers one to a free function taking the instance, which is the transformation
CGeneratoralready 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:(ready ? go : wait)(go if ready else wait)(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 makesifan expression instead.The callee-naming question, settled
#55 flagged this as needing a decision before the node landed:
sqrtisstd::sqrt,Math.Sqrt,math.sqrtandf64::sqrtacross the targets.Calleeis 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 whyTypeReferenceis structure. Choosing the name stays the caller's, exactly asSourceFile.ImportsandCompileTimeAssertion.Conditionalready 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
selfparameter 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 aCallExpressionknows 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.TryAttachAtis 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:
under
-std=c11 -Wall -Wextra -pedantic. And Rust, proving the void call and the if-expression: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.
RustGeneratorderives fromStandardLanguageGenerator, so it inherited the three new dispatch cases — two of the three defaults were already correct for it, and the third was not:?:would not have compiled.RustGeneratornow writes anifexpression, parenthesised because anifat 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 buildanddotnet build -c Release— clean, 0 warnings, both target frameworksccandrustcrather than reporting inconclusive🤖 Generated with Claude Code
https://claude.ai/code/session_01QGCMUrT3jBgmANHNHcPmBf