Repository navigation
Answer instanceof for every kind of untyped value - #3491
Conversation
`value instanceof C`, with C a class of the program that has a prototype, tests the class keys of a capsule and then walks the value's prototype chain. The walk's prototype read refused every value whose chain the runtime does not model: a typed array, an ArrayBuffer, a DataView, a Buffer, a URL, an iterator, a stream, a generator object, a Proxy, and an instance of a generic class below a class with a prototype. Node answers false for all of them; the compiled program threw `Object.getPrototypeOf on this value is not supported yet`. `scr_dyn_get_prototype_for_identity`, the read of that walk, now has an answer for every value. An object, an array, a function, a record's or an array's capsule and an island object keep their chain; a Proxy has its target's (a `getPrototypeOf` trap is refused by name, like the other traps the runtime does not run); every other value ends the walk with null, because only builtin prototypes are on its chain, or because it is a class instance's capsule, which the class test answers by key before it walks. The untyped `instanceof` already follows this rule. `value instanceof Error` builds the view of a class instance to look for the error marker. For a class without prototype data of its own below a class that has it (a generic class with a base), the helper that names the view's prototype answered Object.prototype, which no view can be given: building the view threw `Object.prototype reflection and mutation are not supported yet`, for a value that is no Error. Such a class now has a prototype made from its nearest ancestor's prototype data and named like the class, so the view is built, inherits what the ancestors' prototypes hold, and prints the class's name. The runtime change is one function that only a program with such a class test links; the lowering change reaches only a program with such a class. The family (four corpus programs, differential against Node): 20 targets asked of 46 kinds of value held as `unknown`, the value narrowed to `object`, thrown and caught, the class taken from a variable, objects with a chosen prototype, a Proxy, streams. Co-Authored-By: Claude Code <noreply@anthropic.com>
The read of a class test's walk lived in scr_json.c, which nearly every program links. Its new strings renumbered the unit's later statics, so 4,057 executables changed their symbol names for a read only class tests of untyped values reach. The read is now scr_dyn_class_test_prototype in scr_dyn_class_test.c, a unit selected by the runtime feature classTestPrototype (the libCall dyn.classTestPrototype, which replaces dyn.getPrototypeForIdentity). scr_json.c is main's again without the old read. A program with such a test changes in its compiled code, by the name of the call; every other program keeps its bytes. The chosen-prototypes program gains the cells where Node answers true: objects, arrays and capsules whose chain the program set to a class's prototype, and a Proxy of one. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The full round failed one test in both lanes: the committed runtime profile names scr_dyn_get_prototype_for_identity, and the function was deleted. Deleting it would need a profile refresh, which changes how every speed unit is optimized. scr_json.c is main's file again, byte for byte. The class test's read in scr_dyn_class_test.c decides which values have a chain a program class's prototype can be on and hands those to scr_dyn_get_prototype_for_identity, which reads it as before; every other value still ends the walk. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
CI on this PR's head (run 42653656935): 83 checks passed, every Linux job among them. Two macOS jobs are red (and the aggregate
Main's run of this PR's merge commit 🤖 Generated with Claude Code |
The faults
instanceofthrows for a value that is not an instance. Node throws only when the right-hand side is not callable or has no object as its prototype; for every other question it answers.Node prints
true,false,false. Compiled by main (df9e6918f6) the program builds with no diagnostic, printstrueand ends withUncaught Error: Object.getPrototypeOf on this value is not supported yet [SC2020], exit status 1.Node prints
box. Compiled by main it ends withUncaught TypeError: scriptc: Object.prototype reflection and mutation are not supported yet [SC1090], exit status 1: the throw isv instanceof Error.The causes
value instanceof C, for a classCof the program that has a prototype (a class that is not generic), first tests the class keys of a capsule and then walks the value's prototype chain, for objects that got the class's prototype without being constructed by it. The walk's prototype read,scr_dyn_get_prototype_for_identity, answered for an object, an array, a promise and six native kinds, and passed every other value toObject.getPrototypeOf, which refuses a value whose prototype the runtime does not model.value instanceof Errorbuilds the view of a class instance to look for the error marker. The view gets the prototype the class's helper names, and for a class without prototype data of its own (a generic class below a class that has it) the helper answeredObject.prototype, which no view can be given: attaching it is the refusal ofObject.prototypereflection.The fix
One rule each, where the question is answered.
packages/runtime/src/scr_dyn_class_test.c, a unit of its own): the read of a class test's walk,scr_dyn_class_test_prototype, has an answer for every value. An object, an array, a function, a record's or an array's capsule and an island object keep their chain. A Proxy has its target's prototype; agetPrototypeOftrap is refused by name, like the other traps the runtime does not run. Every other value ends the walk with null, so the test answersfalse: the value has only builtin prototypes on its chain (typed-array bytes, an ArrayBuffer, a DataView, a URL, an iterator, a stream, a generator object), or it is a class instance's capsule, which the class test answers by key before it walks. This is the rule the untypedinstanceof(scr_bytes_instanceof_at) already follows: it walks objects, arrays and functions and answersfalsefor the rest.class-dynamic-dispatch.ts, the helper that names the prototype of an instance's view): a class without prototype data of its own below a class that has it gets a prototype made from its nearest ancestor's prototype data and named like the class, one object per class, created on first use. The view is built, inherits what the ancestors' prototypes hold, andutil.inspectprints the class's name as Node does (GNode { form: null }); on main every operation that builds such a view threw.The old read stays.
scr_dyn_get_prototype_for_identityand the whole ofscr_json.care main's byte for byte; the new read decides which values have a chain a program class's prototype can be on and hands exactly those to the old one, so the two are the halves of one path, not two mechanisms. The walk now calls the libCalldyn.classTestPrototype(the renameddyn.getPrototypeForIdentity), and the unit is selected by the runtime featureclassTestPrototype, asscr_dyn_reflect_proto.candscr_dyn_integrity.care.Two forms of this fix were measured and dropped, which is why it has this shape:
scr_json.c: that unit is linked by nearly every program, and the four new strings renumbered its later statics, so 4,057 executables changed their symbol names (and nothing else) for a read only class tests reach.packages/runtime-pack-common/profiles) names the old function, sothe committed runtime profile names only runtime functionsfailed, and refreshing the profile changes how every speed unit is optimized. With the old read left in place the profile is untouched.instanceofstill answerstrueexactly for an instance: no test that answered on main answers differently (the 920 cells of the first program below include everytruecell of ten program classes). "Not an instance" is never a wrongfalse: a value the walk ends on cannot have a program class's prototype on its chain, becauseObject.setPrototypeOfrefuses it by name (a Map, a Date, a typed array, a typed collection), a Proxy over a class instance is refused when it is made, and a class that extends a builtin either makes instances of its own (Date, Error, EventEmitter, the stream classes), which the key test answers, or is a build refusal. The values that do get a class's prototype at run time keep their chain, and the third program below asks them.The family
Four corpus programs, differential against Node, one line per cell; a cell that throws prints the message.
instanceof-untyped-every-kindError,TypeError,Array,Map,Set,Promise,Date,RegExp,Uint8Array; program classes: plain, with a base, generic, below a generic class, abstract, implementing an interface, extendingError) asked of 46 values held asunknown: primitives, null, undefined, plain and null-prototype objects, an array, typed arrays, an ArrayBuffer, a DataView, a Map, a Set, a WeakMap, a WeakSet, a Date, a RegExp, errors, a promise, functions, a class value, generators, iterators, a URL, URLSearchParams, a TextEncoder, a Buffer, an instance of every program classError× those 2 instancesinstanceof-untyped-formsobjectfirst, thrown and caught, the class taken from a variable, the value held as a unioninstanceof-untyped-chosen-prototypestrueunder Nodeinstanceof-untyped-streamsIn all: 1,294 cells; main differs from Node in 181, every one a throw, and this branch in 0. The cells were counted on main
df9e6918f6; mainb4e908ef00, the one this branch holds, still throws in all four programs.The built-in targets and the generic program classes answer right on main for every value: a generic class has no prototype to walk to, so its test is the key test alone.
Not in this PR
Other faults the family found, all wrong answers and not throws, each with its own cause, each the same on main and on this branch; the corpus programs leave their cells out:
v instanceof KwithK: typeof Baseholding the subclassLeafanswers asv instanceof Base(truefor aBase; Nodefalse): the helper for a class taken from a variable uses the class value only for a function-local class.instanceof Arrayanswers by the kind of the value, not by its chain:Object.create([1, 2]) instanceof Arrayanswersfalse(Nodetrue), and an array set to another prototype still answerstrue(Nodefalse).falsefor the class (Nodetrue).Object.setPrototypeOfof a class instance held untyped returns without effect, and of a typed record, array, tuple or function it acts on a copy.Build refusals the family met, unchanged:
instanceofon a union of class instances and values without a class prototype test (a typed array or a Map beside a class),Object.create(Base.prototype)(class prototype reflection), a Proxy over a class instance or an array.Cost
A program without a class test of an untyped value takes nothing from this change and is byte-identical. A program with one calls the new read where it called the old one and links the new unit: about 0.4 to 0.8 KB.
ir: changed %dyn.class.prototypeChainIdentity,exe: added scr_dyn_class_test_prototype): 93 existing corpus programs, the four this PR adds, the benchmarksthrow-chainsanddynamic-class-tests, thellvm-emitterfixture and the compiler itself. The existing ones grow by 424 to 848 bytes, +0.01 to +0.20 percent; the four new programs by +0.08 to +0.23 percent (compared with main compiling the same source).b4e908ef00and this branch, each built from its own sources): of the 1,563 files of main's pack 1,562 are byte-identical in the branch's pack and the one that differs is.source-hash, the recorded hash of the runtime sources; the pack gainsscr_dyn_class_test.oin the release, dev and speed flavors (2.9 to 4.1 KB each).001-hello.ts,101-arithmetic.ts,1000-json-stringify-basics.tsand1020-async-basics.tscompile to the same bytes.Proof
One full validation round of the pushed head (
32f2b97554) against the main it holds (b4e908ef00), 52.3 minutes, verdict PASS:Also on the pushed head: unit tests 50 of 50 (
json.test.ts, which links the new unit into the C test,dyn.test.ts,classes.test.tsand two more files), lint 0 warnings and 0 errors, format clean,pnpm test:ts7 --baselines-onlycomplete, the four new programs pass in both lanes, the four programs equal Node cell by cell, andthe committed runtime profile names only runtime functionspasses with the profile untouched.The pushed head holds main
b4e908ef00. Main is06ccf2a1e6(#3489) at the time of the push; git merges the two with no conflict (five files changed on both sides, each by lines added in different places: the registration of a runtime unit and a declaration inscr_dyn.h), and the merged tree has no caller of the old libCall name left. The merge with16ef6085c0(#3485, the commit that touched those five files), uncommitted, was built and checked too: the same unit tests, lint, format, the four programs in both lanes, the runtime-profile test and the family all pass; the 92 files main changed after it include none of this change's 33. The lanes of the merge with the newest main are this PR's CI.The 101 programs whose executable changes
🤖 Generated with Claude Code