cove_sema/unique.rs
1//! The conservative local uniqueness proof `Vector.freeze()` needs.
2//!
3//! [ADR 0001](../../../docs/adr/0001-mvp-language-design.md) has said from
4//! the beginning what this pass is:
5//!
6//! > `Vector.freeze()` consumes a vector with uniquely owned storage and
7//! > returns an `Array<T>` in O(1). **The compiler only performs
8//! > conservative, local uniqueness checking for this explicit transition.**
9//! > If uniqueness cannot be proved, `toArray()` creates an independent O(n)
10//! > immutable array.
11//!
12//! Until [issue #240](https://github.com/myuon/cove/issues/240) that sentence
13//! described nobody: the tree-walking interpreter counted `Rc` handles at run
14//! time and refused there, which is a different thing with a different
15//! failure time and a different blast radius. It also cannot be carried
16//! anywhere else. A handle in the linear-memory machine is a word, words are
17//! not counted, and the sharing bit that could have carried the answer went
18//! out with the copy-on-write design. So the choice was to give up the O(1)
19//! transition or to establish uniqueness where the language always said it
20//! was established — in the compiler — and this is the second one.
21//!
22//! # What is proved
23//!
24//! For one `freeze()` call, that the vector it consumes is reached through
25//! exactly one place, and that the place is not read again afterwards. The
26//! four conditions are #240's own list:
27//!
28//! - it **originates at a locally known creation** — `Vector.of(...)`,
29//! `array.toVector()`, `vector.snapshot()`, or a field initialised by one
30//! in a struct literal this body wrote;
31//! - it has **not been copied to another live place** — no `let`/`var` binds
32//! it, no assignment writes it anywhere else;
33//! - it has **not escaped** — no closure captures it, no `return` carries it
34//! out, nothing stores it in another value, and no call it is passed to can
35//! keep it;
36//! - it is **consumed** by the `freeze()` and **not used afterward**.
37//!
38//! One more condition is not on that list and belongs to a static pass rather
39//! than a dynamic one: the site must not be somewhere that **runs twice**. A
40//! `freeze()` in a loop body or a closure body, over storage created outside
41//! it, would consume on the first turn what the second turn would find gone,
42//! so the binding has to be created inside the same region the site is in.
43//!
44//! # What is deliberately not treated as an escape, and why each is safe
45//!
46//! Four positions read a place without keeping the handle. Treating them as
47//! escapes would refuse most of the corpus for nothing:
48//!
49//! - **a method call's receiver.** `items.push(n)` and `items.length()` write
50//! and read through the handle; neither stores it. The exception is a
51//! *declared* method whose result can reach a `Vector`, which may be a
52//! getter handing the field back — that is an escape.
53//! - **a string interpolation operand.** `"{items}"` formats the value and
54//! keeps nothing.
55//! - **a `for` loop's iterable.** Iterating reads elements out; the sequence
56//! is not retained.
57//! - **a by-value call argument**, when the call has no way to keep it. This
58//! is the one that needs an argument rather than an observation, and the
59//! argument is the language's own: a `Vector` is not task-safe, so it
60//! cannot be put in a `Shared` or carried into a `spawn`, and the Host API
61//! boundary materialises a `Value`, so a host cannot hold one either. The
62//! ways out of a callee are therefore its result, a `var` parameter, and
63//! another operand it could write into. So `firstFree(seed, w, h, cells)`
64//! — whose result is an `Int` and whose other operands hold no vector —
65//! keeps nothing, and `into.push(cells)` does. A call whose result can
66//! reach a `Vector`, a call with a `var` argument, and a call where some
67//! *other* operand can reach a `Vector` are all escapes.
68//!
69//! # The one obligation that crosses a call
70//!
71//! A builder's `finish` is the shape that made a purely intraprocedural pass
72//! insufficient:
73//!
74//! ```cove
75//! fn finish(var self) -> Router {
76//! Router(routes: self.routes.freeze())
77//! }
78//! ```
79//!
80//! `self.routes` does not originate here, so this body cannot prove anything
81//! about it — and refusing would refuse `examples/values` and
82//! `examples/callbacks`, both of which are demonstrating the language's own
83//! rule. What the body *can* do is state the condition it needs and make its
84//! callers prove it. So a method that freezes a path rooted at `var self`
85//! becomes a method that **demands a uniquely owned receiver**, and every
86//! call to it is checked by the same local proof, on the receiver place, at
87//! the call site. `fresh.finish()` on a draft this body built passes;
88//! `original.finish()` after `var alias = original` does not.
89//!
90//! The demand is deliberately narrow — a `var self` receiver of a method
91//! written in a plain `impl` block, and nothing else. That is the only
92//! declaration form whose every call site the checker resolves precisely
93//! ([`Facts::target`]), so it is the only one where the obligation cannot be
94//! lost. A `freeze()` rooted at an ordinary parameter, at a captured name, or
95//! at the receiver of a trait method is refused rather than propagated,
96//! because a call through a bound or through `dyn` names no declaration and
97//! there would be nowhere to discharge it.
98//!
99//! There is one way an obligation can be lost, and it is worth naming rather
100//! than leaving implicit: a call whose receiver type the checker declined to
101//! settle records no target, so a `finish()` reached through a value of
102//! unknown type is not checked. That needs a receiver the checker abstained
103//! about — a Host API result a schema declared `Any` — reaching a method of a
104//! declared type, and no program in the corpus does it. Closing it would mean
105//! refusing every unresolved call that shares a name with a demanding method,
106//! which is a diagnostic about a coincidence of names; it is left open, and
107//! written down here, until a program asks for it.
108//!
109//! # What it refuses that the oracle admits
110//!
111//! Conservative means this list is not empty, and the diagnostic's job is to
112//! make each entry legible rather than mysterious. The ones that showed up
113//! while this was written:
114//!
115//! - **storage a call produced.** `var log = freshVector()` then
116//! `log.freeze()` is refused: the initialiser is a call, and whether its
117//! answer is fresh is a fact about another body. Proving it would be an
118//! "answers unaliased storage" summary, which nothing in the corpus asks
119//! for yet.
120//! - **storage an assignment brought in.** `log = lines` gives `log` whatever
121//! the caller is holding, and this refuses `log.freeze()` afterwards —
122//! correctly, in that case.
123//! - **a `var` parameter that is not `self`.** The obligation only travels
124//! back through a method receiver, so `fn take(var v: Vector<Int>) { ... v.freeze() }`
125//! is refused.
126//! - **a name a pattern or a `for` bound.** `match maybe { Some(v) => v.freeze() }`
127//! has no creation to point at.
128//!
129//! Every one of them has the same correction, and the diagnostic gives it:
130//! `toArray()`, which copies in O(n) and asks nothing.
131//!
132//! # This is not a borrow checker
133//!
134//! It answers one question about one method. There is no sharing bit, no
135//! reference count, no copy-on-write and no runtime table; a program that
136//! this pass cannot prove is not a program that is wrong, it is a program
137//! that pays `toArray()`'s O(n) copy instead. The diagnostic says so, and
138//! naming the alias that defeated the proof is most of what it is for.
139
140use std::collections::{BTreeMap, BTreeSet};
141
142use cove_diag::{Diagnostic, FileId, Span};
143use cove_syntax::ast::{
144 Arg, Block, Expr, ExprKind, Ident, ItemKind, Param, Pattern, PatternKind, StmtKind, StrPart,
145 Type, TypeKind,
146};
147
148use crate::facts::Facts;
149use crate::resolve::Program;
150use crate::typeck::Ty;
151
152/// A `freeze()` whose receiver's storage could not be proved uniquely owned.
153pub const NOT_UNIQUE: &str = "cove::unique::not_unique";
154
155/// A read of a vector a `freeze()` has already consumed.
156pub const USED_AFTER_FREEZE: &str = "cove::unique::used_after_freeze";
157
158/// The one sentence this pass enforces, on every diagnostic it raises.
159const RULE: &str =
160 "`freeze()` consumes a vector whose storage the compiler can prove is uniquely owned here, \
161 and returns an immutable array in O(1).";
162
163/// One declaration, as the demand table names it.
164type FnKey = (String, Option<String>, String);
165
166/// A place: the binding it is rooted at and the fields read off it.
167///
168/// `self.guests` is `{ root: "self", binding: None, fields: ["guests"] }`.
169/// `binding` is the index of the `let`, `var` or pattern that introduced the
170/// root, which is what tells two `var parts` in two match arms apart; a root
171/// this body did not bind — a parameter, `self`, a declaration of the module
172/// — has none, and is compared by name.
173///
174/// Two places overlap when one is a prefix of the other, which is exactly
175/// when writing through either is observable through the other.
176#[derive(Clone, Debug, PartialEq, Eq)]
177struct Place {
178 root: String,
179 binding: Option<usize>,
180 fields: Vec<String>,
181}
182
183impl Place {
184 fn overlaps(&self, other: &Place) -> bool {
185 self.binding == other.binding
186 && self.root == other.root
187 && self.fields.iter().zip(&other.fields).all(|(a, b)| a == b)
188 }
189
190 /// The place as a reader wrote it.
191 fn text(&self) -> String {
192 let mut out = self.root.clone();
193 for field in &self.fields {
194 out.push('.');
195 out.push_str(field);
196 }
197 out
198 }
199}
200
201/// One read of a place inside a body.
202#[derive(Clone, Debug)]
203struct Read {
204 place: Place,
205 span: Span,
206 /// What the reader should be told this position was, when the handle
207 /// outlives the expression that read it.
208 retained: Option<&'static str>,
209 /// How many closure bodies deep the read is. A read deeper than its
210 /// binding's own depth is a capture, whatever position it is in.
211 depth: usize,
212}
213
214/// A binding this body introduces.
215#[derive(Clone, Debug)]
216struct Local<'a> {
217 name: &'a str,
218 span: Span,
219 /// The initialiser of a `let` or `var`. A `for` binding, a match arm's
220 /// pattern and a closure parameter have none: what they name came from
221 /// somewhere this body cannot see the creation of.
222 init: Option<&'a Expr>,
223 /// The loop and closure bodies this binding sits inside.
224 regions: Vec<Span>,
225 depth: usize,
226}
227
228/// Something that consumes a place: a `freeze()`, or a call to a method that
229/// demands a uniquely owned receiver.
230#[derive(Clone, Debug)]
231struct Consume {
232 place: Place,
233 /// The whole expression, which is what a diagnostic points at.
234 span: Span,
235 /// The loop and closure bodies this site sits inside.
236 regions: Vec<Span>,
237 /// Whether this site is the operand of a `return`, so that nothing
238 /// written after it in the source runs after it.
239 terminal: bool,
240 /// `None` for a `freeze()`; the callee, for a demanded receiver.
241 through: Option<String>,
242}
243
244/// A call this body makes to a method written in an `impl` block.
245#[derive(Clone, Debug)]
246struct MethodCall {
247 target: FnKey,
248 /// The receiver place, when the receiver is one.
249 receiver: Option<Place>,
250 span: Span,
251 regions: Vec<Span>,
252 terminal: bool,
253}
254
255/// Everything one body says about places.
256#[derive(Default)]
257struct Scan<'a> {
258 locals: Vec<Local<'a>>,
259 reads: Vec<Read>,
260 /// Places an assignment writes, and where.
261 writes: Vec<(Place, Span)>,
262 freezes: Vec<Consume>,
263 calls: Vec<MethodCall>,
264}
265
266/// One body to analyse.
267struct Body<'a> {
268 key: Option<FnKey>,
269 file: FileId,
270 /// Parameter names, receiver excluded.
271 params: Vec<&'a str>,
272 /// `Some(is_var)` when this body has a receiver.
273 receiver: Option<bool>,
274 /// Whether a `var self` here can carry an obligation back to its callers:
275 /// a method of a plain `impl` block, whose every call site the checker
276 /// resolves to this declaration by name.
277 receiver_may_demand: bool,
278 block: &'a Block,
279}
280
281/// The named types whose values can reach a `Vector`.
282///
283/// A struct or enum is one when a field or a payload names a `Vector`, or
284/// names another such type — so `World`, whose fields are `Array`s, is not,
285/// and `BookingDraft`, whose `guests` is a `Vector`, is. Keyed by the type's
286/// own name without its module, because two modules' types of one name are
287/// merged here and merging in this direction only ever refuses more.
288type Bearing = BTreeSet<String>;
289
290/// Checks every `freeze()` in `program`.
291///
292/// The answer is one diagnostic per site that could not be proved, and one
293/// per read of a vector a proved site already consumed.
294pub fn check(program: &Program, facts: &Facts) -> Vec<Diagnostic> {
295 let bearing = vector_bearing(program);
296 let bodies = bodies(program);
297 let scans: Vec<Scan<'_>> = bodies
298 .iter()
299 .map(|body| scan(body, facts, &bearing))
300 .collect();
301
302 // Which methods demand a uniquely owned receiver, to a fixpoint: a
303 // `finish` that freezes `self.routes` demands `routes`, and a method that
304 // calls `finish` on `self.builder` demands `builder.routes` in turn.
305 let mut demands: BTreeMap<FnKey, BTreeSet<Vec<String>>> = BTreeMap::new();
306 loop {
307 let mut changed = false;
308 for (body, scanned) in bodies.iter().zip(&scans) {
309 let (Some(key), Some(true), true) =
310 (&body.key, body.receiver, body.receiver_may_demand)
311 else {
312 continue;
313 };
314 for consumed in consumptions(scanned, &demands) {
315 if consumed.place.root != "self" || consumed.place.binding.is_some() {
316 continue;
317 }
318 changed |= demands
319 .entry(key.clone())
320 .or_default()
321 .insert(consumed.place.fields.clone());
322 }
323 }
324 if !changed {
325 break;
326 }
327 }
328
329 let mut diagnostics = Vec::new();
330 for (body, scanned) in bodies.iter().zip(&scans) {
331 for consumed in consumptions(scanned, &demands) {
332 prove(body, scanned, facts, &demands, &consumed, &mut diagnostics);
333 }
334 }
335 diagnostics.sort_by_key(|diagnostic| {
336 diagnostic
337 .primary
338 .map(|span| (span.file.0, span.start))
339 .unwrap_or((u32::MAX, u32::MAX))
340 });
341 diagnostics
342}
343
344/// Every place a body consumes: its `freeze()` sites, and every call to a
345/// method that demands a uniquely owned receiver.
346fn consumptions(
347 scanned: &Scan<'_>,
348 demands: &BTreeMap<FnKey, BTreeSet<Vec<String>>>,
349) -> Vec<Consume> {
350 let mut out = scanned.freezes.clone();
351 for call in &scanned.calls {
352 let (Some(paths), Some(receiver)) = (demands.get(&call.target), &call.receiver) else {
353 continue;
354 };
355 for fields in paths {
356 let mut place = receiver.clone();
357 place.fields.extend(fields.iter().cloned());
358 out.push(Consume {
359 place,
360 span: call.span,
361 regions: call.regions.clone(),
362 terminal: call.terminal,
363 through: Some(format!(
364 "{}.{}",
365 call.target.1.clone().unwrap_or_default(),
366 call.target.2
367 )),
368 });
369 }
370 }
371 out
372}
373
374// --- the proof -------------------------------------------------------------
375
376/// Proves one consumption, or says what defeated it.
377fn prove(
378 body: &Body<'_>,
379 scanned: &Scan<'_>,
380 facts: &Facts,
381 demands: &BTreeMap<FnKey, BTreeSet<Vec<String>>>,
382 consumed: &Consume,
383 out: &mut Vec<Diagnostic>,
384) {
385 let place = consumed.place.text();
386 let opening = match &consumed.through {
387 None => {
388 format!("`freeze()` cannot prove that `{place}` holds the only handle to its storage")
389 }
390 Some(callee) => format!(
391 "`{callee}()` consumes `{place}`, and this call cannot prove that it holds the only \
392 handle to its storage"
393 ),
394 };
395 let refuse = |span: Span, message: String, out: &mut Vec<Diagnostic>| {
396 out.push(
397 Diagnostic::error(NOT_UNIQUE, opening.clone())
398 .at(consumed.span)
399 .label(span, message)
400 .rule(RULE)
401 .help(
402 "call `toArray()` on the vector instead, which copies the elements in O(n) \
403 and asks nothing about who else is holding it",
404 ),
405 );
406 };
407
408 // Where the root comes from. A binding this body made is provable here; a
409 // `var self` that a plain `impl` method received carries the obligation on
410 // to its callers; anything else is outside what a local pass can see.
411 let (declared, depth) = match consumed.place.binding.map(|at| &scanned.locals[at]) {
412 Some(local) => {
413 let Some(init) = local.init else {
414 refuse(
415 local.span,
416 format!(
417 "`{}` is bound to a value this function did not create, so its storage \
418 may already have another handle",
419 local.name
420 ),
421 out,
422 );
423 return;
424 };
425 if !establishes(init, &consumed.place.fields, facts, body.file) {
426 refuse(
427 init.span,
428 format!(
429 "`{}` is initialised from a value this function did not create, so its \
430 storage may already have another handle",
431 consumed.place.text()
432 ),
433 out,
434 );
435 return;
436 }
437 (local.regions.clone(), local.depth)
438 }
439 None => {
440 let carried = consumed.place.root == "self"
441 && body.receiver == Some(true)
442 && body.receiver_may_demand
443 && body
444 .key
445 .as_ref()
446 .and_then(|key| demands.get(key))
447 .is_some_and(|paths| paths.contains(&consumed.place.fields));
448 if !carried {
449 let from_caller = body.params.contains(&consumed.place.root.as_str())
450 || (consumed.place.root == "self" && body.receiver.is_some());
451 refuse(
452 consumed.span,
453 if from_caller {
454 format!(
455 "`{}` comes from this function's caller, and only a `var self` \
456 receiver of a method written in a plain `impl` block can carry the \
457 obligation back to it",
458 consumed.place.root
459 )
460 } else {
461 format!(
462 "`{}` is not a binding this function creates, so where its storage \
463 came from is not a local fact",
464 consumed.place.root
465 )
466 },
467 out,
468 );
469 return;
470 }
471 // The obligation left this body for its call sites, which this
472 // same pass checks. What stays here is that the body itself
473 // neither copies the handle nor reads it afterwards.
474 (Vec::new(), 0)
475 }
476 };
477
478 // A site inside a loop or a closure body runs more than once unless the
479 // storage is created inside it too, and a second turn would take storage
480 // the first already took.
481 if let Some(repeated) = consumed
482 .regions
483 .iter()
484 .find(|span| !declared.contains(span))
485 {
486 refuse(
487 *repeated,
488 "this may run more than once, and a second turn would consume storage the first one \
489 already took"
490 .to_string(),
491 out,
492 );
493 return;
494 }
495
496 // Written somewhere else: the place no longer names what its initialiser
497 // created.
498 if let Some((written, span)) = scanned
499 .writes
500 .iter()
501 .find(|(written, _)| written.overlaps(&consumed.place))
502 {
503 refuse(
504 *span,
505 format!(
506 "`{}` is assigned here, so the storage it names at the consumption is not the \
507 storage it was created with",
508 written.text()
509 ),
510 out,
511 );
512 return;
513 }
514
515 // Copied to another live place, or escaped.
516 if let Some((read, why)) = scanned.reads.iter().find_map(|read| {
517 if !read.place.overlaps(&consumed.place) || read.span == consumed.span {
518 return None;
519 }
520 if read.depth > depth {
521 return Some((read, "is captured by a closure"));
522 }
523 read.retained.map(|why| (read, why))
524 }) {
525 refuse(
526 read.span,
527 format!("`{}` {why} here", read.place.text()),
528 out,
529 );
530 return;
531 }
532
533 // Consumed, and therefore not usable afterward. A site a `return` carries
534 // out of the function has nothing after it to check.
535 if consumed.terminal {
536 return;
537 }
538 for read in &scanned.reads {
539 if read.place.overlaps(&consumed.place) && read.span.start >= consumed.span.end {
540 out.push(
541 Diagnostic::error(
542 USED_AFTER_FREEZE,
543 format!(
544 "`{}` is read after its storage was consumed",
545 read.place.text()
546 ),
547 )
548 .at(read.span)
549 .label(
550 consumed.span,
551 match &consumed.through {
552 None => "`freeze()` took the storage here".to_string(),
553 Some(callee) => format!("`{callee}()` took the storage here"),
554 },
555 )
556 .rule(RULE)
557 .help(
558 "read the `Array` the transition answered, or call `toArray()` instead, which \
559 copies the elements in O(n) and leaves the vector usable",
560 ),
561 );
562 }
563 }
564}
565
566/// Whether `init` creates storage this body is the only holder of, reached
567/// through `fields`.
568///
569/// With no fields, the initialiser has to be a call [`creates`] recognises.
570/// With fields, the initialiser has to be a literal this body wrote, so that
571/// the named field's own initialiser can be asked the same question.
572fn establishes(init: &Expr, fields: &[String], facts: &Facts, file: FileId) -> bool {
573 let Some((first, rest)) = fields.split_first() else {
574 return creates(init, facts, file);
575 };
576 match &init.kind {
577 ExprKind::Call { args, .. } => args
578 .iter()
579 .find(|arg| arg.label.as_ref().is_some_and(|label| label.node == *first))
580 .is_some_and(|arg| establishes(&arg.value, rest, facts, file)),
581 _ => false,
582 }
583}
584
585/// Whether this expression allocates a vector nothing else holds a handle to.
586///
587/// # Freshness is a fact `cove-schema` states, not a name this pass matches
588///
589/// The answer used to be three method names matched against the source
590/// regardless of what they were called on — `of`, `toVector`, `snapshot` —
591/// which is exactly the coupling
592/// [issue #270](https://github.com/myuon/cove/issues/270) named: a fourth
593/// builtin that answers a fresh `Vector` needed a fourth name added here, and
594/// a call that merely *shared* one of these three names — a user's own
595/// `snapshot()` on an unrelated type, reached through a value of that type —
596/// would have been read as fresh too, because nothing here ever asked what
597/// `base` actually was.
598///
599/// Now the call has to *resolve* to a builtin entry `cove-schema` marks
600/// [`MethodSchema::fresh`](cove_schema::builtins::MethodSchema::fresh), and
601/// resolving it is what tells the two call shapes apart:
602///
603/// - `Vector.of(...)`: an associated function, named through the type
604/// itself. There is no value to type — [`Facts::ty`] answers `None` for
605/// `base` for exactly this reason (see the `facts` module) — so `base`
606/// is read as a builtin type's own name instead, the same name
607/// `cove_schema::is_builtin_type` uses to admit `Vector.of(...)` in the
608/// first place.
609/// - `array.toVector()`, `vector.snapshot()`: a method, named through a
610/// receiver whose type the checker already settled and recorded. That
611/// type is read off [`Facts::ty`] and turned into the builtin schema it
612/// names, so `array.snapshot()` and `vector.snapshot()` are answered from
613/// two different entries even though the source spells the call the same
614/// way.
615///
616/// A call that resolves to neither — a declared function, a method of a
617/// declared type, or a builtin entry the schema does not mark `fresh` —
618/// answers `false`. That includes a call to a Cove-written wrapper such as
619/// `std.vector.filter`, whose own `out.freeze()` this pass proves the
620/// ordinary way from the `Vector.of()` a few lines above it in the same
621/// body: a *caller* of `filter` never reaches this function at all, because
622/// `filter`'s result is not the direct initialiser of anything `creates`
623/// looks at. See `MethodSchema::fresh` for who may assert freshness and why
624/// a declared `fn` is not on that list.
625fn creates(init: &Expr, facts: &Facts, file: FileId) -> bool {
626 let ExprKind::Call { callee, .. } = &init.kind else {
627 return false;
628 };
629 let ExprKind::Field { base, name } = &callee.kind else {
630 return false;
631 };
632 if let ExprKind::Ident(head) = &base.kind {
633 if facts.ty(file, base.id).is_none() {
634 if let Some(schema) = cove_schema::builtin(head) {
635 return schema
636 .associated_function(&name.node)
637 .is_some_and(|method| method.fresh);
638 }
639 }
640 }
641 facts
642 .ty(file, base.id)
643 .and_then(crate::typeck::builtin_schema_of)
644 .and_then(|schema| schema.method(&name.node))
645 .is_some_and(|method| method.fresh)
646}
647
648// --- reading a body --------------------------------------------------------
649
650/// The places one body reads, writes, creates and consumes.
651fn scan<'a>(body: &Body<'a>, facts: &Facts, bearing: &Bearing) -> Scan<'a> {
652 let mut walk = Walk {
653 facts,
654 bearing,
655 file: body.file,
656 regions: Vec::new(),
657 depth: 0,
658 terminal: false,
659 scopes: vec![Vec::new()],
660 scan: Scan::default(),
661 };
662 walk.block(body.block, None);
663 walk.scan
664}
665
666/// A walk of one body, carrying where it is.
667struct Walk<'a, 'f> {
668 facts: &'f Facts,
669 bearing: &'f Bearing,
670 file: FileId,
671 /// The loop and closure bodies enclosing the expression being walked.
672 regions: Vec<Span>,
673 depth: usize,
674 /// Whether what is being walked is carried out of the function by a
675 /// `return`.
676 terminal: bool,
677 /// Names in scope, innermost last, each naming a `Scan::locals` index.
678 scopes: Vec<Vec<(&'a str, usize)>>,
679 scan: Scan<'a>,
680}
681
682impl<'a> Walk<'a, '_> {
683 /// Runs `body` with a scope of its own.
684 fn scoped(&mut self, body: impl FnOnce(&mut Self)) {
685 self.scopes.push(Vec::new());
686 body(self);
687 self.scopes.pop();
688 }
689
690 /// Introduces a binding into the innermost scope.
691 fn bind(&mut self, name: &'a str, span: Span, init: Option<&'a Expr>) {
692 self.scan.locals.push(Local {
693 name,
694 span,
695 init,
696 regions: self.regions.clone(),
697 depth: self.depth,
698 });
699 let at = self.scan.locals.len() - 1;
700 self.scopes
701 .last_mut()
702 .expect("a body is walked inside a scope")
703 .push((name, at));
704 }
705
706 /// The binding `name` resolves to here, if this body made one.
707 fn resolve(&self, name: &str) -> Option<usize> {
708 self.scopes
709 .iter()
710 .rev()
711 .find_map(|scope| scope.iter().rev().find(|(bound, _)| *bound == name))
712 .map(|(_, at)| *at)
713 }
714
715 /// The place an expression names, when it names one.
716 fn place_of(&self, expr: &Expr) -> Option<Place> {
717 match &expr.kind {
718 ExprKind::Ident(name) => Some(Place {
719 root: name.clone(),
720 binding: self.resolve(name),
721 fields: Vec::new(),
722 }),
723 ExprKind::Field { base, name } => {
724 let mut place = self.place_of(base)?;
725 place.fields.push(name.node.clone());
726 Some(place)
727 }
728 _ => None,
729 }
730 }
731
732 /// `retain` is `Some(why)` when the value this position produces outlives
733 /// the expression that produced it.
734 fn block(&mut self, block: &'a Block, retain: Option<&'static str>) {
735 self.scoped(|walk| {
736 for stmt in &block.statements {
737 match &stmt.kind {
738 StmtKind::Let { name, value, .. } => {
739 walk.expr(value, Some("is copied into another binding"));
740 walk.bind(&name.node, name.span, Some(value));
741 }
742 StmtKind::Expr(expr) => walk.expr(expr, None),
743 // A local `fn` is a closure the body can call, so its own
744 // body is walked as one.
745 StmtKind::Item(item) => {
746 if let ItemKind::Fn(decl) = &item.kind {
747 walk.nested(&decl.params, &decl.body);
748 }
749 }
750 }
751 }
752 if let Some(tail) = &block.tail {
753 walk.expr(tail, retain);
754 }
755 });
756 }
757
758 /// A closure body: a region that may run more than once, and whose reads
759 /// of an outer binding are captures.
760 fn nested(&mut self, params: &'a [Param], block: &'a Block) {
761 self.regions.push(block.span);
762 self.depth += 1;
763 let terminal = std::mem::replace(&mut self.terminal, false);
764 self.scoped(|walk| {
765 for param in params {
766 walk.bind(¶m.name.node, param.name.span, None);
767 }
768 walk.block(block, None);
769 });
770 self.terminal = terminal;
771 self.depth -= 1;
772 self.regions.pop();
773 }
774
775 /// A loop body, which may run more than once but captures nothing.
776 fn repeated(&mut self, binding: Option<&'a Ident>, block: &'a Block) {
777 self.regions.push(block.span);
778 let terminal = std::mem::replace(&mut self.terminal, false);
779 self.scoped(|walk| {
780 if let Some(binding) = binding {
781 walk.bind(&binding.node, binding.span, None);
782 }
783 walk.block(block, None);
784 });
785 self.terminal = terminal;
786 self.regions.pop();
787 }
788
789 fn expr(&mut self, expr: &'a Expr, retain: Option<&'static str>) {
790 // A place is read whole. Its base is part of the path rather than a
791 // separate read, so the walk stops here.
792 if let Some(place) = self.place_of(expr) {
793 self.scan.reads.push(Read {
794 place,
795 span: expr.span,
796 retained: retain,
797 depth: self.depth,
798 });
799 return;
800 }
801 match &expr.kind {
802 ExprKind::Call {
803 callee,
804 args,
805 trailing,
806 ..
807 } => self.call(expr, callee, args, trailing.as_deref()),
808 // Reached only when the base is not a place, as in `f().x`.
809 ExprKind::Field { base, .. } => self.expr(base, None),
810 ExprKind::ArrayLit(items) => {
811 for item in items {
812 self.expr(item, Some("is stored in another value"));
813 }
814 }
815 // An interpolation formats its operand and keeps nothing.
816 ExprKind::Str(parts) => {
817 for part in parts {
818 if let StrPart::Interpolation(inner) = part {
819 self.expr(inner, None);
820 }
821 }
822 }
823 ExprKind::Unary { operand, .. } => self.expr(operand, None),
824 ExprKind::Binary { lhs, rhs, .. } => {
825 self.expr(lhs, None);
826 self.expr(rhs, None);
827 }
828 ExprKind::Assign { target, value, .. } => {
829 match self.place_of(target) {
830 Some(place) => self.scan.writes.push((place, expr.span)),
831 None => self.expr(target, None),
832 }
833 self.expr(value, Some("is copied into another place"));
834 }
835 ExprKind::Try(inner) | ExprKind::Await(inner) => self.expr(inner, retain),
836 ExprKind::Block(block) => self.block(block, retain),
837 ExprKind::If {
838 condition,
839 then_branch,
840 else_branch,
841 } => {
842 self.expr(condition, None);
843 self.block(then_branch, retain);
844 if let Some(other) = else_branch {
845 self.expr(other, retain);
846 }
847 }
848 ExprKind::Match { scrutinee, arms } => {
849 self.expr(scrutinee, None);
850 for arm in arms {
851 self.scoped(|walk| {
852 walk.pattern(&arm.pattern);
853 walk.expr(&arm.body, retain);
854 });
855 }
856 }
857 // Iterating reads elements out; the sequence is not retained.
858 ExprKind::For {
859 binding,
860 iterable,
861 body,
862 } => {
863 self.expr(iterable, None);
864 self.repeated(Some(binding), body);
865 }
866 ExprKind::While { condition, body } => {
867 self.expr(condition, None);
868 self.repeated(None, body);
869 }
870 ExprKind::Return(Some(value)) => {
871 let outer = std::mem::replace(&mut self.terminal, true);
872 self.expr(value, Some("is returned"));
873 self.terminal = outer;
874 }
875 // A loop is `Unit` however it leaves, so a `break` value is
876 // evaluated and discarded.
877 ExprKind::Break(Some(value)) => self.expr(value, None),
878 ExprKind::Lambda { params, body, .. } => self.nested(params, body),
879 // A scope's body may outlive the statement that wrote it — a
880 // spawned task runs inside it — so it is a closure body here, and
881 // the name it binds is one of its own.
882 ExprKind::Scope { name, body } => {
883 self.regions.push(body.span);
884 self.depth += 1;
885 let terminal = std::mem::replace(&mut self.terminal, false);
886 self.scoped(|walk| {
887 walk.bind(&name.node, name.span, None);
888 walk.block(body, None);
889 });
890 self.terminal = terminal;
891 self.depth -= 1;
892 self.regions.pop();
893 }
894 ExprKind::Range { start, end, .. } => {
895 self.expr(start, None);
896 self.expr(end, None);
897 }
898 _ => {}
899 }
900 }
901
902 /// Every name a pattern binds, as a binding this body cannot see the
903 /// creation of.
904 fn pattern(&mut self, pattern: &'a Pattern) {
905 match &pattern.kind {
906 PatternKind::Binding(name) => self.bind(name, pattern.span, None),
907 PatternKind::Variant { payload, .. } => {
908 for inner in payload {
909 self.pattern(inner);
910 }
911 }
912 PatternKind::Wildcard | PatternKind::Literal(_) => {}
913 }
914 }
915
916 /// A call, and what each of its operands does with the handle it names.
917 ///
918 /// A `Vector` cannot cross a task boundary and cannot be held by a Host
919 /// resource — the Language Card's task-safety rule and ADR 0017's
920 /// boundary see to both — so the handle a callee is passed can only
921 /// outlive the call by leaving through the callee's own result, through a
922 /// `var` argument the caller can see, or by being written into another
923 /// operand that is itself a container. When none of those is possible the
924 /// copy dies with the call, and treating it as an escape would refuse
925 /// `world.firstFree(..., creatures)` for nothing.
926 fn call(
927 &mut self,
928 call: &'a Expr,
929 callee: &'a Expr,
930 args: &'a [Arg],
931 trailing: Option<&'a Expr>,
932 ) {
933 let result_holds = self
934 .facts
935 .ty(self.file, call.id)
936 .is_some_and(|ty| self.holds_vector(ty));
937 let receiver = match &callee.kind {
938 ExprKind::Field { base, name } => {
939 self.method(call, base, &name.node, result_holds);
940 Some(base)
941 }
942 _ => {
943 self.expr(callee, None);
944 None
945 }
946 };
947 // Which operands could be a container the callee writes another
948 // operand into.
949 let containers: Vec<bool> = receiver
950 .into_iter()
951 .map(|base| &**base)
952 .chain(args.iter().map(|arg| &arg.value))
953 .map(|operand| {
954 self.facts
955 .ty(self.file, operand.id)
956 .is_some_and(|ty| self.holds_vector(ty))
957 })
958 .collect();
959 let elsewhere = |at: usize| {
960 containers
961 .iter()
962 .enumerate()
963 .any(|(j, held)| *held && j != at)
964 };
965 let offset = usize::from(receiver.is_some());
966 for (at, arg) in args.iter().enumerate() {
967 let inout = args
968 .iter()
969 .enumerate()
970 .any(|(j, other)| other.is_var && j != at);
971 let retained = if result_holds {
972 Some("escapes into a call that may answer with it")
973 } else if inout {
974 Some("escapes into a call that writes through a `var` argument")
975 } else if elsewhere(at + offset) {
976 Some("escapes into a call that may store it in another argument")
977 } else {
978 None
979 };
980 self.expr(&arg.value, retained);
981 }
982 if let Some(trailing) = trailing {
983 self.expr(trailing, Some("is captured by a trailing closure"));
984 }
985 }
986
987 /// A method call's receiver, and whether this call is a consumption.
988 fn method(&mut self, call: &'a Expr, base: &'a Expr, name: &str, result_holds: bool) {
989 let receiver = self.place_of(base);
990 if name == "freeze" && matches!(self.facts.ty(self.file, base.id), Some(Ty::Vector(_))) {
991 if let Some(place) = receiver.clone() {
992 self.scan.freezes.push(Consume {
993 place,
994 span: call.span,
995 regions: self.regions.clone(),
996 terminal: self.terminal,
997 through: None,
998 });
999 }
1000 }
1001 let declared = self.facts.target(self.file, call.id);
1002 if let Some(target) = declared {
1003 self.scan.calls.push(MethodCall {
1004 target: (
1005 target.module.clone(),
1006 Some(target.type_name.clone()),
1007 target.method.clone(),
1008 ),
1009 receiver,
1010 span: call.span,
1011 regions: self.regions.clone(),
1012 terminal: self.terminal,
1013 });
1014 }
1015 // A declared method whose result can reach a `Vector` may be handing
1016 // the receiver's own field back, which is a copy of the handle. Every
1017 // builtin that answers one — `snapshot`, `toVector` — answers a fresh
1018 // one, and every other position reads through the receiver without
1019 // keeping it.
1020 self.expr(
1021 base,
1022 (declared.is_some() && result_holds)
1023 .then_some("is handed back by a method called here"),
1024 );
1025 }
1026
1027 /// Whether a value of this type can reach a `Vector`.
1028 fn holds_vector(&self, ty: &Ty) -> bool {
1029 match ty {
1030 Ty::Vector(_) => true,
1031 Ty::Array(inner)
1032 | Ty::Set(inner)
1033 | Ty::Option(inner)
1034 | Ty::Task(inner)
1035 | Ty::Shared(inner) => self.holds_vector(inner),
1036 Ty::Map(key, value) | Ty::MapEntry(key, value) | Ty::Result(key, value) => {
1037 self.holds_vector(key) || self.holds_vector(value)
1038 }
1039 Ty::Struct(name, args) | Ty::Enum(name, args) => {
1040 self.bearing.contains(simple_name(name))
1041 || args.iter().any(|ty| self.holds_vector(ty))
1042 }
1043 Ty::Fn(signature) => {
1044 signature.params.iter().any(|ty| self.holds_vector(ty))
1045 || self.holds_vector(&signature.ret)
1046 }
1047 _ => false,
1048 }
1049 }
1050}
1051
1052/// A type's name without the module that qualifies it.
1053fn simple_name(name: &str) -> &str {
1054 name.rsplit('.').next().unwrap_or(name)
1055}
1056
1057/// Every declared type whose values can reach a `Vector`, to a fixpoint.
1058///
1059/// Read off the written types rather than the checked ones, because what is
1060/// wanted is one bit per declaration and the declarations are what the
1061/// package holds. A name is compared without its module for the reason
1062/// [`Bearing`] states.
1063fn vector_bearing(program: &Program) -> Bearing {
1064 let mut members: BTreeMap<&str, Vec<&Type>> = BTreeMap::new();
1065 for module in program.modules.values() {
1066 for (name, entry) in &module.structs {
1067 members
1068 .entry(simple_name(name))
1069 .or_default()
1070 .extend(entry.decl.fields.iter().map(|field| &field.ty));
1071 }
1072 for (name, entry) in &module.enums {
1073 members
1074 .entry(simple_name(name))
1075 .or_default()
1076 .extend(entry.decl.cases.iter().flat_map(|case| case.payload.iter()));
1077 }
1078 }
1079 let mut bearing = Bearing::new();
1080 loop {
1081 let mut changed = false;
1082 for (name, types) in &members {
1083 if bearing.contains(*name) {
1084 continue;
1085 }
1086 if types.iter().any(|ty| names_a_vector(ty, &bearing)) {
1087 bearing.insert((*name).to_string());
1088 changed = true;
1089 }
1090 }
1091 if !changed {
1092 return bearing;
1093 }
1094 }
1095}
1096
1097/// Whether a written type names `Vector`, or a type already known to bear
1098/// one.
1099fn names_a_vector(ty: &Type, bearing: &Bearing) -> bool {
1100 match &ty.kind {
1101 TypeKind::Named { path, args } => {
1102 let name = path
1103 .last()
1104 .map(|segment| segment.node.as_str())
1105 .unwrap_or_default();
1106 name == "Vector"
1107 || bearing.contains(name)
1108 || args.iter().any(|arg| names_a_vector(arg, bearing))
1109 }
1110 TypeKind::Fn {
1111 params,
1112 return_type,
1113 ..
1114 } => {
1115 params.iter().any(|param| {
1116 param
1117 .ty
1118 .as_ref()
1119 .is_some_and(|ty| names_a_vector(ty, bearing))
1120 }) || return_type
1121 .as_ref()
1122 .is_some_and(|ty| names_a_vector(ty, bearing))
1123 }
1124 TypeKind::Dyn(_) | TypeKind::Unit => false,
1125 }
1126}
1127
1128/// Every body of the package, in one list.
1129///
1130/// A trait's default body is here once, where the trait declares it, exactly
1131/// as the type checker walks it — a conformance that inherits one does not
1132/// get a copy.
1133fn bodies(program: &Program) -> Vec<Body<'_>> {
1134 // A method name any trait declares can be reached through a bound or
1135 // through `dyn`, and neither names a declaration for an obligation to be
1136 // discharged at. Such a method may freeze what it creates, and may not
1137 // demand anything of its callers.
1138 let through_a_trait: BTreeSet<&str> = program
1139 .modules
1140 .values()
1141 .flat_map(|module| module.traits.values())
1142 .flat_map(|entry| entry.decl.methods.iter())
1143 .map(|method| method.name.node.as_str())
1144 .collect();
1145
1146 let mut out = Vec::new();
1147 for (module_name, module) in &program.modules {
1148 for (name, entry) in &module.functions {
1149 out.push(Body {
1150 key: Some((module_name.clone(), None, name.clone())),
1151 file: entry.decl.span.file,
1152 params: names(&entry.decl.params),
1153 receiver: entry.decl.receiver.map(|receiver| receiver.is_var),
1154 receiver_may_demand: false,
1155 block: &entry.decl.body,
1156 });
1157 }
1158 for ((type_name, name), entry) in &module.methods {
1159 if entry.from_trait_default.is_some() {
1160 continue;
1161 }
1162 out.push(Body {
1163 key: Some((module_name.clone(), Some(type_name.clone()), name.clone())),
1164 file: entry.decl.span.file,
1165 params: names(&entry.decl.params),
1166 receiver: entry.decl.receiver.map(|receiver| receiver.is_var),
1167 receiver_may_demand: !through_a_trait.contains(name.as_str()),
1168 block: &entry.decl.body,
1169 });
1170 }
1171 for entry in module.traits.values() {
1172 for method in &entry.decl.methods {
1173 let Some(default) = &method.default else {
1174 continue;
1175 };
1176 out.push(Body {
1177 key: None,
1178 file: method.span.file,
1179 params: names(&method.params),
1180 receiver: method.receiver.map(|receiver| receiver.is_var),
1181 receiver_may_demand: false,
1182 block: default,
1183 });
1184 }
1185 }
1186 }
1187 out
1188}
1189
1190/// The names of a declaration's parameters, in order.
1191fn names(params: &[Param]) -> Vec<&str> {
1192 params
1193 .iter()
1194 .map(|param| param.name.node.as_str())
1195 .collect()
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use std::collections::BTreeMap;
1201 use std::path::PathBuf;
1202
1203 use cove_diag::SourceMap;
1204
1205 use super::*;
1206 use crate::package::{Module, Package, Unit};
1207 use crate::typeck::check;
1208
1209 /// Everything `cove check` reports about one module.
1210 fn errors_of(source: &str) -> Vec<Diagnostic> {
1211 let mut sources = SourceMap::new();
1212 let path = PathBuf::from("main.cove");
1213 let file = sources.add(path.clone(), source);
1214 let ast = cove_syntax::parse_file(&sources, file).expect("test source parses");
1215 let package = Package {
1216 root: PathBuf::new(),
1217 config: crate::config::Config::default(),
1218 modules: BTreeMap::from([(
1219 "main".to_string(),
1220 Module {
1221 name: "main".to_string(),
1222 dir: PathBuf::from("main"),
1223 units: vec![Unit { file, path, ast }],
1224 },
1225 )]),
1226 };
1227 let program = crate::resolve::resolve(&package).expect("test source resolves");
1228 check(&package, &program)
1229 .into_iter()
1230 .filter(|diagnostic| diagnostic.severity == cove_diag::Severity::Error)
1231 .collect()
1232 }
1233
1234 #[track_caller]
1235 fn proves(source: &str) {
1236 let errors = errors_of(source);
1237 assert!(
1238 errors.is_empty(),
1239 "expected the proof to succeed, found: {}",
1240 errors
1241 .iter()
1242 .map(|error| format!("{}: {}", error.code, error.message))
1243 .collect::<Vec<_>>()
1244 .join("; ")
1245 );
1246 }
1247
1248 #[track_caller]
1249 fn refuses(source: &str) -> Diagnostic {
1250 let mut errors = errors_of(source);
1251 assert_eq!(
1252 errors.len(),
1253 1,
1254 "expected exactly one error, found: {}",
1255 errors
1256 .iter()
1257 .map(|error| format!("{}: {}", error.code, error.message))
1258 .collect::<Vec<_>>()
1259 .join("; ")
1260 );
1261 errors.remove(0)
1262 }
1263
1264 /// The shape `freeze()` was written for: build a vector, hand it over.
1265 #[test]
1266 fn a_vector_built_here_and_handed_over_is_proved() {
1267 proves(
1268 "\
1269fn build(upTo: Int) -> Array<Int> {
1270 var building = Vector.of()
1271 for n in 1..upTo {
1272 building.push(n)
1273 }
1274 building.freeze()
1275}
1276",
1277 );
1278 }
1279
1280 /// A temporary holds the only handle to its own storage, so there is
1281 /// nothing to prove and no place to name.
1282 #[test]
1283 fn a_temporary_receiver_needs_no_proof() {
1284 proves("fn build() -> Int {\n Vector.of(1, 2).freeze().length()\n}\n");
1285 proves("fn build(items: Array<Int>) -> Array<Int> {\n items.toVector().freeze()\n}\n");
1286 }
1287
1288 /// The case the corpus pins: a second binding is a second handle.
1289 #[test]
1290 fn a_second_binding_defeats_the_proof_and_the_diagnostic_names_it() {
1291 let error = refuses(
1292 "\
1293fn build() -> Array<Int> {
1294 var building = Vector.of(1, 2)
1295 var alias = building
1296 alias.push(3)
1297 building.freeze()
1298}
1299",
1300 );
1301 assert_eq!(error.code, NOT_UNIQUE);
1302 assert_eq!(
1303 error.message,
1304 "`freeze()` cannot prove that `building` holds the only handle to its storage"
1305 );
1306 assert_eq!(
1307 error.labels[0].message,
1308 "`building` is copied into another binding here"
1309 );
1310 assert!(
1311 error
1312 .help
1313 .as_deref()
1314 .is_some_and(|help| help.contains("toArray()")),
1315 "{:?}",
1316 error.help
1317 );
1318 }
1319
1320 /// Formatting a vector and pushing onto it both read through the handle
1321 /// without keeping it, which is what `tests/e2e:coll_array` needs.
1322 #[test]
1323 fn interpolating_and_pushing_are_not_escapes() {
1324 proves(
1325 "\
1326use console.println
1327
1328fn build(items: Array<Int>) -> Result<Array<Int>, Error> {
1329 var growable = items.toVector()
1330 growable.push(40)
1331 println(\"{growable}\")?
1332 Ok(growable.freeze())
1333}
1334",
1335 );
1336 }
1337
1338 /// A callee that cannot keep the handle is not an escape; the three
1339 /// ways one can are.
1340 /// **A vector handed to a call as its own `var` argument is still the
1341 /// caller's afterwards.**
1342 ///
1343 /// A `var` argument is a write-through borrow that ends when the call
1344 /// returns, and the three ways a handle outlives a call are the three
1345 /// this module's `call` documents: the callee's result, another `var`
1346 /// argument the caller can see, or another operand that is a container.
1347 /// For the `var` argument *itself* the second route would mean writing
1348 /// it into itself, which needs a `Vector<T>` whose `T` is that same
1349 /// `Vector<T>` and is not a type Cove can write.
1350 ///
1351 /// This was refused, and the refusal cost the one pattern the language
1352 /// has for building a sequence: fill a `Vector` through a call, then
1353 /// `freeze` it. `examples/covefmt` wrote `"".join(out.toArray())` at nine
1354 /// sites because of it, and `toArray` copies the whole store where
1355 /// `freeze` re-labels it in place.
1356 #[test]
1357 fn a_var_argument_is_not_made_shared_by_being_the_var_argument() {
1358 proves(
1359 "\
1360fn fill(var into: Vector<Int>) {
1361 into.push(1)
1362}
1363
1364fn build() -> Array<Int> {
1365 var items = Vector.of(1, 2)
1366 fill(var items)
1367 items.freeze()
1368}
1369",
1370 );
1371 // A *second* container beside it is the case that still escapes: the
1372 // callee may write that one into this one, and which way round the
1373 // types would allow is not something this asks.
1374 let beside = refuses(
1375 "\
1376fn fill(var into: Vector<Vector<Int>>, from: Vector<Int>) {
1377 into.push(from)
1378}
1379
1380fn build() -> Array<Int> {
1381 var rows = Vector.of<Vector<Int>>()
1382 var items = Vector.of(1, 2)
1383 fill(var rows, items)
1384 items.freeze()
1385}
1386",
1387 );
1388 assert_eq!(
1389 beside.labels[0].message,
1390 "`items` escapes into a call that writes through a `var` argument here"
1391 );
1392 }
1393
1394 #[test]
1395 fn a_call_escapes_only_when_the_callee_could_keep_the_handle() {
1396 proves(
1397 "\
1398fn total(of: Vector<Int>) -> Int {
1399 of.length()
1400}
1401
1402fn build() -> Array<Int> {
1403 var items = Vector.of(1, 2)
1404 total(items)
1405 items.freeze()
1406}
1407",
1408 );
1409 let answered = refuses(
1410 "\
1411fn wrap(one: Vector<Int>) -> Vector<Vector<Int>> {
1412 Vector.of(one)
1413}
1414
1415fn build() -> Array<Int> {
1416 var items = Vector.of(1, 2)
1417 wrap(items)
1418 items.freeze()
1419}
1420",
1421 );
1422 assert_eq!(
1423 answered.labels[0].message,
1424 "`items` escapes into a call that may answer with it here"
1425 );
1426 let written = refuses(
1427 "\
1428fn fill(var into: Vector<Int>, from: Vector<Int>) {
1429 into.push(from.length())
1430}
1431
1432fn build() -> Array<Int> {
1433 var items = Vector.of(1, 2)
1434 var sink = Vector.of(0)
1435 fill(var sink, items)
1436 items.freeze()
1437}
1438",
1439 );
1440 assert_eq!(
1441 written.labels[0].message,
1442 "`items` escapes into a call that writes through a `var` argument here"
1443 );
1444 let stored = refuses(
1445 "\
1446struct Sink {
1447 rows: Vector<Vector<Int>>
1448}
1449
1450fn keep(one: Vector<Int>, into: Sink) {
1451 var rows = into.rows
1452 rows.push(one)
1453}
1454
1455fn build(sink: Sink) -> Array<Int> {
1456 var items = Vector.of(1, 2)
1457 keep(items, sink)
1458 items.freeze()
1459}
1460",
1461 );
1462 assert_eq!(
1463 stored.labels[0].message,
1464 "`items` escapes into a call that may store it in another argument here"
1465 );
1466 }
1467
1468 /// A closure that mentions the vector holds it for as long as the closure
1469 /// lives, which this pass cannot bound.
1470 #[test]
1471 fn a_closure_capture_defeats_the_proof() {
1472 let error = refuses(
1473 "\
1474fn build() -> Array<Int> {
1475 var items = Vector.of(1, 2)
1476 let count = fn() {
1477 items.length()
1478 }
1479 count()
1480 items.freeze()
1481}
1482",
1483 );
1484 assert_eq!(
1485 error.labels[0].message,
1486 "`items` is captured by a closure here"
1487 );
1488 }
1489
1490 /// `freeze()` consumes, so a read afterwards is an error of its own,
1491 /// pointing at both ends.
1492 #[test]
1493 fn a_read_after_the_freeze_is_reported_where_it_is_written() {
1494 let error = refuses(
1495 "\
1496fn build() -> Int {
1497 var items = Vector.of(1, 2)
1498 let frozen = items.freeze()
1499 frozen.length() + items.length()
1500}
1501",
1502 );
1503 assert_eq!(error.code, USED_AFTER_FREEZE);
1504 assert_eq!(
1505 error.message,
1506 "`items` is read after its storage was consumed"
1507 );
1508 assert_eq!(error.labels[0].message, "`freeze()` took the storage here");
1509 }
1510
1511 /// A second turn would find the storage gone.
1512 #[test]
1513 fn a_freeze_a_loop_runs_twice_is_refused() {
1514 let error = refuses(
1515 "\
1516fn build(rounds: Int) -> Int {
1517 var items = Vector.of(1, 2)
1518 var total = 0
1519 for _n in 0..rounds {
1520 total += items.freeze().length()
1521 }
1522 total
1523}
1524",
1525 );
1526 assert_eq!(error.code, NOT_UNIQUE);
1527 assert!(
1528 error.labels[0].message.contains("more than once"),
1529 "{}",
1530 error.labels[0].message
1531 );
1532 }
1533
1534 /// Storage that came from somewhere this body cannot see the creation of.
1535 #[test]
1536 fn a_binding_this_body_did_not_create_is_refused() {
1537 let error = refuses(
1538 "\
1539fn fresh() -> Vector<Int> {
1540 Vector.of(1)
1541}
1542
1543fn build() -> Array<Int> {
1544 var items = fresh()
1545 items.freeze()
1546}
1547",
1548 );
1549 assert_eq!(error.code, NOT_UNIQUE);
1550 assert!(
1551 error.labels[0]
1552 .message
1553 .contains("initialised from a value this function did not create"),
1554 "{}",
1555 error.labels[0].message
1556 );
1557 }
1558
1559 /// A parameter belongs to the caller, and only a `var self` receiver can
1560 /// carry the obligation back to one.
1561 #[test]
1562 fn an_ordinary_parameter_cannot_be_frozen() {
1563 let error = refuses(
1564 "\
1565fn build(var items: Vector<Int>) -> Array<Int> {
1566 items.freeze()
1567}
1568",
1569 );
1570 assert_eq!(error.code, NOT_UNIQUE);
1571 assert!(
1572 error.labels[0]
1573 .message
1574 .contains("comes from this function's caller"),
1575 "{}",
1576 error.labels[0].message
1577 );
1578 }
1579
1580 /// The builder shape: `finish` demands a unique receiver, and the call
1581 /// site is where that is proved.
1582 #[test]
1583 fn a_var_self_method_moves_the_obligation_to_its_callers() {
1584 proves(
1585 "\
1586struct Draft {
1587 guests: Vector<String>
1588}
1589
1590impl Draft {
1591 fn add(var self, name: String) {
1592 self.guests.push(name)
1593 }
1594
1595 fn finish(var self) -> Array<String> {
1596 self.guests.freeze()
1597 }
1598}
1599
1600fn build(name: String) -> Array<String> {
1601 var fresh = Draft(guests: Vector.of())
1602 fresh.add(name)
1603 fresh.finish()
1604}
1605",
1606 );
1607 }
1608
1609 /// The same method, called on a draft a second binding also observes.
1610 #[test]
1611 fn the_demand_is_discharged_at_the_call_site_and_can_fail_there() {
1612 let error = refuses(
1613 "\
1614struct Draft {
1615 guests: Vector<String>
1616}
1617
1618impl Draft {
1619 fn finish(var self) -> Array<String> {
1620 self.guests.freeze()
1621 }
1622}
1623
1624fn build() -> Array<String> {
1625 var original = Draft(guests: Vector.of(\"a\"))
1626 var alias = original
1627 alias.guests.push(\"b\")
1628 original.finish()
1629}
1630",
1631 );
1632 assert_eq!(error.code, NOT_UNIQUE);
1633 assert_eq!(
1634 error.message,
1635 "`Draft.finish()` consumes `original.guests`, and this call cannot prove that it \
1636 holds the only handle to its storage"
1637 );
1638 assert_eq!(
1639 error.labels[0].message,
1640 "`original` is copied into another binding here"
1641 );
1642 }
1643
1644 /// Two arms are two bindings, however alike their names are.
1645 #[test]
1646 fn a_name_bound_in_two_arms_is_two_bindings() {
1647 proves(
1648 "\
1649enum Shape {
1650 Left
1651 Right
1652}
1653
1654fn render(shape: Shape) -> Int {
1655 match shape {
1656 Shape.Left => {
1657 var parts = Vector.of(1)
1658 parts.freeze().length()
1659 }
1660 Shape.Right => {
1661 var parts = Vector.of(2, 3)
1662 parts.freeze().length()
1663 }
1664 }
1665}
1666",
1667 );
1668 }
1669
1670 /// A `return` carries the site out of the function, so what is written
1671 /// after it is not read after it.
1672 #[test]
1673 fn a_freeze_a_return_carries_out_has_nothing_after_it() {
1674 proves(
1675 "\
1676fn build(early: Bool) -> Array<Int> {
1677 var items = Vector.of()
1678 if early {
1679 return items.freeze()
1680 }
1681 items.push(1)
1682 items.freeze()
1683}
1684",
1685 );
1686 }
1687
1688 // -- issue #270: freshness is a schema fact, not a name this pass reads -
1689
1690 /// `Vector.snapshot()` is `cove-schema`'s third `fresh` entry, and the
1691 /// one that needs a receiver's *settled type* rather than a bare `Ident`
1692 /// to resolve: unlike `Vector.of(...)`, `items.snapshot()` is reached
1693 /// through a value, and [`creates`] has to read that value's type off
1694 /// [`Facts::ty`] and turn it into the right builtin schema before it can
1695 /// ask whether `snapshot` is `fresh` there. Binding the result first,
1696 /// rather than freezing it as a temporary the way
1697 /// [`a_temporary_receiver_needs_no_proof`] does, is what exercises
1698 /// [`establishes`] rather than only the direct case.
1699 #[test]
1700 fn a_vectors_own_snapshot_is_a_fresh_primitive_result() {
1701 proves(
1702 "\
1703fn build(items: Vector<Int>) -> Array<Int> {
1704 var copy = items.snapshot()
1705 copy.freeze()
1706}
1707",
1708 );
1709 }
1710
1711 /// The regression this issue is named for: before, `creates()` matched
1712 /// the method name `toVector` against *any* receiver, so a user's own
1713 /// method of that name — on a type `cove-schema` says nothing about —
1714 /// was read as fresh too. `Holder.toVector()` here hands back a field
1715 /// it did not just allocate, and a caller that binds and freezes it has
1716 /// exactly the alias the proof exists to catch: `holder` and `copy`
1717 /// would share `holder.items`'s storage. A non-fresh result has to stay
1718 /// refused now that the check is `cove-schema`-driven, the same as it
1719 /// was refused by luck before — this program is why "by luck" was not
1720 /// good enough.
1721 #[test]
1722 fn a_declared_methods_result_is_not_fresh_even_when_it_shares_a_builtins_name() {
1723 let error = refuses(
1724 "\
1725struct Holder {
1726 items: Vector<Int>
1727}
1728
1729impl Holder {
1730 fn toVector(self) -> Vector<Int> {
1731 self.items
1732 }
1733}
1734
1735fn build(holder: Holder) -> Array<Int> {
1736 var copy = holder.toVector()
1737 copy.freeze()
1738}
1739",
1740 );
1741 assert_eq!(error.code, NOT_UNIQUE);
1742 assert_eq!(
1743 error.message,
1744 "`freeze()` cannot prove that `copy` holds the only handle to its storage"
1745 );
1746 }
1747
1748 /// The wrapper case the issue asks to think hardest about, in the shape
1749 /// `std.vector.filter` and `std.array.filter` are actually written:
1750 /// `var out = Vector.of(); ...; out.freeze()`. That `freeze()` is
1751 /// proved the ordinary local way, from the `Vector.of()` a few lines
1752 /// above it in the very same body — nothing here has to know `make` is
1753 /// a "wrapper" for its own proof to go through.
1754 ///
1755 /// What does not follow is that `make`'s *result* is fresh to whoever
1756 /// calls it. `establishes()` only ever asks [`creates`] about a call's
1757 /// own callee, and a call to `make` names a declared function, not a
1758 /// builtin schema entry — the same refusal
1759 /// [`a_binding_this_body_did_not_create_is_refused`] already pins for a
1760 /// single-expression wrapper. This test is the multi-statement shape a
1761 /// real one is written in, proved and refused in the same place so the
1762 /// two facts read together: a wrapper's own `freeze()` inside it is
1763 /// unaffected by this pass, and a wrapper's `return` still carries
1764 /// nothing past its own body.
1765 #[test]
1766 fn a_cove_wrappers_return_is_not_fresh_for_its_caller() {
1767 proves(
1768 "\
1769fn make() -> Array<Int> {
1770 var out = Vector.of(1, 2)
1771 out.freeze()
1772}
1773",
1774 );
1775 let error = refuses(
1776 "\
1777fn make() -> Vector<Int> {
1778 Vector.of(1, 2)
1779}
1780
1781fn build() -> Array<Int> {
1782 var log = make()
1783 log.freeze()
1784}
1785",
1786 );
1787 assert_eq!(error.code, NOT_UNIQUE);
1788 assert_eq!(
1789 error.message,
1790 "`freeze()` cannot prove that `log` holds the only handle to its storage"
1791 );
1792 assert_eq!(
1793 error.labels[0].message,
1794 "`log` is initialised from a value this function did not create, so its storage \
1795 may already have another handle"
1796 );
1797 }
1798
1799 /// The same wrapper, renamed. `creates()` never reads a call's callee
1800 /// name at all once the callee is not a builtin's own `Field` — it asks
1801 /// whether the call *resolves* to a schema entry, and a declared
1802 /// function does not, whatever it is spelled. Naming it `toVector` here
1803 /// — a name `cove-schema` itself marks `fresh` on a different type —
1804 /// is deliberate: if this pass still matched by name anywhere in this
1805 /// path, this is the program that would prove when it must not.
1806 #[test]
1807 fn renaming_the_wrapper_changes_nothing() {
1808 let error = refuses(
1809 "\
1810fn toVector() -> Vector<Int> {
1811 Vector.of(1, 2)
1812}
1813
1814fn build() -> Array<Int> {
1815 var log = toVector()
1816 log.freeze()
1817}
1818",
1819 );
1820 assert_eq!(error.code, NOT_UNIQUE);
1821 assert_eq!(
1822 error.labels[0].message,
1823 "`log` is initialised from a value this function did not create, so its storage \
1824 may already have another handle"
1825 );
1826 }
1827}