0.38.0 2026-10-26
MINOR: new API for processing tabular data: the columnar reader
try_csv_readinstd/csvandstring_to_X_range. In addition, an integer that does not fit in 64 bits is now invalid instead of saturating:string_to_inton such a number now aborts (before, it silently gave INT64_MAX).Migration: if your code checks that the text is only digits and then calls
string_to_int, a number of 19+ digits that arrives from outside now aborts the process (before, it saturated). The way out isstring_to_int_or(s, def)or rejecting the overflow beforehand. Today this affects nyx-kv (INCRBY,LRANGE,SCAN,SET … EX,INCR), nyx-db (LIMIT/OFFSET), nyx-proxy (max-age) and the kv webhook: do not recompile them with 0.38.0 until they have their fix (filed inTASKS.md).
Added
-
string_to_int_range(s, ini, fin, def)/string_to_float_range(s, ini, fin, def): parse the number in bytes[ini, fin)ofswithout creating the substring; they always yield the same value asstring_to_X_or(s.substring(ini, fin), def). Reason: in a 5M-row CSV,split()+string_to_floatper row makes ~11 GC allocations per row and takes half the time (bench 2026-10-25 against Python/pandas/polars). Pre-sizing thesplit()Array was tried earlier and discarded: −4% on the giant split, +4..9% on the short ones. +1 test (507), recipeexamples/by-example/145-csv-sin-split.nx. -
std/csv: columnar readertry_csv_read(texto, esquema, opts): parses in a single pass straight into typed columns (int,float,str, andcat= codes + dictionary), with the core in the newruntime/csv.c(RFC 4180: quotes, line breaks inside quotes, CRLF, BOM). Strict: an invalid value is anErrwith row and column.try_csv_guess_schemaproposes the schema. In the 5M-row bench, grouping and filtering takes 0.93 s and 265 MB, against 1.21 s for pandas and 1.19 s for polars with one thread (withsplit()+Map: 5.5 s). +1 test (509), +1 C suite (test_csv.c), recipe146-csv-columnar.
Changed
-
An integer that does not fit in 64 bits is invalid, not saturated:
string_to_intaborts with its own message,string_to_int_or/_rangereturn the default, and anintcolumn oftry_csv_readgivesErr(“does not fit in an int” («no entra en un int»)). Before,strtollleft it at INT64_MAX/MIN and nobody checkedERANGE:string_to_int_or("99999999999999999999", 0)silently gave 9223372036854775807 (Ottavio's decision). Consequences that were fixed along with the change:std/jsonstores such an integer as"float"with its text (before, it saturated it; with the change it would have aborted the process on a number coming from outside), and the interpreter/REPL applies the same NYX2015 rule as the compiler:-9223372036854775808gives exact INT_MIN (it gave -9223372036854775807) and a literal that does not fit is an error (it gave INT_MAX). +2 tests (510,511), +1 C suite (test_int_parse.c), +2 checks in the REPL smoke test. -
string_to_float/_or/_rangeparse with the Clinger fast path: a number[+-]digits[.digits]of up to 15 digits is converted with one exact division, which gives the same double asstrtodbit for bit; the rest (exponents, 16+ digits,inf, hex, whitespace) still goes tostrtod. Verified againststrtodon 5M random values and on the edge cases (test_float_parse.c). In the bench of 5M rows,indexOf+_rangedrops from 2.67 s to 2.13 s. +1 test (508), +1 C suite.
0.37.1 2026-10-24
PATCH: the three nyxerp friction reports of 2026-10-22.
Fixed
-
NYX1046: an enum pattern (
Result.Ok(v),Option.None) in amatchwhose value is primitive (int,float,bool,String) is an error. Before, it passednyx checkand clang failed with invalid IR or, with a String, it compiled and never entered any arm (nyxerp friction withmatch string_to_int(s), which returnsint). +2 error tests.
Added
-
sse_close_room(room) -> intinstd/serve: closes only the channels of one room; the rest keep going and the room can be reopened.sse_drain_closedid not work for this: it leaves the registry draining and no later open gets registered (nyxerp friction). +5 checks in the serve smoke test. -
std/postgrestakes the password like libpq when the string has nopassword=:PGPASSWORD, thenpassfile=,PGPASSFILEor~/.pgpass(wildcards*, escapes\:and\\; a file readable by others is ignored).pg_pgpass_buscaris exposed (nyxerp friction). +1 test (506).
0.37.0 2026-10-22
MINOR: two warnings from 0.36.x become errors, as announced (rule 7 of
docs/VERSIONING.md): NYX1010 between integer and String, and NYX1045 on an unknown escape. Everything else is additive:std/pdf_render, chunked HTTP responses, wildcard SNI,http_serve_en, DNS records,nyx build -j Nand several improvements tostd/pdf,std/xlsx,std/imageandstd/smtp. Before the cut, 1201 files were reviewed (nyxerp, the products, the sites, and std, examples and compiler of this repo) with the 0.36.x compiler: not a single case of the two warnings.
Changed
-
NYX1010 between integer and String is an error (a warning in 0.36.x).
5 + "a",5 == "5"and"a" < 5compiled and died with SIGSEGV when the line ran; now they do not compile, with the same hint (int_to_string,string_to_int_or). The threetest-nyx1010-*-mixtocases move to the error list ofrun_error_tests.sh. It is still a warning when one side reads a slot (dst[1] == "dos"): the inferred element type of an Array can lie (var dst = [9]and thendst.push(src[1])with a String) and the slot carries its tag, so that program works (test-323, case 6; warning intest-nyx1010-casilla-aviso). -
NYX1045 on an unknown escape is an error (a warning in 0.36.x).
"\q"or"\e"were worth the letter without the backslash; now the analysis rejects them with the line and the list of valid escapes (\x1binstead of\e). It comes out with the format and NDJSON of the other analysis errors ("severity":"error").
Added
-
std/http:http_serve_en(host, port, handler)andhttp_serve_mt_en(host, port, workers, handler)choose the bind address;http_serve/http_serve_mtstay on0.0.0.0for compatibility (gotchahttp-serve-all-interfaces). The playground moves to127.0.0.1:8081: in production it was left open directly even though the gateway serves it (server order, 2026-10-21). Testtest-505-http-serve-en(binds127.0.0.2and checks that127.0.0.1does not connect). -
Wildcard SNI in the TLS server:
tls_server_add_cert("*.dominio", cert, key)covers one label (a.dominio, notdominionora.b.dominio, RFC 6125), exact names win over the wildcard regardless of order, the comparison is no longer case-sensitive and the cap goes up from 16 to 64 domains. Request from nyxerp (phase C0 of its automatic onboarding of customers<cliente>.nyxerp.com). Testtest_sni_name_matches(runtime) and real handshake verified withopenssl s_client. -
std/http: reading a response in chunks (nyxerp friction 20261019-200016-team-1: forwarding to the browser each chunk of a language-model API as soon as it arrives).try_http_stream_opts(method, url, headers, body, http_stream_opts())→HttpStream, which returns with the status and the headers:-
read()gives body chunks without the chunked framing;read_event()givestext/event-streamevents already assembled (SseEvent { evento, datos, id }, WHATWG). -
Deadlines
connect_ms,primer_byte_ms,lectura_ms(between two chunks) andtotal_ms(no cap by default). HTTP and HTTPS. -
sse_wait_room(room, ms)instd/servecloses the forwarding race: the channel enters the room after the handler returns, and without waiting the first events were lost. -
Test
test-504-http-stream(split chunked, Content-Length, until close, split SSE, deadlines, gzip, disconnects, TLS),tests/ai-first/42, recipe144-reenviar-eventos.
-
-
std/pdf_render: drawing a PDF page as an image (nyxerp friction 20261019-080021-team-1: tracing, in the format editor, the preprinted sheet that the print shop sends as a PDF). New module in pure Nyx: the same code on native and onwasm32-wasi, with the image byte for byte identical on both.-
pdf_open(bytes, pdf_open_limits()) -> Result<PdfDoc, Error>,doc.page_count(),doc.page_size(p),doc.render(p, ppp, pdf_render_strict())anddoc.render_report(...)withpdf_render_best_effort(), which returns the image and the list of what was omitted. Shortcutpdf_page_to_image(bytes, página, ppp). The output is theImageofstd/image. - Parser with classic xref, xref streams, object streams, incremental updates and reconstruction by sweeping; Flate filters (with predictors), LZW, ASCIIHex, ASCII85 and RunLength.
- Antialiased rasterizer (exact horizontal coverage × 4 subsamples), nonzero and even-odd, strokes with joins, caps, dashing and stroke adjustment, and clipping.
- Color gray, RGB, CMYK (poppler's interpolation), ICCBased, Indexed, Lab, Separation and DeviceN with functions of type 0, 2, 3 and 4; constant alpha.
- JPEG and raw images, with SMask, Mask and stencil. Large ones are downscaled in a single pass to the size they occupy on the page, without materializing them whole.
- Text with embedded TrueType and CFF (simple and Type0/CID). The 14 standard fonts, when not embedded, use a subset of Liberation (OFL 1.1, 227 KB, license included in the generated block). Cache of rasterized glyphs.
-
Strict by default: anything unsupported (shadings, Type 1/3, JBIG2/JPX/CCITT, encryption...) is
Err(unsupported)with the name.PdfOpenLimitsbounds size, decompression, nesting, forms, operators, ppp and pixels: a hostile PDF givesErr(invalid)naming the limit. -
Verified against poppler 25.03: synthetic corpus of 12 PDFs (
tests/pdf-corpus/,scripts/testing/pdf_render_oracle.sh) and 44 real local PDFs (93 pages, none fails). -
Tests
test-503-pdf-render,test-wasm-59-pdf-renderandtests/ai-first/41-pdf-pagina-a-imagen; recipe143-pdf-a-imagen.
-
-
std/dns: TXT, CNAME and MX queries (request from nyxerp, 2026-10-18, to check SPF, DKIM and DMARC before turning on email). New module with its own DNS client in Nyx (RFC 1035) over UDP, with a TCP retry if the response comes truncated and no link-time dependencies.-
dns_txt(one String per record),dns_cname,dns_mx(sorted by preference), rawdns_query,dns_set_server,dns_set_timeout,dns_is_nxdomainanddns_is_nodata. -
Result<_, Error>errors: NXDOMAINnot_found3, NODATAnot_found61,timeout110, SERVFAIL/REFUSEDio,parse. -
Hostile-input parser: cap of 16 compression jumps, names of 255 bytes, random ID and question verified. Servers from
/etc/resolv.conf(up to 3 IPv4); on Windows it requiresdns_set_server. No cache, IPv6, EDNS(0) or DNSSEC. - Verified against the real network (SPF and DMARC of gmail.com, 17 TXT of google.com over TCP).
-
Tests
test-499-dns(fixed packets),test-500-dns-servidor(fake UDP/TCP server) andtest-501-dns-red-real(SKIP without network); recipe142-dns-spf-dkim-dmarc.
-
-
std/smtp: email with text and HTML (multipart/alternative) (request from nyxerp, 2026-10-18).smtp_message_alternative(de, para, asunto, texto, html): text first and HTML after (RFC 2046), each part in quoted-printable (RFC 2045 §6.7). Withsmtp_attachit comes out asmultipart/mixed›alternative+ attachments, with distinct boundaries and dot-stuffing per part.smtp_messageandsmtp_message_htmldo not change a byte. Testtest-stdlib-12-smtp-alternative(includes a full round trip throughmime_parse) and recipe141-smtp-texto-y-html. -
std/image: thumbnails of scanned sheets in tenths of a second (nyxerp friction, 2026-10-18), with byte-for-byte identical output (still equal to Pillow).- Resize, JPEG decoder and encoder read and write through pointers on 64-bit native; wasm32 keeps the previous versions, and the two paths give the same bytes.
-
Sheet of 1275×1650, CPU time of
image_thumbnail: 3.6 s → 1.0 s withnyx build(clang-O0) and 0.92 s → 0.25 s with--release(-O2). At 2550×3300: 8.6 s → 2.4 s and 2.3 s → 0.65 s. -
A good part of the 3.5 s reported was compiling without
--release. -
Test
test-497-image-rapido(CRC32 fingerprints pinned before optimizing), recipe140-imagen-hojas-escaneadasandscripts/bench_image_thumb.nx.
-
std/xlsx: dropdown lists and fill color (request from nyxerp, 2026-10-17, for bulk-upload templates).-
XlsxSheetOut.validate_list(rango, valores)andvalidate_list_range(rango, origen)emit<dataValidations>in the order required by the ECMA-376 schema.xlsx_writereturnsErrif the fixed list exceeds 255 characters, if a value contains a comma or quotes, or if the range is invalid. -
xlsx_fill(celda, "RRGGBB")gives a solid fill and chains withxlsx_bold. -
xlsx_readdoes not change. -
Test
test-495-xlsx-validacionand recipe138-xlsx-plantilla.
-
-
std/pdfphase 2: line width, gray and color, 12 Core14 fonts and rotated text (request from nyxerp, 2026-10-17, for preprinted sheets and labels).-
line_width,grayandcolorchange both the stroke and the fill of whatever is drawn afterwards. Each page starts in the PDF default state: black and 1 pt. -
New fonts:
HelveticaOblique,HelveticaBoldOblique,TimesBold,TimesItalic,TimesBoldItalicand the fourCourier. The three new Times AFM tables were cross-checked between pypdf and pdfminer.six; the only difference is the €, which pdfminer does not include. -
text_rotatedrotates the text with theTmmatrix, at any angle, with sine and cosine computed without libm; multiples of 90° come out exact. - No phase 1 PDF changes bytes.
-
Test
test-496-pdf-fase-2and recipe139-pdf-preimpreso.
-
-
nyx build -j N:[lib]units are compiled in parallel (nyxerp friction, 2026-10-17). Before, they ran serially: 302 modules and about 50 minutes cold on 4 cores. The units do not depend on each other to compile, so now up to N are launched at the same time (-j N,--jobs NorNYX_JOBS; by default, the cores).nyx testgoes through the same path. Output and linking in manifest order; on an error all the units that failed are reported and no half-written object is left behind. Two builds of the same project at the same time (severalnyx testin parallel, as nyxerp runs them) take a lock ontarget/nyx-lib: the second one waits and reuses; before, they deleted each other's temporaries. A build with nothing to compile launches no helper processes (the onenyx testtriggers on each file). Measured on a copy of nyxerp: 300 libraries cold, 2,894 s serial and 1,057 s with-j 4. Measurement indocs/design/spikes/2026-10-17-build-lib-paralelo-medicion.md: under 600 MB per unit. Case 10 ofrun_lib_modules.sh.
Fixed
-
A header written as
"Nombre: valor"in a single element reaches the request (silently-wrong, nyxerp friction). Thestd/httpclient only read[nombre, valor]pairs and dropped it without warning; now it accepts both forms, also mixed (a header name cannot contain:, so there is no ambiguity with values likeBearer a:b). Testtests/ai-first/43. -
CAPABILITIES.mdlists public types and their methods: until now only free functions, so APIs made of methods (doc.render(),s.read(),d.text()) and their structs did not appear (nyxerp friction, 2026-10-20). -
NYX1017 suggests the call when the name is a method:
s.statuson anHttpStreamnow says “'status' is a method: call it with parentheses, .status()” (nyxerp friction, 2026-10-20). Testtest-nyx1017-metodo-sin-parentesis. -
String literals compile in linear time. The lexer and
escape_stringconcatenated one byte at a time: a 64 KB literal took ~4 s in the frontend and a 400 KB one, 32 s. Now 64 KB takes 35 ms.test-lexerandtest-emit-bytes-globalpin it. -
A float from a slot without
<T>keeps its value when assigned and when pushed .x = d[i]withx: floatalready declared, andb.push(a[2 * i]), silently gave the bits of the double as a number (1.5 → 4.6e18). Testtest-502. -
wasm32: a slot passed directly to an
implmethod. With anArrayorStringparameter, the call emitted the raw i64: on native it worked by chance, on wasm32 it was anunreachabletrap. Now it is converted, just like in free functions. Testtest-wasm-58. -
Single-line enum variants no longer depend on the import path (nyxerp friction, 2026-10-18).
std/pdfdeclarespub enum PdfFont { Helvetica, … }on one line; when it arrived first through a transitive import, the line-by-line scan insemanticlooked for the variants on the following lines and registered garbage. Since the first registration wins,PdfFont.Helveticagave NYX2012 “has no variant” (the NYX2012 check in expressions, new in this version, made it visible; before, it failed in codegen with “unknown enum variant”). With the imports reversed it compiled. Regression:test-498. -
std/smtpverifies against the system CAs (nyxerp friction, 2026-10-18).smtp_starttls(587) andsmtp_connect_tls(465) verified against thetls_set_ca_filestore, which starts empty: every public relay with a valid certificate gave “unknown ca”. The workaround,tls_set_ca_file(""), added all the public CAs to a global store that theverify-caofstd/postgresalso uses. Now they usetls_upgrade_fd_verified_system/tls_connect_verified_system, asstd/imapalready did. Verified againstsmtp.gmail.comon 587 and 465: before, both failed; now they verify. The self-signed one is still rejected (test-stdlib-07, case d). The workaround is no longer needed; a per-connection private CA is filed. -
A slot of an
Arraywithout<T>passed directly to a struct parameter, or used as aMapkey, now works (nyxerp friction, 2026-10-17).texto(hs[0]), withfn texto(h: Hoja), passed the rawi64of the slot where the function expected the struct by value. The result was “GC heap exhausted” or, in another nyxerp program, the function received a different element.m.insert(pares[0], pares[1])used thei64as the key pointer and clang rejected the IR. In both casesnyx checkwas green. Now the slot is converted to the parameter's struct (inttoptr+load, as inlet h: Hoja = hs[0]), and the key of a Map method that comes from a slot is read as a String. The compiler itself had the same pattern incg_tipo_duplicado(decls[i], …), which worked by chance thanks to the aarch64 calling convention.ASTNodeis left out: in a slot it is an Array, not a pointer. Regression:tests/ai-first/40-casilla-sin-tipo-como-argumento.nxand gotchauntyped-slot-as-argument. -
nyx checkrejects an enum variant that does not exist (NYX2012) (nyxerp friction, 2026-10-17).PdfFont.Zzzzpassed the check in green and only codegen rejected it, with a message that asked for a type annotation. Now it is NYX2012, with “did you mean” if there is a close variant, for both local and imported enums. It is not validated when the name corresponds to a variable or a parameter. Casestest-nyx2012-variante-{inexistente,importada}. -
String escapes:
\xNNand\u{…}are worth what they say, and an unknown escape warns (NYX1045) . Before, the lexer knew only\n \t \\ \" \r \0and any other escape silently lost the backslash:"\x41"was worthx41,"\u{0}"was worthu{0}and'\x41'printed41. There were three real cases, all wrong without anyone noticing: the"\u{0}"sentinel in the nyxerp configuration, the"\x1F"separator of theDISTINCTkey in nyx-db (and its copy in venezuelainfo), and the ANSI sequences"\x1b[…"in the nyx-edit example. With this fix the three are worth what they meant without touching their code.\'and\$("\${x}"is a literal${x}) are now documented. A malformed\xor\u{…}is a lexer error with line and column. Any other escape iswarning [NYX1045]and becomes an error in 0.37.0. The four lexer paths that read escapes, which were four copies of the sameif, now go through a single function.test-494-escapes-hex-unicode, seventest-nyx1045-*cases and gotchastring-escapes. -
CAPABILITIES.mdno longer hides 34 stdlib modules under “Otros”. The curatedmodule_categorytable incompiler/build.nxlacked 34 of the 78 modules, among themserve,postgres,error,math,map,router,template,xlsx,pdfandimage. All of them now have a category, and two new categories are added: “Núcleo” (error,prelude) and “IA & LLM”. “Otros” remains only for builtins without a topic. New guard (d) inrun_capabilities_test.sh: it fails if astd/module lands in “Otros”, so a new module enters the table in the same commit that creates it. -
The compiler no longer copies quadratically: a large nyxerp test file compiles 9 times faster (nyxerp friction, 2026-10-17: ~42 s per
nyx testfile). Per-phase measurement showed that time grew faster than the IR, and a sampling profiler showed that 81% was GC, caused by concatenations that recopied everything accumulated: the text constants ingenerate_llvm, the module declarations inresolve_source, and each module's symbol prefix recomputed character by character on every unqualified call. Now they useStringBuilder, the prefixes are computed once (ctx.alcances[3]), the lookup of a function among the modules is memoized (ctx.alcances[4], with the set's version in the key),extract_exportsdoes not trim lines that cannot export anything andscan_module_typesdoes not re-read a module already scanned. The[lib]fingerprint innyx_buildhad the same pattern (todo = todo + …over the import closure): an unchangednyx_build libs, whichnyx testruns on every file, dropped from 5.7 s to 2.0 s in nyxerp. The IR comes out byte for byte identical. Measured: 105.6 s → 11.2 s and 822 → 642 MB on the largest file; 3.5 s → 1.6 s on a medium one. Spike:docs/design/spikes/2026-10-17-test-sin-repetir-medicion.md. -
Operators between primitives of different classes: NYX1010 instead of SIGSEGV or invalid IR. The check let arithmetic and comparison through between any pair of types. The matrix measured on 2026-10-17 (four types per side, nine operators) showed that only numbers with numbers,
String + x,==/!=between two Strings or two bools and ordering between two Strings exist; the rest died with SIGSEGV when the line ran (5 + "a",5 == "5","a" < 5) or gave invalid IR that only clang rejected (x * "dos",2.5 + "a",true + 1). The latter never produced a binary and is an error now; the former compiled, so by rule 7 ofdocs/VERSIONING.mdit iswarning [NYX1010]in 0.36.x and an error in 0.37.0. Nyxerp (539 files) has no cases. Tests:test-493-operadores-primitivos-mixtos(the values of what does exist) and five ofmake test-errors; gotchamixed-primitive-operators. -
Reinstalling the same compiler no longer invalidates
target/nyx-lib(nyxerp friction, 2026-10-17). The fingerprint of each[lib]object carried the path and mtime of the compiler binary, and the absolute path of eachstd/in the closure. Since everymake install-local/nyx updatebuilds a new folder in~/.nyx/versions/, reinstalling the same compiler recompiled everything: 302 modules and about 50 minutes in nyxerp. Now the compiler is identified by the sha256 of its content, stored intarget/nyx-lib/.compiladoralong with the path, size and mtime that produced it, and thestd/by their relative path. Case 9 ofrun_lib_modules.sh: an identical reinstall reuses, a different compiler recompiles.
0.36.1 2026-10-17
PATCH: a codegen fix (numeric conversion on an untyped
Arrayslot) and two additive capabilities requested by nyxerp for OAuth2 mail:smtp_auth_xoauth2instd/smtpand the new modulestd/oauth2. Nothing that used to compile stops compiling.
Added
-
std/oauth2: stateless OAuth2 client (nyxerp friction 2026-10-11, GO 2026-10-16): PKCE S256 andstate(CSPRNG), authorization URL (+ provider extras: Google'saccess_type=offline), code exchange, refresh that preserves therefresh_token,client_credentialsand device code (RFC 8628:oauth2_device_code/_poll/_wait, withverification_uriand Google's_url). Errorsauth/protocol/pending/invalid, never carrying the token, the secret, or the verifier. No new runtime:std/http+std/json+std/url+std/base64+std/webpushcrypto.test-stdlib-11-oauth2(RFC 7636 B vector and a canned provider shaped like Google and Microsoft), recipe137-oauth2, SPEC and LLM.md with the Gmail and Microsoft 365 recipes. CAPABILITIES:oauth2under "Crypto & security" ("Cripto & seguridad"), and in passingimapandmimemove from "Other" ("Otros") to "Network" ("Red"). -
smtp_auth_xoauth2(c, usuario, token)instd/smtp(nyxerp friction, 2026-10-11): the counterpart ofimap_auth_xoauth2for SENDING through Gmail and Microsoft 365, which no longer accept username and password. It requires the EHLO to have advertisedXOAUTH2and handles the mechanism's own rejection (334with a base64 JSON -> empty line ->535); the decoded JSON goes into theError, the token never does. Case (e) oftest-stdlib-07-smtp-tls(good token, expired token, server without the mechanism). Obtaining and refreshing the token remains the application's job:std/oauth2was seeded as the arcoauth2-cliente(DRAFT, GO pending) indocs/design/specs/2026-10-16-oauth2-cliente-design.md.
Fixed
-
int_to_float(arr[i])/float_to_int(arr[i])on anArraywithout<T>bound to a typedletgenerated invalid IR thatnyx checkdid not see (nyxerp friction, 2026-10-16). Indexing inherited theexpected_typeof thelet: withlet f: float = int_to_float(cuentas[q])the slot was read asdoubleand thesitofp i64received adouble; withlet n: int = float_to_int(pesos[0]), the reverse (fptosi doubleon a rawi64). Only clang failed, late, with "invalid IR". Now both builtins set the hint of the type they require (int/float) while their argument is code-generated, just likestring_to_intwithString. Regression:tests/ai-first/39-conversion-de-casilla-sin-tipo.nx(+1 inmake test-ai-first).
0.36.0 2026-10-10
MINOR because it rejects code that used to compile (decision #3): NYX2019 becomes an error on the
letto an integer (it was a warning in 0.35.x, rule 7 ofdocs/VERSIONING.md); NYX1031 for the file-handle family given a path (previously, SIGSEGV at run time); NYX1042 (return/break/continueleaving adefer; previously it hung or did nothing), NYX1043 (deferin anasync fn) and NYX1044 (ambiguous capture by name in a block closure). None of them rejects a program that gave the correct result. The bulk is block semantics: real scopes in codegen,deferand#[affine]drops when their block closes (also onthrow), closures inside blocks (decision #22). In addition, the nyxerp requests:std/zip/xml/xlsx,std/image,std/mime,std/imap,std/expr,std/rsa, gzip and limits instd/serve, mTLS and multipart in the HTTP client.
Added
-
std/xlsx: frozen panes, autofilter and thousands-separator format (nyxerp request, 2026-10-02):h.freeze(filas, columnas)(<pane state="frozen">),h.autofilter("A1:F120")(with the hidden name_xlnm._FilterDatabasethat Excel keeps) andxlsx_number_fmt(v, código)with any Excel code ("#,##0.00"uses built-in 4; the regional settings of whoever opens it decide whether it shows 1.234.567,89). Test 462 extended and verified with openpyxl; recipe 124. -
nyxerp's second batch (2026-10-01): JPEG output, thumbnails and mail reading over IMAP, reviewed before integrating and integrated with a full test battery:
-
std/imagewrites JPEG and builds thumbnails:image_encode_jpeg(img, quality)(baseline, libjpeg-style quality 1-100 over Annex K, standard Huffman, JFIF, 4:2:0 up to q90 and 4:4:4 above, single-component grayscale, alpha over white, no EXIF, ICC preserved; integer FDCT: the same bytes in native and wasm). Same PSNR as Pillow at equal quality up to q90.image_thumbnail(bytes, max_w, max_h, quality)andimage_thumbnails(bytes, sizes, quality): decodes with downscaling, shrinks with Lanczos3 and encodes in one step, a single decode for several sizes (12 MP to 600 px in ~1.4 s).image_default_quality()(80) andimage_strip_icc. Tests 481-482, wasm 57, recipe 133. -
std/imap: read a mailbox: implicit TLS (993) and STARTTLS (143), both always verifying against the system CAs; LOGIN, PLAIN and XOAUTH2 (app token), never in cleartext;imap_select/imap_examine,imap_fetch_since(UID cursor, chunked search,staleon a changed UIDVALIDITY, optional global deadline),imap_fetch_message,imap_search_unseen,imap_mark_seen; everything with BODY.PEEK. The server is treated as hostile input: limits on line, literal (before reading it), total per command and depth, plus deadlines. A large message, or one that astd/mimelimit would reject, comes out with its headers andtruncated. Tests 483-485, sub-suitetest_imap_client.py(107 checks against a fake server with PKI and hostile cases), recipe 134. Not yet verified against real Gmail or Dovecot. -
std/mime:mime_parse_headparses only the headers: a body that exceeds a limit no longer loses theIn-Reply-To(resolves the filed item "an exceeded limit takes down the whole message" for imap and for whoever calls it;mime_parsestill returns Err). -
The review found and fixed, each with its case: FETCH split across several responses gave an empty message and advanced the cursor (the mail was lost); no accumulated per-command limit (memory); capacities with quadratic cost; the first run on a mailbox of >130 thousand messages did not start. In
std/image, the re-prefixing ofimage_thumbnailerrors.
-
-
Seven nyxerp requests from 2026-09-29, done by subagents in their own worktrees and integrated with a full test battery:
-
gzip in
std/serve, on by default: text responses between 1 KiB and 1 MiB, when the client sendsAccept-Encoding: gzip(with q-values);Vary,Content-Lengthin bytes, weak ETag, HEAD same as GET, sha256 cache. It changes what gzip-accepting clients see:app_compress(app, false)turns it off.std/zipgainszip_gzip/zip_gunzip. Test 463, recipe 126. -
Limits in
std/serve: per-route body limit (app_max_body, 413) and a per-origin limiter shared by all workers (app_rate_limit,app_route_rate_limit, 429 withRetry-After;app_rate_limit_keyandrate_key_forwarded_forbehind a proxy). Test 464, recipe 127. -
std/rsa: verify RSA-SHA256 signatures (v1.5, base64 and PSS) with a PEM public key, for signed notices from payment gateways.EVP_DigestVerifyinruntime/crypto.c; in wasmErr(invalid 95). Test 465 with vectors from the openssl CLI, recipe 128. -
Mutual TLS in the HTTPS client:
HttpOpts.client_cert_pem/client_key_pem, only for that request and validated before opening the socket (same path ascas_extra). Sub-suitetest_http_mtls.pywith a server that requires a certificate, recipe 129. -
multipart in the HTTP client:
multipart_field,multipart_file,multipart_buildandtry_http_post_multipart; binary-safe, verified against Python'semailparser. Test 467, recipe 130. -
std/expr: safe evaluator of formulas with variables, in fixed-point decimals (6), not float:0.1 + 0.2 == 0.3,round(2.675, 2) = 2.68. 1500 random expressions equal to Python'sdecimal; same result in wasm. Test 468, recipe 131. -
browser fetch and SSE to the local network, with a deadline: Chrome's
targetAddressSpaceand AbortController; the old API does not change (the merge had changed it by accident and it was restored:c3c6a87e). Wasm test 53, recipe 132. The permission in a real Chrome could not be verified. Specs in DRAFT for the large ones (images, IMAP, local network, Android) and, in the nyx-proxy repo, the one for domains without restart and ACME.
-
gzip in
-
std/zip,std/xmlandstd/xlsx: read and write.xlsxspreadsheets in pure Nyx (nyxerp friction, 2026-09-29). An.xlsxis a zip with XML inside, so there are three modules and the two lower ones are useful on their own.std/zip:.zipcontainer (zip_readvia the central directory,zip_writer) with limits against zip bombs (ZipReadOpts).std/xml: event-based reader and small tree;<!DOCTYPEis an error, so there is no XXE or expansion bombs.std/xlsx:xlsx_readyields typed cells (text, number, date, bool, error, empty), with dates in ISO and the 1900 and 1904 epochs; formulas are not evaluated (the cached value is read) and gaps areemptycells;xlsx_writebuilds the workbook (text, numbers with fixed decimals, dates, bold, widths, merged cells). Noextern "C": identical in native andwasm32-wasi. The DEFLATE codec (inflate, deflate, CRC-32, Adler-32) was moved fromstd/pdftostd/zipwithzip_*names (so as not to clash withstd/compress): a single implementation, and PDFs come out byte-for-byte identical. Verified against other readers: 818 cells of a real Excel workbook equal to openpyxl's; whatxlsx_writewrites opens in openpyxl; the.zipfiles fromzip_writerare accepted byzipfileandunzip(run_zip_verify.sh, inmake test-stdlib;run_xlsx_verify.shseparately, it needs openpyxl).test-458..462and recipes 123 (zip), 124 (write a workbook) and 125 (read a sheet). -
First batch of nyxerp's large requests from 2026-09-29 (GO from Ottavio on 2026-09-30), done by subagents in their own worktrees, reviewed and integrated with a full test battery:
-
std/image: decode, shrink and crop photos in pure Nyx, identical in native and wasm .image_info(format by signature, dimensions, orientation without decoding);image_decodeof full PNG and of 8-bit baseline JPEG (gray or color, all subsamplings, restarts), with EXIF orientation already applied, the ICC profile exposed inImage.iccwithout applying it, and 1/2, 1/4 and 1/8 downscaling during decoding (image_decode_scaled,image_decode_min);image_resize,image_resize_fit,image_resize_cover,image_cropandimage_flatten(premultiplied alpha). Limits (ImageLimits: bytes, pixels, side) validated BEFORE reserving memory. Progressive, CMYK and 12-bit giveErr(unsupported)with the output; WebP, GIF, HEIC, AVIF, BMP and TIFF are recognized by signature. Verified against Pillow (photos from several encoders, all 8 orientations, resizing). The PNG reader moved out ofstd/pdf, which now imports it. The JPEG encoder and the one-step thumbnails are still missing (Tasks 5-6). Tests 475-480, wasm 55-56; prototype spike measured. -
std/mime: read incoming mail: headers with folding, RFC 2047 and 2231, base64 and quoted-printable, charsets, nested multipart with a depth limit, binary attachments, bounces, HTML to text, and thread ids (Message-ID, In-Reply-To, References). Foundation ofstd/imap(Tasks 4-6, next batch). Tests 472-474, wasm 54. -
std/smtp: Message-ID and thread headers: the id is fixed when the message is created (previously it changed on everysmtp_renderand there was no way to know the one that went out);smtp_message_id,smtp_set_message_id,smtp_headerandsmtp_reply_to(buildsIn-Reply-To+References; theReply-Toheader is set withsmtp_header).smtp_sendrejects withErr(header)CR/LF/NUL, invalid names and the headers that the library writes itself.test-stdlib-10. nyxerp request from 2026-10-01 (help desk). -
tls_upgrade_fd_verified_systeminstd/tls: STARTTLS verified against the system store, for IMAP (143) and SMTP (587).std/imapTLS probe inmake test-integration. -
Android: arm64 build probe against the NDK sysroot (spike) and the
os_*layer for bionic: own context switch in aarch64 assembly and cooperativeos_thread_timedjoin/cancel.
-
Fixed
-
The drop of an
#[affine]value withDropis per declaration and runs when its block closes . Silently-wrong: the drop went by name and by function, so in a loop each iteration overwrote the same slot and only the last value was released (the rest was lost without a warning); athrowreleased nothing; and two homonymous affines were NYX2020. Now the drop is an implicitdeferof its declaration (own slot, pending scope entry with itstryframe): once per declaration, also per iteration and onthrow. The flag of a returned value is turned off before the exits. The function's cleanup section,%__defer_retvaland NYX2020 are gone. Testtests/ai-first/38; gotchadrop-per-declaration. -
A
deferruns only if it was reached, and athrowruns the reacheddefers . Silently-wrong:if b { return 1 } defer {...}ran thedeferwithbtrue, because the cleanup section generated all the top-level ones on every exit. And athrowskipped all the cleanup. Now the function body is one more scope (thedefers are generated inline at the exits that reached them) and eachdefer, when executed, pushes an implicittryframe whose catch runs it and rethrows: the reached ones run, innermost to outermost, before thecatch; athrowinside adeferdoes not repeat it. Tests 492 andtests/ai-first/37; the gotchadefer-not-on-throwbecomesfixed. -
A closure inside a block can capture the block's variables; a named
fnin a block links. Closures only existed at the top level: a lambda in anifdid not see theif's variables (NYX1002) and a namedfnin a block did not link (clang'suse of undefined value, with no line). The parser leaves the lambda in its block, codegen collects the nested functions of any block and block variables with a unique name enter the SharedEnv. What cannot be captured by name (a name repeated in the function, aforvariable, amatchbinding) is NYX1044, never a wrong value. Tests: ai-first 36 and two error tests; gotchaclosures-capture-block-vars. -
forover an array literal, overArray<T>and overMap: three wrong results in the same codegen function.for q in ["x", "y"]typedqasint(print(q)showed the String's address, with no error); anArray<T>element gave SIGSEGV when used, and aMapone returnedm.size() == 0silently (both fell into the struct branch, with an extra load). The literal takes the common type of all its elements (a mixed one stays untyped);Array<T>andMap/Map<K, V>have their own branch. Testtests/ai-first/35; gotchafor-in-element-type. -
deferis no longer silently discarded outside the function's top level;returnin adeferno longer hangs. Adeferinside a block (if,else,while,for,matcharm,try,catch,unsafe), in a nested fn or in a lambda compiled, passednyx checkand never ran. Areturninside adeferhung the program, abreakdid nothing, adeferinside another was ignored, and a fn withdeferand a nested fn withreturndid not compile (clang error with no line). Now thedefers run, andreturn, orbreak/continuethat leaves the body of adefer, is NYX1042; adeferin anasync fnis NYX1043. Tests:tests/ai-first/34-defer-al-cerrar-bloque.nx,test-488totest-491,test-nyx1042-*,test-nyx1043-*. -
file_write_string("/ruta", "hola")no longer compiles only to crash later: it is NYX1031 (nyxerp friction, 2026-10-09). The file-handle functions (file_write_string,file_write_bytes,file_read_line,file_read_bytes,file_seek,file_tell,file_flush,file_close) take the handle fromfile_open; given aStringpath they passed the checker and the binary died with SIGSEGV with no message, also losing the output in the buffer. Now the checker rejects it with a hint (file_openorwrite_file); a handle stored in anintstill works.LLM.md§4 andCAPABILITIES.mdstate the type of each argument. New error test. -
A name redeclared inside a block no longer shadows the outer variable (nyxerp friction, 2026-10-04): codegen kept a function's variables in a flat map, so a
let qinside anif/else/while/for/matcharm/try/catch/unsafe, the variable of afor q in ...or the binding ofOption.Some(q) =>REPLACED the outer one until the end of the function. It compiled,nyx checkwas green (the checker did restore it) and the result was garbage or a segfault with a different type, and the inner value with no symptom with the same type:let q: S = S{a:3}; if true { let q: S = S{a:99} }; return q.agave 99. Now each block is a real scope and when it closes the name goes back to being the outer variable, with its value and its type. In addition: a function-leveldefersees the function'sqeven if thereturnleaves from a block that declared another; a closure that captures a function-level variable keeps seeing it even if a block declares another with the same name (a lambda returned 801 instead of 707); the checker opens a scope in thetrybody and inunsafe(aletin thetrywas visible in thecatchand rejected valid programs with NYX1014). New diagnostics: NYX1041 (a lambda written inside a block uses a name that in that block is another variable; the arcclosures-capturan-bloqueleft it as a backstop for NYX1044) and NYX2020 (two homonymous affines withDrop; retired in this same release bydrop-por-declaracion). Tests:tests/ai-first/33-sombreado-en-bloque.nx(13 cases), 486, 487,test-nyx1041-lambda-captura-de-bloqueandtest-nyx2020-drop-sombreado; recipe 135; gotchablock-shadowing-restores-outer. -
A local
Fnvariable with the same name as a globalfnwas checked against the global's signature (nyxerp friction, 2026-10-04):let leer: Fn(int) -> int = a.leer; leer(21)gave NYX1005 "expected String, got int" if there was afn leer(s: String)in another file, and with equal signatures the check passed against the wrong function. Semantic now resolves the name like codegen: the most recent variable wins and contributes its ownFnsignature (also the return type andit.map(f)). Regression: ai-first 32 and an error test. -
std/xlsx: a number with 3+ decimals written before the first date was read as a date (silently-wrong): the date had the fixednumFmtId164 and collided with that of"0.000". Now every custom format takes 164 + its position. Found while reviewing the nyxerp request; regression in test 462. -
Editing an
include_bytesfile did not recompile its[lib] moduleslibrary. The fingerprint oftarget/nyx-libhashed the module's source and its import closure, but not the embedded files:nyx run/test/buildreused the.oand served the OLD content with no error (not eventouchon the.nxwas enough). Now every fully hashed file adds the path and the sha256 of eachinclude_bytes("…")it names, including those of non-[lib]modules that the library inlines. nyxerp friction 20260930-170003; three new cases inrun_lib_modules.sh. -
"Verified" STARTTLS verified nothing (
runtime/tls.c).nyx_tls_client_upgradewith mode 4 (strict against the system store) fell into thetls_set_ca_filecontext and no branch verified it: the handshake accepted any certificate. Now it uses the system context and verifies chain and name; a mode outside 0..4 fails closed. Found by thestd/imapTLS probe . -
Review of the batch before integrating (hostile input: uploaded photos and incoming mail):
-
std/image: an SOS of length 2 at the end of the file aborted the process (out-of-range read), and each Huffman table of a DHT, when read, built a 65536-entry table with no limit on how many the file carries (1 MB of empty tables was ~4·10⁹ writes). Now they are built when used. Cases in test 478. -
std/mime:mime_html_to_textlowercased the entire HTML on every<style>(a quadratic DoS controlled by the sender), and the trailing_of an RFC 2047 Q word was lost ("Cotización123"). Cases in test 473. -
std/smtp:smtp_render, which is public, wrote the extra headers without validating (injection of aBccfrom anIn-Reply-Tocopied from an incoming mail). Now it replaces CR/LF/NUL with spaces and omits invalid or reserved names. - The neutral-Spanish guard took the acronym "SOS" (the JPEG marker) for the River Plate "sos".
-
-
An element of an untyped
Array, assigned to a field or compared with a String.c.nombre = xs[i]stored the element's opaque integer directly in the field, andf != xs[0](the element on the right) passed it raw to the comparison: invalid IR that only clang rejected, withnyx checkgreen, whiles = xs[i]andxs[0] == fdid build. Assignment to a field now converts with the same code as assignment to a variable (also an int, float or struct field), and==/!=convert the opaque side wherever it is. nyxerp friction 20260930-040018. Regression:test-471. -
m[k]andm[k] = von aMap. Indexing aMapwas not implemented: it fell to the Array path with a String key as the index and the program did not link (defined with type 'ptr' but expected 'i64'), also as an argument (nums.push(m["a"])) and in a concatenation ("x" + m["k"]). Now codegen rewrites it toget/insert, on local and globalMaps, parameters and struct fields. Two more faults came out while testing it: the value type of aMap<String, int>was deduced from the first type argument (the key), and aMap<String, float>returned the bits of the double as an integer (1.5 → 4.6e18, no error) ingetandget_or; now they are reinterpreted withbitcast. Closes three filed items in TASKS. Regression:test-470; recipe 06. -
String values of a
Mapare stored whole. Codegen stored a String in aMapaschar*and read it withstrlen. Four faults with that root: a value with a 0 byte (gzip, raw hash, image) came back truncated with no error (the gzip cache ofstd/servereceived 3 bytes);values()on aMap<String, String>delivered thosechar*as String and the program asked for 700 GB and died; the opaque path (getas an integer) gave garbage; and in globalMapsinsertalready stored thenyx_string*whilegetread it aschar*. Now the wholenyx_string*is stored, like any other pointer, at the eight codegen sites (literal, methods, globals, struct fields,get_or). The runtime does not change; since the compiler itself uses the convention, the nine fixed-point modules were recompiled in two passes. Cached[lib]objects are invalidated on their own (the fingerprint includes the compiler). Keys remainchar*: a key with a 0 byte is truncated (known limit). Regression:test-469, one case per path. -
e.codewith Linux errno numbering on all platforms. The MSVC CRT numbers differently from 35 on (and 26):ECONNREFUSED107 instead of 111, and there are collisions that misclassified: an unreachable host on Windows came out astimeout. Newos_errno_canon(runtime/os/errno_canon.h, table by symbolic name, identity on Linux) at a single point of thenet.cboundary: the 18*_resultfunctions become wrappers around their*_nativebody, which still sees the native number.test-374passes on Windows; new runtime suitetest_os_errno_canon.c. Implemented and measured on the Windows laptop (W4 Task 9). -
Native socket read and write over IOCP on Windows. win32's
os_ev_read/os_ev_write/os_ev_cancelstop being-ENOSYS, without changing theos_evcontract: the read is a zero-byteWSARecvplus a non-blockingrecvon completion (the kernel never writes into the caller's buffer, and a cancel loses no data), and the write sends from an internal copy.loop_freecancels and drains what is pending.test_os_ev.cruns in full on Windows (110/110) and on Linux (111/111), with two new portable cases (cancel_lossless,free_pending); gate 240/240. Implemented and measured on the Windows laptop (W4 Task 7). -
Windows timers over IOCP, with a growing table. win32's
os_ev_timer/os_ev_wakestop being-ENOSYS: they wait inGetQueuedCompletionStatusExwith the timeout of the nearest deadline, wake withPostQueuedCompletionStatus(coalescing wakes: at most one in flight) and the table grows from 256 to 65536 like POSIX's.event_loop_win32.cbecomes a thin adapter. Before, with more than ~72 sleeping goroutines, the rest did not wake on time (fixed table and workers busy in the fallback):test-411goes from 0/20 to 20/20, and likewise with 1500 goroutines; gate 240/240. Implemented and measured on the Windows laptop (W4 Task 5); new testtests/runtime-unit/win/test_os_ev_timers_win32.c. The final verification of thetest-241fix onmaingave 300/300. -
Nested
awaiton win32: the scheduler no longer reuses the TLS address acrossos_ctx_swap.nyx_goroutine_join(withyieldinlined) computed the address ofg_current_workeronce and reused it after each swap; if the goroutine came back on another worker, it read the previous thread's slot and swapped to the scheduler of a worker that was running something else: two threads on the same fiber, with SEGV, a hang, or the "entry return" abort ("entry retorno").test-241failed ~7-29% on win32 depending on the machine; on Linux it did not, because the ELF backend re-evaluates the address on every use. All reads go through anoinlineaccessor. Diagnosis and patch from the laptop (W4 Task 3, spike2026-09-29-w4-test241-fibers): 723/723 runs and gate 36/36 on win32. -
nyx_goroutine_block_on_fdno longer leaves a goroutine blocked forever. It ignored the -1 fromnyx_event_loop_add: with nothing registered to wake it, the goroutine stayed BLOCKED and itsjoinnever returned (on win32 with any fd; on POSIX with an invalid one). Now it undoes the state and returns -1. New test intest_scheduler.c, with analarmthat turns the hang into a failure. -
s[i]on aStringreads the correct character.codegen_indexdid not distinguish the String receiver and fell into the Array get (same in-memory shape), which reads 8 bytes at positioni*8:"abcdefghijklmnopq"[1]gaveiinstead ofb,[2]gaveq, andlet c: int = "abc"[0]gave 6513249, all with rc 0.[0]toward acharcame out right by chance. Now it is bytei, just likes.charAt(i)(and out of range aborts the same way). Found while measuring NYX2019 false positives. Regression:test-457, which compares againstcharAtin eight contexts (annotated let int/char, unannotated, comparison with char and with int, loop, computed index, builtin argument). Gotchastring-index-byte. -
nyx archivo.nxshows compiler warnings. On successful compilation the wrapper threw away the whole log, so no⚠reached the user on that path (not even NYX2019, which in 0.35.x is a warning precisely so that it is seen before becoming an error in 0.36.0);nyx buildalready forwarded them from the arc compilacion-separada. Found while testing the installed 0.35.0. Guard inrun_tooling_gates.sh(make test-ai-first), with a positive control: a program without warnings shows none.
Changed
-
NYX2019 is an error also on the
letto an integer:let n: int = fila[0]with an element the compiler knows isString(annotatedArray<String>or inferred from its literal, orsplit()) compiled and stored the String's ADDRESS as if it were the number. In 0.35.x it was a⚠ warning, one release before the rejection (rule 7 ofdocs/VERSIONING.md); since 0.36.0 compilation fails with the function and the variable. Convert explicitly (string_to_int) or declare itString. The casesnyx2019-let-intandnyx2019-split-intof the error runner now expect the error; thenyx archivo.nxwarnings guard now uses the "untyped Array" warning. -
New
deferbehavior: it runs when the block that contains it closes . On falling off the end, onreturn, on?with Err, onbreakorcontinue; LIFO within the block and from the inside out; in a loop body, once per iteration; thereturnvalue is evaluated before thedefers and thedeferbody sees the values on exit (Zig/Swift semantics, not Go's). Measured with the full corpus (repo, nyxerp, stacks): 16defers, none outside the top level, so no known program changes result. Athrowthat leaves the function also runs the reacheddefers (arcdefer-alcanzado-y-throw, in this same release). -
New block-shadowing behavior: after the block the outer variable is read . A program that relied on reading or assigning, after the block, the inner variable that had shadowed the outer one now uses the outer one. Measured with
NYX_SCOPE_AUDIT=1(in the checker: reports every use of a name where checker and codegen saw different variables) over 1286 files of the repo, the nyxerp ERP and the stacks: zero real cases, so no known program changes result. To review your own code:NYX_SCOPE_AUDIT=1 nyx check archivo.nx(lines prefixed withscope-audit:).let x = x + 1in the same scope still replaces, as always. -
Goroutine context switching is own assembly on Linux aarch64 (previously only opt-in): glibc's
swapcontextdoes onert_sigprocmaskper switch, ~2230 ns per yield against ~175 ns in asm (64 goroutines, 1 worker). Validated with the runtime suite and 23 goroutine programs on both paths;-DNYX_OS_CTX_ASM=0goes back to ucontext. macOS and x86_64 stay on ucontext. Decision by Ottavio.
0.35.0 2026-09-29
MINOR because it rejects code that used to compile (): NYX1036/NYX2010 without the 0.34 transition, and NYX2019 (an error on
lettobooland on assignment; a warning onletto an integer, which becomes an error in 0.36.0). It also includes everything accumulated since 0.34.0:nyx exec, the public mirror withruntime/wasi/, crypto on wasm32-wasi,HttpOpts.cas_extra,arena_persistphase 1, and the fixes below. Verified withmake test-allon the cut; the only failure istest-wasm-29-arena-autocancelacionunder the node shim, which is intermittent and already filed (LOW): it fails the same way with 0.34 (5 runs out of 10 against 4 out of 10).
Changed — rejects code that used to compile (hence 0.35.0)
-
NYX1036 and NYX2010 without a transition: they are errors. In 0.34 the cases that showed up only because of the resolver fix of 2026-09-24 (closing a transitive import reopens the parent module, and up to 0.33.x that stretch was attributed to the main file) came out as
⚠ warningand compiled. Now they are errors like any other: addpubto the function or qualify the call. Everything that sustained the 0.33.x attribution in semantic and codegen was removed, andresolve.nxno longer emits//#module-reabre. Consumers measured beforehand: nyx-proxy had no cases, nyxerp was resolved on its live line, and nyx-queue was fixed in its repo (mq_ensure_queuebecomespub). -
NYX2019: an Array element that the compiler knows to be
Stringdoes not fit into anintorboolvariable.let n: int = fila[0]over["42"]stored the String's ADDRESS as if it were the number and came out as 0; withbool, and in the assignmentn = fila[0], it was invalid IR at link time, reported as a compiler bug.letwithbooland every assignment are errors from now on (they never linked, so nothing that worked is broken).letto an integer that is notbool, which did compile with the wrong value, is a warning in 0.35.x and becomes an error in 0.36.0 — rule 7 ofdocs/VERSIONING.md: what is going to reject code warns one release earlier. Convert withstring_to_intor declareString. It complements NYX2014, which acts at runtime when the element type is not known. Regression: three errors (let bool, assignment tointand toi32), two warnings that compile (let int,split()), the positive control, andtest-456.
Added
-
nyx exec [--version <id>] -- <cmd>: a single toolchain version for a whole suite (nyxerp, 2026-09-29). The wrapper pins the version per process, so a suite that launches many standalonenyx testruns resolved it in each one, and anyx updatein the middle split it between two compilers: the whole thing had to be relaunched.nyx execruns the command withNYX_HOMEpinned to the real path of the version (the active one, or one installed with--version) and passes the version lock down to it, so pruning does not delete it while it runs either. Andinstall-local/nyx updatewarn if the version they replace is in use.CLAUDE.mdrule 3 said that the foreign suite ended with its version; that held only for a process tree. Guard:make test-toolchain(cases 5-6, with a positive control: the same suite withoutexecgets split). -
CAPABILITIES.mdsays which builtins do not exist on wasm32-wasi (request from nyxerp, 2026-09-27): each one carries "· does not exist on wasm32-wasi" (original: «· no existe en wasm32-wasi»).std/builtins.indexadds a sixth field (no-wasm) that the generator derives fromwasm_forbidden_builtinand from thecodegen_target_guardcalls named after a builtin (atomics,run).run_wasm_builtin_symbols.shchecks that mark against what wasm ACTUALLY blocks —it compiles one probe per builtin— and fails if they differ: it was this cross-check that found the four that did not come from the list. An old five-field index still works. -
Hashes, HMAC and PBKDF2 on wasm32-wasi (friction from nyxerp, 2026-09-27).
sha256,sha256_raw,md5,hmac_sha256,hmac_sha256_rawandpbkdf2_hmac_sha256used to be "not supported on target 'wasm32-wasi'":crypto.cdid not make it into the wasm runtime because it included OpenSSL, and an offline POS had to ask WebCrypto for PBKDF2 from JavaScript. Hashes and HMAC were already our own C; PBKDF2 becomes our own too (previouslyPKCS5_PBKDF2_HMAC), with the HMAC over stack buffers —on wasm without an arena nothing is freed, and 80,000 iterations allocating on each one came to several MB—. Same bytes as OpenSSL: RFC 7914 vectors and two cases pinned against OpenSSL before the change (test_crypto.c). The OpenSSL part (Web Push) stays behind__wasi__. Regression:test-wasm-51-cripto(wasmtime and browser shim). -
HttpOpts.cas_extra: add certificates to ONE HTTPS request (friction from nyxerp, 2026-09-27). A site that sends an intermediate that does not match its leaf does not verify against the system CAs; since the store separation of 2026-09-14,tls_set_ca_fileno longer reaches the HTTPS ofstd/http, andSSL_CERT_FILEreplaces instead of adding, for the whole process and only before the first https.cas_extra(PEM content, one or several) is added to the system CAs only for that connection (SSL_set1_verify_cert_storeover a store owned by the SSL), with the chain and the name verified all the same; the rest of the process does not notice. A PEM with no readable certificates =Err(invalid, 22). At low level,try_tls_connect_cas(std/tls) andnyx_tls_connect_result_cas(runtime). Regression: sub-suitetest_http_cas_extra.py(test PKI with the wrong intermediate: 5 cases, including "does not leak into the next request" and "the name is still verified"). Recipe122-https-intermedio-extra. -
arena_persistcloses its phase 1:-
NYX1039 in semantic: persisting a struct, a
Map, anFnor anArray<Struct>is rejected innyx check, not only when generating code; semantic also sees the type of an Array's ELEMENT, which codegen does not know. -
Nested arrays: an Array stored inside another (literal,
push,a[i] = x) now carries theArraytag instead of none, andarena_persistdeep-copies it (before, it aborted). Typed reads that clash with that tag —reading an Array asStringorfloat— go from silently returning garbage to aborting with a diagnostic; asintthey still give the raw value. Verified: the compiler with the new tags compiles itself without false aborts (fixed point in three passes). -
On native it is the identity (Ottavio's decision): there is no per-event arena and the GC already keeps the value, so the same code runs with
nyx testand on wasm without per-target branches. Before, it was a target error. -
Documentation: SPEC (NYX1039), LLM.md §wasm, gotcha
wasm-arena-closure-env, recipe121-arena-persist-wasm. Regression: 3 negative cases and 1 positive intests/compiler/errors/, and wasm test 36 adds a nested array with computed integers (10,000 events, arena on).
-
NYX1039 in semantic: persisting a struct, a
-
nyx update --sync-docsno longer overwrites what a project wrote in itsAGENTS.md. Until now it reseeded the whole file and only saved aproyecto:inicio…proyecto:finblock that the project had to know about; nyxerp lost its "Project rules" several times that way. Now nyx replaces ONLY its own block, between the lines<!-- nyx:inicio -->and<!-- nyx:fin -->(seeded bynyx init); what is outside belongs to the project, and its own content goes on top, where the agent reads it first. A previousAGENTS.mdis migrated by##sections: the one the template does not recognize as its own is kept verbatim on top, and the sync reports what it kept and what it replaced (with.bak).nyx sdd initputs its trigger above the block. Logic innyx_build agents-merge(agents_merge, compiler/build.nx), the same forinitand--sync-docs. Regression: scenarios (g)–(k) ofrun_sync_docs_migration.sh; on a copy of nyxerp's realAGENTS.md, the previous wrapper left 0 of its rules and the new one keeps them byte for byte. -
Send timeout on the HTTP servers and header timeout in
http_serve/http_serve_mt.NYX_HTTP_SEND_SECS(30 s by default) bounds how long the writing of a response can stay stuck without progress: a client that stopped READING a large response held itsstd/serveworker forever. It is per write, not total (a slow download that makes progress is not cut off). The twostd/httpservers also used to read with no timeout: a connection that dripped the header (slowloris) silencedhttp_serve, which has a single thread; they now use thestd/serveparser with a timeout (NYX_HTTP_HEADER_SECS, 408 and close). Runtime:os_sock_set_send_timeout(SO_SNDTIMEOonly) andnyx_tcp_set_send_timeout. Regression: new sub-suitetest_http_plazos.py(8 checks, red with the previousstd/http) and 1 check intest_serve_std_keepalive.py(22 → 23). -
std/serveannounces its keep-alive timeout: every response withConnection: keep-alivecarriesKeep-Alive: timeout=N(N =NYX_HTTP_KEEPALIVE_SECS). A proxy with a pool that did not know this reused the connection already closed by the server, and a POST —which is not retried— got a 502 (nyxerp → sumain2, 2026-09-25). The proxy side (discarding dead or expired connections before writing) is an assignment to nyx-proxy. Regression: 1 check intest_serve_std_keepalive.py(21 → 22).
Fixed
-
An
assertmessage can be an opaque value (friction from nyxerp, 2026-09-17).assert(arr[0] == "x", arr[0])with anArrayreturned by a fn did not link: the message arrived asi64atnyx_string_to_cstr, which wants%nyx_string*(invalid IR), withnyx checkgreen. The builtins-arg-opaco arc had converted the builtins' arguments, butassertis a statement and was left out. The three sites ofcodegen_assertthat evaluate the message (generic path and theint == intandString == Stringbranches) now go throughcodegen_arg_str. The filed report said it worked inmain(); measured today, it failed the same. Regression:test-455(one line per site, each one broke the link on its own). -
nyx updateleft the version withoutruntime/wasi/and nothing compiled to wasm32-wasi (friction from nyxerp, 2026-09-29). The public mirror —whichnyx updatebuilds from— never had that folder:sync_to_public.shcopiedruntime/*.c, which does not descend into subfolders, and onlyruntime/os/was listed explicitly. It went unnoticed because development installs come frommake install-local, which does copy it. The sync now copies the whole folder and does not commit ifmain_shim.c,nyx_arena.c, thegc.hfiles oros_wasm.care missing. Verified withinstall.shfrom the mirror in an isolated HOME and with a realnyx update. -
.to_string()oncharand on fixed-width integers (i8,u8,i16,u16,i32,u32) aborted with NYX2002.char,i8andu8sharei8in LLVM: the helper now receives the Nyx type of the receiver, so achargives the character ("A") and au8the number ("200"), and the sign decidessext/zext. The NYX1022 catalog addschar(only with an annotation, like the other scalars).codegen_method_callstack unchanged (helper forms without literals). Regression:test-454. -
NYX1022 also on
int,floatandbool. Semantic's per-type method catalog covered String, Array, Map, Option and Result; without a catalog, a scalar approved any method, andx.contains("a")withx: intwent down in codegen through the arrays branch (ani64can be an untyped Array): invalid IR withnyx checkgreen. On a scalar onlyto_stringexists. The check applies only to variables DECLARED with a scalar annotation (var x: int): an inferred scalar can lie ([1, "dos"]is inferred asArray<int>anda[1].contains("d")is legitimate, test-323), so there the runtime tag still decides. Verified with no false positives against all the compiler modules with semantic active, the real nyxerp, and the stacks.charis left without a catalog (itsto_stringis missing; filed in TASKS). Regression:test-nyx1022-metodo-sobre-int(test-errors 347 → 348). -
let x: T = <scalar of another width>converts to the annotated type.let s: int = w + hwithw/hof typei32stored ani32in a variable declaredint: the following.to_string()failed in codegen withnyx checkgreen, and the arithmetic stayed at 32 bits. Assignment already converted;letdid not. Integers among themselves andfloat/f32among themselves; an annotatedboolis not truncated. Regression:test-452. -
A method on a primitive
const(K.to_string(), local or imported) gave "NYX2002: method 'to_string' is not available on the global 'K'": an int/float/bool const is inlined and is not a variable, so it fell into the globals branch. Now it is materialized as a local variable and follows the normal path; the Map trio (contains/insert/get) still gives NYX2007 on a const. Regression:test-453. Along the way it was filed that the same trio on anintVARIABLE gives invalid IR with no diagnostic (a previous bug). The generic "method not available on this receiver" error (the catch-all ofcodegen_method_call) carried no code: it is now NYX2002, like the one for globals — a nonexistent method on a const still gives NYX2002. -
Internal:
interfaz_de(the per-interface fingerprint of[lib],compiler/build.nx) compares againstcharliterals instead of ASCII codes, now that the bug that forced it is fixed. -
Toolchain: a new version no longer inherits the active one's
.fijadamark.install-localbuilds the new version by copying the active one, and if that one was pinned (today, nyxerp's production one) the copy carried the mark along: the new version ended up pinned without anyone asking, and pruning would never delete it.nyx_tc_publicarremoves the mark before publishing. Guard:run_toolchain_versions.sh(4b), red without the fix. -
An opaque value can be passed directly to any builtin. An element of an untyped Array (
get_args()[i], an Array returned by a fn),m.get(k)or the return of a signature-lessFnreached the builtins asi64, and nobody converted it:read_file(args[2])went down tocall @nyx_read_file(i8* %<i64>)—invalid IR that clang rejected at link time, withnyx checkgreen—. Measured: 76 of 87 builtins with a pointer parameter (files, environment, crypto, regex, network/TLS, mutex/channels/condvar/rwlock,panic, …). A single helper (codegen_argand its four forms, incompiler/codegen.nx) converts the argument to the type that the handler already declares:inttoptrtowards a pointer,ptrtointtowards ani64slot (sochannel_send(ch, <String>), which did not link either, works). The four forms do not carry the type as a literal so as not to bloat the frame ofcodegen_call_expr(measured: +5.4 KB per level with the literal; with the forms, within the ceiling). New guard:run_builtin_arg_opaco.sh(intest-ai-first; 76/87 red with the previous compiler), andtest-451checks the result, not just the IR (sha256, setenv/getenv, write/read_file, a String through a channel, a mutex taken out of an Array). Closes two TASKS entries. -
std/postgres: the pool is thread-safe (friction from nyxerp, 2026-09-27). Withstd/serveand 16 threads sharing onepg_pool_new, the server hung in 3 of 6 runs; with 8, it died with "Index 0 out of range" (original: «Índice 0 fuera de rango»).try_pg_pool_getlooked for a free slot and marked it in use in two steps without a lock: two threads took the SAME connection and mixed the protocol on one socket.PgPoolnow carries a mutex (fieldmu) taken only for the slot state, never during I/O (connecting, the ROLLBACK ofpg_pool_put, closing). Regression:tests/postgres/09-pool-hilos.nx(16 threads × 300 queries over a pool of 4, each response checked against its thread): with the previous pool it fails 4 of 4 runs, with the new one it passes 9 of 9. -
std/serve: an idle connection that times out with a request already arrived is served, not reset. The parking sweep (runtime/net.c) closed on timeout without checking whether the socket already had a request that the event loop had not yet seen; closing with unread data makes the kernel send an RST, and the client saw ECONNRESET on a request that the server did receive. Now the expired ones with data are handed to the workers. It came out of analyzing nyxerp's load test (ECONNRESET only under saturation); the data point mostly to the keep-alive race of the generator itself, so this is a hardening, not the measured cause. Deterministic regression with a-DNYX_RUNTIME_TESTINGhook (parking thread held still):test_http_park_vencida_con_pedido, red without the fix (the client receives ECONNRESET). -
Assigning an opaque value to an already declared variable converts it to its type (nyxerp friction 20260925-090024).
let x: S = xs[0]worked, butvar s: S = …+s = xs[0]emittedstore %S %<i64>: invalid IR,nyx checksilent and the link rejected by clang. The conversion of the opaque value (xs[i],m.get(k), return of a signature-lessFn) lived inline incodegen_letand assignment had a partial copy (onlyStringandArray); now both usecoerce_opaque_i64— struct, enum,Map,bool,char, narrow integers, float,Array<T>,Fn(...)— andn = m.get(k)on a declaredintdoes theptrtoint. Regression:test-450-asignar-valor-opaco-a-var-declarada(9 type families). -
std/serve:HEADno longer contaminates the keep-alive connection. AHEADfell to the 404 and the 404 went out with a body; a proxy that reused the connection (nyx-gateway) read those bytes as the start of the next response and returned a 502 to another user — seen in production at serve.nyxlang.com on 2026-09-25. Now aHEADwithout its own route uses theGETroute of the same path (also in mounted routers; an explicitHEADroute wins) and every response to aHEADgoes out without a body with theGET'sContent-Length(RFC 9110 §9.3.2); ansse_opendoes not open the channel on aHEAD. Regression: 8 new checks intest_serve_std_keepalive.py(HEAD+GETpipelined on the same connection; 13 → 21).
0.34.0 2026-09-25
A big release, driven almost entirely by friction from nyxerp (an ERP written in Nyx) ahead of its first production deployment (Sumain, 2026-09-26). The highlights:
-
[lib] modulesready for real projects: every import form, transitivity, type checking identical to a build without[lib](the importer reads the whole module; codegen declares),<module>__<fn>names, and recompilation by INTERFACE (a comment in a central module: 1 module, not 105). The nyxerp suite: 1,566 s → ~366-431 s. -
Toolchain with side-by-side versions in
~/.nyx: installing no longer swaps the compiler under a running suite;nyx update --version <rev>returns to an installed revision instantly;.fijada(pinned) versions that are never pruned. -
New stdlib:
std/pdf,std/barcode(Code 128, EAN-13, QR),std/browser_serial(Web Serial), LISTEN/NOTIFY instd/postgres. -
std/serve: an idle keep-alive connection no longer holds a worker; idle and header timeouts. -
Tooling: automatic precompiled runtime (
nyx build9.9 s → 1.0 s), CAPABILITIES by stdlib signature,nyx checkwithout the LSP protocol on screen, compiler fingerprint innyx test. - wasm: a guard checks every builtin against the wasm runtime (25 cases fixed or given a clear error).
- Transition: calls to a private fn of another module (NYX1036) and the ambiguities (NYX2010) that a resolver bug hid up to 0.33.x remain a WARNING in 0.34.0 and become errors in 0.35.0 (kept for one more cycle so as not to break other people's builds on the eve of production).
Fixed
-
Shared toolchain safety (nyxerp friction
20260924-020001-team-4):nyx update --helpactually updated (every unknown argument fell through to the update); now--help/-hshow help,--checkonly queries, and an unknown option is an error (rc 2).nyx updatetakes the EXCLUSIVE toolchain lock andnyx check/vet/fmtandnyx file.nxtake the SHARED one (with a warning andNYX_LOCK_WAIT).nyx testprints the compiler fingerprint and warns if it changed during the run. -
CAPABILITIES.md is regenerated when the stdlib changes, not only when the version number does: that is why the nyxerp index did not list
std/smtpand they reimplemented it by hand. -
[lib] moduleswith the import form of real projects (nyxerp friction20260924-040001-team-1):import "src/x"without braces —the form used bynyx initand by 3.372 of nyxerp's 3.373 imports— did not cross the boundary (NYX1002 on every call). Now declaring a module in[lib]does not change which names are visible: every import form behaves as if inlined, transitivity included, andstruct/enumwithoutpubtravel too. Library fns are emitted as<module>__<fn>just like inlined ones (two libraries can have homonymous fns). Also fixed in the declaration signature reader: the arrow of anFn(...) -> Tparameter was taken for the return arrow, a multi-line signature silently lost its return type (void), and the generic return type (Result<T, E>) was truncated. If a module uses a fn it does not import,nyx buildsays so with a note and the missing import. Verified on the real nyxerp: 150 modules in[lib]compile and link;make test-lib-realwatches over it. -
Homonymous privates in two modules overwrote each other depending on import order (nyxerp friction
20260924-010014-team-1): the resolver closed an inlined transitive import by returning to the main file, so everything the parent module declared afterwards was attributed to the program. Now the close reopens the parent module. The same bug switched off NYX1036/NYX2010 in those stretches: the cases that show up only because of the corrected attribution come out as a warning (they compile, rc 0) until the next minor version.std/urlandstd/webshare theurl_decodeofstd/percent, which is new.test-439,test-440, +4 cases intest-errors. -
std/serve: an idle keep-alive connection no longer holds a worker (nyxerp friction20260924-200001-team-1): with N threads, N browsers sitting idle between clicks left the server unresponsive to EVERYONE. After responding, the worker stayed blocked reading the next request with no deadline. Now the connection is parked in the runtime event loop (like SSE) until another request arrives. Limits:NYX_HTTP_KEEPALIVE_SECS(15 s of inactivity) andNYX_HTTP_HEADER_SECS(10 s for the complete header: 408 against slowloris);serve_idle_connections(). 13 new E2E checks (6 fail with the old code), recipe 120. -
A private fn of a
[lib]module competed with the importer's own (regression from the previous delivery, nyxerp friction20260924-220014-team-1): NYX2010 «AMBIGUA» (AMBIGUOUS) in 8 test files. A foreign private no longer beats a visible one;nyx testattributes a test file to its module only if it is in[lib]. A toolchain version with.fijadais never pruned. -
Five old bugs:
import { K }of aconstgave NYX1013; the visibility ofconsts depended on the order of the imports; assigningcharAt()to a fixed-widthvaremitted invalid IR; three.node_typereads on Arrays in the parser itself compiled to 0 silently (theE.V(a, b)patterns never came out asmatch_pattern; that catch-all now aborts with NYX2007); andtry_tcp_read_timeddid not drain the per-fd buffer first.test-447..449. -
std/postgres: notifications that arrived during a query were dropped (nyxerp friction20260924-180024-team-1). Now they are stored on the connection and delivered by the next wait. New LISTEN/NOTIFY:try_pg_listen,try_pg_unlisten,try_pg_notifyandtry_pg_wait_notifications(with a timeout), in plaintext and with TLS. The real-time bus between several processes on top of PostgreSQL, without Redis; together with the SSE ofstd/serveit closes the case.std/net:try_tcp_read_timed.test-446(fake server),tests/postgres/08(real PostgreSQL), recipe 119. -
[lib]recompiles by interface, not by content (nyxerp friction20260924-150024-team-1): a comment in a central module recompiled 105 of nyxerp's 171 modules (216 s); now 1 (22 s). For each imported library the fingerprint looks at its signatures, types, constants and globals, not the bodies or the comments. -
[lib]no longer has a type hole at the boundary (nyxerp friction20260924-150024-team-1): aStringwhere anintwas expected, when calling a fn of another library, passednyx buildgreen and arrived as the pointer of the text printed as a number. The importer now reads the full source of each[lib]module (type checking is identical to a build without[lib], by construction) and codegen emits their fns asdeclarefrom the AST, with the exact signature. The hand-built interface was removed (synthetic imports, injected types, signatures read from the text). A library's globals are still initialized by the program, in order, as with inlined ones (guard inrun_lib_modules.sh). -
Toolchain with side-by-side versions (nyxerp friction
20260924-020001-team-4) :~/.nyxkeeps each complete version inversions/<id>/, andcurrent/activesay which one is active; the entries at the root are links, so the usual paths (~/.nyx/bin,~/.nyx/VERSION) keep working.make install-localandnyx updateassemble the new version separately and activate it with arename: a suite in mid-run carries on whole with the version it started with, installing no longer waits for anyone, and a failed update does not leave the installation broken.nyx update --version <rev>returns to an already installed revision instantly (415 ms). Pruning (NYX_KEEP_VERSIONS, 3) never deletes a version in use. The first installation with the new toolchain migrates the flat layout on its own.make test-toolchain. -
nyx checkshows only the diagnostics: it dumped hundreds ofSYM:/DEF:/ENDlines of the LSP protocol even when the file was healthy. The protocol is still available with--lsp. And after a NYX1040 (the samestructin two modules) the cascade of NYX1017/NYX1032 against the first definition no longer appears; it sent people looking for the error where it is not. -
No builtin dies in wasm-ld with «undefined symbol»:
string_from_bytes(and with itbase64_decode) did not link on wasm32-wasi because its runtime lived outsidewasm.srcs. A new guard (run_wasm_builtin_symbols.sh, inmake test-wasm) checks every builtin against the wasm runtime and found 25 cases. The pure-C functions were moved to portable units; those that depend on processes, network, signals or OpenSSL give a clear error with file and line.test-wasm-50. -
A mention of
compiler/in a comment or string removed the prelude:printlnundeclared. Now only animport "compiler/..."or the// nyx:sin-preludemarker counts.test-435.
Added
-
Automatic precompiled C runtime in
~/.cache/nyx/rt/fornyx buildandnyx test(content-keyed;NYX_NO_RT_CACHE=1turns it off):nyx build9.9 s → 1.0 s, onenyx testfile 9.7 s → 1.2 s, without configuring anything. -
std/barcode(nyxerp friction20260923-210010-team-1): Code 128 (sets B/C chosen automatically), EAN-13 (computes or validates the check digit) and QR versions 1-10 with levels L/M/Q/H (qr_matrix), in pure Nyx and with wasm parity. The QR is verified against the standard's published vectors and against an independent decoder written in the test.test-436..438,test-wasm-42..44, recipe 116. -
std/browser_serial(nyxerp friction20260924-020001-team-2): Web Serial withawaitin wasm for point-of-sale and fiscal printers (serial_request_port,serial_open,serial_write,serial_readwith a timeout,serial_close). Bytes cross asArray<int>. Without Web Serial (Firefox, Safari, no HTTPS) it gives anErrof kindio. WebUSB is left for a phase 2.test-wasm-41(with a mock), recipe 117. -
std/pdf(nyxerp friction20260924-020001-team-1): PDF in pure Nyx, also in wasm. Sizes letter, A4, half letter and free width; positions in mm from the top left. Text with the standard fonts and WinAnsi (accents, ñ, ¿¡, €), with/Widthsandpdf_text_widthto align amounts. Lines, rectangles, JPEG (progressive too, with EXIF orientation) and PNG (alpha in SMask). Verified with strict pypdf, pdfminer and Chrome's PDF engine (pdfium), renders inspected by eye.run_pdf_verify.sh(optional).test-441..445,test-wasm-45..49, recipe 118 (an invoice). -
CAPABILITIES.md indexes
async fns:std/browser_idbandstd/browser_serialdid not appear. -
DRAFT specs for approval:
std/pdfand atomic toolchain (side-by-side versions).
Fixed (earlier)
-
NYX2010 also in
nyx check(nyxerp friction20260923-210010-team-3): an unqualified call to a fn that exists in two imported modules only came out atnyx build(in nyxerp, 3.5 minutes after a greencheck). The checker already detected the ambiguity but only switched off the signature check. Resolving by arity when it differs remains an open design decision. -
json_parseof a\uXXXXescape trapped in wasm («unreachable»). Cause in codegen: an i64 argument was not truncated to the width of achar/i8/i16/i32 parameter; natively it went unnoticed.test-wasm-40. -
Linear
json_stringify(nyxerp friction20260923-210010-team-2): writing a 32 KB text went from 504 ms to 0.66 ms — it used to grow with the square of the length. A singleStringBuilderwalks the tree and the stretches without escapes are copied in one go. -
"".trim()under the wasm arena returned length 1 or 2 (nyxerp friction20260923-100024, it ended in «memory access out of bounds»). The cause was nottrim: the empty String that the whole runtime shares was born in turn memory if it was requested for the first time during an event, and the arena recycled it. It affectedtrim,repeat(0)and the ~200nyx_string_from_cstr("")calls in the runtime. Now it is a static object.test-wasm-39. -
json_parsein wasm: 5.000 records from 6.3 s to ~0.15 s in node and from 235 to 35 MB (nyxerp friction20260923-100024). The persistent region requests memory in geometric blocks (64 KB → 8 MB) instead of growing the wasm one page at a time, andparse_stringcopies by stretches instead of concatenating byte by byte (quadratic). What remains (~18 B per byte of JSON) is the size of the tree. -
Two
structs orenums with the same name in different modules: NYX1040 instead of «redefinition of type» over a temporary file (nyxerp friction20260923-160005). It shows up innyx checkand in the build, naming both files and lines. Two identicalenums are accepted (they compile today). Thestruct/enumnode now carries the line of the NAME, not that of the closing}. -
wasm: a global Array or StringBuilder that grew during an event was left pointing at turn memory . With the arena on, the growth
reallocallocated the new block in the arena, which is recycled when the event ends: 1.000pushes of integers to a global gave a garbage sum. Now every persistent block carries a mark in its header andreallocpreserves the region of origin (O(1), without touching alloc/reset/pin).test-wasm-38. -
Computed integers carry a tag in the Array: before, only literals did. The evidence is the shape of the AST (arithmetic,
length(), explicit: int), never the inferred type. Visible effect:a.push(n * 3)read as a String goes from SIGSEGV to an orderly abort. -
The compiler no longer runs out of stack on long expressions: from 52 to ~2.400 operands with the default stack. The cause, measured with
clang -fstack-usage: the toolchain is linked at-O0and each level of an expression stacked ~151 KB of codegen frames. Three pieces: (1)codegen_binopandcodegen_exprsplit so that the recursion goes through small frames —a pure refactor, identical IR in 524 files— (151 → 23 KB per level, ceiling 322); (2) SROA over the seeds when there is anoptof the same version as clang, +2 s per bootstrap (23 KB → 2 KB per level, ceiling ~2.400; withoutopt, 322); (3) the main thread's stack overflow has a name: infinite recursion inmainprints «main thread stack overflow» («desborde de pila del hilo principal») with the limit, instead of a mute «Segmentation fault» (POSIX; Windows is left for W4). New guardrun_stack_frame_audit.shinmake test-ai-first, a ratchet on the frames of the recursive path. The floor that remains isCodegenContextby value: its root is the draft arcstructs-byval. -
A 53-operand expression made the compiler crash with SIGSEGV and no message (nyxerp friction
20260920-200024: a stylesheet of 75 concatenations, an hour of manual bisection;nyx checkandnyx vetgave green). Measured cause: the toolchain is linked without-Oand each level of a binary expression stacks ~151 KB of codegen frames (codegen_binopalone, 132.880 B at-O0against 1.488 B at-O2), so 53 levels exhausted the default 8 MB. Mitigated:nyx,nyx buildandnyx testraise THE COMPILER's stack to 64 MB —the user's program keeps its own— (ceiling from ~52 to ~430 operands), and if it still dies, the message names the cause instead of leaving only «Segmentation fault». The Makefile recipes also raise it (fromscripts/with_testroot_lock.sh):make recompile-allcould no longer compilelexer.nxin 8 MB. The root cause stays in thepila-del-compiladorspec (DRAFT, GO pending). -
Iterating over a
Stringwithfor … inkilled the program with SIGSEGV and no location (nyxerp friction20260921-100001).nyx check,nyx vetandnyx buildgave green: codegen has no String path, so the iterable fell into the array path and the%nyx_string*was read as{ i64, i8* }*. Now it is NYX1038 with file and line, and the hint of iterating by index (bytes withsubstring, UTF-8 characters withchar_substring); the same rule lives in codegen for when semantic is off. It is an error and not a new capability on purpose: iterating over a string forces a choice between bytes and codepoints, and that remains open. The dynamic variant —a String inside a bareArray, iterated withfor g: Array in xs— blew up in the INNER loop, far from the culprit: now theforreads the slot checking its tag and aborts in the outer one with NYX2018 naming the slot and its real type (andfor s: String in xsover anintslot uses the checked read that already existed). Gotchafor-in-string-rejected. -
semanticwas QUADRATIC in the size of the closure: the symbol and signature tables were walked whole, comparing strings.scope_lookupandfn_sig_indexscanned the whole ofg_sym_namesandg_fn_sig_nameson every query. Since the resolver inlines the import closure into a single translation unit, those tables grow with the number of modules while the number of lookups grows with the volume of code: the product is the quadratic. Now there is a per-name index (Map<String,int>+ a parallel array that chains homonyms in declaration order) over the two tables, and the other six consumers —scope_get_arity,lookup_var_type,name_is_builtin,fn_es_privada_de,fn_module_resolve,scope_has_in_current— walk the name's chain instead of the table.Measured (aarch64, median of 3, with the machine verified idle), on a synthetic project of 175.375 lines of IR —the scale of nyxerp's closure, which measured 184,080—: front-end 4,680 → 2,700 ms (−42.3%), exponent over four sizes 1.20 → 1.04, and the cost per line of IR from 26.7 → 15.4 µs (it used to double between 7.5k and 175k lines; now it is flat). Peak RSS unchanged: 288 → 286 MB.
make bench-test-cachereproduces it. The shape of the profile changed: semantic goes from 54.9% to 16.8% of the front-end, andnyx_string_equalsfrom first leaf with 24.4% to sixth with 4.9%. The next bottleneck is allocation (GC_generic_malloc*, 40%), filed inTASKS.md. Confirmed from outside the core (nyxerp, 2026-09-21): their full suite —112 files, 1.200 tests, against PostgreSQL— went from 23 min 07 s to 20 min 43 s (−9.2%), measured against MORE work (1.191 → 1.200 tests) and without a quiet window, so it is not comparable with the 42% of the isolated front-end: there the front-end is a fraction of the total clock. And the effect on MEMORY turned out to be the one that helps them most: the peak of compiling and running their heaviest file (39 tests) went from 1,357 MB to 198 MB — that is the joint effect of this arc and of the quadraticemitfix (v0.32.2), because their previous measurement dates from 2026-09-17. With 2 cores and 3.8 GB, two compilations at once now fit and before did not: the arc unlocked a factor of 2 that they will go looking for on their own. How we verified that behavior does not change: each migrated function ran IN PARALLEL with its original scan and a gate that aborted the compilation if the two paths differed, overmake test(449),test-errors(309),test-m08-types(18) and the dispatch matrix — ~776 tests, zero discrepancies, in the three batches. Only with that green was each scan retired. The gates were verified by sabotaging the index andscope_pop, because a gate that cannot be shown to fail proves nothing. A finding the sabotage uncovered: withscope_popnot undoing its stretch of the index,make test-errorsgives 309/309 — the whole corpus of the repo does NOT catch that bug, and the compiler resolves symbols with the wrongkindwithout saying a word. That is why all queries now go throughindice_cabeza(), which validates that the head of the chain is alive always, with no environment variable: it costs one integer comparison and turns that silently-wrong into a loud error. Tests:test-432-module-fn-homonyms-caller(450 files / 449 ARM64) pins the UNQUALIFIED resolution from inside a module —the rule thatscope_get_arityandfn_sig_indexdepend on—, whichtest-353did not cover. Design:docs/design/specs/2026-09-21-semantic-indice-simbolos-design.md. -
Voseo in five lines of
std/smtp.nx, two of them user-visible messages («probá el 465» ("try 465"), «Pedí STARTTLS primero … o conectá al 465» ("Ask for STARTTLS first … or connect to 465")) . The guardrun_voseo_messages.shhad been red since 2026-09-20, somake test-alldid not pass. -
Writing over TLS to a peer that had already closed KILLED THE PROCESS with SIGPIPE . OpenSSL writes with a raw
write()and this runtime deliberately does not install a globalSIG_IGNfor SIGPIPE (os_sock_sendprotects itself withMSG_NOSIGNAL, and its comment states that this is «load-bearing»), so the TLS layer was left without that protection: atls_write_connagainst a departed peer killed the entire process.runtime/tls.cnow uses its own BIO that writes throughos_sock_send, instead of ignoring the signal at runtime startup —which would have changed the semantics of every program that links Nyx,nyx prog | headincluded—. The 5SSL_set_fdcalls in the file go throughtls_attach_fd, with the same contract (BIO_NOCLOSE: the caller closes the fd) and without depending on theBIO_meth_get_*, deprecated since OpenSSL 3.5.Why only now: in a normal HTTP response the window between «the client closed» and «the server writes» is milliseconds. In a TUNNEL it lasts as long as the stream does, so closing a tab was enough to bring down a gateway that serves every domain. It was found by the
nyx-proxysession while building the SSE tunnel (measured: rc=141 = 128+SIGPIPE).ws_tunnelhad had the same hole since 2026-07-01 and is covered by the same fix. Tests:test_tls_write_to_gone_peer_no_sigpipeintests/runtime-unit/test_tls.c(+4 asserts, 264 → 268), which measures whether the signal IS GENERATED rather than whether the process survives — the suite hadsignal(SIGPIPE, SIG_IGN)in itsmainand so could not see this bug. Confirmed from outside the core (nyx-proxy, 2026-09-20): with the gateway recompiled against this runtime and no mitigation anywhere, an SSE client over TLS that cuts off abruptly with RST mid-stream leaves the process alive; the same case died withrc=141against the previous runtime. The relay did not change (first event at 0.01 s, the second at 0.31 s against a 0.3 s pause from the backend): routing throughos_sock_sendadded no latency or buffering. It remains as a permanent positive control on that side — if it dies with 141 again, the regression is the runtime's. -
test-377-try-httpfailed because of ports, not code: it used 58744-58749, which fall inside/proc/sys/net/ipv4/ip_local_port_range(32768-60999), so the kernel handed them out as the SOURCE port of any outgoing connection and the test died with «cannot bind port NNNNN: Address already in use». It fell three times on 2026-09-15 and once more on 2026-09-20. Moved to 187xx, below the ephemeral range. A red that is not a regression hides the real ones. -
The E2E
tests/integration/test_ws_proxy.pySKIPPED silently since the migration to~/nyx/(2026-07-22): its defaultNYX_PROXY_SRCpointed to~/nyx-proxy-stack, which no longer exists. Default fixed to~/nyx/products/proxyand the skip made more explicit.
Added
-
nyx update --sync-docspreserves the project's own block inAGENTS.md(nyxerp request, 2026-09-24: it wiped their «Reglas de este proyecto» ("Rules of this project") four times). What lies between<!-- proyecto:inicio -->and<!-- proyecto:fin -->survives the reseeding, and a run that changes nothing no longer leaves a new.bak(before, it always did). Documented in the template. -
nyx testwith[lib] modules: libraries are compiled once per suite and each test file only links them (6.4 s → 1.4 s per file on the 250-module bench). -
form_values/query_valuesinstd/web(nyxerp friction20260923-160005): all the values of a repeated key (checkboxes,<select multiple>), in order;parse_form_data_allandparse_query_string_allreturn the pairs uncollapsed.req.formdoes not change: with repeated keys the last one still wins, and now the doc says so. Recipe 115. -
[lib] modulesin nyx.toml: separate compilation with object reuse .nyx buildcompiles each declared module once intotarget/nyx-lib/and reuses it as long as neither it nor its import closure changes; touching a leaf module and recompiling costs 41% (88 modules) and 23% (250) of doing it without[lib]. Functions, structs and enums cross, viaimport { f } fromand viaimport "m" as a; generics,impland traits fall back to inlining with the NYX0302 warning, which is now shown even when the build succeeds.nyx buildalso honorsNYX_RT_ARCHIVE. Limits: no type checking of the arguments at the boundary, andnyx testdoes not use[lib]yet. -
arena_persist(x)andarena_stats()in wasm, PARTIAL phase 1: copies a String or an Array (of String/int/float/bool) from the event arena to the persistent region, to keep it between events;arena_stats()measures persistent and turn bytes from the inside. A struct/Map/Fn is NYX1039 (today in codegen; the check in semantic and the documentation are still pending). -
std/browser_idb: IndexedDB for offline PWAs (nyxerp request). Asynchronousidb_get/idb_put/idb_delete/idb_keys(await, Asyncify) withResult<_, Error>and the usual kinds (not_found,in_use,oom,io); a single database with a composite key[store, key], so that a new store does not trigger a version bump that blocks across tabs. A separate module: only whoever imports it pays for Asyncify. Injectableopts.idbin the shim to test without a browser (test-wasm-35-idb), recipe114-idb-offline-wasm. -
make bench-test-cache— a bench measuring the cost of compiling a test file, by phase (front/clang_ll/link/run/hash), on synthetic three-layer projects with reuse, from 8 to 250 modules. It was born to re-measure the filed item about the closure-hash binary cache and ended up being the instrument of the arc.It demands a quiet window before measuring: it waits until there are no foreign compilers, and if one enters in the middle of a repetition it discards the repetition and repeats it. If the wait times out, it ABORTS naming the process occupying the machine instead of reporting noise. It is not paranoia: the arc's first baseline came out 9% high because another session was running its suite on the same two-core machine, and with the guard in place the bench repeats to 0.1%.
-
std/smtp— sending mail from Nyx (nyxerp request, which was left with no way to send a password-recovery link: the only way out their system could offer was to create a new user). A connection through both real paths —587 withSTARTTLSand 465 with TLS from the greeting—,AUTH PLAIN/LOGIN, and the message with headers, text or HTML body and binary attachments in base64. Zero new builtins: the low-level piece already existed (tls_upgrade_fd, which is the exact shape of STARTTLS and whichstd/postgresalready used that way).What the module solves on its own, which is where hand-made implementations break: dot-stuffing (a body line that starts with
.goes out doubled; without that the server considers the message finished right there and delivers half of it, with NO ERROR from anywhere), CRLF normalization, base64 wrapped at 76 columns, theMessage-IDwith the sender's domain, and the secondEHLOafter encrypting —mandatory, because the capabilities change—.AUTHover a cleartext channel is rejected and there is no flag to force it: the check looks at the real state of the channel, not at what the server announces. And the certificate is verified by default, with_insecurevariants that say so in the name — the same criterion applied tohttps_geton 2026-09-11.Out of scope by decision of the arc: receiving (IMAP/POP3), queues and retries, nested multipart,
quoted-printable, RFC 2047 file names and DKIM/SPF/DMARC. Doc:docs/SPEC.md§Correo saliente,LLM.md§std/smtp. Recipe:examples/by-example/113-smtp.nx. Tests: stdlib 5 → 9 (parsing and dialogue, STARTTLS/TLS/AUTH, message assembly and E2E sending, and errors distinguishable by value). -
scripts/run_stdlib_tests.shlinks the FULL runtime: it had a trimmed list (withoutcompress.c,scheduler.c,event_loop.cand eight more), so a std module that used a builtin from any of those could not be tested there — it failed in the linker with «undefined reference», not in the test. A runner that links a different runtime from the one the toolchain links does not test what people run.
Changed
-
SSE now works behind the gateway:
nyx-proxy0.4.4 tunnels the stream (detectsContent-Type: text/event-streamand stops accumulating), deployed on:443on 2026-09-20 with the 8 domains verified by SNI. The warning «detrás del gateway SSE todavía no funciona» ("SSE does not work behind the gateway yet") is removed fromLLM.md§std/serve,docs/SPEC.md§SSE andexamples/by-example/110-serve-sse.nx. On the proxy side: 8 tunnel cases + 1 over TLS, with the 11 ofpool_framingintact.
0.33.0 2026-09-20
This bumps MINOR because it rejects code that used to compile — code that compiled and blew up at run time, but the repo's rule does not distinguish: whatever the compiler stops accepting changes the minor.
Changed
-
Calling a
Fnvalue WITHOUT A SIGNATURE is now rejected at compile time when the context expects a struct by value or afloat(NYX1037). This is the third appearance of the same cause —aFnwithout a signature does not carry its return convention and the indirect call assumesi64—, and this time it is closed instead of patched: with a struct that was a SIGSEGV (thei64that arrives is the first field, and it gets dereferenced) and withfloat, silent garbage (0.16→281472821301216.0). It was reported by nyxerp from a middleware that compiled cleanly, passednyx checkandnyx vet, and died on the first request.What does NOT change:
int,String,bool,char,Result,Option, any pointer and the discarded result keep working through aFnwithout a signature — for those the assumed convention matches, measured with values. Nor doeslet f: Fn = some_fnfollowed byf()change: codegen refines that slot with the real signature since 2026-08-06. Why not the uniform convention with thunks, which was the spec's recommendation: it is surgery in the closure codegen —whatdom_on_fn, thebrowser_*_fnandResult.maprely on—, it costs one allocation per call, and it does not even close the residual case, because the problem of an unannotatedlet r = f()is not the ABI but the absence of a type. What made it unnecessary is a measurement: in the failing cases the compiler ALREADY knows the expected type. The split between layers is what each one can see:semanticlooks at parameters (nyx checksees them);codegenis the backstop and reads the type AFTER the refinement, so it catches theFnthat comes from a struct field —which blew up and not even W004 covered— and theNYX_SKIP_SEMANTIC=1path. Migration:nyx vetwarns with W004 since 0.32.4, so the dangerous form can be found before the compiler rejects it. The fix is to declare the signature:f: Fn(T) -> R.