aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/compiler/compiler.zc80
-rw-r--r--tests/errors/lend_branch.zc6
-rw-r--r--tests/errors/lend_give.zc6
-rw-r--r--tests/errors/lend_return.zc6
-rw-r--r--tests/errors/lend_unrestored.zc6
-rw-r--r--tests/errors/move_live_owner.zc5
-rw-r--r--tests/errors/recast_plain.zc6
-rw-r--r--tests/errors/unused.zc2
-rw-r--r--tests/programs/ownmove.zc39
-rwxr-xr-xtests/runerr.sh4
-rwxr-xr-xtests/test.sh5
11 files changed, 154 insertions, 11 deletions
diff --git a/src/compiler/compiler.zc b/src/compiler/compiler.zc
index 2a05c58..c33005d 100644
--- a/src/compiler/compiler.zc
+++ b/src/compiler/compiler.zc
@@ -231,6 +231,10 @@ int here, hdelta, hlimit, ptop;
int code_chunk = rt.CODE_END;
word image_buffer, image_words; int image_active, image_org, image_capacity, image_size;
int nloc, lbase, fdepth, fslots, fchk, fret, live, bxdepth, epichain, fframe, infn;
+//: a global that borrows a local owner through g = m*x. each entry is the global's
+//: symbol, the local, and the block depth of the store; the lend ends when the same
+//: global is assigned again in that block, and is an error if the block can end first.
+word lendrec; int nlend, lendcap, xlend = -1;
int finsym, finfer; // the function being compiled, and whether its owned return is still to be inferred (no .u)
int fn_return_type; // a consistent return type inferred from the function body
//: effects come from the real compiler's uses, stores, returns and call edges; unknown bodies stay conservative.
@@ -2729,9 +2733,62 @@ int lroot(int i) {
while (lstate[i] == S_CHILD) i = lowner[i];
return i;
}
+// lend a local owner to global s for the rest of the current block.
+int lend_add(int s, int i) {
+ if (nlend * 3 + 3 > lendcap) { if (!lendcap) lendcap = 48; else lendcap = lendcap * 2; lendrec = chk_resize(lendrec, nlend * 3, lendcap); }
+ lendrec[nlend * 3] = s; lendrec[nlend * 3 + 1] = i; lendrec[nlend * 3 + 2] = fdepth; nlend++;
+ return 0;
+}
+// remove entry k, keeping the others in order.
+int lend_remove(int k) {
+ if (k + 1 < nlend) rt.copy_w(lendrec + k * 3, lendrec + k * 3 + 3, (nlend - k - 1) * 3);
+ nlend--;
+ return 0;
+}
+// an assignment to global s in the block that lent it ends the lend.
+int lend_clear(int s) {
+ int k = nlend;
+ while (k > 0) { k--; if (lendrec[k * 3] == s && lendrec[k * 3 + 2] == fdepth && lendrec[k * 3 + 1] >= lbase) lend_remove(k); }
+ return 0;
+}
+// report local i still lent to the global of entry k.
+int lend_err(int k, char* msg) {
+ int i = lendrec[k * 3 + 1];
+ if (!fchk || !live || panic || dry) return 0;
+ errat(); rt.put_s(lname[i]); rt.put_s(": is lent to global "); rt.put_s(sname[lendrec[k * 3]]); rt.put_s(msg); errend();
+ return 0;
+}
+// local i is about to be given away or released: no global may still borrow it.
+int lend_check(int i) {
+ int k = 0;
+ while (k < nlend) {
+ if (lendrec[k * 3 + 1] == i && i >= lbase) { lend_err(k, "; assign the global again before giving the local away"); lend_remove(k); }
+ else k++;
+ }
+ return 0;
+}
+// leaving every block at depth dep or deeper: a lend made there must have ended.
+int lend_exit(int dep) {
+ int k = 0;
+ while (k < nlend) {
+ if (lendrec[k * 3 + 2] >= dep && lendrec[k * 3 + 1] >= lbase) lend_err(k, "; assign the global again before the block that lent it ends");
+ k++;
+ }
+ return 0;
+}
+// forget the lends made at depth dep or deeper, or by locals from index from onward.
+int lend_drop(int dep, int from) {
+ int k = 0;
+ while (k < nlend) {
+ if ((lendrec[k * 3 + 2] >= dep && lendrec[k * 3 + 1] >= lbase) || lendrec[k * 3 + 1] >= from) lend_remove(k);
+ else k++;
+ }
+ return 0;
+}
// retire existing aliases together with their owner. dead aliases do not keep later loop snapshots live.
int end_owner(int i) {
int j = lbase; word info = talloc(3);
+ lend_check(i);
info[0] = lname[i]; info[1] = lpos[i]; info[2] = tpack();
local_ended[i] = info[2]; local_end_info[i] = info; lstate[i] = S_DEAD;
while (j < nloc) {
@@ -2795,6 +2852,7 @@ int freevar(int i) {
int exit_vars(int dep, int final) {
int i = nloc, st;
if (!fchk || !live) return 0;
+ lend_exit(dep);
while (i > lbase) {
i--;
if (lhome[i] < dep) continue;
@@ -2868,6 +2926,7 @@ int block_close(int save) {
i = lbase;
unused_check(save);
exit_vars(fdepth, 1);
+ lend_drop(fdepth, save);
alias_end(fdepth, save);
while (i < save) { if (lhome[i] >= fdepth) { lstate[i] = S_DEAD; lhome[i] = fdepth - 1; } i++; }
local_drop(save);
@@ -3107,7 +3166,7 @@ int recast(int c, int t) {
int i, st;
if (c == C_FRESH && t == S_M) return C_PLAIN; // m*f(): the unnamed result is let go on purpose, so it can be discarded
if (!fchk) return c;
- if (c < C_VAR) { cerr("only a named owned object can be recast"); return c; }
+ if (c < C_VAR) { cerr("only a named owned object can be recast; this expression has no owner"); return c; }
i = c - C_VAR;
st = lstate[i];
luse(i);
@@ -3229,7 +3288,7 @@ int assigncheck(int k, int i, int rc) {
if (js == S_BORROW || isalias(j)) { lstate[i] = S_CHILD; lowner[i] = lroot(j); lown[i] = 0; touch(j); return C_PLAIN; }
// replace a released owner by moving a same-scope owner into its empty slot.
// fresh declarations still cannot copy owners; the source and its aliases expire.
- if (st == S_DEAD && lown[i] && js == S_M && lown[j] && lhome[i] == lhome[j]
+ if (st == S_DEAD && lown[i] && js == S_M && lown[j] && lhome[j] >= lhome[i]
&& !local_captured[i] && !local_captured[j]) {
lstate[i] = S_M; lused[i] = 0; lown[i] = 1;
lnsz[i] = lnsz[j]; lleaf[i] = lleaf[j];
@@ -3274,6 +3333,9 @@ int assigncheck(int k, int i, int rc) {
}
// a global, or memory the analyzer doesn't track: the collector takes it
// an owner (d, u, or an m local that owns its object) is transferred as if by u*x
+ if (k == LV_GLOBAL) lend_clear(i);
+ if (j >= 0 && k == LV_GLOBAL && xlend == j && lstate[j] == S_M && lown[j]) { xlend = -1; touch(j); lend_add(i, j); return C_PLAIN; }
+ xlend = -1;
if (j >= 0) {
if (lstate[j] == S_D || lstate[j] == S_U || (lstate[j] == S_M && lown[j])) { ev_put(j, EV_GLOBAL, -1); end_owner(j); luse(j); }
else cerrn(j, "is not an owner, so it can't be stored in a global or in untracked memory (the collector takes it over)");
@@ -4185,7 +4247,7 @@ int methdef(char* sn, char* mn, word q, int stat, int typed, int rty, int rtag)
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);
- word previous = m*r; r = d*joined; rt.free(return_tokens); rt.free(buffer); rt.free(previous);
+ rt.free(r); r = joined; rt.free(return_tokens); rt.free(buffer);
}
src_push(1, r, 0, 1);
next();
@@ -7084,9 +7146,12 @@ int unary() {
if (pk_tk == T_OP && pk_tv == s_star && pq_gl[0]) {
t = S_M; if (tv == s_u) t = S_U; if (tv == s_d) t = S_D;
next(); expect(s_star);
- c = unary(); rval();
+ c = unary(); place = -1; if (lvk == LV_LOCAL) place = lvi; rval();
lvk = LV_NONE;
- return recast(c, t);
+ if (c < C_VAR && c != C_FRESH && place >= 0 && fchk) { cerrn(place, "isn't an owned object, so there is no ownership to recast"); return c; }
+ c = recast(c, t);
+ if (t == S_M && c >= C_VAR && lstate[c - C_VAR] == S_M && lown[c - C_VAR]) xlend = c - C_VAR;
+ return c;
}
}
if (tk == T_OP && tv == s_lp && caststart()) { // (int)x, (node*)x: same bits, the type changes
@@ -7334,6 +7399,7 @@ int expr() {
next();
a = o3on && k == LV_LOCAL && (ltag[i] & 16) && !larr[i];
if (a) op(O_DROP); // an o3 variable: its address comes after the value, so it never waits on the stack across a branch
+ xlend = -1;
rc = expr(); rval();
if (k != LV_LOCAL) af_escape(xaf);
fcoerce(lt);
@@ -8456,6 +8522,7 @@ int ctx_pop() {
f = R_CTX + ctxsp * CTX_WORDS;
local_drop(f[0]);
fdrop(nloc);
+ lend_drop(1 << 30, f[0]);
lbase = f[1];
fdepth = f[2];
fslots = f[3];
@@ -8484,6 +8551,7 @@ int ctx_pop() {
int fn_begin(int chk, int ret) {
capture_enabled = 0; effect_value = 0; effect_current = 0; borrow_slots = 0; borrow_pending = 0; borrow_depth = 0; borrow_jump = -1; borrow_entry = 0;
if (cmode && svkind && !dry) rt.fill_w(svkind, 0, nsym); xvk = 0;
+ lend_drop(1 << 30, nloc);
lbase = nloc; fdepth = 1; fslots = 0; fchk = chk && check_on; fret = ret; fty = 0; finfer = 0; fn_return_type = 0;
live = 1; bxdepth = 0; epichain = 0; infn = 1; loopbase = lsp; lv = 0; lvk = LV_NONE; xbor = -1;
return 0;
@@ -9168,7 +9236,7 @@ int syntax_resident(int symbol) {
word block = chk_resize(0, 0, 4096);
syntax_host_symbols = chk_resize(0, 0, from); syntax_host_floor = ubase; syntax_host_from = from;
top = block + block[-5] - rt.HDR;
- block[0] = rt.code_blocks; block[1] = block + 3; block[2] = top; rt.code_blocks = m*block;
+ block[0] = rt.code_blocks; block[1] = block + 3; block[2] = top; rt.code_blocks = block;
if (rt.syscall(9, 9, top, 0) < top) fatal("syntax code address reservation failed");
here = block + 3; hdelta = 0; hlimit = top; image_active = 0; vislo = 0;
clone = syntax_host_function(symbol); if (clone >= 0 && (sflag[clone] & F_DEF)) entry = sval[clone];
diff --git a/tests/errors/lend_branch.zc b/tests/errors/lend_branch.zc
new file mode 100644
index 0000000..594047f
--- /dev/null
+++ b/tests/errors/lend_branch.zc
@@ -0,0 +1,6 @@
+namespace;
+include rt;
+
+// expect: buf: is lent to global sink; assign the global again before the block that lent it ends
+word sink;
+fn main() { word buf = rt.new(4); sink = m*buf; if (buf[0]) { sink = 0; } return 0; }
diff --git a/tests/errors/lend_give.zc b/tests/errors/lend_give.zc
new file mode 100644
index 0000000..2fd2f5b
--- /dev/null
+++ b/tests/errors/lend_give.zc
@@ -0,0 +1,6 @@
+namespace;
+include rt;
+
+// expect: buf: is lent to global sink; assign the global again before giving the local away
+word sink;
+fn main() { word buf = rt.new(4); sink = m*buf; rt.free(buf); sink = 0; return 0; }
diff --git a/tests/errors/lend_return.zc b/tests/errors/lend_return.zc
new file mode 100644
index 0000000..b0c4b53
--- /dev/null
+++ b/tests/errors/lend_return.zc
@@ -0,0 +1,6 @@
+namespace;
+include rt;
+
+// expect: buf: is lent to global sink; assign the global again before the block that lent it ends
+word sink;
+fn main() { word buf = rt.new(4); sink = m*buf; if (buf[0]) return 1; sink = 0; return 0; }
diff --git a/tests/errors/lend_unrestored.zc b/tests/errors/lend_unrestored.zc
new file mode 100644
index 0000000..1b316df
--- /dev/null
+++ b/tests/errors/lend_unrestored.zc
@@ -0,0 +1,6 @@
+namespace;
+include rt;
+
+// expect: buf: is lent to global sink; assign the global again before the block that lent it ends
+word sink;
+fn main() { word buf = rt.new(4); sink = m*buf; buf[0] = 1; return 0; }
diff --git a/tests/errors/move_live_owner.zc b/tests/errors/move_live_owner.zc
new file mode 100644
index 0000000..e36909c
--- /dev/null
+++ b/tests/errors/move_live_owner.zc
@@ -0,0 +1,5 @@
+namespace;
+include rt;
+
+// expect: still owns an object; overwriting it would leak it
+fn main() { word r = rt.new(4); { word n = rt.new(8); n[0] = 1; r = n; } r[0] = 2; return 0; }
diff --git a/tests/errors/recast_plain.zc b/tests/errors/recast_plain.zc
new file mode 100644
index 0000000..7d26f3c
--- /dev/null
+++ b/tests/errors/recast_plain.zc
@@ -0,0 +1,6 @@
+namespace;
+include rt;
+
+// expect: p: isn't an owned object, so there is no ownership to recast
+word sink;
+fn main() { word p = m*rt.new(4); sink = d*p; return 0; }
diff --git a/tests/errors/unused.zc b/tests/errors/unused.zc
index 5c88ce3..7a1c7e4 100644
--- a/tests/errors/unused.zc
+++ b/tests/errors/unused.zc
@@ -1,5 +1,5 @@
namespace;
include rt;
-// expect: x is never read
+// expect: x: owned object is never used
fn main() { let x = rt.new(3); return 0; }
diff --git a/tests/programs/ownmove.zc b/tests/programs/ownmove.zc
new file mode 100644
index 0000000..76a2a17
--- /dev/null
+++ b/tests/programs/ownmove.zc
@@ -0,0 +1,39 @@
+namespace;
+include rt;
+
+// moves between owned locals and bounded lends of a local to a global.
+word sink;
+// the scratch buffer is lent to sink while helper reads it through the global.
+int helper() { return sink[0] + sink[1]; }
+int lend(int a, int b) {
+ word buf = rt.new(4);
+ buf[0] = a; buf[1] = b;
+ word saved = sink;
+ sink = m*buf;
+ int s = helper();
+ buf[0] = s;
+ sink = saved;
+ return buf[0];
+}
+// a replacement built in an inner block becomes the outer owner once the old one is released.
+int grow(int n) {
+ word r = rt.new(2);
+ r[0] = n;
+ if (n > 0) {
+ word bigger = rt.new(8);
+ bigger[0] = r[0] * 10;
+ rt.free(r);
+ r = bigger;
+ }
+ return r[0];
+}
+fn main() {
+ rt.put_n(lend(3, 4)); rt.put_c(32);
+ rt.put_n(grow(5)); rt.put_c(32);
+ rt.put_n(grow(0)); rt.put_c(32);
+ int i = 0;
+ while (i < 100) { grow(i); i++; }
+ rt.gc();
+ rt.put_n(sink == 0); rt.put_c(10);
+ return 0;
+}
diff --git a/tests/runerr.sh b/tests/runerr.sh
index 23ea054..f7e668c 100755
--- a/tests/runerr.sh
+++ b/tests/runerr.sh
@@ -1,9 +1,9 @@
#!/bin/bash
# runs every .zc in a folder (tests/errors by default) and checks the compiler
-# rejects it with the message on its first line: // expect: ...
+# rejects it with the message on its first `// expect: ...` line
vm=${1:-zy}; dir=${2:-$(dirname "$0")/errors}; pass=0; fail=0
for f in "$dir"/*.zc; do
- want=$(head -1 "$f" | sed 's#^// expect: ##')
+ want=$(grep -m1 '^// expect: ' "$f" | sed 's#^// expect: ##')
got=$($vm < "$f" 2>&1 | head -1)
if [[ "$got" == *"$want"* ]]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL $(basename $f): got [$got]"; fi
done
diff --git a/tests/test.sh b/tests/test.sh
index b80f548..16aa1a9 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -116,6 +116,7 @@ run(){ # $1 = vm command, $2 = label
chk "$($vm < tests/programs/o3push.zc | md5sum)" "$(md5sum < tests/programs/o3push.expected)" "$l push and pop of a growable array are inline in o3"
chk "$($vm < tests/programs/o3copy.zc)" "520507 1005" "$l o3 copies of a loop variable read the source instead"
chk "$($vm < tests/programs/borrowmove.zc)" "6" "$l storing into a borrowed object moves it"
+ chk "$($vm < tests/programs/ownmove.zc)" "7 50 0 1" "$l an owned local moves into a released owner, and a global borrows a local until it is restored"
chk "$($vm < tests/programs/alias.zc)" "11 22 4 8" "$l aliases of a live owner: arithmetic, field, index"
chk "$($vm < tests/programs/quarantine.zc)" "0 1234" "$l quarantine: stale alias reads intact data, second free is a no-op"
chk "$($vm < tests/programs/recycle.zc)" "1 0 1" "$l recycle returns a dropped block to its size class"
@@ -160,7 +161,7 @@ ABC" "$l o3 lifts byte loads and stores"
chk "$($vm < examples/strings.zc)" "hello, zc
9 42" "$l byte string example"
chk "$(ZC_BSTR=zybyte $vm < tests/programs/bstr_io.zc)" "byte-io 7 7 1 6 122 101" "$l byte-string file and getenv"
- chk "$($vm < tests/programs/manyerrors.zc | tr '\n' '/')" "zc: line 3: expected an expression, found ';'/zc: line 4: x: m object is never used/zc: line 5: type error: initializer: expected char*, got int/zc: line 6: expected ';', found '}'/zc: 4 errors/" "$l several errors in one run"
+ chk "$($vm < tests/programs/manyerrors.zc | tr '\n' '/')" "zc: line 6: expected an expression, found ';'/zc: line 7: x: owned object is never used/zc: line 8: type error: initializer: expected char*, got int/zc: line 9: expected ';', found '}'/zc: 4 errors/" "$l several errors in one run"
chk "$($vm < tests/programs/overflow_return.zc 2>&1)" "zy: return stack overflow" "$l return stack overflow is caught"
chk "$($vm < tests/programs/overflow_data.zc 2>&1)" "zy: data stack overflow" "$l data stack overflow is caught"
chk "$($vm < tests/programs/overflow_frame.zc 2>&1)" "zc: out of heap memory" "$l frame growth stops at a configured memory limit"
@@ -489,7 +490,7 @@ s_lsp() {
if bash src/tools/mklsp.sh $T/lsp >/dev/null 2>&1; then
lsp=$T/lsp/zclsp
chk "$($lsp < examples/hello.zc)" "no errors" "lsp: a clean program"
- chk "$($lsp < tests/errors/back_to_m.zc | head -1 | cut -d' ' -f2-)" "error: $(head -1 tests/errors/back_to_m.zc | sed 's#^// expect: ##')" "lsp: the compiler's own message"
+ chk "$($lsp < tests/errors/back_to_m.zc | head -1 | cut -d' ' -f2-)" "error: $(grep -m1 '^// expect: ' tests/errors/back_to_m.zc | sed 's#^// expect: ##')" "lsp: the compiler's own message"
h=$($lsp < examples/opt_test.zc)
for w in "is never read" "is never reassigned; make it a const" "is a shift; write << 3" "unannotated numeric literal 1234" "new inside a loop" "stack buffer"; do
case "$h" in *"$w"*) chk 1 1 "lsp hint: $w";; *) chk 0 1 "lsp hint: $w";; esac