Files
ww/cmd/wcc/check.c
Hojun-Cho 939c984f51 wcc,ww: prepend synth use test; user fn run coexists with runner (M4 E2, #80)
The -T harness synthesized `use test;` after name-binding, so the lib/test runner run keyed the bare scope and collided with a user-defined bare fn run — a spurious "duplicate fn run" reject (the E1 tolerance seam). Prepending the synth use before binding keys the runner as test.run in the test module namespace, distinct from the user bare run; the two coexist. Hare-faithful: the runner is its own test module (ref/hare/test/+test.ha:97). Inverts attest_userrun.ww from the #23-mandated reject to a coexist fixture; gate asserts exactly 1 TEXT run + 1 TEXT test.run on the -T asm (distinct symbols, not a dead-dup). Closes #80.
2026-06-17 23:54:21 +09:00

3461 lines
126 KiB
C

/*
* check.c — name resolution + type checking pass.
*
* Two-stage:
* 1) collect: walk top-level decls and install Syms with stub types.
* 2) resolve: expand types, check fn bodies and def initialisers.
*
* Errors do not stop the walk — we keep going so the user gets many
* diagnostics from one run. Nodes get their resolved Type attached.
*/
#include "ww.h"
#include <string.h>
static void cstmt(Checker*, Node*);
static Type *cexpr(Checker*, Node*);
static Type *resolve_type(Checker*, Node*);
static void check_module_shadow(Checker*, const char *name, Pos,
const char *kindstr);
static Type *
err(Checker *c, Pos p, const char *fmt, ...)
{
(void)c;
va_list ap;
fprintf(errout ? errout : stderr,
"%s:%d:%d: error: ", p.file ? p.file : "?", p.line, p.col);
va_start(ap, fmt);
vfprintf(errout ? errout : stderr, fmt, ap);
va_end(ap);
fputc('\n', errout ? errout : stderr);
c->errs++;
return ty_err;
}
static Type *
lookup_builtin(const char *name)
{
if (strcmp(name, "void") == 0) return ty_void;
if (strcmp(name, "bool") == 0) return ty_bool;
if (strcmp(name, "rune") == 0) return ty_rune;
if (strcmp(name, "i8") == 0) return ty_i8;
if (strcmp(name, "i16") == 0) return ty_i16;
if (strcmp(name, "i32") == 0) return ty_i32;
if (strcmp(name, "i64") == 0) return ty_i64;
if (strcmp(name, "u8") == 0) return ty_u8;
if (strcmp(name, "u16") == 0) return ty_u16;
if (strcmp(name, "u32") == 0) return ty_u32;
if (strcmp(name, "u64") == 0) return ty_u64;
if (strcmp(name, "int") == 0) return ty_int;
if (strcmp(name, "uint") == 0) return ty_uint;
if (strcmp(name, "uintptr") == 0) return ty_uintptr;
if (strcmp(name, "size") == 0) return ty_size; /* #85 fold-2 */
if (strcmp(name, "opaque") == 0) return ty_opaque; /* #108(a) */
if (strcmp(name, "f32") == 0) return ty_f32;
if (strcmp(name, "f64") == 0) return ty_f64;
if (strcmp(name, "str") == 0) return ty_str;
if (strcmp(name, "never") == 0) return ty_never;
if (strcmp(name, "nomem") == 0) return ty_nomem; /* #29 */
return NULL;
}
static const char *decl_mod(Node *file, Node *d);
static const char *use_path(Node *file, const char *curmod, const char *alias);
static void resolve_typedecl(Checker *c, Node *d);
static Type *
resolve_typename(Checker *c, Node *n)
{
const char *nm = n->str;
Type *bi = lookup_builtin(nm);
if (bi) return bi;
/* #225: kind-filtered so a same-named value binding (param/let/fn)
* in a closer scope can't hide the type binding it shadows. */
Sym *s = scope_lookup_type(c->cur, c->cur_mod, nm);
if (s == NULL && nm) {
/* module-qualified: io.stream → strip the last dot prefix
* and look up the leaf, filtering on the importing module's
* name so `bufio.stream` and `io.stream` can coexist in the
* same flat scope. `m->use_alias` covers the self-import
* case where the imported module declares a type with the
* same name as the module itself (e.g. `random.random`). */
const char *dot = strrchr(nm, '.');
if (dot) {
char head[128] = {0};
size_t hl = (size_t)(dot - nm);
if (hl < sizeof head) memcpy(head, nm, hl);
Sym *m = scope_lookup(c->cur, head);
if (m && (m->kind == SK_USE || m->use_alias)) {
/* M1 #22: map the qualifier alias to its dotted
* import path (symbols are path-keyed). */
const char *mk = use_path(c->file, c->cur_mod,
head);
if (mk == NULL) mk = head;
s = scope_lookup_in_module(c->cur, mk,
dot + 1);
}
}
}
if (s == NULL || s->kind != SK_TYPE)
return err(c, n->pos, "unknown type '%s'", nm);
/* #62: a typedecl body may reference a typedecl declared LATER in
* the (driver-concatenated) file. Layout is a fixed point over the
* whole module — a function of the member types alone, never of
* decl order — so resolve the referenced decl on demand before
* handing its type out; no consumer may ever see the size-0
* placeholder. Mirrors wwstage's demand-driven tinfofornode, the
* measured order-independent side. */
/* peel-ok: under==NULL probes resolve state, not a dealias */
if (s->type && s->type->kind == TY_NAMED && s->type->under == NULL
&& s->decl && s->decl->kind == N_TYPEDECL)
resolve_typedecl(c, s->decl);
return s->type;
}
/* Variant identity for tagged unions. Mirrors cg_variant_match in
* cgen: NAMED types are nominal (pointer-identical) and don't unify
* with their underlying; everything else is structural type_eq. */
static int
variant_match(Type *a, Type *b)
{
if (a == NULL || b == NULL) return 0;
if (a->kind == TY_NAMED && b->kind == TY_NAMED) return a == b;
if (a->kind == TY_NAMED || b->kind == TY_NAMED) return 0;
return type_eq(a, b);
}
static int
variant_present(Tparam *head, Type *vt)
{
for (Tparam *p = head; p; p = p->next)
if (variant_match(p->type, vt)) return 1;
return 0;
}
/* tagged_array_variant — #5/#60: when `src` is boxed into the tagged
* union `dst`, return the variant `src` selects IFF that variant chases
* to TY_ARRAY, else NULL. The array-payload box is unwired in cgen: the
* widen emitter zero-fills the slot at box materialization (a silent
* MOVQ $0 payload drop), so the construct must be rejected at the
* checker until the faithful array-block-store lands (deferred task #6).
* The tagged TYPE-decl with an array variant stays legal — test 944
* declares (void|size|[5]size) and only boxes the narrow `size` variant
* — only the array-variant CONSTRUCTION is refused. Mirrors the
* concrete->tagged variant select in type_assignable (type.c:324-343)
* so the variant chosen here is the one the box would materialize. */
static Type *
tagged_array_variant(Type *dst, Type *src)
{
if (dst == NULL || src == NULL) return NULL;
Type *du = type_chase_named(dst);
Type *su = type_chase_named(src);
if (du == NULL || du->kind != TY_TAGGED) return NULL;
if (su && su->kind == TY_TAGGED) return NULL; /* tagged->tagged */
for (Tparam *p = du->params; p; p = p->next) {
Type *pu = type_chase_named(p->type);
if (pu && pu->kind == TY_TAGGED) {
if (type_eq(p->type, src)) return NULL;
continue;
}
if (type_assignable(p->type, src)) {
if (pu && pu->kind == TY_ARRAY) return p->type;
return NULL;
}
}
return NULL;
}
/* match_yield_type — walk a match arm's body looking for the type
* of its first `yield expr;` statement. Returns NULL if no yield
* was found. Doesn't descend into nested match bodies — each match
* is its own yield scope. */
static Type *
match_yield_type(Node *body)
{
if (body == NULL) return NULL;
if (body->kind == N_YIELD) return body->lhs ? body->lhs->type : NULL;
if (body->kind == N_MATCH) return NULL; /* inner match: own scope */
if (body->kind == N_BLOCK) {
for (Node *s = body->list; s; s = s->next) {
Type *t = match_yield_type(s);
if (t) return t;
}
return NULL;
}
if (body->kind == N_IF) {
Type *t = match_yield_type(body->body);
if (t) return t;
return match_yield_type(body->els);
}
if (body->kind == N_FOR || body->kind == N_FORRANGE)
return match_yield_type(body->body);
return NULL;
}
/* tagged_has_errflag — true iff any variant is `!`-marked. Determines
* whether the union uses Hare's explicit error subset or the legacy
* "first variant = success" convention. */
static int
tagged_has_errflag(Type *u)
{
if (u == NULL || u->kind != TY_TAGGED) return 0;
for (Tparam *p = u->params; p; p = p->next)
if (p->type && p->type->iserror) return 1;
return 0;
}
/* tagged_is_error_variant — does `v` (a variant of `u`) belong to
* the error subset? Explicit-flag mode: only variants with iserror=1.
* Legacy mode (no flags): everything except the first variant. */
static int
tagged_is_error_variant(Type *u, Type *v)
{
if (u == NULL || u->kind != TY_TAGGED || v == NULL) return 0;
if (tagged_has_errflag(u)) return v->iserror != 0;
/* legacy: first variant is success, rest are errors */
return u->params && u->params->type != v;
}
/* tagged_success_type — the success variant's type. Explicit-flag
* mode: the first non-flagged variant. Legacy: the first variant. */
static Type *
tagged_success_type(Type *u)
{
if (u == NULL || u->kind != TY_TAGGED) return NULL;
if (tagged_has_errflag(u)) {
for (Tparam *p = u->params; p; p = p->next)
if (p->type && !p->type->iserror) return p->type;
return NULL;
}
return u->params ? u->params->type : NULL;
}
/* fold_int_literal — fold the literal subset usable for top-level
* constant slots: int/rune literal, true/false/nil, and a unary
* +/-/~ over the same. No diagnostics; the caller decides what a
* miss means. Shared between eval_enum_value (literal leaves) and
* emit_defs (top-level def rhs).
*
* Whitelist kept tight on purpose: no N_IDENT (no sibling lookup,
* no symbol resolution), no N_BIN. Anything richer belongs in
* eval_enum_value, which calls this for its literal leaves and
* handles sibling/op recursion itself. */
int
fold_int_literal(Node *n, u64 *out)
{
if (n == NULL) return 0;
switch (n->kind) {
case N_INTLIT:
case N_RUNELIT:
*out = n->uval; return 1;
case N_TRUE: *out = 1; return 1;
case N_FALSE:
case N_NIL: *out = 0; return 1;
case N_UN: {
u64 v;
if (!fold_int_literal(n->lhs, &v)) return 0;
switch (n->op) {
case TK_MINUS: *out = (u64)(-(i64)v); return 1;
case TK_TILDE: *out = ~v; return 1;
case TK_PLUS: *out = v; return 1;
default: return 0;
}
}
default: return 0;
}
}
/* fold_binop — apply one constant binary op. The shared arithmetic
* core of the two compile-time-int-eval paths: eval_enum_value (enum
* member exprs) and eval_def_const (top-level def rhs, #88). Both
* route here so wrap/shift/divide semantics are defined ONCE — rule
* 10 demands the cstage and wwstage stamp the bit-identical literal,
* and a single op table is the only way to keep them from drifting.
* Returns 0 on division by zero or an op outside the constant subset;
* the caller maps that to its own diagnostic. */
static int
fold_binop(Tkind op, u64 a, u64 b, u64 *out)
{
switch (op) {
case TK_PLUS: *out = a + b; return 1;
case TK_MINUS: *out = a - b; return 1;
case TK_STAR: *out = a * b; return 1;
case TK_SLASH: if (b == 0) return 0; *out = a / b; return 1;
case TK_PERCENT: if (b == 0) return 0; *out = a % b; return 1;
case TK_AMP: *out = a & b; return 1;
case TK_PIPE: *out = a | b; return 1;
case TK_CARET: *out = a ^ b; return 1;
case TK_LSHIFT: *out = a << b; return 1;
case TK_RSHIFT: *out = a >> b; return 1;
default: return 0;
}
}
/* eval_enum_value — fold an enum member-value expression to a u64
* constant. Sees prior siblings via the `prev` Tfield list (each
* carries the member's name and resolved value in .offset). Returns
* 1 on success; on failure emits the error and returns 0. The op set
* is the constant subset typical of Hare-style flag enums:
* literal, sibling-ident, + - * / % & | ^ << >>, unary - and ~.
* Literal leaves and unary-over-literal are delegated to
* fold_int_literal so the fold logic lives in one place. */
static int
eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out)
{
if (n == NULL) return 0;
if (fold_int_literal(n, out)) return 1;
switch (n->kind) {
case N_IDENT: {
for (Tfield *f = prev; f; f = f->next) {
if (f->name && n->str &&
strcmp(f->name, n->str) == 0) {
*out = f->offset;
return 1;
}
}
err(c, n->pos, "enum value: unknown identifier '%s'",
n->str ? n->str : "?");
return 0;
}
case N_BIN: {
u64 a, b;
if (!eval_enum_value(c, n->lhs, prev, &a) ||
!eval_enum_value(c, n->rhs, prev, &b))
return 0;
if (fold_binop(n->op, a, b, out))
return 1;
if ((n->op == TK_SLASH || n->op == TK_PERCENT) && b == 0)
err(c, n->pos, "enum value: division by zero");
else
err(c, n->pos, "enum value: unsupported binary op %s",
tokname(n->op));
return 0;
}
case N_UN: {
u64 v;
if (!eval_enum_value(c, n->lhs, prev, &v))
return 0;
switch (n->op) {
case TK_MINUS: *out = (u64)(-(i64)v); return 1;
case TK_TILDE: *out = ~v; return 1;
case TK_PLUS: *out = v; return 1;
default:
err(c, n->pos, "enum value: unsupported unary op %s",
tokname(n->op));
return 0;
}
}
default:
err(c, n->pos,
"enum value must be a constant integer expression");
return 0;
}
}
/* def_cast_fits — for a def-rhs `value: T` cast strip (#88), does the
* already-folded u64 `v` survive narrowing to integer target `t`?
* Identity / widening / same-width casts always fit. A genuine
* narrowing cast whose value falls outside the target's range must
* NOT be silently truncated (rule 7 / drew): the caller turns a
* miss into a loud error. Width comes from the type table (t->size,
* rule 13) — never a hardcoded layout literal. Pure-u64 arithmetic
* so the cstage and wwstage range check stay bit-identical (rule 10).
* The `8`s here are CHAR_BIT and the u64 byte-width, not type-layout
* sizes, so they are outside rule 13's scope. */
static int
def_cast_fits(Type *t, u64 v)
{
if (!type_isint(t)) return 1; /* non-int target: keep value as-is */
u64 w = t->size;
if (w >= 8) return 1; /* 64-bit target: no narrowing */
u64 bits = w * 8;
if (type_isunsigned(t))
return (v >> bits) == 0;
/* signed: truncate to `bits` then sign-extend; fits iff unchanged */
u64 mask = ((u64)1 << bits) - 1;
u64 sign = (u64)1 << (bits - 1);
u64 ext = ((v & mask) ^ sign) - sign;
return ext == v;
}
/* addrfn_ptr_matches — true iff ptr is a pointer whose referent
* (after one NAMED peel) is a fn type structurally equal to fnty. */
static int
addrfn_ptr_matches(Type *ptr, Type *fnty)
{
if (ptr == NULL || ptr->kind != TY_PTR) return 0;
Type *ref = ptr->sub;
ref = type_chase_named(ref);
if (ref == NULL || ref->kind != TY_FN) return 0;
return type_eq(ref, fnty);
}
/* assignable_addrfn — project #206. A bare `&fn` types structurally
* as `*fn(...)`, which is nominally distinct from a `*alias`
* fn-pointer slot; type_assignable stays fully nominal (preserving
* harec's nominal pointer rule, ref/harec/src/types.c:1039-1066) so
* any materialized `*fn` value laundered into a `*alias` is rejected.
* This admits the one shape harec accepts via its address-of hint
* (ref/harec/src/check.c:3594-3626 adopts the alias when the operand
* dealiases to the hint's referent): a DIRECT `&`-of-fn-ident whose
* signature structurally matches the destination's pointed-to fn
* alias, or — for a tagged `(*alias | void)` destination — the single
* ptr-to-fn variant it matches (>=2 same-signature variants is
* ambiguous → reject, never silently pick). Lives at the assignment
* boundary, not in cexpr, because ww's tinfo is nominal-lossy and
* cexpr is hint-free (the alias identity is unrecoverable post-typing);
* the caller-site rhs node is the only place the direct-&fn shape
* survives. "Direct" is strict: the gate fires only when the rhs IS
* the address-of node, never on `&fn` nested in a larger expr. */
static int
assignable_addrfn(Checker *c, Type *dst, Node *rhs)
{
if (dst == NULL || rhs == NULL) return 0;
if (rhs->kind != N_UN || rhs->op != TK_AMP) return 0;
Node *id = rhs->lhs;
if (id == NULL || id->kind != N_IDENT || id->str == NULL) return 0;
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, id->str);
if (s == NULL || s->kind != SK_FN) return 0;
Type *fnty = s->type;
if (fnty == NULL || fnty->kind != TY_FN) return 0;
Type *du = type_chase_named(dst);
if (du == NULL) return 0;
if (du->kind == TY_PTR) return addrfn_ptr_matches(du, fnty);
if (du->kind == TY_TAGGED) {
int n = 0;
for (Tparam *p = du->params; p; p = p->next)
if (addrfn_ptr_matches(p->type, fnty)) n++;
return n == 1;
}
return 0;
}
/* #9: an INFER `[_]T` leaves the N_TARRAY length-child NULL; an explicit
* `[N]T` (incl `[0]`) carries an N_INTLIT. resolve_type collapses BOTH to
* alen==0, so the Type can't tell them apart — key off the decl's type-AST
* node (d->lhs) instead. Mirrors wwstage's `arrtn.rhs != nil` test. */
static int
is_infer_arr(Node *tn)
{
return tn != NULL && tn->kind == N_TARRAY && tn->rhs == NULL;
}
/* arrlit_init_fits — #130: accept-if-fits for `let/def A: [N]T = [..]`
* where the whole-array type_assignable failed (bare-int elements
* synthesize [N]i32 via type_default, losing the literal flavor that
* the scalar coercion rule honours). Per element:
* - foldable int literal → range-check against T via def_cast_fits.
* In-range accepts; out-of-range REJECTS loud (rule-7 / Drew:
* Hare range-checks at literal-value level, ref/harec types.c:923
* promote_flexible — never a silent truncate).
* - non-foldable element → type_assignable(T, elem->type), reusing
* the same coercion rule the scalar path uses (untyped-int→u8 ok,
* str→u8 not).
* Returns 1 iff every element fits; the caller only consults this
* after type_assignable already said no, so a 0 means a genuine
* reject. Scoped to the ARRAY path — scalar overflow stays a separate
* language-wide gap (#148). */
static int
arrlit_init_fits(Checker *c, Type *dt, Node *rhs)
{
if (rhs == NULL || rhs->kind != N_ARRLIT) return 0;
Type *u = type_chase_named(dt);
/* #25: a SLICE target is admitted via the same per-element coercion
* as the array path — the #258 borrow demands an exact element
* type_eq, which an arrlit's self-stamped [N]<default> can't meet for
* untyped_str / bare-int-width elements. Peel to the slice element T
* and run the array-element coercion against it. */
if (u == NULL || (u->kind != TY_ARRAY && u->kind != TY_SLICE))
return 0;
Type *et = u->sub;
Type *eu = type_chase_named(et);
u64 count = 0;
for (Node *e = rhs->list; e; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0)
continue;
count++;
}
/* #71: more elements than the declared [N] passed every per-element
* check below and then smashed the frame at cgen (each element is
* stored at its natural offset — the overflow clobbered neighbours
* and even the saved BP). Reject loud before the element walk.
* #9: the `alen > 0` exemption is GONE. An infer `[_]` is resized to
* its real count at the decl sites (is_infer_arr-gated) BEFORE
* reaching here, so an array arriving with alen==0 is necessarily an
* EXPLICIT `[0]` — and `[0] = [1,2]` (count 2 > 0) must be loud, not
* silently resized. `[0] = []` (count 0) stays accepted. SIZE_UNDEFINED
* (opaque/unsized) still exempt. Under-long (count < N, no `...`) stays
* accepted as before; Hare rejects it — task #10. */
if (u->kind == TY_ARRAY && u->alen != SIZE_UNDEFINED
&& count > u->alen) {
err(c, rhs->pos, "array literal has %llu elements "
"but declared array holds %llu",
(unsigned long long)count, (unsigned long long)u->alen);
return 0;
}
for (Node *e = rhs->list; e; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0)
continue;
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
u64 v;
if (ev && type_isint(eu) && fold_int_literal(ev, &v)) {
if (!def_cast_fits(eu, v)) {
err(c, e->pos, "array element out of range "
"for %s", type_name(c->a, et));
return 0;
}
continue;
}
if (!type_assignable(et, e->type) && !assignable_addrfn(c, et, e))
return 0;
}
/* #25/#31: re-stamp the literal as [count]T so desugar_arrayslice keys
* on an exact-element-eq array and the cgen N_SLICE-over-N_ARRLIT arm
* (#31) materialises the borrow backing at the DECLARED element width.
* The array path keeps the declared array type, so this is slice-only. */
if (u->kind == TY_SLICE)
rhs->type = type_array(c->a, et, count);
return 1;
}
/* eval_def_const — fold a top-level def's rhs to a u64 constant,
* resolving sibling and imported def references, casts, and
* arithmetic (#88). Reuses the shared fold_int_literal leaf/unary
* fold and the fold_binop arith core; the ONLY thing it does that
* eval_enum_value doesn't is resolve an identifier through the
* checker's flat scope (scope_lookup_prefer for a bare sibling ref,
* scope_lookup_in_module for a `mod.NAME` qualified ref) to the
* referent def's own rhs, then recurse.
*
* Why this stays a distinct evaluator from eval_enum_value rather
* than a full merge (rule 8 WHY): enum-member eval carries implicit
* prev+1 auto-increment and forward-only sibling lookup over a Tfield
* chain; def eval has neither — it resolves through the scope/decl
* graph, which can reference forward and across modules. The two
* lookup models don't reconcile cleanly, so they share the arith
* core (fold_binop) + leaf fold (fold_int_literal) and keep separate
* top-level shapes.
*
* `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A,
* incl. cross-module) hits the cap and fails loud rather than hanging
* (rule 7), mirroring the cgen.c nsteps>=16 abort precedent. */
static int
eval_def_const(Checker *c, Node *n, u64 *out, int depth)
{
if (n == NULL) return 0;
if (depth >= 16) {
err(c, n->pos,
"def value: reference chain too deep (cycle?)");
return 0;
}
if (fold_int_literal(n, out)) return 1;
switch (n->kind) {
case N_BIN: {
u64 a, b;
if (!eval_def_const(c, n->lhs, &a, depth + 1) ||
!eval_def_const(c, n->rhs, &b, depth + 1))
return 0;
if (fold_binop(n->op, a, b, out))
return 1;
if ((n->op == TK_SLASH || n->op == TK_PERCENT) && b == 0)
err(c, n->pos, "def value: division by zero");
else
err(c, n->pos, "def value: unsupported binary op %s",
tokname(n->op));
return 0;
}
case N_UN: {
/* fold_int_literal already covers unary-over-leaf; this
* arm catches unary over a resolved ref, e.g. `-A`. */
u64 v;
if (!eval_def_const(c, n->lhs, &v, depth + 1)) return 0;
switch (n->op) {
case TK_MINUS: *out = (u64)(-(i64)v); return 1;
case TK_TILDE: *out = ~v; return 1;
case TK_PLUS: *out = v; return 1;
default:
err(c, n->pos,
"def value: unsupported unary op %s",
tokname(n->op));
return 0;
}
}
case N_CAST: {
/* lhs = value, n->type = resolved target (set by cexpr's
* N_CAST arm in pass 2). Strip the cast, keeping the value;
* a narrowing cast that loses the value fails loud. */
u64 v;
if (!eval_def_const(c, n->lhs, &v, depth + 1)) return 0;
if (!def_cast_fits(n->type, v)) {
err(c, n->pos,
"def value: narrowing cast loses value");
return 0;
}
*out = v;
return 1;
}
case N_IDENT: {
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str);
if (s == NULL || s->kind != SK_DEF ||
s->decl == NULL || s->decl->rhs == NULL)
return 0;
return eval_def_const(c, s->decl->rhs, out, depth + 1);
}
case N_DOT: {
if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0;
/* M1 #22: map the qualifier alias to its dotted import path. */
const char *mk = use_path(c->file, c->cur_mod, n->lhs->str);
if (mk == NULL) mk = n->lhs->str;
Sym *s = scope_lookup_in_module(c->cur, mk, n->str);
if (s == NULL || s->kind != SK_DEF ||
s->decl == NULL || s->decl->rhs == NULL)
return 0;
return eval_def_const(c, s->decl->rhs, out, depth + 1);
}
default:
return 0;
}
}
/* stamp_intlit — rewrite a const-folded def rhs in place to the
* literal it evaluates to, preserving the node's cexpr-resolved type
* so the downstream DATA-row emit width and the invariant checks see
* a properly-typed literal leaf. Lets cgen's existing literal-only
* fold lay down the row with no codegen change (#88). */
static void
stamp_intlit(Checker *c, Node *n, u64 v)
{
n->kind = N_INTLIT;
n->uval = v;
n->op = 0;
n->str = aprintf(c->a, "%llu", (unsigned long long)v);
n->strlen = strlen(n->str);
n->lhs = n->rhs = n->cond = n->body = n->els = n->list = NULL;
n->tsuffix = NULL;
/* n->type left intact (the type cexpr inferred for the rhs). */
}
/*
* require_sized — #108(b): opaque is abstract + UNSIZED
* (size == SIZE_UNDEFINED) and is legal ONLY behind indirection:
* `*opaque` (8B) and `[]opaque` (24B header) size themselves
* independently of the element, so they pass this guard. A use that
* needs a concrete byte size — a bare local/param/return value, a
* struct field, an array element — would otherwise fabricate a
* (u64)-1-byte slot: a silent miscompile (rule 7). Reject loud here.
* Mirrors harec's `size == SIZE_UNDEFINED` binding/field/return guards
* (ref/harec/src/check.c:1524 "Cannot create binding for type of
* undefined size", :3931 return-by-value). Returns 1 when the type is
* sized (caller proceeds), 0 when it emitted the error.
*/
static int
require_sized(Checker *c, Type *t, Pos pos, const char *where)
{
if (t == NULL || t->size != SIZE_UNDEFINED)
return 1;
err(c, pos, "unsized type '%s' cannot be %s; use '*%s' or '[]%s'",
type_name(c->a, t), where, type_name(c->a, t),
type_name(c->a, t));
return 0;
}
/* circular_named — #62/#69 cycle guard: a VALUE-position reference to
* a typedecl whose body is still being resolved is a true type cycle
* (the type would have infinite size). Loud, mirroring harec's
* in_progress check (ref/harec/src/check.c:4767 "Circular dependency
* for '%s'"). Pointer/slice/chan/fn positions never read the target's
* size and legitimately receive the in-progress placeholder, so the
* check sits at the size-consuming sites only — `type node = struct {
* next: *node }` stays legal. */
static int
circular_named(Checker *c, Type *t, Pos pos)
{
if (t == NULL || t->kind != TY_NAMED || !t->resolving) return 0;
err(c, pos, "circular type dependency: '%s'",
t->name ? t->name : "?");
return 1;
}
static Type *
resolve_type(Checker *c, Node *n)
{
if (n == NULL) return ty_void;
switch (n->kind) {
case N_TBANG: {
/* `!T` — mark the resolved type as an error type. Wrap
* primitives in a fresh NAMED-less copy so we don't taint
* the shared ty_void / ty_str / ty_i32 globals. NAMED
* types are already unique per alias decl, so we can flip
* the bit in place. */
Type *t = resolve_type(c, n->lhs);
if (t == NULL || t == ty_err) return t;
if (t->kind == TY_NAMED) {
t->iserror = 1;
return t;
}
Type *t2 = newtype(c->a, t->kind);
*t2 = *t;
t2->iserror = 1;
return t2;
}
case N_TNAME:
return resolve_typename(c, n);
case N_TPTR:
return type_ptr(c->a, resolve_type(c, n->lhs));
case N_TSLICE:
return type_slice(c->a, resolve_type(c, n->lhs));
case N_TARRAY: {
u64 len = 0, v;
if (n->rhs == NULL) {
/* `[_]T` — length inferred at the use site (currently
* only `let x: [_]T = arrlit;`). Leave alen=0 as a
* sentinel; clet patches it from the initialiser. */
} else if (n->rhs->kind == N_INTLIT) {
len = n->rhs->uval;
} else if (eval_def_const(c, n->rhs, &v, 0)) {
/* #141: a def-dimensioned `[MAX]u8`; fold the
* const-expr dimension (the same machinery #133's
* let-init fold uses). The err below stays for a
* genuinely non-const rhs. */
len = v;
} else {
err(c, n->pos, "array length must be an integer literal");
}
Type *elem = resolve_type(c, n->lhs);
if (circular_named(c, elem, n->pos)) /* #62/#69 */
return ty_err;
require_sized(c, elem, n->pos, "an array element"); /* #108(b) */
return type_array(c->a, elem, len);
}
case N_TCHAN:
return type_chan(c->a, resolve_type(c, n->lhs));
case N_TTUPLE: {
Type *t = newtype(c->a, TY_TUPLE);
Tparam *head = NULL, *tail = NULL;
u64 sz = 0, al = 1;
for (Node *e = n->list; e; e = e->next) {
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->type = resolve_type(c, e);
if (circular_named(c, tp->type, e->pos)) /* #62/#69 */
continue;
/* #108(b): an unsized member would poison sz with the
* (u64)-1 sentinel. harec ref/harec/src/type_store.c:1147. */
if (!require_sized(c, tp->type, e->pos, "a tuple member"))
continue;
if (tp->type && tp->type->align > al) al = tp->type->align;
/* Slot layout is the tuple SSoT (tuple arc C-t0,
* user-ratified): every element occupies the stride
* cgen's cursor transport actually writes — slot =
* roundup8(size(elem)), 8B a FLOOR not a ceiling
* (#22, user-ratified 2026-06-04): str/slice carry
* their 24B header, a tagged element its full
* tag+payload box ((str,str)=48B predates this;
* tagged was the one truncated >8B kind — the #237
* fieldslotsize precedent), narrow scalars pad UP
* to one 8B eightbyte. ww-internal ABI only (tuples
* never cross extern); size((u32,u32))=16 is
* observable via size() and diverges from Hare
* (harec type_store.c:533-580 anonymous-struct rule)
* AND from ww's own structs (which pack narrow fields
* post-fldloadop) — that internal inconsistency is
* what task #60 eventually fixes; re-open before any
* serialization/FFI/density use. Pre-C-t0 this summed
* packed element sizes while cgen strode 8B slots —
* the checker-says-8/cgen-does-16 split behind the
* packed-tuple miscompile family (#32/#33/#48). */
if (tp->type) {
Type *eu = type_chase_named(tp->type);
/* #24: a composite element (array/struct/nested
* tuple >8B) cannot ride the 8B cursor slot — the
* #60 layout drops it on construction and segvs on
* t.N[i] read. Reject until #60/DISP-A inlines it.
* Hare allows it (harec type_store.c anon-struct). */
if (eu && (eu->kind == TY_ARRAY
|| eu->kind == TY_STRUCT
|| eu->kind == TY_TUPLE))
err(c, n->pos, "tuple element must be a "
"scalar, str, slice, or tagged-union "
"(composite element deferred to task "
"#60)");
if (eu && (eu->kind == TY_STR
|| eu->kind == TY_SLICE
|| eu->kind == TY_TAGGED))
sz += (eu->size + 7) & ~(u64)7;
else if (eu == NULL || eu->kind != TY_VOID)
sz += 8; /* sizelint-ok: the slot IS the 8B eightbyte */
}
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
}
t->params = head;
t->size = sz;
t->align = al;
return t;
}
case N_TTAGGED: {
/* (T1 | T2 | ...) — tag (8B) followed by the largest variant.
* Type-set normalization (Hare-style):
* - Flatten nested anonymous (A | B) | C → (A | B | C). Named
* aliases over tagged unions stay nominal — not flattened.
* - Drop `never`: bottom contributes no values.
* - Dedup variants. Equality follows cg_variant_match: NAMED
* types compare by pointer-identity, others structurally.
* - If exactly one variant remains, the tagged union collapses
* to that variant. (i32 | never) → i32.
* - If zero remain (all variants were `never`), the type is
* `never` itself. */
Type *t = newtype(c->a, TY_TAGGED);
Tparam *head = NULL, *tail = NULL;
u64 maxsz = 0, al = 8;
int nv = 0;
for (Node *e = n->list; e; e = e->next) {
Type *vt = resolve_type(c, e);
if (vt == ty_never) continue;
if (circular_named(c, vt, e->pos)) /* #62/#69 */
continue;
/* #108(b): an unsized variant has no slot in the union
* payload. harec ref/harec/src/type_store.c:449. */
if (!require_sized(c, vt, e->pos, "a tagged union member"))
continue;
int spread = (e->op == TK_ELLIPSIS);
/* `...inner` spread: flatten the variants of the
* (possibly NAMED) inner tagged union into the
* enclosing union — matches Hare's parse-time
* unwrap flag on each tagged_type entry. */
Type *vu = spread ? type_chase_named(vt) : vt;
if (vu && vu->kind == TY_TAGGED &&
(spread || vt->kind == TY_TAGGED)) {
for (Tparam *src = vu->params; src; src = src->next) {
Type *st = src->type;
if (st == ty_never) continue;
if (variant_present(head, st)) continue;
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->type = st;
if (st && st->size > maxsz) maxsz = st->size;
if (st && st->align > al) al = st->align;
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
nv++;
}
continue;
}
if (variant_present(head, vt)) continue;
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->type = vt;
if (vt && vt->size > maxsz) maxsz = vt->size;
if (vt && vt->align > al) al = vt->align;
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
nv++;
}
if (nv == 0) return ty_never;
if (nv == 1 && head) return head->type;
t->params = head;
/* Nullable pointer folding: `(*T | void)` collapses to a
* single 8-byte pointer slot; null bit pattern is the void
* variant. Mirrors Hare's `(*T | null)`. Detected on exact
* two-variant shape with one TY_PTR and one literal TY_VOID
* (not NAMED, not `!`-flagged): aligns DOWN to wwstage's
* isnullabletype which is AST-keyed and only matches a bare
* `void` name. Task #25 — `(*T | nomem)` where `nomem = !void`
* must take the general tagged-return ABI (AX=tag, DX=word0)
* so cstage and wwstage emit byte-identical asm. */
if (nv == 2) {
Tparam *a = head;
Tparam *b = head->next;
int aptr = a->type && a->type->kind == TY_PTR;
int bptr = b->type && b->type->kind == TY_PTR;
int avoid = a->type && a->type->kind == TY_VOID
&& !a->type->iserror;
int bvoid = b->type && b->type->kind == TY_VOID
&& !b->type->iserror;
if ((aptr && bvoid) || (avoid && bptr)) {
t->nullable = 1;
t->size = 8;
t->align = 8;
return t;
}
}
/* Round value payload up to an 8-byte multiple so the slot
* layout (tag + N value words) stays word-aligned. The reg-
* passing ABI counts size/8 words; 12-byte unions like
* (i32 | void) would otherwise lose a value register. */
u64 vsz = (maxsz + 7) & ~(u64)7;
t->size = 8 + vsz;
t->align = al;
return t;
}
case N_TFN: {
Type *t = newtype(c->a, TY_FN);
t->ret = resolve_type(c, n->lhs);
t->size = 8;
t->align = 8;
Tparam *head = NULL, *tail = NULL;
for (Node *p = n->list; p; p = p->next) {
if (strcmp(p->str ? p->str : "", "...") == 0) {
t->variadic = 1;
continue;
}
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->name = p->str;
Type *pt = resolve_type(c, p->lhs);
/* Hare-style `T...` (marked on the param node via
* Node.op == TK_ELLIPSIS): the param's effective type
* inside the callee is []T, and call sites either
* gather N args of type T or forward an `xs...` slice. */
if (p->op == TK_ELLIPSIS) {
tp->variadic = 1;
tp->type = type_slice(c->a, pt);
} else {
tp->type = pt;
}
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
}
t->params = head;
return t;
}
case N_TSTRUCT: {
Type *t = newtype(c->a, TY_STRUCT);
Tfield *head = NULL, *tail = NULL;
u64 off = 0, maxalign = 1;
for (Node *f = n->list; f; f = f->next) {
Type *ft = resolve_type(c, f->lhs);
if (circular_named(c, ft, f->pos)) /* #62/#69 */
continue;
/* #108(b): an unsized field would overflow the offset
* accumulator (align/size == (u64)-1); reject + skip it. */
if (!require_sized(c, ft, f->pos, "a struct field"))
continue;
if (ft->align > maxalign) maxalign = ft->align;
off = (off + ft->align - 1) & ~(ft->align - 1);
if (f->str != NULL) {
/* regular named field */
for (Tfield *e = head; e; e = e->next)
if (e->name && strcmp(e->name, f->str) == 0) {
err(c, f->pos, "duplicate field '%s'",
f->str);
break;
}
Tfield *tf = amalloc(c->a, sizeof *tf);
tf->name = f->str;
tf->type = ft;
tf->offset = off;
off += ft->size;
if (head == NULL) head = tf;
else tail->next = tf;
tail = tf;
continue;
}
/* embed (anonymous struct or bare-name): the inner type
* must be a struct; its fields are promoted to the outer
* scope with offsets shifted by the embed base. */
Type *inner = type_chase_named(ft);
if (inner == NULL || inner->kind != TY_STRUCT) {
err(c, f->pos, "embedded type must be a struct");
off += ft ? ft->size : 0;
continue;
}
u64 base = off;
for (Tfield *src = inner->fields; src; src = src->next) {
for (Tfield *e = head; e; e = e->next)
if (e->name && src->name &&
strcmp(e->name, src->name) == 0) {
err(c, f->pos,
"embedded field '%s' "
"collides with existing field",
src->name);
break;
}
Tfield *tf = amalloc(c->a, sizeof *tf);
tf->name = src->name;
tf->type = src->type;
tf->offset = base + src->offset;
if (head == NULL) head = tf;
else tail->next = tf;
tail = tf;
}
off = base + inner->size;
}
t->fields = head;
t->align = maxalign;
t->size = (off + maxalign - 1) & ~(maxalign - 1);
return t;
}
case N_TENUM: {
Type *t = newtype(c->a, TY_ENUM);
Type *storage = ty_i32; /* default storage */
if (n->lhs) {
Type *s = resolve_type(c, n->lhs);
if (s == ty_err || !type_isint(s))
err(c, n->lhs->pos,
"enum storage type must be integer");
else
storage = s;
}
t->sub = storage;
t->size = storage->size;
t->align = storage->align;
Tfield *head = NULL, *tail = NULL;
u64 prev = (u64)-1; /* so first omitted → 0 */
for (Node *m = n->list; m; m = m->next) {
u64 val;
if (m->lhs == NULL) {
val = prev + 1;
} else if (!eval_enum_value(c, m->lhs, head, &val)) {
val = prev + 1;
}
prev = val;
for (Tfield *e = head; e; e = e->next) {
if (e->name && m->str &&
strcmp(e->name, m->str) == 0) {
err(c, m->pos,
"duplicate enum member '%s'",
m->str);
break;
}
}
Tfield *tf = amalloc(c->a, sizeof *tf);
tf->name = m->str;
tf->type = NULL;
tf->offset = val;
if (head == NULL) head = tf;
else tail->next = tf;
tail = tf;
}
t->fields = head;
return t;
}
default:
return err(c, n->pos, "expected type expression");
}
}
/* ---- expressions -------------------------------------------------- */
static Type *
unify_arith(Checker *c, Pos p, Type *a, Type *b)
{
if (a == ty_err || b == ty_err) return ty_err;
/* untyped + untyped → untyped (prefer float over int) */
if (type_isuntyped(a) && type_isuntyped(b)) {
if (a->kind == TY_UNTYPED_FLOAT || b->kind == TY_UNTYPED_FLOAT)
return ty_untyped_float;
return ty_untyped_int;
}
/* untyped + typed → typed (if assignable) */
if (type_isuntyped(a) && type_assignable(b, a)) return b;
if (type_isuntyped(b) && type_assignable(a, b)) return a;
if (type_eq(a, b)) return a;
/* One-sided alias vs its (transitive) base promotes to the ALIAS
* side; alias vs DIFFERENT alias stays rejected even when the
* bases agree. Mirrors harec type_promote (ref/harec/src/check.c:
* 1083-1105: ALIAS+ALIAS → NULL, then dealias-equal → the alias
* operand). wwstage accepts these shapes and runs them on the
* chased width (F0 m1_binop/m1_binop2, ww runtime-correct).
*
* ERROR axis guard: `type invalid = !i32` must NOT promote against
* a plain i32 — type_eq ignores iserror (primitives compare by
* kind), but harec interns flags into DISTINCT types, so flagged-
* vs-unflagged never reaches type_promote's dealias-equal arm. The
* #246 loud (strconv.invalid != i32, pinned by 949_errtype_compare
* on BOTH stages) rides this reject. */
{
Type *da = type_chase_named(a);
Type *db = type_chase_named(b);
if (!(a->kind == TY_NAMED && b->kind == TY_NAMED) &&
da && db && da->iserror == db->iserror &&
type_eq(da, db))
return (a->kind == TY_NAMED) ? a : b;
}
return err(c, p, "operands have differing types %s and %s",
type_name(c->a, a), type_name(c->a, b));
}
/* #104 fold-2: an un-suffixed float literal stays ty_untyped_float through
* the checker, so fold-1's cgen narrow (gated on the node's f32-ness) never
* fires for `let x: f32 = 1.0` — the literal materialises as a double whose
* low 4 bytes (0.0f for clean values) are what the f32 consumer reads. Stamp
* such a literal f32 when an f32 target type is in context, mirroring harec's
* lower_implicit_cast sites (ref/harec/src/check.c:148). A float literal's
* bit pattern is target-dependent (unlike a width-agnostic int immediate),
* so the value-producing node must carry the f32 type.
*
* SCOPED to untyped_float -> f32 ONLY: untyped_float -> f64 already works via
* cgen's double default, so stamping it would broaden the surface for no gain.
*
* Symmetric subset (rule 10), DIRECT N_FLOATLIT at let-init / return only —
* because the wwstage cgen (selfhost/cmd/wcc/cgenutil.ww exprfloatkind) does
* NOT read node.type_ for a float literal: it hardcodes N_FLOATLIT -> f64 and
* cgbin picks f32 off the OPERANDS' float-kind, not the node stamp. So the
* wwstage materialiser (fold-1 isf32type, the one path that does read the
* stamp) narrows a let-init / return literal correctly, but a stamped literal
* inside an arith-binop or behind a unary minus is NOT narrowed by cgbin /
* the negate — cstage would emit ADDSS/SUBSS while wwstage emits ADDSD/SUBSD,
* breaking the cs==ww byte-id gate. Likewise the wwstage checker has no
* N_ASSIGN check, no param-typed call-arg loop, and a head-only struct
* literal. binop / unary-minus / assign / call-arg / struct-field therefore
* wait on the wwstage cgen + checker gaining those (#120). */
static void
coerce_floatlit(Node *n, Type *target)
{
if (n == NULL || target == NULL)
return;
/* Chase the full alias chain (wwstage resolvealias does the same), so
* a doubly-aliased f32 target stamps in both stages or neither. */
Type *u = type_chase_named(target);
if (u == NULL || u->kind != TY_F32)
return;
if (n->kind == N_FLOATLIT && n->type == ty_untyped_float)
n->type = ty_f32;
}
/* desugar_arrayslice — #258. The single shared injection point for the
* implicit [N]T → []T borrow. type_assignable already admits an array
* with a defined length into a matching []T slot (see type.c:#258); here
* we lower it to the explicit full slice `arr[0:len(arr)]` (an N_SLICE
* over the array base), reusing the existing slice cgen — #252/#257/#135
* made array bases (incl struct-field arrays) correct. No new array→slice
* store cgen, and the borrow header is byte-identical across stages.
*
* Mutates `expr` IN PLACE: the original array expr moves into a fresh base
* node (keeping its stamped array type for cgen's esz/alen), and `expr`
* becomes the N_SLICE — preserving the sibling link so a desugared
* call-arg keeps its place in the argument list. Self-guards on shape, so
* the four acceptance sites can call it unconditionally; it no-ops unless
* the dst is a slice and the src an array with an exactly-matching
* element. */
/* reject_arrlit_borrow — #31/#33: the array-literal → slice borrow is
* supported only at a `let` init, where clet spills the literal to a
* per-borrow backing slot (#31). In call-arg / return / assign position
* there is no addressable backing — the borrow's .ptr would dangle (the
* original #31 silent segfault). Reject loudly here so the gap is a
* compile error, not a miscompile. rule-10: wwstage rejects the same
* source (its untyped-arrlit element fails the borrow's typeeq); aligning
* cstage DOWN keeps both stages loud-identical. Full non-let support is
* #33. Returns 1 (and emits the error) when it refuses the borrow. */
static int
reject_arrlit_borrow(Checker *c, Type *dst, Node *expr)
{
if (expr == NULL || expr->kind != N_ARRLIT) return 0;
Type *du = type_chase_named(dst);
if (du == NULL || du->kind != TY_SLICE) return 0;
err(c, expr->pos, "array literal cannot borrow as a slice here; "
"bind it to a `let` first");
return 1;
}
static void
desugar_arrayslice(Checker *c, Type *dst, Node *expr)
{
if (dst == NULL || expr == NULL || expr->type == NULL)
return;
Type *du = type_chase_named(dst);
Type *su = type_chase_named(expr->type);
if (du == NULL || su == NULL ||
du->kind != TY_SLICE || su->kind != TY_ARRAY)
return;
if (su->alen == SIZE_UNDEFINED || !type_eq(du->sub, su->sub))
return;
Node *base = newnode(c->a, expr->kind, expr->pos);
Node *next = expr->next;
*base = *expr;
base->next = NULL;
memset(expr, 0, sizeof *expr);
expr->kind = N_SLICE;
expr->pos = base->pos;
expr->next = next;
expr->lhs = base; /* sliced base; lo/hi NULL → 0 : len(arr) */
expr->type = type_slice(c->a, su->sub);
}
static Type *
cbinop(Checker *c, Node *n)
{
Type *l = cexpr(c, n->lhs);
Type *r = cexpr(c, n->rhs);
switch (n->op) {
case TK_PLUS: case TK_MINUS: case TK_STAR: case TK_SLASH:
case TK_PERCENT:
/* pointer arithmetic: ptr ± int → ptr; ptr - ptr → int */
if ((n->op == TK_PLUS || n->op == TK_MINUS)
&& l && l->kind == TY_PTR && type_isint(r))
return l;
if (n->op == TK_PLUS && type_isint(l) && r && r->kind == TY_PTR)
return r;
if (n->op == TK_MINUS && l && r && l->kind == TY_PTR
&& r->kind == TY_PTR)
return ty_i64;
if (!type_isnum(l) || !type_isnum(r))
return err(c, n->pos, "arithmetic on non-numeric type");
return unify_arith(c, n->pos, l, r);
case TK_AMP: case TK_PIPE: case TK_CARET: case TK_LSHIFT:
case TK_RSHIFT:
if (!type_isint(l) || !type_isint(r))
return err(c, n->pos, "bitwise on non-integer type");
return unify_arith(c, n->pos, l, r);
case TK_EQ: case TK_NEQ:
(void)unify_arith(c, n->pos, l, r);
return ty_bool;
case TK_LT: case TK_LE: case TK_GT: case TK_GE:
if (!type_isnum(l) || !type_isnum(r))
err(c, n->pos, "ordered comparison on non-numeric");
(void)unify_arith(c, n->pos, l, r);
return ty_bool;
case TK_AND: case TK_OR: {
/* Bool-ness through the alias chain — harec dealiases at the
* logical-binop consumer (ref/harec/src/check.c:3229); ww
* accepts + runs the alias-bool operand (F0 m2_andor). */
Type *lu = type_chase_named(l);
Type *ru = type_chase_named(r);
if (!(lu == ty_bool || lu == ty_untyped_bool || l == ty_err))
err(c, n->pos, "left of %s is not bool", tokname(n->op));
if (!(ru == ty_bool || ru == ty_untyped_bool || r == ty_err))
err(c, n->pos, "right of %s is not bool", tokname(n->op));
return ty_bool;
}
default:
return err(c, n->pos, "unsupported binary op %s", tokname(n->op));
}
}
static Type *
cunop(Checker *c, Node *n)
{
Type *t = cexpr(c, n->lhs);
switch (n->op) {
case TK_MINUS: case TK_PLUS:
if (!type_isnum(t))
return err(c, n->pos, "%s on non-numeric", tokname(n->op));
return t;
case TK_NOT: {
/* harec dealiases at the `!` consumer (ref/harec/src/check.c:
* 3572); ww accepts + runs the alias-bool operand (F0 m2_bang). */
Type *u = type_chase_named(t);
if (!(u == ty_bool || u == ty_untyped_bool || t == ty_err))
err(c, n->pos, "! on non-bool");
return ty_bool;
}
case TK_TILDE:
if (!type_isint(t))
return err(c, n->pos, "~ on non-integer");
return t;
case TK_STAR: { /* deref */
if (t == ty_err) return ty_err;
/* harec type_dereference dealiases the operand (ref/harec/src/
* types.c:19-22); wwstage unoptype TK_STAR resolvealias-chases. */
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_PTR)
return err(c, n->pos, "cannot deref non-pointer %s",
type_name(c->a, t));
return u->sub;
}
case TK_AMP: /* address-of */
/* Slice/str pseudo-fields .len/.cap surface as i32 but live
* in 8B-aligned slots in the header (ptr@0, len@8, cap@16).
* Address-of must be typed *i64 so deref-write hits the full
* slot; otherwise *&s.len = N stores 4B (MOVL) and the upper
* 4B leak from whatever the prior MOVQ store of s.len left
* behind. */
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs &&
n->lhs->str &&
(strcmp(n->lhs->str, "len") == 0 ||
strcmp(n->lhs->str, "cap") == 0)) {
Type *bt = n->lhs->lhs->type;
Type *bu = type_chase_named(bt);
if (bu && bu->kind == TY_PTR) bu = bu->sub;
bu = type_chase_named(bu);
if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR))
return type_ptr(c->a, ty_i64);
}
return type_ptr(c->a, t);
default:
return err(c, n->pos, "unsupported unary %s", tokname(n->op));
}
}
static Type *
cexpr(Checker *c, Node *n)
{
if (n == NULL) return ty_err;
switch (n->kind) {
case N_INTLIT:
if (n->tsuffix) {
Type *t = lookup_builtin(n->tsuffix);
n->type = t ? t : ty_untyped_int;
} else {
n->type = ty_untyped_int;
}
return n->type;
case N_FLOATLIT:
if (n->tsuffix) {
Type *t = lookup_builtin(n->tsuffix);
n->type = t ? t : ty_untyped_float;
} else {
n->type = ty_untyped_float;
}
return n->type;
case N_STRLIT: n->type = ty_untyped_str; return n->type;
case N_RUNELIT: n->type = ty_untyped_rune; return n->type;
case N_TRUE:
case N_FALSE: n->type = ty_untyped_bool; return n->type;
case N_NIL: n->type = ty_untyped_nil; return n->type;
case N_VOIDLIT: n->type = ty_void; return n->type;
case N_IDENT: {
if (n->str && n->str[0] == '\0')
return n->type = err(c, n->pos,
"`_` is only valid as a binding or discard lvalue");
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str);
if (s == NULL)
return n->type = err(c, n->pos, "undefined: %s", n->str);
/* SK_USE has no concrete value type; the only legal use is
* as the lhs of a DOT (module-qualified ref). Surface ty_err
* here; the DOT case below resolves the qualified symbol. */
if (s->kind == SK_USE)
return n->type = ty_err;
n->type = s->type;
return s->type;
}
case N_PARAM:
return n->type = ty_err; /* shouldn't appear in expr ctx */
case N_BIN: n->type = cbinop(c, n); return n->type;
case N_UN: n->type = cunop(c, n); return n->type;
case N_CAST: {
(void)cexpr(c, n->lhs);
n->type = resolve_type(c, n->rhs);
return n->type;
}
case N_DOT: {
/* module-qualified: lhs is an N_IDENT bound as SK_USE.
* Resolve to the symbol with the same leaf name. With
* driver-side concatenation, all symbols live in flat
* scope, so we lookup `n->str` directly. Same-module-
* first via _prefer keeps a bare-leaf enum `Color.M`
* inside module M from collapsing onto another module's
* Color sitting at the head of the flat scope chain —
* symmetric with wwstage's enumlookup graduation. */
if (n->lhs && n->lhs->kind == N_IDENT) {
Sym *ms = scope_lookup_prefer(c->cur, c->cur_mod,
n->lhs->str);
if (ms && (ms->kind == SK_USE || ms->use_alias)) {
/* Module-qualified ref. `use_alias` covers
* the self-import case where the module's
* type name shadowed the SK_USE; the leaf
* still resolves through the flat scope.
* Filter on the importing module name so
* same-leaf-name types from different
* imports (`bufio.stream`/`io.stream`)
* disambiguate to the right one. */
/* M1 #22: symbols are keyed on the dotted
* import path; map the alias the user wrote to
* that path before looking up the leaf. */
const char *mk = use_path(c->file, c->cur_mod,
n->lhs->str);
if (mk == NULL) mk = n->lhs->str;
Sym *fs = scope_lookup_in_module(c->cur,
mk, n->str);
if (fs)
return n->type = fs->type;
if (ms->kind == SK_USE) {
/* Pure SK_USE with missing leaf:
* external declaration. Codegen
* emits CALL/MOVQ by the leaf name
* and the linker resolves it. */
return n->type = ty_err;
}
/* SK_TYPE with use_alias=1 and no leaf
* found: fall through so the enum / type-
* member paths below get a shot. */
}
/* enum member access: TypeName.MEMBER → fold to
* the member's integer literal value. Type is the
* (named) enum type itself, so bitwise ops between
* members yield the same enum type via type_eq. */
if (ms && ms->kind == SK_TYPE && ms->type) {
Type *u = type_chase_named(ms->type);
if (u && u->kind == TY_ENUM) {
for (Tfield *f = u->fields; f; f = f->next) {
if (f->name && n->str &&
strcmp(f->name, n->str) == 0) {
n->kind = N_INTLIT;
n->uval = f->offset;
n->str = aprintf(c->a, "%llu",
(unsigned long long)f->offset);
n->strlen = strlen(n->str);
n->lhs = NULL;
n->rhs = NULL;
n->tsuffix = NULL;
return n->type = ms->type;
}
}
return n->type = err(c, n->pos,
"no enum member '%s' in %s",
n->str ? n->str : "?",
ms->name);
}
}
}
Type *base = cexpr(c, n->lhs);
if (base == NULL || base == ty_err) return n->type = ty_err;
Type *u = type_chase_named(base);
if (u && u->kind == TY_PTR) u = u->sub;
u = type_chase_named(u);
/* Enum member access via a qualified base, e.g. `os.whence.CUR`.
* The inner N_DOT resolved through SK_USE → the SK_TYPE sym's
* named type. Fold the outer access to the member literal. */
if (u && u->kind == TY_ENUM) {
for (Tfield *f = u->fields; f; f = f->next) {
if (f->name && n->str &&
strcmp(f->name, n->str) == 0) {
n->kind = N_INTLIT;
n->uval = f->offset;
n->str = aprintf(c->a, "%llu",
(unsigned long long)f->offset);
n->strlen = strlen(n->str);
n->lhs = NULL;
n->rhs = NULL;
n->tsuffix = NULL;
return n->type = base;
}
}
return n->type = err(c, n->pos,
"no enum member '%s' in %s",
n->str ? n->str : "?",
type_name(c->a, base));
}
/* built-in pseudo-fields on slice/str/array: .len, .cap, .ptr */
if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY ||
u->kind == TY_STR)) {
if (strcmp(n->str, "len") == 0) return n->type = ty_i32;
/* A fixed array has no capacity word — .cap is invalid
* (drew ruling; arrays expose .len + .ptr only). Pre-fix
* cstage typed it i32 → cgen vague-rejected; wwstage
* link-errored / read garbage. Reject loud + early. */
if (u->kind == TY_ARRAY && strcmp(n->str, "cap") == 0)
return n->type = err(c, n->pos,
"no field 'cap' on a fixed-size array "
"(arrays have no capacity; use .len)");
if (strcmp(n->str, "cap") == 0) return n->type = ty_i32;
/* rule-9 divergence-doc (drew, task #13): `array.ptr` is a
* sanctioned ww spelling for `&A[0]` — a faithful Hare
* desugaring (arrays expose .len + .ptr; not .cap). 14 live
* consumers (lib/ww + cmd/wcc/cgen.ww). See
* .ai/drew-gapa-ptr-ruling.md. */
if (strcmp(n->str, "ptr") == 0) {
Type *elem = (u->kind == TY_STR) ? ty_u8 : u->sub;
return n->type = type_ptr(c->a, elem);
}
}
if (u && u->kind == TY_STRUCT) {
for (Tfield *f = u->fields; f; f = f->next)
if (strcmp(f->name, n->str) == 0)
return n->type = f->type;
return n->type = err(c, n->pos, "no field '%s' in %s",
n->str, type_name(c->a, base));
}
/* tuple positional access: t.0, t.1, ... */
if (u && u->kind == TY_TUPLE && n->str) {
int idx = 0;
for (const char *q = n->str; *q; q++) {
if (*q < '0' || *q > '9') { idx = -1; break; }
idx = idx * 10 + (*q - '0');
}
if (idx < 0)
return n->type = err(c, n->pos,
"tuple field must be numeric");
Tparam *tp = u->params;
while (idx > 0 && tp) { tp = tp->next; idx--; }
if (tp == NULL)
return n->type = err(c, n->pos,
"tuple index out of range");
return n->type = tp->type;
}
/* module-qualified: lhs is IDENT bound as SK_USE */
return n->type = ty_err;
}
case N_INDEX: {
Type *base = cexpr(c, n->lhs);
Type *idx = cexpr(c, n->rhs);
if (idx != ty_err && !type_isint(idx))
err(c, n->pos, "index must be integer");
if (base == ty_err) return n->type = ty_err;
Type *u = type_chase_named(base);
if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY)) {
/* #108(b): indexing needs the element size; `[]opaque`
* is a legal (sized) header but its element is unsized.
* harec ref/harec/src/check.c:384. */
if (u->sub && u->sub->size == SIZE_UNDEFINED)
err(c, n->pos, "cannot index %s: element type "
"'%s' has undefined size",
type_name(c->a, base),
type_name(c->a, u->sub));
return n->type = u->sub;
}
if (u && u->kind == TY_STR) {
/* rule-9 divergence-doc (drew .ai/drew-14-ruling.md):
* `str[i] -> u8` is a deliberate Go-like direct
* byte-index, a SANCTIONED ww divergence from Hare's
* `strings::toutf8(s)[i]` (the Hare reference checker
* rejects str-index, harec check.c:362). lib/strings
* is load-bearing on it (compare/dup/join). */
/* #14: a `def` scalar str is an inline compile-time
* CONSTANT (def-as-constant; not storage-backed like a
* `let`), so it has no address to index — cgen would
* load a frame-garbage base and segfault. Only the bare
* def-global operand is unindexable; let/param/local +
* string-literal operands stay valid. The faithful
* def-as-constant splice-index is deferred (no
* consumer). */
if (n->lhs->kind == N_IDENT) {
Sym *s = scope_lookup_prefer(c->cur,
c->cur_mod, n->lhs->str);
if (s && s->kind == SK_DEF)
return n->type = err(c, n->pos,
"cannot index a def-constant str "
"'%s'; bind it to a `let` (def "
"strings are inline constants, not "
"storage-backed)", n->lhs->str);
}
return n->type = ty_u8;
}
/* `*[N]T` auto-decays to `[N]T` indexing — drill into the
* inner T so callers see the element type, matching C's
* pointer-to-array semantics. `*[]T` does NOT auto-decay:
* `p[i]` for `p: *[]T` yields `[]T` via the default `*U → U`
* fall-through below (here U is `[]T`). Hare-faithful: a
* pointer-to-slice is a 1D array of slices, not of T. */
if (u && u->kind == TY_PTR && u->sub &&
u->sub->kind == TY_ARRAY)
return n->type = u->sub->sub;
/* C-style pointer indexing: p[i] → *(p+i) */
if (u && u->kind == TY_PTR && u->sub)
return n->type = u->sub;
return n->type = err(c, n->pos, "indexing non-indexable %s",
type_name(c->a, base));
}
case N_CALL: {
/* Hare-style builtins: len(x), append(s, v), alloc(...).
* Recognised by name with no scope binding; we type-check
* the args ourselves and skip the normal call resolution. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "len") == 0 &&
n->list != NULL && n->list->next == NULL) {
(void)cexpr(c, n->list);
n->type = ty_i32;
n->lhs->type = ty_err; /* mark builtin: no real symbol */
return n->type;
}
/* size(T) / align(T): fold to an integer literal. The arg is
* a type-expr node (planted by the parser, not a regular
* expression). */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
(strcmp(n->lhs->str, "size") == 0 ||
strcmp(n->lhs->str, "align") == 0) &&
n->list != NULL) {
int is_size = strcmp(n->lhs->str, "size") == 0;
Type *t = resolve_type(c, n->list);
u64 v = 0;
if (t && t != ty_err) {
u64 m = is_size ? t->size : t->align;
/* #108(b): size(opaque)/align(opaque) has no
* concrete answer; folding the (u64)-1 sentinel
* would be a silent miscompile (rule 7). harec
* ref/harec/src/check.c:2720. */
if (m == SIZE_UNDEFINED)
err(c, n->pos,
"cannot take %s of unsized type '%s'",
is_size ? "size" : "align",
type_name(c->a, t));
else
v = m;
}
n->kind = N_INTLIT;
n->uval = v;
n->str = aprintf(c->a, "%llu", (unsigned long long)v);
n->strlen = strlen(n->str);
n->lhs = NULL;
n->list = NULL;
n->tsuffix = NULL;
n->type = ty_untyped_int;
return n->type;
}
/* offset(e.f): the byte offset of `f` inside the struct type of
* `e`. Folded to an integer literal at check-time. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "offset") == 0 &&
n->list != NULL && n->list->next == NULL &&
n->list->kind == N_DOT) {
Node *dot = n->list;
Type *bt = cexpr(c, dot->lhs);
Type *u = type_chase_named(bt);
if (u && u->kind == TY_PTR) u = u->sub;
u = type_chase_named(u);
u64 off = 0;
int found = 0;
if (u && u->kind == TY_STRUCT) {
for (Tfield *f = u->fields; f; f = f->next)
if (strcmp(f->name, dot->str) == 0) {
off = f->offset; found = 1; break;
}
}
if (!found)
err(c, n->pos, "offset: no field '%s'",
dot->str ? dot->str : "?");
n->kind = N_INTLIT;
n->uval = off;
n->str = aprintf(c->a, "%llu", (unsigned long long)off);
n->strlen = strlen(n->str);
n->lhs = NULL;
n->list = NULL;
n->tsuffix = NULL;
n->type = ty_untyped_int;
return n->type;
}
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "append") == 0 &&
n->list != NULL && n->list->next != NULL) {
for (Node *a = n->list; a; a = a->next)
(void)cexpr(c, a);
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
/* `alloc(value)` Hare-style builtin — suppressed when the
* current module declares its own `alloc` (user shadow, task
* #23). Strict same-module check (not scope_lookup_prefer):
* `import rt;` brings rt.malloc into a separate qualified
* scope, not flat — only a same-module `fn alloc` registers
* here. The Hare builtin name is `alloc` (ref/hare/hare/lex/
* token.ha:21, ltok::ALLOC); a hypothetical user `fn malloc`
* does not shadow it. Task #23. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
n->list != NULL && n->list->next == NULL &&
!(c->cur_mod &&
scope_lookup_in_module(c->cur, c->cur_mod, "alloc"))) {
Type *t = cexpr(c, n->list);
Type *def = type_default(t);
Type *pt = type_ptr(c->a, def ? def : ty_void);
/* Task #30 — graduate to Hare's `(*T | nomem)` shape;
* the cgen branches on rt_malloc's null return to emit
* the nomem variant. Two-variant union with TY_PTR +
* TY_NAMED(nomem) does not trip the nullable-pointer
* fold (#25), so the result rides the general AX=tag,
* DX=ptr ABI both stages already share. */
Type *tt = newtype(c->a, TY_TAGGED);
Tparam *vp = amalloc(c->a, sizeof *vp);
Tparam *ve = amalloc(c->a, sizeof *ve);
vp->type = pt; vp->next = ve;
ve->type = ty_nomem; ve->next = NULL;
tt->params = vp;
/* #64: tag (8) + max-variant payload, per resolve_type:433. */
tt->size = 8 + pt->size;
tt->align = 8;
n->type = tt;
n->lhs->type = ty_err;
return n->type;
}
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "free") == 0 &&
n->list != NULL && n->list->next == NULL) {
(void)cexpr(c, n->list);
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
/* delete(xs[i]) / delete(xs[lo:hi]) — slice removal, the
* delete-half of #35 (insert() is the twin arm below).
* harec ref/harec/src/check.c:1981-2027 accepts both an
* indexing place (EXPR_ACCESS/ACCESS_INDEX) and a slicing
* place (EXPR_SLICE — Hare spells it delete(xs[i..j]));
* either way the OBJECT must be a slice. The range form
* is the fold-5a prereq P2 (regex.ha:333
* delete(jump_idxs[group_level][..])). */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "delete") == 0) {
Node *d = n->list;
if (d == NULL || d->next != NULL)
err(c, n->pos, "delete: takes exactly one argument");
if (d != NULL) {
(void)cexpr(c, d);
/* harec check.c:2016's reject; wording
* adapted to ww's delete: prefix. */
if (d->kind != N_INDEX && d->kind != N_SLICE)
err(c, n->pos, "delete: operand must be an indexing or slicing expression");
else {
Type *bt = d->lhs ? d->lhs->type : NULL;
bt = type_chase_named(bt);
/* harec check.c:2024 wording; a
* fixed-size [N]T base and a str
* base land here. */
if (bt == NULL || bt->kind != TY_SLICE)
err(c, n->pos, "delete must operate on a slice");
}
}
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
/* insert(xs[idx], v) — single-element slice insertion
* before idx, delete()'s twin (the insert-half of #35).
* harec models append/insert in ONE checker arm
* (ref/harec/src/check.c:745 check_expr_append_insert;
* "insert" at :786): operand 1 must be an indexing place
* over a slice; idx == len is a legal end-insert (the
* ref/hare os/exec platform_cmd.ha:86 idiom). The spread
* form insert(xs[i], vs...) and the with-length form
* (harec :821/:837) stay filed on #35 — regex fold-3's
* consumers are all single-value. A range PLACE is not
* Hare (harec asserts ACCESS_INDEX at :784; the form
* never parses there) — rejected, no task cite. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "insert") == 0) {
Node *d = n->list;
if (d == NULL || d->next == NULL ||
d->next->next != NULL)
err(c, n->pos, "insert: takes exactly two arguments");
if (d != NULL) {
(void)cexpr(c, d);
if (d->kind == N_SLICE)
err(c, n->pos, "insert: range place is invalid; operand must be an indexing expression xs[i]");
else if (d->kind != N_INDEX)
err(c, n->pos, "insert: operand must be an indexing expression xs[i]");
else {
Type *bt = d->lhs ? d->lhs->type : NULL;
bt = type_chase_named(bt);
/* harec check.c:807 wording; a
* fixed-size [N]T base lands here. */
if (bt == NULL || bt->kind != TY_SLICE)
err(c, n->pos, "insert must operate on a slice");
}
if (d->next != NULL) {
if (d->next->kind == N_SPREAD)
err(c, n->pos, "insert: spread form insert(xs[i], vs...) unimplemented (task #35)");
else
(void)cexpr(c, d->next);
}
}
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
/* assert(cond[, msg]) / abort([msg]) — runtime checks that
* call into rt_abort. msg must be a str when present.
* Only treated as builtins when no user symbol shadows the
* name; existing code that declares its own `abort`/`assert`
* (e.g. lib/os/os.ww) keeps working unchanged. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "abort") == 0 &&
scope_lookup_prefer(c->cur, c->cur_mod, "abort") == NULL) {
if (n->list) {
Type *mt = cexpr(c, n->list);
if (mt != ty_err && !type_assignable(ty_str, mt))
err(c, n->pos, "abort: message must be str");
if (n->list->next)
err(c, n->pos, "abort: at most one arg");
}
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "assert") == 0 &&
n->list != NULL &&
scope_lookup_prefer(c->cur, c->cur_mod, "assert") == NULL) {
Type *ct = cexpr(c, n->list);
if (ct != ty_err && ct != ty_bool && ct != ty_untyped_bool)
err(c, n->pos, "assert: cond must be bool");
if (n->list->next) {
Type *mt = cexpr(c, n->list->next);
if (mt != ty_err && !type_assignable(ty_str, mt))
err(c, n->pos, "assert: message must be str");
if (n->list->next->next)
err(c, n->pos, "assert: at most two args");
}
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
/* alloc([], n) — Hare-style fresh slice with cap n. We pin
* the element type to u8 by default; the caller's declared
* slice type drives the actual element size at codegen.
* Same single-key `alloc` gate as the value-form above. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
n->list && n->list->kind == N_ARRLIT &&
n->list->list == NULL &&
n->list->next && n->list->next->next == NULL &&
!(c->cur_mod &&
scope_lookup_in_module(c->cur, c->cur_mod, "alloc"))) {
(void)cexpr(c, n->list->next);
/* B' (#3): an empty `[]` carries no element type. ww
* gets that type only from a let annotation (the
* #45 retype below). Any other empty alloc — return,
* call-arg, bare `let b = alloc([],n)` — has no hint,
* so refuse to guess instead of defaulting to u8 (was
* a silent u8-default + #5 value-form miscompile).
* Aligns ww DOWN to harec, which errors the same way:
* ref/harec/src/check.c:1801-1802. */
if (n != c->alloc_octx) {
err(c, n->pos, "cannot infer slice element "
"type for alloc([], n) without a type "
"hint; annotate the binding, e.g. "
"`let x: []T = alloc([], n)`");
n->lhs->type = ty_err;
return n->type = ty_err;
}
Type *st = type_slice(c->a, ty_u8);
/* Task #30 — slice form graduates the same way:
* `alloc([], n)` now returns `([]T | nomem)`. Slot is
* 8 (tag) + 24 (slice payload) = 32B. The element type
* defaults to u8 here; the let-init shortcut in
* cmd/w6c/cgen.c N_LET drives the real element size
* from the declared slice type. */
Type *tt = newtype(c->a, TY_TAGGED);
Tparam *vs = amalloc(c->a, sizeof *vs);
Tparam *ve = amalloc(c->a, sizeof *ve);
vs->type = st; vs->next = ve;
ve->type = ty_nomem; ve->next = NULL;
tt->params = vs;
/* #64: tag (8) + slice payload, per resolve_type:433. */
tt->size = 8 + st->size;
tt->align = 8;
n->type = tt;
n->lhs->type = ty_err;
return n->type;
}
Type *ft = cexpr(c, n->lhs);
if (ft == ty_err) {
/* Walk args anyway so cgen sees real types. The
* common case is a module-qualified call whose leaf
* isn't in this scope (raw w6c on a single file with
* `use mod;` but no driver concatenation). */
for (Node *a = n->list; a; a = a->next)
(void)cexpr(c, a);
return n->type = ty_err;
}
Type *u = type_chase_named(ft);
if (u == NULL || u->kind != TY_FN)
return n->type = err(c, n->pos, "calling non-function %s",
type_name(c->a, ft));
Tparam *p = u->params;
for (Node *a = n->list; a; a = a->next) {
Type *at = cexpr(c, a);
if (p == NULL) {
if (!u->variadic)
err(c, n->pos, "too many arguments");
continue;
}
/* Hare-style variadic param: every remaining arg either
* - flows into the gather (assignable to element T), or
* - is a single `xs...` spread of `[]T` (forwarding).
* Don't advance p — the variadic slot absorbs the tail. */
if (p->variadic) {
Type *elem = (p->type && p->type->kind == TY_SLICE)
? p->type->sub : ty_err;
if (a->kind == N_SPREAD) {
if (at != ty_err && p->type != ty_err &&
!type_assignable(p->type, at))
err(c, a->pos,
"spread arg: %s not assignable to %s",
type_name(c->a, at),
type_name(c->a, p->type));
if (a->next != NULL)
err(c, a->pos,
"spread arg must be the last");
} else if (elem != ty_err && at != ty_err) {
if (!type_assignable(elem, at) &&
!assignable_addrfn(c, elem, a))
err(c, a->pos,
"variadic arg: %s not assignable to %s",
type_name(c->a, at),
type_name(c->a, elem));
}
continue;
}
if (!type_assignable(p->type, at) && at != ty_err && p->type != ty_err
&& !assignable_addrfn(c, p->type, a))
err(c, a->pos, "argument type %s not assignable to %s",
type_name(c->a, at), type_name(c->a, p->type));
/* #258: `f(arr)` borrows the array as a full slice.
* #31/#33: a bare array LITERAL arg has no backing —
* loud-reject (supported only at a `let`). */
if (!reject_arrlit_borrow(c, p->type, a))
desugar_arrayslice(c, p->type, a);
p = p->next;
}
if (p != NULL && !p->variadic)
err(c, n->pos, "not enough arguments");
return n->type = u->ret ? u->ret : ty_void;
}
case N_ASSIGN: {
/* `_` lvalue: discard the rhs. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && n->lhs->str[0] == '\0') {
(void)cexpr(c, n->rhs);
return n->type = ty_void;
}
/* Reject assignment to a const-bound name. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str) {
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod,
n->lhs->str);
if (s && s->is_const)
err(c, n->pos, "cannot assign to const `%s`",
n->lhs->str);
}
Type *l = cexpr(c, n->lhs);
Type *r = cexpr(c, n->rhs);
if (l != ty_err && r != ty_err && !type_assignable(l, r) &&
!assignable_addrfn(c, l, n->rhs))
err(c, n->pos, "cannot assign %s to %s",
type_name(c->a, r), type_name(c->a, l));
/* #258: `s = arr` borrows the array as a full slice.
* #31/#33: a bare array LITERAL rhs has no backing —
* loud-reject (supported only at a `let`). */
if (!reject_arrlit_borrow(c, l, n->rhs))
desugar_arrayslice(c, l, n->rhs);
return n->type = l;
}
case N_STRUCTLIT: {
/* lhs may be an N_IDENT (the bare type name) or a real type
* expression. Resolve via name lookup first; fall back to
* resolve_type for the synthetic-type-expr case. */
Type *t = NULL;
if (n->lhs && n->lhs->kind == N_IDENT) {
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod,
n->lhs->str);
if (s == NULL || s->kind != SK_TYPE)
t = err(c, n->pos, "unknown struct type '%s'",
n->lhs->str);
else
t = s->type;
} else {
t = resolve_type(c, n->lhs);
}
Type *u = type_chase_named(t);
for (Node *f = n->list; f; f = f->next) {
Type *vt = cexpr(c, f->lhs);
if (u && u->kind == TY_STRUCT) {
Tfield *match = NULL;
for (Tfield *fl = u->fields; fl; fl = fl->next)
if (strcmp(fl->name, f->str) == 0) {
match = fl; break;
}
if (match == NULL)
err(c, f->pos, "no field '%s' in %s",
f->str, type_name(c->a, t));
else if (vt != ty_err &&
!type_assignable(match->type, vt) &&
!arrlit_init_fits(c, match->type, f->lhs) &&
!assignable_addrfn(c, match->type, f->lhs))
err(c, f->pos, "field %s: %s not assignable to %s",
f->str, type_name(c->a, vt),
type_name(c->a, match->type));
}
}
return n->type = t;
}
case N_ARRLIT: {
Type *elt = NULL;
u64 count = 0;
for (Node *e = n->list; e; e = e->next) {
if (e->kind == N_FIELD && e->str &&
strcmp(e->str, "...") == 0)
continue;
Type *t = cexpr(c, e);
if (elt == NULL) elt = type_default(t);
count++;
}
if (elt == NULL) elt = ty_int; /* empty inferred arrlit: pair of int-default (#103); symmetric w/ wwstage check.ww mktname "int" */
return n->type = type_array(c->a, elt, count);
}
case N_SPREAD:
return n->type = cexpr(c, n->lhs);
case N_SLICE: {
Type *base = cexpr(c, n->lhs);
if (n->rhs) (void)cexpr(c, n->rhs);
if (n->cond) (void)cexpr(c, n->cond);
Type *u = type_chase_named(base);
if (u && u->kind == TY_ARRAY)
return n->type = type_slice(c->a, u->sub);
if (u && u->kind == TY_SLICE)
return n->type = base;
if (u && u->kind == TY_STR)
return n->type = ty_str;
/* Retained divergence: *[N]T does NOT decay here —
* `p[lo:hi]` types as [][N]T (C-pointer-slicing), unlike
* the index route (idx_eff) and unlike Hare. Loud on the
* usual []T annotation; for-range likewise. Team task #18
* (#61-residual A). */
if (u && u->kind == TY_PTR && u->sub)
return n->type = type_slice(c->a, u->sub);
return n->type = err(c, n->pos, "cannot slice %s",
type_name(c->a, base));
}
case N_RECV: {
Type *t = cexpr(c, n->lhs);
Type *u = type_chase_named(t);
if (u && u->kind == TY_CHAN) return n->type = u->sub;
return n->type = err(c, n->pos, "<- expects chan, got %s",
type_name(c->a, t));
}
case N_MATCH: {
Type *st = cexpr(c, n->lhs);
Type *u = type_chase_named(st);
if (u == NULL || u->kind != TY_TAGGED) {
return n->type = err(c, n->pos,
"match on non-tagged-union %s", type_name(c->a, st));
}
int has_default = 0;
for (Node *cs = n->list; cs; cs = cs->next) {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
/* Resolve the case pattern's type so codegen can map it
* to the variant tag. Both `case T =>` and `case let v: T
* =>` get this — `case =>` (default) leaves cs->type NULL.
* For multi-pattern `case T1 | T2 =>` each alternative in
* cs->list also gets its type resolved in place. */
if (cs->lhs == NULL) {
has_default = 1;
} else {
Type *vt = resolve_type(c, cs->lhs);
cs->type = vt;
for (Node *alt = cs->list; alt; alt = alt->next)
alt->type = resolve_type(c, alt);
/* Validity: every `case T =>` pattern must
* refer to a variant of the scrutinee's
* tagged union. Mirrors the existing is/as
* check; `match (u) { case f64 => ... }`
* where f64 isn't a variant of u is dead code
* the dispatch never reaches, so refuse it. */
if (vt && vt != ty_err &&
!variant_present(u->params, vt))
err(c, cs->pos,
"case: %s is not a variant of %s",
type_name(c->a, vt),
type_name(c->a, st));
for (Node *alt = cs->list; alt; alt = alt->next) {
if (alt->type == NULL ||
alt->type == ty_err) continue;
if (!variant_present(u->params,
alt->type))
err(c, cs->pos,
"case: %s is not a variant of %s",
type_name(c->a, alt->type),
type_name(c->a, st));
}
if (cs->str && cs->str[0]) {
check_module_shadow(c, cs->str,
cs->pos, "binding");
scope_define(c->cur, cs->str, SK_VAR, vt, cs);
}
}
cstmt(c, cs->body);
c->cur = saved;
}
/* Exhaustiveness: every variant must be handled. A default arm
* absorbs anything not otherwise covered. */
if (!has_default) {
for (Tparam *p = u->params; p; p = p->next) {
int covered = 0;
for (Node *cs = n->list; cs && !covered;
cs = cs->next) {
if (variant_match(cs->type, p->type)) {
covered = 1;
break;
}
for (Node *alt = cs->list; alt;
alt = alt->next)
if (variant_match(alt->type,
p->type)) {
covered = 1;
break;
}
}
if (!covered)
err(c, n->pos,
"match: variant %s not handled",
type_name(c->a, p->type));
}
}
/* match-as-expression: the type is the common yield type
* across arms. If no arm yields, the match is a statement
* and its type is void. */
Type *yt = NULL;
for (Node *cs = n->list; cs; cs = cs->next) {
Type *t = match_yield_type(cs->body);
if (t == NULL) continue;
if (yt == NULL) yt = t;
else if (!type_eq(yt, t) && !type_assignable(yt, t))
err(c, cs->pos,
"match arm yields %s, expected %s",
type_name(c->a, t), type_name(c->a, yt));
}
n->type = yt ? yt : ty_void;
return n->type;
}
case N_TYPETEST: case N_TYPEASSERT: {
/* `e is T` → bool; `e as T` → T.
* Requires lhs to be a tagged union and T to be one of its
* variants. The variant-index lookup lives in cgen (it knows
* NAMED-vs-structural matching for the success-variant rules);
* here we just check the LHS shape and resolve T. */
Type *t = cexpr(c, n->lhs);
Type *vt = resolve_type(c, n->rhs);
/* Stash the variant on rhs->type — cgen reads it uniformly
* whether the expression returns bool (is) or the variant
* itself (as). */
if (n->rhs) n->rhs->type = vt;
Type *u = type_chase_named(t);
/* Enum ↔ integer cast: `enumval as intT` or `int as enumT`.
* Reinterpret-only — the storage shape is already integer, so
* cgen treats the cast as a no-op (the value lives in the same
* register). The `is` form is rejected; enums aren't sums. */
Type *uu = u;
Type *vu = type_chase_named(vt);
int lhs_enum = uu && uu->kind == TY_ENUM;
int rhs_enum = vu && vu->kind == TY_ENUM;
if (n->kind == N_TYPEASSERT && (lhs_enum || rhs_enum) &&
type_isint(t) && type_isint(vt)) {
return n->type = vt;
}
if (u == NULL || u->kind != TY_TAGGED) {
const char *op = (n->kind == N_TYPETEST) ? "is" : "as";
return n->type = err(c, n->pos,
"%s on non-tagged-union %s", op,
type_name(c->a, t));
}
/* Diagnostic-only: verify T appears as a variant. Mirrors
* cg_variant_match (NAMED ≡ pointer-identical, otherwise
* structural). Skipped silently if vt is ty_err. */
if (vt && vt != ty_err) {
int found = 0;
for (Tparam *p = u->params; p; p = p->next) {
if (p->type == NULL) continue;
if (p->type->kind == TY_NAMED &&
vt->kind == TY_NAMED) {
if (p->type == vt) { found = 1; break; }
} else if (p->type->kind == TY_NAMED ||
vt->kind == TY_NAMED) {
continue;
} else if (type_eq(p->type, vt)) {
found = 1; break;
}
}
if (!found)
err(c, n->pos, "%s is not a variant of %s",
type_name(c->a, vt), type_name(c->a, t));
}
return n->type = (n->kind == N_TYPETEST) ? ty_bool : vt;
}
case N_TRYPROP: case N_TRYUNW: {
Type *t = cexpr(c, n->lhs);
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_TAGGED) {
return n->type = err(c, n->pos,
"%s on non-tagged-union %s",
n->kind == N_TRYPROP ? "?" : "!",
type_name(c->a, t));
}
/* Error subset = `!`-flagged variants (Hare semantics) or
* everything-but-first when no flags are present (legacy).
*
* For ? : each error variant must be propagatable — i.e. it
* must be a variant of the enclosing function's return type
* (so the caller can match on it). cgen does the tag remap.
* For ! : no propagation, so no subset check. */
Type *succ = tagged_success_type(u);
int has_errors = 0, nsucc = 0;
for (Tparam *p = u->params; p; p = p->next) {
if (tagged_is_error_variant(u, p->type))
has_errors = 1;
else
nsucc++;
}
/* F8 interim gate (task #5): try-propagation assumes ONE
* success member end-to-end — succ collapses to the first
* non-error variant and cgen emits a single tag compare, so
* any OTHER success member is silently mistaken for an error
* (? propagates it; ! aborts on it). One class, both ops
* (#133 precedent). Until the honest subset-union result
* typing lands (task #14, harec check.c:2759-2835), reject
* loud. */
if (nsucc > 1) {
return n->type = err(c, n->pos,
"%s: multi-success union unwired (task #14): bind and match instead",
n->kind == N_TRYPROP ? "?" : "!");
}
if (n->kind == N_TRYPROP && has_errors) {
Type *r = c->ret;
Type *ru = type_chase_named(r);
if (ru == NULL || ru->kind != TY_TAGGED) {
err(c, n->pos,
"?: enclosing function must return a tagged "
"union to propagate errors (got %s)",
type_name(c->a, r));
} else {
for (Tparam *e = u->params; e; e = e->next) {
if (!tagged_is_error_variant(u, e->type))
continue;
int ok = 0;
for (Tparam *p = ru->params; p;
p = p->next)
if (variant_match(p->type,
e->type)) {
ok = 1; break;
}
if (!ok)
err(c, n->pos,
"?: error variant %s not in enclosing return %s",
type_name(c->a, e->type),
type_name(c->a, r));
}
}
}
return n->type = succ ? succ : ty_err;
}
case N_TUPLE: {
/* keep untyped element types; assignability is checked
* element-wise at the consumer (return / mlet / massign).
* Size/align must still be planted HERE: an inferred
* `let t = (a, b)` takes this type verbatim as the local's
* type (type_default passes TY_TUPLE through) and cgen's
* N_LET sizes the frame slot from ->size — a 0-size tuple
* planted the local at offset 0, over the saved BP/RIP
* (task #44 segfault; wwstage is the correct reference:
* check.ww exprtype N_TUPLE routes through tinfofornode).
* Slot rule mirrors the N_TTUPLE twin (resolve_type) and
* cgen tuple_eslot; untyped elements are sized at their
* default (tuple_eslot's UNTYPED_STR precedent). */
Type *t = newtype(c->a, TY_TUPLE);
Tparam *head = NULL, *tail = NULL;
u64 sz = 0, al = 1;
for (Node *e = n->list; e; e = e->next) {
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->type = cexpr(c, e);
Type *ed = type_default(tp->type);
if (ed && ed->align > al) al = ed->align;
Type *eu = type_chase_named(ed);
/* #24: an INFERRED composite element (array/struct/
* nested tuple >8B) drops on construction + segvs on
* the t.N read, exactly as the explicit N_TTUPLE twin
* (resolve_type) — wwstage's tinfofornode choke-point
* catches declared AND inferred, so cstage must reject
* the inferred literal here too (rule-10 symmetry).
* Reject until #60/DISP-A inlines it. */
if (eu && (eu->kind == TY_ARRAY
|| eu->kind == TY_STRUCT
|| eu->kind == TY_TUPLE))
err(c, n->pos, "tuple element must be a "
"scalar, str, slice, or tagged-union "
"(composite element deferred to task "
"#60)");
if (eu && (eu->kind == TY_STR || eu->kind == TY_SLICE
|| eu->kind == TY_TAGGED))
sz += (eu->size + 7) & ~(u64)7;
else if (eu == NULL || eu->kind != TY_VOID)
sz += 8; /* sizelint-ok: the slot IS the 8B eightbyte */
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
}
t->params = head;
t->size = sz;
t->align = al;
return n->type = t;
}
default:
return n->type = err(c, n->pos, "internal: unhandled expr kind %d",
n->kind);
}
}
/* ---- statements --------------------------------------------------- */
static void
clet(Checker *c, Node *n)
{
Type *declared = n->lhs ? resolve_type(c, n->lhs) : NULL;
Type *initt = NULL;
/* B' (#3): a `let x: []T = alloc([], n)` is the one context that
* lets the empty alloc infer its element type (the #45 retype runs
* AFTER cexpr, so flag the exact call node up front; cexpr errors on
* any empty alloc that isn't this one). Peel the same ?/! wrapper
* #45 peels so the flagged node matches. */
Node *octx = NULL;
if (declared && declared->kind == TY_SLICE && n->rhs) {
Node *call = n->rhs;
if (call->kind == N_TRYPROP || call->kind == N_TRYUNW)
call = call->lhs;
if (call && call->kind == N_CALL && call->lhs
&& call->lhs->kind == N_IDENT && call->lhs->str
&& strcmp(call->lhs->str, "alloc") == 0
&& call->list && call->list->kind == N_ARRLIT
&& call->list->list == NULL
&& call->list->next && call->list->next->next == NULL)
octx = call;
}
Node *saved_octx = c->alloc_octx;
c->alloc_octx = octx;
if (n->rhs) initt = cexpr(c, n->rhs);
c->alloc_octx = saved_octx;
/* `let xs: [_]T = arrlit;` — fill in the inferred length from the
* initialiser. `resolve_type` left alen=0 as a sentinel. A `[_]T`
* with no array-literal initialiser (no init at all, or a non-array
* init) can't infer its length — that is a loud error, never a
* silent zero-length array (rule 7, #7). */
if (declared && declared->kind == TY_ARRAY && declared->alen == 0
&& is_infer_arr(n->lhs)) {
Type *iu = type_chase_named(initt);
if (iu && iu->kind == TY_ARRAY)
declared = type_array(c->a, declared->sub, iu->alen);
else
err(c, n->pos, "[_]T needs an array-literal initialiser");
}
/* #108(b): a bare `let x: opaque` would fabricate a (u64)-1-byte
* local. `*opaque` / `[]opaque` locals are sized and pass. */
if (declared)
require_sized(c, declared, n->pos, "a variable");
Type *t = declared;
if (t == NULL && initt) t = type_default(initt);
if (t == NULL) {
err(c, n->pos, "let needs a type or initialiser");
t = ty_err;
}
/* An array literal with a trailing `...` repeat marker has
* "flexible" length — the last value fills the remaining slots.
* The literal's type carries the explicit-element count, which
* may not match the declared length. Trust the declared type
* when the marker is present.
*
* #71: NOT when the declared type is an array — there the repeat
* only relaxes the length upward (explicit count <= N, fill the
* rest); arrlit_init_fits skips the marker and runs the overlong
* reject + per-element range checks. The blanket bypass let
* `[2]int = [1,2,3...]` write past the slot (saved-BP clobber)
* and `[2]u8 = [999...]` skip the #130 range check — wwstage's
* checkletassign already runs both unconditionally. */
int has_arr_repeat = 0;
if (n->rhs && n->rhs->kind == N_ARRLIT) {
for (Node *e = n->rhs->list; e; e = e->next)
if (e->kind == N_FIELD && e->str &&
strcmp(e->str, "...") == 0) {
has_arr_repeat = 1;
break;
}
}
if (has_arr_repeat && declared) {
Type *du = type_chase_named(declared);
if (du && du->kind == TY_ARRAY)
has_arr_repeat = 0;
}
/* #45: alloc([], n) defers element type to the let-init context
* (Hare-style). cexpr's alloc-slice branch synthesizes
* ([]u8 | nomem) with no LHS context; when the let declares []T,
* retype the inner call (and any ?/! wrapper) to ([]T | nomem) /
* []T so the assignability check below succeeds for any T. cgen
* already drives element size from declared->sub at the N_LET
* shortcut (cmd/w6c/cgen.c). */
if (declared && declared->kind == TY_SLICE && declared->sub
&& declared->sub != ty_u8 && n->rhs) {
Node *wrap = NULL;
Node *call = n->rhs;
if (call->kind == N_TRYPROP || call->kind == N_TRYUNW) {
wrap = call;
call = call->lhs;
}
if (call && call->kind == N_CALL && call->lhs
&& call->lhs->kind == N_IDENT
&& call->lhs->type == ty_err
&& call->lhs->str
&& strcmp(call->lhs->str, "alloc") == 0
&& call->list && call->list->kind == N_ARRLIT
&& call->list->list == NULL
&& call->list->next
&& call->list->next->next == NULL) {
Type *st = type_slice(c->a, declared->sub);
Type *tt = newtype(c->a, TY_TAGGED);
Tparam *vs = amalloc(c->a, sizeof *vs);
Tparam *ve = amalloc(c->a, sizeof *ve);
vs->type = st; vs->next = ve;
ve->type = ty_nomem; ve->next = NULL;
tt->params = vs;
/* #64: tag (8) + slice payload, per resolve_type:433. */
tt->size = 8 + st->size;
tt->align = 8;
call->type = tt;
if (wrap) {
wrap->type = st;
initt = st;
} else {
initt = tt;
}
}
}
if (declared && initt && initt != ty_err && !has_arr_repeat &&
!type_assignable(declared, initt) &&
!arrlit_init_fits(c, declared, n->rhs) &&
!assignable_addrfn(c, declared, n->rhs))
err(c, n->pos, "init %s not assignable to declared %s",
type_name(c->a, initt), type_name(c->a, declared));
/* #5/#60: reject boxing an array payload into a tagged variant. */
if (declared && initt && initt != ty_err &&
tagged_array_variant(declared, initt))
err(c, n->pos, "array-typed tagged-union variant "
"construction unwired — reject (task #5 / #60)");
/* #104 fold-2: `let x: f32 = 1.0` — narrow the init literal to f32. */
coerce_floatlit(n->rhs, declared);
/* #258: `let s: []T = arr` borrows the array as a full slice. */
desugar_arrayslice(c, declared, n->rhs);
n->type = t;
if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "let");
Sym *s = scope_define(c->cur, n->str, SK_VAR, t, n);
if (s == NULL)
err(c, n->pos, "let '%s' redeclared in same scope",
n->str);
else if (n->op == TK_CONST) s->is_const = 1;
}
}
static void
cstmt(Checker *c, Node *n)
{
if (n == NULL) return;
switch (n->kind) {
case N_BLOCK: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
for (Node *s = n->list; s; s = s->next)
cstmt(c, s);
c->cur = saved;
break;
}
case N_EXPRSTMT: (void)cexpr(c, n->lhs); break;
case N_LET: clet(c, n); break;
case N_RETURN: {
Type *rt = n->lhs ? cexpr(c, n->lhs) : ty_void;
if (c->ret == NULL) {
err(c, n->pos, "return outside function");
break;
}
if (c->ret == ty_void && n->lhs)
err(c, n->pos, "return value in void function");
else if (c->ret != ty_void && rt != ty_err && c->ret != ty_err
&& !type_assignable(c->ret, rt)
&& !assignable_addrfn(c, c->ret, n->lhs))
err(c, n->pos, "return %s not assignable to %s",
type_name(c->a, rt), type_name(c->a, c->ret));
/* #5/#60: reject returning an array payload into a tagged variant. */
else if (c->ret != ty_void && rt != ty_err && c->ret != ty_err &&
tagged_array_variant(c->ret, rt))
err(c, n->pos, "array-typed tagged-union variant "
"construction unwired — reject (task #5 / #60)");
/* #104 fold-2: `fn g() f32 = { return 1.0; }` — narrow to f32. */
coerce_floatlit(n->lhs, c->ret);
/* #258: `return arr` borrows the array as a full slice.
* #31/#33: a bare array LITERAL has no backing — loud-reject
* (supported only at a `let`). */
if (!reject_arrlit_borrow(c, c->ret, n->lhs))
desugar_arrayslice(c, c->ret, n->lhs);
break;
}
case N_IF: {
Type *ct = cexpr(c, n->cond);
/* harec dealiases at the if-cond consumer (ref/harec/src/
* check.c:2141); ww accepts + runs alias-bool (F0 m2_if).
* assert stays UN-chased — both stages loud there (F0 2a). */
Type *cu = type_chase_named(ct);
if (ct != ty_err && cu != ty_bool && cu != ty_untyped_bool)
err(c, n->pos, "if condition must be bool, got %s",
type_name(c->a, ct));
cstmt(c, n->body);
cstmt(c, n->els);
break;
}
case N_FORRANGE: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
c->loops++;
Type *st = cexpr(c, n->lhs);
Type *u = type_chase_named(st);
Type *elem = NULL;
if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY)) elem = u->sub;
else if (u && u->kind == TY_STR) elem = ty_u8;
else err(c, n->pos, "for-range needs slice/array/str");
if (n->list != NULL) {
/* tuple destructure: each name binds to a tuple field */
Type *etu = type_chase_named(elem);
Tparam *tp = (etu && etu->kind == TY_TUPLE) ? etu->params : NULL;
for (Node *nm = n->list; nm; nm = nm->next) {
Type *ft = tp ? tp->type : ty_err;
if (nm->str && nm->str[0]) {
check_module_shadow(c, nm->str,
nm->pos, "binding");
if (scope_define(c->cur, nm->str,
SK_VAR, ft, nm) == NULL)
err(c, nm->pos,
"binding '%s' redeclared in same scope",
nm->str);
}
if (tp) tp = tp->next;
}
} else if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "binding");
scope_define(c->cur, n->str, SK_VAR,
elem ? elem : ty_err, n);
}
cstmt(c, n->body);
c->loops--;
if (n->els) cstmt(c, n->els);
c->cur = saved;
break;
}
case N_FOR: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
c->loops++;
if (n->lhs) cstmt(c, n->lhs); /* init may be a let or expr */
if (n->cond) {
Type *ct = cexpr(c, n->cond);
/* harec dealiases at the loop-cond consumer (ref/
* harec/src/check.c:2515); F0 m2_while. */
Type *cu = type_chase_named(ct);
if (ct != ty_err && cu != ty_bool && cu != ty_untyped_bool)
err(c, n->pos, "for condition must be bool, got %s",
type_name(c->a, ct));
}
if (n->rhs) (void)cexpr(c, n->rhs);
cstmt(c, n->body);
c->loops--;
/* `else` block: runs at normal cond-false exit (skipped by
* break). Outside the loop count — break/continue inside the
* else target an enclosing loop, not this one. */
if (n->els) cstmt(c, n->els);
c->cur = saved;
break;
}
case N_MLET: {
Type *rt = cexpr(c, n->rhs);
Type *u = (rt && rt->kind == TY_TUPLE) ? rt : NULL;
if (u == NULL) {
err(c, n->pos, "multi-let rhs is not a tuple (got %s)",
type_name(c->a, rt));
}
Tparam *tp = u ? u->params : NULL;
for (Node *l = n->list; l; l = l->next) {
Type *declared = l->lhs ? resolve_type(c, l->lhs) : NULL;
Type *elem = tp ? tp->type : NULL;
Type *t = declared ? declared :
(elem ? type_default(elem) : ty_err);
if (declared && elem && !type_assignable(declared, elem))
err(c, l->pos, "let %s: %s not assignable from %s",
l->str, type_name(c->a, elem),
type_name(c->a, declared));
l->type = t;
if (l->str && l->str[0]) {
check_module_shadow(c, l->str, l->pos, "let");
Sym *s = scope_define(c->cur, l->str, SK_VAR, t, l);
if (s == NULL)
err(c, l->pos,
"let '%s' redeclared in same scope",
l->str);
else if (n->op == TK_CONST) s->is_const = 1;
}
if (tp) tp = tp->next;
}
if (u && tp != NULL)
err(c, n->pos, "tuple has extra elements");
break;
}
case N_MASSIGN: {
Type *rt = cexpr(c, n->rhs);
Type *u = (rt && rt->kind == TY_TUPLE) ? rt : NULL;
if (u == NULL) {
err(c, n->pos, "multi-assign rhs is not a tuple (got %s)",
type_name(c->a, rt));
}
Tparam *tp = u ? u->params : NULL;
for (Node *lv = n->list; lv; lv = lv->next) {
/* `_` lvalue: skip type check, advance the tuple cursor. */
if (lv->kind == N_IDENT && lv->str && lv->str[0] == '\0') {
if (tp) tp = tp->next;
continue;
}
Type *lt = cexpr(c, lv);
Type *elem = tp ? tp->type : NULL;
if (lt && elem && !type_assignable(lt, elem))
err(c, lv->pos, "cannot assign %s to %s",
type_name(c->a, elem), type_name(c->a, lt));
if (tp) tp = tp->next;
}
break;
}
case N_DEFER: (void)cexpr(c, n->lhs); break;
case N_YIELD: if (n->lhs) (void)cexpr(c, n->lhs); break;
case N_BREAK:
case N_CONTINUE:
if (c->loops == 0)
err(c, n->pos, "%s outside loop",
n->kind == N_BREAK ? "break" : "continue");
break;
case N_SWITCH: {
Type *st = cexpr(c, n->lhs);
(void)st;
for (Node *cs = n->list; cs; cs = cs->next) {
for (Node *e = cs->list; e; e = e->next)
(void)cexpr(c, e);
cstmt(c, cs->body);
}
break;
}
default:
err(c, n->pos, "internal: unhandled stmt kind %d", n->kind);
}
}
/* ---- top-level ---------------------------------------------------- */
static Type *
build_fn_type(Checker *c, Node *fn)
{
Type *t = newtype(c->a, TY_FN);
t->size = 8; t->align = 8;
t->ret = fn->lhs ? resolve_type(c, fn->lhs) : ty_void;
/* #108(b): opaque can't be returned by value (undefined size); harec
* ref/harec/src/check.c:3931. `*opaque` / `[]opaque` returns are
* sized and pass. */
require_sized(c, t->ret, fn->pos, "a return type");
Tparam *head = NULL, *tail = NULL;
for (Node *p = fn->list; p; p = p->next) {
if (p->str && strcmp(p->str, "...") == 0) {
t->variadic = 1;
continue;
}
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->name = p->str;
Type *pt = resolve_type(c, p->lhs);
/* Hare-style `T...` — see resolve_type N_TFN. */
if (p->op == TK_ELLIPSIS) {
tp->variadic = 1;
tp->type = type_slice(c->a, pt);
} else {
tp->type = pt;
}
/* #108(b): a by-value opaque param has undefined size. The
* `T...` variadic form wraps in []T (sized) above, so guard
* tp->type after the wrap, not pt. */
require_sized(c, tp->type, p->pos, "a parameter");
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
}
t->params = head;
return t;
}
void
check_init(Checker *c, Arena *a)
{
memset(c, 0, sizeof *c);
c->a = a;
c->is_test = 0; /* #15: caller (w6c main) sets it after init */
typesinit(a);
c->top = newscope(a, NULL);
c->cur = c->top;
}
/*
* decl_mod — module-tag stamp for a top-level decl.
*
* The driver concatenates imported sources before the primary file
* and emits `// MODULE: foo` directives the lexer pins onto each
* decl's `module` field. We treat a decl as "imported" iff its module
* directive matches some `use IDENT;` bareword in this compilation
* unit. Primary-file decls return NULL so they coexist (mod=NULL)
* with imported decls of the same leaf name in scope_lookup_in_module.
*/
static const char *
decl_mod(Node *file, Node *d)
{
if (d == NULL || d->module == NULL || file == NULL) return NULL;
for (Node *u = file->list; u; u = u->next) {
/* M1 #22: a decl is imported iff some `use` directive's full
* dotted import path equals the decl's module (now the path,
* not the leaf). For single-level packages usepath == leaf so
* this is unchanged; nested packages (`encoding.utf8`) match
* here instead of on the bare leaf. */
if (u->kind == N_USE && u->usepath
&& strcmp(u->usepath, d->module) == 0)
return d->module;
}
return NULL;
}
/*
* use_path — map a `use` alias (leaf bareword the user writes, `utf8`)
* to the full dotted import path it binds (`encoding.utf8`), for the
* module-qualified resolution and the codegen hint (M1 #22, §2.4). For
* single-level packages usepath == alias so the result is unchanged.
*
* The alias→path map is NOT file-global: two modules in the same
* concatenated unit may bind the same leaf alias to different paths
* (sha256's `import crypto.math` and strconv's `import math` both bind
* alias `math`). The import declared in the SAME module as the
* reference (`curmod`) is the authoritative one; preferring it closes
* the cross-module mis-resolution a first-match scan caused. Falls back
* to any matching alias when the referencing module has no own import
* (single-occurrence case, unchanged). Returns NULL if no such `use`.
*/
static const char *
use_path(Node *file, const char *curmod, const char *alias)
{
if (file == NULL || alias == NULL) return NULL;
const char *any = NULL;
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE || u->str == NULL
|| strcmp(u->str, alias) != 0)
continue;
const char *p = u->usepath ? u->usepath : u->str;
int same = (u->module == NULL) ? (curmod == NULL)
: (curmod != NULL && strcmp(u->module, curmod) == 0);
if (same)
return p;
if (any == NULL) any = p;
}
return any;
}
/*
* resolve_typedecl — resolve d's body into its installed TY_NAMED
* placeholder. Reached from check_file's typedecl pass AND on demand
* from resolve_typename (#62): the old file-order pass let any body
* referencing a LATER typedecl read a size-0 placeholder and bake it
* into alias sizes, union maxsz, struct field offsets and array
* element strides — decl-order-dependent layout. The resolving flag
* hands self-references the placeholder, exactly where the file-order
* pass did (sound for pointer fields, which never read the target's
* size). After a completed resolve `under` is never NULL (resolve_type
* returns ty_err/ty_void on failure), so the under==NULL demand gate
* cannot re-fire on a failed body.
*/
static void
resolve_typedecl(Checker *c, Node *d)
{
Type *t = d->type;
/* peel-ok: under!=NULL probes resolve state, not a dealias */
if (t == NULL || t->under != NULL || t->resolving) return;
t->resolving = 1;
const char *save = c->cur_mod;
c->cur_mod = decl_mod(c->file, d);
Type *under = resolve_type(c, d->lhs);
c->cur_mod = save;
/* Alias-root cycle (`type a = b; type b = a` / `type a = a`):
* checked BEFORE clearing the flag so self-aliases trip on their
* own in-progress mark. ty_err instead of the cyclic under keeps
* the table acyclic by construction — every later NAMED-chain
* chase loop (F1/F2's tichase family) stays terminating. */
if (circular_named(c, under, d->pos))
under = ty_err;
t->resolving = 0;
t->under = under; /* peel-ok: construction */
if (under) {
t->size = under->size;
t->align = under->align;
t->iserror = under->iserror;
}
}
/*
* src_imports — does the source file that contributed decl-module
* `modtag` carry `use <name>;` somewhere? With driver concatenation
* the combined N_FILE collects N_USE nodes from every contributing
* source; each carries its origin module tag on n->module. Filter
* by `modtag` so lib/log's `use fmt;` only colours decls whose
* d->module == "log", not lib/fmt's own decls.
*
* modtag == NULL → primary compilation unit's own use directives
* (N_USE nodes with module == NULL).
*/
static int
src_imports(Node *file, const char *modtag, const char *name)
{
if (file == NULL || name == NULL || name[0] == '\0') return 0;
for (Node *u = file->list; u; u = u->next) {
if (u->kind != N_USE) continue;
/* Skip self-imports: lib/fmt/fmttest.ww carries `use fmt;`
* even though its module tag is also "fmt"; that directive
* doesn't introduce a foreign module bareword and lib/fmt's
* own `fn bsprintf(fmt: str, ...)` is not a shadow of it. */
if (u->module && u->usepath && strcmp(u->module, u->usepath) == 0)
continue;
/* decl_mod normalises the raw `// MODULE:` tag back to NULL
* for primary-source N_USEs (the primary's own tag won't
* appear as a `use` import elsewhere). modtag matches the
* same convention from decl_mod called on the binding decl. */
const char *um = decl_mod(file, u);
if (modtag == NULL) {
if (um != NULL) continue;
} else {
if (um == NULL || strcmp(um, modtag) != 0) continue;
}
if (u->str && strcmp(u->str, name) == 0) return 1;
}
return 0;
}
/*
* check_module_shadow — refuse value bindings that shadow an
* in-scope imported module bareword. "Value names and module names
* are disjoint": a fn param / let / mcase binding named `fmt` while
* the declaring source carries `use fmt;` would silently miscompile
* any `fmt.X` body lookup through the shadow's value bits (the
* cstage cexpr N_DOT path resolves the inner ident as the shadow
* and emits CALL through its bytes — task #19).
*
* Scope:
* - Fires only for nested-scope binds (c->cur != c->top). Same-leaf
* top-level decls (`use foo; fn foo(...)`) are intentional and
* handled by the SK_USE→SK_X promotion path with use_alias=1.
* - Filters by the declaring source's own use directives. lib/fmt's
* `fn fprintf(fmt: str, ...)` is fine because lib/fmt doesn't
* import itself.
* - Walks every scope (not just innermost) so a deeper shadow that
* happens to mask the SK_USE entry can't suppress the check.
*/
static void
check_module_shadow(Checker *c, const char *name, Pos pos,
const char *kindstr)
{
if (name == NULL || name[0] == '\0') return;
if (c == NULL || c->cur == c->top) return;
int seen_use = 0;
for (Scope *s = c->cur; s; s = s->parent) {
Sym *r = scope_lookup_local(s, name);
if (r && (r->kind == SK_USE || r->use_alias)) {
seen_use = 1;
break;
}
}
if (!seen_use) return;
if (!src_imports(c->file, c->cur_mod, name)) return;
err(c, pos, "%s '%s' shadows imported module '%s'",
kindstr, name, name);
}
void
check_file(Checker *c, Node *file)
{
if (file == NULL || file->kind != N_FILE) return;
c->file = file;
/* #80: under -T, PREPEND the synth `use test;` BEFORE pass 1 so decl_mod
* (called per-decl during install) sees a matching `use test` when it
* keys lib/test's `run` — keying it under module="test", not "". A pure
* ORDER fix: the synth N_USE was appended AFTER install (below), too late
* for decl_mod, so `run` keyed under "" — leaving the synth `test.run`
* ty_err (the E1 bridge) and colliding with a user root `fn run` (also
* module=""). Decoupled from cgen: the symbol mangle keys off d->module
* (the //ww:module directive, mod_collect), NOT this scope keying — cgen
* already emits the correct CALL test.run. wwstage twin in check.ww. */
if (c->is_test) {
Node *usenode = newnode(c->a, N_USE, file->pos);
usenode->str = "test";
usenode->strlen = 4;
usenode->usepath = "test";
usenode->next = file->list;
file->list = usenode;
}
/* pass 1: install names (types first, then defs/fns).
* For self-referential types we install the named-type placeholder
* BEFORE resolving its body; the body may legitimately mention
* the type itself (`type stream = struct { read: fn(*stream)... }`).
* USE declarations are installed in this same step so dotted type
* references (`strconv.invalid`) resolve when typedecl bodies are
* walked in the next pass. */
for (Node *d = file->list; d; d = d->next) {
if (d->kind == N_USE) {
/* check-(c) self-import: a package may not import
* itself. Pure owner==leaf string compare, package-
* model-independent — sound under ww's filename-keyed
* file-inclusion imports. check-(a) unused and
* (b)/(d) membership DEFERRED to task #8 (filename-
* keyed pulls lack import->file->symbol provenance). */
const char *owner = decl_mod(file, d);
/* M1 #22: self-import ⟺ the imported path equals the
* use's own (owning) module path. Compares paths, not
* leaves, so nested packages are caught too. */
if (owner && owner[0] && d->usepath
&& strcmp(d->usepath, owner) == 0)
err(c, d->pos, "self-import: package "
"'%s' cannot import itself", owner);
Sym *prev = scope_lookup_local(c->cur, d->str);
if (prev != NULL) {
/* Self-import: the driver concatenates the
* imported module's source into the flat
* scope, so its top-level decls (types, fns,
* defs) shadow a same-named SK_USE. Mark
* the colliding sym as also-a-use so dotted
* qualifiers (`mod.x`) still resolve. */
prev->use_alias = 1;
} else {
scope_define(c->cur, d->str, SK_USE, NULL, d);
}
continue;
}
if (d->kind != N_TYPEDECL) continue;
Type *named = type_named(c->a, d->str, NULL);
Sym *prev = scope_lookup_local(c->cur, d->str);
const char *mod = decl_mod(file, d);
if (prev && prev->kind == SK_USE) {
/* `use mod; ... type mod = ...;` — promote the
* SK_USE to the type symbol but remember it was
* also a module name. */
prev->kind = SK_TYPE;
prev->type = named;
prev->decl = d;
prev->use_alias = 1;
if (mod && prev->mod == NULL) prev->mod = mod;
} else if (!scope_define_in_module(c->cur, d->str, mod,
SK_TYPE, named, d)) {
err(c, d->pos, "duplicate type %s", d->str);
}
d->type = named;
}
/* #141: bind def NAMES before resolving type bodies, so a struct
* field `[MAX]u8` whose dimension is a def-ref folds via
* eval_def_const (which reads decl->rhs) when resolve_typedecl
* walks the body below. The def's type is resolved in the
* decl loop further down; only the name->decl binding is needed
* here. A foldable stub carries type NULL until then. A duplicate
* (prev already bound non-USE) is left for that loop to diagnose. */
c->cur_mod = NULL;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_DEF) continue;
c->cur_mod = decl_mod(file, d);
const char *mod = decl_mod(file, d);
Sym *prev = scope_lookup_local(c->cur, d->str);
if (prev && prev->kind == SK_USE) {
prev->kind = SK_DEF; prev->decl = d;
prev->use_alias = 1;
if (mod && prev->mod == NULL) prev->mod = mod;
} else if (prev == NULL) {
scope_define_in_module(c->cur, d->str, mod,
SK_DEF, NULL, d);
}
}
c->cur_mod = NULL;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_TYPEDECL) continue;
resolve_typedecl(c, d);
}
c->cur_mod = NULL;
for (Node *d = file->list; d; d = d->next) {
c->cur_mod = decl_mod(file, d);
switch (d->kind) {
case N_USE:
/* already installed in pass 1; no-op here so the
* old fall-through doesn't re-define. */
break;
case N_DEF: {
Type *t = resolve_type(c, d->lhs);
d->type = t;
Sym *prev = scope_lookup_local(c->cur, d->str);
const char *mod = decl_mod(file, d);
if (prev && prev->kind == SK_DEF && prev->decl == d) {
/* #141: the foldable stub bound before type-body
* resolution; fill in its now-resolved type. */
prev->type = t;
} else if (prev && prev->kind == SK_USE) {
/* `use mod; ... def mod = ...;` — promote the
* SK_USE to the def symbol but remember it was
* also a module name so dotted qualifiers
* (`mod.x`) keep resolving via the N_DOT path's
* use_alias branch. Mirrors L1677. */
prev->kind = SK_DEF; prev->type = t; prev->decl = d;
prev->use_alias = 1;
if (mod && prev->mod == NULL) prev->mod = mod;
} else if (!scope_define_in_module(c->cur, d->str, mod,
SK_DEF, t, d))
err(c, d->pos, "duplicate def %s", d->str);
break;
}
case N_FNDECL: {
Type *t = build_fn_type(c, d);
d->type = t;
Sym *prev = scope_lookup_local(c->cur, d->str);
const char *mod = decl_mod(file, d);
if (prev && prev->kind == SK_USE) {
/* `use mod; ... fn mod(...) ...;` — promote
* but remember the module-alias so dotted
* qualifiers (`mod.x`) keep resolving. The
* lib/fnmatch case: `fn fnmatch(...)` shadows
* the SK_USE leaf, and without use_alias the
* dot-prefix path in resolve_typename loses
* the `fnmatch.flag` lookup. */
prev->kind = SK_FN; prev->type = t; prev->decl = d;
prev->use_alias = 1;
if (mod && prev->mod == NULL) prev->mod = mod;
} else if (!scope_define_in_module(c->cur, d->str, mod,
SK_FN, t, d))
err(c, d->pos, "duplicate fn %s", d->str);
break;
}
case N_LET: {
Type *t = d->lhs ? resolve_type(c, d->lhs) : NULL;
/* #108(b): a top-level `let x: opaque` is the same
* undefined-size footgun as a local one. */
if (t)
require_sized(c, t, d->pos, "a variable");
d->type = t;
if (d->str && d->str[0]) {
Sym *prev = scope_lookup_local(c->cur, d->str);
const char *mod = decl_mod(file, d);
if (prev && prev->kind == SK_USE) {
/* `use mod; ... let mod: T = ...;` —
* same promote-and-alias shape as the
* SK_DEF / SK_FN cases above. */
prev->kind = SK_VAR; prev->type = t;
prev->decl = d;
prev->use_alias = 1;
if (mod && prev->mod == NULL) prev->mod = mod;
} else if (!scope_define_in_module(c->cur,
d->str, mod, SK_VAR, t, d))
err(c, d->pos, "duplicate let %s",
d->str);
}
break;
}
default: break;
}
}
c->cur_mod = NULL;
/* Program-global uniqueness on the ENTRY `main`. M1 #32: the entry
* is the ROOT-unit main (imported==0) — it alone lowers to the bare
* `main` symbol w6l's _start calls. An IMPORTED package's `main`
* (imported==1) mangles on its path (`foo.bar.main`) and may coexist
* — closing the old dup-main collision by construction (#31). Two
* ROOT entries still collide on the bare symbol → reject loud (rule
* 7). Walks USER decls only — runs before the -T synth main is
* appended below — so a hosted-test build never false-counts. */
{
Node *firstmain = NULL;
for (Node *d = file->list; d; d = d->next) {
if (d->str == NULL || strcmp(d->str, "main") != 0)
continue;
if (d->kind != N_FNDECL && d->kind != N_LET
&& d->kind != N_DEF && d->kind != N_TYPEDECL)
continue;
if (d->imported)
continue;
if (firstmain == NULL) {
firstmain = d;
continue;
}
err(c, d->pos, "duplicate entry main: only one root "
"main may exist (#32)");
}
}
/*
* #15 @test harness — under `w6c -T`, synthesize the entry the
* driver would otherwise hand-wire. We sit at the seam between
* fn-install (pass 1, all names now in scope so the synth callees
* resolve) and fn-body-check (pass 2 below, which stamps the
* appended entry for free). This mirrors harec's checker-side
* is_test work — keep @test fns + suppress/own the hosted main
* (ref/harec/src/check.c:3941,4000) — NOT the build driver, which
* only flips a mode bit (ref/hare/cmd/hare/build.ha:46-49). cgen is
* untouched: the appended N_FNDECL rides the existing cgfn path, so
* byte-id holds at the gated choke point by construction.
*
* #17 RECORD-AND-CONTINUE (rob ruling 2026-06-10; drew-17-attest-spec
* §a): instead of straight-line `foo(); bar();` calls (which abort the
* whole run on the first failing @test — the old D3), synthesize a value
* table `[](str, *fn() void) = {("foo", &foo), ...}` and a single call
* to the lib/test runner. The runner forks per test and reads the
* child's wait-status, so abort/div0/SIGSEGV/nonzero each fail THAT test
* and the run proceeds (lib/test/run.ww). The table is the harec
* __test_array reduced to an in-source value table (D1: no linker
* section; D2: real symbols, no testfunc.%d rename). RETAINED reductions
* (user-ratified, reinstatable post-CSP): D4 no sort, no fnmatch filter
* (source/collection order; fnmatch is #17 commit-3); D5 no reflective
* file:line (the runner prints `name ... ok/FAIL` + a count summary).
* The table rides cgen's #117 slice-of-tuple-global DATA path; the
* `run` callee resolves bare against the auto-bundled lib/test (the
* synth runs post-pass-1, so lib/test's `run` sits in the same flat ""
* bucket as the @test fns — bare, like the @test calls themselves).
*/
if (c->is_test) {
Pos fp = file->pos;
/* (b) the synth entry OWNS `main` — loud-reject a user one. */
for (Node *d = file->list; d; d = d->next)
if (d->kind == N_FNDECL && d->str
&& strcmp(d->str, "main") == 0 && d->body != NULL)
err(c, d->pos, "test mode: main is synthesized "
"by -T; remove the explicit main");
/* #24(b): the synth table OWNS `__wwtests` — loud-reject a user
* decl of that name (twin of the `main` reservation above). A user
* __wwtests whose type happens to match run()'s [](str,*fn()void)
* param slips the general call-arg check but still silently shadows
* the synth table; reserve the NAME so the collision is loud
* regardless of type. Any decl kind. */
for (Node *d = file->list; d; d = d->next)
if (d->str && strcmp(d->str, "__wwtests") == 0)
err(c, d->pos, "test mode: __wwtests is reserved "
"by -T; rename the declaration");
/* (c) collect @test fns in file->list order; build one table row
* `("<name>", &<name>)` per validated @test fn. */
Node *rhead = NULL, *rtail = NULL;
int ntest = 0;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_FNDECL)
continue;
int istest = 0;
for (Node *at = d->attr; at; at = at->next)
if (at->str && strcmp(at->str, "test") == 0) {
istest = 1;
break;
}
if (!istest)
continue;
/* `fn f() void` parses the explicit `void` into d->lhs
* (parse.c:1329), so void-returning is lhs==NULL OR an
* N_TNAME "void" — not lhs==NULL alone. */
int retvoid = d->lhs == NULL
|| (d->lhs->kind == N_TNAME && d->lhs->str
&& strcmp(d->lhs->str, "void") == 0);
if (d->list != NULL || !retvoid) {
err(c, d->pos, "@test fn '%s' must be fn() void",
d->str);
continue;
}
Node *nm = newnode(c->a, N_STRLIT, fp);
nm->str = d->str;
nm->strlen = strlen(d->str);
Node *id = newnode(c->a, N_IDENT, fp);
id->str = d->str;
Node *amp = newnode(c->a, N_UN, fp);
amp->op = TK_AMP;
amp->lhs = id;
Node *row = newnode(c->a, N_TUPLE, fp);
row->list = nm;
nm->next = amp;
if (rhead == NULL) rhead = row;
else rtail->next = row;
rtail = row;
ntest++;
}
Node *body = newnode(c->a, N_BLOCK, fp);
Node *tab = NULL;
if (ntest == 0) {
/* no @test fns in this unit — exit 0, nothing to run. */
Node *ret = newnode(c->a, N_RETURN, fp);
ret->lhs = newnode(c->a, N_INTLIT, fp);
ret->lhs->uval = 0;
body->list = ret;
} else {
/* const __wwtests: [](str, *fn() void) = [rows...];
* cstage tuple-type elements chain raw via ->next (no
* N_TPARAM wrap; parse.c:341). */
Node *e0 = newnode(c->a, N_TNAME, fp);
e0->str = "str"; e0->strlen = 3;
Node *vfn = newnode(c->a, N_TFN, fp);
vfn->lhs = newnode(c->a, N_TNAME, fp);
vfn->lhs->str = "void"; vfn->lhs->strlen = 4;
Node *e1 = newnode(c->a, N_TPTR, fp);
e1->lhs = vfn;
Node *tup = newnode(c->a, N_TTUPLE, fp);
tup->list = e0; e0->next = e1;
Node *tsl = newnode(c->a, N_TSLICE, fp);
tsl->lhs = tup;
Node *arr = newnode(c->a, N_ARRLIT, fp);
arr->list = rhead;
tab = newnode(c->a, N_LET, fp);
tab->op = TK_CONST;
tab->str = "__wwtests";
tab->lhs = tsl;
tab->rhs = arr;
/* pass 1 already ran, so install the table's name now —
* pass 2 (below) cexprs its rhs and main references it. */
Type *tt = resolve_type(c, tsl);
tab->type = tt;
scope_define_in_module(c->cur, tab->str, NULL, SK_VAR,
tt, tab);
/* return test.run(__wwtests);
* QUALIFIED, not bare `run`: under the sep producer
* lib/test is a real imported package, so the call must
* carry the `test` module qualifier (N_DOT base ident
* `test`, member `run`). The combined path tags the
* auto-bundled lib/test `//ww:module test` too, so the
* qualified form resolves+mangles identically there
* (test.run either way) — byte-neutral. The base ident
* resolves through a synthetic N_USE injected below. */
Node *arg = newnode(c->a, N_IDENT, fp);
arg->str = "__wwtests";
Node *call = newnode(c->a, N_CALL, fp);
Node *dot = newnode(c->a, N_DOT, fp);
dot->lhs = newnode(c->a, N_IDENT, fp);
dot->lhs->str = "test";
dot->str = "run";
call->lhs = dot;
call->list = arg;
Node *ret = newnode(c->a, N_RETURN, fp);
ret->lhs = call;
body->list = ret;
/* #80: the synth `use test;` (the N_DOT base qualifier +
* the use_path source mapping `test`→its path) is now
* PREPENDED before pass 1 at the top of check_file, so
* decl_mod keys lib/test's `run` under "test" and the synth
* `test.run` type-resolves. Pass 1's N_USE arm installs it
* (SK_USE). */
}
Node *m = newnode(c->a, N_FNDECL, fp);
m->str = "main";
m->export = 1;
m->lhs = newnode(c->a, N_TNAME, fp);
m->lhs->str = "i32";
m->body = body;
m->type = build_fn_type(c, m);
/* pass 1 already ran, so the install loop never stamped m's
* type; set it explicitly (pass 2 below reads d->type).
* Append the table const (if any) then main to file->list. */
Node *tl = file->list;
if (tl == NULL) {
file->list = tab ? tab : m;
if (tab) tab->next = m;
} else {
while (tl->next) tl = tl->next;
if (tab) { tl->next = tab; tab->next = m; }
else tl->next = m;
}
}
/* pass 2: check def initialisers and fn bodies */
for (Node *d = file->list; d; d = d->next) {
c->cur_mod = decl_mod(file, d);
switch (d->kind) {
case N_DEF: {
if (d->rhs) {
Type *rt = cexpr(c, d->rhs);
/* #11: `def xs: [_]T = arrlit;` — infer the length
* from the initialiser, the def twin of the module
* N_LET path below. pass-1 (N_DEF above) installed
* the SK_DEF Sym + d->type with the alen=0 sentinel;
* an indexed read resolves the def through its Sym,
* so re-point BOTH d->type (feeds cgen's emit_defs
* DATA row + defarray registry) and the Sym (feeds
* the N_INDEX / `.len` type read). #7 only wired the
* let decl path; the def path silently stayed length
* 0 (no DATA, garbage reads). Run before the
* assignability check so arrlit_init_fits sees the
* inferred length. */
if (d->type && d->type->kind == TY_ARRAY
&& d->type->alen == 0
&& is_infer_arr(d->lhs)) {
Type *iu = type_chase_named(rt);
if (iu && iu->kind == TY_ARRAY) {
d->type = type_array(c->a,
d->type->sub, iu->alen);
Sym *s = scope_lookup_local(c->cur,
d->str);
if (s) s->type = d->type;
} else
err(c, d->pos, "[_]T needs an "
"array-literal initialiser");
}
/* #88: fold sibling/imported def refs, casts, and
* arithmetic to a constant. litfold (plain literal
* leaf) is already typed UNTYPED_INT by cexpr;
* constfold (#88, gated on litfold missing) covers
* the richer shapes and is the one we stamp, so
* existing literal/unary defs keep their rhs node
* and the emitted bytes stay byte-identical. */
u64 dv;
int litfold = rt != ty_err
&& fold_int_literal(d->rhs, &dv);
int constfold = rt != ty_err && !litfold
&& eval_def_const(c, d->rhs, &dv, 0);
/* #113: a def-ref that folds to a compile-time
* constant (a def ref like `def INT_MIN: int =
* I32_MIN`) carries its referent's concrete declared
* type (i32), not UNTYPED_INT, so the assignability
* check below rejected i32 -> int. In a def
* initializer the rhs is a flexible constant, so
* re-flexibilize the folded value to UNTYPED_INT here
* when it fits the declared integer target — emulating
* Hare's flexible-constant promotion (ICONST ->
* promote_flexible/lower_flexible, range-checked:
* ref/harec/src/types.c:860, reached via the
* STORAGE_ICONST assignability case at :1012/:1019).
* ww has no ICONST flexible-range type; def_cast_fits
* is the range check that keeps a genuine out-of-range
* value a loud "not assignable" error, never a silent
* truncation (rule 7). This is strictly the const
* subset: the general CONCRETE (non-const) integer
* widening Hare does at ref/harec/src/types.c:1021-1037
* is intentionally stricter in ww, see #115. */
Type *art = rt;
if (constfold && d->type && type_isint(d->type)
&& type_isint(rt) && !type_isuntyped(rt)
&& def_cast_fits(d->type, dv))
art = ty_untyped_int;
if (d->type && rt != ty_err && d->type != ty_err
&& !type_assignable(d->type, art)
&& !arrlit_init_fits(c, d->type, d->rhs))
err(c, d->pos, "def %s init %s not assignable to %s",
d->str, type_name(c->a, rt),
type_name(c->a, d->type));
if (constfold)
stamp_intlit(c, d->rhs, dv);
}
break;
}
case N_FNDECL: {
if (d->body == NULL) break; /* extern decl */
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
Type *fnt = d->type;
for (Tparam *p = fnt->params; p; p = p->next) {
if (p->name && p->name[0]) {
check_module_shadow(c, p->name,
d->pos, "param");
if (scope_define(c->cur, p->name,
SK_PARAM, p->type, d) == NULL)
err(c, d->pos,
"param '%s' redeclared",
p->name);
}
}
Type *prev = c->ret;
c->ret = fnt->ret;
cstmt(c, d->body);
c->ret = prev;
c->cur = saved;
break;
}
case N_LET: {
if (d->rhs) {
Type *rt = cexpr(c, d->rhs);
/* `let xs: [_]T = arrlit;` at module level — infer the
* length from the initialiser, the same patch clet
* applies for a local let (#7). pass-1.5 resolve_type
* left alen=0 as the sentinel; patching d->type feeds
* cgen's letvars registration (lv->type = d->type),
* which both lays the full-length DATA row and reads
* the right `.len`. */
if (d->type && d->type->kind == TY_ARRAY
&& d->type->alen == 0
&& is_infer_arr(d->lhs)) {
Type *iu = type_chase_named(rt);
if (iu && iu->kind == TY_ARRAY) {
d->type = type_array(c->a,
d->type->sub, iu->alen);
/* The Sym installed in pass-1.5 still
* carries the alen=0 sentinel; a later
* `x.len` resolves `x` through the Sym
* (its type stamps n->lhs->type, which
* cgen reads as u->alen). Re-point it at
* the inferred-length type too. */
Sym *s = scope_lookup_local(c->cur,
d->str);
if (s) s->type = d->type;
} else
err(c, d->pos, "[_]T needs an "
"array-literal initialiser");
}
if (d->type == NULL) {
d->type = type_default(rt);
/* #150 bug-1: pass-1 installed the Sym with
* the annotation-less NULL type; without
* repointing it, every downstream N_IDENT
* read of this inferred module-global
* resolves nil (`g.a`/`take(g)` → <nil>).
* The general-inferred twin of the #11
* [_]-array Sym repoint above. */
Sym *s = scope_lookup_local(c->cur,
d->str);
if (s) s->type = d->type;
}
if (d->type && rt != ty_err && d->type != ty_err
&& !type_assignable(d->type, rt)
&& !arrlit_init_fits(c, d->type, d->rhs))
err(c, d->pos, "let %s init not assignable",
d->str);
/* #133: const-fold a const-EXPR rhs (N_BIN /
* unary-over-N_BIN / def-ref) to a literal so
* cgen's literal-only DATA emitter lays the row,
* mirroring the N_DEF arm above. GATED on the
* plain literal fold missing first (existing
* literal/unary-literal lets keep their node,
* byte-identical) AND the const-fold succeeding
* (a str/struct/slice/call/runtime-operand rhs
* returns 0 silently and is left untouched).
* Stamp LAST — the assignability check above
* consumes the pre-stamp cexpr type. */
u64 dv;
if (d->rhs && !fold_int_literal(d->rhs, &dv)
&& eval_def_const(c, d->rhs, &dv, 0))
stamp_intlit(c, d->rhs, dv);
} else if (d->type && d->type->kind == TY_ARRAY
&& d->type->alen == 0 && is_infer_arr(d->lhs)) {
/* `let x: [_]T;` — no initialiser, length can't be
* inferred (rule 7, #7). An explicit `[0]T;` with no
* init is a valid empty array, not this error. */
err(c, d->pos, "[_]T needs an array-literal "
"initialiser");
}
break;
}
default: break;
}
}
c->cur_mod = NULL;
/*
* #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is
* fully checked above — pass 2 walked its body like every fn — but
* is NOT emitted in a non-test build. harec skips append_decl for
* FN_TEST && !is_test, so the fn never reaches unit->declarations
* (the list codegen walks); the body is still type-checked, only the
* emission is dropped. ww shares one file->list across check + cgen
* (no separate checked-decl list, project_hare_ast_no_result), so we
* splice the already-checked @test fns out here, after pass 2 — they
* stay checked, never reach cg_file. The -T path is untouched: its
* synth main calls the @test fns, so they must remain. Prereq for
* in-package @test colocation (#9). Twin: selfhost/cmd/wcc/check.ww.
*/
if (!c->is_test) {
Node *prev = NULL;
for (Node *d = file->list; d; ) {
int istest = 0;
if (d->kind == N_FNDECL)
for (Node *at = d->attr; at; at = at->next)
if (at->str
&& strcmp(at->str, "test") == 0) {
istest = 1;
break;
}
Node *nx = d->next;
if (istest) {
if (prev == NULL) file->list = nx;
else prev->next = nx;
} else
prev = d;
d = nx;
}
}
}