Repository navigation
Compile chalk 5 through the dynamic object model - #1309
Merged
Merged
Conversation
chalk's callable builder (a function whose prototype chain is set with
Object.setPrototypeOf, with style getters from Object.defineProperties)
already ran on the checked-dynamic model. What failed was the code
around it. Each fix is general and sits on a path that used to end in a
refusal or a runtime fence, or is confined to JavaScript files:
- package.json "imports" inside a --npm-static package resolve to the
shipped JS with the "node" condition hoisted, like the tsgo fs shadow;
- JavaScript object literals that the file reshapes at run time (keyed
writes with runtime keys, delete, Object.defineProperty/ies) map to
the checked-dynamic type, with the literals nested in them;
- a JavaScript class whose only member is a constructor that returns a
value and never reads `this` answers that value from `new` (the return
used to be discarded silently);
- `const { env } = process` binds the data member in JavaScript;
- `o && x` / `o || x` with an always-truthy object, array or function
left operand;
- Number.parseInt/parseFloat over checked-dynamic values apply ToString;
- Function.prototype as a value is one compiler-emitted function;
- calls of String.prototype-only names (bold, italics, ...) on untyped
receivers call the receiver's own or inherited member; a string
receiver throws the refusal at run time.
Programs without these patterns compile to identical bytes: the runtime
benchmark workloads, the self-hosted compiler (LLVM and executable) and
the TypeScript corpus IR were compared against the baseline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
chalk 5 now compiles from its published JavaScript and matches Node byte for byte at every color level, from TypeScript and from JavaScript callers. No chalk-specific lowering: the builder runs on the existing checked-dynamic machinery, and this change closes the general gaps around it.
Reducing chalk's builder to a minimal repro showed that the core of the builder already worked on
main. That core is a callable whose prototype chain is set withObject.setPrototypeOf, whose styles are getters defined withObject.definePropertieson a function, and whose every property read creates a new callable. The failures were all in the surrounding code:mainimport ansiStyles from '#ansi-styles'inside the packageTypeError: Package import specifier "#ansi-styles" is not defined(the resolver answered the.d.tstwin, and{ node, default }targets ignorednode)--npm-staticpackage,importsentries resolve like the tsgo host's fs shadow: thenodecondition is hoisted and the shipped JS is used, never a declaration twinassembleStylesreshapes an object literal (styles[name] = …,group[name] = …,Object.defineProperty(styles, …))assignment to non-array elementsfence, then wrong-typed reads (expected object | object | object)deleteorObject.defineProperty(ies); the type mapper answers the checked-dynamic type for them (and the literals nested in them)const {env} = processprocess(env,argv,platform,arch,pid,execPath,versions) bind their values when destructured in JavaScriptstreamIsTTY: stream && stream.isTTYo && x/o || xwith an always-truthy object, array or function left operandNumber.parseInt(env.FORCE_COLOR, 10)unknownargumentNumber.parseInt/Number.parseFloatover a checked-dynamic value apply ToString, like the globalparseFloatalready didObject.setPrototypeOf(createChalk.prototype, Function.prototype)Function.prototypehas no loweringFunction.prototypeas a value is one anonymous zero-capture function answering undefined (closure identity, so every read is the same function)class Chalk { constructor(options) { return chalkFactory(options); } }new Chalk()answered an empty instancethis: its instance type is checked-dynamic andnew C()runs the constructor body as a function and answers the returned object or functionchalk.red.bold("x")method calls like 'chalk.red.bold':boldis aString.prototypename, so calls on untyped receivers were refusedString.prototypedeclares (bold,italics,link, …) call the receiver's own or inherited member; a string receiver throws the refusal at run timeZero cost for good code
Every change sits on a path that ended in a refusal or a runtime fence, or is confined to JavaScript files and to declarations the scan finds. No runtime C changed. The checks below compare the baseline (
4ed75111a) and this branch on Linux x86_64, building the same input paths with both compilers:--emit=llvm, header line dropped)benchmarks/runtime/workloadsprograms,builds/log-summary,builds/inventory-reportSCRIPTC_NO_CACHE=1, SHA-256)packages/compiler/src/native/cli.tsbuilt by both compilers--emit=ir)--emit=ir)An earlier iteration of the scan also marked empty literals (
const results = {}; results[k] = v) and changed the IR of two JavaScript corpus programs. Empty literals already have a checked-dynamic binding, so the scan now skips them, and both programs compile exactly as before.The first version kept
Function.prototypein the C runtime (scr_json.c). Its LLVM was identical, but every release executable differed: the new string literal joinedscr_json.o's merged string section, which every program links. The singleton is now a compiler-emitted closure, and the runtime is untouched.Evidence
Corpus programs (Node vs native, stdout/stderr/exit byte-identical):
js-callable-builder.js: the chalk builder pattern: getters on a function prototype chain, symbol-keyed builder state, getter caching withdefineProperty, nested styles,levelget/set through the generator,Function.prototypelink,applythrough the chain.js-function-prototype-value.js:Function.prototypeas a value and as a prototype link.js-reshaped-object-literal.js: the ansi-styles reshaping,delete, runtime-keyed writes.js-class-constructor-returns/: value-returning constructors (function, object, per-call), from JavaScript and TypeScript.js-process-destructure.mjs:const { env, argv, platform } = process.js-number-parse-dynamic.js:Number.parseInt/parseFloatover dyn values.truthy-object-logical.ts:&&/||with object, array and function left operands, including evaluation order.js-builder-string-method-names/: a TypeScript caller of builders whose members are namedbold,italics,link,big.Real package:
tests/fixtures/npm/cases/chalk5: chalk 5.6.2 vendored (MIT), a program exercising every color model, nested styles,chalk.level,new Chalk({ level })at levels 0–3,chalkStderr,supportsColorand the exported name lists. It runs innpm.test.ts.tests/harness/chalk.test.ts: the same program underFORCE_COLORunset,0–3,true,false, empty, and the--color,--no-color,--color=256,--color=16mflags (supports-color reads both), compared with Node.Timing (Linux x86_64 sandbox, chalk 5.6.2,
new Chalk({ level: 3 }), warmed in-process loops, three runs each, median per call):c.red(s)c.red.bold(s)c.hex("#ff8800")(s)c.blue("a " + c.red(s) + " b")new Chalk({ level: 1 }).green(s)chalk.red(s)at level 0chalk calls run on the checked-dynamic path, so they are 6–21× slower than Node in a hot loop. A typical CLI makes few of them. The whole fixture program (
FORCE_COLOR=3) takes 2 ms per run as a native binary and 71 ms under Node, because process startup dominates.Not in this change
requireinside a function (template = require('./templates'), PR Compile classic CommonJS packages: late requires, JSON requires, nested builtin destructuring, settled require cycles #1287),Object.defineProperty(module, 'exports', { get })in ansi-styles 4, and color-name's string-keyedmodule.exports = { "aliceblue": … }table. The builder code itself is the same pattern chalk 5 uses.thiskeeps today's lowering, which still discards the returned value (pre-existing, not fixed here). For the classes this change claims, a constructor that falls through or returns a primitive throws a deferred SC1090 refusal instead of answering the bare instance.Function.prototype's methods as a value through a custom prototype chain (typeof f.applyafterObject.setPrototypeOf(f, P)withPlinked toFunction.prototype) answersundefined; calling them (f.apply(…),f.call(…)) works. Fixing the read needs a change on the shared dyn read path in the runtime, which this change avoids.FORCE_COLOR, Node'sconsole.logcolors numbers and scriptc does not. In TypeScript,arr && arr[0](a union result) still stores the array in the result union and throws a TypeError at run time; the new always-truthy rule only covers results that are not unions holding a different arm.🤖 Generated with Claude Code