aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-15 09:56:19 +0330
committerGravatar zinc <zinc@downsta.ir> 2026-10-15 09:56:19 +0330
commit6bbac2edf7c50dac4d755a364b6dfd16471284d7 (patch)
treee83b493f55ed07287cd2ab4e3a940c04871d9a14
parentanalyzer: use-after-end messages name the borrower, the owner and how the own... (diff)
analyzer: check borrow returns of forward-called functions at the unit's end; borrows clause for prototypes, definitions, externs and function types, checked against the body
-rw-r--r--TODO.md2
-rw-r--r--docs/SPEC.md6
-rw-r--r--src/compiler/analyzer.zc48
-rw-r--r--src/compiler/base.zc3
-rw-r--r--src/compiler/builtins.zc6
-rw-r--r--src/compiler/decl.zc17
-rw-r--r--src/compiler/expr.zc18
-rw-r--r--src/compiler/types.zc32
-rw-r--r--tests/errors/borrows_body.zc13
-rw-r--r--tests/errors/borrows_decl.zc14
-rw-r--r--tests/errors/borrows_extern.zc11
-rw-r--r--tests/errors/borrows_fntype.zc12
-rw-r--r--tests/errors/forward_borrow_return.zc11
-rw-r--r--tests/programs/borrows_clause.zc11
-rw-r--r--tests/programs/forward_borrow.zc16
-rwxr-xr-xtests/test.sh2
16 files changed, 212 insertions, 10 deletions
diff --git a/TODO.md b/TODO.md
index e18045d..558d834 100644
--- a/TODO.md
+++ b/TODO.md
@@ -531,7 +531,7 @@ brief someone without re-reading the code.
- [x] (done on work/dtor, docs/design/destructors.md, spec 3.11.6, 4.10.3-4, 6.15) destructors: drop works on any heap value, set once per value with `drop(v, fn(){...this...})`, `v.drop(f)` or `v.drop = f`, and a struct may declare `fn drop()` as its type default (both run: the value's, then the type's). `heap(v)` boxes an int, a string or a struct into a fresh owned block. drops run where the compiler frees an owned value (block end, early exit, explicit `rt.free`/`rt.recycle`, a temporary at statement end with a hint), parent before children, and the collector runs an unreachable value's drop once after the sweep, resurrection included; a freed block forgets its drop, so a reused address runs no old drop. no drops set or ended inside o3, and no collection inside an o3 region (pending until the outermost exit). copying a value with drop into an actor message is an error. tests: `tests/conformance/6-ownership/drop_*.zc`, `tests/actor/reject/drop_*.zc`, `tests/data/drop_hint.zc`.
- [x] (done on work/dtor2, spec 4.10.3, 4.11, 6.8.5, 6.15.2-5, docs/design/destructors.md "since" and "first users") destructors, the rest: reassigning an `m` owner frees the old value with a hint (`u`, `d` and captured owners keep the leak error; a new value made from the old one is an error); `pop` out of a droppable array is refused; the spawn hint (not for actor structs); memreport's `(drop)`; `break`/`continue` leave `o3 { }` blocks; o3 receiver methods enter the region; droppable cycles finalize one per collection. first users: `std.File` from `Host.open` (drop closes the fd, `writefile` uses it) and declared actors (a generated drop awaits owed replies and closes the endpoint). noticed, not fixed: the lsp says `total is never reassigned; make it a const` for a `let` only assigned inside a closure (tests/actor/accept/lifecycle.zc), and suggests a stack buffer for an actor struct or a value with drop.
- [x] (done on work/line-tables, see "agent work at the end of the session") runtime source positions. architecture: at each statement start (`stmt()`) record (code address `here`, line); per function a permanent data block [entry, end, name, file, count, pairs...] linked into a runtime list (`rt.line_blocks`), or appended to run-only images by `build_image` like the symbol blob. `lines on;`/`lines off;` like `docs on;`; on for user programs, off for the genome unless the growth is small. a new syscall op returns the return-stack pointer (the return stack is in vm memory, 0x1A4100..0x1B40FF, growing up); implement it in `src/core/core.c`, `src/micro/micro.c`, `src/js/tinyvm.ts` and the wasm port. `rt.trace()` walks the return stack, printing ` at file:line in function` for words inside a known function range. the runtime's fatal reports (`oob`, bad heap pointer, double free, out of memory, frame overflow) print the trace after their unchanged first line. check mode skips all of it.
-- [ ] borrow returns tied to an argument. inference already works for a callee defined before the call: `char* skip(char* s) { ...; return s + i; }` makes `let r = skip(t); rt.free(t); r[0]` an error. the gap: when the call comes before the callee's definition, nothing is reported and the program reads freed memory. extend the deferred effect requirements (`effect_require`, `effect_finish` at unit end) so a forward-called function whose return turns out to alias a parameter produces the same error. for bodyless declarations, externs and function types, add an unobtrusive spelling, for example a `borrows s` clause after the parameter list (`char* skip(char* s) borrows s;`), checked against the body when one exists.
+- [x] borrow returns tied to an argument (done: forward calls are checked at the unit's end, see SPEC 6.6.5; `borrows s` for prototypes, definitions, externs and function types, SPEC 6.6.6). inference already works for a callee defined before the call: `char* skip(char* s) { ...; return s + i; }` makes `let r = skip(t); rt.free(t); r[0]` an error. the gap: when the call comes before the callee's definition, nothing is reported and the program reads freed memory. extend the deferred effect requirements (`effect_require`, `effect_finish` at unit end) so a forward-called function whose return turns out to alias a parameter produces the same error. for bodyless declarations, externs and function types, add an unobtrusive spelling, for example a `borrows s` clause after the parameter list (`char* skip(char* s) borrows s;`), checked against the body when one exists.
- [ ] variadic arguments and `std.print`. design: a pack is passed as one hidden argument, a caller-built read-only growable-array handle (`fixed T[?]` view: [len, cap, items, element size] in the caller's frame, so `len(a)` and `a[i]` work unchanged and nothing is allocated). `...int a` is a typed pack; several packs per function when their element types differ (`void f(...int a, ...char b)`): each extra argument goes to the pack whose element type matches its static type, an ambiguous match is an error, packs come after the fixed parameters. `...any args` packs each value with a kind code (int, float, char, string, bstr, pointer, ...) and its type record: `any` is a small built-in record { kind, value, type }. `...*any args` makes the call comma-free: after the fixed arguments, the parser reads unary expressions until `)`, commas optional, so `std.print("x = " name ", age " age "\n");` works and binary expressions must be parenthesized. `std.print(...*any args)` prints each argument by its kind (`put_n`, the float printer, `put_s`, `put_bs`, a character for `char`, hex for pointers).
- [x] `memdebug`'s `obj_base` used to search only 64 words back for a block header, so `buf[255]` on a 256-word block reported "bad heap pointer". it now rounds the pointer down to each size class's alignment (largest first) and takes the first header whose check word, size and class agree and whose payload holds the pointer, so interiors at any depth work, a deep pointer into a freed block reports use after free, and header words and free space still report "bad heap pointer". regression: `tests/programs/memdebug_interior.zc` (in `tests/test.sh` beside the other memdebug checks). note that a stray pointer that lands inside another live block is accepted, since memdebug cannot know which block it came from.
- [ ] (partly done, see "agent work at the end of the session") `defer` follow-ups (small): done on work/defer: `defer` takes any single statement (if/else, loops, switch, unchecked, unbounded). still open: decide whether deferred code may name an owner declared after the `defer` (now rejected by the lexical rule; one line in `defer_one`).
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 923e24b..b2aa5ea 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -890,6 +890,10 @@ placing `rt.free(b)` after the use is accepted (the alias's last use precedes th
6.6.4 a checked function cannot return an alias of a local owner (error: a borrow of a local owner cannot escape through return; return the owner). it can return an alias of a parameter (the caller owns it), but not as an owned result of `.u` (error: a borrowed parameter cannot be returned as an owned result; a borrowed projection cannot be returned as an owned result).
+6.6.5 a function called before its body is checked the same way once the body has been read: the use of the result after the owner ended is remembered at the call, and reported at the end of the unit if the body returns a borrow of that argument (`...; skip, defined after this call, returns a borrow of its argument`). only a result bound directly to a local (`let r = skip(t);`) is followed; the check never fires for a body that does not return a borrow.
+
+6.6.6 `borrows s` after the parameter list of a prototype, a definition, an `extern` or a function type says the result is a borrow of parameter `s` and of no other parameter. a parameter is named, or given by its position from 0 where it has no name (`extern char* strchr(char*, int) borrows 0;`, `char*(char*) borrows 0 f = skip;`); a declaration may list several (`borrows a, b`), a function type one. the clause is part of the signature: a definition after a prototype keeps the prototype's clause, and a definition whose body returns a borrow of a parameter the clause does not name is an error (`first: returns a borrow of b, but its borrows clause doesn't name it`). without a clause, a bodyless callee is assumed to return a borrow of every argument, so arguments with different owners leave the result untracked; with one, only the named argument's owner is tied to the result.
+
### 6.7 recasts
`u*x`, `d*x`, `m*x` (no space between tag and `*`).
@@ -949,6 +953,8 @@ 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; 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
+- F: returns a borrow of P, but its borrows clause doesn't name it
+- borrows names a parameter, by name or by its position from 0
- 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 48debee..103f732 100644
--- a/src/compiler/analyzer.zc
+++ b/src/compiler/analyzer.zc
@@ -83,6 +83,51 @@ int effect_specialize(int original, int instance, word requirements, word edges)
while (e && e != edges) { if (e[3] == original) e[3] = instance; e = e[0]; }
return 0;
}
+// borrow returns of functions called before their body. such a call's result
+// may turn out to alias an argument's owner, which is only known once the body
+// has been read. the result's local keeps a shadow record (callee, the mask of
+// parameters that got the owner, the owner, the token after the call, so only a call that is the whole right side counts); a
+// use of the local after its owner ended is remembered, and effect_finish
+// reports it when the callee's return aliases one of those parameters.
+word shadow_new(int callee, int mask, int owner) {
+ word e;
+ if (!fchk || !live || dry || owner < 0 || !mask) return 0;
+ e = talloc(4); e[0] = callee; e[1] = mask; e[2] = owner; e[3] = tpack();
+ return e;
+}
+// the local i is assigned: it takes the shadow of a forward call that is its whole right side.
+int shadow_take(int i) {
+ if (!local_shadow) return 0;
+ local_shadow[i] = 0;
+ // the local read just before is the target of this assignment, not a use
+ if (effect_shadows && effect_shadows[10] == i && effect_shadows[11] == lreadpos[i]) effect_shadows = effect_shadows[0];
+ if (xshadow && xshadow[3] == tpack()) local_shadow[i] = xshadow;
+ xshadow = 0; return 0;
+}
+int shadow_use(int i) {
+ word s = local_shadow[i], e; int o = s[2];
+ if (!fchk || !live || lstate[o] != S_DEAD) return 0;
+ local_shadow[i] = 0;
+ e = talloc(12); e[10] = i; e[11] = tpack(); e[0] = effect_shadows; e[1] = s[0]; e[2] = s[1]; e[3] = tpack(); e[4] = curinc; e[5] = effect_floor;
+ e[6] = lname[i]; e[7] = 0; if (local_end_info) e[7] = local_end_info[o]; e[8] = lname[o]; e[9] = local_ended[o];
+ effect_shadows = e; return 0;
+}
+int shadow_finish(int from) {
+ word e = effect_shadows, next; int flags, k;
+ effect_shadows = 0;
+ while (e) {
+ next = e[0];
+ if (e[5] < from) { e[0] = effect_shadows; effect_shadows = e; e = next; continue; }
+ flags = 0; k = 0;
+ while (k < 24) { if ((e[2] >> k) & 1) flags = flags | effect_get(e[1], k); k++; }
+ if ((flags & (EF_ALIAS | EF_IDENTITY)) && !(flags & EF_UNKNOWN) && !dry) {
+ tunpack(e[3]); curinc = e[4]; panic = 0;
+ errat(); ended_message(e[6], e[7], e[8], e[9]); rt.put_s("; "); rt.put_s(sname[e[1]]); rt.put_s(", defined after this call, returns a borrow of its argument"); errend();
+ }
+ e = next;
+ }
+ return 0;
+}
int effect_finish(int from) {
word e, r, next; int changed = 1, flags, prior, save = tpack(), inc = curinc, i = 0;
if (chkabort) return 0;
@@ -110,6 +155,7 @@ int effect_finish(int from) {
}
e = next;
}
+ shadow_finish(from);
tunpack(save); curinc = inc; return 0;
}
int borrow_begin() { return 0; }
@@ -393,6 +439,7 @@ int newloc(int n) {
if (!local_captured) local_captured = chk_resize(0, 0, loccap); local_captured[i] = 0;
if (!local_ended) local_ended = chk_resize(0, 0, loccap); local_ended[i] = 0;
if (!local_end_info) local_end_info = chk_resize(0, 0, loccap); local_end_info[i] = 0;
+ if (!local_shadow) local_shadow = chk_resize(0, 0, loccap); local_shadow[i] = 0;
lpos[i] = 0;
lreadpos[i] = 0;
lowner[i] = 0;
@@ -1459,6 +1506,7 @@ int reassign_free(int i, int rc, int bor) {
// a store of class rc into an lvalue of kind k (local or container index i)
int assigncheck(int k, int i, int rc) {
int st, j, r, js, bor, origin = effect_origin(rc);
+ if (k == LV_LOCAL) shadow_take(i); else xshadow = 0;
if (k == LV_LOCAL && local_origin) { local_origin[i] = origin; local_origin_mask[i] = local_origin_mask[i] | effect_mask(rc); }
if (k == LV_ELEM) { effect_note_mask(effect_mask(C_VAR + i), EF_WRITE); }
if (k != LV_LOCAL && isptr(xty)) effect_note_mask(effect_mask(rc), EF_RETAIN);
diff --git a/src/compiler/base.zc b/src/compiler/base.zc
index 32549d4..bfed062 100644
--- a/src/compiler/base.zc
+++ b/src/compiler/base.zc
@@ -254,7 +254,7 @@ 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.
const int EF_READ = 1, EF_WRITE = 2, EF_RETAIN = 4, EF_CONSUME = 8, EF_UNKNOWN = 16, EF_ALIAS = 32, EF_IDENTITY = 64;
word effect_symbols, effect_edges, effect_requirements, effect_current; int effect_floor, effect_argument_base;
-word local_origin, local_origin_mask, local_cell, local_captured, local_ended, local_end_info; word effect_value, argorigin_mask; int capture_enabled; word[1] capture_dummy;
+word local_origin, local_origin_mask, local_cell, local_captured, local_ended, local_end_info, local_shadow, xshadow, effect_shadows; int argowner_mask; word effect_value, argorigin_mask; int capture_enabled; word[1] capture_dummy;
// drops (docs/design/destructors.md): the code position just past a drop form (a fresh
// value with a drop still on the stack there), the type `this` gets in the next closure,
// a local whose drop is set, a mailbox send whose arguments are checked, and o3 block depth.
@@ -758,6 +758,7 @@ int reserve_locals(int need) {
if (local_captured) local_captured = chk_resize(local_captured, nloc, cap);
if (local_ended) local_ended = chk_resize(local_ended, nloc, cap);
if (local_end_info) local_end_info = chk_resize(local_end_info, nloc, cap);
+ if (local_shadow) local_shadow = chk_resize(local_shadow, nloc, cap);
lafact = chk_resize(lafact, nloc, cap); laaddr = chk_resize(laaddr, nloc, cap); lprev = chk_resize(lprev, nloc, cap);
if (lvkind) { lvkind = chk_resize(lvkind, nloc, cap); lvvalue = chk_resize(lvvalue, nloc, cap); lvescaped = chk_resize(lvescaped, nloc, cap); }
o3cache = chk_resize(o3cache, nloc, cap);
diff --git a/src/compiler/builtins.zc b/src/compiler/builtins.zc
index d28d2fd..f40e223 100644
--- a/src/compiler/builtins.zc
+++ b/src/compiler/builtins.zc
@@ -1119,6 +1119,7 @@ int kw_extern() {
}
}
expect(s_rp);
+ int borrowed = borrows_clause(0, np, 1);
if (accept(s_eq)) {
if (tk != T_STR) { die("expected the c name as a string"); return 0; }
cname = tv;
@@ -1194,7 +1195,10 @@ int kw_extern() {
ebn10(i);
i++;
}
- ebs(") { let b = "); ebn10(a); ebs(";");
+ ebs(")");
+ // the stub keeps the declaration's borrows clause, so calls see it in the signature
+ if (borrowed) { ebs(" borrows "); i = 0; g = 0; while (i < np) { if ((borrowed >> i) & 1) { if (g) ebs(", "); ebs("p"); ebn10(i); g = 1; } i++; } }
+ ebs(" { let b = "); ebn10(a); ebs(";");
// the native descriptor borrows an integer address, not ownership of r.
// clear that scratch address after the call so it cannot keep the result rooted.
if (sret) { ebs(" "); ebs(rn); ebs("* r = rt.new("); ebs(rn); ebs("); b[3] = core.addr_b(r) >> 3;"); }
diff --git a/src/compiler/decl.zc b/src/compiler/decl.zc
index dfcff4f..52d08c7 100644
--- a/src/compiler/decl.zc
+++ b/src/compiler/decl.zc
@@ -282,6 +282,18 @@ int body() {
// (this is also where a function's signature is packed into ssig and its type
// into stype, checked against any earlier prototype or prescan result, and
// where the o3 region for an o3 function is handed to the core)
+// a body against its borrows clause: it may return a borrow only of the parameters the clause names.
+int borrows_check(int s, int np, char** pn, word pty) {
+ int mask = sig_borrows(ssig[s]), i = 0;
+ if (!mask || !fchk || !effect_current || effect_current[0] != 1) return 0;
+ while (i < np && i < 24) {
+ if (!((mask >> i) & 1) && (effect_current[26 + i] & (EF_ALIAS | EF_IDENTITY)) && (isptr(pty[i]) || pty[i] == tword)) {
+ if (!panic && !dry) { errat(); rt.put_s(sname[s]); rt.put_s(": returns a borrow of "); if (pn[i]) rt.put_s(pn[i]); else rt.put_n(i); rt.put_s(", but its borrows clause doesn't name it"); errend(); }
+ }
+ i++;
+ }
+ return 0;
+}
int fndef(int chk, int rty, int ret, int n) {
int s, np = 0, tags = 0, t, sig, i; char*[24] pn; word[24] pt; word[24] pty; int typed = 0, ft, explicit_rty = !!rty, o3f = o3nxt, lo, hi, pk, lt, lend, lfl = tline, redef, amb = 0, nonm = 0;
o3nxt = 0;
@@ -348,6 +360,10 @@ int fndef(int chk, int rty, int ret, int n) {
amb = amb >> 1; i++;
}
sig = ret + np + np + tags * 64;
+ lo = borrows_clause(pn, np, 1);
+ // a definition after a prototype keeps the prototype's borrows clause
+ if (!lo && (sflag[s] & F_SIG) && !(sflag[s] & F_PRE)) lo = sig_borrows(ssig[s]);
+ sig = sig + (lo << SIG_BORROWS);
if ((sflag[s] & F_SIG) && !(sflag[s] & F_PRE)) {
if (ssig[s] != sig) serrn(n, "doesn't match its earlier declaration");
}
@@ -408,6 +424,7 @@ int fndef(int chk, int rty, int ret, int n) {
i = 0; while (i < np) { t = lbase + i; o3mkcache(t); o3refresh(t); i++; }
}
body();
+ borrows_check(s, np, pn, pty);
if (!explicit_rty && fn_return_type > 0 && fn_return_type != tword) {
stype[s] = tfn(fn_return_type, np, pty, ssig[s]);
sig_attach(n, fn_return_type, np, pn, pty, 1);
diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc
index d9c91d6..a6cc55c 100644
--- a/src/compiler/expr.zc
+++ b/src/compiler/expr.zc
@@ -63,7 +63,7 @@ int stmtonly() {
// fat asks for every pointer argument to be fattened first, for o3 code
// calling code that is not o3.)
int args(int tags, word ft, int bs, int fat) {
- int np = 0, c, pt, t, cs = ifcall, freeing = 0, harmless = 0; word first = 0; int origin = -1, anytemp = 0, current, tag, place, parameter_base = effect_argument_base, owner = -1, current_owner, owner_seen = 0, aliases; word origins = 0, current_mask, owner_fact = 0;
+ int np = 0, c, pt, t, cs = ifcall, freeing = 0, harmless = 0; word first = 0; int origin = -1, anytemp = 0, current, tag, place, parameter_base = effect_argument_base, owner = -1, current_owner, owner_seen = 0, aliases; word origins = 0, current_mask, owner_fact = 0; int owner_mask = 0, borrowed = callee_borrows(cs, ft);
if (cs >= 0 && cs < ubase) {
freeing = sname[cs] == s_free || sname[cs] == s_recycle;
harmless = sname[cs] == s_put_s || rt.eq_w(sname[cs], "rt.put_n") || rt.eq_w(sname[cs], "rt.put_c") || rt.eq_w(sname[cs], "len") || rt.eq_w(sname[cs], "cap");
@@ -88,9 +88,11 @@ int args(int tags, word ft, int bs, int fat) {
if (c >= C_VAR && tracked(c - C_VAR)) current_owner = lroot(c - C_VAR);
if (tag <= 1 && current_owner >= 0 && tracked(current_owner)) effect_require_mode(cs, np + parameter_base, place, 1);
aliases = effect_get(cs, np + parameter_base);
+ if (borrowed) { aliases = 0; if (np + parameter_base < 12 && ((borrowed >> (np + parameter_base)) & 1)) aliases = EF_ALIAS; }
if (aliases & (EF_ALIAS | EF_IDENTITY | EF_UNKNOWN)) {
- if (!owner_seen) { owner = current_owner; owner_fact = xaf; owner_seen = 1; }
+ if (!owner_seen) { owner = current_owner; owner_fact = xaf; owner_seen = 1; owner_mask = 0; }
else if (owner != current_owner) { owner = -1; owner_fact = 0; }
+ if (owner >= 0 && owner == current_owner && np + parameter_base < 24) owner_mask = owner_mask | (1 << (np + parameter_base));
}
if (fchk && c == C_FRESH && tag != 2) {
if (!(cs < 0 && tag == 1)) effect_require(cs, np + parameter_base, place); emit_borrow_hold(); c = C_PLAIN; anytemp = 1;
@@ -127,7 +129,7 @@ int args(int tags, word ft, int bs, int fat) {
}
}
expect(s_rp);
- argfact = first; argorigin = origin; argorigin_mask = origins; argtemporary = anytemp; argowner = owner; argowner_fact = owner_fact; xaf = 0; vf_effect();
+ argfact = first; argorigin = origin; argorigin_mask = origins; argtemporary = anytemp; argowner = owner; argowner_mask = owner_mask; argowner_fact = owner_fact; xaf = 0; vf_effect();
return np;
}
// memdebug records the source line on the next new, via the runtime global
@@ -487,13 +489,16 @@ int call(int s) {
xlenloc = -1; xstamp = 0;
if (o3on && (sflag[s] & F_O3) && ft && hasptr(xty)) xstamp = 1;
borrow_depth--; effect_value = 0;
- if (!ret && (isptr(xty) || xty == tword)) {
+ if (!ret && (isptr(xty) || xty == tword || callee_borrows(original, ft))) {
effect_value = origins;
- if (owner >= 0 && (isptr(xty) || (effect_get(original, 0) & EF_IDENTITY))) xbor = owner;
+ if (owner >= 0 && (isptr(xty) || (effect_get(original, 0) & EF_IDENTITY) || callee_borrows(original, ft))) xbor = owner;
if (owner_fact && (effect_get(original, 0) & EF_IDENTITY)) xaf = owner_fact;
if (origin >= 0 && owner < 0) { int oi = lbase; while (oi < nloc) { if (lrskind[oi] == RS_PARAM && oi - lbase == origin) { xbor = oi; break; } oi++; } }
if (temporary) { int ai = 0, aliases = 0; while (ai < np) { aliases = aliases | effect_get(original, ai); ai++; } if (aliases & (EF_ALIAS | EF_UNKNOWN)) xaf = af_new(-1, 5, cp); }
}
+ // a result from a function whose body comes later is checked once the body is read
+ xshadow = 0;
+ if (!ret && xbor < 0 && owner >= 0 && original >= ubase && !(sflag[original] & (F_DEF | F_EXT)) && tracked(owner) && (isptr(xty) || xty == tword)) xshadow = shadow_new(original, argowner_mask, owner);
if_value = 0;
if (ifs && !dry) {
word result_node = if_get(original);
@@ -1009,6 +1014,7 @@ int primary() {
if (i >= 0) {
if (if_local) if_value = if_local[i];
lreadpos[i] = tpack();
+ if (local_shadow && local_shadow[i]) shadow_use(i);
luse(i); effect_note_mask(effect_mask(C_VAR + i), EF_READ);
rs_note(lrskind[i], lrsoff[i]); os_at(toff, i);
xaf = lafact[i]; af_record(toff, xaf);
@@ -1588,7 +1594,7 @@ int postfix_tail(int c) {
xty = tword;
if (ft) xty = subst(ft[4], bs);
if (ft && (ft[5] & 1)) c = C_FRESH;
- else if (isptr(xty) || xty == tword) { effect_value = call_origins; if (isptr(xty)) xbor = call_owner; if (temporary) xaf = af_new(-1, 5, place); }
+ else if (isptr(xty) || xty == tword) { effect_value = call_origins; if (isptr(xty) || (ft && sig_borrows(ft[5]))) xbor = call_owner; if (temporary) xaf = af_new(-1, 5, place); }
xfloat = 0; if (isfl(xty)) xfloat = 1; xstr = 0;
continue;
}
diff --git a/src/compiler/types.zc b/src/compiler/types.zc
index 81547a5..322d0fd 100644
--- a/src/compiler/types.zc
+++ b/src/compiler/types.zc
@@ -105,6 +105,35 @@ word tsized(char* name, int bytes, int sgn) {
return t;
}
// the function type with this return, parameter list and signature.
+// ---- borrow returns ----
+// `borrows s` after a parameter list says the result is a borrow of parameter s
+// (several, comma separated, in a declaration). a parameter is named, or given by
+// its position from 0 where it has no name (an extern, a function type). the mask
+// lives in bits 50 to 61 of the signature, so a function type carries it too.
+const int SIG_BORROWS = 50;
+int sig_borrows(int sig) { return (sig >> SIG_BORROWS) & 4095; }
+// the declared borrow mask of a call's callee: its symbol's signature, or the function type.
+int callee_borrows(int s, word ft) {
+ if (s >= 0 && s < nsym && (sflag[s] & F_SIG)) return sig_borrows(ssig[s]);
+ if (ft && ft[0] == TY_FN) return sig_borrows(ft[5]);
+ return 0;
+}
+int borrows_clause(char** names, int np, int many) {
+ int mask = 0, k;
+ if (tk != T_ID || !rt.eq_w(tv, "borrows")) return 0;
+ next();
+ while (1) {
+ k = 0 - 1;
+ if (tk == T_NUM) { if (tv >= 0 && tv < np) k = tv; }
+ else if (tk == T_ID && names) { int j = 0; while (j < np) { if (names[j] && rt.eq_w(names[j], tv)) k = j; j++; } }
+ if (k < 0) { serr("borrows names a parameter, by name or by its position from 0"); next(); return mask; }
+ if (k >= 12) serr("only the first 12 parameters can be borrowed by the result");
+ else mask = mask | (1 << k);
+ next();
+ if (!many || !accept(s_comma)) break;
+ }
+ return mask;
+}
word tfn(int ret, int np, word ps, int sig) {
int i = 0, j; word t;
while (i < ntl) {
@@ -388,7 +417,8 @@ int ptype() {
}
}
expect(s_rp);
- t = tfn(t, np, ps, np + np + tags * 64);
+ tag = borrows_clause(0, np, 0);
+ t = tfn(t, np, ps, np + np + tags * 64 + (tag << SIG_BORROWS));
}
ht_note(start, toff, t);
return t;
diff --git a/tests/errors/borrows_body.zc b/tests/errors/borrows_body.zc
new file mode 100644
index 0000000..e6c92c6
--- /dev/null
+++ b/tests/errors/borrows_body.zc
@@ -0,0 +1,13 @@
+namespace;
+include rt;
+
+// expect: first: returns a borrow of b, but its borrows clause doesn't name it
+// the body is checked against the prototype's borrows clause.
+char* first(char* a, char* b) borrows a;
+fn main() {
+ char* t = rt.new(4); t[0] = 'x'; t[1] = 0;
+ char* u = rt.new(4); u[0] = 'y'; u[1] = 0;
+ let r = first(t, u); rt.put_c(r[0]); rt.free(u); rt.free(t);
+ return 0;
+}
+char* first(char* a, char* b) { return b; }
diff --git a/tests/errors/borrows_decl.zc b/tests/errors/borrows_decl.zc
new file mode 100644
index 0000000..5229979
--- /dev/null
+++ b/tests/errors/borrows_decl.zc
@@ -0,0 +1,14 @@
+namespace;
+include rt;
+
+// expect: r: used after it was given away; r borrows t, and t was freed on line 11 (t declared on line 8)
+// a prototype's borrows clause ties the result to the first argument's owner only.
+char* first(char* a, char* b) borrows a;
+fn main() {
+ char* t = rt.new(4); t[0] = 'x'; t[1] = 0;
+ char* u = rt.new(4); u[0] = 'y'; u[1] = 0;
+ let r = first(t, u); rt.free(u); rt.put_c(r[0]);
+ rt.free(t); rt.put_c(r[0]);
+ return 0;
+}
+char* first(char* a, char* b) { return a; }
diff --git a/tests/errors/borrows_extern.zc b/tests/errors/borrows_extern.zc
new file mode 100644
index 0000000..e9285d4
--- /dev/null
+++ b/tests/errors/borrows_extern.zc
@@ -0,0 +1,11 @@
+namespace;
+include rt;
+
+// expect: r: used after it was given away; r borrows t, and t was freed on line 9 (t declared on line 8)
+// an extern declares its borrow return by parameter position.
+extern char* strchr(char*, int) borrows 0;
+fn main() {
+ char* t = rt.new(4); t[0] = 'x'; t[1] = 0;
+ let r = strchr(t, 'x'); rt.free(t); rt.put_c(r[0]);
+ return 0;
+}
diff --git a/tests/errors/borrows_fntype.zc b/tests/errors/borrows_fntype.zc
new file mode 100644
index 0000000..a73b0d8
--- /dev/null
+++ b/tests/errors/borrows_fntype.zc
@@ -0,0 +1,12 @@
+namespace;
+include rt;
+
+// expect: r: used after it was given away; r borrows t, and t was freed on line 10 (t declared on line 9)
+// a function type carries a borrows clause, by parameter position.
+char* skip(char* s) { return s + 1; }
+fn main() {
+ char*(char*) borrows 0 f = skip;
+ char* t = rt.new(4); t[0] = 'x'; t[1] = 'y'; t[2] = 0;
+ let r = f(t); rt.free(t); rt.put_c(r[0]);
+ return 0;
+}
diff --git a/tests/errors/forward_borrow_return.zc b/tests/errors/forward_borrow_return.zc
new file mode 100644
index 0000000..6c3ae93
--- /dev/null
+++ b/tests/errors/forward_borrow_return.zc
@@ -0,0 +1,11 @@
+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 7); skip, defined after this call, returns a borrow of its argument
+// skip is called before its body: its borrow return is checked when the body is read.
+fn main() {
+ char* t = rt.new(8); t[0] = ' '; t[1] = 'a'; t[2] = 0;
+ let r = skip(t); rt.free(t); rt.put_c(r[0]);
+ return 0;
+}
+char* skip(char* s) { int i = 0; while (s[i] == ' ') i++; return s + i; }
diff --git a/tests/programs/borrows_clause.zc b/tests/programs/borrows_clause.zc
new file mode 100644
index 0000000..6f43581
--- /dev/null
+++ b/tests/programs/borrows_clause.zc
@@ -0,0 +1,11 @@
+namespace;
+include rt;
+// a borrows clause on a prototype and its definition: freeing the other argument keeps the result.
+char* first(char* a, char* b) borrows a;
+fn main() {
+ char* t = rt.new(4); t[0] = 'x'; t[1] = 0;
+ char* u = rt.new(4); u[0] = 'y'; u[1] = 0;
+ let r = first(t, u); rt.free(u); rt.put_c(r[0]); rt.free(t);
+ return 0;
+}
+char* first(char* a, char* b) borrows a { return a; }
diff --git a/tests/programs/forward_borrow.zc b/tests/programs/forward_borrow.zc
new file mode 100644
index 0000000..7cc44b1
--- /dev/null
+++ b/tests/programs/forward_borrow.zc
@@ -0,0 +1,16 @@
+namespace;
+include rt;
+// forward calls whose results do not outlive the owner, or do not borrow it, are accepted.
+fn main() {
+ char* t = rt.new(8); t[0] = ' '; t[1] = 'a'; t[2] = 0;
+ let n = count(t); rt.free(t); rt.put_n(n);
+ let u = rt.new(8); u[0] = 'b'; u[1] = 0;
+ let r = skip(u); rt.put_c(r[0]); rt.free(u);
+ let v = rt.new(8); v[0] = 0;
+ let q = skip(v); rt.free(v); q = 0; rt.put_n(q);
+ let w = rt.new(8); w[0] = 0;
+ let k = skip(w)[0]; rt.free(w); rt.put_n(k);
+ return 0;
+}
+int count(char* s) { int i = 0; while (s[i]) i++; return i; }
+char* skip(char* s) { int i = 0; while (s[i] == ' ') i++; return s + i; }
diff --git a/tests/test.sh b/tests/test.sh
index 7745a66..d8ad0fb 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -216,6 +216,8 @@ ABC" "$l o3 lifts byte loads and stores"
chk "$($vm < tests/programs/o3.zc)" "3142 42 43 9 57 98781 520625 10192 -18782 1152" "$l o3 regions: loops, calls, recursion, switch, arrays, more vars than registers"
chk "$($vm < tests/programs/o3scopes.zc)" "116" "$l o3 registers shared by scopes, cached fixed arrays"
chk "$($vm < tests/programs/borrow_rules.zc)" "$(cat tests/programs/borrow_rules.expected)" "$l inferred transfers, projections and shared closure cells"
+ chk "$($vm < tests/programs/forward_borrow.zc)" "2b00" "$l forward calls: results that do not borrow, or are not used after the owner ends, are accepted"
+ chk "$($vm < tests/programs/borrows_clause.zc)" "x" "$l a borrows clause ties the result to the named argument only"
chk "$($vm < tests/programs/replace_owner.zc)" "$(cat tests/programs/replace_owner.expected)" "$l replacing a released owner moves ownership and cleans up once"
chk "$($vm < tests/programs/temporary_borrows.zc)" "$(cat tests/programs/temporary_borrows.expected)" "$l temporary borrows preserve aliases and release allocations"
chk "$($vm < tests/programs/array_values.zc)" "$(cat tests/programs/array_values.expected)" "$l array initializers, designators, zero filling, nested and compound literals"