selfhost: is/as validity + let/return assignability checks

Three more structural checks from C check.c ported to selfhost,
at the AST level (no resolved tinfo).

is/as validity: e is T / e as T require e's declared type to be a
tagged union and T to name a variant. Mirrors the case-variant
check that just landed.

let init-type and return-type assignability: a new exprtype helper
infers an AST type-node for literal/ident/call/cast/?/as/is
expressions; isassignable approximates C type_assignable on the
shapes we can resolve — exact match, untyped numeric → typed
numeric, untyped nil → ptr/slice/chan/fn, variant inclusion, and
two-primitive-mismatch.

isassignable returns (ok, confident). When confident=false the
check emits no error — better to miss a real bug than fire a
false positive on a binary-op expression we can't infer. This
keeps existing selfhost code clean while still catching the
common typo cases (let x: bool = 42; return "hi" from i32 fn).

Naming: all new helpers follow Plan 9 run-together convention per
CLAUDE.md (`typeeqast`, `isassignable`, `exprtype`, ...). Earlier
work that used snake_case helpers (`case_variant_in`,
`check_match_exhaustive`, ...) got the same treatment — bulk
renamed in this commit.

Five new rows in 950_selfcheck exercise the new checks
(is-not-a-variant, two let mismatches, return mismatch, plus the
case-variant row already there).
This commit is contained in:
2026-05-12 03:49:09 +09:00
parent 751271a6bd
commit cb78abf9e9
4 changed files with 1144 additions and 150 deletions

View File

@@ -103,6 +103,23 @@ static const struct row rows[] = {
" };\n"
"};\n",
"case: not a variant" },
/* is T where T isn't a variant of the operand */
{ "fn pick() (i32 | str) = { return 1; };\n"
"fn caller() void = {\n"
" let v: (i32 | str) = pick();\n"
" if (v is f64) { };\n"
"};\n",
"is/as: not a variant" },
/* let init-type mismatch on primitives */
{ "fn caller() void = {\n"
" let x: bool = 42;\n"
"};\n",
"let: not assignable" },
/* return type mismatch */
{ "fn caller() i32 = {\n"
" return \"hi\";\n"
"};\n",
"return: not assignable" },
};
int
@@ -136,6 +153,14 @@ main(void)
"?: enclosing fn has no tagged-union return") != NULL);
err_present = err_present || (err && strstr(err,
"case: not a variant") != NULL);
err_present = err_present || (err && strstr(err,
"is/as: not a variant") != NULL);
err_present = err_present || (err && strstr(err,
"is/as: operand is not a tagged union") != NULL);
err_present = err_present || (err && strstr(err,
"let: not assignable") != NULL);
err_present = err_present || (err && strstr(err,
"return: not assignable") != NULL);
int ok;
if (expected_no_err) ok = !err_present;
else ok = got_match;