diff --git a/Makefile b/Makefile index a9d0c970..f475a758 100644 --- a/Makefile +++ b/Makefile @@ -421,6 +421,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_structlit_payload_run \ $(BIN)/test_forrange_fieldbase_run \ $(BIN)/test_errtype_compare \ + $(BIN)/test_intbinop_mismatch \ $(BIN)/test_tuple_elem_slice_len_run \ $(BIN)/test_str_forrange_loopvar_run \ $(BIN)/test_composite_call_arg \ @@ -2612,6 +2613,12 @@ $(BIN)/test_errtype_compare: test/wcc/949_errtype_compare.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_intbinop_mismatch: test/wcc/949_intbinop_mismatch.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_composite_call_arg: test/wcc/723_composite_call_arg.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6b0ded0b..9af3f42c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -12874,18 +12874,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { }; // unifyarith — usual-arithmetic-conversion analogue at the AST-tnode -// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec -// ref/harec/src/types.c type_promote. Trailing `return ltn` covers -// mismatched typed pairs; cstage flags the same shape — wwstage's -// checker stays silent here per existing discipline. +// layer. Mirrors cstage cmd/wcc/check.c:1034-1069 `unify_arith` and harec +// ref/harec/src/types.c type_promote. A typed/typed operand mismatch is +// loud (#26), aligning wwstage down to cstage; the only promotion left is +// the one-sided alias-vs-base chase (cstage check.c:1060-1067). // -// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn -// and rtn can be nil when an operand was an inherent-IDENT bail -// (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo- -// builtin callee with sym.decl == nil; #19 retires these as -// dedicated AST kinds). Propagation, not silent gap — asserttyped -// gates those idents at the consumer layer. -fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { +// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): ltn or rtn +// can be nil when an operand was an inherent-IDENT bail (exprtype +// N_IDENT arm L1596-1599 — SK_USE module ref or pseudo-builtin callee +// with sym.decl == nil; #19 retires these as dedicated AST kinds). +// Propagation, not silent gap — asserttyped gates those idents at the +// consumer layer, so a nil operand never reaches the loud reject below. +fn unifyarith(c: *checker, e: *node, ltn: *node, rtn: *node) *node = { let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn); let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn); if (lu && ru) { @@ -12901,10 +12901,54 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { if (ru) { if (isassignable(c, ltn, rtn, &conf)) { return ltn; }; }; + // A rune LITERAL is untyped_rune in cstage (cmd/wcc/check.c:1296), + // assignable to any integer or rune (cmd/wcc/type.c:379), so cstage's + // unify_arith accepts `ch == 'x'` / `c - '0'` and returns the typed + // side. wwstage stamps N_RUNELIT concrete `rune` (the #29 divergence, + // exprtype :2927), so the literal does not reach the isuntyped arms + // above. Mirror cstage here off the operand node so the rune-literal / + // integer mix stays valid (35 selfhost+lib sites) under the #26 reject. + if (e != nil) { + let lr: bool = e.lhs != nil && e.lhs.kind == nkind.N_RUNELIT; + let rr: bool = e.rhs != nil && e.rhs.kind == nkind.N_RUNELIT; + if (lr && isinttypeast(rtn)) { return rtn; }; + if (rr && isinttypeast(ltn)) { return ltn; }; + }; if (typeeqast(c, ltn, rtn)) { return ltn; }; - // Mismatched typed pair — return ltn so the binop stamps something; - // 5-lite-b: the trailing nil-on-mismatch shape was eliminated when - // the helper was split out of binoptype. + // One-sided alias vs its (transitive) base promotes to the alias + // side; alias-vs-different-alias stays rejected even when the bases + // agree. Mirrors cstage unify_arith (cmd/wcc/check.c:1060-1067) and + // harec type_promote (ref/harec/src/check.c:1083-1105). The error + // axis (varianterr) must agree on both sides so a `!i32` alias does + // NOT promote against a plain i32 (#246, 949_errtype_compare). + // A user alias is an SK_TYPE sym WITH a decl body; the primitives are + // SK_TYPE too but decl == nil (check.ww:95-112), so aliassym alone + // would mis-flag i32 as "named". This decl != nil gate is the wwstage + // analog of cstage's `kind == TY_NAMED` (primitives are TY_I32 etc). + let ls: *sym = aliassym(c, unwrapbang(ltn)); + let rs: *sym = aliassym(c, unwrapbang(rtn)); + let la: bool = ls != nil && ls.decl != nil; + let ra: bool = rs != nil && rs.decl != nil; + if (!(la && ra)) { + let da: *node = resolvealias(c, ltn); + let db: *node = resolvealias(c, rtn); + if (da != nil && db != nil + && varianterr(c, ltn) == varianterr(c, rtn) + && typeeqast(c, da, db)) { + if (la) { return ltn; }; + return rtn; + }; + }; + // ww requires explicit integer conversion in binary ops (Go-faithful, + // user-blessed #26): a typed/typed operand mismatch is loud, NOT + // promoted. Diverges from Hare type_promote's implicit signed-widen + // (ref/harec/src/check.c:1079/1337, STORAGE_INT 1129-1134). cstage + // already rejects the same shape (cmd/wcc/check.c:1068); this aligns + // wwstage down. A nil operand is an inherent-IDENT bail (see header) — + // propagate it, never reject. + if (ltn != nil && rtn != nil) { + deffolderr(c, e, "operands have differing types"); + }; return ltn; }; @@ -13019,7 +13063,7 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "arithmetic on non-numeric type"); }; - return unifyarith(c, ltn, rtn); + return unifyarith(c, e, ltn, rtn); }; if (op == tkind.TK_AMP || op == tkind.TK_PIPE || op == tkind.TK_CARET || op == tkind.TK_LSHIFT || @@ -13028,19 +13072,19 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !intkindast(c, rtn))) { deffolderr(c, e, "bitwise on non-integer type"); }; - return unifyarith(c, ltn, rtn); + return unifyarith(c, e, ltn, rtn); }; if (op == tkind.TK_EQ || op == tkind.TK_NEQ || op == tkind.TK_LT || op == tkind.TK_LE || op == tkind.TK_GT || op == tkind.TK_GE) { - // cstage routes every comparison through unify_arith (check.c:952/ - // 955 cbinop), which loud-rejects an error-typed operand paired - // with a differing type, e.g. strconv.invalid != i32. wwstage's - // unifyarith stays silent on generic typed mismatches (5-lite-b), - // but the error-type case is a real cs!=ww edge and must reject to - // match cstage (#246, rule 10). Scoped to error operands so the - // broad differing-types flip (typeeqast vs cstage type_eq - // asymmetry risk) stays out. + // cstage routes every comparison through unify_arith (check.c:1195/ + // 1200 cbinop), which loud-rejects a differing-types pair, e.g. + // strconv.invalid != i32. wwstage routes the ORDERED arm through + // unifyarith below (#26), but EQ/NEQ stays here on the error-type- + // only reject: a full unifyarith route for EQ/NEQ would expose the + // typeeqast-vs-cstage-type_eq asymmetry on struct / tuple / fn-ptr + // equality. The error-type case (#246, rule 10) is the one real + // cs!=ww edge EQ/NEQ must still reject. if (varianterr(c, ltn) || varianterr(c, rtn)) { if (!typeeqast(c, ltn, rtn)) { deffolderr(c, e, "operands have differing types"); @@ -13057,6 +13101,17 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "ordered comparison on non-numeric"); }; + // #26: an ordered comparison routes through unifyarith for + // the differing-types reject — cstage's cbinop sends every + // comparison through unify_arith (cmd/wcc/check.c:1195/1200), + // so `int < len(s):i32` is loud there. Guarded off error + // operands (the #246 block above already rejects those, so + // this avoids a double diagnostic) and scoped to ordered, + // keeping the typeeqast-vs-type_eq asymmetry risk off the + // untouched EQ/NEQ arm. + if (!varianterr(c, ltn) && !varianterr(c, rtn)) { + unifyarith(c, e, ltn, rtn); + }; }; return mktname(c, "bool"); }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 2a91df31..a2ce715b 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -2404,18 +2404,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { }; // unifyarith — usual-arithmetic-conversion analogue at the AST-tnode -// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec -// ref/harec/src/types.c type_promote. Trailing `return ltn` covers -// mismatched typed pairs; cstage flags the same shape — wwstage's -// checker stays silent here per existing discipline. +// layer. Mirrors cstage cmd/wcc/check.c:1034-1069 `unify_arith` and harec +// ref/harec/src/types.c type_promote. A typed/typed operand mismatch is +// loud (#26), aligning wwstage down to cstage; the only promotion left is +// the one-sided alias-vs-base chase (cstage check.c:1060-1067). // -// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn -// and rtn can be nil when an operand was an inherent-IDENT bail -// (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo- -// builtin callee with sym.decl == nil; #19 retires these as -// dedicated AST kinds). Propagation, not silent gap — asserttyped -// gates those idents at the consumer layer. -fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { +// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): ltn or rtn +// can be nil when an operand was an inherent-IDENT bail (exprtype +// N_IDENT arm L1596-1599 — SK_USE module ref or pseudo-builtin callee +// with sym.decl == nil; #19 retires these as dedicated AST kinds). +// Propagation, not silent gap — asserttyped gates those idents at the +// consumer layer, so a nil operand never reaches the loud reject below. +fn unifyarith(c: *checker, e: *node, ltn: *node, rtn: *node) *node = { let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn); let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn); if (lu && ru) { @@ -2431,10 +2431,54 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { if (ru) { if (isassignable(c, ltn, rtn, &conf)) { return ltn; }; }; + // A rune LITERAL is untyped_rune in cstage (cmd/wcc/check.c:1296), + // assignable to any integer or rune (cmd/wcc/type.c:379), so cstage's + // unify_arith accepts `ch == 'x'` / `c - '0'` and returns the typed + // side. wwstage stamps N_RUNELIT concrete `rune` (the #29 divergence, + // exprtype :2927), so the literal does not reach the isuntyped arms + // above. Mirror cstage here off the operand node so the rune-literal / + // integer mix stays valid (35 selfhost+lib sites) under the #26 reject. + if (e != nil) { + let lr: bool = e.lhs != nil && e.lhs.kind == nkind.N_RUNELIT; + let rr: bool = e.rhs != nil && e.rhs.kind == nkind.N_RUNELIT; + if (lr && isinttypeast(rtn)) { return rtn; }; + if (rr && isinttypeast(ltn)) { return ltn; }; + }; if (typeeqast(c, ltn, rtn)) { return ltn; }; - // Mismatched typed pair — return ltn so the binop stamps something; - // 5-lite-b: the trailing nil-on-mismatch shape was eliminated when - // the helper was split out of binoptype. + // One-sided alias vs its (transitive) base promotes to the alias + // side; alias-vs-different-alias stays rejected even when the bases + // agree. Mirrors cstage unify_arith (cmd/wcc/check.c:1060-1067) and + // harec type_promote (ref/harec/src/check.c:1083-1105). The error + // axis (varianterr) must agree on both sides so a `!i32` alias does + // NOT promote against a plain i32 (#246, 949_errtype_compare). + // A user alias is an SK_TYPE sym WITH a decl body; the primitives are + // SK_TYPE too but decl == nil (check.ww:95-112), so aliassym alone + // would mis-flag i32 as "named". This decl != nil gate is the wwstage + // analog of cstage's `kind == TY_NAMED` (primitives are TY_I32 etc). + let ls: *sym = aliassym(c, unwrapbang(ltn)); + let rs: *sym = aliassym(c, unwrapbang(rtn)); + let la: bool = ls != nil && ls.decl != nil; + let ra: bool = rs != nil && rs.decl != nil; + if (!(la && ra)) { + let da: *node = resolvealias(c, ltn); + let db: *node = resolvealias(c, rtn); + if (da != nil && db != nil + && varianterr(c, ltn) == varianterr(c, rtn) + && typeeqast(c, da, db)) { + if (la) { return ltn; }; + return rtn; + }; + }; + // ww requires explicit integer conversion in binary ops (Go-faithful, + // user-blessed #26): a typed/typed operand mismatch is loud, NOT + // promoted. Diverges from Hare type_promote's implicit signed-widen + // (ref/harec/src/check.c:1079/1337, STORAGE_INT 1129-1134). cstage + // already rejects the same shape (cmd/wcc/check.c:1068); this aligns + // wwstage down. A nil operand is an inherent-IDENT bail (see header) — + // propagate it, never reject. + if (ltn != nil && rtn != nil) { + deffolderr(c, e, "operands have differing types"); + }; return ltn; }; @@ -2549,7 +2593,7 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "arithmetic on non-numeric type"); }; - return unifyarith(c, ltn, rtn); + return unifyarith(c, e, ltn, rtn); }; if (op == tkind.TK_AMP || op == tkind.TK_PIPE || op == tkind.TK_CARET || op == tkind.TK_LSHIFT || @@ -2558,19 +2602,19 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !intkindast(c, rtn))) { deffolderr(c, e, "bitwise on non-integer type"); }; - return unifyarith(c, ltn, rtn); + return unifyarith(c, e, ltn, rtn); }; if (op == tkind.TK_EQ || op == tkind.TK_NEQ || op == tkind.TK_LT || op == tkind.TK_LE || op == tkind.TK_GT || op == tkind.TK_GE) { - // cstage routes every comparison through unify_arith (check.c:952/ - // 955 cbinop), which loud-rejects an error-typed operand paired - // with a differing type, e.g. strconv.invalid != i32. wwstage's - // unifyarith stays silent on generic typed mismatches (5-lite-b), - // but the error-type case is a real cs!=ww edge and must reject to - // match cstage (#246, rule 10). Scoped to error operands so the - // broad differing-types flip (typeeqast vs cstage type_eq - // asymmetry risk) stays out. + // cstage routes every comparison through unify_arith (check.c:1195/ + // 1200 cbinop), which loud-rejects a differing-types pair, e.g. + // strconv.invalid != i32. wwstage routes the ORDERED arm through + // unifyarith below (#26), but EQ/NEQ stays here on the error-type- + // only reject: a full unifyarith route for EQ/NEQ would expose the + // typeeqast-vs-cstage-type_eq asymmetry on struct / tuple / fn-ptr + // equality. The error-type case (#246, rule 10) is the one real + // cs!=ww edge EQ/NEQ must still reject. if (varianterr(c, ltn) || varianterr(c, rtn)) { if (!typeeqast(c, ltn, rtn)) { deffolderr(c, e, "operands have differing types"); @@ -2587,6 +2631,17 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "ordered comparison on non-numeric"); }; + // #26: an ordered comparison routes through unifyarith for + // the differing-types reject — cstage's cbinop sends every + // comparison through unify_arith (cmd/wcc/check.c:1195/1200), + // so `int < len(s):i32` is loud there. Guarded off error + // operands (the #246 block above already rejects those, so + // this avoids a double diagnostic) and scoped to ordered, + // keeping the typeeqast-vs-type_eq asymmetry risk off the + // untouched EQ/NEQ arm. + if (!varianterr(c, ltn) && !varianterr(c, rtn)) { + unifyarith(c, e, ltn, rtn); + }; }; return mktname(c, "bool"); }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 84e0ce85..1ea68df9 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -12874,18 +12874,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { }; // unifyarith — usual-arithmetic-conversion analogue at the AST-tnode -// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec -// ref/harec/src/types.c type_promote. Trailing `return ltn` covers -// mismatched typed pairs; cstage flags the same shape — wwstage's -// checker stays silent here per existing discipline. +// layer. Mirrors cstage cmd/wcc/check.c:1034-1069 `unify_arith` and harec +// ref/harec/src/types.c type_promote. A typed/typed operand mismatch is +// loud (#26), aligning wwstage down to cstage; the only promotion left is +// the one-sided alias-vs-base chase (cstage check.c:1060-1067). // -// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn -// and rtn can be nil when an operand was an inherent-IDENT bail -// (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo- -// builtin callee with sym.decl == nil; #19 retires these as -// dedicated AST kinds). Propagation, not silent gap — asserttyped -// gates those idents at the consumer layer. -fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { +// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): ltn or rtn +// can be nil when an operand was an inherent-IDENT bail (exprtype +// N_IDENT arm L1596-1599 — SK_USE module ref or pseudo-builtin callee +// with sym.decl == nil; #19 retires these as dedicated AST kinds). +// Propagation, not silent gap — asserttyped gates those idents at the +// consumer layer, so a nil operand never reaches the loud reject below. +fn unifyarith(c: *checker, e: *node, ltn: *node, rtn: *node) *node = { let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn); let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn); if (lu && ru) { @@ -12901,10 +12901,54 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { if (ru) { if (isassignable(c, ltn, rtn, &conf)) { return ltn; }; }; + // A rune LITERAL is untyped_rune in cstage (cmd/wcc/check.c:1296), + // assignable to any integer or rune (cmd/wcc/type.c:379), so cstage's + // unify_arith accepts `ch == 'x'` / `c - '0'` and returns the typed + // side. wwstage stamps N_RUNELIT concrete `rune` (the #29 divergence, + // exprtype :2927), so the literal does not reach the isuntyped arms + // above. Mirror cstage here off the operand node so the rune-literal / + // integer mix stays valid (35 selfhost+lib sites) under the #26 reject. + if (e != nil) { + let lr: bool = e.lhs != nil && e.lhs.kind == nkind.N_RUNELIT; + let rr: bool = e.rhs != nil && e.rhs.kind == nkind.N_RUNELIT; + if (lr && isinttypeast(rtn)) { return rtn; }; + if (rr && isinttypeast(ltn)) { return ltn; }; + }; if (typeeqast(c, ltn, rtn)) { return ltn; }; - // Mismatched typed pair — return ltn so the binop stamps something; - // 5-lite-b: the trailing nil-on-mismatch shape was eliminated when - // the helper was split out of binoptype. + // One-sided alias vs its (transitive) base promotes to the alias + // side; alias-vs-different-alias stays rejected even when the bases + // agree. Mirrors cstage unify_arith (cmd/wcc/check.c:1060-1067) and + // harec type_promote (ref/harec/src/check.c:1083-1105). The error + // axis (varianterr) must agree on both sides so a `!i32` alias does + // NOT promote against a plain i32 (#246, 949_errtype_compare). + // A user alias is an SK_TYPE sym WITH a decl body; the primitives are + // SK_TYPE too but decl == nil (check.ww:95-112), so aliassym alone + // would mis-flag i32 as "named". This decl != nil gate is the wwstage + // analog of cstage's `kind == TY_NAMED` (primitives are TY_I32 etc). + let ls: *sym = aliassym(c, unwrapbang(ltn)); + let rs: *sym = aliassym(c, unwrapbang(rtn)); + let la: bool = ls != nil && ls.decl != nil; + let ra: bool = rs != nil && rs.decl != nil; + if (!(la && ra)) { + let da: *node = resolvealias(c, ltn); + let db: *node = resolvealias(c, rtn); + if (da != nil && db != nil + && varianterr(c, ltn) == varianterr(c, rtn) + && typeeqast(c, da, db)) { + if (la) { return ltn; }; + return rtn; + }; + }; + // ww requires explicit integer conversion in binary ops (Go-faithful, + // user-blessed #26): a typed/typed operand mismatch is loud, NOT + // promoted. Diverges from Hare type_promote's implicit signed-widen + // (ref/harec/src/check.c:1079/1337, STORAGE_INT 1129-1134). cstage + // already rejects the same shape (cmd/wcc/check.c:1068); this aligns + // wwstage down. A nil operand is an inherent-IDENT bail (see header) — + // propagate it, never reject. + if (ltn != nil && rtn != nil) { + deffolderr(c, e, "operands have differing types"); + }; return ltn; }; @@ -13019,7 +13063,7 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "arithmetic on non-numeric type"); }; - return unifyarith(c, ltn, rtn); + return unifyarith(c, e, ltn, rtn); }; if (op == tkind.TK_AMP || op == tkind.TK_PIPE || op == tkind.TK_CARET || op == tkind.TK_LSHIFT || @@ -13028,19 +13072,19 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !intkindast(c, rtn))) { deffolderr(c, e, "bitwise on non-integer type"); }; - return unifyarith(c, ltn, rtn); + return unifyarith(c, e, ltn, rtn); }; if (op == tkind.TK_EQ || op == tkind.TK_NEQ || op == tkind.TK_LT || op == tkind.TK_LE || op == tkind.TK_GT || op == tkind.TK_GE) { - // cstage routes every comparison through unify_arith (check.c:952/ - // 955 cbinop), which loud-rejects an error-typed operand paired - // with a differing type, e.g. strconv.invalid != i32. wwstage's - // unifyarith stays silent on generic typed mismatches (5-lite-b), - // but the error-type case is a real cs!=ww edge and must reject to - // match cstage (#246, rule 10). Scoped to error operands so the - // broad differing-types flip (typeeqast vs cstage type_eq - // asymmetry risk) stays out. + // cstage routes every comparison through unify_arith (check.c:1195/ + // 1200 cbinop), which loud-rejects a differing-types pair, e.g. + // strconv.invalid != i32. wwstage routes the ORDERED arm through + // unifyarith below (#26), but EQ/NEQ stays here on the error-type- + // only reject: a full unifyarith route for EQ/NEQ would expose the + // typeeqast-vs-cstage-type_eq asymmetry on struct / tuple / fn-ptr + // equality. The error-type case (#246, rule 10) is the one real + // cs!=ww edge EQ/NEQ must still reject. if (varianterr(c, ltn) || varianterr(c, rtn)) { if (!typeeqast(c, ltn, rtn)) { deffolderr(c, e, "operands have differing types"); @@ -13057,6 +13101,17 @@ fn binoptype(c: *checker, e: *node) *node = { (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "ordered comparison on non-numeric"); }; + // #26: an ordered comparison routes through unifyarith for + // the differing-types reject — cstage's cbinop sends every + // comparison through unify_arith (cmd/wcc/check.c:1195/1200), + // so `int < len(s):i32` is loud there. Guarded off error + // operands (the #246 block above already rejects those, so + // this avoids a double diagnostic) and scoped to ordered, + // keeping the typeeqast-vs-type_eq asymmetry risk off the + // untouched EQ/NEQ arm. + if (!varianterr(c, ltn) && !varianterr(c, rtn)) { + unifyarith(c, e, ltn, rtn); + }; }; return mktname(c, "bool"); }; diff --git a/test/wcc/949_intbinop_mismatch.c b/test/wcc/949_intbinop_mismatch.c new file mode 100644 index 00000000..38b7c87c --- /dev/null +++ b/test/wcc/949_intbinop_mismatch.c @@ -0,0 +1,340 @@ +/* + * 949_intbinop_mismatch — #26: ww requires an explicit integer + * conversion in binary ops (Go-faithful, user-blessed). cstage's cbinop + * routes arithmetic/bitwise/comparison through unify_arith + * (cmd/wcc/check.c:1034), which loud-rejects a typed/typed operand + * mismatch (check.c:1068). The wwstage checker's unifyarith returned the + * lhs type for any mismatch (silent accept) AND the comparison arm never + * called it at all, so `int < len(s)` (len() = i32) and `int & i32` + * silently compiled where cstage rejects — a rule-10 break (and a latent + * signed/unsigned miscompile for `int < uint`). + * + * The fix (selfhost/cmd/wcc/check.ww) aligns wwstage DOWN: unifyarith + * loud-rejects a mismatched typed pair, keeping only cstage's one-sided + * alias-vs-base promotion (check.c:1060), and the ORDERED comparison arm + * now routes through unifyarith. A rune LITERAL stays assignable to an + * integer (cstage's untyped_rune, type.c:379), so `c - '0'` is unaffected. + * + * K_BUILDERR rows: build FAILS with the differing-types diagnostic on + * BOTH drivers. K_RUN rows: build+run exit 0 on BOTH drivers AND the + * cs==ww .s is byte-identical — the positive controls pin that matched + * pairs, an explicit cast, the alias-vs-base promotion (part (a), the + * LOAD-BEARING row), and a rune-literal arithmetic mix all still pass. + * + * 940/945 precedent: ww_ww builds /tmp fixtures, so the selfhost-tree + * sibling-write race does not apply. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa), cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +static int +file_contains(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[8192]; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = '\0'; + return strstr(buf, needle) != NULL; +} + +#define K_RUN 0 /* build+run both drivers, exit==want, + cs==ww byte-id */ +#define K_BUILDERR 1 /* build must FAIL with experr on BOTH drivers (rule 7) */ + +struct row { const char *label; const char *src; int kind; int want; + const char *experr; }; + +static const struct row rows[] = { + /* headline #26: `int < len(s)` (len() returns i32) — the latent + * signed-mix comparison that wwstage silently accepted. */ + { "int_lt_len_i32", + "package main;\n" + "export fn main() i32 = {\n" + " let a: int = 1;\n" + " let s: []u8 = [];\n" + " if (a < len(s)) { return 1; };\n" + " return 0;\n" + "};\n", + K_BUILDERR, 0, "operands have differing types" }, + /* signed/unsigned mismatch — the miscompile-prone shape. */ + { "int_lt_uint", + "package main;\n" + "export fn main() i32 = {\n" + " let a: int = 1;\n" + " let b: uint = 2;\n" + " if (a < b) { return 1; };\n" + " return 0;\n" + "};\n", + K_BUILDERR, 0, "operands have differing types" }, + /* differing-width signed pair. */ + { "int_lt_i64", + "package main;\n" + "export fn main() i32 = {\n" + " let a: int = 1;\n" + " let b: i64 = 2i64;\n" + " if (a < b) { return 1; };\n" + " return 0;\n" + "};\n", + K_BUILDERR, 0, "operands have differing types" }, + /* bitwise feeds the same unifyarith path (check.ww :2598). */ + { "int_and_i32", + "package main;\n" + "export fn main() i32 = {\n" + " let a: int = 1;\n" + " let b: i32 = 2;\n" + " let r: int = a & b;\n" + " if (r < 0) { return 1; };\n" + " return 0;\n" + "};\n", + K_BUILDERR, 0, "operands have differing types" }, + /* positive control: matched int/int comparison. */ + { "int_lt_int", + "package main;\n" + "export fn main() i32 = {\n" + " let a: int = 1;\n" + " let b: int = 2;\n" + " if (a < b) { return 0; };\n" + " return 0;\n" + "};\n", + K_RUN, 0, NULL }, + /* positive control: matched i32/i32. */ + { "i32_lt_i32", + "package main;\n" + "export fn main() i32 = {\n" + " let a: i32 = 1;\n" + " let b: i32 = 2;\n" + " if (a < b) { return 0; };\n" + " return 0;\n" + "};\n", + K_RUN, 0, NULL }, + /* positive control: i32 < len(s) — both i32, the canonical loop. */ + { "i32_lt_len", + "package main;\n" + "export fn main() i32 = {\n" + " let s: []u8 = [];\n" + " let a: i32 = 0;\n" + " if (a < len(s)) { return 0; };\n" + " return 0;\n" + "};\n", + K_RUN, 0, NULL }, + /* positive control: explicit cast resolves the #26 mix. */ + { "int_cast_len", + "package main;\n" + "export fn main() i32 = {\n" + " let s: []u8 = [];\n" + " let a: int = 0;\n" + " if (a < len(s): int) { return 0; };\n" + " return 0;\n" + "};\n", + K_RUN, 0, NULL }, + /* LOAD-BEARING positive control for part (a): a one-sided alias vs + * its base promotes (cstage check.c:1060). Drops if the alias-chase + * arm is missing or mis-detects a primitive as a named type. */ + { "alias_vs_base", + "package main;\n" + "type myint = i32;\n" + "export fn main() i32 = {\n" + " let a: myint = 1: myint;\n" + " let b: i32 = 2;\n" + " if (a < b) { return 0; };\n" + " return 0;\n" + "};\n", + K_RUN, 0, NULL }, + /* positive control: a rune LITERAL stays assignable to an integer + * (cstage's untyped_rune, type.c:379); `c - '0'` must NOT trip the + * #26 reject. */ + { "rune_lit_arith", + "package main;\n" + "export fn main() i32 = {\n" + " let c: u8 = 53u8;\n" + " let r: u8 = c - '0';\n" + " if (r < 10u8) { return 0; };\n" + " return 0;\n" + "};\n", + K_RUN, 0, NULL }, + /* LOAD-BEARING positive control: a chained same-enum bitwise OR + * (`flag.A | flag.B | flag.C`, the w6l os.flag pattern). Both operands + * are the SAME enum type, which cstage's unify_arith accepts via + * type_eq's identity check (type.c:250) — wwstage must accept it too. + * Drops if typeeqast lacks the identity fast-path: the #26 reject then + * over-rejects a same-enum binop, breaking the w6l self-compile (994). */ + { "enum_flag_or", + "package main;\n" + "type flag = enum uint { A = 1, B = 2, C = 4 };\n" + "export fn main() i32 = {\n" + " let f: flag = flag.A | flag.B | flag.C;\n" + " return (f: i32) - 7;\n" + "};\n", + K_RUN, 0, NULL }, +}; + +/* build+run via a driver (ww / ww_ww); returns 0 pass, nonzero fail. */ +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], errf[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ibm_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/ibm_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/ibm_%d_e_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s >/dev/null 2>%s", + tmpdir, driver, src, errf); + int brc = runwait(cmd); + + if (r->kind == K_BUILDERR) { + int ok = (brc != 0) + && (r->experr == NULL || file_contains(errf, r->experr)); + if (!ok) + fprintf(stderr, "row[%s]: %s expected loud #26 builderr " + "(brc=%d)\n", r->label, driver, brc); + unlink(src); unlink(errf); rmdir(tmpdir); + return ok ? 0 : 1; + } + + if (brc != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); unlink(errf); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); unlink(errf); rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "row[%s]: %s exit %d, want %d\n", + r->label, driver, got, r->want); + return 1; + } + return 0; +} + +/* cs==ww .s byte-id (rule 10) for the K_RUN rows. */ +static int +byteid(const char *w6c, const char *w6c_ww, const struct row *r, int i) +{ + char src[96], cs_s[96], ws_s[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ibm_bi_%d_%d.ww", getpid(), i); + snprintf(cs_s, sizeof cs_s, "/tmp/ibm_bi_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/ibm_bi_%d_%d_ww.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + int rc = 0; + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src); + if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c failed\n", r->label); rc = 1; } + else { + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c_ww failed\n", r->label); rc = 1; } + else if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER (#26)\n", + r->label); + rc = 1; + } + } + unlink(src); unlink(cs_s); unlink(ws_s); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640], wdrv[640], w6c[640], w6c_ww[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + struct { const char *name; const char *path; int gated; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "intbinop_mismatch: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + total++; + if (run_driver(drivers[d].path, &rows[i], i) != 0) fail++; + } + } + + if (access(w6c_ww, X_OK) == 0) { + for (int i = 0; i < n; i++) { + if (rows[i].kind != K_RUN) continue; + total++; + if (byteid(w6c, w6c_ww, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "intbinop_mismatch: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("intbinop_mismatch: %d/%d ok\n", total, total); + return 0; +}