Skip to content

Family surface: version-agnostic dispatch members on welded family bases - #1

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

skarndev merged 3 commits into
mainfrom
feature/family-surface

Conversation

@skarndev

@skarndev skarndev commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

What

Welded families — two or more welded classes deriving one welded base (the shape a versioned class template welded per range makes) — can gain a synthesized version-agnostic surface on the base: the member intersection the concretes bind identically, each member a type-switch dispatch to the concrete class. Pure managed text over the concretes' own accessors: no new thunks, no new P/Invokes, the shim is byte-identical.

Strictly opt-in, via the rod's own mark: the surface is synthesized only for a base carrying [[=welder::rods::csharp::family_surface]] (<welder/rods/csharp/marks.hpp>). An unmarked base is never touched, however hoistable its family's intersection is — synthesizing members onto a base is too intrusive to infer from structure alone. The mark deliberately lives in the rod, not welder core: no other backend honors it, so core vocabulary stays untouched (the welder pin remains main). A consumer whose welded headers must also parse without this rod (a Python-only build) spells the mark behind a build-driven macro that expands to nothing when the rod is absent — the pattern is documented in marks.hpp.

Hoist rules (for a marked base)

  • Identical C# spellings hoist with the exact type (scalars, strings, the shared scalar-sequence wrappers), settable when every concrete's is.
  • Welded-class members hoist as the member types' common welded base — the getter upcasts, the setter downcasts (InvalidCastException names a wrong-era assignment).
  • Sequences of welded elements (vectors and fixed arrays) hoist as a read-only FamilyVector<ElementBase> live view: Count, indexer, duck-typed foreach over the concrete wrapper.
  • Methods spelled identically on every concrete hoist as forwarding dispatch; a bare base instance throws InvalidOperationException from the default arm.
  • Era-gated / shape-changing members stay on the concretes, reached by pattern matching.

Why

Downstream (wowlib), ForVersion(...) returned a family base that carried verbs but no data — every property access needed a downcast, and the dynamic wrappers consumers built around it were slow (DLR). This gives base-typed code the same intersection semantics Python gets from its union aliases, at type-switch cost in front of the same P/Invoke the concrete path makes.

Mechanics

The field/method emitters record each class's member manifest (names + resolved C# spellings, placeholders included) beside their emissions; the class writer flushes it into document::family_records along with the base's mark state; the render pass groups records by resolved first-welded-base and appends one partial class <Base> block per marked family after its namespace's declarations.

Tests

  • Existing goldens are unchanged.
  • family.hpp / gen_family.cpp + FamilyTests.cs lock the whole matrix: exact-type read/write, base-typed welded members incl. live-view write-through, FamilyVector iteration, wrong-era InvalidCastException, method dispatch, era-gated exclusion, bare-base throw — and the opt-in negative: an unmarked family whose perfectly hoistable intersection must synthesize nothing. 42 managed tests, full ctest suite 7/7, against stock welder main.

🤖 Generated with Claude Code

…mily bases

Two or more welded classes deriving one welded base (the shape a versioned
class template welded per range makes) now gain a synthesized surface ON the
base: the member intersection the concretes bind identically, each member a
type-switch dispatch to the concrete class. Pure managed text over the
concretes' own accessors — no new thunks, no new P/Invokes, the shim is
byte-identical.

Hoist rules:
- identical C# spellings hoist with the exact type (scalars, strings, the
  shared scalar-sequence wrappers), settable when every concrete's is;
- a welded-class member hoists as the member types' common welded base —
  the getter upcasts, the setter downcasts (InvalidCastException names a
  wrong-era assignment);
- a sequence of welded elements hoists as a read-only FamilyVector<Base>
  live view (Count + indexer + duck-typed foreach over the concrete wrapper);
- a method overload spelled identically on every concrete hoists as a
  forwarding dispatch; a bare base instance throws InvalidOperationException
  from the default arm.

Mechanics: the field/method emitters record each class's member manifest
(names + resolved C# spellings, placeholders included) beside their
emissions; the class writer flushes it into document::family_records; the
render pass groups records by resolved first-welded-base and appends one
'partial class <Base>' block per family after its namespace's declarations.
options::family_surface (default ON) restores the prior artifact when off.

Existing surfaces are untouched byte-for-byte (goldens unchanged): the
shared inheritance cases' sibling classes declare disjoint members, so no
family there has a non-empty intersection. New family.hpp/gen_family.cpp
pair + FamilyTests.cs lock the whole matrix (41 managed tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
skarndev and others added 2 commits August 21, 2026 15:46
…the option

Synthesizing members onto a base is too intrusive to infer from structure
alone — any two welded classes sharing a welded base would qualify. The
surface is now synthesized ONLY for a base carrying
[[=welder::mark::family_surface]] (skarndev/welder#3) covering this rod's
language; an unmarked base is never touched, however hoistable its family's
intersection is. The mark lives in welder CORE, not this rod, because base
definitions sit in consumers' core headers, which must parse without any
rod fetched (and gcc-16 ignores annotations on redeclarations, so the
bindings layer cannot attach one after the fact).

options::family_surface is gone — the mark is the one switch. Manifest
recording is unconditional now: an unmarked class's manifest still resolves
member types when it appears inside a marked family's members. welder pin
moves from main to the mark commit (restore main once welder#3 lands).
family.hpp marks its two bases and adds an Unmarked family whose perfectly
hoistable intersection must synthesize nothing; FamilyTests locks that
negative (42 managed tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design reversal of the previous commit's welder-core dependency: the
family_surface mark did not belong in welder core while only this rod
honors it (skarndev/welder#3 closed unmerged). The marker now lives here —
<welder/rods/csharp/marks.hpp> defines
[[=welder::rods::csharp::family_surface]] (a bare tag; no language mask —
it IS this rod's mark) plus the family_surface_marked(type) query, and the
class opener reads it directly. The welder pin returns to main.

A consumer whose welded headers must also parse WITHOUT this rod (a
Python-only build where welder-csharp is not fetched) spells the mark
behind a build-driven macro that expands to nothing when the rod is absent
— documented in marks.hpp; gcc-16 collects annotations only from the
DEFINING declaration, so the macro must be consistent across every TU of
one build tree.

Same tests, same results: 42 managed (marked bases synthesize, the
unmarked family's hoistable intersection synthesizes nothing), goldens
untouched, 7/7 ctest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@skarndev
skarndev merged commit 1e93ecc into main Aug 21, 2026
3 checks passed
@skarndev
skarndev deleted the feature/family-surface branch August 21, 2026 15:01
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