Files
ww/cmd/wcc/check.c
Hojun-Cho f4970d886c wcc: opaque use-guards — reject every unsized use (incl tuple/tagged, recursive) (#108)
#108 sub-fold (b): close the footgun #108(a) opened. opaque is abstract
and UNSIZED (size = align = SIZE_UNDEFINED = (u64)-1), legal only behind
indirection. Without guards a bare use would fabricate a (u64)-1-byte
slot — a silent miscompile (rule 7). opaque is illegal by-value in FOUR
aggregate positions (array element, struct field, tuple member, tagged-
union variant) + as a bare value, under size/align, and as a []opaque
element-index. LOUD guards, mirroring harec's scattered `size ==
SIZE_UNDEFINED` checks:

  1. bare value/local/param/return-by-value  (check.c clet, build_fn_type,
     top-level let; harec check.c:1524, :3931)
  2. opaque struct field                      (resolve_type N_TSTRUCT)
  3. [N]opaque array element                  (resolve_type N_TARRAY)
  3t. opaque tuple member                     (resolve_type N_TTUPLE;
      harec type_store.c:1147)
  3u. opaque tagged-union variant             (resolve_type N_TTAGGED;
      harec type_store.c:449)
  4. size(opaque) / align(opaque)             (size/align fold;
     harec check.c:2720)
  5. indexing []opaque                        (N_INDEX; harec check.c:384)

Detection is via the SIZE_UNDEFINED sentinel the guard consults, so the
sized forms `*opaque` (8B) and `[]opaque` (24B header) pass untouched.

Rule-10 per-guard stage placement:
  - Guards 1/2/3/3t/3u/5 are CSTAGE-ONLY. The wwstage check.ww is an
    AST-level approximation with no binding-size computation (g1) and no
    type-decl field/element/member validation walk (g2/g3/3t/3u); its
    N_INDEX indexresult returns the element type without consulting its
    size and defers invalid-index rejection to the cstage (g5). Same
    cstage-only neg-case precedent as 712_redecl / 708_param_shadow_mod.
  - Guard 4 is BOTH-STAGES. The wwstage HAS the size()/align() fold
    (astsize/astalign would otherwise fold opaque to a bogus 0 — a silent
    miscompile); twinned via astunsized + deffolderr. Because the wwstage
    has NO per-construction guards, its fold alone must catch every
    opaque-containing type: astunsized is RECURSIVE — a type is unsized
    iff it is opaque OR an aggregate (array/struct/tuple/tagged) with a
    recursively-unsized member. This both reaches the tuple/tagged folds
    AND closes the leaf-only size([4]opaque)/size(struct{x:opaque})→0
    leak. The cstage size/align guard stays leaf — the cstage rejects
    unsized aggregates at construction, so its fold only ever sees a leaf.

opaque is unused by the bootstrap, so every guard is inert on the
selfhost corpus — 990-997 stay byte-identical. Regenerates the w6c/wwdump
combined.ww (check.ww embed). New compile-fail probe 961_opaque_guards
(14 build-fails rows incl tuple/tagged/nested + 2 *opaque/[]opaque
positive controls); 960 positive probe unchanged.
2026-05-26 09:52:13 +09:00

2325 lines
78 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 Type *
resolve_typename(Checker *c, Node *n)
{
const char *nm = n->str;
Type *bi = lookup_builtin(nm);
if (bi) return bi;
Sym *s = scope_lookup_prefer(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))
s = scope_lookup_in_module(c->cur, head,
dot + 1);
}
}
if (s == NULL || s->kind != SK_TYPE)
return err(c, n->pos, "unknown type '%s'", nm);
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;
}
/* 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;
}
/* 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;
Sym *s = scope_lookup_in_module(c->cur, n->lhs->str, 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;
}
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;
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 {
err(c, n->pos, "array length must be an integer literal");
}
Type *elem = resolve_type(c, n->lhs);
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);
/* #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;
if (tp->type) sz += tp->type->size;
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;
/* #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 && vt && vt->kind == TY_NAMED
? vt->under : 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);
/* #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 = (ft && ft->kind == TY_NAMED) ? ft->under : 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;
return err(c, p, "operands have differing types %s and %s",
type_name(c->a, a), type_name(c->a, b));
}
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:
if (!(l == ty_bool || l == ty_untyped_bool || l == ty_err))
err(c, n->pos, "left of %s is not bool", tokname(n->op));
if (!(r == ty_bool || r == 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:
if (!(t == ty_bool || t == 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;
if (t->kind != TY_PTR)
return err(c, n->pos, "cannot deref non-pointer %s",
type_name(c->a, t));
return t->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 = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
if (bu && bu->kind == TY_PTR) bu = bu->sub;
if (bu && bu->kind == TY_NAMED) bu = bu->under;
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. */
Sym *fs = scope_lookup_in_module(c->cur,
n->lhs->str, 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 = (ms->type->kind == TY_NAMED)
? ms->type->under : 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 = (base->kind == TY_NAMED) ? base->under : base;
if (u && u->kind == TY_PTR) u = u->sub;
if (u && u->kind == TY_NAMED) u = u->under;
/* 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;
if (strcmp(n->str, "cap") == 0) return n->type = ty_i32;
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 = (base->kind == TY_NAMED) ? base->under : 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)
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 = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
if (u && u->kind == TY_PTR) u = u->sub;
if (u && u->kind == TY_NAMED) u = u->under;
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;
}
/* 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);
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 = (ft->kind == TY_NAMED) ? ft->under : 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))
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)
err(c, a->pos, "argument type %s not assignable to %s",
type_name(c->a, at), type_name(c->a, p->type));
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))
err(c, n->pos, "cannot assign %s to %s",
type_name(c->a, r), type_name(c->a, l));
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 = (t && t->kind == TY_NAMED) ? t->under : 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))
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_i32;
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 = (base && base->kind == TY_NAMED) ? base->under : 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;
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 = (t && t->kind == TY_NAMED) ? t->under : 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 = (st && st->kind == TY_NAMED) ? st->under : 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 = (t && t->kind == TY_NAMED) ? t->under : 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 = (vt && vt->kind == TY_NAMED) ? vt->under : 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 = (t && t->kind == TY_NAMED) ? t->under : 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;
for (Tparam *p = u->params; p; p = p->next)
if (tagged_is_error_variant(u, p->type)) {
has_errors = 1; break;
}
if (n->kind == N_TRYPROP && has_errors) {
Type *r = c->ret;
Type *ru = (r && r->kind == TY_NAMED) ? r->under : 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). */
Type *t = newtype(c->a, TY_TUPLE);
Tparam *head = NULL, *tail = NULL;
for (Node *e = n->list; e; e = e->next) {
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->type = cexpr(c, e);
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
}
t->params = head;
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;
if (n->rhs) initt = cexpr(c, n->rhs);
/* `let xs: [_]T = arrlit;` — fill in the inferred length from the
* initialiser. `resolve_type` left alen=0 as a sentinel. */
if (declared && declared->kind == TY_ARRAY && declared->alen == 0 &&
initt) {
Type *iu = (initt->kind == TY_NAMED) ? initt->under : 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. */
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;
}
}
/* #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))
err(c, n->pos, "init %s not assignable to declared %s",
type_name(c->a, initt), type_name(c->a, declared));
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))
err(c, n->pos, "return %s not assignable to %s",
type_name(c->a, rt), type_name(c->a, c->ret));
break;
}
case N_IF: {
Type *ct = cexpr(c, n->cond);
if (ct != ty_err && ct != ty_bool && ct != 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 = (st && st->kind == TY_NAMED) ? st->under : 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 = (elem && elem->kind == TY_NAMED) ? elem->under : 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);
if (ct != ty_err && ct != ty_bool && ct != 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;
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) {
if (u->kind == N_USE && u->str
&& strcmp(u->str, d->module) == 0)
return d->module;
}
return NULL;
}
/*
* 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->str && strcmp(u->module, u->str) == 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;
/* 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) {
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;
}
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_TYPEDECL) continue;
c->cur_mod = decl_mod(file, d);
Type *under = resolve_type(c, d->lhs);
d->type->under = under;
if (under) {
d->type->size = under->size;
d->type->align = under->align;
d->type->iserror = under->iserror;
}
}
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_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;
/* 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);
/* #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))
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);
if (d->type == NULL) d->type = type_default(rt);
if (d->type && rt != ty_err && d->type != ty_err
&& !type_assignable(d->type, rt))
err(c, d->pos, "let %s init not assignable",
d->str);
}
break;
}
default: break;
}
}
c->cur_mod = NULL;
}