aboutsummaryrefslogtreecommitdiff
path: root/src/runtime/rt.zc
diff options
context:
space:
mode:
authorGravatar Moon Flower <moonflower@elhenney.local> 2026-10-14 08:29:46 +0330
committerGravatar Moon Flower <moonflower@elhenney.local> 2026-10-14 08:29:46 +0330
commit1c53304d03414890af0da7432d1aea87e754a14a (patch)
tree97190d86cf548c9bc63f68c86a4d2a4f09809713 /src/runtime/rt.zc
parenta (diff)
feat: grow memory limits, add icon/actor libraries, overhaul LSP diagnostics
Compiler and VM - make every memory map limit dynamic and adjustable at both build time and runtime, including compiler tables, stacks, and every execution region - reserve and grow tables, operand and return stacks, frame segments, and image-building output without relocating live code or globals - rename i/o and string helpers consistently (puts -> put_s, putn -> put_n, scat -> cat_s) across compiler, runtime, examples, tests, and docs - recast and consume values assigned to globals from inside functions - allow fixed and const together, add typed closure returns (fn:t(...)), multi-include syntax (include rt, run, core;), and recurse type inference - never infer string literals as word, anywhere, as argument, variable, or return type - draft docs/BORROW.md: a borrow checker less intrusive than Rust's but more intelligent LSP and editor integration - require explicit includes and give every toolchain area its own namespace (rt, lsp, run, core) - hover for built-in types, inferred types, values known at hover time, array and pointer lifetime/size, library names in include statements, and inferred lifetimes with explanations - honor /* */ comments as documentation for variables and arguments, and stop misattributing section headers or trailing comments as variable hints - stream diagnostics and cancel stale requests on change - jump to definition and references, including cross-file - quick fixes with automatic library import, and namespace completion Hints and diagnostics - render hints as blue squigglies by default, with hints on/small/off per file - suppress magic-number hints via a preceding /* */ comment, and say so - warn on unused arguments; allow multiple _ discards in parameters, pattern matching, and tagged enums, warning if a _ is actually used - rewrite hint and comment prose in professional compiler-doc register Libraries - add icon.zc: run-time self-modification, find/replace syntax, type and block edits, and step-through inspection of locals and globals - add actor.zc: async actors with Actor<I,O>, implicit this, spawn/recv/send, injected into the compiler machinery at include time rather than built in Tooling and docs - reorganize sources, tests, and designs into subdirectories - unify windows entry points in tinyvm.bat and add a matching tinyvm.sh - add --help across the toolchain - polish the learning guide, stdlib docs, and design notes Tests - add accept, reject, and regression coverage across the compiler, LSP, - runtime, icon, actors, threads, formatting, and the toolchain contracts, plus fuzzers and property tests
Diffstat (limited to 'src/runtime/rt.zc')
-rw-r--r--src/runtime/rt.zc512
1 files changed, 284 insertions, 228 deletions
diff --git a/src/runtime/rt.zc b/src/runtime/rt.zc
index 3badcc2..ee3594d 100644
--- a/src/runtime/rt.zc
+++ b/src/runtime/rt.zc
@@ -1,71 +1,77 @@
-// ============================================================================
-// rt.zc: the runtime half of the genome. memory map, i/o, arithmetic helpers,
-// the heap (size-class free lists) and the mark-sweep collector.
-// ============================================================================
+namespace rt;
+include run;
+
+//: ============================================================================
+//: rt.zc: the runtime half of the genome. memory map, i/o, arithmetic helpers,
+//: the heap (size-class free lists) and the mark-sweep collector.
+//: ============================================================================
+
+//: how the runtime is laid out, for a reader coming in cold:
+//:
+//: memory map generated constants name the regions used by a build. the
+//: compiler and runtime use the same names and values.
+//: i/o, strings thin wrappers over the cpu's i/o opcodes, plus the two
+//: string forms (char* strings and packed byte strings).
+//: heap a buddy allocator with a 5 word header on every block.
+//: new/free/recycle are built on it. every heap object has at
+//: most one owner (the container that holds it), and freeing
+//: an owner frees what it owns.
+//: growable arrays T[?] handles, built on the heap.
+//: mark-sweep a conservative collector. it is the safety net behind
+//: free: anything the compiler's ownership analysis could not
+//: free gets reclaimed here.
+//: memory debug optional checks (use after free, double free, leaks).
+//: syscalls file and environment access through one gate word.
+//:
+//: everything is a 64-bit word, and an address is a word index, not a byte
+//: index. the cpu is a stack machine, so "asm { a @ . }" pushes a, fetches the
+//: word at that address with @, then outputs it with . (see the vm reference).
+//:
+//: ---- memory map (words) ----
-// how the runtime is laid out, for a reader coming in cold:
-//
-// memory map generated constants name the regions used by a build. the
-// compiler and runtime use the same names and values.
-// i/o, strings thin wrappers over the cpu's i/o opcodes, plus the two
-// string forms (word strings and packed byte strings).
-// heap a buddy allocator with a 5 word header on every block.
-// new/free/recycle are built on it. every heap object has at
-// most one owner (the container that holds it), and freeing
-// an owner frees what it owns.
-// growable arrays []T handles, built on the heap.
-// mark-sweep a conservative collector. it is the safety net behind
-// free: anything the compiler's ownership analysis could not
-// free gets reclaimed here.
-// memory debug optional checks (use after free, double free, leaks).
-// syscalls file and environment access through one gate word.
-//
-// everything is a 64-bit word, and an address is a word index, not a byte
-// index. the cpu is a stack machine, so "asm { a @ . }" pushes a, fetches the
-// word at that address with @, then outputs it with . (see the vm reference).
-//
-// ---- memory map (words) ----
// src/tools/genmap writes the map values for each build between these markers.
// keep the names and order stable so programs can use the same map interface
// across targets and image sizes.
// @map-rt
-
-const word MW = 0x1C5000;
-const word CODE_END = 0x80000;
-const word HEAP_LO = 0x1C5000;
-const word HEAP_HI = 0x225000;
-const int HEAP_GROW = 0x1;
-const word ZFP = 0x90000;
-const word GORG = 0x114100;
-const word FBOT = 0x164100;
-const word FTOP = 0x1A4100;
-const word RS0 = 0x1A4100;
-const word MARK0 = 0x1B4100;
-const int MARKN = 0x8000;
-const word DSLO = 0x1BC100;
-const word DSHI = 0x1C4FF0;
-const int MAGIC = 0x2B7E1516;
+const MW = 0x1D5000;
+const CODE_END = 0x80000;
+const HEAP_LO = 0x1D5000;
+const HEAP_HI = 0x235000;
+const HEAP_GROW = 0x1;
+const ZFP = 0x90000;
+const GORG = 0x114100;
+const FBOT = 0x174100;
+const FTOP = 0x1B4100;
+const RS0 = 0x1B4100;
+const MARK0 = 0x1C4100;
+const MARKN = 0x8000;
+const DSLO = 0x1CC100;
+const DSHI = 0x1D4FF0;
+const MAGIC = 0x2B7E1516;
// @end-map
-// frames grow in stable segments so pointers to a caller's locals stay valid.
+//: frames grow in stable segments so pointers to a caller's locals stay valid.
+
word frame_floor = FBOT, frame_top = FTOP, frame_node;
int frame_chunk = FTOP - FBOT, frame_guard = (FTOP - FBOT) >> 2, frame_busy, gc_active;
-[17]word frame_control;
+word[17] frame_control;
word data_low = DSLO, data_top = DSHI, data_node;
word return_low = RS0, return_top = MARK0 - 8, return_node;
const int F_STACK = 1024;
const int F_INTERNAL = 512;
-// ---- virtual enhancements ----
-// install(desc) hands the core a descriptor through the % opcode. the core reads
-// it and wires the region it describes into the cpu, so the compiler can replace
+//: ---- virtual enhancements ----
+
+// install(desc) passes a descriptor to the core through the % opcode. the core
+// reads it and installs the region it describes in the cpu, so the compiler can replace
// a run of vm code with native code. the descriptor is laid out by the compiler
// for each enhance and o3 region (see install in compiler.zc). the enhance lines
// below are the micro forms of the same thing: a single machine instruction each.
// zdiv, zmod and zshl further down do the same arithmetic in software. this block
// comes first, since the compiler resolves / % and << to these names.
+
// install the descriptor for an optimized region.
int install(int desc) { return asm { desc @ % }; }
// signed division of a by b, as one machine instruction.
@@ -78,9 +84,11 @@ enhance ishl(a, b) micro "a = shl a b";
enhance lfa(k) micro "r1 = ld 589824; k = add r1 k";
// 1 when i is outside 0..n-1 (the unsigned compare also catches negative i), else 0.
enhance outside(i, n) micro "i = ltu i n; i = xor i 1";
-// ---- bit operations and float conversions ----
+//: ---- bit operations and float conversions ----
+
// these use single machine instructions where the cpu has them.
+
// the number of leading zero bits in a.
enhance clz(a) micro "a = clz a";
// the number of trailing zero bits of a.
@@ -106,7 +114,7 @@ enhance zf2s(a) micro "a = f2s a";
// s2f, the software form used where the cpu has none.
enhance zs2f(a) micro "a = s2f a";
-// runtime code segments retain the addresses of earlier functions and globals.
+//: runtime code segments retain the addresses of earlier functions and globals.
word code_blocks;
// distinguish a raw function address from a heap closure's environment block.
int callable_code(word f) {
@@ -116,13 +124,15 @@ int callable_code(word f) {
return 0;
}
-// ---- i/o ----
-// output can be redirected. mode 1 drops what is written and mode 2 appends
-// it to a buffer; mode 3 counts characters without storing them (the server
-// uses these modes while it checks a document,
-// so a parse word that prints can't write into the protocol stream).
-// raw_c always reaches the terminal.
-int outmode; word outbuf; int outn, outcap; word outgrow;
+//: ---- i/o ----
+
+//: output can be redirected. mode 1 drops what is written and mode 2 appends
+//: it to a buffer; mode 3 counts characters without storing them (the server
+//: uses these modes while it checks a document,
+//: so a parse word that prints can't write into the protocol stream).
+//: raw_c always reaches the terminal.
+
+int outmode; char* outbuf; int outn, outcap; word outgrow;
// write one character to the terminal with no translation.
int raw_c(int ch) { asm { ch @ . } return 0; }
// write one character. normally it goes to the terminal; while a capture is
@@ -156,34 +166,35 @@ int put_s(char* s) {
// write the decimal form of a number, with a minus sign for a negative one.
// the smallest integer is handled as its own case.
int put_n(int n) {
- [24]word b; int i = 0;
+ char[24] b; int i = 0;
if (n < 0) { put_c('-'); n = 0 - n; }
if (n < 0) { put_s("9223372036854775808"); return 0; }
while (1) { b[i++] = '0' + n % 10; n = n / 10; if (!n) break; }
while (i) put_c(b[--i]);
return 0;
}
-// the length of a word string, not counting its 0.
+// the length of a zero-terminated character string, not counting its 0.
int len_w(char* s) {
int n = 0;
while (s[n]) n++;
return n;
}
-// true when two word strings hold the same characters.
+// true when two zero-terminated character strings hold the same characters.
int eq_w(char* a, char* b) {
int i = 0;
while (a[i] && a[i] == b[i]) i++;
return a[i] == b[i];
}
-// ---- word helpers ----
-// copy and fill by word. copy_w copies forward, so overlapping ranges are only
-// safe when dst is below src. a long run above the program area goes to the
-// host in one call (ops 18 and 19 of the syscall gate); a host that answers -1
-// (a board, the js vm, a range that overlaps itself) gets the plain loop.
+//: ---- word helpers ----
-const int SYS_FILL = 18, /* wowie! */ SYS_COPY = 19;
+//: copy and fill by word. copy_w copies forward, so overlapping ranges are only
+//: valid when dst is below src. a long run above the program area goes to the
+//: host in one call (ops 18 and 19 of the syscall gate); a host that answers -1
+//: (a board, the js vm, a range that overlaps itself) gets the plain loop.
+
+const int SYS_FILL = 18, SYS_COPY = 19;
// copy n words from src to dst.
-int copy_w(word* dst, word* src, int n) {
+unchecked int copy_w(word*.m dst, word*.m src, int n) {
int i = 0; word g = ZFP + 2;
if (n >= 16 && dst >= MW) {
g[1] = dst; g[2] = src; g[3] = n; g[0] = SYS_COPY;
@@ -193,7 +204,7 @@ int copy_w(word* dst, word* src, int n) {
return 0;
}
// set n words at dst to v.
-int fill_w(word* dst, int v, int n) {
+unchecked int fill_w(word*.m dst, int v, int n) {
int i = 0; word g = ZFP + 2;
if (n >= 16 && dst >= MW) {
g[1] = dst; g[2] = v; g[3] = n; g[0] = SYS_FILL;
@@ -210,6 +221,7 @@ int fill_w(word* dst, int v, int n) {
// and may cross a word. an access that runs off the end of memory loads 0 and
// stores nothing. a byte store never fires the syscall gate; that is a word
// store at SYSGATE. opcodes 612..617, words 1224..1234.
+
int addr_b(int w) { return w << 3; }
// the word address of a byte address b.
int addr_w(int b) { return b >> 3; }
@@ -230,36 +242,42 @@ int st32(int a, int v) { asm { v @ a @ #1234 }; return 0; }
// starting at the next word, and a 0 byte after the last character. literals
// b"..." live in the code and are not heap objects. cat_s, slice_s, n_to_s and
// new_s return a shared heap object with the same layout: many pointers may
-// hold it, free does nothing, and the collector reclaims it. word strings
+// hold it, free does nothing, and the collector reclaims it. char* strings
// (one character per word) are unchanged.
-int len_s(word s) { return s[0]; }
+
+// exposes the length header and packed storage for low-level runtime operations.
+word* bstr_storage(bstr s) { return asm { s @ }; }
+// forms a packed-string handle from storage with a valid length header.
+bstr bstr_handle(word* p) { return asm { p @ }; }
+// the number of bytes in a packed byte string.
+int len_s(bstr s) { return bstr_storage(s)[0]; }
// byte i of a packed byte string.
-int at_s(word s, int i) {
- if (i < 0 || i >= s[0]) return 0;
- return ld8(addr_b(s + 1) + i);
+int at_s(bstr s, int i) {
+ if (i < 0 || i >= len_s(s)) return 0;
+ return ld8(addr_b(bstr_storage(s) + 1) + i);
}
-// a new packed byte string of n bytes, owned by the caller.
-unchecked word new_s(int n) {
- word p = new(1 + ((n + 8) >> 3));
+// a new packed byte string of n bytes, managed by the collector.
+bstr new_s(int n) {
+ word* p = new(1 + ((n + 8) >> 3));
p[0] = n;
- return share(p);
-}
-// store byte v at position i of a word string.
-int sput(word s, int i, int v) { st8(addr_b(s + 1) + i, v); return 0; }
-// a new string with a followed by b, owned by the caller.
-unchecked word cat_s(word a, word b) {
- int n = a[0] + b[0]; word p = new_s(n); int i = 0;
- while (i < a[0]) { sput(p, i, at_s(a, i)); i++; }
+ return bstr_handle(share(p));
+}
+// store byte v at position i of a packed byte string.
+int sput(bstr s, int i, int v) { st8(addr_b(bstr_storage(s) + 1) + i, v); return 0; }
+// a new string with a followed by b, managed by the collector.
+bstr cat_s(bstr a, bstr b) {
+ int n = len_s(a) + len_s(b); bstr p = new_s(n); int i = 0;
+ while (i < len_s(a)) { sput(p, i, at_s(a, i)); i++; }
i = 0;
- while (i < b[0]) { sput(p, a[0] + i, at_s(b, i)); i++; }
+ while (i < len_s(b)) { sput(p, len_s(a) + i, at_s(b, i)); i++; }
sput(p, n, 0);
return p;
}
-// a new string with the characters from i to j of s, owned by the caller.
-unchecked word slice_s(word s, int i, int j) {
- int n; word p; int k = 0;
+// a new string with the characters from i to j of s, managed by the collector.
+bstr slice_s(bstr s, int i, int j) {
+ int n; bstr p; int k = 0;
if (i < 0) i = 0;
- if (j > s[0]) j = s[0];
+ if (j > len_s(s)) j = len_s(s);
if (j < i) j = i;
n = j - i;
p = new_s(n);
@@ -267,36 +285,36 @@ unchecked word slice_s(word s, int i, int j) {
sput(p, n, 0);
return p;
}
-// compare two word strings: negative, 0 or positive, like strcmp.
-int cmp_s(word a, word b) {
- int i = 0, n = a[0], ca, cb;
- if (b[0] < n) n = b[0];
+// compare two packed byte strings: negative, 0 or positive, like strcmp.
+int cmp_s(bstr a, bstr b) {
+ int i = 0, n = len_s(a), ca, cb;
+ if (len_s(b) < n) n = len_s(b);
while (i < n) {
ca = at_s(a, i); cb = at_s(b, i);
if (ca != cb) { if (ca < cb) return 0 - 1; return 1; }
i++;
}
- if (a[0] == b[0]) return 0;
- if (a[0] < b[0]) return 0 - 1;
+ if (len_s(a) == len_s(b)) return 0;
+ if (len_s(a) < len_s(b)) return 0 - 1;
return 1;
}
-// true when two word strings are equal.
-int eq_s(word a, word b) { return cmp_s(a, b) == 0; }
-// true when word string a sorts before b.
-int lt_s(word a, word b) { return cmp_s(a, b) < 0; }
-// true when word string a sorts before b or equals it.
-int le_s(word a, word b) { return cmp_s(a, b) <= 0; }
+// true when two packed byte strings are equal.
+int eq_s(bstr a, bstr b) { return cmp_s(a, b) == 0; }
+// true when packed byte string a sorts before b.
+int lt_s(bstr a, bstr b) { return cmp_s(a, b) < 0; }
+// true when packed byte string a sorts before b or equals it.
+int le_s(bstr a, bstr b) { return cmp_s(a, b) <= 0; }
// write a packed byte string.
-int put_bs(word s) {
+int put_bs(bstr s) {
int i = 0;
- while (i < s[0]) { put_c(at_s(s, i)); i++; }
+ while (i < len_s(s)) { put_c(at_s(s, i)); i++; }
return 0;
}
// the decimal number at the start of a packed byte string.
-int s_to_n(word s) {
+int s_to_n(bstr s) {
int n = 0, i = 0, sign = 1; word c;
- if (s[0] && at_s(s, 0) == '-') { sign = 0 - 1; i = 1; }
- while (i < s[0]) {
+ if (len_s(s) && at_s(s, 0) == '-') { sign = 0 - 1; i = 1; }
+ while (i < len_s(s)) {
c = at_s(s, i);
if (c < '0' || c > '9') break;
n = n * 10 + c - '0';
@@ -305,8 +323,8 @@ int s_to_n(word s) {
return n * sign;
}
// a new packed byte string holding the decimal form of n.
-unchecked word n_to_s(int n) {
- [24]word b; int i = 0, j = 0, neg = 0; word p, t;
+bstr n_to_s(int n) {
+ word[24] b; int i = 0, j = 0, neg = 0; bstr p; char* t;
if (!n) { p = new_s(1); sput(p, 0, '0'); sput(p, 1, 0); return p; }
if (n < 0) { neg = 1; n = 0 - n; }
if (n < 0) {
@@ -324,8 +342,9 @@ unchecked word n_to_s(int n) {
return p;
}
-// ---- arithmetic the hardware doesn't have ----
-[64]word pow2;
+//: ---- arithmetic the hardware doesn't have ----
+
+word[64] pow2;
int zrem;
// unsigned division of a by dv.
int zudiv(int n, int dv) {
@@ -361,35 +380,43 @@ int zmod(int a, int b) {
// a shifted left by b bits, for b under 64.
int zshl(int a, int b) { return a * pow2[b & 63]; }
-// ---- heap ----
-// a buddy allocator. the heap is cut into aligned power-of-two chunks, every
-// block is a power of two words, and a freed block merges with its buddy
-// (the other half of the block they were split from) whenever that one is
-// free too, so freed space can be reused at any size. every block has a 5
-// word header before the pointer the program sees:
-// p[-5] block size
-// p[-4] flags in the low 16 bits, size class in the next 5
-// 0 free (check word cleared) or quarantined (check word still p ^ MAGIC)
-// 1 live 2 releasing 3 marked 4 quarantined but a word still points at it
-// bit 8 (value 8) is orthogonal: the object is shared. free is a no-op,
-// own and release do not follow it, and the sweep still collects it when
-// nothing points at it. bits 0..2 keep the meanings above.
-// p[-3] p ^ MAGIC p[-2] owner (the container that owns it, or 0)
-// p[-1] its type (a type record from new(T), 0 from new(n), -(n + 1) from o3's new_raw)
-// free blocks sit on a doubly linked list per class, linked through p[0], p[1].
-// free quarantines: the block stays intact until a sweep finds nothing pointing
-// at it. recycle is the fast path for a pointer the program has already
-// dropped: it pushes the block onto its size-class list and does not walk the
-// heap. a two-word node is still 8 words (the smallest block); a three-word
-// node fits in 8 instead of 16.
+//: ---- heap ----
+
+//: a buddy allocator. the heap is cut into aligned power-of-two chunks, every
+//: block is a power of two words, and a freed block merges with its buddy
+//: (the other half of the block they were split from) whenever that one is
+//: free too, so freed space can be reused at any size. every block has a 5
+//: word header before the pointer the program sees:
+//: p[-5] block size
+//: p[-4] flags in the low 16 bits, size class in the next 5
+//: 0 free (check word cleared) or quarantined (check word still p ^ MAGIC)
+//: 1 live 2 releasing 3 marked 4 quarantined but a word still points at it
+//: bit 8 (value 8) is orthogonal: the object is shared. free is a no-op,
+//: own and release do not follow it, and the sweep still collects it when
+//: nothing points at it. bits 0..2 keep the meanings above.
+//: p[-3] p ^ MAGIC p[-2] owner (the container that owns it, or 0)
+//: p[-1] its type (a type record from new(T), 0 from new(n), -(n + 1) from o3's new_raw)
+//: free blocks sit on a doubly linked list per class, linked through p[0], p[1].
+//: free quarantines: the block stays intact until a sweep finds nothing pointing
+//: at it. recycle is the fast path for a pointer the program has already
+//: dropped: it pushes the block onto its size-class list and does not walk the
+//: heap. a two-word node is still 8 words (the smallest block); a three-word
+//: node fits in 8 instead of 16.
+
const int HDR = 5;
-const int F_SHARE = 8; // p[-4] bit, orthogonal to live / mark / releasing
-const int F_LOW = 65535; // flags live in these bits; the size class is above them
-int hbump; [32]word freel; int gc_runs, gc_freed, gc_auto = 1;
-int hstop; // block walks and new chunks stop here
-int hold_lo, hold_hi; // while nonzero, balloc will not return a block overlapping this range
-int hmax, hcut; // words of memory the host has (0: the heap cannot grow); hcut: the heap was cut, so it must not grow
-int dbg, alloc_line; // memdebug bits, and the line the compiler stamps before new
+//: p[-4] bit, orthogonal to live / mark / releasing.
+const int F_SHARE = 8;
+//: flags live in these bits; the size class is above them.
+const int F_LOW = 65535;
+int hbump; word[32] freel; int gc_runs, gc_freed, gc_auto = 1;
+//: block walks and new chunks stop here.
+int hstop;
+//: while nonzero, balloc will not return a block overlapping this range.
+int hold_lo, hold_hi;
+//: words of memory the host has (0: the heap cannot grow); hcut: the heap was cut, so it must not grow.
+int hmax, hcut;
+//: memdebug bits, and the line the compiler stamps before new.
+int dbg, alloc_line;
// the size of the top-level chunk holding heap offset off, within [0, hstop)
int topsize(int off) {
@@ -462,7 +489,7 @@ int heap_init() {
hold_hi = 0;
hcut = 0;
hmax = 0;
- // a map with a growing heap asks the host how much memory there is. a host
+ // a map with a growing heap queries the host for available memory. a host
// that does not answer (-1) or has no more than the initial heap gets a
// fixed heap
if (HEAP_GROW) { hmax = syscall(9, 0, 0, 0); if (hmax <= HEAP_LO) hmax = 0; }
@@ -513,7 +540,7 @@ int heap_uncut(int addr, int k) {
// take a block of 2^k words. find the smallest non-empty class at or above k,
// unlink its first block and split it down: each half that is not kept goes
// back on its own list. a block that overlaps the held range is marked
-// "releasing" and dropped from the lists for good (the loop then tries again),
+// "releasing" and dropped from the lists permanently (the loop retries),
// so the compiler's type area is never handed out as heap.
word balloc(int k) {
int j; word b; int h, ok;
@@ -536,7 +563,8 @@ word balloc(int k) {
}
return 0;
}
-// ---- a heap that grows ----
+//: ---- a heap that grows ----
+
// the heap is [HEAP_LO, hstop). on a map with HEAP_GROW the memory above
// HEAP_HI belongs to the host (a big zeroed array nobody has touched yet), and
// when the free lists cannot satisfy a request the heap takes more of it. the
@@ -544,6 +572,7 @@ word balloc(int k) {
// chunks (topsize) are right: extending hstop re-cuts them, and every new
// piece goes through bfree, which merges it with a free buddy below it. a
// block that is in use stays where it is, so no pointer moves.
+
// grow so that a block of 2^k words fits: double the heap, or less when the
// host runs out. returns 0 when it cannot.
int heap_grow(int k) {
@@ -553,7 +582,7 @@ int heap_grow(int k) {
need = (h + pow2[k] - 1) / pow2[k] * pow2[k] + pow2[k];
h2 = h + h;
if (h2 < need) h2 = need;
- if (h2 > room) { // the machine is smaller than the heap wants: the host grows it (syscall 9, 4)
+ if (h2 > room) { // the machine is smaller than the requested heap: the host grows it (syscall 9, 4)
hmax = syscall(9, 4, HEAP_LO + h2, 0);
if (hmax <= HEAP_LO) hmax = 0;
room = hmax - HEAP_LO;
@@ -584,7 +613,8 @@ int hfree() {
}
return n;
}
-int fend; // end of the block bfree last made, for the sweep
+//: end of the block bfree last made, for the sweep.
+int fend;
// put block b back, merging it with its buddy as long as it can
int bfree(word b) {
int k = bclass(b), sz = pow2[k], off; word bud;
@@ -617,13 +647,13 @@ int bdrop(word b) {
return 0;
}
// is x the payload address of a block that has been allocated (live or
-// quarantined)? the check word p[-3] == p ^ MAGIC is what makes this safe to
-// ask about any integer: a random value would have to match by chance.
+// quarantined)? the check word p[-3] == p ^ MAGIC validates the pointer before
+// checking it against block metadata; a random value would have to match by chance.
word isobj(word x) { return x >= HEAP_LO + HDR && x < hbump && x[-3] == (x ^ MAGIC) && (x[-4] & 1); }
-// the number of words to skip in p[0..n) before one that could be a pointer
-// to a block (see op 20 in core.c); n when none can be. a long payload of
-// plain numbers costs one host call instead of an isobj per word. a host that
-// does not have the op (-1) is asked once and then never again.
+//: the number of words to skip in p[0..n) before one that could be a pointer
+//: to a block (see op 20 in core.c); n when none can be. a long payload of
+//: plain numbers costs one host call instead of an isobj per word. a host that
+//: does not have the op (-1) is asked once and then never again.
const int SYS_SCAN = 20;
int scan_off;
// walk the n words at p and mark every heap object they point to.
@@ -650,10 +680,11 @@ int reserve_frames(int words) {
}
// reserve stack words without invalidating locals or their borrowed addresses.
int reserve_data(int words) { return syscall(9, 7, words, 0); }
+// grow the return-address stack to hold at least words entries.
int reserve_returns(int words) { return syscall(9, 8, words, 0); }
// the prologue restores oldfp before calling this helper. its own calls use
// the guard space below oldfp; they must not try to grow another segment.
-unchecked word frame_grow(int oldfp, int attempted) {
+word frame_grow(int oldfp, int attempted) {
int size = frame_chunk, slots = oldfp - attempted; word p;
frame_busy = 1;
if (!hstop || slots < 0 || slots > 0x7FFFFFFF - frame_guard - 4) {
@@ -671,7 +702,7 @@ unchecked word frame_grow(int oldfp, int attempted) {
}
// only the final frame in a segment calls this helper. the helper's own
// epilogue still uses that segment; the caller stores the restored pointer.
-unchecked word frame_leave() {
+word frame_leave() {
word p = frame_node; int oldfp = p[0];
frame_busy = 1;
frame_floor = p[1]; frame_top = p[2]; frame_node = p[3];
@@ -689,7 +720,7 @@ int oom() {
// n words, untyped and not zeroed. the type word keeps -(n + 1), so fatten
// can type it later. typed new goes through new_t. o3 new(n) goes through
// new_oz, which zeroes the words that were asked for.
-unchecked int .u new_raw(int n) {
+int .u new_raw(int n) {
int need = n + HDR, k = 3; word b; int p;
if (n > 0x7FFFFFFF - HDR) { oom(); return 0; }
if (n < 0) need = HDR;
@@ -716,29 +747,30 @@ unchecked int .u new_raw(int n) {
}
// n words, untyped, zeroed. the type word stays -(n + 1). new_raw does not
// zero the words that were asked for, so typed new does not zero them twice.
-unchecked int .u new_oz(int n) {
+int .u new_oz(int n) {
int p = new_raw(n);
if (n > 0) fill_w(p, 0, n);
return p;
}
// n words typed t, zeroed. memdebug records the line the compiler stored in alloc_line
-unchecked word .u new_t(int n, int t) {
+word .u new_t(int n, int t) {
word p = new_raw(n);
p[-1] = t;
fill_w(p, 0, p[-5] - HDR);
if (dbg & 1) note_alloc(p, alloc_line);
return p;
}
-unchecked word .u new(int n) { return new_t(n, 0); }
+// allocate n words without a type stamp; the caller owns the fresh object.
+word .u new(int n) { return new_t(n, 0); }
// the compiler works out the size class of a constant-size new at compile
// time (k is the class of n + HDR words), so a repeated allocation of the same
// shape pays for no class search. when the class list has a block, pop it
// directly; a free block already has the right size and an empty payload to
// refill. in a loop that frees its object every pass (an m local) this is the
-// block that was just recycled, so the pair costs about a dozen steps.
+// recycled block, so the pair costs about a dozen steps.
// memdebug, an empty list, or a block inside the held range (balloc's rule
// for the compiler's type area) fall back to the general path.
-unchecked word .u new_k(int n, int t, int k) {
+word .u new_k(int n, int t, int k) {
word b = freel[k], p, nx;
if (!b || dbg || (hold_hi && b < hold_hi && b + pow2[k] > hold_lo)) return new_t(n, t);
nx = b[5];
@@ -781,10 +813,10 @@ int release(word p) {
b[1] = b[1] & ~7; // quarantined. the check word and the bytes stay; the next sweep reclaims it
return 0;
}
-// frees a heap object: p has to be tagged d, so a call is free(d*p). the object is
-// not reused at once. it is kept in quarantine (a stale pointer still reads the old
-// contents), and the next collection reclaims it when nothing points at it. a second
-// free does nothing. a shared object is left alone.
+// releases a heap object and consumes an owned object automatically. an explicit
+// d* recast is also accepted; a borrowed reference cannot be consumed.
+// storage remains quarantined until collection finds no references to it.
+// releasing an already released or shared object has no effect.
unchecked int free(.d p) {
int n;
if (isobj(p) && (p[-4] & F_SHARE)) return 0;
@@ -821,7 +853,7 @@ unchecked int recycle(.d p) {
bdrop(b);
return 0;
}
-// the matching free for an object the compiler knows has no pointer fields
+// the matching free for an object whose type has no pointer fields
// (a typed new of a struct of numbers). it checks the type word and the size
// class still match what the compiler assumed, since the variable may have been
// reassigned; if not, or under memdebug, it takes the general route.
@@ -845,9 +877,9 @@ word own(word c, int p) {
return c;
}
// an opt-in bump arena of n words, made as one block. the handle owns that block.
-// arena_alloc hands out interior pointers into it and never asks the buddy lists,
+// arena_alloc returns interior pointers into it and does not use the buddy lists,
// and arena_reset gives all of it back at once.
-unchecked word .u arena_new(int n) {
+word .u arena_new(int n) {
word h = new(3), b; int cap = n;
if (cap < 1) cap = 1;
b = new(cap);
@@ -858,7 +890,7 @@ unchecked word .u arena_new(int n) {
}
// take n zeroed words from arena a. it returns the address, or 0 when the arena
// does not have n words left.
-unchecked int arena_alloc(word a, int n) {
+int arena_alloc(word a, int n) {
int p, at;
if (!a || n < 0) return 0;
at = a[2];
@@ -877,7 +909,7 @@ int arena_reset(word a) {
// publish p. a shared object has no owner: free does nothing to it, copies of its
// pointer are ordinary words, and the collector reclaims it once every copy is gone.
// returns p.
-unchecked word share(word p) {
+word share(word p) {
if (!isobj(p)) return p;
p[-4] = (p[-4] & ~7) | 1 | F_SHARE;
p[-2] = 0;
@@ -906,24 +938,26 @@ unchecked word triple(int a, int b, int c) {
return share(p);
}
// the first word of a pair made by cons.
-unchecked word car(word p) {
+word car(word p) {
if (dbg & 1) checkptr(p);
return p[0];
}
// the second word of a pair made by cons.
-unchecked word cdr(word p) {
+word cdr(word p) {
if (dbg & 1) checkptr(p);
return p[1];
}
// word i (0, 1 or 2) of a triple made by triple.
-unchecked word tget(word p, int i) {
+word tget(word p, int i) {
if (dbg & 1) checkptr(p);
return p[i];
}
-// ---- growable arrays ----
-// a []T is a handle: [len, cap, items, element size]. items is an object the
+//: ---- growable arrays ----
+
+// a T[?] is a handle: [len, cap, items, element size]. items is an object the
// handle owns; growing swaps it for a bigger one and moves its children over.
+
unchecked word .u dnew(int es, int n, int t) {
word h = new_t(4, t); int c = n; word it;
if (c < 4) c = 4;
@@ -949,7 +983,7 @@ unchecked int dgrow(word h, int need) {
free(old);
return 0;
}
-// the out-of-range report for []T indexing (dat, dpop)
+// the out-of-range report for T[?] indexing (dat, dpop)
int oob(int i, int n) {
put_s("zy: index ");
put_n(i);
@@ -961,7 +995,7 @@ int oob(int i, int n) {
}
// element i of a growable array h. indexing with [] does the same, with the bounds
// check. the bounds check is what oob reports when i is outside the array.
-unchecked word dat(word h, int i) {
+word dat(word h, int i) {
if (i < 0 || i >= h[0]) oob(i, h[0]);
return h[2] + i * h[3];
}
@@ -976,28 +1010,40 @@ unchecked int dpush(word h, .d x) {
}
// remove the last element of a growable array h and give it back. the array
// must not be empty.
-unchecked word dpop(word h) {
+word dpop(word h) {
+ word value;
if (!h[0]) oob(-1, 0);
h[0] = h[0] - 1;
- return h[2][h[0]];
+ value = h[2][h[0]]; h[2][h[0]] = 0;
+ if (isobj(value) && value[-2] == h[2]) own(value, 0);
+ return value;
}
// set the length of a growable array h to n. growing it fills the new elements with
// zero, and shrinking it drops the elements past n.
-unchecked int dresize(word h, int n) {
- int old = h[0] * h[3];
+int dresize(word h, int n) {
+ int old = h[0] * h[3], end, i; word value;
if (n < 0) n = 0;
dgrow(h, n);
- if (n * h[3] > old) fill_w(h[2] + old, 0, n * h[3] - old);
+ end = n * h[3];
+ if (end > old) fill_w(h[2] + old, 0, end - old);
+ else {
+ i = end;
+ while (i < old) {
+ value = h[2][i]; h[2][i] = 0;
+ if (isobj(value) && value[-2] == h[2]) free(value);
+ i++;
+ }
+ }
h[0] = n;
return 0;
}
// packed elements (h[3] is minus their size in bytes): the address is a byte address
-unchecked word datb(word h, int i) {
+word datb(word h, int i) {
if (i < 0 || i >= h[0]) oob(i, h[0]);
return addr_b(h[2]) + i * (0 - h[3]);
}
// add the byte x to the end of a growable byte array h.
-unchecked int dpushb(word h, int x) {
+int dpushb(word h, int x) {
int b = 0 - h[3], a;
dgrow(h, h[0] + 1);
a = addr_b(h[2]) + h[0] * b;
@@ -1006,7 +1052,7 @@ unchecked int dpushb(word h, int x) {
return 0;
}
// remove and return the last byte of a growable byte array h.
-unchecked int dpopb(word h) {
+int dpopb(word h) {
int b = 0 - h[3], a;
if (!h[0]) oob(-1, 0);
h[0] = h[0] - 1;
@@ -1016,7 +1062,7 @@ unchecked int dpopb(word h) {
return ld32(a);
}
// growing zeroes the new elements, including the unused tail of the last word
-unchecked int dresizeb(word h, int n) {
+int dresizeb(word h, int n) {
int b = 0 - h[3], a, e, old = (h[0] * b + 7) >> 3, nw;
if (n < 0) n = 0;
if (n > h[0]) {
@@ -1036,15 +1082,17 @@ int dlen(word h) { return h[0]; }
// the capacity of a growable array: how many elements it can hold before it grows.
int dcap(word h) { return h[1]; }
-// ---- mark-sweep ----
-// conservative and non-moving. roots: the program area (globals live
-// between functions), the genome's globals, the compiler's input stack,
-// the live part of the frame stack and the live part of the data stack.
-// opcode 619 pushes that index. a word below it is not a root.
+//: ---- mark-sweep ----
+
+//: conservative and non-moving. roots: the program area (globals live
+//: between functions), the genome's globals, the compiler's input stack,
+//: the live part of the frame stack and the live part of the data stack.
+//: opcode 619 pushes that index. a word below it is not a root.
+
int ms_top, ms_over, gc_lo, gc_hi;
word ms_buf = MARK0; int markcap = MARKN;
// mark work must grow without starting another collection inside this one.
-unchecked int reserve_mark(int need) {
+int reserve_mark(int need) {
int cap = markcap, auto = gc_auto; word p;
if (need <= cap) return cap;
while (cap < need) cap = cap * 2;
@@ -1098,15 +1146,15 @@ int gc_frames() {
while (p) { mark(p); markrange(p[0], p[2]); p = p[3]; }
return 0;
}
-// gc_roots is defined in the compiler half (it knows where the compiler's
+// gc_roots is defined in the compiler half (it identifies the compiler's
// own tables live); it calls markrange over every root range
-int gc_roots();
+int run.gc_roots();
// runs a collection: the blocks nothing points at are reclaimed, quarantined blocks
// included, and the number of blocks freed is returned. cycles are reclaimed too.
unchecked int gc() {
word b = 0; int sz = 0, n = 0; // a leftover pointer here would be a root and pin a block
ms_top = 0; ms_over = 0; gc_active = 1;
- gc_roots();
+ run.gc_roots();
drain();
while (ms_over) { // the mark stack overflowed: rescan marked objects
ms_over = 0;
@@ -1162,15 +1210,17 @@ int scrub() {
return 0;
}
-// ---- memory debug ----
-// dbg bits: 1 header checks and allocation sites, 2 poison the payload on free
-// (the word 57005, which is 0xDEAD), 4 interior pointers keep a quarantined
-// block (the mark walks back at most 64 words to a real header). off unless
-// a program asks. sites are p ^ MAGIC so the collector doesn't see them.
+//: ---- memory debug ----
+
+//: dbg bits: 1 header checks and allocation sites, 2 poison the payload on free
+//: (the word 57005, which is 0xDEAD), 4 interior pointers keep a quarantined
+//: block (the mark walks back at most 64 words to a real header). off unless
+//: a program requests. sites are p ^ MAGIC so the collector does not treat them as pointers.
+
const int NSITE = 64;
const int POISON = 57005;
int site_drop;
-[64]word site_p; [64]word site_line;
+word[64] site_p; word[64] site_line;
// true when p is a freed block still held in quarantine.
word quar(word p) { return p >= HEAP_LO + HDR && p < hbump && p[-3] == (p ^ MAGIC) && !(p[-4] & 1); }
// the object whose payload contains w, or 0. exact pointers and interiors.
@@ -1248,48 +1298,54 @@ int memreport() {
return n;
}
-// ---- host syscalls ----
-// a store of the op number to SYSGATE runs it. the three words after the gate
-// are the arguments, and the store leaves the result in the gate.
-// 1 open(path, 0 read | 1 write-create) 2 close(fd)
-// 3 read(fd, buf, n) 4 write(fd, buf, n) bytes, one per word
-// 5 getenv(name, buf, cap) 6 time() unix seconds
-// 21 argc() 22 argv(i, buf, cap) 23 envc() 24 envp(i, buf, cap)
-// 40 raw(block) any linux syscall: the block holds the number and six arguments.
-// the result is the kernel's, or -errno. see Host.raw in src/lib/std.zc
+//: ---- host syscalls ----
+
+//: a store of the op number to SYSGATE runs it. the three words after the gate
+//: are the arguments, and the store leaves the result in the gate.
+//: 1 open(path, 0 read | 1 write-create) 2 close(fd)
+//: 3 read(fd, buf, n) 4 write(fd, buf, n) bytes, one per word
+//: 5 getenv(name, buf, cap) 6 time() unix seconds
+//: 21 argc() 22 argv(i, buf, cap) 23 envc() 24 envp(i, buf, cap)
+//: 40 raw(block) any linux syscall: the block holds the number and six arguments.
+//: the result is the kernel's, or -errno. see Host.raw in src/lib/std.zc
+
const int SYSGATE = ZFP + 2;
const int SYS_OPEN = 1, SYS_CLOSE = 2, SYS_READ = 3, SYS_WRITE = 4, SYS_GETENV = 5, SYS_TIME = 6;
+/* terminates the host process with the exit status supplied in argument a. */
+const int SYS_EXIT = 7;
const int SYS_BOPEN = 10, SYS_BREAD = 11, SYS_BWRITE = 12, SYS_BGETENV = 13;
const int SYS_RAW = 40;
-// the command line and the environment. argv holds the program path, which is
-// <eval> when the source came from stdin, so argc is 1 even with no arguments.
-// the string ops take an index and copy one entry across, one character per
-// word like SYS_GETENV: a zero buffer asks for the length alone, so the caller
-// can size the string before it fetches it. src/lib/std.zc builds the arrays.
+//: the command line and the environment. argv holds the program path, which is
+//: <eval> when the source came from stdin, so argc is 1 even with no arguments.
+//: the string ops take an index and copy one entry across, one character per
+//: word like SYS_GETENV: a zero buffer requests the length alone, so the caller
+//: can size the string before it fetches it. src/lib/std.zc builds the arrays.
const int SYS_ARGC = 21, SYS_ARG = 22, SYS_ENVC = 23, SYS_ENV = 24;
// byte-string paths. the address is the first byte (addr_b of the word after
-// the length), not the string object. word-string syscalls are unchanged.
-int bopen(word path, int mode) { return syscall(SYS_BOPEN, addr_b(path + 1), mode, 0); }
+// the length), not the string object. char* string syscalls are unchanged.
+int bopen(bstr path, int mode) { return syscall(SYS_BOPEN, addr_b(bstr_storage(path) + 1), mode, 0); }
// read up to n bytes from fd into the packed byte string s.
-int bread(int fd, word s, int n) { return syscall(SYS_BREAD, fd, addr_b(s + 1), n); }
+int bread(int fd, bstr s, int n) { return syscall(SYS_BREAD, fd, addr_b(bstr_storage(s) + 1), n); }
// write the first n bytes of the packed byte string s to fd.
-int bwrite(int fd, word s, int n) { return syscall(SYS_BWRITE, fd, addr_b(s + 1), n); }
+int bwrite(int fd, bstr s, int n) { return syscall(SYS_BWRITE, fd, addr_b(bstr_storage(s) + 1), n); }
// copy the value of the environment variable name into dst, returning its length.
-int bgetenv(word name, word dst, int n) { return syscall(SYS_BGETENV, addr_b(name + 1), addr_b(dst + 1), n); }
-// ---- c abi ----
-// ops 14..17 reach native code (see the c abi section of core.c and extern in
-// compiler.zc). cfind(name) is 1 if a native symbol resolves (name is a byte
-// string), cload(path) opens a library so its symbols resolve, and
-// cbmake(entry, sig) is a native function pointer that runs the zc function at
-// entry (cfunc(f) in the compiler builds the signature). a native pointer
-// from cbmake only works while a native call is in progress, and not on the
-// arm64 jit, where it is 0.
+int bgetenv(bstr name, bstr dst, int n) { return syscall(SYS_BGETENV, addr_b(bstr_storage(name) + 1), addr_b(bstr_storage(dst) + 1), n); }
+//: ---- c abi ----
+
+//: ops 14..17 reach native code (see the c abi section of core.c and extern in
+//: compiler.zc). cfind(name) is 1 if a native symbol resolves (name is a byte
+//: string), cload(path) opens a library so its symbols resolve, and
+//: cbmake(entry, sig) is a native function pointer that runs the zc function at
+//: entry (cfunc(f) in the compiler builds the signature). a native pointer
+//: from cbmake only works while a native call is in progress, and not on the
+//: arm64 jit, where it is 0.
+
const int SYS_CCALL = 14, SYS_CFIND = 15, SYS_CBMAKE = 16, SYS_CLOAD = 17;
// the address of the native symbol name in the loaded libraries, or 0 when it is not
// loaded. see cload.
-int cfind(word name) { return syscall(SYS_CFIND, addr_b(name + 1), 0, 0); }
+int cfind(bstr name) { return syscall(SYS_CFIND, addr_b(bstr_storage(name) + 1), 0, 0); }
// loads the shared library at path, so the symbols it defines can be found with cfind.
-int cload(word path) { return syscall(SYS_CLOAD, addr_b(path + 1), 0, 0); }
+int cload(bstr path) { return syscall(SYS_CLOAD, addr_b(bstr_storage(path) + 1), 0, 0); }
// makes a native function pointer that runs the vm entry with the signature sig,
// so c code can call a zc function through it.
int cbmake(int entry, int sig) { return syscall(SYS_CBMAKE, entry, sig, 0); }
@@ -1301,9 +1357,9 @@ int syscall(int op, word a, word b, word c) {
g[1] = a; g[2] = b; g[3] = c; g[0] = op;
return g[0];
}
-// the whole file as a fresh string, or 0 if it can't be opened
-unchecked word .u readfile(int path) {
- int fd; word buf; int n = 0, got, cap; word nb;
+// reads a file into an owned character string; returns null if opening fails.
+char* .u readfile(char* path) {
+ int fd; char* buf; int n = 0, got, cap; char* nb;
fd = syscall(SYS_OPEN, path, 0, 0);
if (fd < 0) return 0;
cap = 256;