selfhost/cmd/wcc/check+test: reject bare alloc(v) at let-init in wwstage

Cstage's check.c:981-1006/1052-1082 builds a real (*T|nomem) /
([]T|nomem) return type for the alloc builtin; wwstage was returning
nil from exprtype's N_CALL arm (alloc is SK_FN with decl=nil under
seedprimitives), and checkletassign early-returned on nil src,
silently accepting `let p: *T = alloc(v);` without `!`. Stage
asymmetry that #30 papered over until now.

Three coordinated edits in check.ww:
- exprtype N_CALL: synthesize N_TTAGGED{N_TPTR{argt}, nomem} or
  {N_TSLICE{u8}, nomem} for bare alloc (same-module gated, mirrors
  cstage check.c:981-985 / task #23).
- exprtype N_TRYUNW: project the success variant so `let p:*T =
  alloc(v)!;` resolves rhs to *T.
- isassignable: tagged → non-tagged is unconditionally not
  assignable, forcing match/?/!.

950_selfcheck.c rows pin both ptr and slice forms.
This commit is contained in:
2026-05-20 00:03:43 +09:00
parent dd27ce3339
commit 6f10c832a4
4 changed files with 240 additions and 0 deletions

View File

@@ -7423,6 +7423,54 @@ fn exprtype(c: *checker, e: *node) *node = {
if (k == nkind.N_CALL) {
let callee: *node = e.lhs;
if (callee == nil) { return nil; };
// #31: synthesize the `alloc(value)` / `alloc([], n)` builtin
// return shape so checkletassign sees the same `(*T | nomem)` /
// `([]T | nomem)` cstage's check.c stamps at L981-1006. Without
// this, exprtype returns the seeded decl's nil lhs and the let
// silently accepts `let p: *T = alloc(v);` — rule 10 trap.
// Same-module gate mirrors cstage's `c->cur_mod &&
// scope_lookup_in_module(...)` check from task #23.
if (callee.kind == nkind.N_IDENT) {
if (streq(callee.str, "alloc")) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) {
shadowed = true;
};
};
if (!shadowed) {
if (e.list != nil) {
// Slice form: `alloc([], n)`.
if (e.list.kind == nkind.N_ARRLIT) {
if (e.list.list == nil) {
if (e.list.next != nil) {
if (e.list.next.next == nil) {
let sl: *node = newnode(c.a, nkind.N_TSLICE, "", 0, 0);
sl.lhs = mktname(c, "u8");
let nome: *node = mktname(c, "nomem");
sl.next = nome;
let tt: *node = newnode(c.a, nkind.N_TTAGGED, "", 0, 0);
tt.list = sl;
return tt;
};
};
};
};
// Value form: `alloc(value)`.
if (e.list.next == nil) {
let argt: *node = exprtype(c, e.list);
let ptr: *node = newnode(c.a, nkind.N_TPTR, "", 0, 0);
ptr.lhs = argt;
let nome: *node = mktname(c, "nomem");
ptr.next = nome;
let tt: *node = newnode(c.a, nkind.N_TTAGGED, "", 0, 0);
tt.list = ptr;
return tt;
};
};
};
};
};
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
@@ -7453,6 +7501,24 @@ fn exprtype(c: *checker, e: *node) *node = {
};
return ou.list;
};
if (k == nkind.N_TRYUNW) {
// `e!` abort-on-error unwrap; success variant is what the
// receiver gets, identical to `?` shape modulo control flow.
// #31: required so `let p: *T = alloc(v)!;` resolves to *T.
let opt: *node = exprtype(c, e.lhs);
let ou: *node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != nkind.N_TTAGGED) { return nil; };
if (taggedhaserr(c, ou)) {
let v: *node = ou.list;
for (v != nil) {
if (!iserrvariant(c, ou, v)) { return v; };
v = v.next;
};
return nil;
};
return ou.list;
};
if (k == nkind.N_TYPEASSERT) {
// `e as T` → T
return e.rhs;
@@ -7604,6 +7670,12 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
*confident = false;
return true;
};
// tagged → non-tagged: requires `?` / `!` / match to project a
// variant. #31: this is what traps `let p: *T = alloc(v);`
// where the builtin returns `(*T | nomem)` and the LHS is bare.
if (su.kind == nkind.N_TTAGGED && du.kind != nkind.N_TTAGGED) {
return false;
};
// Two known primitives with different names are confidently
// incompatible. `i32 ↔ bool`, `str ↔ i32`, etc.
if (du.kind == nkind.N_TNAME && su.kind == nkind.N_TNAME) {

View File

@@ -561,6 +561,54 @@ fn exprtype(c: *checker, e: *node) *node = {
if (k == nkind.N_CALL) {
let callee: *node = e.lhs;
if (callee == nil) { return nil; };
// #31: synthesize the `alloc(value)` / `alloc([], n)` builtin
// return shape so checkletassign sees the same `(*T | nomem)` /
// `([]T | nomem)` cstage's check.c stamps at L981-1006. Without
// this, exprtype returns the seeded decl's nil lhs and the let
// silently accepts `let p: *T = alloc(v);` — rule 10 trap.
// Same-module gate mirrors cstage's `c->cur_mod &&
// scope_lookup_in_module(...)` check from task #23.
if (callee.kind == nkind.N_IDENT) {
if (streq(callee.str, "alloc")) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) {
shadowed = true;
};
};
if (!shadowed) {
if (e.list != nil) {
// Slice form: `alloc([], n)`.
if (e.list.kind == nkind.N_ARRLIT) {
if (e.list.list == nil) {
if (e.list.next != nil) {
if (e.list.next.next == nil) {
let sl: *node = newnode(c.a, nkind.N_TSLICE, "", 0, 0);
sl.lhs = mktname(c, "u8");
let nome: *node = mktname(c, "nomem");
sl.next = nome;
let tt: *node = newnode(c.a, nkind.N_TTAGGED, "", 0, 0);
tt.list = sl;
return tt;
};
};
};
};
// Value form: `alloc(value)`.
if (e.list.next == nil) {
let argt: *node = exprtype(c, e.list);
let ptr: *node = newnode(c.a, nkind.N_TPTR, "", 0, 0);
ptr.lhs = argt;
let nome: *node = mktname(c, "nomem");
ptr.next = nome;
let tt: *node = newnode(c.a, nkind.N_TTAGGED, "", 0, 0);
tt.list = ptr;
return tt;
};
};
};
};
};
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
@@ -591,6 +639,24 @@ fn exprtype(c: *checker, e: *node) *node = {
};
return ou.list;
};
if (k == nkind.N_TRYUNW) {
// `e!` abort-on-error unwrap; success variant is what the
// receiver gets, identical to `?` shape modulo control flow.
// #31: required so `let p: *T = alloc(v)!;` resolves to *T.
let opt: *node = exprtype(c, e.lhs);
let ou: *node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != nkind.N_TTAGGED) { return nil; };
if (taggedhaserr(c, ou)) {
let v: *node = ou.list;
for (v != nil) {
if (!iserrvariant(c, ou, v)) { return v; };
v = v.next;
};
return nil;
};
return ou.list;
};
if (k == nkind.N_TYPEASSERT) {
// `e as T` → T
return e.rhs;
@@ -742,6 +808,12 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
*confident = false;
return true;
};
// tagged → non-tagged: requires `?` / `!` / match to project a
// variant. #31: this is what traps `let p: *T = alloc(v);`
// where the builtin returns `(*T | nomem)` and the LHS is bare.
if (su.kind == nkind.N_TTAGGED && du.kind != nkind.N_TTAGGED) {
return false;
};
// Two known primitives with different names are confidently
// incompatible. `i32 ↔ bool`, `str ↔ i32`, etc.
if (du.kind == nkind.N_TNAME && su.kind == nkind.N_TNAME) {

View File

@@ -7423,6 +7423,54 @@ fn exprtype(c: *checker, e: *node) *node = {
if (k == nkind.N_CALL) {
let callee: *node = e.lhs;
if (callee == nil) { return nil; };
// #31: synthesize the `alloc(value)` / `alloc([], n)` builtin
// return shape so checkletassign sees the same `(*T | nomem)` /
// `([]T | nomem)` cstage's check.c stamps at L981-1006. Without
// this, exprtype returns the seeded decl's nil lhs and the let
// silently accepts `let p: *T = alloc(v);` — rule 10 trap.
// Same-module gate mirrors cstage's `c->cur_mod &&
// scope_lookup_in_module(...)` check from task #23.
if (callee.kind == nkind.N_IDENT) {
if (streq(callee.str, "alloc")) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) {
shadowed = true;
};
};
if (!shadowed) {
if (e.list != nil) {
// Slice form: `alloc([], n)`.
if (e.list.kind == nkind.N_ARRLIT) {
if (e.list.list == nil) {
if (e.list.next != nil) {
if (e.list.next.next == nil) {
let sl: *node = newnode(c.a, nkind.N_TSLICE, "", 0, 0);
sl.lhs = mktname(c, "u8");
let nome: *node = mktname(c, "nomem");
sl.next = nome;
let tt: *node = newnode(c.a, nkind.N_TTAGGED, "", 0, 0);
tt.list = sl;
return tt;
};
};
};
};
// Value form: `alloc(value)`.
if (e.list.next == nil) {
let argt: *node = exprtype(c, e.list);
let ptr: *node = newnode(c.a, nkind.N_TPTR, "", 0, 0);
ptr.lhs = argt;
let nome: *node = mktname(c, "nomem");
ptr.next = nome;
let tt: *node = newnode(c.a, nkind.N_TTAGGED, "", 0, 0);
tt.list = ptr;
return tt;
};
};
};
};
};
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
@@ -7453,6 +7501,24 @@ fn exprtype(c: *checker, e: *node) *node = {
};
return ou.list;
};
if (k == nkind.N_TRYUNW) {
// `e!` abort-on-error unwrap; success variant is what the
// receiver gets, identical to `?` shape modulo control flow.
// #31: required so `let p: *T = alloc(v)!;` resolves to *T.
let opt: *node = exprtype(c, e.lhs);
let ou: *node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != nkind.N_TTAGGED) { return nil; };
if (taggedhaserr(c, ou)) {
let v: *node = ou.list;
for (v != nil) {
if (!iserrvariant(c, ou, v)) { return v; };
v = v.next;
};
return nil;
};
return ou.list;
};
if (k == nkind.N_TYPEASSERT) {
// `e as T` → T
return e.rhs;
@@ -7604,6 +7670,12 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
*confident = false;
return true;
};
// tagged → non-tagged: requires `?` / `!` / match to project a
// variant. #31: this is what traps `let p: *T = alloc(v);`
// where the builtin returns `(*T | nomem)` and the LHS is bare.
if (su.kind == nkind.N_TTAGGED && du.kind != nkind.N_TTAGGED) {
return false;
};
// Two known primitives with different names are confidently
// incompatible. `i32 ↔ bool`, `str ↔ i32`, etc.
if (du.kind == nkind.N_TNAME && su.kind == nkind.N_TNAME) {