Skip to content

C#: the family surface — ForVersion results carry data, not just verbs - #2

Merged
skarndev merged 3 commits into
mainfrom
feature/csharp-family-surface
Aug 21, 2026
Merged

skarndev merged 3 commits into
mainfrom
feature/csharp-family-surface

Conversation

@skarndev

@skarndev skarndev commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

The gap

C#'s ForVersion returned the family base, which had no data members — every property access needed a pattern-match downcast across up to nine range classes, and the dynamic wrappers people built around it were slow (DLR call-site binding + reflection). Python never had this problem: for_version() returns the concrete class, and AnyWMO-style unions give type checkers intersection semantics.

The fix

skarndev/welder-csharp#1 closes it at the rod: a welded family base that opts in gains a synthesized version-agnostic surface — the member intersection the era classes bind identically, as type-switch dispatch members. Base-typed code now reads, writes and processes without naming an era:

static int TriangleCount(Formats.Wmo.WMO model)   // any era
{
    var n = 0;
    foreach (var group in model.Groups)           // FamilyVector<WMOGroup> live view
        n += group.Body.Indices.Count / 3;        // base body -> shared VectorUshort
    return n;
}
  • Identical spellings hoist exactly — including the shared scalar-sequence wrappers, so zero-copy views survive the base-typed path.
  • Welded members hoist as their own family base (WMO.Root → WMORoot); a wrong-era assignment throws InvalidCastException.
  • Welded-element sequences hoist as read-only FamilyVector<Base> live views.
  • Identically-spelled methods (Read/Write/Validate) hoist as forwarding dispatch.
  • Era-gated members stay on the concretes — pattern matching, the same contract as Python's isinstance narrowing.

Performance: one isinst chain in front of the same P/Invoke the concrete path makes — no DLR, no reflection.

The opt-in

Synthesizing members onto a base is too intrusive to infer from structure, so it is strictly opt-in per base via the rod's own mark, [[=welder::rods::csharp::family_surface]] (deliberately not welder-core vocabulary — no other backend honors it). The *Base definitions live in format headers that must parse in Python-only builds where welder-csharp isn't even fetched, and gcc-16 reads annotations off the defining declaration only — so the mark rides the annotation lists behind WOWLIB_CS_FAMILY_SURFACE (core/lang.hpp, the header that already respells the rod's lang identity). On WOWLIB_BUILD_CSHARP configures, Dependencies.cmake defines WOWLIB_CSHARP_ROD and puts the rod's headers on the include path directory-wide, so every TU of the tree agrees on each class's annotation list; everywhere else the macro expands to nothing and the annotation never exists. All 21 *Base classes carry it (18 families synthesize today; Skeleton/WDTOcclusion/WDTParticulates are single-range until a future split).

Changes here

  • Pin bump to welder-csharp ffa71b2 (merge Family surface: version-agnostic dispatch members on welded family bases welder-csharp#1 first); welder stays at its existing pin.
  • 21 *Base annotations + the WOWLIB_CS_FAMILY_SURFACE macro + the CMake wiring.
  • tools/gen_cs_format_facades.py: the FS_VERBS dispatch block is deleted — the rod hoists the fs verbs itself now (emitting both would be CS0111). The script keeps the one thing the rod cannot know: the era→range factory mapping (ForVersion + per-era statics).
  • Docs: the version-agnostic guide's C# tabs now show the base-typed spelling alongside Python's.
  • Tests: new FamilySurfaceTests lock the tree walk (base WMO → FamilyVector<WMOGroup> → base body → shared Indices), wrong-era InvalidCastException, base Validate dispatch, and the bare-base InvalidOperationException. dotnet test tests/csharp: 33/33 green against the regenerated gcc16-csharp tree; a marked format header also syntax-checks clean with no rod on the include path (the Python-only shape).

🤖 Generated with Claude Code

skarndev and others added 3 commits August 20, 2026 00:09
…verbs

The tester-reported gap: C#'s ForVersion returned the family base, which had
no data members — every property access needed a downcast across up to nine
range classes, and the dynamic wrappers people built around it (DLR) were
slow. Python never had this problem: for_version returns the concrete class
and AnyWMO gives checkers intersection semantics.

welder-csharp 6c5bdbf closes it at the rod: every welded family base gains
a synthesized version-agnostic surface — the member intersection the era
classes bind identically, as type-switch dispatch members. Identical
spellings hoist exactly (incl. the shared scalar-seq wrappers, zero-copy
views intact); welded members hoist as their own family base (getter
upcasts, setter downcasts); welded-element sequences hoist as read-only
FamilyVector<Base> live views; identically-spelled methods (Read/Write/
Validate) hoist as forwarding dispatch. Era-gated members stay on the
concretes — pattern matching, the same contract as Python's isinstance
narrowing. Pure managed text: no new P/Invokes, the shim is byte-identical.

Here: bump the welder-csharp pin; DELETE the facade script's FS_VERBS block
(the rod now hoists the fs verbs itself — emitting both would be CS0111;
the script keeps the one thing the rod cannot know, the era->range factory
mapping); rewrite the guide's C# tabs to the base-typed spelling; new
FamilySurfaceTests lock the tree walk (base WMO -> FamilyVector<WMOGroup>
-> base body -> shared Indices), the wrong-era InvalidCastException, base
Validate dispatch, and the bare-base InvalidOperationException. 33 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… a macro

The blanket synthesis had no right to shove members into every common base;
each family base now opts in explicitly. The marker is the C# rod's own
([[=welder::rods::csharp::family_surface]], welder-csharp ffa71b2) — NOT
welder core vocabulary (a core mark was tried and reverted, welder#3
closed: core should not name a feature only one rod honors).

The layering squeeze and its resolution: the *Base definitions live in
format headers that must parse in Python-only builds where welder-csharp is
not even fetched, and gcc-16 reads annotations off the DEFINING declaration
only (verified: annotated redeclarations are silently dropped), so the
bindings layer cannot attach the mark after the fact. The mark therefore
rides the base annotation lists behind WOWLIB_CS_FAMILY_SURFACE
(core/lang.hpp — the header that already respells the rod's lang identity):
on WOWLIB_BUILD_CSHARP configures, Dependencies.cmake defines
WOWLIB_CSHARP_ROD and puts the rod's headers on the include path
DIRECTORY-WIDE, so the library, the generator TUs and the shim all agree on
every class's annotation list; everywhere else the macro expands to nothing
and the annotation never exists (a marked header syntax-checks clean with
no rod on the path).

All 21 *Base classes carry the macro (after weld_as, before doc; the
trailing comma rides inside the macro — 18 families synthesize today,
Skeleton/WDTOcclusion/WDTParticulates are single-range until a future
split). welder pin returns to main-era 542176e; welder-csharp pin bumps to
ffa71b2. Same surface, same tests: 18 family blocks, 33/33 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… on main

PR#1 merged (true merge, tree byte-identical to the ffa71b2 branch head
this previously pinned) — repoint at the main-history SHA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@skarndev
skarndev merged commit 0e8a3ee into main Aug 21, 2026
8 checks passed
@skarndev
skarndev deleted the feature/csharp-family-surface branch August 21, 2026 21:03
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.

1 participant