Technical Debt Tracker #
Status: active Last reviewed: 2026-10-17 Owner: theMackabu
Use this file to record debt that is important enough to preserve but not yet scheduled.
Format #
- Area:
- Issue:
- Impact:
- Proposed fix:
- Owner:
- Status:
Open Items #
-
Area: Reflect.set receiver and success semantics (
src/modules/reflect.c, property-setting internals)- Issue: Reflect.set ignores its optional receiver and returns true after js_setprop unless an exception occurs. js_setprop returns the assigned value even for some rejected writes, so its result cannot be interpreted as a success boolean. Existing own-property read-only checks do not cover inherited restrictions, non-extensible receivers, or falsy proxy set traps.
- Impact: Accessors can receive the wrong this value, data writes can modify the target instead of the receiver, and callers can observe success when nothing was written.
- Proposed fix: Address in a separate compatibility PR. Share a receiver-aware internal [[Set]] operation with explicit success/rejection/exception results; retain assignment-expression values and strict-mode throwing behavior at their callers. Avoid duplicating the property-definition and proxy machinery in a Reflect-only implementation. Preserve thrown-value identity and GC roots across callbacks.
- Validation: Compare with Node for inherited setters and read-only properties, distinct and primitive receivers, receiver-side accessors, frozen/non-extensible objects and arrays, proxy set/defineProperty traps and transparent fallbacks, revocation, SameValue invariants, and exception propagation. Run focused tests and the full spec suite before landing.
- Owner: theMackabu
- Status: backlog (2026-09-05); unfinished Reflect-specific implementation and test removed from this PR at user request. The earlier frozen-own-property fix remains. Recoverable local experiment:
/tmp/ant-reflect-deferred.0pgnxD(not a validated implementation or durable dependency).
-
Area: JS path performance (
src/builtins/node/path.cjs, Silver runtime)- Issue: The compatible JS implementation beats installed Ant on three of eight path microbenchmarks, but normalize, join, and relative remain 3.7-7.2x slower. Performance parity is deferred; Node-compatible correctness takes priority over speed. Keep primordial protection and do not trade semantics for benchmark gains.
- Impact: Current timings are roughly 0.4-1.5 microseconds per call. This is a substantial microbenchmark gap, but does not establish an application-level regression. Profile representative applications before adding runtime complexity; path-heavy bundling, crawling, and module resolution are useful candidates.
- Proposed fix: Revisit only with measured application impact. Profile the remaining JIT/string costs, retain only repeatable wins, add compatibility regressions, and compare pinned installed/current/Node binaries serially with ABBA ordering. Do not revive the removed hoisting, grouped-initializer, or register-argument experiments without new evidence.
- Owner: theMackabu
- Status: backlog (2026-09-05; accuracy first)
- Evidence: Accepted-only checkpoint, median milliseconds per 100,000 calls, 20,000 warmups, 12 samples per binary; lower is better. Hashes and raw runs are in
/tmp/ant-path-goal.KC4jj3/three-way-results.json. These measurements predate the reserved-name compatibility fix. Details and experiment history: path performance plan. All 4,136 spec tests and 6,090 Node differential cases passed at this checkpoint; finite coverage is not proof of complete compatibility.
Workload Baseline $(which ant)Current ./build/antNode posix.normalize 9.19 37.97 8.17 win32.normalize 10.38 74.28 14.72 posix.join 14.62 73.63 18.15 posix.resolve (cwd) 945.52 147.84 32.54 posix.relative (absolute) 25.30 93.40 31.88 win32.resolve (absolute) 960.83 113.88 20.72 win32.relative (absolute) 27.67 142.84 36.54 posix.parse 23.10 19.01 2.39 -
Area: primitive receivers + prototype accessors
- Issue: A getter/setter defined on a type prototype (e.g.
Object.defineProperty(String.prototype, 'x', { get() {...} })) never fires for primitive receivers —"s".xreturns undefined where node runs the getter. Pre-existing on all binaries (installed release, pre-port master, current); the primitive lookup paths (js_try_get_lenboxing path,sv_prop_get_atfallback) skip accessor invocation for proto-held accessors on primitives. - Impact: Rare pattern (accessors on builtin prototypes), but a silent wrong-value divergence from node. The primitive-IC regression test pins only "warmed site agrees with cold access" for this case; fix the engine gap and the test can assert node's value.
- Proposed fix: In the primitive branch of
js_try_get_len(and the field-IC slow path), when the proto-chain lookup lands on an accessor, invoke the getter with the primitive asthis(try_accessor_getterwith the unboxed receiver) instead of falling through to paths that load nothing. - Status: backlog
- Issue: A getter/setter defined on a type prototype (e.g.
-
Area:
src/modules/child_process.c— WindowsspawnSyncoption parity- Issue: The win32
spawn_sync_implbranch ignorestimeout,killSignal, andmaxBufferentirely (the POSIX branch implements all three via the select loop andsync_read_ctl_t). Theoutputarray anderrorshape gaps were fixed 2026-08-02, but the three options silently do nothing on Windows. - Impact:
execSync/spawnSyncwith a timeout can hang forever on Windows, andmaxBufferprovides no protection;tests/test_child_process_exec_sync_options.cjsskips on win32 for exactly this reason, so CI would not catch a fix or a regression. - Proposed fix: Implement the deadline with
WaitForSingleObject(pi.hProcess, remaining_ms)+TerminateProcesson expiry, and cap the stdout/stderr accumulation loops with the maxBuffer budget (mirrorsync_read_ctl_tsemantics: set the ETIMEDOUT/ENOBUFSerrorvia the samesync_result_errorshape). Needs a Windows machine to validate; un-skip the options test on win32 once done. - Status: backlog (blocked on Windows validation)
- Issue: The win32
-
Area:
src/sandbox/backends/darwin.c/ macOS HVF VMM interrupts- Issue: The Darwin Hypervisor.framework backend currently continues when
hv_gic_config_set_msi_interrupt_range()fails, and device bringup still includes legacy/polling/manual wake paths instead of a fully interrupt-driven virtio PCI model. - Impact: This is acceptable for sandbox bringup, but it leaves the backend less representative of the final VMM contract. Future virtio devices, networking, and lower-latency I/O may rely on proper MSI/MSI-X delivery instead of polling or legacy compatibility paths.
- Proposed fix: Implement proper GIC MSI/MSI-X wiring for PCI virtio devices on Apple Silicon HVF, make block/net/vsock/9p completions use real device interrupts, then remove the legacy/polling/manual-wake bringup paths and any fallback code that only exists because MSI is missing.
- Status: backlog
- Issue: The Darwin Hypervisor.framework backend currently continues when
-
Area:
src/modules/storage.c/ globallocalStorage- Issue:
JSON.stringify(globalThis)can throw when it reaches the globallocalStorageproperty and no--localstorage-fileorlocalStorage.setFile()path has been configured. Repro:console.log(JSON.stringify(this));currently reportsTypeError: localStorage requires a file path: pass --localstorage-file or call localStorage.setFile(path) - Impact: Object inspection and serialization of broad global objects can fail because a host API accessor performs configuration validation too early. This is surprising behavior and unrelated code can trip over localStorage simply by enumerating or stringifying globals.
- Proposed fix: Make the global
localStorageproperty safe to read when unconfigured, either by returning an inert/lazy storage object that throws only when storage operations are used, or by omitting/marking the property so generic stringify/inspection does not invoke a throwing accessor. - Status: backlog
- Issue:
-
Area: Silver closure allocation / function-object initialization
- Issue:
sv_init_closure_function_object()assignsclosure->func_objbefore proving that the backing function object and its required metadata were initialized successfully. Allocation failures inmkobj(),.lengthsetup, or prototype setup can therefore leave a closure with an error or partially initializedfunc_obj. - Impact: This is an OOM-hardening issue rather than a normal JS-level repro; it likely needs allocator fault injection or a real process memory cap to observe reliably. If it does happen, the interpreter or JIT can expose a callable with invalid/incomplete function-object state.
- Proposed fix: Make
sv_init_closure_function_object()return a status value, assignclosure->func_objonly aftermkobj()succeeds, propagate failures from length/prototype setup, and havesv_op_closure()/jit_helper_closure()return the error instead of publishingfunc_val. Avoid fixing this by forcing generic bailout forOP_CLOSURE; that would risk large JIT/Newt regressions. - Status: backlog
- Issue:
-
Area: Silver closure/upvalue allocation OOM handling
- Issue:
sv_closure_init()does not check the external upvalue-pointer-arraycalloc()used when a child captures more thanSV_CLOSURE_INLINE_UPVALSvalues. It returns a non-NULL closure withclosure->upvalues == NULL, after which bothsv_op_closure()andjit_helper_closure()immediately write through that pointer. The adjacentsv_capture_upvalue(),jit_capture_upvalue(), andjit_make_undef_upvalue()paths also dereference uncheckedjs_upvalue_alloc()results. - Impact: A native allocation failure during ordinary closure creation causes undefined behavior, normally a NULL-pointer crash, instead of following the existing
kTypeErrorOOM path. The external-array bug needs only five captured values; it is OOM-only, not an unreachable closure shape. - Proposed fix: First check the
calloc()result insv_closure_init()and returnNULL; both callers already propagate that result, and the initialized unreachable arena slot is safe for a later young/major sweep. Do not free the slot immediately without also removing its young-roster entry. Then audit upvalue-cell creation as a separate step: make allocation failure propagate through the capture loop without publishing a partially initialized closure or leaving partially linked open cells. Add normal 5+-capture interpreter/JIT/GC coverage, but do not add a production fault-injection hook solely for the OOM branch. - Status: backlog
- Issue:
-
Area:
src/modules/readline.c- Issue: Rendering assumes readline owns the full visible prompt line, so redraws are anchored to the logical prompt text instead of the terminal position where editing actually begins.
- Impact: Full redraw paths can clobber externally rendered prefixes, boxed prompts, or other same-line UI written before
question()orprompt()starts editing. - Proposed fix: Track an explicit render origin / prompt anchor, separate logical prompt text from the screen position where input begins, and make redraws preserve external prefixes and custom prompt chrome.
- Status: open
-
Area:
src/modules/process.c/src/modules/tty.c- Issue: Stdio TTY stream setup still has split ownership.
process.ccreatesprocess.stdout/process.stderr, installs stdoutrows/columns, and keeps its own terminal sizing helper;tty.clater reshapes the same streams, reinstallsrows/columns, and keeps a separate terminal sizing helper. The stale SIGWINCH setter path has already been removed, but the duplicated ownership that allowed it to drift remains. - Impact: Future stdio or TTY changes can diverge between the process bootstrap path and the TTY module path, especially around descriptor shape,
getWindowSize(), resize behavior, and platform-specific terminal sizing. - Proposed fix: Make
tty.cthe single owner of TTY stream shape and terminal sizing for stdout/stderr, leavingprocess.cto create or expose process streams and wire process-specific event-emitter behavior. Share one sizing helper or module-level API sorows,columns, andgetWindowSize()all use the same implementation. - Status: backlog
- Issue: Stdio TTY stream setup still has split ownership.
-
Area: Node-style stream duck typing
- Issue:
src/modules/stream.cstill probes and calls arbitrary stream-like properties such as.write,.end,.pause,.resume,.pipe,.read,.next,_read,_write,_transform, and.getReader. Related ad-hoc stream/event probes also appear insrc/modules/fs.c(.destroy/.end),src/modules/zlib.c(.write/.end), andsrc/modules/child_process.c(.once). Lower-confidence event-handler cases insrc/modules/worker_threads.c(.onmessage) andsrc/modules/abort.c(.onabort) should be checked for consistent non-callable handling, but should not be treated as bugs without a concrete repro because handler properties are normal platform-style APIs. - Impact: Each call site currently decides for itself which missing or non-callable methods are ignored, treated as compatibility no-ops, or allowed to fail later. That makes Node-stream compatibility behavior harder to reason about and can hide inconsistent errors across modules.
- Proposed fix: Start with
src/modules/stream.c: add small helper gates such asstream_get_callable_prop(),stream_is_node_writable_like(), andstream_is_reader_like(), then make required-method errors and optional-method no-ops consistent. After that, route thefs.c,zlib.c, andchild_process.cstream/event probes through shared helpers. Finally, auditworker_threads.candabort.conly for consistent ignore-vs-call behavior on non-callable handler properties. - Status: backlog
- Issue:
-
Area: Silver compiler
- Issue:
sv_compiler_tscratch storage is still allocated per compilation, so repeated compiles in a long-lived process pay allocator churn for locals, bytecode buffers, constants, atoms, upvalue descriptors, loops, srcpos data, and maybe slot-type scratch. - Impact: One-shot CLI compiles are fine, but a REPL, watch mode, embedder, or other long-lived process cannot yet recycle compiler scratch space across compiles.
- Proposed fix: Add a real
compile_poolscratch allocator after thecompile_ctxextraction. Pool the resizable arrays forlocals,local_lookup_heads,code,constants,atoms,upval_descs,loops,srcpos, and potentiallyslot_types. Keepline_tableseparate or make it poolable scratch, since it is derived from the current source buffer rather than a semantic cache. - Status: backlog
- Issue:
-
Area:
src/modules/intl.c- Issue:
Intlis now present and passes the current compat-table target, but several behaviors are still simplified compatibility implementations rather than fuller ECMA-402 semantics. - Impact:
Intl.Collator,Intl.NumberFormat,Intl.DateTimeFormat, andIntl.Segmentercan still diverge from web or Node behavior for anything beyond the currently covered compat surface. - Proposed fix: Continue expanding
Intlincrementally: replacestrcoll-only collation, deepenresolvedOptions(), makeDateTimeFormatactually honor stored timezone and locale options, and moveSegmentercloser to the expected iterable/result object shape. - Status: backlog
- Issue:
-
Area:
src/modules/timer.c- Issue:
node:timers/promises setInterval()is still explicitly unimplemented. - Impact: Promise-based timer APIs remain incomplete and can block compatibility with code that expects the Node timers/promises interval surface.
- Proposed fix: Implement
setInterval()on top of the existing timer promise scheduling machinery, including cancellation and signal handling behavior consistent with the existingsetTimeout()andsetImmediate()support. - Status: backlog
- Issue:
-
Area:
src/modules/dns.c- Issue:
node:dnsis still a minimal shim centered ondns.promises.lookup. - Impact: Tooling or apps that expect more of the Node DNS surface still need polyfills or will fail outright.
- Proposed fix: Expand the module incrementally from the existing lookup path, prioritizing the most commonly used sync, callback, and
promisesAPIs needed by current ecosystem packages. - Status: backlog
- Issue:
-
Area:
src/modules/crypto.c- Issue:
crypto.subtleis only partially implemented and still marked for extension beyond the current digest-oriented support. - Impact: Web Crypto compatibility is incomplete, which blocks packages and runtime features that expect a broader
SubtleCryptosurface. - Proposed fix: Extend
crypto.subtlemethod coverage incrementally, starting with the highest-value operations after digest and preserving the existing algorithm parsing entrypoints. - Status: backlog
- Issue:
-
Area:
src/modules/async_hooks.c- Issue:
node:async_hooksis still a minimal compatibility layer intended mainly to satisfy framework expectations. - Impact: Async context tracking semantics remain shallow, which can break libraries that rely on realistic async IDs, resources, or hook lifecycle behavior.
- Proposed fix: Replace the placeholder async ID and resource behavior with real runtime-backed tracking, while keeping
AsyncLocalStoragecompatibility stable during the transition. - Status: backlog
- Issue:
-
Area: Native cfunc ABI / iterator
next()trampolines- Issue: The lightweight native-function ABI (
ant_cfunc_t, registered viaAnt_Function_Createwith a staticant_cfunc_meta_t) has no per-registration data channel — no userdata pointer and no QuickJS-stylemagicint. Any family of native callbacks that share logic but differ by one parameter therefore needs a named trampoline per variant. Current example: the six*_iter_nextwrappers (array, string, map, set, typed array, headers) that each just calljs_iter_next_result(js, advance_*). That helper was introduced by PR #40 (issue #39) to fix unspecified-evaluation-order reads of an uninitializedant_value_tin the oldjs_iter_result(js, advance_*(js, &it, &value), value)pattern, which GCC on Linux miscompiled (stalevalueread before the advance call) while Apple clang happened to evaluate in the safe order. - Impact: Source-level boilerplate only. The trampolines are zero runtime cost:
js_iter_next_resultisstatic inlineand the advance function is a compile-time constant at each call site, so calls are devirtualized and inlined, and each wrapper keeps a distinct symbol in profiles and stack traces. Alternatives considered and rejected:js_heavy_mkfun_native()binding the advance pointer to one generic callback (adds a slot load plus an indirect call on the hot iterator path, allocates a heavier function object, requires a function-pointer-through-void *cast, and collapses profiler frames); macro-generated wrappers (pure taste, less greppable). - Proposed fix: Only if per-function registration data is wanted in several more places, add a QuickJS-style
magicint toant_cfunc_meta_tand pass it through to callbacks, letting one genericiter_nextdispatch through a static advance table with no runtime pointer chasing. This is an engine-wide ABI change — every builtin signature, the call dispatcher, and the JIT native-call convention — so it must not be done just to delete the six iterator trampolines. - Owner: theMackabu
- Status: backlog (deliberately deferred; revisit only alongside a broader cfunc ABI change)
- Issue: The lightweight native-function ABI (
-
Area:
src/streams/readable.c- Issue:
ReadableStreamBYOBReaderis still explicitly unimplemented, and byte-source support is still called out as incomplete. - Impact: Web Streams byte-oriented consumers cannot rely on BYOB reader semantics, leaving an important platform feature gap for stream-heavy or browser-compatible code.
- Proposed fix: Add real byte-source plumbing and implement
ReadableStreamBYOBReaderon top of it instead of routing byte sources through the default reader path. - Status: backlog
- Issue:
-
Area: sloppy-mode
thisboxing / per-call primitive wrapper allocation- Issue: The 2026-08-06 parity fix made sloppy non-arrow functions that observe
this(bytecode containsOP_THIS,OP_CLOSURE, orOP_EVAL) box primitive receivers per spec (js_normalize_sloppy_this+jit_helper_normalize_sloppy_this). Each such call allocates a fresh wrapper object: 3M warmed calls measure ~260ms vs node's ~20.6ms (node caches/inline-allocates its wrappers). Strict functions and sloppy functions that never touchthisare proven flat (interleaved A/B, 20M-call micro). - Impact: Correct semantics (the old fast path returned
typeof this === "number"where spec says"object"), and the cost is confined to a rare pattern — sloppy functions invoked with primitive receivers that actually readthis. Only matters if it ever shows up in a real profile. - Proposed fix: A per-primitive-value wrapper cache (or a small per-isolate cache keyed on the primitive value/type) so repeated calls with the same receiver reuse one wrapper. Mind identity semantics: each sloppy call must still observe a spec-fresh wrapper if the program can detect identity (e.g.
this.x = 1then re-entry) — so a cache is only sound if invalidated on any property write, or scoped to reads-only paths; otherwise consider a nursery-friendly fast allocation path instead. - Owner: theMackabu
- Status: backlog (perf only; do not regress the 2026-08-06 semantics)
- Issue: The 2026-08-06 parity fix made sloppy non-arrow functions that observe
-
Area:
src/modules/regex.c/ exact ECMAScript Unicode properties of strings- Issue: PCRE2 supports Unicode properties of individual code points but not ECMAScript's finite multi-code-point properties of strings. Ant therefore lowers properties such as
RGI_Emojiinto hand-written structural patterns in the current textual/vtranslator. Those approximations can admit malformed sequences, miss valid sequences, and cannot compose exactly with/vunion, intersection, difference, nested sets, or\q{...}string members. Ant currently vendors PCRE2 10.47 with Unicode 16.0 tables, which also prevents exact Unicode 17 behavior even if the string-property expansion itself is updated. - Impact:
/vexpressions usingBasic_Emoji,Emoji_Keycap_Sequence,RGI_Emoji_Modifier_Sequence,RGI_Emoji_Flag_Sequence,RGI_Emoji_Tag_Sequence,RGI_Emoji_ZWJ_Sequence, orRGI_Emojican produce false positives or false negatives. Ad hoc structural fixes only move the boundary, while inconsistent code-point and string-property Unicode versions can make set operations internally contradictory. - Proposed fix: Keep PCRE2 as the final matcher and JIT, but replace textual
/vrewriting with a typed set AST whose values contain code-point ranges plus finite string tries. At build time, pin oneANT_UNICODE_VERSION, consume the authoritative Unicode data for all seven properties, and generate checked-in compact trie/minimized-DAG tables with generator provenance. At pattern compile time, evaluate union, intersection, and difference exactly; emit code-point ranges as PCRE2 classes and emit string tries as compact longest-first, non-atomic alternations so normal ECMAScript backtracking to shorter members remains possible. Enforce the specification's early errors for complements of string properties and negated classes containing them. Keep PCRE2's code-point tables on the same pinned Unicode version, preferably by updating/regenerating its UCD data rather than maintaining a divergent overlay. A first milestone may lower direct uses of the seven properties exactly, but the typed AST is required for complete/vset algebra. - Rejected alternatives: Do not post-filter PCRE2 matches because that breaks captures, backtracking, lookarounds, quantifiers, replacements, and match indices. Do not depend on zero-width callouts to consume variable-length strings, fork PCRE2 with an Ant-only opcode, replace the regex engine, or continue growing structural emoji approximations; each has a larger correctness or maintenance cost than generated exact lowering.
- Owner: theMackabu
- Status: backlog (targeted
RGI_Emojifalse-positive fix exists; complete property-of-strings and Unicode-version conformance remain open)
- Issue: PCRE2 supports Unicode properties of individual code points but not ECMAScript's finite multi-code-point properties of strings. Ant therefore lowers properties such as
-
Area:
src/modules/regex.c/ remaining RegExp divergences from node- Issue: A 68-probe node-differential harness (2026-08-06 validation of the regex rework) leaves 15 pre-existing divergences, all reproduced identically on the installed release and on pre-rework binaries — untouched by and out of scope for the rework. Families: (1) exec-result
groupsis an accessor property where node has a plain writable/enumerable/configurable data property (affectsObject.getOwnPropertyDescriptorshape onexec/d-flag results); (2)$<name>named-group references in string replacements are not substituted ('abc'.replace(/(?<m>b)/g, '[$<m>]')emits the literal text) and the function-replacer named-groupstrailing argument is missing; (3) legacy staticsRegExp.leftContext/rightContextare undefined after matches ($1..$9/lastMatchwork); (4) subclass/own-property dispatch: an own or subclassglobal/flagsgetter and a subclassexecoverride are bypassed by the built-in@@match/@@replacefast paths (a swappedRegExp.prototype.execdata property IS honored); (5)lastIndexstring values are not coerced before use,lastIndexis defined non-configurable so accessor redefinition throws where node allows it pre-first-exec; (6)new RegExp('').sourceis""instead of"(?:)", and/(?:)/g-style empty-match iteration over non-BMP text withoutuadvances by code point instead of code unit. - Impact: Silent wrong values or missing substitutions for code relying on named-group replacement strings, legacy statics, or RegExp subclassing; all are node-observable divergences that a differential test will keep flagging. None affect the perf-critical exec/match/replace fast paths' results for ordinary regexps.
- Proposed fix: Fix per family, cheapest first:
sourcenormalization andlastIndexToNumber coercion are one-liners;$<name>substitution + replacergroupsargument extend the existingrepl_template/replacer marshaling with the already-cached named-groups meta;groups-as-data-property is a result-shape change (canonical shape already exists — add the slot); leftContext/rightContext extendupdate_regexp_statics; subclass/own-getter dispatch requires the batch/fast-path guards to also checkglobal/flags/execown-or-overridden state (guards already exist forexecdata-property swaps — extend to accessors and subclass prototypes). - Owner: theMackabu
- Status: backlog (pre-existing node-parity gaps; validation harnesses in /tmp/v_regex*.cjs shapes, recreate if wiped)
- Issue: A 68-probe node-differential harness (2026-08-06 validation of the regex rework) leaves 15 pre-existing divergences, all reproduced identically on the installed release and on pre-rework binaries — untouched by and out of scope for the rework. Families: (1) exec-result
-
Area: compiled-code lifetime — permanent strings, BigInt literals and pinned template objects (
src/runtime.ccode arena,js_mkstr_permanent,js_mkbigint_permanent,gc_pin_permanent)- Issue: Compiled functions (
sv_func_t, bytecode, constants) live in the code arena and are never freed individually. Every compile-time string literal, including tagged-template cooked and raw strings, is allocated byjs_mkstr_permanentinjs->pool.permanent, which is only released at isolate teardown. BigInt literals are the same: the compiler builds them withjs_mkbigint_permanent(src/silver/compiler.c), whose payload comes fromjs_permanent_allocin the same pool, because the collector does not trace constant tables and a collected payload would be swept while the function still refers to it. Runtime BigInts (arithmetic results,BigInt(), conversions) stay in the collected BigInt pool; only thepermanentflag threaded throughbigint_alloc_payload_in,js_mkbigint_limbs_inandbigint_from_string_digitsseparates the two. Tagged-template objects (OP_PUT_CONST) and JIT object-literal templates (jit_helper_object_template) are kept alive withgc_pin_permanent, which is sound only because the code that owns them is immortal. Code compiled repeatedly at runtime (eval,new Function, VM scripts, template engines that compile on the fly, REPL input, hot module reload) therefore grows memory without bound, including pinned objects that every major collection traces. V8 instead keeps template objects alive through the code that owns their site and collects both when that code dies. - Impact: Unbounded memory growth for programs that compile code repeatedly. The pins also add major-collection tracing proportional to their count: on a GC-heavy churn benchmark, 16,000 pinned template objects cost about 4-5%, which matches keeping the same objects alive through a plain array. The string part of that cost is removed by the permanent-string marking fast path (
STR_META_PERMANENT). - Proposed fix: Give each compiled unit (script, module, or function tree) an owner the collector can reclaim once no closure, frame, or JIT code references it. Allocate the unit's literal strings and BigInt literal payloads in that unit's own arena instead of the global permanent pool, and replace
gc_pin_permanentfor template objects with ownership by the unit: traced from it and freed with it. KeepSTR_META_PERMANENTmeaning "memory owned outside the collector"; it stays valid because the collector still never frees those strings individually, their unit does. The permanent-root vector then only holds truly isolate-lifetime objects. - Validation: A loop compiling
new Function/evalsources that contain tagged templates, object literals and BigInt literals must reach steady-state RSS. Template identity per call site must hold for as long as the code is alive, and a GC torture pass must cover reclaiming a unit while one of its closures is still on the stack. - Owner: theMackabu
- Status: in progress (2026-09-23). Code from
evaland the Function constructors is now reclaimed through code units (Reclaim Dynamic Code); scripts, modules, the REPL, the atom intern table and JIT machine code are still immortal. Related: the cross-isolate code-arena entry above.
- Issue: Compiled functions (
-
Area: GC native-stack scan — compiled (MIR) frames are still scanned conservatively (
src/gc/objects.cgc_scan_current_stack/gc_scan_segmented,include/gc/objects.hgc_vm_seg_t,src/jit/entry_stub.c)- Issue: Interpreter activations are skipped by the stack scan and the event loop keeps libuv's poll buffer free of JS values, but compiled frames are still scanned word by word. A stale word in a compiled frame (a dead spill slot, or an interpreter register saved by a JIT prologue) counts as a root. Retention only grows when such a word stays at the same address across several majors: one stale pointer to a removed node keeps every node removed after it alive, and the chain grows each major. Compiled frames rarely qualify because they are short-lived and the next call at the same depth rewrites them, so making them precise was deferred. Per-word attribution on canonical Splay (2026-09-22) found every retaining word in the interpreter's or
uv_run's frame and none in the MIR frames active at every major; with those two fixed, post-major live stays flat at about 768.7k objects (it used to climb to 2.86M). The one known compiled-frame path, stale interpreter callee-saved registers saved by a JIT prologue, is handled byant_jit_enter_cleanzeroing them on entry from non-compiled code. - Impact: Not observed. The exposure is a long-running compiled frame that never returns and keeps a dead pointer in a slot it stops writing, for example an OSR'd outer loop whose spill slot held a node that was later removed. The symptom would be post-major live objects climbing on a workload whose real live set is constant.
- Proposed fix: Make compiled frames precise. Record GC stack maps in MIR at each safepoint (helper calls and allocation sites): which spill slots and callee-saved registers hold tagged values. Keep a code-range → function registry so the scan can map a return address to its map. At GC time, walk the compiled frames by frame pointer (MIR keeps a frame chain on arm64, with the frame record at the bottom of the frame and the saves at the top), mark only the slots the map lists, and scan any frame without a map conservatively. Once every compiled frame has a map,
ant_jit_enter_cleanand thevm_top_kindbyte can go, since the saved stale registers would no longer be read. The MIR changes (safepoint maps, the saved-area layout, per-function code ranges) are the bulk of the work. - Diagnosis: To check whether a retention report is this issue, instrument a major temporarily. Drain precise roots before
gc_scan_current_stack, then count the objects the stack scan greys. Scan the stack one word at a time, draining after each, and log any word that greys more than a few hundred objects. Walk the frame-pointer chain withdladdron each saved return address to name the frame each word sits in. Words insv_execute_frameoruv_runpoint at the existing mechanisms. Words in unnamed frames are MIR code, and point here. - Validation: A benchmark with a constant live set (canonical Splay; the retention microbenches in the 2026-09-22 session) keeps flat post-major live counts with the conservative JIT-frame scan replaced.
ANT_GC_STRESSsweeps pass on the verify build with and without--jitless. The boundary microbench (tests/bench_vm_boundaries.cjs) shows no slowdown on the compiled-code crossings. - Owner: theMackabu
- Status: backlog (2026-09-22); deferred because no workload shows retention through compiled frames.
- Shared infrastructure (2026-10-09): the code-range → function registry and per-call-site records keyed by return offset now exist for stack traces (
src/jit/unwind.c, MIR patchmir-call-site-info.patchtags call insns and reports their return offsets). The stack maps should extend those records (one per safepoint: tagged slots plus bytecode offset), as V8's safepoint table and deoptimization data serve both GC and stack traces, rather than add a second table.
- Issue: Interpreter activations are skipped by the stack scan and the event loop keeps libuv's poll buffer free of JS values, but compiled frames are still scanned word by word. A stale word in a compiled frame (a dead spill slot, or an interpreter register saved by a JIT prologue) counts as a root. Retention only grows when such a word stays at the same address across several majors: one stale pointer to a removed node keeps every node removed after it alive, and the chain grows each major. Compiled frames rarely qualify because they are short-lived and the next call at the same depth rewrites them, so making them precise was deferred. Per-word attribution on canonical Splay (2026-09-22) found every retaining word in the interpreter's or
-
Area: error stack text — built eagerly on every throw and every
new Error()(src/errors.cjs_throw,js_build_stack_text,js_capture_stack;Ant_Exception_Raise)- Issue:
js_throwrenders the complete stack trace for any thrown value that has no stringstack(including primitives likethrow 5andgenerator.throw(x)) and hands it toAnt_Exception_Raisein case the exception goes uncaught. Error construction does the same throughjs_capture_stack(JS_STACK_TEXT_FROM_ERROR_OBJECT). Rendering walks every VM frame and formats colored, syntax-highlighted text with a source snippet, and checks the terminal (ioctl). The text is discarded whenever the exception is caught, which is the common case. A per-framegetcwdused to dominate this; it is now cached per thread againstprocess_cwd_epoch(2026-09-22), which cut a caught throw from ~18 µs to ~5.2 µs (100k caughtgenerator.throwcalls: 1.8 s → 0.52 s). What remains is the formatting itself: coloredcrsprintf(~15% of the original profile), highlighting (hl_iter_next, ~5%), terminal checks (~2%), and string building. - Impact: About 5 µs per throw and per
new Error(), paid even when nobody reads the trace. It matters for code that uses exceptions for control flow: iterator and generator protocols (throw/returninto generators), validation libraries that throw and catch per item, retry loops. A caught throw should cost well under a microsecond. - Proposed fix: Capture, render later. At throw or construction time, record only the raw frames:
sv_func_t *plus bytecode offset per VM frame, plus native frame names. That is cheap and safe to keep, because compiled functions live in the code arena and are never freed individually (see the compiled-code lifetime entry). Render the text only when it is needed: the uncaught-exception reporter, the unhandled-rejection printer, and the first read of an Error'sstackproperty (an accessor, or a lazily materialized data property, cached after the first read). KeepJS_ERR_NO_STACKand user-assignedstackstrings working as today. V8 works the same way: structured frames at capture, formatting on firststackaccess. - Validation: Byte-for-byte identical output against the current build for uncaught errors, unhandled rejections and
console.log(e.stack), colored and uncolored, before and afterprocess.chdir(the 2026-09-22 check comparedFORCE_COLOR=1traces from a nested-call script).Error.captureStackTrace,prepareStackTrace/CallSite behaviour and the error-output harness tests unchanged. Timing: 100k caughtgenerator.throwcalls and 100knew Error()in a loop, before and after, against master. - Owner: theMackabu
- Status: mostly done (2026-10-09). Errors now capture a frame record and render lazily (see per-error-capture-lazy-stack.md):
new Errorwent from about 11 µs to 0.24 µs. Primitive throws (throw 5,generator.throw(x)) still render eagerly injs_throw. The cwd cache landed first (2026-09-22).
- Issue:
-
Area:
src/modules/buffer.c/ resizableArrayBuffer(ES2024) and length-tracking views- Issue:
new ArrayBuffer(len, { maxByteLength })ignores its options;resizable,maxByteLength,resize()andtransferToFixedLength()are missing, and views cannot track a buffer's size. Related gap with the same fix mechanism: DataViewbyteLength/byteOffsetandget*/set*keep working on a detached buffer (stale offset/length, reads succeed) where node throwsTypeError(checked 2026-10-13), because the JIT-inlined DataView getters useANT_NATIVE_DETACH_NONE. - Impact: Code that feature-detects or relies on resizable buffers (streams, Wasm-style growable memory, protocol parsers) gets fixed-size buffers silently, and
resizeisundefined. - Proposed fix: Reserve
maxByteLengthinline at construction so the data pointer never moves across a resize (untouched pages are lazily zeroed; GC accounting counts the current length, not the reservation, andresizeadjustsjs->buffers.bytes). KeepTypedArrayData/DataViewDatalength/byte_length/byte_offsetas the effective values and recompute them eagerly: a resizable buffer keeps a list of its views (allocated only for resizable buffers, views unregister in their finalizers), andresizeupdates every view (length-tracking views get the new length, out-of-bounds fixed views become 0/0/0, each view keeps its declared offset/length to recover). This leaves the ~175 direct readers and the JIT's inlined size getters correct unchanged and costs nothing for fixed buffers. Add a per-viewinvalidbyte toDataViewDataand point the DataView native getter descriptors at it (ANT_NATIVE_DETACH_SELF) so inlined reads fall back to the throwing C getter when out of bounds or detached. Then: zero newly exposed bytes on grow,transferpreserves resizability,slice/transferToFixedLengthdrop it, structured clone preservesmaxByteLength, and audit the ~40 typed-array methods to treat out-of-bounds like detached. GrowableSharedArrayBuffer(grow/growable) is a follow-up on the same view mechanism. - Validation: test262
built-ins/ArrayBuffer/prototype/resize,.../TypedArray*/**/resizable*,.../DataView/**/resizable*and length-tracking cases where the harness allows, plus node differentials for shrink/regrow of fixed and tracking views, detach after resize, and JIT-warmedlength/byteLengthreads across a resize. Write an exec plan underdocs/exec-plans/active/when work starts (multi-step runtime invariant change). - Owner: theMackabu
- Status: backlog (2026-10-13; scoped, estimated ~400-600 lines mostly in
buffer.c)
- Issue:
-
Area:
src/modules/assert.c/ objectexpectedinassert.throwsandassert.rejects- Issue:
assert_exception_matcheschecks a constructor, a validator function or a RegExp, and accepts anything else without checking. Node treats an object (or an Error instance) as a property-by-property match: every own key ofexpected, plusnameandmessagewhen present, must be deep-strictly equal on the thrown value, and a RegExp value is tested against a string property. Node also rejects an empty object withERR_INVALID_ARG_VALUE. Example:assert.throws(fn, { code: 'ERR_X' })passes in Ant whateverfnthrows. - Impact: Tests that use the object form verify nothing, so wrong error shapes go unnoticed.
tests/test_worker_post_message_to_thread.cjsrelies onassert.rejects(..., { name: 'DataCloneError' }). The RegExp and validator gaps hid two real bugs (a wrongtest_ant_c.cjsexpectation and a missingERR_INVALID_ARG_TYPEcode inwire.c), so expect this one to surface similar failures once it is fixed. - Proposed fix: Add the object branch to
assert_exception_matches, after the RegExp check and before the callable check. Fail when the thrown value is not an object, then compare each key ofexpectedagainst the thrown value with the existingjs_deep_equal(..., true), testing a RegExp value against a string actual withassert_regexp. Report failures as anAssertionErrorwith a+ actual - expecteddiff of the compared keys, using the operator passed in (throwsorrejects). ThrowERR_INVALID_ARG_VALUEfor an empty object. - Validation: Node differential for matching and mismatching keys, nested objects, RegExp values, Error instances as
expected, non-object thrown values, the empty object, and the custom-message argument, for boththrowsandrejects. Run every test that passes an object asexpectedand fix any expectation that turns out to be wrong instead of loosening the check. - Owner: theMackabu
- Status: backlog (2026-10-21; RegExp and
rejectsmatching landed first)
- Issue: