5025 lines
177 KiB
C
5025 lines
177 KiB
C
/*
|
|
* 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 <limits.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static void cstmt(Checker*, Node*);
|
|
static Type *cexpr(Checker*, Node*);
|
|
static Type *resolve_type(Checker*, Node*);
|
|
static void check_module_shadow(Checker*, const char *name, Pos,
|
|
const char *kindstr);
|
|
|
|
static Type *
|
|
err(Checker *c, Pos p, const char *fmt, ...)
|
|
{
|
|
(void)c;
|
|
va_list ap;
|
|
fprintf(errout ? errout : stderr,
|
|
"%s:%d:%d: error: ", p.file ? p.file : "?", p.line, p.col);
|
|
va_start(ap, fmt);
|
|
vfprintf(errout ? errout : stderr, fmt, ap);
|
|
va_end(ap);
|
|
fputc('\n', errout ? errout : stderr);
|
|
c->errs++;
|
|
return ty_err;
|
|
}
|
|
|
|
static Type *
|
|
lookup_builtin(const char *name)
|
|
{
|
|
if (strcmp(name, "void") == 0) return ty_void;
|
|
if (strcmp(name, "bool") == 0) return ty_bool;
|
|
if (strcmp(name, "rune") == 0) return ty_rune;
|
|
if (strcmp(name, "i8") == 0) return ty_i8;
|
|
if (strcmp(name, "i16") == 0) return ty_i16;
|
|
if (strcmp(name, "i32") == 0) return ty_i32;
|
|
if (strcmp(name, "i64") == 0) return ty_i64;
|
|
if (strcmp(name, "u8") == 0) return ty_u8;
|
|
if (strcmp(name, "u16") == 0) return ty_u16;
|
|
if (strcmp(name, "u32") == 0) return ty_u32;
|
|
if (strcmp(name, "u64") == 0) return ty_u64;
|
|
if (strcmp(name, "int") == 0) return ty_int;
|
|
if (strcmp(name, "uint") == 0) return ty_uint;
|
|
if (strcmp(name, "uintptr") == 0) return ty_uintptr;
|
|
if (strcmp(name, "size") == 0) return ty_size; /* #85 fold-2 */
|
|
if (strcmp(name, "opaque") == 0) return ty_opaque; /* #108(a) */
|
|
if (strcmp(name, "f32") == 0) return ty_f32;
|
|
if (strcmp(name, "f64") == 0) return ty_f64;
|
|
if (strcmp(name, "str") == 0) return ty_str;
|
|
if (strcmp(name, "never") == 0) return ty_never;
|
|
if (strcmp(name, "nomem") == 0) return ty_nomem; /* #29 */
|
|
return NULL;
|
|
}
|
|
|
|
static const char *decl_mod(Node *file, Node *d);
|
|
static const char *use_path(Node *file, const char *curmod, 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 Sym *lookup_bare_import_binding(Checker *c, const char *name);
|
|
static int reject_bare_import_values(Checker *c, Node *n);
|
|
static int reject_bare_import_types(Checker *c, Node *n);
|
|
static void resolve_typedecl(Checker *c, Node *d);
|
|
|
|
static Type *
|
|
resolve_typename(Checker *c, Node *n)
|
|
{
|
|
const char *nm = n->str;
|
|
if (lookup_bare_import_binding(c, nm) != NULL)
|
|
return err(c, n->pos, "%s (package name) is not a type", nm);
|
|
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: {
|
|
if (lookup_bare_import_binding(c, n->str) != NULL) {
|
|
n->type = err(c, n->pos,
|
|
"use of package %s not in selector", n->str);
|
|
return 0;
|
|
}
|
|
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)) {
|
|
if ((n->lhs && n->lhs->type == ty_err) ||
|
|
(n->rhs && n->rhs->type == ty_err))
|
|
n->type = ty_err;
|
|
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)) {
|
|
if (n->lhs && n->lhs->type == ty_err) n->type = ty_err;
|
|
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]<default> can't meet for
|
|
* untyped_str / bare-int-width elements. Peel to the slice element T
|
|
* and run the array-element coercion against it. */
|
|
if (u == NULL || (u->kind != TY_ARRAY && u->kind != TY_SLICE))
|
|
return 0;
|
|
Type *et = u->sub;
|
|
Type *eu = type_chase_named(et);
|
|
u64 count = 0;
|
|
for (Node *e = rhs->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0)
|
|
continue;
|
|
count++;
|
|
}
|
|
/* #71: more elements than the declared [N] passed every per-element
|
|
* check below and then smashed the frame at cgen (each element is
|
|
* stored at its natural offset — the overflow clobbered neighbours
|
|
* and even the saved BP). Reject loud before the element walk.
|
|
* #9: the `alen > 0` exemption is GONE. An infer `[_]` is resized to
|
|
* its real count at the decl sites (is_infer_arr-gated) BEFORE
|
|
* reaching here, so an array arriving with alen==0 is necessarily an
|
|
* EXPLICIT `[0]` — and `[0] = [1,2]` (count 2 > 0) must be loud, not
|
|
* silently resized. `[0] = []` (count 0) stays accepted. SIZE_UNDEFINED
|
|
* (opaque/unsized) still exempt. Under-long (count < N, no `...`) stays
|
|
* accepted as before; Hare rejects it — task #10. */
|
|
if (u->kind == TY_ARRAY && u->alen != SIZE_UNDEFINED
|
|
&& count > u->alen) {
|
|
err(c, rhs->pos, "array literal has %llu elements "
|
|
"but declared array holds %llu",
|
|
(unsigned long long)count, (unsigned long long)u->alen);
|
|
return 0;
|
|
}
|
|
for (Node *e = rhs->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0)
|
|
continue;
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
u64 v;
|
|
if (ev && type_isint(eu) && fold_int_literal(ev, &v)) {
|
|
if (!def_cast_fits(eu, v)) {
|
|
err(c, e->pos, "array element out of range "
|
|
"for %s", type_name(c->a, et));
|
|
return 0;
|
|
}
|
|
continue;
|
|
}
|
|
if (!type_assignable(et, e->type) && !assignable_addrfn(c, et, e))
|
|
return 0;
|
|
}
|
|
/* #25/#31: re-stamp the literal as [count]T so desugar_arrayslice keys
|
|
* on an exact-element-eq array and the cgen N_SLICE-over-N_ARRLIT arm
|
|
* (#31) materialises the borrow backing at the DECLARED element width.
|
|
* The array path keeps the declared array type, so this is slice-only. */
|
|
if (u->kind == TY_SLICE)
|
|
rhs->type = type_array(c->a, et, count);
|
|
return 1;
|
|
}
|
|
|
|
/* eval_def_const — fold a top-level def's rhs to a u64 constant,
|
|
* resolving sibling and imported def references, casts, and
|
|
* arithmetic (#88). Reuses the shared fold_int_literal leaf/unary
|
|
* fold and the fold_binop arith core; the ONLY thing it does that
|
|
* eval_enum_value doesn't is resolve an identifier through the
|
|
* checker's flat scope (scope_lookup_prefer for a bare sibling ref,
|
|
* scope_lookup_in_module for a `mod.NAME` qualified ref) to the
|
|
* referent def's own rhs, then recurse.
|
|
*
|
|
* Why this stays a distinct evaluator from eval_enum_value rather
|
|
* than a full merge (rule 8 WHY): enum-member eval carries implicit
|
|
* prev+1 auto-increment and forward-only sibling lookup over a Tfield
|
|
* chain; def eval has neither — it resolves through the scope/decl
|
|
* graph, which can reference forward and across modules. The two
|
|
* lookup models don't reconcile cleanly, so they share the arith
|
|
* core (fold_binop) + leaf fold (fold_int_literal) and keep separate
|
|
* top-level shapes.
|
|
*
|
|
* `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A,
|
|
* incl. cross-module) hits the cap and fails loud rather than hanging
|
|
* (rule 7), mirroring the cgen.c nsteps>=16 abort precedent. */
|
|
static int
|
|
eval_def_const(Checker *c, Node *n, u64 *out, int depth)
|
|
{
|
|
if (n == NULL) return 0;
|
|
if (depth >= 16) {
|
|
err(c, n->pos,
|
|
"def value: reference chain too deep (cycle?)");
|
|
return 0;
|
|
}
|
|
if (fold_int_literal(n, out)) return 1;
|
|
switch (n->kind) {
|
|
case N_BIN: {
|
|
u64 a, b;
|
|
if (!eval_def_const(c, n->lhs, &a, depth + 1) ||
|
|
!eval_def_const(c, n->rhs, &b, depth + 1)) {
|
|
if ((n->lhs && n->lhs->type == ty_err) ||
|
|
(n->rhs && n->rhs->type == ty_err))
|
|
n->type = ty_err;
|
|
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)) {
|
|
if (n->lhs && n->lhs->type == ty_err) n->type = ty_err;
|
|
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)) {
|
|
if (n->lhs && n->lhs->type == ty_err) n->type = ty_err;
|
|
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: {
|
|
if (lookup_bare_import_binding(c, n->str) != NULL) {
|
|
if (n->type != ty_err)
|
|
n->type = err(c, n->pos,
|
|
"use of package %s not in selector", n->str);
|
|
return 0;
|
|
}
|
|
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 (reject_bare_import_values(c, n->rhs)) {
|
|
/* These leaves own their package-name diagnostics; do not
|
|
* replace them with a dependent constant-fold error. */
|
|
} 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 if (n->rhs->type != ty_err) {
|
|
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 (reject_bare_import_values(c, m->lhs)) {
|
|
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);
|
|
/* Both operands have now been checked. An invalid operand owns the
|
|
* diagnostic; do not add a dependent operator-type error. */
|
|
if (l == ty_err || r == ty_err) return ty_err;
|
|
/* #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);
|
|
if (t == ty_err) return ty_err;
|
|
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");
|
|
if (lookup_bare_import_binding(c, n->str) != NULL)
|
|
return n->type = err(c, n->pos,
|
|
"use of package %s not in selector", n->str);
|
|
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->refdecl = s->decl;
|
|
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) {
|
|
n->refdecl = fs->decl;
|
|
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: {
|
|
/* Package qualifiers are not callable values, even when their local
|
|
* name has builtin spelling (len, size, alloc, ...). Diagnose before
|
|
* any builtin rewrite can erase the callee identifier. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str
|
|
&& lookup_bare_import_binding(c, n->lhs->str) != NULL) {
|
|
int type_args = strcmp(n->lhs->str, "size") == 0
|
|
|| strcmp(n->lhs->str, "align") == 0;
|
|
(void)cexpr(c, n->lhs);
|
|
for (Node *a = n->list; a; a = a->next) {
|
|
if (type_args)
|
|
(void)resolve_type(c, a);
|
|
else
|
|
(void)cexpr(c, a);
|
|
}
|
|
return n->type = ty_err;
|
|
}
|
|
/* 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) {
|
|
if (lookup_bare_import_binding(c, n->lhs->str) != NULL) {
|
|
t = err(c, n->lhs->pos,
|
|
"%s (package name) is not a type", n->lhs->str);
|
|
n->lhs->type = ty_err;
|
|
} else {
|
|
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_targets = NULL;
|
|
c->n_test_targets = 0;
|
|
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;
|
|
}
|
|
|
|
static int
|
|
invalid_init_import(Node *u)
|
|
{
|
|
return u != NULL && u->kind == N_USE && !u->useblank
|
|
&& u->str != NULL && strcmp(u->str, "init") == 0;
|
|
}
|
|
|
|
static Pos
|
|
import_binding_pos(Node *u)
|
|
{
|
|
Pos p = u->pos;
|
|
if (u->usefile != NULL) {
|
|
p.file = u->usefile;
|
|
p.line = u->useline;
|
|
p.col = u->usecol;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/*
|
|
* 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 || invalid_init_import(u)
|
|
|| 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 <name>;` somewhere? With driver concatenation
|
|
* the combined N_FILE collects N_USE nodes from every contributing
|
|
* source; each carries its origin module tag on n->module. Filter
|
|
* by `modtag` so lib/log's `use fmt;` only colours decls whose
|
|
* d->module == "log", not lib/fmt's own decls.
|
|
*
|
|
* modtag == NULL → primary compilation unit's own use directives
|
|
* (N_USE nodes with module == NULL).
|
|
*/
|
|
static int
|
|
src_imports(Node *file, const char *modtag, 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->useblank || invalid_init_import(u)
|
|
|| 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) {
|
|
/* An SK_USE declaration belongs to one contributing source file,
|
|
* unlike ordinary package declarations. Defer it to the source-owned
|
|
* lookup below instead of leaking a sibling file's qualifier. */
|
|
if (s->decl != NULL && s->kind != SK_USE) return s;
|
|
if (s->decl == NULL) 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;
|
|
}
|
|
|
|
/* Return the effective package-name object for a bare identifier without
|
|
* marking the import used. The import edge and the visible symbol must both
|
|
* agree: the former enforces file ownership, while the latter lets an existing
|
|
* closer declaration win and retains the flat-scope use_alias bridge. */
|
|
static Sym *
|
|
lookup_bare_import_binding(Checker *c, const char *name)
|
|
{
|
|
if (c == NULL || name == NULL || name[0] == '\0') return NULL;
|
|
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, name);
|
|
if (s == NULL || (s->kind != SK_USE && !s->use_alias)) return NULL;
|
|
if (src_imports(c->file, c->cur_mod, c->cur_source, name)) return s;
|
|
return NULL;
|
|
}
|
|
|
|
/* Walk a type subtree only far enough to classify package-name objects. This
|
|
* is deliberately not general type checking: the enum/array constant folders
|
|
* need the package diagnostic before they reduce an unsupported outer shape
|
|
* to a generic constant error. */
|
|
static int
|
|
reject_bare_import_types(Checker *c, Node *n)
|
|
{
|
|
if (n == NULL) return 0;
|
|
if (n->kind == N_IDENT || n->kind == N_TNAME) {
|
|
if (lookup_bare_import_binding(c, n->str) != NULL) {
|
|
if (n->type != ty_err)
|
|
n->type = err(c, n->pos,
|
|
"%s (package name) is not a type", n->str);
|
|
return 1;
|
|
}
|
|
/* Qualified type syntax is a legal selector use. Mark the exact
|
|
* source-owned binding even if the surrounding constant shape is
|
|
* independently invalid. */
|
|
if (n->kind == N_TNAME && n->str != NULL) {
|
|
const char *dot = strrchr(n->str, '.');
|
|
if (dot != NULL) {
|
|
char *head = astrndup(c->a, n->str,
|
|
(u64)(dot - n->str));
|
|
if (lookup_bare_import_binding(c, head) != NULL)
|
|
(void)use_path(c->file, c->cur_mod,
|
|
c->cur_source, head);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
int bad = 0;
|
|
if (n->kind == N_TARRAY) {
|
|
bad |= reject_bare_import_types(c, n->lhs);
|
|
bad |= reject_bare_import_values(c, n->rhs);
|
|
return bad;
|
|
}
|
|
if (n->kind == N_TENUM) {
|
|
bad |= reject_bare_import_types(c, n->lhs);
|
|
for (Node *p = n->list; p; p = p->next)
|
|
bad |= reject_bare_import_values(c, p->lhs);
|
|
return bad;
|
|
}
|
|
if (n->kind == N_TENUMMEMBER)
|
|
return reject_bare_import_values(c, n->lhs);
|
|
bad |= reject_bare_import_types(c, n->lhs);
|
|
bad |= reject_bare_import_types(c, n->rhs);
|
|
bad |= reject_bare_import_types(c, n->cond);
|
|
bad |= reject_bare_import_types(c, n->body);
|
|
bad |= reject_bare_import_types(c, n->els);
|
|
for (Node *p = n->list; p; p = p->next)
|
|
bad |= reject_bare_import_types(c, p);
|
|
return bad;
|
|
}
|
|
|
|
/* Reject every bare package-name value in a constant-expression subtree in
|
|
* source order. Direct package receivers remain legal selector qualifiers;
|
|
* cast operands, struct-literal heads, and size/align arguments retain their
|
|
* type context. */
|
|
static int
|
|
reject_bare_import_values(Checker *c, Node *n)
|
|
{
|
|
if (n == NULL) return 0;
|
|
switch (n->kind) {
|
|
case N_TPTR: case N_TSLICE: case N_TARRAY: case N_TFN:
|
|
case N_TSTRUCT: case N_TFIELD: case N_TCHAN:
|
|
case N_TTUPLE: case N_TTAGGED: case N_TBANG: case N_TENUM:
|
|
case N_TENUMMEMBER:
|
|
return reject_bare_import_types(c, n);
|
|
case N_IDENT: case N_TNAME:
|
|
if (lookup_bare_import_binding(c, n->str) != NULL) {
|
|
if (n->type != ty_err)
|
|
n->type = err(c, n->pos,
|
|
"use of package %s not in selector", n->str);
|
|
return 1;
|
|
}
|
|
if (n->kind == N_TNAME && n->str != NULL) {
|
|
const char *dot = strrchr(n->str, '.');
|
|
if (dot != NULL) {
|
|
char *head = astrndup(c->a, n->str,
|
|
(u64)(dot - n->str));
|
|
if (lookup_bare_import_binding(c, head) != NULL)
|
|
(void)use_path(c->file, c->cur_mod,
|
|
c->cur_source, head);
|
|
}
|
|
}
|
|
return 0;
|
|
case N_DOT: {
|
|
int bad = 0;
|
|
if (n->lhs && n->lhs->kind == N_IDENT
|
|
&& lookup_bare_import_binding(c, n->lhs->str) != NULL) {
|
|
(void)use_path(c->file, c->cur_mod, c->cur_source,
|
|
n->lhs->str);
|
|
} else {
|
|
bad |= reject_bare_import_values(c, n->lhs);
|
|
}
|
|
return bad;
|
|
}
|
|
case N_CAST: case N_TYPEASSERT: case N_TYPETEST: {
|
|
int bad = reject_bare_import_values(c, n->lhs);
|
|
bad |= reject_bare_import_types(c, n->rhs);
|
|
return bad;
|
|
}
|
|
case N_STRUCTLIT: {
|
|
int bad = reject_bare_import_types(c, n->lhs);
|
|
for (Node *p = n->list; p; p = p->next)
|
|
bad |= reject_bare_import_values(c, p->lhs);
|
|
return bad;
|
|
}
|
|
case N_CALL: {
|
|
int bad = reject_bare_import_values(c, n->lhs);
|
|
int typearg = n->lhs && n->lhs->kind == N_IDENT
|
|
&& (strcmp(n->lhs->str, "size") == 0
|
|
|| strcmp(n->lhs->str, "align") == 0);
|
|
for (Node *p = n->list; p; p = p->next)
|
|
bad |= typearg ? reject_bare_import_types(c, p)
|
|
: reject_bare_import_values(c, p);
|
|
return bad;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
int bad = 0;
|
|
bad |= reject_bare_import_values(c, n->lhs);
|
|
bad |= reject_bare_import_values(c, n->rhs);
|
|
bad |= reject_bare_import_values(c, n->cond);
|
|
bad |= reject_bare_import_values(c, n->body);
|
|
bad |= reject_bare_import_values(c, n->els);
|
|
for (Node *p = n->list; p; p = p->next)
|
|
bad |= reject_bare_import_values(c, p);
|
|
return bad;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static int
|
|
init_private_symbol(Node *d)
|
|
{
|
|
for (Node *a = d ? d->attr : NULL; a; a = a->next) {
|
|
if (a->kind != N_ATTR || a->str == NULL
|
|
|| strcmp(a->str, "symbol") != 0 || a->list == NULL
|
|
|| a->list->kind != N_STRLIT || a->list->str == NULL)
|
|
continue;
|
|
if (strncmp(a->list->str, "__ww..", 6) == 0)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Mutable module lets are WW's Go-variable analogue. Literal data stays in
|
|
* the archive's static image; every surviving value computation is moved to
|
|
* the package task. `def` and `const` deliberately remain outside this path. */
|
|
static Node *
|
|
init_strip_cast(Node *n)
|
|
{
|
|
while (n != NULL && n->kind == N_CAST) n = n->lhs;
|
|
return n;
|
|
}
|
|
|
|
static int init_expr_static(Type *, Node *);
|
|
static int init_array_static(Type *, Node *, unsigned);
|
|
|
|
static int
|
|
init_fnptr_static(Node *n)
|
|
{
|
|
Node *r = init_strip_cast(n);
|
|
if (r == NULL || r->kind != N_UN || r->op != TK_AMP) return 0;
|
|
Node *v = init_strip_cast(r->lhs);
|
|
Type *vt = type_chase_named(v ? v->type : NULL);
|
|
if (v == NULL || vt == NULL || vt->kind != TY_FN) return 0;
|
|
if (v->kind == N_IDENT) return 1;
|
|
return v->kind == N_DOT && v->lhs != NULL
|
|
&& v->lhs->kind == N_IDENT
|
|
&& (v->lhs->type == NULL || v->lhs->type == ty_err);
|
|
}
|
|
|
|
static int
|
|
init_float_static(Node *n)
|
|
{
|
|
Node *r = init_strip_cast(n);
|
|
if (r != NULL && r->kind == N_UN
|
|
&& (r->op == TK_PLUS || r->op == TK_MINUS))
|
|
r = init_strip_cast(r->lhs);
|
|
return r != NULL && r->kind == N_FLOATLIT;
|
|
}
|
|
|
|
static int
|
|
init_tagged_raw_static(Type *u, Node *n)
|
|
{
|
|
Node *r = init_strip_cast(n);
|
|
/* Cstage keeps true/false as untyped-bool, distinct from the concrete
|
|
* bool variant selected by runtime boxing. Keep boolean payloads on that
|
|
* runtime path in both stages so package artifacts remain identical. */
|
|
if (r != NULL && (r->kind == N_TRUE || r->kind == N_FALSE)) return 0;
|
|
Type *ru = type_chase_named(r ? r->type : NULL);
|
|
u64 ignored;
|
|
return u != NULL && u->kind == TY_TAGGED && !u->nullable
|
|
&& r != NULL && variant_present(u->params, r->type)
|
|
&& (ru == NULL || (ru->kind != TY_STR && ru->kind != TY_SLICE))
|
|
&& fold_int_literal(r, &ignored);
|
|
}
|
|
|
|
static int
|
|
init_tuple_static(Type *u, Node *n)
|
|
{
|
|
Node *r = init_strip_cast(n);
|
|
if (u == NULL || u->kind != TY_TUPLE
|
|
|| r == NULL || r->kind != N_TUPLE)
|
|
return 0;
|
|
Tparam *tp = u->params;
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
if (tp == NULL) return 0;
|
|
Node *v = init_strip_cast(e);
|
|
Type *et = type_chase_named(tp->type);
|
|
if (v == NULL || (et != NULL && et->kind == TY_TAGGED)) return 0;
|
|
if (et != NULL && (et->kind == TY_STR || et->kind == TY_SLICE)) {
|
|
if (v->kind != N_STRLIT) return 0;
|
|
} else if (!init_fnptr_static(v)) {
|
|
u64 ignored;
|
|
if (!fold_int_literal(v, &ignored)) return 0;
|
|
}
|
|
tp = tp->next;
|
|
}
|
|
return tp == NULL;
|
|
}
|
|
|
|
static int
|
|
init_struct_static(Type *u, Node *n)
|
|
{
|
|
Node *r = init_strip_cast(n);
|
|
if (u == NULL || u->kind != TY_STRUCT
|
|
|| r == NULL || r->kind != N_STRUCTLIT)
|
|
return 0;
|
|
for (Tfield *f = u->fields; f; f = f->next) {
|
|
Node *value = NULL;
|
|
for (Node *e = r->list; e; e = e->next)
|
|
if (e->str != NULL && f->name != NULL
|
|
&& strcmp(e->str, f->name) == 0) {
|
|
value = e->lhs;
|
|
break;
|
|
}
|
|
if (value != NULL) {
|
|
Type *fu = type_chase_named(f->type);
|
|
int ok = 0;
|
|
if (fu != NULL && fu->kind == TY_TAGGED && !fu->nullable)
|
|
ok = init_tagged_raw_static(fu, value);
|
|
else if (fu != NULL && fu->kind == TY_STRUCT)
|
|
ok = init_struct_static(fu, value);
|
|
else if (fu != NULL && fu->kind == TY_ARRAY)
|
|
ok = init_array_static(fu, value, 1);
|
|
else if (type_isfloat(f->type))
|
|
ok = init_float_static(value);
|
|
else {
|
|
u64 ignored;
|
|
ok = fold_int_literal(init_strip_cast(value), &ignored);
|
|
}
|
|
if (!ok) return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
#define INIT_ARR_REPEAT 1u
|
|
#define INIT_ARR_STR_RELOC 2u
|
|
#define INIT_ARR_TUPLE_ROWS 4u
|
|
|
|
static int
|
|
init_array_static(Type *u, Node *n, unsigned flags)
|
|
{
|
|
Node *r = init_strip_cast(n);
|
|
if (u == NULL || (u->kind != TY_ARRAY && u->kind != TY_SLICE)
|
|
|| r == NULL || r->kind != N_ARRLIT)
|
|
return 0;
|
|
Type *et = u->sub;
|
|
Type *eu = type_chase_named(et);
|
|
int seen = 0;
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str != NULL
|
|
&& strcmp(e->str, "...") == 0)
|
|
return (flags & INIT_ARR_REPEAT) != 0 && seen > 0
|
|
&& e->next == NULL
|
|
&& (eu == NULL || eu->kind != TY_ARRAY);
|
|
Node *v = init_strip_cast(e);
|
|
if (v == NULL) return 0;
|
|
if (eu != NULL && eu->kind == TY_STR) {
|
|
if ((flags & INIT_ARR_STR_RELOC) == 0
|
|
|| v->kind != N_STRLIT) return 0;
|
|
} else if (eu != NULL && eu->kind == TY_STRUCT) {
|
|
if (!init_struct_static(eu, v)) return 0;
|
|
} else if (eu != NULL && eu->kind == TY_ARRAY) {
|
|
if (!init_array_static(eu, v, INIT_ARR_REPEAT)) return 0;
|
|
} else if (eu != NULL && eu->kind == TY_TUPLE) {
|
|
if ((flags & INIT_ARR_TUPLE_ROWS) == 0
|
|
|| !init_tuple_static(eu, v)) return 0;
|
|
} else if (eu != NULL && eu->kind == TY_TAGGED) {
|
|
if (!init_tagged_raw_static(eu, v)) return 0;
|
|
} else if (eu != NULL && (eu->kind == TY_SLICE
|
|
|| eu->kind == TY_PTR
|
|
|| eu->kind == TY_FN)) {
|
|
return 0;
|
|
} else if (type_isfloat(et)) {
|
|
if (!init_float_static(v)) return 0;
|
|
} else {
|
|
u64 ignored;
|
|
if (!fold_int_literal(v, &ignored)) return 0;
|
|
}
|
|
seen++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* This predicate is the checker's validate-only twin of cgen's static-data
|
|
* arms. A true result must be safe to emit; every other valid mutable value
|
|
* is zero-backed and evaluated exactly once by the package task. */
|
|
static int
|
|
init_expr_static(Type *t, Node *n)
|
|
{
|
|
Node *r = init_strip_cast(n);
|
|
if (r == NULL) return 1;
|
|
Type *u = type_chase_named(t);
|
|
if (type_isfloat(t)) return init_float_static(r);
|
|
if (u != NULL && (u->kind == TY_STR || u->kind == TY_UNTYPED_STR))
|
|
return r->kind == N_STRLIT || r->kind == N_NIL;
|
|
if (u != NULL && u->kind == TY_ARRAY)
|
|
return init_array_static(u, r,
|
|
INIT_ARR_REPEAT | INIT_ARR_STR_RELOC);
|
|
if (u != NULL && u->kind == TY_STRUCT)
|
|
return init_struct_static(u, r);
|
|
if (u != NULL && u->kind == TY_TUPLE)
|
|
return init_tuple_static(u, r);
|
|
if (u != NULL && u->kind == TY_SLICE) {
|
|
if (r->kind == N_NIL) return 1;
|
|
return init_array_static(u, r, INIT_ARR_TUPLE_ROWS);
|
|
}
|
|
if (u != NULL && u->kind == TY_TAGGED && !u->nullable) {
|
|
Type *ru = type_chase_named(r->type);
|
|
if (!variant_present(u->params, r->type)) return 0;
|
|
/* String carriers need a relocation-bearing tagged row. Cstage's
|
|
* untyped string is not the concrete str variant here; keep every
|
|
* such mutable value on the common runtime path rather than let one
|
|
* stage classify the same spelling as static data. */
|
|
if (ru != NULL && (ru->kind == TY_STR
|
|
|| ru->kind == TY_UNTYPED_STR || ru->kind == TY_SLICE))
|
|
return 0;
|
|
return init_tagged_raw_static(u, r);
|
|
}
|
|
if (init_fnptr_static(r)) return 1;
|
|
u64 ignored;
|
|
return fold_int_literal(r, &ignored);
|
|
}
|
|
|
|
/* A slice literal has no declared element count for WW's trailing `...`
|
|
* repeat to fill. Keep this a checker-owned semantic rejection when a
|
|
* mutable package let moves from static data to runtime initialization;
|
|
* otherwise the backing counter drops the marker and silently publishes the
|
|
* explicit prefix. Array repeats remain valid because their target length is
|
|
* known. Recurse through the aggregate shapes package-init lowering owns so
|
|
* a nested slice cannot bypass the same rule. */
|
|
static int
|
|
init_validate_slice_repeats(Checker *c, Type *want, Node *expr)
|
|
{
|
|
Node *r = init_strip_cast(expr);
|
|
Type *u = type_chase_named(want);
|
|
if (r == NULL || u == NULL) return 0;
|
|
if (u->kind == TY_SLICE && r->kind == N_ARRLIT) {
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str != NULL
|
|
&& strcmp(e->str, "...") == 0) {
|
|
err(c, e->pos, "'...' repeat has no target length "
|
|
"in a slice literal");
|
|
return -1;
|
|
}
|
|
if (init_validate_slice_repeats(c, u->sub, e) < 0)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
if (u->kind == TY_ARRAY && r->kind == N_ARRLIT) {
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str != NULL
|
|
&& strcmp(e->str, "...") == 0)
|
|
continue;
|
|
if (init_validate_slice_repeats(c, u->sub, e) < 0)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
if (u->kind == TY_STRUCT && r->kind == N_STRUCTLIT) {
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
Tfield *field = NULL;
|
|
for (Tfield *f = u->fields; f; f = f->next)
|
|
if (e->str != NULL && f->name != NULL
|
|
&& strcmp(e->str, f->name) == 0) {
|
|
field = f;
|
|
break;
|
|
}
|
|
if (field != NULL
|
|
&& init_validate_slice_repeats(c, field->type,
|
|
e->lhs) < 0)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
if (u->kind == TY_TUPLE && r->kind == N_TUPLE) {
|
|
Tparam *p = u->params;
|
|
for (Node *e = r->list; e && p; e = e->next, p = p->next)
|
|
if (init_validate_slice_repeats(c, p->type, e) < 0)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
struct initwalkitem {
|
|
Node *node;
|
|
struct initwalkitem *next;
|
|
};
|
|
|
|
struct initwalk {
|
|
struct initwalkitem *stack;
|
|
struct initwalkitem *seenfn;
|
|
};
|
|
|
|
static int
|
|
initwalk_push(struct initwalkitem **head, Node *x)
|
|
{
|
|
if (x == NULL) return 0;
|
|
struct initwalkitem *p = malloc(sizeof *p);
|
|
if (p == NULL) return -1;
|
|
p->node = x;
|
|
p->next = *head;
|
|
*head = p;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
initwalk_seen_fn(struct initwalk *w, Node *fn)
|
|
{
|
|
for (struct initwalkitem *p = w->seenfn; p; p = p->next)
|
|
if (p->node == fn) return 1;
|
|
if (initwalk_push(&w->seenfn, fn) < 0) return -1;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
initwalk_free(struct initwalkitem *p)
|
|
{
|
|
while (p != NULL) {
|
|
struct initwalkitem *next = p->next;
|
|
free(p);
|
|
p = next;
|
|
}
|
|
}
|
|
|
|
/* Does variable `from` depend on `target`? Function nodes are transparent,
|
|
* as in go/types initOrder: references in their bodies become variable edges. */
|
|
static int
|
|
init_refers(Node *from, Node *target)
|
|
{
|
|
struct initwalk w = {0};
|
|
int result = 0;
|
|
if (initwalk_push(&w.stack, from->rhs) < 0)
|
|
result = -1;
|
|
while (result == 0 && w.stack != NULL) {
|
|
struct initwalkitem *top = w.stack;
|
|
Node *n = top->node;
|
|
w.stack = top->next;
|
|
free(top);
|
|
if (n->refdecl == target) {
|
|
result = 1;
|
|
break;
|
|
}
|
|
Node *r = n->refdecl;
|
|
if (r != NULL && r->kind == N_FNDECL && !r->imported
|
|
&& !r->initfn && r->body != NULL) {
|
|
int seen = initwalk_seen_fn(&w, r);
|
|
if (seen < 0) { result = -1; break; }
|
|
if (!seen && initwalk_push(&w.stack, r->body) < 0) {
|
|
result = -1;
|
|
break;
|
|
}
|
|
}
|
|
Node *child[] = { n->attr, n->lhs, n->rhs, n->cond,
|
|
n->body, n->els, n->list, n->next };
|
|
for (size_t i = 0; result == 0 && i < nelem(child); i++)
|
|
if (initwalk_push(&w.stack, child[i]) < 0)
|
|
result = -1;
|
|
}
|
|
initwalk_free(w.stack);
|
|
initwalk_free(w.seenfn);
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
init_cycle_note(Node *from, Node *to)
|
|
{
|
|
FILE *f = errout ? errout : stderr;
|
|
fprintf(f, "\t%s:%d:%d: %s refers to %s\n",
|
|
from->pos.file ? from->pos.file : "?", from->pos.line,
|
|
from->pos.col, from->str, to->str);
|
|
}
|
|
|
|
/* Go's findPath, iteratively: dependencies are visited in source order and a
|
|
* global seen set prevents a side cycle from consuming the native stack. */
|
|
static int
|
|
init_find_cycle(Checker *c, Node **vars, size_t nvar, size_t start)
|
|
{
|
|
unsigned char *seen = calloc(nvar, 1);
|
|
size_t *path = malloc(nvar * sizeof *path);
|
|
size_t *next = calloc(nvar, sizeof *next);
|
|
if (seen == NULL || path == NULL || next == NULL) {
|
|
free(next); free(path); free(seen);
|
|
err(c, vars[start]->pos,
|
|
"out of memory while ordering package initialization");
|
|
return -1;
|
|
}
|
|
size_t depth = 1;
|
|
path[0] = start;
|
|
seen[start] = 1;
|
|
while (depth > 0) {
|
|
size_t from = path[depth - 1];
|
|
int descended = 0;
|
|
while (next[depth - 1] < nvar) {
|
|
size_t to = next[depth - 1]++;
|
|
int dep = init_refers(vars[from], vars[to]);
|
|
if (dep < 0) {
|
|
err(c, vars[start]->pos,
|
|
"out of memory while ordering package initialization");
|
|
free(next); free(path); free(seen);
|
|
return -1;
|
|
}
|
|
if (!dep) continue;
|
|
if (to == start) {
|
|
if (depth == 1) {
|
|
err(c, vars[start]->pos,
|
|
"initialization cycle: %s refers to itself",
|
|
vars[start]->str);
|
|
} else {
|
|
err(c, vars[start]->pos,
|
|
"initialization cycle for %s",
|
|
vars[start]->str);
|
|
for (size_t i = 1; i < depth; i++)
|
|
init_cycle_note(vars[path[i - 1]],
|
|
vars[path[i]]);
|
|
init_cycle_note(vars[path[depth - 1]],
|
|
vars[start]);
|
|
}
|
|
free(next); free(path); free(seen);
|
|
return 1;
|
|
}
|
|
if (seen[to]) continue;
|
|
seen[to] = 1;
|
|
path[depth] = to;
|
|
next[depth] = 0;
|
|
depth++;
|
|
descended = 1;
|
|
break;
|
|
}
|
|
if (!descended) depth--;
|
|
}
|
|
free(next); free(path); free(seen);
|
|
return 0;
|
|
}
|
|
|
|
static Node *
|
|
init_make_call(Checker *c, Node *fn, Pos pos)
|
|
{
|
|
Node *id = newnode(c->a, N_IDENT, pos);
|
|
id->str = fn->str;
|
|
id->strlen = strlen(fn->str);
|
|
id->type = fn->type;
|
|
id->refdecl = fn;
|
|
Node *call = newnode(c->a, N_CALL, pos);
|
|
call->lhs = id;
|
|
call->type = ty_void;
|
|
Node *stmt = newnode(c->a, N_EXPRSTMT, pos);
|
|
stmt->lhs = call;
|
|
return stmt;
|
|
}
|
|
|
|
static u64
|
|
init_arrlit_count(Node *lit)
|
|
{
|
|
u64 count = 0;
|
|
for (Node *e = lit ? lit->list : NULL; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str != NULL
|
|
&& strcmp(e->str, "...") == 0)
|
|
continue;
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/* Runtime slice literals cannot use the ordinary local-literal backing: that
|
|
* storage dies when the compiler-generated variable helper returns. Attach a
|
|
* canonical package-owned backing symbol to every slice literal contained in
|
|
* the value being published. The cgen emits zeroed writable storage for the
|
|
* symbol and fills it at the literal's exact evaluation point.
|
|
*
|
|
* The name is derived only from the action-owned package-init symbol, the
|
|
* Go-ordered variable ordinal, and literal preorder. Source names, import
|
|
* aliases, declared package names, and path leaves never enter the identity. */
|
|
static void
|
|
init_mark_slice_backings(Checker *c, Type *want, Node *expr,
|
|
const char *base, u64 order, u64 *preorder)
|
|
{
|
|
Node *r = init_strip_cast(expr);
|
|
Type *u = type_chase_named(want);
|
|
if (r == NULL || u == NULL) return;
|
|
if (u->kind == TY_SLICE && r->kind == N_ARRLIT) {
|
|
u64 count = init_arrlit_count(r);
|
|
(*preorder)++;
|
|
r->linksym = aprintf(c->a, "%s.v.%llu.b.%llu", base,
|
|
(unsigned long long)order,
|
|
(unsigned long long)*preorder);
|
|
r->type = type_array(c->a, u->sub, count);
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str != NULL
|
|
&& strcmp(e->str, "...") == 0)
|
|
continue;
|
|
init_mark_slice_backings(c, u->sub, e, base, order,
|
|
preorder);
|
|
}
|
|
return;
|
|
}
|
|
if (u->kind == TY_ARRAY && r->kind == N_ARRLIT) {
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str != NULL
|
|
&& strcmp(e->str, "...") == 0)
|
|
continue;
|
|
init_mark_slice_backings(c, u->sub, e, base, order,
|
|
preorder);
|
|
}
|
|
return;
|
|
}
|
|
if (u->kind == TY_STRUCT && r->kind == N_STRUCTLIT) {
|
|
for (Node *e = r->list; e; e = e->next) {
|
|
Tfield *field = NULL;
|
|
for (Tfield *f = u->fields; f; f = f->next)
|
|
if (e->str != NULL && f->name != NULL
|
|
&& strcmp(e->str, f->name) == 0) {
|
|
field = f;
|
|
break;
|
|
}
|
|
if (field != NULL)
|
|
init_mark_slice_backings(c, field->type, e->lhs,
|
|
base, order, preorder);
|
|
}
|
|
return;
|
|
}
|
|
if (u->kind == TY_TUPLE && r->kind == N_TUPLE) {
|
|
Tparam *p = u->params;
|
|
for (Node *e = r->list; e && p; e = e->next, p = p->next)
|
|
init_mark_slice_backings(c, p->type, e, base, order,
|
|
preorder);
|
|
}
|
|
}
|
|
|
|
static Node *
|
|
init_make_helper(Checker *c, Node *d, const char *base)
|
|
{
|
|
Node *fn = newnode(c->a, N_FNDECL, d->pos);
|
|
fn->str = aprintf(c->a, "__ww_init_var_%llu",
|
|
(unsigned long long)d->initorder);
|
|
fn->strlen = strlen(fn->str);
|
|
fn->module = d->module;
|
|
fn->pkgname = d->pkgname;
|
|
fn->sourceid = d->sourceid;
|
|
fn->initsynthetic = 1;
|
|
fn->linksym = aprintf(c->a, "%s.v.%llu", base,
|
|
(unsigned long long)d->initorder);
|
|
Type *ft = newtype(c->a, TY_FN);
|
|
ft->size = 8; ft->align = 8; ft->ret = ty_void;
|
|
fn->type = ft;
|
|
|
|
/* Evaluate into an addressable local first. The established local-let and
|
|
* full-value assignment paths cover arrays, structs, tuples, calls, and
|
|
* allocation without asking the static-data emitter to understand them. */
|
|
Node *tmp = newnode(c->a, N_LET, d->pos);
|
|
tmp->str = aprintf(c->a, "__ww_init_tmp_%llu",
|
|
(unsigned long long)d->initorder);
|
|
tmp->strlen = strlen(tmp->str);
|
|
tmp->lhs = d->lhs;
|
|
tmp->rhs = d->rhs;
|
|
tmp->type = d->type;
|
|
tmp->initsynthetic = 1;
|
|
|
|
Node *id = newnode(c->a, N_IDENT, d->pos);
|
|
id->str = d->str;
|
|
id->strlen = strlen(d->str);
|
|
id->type = d->type;
|
|
id->refdecl = d;
|
|
Node *value = newnode(c->a, N_IDENT, d->pos);
|
|
value->str = tmp->str;
|
|
value->strlen = tmp->strlen;
|
|
value->type = d->type;
|
|
value->refdecl = tmp;
|
|
Node *assign = newnode(c->a, N_ASSIGN, d->pos);
|
|
assign->op = TK_ASSIGN;
|
|
assign->lhs = id;
|
|
assign->rhs = value;
|
|
assign->type = d->type;
|
|
Node *stmt = newnode(c->a, N_EXPRSTMT, d->pos);
|
|
stmt->lhs = assign;
|
|
Node *body = newnode(c->a, N_BLOCK, d->pos);
|
|
body->list = tmp;
|
|
tmp->next = stmt;
|
|
fn->body = body;
|
|
d->rhs = NULL;
|
|
return fn;
|
|
}
|
|
|
|
static int
|
|
init_lower_package(Checker *c, Node *file)
|
|
{
|
|
int nvar = 0;
|
|
u64 nruntime = 0;
|
|
u64 ninit = 0;
|
|
Node *firstinit = NULL;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->initfn && !d->imported) {
|
|
ninit++;
|
|
if (firstinit == NULL) firstinit = d;
|
|
}
|
|
if (d->kind != N_LET || d->imported || d->rhs == NULL)
|
|
continue;
|
|
if (d->op == TK_CONST) continue;
|
|
if (nvar == INT_MAX) {
|
|
err(c, d->pos,
|
|
"out of memory while ordering package initialization");
|
|
return -1;
|
|
}
|
|
nvar++;
|
|
if (init_validate_slice_repeats(c, d->type, d->rhs) < 0)
|
|
continue;
|
|
if (!init_expr_static(d->type, d->rhs)) {
|
|
d->runtimeinit = 1;
|
|
nruntime++;
|
|
if (firstinit == NULL) firstinit = d;
|
|
}
|
|
}
|
|
if (c->errs) return -1;
|
|
/* A separate package with executable initialization must have the exact
|
|
* action-owned symbol supplied by its driver. Legacy non-separate mode
|
|
* retains its historical private fallback. */
|
|
if (c->sep_mode && (c->package_init_symbol == NULL
|
|
|| c->package_init_symbol[0] == '\0')
|
|
&& (nruntime != 0 || ninit != 0)) {
|
|
err(c, firstinit ? firstinit->pos : file->pos,
|
|
"package initialization requires --package-init-symbol under -c");
|
|
return -1;
|
|
}
|
|
if ((c->package_init_symbol == NULL
|
|
|| c->package_init_symbol[0] == '\0') && nruntime == 0 && ninit == 0)
|
|
return 0;
|
|
|
|
Node **vars = NULL;
|
|
if (nvar != 0) {
|
|
if ((size_t)nvar > (size_t)-1 / sizeof *vars
|
|
|| (vars = malloc((size_t)nvar * sizeof *vars)) == NULL) {
|
|
err(c, file->pos,
|
|
"out of memory while ordering package initialization");
|
|
return -1;
|
|
}
|
|
int vi = 0;
|
|
for (Node *d = file->list; d; d = d->next)
|
|
if (d->kind == N_LET && !d->imported && d->rhs != NULL
|
|
&& d->op != TK_CONST)
|
|
vars[vi++] = d;
|
|
}
|
|
int done = 0;
|
|
while (done < nvar) {
|
|
size_t best = (size_t)-1;
|
|
size_t bestdeps = (size_t)-1;
|
|
for (int i = 0; i < nvar; i++) {
|
|
if (vars[i]->initorder != 0) continue;
|
|
size_t ndeps = 0;
|
|
for (int j = 0; j < nvar; j++) {
|
|
if (vars[j]->initorder != 0) continue;
|
|
int dep = init_refers(vars[i], vars[j]);
|
|
if (dep < 0) {
|
|
err(c, vars[i]->pos,
|
|
"out of memory while ordering package initialization");
|
|
free(vars);
|
|
return -1;
|
|
}
|
|
if (dep) ndeps++;
|
|
}
|
|
if (best == (size_t)-1 || ndeps < bestdeps) {
|
|
best = (size_t)i;
|
|
bestdeps = ndeps;
|
|
}
|
|
}
|
|
if (best == (size_t)-1) break;
|
|
if (bestdeps != 0) {
|
|
int cycle = init_find_cycle(c, vars, (size_t)nvar, best);
|
|
if (cycle < 0) { free(vars); return -1; }
|
|
/* A reported cycle is broken by removing this node, exactly
|
|
* like go/types' priority-queue walk. Continue so disjoint or
|
|
* overlapping later cycles retain their deterministic errors. */
|
|
}
|
|
vars[best]->initorder = (u64)++done;
|
|
}
|
|
free(vars);
|
|
if (c->errs) return -1;
|
|
|
|
const char *base = c->package_init_symbol;
|
|
if (base == NULL || base[0] == '\0')
|
|
base = "__ww..pkg.v0.r0.e.init";
|
|
Node *tail = file->list;
|
|
while (tail && tail->next) tail = tail->next;
|
|
Node *helpers = NULL, *helpertail = NULL;
|
|
for (u64 order = 1; order <= (u64)nvar; order++)
|
|
for (Node *d = file->list; d; d = d->next)
|
|
if (d->runtimeinit && d->initorder == order) {
|
|
u64 preorder = 0;
|
|
init_mark_slice_backings(c, d->type, d->rhs,
|
|
base, order, &preorder);
|
|
Node *fn = init_make_helper(c, d, base);
|
|
if (helpers == NULL) helpers = fn;
|
|
else helpertail->next = fn;
|
|
helpertail = fn;
|
|
break;
|
|
}
|
|
if (tail) tail->next = helpers;
|
|
else file->list = helpers;
|
|
if (helpertail) tail = helpertail;
|
|
|
|
Node *task = newnode(c->a, N_FNDECL, file->pos);
|
|
task->str = "__ww_init_task";
|
|
task->strlen = strlen(task->str);
|
|
task->initsynthetic = 1;
|
|
task->linksym = base;
|
|
Type *tt = newtype(c->a, TY_FN);
|
|
tt->size = 8; tt->align = 8; tt->ret = ty_void;
|
|
task->type = tt;
|
|
Node *body = newnode(c->a, N_BLOCK, file->pos);
|
|
Node *stail = NULL;
|
|
for (Node *fn = helpers; fn; fn = fn->next) {
|
|
Node *s = init_make_call(c, fn, fn->pos);
|
|
if (body->list == NULL) body->list = s;
|
|
else stail->next = s;
|
|
stail = s;
|
|
}
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (!d->initfn || d->imported) continue;
|
|
Node *s = init_make_call(c, d, d->pos);
|
|
if (body->list == NULL) body->list = s;
|
|
else stail->next = s;
|
|
stail = s;
|
|
}
|
|
task->body = body;
|
|
if (tail) tail->next = task;
|
|
else file->list = task;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
classify_init_decls(Checker *c, Node *file)
|
|
{
|
|
u64 ordinal = 0;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (init_private_symbol(d))
|
|
err(c, d->pos, "@symbol name uses reserved prefix __ww..");
|
|
if (d->str == NULL || strcmp(d->str, "init") != 0
|
|
|| !top_decl_kind(d))
|
|
continue;
|
|
if (d->kind != N_FNDECL) {
|
|
err(c, d->pos, "cannot declare init - must be func");
|
|
continue;
|
|
}
|
|
d->initfn = 1;
|
|
d->initorder = ++ordinal;
|
|
if (d->export)
|
|
err(c, d->pos, "func init cannot be exported");
|
|
if (d->body == NULL)
|
|
err(c, d->pos, "func init must have a body");
|
|
if (d->attr != NULL)
|
|
err(c, d->pos, "func init cannot have attributes");
|
|
}
|
|
}
|
|
|
|
/* Pinned Go 1.26.5 types2 rejects a non-function package-scope `main`
|
|
* before declaring it, but only when the declared package name is `main`
|
|
* (resolver.go:90-110 declarePkgObj). WW's function entry ABI deliberately
|
|
* permits argument/result-bearing functions, so this is the independent
|
|
* declaration-kind rule: canonical identity, physical directory, path leaf,
|
|
* and root/action status are not inputs. Remove rejected declarations from
|
|
* later name installation just as the pinned resolver returns without
|
|
* declaring its object. */
|
|
static void
|
|
reject_nonfunction_main_decls(Checker *c, Node *file)
|
|
{
|
|
Node *prev = NULL;
|
|
for (Node *d = file->list; d; ) {
|
|
Node *next = d->next;
|
|
int invalid = top_decl_kind(d) && d->kind != N_FNDECL
|
|
&& d->str != NULL && strcmp(d->str, "main") == 0
|
|
&& d->pkgname != NULL && strcmp(d->pkgname, "main") == 0;
|
|
if (invalid) {
|
|
err(c, d->pos, "cannot declare main - must be func");
|
|
if (prev == NULL)
|
|
file->list = next;
|
|
else
|
|
prev->next = next;
|
|
} else
|
|
prev = d;
|
|
d = next;
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Pinned Go 1.26.5 types2 rejects an effective import binding named init and
|
|
* immediately continues before creating its PkgName. Diagnose every resolved
|
|
* occurrence at the first import-spec token, then keep it out of all binding
|
|
* recovery below while retaining the real loader-owned dependency edge. */
|
|
static void
|
|
reject_init_imports(Checker *c, Node *file)
|
|
{
|
|
for (Node *u = file->list; u; u = u->next)
|
|
if (invalid_init_import(u))
|
|
err(c, import_binding_pos(u),
|
|
"cannot import package as init - init must be a func");
|
|
}
|
|
|
|
/* 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->useblank || invalid_init_import(u)
|
|
|| u->imported || u->str == NULL)
|
|
continue;
|
|
for (Node *v = file->list; v != u; v = v->next) {
|
|
if (v->kind != N_USE || v->useblank || invalid_init_import(v)
|
|
|| 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->useblank || invalid_init_import(u)
|
|
|| 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->useblank || invalid_init_import(u)
|
|
|| 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int
|
|
check_test_target(const Checker *c, const char *path)
|
|
{
|
|
if (path == NULL) return 0;
|
|
for (int i = 0; i < c->n_test_targets; i++)
|
|
if (strcmp(c->test_targets[i], path) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
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
|
|
|| check_test_target(c, u->usepath)))
|
|
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;
|
|
}
|
|
}
|
|
reject_init_imports(c, file);
|
|
reject_nonfunction_main_decls(c, file);
|
|
mark_import_uses(c, file);
|
|
check_import_redeclarations(c, file);
|
|
check_import_usage_and_collisions(c, file);
|
|
classify_init_decls(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);
|
|
if (d->useblank)
|
|
continue;
|
|
if (invalid_init_import(d))
|
|
continue;
|
|
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;
|
|
if (d->str != NULL && strcmp(d->str, "init") == 0) 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;
|
|
if (d->str != NULL && strcmp(d->str, "init") == 0) 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);
|
|
if (d->str != NULL && strcmp(d->str, "init") == 0) {
|
|
d->type = t;
|
|
break;
|
|
}
|
|
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;
|
|
if (d->initfn) {
|
|
Type *ret = type_chase_named(t->ret);
|
|
if (d->list != NULL || ret != ty_void)
|
|
err(c, d->pos, "func init must have no arguments and no return values");
|
|
const char *base = c->package_init_symbol;
|
|
if ((base == NULL || base[0] == '\0') && !c->sep_mode)
|
|
base = "__ww..pkg.v0.r0.e.init";
|
|
if (base != NULL && base[0] != '\0')
|
|
d->linksym = aprintf(c->a, "%s.f.%llu", base,
|
|
(unsigned long long)d->initorder);
|
|
break;
|
|
}
|
|
Sym *prev = scope_lookup_local(c->cur, d->str);
|
|
const char *mod = decl_mod(file, d);
|
|
if (prev && prev->kind == SK_USE) {
|
|
/* `use mod; ... fn mod(...) ...;` — promote
|
|
* but remember the module-alias so dotted
|
|
* qualifiers (`mod.x`) keep resolving. The
|
|
* lib/fnmatch case: `fn fnmatch(...)` shadows
|
|
* the SK_USE leaf, and without use_alias the
|
|
* dot-prefix path in resolve_typename loses
|
|
* the `fnmatch.flag` lookup. */
|
|
prev->kind = SK_FN; prev->type = t; prev->decl = d;
|
|
prev->use_alias = 1;
|
|
if (mod && prev->mod == NULL) prev->mod = mod;
|
|
} else if (!scope_define_in_module(c->cur, d->str, mod,
|
|
SK_FN, t, d))
|
|
err(c, d->pos, "duplicate fn %s", d->str);
|
|
break;
|
|
}
|
|
case N_LET: {
|
|
Type *t = d->lhs ? resolve_type(c, d->lhs) : NULL;
|
|
/* #108(b): a top-level `let x: opaque` is the same
|
|
* undefined-size footgun as a local one. */
|
|
if (t)
|
|
require_sized(c, t, d->pos, "a variable");
|
|
d->type = t;
|
|
if (d->str != NULL && strcmp(d->str, "init") == 0)
|
|
break;
|
|
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
|
|
* `("<name>", &<name>)` per validated @test fn. */
|
|
Node *rhead = NULL, *rtail = NULL;
|
|
int ntest = 0;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_FNDECL)
|
|
continue;
|
|
int 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 (check_test_target(c, d->module))
|
|
alias = d->module;
|
|
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)` → <nil>).
|
|
* The general-inferred twin of the #11
|
|
* [_]-array Sym repoint above. */
|
|
Sym *s = scope_lookup_local(c->cur,
|
|
d->str);
|
|
if (s) s->type = d->type;
|
|
}
|
|
if (d->type && rt != ty_err && d->type != ty_err
|
|
&& !type_assignable(d->type, rt)
|
|
&& !arrlit_init_fits(c, d->type, d->rhs))
|
|
err(c, d->pos, "let %s init not assignable",
|
|
d->str);
|
|
/* #133: const-fold a const-EXPR rhs (N_BIN /
|
|
* unary-over-N_BIN / def-ref) to a literal so
|
|
* cgen's literal-only DATA emitter lays the row,
|
|
* mirroring the N_DEF arm above. GATED on the
|
|
* plain literal fold missing first (existing
|
|
* literal/unary-literal lets keep their node,
|
|
* byte-identical) AND the const-fold succeeding
|
|
* (a str/struct/slice/call/runtime-operand rhs
|
|
* returns 0 silently and is left untouched).
|
|
* Stamp LAST — the assignability check above
|
|
* consumes the pre-stamp cexpr type. */
|
|
/* An explicit scalar-to-tagged cast is also the carrier
|
|
* selection. Folding it to an integer literal while keeping
|
|
* the tagged result type makes cgen consume scalar registers
|
|
* as an already-wide tagged ABI value. Mutable package lets
|
|
* preserve the cast so the concrete carrier is boxed correctly;
|
|
* const remains on its established static-only path. */
|
|
Type *foldt = type_chase_named(d->type);
|
|
int preserve_tagged_cast = d->op != TK_CONST && d->rhs
|
|
&& d->rhs->kind == N_CAST && foldt != NULL
|
|
&& foldt->kind == TY_TAGGED && !foldt->nullable;
|
|
u64 dv;
|
|
if (d->rhs && !preserve_tagged_cast
|
|
&& !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;
|
|
(void)init_lower_package(c, file);
|
|
/*
|
|
* #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;
|
|
}
|
|
}
|
|
}
|