github vercel-labs/scriptc v0.4.13

33 minutes ago

0.4.13

Highlights

  • Faster programs:
    • @scriptc/experimental adds parallelInto, parallelReduce and parallelFor: the chunks of a loop run on a runtime thread pool when a syntactic check proves that no chunk writes a value another chunk reads or writes, the same source runs its chunks in order under Node.js, and a loop whose chunks cannot run in parallel compiles to the sequential loop it compiled to before, with an SC6106 warning and the same output.
    • new URL() answers an input that repeats the last parse from that parse's component strings, so a server that builds a Request from a URL and then parses request.url parses the URL once per request instead of three times, and every returned URL is still its own mutable value. A compiled HTTP server serves 1.20 to 1.23 times the requests per second of the same server on the previous build, over 11 paired samples per workload with 95% confidence intervals that exclude 1.0 in all three, and the IPv4 host parser and the port normalization write their digits directly instead of calling snprintf.
    • end() on a chunked response whose head and chunks are still corked sends the head, the chunks and the terminator, with any trailers, in one write, as Node does, where the previous build sent the terminator in a second write; the bytes on the wire do not change. Small responses from a server built on node:http and Response serve 15% to 16% more requests per second, and a larger framework app 3% to 8% on its text, JSON and POST routes, over 10 paired runs each with confidence intervals above zero; a response that already carried a Content-Length went out in one write and is unchanged.
    • A checked element read that the lowering stabilizes in its hidden local is lent to the call that projects it, which removes the retain and release pair that each iteration took: the two target workloads run 1.57 to 1.62 times faster, with both intervals excluding no difference, and a program that stores the read in a const first reaches the speed of the form without the copy (1.0327, an interval that includes no difference).
    • A class whose tree children are typed C | undefined, or C | null | undefined, takes the tree-class layout that the C | null spelling already used, which is the same pointer shape: a tree built with undefined children runs 5% to 8% faster, and the gap between the two spellings falls from 1.18 times to 1.05.
    • (x / c) | 0 for a proven-int32 x and a constant nonzero integer c divides with one sdiv i64 instead of a floating-point divide and a conversion, 1.43 times faster on the workload that divides in a loop, with a 95% interval excluding no difference; INT32_MIN / -1 keeps the conversion, because the 32-bit result wraps there.
  • Faster builds:
    • A build answered from the executable cache prints every warning the build reported, not only the performance summary, so a program with a type patch, a divergence or a start-up warning keeps the cached fast path, and a replayed warning uses the source text the cache entry kept, so the cached stderr matches the original build.
  • Node.js parity:
    • fs.openSync, fs.mkdirSync, fs.chmodSync and fs.fchmodSync read the modes Node's parseFileMode accepts, an octal string or an integer in 0..2^32-1, and an argument whose static type is any or a union runs Node's own validation ladder at run time instead of being refused by the build: openSync takes a string flag name or raw open(2) bits, mkdirSync answers the first directory a recursive walk created and accepts the mode as the options argument itself, and each check throws Node's error with its code and message. fs.open's callback form reads numeric flags the same way, and an omitted, null or undefined mode there opens with parseFileMode's 0o666 default. The options record of fs.writeFileSync and fs.appendFileSync takes mode as a number or an octal string too, over string, Buffer and checked data, and a value whose type the call site did not pin down runs the same ladder at run time, so an invalid mode throws parseFileMode's error before the path is touched. Their flag option takes raw open(2) bits as well as a name, and a value the call site did not pin down runs Node's options.flag || default and stringToFlags at run time, so an invalid flag throws its ERR_OUT_OF_RANGE or ERR_INVALID_ARG_VALUE before the mode is read.
    • The stat family is supported: fs.stat, fs.lstat and fs.fstat callbacks and fs.statSync, fs.lstatSync and fs.fstatSync read Node's option ladders when their options are only known at run time, bigint: true answers the BigIntStats table, in which every number member is a bigint and the four *Ns members sit beside them, and fs.Stats answers every field and its seven is* predicates. The three callback rows, the three sync forms, fs.Stats and its field rows, and the dev, ino and ctimeMs rows move to supported.
    • fs.readSync and fs.writeSync run Node's own argument ladders when the argument kinds are only known at run time: the options record read in destructuring order, length |= 0, validateOffsetLengthRead, validatePosition with bigint positions, every ArrayBufferView and Buffer encoding for writes, and the descriptor where Node checks it. fs.closeSync, fs.fsyncSync, fs.fdatasyncSync, fs.ftruncateSync, fs.fchmodSync, fs.fchownSync, fs.readvSync and fs.writevSync take the same run-time path with Node's check order (the mode and the owner ids before the descriptor, the truncate length before it, an empty write vector answering 0 before it) and Node's error messages, and their rows move to supported together with fs.futimesSync. A readSync position may be a bigint up to 2n ** 63n - 1n less the read length.
    • node:os is complete: getPriority and setPriority follow Node's default pid, one-argument shift, integer and range validation, userInfo reads its encoding as Node does, and os.cpus() rows read their fields when the array comes from a binding; all 24 os rows move from partial to supported.
    • node:dns answers resolve queries natively with a stub resolver that is linked only into programs that use it: UDP with a TCP retry, every resolve record type, Node's DNSException shapes and retry ladder, the Resolver classes with cancel and setLocalAddress, and ERR_INVALID_IP_ADDRESS. The 31 resolve and reverse rows across dns and dns/promises, the four server-list rows and the Resolver rows move from not-implemented to partial, 42 rows in all.
    • subprocess.channel, its ref() and unref(), answer as Node's do, a null or undefined scalar or tuple entry in options.stdio opens Node's default pipe in spawn, spawnSync and fork, and child.stdio is Node's one array per child: the piped handles at their slots (the same objects as stdin, stdout and stderr), null elsewhere and the 3- or 4-entry length Node reports. send lowers a nullish handle, the keepOpen options record and a callback in either slot, and a 'message' listener's second parameter is always undefined; handle transfer stays refused by name.
    • crypto adds publicEncrypt, privateDecrypt, privateEncrypt and publicDecrypt with Node 24's padding coercions, OAEP handling, error codes and implicit-rejection bytes, plus getFips, getMacs, secureHeapUsed, setEngine, the deprecated crypto.fips, createMac with its Mac handle, and X509Certificate.issuerCertificate, which answers undefined for every certificate a program builds and leaves every member on the prototype Node keeps it on. Node 24's Argon2, ECDH, KMAC and RSA-OAEP and import parameter dictionaries are declared for typed sources, and the new rows include 30 WebCrypto rows.
    • NodeEventTarget and its eleven documented listener methods are available on a Worker or MessagePort and follow Node's listener behavior, and Timeout.close() answers with the handle's Symbol.toPrimitive and Symbol.dispose spellings. events.once and events.on register the listener under a computed, symbol or other non-literal event name and move to supported.
    • readline takes a non-string prompt through getPrompt, getCursorPos and write, redraws on a terminal resize for stream and process.stdout outputs, and pauses process.stdin when the interface closes; 38 rows and ERR_USE_AFTER_CLOSE and ERR_INVALID_CURSOR_POS move to supported. InterfaceConstructor is recorded as not applicable, since Node does not export it.
    • The util.types brand checks answer Node's brands: a module namespace and a compiled arguments object answer their brand, a class extending Date answers isDate, iterator helpers stop answering isGeneratorObject, and the last two that refused to compile, isArgumentsObject and isModuleNamespaceObject, answer at a call site the compiler knows the value for (the arguments object, a provable import * as namespace) and through the runtime's thread-local namespace hook otherwise. 44 util rows move to supported, and the two predicates get partial rows.
    • zlib is complete: the Brotli and zstd one-shots and factories cover every parameter, a chunkSize above 2^53-1 throws Node's ERR_OUT_OF_RANGE on "size", a refused allocation throws ERR_MEMORY_ALLOCATION_FAILED, and the parameter error codes and the info: true engine result are pinned against Node. The nine remaining rows move to supported, and the chapter is fully supported at 77 of 77 rows.
    • TextEncoder.encodeInto lowers to the typed runtime entry when both operands have their static string and Uint8Array types and the result is the { read, written } record; the runtime fills the two counts through one walk, and every other shape keeps the checked-dynamic path with its destination check.
    • perf_hooks adds resource timing and the entry classes: performance.markResourceTiming, clearResourceTimings and setResourceTimingBufferSize follow Node's cacheMode assertion, getter values, toJSON() order, 250-entry buffer and setImmediate secondary-buffer flush, and getEntries returns resource entries as PerformanceResourceTiming. Entry records take the class their entryType names, a connected PerformanceObserver buffers the entries its marks and measures produce, takeRecords drains them synchronously, the callback receives a PerformanceObserverEntryList from an immediate, observe validates entryTypes and type, and the three classes Node keeps unconstructible throw ERR_ILLEGAL_CONSTRUCTOR. performance.nodeTiming answers PerformanceNodeTiming's fields in Node's key order with the live loop metrics, and performance.toJSON(), supportedEntryTypes and the module's free eventLoopUtilization lower too. 40 rows move off not-implemented, ten of them to supported.
    • console.trace prints Node's Trace[: args] line on stderr with Node's formatting and then the frames the call captured; the line, the formatting, the stream and which frames exist match Node, and the frame text is the program's own, so the row stays partial.
    • The stream utility predicates isDestroyed, isErrored, isDisturbed, isReadable and isWritable, and Readable.isDisturbed, answer for the node:stream classes in every lifecycle state, for the web stream classes, their readers and writers and a Response body, and for any other value by the members Node's duck typing reads. The readable operators map, filter, forEach, toArray, some, find, every, flatMap, drop, take and reduce read their concurrency, highWaterMark and signal options from a value, find's type-guard callback narrows its result, and a checked-dynamic reduce accumulator settles at its declared type. 21 rows move to supported.
    • Colored util.inspect renders errors, ArrayBuffer, SharedArrayBuffer, DataView, weak collections, URL and MIME handles, generator objects and Date subclass instances instead of refusing them, and formatWithOptions' %o and %O reach the same renderings; a colored error dims builtin frames, underlines node_modules packages and dims the working-directory prefix.
    • A compiled program takes --test-reporter=spec and --test-reporter-destination=stdout|stderr|<file> from its leading arguments in any order, sends the report where they say while program output stays on stdout, reports every taken flag in process.execArgv in the order given, and refuses an unknown reporter or an unopenable destination with a message on stderr and exit code 1.
    • A compiled program resets inherited ignored signal dispositions at startup, as Node does before it runs any program code.
    • Math.max, Math.min and Math.hypot accept a spread iterable such as a Map's keys instead of throwing at run time, fetch() takes a Request, RequestInit.cache and credentials follow Node, and a program with an unhandled rejection exits with Node's status and message.
  • TypeScript parity:
    • Computed class heritage expressions and generic bases whose type argument is any lower, including defaulted arguments and JSDoc forms; a function-local class carries its static members on its class value, which reads static fields and accessors through any, Reflect.get and Object.getOwnPropertyDescriptor; construction through a mixin result binding works; and this in a class static block is the class constructor, so this.prototype.x and this.staticField run as in Node.
    • Class index signatures take a number key or a typed value and a read of a missing key answers undefined; a generic class taken as a value without type arguments is its erased instantiation and instanceof through it answers for every instantiation; a getter override may narrow its base getter's type and a getter and setter may declare different types, with reads taking the getter's type and writes the setter's; a member of a class that extends Array can be called through elements of an array of its instances, and a JavaScript subclass's this.length = n writes the inherited length.
    • A slot typed as a class's class value (typeof C, or a constructor type returning C) holds a subclass whose constructor takes other parameters, and construction through the slot runs the held class's constructor with the arguments as written, as in Node.
    • A constructor declared inside a function can return an object that replaces the receiver as it does at the top level; a field holding a function that reads arguments or takes an untyped rest parameter can be called from typed code with the declared parameters and the argument array passed natively; and a let without an initializer can be assigned from a nested function declared above it, while reads and writes before the declaration throw Node's catchable ReferenceError.
    • A JavaScript value of a union type converts to a narrower union at run time: an arm the destination holds converts on its tag, and any other arm throws the catchable TypeError instead of failing the build. Setting length on a JavaScript array held as a checked-dynamic value follows Node's rules: a shorter length truncates, a longer one leaves holes, an invalid one throws RangeError, and a string is coerced first.
    • A typeof of an import-cycle binding that can run before the declaration compiles instead of failing with SC1016, and until the declaration runs it throws Node's ReferenceError, as a value and inside a typeof x !== "undefined" guard. A JavaScript parameter documented with a bare {Map} or {Set} takes checked-dynamic storage as a bare array parameter already does, and a typed Set or Map from TypeScript is passed as the caller's own collection.
    • A JSDoc generic written without its type arguments ({Array}, {Map}, {Set}, {WeakMap}, {WeakSet}) documents the type with any arguments, and @readonly on a getter, a setter or a method is read as documentation, so both compile instead of failing the build.
    • new WeakRef(target) and deref() compile for the objects and symbols Node accepts: a reference keeps its target alive, so deref() answers it, and any other target throws Node's TypeError. A JavaScript function reached through dynamic dispatch runs when its caller leaves a documented callback argument out and the callee reads undefined, and a typeof or property-key read of a binding declared later in its module cycle compiles when the read runs after the declaration.
    • Logical assignment and chained assignment through properties, computed members and callable own properties compile, as do super[key](...) and Object.prototype.isPrototypeOf over live prototype chains. Function.prototype.toString on a boxed function prints its source text, and util.inspect renders an Error subclass instance the way Node inspects it, including its cause and fields created at run time, with the runtime additions linked only where the construct is used.
    • In unchecked JavaScript, an undeclared global read is a run-time ReferenceError so typeof guards work, a subclass static whose signature differs from its base's compiles, and a truthiness test of a void-typed operand reads the value the call returned, while a // @ts-check file keeps all three diagnostics.
    • A JavaScript constructor that returns an object and reads or captures this, or returns a primitive on another path, is refused at the construction site, and constructors that only return objects keep compiling, including one declared inside another function.
    • Converting a class instance to a primitive compiles in programs that write properties under computed keys: the conversion uses the declared methods until a write replaces one and the replacement after that, as in Node. name in C.prototype answers for every class, and a TypeScript class's prototype works as a value for Object.create, Object.getPrototypeOf, isPrototypeOf and ===, as a JavaScript class's already did.
    • A type-position import('spec')['name'] resolves as typeof import('spec')['name'], the form the type checker's own diagnostic asks for; the strict class-field demand is no longer reported for JavaScript the project did not ask the type checker to check, and reads of fields the constructor never assigns are validated.
  • CommonJS and islands:
    • CommonJS modules whose whole export object or member exports are chosen inside a conditional compile, so require sees the table the executed branch named.
    • A function whose result is a promise of a module namespace converts into an untyped slot through an interned adapter, so () => import("./chunk.mjs") loaders compile and the chunk loads at run time.
    • Package resolution selects a package's scriptc export condition before node, import, require and default, with a types branch still leading, in every resolution the compiler performs, including the type-checked program.
    • A program built with --libnode=beside hashes the Node island modules next to it against the executable's manifest on every start and refuses a tree that differs before Node loads. The build no longer writes a tree-name file beside the modules and removes one an earlier build left there, and trees installed in the user's cache directory are unchanged.
  • Running under Node.js:
    • @scriptc/experimental/register is a preload for node --require and node --import that refuses unsupported builtin modules, native addons and process.getBuiltinModule calls with the compiler's code and message, and warns when the running Node.js version or the code-generation flag differs from what a compiled binary enforces. It reads the refused modules from @scriptc/experimental/refusals.json, which lists every refused API with the builtin module, member path and access that reach it and every fenced builtin module with the SC1010 code and message the preload raises, beside the schema version, the Node pin and the compiler version.
    • @scriptc/experimental/dual-run adds dualRun(entry, options), which builds a test file through the executable cache, runs it under Node.js and as the binary, and returns both runs, equal and a readable diff; a build the compiler refuses rejects with a DualRunBuildError holding its diagnostics. Its reporter compares every file node --test ran, prints one line per file with a diff for a difference, keeps Node's own test failures in the report, reports a file whose Node run varies as flaky and fails the run on a difference.
  • Fixes:
    • A LifetimeRegion over (C | null)[] no longer crashes, because the region's retain and release skip the shared null sentinel, and a nullable Map value read under a split key links and answers undefined on a miss as Node does.
    • A stabilized checked read's hidden local is released by the statement or branch arm that stored it instead of by the enclosing block, which a short-circuit pass reaches without storing anything, and the read's projection is lent only to an in-place receiver, so a field read off it takes its own count instead of pointing into a temporary that dies with the statement.
    • An Immutable value passed to a library call that always throws is no longer counted as escaping to it, because the call throws before it can keep or write the value: a for…of loop over an Immutable array compiles when some other array in the program holds elements of a foreign type and makes the loop's checked element read lower to array.foreignThrow, the read still throws, and a write through the array is still refused.
    • An indexed read of an Immutable array may yield an Immutable value only when the element type can hold an object: reading t.nums[0] of an Immutable<number[]> into untyped code compiles, while an element of an Immutable<Row[]> or an Immutable<number[][]> keeps SC7005.
    • A keyed read or write through an untyped value dispatches to the fields of a compiled class only when the value it receives can be a class instance, so a record or array receiver no longer hands a class's Immutable field to dynamic code; a site that can receive an instance keeps its per-class branches, and a write through an Immutable value is still refused.
    • A native Proxy keeps its traps when it flows into a Record<string, T> or another dictionary-typed slot, and String(proxy), concatenation and template literals produce Node's result ([object Object], or the handler's own toString or @@toPrimitive), with a named SC2020 diagnostic when a typed slot still cannot hold one. Promise.resolve(this) inside a method, and Promise.resolve of any value typed as the enclosing class, compile, and a destructuring loop variable in a for-of over a class with its own [Symbol.iterator] binds array and object patterns from each yielded value.
    • A method call through a class-backed interface on a user Error subclass that does not override toString calls the runtime's error.toString instead of failing the build with SC9001; the arm tests the exact class, whose chain overrides nothing below the root.
    • The abort check of the fs.stat callback form and of fs.writeFile and fs.appendFile reads aborted and reason as ordinary JavaScript properties, so a stateful options.signal getter cannot hand a non-handle value to native dispatch.
    • An extended key usage OID renders every arc exactly, so an arc wider than 64 bits no longer prints as a different, standard purpose in keyUsage or in toLegacyObject().ext_key_usage.
    • net holds the listen, backlog and keep-alive option values while user code runs during coercion and reads bound handles only after callbacks register, drops connection-attempt and signal listeners once a socket settles, and dns frees a partly parsed server list on rejection.
    • An aborted spawn's 'error' emit calls the listeners registered when the emit began: a handler may unregister itself or a sibling and still gets its call, a handler added during the emit waits for the next one, and a handler that unregisters itself keeps its captured values readable for the rest of its body.
    • A parallelInto or parallelReduce callback that is async, or that the compiler adapts to the documented callback type, runs its chunks one after another on the calling thread with an SC6106 warning, and in a program that reads stack traces every parallel loop runs its chunks the same way, as Node runs them. The runtime holds its own reference to the uncaught exception capture callback while calling it, so a callback that clears itself keeps its captured state.
    • util.inspect holds a Map or Set iterator's collection and copies the property names and values it prints for as long as rendering can run program code, through custom inspect functions and the inspect.styles accessors, and a query pair whose value conversion throws releases both converted halves.
    • DNS queries draw their transaction id from the system CSPRNG without scanning the outstanding-query list, because each query's responses are read from its own connected socket, so allocating an id always finishes however many queries are outstanding. net.connect and net.Server.listen validate an abort signal before opening anything: a value that only looks like an AbortSignal fails with the TypeError the listener install raises, and the socket, callback and registration are dropped instead of dialing behind a pending exception.
    • A member of a class that extends Array, used on an array value, checks that the array is an instance of the class: a method call on any other array throws Node's "is not a function" TypeError, and a field or accessor throws a TypeError naming the member. Native Proxy operations hold their own reference to the Proxy until they return, and new DOMException holds options.name while converting it, so a trap may delete the property that refers to the Proxy.
    • A native Proxy returned from a function declared any or with an interface or object type keeps its traps for typed callers, and the TypeError for a Proxy converted to a program class type names interface and object types, says that a class type holds only instances of the class and suggests an interface.
    • Object.setPrototypeOf on a class instance lasts as long as the instance does: getPrototypeOf, isPrototypeOf, in, instanceof, method calls and reads go through the new prototype, while instances whose prototype was never replaced keep native class dispatch.
    • The abort-signal check that net's connect path uses is linked with the net unit, so a program that links http, http2 and net without event targets builds again.
    • The sequence scope of a stabilized checked read opens with the emitter's scope pair instead of a closure: through that closure the compiler's own build had lowered emitReadReceiver dynamically (4,933 stack slots, 108,940 LLVM lines and 4,815 tag checks, against 409 slots and 14,196 lines), and the bootstrapped second generation overran the shard's 8 MiB stack. The emitted module of every program is unchanged.
    • A compiled program suspends when its launcher blocked SIGTSTP, restores the terminal mode at exit and on process.exit the way Node's uv_tty_reset_mode does, and clears an inherited signal mask at startup.

Don't miss a new scriptc release

NewReleases is sending notifications on new releases.