/* * 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 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, int source, const char *alias); static const char *find_use_path(Node *file, const char *curmod, int source, const char *alias, int mark); static int src_imports(Node *file, const char *modtag, int source, const char *name); static Sym *lookup_visible(Checker *c, const char *name); static Sym *lookup_visible_type(Checker *c, const char *name); 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 = lookup_visible_type(c, 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) { size_t hl = (size_t)(dot - nm); char *head = astrndup(c->a, 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, c->cur_source, head); if (mk != NULL) s = scope_lookup_in_module(c->cur, mk, dot + 1); if (s && s->decl && s->decl->imported && !s->decl->export) return err(c, n->pos, "package '%s' has no exported declaration '%s'", head, 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. */ /* A missing underlying type is the unresolved-state marker. */ 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); /* a yield inside a switch arm yields from the enclosing MATCH * (switch is a statement, not a yield scope) — invisible here, * the match typed void and the yielded value was dropped. */ if (body->kind == N_SWITCH) { for (Node *cs = body->list; cs; cs = cs->next) { Type *t = match_yield_type(cs->body); if (t) return t; } return NULL; } 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 = lookup_visible(c, 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] 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 = lookup_visible(c, 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, c->cur_source, n->lhs->str); if (mk == NULL) return 0; 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; } } int check_eval_const(Checker *c, Node *n, const char *owner, int source, u64 *out) { const char *saved = c->cur_mod; int savesource = c->cur_source; c->cur_mod = owner; c->cur_source = source; int ok = eval_def_const(c, n, out, 0); c->cur_mod = saved; c->cur_source = savesource; return ok; } /* 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; /* Each scalar occupies one SysV 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; /* packed: no inter-field padding (harec * type_store.c:206-213); align still tracks the * max field align below. */ if (!n->packed) 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->packed = n->packed; t->align = maxalign; /* packed: skip the trailing pad-to-align (harec * type_store.c:886 `!packed`); align value unchanged. */ t->size = n->packed ? off : ((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"); } } 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 the r949_errtype_* * fixtures 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 / #120: 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, * cgen.c:4163) never fires — 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 (ref/harec/src/check.c:148): a flexible fconst adapts to * the hinted type exactly as a flexible iconst does. 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. * * #120 broadens the reach (was let-init / return only): descend the * lower_implicit_cast operand shapes so every untyped float LEAF in an f32 * context gets the stamp — a unary ± / paren-cast wrapper, both operands of an * arith binop (harec lowers a binop's operands to its result type, * ref/harec/src/check.c:1347-1348; this is the only path that reaches a * literal-on-BOTH-sides `2.0 + 3.0` under an f32 target — narrowing per-leaf, * never via an f64 intermediate that would double-round), and each element of * an array literal against the array's element type. f64-only targets recurse * harmlessly (the leaf gate stays TY_F32). The (B) sibling-lowering of a * comparison's untyped operand — which has no f32 target above (its result is * bool) — lives in cbinop. */ 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) return; switch (n->kind) { case N_UN: if (n->op == TK_MINUS || n->op == TK_PLUS) coerce_floatlit(n->lhs, target); return; case N_CAST: coerce_floatlit(n->lhs, target); return; case N_BIN: switch (n->op) { case TK_PLUS: case TK_MINUS: case TK_STAR: case TK_SLASH: coerce_floatlit(n->lhs, target); coerce_floatlit(n->rhs, target); /* harec lowers the binop's RESULT to the hint too, not * only its operands. A literal-on-both-sides binop's node * stays ty_untyped_float (unify_arith of two untyped), and * cstage's arith cgen keys the op/spill width on the BINOP * node (cgen.c:4944 node_isf32(n)) — so without stamping the * node f32 it emits ADDSD over the f32-narrowed operands * (garbage), diverging from wwstage (which keys on operands). * Stamping the node converges both stages on ADDSS. */ if (u->kind == TY_F32 && n->type == ty_untyped_float) n->type = ty_f32; break; default: break; } return; case N_ARRLIT: if (u->kind == TY_ARRAY) for (Node *e = n->list; e; e = e->next) coerce_floatlit(e, u->sub); return; case N_FLOATLIT: if (u->kind == TY_F32 && n->type == ty_untyped_float) n->type = ty_f32; return; default: return; } } /* 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); /* #13: the borrow target may be a SLICE success variant of a tagged- * union return — the same no-outliving-backing dangle as a bare slice, * but it slips the TY_SLICE gate (c->ret chases to TY_TAGGED). Chase to * the assignable slice variant so the reject sees through the union. An * array-typed variant is the separate #5/#60 reject upstream; full * non-let support (an outliving backing) is #33. */ if (du && du->kind == TY_TAGGED) { for (Tparam *p = du->params; p; p = p->next) { Type *pu = type_chase_named(p->type); if (pu && pu->kind == TY_SLICE && type_assignable(p->type, expr->type)) { du = pu; break; } } } 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); /* #17: the borrow target may be the SLICE success variant of a tagged * union (`return/assign/let/f(arr)` into `([]T|e)`). Chase du to that * variant so an array VARIABLE lowers to a full slice exactly as a * bare []T dst does — the existing slice cgen then builds the full * {ptr,len,cap} header and the union widen's slice arm wraps it, * closing the silent len/cap drop. Mirrors #13's reject_arrlit_borrow * TY_TAGGED chase (an array LITERAL has no outliving backing and is * rejected there first; a variable has storage, so this borrow is * legal). An array-typed variant is the separate #5/#60 reject. */ if (du && du->kind == TY_TAGGED && su && su->kind == TY_ARRAY) { for (Tparam *p = du->params; p; p = p->next) { Type *pu = type_chase_named(p->type); if (pu && pu->kind == TY_SLICE && type_eq(pu->sub, su->sub)) { du = pu; break; } } } 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); /* #120 (B): a binop/compare with one f32 operand lowers an untyped- * float peer to f32 — harec unifies both operands to the operand type * (ref/harec/src/check.c:1347-1348). A comparison's result is bool, so * no f32 target is above its operands and this sibling is their only * lowering path. Each coerce is inert unless the PEER type resolves f32 * and this operand carries an untyped float leaf, so a both-untyped pair * (`4.0 == 5.0`, no f32 context) stays f64. Held byte-id-symmetric with * wwstage binoptype (two unconditional, internally-gated coerce calls). */ coerce_floatlit(n->rhs, l); coerce_floatlit(n->lhs, r); 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"); /* % is integer-only (harec check.c binarithm BIN_MODULO): * floats have no SSE modulo lowering, so an admitted float % * fell through cgen half-lowered (cs!=ww divergence). */ if (n->op == TK_PERCENT && (!type_isint(l) || !type_isint(r))) return err(c, n->pos, "modulo on non-integer 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 = lookup_visible(c, 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 = lookup_visible(c, 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, c->cur_source, n->lhs->str); if (mk == NULL) { if (ms->kind == SK_USE) return n->type = err(c, n->pos, "package '%s' is not directly imported", n->lhs->str); } else { Sym *fs = scope_lookup_in_module(c->cur, mk, n->str); if (fs && fs->decl && fs->decl->imported && !fs->decl->export && !n->imported) return n->type = err(c, n->pos, "package '%s' has no exported declaration '%s'", n->lhs->str, n->str); if (fs) return n->type = fs->type; /* A bare `w6c -T` intentionally leaves the * compiler-generated support.run hook external; the * ordinary driver supplies lib/test. This is the only * missing direct member that is not a package export * error. */ if (c->sep_mode && ms->kind == SK_USE && n != c->synth_test_run) return n->type = err(c, n->pos, "package '%s' has no exported declaration '%s'", n->lhs->str, n->str); if (ms->kind == SK_USE) 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 = lookup_visible(c, 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 && lookup_visible(c, "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 && lookup_visible(c, "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); /* Autodereference a pointer callee to its fn type before the * gate (harec check_autodereference → type_dereference, * ref/harec/src/check.c:1566, types.c:13). ONE level only: the * #181-cgen path lowers an indirect call by using the callee * VALUE as the target (CALL AX) — the fn address for a single * `*fn`, but only the *address of* the fn-ptr for `**fn`. So a * deref-less `**fn` call drops a `MOVQ (AX),AX` and miscompiles * on BOTH stages (byte-id-blind, returns garbage). Stay loud on * `**fn` (rule 7) rather than match wwstage's loop-accept * (selfhost/cmd/wcc/check.ww:3763, which over-accepts past * #181-cgen); multi-level fn-ptr autoderef is deferred (#181). */ if (u && u->kind == TY_PTR) u = type_chase_named(u->sub); 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)); /* #120: `f(1.0)` narrows the arg literal to the param's f32. */ coerce_floatlit(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; } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str) { Sym *s = lookup_visible(c, 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)); /* Compound ops carry their binary op's operand class (harec * check.c binarithm): += -= *= /= need numeric operands, * %= modulo-integer, the bitwise/shift five integer. The * assignability check above cannot see the op, so `f %= x` * and `s += "x"` passed and cgen's fallback plain-stored the * rhs, silently dropping the operation. */ if (n->op != TK_ASSIGN && l != ty_err && r != ty_err) { switch (n->op) { case TK_PLUSEQ: case TK_MINUSEQ: case TK_STAREQ: case TK_SLASHEQ: if (!type_isnum(l) || !type_isnum(r)) err(c, n->pos, "arithmetic on " "non-numeric type"); break; case TK_PERCENTEQ: if (!type_isint(l) || !type_isint(r)) err(c, n->pos, "modulo on " "non-integer type"); break; default: if (!type_isint(l) || !type_isint(r)) err(c, n->pos, "bitwise on " "non-integer type"); break; } } /* #120: `w = 1.0` narrows the rhs literal to the lvalue's f32. */ coerce_floatlit(n->rhs, 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 = lookup_visible(c, 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)); /* #120: `S{ f: 1.0 }` narrows the init to the field's f32. */ if (match != NULL) coerce_floatlit(f->lhs, 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); } } c->matcharms++; cstmt(c, cs->body); c->matcharms--; 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; /* Each scalar occupies one SysV 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); } } 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); /* #99 alias transparency: a NAMED tuple alias rhs * (`type pair = (i64,i64)`) destructures like its base. */ Type *ru = type_chase_named(rt); Type *u = (ru && ru->kind == TY_TUPLE) ? ru : 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, declared), type_name(c->a, elem)); 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); /* #99 alias transparency — the N_MLET chase's twin. */ Type *ru = type_chase_named(rt); Type *u = (ru && ru->kind == TY_TUPLE) ? ru : 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: /* break/continue get the c->loops gate; a stray yield * outside any match arm reached cgen unchecked and its * value silently vanished. */ if (c->matcharms == 0) err(c, n->pos, "yield outside match"); 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); } } 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 */ c->is_test_package = 0; c->test_module = "test"; c->test_target = NULL; 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; /* Separate-compilation interfaces already carry their canonical owner. * Visibility is checked independently against the referencing source's * import binding; never erase semantic ownership for a transitive fact. */ if (d->imported) return d->module; 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 source-file default qualifier (the imported package's * declared name) to the full canonical import path it binds, for * module-qualified resolution and the codegen hint (M1 #22, §2.4). * * The qualifier→path map is source-file local: two files in the same * concatenated unit may bind the same declared name to different paths. The * import with the reference's source ID and owner is authoritative, closing * both cross-file mis-resolution and accidental transitive visibility. * Returns NULL if the referencing package has no such `use`. */ static const char * find_use_path(Node *file, const char *curmod, int source, const char *alias, int mark) { if (file == NULL || alias == NULL) return NULL; /* Legacy inline multi-package units may spell a package's own * declarations as `pkg.member`. That is self-qualification, not an * imported namespace; preserve it without reopening transitive lookup. */ if (source == 0 && curmod != NULL) { const char *dot = strrchr(curmod, '.'); const char *leaf = dot ? dot + 1 : curmod; if (strcmp(alias, leaf) == 0) return curmod; } for (Node *u = file->list; u; u = u->next) { if (u->kind != N_USE || u->str == NULL || u->sourceid != source || strcmp(u->str, alias) != 0) continue; const char *p = u->usepath ? u->usepath : u->str; const char *um = decl_mod(file, u); int same = (um == NULL) ? (curmod == NULL) : (curmod != NULL && strcmp(um, curmod) == 0); if (same) { if (mark) u->used = 1; return p; } } return NULL; } static const char * use_path(Node *file, const char *curmod, int source, const char *alias) { return find_use_path(file, curmod, source, alias, 1); } /* * 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; /* A present underlying type is the resolved-state marker. */ if (t == NULL || t->under != NULL || t->resolving) return; t->resolving = 1; const char *save = c->cur_mod; int savesource = c->cur_source; c->cur_mod = decl_mod(c->file, d); c->cur_source = d->sourceid; Type *under = resolve_type(c, d->lhs); c->cur_mod = save; c->cur_source = savesource; /* 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; 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 ;` 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, int source, 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 || u->sourceid != source) continue; /* Skip self-imports: lib/fmt/fmt_test.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; } static Sym * lookup_visible(Checker *c, const char *name) { Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, name); /* A real source declaration shadows a decl-less pseudo-builtin. Keep * the builtin as fallback while checking direct imported declarations. */ Sym *builtin = NULL; if (s != NULL) { if (s->decl != NULL) return s; builtin = s; } /* Flat scope installation may coalesce equal qualifiers from distinct * files. The source-owned binding is authoritative, but a bare mention is * not usage; only the enclosing qualified lookup may mark it. */ if (find_use_path(c->file, c->cur_mod, c->cur_source, name, 0) != NULL) { for (Scope *p = c->cur; p; p = p->parent) for (Sym *b = p->first; b; b = b->next) if (strcmp(b->name, name) == 0 && (b->kind == SK_USE || b->use_alias)) return b; } return builtin; } static Sym * lookup_visible_type(Checker *c, const char *name) { Sym *s = scope_lookup_type(c->cur, c->cur_mod, name); if (s != NULL) return s; return NULL; } /* * 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, c->cur_source, name)) return; err(c, pos, "%s '%s' shadows imported module '%s'", kindstr, name, name); } /* Self-contained `.wwi` files can carry the same origin-owned type/const * fact through two direct dependencies (a diamond), or alongside a direct * import of that origin. In strict package mode those compiler-generated * declarations describe one canonical package symbol: reuse the first exact * (module, kind, name) binding so nominal Type pointer identity is preserved. * Raw w6c mode deliberately keeps the historical duplicate diagnostics. */ static Sym * same_import_fact(Checker *c, Node *d, const char *mod, Skind kind) { if (!c->sep_mode || d == NULL || !d->imported || mod == NULL || mod[0] == '\0') return NULL; if (kind != SK_TYPE && kind != SK_DEF) return NULL; /* Compiler-private nominal facts may recur through a self-contained * export diamond. Private defs are never valid closure facts. */ if (!d->export && kind != SK_TYPE) return NULL; Sym *s = scope_lookup_in_module(c->cur, mod, d->str); if (s == NULL || s->kind != kind || s->decl == NULL || s->decl == d || !s->decl->imported || (!s->decl->export && kind != SK_TYPE)) return NULL; return s; } static int top_decl_kind(Node *d) { return d != NULL && (d->kind == N_TYPEDECL || d->kind == N_DEF || d->kind == N_FNDECL || d->kind == N_LET); } /* Import usage is a property of the file-local qualifier occurrence. Record * qualified syntax before resolving declaration bodies so import diagnostics * retain production Go's source order without making a failed bare lookup a * use. WW already rejects lexical bindings that shadow an import qualifier. */ static void mark_import_uses_node(Checker *c, Node *n, const char *owner, int source) { if (n == NULL) return; if (n->kind == N_TNAME && n->str != NULL) { const char *dot = strrchr(n->str, '.'); if (dot != NULL) { char *head = astrndup(c->a, n->str, (size_t)(dot - n->str)); (void)find_use_path(c->file, owner, source, head, 1); } } else if (n->kind == N_DOT && n->lhs != NULL && n->lhs->kind == N_IDENT && n->lhs->str != NULL) { (void)find_use_path(c->file, owner, source, n->lhs->str, 1); } for (Node *p = n->attr; p; p = p->next) mark_import_uses_node(c, p, owner, source); mark_import_uses_node(c, n->lhs, owner, source); mark_import_uses_node(c, n->rhs, owner, source); mark_import_uses_node(c, n->cond, owner, source); mark_import_uses_node(c, n->body, owner, source); mark_import_uses_node(c, n->els, owner, source); for (Node *p = n->list; p; p = p->next) mark_import_uses_node(c, p, owner, source); } static void mark_import_uses(Checker *c, Node *file) { for (Node *d = file->list; d; d = d->next) { if (d->kind == N_USE) continue; mark_import_uses_node(c, d, decl_mod(file, d), d->sourceid); } } static void check_import_alt(Node *d, const char *name) { FILE *f = errout ? errout : stderr; fprintf(f, "\t%s:%d:%d: other declaration of %s\n", d->pos.file ? d->pos.file : "?", d->pos.line, d->pos.col, name); } /* Go's default import binding lives in the importing file's scope. Reject * only another binding in that same source section; equal names in sibling * files are independent even though their canonical edges are package-wide. */ static void check_import_redeclarations(Checker *c, Node *file) { if (!c->sep_mode) return; for (Node *u = file->list; u; u = u->next) { if (u->kind != N_USE || u->imported || u->str == NULL) continue; for (Node *v = file->list; v != u; v = v->next) { if (v->kind != N_USE || v->imported || v->str == NULL || v->sourceid != u->sourceid) continue; if (strcmp(v->str, u->str) == 0) { err(c, u->pos, "%s redeclared in this block", u->str); check_import_alt(v, u->str); break; } } } } /* Report file-local unused bindings before package-declaration collisions, * matching the pinned Go resolver's stable ordering. The package declarations * themselves are package-scoped, so they collide with an equal import name in * any contributing source file. */ static void check_import_usage_and_collisions(Checker *c, Node *file) { if (!c->sep_mode) return; for (Node *u = file->list; u; u = u->next) { if (u->kind != N_USE || u->imported || u->used || u->str == NULL) continue; const char *path = u->usesource ? u->usesource : u->usepath ? u->usepath : u->str; const char *dot = strrchr(path, '.'); const char *leaf = dot ? dot + 1 : path; if (strcmp(u->str, leaf) == 0) err(c, u->pos, "\"%s\" imported and not used", path); else err(c, u->pos, "\"%s\" imported as %s and not used", path, u->str); } for (Node *d = file->list; d; d = d->next) { if (d->imported || !top_decl_kind(d) || d->str == NULL) continue; for (Node *u = file->list; u; u = u->next) { if (u->kind != N_USE || u->imported || u->str == NULL || strcmp(d->str, u->str) != 0) continue; const char *path = u->usesource ? u->usesource : u->usepath ? u->usepath : u->str; err(c, d->pos, "%s already declared through import of package %s (\"%s\")", d->str, u->str, path); check_import_alt(u, d->str); } } } void check_file(Checker *c, Node *file) { if (file == NULL || file->kind != N_FILE) return; c->file = file; /* Under -T, prepend the dispatcher support import before pass 1 so * decl_mod keys the runner under the selected support module. A pure * ORDER fix: the synth N_USE was appended AFTER install (below), too late * for decl_mod, so `run` keyed under "" — leaving the qualified call * 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 qualified call. wwstage twin in check.ww. */ if (c->is_test) { int present = 0; /* The synthetic runner belongs to file->sourceid. An import in an * earlier module-reset section neither supplies nor uses its binding. */ for (Node *u = file->list; u; u = u->next) { if (u->kind == N_USE && !u->imported && u->sourceid == file->sourceid && u->usepath && (strcmp(u->usepath, c->test_module) == 0 || (c->test_target != NULL && strcmp(u->usepath, c->test_target) == 0))) u->used = 1; if (u->kind == N_USE && !u->imported && u->sourceid == file->sourceid && u->usepath && strcmp(u->usepath, c->test_module) == 0) { present = 1; } } if (!present) { Node *usenode = newnode(c->a, N_USE, file->pos); usenode->str = c->test_module; usenode->strlen = strlen(c->test_module); usenode->usesource = c->test_module; usenode->usepath = c->test_module; usenode->pkgname = file->pkgname; usenode->sourceid = file->sourceid; usenode->used = 1; usenode->next = file->list; file->list = usenode; } } mark_import_uses(c, file); check_import_redeclarations(c, file); check_import_usage_and_collisions(c, 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) { c->cur_source = d->sourceid; if (d->kind == N_USE) { /* A package may not import its own canonical owner. Import * usage and membership are checked with source-file 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; const char *mod = decl_mod(file, d); Sym *fact = same_import_fact(c, d, mod, SK_TYPE); if (fact != NULL) { d->type = fact->type; continue; } Type *named = type_named(c->a, d->str, NULL); Sym *prev = scope_lookup_local(c->cur, d->str); 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; c->cur_source = 0; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF) continue; c->cur_mod = decl_mod(file, d); c->cur_source = d->sourceid; const char *mod = decl_mod(file, d); if (same_import_fact(c, d, mod, SK_DEF) != NULL) continue; 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; c->cur_source = 0; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_TYPEDECL) continue; resolve_typedecl(c, d); } c->cur_mod = NULL; c->cur_source = 0; for (Node *d = file->list; d; d = d->next) { c->cur_mod = decl_mod(file, d); c->cur_source = d->sourceid; 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); const char *mod = decl_mod(file, d); Sym *fact = same_import_fact(c, d, mod, SK_DEF); if (fact != NULL) { d->type = fact->type ? fact->type : t; break; } d->type = t; Sym *prev = scope_lookup_local(c->cur, d->str); 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; /* A module-scope initializer must be link-time data: * an alloc/call rhs runs code, emit_lets' fold-fail * skipped the DATAW slot silently, and every * reference died at LINK time ("undefined reference") * — reject at the declaration instead (rule 7). * Hare rejects at check time too (ref/harec/src/ * check.c:4360 "Unable to evaluate initializer at * compile time"); ww has no @init path. */ { Node *r = d->rhs; while (r != NULL && (r->kind == N_CAST || r->kind == N_TRYPROP || r->kind == N_TRYUNW)) r = r->lhs; if (r != NULL && (r->kind == N_ALLOC || r->kind == N_CALL)) err(c, d->pos, "module-scope let %s: " "runtime initializer unsupported " "(alloc/call; rule 7)", d->str); } 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; c->cur_source = 0; /* 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 || c->is_test_package) { Pos fp = file->pos; if (c->is_test) { /* (b) the synth entry OWNS `main` — loud-reject a user one. */ for (Node *d = file->list; d; d = d->next) if (!d->imported && 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"); /* The generated table name is reserved only in the owning source, * never by an unrelated declaration carried in dependency exports. */ for (Node *d = file->list; d; d = d->next) if (!d->imported && 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 * `("", &)` 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 ntestattr = 0; for (Node *at = d->attr; at; at = at->next) if (at->str && strcmp(at->str, "test") == 0) ntestattr++; if (ntestattr == 0) continue; /* Attribute-shape rejects, fixed order, first failure * wins per fn; wording is byte-stable with the wwstage * twin (check.ww). A silently-dropped shape here ships * a test that never runs. */ if (ntestattr > 1) { err(c, d->pos, "duplicate @test on fn '%s'", d->str); continue; } if (d->export) { err(c, d->pos, "@test fn '%s' cannot be exported", d->str); continue; } if (d->body == NULL && !d->imported) { err(c, d->pos, "@test fn '%s' needs a body", d->str); 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; } /* A package-test variant validates and retains the body. Its * interface records this declaration as compiler-private metadata; * only a separate -T generated-main action consumes that metadata. */ if (!c->is_test) continue; Node *nm = newnode(c->a, N_STRLIT, fp); nm->str = d->str; nm->strlen = strlen(d->str); Node *id; if (d->imported && d->module && d->module[0]) { const char *alias = d->pkgname && d->pkgname[0] ? d->pkgname : d->module; /* A generated dispatcher cannot bind a command test * package as `main`: that would collide with its own entry. * This is a compiler-owned binding, never source alias syntax; * use the canonical target path as its private qualifier. */ if (c->test_target != NULL && strcmp(d->module, c->test_target) == 0) alias = c->test_target; id = newnode(c->a, N_DOT, fp); id->lhs = newnode(c->a, N_IDENT, fp); id->lhs->str = alias; id->str = d->str; /* Nested nodes never acquire imported from parsing. This marks * the compiler-generated private metadata reference so ordinary * source qualification remains export-checked. */ id->imported = 1; } else { 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++; } if (c->is_test) { 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->pkgname = file->pkgname; tab->sourceid = file->sourceid; 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 support.run(__wwtests). * QUALIFIED, not bare `run`: under the sep producer the * toolchain test runtime is a real imported package. The * selected module is normally `test`, or the reserved alias * chosen by the package driver when user source owns `test`. * The base ident resolves through the synthetic N_USE. */ 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 = c->test_module; dot->str = "run"; c->synth_test_run = dot; call->lhs = dot; call->list = arg; Node *ret = newnode(c->a, N_RETURN, fp); ret->lhs = call; body->list = ret; /* The synthetic support use (the N_DOT base qualifier plus * its use_path source mapping) is now * PREPENDED before pass 1 at the top of check_file, so * decl_mod keys the runtime's `run` under the selected module * and the synthesized call type-resolves. Pass 1 installs it * (SK_USE). */ } Node *m = newnode(c->a, N_FNDECL, fp); m->str = "main"; m->export = 1; m->pkgname = file->pkgname; m->sourceid = file->sourceid; m->lhs = newnode(c->a, N_TNAME, fp); m->lhs->str = "i32"; m->body = body; int savesource = c->cur_source; c->cur_source = m->sourceid; m->type = build_fn_type(c, m); c->cur_source = savesource; /* 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); c->cur_source = d->sourceid; 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: { /* ww restricts C-style ... to bodiless decls pending * vastart/vaarg/vaend builtins (#16); harec permits bodied * C-variadic fns (check.c:3656). */ if (d->body != NULL && d->type->variadic) err(c, d->pos, "C-style variadic '...' " "requires a bodiless declaration"); 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)` → ). * 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; c->cur_source = 0; /* * #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 && !c->is_test_package) { 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; } } }