Skip to content

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

Closed

Description

@matt-edmondson

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.Coder has 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 a constexpr constructor, 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, because a.b(c) is a member call in C#, C++, Python and JS but b(&a, c) in C — which is exactly the transformation CGenerator already 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 it b 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions