frostney / frostney/GocciaScript
Unify built-in and user-defined classes via first-class TGocciaIntrinsic with RTTI
Nobody has claimed this yet.
- Dominant language
- Pascal
- Stars
- 20
- Forks
- 3
- Avg merge
- 3d 4h
- Merged PRs (30d)
- 45
Description
Motivation
Built-in classes (Array, Performance, TextEncoder, Symbol, BigInt, URL, …) and user-defined classes (class Foo {}) currently use parallel, divergent machinery. This causes recurring problems:
1. Publish-before-register ordering drift
Across ~12 shared-prototype sites, the install pattern is:
CurrentRealm.SetOwnedSlot(SLOT_FOO_PROTO, FFooPrototype); // published
RegisterMemberDefinitions(FFooPrototype, FooMethods); // populated AFTER
Between those two lines the slot is publicly reachable but missing methods. If anything resolves the prototype during member registration (re-entrant init, shared method-host setup, GC walk during allocation), it sees a half-built object. Today this is masked by initialization order, not prevented. Adding a new built-in means knowing this convention exists — there is no compile-time enforcement.
See PR #402 / commit 62c00ef7 for a recent bug where the includes sparse-path fix surfaced through a similar ordering subtlety.
2. Identity / RTTI is scattered
Type identification today branches across four mechanisms:
- Pascal
is TGocciaSymbolValuefor primitives FPrototypechain walks for built-in objectsFClassfield for user-defined classes- Ad-hoc tag checks (Array.isArray, etc.)
There is no single way to ask "what kind of value is this?" New code paths re-derive this each time, and the answer differs depending on whether the value originates from a built-in or from class X {} in user code.
3. Built-in vs user-defined classes are not unified
User-defined classes already have a coherent shape: constructor, prototype, method host, per-instance fields. Built-ins reproduce this ad hoc per unit. Adding a new built-in requires copying ~12 lines of slot wiring, prototype creation, method-host pinning, and member registration — and each copy can diverge.
Proposal
Introduce a first-class TGocciaIntrinsic (working name) that owns the full shape of any class — built-in or user-defined — and makes RTTI a first-class field rather than a derived property.
type
TGocciaIntrinsicKind = (gikBuiltin, gikUserDefined);
TGocciaInstanceShape = record
Slots: array of TGocciaSlotDescriptor;
StorageKind: TGocciaStorageKind; // dense array, dictionary, primitive box, etc.
InternalSlots: TGocciaInternalSlotSet;
end;
TGocciaIntrinsic = class
private
FKind: TGocciaIntrinsicKind;
FName: TGocciaString;
FConstructor: TGocciaObjectValue;
FPrototype: TGocciaObjectValue;
FMethodHost: TGocciaObjectValue;
FInstanceShape: TGocciaInstanceShape;
FRealm: TGocciaRealm;
FRTTI: TGocciaTypeId; // first-class identity
public
// Pure: assemble the shape, no realm publication, no GC pinning
procedure Build;
// Atomic: pin + register all slots in one step, fully populated
procedure Publish(ARealm: TGocciaRealm);
procedure Teardown(ARealm: TGocciaRealm);
// RTTI lookup is O(1), works uniformly for built-in + user classes
function Matches(AValue: TGocciaValue): Boolean;
end;
Build / Publish split
Buildis pure: construct the prototype, attach methods, set up the method host, prepare the instance shape. No realm interaction. Safe to fail / re-run.Publishis atomic from the realm's perspective: pins the constructor, prototype, and method host together; registers slots only after the object graph is fully populated.
This eliminates the ordering hazard by construction — there is no public state until the intrinsic is complete.
First-class RTTI
FRTTI: TGocciaTypeId (or similar) replaces ad-hoc identity checks:
- Primitives carry their type id directly.
- Objects carry it via their intrinsic.
- User-defined classes get a freshly-allocated id at class creation.
instanceof,Array.isArray,typeof, internal dispatch all read the same field.
Subclassing across the boundary becomes coherent: a user class extending Array gets the Array intrinsic's RTTI plus its own.
Unifying built-in and user-defined classes
Today TGocciaClassValue (user) and the per-builtin units describe the same thing two different ways. With TGocciaIntrinsic:
- User-defined
class X {}→ instantiatesTGocciaIntrinsicwithKind = gikUserDefined, RTTI allocated on the fly. - Built-in Performance / TextEncoder / Array → instantiates
TGocciaIntrinsicwithKind = gikBuiltin, RTTI from a fixed enum.
The instance creation path, prototype lookup, method dispatch, and teardown all go through one set of code. New built-ins become a declaration, not a copy of 12 lines of slot wiring.
Migration strategy
Big-bang is unrealistic — built-ins have real quirks:
- Symbol: primitive value with object-like methods
- BigInt: primitive with prototype, currently uses singleton method host
- Number/String/Boolean: primitive boxing
- Array: dense-vs-sparse internal storage
Suggested order:
- Spike on Performance and TextEncoder — simplest built-ins, both currently exhibit the publish-before-register pattern. Validates the abstraction without quirks.
- Migrate the rest of the object built-ins — URL, TextDecoder, etc.
- Tackle primitives with boxing — Number, String, Boolean.
- Tackle quirky primitives — Symbol, BigInt.
- Migrate Array last — internal storage interaction with the instance shape needs design work.
- Fold
TGocciaClassValueintoTGocciaIntrinsiconce both shapes are stable.
Each step should be a separate PR with a prototype-shape regression test (see below) gating the migration.
Testing strategy
- Prototype shape snapshot: capture the prototype's own-property names, descriptors, and method identities for each migrated built-in before and after. Assert exact equality.
- RTTI parity test: every existing identity check (
instanceof,Array.isArray, internal dispatch sites) must produce identical results before and after migration. - Realm teardown: existing TGocciaRealm tests should pass unchanged (per-engine isolation, see PR #403).
- Re-entrancy regression: add a test that triggers GC during
Buildand confirms no published-but-empty prototype is observable.
Open questions
- Naming:
TGocciaIntrinsicis fine for built-ins but slightly awkward for user-defined classes.TGocciaClassDescriptor?TGocciaClassShape? - Should
FInstanceShapebe shared per-intrinsic or per-instance? Likely per-intrinsic with copy-on-write for user classes that mutateprototype. - RTTI allocation strategy for user classes: monotonic counter is simplest; needs to survive realm teardown without leaking.
- Interaction with
TGocciaSharedPrototypelifetime (Pin/Unpin via PinObject + RememberPin) — likely subsumed but needs verification.
Related
- PR #402 — recent CodeRabbit findings touching this area (publish-before-register noted but declined as cross-cutting)
- PR #403 / commit d01aa9c6 — TGocciaRealm per-engine intrinsic isolation; this issue builds on that foundation
- Commit 62c00ef7 — sparse-path includes fix; representative of the kind of correctness work that gets harder when class machinery is duplicated
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading TGocciaClassValue and the built-in initialization paths for Performance and TextEncoder, especially their prototype publication and member registration. Review the existing TGocciaRealm teardown tests and prototype-shape testing strategy before defining the intrinsic boundary. Done means the smallest migrated built-ins have equivalent prototype behavior, atomic publication, RTTI parity, and passing regression tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- compilers
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 28/100