|
41 | 41 | //! `A` **dense** and monomorphic for its whole lifetime. |
42 | 42 | //! * **E3 — array containment.** Every *other* use of `A` is an in-bounds |
43 | 43 | //! element read (E5), a `.length` read, or `return A`. The return exemption |
44 | | -//! is #7034 §4's, unchanged and for the same reason. Anything else — call |
45 | | -//! argument, closure capture, reassignment, `IndexSet`, an unrecognised |
46 | | -//! array method, being an element of another container — disqualifies `A`. |
| 44 | +//! is #7034 §4's, unchanged and for the same reason. The one conditional |
| 45 | +//! escape is the compiler-generated `ArrayIterationPatched` guard described |
| 46 | +//! below. Anything else — call argument, closure capture, reassignment, |
| 47 | +//! `IndexSet`, an unrecognised array method, being an element of another |
| 48 | +//! container — disqualifies `A`. |
47 | 49 | //! * **E4 — class admissibility.** `C` passes the same `chain_admissible` |
48 | 50 | //! gate rule 1 applies to a `new C(...)` local, and the module-wide rule-5 |
49 | 51 | //! barrier scan is clear. |
|
58 | 60 | //! `A[i]` can be `undefined`, and a guard-free fixed-offset load masks a |
59 | 61 | //! NaN-boxed `undefined` into a wild pointer. |
60 | 62 | //! |
61 | | -//! `for (const r of A)` desugars to exactly the E5 shape |
| 63 | +//! `for (const r of A)` has an E5 index arm |
62 | 64 | //! (`lower/stmt_loops.rs::lazy_or_index_elem` — a `__idx` local, `__idx < |
63 | | -//! __arr.length`, `Let r = IndexGet(__arr, __idx)`), so the iterator form is |
64 | | -//! covered by the indexed proof rather than by a second one. |
| 65 | +//! __arr.length`, `Let r = IndexGet(__arr, __idx)`) behind the |
| 66 | +//! `ArrayIterationPatched` runtime guard. Its lazy arm passes `A` to |
| 67 | +//! `GetIterator`, which is ordinarily an E3 escape: a custom iterator can |
| 68 | +//! reshape an element before returning. The index-arm facts remain sound only |
| 69 | +//! when that top-level guard is the last use of both the array and every |
| 70 | +//! element-group member. Then the mutating lazy arm and the proven index arm |
| 71 | +//! are mutually exclusive, and no fact crosses their join. A nested guard is |
| 72 | +//! refused because a loop backedge could bring the mutated array to an |
| 73 | +//! earlier proven access on the next iteration. |
65 | 74 | //! |
66 | 75 | //! ## What the facts are used for |
67 | 76 | //! |
@@ -314,14 +323,15 @@ pub(crate) fn collect_element_shape_facts( |
314 | 323 | // E3/E5: the array use walk. |
315 | 324 | let mut walk = ArrayWalk { |
316 | 325 | roots: &array_roots, |
| 326 | + alias_edges: &alias_edges, |
317 | 327 | disqualified: HashSet::new(), |
318 | 328 | pushes: HashMap::new(), |
319 | 329 | reads: Vec::new(), |
320 | 330 | idx_writes: HashMap::new(), |
321 | 331 | bounded: Vec::new(), |
322 | 332 | in_closure: false, |
323 | 333 | }; |
324 | | - walk.walk_stmts(stmts); |
| 334 | + walk.walk_region_stmts(stmts); |
325 | 335 | let ArrayWalk { |
326 | 336 | mut disqualified, |
327 | 337 | pushes, |
@@ -487,6 +497,7 @@ struct ReadSite { |
487 | 497 |
|
488 | 498 | struct ArrayWalk<'a> { |
489 | 499 | roots: &'a HashMap<u32, u32>, |
| 500 | + alias_edges: &'a [(u32, u32)], |
490 | 501 | disqualified: HashSet<u32>, |
491 | 502 | pushes: HashMap<u32, Vec<PushValue>>, |
492 | 503 | reads: Vec<ReadSite>, |
@@ -520,6 +531,110 @@ impl<'a> ArrayWalk<'a> { |
520 | 531 | } |
521 | 532 | } |
522 | 533 |
|
| 534 | + /// Walk the region's outer statement list, where a one-shot temporal |
| 535 | + /// boundary can be proved. Nested statement lists deliberately use |
| 536 | + /// `walk_stmts`: admitting a guarded escape inside a loop would let its |
| 537 | + /// lazy arm reshape the array before a backedge reaches an earlier fact. |
| 538 | + fn walk_region_stmts(&mut self, stmts: &[Stmt]) { |
| 539 | + for (index, stmt) in stmts.iter().enumerate() { |
| 540 | + if self.walk_terminal_array_iteration_guard(stmt, &stmts[index + 1..]) { |
| 541 | + continue; |
| 542 | + } |
| 543 | + self.walk_stmt(stmt); |
| 544 | + } |
| 545 | + } |
| 546 | + |
| 547 | + /// Admit the compiler-generated guarded `for…of` shape without treating |
| 548 | + /// its one `GetIterator(A)` as an unconditional E3 escape. |
| 549 | + /// |
| 550 | + /// A patched iterator is arbitrary code and may transition any element's |
| 551 | + /// shape. Consequently the exception is temporal, not semantic: the lazy |
| 552 | + /// and index arms must be the final use of the array and of every producer |
| 553 | + /// or licensed reader in its element group. Otherwise the whole root is |
| 554 | + /// disqualified exactly as a normal bare escape would be. |
| 555 | + fn walk_terminal_array_iteration_guard(&mut self, stmt: &Stmt, following: &[Stmt]) -> bool { |
| 556 | + let Stmt::If { |
| 557 | + condition: Expr::ArrayIterationPatched, |
| 558 | + then_branch, |
| 559 | + else_branch: Some(index_branch), |
| 560 | + } = stmt |
| 561 | + else { |
| 562 | + return false; |
| 563 | + }; |
| 564 | + let Some(Stmt::Let { |
| 565 | + id: iterator_id, |
| 566 | + init: Some(Expr::GetIterator(source)), |
| 567 | + .. |
| 568 | + }) = then_branch.first() |
| 569 | + else { |
| 570 | + return false; |
| 571 | + }; |
| 572 | + let Expr::LocalGet(source_id) = source.as_ref() else { |
| 573 | + return false; |
| 574 | + }; |
| 575 | + let Some(root) = self.root_of(*source_id) else { |
| 576 | + return false; |
| 577 | + }; |
| 578 | + |
| 579 | + // Preserve the Let write, but exempt exactly its GetIterator source. |
| 580 | + // Every later lazy-arm statement is walked normally, so a second use |
| 581 | + // of the array still disqualifies it through the ordinary E3 rules. |
| 582 | + self.note_write(*iterator_id); |
| 583 | + self.walk_stmts(&then_branch[1..]); |
| 584 | + self.walk_stmts(index_branch); |
| 585 | + |
| 586 | + let array_aliases: HashSet<u32> = self |
| 587 | + .roots |
| 588 | + .iter() |
| 589 | + .filter_map(|(id, candidate_root)| (*candidate_root == root).then_some(*id)) |
| 590 | + .collect(); |
| 591 | + let mut group_members: HashSet<u32> = self |
| 592 | + .pushes |
| 593 | + .get(&root) |
| 594 | + .into_iter() |
| 595 | + .flatten() |
| 596 | + .filter_map(|push| match push { |
| 597 | + PushValue::Local(id) => Some(*id), |
| 598 | + PushValue::Fresh(_) | PushValue::Other => None, |
| 599 | + }) |
| 600 | + .chain( |
| 601 | + self.reads |
| 602 | + .iter() |
| 603 | + .filter_map(|read| (read.root == root).then_some(read.local)), |
| 604 | + ) |
| 605 | + .collect(); |
| 606 | + // `ptr_shape` promotes immutable aliases with their root. A use of an |
| 607 | + // alias after the iterator escape is therefore a use of the same |
| 608 | + // potentially-reshaped object and must participate in this boundary. |
| 609 | + loop { |
| 610 | + let mut changed = false; |
| 611 | + for (alias, source) in self.alias_edges { |
| 612 | + if group_members.contains(source) { |
| 613 | + changed |= group_members.insert(*alias); |
| 614 | + } |
| 615 | + } |
| 616 | + if !changed { |
| 617 | + break; |
| 618 | + } |
| 619 | + } |
| 620 | + |
| 621 | + let lazy_refs = local_refs(then_branch); |
| 622 | + let following_refs = local_refs(following); |
| 623 | + let lazy_array_uses = lazy_refs |
| 624 | + .iter() |
| 625 | + .filter(|id| array_aliases.contains(id)) |
| 626 | + .count(); |
| 627 | + let unsafe_after_escape = lazy_array_uses != 1 |
| 628 | + || lazy_refs.iter().any(|id| group_members.contains(id)) |
| 629 | + || following_refs |
| 630 | + .iter() |
| 631 | + .any(|id| array_aliases.contains(id) || group_members.contains(id)); |
| 632 | + if unsafe_after_escape { |
| 633 | + self.disqualified.insert(root); |
| 634 | + } |
| 635 | + true |
| 636 | + } |
| 637 | + |
523 | 638 | fn walk_stmt(&mut self, s: &Stmt) { |
524 | 639 | match s { |
525 | 640 | Stmt::Let { id, init, .. } => { |
@@ -864,6 +979,18 @@ impl<'a> ArrayWalk<'a> { |
864 | 979 | } |
865 | 980 | } |
866 | 981 |
|
| 982 | +/// Every local referenced from `stmts`, including id-keyed array operations |
| 983 | +/// and nested closure bodies. Reuse HIR's exhaustive local-id walker rather |
| 984 | +/// than maintaining another list of expression variants in this proof pass. |
| 985 | +fn local_refs(stmts: &[Stmt]) -> Vec<u32> { |
| 986 | + let mut refs = Vec::new(); |
| 987 | + let mut visited_closures = HashSet::new(); |
| 988 | + for stmt in stmts { |
| 989 | + perry_hir::collect_local_refs_stmt(stmt, &mut refs, &mut visited_closures); |
| 990 | + } |
| 991 | + refs |
| 992 | +} |
| 993 | + |
867 | 994 | /// Statement walker over one region's statement tree. |
868 | 995 | /// |
869 | 996 | /// It does NOT descend into closure bodies — those live inside `Expr`s, not |
|
0 commit comments