diff options
| author | 2026-10-14 10:05:40 +0000 | |
|---|---|---|
| committer | 2026-10-14 10:05:40 +0000 | |
| commit | ce920b6ff47f32bc06dd27124e1b882f82f3d6ae (patch) | |
| tree | 89f3dac0aa670294053320599a742d670c0ec5a4 /src/compiler/types.zc | |
| parent | todo: tick the parity runner; zc0 follow-ups it found (diff) | |
split compiler.zc into files by phase
src/compiler/compiler.zc is now twelve files under src/compiler/, listed in
order in src/compiler/parts. cat of the parts in order is byte-identical to
the old file, so every compiler and image except the language server's is
unchanged. no function was edited.
consumers read the list: the makefile, bootstrap-check, genmap (which still
writes one compiler.zc into a generated map directory), tinyvm.bat,
test.sh, test_win.ps1 and the lsp drivers. the language server embeds each
part and serves them joined as the one document tinyvm:/compiler.zc.
Diffstat (limited to 'src/compiler/types.zc')
| -rw-r--r-- | src/compiler/types.zc | 1003 |
1 files changed, 1003 insertions, 0 deletions
diff --git a/src/compiler/types.zc b/src/compiler/types.zc new file mode 100644 index 0000000..c643f0b --- /dev/null +++ b/src/compiler/types.zc @@ -0,0 +1,1003 @@ +// ---- types ---- + +// types are a compiler primitive: records in the type area, hash-consed so +// the same type is always the same pointer and comparing types is comparing +// words. untyped code is typed word, which goes with anything. the records +// stay in memory after compiling, so programs can inspect them. +// t[0] kind t[1] size in words t[2] name t[3] n t[4] a t[5] b t[6] c t[7..] extra +// ptr: a = element arr: a = element, n = length +// fn: a = return type, n = parameter count, b = signature (u m d tags), extra = parameter types +// struct: n = field count (-1 until laid out), a = fields (name, type, offset), +// b = generic template, c = type arguments [n, t1, t2 ...] +// var: a type parameter, n = its index +// gen: a generic struct: n = parameter count, a = fields (name, type), b = field count, c = parameter names + + +// the type area is a bump allocator of its own (tytop .. R_TYEND). talloc +// hands out zeroed words, tnew makes a record and adds it to the list of all +// types (R_TY), and the constructors below look that list up first, which is +// the hash-consing: asking for the same pointer type twice returns the same +// record. the list is searched linearly, which is fine for the few hundred +// types a program has. +int talloc(int n) { + int p = tytop; + if (tytop + n > tyend) { + p = chk_resize(0, 0, n + 16385); p[0] = typeblocks; typeblocks = p; + p++; tytop = p; tyend = p + n + 16384; + } + tytop = tytop + n; + rt.fill_w(p, 0, n); + return p; +} +// make a type record of the given kind, size and name in the type area. +word tnew(int kind, int size, char* nm, int extra) { + word t = talloc(7 + extra); + t[0] = kind; t[1] = size; t[2] = nm; + if (ntl >= typecap) { typelist = chk_resize(typelist, ntl, typecap * 2); typecap = typecap * 2; } + typelist[ntl] = t; ntl++; + return t; +} +// qualified types retain their layout and reference their unqualified record. +word tunconst(word t) { + word entry = readonly_types; + while (entry) { if (entry[1] == t) return entry[2]; entry = entry[0]; } + return t; +} +int treadonly(word t) { return t && tunconst(t) != t; } +word tconst(word t) { + word entry = readonly_types, qualified; int words = 7; + if (treadonly(t)) return t; + while (entry) { if (entry[2] == t) return entry[1]; entry = entry[0]; } + if (t[0] == TY_FN) words = words + t[3]; + qualified = tnew(t[0], t[1], t[2], words - 7); rt.copy_w(qualified, t, words); + entry = talloc(3); entry[0] = readonly_types; entry[1] = qualified; entry[2] = t; readonly_types = entry; + return qualified; +} +// writes require mutable storage, independently of optional type checking. +int require_mutable(word t) { + if (treadonly(t)) serr("const storage cannot be modified"); + return 0; +} +// the pointer type to type e, reused when it already exists. +word tptr(int e) { + int i = 0; word t; + while (i < ntl) { t = typelist[i]; if (t[0] == TY_PTR && t[4] == e && !treadonly(t)) return t; i++; } + t = tnew(TY_PTR, 1, 0, 0); t[4] = e; + return t; +} +// sized integers (i8 u8 i16 u16 i32 u32) are TY_INT records with t[5] = bytes, +// t[6] bit 0 = signed, and t[4] = the "packed view" twin: the same name with +// t[6] bit 1 set. a variable, parameter or field of a sized type is an +// ordinary word that is cut to size whenever it is stored. an element of an +// array or a pointer of a sized type is stored packed, 8, 4 or 2 to a word, +// and indexing yields the twin, whose lvalue is a byte address. +word tbytes(word t) { if (t[0] == TY_INT || t[0] == TY_FLOAT) return t[5]; return 0; } +// true for a packed integer or float type. +word ispk(word t) { return (t[0] == TY_INT || t[0] == TY_FLOAT) && (t[6] & 2); } +// the type a packed type stores its elements as, or the type itself. +word tpk(word t) { if (treadonly(t)) return tconst(tpk(tunconst(t))); if ((t[0] == TY_INT || t[0] == TY_FLOAT) && t[4]) return t[4]; return t; } +// an enhancement used by the compiler itself, inline when the runtime has it +int xenh(char* name) { + int s = lookup(intern(name)); + if (s >= 0 && skind[s] == K_ENH) { op(XOP0 + sval[s]); return 0; } + callname(intern(name)); + return 0; +} +// words an array of n elements of e takes: packed for a sized integer +word tasz(word e, int n) { + if (n < 0) return 0; + if (tbytes(e)) return (n * e[5] + 7) >> 3; + return n * tsz(e); +} +// the fixed array type of n elements of type e, reused when it already exists. +word tarr(int e, int n) { + int i = 0; word t; + while (i < ntl) { t = typelist[i]; if (t[0] == TY_ARR && t[4] == e && t[3] == n && !treadonly(t)) return t; i++; } + t = tnew(TY_ARR, tasz(e, n), 0, 0); t[4] = e; t[3] = n; + return t; +} +// a sized integer type and its packed twin +word tsized(char* name, int bytes, int sgn) { + word t = tnew(TY_INT, 1, intern(name), 0), twin = tnew(TY_INT, 1, t[2], 0); + t[5] = bytes; t[6] = sgn; t[4] = twin; + twin[5] = bytes; twin[6] = sgn | 2; + addsym(t[2], K_TYPE, t); + return t; +} +// the function type with this return, parameter list and signature. +word tfn(int ret, int np, word ps, int sig) { + int i = 0, j; word t; + while (i < ntl) { + t = typelist[i]; + if (t[0] == TY_FN && t[4] == ret && t[3] == np && t[5] == sig) { + j = 0; while (j < np && t[7 + j] == ps[j]) j++; + if (j == np) return t; + } + i++; + } + t = tnew(TY_FN, 1, 0, np); t[4] = ret; t[3] = np; t[5] = sig; + rt.copy_w(t + 7, ps, np); + return t; +} +// T[?], or fixed T[?] +word tdyn(int e, int f) { + int i = 0; word t; + while (i < ntl) { t = typelist[i]; if (t[0] == TY_DYN && t[4] == e && t[3] == f && !treadonly(t)) return t; i++; } + t = tnew(TY_DYN, 1, 0, 0); t[4] = e; t[3] = f; + return t; +} +// the type variable with index i, reused when it already exists. +word tvar(int i, char* nm) { + int k = 0; word t; + while (k < ntl) { t = typelist[k]; if (t[0] == TY_VAR && t[3] == i) return t; k++; } + t = tnew(TY_VAR, 1, nm, 0); t[3] = i; + return t; +} +// the instance of generic struct g with type arguments args[0..n). found if it +// already exists, otherwise created unlaid-out (n = -1), so that mentioning +// vec<T> inside vec<T>'s own methods does not recurse; tresolve lays it out. +word tinst(word g, word args, int n) { + int i = 0, j; word t, v; + while (i < ntl) { + t = typelist[i]; + if (t[0] == TY_STRUCT && t[5] == g) { + v = t[6]; j = 0; while (j < n && v[1 + j] == args[j]) j++; + if (j == n) return t; + } + i++; + } + v = talloc(n + 1); v[0] = n; rt.copy_w(v + 1, args, n); + t = tnew(TY_STRUCT, 0, g[2], 0); t[3] = -1; t[5] = g; t[6] = v; + return t; +} +// lay out a generic instance on first use +word tresolve(word t) { + word g, f; int n, i; word fs; int off = 0, ft; word[8] bs; + if (t[0] != TY_STRUCT || t[3] >= 0) return t; + if (t[3] == -2) { serr("a struct can't contain itself by value"); t[3] = 0; return t; } + t[3] = -2; + g = t[5]; n = g[5]; f = g[4]; + rt.fill_w(bs, 0, 8); rt.copy_w(bs, t[6] + 1, t[6][0]); + fs = talloc(n * 3 + 1); + i = 0; + while (i < n) { + ft = subst(f[i + i + 1], bs); + fs[i * 3] = f[i + i]; fs[i * 3 + 1] = ft; fs[i * 3 + 2] = off; + off = off + tsz(ft); + i++; + } + t[4] = fs; t[1] = off; t[3] = n; + return t; +} +// size of a type in words, laying out a generic instance first if it needs it +word tsz(word t) { + if (t[0] == TY_STRUCT && t[3] < 0) tresolve(t); + return t[1]; +} +// replace type parameters with what they were bound to (unbound ones become word) +word subst(word t, word bs) { + if (treadonly(t)) return tconst(subst(tunconst(t), bs)); + int k = t[0], i, n; word[24] v; word vec; + if (k == TY_VAR) { if (bs[t[3]]) return bs[t[3]]; return tword; } + if (k == TY_PTR) return tptr(subst(t[4], bs)); + if (k == TY_ARR) return tarr(subst(t[4], bs), t[3]); + if (k == TY_DYN) return tdyn(subst(t[4], bs), t[3]); + if (k == TY_FN) { + n = t[3]; + i = 0; + while (i < n) { v[i] = subst(t[7 + i], bs); i++; } + return tfn(subst(t[4], bs), n, v, t[5]); + } + if (k == TY_STRUCT && t[5]) { + vec = t[6]; + n = vec[0]; + i = 0; + while (i < n) { v[i] = subst(vec[1 + i], bs); i++; } + return tinst(t[5], v, n); + } + return t; +} +// small type predicates. an array decays to a pointer to its element where a +// value is wanted, and telem is what indexing a pointer or array yields. +word isnum(word t) { return t[0] == TY_INT || t[0] == TY_CHAR || t[0] == TY_BOOL; } +// true for an aggregate type: an array or a struct. +word isagg(word t) { return t[0] == TY_ARR || t[0] == TY_STRUCT; } +// true for a pointer or an array type. +word isptr(word t) { return t[0] == TY_PTR || t[0] == TY_ARR || t[0] == TY_BSTR; } +// true for a reference value, including growable-array handles and functions. +word isref(word t) { return isptr(t) || t[0] == TY_DYN || t[0] == TY_FN; } +// the pointer an array type turns into when used as a value. +word decay(word t) { + if (treadonly(t)) return tconst(decay(tunconst(t))); + if (t[0] == TY_ARR) return tptr(t[4]); + return t; +} +// the element type of a pointer or array, or word for anything else. +word telem(word t) { + if (treadonly(t)) return tconst(telem(tunconst(t))); + if (t[0] == TY_PTR || t[0] == TY_ARR) return t[4]; + return tword; +} +// can a value of type src go where dst is expected? +// type compatibility, used by the optional typechecker. word is compatible with +// everything (untyped code stays legal), numbers convert freely, pointers need +// the same element unless one side is untyped or void, and a literal 0 is a +// null pointer. +word compat(word dst, word src) { + if (treadonly(src) && isref(src) && !treadonly(dst)) return 0; + dst = tunconst(dst); src = tunconst(src); + if (dst == src) return 1; + if (dst == tword && src && (src == tbstr || (src[0] == TY_PTR && src[4] == tchar) || (src[0] == TY_ARR && src[4] == tchar))) return 0; + if (dst == tword || src == tword) return 1; + if (dst == tbstr || src == tbstr) return dst == tbstr && xzero; + if (dst[0] == TY_PTR && src[0] == TY_STRUCT && dst[4] == src) return 1; // an in-place struct supplies its address + if (isen(dst) || isen(src)) { // an enum takes only its own enum; an enum still becomes an int + if (isen(dst) && isen(src)) return dst == src; + if (isen(dst)) return 0; + } + if (isnum(dst) && isnum(src)) return 1; + if (isfl(dst) && isfl(src)) return 1; + src = decay(src); + if (dst[0] == TY_PTR && src[0] == TY_PTR) { + if (dst[4] == tword || src[4] == tword || dst[4] == tvoid || src[4] == tvoid) return 1; + return dst[4] == src[4]; + } + if ((dst[0] == TY_PTR || dst[0] == TY_FN || dst[0] == TY_DYN) && xzero) return 1; // a literal 0 is a null pointer + if (dst[0] == TY_DYN && src[0] == TY_DYN && dst[4] == src[4]) return dst[3] || !src[3]; // a growable array can't hold a fixed one + return dst == src; +} +// match pattern type p (which may contain type parameters) against actual type +// a, filling the bindings bs. this is how a call to a generic function infers +// T from its arguments. +word unify(word p, word a, word bs) { + int i, n; word pv, av; + if (p[0] != TY_VAR) { + if (treadonly(a) && isref(a) && !treadonly(p)) return 0; + p = tunconst(p); a = tunconst(a); + } + if (p[0] == TY_VAR) { i = p[3]; if (!bs[i]) { bs[i] = a; return 1; } return compat(bs[i], a); } + a = decay(a); + if (p[0] == TY_PTR && a[0] == TY_PTR) { if (a[4] == tword) return 1; return unify(p[4], a[4], bs); } + if (p[0] == TY_DYN && a[0] == TY_DYN) return (p[3] || !a[3]) && unify(p[4], a[4], bs); + if (p[0] == TY_FN && a[0] == TY_FN) { + if (p[3] != a[3] || ((p[5] >> 6) != (a[5] >> 6))) return 0; + i = 0; + while (i < p[3]) { + pv = subst(p[7 + i], bs); av = a[7 + i]; + if (pv != tword && av != tword && pv != av) return 0; + if (!unify(p[7 + i], av, bs)) return 0; + i++; + } + return unify(p[4], a[4], bs); + } + if (p[0] == TY_STRUCT && p[5] && a[0] == TY_STRUCT && a[5] == p[5]) { + pv = p[6]; av = a[6]; n = pv[0]; i = 0; + while (i < n) { if (!unify(pv[1 + i], av[1 + i], bs)) return 0; i++; } + return 1; + } + return compat(p, a); +} +// print a type the way it is written in source. function pointers use an alias +// for source spelling; array pointers use T[N]*. +int tprint_dimensions(word t) { + while (t[0] == TY_ARR || t[0] == TY_DYN) { rt.put_c('['); if (t[0] == TY_DYN) rt.put_c('?'); else if (t[3] >= 0) rt.put_n(t[3]); rt.put_c(']'); t = t[4]; } + return 0; +} +word tprint(word t) { + if (treadonly(t)) { rt.put_s("const "); return tprint(tunconst(t)); } + int k = t[0], i; word v; + if (k == TY_PTR) { // postfix pointers; only function pointers keep the display-only prefix form + v = t[4][0]; + if (v == TY_FN) { rt.put_c('*'); return tprint(t[4]); } + tprint(t[4]); rt.put_c('*'); + return 0; + } + if (k == TY_ARR || k == TY_DYN) { if (k == TY_DYN && t[3]) rt.put_s("fixed "); v = t; while (v[0] == TY_ARR || v[0] == TY_DYN) v = v[4]; tprint(v); return tprint_dimensions(t); } + if (k == TY_FN) { + if (t[4] == tword) rt.put_s("fn"); else tprint(t[4]); + if (t[5] & 1) rt.put_s(".u"); + rt.put_c('('); + i = 0; + while (i < t[3]) { + if (i) rt.put_s(", "); + tprint(t[7 + i]); + if (i < 12 && tparamtag(t, i) == 1) rt.put_s(".m"); + if (i < 12 && tparamtag(t, i) == 2) rt.put_s(".d"); + i++; + } + rt.put_c(')'); + return 0; + } + if (t[2]) rt.put_s(t[2]); else rt.put_s("?"); + if (k == TY_STRUCT && t[5]) { + v = t[6]; rt.put_c('<'); + i = 0; while (i < v[0]) { if (i) rt.put_s(", "); tprint(v[1 + i]); i++; } + rt.put_c('>'); + } + return 0; +} +// typechecker errors. they only fire when "typecheck on" is set, in reachable +// code, and never during a dry run. +int tcerr(int what, int want, int got) { + if (!tc_on || !live || dry || panic) return 0; + errat(); rt.put_s("type error: "); rt.put_s(what); + rt.put_s(": expected "); tprint(want); rt.put_s(", got "); tprint(got); + errend(); + return 0; +} +// report a type error when ok is false, showing the type it got. +int tcneed(int ok, int what, int got) { + if (ok || !tc_on || !live || dry || panic) return 0; + errat(); rt.put_s("type error: "); rt.put_s(what); rt.put_s(", got "); tprint(got); + errend(); + return 0; +} +// the type parameters in scope (tpn): a generic function or struct body sees +// T as a type. istypename says whether a name starts a type here, which is how +// the parser tells a declaration from an expression. +int tpfind(int n) { + int i = ntp; + while (i) { + i--; + if (tpn[i] == n) return i; + } + return -1; +} +// true when the name n is a type, a type parameter or a generic type. +word istypename(char* n) { + int s; + if (n == s_fixed || n == s_const) return 1; + if (ginst && gfind(n) >= 0) return 1; + if (tpfind(n) >= 0) return 1; + if (findloc(n) >= 0) return 0; + s = lookup(n); + return s >= 0 && skind[s] == K_TYPE; +} +// the closing > of a type argument list; splits >> in two +int closeangle() { + if (is_op(s_shr)) { tv = s_gt; return 0; } + expect(s_gt); + return 0; +} +// a type, then any number of (params) after it: int(int, int) is a function +// returning int, fn(int) one returning a plain word +// ---- parsing types ---- + +// grammar, outermost first: +// ptype a type, then any number of (params): int(int, int) is a function type +// ptype0 postfix pointers and array groups: T*, T[N], T[], T[?] +// ptype1 a type name, fixed qualifier, function base, or generic instance +// the first thing in ptype's comment applies here too: fn(int) is a function +// returning a plain word. +int ptype() { + int start = toff, t = ptype0(), np, tags, tag; word[24] ps; + while (is_op(s_lp)) { + next(); np = 0; tags = 0; + if (!is_op(s_rp)) { + while (1) { + if (np >= 24) fatal("too many parameters"); + ps[np] = ptype(); if (ps[np][0] == TY_ARR) ps[np] = decay(ps[np]); + tag = 0; + if (accept(s_dot)) { + if (tv == s_m) tag = 1; else if (tv == s_d) tag = 2; else die("callback parameter lifetime is .m or .d"); + next(); + } + tags = tags | (tag << (np + np)); + np++; + if (!accept(s_comma)) break; + } + } + expect(s_rp); + t = tfn(t, np, ps, np + np + tags * 64); + } + ht_note(start, toff, t); + return t; +} +// a type with any stars after it: int*, vec<T>*, point** +int ptype0() { + int t = ptype1(), cap = 8, n, count, j; word dims = 0; + while (is_op(s_star) || is_op(s_lk)) { + if (accept(s_star)) t = tptr(t); + else { + count = 0; + while (accept(s_lk)) { + if (accept(s_q)) { expect(s_rk); n = -2; } + else if (accept(s_rk)) n = -1; + else { n = cexpr(); expect(s_rk); if (n < 0) serr("a negative array length"); } + if (!dims || count == cap) { if (dims) cap = cap * 2; dims = chk_resize(dims, count, cap); } + dims[count] = n; count++; + } + j = count; while (j) { j--; if (dims[j] == -2) t = tdyn(t, 0); else t = tarr(t, dims[j]); } + } + } + if (dims) rt.free(dims); + return t; +} +// parse one type with no trailing function parts. +word ptype1() { + int n, s; word t; word[8] args; int na = 0, i; + if (is_op(s_lk)) { die("array types are postfix: T[N], T[] or T[?]"); return tword; } + if (is_id(s_const)) { next(); return tconst(ptype0()); } + if (is_id(s_fixed)) { // fixed T[?] can't grow; fixed T[n] says what T[n] already means + next(); t = ptype0(); + if (t[0] == TY_DYN) { if (treadonly(t)) return tconst(tdyn(t[4], 1)); return tdyn(t[4], 1); } + if (t[0] != TY_ARR) serr("only arrays can be fixed"); + return t; + } + if (is_id(s_fn)) { + next(); + if (!is_op(s_lp)) die("fn as a type needs its parameters: fn(int)"); + return tword; + } + if (tk != T_ID) { diefound("expected a type"); return tword; } + n = tv; + if (ginst) { + i = gfind(n); + if (i >= 0) { next(); return gib[i]; } + } + i = tpfind(n); + if (i >= 0) { next(); return tvar(i, n); } + s = source_lookup(n); + // T in T first<T>(...) + if (tvauto && (s < 0 || skind[s] != K_TYPE) && ntp < 8) { + tpn[ntp] = n; + lsmark(2, n); + ntp++; + next(); + return tvar(ntp - 1, n); + } + if (s < 0 || skind[s] != K_TYPE) { dien(n, "isn't a type"); return tword; } + rs_put(toff, RS_SYM, rs_sym(s), s); + next(); + t = sval[s]; + if (t[0] != TY_GEN) return t; + expect(s_lt); + while (1) { + if (na >= 8) die("too many type arguments"); + args[na] = ptype(); + if (args[na] == tvoid) serr("a type argument can't be void"); + na++; + if (!accept(s_comma)) break; + } + closeangle(); + if (na != t[3]) serrn(n, "wrong number of type arguments"); + return tinst(t, args, na); +} +// does a declaration start here? (a type, or a bare .tag) +// does a declaration start at the current token? (a type name, [, or a bare .tag) +int tstart() { + if (tk == T_OP) { + if (tv == s_lk || tv == s_dot) return 1; + return 0; + } + if (tk != T_ID) return 0; + if (tv == s_fn) return 1; + if (!istypename(tv)) return 0; + // Type.name(...) at the start of a statement is a static call, not a declaration: + // a declaration's dot is followed by a tag, u, m or d + peekat(0); + if (pk_tk == T_OP && pk_tv == s_dot) { + peekat(1); + if (pk_tk == T_ID && pk_tv != s_u && pk_tv != s_m && pk_tv != s_d) return 0; + } + return 1; +} +// [type] [.tag] +// parse a declaration's [type] [.tag] into dty and dtag +int dtype() { + dty = 0; dtag = 0; + if (!is_op(s_dot)) dty = ptype(); + if (accept(s_dot)) { + if (tv == s_m) { + dtag = S_M; + } else if (tv == s_u) { + dtag = S_U; + } else if (tv == s_d) dtag = S_D; else die("a tag is .u, .m or .d"); + next(); + } + return 0; +} +// the struct whose body or methods are being compiled, when it declares constants: a bare +// name there is looked up as Struct.name before the global scope +char* mstruct; +// does a constant declaration start in a struct body? const NAME = e; const T NAME = e; +// (a field of a const type has no initializer, so the = tells them apart) +int struct_const_start() { + if (!is_id(s_const)) return 0; + peekat(0); if (pk_tk != T_ID) return 0; + peekat(1); + if (pk_tk == T_OP && pk_tv == s_eq) return 1; + if (pk_tk == T_ID) { peekat(2); return pk_tk == T_OP && pk_tv == s_eq; } + if (pk_tk == T_OP && pk_tv == s_star) { peekat(2); if (pk_tk != T_ID) return 0; peekat(3); return pk_tk == T_OP && pk_tv == s_eq; } + return 0; +} +// const NAME = e, NAME = e; inside struct sn: a compile-time constant named Struct.NAME that takes no +// space in the layout. the optional type is read and dropped, as in gconst. +int struct_const(char* sn) { + int off, s; char* cn; + next(); + if (tk == T_ID) { peekat(0); if (pk_tk == T_ID || (pk_tk == T_OP && pk_tv == s_star)) ptype(); } + mstruct = sn; + while (1) { + off = toff; cn = name(); + s = lookup(methname(sn, cn)); + if (s >= ubase && s >= 0) serrn(cn, "is defined twice in this struct"); + expect(s_eq); + s = addsym(methname(sn, cn), K_CONST, cexpr()); + rs_put(off, RS_SYM, rs_sym(s), s); + if (!is_op(s_comma) && !is_op(s_semi)) { // a call or a variable's value follows what cprim could read + serr("a struct constant must be a constant expression"); + while (!is_op(s_semi) && !is_op(s_rb) && tk != T_EOF) next(); + break; + } + if (!accept(s_comma)) break; + } + accept(s_semi); + return 0; +} +// struct name { type field, ... } or struct name<A, B> { ... } +// the struct keyword. fields go in a scratch buffer first (name, type pairs), +// then are laid out: each field takes tsz(type) words, so offsets are words. +// methods are collected as token lists while the struct is read (grabfn) and +// compiled only when the struct is complete, so a method can use every field +// and call its siblings. a generic struct keeps its fields unresolved and +// can't have methods; each instance is laid out later by tresolve. +int kw_struct() { + char* n; word t; int s, nf = 0; word buf = chk_resize(0, 0, 64); int bufcap = 64, i; word fs; int off = 0, gen = 0, ft, nm = 0, mcap = STRUCT_METHODS; char** mn = chk_resize(0, 0, mcap); word mq = chk_resize(0, 0, mcap), mst = chk_resize(0, 0, mcap), mty = chk_resize(0, 0, mcap), mrt = chk_resize(0, 0, mcap), mrg = chk_resize(0, 0, mcap); int bo, k, stat = 0; char* fname = 0; char* msv = mstruct; + next(); + n = module_symbol(name()); + if (is_op(s_lt)) { // generic + next(); ntp = 0; + while (1) { + if (ntp >= 8) die("too many type parameters"); + tpn[ntp] = name(); + lsmark(2, tpn[ntp]); + ntp++; + if (!accept(s_comma)) break; + } + closeangle(); + gen = 1; + t = tnew(TY_GEN, 0, n, 0); t[3] = ntp; + fs = talloc(ntp); rt.copy_w(fs, tpn, ntp); t[6] = fs; + } else { t = tnew(TY_STRUCT, 0, n, 0); t[3] = -2; } // -2 while its fields are read + s = addsym(n, K_TYPE, t); + expect(s_lb); + while (!is_op(s_rb)) { + if (tk == T_EOF) { die("missing }"); break; } + stat = 0; + if (struct_const_start()) { struct_const(n); continue; } + if (is_id(s_unchecked)) { next(); stat = 2; } + if (is_id(s_static)) { // static fn name(params) { body }, or static T name(params) { body }: a function of the struct, called as Struct.name(args), with no this + next(); + stat = stat | 1; + } + if (is_id(s_fn)) { // fn name(params) { body }: a method, compiled once the struct is complete + next(); + if (nm >= mcap) { + mcap = mcap * 2; mn = chk_resize(mn, nm, mcap); mq = chk_resize(mq, nm, mcap); + mst = chk_resize(mst, nm, mcap); mty = chk_resize(mty, nm, mcap); + mrt = chk_resize(mrt, nm, mcap); mrg = chk_resize(mrg, nm, mcap); + } + mn[nm] = name(); + mst[nm] = stat; + mq[nm] = grabfn(); + mty[nm] = 0; + k = methdecl(n, mn[nm]); + if (stat & 1) sflag[k] = sflag[k] | F_STATIC; + nm++; + continue; + } + // let x, y; fields are plain words + fname = 0; + if (is_id(s_var)) { + if (stat & 1) die("static must be followed by fn or a typed method"); + next(); + ft = tword; + } else { + // T name(params) { body } is a typed method; T name, ...; is a typed field + rettype(); + fname = name(); + if (is_op(s_lp)) { + if (nm >= mcap) { + mcap = mcap * 2; mn = chk_resize(mn, nm, mcap); mq = chk_resize(mq, nm, mcap); + mst = chk_resize(mst, nm, mcap); mty = chk_resize(mty, nm, mcap); + mrt = chk_resize(mrt, nm, mcap); mrg = chk_resize(mrg, nm, mcap); + } + mn[nm] = fname; + mst[nm] = stat; + mq[nm] = grabfn(); + mty[nm] = 1; mrt[nm] = dty; mrg[nm] = dtag == S_U; + if (dtag && dtag != S_U) serr("the only return tag is .u"); + k = methdecl(n, fname); + if (stat & 1) sflag[k] = sflag[k] | F_STATIC; + nm++; + continue; + } + if (stat & 1) die("static must be followed by fn or a typed method"); + if (dtag) serr("a field has no tag"); + ft = dty; + } + while (1) { + if ((nf + 1) * 2 > bufcap) { buf = chk_resize(buf, nf * 2, bufcap * 2); bufcap = bufcap * 2; } + if (fname) { buf[nf + nf] = fname; fname = 0; } else buf[nf + nf] = name(); + lsmark(3, buf[nf + nf]); buf[nf + nf + 1] = ft; + field_note(t, buf[nf + nf], nameoff); lt_note(nameoff, ft); rs_put(nameoff, RS_FIELD, nameoff, 0); nf++; + if (!accept(s_comma)) break; + } + expect(s_semi); + } + next(); + if (gen) { + fs = talloc(nf + nf); rt.copy_w(fs, buf, nf + nf); + t[4] = fs; t[5] = nf; + mdg = ntp; rt.copy_w(mdn, tpn, ntp); + ntp = 0; + } else { + fs = talloc(nf * 3 + 1); + i = 0; + bo = 0; + while (i < nf) { + ft = buf[i + i + 1]; + if (ft == t) { serr("a struct can't contain itself by value; use a pointer"); ft = tword; } + fs[i * 3] = buf[i + i]; fs[i * 3 + 1] = ft; + // a sized field packs into the current word at its own alignment; its + // offset is recorded as minus (byte offset + 1). anything else starts a word + k = tbytes(ft); + if (k) { bo = (bo + k - 1) / k * k; fs[i * 3 + 2] = 0 - bo - 1; bo = bo + k; } + else { bo = (bo + 7) / 8 * 8; fs[i * 3 + 2] = bo / 8; bo = bo + tsz(ft) * 8; } + i++; + } + off = (bo + 7) / 8; + t[4] = fs; t[1] = off; t[3] = nf; + } + accept(s_semi); + i = 0; + while (i < nm) { fn_method_compile = 1; methdef(n, mn[i], mq[i], mst[i], mty[i], mrt[i], mrg[i]); fn_method_compile = 0; i++; } + mdg = 0; + mstruct = msv; + return s; +} +// ---- methods ---- + +// a method is an ordinary function named Struct.name whose first parameter is +// this, a pointer to the struct. e.method(a) calls Struct.name(e, a). the +// receiver's struct comes from its type, or, for an untyped word, from the +// one struct that has a method of that name. +// the symbol for a method is the interned string "Struct.name" +char* methname(char* sn, char* mn) { + char* b = chk_resize(0, 0, rt.len_w(sn) + rt.len_w(mn) + 2); int i = 0, j = 0; + while (sn[j]) { b[i] = sn[j]; i++; j++; } + b[i] = '.'; i++; + j = 0; + while (mn[j]) { b[i] = mn[j]; i++; j++; } + b[i] = 0; + return intern(b); +} +// the method called n on struct type t, as a symbol index, or -1. methwhich +// (below) does the untyped version: search every struct for one with that +// method, which is unique or ambiguous (-2). +int methof(word t, char* n) { + int s = lookup(methname(t[2], n)); + if (s >= 0 && skind[s] == K_FN) return s; + return 0 - 1; +} +// the one method called n, -1 for none, -2 for several structs having one +int methscan(char* n) { + int i = 0; word t; int s, hit = 0 - 1; + while (i < ntl) { + t = typelist[i]; + if (t[0] == TY_STRUCT && t[3] >= 0) { + s = methof(t, n); + if (s >= 0) { if (hit >= 0) return 0 - 2; hit = s; } + } + i++; + } + return hit; +} +// declare Struct.name now, so methods can call each other in any order +// (the declaration is what makes mutual recursion between methods work: the +// symbol exists with no body yet, and calls to it go on the patch chain) +int methdecl(char* sn, char* mn) { + int nm = methname(sn, mn), s = lookup(nm); + if (s >= ubase && s >= 0 && skind[s] == K_FN) { + serrn(mn, "is defined twice in this struct"); + return s; + } + s = addsym(nm, K_FN, 0); + sflag[s] = 0; + return s; +} +// the tokens of a method, from its ( to the } that closes its body, as a token list +// a method body is not parsed when the struct is read. its tokens are copied +// into a token list (kind, value, place triples) up to the matching brace, and +// methdef replays them later. the place travels with each token, so errors +// inside a method still point at the right line. +word .u grabfn() { + int cap = 768; word q = rt.new(cap); int n = 0, depth = 0; word nq; + q[0] = 0; + if (!is_op(s_lp)) { die("expected ( after the method name"); return q; } + while (tk != T_EOF) { + if (n * 3 + 4 > cap) { + nq = rt.new(cap + cap); + rt.copy_w(nq, q, cap); + rt.free(q); + q = nq; + cap = cap + cap; + } + q[1 + n * 3] = tk; q[2 + n * 3] = tv; q[3 + n * 3] = tpack(); n++; + if (is_op(s_lb)) depth++; + else if (is_op(s_rb)) { depth--; if (depth <= 0) { next(); break; } } + next(); + } + if (tk == T_EOF && depth > 0) die("missing } at the end of a method"); + q[0] = n; + return q; +} +// compile a method by feeding the parser fn Struct.name(Struct* this, rest...) +// build the token list "fn Struct.name ( Struct* this , rest..." from the +// method's saved tokens, push it as an input source and parse it as an +// ordinary top-level function. a method is therefore just a function whose +// first parameter is this; nothing else about it is special. +int tkput(word r, int k, int kind, int val, int pl) { + r[1 + k * 3] = kind; r[2 + k * 3] = val; r[3 + k * 3] = pl; + return k + 1; +} +// compile a struct method once the struct is complete. +int methdef(char* sn, char* mn, word q, int stat, int typed, int rty, int rtag) { + int n = q[0]; word r; int i, k = 0, lnp, unchecked_method = stat & 2; + stat = stat & 1; + if (!n) { rt.free(q); return 0; } + if (!stat) { + i = 1; + while (i < n && q[2 + i * 3] != s_comma && q[2 + i * 3] != s_rp) { + if (q[1 + i * 3] == T_ID && q[2 + i * 3] == s_this) serr("this is implicit and cannot be declared as a method parameter"); + i++; + } + } + r = rt.new((n + 8 + mdg * 4) * 3 + 1); + lnp = q[3]; + if (unchecked_method) k = tkput(r, k, T_ID, s_unchecked, lnp); + k = tkput(r, k, T_ID, s_fn, lnp); + r[1 + k * 3] = T_ID; r[2 + k * 3] = methname(sn, mn); r[3 + k * 3] = lnp; k++; + if (mdg) { // a method of a generic struct is a generic function over the struct's parameters: fn S.m<K, V>(S<K, V>* this, ...) + k = tkput(r, k, T_OP, s_lt, lnp); + i = 0; + while (i < mdg) { + if (i) k = tkput(r, k, T_OP, s_comma, lnp); + k = tkput(r, k, T_ID, mdn[i], lnp); + i++; + } + k = tkput(r, k, T_OP, s_gt, lnp); + } + r[1 + k * 3] = q[1]; r[2 + k * 3] = q[2]; r[3 + k * 3] = q[3]; k++; + if (stat && mdg) die("a static function can't be in a generic struct"); + if (!stat) { + r[1 + k * 3] = T_ID; r[2 + k * 3] = sn; r[3 + k * 3] = lnp; k++; + if (mdg) { + k = tkput(r, k, T_OP, s_lt, lnp); + i = 0; + while (i < mdg) { + if (i) k = tkput(r, k, T_OP, s_comma, lnp); + k = tkput(r, k, T_ID, mdn[i], lnp); + i++; + } + k = tkput(r, k, T_OP, s_gt, lnp); + } + r[1 + k * 3] = T_OP; r[2 + k * 3] = s_star; r[3 + k * 3] = lnp; k++; + r[1 + k * 3] = T_ID; r[2 + k * 3] = s_this; r[3 + k * 3] = lnp; k++; + } + if (!stat && n > 1 && !(q[1 + 3] == T_OP && q[2 + 3] == s_rp)) { + r[1 + k * 3] = T_OP; + r[2 + k * 3] = s_comma; + r[3 + k * 3] = lnp; + k++; + } + i = 1; + while (i < n) { + r[1 + k * 3] = q[1 + i * 3]; + r[2 + k * 3] = q[2 + i * 3]; + r[3 + k * 3] = q[3 + i * 3]; + k++; + i++; + } + r[0] = k; + rt.free(q); + if (typed && mdg) { + // retain a typed generic method as ordinary typed function tokens. + int mode = rt.outmode, length = rt.outn, capacity = rt.outcap, prefix = unchecked_method != 0; + word output = rt.outbuf, grow = rt.outgrow, buffer = rt.new(4096), return_tokens, joined; + rt.outmode = 2; rt.outbuf = m*buffer; rt.outn = 0; rt.outcap = 4095; rt.outgrow = 0; + tprint(rty); if (rtag) rt.put_s(".u"); buffer[rt.outn] = 0; + rt.outmode = mode; rt.outbuf = output; rt.outn = length; rt.outcap = capacity; rt.outgrow = grow; + return_tokens = tokens(buffer); + int token = 0, variable; + while (token < return_tokens[0]) { + variable = 0; + while (variable < ntl) { + word descriptor = typelist[variable]; + if (descriptor[0] == TY_VAR && descriptor[3] < mdg && return_tokens[1 + token * 3] == T_ID && return_tokens[2 + token * 3] == descriptor[2]) return_tokens[2 + token * 3] = mdn[descriptor[3]]; + variable++; + } + token++; + } + joined = rt.new((r[0] + return_tokens[0]) * 3 + 1); + joined[0] = r[0] - 1 + return_tokens[0]; + if (prefix) rt.copy_w(joined + 1, r + 1, 3); + rt.copy_w(joined + 1 + prefix * 3, return_tokens + 1, return_tokens[0] * 3); + rt.copy_w(joined + 1 + (prefix + return_tokens[0]) * 3, r + 1 + (prefix + 1) * 3, (r[0] - prefix - 1) * 3); + rt.free(r); r = joined; rt.free(return_tokens); rt.free(buffer); + } + src_push(1, r, 0, 1); + next(); + if (typed && !mdg) { + // a typed method: skip the fn and the name, then compile from the ( with the declared return type + next(); + next(); + ifkp = tpack(); ifkk = 0 - 1; if (isp != 0) ifkk = 0; + o3nxt = 0; ntp = 0; + fndone(fndef(1, rty, rtag, methname(sn, mn)), 0); + } else topitem(); + if (tk != T_EOF) die("unexpected tokens after a method"); + src_pop(); + return 0; +} +// the call e.m(args): the receiver is on the stack and becomes this +// e.m(args): by now the receiver is on the stack, so it simply becomes the +// first argument. the call is checked against the method's signature if it +// has been compiled, and its result class is fresh if the method returns .u. +int methcall(int s, word rst, word receiver_fact, int receiver, word receiver_mask) { + af_forget(); + int np, tags = 0, ret = 0, temporary, owner, aliases, i; word ft = stype[s]; word[8] bs; word owner_fact, origins; + word requirements = effect_requirements, edges = effect_edges; int original = s; + effect_args_mask(s, 0, receiver_mask); + if (receiver_fact && (receiver_fact[2] == 4 || receiver_fact[2] == 5)) effect_require(s, 0, callpos); + if (receiver >= 0 && tracked(lroot(receiver))) effect_require_mode(s, 0, callpos, 1); + if (sflag[s] & F_SIG) { tags = ssig[s] >> 6; ret = ssig[s] & 1; } + else sflag[s] = sflag[s] | F_UNTYPED; + if (sflag[s] & F_RETOWN) ret = 1; + rt.fill_w(bs, 0, 8); + if (ft && rst) { + word actual = rst; if (rst[0] == TY_STRUCT) actual = tptr(rst); + if (!unify(ft[7], actual, bs)) tcerr("receiver", subst(ft[7], bs), actual); + } + ifcall = s; effect_argument_base = 1; + np = args(tags >> 2, ft, bs, 0) + 1; + temporary = argtemporary || (receiver_fact && receiver_fact[2] == 5); owner = argowner; owner_fact = argowner_fact; origins = argorigin_mask; + if (effect_get(s, 0) & (EF_ALIAS | EF_IDENTITY | EF_UNKNOWN)) { + origins = origins | receiver_mask; + if (owner < 0) { owner = receiver; owner_fact = receiver_fact; } + else if (owner != receiver) { owner = -1; owner_fact = 0; } + } + if ((sflag[s] & F_SIG) && np != ((ssig[s] >> 1) & 31)) serrn(sname[s], "wrong number of arguments"); + if (sflag[s] & F_GEN) { s = ginstance(s, bs); effect_specialize(original, s, requirements, edges); } + fnref(s); op(O_CALL); + lv = 0; + lvk = LV_NONE; + xzero = 0; + xbor = -1; + xconst = 0; + xfloat = 0; + xstr = 0; + xlenloc = -1; + xstamp = 0; + xty = tword; + if (ft) xty = subst(ft[4], bs); + if (isfl(xty)) xfloat = 1; + effect_value = 0; + if (!ret && (isptr(xty) || xty == tword)) { + effect_value = origins; + if (isptr(xty)) xbor = owner; + if (temporary) { aliases = 0; i = 0; while (i < np) { aliases = aliases | effect_get(s, i); i++; } if (aliases & (EF_ALIAS | EF_UNKNOWN)) xaf = af_new(-1, 5, callpos); } + } + if (ret) return C_FRESH; + return C_PLAIN; +} +// type name = T; +// the type keyword makes an alias, which is also how a pointer to an array or +// function type gets a name. +int kw_type() { + int n; + next(); + n = name(); + expect(s_eq); + addsym(n, K_TYPE, ptype()); + expect(s_semi); + return 0; +} +// does a parameter start with a type? a type name straight before , or ) is the parameter's +// own name (fn f(string)), not a type, since a typed parameter always has a name after it. +int paramstart() { + if (tk == T_OP) return tv == s_lk; + if (tk != T_ID) return 0; + if (tv == s_fn) return 1; + if (!istypename(tv)) return 0; + peek(); + return !(pk_tk == T_OP && (pk_tv == s_comma || pk_tv == s_rp)); +} +// does a typedef start here? typedef followed by a name is one, so typedef can still be +// a variable or a function's name. +int typedef_ahead() { + if (tk != T_ID || tv != s_typedef) return 0; + peekat(0); + return pk_tk == T_ID; +} +// unlink the symbol s from its name, leaving the rest of the name's chain as it was. +int td_unlink(int s) { + char* nm = sname[s]; int i = nm[-1] - 1; + if (i == s) { nm[-1] = snext[s]; return 0; } + while (i >= 0 && snext[i] - 1 != s) i = snext[i] - 1; + if (i >= 0) snext[i] = snext[s]; + return 0; +} +// end the scope of the local typedefs made at block depth depth or deeper in this +// compile context (and of any made in a context nested inside it). +int td_end(int depth) { + int k; + while (ntd) { + k = ntd * 3 - 3; + if (tdstk[k + 2] < ctxsp || (tdstk[k + 2] == ctxsp && tdstk[k + 1] < depth)) break; + td_unlink(tdstk[k]); + ntd--; + } + return 0; +} +// is the name q (as a symbol is named) already a typedef of the block being compiled? +int td_here(char* q) { + int k = ntd; + while (k) { + k--; + if (tdstk[k * 3 + 2] != ctxsp || tdstk[k * 3 + 1] != fdepth) break; + if (sname[tdstk[k * 3]] == q) return 1; + } + return 0; +} +// typedef T name; +// the C spelling of type name = T;. name becomes another way to write T: the very same +// type record, so the two are interchangeable everywhere. at the top level the name is +// global; in a function it lasts to the end of its block, like a local. a name that is +// already in use is an error, except the same type under the same name again. a tag +// (.u, .m, .d) belongs to a declaration, not to a type, so a typedef has none. +int kw_typedef() { + char* n; char* q; word t; int s, at, clash = 0, local = infn; + next(); + t = ptype(); + if (panic) return 0; + if (is_op(s_dot)) { die("a typedef names a type, which has no tag: .u, .m and .d go on a declaration"); return 0; } + at = toff; n = name(); + if (panic) return 0; + q = module_symbol(n); + if (local) { + s = findloc(n); + if ((s >= 0 && lhome[s] >= fdepth) || td_here(q)) clash = 1; + } else { + s = lookup_raw(q); + if (s < 0 && module_name) { s = lookup_raw(n); if (s >= 0 && skind[s] != K_TYPE && skind[s] != K_KW) s = 0 - 1; } + if (s >= 0 && skind[s] == K_TYPE && sval[s] == t) return expect(s_semi); // the same typedef again + if (s >= 0) clash = 1; + } + if (clash) serrn(n, "already defined"); + else { + s = addsym(n, K_TYPE, t); + rs_put(at, RS_SYM, rs_sym(s), s); + if (local) { + if (ntd == tdcap) { if (tdcap) tdcap = tdcap * 2; else tdcap = 16; tdstk = chk_resize(tdstk, ntd * 3, tdcap * 3); } + tdstk[ntd * 3] = s; tdstk[ntd * 3 + 1] = fdepth; tdstk[ntd * 3 + 2] = ctxsp; ntd++; + } + } + expect(s_semi); + return 0; +} +// (T) or (T*) ahead of the current '(', within the lookahead we have +// the parser sees ( and has to decide between a parenthesized expression and a +// cast. a cast is ( type stars ) so peek ahead through the stars to the ). +int caststart() { + int k; + peek(); + if (pk_tk == T_ID && (istypename(pk_tv) || pk_tv == s_fn)) { + k = 1; peekat(k); + if (pk_tk == T_OP && (pk_tv == s_lk || pk_tv == s_lt || (pk_tv == s_lp && pq_tv[0] == s_fn))) return 1; + while (pk_tk == T_OP && pk_tv == s_star && k < 3) { k++; peekat(k); } // (int*)x, (node**)x + return pk_tk == T_OP && pk_tv == s_rp; + } + return 0; +} |
