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?
make test runs three layers:
make test-unit — every package carrying a *_test.mx, through the compiler's own harness (moxie test ./<pkg>).
make build-test-relay + make test-signer — the relay and the browsersigner used by the integration suite.
python3 -m pytest test/ — the integration suite (HTTP, WebSocket, NIP-01policy, 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.
moxie test never built a real closure (four compiler bugs)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
`
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.
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.
moxie test -work now keeps and prints the test scratchdirectory like the build path, which is how bugs 1-3 were found.
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.
| where | bug | fix |
|---|---|---|
pkg/relay/ratelimit | map[string]*bucket{} assigned to a map[string]bucket field (hidden by the literal hole above) | literal corrected |
pkg/nostr/timestamp | FromVarint 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/mode | IsOpen's logic was unreachable from a test: it reads the ACLMode global, and package globals are immutable outside init | logic extracted to the pure IsOpenMode, which the test covers |
pkg/nostr/filter | F.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 kinds | exact 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 test | shadowed signer/err (Moxie forbids shadowing; the test predated enforcement) and never compiled | rewritten with per-function names |
pkg/crawler, pkg/find, pkg/grapevine tests | used t.TempDir() and t.Cleanup(func(){ eng.Close() }); Moxie has neither, and a cleanup closure would have to capture eng | open 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 3 | key 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/checkpoint | Set 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 behind | remove 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 path | bound 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 |
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.
_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.
Verdicts are from make test-unit (see the runner for the exact command).
| package | unit test | integration | verdict |
|---|---|---|---|
. (root main.mx) | main_test.mx + main_session_test.mx | the relay itself | pass (26, main.mx 20.3% -> 59.6%; the network helpers run against loopback WebSocket peers) |
pkg/errors | errors_test.mx + errors_paths_test.mx | relay error paths | pass (9, 98.3% of errors.mx) |
pkg/nostr/hex | hex_test.mx (new) | via event serialization | pass (3) |
pkg/nostr/varint | varint_test.mx (new) | via event tags/ints | pass (3) |
pkg/nostr/ints | ints_test.mx (new) | via event JSON | pass (5) |
pkg/nostr/timestamp | timestamp_test.mx (new) | via event JSON | pass (5) |
pkg/nostr/event | event_test.mx (repaired) + event_paths_test.mx | policy/roundtrip | pass (15, 91.8% of the package) |
pkg/relay/ratelimit | ratelimit_test.mx (new) | free-write limit test | pass (5) |
pkg/find | find_test.mx (repaired) | not on the request path | pass (4) |
pkg/mode | mode_test.mx (repaired) | every ACL check | pass (1) |
pkg/crawler | crawler_test.mx (repaired) | not on the request path | pass (3) |
pkg/grapevine | grapevine_test.mx (repaired) | not on the request path | pass (3) |
pkg/store | engine_test.mx + 7 subpackage files | every query | pass (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/pipeline | pipeline_test.mx | every write | pass (27), 95.5% of pipeline.mx |
pkg/nostr/ws | client_test.mx, client_session_test.mx, ws_frames_test.mx, dns_test.mx | relay does not import it | pass (20, against loopback servers; client.mx 97.8%, ws.mx 75.2%, dns.mx 56.9%) |
pkg/sync/negentropy | negentropy_test.mx | cmd/sync only | pass (29), 90% of negentropy.mx |
pkg/mediaproxy | mediaproxy_test.mx + mediaproxy_http_test.mx | media proxy worker | pass (22, 88.7% of mediaproxy.mx; the HTTP half runs against a scripted loopback origin) |
pkg/blossom | blossom_test.mx + blossom_serve_test.mx | blob routes | pass (14, 99.1% of blossom.mx; the CORS-proxy fallback runs against a loopback origin) |
pkg/lol | lol_test.mx | logging | 8/9 pass; TestErrorf blocked by the fmt multi-verb bug |
web/common/marmot | varint_test.mx + codec_test.mx | browser module | pass (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.
| file | what it validates |
|---|---|
test/test_relay_policy.py | 24 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.py | publish/receive, live subscription, feed, localhost relay, ordering |
test/test_smoke.py | index, wasm host, css, service worker, NIP-11, SPA fallback, REQ/EOSE |
test/test_signer.py | 16 tests over the browser signer (BIP-340 vectors, nsec login, vault export/import) |
test/test_app.py, test/test_infinite_scroll.py | front-end flows served by the relay |
test/test_http_proxy_worker.py | 14 tests over the proxy worker shim |
test/test_mls_broadcast.py, test/test_mls_dm.py | MLS messaging paths |
test/test_sab_verify.py | SharedArrayBuffer verification shim |
test/test_memory_profile.py | allocator 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.
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.
| file | covered | note |
|---|---|---|
main.mx | 9.8% (69/697) | the relay main, plus the sync/crawl/env/proxy subcommands |
pkg/access/access.mx | 21.4% (3/14) | |
pkg/acl/acl.mx | 11.1% (1/9) | ACL mode dispatch: ORLY_ACL_MODE=none by default |
pkg/acl/follows.mx | 0.0% (0/45) | follows ACL, off by default |
pkg/acl/social.mx | 0.0% (0/51) | social/grapevine ACL, off by default |
pkg/blossom/blossom.mx | 2.4% (3/123) | covered by its unit test (99.1%), not by pytest |
pkg/broadcast/broadcast.mx | 68.5% (24/35) | |
pkg/grapevine/grapevine.mx | 0.0% (0/82) | ORLY_GRAPEVINE_ENABLED=false by default |
pkg/lol/chk/chk.mx | 33.3% (2/6) | |
pkg/lol/errorf/errorf.mx | 0.0% (0/6) | logger sub-package |
pkg/lol/log/log.mx | 0.0% (0/2) | logger sub-package |
pkg/mediaproxy/mediaproxy.mx | 0.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.mx | 0.0% (0/9) | covered by its unit test, not by pytest |
pkg/metrics/metrics.mx | 19.7% (14/71) | covered by its unit test, not by pytest |
pkg/nostr/envelope/auth.mx | 21.4% (9/42) | |
pkg/nostr/envelope/envelope.mx | 90.0% (36/40) | |
pkg/nostr/envelope/event.mx | 42.8% (18/42) | |
pkg/nostr/envelope/req.mx | 34.6% (9/26) | |
pkg/nostr/envelope/simple.mx | 31.1% (52/167) | |
pkg/nostr/event/binary.mx | 69.6% (69/99) | |
pkg/nostr/event/canonical.mx | 93.1% (27/29) | |
pkg/nostr/event/event.mx | 72.6% (157/216) | |
pkg/nostr/event/sign.mx | 38.2% (13/34) | |
pkg/nostr/event/verify.mx | 40.0% (8/20) | |
pkg/nostr/filter/filter.mx | 44.3% (121/273) | |
pkg/nostr/filter/filters.mx | 39.2% (22/56) | |
pkg/nostr/filter/skip.mx | 1.1% (1/89) | |
pkg/nostr/hex/hex.mx | 70.5% (12/17) | |
pkg/nostr/ints/ints.mx | 84.8% (56/66) | |
pkg/nostr/kind/embed.mx | 4.1% (9/216) | |
pkg/nostr/kind/kind.mx | 83.1% (148/178) | |
pkg/nostr/kind/kinds.mx | 40.0% (28/70) | |
pkg/nostr/signer/p8k/p8k.mx | 15.0% (14/93) | |
pkg/nostr/tag/tag.mx | 43.7% (73/167) | |
pkg/nostr/tag/tags.mx | 50.0% (51/102) | |
pkg/nostr/text/escape.mx | 30.1% (32/106) | |
pkg/nostr/text/helpers.mx | 58.9% (105/178) | |
pkg/nostr/text/wrap.mx | 37.0% (20/54) | |
pkg/nostr/timestamp/timestamp.mx | 19.5% (8/41) | |
pkg/nostr/varint/varint.mx | 92.3% (12/13) | |
pkg/nostr/ws/client.mx | 0.0% (0/95) | WS client: not used by the relay |
pkg/nostr/ws/dns.mx | 0.0% (0/127) | WS client DNS |
pkg/nostr/ws/ws.mx | 0.0% (0/184) | WS client |
pkg/pool/pool.mx | 83.3% (5/6) | |
pkg/relay/config/config.mx | 51.1% (323/631) | covered by its unit test (99.6%), not by pytest |
pkg/relay/dbengine/dbengine.mx | 80.7% (126/156) | |
pkg/relay/mute/mute.mx | 31.8% (14/44) | |
pkg/relay/pipeline/pipeline.mx | 66.4% (89/134) | |
pkg/relay/ratelimit/ratelimit.mx | 76.1% (16/21) | |
pkg/relay/server/server.mx | 79.7% (322/404) | |
pkg/relay/server/server_subs.mx | 43.7% (59/135) | |
pkg/relay/server/server_workers.mx | 33.0% (66/200) | |
pkg/relay/tree/messages.mx | 84.4% (136/161) | |
pkg/relay/wire/blossom_worker.mx | 16.6% (6/36) | now reports; no blossom request in the suite |
pkg/relay/wire/broadcast_worker.mx | 83.7% (67/80) | covered by its unit test (97.5%) |
pkg/relay/wire/ingest_worker.mx | 90.9% (30/33) | covered by its unit test (100%) |
pkg/relay/wire/proxy_worker.mx | 15.3% (4/26) | now reports; the dispatch block is never taken |
pkg/relay/wire/wire.mx | 35.4% (117/330) | covered by its unit test (93.3%), not by pytest |
pkg/store/checkpoint/checkpoint.mx | 62.7% (32/51) | |
pkg/store/engine.mx | 58.4% (405/693) | |
pkg/store/index/keys.mx | 73.8% (110/149) | |
pkg/store/serial/serial.mx | 74.0% (20/27) | |
pkg/store/sorted/sorted.mx | 70.4% (289/410) | |
pkg/store/wal/wal.mx | 40.8% (71/174) | |
pkg/transport/http.mx | 67.6% (140/207) | |
pkg/transport/transport.mx | 63.0% (208/330) | |
pkg/transport/ws.mx | 76.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.
(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%.
make cover run had three test_relay_policy.py tests fail because the relay exited 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.
-cover links the same runtime with no cost: the hook is anil check and the table is allocated lazily on the first hit.
SMESH_RELAY_BIN. 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.
test_relay_proxy_high_event_volume used to be dropped from make cover: 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%).
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:
| file | integration | unit |
|---|---|---|
pkg/relay/config/config.mx | 51.1% | 99.7% |
pkg/relay/wire/wire.mx | 7.5% | 93.3% |
pkg/relay/wire/ingest_worker.mx | 0% | 100.0% |
pkg/relay/wire/proxy_worker.mx | 3.8% | 65.4% |
pkg/relay/wire/blossom_worker.mx | 0% | 94.4% |
pkg/relay/wire/broadcast_worker.mx | 2.5% | 97.5% |
pkg/acl/acl.mx / follows.mx / social.mx | 11% / 0% / 0% | 100.0% / 97.8% / 95.1% |
pkg/relay/mute/mute.mx | 30.9% | 93.2% |
pkg/metrics/metrics.mx / global.mx | 19.7% / 0% | 98.6% / 100.0% |
pkg/access/access.mx | 21.4% | 100.0% |
pkg/pool/pool.mx | 33.3% | 100.0% |
pkg/blossom/blossom.mx | 2.4% | 69.9% |
pkg/nostr/text/escape.mx / helpers.mx / wrap.mx | 30% / 58% / 37% | 93.4% / 94.9% / 100.0% |
pkg/nostr/envelope/auth.mx / event.mx / req.mx / simple.mx | 21% / 40% / 35% / 31% | 95.2% / 97.6% / 96.1% / 96.4% |
pkg/store/engine.mx | 59.3% | 92.3% |
pkg/store/sorted.mx / wal.mx / serial.mx / index/keys.mx / checkpoint.mx | 72% / 41% / 74% / 94% / 63% | 93.4% / 85.9% / 100.0% / 100.0% / 88.4% |
pkg/relay/pipeline/pipeline.mx | 65.6% | 97.8% |
pkg/sync/negentropy/negentropy.mx | not in the relay | 93.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.
Ordered by how much untested code each item unlocks, with the shape of the test that would reach it:
| # | target | uncovered | test |
|---|---|---|---|
| 1 | pkg/nostr/kind/embed.mx, kinds.mx | 250 | done: 39 unit tests, 99-100% of the three files |
| 2 | pkg/nostr/filter (filter, filters, skip) | 298 | done: 21 unit tests, 98-100% of the three files (and the key-dispatch bug they found) |
| 3 | pkg/nostr/tag (tag, tags) | 235 | done: 22 unit tests, 98-100% of the two files (and two bugs they found) |
| 4 | pkg/nostr/text (escape, helpers, wrap) | 195 | done: 19 unit tests, 93-100% of the three files |
| 5 | pkg/mediaproxy | 187 | done: 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 |
| 6 | pkg/acl (acl, follows, social) | 140 | done: 11 unit tests, 95-100% of the three files |
| 7 | pkg/relay/wire workers | 174 | done: 28 unit tests plus a run with ORLY_INGEST_WORKERS=2; 93-100% of wire.mx and the four domains |
| 8 | pkg/store (sorted, index, wal, engine) | 340 | done: 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 |
| 9 | pkg/relay/server (server, _subs, _workers) | 400 | done 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 |
| 10 | pkg/relay/config | 309 | done: 11 unit tests over Load with a controlled environment, 99.6% |
| 11 | main.mx (sync, crawl, env, proxy subcommands) | 629 | done 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 |
| 12 | pkg/nostr/envelope (auth, simple, event, req) | 230 | done: 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) |
| 13 | pkg/grapevine, pkg/nostr/ws, pkg/nostr/signer/p8k | 250 | done: grapevine 3 tests, ws loopback tests (20, client.mx 33.6% -> 97.8%), p8k 9 tests including BIP-340 vector 0 (96.7%) |
| 14 | logger, metrics, access, pool | 100 | done: metrics 9 tests (98.5-100%), access 4 (100%), pool 3 (100%) |
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".
| item | evidence | next step |
|---|---|---|
| direct messaging is parked in the app | the 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 lines | fix 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 KeyPackage | the 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 process | a 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 them | the *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.
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.
| bug | evidence | fix |
|---|---|---|
select bound the comma-ok name to the received value | case 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 cases | the 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 events | it 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 nothing | point 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 lookups | sorted.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 it | Get 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 succeeded | stage4'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 bug | prepareDispatchIndex 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 silently | func 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 tests | registerFunc 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 silently | func (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 only | extend the same declared record to receiver-qualified method names |
the package cache key ignored -cover | a 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 nothing | pkgCacheKey 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 wrapper | moxie 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 exempt | cmdTest 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 byte | the 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=64 | ParseUint("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 empty | emitShift 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 wide | func id(n int64) (r int64) { r = n; return } then id(-42) printed 4294967254; var v int64 = -42 and int64(-42) were correct | call arguments are converted to the parameter's width (callArgTexts/coerceIntArg), so id(-42) is -42 |
mute.Blacklist.Load never loaded a real mute list | it 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 muted | accept the 33-byte binary form; mute_test.mx asserts a 33-byte p-tag is loaded |
stage4 compiled error == syscall.Errno to false | var 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 included | interface-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 value | fmt.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 killed | ingest_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 module | a 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/hkdf | both 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 trip | x := 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.Set | ConstInt.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 |