aboutsummaryrefslogtreecommitdiff
path: root/docs/BORROW.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/BORROW.md')
-rw-r--r--docs/BORROW.md100
1 files changed, 35 insertions, 65 deletions
diff --git a/docs/BORROW.md b/docs/BORROW.md
index 78b28e9..818eea4 100644
--- a/docs/BORROW.md
+++ b/docs/BORROW.md
@@ -1,89 +1,59 @@
-# a quieter ownership and lifetime checker
+# borrowing, ownership, and lifetime rules
-## objective
+the compiler records ownership, aliases, parameter effects, local lifetime events, array projections, and source locations while it parses and emits code. these facts allow common borrows and transfers without spelling `.m`, `.d`, or `u*` at every call site. unknown facts remain conservative; low-level word and native boundaries still need explicit contracts or unchecked code.
-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.
+## borrowed arguments and temporary results
-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.
+a function body records whether each parameter is read, written, retained, consumed, or returned as an alias. summaries for checked functions are propagated through calls to a fixed point, including recursion. when a fresh owned result is passed to a parameter proved not to retain or consume it, the compiler stores the result in a hidden frame slot for the call and releases it on every exit from the containing expression. this makes calls such as `rt.put_s(message())` safe without adding an explicit local. nested calls, short-circuit branches, conditional expressions, loops, and early returns use the same cleanup path rules.
```zc
-char*.u message() { char* s = new(2); s[0] = 'x'; s[1] = 0; return u*s; }
+namespace;
+include rt;
+char*.u message() { char* s = rt.new(2); s[0] = 'x'; s[1] = 0; return s; }
fn main() {
- let text = message();
- put_s(text);
- return 0;
+ rt.put_s(message());
}
```
-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.
+an alias returned from an argument keeps that temporary alive until the alias's last use in the expression. passing a temporary to a known retaining or consuming parameter is rejected; use an owned `.d` parameter for a transfer. a checked borrower may return a borrow of its parameter, but it cannot return a local object's borrow as if it outlived that object. unchecked, external, indirect, or otherwise unknown callees need an explicit borrow/transfer contract or a named binding where lifetime is visible.
-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.
+## inferred parameter effects and transfers
-### borrows end at their last use
+the compiler tracks parameter origins through typed pointer expressions and uses their effect summaries to decide what a call may do. a call to an explicit `.m` parameter is a borrow contract; `.d` is a transfer contract. for checked bodies, a plain parameter that retains or consumes an owner is diagnosed at the caller. owned values passed to `.d` parameters transfer implicitly, and definite later uses identify the transfer and use locations. a pointer returned as an alias carries the source owner when the compiler can prove one origin.
-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.
+effects are cached for the current compiler analysis and participate in the language server's bounded result cache. the server cache is invalidated by source and included-file changes. summaries are not persisted as reusable per-function compiler records across arbitrary new compilations. externs, unchecked code, raw assembly, integer-to-pointer casts, untyped word storage, unknown indirect targets, and multiple ambiguous owners remain analysis boundaries.
-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.
+## last uses, fields, and elements
-### fields, elements, and mutation
+aliases are tied to their owner. when a tracked alias has no later use, a subsequent move or free can proceed; uses after a definite transfer or free produce an error that points to both the transfer and the later use. independent struct fields and constant array elements can be used when their projections prove they do not overlap. unknown indices and overlapping projections are not treated as disjoint.
-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.
+after an owned local is released, assignment from another owner in the same declaring scope moves the replacement into that empty slot. the source and its aliases expire, the destination receives its cleanup responsibility, and loop paths still have to agree. this supports buffer-growth code such as `rt.free(buffer); buffer = replacement;` without a cast. initial declarations still cannot copy an owned local, overwriting a live owner remains an error, and captured owners are excluded from this replacement rule.
-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.
+an element borrow records its base array and known range. a constant index outside a known length gets a bounds diagnostic. mutations such as `push`, `pop`, and `resize` advance the array generation and invalidate element borrows; a later use reports the mutation and where the borrow was made. uncertain branch and loop facts are not promoted to definite errors. runtime checks remain for dynamic indices.
-### useful diagnostics
+## closure captures
-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.
+closures share captured local and parameter cells with their enclosing function and sibling closures. updates are visible through every closure that captured the variable, including after collection and after the creating function returns. captured owned objects are kept alive by those cells. captures of short-lived borrowed or temporary values are rejected, and aggregate captures remain in place rather than being copied into a closure.
-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.
+## limits and acceptance examples
-## implementation order
+the compiler functions, standard library, and language server currently run with lifetime checking enabled. compiler table allocations use an explicit `m`-tagged `rt.new(...)` collector-managed lifetime: there is no scope cleanup for those buffers, and live local or global roots keep them available to collection. owned source and token buffers return `.u`, while generic-token attachment consumes a `.d` buffer. generated native struct-return stubs keep their result owner checked and lend its numeric address to the native descriptor only for the call.
-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.
+the analysis runs with the existing parser, ownership checker, and lifetime records; it does not have a separate source parser or a complete path-sensitive proof. snapshots conservatively join branches and loops. low-level and unknown boundaries can hide behavior, so a quiet diagnostic set is not a whole-program safety proof. zc is single-threaded; shared closure cells do not define concurrent mutation rules.
-## acceptance cases
+the maintained regressions cover these cases:
-| case | required behavior |
+| case | 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.
+| owned temporary passed to a known non-retaining output function | held in a hidden frame slot and released after the call |
+| temporary passed to a retaining or consuming callee | rejected unless ownership is transferred |
+| returned alias of a temporary used later in the same expression | temporary stays alive through the dependent use |
+| alias whose last use precedes an ownership transfer | transfer is accepted |
+| released local receives a same-scope owned replacement | ownership and cleanup move; source and old aliases expire |
+| two independent struct fields or constant array elements | permitted when projections prove separation |
+| element alias used after array mutation | rejected with borrow and mutation locations |
+| use after definite free or move | rejected with both source locations |
+| definitely out-of-range constant index | diagnosed; unknown dynamic indices retain runtime checks |
+| counter updated by one or more closures | all closures observe the shared state |
+
+`tests/programs/temporary_borrows.zc`, `tests/programs/borrow_rules.zc`, `tests/programs/replace_owner.zc`, and the focused error fixtures exercise these rules. the checker deliberately leaves ambiguous native, unchecked, opaque-word, and unknown-indirect effects conservative.