w6c+selfhost: principled sub-word signedness (closes #5/#10)

type_isunsigned recurses TY_ENUM and includes TY_RUNE on both stages.
13 LOAD + 6 STORE ladder sites (cstage) plus 4 more wwstage stragglers
in cgindex/cgforrange collapsed to fldloadop/fldstoreop helpers. N_CAST
narrow gate symmetrised; task #1's literal-kind workaround retired.
bool kept out of type_isunsigned, special-cased in field helpers.

Retroactively fixes a u32 mis-sign-extend in deref-compound (sz=4
hardcoded MOVSXD), pinned by new 660_field_signed row.
This commit is contained in:
2026-05-13 22:23:00 +09:00
parent 461a448d5f
commit c4b3aca5e4
9 changed files with 976 additions and 535 deletions

View File

@@ -502,7 +502,10 @@ fn nodeisstr(c: *cgen, n: *node) bool = {
return false;
};
// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr.
// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr/rune.
// rune is a Unicode codepoint (0..0x10FFFF); cgen treats it as
// unsigned so narrow-cast / sub-word load paths zero-extend (MOVL,
// not MOVSXD). Mirrors cstage's type_isunsigned post task #5.
fn typenameisunsigned(nm: str) bool = {
if (streq(nm, "u8")) { return true; };
if (streq(nm, "u16")) { return true; };
@@ -510,10 +513,39 @@ fn typenameisunsigned(nm: str) bool = {
if (streq(nm, "u64")) { return true; };
if (streq(nm, "uint")) { return true; };
if (streq(nm, "uintptr")) { return true; };
if (streq(nm, "rune")) { return true; };
return false;
};
// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc.
// typenodeisunsigned — recurse through TNAME aliases / TBANG / TENUM
// to the resolved primitive. Mirrors cstage's type_isunsigned which
// recurses into TY_NAMED.under and TY_ENUM.sub.
fn typenodeisunsignedc(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { return typenodeisunsignedc(c, t.lhs); };
if (k == nkind.N_TENUM) { return typenodeisunsignedc(c, t.lhs); };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (typenameisunsigned(nm)) { return true; };
if (typenameissigned(nm)) { return false; };
// Follow aliases / enum storage.
let al: *node = aliaslookup(c, nm);
if (al != nil) { return typenodeisunsignedc(c, al); };
let en: *enumtype = enumlookup(c, nm);
if (en != nil) {
if (en.storage != nil) {
return typenodeisunsignedc(c, en.storage);
};
return false; // default storage i32 is signed
};
};
return false;
};
// typenodeisunsigned — legacy callers without *cgen context. Only
// resolves primitive TNAMEs (no alias/enum recursion); use the
// _c variant where the cgen registry is in scope.
fn typenodeisunsigned(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TNAME) { return typenameisunsigned(t.str); };
@@ -568,9 +600,21 @@ fn typeis8byteprimitive(c: *cgen, t: *node) bool = {
};
// elemissigned — given an indexable type (`*T`, `[]T`, `[N]T`), is
// its element a signed narrow primitive (i8/i16/i32/rune)? Used by
// cgindex to pick MOVSXD vs MOVL at esz=4. Mirrors C cgen's
// `signed_elem` check.
// its element a signed narrow primitive (i8/i16/i32)? Used by
// cgindex to pick MOVSXD vs MOVL at esz=4 (and MOVSBQ/MOVSWQ at
// esz=1/2). Mirrors cstage's `signed_elem`. Follows alias/enum
// chains so `[]Alias` arrays resolve to the underlying signedness.
fn elemissignedc(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let elem: *node = nil;
let k: nkind = t.kind;
if (k == nkind.N_TPTR) { elem = t.lhs; };
if (k == nkind.N_TSLICE) { elem = t.lhs; };
if (k == nkind.N_TARRAY) { elem = t.lhs; };
if (elem == nil) { return false; };
return fieldissignedc(c, elem);
};
fn elemissigned(t: *node) bool = {
if (t == nil) { return false; };
let elem: *node = nil;
@@ -583,47 +627,103 @@ fn elemissigned(t: *node) bool = {
return typenameissigned(elem.str);
};
// typenameissigned — true for i8/i16/i32/i64/int/rune.
// typenameissigned — true for i8/i16/i32/i64/int. rune is excluded
// (it's a non-negative Unicode codepoint, treated as unsigned).
fn typenameissigned(nm: str) bool = {
if (streq(nm, "i8")) { return true; };
if (streq(nm, "i16")) { return true; };
if (streq(nm, "i32")) { return true; };
if (streq(nm, "i64")) { return true; };
if (streq(nm, "int")) { return true; };
if (streq(nm, "rune")) { return true; };
return false;
};
// fieldissignedc — does this field/element type need sign-extension
// on a sub-word load? Walks TBANG / TENUM / TNAME-aliases to the
// resolved primitive. Mirrors cstage's fld_issigned: bool is treated
// as unsigned (0/1 ⇒ MOVZBQ); rune is unsigned (codepoint ⇒ MOVL).
fn fieldissignedc(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { return fieldissignedc(c, t.lhs); };
if (k == nkind.N_TENUM) { return fieldissignedc(c, t.lhs); };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "bool")) { return false; };
if (typenameisunsigned(nm)) { return false; };
if (typenameissigned(nm)) { return true; };
let al: *node = aliaslookup(c, nm);
if (al != nil) { return fieldissignedc(c, al); };
let en: *enumtype = enumlookup(c, nm);
if (en != nil) {
if (en.storage != nil) {
return fieldissignedc(c, en.storage);
};
return true; // default i32 storage is signed
};
};
return false;
};
// fieldloadop — pick the load instruction for a non-str struct
// field by its declared size + signedness. Mirrors the C cgen op
// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ
// for 8-byte). f might be nil for fields outside our struct registry.
fn fieldloadop(f: *fieldinfo) str = {
// field by its declared size + signedness. Mirrors cstage's
// fldloadop: MOVZBQ/MOVSBQ for 1B, MOVZWQ/MOVSWQ for 2B,
// MOVL/MOVSXD for 4B, MOVQ for 8B. f might be nil for fields
// outside our struct registry.
fn fieldloadop(c: *cgen, f: *fieldinfo) str = {
if (f == nil) { return "MOVQ"; };
let sz: i32 = f.fsz;
if (sz == 1) { return "MOVZBQ"; };
if (sz == 4) {
let t: *node = f.tnode;
if (t != nil) {
if (t.kind == nkind.N_TNAME) {
if (typenameissigned(t.str)) { return "MOVSXD"; };
};
};
return "MOVL";
};
let sigd: bool = fieldissignedc(c, f.tnode);
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
return "MOVQ";
};
// fieldstoreop — pick the store instruction for a non-str struct
// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8.
fn fieldstoreop(f: *fieldinfo) str = {
// field by its declared size. MOVB for 1, MOVW for 2, MOVL for 4,
// MOVQ for 8. c kept in the signature for symmetry with fieldloadop.
fn fieldstoreop(c: *cgen, f: *fieldinfo) str = {
if (f == nil) { return "MOVQ"; };
let sz: i32 = f.fsz;
if (sz == 1) { return "MOVB"; };
if (sz == 2) { return "MOVW"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// tnodeloadop / tnodestoreop — same dispatch as fieldloadop /
// fieldstoreop but keyed on a raw type-AST node (tuple element type,
// pointer-target, slice-element, etc.) rather than a struct fieldinfo.
// Used at the index / tuple / pointer-deref sites where there's no
// fieldinfo entry but the type-node + size are both known.
fn tnodeloadop(c: *cgen, t: *node, sz: i32) str = {
let sigd: bool = fieldissignedc(c, t);
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
return "MOVQ";
};
fn tnodestoreop(c: *cgen, t: *node, sz: i32) str = {
if (sz == 1) { return "MOVB"; };
if (sz == 2) { return "MOVW"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// loadopsz — load op when the (size, signedness) pair has already
// been resolved upstream and the type-node isn't carried through.
// cgindex precomputes `signed_elem` via elemissignedc; cgforrange
// precomputes `bind_signed[b]` via paramissigned. Same dispatch as
// tnodeloadop's tail; only the keying differs.
fn loadopsz(sigd: bool, sz: i32) str = {
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
return "MOVQ";
};
// indexbaseesz — element size for `arr[i]` where the base is a
// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice).
// For str the element is one byte; for `[]T` / `*[]T` we drill into
@@ -2097,7 +2197,7 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz:
emitoff((slot_off + 8 + fi.foff + 8): i64);
emitline("(BP)\n");
} else {
let sop: str = fieldstoreop(fi);
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");