wcc: &s.len / &s.cap typed as *i64 (closes #13)

TK_AMP early-exit on slice/str .len/.cap pseudo-fields now returns
*i64 instead of legacy *i32. Slice ABI is 24B fixed; LEAQ at the slot
was already correct, only the pointer typing was wrong — store-width
flips from MOVL to MOVQ via the existing primsize-from-tnode path.
&s.ptr untouched (already **T).
This commit is contained in:
2026-05-14 00:21:53 +09:00
parent 6354f5267c
commit f2b47087a2
2 changed files with 109 additions and 4 deletions

View File

@@ -623,6 +623,23 @@ cunop(Checker *c, Node *n)
type_name(c->a, t));
return t->sub;
case TK_AMP: /* address-of */
/* Slice/str pseudo-fields .len/.cap surface as i32 but live
* in 8B-aligned slots in the header (ptr@0, len@8, cap@16).
* Address-of must be typed *i64 so deref-write hits the full
* slot; otherwise *&s.len = N stores 4B (MOVL) and the upper
* 4B leak from whatever the prior MOVQ store of s.len left
* behind. */
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs &&
n->lhs->str &&
(strcmp(n->lhs->str, "len") == 0 ||
strcmp(n->lhs->str, "cap") == 0)) {
Type *bt = n->lhs->lhs->type;
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
if (bu && bu->kind == TY_PTR) bu = bu->sub;
if (bu && bu->kind == TY_NAMED) bu = bu->under;
if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR))
return type_ptr(c->a, ty_i64);
}
return type_ptr(c->a, t);
default:
return err(c, n->pos, "unsupported unary %s", tokname(n->op));

View File

@@ -102,8 +102,10 @@ static const struct row rows[] = {
55 },
/* The motivating idiom: write through `&s.len` on a slice header
* to truncate without re-allocating. Mirrors the Hare slice-
* header poke pattern that bufio will eventually want. Test:
* fill a slice's len to 5, then *&s.len = 0; assert s.len == 0. */
* header poke pattern that bufio will eventually want. The slice
* header's .len slot is 8B even though the surface type is i32,
* so `&s.len` is `*i64` (matches storage); deref-store hits all
* 8B, and the i64 load reads back the value the user wrote. */
{ "amp_dot_slice_len_writethrough",
"fn main() i32 = {\n"
" let arr: [4]u8;\n"
@@ -111,11 +113,97 @@ static const struct row rows[] = {
" s.ptr = &arr[0];\n"
" s.len = 4;\n"
" s.cap = 4;\n"
" let q: *i32 = &s.len;\n"
" *q = 0;\n"
" let q: *i64 = &s.len;\n"
" *q = 0i64;\n"
" return s.len: i32;\n"
"};\n",
0 },
/* Width-stress for `&s.len`: write a value whose lower-32B
* differs from upper-32B and confirm the upper bytes don't
* leak from the prior 8B store of `s.len = 4`. Before the fix,
* `&s.len` was `*i32` and `*q = v` lowered to MOVL, leaving
* the upper 4B at whatever the MOVQ store of 4 left there
* (zero — pass by accident). With a non-zero stale upper or a
* fresh write that fills both halves, the i64 read of s.len
* exposes the mismatch. We write 0xFFFF_FFFF_FFFF_FFFF and
* return 1 iff s.len reads back as -1 (i64). */
{ "amp_dot_slice_len_width_stress",
"fn main() i32 = {\n"
" let arr: [4]u8;\n"
" let s: []u8;\n"
" s.ptr = &arr[0];\n"
" s.len = 4;\n"
" s.cap = 4;\n"
" let q: *i64 = &s.len;\n"
" *q = -1i64;\n"
" let v: i64 = s.len: i64;\n"
" if (v == -1i64) { return 1; };\n"
" return 0;\n"
"};\n",
1 },
/* Same shape for `&s.cap`. cap lives at +16 in the slice header
* and was the second pseudo-field broken by the same width bug. */
{ "amp_dot_slice_cap_width_stress",
"fn main() i32 = {\n"
" let arr: [4]u8;\n"
" let s: []u8;\n"
" s.ptr = &arr[0];\n"
" s.len = 4;\n"
" s.cap = 4;\n"
" let q: *i64 = &s.cap;\n"
" *q = -1i64;\n"
" let v: i64 = s.cap: i64;\n"
" if (v == -1i64) { return 1; };\n"
" return 0;\n"
"};\n",
1 },
/* Same shape for str.len. str header is (ptr, len) with len at
* +8, also 8B storage. */
{ "amp_dot_str_len_width_stress",
"fn main() i32 = {\n"
" let s: str = \"abcd\";\n"
" let q: *i64 = &s.len;\n"
" *q = -1i64;\n"
" let v: i64 = s.len: i64;\n"
" if (v == -1i64) { return 1; };\n"
" return 0;\n"
"};\n",
1 },
/* `&s.ptr` write-through. ptr lives at +0 and is `*T` (not a
* pseudo-i32), so `&s.ptr` types as `**T` — already the right
* width pre-fix. Pin it so a future regression doesn't slip the
* other way. */
{ "amp_dot_slice_ptr_writethrough",
"fn main() i32 = {\n"
" let arr: [2]u8;\n"
" arr[0] = 13; arr[1] = 0;\n"
" let s: []u8;\n"
" s.len = 2; s.cap = 2;\n"
" let pp: **u8 = &s.ptr;\n"
" *pp = &arr[0];\n"
" return s.ptr[0]: i32;\n"
"};\n",
13 },
/* Chained `&w.s.len` — slice WRAPPED in a struct. The pseudo-
* field width override gates on the leaf .len/.cap with base
* type TY_SLICE/TY_STR; for `&w.s.len` the base of the leaf
* dot is `w.s` (type []u8), so the override must still fire
* and the deref-store must hit all 8B at the wrapped slice
* header's len slot (offset of .s + 8). */
{ "amp_dot_slice_field_len_width",
"type wrap = struct { s: []u8, x: i32 };\n"
"fn main() i32 = {\n"
" let arr: [4]u8;\n"
" let w: wrap;\n"
" w.s.ptr = &arr[0];\n"
" w.s.len = 4; w.s.cap = 4;\n"
" let q: *i64 = &w.s.len;\n"
" *q = -1i64;\n"
" let v: i64 = w.s.len: i64;\n"
" if (v == -1i64) { return 1; };\n"
" return 0;\n"
"};\n",
1 },
/* &o.s.ptr — slice-FIELD of a struct: combines shape 1 (walk
* into struct field at .s) and shape 3 (slice pseudo-tail
* .ptr at offset 0 of the header). Proves the spine walker's