Vocabulary the AST was missing: a type that is a type, and two modifiers on a function - #44
Merged
Merged
Conversation
Every type in the AST was a bare string: ClassDeclaration.BaseType,
FunctionDeclaration.ReturnType, Parameter.Type, VariableDeclaration.Type. That
is enough for Foo and hopeless for std::span<const Velocity> -- a generator
handed that string can only paste it, so whoever built the node had to spell
the target language themselves, which is the generator writing C++ by hand
again.
TypeReference carries a name, the types it is parameterised by in order, whether
the value may be written through, and how it is reached. Each generator then
decides the spelling: C++ writes const, & and *; C# writes none of them, because
read-only-ness of an argument is `in` and belongs on the parameter; Python
parameterises with brackets, which is only expressible now that the arguments
are a list rather than part of a name.
Parse and ToString are inverses, and text the grammar cannot read becomes a name
holding that text verbatim. So a string that means nothing to this grammar
survives a round trip intact rather than being dropped or half-read -- which is
what lets the string-shaped API keep working. An implicit conversion from string
keeps the common case short, so `new Parameter("scale", "int")` and the editor's
text row are untouched, and YAML still stores the text form.
The change found a bug in the C++ and C# type tables. Both mapped a name to a
whole type -- list to std::vector<std::any> -- so a name and its arguments could
not both be honoured: list<int> used to come out as neither (it missed the
lookup and passed through as `list<int>`), and mapping the name alone would have
produced std::vector<std::any><int>. The tables now map a name to a name, with
the argument a container is named without kept separately. So list is still
std::vector<std::any> and list<int> is now std::vector<int>.
400 tests pass, 378 before and 22 new, with 0 warnings across the solution. The
last two cover the case this exists for: In Span<Velocity> generating
std::span<const Velocity>, and one type spelled three different ways by three
generators.
Nothing here decides the node set. That comes from the target header in
matt-edmondson/Holotype#5, which this is the first change derived from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhoPJ5AbxP8QEBNxPQYEPk
Two modifiers on FunctionDeclaration, both in the mould Visibility already set: the AST says what is true and each generator says as much of it as its language can. IsStatic is the more interesting of the two, because it is the one modifier that changes a signature rather than decorating it. C++, C# and JavaScript spell it `static` alongside the declaration; Python spells it @staticmethod and drops `self`, which the generator supplies precisely because no other target language has a receiver in its parameter list. An instance method still takes one. IsPure means the result depends only on the arguments and calling it changes nothing. C++ gets [[nodiscard]] and C# gets [Pure]; Python and JavaScript get nothing, because they have nothing to say about it and inventing a spelling would be worse than silence. [[nodiscard]] rather than __attribute__((pure)) deliberately. Both start from the same observation -- discarding the result of a call that does nothing else is always a mistake -- but the attribute also tells the optimiser the call may be elided or duplicated, which is a stronger promise than the AST is in a position to make about a body it did not read. The standard spelling says the part that is certainly true. [Pure] is written in full as [System.Diagnostics.Contracts.Pure], because the AST has no import node to hang a `using` on: generated C# is a fragment, and a short name in it would be one the reader has to arrange for. Worth being explicit that IsPure is not C++'s pure virtual, which means a declaration has no definition. That is a different property, it is what the Holotype target header needs for an interface, and the AST does not model it yet. Both are written to YAML only when true, so a document says nothing rather than saying false twice on every function ever written, and a document written before they existed still opens with both off. The editor's inspector offers each as a flag. 413 tests pass, 400 before and 13 new, with 0 warnings across the solution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhoPJ5AbxP8QEBNxPQYEPk
SonarCloud reported three S3358s on the two type mappers: a ternary whose false branch is another ternary, which reads as one expression and is three. Each is now a statement. The C++ mapper's argument list is its own method, since "the arguments, or the one a bare container does not name" is a decision worth a name; the C# one splits the name lookup out for the same reason, which also gives the void case somewhere to explain itself -- it is matched case-insensitively where the rest of that table is not, and nothing said why. One of the three predates this branch. The line changed, so the rule fired on it, and it is the same fix as the other two. Found by querying the analysis on the pull request rather than by building locally, which is the check that works. 413 tests still pass, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhoPJ5AbxP8QEBNxPQYEPk
|
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.



Two things the AST could not say. Both in the mould
Visibilityalready set — the AST says what is true, and each generator says as much of it as its language can.1. A type is a type, not a string that happens to spell one
Every type in the AST was a bare string —
ClassDeclaration.BaseType,FunctionDeclaration.ReturnType,Parameter.Type,VariableDeclaration.Type.That is enough for
Fooand hopeless forstd::span<const Velocity>. A generator handed that string can only paste it, so whoever built the node had to spell the target language themselves — which is the generator writing C++ by hand again, one layer up.TypeReferencecarries a name, the types it is parameterised by in order, whether the value may be written through, and how it is reached:span<readonly Velocity>std::span<const Velocity>span<Velocity>span[Velocity]List<int>std::vector<int>List<int>List[int]C# writes neither
constnor*: read-only-ness of an argument isin, which belongs on the parameter rather than on the type. Python parameterises with brackets, which is only expressible now that the arguments are a list rather than part of a name.IsReadOnlyrather thanIsConstbecause the property is named for what it means, not for how one language spells it.Nothing that held a string stops working
ParseandToStringare inverses, and text the grammar cannot read becomes a name holding that text verbatim — so a string that means nothing to this grammar survives a round trip intact rather than being dropped or half-read. That property is what lets the string-shaped API stay:The editor's inspector row is still a text field, and what a user types there now parses into structure.
It found a bug on the way in
Both type tables mapped a name to a whole type —
list→std::vector<std::any>— so a name and its arguments could not both be honoured:list<int>used to miss the lookup entirely and pass through aslist<int>, which is not C++.std::vector<std::any><int>, which the new test caught.The tables now map a name to a name, with the argument a container named without one is a container of kept separately. So
listis stillstd::vector<std::any>andlist<int>is nowstd::vector<int>.2. A function can say it takes no receiver and changes nothing
IsStaticThe one modifier that changes a signature rather than decorating it. C++, C# and JavaScript spell it
staticalongside the declaration; Python spells it@staticmethodand dropsself— which the generator supplies precisely because no other target language has a receiver in its parameter list.IsPureThe result depends only on the arguments and calling it changes nothing. C++ gets
[[nodiscard]], C# gets[Pure]; Python and JavaScript get nothing, because they have nothing to say about it and inventing a spelling would be worse than silence.[[nodiscard]]rather than__attribute__((pure)), deliberately. Both start from the same observation — discarding the result of a call that does nothing else is always a mistake — but the attribute additionally tells the optimiser the call may be elided or duplicated, which is a stronger promise than the AST is in a position to make about a body it did not read. The standard spelling says the part that is certainly true.[Pure]is written out as[System.Diagnostics.Contracts.Pure]because the AST has no import node to hang ausingon: generated C# is a fragment, and a short name in it would be one the reader has to arrange for.IsPureis not C++'s pure virtual, which means a declaration has no definition. That is a different property, it is what the Holotype target header needs for an interface, and the AST does not model it yet.Both are written to YAML only when true — a document says nothing rather than saying false twice on every function ever written — and a document written before they existed still opens with both off. The inspector offers each as a flag.
Verification
dotnet build -c Release— 0 warnings, 0 errors across the solutionThe tests worth reading are the ones that pin a spelling per language:
In Span<Velocity>generatingstd::span<const Velocity>, one type spelled three different ways by three generators, and Python keepingselfon an instance method while dropping it from a static one.Two targeted suppressions, no global ones:
CA1720onTypeIndirection.Pointer(a pointer is what the member means; naming around the rule would leave the enum unable to say the one thing it exists to distinguish) andCA2225on the string conversion (Parseis the named alternate one way,ToStringthe other).What this does not do
It does not decide the node set. The struct, enum, field, namespace, using-alias, constructor and conversion-operator nodes come from the target header in matt-edmondson/Holotype#5, which is still open for redlining. What is here is downstream of nothing in that document:
std::span<const Velocity>is unspellable however the exemplar settles, and so are these two modifiers.🤖 Generated with Claude Code
https://claude.ai/code/session_01AhoPJ5AbxP8QEBNxPQYEPk