Skip to content

The bridge: a schema becomes C++ - #160

Open
matt-edmondson wants to merge 2 commits into
mainfrom
claude/peaceful-mayer-oo6l2u
Open

The bridge: a schema becomes C++#160
matt-edmondson wants to merge 2 commits into
mainfrom
claude/peaceful-mayer-oo6l2u

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

All three sections of Holotype's docs/generated-cpp-target.md now generate from a .schema.json rather than from an AST built by hand. ktsu.Coder already had acceptance tests that built those headers as ASTs; these start one step further back, from the file a person would actually write, and so are what says the schema can describe the thing at all.

Schema.Cpp turns a schema into a ktsu.Coder AST and hands it to CppGenerator, which owns every question about how C++ is spelled. Nothing in this project writes a brace.

Where it lives, and why

It cannot ship inside ktsu.Schema: that library publishes net8.0 and ktsu.Coder does not. SchemaGenerator.Register is how it is found by the language a schema names, and SchemaTool/Program.cs is the worked example of a host doing it — which makes that seam load-bearing rather than hypothetical.

The options record is smaller than expected

Almost everything comes from the schema. A unit is a semantic type, so the generator emits the class. A class is a struct. The standard library spells a string, a sequence, a view and an absent value.

What's left is the short list a program supplies for itself:

Option Because standard C++ has
Vector2 / Vector3 / Vector4 no fixed-shape numeric vector (std::array makes the arity a type argument, and a schema's vector is one value with named components rather than a short sequence)
ColorRgb / ColorRgba no colour
Handle no identifier-with-a-generation
Result no fallible return
DateTime an answer that is rarely the one wanted — system_clock::time_point is a duration since an epoch, not a calendar date

A target that has them says how it spells them and which header they come from — a generated file naming holo::Vector3 without including the header that declares it does not compile, and nothing but whoever supplied the name knows which header that is. A target that has not is refused those schema types by name, naming the option to set.

ExistingTypes is the other direction: a semantic type the target already hand-wrote is named rather than generated a second time beside it, which is how the exemplar gets holo::Kilograms instead of a freshly emitted Kilograms.

One deliberate deviation from the document

An enum goes to namespace scope in a header of its own rather than nested in the struct.

The document nests it, on the grounds that two components in one file might both declare a kind — true of the TOML front end, where an enum belonged to the component that declared it. In ktsu.Schema an enum is a top-level element any class may name, so nesting it in the one class that uses it today would move the type the moment a second class used it, breaking every caller that spelled RigidBody::BodyKind because of an unrelated edit. Field offsets are unaffected, so generated_compiles.cpp still holds. The acceptance test asserts this difference rather than absorbing it.

Three defects found by reading the output, not by a test going green

  • A file naming a generated type did not include the header that declares it. A component using an enum, a class or a semantic type did not compile.
  • A float default was written as the schema holds it. C++ reads 1 as an int and 1.0 as a double, and a braced initialiser refuses either for narrowing — which is the whole reason to brace it. Now 1.0f.
  • An enum default was constructed rather than named: BodyKind{ BodyKind::Dynamic } where the document has BodyKind::Dynamic. The schema holds a value's name, so qualifying it is the generator's job.

And two in code I had just written: a file with no namespace configured indexed an empty array, and the banner cited a source file the schema does not remember. Schema now keeps SourceFileName beside SourceDirectory — not serialized, for the same reason the directory is not.

One gap left open, and asserted

SchemaFunction cannot say a call leaves the receiver unchanged, so the document's const on find() is not emitted. The four conventions cover whether an argument is read-only and say nothing about the receiver. AQueryCannotYetSayItLeavesTheReceiverUnchanged asserts the gap so it is visible rather than merely absent, and fails when it is closed. The AST is ready for it (FunctionDeclaration.IsReadOnly emits the trailing const), so closing it is a property on SchemaFunction and one line in the builder.

Testing

46 tests in Schema.Cpp.Test — three acceptance tests against the document's sections, the complete §4 type table a row at a time, every refusal, the naming rules, and the registration seam. 397 in Schema.Test, 201 in SchemaEditor.Test. Whole solution builds with 0 warnings on both net10.0 and net9.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AhoPJ5AbxP8QEBNxPQYEPk


Generated by Claude Code

Schema.Cpp turns a schema into a ktsu.Coder AST and hands that to CppGenerator,
which owns every question about how C++ is spelled. Nothing here writes a
brace. All three sections of Holotype's docs/generated-cpp-target.md now
generate from a .schema.json rather than from an AST built by hand, which is
what says the schema can describe the thing at all.

It lives outside the core library because it cannot ship there: ktsu.Schema
publishes net8.0 and ktsu.Coder does not. SchemaGenerator.Register is how it is
found by the language a schema names, and SchemaTool is the worked example of a
host doing it.

CppGeneratorOptions is what a target says that the schema cannot, and the list
is shorter than expected. A unit is a semantic type, so the generator emits the
class; a class is a struct; the standard library spells a string, a sequence, a
view and an absent value. What is left is what a program supplies for itself: a
fixed-shape numeric vector, a colour, an identifier with a generation, a
fallible return, a calendar date. A target that has them says how it spells them
and which header they come from. A target that has not is refused those schema
types by name, naming the option to set, rather than handed a header that will
not compile. ExistingTypes is the other direction: a semantic type the target
already hand-wrote is named rather than generated a second time beside it.

One file per element. An enum goes to namespace scope in a header of its own
rather than nested in the class that names it - the one place the output
deliberately differs from the document. The document nests it, which was right
when an enum belonged to the component that declared it; in ktsu.Schema an enum
is a top-level element any class may name, so nesting it in the one class using
it today would move the type the moment a second class used it, breaking every
caller that spelled RigidBody::BodyKind because of an edit somewhere else. Field
offsets are unaffected, so generated_compiles.cpp still holds.

Three defects found by reading the output against the document rather than by a
test going green:

- A file naming a generated type did not include the header that declares it,
  so a component using an enum, a class or a semantic type did not compile.
- A float default was written as the schema holds it. C++ reads 1 as an int and
  1.0 as a double, and a braced initialiser refuses either for narrowing, which
  is the whole reason to brace it.
- An enum default was constructed rather than named: BodyKind{ BodyKind::Dynamic }
  where the document has BodyKind::Dynamic. The schema holds a value's name, so
  the generator is what qualifies it.

And two in code I had just written: a file with no namespace configured indexed
an empty array, and the banner cited a source file the schema does not remember.
Schema now keeps SourceFileName beside SourceDirectory - not serialized, for the
same reason the directory is not.

One gap the exemplar exposes is left open and asserted rather than absent:
SchemaFunction cannot say a call leaves the receiver unchanged, so the document's
const on find() is not emitted. The AST is ready for it; closing it is a property
on SchemaFunction and one line in the builder.

46 tests in Schema.Cpp.Test, 397 in Schema.Test, 201 in SchemaEditor.Test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhoPJ5AbxP8QEBNxPQYEPk
SonarCloud on #160: four findings, all in code this PR added.

S1192 three times - "underlying", "value_" and "value" each appear four times
across the semantic type's members, and they are one word each rather than four
coincidences. The alias is what the shim calls its representation, the field is
where it holds the value, and the accessor and the parameter that fills it share
a name because they are the same idea from either side. Naming them says that,
and says why the field has its trailing underscore: the accessor beside it is
value(), so the two would otherwise collide.

S3267 once - the loop over semantic types filtered inside its body, which reads
as a loop over all of them. Saying Where once says what is being skipped where
a reader looks for it.

46 tests in Schema.Cpp.Test and 397 in Schema.Test still pass, and the three
acceptance tests are what say the generated text did not move: every one of
these names appears in the exemplar's output.

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

Copy link
Copy Markdown

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.

2 participants