Nexium roadmap

Where the language is, where it goes, and in what order. Dates are not promises; the order is. Each phase ends when its exit criteria hold, and every item lands with tests, docs, and a decision entry when it settles a design question. This file is updated when a phase closes.

The spec's one sentence is the compass: a language complete enough to build everything in, that is also the best thing to adopt for one piece of something else. The second half is ahead of the first, on purpose.

Now: 1.5.0, and 1.6 next#

The work in progress, dated and with its numbers, is in docs/progress.md; this file is the plan.

What exists and is verified on Windows, Linux, and macOS (1.1 to 1.5 each have a section under Past 1.0 saying what it added):

Numbers, at 1.5.0: 61.8k lines of Nexium (35.6k of them the compiler and its tools), 4.5k of C (the runtime and the GUI window layer), 579 std functions, 32 examples, 21 tutorial programs, 75 spec conformance cases and 101 compile-fail cases, 14 harness suites, verified by CI on five machines (the three desktops, Linux and Windows on ARM), for four more targets (32-bit x86, ARMv7, RISC-V 32, WebAssembly), and under the sanitizers.

Phase 1: a language you can write your tools in (0.3)#

Goal: someone replaces a Python or Rust command-line tool with Nexium and misses nothing for the tool's own needs.

Exit: examples/tool.nx, a real utility (a log grep with regex, dates, and file walking) under 300 lines, runs on all three platforms from the release. Status: done except the nx fmt trailing-comma rule (?T chaining, a?.b, landed in 1.1). std.fs, std.time, std.regex, std.text, std.testing, std.stream, range steps, while/else, full match exhaustiveness, nx test --filter/--verbose, os.environ, and examples/tool.nx (142 lines) shipped in 0.3.0.

Phase 2: a language that talks to the world (0.4)#

Goal: services and clients without leaving the language.

Exit: examples/server.nx, an HTTP server serving JSON from a directory, and examples/client.nx fetching and parsing a URL, both under test.

Phase 3: a language other people can build on (0.5)#

Goal: sharing code beyond a sibling file.

Exit: a second person's package is used by a third person's program.

Phase 4: the compiler in Nexium (0.6 to 1.0)#

0.6 opens with the syntax settled for 1.0 (decision 87): if c { }, while c { }, for x in items { }, if let v = opt { }, braces always, no semicolons. nx fmt migrates 0.5 sources. Nothing else about the surface syntax is planned to change before 1.0.

Goal: nx built by nx. Runs in parallel with phases 1 to 3; each stage is checked against the Rust compiler on identical inputs.

The rest of the climb, one release each (decision 90):

Exit: cargo is no longer needed to build nx from a release tarball (0.8), and no Rust is left in the repository (1.0). Both reached.

Phase 5: 1.0#

A 1.0 means the language stops changing under people's feet.

Every item is done: 1.0 is the release that deletes bootstrap/rust/.

Past 1.0#

What 1.0 is not yet#

The four things a careful reader will hold against 1.0, and where this roadmap answers each:

weaknesswhere it is answered
Memory safety is not guaranteed: ownership without a borrow checker still lets a view outlive its storage (two were found in std and the compiler under AddressSanitizer while the tutorial was written)1.2, the whole theme; until it lands the docs say "memory-safe by default" nowhere
Maturity: writing one tutorial found eight compiler bugs; a stranger will find morethe hardening commitments under Always below: the corpus, the fuzz budget, the sanitizer job, a patch within a week of a fix
No performance numbers: it compiles through C, which is not the same as a tablethe numbers page, pulled forward from 1.6 into 1.1
No ecosystem: sixteen std modules, no registry, no third-party packages, one maintainer1.4 (std), the registry and the second-maintainer items under Ecosystem; async is answered under Not planned, errors with payloads under 2.0 candidates

Where the four stand at 1.5.0: memory safety shipped in 1.2 (the view rules, errors since 1.3); the numbers page is up, measured every week and at every release, and the README's Speed section carries its table; maturity is the hardening under Always, which goes on; the ecosystem has twenty-nine std modules and four projects outside the tree, and still one maintainer and no registry.

After 1.0 the language changes only by addition, under the stability policy of phase 5. Each minor version has a theme; a bullet moves into a version when it has a decision entry and a test plan. The compass sentence still decides the order: what the compiler and the tools in self/ and std/ needed first, what other people's programs need next. Everything here was found by writing Nexium, not by reading other languages' feature lists.

Before anything: the open bugs, and 1.0.3#

KNOWN_ISSUES.md is this roadmap's page zero: every open bug lives there with a reproduction and the fix it needs, a fix removes the entry, adds a Fixed line and a regression test, and the next patch release ships it. Nothing below is started while a known bug that a user can hit sits there. (1.0.2 was the hotfix for the release binaries, which were built for the runner's CPU.) The four entries an outside review of 1.0.1 added shipped in 1.0.3:

  1. A contained panic leaked what the call acquired: the export wrapper's longjmp skipped every drop between the panic and the boundary. A per-call tracker in the runtime now releases live allocations, open files, sockets and held locks on the panic path; it is also the first half of 1.2's "a panic releases what it owned".
  2. nexium.lock was written and never read, so the lockfile pinned nothing: nx fetch honours it, nx update is what re-resolves.
  3. The Python wrapper took a list for a []mut T parameter and dropped the writes: mutable slices accept only writable, contiguous, matching buffers.
  4. Effect notes pointed at the function rather than the recorded witness; they point at the witness, and the compile-fail cases pin the line and column.

With them, the words the same review found wrong, also done: SECURITY.md said "memory-safe" in one sentence and listed safe-code use-after-free cases in the next; until 1.2 the claim is "deterministically memory-managed without a garbage collector, memory safety complete in 1.2", everywhere. The README says "language-stable, early ecosystem" near the top. And for discovery: "Nexium language" in every page title, and nexium-lang for the language's own packages on PyPI and npm when they come, since nexium is taken there by unrelated projects (decision 91).

First: the quick wins, in order#

Everything below is described in a theme further down; this is the same work sorted by how soon it can ship. None of it changes the language, so each lands as a 1.0.x patch or in 1.1 the day it is done, in roughly this order. The themes after this section keep their order; this is the queue in front of them.

Hours each.

  1. nx doctor compiles and runs a one-line program before it says "everything works": the checker, the C emitter, the C compiler, the linker and the executable all have to answer, and an illegal instruction from the probe is named as such with the fix. 1.0.1's doctor reported a working installation on a machine where the compiler could not compile a line, because it had only asked the C compiler for its version. The harness runs the check. (Shipped in 1.0.3.)
  2. The installer refuses to run twice (a setup mutex naming the one already open), closes a running nx before replacing files, and writes a log; the uninstaller the same. (Done, for the compiler's installer and the ones nx ship writes.)
  3. The one-C-file install on the front page of the docs, and install.sh falling back to cc bootstrap/nx.c when there is nothing to download. (Done; NEXIUM_FROM_SOURCE=1 asks for it.)
  4. The release workflow writes a .torrent with the assets as web seeds and puts the magnet link in the notes; a QR code beside it. (Done: one torrent per file, from 1.0.4 on.)
  5. A Homebrew tap, a Scoop bucket and the winget manifest, from the archives the release already builds. (Done: the repository is the tap and the bucket, scripts/packaging.py writes all three at each release; the winget submission to microsoft/winget-pkgs is a pull request by hand.)
  6. nx doctor says when a newer release exists. (Done.)
  7. The VS Code extension on Open VSX beside the Marketplace. (Done in the release workflow: both publishes run when VSCE_PAT and OVSX_PAT are set; the tokens and the namespaces are the maintainer's to create, see editors/vscode/README.md.)
  8. A packages.md: the packages people can nx add from git today. (Done, as a section of docs/packages.md: the honest list is the Topo's two example packages, and how to get a row.)
  9. An "Open REPL here" folder entry and a Windows Terminal profile from the installer. (Done.)
  10. ghcr.io/londopy/nexium: the compiler with Zig, on Alpine and Debian. (Done: docker/, published by the Docker workflow at each tag, for amd64 and arm64; CI builds and runs both on every push.)

A day or two each.

  1. install.ps1, the PowerShell one-liner, and the Chocolatey package that wraps it. (Done; the package wraps the installer with its checksum, as Chocolatey's moderation asks, and the push needs the maintainer's CHOCO_API_KEY.)
  2. REPL :undo, :save and :load; :effects expr. (Done; :load was there already.)
  3. nx -e and nx -p one-liners on the REPL's compile cache. (Done, on the interpreter: nothing is compiled, so there is no cache to keep.)
  4. nx layout Type: offsets, sizes, padding, the reordering. (Done: nx layout FILE [Type...].)
  5. expect_snapshot in std.testing. (Done, beside snapshot.)
  6. pip install nexium and npm install nexium, wheels and packages that carry the binary. (Done as nexium-lang, decision 91: scripts/pypi_npm.py builds them from the release archives, the release attaches them and uploads when PYPI_TOKEN and NPM_TOKEN are set.)
  7. The installer detects an installed version: upgrade, repair, remove, the previous choices as defaults. (Done in the compiler's installer; the ones nx ship writes keep Inno's own upgrade-in-place.)
  8. nx upgrade, and nxup behind it later. (nx upgrade done: the release for the machine, verified, over the running executable; a separate nxup when one is needed.)
  9. Portable mode, and nx install from a portable copy. (Done: a portable marker keeps Zig's cache beside nx, nx install [DIR] copies the folder into the user's place and onto the PATH.)
  10. The effects lockfile (nx audit --lock) and the CI check. (Done: file.effects.lock, --check in CI on the shipped example.)
  11. Panic proofs as code lenses in the language server. (Done.)
  12. Colour as you type and completion in the REPL, from the server. (Done: a line editor on raw terminal input, the server's completion at the cursor, history; NX_PLAIN=1 for the plain reader.)

About a week each.

  1. nx explain f effect: the provenance tree. (Done.)
  2. The numbers page: bench/ in four languages, on a fixed runner, published. (Done: bench/ in Nexium, C, Rust, Go and Python, the Bench workflow on a GitHub runner weekly and at tags, the medians on docs/numbers.md, a regression a failing job.)
  3. nx test --watch and nx run --watch (file watching in the runtime). (Done, polling the program's files twice a second; a watcher in the runtime can replace the poll when one is wanted.)
  4. The ownership trace, nx run --trace own and the REPL's :own.
  5. The binary-pattern debugger, nx bin.
  6. :show and :plot in the REPL: the inspector and chart windows on nexium-gui.
  7. The installer's editor page, and the wizard in six languages.
  8. Authenticode signing and macOS notarization in the release workflow (the certificates are the slow part).

Everything longer, in the order of the themes: memory safety (1.2), incremental builds and the semantic language server (1.3), std.tui and nx topo (1.4), WebAssembly and the 32-bit targets (1.5), hot reload, profiling by effect, the visual tools (1.6), the seam (1.7), nexium-gui grown up and the Hut (1.8).

1.1: the language the compiler wanted#

Ergonomics the self-hosted compiler paid for by hand.

Exit: the compile-time interpreter and check.nx lose their hand-written copies and index workarounds; nx audit self/check.nx reports fewer panics than in 1.0.

Status: shipped as 1.1.0 on 2026-09-21, every item. check.nx lost cv_clone and frames_clone to derive(Clone), and the share of its functions carrying panics fell from 549 of 589 (1.0.3) to 551 of 611 (the new proofs against the new code); the rest index a list by a computed position, which no guard proves.

1.2: memory safety without a garbage collector#

Status: shipped as 1.2.0 on 2026-09-21. V1 to V5 are in the checker as warnings (decisions 100 to 104), --strict makes them errors, @escape(v) is how a value leaves an arena block, every unsafe block in self/ and std/ carries its reason, and the fuzzer runs mutants in debug mode. Left for 1.3: the warnings become errors, and nx fix inserts .clone() where that is the fix. (Done in 1.3: the rules are errors, and nx fix makes V4's .clone() and V5's @escape, each kept only when a check of the edited program agrees, decision 111.) The rules found three bugs in the compiler on their first run (changelog, Fixed).

The promise of section 12, no undefined behaviour in safe code, has four holes that 5.6 and 5.7 hand to the programmer: a view stored past the storage it points into, a view kept across a growth of its container, a view into a value that is then moved or dropped, and a value that leaves its arena. 1.2 closes them, so that ordinary safe Nexium code cannot create a dangling view and unsafe marks everything the compiler cannot prove: memory-safe by default, with explicit escape hatches, the guarantee Rust gives, kept in Nexium's simpler shape.

Not a garbage collector. A GC would simplify the language and weaken several of its defining promises at once: predictable destruction, visible allocation behaviour, !allocates, embedding in C, Python, Rust and Node processes, no substantial runtime. And not a borrow checker with lifetime annotations either: the language keeps ownership without one. What it keeps, and what it adds:

Rolling it out under the stability policy: undefined behaviour was never promised, so a program that had it may become an error in a minor (docs/stability.md). Each rule arrives as a warning in one release and an error in the next, so a codebase gets a release to run nx fix.

Exit: sections 5.6 and 5.7 list no case that is the programmer's responsibility; SECURITY.md extends its promise to views; every rule has a compile-fail case and a spec case; the fuzzer gains a hunter that compiles mutants in debug mode (freed storage filled with 0xDD) and runs them, so a dangling view that slips through is a finding rather than luck.

1.3: the toolchain grown up#

Status: shipped as 1.3.0 on 2026-09-24. Every item below is in; three are narrower than written, and their notes say how: nx fix makes only the edits that keep what a program does (decision 113), std's platform code leaves the runtime's #ifdefs with 1.6 (moved on from 1.5), and the rebuild of "well under a second" everywhere waits for checking by module.

Exit: a one-line change rebuilds in well under a second on the compiler's own sources, and the language server answers from the checker.

1.4: a standard library people stop supplementing#

Status: shipped as 1.4.0 on 2026-09-25. Every item below is in; two are narrower than written, and their notes say how: the scoped threads are calls, thread.each and thread.both, where a block that joins when it ends would need a destructor the language does not have, and text.chars keeps its u32s as 1.x promises, with text.scalars giving the chars beside it. The exit criterion holds: examples/tool.nx, service.nx and the compiler use the library where they wrote their own (the compiler checks its downloads with std.hash and reads GitHub's answers with std.json, the tool counts in a Map).

Order, with the collections, std.hash, random.secure, the small modules (std.path, std.env, std.uuid, std.log, std.csv, std.toml, std.base64), Map on SipHash, std.time (and with it the known issue of import std.time hiding time.now() fixed), the HTTP client with the TLS slot and nxtls in it, std.websocket, the discord package, std.process, std.thread, std.text, std.testing and the platform's TLS all in.

Exit: examples/tool.nx, service.nx and the self-hosted compiler import nothing they had to write themselves.

1.5: platforms#

Status: shipped as 1.5.0 on 2026-10-04, and the exit criterion holds: one source runs in a browser, on a Raspberry Pi (ARMv7 and ARM64) and on the three desktops, and docs/platforms.md says which combinations CI proves. In: WebAssembly programs (wasm32-wasi), which run every spec case, example, standard library test and Topo program under Node.js in CI, a panic unwinding as it does natively (decision 123); artifact wasm, a library as a module with a JavaScript loader, which CI loads into a page in headless Chrome (decision 124); the 32-bit targets (decision 122), the same suites run for i686 on CI's machines and for armv7 and riscv32 under QEMU, the compiler building itself as a 32-bit program, and every release carrying nx for 32-bit Windows and Linux and ARMv7 Linux (tier 2); Linux and Windows on ARM at tier 1, the test job running on GitHub's arm64 machines. The rest moved to the themes it belongs with: the runtime's platform code into std, the runtime split, the embedded targets and wasm32-freestanding, static 64-bit Linux builds and FreeBSD to 1.6; the cross-compilation matrix of nx ship to 1.7; nexium-gui's X11, Wayland, Cocoa and canvas backends, and the playground's C backend and share URL, to 1.8.

Exit: a Nexium program runs in the browser, on a Raspberry Pi and on the three desktops from one source, and the docs say which combinations CI proves.

1.6: the runtime that release builds deserve#

Status: under way on main. The first measurement, with the same C compiler on both sides, found three of the four benchmarks (fib, nbody, sieve) already at C's speed in fast mode; the gap is words, maps and strings, which has gone from 3.0 times C to 1.6 on Windows and 1.6 to 1.8 on Linux, where the figure moves with the run (get_or_put, integers formatted in 64 bits, a string's first block of 16 bytes, small freed blocks kept for reuse). Safe mode's cost is the overflow checks on calls, 1.5 times C on fib. The fifth benchmark, trees, binary trees of ref class nodes added for the ARC elision item, runs at 1.3 times C in both modes on Windows, Rust at 1.1 and Go at 0.6 with its collector.

Exit: every example in release mode is within a documented factor of its C counterpart, and the factor does not grow between releases.

Platforms, continued (from 1.5).

1.7: the seam, both ways#

Today a Nexium library leaves the tree through nx ship: a C header and archive, a Python wheel over ctypes, a Rust crate with safe wrappers, an npm package. What crosses is what C carries: integers, floats, slices, layout(c) structs, a status code with an error name. Everything else stays home, and the traffic runs one way. This theme widens the seam in both directions, with the same rule as 1.0's spec section 15: a panic never crosses, no initialization call, no process-global state, and a symbol means the same thing from every language.

Python, deeper.

Rust, deeper.

More languages out. Each is an artifact kind with a wrapper in that language's idiom over the same C ABI, a test in CI that calls ropesim from it, and a chapter in the embedding guide:

The ABI itself.

Exit: ropesim shipped to C++, Go, Java, C#, Ruby, Lua and the browser and called from each in CI; the Python wheel takes a NumPy array without a copy and raises ropesim.InvalidInput; the Rust crate's error enums come from the Nexium error sets; an example calls a Rust crate and a Python library from Nexium; --abi-check fails on a removed export.

Errors you can fix alone#

An intermediate programmer should be able to understand and fix any problem in their own Nexium code without ever reading the compiler or standard library source. Every diagnostic, including warnings and runtime panics, gets a permanent NX code and a page, and nx explain fills that page in with the programmer's own code instead of a generic example. Messages point at the actual mistake, not just where it was caught, and only suggest fixes the compiler has checked will compile. The standard library documents what every function returns, how it fails, and whether it allocates, and a written reference covers the exact rules of the language. None of this relies on discipline. The compiler can't emit an uncoded diagnostic, every documented example is tested in CI, and every feature's errors are written before the feature is built, so a feature that can't produce a clear error gets simplified. We prove it by breaking known-good programs to confirm that one mistake gives one clear error in the right place, and by timing intermediate programmers as they fix each error with nothing but the message and the page.

Where it starts: a message today names the problem and the line where it was caught, with no code and nothing to open. Assigning to a let says "count is immutable; declare it with var" at the assignment and never shows the declaration, which is the line to change; a var that never changes and a binding never used are not warned about at all. What there is to build on: nx fix keeps an edit only when a check of the edited program agrees (decision 113), nx explain already answers why a function has an effect, the compile-fail cases pin each diagnostic with // EXPECT: lines, the fuzzer compiles mutants of real programs, and SPEC.md is the written reference. Pencilled from 1.6, with every diagnostic coded by 2.0:

Tools only this language can have#

The REPL was the first of these: a feature no systems language is expected to have, cheap because the compiler was already there. Each item below is the same shape, built on a fact the compiler already knows (every function's effects, every move and drop, every binary pattern's shape) and offered as a tool. Each is pencilled into the minor it fits; the cheap ones may land earlier as patches to the tools, since none changes the language.

Answers from the effect system.

Answers from ownership and layout.

Binary patterns.

Interactive, in the family of the REPL.

The clickable line: windows, wizards and the REPL#

The installer wizard, the REPL and the GUI library are the parts of the project people touch rather than read. This line grows them together: each item is something to click, type into or look at, and each stands on what exists (the REPL recompiles a session; nexium-gui draws in a window or headless; the installer is a wizard already). Pencilled into minors the same way as the tools above.

The REPL you can look into (1.1, 1.3).

Run from the editor, everywhere (1.3). Ctrl+B runs the file, the variant tests it, and a diagnostic is a click away, in every editor the repository supports, the same way:

The first five minutes (1.3, 1.8).

The installer grown up (1.3). The wizard is the first thing a Windows user sees of the language, so it should behave like software people pay for, in every situation it can find itself in:

Every road in (1.3 to 1.6). Today nx arrives by the Windows setup, the install script, the portable archives, source, and the GitHub Action. People install software the way their platform taught them, so each road below is a manifest or a package built by the release workflow from the same archives, in the order of how many people stand on it. The community usually maintains the distribution-owned ones (AUR, nixpkgs, Homebrew core); the project's job is a release that makes that easy: stable URLs, checksums, and no post-install step.

The niche roads (1.3 onward). The ones people remember:

nx topo: the tutorial you can run (1.4; the runner shipped in 1.2.1).

Status: nx topo is in the compiler (self/topo.nx): the route map with progress, nx topo <n> writing a chapter's exercises to work on, check grading them with the compiler, hint, solution, quiz, reset; progress in topo-progress beside the update cache. The site shows the same exercises at the end of each chapter with the solutions folded away, a "done" box per exercise kept in the browser, and the count on the chapter list. Forty-one exercises of five kinds cover chapters 2 to 23 (decision 107), eleven quizzes grade their questions against the compiler where a question asks it (decision 109, nx topo verify), and the exercises run and grade in the page (decision 108). Left: the more chapters listed below, and the playground's share URL (1.8).

The Topo as a course (1.4 for the content and the page, 1.8 for the share URL). The chapters teach by showing; a course asks the reader to do. Every exercise is a file in the repository the harness runs, so the course cannot rot:

The playground, light: the compiler in the page without the C backend (next, before the rest of the course; the full wasm target stays in 1.5).

Status: done (decision 108). nx play, the NX_WASM runtime and site/play.js are in; every fill and fix exercise of chapters 2 to 15 grades in the page, and every other code block with a main runs. The exit check runs every exercise through the WebAssembly build and the page's own WASI layer in Node (CI's playground job and the Pages workflow) rather than a headless browser; the harness's play suite holds the interpreter to the compiled output of every recorded program.

The site is static files on GitHub Pages, so running code there means the compiler itself runs in the browser. Two things make that possible now rather than in 1.5: the compiler is one C file with no dependencies (bootstrap/nx.c), which zig cc -target wasm32-wasi compiles to WebAssembly, and the REPL already runs programs without the C backend, through the compile-time interpreter, println included. So a wasm build of nx can check a program (the real diagnostics, for the fix-the-error exercises) and run it through the interpreter, capturing the output to compare with the recorded one: the whole grading loop, client side, no server. The pieces:

1.5 added the real target, --target wasm32-wasi for programs; the C backend in the browser through a wasm C compiler, and the share URL, moved to 1.8's apps.

The docs, more (from 1.3).

std.tui (1.4).

Visual tools (1.6).

nexium-gui grows up (1.8, Gangapurna).

nexium-web: a web front end, native (1.8). Today a Nexium program gets a web front end two ways. One is a Tauri app whose commands are thin wrappers over artifact rustlib exports: the export seam is stateless on purpose (no mutable globals behind an export, and no objects or slices handed back across it yet), so rich data crosses as JSON in a caller's []mut u8 and the state lives in Rust, or the program runs as a Tauri sidecar speaking JSON lines. The other is a localhost server on std.http's router with the page embedded by @embedFile, which owns no window and needs an Origin check and a per-launch token, since any page can reach localhost. This is the third way, Tauri's idea done in Nexium:

nexium var app = nexium_web.App.open(@cstr("notes"), 900, 600).? app.set_html(@embedFile("ui/index.html")) var notes = List(String).new() while app.pump() { // like begin_frame(): false once closed for call in app.calls() { // each JS invoke() since the last pump match call.name[..] { "add" => { notes.append(String.from(call.arg(0))); call.ok("null") }, "count" => call.ok(format("{}", .{notes.len})), _ => call.fail("unknown command"), } } }

Apps (1.8).

2.0 candidates: questions the spec review should settle#

Additions large enough to deserve a spec version of their own. Each is a decision entry first, an implementation second, and none is promised.

Ecosystem, in parallel with all of the above#

Always#

Not planned#