Summary
This report bundles 18 reproducible bugs found across the tokenizer, parser, type checker, borrow checker, direct backends, and source-backed stdlib. Each finding is reproduced on the current main (commit f1a5cc9), each file:line citation has been verified, and no finding duplicates an existing issue, PR, or commit. Severity ranges from wrong-code emission on darwin-arm64 (the default macOS host backend) through borrow-checker soundness gaps to documentation drift.
Methodology: six parallel investigations across disjoint domains, then four independent verification passes that re-reproduced each finding from scratch and cross-referenced against open issues, PRs, and git log. 18 of 18 verified CONFIRMED + UNFILED.
Bundling note
I'm filing this as a single audit rather than 18 separate issues to avoid flooding the tracker. Happy to split into per-domain or per-finding issues if that's easier to triage — let me know. I'm planning to send focused PRs for the highest-severity items (S1, S2) unless you'd prefer to fix them directly.
Tier S — soundness / wrong-code
S1. i64 comparison emits 32-bit CMP w on darwin-arm64
- File:
native/zero-c/src/emit_macho64.c:584 - Reference (correct) impl:
native/zero-c/src/aarch64_direct.c:416-419choosescmp_xvscmp_wviaa64_type_is_scalar64(value->left->type). The Mach-O direct emitter callsz_aarch64_emit_cmp_wunconditionally. - Repro (
/tmp/s1.0):
fn shift32 i64 a i64
ret + a 4294967296
pub fn main Void world World !
let big i64 shift32(0)
if == big 0
check world.out.write "BUG: 2^32 == 0\n"
if > big 0
check world.out.write "ok positive\n"
else
check world.out.write "BUG: not > 0\n"- Expected:
ok positive. Actual on darwin-arm64:BUG: 2^32 == 0thenBUG: not > 0— every i64/u64 compare on darwin-arm64 compares only the low 32 bits. - Disasm proof: every comparison site in
_mainiscmp w8, w9(encoding6b09011f) where it should becmp x8, x9. - Fix sketch: mirror the
aarch64_direct.c:416-419pattern at the macho call site — checka64_type_is_scalar64and emitcmp_xwhen wide. - Distinct from #230 (which catalogs missing IR ops, not wrong-code in existing ops).
S2. macho-arm64 oversized stack frame silently truncated → segfault
- File:
native/zero-c/src/buildability_value_targets.c:6—z_build_backend_is_aarch64_directreturns true forZ_DIRECT_BACKEND_ELF_AARCH64andZ_DIRECT_BACKEND_COFF_AARCH64but omitsZ_DIRECT_BACKEND_MACHO64. - Consequence:
buildability.c:181routes ELF/COFF aarch64 functions toz_build_check_aarch64_function_shape(which gatesframe_bytes > 4095atbuildability_value_targets.c:304). darwin-arm64 falls through to the x86-style shape check with no frame-size limit. The emitter ataarch64_emit.c:129-135then maskssub sp, sp, #immto 12 bits (imm & 0xfffu) with no overflow check. - Repro (
/tmp/s2.0):
pub fn main Void world World !
mut arr [1024]i32 [0_i32;1024]
set arr[0] 1
set arr[1023] 42
if == arr[1023] 42
check world.out.write "ok\n"- Expected: BLD004 at build time (matching ELF/COFF) or a correctly encoded prologue (
LSL #12or temp reg). - Actual: builds, prints
ok, exits 139 (SIGSEGV). Boundary observed:n=956(frame 4080) works (sub sp, #0xff0);n=957+(frame ≥4096) silently truncates and segfaults. - Fix sketch: add
|| backend == Z_DIRECT_BACKEND_MACHO64toz_build_backend_is_aarch64_direct. - Suggested test: mirror the existing
conformance/native/fail/macho-large-byte-slice-blocked.0shape.
S3. Dynamic-index array set arr[i] clears ALL borrow tracking (soundness)
- File:
native/zero-c/src/checker.c:189-205—origin_path_array_segment_definitely_within. Line 197's wildcard-prefix short-circuit incorrectly returns true when the prefix is wildcard. - Repro (
/tmp/s3.0):
pub fn main Void
mut x i32 1
let i usize 0
mut arr [2]ref<i32> [(&x), (&x)]
let y i32 2
set arr[i] &y
set x 99_i32- Expected: BOR001 on
set x 99_i32— with non-constanti,set arr[i] &yoverwrites at most one of the two&xborrows; the other still borrowsx. - Actual:
zero checkreportsok. The dynamic-index target gets path[*], thenscope_replace_value_provenance_path_at(line 743) clears every concrete entry ([0],[1]) because the wildcard prefix is treated as definitely-within concrete indices. - Control: the same program with a constant index
set arr[0] &ycorrectly raises BOR001 — only the dynamic-index path is unsound. - Asymmetry:
origin_path_array_segment_match(line 172) correctly accepts wildcards for intersection checks. Only_definitely_within(containment) is unsound. - Fix direction: in
_definitely_within, a wildcard prefix should NOT be treated as definitely covering concrete indices — dynamic-index writes downgrade to "maybe overwrites" (keep concrete borrows live). - Distinct from #31 (closed) which used the old curly-brace syntax and a different code path.
Tier A — high-value correctness
A1. std.math.modPowU32 u32 overflow for modulus > 65536
- File:
std/math.0:45-56(__zero_std_math_mod_pow_u32) * result currentand* current currenthappen as u32 before% modulus. For modulus > sqrt(2^32) ≈ 65536, intermediates overflow.- Repro:
mod_pow_u32(65535_u32, 4_u32, 4294967291_u32)returns wrong value (Python reference: 4293394483). - Fix sketch: widen intermediates to u64, narrow after
%. - Related recent fix: commit
7976f0b"Fix std.math unsigned edge cases" touched math but didn't includemod_pow.
A2. Silent decode of unknown string escapes
- File:
native/zero-c/src/row_syntax.c:261— catch-allelse row_append_char(...) - Asymmetric with:
row_scan_char:318-321correctly emits PAR100 "unsupported character escape". - Repro:
let s "\q"tokenizes silently to string"q". - Fix sketch: mirror char-scanner — reject unknown escapes with PAR100.
A3. "\0" silently → "0" (string scanner has no \0 case)
- File:
row_syntax.c:239-261(string) vs:302(char hasescaped == '0'→ NUL). - Note:
\x00is rejected at:253("zero byte"), but bare\0falls through to the catch-all and silently decodes to literal'0'. - Fix: falls out of A2 (catch-all errors), plus decide policy on
\0(likely reject like\x00).
A4. Reserved-word table missing set, alias, interface
- File:
row_syntax.c:727-732(row_is_reserved_word) - Repro:
let set i32 0,let alias i32 0,let interface i32 0all accepted as identifiers, whilesetis consumed as STMT_ASSIGN at:1296andalias/interfaceat:1839/:1843. A binding can be declared with these names and then never reused at statement position. - Fix: add the three strings to the keywords array.
A5. Array literal [, 1, 2], [1,, 2], [,] silently accepted
- File:
row_syntax.c:987-999(array loop); same shape at:1050-1056(call args). - The comma loop continues without tracking whether a value was seen since the previous comma.
- Fix: track expression-seen state across the comma loop.
A6. Array repeat [;0] accepted without count
- File:
row_syntax.c:992-996— when;is seen, value is pushed without verifying exactly one count already inargs. let x [;0]parses withargs.len == 1andarray_repeat = true(malformed shape for the checker).[1 2; 3]parses withargs.len == 3andarray_repeat = true.- Note: checker catches some forms via TYP002, but the parser constructs a malformed AST regardless.
A7. aarch64 SDIV with zero divisor silently returns 0
- File:
native/zero-c/src/aarch64_emit.c:229(z_aarch64_emit_div_reg) — no zero guard. No checker/buildability diagnostic for constant/ x 0or% x 0either. - Repro:
let x i32 / 10 0runs cleanly, x==0. - Portability claim violated: x86_64 IDIV traps with #DE; aarch64 SDIV returns 0. Same source, different runtime behavior, no documented contract.
Tier B — design / drift / UX
B1. i64.MIN / i32.MIN unrepresentable as a literal
- File:
checker.c:4664-4680(validate_integer_literal_for_type). - The validator compares unsigned magnitude against
info.max, so2147483648is rejected before- 0 2147483648can fold into i32.MIN. There is no syntactic form that produces signed MIN. - Design call: add
i32.MIN/i64.MINconstant, support unary minus, or special-case- 0 ...in the validator.
B2. std.path.normalize(".") returns empty string
- File:
std/path.0:130-131— single-dot segment unconditionally consumed; no tail to emit.when no segments emitted. - POSIX
realpath, Gofilepath.Clean, Pythonos.path.normpathall return.. - Related recent fix: commit
8303bcf"Fix std.path normalization edge cases" touched path but bare.wasn't covered.
B3. std.path.is_sep accepts \ but docs say /-only
- Code:
std/path.0:2returns true for both47_u8(/) and92_u8(\). - Docs:
docs/articles/modules/path.md:16, 36— "target-neutral lexical operations over/-separated paths" and "uses/as the portable package/example separator". - Effect:
path.basename "foo\bar"returns"bar". - Pick a side: drop the byte-92 branch or update docs.
B4. Parser EOF "expected expression" diagnostic at (1,1) instead of last token
- File:
row_syntax.c:959(row_parse_primaryhardcodesrow_diag(parser->diag, 1, 1, 1, ...)); same pattern at:806(row_append_type_atom). - Repro: source
pub fn main Voidthenlet myvarreports at (1,1) instead of the location ofmyvar. - Fix: fall back to the previous token's line/column (see
row_expect_word:721-722for the existing pattern).
B5. Underscores rejected in float literals
- File:
checker.c:4684—parse_float_literalreturns false unconditionally if_appears. - Inconsistency:
parse_integer_literalaccepts1_000;parse_float_literalrejects1_000.0. - Fix: one-line — strip
_before parse, matching the integer path.
B6. \xFF in char accepted but raw 0xFF byte rejected
- File:
row_syntax.c:306-317(char\xno range check) vs:326-328(raw bytes ≥ 128 rejected). - Pick a side: accept full byte range in both, or restrict
\xto ≤ 0x7F.
B7. Raw NUL byte in source treated as EOF
- File:
row_syntax.c:500—while (lexer.source[lexer.offset])uses a C-string idiom with no length tracking. Same pattern inrow_scan_string:217androw_scan_char:286. - Repro: a file with embedded 0x00 mid-content reports "unterminated string literal" at the wrong column.
- Fix: carry source length separately, or emit a precise diagnostic when a NUL is encountered.
B8. No \u Unicode escape — silently mis-decoded
- File: same catch-all as A2 (
row_syntax.c:261). - Repro:
let s "A"decodes to"u0041"(the\is consumed,uthen literal0041follow). - Fix: falls out of A2 — once unknown escapes error, this becomes a precise diagnostic. Optionally implement
\u{...}for UTF-8.
Related (non-duplicate) prior work
- #230 — darwin-arm64 Mach-O missing IR ops (CGEN004 errors at build). Distinct category from S1/S2, which generate wrong code for IR ops that do compile.
- #96 — superseded by #230, same domain.
- #31 (closed) — type-checker fuzzing in the old curly-brace syntax. S3 is in the row-syntax-era checker (different code path).
- commit
7976f0b"Fix std.math unsigned edge cases" — touched math but didn't includemod_pow. - commit
8303bcf"Fix std.path normalization edge cases" — touched path but missed bare..
Methodology
- Six parallel investigations across disjoint domains (tokenizer, parser, numeric, codegen, stdlib, type/borrow).
- Four independent verification passes that re-reproduced each finding from scratch on
f1a5cc9and searched issues / PRs / git log for duplicates. - 18 of 18 findings verified CONFIRMED + UNFILED.
What's next
Happy to send focused PRs for any subset — S1 and S2 are the smallest diffs and have the highest user impact (silent miscompile / segfault on the default macOS host backend). Let me know if you'd prefer this split into multiple issues, want any items dropped, or would rather fix some directly.
Re-audit against current
main(51041a5, v0.2.0)The original report was filed against
f1a5cc9(674 commits ago) using the.rowsurface syntax, which has since been removed in the.row→ canonical.0migration. I re-tested every finding from scratch in canonical.0on currentmain.Result: 6 of 18 resolved, 12 still reproduce.
The split falls cleanly along the migration line: findings in code that was rewritten for
.0are fixed; findings in the canonical lexer's escape/byte handling, the checker, the aarch64/Mach-O backends, andstd.pathcarried over and still reproduce.✅ Resolved (6)
CMP won darwin-arm64cmp x(eb09011f); runtime correct.std.math.modPowU32overflow for modulus > 65536std/math.0now widens intermediates tou64and narrows on return;pow(65535,4,4294967291)is correct.set/alias/interface[, 1, 2]/[1,, 2]/[,]accepted[;0]accepted without count[0_i32; 3]still accepted.🔴 Still reproduces on
main(12)Re-verified with canonical repros;
file:lineupdated to the current tree.sub sp, sp, #0x0(12-bit immediate masked); ELF aarch64 correctly rejects the same program with BLD004 — Mach-O bypasses the frame-size gate.arr[i] = &yclears all borrow tracking (soundness)checker.corigin_path_array_segment_definitely_withinstill treats a wildcard prefix as definitely-within; constant-index path correctly raises BOR001, dynamic-index path does not.canon_scan_stringhas a catch-all*offset += 2; char scanner still rejects via PAR100 — asymmetry intact."\0"silently →"0"\x00is rejected but bare\0is not.z_aarch64_emit_div_regemits a bare divide with no guard; no checker/buildability diagnostic for/ 0or% 0(constant or runtime). Diverges from x86_64 (#DE).i64.MIN/i32.MINunrepresentable as a literalstd.path.normalize(".")returns empty stringMaybe<String>); still returns[]instead of..\but docs say/-only__zero_std_path_is_sep(/or\); observable viastd.path.basename("foo\\bar")→bar. Docs still say/-only.1_000.5→ TYP019, while integer1_000is accepted.\xFFin char accepted but raw 0xFF byte rejected\xchar escape has no range check; raw non-ASCII byte rejected — asymmetry intact.\uUnicode escape — silently mis-decoded\u...(and\u{...}) silently; the char scanner whitelists escapes.Notes