frostney / frostney/GocciaScript

Unify built-in and user-defined classes via first-class TGocciaIntrinsic with RTTI

Open
#410 1 comment 0 reactions 0 assignees View on GitHub

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 TGocciaSymbolValue for primitives
  • FPrototype chain walks for built-in objects
  • FClass field 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
  • Build is pure: construct the prototype, attach methods, set up the method host, prepare the instance shape. No realm interaction. Safe to fail / re-run.
  • Publish is 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 {} → instantiates TGocciaIntrinsic with Kind = gikUserDefined, RTTI allocated on the fly.
  • Built-in Performance / TextEncoder / Array → instantiates TGocciaIntrinsic with Kind = 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:

  1. Spike on Performance and TextEncoder — simplest built-ins, both currently exhibit the publish-before-register pattern. Validates the abstraction without quirks.
  2. Migrate the rest of the object built-ins — URL, TextDecoder, etc.
  3. Tackle primitives with boxing — Number, String, Boolean.
  4. Tackle quirky primitives — Symbol, BigInt.
  5. Migrate Array last — internal storage interaction with the instance shape needs design work.
  6. Fold TGocciaClassValue into TGocciaIntrinsic once 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 Build and confirms no published-but-empty prototype is observable.

Open questions

  • Naming: TGocciaIntrinsic is fine for built-ins but slightly awkward for user-defined classes. TGocciaClassDescriptor? TGocciaClassShape?
  • Should FInstanceShape be shared per-intrinsic or per-instance? Likely per-intrinsic with copy-on-write for user classes that mutate prototype.
  • RTTI allocation strategy for user classes: monotonic counter is simplest; needs to survive realm teardown without leaking.
  • Interaction with TGocciaSharedPrototype lifetime (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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.