Skip to content

refactor(codemode): move built-ins onto real prototypes with native function objects - #48608

Merged
rekram1-node merged 1 commit into
v2from
prototype-methods-pr
Sep 12, 2026
Merged

refactor(codemode): move built-ins onto real prototypes with native function objects#48608
rekram1-node merged 1 commit into
v2from
prototype-methods-pr

Conversation

@rekram1-node

Copy link
Copy Markdown
Collaborator

Fourth step of the value-model rewrite (after #48527, #48541, #48559): built-in methods live on real prototype objects, built-in functions are program objects, and member access is one ordinary property lookup for every value. Internals only: no new syntax, no this in program functions, no classes.

Before / after

before                                             after

arr.map(fn)                                        arr.map(fn)
 └─ IntrinsicReference(arr, "map")                  └─ get(arr, "map")
     └─ methods.ts switch on receiver type              └─ not own → arr.proto (Array.prototype)
        "array" + "map" → run                               └─ NativeFunction, call(this = arr, args)
 ProgramObject                proto · props: Map<key, Slot>
-├── ProgramArray             (proto: null)
+├── ProgramArray             (proto: Array.prototype)
 ├── ProgramError
-└── ProgramFunction          name · length as class fields
+├── ProgramDate · ProgramRegExp · ProgramMap · ProgramSet · ProgramURL · ProgramURLSearchParams
+├── ProgramPromise · ProgramGenerator
+└── Callable                 name · length as own read-only properties
+    ├── ProgramFunction
+    └── NativeFunction       call(thisValue, args, node) · construct?(args, newTarget, node)
-HostFunction / HostNamespace  (host-side, invisible to the object model)
-Values.Date / Map / Set / …   (host-side wrappers)
-IntrinsicReference · ComputedValue · GeneratorMethodReference · PromiseInstanceMethodReference

Slot is { value, writable, enumerable, configurable } or { get, set, enumerable, configurable }. Built-in methods are installed hidden (non-enumerable), Math.PI and C.prototype are frozen, function name/length are readonly. [[Set]] and delete respect them, so Math.PI = 3 and delete arr.length throw as in strict mode — today they were silently ignored or returned false.

Realm

Every prototype is allocated once per run, empty, in dependency order; the globals then populate them and attach constructors. Nothing prototype-related is module-level anymore, so two programs can never see each other's Number.x = ….

createPrototypes()                                   globals(host)
  Object          ProgramObject(null)                  Object       constructor(protos, protos.Object, …) + methods(protos.Object, [hasOwnProperty, toString, …])
  Function        NativeFunction(Object)  ← callable   Function     (throws; exists so fn.constructor === Function)
  Array           ProgramArray(Object)    ← an array   Array        methods(protos.Array, [push, map, …])
  String/Number/Boolean                                String …
  Error → TypeError, RangeError, …                     Error …      Error.prototype.toString
  Date, RegExp, Map, Set, URL, URLSearchParams, Promise
  Iterator → Generator, AsyncIterator → AsyncGenerator Generator    next/return/throw, [Symbol.iterator]

Runner.prototypes replaces Runner.intrinsics; toProgram/fromData take the prototypes so no object is ever allocated without one (ProgramObject's constructor requires a proto).

this

A member callee keeps its base object as the receiver (EvaluateCall); everything else calls with undefined. Natives check their receiver, so a detached built-in method behaves like JS:

 ["a","z"].filter("abc".includes)
-// ["a"]        — IntrinsicReference carried the string
+// TypeError: String.prototype.includes called on null or undefined

Accessors on prototypes cover what used to be special-cased in member access: map.size, regex.flags/.global/…, url.href (with setters), url.searchParams. regex.lastIndex is an own non-configurable data property.

new and instanceof

-callee.construct(args, node)                         # one closure per built-in, prototype baked in
+callee.construct(args, newTarget, node)              # instance proto = prototypeFrom(newTarget, fallback)
-rhs.instanceOf(lhs)                                  # per-constructor hook (Object: () => true, Boolean: () => false)
+hasPrototype(lhs, get(rhs, "prototype"))             # OrdinaryHasInstance

What moved

src/interpreter/
├── objects.ts      Slot/Attributes, Callable, NativeFunction, Program{Date,RegExp,Map,Set,URL,URLSearchParams,Promise,Generator}, own/define/defineAccessor/keys/entries
├── native.ts       new: fn, methods, constants, constructor, prototypeFrom, receiver, requiresNew   (replaces host.ts)
├── intrinsics.ts   Prototypes table, createPrototypes() in bootstrap order
├── generators.ts   new: Generator/AsyncGenerator prototype methods
├── runtime.ts      call site supplies this; getMemberReference is ~40 lines of ordinary lookup; instanceof via prototype
├── runner.ts       invokeCallable(callable, thisValue, args, node); generic ToPrimitive via valueOf/toString lookup
├── promises.ts     ProgramPromise; then/catch/finally on Promise.prototype
├── errors.ts       error constructors via constructor(); Error.prototype.toString
├── references.ts   typeof/isRuntimeReference over Callable and the subclasses
-├── host.ts         HostFunction, HostNamespace
-├── methods.ts      895-line dispatch switch
src/stdlib/*.ts     each xGlobal(runner) builds the constructor and populates runner.prototypes.X
src/data.ts         toProgram(protos, …) / fromData(protos, …)
-src/values.ts       host-facing Values.* wrappers (hosts pass plain Date/Map/…; the boundary wraps)
test/test262/       manifest drops the ".prototype" boundary (+291 files); harness built from native.ts

Program-visible changes

All already listed as [ ] gaps in interpreter-support.md, now checked:

before after
({}).hasOwnProperty("a"), ({}).toString() not a function works; "[object Object]"
Array.prototype, Object.prototype.hasOwnProperty === ({}).hasOwnProperty undefined real objects, true
Math.max.length, Array.prototype.push.name undefined 2, "push"
Object.keys(new Error("m")) ["message"] [] (message non-enumerable, as in JS)
const { slice } = [1]; slice(0) worked (bound) TypeError, as in JS
Math.PI = 3, delete arr.length ignored / false TypeError, as in strict mode
Object.groupBy(...) plain object null-prototype object, as in JS
Promise.withResolvers() "Promise.withResolvers is not available. Available: …" "Promise.withResolvers is not a function"
x instanceof Fn for any function with a prototype rejected works

Results

suite before after
unit + test262 (this branch) 4949 pass / 248 skip 5062 pass / 426 skip on 291 more files; 0 new failures on the previously covered 3993
wider local baseline (11 044 files) 1119 skip 1335 skip on 733 more files; 0 new failures on the previous 10 311

Tests updated to the JS behaviors above; new assertions cover prototype identity, name/length, non-enumerability, and per-run confinement of prototype mutation (Object.prototype.polluted = 1 in one program is invisible to the next).

@rekram1-node
rekram1-node merged commit 6add966 into v2 Sep 12, 2026
10 checks passed
@rekram1-node
rekram1-node deleted the prototype-methods-pr branch September 12, 2026 07:19
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