Skip to content

Emit interfaces, and spell the four types in a signature - #176

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/blissful-euler-fzuap2
Sep 12, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/blissful-euler-fzuap2

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

A Span, a Result, an Optional and an Interface were 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, an Interface member named a type nothing produced.

public interface Renderer
{
    void Draw(System.ReadOnlySpan<float> Vertices);

    ktsu.Schema.Runtime.Result<ktsu.Schema.Runtime.Handle<Texture>, Failure> Load(string Path);

    ktsu.Schema.Runtime.Result<Failure> Flush();

    [ktsu.Schema.Runtime.SchemaQuery]
    int Count(System.Span<int> Scratch, ref int Tally);
}

An interface keeps the name the schema gave it

No I prefix. 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 called Item into tem. The classes and the enums already follow this rule.

A view is two types where C++ needs one qualifier

ReadOnlySpan<T> when the elements are In, Span<T> otherwise — the same reading the C++ generator gives it, where the difference is a const on the element. It is spelled by MapParameter rather than 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 outright, which is a limit rather than a gap; CLAUDE.md says so rather than leaving it implicit.

A fallible return has two arities

Result<T, TError>, and Result<TError> for a Result<Void> — C# has no void type 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 by errorType, read through the type's ParentSchema the same way a Semantic resolves its declaration.

An absent value is Optional<T>, not T?

T? would be idiomatic and means two different things: over a value type it is Nullable<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 writing T? would keep an Optional<Int> and lose an Optional<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

  • isQuery has no syntax at all — C++ writes a trailing const — so a query carries [SchemaQuery].
  • Direction is the parameter modifier, and In is no modifier: 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 without an attribute.

void is spelled only as a return type, by MapReturnType — there is no field, property or parameter of one.

The round trip now includes interfaces

Schema.AddInterface(Type) is the counterpart of AddClass(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.

TestEverySignatureRoundTripsAsTheSchemaDeclaredIt asserts the whole set in one line each:

Void Draw(IN Span<Float> Vertices)
Result<Handle<Texture>> Load(IN String Path)
Result<Void> Flush()
query Int Count(OUT Span<Int> Scratch, INOUT Int Tally)

Along the way

GetOrCreateSchemaType hit 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 with Expected: O, Actual: I.

One real bug caught this way and fixed before it shipped: a void return was coming out as object?, because MapType had no Void case 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.0 and net8.0 with no new analyzer warnings.

One suppression

CA1716 on Optional<T>Optional is a reserved word in VB. Suppressed narrowly on the type with a justification: the schema type is called Optional, 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

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
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit dafee99 into main Sep 12, 2026
12 checks passed
matt-edmondson added a commit that referenced this pull request Sep 12, 2026
Clear the two findings #176 merged just before
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