From 0a87c4663fc52bf60f3d440200b3b3ad6b39bd3e Mon Sep 17 00:00:00 2001 From: zinc Date: Thu, 15 Oct 2026 09:48:10 +0330 Subject: analyzer: use-after-end messages name the borrower, the owner and how the owner ended (freed, moved, .d parameter, push, global, reassignment), with fixtures --- src/compiler/analyzer.zc | 31 ++++++++++++++++++++++--------- src/compiler/base.zc | 30 +++++++++++++++++++++++------- src/compiler/builtins.zc | 2 +- src/compiler/expr.zc | 2 +- 4 files changed, 47 insertions(+), 18 deletions(-) (limited to 'src/compiler') diff --git a/src/compiler/analyzer.zc b/src/compiler/analyzer.zc index 42cb7b5..48debee 100644 --- a/src/compiler/analyzer.zc +++ b/src/compiler/analyzer.zc @@ -949,10 +949,16 @@ int lend_drop(int dep, int from) { return 0; } // retire existing aliases together with their owner. dead aliases do not keep later loop snapshots live. +// end_why and end_whom say how the next end_owner ends its owner, for the message +// of a later use: 0 transferred, 1 freed, 2 given to a .d parameter of end_whom, +// 3 moved into end_whom, 4 stored in a container, 5 returned, 6 stored in a global +// or untracked memory, 7 freed by reassigning it, 8 pushed into end_whom. +int end_why; char* end_whom; int end_owner(int i) { - int j = lbase; word info = talloc(3); + int j = lbase; word info = talloc(5); lend_check(i); - info[0] = lname[i]; info[1] = lpos[i]; info[2] = tpack(); + info[0] = lname[i]; info[1] = lpos[i]; info[2] = tpack(); info[3] = end_why; info[4] = end_whom; + end_why = 0; end_whom = 0; local_ended[i] = info[2]; local_end_info[i] = info; lstate[i] = S_DEAD; while (j < nloc) { if (j != i && isalias(j) && lroot(j) == i) { local_ended[j] = info[2]; local_end_info[j] = info; lstate[j] = S_DEAD; } @@ -1345,6 +1351,8 @@ int recast(int c, int t) { else lstate[i] = t; return c; } +// where the argument argcheck is looking at goes: 0 unknown, 1 a push, else the callee's symbol + 2 +int arg_site; // an argument for a parameter tagged t (0 plain, 1 m, 2 d) int argcheck(int c, int t) { int i, st; @@ -1373,7 +1381,12 @@ int argcheck(int c, int t) { touch(i); return 0; } - if (st == S_D || ((st == S_M || st == S_U) && lown[i])) { end_owner(i); luse(i); xgive = 1; ev_put(i, EV_DOWN, -1); return 0; } + if (st == S_D || ((st == S_M || st == S_U) && lown[i])) { + if (arg_site >= 2 && arg_site - 2 < ubase && (sname[arg_site - 2] == s_free || sname[arg_site - 2] == s_recycle)) end_why = 1; + else if (arg_site == 1) end_why = 8; + else if (arg_site >= 2) { end_why = 2; end_whom = sname[arg_site - 2]; } + end_owner(i); luse(i); xgive = 1; ev_put(i, EV_DOWN, -1); return 0; + } if (st == S_M) cerrn(i, "is m; write d*x to give it away"); else if (st == S_U) cerrn(i, "is u; write d*x to give it away instead of returning it"); else if (st == S_CHILD) cerrn(i, "belongs to a container and can't be given away alone"); @@ -1437,7 +1450,7 @@ int reassign_free(int i, int rc, int bor) { if (lleaf[i] && !dry && lookup(s_recycle_leaf) >= 0) { lit(lleaf[i]); lit(szclass(lnsz[i])); callname(s_recycle_leaf); } else callname(s_recycle); op(O_DROP); - end_owner(i); + end_why = 7; end_owner(i); if (local_dropset) local_dropset[i] = 0; lnsz[i] = 0; lleaf[i] = 0; xty = t; xaf = af; xstr = st; xfloat = fl; xbor = bor; @@ -1491,7 +1504,7 @@ int assigncheck(int k, int i, int rc) { lstate[i] = S_M; lused[i] = 0; lown[i] = 1; lnsz[i] = lnsz[j]; lleaf[i] = lleaf[j]; local_ended[i] = 0; local_end_info[i] = 0; - end_owner(j); luse(j); ev_put(j, EV_MOVE, i); + end_why = 3; end_whom = lname[i]; end_owner(j); luse(j); ev_put(j, EV_MOVE, i); if (ltag[i] & 15) lstate[i] = ltag[i] & 15; return C_PLAIN; } @@ -1499,7 +1512,7 @@ int assigncheck(int k, int i, int rc) { lstate[i] = S_M; lused[i] = 0; lown[i] = 1; - end_owner(j); + end_why = 3; end_whom = lname[i]; end_owner(j); luse(j); ev_put(j, EV_UP, i); if (ltag[i] & 15) lstate[i] = ltag[i] & 15; return C_PLAIN; @@ -1523,7 +1536,7 @@ int assigncheck(int k, int i, int rc) { if (js == S_U) { cerrn(j, "is u; it has to leave through the block above, not into a borrowed object"); } - else { end_owner(j); luse(j); ev_put(j, EV_STORE, i); emitown(i); } // storing it gives it away + else { end_why = 4; end_whom = lname[i]; end_owner(j); luse(j); ev_put(j, EV_STORE, i); emitown(i); } // storing it gives it away } else if (js != st) cerrn(j, "its tag doesn't match its container's (both have to be m, u or d)"); else if (lhome[j] < lhome[r]) cerrn(j, "lives in an outer block than the container it's stored into"); else { lstate[j] = S_CHILD; lowner[j] = i; luse(j); ev_put(j, EV_STORE, i); emitown(i); } @@ -1535,7 +1548,7 @@ int assigncheck(int k, int i, int rc) { if (j >= 0 && k == LV_GLOBAL && xlend == j && lstate[j] == S_M && lown[j]) { xlend = -1; touch(j); lend_add(i, j); return C_PLAIN; } xlend = -1; if (j >= 0) { - if (lstate[j] == S_D || lstate[j] == S_U || (lstate[j] == S_M && lown[j])) { ev_put(j, EV_GLOBAL, -1); end_owner(j); luse(j); } + if (lstate[j] == S_D || lstate[j] == S_U || (lstate[j] == S_M && lown[j])) { ev_put(j, EV_GLOBAL, -1); end_why = 6; end_owner(j); luse(j); } else cerrn(j, "is not an owner, so it can't be stored in a global or in untracked memory (the collector takes it over)"); } return C_PLAIN; @@ -1582,7 +1595,7 @@ int retcheck(int c) { } if (fret) { if (i >= 0) { - if (lstate[i] == S_U) { end_owner(i); luse(i); ev_put(i, EV_RETURN, -1); } + if (lstate[i] == S_U) { end_why = 5; end_owner(i); luse(i); ev_put(i, EV_RETURN, -1); } else cerrn(i, "has to be u to be returned"); } return 0; diff --git a/src/compiler/base.zc b/src/compiler/base.zc index dc786b5..32549d4 100644 --- a/src/compiler/base.zc +++ b/src/compiler/base.zc @@ -958,6 +958,27 @@ int cerr(char* msg) { if (fchk && live) serr(msg); return 0; } +// how an owner ended, from end_owner's record: the use, the owner, what ended it and where. +// "r: used after it was given away; r borrows t, and t was freed on line 5 (t declared on line 4)" +int ended_message(char* use, word info, char* root, int cause) { + char* owner = root; int why = 0; char* whom = 0; + if (info) { owner = info[0]; why = info[3]; whom = info[4]; } + rt.put_s(use); rt.put_s(": used after it was given away; "); + if (!rt.eq_w(use, owner)) { rt.put_s(use); rt.put_s(" borrows "); rt.put_s(owner); rt.put_s(", and "); } + rt.put_s(owner); + if (why == 1) rt.put_s(" was freed"); + else if (why == 2) { rt.put_s(" was given to "); rt.put_s(whom); rt.put_s("'s .d parameter"); } + else if (why == 3) { rt.put_s(" was moved into "); rt.put_s(whom); } + else if (why == 4) { rt.put_s(" was stored into "); rt.put_s(whom); } + else if (why == 5) rt.put_s(" was returned"); + else if (why == 6) rt.put_s(" was stored in a global or untracked memory"); + else if (why == 7) rt.put_s(" was freed by reassigning it"); + else if (why == 8) rt.put_s(" was pushed into an array"); + else rt.put_s(" was transferred"); + rt.put_s(" on line "); rt.put_n(cause & 0xFFFFFF); + if (info && info[1]) { rt.put_s(" ("); if (!rt.eq_w(use, owner)) { rt.put_s(owner); rt.put_c(' '); } rt.put_s("declared on line "); rt.put_n(info[1] & 0xFFFFFF); rt.put_c(')'); } + return 0; +} // report an analyzer error about local i, named in the message. int cerrn(int i, char* msg) { int root = i, cause = 0; word info = 0; @@ -967,13 +988,8 @@ int cerrn(int i, char* msg) { if (!info && local_end_info) info = local_end_info[root]; if (info) cause = info[2]; else if (local_ended) cause = local_ended[root]; } - if (cause && !panic && !dry) { - errat(); rt.put_s(lname[i]); rt.put_s(": "); rt.put_s(msg); - rt.put_s("; owner "); if (info) rt.put_s(info[0]); else rt.put_s(lname[root]); - rt.put_s(" was transferred on line "); rt.put_n(cause & 0xFFFFFF); - if (info && info[1]) { rt.put_s(" (declared on line "); rt.put_n(info[1] & 0xFFFFFF); rt.put_c(')'); } - errend(); - } else serrn(lname[i], msg); + if (cause && !panic && !dry) { errat(); ended_message(lname[i], info, lname[root], cause); errend(); } + else serrn(lname[i], msg); } return 0; } diff --git a/src/compiler/builtins.zc b/src/compiler/builtins.zc index be8c3b4..d28d2fd 100644 --- a/src/compiler/builtins.zc +++ b/src/compiler/builtins.zc @@ -736,7 +736,7 @@ int array_push(word t) { effect_note_mask(effect_value, EF_WRITE); af_mutate(xaf, tpack()); nogrow(t); if (t[0] == TY_DYN && isagg(t[4])) serr("push takes scalar or reference elements; use resize for aggregates"); c = expr(); rval(); if (t[0] == TY_DYN) fcoerce(t[4]); - if (isptr(xty) || xty == tword) effect_note_mask(effect_mask(c), EF_RETAIN); argcheck(c, 2); + if (isptr(xty) || xty == tword) effect_note_mask(effect_mask(c), EF_RETAIN); arg_site = 1; argcheck(c, 2); arg_site = 0; if (t[0] == TY_DYN && !compat(t[4], xty)) tcerr("push", t[4], xty); if (o3on && t[0] == TY_DYN && hasptr(t[4])) { lit(t[4]); callname(s_fatten); } expect(s_rp); diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc index 7471556..d9c91d6 100644 --- a/src/compiler/expr.zc +++ b/src/compiler/expr.zc @@ -110,7 +110,7 @@ int args(int tags, word ft, int bs, int fat) { } if (ft && np + parameter_base < ft[3]) fcoerce(subst(ft[7 + np + parameter_base], bs)); t = xty; - argcheck(c, (tags >> (np + np)) & 3); + arg_site = 0; if (cs >= 0) arg_site = cs + 2; argcheck(c, (tags >> (np + np)) & 3); arg_site = 0; // o3 data handed to normal code if (fat && hasptr(t)) { lit(t); -- cgit v1.2.3 From 6bbac2edf7c50dac4d755a364b6dfd16471284d7 Mon Sep 17 00:00:00 2001 From: zinc Date: Thu, 15 Oct 2026 09:56:19 +0330 Subject: 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 --- TODO.md | 2 +- docs/SPEC.md | 6 +++++ src/compiler/analyzer.zc | 48 +++++++++++++++++++++++++++++++++++ src/compiler/base.zc | 3 ++- src/compiler/builtins.zc | 6 ++++- src/compiler/decl.zc | 17 +++++++++++++ src/compiler/expr.zc | 18 ++++++++----- src/compiler/types.zc | 32 ++++++++++++++++++++++- tests/errors/borrows_body.zc | 13 ++++++++++ tests/errors/borrows_decl.zc | 14 ++++++++++ tests/errors/borrows_extern.zc | 11 ++++++++ tests/errors/borrows_fntype.zc | 12 +++++++++ tests/errors/forward_borrow_return.zc | 11 ++++++++ tests/programs/borrows_clause.zc | 11 ++++++++ tests/programs/forward_borrow.zc | 16 ++++++++++++ tests/test.sh | 2 ++ 16 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 tests/errors/borrows_body.zc create mode 100644 tests/errors/borrows_decl.zc create mode 100644 tests/errors/borrows_extern.zc create mode 100644 tests/errors/borrows_fntype.zc create mode 100644 tests/errors/forward_borrow_return.zc create mode 100644 tests/programs/borrows_clause.zc create mode 100644 tests/programs/forward_borrow.zc (limited to 'src/compiler') 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" -- cgit v1.2.3 From e1e0a265e931637f452ac05bb7d912e07bcee574 Mon Sep 17 00:00:00 2001 From: zinc Date: Thu, 15 Oct 2026 10:11:56 +0330 Subject: lsp: borrow errors ranged on the use, with related information for where the owner ended and was declared; vscode client passes relatedInformation through --- README.md | 3 +- editors/vscode/README.md | 2 +- editors/vscode/client/extension.js | 7 ++++ src/compiler/analyzer.zc | 3 +- src/compiler/base.zc | 25 +++++++++++-- src/lsp/server.zc | 30 ++++++++++++++++ tests/drivers/borrow_diag_test.js | 73 ++++++++++++++++++++++++++++++++++++++ tests/drivers/ext_cache_test.js | 13 +++++++ tests/test.sh | 4 +++ 9 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 tests/drivers/borrow_diag_test.js (limited to 'src/compiler') 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/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 103f732..ff6a6c1 100644 --- a/src/compiler/analyzer.zc +++ b/src/compiler/analyzer.zc @@ -1002,9 +1002,10 @@ int lend_drop(int dep, int from) { // 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(5); + int j = lbase; word info = talloc(6); lend_check(i); 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) { diff --git a/src/compiler/base.zc b/src/compiler/base.zc index bfed062..7e4b5bc 100644 --- a/src/compiler/base.zc +++ b/src/compiler/base.zc @@ -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; } @@ -963,7 +982,7 @@ int cerr(char* msg) { // "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]; } + 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); @@ -989,7 +1008,7 @@ 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(); ended_message(lname[i], info, lname[root], cause); errend(); } + 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/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/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/test.sh b/tests/test.sh index d8ad0fb..6d8453e 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -637,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" -- cgit v1.2.3