# test262 (conformance ECMA-262) ## Cómo correrlo ``` ./scripts/setup-test262.sh # clona tc39/test262 en tests/test262/ (gitignored) TZ=UTC cargo test -p vvva_test --test test262 -- --ignored --nocapture ``` En CI lo ejecuta `.github/workflows/deep-checks.yml` cada lunes (y bajo demanda). El test es un *trinquete*: `NON_MODULE_KNOWN_FAILURES` (2244 al 2026-10-21, con `TZ=UTC`) e `INTL402_KNOWN_FAILURES` (72) fijan la línea base. Si el número de fallos sube es una regresión; si baja hay que bajar la constante y actualizar esta página. **La zona horaria importa.** Los tests `built-ins/Date/S15.9.3.1_A5_T1..T6` (12 resultados, sloppy y strict) fallan fuera de UTC, así que el mismo commit dio 2654 en un portátil en CST y 2642 en el runner de CI. Por eso el test se niega a correr sin `TZ=UTC` y el workflow lo fija. Para comparar dos ejecuciones, `TEST262_FAILURES_OUT=archivo.txt` escribe la lista completa de fallos (el test solo imprime los 50 primeros por suite) y el workflow la sube como artefacto `test262-failures`; basta un `diff` de las dos listas ordenadas. El runner vive en `crates/test/src/test262.rs`. Lee el frontmatter YAML-ish de cada test a mano (sin dependencia de `serde_yaml`), arma el harness (`assert.js`, `sta.js`, `includes`, `$262`), y lo corre contra `vvva_js::JsEngine`. ## Estado de cobertura (por área) | Área | Tests (aprox.) | Estado | |---|---|---| | Sync (`language/`, `built-ins/` sin flags especiales) | ~44,000 | Soportado | | `flags: [async]` (`$DONE`) | 5,624 | Soportado — ver `JsEngine::pump_timers` | | `flags: [module]` | 832 | Soportado parcialmente (~62% pasan) — ver nota abajo | | `$262.createRealm` | 284 | Soportado — ver `JsEngine::install_test262_realm_support` | | `$262.agent` | 112 | Soportado (112/112) — ver `JsEngine::install_test262_agent_support` | | `intl402/` (ECMA-402, `Intl.*`) | 3,357 | **Soportado: 6,642/6,714 (~99%)** — ver abajo | ### Módulos ES (`flags: [module]`) 3va no usa `v8::Module` nativo — los módulos se ejecutan vía el pipeline existente de `transpiler::transpile_to_cjs` + `require()`. Esto cubre bien import/export básico, bindings, namespace objects, pero **no puede** replicar fielmente la semántica de "early errors" de linking entre módulos (bindings duplicados vía `export *`, ciclos de importación, etc.) — eso requeriría `v8::Module` real. Los fallos en esa categoría son reales y se reportan tal cual (no se esconden con skip) para que el número refleje la cobertura real. ### Fallos aceptados: prioridad Medido el 2026-10-12 (commit de test262 fijado, V8 150, `TZ=UTC`): **2501 entradas fallidas** (language 1219, built-ins 1210, intl402 72). Antes de este PR eran 3081 (language 1799). Cada test sin `onlyStrict`/`noStrict` cuenta dos veces (sloppy + strict), así que las cifras son entradas, no archivos. La lista completa se obtiene con `TEST262_FAILURES_OUT=fallos.txt cargo test --release -p vvva_test --test test262 -- --ignored`. Los conteos por grupo salen de la lista completa; la causa de cada grupo está inferida del nombre del directorio y de comprobar qué APIs trae el binario, no verificada test a test. Criterio: P0 = rompe código de aplicación real; P1 = semántica de módulos/async que las apps pueden tocar; P2 = rarezas del lenguaje; P3 = huecos atados a la versión de V8 o a los datos de ICU. **No todo P3 es una propuesta ausente**: el binario ya trae `Error.isError`, `Atomics.waitAsync`, `Temporal` e `Iterator.concat`, y esos grupos fallan solo en una parte. Se resuelven al subir V8, no a mano en 3va. | Prio | Grupo | Entradas | Nota | |---|---|---|---| | P0 | `language/module-code/top-level-await` | 201 → 14 | **Atacado.** El runner no forzaba ESM en módulos sin `import`/`export` (solo `await`): V8 los veía como scripts. Se marca con `export {}` (solo tests positivos) y el transpiler ya re-exporta `export var/let/const { … }` y `export default class extends …`. Quedan los ticks de resolución/rechazo entre módulos hermanos, `dfs-invariant`, `async-module-does-not-block-sibling-modules` y `new-await`. | | P0 | `language/expressions/dynamic-import` (todo el subárbol) | 959 → 583 | **Atacado.** 3va no usaba `v8::Module`, así que V8 no tenía *host callback* para `import()` en scripts clásicos (`Error: Not supported`). Ahora `JsEngine` instala `set_host_import_module_dynamically_callback` y delega en `__importAsync` (mismo cargador CJS). El runner da identidad de fichero (`__dirname`) y permiso de lectura. De los 583 restantes, **~290 no son arreglables** sin subir V8: `syntax/valid` (160) y 128 de `catch/` usan `import.source`/`import.defer`. Arreglables: `catch/` 75, `usage/` 72, `syntax/invalid` 40; `namespace/` (84) necesita `v8::Module`. | | P1 | `language/module-code/namespace`, `ambiguous-export-bindings` | 28 | Namespace exótico y early errors de enlace: requieren `v8::Module` real (ver arriba). Sin cambios. | | P1 | `language/import/import-attributes` + `module-code/import-attributes`, `import-bytes` | 54 → 11 | **Parcial.** Fixtures JSON/text se copian al tmpdir y el *unwrap* `.default` ya no revienta con `null`. Quedan módulos de texto/bytes (`type: 'text'`/`'bytes'`), `via-namespace` e idempotencia. | | P2 | `language/eval-code` | 94 | `arguments` en `eval` directo dentro de funciones async/generadoras. | | P2 | `language/expressions/compound-assignment` + `*crement` + `assignmenttargettype` | 96 (66 + 20 + 10) | Orden de evaluación / errores tempranos en asignaciones. | | P2 | `language/statements/class`, `expressions/class` | 45 | Casos de borde de clases. | | P2 | `language/global-code` | 19 | Declaraciones e inicialización de script global. | | P2 | `language/statements/with`, `await-using` | 8 + 6 | Rarezas; `await using` depende de la propuesta de recursos. | | P3 | `built-ins/Iterator` | 404 | `zip` 70 + `zipKeyed` 84 (no existen en el binario) y `includes` 80, `windows` 70, `chunks` 68, `join` 32 (helpers nuevos; no comprobado si existen). `Iterator.concat` ya está. | | P3 | `built-ins/Promise` (`allKeyed`, `allSettledKeyed`) | 182 | Propuesta nueva (no existe en el binario). | | P3 | `built-ins/ShadowRealm` (+ `AbstractModuleSource` 8) | 124 | Propuesta nueva (no existe en el binario). | | P3 | `built-ins/Atomics` | 114 | `Atomics.waitAsync` ya está en el binario: fallan sobre todo `waitAsync`/bigint, más `notify` y `wait`. Posible arreglo en 3va (`$262.agent`), no confirmado. | | P3 | `language/import/import-defer` | 94 | `import defer`, propuesta nueva. | | P3 | `built-ins/RegExp/property-escapes` | 164 | Versión de Unicode de V8 vs la de test262. | | P3 | `built-ins/ArrayBuffer`, `DataView`, `TypedArray`, `Uint8Array`, `Error/prototype`, `Function/prototype`, `Temporal` | ~200 | `ArrayBuffer.prototype.immutable` (no existe en el binario). `Error.isError` ya está: `Error` falla solo en `stack` (70). `Temporal` ya está: sus 18 fallos son casos `float64-representable-integer`. | | P3 | `intl402/` | 72 | Conjunto fijo de datos ICU, ver sección siguiente. | Efecto del PR #54: pasan 6 tests que fallaban (`instn-named-id-name`, `instn-star-id-name`, `await-dynamic-import-resolution`, `dynamic-import-resolution`, `module-import-resolution`, `module-import-unwrapped`) y ninguno de los que pasaban falla. Neto: 3087 → 3081. Efecto de este PR (P0/P1 de la tabla): **3081 → 2501 entradas** (language 1799 → 1219; el gate no-módulo baja de 2642 a **2249**). Fijos por grupo: `top-level-await` 201 → 14, `dynamic-import` 959 → 583, `import-attributes` 54 → 11. Una única regresión, en `module-code/` (no cuenta para el gate): `source-phase-import/import-source.js`, que solo pasaba porque `import()` aún no funcionaba (rechazaba al instante); ahora el `import()` real evalúa los fixtures de *source phase*, propuesta que V8 150 no soporta. Se documenta, no se esconde. Los 160 `dynamic-import/syntax/valid` y los `import.source/defer` siguen fallando por **sintaxis que V8 150 no reconoce** (`Unexpected identifier 'source'/'defer'`): no son arreglables en 3va sin subir V8. Igual que el resto de P3. ### P1 namespace, P2 y P3: qué no se toca y por qué - **P1 `namespace` / `ambiguous-export-bindings`**: sin cambios. Los objetos *namespace* exóticos y los *early errors* de enlace (`export *` ambiguo, ciclos) requieren registros de módulo reales (`v8::Module`), que 3va no usa (sección "Módulos ES"). El pipeline CJS puede acercarse, pero nunca satisface la identidad intrínseca (`Symbol.toStringTag === 'Module'`, prototipo nulo, *live bindings*): se reportan tal cual, no se esconden con skip. - **P2** (`eval-code/direct`, `compound-assignment`/`*crement`/`assignmenttargettype`, clases): no abordados. Son casos de borde del parser/evaluador de V8 o del orden de evaluación, ajenos a la ruta ESM; siguen en la línea base. - **P3**: huecos de V8 150 (`Iterator.zip/zipKeyed`, `Promise.allKeyed`, `ShadowRealm`, `import defer`, `import source`, `ArrayBuffer.prototype.immutable`, property-escapes de Unicode posterior). `Atomics.waitAsync`, `Error.isError`, `Temporal` e `Iterator.concat` **sí existen** y sus fallos son parciales. No se implementan a mano: al subir el pin `v8 = "150.0.0"` a una versión que las traiga pasarán solas, y entonces hay que bajar las constantes de `crates/test/tests/test262.rs`. 2.12.2 (grafo de módulos asíncronos: un importador estático espera a la dependencia que sigue en su `await` de nivel superior, también en ciclos): **2501 → 2497 entradas**, gate no-módulo 2249 → **2247**, `module-code/` 180 → 178. Pasan `module-async-import-async-resolution-ticks`, `module-sync-import-async-resolution-ticks` y `dynamic-import/import-fulfilled-member-of-errored-cycle` (sloppy + strict); ningún test que pasaba falla. Las cifras por grupo de la tabla son las de 2.12.1. 2.13.0 (cargador CJS/ESM: `import()` dinámico nativo y detección de ESM que ya no marca por `import()`): gate no-módulo **2247 → 2244**, `intl402` 72 (sin cambios), `module-code/` 178 → 181. Total sin cambios (2497): los 3 que bajan del gate no-módulo son `dynamic-import` que ahora resuelven por el host de V8, y 3 casos de `module-code/` pasan a contarse ahí (el contador los clasifica por ruta, no por la detección de 3va). Ningún test que pasaba falla en el gate. Cómo avanzar: atacar P0 primero, siempre con un test de regresión en `crates/js/tests/` (test262 mide el número, no sustituye a un test con módulos reales; ver `esm_tla.rs`). Los fallos de `module-code/` no entran en el contador `NON_MODULE_KNOWN_FAILURES`, así que una regresión ahí no rompe el gate: comparar la lista completa antes/después. ## `intl402/` — Intl.* completo vía V8 nativo + shim pequeño 3va embebe V8 **150.0.0** (crate `v8`), y ese V8 ya trae el `Intl` de ECMA-402 casi completo (ICU): `Intl.DateTimeFormat`, `Intl.NumberFormat`, `Intl.Collator`, `Intl.PluralRules`, `Intl.Locale`, `Intl.ListFormat`, `Intl.RelativeTimeFormat`, `Intl.DisplayNames`, `Intl.Segmenter`, `Intl.DurationFormat` y `Temporal`. La suite `intl402/` (3,357 archivos) pasa **6,642 / 6,714 (~99%)** con la caja. Los ~72 fallos restantes son casos límite de **versión de ICU/CLDR** (V8 está pineado a 150 → determinista). Dos de los bajos-costos se parchan con un shim JS chico inyectado al startup, `crates/js/src/builtins/intl.rs`: 1. `String.prototype.toLocaleLowerCase/UpperCase` validan **todos** los identificadores de locale (`Intl.getCanonicalLocales` lanza `RangeError`). 2. `Intl.getCanonicalLocales` conserva `yes` para las claves de extensión no booleanas (`ka`/`kf`/`kr`/`ks`/`kv`) — solo las booleanas lo descartan. **Restricción dura:** los objetos `Intl.*` no se pueden reemplazar a nivel JS, porque test262 valida la identidad intrínseca (`name`, `length`, descriptor no enumerable, no-constructor sin `prototype`, `[[FallbackSymbol]]`). Los reemplazos del shim son arrows/funciones-no-constructibles con el mismo `name`/`length`/descriptores. El runner lo amarra con `INTL402_KNOWN_FAILURES = 72`. Los fallos conocidos restantes, todos por datos de ICU (no por código 3va): - `DateTimeFormat`: canonicalización de timezones que ICU fusiona (`Canberra`→`Sydney`, `Calcutta`→`Kolkata`, `Etc/GMT`→`UTC`, case-insensitive de `America/Argentina/Buenos_Aires`), timezone legacy non-IANA (`ACT`), offset con U+2212, fallback `islamic`/`islamic-rgsa`, eras únicas de `islamic-civil`, `hourCycle` por defecto h11/h12 y `Temporal`+era. - `Temporal/*/toLocaleString`: era / hourcycle / `datestyle` (~30). - `Intl.supportedValuesOf`: requiere que ICU soporte `Etc/GMT+13/+14` (no lo soporta), calendars `islamic` sin fallback válido, y numbering systems con digit-mapping simple. - `Locale.getHourCycles`: `en-u-sd-gbeng` debería dar h23 (sigue h12). - `DurationFormat` digital-style/precision, `FallbackSymbol/per-realm`, `NumberFormat/prototype/format/numbering-systems`. Workflow local: para iterar solo sobre `intl402/` sin correr los ~44k de `language/`+`built-ins/`, recompilar y usar el helper `./target/debug/intl402run tests/test262 intl402/...` (bin temporal en `crates/test/src/bin/intl402run.rs`). ## `$262.createRealm` y `$262.agent` Ambos implementados en `crates/js/src/lib.rs` y expuestos vía `JsEngine::install_test262_realm_support` / `install_test262_agent_support` (llamados desde `crates/test/src/test262.rs` antes de correr cada caso). - **`createRealm`** crea un `v8::Context` nuevo dentro del MISMO isolate (no un `JsEngine` nuevo) — intrínsecos propios (su propio `Array`, `Error`, etc.), y comparte el "security token" del contexto que lo pidió para que el acceso cross-realm a `realm.global.X` no dispare el `TypeError: no access` que V8 lanza por defecto entre contextos con tokens distintos. Verificado: `built-ins/Boolean` 99/2 → 101/0, `language/types` 207/4 → 211/0, `built-ins/Proxy` 607/0, `built-ins/Symbol` 192/0. - **`agent`** es harina de otro costal: cada agente corre en un hilo de SO real con su PROPIO isolate (los isolates de V8 no son `Send`, así que no se puede simular con tareas async como hace `createRealm`). Lo único que cruza threads es el backing store del `SharedArrayBuffer` — V8 ya implementa `Atomics.wait`/`notify` correctamente entre isolates una vez que comparten el mismo backing store, así que no hubo que reimplementar esa sincronización, solo la plomería (`$262.agent.start/broadcast/ getReport/report/receiveBroadcast/leaving/sleep/monotonicNow`). Verificado: **112/112** tests de `$262.agent` pasan (todos viven en `built-ins/Atomics`). ## Notas del motor que salieron de este trabajo - `JsEngine::eval()` ahora usa `v8::TryCatch` y devuelve el error real de V8 con fase: `"[parse] SyntaxError: ..."` o `"[runtime] TypeError: ..."`. Antes devolvía strings genéricos (`"compile error"` / `"execution error"`) sin el mensaje real — esto mejora los errores reportados en todo el motor, no solo en test262. - `JsEngine::pump_timers()` — `idle()` solo drena microtasks y tareas de platform de V8, nunca avanza la rueda de `setTimeout`/`setInterval`. Un loop de test (o cualquier consumidor que necesite esperar timers reales) debe llamar `pump_timers()` explícitamente.