relay-test-coverage.md raw

Relay test coverage audit

Scope: every package compiled into the relay (git.smesh.lol/smesh, the main.mx closure) plus the web/common modules the tests pull in. The question this answers is the one that matters for debugging: is every line of useful code in the relay validated by a test that runs?

How the suite runs now

make test runs three layers:

  1. make test-unit — every package carrying a *_test.mx, through the

compiler's own harness (moxie test ./<pkg>).

  1. make build-test-relay + make test-signer — the relay and the browser

signer used by the integration suite.

  1. python3 -m pytest test/ — the integration suite (HTTP, WebSocket, NIP-01

policy, NIP-11, front-end and worker shims, MLS, memory instrumentation).

Before this audit layer 1 did not exist: twelve *_test.mx files sat in the tree and no target ever ran them. When they were finally run, eleven of the twelve did not compile, and moxie test itself was broken in three separate ways (below). Everything in this document is what running them revealed.

As of the current tree, make test-unit runs **40 packages plus the root main package** (the loop used to find pkg web, which never saw the root main_test.mx) and the skip list is empty. Nothing below is skipped for a known-broken reason any more, and the cover target no longer deselects a test: the one it used to drop (test_relay_proxy_high_event_volume) now runs against the instrumented binary with a widened post-EOSE window.

What the audit fixed

moxie test never built a real closure (four compiler bugs)

  1. The test files' imports were not in the closure. cmdTest added

testing to the package's imports and nothing else, so a dependency the package under test does not itself use (bytes, crypto/rand, time in a test) was never resolved: the type came out invalid, var bytes.Buffer allocated one byte, its method calls were dropped, and the test miscompiled silently. Minimal repro before the fix, pkg/nostr/varint's bytes.Buffer + io.Writer use:

` % moxie test ./pkg/nostr/varint panic: runtime error: interface dispatch: no impl for Write `

  1. No dispatch index. cmdBuild type-checks every package into the

registry and builds dispIfaceMethods/dispMethodsByPkg before the first emission; cmdTest did neither, so every package looked tier 2 and the emitted dispatch had no arms. The two passes are now one function, prepareDispatchIndex, called by both.

  1. No link-set cache key. A package's cache key carries bst.linkSetKey

because the dispatch arms it emits depend on the closure. cmdTest never set it, so a test build looked up packages under the empty key and reused arms compiled for a different closure. Minimal repro: moxie test on a package that calls crypto/rand;

` panic: runtime error: interface dispatch: no impl for Read `

The same code in a normal moxie build worked, which is why the relay itself never showed it.

  1. No `-work`. moxie test -work now keeps and prints the test scratch

directory like the build path, which is how bugs 1-3 were found.

Assignability of composite literals (compiler)

cmdBuild's assignability check covered assignments and arguments but not composite-literal elements: store, slice and map literals stored their values unchecked. Legacy rejects these through go/types; stage4 accepted them. The audit hit it first in smesh:

type Limiter struct {
    buckets map[string]bucket
}
...
    buckets: map[string]*bucket{},   // legacy: cannot use ... as map[string]bucket

pkg/relay/ratelimit carried exactly that, and stage4 compiled it happily (runtime map element layout happened to match, so it even worked). stage4 now checks struct-literal fields, array/slice elements and map keys/values and reports the same wording legacy does.

String() on a builtin (compiler)

resolveMethodCallWithRecv synthesised runtime.(*uint64).String for a direct .String() call on a basic type. No such symbol exists — the builtin stringers are interface-only identity thunks (basic:<type>.String$identity, emitted by emitBasicIdentities) — so the call was accepted and failed at link time:

% moxie build ./probe   # x := uint64(5); println(x.String())
ld.lld-22: error: undefined symbol: runtime.(*uint64).String

Legacy rejects the same source as `x.String undefined (type uint64 has no field or method String)`. stage4 now falls through to the same undefined-method error. The supported path — boxing through fmt.Stringer (fmt.Println(x), t.Fatalf("%d", x)) — is unchanged and covered by the tests.

smesh source bugs found by the new tests

wherebugfix
pkg/relay/ratelimitmap[string]*bucket{} assigned to a map[string]bucket field (hidden by the literal hole above)literal corrected
pkg/nostr/timestampFromVarint returned the consumed prefix as rem instead of the remainder, unlike every other Unmarshal in the tree (no callers yet)returns b[read:], covered by a test
pkg/modeIsOpen's logic was unreachable from a test: it reads the ACLMode global, and package globals are immutable outside initlogic extracted to the pure IsOpenMode, which the test covers
pkg/nostr/filterF.Unmarshal dispatched on key[0] plus a length bound instead of an exact key match: {"arr":[1]} and {"s":1} were rejected as invalid, and {"iXXX":"hello","kinds":[1]} was parsed as ids, which swallowed the rest of the filter and silently dropped kindsexact key match (keyIs); unknown keys land in Extra, asserted by the test
pkg/nostr/tag(*T).Unmarshal accepted a closing ] with no opening [ (the early return skipped the final guard)guard the early return on openedBracket, asserted by the test
pkg/nostr/tag(*S).GetTagElement(i) panicked at i == Len() (len(s.T) < i let the boundary through)i < 0 \|\| i >= len(s.T) returns nil, asserted by the test
pkg/nostr/event testshadowed signer/err (Moxie forbids shadowing; the test predated enforcement) and never compiledrewritten with per-function names
pkg/crawler, pkg/find, pkg/grapevine testsused t.TempDir() and t.Cleanup(func(){ eng.Close() }); Moxie has neither, and a cleanup closure would have to capture engopen the store in os.MkdirTemp and close it with defer in the test
root main.mx (outboxDiscover)the follow list holds 64-char hex pubkeys (ValueHex) but the kind-10002 map was keyed by the event's raw 32-byte pubkey (string(ev.Pubkey)), so the inversion never matched a follow and every author fell back to wss://relay.damus.io - the outbox model was silently the default model. Found by TestOutboxDiscoverBuildsWriteRelayMap, which printed 1 write relays discovered instead of 3key the map with hexEnc(ev.Pubkey) so both sides are hex; the test now sees the three buckets and asserts the write/read/absent r-tag markers
pkg/store/checkpointSet removed its temporary file when the write or the sync failed but not when the rename did, so a failed Set left checkpoint.dat.tmp behindremove the temporary file on the rename failure too, and assert it in checkpoint_paths_test.mx
pkg/nostr/kind(*S).Unmarshal read r[0] after the last kind without checking the remainder, so a truncated kinds array (["REQ","s",{"kinds":[1) crashed the relay - index out of range, and with that read bounded, SIGSEGV further along the filter pathbound the access, which keeps the documented leniency ([ parses to no kinds) while a truncated array no longer reads past its end. TestSUnmarshalTruncatedArray locks it. The deeper SIGSEGV a truncated filter object still causes is the open item below

New unit tests

pkg/nostr/hex, pkg/nostr/varint, pkg/nostr/ints, pkg/nostr/timestamp, pkg/nostr/kind (39 tests, embed/kind/kinds to 99-100%), pkg/nostr/tag (22 tests, tag.mx 98%, tags.mx 100%) — the pure-logic packages the relay depends on that had no direct test at all.

The tag tests found two production bugs, both fixed here with the assertions kept: (*T).Unmarshal accepted a closing ] with no opening [, and (*S).GetTagElement(Len()) indexed one past the end and panicked.

The integration harness dropped bytes read past the WS upgrade

_ws_handshake (and the smoke test's inline handshake) read the 101 response with recv(4096). When the relay coalesced the NIP-42 AUTH challenge into the same segment, those bytes were read into the response buffer and discarded, so the challenge was lost and _drain_until(ws, "AUTH") timed out on roughly 1 connection in 100. That is the intermittent test_unauthed_event_rejected / test_free_writes_then_rate_limit failure: the relay had sent AUTH, the reader threw it away. It reproduces on both the current relay and one built before the defer-frame fix (2/300 misses each), so it was never a relay bug. The bytes after the header are now kept and consumed before recv; 0/300 misses and the full pytest suite is green. smesh 190f2deb.

Coverage matrix

Verdicts are from make test-unit (see the runner for the exact command).

packageunit testintegrationverdict
. (root main.mx)main_test.mx + main_session_test.mxthe relay itselfpass (26, main.mx 20.3% -> 59.6%; the network helpers run against loopback WebSocket peers)
pkg/errorserrors_test.mx + errors_paths_test.mxrelay error pathspass (9, 98.3% of errors.mx)
pkg/nostr/hexhex_test.mx (new)via event serializationpass (3)
pkg/nostr/varintvarint_test.mx (new)via event tags/intspass (3)
pkg/nostr/intsints_test.mx (new)via event JSONpass (5)
pkg/nostr/timestamptimestamp_test.mx (new)via event JSONpass (5)
pkg/nostr/eventevent_test.mx (repaired) + event_paths_test.mxpolicy/roundtrippass (15, 91.8% of the package)
pkg/relay/ratelimitratelimit_test.mx (new)free-write limit testpass (5)
pkg/findfind_test.mx (repaired)not on the request pathpass (4)
pkg/modemode_test.mx (repaired)every ACL checkpass (1)
pkg/crawlercrawler_test.mx (repaired)not on the request pathpass (3)
pkg/grapevinegrapevine_test.mx (repaired)not on the request pathpass (3)
pkg/storeengine_test.mx + 7 subpackage filesevery querypass (23 + 47), engine.mx 92.3%, sorted.mx 87.8%, wal.mx 81.0%, index/keys.mx 100%, serial.mx 100%, checkpoint.mx 88.4%
pkg/relay/pipelinepipeline_test.mxevery writepass (27), 95.5% of pipeline.mx
pkg/nostr/wsclient_test.mx, client_session_test.mx, ws_frames_test.mx, dns_test.mxrelay does not import itpass (20, against loopback servers; client.mx 97.8%, ws.mx 75.2%, dns.mx 56.9%)
pkg/sync/negentropynegentropy_test.mxcmd/sync onlypass (29), 90% of negentropy.mx
pkg/mediaproxymediaproxy_test.mx + mediaproxy_http_test.mxmedia proxy workerpass (22, 88.7% of mediaproxy.mx; the HTTP half runs against a scripted loopback origin)
pkg/blossomblossom_test.mx + blossom_serve_test.mxblob routespass (14, 99.1% of blossom.mx; the CORS-proxy fallback runs against a loopback origin)
pkg/lollol_test.mxlogging8/9 pass; TestErrorf blocked by the fmt multi-verb bug
web/common/marmotvarint_test.mx + codec_test.mxbrowser modulepass (18, the AEAD round trip is asserted again; marmot.mx 61.5%)

Total after this audit: 91 test functions passing, 11 packages at the time (errors, find, mode, nostr/event, nostr/hex, nostr/ints, nostr/kind, nostr/tag, nostr/timestamp, nostr/varint, plus the repaired suites); the loop now runs 49 packages. Before it, exactly one of the twelve pre-existing unit-test files compiled and passed (pkg/errors); the other eleven had never run.

The crawler/grapevine pair now reaches IR emission and stops in clang:

% moxie test ./pkg/crawler
./tmp/mxc-testbuild-*/._pkg_crawler.ll:1772:40: error: '%t18' defined with
  type '{ { ptr, i64, i64 } }' but expected '{ ptr, i64, i64 }'

The function is makeRelayList in the test file: a variadic ...string parameter whose element is converted with []byte(r) and pushed into a *tag.S through push(*tags, ...). The emitted value is a one-field struct wrapping the slice where the slice itself is expected — the string/[]byte unification losing a wrapper level in the variadic path. It is a compiler bug, not a test problem, and it predates the rewrite: the test only failed earlier (TempDir) before reaching emission.

Integration coverage (pytest)

filewhat it validates
test/test_relay_policy.py24 tests: health, name, CORS, connection/per-IP caps, bot user-agents, publish/query, unauthenticated writes and REQs, free-write rate limit, excess subscriptions, result caps, live delivery, replaceable kinds, ephemeral events, NIP-09 delete, NIP-42 challenge/auth, bad signatures, future timestamps, CLOSE, COUNT
test/test_relay_roundtrip.pypublish/receive, live subscription, feed, localhost relay, ordering
test/test_smoke.pyindex, wasm host, css, service worker, NIP-11, SPA fallback, REQ/EOSE
test/test_signer.py16 tests over the browser signer (BIP-340 vectors, nsec login, vault export/import)
test/test_app.py, test/test_infinite_scroll.pyfront-end flows served by the relay
test/test_http_proxy_worker.py14 tests over the proxy worker shim
test/test_mls_broadcast.py, test/test_mls_dm.pyMLS messaging paths
test/test_sab_verify.pySharedArrayBuffer verification shim
test/test_memory_profile.pyallocator instrumentation against a real profile

Not covered by the integration suite because they are not on the relay's request path: pkg/crawler, pkg/find, pkg/grapevine (now unit-tested), pkg/nostr/ws (client), pkg/sync/negentropy (used by cmd/sync), the cmd/* tools, and the non-relay web/ modules.

Measured coverage (integration run)

make cover builds the relay with -cover and runs the integration suite against it; the compiler's site table plus the runtime's counters give per-file statement coverage of this module. The latest run:

sites: 8733 covered: 4028 (46.1%)

46.1% of the relay module's statements run under pytest test/ (the run-to-run spread is a handful of sites, because a few worker domains are timing dependent). That number is only the integration half: the unit tests are separate binaries with their own site tables, so their hits are not in this file. The table below is the integration per-file view - the work list, because it says which code has never executed under pytest; the unit measurement of the same files is in "Unit coverage" further down, and several files here read 0% precisely because their coverage lives there. Files at 0% are dormant or untested, not necessarily wrong - several are behind config defaults that the suite does not turn on.

filecoverednote
main.mx9.8% (69/697)the relay main, plus the sync/crawl/env/proxy subcommands
pkg/access/access.mx21.4% (3/14)
pkg/acl/acl.mx11.1% (1/9)ACL mode dispatch: ORLY_ACL_MODE=none by default
pkg/acl/follows.mx0.0% (0/45)follows ACL, off by default
pkg/acl/social.mx0.0% (0/51)social/grapevine ACL, off by default
pkg/blossom/blossom.mx2.4% (3/123)covered by its unit test (99.1%), not by pytest
pkg/broadcast/broadcast.mx68.5% (24/35)
pkg/grapevine/grapevine.mx0.0% (0/82)ORLY_GRAPEVINE_ENABLED=false by default
pkg/lol/chk/chk.mx33.3% (2/6)
pkg/lol/errorf/errorf.mx0.0% (0/6)logger sub-package
pkg/lol/log/log.mx0.0% (0/2)logger sub-package
pkg/mediaproxy/mediaproxy.mx0.0% (0/187)media proxy worker; pytest drives no /proxy/ media request, its unit test covers 88.7% through a scripted loopback origin
pkg/metrics/global.mx0.0% (0/9)covered by its unit test, not by pytest
pkg/metrics/metrics.mx19.7% (14/71)covered by its unit test, not by pytest
pkg/nostr/envelope/auth.mx21.4% (9/42)
pkg/nostr/envelope/envelope.mx90.0% (36/40)
pkg/nostr/envelope/event.mx42.8% (18/42)
pkg/nostr/envelope/req.mx34.6% (9/26)
pkg/nostr/envelope/simple.mx31.1% (52/167)
pkg/nostr/event/binary.mx69.6% (69/99)
pkg/nostr/event/canonical.mx93.1% (27/29)
pkg/nostr/event/event.mx72.6% (157/216)
pkg/nostr/event/sign.mx38.2% (13/34)
pkg/nostr/event/verify.mx40.0% (8/20)
pkg/nostr/filter/filter.mx44.3% (121/273)
pkg/nostr/filter/filters.mx39.2% (22/56)
pkg/nostr/filter/skip.mx1.1% (1/89)
pkg/nostr/hex/hex.mx70.5% (12/17)
pkg/nostr/ints/ints.mx84.8% (56/66)
pkg/nostr/kind/embed.mx4.1% (9/216)
pkg/nostr/kind/kind.mx83.1% (148/178)
pkg/nostr/kind/kinds.mx40.0% (28/70)
pkg/nostr/signer/p8k/p8k.mx15.0% (14/93)
pkg/nostr/tag/tag.mx43.7% (73/167)
pkg/nostr/tag/tags.mx50.0% (51/102)
pkg/nostr/text/escape.mx30.1% (32/106)
pkg/nostr/text/helpers.mx58.9% (105/178)
pkg/nostr/text/wrap.mx37.0% (20/54)
pkg/nostr/timestamp/timestamp.mx19.5% (8/41)
pkg/nostr/varint/varint.mx92.3% (12/13)
pkg/nostr/ws/client.mx0.0% (0/95)WS client: not used by the relay
pkg/nostr/ws/dns.mx0.0% (0/127)WS client DNS
pkg/nostr/ws/ws.mx0.0% (0/184)WS client
pkg/pool/pool.mx83.3% (5/6)
pkg/relay/config/config.mx51.1% (323/631)covered by its unit test (99.6%), not by pytest
pkg/relay/dbengine/dbengine.mx80.7% (126/156)
pkg/relay/mute/mute.mx31.8% (14/44)
pkg/relay/pipeline/pipeline.mx66.4% (89/134)
pkg/relay/ratelimit/ratelimit.mx76.1% (16/21)
pkg/relay/server/server.mx79.7% (322/404)
pkg/relay/server/server_subs.mx43.7% (59/135)
pkg/relay/server/server_workers.mx33.0% (66/200)
pkg/relay/tree/messages.mx84.4% (136/161)
pkg/relay/wire/blossom_worker.mx16.6% (6/36)now reports; no blossom request in the suite
pkg/relay/wire/broadcast_worker.mx83.7% (67/80)covered by its unit test (97.5%)
pkg/relay/wire/ingest_worker.mx90.9% (30/33)covered by its unit test (100%)
pkg/relay/wire/proxy_worker.mx15.3% (4/26)now reports; the dispatch block is never taken
pkg/relay/wire/wire.mx35.4% (117/330)covered by its unit test (93.3%), not by pytest
pkg/store/checkpoint/checkpoint.mx62.7% (32/51)
pkg/store/engine.mx58.4% (405/693)
pkg/store/index/keys.mx73.8% (110/149)
pkg/store/serial/serial.mx74.0% (20/27)
pkg/store/sorted/sorted.mx70.4% (289/410)
pkg/store/wal/wal.mx40.8% (71/174)
pkg/transport/http.mx67.6% (140/207)
pkg/transport/transport.mx63.0% (208/330)
pkg/transport/ws.mx76.9% (87/113)

Five rounds of coverage work past the first measurement have moved the unit numbers per file: pkg/nostr/event 67.8% -> 91.8%, pkg/errors 71.2% -> 98.3%, pkg/nostr/ws 30.5% -> 74.4% (client.mx 33.6% -> 97.8%, ws.mx 75.2%, dns.mx 56.9%), pkg/mediaproxy 48.6% -> 88.7%, pkg/blossom 69.9% -> 99.1%, pkg/store/engine.mx 87.6% -> 92.3%, pkg/store/sorted.mx 87.8% -> 93.4%, pkg/store/wal.mx 81.0% -> 85.9%, pkg/store/checkpoint/checkpoint.mx 82.4% -> 88.4%, the pkg/nostr/envelope files 92.3-95.2% -> 95.2-97.6%, the root main.mx 19.7% -> 59.6% (its network helpers run against loopback WebSocket peers) and web/common/marmot 15.0% -> 61.5%. Across the unit-cover loop (one run per *_test.mx, 54 runs over 39 distinct packages) the 63 non-test own files carry 7204/7919 sites = 91.0% (86.5% before the main.mx round, 90.0% and then 90.5% in the two rounds after it; a package's own sources only, because the harness's per-package tables also name the closure). The largest remaining blocks are the relay server package's tail, the pkg/store tails (wal.mx's segment rotation needs a 4GB segment), the root main.mx subcommands that loop forever or spawn the binary, and dns.mx's dnsLookup: the UDP exchange needs a nameserver, so it stays integration-only.

Whole-package numbers are not in the table where a package has one file only (pkg/mode, pkg/nostr/varint, ...); the report prints every file the site table names.

How the measurement works, and what it cannot see

(legacy). A line with two statements has two sites; a block that never ran has one.

taken at fork, so each domain appends its own dump to MOXIE_COVER_OUT when it exits (spawn.mx, pipe_channel.mx). Without that, the store, the ingest and the proxy domains - most of the relay - would report 0%.

fork are counted once per domain. The report only asks whether a site ran, so a count is inflated but coverage is not.

exit; pytest sends SIGTERM first, which the relay handles. A worker domain that never returns from its loop gets no such chance: the fixture's SIGTERM reaches the group, the worker ignores it (its loop only watches its channel) and SIGKILL follows. That is why the store domain, which exits when its channel closes, reports 59-94% while the ingest, proxy, blossom and broadcast workers report ~0%.

early at startup for that test's port (ConnectionRefusedError, with Engine.Open's step trace in the captured output); the same 24 tests pass alone against the same binary and the retry ran 82 passed. pkg/store/engine.mx still carries the os.Stderr.Write trace that failure printed (store.Open: opening index eid / OK eid, 17 calls), which is debugging left in the boot path and should come out with the next round's verification.

nil check and the table is allocated lazily on the first hit.

test_relay_policy.py and test_http_proxy_worker.py hardcoded smesh, so the first measurement (37.5%) ran 38 of 77 tests against the plain relay and reported it as the suite's coverage. Fixed in those two files.

its poll initialized quietSince = eoseAt, so a 2s gap with no event counted as quiet and the run reported zero events (deterministically against smesh-test, about 1 isolated run in 4 against smesh). It now waits for the first event up to PROXY_VOLUME_WINDOW_MS and passes against both; the cover target sets 180000 and runs it. make cover exit 0 with the test included: sites 8733 covered 4028 (46.1%).

Unit coverage added after the measurement (not in the table above)

The integration table above is make cover's number, which only counts what the pytest suite exercises. The unit suites written since then cover their packages directly. make unit-cover runs the same package loop as make test-unit with -cover, and the harness prints the report itself: moxie test -cover writes the site table, starts the instrumented test binary and joins the two with pkg/mxcover (the same implementation the mxcover command uses), so no external tool is needed. The percentages below are from that report:

fileintegrationunit
pkg/relay/config/config.mx51.1%99.7%
pkg/relay/wire/wire.mx7.5%93.3%
pkg/relay/wire/ingest_worker.mx0%100.0%
pkg/relay/wire/proxy_worker.mx3.8%65.4%
pkg/relay/wire/blossom_worker.mx0%94.4%
pkg/relay/wire/broadcast_worker.mx2.5%97.5%
pkg/acl/acl.mx / follows.mx / social.mx11% / 0% / 0%100.0% / 97.8% / 95.1%
pkg/relay/mute/mute.mx30.9%93.2%
pkg/metrics/metrics.mx / global.mx19.7% / 0%98.6% / 100.0%
pkg/access/access.mx21.4%100.0%
pkg/pool/pool.mx33.3%100.0%
pkg/blossom/blossom.mx2.4%69.9%
pkg/nostr/text/escape.mx / helpers.mx / wrap.mx30% / 58% / 37%93.4% / 94.9% / 100.0%
pkg/nostr/envelope/auth.mx / event.mx / req.mx / simple.mx21% / 40% / 35% / 31%95.2% / 97.6% / 96.1% / 96.4%
pkg/store/engine.mx59.3%92.3%
pkg/store/sorted.mx / wal.mx / serial.mx / index/keys.mx / checkpoint.mx72% / 41% / 74% / 94% / 63%93.4% / 85.9% / 100.0% / 100.0% / 88.4%
pkg/relay/pipeline/pipeline.mx65.6%97.8%
pkg/sync/negentropy/negentropy.mxnot in the relay93.7%

The relay's spawn domains now report too: coverDump writes one line per write(2) (a shared append file with five writes per line interleaved inside lines, so 1846 of 9053 lines parsed and every worker file read 0%), and the broadcast domain is spawned after the signal handlers are installed. 16 of 16 domains dump and every line parses.

The gap plan

Ordered by how much untested code each item unlocks, with the shape of the test that would reach it:

#targetuncoveredtest
1pkg/nostr/kind/embed.mx, kinds.mx250done: 39 unit tests, 99-100% of the three files
2pkg/nostr/filter (filter, filters, skip)298done: 21 unit tests, 98-100% of the three files (and the key-dispatch bug they found)
3pkg/nostr/tag (tag, tags)235done: 22 unit tests, 98-100% of the two files (and two bugs they found)
4pkg/nostr/text (escape, helpers, wrap)195done: 19 unit tests, 93-100% of the three files
5pkg/mediaproxy187done: 22 unit tests, 88.7% - a scripted loopback HTTP origin now drives the request line, the status and header parse, all three body framings, redirects and the in-connection error returns, not just the pre-dial rejections
6pkg/acl (acl, follows, social)140done: 11 unit tests, 95-100% of the three files
7pkg/relay/wire workers174done: 28 unit tests plus a run with ORLY_INGEST_WORKERS=2; 93-100% of wire.mx and the four domains
8pkg/store (sorted, index, wal, engine)340done: 78 unit tests across the sort/index/wal/engine primitives (no TempDir: os.MkdirTemp + defer), 81-100% per file. store_paths_test.mx drives every failure return of Engine.Open (each index file, the WAL directory, a segment, the checkpoint, MkdirAll) and the stale-checkpoint rebuild from the WAL; checkpoint_paths_test.mx covers Open's read error and both Set failure paths
9pkg/relay/server (server, _subs, _workers)400done as far as the harness allows: 3 test files drive subscription edge cases, the connection lifecycle and the worker restart; the rest needs the integration suite
10pkg/relay/config309done: 11 unit tests over Load with a controlled environment, 99.6%
11main.mx (sync, crawl, env, proxy subcommands)629done as far as the harness allows: main_test.mx covers the pure helpers, the relay database and clog, and main_session_test.mx drives syncOnce, outboxDiscover, outboxBootstrapRelayLists, crawlRelay, crawlPublishBatch and crawlPass against loopback WebSocket peers (13 new tests, 26 in the package, main.mx 20.3% -> 59.6%). The rest is runRelay/runSync/runOutbox/runCrawl (infinite loops), spawn* (they exec the binary), handleProxy (TLS fetch) and outboxBootstrapFollows (a fixed real seed list), so they stay integration-only
12pkg/nostr/envelope (auth, simple, event, req)230done: 20 unit tests, 96-98% of the four files, NIP-42 AUTH included; the truncated-input test covers every parser's rejection paths (and found the truncated kinds array that crashed the relay)
13pkg/grapevine, pkg/nostr/ws, pkg/nostr/signer/p8k250done: grapevine 3 tests, ws loopback tests (20, client.mx 33.6% -> 97.8%), p8k 9 tests including BIP-340 vector 0 (96.7%)
14logger, metrics, access, pool100done: metrics 9 tests (98.5-100%), access 4 (100%), pool 3 (100%)

Open items (each with its repro)

Everything that this audit found and the tree still gets wrong. The items the audit found and fixed are in the next section; the ones that were on this list and are now gone from it (range over a string, strconv bitSize=64, the untyped-constant argument width, mute.Load, error == syscall.Errno, ws loopback dial, pkg/lol, the worker-domain coverage dump, the store/pipeline/ negentropy test rewrites) have their evidence recorded as new rows in "Compiler bugs this audit found and fixed".

itemevidencenext step
direct messaging is parked in the appthe AEAD fix (below) made the MLS worker reach generating new KPP, where it then panicked in Moxie's codec: codec bad type: kind=0 size=0, WASM fatal - restarting. The entry points were gated off again (sidebar, /msg routes, mlsWorkerTypes, M_SET_PUBKEY, build-mls-wasm out of the default targets, mls.wasm not served) with the mls code, worker and host plumbing left in the tree. Verified with the headless boot probe: the boot list is verify/store/rproxy/signer/app/profile/feed/notif/domain ok (no mls entry), /msg/<npub> lands on the app shell, and the console has zero mls or panic linesfix the codec bug in the row below, then restore the entry points and drop the two pytest skips together
the wasm MLS worker panics generating its first KeyPackagethe app's DM page now opens and the worker starts ([mls-worker] handleInit: generating new KPP), then Moxie's codec aborts: codec bad type: kind=0 size=0 and WASM fatal - restarting, so no kind-443 event is ever published and test_mls_dm.py/test_mls_broadcast.py stay skipped. Traced with a temporary ring buffer in codecEncodeValue: the eight instructions before the panic are KindPointer sz=4 (wasm32) and the ninth is the invalid type, i.e. some pointer-to-X whose element descriptor is nil in a wasm build. The AEAD itself is fine now (web/common/marmot's round trip runs and passes natively)narrow the offending type by printing the parent chain with types (the codec has no names) or by dumping the wasm build's type descriptors for web/common/mls; then fix the emitter of the zeroed descriptor. Re-enable the two pytest modules when it lands
dialing any wss:// URL aborts the processa 10-line program that calls ws.Dial("wss://purplepag.es") prints trying wss://purplepag.es and then dies: panic: runtime error at 0x...: interface dispatch: no impl for Write (in a -cover test binary the same call dies with caught signal SIGSEGV). ws:// to the same host and port is fine, wss://127.0.0.1:1 fails cleanly at the dial, and the same source built by the compiler from before this round's shift fix aborts identically, so it is not this round's change; the build log's discover crypto/tls lists every file, so the package is in the closure. Consequence: smesh crawl, smesh sync wss://... and the outbox bootstrap (which dials wss://purplepag.es) crash the process, and no test reaches themthe *tls.Conn that tls.Client returns is assigned to the net.Conn variable in ws.Dial (pkg/nostr/ws/ws.mx), and the following conn.Write(req) has no arm; the cached crypto/tls .mxh does list method Conn Write []byte->int32,error and the net .mxh lists the Conn interface, so it is the pairing that emits dispIfaceMethods that misses this one - instrument that build for the pair and lock it with a loopback TLS fixture in moxie's phase6

The last row that was open before this one (a legacy-built spawn program that SEGVs after its first exchange) was fixed with the futex park: the legacy compiler passes a nil destination pointer for a discarded receive (<-done with no assignment) while stage4 passes a scratch, and the runtime wrote the delivered value through it. recvStore/recvZero now treat a nil destination as "discard", and phase6 6k runs under both compilers. moxie 39b164ac.

testing.T.TempDir/Cleanup is a deliberate non-addition, not a defect: a cleanup list has to live in root-arena memory or in a sovereign holder, and a cleanup closure cannot capture its test's locals. defer is the Moxie shape, and testing.Short() was added because it is stateless.

Spawn channel parking (runtime)

L21 says the index word is simultaneously the state and the notification, and that the parking primitive is a futex on that word with FUTEX_WAIT's expected-value check closing the check-then-sleep race. The spawn ring path did not do that: PipeChanSend/PipeChanRecv slept 1ms and retried, and the only shared futex in the tree was PipeWaitChildReady.

The ring path now parks. ringbuf.mx gained two sleeper flags in its header (wake elision: the uncontended publish stays syscall-free) and four helpers. A receiver waits on writeIdx, a writer on readIdx, each passing the value it just loaded as FUTEX_WAIT's expectation, so a publish that lands between the check and the sleep returns EAGAIN instead of being lost. Every wait is bounded at 20ms because a crashed peer can never issue the wake; the loops recheck ring state and peer liveness after each return. ringSend/ringRecv wake the other side after advancing their index, and ringClose wakes both so a close unparks immediately. runtime_unix.mx gained futex.WaitUntilShared /WakeAllShared, which use the non-private syscalls the fork boundary needs (the private flag keys the wait queue per mm, so a child's wake can never reach a parent's wait).

Measured with strace -f -e trace=futex on tests/data/futexpark/main.mx: the child waits on the shared ring word with the current index as its expectation and no FUTEXPRIVATEFLAG, and both sides issue FUTEX_WAKE on the same address.

A select over several channels deliberately keeps the bounded timer poll (chan.mx): it cannot park on one ring's index without going deaf on the others, and it must keep servicing timers while it waits. Parking is sound in the dedicated single-exchange domains, which is where the send/recv helpers run.

Two tests are owed by the change and both live in tests/data/futexpark + phase6 6k (both compilers): 200 park/publish rounds assert no wakeup is lost, and a parent parked on a ring whose child exits unpacks, reports the closed channel and finishes well inside a 3s bound. moxie 7def0c30.

Compiler bugs this audit found and fixed

bugevidencefix
select bound the comma-ok name to the received valuecase ev, ok := <-events: with chan *T typed ok as *T, so if !ok negated a pointer and clang rejected the module (%t83 = xor ptr %t82, -1); reduced to a ~40-line probe with one receive case and two closed-channel casesthe pair now extracts the value from the receive slot and ok from the tuple's recvOk slot (pkg/nostr/ws compiles and runs; previously it could not build)
Engine.GetByID answered event not found for stored eventsit scanned MakeEid(hash,0)..MakeEid(hash,Max), 16 bytes of bounds over an index that compares only 11 (EidCmpLen), so the range was empty; negentropy.Syncer.FindHave calls it and dropped every event, sending nothingpoint lookup through getEventSerial + GetBySerial, the shape queryByIDs was already moved to; the store tests now assert a stored event and an unknown id
a delete tombstone was invisible to point lookupssorted.File.Get read the on-disk record without consulting the delete set that Scan honours, so QueryEvents(&filter.F{Ids: a.ID}) still returned a deleted event while a filter-less query hid itGet skips tombstoned records in the main file, the .buf sidecar and the in-memory buffer; asserted before and after Flush
a from-source build of pkg/find/pkg/grapevine failed while a cached one succeededstage4's prepareDispatchIndex pre-pass skipped a package whose .mxh was cached (continue) instead of loading it. Packages run in dependency order, so when pkg/store had to be re-type-checked while event/filter were still cache-only, its signature was generated before those exports were in the registry and generateMxh wrote __any for the unresolved types (method Engine QueryEvents __any->__any,error); mxhLoaded then made the correct export a no-op, so every caller saw { i64, ptr } where a slice belongs. The whole "IR-emit bug" in crawler/grapevine was this plus one test bugprepareDispatchIndex loads the cached .mxh (stage4-only code: legacy has no such pre-pass); pkg/find and pkg/grapevine build from source and their tests pass (4 and 3)
stage4 accepted push on a non-slice*tags = push(*tags, tag.NewFromSlice(...)) on a *tag.S compiled and emitted IR clang rejects ('{ { ptr, i64, i64 } }' but expected '{ ptr, i64, i64 }'), which read as a variadic/IR-emit bug in pkg/crawler and pkg/grapevine. Legacy rejects it: "push on non-slice type"stage4's push lowering now checks the operand is a slice or basic and reports legacy's error; the two test helpers pushed onto the struct instead of its T slice, and pkg/crawler comes off the skip list (3 tests pass)
a duplicate top-level declaration was accepted silentlyfunc main() twice in one file compiled and ran the first body while the second was dropped; legacy reports main redeclared in this block. It also made moxie test on a command-line tool run the tool and never the testsregisterFunc records every declared name and rejects a repeat with legacy's wording; concatTestSources renames the tested package's own main out of the way so the generated test runner owns the entry (moxie test ./cmd/mxcover: 9 tests)
a duplicate method is still accepted silentlyfunc (t *T) M() declared twice compiles and x.M() returns 1; legacy reports method T.M already declared. The fix above covers top-level functions onlyextend the same declared record to receiver-qualified method names
the package cache key ignored -covera plain moxie test after a -cover run reused the instrumented bitcode and dumped counters to stderr; a cover build could as easily have reused plain bitcode and reported nothingpkgCacheKey salts the hash when bst.cover (legacy already had it: compiler.Config is part of the action ID)
moxie test judged the tested package by its synthetic main wrappermoxie test ./pkg/nostr/kind failed with 129 package globals are immutable outside init errors on code that a normal build compiles, because the harness compiles the package as main and isUserPackagePath("main") is user code while .../pkg/nostr/kind is exemptcmdTest records the path a normal build would use (bst.testSrcPkgPath) and restrictPath maps the wrapper back for every restriction check; testWrapsLibrary likewise stops init() is not allowed in main package firing on a library's own init()
range over a string loaded a 4-byte word per bytethe loop value was loaded as i32 for every element type, so for i := 0; i < len(s); i++ { s[i] } inside a range was fine but for _, c := range s read four bytes and sign-extended. Consequence: pkg/blossom's isHex rejected every real 64-hex hash (all blob GET/HEAD answered 404) and net/url.validOptionalPort rejected http://host:8080 as an invalid port_mxc_stage4/ir_iter.mx emitNextSlice loads an i8 for a string element and zexts it to the tuple's i32 value slot; blossom's blob GET/HEAD now returns the blob and the host:port upstream test passes
strconv.ParseInt/ParseUint returned 0, nil for bitSize=64ParseUint("123",10,64) was (0, err) while Atoi("123") was 123: maxVal := uint64(1)<<uint32(bitSize) - 1 needs Go's rule that a shift >= the operand width yields 0, and stage4's codegen let the x86 backend mask the count (64 became a shift by 0), so maxVal read 0 and every digit tripped the range check; on top of that const maxUint64 = 1<<64 - 1 folded as a 64-bit signed value, so maxUint64/10 was 0 instead of 1844674407370955161. Consequence: pipeline.isExpired never returned true and NIP-40 expiration was unenforced, and every Content-Length/chunk-size body read as emptyemitShift materialises Go's shift rule (a constant count >= the width folds to 0; a dynamic count gets an explicit overshift select, with ashr capping at width-1 for the sign fill), and ConstInt carries an Unsigned mark so an untyped constant whose exact value is >= 2^63 folds its divide/remainder/right-shift unsigned. ParseInt/ParseUint at 64 bits, both range ends and out-of-range rejection now match Go; pipeline's expiry assertion and mediaproxy's decoded chunked body are asserted rather than commented
an untyped constant argument to a typed parameter was emitted 32 bits widefunc id(n int64) (r int64) { r = n; return } then id(-42) printed 4294967254; var v int64 = -42 and int64(-42) were correctcall arguments are converted to the parameter's width (callArgTexts/coerceIntArg), so id(-42) is -42
mute.Blacklist.Load never loaded a real mute listit accepted only a 32-byte raw or 64-byte hex p-tag, but tag.Unmarshal binary-optimises a 64-hex p-tag to 33 bytes and it stays 33 through the store round trip, so every real list was skipped and nothing was mutedaccept the 33-byte binary form; mute_test.mx asserts a 33-byte p-tag is loaded
stage4 compiled error == syscall.Errno to falsevar e error = syscall.Errno(syscall.EINPROGRESS); if e == syscall.EINPROGRESS took the else arm while Go and legacy match, so src/net/fd_unix.mx's connect treated a non-blocking connect's EINPROGRESS as fatal and net.Dial answered connect: operation now in progress for every TCP dial, loopback includedinterface-vs-concrete ==/!= boxes the concrete side and calls runtime.interfaceEqual; net.Dial reaches loopback and the pkg/nostr/ws loopback tests pass as written
a stage4-built program printed a phantom %!(NOVERB) after every formatted valuefmt.Sprintf("%d", int64(-42)) printed -42%!(NOVERB) where the same source built by legacy printed -42; the relay log carried ... interrupted system call%!(NOVERB)%!(NOVERB)a labelled continue in fmt's scanner ran the inner loop's post statement when the format was exhausted; buildBranch now honours the label, and pkg/lol's 9 tests pass
worker-domain coverage was lost when a domain was killedingest_worker.mx, broadcast_worker.mx, proxy_worker.mx and blossom_worker.mx reported 0% while the store domain reported 52-66%it was not a missing dump path: coverDump issued five write(2) calls per line onto one shared append file, so lines interleaved and only 1846 of 9053 parsed; each line is now assembled in one buffer and written once. The broadcast domain was also forked before InitSignals(), so the fixture's group SIGTERM killed it before it could dump - server.InitSignals() now runs before broadcast.PreSpawn(). 16 of 16 domains dump and every line parses
a standard-library import path could resolve into a nested modulea build failed in crypto/hkdf with a clang error on extractvalue {ptr,i64,i64} %t14 from [32 x i8]: discoverPkg/resolveDir looked for <pkg> relative to the module first, so crypto/hkdf resolved to web/common/crypto/hkdfboth compilers resolve root/src/<pkg> before the module-relative fallback (bilateral: main.mx and legacy/loader/mxlist.go)
a re-exported constant lost its value through the .mxh round tripx := uint64(math.MaxUint64) then x == 0 was true, and every negative exported constant read back as 0: ConstInt.String printed the signed -1 for the unsigned-marked maximum, and ssaParseInt64 stops at any non-digit, so the reader saw -1 and parsed 0; a value above int64 also wrapped without its unsigned mark, so math.MaxUint64/10 folded as -1/10 == 0. In the store this was not cosmetic: flushSidecars seeds its minimum with that constant, so the minimum read 0 and writeSidecars set the checkpoint back to 0 on every incremental flush - TestIncrementalRecovery had been written around it with a manual ckpt.SetConstInt.String prints the exact unsigned decimal when the constant carries the unsigned mark, and the .mxh reader parses the sign and the full 64-bit range, marking a value above int64 unsigned. uint64(math.MaxUint64), var y uint64 = math.MaxUint64, math.MaxUint64/10 and math.MaxUint64>>32 are all exact now, and TestIncrementalRecovery drives the real quickCheckpoint and asserts the checkpoint it writes before the crash and the one recovery advances it to

| the math arch asm was declared but not callable | math.Ceil(2.1) killed the process (caught signal SIGSEGV) while math.Sqrt(4) was 2; math.Floor/Trunc/Exp/Hypot/Log/Max/Min/Modf were equally unusable. src/math/*_asm.mx set haveArch* = true for amd64, and sysroot/obj/asm_math_*.o (ported from Go in sysroot/obj/src/*_gas.s) read the argument from 8(%rsp) and wrote the result to 16(%rsp) - Go's stack ABI, while the compiler emits the register convention its runtime asm uses (moxie_longjmp takes its pointer in %rdi). The call returned garbage and wrote 8 bytes above the caller's frame. Legacy never linked those objects, which is why the same source worked there | the seven *_asm.mx files and the unusable objects are deleted, and the portable files that define haveArch* = false are no longer arch-gated, so both compilers run the portable implementations | | every floating-point constant that went through the .mxh | six separate faults, each reproduced by comparing stage4 with legacy on one probe: typeDescWrite wrote every untyped kind as int32, so const MaxFloat64 untyped_float ... came back an integer constant; ConstFloat.String returned "0.0" for any constant folded from an expression, so math.MaxFloat64 was imported as 0; parseFloatLocal had no hex-float support (0x1p1023 parsed as 0) and built decimal exponents by repeated multiplication; NormalizeLLVMFloat appended ".0" to +Inf and to 0x1p1023, which clang rejects (bitcast double +Inf.0, store double 0x1p1023.0); 1 - 0x1p-52 folded in integer space because the fold tested the left operand first, so math.MaxFloat64 = 0x1p1023*(1+(1-0x1p-52)) folded to 2^1023*2 and overflowed to +Inf; and a hexadecimal float needed the bit-pattern form LLVM accepts | untyped kinds have their own .mxh tokens and are mapped back; ConstFloat.String writes a text strconv.ParseFloat round-trips; hex floats are parsed with strconv.ParseFloat and emitted as 0xK...K; Inf/NaN are emitted as their hexadecimal bits; an integer operand against a float operand is promoted before the fold | | an untyped numeric constant was stored or passed at its own width | var a float64 = 4 stored the integer bit pattern 4 into a double slot (a printed 2e-323, math.Float64bits(a) was 4, and math.Sqrt(4) answered 0 because sqrt reads Float64bits); math.Sqrt(4) passed the integer 4 where a double parameter expected it, so the callee read 0 and Sqrt/Pow/Hypot answered f(0) while Exp(1) answered 1; x &^= 1<<(64-12-e) - 1 folded its mask at the untyped i32 default (0xFFFFFFFF), so math.Modf(2.1) returned 2.0999984 and math.Ceil/Floor returned x±1 | emitStore converts an untyped numeric constant to the destination's type, checkCallArgs converts an untyped numeric argument to the parameter's type, and a compound assignment gives its shift operand the assignment target's type | | the Node signer harness could not instantiate the signer wasm it is given | after the artifacts were refreshed, make test-signer failed at WebAssembly.instantiate: the module imports bridge.channel_close/channel_recv/channel_send, bridge.signer_signal_ready and wasi_snapshot_preview1.fd_read, and web/_test/signertest/run.mjs supplied none of them ("function import requires a callable"). The harness had not been run against a current artifact since the rebuild | the harness now supplies the spawn-channel ABI (close is a no-op, send/recv throw so a future spawn cannot silently do nothing), the readiness callback and an EOF fd_read; its wrong-password assertion checks the error the signer actually returns (mgmtUnlockVault surfaces the hash diagnostic instead of a bare false). make test-signer 108/108 | | an untyped constant that does not fit 32 bits | Moxie's int is 32 bits on every target, so these must be exact where a type is given and an error where the default type applies; stage4 evaluated them at the untyped 32-bit default and truncated. 0 - 9223372036854775807 printed 1, math.MaxUint64 / 10 printed its low half (-1717986919), math.MaxUint64 >> uint32(32) printed -1, and 10.0 >= 1 << 63 was true - which sent math.Pow down its overflow branch for every integer exponent (math.Pow(2, 10) = +Inf). Legacy rejects the untyped forms ("overflows int") and answers the typed ones exactly | the emitter materialises an unsigned-marked untyped integer at i64 and runs an untyped operation at the wider of its operands; ssa_builder.mx checkDefaultRepr reports an untyped constant that does not fit the type it takes (interface argument, :=, var x = ...) with go/types' verdict; cvFloat and the integer/float promotion use the unsigned value, and an integer literal whose exact value is >= 2^63 is marked unsigned like a folded one. Probes agree with legacy line for line, and math.Pow(2,10) is 1024 | a parameter named b1 collided with a generated block label | LLVM gives blocks and values one namespace, and stage4 named its blocks b1, b2, ... and _entry, so func f(b1 byte) failed with "'%b1' is not a basic block" at br label %b1 (a parameter named _entry failed with "unable to create block named 'entry'"). `pkg/store`'s test helper had been renamed to `k0..k3` to work around it | generated labels are dotted (`bb.1`, `bb.entry`), which no source identifier can spell; `tests/regressions/shouldcompile/blocklabelnames.mx compiles and runs a b1/b2/_entry` program, and the old compiler rejects it | a duplicate method was accepted silently | func (t *T) M() declared twice compiled and x.M() returned 1; legacy reports "method T.M already declared". The top-level redeclaration check covered functions only | the check is receiver-qualified (method T.M), so T.M and U.M coexist while a second T.M is rejected with legacy's wording; tests/restrictions/reject_duplicate_method.mx locks it | a package-level var a, b T = x, y gave every name one value | var g1, g2 int32 = 3, 4 printed 4 4 and var f1, f2 float64 = 1.5, 2.5 printed 2.5 2.5 in a stage4 build - the init list was built once and its result stored to both names. The same defect in the function-scope form is what broke bytealg.HashStrBytes (row below). legacy answers the integer case correctly (3 4) but prints 0 0 for the float pair, so the two compilers are wrong in different ways and stage4 is now the correct one; that legacy divergence is recorded in the open items | both loops that build package variable initializers (__varinit and the in-place path) build one value per name, as buildLocalDecl now does; tests/regressions/should_compile/multi_value_var.mx covers int32/uint32/float64/slice pairs and the loop-body shape | | bytes.Contains/bytes.Index missed a present needle | bytealg.IndexRabinKarp (the path bytes.Index hands over to once its brute scan has failed 4+i>>4 times) answered -1 for needles that are present - 23112 mismatches against a manual scan in a sweep over offsets and buffer sizes. The cause was one line: HashStrBytes computes its rolling factor with var pow, sq uint32 = 1, PrimeRK, and stage4 built a multi-value initializer list once and stored the first value to every name, so sq was 0, the factor came out PrimeRK^14 instead of PrimeRK^13, and every rolling hash after the first was wrong | buildLocalDecl now builds one value per name for var a, b T = x, y; HashStrBytes returns PrimeRK^13 and the sweep reports 0 mismatches. src/internal/bytealg/bytealg_test.mx runs the differential sweep (Index and LastIndexRabinKarp against a manual scan, present and absent needles) in run_tests.sh, and tests/regressions/should_compile/multi_value_var.mx covers the declaration shape | secp256k1.ECDH/LiftX accepted an x coordinate >= the field prime | Signer.ECDHRaw(0xFF * 32) returned a shared secret while Signer.InitPub(0xFF * 32) was rejected: feFromBytes copies 32 bytes into the limb representation without reducing, and LiftX lifted the non-canonical value. BIP-340 requires "fail if x >= p", and vector 14 of the vendored CSV (x = p+1) only passed verification by accident | LiftX rejects x >= p with feCmp(x, _feP()); src/crypto/secp256k1/bip340_vectors_test.mx lifts p, p+1 and 0xFF32 and requires all three to fail (and the generator's x to succeed), ECDH must refuse the same key, and `p8k_test.mx` now asserts `ECDHRaw`/`ECDH` reject 0xFF32 | stage4 accepted a method call on a non-function field | sb.Info() where Info is a types.BasicInfo (uint32) field of *Basic was accepted by stage4 and only failed when legacy compiled the same file (cannot call sb.Info (variable of uint32 type types.BasicInfo)) | fixed by the reportUndefinedField fallback in ssa_builder.mx buildCall: a selector that is not a method, interface dispatch, function field or generic instantiation is reported instead of dropped. Verified on the current tree: stage4 answers ; Info undefined (type main.Basic has no field or method Info) and legacy answers uint32 is not a function | | legacy dropped every package-level float initializer | var a1 float64 = 1.5 prints 0 under legacy while stage4 prints 1.5; every package-level float32/float64 variable is affected, single and multi-value. LLVMConstRealGetDouble returns its double in XMM0, but the purego build of go-llvm read the return register RAX, so the interp's rawValue.set stored zero for every float constant. legacy is the bootstrap compiler, so any package-level float variable in the compiler or stdlib was silently zero | the glue library gains MoxieConstRealGetBits, which returns the IEEE-754 bit pattern in RAX, and Value.DoubleValue reads that. build.sh now rebuilds the glue when a source is newer than the .so (the purego bindings resolve glue symbols with mustSym, so a stale .so is a hard failure). tests/data/globalinit plus a phase6 check asserts single and multi-value float32/float64 package globals in both compilers. moxie ea36dbac | | len()/cap() on a channel was 0 | stage4's len/cap lowering had no *TCChan arm, so it fell through to "return zero" while legacy emitted runtime.chanLen/runtime.chanCap. A buffered channel created with chan int32{4} answered cap == 0 under stage4 and cap == 4 under legacy, so any code sizing work off len/cap read the wrong ring | stage4 now emits the same runtime calls, converting the runtime's i32 to the result width. tests/data/chanlen plus a phase6 check asserts the capacity, the length of a new channel, and the length after a send and after a receive in both compilers. moxie aeda3f73 | | test_relay_proxy_high_event_volume flaked and was deselected from make cover | the poll after EOSE initialized quietSince = eoseAt, so an empty 2s gap counted as "quiet" and the run finished with eventCount: 0, gotEose: True. That is the deterministic smesh-test failure and the stock flake (about 1 isolated run in 4): the worker lingers 25s after EOSE for exactly these stragglers, so the first result can land later | the poll only treats the stream as quiet once at least one event has arrived, and waits for the first event up to PROXY_VOLUME_WINDOW_MS (default 30s, longer than the linger). make cover sets PROXY_VOLUME_WINDOW_MS=180000 and no longer deselects the test. Measured with the old poll against smesh-test: eventCount 0; with the fix it passes against both binaries. make test exit 0, make cover exit 0, sites 8728 covered 4026 (46.1%) at the time. smesh 9957fa8e | | testing.T.Fatal did not abort the test | after a Fatal the rest of the test body ran, so a failing assertion could also crash later and hide which check failed. Root cause measured: stage4 never emitted the per-call defer checkpoint. w() sniffed an emitted segment for call , but every call emitter writes the line in fragments (e.w(" ") then e.w("call ")), so the sniffer never fired and every frame's JumpPC stayed 0 - a moxie_longjmp jumped to address 0. stage4 also lowered explicit panic("const") to the abort-only _panicstr while legacy used the recoverable _panic, and the first testing-package attempt crashed because a deferred method value goes through a thunk that called a garbage pointer | join each call prefix into one segment so the checkpoint fires, suppressing it inside a multi-impl interface dispatch (the split broke the merge PHIs); lower explicit panic to _panic; declare runtime._recover as returning a string as legacy does; and make the recover handler a plain function over a package global instead of a deferred method. tests/data/fatal plus a phase6 check asserts Fatal and Skip abort the body, the run continues, and each is reported. moxie c7ca5f37. Runtime-error panics (nil deref, index out of range) still abort without unwinding in both compilers - runtimePanicAt never consults the defer frame - which is a separate item | | crawlPublishBatch SIGSEGVs nondeterministically under moxie test | crawlPublishBatch("ws://127.0.0.1:1", batch, out) with any *os.File open panicked at a fixed low address in some runs and returned 0 in others; ws.Dial alone returned the refused error cleanly, clog alone was fine, and a trivial func([][]byte) int32 was fine. It is the unchecked-defer-frame class: the function has a defer, stage4 never armed the frame's JumpPC, and a longjmp jumped to address 0 - the same defect that blocked testing.T.Fatal (moxie c7ca5f37). The exact panic site was not isolated | TestCrawlPublishBatchDialError drives the dial-error branch again and asserts it publishes nothing. 26 consecutive moxie test runs are clean (13 tests each) and make test-unit is green, so the branch is covered. smesh 387b902e | | stage4 had no clear lowering at all | clear(x) compiled to nothing. math/big's basicMul opens with clear(z[0:len(x)+len(y)]) and then accumulates into z, so a reused accumulator kept the digits of its previous product: big.Int.Exp(10,20) was 10^20+10^5, big.Rat.SetString("1.7976931348623157e308").String() was wrong, and a 300-digit Text(10) divided by a stale zero and died with SIGFPE. clear was listed in the builtin switch with close/delete/print but the emitter had no arm for it, so the call was dropped silently - the same class as the missing len(chan) arm (row above) | ir_call.mx emits llvm.memset.p0.<ptr>(buf, 0, len*sizeof(elem)) for a slice and runtime.hashmapClear for a map, mirroring legacy's inline memset; ssa_builder.mx rejects anything else before the compileErrors gate so the failure is a failed build, not a printed line after linking. tests/data/clearzero + phase6 6j (both compilers) assert whole/sub/prefix/suffix slices, multi-byte elements, nil slices, and maps; tests/restrictions/reject_clear_non_slice.mx locks the rejection; src/math/big/natconv_test.mx (+run_tests.sh) pins Exp, the 309-digit round trip and the Rat repro. moxie 7def0c30 | | stage4 typed a folded untyped int/float constant by its left operand | 1/2.0 folded to the float value 0.5 but kept the left operand's untyped-int type, so every use truncated it: 1/2.0 was 0, 7/2.0 was 3, 1/math.Ln2 was 1 and math.Log2(10) was 3.529996 instead of 3.321928 - which also mis-sized math/big's base-10 buffer (the "leading two digits gone" symptom). The non-fold path already promoted correctly; only the SSA builder's fold took xc.typ. A first cut of the fix then let a constant shift adopt its count's type, which truncated 0x20000000000000 >> uint32(3) - caught by the existing shift_const_width regression before it shipped | the fold promotes only when the left type is untyped, never for OpShl/OpShr (a shift's result type follows its left operand), and takes the folded value's kind for two untyped operands. tests/data/constpromo + phase6 6i (both compilers) assert mixed int/float division, multiplication, addition and math.Log2. moxie 7def0c30 | | the base-10 divisor table cached arena allocations in a global | var d big.Int; d.Exp(big.NewInt(2), big.NewInt(1074), nil); var r big.Rat; r.SetFrac(one, &d); r.RatString() SIGFPEd in bits.Div32 (legacy aborted with divide by zero), and 2^260.String() crashed the same way while 2^250 was fine - the break lands exactly where nat.itoa enters its recursive branch. divisors reported a divisor with 256 bits and a nonzero top word, and the very next convertWords saw the same entry with a zero top word: the table was cached in the package global cacheBase10, but the nats it holds are allocated in the calling frame's arena, so the metadata outlived the words and the next conversion divided by a recycled buffer | the table is built per conversion in the caller's arena (cacheBase10 and its ndigits == 0 extension path are gone), which is sound because the divisions it feeds dominate the squarings that build it. bits.Div32 also gained the y == 0 guard Div64 already had, so the next occurrence is a named panic instead of a hardware SIGFPE. src/math/big/natconv_test.mx TestRatSmallestSubnormal (1/2^1074, 326 bytes) plus a 250..1200-bit 2^k sweep lock it; run_tests.sh runs the suite. moxie 7def0c30 | | untyped float constants were rounded where go/types is exact | math.MaxFloat64 == 1.7976931348623157e308 was true in a stage4 build and false in a legacy build and in Go: constEqual compared the rounded float64, and the .mxh writer exported a folded constant as its shortest round-tripping decimal, so an importer re-read a value whose decimal is the literal on the other side of ==. 0.1+0.2 == 0.3 was false where Go says true (constants are exact rationals), and 1.0/3.0 == 0.3333333333333333 was true where Go says false | ConstFloat carries its exact *big.Rat: literals parse it (decimal via Rat.SetString, C99 hex floats via a hand-rolled mantissa/exponent parse, which Go's Rat does not accept), every fold is Rat arithmetic with V = Exact.Float64() for codegen, comparisons use Rat.Cmp, and a sized conversion rounds as Go does. .mxh writes Exact.RatString() for folded constants (parsed back by ExactFloatText) while String() keeps the human decimal for diagnostics. tests/data/exactfloat + phase6 6l (both compilers) assert the two MaxFloat64 comparisons, the in-package form, and the two exact-arithmetic cases. moxie 7def0c30 | | a legacy-built spawn program SIGSEGVs after its first exchange | spawn(worker, in, out) with in <- 21; <-out printed 42 and then died in runtime.memcpy(dst=0x0, size=24): main's <-done is a discarded receive, the legacy compiler's codegen passes a nil destination for the unused operand (stage4 materialises a scratch), and trySend copied the delivered value straight through it. It reproduced with either compiler's runtime bake, so it was not a blob mismatch | recvStore/recvZero in runtime.mx make a nil receive destination the defined "discard" case and are used by bufferPop, trySend, tryRecv, tryPipeRecv and PipeChanRecvRaw; phase6 6k now runs tests/data/futexpark under both compilers. moxie 39b164ac | | -cover produced both halves of a measurement but nothing joined them | make test-unit could not report coverage at all: moxie test -cover wrote the site table and started the instrumented binary, and the only join was the standalone mxcover command, so the unit table above had to be produced by driving that command from a shell loop outside the compiler. A reviewer asking "what is the coverage?" had to know the recipe | the join lives in pkg/mxcover (a package, so both callers share one implementation); moxie test -cover now points MOXIE_COVER_OUT at a per-run counts file, runs the instrumented test binary and prints the totals, per-file table and uncovered list itself, and mxcover is a thin CLI over the same package. make unit-cover is the package loop with -cover, so the report comes from the harness. Legacy's harness mirrors the print (legacy/cover, with go test ./cover asserting the shared fixture); legacy's own moxie test still cannot build these packages in this tree, which is why phase6 6m locks the mxcover command against the fixture and 6n runs the parser units. moxie 421d93b6 | | event.UnmarshalBinary accepted a truncated frame | io.Reader.Read returns whatever it has with a nil error, and every fixed-size field was read with r.Read, so a frame cut short left the rest of the field zero-filled and decoded as a valid-looking event - an ID of 20 real bytes and 12 zeros, a zero-padded signature. The JSON path length-checks every field, so only the binary path was exposed. Found by the truncation test added with this round's coverage work (pkg/nostr/event/event_paths_test.mx), which asserts every strict prefix of an encoded event fails | every field read goes through io.ReadFull (a local readFull helper), which turns a partial read into ErrUnexpectedEOF. The new test file raises the package's own-file coverage from 67.8% to 91.8% and covers Clone/Free/EstimateSize, the JSON entry points, both Verify outcomes and the sort helpers. smesh 8037fb4f | | the ChaCha20-Poly1305 AEAD dispatched to an asm implementation that does not exist | web/common/crypto/chacha20poly1305.Seal returned an all-zero buffer for every input, so Open always failed "message authentication failed": the vendored package's amd64 file declared chacha20Poly1305Seal/Open as external functions and dispatched to them whenever the CPU had SSSE3 (all of them); nothing in the tree implements them. Found by writing the RFC 8439 known-answer test for the new coverage pass - the bug had hidden behind passing integration tests because they never round-trip a ciphertext through this wrapper | the amd64 variant is deleted and the portable implementation is no longer arch-gated (the same shape as the math arch-asm row above). src/vendor/golang.org/x/crypto/chacha20poly1305/chacha20poly1305_test.mx is the RFC 8439 §2.8.2 known-answer vector plus round trips at eleven sizes, tamper/wrong-AAD/wrong-nonce rejection, short-key rejection and an XChaCha20 round trip, and run_tests.sh runs it (10 packages now). moxie bf3d24f1 | | a compiler binary that does not match the tree poisoned the newer compiler's package cache | compilerHash() keyed the cache on the compiler sources under MOXIEROOT plus src/runtime - deliberately not on the running binary, so the self-host fixpoint can converge - so an older binary run against a tree whose sources changed computed the NEW source hash and wrote objects it compiled under the OLD sources into the directory the new compiler read. Measured while narrowing: after adding a field to a struct and rebuilding a dependent without -a the dependent was correct (the dep hashes are in the key); the compiler-binary mismatch is what remained | fold a content hash of the running binary into the cache DIRECTORY name only, never into generated code, so identical generations still emit identical binaries. phase6 cacheid:stage4 copies the compiler, appends a byte, and asserts the mxc env CACHE path differs while mxc version still prints the source hash. The fixpoint still converges (131/131). moxie 5e7dd69e | | a shift's untyped left operand was typed by its count | func f(c byte) (r uint32) { return 1 << (c % 32) } answered 0 for c = 13 where legacy answered 8192, and bytes.TrimRight([]byte("\r\n"), "\r\n") returned "\r\n" unchanged: stage4 typed the shift as the count's type (uint8 for a byte count), so emitShift's overshift select - Go makes a count >= the operand width shift to zero - rejected every count >= 8. The stdlib's asciiSet ([8]uint32) is built with as[c/32] |= 1 << (c % 32), so every bytes.Trim* silently stopped trimming; the mediaproxy header loop then never stopped at the blank line and fetchOnce answered read header: EOF for every response once the peer closed. Found by the loopback HTTP origin added with this round's coverage work, after strace showed the server writing all 75 bytes and the client's single read receiving them | a shift whose left operand is untyped now takes defaultReprType (int32, or the widened 64-bit type when the constant does not fit) and stays marked context-dependent; the return statement, call arguments, var declarations and composite-literal elements join assignments, compound assignments and conversions as retype sites, so return 1 << (c % 64) from a uint64 function and []uint64{1 << (c % 64)} are 64-bit shifts. tests/data/shiftconst + phase6 6o assert the untyped and typed forms, the 64-bit return, argument, declaration, slice/map literal, if initializer and package variable in both compilers (legacy was already correct and is unchanged). moxie 8d35c38a | | a reslice past the end gave the slice a negative length | t := []byte{}[1:] had len(t) == -1 under both compilers (legacy's createSliceBoundsCheck is a documented no-op), so the tree's for ; len(r) > 0; r = r[1:] loops left the cursor at -1 and every if len(r) == 0 guard that followed never fired: a REQ whose filter was cut short (["REQ","s",{"kinds":[1) read the byte after the input as a key and killed the relay with SIGSEGV. Found by the envelope truncated-input test. A first cut that clamped low/high/max broke the self-host chain (codec default bad size), and a rebuild ordering mistake made the low-only clamp look like it failed 10 regressions | clamp the low bound only - low = umin(low, len), then umin(low, high) - so every valid slice (0 <= low <= high <= len, including s[:cap(s)]) is byte-identical and an exhausted or inverted range is empty; stage4 emits the two icmp ult/select pairs in emitSliceOp (uminIpt), legacy does the same in clampSliceLow. tests/data/sliceres + phase6 6p cover the empty reslice, the consume loop, capacity extension, an inverted range and a cursor past the end in both compilers; the smesh envelope test feeds the crashing REQ again. tests/run.sh --full 146/146 with the fixpoint converged, run_tests.sh 10/10. moxie dd8ce5e2 | | the ChaCha20-Poly1305 AEAD returned zeros in application builds | Seal was all zeros and Open answered message authentication failed when the package was a dependency of any moxie build, while the root-package RFC 8439 known-answer test passed. The doc first filed it as "a stdlib package is miscompiled when it is a dependency", but the root cause is the port, not the codegen: the Go sliceForAppend idiom splits one allocation into a head and a sub-slice tail, and Moxie relocates each returned slice on its own - the tail comes back as a copy, so the ciphertext and tag written through it never reached the returned buffer | rewrote sealGeneric/openGeneric aliasing-free (build the ciphertext and tag in their own buffers and concatenate; Open writes nothing before the tag verifies), removed the now-unused sliceForAppend, and made chacha20's arch flag a function because legacy's go/types cannot fold runtime.GOARCH == ... in a constant expression. tests/data/aeaddep + phase6 6q assert the RFC vector in an application build under both compilers; web/common/marmot's round trip is un-skipped (18 tests). moxie ac5b4093 |