diff --git a/selfhost/cmd/wwc/cgen.ww b/selfhost/cmd/wwc/cgen.ww index 6519e287..cf3482f4 100644 --- a/selfhost/cmd/wwc/cgen.ww +++ b/selfhost/cmd/wwc/cgen.ww @@ -804,15 +804,14 @@ fn node_isstr(c: *cgen, n: *node) bool = { if (streq(fld, "len")) { return false; }; if (streq(fld, "cap")) { return false; }; if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; if (base.kind == N_IDENT) { - let bn: str = base.str; - let lc: *local = local_find_node(c, bn); + let lc: *local = local_find_node(c, base.str); if (lc != nil) { let tn: *node = lc.tnode; let lkind: i32 = -1; if (tn != nil) { lkind = tn.kind; }; - let sname: str; - sname.ptr = nil; sname.len = 0; if (lkind == N_TNAME) { sname = tn.str; }; if (lkind == N_TPTR) { let inner: *node = tn.lhs; @@ -820,18 +819,27 @@ fn node_isstr(c: *cgen, n: *node) bool = { if (inner.kind == N_TNAME) { sname = inner.str; }; }; }; - if (sname.len > 0) { - let si: *struct_info = struct_lookup(c, sname); - if (si != nil) { - let fi: *field_info = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return is_str_type(c, fi.tnode); - }; - fi = fi.finext; - }; + }; + }; + // Chained dot (`p.foo.bar`): use dot_inner_struct_ptr + // to resolve the inner chain to the *struct it lands + // on, then look up `fld` in that struct. + if (base.kind == N_DOT) { + let inner_t: *node = dot_inner_struct_ptr(c, base); + if (inner_t != nil) { + if (inner_t.kind == N_TNAME) { sname = inner_t.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return is_str_type(c, fi.tnode); }; + fi = fi.finext; }; }; }; @@ -862,6 +870,36 @@ fn type_node_isunsigned(t: *node) bool = { return false; }; +// type_is_8byte_primitive — does this type take exactly one 8-byte +// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive +// padded up to 8) rather than a wider aggregate? Used by N_LET +// zero-init to mirror C cgen's "only zero if sz == 8 at the type +// level" rule. Strings (16), slices (24), tagged unions (>=16), +// tuples (16), structs (varies), arrays — all fall through to +// false here even when their *slot* rounds up to 8. +fn type_is_8byte_primitive(c: *cgen, t: *node) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == N_TPTR) { return true; }; + if (k == N_TFN) { return true; }; + if (k == N_TCHAN) { return true; }; + if (k == N_TSLICE) { return false; }; + if (k == N_TARRAY) { return false; }; + if (k == N_TTUPLE) { return false; }; + if (k == N_TTAGGED){ return false; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return false; }; + // Struct alias: not a primitive even if the slot is 8B. + if (struct_lookup(c, nm) != nil) { return false; }; + // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). + // All of these get slot-padded to 8 and zero-init in C. + if (prim_size(nm) > 0) { return true; }; + return false; + }; + return false; +}; + // type_name_issigned — true for i8/i16/i32/i64/int/rune. fn type_name_issigned(nm: str) bool = { if (streq(nm, "i8")) { return true; }; @@ -964,6 +1002,12 @@ fn index_base_esz(c: *cgen, base: *node) i32 = { return 8; }; if (ft.kind == N_TSLICE) { return elem_size_of(ft); }; + // str-typed field: indexing yields one byte + // (`n.s[i]` where .s is str — matches C cgen's + // MOVZBQ for byte indexing). + if (ft.kind == N_TNAME) { + if (streq(ft.str, "str")) { return 1; }; + }; return 8; }; fi = fi.finext; @@ -1113,6 +1157,32 @@ fn node_isunsigned(c: *cgen, n: *node) bool = { return node_isunsigned(c, n.rhs); }; if (k == N_UN) { return node_isunsigned(c, n.lhs); }; + // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. + // Walks the base local's declared type and pulls the element + // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the + // compare-codegen for `p[i] >= 48u8` falls back to signed JGE + // instead of JAE, diverging from C 6c on byte indexing. + if (k == N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == N_IDENT) { + let lc: *local = local_find_node(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let elem: *node = nil; + if (tn.kind == N_TPTR) { elem = tn.lhs; }; + if (tn.kind == N_TARRAY) { elem = tn.lhs; }; + if (tn.kind == N_TSLICE) { elem = tn.lhs; }; + if (elem != nil) { + return type_node_isunsigned(elem); + }; + }; + }; + }; + }; + return false; + }; return false; }; @@ -2623,6 +2693,20 @@ fn cgstmt(c: *cgen, n: *node) void = { emit_off((off + 16): i64); emit_line("(BP)\n"); }; + } else { + // Bare `let x: T;` with no initializer. C cgen + // (cmd/6c/cgen.c:2181-2183) zero-inits only when + // the underlying type's natural size is 8 — pointers, + // i64/u64, function pointers, ints. Structs/arrays/ + // slices/strings/tagged/tuples are left for per-field + // writes. ww's slot_size pads struct slots up to 8, + // so we can't just check sz == 8: walk the type AST + // directly to make the same call. + if (type_is_8byte_primitive(c, n.lhs)) { + emit_line("\tMOVQ\t$0, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; }; c.last_was_return = 0; return; @@ -2679,6 +2763,42 @@ fn cgstmt(c: *cgen, n: *node) void = { return; }; + // Tuple-destructure assign: `a, b = call();`. The call's tuple + // return lands in (AX, DX); push DX to free it, store AX into + // the first lvalue, then pop DX into the second. Mirrors + // cmd/6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same + // as C — no fixture uses >2 today). + if (k == N_MASSIGN) { + if (n.rhs != nil) { cgexpr(c, n.rhs); }; + emit_line("\tPUSHQ\tDX\n"); + let l0: *node = n.list; + let l1: *node = nil; + if (l0 != nil) { l1 = l0.next; }; + if (l0 != nil) { + if (l0.kind == N_IDENT) { + let off: i32 = local_find(c, l0.str); + if (off != 0) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; + }; + }; + emit_line("\tPOPQ\tDX\n"); + if (l1 != nil) { + if (l1.kind == N_IDENT) { + let off: i32 = local_find(c, l1.str); + if (off != 0) { + emit_line("\tMOVQ\tDX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; + }; + }; + c.last_was_return = 0; + return; + }; + if (k == N_BREAK) { if (c.loop_top > 0) { let lbl: str = c.loop_end_buf[c.loop_top - 1]; diff --git a/selfhost/cmd/wwc/parse.ww b/selfhost/cmd/wwc/parse.ww index b012dbff..968e7bb0 100644 --- a/selfhost/cmd/wwc/parse.ww +++ b/selfhost/cmd/wwc/parse.ww @@ -744,9 +744,31 @@ fn parsestmt(p: *parser) *node = { expect_tok(p, TK_SEMI, "expected ';' after continue"); return newnode(p.a, N_CONTINUE, pf, pl, pc); }; - // expression statement + // expression statement, or tuple-destructure multi-assign: + // a, b = expr; + // Mirrors cmd/wwc/parse.c:1015-1031. We parse the first lvalue + // with parseexpr (matches the C side); subsequent lvalues go + // through parsebin(parseunary, 1) so the `=` stays for us to + // consume — parseexpr would absorb it. + let e: *node = parseexpr(p); + if (p.cur_kind == TK_COMMA) { + let m: *node = newnode(p.a, N_MASSIGN, pf, pl, pc); + let head: *node = e; + let tail: *node = e; + for (p.cur_kind == TK_COMMA) { + advance(p); + let lv: *node = parsebin(p, parseunary(p), 1); + tail.next = lv; + tail = lv; + }; + expect_tok(p, TK_ASSIGN, "expected '=' after multi-assign lvalues"); + m.rhs = parseexpr(p); + m.list = head; + expect_tok(p, TK_SEMI, "expected ';' after multi-assign"); + return m; + }; let n: *node = newnode(p.a, N_EXPRSTMT, pf, pl, pc); - n.lhs = parseexpr(p); + n.lhs = e; expect_tok(p, TK_SEMI, "expected ';' after expression statement"); return n; };