wcc: nullable pointer folding for (*T | void)
A tagged union with exactly one `*T` variant and one `void` variant collapses to a single 8-byte pointer slot, where the null bit pattern is the void variant and any non-null is the *T variant. Mirrors Hare's `(*T | null)` ABI optimisation. Detected in resolve_type when the post-flatten variant list has exactly two entries of the right shape; Type.nullable = 1 and size = 8. Codegen branches every tagged-handling site on the flag: - match: discriminator = pointer-vs-zero, not slot+0 tag word. Binding for the *T case copies the same word (the pointer itself) rather than slot+8. - is/as: same ptr-vs-zero discriminator. - ?: null = error (propagate AX=0 to caller's matching null encoding); non-null = success (AX is already the pointer). - !: null aborts; non-null falls through with AX = pointer. - let-init / return: spill or set just AX (no tag/value pair). - call-arg push: push only AX, not the now-unused DX/CX. Prologue spill already pulled size/8 = 1 arg register via the existing tagged-arg loop, so no change needed there. Two existing helpers in cgen.c get nullable-aware spelling: type_isnullable() and nullable_ptr_tag() (which variant index is the *T side; the void side is the other one). The Hare-style `(*T | null)` spelling isn't supported — `null` is not a type keyword in ww. Callers use `void` instead, which is already a real type. The result is the same bit-level layout.
This commit is contained in:
236
cmd/w6c/cgen.c
236
cmd/w6c/cgen.c
@@ -118,8 +118,9 @@ struct_arg_size(Type *t)
|
||||
}
|
||||
|
||||
/* Tagged-union arg byte size: 16 (8B variants) or 24 (16B variants).
|
||||
* Both fit in our 3-register classification. Returns 0 if not a
|
||||
* tagged union or too large to pass in registers. */
|
||||
* Nullable-folded `(*T | void)` collapses to 8 bytes (just the
|
||||
* pointer). Returns 0 if not a tagged union or too large to pass
|
||||
* in registers. */
|
||||
static int
|
||||
tagged_arg_size(Type *t)
|
||||
{
|
||||
@@ -130,6 +131,32 @@ tagged_arg_size(Type *t)
|
||||
return (int)t->size;
|
||||
}
|
||||
|
||||
/* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */
|
||||
static int
|
||||
type_isnullable(Type *t)
|
||||
{
|
||||
if (t == NULL) return 0;
|
||||
if (t->kind == TY_NAMED) t = t->under;
|
||||
return t && t->kind == TY_TAGGED && t->nullable;
|
||||
}
|
||||
|
||||
/* nullable_ptr_tag — index of the *T variant in a nullable union.
|
||||
* Returns 0 or 1; the void variant takes the other slot. */
|
||||
static int
|
||||
nullable_ptr_tag(Type *t)
|
||||
{
|
||||
if (t == NULL) return 0;
|
||||
if (t->kind == TY_NAMED) t = t->under;
|
||||
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
||||
int i = 0;
|
||||
for (Tparam *p = t->params; p; p = p->next, i++) {
|
||||
Type *pu = (p->type && p->type->kind == TY_NAMED)
|
||||
? p->type->under : p->type;
|
||||
if (pu && pu->kind == TY_PTR) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
node_istaggedarg(Node *n)
|
||||
{
|
||||
@@ -1713,11 +1740,14 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
} else if (node_istaggedarg(args[i])) {
|
||||
/* Tagged-return ABI: AX=tag, DX=val0[, CX=val1].
|
||||
* Push high-to-low so pop drains tag first (into
|
||||
* arg-reg[0]), then values into arg-reg[1..]. */
|
||||
* arg-reg[0]), then values into arg-reg[1..].
|
||||
* Nullable (sz=8): AX holds the pointer, no
|
||||
* value-word registers — push just AX. */
|
||||
int sz = tagged_arg_size(args[i]->type);
|
||||
if (sz > 16)
|
||||
ins1(c, A_PUSHQ, areg(D_CX));
|
||||
ins1(c, A_PUSHQ, areg(D_DX));
|
||||
if (sz > 8)
|
||||
ins1(c, A_PUSHQ, areg(D_DX));
|
||||
ins1(c, A_PUSHQ, areg(D_AX));
|
||||
} else {
|
||||
ins1(c, A_PUSHQ, areg(D_AX));
|
||||
@@ -1838,10 +1868,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
*
|
||||
* Slot layout: [+0]=tag, [+8]=value0, [+16]=value1. The third
|
||||
* word is only meaningful for variants whose payload is >8B
|
||||
* (e.g. str). Bindings sized 16B (str) copy two words. */
|
||||
* (e.g. str). Bindings sized 16B (str) copy two words.
|
||||
*
|
||||
* Nullable folded `(*T | void)`: slot is one 8B word holding
|
||||
* the pointer; null IS the void variant. Discriminator =
|
||||
* value, not a separate tag. */
|
||||
Node *s = n->lhs;
|
||||
Type *st = s ? s->type : NULL;
|
||||
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
||||
int is_nullable = type_isnullable(st);
|
||||
int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16;
|
||||
int sl_off;
|
||||
if (s->kind == N_IDENT) {
|
||||
@@ -1877,15 +1912,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
|
||||
* a scratch slot so we can index out the tag/value. The
|
||||
* call ABI for tagged returns is AX=tag, DX=value0,
|
||||
* CX=value1 — copy each word into the slot. */
|
||||
* CX=value1 — copy each word into the slot. Nullable
|
||||
* returns are single-word: AX is the pointer; spill
|
||||
* only that. */
|
||||
sl_off = localoff(c, &locals, "@match_spill", slot_size,
|
||||
cg_frame);
|
||||
cgexpr(c, s, locals);
|
||||
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
||||
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, sl_off + 8));
|
||||
if (slot_size > 16)
|
||||
ins2(c, A_MOVQ, areg(D_CX),
|
||||
amem(D_BP, sl_off + 16));
|
||||
if (!is_nullable) {
|
||||
ins2(c, A_MOVQ, areg(D_DX),
|
||||
amem(D_BP, sl_off + 8));
|
||||
if (slot_size > 16)
|
||||
ins2(c, A_MOVQ, areg(D_CX),
|
||||
amem(D_BP, sl_off + 16));
|
||||
}
|
||||
}
|
||||
char *end = mklabel(c, "match_end");
|
||||
for (Node *cs = n->list; cs; cs = cs->next) {
|
||||
@@ -1893,7 +1933,18 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
if (cs->type != NULL) {
|
||||
int tag = cg_tag_for_variant(su, cs->type);
|
||||
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
||||
if (cs->list != NULL) {
|
||||
if (is_nullable) {
|
||||
/* discriminator = pointer-vs-null.
|
||||
* *T variant: skip if ptr == 0.
|
||||
* void variant: skip if ptr != 0. */
|
||||
int ptr_tag = nullable_ptr_tag(su);
|
||||
int want_ptr = (tag == ptr_tag);
|
||||
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
||||
if (want_ptr)
|
||||
ins1(c, A_JE, abranch(next));
|
||||
else
|
||||
ins1(c, A_JNE, abranch(next));
|
||||
} else if (cs->list != NULL) {
|
||||
/* Multi-pattern `case T1 | T2 | ... =>`:
|
||||
* if the tag matches any of the alts,
|
||||
* jump to body; otherwise to the next
|
||||
@@ -1923,16 +1974,34 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
Type *bt = cs->type;
|
||||
Type *bu = (bt && bt->kind == TY_NAMED)
|
||||
? bt->under : bt;
|
||||
int bsz = (bu && bu->kind == TY_STR) ? 16 : 8;
|
||||
int voff = localoff(c, &locals, cs->str, bsz,
|
||||
cg_frame);
|
||||
int nwords = (bsz + 7) / 8;
|
||||
for (int w = 0; w < nwords; w++) {
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, sl_off + 8 + 8*w),
|
||||
areg(D_AX));
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BP, voff + 8*w));
|
||||
if (is_nullable) {
|
||||
/* Bind *T or void to a local. The
|
||||
* value IS the slot's pointer word; no
|
||||
* payload to copy. void binding is
|
||||
* unusable (size 0), so only emit for
|
||||
* the *T variant. */
|
||||
if (bu && bu->kind == TY_PTR) {
|
||||
int voff = localoff(c, &locals,
|
||||
cs->str, 8, cg_frame);
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, sl_off + 0),
|
||||
areg(D_AX));
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BP, voff));
|
||||
}
|
||||
} else {
|
||||
int bsz = (bu && bu->kind == TY_STR)
|
||||
? 16 : 8;
|
||||
int voff = localoff(c, &locals, cs->str,
|
||||
bsz, cg_frame);
|
||||
int nwords = (bsz + 7) / 8;
|
||||
for (int w = 0; w < nwords; w++) {
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, sl_off + 8 + 8*w),
|
||||
areg(D_AX));
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BP, voff + 8*w));
|
||||
}
|
||||
}
|
||||
}
|
||||
cgstmt(c, cs->body, &locals, cg_frame);
|
||||
@@ -1948,12 +2017,29 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
* current function's return (with a tag remap to the
|
||||
* enclosing fn's variant order). On success, unwrap to the
|
||||
* success-variant ABI: ≤8B values in AX; str values in
|
||||
* (AX=ptr, BX=len). */
|
||||
* (AX=ptr, BX=len).
|
||||
*
|
||||
* Nullable: AX is the pointer; *T variant is the success
|
||||
* (any non-null), void variant is the error (null). The
|
||||
* enclosing fn's null encoding is the same — RET with AX=0
|
||||
* if propagating; otherwise leave AX as-is on success. */
|
||||
cgexpr(c, n->lhs, locals);
|
||||
Type *u = n->lhs ? n->lhs->type : NULL;
|
||||
if (u && u->kind == TY_NAMED) u = u->under;
|
||||
Type *r = cg_ret_type;
|
||||
if (r && r->kind == TY_NAMED) r = r->under;
|
||||
if (u && u->kind == TY_TAGGED && u->nullable) {
|
||||
char *cont = mklabel(c, "tryprop_ok");
|
||||
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
||||
ins1(c, A_JNE, abranch(cont));
|
||||
/* null = error: propagate. AX already 0; matches
|
||||
* the enclosing nullable encoding if it has one. */
|
||||
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
||||
ins1(c, A_POPQ, areg(D_BP));
|
||||
ins0(c, A_RET);
|
||||
label(c, cont);
|
||||
break;
|
||||
}
|
||||
int s_tag = cg_tagged_success_tag(u);
|
||||
Type *succ_t = NULL;
|
||||
if (u && u->kind == TY_TAGGED) {
|
||||
@@ -1992,10 +2078,22 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
break;
|
||||
}
|
||||
case N_TRYUNW: {
|
||||
/* On error variant: exit(1) directly via the syscall. */
|
||||
/* On error variant: exit(1) directly via the syscall.
|
||||
* Nullable: null = error; non-null = success (AX is the
|
||||
* pointer, ready to use). */
|
||||
cgexpr(c, n->lhs, locals);
|
||||
Type *u = n->lhs ? n->lhs->type : NULL;
|
||||
if (u && u->kind == TY_NAMED) u = u->under;
|
||||
if (u && u->kind == TY_TAGGED && u->nullable) {
|
||||
char *cont = mklabel(c, "tryunw_ok");
|
||||
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
||||
ins1(c, A_JNE, abranch(cont));
|
||||
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
||||
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
||||
ins0(c, A_SYSCALL);
|
||||
label(c, cont);
|
||||
break;
|
||||
}
|
||||
int s_tag = cg_tagged_success_tag(u);
|
||||
Type *succ_t = NULL;
|
||||
if (u && u->kind == TY_TAGGED) {
|
||||
@@ -2018,19 +2116,28 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
}
|
||||
case N_TYPETEST: {
|
||||
/* `e is T` — Compare scrutinee tag against T's variant index.
|
||||
* Result is bool (0/1) in AX. We only need the tag, not the
|
||||
* value words, so cgexpr's AX-only result is enough for both
|
||||
* register-returning callers (AX=tag) and N_IDENT locals
|
||||
* (cgexpr loads slot+0 → AX, which is the tag). */
|
||||
* Result is bool (0/1) in AX. Nullable: discriminator is
|
||||
* pointer-vs-null, not a tag. */
|
||||
cgexpr(c, n->lhs, locals);
|
||||
Type *u = n->lhs ? n->lhs->type : NULL;
|
||||
if (u && u->kind == TY_NAMED) u = u->under;
|
||||
Type *vt = n->rhs ? n->rhs->type : NULL;
|
||||
int tag = cg_tag_for_variant(u, vt);
|
||||
char *ne = mklabel(c, "is_ne");
|
||||
char *done = mklabel(c, "is_done");
|
||||
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
||||
ins1(c, A_JNE, abranch(ne));
|
||||
if (u && u->kind == TY_TAGGED && u->nullable) {
|
||||
int tag = cg_tag_for_variant(u, vt);
|
||||
int ptr_tag = nullable_ptr_tag(u);
|
||||
int want_ptr = (tag == ptr_tag);
|
||||
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
||||
if (want_ptr)
|
||||
ins1(c, A_JE, abranch(ne));
|
||||
else
|
||||
ins1(c, A_JNE, abranch(ne));
|
||||
} else {
|
||||
int tag = cg_tag_for_variant(u, vt);
|
||||
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
||||
ins1(c, A_JNE, abranch(ne));
|
||||
}
|
||||
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
||||
ins1(c, A_JMP, abranch(done));
|
||||
label(c, ne);
|
||||
@@ -2047,12 +2154,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
* the value lives at slot+8/+16 — cgexpr's single-MOVQ path
|
||||
* does not load it. Mirror match's pattern: resolve a slot
|
||||
* offset (existing local or a fresh @asrt_spill) and index
|
||||
* out tag/value from memory. */
|
||||
* out tag/value from memory.
|
||||
*
|
||||
* Nullable: the slot's word IS the pointer. *T variant
|
||||
* asserts non-null; void variant asserts null. The value
|
||||
* left in AX after the check is the pointer itself. */
|
||||
Node *s = n->lhs;
|
||||
Type *st = s ? s->type : NULL;
|
||||
Type *u = (st && st->kind == TY_NAMED) ? st->under : st;
|
||||
Type *vt = n->type;
|
||||
int tag = cg_tag_for_variant(u, vt);
|
||||
int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16;
|
||||
int sl_off = 0;
|
||||
if (s && s->kind == N_IDENT && s->str) {
|
||||
@@ -2063,12 +2173,35 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
slot_size, cg_frame);
|
||||
cgexpr(c, s, locals);
|
||||
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
||||
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, sl_off + 8));
|
||||
if (slot_size > 16)
|
||||
ins2(c, A_MOVQ, areg(D_CX),
|
||||
amem(D_BP, sl_off + 16));
|
||||
if (!(u && u->kind == TY_TAGGED && u->nullable)) {
|
||||
ins2(c, A_MOVQ, areg(D_DX),
|
||||
amem(D_BP, sl_off + 8));
|
||||
if (slot_size > 16)
|
||||
ins2(c, A_MOVQ, areg(D_CX),
|
||||
amem(D_BP, sl_off + 16));
|
||||
}
|
||||
}
|
||||
char *ok = mklabel(c, "asrt_ok");
|
||||
if (u && u->kind == TY_TAGGED && u->nullable) {
|
||||
int tag = cg_tag_for_variant(u, vt);
|
||||
int ptr_tag = nullable_ptr_tag(u);
|
||||
int want_ptr = (tag == ptr_tag);
|
||||
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
||||
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
||||
if (want_ptr)
|
||||
ins1(c, A_JNE, abranch(ok));
|
||||
else
|
||||
ins1(c, A_JE, abranch(ok));
|
||||
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
||||
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
||||
ins0(c, A_SYSCALL);
|
||||
label(c, ok);
|
||||
/* AX already holds the pointer (or 0 for void
|
||||
* variant, where the result type has size 0 and
|
||||
* no consumer reads it). */
|
||||
break;
|
||||
}
|
||||
int tag = cg_tag_for_variant(u, vt);
|
||||
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
||||
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
||||
ins1(c, A_JE, abranch(ok));
|
||||
@@ -2547,10 +2680,19 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
* 2) rhs is a bare variant value (e.g. `let r: (i64|i32) = 7`).
|
||||
* Synthesise the tag from rhs's static type and store it
|
||||
* alongside the value. str-typed rhs flows as
|
||||
* (AX=ptr, BX=len) so we store both halves. */
|
||||
* (AX=ptr, BX=len) so we store both halves.
|
||||
*
|
||||
* Nullable folded `(*T | void)`: the slot is a single 8B
|
||||
* pointer. Both the value-from-call and bare-variant paths
|
||||
* simplify to "spill AX". void variant stores 0; *T variant
|
||||
* stores the pointer. */
|
||||
if (n->rhs && lu && lu->kind == TY_TAGGED) {
|
||||
Type *rt = n->rhs->type;
|
||||
if (type_istagged(rt)) {
|
||||
if (lu->nullable) {
|
||||
cgexpr(c, n->rhs, *locals);
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BP, off + 0));
|
||||
} else if (type_istagged(rt)) {
|
||||
cgexpr(c, n->rhs, *locals);
|
||||
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
||||
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8));
|
||||
@@ -2780,9 +2922,15 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
Type *rt = cg_ret_type;
|
||||
if (rt->kind == TY_NAMED) rt = rt->under;
|
||||
if (rt && rt->kind == TY_TAGGED) {
|
||||
int tag = cg_tag_for_variant(rt, ty_void);
|
||||
if (tag < 0) tag = 0;
|
||||
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
||||
if (rt->nullable) {
|
||||
/* bare `return;` is the void/null
|
||||
* variant: emit AX = 0. */
|
||||
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
||||
} else {
|
||||
int tag = cg_tag_for_variant(rt, ty_void);
|
||||
if (tag < 0) tag = 0;
|
||||
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
||||
}
|
||||
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
||||
ins1(c, A_POPQ, areg(D_BP));
|
||||
ins0(c, A_RET);
|
||||
@@ -2795,7 +2943,11 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
if (rt && rt->kind == TY_TAGGED) {
|
||||
Type *vt = n->lhs->type;
|
||||
cgexpr(c, n->lhs, *locals);
|
||||
if (!type_istagged(vt)) {
|
||||
if (rt->nullable) {
|
||||
/* AX already holds the pointer (or
|
||||
* 0 if the value was `nil` / `void`).
|
||||
* No tag word, no shuffle. */
|
||||
} else if (!type_istagged(vt)) {
|
||||
int tag = cg_tag_for_variant(rt, vt);
|
||||
if (type_isstr(vt)) {
|
||||
/* AX=ptr, BX=len → DX=ptr,
|
||||
|
||||
@@ -247,6 +247,28 @@ resolve_type(Checker *c, Node *n)
|
||||
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 TY_VOID. */
|
||||
if (nv == 2) {
|
||||
Tparam *a = head;
|
||||
Tparam *b = head->next;
|
||||
Type *au = (a->type && a->type->kind == TY_NAMED)
|
||||
? a->type->under : a->type;
|
||||
Type *bu = (b->type && b->type->kind == TY_NAMED)
|
||||
? b->type->under : b->type;
|
||||
int aptr = au && au->kind == TY_PTR;
|
||||
int bptr = bu && bu->kind == TY_PTR;
|
||||
int avoid = au && au->kind == TY_VOID;
|
||||
int bvoid = bu && bu->kind == TY_VOID;
|
||||
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
|
||||
|
||||
@@ -408,6 +408,10 @@ struct Type {
|
||||
* through NAMED aliases. Variants with
|
||||
* iserror=1 are the propagation target of
|
||||
* the `?` operator. */
|
||||
int nullable;/* TY_TAGGED with exactly `(*T | void)` —
|
||||
* stored as a single 8-byte pointer; null
|
||||
* is the void variant. Mirrors Hare's
|
||||
* `(*T | null)` folding. */
|
||||
};
|
||||
|
||||
extern Type *ty_void, *ty_bool, *ty_rune;
|
||||
|
||||
Reference in New Issue
Block a user