1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
# borrowing, ownership, and lifetime rules
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.
## borrowed arguments and temporary results
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
namespace;
include rt;
char*.u message() { char* s = rt.new(2); s[0] = 'x'; s[1] = 0; return s; }
fn main() {
rt.put_s(message());
}
```
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.
## inferred parameter effects and transfers
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.
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.
## last uses, fields, and elements
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.
after an owned local is released, assignment from another owner declared in the same scope or a deeper one 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, including a replacement built inside a branch. initial declarations still cannot copy an owned local, overwriting a live owner remains an error, and captured owners are excluded from this replacement rule.
## lends to globals
`g = m*x;` lends an owned local to a global for the rest of the block: `x` keeps ownership and stays usable while code reached through `g` reads it, and assigning `g` again in the same block ends the lend. giving `x` away, returning, breaking out, or reaching the end of the block while the lend is open is an error, so a global never holds a borrow that outlives its owner. a restore inside a nested branch does not end a lend made outside it; the restore has to be unconditional. this is the idiom for pointing the output stream at a scratch buffer.
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.
## closure captures
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.
## limits and acceptance examples
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.
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.
two completeness limits once stopped the compiler from compiling itself: an owner held in a local could not move into another local, and a local could not be lent to a global. the replacement and lend rules above close them; `docs/history/BOOTSTRAP-OWNERSHIP.md` keeps the record.
the maintained regressions cover these cases:
| case | behavior |
|---|---|
| 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.
|