0.4.11
Highlights
- Faster programs:
- A read through an untyped value of a typed record or array no longer copies it: three React render loops execute 31% to 57% fewer instructions per render (294,339 to 197,439, 6,765,442 to 4,653,659 and 6,648,657 to 2,880,208 by callgrind), and the
untyped-record-probesworkload runs 72.8% faster, about 0.7 s instead of 0.18 s against 0.13 s in Node. Of 74 run-time workloads, 73 are neutral, one is faster and none is slower. - A write by a name known only at run time (
v[k] = x) costs 134 instructions instead of 300 when the receiver is a class instance, 379 instead of 534 for a typed record, 349 instead of 528 for a parsed object and 319 instead of 501 for an object literal;k in vcosts 112 instead of 278 for a class instance, an element read 109 instead of 174, atypeoftest 87 instead of 125, and an object spread of three members 1,324 and 3,160 instead of 9,364. Executables that link the changed functions grow by 256 to 512 bytes. - A typed array read through an untyped value reads the element from the array itself:
v[i]costs 508 instructions an item instead of 1,189 for an array of class instances and 346 instead of 1,026 for an array of numbers, with identical compiled code. - A property copied from a layout-mode object onto an array view takes the ordinary writable, enumerable, configurable write instead of the descriptor path: 300,000 Array subclass instances with three fields, each read once through a view, take 206 ms instead of 559 ms, against 18 ms in Node.
- A type that carries the collector header only for the weak analysis is emitted without cycle-capable children, so it nominates nothing to the collector: a program of 200 plain records a round filed in a
WeakMap<object, …>over 25 rounds costs 242,083 instructions a round instead of 349,827, against 217,117 before the weak analysis needed the header. - An
Object.freeze,Object.sealorObject.preventExtensionscall that can never run, underif (false), aconstthat isfalseor a comparison--definedecides, no longer sets the program-wide integrity flag: a loop of five field stores and one object goes from 156 to 97 instructions an iteration, and its executable from 382,520 to 188,864 bytes. - A
switchon the code unit inside a scanning loop and aforwithout an initializer take the UTF-16 units copy, at 7.7 instructions a character for non-ASCII text instead of 26.7 and 8.0 instead of 22.0 for a counter declared before thefor, and a chain of fivereplaceAllcalls with one-character literals becomes one scan at 410 instructions instead of 637. - A question about one member of a Node stream held in an untyped value no longer builds the stream's member view when the name is absent or the question is
inorReflect.has:typeof h.flushon aWritablecosts 541 instructions instead of 33,952, and asked twice 867 instead of 73,587, with every answer unchanged. - The own-key questions a serializer asks about a value held under a closed record type, whether its prototype is
Object.prototype, how many own keys it has and whether a key is a given name, are answered from the record's static layout and one runtime fact: 44 instructions a question instead of 985 for one member withObject.keys, 41 instead of 4,777 withReflect.ownKeysandObject.keys(r).length, and 78 instead of 20,205 for 11 members. A record that kept a source object, a linked copy or a live capsule falls back to building the key array. - A write to an untyped record by a name known only at run time costs 100 instructions instead of 361, against 129 for the same write with the name in the source. A program with no such write is identical to its previous build.
- A field append (
this.text += s,obj.text += s, a record field) is linear instead of quadratic: at 10,000 characters an append costs 126 instructions instead of 2,225, and at 1,000,000 characters it costs 125 where the previous build does not finish. A localtext += piececosts about 80 instructions instead of 146. - The first
.lengthof a finished JSON document scans it 32 bytes a step and marks it ASCII instead of walking it through the index, at 155 additional instructions instead of 563. parts.join(sep)on astring[]costs 21.5 instructions an element instead of 53.0 with an empty separator and 22.5 instead of 63.0 with a comma, andArray.from<string>({ length: 2000 })14.8 instead of 33.0. The join is a typed runtime entry of its own: the 1,745 programs that join a string array grow by 720 to 856 bytes, and the 3,138 programs that never call it shrink by 640 to 704 bytes.- A
string | null,string | undefinedor three-state union keyed in aMapor held in aSetstays the string pointer, so one such collection no longer boxes the union in every position of the program: astring | nullfield makes 0 union boxes per 1,000 iterations instead of 750,Set.hascosts 91 instructions instead of 226,Map.get90 instead of 251,Set.add118 instead of 335 andSet.delete112 instead of 286. - A chain of
setImmediatecallbacks no longer grows the immediate queue: 200,000 turns of a chain peak at 1,660 KB of maximum resident size instead of 6,524 KB, and a turn that queues nine immediates peaks at 1,644 KB instead of 48,508 KB.
- A read through an untyped value of a typed record or array no longer copies it: three React render loops execute 31% to 57% fewer instructions per render (294,339 to 197,439, 6,765,442 to 4,653,659 and 6,648,657 to 2,880,208 by callgrind), and the
- Faster builds:
- A statement that holds many values across steps that can throw compiles in linear time instead of quadratic release calls: a dynamic object literal of 494 members builds in 15.9 s instead of 52 minutes 43 s, and generated tables of 100, 200 and 400 strings take 0.17 s, 0.28 s and 0.67 s of native code generation instead of 6.6 s, 60.4 s and 535 s.
- The TypeScript 7.1 symbol table keeps one parsed descriptor per source file instead of one per symbol: the compiler's own build holds 3,404 MB of live heap at 137 bytes per source byte instead of 3,572 MB at 144, and among the 1,443,719 references that carry a descriptor for 889 files no two descriptors for one node ID differ.
- Every uncompiled
import()in one statement is decided in one island round, so 29 programs take fewer lowerings (142 to 102 in all) and a large program's build time grows 1.0% instead of about 49%.
- Node.js parity:
writeFileSync,appendFileSync, thefs/promiseswrites, the descriptor form andFileHandle.writeFileandappendFileaccept anyArrayBufferView, including a view over WebAssembly memory, a subarray at an offset, aDataViewand a typed array of any element, and write the bytes it covers; other data throws Node'sERR_INVALID_ARG_TYPEwith the argument checks in Node's order. 200,000 descriptor-form writes of a 4 KiB view take 25.5 ms against 74.5 ms in Node.new TextEncoder(label)evaluates and ignores its arguments as Node does, including spread arguments, sonew TextEncoder("utf-8")compiles, and construction runs 1.63x faster than Node's (373 ms against 610 ms for 20 million encoder and decoder pairs).Buffer.alloc(size, fill, encoding)andbuf.fill(value, offset, end, encoding)with arguments that have no static type are filled by the kinds the values have at run time instead of being refused withSC2020.- Terminal members of a stream (
isTTY,columns,rows,isRaw) read through an optional chain, off an alias ofprocess.stdinor bound by a pattern answer the declared type plusundefinedon a terminal, a pipe, a file, the null device and a closed descriptor: 501 of 619 cells match Node instead of 283, and the 34 cells that differed now match. getColorDepth()andhasColors()on a process stream answer by Node's rules on a terminal and on a pipe: 115 of 132 rows match Node instead of 5.- A builtin global read by name off a stored global object (
const root = globalThis; root.Headers) reads the value the bare name reads (2,687 of 3,480 cells match Node instead of 1,055), and a program that asks the global object about itself gets Node's answers. process.hrtimereadsmach_continuous_timescaled by the time base on macOS, the clock Node reads, so a Node island's load-time reading and the program's own reading share one origin.
- TypeScript parity:
instanceofanswers for every kind of untyped value instead of throwing: aUint8Arrayor aURLtested against a program class answersfalsewhere the previous build reportedSC2020, andv instanceof Errorinside a chain of class tests answers instead of reportingSC1090. Over 1,294 cells of four corpus programs, 181 cells differed from Node on the previous build, every one a throw, and none differs now.- The
argumentsobject answers as Node does forArray.isArray,Object.prototype.toString.call,arguments instanceof ArrayandJSON.stringify, and the builtinconcatover it is refused by name: over a family of 147 cells, 114 match Node instead of 75. Object.assigncopies in its source's key order and aconstbound to its result is its target,Object.getPrototypeOfof an instance held as its base class answers the instance's own class, and a record type whose objects create their keys in orders the compiler does not follow per object is reported once as the warningSC6014, which--no-key-order-warningleaves out.- A generic function over an interface held as an untyped value gets one instance per set of type arguments (292 of 292 cells match Node instead of 186, where the previous build threw
TypeError: expected number at $, got string). - A generator, async or async generator function reports
GeneratorFunction,AsyncFunctionorAsyncGeneratorFunction, and a handled rejection is not reported when the same promise also crossed into an untyped position: 150 of 336 cells that ended in a build refusal or a run-time error become matches. - A function's body lowers when a destructured parameter's binding has no static type, and a
Promise.allposition whose type has no static form is checked-dynamic when its entry is: 464 of 468 generated programs match Node instead of 428, and 438 of 641 instead of 432. String,Number,BooleanandBigIntwritten as values are one function per constructor, so identity holds across TypeScript and JavaScript: 2,433 of 2,697 generated programs match Node instead of 1,653. A statement that now throws where the build previously refused the whole function carries the warningSC6015.- A read of a generic instance's binding keeps the union its slot holds (105 of 120 generated programs match Node instead of 46), and
Reflect.applyandReflect.getPrototypeOfused as values are the real functions with Node's order of checks and its errors instead of a stand-in that threw when called. - A JavaScript declaration initialized by a chain of assignments (
var x = exports.x = []) is classified from the value the chain yields (50 of 58 cells instead of 44), and an optional chain's tail used under!==,??,||or a condition short-circuits as a whole, where 40 cells differed from Node before and none do now. - A fresh object or array literal as the right operand of
&&,||or??answers at the type it was built:c && { a: null, b: null }typedPair | nullprints its two keys instead of throwing a representabilityTypeError, and pairs, chains, right-nested pairs three deep and a choice between two literals as the operand build too. - A JavaScript
varorletdeclarator whose initializer isvoidof a literal, the spelling a transpiler writes forlet val;, is typed where the value enters: 9 of 12 probe programs match Node instead of 6, and a retry wrapper in an untyped npm package that was refused when it ran now equals Node. A diagnostic that shows such a line reports the column and the text as the file is written. - TypeScript 7.1 is supported:
typescriptis pinned to 7.1.0-beta and tsc-ts to 0.1.3, the AST kinds, enums, guards and protocol constants are generated from 7.1, a 7.0 server's responses convert to 7.1's numbering, and the recorded preflight and order baselines equal 7.1's answers.
- CommonJS and islands:
- CommonJS modules that re-export another module's export table through a binding (
var lib; lib = require("./lib.cjs"); module.exports = lib), including chains of such wrappers, compile: member reads, calls,typeof, destructuring and ES default imports resolve to the original export table, eachrequirestill initializes its own wrapper at its source position, and a getter runs once at the destructuring in pattern order. Calls of an export through a re-exported table compile when the export is proven not to readthis, and a re-exported table used as a value, a wrapper whose own member exports change the table and wrappers that re-export each other stay refused by name. - A
varorletrequireof a module that runs in a Node island is the module at every use when the binding is never written again. - A
varexport of a module shared with a Node island is read only after its declaration ran, and before that the accessor answersundefinedinstead of reading uninitialized storage, where the previous build exited 139. - A package that exports its class through an
export { … }list is shared with the island instead of duplicated, soinstanceofacross the boundary answerstruewhere the previous build printedfalse. - A module probed after one that evaluates at its import has no snapshot, so a snapshot that was never read cannot carry what the earlier load left behind, such as a private environment variable's value, and after a
"lazy"load that read the machine later snapshots keep no constants. - A data member declared by a types-only package interface is read through a copy the program made of a value that came out of a Node island (
SC2013before), while members of a class, or of an interface merged with a value declaration, stay refused. (await import("./m")).fevaluates the import before it reads the member: 22 of 27 probe forms match Node and 5 are refused by name, against 5 matching, 4 refused and 18 differing on the previous build.- The shipped package shims are compiled under any install, including scriptc inside a project's
node_modules, by asking whether a path is an installed package's file rather than whether it contains anode_modulessegment, so undici, fs-extra, chalk and the other shimmed packages compile where they were refused, and the IR is the same inside and outsidenode_modules. - The native compiler builds programs with Node islands: the island build steps run from one shared step in the Node-hosted driver and the native one, so a natively built program records its pack and carries its module manifest, and the native frontend cache keeps the pack identity.
- CommonJS modules that re-export another module's export table through a binding (
- Fixes:
- A
WeakMaporWeakSetwhose values hold the last reference to a key no longer crashes the collector while it is freed: six corpus programs that exited 139 as a release build and reported a heap use after free as a sanitized build equal Node, and every executable is 296 to 448 bytes smaller. - Immediates that are all cleared in a turn no longer leak when a timer drives the turns: 6,544 live bytes after 4,000 turns instead of 6,299,088, one turn that queues and clears 100,000 immediates takes 3 ms instead of 2,871 ms, and the next two such turns 3 ms each instead of 11,620 ms and 20,283 ms.
removeAllListenersunlinks listeners before releasing them, and owners such as records, classes, unions, arrays andMaps that hold a stream class or anEventEmittersubclass are traced, so a program that removes listeners no longer exits 139 or reports a use after free.- An arena no longer drops objects that stay reachable through a virtual call's result, a generator resume, a library call that can run program code (
emitrunning a listener, a Promise executor, aString.replacereplacer) or a cycle a callback makes, so programs that exited 139 or printed other values equal Node. - A promise that has passed through an untyped value is found again as the key of a
Mapor the member of aSet: 35 of 35 forms find it at a collection size of 1, where the previous build found it in 1 form at a size of 34, andindexOfandincludesof aPromise<T>[]find it instead of answering-1andfalse. - An instance of a subclass that enters a union through an ancestor's arm and then passes
instanceofof its own class converts to that class instead of throwingUncaught TypeError: expected null | TNode at $, got object: over five programs and 184 cells, the 54 cells that differed from Node now all match. - An untyped plain object or array handed to a typed record or array slot stays one object under both names, so identity checks and later writes through either name agree: a family of 720 generated cells matches Node in 515 cases instead of 234, with none of the previous matches lost. The conversion adopts all of a value or none of it, and a source that becomes the typed record throws at a write the type refuses.
- In a JavaScript class,
this.#f(x)where#fis a private field that holds a function is called as the field's value off the receiver after the private name's brand check instead of as a call by the name"#f": 211 of 253 lines from 16 neutral programs equal Node, against 92 on the previous build, and the message of a failed call names a private callee as V8 does. - An error seen through an interface that extends
Errorkeeps its identity:failure.stack,failure instanceof Errorandfailure === eanswer as Node does, a write through the interface reaches the error, andJSON.stringify,Object.keysandutil.inspectlist the error's own keys. Of 2,430 cells of a generated family, 2,112 match Node instead of 1,818. - A value typed by such an interface that meets a slot typed by an
Errorclass is converted by a checked cast, with the warningSC6004where the build previously refused it; a record parameter reads the error's checked view member by member, and acauseread by name from an untyped reference to anErrorsubclass instance answers where it readundefined. Astackaccessor that anErrorsubclass declares answers a by-name read only in a program that can delete the error's ownstack. - The proof that a factory-made export ignores
thisfollowsObject.definePropertyonly when its key is a string literal other thanapply,calland__proto__, followsObject.definePropertiesandObject.assignonly when every source is an object literal whose properties all have plain names other than those three, and no longer followsObject.setPrototypeOf, so a module export that gives its export its owncallbefore calling it through the module binding is refused again. fsp.writeFileData,fsp.appendFileData,fsp.writeFileDataFlagsandfileHandle.writeFileDataare recorded as calls that can throw, so the programs that call them carry the check that delivers their error, and the proof of a factory-made export follows a namespace import's member, a rest parameter and a parameter the call passes nothing for, with the depth limit raised from 6 to 16, so three published exports that were refused compile again.- An untyped
MaporSethanded to a typed slot takes the typed layout in place instead of throwingTypeError: expected map<string,f64> at $[0], got Map. - Three compiler crashes are fixed: a chained JavaScript assignment's unit value (
domain.active = process.domain = null) compiles and matches Node,skipLibCheckis forced for every program so a dependency's declarations that name a type the bundled declarations do not spell cannot fail that package's island build, andSC3004reports the LLVM helper's structured diagnostic or how it ended instead of a bareCommand failed:line.
- A