github vercel-labs/scriptc v0.4.11

30 minutes ago

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-probes workload 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 v costs 112 instead of 278 for a class instance, an element read 109 instead of 174, a typeof test 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.seal or Object.preventExtensions call that can never run, under if (false), a const that is false or a comparison --define decides, 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 switch on the code unit inside a scanning loop and a for without 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 the for, and a chain of five replaceAll calls 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 in or Reflect.has: typeof h.flush on a Writable costs 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 with Object.keys, 41 instead of 4,777 with Reflect.ownKeys and Object.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 local text += piece costs about 80 instructions instead of 146.
    • The first .length of 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 a string[] costs 21.5 instructions an element instead of 53.0 with an empty separator and 22.5 instead of 63.0 with a comma, and Array.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 | undefined or three-state union keyed in a Map or held in a Set stays the string pointer, so one such collection no longer boxes the union in every position of the program: a string | null field makes 0 union boxes per 1,000 iterations instead of 750, Set.has costs 91 instructions instead of 226, Map.get 90 instead of 251, Set.add 118 instead of 335 and Set.delete 112 instead of 286.
    • A chain of setImmediate callbacks 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.
  • 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, the fs/promises writes, the descriptor form and FileHandle.writeFile and appendFile accept any ArrayBufferView, including a view over WebAssembly memory, a subarray at an offset, a DataView and a typed array of any element, and write the bytes it covers; other data throws Node's ERR_INVALID_ARG_TYPE with 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, so new 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) and buf.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 with SC2020.
    • Terminal members of a stream (isTTY, columns, rows, isRaw) read through an optional chain, off an alias of process.stdin or bound by a pattern answer the declared type plus undefined on 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() and hasColors() 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.hrtime reads mach_continuous_time scaled 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:
    • instanceof answers for every kind of untyped value instead of throwing: a Uint8Array or a URL tested against a program class answers false where the previous build reported SC2020, and v instanceof Error inside a chain of class tests answers instead of reporting SC1090. 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 arguments object answers as Node does for Array.isArray, Object.prototype.toString.call, arguments instanceof Array and JSON.stringify, and the builtin concat over it is refused by name: over a family of 147 cells, 114 match Node instead of 75.
    • Object.assign copies in its source's key order and a const bound to its result is its target, Object.getPrototypeOf of 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 warning SC6014, which --no-key-order-warning leaves 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, AsyncFunction or AsyncGeneratorFunction, 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.all position 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, Boolean and BigInt written 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 warning SC6015.
    • 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.apply and Reflect.getPrototypeOf used 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 } typed Pair | null prints its two keys instead of throwing a representability TypeError, and pairs, chains, right-nested pairs three deep and a choice between two literals as the operand build too.
    • A JavaScript var or let declarator whose initializer is void of a literal, the spelling a transpiler writes for let 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: typescript is 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, each require still 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 read this, 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 var or let require of a module that runs in a Node island is the module at every use when the binding is never written again.
    • A var export of a module shared with a Node island is read only after its declaration ran, and before that the accessor answers undefined instead 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, so instanceof across the boundary answers true where the previous build printed false.
    • 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 (SC2013 before), while members of a class, or of an interface merged with a value declaration, stay refused.
    • (await import("./m")).f evaluates 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 a node_modules segment, so undici, fs-extra, chalk and the other shimmed packages compile where they were refused, and the IR is the same inside and outside node_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.
  • Fixes:
    • A WeakMap or WeakSet whose 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.
    • removeAllListeners unlinks listeners before releasing them, and owners such as records, classes, unions, arrays and Maps that hold a stream class or an EventEmitter subclass 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 (emit running a listener, a Promise executor, a String.replace replacer) 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 Map or the member of a Set: 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, and indexOf and includes of a Promise<T>[] find it instead of answering -1 and false.
    • An instance of a subclass that enters a union through an ancestor's arm and then passes instanceof of its own class converts to that class instead of throwing Uncaught 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 #f is 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 Error keeps its identity: failure.stack, failure instanceof Error and failure === e answer as Node does, a write through the interface reaches the error, and JSON.stringify, Object.keys and util.inspect list 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 Error class is converted by a checked cast, with the warning SC6004 where the build previously refused it; a record parameter reads the error's checked view member by member, and a cause read by name from an untyped reference to an Error subclass instance answers where it read undefined. A stack accessor that an Error subclass declares answers a by-name read only in a program that can delete the error's own stack.
    • The proof that a factory-made export ignores this follows Object.defineProperty only when its key is a string literal other than apply, call and __proto__, follows Object.defineProperties and Object.assign only when every source is an object literal whose properties all have plain names other than those three, and no longer follows Object.setPrototypeOf, so a module export that gives its export its own call before calling it through the module binding is refused again.
    • fsp.writeFileData, fsp.appendFileData, fsp.writeFileDataFlags and fileHandle.writeFileData are 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 Map or Set handed to a typed slot takes the typed layout in place instead of throwing TypeError: 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, skipLibCheck is 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, and SC3004 reports the LLVM helper's structured diagnostic or how it ended instead of a bare Command failed: line.

Don't miss a new scriptc release

NewReleases is sending notifications on new releases.