.nim / .aowl → aowlparse → aowlsem* → aowlhexer* → { aowlc → C · aowljs → JS · aowli → interpret }
A rewrite of the entire Nimony ecosystem
- parser, semantic checker, lowering
- interpreter and runtime
- standard library
- LSP, MCP, vscode, claude plugin
- code fix suggestions, formatter, obfuscator
- and much, much, more...
translates to:
- C
- faithful and native JavaScript & TypeScript
- Python
— all self-hosted, written in itself
Between the frontend stages we use AIF, which is NIF, byte for byte, so any part you find here is compatabile with Nim or Nimony.
The big projects are private for now, but the docs are public and anything private is yours if you just ask — message me on Discord (timbuktu_guy) and I'll add you, no hoops. The playground moves onto the new sem + hexing shortly.
| Project | Docs |
|---|---|
aowl toolchain — aowlparse · aowlsem · aowlhexer · aowlc · aowljs · aowli |
AIF ≡ NIF |
aowlup — rustup for the stack: installs / versions / selects the toolchain (variants · profiles) |
repo ↗ |
aowlabi — the stack's shared value-representation / ABI: one canonical per-type layout + marshal matrix, read by aowlc · aowljs · aowli instead of each keeping its own copy |
docs · repo ↗ |
aowlcode — Claude Code plugin + MCP server (trace/debug, /land, cheap-applier fan-out) |
docs |
| aowllsp — Language Server + VSCode extension | docs |
aowli-release — public, binary-only distribution of aowli (the aowli source is private); runs a nimony program's typed NIF; prebuilt aowli-interp + aowli-dbg, GitHub Release v0.3.1, hardened (obfnif IR + licence gate + stripped), SHA256 + VirusTotal per binary |
docs |
net stack — tcp·net·tls·http·compress·serve·ws·requests — TLS 1.3, HTTP/1.1 · 2 · 3, QUIC + WebTransport, Autobahn WebSocket, single-thread async reactor |
docs |
| web / html / css — typed HTML5 + MDN CSS engine + DSL | docs |
| aowljs / aowlts / aowlpy / aowlhl — idiomatic JS/TS/PY backends + shared HL-IR | ts · py · hl |
· and more, here
aowlsem — 29 commits. Compile-time evaluation beyond const initialisers, then bounded by a capability policy. Four sites treated "cannot compute" as a definite answer; three miscompiled silently.
when big():took the wrong branch, no diagnostic —evalCondreturns unknown, an unknownelifis not taken,elseis unconditional. Three sites:semWhenmodule-level,semWhenin-proc,whenTakenBody(type bodies — wrong field, so a wrong type). All now run the condition.- Enum explicit value kept the auto-increment ordinal unless a bare
IntLit:b = v()→ 2, nimony 7.foldRawArrayDimhandles1 + 4;c = K * 10needsceEvalInt(force = true)— the contains-a-call guard assumes cheaper folding, false here because the prescan precedesconstVals. - A
constinside a proc forked aowlsem until killed.ceDeclaresNamematched only top-level(const …), so the copy never stopped, the enclosing proc came in whole still holding it, each child regenerated the evaluator;ceBudgetis per-process. RecursiveceDeclaresNamerestored the stop,--ceDepth:(max 4) backstops. array[sz(), int]emitted(array (i 64) (call sz)).ceTypeNeedsEvalkeeps avar/letwhose own type awaits evaluation out of its evaluator —varTis inceIsDecl, so it re-entered.- Const evaluator had one executor, macro plugins two;
--macros:interp|compilednow drives both. Compiled path exposed:std/writenifimport only in.p.deps.nif(aowlsem mreads deps,nimony sthe body) →undeclared identifier: 'setup';-o:<nimcache>/<base>collided with nimony's per-module directory. - Aggregate consts fold:
array[N,int]/array[N,float]unrolled,seq[int]looped tolen, all-intobjects by field name. Value bound to aletso the initialiser runs once; caller rebuilds(aconstr …)/(oconstr …). Wrong count fails rather than folding a partial. - Inferred
consttype named the bare generic:const S = firstN(3)→(at seq (i 64)), the same call underlet→seq.0.I·..semConstlackedsemLetVar's resolution; the seq materialisation keys off it, so folding was off too. writeNifInt(<a seq>)semchecked clean — nimony:expected: int64 but got: seq.reliabledeclines any pair with a collection either side; mirror ofcontainerParamadded.- Earlier: a copied instance resolves an
ochoicecallee; an assignment spells its ref upcast; a range is iterable whatever its bounds resolved to. - Compile-time code runs under aowli's policy.
mpRunandceRunInterppass--allow: --allow-path:<nifcache>=fs.read,fs.write,fs.meta— a plugin must read its input NIF and write its answer, and gets nothing else.--ctfe-allow:PATHgrants one file; exit 77 becomesSTOPPED by the compile-time policy;--ctfe-policy:offis the escape hatch. - A granted read is recorded, not just permitted:
<out>.s.nif.ctfe-readscarriesread<TAB>path<TAB>hash<TAB>size, andaowlsem ctfe-check <out.s.nif>exits 0 current / 1 stale (naming the changed file) / 2 no record — an exit code, so nifmake, aowltest and aowlmony need not parse the format. --lens:<out.lens.nif>publishes what the checker resolved.(d …)declarations with position, type and signature;(u …)occurrences naming the symbol they bound;(t …)object/enum member tables with inheritance depth and visibility. Plain NIF.- Positions are recorded during checking, not read back. The
.s.nifcarries one only where a subtree was copied verbatim, so every minted symbol has none;tests/diff.shcanonicalises line info away, so nothing measured that gap. Seams:define,lensAt,lensUse. (u …)carriesownerandrole,(d …)carriesrecv, so call edges and UFCS candidates are index filters, not tree walks.ownerneeded a routine stacksemProcnow pushes;recvcame fromSym.paramTypes.
Gates. diff 685/685 → 701/701, check 400/400 → 401/401. New tests/consteval.sh 18/18 in all.sh: both executors match the oracle and each other, and each actually evaluated — proved by the serialized value file, since the shape folds otherwise make a no-eval run look green. nofp 35/35, diag 175/175, explain 93/93, e2e 6/6. New tests/ctfe.sh 7/7: an ungranted read traps and folds nothing, the same read granted folds, the hash moves when the file does, ctfe-check reads stale on the outdated compile and current on the fresh one, and the digest aowli recorded equals what aowltest --ctfe-hash computes — three copies of FNV-1a/64 with nothing else asserting they agree.
Cost. In-proc when nests to --ceDepth: 1 condition 20s, 4 → 32s, no per-condition branching (ctfe_when_call_multi.nim). whenTakenBody is in the prescan over every module; plain corpus file 1.78s → 1.82s — when defined(…)/x is T fold in evalCond.
Standing. scanFeatures has the same shape but only gates feature scanning. Upstream rejects static:, of v():, generic-routine const calls. selectedWhenBody can't call the evaluator (non-var context) but no longer disagrees with semWhen. New tests/lens.sh 16/16; corpus 705/705 on the uncolored-async branch with the index build.
aowllens — 2 commits. Reads aowlsem's index instead of reconstructing it from the tree.
typeatcould not answer on an aowlsem artifact — it reads positions off emitted tokens, which aowlsem mints without them.lens.nimparses the sidecar; the occurrence at a position already names its symbol, so shadowing is the checker's answer, not a guess.memberstakes fields from the index (inheritance already flattened, per-member visibility) and UFCS candidates byrecv;callsreads edges fromcall-role records paired withowner, carrying the call site.- Fallback is per-answer, not per-run: no sidecar, or nothing in it for this question, and the existing walk answers exactly as before.
Gates. New tests/lens.sh 5/5, including the negative control — hide the sidecar and 7:29 goes back to {}. difftest.sh 5/5 and newcmds.sh 15/15 unchanged.
Standing. Nothing writes the sidecar in a normal build: nimony's driver invokes nimsem, aowllsp drives nimony check. Two implementations now answer the same questions with no differential between them.
aowlmony — 3 commits. verify diffs native against interpreted off one front end. Its first two findings were both artefacts of which binary ran, not backend defects.
aowlmony verifyadded. On a mismatch it re-runs the interpreted leg underaowli --trace, rebuilds stdout from thewrite(stdout, …)args, and names the op owning the first divergent byte. Default--native:nimonyreuses the binarynimony calready linked at<nimcache>/<mainHash>/<srcStem>.- Reported
s[2..5]→"a"as an aowli defect. It was a stale install. The registry resolves interp to~/.aowl/bin/aowli-interp(07-26), shadowing a fixed~/aowli/bin(08-02); same.s.aif, different answer. Every verdict now names both realizers with build dates, andnewerBuildThan()prints the newer build plus theAOWLMONY_NIFI=re-run. - Exit 1 meant both "backends disagree" and "compile failed", so a shared
nimcache_staticlink race read as a divergence.COMPILE_FAIL_CODE=2puts it with the could-not-run cases; 1 is now that one claim. locateOpwalked ancestor frames only, so a top-levelecho—write(stdout, …)recorded at system's line, no user frame above it — had no location. Falls back to the last op run at a line in the entry module, a preceding sibling.
Gates. npm test 25/25 → 41/41, twice clean. The slice case asserted a stale-binary artefact and is gone; the --native:aowlc case asserts the invariant (never exit 1) since aowlc gained multi-module linking mid-session.
Standing. Genuine: 7 div 0 returned 0 and exit 0, fixed in aowli, which now raises division by zero. Native SIGFPEs and loses buffered stdout, so it stays an expected divergence, not a match.
aowltest — new repo, 3 commits. Test results keyed by transitive input hash; an unchanged closure is never re-run, and a compile-time read counts as an input.
-
Key is
sha1of a sorted manifest:dep <path> <sha1>per transitive local import,ext <name>for unresolved stdlib specs,gdepfor--depglobals, the command line,--salt. Entry present ⇒ skip. -
Content-hashed, never mtime. Restoring bytes restores the key, so a branch switch re-hits.
--explaindiffs a miss againstlast/<sha1(testpath)>and names the input that moved. -
isFileas "tryopen" called every directory a file — glibcfopen()succeeds on directories, so the test root read as one test; 19 of 34 assertions failed at once.std/private/oscommons.fileExistsstats;std/filesresolves to Nim 2's lib, not nimony's. -
Import scan is lexical —
std/[a,b],from … import, block and comma continuation,include; nowhenevaluation, so it over-approximates: costs a re-run, never a wrong skip. -
A compile-time read is an input no static scan can find.
--ctfe-dir:DIRmerges the*.ctfe-readsaowlsem wrote intodisc/<key>after a run; a later hit on the same static key re-hashes each one first, so a moved schema is a miss with identical source bytes. Off unless asked — a wrong guess at the sidecar location would silently skip a changed test.
Gates. tests/run.sh 35/35 → 41/41 over the cache decision itself: editing lib/base.nim re-runs its two dependents and leaves the third cached at 33.3%; restoring the bytes returns 100%. The six CTFE cases carry the control that matters — without --ctfe-dir the same moved schema is invisible and the run hits, so the re-run is attributable to the record.
aowlrepl — new repo, 4 commits. A nimony REPL on aowli. State persists because the session is one module, re-run from the top on every cell.
- Imports hoisted, everything else in entry order, then
nimony c --nimcache:<dir>andaowli-interpon the main.s.nif— stable across cells because its name hashes the module path. Cold 2.15s, warm 0.19s, run 1ms; only the stdout suffix the previous run did not produce is printed. - Completion reads the session's own typed NIF, which the REPL just compiled:
aowllens declsper successful compile,aowllens members <recv>memoised — the two queries backing aowllsp.xs.lnarrows tolenbecause the NIF saysxsis aseq. Raw-mode editor overtcsetattr(std/terminalstops atisatty), ghost text, menu, history. - Highlighting and cell-completeness both ran on a hand-rolled scanner; both now use aowlparser
tokenize, soecho "a:b",echo '('andecho 1'u8are complete and an unterminated literal is not.aowlparser checkcannot answer completeness —[]fortype,if x > 1:,proc f(): int =, andexpression-expectedsits in the driver'scollectDiags(aowlparser.nim:1188), not the library. Filed. - Three silent-wrong-answer defects.
compilecounted a non-zero nimony exit with unparseable output as success, so the REPL ran the previous session's NIF;snifIsFreshnow refuses a NIF older than the module just written.:reset-cachenever worked —std/dirs.removeDirisrmdir(2), ENOTEMPTY on a populated nimcache — which wedged a session once that guard fired.
Gates. New tests/run.sh 8/8: 5 golden transcripts, 22 reader verdicts (--analyze), the candidate set (--complete), and the stale-.s.nif guard put back with NIMONY=/bin/true.
Standing. ~/nimony/nimcache_static is shared by every nimony on the machine whatever --nimcache: says, so any concurrent build, test run or LSP deletes static.o mid-link; those processes take no lock, so compile and build.sh take the lock and retry on the signature. Installing to ~/.aowl/bin leaves nothing on PATH — the build now symlinks into ~/.local/bin.
aowli — 11 commits. Interpreted code is now replaceable and compilable while the process runs, and bounded by a capability policy.
-
Hot module swap.
tryLoadSymanswers fromprog.membefore touching a file, soswapHotre-reads the.s.nifandpublishes each decl over the same SymIds. ClearscallCacheand the for/if/case layout caches — those key on a buffer address, which a reallocated buffer reuses. -
Module-level
vars are not re-run, so state survives the code change; a global added by the new version is never initialised. Demoed on a live aowlserve io_uring handler: same pid, same socket,hitskeeps counting. -
Mid-run JIT via aowlc.
hybridgenrunsnimony c --app:lib— seconds, so startup-only.aowlcjit.nimemits the same uniform shim ABI from the.c.nifplus onegcc -shared -fPIC;--jit:Ncompiles on the first crossing. Scalar tier, own-module procs; everything else declines to interpret. -
7 div 0returned 0, exit 0 — the divisor reachedxint, whosedivanswers NaN, whichmasknarrowed to an ordinary 0.isDivByZeroraises in both engines, integer only.build.shnow verifies the artifact, not the exit status: a "clean-cache rebuild SUCCEEDED" had left no binary. -
A native is the only door out of the value model, so
nativeCallis a complete capability boundary.iopolicy.nimgates it on an int bitmask (fs.read/fs.write/fs.meta/process/env) plus--allow-path:PREFIX=CAPS; a denial halts throughdoQuit, so it is not catchable and never returns a substitute value.--audit-reads:FILErecords each granted read as path + FNV-1a/64 + size, written by the driver.policyOn()is false untilrestrictTo, so an unrestricted run is unchanged. -
The
hostOpenbackstop demanded fs.read AND fs.write, vetoing awriteFileunder a write-only grant thatnativeCallhad already allowed.capsDeniedOn(need, path)is now the single decision the gate and the hostfd/hostdir backstops share, and the backstop asks for either — its job is catching a syscall that bypassed the gate, not re-deciding it.
Gates. tests/run.sh all 449/449 with the raise in. New demo/hotswap/test.sh 9/9 and demo/hotjit/test.sh 6/6; both carry a negative control (--frozen, --jit off) because the same-answer assertion passes even when nothing happened, and hotjit also asserts hybridNativeCalls > 0. Collatz over 30k inputs: interpreted 3.467s, JIT 0.336s including the mid-run compile, native 0.006s, byte-identical. New tests/policy.sh 11/11, every grant paired with its denial control.
aowlhost — new repo, 4 commits. Runs an aowl module as a plugin under a capability policy. Default grant is nothing.
- Embeds aowli as a library rather than shelling out: parses the plugin
.s.nif, replays imported modules in dependency order, installs the policy before any plugin code runs, and owns its stdout/stderr and exit code. A denied call exits 77. --allow-path:/etc/hostname=fs.readreads that file while a sibling under the same policy is denied and named.plugins/snoop.nimwraps itsreadFilein try/except and the except arm never runs — the halt is below the language.
Gates. New tests/run.sh 9/9; every denial paired with its granted control, since a trap alone cannot be told from a read that never worked. The write case checks the filesystem afterwards: the absent file is what proves the syscall was never issued.
aowlc — 3 commits. build/run emitted one translation unit; nothing that touched stdout linked.
unknown type name 'LongString_0_<system>'on any program callingecho.compileModulestubs missing externs, which covers a function and cannot cover a type.build/runnow route throughcompileProgramover the sibling.c.niffiles, entry module last;--singleopts out.exec --entrywas unaffected, so it read as a codegen bug.--emit-onlywrites the linked C without runningcc— what aowli's JIT consumes before appending shims.test/driver.shcoversbuild+exec;test/single.shcompiles one TU alone, separating a codegen failure from a missing link step.
web — 6 commits. component gives a tree typed parameters; one lowering engine now backs both surfaces.
h1 titlerendered<h1><title></title></h1>. A bare ident naming an HTML tag was read as an element, andtitle/label/footer/data/form/summary/time/codeare tags and ordinary parameter names. Onlycall/cmdforms are elements now.web:andcomponent:sharedeps/weblowerinstead of two lowerings that drift;web:gainedfor/if/whileand runtime children. A child's meaning comes from its type — overloadedwebAppend(string|HTMLNode|HTML)— and a call is an element iffhtml's registry knows its name.[placeholder]:lowered to:placeholder— one colon on a pseudo-element selects nothing, so the rule silently never applied.::for placeholder/before/after/selection/marker/backdrop.[hover]:/[disabled]:get their own class, and the suffix joins the hash so a state and a base block with equal declarations stop colliding.@stylewith no enclosing element emitted nothing and the page rendered unstyled; it now fails asdirectiveNeedsAnEnclosingElement.document()adds doctype/charset/viewport/embedded CSS, escapes the title, and emits<as\3cso a declaration cannot close<style>early.
Gates. New tests/run.sh 6/6 (tweb, tcomponent, tsheet, tescape, tdocument, tstates). tescape asserts a component input escaped in both positions it lands in — text child and attribute value — with rawNode the only opt-out.
css — 2 commits. Styling by value, not by property soup.
Styleis an ordered set of validated declarations merged right-wins by&, so a theme is a proc returning a value andtheme & declare("color","red")keeps every property it does not name.Stylesheetmaps selector →Style: declaring.btntwice merges rather than shadows,sheet[".btn"]returns aStylereusable per element, selectors go throughvalidateSelector, anduseStylesheetinstalls one process-wide.
web-state — 2 commits, plus 1 in js. Fine-grained DOM binding; the blocker was structural, not scheduling.
- An effect could not know which node to update.
JsProc0/JsProc1are{.nimcall.}and carry no captured environment, so an effect could only touch globals.toJsWith(p, ctx)—_fnToJsCtx, context captured by value — backseffectWith, thenweb_state/dom'sbindText. tests/tdomcounts runs per binding, not output: writingcountleaves the name binding at 1 run and a no-op write re-runs neither, so a whole-tree re-render fails the test instead of passing it.run.shhid its failures — every compile error printed asno .c.nif, and a missing.outputwas a silent skip. Both now report what happened.
Gates. 3/3 (tauto, tdom, treactive).
aowlui — 3 commits. The lab's 3055-line stylesheet and its page, as values, both gated.
tools/ingestclassifies 429 selectors → 195 components + 64 raw. A selector the model does not explain becomesstRawverbatim, so the raw count measures the model instead of hiding the gap.- Trimming collapsed
.a .band.a.b, ingesting every.owner .partas a modifier class — declaration counts matched exactly, 1739 = 1739, while the design broke.troundcompares the declaration multiset both ways: 0 missing, 0 invented. .aowl→.nimand the pack compiles. 144 errors were one: imports still namedaoughwl/web, so everyweb:block was semchecked as ordinary code. The genuinely missingfield,name = value,webClass,webSlotlanded inweb.shell.nimportsindex.html;tshellreads the kernel script order out of the reference at test time rather than transcribing it, so the gate cannot drift from what it checks.
Gates. 3/3 (tround, tshell, tpage).
Standing. serve does not build here: ~/nimony combined-prs keeps the posix modules under src/lib/posix/ where the transport deps expect src/lib/, and forcing that path pulls a second stdlib (type mismatch: got string but wanted string). Reproduced on an untouched reactor_http.nim, so examples/web_page.nim is committed unverified.
aowli — 18 commits. Every defect was a silent wrong answer: plausible output, exit 0, empty stderr.
- Nested
returnlost its value to the enclosing case expression (tree-walker only). Outerretoverwrote the result. (expr STMT… VALUE)discardedflReturn/flRaise/flBreakand continued to the trailing value.- Cell-backed pointers had no address (
cast[uint](addr s[i])→ 0). Stable synthetic addresses added. - No conversion op for
cast[ptr T]with non-scalar pointee; integer stayed integer and read as 0. - Loud-halt not byte-identical across engines; post-
quitstdout now dropped at shared sink. - Hybrid boundary failed open: aggregates containing pointers crossed by value and segfaulted. Fixed in aowlabi.
build.shalways returned 0; now exits non-zero, keeps linker lines, retries once on intermittent errors.- Debugger: unmatched breakpoints report nearest executed lines; DAG reported as
<cycle>; tuples unreachable by expand. advance()/complete()returned nothing — neither engine could call an imported global as callee. Both now fall back to lazy imported-global slot.- VM compiled
completeas unconditionalopCallNative; now gated onvkCont. setSchedulerwrote only engine state, never system’s global. Both engines now update both views.--break-funcfired once per statement; now once per invocation.readFilefailed on correct paths because aowli launched from temp nimcache.--cwd/AOWLI_CWDtried after cwd-relative open fails.
Gates. run.sh defaulted to 6 of ~45 categories. Now takes all. Real figures: 414/414 → later 434/434 all categories, three-way crosscheck 0 divergences, hybrid 6/6. New lanes: semantics.sh, dbg.sh 15/15, async.sh 9/9. Crosscheck/run defaulted to absolute master paths; now $NIFI_ROOT/bin. On complete-integration: 457 AGREE-PASS / 19 AGREE-FAIL / 0 DIVERGE.
c3b572c does not compile alone (definition lands in next commit). Pair must move together. Both engines agree on every runnable test. master and complete-integration each carry fixes the other lacks; convergence is a merge with behavioural gate. Arena slices 1–5a in; 5b (Cursor interior pointers) remains the blocker.
aowlcode — 44 commits. One question for every tool: what is its verdict actually resting on?
nimonyexits 0 on rejected arguments; usage banner now treated as failure.explain_failureanswered “OK: compiles clean” for failing compiles; verdict now from compile’s ownok.- Equivalence tools reported identical/preserved for runs that never ran or both crashed.
- ddmin/bisect unbounded by time; total budget added, killed runs no longer count as reproducing.
- Stale server binary answered while signals said healthy;
doctor.server_stale+ build-wait on launch. - aowli binaries already hot-swap; doctor now identifies live build and reports swapped paused sessions.
- Concurrent nimony builds raced on shared nimcache_static; every invocation now serialises through one flock.
Gates. Differential harness compared tool names only (6 of 26 differed in contract). Now 197 curated + 116 sweep cases, 107 unit, 39 e2e, all 8 hooks smoke-tested.
Messaging. /listen <label> + maildir transport; messages become events. One inbox per repo; /work <repo> drains backlog then works. Heartbeat distinguishes live/wedged/dead; --ping is proof without costing tokens. Chain depth + rate limits break reply loops.
serve — 25 commits. h2spec 95/146 → 146/146: the 95 was concurrency, not protocol.
- Accept loop ran whole session before accepting again → timeouts in aggregate. Fixed.
nghttp2_session_mem_recvreturn ignored; frames in read tail dropped.close()with unread bytes sent RST instead of GOAWAY; now close_notify → shutdown write → drain.- Stream-id reuse never reached on_begin_headers; tracked and terminates with PROTOCOL_ERROR.
- Reactor gained TLS, timers, stop, outbound client, produced bodies. Idle timeout via CLOCK_MONOTONIC; graceful stop via eventfd.
- All TLS entry points now take context overload. Streaming bodies via pull producer; static serving gained ETag/304/ranges.
- WebTransport streams closed out; QUIC shim counts resource overflows.
Gates. h2spec 146/146 h2c and TLS. Streaming e2e: 128 MiB byte-exact at 6 MB peak RSS. Proxy e2e: 12 concurrent 0.5 s upstreams in 0.53 s. One thread serves HTTP/1.1 + 2 + 3 on one port with TLS, timeouts, shutdown, client, streaming.
http — 2 commits. Added parseResponse (mirror of Request). Header count and body size now bounded (128 headers, 64 MiB).
aowlsem — 71 commits. Macros and const initialisers stopped being matched by shape and started being run.
- Macros were expanded by matching shapes. Now executed: the body is lowered to a plugin module, built, and run per call site with argument trees marshalled in and the expansion read back. Two executors for the same module —
--macros:interpsemchecks to.s.nifand runs it under aowli,compiledbuilds a host-native binary withnimony s.tests/macros.shasserts each matches the nimsem oracle and that the two match each other. - A
constno fold recognised was emitted unchanged inside its(suf … "i64")wrapper — a call where the wrapper promises a literal. Now evaluated by generating a module from the host's own declarations up to that const and running it; the value returns throughstd/writenif, notecho, which renders for a human and loses float digits. Covers int, bool, float, string; 64 evaluations per module. - The whole transitive import closure was one flat scope, so
bindSym("add")in a module importing only std/syncio and std/macros froze std/paths' and std/strutils'addinto the choice. Visibility is now a fixpoint over(import (kv <suffix> "<path>") …), seeded from direct imports and extended across export edges. - A template selected itself; a module's own declarations, reached back through an import, counted as rivals; the imported-template merge skipped the self-import filter.
- Untyped imported generics left dirty templates unexpanded;
emitInstanceUntypednow semchecks body. Multiple template overloads kept. Hygienic rename of template locals. - A generic type argument was substituted only when bare, not recursively —
Table[string, int]'s backingseq[(K, V)]kept std/tables' own typevars. Two instances of one generic differing only in hash are no longer a provable clash. - Concepts parsed/emitted but never enforced; requirements now checked at instantiation (E0282).
- Nested generic instances drained at module level (enclosing locals missing); now spliced in place.
build.shinstalled newest binary, not the one just linked; now asserts artifact newer than sources.typeof(expr)copied as tree; now demandstypeOfValueand emits answer. Macro bodies: parameterised stay raw, zero-arg semchecked.- A
trybody is a scope. An imported type's base class is recorded, so an upcast is recognised. User pragma-aliases kept and expanded at the use site. E0410 addr suppression narrowed to object-constructor field values; E0100 for a call-shaped type whose callee names nothing.
Gates. diff.sh 677/677. check 400/400, beat 4/4, nofp 35/35, diag 175/175, explain 93/93, e2e 4/4. defined() byte-exact; compiles()/declared() still open.
aowlsem passed its 1000th commit — 16 days after the first, 65 landed today, all in the checker. ~32.5k lines of self-hosted Nimony; byte-exact differential corpus 632 → 659, no-false-positive gate 23 → 35.
The productive gate was the broad-module differential, not hand-written probes. tests/moddiff.sh compiles a real project into a fresh nimcache, re-semchecks every module the oracle produced both a .p.nif and a .s.nif for, and canon-diffs each. Feature probes had gone several rounds finding nothing — that seam is saturated; forty real modules found a day's work in one run. 18/40 byte-exact, 0 crashes, the rest quantified in tokens.
The dominant class was when an instance gets minted, three rules:
- A generic type decl's body keeps
(at G …)raw; a generic routine's signature cannot, being shared by every instantiation —proc parseNum[T: SomeInteger](s: openArray[char])instantiatesopenArray[char]at decl time. - Inside a generic decl, a call over still-abstract operands stays symbolic:
t[k] = vinproc addTo[K: Keyable, V](t: var Table[K, V]; …)is the generic symbol, no instance, nohaddr. Instantiating there dragged in the operator's whole lifetime-hook cascade — 5922 tokens on a two-line program;std/sets7530 → 147. typeVarLike, consulted from ~25 places, guessed from spelling — one uppercase letter, soBiTable*[Id, T]had one of two parameters recognised. Every typevar mint is now recorded, spelling kept only as fallback for imported generics.std/bitabs5863 → 1236.
Commit #1000 fixed the ordering assumption underneath. Type instances emit in request order and a dependency is requested by the body needing it, so HashSet[T] = object; t: Table[T, bool] had its field scanned before Table[TagId, bool] registered hooks: the field read unmanaged, and neither HashSet nor anything holding one got lifetime hooks. Reordering is not the answer — the oracle emits in the same order. nimsem decides off the declaration, so we now do: a generic whose body structurally holds a lifetime pre-registers its instance's hook names at request time. std/optcore 18369 → 16052.
A module-qualified name was unhandled in three emitters — typeOfExpr (no dot case at all), semType, and the case of-value list. One shared test resolves all three: the left of the dot is nothing in scope, which is what an import name is. Relatedly from std/dirs import walkDir is now really selective — loading the whole module registered dirs.getCurrentDir(): Path alongside ospaths2.getCurrentDir(): string.
Smaller parity fixes, each pinned to a real program: a distinct over a primitive keys the base's magic on its own symbol, and !=/>/>= cancel on the operator they derive from; an anonymous routine must never consume a prescan slot, or a lambda in a global initializer takes the next proc's name; proc-type parameters are numbered at the type decl's position; a user-declared lifetime hook suppresses fieldwise synthesis; {.push header: … .} applies to every decl and parameter; ordering two pointers is an address compare; a prefix operator can be a template; a named argument may skip a defaulted parameter; and four absent folds — set consts by name, float consts, (par …) in a when condition (not (defined(cpu16) or defined(cpu8)) was dropping all of std/widestrs, 5009 → 415), and an {.untyped.} template body.
One thing is written down as not reproducible rather than chased. nimsem lists an (ochoice …) in a hash order — system's + comes out 3, 6, 12, 10, 14, 0, …, which is not declaration order, not .s.idx.nif order, and not sorted. We emit the right set, sorted, and the commit says so.
aowlcode is now private indefinitely.
aowlcode 1.0 — the tool gate is on by default. aowlcode fronts the Nim and Nimony toolchains for an agent: structured diagnostics, NIF slices and navigation instead of raw compiler output and 40KB single-line artifacts. The tools shipped months ago and were still bypassed for grep -rn + sed -n, so 1.0 inverts the default rather than the documentation.
Aowl mode, default guided. A PreToolUse hook denies Grep, Glob, and Bash segments that are a code search, a source/NIF dump, a tree walk, or a raw nim c / nimony c / nim check. git, test scripts and running a built binary pass; strict denies Bash outright. Each denial carries a redirect table and appends to a ledger /aowl-mode status reports. No state file ⇒ guided; off is a written state, not its absence, and expires on the same 12h TTL, so a stale strict and a stale off both fall back to baseline. Escape hatches: aowlcode-mode commands always pass, AOWLCODE_DEFAULT_MODE moves the baseline, AOWLCODE_NO_MODE_GATE=1 removes the hook, every hook fail-open.
Four tools cover what the gate removes, each replacing a habit whose failure mode is unbounded output — search (excludes generated trees and hidden dirs; .claude/worktrees/ alone multiplied one repo's apparent source count ×10; caps and reports truncation), map (one-call orientation, parsing the build script's actual compiler invocation so a Nimony project with no nimony.cfg marker stops resolving as Nim), changes (git diff as per-file +N -M and hunk headers, ~1% of patch bytes), and run (output middle elided, head 30 / tail 60, so the failing assertion at the tail always survives).
0.8, same cycle, closed three gaps two agents had each hand-rolled in shell: nif_run executes a built .s.nif on aowli with its sibling modules, deriving the install name from the artifact's own stmts header (getting it wrong silently runs the oracle instead of the candidate); bisect runs ddmin over a flag matrix for the minimal reproducing toggle set, catching multi-flag interactions a linear scan misses; nif_diff mode=canon|semantic strips line info and framing and folds generic-instance hashes, replacing a hand-written canon.py. Plus 28 end-to-end checks over the real MCP loop.
aowlsem gained its second half: an optimizer on .s.aif. ~30.5k lines, 917 commits (67 today), corpus 631 modules. Separate command — aowlsem m checks and must match nimsem byte for byte; aowlsem opt rewrites and must only preserve meaning. Those claims cannot share an exit path, so the parity gate is structurally untouched.
Twenty-one passes to a fixpoint (nine sweeps max, tree strictly shrinks): constant folding over the twelve arithmetic/bitwise magics; comparison/not/xor of constants; short-circuit and/or; algebraic identities including operand-discarding ones (legal only once purity can be asked); redundant-conversion drop; constant if/case selection; while false; unreachable statements; dead and write-only locals; unreferenced private procs; constant and copy propagation, where the constant query sees through propagated locals — which is what lets folding and case selection fire at all. Inlining runs first each sweep in three shapes, alpha-renaming the inlined body since .s.aif symbols are module-wide unique. Two invariants cut across: exported means live, and never drop work with effects.
The verification is the substantive part. A dozen hand-written programs will call an optimizer green. A scale gate builds each real program three ways (nimsem's output, ours unoptimized, ours optimized), runs all three on aowli, and demands byte-identical stdout and exit. It caught nine genuine miscompiles the small suite passed — an expression-if deleted outright, an inliner accepting any three-statement proc as one-expression, {.keepOverflowFlag.} making arithmetic observable, inlined generic-instance bodies, statements pasted into expression position — each narrowed by --no:PASS sweep and pinned with a regression program. The gate went 103 → 344 of 609 candidates; one fix (deriving the install name from the artifact header) recovered 145 programs silently recorded as "did not run".
Measured payoff from bench.sh: three nested one-line procs in a hot loop, 360,003 calls → 3, 505ms → 95ms (5.3x); one hot one-liner, 120,003 → 3, 212ms → 77ms (2.8x); a partly-constant loop body, 118ms → 64ms (1.8x). On whole library modules it removes ~1–3% of nodes, and the doc says so — a library is nearly all exported surface.
Anonymous sum types closed — construction and of-pattern matching, including failures that only appear across a module boundary: instantiating an imported generic sum type (Opt[T], Result[T, E]) left pattern bindings holding a field's address rather than its value, and the family's shared tag type was re-declared in every importing module instead of being recognised as foreign by its mangled module segment. That exposed a wider gap worth more than the feature — a local initialised by a call returning a generic application had no type at all. std/opt and std/result check byte-exact both as the defining module and from an importing one.
The playground grew into a real in-browser IDE — still the whole toolchain (parser, checker, interpreter, debugger) compiled to JavaScript, running entirely in the tab.
- Multi-file projects + explorer. Dockable tree with context menus, multi-select, drag-to-move, preview tabs (single-click italic preview, double-click or edit keeps it), and navigation history on the mouse back/forward buttons (or Alt+←/→).
- Clone a repo, or share a workspace, from a link. Type
owner/repoto clone a public GitHub repo client-side, or hand someone a#clone=owner/repolink. Share packs the entire workspace — every project and file — into one compressed link, not just the active buffer. - The aowli debugger, live in the browser. Step through a program on a flame / depth timeline: every statement a cell, call depth stacked into lanes, per-routine colour, zoomable slice and full-run minimap — scrub, reverse-step, jump anywhere, auto-captured on open. (Fixing the current-line highlight traced a neat root cause:
echois a template, so its expansion carries the stdlib's line info, not your call site — the debugger is now file-aware.) - Split editors + stdlib browsing. Drag a tab to any edge for side-by-side or stacked. Ctrl-click or F12 on an
importopens the real std source..json/.js/.c/.nifget native highlighting — including a proper NIF grammar — and skip the nimony pipeline, so only.nim/.aowlare checked and run. - Latest bundles: obfuscated aowlsem and the aowli interpreter + debugger refreshed.
Every doc page for a runnable library now carries a "▶ Try it live in the Playground" link.
aowlsem spent the day on the half of a checker that never shows up in its output: deciding which programs are wrong. Byte-for-byte agreement on valid programs says nothing about invalid ones, and a checker that quietly accepts a broken program is worse than one emitting a slightly different tree for a good one. The method is a checked-in tool: ten small programs around one theme — arguments, literals, control flow, generics and closures, declarations, numeric types, exceptions — through both checkers, verdicts side by side. Rejecting what the reference accepts is the urgent kind; accepting what it rejects is a missing check; occasionally the reference is wrong.
About thirty checks landed. A sample of what now errors instead of passing silently: a named argument no parameter answers to (with a did-you-mean); for a, b in 0 ..< 3, where the range yields one value per step; assigning to the result of a call; a case on a float; the branches of an if-expression disagreeing on type; a variant constructor setting a field from an unselected branch; a nested proc reading its enclosing local without being a closure; arithmetic on types that have none (true * false, 'z' - 'a', "a" + "a"); indexing with a non-number; mixing signed and unsigned; a set over a non-ordinal; deriving from an object never made a base type; the wrong number of type arguments; a converter not taking exactly one parameter; a pragma that is not one (checked against the whole vocabulary plus your own); and an entire family that had been silently accepted — an undeclared type name in any position: field, parameter, local, parent, generic argument, except filter.
Four false positives came out of the same loop, and mattered more than the gaps. Two methods along one inheritance chain reported as ambiguous — the subtype relation made the signatures look identical. Shadowing a parameter (proc f(a: int) = let a = a + 1), ordinary Nim, reported as redeclaration because parameters share a scope with the body. Both directions of an enum conversion rejected as impossible. A for over an enum range mis-flagged as iterating a non-collection.
That last pair had been hiding: the probe programs also tripped a real error from the reference, so the verdicts "agreed" and the tool said nothing. It now prints why each side rejected, and every new check was confirmed to fire for the same reason rather than by coincidence. Re-running the day's probes through that lens found five more rejected for the wrong reason — three fixed, two written down as open.
Gates: corpus 618 modules, accept/reject agreement 76 → 139, error-message snapshots 64 → 97, and every one of the 71 diagnostic codes has a long-form --explain article. A third case joined the set where aowlsem is right and the reference is not: proc maxOf[T](a, b: T): T = if a > b: a else: b, textbook Nim the reference cannot instantiate.
aowli-release is now private indefinitely.
Debugging a big program under aowli stopped meaning "recompile it every time." Pointing the debugger at aowlsem took minutes per run, but the interpret is ~1 second — the minutes were aowlcode's debug/trace recompiling the whole ~20k-line compiler plus stdlib from scratch with -f, then deleting it. The obvious "persistent cache → incremental rebuild" fix was measured and does not help: a warm no--f rebuild costs the same ~47s. The real fix is to skip the compiler when nothing changed — reuse the built .s.nif, recompile only on an actual source edit. First debug of a session ~47s; every one after ~1 second. (Or hand the tools a prebuilt .s.nif.)
Released aowli v0.3.3 — hybrid-native mode crosses ref/seq-bearing data. Hybrid runs the modules you are not debugging as compiled code while interpreting the one you are; a shared-memory arena lays a live value graph out at native layout, so calls taking ref objects, nested ref graphs and seq[T] fields (including seq-of-object) cross too — the native side reads and mutates the same memory, synced back. Additive and dormant: without the flag, execution is byte-for-byte v0.3.2, and anything not safely marshalable falls back to interpretation.
aowlsem spent the day closing byte-level gaps against the reference's typed output. Method: a small valid program per feature, both checkers, diff token-for-token; every difference is a bug or a recorded lowering choice. ~21.7k lines, 700+ commits, corpus 500/500, accept/reject 10/10, std/system clean.
- Generic
ref objecttypes reached full byte-identity.Container[int](…)constructs a real heapref(newobj) instead of a value; a generic instance's synthesized lifetime hooks no longer bake in the defining module (the instance is content-addressed already); the object half numbers its type parameterT.1to the alias'sT.0, matching how the reference counts aref object's two declarations. - Value objects that carry methods. An inheritable object with managed fields that also declares
methods emitted the full four-hook lifetime form; the reference emits only the user-method vtable, because a type with a real vtable routes its own destruction through it. The trigger is the presence of a user method, not inheritability. - Smaller parity fixes. Generic variants resolve named-branch fields;
{.borrow.}operators returning a distinct type convert the result back;untyped/typedtemplate parameters are wildcards, sotemplate twice(x: untyped)inlines at the call site; boolcaselabels emit literal(true)/(false)tags. - Reading a variable before it is set is now an error.
var x: int; return xis rejected, as by the reference (alsodiscard x,s.add …on an untouchedvar s: seq). The definite-assignment analysis existed for a single-assignment check; today it started reporting. A branch initializes only when every path does, so a value set in one arm of anifwith noelseis flagged while an exhaustivecaseorif/elseis accepted. This is what lifts accept/reject to 10/10. var-returning calls as assignment targets.first(c) = 99writes through the location the call yields; avar-returning proc forwarding another such call emits the bare pointer-to-pointer copy instead of address-of-a-dereference. (Both emission sites were pinned by stepping aowlsem itself under aowli's interactive debugger.)
Earlier the same grind landed lambdas as expressions, cross-scope iterator resolution, custom []/[]=/{}/contains, multi-index x[i, j] read and write (two assertion crashes fixed), cross-module import-resolution fixes, and a relative include resolving straight from its parsed artifact with no source file on disk.
aowli's debugger can now pause a running program and step through it interactively, instead of only printing breakpoint snapshots after a run finishes. Three additions, all in aowlcode 0.6.13 and documented under Debugging:
- Interactive stepping (
--session). Run a program once and keep it paused between commands: step into a call, step over it, run until the current routine returns, or continue to the next breakpoint. You can set breakpoints while it's paused and inspect the current frame — without re-running the program for each look. In the plugin this is the newdebug_sessiontool. - Readable output for big values. A large local — say a compiler's context object full of lookup tables — used to print as thousands of lines. Values are now rendered under a size budget and the rest is elided with a marker, so a frame dump stays readable regardless of how large the values are.
- Drill into one field. Rather than print a whole value, name the part you want —
expand c.currentModule.name,expand xs.3.field— and only that piece is shown. Object fields resolve by name, seq/array elements by index.
Also fixed a build issue where a rebuilt debugger binary could be shadowed by an older copy earlier on the lookup path. The binary now reports its build version (aowli-dbg --version) and installs to one canonical location. Full command reference: aowlcode → Execution.
Released aowli v0.3.2 — two shipped-runtime correctness fixes surfaced by running a real argument parser under the interpreter: s[a..b] / s[a..<b] slices returned only the first element instead of the substring, and a non-string value (a nil/default) could compare == equal to a string. Both fixed; byte-identical to a native compile on the repro, and the differential corpus stays at 77/77. Hardened binaries (obfuscated IR + licence gate + stripped) with SHA256 are on the release page.
aowlsem, the from-scratch semantic checker, keeps closing on full parity with the reference compiler. It is now ~18.6k lines of self-hosted Nimony across 550+ commits, and its byte-exact differential corpus stands at 498/498 modules matching nimony's own typed output, with the entire std/system checking clean (0 diagnostics). Today's work brought generic type instantiation in line with the compiler's own behavior:
- Generic sum types construct by inference.
let d = Some(99)works outOption[int]from the argument, sod.valis anintandd.val == 99resolves to a single integer comparison instead of a 25-way overload set — the same inference drives annotated conversions (Option[int](x)) and two-parameter sums likeEither[int, string]. - Plain generic value objects infer their instance too —
Pair(first: 1, second: 2)picksPair[int]straight from its field values. - Generic
ref objecttypes instantiate in full. A recursiveTree[T] = ref objectvariant now emits both halves the compiler expects — the reference alias and its underlying object type — each carrying its own per-instance lifetime hooks (destroy / move / copy), with matching typevar numbering, and its constructors (Branch(…),Leaf(…)) build the concrete instance rather than the generic origin.
aowlmcp now speaks the MCP 2026-07-28 spec — the biggest MCP revision since launch, and a clean break with its stateful past. The library serves both protocol versions, negotiated per request, so upgrading a client is never a flag day:
- Stateless core. No
initializehandshake and noMcp-Session-Id: every request carries its own context in_meta(protocolVersion/clientInfo/clientCapabilities), and a newserver/discoverRPC advertises capabilities up front (it doubles as the stdio back-compat probe). Any instance can serve any request. - Multi-Round-Trip Requests (MRTR). A tool can return an
InputRequiredResultto elicit user input mid-call; the client re-issues withinputResponsesand the echoedrequestState.registerToolMRTR+newInputRequired. - Tasks extension.
registerTaskToolreturns a task handle;tasks/get/tasks/cancel/tasks/updatedrive it — advertised inserver/discoveronly when a task tool exists. - Caching + routing.
tools/listgainsttlMs/cacheScope; the Streamable-HTTPMcp-Method/Mcp-Namerouting headers are accepted. Roots / Sampling / Logging are deprecated (aowlmcp never shipped them).
Every new flow is proven over all three transports — stdio, HTTP, and HTTP/3 (QUIC) — because they share one transport-agnostic dispatch core: stdio 27/27, HTTP 15/15 (routing headers + MRTR + Tasks over the wire), HTTP/3 4/4. Docs updated alongside.
Also released aowli v0.3.1 — the interpreter now runs the Nimony semantic checker itself, byte-identical to a native compile. Pointing the interpreter at aowlsem — a real, compiler-grade program — and diffing against native turned up three root-cause bugs; fixing them brought the run to 520/520 tokens identical. This means aowlcode's debug/trace can now be aimed at the compiler's own passes.
- Fully-initialised pointer values. Constructing an interior
ptrleft its flat-memory view fields (region/foff/elemBits/base) uninitialised, so a later read saw garbage — a genuine memory-safety class, not a cosmetic gap. Caught with valgrind running under mimalloc's Valgrind-tracking build (mimalloc otherwise bypasses the sanitizer); every pointer construction now initialises all fields. seqappend value-copy.s.add xnow copiesxon the way in — the same=copyenvelope semantics v0.3.0 gave assignment — so mutating the appended element never aliases the source.- Content-addressed tag dedup.
StringView.==is gated so NIF tag interning deduplicates by identity, the wayTokenBuf-style content-addressed programs expect.
The debugger got sharper alongside it. --break-func:NAME and file-scoped --break:file.nim:LINE resolve routines through the include chain (a bare line number is ambiguous once modules are merged), and program_args forward to the interpreted program's commandLineParams() — so a breakpoint can land inside semCall while the checker runs on a real input.
A plugin-packaging note. The aowlcode MCP server a session runs isn't the marketplace checkout or the newest cached build — it's the exact version pinned in installed_plugins.json. Ours was stuck several versions back at 0.6.0, which silently dropped any newer argument (like program_args) before it reached the handler. The fix is a one-line pin bump (to 0.6.9) plus a restart; the thing to watch for is a parameter that's present in the source but missing from the live tool's schema.
Created aoughwlup and aoughwl
Created aoughwl-code and aoughwl-code-release
Released aowli v0.3.0 — the correctness-complete build. Both engines — the tree-walker behind the public aowli-interp / aowli-dbg, and the internal bytecode VM — now hit zero in-scope divergence across a 423-program differential corpus run against the nimony compiler. The engines agree with each other and with native, program for program.
Value semantics landed. Assigning or binding a value object / tuple / value-array now copies the envelope (refs stay shared) — var x = a; x.a = 999 no longer reaches back and mutates a. This was the last big place aowli's aliasing quietly disagreed with the real =copy.
The last OS-boundary gaps closed. Real host stat / lstat (so fileExists / dirExists are correct), pointer identity in == / !=, cast[int](ptr) round-tripping through flat memory, and VM argv / stdin seeding. Plus a sweep of narrower fixes: float→int conversion, block-expression values, cyclic-import init order, a self-nested-iterator hang, and Table element write-back.
The one boundary the pure value interpreter can't cross by itself is literal-C: {.emit.} and C FFI have no C to run inside the value model. That's exactly what hybrid-native mode absorbs — and today it ran real foreign C for the first time.
Hybrid-native executed real C-FFI. aowli's hybrid mode — interpret most modules, run selected ones as native code — now offloads a header-backed {.importc.} proc to a compiled shim and calls into genuine C: a static inline cadd crossed the boundary, ran natively, and marshaled its result back into the interpreter byte-identical to native. The proc-offload path (pure-nimony procs run native) also picked up a permanent regression lane. This is interpreter-development progress, not yet in the public v0.3.0 binary; the remaining piece is top-level {.emit.} / importc-var in the main module.
Playground refresh. The in-browser playground got a round of work: rebuilt engine bundles (fixing a stale-bundle bug where Bytecode-VM and Native-JS runs showed no output), a unified Pipeline config panel (parser · checker · lowering · engine), aowlsem rebuilt from latest, the two source-pane toolbars merged into one, a clearer footer with an inline engine picker, and curly-brace block mode no longer false-flags a { … } body as "not a Nim block."
Gave the whole stack one source of truth for how values are laid out: aowlabi. Three places each kept their own copy of how is a string / seq / object / ref actually represented — the interpreter, the C backend, the JS backend — and they had quietly drifted. aowlabi is now the single canonical answer:
- the size / alignment / field-offset engine — one implementation of the C-struct layout rules, parameterized by pointer size
- the canonical heap-block spec — string SSO +
LongString{fullLen,rc,cap,data}, seq{len,data}, the ARC ref box{rc,data}— as named offset constants, one truth - the marshal matrix — which types cross a native boundary by value / by buffer / by fallback, plus the JS representation mapping (fast
numbervs faithfulbigint, char, tuple, and so on)
aowlc, aowljs and aowli all read the same spec now instead of re-deriving it.
aowli grew a real runtime layer. The scattered places where the interpreter crossed from its value world into a faster / foreign executor — host natives, flat memory, syscalls, the miss policy — are one spine now: a provider registry (interpret · host-native · syscall · hybrid-native), a codec (identity · flat C-ABI · JS value), and a policy that is never silently wrong — an unsupported crossing fails loud or falls back to interpret, never a wrong answer.
And the payoff — hybrid execution. aowli can now interpret only the file you care about and run every other module as natively-compiled code at full speed. Debug one file slowly, with full observability, while its libraries run native. aowli auto-generates C-callable shims for the cross-boundary calls, marshals scalars, POD objects / tuples, strings and seqs across using aowlabi's layout, and dispatches at the call site. Every result is byte-identical to the fully-native build; anything that can't be safely marshaled (refs, closures) transparently falls back to the interpreter — so it is faster where it can be and correct everywhere.
aowli --hybrid --interpret:mymod prog # mymod stays observable, everything else runs native
Rebuilt the net stack around a single-threaded async reactor: one OS thread, epoll, passive-proc coroutines, no std async or thread pool.
- HTTP/1.1: keep-alive, chunked, 300/300 concurrent
- WebSocket: masking, frame/control validation, fragmentation, incremental UTF-8, close validation,
permessage-deflate; 19/19 conformance, 160/160 echo - HTTP/3: ngtcp2 + nghttp3 + GnuTLS behind a small pull API; 20 QUIC clients, one thread, ASan/LSan clean
- RFC 9221 datagrams + WebTransport datagrams over H3; streams remain
Created aowljson: reusable JSON values, error-as-value parsing, serializer, builders, v{"key"}, v.at(i).
Created aowlmcp: transport-independent MCP dispatch over stdio, HTTP, and HTTP/3. Tests: 13/13, 6/6, 4/4. Includes compile diagnostics and NIF outlines through aowlcode.
aowli became an actual runtime: flat memory, casts, copyMem, allocation, unchecked arrays, fd-backed file I/O, env access, ownership hooks, refcounted ref objects, and fail-fast unsupported stdlib calls. It now runs about 92% of compiler-buildable programs, with no known silent wrong-result cases. Remaining: some OS/VM gaps, threads, async.
Released aowli-release v0.1.0 with:
aowli-interp: run typed NIF, optional call-tree traceaowli-dbg: batch breakpoints and structured frame dumps- stripped binaries, fail-closed licence gate, SHA256, VirusTotal links
- no source paths or internal proc/type names
Updated aowlcode with trace/debug tools, /land, Haiku appliers, and parallel edit application.
Back to work after a couple of quiet days.
A quiet day.
Shout-out to a fellow Nim'er's project — 3code, worth a look.
More aowlsem — the whole day is a generics push. The semchecker now instantiates and preserves generic constructs end to end: typevar calls and signatures, generic object applications with substituted field types and attached hooks, generic array bounds and range iterators, generic seq index reads, var forwarding through generic params, late-bound generic hook calls, and quoted generic operators. Around it: out parameter type resolution, sink/source normalization, typed pointer comparisons lowered to magics, unchecked-pointer index assignments wrapped, requires pragma expression checking, and threadvar globals emitted. Steady, surgical commits — aowlsem is now past 340 total commits since Tuesday.
A major day for aowlsem — 126 commits landing the clean-room semchecker's core. It now passes 397/397 corpus fixtures byte-exact against the nimony oracle, and — the milestone — it does a complete zero-diagnostic traversal of the full std/system: the whole system.nim plus its included std/system/*.nim set, ~6,383 lines, semcheck with 0 errors and 0 log lines. Full-system parity against nimony's own output is down to ~33k canonical diff lines from an earliest baseline of ~62k — a 46.5% reduction, with the first mismatch now a third of the way into the semantic output.
Under that headline: the magic table (arithmetic / comparison / set / pointer magics), varargs[T] params with call-site collection, membership (x in coll) generalized across seq/array/string, seq slicing and s[a..b], for (a, b) in … tuple destructuring, ^k backwards indexing, countdown typevar inference, concept declarations, lifetime-hook attachment, and ptr UncheckedArray[T] indexing. aowlsem also grows diagnostics that go beyond nimony — E0205 self-comparison, E0206 unsigned-compared-to-zero, E0207 empty-loop-range, E0208 tuple-index-out-of-bounds, E0209 shift-amount-out-of-range. (source private for now; docs public, access on request)
We also stood up the whole distribution story — private components, public binaries. The plan is simple: anything that stays source-private, we still ship to everyone — obfuscated, inside a stripped binary.
- obfuscate was reworked to be IR-only. It operates entirely on the compiler's own NIF/AIF token tree, never on source text, so it inherently can't corrupt runtime data — strings, chars and comments are their own token kinds and are never touched. Its
obfnifpass renames every declared symbol to an opaque token (by spelling on parsed NIF, symbol-precise on typed NIF) and weaves in behaviour-preserving control flow, then the result re-feeds the pipeline and behaves identically. - aowl-release is the hardening harness — a
build-release.shthat wraps each component's own build and layers source obfuscation → a fail-closed licence/version gate → NIF control-flow injection →--strip-all(drops the symbol table decompilers love). The gate refuses to run an expired build; there's no risky client-side kill-switch. - Five
-releaserepos now exist as the public homes for the currently-private stages — aowlsem-release, aowli-release, aowlts-release, aowlpy-release, aowlweb-release. Source stays private; the obfuscated, gated, stripped binaries land here shortly so anyone can run the full stack.
And aowlup — rustup for the aowl/nimony stack. It installs, versions, and selects the pipeline: every slot has interchangeable variants (parser aowlparser|nifler, sem aowlsem|nimsem, hexer aowlhexer|hexer, plus backends and tooling), grouped into one-command profiles (aowl = all ours, nimony = all theirs, hybrid = the driver default), each pinned to a git version with a GitHub update check (it doubles as a nimony version manager). aowlmony then compiles against whatever aowlup has selected — exactly the rustup : cargo split. The -release binaries plug straight into this: aowlup is how you'll pull, pin, and select them, and aowlup +nimony gives one-shot toolchain overrides.
And the playground grew a semantics choice. aowlsem now runs in the browser — you can pick aowl semantics instead of the default nim semantics when type-checking, right in the playground. It's marked experimental: real and checking a substantial slice today, but not the full stdlib or generics yet, so it grows from here. aowlsuggest moved into the playground too — its quick-fix / lint layer now runs client-side over the parser's diagnostics, so fix-its surface as you edit — and aowlparser got another update, with the latest parser bundle now shipping in the playground.
A heavy day on the front and middle of the pipeline.
aowlparser — reached full 310/310 structural parity with the upstream Nim standard library: the entire stdlib round-trips. Shipped a real check lint mode — grammar-level error detection with fix-its and source-ordered diagnostics: assignment = where == was meant, a for missing its in, identifier-expected on let/const, and more. Fixed three parser hangs (infinite recursion) and hardened the lexer — UTF-8 identifiers, BOM stripping, custom numeric literals (N'big), parenthesized proc literals, term-rewriting template patterns.
aowlsem — a big step toward a true drop-in: an auto-import system that pulls in system and the module's own imports with no manual flags, real include splicing, when not defined(...) folding, definite-assignment that honours noinit/threadvar/importc, typedesc modelled as a type, templates as an overload set, accent-quoted/operator routine names, and the first value-object ARC hook synthesis — the foundation for Table. (source private for now; docs public, access on request)
aowli — the interpreter/VM now reads the shared aowlhl HL-IR layer (hlload / hlclassify / hlwalk) instead of its own tree-walk, and gained dynamic method dispatch with field write-through for ptr/var receivers, closures with nested capture, and UTF-8 add(string, Rune). With this, aowli is feature-complete: it reproduces 100% of the runnable test corpus byte-for-byte against nimony's own compile-and-run, on both engines (tree-walker and bytecode VM).
aowlhl is now the shared high-level IR — one Nim→HL-IR reader that both aowli and aowljs consume, so the interpreter and the JavaScript backend classify and walk the same skeleton. One lowering, many emitters.
The docs site got a ground-up rebuild. Migrated aoughwl.github.io off Jekyll / just-the-docs onto VitePress — it's now a single-page app with instant client-side navigation (no full reloads), a collapsible nested sidebar, a near-black dark theme, local search, and self-hosted fonts (no font or page flash). It deploys through a GitHub Actions workflow instead of Jekyll, and the in-browser playground is preserved byte-identically.
The nav is region-grouped — Overview / Pipeline / Emitters / Tools / Libraries — and every pipeline, emitter, and tool row carries a small right-aligned "↗" straight to that project's GitHub repo. A floating theme toggle sits in the corner, and GitHub · Discord · Support links live in the top bar.
Repositioned: aoughwl is a ground-up Nimony toolchain. Wrote the interop contract — AIF ≡ NIF, byte-for-byte, so any Nim/Nimony program behaves identically. Renamed the compiler stages aif* → aowl* (aowlparse / aowlsem / aowlhexer / aowlc / aowljs / aowli / aowlmony) — aif now names the format only — and nim-code → aowl-code. Reworked the docs into two homes — Documentation (terse reference) and Engineering Notes (opinionated writeups) — collapsed the changelog into a single Changes record, and normalized every repo description + topics across the org. aowlsem and aowlhexer stay private for now (docs public, access on request); the playground moves onto the new sem + hexing shortly.
Created aifhexer
Created aif
Created aifmony
Created aifc
Created aifjs
Created aifjs-js
Created aifsem
Updated aifi
Updated aifparser
Updated nimony-playground
Took nifi private.
Updated nifi — 6–10× performance gain.
Updated nimony-playground
Updated nifparser
Updated nifi
- Added curly bracket support to the nifparser
- Finalized nifparser, passing against the full nimony suite- byte identical to niffler
- Nearly finished nimony-playground, missing small QOL and a final port to the aoughwl ecosystem (I cannot wait)
Created nimony-playground
Created nifparser
Created nifi, a Nimony NIF Interpreter
Created aowl-lsp, it's nimony-lsp, but with a universal plugin system. Obtain novel features!
Created vscode-aowl, aoughwl hosted on-machine within VSCode
Finalized the aoughwl core spec.
Noticed a memory bug exists- likely roots as a true Nimony bug which interacts with niflens and my nim-code instances
Created nifrewrite makes NIF rewrites simple
Fixed IC
Updated niflens
Updated nimony-lsp
Our VSCode extension + Nimony LSP is nearly as performant and featurefull as it can be, live diagnostics work phenomenally as you type... more to come here.
Pushed IC to aoughwl/Nimony: ~1s -> ~10ms
see: ic-parallel-deps, ic-cursor-traversal, ic-warm-daemon, ic-batch-intern
Created niflens, a CLI tool for parsing and viewing NIF
Updated nimony-lsp and nim-code to benefit from niflens — live diagnostics and suggestions now work as you type!
Massively expanded the net stack — now 8 one-concern repos:
• tls — TLS 1.3 over OpenSSL 3, client + server (SNI, ALPN, verification); pulled into its own repo
• serve — HTTPS, a concurrent worker pool, HTTP/2 (nghttp2: h2c + ALPN h2), chunked request bodies + Expect: 100-continue, opt-in gzip/br compression
• dual-stack IPv6 across tcp / net — one listener serves v4 + v6
• new compress repo — one-shot gzip / brotli / zstd codecs
• ws — a nimony-native WebSocket (RFC 6455), server + client, ws:// and wss://
• HTTP/3 in requests (useHttp3) via curl-impersonate's bundled ngtcp2
Every layer is tested against real clients — curl --http2, live wss://, TLS 1.3 handshakes.
Today starts the official aoughwl/nimony fork.
This is now the main place my Nimony work will go:
- features
- bug fixes
- more opinionated, dynamic, and substantial stdlib
We also shipped aoughwl/nimony-lsp and aoughwl/nim-code:
nimony-lspis the language-server sidenim-codeis a Claude Code plugin and MCP server focused on reducing token usage with Nim and Nimony
Effective indefinitely, the Nimony JavaScript/TypeScript/WASM/Python backend work is private.
For interested parties: the JavaScript and WebAssembly backends are ~complete and remain true to the original vision.
I will gladly and promptly add anyone who wants access, but you will need to reach out to me directly over Discord (timbuktu_guy)
