Novedades

Novedades

Las últimas versiones, de la más nueva a la más vieja, copiadas del CHANGELOG del repositorio cada vez que se genera el sitio. Cada entrada dice qué cambió y por qué.

0.38.0 2026-10-26

MINOR: API nueva para procesar datos tabulares: el lector columnar try_csv_read de std/csv y string_to_X_range. Además, un entero que no entra en 64 bits pasa a ser inválido en vez de saturarse: string_to_int sobre un número así ahora aborta (antes daba INT64_MAX en silencio).

Migración: si tu código valida que el texto sean solo dígitos y después llama a string_to_int, un número de 19+ dígitos que llega de afuera ahora aborta el proceso (antes se saturaba). La salida es string_to_int_or(s, def) o rechazar el desborde antes. Afecta hoy a nyx-kv (INCRBY, LRANGE, SCAN, SET … EX, INCR), nyx-db (LIMIT/OFFSET), nyx-proxy (max-age) y el webhook de kv: no recompilarlos con 0.38.0 hasta que tengan su arreglo (fichado en TASKS.md).

Agregado

  • string_to_int_range(s, ini, fin, def) / string_to_float_range(s, ini, fin, def): parsean el número de los bytes [ini, fin) de s sin crear el substring; valen siempre lo mismo que string_to_X_or(s.substring(ini, fin), def). Motivo: en un CSV de 5M filas, split() + string_to_float por fila hace ~11 asignaciones GC por fila y se lleva la mitad del tiempo (bench 2026-10-25 contra Python/pandas/polars). Antes se probó pre-dimensionar el Array de split() y se descartó: −4% en el split gigante, +4..9% en los cortos. +1 test (507), receta examples/by-example/145-csv-sin-split.nx.
  • std/csv: lector columnar try_csv_read(texto, esquema, opts): parsea en una pasada directo a columnas tipadas (int, float, str, y cat = códigos + diccionario), con el núcleo en el nuevo runtime/csv.c (RFC 4180: comillas, saltos de línea entre comillas, CRLF, BOM). Estricto: un dato inválido es Err con fila y columna. try_csv_guess_schema propone el esquema. En el bench de 5M filas agrupar y filtrar tarda 0.93 s y 265 MB, contra 1.21 s de pandas y 1.19 s de polars con un hilo (con split() + Map: 5.5 s). +1 test (509), +1 suite C (test_csv.c), receta 146-csv-columnar.

Cambiado

  • Un entero que no entra en 64 bits es inválido, no se satura: string_to_int aborta con un mensaje propio, string_to_int_or/_range devuelven el default y una columna int de try_csv_read da Err («no entra en un int»). Antes strtoll lo dejaba en INT64_MAX/MIN y nadie miraba ERANGE: string_to_int_or("99999999999999999999", 0) daba 9223372036854775807 en silencio (decisión de Ottavio). Consecuencias que se arreglaron con el cambio: std/json guarda un entero así como "float" con su texto (antes lo saturaba; con el cambio habría abortado el proceso por un número llegado de afuera), y el intérprete/REPL aplica la misma regla NYX2015 que el compilador: -9223372036854775808 da INT_MIN exacto (daba -9223372036854775807) y un literal que no entra es error (daba INT_MAX). +2 tests (510, 511), +1 suite C (test_int_parse.c), +2 checks en el smoke del REPL.
  • string_to_float / _or / _range parsean con el fast path de Clinger: un número [+-]dígitos[.dígitos] de hasta 15 dígitos se convierte con una división exacta, que da el mismo double que strtod bit a bit; el resto (exponentes, 16+ dígitos, inf, hex, espacios) sigue yendo a strtod. Verificado contra strtod en 5M valores aleatorios y en los bordes (test_float_parse.c). En el bench de 5M filas, indexOf + _range baja de 2.67 s a 2.13 s. +1 test (508), +1 suite C.

0.37.1 2026-10-24

PATCH: los tres reportes de fricción de nyxerp del 2026-10-22.

Corregido

  • NYX1046: un patrón de enum (Result.Ok(v), Option.None) en un match cuyo valor es primitivo (int, float, bool, String) es error. Antes pasaba nyx check y clang fallaba con IR inválido o, con un String, compilaba y no entraba a ningún brazo (fricción de nyxerp con match string_to_int(s), que devuelve int). +2 tests de error.

Agregado

  • sse_close_room(room) -> int en std/serve: cierra solo los canales de un room; el resto sigue y el room se puede volver a abrir. sse_drain_close no servía: deja el registro en drenaje y ninguna apertura posterior se registra (fricción de nyxerp). +5 checks en el smoke de serve.
  • std/postgres toma la clave como libpq cuando la cadena no trae password=: PGPASSWORD, después passfile=, PGPASSFILE o ~/.pgpass (comodines *, escapes \: y \\; un archivo legible por otros se ignora). Expuesta pg_pgpass_buscar (fricción de nyxerp). +1 test (506).

0.37.0 2026-10-22

MINOR: dos avisos de 0.36.x pasan a error, como anunciaban (regla 7 de docs/VERSIONING.md): NYX1010 entre entero y String, y NYX1045 en un escape desconocido. Lo demás es aditivo: std/pdf_render, respuestas HTTP por partes, SNI con comodín, http_serve_en, registros DNS, nyx build -j N y varias mejoras de std/pdf, std/xlsx, std/image y std/smtp. Antes del corte se revisaron 1201 archivos (nyxerp, los productos, los sitios, y std, ejemplos y compilador de este repo) con el compilador de 0.36.x: ningún caso de los dos avisos.

Cambiado

  • NYX1010 entre entero y String es error (aviso en 0.36.x). 5 + "a", 5 == "5" y "a" < 5 compilaban y morían con SIGSEGV al ejecutar la línea; ahora no compilan, con la misma pista (int_to_string, string_to_int_or). Los tres casos test-nyx1010-*-mixto pasan a la lista de errores de run_error_tests.sh. Sigue siendo aviso cuando un lado lee una casilla (dst[1] == "dos"): el tipo de elemento inferido de un Array puede mentir (var dst = [9] y después dst.push(src[1]) con un String) y la casilla lleva su tag, así que ese programa anda (test-323, caso 6; aviso en test-nyx1010-casilla-aviso).
  • NYX1045 en un escape desconocido es error (aviso en 0.36.x). "\q" o "\e" valían la letra sin la barra; ahora el análisis los rechaza con línea y la lista de escapes válidos (\x1b en lugar de \e). Sale con el formato y el NDJSON de los demás errores del análisis ("severity":"error").

Agregado

  • std/http: http_serve_en(host, port, handler) y http_serve_mt_en(host, port, workers, handler) eligen la dirección de bind; http_serve/http_serve_mt siguen en 0.0.0.0 por compatibilidad (gotcha http-serve-all-interfaces). El playground pasa a 127.0.0.1:8081: en producción quedaba abierto directo aunque lo sirve el gateway (orden del servidor, 2026-10-21). Test test-505-http-serve-en (liga 127.0.0.2 y mide que 127.0.0.1 no conecte).
  • SNI con comodín en el servidor TLS: tls_server_add_cert("*.dominio", cert, key) cubre una etiqueta (a.dominio, no dominio ni a.b.dominio, RFC 6125), los nombres exactos ganan sobre el comodín sin importar el orden, la comparación ya no distingue mayúsculas y el tope sube de 16 a 64 dominios. Pedido de nyxerp (fase C0 de su alta automática de clientes <cliente>.nyxerp.com). Test test_sni_name_matches (runtime) y handshake real verificado con openssl s_client.
  • std/http: leer una respuesta por partes (fricción nyxerp 20261019-200016-team-1: reenviar al navegador cada trozo de una API de modelos de lenguaje apenas llega). try_http_stream_opts(method, url, headers, body, http_stream_opts()) → HttpStream, que vuelve con el estado y las cabeceras:
    • read() da trozos del cuerpo sin el marco chunked; read_event() da eventos text/event-stream ya armados (SseEvent { evento, datos, id }, WHATWG).
    • Plazos connect_ms, primer_byte_ms, lectura_ms (entre dos trozos) y total_ms (sin tope por defecto). HTTP y HTTPS.
    • sse_wait_room(room, ms) en std/serve cierra la carrera del reenvío: el canal entra al room después de que el handler devuelve, y sin esperar los primeros eventos se perdían.
    • Test test-504-http-stream (chunked partido, Content-Length, hasta el cierre, SSE partido, plazos, gzip, cortes, TLS), tests/ai-first/42, receta 144-reenviar-eventos.
  • std/pdf_render: dibujar una página de un PDF como imagen (fricción nyxerp 20261019-080021-team-1: calcar en el editor de formatos la hoja preimpresa que la imprenta manda en PDF). Módulo nuevo en Nyx puro: el mismo código en nativo y en wasm32-wasi, con la imagen byte a byte igual en los dos.
    • pdf_open(bytes, pdf_open_limits()) -> Result<PdfDoc, Error>, doc.page_count(), doc.page_size(p), doc.render(p, ppp, pdf_render_strict()) y doc.render_report(...) con pdf_render_best_effort(), que devuelve la imagen y la lista de lo omitido. Atajo pdf_page_to_image(bytes, página, ppp). La salida es la Image de std/image.
    • Parser con xref clásica, xref streams, object streams, actualizaciones incrementales y reconstrucción por barrido; filtros Flate (con predictores), LZW, ASCIIHex, ASCII85 y RunLength.
    • Rasterizador con antialias (cobertura exacta horizontal × 4 submuestras), nonzero y even-odd, trazos con uniones, puntas, punteado y stroke adjustment, y recortes.
    • Color gris, RGB, CMYK (la interpolación de poppler), ICCBased, Indexed, Lab, Separation y DeviceN con funciones de tipo 0, 2, 3 y 4; alfa constante.
    • Imágenes JPEG y crudas, con SMask, Mask y stencil. Las grandes se reducen en una pasada al tamaño que ocupan en la página, sin materializarlas enteras.
    • Texto con TrueType y CFF incrustadas (simples y Type0/CID). Las 14 estándar sin incrustar usan un subconjunto de Liberation (OFL 1.1, 227 KB, licencia incluida en el bloque generado). Caché de glifos rasterizados.
    • Estricto por defecto: lo no soportado (sombreados, Type 1/3, JBIG2/JPX/CCITT, cifrado...) es Err(unsupported) con el nombre. PdfOpenLimits acota tamaño, descompresión, anidamiento, formularios, operadores, ppp y píxeles: un PDF hostil da Err(invalid) nombrando el límite.
    • Verificado contra poppler 25.03: corpus sintético de 12 PDFs (tests/pdf-corpus/, scripts/testing/pdf_render_oracle.sh) y 44 PDFs reales locales (93 páginas, ninguna falla).
    • Tests test-503-pdf-render, test-wasm-59-pdf-render y tests/ai-first/41-pdf-pagina-a-imagen; receta 143-pdf-a-imagen.
  • std/dns: consultas TXT, CNAME y MX (pedido de nyxerp, 2026-10-18, para revisar SPF, DKIM y DMARC antes de encender el correo). Módulo nuevo con cliente DNS propio en Nyx (RFC 1035) sobre UDP, con reintento por TCP si la respuesta viene truncada y sin dependencias de enlace.
    • dns_txt (un String por registro), dns_cname, dns_mx (ordenado por preferencia), dns_query crudo, dns_set_server, dns_set_timeout, dns_is_nxdomain y dns_is_nodata.
    • Errores Result<_, Error>: NXDOMAIN not_found 3, NODATA not_found 61, timeout 110, SERVFAIL/REFUSED io, parse.
    • Parser de entrada hostil: tope de 16 saltos de compresión, nombres de 255 bytes, ID aleatorio y pregunta verificados. Servidores desde /etc/resolv.conf (hasta 3 IPv4); en Windows exige dns_set_server. Sin caché, IPv6, EDNS(0) ni DNSSEC.
    • Verificado contra la red real (SPF y DMARC de gmail.com, 17 TXT de google.com por TCP).
    • Tests test-499-dns (paquetes fijos), test-500-dns-servidor (servidor falso UDP/TCP) y test-501-dns-red-real (SKIP sin red); receta 142-dns-spf-dkim-dmarc.
  • std/smtp: correo con texto y HTML (multipart/alternative) (pedido de nyxerp, 2026-10-18). smtp_message_alternative(de, para, asunto, texto, html): texto primero y HTML después (RFC 2046), cada parte en quoted-printable (RFC 2045 §6.7). Con smtp_attach sale multipart/mixed › alternative + adjuntos, con boundaries distintos y dot-stuffing por parte. smtp_message y smtp_message_html no cambian un byte. Test test-stdlib-12-smtp-alternative (incluye vuelta completa por mime_parse) y receta 141-smtp-texto-y-html.
  • std/image: miniaturas de hojas escaneadas en décimas de segundo (fricción nyxerp, 2026-10-18), con la salida idéntica byte a byte (sigue igual a Pillow).
    • Resize, decodificador y codificador JPEG leen y escriben por punteros en nativo de 64 bits; wasm32 conserva las versiones anteriores, y las dos rutas dan los mismos bytes.
    • Hoja de 1275×1650, tiempo de CPU de image_thumbnail: 3,6 s → 1,0 s con nyx build (clang -O0) y 0,92 s → 0,25 s con --release (-O2). En 2550×3300: 8,6 s → 2,4 s y 2,3 s → 0,65 s.
    • Buena parte de los 3,5 s reportados era compilar sin --release.
    • Test test-497-image-rapido (huellas CRC32 fijadas antes de optimizar), receta 140-imagen-hojas-escaneadas y scripts/bench_image_thumb.nx.
  • std/xlsx: listas desplegables y color de relleno (pedido de nyxerp, 2026-10-17, para plantillas de carga masiva).
    • XlsxSheetOut.validate_list(rango, valores) y validate_list_range(rango, origen) emiten <dataValidations> en el orden que exige el esquema ECMA-376. xlsx_write devuelve Err si la lista fija pasa de 255 caracteres, si un valor lleva coma o comillas, o si el rango es inválido.
    • xlsx_fill(celda, "RRGGBB") da relleno sólido y se encadena con xlsx_bold.
    • xlsx_read no cambia.
    • Test test-495-xlsx-validacion y receta 138-xlsx-plantilla.
  • std/pdf fase 2: grosor, gris y color, 12 fuentes Core14 y texto girado (pedido de nyxerp, 2026-10-17, para hojas preimpresas y etiquetas).
    • line_width, gray y color cambian a la vez el trazo y el relleno de lo que se dibuja después. Cada página arranca en el estado por omisión del PDF: negro y 1 pt.
    • Fuentes nuevas: HelveticaOblique, HelveticaBoldOblique, TimesBold, TimesItalic, TimesBoldItalic y las cuatro Courier. Las tres tablas AFM nuevas de Times se cruzaron entre pypdf y pdfminer.six; la única diferencia es el €, que pdfminer no trae.
    • text_rotated gira el texto con la matriz Tm, a cualquier ángulo, con seno y coseno calculados sin libm; los múltiplos de 90° salen exactos.
    • Ningún PDF de la fase 1 cambia de bytes.
    • Test test-496-pdf-fase-2 y receta 139-pdf-preimpreso.
  • nyx build -j N: las unidades de [lib] se compilan en paralelo (fricción nyxerp, 2026-10-17). Antes iban en serie: 302 módulos y unos 50 minutos en frío en 4 núcleos. Las unidades no dependen unas de otras para compilar, así que ahora se lanzan hasta N a la vez (-j N, --jobs N o NYX_JOBS; por omisión, los núcleos). nyx test pasa por el mismo camino. Salida y enlace en el orden del manifiesto; con un error se reportan todas las unidades que fallaron y no queda ningún objeto a medio escribir. Dos builds del mismo proyecto a la vez (varios nyx test en paralelo, como corre nyxerp) toman un candado sobre target/nyx-lib: el segundo espera y reutiliza; antes se borraban los temporales uno al otro. Un build sin nada que compilar no lanza procesos auxiliares (el que pasa nyx test en cada archivo). Medido en una copia de nyxerp: 300 bibliotecas en frío, 2.894 s en serie y 1.057 s con -j 4. Medición en docs/design/spikes/2026-10-17-build-lib-paralelo-medicion.md: menos de 600 MB por unidad. Caso 10 de run_lib_modules.sh.

Arreglado

  • Una cabecera escrita como "Nombre: valor" en un solo elemento llega al pedido (silently-wrong, fricción nyxerp). El cliente de std/http solo leía pares [nombre, valor] y la descartaba sin aviso; ahora acepta las dos formas, también mezcladas (un nombre de cabecera no puede tener :, así que no hay ambigüedad con valores como Bearer a:b). Test tests/ai-first/43.
  • CAPABILITIES.md lista los tipos públicos y sus métodos: hasta acá solo funciones libres, así que APIs hechas de métodos (doc.render(), s.read(), d.text()) y sus structs no aparecían (fricción nyxerp, 2026-10-20).
  • NYX1017 sugiere la llamada cuando el nombre es un método: s.status sobre un HttpStream ahora dice «'status' is a method: call it with parentheses, .status()» (fricción nyxerp, 2026-10-20). Test test-nyx1017-metodo-sin-parentesis.
  • Los literales de string compilan en tiempo lineal. El lexer y escape_string concatenaban un byte por vez: un literal de 64 KB tardaba ~4 s en el frontend y uno de 400 KB, 32 s. Ahora 64 KB tardan 35 ms. test-lexer y test-emit-bytes-global lo fijan.
  • Un float de una casilla sin <T> conserva su valor al asignarlo y al empujarlo . x = d[i] con x: float ya declarada, y b.push(a[2 * i]), daban los bits del double como número (1.5 → 4,6e18) en silencio. Test test-502.
  • wasm32: una casilla pasada directo a un método de impl. Con un parámetro Array o String, la llamada emitía el i64 crudo: en nativo pasaba de casualidad, en wasm32 era un trap unreachable. Ahora se convierte, igual que en las funciones libres. Test test-wasm-58.
  • Las variantes de un enum de una sola línea ya no dependen del camino del import (fricción nyxerp, 2026-10-18). std/pdf declara pub enum PdfFont { Helvetica, … } en una línea; cuando llegaba primero por un import transitivo, el escaneo por líneas de semantic buscaba las variantes en las líneas siguientes y registraba basura. Como el primer registro gana, PdfFont.Helvetica daba NYX2012 «has no variant» (el chequeo de NYX2012 en expresiones, nuevo en esta versión, lo hizo visible; antes fallaba en codegen con «unknown enum variant»). Con los imports al revés compilaba. Regresión: test-498.
  • std/smtp verifica contra las CAs del sistema (fricción nyxerp, 2026-10-18). smtp_starttls (587) y smtp_connect_tls (465) verificaban contra el almacén de tls_set_ca_file, que arranca vacío: todo relé público con certificado válido daba «unknown ca». El rodeo, tls_set_ca_file(""), sumaba todas las CAs públicas a un almacén global que también usa el verify-ca de std/postgres. Ahora usan tls_upgrade_fd_verified_system / tls_connect_verified_system, como ya hacía std/imap. Verificado contra smtp.gmail.com en 587 y 465: antes fallaban los dos, ahora verifican. El autofirmado sigue rechazado (test-stdlib-07, caso d). El rodeo ya no hace falta; una CA privada por conexión queda fichada.
  • Una casilla de un Array sin <T> pasada directo a un parámetro struct, o usada como clave de Map, ya funciona (fricción nyxerp, 2026-10-17). texto(hs[0]), con fn texto(h: Hoja), pasaba el i64 crudo de la casilla donde la función esperaba el struct por valor. El resultado era «GC heap exhausted» o, en otro programa de nyxerp, la función recibía otro elemento. m.insert(pares[0], pares[1]) usaba el i64 como puntero de la clave y clang rechazaba el IR. En los dos casos nyx check daba verde. Ahora la casilla se convierte al struct del parámetro (inttoptr + load, como en let h: Hoja = hs[0]), y la clave de un método de Map que sale de una casilla se lee como String. El propio compilador tenía el mismo patrón en cg_tipo_duplicado(decls[i], …), que andaba de casualidad por la convención de llamadas de aarch64. ASTNode queda afuera: en una casilla es un Array y no un puntero. Regresión: tests/ai-first/40-casilla-sin-tipo-como-argumento.nx y gotcha untyped-slot-as-argument.
  • nyx check rechaza una variante de enum que no existe (NYX2012) (fricción nyxerp, 2026-10-17). PdfFont.Zzzz pasaba el chequeo en verde y recién el codegen lo rechazaba, con un mensaje que pedía una anotación de tipo. Ahora es NYX2012, con «did you mean» si hay una variante cercana, tanto para enums locales como importados. No se valida cuando el nombre corresponde a una variable o a un parámetro. Casos test-nyx2012-variante-{inexistente,importada}.
  • Escapes de String: \xNN y \u{…} valen lo que dicen, y un escape desconocido avisa (NYX1045) . Antes el lexer conocía solo \n \t \\ \" \r \0 y cualquier otro escape perdía la barra en silencio: "\x41" valía x41, "\u{0}" valía u{0} y '\x41' imprimía 41. Había tres casos reales, todos equivocados sin que nadie lo notara: el centinela "\u{0}" de la configuración de nyxerp, el separador "\x1F" de la clave de DISTINCT de nyx-db (y su copia en venezuelainfo), y las secuencias ANSI "\x1b[…" del ejemplo de nyx-edit. Con este arreglo los tres valen lo que quisieron decir sin tocar su código. \' y \$ ("\${x}" es ${x} literal) quedan documentados. Un \x o \u{…} malformado es error de lexer con línea y columna. Cualquier otro escape es warning [NYX1045] y pasa a error en 0.37.0. Los cuatro caminos del lexer que leían escapes, que eran cuatro copias del mismo if, ahora pasan por una sola función. test-494-escapes-hex-unicode, siete casos test-nyx1045-* y gotcha string-escapes.
  • CAPABILITIES.md ya no esconde 34 módulos de la stdlib en «Otros». La tabla curada module_category de compiler/build.nx no tenía 34 de los 78 módulos, entre ellos serve, postgres, error, math, map, router, template, xlsx, pdf e image. Todos tienen ahora categoría, y se suman dos categorías nuevas: «Núcleo» (error, prelude) e «IA & LLM». «Otros» queda solo para builtins sin tema. Guarda nueva (d) en run_capabilities_test.sh: falla si un módulo de std/ cae en «Otros», así que un módulo nuevo entra a la tabla en el mismo commit que lo crea.
  • El compilador ya no copia de forma cuadrática: un archivo de prueba grande de nyxerp compila 9 veces más rápido (fricción nyxerp, 2026-10-17: ~42 s por archivo de nyx test). La medición por fase mostró que el tiempo crecía más rápido que el IR, y un perfilador de muestreo que el 81% era el GC, provocado por concatenaciones que recopiaban todo lo acumulado: las constantes de texto en generate_llvm, las declaraciones de módulo en resolve_source, y el prefijo de símbolo de cada módulo recalculado carácter por carácter en cada llamada sin calificar. Ahora van con StringBuilder, los prefijos se calculan una vez (ctx.alcances[3]), la búsqueda de una función entre los módulos se memoriza (ctx.alcances[4], con la versión del conjunto en la clave), extract_exports no recorta las líneas que no pueden exportar nada y scan_module_types no relee un módulo ya escaneado. La huella de [lib] en nyx_build tenía el mismo patrón (todo = todo + … sobre el cierre de imports): un nyx_build libs sin cambios, que nyx test corre en cada archivo, bajó de 5,7 s a 2,0 s en nyxerp. El IR sale idéntico byte a byte. Medido: 105,6 s → 11,2 s y 822 → 642 MB en el archivo más grande; 3,5 s → 1,6 s en uno mediano. Spike: docs/design/spikes/2026-10-17-test-sin-repetir-medicion.md.
  • Operadores entre primitivos de clases distintas: NYX1010 en vez de SIGSEGV o IR inválido. El chequeo dejaba pasar aritmética y comparación entre cualquier par de tipos. La matriz medida el 2026-10-17 (cuatro tipos por lado, nueve operadores) mostró que solo existen números con números, String + x, ==/!= entre dos String o dos bool y el orden entre dos String; el resto moría con SIGSEGV al ejecutar la línea (5 + "a", 5 == "5", "a" < 5) o daba IR inválido que recién rechazaba clang (x * "dos", 2.5 + "a", true + 1). Lo segundo nunca produjo un binario y es error ya; lo primero compilaba, así que por la regla 7 de docs/VERSIONING.md es warning [NYX1010] en 0.36.x y error en 0.37.0. Nyxerp (539 archivos) no tiene ningún caso. Tests: test-493-operadores-primitivos-mixtos (los valores de lo que sí existe) y cinco de make test-errors; gotcha mixed-primitive-operators.
  • Reinstalar el mismo compilador ya no invalida target/nyx-lib (fricción nyxerp, 2026-10-17). La huella de cada objeto de [lib] llevaba la ruta y el mtime del binario del compilador, y la ruta absoluta de cada std/ del cierre. Como cada make install-local / nyx update arma una carpeta nueva en ~/.nyx/versions/, reinstalar el mismo compilador recompilaba todo: 302 módulos y unos 50 minutos en nyxerp. Ahora el compilador cuenta por el sha256 de su contenido, guardado en target/nyx-lib/.compilador junto a la ruta, el tamaño y el mtime que lo produjeron, y los std/ por su ruta relativa. Caso 9 de run_lib_modules.sh: reinstalación igual reutiliza, otro compilador recompila.

0.36.1 2026-10-17

PATCH: un arreglo de codegen (conversión numérica sobre una casilla de Array sin tipo) y dos capacidades aditivas pedidas por nyxerp para correo con OAuth2: smtp_auth_xoauth2 en std/smtp y el módulo nuevo std/oauth2. Nada que compilaba deja de compilar.

Agregado

  • std/oauth2: cliente OAuth2 sin estado (fricción nyxerp 2026-10-11, GO 2026-10-16): PKCE S256 y state (CSPRNG), URL de autorización (+ extras del proveedor: access_type=offline de Google), canje del código, renovación que conserva el refresh_token, client_credentials y device code (RFC 8628: oauth2_device_code / _poll / _wait, con verification_uri y la _url de Google). Errores auth / protocol / pending / invalid, nunca con el token, el secreto ni el verifier. Sin runtime nuevo: std/http + std/json + std/url + std/base64 + std/webpushcrypto. test-stdlib-11-oauth2 (vector RFC 7636 B y un proveedor canned con la forma de Google y Microsoft), receta 137-oauth2, SPEC y LLM.md con las recetas Gmail y Microsoft 365. CAPABILITIES: oauth2 en «Cripto & seguridad», y de paso imap y mime salen de «Otros» a «Red».
  • smtp_auth_xoauth2(c, usuario, token) en std/smtp (fricción nyxerp, 2026-10-11): el simétrico de imap_auth_xoauth2 para ENVIAR por Gmail y Microsoft 365, que ya no aceptan usuario y contraseña. Exige que el EHLO haya anunciado XOAUTH2 y maneja el rechazo propio del mecanismo (334 con un JSON en base64 → línea vacía → 535); el JSON decodificado va al Error, el token nunca. Caso (e) de test-stdlib-07-smtp-tls (token bueno, token vencido, servidor sin el mecanismo). Obtener y renovar el token sigue siendo de la aplicación: std/oauth2 quedó sembrado como arco oauth2-cliente (BORRADOR, GO pendiente) en docs/design/specs/2026-10-16-oauth2-cliente-design.md.

Arreglado

  • int_to_float(arr[i]) / float_to_int(arr[i]) sobre un Array sin <T> ligados a un let con tipo generaban IR inválido que nyx check no veía (fricción nyxerp, 2026-10-16). El indexado heredaba el expected_type del let: con let f: float = int_to_float(cuentas[q]) la casilla se leía como double y el sitofp i64 recibía un double; con let n: int = float_to_int(pesos[0]), al revés (fptosi double sobre un i64 crudo). Recién fallaba clang con «invalid IR». Ahora los dos builtins fijan el hint del tipo que exigen (int / float) mientras se codegena su argumento, igual que string_to_int con String. Regresión: tests/ai-first/39-conversion-de-casilla-sin-tipo.nx (+1 en make test-ai-first).

0.36.0 2026-10-10

MINOR porque rechaza código que antes compilaba (decisión #3): NYX2019 pasa a error en el let a un entero (fue aviso en 0.35.x, regla 7 de docs/VERSIONING.md); NYX1031 para la familia del handle de archivo con una ruta (antes, SIGSEGV al correr); NYX1042 (return/break/continue que sale de un defer; antes colgaba o no hacía nada), NYX1043 (defer en async fn) y NYX1044 (captura por nombre ambigua en una closure de bloque). Ninguno rechaza un programa que diera el resultado correcto. El grueso es la semántica de bloques: alcances reales en codegen, defer y drops de #[affine] al cerrar su bloque (también en throw), closures dentro de bloques (decisión #22). Además, los pedidos de nyxerp: std/zip/xml/xlsx, std/image, std/mime, std/imap, std/expr, std/rsa, gzip y límites en std/serve, mTLS y multipart en el cliente HTTP.

Agregado

  • std/xlsx: paneles fijos, autofiltro y formato con separador de miles (pedido de nyxerp, 2026-10-02): h.freeze(filas, columnas) (<pane state="frozen">), h.autofilter("A1:F120") (con el nombre oculto _xlnm._FilterDatabase que guarda Excel) y xlsx_number_fmt(v, código) con cualquier código de Excel ("#,##0.00" usa el integrado 4; la configuración regional de quien abre decide si se ve 1.234.567,89). Test 462 ampliado y verificado con openpyxl; receta 124.
  • Segunda tanda de nyxerp (2026-10-01): JPEG de salida, miniaturas y lectura de correo por IMAP, revisada antes de integrar e integrada con una batería completa:
    • std/image escribe JPEG y arma miniaturas: image_encode_jpeg(img, quality) (baseline, calidad 1-100 estilo libjpeg sobre el Anexo K, Huffman estándar, JFIF, 4:2:0 hasta q90 y 4:4:4 por encima, gris de un componente, alfa sobre blanco, sin EXIF, ICC conservado; FDCT entera: los mismos bytes en nativo y wasm). Mismo PSNR que Pillow a igual calidad hasta q90. image_thumbnail(bytes, max_w, max_h, quality) e image_thumbnails(bytes, sizes, quality): decodifica con reducción, achica con Lanczos3 y codifica en un paso, una sola decodificación para varios tamaños (12 MP a 600 px en ~1,4 s). image_default_quality() (80) e image_strip_icc. Tests 481-482, wasm 57, receta 133.
    • std/imap: leer un buzón: TLS implícito (993) y STARTTLS (143), ambos verificando siempre contra las CAs del sistema; LOGIN, PLAIN y XOAUTH2 (token de la app), nunca en claro; imap_select/imap_examine, imap_fetch_since (cursor por UID, búsqueda por tramos, stale ante UIDVALIDITY cambiado, plazo global opcional), imap_fetch_message, imap_search_unseen, imap_mark_seen; todo con BODY.PEEK. El servidor se trata como entrada hostil: topes de línea, literal (antes de leerlo), total por comando y profundidad, y plazos. Un mensaje grande, o que un tope de std/mime rechazaría, sale con sus cabeceras y truncated. Tests 483-485, sub-suite test_imap_client.py (107 comprobaciones contra un servidor de mentira con PKI y casos hostiles), receta 134. Sin verificar todavía contra Gmail ni Dovecot reales.
    • std/mime: mime_parse_head analiza solo las cabeceras: un cuerpo que pasa un tope ya no hace perder el In-Reply-To (resuelve la ficha «un tope superado tumba el mensaje entero» para imap y para quien la llame; mime_parse sigue dando Err).
    • La revisión encontró y se arreglaron, con su caso: FETCH repartido en varias respuestas daba un mensaje vacío y adelantaba el cursor (el correo se perdía); sin tope acumulado por comando (memoria); capacidades con costo cuadrático; la primera corrida de un buzón de >130 mil mensajes no arrancaba. En std/image, el re-prefijo de los errores de image_thumbnail.
  • Siete pedidos de nyxerp del 2026-09-29, hechos por subagentes en worktrees propios e integrados con una batería completa:
    • gzip en std/serve, encendido por omisión: respuestas de texto entre 1 KiB y 1 MiB, cuando el cliente manda Accept-Encoding: gzip (con q-values); Vary, Content-Length en bytes, ETag débil, HEAD igual que GET, caché por sha256. Cambia lo que ven los clientes que aceptan gzip: app_compress(app, false) lo apaga. std/zip gana zip_gzip/zip_gunzip. Test 463, receta 126.
    • Límites en std/serve: tope de cuerpo por ruta (app_max_body, 413) y limitador por origen compartido por todos los workers (app_rate_limit, app_route_rate_limit, 429 con Retry-After; app_rate_limit_key y rate_key_forwarded_for detrás de un proxy). Test 464, receta 127.
    • std/rsa: verificar firmas RSA-SHA256 (v1.5, base64 y PSS) con clave pública PEM, para avisos firmados de pasarelas de pago. EVP_DigestVerify en runtime/crypto.c; en wasm Err(invalid 95). Test 465 con vectores del CLI de openssl, receta 128.
    • TLS mutuo en el cliente HTTPS: HttpOpts.client_cert_pem/client_key_pem, solo en esa petición y validado antes de abrir el socket (mismo camino que cas_extra). Sub-suite test_http_mtls.py con un servidor que exige certificado, receta 129.
    • multipart en el cliente HTTP: multipart_field, multipart_file, multipart_build y try_http_post_multipart; binario seguro, verificado contra el parser email de Python. Test 467, receta 130.
    • std/expr: evaluador seguro de fórmulas con variables, en decimales de punto fijo (6), no float: 0.1 + 0.2 == 0.3, round(2.675, 2) = 2.68. 1500 expresiones al azar iguales a decimal de Python; mismo resultado en wasm. Test 468, receta 131.
    • fetch y SSE del navegador hacia la red local, con plazo: targetAddressSpace de Chrome y AbortController; la API vieja no cambia (el merge la había cambiado sin querer y se restauró: c3c6a87e). Test wasm 53, receta 132. El permiso en un Chrome real no se pudo verificar. Specs en BORRADOR para los grandes (imágenes, IMAP, red local, Android) y, en el repo de nyx-proxy, la de dominios sin reinicio y ACME.
  • std/zip, std/xml y std/xlsx: leer y escribir planillas .xlsx en Nyx puro (fricción de nyxerp, 2026-09-29). Un .xlsx es un zip con XML adentro, así que son tres módulos y los dos de abajo sirven solos. std/zip: contenedor .zip (zip_read por el directorio central, zip_writer) con topes contra bombas zip (ZipReadOpts). std/xml: lector por eventos y árbol chico; <!DOCTYPE es error, así que no hay XXE ni bombas de expansión. std/xlsx: xlsx_read da celdas tipadas (texto, número, fecha, bool, error, vacía), con fechas en ISO y las épocas 1900 y 1904; las fórmulas no se evalúan (se lee el valor en caché) y los huecos son celdas empty; xlsx_write arma el libro (texto, números con decimales fijos, fechas, negrita, anchos, celdas combinadas). Sin extern "C": igual en nativo y en wasm32-wasi. El códec DEFLATE (inflate, deflate, CRC-32, Adler-32) se trasladó de std/pdf a std/zip con nombres zip_* (para no chocar con std/compress): una sola implementación, y los PDF salen idénticos byte a byte. Verificado contra otros lectores: 818 celdas de un libro real de Excel iguales a las de openpyxl; lo que escribe xlsx_write lo abre openpyxl; los .zip de zip_writer los aceptan zipfile y unzip (run_zip_verify.sh, en make test-stdlib; run_xlsx_verify.sh aparte, necesita openpyxl). test-458..462 y las recetas 123 (zip), 124 (escribir un libro) y 125 (leer una hoja).
  • Primera tanda de los pedidos grandes de nyxerp del 2026-09-29 (GO de Ottavio el 2026-09-30), hecha por subagentes en worktrees propios, revisada e integrada con una batería completa:
    • std/image: decodificar, achicar y recortar fotos en Nyx puro, igual en nativo y wasm . image_info (formato por firma, lados, orientación sin decodificar); image_decode de PNG completo y de JPEG baseline de 8 bits (gris o color, todos los submuestreos, reinicios), con la orientación EXIF ya aplicada, el perfil ICC expuesto en Image.icc sin aplicarlo, y reducción 1/2, 1/4 y 1/8 durante la decodificación (image_decode_scaled, image_decode_min); image_resize, image_resize_fit, image_resize_cover, image_crop e image_flatten (alfa premultiplicado). Topes (ImageLimits: bytes, píxeles, lado) validados ANTES de reservar memoria. Progresivo, CMYK y 12 bits dan Err(unsupported) con la salida; WebP, GIF, HEIC, AVIF, BMP y TIFF se reconocen por firma. Verificado contra Pillow (fotos de varios codificadores, las 8 orientaciones, redimensionado). El lector PNG salió de std/pdf, que ahora lo importa. Falta el codificador JPEG y las miniaturas en un paso (Tasks 5-6). Tests 475-480, wasm 55-56; spike del prototipo medido.
    • std/mime: leer correo entrante: cabeceras con plegado, RFC 2047 y 2231, base64 y quoted-printable, charsets, multipart anidado con tope de profundidad, adjuntos binarios, rebotes, HTML a texto, y los ids de hilo (Message-ID, In-Reply-To, References). Base de std/imap (Tasks 4-6, próxima tanda). Tests 472-474, wasm 54.
    • std/smtp: Message-ID y cabeceras de hilo: el id se fija al crear el mensaje (antes cambiaba en cada smtp_render y no había forma de saber el que salió); smtp_message_id, smtp_set_message_id, smtp_header y smtp_reply_to (arma In-Reply-To + References; la cabecera Reply-To se pone con smtp_header). smtp_send rechaza con Err(header) CR/LF/NUL, nombres inválidos y las cabeceras que escribe la biblioteca. test-stdlib-10. Pedido de nyxerp del 2026-10-01 (mesa de ayuda).
    • tls_upgrade_fd_verified_system en std/tls: STARTTLS verificado contra el almacén del sistema, para IMAP (143) y SMTP (587). Sonda TLS de std/imap en make test-integration.
    • Android: sonda del build arm64 contra el sysroot del NDK (spike) y la capa os_* para bionic: cambio de contexto propio en ensamblador aarch64 y os_thread_timedjoin/cancel cooperativos.

Arreglado

  • El drop de un valor #[affine] con Drop es por declaración y corre al cerrar su bloque . Silently-wrong: el drop iba por nombre y por función, así que en un loop cada iteración pisaba el mismo slot y solo se soltaba el último valor (el resto se perdía sin aviso); un throw no soltaba nada; y dos afines homónimos eran NYX2020. Ahora el drop es un defer implícito de su declaración (slot propio, entrada pendiente del alcance con su marco de try): una vez por declaración, también por iteración y en throw. La bandera de un valor devuelto se apaga antes de las salidas. Se fueron la sección de limpieza de la función, %__defer_retval y NYX2020. Test tests/ai-first/38; gotcha drop-per-declaration.
  • Un defer corre solo si se alcanzó, y un throw corre los defer alcanzados . Silently-wrong: if b { return 1 } defer {...} corría el defer con b verdadero, porque la sección de limpieza generaba todos los de nivel superior en cada salida. Y un throw saltaba toda la limpieza. Ahora el cuerpo de la función es un alcance más (los defer se generan en línea en las salidas que los alcanzaron) y cada defer empuja al ejecutarse un marco de try implícito cuyo catch lo corre y relanza: corren los alcanzados, del más interno al más externo, antes del catch; un throw dentro de un defer no lo repite. Tests 492 y tests/ai-first/37; el gotcha defer-not-on-throw pasa a fixed.
  • Una closure dentro de un bloque puede capturar las variables del bloque; una fn con nombre en un bloque linkea. Las closures solo existían en el nivel superior: una lambda en un if no veía las variables del if (NYX1002) y una fn con nombre en un bloque no linkeaba (use of undefined value de clang, sin línea). El parser deja la lambda en su bloque, codegen junta las funciones anidadas de cualquier bloque y las variables de bloque con nombre único entran al SharedEnv. Lo que no se puede capturar por nombre (un nombre repetido en la función, la variable de un for, un binding de match) es NYX1044, nunca un valor equivocado. Tests: ai-first 36 y dos de error; gotcha closures-capture-block-vars.
  • for sobre un literal de array, sobre Array<T> y sobre Map: tres resultados equivocados en la misma función de codegen. for q in ["x", "y"] tipaba q como int (print(q) mostraba la dirección del String, sin error); un elemento Array<T> daba SIGSEGV al usarlo, y uno Map devolvía m.size() == 0 en silencio (los dos caían en la rama de struct, con un load de más). El literal toma el tipo común de todos sus elementos (uno mixto queda sin tipo); Array<T> y Map/Map<K, V> tienen su rama. Test tests/ai-first/35; gotcha for-in-element-type.
  • defer ya no se descarta en silencio fuera del nivel superior de la función; return en un defer ya no cuelga. Un defer dentro de un bloque (if, else, while, for, brazo de match, try, catch, unsafe), en una fn anidada o en una lambda compilaba, pasaba nyx check y no corría nunca. Un return dentro de un defer colgaba el programa, un break no hacía nada, un defer dentro de otro se ignoraba, y una fn con defer y una fn anidada con return no compilaba (error de clang sin línea). Ahora los defer corren, y return, o break/continue que salga del cuerpo de un defer, es NYX1042; un defer en una async fn es NYX1043. Tests: tests/ai-first/34-defer-al-cerrar-bloque.nx, test-488 a test-491, test-nyx1042-*, test-nyx1043-*.
  • file_write_string("/ruta", "hola") ya no compila para caerse después: es NYX1031 (fricción nyxerp, 2026-10-09). Las funciones del handle de archivo (file_write_string, file_write_bytes, file_read_line, file_read_bytes, file_seek, file_tell, file_flush, file_close) reciben el handle de file_open; con una ruta String pasaban el checker y el binario moría con SIGSEGV sin mensaje, perdiendo además la salida en el buffer. Ahora el checker lo rechaza con una pista (file_open o write_file); un handle guardado en un int sigue andando. LLM.md §4 y CAPABILITIES.md dicen el tipo de cada argumento. Test de error nuevo.
  • Un nombre redeclarado dentro de un bloque ya no tapa a la variable de afuera (fricción nyxerp, 2026-10-04): el codegen guardaba las variables de una función en un mapa plano, así que un let q dentro de un if/else/while/for/brazo de match/try/ catch/unsafe, la variable de un for q in ... o el binding de Option.Some(q) => REEMPLAZABA a la de afuera hasta el final de la función. Compilaba, nyx check daba verde (el checker sí la restauraba) y el resultado era basura o segfault con otro tipo, y el valor de la interna sin ningún síntoma con el mismo: let q: S = S{a:3}; if true { let q: S = S{a:99} }; return q.a daba 99. Ahora cada bloque es un alcance real y al cerrarlo el nombre vuelve a ser la variable de afuera, con su valor y su tipo. Además: un defer de nivel de función ve la q de la función aunque el return salga de un bloque que declaró otra; una closure que captura una variable de nivel de función la sigue viendo aunque un bloque declare otra igual (una lambda devolvía 801 en vez de 707); el checker abre alcance en el cuerpo de try y en unsafe (un let del try era visible en el catch y rechazaba programas válidos con NYX1014). Diagnósticos nuevos: NYX1041 (una lambda escrita dentro de un bloque usa un nombre que en ese bloque es otra variable; el arco closures-capturan-bloque lo dejó como respaldo de NYX1044) y NYX2020 (dos afines con Drop homónimos; retirado en este mismo release por drop-por-declaracion). Tests: tests/ai-first/33-sombreado-en-bloque.nx (13 casos), 486, 487, test-nyx1041-lambda-captura-de-bloque y test-nyx2020-drop-sombreado; receta 135; gotcha block-shadowing-restores-outer.
  • Una variable local Fn que se llama igual que una fn global se chequeaba contra la firma de la global (fricción nyxerp, 2026-10-04): let leer: Fn(int) -> int = a.leer; leer(21) daba NYX1005 «expected String, got int» si había una fn leer(s: String) en otro archivo, y con firmas iguales el chequeo pasaba contra la función equivocada. Semantic resuelve ahora el nombre como el codegen: la variable más reciente gana y aporta su propia firma Fn (también el tipo de retorno y it.map(f)). Regresión: ai-first 32 y un test de error.
  • std/xlsx: un número con 3+ decimales escrito antes que la primera fecha se leía como fecha (silently-wrong): la fecha tenía el numFmtId 164 fijo y chocaba con el del "0.000". Ahora todo formato propio toma 164 + su posición. Hallado al revisar el pedido de nyxerp; regresión en test 462.
  • Editar un archivo de include_bytes no recompilaba su biblioteca de [lib] modules. La huella de target/nyx-lib hasheaba el fuente del módulo y su cierre de imports, pero no los archivos incrustados: nyx run/test/build reutilizaban el .o y servían el contenido VIEJO sin error (ni touch del .nx alcanzaba). Ahora cada archivo hasheado entero suma la ruta y el sha256 de cada include_bytes("…") que nombra, incluidos los de módulos no-[lib] que la biblioteca inlinea. Fricción nyxerp 20260930-170003; tres casos nuevos en run_lib_modules.sh.
  • STARTTLS «verificado» no verificaba nada (runtime/tls.c). nyx_tls_client_upgrade con modo 4 (estricto contra el almacén del sistema) caía en el contexto de tls_set_ca_file y ninguna rama lo verificaba: el handshake aceptaba cualquier certificado. Ahora usa el contexto del sistema y verifica cadena y nombre; un modo fuera de 0..4 falla cerrado. Hallado por la sonda TLS de std/imap.
  • Revisión de la tanda antes de integrar (entrada hostil: fotos subidas y correo entrante):
    • std/image: un SOS de largo 2 al final del archivo abortaba el proceso (lectura fuera de rango), y cada tabla Huffman de un DHT armaba al leerse una tabla de 65536 entradas sin tope de cuántas trae el archivo (1 MB de tablas vacías eran ~4·10⁹ escrituras). Ahora se arman al usarlas. Casos en el test 478.
    • std/mime: mime_html_to_text pasaba el HTML entero a minúsculas en cada <style> (DoS cuadrático que controla el remitente), y el _ final de una palabra Q de RFC 2047 se perdía («Cotización123»). Casos en el test 473.
    • std/smtp: smtp_render, que es pública, escribía las cabeceras extra sin validar (inyección de un Bcc desde un In-Reply-To copiado de un correo entrante). Ahora cambia CR/LF/NUL por espacios y omite los nombres inválidos o reservados.
    • La guarda de español neutro tomaba la sigla «SOS» (el marcador JPEG) por el «sos» rioplatense.
  • Un elemento de un Array sin tipo, asignado a un campo o comparado con un String. c.nombre = xs[i] guardaba el entero opaco del elemento directo en el campo, y f != xs[0] (el elemento a la derecha) se lo pasaba crudo a la comparación: IR inválido que solo clang rechazaba, con nyx check en verde, mientras s = xs[i] y xs[0] == f sí construían. La asignación a campo convierte ahora con el mismo código que la asignación a variable (también un campo int, float o struct), y ==/!= convierten el lado opaco esté donde esté. Fricción nyxerp 20260930-040018. Regresión: test-471.
  • m[k] y m[k] = v sobre un Map. La indexación de un Map no estaba implementada: caía al camino de Array con una clave String como índice y el programa no enlazaba (defined with type 'ptr' but expected 'i64'), también como argumento (nums.push(m["a"])) y en una concatenación ("x" + m["k"]). Ahora el codegen la reescribe a get/insert, sobre Map locales, globales, parámetros y campos de struct. Dos fallas más salieron al probarlo: el tipo de valor de un Map<String, int> se deducía del primer argumento de tipo (la clave), y un Map<String, float> devolvía los bits del double como entero (1.5 → 4.6e18, sin error) en get y get_or; ahora se reinterpretan con bitcast. Cierra tres fichas de TASKS. Regresión: test-470; receta 06.
  • Los valores String de un Map se guardan enteros. El codegen guardaba un String en un Map como char* y lo leía con strlen. Cuatro fallas con esa raíz: un valor con un byte 0 (gzip, hash crudo, imagen) volvía cortado sin error —el caché de gzip de std/serve recibía 3 bytes—; values() sobre un Map<String, String> entregaba esos char* como String y el programa pedía 700 GB y moría; el camino opaco (get como entero) daba basura; y en los Map globales insert ya guardaba el nyx_string* mientras get lo leía como char*. Ahora se guarda el nyx_string* entero, como cualquier otro puntero, en los ocho sitios del codegen (literal, métodos, globales, campos de struct, get_or). El runtime no cambia; como la convención la usa el propio compilador, se recompilaron los nueve módulos con punto fijo en dos pasadas. Los objetos [lib] cacheados se invalidan solos (la huella incluye el compilador). Las claves siguen como char*: una clave con un byte 0 se corta (límite conocido). Regresión: test-469, un caso por camino.
  • e.code con la numeración de errno de Linux en todas las plataformas. La CRT de MSVC numera distinto desde el 35 (y el 26): ECONNREFUSED 107 en vez de 111, y hay colisiones que clasificaban mal —un host inalcanzable en Windows salía como timeout—. Nueva os_errno_canon (runtime/os/errno_canon.h, tabla por nombre simbólico, identidad en Linux) en un solo punto de la frontera de net.c: las 18 funciones *_result pasan a envoltorios de su cuerpo *_native, que sigue viendo el número nativo. test-374 pasa en Windows; suite de runtime nueva test_os_errno_canon.c. Implementado y medido en la laptop Windows (W4 Task 9).
  • Lectura y escritura de sockets nativas sobre IOCP en Windows. os_ev_read/os_ev_write/os_ev_cancel de win32 dejan de ser -ENOSYS, sin cambiar el contrato de os_ev: el read es un WSARecv de cero bytes más un recv no bloqueante al completarse (el kernel nunca escribe en el buffer del llamador, y un cancel no pierde datos), y el write envía desde una copia interna. loop_free cancela y drena lo pendiente. test_os_ev.c corre completo en Windows (110/110) y en Linux (111/111), con dos casos nuevos portables (cancel_lossless, free_pending); gate 240/240. Implementado y medido en la laptop Windows (W4 Task 7).
  • Timers de Windows sobre IOCP, con tabla que crece. os_ev_timer/os_ev_wake de win32 dejan de ser -ENOSYS: esperan en GetQueuedCompletionStatusEx con el timeout del deadline más próximo, despiertan con PostQueuedCompletionStatus (agrupando wakes: a lo sumo uno en vuelo) y la tabla crece de 256 a 65536 como la de POSIX. event_loop_win32.c pasa a ser un adaptador fino. Antes, con más de ~72 goroutinas durmiendo, el resto no despertaba a tiempo (tabla fija y workers ocupados en el fallback): test-411 pasa de 0/20 a 20/20, y lo mismo con 1500 goroutinas; gate 240/240. Implementado y medido en la laptop Windows (W4 Task 5); test nuevo tests/runtime-unit/win/test_os_ev_timers_win32.c. La verificación final del arreglo de test-241 sobre main dio 300/300.
  • await anidado en win32: el scheduler ya no reusa la dirección TLS a través de os_ctx_swap . nyx_goroutine_join (con yield inlineado) calculaba la dirección de g_current_worker una vez y la reusaba después de cada swap; si la goroutina volvía en otro worker, leía el slot del hilo anterior y hacía swap hacia el scheduler de un worker que estaba corriendo otra cosa: dos hilos sobre la misma fiber, con SEGV, cuelgue o el abort de «entry retorno». test-241 fallaba ~7-29% en win32 según la máquina; en Linux no, porque el backend ELF re-evalúa la dirección en cada uso. Todas las lecturas pasan por un accessor noinline. Diagnóstico y parche de la laptop (W4 Task 3, spike 2026-09-29-w4-test241-fibers): 723/723 corridas y gate 36/36 en win32.
  • nyx_goroutine_block_on_fd ya no deja una goroutina bloqueada para siempre. Ignoraba el -1 de nyx_event_loop_add: sin nada registrado que la despertara, la goroutina quedaba BLOCKED y su join no volvía (en win32 con cualquier fd; en POSIX con uno inválido). Ahora deshace el estado y devuelve -1. Test nuevo en test_scheduler.c, con un alarm que convierte el cuelgue en falla.
  • s[i] sobre un String lee el carácter correcto. codegen_index no distinguía el receptor String y caía al get de Array —misma forma en memoria—, que lee 8 bytes en la posición i*8: "abcdefghijklmnopq"[1] daba la i en vez de la b, [2] la q, y let c: int = "abc"[0] daba 6513249, todo con rc 0. [0] hacia un char salía bien por casualidad. Ahora es el byte i, igual que s.charAt(i) (y fuera de rango aborta igual). Encontrado midiendo falsos positivos de NYX2019. Regresión: test-457, que compara contra charAt en ocho contextos (let anotado int/char, sin anotar, comparación con char y con int, bucle, índice calculado, argumento de builtin). Gotcha string-index-byte.
  • nyx archivo.nx muestra los avisos del compilador. Con compilación exitosa el wrapper tiraba el log entero, así que ningún ⚠ llegaba al usuario en ese camino —tampoco NYX2019, que en 0.35.x es aviso justamente para que se vea antes de ser error en 0.36.0—; nyx build ya los reenviaba desde el arco compilacion-separada. Encontrado probando la 0.35.0 instalada. Guarda en run_tooling_gates.sh (make test-ai-first), con control positivo: un programa sin avisos no muestra ninguno.

Cambiado

  • NYX2019 es error también en el let a un entero: let n: int = fila[0] con un elemento que el compilador sabe String (Array<String> anotado o inferido de su literal, o split()) compilaba y guardaba la DIRECCIÓN del String como si fuera el número. En 0.35.x fue ⚠ warning, un release antes del rechazo (regla 7 de docs/VERSIONING.md); desde 0.36.0 la compilación falla con la función y la variable. Se convierte explícito (string_to_int) o se declara String. Los casos nyx2019-let-int y nyx2019-split-int del runner de errores pasan a esperar el error; la guarda de avisos de nyx archivo.nx usa ahora el aviso de «untyped Array».
  • Conducta nueva de defer: corre al cerrar el bloque que lo contiene . Al caer al final, por return, por ? con Err, por break o continue; LIFO en el bloque y de adentro hacia afuera; en el cuerpo de un loop, una vez por iteración; el valor de return se evalúa antes de los defer y el cuerpo del defer ve los valores al salir (semántica de Zig/Swift, no de Go). Se midió con el corpus completo (repo, nyxerp, stacks): 16 defer, ninguno fuera del nivel superior, así que ningún programa conocido cambia de resultado. Un throw que sale de la función también corre los defer alcanzados (arco defer-alcanzado-y-throw, en este mismo release).
  • Conducta nueva del sombreado en bloque: tras el bloque se lee la variable de afuera . Un programa que dependía de leer o asignar, después del bloque, la variable interna que había tapado a la de afuera ahora usa la de afuera. Se midió con NYX_SCOPE_AUDIT=1 (en el checker: informa cada uso de un nombre donde checker y codegen veían variables distintas) sobre 1286 archivos del repo, el ERP nyxerp y los stacks: cero casos reales, así que ningún programa conocido cambia de resultado. Para revisar código propio: NYX_SCOPE_AUDIT=1 nyx check archivo.nx (líneas con prefijo scope-audit:). let x = x + 1 en el mismo alcance sigue reemplazando, como siempre.
  • El cambio de contexto de las goroutines es ensamblador propio en Linux aarch64 (antes solo opt-in): swapcontext de glibc hace un rt_sigprocmask por cambio, ~2230 ns por yield contra ~175 ns en asm (64 goroutines, 1 worker). Validado con la suite de runtime y 23 programas de goroutines en las dos rutas; -DNYX_OS_CTX_ASM=0 vuelve a ucontext. macOS y x86_64 siguen con ucontext. Decisión de Ottavio.

0.35.0 2026-09-29

MINOR porque rechaza código que antes compilaba (): NYX1036/NYX2010 sin la transición de 0.34 y NYX2019 (error en el let a bool y en la asignación; aviso en el let a un entero, que pasa a error en 0.36.0). Incluye además todo lo acumulado desde 0.34.0: nyx exec, el espejo público con runtime/wasi/, cripto en wasm32-wasi, HttpOpts.cas_extra, arena_persist fase 1 y los arreglos de abajo. Verificado con make test-all sobre el corte; la única falla es test-wasm-29-arena-autocancelacion bajo el shim de node, intermitente y ya fichada (BAJA): falla igual con 0.34 (5 de 10 corridas contra 4 de 10).

Cambiado — rechaza código que antes compilaba (por eso 0.35.0)

  • NYX1036 y NYX2010 sin transición: son error. En 0.34 los casos que aparecían solo por el arreglo del resolvedor del 2026-09-24 (el cierre de un import transitivo reabre al módulo padre, y hasta 0.33.x ese tramo se atribuía al archivo principal) salían como ⚠ aviso y compilaban. Ahora son error como cualquier otro: agregar pub a la función o calificar la llamada. Se retiró todo lo que sostenía la atribución de 0.33.x en semantic y codegen, y resolve.nx ya no emite //#module-reabre. Consumidores medidos antes: nyx-proxy sin casos, nyxerp resueltos en su línea viva, nyx-queue arreglado en su repo (mq_ensure_queue pasa a pub).
  • NYX2019: un elemento de Array que el compilador sabe String no entra en una variable int o bool. let n: int = fila[0] sobre ["42"] guardaba la DIRECCIÓN del String como si fuera el número y salía 0; con bool, y en la asignación n = fila[0], era IR inválido al enlazar, reportado como bug del compilador. El let con bool y toda asignación son error desde ya (nunca enlazaron, así que no se rompe nada que anduviera). El let a un entero que no es bool, que sí compilaba con el valor equivocado, es aviso en 0.35.x y pasa a error en 0.36.0 — la regla 7 de docs/VERSIONING.md: lo que va a rechazar código avisa un release antes. Convertir con string_to_int o declarar String. Complementa a NYX2014, que actúa en runtime cuando el tipo del elemento no se conoce. Regresión: tres errores (let bool, asignación a int y a i32), dos avisos que compilan (let int, split()), el control positivo y test-456.

Agregado

  • nyx exec [--version <id>] -- <cmd>: una sola versión del toolchain para toda una suite (nyxerp, 2026-09-29). El wrapper fija la versión por proceso, así que una suite que lanza muchos nyx test sueltos la resolvía en cada uno, y un nyx update a mitad la partió entre dos compiladores: hubo que relanzarla entera. nyx exec corre el comando con NYX_HOME fijo en la ruta real de la versión (la activa, o una instalada con --version) y le hereda el candado de versión, así la poda tampoco la borra mientras corre. Y install-local/nyx update avisan si la versión que reemplazan está en uso. CLAUDE.md regla 3 decía que la suite ajena terminaba con su versión; valía solo para un árbol de procesos. Guarda: make test-toolchain (casos 5-6, con control positivo: la misma suite sin exec se parte).
  • CAPABILITIES.md dice qué builtins no existen en wasm32-wasi (pedido de nyxerp, 2026-09-27): cada uno lleva «· no existe en wasm32-wasi». std/builtins.index suma un sexto campo (no-wasm) que el generador saca de wasm_forbidden_builtin y de los codegen_target_guard con nombre de builtin (atomics, run). run_wasm_builtin_symbols.sh contrasta esa marca con lo que wasm frena DE VERDAD —compila una sonda por builtin— y falla si difieren: fue el cruce el que encontró los cuatro que no venían de la lista. Un índice viejo de cinco campos sigue sirviendo.
  • Hashes, HMAC y PBKDF2 en wasm32-wasi (fricción de nyxerp, 2026-09-27). sha256, sha256_raw, md5, hmac_sha256, hmac_sha256_raw y pbkdf2_hmac_sha256 eran «not supported on target 'wasm32-wasi'»: crypto.c no entraba al runtime wasm porque incluía OpenSSL, y un POS offline tenía que pedirle PBKDF2 a WebCrypto desde JavaScript. Hashes y HMAC ya eran C propio; PBKDF2 pasa a serlo también (antes PKCS5_PBKDF2_HMAC), con el HMAC sobre buffers de pila —en wasm sin arena nada se libera, y 80.000 iteraciones reservando en cada una eran varios MB—. Mismos bytes que OpenSSL: vectores del RFC 7914 y dos casos fijados con OpenSSL antes del cambio (test_crypto.c). Lo de OpenSSL (Web Push) queda detrás de __wasi__. Regresión: test-wasm-51-cripto (wasmtime y shim del navegador).
  • HttpOpts.cas_extra: sumar certificados a UNA petición HTTPS (fricción de nyxerp, 2026-09-27). Un sitio que manda un intermedio que no corresponde a su hoja no verifica contra las CAs del sistema; desde la separación de almacenes del 2026-09-14, tls_set_ca_file ya no llega al HTTPS de std/http, y SSL_CERT_FILE reemplaza en vez de sumar, para todo el proceso y solo antes del primer https. cas_extra (contenido PEM, uno o varios) se suma a las CAs del sistema solo para esa conexión (SSL_set1_verify_cert_store sobre un almacén propio del SSL), con la cadena y el nombre verificados igual; el resto del proceso no se entera. PEM sin certificados legibles = Err(invalid, 22). A bajo nivel, try_tls_connect_cas (std/tls) y nyx_tls_connect_result_cas (runtime). Regresión: sub-suite test_http_cas_extra.py (PKI de prueba con intermedio equivocado: 5 casos, incluidos «no se filtra a la petición siguiente» y «el nombre se sigue verificando»). Receta 122-https-intermedio-extra.
  • arena_persist cierra su fase 1:
    • NYX1039 en semantic: persistir un struct, un Map, un Fn o un Array<Struct> se rechaza en nyx check, no solo al generar código; semantic además ve el tipo del ELEMENTO de un Array, que codegen no conoce.
    • Arrays anidados: un Array guardado dentro de otro (literal, push, a[i] = x) lleva ahora tag Array en vez de ninguno, y arena_persist lo copia en profundidad (antes abortaba). Las lecturas tipadas que chocan con ese tag —leer un Array como String o float— pasan de devolver basura en silencio a abortar con diagnóstico; como int siguen dando el valor crudo. Verificado: el compilador con los tags nuevos se compila a sí mismo sin abortos en falso (punto fijo en tres pasadas).
    • En nativo es la identidad (decisión de Ottavio): no hay arena por evento y el GC ya conserva el valor, así que el mismo código corre con nyx test y en wasm sin ramas por target. Antes era un error de target.
    • Documentación: SPEC (NYX1039), LLM.md §wasm, gotcha wasm-arena-closure-env, receta 121-arena-persist-wasm. Regresión: 3 casos negativos y 1 positivo en tests/compiler/errors/, y el test wasm 36 suma un array anidado con enteros calculados (10.000 eventos, arena encendida).
  • nyx update --sync-docs ya no pisa lo que un proyecto escribió en su AGENTS.md. Hasta acá resembraba el archivo entero y solo salvaba un bloque proyecto:inicio…proyecto:fin que el proyecto tenía que conocer; nyxerp perdió así sus «Reglas de este proyecto» varias veces. Ahora nyx reemplaza SOLO su bloque, entre las líneas <!-- nyx:inicio --> y <!-- nyx:fin --> (lo siembra nyx init); lo de afuera es del proyecto, y lo propio va arriba, donde el agente lo lee primero. Un AGENTS.md anterior se migra por secciones ## : la que la plantilla no reconoce como suya se conserva textual arriba, y el sync informa qué conservó y qué reemplazó (con .bak). nyx sdd init pone su disparador arriba del bloque. Lógica en nyx_build agents-merge (agents_merge, compiler/build.nx), la misma para init y --sync-docs. Regresión: escenarios (g)–(k) de run_sync_docs_migration.sh; sobre una copia del AGENTS.md real de nyxerp, el wrapper anterior dejaba 0 de sus reglas y el nuevo las conserva byte a byte.
  • Plazo de envío en los servidores HTTP y plazo de cabecera en http_serve/http_serve_mt. NYX_HTTP_SEND_SECS (30 s por omisión) acota cuánto puede quedar trabada la escritura de una respuesta sin avanzar: un cliente que dejaba de LEER una respuesta grande retenía su worker de std/serve para siempre. Es por escritura, no total (una descarga lenta que avanza no se corta). Los dos servidores de std/http leían además sin plazo: una conexión que goteaba la cabecera (slowloris) dejaba mudo a http_serve, que tiene un solo hilo; ahora usan el parser con plazo de std/serve (NYX_HTTP_HEADER_SECS, 408 y cierre). Runtime: os_sock_set_send_timeout (solo SO_SNDTIMEO) y nyx_tcp_set_send_timeout. Regresión: sub-suite nueva test_http_plazos.py (8 checks, en rojo con el std/http anterior) y 1 check en test_serve_std_keepalive.py (22 → 23).
  • std/serve anuncia su plazo de keep-alive: cada respuesta con Connection: keep-alive lleva Keep-Alive: timeout=N (N = NYX_HTTP_KEEPALIVE_SECS). Un proxy con pool que no lo sabía reutilizaba la conexión ya cerrada por el servidor y a un POST —que no se reintenta— le tocaba un 502 (nyxerp → sumain2, 2026-09-25). El lado del proxy (descartar conexiones muertas o vencidas antes de escribir) es un encargo a nyx-proxy. Regresión: 1 check en test_serve_std_keepalive.py (21 → 22).

Arreglado

  • El mensaje de un assert puede ser un valor opaco (fricción de nyxerp, 2026-09-17). assert(arr[0] == "x", arr[0]) con un Array devuelto por una fn no enlazaba: el mensaje llegaba como i64 a nyx_string_to_cstr, que pide %nyx_string* (IR inválido), con nyx check en verde. El arco builtins-arg-opaco había convertido los argumentos de los builtins, pero assert es sentencia y quedó afuera. Los tres sitios de codegen_assert que evalúan el mensaje (camino genérico y ramas int == int y String == String) pasan por codegen_arg_str. La ficha decía que en main() andaba; medido hoy, fallaba igual. Regresión: test-455 (una línea por sitio, cada una rompía el enlace sola).
  • nyx update dejaba la versión sin runtime/wasi/ y nada compilaba a wasm32-wasi (fricción de nyxerp, 2026-09-29). El espejo público —del que construye nyx update— nunca tuvo esa carpeta: sync_to_public.sh copiaba runtime/*.c, que no entra en subcarpetas, y solo runtime/os/ iba explícito. Pasó inadvertido porque las instalaciones de desarrollo salen de make install-local, que sí la copia. El sync copia ahora la carpeta entera y no commitea si faltan main_shim.c, nyx_arena.c, los gc.h u os_wasm.c. Verificado con install.sh desde el espejo en un HOME aislado y con nyx update real.
  • .to_string() sobre char y sobre enteros de ancho fijo (i8, u8, i16, u16, i32, u32) abortaba con NYX2002. char, i8 y u8 comparten i8 en LLVM: el helper recibe ahora el tipo Nyx del receptor, así que un char da el carácter («A») y un u8 el número («200»), y el signo decide sext/zext. El catálogo de NYX1022 suma char (solo con anotación, como los demás escalares). Pila de codegen_method_call sin cambios (formas auxiliares sin literales). Regresión: test-454.
  • NYX1022 también sobre int, float y bool. El catálogo de métodos por tipo de semantic cubría String, Array, Map, Option y Result; sin catálogo, un escalar aprobaba cualquier método, y x.contains("a") con x: int bajaba en codegen por la rama de arrays (un i64 puede ser un Array sin tipo): IR inválido con nyx check en verde. Sobre un escalar solo existe to_string. El chequeo vale solo para variables DECLARADAS con anotación escalar (var x: int): un escalar inferido puede mentir ([1, "dos"] se infiere Array<int> y a[1].contains("d") es legítimo, test-323), así que ahí sigue decidiendo el tag en runtime. Verificado sin falsos positivos contra todos los módulos del compilador con semantic activo, el nyxerp real y los stacks. char queda sin catálogo (su to_string falta; ficha en TASKS). Regresión: test-nyx1022-metodo-sobre-int (test-errors 347 → 348).
  • let x: T = <escalar de otro ancho> convierte al tipo anotado. let s: int = w + h con w/h de tipo i32 guardaba un i32 en una variable declarada int: el .to_string() siguiente fallaba en codegen con nyx check en verde, y la aritmética seguía en 32 bits. La asignación ya convertía; el let no. Enteros entre sí y float/f32 entre sí; un bool anotado no se trunca. Regresión: test-452.
  • Un método sobre una const primitiva (K.to_string(), local o importada) daba «NYX2002: method 'to_string' is not available on the global 'K'»: una const int/float/bool se inlinea y no es variable, así que caía en la rama de globales. Ahora se materializa como variable local y sigue el camino normal; el trío de Map (contains/insert/get) sigue dando NYX2007 sobre una const. Regresión: test-453. De paso quedó fichado que ese mismo trío sobre una VARIABLE int da IR inválido sin diagnóstico (bug previo). El error genérico de «método no disponible sobre este receptor» (el catch-all de codegen_method_call) no llevaba código: ahora es NYX2002, como el de los globales — un método inexistente sobre un const sigue dando NYX2002.
  • Interno: interfaz_de (la huella por interfaz de [lib], compiler/build.nx) compara contra literales char en vez de códigos ASCII, ahora que el bug que lo obligaba está arreglado.
  • Toolchain: una versión nueva ya no hereda la marca .fijada de la activa. install-local arma la versión nueva copiando la activa, y si esa estaba fijada (hoy, la de producción de nyxerp) la copia se llevaba la marca: la versión nueva quedaba fijada sin que nadie lo pidiera, y la poda nunca la iba a borrar. nyx_tc_publicar quita la marca antes de publicar. Guarda: run_toolchain_versions.sh (4b), en rojo sin el arreglo.
  • Un valor opaco se puede pasar directo a cualquier builtin. Un elemento de un Array sin tipo (get_args()[i], un Array devuelto por una fn), m.get(k) o el retorno de una Fn sin firma llegaba a los builtins como i64, y nadie lo convertía: read_file(args[2]) bajaba a call @nyx_read_file(i8* %<i64>) —IR inválido que clang rechazaba al enlazar, con nyx check en verde—. Medido: 76 de 87 builtins con un parámetro puntero (archivos, entorno, cripto, regex, red/TLS, mutex/canales/condvar/rwlock, panic, …). Un helper único (codegen_arg y sus cuatro formas, en compiler/codegen.nx) convierte el argumento al tipo que el handler ya declara: inttoptr hacia un puntero, ptrtoint hacia un slot i64 (así channel_send(ch, <String>), que tampoco enlazaba, funciona). Las cuatro formas no llevan el tipo como literal para no engordar el marco de codegen_call_expr (medido: +5,4 KB por nivel con el literal; con las formas, dentro del techo). Guarda nueva: run_builtin_arg_opaco.sh (en test-ai-first; 76/87 en rojo con el compilador anterior), y test-451 comprueba el resultado, no solo el IR (sha256, setenv/getenv, write/read_file, un String por un canal, un mutex sacado de un Array). Cierra dos fichas de TASKS.
  • std/postgres: el pool es seguro entre hilos (fricción de nyxerp, 2026-09-27). Con std/serve y 16 hilos compartiendo un pg_pool_new, el servidor se colgaba en 3 de 6 corridas; con 8, moría con «Índice 0 fuera de rango». try_pg_pool_get buscaba un slot libre y lo marcaba en uso en dos pasos sin lock: dos hilos se llevaban la MISMA conexión y mezclaban el protocolo en un socket. PgPool lleva ahora un mutex (campo mu) tomado solo para el estado de los slots, nunca durante E/S (conectar, el ROLLBACK de pg_pool_put, cerrar). Regresión: tests/postgres/09-pool-hilos.nx (16 hilos × 300 consultas sobre un pool de 4, cada respuesta comprobada contra su hilo): con el pool anterior falla 4 de 4 corridas, con el nuevo pasa 9 de 9.
  • std/serve: una conexión ociosa que vence con un pedido ya llegado se atiende, no se resetea. El barrido del estacionamiento (runtime/net.c) cerraba por plazo sin mirar si el socket ya tenía un pedido que el event loop todavía no había visto; cerrar con datos sin leer hace que el kernel mande RST, y el cliente veía ECONNRESET en un pedido que el servidor sí recibió. Ahora las vencidas con datos se entregan a los workers. Salió al analizar la prueba de carga de nyxerp (ECONNRESET solo bajo saturación); los datos apuntan sobre todo a la carrera de keep-alive del propio generador, así que esto es un blindaje, no la causa medida. Regresión determinista con un gancho -DNYX_RUNTIME_TESTING (hilo del estacionamiento quieto): test_http_park_vencida_con_pedido, en rojo sin el arreglo (el cliente recibe ECONNRESET).
  • Asignar un valor opaco a una variable ya declarada lo convierte a su tipo (fricción nyxerp 20260925-090024). let x: S = xs[0] andaba, pero var s: S = … + s = xs[0] emitía store %S %<i64>: IR inválido, nyx check callado y el enlace rechazado por clang. La conversión del valor opaco (xs[i], m.get(k), retorno de una Fn sin firma) vivía en línea en codegen_let y la asignación tenía una copia parcial (solo String y Array); ahora las dos usan coerce_opaque_i64 — struct, enum, Map, bool, char, enteros angostos, float, Array<T>, Fn(...) — y n = m.get(k) sobre un int declarado hace el ptrtoint. Regresión: test-450-asignar-valor-opaco-a-var-declarada (9 familias de tipo).
  • std/serve: HEAD ya no contamina la conexión keep-alive. Un HEAD caía al 404 y el 404 salía con cuerpo; un proxy que reutilizaba la conexión (nyx-gateway) leía esos bytes como el comienzo de la respuesta siguiente y devolvía 502 a otro usuario — visto en producción en serve.nyxlang.com el 2026-09-25. Ahora un HEAD sin ruta propia usa la ruta GET del mismo path (también en routers montados; una ruta HEAD explícita gana) y toda respuesta a un HEAD sale sin cuerpo con el Content-Length del GET (RFC 9110 §9.3.2); un sse_open no abre el canal ante un HEAD. Regresión: 8 checks nuevos en test_serve_std_keepalive.py (HEAD + GET pipelineados por la misma conexión; 13 → 21).

0.34.0 2026-09-25

Release grande, empujada casi entera por la fricción de nyxerp (ERP en Nyx) antes de su primera producción (Sumain, 2026-09-26). Lo principal:

  • [lib] modules listo para proyectos reales: cualquier forma de import, transitividad, chequeo de tipos idéntico a sin [lib] (el importador lee el módulo entero; codegen declara), nombres <módulo>__<fn>, y recompilación por INTERFAZ (un comentario en un módulo central: 1 módulo, no 105). Suite de nyxerp: 1.566 s → ~366-431 s.
  • Toolchain con versiones lado a lado en ~/.nyx: instalar ya no le cambia el compilador a una suite en curso; nyx update --version <rev> vuelve a una revisión instalada al instante; versiones .fijada que no se podan.
  • Stdlib nueva: std/pdf, std/barcode (Code 128, EAN-13, QR), std/browser_serial (Web Serial), LISTEN/NOTIFY en std/postgres.
  • std/serve: una conexión keep-alive ociosa ya no retiene un worker; plazos de inactividad y de cabecera.
  • Tooling: runtime precompilado automático (nyx build 9,9 s → 1,0 s), CAPABILITIES por firma de la stdlib, nyx check sin el protocolo LSP en pantalla, huella del compilador en nyx test.
  • wasm: una guarda contrasta todos los builtins con el runtime de wasm (25 casos arreglados o con error claro).
  • Transición: las llamadas a una fn privada de otro módulo (NYX1036) y las ambigüedades (NYX2010) que un bug del resolvedor ocultaba hasta 0.33.x siguen como AVISO en 0.34.0 y pasan a error en 0.35.0 (se mantuvo un ciclo más para no romper builds ajenos en la víspera de producción).

Fixed

  • Seguridad del toolchain compartido (fricción nyxerp 20260924-020001-team-4): nyx update --help actualizaba de verdad (todo argumento desconocido caía a la actualización); ahora --help/-h muestran ayuda, --check solo consulta y una opción desconocida es error (rc 2). nyx update toma el candado del toolchain EXCLUSIVO y nyx check/vet/fmt y nyx archivo.nx COMPARTIDO (con aviso y NYX_LOCK_WAIT). nyx test imprime la huella del compilador y avisa si cambió durante la corrida.
  • CAPABILITIES.md se regenera cuando cambia la stdlib, no solo el número de versión: por eso el índice de nyxerp no listaba std/smtp y lo reimplementaron a mano.
  • [lib] modules con la forma de import de los proyectos reales (fricción nyxerp 20260924-040001-team-1): import "src/x" sin llaves —la de nyx init y la de 3.372 de los 3.373 imports de nyxerp— no cruzaba la frontera (NYX1002 en cada llamada). Ahora declarar un módulo en [lib] no cambia qué nombres se ven: toda forma de import se comporta como inlineada, transitividad incluida, y viajan también los struct/enum sin pub. Las fns de biblioteca se emiten como <módulo>__<fn> igual que inlineadas (dos bibliotecas pueden tener homónimas). Arreglados de paso en el lector de firmas de las declaraciones: la flecha de un parámetro Fn(...) -> T se tomaba por la del retorno, una firma en varias líneas perdía el retorno en silencio (void), y el tipo genérico del retorno (Result<T, E>) se recortaba. Si un módulo usa una fn que no importa, nyx build lo dice con una nota y el import que falta. Verificado sobre nyxerp real: 150 módulos en [lib] compilan y enlazan; make test-lib-real lo vigila.
  • Privadas homónimas en dos módulos se pisaban según el orden de los imports (fricción nyxerp 20260924-010014-team-1): el resolvedor cerraba un import transitivo inlineado volviendo al archivo principal, así que todo lo que el módulo padre declaraba después quedaba atribuido al programa. Ahora el cierre reabre al módulo padre. El mismo bug apagaba NYX1036/NYX2010 en esos tramos: los casos que aparecen solo por la atribución corregida salen como aviso (compilan, rc 0) hasta la próxima versión menor. std/url y std/web comparten el url_decode de std/percent, nuevo. test-439, test-440, +4 casos en test-errors.
  • std/serve: una conexión keep-alive ociosa ya no retiene un worker (fricción nyxerp 20260924-200001-team-1): con N hilos, N navegadores quietos entre clics dejaban el servidor sin responder a NADIE. Tras responder, el worker quedaba bloqueado leyendo el próximo pedido sin plazo. Ahora la conexión se estaciona en el event loop del runtime (como el SSE) hasta que llega otro pedido. Límites: NYX_HTTP_KEEPALIVE_SECS (15 s de inactividad) y NYX_HTTP_HEADER_SECS (10 s para la cabecera completa: 408 contra slowloris); serve_idle_connections(). 13 checks E2E nuevos (6 fallan con el código viejo), receta 120.
  • Una fn privada de un módulo [lib] competía con la propia del que importa (regresión de la entrega anterior, fricción nyxerp 20260924-220014-team-1): NYX2010 «AMBIGUA» en 8 archivos de pruebas. Una privada ajena ya no le gana a una visible; nyx test solo atribuye un archivo de pruebas a su módulo si está en [lib]. Una versión del toolchain con .fijada no se poda nunca.
  • Cinco bugs viejos: import { K } de una const daba NYX1013; la visibilidad de las const dependía del orden de los imports; asignar charAt() a un var de ancho fijo emitía IR inválido; tres lecturas .node_type sobre Arrays en el propio parser se compilaban como 0 en silencio (los patrones E.V(a, b) nunca salían como match_pattern; ese catch-all ahora aborta con NYX2007); y try_tcp_read_timed no drenaba primero el buffer por fd. test-447..449.
  • std/postgres: las notificaciones que llegaban durante una consulta se tiraban (fricción nyxerp 20260924-180024-team-1). Ahora se guardan en la conexión y las entrega la próxima espera. Nuevo LISTEN/NOTIFY: try_pg_listen, try_pg_unlisten, try_pg_notify y try_pg_wait_notifications (con tiempo límite), en claro y con TLS. El bus de tiempo real entre varios procesos sobre PostgreSQL, sin Redis; con el SSE de std/serve cierra el caso. std/net: try_tcp_read_timed. test-446 (servidor falso), tests/postgres/08 (PostgreSQL real), receta 119.
  • [lib] recompila por interfaz, no por contenido (fricción nyxerp 20260924-150024-team-1): un comentario en un módulo central recompilaba 105 de 171 módulos de nyxerp (216 s); ahora 1 (22 s). La huella mira de cada biblioteca importada sus firmas, tipos, constantes y globales, no los cuerpos ni los comentarios.
  • [lib] ya no tiene hueco de tipos en la frontera (fricción nyxerp 20260924-150024-team-1): un String donde iba un int, llamando a una fn de otra biblioteca, pasaba nyx build en verde y llegaba como el puntero del texto impreso como número. Quien importa lee ahora el fuente completo de cada módulo de [lib] (el chequeo de tipos es idéntico a sin [lib], por construcción) y codegen emite sus fns como declare desde el AST, con la firma exacta. Se retiró la interfaz armada a mano (imports sintéticos, tipos inyectados, firmas leídas del texto). Los globales de una biblioteca los sigue inicializando el programa, en orden, como inlineados (guarda en run_lib_modules.sh).
  • Toolchain con versiones lado a lado (fricción nyxerp 20260924-020001-team-4) : ~/.nyx guarda cada versión completa en versions/<id>/, y current/active dicen cuál está activa; las entradas de la raíz son enlaces, así que las rutas de siempre (~/.nyx/bin, ~/.nyx/VERSION) siguen andando. make install-local y nyx update arman la versión nueva aparte y la activan con un rename: una suite a mitad de corrida sigue entera con la versión con que arrancó, instalar ya no espera a nadie, y un update que falla no deja la instalación rota. nyx update --version <rev> vuelve a una revisión ya instalada al instante (415 ms). La poda (NYX_KEEP_VERSIONS, 3) nunca borra una versión en uso. La primera instalación con el toolchain nuevo migra el layout plano sola. make test-toolchain.
  • nyx check muestra solo los diagnósticos: volcaba cientos de líneas SYM:/DEF:/END del protocolo del LSP aunque el archivo estuviera sano. El protocolo sigue disponible con --lsp. Y después de un NYX1040 (el mismo struct en dos módulos) ya no sale la cascada de NYX1017/NYX1032 contra la primera definición, que mandaba a buscar el error donde no está.
  • Ningún builtin muere en wasm-ld con «undefined symbol»: string_from_bytes (y con él base64_decode) no enlazaba en wasm32-wasi porque su runtime vivía fuera de wasm.srcs. Una guarda nueva (run_wasm_builtin_symbols.sh, en make test-wasm) contrasta todos los builtins con el runtime de wasm y encontró 25 casos. Las funciones de C puro se mudaron a unidades portables; las que dependen de procesos, red, señales u OpenSSL dan un error claro con archivo y línea. test-wasm-50.
  • Una mención de compiler/ en un comentario o string quitaba el prelude: println no declarado. Ahora solo cuenta un import "compiler/..." o la marca // nyx:sin-prelude. test-435.

Added

  • Runtime C precompilado automático en ~/.cache/nyx/rt/ para nyx build y nyx test (clave por contenido; NYX_NO_RT_CACHE=1 lo apaga): nyx build 9,9 s → 1,0 s, un archivo de nyx test 9,7 s → 1,2 s, sin configurar nada.
  • std/barcode (fricción nyxerp 20260923-210010-team-1): Code 128 (juegos B/C elegidos solos), EAN-13 (calcula o valida el control) y QR versiones 1-10 con niveles L/M/Q/H (qr_matrix), en Nyx puro y con paridad en wasm. El QR se verifica con los vectores publicados del estándar y con un decodificador independiente escrito en el test. test-436..438, test-wasm-42..44, receta 116.
  • std/browser_serial (fricción nyxerp 20260924-020001-team-2): Web Serial con await en wasm para impresoras de punto de venta y fiscales (serial_request_port, serial_open, serial_write, serial_read con tiempo límite, serial_close). Los bytes cruzan como Array<int>. Sin Web Serial (Firefox, Safari, sin HTTPS) da Err de tipo io. WebUSB queda para una fase 2. test-wasm-41 (con un mock), receta 117.
  • std/pdf (fricción nyxerp 20260924-020001-team-1): PDF en Nyx puro, también en wasm. Tamaños carta, A4, media carta y ancho libre; posiciones en mm desde arriba a la izquierda. Texto con las fuentes estándar y WinAnsi (tildes, ñ, ¿¡, €), con /Widths y pdf_text_width para alinear montos. Líneas, rectángulos, JPEG (también progresivo, con orientación EXIF) y PNG (alfa en SMask). Verificado con pypdf estricto, pdfminer y el motor de PDF de Chrome (pdfium), renders mirados. run_pdf_verify.sh (opcional). test-441..445, test-wasm-45..49, receta 118 (una factura).
  • CAPABILITIES.md indexa las async fn: std/browser_idb y std/browser_serial no aparecían.
  • Specs en BORRADOR para aprobar: std/pdf y toolchain atómico (versiones lado a lado).

Fixed (anteriores)

  • NYX2010 también en nyx check (fricción nyxerp 20260923-210010-team-3): una llamada sin calificar a una fn que existe en dos módulos importados salía recién en nyx build (en nyxerp, 3,5 minutos después de un check en verde). El checker ya detectaba la ambigüedad pero solo apagaba el chequeo de firma. Resolver por aridad cuando difiere queda como decisión de diseño abierta.
  • json_parse de un escape \uXXXX trapeaba en wasm («unreachable»). Causa en codegen: un argumento i64 no se truncaba al ancho de un parámetro char/i8/i16/i32; en nativo pasaba desapercibido. test-wasm-40.
  • json_stringify lineal (fricción nyxerp 20260923-210010-team-2): escribir un texto de 32 KB pasaba de 504 ms a 0,66 ms — antes crecía con el cuadrado del largo. Un solo StringBuilder recorre el árbol y los tramos sin escapes se copian de una vez.
  • "".trim() bajo la arena de wasm devolvía largo 1 o 2 (fricción nyxerp 20260923-100024, terminaba en «memory access out of bounds»). La causa no era trim: el String vacío que comparte todo el runtime nacía en memoria del turno si se pedía por primera vez durante un evento, y la arena lo reciclaba. Afectaba a trim, repeat(0) y a los ~200 nyx_string_from_cstr("") del runtime. Ahora es un objeto estático. test-wasm-39.
  • json_parse en wasm: 5.000 registros de 6,3 s a ~0,15 s en node y de 235 a 35 MB (fricción nyxerp 20260923-100024). La región persistente pide memoria en bloques geométricos (64 KB → 8 MB) en vez de hacer crecer el wasm de a una página, y parse_string copia por tramos en vez de concatenar byte a byte (cuadrático). Lo que queda (~18 B por byte de JSON) es el tamaño del árbol.
  • Dos struct o enum con el mismo nombre en módulos distintos: NYX1040 en vez de «redefinition of type» sobre un archivo temporal (fricción nyxerp 20260923-160005). Sale en nyx check y en el build, nombrando los dos archivos y líneas. Dos enum idénticos se aceptan (hoy compilaban). El nodo struct/enum ahora lleva la línea del NOMBRE, no la de la } de cierre.
  • wasm: un Array o StringBuilder global que crecía durante un evento quedaba apuntando a memoria del turno. Con la arena encendida, el realloc del crecimiento alocaba el bloque nuevo en la arena, que se recicla al terminar el evento: 1.000 push de enteros a un global daban una suma basura. Ahora cada bloque persistente lleva una marca en su cabecera y realloc conserva la región de origen (O(1), sin tocar alloc/reset/pin). test-wasm-38.
  • Los enteros calculados llevan tag en el Array: antes solo los literales. La evidencia es la forma del AST (aritmética, length(), : int explícito), nunca el tipo inferido. Efecto visible: a.push(n * 3) leído como String pasa de SIGSEGV a un abort ordenado.
  • El compilador ya no se queda sin pila con expresiones largas: de 52 a ~2.400 operandos con la pila por omisión. La causa, medida con clang -fstack-usage: el toolchain se enlaza en -O0 y cada nivel de una expresión apilaba ~151 KB de marcos de codegen. Tres piezas: (1) codegen_binop y codegen_expr partidos para que la recursión pase por marcos chicos —refactor puro, IR idéntico en 524 archivos— (151 → 23 KB por nivel, techo 322); (2) SROA sobre las semillas cuando hay un opt de la misma versión que clang, +2 s por bootstrap (23 KB → 2 KB por nivel, techo ~2.400; sin opt, 322); (3) el desborde de pila del hilo principal tiene nombre: una recursión infinita en main imprime «desborde de pila del hilo principal» con el límite, en vez de un «Segmentation fault» mudo (POSIX; Windows queda para W4). Guarda nueva run_stack_frame_audit.sh en make test-ai-first, un ratchet de los marcos del camino recursivo. El piso que queda es CodegenContext por valor: su raíz es el arco en borrador structs-byval.
  • Una expresión de 53 operandos hacía caer al compilador con SIGSEGV y sin mensaje (fricción nyxerp 20260920-200024: una hoja de estilos de 75 concatenaciones, una hora de bisección a mano; nyx check y nyx vet daban verde). Causa medida: el toolchain se enlaza sin -O y cada nivel de una expresión binaria apila ~151 KB de marcos de codegen (codegen_binop solo, 132.880 B en -O0 contra 1.488 B en -O2), así que 53 niveles agotaban los 8 MB por omisión. Mitigado: nyx, nyx build y nyx test suben a 64 MB la pila DEL COMPILADOR —el programa del usuario conserva la suya— (techo de ~52 a ~430 operandos), y si igual muere, el mensaje nombra la causa en vez de dejar solo «Segmentation fault». Las recetas del Makefile también la suben (desde scripts/with_testroot_lock.sh): make recompile-all ya no podía compilar lexer.nx en 8 MB. La causa de raíz queda en el spec pila-del-compilador (BORRADOR, GO pendiente).
  • Recorrer un String con for … in mataba el programa con SIGSEGV y sin ubicación (fricción nyxerp 20260921-100001). nyx check, nyx vet y nyx build daban verde: codegen no tiene camino de String, así que el iterable caía al de arrays y el %nyx_string* se leía como { i64, i8* }*. Ahora es NYX1038 con archivo y línea, y la pista del recorrido por índice (bytes con substring, caracteres UTF-8 con char_substring); la misma regla vive en codegen para cuando semantic está apagado. Es error y no una capacidad nueva a propósito: recorrer un string obliga a decidir entre bytes y codepoints, y eso queda abierto. La variante dinámica —un String dentro de un Array pelado, recorrido con for g: Array in xs— reventaba en el bucle INTERIOR, lejos del culpable: ahora el for lee el slot chequeando su tag y aborta en el exterior con NYX2018 nombrando el slot y su tipo real (y for s: String in xs sobre un slot int usa la lectura chequeada que ya existía). Gotcha for-in-string-rejected.
  • semantic era CUADRÁTICO en el tamaño del cierre: las tablas de símbolos y de firmas se recorrían enteras, comparando strings. scope_lookup y fn_sig_index escaneaban g_sym_names y g_fn_sig_names completos en cada consulta. Como el resolvedor inlinea el cierre de imports en una sola unidad de traducción, esas tablas crecen con el número de módulos mientras el número de búsquedas crece con el volumen de código: el producto es el cuadrático. Ahora hay un índice por nombre (Map<String,int> + un array paralelo que encadena los homónimos en orden de declaración) sobre las dos tablas, y los otros seis consumidores —scope_get_arity, lookup_var_type, name_is_builtin, fn_es_privada_de, fn_module_resolve, scope_has_in_current— recorren la cadena del nombre en vez de la tabla.

    Medido (aarch64, mediana de 3, con la máquina quieta verificada), sobre un proyecto sintético de 175.375 líneas de IR —la escala del cierre de nyxerp, que medía 184.080—: front-end 4.680 → 2.700 ms (−42,3%), exponente sobre cuatro tamaños 1,20 → 1,04, y el costo por línea de IR de 26,7 → 15,4 µs (antes se duplicaba entre 7,5k y 175k líneas; ahora es plano). Pico de RSS sin cambios: 288 → 286 MB. make bench-test-cache lo reproduce. El perfil cambió de forma: semantic pasa de 54,9% a 16,8% del front-end, y nyx_string_equals de primera hoja con 24,4% a sexta con 4,9%. El próximo cuello es la alocación (GC_generic_malloc*, 40%), fichado en TASKS.md. Confirmado desde afuera del core (nyxerp, 2026-09-21): su suite completa —112 archivos, 1.200 pruebas, contra PostgreSQL— pasó de 23 min 07 s a 20 min 43 s (−9,2%), medido contra MÁS trabajo (1.191 → 1.200 pruebas) y sin ventana tranquila, así que no es comparable con el 42% del front-end aislado: ahí el front-end es una fracción del reloj total. Y el efecto en MEMORIA resultó ser el que más les sirve: el pico de compilar y correr su archivo más pesado (39 pruebas) pasó de 1.357 MB a 198 MB — eso es el efecto conjunto de este arco y del fix del emit cuadrático (v0.32.2), porque su medición previa es del 2026-09-17. Con 2 núcleos y 3,8 GB, dos compilaciones a la vez ahora entran y antes no: el arco les destrabó un factor 2 que van a buscar por su cuenta. Cómo se verificó que no cambia comportamiento: cada función migrada corrió EN PARALELO con su escaneo original y un gate que abortaba la compilación si los dos caminos diferían, sobre make test (449), test-errors (309), test-m08-types (18) y la matriz de dispatch — ~776 pruebas, cero discrepancias, en las tres tandas. Recién con eso en verde se retiró cada escaneo. Los gates se verificaron saboteando el índice y el scope_pop, porque un gate que no se puede probar que falla no prueba nada. Un hallazgo que el sabotaje destapó: con el scope_pop sin deshacer su tramo del índice, make test-errors da 309/309 — el corpus entero del repo NO caza ese bug, y el compilador resuelve símbolos con el kind equivocado sin decir una palabra. Por eso todas las consultas pasan ahora por indice_cabeza(), que valida que la cabeza de la cadena esté viva siempre, sin variable de entorno: cuesta una comparación de enteros y convierte ese silently-wrong en un error ruidoso. Tests: test-432-module-fn-homonyms-caller (450 archivos / 449 ARM64) fija la resolución NO calificada desde dentro de un módulo —la regla de la que dependen scope_get_arity y fn_sig_index—, que test-353 no cubría. Diseño: docs/design/specs/2026-09-21-semantic-indice-simbolos-design.md.

  • Voseo en cinco líneas de std/smtp.nx, dos de ellas mensajes que ve el usuario («probá el 465», «Pedí STARTTLS primero … o conectá al 465»). La guarda run_voseo_messages.sh estaba en rojo desde el 2026-09-20, así que make test-all no pasaba.
  • Escribir por TLS a un peer que ya cerró MATABA EL PROCESO por SIGPIPE. OpenSSL escribe con write() crudo y este runtime no instala un SIG_IGN global de SIGPIPE a propósito (os_sock_send se protege con MSG_NOSIGNAL, y su comentario aclara que eso es «load-bearing»), así que la capa TLS quedaba sin esa protección: un tls_write_conn contra un peer ido mataba el proceso entero. runtime/tls.c ahora usa un BIO propio que escribe por os_sock_send, en vez de ignorar la señal en el arranque del runtime —que le habría cambiado la semántica a todo programa que enlace Nyx, un nyx prog | head incluido—. Los 5 SSL_set_fd del archivo pasan por tls_attach_fd, con el mismo contrato (BIO_NOCLOSE: el fd lo cierra el llamador) y sin depender de los BIO_meth_get_*, deprecados desde OpenSSL 3.5.

    Por qué recién ahora: en una respuesta HTTP normal la ventana entre «el cliente cerró» y «el servidor escribe» es de milisegundos. En un TÚNEL dura lo que dure el stream, así que alcanzaba con cerrar una pestaña para tirar abajo un gateway que sirve todos los dominios. Lo encontró la sesión de nyx-proxy armando el túnel SSE (medido: rc=141 = 128+SIGPIPE). ws_tunnel tenía el mismo agujero desde 2026-07-01 y queda cubierto por el mismo fix. Tests: test_tls_write_to_gone_peer_no_sigpipe en tests/runtime-unit/test_tls.c (+4 asserts, 264 → 268), que mide si la señal SE GENERA en vez de si el proceso sobrevive — la suite tenía signal(SIGPIPE, SIG_IGN) en su main y por eso no podía ver este bug. Confirmado desde afuera del core (nyx-proxy, 2026-09-20): con el gateway recompilado contra este runtime y sin mitigación en ningún lado, un cliente SSE sobre TLS que corta en seco con RST a mitad del stream deja el proceso vivo; el mismo caso moría con rc=141 contra el runtime anterior. El relay no cambió (primer evento a 0.01 s, el segundo a 0.31 s contra una pausa de 0.3 s del backend): rutear por os_sock_send no metió latencia ni buffering. Quedó como control positivo permanente de ese lado — si vuelve a morir con 141, la regresión es del runtime.

  • test-377-try-http fallaba por puertos, no por código: usaba 58744-58749, que caen dentro de /proc/sys/net/ipv4/ip_local_port_range (32768-60999), así que el kernel se los daba como puerto de ORIGEN a cualquier conexión saliente y el test moría con «cannot bind port NNNNN: Address already in use». Cayó tres veces el 2026-09-15 y otra el 2026-09-20. Movidos a 187xx, por debajo del rango efímero. Un rojo que no es regresión tapa las reales.
  • El E2E tests/integration/test_ws_proxy.py SKIPeaba en silencio desde la migración a ~/nyx/ (2026-07-22): su NYX_PROXY_SRC por defecto apuntaba a ~/nyx-proxy-stack, que ya no existe. Default corregido a ~/nyx/products/proxy y skip más explícito.

Added

  • nyx update --sync-docs conserva el bloque propio del proyecto en AGENTS.md (pedido de nyxerp, 2026-09-24: les borró cuatro veces sus «Reglas de este proyecto»). Lo que esté entre <!-- proyecto:inicio --> y <!-- proyecto:fin --> sobrevive al resembrado, y una corrida que no cambia nada ya no deja un .bak nuevo (antes lo dejaba siempre). Documentado en la plantilla.
  • nyx test con [lib] modules: las bibliotecas se compilan una vez por suite y cada archivo de prueba solo las enlaza (6,4 s → 1,4 s por archivo en el banco de 250 módulos).
  • form_values/query_values en std/web (fricción nyxerp 20260923-160005): todos los valores de una clave repetida (casillas, <select multiple>), en orden; parse_form_data_all y parse_query_string_all devuelven los pares sin colapsar. req.form no cambia: con claves repetidas sigue ganando la última, y ahora la doc lo dice. Receta 115.
  • [lib] modules en nyx.toml: compilación separada con reutilización de objetos . nyx build compila cada módulo declarado una vez a target/nyx-lib/ y lo reutiliza mientras no cambien él ni su cierre de imports; tocar un módulo hoja y recompilar cuesta el 41% (88 módulos) y el 23% (250) de hacerlo sin [lib]. Cruzan funciones, structs y enums, por import { f } from y por import "m" as a; los genéricos, impl y traits caen al inlining con el aviso NYX0302, que ahora se ve aunque el build salga bien. nyx build además honra NYX_RT_ARCHIVE. Límites: sin chequeo de tipos de los argumentos en la frontera, y nyx test todavía no usa [lib].
  • arena_persist(x) y arena_stats() en wasm, fase 1 PARCIAL: copia un String o un Array (de String/int/float/bool) de la arena del evento a la región persistente, para guardarlo entre eventos; arena_stats() mide bytes persistentes y del turno desde adentro. Un struct/Map/Fn es NYX1039 (hoy en codegen; el chequeo en semantic y la documentación siguen pendientes).
  • std/browser_idb: IndexedDB para PWAs sin conexión (pedido de nyxerp). idb_get/idb_put/idb_delete/idb_keys asíncronos (await, Asyncify) con Result<_, Error> y los kinds de siempre (not_found, in_use, oom, io); una sola base con clave compuesta [store, key], para que un store nuevo no dispare una subida de versión que bloquee entre pestañas. Módulo aparte: solo paga Asyncify quien lo importa. opts.idb inyectable en el shim para testear sin navegador (test-wasm-35-idb), receta 114-idb-offline-wasm.
  • make bench-test-cache — banco de medición del costo de compilar un archivo de prueba, por fases (front / clang_ll / link / run / hash), sobre proyectos sintéticos en tres capas con reuso, de 8 a 250 módulos. Nació para re-medir la ficha de la caché de binarios por hash del cierre y terminó siendo el instrumento del arco.

    Exige una ventana tranquila antes de medir: espera a que no haya compiladores ajenos, y si alguno entra a mitad de una repetición la descarta y la repite. Si la espera vence, ABORTA nombrando el proceso que ocupa la máquina en vez de reportar ruido. No es paranoia: la primera línea de base del arco salió 9% alta porque otra sesión corría su suite en la misma máquina de dos núcleos, y con la guarda puesta el banco repite al 0,1%.

  • std/smtp — mandar correo desde Nyx (pedido de nyxerp, que se quedó sin forma de mandar un enlace de recuperación de contraseña: la única salida que su sistema pudo ofrecer fue crear un usuario nuevo). Conexión por los dos caminos reales —587 con STARTTLS y 465 con TLS desde el saludo—, AUTH PLAIN/LOGIN, y el mensaje con headers, cuerpo de texto o HTML y adjuntos binarios en base64. Cero builtins nuevos: la pieza de bajo nivel ya existía (tls_upgrade_fd, que es la forma exacta de STARTTLS y que std/postgres ya usaba así).

    Lo que el módulo resuelve solo, que es donde se rompen las implementaciones a mano: el dot-stuffing (una línea del cuerpo que empieza con . sale duplicada; sin eso el servidor da el mensaje por terminado ahí y entrega la mitad, SIN ERROR de ningún lado), la normalización a CRLF, el base64 cortado a 76 columnas, el Message-ID con el dominio del remitente, y el segundo EHLO después de cifrar —obligatorio, porque las capacidades cambian—. AUTH sobre un canal en claro se rechaza y no hay flag para forzarlo: la comprobación mira el estado real del canal, no lo que el servidor anuncia. Y se verifica el certificado por omisión, con variantes _insecure que lo dicen en el nombre — mismo criterio que se le aplicó a https_get el 2026-09-11.

    Fuera de alcance por decisión del arco: recibir (IMAP/POP3), colas y reintentos, multipart anidado, quoted-printable, nombres de archivo RFC 2047 y DKIM/SPF/DMARC. Doc: docs/SPEC.md §Correo saliente, LLM.md §std/smtp. Receta: examples/by-example/113-smtp.nx. Tests: stdlib 5 → 9 (parseo y diálogo, STARTTLS/TLS/AUTH, armado del mensaje y envío E2E, y errores distinguibles por valor).

  • scripts/run_stdlib_tests.sh enlaza el runtime COMPLETO: tenía una lista recortada (sin compress.c, scheduler.c, event_loop.c y ocho más), así que un módulo de std que usara un builtin de cualquiera de esos no se podía testear ahí — fallaba en el linker con «undefined reference», no en el test. Un runner que enlaza un runtime distinto del que enlaza el toolchain no prueba lo que la gente corre.

Changed

  • SSE ya funciona detrás del gateway: nyx-proxy 0.4.4 tuneliza el stream (detecta Content-Type: text/event-stream y deja de acumular), desplegado en :443 el 2026-09-20 con los 8 dominios verificados por SNI. Se retira el aviso «detrás del gateway SSE todavía no funciona» de LLM.md §std/serve, docs/SPEC.md §SSE y examples/by-example/110-serve-sse.nx. Del lado del proxy: 8 casos de túnel + 1 sobre TLS, con las 11 de pool_framing intactas.

0.33.0 2026-09-20

Sube MINOR porque rechaza código que antes compilaba — código que compilaba y reventaba al correr, pero la regla del repo no distingue: lo que el compilador deja de aceptar cambia de minor.

Changed

  • Llamar a un valor Fn SIN FIRMA ahora se rechaza al compilar cuando el contexto espera un struct por valor o un float (NYX1037). Es la tercera aparición de la misma causa —un Fn sin firma no lleva su convenio de retorno y la llamada indirecta asume i64—, y esta vez se cierra en vez de parchearse: con un struct eso era un SIGSEGV (el i64 que llega es el primer campo, y se desreferencia) y con float, basura en silencio (0.16 → 281472821301216.0). Lo reportó nyxerp desde un middleware que compilaba limpio, pasaba nyx check y nyx vet, y moría en el primer pedido.

    Lo que NO cambia: int, String, bool, char, Result, Option, cualquier puntero y el resultado descartado siguen funcionando a través de un Fn sin firma — para esos el convenio asumido coincide, medido con valores. Tampoco cambia let f: Fn = una_fn seguido de f(): codegen refina ese slot con la firma real desde el 2026-08-06. Por qué no el convenio uniforme con thunks, que era la recomendación de la spec: es cirugía en el codegen de cierres —lo que sostiene dom_on_fn, los browser_*_fn y Result.map—, cuesta una asignación por llamada, y ni siquiera cierra el caso residual, porque el problema de let r = f() sin anotar no es el ABI sino la ausencia de tipo. Lo que lo hizo innecesario es una medición: en los casos que fallan el compilador YA conoce el tipo esperado. El reparto entre capas es lo que cada una puede ver: semantic mira parámetros (nyx check los ve); codegen es el backstop y lee el tipo DESPUÉS del refinamiento, así que agarra el Fn que viene de un campo de struct —que reventaba y ni W004 cubría— y el camino NYX_SKIP_SEMANTIC=1. Migración: nyx vet avisa con W004 desde 0.32.4, así que la forma peligrosa se puede encontrar antes de que el compilador la rechace. El arreglo es declarar la firma: f: Fn(T) -> R.

El CHANGELOG.md completoTodas las versiones desde el principio, en el repositorio.GitHub →