Emit interfaces, and spell the four types in a signature - #176
Merged
Merged
Conversation
A Span, a Result, an Optional and an Interface were the four types left once a handle, a semantic type and a vector were spelled, and they were never a gap in the mapping the way those were. Each is a decision about the generated C# API - and the one that had to come first is that an interface is emitted at all. Until it was, an Interface member named a type nothing produced. An interface is emitted under the name the schema gave it, with no `I` prefix. The compiled name is what the importer reads back, so a prefix would have to be stripped again, and stripping cannot tell a prefix from a first letter - an interface called `Item` would come back as `tem`. The classes and the enums follow the same rule. A view is a ReadOnlySpan when its elements are In and a Span otherwise, which is the same reading the C++ generator gives it, where the difference is a const on the element rather than a second type. It is spelled by the parameter rather than by MapType, because the direction that decides which belongs to the parameter and a type has no route back to one. In a member position a Span stays object?: a field of a ref struct is something the language forbids, not something this mapping is missing. A fallible return is Result<T, TError>, or Result<TError> for a Result<Void>, since C# has no void type argument to close the value-carrying form over. The arity is what tells the two apart on reimport. The error is the schema's, once, read through the type's ParentSchema the way a Semantic resolves its declaration. An absent value is Optional<T> rather than T?, which means two different things: over a value type it is a type and over a reference type it is an annotation that is not part of the type at all. One survives a reimport and the other does not, so T? would keep an Optional<Int> and lose an Optional<Item> - the one asymmetry the mapping cannot afford. Two things a signature says that C# cannot. IsQuery has no syntax at all, so a query carries SchemaQueryAttribute. Direction is the parameter modifier, and In is no modifier, because an ordinary by-value parameter is already one the caller supplies and the callee does not modify; out, ref and by-value are three different compiled signatures, so all three read back with no attribute. Void is spelled only as a return type. Schema.AddInterface(Type) is the counterpart of AddClass(Type), and is what makes an interface part of the round trip rather than something emitted and never read back. GetOrCreateSchemaType had grown past what one method should hold, so the generic types it recognises are a table beside the direct one, and the enum and class arms are named methods. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
All five are in code the previous change added, and the quality gate passed with them open rather than because of it. The critical one is already gone: `GetOrCreateSchemaType` was flagged at a cognitive complexity of 18, which is what the table of generic types and the named enum and class arms were for. `Handle<T>`'s unused type parameter is the design rather than an oversight, so it is suppressed where it is, with why: T is what keeps a handle to a texture out of a slot meant for a mesh, and storing nothing of it is what keeps the handle the same bytes whatever it names. The repeated doc-comment delimiters are now two constants and a `WriteSummary` helper, which is what the six sites were each spelling by hand. It reads better than the finding asked for: the delimiters appear once, and a one-line summary is one call rather than three. The test assertions move to `Assert.AreSequenceEqual`, which is the MSTest 4 spelling and drops a `ToArray` at each site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
|
matt-edmondson
added a commit
that referenced
this pull request
Sep 12, 2026
Clear the two findings #176 merged just before
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.



A
Span, aResult, anOptionaland anInterfacewere the four types left after #175, and they were never a gap in the mapping the way a handle or a semantic type was. Each is a decision about the generated C# API — and the one that had to come first is that an interface is emitted at all. Until it was, anInterfacemember named a type nothing produced.An interface keeps the name the schema gave it
No
Iprefix. The prefix is the C# convention and it is not available here: the compiled name is what the importer reads back, so one would have to be stripped again — and stripping cannot tell a prefix from a first letter, which would turn an interface calledItemintotem. The classes and the enums already follow this rule.A view is two types where C++ needs one qualifier
ReadOnlySpan<T>when the elements areIn,Span<T>otherwise — the same reading the C++ generator gives it, where the difference is aconston the element. It is spelled byMapParameterrather thanMapType, because the direction that decides which belongs to the parameter and a type has no route back to one.In a member position a
Spanstaysobject?. A field of aref structis something the language forbids outright, which is a limit rather than a gap;CLAUDE.mdsays so rather than leaving it implicit.A fallible return has two arities
Result<T, TError>, andResult<TError>for aResult<Void>— C# has novoidtype argument to close the value-carrying form over, and the arity is what tells the two apart on reimport. The error is the schema's, named once byerrorType, read through the type'sParentSchemathe same way aSemanticresolves its declaration.An absent value is
Optional<T>, notT?T?would be idiomatic and means two different things: over a value type it isNullable<T>, a type; over a reference type it is an annotation that is not part of the type at all. One survives a reimport and the other does not, so a generator writingT?would keep anOptional<Int>and lose anOptional<Item>. That is the one asymmetry the mapping cannot afford, so this is one type meaning one thing whatever it wraps.Two things a signature says that C# cannot
isQueryhas no syntax at all — C++ writes a trailingconst— so a query carries[SchemaQuery].Inis no modifier: an ordinary by-value parameter is already one the caller supplies and the callee does not modify.out,refand by-value are three different compiled signatures, so all three read back without an attribute.voidis spelled only as a return type, byMapReturnType— there is no field, property or parameter of one.The round trip now includes interfaces
Schema.AddInterface(Type)is the counterpart ofAddClass(Type). Without it an interface was something emitted and never read back, which is the half of "the two mappings are inverses" that interfaces had no part in.TestEverySignatureRoundTripsAsTheSchemaDeclaredItasserts the whole set in one line each:Along the way
GetOrCreateSchemaTypehit CA1502 at a cyclomatic complexity of 32. Rather than suppress it, the generic types it recognises are now a table beside the direct one — which reads as the mapping it is and collapsed three bespoke probes into nine entries — and the enum and class arms are named methods.Tests
Thirteen new in
GeneratedSignatureSpellingTests, all compiling the generated source, plus two exercising the new runtime types directly since nothing else does. Both halves verified by reverting them one at a time: neutering the four generator cases fails seven of the thirteen, and dropping the importer's reading of span direction fails the round-trip test withExpected: O, Actual: I.One real bug caught this way and fixed before it shipped: a
voidreturn was coming out asobject?, becauseMapTypehad noVoidcase and the member position cannot have one.Schema.Test 440/440, Schema.Cpp.Test 67/67, Schema.Editor.Test 201/201. Full solution builds clean on
net10.0,net9.0andnet8.0with no new analyzer warnings.One suppression
CA1716onOptional<T>—Optionalis a reserved word in VB. Suppressed narrowly on the type with a justification: the schema type is calledOptional, and renaming the runtime type would leave the two vocabularies disagreeing.Tagged
[minor]: the generated C# API gains interfaces and three runtime types.🤖 Generated with Claude Code
https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
Generated by Claude Code