From 0a87c4663fc52bf60f3d440200b3b3ad6b39bd3e Mon Sep 17 00:00:00 2001 From: zinc Date: Thu, 15 Oct 2026 09:48:10 +0330 Subject: analyzer: use-after-end messages name the borrower, the owner and how the owner ended (freed, moved, .d parameter, push, global, reassignment), with fixtures --- docs/SPEC.md | 4 ++-- src/compiler/analyzer.zc | 31 ++++++++++++++++++++++--------- src/compiler/base.zc | 30 +++++++++++++++++++++++------- src/compiler/builtins.zc | 2 +- src/compiler/expr.zc | 2 +- tests/errors/use_after_free_borrow.zc | 10 ++++++++++ tests/errors/use_after_free_owner.zc | 9 +++++++++ tests/errors/use_after_global.zc | 10 ++++++++++ tests/errors/use_after_inferred_give.zc | 2 +- tests/errors/use_after_move.zc | 11 +++++++++++ tests/errors/use_after_push.zc | 10 ++++++++++ tests/errors/use_after_reassign.zc | 10 ++++++++++ 12 files changed, 110 insertions(+), 21 deletions(-) create mode 100644 tests/errors/use_after_free_borrow.zc create mode 100644 tests/errors/use_after_free_owner.zc create mode 100644 tests/errors/use_after_global.zc create mode 100644 tests/errors/use_after_move.zc create mode 100644 tests/errors/use_after_push.zc create mode 100644 tests/errors/use_after_reassign.zc 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]; +} -- cgit v1.2.3