aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-15 09:48:10 +0330
committerGravatar zinc <zinc@downsta.ir> 2026-10-15 09:48:10 +0330
commit0a87c4663fc52bf60f3d440200b3b3ad6b39bd3e (patch)
tree40d88acb972080ed90d89c7a866247263c252cae
parentmerge work/stdio: std.Io and one-level module completion (diff)
analyzer: use-after-end messages name the borrower, the owner and how the owner ended (freed, moved, .d parameter, push, global, reassignment), with fixtures
-rw-r--r--docs/SPEC.md4
-rw-r--r--src/compiler/analyzer.zc31
-rw-r--r--src/compiler/base.zc30
-rw-r--r--src/compiler/builtins.zc2
-rw-r--r--src/compiler/expr.zc2
-rw-r--r--tests/errors/use_after_free_borrow.zc10
-rw-r--r--tests/errors/use_after_free_owner.zc9
-rw-r--r--tests/errors/use_after_global.zc10
-rw-r--r--tests/errors/use_after_inferred_give.zc2
-rw-r--r--tests/errors/use_after_move.zc11
-rw-r--r--tests/errors/use_after_push.zc10
-rw-r--r--tests/errors/use_after_reassign.zc10
12 files changed, 110 insertions, 21 deletions
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 06ee11c..923e24b 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -884,7 +884,7 @@ char* tail(char* s) { return s + 1; }
char* b = rt.new(4); b[0] = 'a'; b[1] = 'b'; b[2] = 0;
char* t = tail(b);
rt.free(b);
-rt.put_s(t); // error: t: used after it was given away; owner b was transferred on line N (declared on line M)
+rt.put_s(t); // error: t: used after it was given away; t borrows b, and b was freed on line N (b declared on line M)
```
placing `rt.free(b)` after the use is accepted (the alias's last use precedes the transfer).
@@ -948,7 +948,7 @@ rt.outbuf = saved;
each fixture in `tests/errors/` starts with `// expect: MESSAGE`; the messages that the analyzer produces are (X is a variable name):
-- X: used after it was given away; owner Y was transferred on line N (declared on line M)
+- X: used after it was given away; X borrows Y, and Y was HOW on line N (Y declared on line M), or, for the owner itself, X was HOW on line N (declared on line M). HOW is one of: freed, freed by reassigning it, given to F's .d parameter, moved into Z, stored into Z, pushed into an array, stored in a global or untracked memory, returned, or transferred when the analyzer has no more specific cause
- X: owned object is never used
- X: still owns an object; overwriting it would leak it
- X: is an owned object and can't be copied into another variable
diff --git a/src/compiler/analyzer.zc b/src/compiler/analyzer.zc
index 42cb7b5..48debee 100644
--- a/src/compiler/analyzer.zc
+++ b/src/compiler/analyzer.zc
@@ -949,10 +949,16 @@ int lend_drop(int dep, int from) {
return 0;
}
// retire existing aliases together with their owner. dead aliases do not keep later loop snapshots live.
+// end_why and end_whom say how the next end_owner ends its owner, for the message
+// of a later use: 0 transferred, 1 freed, 2 given to a .d parameter of end_whom,
+// 3 moved into end_whom, 4 stored in a container, 5 returned, 6 stored in a global
+// or untracked memory, 7 freed by reassigning it, 8 pushed into end_whom.
+int end_why; char* end_whom;
int end_owner(int i) {
- int j = lbase; word info = talloc(3);
+ int j = lbase; word info = talloc(5);
lend_check(i);
- info[0] = lname[i]; info[1] = lpos[i]; info[2] = tpack();
+ info[0] = lname[i]; info[1] = lpos[i]; info[2] = tpack(); info[3] = end_why; info[4] = end_whom;
+ end_why = 0; end_whom = 0;
local_ended[i] = info[2]; local_end_info[i] = info; lstate[i] = S_DEAD;
while (j < nloc) {
if (j != i && isalias(j) && lroot(j) == i) { local_ended[j] = info[2]; local_end_info[j] = info; lstate[j] = S_DEAD; }
@@ -1345,6 +1351,8 @@ int recast(int c, int t) {
else lstate[i] = t;
return c;
}
+// where the argument argcheck is looking at goes: 0 unknown, 1 a push, else the callee's symbol + 2
+int arg_site;
// an argument for a parameter tagged t (0 plain, 1 m, 2 d)
int argcheck(int c, int t) {
int i, st;
@@ -1373,7 +1381,12 @@ int argcheck(int c, int t) {
touch(i);
return 0;
}
- if (st == S_D || ((st == S_M || st == S_U) && lown[i])) { end_owner(i); luse(i); xgive = 1; ev_put(i, EV_DOWN, -1); return 0; }
+ if (st == S_D || ((st == S_M || st == S_U) && lown[i])) {
+ if (arg_site >= 2 && arg_site - 2 < ubase && (sname[arg_site - 2] == s_free || sname[arg_site - 2] == s_recycle)) end_why = 1;
+ else if (arg_site == 1) end_why = 8;
+ else if (arg_site >= 2) { end_why = 2; end_whom = sname[arg_site - 2]; }
+ end_owner(i); luse(i); xgive = 1; ev_put(i, EV_DOWN, -1); return 0;
+ }
if (st == S_M) cerrn(i, "is m; write d*x to give it away");
else if (st == S_U) cerrn(i, "is u; write d*x to give it away instead of returning it");
else if (st == S_CHILD) cerrn(i, "belongs to a container and can't be given away alone");
@@ -1437,7 +1450,7 @@ int reassign_free(int i, int rc, int bor) {
if (lleaf[i] && !dry && lookup(s_recycle_leaf) >= 0) { lit(lleaf[i]); lit(szclass(lnsz[i])); callname(s_recycle_leaf); }
else callname(s_recycle);
op(O_DROP);
- end_owner(i);
+ end_why = 7; end_owner(i);
if (local_dropset) local_dropset[i] = 0;
lnsz[i] = 0; lleaf[i] = 0;
xty = t; xaf = af; xstr = st; xfloat = fl; xbor = bor;
@@ -1491,7 +1504,7 @@ int assigncheck(int k, int i, int rc) {
lstate[i] = S_M; lused[i] = 0; lown[i] = 1;
lnsz[i] = lnsz[j]; lleaf[i] = lleaf[j];
local_ended[i] = 0; local_end_info[i] = 0;
- end_owner(j); luse(j); ev_put(j, EV_MOVE, i);
+ end_why = 3; end_whom = lname[i]; end_owner(j); luse(j); ev_put(j, EV_MOVE, i);
if (ltag[i] & 15) lstate[i] = ltag[i] & 15;
return C_PLAIN;
}
@@ -1499,7 +1512,7 @@ int assigncheck(int k, int i, int rc) {
lstate[i] = S_M;
lused[i] = 0;
lown[i] = 1;
- end_owner(j);
+ end_why = 3; end_whom = lname[i]; end_owner(j);
luse(j); ev_put(j, EV_UP, i);
if (ltag[i] & 15) lstate[i] = ltag[i] & 15;
return C_PLAIN;
@@ -1523,7 +1536,7 @@ int assigncheck(int k, int i, int rc) {
if (js == S_U) {
cerrn(j, "is u; it has to leave through the block above, not into a borrowed object");
}
- else { end_owner(j); luse(j); ev_put(j, EV_STORE, i); emitown(i); } // storing it gives it away
+ else { end_why = 4; end_whom = lname[i]; end_owner(j); luse(j); ev_put(j, EV_STORE, i); emitown(i); } // storing it gives it away
} else if (js != st) cerrn(j, "its tag doesn't match its container's (both have to be m, u or d)");
else if (lhome[j] < lhome[r]) cerrn(j, "lives in an outer block than the container it's stored into");
else { lstate[j] = S_CHILD; lowner[j] = i; luse(j); ev_put(j, EV_STORE, i); emitown(i); }
@@ -1535,7 +1548,7 @@ int assigncheck(int k, int i, int rc) {
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); }
+ if (lstate[j] == S_D || lstate[j] == S_U || (lstate[j] == S_M && lown[j])) { ev_put(j, EV_GLOBAL, -1); end_why = 6; 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)");
}
return C_PLAIN;
@@ -1582,7 +1595,7 @@ int retcheck(int c) {
}
if (fret) {
if (i >= 0) {
- if (lstate[i] == S_U) { end_owner(i); luse(i); ev_put(i, EV_RETURN, -1); }
+ if (lstate[i] == S_U) { end_why = 5; end_owner(i); luse(i); ev_put(i, EV_RETURN, -1); }
else cerrn(i, "has to be u to be returned");
}
return 0;
diff --git a/src/compiler/base.zc b/src/compiler/base.zc
index dc786b5..32549d4 100644
--- a/src/compiler/base.zc
+++ b/src/compiler/base.zc
@@ -958,6 +958,27 @@ int cerr(char* msg) {
if (fchk && live) serr(msg);
return 0;
}
+// how an owner ended, from end_owner's record: the use, the owner, what ended it and where.
+// "r: used after it was given away; r borrows t, and t was freed on line 5 (t declared on line 4)"
+int ended_message(char* use, word info, char* root, int cause) {
+ char* owner = root; int why = 0; char* whom = 0;
+ if (info) { owner = info[0]; why = info[3]; whom = info[4]; }
+ rt.put_s(use); rt.put_s(": used after it was given away; ");
+ if (!rt.eq_w(use, owner)) { rt.put_s(use); rt.put_s(" borrows "); rt.put_s(owner); rt.put_s(", and "); }
+ rt.put_s(owner);
+ if (why == 1) rt.put_s(" was freed");
+ else if (why == 2) { rt.put_s(" was given to "); rt.put_s(whom); rt.put_s("'s .d parameter"); }
+ else if (why == 3) { rt.put_s(" was moved into "); rt.put_s(whom); }
+ else if (why == 4) { rt.put_s(" was stored into "); rt.put_s(whom); }
+ else if (why == 5) rt.put_s(" was returned");
+ else if (why == 6) rt.put_s(" was stored in a global or untracked memory");
+ else if (why == 7) rt.put_s(" was freed by reassigning it");
+ else if (why == 8) rt.put_s(" was pushed into an array");
+ else rt.put_s(" was transferred");
+ rt.put_s(" on line "); rt.put_n(cause & 0xFFFFFF);
+ if (info && info[1]) { rt.put_s(" ("); if (!rt.eq_w(use, owner)) { rt.put_s(owner); rt.put_c(' '); } rt.put_s("declared on line "); rt.put_n(info[1] & 0xFFFFFF); rt.put_c(')'); }
+ return 0;
+}
// report an analyzer error about local i, named in the message.
int cerrn(int i, char* msg) {
int root = i, cause = 0; word info = 0;
@@ -967,13 +988,8 @@ int cerrn(int i, char* msg) {
if (!info && local_end_info) info = local_end_info[root];
if (info) cause = info[2]; else if (local_ended) cause = local_ended[root];
}
- if (cause && !panic && !dry) {
- errat(); rt.put_s(lname[i]); rt.put_s(": "); rt.put_s(msg);
- rt.put_s("; owner "); if (info) rt.put_s(info[0]); else rt.put_s(lname[root]);
- rt.put_s(" was transferred on line "); rt.put_n(cause & 0xFFFFFF);
- if (info && info[1]) { rt.put_s(" (declared on line "); rt.put_n(info[1] & 0xFFFFFF); rt.put_c(')'); }
- errend();
- } else serrn(lname[i], msg);
+ if (cause && !panic && !dry) { errat(); ended_message(lname[i], info, lname[root], cause); errend(); }
+ else serrn(lname[i], msg);
}
return 0;
}
diff --git a/src/compiler/builtins.zc b/src/compiler/builtins.zc
index be8c3b4..d28d2fd 100644
--- a/src/compiler/builtins.zc
+++ b/src/compiler/builtins.zc
@@ -736,7 +736,7 @@ int array_push(word t) {
effect_note_mask(effect_value, EF_WRITE); af_mutate(xaf, tpack()); nogrow(t);
if (t[0] == TY_DYN && isagg(t[4])) serr("push takes scalar or reference elements; use resize for aggregates");
c = expr(); rval(); if (t[0] == TY_DYN) fcoerce(t[4]);
- if (isptr(xty) || xty == tword) effect_note_mask(effect_mask(c), EF_RETAIN); argcheck(c, 2);
+ if (isptr(xty) || xty == tword) effect_note_mask(effect_mask(c), EF_RETAIN); arg_site = 1; argcheck(c, 2); arg_site = 0;
if (t[0] == TY_DYN && !compat(t[4], xty)) tcerr("push", t[4], xty);
if (o3on && t[0] == TY_DYN && hasptr(t[4])) { lit(t[4]); callname(s_fatten); }
expect(s_rp);
diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc
index 7471556..d9c91d6 100644
--- a/src/compiler/expr.zc
+++ b/src/compiler/expr.zc
@@ -110,7 +110,7 @@ int args(int tags, word ft, int bs, int fat) {
}
if (ft && np + parameter_base < ft[3]) fcoerce(subst(ft[7 + np + parameter_base], bs));
t = xty;
- argcheck(c, (tags >> (np + np)) & 3);
+ arg_site = 0; if (cs >= 0) arg_site = cs + 2; argcheck(c, (tags >> (np + np)) & 3); arg_site = 0;
// o3 data handed to normal code
if (fat && hasptr(t)) {
lit(t);
diff --git a/tests/errors/use_after_free_borrow.zc b/tests/errors/use_after_free_borrow.zc
new file mode 100644
index 0000000..ed4e9d3
--- /dev/null
+++ b/tests/errors/use_after_free_borrow.zc
@@ -0,0 +1,10 @@
+namespace;
+include rt;
+
+// expect: r: used after it was given away; r borrows t, and t was freed on line 8 (t declared on line 6)
+fn main() {
+ char* t = rt.new(4); t[0] = 65;
+ char* r = t + 1;
+ rt.free(t);
+ return r[0];
+}
diff --git a/tests/errors/use_after_free_owner.zc b/tests/errors/use_after_free_owner.zc
new file mode 100644
index 0000000..e530bf5
--- /dev/null
+++ b/tests/errors/use_after_free_owner.zc
@@ -0,0 +1,9 @@
+namespace;
+include rt;
+
+// expect: t: used after it was given away; t was freed on line 7 (declared on line 6)
+fn main() {
+ let t = rt.new(4);
+ rt.free(t);
+ return t[0];
+}
diff --git a/tests/errors/use_after_global.zc b/tests/errors/use_after_global.zc
new file mode 100644
index 0000000..01a69c0
--- /dev/null
+++ b/tests/errors/use_after_global.zc
@@ -0,0 +1,10 @@
+namespace;
+include rt;
+
+// expect: a: used after it was given away; a was stored in a global or untracked memory on line 8 (declared on line 7)
+word g;
+fn main() {
+ let a = rt.new(4);
+ g = a;
+ return a[0];
+}
diff --git a/tests/errors/use_after_inferred_give.zc b/tests/errors/use_after_inferred_give.zc
index 19841cb..e393c29 100644
--- a/tests/errors/use_after_inferred_give.zc
+++ b/tests/errors/use_after_inferred_give.zc
@@ -1,6 +1,6 @@
namespace;
include rt;
-// expect: x: used after it was given away; owner x was transferred on line 6 (declared on line 6)
+// expect: x: used after it was given away; x was given to take's .d parameter on line 6 (declared on line 6)
fn take(.d p) { return p[0]; }
fn main() { let x = rt.new(3); take(x); return x[0]; }
diff --git a/tests/errors/use_after_move.zc b/tests/errors/use_after_move.zc
new file mode 100644
index 0000000..a5a9abb
--- /dev/null
+++ b/tests/errors/use_after_move.zc
@@ -0,0 +1,11 @@
+namespace;
+include rt;
+
+// expect: a: used after it was given away; a was moved into b on line 9 (declared on line 6)
+fn main() {
+ let a = rt.new(4);
+ let b = rt.new(4);
+ rt.free(b);
+ b = a;
+ return a[0] + b[0];
+}
diff --git a/tests/errors/use_after_push.zc b/tests/errors/use_after_push.zc
new file mode 100644
index 0000000..a4baf69
--- /dev/null
+++ b/tests/errors/use_after_push.zc
@@ -0,0 +1,10 @@
+namespace;
+include rt;
+
+// expect: a: used after it was given away; a was pushed into an array on line 8 (declared on line 7)
+fn main() {
+ word[?] items = rt.new(word[?]);
+ let a = rt.new(4);
+ push(items, a);
+ return a[0];
+}
diff --git a/tests/errors/use_after_reassign.zc b/tests/errors/use_after_reassign.zc
new file mode 100644
index 0000000..a295591
--- /dev/null
+++ b/tests/errors/use_after_reassign.zc
@@ -0,0 +1,10 @@
+namespace;
+include rt;
+
+// expect: r: used after it was given away; r borrows a, and a was freed by reassigning it on line 8 (a declared on line 6)
+fn main() {
+ let a = rt.new(4);
+ char* r = a + 1;
+ a = rt.new(4);
+ return r[0];
+}