diff options
| author | 2026-10-12 08:46:23 +0330 | |
|---|---|---|
| committer | 2026-10-12 08:46:23 +0330 | |
| commit | ecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch) | |
| tree | 70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /docs/BORROW.md | |
a
Diffstat (limited to 'docs/BORROW.md')
| -rw-r--r-- | docs/BORROW.md | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/docs/BORROW.md b/docs/BORROW.md new file mode 100644 index 0000000..78b28e9 --- /dev/null +++ b/docs/BORROW.md @@ -0,0 +1,89 @@ +# a quieter ownership and lifetime checker + +## objective + +accept ordinary, safe code with little annotation. infer ownership transfers and borrow duration when the compiler can prove them, and explain the cases it cannot prove. measure this on a corpus of small programs rather than claiming the checker is generally smarter than rust's. + +rust already infers non-lexical lifetimes from control flow; reproducing that idea would not establish an advantage. its checker also tracks initialization and moves. zc can choose different ergonomics for its single-threaded ownership model and collector, while documenting where that model provides fewer guarantees. see the [rust compiler guide](https://rustc-dev-guide.rust-lang.org/borrow-check.html). + +## current behavior + +the analyzer runs alongside parsing and code emission. a local records an owner, scope, and state: held here, moving up, moving down, borrowed, or dead. snapshots reconcile branches and loops; derived pointer aliases retain their owner. explicit `.m`, `.d`, and `.u` tags describe borrowing and transfers. + +passing a fresh owned result directly to a borrowing parameter is deliberately rejected by `argcheck`. only a consuming `.d` parameter can currently receive it without a named local. naming the result gives the caller a place to insert cleanup. accepting the same call without generating that cleanup would change lifetime behavior and can leak the temporary until collection. + +```zc +char*.u message() { char* s = new(2); s[0] = 'x'; s[1] = 0; return u*s; } +fn main() { + let text = message(); + put_s(text); + return 0; +} +``` + +plain borrowing parameters do not provide a complete interprocedural escape contract. unchecked code, raw host calls, integer-to-pointer conversions, and unknown native functions remain boundaries of static reasoning. + +## proposed rules + +### temporary borrows + +allow `put_s(message())` when the callee is proved to borrow without retaining, freeing, or transferring the argument. evaluate arguments in their existing order, hold each owned temporary in a compiler-generated slot, call the function, then free the temporary after its last dependent use. keep a returned alias alive through the enclosing expression when necessary. + +do not substitute `.d`: the output function does not consume its argument. nested calls, short-circuit expressions, early returns, and error recovery need their own cleanup paths. a temporary cannot be destroyed while another argument or the result still refers to it. unknown or escaping callees keep the explicit binding requirement. + +rust also has temporary scopes and lifetime extension rules; the proposed benefit here is a smaller set of user-facing rules for zc, not the invention of temporary lifetimes. see the [rust reference](https://doc.rust-lang.org/reference/destructors.html#temporary-scopes). + +### function effects + +infer a summary for each parameter: read, write, retain, consume, or return an alias. distinguish a returned owned result from a returned borrow and record which argument the borrow depends on. compute summaries to a fixed point for recursive functions; cache them by function body and relevant callee summaries. + +start with the runtime and ordinary functions whose bodies are available. annotations remain useful for externs, separate compilation, and deliberately unchecked code. a missing summary means unknown effects, not a promise that the call is safe. + +### borrows end at their last use + +derive borrow duration from control flow. a temporary alias that is no longer used should not prevent a later move of its owner. infer ordinary moves into consuming calls and returns where there is one unambiguous owner; retain explicit tags for ambiguous transfers and low-level code. + +use one fact graph for ownership, initialization, aliases, and known bounds. keep the existing parser and parse words: record analysis events from the real compiler, rather than adding an independent source parser that can disagree with it. + +### fields, elements, and mutation + +track accesses by owner and projection: a struct field, a constant array index, or an element range. allow disjoint accesses when their projections are proved separate. invalidate those facts after an overlapping write, resize, unknown call, or unchecked operation. + +ordinary aliases may coexist in zc; mutation is safe only while the owner remains alive and the access stays valid. permitting aliases does not provide rust's concurrency guarantees. do not infer disjointness from uncertain indices, and do not treat a quarantined block's intact bytes as a valid lifetime. + +### useful diagnostics + +report the operation that makes a later use invalid, the later use, and the relevant owner or bound. show known size, ownership, borrow source, and lifetime end in hover text. use "unknown" when a fact is unavailable. + +preserve hard errors for existing definite lifetime violations. add warnings for newly detected definite out-of-bounds access or use after free in code the old analyzer did not track. keep heuristic suggestions separate from definite findings, and avoid claiming an uncertain path is definitely invalid. + +## implementation order + +1. finish the existing maintenance and memory work in `TODO.md`; analysis tables must grow without silently dropping facts. +2. add compiler events with source ranges and a small control-flow representation. reproduce the current analyzer's accepted and rejected corpus before changing its rules. +3. infer non-escaping summaries for the runtime and add temporary cleanup for proved borrowing calls. +4. implement last-use borrow endings and unambiguous ownership transfer inference. +5. add projection facts, bounds facts, and explanatory editor previews. perform the final warning phase only after the preceding maintenance is complete. +6. compare the old and new checker on the same programs. expose unexplained behavior changes as regressions, not as an improvement count. + +## acceptance cases + +| case | required behavior | +|---|---| +| an owned temporary passed to a proved non-escaping output function | accepted; one cleanup after its last use | +| a temporary passed to a callee that stores it globally | rejected unless ownership is transferred | +| a returned alias of a temporary used later in the same expression | accepted only with a proved lifetime extension | +| two arguments depending on the same owner | evaluate without freeing or moving that owner too early | +| an alias whose last use precedes an ownership transfer | accepted when no surviving alias escapes | +| independent struct fields or constant array elements | permit proved disjoint accesses | +| resize while an element alias remains live | reject or invalidate the alias according to an explicit rule | +| a use after a definite free or move | retain the error, with both source locations | +| an index definitely outside a known length | show a definite bounds finding | +| a dynamic index with no useful bound | retain runtime checks; no definite warning | +| unknown native or unchecked effects | require a contract or explicit escape hatch | + +verify cleanup counts, allocation counts, alias lifetimes, and output on the interpreter, microcore, and available native backends. tests must include safe neighbors of every rejected example. track annotations removed, safe programs accepted, compile time, and diagnostic precision as separate outcomes. + +## scope + +this is a design plan, not an implemented guarantee. lifetime inference must not change evaluation order or silently choose a different ownership contract. the initial work targets zc's present single-threaded execution model; shared mutable concurrency needs a separate model. |
