Repository navigation
The expression vocabulary cannot say a call, so every non-trivial body is smuggled through VariableReference as target-language text #55
Description
The gap
Coder/Ast/ has five expression nodes and two statement nodes:
| Expressions | Statements |
|---|---|
BinaryExpression |
ReturnStatement |
UnaryExpression |
AssignmentStatement |
LiteralExpression<T> |
|
VariableReference |
|
ConstructionExpression |
There is no way to say a call. ConstructionExpression builds an instance of a type; it cannot express sqrt(x), PhysicalQuantityCore.Compare(this, other), or value.count(). There is also no statement node for an expression evaluated for its effect, so a void call — assert(...), printf(...), list.clear() — is not something the AST can say at all.
The declaration half of the AST is genuinely language-agnostic: ClassDeclaration, FunctionDeclaration, FieldDeclaration, TypeReference and Visibility all model structure rather than syntax, and the five generators spell that structure differently on purpose. The body half does not reach the same bar. Anything beyond a return of a literal, a variable, an operator application or a constructor call has to be handed to VariableReference as a name holding target-language text.
Why this matters beyond tidiness
VariableReference holding "sqrt(magnitude_squared())" passes straight through every generator unchanged. That is correct for CppGenerator and wrong for the other four, which means a tree built for one language cannot be emitted as another — the declarations translate and the bodies do not. That is the whole premise of a language-agnostic AST, so the gap is load-bearing rather than cosmetic.
It also means the graph editor can draw a function's shape but not its body, and the YAML round-trip preserves a C++ expression as an opaque string.
Evidence from a real consumer
ktsu.Semantics' C++ projection (Semantics.Cpp/CppQuantityGenerator.cs) builds SourceFile trees directly and runs CppGenerator over them. It hits this in three places, and each one is C++ text inside a node that claims to be a name:
A call in a return — magnitude():
string size = type.IsVector ? "sqrt(magnitude_squared())" : $"abs({Field(ValueName)})";
magnitude.Body.Add(new ReturnStatement(new ConstructionExpression(type.MagnitudeType)
{
Arguments = { new VariableReference(size) },
}));A fold of binary operators — magnitude_squared(), where a BinaryExpression tree would have worked but building it is more awkward than joining strings, so the string won:
squared.Body.Add(new ReturnStatement(new VariableReference(
string.Join(" + ", Components(type).Select(component => $"{Field(component)} * {Field(component)}")))));A void call with no home, which the generator's own comment calls out as a limitation rather than a preference:
return $"(assert({component}.count() {comparison} 0 && \"{says}\"), {component})";The check rides in the initialiser rather than sitting in the constructor's body, and that is a limitation rather than a preference:
ktsu.Coderhas no expression-statement node, so a call made for its effect —assert(...), and every other void call — is not something the AST can currently say. A comma expression in the initialiser is the recognised way to check a precondition in aconstexprconstructor, so this is a legitimate spelling rather than a workaround wearing a disguise, but the reason it was chosen is that the alternative could not be written.
That is a consumer reaching for a comma expression because the AST left it no other option.
Proposed scope
Two nodes carry most of the weight:
CallExpression— a callee, an argument list, and an optional receiver for a member call. The receiver has to be modelled rather than folded into the name, becausea.b(c)is a member call in C#, C++, Python and JS butb(&a, c)in C — which is exactly the transformationCGeneratoralready performs for member functions at the declaration level, and would need to perform for call sites too.ExpressionStatement— an expression evaluated for its effect. Trivial to emit (the expression plus the language's terminator) and it closes the void-call hole.
Two more are worth considering in the same pass, since both currently force the same text escape hatch:
ConditionalExpression—a ? b : c. Python spells itb if a else c, which is precisely the kind of difference the AST exists to absorb.ThrowExpression/ThrowStatement— C++ and C diverge sharply here, so this may be better deferred until there is a consumer for it.
Each new node also needs AstSchema, AstFields, the YAML serializer and deserializer, and all five generators — which is what #49 proposes to make enforceable, so landing that first would be worth it.
Open question
CallExpression reintroduces the name-mapping problem at the body level. sqrt is std::sqrt in C++, Math.Sqrt in C#, math.sqrt in Python and Math.sqrt in JS. The declaration half solved this for types with TypeReference plus per-generator mappings; the body half would need the equivalent for functions, or a documented decision that a callee name is passed through verbatim and choosing it is the caller's job. Worth settling before the node lands, since it determines whether CallExpression.Callee is a string or something structured.
Activity