diff options
| -rw-r--r-- | src/compiler/compiler.zc | 80 | ||||
| -rw-r--r-- | tests/errors/lend_branch.zc | 6 | ||||
| -rw-r--r-- | tests/errors/lend_give.zc | 6 | ||||
| -rw-r--r-- | tests/errors/lend_return.zc | 6 | ||||
| -rw-r--r-- | tests/errors/lend_unrestored.zc | 6 | ||||
| -rw-r--r-- | tests/errors/move_live_owner.zc | 5 | ||||
| -rw-r--r-- | tests/errors/recast_plain.zc | 6 | ||||
| -rw-r--r-- | tests/errors/unused.zc | 2 | ||||
| -rw-r--r-- | tests/programs/ownmove.zc | 39 | ||||
| -rwxr-xr-x | tests/runerr.sh | 4 | ||||
| -rwxr-xr-x | tests/test.sh | 5 |
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 |
