fix: zera os vermelhos conhecidos do differential (12/13) e fixa o oráculo no primary - #14
Merged
Merged
Conversation
An island-rest signature SPELLS its trailing engine-array parameter, so its params list already carries that jsval slot AND the type is marked rest. The dyn-boxed call thunk read both literally: it filled every param positionally — handing the trailing slot the first surplus ARGUMENT where the closure expects the pack — and then appended an extra dyn rest array the callee has no parameter for. So `const f = (...args) => args.length; f(1, 2)` in a --dynamic .js program threw "expected number, got undefined": a module-level arrow is a dyn global, so the call routes through this thunk rather than the direct path, and `args` was bound to the number 1. Every rest-forwarding and engine-value-through-rest idiom failed the same way (2568's very first call, 2590's Object.create prototype). The thunk now fills only the LEADING params positionally and builds the trailing slot with scr_jsval_rest_from_dyn — the surplus dyn arguments marshalled into one fresh engine array, the same pack the direct call builds inline (jsOp arrLit) and isl_hostfn_invoke builds for a closure entering the island as a host function. Fixed in the C and LLVM emitters; the Rust backend already sliced the trailing slot off correctly. 2859 pins the ABI minimally on all three lanes. Claude-Session: https://claude.ai/code/session_0197JoEpMBBqqkSiBb2vNX5A
The LLVM emitter's jsval-receiver optChain arm handled only a void body and an engine-valued result, and threw an InternalCompilerError for anything else. A chain step that lands back in the STATIC world — `flatValue(text, key)?.trim()`, a package's optional string through an island handle — answers `string | undefined`, so 2716 could not compile at all on the LLVM lane. Give the arm the same two shapes the C emitter already has: an engine result takes the engine's undefined cell, a union result takes that union's interned undefined arm. An unmodelled result kind now raises LlvmUnsupportedError (a backend-coverage fence) rather than claiming an emitter bug. Claude-Session: https://claude.ai/code/session_0197JoEpMBBqqkSiBb2vNX5A
…tcome
`.finally` over a checked-dynamic promise dropped its callback's result
outright — a documented shortcut ("a finally callback returning a promise
would delay adoption; that refinement waits for a use"). Two things
followed in 2210:
- A cleanup promise that REJECTED never reached the chain. The rejection
escaped the reaction fiber entirely and the binary died reporting an
unhandled rejection, where JS replaces the source settlement with it —
so `.finally(() => cleanupFails()).catch(...)` never fired.
- Not awaiting a cleanup that FULFILLS also settled the chain too early,
so `finally kept 7` overtook a longer chain's `finally ran`. The
ordering was a symptom of the same missing await, and falls out with it.
The reaction now walks a promise result the way the .then arm already
does: a cleanup rejection rejects dst (dropping the source's caught
record), a fulfillment is discarded and the source settlement passes
through, and a non-thenable result behaves exactly as before.
Claude-Session: https://claude.ai/code/session_0197JoEpMBBqqkSiBb2vNX5A
Three independent divergences in the island's own web globals, all read by
corpus 1120 through __island_eval against Node's real implementations.
Astral percent-encoding. formDecode walked the raw query by UTF-16 CODE
UNIT and handed each one to TextEncoder, so a non-BMP character arrived as
a lone high surrogate then a lone low surrogate and became two U+FFFD
before ever reaching toString() — `new URLSearchParams('x=<U+1F600>')`
serialized as %EF%BF%BD%EF%BF%BD instead of %F0%9F%98%80. The literal
branch now pairs a high surrogate with its low surrogate; a genuinely lone
surrogate still replaces. (The sequence-init path was already correct,
which is why only the parsed spelling failed.)
Pair-iteration liveness. entries/keys/values were generators over
this._pairs and forEach iterated a slice() — both snapshots. WebIDL pair
iteration is LIVE: it holds the object plus a positional index and
re-reads the current list each step, so appending from a forEach callback
re-enters for the new tail, a mid-iteration delete skips forward over the
hole, and a mid-iteration sort can re-yield a pair that moved past the
cursor. All four of 1120's mutation ladders were wrong, forEach included.
btoa/atob rejections. The prelude's own invalidChar built a plain Error
and stamped .name, leaving .code undefined and `instanceof DOMException`
false where Node answers InvalidCharacterError with the legacy code 5. It
now throws the DOMException the same prelude already defines, matching the
static tier's scr_btoa/scr_atob.
The Rust island twin (island_web.js) already carried the surrogate pairing
and the DOMException; it gets the liveness fix so both islands stay
behaviourally identical. No vendored file is touched.
Claude-Session: https://claude.ai/code/session_0197JoEpMBBqqkSiBb2vNX5A
2084 printed the message of a TypeError thrown INSIDE the island engine.
quickjs-ng words its engine-internal errors its own way ("not a number"
where V8 writes "Number.prototype.toFixed requires that 'this' be a
Number"), so that text can never match the Node oracle.
It is also not ours to align: the vendored engine is an unmodified
upstream snapshot by policy, and its prebuilt libqjs.a is cached by
upstream commit, so a local edit to quickjs.c would not even key the cache
correctly. Pin the error TYPE instead, which is the actual contract — the
receiver rules reject identically on both sides.
Documented in the dynamic-tier limits: match on error type, not message
text.
Claude-Session: https://claude.ai/code/session_0197JoEpMBBqqkSiBb2vNX5A
The three differential lanes still resolved their oracle with nodeOracleExecutable(), which follows the HOST. node-matrix.ts landed the distinction they need and says so in its own docstring: a census follows the host, but a SEMANTIC oracle pins to the primary, because a compiled binary reproduces one Node's observable behavior and cannot reproduce two. On a Node 26 host that mismatch turned six corpus programs red for reasons that say nothing about the compiler: - 1746 and 2813 — read() with no size stopped concatenating the internal buffer in 26.0.0 (nodejs/node#60441, semver-major), and the async iterator inherits it. - 1640 — v26 validates `position` even when the read window is empty; v24 short-circuited first. - 2631 — builtinModules.length moved from 72 to 66. - 2599 — v26 dropped stream_base's 'buffer' encoding special case. - 1967 — v26 removed --experimental-transform-types, so the harness fell back to the tsc hook, which ELIDES an import= alias of an uninstantiated namespace where the native transform emitted `var P = T` and threw. All six pass unchanged against the primary. Pinning keeps their assertions intact rather than deleting the version-dependent half of each one, and SCRIPTC_NODE_ORACLE still overrides — which is how you go looking for these divergences deliberately instead of tripping over them. Claude-Session: https://claude.ai/code/session_0197JoEpMBBqqkSiBb2vNX5A
socket.write(string, encoding) lowered only three shapes: the literal
'buffer' (Node's stream_base special case), the utf8 spellings, and a
Buffer chunk that ignores the encoding. Every other literal encoding fell
through to the "write with 2 arguments" fence, so a program Node answers
with a plain TypeError failed to compile at all.
An encoding Node does not know is its synchronous ERR_UNKNOWN_ENCODING,
raised before anything is written. Known but not-yet-lowered spellings
('hex', 'base64', ...) keep the fence rather than silently writing the
wrong bytes.
2599 gains a rung for it beside the existing 'buffer' one, which stays.
Claude-Session: https://claude.ai/code/session_0197JoEpMBBqqkSiBb2vNX5A
This was referenced Sep 2, 2026
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.
Fecha 12 dos 13 vermelhos conhecidos do differential. O 13º (2794) fica com diagnóstico completo: não é bug, é uma porta de backend que falta.
A descoberta que organiza metade do trabalho
Seis dos treze não eram regressão do scriptc — era o oráculo seguindo o HOST. As três lanes de differential ainda resolviam o oráculo com
nodeOracleExecutable(), que segue o host, e esta máquina roda Node 26. O.node-versiondo repositório fixa 24.15.0, que é também oprimarydoNODE_COMPAT_MATRIX— ou seja, esses seis só ficam vermelhos numa máquina que roda um Node diferente do que o repositório fixa.O
node-matrix.tsque acabou de entrar já descreve exatamente essa distinção, e a docstring dele diz o que fazer: um censo segue o host, mas um oráculo SEMÂNTICO fixa no primary, porque um binário compilado reproduz o comportamento observável de UM Node e não consegue reproduzir dois. A infra entrou; as lanes de differential é que ainda não tinham sido migradas.Migrar as três (
primaryOracleExecutable(NODE_COMPAT_MATRIX)) faz os seis passarem sem tocar em nenhuma fixture, e faz uma execução local reproduzir o oráculo do CI em vez de depender do Node que estiver instalado.Cheguei primeiro pelo caminho errado — normalizei as seis fixtures para serem estáveis nos dois majors — e refiz depois de ler o node-matrix: aquela abordagem apagava justamente a metade version-dependent de cada asserção, que é o que fixa a semântica do primary. Fixar o oráculo preserva a cobertura inteira, e
SCRIPTC_NODE_ORACLEcontinua sobrescrevendo — que é como se vai ATRÁS dessas divergências de propósito, em vez de tropeçar nelas.Confirmado empiricamente em
origin/mainlimpo, sem nenhuma mudança minha, com o oráculo no primary: exatamente esses seis passam, exatamente os seis bugs reais continuam falhando.Tabela: fixture → causa-raiz → veredito
read()semsizeparou de concatenar o buffer interno (nodejs/node#60441, semver-major, 26.0.0); o async-iterator herdapositionmesmo com janela de leitura vazia; v24 fazia curto-circuito antesbuiltinModules.lengthmudou de 72 para 66'buffer'— e revelou que não havia lowering geral dewrite(string, encoding)--experimental-transform-types, então o harness caía no hook tsc, que ELIDE o alias onde o transform nativo emitiavar P = Te estourava.finallydescartava a promise do callback — rejeição escapava como unhandledrl.nextLinesó existe no backend Rust; o emissor C recusa de propósitoOs bugs reais
ABI do islandRest (2568, 2590) — o mais grave. Uma assinatura island-rest SOletra seu parâmetro final (array do engine) na lista de params e é marcada
rest. O thunk de chamada boxed lia as duas coisas literalmente: preenchia todo param posicionalmente — entregando ao slot final o primeiro argumento EXCEDENTE onde a closure espera o pacote — e ainda anexava um array dyn extra que o callee não tem parâmetro para receber.const f = (...args) => args.length; f(1, 2)num programa--dynamicestourava comexpected number, got undefined, porqueargsera o número1: uma arrow no topo do módulo é um global dyn, então a chamada passa por esse thunk e não pelo caminho direto. Todo idioma de rest-forwarding sob--dynamicquebrava assim. Corrigido no emissor C e no LLVM (o Rust já fatiava certo);2859-island-rest-boxed-call.jsfixa a ABI de forma mínima nas três lanes..finallysobre promise dyn (2210). O resultado do callback era descartado — atalho documentado no próprio código ("that refinement waits for a use"). Uma cleanup que REJEITA nunca chegava à cadeia: escapava da fiber de reação e o binário morria com unhandled rejection, onde o JS substitui a settlement. E não esperar uma cleanup que RESOLVE settlava a cadeia cedo demais, o que era a causa real definally kept 7passar na frente definally ran— a ordem era sintoma do mesmo await faltando.Web globals da island (1120).
formDecodeandava por CODE UNIT do UTF-16 e entregava surrogates soltos ao TextEncoder, então astral virava dois U+FFFD antes de chegar aotoString();entries/keys/values/forEachtiravam snapshot, quando a iteração de pares do WebIDL é VIVA (índice posicional relendo a lista atual — as quatro escadas de mutação estavam erradas,forEachincluído); ebtoa/atoblançavamErrorcarimbado em vez doDOMExceptionque o próprio prelúdio já define. Nada sobvendor/foi tocado — a política do vendor README proíbe, e olibqjs.aé cacheado por commit upstream.optChain island → union (2716). O braço jsval do emissor LLVM só tinha resultado void e resultado do engine, e lançava
InternalCompilerErrorno resto. Um passo que aterrissa de volta no mundo ESTÁTICO (flatValue(...)?.trim()) respondestring | undefined. Ganhou as mesmas duas formas que o emissor C já tinha; um kind não modelado agora viraLlvmUnsupportedErrorem vez de alegar bug do emissor.write(string, encoding)em socket (2599). Só três formas eram lowered; qualquer outra encoding literal caía na cerca "write with 2 arguments" e o programa nem compilava. Uma encoding que o Node não conhece agora é oERR_UNKNOWN_ENCODINGsíncrono dele. Spellings conhecidas mas ainda não lowered (hex,base64) mantêm a cerca em vez de escrever bytes errados. A fixture ganhou um degrau para isso ao lado do'buffer', que fica.2084 é divergência de motor, não bug: o erro é lançado dentro do quickjs-ng, que escreve
not a numberonde o V8 escreveNumber.prototype.toFixed requires that 'this' be a Number. O vendor não é nosso para alinhar, então a fixture passa a fixar o TIPO do erro (que é o contrato real) e a limitação está escrita nos dynamic-tier limits.Estado do gate: 13 vermelhos herdados do main
O differential C completo termina com 14 falhas: 2794 (acima) e 13 que já estão vermelhas no
main, todas verificadas emorigin/mainlimpo, sem nenhuma mudança minha, contra o Node que o repositório fixa.Sete regridem em
e313ba28. Bisect com build + differential em cada ponto:c6aef3e3f7fa8e2fe313ba28feat: bridge Rust dynamic module interfacesb58549fe…16268662São 1559-conditional-spread-index-merge, 1562-optional-chain-tails, 1575-unknown-assert-into-record, 1576-width-coercions, 2047-objlit-accessors-shapes, 2464-qs-require-forms e 2678-util-parseargs. O sintoma é sempre leitura de propriedade virando
undefined(app: dev 5173→app: none 0;127.0.0.1 3000 yes→undefined undefined undefined). Não é sensibilidade de versão: falham também com o oráculo no primary.e313ba28mexeu em lowering do frontend (lower-calls.ts,lower-exprs.ts,lower-island-interface.ts) numa feature mirada no Rust e regrediu a lane C de referência.Quatro são de tuplas/destructuring: 540-tuples-basics, 1432-destructured-params, 1572-promise-reject-all-tuple, 2575-string-destructuring-decl. Falham nos dois oráculos, e no
mainatual sem nada meu.Duas são de stream, e valem uma olhada à parte: 2845-readable-paused-read-boundaries e 2846-readable-unshift-order falham contra o primary — e passam contra Node 26. Elas fixam a semântica NOVA do
read(): depois deread(3)devolverhel, oread()seguinte respondelo(só o resto do primeiro chunk) em vez dolo worldque o Node 24 concatena. Isso expõe uma incoerência interna do runtime: o async-iterator concatena (semântica pré-26, o que 1746/2813 fixam) enquanto oread()pausado devolve um chunk por vez (semântica 26). As duas já estão vermelhas contra o Node que o repositório fixa; o oráculo seguindo o host é que escondia isso. Não mexi — escolher qual das duas semânticas o runtime implementa é decisão de vocês, e é a mesma decisão do follow-up abaixo.Nenhum desses 13 é regressão deste PR.
Issue: portar
rl.nextLinepara o runtime C2794 não é regressão — é cobertura de backend faltando, e o emissor C já declara isso no código ("The native async-iterator slice currently belongs to the Rust runtime").
for await (const line of rl)baixa para o libCallrl.nextLine(Promise<string | undefined>), implementado só embackend/rust/readline.ts. A lane LLVM falha pelo mesmo erro porque cai no fallback C — então uma implementação em C resolve as duas lanes.O que falta:
packages/runtime/src/scr_readline.c— o slot de callback pendente dequestiontem quase a forma certa, mas falta o modo "próxima linha OU fim": hojescr_rl_settle_closeDESCARTA um callback pendente (aquestiondo Node nunca responde), enquantonextLineprecisa resolverundefinedno fim do stdin, e resolver de novo em toda chamada seguinte para o laçofor awaitterminar. Uma linha já bufferizada tem de responder na hora, comoquestionfaz viascr_rl_drain.packages/compiler/src/backend/emission/emit-exprs.ts— o casorl.nextLinecriascr_promise_new()e passa um adaptador internado por union que montastring | undefinede cumpre a promise. O precedente exato já existe:scr_promise_race_add(result, p, &adapter)comE.raceAdapterFor.O risco não é o tamanho, é a corretude assíncrona: manter o event loop vivo enquanto um
nextLineestá pendente, resolverundefinedexatamente uma vez no EOF, ecrlfDelay: Infinity. Preferi deixar diagnosticado a entregar meia implementação de I/O assíncrono.Follow-up: decidir a semântica de
read()O
read()semsizeconcatena o buffer inteiro no caminho do async-iterator — semântica pré-26, e a que a documentação do Node ainda descreve — mas o caminho pausado devolve um chunk por vez, semântica da #60441. Como o binário reproduz UM Node, essa divisão é um bug em si, independente de qual lado se escolha: 1746/2813 fixam um lado e 2845/2846 o outro.Gates
-tnas corrigidas + 2716-tnas tocadascargo test(runtime-rust)pnpm lintpnpm build🤖 Generated with Claude Code
https://claude.ai/code/session_01Bab3v6PNzMBUq7nJhLR8i7