aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-15 11:29:26 +0330
committerGravatar zinc <zinc@downsta.ir> 2026-10-15 11:29:26 +0330
commita3f769ea02b5686084c5b3c2a382b9934b535050 (patch)
tree47caed7e1bc49dbb9ee7aecd76f88c2048963f21
parentmerge work/stdio: std.Io and one-level module completion (diff)
parentxref: baseline records the borrow globals (end_why, end_whom, arg_site, local... (diff)
merge work/borrow: borrow returns before definition, borrows clause, borrow messages and LSP diagnostics, mkgenome header mismatch values
-rw-r--r--README.md3
-rw-r--r--TODO.md2
-rw-r--r--docs/SPEC.md10
-rw-r--r--editors/vscode/README.md2
-rw-r--r--editors/vscode/client/extension.js7
-rw-r--r--src/compiler/analyzer.zc80
-rw-r--r--src/compiler/base.zc54
-rw-r--r--src/compiler/builtins.zc8
-rw-r--r--src/compiler/decl.zc17
-rw-r--r--src/compiler/expr.zc20
-rw-r--r--src/compiler/types.zc32
-rw-r--r--src/lsp/server.zc30
-rw-r--r--src/tools/mkgenome.c30
-rw-r--r--tests/drivers/borrow_diag_test.js73
-rw-r--r--tests/drivers/ext_cache_test.js13
-rw-r--r--tests/drivers/tools_contract_test.py5
-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/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
-rw-r--r--tests/programs/borrows_clause.zc11
-rw-r--r--tests/programs/forward_borrow.zc16
-rwxr-xr-xtests/test.sh6
-rw-r--r--tests/xref.baseline16
32 files changed, 514 insertions, 44 deletions
diff --git a/README.md b/README.md
index f6e5ce8..7f022fa 100644
--- a/README.md
+++ b/README.md
@@ -96,8 +96,9 @@ hover shows declaration signatures and documentation. a top-level function, glob
check mode records errors instead of printing them and continues after an error, up to 20 diagnostics per document. each record contains a line and token offset; the server maps these to utf-16 ranges. check mode also reports non-error findings as hints.
- **diagnostics** on open, change and save, with an id echoed on every reply, `MethodNotFound` for unknown requests and `-32002` before `initialize`.
+- **borrow diagnostics**: a use after its owner ended is the compiler's own error (so the editor and `make` agree), ranged on the conflicting use, with `relatedInformation` for where the owner ended (freed, moved, given to a `.d` parameter, ...) and where it was declared. a function called before its body is checked when the body has been read, and the error still lands on the use.
- **definition and references** (`textDocument/definition`, `textDocument/references`): a local, parameter or field goes to its declaration by the offset the compiler recorded, and a function, global, constant, struct or type goes to where the compiler saw it defined, in an included file too (the file is found from the document's directory upward, then as written in the `include`). references lists the uses in the document, with the declaration when asked. built-in names navigate to their embedded source when one is available.
-- **module completions**: after an imported namespace and a dot, completion lists that module's exported members with signatures and documentation; it does not add unrelated local or module names. module hovers and completions cover `rt`, `run`, `core`, `lsp`, `std` and `icon`.
+- **module completions**: after an imported namespace and a dot, completion lists that module's exported members one level at a time (`std.` gives the groups such as `File` and `Io` and the plain members; `std.Io.` the members of `Io`) with signatures and documentation; it does not add unrelated local or module names. module hovers and completions cover `rt`, `run`, `core`, `lsp`, `std` and `icon`.
- **import quick fixes**: compiler diagnostics for missing imports or unqualified exports offer a workspace edit. a bare name is looked up in every module: the runtime and compiler modules, loaded files, and the libraries under `src/lib` read from source even when they are not included, including static groups (`itoa` becomes `std.Text.itoa` with `include std;`). a misspelled name gets the closest defined name or module export (`putc` becomes `rt.put_c`). the edit adds the needed `include` without duplicating an existing import, qualifies bare exports, and preserves namespace declarations, comments, and crlf line endings. the editor client applies only edits for the current document version, while keeping protocol version epochs separate from editor document versions.
- **semantic tokens** (`textDocument/semanticTokens/full`) in 12 types: keyword, type, function, variable, string, number, operator, comment, parameter, property, macro, enumMember. token kinds come from the compiler's symbol tables and declarations. check mode records each resolved name and source location, following the compiler's lookup order: locals and parameters precede globals, and built-ins are checked last. a local named `cap` is a variable wherever it is in scope, and the built-in `cap` only applies to uses outside that scope.
- **formatting** (`textDocument/formatting`, and `zclsp --format < file.zc` on the command line): a pretty-printer in the spirit of clang-format with `ColumnLimit: 0`. it keeps the author's line breaks and sets the indent of every line (from braces, open parentheses and unfinished statements; `case` labels are indented one level inside their `switch` by default), the spaces after commas and around `=`, `==`, `&&` and the like, the blank lines between lines, and the final line break. it never moves a token to another line, and it leaves strings, comments, `asm` and `vm` blocks and everything after a backtick as written, so the tokens of a program are the same before and after. options come from a `.zcformat` file in the document's directory or the nearest directory above it, one `Key: value` per line (`#` starts a comment), then from the editor's tab size and `insertSpaces`, then from the defaults: `UseTab: true`, `IndentWidth: 4` (the spaces of a level when `UseTab` is false), `MaxEmptyLines: 1`, `ContinuationIndent: 1`, `NormalizeSpacing: true`, `CaseIndent: 1`, `FinalNewline: true`. `CaseIndent: 0` aligns each `case` label with its `switch`. `tests/drivers/format_test.js` formats every `.zc` file in the repository and checks that the tokens are unchanged and that formatting twice gives the same text.
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 06ee11c..b2aa5ea 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -884,12 +884,16 @@ 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).
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 `*`).
@@ -948,7 +952,9 @@ 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
+- 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/editors/vscode/README.md b/editors/vscode/README.md
index 24f27bb..506da42 100644
--- a/editors/vscode/README.md
+++ b/editors/vscode/README.md
@@ -2,7 +2,7 @@
this extension connects vscode to the zc language server. the server runs the compiler in check mode, so diagnostics and semantic highlighting reflect the language the compiler actually parses, including syntax introduced with parse words.
-features include compiler diagnostics, import quick fixes, optimization hints, semantic highlighting, hover documentation (values, types, ownership and lifetimes, module include names, embedded declarations and assembly instruction descriptions), fuzzy completions with signatures and descriptions (consumed variables are crossed out and sorted last), go to definition and find references (also into included files), document formatting and immediate textmate highlighting while the server starts. module completion lists exported members after an imported namespace. import quick fixes preserve namespace headers, comments and crlf line endings, and the editor drops edits for stale document versions. hints are shown as blue information squiggles; `hints on`, `hints small` and `hints off` in the source change which ones appear. formatting reads a `.zcformat` file (`UseTab`, `IndentWidth`, `MaxEmptyLines`, `ContinuationIndent`, `NormalizeSpacing`, `CaseIndent`, `FinalNewline`) from the document's directory upward, and falls back to the editor's tab settings. by default, case labels are indented one level inside their switch; `CaseIndent: 0` aligns them with the switch.
+features include compiler diagnostics, import quick fixes, optimization hints, semantic highlighting, hover documentation (values, types, ownership and lifetimes, module include names, embedded declarations and assembly instruction descriptions), fuzzy completions with signatures and descriptions (consumed variables are crossed out and sorted last), go to definition and find references (also into included files), document formatting and immediate textmate highlighting while the server starts. module completion goes one level at a time: `std.` lists the groups (`File`, `Io`, `Text`, ...) and plain members, `std.Io.` the members of that group. borrow errors are the compiler's own messages (the same check `zy` runs), marked on the conflicting use, with related information pointing at where the owner ended and where it was declared. import quick fixes preserve namespace headers, comments and crlf line endings, and the editor drops edits for stale document versions. hints are shown as blue information squiggles; `hints on`, `hints small` and `hints off` in the source change which ones appear. formatting reads a `.zcformat` file (`UseTab`, `IndentWidth`, `MaxEmptyLines`, `ContinuationIndent`, `NormalizeSpacing`, `CaseIndent`, `FinalNewline`) from the document's directory upward, and falls back to the editor's tab settings. by default, case labels are indented one level inside their switch; `CaseIndent: 0` aligns them with the switch.
## install
diff --git a/editors/vscode/client/extension.js b/editors/vscode/client/extension.js
index cd497d8..38c4c73 100644
--- a/editors/vscode/client/extension.js
+++ b/editors/vscode/client/extension.js
@@ -207,6 +207,13 @@ function showDiags(uri, raw) {
: vscode.DiagnosticSeverity.Error;
const diag = new vscode.Diagnostic(new vscode.Range(r.start.line, r.start.character, r.end.line, r.end.character), d.message, sev);
diag.source = d.source || 'zc';
+ // a borrow error names its owner: where it ended and where it was declared
+ if (Array.isArray(d.relatedInformation)) {
+ diag.relatedInformation = d.relatedInformation.filter(ri => ri && ri.location && ri.location.range).map(ri => {
+ const lr = ri.location.range;
+ return new vscode.DiagnosticRelatedInformation(new vscode.Location(vscode.Uri.parse(ri.location.uri || uri), new vscode.Range(lr.start.line, lr.start.character, lr.end.line, lr.end.character)), ri.message || '');
+ });
+ }
return diag;
});
stats.diagnostics += list.length;
diff --git a/src/compiler/analyzer.zc b/src/compiler/analyzer.zc
index 42cb7b5..ff6a6c1 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;
@@ -949,10 +996,17 @@ 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(6);
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;
+ info[5] = lreadpos[i]; if (!info[5]) info[5] = info[2]; // the owner's own token where it ended, for the editor
+ 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 +1399,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 +1429,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 +1498,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;
@@ -1446,6 +1507,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);
@@ -1491,7 +1553,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 +1561,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 +1585,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 +1597,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 +1644,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..7e4b5bc 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.
@@ -415,6 +415,11 @@ int s_new_k, s_recycle_leaf;
// server streams diagnostics and drops a check whose source has been replaced
word chk_hook;
int cmode, chkabort, chkfuel; word dgbuf, dggrow; int dgcap, dgpos, dgn, dgopen, dgprev, dgmode;
+// related places of check-mode records, for the editor: DGREL_W words each, [the record's
+// offset in dgbuf, the packed place where the owner ended, the packed place of the owner's
+// declaration, how it ended (end_why), the owner's name]. reset by chk_begin.
+const int DGREL_W = 5;
+word dgrel; int dgreln, dgrelcap;
// expand a diagnostic in progress, keeping its output offset within the buffer.
int dg_expand() {
dgbuf = chk_resize(dgbuf, dgpos + 5 + rt.outn, dgcap * 2);
@@ -433,8 +438,22 @@ int dg_expand() {
int probe_off = -1, probe_has, probe_n, probe_cap, probe_static;
char** probe_name; word probe_kind, probe_state, probe_type, probe_decl, probe_mem;
char* curinc; int incdepth, dgdrop; int incplace; // how many includes deep the compile is; a record to throw away; the place of the outermost include
+// an open record's range becomes the packed place p (the use itself, not the token after it).
+int dg_place(int p) {
+ if (!cmode || !dgopen || incdepth > 0 || !p || (p & 0xFFFFFF) != tline) return 0;
+ dgbuf[dgpos + 1] = p & 0xFFFFFF; dgbuf[dgpos + 2] = ((p >> 40) & 0x1FFFFF) - 1; dgbuf[dgpos + 3] = (p >> 24) & 0xFFFF;
+ return 0;
+}
+int dg_related(int end_place, int decl_place, int why, char* owner) {
+ if (!cmode || !dgopen || dgdrop || incdepth > 0) return 0;
+ if (dgreln == dgrelcap) { int cap = dgrelcap * 2; if (!cap) cap = 16; dgrel = chk_resize(dgrel, dgreln * DGREL_W, cap * DGREL_W); dgrelcap = cap; }
+ word e = dgrel + dgreln * DGREL_W;
+ e[0] = dgpos; e[1] = end_place; e[2] = decl_place; e[3] = why; e[4] = owner; dgreln++;
+ return 0;
+}
int dg_close() {
if (!dgopen) return 0;
+ if (dgdrop && dgreln && dgrel[(dgreln - 1) * DGREL_W] == dgpos) dgreln--; // its record is thrown away
if (!dgdrop) { dgbuf[dgpos + 4] = rt.outn; dgpos = dgpos + 5 + rt.outn; dgn++; }
dgdrop = 0;
dgopen = 0; rt.outmode = dgmode; rt.outgrow = dggrow;
@@ -470,7 +489,7 @@ word[256] dgseen_initial; word dgseen = dgseen_initial; int dgseen_cap = 256, dg
// start and stop a check run. chk_begin points the diagnostics at a buffer
// the caller owns; chk_end flushes any open record and restores output.
int chk_begin(int buf, int cap) {
- cmode = 1; chkabort = 0; chkfuel = 0; dgbuf = buf; dgcap = cap; dgpos = 0; dgn = 0; dgopen = 0;
+ cmode = 1; chkabort = 0; chkfuel = 0; dgbuf = buf; dgcap = cap; dgpos = 0; dgn = 0; dgopen = 0; dgreln = 0;
nerr = 0; panic = 0; dgmode = rt.outmode; rt.fill_w(dgseen, 0, dgseen_cap); dgseen_n = 0;
return 0;
}
@@ -758,6 +777,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);
@@ -958,6 +978,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]; dg_related(info[5], info[1], why, owner); }
+ 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 +1008,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(); dg_place(lreadpos[i]); 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..f40e223 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);
@@ -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 7471556..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;
@@ -110,7 +112,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);
@@ -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/src/lsp/server.zc b/src/lsp/server.zc
index 6f4ce67..65881a9 100644
--- a/src/lsp/server.zc
+++ b/src/lsp/server.zc
@@ -258,6 +258,35 @@ int wpos(int off) {
wc('}');
return 0;
}
+// a packed compiler place (tpack) as an lsp location in document gd.
+int wplace(int p) {
+ int o = ((p >> 40) & 0x1FFFFF) - 1, n = (p >> 24) & 0xFFFF;
+ if (o < 0) o = 0; if (o > plen) o = plen;
+ if (n < 1) n = 1; if (o + n > plen) n = plen - o;
+ ws("{\"uri\":"); wjs(duri[gd], dulen[gd]); ws(",\"range\":{\"start\":"); wpos(o); ws(",\"end\":"); wpos(o + n); ws("}}");
+ return 0;
+}
+// the related places the compiler recorded for the record at offset at (run.dg_related):
+// where the owner ended and where it was declared, as relatedInformation.
+int diag_related(int at) {
+ int k = 0, why; word e; char* what;
+ while (k < run.dgreln) {
+ e = run.dgrel + k * run.DGREL_W;
+ if (e[0] == at) {
+ why = e[3]; what = " ended here";
+ if (why == 1) what = " was freed here"; else if (why == 2) what = " was given to a .d parameter here";
+ else if (why == 3) what = " was moved here"; else if (why == 4) what = " was stored into a container here";
+ else if (why == 5) what = " was returned here"; else if (why == 6) what = " was stored in a global here";
+ else if (why == 7) what = " was freed by this reassignment"; else if (why == 8) what = " was pushed into an array here";
+ ws(",\"relatedInformation\":[{\"location\":"); wplace(e[1]); ws(",\"message\":\"owner "); ws(e[4]); ws(what); ws("\"}");
+ if (e[2]) { ws(",{\"location\":"); wplace(e[2]); ws(",\"message\":\"owner "); ws(e[4]); ws(" is declared here\"}"); }
+ wc(']');
+ return 0;
+ }
+ k++;
+ }
+ return 0;
+}
// writes the publishDiagnostics message for document gd from dgb. each record
// becomes a range (the whole line when the compiler only knew the line), a
// severity and its message.
@@ -295,6 +324,7 @@ int gen_diag() {
ws("{\"range\":{\"start\":"); wpos(o); ws(",\"end\":"); wpos(e);
ws("},\"severity\":"); wn(sev); ws(",\"source\":\"zc\",\"message\":");
wjs(dgb + at + 5, n);
+ diag_related(at);
wc('}');
at = at + 5 + n;
}
diff --git a/src/tools/mkgenome.c b/src/tools/mkgenome.c
index 9720760..051de62 100644
--- a/src/tools/mkgenome.c
+++ b/src/tools/mkgenome.c
@@ -40,15 +40,33 @@ int main(int argc, char **argv)
}
}
if (ferror(stdin)) { fputs("mkgenome: read failed\n", stderr); return 1; }
- int ok = n >= 8 * 7 && n % 8 == 0;
+ int ok = n >= 8 * 7 && n % 8 == 0, bad[3] = {0, 0, 0};
+ uint64_t got3[3] = {0, 0, 0}, want[3] = {(uint64_t)FTOP * 2 + 1, (uint64_t)ZFP * 2 + 1, 4};
for (int i = 0; ok && i < 3; i++) {
- uint64_t u = 0;
- for (int k = 0; k < 8; k++) u |= (uint64_t)b[i * 8 + k] << (k * 8);
- if (u != (i == 0 ? (uint64_t)FTOP * 2 + 1 : i == 1 ? (uint64_t)ZFP * 2 + 1 : 4)) ok = 0;
+ for (int k = 0; k < 8; k++) got3[i] |= (uint64_t)b[i * 8 + k] << (k * 8);
+ if (got3[i] != want[i]) bad[i] = 1;
}
+ int text = n >= 4 && !memcmp(b, "zc: ", 4);
+ if (ok && (bad[0] || bad[1] || bad[2])) ok = 0;
if (!ok) {
- if (n >= 4 && !memcmp(b, "zc: ", 4)) fwrite(b, 1, n, stderr);
- fprintf(stderr, n % 8 || n < 8 * 7 ? "mkgenome: incomplete genome\n" : "mkgenome: input is not a genome for this map\n");
+ if (text) fwrite(b, 1, n, stderr);
+ if (n % 8 || n < 8 * 7) { fputs("mkgenome: incomplete genome\n", stderr); return 1; }
+ fputs("mkgenome: input is not a genome for this map\n", stderr);
+ if (!text) {
+ /* the header: FTOP and ZFP as literal words (2v+1), then 4. say which differ
+ and by what, since a mismatch usually means rt.zc and the map disagree */
+ static const char *names[3] = {"FTOP (frame stack top)", "ZFP (zero page)", "the header's third word"};
+ for (int i = 0; i < 3; i++) {
+ if (!bad[i]) continue;
+ if (i < 2 && (got3[i] & 1)) fprintf(stderr, " word %d, %s: image has 0x%llX, the map has 0x%llX\n", i, names[i], (unsigned long long)(got3[i] >> 1), (unsigned long long)(want[i] >> 1));
+ else if (i < 2) fprintf(stderr, " word %d, %s: image has the raw word 0x%llX, not a literal; the map has 0x%llX\n", i, names[i], (unsigned long long)got3[i], (unsigned long long)(want[i] >> 1));
+ else fprintf(stderr, " word 2, %s: image has 0x%llX, expected 4\n", names[i], (unsigned long long)got3[i]);
+ }
+ if (bad[0] || bad[1])
+ fputs(" the compiler read a different layout than the one in build/core/map: check that FTOP and ZFP in\n"
+ " src/runtime/rt.zc match the map (a stale src/runtime/rt.zc, e.g. a zip unpacked over an older\n"
+ " checkout, or a stale build/core/map: run make clean, then make)\n", stderr);
+ }
return 1;
}
if (fwrite(b, 1, n, stdout) != n || fflush(stdout)) { fputs("mkgenome: write failed\n", stderr); return 1; }
diff --git a/tests/drivers/borrow_diag_test.js b/tests/drivers/borrow_diag_test.js
new file mode 100644
index 0000000..93a7561
--- /dev/null
+++ b/tests/drivers/borrow_diag_test.js
@@ -0,0 +1,73 @@
+// borrow diagnostics in the editor: the server publishes the compiler's own messages
+// (the same check `zy` runs), with the range of the conflicting use and related
+// information for where the owner ended and where it was declared.
+// usage: node tests/drivers/borrow_diag_test.js path/to/zclsp [server arguments]
+const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1;
+const assert = require('assert');
+const cp = require('child_process');
+const path = require('path');
+const uri = 'file://' + path.resolve('tests/drivers/borrow-diag-fixture.zc');
+const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); };
+function diagnose(text) {
+ const msgs = [
+ { id: 1, method: 'initialize', params: {} },
+ { method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } },
+ { id: 2, method: 'shutdown' },
+ { method: 'exit' }
+ ].map(m => ({ jsonrpc: '2.0', ...m }));
+ const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 });
+ assert.strictEqual(r.status, 0, String(r.stderr));
+ let b = r.stdout, last = null;
+ while (b.length) {
+ const e = b.indexOf('\r\n\r\n'); assert.ok(e >= 0);
+ const n = Number(/Content-Length:\s*(\d+)/i.exec(b.subarray(0, e).toString())[1]);
+ const m = JSON.parse(b.subarray(e + 4, e + 4 + n)); b = b.subarray(e + 4 + n);
+ if (m.method === 'textDocument/publishDiagnostics') last = m.params.diagnostics;
+ }
+ assert.ok(last, 'no diagnostics published');
+ return last;
+}
+function zy(text) {
+ const zyBin = process.env.ZY || path.join(path.dirname(process.argv[2]), 'zy');
+ const r = cp.spawnSync(zyBin, [], { input: text, timeout: 30000 * TIMEOUT_SCALE });
+ const line = String(r.stdout).split('\n')[0];
+ return line.replace(/^zc: line \d+: /, '');
+}
+const at = (text, needle, nth = 0) => { let o = -1; for (let k = 0; k <= nth; k++) o = text.indexOf(needle, o + 1); const before = text.slice(0, o); return { line: before.split('\n').length - 1, character: o - before.lastIndexOf('\n') - 1 }; };
+let checks = 0;
+function same(text, label) {
+ const d = diagnose(text).filter(x => x.severity === 1);
+ assert.ok(d.length >= 1, label + ': no error');
+ const fromZy = zy(text);
+ assert.ok(fromZy && d.some(x => x.message === fromZy), `${label}: editor and zy disagree: [${d.map(x => x.message)}] vs [${fromZy}]`);
+ checks++;
+ return d.find(x => x.message === fromZy);
+}
+// a borrow used after its owner is freed
+let text = 'include rt;\nfn main() {\n\tchar* t = rt.new(4); t[0] = 65;\n\tchar* r = t + 1;\n\trt.free(t);\n\treturn r[0];\n}\n';
+let d = same(text, 'freed owner');
+assert.match(d.message, /r borrows t, and t was freed/);
+assert.deepStrictEqual(d.range.start, at(text, 'r[0]'), 'the range is the conflicting use');
+assert.ok(d.relatedInformation && d.relatedInformation.length === 2, JSON.stringify(d));
+assert.strictEqual(d.relatedInformation[0].message, 'owner t was freed here');
+assert.deepStrictEqual(d.relatedInformation[0].location.range.start, at(text, 't);'));
+assert.strictEqual(d.relatedInformation[0].location.uri, uri);
+assert.strictEqual(d.relatedInformation[1].message, 'owner t is declared here');
+assert.deepStrictEqual(d.relatedInformation[1].location.range.start, at(text, 't ='));
+// the owner itself, given to a .d parameter
+text = 'include rt;\nfn take(.d p) { return p[0]; }\nfn main() { let x = rt.new(3); take(x); return x[0]; }\n';
+d = same(text, '.d parameter');
+assert.match(d.message, /x was given to take's \.d parameter/);
+assert.strictEqual(d.relatedInformation[0].message, 'owner x was given to a .d parameter here');
+assert.deepStrictEqual(d.relatedInformation[0].location.range.start, at(text, 'x)'));
+// a function called before its body, reported at the end of the unit at the use
+text = 'include rt;\nfn main() {\n\tchar* t = rt.new(8); t[0] = 32; t[1] = 0;\n\tlet r = skip(t); rt.free(t); rt.put_c(r[0]);\n\treturn 0;\n}\nchar* skip(char* s) { return s + 1; }\n';
+d = same(text, 'forward call');
+assert.match(d.message, /skip, defined after this call, returns a borrow/);
+assert.deepStrictEqual(d.range.start, at(text, 'r[0]'));
+assert.strictEqual(d.relatedInformation[0].message, 'owner t was freed here');
+// a borrows clause checked against its body
+text = 'include rt;\nchar* first(char* a, char* b) borrows a;\nchar* first(char* a, char* b) { return b; }\nfn main() { return 0; }\n';
+d = same(text, 'borrows clause');
+assert.match(d.message, /returns a borrow of b/);
+console.log(`ok borrow diagnostics: ${checks} cases match zy, with ranges and related owner places`);
diff --git a/tests/drivers/ext_cache_test.js b/tests/drivers/ext_cache_test.js
index 93caf94..9bd3e3a 100644
--- a/tests/drivers/ext_cache_test.js
+++ b/tests/drivers/ext_cache_test.js
@@ -31,6 +31,8 @@ const stub = {
EventEmitter: class { constructor() { this.event = () => ({ dispose() {} }); } fire() {} },
Diagnostic: class { constructor(range, message, severity) { Object.assign(this, { range, message, severity }); } },
Range: class { constructor(a, b, c, d) { Object.assign(this, { start: { line: a, character: b }, end: { line: c, character: d } }); } },
+ Location: class { constructor(uri, range) { Object.assign(this, { uri, range }); } },
+ DiagnosticRelatedInformation: class { constructor(location, message) { Object.assign(this, { location, message }); } },
DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
SemanticTokensLegend: class { constructor(t, m) { this.tokenTypes = t; this.tokenModifiers = m; } },
SemanticTokensBuilder: class { constructor() {} push() {} build() { return null; } },
@@ -177,6 +179,17 @@ const mainUri = 'file://' + main;
const expiredList = await world.completion.provideCompletionItems(expiredDoc, { line: 3, character: 1 }, { isCancellationRequested: false });
const expiredItem = expiredList && expiredList.items.find(c => c.label === 'used');
check('completion maps consumed lifetimes to the editor strikethrough tag', expiredItem && expiredItem.tags?.[0] === stub.CompletionItemTag.Deprecated && /lifetime ended/.test(expiredItem.documentation?.value));
+ // a borrow error keeps the server's related information: where the owner ended and where it was declared
+ const borrowPath = path.join(root, 'borrow.zc');
+ const borrowDoc = doc(borrowPath, 'include rt;\nfn main() {\n\tchar* t = rt.new(4); t[0] = 65;\n\tchar* r = t + 1;\n\trt.free(t);\n\treturn r[0];\n}\n');
+ fs.writeFileSync(borrowPath, borrowDoc.text);
+ world.textDocuments.push(borrowDoc);
+ if (world.listeners.onDidOpenTextDocument) world.listeners.onDidOpenTextDocument(borrowDoc);
+ await until(() => world.sets.some(s => s.uri === 'file://' + borrowPath && s.list.length));
+ const borrowSet = world.sets.filter(s => s.uri === 'file://' + borrowPath).pop();
+ const borrowDiag = borrowSet && borrowSet.list.find(d => /r borrows t/.test(d.message));
+ const related = borrowDiag && borrowDiag.relatedInformation;
+ check('borrow diagnostics pass related information through to the editor', related && related.length === 2 && related[0] instanceof stub.DiagnosticRelatedInformation && related[0].message === 'owner t was freed here' && related[0].location.range.start.line === 4 && related[1].message === 'owner t is declared here', JSON.stringify(borrowSet && borrowSet.list));
const actionDoc = doc(path.join(root, 'imports.zc'), 'fn main() { rt.put_n(7); }');
const actions = await world.actions.provideCodeActions(actionDoc, new stub.Range(0, 15, 0, 15), {}, { isCancellationRequested: false });
check('quick fixes reach the editor as workspace edits', actions.some(a => a.kind === 'quickfix' && a.edit.edits.some(e => e.newText === 'include rt;\n')));
diff --git a/tests/drivers/tools_contract_test.py b/tests/drivers/tools_contract_test.py
index 6a98678..9323a47 100644
--- a/tests/drivers/tools_contract_test.py
+++ b/tests/drivers/tools_contract_test.py
@@ -28,9 +28,12 @@ if args.group == 'image':
changed = words[:]; changed[index] ^= 2
r = command(['build/bin/mkgenome'], pack(changed))
assert r.returncode != 0 and b'not a genome' in r.stderr and not r.stdout
+ # the header words are named, with both values, and a layout mismatch points at rt.zc and the map
+ assert [b'FTOP', b'ZFP', b'third word'][index] in r.stderr, r.stderr
+ if index < 2: assert b'the map has 0x' in r.stderr and b'src/runtime/rt.zc' in r.stderr and b'make clean' in r.stderr, r.stderr
r = command(['build/bin/mkgenome'], b'zc: line 1: something is wrong\nzc: 1 error\n')
assert r.returncode != 0 and b'something is wrong' in r.stderr and not r.stdout
- print('ok mkgenome roundtrip, truncation, alignment, map-header mutation and diagnostic contracts')
+ print('ok mkgenome roundtrip, truncation, alignment, map-header mutation (naming FTOP, ZFP and both values) and diagnostic contracts')
elif args.group == 'map':
for preset in ['default', 'small', 'run', 'lsp', 'fixed']:
with tempfile.TemporaryDirectory(prefix='tinyvm preset ') as directory:
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/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];
+}
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..6d8453e 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"
@@ -635,6 +637,10 @@ s_lsp() {
node tests/drivers/completion_test.js $lsp > $T/completion.out 2>&1; r=$?
grep -v '^ok' $T/completion.out
chk $r 0 "lsp: fuzzy completions, signatures, scope and expired lifetimes"
+ $CC -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c $GI -o $T/zy
+ ZY=$T/zy node tests/drivers/borrow_diag_test.js $lsp > $T/borrowdiag.out 2>&1; r=$?
+ grep -v '^ok' $T/borrowdiag.out
+ chk $r 0 "lsp: borrow errors are zy's own messages, with the use's range and the owner as related information"
node tests/drivers/analysis_cache_test.js $lsp > $T/analysiscache.out 2>&1; r=$?
grep -v '^ok' $T/analysiscache.out
chk $r 0 "lsp: analysis cache reuse and invalidation"
diff --git a/tests/xref.baseline b/tests/xref.baseline
index c0e2233..26b6931 100644
--- a/tests/xref.baseline
+++ b/tests/xref.baseline
@@ -17,12 +17,14 @@ af_mutation_tick af_mutate
aname add_binop reserve_assignment
ar_flat array_designator
ar_target array_designator
+arg_site args array_push
argfact args chk_reset
argloc arrvalue chk_reset
argorigin args
argorigin_mask args
argowner args
argowner_fact args
+argowner_mask args
argtemporary args
arrloc arrvalue chk_reset
arrroot arrvalue
@@ -88,7 +90,7 @@ closure_this closure drop_attach
cmode chk_begin chk_end
code_chunk chk_reset reserve_code syntax_resident
ctxsp ctx_pop ctx_push zc_init
-curinc chk_unit effect_finish gcompile inc_compile
+curinc chk_unit effect_finish gcompile inc_compile shadow_finish
dbuf doc_char
dbuf_initial
dfr defer_drop defer_one kw_defer
@@ -96,7 +98,7 @@ dfr_base chk_reset ctx_pop fn_begin
dfr_ctx defer_one
dfr_lsp defer_one
dfr_on chk_reset defer_one
-dgbuf chk_begin dg_close dg_expand dg_open
+dgbuf chk_begin dg_close dg_expand dg_open dg_place
dgcap chk_begin dg_expand dg_open
dgdrop chk_unit dg_close dg_open
dggrow dg_open
@@ -105,6 +107,9 @@ dgn chk_begin dg_close
dgopen chk_begin dg_close dg_open
dgpos chk_begin dg_close
dgprev chk_reset expect
+dgrel dg_related
+dgrelcap dg_related
+dgreln chk_begin dg_close dg_related
dgseen dg_dup
dgseen_cap dg_dup
dgseen_initial
@@ -137,8 +142,11 @@ effect_current ctx_pop effect_note fn_begin fndef kw_asm zc_init
effect_edges effect_arg zc_init
effect_floor compile_list_context ctx_pop unit_top zc_init
effect_requirements effect_finish effect_require_mode zc_init
+effect_shadows shadow_finish shadow_take shadow_use
effect_symbols effect_record reserve_symbols zc_init
effect_value array_cap array_len array_pop arrvalue bin call ctx_pop drop_attach field fn_begin heap_box kw_share_value methcall postfix_tail primary ternary unary
+end_whom argcheck assigncheck end_owner
+end_why argcheck assigncheck end_owner reassign_free retcheck
enum_open kw_enum
epichain chk_reset ctx_pop fn_begin kw_return
evcap ev_put
@@ -306,6 +314,7 @@ local_end_info assigncheck end_owner newloc reserve_locals
local_ended assigncheck end_owner newloc reserve_locals
local_origin assigncheck newloc reserve_locals
local_origin_mask assigncheck newloc reserve_locals
+local_shadow newloc reserve_locals shadow_take shadow_use
loccap reserve_locals
loopbase ctx_pop fn_begin
loopfacts_cap loop_push reserve_R_LOOP
@@ -427,7 +436,7 @@ o3pend_initial
o3pendcap kw_o3
o3rg o3promote
o3sl o3promote
-panic body_scan capture_scan chk_begin chk_reset die diefound dien effect_finish expect hl_begin prescan replace_function resync topsync unit_end
+panic body_scan capture_scan chk_begin chk_reset die diefound dien effect_finish expect hl_begin prescan replace_function resync shadow_finish topsync unit_end
pe_first enum_tagged reserve_tagged_enums
pe_first_initial
pe_n enum_tagged reserve_tagged_enums
@@ -711,6 +720,7 @@ xgive argcheck args chk_reset
xlend assigncheck expr unary
xlenloc array_len bin call closure enhcall methcall postfix_tail primary unary
xprojection ctx_pop field postfix_tail primary
+xshadow assigncheck call shadow_take
xstamp array_dynamic_literal array_len bin call closure enhcall methcall newtyped postfix_tail primary unary
xstr array_dynamic_literal array_len array_literal_tokens bin blockexpr call closure drop_attach heap_box incdec methcall postfix_tail primary reassign_free unary
xty args array_cap array_dynamic_literal array_len array_literal_tokens array_pop array_push array_resize bin blockexpr call closure ctx_pop drop_attach drop_member enhcall expr fcoerce field ftest heap_box kw_cfunc kw_share_value kw_sizeof kw_typeof kwcall methcall newtyped postfix_tail primary reassign_free ternary unary