From e698c7303dd85f3ed302e37133f18ddf6a579887 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:10:37 +0100 Subject: [PATCH 01/25] own passes: pin the keep backstop, the 21.2 message and the shared-tail negatives - own_err_global_kept_typeof: `k.w = M.w` with `k: typeof M` reaches checkGlobalRead's keep backstop alone (teeth: with that rejection silenced the program compiles). Spec 25 now says the rule is a backstop and names its one known shape. - own_err_record_literal_nested / own_err_record_literal_let_field pin the spec 21.2 object-literal message, unpinned since T7; the 21.2 row names them. - M1: 191 negatives whose regex matched only a shared tail (a `.*`, no quoted name, or the same text as another case) now pin `:line:col: error: ` from the baseline binary. Names are matched as '[^']*': without --di every local is named by nameAt's fallback 'this value'. Left as they were: three whose text is in a note, and own_err_generator_yields_borrow, whose error sits at a synthetic location (12:1025). - vtableWritten: a comment that MLIRGen makes ts.ThisVirtualSymbolRef only for methods (a bound function, never a !ts.ref), so no program reaches that branch and no test can. Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 6 +- .../lib/TypeScript/OwnershipSignaturePass.cpp | 4 +- tslang/test/tester/CMakeLists.txt | 385 +++++++++--------- .../tester/own/own_err_global_kept_typeof.ts | 21 + .../own/own_err_record_literal_let_field.ts | 14 + .../own/own_err_record_literal_nested.ts | 12 + 6 files changed, 248 insertions(+), 194 deletions(-) create mode 100644 tslang/test/tester/own/own_err_global_kept_typeof.ts create mode 100644 tslang/test/tester/own/own_err_record_literal_let_field.ts create mode 100644 tslang/test/tester/own/own_err_record_literal_nested.ts diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index a3aa8f239..35c6e1e49 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -1626,7 +1626,7 @@ place, so all four places that report it get the same names. Each message has a | a global's value, stored, returned or boxed | `'x' borrows the global 'g' and cannot be stored, returned or captured` | `own_err_global_returned` | | a global's value read into a local | `'x' borrows the global 'g', but -mm=own cannot prove the global keeps its value while 'x' is used: a call or an assignment may overwrite it` | `own_err_global_let_call` | | a captured variable's value, through its cell | `'x' borrows a captured variable and cannot be stored, returned or captured` | `own_err_captured_returned` | -| an object literal holding a value it does not own outright: a literal nested by value, or one holding a borrowed or `let` field when it is not seen as an interface (§32; since §27, before which any literal given anywhere but an owning local or the result was one) | `an object literal that holds a value it owns is moved only into a local or returned as it is; -mm=own cannot move it here yet` | the interface-typed shapes are borrowing records (§32): `own_err_borrowing_record_*`, `own_literal_moves`, `own_borrowing_record_arg`; `own_err_record_literal_let` (§27) is gone | +| an object literal holding a value it does not own outright: a literal nested by value, or one holding a borrowed or `let` field when it is not seen as an interface (§32; since §27, before which any literal given anywhere but an owning local or the result was one) | `an object literal that holds a value it owns is moved only into a local or returned as it is; -mm=own cannot move it here yet` | `own_err_record_literal_nested` (nested by value), `own_err_record_literal_let_field` (a `let`'s value, not an interface); the interface-typed shapes are borrowing records (§32): `own_err_borrowing_record_*`, `own_literal_moves`, `own_borrowing_record_arg`; `own_err_record_literal_let` (§27) is gone | The mixed-return message is the largest bucket (17 files). Most are two synthesized helpers: - `___unbox` (`` of an `any`) returns the box's own string when it holds one, and a @@ -2978,7 +2978,9 @@ either: - or rejected: "the value read from 'G' is kept where its end at an assignment of 'G' cannot be checked; -mm=own cannot prove it outlives the global's value" - a field of an object, an array, a global, a callee that keeps it, a capture, a return. A string read is copied where it is kept - (§22), so its keeps are not this check's. + (§22), so its keeps are not this check's. This is a backstop: almost every keep fails an earlier + rule first. One shape is known to reach it alone, `k.w = M.w` with `k: typeof M` (a static kept + through a vtable slot), pinned by `own_err_global_kept_typeof`. Tests: `own_err_global_let_holder_static` (a static block's loop), `own_err_global_let_destructure` (`let { v } = { v: K.w }` in a top-level loop) and `own_err_global_let_holder` (a module global), diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 527c93ff7..fd3a6b1e8 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -195,7 +195,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper(op) && diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2dc4d3043..50d8b4fe2 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3887,181 +3887,184 @@ set_tests_properties(test-own-err-names-the-variable # Each negative program has one shape phase 0 cannot prove, and must be rejected with this text. set(own_error_cases - "own_err_assign_after_move|borrows .* but is used here after .* is released or overwritten" - "own_err_borrow_escape|borrows .* and cannot be stored, returned or captured" - "own_err_move_some_paths_field|is moved here on some paths only" - "own_err_borrow_and_move|is moved here while .* borrows it" - "own_err_borrow_bound_method|borrows .* but is used here after .* is released or overwritten" - "own_err_const_bound_method_reassigned|takes a second reference" - "own_err_borrow_catch_local|borrows .* but is used here after .* is released or overwritten" - "own_err_borrow_finally_local|borrows .* but is used here after .* is released or overwritten" - "own_err_borrow_derived_ref|borrows .* but is used here after .* is released or overwritten" - "own_err_const_alias_move|borrows .* and cannot be assigned" - "own_err_const_alias_field|is moved here while .* borrows it" - "own_err_field_alias|is used here after its value was moved" - "own_err_call_in_try_drops|borrows .* but is used here after it may be released or overwritten" - "own_err_closure_escape_used|is captured by a closure that escapes, and is still used here" - "own_err_closure_escape_twice|is captured by a closure that escapes, and is still used here" - "own_err_closure_escape_param|cannot move this variable into it: its value is the caller's" - "own_err_closure_escape_inherited|cannot move this variable into it: it is captured from an enclosing function" - "own_err_closure_escape_loop|is moved inside a loop" - "own_err_closure_escape_some_paths|is moved here on some paths only" - "own_err_closure_outlives_cell|borrows .*, which it captures, but runs here after .* is released" - "own_err_closure_copy_moved|borrows .*, which it captures, but runs here after .* is released" - "own_err_closure_param_assigned|is a captured parameter and cannot be assigned under -mm=own" - "own_err_closure_assigns_param|is a captured parameter and cannot be assigned under -mm=own" - "own_err_cell_assigned_in_frame|borrows .* but is used here after it may be released or overwritten" - "own_err_cell_assigned_in_closure|borrows .* but is used here after it may be released or overwritten" - "own_err_closure_assigns_captured_field|borrows .* but is used here after it may be released or overwritten" - "own_err_unknown_producer|takes a second reference" - "own_err_interface_two_views|takes a second reference" - "own_err_field_function_reassigned|borrows .* but is used here after it may be released or overwritten" - "own_err_generator_outlives_arg|borrows .* but is used here after .* is released or overwritten" - "own_err_generator_bounded_escapes|borrows its generator's arguments and cannot be stored, returned or captured" - "own_err_literal_two_records|is used here after its value was moved" + "own_err_assign_after_move|:12:16: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrow_escape|:9:5: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" + "own_err_move_some_paths_field|:8:9: error: '[^']*' is moved here on some paths only" + "own_err_borrow_and_move|:8:5: error: '[^']*' is moved here while '[^']*' borrows it" + "own_err_borrow_bound_method|:20:17: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_const_bound_method_reassigned|:20:11: error: '[^']*' takes a second reference" + "own_err_borrow_catch_local|:31:20: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrow_finally_local|:30:20: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrow_derived_ref|:28:5: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_const_alias_move|:12:5: error: '[^']*' borrows '[^']*' and cannot be assigned" + "own_err_const_alias_field|:15:5: error: '[^']*' is moved here while '[^']*' borrows it" + "own_err_field_alias|:9:11: error: '[^']*' is used here after its value was moved" + "own_err_call_in_try_drops|:30:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_closure_escape_used|:9:11: error: '[^']*' is captured by a closure that escapes, and is still used here" + "own_err_closure_escape_twice|:7:19: error: '[^']*' is captured by a closure that escapes, and is still used here" + "own_err_closure_escape_param|:7:12: error: a closure that escapes owns what it captures, but -mm=own cannot move this variable into it: its value is the caller's" + "own_err_closure_escape_inherited|:9:21: error: a closure that escapes owns what it captures, but -mm=own cannot move this variable into it: it is captured from an enclosing function" + "own_err_closure_escape_loop|:11:23: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_closure_escape_some_paths|:12:23: error: '[^']*' is moved here on some paths only" + "own_err_closure_outlives_cell|:13:11: error: '[^']*' borrows '[^']*', which it captures, but runs here after '[^']*' is released" + "own_err_closure_copy_moved|:13:11: error: '[^']*' borrows '[^']*', which it captures, but runs here after '[^']*' is released" + "own_err_closure_param_assigned|:9:5: error: '[^']*' is a captured parameter and cannot be assigned under -mm=own: its value is the caller's" + "own_err_closure_assigns_param|:8:15: error: '[^']*' is a captured parameter and cannot be assigned under -mm=own: its value is the caller's" + "own_err_cell_assigned_in_frame|:16:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_cell_assigned_in_closure|:18:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_closure_assigns_captured_field|:19:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_unknown_producer|:5:11: error: '[^']*' takes a second reference" + "own_err_interface_two_views|:20:11: error: '[^']*' takes a second reference" + "own_err_field_function_reassigned|:7:17: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_generator_outlives_arg|:12:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_generator_bounded_escapes|:8:17: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured" + "own_err_literal_two_records|:11:16: error: '[^']*' is used here after its value was moved" "own_err_generator_yields_borrow|borrows the generator's argument and cannot be stored, returned or captured" - "own_err_generator_map_outlives|borrows .* but is used here after .* is released or overwritten" - "own_err_generator_place_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_two_owners|is used here after its value was moved" - "own_err_reassign_some_paths|is moved here on some paths only" - "own_err_push_let|is used here after its value was moved" - "own_err_push_loop|is moved inside a loop" - "own_err_field_loop|is moved inside a loop" - "own_err_union_moved|is used here after its value was moved" - "own_err_field_read_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_element_read_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_element_read_popped|borrows .* but is used here after it may be released or overwritten" - "own_err_element_read_loop_pop|borrows .* but is used here after it may be released or overwritten" - "own_err_field_read_owner_assigned|borrows .* but is used here after it may be released or overwritten" - "own_err_field_read_owner_moved|borrows .* but is used here after it may be released or overwritten" - "own_err_field_read_call|borrows .* but is used here after it may be released or overwritten" - "own_err_param_read_call|borrows .* but is used here after it may be released or overwritten" - "own_err_global_read_assigned|borrows .* but is used here after it may be released or overwritten" - "own_err_merge_read_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_alias_read_popped|borrows .* but is used here after it may be released or overwritten" - "own_err_alias_read_stale|borrows .* but is used here after it may be released or overwritten" - "own_err_merge_root_assigned|borrows .* but is used here after it may be released or overwritten" - "own_err_container_read_stored|borrows .* and cannot be stored, returned or captured" - "own_err_field_let_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_field_let_assigned|borrows .* and cannot be assigned" - "own_err_call_drops_indirectly|borrows .* but is used here after it may be released or overwritten" - "own_err_call_given_borrow|borrows .* but is used here after it may be released or overwritten" - "own_err_call_this_borrow|borrows .* but is used here after it may be released or overwritten" - "own_err_declared_drops|borrows .* but is used here after it may be released or overwritten" - "own_err_exported_virtual_call|borrows .* but is used here after it may be released or overwritten" + "own_err_generator_map_outlives|:7:11: error: '[^']*' borrows 'the generator's source' but is used here after 'the generator's source' is released or overwritten" + "own_err_generator_place_overwritten|:15:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_two_owners|:8:13: error: '[^']*' is used here after its value was moved" + "own_err_reassign_some_paths|:10:5: error: '[^']*' is moved here on some paths only" + "own_err_push_let|:7:9: error: '[^']*' is used here after its value was moved" + "own_err_push_loop|:6:33: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_field_loop|:10:9: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_union_moved|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_field_read_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_element_read_overwritten|:22:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_element_read_popped|:26:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_element_read_loop_pop|:25:15: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_field_read_owner_assigned|:27:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_field_read_owner_moved|:33:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_field_read_call|:31:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_param_read_call|:33:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_global_read_assigned|:29:11: error: '[^']*' borrows 'g[.]c' but is used here after it may be released or overwritten" + "own_err_merge_read_overwritten|:28:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_alias_read_popped|:28:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_alias_read_stale|:31:19: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_merge_root_assigned|:36:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_container_read_stored|:26:5: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_field_let_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_field_let_assigned|:26:5: error: '[^']*' borrows a field and cannot be assigned" + "own_err_call_drops_indirectly|:24:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_call_given_borrow|:21:5: error: '[^']*' borrows 'g[.]c' but is used here after it may be released or overwritten" + "own_err_call_this_borrow|:19:5: error: '[^']*' borrows 'g[.]c' but is used here after it may be released or overwritten" + "own_err_declared_drops|:15:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_exported_virtual_call|:11:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_borrowed_result_stored|borrows the result of 'firstOf' and cannot be stored, returned or captured" - "own_err_borrowed_result_outlives|borrows the result of 'firstOf' but is used here after it may be released or overwritten" + "own_err_borrowed_result_outlives|:19:11: error: '[^']*' borrows the result of 'firstOf' but is used here after it may be released or overwritten" "own_err_borrowed_result_let_outlives|borrows the result of 'H.first' but is used here after it may be released or overwritten" "own_err_borrowed_result_escapes|could return a borrow or keep a parameter, but it is used other than by a call" "own_err_borrowed_result_family|an override in its class family disagrees" - "own_err_param_kept_used|is used here after its value was moved" - "own_err_any_unboxed_outlives|borrows the result of '___unbox<.*>' but is used here after it may be released or overwritten" - "own_err_param_kept_some_paths|is moved here on some paths only" - "own_err_param_kept_loop|is moved inside a loop" + "own_err_param_kept_used|:19:11: error: '[^']*' is used here after its value was moved" + "own_err_any_unboxed_outlives|:11:11: error: '[^']*' borrows the result of '___unbox>>' but is used here after it may be released or overwritten" + "own_err_param_kept_some_paths|:13:9: error: '[^']*' is moved here on some paths only" + "own_err_param_kept_loop|:19:9: error: '[^']*' is moved inside a loop but was made outside it" "own_err_param_kept_not_owned|is given to 'keep', which keeps it, but -mm=own cannot move it here" "own_err_delete|'delete' is not supported by -mm=own yet" - "own_err_mixed_return|is used here after its value was moved" - "own_err_mixed_return_join_use|is moved here on some paths only" + "own_err_mixed_return|:13:28: error: '[^']*' is used here after its value was moved" + "own_err_mixed_return_join_use|:14:15: error: '[^']*' is moved here on some paths only" "own_err_callback_escapes|cannot make the callers of 'pick' give it up" "own_err_callback_disagrees|a function passed in its place disagrees" - "own_err_callback_promoted_used|is used here after its value was moved" - "own_err_kept_param_let_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_kept_param_field_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_kept_and_borrowed_arg|is used here after its value was moved" - "own_err_kept_and_borrowed_arg_slot|is used here after its value was moved" - "own_err_kept_and_borrowed_arg_field|is used here after its value was moved" - "own_err_kept_twice|is used here after its value was moved" - "own_err_kept_twice_field|is used here after its value was moved" - "own_err_kept_twice_push|is used here after its value was moved" - "own_err_kept_arg_assigned_in_call|is used here after its value was moved" - "own_err_kept_and_borrowed_arg_assigned_in_call|is used here after its value was moved" - "own_err_kept_arg_assigned_on_arm|is used here after its value was moved" - "own_err_kept_arg_assigned_in_and|is used here after its value was moved" - "own_err_call_result_bounded|borrows a field but is used here after it may be released or overwritten" - "own_err_call_result_record|borrows a field but is used here after it may be released or overwritten" + "own_err_callback_promoted_used|:25:26: error: '[^']*' is used here after its value was moved" + "own_err_kept_param_let_overwritten|:30:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_kept_param_field_overwritten|:34:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_kept_and_borrowed_arg|:41:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_and_borrowed_arg_slot|:44:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_and_borrowed_arg_field|:41:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_twice|:38:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_twice_field|:42:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_twice_push|:36:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_arg_assigned_in_call|:39:20: error: '[^']*' is used here after its value was moved" + "own_err_kept_and_borrowed_arg_assigned_in_call|:37:23: error: '[^']*' is used here after its value was moved" + "own_err_kept_arg_assigned_on_arm|:40:29: error: '[^']*' is used here after its value was moved" + "own_err_kept_arg_assigned_in_and|:40:35: error: '[^']*' is used here after its value was moved" + "own_err_call_result_bounded|:43:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_call_result_record|:47:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_param_returned_exported|is a parameter, returned here, but -mm=own cannot tell the callers of 'identity' that the result borrows it" "own_err_param_kept_exported|is a parameter, kept here, but -mm=own cannot make the callers of 'Animal.constructor' give it up" - "own_err_param_assigned|is a parameter and cannot be assigned under -mm=own yet" - "own_err_local_owns_nothing|owns nothing under -mm=own .* so it cannot be given a new value here" - "own_err_merged_result|is merged from several branches" + "own_err_param_assigned|:7:5: error: '[^']*' is a parameter and cannot be assigned under -mm=own yet" + "own_err_local_owns_nothing|:8:5: error: '[^']*' owns nothing under -mm=own [(]it is declared without a value, or in a catch or finally clause[)], so it cannot be given a new value here" + "own_err_merged_result|:7:5: error: '[^']*' is merged from several branches [(]`[?]:`, `&&`, `[|][|]`, `[?][?]`, a loop[)]" "own_err_global_borrowed_result|the initializer of global 'c' holds the result of 'child', which borrows its argument" - "own_err_global_borrowed_temporary|the initializer of global 'c' holds the result of 'id', which borrows its argument" + "own_err_global_borrowed_temporary|:11:11: error: the initializer of global 'c' holds the result of 'id', which borrows its argument" "own_err_global_kept_not_fresh|the initializer of global 'n' gives 'N.constructor' a value it keeps, but -mm=own cannot move it there" "own_err_global_returned|borrows the global 'V.def' and cannot be stored, returned or captured" - "own_err_immortal_borrowed_init|borrows 'K.list.*' and cannot be stored, returned or captured" - "own_err_immortal_field_overwritten|borrows .* but is used here after .* released or overwritten" + "own_err_immortal_borrowed_init|:9:5: error: '[^']*' borrows 'K[.]list\\[\\]' and cannot be stored, returned or captured" + "own_err_immortal_field_overwritten|:15:11: error: '[^']*' borrows 'K[.]n[.]c' but is used here after it may be released or overwritten" "own_err_immortal_region_borrow|borrows the global 'c' and cannot be stored, returned or captured" - "own_err_immortal_region_call|the initializer of global 'c' holds the result of 'id', which borrows its argument" - "own_err_global_loop_store_static|borrows the global 'K[.]w' and cannot be stored, returned or captured" - "own_err_global_loop_store_script|borrows the global 'K[.]w' and cannot be stored, returned or captured" - "own_err_global_const_store_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_const_store_script|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_const_store|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_record_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_record|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_let_holder_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_let_destructure|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_let_holder|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_outer_holder|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_outer_holder_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_uninit_let|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_uninit_let_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_argument_assigned|a value read out of the global 'G' is lent to a call that may release or overwrite it" - "own_err_global_method_assigns|a value read out of the global 'G' is lent to a call that may release or overwrite it" - "own_err_global_argument_transitive|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_immortal_region_call|:10:11: error: the initializer of global 'c' holds the result of 'id', which borrows its argument" + "own_err_global_loop_store_static|:18:5: error: '[^']*' borrows the global 'K[.]w' and cannot be stored, returned or captured" + "own_err_global_loop_store_script|:16:5: error: '[^']*' borrows the global 'K[.]w' and cannot be stored, returned or captured" + "own_err_global_const_store_static|:21:28: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_const_store_script|:21:20: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_const_store|:19:16: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_record_static|:30:23: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_record|:26:11: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_let_holder_static|:30:28: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_let_destructure|:30:20: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_let_holder|:27:16: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_outer_holder|:32:15: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_outer_holder_static|:33:23: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_uninit_let|:28:19: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_uninit_let_static|:28:23: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_argument_assigned|:19:5: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_method_assigns|:15:11: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_argument_transitive|:23:5: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" "own_err_global_read_callee_assigns|borrows 'G' but is used here after 'G' is released or overwritten" "own_err_global_read_callee_assigns_transitive|borrows 'G' but is used here after 'G' is released or overwritten" "own_err_argument_assigned|is lent to 'g', but is given a new value before the call takes it" "own_err_argument_assigned_conditional|is lent to 'g', but is given a new value before the call takes it" "own_err_argument_assigned_closure|is lent to 'g', but is given a new value before the call takes it" - "own_err_global_method_transitive|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_method_transitive|:19:11: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" "own_err_global_this_field_move|a value read out of the global 'GK' is lent to a call that may release or overwrite it" - "own_err_global_argument_field_let|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_argument_field_let|:21:5: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" "own_err_global_kept_field|a value read out of the global 'g' is lent to a call that may release or overwrite it" "own_err_global_let_call|borrows the global 'names', but -mm=own cannot prove the global keeps its value" - "own_err_global_from_global|the initializer of global 'k' reads a value another owner keeps" - "own_err_global_from_field|the initializer of global 'k' reads a value another owner keeps" - "own_err_captured_returned|borrows a captured variable and cannot be stored, returned or captured" - "own_err_literal_moved_twice|is used here after its value was moved" + "own_err_global_from_global|:8:5: error: the initializer of global 'k' reads a value another owner keeps" + "own_err_global_from_field|:12:7: error: the initializer of global 'k' reads a value another owner keeps" + "own_err_global_kept_typeof|:15:9: error: the value read from 'M[.]w' is kept where its end at an assignment of 'M[.]w' cannot be checked" + "own_err_record_literal_nested|:8:24: error: an object literal that holds a value it owns is moved only into a local or returned as it is" + "own_err_record_literal_let_field|:10:11: error: an object literal that holds a value it owns is moved only into a local or returned as it is" + "own_err_captured_returned|:6:9: error: '[^']*' borrows a captured variable and cannot be stored, returned or captured" + "own_err_literal_moved_twice|:14:16: error: '[^']*' is used here after its value was moved" "own_err_interface_keeps_literal|is a parameter, kept here, but -mm=own cannot make the callers of 'KS.take' give it up" "own_err_interface_keeps_param|is a parameter, kept here, but -mm=own cannot make the callers of 'KV.take' give it up" - "own_err_interface_borrow_param|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.get' that the result borrows it" - "own_err_interface_borrow_element|borrows an element and cannot be stored, returned or captured" - "own_err_interface_borrow_field|borrows a field and cannot be stored, returned or captured" - "own_err_interface_exported_impl|borrows .* but is used here after it may be released or overwritten" + "own_err_interface_borrow_param|:9:44: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]get' that the result borrows it" + "own_err_interface_borrow_element|:9:66: error: '[^']*' borrows an element and cannot be stored, returned or captured" + "own_err_interface_borrow_field|:9:59: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_interface_exported_impl|:15:36: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_interface_family_disagrees|is a parameter, returned here, but -mm=own cannot tell the callers of 'GA.get' that the result borrows it" - "own_err_interface_member_drops|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_from_interface|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.get' that the result borrows it" + "own_err_interface_member_drops|:13:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_from_interface|:9:65: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]get' that the result borrows it" "own_err_interface_result_outlives|borrows the result of 'First.first' but is used here after it may be released or overwritten" - "own_err_interface_fn_field_drops|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_fn_field_unknown|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_fn_field_exported|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_fn_any_param|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.take' that the result borrows it" - "own_err_interface_fn_any_result|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.take' that the result borrows it" - "own_err_interface_fn_any_drops|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_public_exported|is a parameter, returned here, but -mm=own cannot tell the callers of 'PA.pick' that the result borrows it" - "own_err_interface_public_reachable|is a parameter, returned here, but -mm=own cannot tell the callers of 'PA.pick' that the result borrows it" - "own_err_interface_member_types|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_member_methods|borrows a field but is used here after it may be released or overwritten" - "own_err_declared_callback|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_global|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_method|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_tuple|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_indirect|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_wrapper|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_class_method|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_interface_field|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_stored_global|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_stored_reference|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_cpp_class|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_field_reference|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_intrinsic|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_interface_member_class|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_member_any|borrows a field but is used here after it may be released or overwritten" - "own_err_override_nameable_base|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" - "own_err_override_imported_base|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" - "own_err_override_imported_class|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" - "own_err_string_kept_param_known|is used here after its value was moved" + "own_err_interface_fn_field_drops|:12:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_field_unknown|:8:27: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_field_exported|:6:27: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_any_param|:9:41: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]take' that the result borrows it" + "own_err_interface_fn_any_result|:9:41: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]take' that the result borrows it" + "own_err_interface_fn_any_drops|:11:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_public_exported|:9:58: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'PA[.]pick' that the result borrows it" + "own_err_interface_public_reachable|:8:35: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'PA[.]pick' that the result borrows it" + "own_err_interface_member_types|:12:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_member_methods|:13:90: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_declared_callback|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_global|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_method|:10:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_tuple|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_indirect|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_wrapper|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_class_method|:10:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_interface_field|:15:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_stored_global|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_stored_reference|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_cpp_class|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_field_reference|:13:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_intrinsic|:13:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_interface_member_class|:12:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_member_any|:10:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_override_nameable_base|:6:63: error: '[^']*' is a parameter, kept here, but -mm=own cannot make the callers of 'Der[.]keep' give it up" + "own_err_override_imported_base|:5:63: error: '[^']*' is a parameter, kept here, but -mm=own cannot make the callers of 'Der[.]keep' give it up" + "own_err_override_imported_class|:5:60: error: '[^']*' is a parameter, kept here, but -mm=own cannot make the callers of 'Der[.]keep' give it up" + "own_err_string_kept_param_known|:12:11: error: '[^']*' is used here after its value was moved" "own_err_keeps_mismatch|'@keeps' of 'Pair.constructor' names 'a', but its body keeps 'b'" "own_err_borrows_not_generator|'@borrows' of 'copyOf' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_borrows_mixed_return|'@borrows' of 'pick' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" @@ -4073,53 +4076,53 @@ set(own_error_cases "own_err_lends_and_borrows|'firstOf' cannot say both '@lends' and '@borrows'" "own_err_lends_two_params|'@lends' of 'pick' names 'a', 'b', but its body returns a borrow of 'a'" "own_err_lends_index_64|'@lends' of 'K[.]pick' names its argument 64, but a method's '@lends' can name only its arguments 0 to 63" - "own_err_lends_outlives|borrows the result of 'firstOf' but is used here after it may be released or overwritten" + "own_err_lends_outlives|:20:11: error: '[^']*' borrows the result of 'firstOf' but is used here after it may be released or overwritten" "own_err_generator_method_this|'this value' is a parameter, kept here, but -mm=own cannot make the callers of 'S.chars' give it up" - "own_err_string_call_keeps_twice|borrows a field and cannot be stored, returned or captured" - "own_err_string_pushed_twice|is used here after its value was moved" - "own_err_shared_borrow_write|but is used here after it may be released or overwritten" - "own_err_shared_borrow_call|but is used here after it may be released or overwritten" - "own_err_shared_borrow_push|but is used here after it may be released or overwritten" - "own_err_shared_moved_in|is used here after its value was moved" - "own_err_shared_borrow_other_type|but is used here after it may be released or overwritten" - "own_err_shared_borrow_push_call|but is used here after it may be released or overwritten" - "own_err_shared_borrow_reassigned|but is used here after it may be released or overwritten" + "own_err_string_call_keeps_twice|:19:5: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_string_pushed_twice|:9:5: error: '[^']*' is used here after its value was moved" + "own_err_shared_borrow_write|:12:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_call|:19:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_push|:17:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_shared_moved_in|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_shared_borrow_other_type|:29:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_push_call|:20:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_reassigned|:11:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_shared_union_owned|a Shared in a union with 'string', which -mm=own does not count, is not supported" - "own_err_shared_place_arg|borrows a field but is used here after it may be released or overwritten" - "own_err_shared_place_two_uses|borrows a field but is used here after it may be released or overwritten" - "own_err_shared_place_literal|borrows a field and cannot be stored, returned or captured" - "own_err_shared_place_tuple|borrows a field and cannot be stored, returned or captured" + "own_err_shared_place_arg|:29:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_place_two_uses|:32:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_place_literal|:31:15: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_shared_place_tuple|:31:39: error: '[^']*' borrows a field and cannot be stored, returned or captured" "own_err_shared_literal_borrowed|the initializer of global 'b' takes a second reference" - "own_err_iface_field_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_iface_field_view_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_iface_field_call|borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_overwritten|:23:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_view_overwritten|:26:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_call|:27:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_iface_field_result|borrows the result of 'dirOf' but is used here after it may be released or overwritten" - "own_err_read_after_move_overwritten|is used here after its value was moved" - "own_err_read_after_move_view|is used here after its value was moved" - "own_err_read_after_move_call|is used here after its value was moved" - "own_err_read_after_move_loop|is used here after its value was moved" - "own_err_read_after_move_after_loop|is used here after its value was moved" - "own_err_read_after_move_global|is used here after its value was moved" - "own_err_field_let_global_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_conditional_move_read|is used here after its value was moved" - "own_err_conditional_move_outer|is moved inside a loop but was made outside it" - "own_err_conditional_move_read_after_join|is moved here on some paths only" - "own_err_conditional_move_field_borrow|is moved here on some paths only" - "own_err_conditional_move_catch|is moved here on some paths only" - "own_err_conditional_move_bound_method|is moved here on some paths only" - "own_err_conditional_move_view|is moved here on some paths only" - "own_err_conditional_move_old_view|is moved here on some paths only" - "own_err_borrowing_record_stored|borrows the result of .* and cannot be stored, returned or captured" - "own_err_borrowing_record_outlives|borrows the result of .* but is used here after it may be released or overwritten" - "own_err_borrowing_record_kept_arg|borrows a field and cannot be stored, returned or captured" - "own_err_borrowing_record_let_outlives|borrows '.*' but is used here after '.*' is released or overwritten" - "own_err_borrowing_record_returned_local|borrows '.*' and cannot be stored, returned or captured" - "own_err_borrowing_record_field_written|borrows a field and cannot be stored, returned or captured" - "own_err_borrowing_record_cell|borrows a captured variable but is used here after it may be released or overwritten" - "own_err_borrowing_record_kept_param|borrows '.*' but is used here after it may be released or overwritten" - "own_err_borrowing_record_place_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_borrowing_record_open|borrows '.*' and cannot be stored, returned or captured" - "own_err_borrowing_record_borrowed_again|borrows the result of .* but is used here after it may be released or overwritten") + "own_err_read_after_move_overwritten|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_view|:12:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_call|:11:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_loop|:25:14: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_after_loop|:14:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_global|:42:15: error: '[^']*' is used here after its value was moved" + "own_err_field_let_global_overwritten|:42:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_conditional_move_read|:20:11: error: '[^']*' is used here after its value was moved" + "own_err_conditional_move_outer|:19:13: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_conditional_move_read_after_join|:18:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_field_borrow|:22:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_catch|:24:13: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_bound_method|:43:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_view|:43:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_old_view|:43:9: error: '[^']*' is moved here on some paths only" + "own_err_borrowing_record_stored|:27:5: error: '[^']*' borrows the result of 'Th[.]hit' and cannot be stored, returned or captured" + "own_err_borrowing_record_outlives|:26:11: error: '[^']*' borrows the result of 'A[.]hit' but is used here after it may be released or overwritten" + "own_err_borrowing_record_kept_arg|:24:21: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_borrowing_record_let_outlives|:24:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrowing_record_returned_local|:17:20: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" + "own_err_borrowing_record_field_written|:15:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_borrowing_record_cell|:25:16: error: '[^']*' borrows a captured variable but is used here after it may be released or overwritten" + "own_err_borrowing_record_kept_param|:30:12: error: '[^']*' borrows '[^']*' but is used here after it may be released or overwritten" + "own_err_borrowing_record_place_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_borrowing_record_open|:17:31: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" + "own_err_borrowing_record_borrowed_again|:29:11: error: '[^']*' borrows the result of 'A[.]hit' but is used here after it may be released or overwritten") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_global_kept_typeof.ts b/tslang/test/tester/own/own_err_global_kept_typeof.ts new file mode 100644 index 000000000..534dd138b --- /dev/null +++ b/tslang/test/tester/own/own_err_global_kept_typeof.ts @@ -0,0 +1,21 @@ +// -mm=own rejects: `k.w = M.w` keeps the read of `M.w` in whatever static the vtable slot of `k` +// holds, so no assignment of `M.w` can be seen ending it (spec 25). `M.w` is mortal: it is written +// through the vtable. Only checkGlobalRead's keep backstop catches this shape; every other rule +// lets it through. +class V { + constructor(public x: number) {} +} + +class M { + static w = new V(2); +} + +class D { + set(k: typeof M) { + k.w = M.w; + } +} + +function main() { + print(M.w.x); +} diff --git a/tslang/test/tester/own/own_err_record_literal_let_field.ts b/tslang/test/tester/own/own_err_record_literal_let_field.ts new file mode 100644 index 000000000..d08f6e292 --- /dev/null +++ b/tslang/test/tester/own/own_err_record_literal_let_field.ts @@ -0,0 +1,14 @@ +// -mm=own rejects (spec 21.2): `r` holds the value of the `let` `v`, which `v` owns, and is not seen as +// an interface (where it would be a borrowing record, spec 32), so it moves only into a local or the +// result, not into the array. +class V { + constructor(public x: number) {} +} + +function main() { + let v = new V(1); + const r = { v: v }; + const a: { v: V }[] = []; + a.push(r); + print(a[0].v.x); +} diff --git a/tslang/test/tester/own/own_err_record_literal_nested.ts b/tslang/test/tester/own/own_err_record_literal_nested.ts new file mode 100644 index 000000000..5097786e1 --- /dev/null +++ b/tslang/test/tester/own/own_err_record_literal_nested.ts @@ -0,0 +1,12 @@ +// -mm=own rejects (spec 21.2): `r` holds a literal nested by value, which it does not own outright, so +// it moves only into a local or the result, not into the array. `{ v: new V(1) }` alone moves. +class V { + constructor(public x: number) {} +} + +function main() { + const r = { inner: { v: new V(1) } }; + const a: { inner: { v: V } }[] = []; + a.push(r); + print(a[0].inner.v.x); +} From cf8d32eb78231e263e611bd4d66d3cbf44207ae6 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:11:32 +0100 Subject: [PATCH 02/25] own passes: the @lends index >= 64 error names the parameter (M4) "'@lends' of 'K.pick' names its argument 64" counted the receiver, which the user never wrote; it now names the parameter as `@lends` did, from the function's declared parameter names: "'@lends' of 'K.pick' names 'p63', but a method's '@lends' can name only its first 64 arguments, the receiver among them". Without the names it falls back to the argument number. Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 5 +++-- tslang/lib/TypeScript/MLIRGenImpl.h | 18 ++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 35c6e1e49..0e738b468 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -3945,8 +3945,9 @@ is gone; its shape is in `own_borrowing_record_arg`. `own_err_lends_two_params` of 'pick' names 'a', 'b', but its body returns a borrow of 'a'`. A method's `@lends` reaches its class's declaration as a 64-bit mask, so since the final review (M4, 2026-10-23) an index past 63 is an error rather than a borrow an importer silently never learns of: `own_err_lends_index_64` (a -method of 64 parameters lending its last, argument 64), "'@lends' of 'K.pick' names its argument -64, but a method's '@lends' can name only its arguments 0 to 63". The compiler before it accepted +method of 64 parameters lending its last, argument 64), "'@lends' of 'K.pick' names 'p63', but a +method's '@lends' can name only its first 64 arguments, the receiver among them" (the parameter by +its name since the follow-up; it read "names its argument 64" before). The compiler before it accepted the program; the 63rd parameter (argument 63) is still accepted, under own, rc and gc. Teeth (temporary `OWN_TEETH` switches, removed): with the per-site routine dropped (every record's diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index ce82cca98..09233e7e1 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -12723,8 +12723,22 @@ class MLIRGenImpl // 64 bits: an index past it would be a borrow an importer never learns of if (index >= 64) { - emitError(location) << "'@lends' of '" << funcOp.getSymName() << "' names its argument " << index - << ", but a method's '@lends' can name only its arguments 0 to 63"; + // the parameter by the name `@lends` gave it (processOwnParamMarkers) + auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); + auto name = names && index < static_cast(names.size()) + ? mlir::dyn_cast(names[index]) + : mlir::StringAttr(); + auto diag = emitError(location) << "'@lends' of '" << funcOp.getSymName() << "' names "; + if (name) + { + diag << "'" << name.getValue() << "'"; + } + else + { + diag << "its argument " << index; + } + + diag << ", but a method's '@lends' can name only its first 64 arguments, the receiver among them"; return false; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 50d8b4fe2..6f840bb26 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -4075,7 +4075,7 @@ set(own_error_cases "own_err_lends_not_borrow|'@lends' of 'made' names 'l', but its body does not return a borrow of it" "own_err_lends_and_borrows|'firstOf' cannot say both '@lends' and '@borrows'" "own_err_lends_two_params|'@lends' of 'pick' names 'a', 'b', but its body returns a borrow of 'a'" - "own_err_lends_index_64|'@lends' of 'K[.]pick' names its argument 64, but a method's '@lends' can name only its arguments 0 to 63" + "own_err_lends_index_64|'@lends' of 'K[.]pick' names 'p63', but a method's '@lends' can name only its first 64 arguments, the receiver among them" "own_err_lends_outlives|:20:11: error: '[^']*' borrows the result of 'firstOf' but is used here after it may be released or overwritten" "own_err_generator_method_this|'this value' is a parameter, kept here, but -mm=own cannot make the callers of 'S.chars' give it up" "own_err_string_call_keeps_twice|:19:5: error: '[^']*' borrows a field and cannot be stored, returned or captured" From dfa615bf3ef7e6c04a68deec1c51d5e536fb5f6c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:23:39 +0100 Subject: [PATCH 03/25] own passes: one placeBase for the place walkers, one MLIRTypeHelper constructor (I2) own_facts::placeBase(ref) is now the one definition of a place - a field, an element, a handle's payload, a field through an interface - and of the value it is reached through; isPlace is placeBase's answer. The five walkers that listed the place ops by hand (reachesSharedValue, borrowedParam, chainOf, pathOf, reachesOutside) take the base from it and keep their own decision per kind: a handle's payload is still "reaches outside" / "reaches a shared value", still no parameter's for borrowedParam, still `.value` in a path. A new kind of place is now seen by all of them. The nine `MLIRTypeHelper mth(ctx, CompileOptions{})` constructions go through own_facts::typeHelper(ctx), and the single ownsHeapMemory questions through own_facts::ownsHeap(location, type). MLIRTypeHelper holds only the context and the options, so one per loop as before. Windows full suite 5251/5251. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipFacts.h | 108 ++++++++++++------ .../lib/TypeScript/OwnershipInferencePass.cpp | 81 ++++++------- .../lib/TypeScript/OwnershipSignaturePass.cpp | 33 ++---- 3 files changed, 118 insertions(+), 104 deletions(-) diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 9501ade8a..abb342acf 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -421,13 +421,60 @@ inline bool isInterfaceField(mlir::Value ref) return ref.getDefiningOp() && mlir::isa(ref.getType()); } +// The kinds of place (isPlace), and the value each is reached through (placeBase). +enum class PlaceKind +{ + None, + Field, // `o.f`: the object + Element, // `a[i]`: the array + SharedValue, // `s.value`, a handle's payload (spec 23.1): the handle + InterfaceField // `i.f`, a field reached through an interface (spec 28): the interface value +}; + +struct PlaceBase +{ + mlir::Value base; + PlaceKind kind = PlaceKind::None; + + explicit operator bool() const + { + return kind != PlaceKind::None; + } +}; + +// The one definition of a place: every walk from a place up to what holds it starts here, so a +// new kind of place is seen by all of them. Each walk decides for itself what a kind means. +inline PlaceBase placeBase(mlir::Value ref) +{ + auto *def = ref.getDefiningOp(); + if (auto propertyRefOp = mlir::dyn_cast_or_null(def)) + { + return {propertyRefOp.getObjectRef(), PlaceKind::Field}; + } + + if (auto elementRefOp = mlir::dyn_cast_or_null(def)) + { + return {elementRefOp.getArray(), PlaceKind::Element}; + } + + if (auto sharedValueRefOp = mlir::dyn_cast_or_null(def)) + { + return {sharedValueRefOp.getShared(), PlaceKind::SharedValue}; + } + + if (auto interfaceRefOp = mlir::dyn_cast_or_null(def); interfaceRefOp && isInterfaceField(ref)) + { + return {interfaceRefOp.getInterfaceVal(), PlaceKind::InterfaceField}; + } + + return {}; +} + // A field, an element, or `s.value` - the payload of a handle (spec 23.1) - and a field reached // through an interface (spec 28). inline bool isPlace(mlir::Value ref) { - return mlir::isa_and_nonnull( - ref.getDefiningOp()) || - isInterfaceField(ref); + return static_cast(placeBase(ref)); } // The storage a reference addresses - a local's `ts.Variable` or a global's `ts.AddressOf` - itself @@ -679,22 +726,14 @@ inline bool reachesSharedValue(mlir::Value ref) continue; } - if (value.getDefiningOp()) + if (auto place = placeBase(value)) { - return true; - } + if (place.kind == PlaceKind::SharedValue) + { + return true; + } - if (auto propertyRefOp = value.getDefiningOp()) - { - work.push_back(propertyRefOp.getObjectRef()); - } - else if (auto elementRefOp = value.getDefiningOp()) - { - work.push_back(elementRefOp.getArray()); - } - else if (auto interfaceRefOp = value.getDefiningOp(); interfaceRefOp && isInterfaceField(value)) - { - work.push_back(interfaceRefOp.getInterfaceVal()); + work.push_back(place.base); } else if (auto loadOp = value.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) { @@ -733,11 +772,21 @@ inline mlir_ts::LoadOp slotLoadOf(mlir::Value value) return loadOp; } +// The ownership passes ask MLIRTypeHelper about types only, under the default options. +inline MLIRTypeHelper typeHelper(mlir::MLIRContext *context) +{ + return MLIRTypeHelper(context, CompileOptions{}); +} + // Does a value of this type own a block that a release would destroy? +inline bool ownsHeap(mlir::Location location, mlir::Type type) +{ + return typeHelper(type.getContext()).ownsHeapMemory(location, type); +} + inline bool ownsHeap(mlir::Value value) { - MLIRTypeHelper mth(value.getContext(), CompileOptions{}); - return mth.ownsHeapMemory(value.getLoc(), value.getType()); + return ownsHeap(value.getLoc(), value.getType()); } // Holds no block: a number, `null` or `undefined` widened, an empty optional, a string literal. @@ -811,8 +860,7 @@ inline bool isCopyableString(mlir::Type type) } // only a union that lowers to one pointer: a tagged union is a struct - MLIRTypeHelper mth(type.getContext(), CompileOptions{}); - return !mth.isUnionTypeNeedsTag(mlir::UnknownLoc::get(type.getContext()), unionType); + return !typeHelper(type.getContext()).isUnionTypeNeedsTag(mlir::UnknownLoc::get(type.getContext()), unionType); } return isText(type); @@ -976,7 +1024,7 @@ inline bool isOwningRecord(mlir::Value value) return false; } - MLIRTypeHelper mth(value.getContext(), CompileOptions{}); + auto mth = typeHelper(value.getContext()); for (auto *user : varOp->getUsers()) { auto propertyRefOp = mlir::dyn_cast(user); @@ -1175,19 +1223,11 @@ inline int borrowedParam(mlir::Value value, int depth = 0) return borrowedParam(unboxOp.getIn(), depth + 1); } - if (auto propertyRefOp = mlir::dyn_cast_or_null(def)) - { - return borrowedParam(propertyRefOp.getObjectRef(), depth + 1); - } - - if (auto elementRefOp = mlir::dyn_cast_or_null(def)) - { - return borrowedParam(elementRefOp.getArray(), depth + 1); - } - - if (auto interfaceRefOp = mlir::dyn_cast_or_null(def); interfaceRefOp && isInterfaceField(value)) + // a field, an element, a field through an interface: the argument's block holds them. A + // handle's payload is any handle's, no parameter's. + if (auto place = placeBase(value); place && place.kind != PlaceKind::SharedValue) { - return borrowedParam(interfaceRefOp.getInterfaceVal(), depth + 1); + return borrowedParam(place.base, depth + 1); } if (auto loadOp = mlir::dyn_cast_or_null(def)) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 0271ab6e5..aaba25a4b 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1783,10 +1783,9 @@ class OwnershipInferencePass } auto name = varName(varOp); - MLIRTypeHelper mth(&getContext(), CompileOptions{}); auto elementType = mlir::cast(value.getType()).getElementType(); // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) - if (!isOwningVariable(varOp) && !closure.borrowsValues && mth.ownsHeapMemory(varOp.getLoc(), elementType) && + if (!isOwningVariable(varOp) && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) && !MLIRTypeHelper::isSharedHandleType(elementType)) { reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); @@ -1870,9 +1869,8 @@ class OwnershipInferencePass return; } - MLIRTypeHelper mth(&getContext(), CompileOptions{}); auto elementType = mlir::cast(cell.getType()).getElementType(); - if (!mth.ownsHeapMemory(varOp.getLoc(), elementType)) + if (!ownsHeap(varOp.getLoc(), elementType)) { return; } @@ -3707,31 +3705,26 @@ class OwnershipInferencePass else { auto ref = refs.pop_back_val(); - mlir::Value base; - if (auto propertyRefOp = ref.getDefiningOp()) + auto place = placeBase(ref); + assert(place && "chainOf walks places only"); + switch (place.kind) { - chain.places.push_back({propertyRefOp.getPosition(), propertyRefOp.getType()}); - base = propertyRefOp.getObjectRef(); - } - else if (auto sharedValueRefOp = ref.getDefiningOp()) - { - chain.places.push_back({Chain::SharedValue, sharedValueRefOp.getType()}); - base = sharedValueRefOp.getShared(); - handleBases.insert(rootOf(base)); - } - else if (auto interfaceRefOp = ref.getDefiningOp()) - { - chain.places.push_back({Chain::InterfaceField, interfaceRefOp.getType()}); - base = interfaceRefOp.getInterfaceVal(); - } - else - { - auto elementRefOp = ref.getDefiningOp(); - chain.places.push_back({-1, elementRefOp.getType()}); - base = elementRefOp.getArray(); + case PlaceKind::Field: + chain.places.push_back({ref.getDefiningOp().getPosition(), ref.getType()}); + break; + case PlaceKind::SharedValue: + chain.places.push_back({Chain::SharedValue, ref.getType()}); + handleBases.insert(rootOf(place.base)); + break; + case PlaceKind::InterfaceField: + chain.places.push_back({Chain::InterfaceField, ref.getType()}); + break; + default: + chain.places.push_back({-1, ref.getType()}); + break; } - bases.push_back(rootOf(base)); + bases.push_back(rootOf(place.base)); } while (!bases.empty()) @@ -4678,46 +4671,41 @@ class OwnershipInferencePass std::string pathOf(mlir::Value ref) { - mlir::Value base; + auto place = placeBase(ref); std::string step; - if (auto propertyRefOp = ref.getDefiningOp()) + switch (place.kind) { - auto field = fieldName(propertyRefOp); + case PlaceKind::Field: { + auto field = fieldName(ref.getDefiningOp()); if (field.empty()) { return {}; } - base = propertyRefOp.getObjectRef(); step = "." + field; + break; } - else if (auto elementRefOp = ref.getDefiningOp()) - { - base = elementRefOp.getArray(); + case PlaceKind::Element: step = "[]"; - } - else if (auto sharedValueRefOp = ref.getDefiningOp()) - { - base = sharedValueRefOp.getShared(); + break; + case PlaceKind::SharedValue: step = ".value"; - } - else if (auto interfaceRefOp = ref.getDefiningOp()) - { - auto field = mlir::dyn_cast(interfaceRefOp.getIdentifier()); + break; + case PlaceKind::InterfaceField: { + auto field = mlir::dyn_cast(ref.getDefiningOp().getIdentifier()); if (!field) { return {}; } - base = interfaceRefOp.getInterfaceVal(); step = "." + field.getValue().str(); + break; } - else - { + case PlaceKind::None: return {}; } - base = rootOf(base); + auto base = rootOf(place.base); std::string prefix; if (isPlace(base)) { @@ -4869,8 +4857,7 @@ class OwnershipInferencePass bool ownsHeap(mlir::Location location, mlir::Type type) { - MLIRTypeHelper mth(&getContext(), CompileOptions{}); - return mth.ownsHeapMemory(location, type); + return own_facts::ownsHeap(location, type); } // A tuple read out of a local that a `ts.Constant` initializes, whose owning fields hold what diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index fd3a6b1e8..6dcbdc9f3 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -222,7 +222,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper> candidates; - MLIRTypeHelper mth(&getContext(), CompileOptions{}); + auto mth = typeHelper(&getContext()); module.walk([&](mlir_ts::GlobalOp globalOp) { if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || @@ -4425,7 +4425,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper borrowed; - MLIRTypeHelper mth(&getContext(), CompileOptions{}); + auto mth = typeHelper(&getContext()); for (auto *user : varOp->getUsers()) { auto propertyRefOp = mlir::dyn_cast(user); @@ -5205,8 +5205,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper()) + if (auto place = placeBase(value)) { - return true; - } - - if (auto propertyRefOp = value.getDefiningOp()) - { - work.push_back(propertyRefOp.getObjectRef()); - continue; - } - - if (auto elementRefOp = value.getDefiningOp()) - { - work.push_back(elementRefOp.getArray()); - continue; - } + // a handle's payload: any other handle may reach it (spec 23.3) + if (place.kind == PlaceKind::SharedValue) + { + return true; + } - if (auto interfaceRefOp = value.getDefiningOp(); interfaceRefOp && isInterfaceField(value)) - { - work.push_back(interfaceRefOp.getInterfaceVal()); + work.push_back(place.base); continue; } From eccfb277fe595c35e8e97481afcc522b239318c5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:28:56 +0100 Subject: [PATCH 04/25] own passes: OwnershipInferencePass::analyze in named steps (I1) analyze (391 lines) keeps its order and reads as the steps it runs: resetFunctionState, the classifications, collectOwnershipOps (the walk, now filling a FunctionOps of the candidates, retains, place and global reads and keeping calls it declared as locals), checkOwnershipOps (the owner checks; the proven values), decideRetains, giveKeptArguments, the decide/check calls, the erase, and stripFacts. The bodies are moved as they were; SlotReceiver moves up beside the Receivers map the steps pass. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 219 +++++++++++------- 1 file changed, 135 insertions(+), 84 deletions(-) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index aaba25a4b..3b4261590 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -119,31 +119,7 @@ class OwnershipInferencePass // it is this call's: nothing after it (the copies, the next run) may read it auto dominanceGone = llvm::make_scope_exit([&]() { dominance = nullptr; }); - // every value some ownership operation names, in the order the walk meets them - llvm::SetVector candidates; - llvm::SmallVector retains; - // reads out of fields and elements, and results that borrow an argument, and what may - // destroy what they read (spec 2.3, 2.4) - llvm::SmallVector placeReads; - // reads of what a mortal global holds (isGlobalRead) - llvm::SmallVector globalReads; - // calls whose callee keeps an argument: each such argument moves into the call - llvm::SmallVector keepingCalls; - drops.clear(); - edgeReleases.clear(); - derivedCache.clear(); - closures.clear(); - closureOf.clear(); - stateBoxes.clear(); - fieldBorrowRetains.clear(); - fieldBorrowReleases.clear(); - captureStores.clear(); - captureRetains.clear(); - returnedParams.clear(); - returnsBorrowOf = resultBorrows(f); - returnsRecordOf = isRecordResult(f) ? resultBorrowsAll(f) : llvm::SmallVector(); - records.clear(); - recordStoreUses.clear(); + resetFunctionState(); // first: whether a closure borrows what it captures changes what its box's stores are classifyClosures(); @@ -167,6 +143,102 @@ class OwnershipInferencePass exitReleases.clear(); }); + FunctionOps ops; + collectOwnershipOps(ops, toErase); + auto proven = checkOwnershipOps(ops, toErase); + + Receivers slotReceivers; + Receivers paramReceivers; + decideRetains(ops, proven, slotReceivers, paramReceivers, toErase); + giveKeptArguments(ops, slotReceivers, paramReceivers); + + for (auto &entry : slotReceivers) + { + decideSlot(entry.first, entry.second, toErase); + } + + for (auto &entry : paramReceivers) + { + decideParam(entry.first, entry.second, toErase); + } + + decideClosures(toErase); + decideCells(); + checkBorrowedReads(); + checkBoundedResults(); + checkBorrowingRecords(); + checkRecordResults(); + toErase.insert(fieldBorrowReleases.begin(), fieldBorrowReleases.end()); + + if (dryRun) + { + return failures == 0; + } + + // a handle's retains and releases are rc's, and stay (spec 23.2) + toErase.remove_if([](mlir::Operation *op) { return touchesHandle(op); }); + placeEdgeReleases(); + for (auto *op : toErase) + { + op->erase(); + } + + stripFacts(); + return true; + } + + // Each analysis starts from nothing this function has not set up again. + void resetFunctionState() + { + auto f = getFunction(); + drops.clear(); + edgeReleases.clear(); + derivedCache.clear(); + closures.clear(); + closureOf.clear(); + stateBoxes.clear(); + fieldBorrowRetains.clear(); + fieldBorrowReleases.clear(); + captureStores.clear(); + captureRetains.clear(); + returnedParams.clear(); + returnsBorrowOf = resultBorrows(f); + returnsRecordOf = isRecordResult(f) ? resultBorrowsAll(f) : llvm::SmallVector(); + records.clear(); + recordStoreUses.clear(); + } + + // What the ownership operations of one function name, in the order the walk meets them + // (collectOwnershipOps). + struct FunctionOps + { + // every value some ownership operation names, in the order the walk meets them + llvm::SetVector candidates; + llvm::SmallVector retains; + // reads out of fields and elements, and results that borrow an argument, and what may + // destroy what they read (spec 2.3, 2.4) + llvm::SmallVector placeReads; + // reads of what a mortal global holds (isGlobalRead) + llvm::SmallVector globalReads; + // calls whose callee keeps an argument: each such argument moves into the call + llvm::SmallVector keepingCalls; + }; + + struct SlotReceiver + { + mlir::Operation *retain; + mlir::Value value; + mlir_ts::LoadOp load; + }; + + // the receivers of each owning local, or of each parameter this function keeps + using Receivers = llvm::MapVector>; + + // Every op of the function that names an ownership: the values to check, the retains to + // decide, the reads out of places and globals, the calls that keep an argument, and the drops. + void collectOwnershipOps(FunctionOps &ops, llvm::SetVector &toErase) + { + auto f = getFunction(); // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { // A handle put into a union with a member that owns a block of its own: counting the @@ -184,22 +256,22 @@ class OwnershipInferencePass if (auto retainOp = mlir::dyn_cast(op)) { - retains.push_back(op); - candidates.insert(rootOf(retainOp.getReference())); + ops.retains.push_back(op); + ops.candidates.insert(rootOf(retainOp.getReference())); } else if (auto retainSlotOp = mlir::dyn_cast(op)) { - retains.push_back(op); + ops.retains.push_back(op); } else if (auto releaseOp = mlir::dyn_cast(op)) { - candidates.insert(rootOf(releaseOp.getReference())); + ops.candidates.insert(rootOf(releaseOp.getReference())); } else if (auto varOp = mlir::dyn_cast(op)) { if (varOp.getInitializer() && isOwningVariable(varOp) && !isHandle(varOp.getInitializer())) { - candidates.insert(rootOf(varOp.getInitializer())); + ops.candidates.insert(rootOf(varOp.getInitializer())); } } else if (mlir::isa(op)) @@ -225,11 +297,11 @@ class OwnershipInferencePass // the value out: fresh, not a borrow of the state if (placeReadOf(loadOp.getResult()) && !loadOp->hasAttr(OWN_FRESH_RESULT_ATTR_NAME)) { - placeReads.push_back(loadOp); + ops.placeReads.push_back(loadOp); } else if (isGlobalRead(loadOp)) { - globalReads.push_back(loadOp); + ops.globalReads.push_back(loadOp); } } else if (auto releaseSlotOp = mlir::dyn_cast(op); releaseSlotOp && isYieldedField(releaseSlotOp.getSlot())) @@ -263,12 +335,12 @@ class OwnershipInferencePass drops.push_back(op); if (op->getNumResults() == 1 && placeReadOf(op->getResult(0)) == op) { - placeReads.push_back(op); + ops.placeReads.push_back(op); } if (!ownedParams(op).empty()) { - keepingCalls.push_back(op); + ops.keepingCalls.push_back(op); } } @@ -276,16 +348,21 @@ class OwnershipInferencePass { if (!isView(op) && isFresh(result) && ownsHeap(result) && !isHandle(result)) { - candidates.insert(result); + ops.candidates.insert(result); } } }); + } + // The owner check of each value, then of each read out of a place and of a mortal global. The + // fresh values proven, whose retains go. + llvm::DenseSet checkOwnershipOps(const FunctionOps &ops, llvm::SetVector &toErase) + { // Every value gets the owner check, fresh or not: rc can hand a value it did not make to // two consuming declarations without a single retain (the result of an indirect call // taken by `let b = a; let c = a;`). Only a fresh value's retains can be erased. llvm::DenseSet proven; - for (auto value : candidates) + for (auto value : ops.candidates) { if (checkValueMoves(value, toErase) && isFresh(value)) { @@ -293,26 +370,32 @@ class OwnershipInferencePass } } - for (auto read : placeReads) + for (auto read : ops.placeReads) { checkPlaceRead(read, toErase); } - for (auto read : globalReads) + for (auto read : ops.globalReads) { checkGlobalRead(read); } checkArgumentsAssigned(); + return proven; + } + + // Each retain: erased, given to the receivers of an owning local or of a kept parameter, or + // reported. + void decideRetains(const FunctionOps &ops, const llvm::DenseSet &proven, Receivers &slotReceivers, + Receivers ¶mReceivers, llvm::SetVector &toErase) + { // What is proven loses its retains; any other retain is a second reference nobody // proved safe - of a loaded value, a parameter, an unknown producer, or of a fresh value // whose own check was reported above. // Receivers of each owning local are decided together: one may move it, but where there // are several, each borrows it, since a borrow pins its owner. - llvm::MapVector> slotReceivers; - llvm::MapVector> paramReceivers; - for (auto *op : retains) + for (auto *op : ops.retains) { if (touchesHandle(op)) { @@ -421,12 +504,17 @@ class OwnershipInferencePass reportSecondReference(op); } } + } + // The arguments a callee keeps, given to the receivers of the local or kept parameter they come + // from. + void giveKeptArguments(const FunctionOps &ops, Receivers &slotReceivers, Receivers ¶mReceivers) + { // An argument a callee keeps moves into the call (spec 2.4): out of an owning local or a // parameter this function keeps, like any other taker; a fresh value is decided with the // other takers of it, a read out of a container by its own check. Anything else is not // this function's to give. - for (auto *call : keepingCalls) + for (auto *call : ops.keepingCalls) { auto args = callArgs(call); for (auto index : ownedParams(call)) @@ -454,41 +542,13 @@ class OwnershipInferencePass } } } + } - for (auto &entry : slotReceivers) - { - decideSlot(entry.first, entry.second, toErase); - } - - for (auto &entry : paramReceivers) - { - decideParam(entry.first, entry.second, toErase); - } - - decideClosures(toErase); - decideCells(); - checkBorrowedReads(); - checkBoundedResults(); - checkBorrowingRecords(); - checkRecordResults(); - toErase.insert(fieldBorrowReleases.begin(), fieldBorrowReleases.end()); - - if (dryRun) - { - return failures == 0; - } - - // a handle's retains and releases are rc's, and stay (spec 23.2) - toErase.remove_if([](mlir::Operation *op) { return touchesHandle(op); }); - placeEdgeReleases(); - for (auto *op : toErase) - { - op->erase(); - } - - // the facts were for this pass only + // The facts were for this pass only. + void stripFacts() + { returnedParams.clear(); - f.walk([](mlir::Operation *op) { + getFunction().walk([](mlir::Operation *op) { for (auto *name : {OWN_PARAMS_ATTR_NAME, OWN_PARAMS_RELEASED_ATTR_NAME, OWN_RESULT_BORROWS_ATTR_NAME, OWN_RESULT_RECORD_ATTR_NAME, OWN_NO_DROPS_ATTR_NAME, OWN_FACTS_LOST_ATTR_NAME, OWN_FRESH_RESULT_ATTR_NAME, @@ -499,8 +559,6 @@ class OwnershipInferencePass op->removeAttr(name); } }); - - return true; } private: @@ -2034,13 +2092,6 @@ class OwnershipInferencePass return false; } - struct SlotReceiver - { - mlir::Operation *retain; - mlir::Value value; - mlir_ts::LoadOp load; - }; - template bool quietly(F check) { ++quiet; From 1541ae3ccc9343cc6e3407688dc7c524ea4e1794 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:33:13 +0100 Subject: [PATCH 05/25] own passes: placeOnUnmovedEdges in named steps (I1) placeOnUnmovedEdges (340 lines) keeps its order and its exits and reads as its steps: slotReleasesInBody (the slot's releases, all in the body), slotStates (the slot's state at each block's entry and exit and before each op - the exit map is the old exitOf, folded once over the same ops), the releases a moved value reaches, unmovedEdges (the releases placed on the edges into the join), and noUseWhereJoined (the taint walk; it still sets joinedUse). The bodies are moved as they were. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 131 +++++++++++++----- 1 file changed, 97 insertions(+), 34 deletions(-) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 3b4261590..ec5977112 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -6148,11 +6148,67 @@ class OwnershipInferencePass // and has not moved it, and erases every release a moved value reaches - when nothing uses the // value, through the slot or through anything read out of it, where the paths have joined. False, // with nothing placed or erased, when it cannot. + // The slot of an owning local through the function (spec 30): its state at the entry and the exit + // of each block, and before each op of the body. + struct SlotStates + { + llvm::DenseMap entry; + llvm::DenseMap exit; + llvm::DenseMap before; + }; + bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, mlir::Operation *taker, llvm::ArrayRef assignments, llvm::SetVector &toErase) { joinedUse = nullptr; + llvm::SmallVector releases; + if (!slotReleasesInBody(varOp, acquire, assignments, releases)) + { + return false; + } + + auto states = slotStates(varOp, acquire, assignments, releases); + + // every release a moved value reaches goes; one some path reaches with nothing in the slot + // is not this rule's + llvm::SmallVector erased; + for (auto *release : releases) + { + auto state = states.before.lookup(release); + if (state & SlotMoved) + { + if (state & SlotDead) + { + return false; + } + + erased.push_back(release); + } + } + + llvm::SmallVector placed; + llvm::SetVector releasedInto; + if (!unmovedEdges(varOp, states, placed, releasedInto)) + { + return false; + } + + if (!noUseWhereJoined(varOp, retain, taker, assignments, states, releasedInto)) + { + return false; + } + + toErase.insert(erased.begin(), erased.end()); + edgeReleases.append(placed.begin(), placed.end()); + return true; + } + + // The releases of the slot, when the local, the move, its assignments and its releases are all in + // the function's body. False when one is not. + bool slotReleasesInBody(mlir_ts::VariableOp varOp, mlir::Operation *acquire, + llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases) + { auto &body = getFunction().getFunctionBody(); if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || acquire->getParentRegion() != &body || @@ -6162,7 +6218,6 @@ class OwnershipInferencePass } auto slot = varOp.getResult(); - llvm::SmallVector releases; for (auto *user : slot.getUsers()) { if (mlir::isa(user)) @@ -6176,7 +6231,15 @@ class OwnershipInferencePass } } - // the slot's state through the function, forward from its entry + return true; + } + + // The slot's state through the function, forward from its entry: assigned, moved at `acquire`, + // released. + SlotStates slotStates(mlir_ts::VariableOp varOp, mlir::Operation *acquire, llvm::ArrayRef assignments, + llvm::ArrayRef releases) + { + auto &body = getFunction().getFunctionBody(); auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) { @@ -6196,7 +6259,8 @@ class OwnershipInferencePass return state; }; - llvm::DenseMap entry; + SlotStates states; + auto &entry = states.entry; auto exitOf = [&](mlir::Block *block) { auto state = entry.lookup(block); for (auto &op : *block) @@ -6225,43 +6289,32 @@ class OwnershipInferencePass } // ...and before each op of the body - llvm::DenseMap before; for (auto &block : body) { auto state = entry.lookup(&block); for (auto &op : block) { - before[&op] = state; + states.before[&op] = state; state = step(&op, state); } - } - auto joinedAt = [&](mlir::Operation *user) { - auto *top = body.findAncestorOpInRegion(*user); - return !top || isJoined(before.lookup(top)); - }; + states.exit[&block] = state; + } - // every release a moved value reaches goes; one some path reaches with nothing in the slot - // is not this rule's - llvm::SmallVector erased; - for (auto *release : releases) - { - auto state = before.lookup(release); - if (state & SlotMoved) - { - if (state & SlotDead) - { - return false; - } + return states; + } - erased.push_back(release); - } - } + // Places a release of the slot on every edge into the join that holds the value and has not moved + // it. False, with nothing placed, when an edge cannot take one, or there is none. + bool unmovedEdges(mlir_ts::VariableOp varOp, const SlotStates &states, llvm::SmallVectorImpl &placed, + llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + auto &entry = states.entry; // the edges into the join that hold the value and did not move it auto handler = handlerBlocks(); - llvm::SmallVector placed; - llvm::SetVector releasedInto; for (auto &block : body) { if (!entry.count(&block)) @@ -6269,7 +6322,7 @@ class OwnershipInferencePass continue; // unreachable } - auto state = exitOf(&block); + auto state = states.exit.lookup(&block); for (auto index : llvm::seq(0, block.getNumSuccessors())) { auto into = entry.lookup(block.getSuccessor(index)); @@ -6302,10 +6355,22 @@ class OwnershipInferencePass } } - if (placed.empty()) - { - return false; - } + return !placed.empty(); + } + + // Whether nothing uses the value where the paths have joined, or after an edge released it. + // False, with the use in `joinedUse` where there is one, when the release cannot be placed. + bool noUseWhereJoined(mlir_ts::VariableOp varOp, mlir::Operation *retain, mlir::Operation *taker, + llvm::ArrayRef assignments, const SlotStates &states, + const llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + + auto joinedAt = [&](mlir::Operation *user) { + auto *top = body.findAncestorOpInRegion(*user); + return !top || isJoined(states.before.lookup(top)); + }; // Nothing uses the value where the paths have joined - a read there, on the path that did // not move it, needs what the edge released - neither through the slot nor through anything @@ -6481,8 +6546,6 @@ class OwnershipInferencePass } } - toErase.insert(erased.begin(), erased.end()); - edgeReleases.append(placed.begin(), placed.end()); return true; } From 19b847bb458c50fc68f19983e50d05fed183d285 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:33:14 +0100 Subject: [PATCH 06/25] own passes: collectInterfaceFamilies in named steps (I1) collectInterfaceFamilies (534 lines) keeps its order and reads as its steps over an InterfaceScan, which holds what the steps share and the function declared as locals: readInterfaceVTables, traceInterfaceValues (the op walk, with its vtableOf / readFromClass / onlyMakesValues helpers), openExposedInterfaces, openThroughMembers, openLooseVTables, buildInterfaceFamilies (with isOpen), collectFieldFunctions, markInterfaceReached. The bodies are moved as they were; InterfaceVTable moves to class scope for the scan to hold. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipSignaturePass.cpp | 235 +++++++++++------- 1 file changed, 140 insertions(+), 95 deletions(-) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 6dcbdc9f3..6da3456b2 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1523,34 +1523,72 @@ class OwnershipSignaturePass : public mlir::PassWrapper open; - auto openAll = [&](const llvm::StringSet<> &names) { - for (auto &entry : names) - { - open.insert(entry.getKey()); - } - }; - - // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a - // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the - // slots of a class's own vtable that hold the address of one of its interface vtables - struct InterfaceVTable - { - mlir_ts::TupleType type; - llvm::DenseMap slots; - // a field's slot: the object literal's field it is the offset of - llvm::DenseMap> fields; - bool written = false; - }; + mlir_ts::TupleType type; + llvm::DenseMap slots; + // a field's slot: the object literal's field it is the offset of + llvm::DenseMap> fields; + bool written = false; + }; + // What collectInterfaceFamilies learns of the module, one step after another. + struct InterfaceScan + { + // the interfaces open whatever their vtables hold + llvm::StringSet<> open; llvm::StringMap vtables; // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) llvm::SmallVector unread; // interface name -> the interface vtables class vtables hold for it, read through an // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) llvm::StringMap> classHeld; + // interface name -> the vtables its values are made with + llvm::StringMap> madeWith; + // An interface value made out of another one reaches the other's members: both are open. One + // made out of `any` or an opaque pointer may be any interface value put into one: every + // interface so boxed is open with it, and every interface at all when it is untraceable. + bool allOpen = false; + llvm::StringSet<> boxed; + llvm::StringSet<> unboxedInto; + bool unboxedFunction = false; + // the interfaces a cast makes a value of out of another interface's or an opaque one, and + // the ones it came from: closed over their members' types below + llvm::StringSet<> castOpened; + // interface -> the interfaces its members' types mention: its vtables' slots and what is + // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields + // included + llvm::StringMap> memberInterfaces; + llvm::DenseSet tied; + + void openAll(const llvm::StringSet<> &names) + { + for (auto &entry : names) + { + open.insert(entry.getKey()); + } + } + }; + + void collectInterfaceFamilies(mlir::ModuleOp module) + { + InterfaceScan scan; + readInterfaceVTables(module, scan); + traceInterfaceValues(module, scan); + openExposedInterfaces(module, scan); + openThroughMembers(scan); + openLooseVTables(scan); + buildInterfaceFamilies(scan); + collectFieldFunctions(module); + markInterfaceReached(scan); + } + + // The vtables: each interface vtable's slots, and the interface vtables a class's own vtable holds. + void readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan) + { module.walk([&](mlir_ts::GlobalOp globalOp) { auto name = globalOp.getSymName(); if (!name.ends_with("..vtbl")) @@ -1571,7 +1609,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(globalOp.getType()); if (!header || !tupleType) { - unread.push_back(name); + scan.unread.push_back(name); return; } @@ -1596,10 +1634,10 @@ class OwnershipSignaturePass : public mlir::PassWrapper the vtables its values are made with - llvm::StringMap> madeWith; + // What makes an interface value, out of which vtable; the casts that box, unbox or reinterpret one; + // the interfaces its members' types mention; the vtables written. + void traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan) + { auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { // the global itself if (auto addressOfOp = addressOfThroughCasts(value)) { - return vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); + return scan.vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); } // a copy of it on the heap, its absent members' slots patched @@ -1652,7 +1693,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper() : mlir_ts::LoadOp(); auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); - if (addressOfOp && vtables.count(addressOfOp.getGlobalName())) + if (addressOfOp && scan.vtables.count(addressOfOp.getGlobalName())) { if (!copied.empty()) { @@ -1711,20 +1752,6 @@ class OwnershipSignaturePass : public mlir::PassWrapper boxed; - llvm::StringSet<> unboxedInto; - auto unboxedFunction = false; - // the interfaces a cast makes a value of out of another interface's or an opaque one, and - // the ones it came from: closed over their members' types below - llvm::StringSet<> castOpened; - // interface -> the interfaces its members' types mention: its vtables' slots and what is - // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields - // included - llvm::StringMap> memberInterfaces; auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; module.walk([&](mlir::Operation *op) { if (auto newInterfaceOp = mlir::dyn_cast(op)) @@ -1733,24 +1760,24 @@ class OwnershipSignaturePass : public mlir::PassWrapper()) { - castOpened.insert(name); - valueInterfaces(extractOp.getInterfaceVal().getType(), castOpened); + scan.castOpened.insert(name); + valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); } else { - allOpen = true; + scan.allOpen = true; } return; @@ -1762,16 +1789,16 @@ class OwnershipSignaturePass : public mlir::PassWrapperuse_empty()) { - open.insert(interfaceRefOp.getIdentifier()); + scan.open.insert(interfaceRefOp.getIdentifier()); } return; @@ -1812,7 +1839,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(symbolRefOp.getInterfaceVal().getType())) { - memberTypeInterfaces(symbolRefOp.getType(), memberInterfaces[interfaceType.getName().getValue()]); + memberTypeInterfaces(symbolRefOp.getType(), scan.memberInterfaces[interfaceType.getName().getValue()]); } return; @@ -1820,13 +1847,17 @@ class OwnershipSignaturePass : public mlir::PassWrapper(op)) { - if (auto found = vtables.find(addressOfOp.getGlobalName()); found != vtables.end() && !onlyMakesValues(addressOfOp.getResult())) + if (auto found = scan.vtables.find(addressOfOp.getGlobalName()); found != scan.vtables.end() && !onlyMakesValues(addressOfOp.getResult())) { found->second.written = true; } } }); + } + // The interfaces another module sees, and the ones a value boxed into `any` may be unboxed into. + void openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan) + { // seen from another module: a word of its exported declarations, or in a reachable type. An // executable's exports are seen by no other module (USER decision, spec 31): only what it // imports - a declared function, an imported global - comes from outside. @@ -1835,7 +1866,6 @@ class OwnershipSignaturePass : public mlir::PassWrapper exposed; llvm::DenseSet seen; for (auto &entry : functions) @@ -1863,16 +1893,20 @@ class OwnershipSignaturePass : public mlir::PassWrappersecond.type) + if (auto found = scan.vtables.find(vtableName); found != scan.vtables.end() && found->second.type) { for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) { if (position > 0) { - memberTypeInterfaces(field.type, memberInterfaces[name]); + memberTypeInterfaces(field.type, scan.memberInterfaces[name]); } } } @@ -1905,7 +1939,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper work; - for (auto &entry : castOpened) + for (auto &entry : scan.castOpened) { work.push_back(entry.getKey().str()); } @@ -1919,31 +1953,29 @@ class OwnershipSignaturePass : public mlir::PassWrapper...vtbl`, `...vtbl`): the interface may have an // implementation outside its families. Belt and braces - every way to make an interface // value is traced above - but a missed member would get its family's facts. llvm::DenseSet made; - for (auto &entry : madeWith) + for (auto &entry : scan.madeWith) { made.insert(entry.second.begin(), entry.second.end()); } - llvm::SmallVector loose(unread.begin(), unread.end()); - for (auto &entry : vtables) + llvm::SmallVector loose(scan.unread.begin(), scan.unread.end()); + for (auto &entry : scan.vtables) { if (!made.contains(entry.getKey())) { @@ -1951,38 +1983,49 @@ class OwnershipSignaturePass : public mlir::PassWrapper tied; - for (auto &[name, made] : madeWith) + for (auto &[name, made] : scan.madeWith) { // a class vtable's slot holding a vtable this pass did not read (another module's) - auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return vtables.count(vtableName); }); + auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return scan.vtables.count(vtableName); }); auto closed = !isOpen(name) && allRead; for (auto vtableName : made) { - if (!vtables.count(vtableName)) + if (!scan.vtables.count(vtableName)) { continue; } if (allRead) { - interfaceVTableTypes[name].push_back(vtables[vtableName].type); + interfaceVTableTypes[name].push_back(scan.vtables[vtableName].type); } - tied.insert(vtableName); - auto &vtable = vtables[vtableName]; + scan.tied.insert(vtableName); + auto &vtable = scan.vtables[vtableName]; for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) { if (position == 0) @@ -2026,9 +2069,11 @@ class OwnershipSignaturePass : public mlir::PassWrapper Date: Fri, 23 Oct 2026 12:04:23 +0100 Subject: [PATCH 07/25] own passes: OwnershipSignaturePass into OwnershipSignaturePass.h (I1) The class moves out of its anonymous namespace into a private header (namespace own_signature), its comments staying with the declarations, as MLIRGenImpl.h did. OwnershipSignaturePass.cpp keeps the pass itself, the facts a signature says, the facts callers must know and their fixpoint, drops, and the facts a library exports. The other methods stay inline in the header until the next commits give each analysis a file of its own. A pure move: checked as line multisets, the only lines not carried over verbatim are the declarations, the qualified definitions and the anonymous namespace. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipSignaturePass.cpp | 5901 ++--------------- .../lib/TypeScript/OwnershipSignaturePass.h | 4866 ++++++++++++++ 2 files changed, 5423 insertions(+), 5344 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignaturePass.h diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 6da3456b2..553ec9c36 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1,416 +1,14 @@ -#include "mlir/Pass/Pass.h" -#include "mlir/IR/BuiltinOps.h" -#include "mlir/IR/SymbolTable.h" -#include "mlir/Interfaces/FunctionInterfaces.h" +// OwnershipSignaturePass (see OwnershipSignaturePass.h): the pass itself, the facts a signature says, +// the facts callers must know and their fixpoint, drops, and the facts a library exports. -#include "TypeScript/TypeScriptDialect.h" -#include "TypeScript/TypeScriptOps.h" -#include "TypeScript/Passes.h" -#include "TypeScript/Defines.h" -#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" - -#include "OwnershipFacts.h" - -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/MapVector.h" -#include "llvm/ADT/SetVector.h" -#include "llvm/ADT/SmallVector.h" -#include "llvm/ADT/StringMap.h" -#include "llvm/ADT/StringSet.h" - -#include +#include "OwnershipSignaturePass.h" #define DEBUG_TYPE "own" -namespace mlir_ts = mlir::typescript; - -namespace +namespace own_signature { -using namespace own_facts; - -// The callee facts of -mm=own (spec 2.4, 3.2), computed over the whole module before the -// per-function inference runs. Each call the pass can resolve - a direct call, a virtual call -// whose candidates are all defined here, the `.instanceOf` slot - gets the facts its candidates -// agree on, pinned on the call op, so the inference reads them without looking anything up. -// -// `__own_no_drops` goes one way: a call with no facts may destroy anything, which can only turn a -// program into an error. The facts a callee's own body relies on - a parameter it keeps, a result -// that borrows an argument - go the other way: a caller that does not know them borrows the -// argument and releases it, or releases a result it does not own, a double free either way. So a -// function has those only when every call that can reach it is one this pass resolved (the closed -// world, see `open`). -// -// `__own_no_drops` alone crosses a module boundary: a library built under own exports the names of -// its exported functions that have it (SHARED_LIB_OWN_FACTS), and a module importing it calls them -// knowing it. The other two cannot: a module linking the library statically re-parses its source -// and sees no facts at all, and a caller that does not know them frees twice. -class OwnershipSignaturePass : public mlir::PassWrapper> -{ - public: - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipSignaturePass) - - // `executable`: the module is built into a program (`--emit=exe`, or the JIT's entry module), - // whose exports no other module sees (USER decision, spec 31). Unknown is a library. - explicit OwnershipSignaturePass(bool executable = false) : executable(executable) - { - } - - bool executable = false; - - // A global is a root (rc 5ai): what its initializer makes moves into it, and nothing gives it - // back. rc retains a fresh value for its `ts.GlobalResult`, and that retain is the move: it - // goes. The per-function inference never sees a global's region, so any other retain there is - // reported here, by the global's name, rather than left to the lowering's "left a retain - // behind". - // Every use of `value` ends in the global's `ts.GlobalResult`: given to it, or an element of a - // fresh array literal that is (`const nested = [[1, 2], [3]]`), or the payload of a - // `new Shared(x)` (`let g = new Shared({...})`). Its retains are judged on their own. - static bool movesIntoResult(mlir::Value value) - { - return llvm::all_of(value.getUses(), [](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - return true; - } - - // given to `new Shared(x)`: it moves into the handle's block, which owns it from then on - // whatever becomes of the handle - rc counts the handle (spec 23.2, 26) - if (auto sharedNewOp = mlir::dyn_cast(user)) - { - return sharedNewOp.getValue() == use.get(); - } - - if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) - { - return movesIntoResult(user->getResult(0)); - } - - // copied into a block `ts.New` made, which itself moves in: an object literal's box - if (auto storeOp = mlir::dyn_cast(user); storeOp && use.getOperandNumber() == 0) - { - auto block = storeOp.getReference(); - return block.getDefiningOp() && - llvm::all_of(block.getUses(), [&](mlir::OpOperand &blockUse) { - return blockUse.getOwner() == storeOp.getOperation() || - (isView(blockUse.getOwner()) && movesIntoResult(blockUse.getOwner()->getResult(0))); - }); - } - - return false; - }); - } - - // A record copied out of a local a `ts.Constant` initializes, whose owning fields are given - // nothing that holds a block: it holds constants only - numbers, immortal string literals, - // functions - and nobody else's reference. - static bool holdsOnlyConstants(mlir::Value value) - { - auto loadOp = value.getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp || !varOp.getInitializer() || !varOp.getInitializer().getDefiningOp()) - { - return false; - } - - return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { - if (mlir::isa(user)) - { - return true; - } - - auto propertyRefOp = mlir::dyn_cast(user); - return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { - auto storeOp = mlir::dyn_cast(fieldUser); - return mlir::isa(fieldUser) || - (storeOp && storeOp.getReference() == propertyRefOp.getResult() && - holdsNoBlock(storeOp.getValue())); - }); - }); - } - - // Whether `op` may run more than once in one run of its function: it is inside a loop op, or its - // block - or the block of a region op holding it - is on a cycle of its region's CFG, a path from - // the block's successors back to it. (By this pass the affine lowering has made every loop a CFG - // cycle of the function body; the loop op is kept for safety.) - static bool mayRunAgain(mlir::Operation *op) - { - for (auto *at = op; at && !mlir::isa(at); at = at->getParentOp()) - { - if (mlir::isa(at)) - { - return true; - } - - auto *block = at->getBlock(); - if (!block) - { - return true; // not in a function: nothing is known of how often it runs - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work(block->getSuccessors().begin(), block->getSuccessors().end()); - while (!work.empty()) - { - auto *next = work.pop_back_val(); - if (next == block) - { - return true; - } - - if (seen.insert(next).second) - { - work.append(next->getSuccessors().begin(), next->getSuccessors().end()); - } - } - } - - return false; - } - - // The immortal globals (OWN_IMMORTAL_GLOBAL_ATTR_NAME, spec 25). A global of a class type, - // not exported, is immortal when only its initializer writes it, once, and gives it a fresh - // object, nothing, or another immortal global's value. Its initializer is either its own region - // (`const c = new C()`) or, for a class static, the one store into it in the class's static - // constructor, which a `ts.GlobalConstructor` runs (the region then yields only the default). - // Top-level script code is such a constructor too, so a module-level `let c: C;` it stores - // into once is one as well. "Once" is how often the store runs, not how many stores there are: - // a store in a loop (`static { for (...) K.w = new V(i); }`) would mark every block it is given - // immortal, and each old one would leak, so a store that may run again leaves the global mortal, - // and each store releases the old value, as for any other global. - // - // The store - the region's, which the lowering makes - marks the block HEAP_BLOCK_IMMORTAL, so - // every block the global ever holds is one no release frees. The value test makes sure the block - // is the global's to keep: an object made right there and given back by nobody. A call's result - // is not one, whatever its callee: these facts are found before the callees' (computeFacts), and - // a global's region is no function the inference checks, so a result that borrows its argument - - // a temporary the region frees, an element of another global's array - would be marked, and - // stored, as the global's own. Such a global stays mortal, and a read of it keeps today's errors. - void findImmortalGlobals(mlir::ModuleOp module) - { - // the static constructors, which run once before the program - llvm::StringSet<> constructors; - module.walk([&](mlir_ts::GlobalConstructorOp constructorOp) { constructors.insert(constructorOp.getGlobalName()); }); - - // a static's address taken out of a class's vtable (`k.w = x`, `k: typeof K`) and written - // through: it may be any class's static the vtable slot holds, so no static is immortal. - // MLIRGen makes a `ts.ThisVirtualSymbolRef` only for a method (a bound function, never a - // `!ts.ref`), so no program reaches this through one today; it is listed for an IR that would. - auto vtableWritten = false; - module.walk([&](mlir::Operation *op) { - if (mlir::isa(op) && - mlir::isa(op->getResult(0).getType()) && - !llvm::all_of(op->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) - { - vtableWritten = true; - } - }); - - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - return; // an op this pass cannot read symbols through: nothing is known never written - } - - llvm::StringMap> usesOf; - for (auto &use : *uses) - { - usesOf[use.getSymbolRef().getRootReference().getValue()].push_back(use.getUser()); - } - - // each candidate with the value its initializer gives it, and the store that does, if any - llvm::MapVector> candidates; - auto mth = typeHelper(&getContext()); - module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || - globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || - globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock()) - { - return; - } - - auto resultOp = mlir::dyn_cast(globalOp.getInitializerBlock()->getTerminator()); - if (!resultOp || resultOp->getNumOperands() != 1) - { - return; - } - - mlir_ts::StoreOp initStore; - for (auto *user : usesOf.lookup(globalOp.getSymName())) - { - // its vtable's entry, which only a `ts.VirtualSymbolRef` reads (vtableWritten) - if (mlir::isa(user) && user->getParentOfType()) - { - if (vtableWritten) - { - return; - } - - continue; - } - - if (mlir::isa(user)) - { - continue; // only loaded, or vtableWritten - } - - auto addressOfOp = mlir::dyn_cast(user); - if (!addressOfOp) - { - return; - } - - auto released = false; - auto stored = false; - for (auto *addressUser : addressOfOp->getUsers()) - { - if (mlir::isa(addressUser)) - { - continue; - } - - // the old value, the default, given up before the store - if (mlir::isa(addressUser)) - { - released = true; - continue; - } - - // the one store, in a static constructor, run once - auto storeOp = mlir::dyn_cast(addressUser); - auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); - if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || - storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || - mayRunAgain(storeOp)) - { - return; - } - - initStore = storeOp; - stored = true; - } - - if (released && !stored) - { - return; - } - } - - // a static's region yields only its default, which the store replaces - if (initStore && !regionYieldsDefault(globalOp)) - { - return; - } - - candidates[globalOp] = {initStore ? initStore.getValue() : resultOp->getOperand(0), initStore}; - }); - - // given a fresh object, nothing, or another immortal global's value: in module order, until - // no more are found - llvm::StringSet<> immortal; - auto madeHere = [&](mlir::Value value) { - auto root = rootOf(value); - if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) - { - return false; - } - - // and not given back before the global takes it - auto released = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); }); - return !released; - }; - // isImmortalGlobalRead, of the globals found so far: none is marked yet - auto readsImmortal = [&](mlir::Value value) { - auto addressOfOp = globalReadOf(value); - return addressOfOp && immortal.contains(addressOfOp.getGlobalName()); - }; - - for (auto found = true; found;) - { - found = false; - for (auto &[globalOp, init] : candidates) - { - auto value = init.first; - if (!immortal.contains(globalOp.getSymName()) && (madeHere(value) || holdsNoBlock(value) || readsImmortal(value))) - { - immortal.insert(globalOp.getSymName()); - found = true; - } - } - } - - auto unit = mlir::UnitAttr::get(&getContext()); - for (auto &[globalOp, init] : candidates) - { - if (!immortal.contains(globalOp.getSymName())) - { - continue; - } - - globalOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - if (init.second) - { - init.second->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - } - - for (auto *user : usesOf.lookup(globalOp.getSymName())) - { - if (mlir::isa(user)) - { - user->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - } - } - } - } - - void moveIntoGlobals(mlir::ModuleOp module) - { - module.walk([&](mlir_ts::GlobalOp globalOp) { - llvm::SmallVector retains; - globalOp.walk([&](mlir_ts::RetainOp retainOp) { retains.push_back(retainOp); }); - for (auto retainOp : retains) - { - // a handle's retain is rc's: the global's count, which own leaves in place (spec 23.2) - if (touchesHandle(retainOp)) - { - continue; - } - - auto value = retainOp.getReference(); - - // an immortal global's value (spec 25): owned by no one, held by any number - if (isImmortalGlobalRead(value)) - { - retainOp.erase(); - continue; - } - - // nothing in the region gives the block back, or takes a second reference to it: - // the global is its one owner - auto released = false; - auto retained = 0; - forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { - released = released || mlir::isa(user); - retained += mlir::isa(user); - }); - - if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) - { - retainOp.erase(); - continue; - } - - retainOp.emitError("the initializer of global '") - << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; - signalPassFailure(); - } - }); - } - - void runOnOperation() override + void OwnershipSignaturePass::runOnOperation() { auto module = getOperation(); module.walk([&](mlir_ts::FuncOp funcOp) { functions[funcOp.getSymName()] = funcOp; }); @@ -540,9 +138,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); if (!symbolRefOp) @@ -554,10 +150,7 @@ class OwnershipSignaturePass : public mlir::PassWrappersecond.isDeclaration() ? found->second : mlir_ts::FuncOp(); } - // The fields of a closure body's capture box whose cell it assigns (OWN_ASSIGNS_CAPTURES_ATTR_NAME), - // directly or through a closure it builds over the cell. Every field, when the body is unknown or - // a cell goes anywhere this does not follow. - llvm::SetVector assignedCaptures(mlir_ts::FuncOp funcOp, int depth = 0) + llvm::SetVector OwnershipSignaturePass::assignedCaptures(mlir_ts::FuncOp funcOp, int depth) { llvm::SetVector assigned; auto boxType = funcOp && funcOp.getNumArguments() > 0 && !funcOp.getBody().empty() @@ -608,8 +201,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper> assignedCache; - - // What a call may reach: known, when every candidate is a function defined in this module (or - // the call goes through the `.instanceOf` slot, or reaches a function another module says - // destroys nothing); else anything. - struct Callees + llvm::ArrayRef OwnershipSignaturePass::declared(mlir::Operation *funcOp, llvm::StringRef name) { - bool known = false; - bool instanceOf = false; - bool imported = false; - // a function read out of a field that only ever holds it (fieldFunctions) - bool fieldFunction = false; - // the slot of a closed interface (resolveInterfaceSlot) - bool viaInterface = false; - // a parameter of a closed function that callers pass only functions of this module - // (callbackFamilies), with each member's shift: a closure's body takes its capture box - // first, so its parameter k is the call's argument k - 1 - bool viaCallback = false; - llvm::SmallVector shifts; - // the name the imported function is exported under - llvm::StringRef importedName; - llvm::SmallVector funcs; - // the symbols the call names, whether or not it is known: a direct callee, or every - // member of a virtual slot's family (for the facts a declaration says, declaredFacts) - llvm::SmallVector named; - }; - - struct Call + if (auto attr = funcOp->getAttrOfType(name)) + { + return attr.asArrayRef(); + } + + return {}; + } + + bool OwnershipSignaturePass::declaresFacts(mlir::Operation *funcOp) { - mlir::Operation *op; - Callees callees; - }; - - llvm::StringMap functions; - llvm::StringMap globals; - // Globals whose address is used other than to load from it: what they hold may change. - llvm::StringSet<> writtenGlobals; - llvm::SmallVector calls; - - // Functions of imported libraries that destroy nothing a caller can reach, by the name they - // are exported under (SHARED_LIB_OWN_NO_DROPS_ATTR_NAME). - llvm::StringSet<> importedNoDrops; - - // (tuple type, field) -> the one function an object of that type holds in that field. A - // generator's state object holds its `next` so. - own_facts::FieldFunctions fieldFunctions; - - // Class names, from their vtables, to split a method's symbol into class and method name. - llvm::StringSet<> classNames; - // (vtable position, method name) -> the functions at that position under that name, in every - // class vtable of the module: what a virtual call through that slot may reach. - std::map, llvm::SmallVector> families; - - // Functions whose callers are not all known - a caller outside the module, or a reference to - // the symbol that is not a call this pass resolves - and why, for the error in their body. - llvm::DenseMap open; - - // Methods an interface's vtable holds (`...vtbl`, or `...vtbl` - // for an object literal) whose calls through the interface are not resolved: the slot's family - // is open (interfaceFamilies), or the vtable is one this pass cannot tie to an interface. Such - // a call (`ts.InterfaceSymbolRef`, then an indirect call) is given none of the method's facts: a - // caller that hands it an argument it keeps still releases it, and a caller of one whose result - // borrows an argument or `this` releases the borrow as its own (a double free either way). So a - // method reached this way that keeps a parameter or returns a borrow - by its body or by `@keeps` - // / `@lends` - loses its facts, and the body's error says why (`is a parameter, kept here, but - // -mm=own cannot make the callers of ... give it up`, `is a parameter, returned here, but -mm=own - // cannot tell the callers of ... that the result borrows it`). A method reached only through - // closed interfaces keeps its facts: their calls get them (spec 31). - llvm::DenseSet interfaceReached; - - // ---- Interfaces closed in this module (G9, spec 31) ---- - // - // A call through an interface reaches whatever function the slot of the interface value's vtable - // holds. When every such vtable is one this module defines and every interface value is made - // from one of them, the slot holds one of a known set of functions - the slot's family - and the - // call is resolved to it like a virtual call of a private class family: it gets the facts its - // members agree on (meetFacts demotes a family that disagrees), and `__own_no_drops` when every - // member destroys nothing. Anything this cannot see opens the interface, and then the call is - // what it was: no facts, and its members reached that way lose theirs (interfaceReached). - // - // Open, for an interface I: - // - another module can see it: its name in an exported declaration (`__decls`), or `!ts.iface<@I>` - // in the type of a function or global another module can reach - an exported or declared - // function, a global that is not private, or is exported or imported; - // - an I value made other than by `ts.NewInterface` over a vtable this pass reads (the global, a - // heap copy of it, or the one an object's class vtable holds for I): a cast from another - // interface (`J` to `I`, which opens `J` too: its members are reached through `I`), from - // `any` or an opaque pointer (which opens every interface put into one), from a tuple or array - // that does not hold I already (castMakesInterface), a `ts.InterfaceRef` used as a value; - // - a vtable of I written after its initializer, or read other than to make an interface value; - // - a class vtable that holds a vtable of I and is not private (an exported class: another - // module may extend it); - // - and, per slot, a member that is not a function this module defines, a slot that holds no - // function (a field's offset, a thunk's cell), or a member that says `@keeps` or `@lends` (its - // facts are its signature's, which a family cannot change). A member that is not private (a - // `public` method) is open (findOpen): it has no facts of its own to give the family. - struct InterfaceFamily + return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || + funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); + } + + bool OwnershipSignaturePass::declaresCallerFacts(mlir::Operation *funcOp) { - // the slot's name, which every vtable of the interface agrees on - mlir::StringAttr field; - llvm::SmallVector members; - bool closed = true; - }; - - // (interface, vtable position) -> the functions every vtable of the interface holds there - std::map, InterfaceFamily> interfaceFamilies; - - // The words of the module's exported declarations (none in an executable), the classes a class - // vtable is the own vtable of, and the classes another module can name: exported, or in the - // type of an exported or declared function or of an exported or imported global (findOpen). - llvm::StringSet<> exportWords; - llvm::StringSet<> ownClasses; - llvm::StringSet<> nameableClasses; - - // A field of function type (`f: (x: number) => number`), called through the interface: the slot - // holds the offset of a field of each implementing object literal, and the call goes to whatever - // function that field holds. When every vtable's slot is such an offset and every function ever - // stored into those fields is one this module defines - the literal's own (`{ f: function ... }`, - // in its `ts.Constant`), or one stored into the field later, through the object or through the - // interface - the call is resolved to them (spec 31). A store of anything else (a parameter, a - // call's result, a closure), a field reached other than to be read or stored, or an object of - // the literal's type made other than by the literal opens the slot. A class implementing the - // field holds a thunk's cell, which this does not follow: open. - struct InterfaceFieldFamily + return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); + } + + llvm::SmallVector OwnershipSignaturePass::declaredLends(mlir::Operation *funcOp) { - mlir::StringAttr field; - // (object literal's storage tuple, field index) of each vtable - llvm::SmallVector, 2> targets; - llvm::SmallVector members; - bool closed = true; - }; - - std::map, InterfaceFieldFamily> interfaceFieldFamilies; - - // interface name -> the tuple types of the vtables its values are made with, when every one of - // them is read here (holdsFunction: what a value of the interface carries) - llvm::StringMap> interfaceVTableTypes; - - // `ts.PropertyRef(ts.Cast(null), i)`, seen through casts: the offset of field `i` of an object of - // that type, as an object literal's interface vtable holds it. Null for anything else. - std::pair slotField(mlir::Value value) + auto lends = declared(funcOp, OWN_DECLARED_LENDS_ATTR_NAME); + llvm::SmallVector sorted(lends.begin(), lends.end()); + llvm::sort(sorted); + sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end()); + return sorted; + } + + llvm::SmallVector OwnershipSignaturePass::declaredByCallees(const Callees &callees, llvm::StringRef name) { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + llvm::SmallVector agreed; + for (auto [index, symbol] : llvm::enumerate(callees.named)) { - value = castOp.getIn(); - } + // a function, or the global holding the address of one a library exports (a dynamic + // import), which MLIRGen gave the declaration's markers + mlir::Operation *declaring = functions.lookup(symbol.getValue()); + if (!declaring) + { + declaring = globals.lookup(symbol.getValue()); + } - auto propertyRefOp = value ? value.getDefiningOp() : mlir_ts::PropertyRefOp(); - auto nullObject = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::CastOp(); - if (!nullObject || !nullObject.getIn().getDefiningOp() || - !mlir::isa(nullObject.getType())) - { - return {}; - } + if (!declaring || !declaring->hasAttr(name)) + { + return {}; + } - auto tupleType = storageTupleOf(nullObject.getType()); - if (!tupleType) - { - return {}; + auto indices = declared(declaring, name); + if (index == 0) + { + agreed.assign(indices.begin(), indices.end()); + } + else if (llvm::ArrayRef(agreed) != indices) + { + return {}; + } } - return {tupleType, static_cast(propertyRefOp.getPosition())}; + return agreed; } - // What every object-literal field an interface's field slot may be holds (InterfaceFieldFamily). - void collectFieldFunctions(mlir::ModuleOp module) + std::string OwnershipSignaturePass::describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices) { - llvm::DenseMap, llvm::SetVector> stored; - llvm::DenseSet> unknownFields; - llvm::DenseSet unknownTypes; - // a function of unknown origin stored through an interface this pass cannot follow - auto unknownAnywhere = false; - - auto store = [&](std::pair key, mlir::Value value) { - auto symbol = slotFunction(value); - auto funcOp = symbol ? functions.lookup(symbol.getValue()) : mlir_ts::FuncOp(); - if (funcOp && !funcOp.isDeclaration()) + if (indices.empty()) + { + return "nothing"; + } + + auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); + std::string text; + llvm::raw_string_ostream os(text); + for (auto [position, index] : llvm::enumerate(indices)) + { + os << (position ? ", " : "") << "'"; + if (names && static_cast(index) < names.size()) { - stored[key].insert(symbol); + os << mlir::cast(names[index]).getValue(); } else { - unknownFields.insert(key); + os << "#" << index; } - }; - auto isFunctionType = [](mlir::Type type) { - return mlir::isa(type); - }; + os << "'"; + } - module.walk([&](mlir::Operation *op) { - // the literal's own values - if (auto constantOp = mlir::dyn_cast(op)) + return os.str(); + } + + int32_t OwnershipSignaturePass::parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp) + { + auto root = rootOf(value); + if (auto loadOp = root.getDefiningOp()) + { + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) { - auto constTupleType = mlir::dyn_cast(constantOp.getType()); - auto values = mlir::dyn_cast(constantOp.getValue()); - if (!constTupleType || !values) - { - return; - } + return parameterOf(propertyRefOp->getOperand(0), funcOp); + } - mlir::Type tupleType = mlir_ts::TupleType::get(&getContext(), constTupleType.getFields()); - for (auto [index, value] : llvm::enumerate(values)) - { - std::pair key{tupleType, static_cast(index)}; - if (auto symbol = mlir::dyn_cast(value)) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (funcOp && !funcOp.isDeclaration()) - { - stored[key].insert(symbol.getAttr()); - } - else - { - unknownFields.insert(key); - } - } - else if (!mlir::isa(value) && index < constTupleType.getFields().size() && - isFunctionType(constTupleType.getFields()[index].type)) - { - unknownFields.insert(key); - } - } + auto varOp = loadOp.getReference().getDefiningOp(); + int index = -1; + if (varOp && isParameterSlot(varOp, index) && varOp->getParentOfType() == funcOp) + { + return index; + } - return; + return -1; + } + + auto argument = mlir::dyn_cast(root); + if (argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) + { + return static_cast(argument.getArgNumber()); + } + + return -1; + } + + void OwnershipSignaturePass::checkDeclared() + { + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration()) + { + continue; } - // a store into the field through the object - if (auto propertyRefOp = mlir::dyn_cast(op)) + if (funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME) && funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) { - auto tupleType = storageTupleOf(propertyRefOp.getObjectRef().getType()); - if (!tupleType) - { - return; - } + funcOp.emitError("'") << funcOp.getSymName() + << "' cannot say both '@" LENDS_DECORATOR "' and '@" BORROWS_DECORATOR + "': its caller owns a result that borrows, and nothing of one it is lent"; + signalPassFailure(); + continue; + } - std::pair key{tupleType, static_cast(propertyRefOp.getPosition())}; - for (auto &use : propertyRefOp->getUses()) + if (declaresCallerFacts(funcOp)) + { + auto local = factsLocal.lookup(funcOp); + auto keeps = declared(funcOp, OWN_DECLARED_KEEPS_ATTR_NAME); + auto lends = declaredLends(funcOp); + if (local.borrows != lends) { - auto *user = use.getOwner(); - if (mlir::isa(user)) + if (lends.empty()) { - continue; + funcOp.emitError("'") << funcOp.getSymName() << "' returns a borrow of " + << describeParams(funcOp, local.borrows) + << ", which it has to say with '@" LENDS_DECORATOR "'"; } - - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == propertyRefOp.getResult() && storeOp.getValue() != propertyRefOp.getResult()) + else if (local.borrows.empty()) { - store(key, storeOp.getValue()); - continue; + funcOp.emitError("'@" LENDS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) + << ", but its body does not return a borrow of it"; } - - // an interface vtable's offset of the field (slotField) reads nothing - if (user->getParentOfType() && mlir::isa(user)) + else { - continue; + funcOp.emitError("'@" LENDS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) + << ", but its body returns a borrow of " << describeParams(funcOp, local.borrows); } - unknownFields.insert(key); - } - - return; - } - - // a store into the field through an interface: into each literal its vtables reach - if (auto interfaceRefOp = mlir::dyn_cast(op)) - { - auto refType = mlir::dyn_cast(interfaceRefOp.getType()); - if (!refType || !isFunctionType(refType.getElementType())) - { - return; + signalPassFailure(); } - - auto interfaceType = mlir::dyn_cast(interfaceRefOp.getInterfaceVal().getType()); - auto found = interfaceType ? interfaceFieldFamilies.find({interfaceType.getName().getValue().str(), - static_cast(interfaceRefOp.getIndex()) + - INTERFACE_VTABLE_HEADER_SLOTS}) - : interfaceFieldFamilies.end(); - for (auto &use : interfaceRefOp->getUses()) + else if (llvm::ArrayRef(local.owned) != keeps) { - auto *user = use.getOwner(); - if (mlir::isa(user)) + if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) { - continue; + funcOp.emitError("'@" KEEPS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, keeps) << ", but its body keeps " + << describeParams(funcOp, local.owned); } - - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == interfaceRefOp.getResult() && found != interfaceFieldFamilies.end() && - found->second.closed) + else { - for (auto target : found->second.targets) - { - store(target, storeOp.getValue()); - } - - continue; + funcOp.emitError("'") << funcOp.getSymName() << "' keeps " << describeParams(funcOp, local.owned) + << ", which it has to say with '@" KEEPS_DECORATOR "'"; } - unknownAnywhere = true; + signalPassFailure(); } - - return; } - // an object of the literal's type made other than by the literal: its fields may hold - // anything. A cast keeps the type's own value (the literal's box seen as the object, an - // object, a reference to its storage) or makes the null object a vtable's offsets are - // taken from; from anything else it reinterprets. - if (auto castOp = mlir::dyn_cast(op)) + if (funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) { - auto tupleType = storageTupleOf(castOp.getType()); - if (tupleType && !mlir::isa(castOp.getIn().getType()) && - storageTupleOf(castOp.getIn().getType()) != tupleType) + auto says = declared(funcOp, OWN_DECLARED_BORROWS_ATTR_NAME); + auto found = borrowsFound.find(funcOp); + if (found == borrowsFound.end()) + { + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but it neither makes a generator whose state borrows its arguments nor returns one"; + signalPassFailure(); + } + else if (llvm::ArrayRef(found->second) != says) { - unknownTypes.insert(tupleType); + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but the state it makes borrows " << describeParams(funcOp, found->second); + signalPassFailure(); } - - return; - } - - if (mlir::isa(op) || isCall(op) || isView(op) || mlir::isa(op)) - { - return; } + } - for (auto result : op->getResults()) + // the members of a family a declaring method is in: each says the same + for (auto &[key, members] : families) + { + mlir_ts::FuncOp declaring; + for (auto member : members) { - if (mlir::isa(result.getType())) + if (auto funcOp = functions.lookup(member.getValue()); funcOp && declaresFacts(funcOp)) { - if (auto tupleType = storageTupleOf(result.getType())) - { - unknownTypes.insert(tupleType); - } + declaring = funcOp; + break; } } - }); - for (auto &[key, family] : interfaceFieldFamilies) - { - if (!family.closed) + if (!declaring) { continue; } - for (auto target : family.targets) + for (auto member : members) { - if (unknownAnywhere || unknownTypes.contains(target.first) || unknownFields.contains(target)) + auto funcOp = functions.lookup(member.getValue()); + if (!funcOp || funcOp == declaring) { - family.closed = false; - break; + continue; } - for (auto member : stored.lookup(target)) + for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_LENDS_ATTR_NAME}) { - if (!llvm::is_contained(family.members, member)) + if (funcOp->hasAttr(name) != declaring->hasAttr(name) || declared(funcOp, name) != declared(declaring, name)) { - family.members.push_back(member); + funcOp.emitError("'") << funcOp.getSymName() << "' overrides '" << declaring.getSymName() + << "', and has to say the same '@" KEEPS_DECORATOR "', '@" BORROWS_DECORATOR + "' and '@" LENDS_DECORATOR "'"; + signalPassFailure(); + break; } } } - - family.closed = family.closed && !family.members.empty(); } } - // The interfaces mentioned by a type as values: the interface itself, a union or optional, a - // tuple's fields, an array's elements, what a reference points to. Not a class's or an object's - // fields (a reference to a block made elsewhere), nor a function's parameters and result. - static void valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth = 0) + bool OwnershipSignaturePass::factsThroughInterface(mlir::Operation *funcOp, const Facts &facts) { - if (!type || depth > 16) - { - return; - } - - if (auto interfaceType = mlir::dyn_cast(type)) - { - found.insert(interfaceType.getName().getValue()); - return; - } + return !facts.empty() && interfaceReached.contains(funcOp); + } - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) + llvm::SmallVector OwnershipSignaturePass::returnedValues(mlir_ts::FuncOp funcOp) + { + llvm::SmallVector values; + funcOp.walk([&](mlir_ts::ReturnInternalOp returnOp) { + for (auto operand : returnOp.getRetOperands()) { - valueInterfaces(info.type, found, depth + 1); - } - }; + auto loadOp = operand.getDefiningOp(); + auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!slot || slot.getInitializer() || isOwningVariable(slot)) + { + values.push_back(operand); + continue; + } - if (auto tupleType = mlir::dyn_cast(type)) - { - fields(tupleType.getFields()); - } - else if (auto constTupleType = mlir::dyn_cast(type)) - { - fields(constTupleType.getFields()); - } - else if (auto unionType = mlir::dyn_cast(type)) - { - for (auto member : unionType.getTypes()) - { - valueInterfaces(member, found, depth + 1); + for (auto *user : slot.getResult().getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == slot.getResult()) + { + values.push_back(storeOp.getValue()); + } + } } - } - else if (auto optionalType = mlir::dyn_cast(type)) - { - valueInterfaces(optionalType.getElementType(), found, depth + 1); - } - else if (auto arrayType = mlir::dyn_cast(type)) - { - valueInterfaces(arrayType.getElementType(), found, depth + 1); - } - else if (auto constArrayType = mlir::dyn_cast(type)) - { - valueInterfaces(constArrayType.getElementType(), found, depth + 1); - } - else if (auto refType = mlir::dyn_cast(type)) - { - valueInterfaces(refType.getElementType(), found, depth + 1); - } - else if (auto valueRefType = mlir::dyn_cast(type)) - { - valueInterfaces(valueRefType.getElementType(), found, depth + 1); - } - else if (auto boundRefType = mlir::dyn_cast(type)) - { - valueInterfaces(boundRefType.getElementType(), found, depth + 1); - } - else if (auto sharedType = mlir::dyn_cast(type)) - { - valueInterfaces(sharedType.getElementType(), found, depth + 1); - } + }); + + return values; } - // Every interface a type mentions anywhere, a class's fields and a function's parameters - // included: what another module handed a value of this type could reach. With `classes`, the - // classes it mentions too (a base class is its derived class's first field). - static void anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, - llvm::StringSet<> *classes = nullptr) + OwnershipSignaturePass::ResultBorrows OwnershipSignaturePass::localResultBorrows(mlir_ts::FuncOp funcOp) { - if (!type || !seen.insert(type).second) + auto found = -1; + auto record = false; + llvm::SmallVector params; + for (auto value : returnedValues(funcOp)) { - return; - } + if (holdsNoBlock(value)) + { + continue; + } - if (auto interfaceType = mlir::dyn_cast(type)) - { - found.insert(interfaceType.getName().getValue()); - return; - } + // a handle returned is a counted copy, never a borrow (spec 23.2) + if (isHandle(value)) + { + return {}; + } - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) + switch (recordParamsOf(value, params)) { - anyInterfaces(info.type, found, seen, classes); + case RecordFlow::Bad: + return {}; + case RecordFlow::Record: + record = true; + continue; + case RecordFlow::None: + break; } - }; - // the types whose fields are not parameters of the type - if (auto classStorageType = mlir::dyn_cast(type)) - { - if (classes) + auto index = borrowedParam(value); + if (index < 0 || (found >= 0 && index != found)) { - classes->insert(classStorageType.getName().getValue()); + return {}; } - fields(classStorageType.getFields()); - return; + found = index; } - if (auto objectStorageType = mlir::dyn_cast(type)) + if (record && found >= 0) { - fields(objectStorageType.getFields()); - return; + return {}; } - if (auto tupleType = mlir::dyn_cast(type)) + if (record) { - fields(tupleType.getFields()); - return; + llvm::sort(params); + params.erase(std::unique(params.begin(), params.end()), params.end()); + return {params, true}; } - if (auto constTupleType = mlir::dyn_cast(type)) + if (found >= 0) { - fields(constTupleType.getFields()); - return; + return {{found}, false}; } - type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { anyInterfaces(sub, found, seen, classes); }); + return {}; } - // The function values a value of this type holds: the type itself, a union's or an optional's - // member, a tuple's field, an array's element. Not what a reference points to (a vtable's - // address seen as an opaque pointer is the compiler's own, not a value). - static void functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth = 0) + int32_t OwnershipSignaturePass::mixedReturnParam(mlir_ts::FuncOp funcOp) { - if (!type || depth > 16) - { - return; - } - - if (mlir::isa(type)) + auto param = -1; + auto other = false; + auto returned = returnedValues(funcOp); + llvm::SmallVector work(returned.begin(), returned.end()); + llvm::DenseSet seen; + while (!work.empty()) { - found.push_back(type); - return; - } + auto value = work.pop_back_val(); + if (!seen.insert(value).second || holdsNoBlock(value)) + { + continue; + } - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) + if (isHandle(value) || isCopyableString(value.getType())) { - functionValues(info.type, found, depth + 1); + return -1; } - }; - if (auto tupleType = mlir::dyn_cast(type)) - { - fields(tupleType.getFields()); - } - else if (auto constTupleType = mlir::dyn_cast(type)) - { - fields(constTupleType.getFields()); - } - else if (auto unionType = mlir::dyn_cast(type)) - { - for (auto member : unionType.getTypes()) - { - functionValues(member, found, depth + 1); - } - } - else if (auto optionalType = mlir::dyn_cast(type)) - { - functionValues(optionalType.getElementType(), found, depth + 1); - } - else if (auto arrayType = mlir::dyn_cast(type)) - { - functionValues(arrayType.getElementType(), found, depth + 1); - } - else if (auto constArrayType = mlir::dyn_cast(type)) - { - functionValues(constArrayType.getElementType(), found, depth + 1); - } - else if (auto sharedType = mlir::dyn_cast(type)) - { - functionValues(sharedType.getElementType(), found, depth + 1); - } - } - - // A function type's parameters and results, whatever kind of function it is. - static std::pair, llvm::SmallVector> signatureOf(mlir::Type type) - { - llvm::SmallVector inputs; - llvm::SmallVector results; - if (auto functionType = mlir::dyn_cast(type)) - { - inputs.append(functionType.getInputs().begin(), functionType.getInputs().end()); - results.append(functionType.getResults().begin(), functionType.getResults().end()); - } - else if (auto boundType = mlir::dyn_cast(type)) - { - inputs.append(boundType.getInputs().begin(), boundType.getInputs().end()); - results.append(boundType.getResults().begin(), boundType.getResults().end()); - } - else if (auto hybridType = mlir::dyn_cast(type)) - { - inputs.append(hybridType.getInputs().begin(), hybridType.getInputs().end()); - results.append(hybridType.getResults().begin(), hybridType.getResults().end()); - } - else - { - // a kind this does not read: never the same signature as another - inputs.push_back(type); - } - - return {inputs, results}; - } - - // Does a cast reinterpret a function value - from `any` or `unknown`, or as a function of another - // signature? Then an interface value its parameters or - // results hold crosses unseen (a `(o: Op2) => ...` called as `(o: Op) => ...`): `interfaces` - // gets every interface either side's signatures mention, and `boxed` the input's when it is put - // into `any`. - static bool castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing) - { - // `!ts.opaque` is the compiler's own (a vtable slot read as the `.instanceOf` it holds), not - // a value a program can put a function into - auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; - llvm::SmallVector inFunctions; - llvm::SmallVector outFunctions; - functionValues(in, inFunctions); - functionValues(out, outFunctions); - boxing = false; - if ((inFunctions.empty() && outFunctions.empty()) || in == out) - { - return false; - } - - auto mention = [&](llvm::ArrayRef types) { - for (auto type : types) + auto root = rootOf(value); + if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) { - llvm::DenseSet seen; - anyInterfaces(type, interfaces, seen); - } - }; - - if (isOpaque(out)) - { - boxing = true; - mention(inFunctions); - return false; - } - - // the same signatures, but for `this` seen as an opaque pointer on one side, or bound on one - // side only (a closure's box, a bound function's object: the caller supplies neither) - auto sameSignature = [](mlir::Type a, mlir::Type b) { - auto [aInputs, aResults] = signatureOf(a); - auto [bInputs, bResults] = signatureOf(b); - auto dropBoundThis = [](mlir::Type type, llvm::SmallVector &inputs) { - if (!inputs.empty() && (mlir::isa(type) || mlir::isa(inputs.front()))) + llvm::SmallVector merged; + if (!mergedInto(argument, merged)) { - inputs.erase(inputs.begin()); + return -1; } - }; - - if (aInputs.size() == bInputs.size() + 1) - { - dropBoundThis(a, aInputs); - } - else if (bInputs.size() == aInputs.size() + 1) - { - dropBoundThis(b, bInputs); - } - if (aResults != bResults || aInputs.size() != bInputs.size()) - { - return false; + work.append(merged.begin(), merged.end()); + continue; } - for (auto [index, pair] : llvm::enumerate(llvm::zip(aInputs, bInputs))) + int32_t index = -1; + if (readsParameterSlot(root, funcOp, index)) { - auto [aInput, bInput] = pair; - if (aInput != bInput && - !(index == 0 && (mlir::isa(aInput) || mlir::isa(bInput)))) + if ((param >= 0 && param != index) || isHandle(root) || isCopyableString(root.getType())) { - return false; + return -1; } - } - return true; - }; + param = index; + continue; + } - // a function out of `any`, or one function seen as another; a function seen as something - // that is no function (a vtable slot's reference to it) is the compiler's own - auto reinterprets = isOpaque(in) && !outFunctions.empty(); - if (!inFunctions.empty() && !outFunctions.empty()) - { - reinterprets = reinterprets || inFunctions.size() != outFunctions.size(); - for (auto [inFunction, outFunction] : llvm::zip(inFunctions, outFunctions)) + llvm::SmallVector recordParams; + if (borrowedParam(root) >= 0 || recordParamsOf(root, recordParams) != RecordFlow::None) { - reinterprets = reinterprets || !sameSignature(inFunction, outFunction); + return -1; } - } - if (reinterprets) - { - mention(inFunctions); - mention(outFunctions); + other = true; } - return reinterprets; - } - - // The interfaces an interface member's type mentions: a method's or a function field's - // parameters and result, a field's type, anything they hold (a class's fields included). - static void memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found) - { - llvm::DenseSet seen; - anyInterfaces(type, found, seen); - } - - static bool isNullish(mlir::Type type) - { - return mlir::isa(type); + return other ? param : -1; } - // May a cast from `in` to `out` make an interface value out of something that was not that - // interface's value already? A union or optional narrowed to its interface member (whose other - // members are null or undefined) or widened from it, null given a nullable interface type, a - // class or object seen as another (the same block, its fields as they are) and a function seen as - // another are not; a cast from another interface, `any`, an opaque pointer, or a tuple or an - // array whose element is not the same interface is. - static bool castMakesInterface(mlir::Type in, mlir::Type out, int depth = 0) + bool OwnershipSignaturePass::keeps(mlir::Operation *user, mlir::Value used) { - // null seen as an interface value has no vtable: a call through it reaches nothing - if (in == out || depth > 16 || isNullish(in)) - { - return false; - } - - llvm::StringSet<> made; - valueInterfaces(out, made); - if (made.empty()) + // a number, a boolean, a string literal: nothing to keep + if (holdsNoBlock(used)) { return false; } - if (mlir::isa(out)) - { - return !holdsAsIs(in, out, /*onlyNullable=*/true); - } - - if (mlir::isa(out)) + if (auto storeOp = mlir::dyn_cast(user)) { - if (isNullish(in) || holdsAsIs(out, in)) + // a field a borrowing record borrows (spec 32): the record keeps nothing + if (isBorrowedRecordFieldStore(user)) { return false; } - llvm::SmallVector members; - if (auto unionType = mlir::dyn_cast(in)) - { - members.append(unionType.getTypes().begin(), unionType.getTypes().end()); - } - else if (auto optionalType = mlir::dyn_cast(in)) - { - members.push_back(optionalType.getElementType()); - } - else - { - return true; - } - - return !llvm::all_of(members, [&](mlir::Type member) { return isNullish(member) || holdsAsIs(out, member); }); + auto ref = storeOp.getReference(); + return storeOp.getValue() == used && (isPlace(ref) || ref.getDefiningOp()); } - auto tupleFields = [](mlir::Type type) -> std::optional> { - if (auto tupleType = mlir::dyn_cast(type)) - { - return tupleType.getFields(); - } - - if (auto constTupleType = mlir::dyn_cast(type)) - { - return constTupleType.getFields(); - } - - return std::nullopt; - }; - - if (auto outFields = tupleFields(out)) + if (auto pushOp = mlir::dyn_cast(user)) { - auto inFields = tupleFields(in); - if (!inFields || inFields->size() != outFields->size()) - { - return true; - } - - for (auto [inField, outField] : llvm::zip(*inFields, *outFields)) - { - if (castMakesInterface(inField.type, outField.type, depth + 1)) - { - return true; - } - } - - return false; + return llvm::is_contained(pushOp.getItems(), used); } - auto elementOf = [](mlir::Type type) -> mlir::Type { - if (auto arrayType = mlir::dyn_cast(type)) - { - return arrayType.getElementType(); - } - - if (auto constArrayType = mlir::dyn_cast(type)) - { - return constArrayType.getElementType(); - } - - return {}; - }; - - if (auto outElement = elementOf(out)) + // `new Shared(x)` moves `x` into the block, as a store into a place does (spec 23.1) + if (auto sharedNewOp = mlir::dyn_cast(user)) { - auto inElement = elementOf(in); - return !inElement || castMakesInterface(inElement, outElement, depth + 1); + return sharedNewOp.getValue() == used; } - // a reference seen as another (`!ts.bound_ref` as `!ts.ref`): what it points to - auto pointee = [](mlir::Type type) -> mlir::Type { - if (auto refType = mlir::dyn_cast(type)) - { - return refType.getElementType(); - } - - if (auto valueRefType = mlir::dyn_cast(type)) - { - return valueRefType.getElementType(); - } - - if (auto boundRefType = mlir::dyn_cast(type)) - { - return boundRefType.getElementType(); - } - - return {}; - }; + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return llvm::is_contained(unshiftOp.getItems(), used); + } - if (auto outPointee = pointee(out)) + if (auto spliceOp = mlir::dyn_cast(user)) { - auto inPointee = pointee(in); - return !inPointee || castMakesInterface(inPointee, outPointee, depth + 1); + return llvm::is_contained(spliceOp.getItems(), used); } - // anything else reinterpreted: what is read out of it was never checked - return true; - } + if (isCall(user)) + { + auto args = callArgs(user); + return llvm::any_of(ownedParams(user), [&](int32_t index) { + return static_cast(index) < args.size() && args[index] == used; + }); + } - // The function a vtable slot holds: `ts.SymbolRef`, seen through casts. Null for anything else. - static mlir::StringAttr slotFunction(mlir::Value value) - { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + // `let r = init; ... r = f(r, v)`: a local that owns what it is declared from and is given + // another value is no borrow of the parameter - the assignment releases what it holds - so + // the parameter moves into it (spec 33: `___array_reduce`'s accumulator). Not a string: the + // local copies it (spec 22) + if (auto varOp = mlir::dyn_cast(user)) { - value = castOp.getIn(); + return varOp.getInitializer() == used && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false) && + isAssigned(varOp) && !isCopyableString(used.getType()); } - auto symbolRefOp = value ? value.getDefiningOp() : mlir_ts::SymbolRefOp(); - return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + return false; } - static mlir_ts::AddressOfOp addressOfThroughCasts(mlir::Value value) + llvm::SmallVector OwnershipSignaturePass::localOwnedParams(mlir_ts::FuncOp funcOp) { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + llvm::SmallVector owned; + if (funcOp.getBody().empty()) { - value = castOp.getIn(); + return owned; } - return value ? value.getDefiningOp() : mlir_ts::AddressOfOp(); - } - - // The words of the module's exported declarations (`__decls*` globals): what another module - // can name. - static llvm::StringSet<> exportedWords(mlir::ModuleOp module) - { - llvm::StringSet<> words; - module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!globalOp.getSymName().starts_with("__decls")) + for (auto argument : funcOp.getBody().front().getArguments()) + { + // a handle kept is a counted copy: its caller gives nothing up (spec 23.2) + if (isHandle(argument)) { - return; + continue; } - globalOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { - auto text = mlir::dyn_cast(constantOp.getValue()); - if (!text) + auto kept = false; + for (auto *user : argument.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index)) { - return; + continue; } - llvm::StringRef rest = text.getValue(); - while (!rest.empty()) + for (auto *slotUser : varOp.getResult().getUsers()) { - auto isWordChar = [](char c) { return llvm::isAlnum(c) || c == '_' || c == '$'; }; - rest = rest.drop_while([&](char c) { return !isWordChar(c); }); - auto word = rest.take_while(isWordChar); - if (!word.empty()) + if (auto loadOp = mlir::dyn_cast(slotUser)) { - words.insert(word); + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + kept = kept || keeps(use, used); + }); } - - rest = rest.drop_front(word.size()); } - }); - }); - - return words; - } + } - // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a - // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the - // slots of a class's own vtable that hold the address of one of its interface vtables - struct InterfaceVTable - { - mlir_ts::TupleType type; - llvm::DenseMap slots; - // a field's slot: the object literal's field it is the offset of - llvm::DenseMap> fields; - bool written = false; - }; - - // What collectInterfaceFamilies learns of the module, one step after another. - struct InterfaceScan - { - // the interfaces open whatever their vtables hold - llvm::StringSet<> open; - llvm::StringMap vtables; - // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) - llvm::SmallVector unread; - // interface name -> the interface vtables class vtables hold for it, read through an - // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) - llvm::StringMap> classHeld; - // interface name -> the vtables its values are made with - llvm::StringMap> madeWith; - // An interface value made out of another one reaches the other's members: both are open. One - // made out of `any` or an opaque pointer may be any interface value put into one: every - // interface so boxed is open with it, and every interface at all when it is untraceable. - bool allOpen = false; - llvm::StringSet<> boxed; - llvm::StringSet<> unboxedInto; - bool unboxedFunction = false; - // the interfaces a cast makes a value of out of another interface's or an opaque one, and - // the ones it came from: closed over their members' types below - llvm::StringSet<> castOpened; - // interface -> the interfaces its members' types mention: its vtables' slots and what is - // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields - // included - llvm::StringMap> memberInterfaces; - llvm::DenseSet tied; - - void openAll(const llvm::StringSet<> &names) - { - for (auto &entry : names) + if (kept) { - open.insert(entry.getKey()); + owned.push_back(argument.getArgNumber()); } } - }; - void collectInterfaceFamilies(mlir::ModuleOp module) - { - InterfaceScan scan; - readInterfaceVTables(module, scan); - traceInterfaceValues(module, scan); - openExposedInterfaces(module, scan); - openThroughMembers(scan); - openLooseVTables(scan); - buildInterfaceFamilies(scan); - collectFieldFunctions(module); - markInterfaceReached(scan); + return owned; } - // The vtables: each interface vtable's slots, and the interface vtables a class's own vtable holds. - void readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan) + void OwnershipSignaturePass::computeFacts() { - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto name = globalOp.getSymName(); - if (!name.ends_with("..vtbl")) - { - return; - } - - auto owner = name.drop_back(llvm::StringRef("..vtbl").size()); - llvm::DenseMap values; - globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { - if (insertOp.getPosition().size() == 1) - { - values[insertOp.getPosition()[0]] = insertOp.getValue(); - } - }); - - auto header = slotFunction(values.lookup(0)); - auto tupleType = mlir::dyn_cast(globalOp.getType()); - if (!header || !tupleType) - { - scan.unread.push_back(name); - return; - } + llvm::DenseMap pinned; + for (auto round = 0; round < 16; ++round) + { + // which object literals borrow depends on what the calls stored into them return, and a + // function's facts on which literals borrow (spec 32) + classifyRecordBoxes(); - if (header.getValue() == (owner + "..instanceOf").str()) + for (auto &entry : functions) { - // an exported class (not in an executable, whose exports no module sees) - auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); - ownClasses.insert(owner); - if (exposed) - { - nameableClasses.insert(owner); - } - - for (auto &[position, value] : values) + if (!entry.second.isDeclaration()) { - auto addressOfOp = addressOfThroughCasts(value); - auto id = position < static_cast(tupleType.size()) - ? mlir::dyn_cast_or_null(tupleType.getFieldInfo(position).id) - : mlir::StringAttr(); - if (!addressOfOp || !id) + auto result = localResultBorrows(entry.second); + Facts facts; + facts.borrows = std::move(result.borrows); + facts.record = result.record; + facts.owned = localOwnedParams(entry.second); + // what the body takes into something on every path; a parameter it returns on + // some paths only is kept too, and released on the others (spec 33) + takenParams[entry.second] = facts.owned; + if (auto mixed = mixedReturnParam(entry.second); mixed >= 0 && !llvm::is_contained(facts.owned, mixed)) { - continue; + facts.owned.insert(llvm::upper_bound(facts.owned, mixed), mixed); } - scan.classHeld[id.getValue()].insert(addressOfOp.getGlobalName()); - if (exposed) - { - scan.open.insert(id.getValue()); // another module may extend the class - } + factsLocal[entry.second] = std::move(facts); } - - return; } - if (!header.getValue().ends_with("..instanceOf") && header.getValue() != ".instanceOf.none") - { - scan.unread.push_back(name); - return; - } + meetFacts(); - auto &vtable = scan.vtables[name]; - vtable.type = tupleType; - for (auto &[position, value] : values) + llvm::DenseMap next; + for (auto &call : calls) { - if (position > 0) + auto facts = callFacts(call.callees); + if (facts.empty()) { - vtable.slots[position] = slotFunction(value); - if (auto field = slotField(value); field.first) - { - vtable.fields[position] = field; - } - } - } - }); - } - - // What makes an interface value, out of which vtable; the casts that box, unbox or reinterpret one; - // the interfaces its members' types mention; the vtables written. - void traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan) - { - auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { - // the global itself - if (auto addressOfOp = addressOfThroughCasts(value)) - { - return scan.vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); - } - - // a copy of it on the heap, its absent members' slots patched - while (auto castOp = value.getDefiningOp()) - { - value = castOp.getIn(); - } - - if (auto newOp = value.getDefiningOp()) - { - llvm::StringRef copied; - for (auto *user : newOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() - ? storeOp.getValue().getDefiningOp() - : mlir_ts::LoadOp(); - auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); - if (addressOfOp && scan.vtables.count(addressOfOp.getGlobalName())) - { - if (!copied.empty()) - { - return {}; - } - - copied = addressOfOp.getGlobalName(); - } - } - - return copied; - } - - return {}; - }; - - // an object's class vtable read at run time: `ts.VTableOffsetRef(ts.Load(ts.PropertyRef(object, 0)), k)` - auto readFromClass = [](mlir::Value value) { - auto offsetRefOp = value.getDefiningOp(); - auto loadOp = offsetRefOp ? offsetRefOp.getVtable().getDefiningOp() : mlir_ts::LoadOp(); - auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - return propertyRefOp && propertyRefOp.getPosition() == 0 && - mlir::isa(propertyRefOp.getObjectRef().getType()); - }; - - // what reads a vtable's address: making an interface value, a class vtable's slot, the copy - std::function onlyMakesValues = [&](mlir::Value address) { - return llvm::all_of(address.getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (auto newInterfaceOp = mlir::dyn_cast(user)) - { - return newInterfaceOp.getInterfaceVTable() == use.get(); - } - - if (mlir::isa(user)) - { - return onlyMakesValues(user->getResult(0)); - } - - if (mlir::isa(user)) - { - return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; - } - - // the copy: read whole, and stored into a block `ts.New` made - if (auto loadOp = mlir::dyn_cast(user)) - { - return llvm::all_of(loadOp->getUsers(), [&](mlir::Operation *loadUser) { - auto storeOp = mlir::dyn_cast(loadUser); - return storeOp && storeOp.getValue() == loadOp.getResult() && - storeOp.getReference().getDefiningOp(); - }); - } - - return false; - }); - }; - - auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; - module.walk([&](mlir::Operation *op) { - if (auto newInterfaceOp = mlir::dyn_cast(op)) - { - auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); - auto vtable = newInterfaceOp.getInterfaceVTable(); - if (auto found = vtableOf(vtable); !found.empty()) - { - scan.madeWith[name].insert(found); - } - else if (readFromClass(vtable)) - { - // any class's vtable for the interface (classHeld, all in its families below) - if (!scan.classHeld.count(name)) - { - scan.open.insert(name); - } - } - else if (auto extractOp = vtable.getDefiningOp()) - { - scan.castOpened.insert(name); - valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); - } - else - { - scan.allOpen = true; - } - - return; - } - - if (auto castOp = mlir::dyn_cast(op)) - { - auto in = castOp.getIn().getType(); - auto out = castOp.getType(); - if (isOpaque(out)) - { - valueInterfaces(in, scan.boxed); - } - - if (castMakesInterface(in, out)) - { - valueInterfaces(out, scan.castOpened); - valueInterfaces(in, scan.castOpened); - if (isOpaque(in)) - { - valueInterfaces(out, scan.unboxedInto); - } - } - - // a function value reinterpreted carries the interface values of its parameters and - // results across unseen (review6 x1c, x1r) - llvm::StringSet<> signatures; - auto boxing = false; - if (castReinterpretsFunction(in, out, signatures, boxing)) - { - scan.openAll(signatures); - if (isOpaque(in)) - { - scan.unboxedFunction = true; - } - } - else if (boxing) - { - for (auto &entry : signatures) - { - scan.boxed.insert(entry.getKey()); - } - } - - return; - } - - if (auto interfaceRefOp = mlir::dyn_cast(op)) - { - if (!interfaceRefOp->use_empty()) - { - scan.open.insert(interfaceRefOp.getIdentifier()); - } - - return; - } - - if (auto symbolRefOp = mlir::dyn_cast(op)) - { - if (auto interfaceType = mlir::dyn_cast(symbolRefOp.getInterfaceVal().getType())) - { - memberTypeInterfaces(symbolRefOp.getType(), scan.memberInterfaces[interfaceType.getName().getValue()]); - } - - return; - } - - if (auto addressOfOp = mlir::dyn_cast(op)) - { - if (auto found = scan.vtables.find(addressOfOp.getGlobalName()); found != scan.vtables.end() && !onlyMakesValues(addressOfOp.getResult())) - { - found->second.written = true; - } - } - }); - } - - // The interfaces another module sees, and the ones a value boxed into `any` may be unboxed into. - void openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan) - { - // seen from another module: a word of its exported declarations, or in a reachable type. An - // executable's exports are seen by no other module (USER decision, spec 31): only what it - // imports - a declared function, an imported global - comes from outside. - if (!executable) - { - exportWords = exportedWords(module); - } - - llvm::StringSet<> exposed; - llvm::DenseSet seen; - for (auto &entry : functions) - { - // a `public` method of a class no other module can name is not one: only a declaration - // re-parsed from `__decls` names a function of another module - auto exported = !executable && (entry.second->hasAttr("export") || entry.second->hasAttr(DLL_EXPORT)); - if (entry.second.isDeclaration() || exported) - { - anyInterfaces(entry.second.getFunctionType(), exposed, seen, &nameableClasses); - } - } - - for (auto &entry : globals) - { - auto globalOp = entry.second; - // another module's global, declared here with no initializer (an import of a .ts module - // carries `export`) - auto imported = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || - globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); - auto exported = !globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT); - if (imported || (exported && !executable)) - { - anyInterfaces(globalOp.getType(), exposed, seen, &nameableClasses); - } - } - - scan.openAll(exposed); - if (!scan.unboxedInto.empty() || scan.unboxedFunction) - { - scan.openAll(scan.boxed); - for (auto &entry : scan.boxed) - { - scan.castOpened.insert(entry.getKey()); - } - } - } - - // Every interface the members of one made out of another mention, recursively. - void openThroughMembers(InterfaceScan &scan) - { - // An interface value made out of another one (or out of `any`) is called with the other's - // members: `HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` hands an `Op` to a - // function that calls through `Op2` (review6r1 v5, v11f). So every interface either side's - // members mention is open too, and theirs, recursively (controller ruling, spec 31). - // - // A class's vtables for its interfaces are its members' families whether or not this module - // makes an interface value of the class (a method called directly then keeps its facts), and - // their slots' types are the interface's members' types: an interface whose values are all - // made from class objects is read through them (review6r2 d1-d3). - for (auto &[name, held] : scan.classHeld) - { - scan.madeWith[name].insert(held.begin(), held.end()); - } - - for (auto &[name, made] : scan.madeWith) - { - for (auto vtableName : made) - { - if (auto found = scan.vtables.find(vtableName); found != scan.vtables.end() && found->second.type) - { - for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) - { - if (position > 0) - { - memberTypeInterfaces(field.type, scan.memberInterfaces[name]); - } - } - } - } - } - - llvm::SmallVector work; - for (auto &entry : scan.castOpened) - { - work.push_back(entry.getKey().str()); - } - - llvm::StringSet<> expanded; - while (!work.empty()) - { - auto name = work.pop_back_val(); - if (!expanded.insert(name).second) - { - continue; - } - - scan.open.insert(name); - for (auto &member : scan.memberInterfaces[name]) - { - work.push_back(member.getKey().str()); - } - } - } - - // An interface with a vtable this pass could not tie to its values. - void openLooseVTables(InterfaceScan &scan) - { - // A vtable this pass could not read, or one no value of any interface is made with, that is - // named for an interface (`...vtbl`, `...vtbl`): the interface may have an - // implementation outside its families. Belt and braces - every way to make an interface - // value is traced above - but a missed member would get its family's facts. - llvm::DenseSet made; - for (auto &entry : scan.madeWith) - { - made.insert(entry.second.begin(), entry.second.end()); - } - - llvm::SmallVector loose(scan.unread.begin(), scan.unread.end()); - for (auto &entry : scan.vtables) - { - if (!made.contains(entry.getKey())) - { - loose.push_back(entry.getKey()); - } - } - - for (auto &entry : scan.madeWith) - { - auto name = entry.getKey().str(); - if (llvm::any_of(loose, [&](llvm::StringRef vtableName) { - return vtableName.ends_with("." + name + "..vtbl") || vtableName.starts_with(name + "."); - })) - { - scan.open.insert(entry.getKey()); - } - } - } - - // The families of each interface's slots - its methods and its function-typed fields - closed - // where nothing opened the interface. - void buildInterfaceFamilies(InterfaceScan &scan) - { - auto &words = exportWords; - auto isOpen = [&](llvm::StringRef name) { - // a namespaced interface is declared by its last name - auto last = name.rsplit('.').second; - return scan.allOpen || scan.open.contains(name) || words.contains(name) || (!last.empty() && words.contains(last)); - }; - - // the families - for (auto &[name, made] : scan.madeWith) - { - // a class vtable's slot holding a vtable this pass did not read (another module's) - auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return scan.vtables.count(vtableName); }); - auto closed = !isOpen(name) && allRead; - for (auto vtableName : made) - { - if (!scan.vtables.count(vtableName)) - { - continue; - } - - if (allRead) - { - interfaceVTableTypes[name].push_back(scan.vtables[vtableName].type); - } - - scan.tied.insert(vtableName); - auto &vtable = scan.vtables[vtableName]; - for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) - { - if (position == 0) - { - continue; - } - - auto &family = interfaceFamilies[{name.str(), static_cast(position)}]; - auto id = mlir::dyn_cast_or_null(field.id); - auto member = vtable.slots.lookup(position); - auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp(); - if (!family.field) - { - family.field = id; - } - - // a member that is not private (a `public` method) is open (findOpen): it has no - // facts to give, and gives the family none - family.closed = family.closed && closed && !vtable.written && id && family.field == id && funcOp && - !funcOp.isDeclaration() && !declaresCallerFacts(funcOp); - if (member && !llvm::is_contained(family.members, member)) - { - family.members.push_back(member); - } - - // the same slot as a field: an object literal's field of function type - auto &fieldFamily = interfaceFieldFamilies[{name.str(), static_cast(position)}]; - auto target = vtable.fields.lookup(position); - auto refType = mlir::dyn_cast(field.type); - if (!fieldFamily.field) - { - fieldFamily.field = id; - } - - fieldFamily.closed = fieldFamily.closed && closed && !vtable.written && id && fieldFamily.field == id && - target.first && refType && mlir::isa(refType.getElementType()); - if (target.first && !llvm::is_contained(fieldFamily.targets, target)) - { - fieldFamily.targets.push_back(target); - } - } - } - } - } - - // What an open family holds, and what a vtable no interface value is made with holds. - void markInterfaceReached(InterfaceScan &scan) - { - // what an open family holds, and a vtable no interface value is made with, is reached - // through calls that are not resolved - auto reached = [&](mlir::StringAttr member) { - if (auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp()) - { - interfaceReached.insert(funcOp); - } - }; - - for (auto &[key, family] : interfaceFamilies) - { - if (!family.closed) - { - llvm::for_each(family.members, reached); - } - } - - for (auto &entry : scan.vtables) - { - if (!scan.tied.contains(entry.getKey())) - { - for (auto &slot : entry.second.slots) - { - reached(slot.second); - } - } - } - } - static constexpr const char *INTERFACE_REACHED_WHY = - "it is reached through an interface, whose calls are not given its facts"; - - // The parameters a function's result borrows and the parameters it keeps (spec 2.4); absent is - // rc's convention. A plain borrow names one parameter; a borrowing record (`record`, spec 32) - // any number, and its caller owns the box. - struct Facts - { - llvm::SmallVector borrows; - llvm::SmallVector owned; - bool record = false; - - bool empty() const - { - return borrows.empty() && owned.empty(); - } - - bool operator==(const Facts &other) const - { - return borrows == other.borrows && owned == other.owned && record == other.record; - } - - bool operator!=(const Facts &other) const - { - return !(*this == other); - } - }; - - // Each function's facts, from its body (local) and as its callers see them (effective: closed, - // and agreed by every family it is in). - llvm::DenseMap factsLocal; - llvm::DenseMap factsEffective; - // The parameters each body takes into something - a field, an element, a global, a keeping call, - // an owning local it assigns (localOwnedParams). A kept parameter that is not one of them is - // released on the paths that do not move it (OWN_PARAMS_RELEASED_ATTR_NAME, spec 33). - llvm::DenseMap> takenParams; - - // ---- Facts a signature says (`@keeps`, `@borrows`, `@lends`) ---- - // - // A function that says them has them whoever calls it: a caller in another module reads them - // from the declaration it re-parses. Its body is checked against them instead (checkDeclared). - - static llvm::ArrayRef declared(mlir::Operation *funcOp, llvm::StringRef name) - { - if (auto attr = funcOp->getAttrOfType(name)) - { - return attr.asArrayRef(); - } - - return {}; - } - - static bool declaresFacts(mlir::Operation *funcOp) - { - return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || - funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); - } - - // The facts `@keeps` and `@lends` say (Facts): what callers move in, and what the result is a - // borrow of. - static bool declaresCallerFacts(mlir::Operation *funcOp) - { - return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); - } - - // The parameters `@lends` names: one for a plain borrow, any number for a borrowing record - // (spec 32). - static llvm::SmallVector declaredLends(mlir::Operation *funcOp) - { - auto lends = declared(funcOp, OWN_DECLARED_LENDS_ATTR_NAME); - llvm::SmallVector sorted(lends.begin(), lends.end()); - llvm::sort(sorted); - sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end()); - return sorted; - } - - // What every function a call names declares under `name`, when they all declare the same; - // nothing when one of them does not. - llvm::SmallVector declaredByCallees(const Callees &callees, llvm::StringRef name) - { - llvm::SmallVector agreed; - for (auto [index, symbol] : llvm::enumerate(callees.named)) - { - // a function, or the global holding the address of one a library exports (a dynamic - // import), which MLIRGen gave the declaration's markers - mlir::Operation *declaring = functions.lookup(symbol.getValue()); - if (!declaring) - { - declaring = globals.lookup(symbol.getValue()); - } - - if (!declaring || !declaring->hasAttr(name)) - { - return {}; - } - - auto indices = declared(declaring, name); - if (index == 0) - { - agreed.assign(indices.begin(), indices.end()); - } - else if (llvm::ArrayRef(agreed) != indices) - { - return {}; - } - } - - return agreed; - } - - // The parameters, for an error, by the names MLIRGen recorded beside the marker. - static std::string describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices) - { - if (indices.empty()) - { - return "nothing"; - } - - auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); - std::string text; - llvm::raw_string_ostream os(text); - for (auto [position, index] : llvm::enumerate(indices)) - { - os << (position ? ", " : "") << "'"; - if (names && static_cast(index) < names.size()) - { - os << mlir::cast(names[index]).getValue(); - } - else - { - os << "#" << index; - } - - os << "'"; - } - - return os.str(); - } - - // Functions this module defines that `@borrows`, and those whose state was found to borrow. - llvm::DenseMap> borrowsFound; - - // The argument of `funcOp` that `value` is, or a field of: its own value, its slot's, or a - // read out of either. -1 for anything else. - static int32_t parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp) - { - auto root = rootOf(value); - if (auto loadOp = root.getDefiningOp()) - { - if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) - { - return parameterOf(propertyRefOp->getOperand(0), funcOp); - } - - auto varOp = loadOp.getReference().getDefiningOp(); - int index = -1; - if (varOp && isParameterSlot(varOp, index) && varOp->getParentOfType() == funcOp) - { - return index; - } - - return -1; - } - - auto argument = mlir::dyn_cast(root); - if (argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) - { - return static_cast(argument.getArgNumber()); - } - - return -1; - } - - // ---- The iterator protocol ---- - // - // `it.next()` returns a `{value, done}` record, and under own the caller owns what it holds, - // whoever implements it: a generator's `next` moves what it yields into the record (a yield of - // a borrow is an error in its body), and any other `next` is a function whose callers this pass - // cannot all see, which may not return a borrow. So the result is fresh even when the call goes - // through the `Iterator` interface, or into another module - the default library's `matchAll`. - - // A call of a function named `next` - read through an interface, out of a field, or by its - // symbol - whose one result is a `{value, done}` record. - static bool isIteratorNext(mlir::Operation *op) - { - if (op->getNumResults() != 1) - { - return false; - } - - auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); - if (!tupleType || tupleType.getFields().size() != 2) - { - return false; - } - - auto fieldName = [&](int index) { - auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); - return id ? id.getValue() : llvm::StringRef(); - }; - if (fieldName(0) != "value" || fieldName(1) != "done") - { - return false; - } - - auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; - if (auto callee = directCallee(op)) - { - return isNext(callee.getValue()); - } - - auto function = calleeValue(op); - if (!function) - { - return false; - } - - if (auto getMethodOp = function.getDefiningOp()) - { - function = getMethodOp.getBoundFunc(); - } - - if (auto boundOp = function.getDefiningOp()) - { - function = boundOp.getFunc(); - } - - if (auto loadOp = function.getDefiningOp()) - { - if (auto refOp = loadOp.getReference().getDefiningOp()) - { - auto identifier = mlir::dyn_cast(refOp.getIdentifier()); - return identifier && isNext(identifier.getValue()); - } - - if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) - { - auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); - if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) - { - return false; - } - - auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); - return id && isNext(id.getValue()); - } - - return false; - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - return false; - } - - // A generator another module made: its state object's type is only its shape here, and what each - // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is - // known in that module alone. A release routine made here from the shape would give back what is - // not the state's. So this module's gives back none of it: only the `.captured` box's cells, and - // their values too unless the maker says it borrows them (`@borrows`). What the generator's own - // locals still hold when it is given up early leaks rather than being freed twice. - void findImportedStates(mlir::ModuleOp module) - { - llvm::MapVector states; - for (auto &call : calls) - { - if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) - { - continue; - } - - auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); - auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); - states[tupleType] = states.lookup(tupleType) || borrows; - } - - if (states.empty()) - { - return; - } - - auto *context = &getContext(); - llvm::SmallVector entries; - if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) - { - entries.append(existing.begin(), existing.end()); - } - - for (auto &[type, borrows] : states) - { - auto tupleType = mlir::cast(type); - auto captured = fieldIndex(tupleType, CAPTURED_NAME); - // the box first, when it borrows; -1 names no field, so a box that owns is released - llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) - { - fields.push_back(static_cast(index)); - } - } - - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); - } - - module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } - - // A call whose result is a generator's state object (an object whose fields are `.step` and - // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the - // caller owns - in another module too, as the iteration that follows needs. - static bool makesGeneratorState(mlir::Operation *op) - { - if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) - { - return false; - } - - auto tupleType = storageTupleOfType(op->getResult(0).getType()); - if (!tupleType) - { - return false; - } - - auto hasStep = false; - auto hasNext = false; - for (auto field : tupleType.getFields()) - { - if (auto id = mlir::dyn_cast_or_null(field.id)) - { - hasStep = hasStep || id.getValue() == ".step"; - hasNext = hasNext || id.getValue() == "next"; - } - } - - return hasStep && hasNext; - } - - // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple - static mlir_ts::TupleType storageTupleOfType(mlir::Type type) - { - if (auto objectType = mlir::dyn_cast(type)) - { - if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) - { - return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); - } - - // an object a declaration names by its shape (another module's generator) - return mlir::dyn_cast(objectType.getStorageType()); - } - - if (auto refType = mlir::dyn_cast(type)) - { - type = refType.getElementType(); - } - else if (auto valueRefType = mlir::dyn_cast(type)) - { - type = valueRefType.getElementType(); - } - - return mlir::dyn_cast(type); - } - - // ---- Fields a generator yields ---- - // - // A generator's locals are fields of its state object, and `yield groups` puts what one holds into - // the `{value, done}` record its `next` returns: a borrow of the state, which the caller cannot - // keep. When the field is read for nothing but yields, and each value stored into it is yielded - // before the generator can stop (later in the same block), the yield moves the value instead: - // the read is fresh (OWN_FRESH_RESULT_ATTR_NAME) and the field owns nothing - // (OWN_YIELDED_FIELDS_ATTR_NAME). - - // `ts.Store(value, ts.PropertyRef(record, i))`, where `record` is a local a `ts.Constant` - // initializes: a field of a record built to be returned, a yield's. - static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value) - { - auto storeOp = mlir::dyn_cast(user); - auto propertyRefOp = storeOp && storeOp.getValue() == value - ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); - } - - void findYieldedFields(mlir::ModuleOp module) - { - // (state type, field) -> its reads and its stores - struct FieldUses - { - llvm::SmallVector reads; - llvm::SmallVector stores; - bool other = false; - }; - - llvm::MapVector, FieldUses> fields; - module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { - auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); - auto position = static_cast(propertyRefOp.getPosition()); - if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || - !mlir::isa(propertyRefOp.getType()) || - !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) - { - return; - } - - auto &uses = fields[{tupleType, position}]; - for (auto *user : propertyRefOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - uses.reads.push_back(loadOp); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) - { - uses.stores.push_back(storeOp); - } - else if (!mlir::isa(user)) - { - uses.other = true; - } - } - }); - - // a field the signature pass already found borrowing is the caller's, not the generator's - llvm::DenseSet> borrowing; - if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) - { - for (auto entry : entries.getAsRange()) - { - for (auto index : mlir::cast(entry[1]).asArrayRef()) - { - borrowing.insert({mlir::cast(entry[0]).getValue(), index}); - } - } - } - - llvm::MapVector> yielded; - for (auto &[key, uses] : fields) - { - if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) - { - continue; - } - - // read only to be put into a yield's record - auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { - auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { - return !mlir::isa(user); - }); - return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); - }); - - // each value stored is yielded later in its block: nothing between stops the generator - auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { - return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { - return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); - }); - }); - - if (!onlyYielded || !eachYielded) - { - continue; - } - - yielded[key.first].push_back(key.second); - for (auto loadOp : uses.reads) - { - loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); - } - } - - if (yielded.empty()) - { - return; - } - - auto *context = &getContext(); - llvm::SmallVector entries; - for (auto &[type, indices] : yielded) - { - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); - } - - module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } - - // A function that says `@borrows` and makes no state itself, but returns what a call that has - // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters - // the call's bounded arguments are, or are read out of. - void findBoundedForwarders() - { - llvm::DenseMap calleesOf; - for (auto &call : calls) - { - calleesOf[call.op] = &call.callees; - } - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) - { - continue; - } - - llvm::SmallVector found; - auto forwards = true; - for (auto value : returnedValues(funcOp)) - { - if (holdsNoBlock(value)) - { - continue; - } - - auto *call = rootOf(value).getDefiningOp(); - auto known = call ? calleesOf.find(call) : calleesOf.end(); - if (known == calleesOf.end()) - { - forwards = false; - break; - } - - llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); - if (bounded.empty() && known->second->funcs.size() == 1 && !known->second->viaCallback) - { - auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); - bounded.assign(made.begin(), made.end()); - } - - if (bounded.empty()) - { - forwards = false; - break; - } - - auto args = callArgs(call); - for (auto index : bounded) - { - auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; - if (param < 0) - { - forwards = false; - break; - } - - if (!llvm::is_contained(found, param)) - { - found.push_back(param); - } - } - } - - if (forwards && !found.empty()) - { - llvm::sort(found); - borrowsFound[funcOp] = found; - } - } - } - - // A body that does other than its signature says, and an override that says other than the - // method it overrides: either way a caller that trusts the signature frees twice or leaks. - void checkDeclared() - { - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration()) - { - continue; - } - - if (funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME) && funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) - { - funcOp.emitError("'") << funcOp.getSymName() - << "' cannot say both '@" LENDS_DECORATOR "' and '@" BORROWS_DECORATOR - "': its caller owns a result that borrows, and nothing of one it is lent"; - signalPassFailure(); - continue; - } - - if (declaresCallerFacts(funcOp)) - { - auto local = factsLocal.lookup(funcOp); - auto keeps = declared(funcOp, OWN_DECLARED_KEEPS_ATTR_NAME); - auto lends = declaredLends(funcOp); - if (local.borrows != lends) - { - if (lends.empty()) - { - funcOp.emitError("'") << funcOp.getSymName() << "' returns a borrow of " - << describeParams(funcOp, local.borrows) - << ", which it has to say with '@" LENDS_DECORATOR "'"; - } - else if (local.borrows.empty()) - { - funcOp.emitError("'@" LENDS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) - << ", but its body does not return a borrow of it"; - } - else - { - funcOp.emitError("'@" LENDS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) - << ", but its body returns a borrow of " << describeParams(funcOp, local.borrows); - } - - signalPassFailure(); - } - else if (llvm::ArrayRef(local.owned) != keeps) - { - if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) - { - funcOp.emitError("'@" KEEPS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, keeps) << ", but its body keeps " - << describeParams(funcOp, local.owned); - } - else - { - funcOp.emitError("'") << funcOp.getSymName() << "' keeps " << describeParams(funcOp, local.owned) - << ", which it has to say with '@" KEEPS_DECORATOR "'"; - } - - signalPassFailure(); - } - } - - if (funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) - { - auto says = declared(funcOp, OWN_DECLARED_BORROWS_ATTR_NAME); - auto found = borrowsFound.find(funcOp); - if (found == borrowsFound.end()) - { - funcOp.emitError("'@" BORROWS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, says) - << ", but it neither makes a generator whose state borrows its arguments nor returns one"; - signalPassFailure(); - } - else if (llvm::ArrayRef(found->second) != says) - { - funcOp.emitError("'@" BORROWS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, says) - << ", but the state it makes borrows " << describeParams(funcOp, found->second); - signalPassFailure(); - } - } - } - - // the members of a family a declaring method is in: each says the same - for (auto &[key, members] : families) - { - mlir_ts::FuncOp declaring; - for (auto member : members) - { - if (auto funcOp = functions.lookup(member.getValue()); funcOp && declaresFacts(funcOp)) - { - declaring = funcOp; - break; - } - } - - if (!declaring) - { - continue; - } - - for (auto member : members) - { - auto funcOp = functions.lookup(member.getValue()); - if (!funcOp || funcOp == declaring) - { - continue; - } - - for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_LENDS_ATTR_NAME}) - { - if (funcOp->hasAttr(name) != declaring->hasAttr(name) || declared(funcOp, name) != declared(declaring, name)) - { - funcOp.emitError("'") << funcOp.getSymName() << "' overrides '" << declaring.getSymName() - << "', and has to say the same '@" KEEPS_DECORATOR "', '@" BORROWS_DECORATOR - "' and '@" LENDS_DECORATOR "'"; - signalPassFailure(); - break; - } - } - } - } - } - - // Functions that destroy nothing their caller can reach. - llvm::DenseSet noDrops; - - // A class's vtable is a global named `..vtbl` that holds its `..instanceOf`. An - // interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call - // through an interface does not name what it reaches. - void collectClassVTables(mlir::ModuleOp module) - { - llvm::SmallVector>>> vtables; - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto entries = classVTableEntries(globalOp); - if (!entries.empty()) - { - auto name = globalOp.getSymName(); - classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); - vtables.push_back({name, std::move(entries)}); - } - }); - - for (auto &[name, entries] : vtables) - { - for (auto [position, symbol] : entries) - { - families[{position, methodName(symbol.getValue())}].push_back(symbol); - } - } - } - - // Does a method reached through an open interface keep a parameter or return a borrow - // (interfaceReached)? Then its callers through the interface cannot be told, and it has no facts - // at all. - bool factsThroughInterface(mlir::Operation *funcOp, const Facts &facts) - { - return !facts.empty() && interfaceReached.contains(funcOp); - } - - mlir_ts::TupleType storageTupleOf(mlir::Type type) - { - return own_facts::storageTupleOf(&getContext(), type); - } - - // The functions a class vtable holds, by position; empty for any other global. - static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) - { - llvm::SmallVector> entries; - if (!globalOp.getSymName().ends_with("..vtbl")) - { - return entries; - } - - auto isClass = false; - globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { - auto symbolRefOp = insertOp.getValue().getDefiningOp(); - if (!symbolRefOp || insertOp.getPosition().size() != 1) - { - return; - } - - auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); - isClass = isClass || symbol.getValue().ends_with("..instanceOf"); - entries.push_back({insertOp.getPosition()[0], symbol}); - }); - - if (!isClass) - { - entries.clear(); - } - - return entries; - } - - static bool isClassVTableEntry(mlir::Operation *op) - { - auto globalOp = op->getParentOfType(); - return globalOp && !classVTableEntries(globalOp).empty(); - } - - // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An - // override keeps its base's method name, which is what puts both in one family. - std::string methodName(llvm::StringRef symbol) - { - size_t best = 0; - for (auto &entry : classNames) - { - auto name = entry.getKey(); - if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && - symbol[name.size()] == '.') - { - best = name.size() + 1; - } - } - - return symbol.drop_front(best).str(); - } - - llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier) - { - llvm::SmallVector members; - if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) - { - members = found->second; - } - - if (!llvm::is_contained(members, identifier)) - { - members.push_back(identifier); - } - - return members; - } - - // Adds `symbol`'s function to `callees`; false when it is not defined in this module. - bool addDefined(mlir::StringAttr symbol, Callees &callees) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || funcOp.isDeclaration()) - { - return false; - } - - if (!llvm::is_contained(callees.funcs, funcOp)) - { - callees.funcs.push_back(funcOp); - } - - return true; - } - - // A function declared here and defined in a library that exported its `__own_no_drops`: one - // linked with this module, through its import library. Or a C function declared - // `@nodrops` (OWN_DECLARED_NO_DROPS_ATTR_NAME), or one whose parameters and result hold no - // heap block (scalarDeclaration). - bool addImported(mlir::StringAttr symbol, Callees &callees) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || !funcOp.isDeclaration() || - !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME) || - (scalarDeclaration(funcOp) && !functionsEscape))) - { - return false; - } - - callees.imported = true; - callees.importedName = symbol.getValue(); - return true; - } - - // A number, a boolean, a character, or an enum or a literal of one: a value that holds no block - // and no function of this module, so given to foreign code it hands it neither. Not an opaque - // pointer, a reference, a string, a class, an interface, an object, a tuple, an array, a union - // with such a member, a function, `any` or `unknown`. - static bool scalarType(mlir::Type type) - { - if (auto literalType = mlir::dyn_cast(type)) - { - return scalarType(literalType.getElementType()); - } - - if (auto enumType = mlir::dyn_cast(type)) - { - return scalarType(enumType.getElementType()); - } - - return mlir::isa(type); - } - - // Every parameter and result scalar (a void result too), and no rest parameter. - static bool scalarSignature(mlir_ts::FunctionType functionType) - { - return !functionType.isVarArg() && llvm::all_of(functionType.getInputs(), scalarType) && - llvm::all_of(functionType.getResults(), - [](mlir::Type type) { return mlir::isa(type) || scalarType(type); }); - } - - // A body-less `declare function` of C (no `export` or import marker: not another TypeScript - // module's, whose declarations carry `export`) whose parameters and result are scalar - // (scalarType): given no block of this module, it can destroy none (controller ruling, spec 31: - // `sqrt`, `floor`, `pow`, `fabs`). The risk is a C function that calls back into the module - // (`signal(15, f as Opaque)` then `raise(15)`, review6r1 c3), which it can only do with a - // function it was given - so the rule is off in a module whose foreign surface could hand it - // one (functionsEscape) - or an exported function reached by name, which a `@nodrops` - // declaration takes on as well. - static bool scalarDeclaration(mlir_ts::FuncOp funcOp) - { - return funcOp.isDeclaration() && !funcOp->hasAttr("export") && !funcOp->hasAttr("import") && - !funcOp->hasAttr(DLL_EXPORT) && !funcOp->hasAttr(DLL_IMPORT) && scalarSignature(funcOp.getFunctionType()); - } - - // `ts.Load(ts.AddressOf @f)`, where the global `f` holds the address of a function exported - // by a library loaded at run time, looked up by name: that function, if the library exported - // its `__own_no_drops`. - bool addImportedByAddress(mlir::Value callee, Callees &callees) - { - auto loadOp = callee.getDefiningOp(); - auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - if (!addressOfOp) - { - return false; - } - - auto globalOp = globals.lookup(addressOfOp.getGlobalName()); - if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) - { - return false; - } - - // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` - auto ®ion = globalOp.getInitializerRegion(); - if (!region.hasOneBlock()) - { - return false; - } - - auto resultOp = mlir::dyn_cast(region.front().getTerminator()); - auto castOp = resultOp && resultOp->getNumOperands() == 1 - ? resultOp->getOperand(0).getDefiningOp() - : mlir_ts::CastOp(); - auto searchOp = castOp ? castOp.getIn().getDefiningOp() - : mlir_ts::SearchForAddressOfSymbolOp(); - auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); - auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); - if (!name || !importedNoDrops.contains(name.getValue())) - { - return false; - } - - callees.imported = true; - callees.importedName = name.getValue(); - return true; - } - - Callees resolve(mlir::Operation *op) - { - Callees callees; - if (auto callee = directCallee(op)) - { - callees.named.push_back(callee.getAttr()); - callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); - } - else if (mlir::isa(op)) - { - resolveValue(calleeValue(op), callees); - } - - if (!callees.known) - { - callees.funcs.clear(); - } - - return callees; - } - - // The function value a call goes through: a method of a class, virtual or not, a symbol, or - // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an - // interface's method - is unknown. - void resolveValue(mlir::Value callee, Callees &callees) - { - // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function - // keeps; a method of a class imported so is bound to its object first - auto function = callee; - if (auto getMethodOp = function.getDefiningOp()) - { - function = getMethodOp.getBoundFunc(); - } - - if (auto boundOp = function.getDefiningOp()) - { - function = boundOp.getFunc(); - } - - auto loadOp = function.getDefiningOp(); - if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) - { - callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); - } - - if (addImportedByAddress(callee, callees)) - { - callees.known = true; - return; - } - - if (auto getMethodOp = callee.getDefiningOp()) - { - callee = getMethodOp.getBoundFunc(); - } - - auto *def = callee.getDefiningOp(); - mlir::StringAttr identifier; - int64_t index = -1; - if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (isInstanceOfSlot(def)) - { - callees.known = true; - callees.instanceOf = true; - return; - } - else if (auto symbol = fieldFunctions.of(def)) - { - callees.known = addDefined(symbol, callees); - callees.fieldFunction = callees.known; - return; - } - else if (auto interfaceRefOp = mlir::dyn_cast_or_null(def)) - { - resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); - return; - } - else if (auto loadOp = mlir::dyn_cast_or_null(def)) - { - resolveCallback(loadOp, callees); - return; - } - else if (auto boundOp = mlir::dyn_cast_or_null(def)) - { - // a function-typed field of an interface, called: `ts.CreateBoundFunction( - // ts.ExtractInterfaceThis(i), ts.Load(ts.InterfaceSymbolRef(i, f)))` - auto loadOp = boundOp.getFunc().getDefiningOp(); - auto refOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::InterfaceSymbolRefOp(); - auto thisOp = boundOp.getThisVal().getDefiningOp(); - if (refOp && isInterfaceField(refOp.getResult()) && thisOp && thisOp.getInterfaceVal() == refOp.getInterfaceVal()) - { - resolveInterfaceSlot(refOp, interfaceFieldFamilies, callees); - } - - return; - } - - if (!identifier) - { - return; - } - - if (index < 0) - { - callees.named.push_back(identifier); - callees.known = addDefined(identifier, callees) || addImported(identifier, callees); - return; - } - - callees.named = familyOf(index, identifier); - resolveFamily(index, identifier, callees); - } - - // A method of a closed interface (interfaceFamilies), `ts.InterfaceSymbolRef(value, index)`, or a - // function-typed field of one (interfaceFieldFamilies), read and called: the family of the slot - // after the vtable's header - every function the vtables hold there, or every function the - // implementing literals' fields are ever given. Open, the call is unknown, as before. - template - void resolveInterfaceSlot(mlir_ts::InterfaceSymbolRefOp refOp, const std::map, Family> &families, - Callees &callees) - { - auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); - auto identifier = mlir::dyn_cast(refOp.getIdentifier()); - if (!interfaceType || !identifier) - { - return; - } - - auto position = static_cast(refOp.getIndex()) + INTERFACE_VTABLE_HEADER_SLOTS; - auto found = families.find({interfaceType.getName().getValue().str(), position}); - if (found == families.end() || !found->second.closed || found->second.field != identifier || - found->second.members.empty()) - { - return; - } - - callees.named.assign(found->second.members.begin(), found->second.members.end()); - callees.viaInterface = true; - callees.known = true; - for (auto member : found->second.members) - { - callees.known = addDefined(member, callees) && callees.known; - } - } - - // Can a module other than this one call the function? A `public` method (not private, not - // exported itself) of a class no other module can name - not exported, not a word of the - // exported declarations, not in the type of an exported or declared function or of an exported - // or imported global (nameableClasses) - no: another module calls a function of this one through - // the declaration `__decls` gives it (controller ruling, spec 31). But a method of a class that - // extends a class another module can name, or one another module defines, yes, private or not: - // that module calls it through the base's vtable. - bool callableFromOutside(mlir_ts::FuncOp funcOp) - { - if (!funcOp.isPrivate() && (funcOp.isDeclaration() || funcOp->hasAttr("export") || funcOp->hasAttr(DLL_EXPORT))) - { - return true; - } - - // its class: the longest own class name that prefixes its symbol - auto symbol = funcOp.getSymName(); - llvm::StringRef owner; - for (auto &entry : ownClasses) - { - auto name = entry.getKey(); - if (name.size() > owner.size() && symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.') - { - owner = name; - } - } - - if (owner.empty()) - { - return !funcOp.isPrivate(); - } - - auto nameable = [&](llvm::StringRef name) { - auto last = name.rsplit('.').second; - return nameableClasses.contains(name) || exportWords.contains(name) || (!last.empty() && exportWords.contains(last)); - }; - - if (!funcOp.isPrivate() && nameable(owner)) - { - return true; - } - - // An override of a method of a nameable class, or of one another module defines (no own - // vtable here), fills a slot of that class's vtable: another module calls it through the - // base (review6r1 p5, p6, p7; controller ruling: any base of that kind on the chain). The - // base is the first field of a derived class's storage. - auto storage = classStorages.lookup(owner); - if (!storage) - { - // its storage is not known here: no telling what it extends - return !funcOp.isPrivate(); - } - - for (auto depth = 0; depth < 64; ++depth) - { - auto fields = storage.getFields(); - auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); - if (!base) - { - return false; - } - - auto name = base.getName().getValue(); - if (nameable(name) || !ownClasses.contains(name)) - { - return true; - } - - storage = base; - } - - return true; - } - - // Can foreign code learn of a function of this module, to call it back during a call of a - // scalar declaration (scalarDeclaration)? It can only through the module's foreign surface, or - // through a function turned into data (controller ruling, spec 31, fix round 5: a closed-world - // test, in place of listing the routes a function value takes out). So the rule is on only when: - // - every foreign symbol is scalar (scalarType): every body-less function (C's, another - // module's - `import`, a DLL's - a `declare class`'s, a `@cpp` class's slot; a rest parameter - // is not scalar), every global declared with no initializer or imported (a `declare var`, a - // `declare class`'s vtable and type name), every `@cpp` class's fields and its bases', with - // no method of its own (C++ calls one through the C++ vtable), every function found in a - // library at run time (`SearchForAddressOfSymbol`, seen through its cast); an opaque pointer - // or a reference anywhere in such a type is a value of foreign memory, not scalar; - // - no cast makes an opaque pointer or a reference out of a number (`0x1234`, then a - // store through it reaches foreign memory); - // - no cast makes a value of a type that holds no function (`any`, `unknown`, an opaque - // pointer, an integer, a reference; a boolean cannot carry one and does not count) out of one - // that holds one (`onSig as Opaque`, `k.m as u64`, `>k`; a class or object - // takes its fields along unless seen as a class or an interface); - // - no `inline_asm` or `call_intrinsic` is given a value whose type holds a function. - bool functionsEscape = false; - - // The types a holdsFunction walk has been through, with or without their fields. - using SeenTypes = llvm::DenseSet>; - - // Does a value of this type hold a function: the type itself, a union's or an optional's - // member, a tuple's field, an array's element, what a reference points to, an interface whose - // vtable holds one past its header - a method, a function-typed field, or a field whose type - // holds one, recursively (an interface with methods, a class with a function field, an array of - // them) - or whose vtable this module does not make values with (a foreign one: `declare - // function makeW(): W`), and with `fields` a class's or an object's fields. The header slot - // (`.instanceOf`, the compiler's own, which destroys nothing) does not count. - bool holdsFunction(mlir::Type type, bool fields, SeenTypes &seen) - { - if (!type || !seen.insert({type, fields}).second) - { - return false; - } - - if (mlir::isa(type)) - { - return true; - } - - if (auto interfaceType = mlir::dyn_cast(type)) - { - auto found = interfaceVTableTypes.find(interfaceType.getName().getValue()); - if (found == interfaceVTableTypes.end()) - { - return true; - } - - return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { - return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), - [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); - }); - } - - auto any = [&](llvm::ArrayRef infos) { - return llvm::any_of(infos, [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, fields, seen); }); - }; - - if (auto tupleType = mlir::dyn_cast(type)) - { - return any(tupleType.getFields()); - } - - if (auto constTupleType = mlir::dyn_cast(type)) - { - return any(constTupleType.getFields()); - } - - if (auto classStorageType = mlir::dyn_cast(type)) - { - return fields && any(classStorageType.getFields()); - } - - if (auto objectStorageType = mlir::dyn_cast(type)) - { - return fields && any(objectStorageType.getFields()); - } - - if (mlir::isa(type) && !fields) - { - return false; - } - - auto found = false; - type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { found = found || holdsFunction(sub, fields, seen); }); - return found; - } - - // The `@cpp` class storage a method's `this` is of, if any: the storage of its first - // parameter's class, when the method's symbol is `Class.name`. - mlir_ts::ClassStorageType cppClassOfMethod(mlir_ts::FuncOp funcOp) - { - auto inputs = funcOp.getFunctionType().getInputs(); - auto classType = inputs.empty() ? mlir_ts::ClassType() : mlir::dyn_cast(inputs.front()); - auto storage = classType ? mlir::dyn_cast(classType.getStorageType()) : mlir_ts::ClassStorageType(); - if (!storage || !storage.isCpp()) - { - return {}; - } - - auto name = storage.getName().getValue(); - auto symbol = funcOp.getSymName(); - return symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.' ? storage : mlir_ts::ClassStorageType(); - } - - // Does a value of this type carry an address of memory this module made - so a cast of it to - // an opaque pointer or a reference points into this module, not into foreign memory? - static bool addressType(mlir::Type type) - { - return mlir::isa(type); - } - - void findEscapingFunctions(mlir::ModuleOp module) - { - // every body-less function: C's, another module's, a `declare class`'s (its methods, its - // `..instanceOf`, its `..new`), a `@cpp` class's slot - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() && !scalarSignature(funcOp.getFunctionType())) - { - functionsEscape = true; - return; - } - - // a method of a `@cpp` class: C++ calls it through the class's C++ vtable - if (cppClassOfMethod(funcOp)) - { - functionsEscape = true; - return; - } - } - - // every global declared with no initializer or imported: a `declare var`, a `declare - // class`'s vtable (functions) and type name (a string) - for (auto &entry : globals) - { - auto globalOp = entry.second; - auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || - globalOp.getInitializerRegion().front().empty(); - if (foreign && !scalarType(globalOp.getType())) - { - functionsEscape = true; - return; - } - } - - // every `@cpp` class's fields; a base (the first field, a storage) is a `@cpp` class of its - // own, judged on its own - for (auto &entry : classStorages) - { - auto storage = entry.second; - if (!storage.isCpp()) - { - continue; - } - - for (auto [index, field] : llvm::enumerate(storage.getFields())) - { - auto base = index == 0 ? mlir::dyn_cast(field.type) : mlir_ts::ClassStorageType(); - if (base ? !base.isCpp() : !scalarType(field.type)) - { - functionsEscape = true; - return; - } - } - } - - module.walk([&](mlir::Operation *op) { - if (functionsEscape) - { - return; - } - - // a function found in a library at run time: scalar through every cast of its address - if (mlir::isa(op)) - { - functionsEscape = !llvm::all_of(op->getResult(0).getUsers(), [](mlir::Operation *user) { - auto castOp = mlir::dyn_cast(user); - auto functionType = castOp ? mlir::dyn_cast(castOp.getType()) : mlir_ts::FunctionType(); - return functionType && scalarSignature(functionType); - }); - return; - } - - // `inline_asm(...)` and `call_intrinsic(...)`: a function given to either is data - // of no type (review6r5 h1, h2: `inline_asm("mov $1, $0", "=r,r", onSig)`) - if (mlir::isa(op)) - { - functionsEscape = llvm::any_of(op->getOperands(), [&](mlir::Value operand) { - SeenTypes seen; - return holdsFunction(operand.getType(), /*fields=*/true, seen); - }); - return; - } - - auto castOp = mlir::dyn_cast(op); - if (!castOp) - { - return; - } - - // a vtable's address (a class's, an interface's, a thunk's cell) seen as an opaque - // pointer or an integer: the compiler's own, inside a vtable or storing an object's - // vtable, never a value a program gives C - auto vtableOp = op->getParentOfType(); - auto addressOfOp = addressOfThroughCasts(castOp.getIn()); - auto isVTable = [](llvm::StringRef name) { return name.ends_with("..vtbl") || name.contains("..vtbl."); }; - if ((vtableOp && isVTable(vtableOp.getSymName())) || (addressOfOp && isVTable(addressOfOp.getGlobalName()))) - { - return; - } - - // an opaque pointer or a reference made out of a number: an address of foreign memory - if (mlir::isa(castOp.getType()) && !addressType(castOp.getIn().getType())) - { - functionsEscape = true; - return; - } - - // a function seen as something that holds none; a boolean cannot carry one. A class or - // object takes its fields along, seen as anything (`any`, an opaque pointer, an integer, - // a reference: `>k`, then a load of the field, review6r5 g2) but a class - // (its base) or an interface (a view of it, read through its vtable only) - if (mlir::isa(castOp.getType())) - { - return; - } - - auto fields = !mlir::isa(castOp.getType()); - SeenTypes seenIn; - SeenTypes seenOut; - functionsEscape = holdsFunction(castOp.getIn().getType(), fields, seenIn) && - !holdsFunction(castOp.getType(), /*fields=*/true, seenOut); - }); - } - - // class name -> its storage, from every class type a function's signature or a global mentions - llvm::StringMap classStorages; - - void collectClassStorages(mlir::ModuleOp module) - { - llvm::DenseSet seen; - std::function visit = [&](mlir::Type type) { - if (!type || !seen.insert(type).second) - { - return; - } - - if (auto storage = mlir::dyn_cast(type)) - { - classStorages[storage.getName().getValue()] = storage; - for (auto &field : storage.getFields()) - { - visit(field.type); - } - - return; - } - - type.walkImmediateSubElements([](mlir::Attribute) {}, visit); - }; - - for (auto &entry : functions) - { - visit(entry.second.getFunctionType()); - } - - module.walk([&](mlir::Operation *op) { - for (auto result : op->getResults()) - { - if (mlir::isa(result.getType())) - { - visit(result.getType()); - } - } - }); - } - - // A class another module can see may be extended there, and its override is a candidate this - // module never sees: every member has to be private and defined here. - void resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) - { - callees.known = true; - for (auto member : familyOf(index, identifier)) - { - callees.known = addDefined(member, callees) && callees.known; - } - - callees.known = callees.known && llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp funcOp) { return !callableFromOutside(funcOp); }); - } - - // `ts.Cast(ts.VTableOffsetRef(vtable, 0))`, where `vtable` is the one an object's first word - // points to or an interface value's own: slot 0 of either is `.instanceOf` - a class's - // generated one, a string compare, or `.instanceOf.none` for an object literal behind an - // interface. Only mlirGenInstanceOfThroughVTable builds a call through it. Neither destroys - // anything. - static bool isInstanceOfSlot(mlir::Operation *def) - { - auto castOp = mlir::dyn_cast_or_null(def); - auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); - if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) - { - return false; - } - - // a class vtable, read out of an object's first word, or an interface's own vtable, - // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) - auto *vtable = slot.getVtable().getDefiningOp(); - if (auto classVTable = mlir::dyn_cast_or_null(vtable)) - { - return classVTable.getIndex() == 0; - } - - return mlir::isa_and_nonnull(vtable); - } - - // Is every use of this function value the callee of a call - through `ts.GetMethod`, with - // `ts.GetThis` reading the object beside it? - static bool onlyCalled(mlir::Value value) - { - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) && - calleeValue(user) == use.get() && use.getOperandNumber() == 0) - { - continue; - } - - if (mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) && onlyCalled(user->getResult(0))) - { - continue; - } - - return false; - } - - return true; - } - - // The closed world. A function is open when a caller this pass cannot see may reach it: it is - // not private (another module may call it), or its symbol is used other than by a direct call, - // a class vtable's entry, or a method reference used only as a callee. A virtual reference - // that escapes opens its whole family. - void findOpen(mlir::ModuleOp module) - { - for (auto &entry : functions) - { - if (callableFromOutside(entry.second)) - { - open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR - "' would say it in its signature"); - } - } - - // the module is a symbol table, which getSymbolUses does not look inside: walk its body - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - // an op this pass cannot read symbols through: nothing is closed - for (auto &entry : functions) - { - open.try_emplace(entry.second, "its callers cannot all be found"); - } - - return; - } - - for (auto &use : *uses) - { - auto symbol = use.getSymbolRef().getRootReference(); - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp) - { - continue; - } - - auto *user = use.getUser(); - if (mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) && isClassVTableEntry(user)) - { - continue; - } - - // passed only to a parameter whose family is closed: its calls are resolved (spec 33) - if (callbackPassing.contains(user)) - { - continue; - } - - int64_t index = -1; - if (auto refOp = mlir::dyn_cast(user)) - { - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast(user)) - { - index = refOp.getIndex(); - } - - auto inFunction = !!user->getParentOfType(); - if (inFunction && mlir::isa(user) && - onlyCalled(user->getResult(0))) - { - // a virtual call this pass cannot resolve reaches its family without the facts - if (index >= 0) - { - Callees callees; - resolveFamily(index, symbol, callees); - if (!callees.known) - { - openFamily(index, symbol, "an override may be defined in another module"); - } - } - - continue; - } - - open.try_emplace(funcOp, "it is used other than by a call"); - if (index >= 0) - { - openFamily(index, symbol, "it is used other than by a call"); - } - } - } - - void openFamily(int64_t index, mlir::StringAttr symbol, const char *why) - { - for (auto member : familyOf(index, symbol)) - { - if (auto memberOp = functions.lookup(member.getValue())) - { - open.try_emplace(memberOp, why); - } - } - } - - // ---- Callbacks (spec 33) ---- - // - // `___array_reduce(arr, f, init)` calls its parameter `f`: an indirect call, which reaches - // whatever function its callers pass. When the function is closed - private, and every use of its - // symbol a direct call - the parameter is only ever called, and every caller passes there a - // function this module defines, by its symbol or as a closure over a capture box made right - // there, a call through the parameter reaches exactly those functions: the parameter's family. - // Its members are closed too, when their symbols are used only to be called directly or passed - // so; then the call gets the facts the members agree on (callFacts), and `__own_no_drops` when - // none of them destroys anything. A closure's body takes its capture box first: its parameter k - // is the call's argument k - 1 (CallbackMember::shift). - // - // A kept parameter is agreed on the other way round from a class family's (meetCallbacks): a - // member that only borrows a parameter another member keeps is given it to keep too, and releases - // it on every path (OWN_PARAMS_RELEASED_ATTR_NAME) - `reduce` with a callback that returns its - // accumulator on some paths and one that never does, in one `___array_reduce`. Anything - // else the members disagree on demotes them all, as a class family's members are. - - struct CallbackMember - { - mlir::StringAttr symbol; - int32_t shift = 0; - }; - - struct CallbackFamily - { - llvm::SmallVector members; - // the ops that name a member's symbol to pass it here - llvm::SmallVector passing; - bool closed = true; - }; - - // (function, parameter index) -> the functions its callers pass there - std::map, CallbackFamily> callbackFamilies; - // the members whose families disagreed (meetFacts), for the error in their bodies - llvm::DenseSet demotedCallbacks; - // uses of a member's symbol that only pass it to a closed family: no escape (findOpen) - llvm::DenseSet callbackPassing; - - static bool isFunctionValueType(mlir::Type type) - { - return mlir::isa(type); - } - - // Is every use of this parameter - through its slot, never assigned or captured - the function - // a call goes through? - static bool onlyCalledParam(mlir::BlockArgument argument) - { - for (auto *user : argument.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) - { - return false; - } - - for (auto *slotUser : varOp->getUsers()) - { - if (mlir::isa(slotUser)) - { - continue; - } - - auto loadOp = mlir::dyn_cast(slotUser); - if (!loadOp) - { - return false; - } - - for (auto &use : loadOp->getUses()) - { - auto *call = use.getOwner(); - if (!isCall(call) || calleeValue(call) != use.get() || - llvm::is_contained(callArgs(call), use.get())) - { - return false; - } - } - } - } - - return true; - } - - // The function a caller passes as `value`: its symbol, seen as a function value through casts - // between function types, or a closure over a capture box built of it. Null for anything else. - static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift) - { - while (auto castOp = value.getDefiningOp()) - { - if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) - { - return {}; - } - - value = castOp.getIn(); - } - - if (auto refOp = value.getDefiningOp()) - { - shift = 0; - return refOp; - } - - auto boundOp = value.getDefiningOp(); - if (!boundOp || closureOfBox(boundOp.getThisVal()) != boundOp) - { - return {}; - } - - shift = 1; - return boundOp.getFunc().getDefiningOp(); - } - - // Does the function value `value` go only where `passes` allows - as an argument of a call - and - // through casts between function types, a closure built of it, and that closure's own retains - // and releases? - static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0) - { - if (depth > 8) - { - return false; - } - - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || passes(use)) - { - continue; - } - - auto castOp = mlir::dyn_cast(user); - auto boundOp = mlir::dyn_cast(user); - if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) - { - continue; - } - - if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && - passedOnly(boundOp.getResult(), passes, depth + 1)) - { - continue; - } - - return false; - } - - return true; - } - - void collectCallbackFamilies(mlir::ModuleOp module) - { - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - return; - } - - llvm::DenseMap> usersOf; - for (auto &use : *uses) - { - if (auto funcOp = functions.lookup(use.getSymbolRef().getRootReference().getValue())) - { - usersOf[funcOp].push_back(use.getUser()); - } - } - - auto callsOnly = [&](mlir_ts::FuncOp funcOp) { - auto &users = usersOf[funcOp]; - return !users.empty() && llvm::all_of(users, [&](mlir::Operation *user) { - auto callee = directCallee(user); - return callee && callee.getValue() == funcOp.getSymName(); - }); - }; - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() || callableFromOutside(funcOp) || declaresFacts(funcOp) || !callsOnly(funcOp)) - { - continue; - } - - for (auto argument : funcOp.getBody().front().getArguments()) - { - if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) - { - continue; - } - - auto index = static_cast(argument.getArgNumber()); - CallbackFamily family; - for (auto *user : usersOf[funcOp]) - { - auto args = callArgs(user); - auto shift = 0; - auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) - : mlir_ts::SymbolRefOp(); - auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); - if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) - { - family.closed = false; - break; - } - - auto symbol = member.getSymNameAttr(); - auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); - if (same != family.members.end() && same->shift != shift) - { - family.closed = false; - break; - } - - if (same == family.members.end()) - { - family.members.push_back({symbol, shift}); - } - - family.passing.push_back(refOp); - } - - callbackFamilies[{funcOp, index}] = std::move(family); - } - } - - // A member's symbol used anywhere else - a call through another value, a field, a vtable, a - // global - reaches callers this cannot see; so does one passed to a family that is not closed. - // Each family that has such a member is open, until nothing changes. - auto passesToClosed = [&](mlir::OpOperand &use) { - auto *call = use.getOwner(); - auto callee = directCallee(call); - auto calleeOp = callee ? functions.lookup(callee.getValue()) : mlir_ts::FuncOp(); - if (!calleeOp) - { - return false; - } - - // the argument this use is, by its operand's position - auto args = callArgs(call); - if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || - use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) - { - return false; - } - - auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); - auto found = callbackFamilies.find({calleeOp, index}); - return found != callbackFamilies.end() && found->second.closed; - }; - - for (auto changed = true; changed;) - { - changed = false; - for (auto &[key, family] : callbackFamilies) - { - if (!family.closed) - { - continue; - } - - for (auto &member : family.members) - { - auto memberOp = functions.lookup(member.symbol.getValue()); - auto escapes = llvm::any_of(usersOf[memberOp], [&](mlir::Operation *user) { - if (auto callee = directCallee(user); callee && callee.getValue() == member.symbol.getValue()) - { - return false; - } - - auto refOp = mlir::dyn_cast(user); - return !refOp || !user->getParentOfType() || - !passedOnly(refOp.getResult(), passesToClosed); - }); - - if (escapes) - { - family.closed = false; - changed = true; - break; - } - } - } - } - - for (auto &[key, family] : callbackFamilies) - { - if (family.closed) - { - callbackPassing.insert(family.passing.begin(), family.passing.end()); - } - } - } - - // A call through a parameter whose family is closed: its members. - void resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) - { - auto varOp = loadOp.getReference().getDefiningOp(); - auto funcOp = loadOp->getParentOfType(); - auto index = -1; - if (!varOp || !funcOp || !isParameterSlot(varOp, index)) - { - return; - } - - auto found = callbackFamilies.find({funcOp.getOperation(), index}); - if (found == callbackFamilies.end() || !found->second.closed || found->second.members.empty()) - { - return; - } - - callees.viaCallback = true; - callees.known = true; - for (auto &member : found->second.members) - { - callees.named.push_back(member.symbol); - callees.known = addDefined(member.symbol, callees) && callees.known; - callees.shifts.push_back(member.shift); - } - - callees.known = callees.known && callees.shifts.size() == callees.funcs.size(); - } - - // A member's facts as a call through its family sees them: each parameter k as argument - // k - shift. False when a fact is about the capture box itself, which the call does not pass. - static bool shiftFacts(const Facts &facts, int32_t shift, Facts &shifted) - { - shifted = Facts(); - shifted.record = facts.record; - for (auto [from, to] : {std::make_pair(&facts.owned, &shifted.owned), std::make_pair(&facts.borrows, &shifted.borrows)}) - { - for (auto index : *from) - { - if (index < shift) - { - return false; - } - - to->push_back(index - shift); - } - } - - return true; - } - - // Can the body of `funcOp` be given parameter `index` to keep and release it where it does not - // move it: a plain slot (isParameterSlot), a block, not a handle, not a string? - bool canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index) - { - if (funcOp.getBody().empty() || static_cast(index) >= funcOp.getNumArguments()) - { - return false; - } - - auto argument = funcOp.getBody().front().getArgument(index); - if (isHandle(argument) || isCopyableString(argument.getType()) || holdsNoBlock(argument)) - { - return false; - } - - return llvm::any_of(argument.getUsers(), [&](mlir::Operation *user) { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - return varOp && isParameterSlot(varOp, position) && position == index; - }); - } - - // The members of each closed callback family agree (the section above), by promotion of a kept - // parameter or by demotion. True when any member's facts changed. - bool meetCallbacks(llvm::DenseSet &demoted) - { - auto changed = false; - for (auto &[key, family] : callbackFamilies) - { - if (!family.closed) - { - continue; - } - - llvm::SmallVector members; - llvm::SmallVector seen; - auto shiftable = true; - for (auto &member : family.members) - { - auto funcOp = functions.lookup(member.symbol.getValue()); - Facts shifted; - shiftable = shiftable && funcOp && shiftFacts(factsEffective.lookup(funcOp), member.shift, shifted); - members.push_back(funcOp); - seen.push_back(shifted); - } - - if (shiftable && llvm::all_equal(seen)) - { - continue; - } - - // a parameter some member keeps, and each other one only borrows: kept by all - llvm::SmallVector kept; - for (auto &facts : seen) - { - kept.append(facts.owned.begin(), facts.owned.end()); - } - - llvm::sort(kept); - kept.erase(std::unique(kept.begin(), kept.end()), kept.end()); - auto promotable = shiftable && llvm::all_of(seen, [&](const Facts &facts) { - return facts.borrows == seen.front().borrows && facts.record == seen.front().record && - llvm::none_of(kept, [&](int32_t index) { return llvm::is_contained(facts.borrows, index); }); - }); - - for (auto [position, member] : llvm::enumerate(family.members)) - { - auto funcOp = members[position]; - // a member another caller reaches without the facts is given none (findOpen) - for (auto index : kept) - { - promotable = promotable && !demoted.contains(funcOp) && !open.contains(funcOp) && - !interfaceReached.contains(funcOp) && - (llvm::is_contained(seen[position].owned, index) || - canReleaseParam(funcOp, index + member.shift)); - } - } - - if (promotable) - { - for (auto [position, member] : llvm::enumerate(family.members)) - { - auto funcOp = members[position]; - auto facts = factsEffective.lookup(funcOp); - for (auto index : kept) - { - auto param = index + member.shift; - if (!llvm::is_contained(facts.owned, param)) - { - facts.owned.insert(llvm::upper_bound(facts.owned, param), param); - } - } - - if (facts != factsEffective.lookup(funcOp)) - { - factsEffective[funcOp] = facts; - changed = true; - } - } - - continue; - } - - for (auto funcOp : members) - { - if (funcOp && !declaresCallerFacts(funcOp)) - { - demoted.insert(funcOp); - if (factsEffective.erase(funcOp)) - { - changed = true; - } - } - } - } - - return changed; - } - - // ---- Results that borrow an argument ---- - - // The values a function returns: what is stored into the local its returns read (MLIRGen's - // result slot), or what a return hands back directly. - static llvm::SmallVector returnedValues(mlir_ts::FuncOp funcOp) - { - llvm::SmallVector values; - funcOp.walk([&](mlir_ts::ReturnInternalOp returnOp) { - for (auto operand : returnOp.getRetOperands()) - { - auto loadOp = operand.getDefiningOp(); - auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!slot || slot.getInitializer() || isOwningVariable(slot)) - { - values.push_back(operand); - continue; - } - - for (auto *user : slot.getResult().getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == slot.getResult()) - { - values.push_back(storeOp.getValue()); - } - } - } - }); - - return values; - } - - struct ResultBorrows - { - llvm::SmallVector borrows; - bool record = false; - }; - - // What the function's result borrows (spec 2.4, 32): the one parameter every heap value it returns - // is a borrow of, or, when every such value is a borrowing record, the parameters the records - // borrow. A result that holds no block (`null`, a number) agrees with any; a record on one path - // and anything else on another is nothing a caller can be told. - ResultBorrows localResultBorrows(mlir_ts::FuncOp funcOp) - { - auto found = -1; - auto record = false; - llvm::SmallVector params; - for (auto value : returnedValues(funcOp)) - { - if (holdsNoBlock(value)) - { - continue; - } - - // a handle returned is a counted copy, never a borrow (spec 23.2) - if (isHandle(value)) - { - return {}; - } - - switch (recordParamsOf(value, params)) - { - case RecordFlow::Bad: - return {}; - case RecordFlow::Record: - record = true; - continue; - case RecordFlow::None: - break; - } - - auto index = borrowedParam(value); - if (index < 0 || (found >= 0 && index != found)) - { - return {}; - } - - found = index; - } - - if (record && found >= 0) - { - return {}; - } - - if (record) - { - llvm::sort(params); - params.erase(std::unique(params.begin(), params.end()), params.end()); - return {params, true}; - } - - if (found >= 0) - { - return {{found}, false}; - } - - return {}; - } - - // The parameter a function returns as it is on some paths, where every other value it returns - // that holds a block is one it owns (spec 33): `return v.x > 2 ? acc : V.plus(acc, v)`. Such a - // parameter is kept: the callers move the argument in, the path that returns it moves it out, - // and every other path releases it. -1 for none - a parameter returned on every path is a - // borrowed result; two parameters returned, or a part of one (`acc.next`), or a borrowing - // record, have no such answer; a string is copied instead (spec 22). - int32_t mixedReturnParam(mlir_ts::FuncOp funcOp) - { - auto param = -1; - auto other = false; - auto returned = returnedValues(funcOp); - llvm::SmallVector work(returned.begin(), returned.end()); - llvm::DenseSet seen; - while (!work.empty()) - { - auto value = work.pop_back_val(); - if (!seen.insert(value).second || holdsNoBlock(value)) - { - continue; - } - - if (isHandle(value) || isCopyableString(value.getType())) - { - return -1; - } - - auto root = rootOf(value); - if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) - { - llvm::SmallVector merged; - if (!mergedInto(argument, merged)) - { - return -1; - } - - work.append(merged.begin(), merged.end()); - continue; - } - - int32_t index = -1; - if (readsParameterSlot(root, funcOp, index)) - { - if ((param >= 0 && param != index) || isHandle(root) || isCopyableString(root.getType())) - { - return -1; - } - - param = index; - continue; - } - - llvm::SmallVector recordParams; - if (borrowedParam(root) >= 0 || recordParamsOf(root, recordParams) != RecordFlow::None) - { - return -1; - } - - other = true; - } - - return other ? param : -1; - } - - // ---- Borrowing records (spec 32) ---- - // - // `{ thing: this, ray: ray, dist: d }` given as an interface: an object literal's box holding values - // it does not own beside values it does. Rust's `struct Hit<'a> { thing: &'a Th, .. }`: the box is - // its owner's - the caller's, when it is returned - and it may not outlive what it borrows. Each box - // is classified where it is made: the fields it borrows (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, on the - // box and on every interface made of it, whose tag then names a release routine that leaves them), - // and, when every one of them is a parameter, `this`, or a read out of one, those parameters - what - // a function returning it lends (OWN_RESULT_RECORD_ATTR_NAME). The inference bounds every use. - - struct RecordBox - { - llvm::SmallVector params; - // every borrowed field is a parameter's, `this`'s, a read out of one, or immortal - bool returnable = true; - }; - - llvm::DenseMap recordBoxes; - - void classifyRecordBoxes() - { - recordBoxes.clear(); - getOperation().walk([&](mlir_ts::NewOp newOp) { - newOp->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); - for (auto *user : newOp->getUsers()) - { - for (auto *viewUser : user->getUsers()) - { - if (mlir::isa(viewUser)) - { - viewUser->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); - } - } - } - - classifyRecordBox(newOp); - }); - } - - void classifyRecordBox(mlir_ts::NewOp newOp) - { - auto boxOp = recordBoxOf(newOp.getResult()); - auto varOp = boxOp == newOp ? recordLocalOf(newOp.getResult()) : mlir_ts::VariableOp(); - if (!varOp) - { - return; - } - - // the record read once, and only into its box (rc's retain of it beside) - mlir_ts::LoadOp read; - for (auto *user : varOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - if (read) - { - return; - } - - read = loadOp; - } - } - - for (auto *user : read->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (!mlir::isa(user) && !(storeOp && storeOp.getReference() == newOp.getResult())) - { - return; - } - } - - // the box seen only as interfaces: every release of it reads their tag - llvm::SmallVector interfaces; - for (auto &use : newOp->getUses()) - { - auto *user = use.getOwner(); - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) - { - continue; - } - - auto castOp = mlir::dyn_cast(user); - if (!castOp || !isView(castOp)) - { - return; - } - - for (auto *castUser : castOp->getUsers()) - { - auto newInterfaceOp = mlir::dyn_cast(castUser); - if (!newInterfaceOp || newInterfaceOp.getThisVal() != castOp.getResult()) - { - return; - } - - interfaces.push_back(castUser); - } - } - - if (interfaces.empty()) - { - return; - } - - // each field that holds a block: owned (made here, or nothing) or borrowed, never both - RecordBox info; - llvm::SmallVector borrowed; - auto mth = typeHelper(&getContext()); - for (auto *user : varOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - auto field = propertyRefOp.getResult(); - auto refType = mlir::dyn_cast(field.getType()); - if (!refType || !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) - { - continue; - } - - auto owns = false; - auto borrows = false; - for (auto *fieldUser : field.getUsers()) - { - if (mlir::isa(fieldUser)) - { - continue; - } - - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != field) - { - return; - } - - auto value = storeOp.getValue(); - switch (recordFieldSource(value)) - { - case RecordFieldSource::NoBlock: - continue; - case RecordFieldSource::Fresh: - owns = true; - continue; - case RecordFieldSource::Unknown: - return; - case RecordFieldSource::Immortal: - borrows = true; - continue; - case RecordFieldSource::Param: - case RecordFieldSource::Place: - if (auto param = borrowedParam(value); param >= 0) - { - info.params.push_back(param); - } - else - { - info.returnable = false; - } - - borrows = true; - continue; - case RecordFieldSource::Slot: - case RecordFieldSource::Cell: - info.returnable = false; - borrows = true; - continue; - } - } - - // a string is copied, not borrowed (spec 22) - if ((owns && borrows) || (borrows && isCopyableString(refType.getElementType()))) - { - return; - } - - if (borrows) - { - borrowed.push_back(propertyRefOp.getPosition()); - } - } - - if (borrowed.empty()) - { - return; - } - - llvm::sort(borrowed); - borrowed.erase(std::unique(borrowed.begin(), borrowed.end()), borrowed.end()); - llvm::sort(info.params); - info.params.erase(std::unique(info.params.begin(), info.params.end()), info.params.end()); - - auto attr = mlir::DenseI32ArrayAttr::get(&getContext(), borrowed); - newOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); - for (auto *newInterfaceOp : interfaces) - { - newInterfaceOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); - } - - recordBoxes[newOp.getOperation()] = std::move(info); - } - - enum class RecordFlow - { - None, // no borrowing record - Record, // borrowing records only (or values that hold no block) - Bad // a record that borrows what is not a parameter, or one beside anything else - }; - - // Whether `value` - through views, merges and the owning locals that hold it - is a borrowing - // record, and the parameters it borrows: a box made here, or a call's result that is one, whose - // arguments are this function's parameters or reads out of them. - RecordFlow recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms) - { - llvm::SmallVector work{value}; - llvm::SmallPtrSet seen; - auto record = false; - auto other = false; - while (!work.empty()) - { - // what holds no block, by its own type (a box's `value_ref` owns nothing by type, but - // the object its view shows does) - auto next = work.pop_back_val(); - auto current = rootOf(next); - if (!seen.insert(current).second || holdsNoBlock(next)) - { - continue; - } - - auto *def = current.getDefiningOp(); - if (auto found = def ? recordBoxes.find(def) : recordBoxes.end(); found != recordBoxes.end()) - { - if (!found->second.returnable) - { - return RecordFlow::Bad; - } - - params.append(found->second.params.begin(), found->second.params.end()); - record = true; - continue; - } - - if (def && isCall(def) && isRecordResult(def)) - { - auto args = callArgs(def); - for (auto index : resultBorrowsAll(def)) - { - if (static_cast(index) >= args.size()) - { - return RecordFlow::Bad; - } - - auto arg = args[index]; - if (argumentHoldsNoBlock(arg) || isImmortalGlobalRead(arg)) - { - continue; - } - - auto param = borrowedParam(arg); - if (param < 0) - { - return RecordFlow::Bad; - } - - params.push_back(param); - } - - record = true; - continue; - } - - if (auto argument = mlir::dyn_cast(current)) - { - llvm::SmallVector merged; - if (mergedInto(argument, merged)) - { - work.append(merged.begin(), merged.end()); - continue; - } - - other = true; - continue; - } - - // what an owning local holds: its initializer and every value stored into it - auto loadOp = mlir::dyn_cast_or_null(def); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) - { - if (varOp.getInitializer()) - { - work.push_back(varOp.getInitializer()); - } - - for (auto *user : varOp->getUsers()) - { - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == varOp.getResult()) - { - work.push_back(storeOp.getValue()); - } - } - - continue; - } - - other = true; - } - - if (!record) - { - return RecordFlow::None; - } - - return other ? RecordFlow::Bad : RecordFlow::Record; - } - - // ---- Parameters the callee keeps ---- - - // Does `user` take `used` - a read of a parameter - into something that outlives the call: a - // store into a field, an element or a global, an insertion into an array, or a call that keeps - // it in turn? A local that holds it, and the result it is returned as, do not count: the first - // is a borrow, the second a borrowed result. - static bool keeps(mlir::Operation *user, mlir::Value used) - { - // a number, a boolean, a string literal: nothing to keep - if (holdsNoBlock(used)) - { - return false; - } - - if (auto storeOp = mlir::dyn_cast(user)) - { - // a field a borrowing record borrows (spec 32): the record keeps nothing - if (isBorrowedRecordFieldStore(user)) - { - return false; - } - - auto ref = storeOp.getReference(); - return storeOp.getValue() == used && (isPlace(ref) || ref.getDefiningOp()); - } - - if (auto pushOp = mlir::dyn_cast(user)) - { - return llvm::is_contained(pushOp.getItems(), used); - } - - // `new Shared(x)` moves `x` into the block, as a store into a place does (spec 23.1) - if (auto sharedNewOp = mlir::dyn_cast(user)) - { - return sharedNewOp.getValue() == used; - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return llvm::is_contained(unshiftOp.getItems(), used); - } - - if (auto spliceOp = mlir::dyn_cast(user)) - { - return llvm::is_contained(spliceOp.getItems(), used); - } - - if (isCall(user)) - { - auto args = callArgs(user); - return llvm::any_of(ownedParams(user), [&](int32_t index) { - return static_cast(index) < args.size() && args[index] == used; - }); - } - - // `let r = init; ... r = f(r, v)`: a local that owns what it is declared from and is given - // another value is no borrow of the parameter - the assignment releases what it holds - so - // the parameter moves into it (spec 33: `___array_reduce`'s accumulator). Not a string: the - // local copies it (spec 22) - if (auto varOp = mlir::dyn_cast(user)) - { - return varOp.getInitializer() == used && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false) && - isAssigned(varOp) && !isCopyableString(used.getType()); - } - - return false; - } - - // The parameters the body keeps (spec 2.4, owned-by-callee): a read of the parameter's slot - - // never assigned - that something keeps. - static llvm::SmallVector localOwnedParams(mlir_ts::FuncOp funcOp) - { - llvm::SmallVector owned; - if (funcOp.getBody().empty()) - { - return owned; - } - - for (auto argument : funcOp.getBody().front().getArguments()) - { - // a handle kept is a counted copy: its caller gives nothing up (spec 23.2) - if (isHandle(argument)) - { - continue; - } - - auto kept = false; - for (auto *user : argument.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) - { - continue; - } - - for (auto *slotUser : varOp.getResult().getUsers()) - { - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - kept = kept || keeps(use, used); - }); - } - } - } - - if (kept) - { - owned.push_back(argument.getArgNumber()); - } - } - - return owned; - } - - // ---- The facts callers must know ---- - - // Owned parameters and a result that borrows only hold where every caller knows them, so the - // facts survive only on a closed function whose every family agrees, and families are - // demoted until they do: a member that loses its facts can make another family disagree. A - // caller that returns what such a call returns, or passes a parameter on to a kept one, gets - // the fact too, so the facts are recomputed with the calls pinned until nothing changes; if - // that does not settle, nobody gets any. - void computeFacts() - { - llvm::DenseMap pinned; - for (auto round = 0; round < 16; ++round) - { - // which object literals borrow depends on what the calls stored into them return, and a - // function's facts on which literals borrow (spec 32) - classifyRecordBoxes(); - - for (auto &entry : functions) - { - if (!entry.second.isDeclaration()) - { - auto result = localResultBorrows(entry.second); - Facts facts; - facts.borrows = std::move(result.borrows); - facts.record = result.record; - facts.owned = localOwnedParams(entry.second); - // what the body takes into something on every path; a parameter it returns on - // some paths only is kept too, and released on the others (spec 33) - takenParams[entry.second] = facts.owned; - if (auto mixed = mixedReturnParam(entry.second); mixed >= 0 && !llvm::is_contained(facts.owned, mixed)) - { - facts.owned.insert(llvm::upper_bound(facts.owned, mixed), mixed); - } - - factsLocal[entry.second] = std::move(facts); - } - } - - meetFacts(); - - llvm::DenseMap next; - for (auto &call : calls) - { - auto facts = callFacts(call.callees); - if (facts.empty()) - { - // a call this pass cannot resolve still has what its callees' signatures say - // A declaration's `@lends` is a plain borrow to its callers: its body, which would - // say whether the result is a record the caller owns (spec 32), is not here. A - // record's box then leaks, and nothing is freed twice. - auto keeps = declaredByCallees(call.callees, OWN_DECLARED_KEEPS_ATTR_NAME); - facts.owned.assign(keeps.begin(), keeps.end()); - if (auto lends = declaredByCallees(call.callees, OWN_DECLARED_LENDS_ATTR_NAME); !lends.empty()) - { - llvm::sort(lends); - lends.erase(std::unique(lends.begin(), lends.end()), lends.end()); - facts.borrows = lends; - } - } - - if (!facts.empty()) - { - next[call.op] = facts; - } - } - - auto settled = next == pinned; - pinned = std::move(next); - pinFacts(pinned); - if (settled) - { - return; - } - } - - factsEffective.clear(); - pinFacts(llvm::DenseMap()); - } - - void meetFacts() - { - factsEffective.clear(); - for (auto &[funcOp, facts] : factsLocal) - { - if (!facts.empty() && !open.contains(funcOp) && !factsThroughInterface(funcOp, facts)) - { - factsEffective[funcOp] = facts; - } - } - - // what a signature says stands, open or not; checkDeclared holds the body to it. But not a - // `@keeps` or `@lends` through an interface: the interface's callers are not told - // (interfaceReached). - for (auto &entry : functions) - { - if (declaresCallerFacts(entry.second)) - { - auto keeps = declared(entry.second, OWN_DECLARED_KEEPS_ATTR_NAME); - Facts said; - said.borrows = declaredLends(entry.second); - said.owned.assign(keeps.begin(), keeps.end()); - // a body here that returns a borrowing record of what `@lends` names: callers in this - // module own the box (spec 32); checkDeclared holds the body to the parameters - auto local = factsLocal.lookup(entry.second); - said.record = !said.borrows.empty() && local.record && local.borrows == said.borrows; - if (factsThroughInterface(entry.second, said)) - { - continue; - } - - factsEffective[entry.second] = said; - } - } - - // the members of a family that disagree lose their facts: a call through the family is - // given only what all of them say. A closed interface's slot is such a family too. - auto demote = [&](llvm::ArrayRef members) { - llvm::SmallVector seen; - for (auto member : members) - { - auto funcOp = functions.lookup(member.getValue()); - seen.push_back(funcOp ? factsEffective.lookup(funcOp) : Facts()); - } - - auto changed = false; - if (llvm::all_equal(seen)) - { - return changed; - } - - for (auto member : members) - { - if (auto funcOp = functions.lookup(member.getValue()); - funcOp && !declaresCallerFacts(funcOp) && factsEffective.erase(funcOp)) - { - changed = true; - } - } - - return changed; - }; - - // a member a callback family demoted is never promoted again: each step only adds or only - // takes away, so this settles - demotedCallbacks.clear(); - for (auto changed = true; changed;) - { - changed = false; - for (auto &[key, members] : families) - { - changed = demote(members) || changed; - } - - changed = meetCallbacks(demotedCallbacks) || changed; - - for (auto &[key, family] : interfaceFamilies) - { - if (family.closed) - { - changed = demote(family.members) || changed; - } - } - - for (auto &[key, family] : interfaceFieldFamilies) - { - if (family.closed) - { - changed = demote(family.members) || changed; - } - } - } - } - - Facts callFacts(const Callees &callees) - { - if (!callees.known || callees.instanceOf || callees.funcs.empty()) - { - return {}; - } - - // each member's facts as the call's arguments see them (a closure's body takes its box first) - if (callees.viaCallback) - { - std::optional agreed; - for (auto [funcOp, shift] : llvm::zip(callees.funcs, callees.shifts)) - { - Facts shifted; - if (!shiftFacts(factsEffective.lookup(funcOp), shift, shifted) || (agreed && *agreed != shifted)) - { - return {}; - } - - agreed = shifted; - } - - return agreed.value_or(Facts()); - } - - auto facts = factsEffective.lookup(callees.funcs.front()); - auto agree = llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp callee) { - return factsEffective.lookup(callee) == facts; - }); - return agree ? facts : Facts(); - } - - static void setFacts(mlir::Operation *op, const Facts &facts) - { - auto *context = op->getContext(); - op->removeAttr(OWN_RESULT_BORROWS_ATTR_NAME); - op->removeAttr(OWN_RESULT_RECORD_ATTR_NAME); - op->removeAttr(OWN_PARAMS_ATTR_NAME); - if (!facts.borrows.empty()) - { - op->setAttr(OWN_RESULT_BORROWS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.borrows)); - if (facts.record) - { - op->setAttr(OWN_RESULT_RECORD_ATTR_NAME, mlir::UnitAttr::get(context)); - } - } - - if (!facts.owned.empty()) - { - op->setAttr(OWN_PARAMS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.owned)); - } - } - - // The calls get the facts their callees agree on; each function gets its own, or the reason it - // lost those its body has, for the error there. - void pinFacts(const llvm::DenseMap &pinned) - { - for (auto &call : calls) - { - setFacts(call.op, pinned.lookup(call.op)); - } - - for (auto &[funcOp, local] : factsLocal) - { - auto effective = factsEffective.lookup(funcOp); - setFacts(funcOp, effective); - funcOp->removeAttr(OWN_FACTS_LOST_ATTR_NAME); - funcOp->removeAttr(OWN_PARAMS_RELEASED_ATTR_NAME); - // a signature's `@keeps` is checked against the body as it is (checkDeclared) - llvm::SmallVector released; - for (auto index : effective.owned) - { - if (!declaresCallerFacts(funcOp) && !llvm::is_contained(takenParams.lookup(funcOp), index)) - { - released.push_back(index); - } - } - - if (!released.empty()) - { - funcOp->setAttr(OWN_PARAMS_RELEASED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(&getContext(), released)); - } - - if (effective.empty() && !local.empty()) - { - // through an interface first: a marker would not help there (interfaceReached) - auto *why = factsThroughInterface(funcOp, local) ? INTERFACE_REACHED_WHY : open.lookup(funcOp); - if (!why && demotedCallbacks.contains(funcOp)) - { - why = "a function passed in its place disagrees"; - } - - funcOp->setAttr(OWN_FACTS_LOST_ATTR_NAME, - mlir::StringAttr::get(&getContext(), why ? why : "an override in its class family disagrees")); - } - } - } - - // ---- Generators that borrow (phase 7b) ---- - // - // A generator over a parameter that holds a block - `function* each(a: number[])` - keeps the - // parameter's cell in a box in its state object's `.captured` field. Under 5b's rule that is an - // escaping closure over a value that is the caller's, an error. Instead the state object - // borrows: the caller owns it, it may not outlive the argument (OWN_RESULT_BOUNDED_ATTR_NAME), - // and its release gives back neither the parameter's value nor any field that only ever holds - // what was read out of it (OWN_BORROWING_FIELDS_ATTR_NAME). A state type is unique to its - // generator - its `next` field's type names a per-site `object_storage` - so a fact per type is - // a fact per generator, which is what the per-type release routine needs. - - struct BorrowingState - { - mlir_ts::FuncOp maker; - int32_t captured = -1; - llvm::SmallVector bounded; - llvm::SetVector fields; - }; - - static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name) - { - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - if (auto id = mlir::dyn_cast_or_null(field.id); id && id.getValue() == name) - { - return static_cast(index); - } - } - - return -1; - } - - // The box the maker stores into the `.captured` field of the initial state it copies into - // `newOp`'s block: `ts.Store(box, ts.PropertyRef(state, captured))`, `state` a local whose only - // read fills the block. - static mlir::Value boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured) - { - mlir_ts::StoreOp fill; - for (auto *user : newOp->getUsers()) - { - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) - { - fill = storeOp; - } - } - - auto loadOp = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); - auto stateOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!stateOp) - { - return {}; - } - - mlir::Value box; - for (auto *user : stateOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp || propertyRefOp.getPosition() != captured) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || box) - { - return {}; - } - - box = storeOp.getValue(); - } - } - - auto boxOp = box ? box.getDefiningOp() : mlir_ts::VariableOp(); - return boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); - } - - // Is every caller of `funcOp` a direct call this pass sees? It is private, and each use of its - // symbol is a call, or a closure over it that is only retained and released - `.map(f)` builds - // one and calls its body straight through the box. - bool calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo = false) - { - if (!funcOp.isPrivate() && !publicToo) - { - return false; - } - - auto uses = mlir::SymbolTable::getSymbolUses(funcOp.getSymNameAttr(), &module.getBodyRegion()); - if (!uses) - { - return false; - } - - for (auto &use : *uses) - { - auto *user = use.getUser(); - if (mlir::isa(user)) - { - continue; - } - - // a method whose signature gives its callers the bound: called through its class's - // vtable or a reference to it, which the facts reach too (declaredByCallees) - if (publicToo && - (isClassVTableEntry(user) || - (mlir::isa(user) && - onlyCalled(user->getResult(0))))) - { - continue; - } - - auto symbolRefOp = mlir::dyn_cast(user); - if (!symbolRefOp) - { - return false; - } - - for (auto *refUser : symbolRefOp->getUsers()) - { - auto boundOp = mlir::dyn_cast(refUser); - if (!boundOp || !llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { - return mlir::isa(boundUser); - })) - { - return false; - } - } - } - - return true; - } - - // A parameter's cell: a captured local declared from an argument of `maker`'s entry block, that - // owns nothing (its value is the caller's). - static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) - { - auto argument = cellOp.getInitializer() ? mlir::dyn_cast(cellOp.getInitializer()) - : mlir::BlockArgument(); - if (!cellOp.getCaptured().value_or(false) || isOwningVariable(cellOp) || !argument || - !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) - { - return false; - } - - index = static_cast(argument.getArgNumber()); - return true; - } - - // `ts.Load` of one of `maker`'s parameter slots (isParameterSlot): `index` is its position. - static bool readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - int position = -1; - if (!varOp || !isParameterSlot(varOp, position) || varOp->getParentOfType() != maker) - { - return false; - } - - index = static_cast(position); - return true; - } - - // `ts.Load(ts.PropertyRef(argument, i))` of one of `maker`'s parameters: `index` is its position. - static bool readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp->getOperand(0)) : mlir::BlockArgument(); - if (!argument || !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) - { - return false; - } - - index = static_cast(argument.getArgNumber()); - return true; - } - - bool ownsHeap(mlir::Location location, mlir::Type type) - { - return own_facts::ownsHeap(location, type); - } - - void findBorrowingStates(mlir::ModuleOp module) - { - llvm::MapVector states; - llvm::DenseMap made; - module.walk([&](mlir_ts::NewOp newOp) { - auto valueRefType = mlir::dyn_cast(newOp.getType()); - auto tupleType = valueRefType ? mlir::dyn_cast(valueRefType.getElementType()) - : mlir_ts::TupleType(); - if (!tupleType) - { - return; - } - - ++made[tupleType]; - auto captured = fieldIndex(tupleType, CAPTURED_NAME); - auto maker = newOp->getParentOfType(); - // an open maker has callers this pass cannot give the bound to, unless its signature - // gives it to them (`@borrows`) - if (captured < 0 || !maker || - !calledOnlyDirectly(module, maker, /*publicToo=*/maker->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME))) - { - return; - } - - auto box = boxStoredIntoState(newOp, captured); - if (!box) - { - return; - } - - BorrowingState state{maker, captured}; - for (auto *user : box.getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp) - { - return; - } - - auto value = storeOp.getValue(); - int32_t index = -1; - if (auto cellOp = value.getDefiningOp(); cellOp && isParameterCell(cellOp, maker, index)) - { - auto cellType = mlir::cast(cellOp.getType()); - // a handle parameter's cell holds a count of its own (spec 23.2), which only - // the box's release gives back: a box holding one owns what it holds - if (MLIRTypeHelper::isSharedHandleType(cellType.getElementType())) - { - return; - } - - if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) - { - state.bounded.push_back(index); - } - - continue; - } - - // a copy of what a parameter holds in a field: `.map`'s maker is a closure body - // that copies its own box's fields into the generator's - if (!ownsHeap(value.getLoc(), value.getType())) - { - continue; - } - - // `this`, which the box holds by value (`.this`), not in a cell: read out of - // the parameter's slot - if (readsParameterSlot(value, maker, index)) - { - if (MLIRTypeHelper::isSharedHandleType(value.getType())) - { - return; - } - - if (!llvm::is_contained(state.bounded, index)) - { - state.bounded.push_back(index); - } - - continue; - } - - if (!readsParameterField(value, maker, index)) - { - return; // a box with anything but what the parameters hold owns what it holds - } - - if (!llvm::is_contained(state.bounded, index)) - { - state.bounded.push_back(index); - } - } - } - - if (!state.bounded.empty()) - { - states[tupleType] = state; - } - }); - - // made in one place only: the maker is the one function a bound can come from - states.remove_if([&](auto &entry) { return made[entry.first] != 1; }); - for (auto &[type, state] : states) - { - auto bounded = state.bounded; - llvm::sort(bounded); - borrowsFound[state.maker] = bounded; - } - if (states.empty()) - { - return; - } - - // Each store into a field of a state type, by field: what the field may be given. - llvm::DenseMap, llvm::SmallVector> stored; - module.walk([&](mlir_ts::StoreOp storeOp) { - auto propertyRefOp = storeOp.getReference().getDefiningOp(); - auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); - if (tupleType && states.count(tupleType)) - { - stored[{tupleType, propertyRefOp.getPosition()}].push_back(storeOp.getValue()); - } - }); - - for (auto &[type, state] : states) - { - auto tupleType = mlir::cast(type); - // least fixpoint: a field borrows once everything stored into it is a read of a borrowing - // cell or field - for (auto changed = true; changed;) - { - changed = false; - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - auto position = static_cast(index); - auto found = stored.find({type, position}); - if (position == state.captured || state.fields.contains(position) || found == stored.end() || - !ownsHeap(state.maker.getLoc(), field.type)) - { - continue; - } - - if (llvm::all_of(found->second, [&](mlir::Value value) { return readsBorrowed(value, tupleType, state); })) - { - state.fields.insert(position); - changed = true; - } - } - } - } - - auto *context = &getContext(); - llvm::SmallVector entries; - for (auto &[type, state] : states) - { - llvm::SmallVector fields{state.captured}; - fields.append(state.fields.begin(), state.fields.end()); - entries.push_back(mlir::ArrayAttr::get( - context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); - state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded)); - } - - module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } - - // Is `value` a read of what a state of `tupleType` borrows: the value in one of its box's cells - // (`ts.Load` of `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`), or a - // borrowing field's (`ts.Load(ts.PropertyRef(state, p))`)? - bool readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state) - { - auto loadOp = rootOf(value).getDefiningOp(); - if (!loadOp) - { - return false; - } - - auto isStateField = [&](mlir::Value ref, int32_t &position) { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp || storageTupleOf(propertyRefOp->getOperand(0).getType()) != tupleType) - { - return false; - } - - position = propertyRefOp.getPosition(); - return true; - }; - - int32_t position = -1; - if (isStateField(loadOp.getReference(), position)) - { - return state.fields.contains(position); - } - - // a field of the box the state holds, copied in by value, or a cell read out of it - auto isBoxField = [&](mlir::Value ref) { - auto fieldRef = ref.getDefiningOp(); - auto boxLoad = fieldRef ? fieldRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); - return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; - }; - - if (isBoxField(loadOp.getReference())) - { - return true; - } - - auto cellLoad = loadOp.getReference().getDefiningOp(); - return cellLoad && isBoxField(cellLoad.getReference()); - } - - // ---- Globals given a borrow ---- - - // A global initialized with a read of another global, a field or an element (`let k = a`, - // `let k = h.c`) would be a second owner of a block another owner keeps: the first overwrite - // of either frees what the other still reads, and both give it back at exit. rc counts the - // global's own reference (OwnedReturnConsumptionPass, claimGlobalInitializers); own cannot - // move the value out of its owner, and a global cannot hold a borrow. A handle is counted - // under own too. - void rejectBorrowedGlobalInitializers(mlir::ModuleOp module) - { - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto *block = globalOp.getInitializerBlock(); - auto resultOp = block && !block->empty() ? mlir::dyn_cast(block->getTerminator()) - : mlir_ts::GlobalResultOp(); - if (!resultOp || resultOp->getNumOperands() != 1) - { - return; - } - - auto value = resultOp->getOperand(0); - while (value.getDefiningOp() && isView(value.getDefiningOp())) - { - value = value.getDefiningOp()->getOperand(0); - } - - auto loadOp = value.getDefiningOp(); - auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; - // an immortal global's value (spec 25) is owned by no one: any number of globals hold it - if (!place || !mlir::isa(place) || - isHandle(value) || isConstantData(value) || isImmortalGlobalRead(value) || - !ownsHeap(value.getLoc(), value.getType())) - { - return; - } - - resultOp.emitError("the initializer of global '") - << globalOp.getSymName() - << "' reads a value another owner keeps; -mm=own cannot make a global a second owner"; - signalPassFailure(); - }); - } - - // ---- Drops ---- - - // Least fixpoint: a function drops nothing until its body or a call it makes shows it may. - void computeDrops() - { - llvm::DenseMap> callsIn; - for (auto &call : calls) - { - callsIn[call.op->getParentOfType()].push_back(&call); - } - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (!funcOp.isDeclaration() && !dropsInBody(funcOp)) - { - noDrops.insert(funcOp); - } - } - - for (auto changed = true; changed;) - { - changed = false; - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (!noDrops.contains(funcOp)) - { - continue; - } - - for (auto *call : callsIn[funcOp]) - { - if (!callMayDrop(call->callees)) + // a call this pass cannot resolve still has what its callees' signatures say + // A declaration's `@lends` is a plain borrow to its callers: its body, which would + // say whether the result is a record the caller owns (spec 32), is not here. A + // record's box then leaks, and nothing is freed twice. + auto keeps = declaredByCallees(call.callees, OWN_DECLARED_KEEPS_ATTR_NAME); + facts.owned.assign(keeps.begin(), keeps.end()); + if (auto lends = declaredByCallees(call.callees, OWN_DECLARED_LENDS_ATTR_NAME); !lends.empty()) { - continue; + llvm::sort(lends); + lends.erase(std::unique(lends.begin(), lends.end()), lends.end()); + facts.borrows = lends; } + } - noDrops.erase(funcOp); - changed = true; - break; + if (!facts.empty()) + { + next[call.op] = facts; } } - } - } - bool callMayDrop(const Callees &callees) - { - if (!callees.known) - { - return true; + auto settled = next == pinned; + pinned = std::move(next); + pinFacts(pinned); + if (settled) + { + return; + } } - return !callees.instanceOf && !callees.imported && - llvm::any_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return !noDrops.contains(callee); }); + factsEffective.clear(); + pinFacts(llvm::DenseMap()); } - // ---- Globals a call may assign ---- - - // The globals each call may give a new value, pinned on the call for the inference - // (OWN_ASSIGNS_GLOBALS_ATTR_NAME, OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME): a store to a global, or its - // release, in a callee's body, or in a call it makes. A call that may drop and whose callees are - // not known may assign any; one that drops nothing assigns none. A read of a global ends where - // the global is given a new value (checkGlobalRead), and a call that does so ends it as surely as - // a store in the same function: `const c = G; reset(); print(c.x)`. - void pinAssignedGlobals() + void OwnershipSignaturePass::meetFacts() { - llvm::DenseMap> callsIn; - for (auto &call : calls) + factsEffective.clear(); + for (auto &[funcOp, facts] : factsLocal) { - callsIn[call.op->getParentOfType()].push_back(&call); + if (!facts.empty() && !open.contains(funcOp) && !factsThroughInterface(funcOp, facts)) + { + factsEffective[funcOp] = facts; + } } - llvm::DenseMap> assigns; - llvm::DenseSet assignsAny; + // what a signature says stands, open or not; checkDeclared holds the body to it. But not a + // `@keeps` or `@lends` through an interface: the interface's callers are not told + // (interfaceReached). for (auto &entry : functions) { - auto funcOp = entry.second; - funcOp.walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - else if (auto releaseSlotOp = mlir::dyn_cast(op)) + if (declaresCallerFacts(entry.second)) + { + auto keeps = declared(entry.second, OWN_DECLARED_KEEPS_ATTR_NAME); + Facts said; + said.borrows = declaredLends(entry.second); + said.owned.assign(keeps.begin(), keeps.end()); + // a body here that returns a borrowing record of what `@lends` names: callers in this + // module own the box (spec 32); checkDeclared holds the body to the parameters + auto local = factsLocal.lookup(entry.second); + said.record = !said.borrows.empty() && local.record && local.borrows == said.borrows; + if (factsThroughInterface(entry.second, said)) { - slot = releaseSlotOp.getSlot(); + continue; } - if (auto addressOfOp = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp()) - { - assigns[funcOp].insert(addressOfOp.getGlobalName()); - } - }); + factsEffective[entry.second] = said; + } } - // what a call may assign, from what its callees do so far - auto assignedBy = [&](Call &call, llvm::SetVector &globals) { - if (!callMayDrop(call.callees)) + // the members of a family that disagree lose their facts: a call through the family is + // given only what all of them say. A closed interface's slot is such a family too. + auto demote = [&](llvm::ArrayRef members) { + llvm::SmallVector seen; + for (auto member : members) { - return false; + auto funcOp = functions.lookup(member.getValue()); + seen.push_back(funcOp ? factsEffective.lookup(funcOp) : Facts()); } - if (!call.callees.known) + auto changed = false; + if (llvm::all_equal(seen)) { - return true; + return changed; } - auto any = false; - for (auto callee : call.callees.funcs) + for (auto member : members) { - any |= assignsAny.contains(callee); - if (auto found = assigns.find(callee); found != assigns.end()) + if (auto funcOp = functions.lookup(member.getValue()); + funcOp && !declaresCallerFacts(funcOp) && factsEffective.erase(funcOp)) { - globals.insert(found->second.begin(), found->second.end()); + changed = true; } } - return any; + return changed; }; + // a member a callback family demoted is never promoted again: each step only adds or only + // takes away, so this settles + demotedCallbacks.clear(); for (auto changed = true; changed;) { changed = false; - for (auto &entry : functions) + for (auto &[key, members] : families) { - auto funcOp = entry.second; - for (auto *call : callsIn[funcOp]) + changed = demote(members) || changed; + } + + changed = meetCallbacks(demotedCallbacks) || changed; + + for (auto &[key, family] : interfaceFamilies) + { + if (family.closed) { - llvm::SetVector globals; - if (assignedBy(*call, globals)) - { - changed |= assignsAny.insert(funcOp).second; - } + changed = demote(family.members) || changed; + } + } - for (auto global : globals) - { - changed |= assigns[funcOp].insert(global); - } + for (auto &[key, family] : interfaceFieldFamilies) + { + if (family.closed) + { + changed = demote(family.members) || changed; } } } + } - for (auto &call : calls) + OwnershipSignaturePass::Facts OwnershipSignaturePass::callFacts(const Callees &callees) + { + if (!callees.known || callees.instanceOf || callees.funcs.empty()) { - llvm::SetVector globals; - if (assignedBy(call, globals)) - { - call.op->setAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME, mlir::UnitAttr::get(&getContext())); - continue; - } + return {}; + } - if (globals.empty()) + // each member's facts as the call's arguments see them (a closure's body takes its box first) + if (callees.viaCallback) + { + std::optional agreed; + for (auto [funcOp, shift] : llvm::zip(callees.funcs, callees.shifts)) { - continue; + Facts shifted; + if (!shiftFacts(factsEffective.lookup(funcOp), shift, shifted) || (agreed && *agreed != shifted)) + { + return {}; + } + + agreed = shifted; } - llvm::SmallVector names; - for (auto global : globals) + return agreed.value_or(Facts()); + } + + auto facts = factsEffective.lookup(callees.funcs.front()); + auto agree = llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp callee) { + return factsEffective.lookup(callee) == facts; + }); + return agree ? facts : Facts(); + } + + void OwnershipSignaturePass::setFacts(mlir::Operation *op, const Facts &facts) + { + auto *context = op->getContext(); + op->removeAttr(OWN_RESULT_BORROWS_ATTR_NAME); + op->removeAttr(OWN_RESULT_RECORD_ATTR_NAME); + op->removeAttr(OWN_PARAMS_ATTR_NAME); + if (!facts.borrows.empty()) + { + op->setAttr(OWN_RESULT_BORROWS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.borrows)); + if (facts.record) { - names.push_back(mlir::StringAttr::get(&getContext(), global)); + op->setAttr(OWN_RESULT_RECORD_ATTR_NAME, mlir::UnitAttr::get(context)); } + } - call.op->setAttr(OWN_ASSIGNS_GLOBALS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), names)); + if (!facts.owned.empty()) + { + op->setAttr(OWN_PARAMS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.owned)); } } - // An argument read out of a global (`run(G)`, `G.m()`) is lent to the callee, and the callee may - // free it under its own parameter: `function run(h: C) { G = mk(); print(h.x); }`. The caller's - // borrow ends at the call that takes it, so nothing in the caller sees that, and in the callee - // the parameter is not known to be the global's. A call that may drop is an error for such an - // argument, when something assigns that global. (A field of a global given that way is a - // borrow of a place, which the inference already bounds by the call.) - void rejectGlobalArgumentsDropped() + void OwnershipSignaturePass::pinFacts(const llvm::DenseMap &pinned) { for (auto &call : calls) { - if (!callMayDrop(call.callees)) - { - continue; - } + setFacts(call.op, pinned.lookup(call.op)); + } - for (auto argument : callArgs(call.op)) + for (auto &[funcOp, local] : factsLocal) + { + auto effective = factsEffective.lookup(funcOp); + setFacts(funcOp, effective); + funcOp->removeAttr(OWN_FACTS_LOST_ATTR_NAME); + funcOp->removeAttr(OWN_PARAMS_RELEASED_ATTR_NAME); + // a signature's `@keeps` is checked against the body as it is (checkDeclared) + llvm::SmallVector released; + for (auto index : effective.owned) { - // a method's object: `this` of the method bound to it - if (auto getThisOp = argument.getDefiningOp()) + if (!declaresCallerFacts(funcOp) && !llvm::is_contained(takenParams.lookup(funcOp), index)) { - auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); - if (mlir::isa_and_nonnull(boundOp)) - { - argument = boundOp->getOperand(0); - } + released.push_back(index); } + } - // an immortal global (spec 25) is written by its initializer only, and that runs once - // (a static's is a store in its static constructor, outside any loop, which - // writtenGlobals counts), and no release frees its block - if (isHandle(argument) || isImmortalGlobalRead(argument) || !ownsHeap(call.op->getLoc(), argument.getType())) - { - continue; - } + if (!released.empty()) + { + funcOp->setAttr(OWN_PARAMS_RELEASED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(&getContext(), released)); + } - auto loadOp = rootOf(argument).getDefiningOp(); - auto global = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - if (!global || !writtenGlobals.contains(global.getGlobalName())) + if (effective.empty() && !local.empty()) + { + // through an interface first: a marker would not help there (interfaceReached) + auto *why = factsThroughInterface(funcOp, local) ? INTERFACE_REACHED_WHY : open.lookup(funcOp); + if (!why && demotedCallbacks.contains(funcOp)) { - continue; + why = "a function passed in its place disagrees"; } - call.op->emitError("a value read out of the global '") - << global.getGlobalName() - << "' is lent to a call that may release or overwrite it; -mm=own cannot prove it outlives the call"; - signalPassFailure(); + funcOp->setAttr(OWN_FACTS_LOST_ATTR_NAME, + mlir::StringAttr::get(&getContext(), why ? why : "an override in its class family disagrees")); } } } - // ---- Calls in a global's initializer ---- + bool OwnershipSignaturePass::ownsHeap(mlir::Location location, mlir::Type type) + { + return own_facts::ownsHeap(location, type); + } - // A global's region is no function: its calls get no facts pinned, and the inference never sees - // it, so its calls were read with the defaults - an owned result, borrowed arguments. A result - // that borrows an argument (`const c = child(n)`, `id(new V(1))`) then made the global a second - // owner of a block its argument's owner keeps, or kept one the region freed; and a fresh - // argument a callee keeps (`new N(new V(4))`, the constructor keeping `c`) was released at the - // end of the region, under the object that kept it. A kept argument made in the region and used - // by nothing after the call moves into the callee: its release goes. Anything else is an error. - void ownGlobalInitializerCalls(mlir::ModuleOp module) + void OwnershipSignaturePass::computeDrops() { - module.walk([&](mlir_ts::GlobalOp globalOp) { - // collected first: a move erases a release the walk has not reached yet - llvm::SmallVector calls; - globalOp.walk([&](mlir::Operation *op) { - if (isCall(op)) - { - calls.push_back(op); - } - }); + llvm::DenseMap> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].push_back(&call); + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (!funcOp.isDeclaration() && !dropsInBody(funcOp)) + { + noDrops.insert(funcOp); + } + } - for (auto *op : calls) + for (auto changed = true; changed;) + { + changed = false; + for (auto &entry : functions) { - auto callees = resolve(op); - auto facts = callFacts(callees); - auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName(); - if (!facts.borrows.empty()) + auto funcOp = entry.second; + if (!noDrops.contains(funcOp)) { - op->emitError("the initializer of global '") - << globalOp.getSymName() << "' holds the result of '" << calleeName - << "', which borrows its argument; a global cannot hold a borrow under -mm=own"; - signalPassFailure(); continue; } - auto args = callArgs(op); - llvm::SmallPtrSet given; - for (auto index : facts.owned) + for (auto *call : callsIn[funcOp]) { - if (index < 0 || index >= static_cast(args.size()) || - !given.insert(rootOf(args[index])).second || !moveIntoCallee(op, args[index])) + if (!callMayDrop(call->callees)) { - op->emitError("the initializer of global '") - << globalOp.getSymName() << "' gives '" << calleeName - << "' a value it keeps, but -mm=own cannot move it there"; - signalPassFailure(); - break; + continue; } + + noDrops.erase(funcOp); + changed = true; + break; } } - }); + } } - // `value` was made in the call's block and nothing but its release, if it has one, comes after - // the call: the release goes, and the callee owns the block. Constant data (a string literal) - // is nobody's, and is given as it is. - static bool moveIntoCallee(mlir::Operation *call, mlir::Value value) + bool OwnershipSignaturePass::callMayDrop(const Callees &callees) { - auto root = rootOf(value); - if (isConstantData(root)) + if (!callees.known) { return true; } - if (!isFresh(root) || root.getDefiningOp()->getBlock() != call->getBlock()) - { - return false; - } - - mlir::Operation *release = nullptr; - auto usedAfter = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value) { - if (user == call) - { - return; - } - - if (user->getBlock() != call->getBlock()) - { - usedAfter = true; - } - else if (call->isBeforeInBlock(user)) - { - usedAfter = usedAfter || release || !mlir::isa(user); - release = mlir::isa(user) ? user : release; - } - }); - - if (usedAfter) - { - return false; - } - - if (release) - { - release->erase(); - } - - return true; + return !callees.instanceOf && !callees.imported && + llvm::any_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return !noDrops.contains(callee); }); } - // ---- Export ---- - - // A library says, beside its memory-model marker, which of its exported functions destroy - // nothing (SHARED_LIB_OWN_FACTS). The global is a copy of the marker `__tsmm_own_` - // under another name and with another value, so it is exported exactly as the marker is, and - // MLIRGen emits the same under own as under rc. No marker, nothing is exported. - void exportNoDrops() + void OwnershipSignaturePass::exportNoDrops() { auto markerPrefix = std::string(SHARED_LIB_MEMORY_MODEL) + "own_"; mlir_ts::GlobalOp marker; @@ -5888,11 +1112,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper work{ref}; @@ -6025,7 +1241,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user); @@ -6033,9 +1249,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper mlir_ts::createOwnershipSignaturePass(bool executable) { - return std::make_unique(executable); + return std::make_unique(executable); } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h new file mode 100644 index 000000000..e05d9b032 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -0,0 +1,4866 @@ +// OwnershipSignaturePass, the callee facts of -mm=own. Its methods are defined across the +// lib/TypeScript/OwnershipSignature*.cpp files, one analysis each; templates stay here. + +#ifndef MLIR_TYPESCRIPT_OWNERSHIPSIGNATUREPASS_H +#define MLIR_TYPESCRIPT_OWNERSHIPSIGNATUREPASS_H + +#include "mlir/Pass/Pass.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/FunctionInterfaces.h" + +#include "TypeScript/TypeScriptDialect.h" +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/Passes.h" +#include "TypeScript/Defines.h" +#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" + +#include "OwnershipFacts.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/MapVector.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringSet.h" + +#include + +namespace mlir_ts = mlir::typescript; + +namespace own_signature +{ + +using namespace own_facts; + +// The callee facts of -mm=own (spec 2.4, 3.2), computed over the whole module before the +// per-function inference runs. Each call the pass can resolve - a direct call, a virtual call +// whose candidates are all defined here, the `.instanceOf` slot - gets the facts its candidates +// agree on, pinned on the call op, so the inference reads them without looking anything up. +// +// `__own_no_drops` goes one way: a call with no facts may destroy anything, which can only turn a +// program into an error. The facts a callee's own body relies on - a parameter it keeps, a result +// that borrows an argument - go the other way: a caller that does not know them borrows the +// argument and releases it, or releases a result it does not own, a double free either way. So a +// function has those only when every call that can reach it is one this pass resolved (the closed +// world, see `open`). +// +// `__own_no_drops` alone crosses a module boundary: a library built under own exports the names of +// its exported functions that have it (SHARED_LIB_OWN_FACTS), and a module importing it calls them +// knowing it. The other two cannot: a module linking the library statically re-parses its source +// and sees no facts at all, and a caller that does not know them frees twice. +class OwnershipSignaturePass : public mlir::PassWrapper> +{ + public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipSignaturePass) + + // `executable`: the module is built into a program (`--emit=exe`, or the JIT's entry module), + // whose exports no other module sees (USER decision, spec 31). Unknown is a library. + explicit OwnershipSignaturePass(bool executable = false) : executable(executable) + { + } + + bool executable = false; + + // A global is a root (rc 5ai): what its initializer makes moves into it, and nothing gives it + // back. rc retains a fresh value for its `ts.GlobalResult`, and that retain is the move: it + // goes. The per-function inference never sees a global's region, so any other retain there is + // reported here, by the global's name, rather than left to the lowering's "left a retain + // behind". + // Every use of `value` ends in the global's `ts.GlobalResult`: given to it, or an element of a + // fresh array literal that is (`const nested = [[1, 2], [3]]`), or the payload of a + // `new Shared(x)` (`let g = new Shared({...})`). Its retains are judged on their own. + static bool movesIntoResult(mlir::Value value) + { + return llvm::all_of(value.getUses(), [](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + return true; + } + + // given to `new Shared(x)`: it moves into the handle's block, which owns it from then on + // whatever becomes of the handle - rc counts the handle (spec 23.2, 26) + if (auto sharedNewOp = mlir::dyn_cast(user)) + { + return sharedNewOp.getValue() == use.get(); + } + + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + return movesIntoResult(user->getResult(0)); + } + + // copied into a block `ts.New` made, which itself moves in: an object literal's box + if (auto storeOp = mlir::dyn_cast(user); storeOp && use.getOperandNumber() == 0) + { + auto block = storeOp.getReference(); + return block.getDefiningOp() && + llvm::all_of(block.getUses(), [&](mlir::OpOperand &blockUse) { + return blockUse.getOwner() == storeOp.getOperation() || + (isView(blockUse.getOwner()) && movesIntoResult(blockUse.getOwner()->getResult(0))); + }); + } + + return false; + }); + } + + // A record copied out of a local a `ts.Constant` initializes, whose owning fields are given + // nothing that holds a block: it holds constants only - numbers, immortal string literals, + // functions - and nobody else's reference. + static bool holdsOnlyConstants(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || !varOp.getInitializer() || !varOp.getInitializer().getDefiningOp()) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { + if (mlir::isa(user)) + { + return true; + } + + auto propertyRefOp = mlir::dyn_cast(user); + return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { + auto storeOp = mlir::dyn_cast(fieldUser); + return mlir::isa(fieldUser) || + (storeOp && storeOp.getReference() == propertyRefOp.getResult() && + holdsNoBlock(storeOp.getValue())); + }); + }); + } + + // Whether `op` may run more than once in one run of its function: it is inside a loop op, or its + // block - or the block of a region op holding it - is on a cycle of its region's CFG, a path from + // the block's successors back to it. (By this pass the affine lowering has made every loop a CFG + // cycle of the function body; the loop op is kept for safety.) + static bool mayRunAgain(mlir::Operation *op) + { + for (auto *at = op; at && !mlir::isa(at); at = at->getParentOp()) + { + if (mlir::isa(at)) + { + return true; + } + + auto *block = at->getBlock(); + if (!block) + { + return true; // not in a function: nothing is known of how often it runs + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(block->getSuccessors().begin(), block->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == block) + { + return true; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + } + + return false; + } + + // The immortal globals (OWN_IMMORTAL_GLOBAL_ATTR_NAME, spec 25). A global of a class type, + // not exported, is immortal when only its initializer writes it, once, and gives it a fresh + // object, nothing, or another immortal global's value. Its initializer is either its own region + // (`const c = new C()`) or, for a class static, the one store into it in the class's static + // constructor, which a `ts.GlobalConstructor` runs (the region then yields only the default). + // Top-level script code is such a constructor too, so a module-level `let c: C;` it stores + // into once is one as well. "Once" is how often the store runs, not how many stores there are: + // a store in a loop (`static { for (...) K.w = new V(i); }`) would mark every block it is given + // immortal, and each old one would leak, so a store that may run again leaves the global mortal, + // and each store releases the old value, as for any other global. + // + // The store - the region's, which the lowering makes - marks the block HEAP_BLOCK_IMMORTAL, so + // every block the global ever holds is one no release frees. The value test makes sure the block + // is the global's to keep: an object made right there and given back by nobody. A call's result + // is not one, whatever its callee: these facts are found before the callees' (computeFacts), and + // a global's region is no function the inference checks, so a result that borrows its argument - + // a temporary the region frees, an element of another global's array - would be marked, and + // stored, as the global's own. Such a global stays mortal, and a read of it keeps today's errors. + void findImmortalGlobals(mlir::ModuleOp module) + { + // the static constructors, which run once before the program + llvm::StringSet<> constructors; + module.walk([&](mlir_ts::GlobalConstructorOp constructorOp) { constructors.insert(constructorOp.getGlobalName()); }); + + // a static's address taken out of a class's vtable (`k.w = x`, `k: typeof K`) and written + // through: it may be any class's static the vtable slot holds, so no static is immortal. + // MLIRGen makes a `ts.ThisVirtualSymbolRef` only for a method (a bound function, never a + // `!ts.ref`), so no program reaches this through one today; it is listed for an IR that would. + auto vtableWritten = false; + module.walk([&](mlir::Operation *op) { + if (mlir::isa(op) && + mlir::isa(op->getResult(0).getType()) && + !llvm::all_of(op->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) + { + vtableWritten = true; + } + }); + + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + return; // an op this pass cannot read symbols through: nothing is known never written + } + + llvm::StringMap> usesOf; + for (auto &use : *uses) + { + usesOf[use.getSymbolRef().getRootReference().getValue()].push_back(use.getUser()); + } + + // each candidate with the value its initializer gives it, and the store that does, if any + llvm::MapVector> candidates; + auto mth = typeHelper(&getContext()); + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || + globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || + globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock()) + { + return; + } + + auto resultOp = mlir::dyn_cast(globalOp.getInitializerBlock()->getTerminator()); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + mlir_ts::StoreOp initStore; + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + // its vtable's entry, which only a `ts.VirtualSymbolRef` reads (vtableWritten) + if (mlir::isa(user) && user->getParentOfType()) + { + if (vtableWritten) + { + return; + } + + continue; + } + + if (mlir::isa(user)) + { + continue; // only loaded, or vtableWritten + } + + auto addressOfOp = mlir::dyn_cast(user); + if (!addressOfOp) + { + return; + } + + auto released = false; + auto stored = false; + for (auto *addressUser : addressOfOp->getUsers()) + { + if (mlir::isa(addressUser)) + { + continue; + } + + // the old value, the default, given up before the store + if (mlir::isa(addressUser)) + { + released = true; + continue; + } + + // the one store, in a static constructor, run once + auto storeOp = mlir::dyn_cast(addressUser); + auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); + if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || + storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || + mayRunAgain(storeOp)) + { + return; + } + + initStore = storeOp; + stored = true; + } + + if (released && !stored) + { + return; + } + } + + // a static's region yields only its default, which the store replaces + if (initStore && !regionYieldsDefault(globalOp)) + { + return; + } + + candidates[globalOp] = {initStore ? initStore.getValue() : resultOp->getOperand(0), initStore}; + }); + + // given a fresh object, nothing, or another immortal global's value: in module order, until + // no more are found + llvm::StringSet<> immortal; + auto madeHere = [&](mlir::Value value) { + auto root = rootOf(value); + if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) + { + return false; + } + + // and not given back before the global takes it + auto released = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); }); + return !released; + }; + // isImmortalGlobalRead, of the globals found so far: none is marked yet + auto readsImmortal = [&](mlir::Value value) { + auto addressOfOp = globalReadOf(value); + return addressOfOp && immortal.contains(addressOfOp.getGlobalName()); + }; + + for (auto found = true; found;) + { + found = false; + for (auto &[globalOp, init] : candidates) + { + auto value = init.first; + if (!immortal.contains(globalOp.getSymName()) && (madeHere(value) || holdsNoBlock(value) || readsImmortal(value))) + { + immortal.insert(globalOp.getSymName()); + found = true; + } + } + } + + auto unit = mlir::UnitAttr::get(&getContext()); + for (auto &[globalOp, init] : candidates) + { + if (!immortal.contains(globalOp.getSymName())) + { + continue; + } + + globalOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + if (init.second) + { + init.second->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + if (mlir::isa(user)) + { + user->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + } + } + } + + void moveIntoGlobals(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + llvm::SmallVector retains; + globalOp.walk([&](mlir_ts::RetainOp retainOp) { retains.push_back(retainOp); }); + for (auto retainOp : retains) + { + // a handle's retain is rc's: the global's count, which own leaves in place (spec 23.2) + if (touchesHandle(retainOp)) + { + continue; + } + + auto value = retainOp.getReference(); + + // an immortal global's value (spec 25): owned by no one, held by any number + if (isImmortalGlobalRead(value)) + { + retainOp.erase(); + continue; + } + + // nothing in the region gives the block back, or takes a second reference to it: + // the global is its one owner + auto released = false; + auto retained = 0; + forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { + released = released || mlir::isa(user); + retained += mlir::isa(user); + }); + + if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) + { + retainOp.erase(); + continue; + } + + retainOp.emitError("the initializer of global '") + << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + signalPassFailure(); + } + }); + } + + void runOnOperation() override; + + private: + // The function a closure runs, when it names one defined here. + mlir_ts::FuncOp closureBody(mlir_ts::CreateBoundFunctionOp boundOp); + + // The fields of a closure body's capture box whose cell it assigns (OWN_ASSIGNS_CAPTURES_ATTR_NAME), + // directly or through a closure it builds over the cell. Every field, when the body is unknown or + // a cell goes anywhere this does not follow. + llvm::SetVector assignedCaptures(mlir_ts::FuncOp funcOp, int depth = 0); + + // Is the variable behind this cell assigned - here, or by a closure built over it? + bool cellAssigned(mlir::Value cell, int depth); + + llvm::DenseMap> assignedCache; + + // What a call may reach: known, when every candidate is a function defined in this module (or + // the call goes through the `.instanceOf` slot, or reaches a function another module says + // destroys nothing); else anything. + struct Callees + { + bool known = false; + bool instanceOf = false; + bool imported = false; + // a function read out of a field that only ever holds it (fieldFunctions) + bool fieldFunction = false; + // the slot of a closed interface (resolveInterfaceSlot) + bool viaInterface = false; + // a parameter of a closed function that callers pass only functions of this module + // (callbackFamilies), with each member's shift: a closure's body takes its capture box + // first, so its parameter k is the call's argument k - 1 + bool viaCallback = false; + llvm::SmallVector shifts; + // the name the imported function is exported under + llvm::StringRef importedName; + llvm::SmallVector funcs; + // the symbols the call names, whether or not it is known: a direct callee, or every + // member of a virtual slot's family (for the facts a declaration says, declaredFacts) + llvm::SmallVector named; + }; + + struct Call + { + mlir::Operation *op; + Callees callees; + }; + + llvm::StringMap functions; + llvm::StringMap globals; + // Globals whose address is used other than to load from it: what they hold may change. + llvm::StringSet<> writtenGlobals; + llvm::SmallVector calls; + + // Functions of imported libraries that destroy nothing a caller can reach, by the name they + // are exported under (SHARED_LIB_OWN_NO_DROPS_ATTR_NAME). + llvm::StringSet<> importedNoDrops; + + // (tuple type, field) -> the one function an object of that type holds in that field. A + // generator's state object holds its `next` so. + own_facts::FieldFunctions fieldFunctions; + + // Class names, from their vtables, to split a method's symbol into class and method name. + llvm::StringSet<> classNames; + // (vtable position, method name) -> the functions at that position under that name, in every + // class vtable of the module: what a virtual call through that slot may reach. + std::map, llvm::SmallVector> families; + + // Functions whose callers are not all known - a caller outside the module, or a reference to + // the symbol that is not a call this pass resolves - and why, for the error in their body. + llvm::DenseMap open; + + // Methods an interface's vtable holds (`...vtbl`, or `...vtbl` + // for an object literal) whose calls through the interface are not resolved: the slot's family + // is open (interfaceFamilies), or the vtable is one this pass cannot tie to an interface. Such + // a call (`ts.InterfaceSymbolRef`, then an indirect call) is given none of the method's facts: a + // caller that hands it an argument it keeps still releases it, and a caller of one whose result + // borrows an argument or `this` releases the borrow as its own (a double free either way). So a + // method reached this way that keeps a parameter or returns a borrow - by its body or by `@keeps` + // / `@lends` - loses its facts, and the body's error says why (`is a parameter, kept here, but + // -mm=own cannot make the callers of ... give it up`, `is a parameter, returned here, but -mm=own + // cannot tell the callers of ... that the result borrows it`). A method reached only through + // closed interfaces keeps its facts: their calls get them (spec 31). + llvm::DenseSet interfaceReached; + + // ---- Interfaces closed in this module (G9, spec 31) ---- + // + // A call through an interface reaches whatever function the slot of the interface value's vtable + // holds. When every such vtable is one this module defines and every interface value is made + // from one of them, the slot holds one of a known set of functions - the slot's family - and the + // call is resolved to it like a virtual call of a private class family: it gets the facts its + // members agree on (meetFacts demotes a family that disagrees), and `__own_no_drops` when every + // member destroys nothing. Anything this cannot see opens the interface, and then the call is + // what it was: no facts, and its members reached that way lose theirs (interfaceReached). + // + // Open, for an interface I: + // - another module can see it: its name in an exported declaration (`__decls`), or `!ts.iface<@I>` + // in the type of a function or global another module can reach - an exported or declared + // function, a global that is not private, or is exported or imported; + // - an I value made other than by `ts.NewInterface` over a vtable this pass reads (the global, a + // heap copy of it, or the one an object's class vtable holds for I): a cast from another + // interface (`J` to `I`, which opens `J` too: its members are reached through `I`), from + // `any` or an opaque pointer (which opens every interface put into one), from a tuple or array + // that does not hold I already (castMakesInterface), a `ts.InterfaceRef` used as a value; + // - a vtable of I written after its initializer, or read other than to make an interface value; + // - a class vtable that holds a vtable of I and is not private (an exported class: another + // module may extend it); + // - and, per slot, a member that is not a function this module defines, a slot that holds no + // function (a field's offset, a thunk's cell), or a member that says `@keeps` or `@lends` (its + // facts are its signature's, which a family cannot change). A member that is not private (a + // `public` method) is open (findOpen): it has no facts of its own to give the family. + struct InterfaceFamily + { + // the slot's name, which every vtable of the interface agrees on + mlir::StringAttr field; + llvm::SmallVector members; + bool closed = true; + }; + + // (interface, vtable position) -> the functions every vtable of the interface holds there + std::map, InterfaceFamily> interfaceFamilies; + + // The words of the module's exported declarations (none in an executable), the classes a class + // vtable is the own vtable of, and the classes another module can name: exported, or in the + // type of an exported or declared function or of an exported or imported global (findOpen). + llvm::StringSet<> exportWords; + llvm::StringSet<> ownClasses; + llvm::StringSet<> nameableClasses; + + // A field of function type (`f: (x: number) => number`), called through the interface: the slot + // holds the offset of a field of each implementing object literal, and the call goes to whatever + // function that field holds. When every vtable's slot is such an offset and every function ever + // stored into those fields is one this module defines - the literal's own (`{ f: function ... }`, + // in its `ts.Constant`), or one stored into the field later, through the object or through the + // interface - the call is resolved to them (spec 31). A store of anything else (a parameter, a + // call's result, a closure), a field reached other than to be read or stored, or an object of + // the literal's type made other than by the literal opens the slot. A class implementing the + // field holds a thunk's cell, which this does not follow: open. + struct InterfaceFieldFamily + { + mlir::StringAttr field; + // (object literal's storage tuple, field index) of each vtable + llvm::SmallVector, 2> targets; + llvm::SmallVector members; + bool closed = true; + }; + + std::map, InterfaceFieldFamily> interfaceFieldFamilies; + + // interface name -> the tuple types of the vtables its values are made with, when every one of + // them is read here (holdsFunction: what a value of the interface carries) + llvm::StringMap> interfaceVTableTypes; + + // `ts.PropertyRef(ts.Cast(null), i)`, seen through casts: the offset of field `i` of an object of + // that type, as an object literal's interface vtable holds it. Null for anything else. + std::pair slotField(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + auto propertyRefOp = value ? value.getDefiningOp() : mlir_ts::PropertyRefOp(); + auto nullObject = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::CastOp(); + if (!nullObject || !nullObject.getIn().getDefiningOp() || + !mlir::isa(nullObject.getType())) + { + return {}; + } + + auto tupleType = storageTupleOf(nullObject.getType()); + if (!tupleType) + { + return {}; + } + + return {tupleType, static_cast(propertyRefOp.getPosition())}; + } + + // What every object-literal field an interface's field slot may be holds (InterfaceFieldFamily). + void collectFieldFunctions(mlir::ModuleOp module) + { + llvm::DenseMap, llvm::SetVector> stored; + llvm::DenseSet> unknownFields; + llvm::DenseSet unknownTypes; + // a function of unknown origin stored through an interface this pass cannot follow + auto unknownAnywhere = false; + + auto store = [&](std::pair key, mlir::Value value) { + auto symbol = slotFunction(value); + auto funcOp = symbol ? functions.lookup(symbol.getValue()) : mlir_ts::FuncOp(); + if (funcOp && !funcOp.isDeclaration()) + { + stored[key].insert(symbol); + } + else + { + unknownFields.insert(key); + } + }; + + auto isFunctionType = [](mlir::Type type) { + return mlir::isa(type); + }; + + module.walk([&](mlir::Operation *op) { + // the literal's own values + if (auto constantOp = mlir::dyn_cast(op)) + { + auto constTupleType = mlir::dyn_cast(constantOp.getType()); + auto values = mlir::dyn_cast(constantOp.getValue()); + if (!constTupleType || !values) + { + return; + } + + mlir::Type tupleType = mlir_ts::TupleType::get(&getContext(), constTupleType.getFields()); + for (auto [index, value] : llvm::enumerate(values)) + { + std::pair key{tupleType, static_cast(index)}; + if (auto symbol = mlir::dyn_cast(value)) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (funcOp && !funcOp.isDeclaration()) + { + stored[key].insert(symbol.getAttr()); + } + else + { + unknownFields.insert(key); + } + } + else if (!mlir::isa(value) && index < constTupleType.getFields().size() && + isFunctionType(constTupleType.getFields()[index].type)) + { + unknownFields.insert(key); + } + } + + return; + } + + // a store into the field through the object + if (auto propertyRefOp = mlir::dyn_cast(op)) + { + auto tupleType = storageTupleOf(propertyRefOp.getObjectRef().getType()); + if (!tupleType) + { + return; + } + + std::pair key{tupleType, static_cast(propertyRefOp.getPosition())}; + for (auto &use : propertyRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == propertyRefOp.getResult() && storeOp.getValue() != propertyRefOp.getResult()) + { + store(key, storeOp.getValue()); + continue; + } + + // an interface vtable's offset of the field (slotField) reads nothing + if (user->getParentOfType() && mlir::isa(user)) + { + continue; + } + + unknownFields.insert(key); + } + + return; + } + + // a store into the field through an interface: into each literal its vtables reach + if (auto interfaceRefOp = mlir::dyn_cast(op)) + { + auto refType = mlir::dyn_cast(interfaceRefOp.getType()); + if (!refType || !isFunctionType(refType.getElementType())) + { + return; + } + + auto interfaceType = mlir::dyn_cast(interfaceRefOp.getInterfaceVal().getType()); + auto found = interfaceType ? interfaceFieldFamilies.find({interfaceType.getName().getValue().str(), + static_cast(interfaceRefOp.getIndex()) + + INTERFACE_VTABLE_HEADER_SLOTS}) + : interfaceFieldFamilies.end(); + for (auto &use : interfaceRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == interfaceRefOp.getResult() && found != interfaceFieldFamilies.end() && + found->second.closed) + { + for (auto target : found->second.targets) + { + store(target, storeOp.getValue()); + } + + continue; + } + + unknownAnywhere = true; + } + + return; + } + + // an object of the literal's type made other than by the literal: its fields may hold + // anything. A cast keeps the type's own value (the literal's box seen as the object, an + // object, a reference to its storage) or makes the null object a vtable's offsets are + // taken from; from anything else it reinterprets. + if (auto castOp = mlir::dyn_cast(op)) + { + auto tupleType = storageTupleOf(castOp.getType()); + if (tupleType && !mlir::isa(castOp.getIn().getType()) && + storageTupleOf(castOp.getIn().getType()) != tupleType) + { + unknownTypes.insert(tupleType); + } + + return; + } + + if (mlir::isa(op) || isCall(op) || isView(op) || mlir::isa(op)) + { + return; + } + + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + if (auto tupleType = storageTupleOf(result.getType())) + { + unknownTypes.insert(tupleType); + } + } + } + }); + + for (auto &[key, family] : interfaceFieldFamilies) + { + if (!family.closed) + { + continue; + } + + for (auto target : family.targets) + { + if (unknownAnywhere || unknownTypes.contains(target.first) || unknownFields.contains(target)) + { + family.closed = false; + break; + } + + for (auto member : stored.lookup(target)) + { + if (!llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + } + } + + family.closed = family.closed && !family.members.empty(); + } + } + + // The interfaces mentioned by a type as values: the interface itself, a union or optional, a + // tuple's fields, an array's elements, what a reference points to. Not a class's or an object's + // fields (a reference to a block made elsewhere), nor a function's parameters and result. + static void valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth = 0) + { + if (!type || depth > 16) + { + return; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + found.insert(interfaceType.getName().getValue()); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + valueInterfaces(info.type, found, depth + 1); + } + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + } + else if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + } + else if (auto unionType = mlir::dyn_cast(type)) + { + for (auto member : unionType.getTypes()) + { + valueInterfaces(member, found, depth + 1); + } + } + else if (auto optionalType = mlir::dyn_cast(type)) + { + valueInterfaces(optionalType.getElementType(), found, depth + 1); + } + else if (auto arrayType = mlir::dyn_cast(type)) + { + valueInterfaces(arrayType.getElementType(), found, depth + 1); + } + else if (auto constArrayType = mlir::dyn_cast(type)) + { + valueInterfaces(constArrayType.getElementType(), found, depth + 1); + } + else if (auto refType = mlir::dyn_cast(type)) + { + valueInterfaces(refType.getElementType(), found, depth + 1); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + valueInterfaces(valueRefType.getElementType(), found, depth + 1); + } + else if (auto boundRefType = mlir::dyn_cast(type)) + { + valueInterfaces(boundRefType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + valueInterfaces(sharedType.getElementType(), found, depth + 1); + } + } + + // Every interface a type mentions anywhere, a class's fields and a function's parameters + // included: what another module handed a value of this type could reach. With `classes`, the + // classes it mentions too (a base class is its derived class's first field). + static void anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, + llvm::StringSet<> *classes = nullptr) + { + if (!type || !seen.insert(type).second) + { + return; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + found.insert(interfaceType.getName().getValue()); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + anyInterfaces(info.type, found, seen, classes); + } + }; + + // the types whose fields are not parameters of the type + if (auto classStorageType = mlir::dyn_cast(type)) + { + if (classes) + { + classes->insert(classStorageType.getName().getValue()); + } + + fields(classStorageType.getFields()); + return; + } + + if (auto objectStorageType = mlir::dyn_cast(type)) + { + fields(objectStorageType.getFields()); + return; + } + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + return; + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + return; + } + + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { anyInterfaces(sub, found, seen, classes); }); + } + + // The function values a value of this type holds: the type itself, a union's or an optional's + // member, a tuple's field, an array's element. Not what a reference points to (a vtable's + // address seen as an opaque pointer is the compiler's own, not a value). + static void functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth = 0) + { + if (!type || depth > 16) + { + return; + } + + if (mlir::isa(type)) + { + found.push_back(type); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + functionValues(info.type, found, depth + 1); + } + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + } + else if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + } + else if (auto unionType = mlir::dyn_cast(type)) + { + for (auto member : unionType.getTypes()) + { + functionValues(member, found, depth + 1); + } + } + else if (auto optionalType = mlir::dyn_cast(type)) + { + functionValues(optionalType.getElementType(), found, depth + 1); + } + else if (auto arrayType = mlir::dyn_cast(type)) + { + functionValues(arrayType.getElementType(), found, depth + 1); + } + else if (auto constArrayType = mlir::dyn_cast(type)) + { + functionValues(constArrayType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + functionValues(sharedType.getElementType(), found, depth + 1); + } + } + + // A function type's parameters and results, whatever kind of function it is. + static std::pair, llvm::SmallVector> signatureOf(mlir::Type type) + { + llvm::SmallVector inputs; + llvm::SmallVector results; + if (auto functionType = mlir::dyn_cast(type)) + { + inputs.append(functionType.getInputs().begin(), functionType.getInputs().end()); + results.append(functionType.getResults().begin(), functionType.getResults().end()); + } + else if (auto boundType = mlir::dyn_cast(type)) + { + inputs.append(boundType.getInputs().begin(), boundType.getInputs().end()); + results.append(boundType.getResults().begin(), boundType.getResults().end()); + } + else if (auto hybridType = mlir::dyn_cast(type)) + { + inputs.append(hybridType.getInputs().begin(), hybridType.getInputs().end()); + results.append(hybridType.getResults().begin(), hybridType.getResults().end()); + } + else + { + // a kind this does not read: never the same signature as another + inputs.push_back(type); + } + + return {inputs, results}; + } + + // Does a cast reinterpret a function value - from `any` or `unknown`, or as a function of another + // signature? Then an interface value its parameters or + // results hold crosses unseen (a `(o: Op2) => ...` called as `(o: Op) => ...`): `interfaces` + // gets every interface either side's signatures mention, and `boxed` the input's when it is put + // into `any`. + static bool castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing) + { + // `!ts.opaque` is the compiler's own (a vtable slot read as the `.instanceOf` it holds), not + // a value a program can put a function into + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + llvm::SmallVector inFunctions; + llvm::SmallVector outFunctions; + functionValues(in, inFunctions); + functionValues(out, outFunctions); + boxing = false; + if ((inFunctions.empty() && outFunctions.empty()) || in == out) + { + return false; + } + + auto mention = [&](llvm::ArrayRef types) { + for (auto type : types) + { + llvm::DenseSet seen; + anyInterfaces(type, interfaces, seen); + } + }; + + if (isOpaque(out)) + { + boxing = true; + mention(inFunctions); + return false; + } + + // the same signatures, but for `this` seen as an opaque pointer on one side, or bound on one + // side only (a closure's box, a bound function's object: the caller supplies neither) + auto sameSignature = [](mlir::Type a, mlir::Type b) { + auto [aInputs, aResults] = signatureOf(a); + auto [bInputs, bResults] = signatureOf(b); + auto dropBoundThis = [](mlir::Type type, llvm::SmallVector &inputs) { + if (!inputs.empty() && (mlir::isa(type) || mlir::isa(inputs.front()))) + { + inputs.erase(inputs.begin()); + } + }; + + if (aInputs.size() == bInputs.size() + 1) + { + dropBoundThis(a, aInputs); + } + else if (bInputs.size() == aInputs.size() + 1) + { + dropBoundThis(b, bInputs); + } + + if (aResults != bResults || aInputs.size() != bInputs.size()) + { + return false; + } + + for (auto [index, pair] : llvm::enumerate(llvm::zip(aInputs, bInputs))) + { + auto [aInput, bInput] = pair; + if (aInput != bInput && + !(index == 0 && (mlir::isa(aInput) || mlir::isa(bInput)))) + { + return false; + } + } + + return true; + }; + + // a function out of `any`, or one function seen as another; a function seen as something + // that is no function (a vtable slot's reference to it) is the compiler's own + auto reinterprets = isOpaque(in) && !outFunctions.empty(); + if (!inFunctions.empty() && !outFunctions.empty()) + { + reinterprets = reinterprets || inFunctions.size() != outFunctions.size(); + for (auto [inFunction, outFunction] : llvm::zip(inFunctions, outFunctions)) + { + reinterprets = reinterprets || !sameSignature(inFunction, outFunction); + } + } + + if (reinterprets) + { + mention(inFunctions); + mention(outFunctions); + } + + return reinterprets; + } + + // The interfaces an interface member's type mentions: a method's or a function field's + // parameters and result, a field's type, anything they hold (a class's fields included). + static void memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found) + { + llvm::DenseSet seen; + anyInterfaces(type, found, seen); + } + + static bool isNullish(mlir::Type type) + { + return mlir::isa(type); + } + + // May a cast from `in` to `out` make an interface value out of something that was not that + // interface's value already? A union or optional narrowed to its interface member (whose other + // members are null or undefined) or widened from it, null given a nullable interface type, a + // class or object seen as another (the same block, its fields as they are) and a function seen as + // another are not; a cast from another interface, `any`, an opaque pointer, or a tuple or an + // array whose element is not the same interface is. + static bool castMakesInterface(mlir::Type in, mlir::Type out, int depth = 0) + { + // null seen as an interface value has no vtable: a call through it reaches nothing + if (in == out || depth > 16 || isNullish(in)) + { + return false; + } + + llvm::StringSet<> made; + valueInterfaces(out, made); + if (made.empty()) + { + return false; + } + + if (mlir::isa(out)) + { + return !holdsAsIs(in, out, /*onlyNullable=*/true); + } + + if (mlir::isa(out)) + { + if (isNullish(in) || holdsAsIs(out, in)) + { + return false; + } + + llvm::SmallVector members; + if (auto unionType = mlir::dyn_cast(in)) + { + members.append(unionType.getTypes().begin(), unionType.getTypes().end()); + } + else if (auto optionalType = mlir::dyn_cast(in)) + { + members.push_back(optionalType.getElementType()); + } + else + { + return true; + } + + return !llvm::all_of(members, [&](mlir::Type member) { return isNullish(member) || holdsAsIs(out, member); }); + } + + auto tupleFields = [](mlir::Type type) -> std::optional> { + if (auto tupleType = mlir::dyn_cast(type)) + { + return tupleType.getFields(); + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + return constTupleType.getFields(); + } + + return std::nullopt; + }; + + if (auto outFields = tupleFields(out)) + { + auto inFields = tupleFields(in); + if (!inFields || inFields->size() != outFields->size()) + { + return true; + } + + for (auto [inField, outField] : llvm::zip(*inFields, *outFields)) + { + if (castMakesInterface(inField.type, outField.type, depth + 1)) + { + return true; + } + } + + return false; + } + + auto elementOf = [](mlir::Type type) -> mlir::Type { + if (auto arrayType = mlir::dyn_cast(type)) + { + return arrayType.getElementType(); + } + + if (auto constArrayType = mlir::dyn_cast(type)) + { + return constArrayType.getElementType(); + } + + return {}; + }; + + if (auto outElement = elementOf(out)) + { + auto inElement = elementOf(in); + return !inElement || castMakesInterface(inElement, outElement, depth + 1); + } + + // a reference seen as another (`!ts.bound_ref` as `!ts.ref`): what it points to + auto pointee = [](mlir::Type type) -> mlir::Type { + if (auto refType = mlir::dyn_cast(type)) + { + return refType.getElementType(); + } + + if (auto valueRefType = mlir::dyn_cast(type)) + { + return valueRefType.getElementType(); + } + + if (auto boundRefType = mlir::dyn_cast(type)) + { + return boundRefType.getElementType(); + } + + return {}; + }; + + if (auto outPointee = pointee(out)) + { + auto inPointee = pointee(in); + return !inPointee || castMakesInterface(inPointee, outPointee, depth + 1); + } + + // anything else reinterpreted: what is read out of it was never checked + return true; + } + + // The function a vtable slot holds: `ts.SymbolRef`, seen through casts. Null for anything else. + static mlir::StringAttr slotFunction(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + auto symbolRefOp = value ? value.getDefiningOp() : mlir_ts::SymbolRefOp(); + return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + } + + static mlir_ts::AddressOfOp addressOfThroughCasts(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + return value ? value.getDefiningOp() : mlir_ts::AddressOfOp(); + } + + // The words of the module's exported declarations (`__decls*` globals): what another module + // can name. + static llvm::StringSet<> exportedWords(mlir::ModuleOp module) + { + llvm::StringSet<> words; + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!globalOp.getSymName().starts_with("__decls")) + { + return; + } + + globalOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { + auto text = mlir::dyn_cast(constantOp.getValue()); + if (!text) + { + return; + } + + llvm::StringRef rest = text.getValue(); + while (!rest.empty()) + { + auto isWordChar = [](char c) { return llvm::isAlnum(c) || c == '_' || c == '$'; }; + rest = rest.drop_while([&](char c) { return !isWordChar(c); }); + auto word = rest.take_while(isWordChar); + if (!word.empty()) + { + words.insert(word); + } + + rest = rest.drop_front(word.size()); + } + }); + }); + + return words; + } + + // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a + // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the + // slots of a class's own vtable that hold the address of one of its interface vtables + struct InterfaceVTable + { + mlir_ts::TupleType type; + llvm::DenseMap slots; + // a field's slot: the object literal's field it is the offset of + llvm::DenseMap> fields; + bool written = false; + }; + + // What collectInterfaceFamilies learns of the module, one step after another. + struct InterfaceScan + { + // the interfaces open whatever their vtables hold + llvm::StringSet<> open; + llvm::StringMap vtables; + // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) + llvm::SmallVector unread; + // interface name -> the interface vtables class vtables hold for it, read through an + // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) + llvm::StringMap> classHeld; + // interface name -> the vtables its values are made with + llvm::StringMap> madeWith; + // An interface value made out of another one reaches the other's members: both are open. One + // made out of `any` or an opaque pointer may be any interface value put into one: every + // interface so boxed is open with it, and every interface at all when it is untraceable. + bool allOpen = false; + llvm::StringSet<> boxed; + llvm::StringSet<> unboxedInto; + bool unboxedFunction = false; + // the interfaces a cast makes a value of out of another interface's or an opaque one, and + // the ones it came from: closed over their members' types below + llvm::StringSet<> castOpened; + // interface -> the interfaces its members' types mention: its vtables' slots and what is + // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields + // included + llvm::StringMap> memberInterfaces; + llvm::DenseSet tied; + + void openAll(const llvm::StringSet<> &names) + { + for (auto &entry : names) + { + open.insert(entry.getKey()); + } + } + }; + + void collectInterfaceFamilies(mlir::ModuleOp module) + { + InterfaceScan scan; + readInterfaceVTables(module, scan); + traceInterfaceValues(module, scan); + openExposedInterfaces(module, scan); + openThroughMembers(scan); + openLooseVTables(scan); + buildInterfaceFamilies(scan); + collectFieldFunctions(module); + markInterfaceReached(scan); + } + + // The vtables: each interface vtable's slots, and the interface vtables a class's own vtable holds. + void readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto name = globalOp.getSymName(); + if (!name.ends_with("..vtbl")) + { + return; + } + + auto owner = name.drop_back(llvm::StringRef("..vtbl").size()); + llvm::DenseMap values; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + if (insertOp.getPosition().size() == 1) + { + values[insertOp.getPosition()[0]] = insertOp.getValue(); + } + }); + + auto header = slotFunction(values.lookup(0)); + auto tupleType = mlir::dyn_cast(globalOp.getType()); + if (!header || !tupleType) + { + scan.unread.push_back(name); + return; + } + + if (header.getValue() == (owner + "..instanceOf").str()) + { + // an exported class (not in an executable, whose exports no module sees) + auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); + ownClasses.insert(owner); + if (exposed) + { + nameableClasses.insert(owner); + } + + for (auto &[position, value] : values) + { + auto addressOfOp = addressOfThroughCasts(value); + auto id = position < static_cast(tupleType.size()) + ? mlir::dyn_cast_or_null(tupleType.getFieldInfo(position).id) + : mlir::StringAttr(); + if (!addressOfOp || !id) + { + continue; + } + + scan.classHeld[id.getValue()].insert(addressOfOp.getGlobalName()); + if (exposed) + { + scan.open.insert(id.getValue()); // another module may extend the class + } + } + + return; + } + + if (!header.getValue().ends_with("..instanceOf") && header.getValue() != ".instanceOf.none") + { + scan.unread.push_back(name); + return; + } + + auto &vtable = scan.vtables[name]; + vtable.type = tupleType; + for (auto &[position, value] : values) + { + if (position > 0) + { + vtable.slots[position] = slotFunction(value); + if (auto field = slotField(value); field.first) + { + vtable.fields[position] = field; + } + } + } + }); + } + + // What makes an interface value, out of which vtable; the casts that box, unbox or reinterpret one; + // the interfaces its members' types mention; the vtables written. + void traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan) + { + auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { + // the global itself + if (auto addressOfOp = addressOfThroughCasts(value)) + { + return scan.vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); + } + + // a copy of it on the heap, its absent members' slots patched + while (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + + if (auto newOp = value.getDefiningOp()) + { + llvm::StringRef copied; + for (auto *user : newOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() + ? storeOp.getValue().getDefiningOp() + : mlir_ts::LoadOp(); + auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); + if (addressOfOp && scan.vtables.count(addressOfOp.getGlobalName())) + { + if (!copied.empty()) + { + return {}; + } + + copied = addressOfOp.getGlobalName(); + } + } + + return copied; + } + + return {}; + }; + + // an object's class vtable read at run time: `ts.VTableOffsetRef(ts.Load(ts.PropertyRef(object, 0)), k)` + auto readFromClass = [](mlir::Value value) { + auto offsetRefOp = value.getDefiningOp(); + auto loadOp = offsetRefOp ? offsetRefOp.getVtable().getDefiningOp() : mlir_ts::LoadOp(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + return propertyRefOp && propertyRefOp.getPosition() == 0 && + mlir::isa(propertyRefOp.getObjectRef().getType()); + }; + + // what reads a vtable's address: making an interface value, a class vtable's slot, the copy + std::function onlyMakesValues = [&](mlir::Value address) { + return llvm::all_of(address.getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (auto newInterfaceOp = mlir::dyn_cast(user)) + { + return newInterfaceOp.getInterfaceVTable() == use.get(); + } + + if (mlir::isa(user)) + { + return onlyMakesValues(user->getResult(0)); + } + + if (mlir::isa(user)) + { + return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; + } + + // the copy: read whole, and stored into a block `ts.New` made + if (auto loadOp = mlir::dyn_cast(user)) + { + return llvm::all_of(loadOp->getUsers(), [&](mlir::Operation *loadUser) { + auto storeOp = mlir::dyn_cast(loadUser); + return storeOp && storeOp.getValue() == loadOp.getResult() && + storeOp.getReference().getDefiningOp(); + }); + } + + return false; + }); + }; + + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + module.walk([&](mlir::Operation *op) { + if (auto newInterfaceOp = mlir::dyn_cast(op)) + { + auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); + auto vtable = newInterfaceOp.getInterfaceVTable(); + if (auto found = vtableOf(vtable); !found.empty()) + { + scan.madeWith[name].insert(found); + } + else if (readFromClass(vtable)) + { + // any class's vtable for the interface (classHeld, all in its families below) + if (!scan.classHeld.count(name)) + { + scan.open.insert(name); + } + } + else if (auto extractOp = vtable.getDefiningOp()) + { + scan.castOpened.insert(name); + valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); + } + else + { + scan.allOpen = true; + } + + return; + } + + if (auto castOp = mlir::dyn_cast(op)) + { + auto in = castOp.getIn().getType(); + auto out = castOp.getType(); + if (isOpaque(out)) + { + valueInterfaces(in, scan.boxed); + } + + if (castMakesInterface(in, out)) + { + valueInterfaces(out, scan.castOpened); + valueInterfaces(in, scan.castOpened); + if (isOpaque(in)) + { + valueInterfaces(out, scan.unboxedInto); + } + } + + // a function value reinterpreted carries the interface values of its parameters and + // results across unseen (review6 x1c, x1r) + llvm::StringSet<> signatures; + auto boxing = false; + if (castReinterpretsFunction(in, out, signatures, boxing)) + { + scan.openAll(signatures); + if (isOpaque(in)) + { + scan.unboxedFunction = true; + } + } + else if (boxing) + { + for (auto &entry : signatures) + { + scan.boxed.insert(entry.getKey()); + } + } + + return; + } + + if (auto interfaceRefOp = mlir::dyn_cast(op)) + { + if (!interfaceRefOp->use_empty()) + { + scan.open.insert(interfaceRefOp.getIdentifier()); + } + + return; + } + + if (auto symbolRefOp = mlir::dyn_cast(op)) + { + if (auto interfaceType = mlir::dyn_cast(symbolRefOp.getInterfaceVal().getType())) + { + memberTypeInterfaces(symbolRefOp.getType(), scan.memberInterfaces[interfaceType.getName().getValue()]); + } + + return; + } + + if (auto addressOfOp = mlir::dyn_cast(op)) + { + if (auto found = scan.vtables.find(addressOfOp.getGlobalName()); found != scan.vtables.end() && !onlyMakesValues(addressOfOp.getResult())) + { + found->second.written = true; + } + } + }); + } + + // The interfaces another module sees, and the ones a value boxed into `any` may be unboxed into. + void openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan) + { + // seen from another module: a word of its exported declarations, or in a reachable type. An + // executable's exports are seen by no other module (USER decision, spec 31): only what it + // imports - a declared function, an imported global - comes from outside. + if (!executable) + { + exportWords = exportedWords(module); + } + + llvm::StringSet<> exposed; + llvm::DenseSet seen; + for (auto &entry : functions) + { + // a `public` method of a class no other module can name is not one: only a declaration + // re-parsed from `__decls` names a function of another module + auto exported = !executable && (entry.second->hasAttr("export") || entry.second->hasAttr(DLL_EXPORT)); + if (entry.second.isDeclaration() || exported) + { + anyInterfaces(entry.second.getFunctionType(), exposed, seen, &nameableClasses); + } + } + + for (auto &entry : globals) + { + auto globalOp = entry.second; + // another module's global, declared here with no initializer (an import of a .ts module + // carries `export`) + auto imported = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || + globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); + auto exported = !globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT); + if (imported || (exported && !executable)) + { + anyInterfaces(globalOp.getType(), exposed, seen, &nameableClasses); + } + } + + scan.openAll(exposed); + if (!scan.unboxedInto.empty() || scan.unboxedFunction) + { + scan.openAll(scan.boxed); + for (auto &entry : scan.boxed) + { + scan.castOpened.insert(entry.getKey()); + } + } + } + + // Every interface the members of one made out of another mention, recursively. + void openThroughMembers(InterfaceScan &scan) + { + // An interface value made out of another one (or out of `any`) is called with the other's + // members: `HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` hands an `Op` to a + // function that calls through `Op2` (review6r1 v5, v11f). So every interface either side's + // members mention is open too, and theirs, recursively (controller ruling, spec 31). + // + // A class's vtables for its interfaces are its members' families whether or not this module + // makes an interface value of the class (a method called directly then keeps its facts), and + // their slots' types are the interface's members' types: an interface whose values are all + // made from class objects is read through them (review6r2 d1-d3). + for (auto &[name, held] : scan.classHeld) + { + scan.madeWith[name].insert(held.begin(), held.end()); + } + + for (auto &[name, made] : scan.madeWith) + { + for (auto vtableName : made) + { + if (auto found = scan.vtables.find(vtableName); found != scan.vtables.end() && found->second.type) + { + for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) + { + if (position > 0) + { + memberTypeInterfaces(field.type, scan.memberInterfaces[name]); + } + } + } + } + } + + llvm::SmallVector work; + for (auto &entry : scan.castOpened) + { + work.push_back(entry.getKey().str()); + } + + llvm::StringSet<> expanded; + while (!work.empty()) + { + auto name = work.pop_back_val(); + if (!expanded.insert(name).second) + { + continue; + } + + scan.open.insert(name); + for (auto &member : scan.memberInterfaces[name]) + { + work.push_back(member.getKey().str()); + } + } + } + + // An interface with a vtable this pass could not tie to its values. + void openLooseVTables(InterfaceScan &scan) + { + // A vtable this pass could not read, or one no value of any interface is made with, that is + // named for an interface (`...vtbl`, `...vtbl`): the interface may have an + // implementation outside its families. Belt and braces - every way to make an interface + // value is traced above - but a missed member would get its family's facts. + llvm::DenseSet made; + for (auto &entry : scan.madeWith) + { + made.insert(entry.second.begin(), entry.second.end()); + } + + llvm::SmallVector loose(scan.unread.begin(), scan.unread.end()); + for (auto &entry : scan.vtables) + { + if (!made.contains(entry.getKey())) + { + loose.push_back(entry.getKey()); + } + } + + for (auto &entry : scan.madeWith) + { + auto name = entry.getKey().str(); + if (llvm::any_of(loose, [&](llvm::StringRef vtableName) { + return vtableName.ends_with("." + name + "..vtbl") || vtableName.starts_with(name + "."); + })) + { + scan.open.insert(entry.getKey()); + } + } + } + + // The families of each interface's slots - its methods and its function-typed fields - closed + // where nothing opened the interface. + void buildInterfaceFamilies(InterfaceScan &scan) + { + auto &words = exportWords; + auto isOpen = [&](llvm::StringRef name) { + // a namespaced interface is declared by its last name + auto last = name.rsplit('.').second; + return scan.allOpen || scan.open.contains(name) || words.contains(name) || (!last.empty() && words.contains(last)); + }; + + // the families + for (auto &[name, made] : scan.madeWith) + { + // a class vtable's slot holding a vtable this pass did not read (another module's) + auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return scan.vtables.count(vtableName); }); + auto closed = !isOpen(name) && allRead; + for (auto vtableName : made) + { + if (!scan.vtables.count(vtableName)) + { + continue; + } + + if (allRead) + { + interfaceVTableTypes[name].push_back(scan.vtables[vtableName].type); + } + + scan.tied.insert(vtableName); + auto &vtable = scan.vtables[vtableName]; + for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) + { + if (position == 0) + { + continue; + } + + auto &family = interfaceFamilies[{name.str(), static_cast(position)}]; + auto id = mlir::dyn_cast_or_null(field.id); + auto member = vtable.slots.lookup(position); + auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp(); + if (!family.field) + { + family.field = id; + } + + // a member that is not private (a `public` method) is open (findOpen): it has no + // facts to give, and gives the family none + family.closed = family.closed && closed && !vtable.written && id && family.field == id && funcOp && + !funcOp.isDeclaration() && !declaresCallerFacts(funcOp); + if (member && !llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + + // the same slot as a field: an object literal's field of function type + auto &fieldFamily = interfaceFieldFamilies[{name.str(), static_cast(position)}]; + auto target = vtable.fields.lookup(position); + auto refType = mlir::dyn_cast(field.type); + if (!fieldFamily.field) + { + fieldFamily.field = id; + } + + fieldFamily.closed = fieldFamily.closed && closed && !vtable.written && id && fieldFamily.field == id && + target.first && refType && mlir::isa(refType.getElementType()); + if (target.first && !llvm::is_contained(fieldFamily.targets, target)) + { + fieldFamily.targets.push_back(target); + } + } + } + } + } + + // What an open family holds, and what a vtable no interface value is made with holds. + void markInterfaceReached(InterfaceScan &scan) + { + // what an open family holds, and a vtable no interface value is made with, is reached + // through calls that are not resolved + auto reached = [&](mlir::StringAttr member) { + if (auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp()) + { + interfaceReached.insert(funcOp); + } + }; + + for (auto &[key, family] : interfaceFamilies) + { + if (!family.closed) + { + llvm::for_each(family.members, reached); + } + } + + for (auto &entry : scan.vtables) + { + if (!scan.tied.contains(entry.getKey())) + { + for (auto &slot : entry.second.slots) + { + reached(slot.second); + } + } + } + } + static constexpr const char *INTERFACE_REACHED_WHY = + "it is reached through an interface, whose calls are not given its facts"; + + // The parameters a function's result borrows and the parameters it keeps (spec 2.4); absent is + // rc's convention. A plain borrow names one parameter; a borrowing record (`record`, spec 32) + // any number, and its caller owns the box. + struct Facts + { + llvm::SmallVector borrows; + llvm::SmallVector owned; + bool record = false; + + bool empty() const + { + return borrows.empty() && owned.empty(); + } + + bool operator==(const Facts &other) const + { + return borrows == other.borrows && owned == other.owned && record == other.record; + } + + bool operator!=(const Facts &other) const + { + return !(*this == other); + } + }; + + // Each function's facts, from its body (local) and as its callers see them (effective: closed, + // and agreed by every family it is in). + llvm::DenseMap factsLocal; + llvm::DenseMap factsEffective; + // The parameters each body takes into something - a field, an element, a global, a keeping call, + // an owning local it assigns (localOwnedParams). A kept parameter that is not one of them is + // released on the paths that do not move it (OWN_PARAMS_RELEASED_ATTR_NAME, spec 33). + llvm::DenseMap> takenParams; + + // ---- Facts a signature says (`@keeps`, `@borrows`, `@lends`) ---- + // + // A function that says them has them whoever calls it: a caller in another module reads them + // from the declaration it re-parses. Its body is checked against them instead (checkDeclared). + + static llvm::ArrayRef declared(mlir::Operation *funcOp, llvm::StringRef name); + + static bool declaresFacts(mlir::Operation *funcOp); + + // The facts `@keeps` and `@lends` say (Facts): what callers move in, and what the result is a + // borrow of. + static bool declaresCallerFacts(mlir::Operation *funcOp); + + // The parameters `@lends` names: one for a plain borrow, any number for a borrowing record + // (spec 32). + static llvm::SmallVector declaredLends(mlir::Operation *funcOp); + + // What every function a call names declares under `name`, when they all declare the same; + // nothing when one of them does not. + llvm::SmallVector declaredByCallees(const Callees &callees, llvm::StringRef name); + + // The parameters, for an error, by the names MLIRGen recorded beside the marker. + static std::string describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices); + + // Functions this module defines that `@borrows`, and those whose state was found to borrow. + llvm::DenseMap> borrowsFound; + + // The argument of `funcOp` that `value` is, or a field of: its own value, its slot's, or a + // read out of either. -1 for anything else. + static int32_t parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp); + + // ---- The iterator protocol ---- + // + // `it.next()` returns a `{value, done}` record, and under own the caller owns what it holds, + // whoever implements it: a generator's `next` moves what it yields into the record (a yield of + // a borrow is an error in its body), and any other `next` is a function whose callers this pass + // cannot all see, which may not return a borrow. So the result is fresh even when the call goes + // through the `Iterator` interface, or into another module - the default library's `matchAll`. + + // A call of a function named `next` - read through an interface, out of a field, or by its + // symbol - whose one result is a `{value, done}` record. + static bool isIteratorNext(mlir::Operation *op) + { + if (op->getNumResults() != 1) + { + return false; + } + + auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); + if (!tupleType || tupleType.getFields().size() != 2) + { + return false; + } + + auto fieldName = [&](int index) { + auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); + return id ? id.getValue() : llvm::StringRef(); + }; + if (fieldName(0) != "value" || fieldName(1) != "done") + { + return false; + } + + auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; + if (auto callee = directCallee(op)) + { + return isNext(callee.getValue()); + } + + auto function = calleeValue(op); + if (!function) + { + return false; + } + + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + if (auto loadOp = function.getDefiningOp()) + { + if (auto refOp = loadOp.getReference().getDefiningOp()) + { + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + return identifier && isNext(identifier.getValue()); + } + + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + { + auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); + if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) + { + return false; + } + + auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); + return id && isNext(id.getValue()); + } + + return false; + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + return false; + } + + // A generator another module made: its state object's type is only its shape here, and what each + // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is + // known in that module alone. A release routine made here from the shape would give back what is + // not the state's. So this module's gives back none of it: only the `.captured` box's cells, and + // their values too unless the maker says it borrows them (`@borrows`). What the generator's own + // locals still hold when it is given up early leaks rather than being freed twice. + void findImportedStates(mlir::ModuleOp module) + { + llvm::MapVector states; + for (auto &call : calls) + { + if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) + { + continue; + } + + auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); + auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); + states[tupleType] = states.lookup(tupleType) || borrows; + } + + if (states.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + entries.append(existing.begin(), existing.end()); + } + + for (auto &[type, borrows] : states) + { + auto tupleType = mlir::cast(type); + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + // the box first, when it borrows; -1 names no field, so a box that owns is released + llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) + { + fields.push_back(static_cast(index)); + } + } + + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // A call whose result is a generator's state object (an object whose fields are `.step` and + // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the + // caller owns - in another module too, as the iteration that follows needs. + static bool makesGeneratorState(mlir::Operation *op) + { + if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) + { + return false; + } + + auto tupleType = storageTupleOfType(op->getResult(0).getType()); + if (!tupleType) + { + return false; + } + + auto hasStep = false; + auto hasNext = false; + for (auto field : tupleType.getFields()) + { + if (auto id = mlir::dyn_cast_or_null(field.id)) + { + hasStep = hasStep || id.getValue() == ".step"; + hasNext = hasNext || id.getValue() == "next"; + } + } + + return hasStep && hasNext; + } + + // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple + static mlir_ts::TupleType storageTupleOfType(mlir::Type type) + { + if (auto objectType = mlir::dyn_cast(type)) + { + if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) + { + return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); + } + + // an object a declaration names by its shape (another module's generator) + return mlir::dyn_cast(objectType.getStorageType()); + } + + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + type = valueRefType.getElementType(); + } + + return mlir::dyn_cast(type); + } + + // ---- Fields a generator yields ---- + // + // A generator's locals are fields of its state object, and `yield groups` puts what one holds into + // the `{value, done}` record its `next` returns: a borrow of the state, which the caller cannot + // keep. When the field is read for nothing but yields, and each value stored into it is yielded + // before the generator can stop (later in the same block), the yield moves the value instead: + // the read is fresh (OWN_FRESH_RESULT_ATTR_NAME) and the field owns nothing + // (OWN_YIELDED_FIELDS_ATTR_NAME). + + // `ts.Store(value, ts.PropertyRef(record, i))`, where `record` is a local a `ts.Constant` + // initializes: a field of a record built to be returned, a yield's. + static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == value + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); + } + + void findYieldedFields(mlir::ModuleOp module) + { + // (state type, field) -> its reads and its stores + struct FieldUses + { + llvm::SmallVector reads; + llvm::SmallVector stores; + bool other = false; + }; + + llvm::MapVector, FieldUses> fields; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); + auto position = static_cast(propertyRefOp.getPosition()); + if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || + !mlir::isa(propertyRefOp.getType()) || + !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) + { + return; + } + + auto &uses = fields[{tupleType, position}]; + for (auto *user : propertyRefOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + uses.reads.push_back(loadOp); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) + { + uses.stores.push_back(storeOp); + } + else if (!mlir::isa(user)) + { + uses.other = true; + } + } + }); + + // a field the signature pass already found borrowing is the caller's, not the generator's + llvm::DenseSet> borrowing; + if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + for (auto entry : entries.getAsRange()) + { + for (auto index : mlir::cast(entry[1]).asArrayRef()) + { + borrowing.insert({mlir::cast(entry[0]).getValue(), index}); + } + } + } + + llvm::MapVector> yielded; + for (auto &[key, uses] : fields) + { + if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) + { + continue; + } + + // read only to be put into a yield's record + auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { + auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { + return !mlir::isa(user); + }); + return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); + }); + + // each value stored is yielded later in its block: nothing between stops the generator + auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { + return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { + return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); + }); + }); + + if (!onlyYielded || !eachYielded) + { + continue; + } + + yielded[key.first].push_back(key.second); + for (auto loadOp : uses.reads) + { + loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + } + + if (yielded.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, indices] : yielded) + { + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); + } + + module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // A function that says `@borrows` and makes no state itself, but returns what a call that has + // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters + // the call's bounded arguments are, or are read out of. + void findBoundedForwarders() + { + llvm::DenseMap calleesOf; + for (auto &call : calls) + { + calleesOf[call.op] = &call.callees; + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) + { + continue; + } + + llvm::SmallVector found; + auto forwards = true; + for (auto value : returnedValues(funcOp)) + { + if (holdsNoBlock(value)) + { + continue; + } + + auto *call = rootOf(value).getDefiningOp(); + auto known = call ? calleesOf.find(call) : calleesOf.end(); + if (known == calleesOf.end()) + { + forwards = false; + break; + } + + llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); + if (bounded.empty() && known->second->funcs.size() == 1 && !known->second->viaCallback) + { + auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); + bounded.assign(made.begin(), made.end()); + } + + if (bounded.empty()) + { + forwards = false; + break; + } + + auto args = callArgs(call); + for (auto index : bounded) + { + auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; + if (param < 0) + { + forwards = false; + break; + } + + if (!llvm::is_contained(found, param)) + { + found.push_back(param); + } + } + } + + if (forwards && !found.empty()) + { + llvm::sort(found); + borrowsFound[funcOp] = found; + } + } + } + + // A body that does other than its signature says, and an override that says other than the + // method it overrides: either way a caller that trusts the signature frees twice or leaks. + void checkDeclared(); + + // Functions that destroy nothing their caller can reach. + llvm::DenseSet noDrops; + + // A class's vtable is a global named `..vtbl` that holds its `..instanceOf`. An + // interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call + // through an interface does not name what it reaches. + void collectClassVTables(mlir::ModuleOp module) + { + llvm::SmallVector>>> vtables; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto entries = classVTableEntries(globalOp); + if (!entries.empty()) + { + auto name = globalOp.getSymName(); + classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); + vtables.push_back({name, std::move(entries)}); + } + }); + + for (auto &[name, entries] : vtables) + { + for (auto [position, symbol] : entries) + { + families[{position, methodName(symbol.getValue())}].push_back(symbol); + } + } + } + + // Does a method reached through an open interface keep a parameter or return a borrow + // (interfaceReached)? Then its callers through the interface cannot be told, and it has no facts + // at all. + bool factsThroughInterface(mlir::Operation *funcOp, const Facts &facts); + + mlir_ts::TupleType storageTupleOf(mlir::Type type) + { + return own_facts::storageTupleOf(&getContext(), type); + } + + // The functions a class vtable holds, by position; empty for any other global. + static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) + { + llvm::SmallVector> entries; + if (!globalOp.getSymName().ends_with("..vtbl")) + { + return entries; + } + + auto isClass = false; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + auto symbolRefOp = insertOp.getValue().getDefiningOp(); + if (!symbolRefOp || insertOp.getPosition().size() != 1) + { + return; + } + + auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); + isClass = isClass || symbol.getValue().ends_with("..instanceOf"); + entries.push_back({insertOp.getPosition()[0], symbol}); + }); + + if (!isClass) + { + entries.clear(); + } + + return entries; + } + + static bool isClassVTableEntry(mlir::Operation *op) + { + auto globalOp = op->getParentOfType(); + return globalOp && !classVTableEntries(globalOp).empty(); + } + + // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An + // override keeps its base's method name, which is what puts both in one family. + std::string methodName(llvm::StringRef symbol) + { + size_t best = 0; + for (auto &entry : classNames) + { + auto name = entry.getKey(); + if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && + symbol[name.size()] == '.') + { + best = name.size() + 1; + } + } + + return symbol.drop_front(best).str(); + } + + llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier) + { + llvm::SmallVector members; + if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) + { + members = found->second; + } + + if (!llvm::is_contained(members, identifier)) + { + members.push_back(identifier); + } + + return members; + } + + // Adds `symbol`'s function to `callees`; false when it is not defined in this module. + bool addDefined(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || funcOp.isDeclaration()) + { + return false; + } + + if (!llvm::is_contained(callees.funcs, funcOp)) + { + callees.funcs.push_back(funcOp); + } + + return true; + } + + // A function declared here and defined in a library that exported its `__own_no_drops`: one + // linked with this module, through its import library. Or a C function declared + // `@nodrops` (OWN_DECLARED_NO_DROPS_ATTR_NAME), or one whose parameters and result hold no + // heap block (scalarDeclaration). + bool addImported(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || !funcOp.isDeclaration() || + !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME) || + (scalarDeclaration(funcOp) && !functionsEscape))) + { + return false; + } + + callees.imported = true; + callees.importedName = symbol.getValue(); + return true; + } + + // A number, a boolean, a character, or an enum or a literal of one: a value that holds no block + // and no function of this module, so given to foreign code it hands it neither. Not an opaque + // pointer, a reference, a string, a class, an interface, an object, a tuple, an array, a union + // with such a member, a function, `any` or `unknown`. + static bool scalarType(mlir::Type type) + { + if (auto literalType = mlir::dyn_cast(type)) + { + return scalarType(literalType.getElementType()); + } + + if (auto enumType = mlir::dyn_cast(type)) + { + return scalarType(enumType.getElementType()); + } + + return mlir::isa(type); + } + + // Every parameter and result scalar (a void result too), and no rest parameter. + static bool scalarSignature(mlir_ts::FunctionType functionType) + { + return !functionType.isVarArg() && llvm::all_of(functionType.getInputs(), scalarType) && + llvm::all_of(functionType.getResults(), + [](mlir::Type type) { return mlir::isa(type) || scalarType(type); }); + } + + // A body-less `declare function` of C (no `export` or import marker: not another TypeScript + // module's, whose declarations carry `export`) whose parameters and result are scalar + // (scalarType): given no block of this module, it can destroy none (controller ruling, spec 31: + // `sqrt`, `floor`, `pow`, `fabs`). The risk is a C function that calls back into the module + // (`signal(15, f as Opaque)` then `raise(15)`, review6r1 c3), which it can only do with a + // function it was given - so the rule is off in a module whose foreign surface could hand it + // one (functionsEscape) - or an exported function reached by name, which a `@nodrops` + // declaration takes on as well. + static bool scalarDeclaration(mlir_ts::FuncOp funcOp) + { + return funcOp.isDeclaration() && !funcOp->hasAttr("export") && !funcOp->hasAttr("import") && + !funcOp->hasAttr(DLL_EXPORT) && !funcOp->hasAttr(DLL_IMPORT) && scalarSignature(funcOp.getFunctionType()); + } + + // `ts.Load(ts.AddressOf @f)`, where the global `f` holds the address of a function exported + // by a library loaded at run time, looked up by name: that function, if the library exported + // its `__own_no_drops`. + bool addImportedByAddress(mlir::Value callee, Callees &callees) + { + auto loadOp = callee.getDefiningOp(); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!addressOfOp) + { + return false; + } + + auto globalOp = globals.lookup(addressOfOp.getGlobalName()); + if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + { + return false; + } + + // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` + auto ®ion = globalOp.getInitializerRegion(); + if (!region.hasOneBlock()) + { + return false; + } + + auto resultOp = mlir::dyn_cast(region.front().getTerminator()); + auto castOp = resultOp && resultOp->getNumOperands() == 1 + ? resultOp->getOperand(0).getDefiningOp() + : mlir_ts::CastOp(); + auto searchOp = castOp ? castOp.getIn().getDefiningOp() + : mlir_ts::SearchForAddressOfSymbolOp(); + auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); + auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); + if (!name || !importedNoDrops.contains(name.getValue())) + { + return false; + } + + callees.imported = true; + callees.importedName = name.getValue(); + return true; + } + + Callees resolve(mlir::Operation *op) + { + Callees callees; + if (auto callee = directCallee(op)) + { + callees.named.push_back(callee.getAttr()); + callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); + } + else if (mlir::isa(op)) + { + resolveValue(calleeValue(op), callees); + } + + if (!callees.known) + { + callees.funcs.clear(); + } + + return callees; + } + + // The function value a call goes through: a method of a class, virtual or not, a symbol, or + // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an + // interface's method - is unknown. + void resolveValue(mlir::Value callee, Callees &callees) + { + // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function + // keeps; a method of a class imported so is bound to its object first + auto function = callee; + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + auto loadOp = function.getDefiningOp(); + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) + { + callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); + } + + if (addImportedByAddress(callee, callees)) + { + callees.known = true; + return; + } + + if (auto getMethodOp = callee.getDefiningOp()) + { + callee = getMethodOp.getBoundFunc(); + } + + auto *def = callee.getDefiningOp(); + mlir::StringAttr identifier; + int64_t index = -1; + if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (isInstanceOfSlot(def)) + { + callees.known = true; + callees.instanceOf = true; + return; + } + else if (auto symbol = fieldFunctions.of(def)) + { + callees.known = addDefined(symbol, callees); + callees.fieldFunction = callees.known; + return; + } + else if (auto interfaceRefOp = mlir::dyn_cast_or_null(def)) + { + resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); + return; + } + else if (auto loadOp = mlir::dyn_cast_or_null(def)) + { + resolveCallback(loadOp, callees); + return; + } + else if (auto boundOp = mlir::dyn_cast_or_null(def)) + { + // a function-typed field of an interface, called: `ts.CreateBoundFunction( + // ts.ExtractInterfaceThis(i), ts.Load(ts.InterfaceSymbolRef(i, f)))` + auto loadOp = boundOp.getFunc().getDefiningOp(); + auto refOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::InterfaceSymbolRefOp(); + auto thisOp = boundOp.getThisVal().getDefiningOp(); + if (refOp && isInterfaceField(refOp.getResult()) && thisOp && thisOp.getInterfaceVal() == refOp.getInterfaceVal()) + { + resolveInterfaceSlot(refOp, interfaceFieldFamilies, callees); + } + + return; + } + + if (!identifier) + { + return; + } + + if (index < 0) + { + callees.named.push_back(identifier); + callees.known = addDefined(identifier, callees) || addImported(identifier, callees); + return; + } + + callees.named = familyOf(index, identifier); + resolveFamily(index, identifier, callees); + } + + // A method of a closed interface (interfaceFamilies), `ts.InterfaceSymbolRef(value, index)`, or a + // function-typed field of one (interfaceFieldFamilies), read and called: the family of the slot + // after the vtable's header - every function the vtables hold there, or every function the + // implementing literals' fields are ever given. Open, the call is unknown, as before. + template + void resolveInterfaceSlot(mlir_ts::InterfaceSymbolRefOp refOp, const std::map, Family> &families, + Callees &callees) + { + auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + if (!interfaceType || !identifier) + { + return; + } + + auto position = static_cast(refOp.getIndex()) + INTERFACE_VTABLE_HEADER_SLOTS; + auto found = families.find({interfaceType.getName().getValue().str(), position}); + if (found == families.end() || !found->second.closed || found->second.field != identifier || + found->second.members.empty()) + { + return; + } + + callees.named.assign(found->second.members.begin(), found->second.members.end()); + callees.viaInterface = true; + callees.known = true; + for (auto member : found->second.members) + { + callees.known = addDefined(member, callees) && callees.known; + } + } + + // Can a module other than this one call the function? A `public` method (not private, not + // exported itself) of a class no other module can name - not exported, not a word of the + // exported declarations, not in the type of an exported or declared function or of an exported + // or imported global (nameableClasses) - no: another module calls a function of this one through + // the declaration `__decls` gives it (controller ruling, spec 31). But a method of a class that + // extends a class another module can name, or one another module defines, yes, private or not: + // that module calls it through the base's vtable. + bool callableFromOutside(mlir_ts::FuncOp funcOp) + { + if (!funcOp.isPrivate() && (funcOp.isDeclaration() || funcOp->hasAttr("export") || funcOp->hasAttr(DLL_EXPORT))) + { + return true; + } + + // its class: the longest own class name that prefixes its symbol + auto symbol = funcOp.getSymName(); + llvm::StringRef owner; + for (auto &entry : ownClasses) + { + auto name = entry.getKey(); + if (name.size() > owner.size() && symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.') + { + owner = name; + } + } + + if (owner.empty()) + { + return !funcOp.isPrivate(); + } + + auto nameable = [&](llvm::StringRef name) { + auto last = name.rsplit('.').second; + return nameableClasses.contains(name) || exportWords.contains(name) || (!last.empty() && exportWords.contains(last)); + }; + + if (!funcOp.isPrivate() && nameable(owner)) + { + return true; + } + + // An override of a method of a nameable class, or of one another module defines (no own + // vtable here), fills a slot of that class's vtable: another module calls it through the + // base (review6r1 p5, p6, p7; controller ruling: any base of that kind on the chain). The + // base is the first field of a derived class's storage. + auto storage = classStorages.lookup(owner); + if (!storage) + { + // its storage is not known here: no telling what it extends + return !funcOp.isPrivate(); + } + + for (auto depth = 0; depth < 64; ++depth) + { + auto fields = storage.getFields(); + auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); + if (!base) + { + return false; + } + + auto name = base.getName().getValue(); + if (nameable(name) || !ownClasses.contains(name)) + { + return true; + } + + storage = base; + } + + return true; + } + + // Can foreign code learn of a function of this module, to call it back during a call of a + // scalar declaration (scalarDeclaration)? It can only through the module's foreign surface, or + // through a function turned into data (controller ruling, spec 31, fix round 5: a closed-world + // test, in place of listing the routes a function value takes out). So the rule is on only when: + // - every foreign symbol is scalar (scalarType): every body-less function (C's, another + // module's - `import`, a DLL's - a `declare class`'s, a `@cpp` class's slot; a rest parameter + // is not scalar), every global declared with no initializer or imported (a `declare var`, a + // `declare class`'s vtable and type name), every `@cpp` class's fields and its bases', with + // no method of its own (C++ calls one through the C++ vtable), every function found in a + // library at run time (`SearchForAddressOfSymbol`, seen through its cast); an opaque pointer + // or a reference anywhere in such a type is a value of foreign memory, not scalar; + // - no cast makes an opaque pointer or a reference out of a number (`0x1234`, then a + // store through it reaches foreign memory); + // - no cast makes a value of a type that holds no function (`any`, `unknown`, an opaque + // pointer, an integer, a reference; a boolean cannot carry one and does not count) out of one + // that holds one (`onSig as Opaque`, `k.m as u64`, `>k`; a class or object + // takes its fields along unless seen as a class or an interface); + // - no `inline_asm` or `call_intrinsic` is given a value whose type holds a function. + bool functionsEscape = false; + + // The types a holdsFunction walk has been through, with or without their fields. + using SeenTypes = llvm::DenseSet>; + + // Does a value of this type hold a function: the type itself, a union's or an optional's + // member, a tuple's field, an array's element, what a reference points to, an interface whose + // vtable holds one past its header - a method, a function-typed field, or a field whose type + // holds one, recursively (an interface with methods, a class with a function field, an array of + // them) - or whose vtable this module does not make values with (a foreign one: `declare + // function makeW(): W`), and with `fields` a class's or an object's fields. The header slot + // (`.instanceOf`, the compiler's own, which destroys nothing) does not count. + bool holdsFunction(mlir::Type type, bool fields, SeenTypes &seen) + { + if (!type || !seen.insert({type, fields}).second) + { + return false; + } + + if (mlir::isa(type)) + { + return true; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + auto found = interfaceVTableTypes.find(interfaceType.getName().getValue()); + if (found == interfaceVTableTypes.end()) + { + return true; + } + + return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { + return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), + [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); + }); + } + + auto any = [&](llvm::ArrayRef infos) { + return llvm::any_of(infos, [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, fields, seen); }); + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + return any(tupleType.getFields()); + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + return any(constTupleType.getFields()); + } + + if (auto classStorageType = mlir::dyn_cast(type)) + { + return fields && any(classStorageType.getFields()); + } + + if (auto objectStorageType = mlir::dyn_cast(type)) + { + return fields && any(objectStorageType.getFields()); + } + + if (mlir::isa(type) && !fields) + { + return false; + } + + auto found = false; + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { found = found || holdsFunction(sub, fields, seen); }); + return found; + } + + // The `@cpp` class storage a method's `this` is of, if any: the storage of its first + // parameter's class, when the method's symbol is `Class.name`. + mlir_ts::ClassStorageType cppClassOfMethod(mlir_ts::FuncOp funcOp) + { + auto inputs = funcOp.getFunctionType().getInputs(); + auto classType = inputs.empty() ? mlir_ts::ClassType() : mlir::dyn_cast(inputs.front()); + auto storage = classType ? mlir::dyn_cast(classType.getStorageType()) : mlir_ts::ClassStorageType(); + if (!storage || !storage.isCpp()) + { + return {}; + } + + auto name = storage.getName().getValue(); + auto symbol = funcOp.getSymName(); + return symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.' ? storage : mlir_ts::ClassStorageType(); + } + + // Does a value of this type carry an address of memory this module made - so a cast of it to + // an opaque pointer or a reference points into this module, not into foreign memory? + static bool addressType(mlir::Type type) + { + return mlir::isa(type); + } + + void findEscapingFunctions(mlir::ModuleOp module) + { + // every body-less function: C's, another module's, a `declare class`'s (its methods, its + // `..instanceOf`, its `..new`), a `@cpp` class's slot + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() && !scalarSignature(funcOp.getFunctionType())) + { + functionsEscape = true; + return; + } + + // a method of a `@cpp` class: C++ calls it through the class's C++ vtable + if (cppClassOfMethod(funcOp)) + { + functionsEscape = true; + return; + } + } + + // every global declared with no initializer or imported: a `declare var`, a `declare + // class`'s vtable (functions) and type name (a string) + for (auto &entry : globals) + { + auto globalOp = entry.second; + auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || + globalOp.getInitializerRegion().front().empty(); + if (foreign && !scalarType(globalOp.getType())) + { + functionsEscape = true; + return; + } + } + + // every `@cpp` class's fields; a base (the first field, a storage) is a `@cpp` class of its + // own, judged on its own + for (auto &entry : classStorages) + { + auto storage = entry.second; + if (!storage.isCpp()) + { + continue; + } + + for (auto [index, field] : llvm::enumerate(storage.getFields())) + { + auto base = index == 0 ? mlir::dyn_cast(field.type) : mlir_ts::ClassStorageType(); + if (base ? !base.isCpp() : !scalarType(field.type)) + { + functionsEscape = true; + return; + } + } + } + + module.walk([&](mlir::Operation *op) { + if (functionsEscape) + { + return; + } + + // a function found in a library at run time: scalar through every cast of its address + if (mlir::isa(op)) + { + functionsEscape = !llvm::all_of(op->getResult(0).getUsers(), [](mlir::Operation *user) { + auto castOp = mlir::dyn_cast(user); + auto functionType = castOp ? mlir::dyn_cast(castOp.getType()) : mlir_ts::FunctionType(); + return functionType && scalarSignature(functionType); + }); + return; + } + + // `inline_asm(...)` and `call_intrinsic(...)`: a function given to either is data + // of no type (review6r5 h1, h2: `inline_asm("mov $1, $0", "=r,r", onSig)`) + if (mlir::isa(op)) + { + functionsEscape = llvm::any_of(op->getOperands(), [&](mlir::Value operand) { + SeenTypes seen; + return holdsFunction(operand.getType(), /*fields=*/true, seen); + }); + return; + } + + auto castOp = mlir::dyn_cast(op); + if (!castOp) + { + return; + } + + // a vtable's address (a class's, an interface's, a thunk's cell) seen as an opaque + // pointer or an integer: the compiler's own, inside a vtable or storing an object's + // vtable, never a value a program gives C + auto vtableOp = op->getParentOfType(); + auto addressOfOp = addressOfThroughCasts(castOp.getIn()); + auto isVTable = [](llvm::StringRef name) { return name.ends_with("..vtbl") || name.contains("..vtbl."); }; + if ((vtableOp && isVTable(vtableOp.getSymName())) || (addressOfOp && isVTable(addressOfOp.getGlobalName()))) + { + return; + } + + // an opaque pointer or a reference made out of a number: an address of foreign memory + if (mlir::isa(castOp.getType()) && !addressType(castOp.getIn().getType())) + { + functionsEscape = true; + return; + } + + // a function seen as something that holds none; a boolean cannot carry one. A class or + // object takes its fields along, seen as anything (`any`, an opaque pointer, an integer, + // a reference: `>k`, then a load of the field, review6r5 g2) but a class + // (its base) or an interface (a view of it, read through its vtable only) + if (mlir::isa(castOp.getType())) + { + return; + } + + auto fields = !mlir::isa(castOp.getType()); + SeenTypes seenIn; + SeenTypes seenOut; + functionsEscape = holdsFunction(castOp.getIn().getType(), fields, seenIn) && + !holdsFunction(castOp.getType(), /*fields=*/true, seenOut); + }); + } + + // class name -> its storage, from every class type a function's signature or a global mentions + llvm::StringMap classStorages; + + void collectClassStorages(mlir::ModuleOp module) + { + llvm::DenseSet seen; + std::function visit = [&](mlir::Type type) { + if (!type || !seen.insert(type).second) + { + return; + } + + if (auto storage = mlir::dyn_cast(type)) + { + classStorages[storage.getName().getValue()] = storage; + for (auto &field : storage.getFields()) + { + visit(field.type); + } + + return; + } + + type.walkImmediateSubElements([](mlir::Attribute) {}, visit); + }; + + for (auto &entry : functions) + { + visit(entry.second.getFunctionType()); + } + + module.walk([&](mlir::Operation *op) { + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + visit(result.getType()); + } + } + }); + } + + // A class another module can see may be extended there, and its override is a candidate this + // module never sees: every member has to be private and defined here. + void resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) + { + callees.known = true; + for (auto member : familyOf(index, identifier)) + { + callees.known = addDefined(member, callees) && callees.known; + } + + callees.known = callees.known && llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp funcOp) { return !callableFromOutside(funcOp); }); + } + + // `ts.Cast(ts.VTableOffsetRef(vtable, 0))`, where `vtable` is the one an object's first word + // points to or an interface value's own: slot 0 of either is `.instanceOf` - a class's + // generated one, a string compare, or `.instanceOf.none` for an object literal behind an + // interface. Only mlirGenInstanceOfThroughVTable builds a call through it. Neither destroys + // anything. + static bool isInstanceOfSlot(mlir::Operation *def) + { + auto castOp = mlir::dyn_cast_or_null(def); + auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); + if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) + { + return false; + } + + // a class vtable, read out of an object's first word, or an interface's own vtable, + // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) + auto *vtable = slot.getVtable().getDefiningOp(); + if (auto classVTable = mlir::dyn_cast_or_null(vtable)) + { + return classVTable.getIndex() == 0; + } + + return mlir::isa_and_nonnull(vtable); + } + + // Is every use of this function value the callee of a call - through `ts.GetMethod`, with + // `ts.GetThis` reading the object beside it? + static bool onlyCalled(mlir::Value value) + { + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) && + calleeValue(user) == use.get() && use.getOperandNumber() == 0) + { + continue; + } + + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && onlyCalled(user->getResult(0))) + { + continue; + } + + return false; + } + + return true; + } + + // The closed world. A function is open when a caller this pass cannot see may reach it: it is + // not private (another module may call it), or its symbol is used other than by a direct call, + // a class vtable's entry, or a method reference used only as a callee. A virtual reference + // that escapes opens its whole family. + void findOpen(mlir::ModuleOp module) + { + for (auto &entry : functions) + { + if (callableFromOutside(entry.second)) + { + open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR + "' would say it in its signature"); + } + } + + // the module is a symbol table, which getSymbolUses does not look inside: walk its body + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + // an op this pass cannot read symbols through: nothing is closed + for (auto &entry : functions) + { + open.try_emplace(entry.second, "its callers cannot all be found"); + } + + return; + } + + for (auto &use : *uses) + { + auto symbol = use.getSymbolRef().getRootReference(); + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp) + { + continue; + } + + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && isClassVTableEntry(user)) + { + continue; + } + + // passed only to a parameter whose family is closed: its calls are resolved (spec 33) + if (callbackPassing.contains(user)) + { + continue; + } + + int64_t index = -1; + if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + + auto inFunction = !!user->getParentOfType(); + if (inFunction && mlir::isa(user) && + onlyCalled(user->getResult(0))) + { + // a virtual call this pass cannot resolve reaches its family without the facts + if (index >= 0) + { + Callees callees; + resolveFamily(index, symbol, callees); + if (!callees.known) + { + openFamily(index, symbol, "an override may be defined in another module"); + } + } + + continue; + } + + open.try_emplace(funcOp, "it is used other than by a call"); + if (index >= 0) + { + openFamily(index, symbol, "it is used other than by a call"); + } + } + } + + void openFamily(int64_t index, mlir::StringAttr symbol, const char *why) + { + for (auto member : familyOf(index, symbol)) + { + if (auto memberOp = functions.lookup(member.getValue())) + { + open.try_emplace(memberOp, why); + } + } + } + + // ---- Callbacks (spec 33) ---- + // + // `___array_reduce(arr, f, init)` calls its parameter `f`: an indirect call, which reaches + // whatever function its callers pass. When the function is closed - private, and every use of its + // symbol a direct call - the parameter is only ever called, and every caller passes there a + // function this module defines, by its symbol or as a closure over a capture box made right + // there, a call through the parameter reaches exactly those functions: the parameter's family. + // Its members are closed too, when their symbols are used only to be called directly or passed + // so; then the call gets the facts the members agree on (callFacts), and `__own_no_drops` when + // none of them destroys anything. A closure's body takes its capture box first: its parameter k + // is the call's argument k - 1 (CallbackMember::shift). + // + // A kept parameter is agreed on the other way round from a class family's (meetCallbacks): a + // member that only borrows a parameter another member keeps is given it to keep too, and releases + // it on every path (OWN_PARAMS_RELEASED_ATTR_NAME) - `reduce` with a callback that returns its + // accumulator on some paths and one that never does, in one `___array_reduce`. Anything + // else the members disagree on demotes them all, as a class family's members are. + + struct CallbackMember + { + mlir::StringAttr symbol; + int32_t shift = 0; + }; + + struct CallbackFamily + { + llvm::SmallVector members; + // the ops that name a member's symbol to pass it here + llvm::SmallVector passing; + bool closed = true; + }; + + // (function, parameter index) -> the functions its callers pass there + std::map, CallbackFamily> callbackFamilies; + // the members whose families disagreed (meetFacts), for the error in their bodies + llvm::DenseSet demotedCallbacks; + // uses of a member's symbol that only pass it to a closed family: no escape (findOpen) + llvm::DenseSet callbackPassing; + + static bool isFunctionValueType(mlir::Type type) + { + return mlir::isa(type); + } + + // Is every use of this parameter - through its slot, never assigned or captured - the function + // a call goes through? + static bool onlyCalledParam(mlir::BlockArgument argument) + { + for (auto *user : argument.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index)) + { + return false; + } + + for (auto *slotUser : varOp->getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + auto loadOp = mlir::dyn_cast(slotUser); + if (!loadOp) + { + return false; + } + + for (auto &use : loadOp->getUses()) + { + auto *call = use.getOwner(); + if (!isCall(call) || calleeValue(call) != use.get() || + llvm::is_contained(callArgs(call), use.get())) + { + return false; + } + } + } + } + + return true; + } + + // The function a caller passes as `value`: its symbol, seen as a function value through casts + // between function types, or a closure over a capture box built of it. Null for anything else. + static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift) + { + while (auto castOp = value.getDefiningOp()) + { + if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) + { + return {}; + } + + value = castOp.getIn(); + } + + if (auto refOp = value.getDefiningOp()) + { + shift = 0; + return refOp; + } + + auto boundOp = value.getDefiningOp(); + if (!boundOp || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return {}; + } + + shift = 1; + return boundOp.getFunc().getDefiningOp(); + } + + // Does the function value `value` go only where `passes` allows - as an argument of a call - and + // through casts between function types, a closure built of it, and that closure's own retains + // and releases? + static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0) + { + if (depth > 8) + { + return false; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || passes(use)) + { + continue; + } + + auto castOp = mlir::dyn_cast(user); + auto boundOp = mlir::dyn_cast(user); + if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) + { + continue; + } + + if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && + passedOnly(boundOp.getResult(), passes, depth + 1)) + { + continue; + } + + return false; + } + + return true; + } + + void collectCallbackFamilies(mlir::ModuleOp module) + { + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + return; + } + + llvm::DenseMap> usersOf; + for (auto &use : *uses) + { + if (auto funcOp = functions.lookup(use.getSymbolRef().getRootReference().getValue())) + { + usersOf[funcOp].push_back(use.getUser()); + } + } + + auto callsOnly = [&](mlir_ts::FuncOp funcOp) { + auto &users = usersOf[funcOp]; + return !users.empty() && llvm::all_of(users, [&](mlir::Operation *user) { + auto callee = directCallee(user); + return callee && callee.getValue() == funcOp.getSymName(); + }); + }; + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || callableFromOutside(funcOp) || declaresFacts(funcOp) || !callsOnly(funcOp)) + { + continue; + } + + for (auto argument : funcOp.getBody().front().getArguments()) + { + if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) + { + continue; + } + + auto index = static_cast(argument.getArgNumber()); + CallbackFamily family; + for (auto *user : usersOf[funcOp]) + { + auto args = callArgs(user); + auto shift = 0; + auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) + : mlir_ts::SymbolRefOp(); + auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); + if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) + { + family.closed = false; + break; + } + + auto symbol = member.getSymNameAttr(); + auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); + if (same != family.members.end() && same->shift != shift) + { + family.closed = false; + break; + } + + if (same == family.members.end()) + { + family.members.push_back({symbol, shift}); + } + + family.passing.push_back(refOp); + } + + callbackFamilies[{funcOp, index}] = std::move(family); + } + } + + // A member's symbol used anywhere else - a call through another value, a field, a vtable, a + // global - reaches callers this cannot see; so does one passed to a family that is not closed. + // Each family that has such a member is open, until nothing changes. + auto passesToClosed = [&](mlir::OpOperand &use) { + auto *call = use.getOwner(); + auto callee = directCallee(call); + auto calleeOp = callee ? functions.lookup(callee.getValue()) : mlir_ts::FuncOp(); + if (!calleeOp) + { + return false; + } + + // the argument this use is, by its operand's position + auto args = callArgs(call); + if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || + use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + { + return false; + } + + auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); + auto found = callbackFamilies.find({calleeOp, index}); + return found != callbackFamilies.end() && found->second.closed; + }; + + for (auto changed = true; changed;) + { + changed = false; + for (auto &[key, family] : callbackFamilies) + { + if (!family.closed) + { + continue; + } + + for (auto &member : family.members) + { + auto memberOp = functions.lookup(member.symbol.getValue()); + auto escapes = llvm::any_of(usersOf[memberOp], [&](mlir::Operation *user) { + if (auto callee = directCallee(user); callee && callee.getValue() == member.symbol.getValue()) + { + return false; + } + + auto refOp = mlir::dyn_cast(user); + return !refOp || !user->getParentOfType() || + !passedOnly(refOp.getResult(), passesToClosed); + }); + + if (escapes) + { + family.closed = false; + changed = true; + break; + } + } + } + } + + for (auto &[key, family] : callbackFamilies) + { + if (family.closed) + { + callbackPassing.insert(family.passing.begin(), family.passing.end()); + } + } + } + + // A call through a parameter whose family is closed: its members. + void resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) + { + auto varOp = loadOp.getReference().getDefiningOp(); + auto funcOp = loadOp->getParentOfType(); + auto index = -1; + if (!varOp || !funcOp || !isParameterSlot(varOp, index)) + { + return; + } + + auto found = callbackFamilies.find({funcOp.getOperation(), index}); + if (found == callbackFamilies.end() || !found->second.closed || found->second.members.empty()) + { + return; + } + + callees.viaCallback = true; + callees.known = true; + for (auto &member : found->second.members) + { + callees.named.push_back(member.symbol); + callees.known = addDefined(member.symbol, callees) && callees.known; + callees.shifts.push_back(member.shift); + } + + callees.known = callees.known && callees.shifts.size() == callees.funcs.size(); + } + + // A member's facts as a call through its family sees them: each parameter k as argument + // k - shift. False when a fact is about the capture box itself, which the call does not pass. + static bool shiftFacts(const Facts &facts, int32_t shift, Facts &shifted) + { + shifted = Facts(); + shifted.record = facts.record; + for (auto [from, to] : {std::make_pair(&facts.owned, &shifted.owned), std::make_pair(&facts.borrows, &shifted.borrows)}) + { + for (auto index : *from) + { + if (index < shift) + { + return false; + } + + to->push_back(index - shift); + } + } + + return true; + } + + // Can the body of `funcOp` be given parameter `index` to keep and release it where it does not + // move it: a plain slot (isParameterSlot), a block, not a handle, not a string? + bool canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index) + { + if (funcOp.getBody().empty() || static_cast(index) >= funcOp.getNumArguments()) + { + return false; + } + + auto argument = funcOp.getBody().front().getArgument(index); + if (isHandle(argument) || isCopyableString(argument.getType()) || holdsNoBlock(argument)) + { + return false; + } + + return llvm::any_of(argument.getUsers(), [&](mlir::Operation *user) { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + return varOp && isParameterSlot(varOp, position) && position == index; + }); + } + + // The members of each closed callback family agree (the section above), by promotion of a kept + // parameter or by demotion. True when any member's facts changed. + bool meetCallbacks(llvm::DenseSet &demoted) + { + auto changed = false; + for (auto &[key, family] : callbackFamilies) + { + if (!family.closed) + { + continue; + } + + llvm::SmallVector members; + llvm::SmallVector seen; + auto shiftable = true; + for (auto &member : family.members) + { + auto funcOp = functions.lookup(member.symbol.getValue()); + Facts shifted; + shiftable = shiftable && funcOp && shiftFacts(factsEffective.lookup(funcOp), member.shift, shifted); + members.push_back(funcOp); + seen.push_back(shifted); + } + + if (shiftable && llvm::all_equal(seen)) + { + continue; + } + + // a parameter some member keeps, and each other one only borrows: kept by all + llvm::SmallVector kept; + for (auto &facts : seen) + { + kept.append(facts.owned.begin(), facts.owned.end()); + } + + llvm::sort(kept); + kept.erase(std::unique(kept.begin(), kept.end()), kept.end()); + auto promotable = shiftable && llvm::all_of(seen, [&](const Facts &facts) { + return facts.borrows == seen.front().borrows && facts.record == seen.front().record && + llvm::none_of(kept, [&](int32_t index) { return llvm::is_contained(facts.borrows, index); }); + }); + + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + // a member another caller reaches without the facts is given none (findOpen) + for (auto index : kept) + { + promotable = promotable && !demoted.contains(funcOp) && !open.contains(funcOp) && + !interfaceReached.contains(funcOp) && + (llvm::is_contained(seen[position].owned, index) || + canReleaseParam(funcOp, index + member.shift)); + } + } + + if (promotable) + { + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + auto facts = factsEffective.lookup(funcOp); + for (auto index : kept) + { + auto param = index + member.shift; + if (!llvm::is_contained(facts.owned, param)) + { + facts.owned.insert(llvm::upper_bound(facts.owned, param), param); + } + } + + if (facts != factsEffective.lookup(funcOp)) + { + factsEffective[funcOp] = facts; + changed = true; + } + } + + continue; + } + + for (auto funcOp : members) + { + if (funcOp && !declaresCallerFacts(funcOp)) + { + demoted.insert(funcOp); + if (factsEffective.erase(funcOp)) + { + changed = true; + } + } + } + } + + return changed; + } + + // ---- Results that borrow an argument ---- + + // The values a function returns: what is stored into the local its returns read (MLIRGen's + // result slot), or what a return hands back directly. + static llvm::SmallVector returnedValues(mlir_ts::FuncOp funcOp); + + struct ResultBorrows + { + llvm::SmallVector borrows; + bool record = false; + }; + + // What the function's result borrows (spec 2.4, 32): the one parameter every heap value it returns + // is a borrow of, or, when every such value is a borrowing record, the parameters the records + // borrow. A result that holds no block (`null`, a number) agrees with any; a record on one path + // and anything else on another is nothing a caller can be told. + ResultBorrows localResultBorrows(mlir_ts::FuncOp funcOp); + + // The parameter a function returns as it is on some paths, where every other value it returns + // that holds a block is one it owns (spec 33): `return v.x > 2 ? acc : V.plus(acc, v)`. Such a + // parameter is kept: the callers move the argument in, the path that returns it moves it out, + // and every other path releases it. -1 for none - a parameter returned on every path is a + // borrowed result; two parameters returned, or a part of one (`acc.next`), or a borrowing + // record, have no such answer; a string is copied instead (spec 22). + int32_t mixedReturnParam(mlir_ts::FuncOp funcOp); + + // ---- Borrowing records (spec 32) ---- + // + // `{ thing: this, ray: ray, dist: d }` given as an interface: an object literal's box holding values + // it does not own beside values it does. Rust's `struct Hit<'a> { thing: &'a Th, .. }`: the box is + // its owner's - the caller's, when it is returned - and it may not outlive what it borrows. Each box + // is classified where it is made: the fields it borrows (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, on the + // box and on every interface made of it, whose tag then names a release routine that leaves them), + // and, when every one of them is a parameter, `this`, or a read out of one, those parameters - what + // a function returning it lends (OWN_RESULT_RECORD_ATTR_NAME). The inference bounds every use. + + struct RecordBox + { + llvm::SmallVector params; + // every borrowed field is a parameter's, `this`'s, a read out of one, or immortal + bool returnable = true; + }; + + llvm::DenseMap recordBoxes; + + void classifyRecordBoxes() + { + recordBoxes.clear(); + getOperation().walk([&](mlir_ts::NewOp newOp) { + newOp->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + for (auto *user : newOp->getUsers()) + { + for (auto *viewUser : user->getUsers()) + { + if (mlir::isa(viewUser)) + { + viewUser->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + } + } + } + + classifyRecordBox(newOp); + }); + } + + void classifyRecordBox(mlir_ts::NewOp newOp) + { + auto boxOp = recordBoxOf(newOp.getResult()); + auto varOp = boxOp == newOp ? recordLocalOf(newOp.getResult()) : mlir_ts::VariableOp(); + if (!varOp) + { + return; + } + + // the record read once, and only into its box (rc's retain of it beside) + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + if (read) + { + return; + } + + read = loadOp; + } + } + + for (auto *user : read->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (!mlir::isa(user) && !(storeOp && storeOp.getReference() == newOp.getResult())) + { + return; + } + } + + // the box seen only as interfaces: every release of it reads their tag + llvm::SmallVector interfaces; + for (auto &use : newOp->getUses()) + { + auto *user = use.getOwner(); + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) + { + continue; + } + + auto castOp = mlir::dyn_cast(user); + if (!castOp || !isView(castOp)) + { + return; + } + + for (auto *castUser : castOp->getUsers()) + { + auto newInterfaceOp = mlir::dyn_cast(castUser); + if (!newInterfaceOp || newInterfaceOp.getThisVal() != castOp.getResult()) + { + return; + } + + interfaces.push_back(castUser); + } + } + + if (interfaces.empty()) + { + return; + } + + // each field that holds a block: owned (made here, or nothing) or borrowed, never both + RecordBox info; + llvm::SmallVector borrowed; + auto mth = typeHelper(&getContext()); + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + auto field = propertyRefOp.getResult(); + auto refType = mlir::dyn_cast(field.getType()); + if (!refType || !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) + { + continue; + } + + auto owns = false; + auto borrows = false; + for (auto *fieldUser : field.getUsers()) + { + if (mlir::isa(fieldUser)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != field) + { + return; + } + + auto value = storeOp.getValue(); + switch (recordFieldSource(value)) + { + case RecordFieldSource::NoBlock: + continue; + case RecordFieldSource::Fresh: + owns = true; + continue; + case RecordFieldSource::Unknown: + return; + case RecordFieldSource::Immortal: + borrows = true; + continue; + case RecordFieldSource::Param: + case RecordFieldSource::Place: + if (auto param = borrowedParam(value); param >= 0) + { + info.params.push_back(param); + } + else + { + info.returnable = false; + } + + borrows = true; + continue; + case RecordFieldSource::Slot: + case RecordFieldSource::Cell: + info.returnable = false; + borrows = true; + continue; + } + } + + // a string is copied, not borrowed (spec 22) + if ((owns && borrows) || (borrows && isCopyableString(refType.getElementType()))) + { + return; + } + + if (borrows) + { + borrowed.push_back(propertyRefOp.getPosition()); + } + } + + if (borrowed.empty()) + { + return; + } + + llvm::sort(borrowed); + borrowed.erase(std::unique(borrowed.begin(), borrowed.end()), borrowed.end()); + llvm::sort(info.params); + info.params.erase(std::unique(info.params.begin(), info.params.end()), info.params.end()); + + auto attr = mlir::DenseI32ArrayAttr::get(&getContext(), borrowed); + newOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + for (auto *newInterfaceOp : interfaces) + { + newInterfaceOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + } + + recordBoxes[newOp.getOperation()] = std::move(info); + } + + enum class RecordFlow + { + None, // no borrowing record + Record, // borrowing records only (or values that hold no block) + Bad // a record that borrows what is not a parameter, or one beside anything else + }; + + // Whether `value` - through views, merges and the owning locals that hold it - is a borrowing + // record, and the parameters it borrows: a box made here, or a call's result that is one, whose + // arguments are this function's parameters or reads out of them. + RecordFlow recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms) + { + llvm::SmallVector work{value}; + llvm::SmallPtrSet seen; + auto record = false; + auto other = false; + while (!work.empty()) + { + // what holds no block, by its own type (a box's `value_ref` owns nothing by type, but + // the object its view shows does) + auto next = work.pop_back_val(); + auto current = rootOf(next); + if (!seen.insert(current).second || holdsNoBlock(next)) + { + continue; + } + + auto *def = current.getDefiningOp(); + if (auto found = def ? recordBoxes.find(def) : recordBoxes.end(); found != recordBoxes.end()) + { + if (!found->second.returnable) + { + return RecordFlow::Bad; + } + + params.append(found->second.params.begin(), found->second.params.end()); + record = true; + continue; + } + + if (def && isCall(def) && isRecordResult(def)) + { + auto args = callArgs(def); + for (auto index : resultBorrowsAll(def)) + { + if (static_cast(index) >= args.size()) + { + return RecordFlow::Bad; + } + + auto arg = args[index]; + if (argumentHoldsNoBlock(arg) || isImmortalGlobalRead(arg)) + { + continue; + } + + auto param = borrowedParam(arg); + if (param < 0) + { + return RecordFlow::Bad; + } + + params.push_back(param); + } + + record = true; + continue; + } + + if (auto argument = mlir::dyn_cast(current)) + { + llvm::SmallVector merged; + if (mergedInto(argument, merged)) + { + work.append(merged.begin(), merged.end()); + continue; + } + + other = true; + continue; + } + + // what an owning local holds: its initializer and every value stored into it + auto loadOp = mlir::dyn_cast_or_null(def); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) + { + if (varOp.getInitializer()) + { + work.push_back(varOp.getInitializer()); + } + + for (auto *user : varOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == varOp.getResult()) + { + work.push_back(storeOp.getValue()); + } + } + + continue; + } + + other = true; + } + + if (!record) + { + return RecordFlow::None; + } + + return other ? RecordFlow::Bad : RecordFlow::Record; + } + + // ---- Parameters the callee keeps ---- + + // Does `user` take `used` - a read of a parameter - into something that outlives the call: a + // store into a field, an element or a global, an insertion into an array, or a call that keeps + // it in turn? A local that holds it, and the result it is returned as, do not count: the first + // is a borrow, the second a borrowed result. + static bool keeps(mlir::Operation *user, mlir::Value used); + + // The parameters the body keeps (spec 2.4, owned-by-callee): a read of the parameter's slot - + // never assigned - that something keeps. + static llvm::SmallVector localOwnedParams(mlir_ts::FuncOp funcOp); + + // ---- The facts callers must know ---- + + // Owned parameters and a result that borrows only hold where every caller knows them, so the + // facts survive only on a closed function whose every family agrees, and families are + // demoted until they do: a member that loses its facts can make another family disagree. A + // caller that returns what such a call returns, or passes a parameter on to a kept one, gets + // the fact too, so the facts are recomputed with the calls pinned until nothing changes; if + // that does not settle, nobody gets any. + void computeFacts(); + + void meetFacts(); + + Facts callFacts(const Callees &callees); + + static void setFacts(mlir::Operation *op, const Facts &facts); + + // The calls get the facts their callees agree on; each function gets its own, or the reason it + // lost those its body has, for the error there. + void pinFacts(const llvm::DenseMap &pinned); + + // ---- Generators that borrow (phase 7b) ---- + // + // A generator over a parameter that holds a block - `function* each(a: number[])` - keeps the + // parameter's cell in a box in its state object's `.captured` field. Under 5b's rule that is an + // escaping closure over a value that is the caller's, an error. Instead the state object + // borrows: the caller owns it, it may not outlive the argument (OWN_RESULT_BOUNDED_ATTR_NAME), + // and its release gives back neither the parameter's value nor any field that only ever holds + // what was read out of it (OWN_BORROWING_FIELDS_ATTR_NAME). A state type is unique to its + // generator - its `next` field's type names a per-site `object_storage` - so a fact per type is + // a fact per generator, which is what the per-type release routine needs. + + struct BorrowingState + { + mlir_ts::FuncOp maker; + int32_t captured = -1; + llvm::SmallVector bounded; + llvm::SetVector fields; + }; + + static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name) + { + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (auto id = mlir::dyn_cast_or_null(field.id); id && id.getValue() == name) + { + return static_cast(index); + } + } + + return -1; + } + + // The box the maker stores into the `.captured` field of the initial state it copies into + // `newOp`'s block: `ts.Store(box, ts.PropertyRef(state, captured))`, `state` a local whose only + // read fills the block. + static mlir::Value boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured) + { + mlir_ts::StoreOp fill; + for (auto *user : newOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) + { + fill = storeOp; + } + } + + auto loadOp = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); + auto stateOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp) + { + return {}; + } + + mlir::Value box; + for (auto *user : stateOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp || propertyRefOp.getPosition() != captured) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || box) + { + return {}; + } + + box = storeOp.getValue(); + } + } + + auto boxOp = box ? box.getDefiningOp() : mlir_ts::VariableOp(); + return boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); + } + + // Is every caller of `funcOp` a direct call this pass sees? It is private, and each use of its + // symbol is a call, or a closure over it that is only retained and released - `.map(f)` builds + // one and calls its body straight through the box. + bool calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo = false) + { + if (!funcOp.isPrivate() && !publicToo) + { + return false; + } + + auto uses = mlir::SymbolTable::getSymbolUses(funcOp.getSymNameAttr(), &module.getBodyRegion()); + if (!uses) + { + return false; + } + + for (auto &use : *uses) + { + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + // a method whose signature gives its callers the bound: called through its class's + // vtable or a reference to it, which the facts reach too (declaredByCallees) + if (publicToo && + (isClassVTableEntry(user) || + (mlir::isa(user) && + onlyCalled(user->getResult(0))))) + { + continue; + } + + auto symbolRefOp = mlir::dyn_cast(user); + if (!symbolRefOp) + { + return false; + } + + for (auto *refUser : symbolRefOp->getUsers()) + { + auto boundOp = mlir::dyn_cast(refUser); + if (!boundOp || !llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { + return mlir::isa(boundUser); + })) + { + return false; + } + } + } + + return true; + } + + // A parameter's cell: a captured local declared from an argument of `maker`'s entry block, that + // owns nothing (its value is the caller's). + static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) + { + auto argument = cellOp.getInitializer() ? mlir::dyn_cast(cellOp.getInitializer()) + : mlir::BlockArgument(); + if (!cellOp.getCaptured().value_or(false) || isOwningVariable(cellOp) || !argument || + !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + + // `ts.Load` of one of `maker`'s parameter slots (isParameterSlot): `index` is its position. + static bool readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + int position = -1; + if (!varOp || !isParameterSlot(varOp, position) || varOp->getParentOfType() != maker) + { + return false; + } + + index = static_cast(position); + return true; + } + + // `ts.Load(ts.PropertyRef(argument, i))` of one of `maker`'s parameters: `index` is its position. + static bool readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp->getOperand(0)) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + + bool ownsHeap(mlir::Location location, mlir::Type type); + + void findBorrowingStates(mlir::ModuleOp module) + { + llvm::MapVector states; + llvm::DenseMap made; + module.walk([&](mlir_ts::NewOp newOp) { + auto valueRefType = mlir::dyn_cast(newOp.getType()); + auto tupleType = valueRefType ? mlir::dyn_cast(valueRefType.getElementType()) + : mlir_ts::TupleType(); + if (!tupleType) + { + return; + } + + ++made[tupleType]; + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + auto maker = newOp->getParentOfType(); + // an open maker has callers this pass cannot give the bound to, unless its signature + // gives it to them (`@borrows`) + if (captured < 0 || !maker || + !calledOnlyDirectly(module, maker, /*publicToo=*/maker->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME))) + { + return; + } + + auto box = boxStoredIntoState(newOp, captured); + if (!box) + { + return; + } + + BorrowingState state{maker, captured}; + for (auto *user : box.getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp) + { + return; + } + + auto value = storeOp.getValue(); + int32_t index = -1; + if (auto cellOp = value.getDefiningOp(); cellOp && isParameterCell(cellOp, maker, index)) + { + auto cellType = mlir::cast(cellOp.getType()); + // a handle parameter's cell holds a count of its own (spec 23.2), which only + // the box's release gives back: a box holding one owns what it holds + if (MLIRTypeHelper::isSharedHandleType(cellType.getElementType())) + { + return; + } + + if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) + { + state.bounded.push_back(index); + } + + continue; + } + + // a copy of what a parameter holds in a field: `.map`'s maker is a closure body + // that copies its own box's fields into the generator's + if (!ownsHeap(value.getLoc(), value.getType())) + { + continue; + } + + // `this`, which the box holds by value (`.this`), not in a cell: read out of + // the parameter's slot + if (readsParameterSlot(value, maker, index)) + { + if (MLIRTypeHelper::isSharedHandleType(value.getType())) + { + return; + } + + if (!llvm::is_contained(state.bounded, index)) + { + state.bounded.push_back(index); + } + + continue; + } + + if (!readsParameterField(value, maker, index)) + { + return; // a box with anything but what the parameters hold owns what it holds + } + + if (!llvm::is_contained(state.bounded, index)) + { + state.bounded.push_back(index); + } + } + } + + if (!state.bounded.empty()) + { + states[tupleType] = state; + } + }); + + // made in one place only: the maker is the one function a bound can come from + states.remove_if([&](auto &entry) { return made[entry.first] != 1; }); + for (auto &[type, state] : states) + { + auto bounded = state.bounded; + llvm::sort(bounded); + borrowsFound[state.maker] = bounded; + } + if (states.empty()) + { + return; + } + + // Each store into a field of a state type, by field: what the field may be given. + llvm::DenseMap, llvm::SmallVector> stored; + module.walk([&](mlir_ts::StoreOp storeOp) { + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); + if (tupleType && states.count(tupleType)) + { + stored[{tupleType, propertyRefOp.getPosition()}].push_back(storeOp.getValue()); + } + }); + + for (auto &[type, state] : states) + { + auto tupleType = mlir::cast(type); + // least fixpoint: a field borrows once everything stored into it is a read of a borrowing + // cell or field + for (auto changed = true; changed;) + { + changed = false; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + auto position = static_cast(index); + auto found = stored.find({type, position}); + if (position == state.captured || state.fields.contains(position) || found == stored.end() || + !ownsHeap(state.maker.getLoc(), field.type)) + { + continue; + } + + if (llvm::all_of(found->second, [&](mlir::Value value) { return readsBorrowed(value, tupleType, state); })) + { + state.fields.insert(position); + changed = true; + } + } + } + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, state] : states) + { + llvm::SmallVector fields{state.captured}; + fields.append(state.fields.begin(), state.fields.end()); + entries.push_back(mlir::ArrayAttr::get( + context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded)); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // Is `value` a read of what a state of `tupleType` borrows: the value in one of its box's cells + // (`ts.Load` of `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`), or a + // borrowing field's (`ts.Load(ts.PropertyRef(state, p))`)? + bool readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state) + { + auto loadOp = rootOf(value).getDefiningOp(); + if (!loadOp) + { + return false; + } + + auto isStateField = [&](mlir::Value ref, int32_t &position) { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp || storageTupleOf(propertyRefOp->getOperand(0).getType()) != tupleType) + { + return false; + } + + position = propertyRefOp.getPosition(); + return true; + }; + + int32_t position = -1; + if (isStateField(loadOp.getReference(), position)) + { + return state.fields.contains(position); + } + + // a field of the box the state holds, copied in by value, or a cell read out of it + auto isBoxField = [&](mlir::Value ref) { + auto fieldRef = ref.getDefiningOp(); + auto boxLoad = fieldRef ? fieldRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; + }; + + if (isBoxField(loadOp.getReference())) + { + return true; + } + + auto cellLoad = loadOp.getReference().getDefiningOp(); + return cellLoad && isBoxField(cellLoad.getReference()); + } + + // ---- Globals given a borrow ---- + + // A global initialized with a read of another global, a field or an element (`let k = a`, + // `let k = h.c`) would be a second owner of a block another owner keeps: the first overwrite + // of either frees what the other still reads, and both give it back at exit. rc counts the + // global's own reference (OwnedReturnConsumptionPass, claimGlobalInitializers); own cannot + // move the value out of its owner, and a global cannot hold a borrow. A handle is counted + // under own too. + void rejectBorrowedGlobalInitializers(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto *block = globalOp.getInitializerBlock(); + auto resultOp = block && !block->empty() ? mlir::dyn_cast(block->getTerminator()) + : mlir_ts::GlobalResultOp(); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + auto value = resultOp->getOperand(0); + while (value.getDefiningOp() && isView(value.getDefiningOp())) + { + value = value.getDefiningOp()->getOperand(0); + } + + auto loadOp = value.getDefiningOp(); + auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; + // an immortal global's value (spec 25) is owned by no one: any number of globals hold it + if (!place || !mlir::isa(place) || + isHandle(value) || isConstantData(value) || isImmortalGlobalRead(value) || + !ownsHeap(value.getLoc(), value.getType())) + { + return; + } + + resultOp.emitError("the initializer of global '") + << globalOp.getSymName() + << "' reads a value another owner keeps; -mm=own cannot make a global a second owner"; + signalPassFailure(); + }); + } + + // ---- Drops ---- + + // Least fixpoint: a function drops nothing until its body or a call it makes shows it may. + void computeDrops(); + + bool callMayDrop(const Callees &callees); + + // ---- Globals a call may assign ---- + + // The globals each call may give a new value, pinned on the call for the inference + // (OWN_ASSIGNS_GLOBALS_ATTR_NAME, OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME): a store to a global, or its + // release, in a callee's body, or in a call it makes. A call that may drop and whose callees are + // not known may assign any; one that drops nothing assigns none. A read of a global ends where + // the global is given a new value (checkGlobalRead), and a call that does so ends it as surely as + // a store in the same function: `const c = G; reset(); print(c.x)`. + void pinAssignedGlobals() + { + llvm::DenseMap> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].push_back(&call); + } + + llvm::DenseMap> assigns; + llvm::DenseSet assignsAny; + for (auto &entry : functions) + { + auto funcOp = entry.second; + funcOp.walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + else if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + + if (auto addressOfOp = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp()) + { + assigns[funcOp].insert(addressOfOp.getGlobalName()); + } + }); + } + + // what a call may assign, from what its callees do so far + auto assignedBy = [&](Call &call, llvm::SetVector &globals) { + if (!callMayDrop(call.callees)) + { + return false; + } + + if (!call.callees.known) + { + return true; + } + + auto any = false; + for (auto callee : call.callees.funcs) + { + any |= assignsAny.contains(callee); + if (auto found = assigns.find(callee); found != assigns.end()) + { + globals.insert(found->second.begin(), found->second.end()); + } + } + + return any; + }; + + for (auto changed = true; changed;) + { + changed = false; + for (auto &entry : functions) + { + auto funcOp = entry.second; + for (auto *call : callsIn[funcOp]) + { + llvm::SetVector globals; + if (assignedBy(*call, globals)) + { + changed |= assignsAny.insert(funcOp).second; + } + + for (auto global : globals) + { + changed |= assigns[funcOp].insert(global); + } + } + } + } + + for (auto &call : calls) + { + llvm::SetVector globals; + if (assignedBy(call, globals)) + { + call.op->setAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + continue; + } + + if (globals.empty()) + { + continue; + } + + llvm::SmallVector names; + for (auto global : globals) + { + names.push_back(mlir::StringAttr::get(&getContext(), global)); + } + + call.op->setAttr(OWN_ASSIGNS_GLOBALS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), names)); + } + } + + // An argument read out of a global (`run(G)`, `G.m()`) is lent to the callee, and the callee may + // free it under its own parameter: `function run(h: C) { G = mk(); print(h.x); }`. The caller's + // borrow ends at the call that takes it, so nothing in the caller sees that, and in the callee + // the parameter is not known to be the global's. A call that may drop is an error for such an + // argument, when something assigns that global. (A field of a global given that way is a + // borrow of a place, which the inference already bounds by the call.) + void rejectGlobalArgumentsDropped() + { + for (auto &call : calls) + { + if (!callMayDrop(call.callees)) + { + continue; + } + + for (auto argument : callArgs(call.op)) + { + // a method's object: `this` of the method bound to it + if (auto getThisOp = argument.getDefiningOp()) + { + auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); + if (mlir::isa_and_nonnull(boundOp)) + { + argument = boundOp->getOperand(0); + } + } + + // an immortal global (spec 25) is written by its initializer only, and that runs once + // (a static's is a store in its static constructor, outside any loop, which + // writtenGlobals counts), and no release frees its block + if (isHandle(argument) || isImmortalGlobalRead(argument) || !ownsHeap(call.op->getLoc(), argument.getType())) + { + continue; + } + + auto loadOp = rootOf(argument).getDefiningOp(); + auto global = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!global || !writtenGlobals.contains(global.getGlobalName())) + { + continue; + } + + call.op->emitError("a value read out of the global '") + << global.getGlobalName() + << "' is lent to a call that may release or overwrite it; -mm=own cannot prove it outlives the call"; + signalPassFailure(); + } + } + } + + // ---- Calls in a global's initializer ---- + + // A global's region is no function: its calls get no facts pinned, and the inference never sees + // it, so its calls were read with the defaults - an owned result, borrowed arguments. A result + // that borrows an argument (`const c = child(n)`, `id(new V(1))`) then made the global a second + // owner of a block its argument's owner keeps, or kept one the region freed; and a fresh + // argument a callee keeps (`new N(new V(4))`, the constructor keeping `c`) was released at the + // end of the region, under the object that kept it. A kept argument made in the region and used + // by nothing after the call moves into the callee: its release goes. Anything else is an error. + void ownGlobalInitializerCalls(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + // collected first: a move erases a release the walk has not reached yet + llvm::SmallVector calls; + globalOp.walk([&](mlir::Operation *op) { + if (isCall(op)) + { + calls.push_back(op); + } + }); + + for (auto *op : calls) + { + auto callees = resolve(op); + auto facts = callFacts(callees); + auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName(); + if (!facts.borrows.empty()) + { + op->emitError("the initializer of global '") + << globalOp.getSymName() << "' holds the result of '" << calleeName + << "', which borrows its argument; a global cannot hold a borrow under -mm=own"; + signalPassFailure(); + continue; + } + + auto args = callArgs(op); + llvm::SmallPtrSet given; + for (auto index : facts.owned) + { + if (index < 0 || index >= static_cast(args.size()) || + !given.insert(rootOf(args[index])).second || !moveIntoCallee(op, args[index])) + { + op->emitError("the initializer of global '") + << globalOp.getSymName() << "' gives '" << calleeName + << "' a value it keeps, but -mm=own cannot move it there"; + signalPassFailure(); + break; + } + } + } + }); + } + + // `value` was made in the call's block and nothing but its release, if it has one, comes after + // the call: the release goes, and the callee owns the block. Constant data (a string literal) + // is nobody's, and is given as it is. + static bool moveIntoCallee(mlir::Operation *call, mlir::Value value) + { + auto root = rootOf(value); + if (isConstantData(root)) + { + return true; + } + + if (!isFresh(root) || root.getDefiningOp()->getBlock() != call->getBlock()) + { + return false; + } + + mlir::Operation *release = nullptr; + auto usedAfter = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { + if (user == call) + { + return; + } + + if (user->getBlock() != call->getBlock()) + { + usedAfter = true; + } + else if (call->isBeforeInBlock(user)) + { + usedAfter = usedAfter || release || !mlir::isa(user); + release = mlir::isa(user) ? user : release; + } + }); + + if (usedAfter) + { + return false; + } + + if (release) + { + release->erase(); + } + + return true; + } + + // ---- Export ---- + + // A library says, beside its memory-model marker, which of its exported functions destroy + // nothing (SHARED_LIB_OWN_FACTS). The global is a copy of the marker `__tsmm_own_` + // under another name and with another value, so it is exported exactly as the marker is, and + // MLIRGen emits the same under own as under rc. No marker, nothing is exported. + void exportNoDrops(); + + // Does the body itself destroy something a caller may reach: overwrite a field or an element + // of a block it did not make, assign a global, remove elements from an array it did not make, + // or `delete`? A local's own releases are not drops, nor is the constructor's filling of the + // object it is building. + bool dropsInBody(mlir_ts::FuncOp funcOp); + + // Can the block this reference points into be one the function's caller can reach? Walks up + // from a place to what holds it. Not when every way up ends in a block the function made (an + // allocation, a call's result it owns) or in an owning local that only ever held such blocks, + // or, in a constructor, in the object being built. + bool reachesOutside(mlir::Value ref, mlir_ts::FuncOp funcOp); + + static bool isAssigned(mlir_ts::VariableOp varOp); + + // An owning, uncaptured local whose every value - its initializer and each assignment - is one + // the function made. + static bool holdsOnlyFresh(mlir_ts::VariableOp varOp); +}; + +} // namespace own_signature + +#endif // MLIR_TYPESCRIPT_OWNERSHIPSIGNATUREPASS_H From f826628dd312ef826d91b9a84ac98cae283a9fbc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:14:28 +0100 Subject: [PATCH 08/25] own passes: OwnershipSignatureGlobals.cpp out of OwnershipSignaturePass.h (I1) Globals - what an initializer moves in, immortal globals (spec 25), globals given a borrow, calls in a global's initializer, the globals a call may assign. A pure move of movesIntoResult, holdsOnlyConstants, mayRunAgain, findImmortalGlobals, moveIntoGlobals, rejectBorrowedGlobalInitializers, rejectGlobalArgumentsDropped, ownGlobalInitializerCalls, moveIntoCallee, pinAssignedGlobals. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../TypeScript/OwnershipSignatureGlobals.cpp | 599 ++++++++++++++++++ .../lib/TypeScript/OwnershipSignaturePass.h | 587 +---------------- 3 files changed, 610 insertions(+), 577 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index dbfc61897..6b72df68b 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -34,6 +34,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipVerifierPass.cpp OwnershipInferencePass.cpp OwnershipSignaturePass.cpp + OwnershipSignatureGlobals.cpp OwnedReturnConsumptionPass.cpp StackPromotionPass.cpp BorrowFactsExportPass.cpp diff --git a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp new file mode 100644 index 000000000..b679c02d4 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp @@ -0,0 +1,599 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): globals - what an initializer moves in, +// immortal globals (spec 25), globals given a borrow, calls in a global's initializer, the globals a +// call may assign. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::movesIntoResult(mlir::Value value) + { + return llvm::all_of(value.getUses(), [](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + return true; + } + + // given to `new Shared(x)`: it moves into the handle's block, which owns it from then on + // whatever becomes of the handle - rc counts the handle (spec 23.2, 26) + if (auto sharedNewOp = mlir::dyn_cast(user)) + { + return sharedNewOp.getValue() == use.get(); + } + + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + return movesIntoResult(user->getResult(0)); + } + + // copied into a block `ts.New` made, which itself moves in: an object literal's box + if (auto storeOp = mlir::dyn_cast(user); storeOp && use.getOperandNumber() == 0) + { + auto block = storeOp.getReference(); + return block.getDefiningOp() && + llvm::all_of(block.getUses(), [&](mlir::OpOperand &blockUse) { + return blockUse.getOwner() == storeOp.getOperation() || + (isView(blockUse.getOwner()) && movesIntoResult(blockUse.getOwner()->getResult(0))); + }); + } + + return false; + }); + } + + bool OwnershipSignaturePass::holdsOnlyConstants(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || !varOp.getInitializer() || !varOp.getInitializer().getDefiningOp()) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { + if (mlir::isa(user)) + { + return true; + } + + auto propertyRefOp = mlir::dyn_cast(user); + return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { + auto storeOp = mlir::dyn_cast(fieldUser); + return mlir::isa(fieldUser) || + (storeOp && storeOp.getReference() == propertyRefOp.getResult() && + holdsNoBlock(storeOp.getValue())); + }); + }); + } + + bool OwnershipSignaturePass::mayRunAgain(mlir::Operation *op) + { + for (auto *at = op; at && !mlir::isa(at); at = at->getParentOp()) + { + if (mlir::isa(at)) + { + return true; + } + + auto *block = at->getBlock(); + if (!block) + { + return true; // not in a function: nothing is known of how often it runs + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(block->getSuccessors().begin(), block->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == block) + { + return true; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + } + + return false; + } + + void OwnershipSignaturePass::findImmortalGlobals(mlir::ModuleOp module) + { + // the static constructors, which run once before the program + llvm::StringSet<> constructors; + module.walk([&](mlir_ts::GlobalConstructorOp constructorOp) { constructors.insert(constructorOp.getGlobalName()); }); + + // a static's address taken out of a class's vtable (`k.w = x`, `k: typeof K`) and written + // through: it may be any class's static the vtable slot holds, so no static is immortal. + // MLIRGen makes a `ts.ThisVirtualSymbolRef` only for a method (a bound function, never a + // `!ts.ref`), so no program reaches this through one today; it is listed for an IR that would. + auto vtableWritten = false; + module.walk([&](mlir::Operation *op) { + if (mlir::isa(op) && + mlir::isa(op->getResult(0).getType()) && + !llvm::all_of(op->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) + { + vtableWritten = true; + } + }); + + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + return; // an op this pass cannot read symbols through: nothing is known never written + } + + llvm::StringMap> usesOf; + for (auto &use : *uses) + { + usesOf[use.getSymbolRef().getRootReference().getValue()].push_back(use.getUser()); + } + + // each candidate with the value its initializer gives it, and the store that does, if any + llvm::MapVector> candidates; + auto mth = typeHelper(&getContext()); + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || + globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || + globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock()) + { + return; + } + + auto resultOp = mlir::dyn_cast(globalOp.getInitializerBlock()->getTerminator()); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + mlir_ts::StoreOp initStore; + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + // its vtable's entry, which only a `ts.VirtualSymbolRef` reads (vtableWritten) + if (mlir::isa(user) && user->getParentOfType()) + { + if (vtableWritten) + { + return; + } + + continue; + } + + if (mlir::isa(user)) + { + continue; // only loaded, or vtableWritten + } + + auto addressOfOp = mlir::dyn_cast(user); + if (!addressOfOp) + { + return; + } + + auto released = false; + auto stored = false; + for (auto *addressUser : addressOfOp->getUsers()) + { + if (mlir::isa(addressUser)) + { + continue; + } + + // the old value, the default, given up before the store + if (mlir::isa(addressUser)) + { + released = true; + continue; + } + + // the one store, in a static constructor, run once + auto storeOp = mlir::dyn_cast(addressUser); + auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); + if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || + storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || + mayRunAgain(storeOp)) + { + return; + } + + initStore = storeOp; + stored = true; + } + + if (released && !stored) + { + return; + } + } + + // a static's region yields only its default, which the store replaces + if (initStore && !regionYieldsDefault(globalOp)) + { + return; + } + + candidates[globalOp] = {initStore ? initStore.getValue() : resultOp->getOperand(0), initStore}; + }); + + // given a fresh object, nothing, or another immortal global's value: in module order, until + // no more are found + llvm::StringSet<> immortal; + auto madeHere = [&](mlir::Value value) { + auto root = rootOf(value); + if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) + { + return false; + } + + // and not given back before the global takes it + auto released = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); }); + return !released; + }; + // isImmortalGlobalRead, of the globals found so far: none is marked yet + auto readsImmortal = [&](mlir::Value value) { + auto addressOfOp = globalReadOf(value); + return addressOfOp && immortal.contains(addressOfOp.getGlobalName()); + }; + + for (auto found = true; found;) + { + found = false; + for (auto &[globalOp, init] : candidates) + { + auto value = init.first; + if (!immortal.contains(globalOp.getSymName()) && (madeHere(value) || holdsNoBlock(value) || readsImmortal(value))) + { + immortal.insert(globalOp.getSymName()); + found = true; + } + } + } + + auto unit = mlir::UnitAttr::get(&getContext()); + for (auto &[globalOp, init] : candidates) + { + if (!immortal.contains(globalOp.getSymName())) + { + continue; + } + + globalOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + if (init.second) + { + init.second->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + if (mlir::isa(user)) + { + user->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + } + } + } + + void OwnershipSignaturePass::moveIntoGlobals(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + llvm::SmallVector retains; + globalOp.walk([&](mlir_ts::RetainOp retainOp) { retains.push_back(retainOp); }); + for (auto retainOp : retains) + { + // a handle's retain is rc's: the global's count, which own leaves in place (spec 23.2) + if (touchesHandle(retainOp)) + { + continue; + } + + auto value = retainOp.getReference(); + + // an immortal global's value (spec 25): owned by no one, held by any number + if (isImmortalGlobalRead(value)) + { + retainOp.erase(); + continue; + } + + // nothing in the region gives the block back, or takes a second reference to it: + // the global is its one owner + auto released = false; + auto retained = 0; + forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { + released = released || mlir::isa(user); + retained += mlir::isa(user); + }); + + if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) + { + retainOp.erase(); + continue; + } + + retainOp.emitError("the initializer of global '") + << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + signalPassFailure(); + } + }); + } + + void OwnershipSignaturePass::rejectBorrowedGlobalInitializers(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto *block = globalOp.getInitializerBlock(); + auto resultOp = block && !block->empty() ? mlir::dyn_cast(block->getTerminator()) + : mlir_ts::GlobalResultOp(); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + auto value = resultOp->getOperand(0); + while (value.getDefiningOp() && isView(value.getDefiningOp())) + { + value = value.getDefiningOp()->getOperand(0); + } + + auto loadOp = value.getDefiningOp(); + auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; + // an immortal global's value (spec 25) is owned by no one: any number of globals hold it + if (!place || !mlir::isa(place) || + isHandle(value) || isConstantData(value) || isImmortalGlobalRead(value) || + !ownsHeap(value.getLoc(), value.getType())) + { + return; + } + + resultOp.emitError("the initializer of global '") + << globalOp.getSymName() + << "' reads a value another owner keeps; -mm=own cannot make a global a second owner"; + signalPassFailure(); + }); + } + + void OwnershipSignaturePass::pinAssignedGlobals() + { + llvm::DenseMap> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].push_back(&call); + } + + llvm::DenseMap> assigns; + llvm::DenseSet assignsAny; + for (auto &entry : functions) + { + auto funcOp = entry.second; + funcOp.walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + else if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + + if (auto addressOfOp = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp()) + { + assigns[funcOp].insert(addressOfOp.getGlobalName()); + } + }); + } + + // what a call may assign, from what its callees do so far + auto assignedBy = [&](Call &call, llvm::SetVector &globals) { + if (!callMayDrop(call.callees)) + { + return false; + } + + if (!call.callees.known) + { + return true; + } + + auto any = false; + for (auto callee : call.callees.funcs) + { + any |= assignsAny.contains(callee); + if (auto found = assigns.find(callee); found != assigns.end()) + { + globals.insert(found->second.begin(), found->second.end()); + } + } + + return any; + }; + + for (auto changed = true; changed;) + { + changed = false; + for (auto &entry : functions) + { + auto funcOp = entry.second; + for (auto *call : callsIn[funcOp]) + { + llvm::SetVector globals; + if (assignedBy(*call, globals)) + { + changed |= assignsAny.insert(funcOp).second; + } + + for (auto global : globals) + { + changed |= assigns[funcOp].insert(global); + } + } + } + } + + for (auto &call : calls) + { + llvm::SetVector globals; + if (assignedBy(call, globals)) + { + call.op->setAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + continue; + } + + if (globals.empty()) + { + continue; + } + + llvm::SmallVector names; + for (auto global : globals) + { + names.push_back(mlir::StringAttr::get(&getContext(), global)); + } + + call.op->setAttr(OWN_ASSIGNS_GLOBALS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), names)); + } + } + + void OwnershipSignaturePass::rejectGlobalArgumentsDropped() + { + for (auto &call : calls) + { + if (!callMayDrop(call.callees)) + { + continue; + } + + for (auto argument : callArgs(call.op)) + { + // a method's object: `this` of the method bound to it + if (auto getThisOp = argument.getDefiningOp()) + { + auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); + if (mlir::isa_and_nonnull(boundOp)) + { + argument = boundOp->getOperand(0); + } + } + + // an immortal global (spec 25) is written by its initializer only, and that runs once + // (a static's is a store in its static constructor, outside any loop, which + // writtenGlobals counts), and no release frees its block + if (isHandle(argument) || isImmortalGlobalRead(argument) || !ownsHeap(call.op->getLoc(), argument.getType())) + { + continue; + } + + auto loadOp = rootOf(argument).getDefiningOp(); + auto global = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!global || !writtenGlobals.contains(global.getGlobalName())) + { + continue; + } + + call.op->emitError("a value read out of the global '") + << global.getGlobalName() + << "' is lent to a call that may release or overwrite it; -mm=own cannot prove it outlives the call"; + signalPassFailure(); + } + } + } + + void OwnershipSignaturePass::ownGlobalInitializerCalls(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + // collected first: a move erases a release the walk has not reached yet + llvm::SmallVector calls; + globalOp.walk([&](mlir::Operation *op) { + if (isCall(op)) + { + calls.push_back(op); + } + }); + + for (auto *op : calls) + { + auto callees = resolve(op); + auto facts = callFacts(callees); + auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName(); + if (!facts.borrows.empty()) + { + op->emitError("the initializer of global '") + << globalOp.getSymName() << "' holds the result of '" << calleeName + << "', which borrows its argument; a global cannot hold a borrow under -mm=own"; + signalPassFailure(); + continue; + } + + auto args = callArgs(op); + llvm::SmallPtrSet given; + for (auto index : facts.owned) + { + if (index < 0 || index >= static_cast(args.size()) || + !given.insert(rootOf(args[index])).second || !moveIntoCallee(op, args[index])) + { + op->emitError("the initializer of global '") + << globalOp.getSymName() << "' gives '" << calleeName + << "' a value it keeps, but -mm=own cannot move it there"; + signalPassFailure(); + break; + } + } + } + }); + } + + bool OwnershipSignaturePass::moveIntoCallee(mlir::Operation *call, mlir::Value value) + { + auto root = rootOf(value); + if (isConstantData(root)) + { + return true; + } + + if (!isFresh(root) || root.getDefiningOp()->getBlock() != call->getBlock()) + { + return false; + } + + mlir::Operation *release = nullptr; + auto usedAfter = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { + if (user == call) + { + return; + } + + if (user->getBlock() != call->getBlock()) + { + usedAfter = true; + } + else if (call->isBeforeInBlock(user)) + { + usedAfter = usedAfter || release || !mlir::isa(user); + release = mlir::isa(user) ? user : release; + } + }); + + if (usedAfter) + { + return false; + } + + if (release) + { + release->erase(); + } + + return true; + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index e05d9b032..36cd56a9d 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -71,108 +71,18 @@ class OwnershipSignaturePass : public mlir::PassWrapper({...})`). Its retains are judged on their own. - static bool movesIntoResult(mlir::Value value) - { - return llvm::all_of(value.getUses(), [](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - return true; - } - - // given to `new Shared(x)`: it moves into the handle's block, which owns it from then on - // whatever becomes of the handle - rc counts the handle (spec 23.2, 26) - if (auto sharedNewOp = mlir::dyn_cast(user)) - { - return sharedNewOp.getValue() == use.get(); - } - - if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) - { - return movesIntoResult(user->getResult(0)); - } - - // copied into a block `ts.New` made, which itself moves in: an object literal's box - if (auto storeOp = mlir::dyn_cast(user); storeOp && use.getOperandNumber() == 0) - { - auto block = storeOp.getReference(); - return block.getDefiningOp() && - llvm::all_of(block.getUses(), [&](mlir::OpOperand &blockUse) { - return blockUse.getOwner() == storeOp.getOperation() || - (isView(blockUse.getOwner()) && movesIntoResult(blockUse.getOwner()->getResult(0))); - }); - } - - return false; - }); - } + static bool movesIntoResult(mlir::Value value); // A record copied out of a local a `ts.Constant` initializes, whose owning fields are given // nothing that holds a block: it holds constants only - numbers, immortal string literals, // functions - and nobody else's reference. - static bool holdsOnlyConstants(mlir::Value value) - { - auto loadOp = value.getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp || !varOp.getInitializer() || !varOp.getInitializer().getDefiningOp()) - { - return false; - } - - return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { - if (mlir::isa(user)) - { - return true; - } - - auto propertyRefOp = mlir::dyn_cast(user); - return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { - auto storeOp = mlir::dyn_cast(fieldUser); - return mlir::isa(fieldUser) || - (storeOp && storeOp.getReference() == propertyRefOp.getResult() && - holdsNoBlock(storeOp.getValue())); - }); - }); - } + static bool holdsOnlyConstants(mlir::Value value); // Whether `op` may run more than once in one run of its function: it is inside a loop op, or its // block - or the block of a region op holding it - is on a cycle of its region's CFG, a path from // the block's successors back to it. (By this pass the affine lowering has made every loop a CFG // cycle of the function body; the loop op is kept for safety.) - static bool mayRunAgain(mlir::Operation *op) - { - for (auto *at = op; at && !mlir::isa(at); at = at->getParentOp()) - { - if (mlir::isa(at)) - { - return true; - } - - auto *block = at->getBlock(); - if (!block) - { - return true; // not in a function: nothing is known of how often it runs - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work(block->getSuccessors().begin(), block->getSuccessors().end()); - while (!work.empty()) - { - auto *next = work.pop_back_val(); - if (next == block) - { - return true; - } - - if (seen.insert(next).second) - { - work.append(next->getSuccessors().begin(), next->getSuccessors().end()); - } - } - } - - return false; - } + static bool mayRunAgain(mlir::Operation *op); // The immortal globals (OWN_IMMORTAL_GLOBAL_ATTR_NAME, spec 25). A global of a class type, // not exported, is immortal when only its initializer writes it, once, and gives it a fresh @@ -192,227 +102,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper constructors; - module.walk([&](mlir_ts::GlobalConstructorOp constructorOp) { constructors.insert(constructorOp.getGlobalName()); }); - - // a static's address taken out of a class's vtable (`k.w = x`, `k: typeof K`) and written - // through: it may be any class's static the vtable slot holds, so no static is immortal. - // MLIRGen makes a `ts.ThisVirtualSymbolRef` only for a method (a bound function, never a - // `!ts.ref`), so no program reaches this through one today; it is listed for an IR that would. - auto vtableWritten = false; - module.walk([&](mlir::Operation *op) { - if (mlir::isa(op) && - mlir::isa(op->getResult(0).getType()) && - !llvm::all_of(op->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) - { - vtableWritten = true; - } - }); - - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - return; // an op this pass cannot read symbols through: nothing is known never written - } - - llvm::StringMap> usesOf; - for (auto &use : *uses) - { - usesOf[use.getSymbolRef().getRootReference().getValue()].push_back(use.getUser()); - } - - // each candidate with the value its initializer gives it, and the store that does, if any - llvm::MapVector> candidates; - auto mth = typeHelper(&getContext()); - module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || - globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || - globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock()) - { - return; - } + void findImmortalGlobals(mlir::ModuleOp module); - auto resultOp = mlir::dyn_cast(globalOp.getInitializerBlock()->getTerminator()); - if (!resultOp || resultOp->getNumOperands() != 1) - { - return; - } - - mlir_ts::StoreOp initStore; - for (auto *user : usesOf.lookup(globalOp.getSymName())) - { - // its vtable's entry, which only a `ts.VirtualSymbolRef` reads (vtableWritten) - if (mlir::isa(user) && user->getParentOfType()) - { - if (vtableWritten) - { - return; - } - - continue; - } - - if (mlir::isa(user)) - { - continue; // only loaded, or vtableWritten - } - - auto addressOfOp = mlir::dyn_cast(user); - if (!addressOfOp) - { - return; - } - - auto released = false; - auto stored = false; - for (auto *addressUser : addressOfOp->getUsers()) - { - if (mlir::isa(addressUser)) - { - continue; - } - - // the old value, the default, given up before the store - if (mlir::isa(addressUser)) - { - released = true; - continue; - } - - // the one store, in a static constructor, run once - auto storeOp = mlir::dyn_cast(addressUser); - auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); - if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || - storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || - mayRunAgain(storeOp)) - { - return; - } - - initStore = storeOp; - stored = true; - } - - if (released && !stored) - { - return; - } - } - - // a static's region yields only its default, which the store replaces - if (initStore && !regionYieldsDefault(globalOp)) - { - return; - } - - candidates[globalOp] = {initStore ? initStore.getValue() : resultOp->getOperand(0), initStore}; - }); - - // given a fresh object, nothing, or another immortal global's value: in module order, until - // no more are found - llvm::StringSet<> immortal; - auto madeHere = [&](mlir::Value value) { - auto root = rootOf(value); - if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) - { - return false; - } - - // and not given back before the global takes it - auto released = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); }); - return !released; - }; - // isImmortalGlobalRead, of the globals found so far: none is marked yet - auto readsImmortal = [&](mlir::Value value) { - auto addressOfOp = globalReadOf(value); - return addressOfOp && immortal.contains(addressOfOp.getGlobalName()); - }; - - for (auto found = true; found;) - { - found = false; - for (auto &[globalOp, init] : candidates) - { - auto value = init.first; - if (!immortal.contains(globalOp.getSymName()) && (madeHere(value) || holdsNoBlock(value) || readsImmortal(value))) - { - immortal.insert(globalOp.getSymName()); - found = true; - } - } - } - - auto unit = mlir::UnitAttr::get(&getContext()); - for (auto &[globalOp, init] : candidates) - { - if (!immortal.contains(globalOp.getSymName())) - { - continue; - } - - globalOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - if (init.second) - { - init.second->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - } - - for (auto *user : usesOf.lookup(globalOp.getSymName())) - { - if (mlir::isa(user)) - { - user->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - } - } - } - } - - void moveIntoGlobals(mlir::ModuleOp module) - { - module.walk([&](mlir_ts::GlobalOp globalOp) { - llvm::SmallVector retains; - globalOp.walk([&](mlir_ts::RetainOp retainOp) { retains.push_back(retainOp); }); - for (auto retainOp : retains) - { - // a handle's retain is rc's: the global's count, which own leaves in place (spec 23.2) - if (touchesHandle(retainOp)) - { - continue; - } - - auto value = retainOp.getReference(); - - // an immortal global's value (spec 25): owned by no one, held by any number - if (isImmortalGlobalRead(value)) - { - retainOp.erase(); - continue; - } - - // nothing in the region gives the block back, or takes a second reference to it: - // the global is its one owner - auto released = false; - auto retained = 0; - forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { - released = released || mlir::isa(user); - retained += mlir::isa(user); - }); - - if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) - { - retainOp.erase(); - continue; - } - - retainOp.emitError("the initializer of global '") - << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; - signalPassFailure(); - } - }); - } + void moveIntoGlobals(mlir::ModuleOp module); void runOnOperation() override; @@ -4532,39 +4224,7 @@ class OwnershipSignaturePass : public mlir::PassWrapperempty() ? mlir::dyn_cast(block->getTerminator()) - : mlir_ts::GlobalResultOp(); - if (!resultOp || resultOp->getNumOperands() != 1) - { - return; - } - - auto value = resultOp->getOperand(0); - while (value.getDefiningOp() && isView(value.getDefiningOp())) - { - value = value.getDefiningOp()->getOperand(0); - } - - auto loadOp = value.getDefiningOp(); - auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; - // an immortal global's value (spec 25) is owned by no one: any number of globals hold it - if (!place || !mlir::isa(place) || - isHandle(value) || isConstantData(value) || isImmortalGlobalRead(value) || - !ownsHeap(value.getLoc(), value.getType())) - { - return; - } - - resultOp.emitError("the initializer of global '") - << globalOp.getSymName() - << "' reads a value another owner keeps; -mm=own cannot make a global a second owner"; - signalPassFailure(); - }); - } + void rejectBorrowedGlobalInitializers(mlir::ModuleOp module); // ---- Drops ---- @@ -4581,107 +4241,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper> callsIn; - for (auto &call : calls) - { - callsIn[call.op->getParentOfType()].push_back(&call); - } - - llvm::DenseMap> assigns; - llvm::DenseSet assignsAny; - for (auto &entry : functions) - { - auto funcOp = entry.second; - funcOp.walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - else if (auto releaseSlotOp = mlir::dyn_cast(op)) - { - slot = releaseSlotOp.getSlot(); - } - - if (auto addressOfOp = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp()) - { - assigns[funcOp].insert(addressOfOp.getGlobalName()); - } - }); - } - - // what a call may assign, from what its callees do so far - auto assignedBy = [&](Call &call, llvm::SetVector &globals) { - if (!callMayDrop(call.callees)) - { - return false; - } - - if (!call.callees.known) - { - return true; - } - - auto any = false; - for (auto callee : call.callees.funcs) - { - any |= assignsAny.contains(callee); - if (auto found = assigns.find(callee); found != assigns.end()) - { - globals.insert(found->second.begin(), found->second.end()); - } - } - - return any; - }; - - for (auto changed = true; changed;) - { - changed = false; - for (auto &entry : functions) - { - auto funcOp = entry.second; - for (auto *call : callsIn[funcOp]) - { - llvm::SetVector globals; - if (assignedBy(*call, globals)) - { - changed |= assignsAny.insert(funcOp).second; - } - - for (auto global : globals) - { - changed |= assigns[funcOp].insert(global); - } - } - } - } - - for (auto &call : calls) - { - llvm::SetVector globals; - if (assignedBy(call, globals)) - { - call.op->setAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME, mlir::UnitAttr::get(&getContext())); - continue; - } - - if (globals.empty()) - { - continue; - } - - llvm::SmallVector names; - for (auto global : globals) - { - names.push_back(mlir::StringAttr::get(&getContext(), global)); - } - - call.op->setAttr(OWN_ASSIGNS_GLOBALS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), names)); - } - } + void pinAssignedGlobals(); // An argument read out of a global (`run(G)`, `G.m()`) is lent to the callee, and the callee may // free it under its own parameter: `function run(h: C) { G = mk(); print(h.x); }`. The caller's @@ -4689,49 +4249,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper()) - { - auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); - if (mlir::isa_and_nonnull(boundOp)) - { - argument = boundOp->getOperand(0); - } - } - - // an immortal global (spec 25) is written by its initializer only, and that runs once - // (a static's is a store in its static constructor, outside any loop, which - // writtenGlobals counts), and no release frees its block - if (isHandle(argument) || isImmortalGlobalRead(argument) || !ownsHeap(call.op->getLoc(), argument.getType())) - { - continue; - } - - auto loadOp = rootOf(argument).getDefiningOp(); - auto global = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - if (!global || !writtenGlobals.contains(global.getGlobalName())) - { - continue; - } - - call.op->emitError("a value read out of the global '") - << global.getGlobalName() - << "' is lent to a call that may release or overwrite it; -mm=own cannot prove it outlives the call"; - signalPassFailure(); - } - } - } + void rejectGlobalArgumentsDropped(); // ---- Calls in a global's initializer ---- @@ -4742,97 +4260,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper calls; - globalOp.walk([&](mlir::Operation *op) { - if (isCall(op)) - { - calls.push_back(op); - } - }); - - for (auto *op : calls) - { - auto callees = resolve(op); - auto facts = callFacts(callees); - auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName(); - if (!facts.borrows.empty()) - { - op->emitError("the initializer of global '") - << globalOp.getSymName() << "' holds the result of '" << calleeName - << "', which borrows its argument; a global cannot hold a borrow under -mm=own"; - signalPassFailure(); - continue; - } - - auto args = callArgs(op); - llvm::SmallPtrSet given; - for (auto index : facts.owned) - { - if (index < 0 || index >= static_cast(args.size()) || - !given.insert(rootOf(args[index])).second || !moveIntoCallee(op, args[index])) - { - op->emitError("the initializer of global '") - << globalOp.getSymName() << "' gives '" << calleeName - << "' a value it keeps, but -mm=own cannot move it there"; - signalPassFailure(); - break; - } - } - } - }); - } + void ownGlobalInitializerCalls(mlir::ModuleOp module); // `value` was made in the call's block and nothing but its release, if it has one, comes after // the call: the release goes, and the callee owns the block. Constant data (a string literal) // is nobody's, and is given as it is. - static bool moveIntoCallee(mlir::Operation *call, mlir::Value value) - { - auto root = rootOf(value); - if (isConstantData(root)) - { - return true; - } - - if (!isFresh(root) || root.getDefiningOp()->getBlock() != call->getBlock()) - { - return false; - } - - mlir::Operation *release = nullptr; - auto usedAfter = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value) { - if (user == call) - { - return; - } - - if (user->getBlock() != call->getBlock()) - { - usedAfter = true; - } - else if (call->isBeforeInBlock(user)) - { - usedAfter = usedAfter || release || !mlir::isa(user); - release = mlir::isa(user) ? user : release; - } - }); - - if (usedAfter) - { - return false; - } - - if (release) - { - release->erase(); - } - - return true; - } + static bool moveIntoCallee(mlir::Operation *call, mlir::Value value); // ---- Export ---- From fccc71b7ee54093e3226f7c7bdc11a9862570119 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:15:19 +0100 Subject: [PATCH 09/25] own passes: OwnershipSignatureInterfaces.cpp out of OwnershipSignaturePass.h (I1) Interfaces closed in this module (G9, spec 31) - their method and field families, and the casts that open them. A pure move of slotField, collectFieldFunctions, valueInterfaces, anyInterfaces, functionValues, signatureOf, castReinterpretsFunction, memberTypeInterfaces, isNullish, castMakesInterface, slotFunction, addressOfThroughCasts, exportedWords, collectInterfaceFamilies, readInterfaceVTables, traceInterfaceValues, openExposedInterfaces, openThroughMembers, openLooseVTables, buildInterfaceFamilies, markInterfaceReached. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipSignatureInterfaces.cpp | 1241 +++++++++++++++ .../lib/TypeScript/OwnershipSignaturePass.h | 1379 ++--------------- 3 files changed, 1338 insertions(+), 1283 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 6b72df68b..004c82b6a 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -35,6 +35,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferencePass.cpp OwnershipSignaturePass.cpp OwnershipSignatureGlobals.cpp + OwnershipSignatureInterfaces.cpp OwnedReturnConsumptionPass.cpp StackPromotionPass.cpp BorrowFactsExportPass.cpp diff --git a/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp new file mode 100644 index 000000000..58e6b18ec --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp @@ -0,0 +1,1241 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): interfaces closed in this module (G9, +// spec 31) - their method and field families, and the casts that open them. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + std::pair OwnershipSignaturePass::slotField(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + auto propertyRefOp = value ? value.getDefiningOp() : mlir_ts::PropertyRefOp(); + auto nullObject = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::CastOp(); + if (!nullObject || !nullObject.getIn().getDefiningOp() || + !mlir::isa(nullObject.getType())) + { + return {}; + } + + auto tupleType = storageTupleOf(nullObject.getType()); + if (!tupleType) + { + return {}; + } + + return {tupleType, static_cast(propertyRefOp.getPosition())}; + } + + void OwnershipSignaturePass::collectFieldFunctions(mlir::ModuleOp module) + { + llvm::DenseMap, llvm::SetVector> stored; + llvm::DenseSet> unknownFields; + llvm::DenseSet unknownTypes; + // a function of unknown origin stored through an interface this pass cannot follow + auto unknownAnywhere = false; + + auto store = [&](std::pair key, mlir::Value value) { + auto symbol = slotFunction(value); + auto funcOp = symbol ? functions.lookup(symbol.getValue()) : mlir_ts::FuncOp(); + if (funcOp && !funcOp.isDeclaration()) + { + stored[key].insert(symbol); + } + else + { + unknownFields.insert(key); + } + }; + + auto isFunctionType = [](mlir::Type type) { + return mlir::isa(type); + }; + + module.walk([&](mlir::Operation *op) { + // the literal's own values + if (auto constantOp = mlir::dyn_cast(op)) + { + auto constTupleType = mlir::dyn_cast(constantOp.getType()); + auto values = mlir::dyn_cast(constantOp.getValue()); + if (!constTupleType || !values) + { + return; + } + + mlir::Type tupleType = mlir_ts::TupleType::get(&getContext(), constTupleType.getFields()); + for (auto [index, value] : llvm::enumerate(values)) + { + std::pair key{tupleType, static_cast(index)}; + if (auto symbol = mlir::dyn_cast(value)) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (funcOp && !funcOp.isDeclaration()) + { + stored[key].insert(symbol.getAttr()); + } + else + { + unknownFields.insert(key); + } + } + else if (!mlir::isa(value) && index < constTupleType.getFields().size() && + isFunctionType(constTupleType.getFields()[index].type)) + { + unknownFields.insert(key); + } + } + + return; + } + + // a store into the field through the object + if (auto propertyRefOp = mlir::dyn_cast(op)) + { + auto tupleType = storageTupleOf(propertyRefOp.getObjectRef().getType()); + if (!tupleType) + { + return; + } + + std::pair key{tupleType, static_cast(propertyRefOp.getPosition())}; + for (auto &use : propertyRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == propertyRefOp.getResult() && storeOp.getValue() != propertyRefOp.getResult()) + { + store(key, storeOp.getValue()); + continue; + } + + // an interface vtable's offset of the field (slotField) reads nothing + if (user->getParentOfType() && mlir::isa(user)) + { + continue; + } + + unknownFields.insert(key); + } + + return; + } + + // a store into the field through an interface: into each literal its vtables reach + if (auto interfaceRefOp = mlir::dyn_cast(op)) + { + auto refType = mlir::dyn_cast(interfaceRefOp.getType()); + if (!refType || !isFunctionType(refType.getElementType())) + { + return; + } + + auto interfaceType = mlir::dyn_cast(interfaceRefOp.getInterfaceVal().getType()); + auto found = interfaceType ? interfaceFieldFamilies.find({interfaceType.getName().getValue().str(), + static_cast(interfaceRefOp.getIndex()) + + INTERFACE_VTABLE_HEADER_SLOTS}) + : interfaceFieldFamilies.end(); + for (auto &use : interfaceRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == interfaceRefOp.getResult() && found != interfaceFieldFamilies.end() && + found->second.closed) + { + for (auto target : found->second.targets) + { + store(target, storeOp.getValue()); + } + + continue; + } + + unknownAnywhere = true; + } + + return; + } + + // an object of the literal's type made other than by the literal: its fields may hold + // anything. A cast keeps the type's own value (the literal's box seen as the object, an + // object, a reference to its storage) or makes the null object a vtable's offsets are + // taken from; from anything else it reinterprets. + if (auto castOp = mlir::dyn_cast(op)) + { + auto tupleType = storageTupleOf(castOp.getType()); + if (tupleType && !mlir::isa(castOp.getIn().getType()) && + storageTupleOf(castOp.getIn().getType()) != tupleType) + { + unknownTypes.insert(tupleType); + } + + return; + } + + if (mlir::isa(op) || isCall(op) || isView(op) || mlir::isa(op)) + { + return; + } + + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + if (auto tupleType = storageTupleOf(result.getType())) + { + unknownTypes.insert(tupleType); + } + } + } + }); + + for (auto &[key, family] : interfaceFieldFamilies) + { + if (!family.closed) + { + continue; + } + + for (auto target : family.targets) + { + if (unknownAnywhere || unknownTypes.contains(target.first) || unknownFields.contains(target)) + { + family.closed = false; + break; + } + + for (auto member : stored.lookup(target)) + { + if (!llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + } + } + + family.closed = family.closed && !family.members.empty(); + } + } + + void OwnershipSignaturePass::valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth) + { + if (!type || depth > 16) + { + return; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + found.insert(interfaceType.getName().getValue()); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + valueInterfaces(info.type, found, depth + 1); + } + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + } + else if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + } + else if (auto unionType = mlir::dyn_cast(type)) + { + for (auto member : unionType.getTypes()) + { + valueInterfaces(member, found, depth + 1); + } + } + else if (auto optionalType = mlir::dyn_cast(type)) + { + valueInterfaces(optionalType.getElementType(), found, depth + 1); + } + else if (auto arrayType = mlir::dyn_cast(type)) + { + valueInterfaces(arrayType.getElementType(), found, depth + 1); + } + else if (auto constArrayType = mlir::dyn_cast(type)) + { + valueInterfaces(constArrayType.getElementType(), found, depth + 1); + } + else if (auto refType = mlir::dyn_cast(type)) + { + valueInterfaces(refType.getElementType(), found, depth + 1); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + valueInterfaces(valueRefType.getElementType(), found, depth + 1); + } + else if (auto boundRefType = mlir::dyn_cast(type)) + { + valueInterfaces(boundRefType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + valueInterfaces(sharedType.getElementType(), found, depth + 1); + } + } + + void OwnershipSignaturePass::anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, + llvm::StringSet<> *classes) + { + if (!type || !seen.insert(type).second) + { + return; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + found.insert(interfaceType.getName().getValue()); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + anyInterfaces(info.type, found, seen, classes); + } + }; + + // the types whose fields are not parameters of the type + if (auto classStorageType = mlir::dyn_cast(type)) + { + if (classes) + { + classes->insert(classStorageType.getName().getValue()); + } + + fields(classStorageType.getFields()); + return; + } + + if (auto objectStorageType = mlir::dyn_cast(type)) + { + fields(objectStorageType.getFields()); + return; + } + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + return; + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + return; + } + + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { anyInterfaces(sub, found, seen, classes); }); + } + + void OwnershipSignaturePass::functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth) + { + if (!type || depth > 16) + { + return; + } + + if (mlir::isa(type)) + { + found.push_back(type); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + functionValues(info.type, found, depth + 1); + } + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + } + else if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + } + else if (auto unionType = mlir::dyn_cast(type)) + { + for (auto member : unionType.getTypes()) + { + functionValues(member, found, depth + 1); + } + } + else if (auto optionalType = mlir::dyn_cast(type)) + { + functionValues(optionalType.getElementType(), found, depth + 1); + } + else if (auto arrayType = mlir::dyn_cast(type)) + { + functionValues(arrayType.getElementType(), found, depth + 1); + } + else if (auto constArrayType = mlir::dyn_cast(type)) + { + functionValues(constArrayType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + functionValues(sharedType.getElementType(), found, depth + 1); + } + } + + std::pair, llvm::SmallVector> OwnershipSignaturePass::signatureOf(mlir::Type type) + { + llvm::SmallVector inputs; + llvm::SmallVector results; + if (auto functionType = mlir::dyn_cast(type)) + { + inputs.append(functionType.getInputs().begin(), functionType.getInputs().end()); + results.append(functionType.getResults().begin(), functionType.getResults().end()); + } + else if (auto boundType = mlir::dyn_cast(type)) + { + inputs.append(boundType.getInputs().begin(), boundType.getInputs().end()); + results.append(boundType.getResults().begin(), boundType.getResults().end()); + } + else if (auto hybridType = mlir::dyn_cast(type)) + { + inputs.append(hybridType.getInputs().begin(), hybridType.getInputs().end()); + results.append(hybridType.getResults().begin(), hybridType.getResults().end()); + } + else + { + // a kind this does not read: never the same signature as another + inputs.push_back(type); + } + + return {inputs, results}; + } + + bool OwnershipSignaturePass::castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing) + { + // `!ts.opaque` is the compiler's own (a vtable slot read as the `.instanceOf` it holds), not + // a value a program can put a function into + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + llvm::SmallVector inFunctions; + llvm::SmallVector outFunctions; + functionValues(in, inFunctions); + functionValues(out, outFunctions); + boxing = false; + if ((inFunctions.empty() && outFunctions.empty()) || in == out) + { + return false; + } + + auto mention = [&](llvm::ArrayRef types) { + for (auto type : types) + { + llvm::DenseSet seen; + anyInterfaces(type, interfaces, seen); + } + }; + + if (isOpaque(out)) + { + boxing = true; + mention(inFunctions); + return false; + } + + // the same signatures, but for `this` seen as an opaque pointer on one side, or bound on one + // side only (a closure's box, a bound function's object: the caller supplies neither) + auto sameSignature = [](mlir::Type a, mlir::Type b) { + auto [aInputs, aResults] = signatureOf(a); + auto [bInputs, bResults] = signatureOf(b); + auto dropBoundThis = [](mlir::Type type, llvm::SmallVector &inputs) { + if (!inputs.empty() && (mlir::isa(type) || mlir::isa(inputs.front()))) + { + inputs.erase(inputs.begin()); + } + }; + + if (aInputs.size() == bInputs.size() + 1) + { + dropBoundThis(a, aInputs); + } + else if (bInputs.size() == aInputs.size() + 1) + { + dropBoundThis(b, bInputs); + } + + if (aResults != bResults || aInputs.size() != bInputs.size()) + { + return false; + } + + for (auto [index, pair] : llvm::enumerate(llvm::zip(aInputs, bInputs))) + { + auto [aInput, bInput] = pair; + if (aInput != bInput && + !(index == 0 && (mlir::isa(aInput) || mlir::isa(bInput)))) + { + return false; + } + } + + return true; + }; + + // a function out of `any`, or one function seen as another; a function seen as something + // that is no function (a vtable slot's reference to it) is the compiler's own + auto reinterprets = isOpaque(in) && !outFunctions.empty(); + if (!inFunctions.empty() && !outFunctions.empty()) + { + reinterprets = reinterprets || inFunctions.size() != outFunctions.size(); + for (auto [inFunction, outFunction] : llvm::zip(inFunctions, outFunctions)) + { + reinterprets = reinterprets || !sameSignature(inFunction, outFunction); + } + } + + if (reinterprets) + { + mention(inFunctions); + mention(outFunctions); + } + + return reinterprets; + } + + void OwnershipSignaturePass::memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found) + { + llvm::DenseSet seen; + anyInterfaces(type, found, seen); + } + + bool OwnershipSignaturePass::isNullish(mlir::Type type) + { + return mlir::isa(type); + } + + bool OwnershipSignaturePass::castMakesInterface(mlir::Type in, mlir::Type out, int depth) + { + // null seen as an interface value has no vtable: a call through it reaches nothing + if (in == out || depth > 16 || isNullish(in)) + { + return false; + } + + llvm::StringSet<> made; + valueInterfaces(out, made); + if (made.empty()) + { + return false; + } + + if (mlir::isa(out)) + { + return !holdsAsIs(in, out, /*onlyNullable=*/true); + } + + if (mlir::isa(out)) + { + if (isNullish(in) || holdsAsIs(out, in)) + { + return false; + } + + llvm::SmallVector members; + if (auto unionType = mlir::dyn_cast(in)) + { + members.append(unionType.getTypes().begin(), unionType.getTypes().end()); + } + else if (auto optionalType = mlir::dyn_cast(in)) + { + members.push_back(optionalType.getElementType()); + } + else + { + return true; + } + + return !llvm::all_of(members, [&](mlir::Type member) { return isNullish(member) || holdsAsIs(out, member); }); + } + + auto tupleFields = [](mlir::Type type) -> std::optional> { + if (auto tupleType = mlir::dyn_cast(type)) + { + return tupleType.getFields(); + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + return constTupleType.getFields(); + } + + return std::nullopt; + }; + + if (auto outFields = tupleFields(out)) + { + auto inFields = tupleFields(in); + if (!inFields || inFields->size() != outFields->size()) + { + return true; + } + + for (auto [inField, outField] : llvm::zip(*inFields, *outFields)) + { + if (castMakesInterface(inField.type, outField.type, depth + 1)) + { + return true; + } + } + + return false; + } + + auto elementOf = [](mlir::Type type) -> mlir::Type { + if (auto arrayType = mlir::dyn_cast(type)) + { + return arrayType.getElementType(); + } + + if (auto constArrayType = mlir::dyn_cast(type)) + { + return constArrayType.getElementType(); + } + + return {}; + }; + + if (auto outElement = elementOf(out)) + { + auto inElement = elementOf(in); + return !inElement || castMakesInterface(inElement, outElement, depth + 1); + } + + // a reference seen as another (`!ts.bound_ref` as `!ts.ref`): what it points to + auto pointee = [](mlir::Type type) -> mlir::Type { + if (auto refType = mlir::dyn_cast(type)) + { + return refType.getElementType(); + } + + if (auto valueRefType = mlir::dyn_cast(type)) + { + return valueRefType.getElementType(); + } + + if (auto boundRefType = mlir::dyn_cast(type)) + { + return boundRefType.getElementType(); + } + + return {}; + }; + + if (auto outPointee = pointee(out)) + { + auto inPointee = pointee(in); + return !inPointee || castMakesInterface(inPointee, outPointee, depth + 1); + } + + // anything else reinterpreted: what is read out of it was never checked + return true; + } + + mlir::StringAttr OwnershipSignaturePass::slotFunction(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + auto symbolRefOp = value ? value.getDefiningOp() : mlir_ts::SymbolRefOp(); + return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + } + + mlir_ts::AddressOfOp OwnershipSignaturePass::addressOfThroughCasts(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + return value ? value.getDefiningOp() : mlir_ts::AddressOfOp(); + } + + llvm::StringSet<> OwnershipSignaturePass::exportedWords(mlir::ModuleOp module) + { + llvm::StringSet<> words; + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!globalOp.getSymName().starts_with("__decls")) + { + return; + } + + globalOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { + auto text = mlir::dyn_cast(constantOp.getValue()); + if (!text) + { + return; + } + + llvm::StringRef rest = text.getValue(); + while (!rest.empty()) + { + auto isWordChar = [](char c) { return llvm::isAlnum(c) || c == '_' || c == '$'; }; + rest = rest.drop_while([&](char c) { return !isWordChar(c); }); + auto word = rest.take_while(isWordChar); + if (!word.empty()) + { + words.insert(word); + } + + rest = rest.drop_front(word.size()); + } + }); + }); + + return words; + } + + void OwnershipSignaturePass::collectInterfaceFamilies(mlir::ModuleOp module) + { + InterfaceScan scan; + readInterfaceVTables(module, scan); + traceInterfaceValues(module, scan); + openExposedInterfaces(module, scan); + openThroughMembers(scan); + openLooseVTables(scan); + buildInterfaceFamilies(scan); + collectFieldFunctions(module); + markInterfaceReached(scan); + } + + void OwnershipSignaturePass::readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto name = globalOp.getSymName(); + if (!name.ends_with("..vtbl")) + { + return; + } + + auto owner = name.drop_back(llvm::StringRef("..vtbl").size()); + llvm::DenseMap values; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + if (insertOp.getPosition().size() == 1) + { + values[insertOp.getPosition()[0]] = insertOp.getValue(); + } + }); + + auto header = slotFunction(values.lookup(0)); + auto tupleType = mlir::dyn_cast(globalOp.getType()); + if (!header || !tupleType) + { + scan.unread.push_back(name); + return; + } + + if (header.getValue() == (owner + "..instanceOf").str()) + { + // an exported class (not in an executable, whose exports no module sees) + auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); + ownClasses.insert(owner); + if (exposed) + { + nameableClasses.insert(owner); + } + + for (auto &[position, value] : values) + { + auto addressOfOp = addressOfThroughCasts(value); + auto id = position < static_cast(tupleType.size()) + ? mlir::dyn_cast_or_null(tupleType.getFieldInfo(position).id) + : mlir::StringAttr(); + if (!addressOfOp || !id) + { + continue; + } + + scan.classHeld[id.getValue()].insert(addressOfOp.getGlobalName()); + if (exposed) + { + scan.open.insert(id.getValue()); // another module may extend the class + } + } + + return; + } + + if (!header.getValue().ends_with("..instanceOf") && header.getValue() != ".instanceOf.none") + { + scan.unread.push_back(name); + return; + } + + auto &vtable = scan.vtables[name]; + vtable.type = tupleType; + for (auto &[position, value] : values) + { + if (position > 0) + { + vtable.slots[position] = slotFunction(value); + if (auto field = slotField(value); field.first) + { + vtable.fields[position] = field; + } + } + } + }); + } + + void OwnershipSignaturePass::traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan) + { + auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { + // the global itself + if (auto addressOfOp = addressOfThroughCasts(value)) + { + return scan.vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); + } + + // a copy of it on the heap, its absent members' slots patched + while (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + + if (auto newOp = value.getDefiningOp()) + { + llvm::StringRef copied; + for (auto *user : newOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() + ? storeOp.getValue().getDefiningOp() + : mlir_ts::LoadOp(); + auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); + if (addressOfOp && scan.vtables.count(addressOfOp.getGlobalName())) + { + if (!copied.empty()) + { + return {}; + } + + copied = addressOfOp.getGlobalName(); + } + } + + return copied; + } + + return {}; + }; + + // an object's class vtable read at run time: `ts.VTableOffsetRef(ts.Load(ts.PropertyRef(object, 0)), k)` + auto readFromClass = [](mlir::Value value) { + auto offsetRefOp = value.getDefiningOp(); + auto loadOp = offsetRefOp ? offsetRefOp.getVtable().getDefiningOp() : mlir_ts::LoadOp(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + return propertyRefOp && propertyRefOp.getPosition() == 0 && + mlir::isa(propertyRefOp.getObjectRef().getType()); + }; + + // what reads a vtable's address: making an interface value, a class vtable's slot, the copy + std::function onlyMakesValues = [&](mlir::Value address) { + return llvm::all_of(address.getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (auto newInterfaceOp = mlir::dyn_cast(user)) + { + return newInterfaceOp.getInterfaceVTable() == use.get(); + } + + if (mlir::isa(user)) + { + return onlyMakesValues(user->getResult(0)); + } + + if (mlir::isa(user)) + { + return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; + } + + // the copy: read whole, and stored into a block `ts.New` made + if (auto loadOp = mlir::dyn_cast(user)) + { + return llvm::all_of(loadOp->getUsers(), [&](mlir::Operation *loadUser) { + auto storeOp = mlir::dyn_cast(loadUser); + return storeOp && storeOp.getValue() == loadOp.getResult() && + storeOp.getReference().getDefiningOp(); + }); + } + + return false; + }); + }; + + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + module.walk([&](mlir::Operation *op) { + if (auto newInterfaceOp = mlir::dyn_cast(op)) + { + auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); + auto vtable = newInterfaceOp.getInterfaceVTable(); + if (auto found = vtableOf(vtable); !found.empty()) + { + scan.madeWith[name].insert(found); + } + else if (readFromClass(vtable)) + { + // any class's vtable for the interface (classHeld, all in its families below) + if (!scan.classHeld.count(name)) + { + scan.open.insert(name); + } + } + else if (auto extractOp = vtable.getDefiningOp()) + { + scan.castOpened.insert(name); + valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); + } + else + { + scan.allOpen = true; + } + + return; + } + + if (auto castOp = mlir::dyn_cast(op)) + { + auto in = castOp.getIn().getType(); + auto out = castOp.getType(); + if (isOpaque(out)) + { + valueInterfaces(in, scan.boxed); + } + + if (castMakesInterface(in, out)) + { + valueInterfaces(out, scan.castOpened); + valueInterfaces(in, scan.castOpened); + if (isOpaque(in)) + { + valueInterfaces(out, scan.unboxedInto); + } + } + + // a function value reinterpreted carries the interface values of its parameters and + // results across unseen (review6 x1c, x1r) + llvm::StringSet<> signatures; + auto boxing = false; + if (castReinterpretsFunction(in, out, signatures, boxing)) + { + scan.openAll(signatures); + if (isOpaque(in)) + { + scan.unboxedFunction = true; + } + } + else if (boxing) + { + for (auto &entry : signatures) + { + scan.boxed.insert(entry.getKey()); + } + } + + return; + } + + if (auto interfaceRefOp = mlir::dyn_cast(op)) + { + if (!interfaceRefOp->use_empty()) + { + scan.open.insert(interfaceRefOp.getIdentifier()); + } + + return; + } + + if (auto symbolRefOp = mlir::dyn_cast(op)) + { + if (auto interfaceType = mlir::dyn_cast(symbolRefOp.getInterfaceVal().getType())) + { + memberTypeInterfaces(symbolRefOp.getType(), scan.memberInterfaces[interfaceType.getName().getValue()]); + } + + return; + } + + if (auto addressOfOp = mlir::dyn_cast(op)) + { + if (auto found = scan.vtables.find(addressOfOp.getGlobalName()); found != scan.vtables.end() && !onlyMakesValues(addressOfOp.getResult())) + { + found->second.written = true; + } + } + }); + } + + void OwnershipSignaturePass::openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan) + { + // seen from another module: a word of its exported declarations, or in a reachable type. An + // executable's exports are seen by no other module (USER decision, spec 31): only what it + // imports - a declared function, an imported global - comes from outside. + if (!executable) + { + exportWords = exportedWords(module); + } + + llvm::StringSet<> exposed; + llvm::DenseSet seen; + for (auto &entry : functions) + { + // a `public` method of a class no other module can name is not one: only a declaration + // re-parsed from `__decls` names a function of another module + auto exported = !executable && (entry.second->hasAttr("export") || entry.second->hasAttr(DLL_EXPORT)); + if (entry.second.isDeclaration() || exported) + { + anyInterfaces(entry.second.getFunctionType(), exposed, seen, &nameableClasses); + } + } + + for (auto &entry : globals) + { + auto globalOp = entry.second; + // another module's global, declared here with no initializer (an import of a .ts module + // carries `export`) + auto imported = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || + globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); + auto exported = !globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT); + if (imported || (exported && !executable)) + { + anyInterfaces(globalOp.getType(), exposed, seen, &nameableClasses); + } + } + + scan.openAll(exposed); + if (!scan.unboxedInto.empty() || scan.unboxedFunction) + { + scan.openAll(scan.boxed); + for (auto &entry : scan.boxed) + { + scan.castOpened.insert(entry.getKey()); + } + } + } + + void OwnershipSignaturePass::openThroughMembers(InterfaceScan &scan) + { + // An interface value made out of another one (or out of `any`) is called with the other's + // members: `HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` hands an `Op` to a + // function that calls through `Op2` (review6r1 v5, v11f). So every interface either side's + // members mention is open too, and theirs, recursively (controller ruling, spec 31). + // + // A class's vtables for its interfaces are its members' families whether or not this module + // makes an interface value of the class (a method called directly then keeps its facts), and + // their slots' types are the interface's members' types: an interface whose values are all + // made from class objects is read through them (review6r2 d1-d3). + for (auto &[name, held] : scan.classHeld) + { + scan.madeWith[name].insert(held.begin(), held.end()); + } + + for (auto &[name, made] : scan.madeWith) + { + for (auto vtableName : made) + { + if (auto found = scan.vtables.find(vtableName); found != scan.vtables.end() && found->second.type) + { + for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) + { + if (position > 0) + { + memberTypeInterfaces(field.type, scan.memberInterfaces[name]); + } + } + } + } + } + + llvm::SmallVector work; + for (auto &entry : scan.castOpened) + { + work.push_back(entry.getKey().str()); + } + + llvm::StringSet<> expanded; + while (!work.empty()) + { + auto name = work.pop_back_val(); + if (!expanded.insert(name).second) + { + continue; + } + + scan.open.insert(name); + for (auto &member : scan.memberInterfaces[name]) + { + work.push_back(member.getKey().str()); + } + } + } + + void OwnershipSignaturePass::openLooseVTables(InterfaceScan &scan) + { + // A vtable this pass could not read, or one no value of any interface is made with, that is + // named for an interface (`...vtbl`, `...vtbl`): the interface may have an + // implementation outside its families. Belt and braces - every way to make an interface + // value is traced above - but a missed member would get its family's facts. + llvm::DenseSet made; + for (auto &entry : scan.madeWith) + { + made.insert(entry.second.begin(), entry.second.end()); + } + + llvm::SmallVector loose(scan.unread.begin(), scan.unread.end()); + for (auto &entry : scan.vtables) + { + if (!made.contains(entry.getKey())) + { + loose.push_back(entry.getKey()); + } + } + + for (auto &entry : scan.madeWith) + { + auto name = entry.getKey().str(); + if (llvm::any_of(loose, [&](llvm::StringRef vtableName) { + return vtableName.ends_with("." + name + "..vtbl") || vtableName.starts_with(name + "."); + })) + { + scan.open.insert(entry.getKey()); + } + } + } + + void OwnershipSignaturePass::buildInterfaceFamilies(InterfaceScan &scan) + { + auto &words = exportWords; + auto isOpen = [&](llvm::StringRef name) { + // a namespaced interface is declared by its last name + auto last = name.rsplit('.').second; + return scan.allOpen || scan.open.contains(name) || words.contains(name) || (!last.empty() && words.contains(last)); + }; + + // the families + for (auto &[name, made] : scan.madeWith) + { + // a class vtable's slot holding a vtable this pass did not read (another module's) + auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return scan.vtables.count(vtableName); }); + auto closed = !isOpen(name) && allRead; + for (auto vtableName : made) + { + if (!scan.vtables.count(vtableName)) + { + continue; + } + + if (allRead) + { + interfaceVTableTypes[name].push_back(scan.vtables[vtableName].type); + } + + scan.tied.insert(vtableName); + auto &vtable = scan.vtables[vtableName]; + for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) + { + if (position == 0) + { + continue; + } + + auto &family = interfaceFamilies[{name.str(), static_cast(position)}]; + auto id = mlir::dyn_cast_or_null(field.id); + auto member = vtable.slots.lookup(position); + auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp(); + if (!family.field) + { + family.field = id; + } + + // a member that is not private (a `public` method) is open (findOpen): it has no + // facts to give, and gives the family none + family.closed = family.closed && closed && !vtable.written && id && family.field == id && funcOp && + !funcOp.isDeclaration() && !declaresCallerFacts(funcOp); + if (member && !llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + + // the same slot as a field: an object literal's field of function type + auto &fieldFamily = interfaceFieldFamilies[{name.str(), static_cast(position)}]; + auto target = vtable.fields.lookup(position); + auto refType = mlir::dyn_cast(field.type); + if (!fieldFamily.field) + { + fieldFamily.field = id; + } + + fieldFamily.closed = fieldFamily.closed && closed && !vtable.written && id && fieldFamily.field == id && + target.first && refType && mlir::isa(refType.getElementType()); + if (target.first && !llvm::is_contained(fieldFamily.targets, target)) + { + fieldFamily.targets.push_back(target); + } + } + } + } + } + + void OwnershipSignaturePass::markInterfaceReached(InterfaceScan &scan) + { + // what an open family holds, and a vtable no interface value is made with, is reached + // through calls that are not resolved + auto reached = [&](mlir::StringAttr member) { + if (auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp()) + { + interfaceReached.insert(funcOp); + } + }; + + for (auto &[key, family] : interfaceFamilies) + { + if (!family.closed) + { + llvm::for_each(family.members, reached); + } + } + + for (auto &entry : scan.vtables) + { + if (!scan.tied.contains(entry.getKey())) + { + for (auto &slot : entry.second.slots) + { + reached(slot.second); + } + } + } + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 36cd56a9d..6538f1204 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -260,1321 +260,134 @@ class OwnershipSignaturePass : public mlir::PassWrapper slotField(mlir::Value value) - { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) - { - value = castOp.getIn(); - } - - auto propertyRefOp = value ? value.getDefiningOp() : mlir_ts::PropertyRefOp(); - auto nullObject = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::CastOp(); - if (!nullObject || !nullObject.getIn().getDefiningOp() || - !mlir::isa(nullObject.getType())) - { - return {}; - } - - auto tupleType = storageTupleOf(nullObject.getType()); - if (!tupleType) - { - return {}; - } - - return {tupleType, static_cast(propertyRefOp.getPosition())}; - } - - // What every object-literal field an interface's field slot may be holds (InterfaceFieldFamily). - void collectFieldFunctions(mlir::ModuleOp module) - { - llvm::DenseMap, llvm::SetVector> stored; - llvm::DenseSet> unknownFields; - llvm::DenseSet unknownTypes; - // a function of unknown origin stored through an interface this pass cannot follow - auto unknownAnywhere = false; - - auto store = [&](std::pair key, mlir::Value value) { - auto symbol = slotFunction(value); - auto funcOp = symbol ? functions.lookup(symbol.getValue()) : mlir_ts::FuncOp(); - if (funcOp && !funcOp.isDeclaration()) - { - stored[key].insert(symbol); - } - else - { - unknownFields.insert(key); - } - }; - - auto isFunctionType = [](mlir::Type type) { - return mlir::isa(type); - }; - - module.walk([&](mlir::Operation *op) { - // the literal's own values - if (auto constantOp = mlir::dyn_cast(op)) - { - auto constTupleType = mlir::dyn_cast(constantOp.getType()); - auto values = mlir::dyn_cast(constantOp.getValue()); - if (!constTupleType || !values) - { - return; - } - - mlir::Type tupleType = mlir_ts::TupleType::get(&getContext(), constTupleType.getFields()); - for (auto [index, value] : llvm::enumerate(values)) - { - std::pair key{tupleType, static_cast(index)}; - if (auto symbol = mlir::dyn_cast(value)) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (funcOp && !funcOp.isDeclaration()) - { - stored[key].insert(symbol.getAttr()); - } - else - { - unknownFields.insert(key); - } - } - else if (!mlir::isa(value) && index < constTupleType.getFields().size() && - isFunctionType(constTupleType.getFields()[index].type)) - { - unknownFields.insert(key); - } - } - - return; - } - - // a store into the field through the object - if (auto propertyRefOp = mlir::dyn_cast(op)) - { - auto tupleType = storageTupleOf(propertyRefOp.getObjectRef().getType()); - if (!tupleType) - { - return; - } - - std::pair key{tupleType, static_cast(propertyRefOp.getPosition())}; - for (auto &use : propertyRefOp->getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - continue; - } - - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == propertyRefOp.getResult() && storeOp.getValue() != propertyRefOp.getResult()) - { - store(key, storeOp.getValue()); - continue; - } - - // an interface vtable's offset of the field (slotField) reads nothing - if (user->getParentOfType() && mlir::isa(user)) - { - continue; - } - - unknownFields.insert(key); - } - - return; - } - - // a store into the field through an interface: into each literal its vtables reach - if (auto interfaceRefOp = mlir::dyn_cast(op)) - { - auto refType = mlir::dyn_cast(interfaceRefOp.getType()); - if (!refType || !isFunctionType(refType.getElementType())) - { - return; - } - - auto interfaceType = mlir::dyn_cast(interfaceRefOp.getInterfaceVal().getType()); - auto found = interfaceType ? interfaceFieldFamilies.find({interfaceType.getName().getValue().str(), - static_cast(interfaceRefOp.getIndex()) + - INTERFACE_VTABLE_HEADER_SLOTS}) - : interfaceFieldFamilies.end(); - for (auto &use : interfaceRefOp->getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - continue; - } - - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == interfaceRefOp.getResult() && found != interfaceFieldFamilies.end() && - found->second.closed) - { - for (auto target : found->second.targets) - { - store(target, storeOp.getValue()); - } - - continue; - } - - unknownAnywhere = true; - } - - return; - } - - // an object of the literal's type made other than by the literal: its fields may hold - // anything. A cast keeps the type's own value (the literal's box seen as the object, an - // object, a reference to its storage) or makes the null object a vtable's offsets are - // taken from; from anything else it reinterprets. - if (auto castOp = mlir::dyn_cast(op)) - { - auto tupleType = storageTupleOf(castOp.getType()); - if (tupleType && !mlir::isa(castOp.getIn().getType()) && - storageTupleOf(castOp.getIn().getType()) != tupleType) - { - unknownTypes.insert(tupleType); - } - - return; - } - - if (mlir::isa(op) || isCall(op) || isView(op) || mlir::isa(op)) - { - return; - } - - for (auto result : op->getResults()) - { - if (mlir::isa(result.getType())) - { - if (auto tupleType = storageTupleOf(result.getType())) - { - unknownTypes.insert(tupleType); - } - } - } - }); - - for (auto &[key, family] : interfaceFieldFamilies) - { - if (!family.closed) - { - continue; - } - - for (auto target : family.targets) - { - if (unknownAnywhere || unknownTypes.contains(target.first) || unknownFields.contains(target)) - { - family.closed = false; - break; - } - - for (auto member : stored.lookup(target)) - { - if (!llvm::is_contained(family.members, member)) - { - family.members.push_back(member); - } - } - } - - family.closed = family.closed && !family.members.empty(); - } - } - - // The interfaces mentioned by a type as values: the interface itself, a union or optional, a - // tuple's fields, an array's elements, what a reference points to. Not a class's or an object's - // fields (a reference to a block made elsewhere), nor a function's parameters and result. - static void valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth = 0) - { - if (!type || depth > 16) - { - return; - } - - if (auto interfaceType = mlir::dyn_cast(type)) - { - found.insert(interfaceType.getName().getValue()); - return; - } - - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) - { - valueInterfaces(info.type, found, depth + 1); - } - }; - - if (auto tupleType = mlir::dyn_cast(type)) - { - fields(tupleType.getFields()); - } - else if (auto constTupleType = mlir::dyn_cast(type)) - { - fields(constTupleType.getFields()); - } - else if (auto unionType = mlir::dyn_cast(type)) - { - for (auto member : unionType.getTypes()) - { - valueInterfaces(member, found, depth + 1); - } - } - else if (auto optionalType = mlir::dyn_cast(type)) - { - valueInterfaces(optionalType.getElementType(), found, depth + 1); - } - else if (auto arrayType = mlir::dyn_cast(type)) - { - valueInterfaces(arrayType.getElementType(), found, depth + 1); - } - else if (auto constArrayType = mlir::dyn_cast(type)) - { - valueInterfaces(constArrayType.getElementType(), found, depth + 1); - } - else if (auto refType = mlir::dyn_cast(type)) - { - valueInterfaces(refType.getElementType(), found, depth + 1); - } - else if (auto valueRefType = mlir::dyn_cast(type)) - { - valueInterfaces(valueRefType.getElementType(), found, depth + 1); - } - else if (auto boundRefType = mlir::dyn_cast(type)) - { - valueInterfaces(boundRefType.getElementType(), found, depth + 1); - } - else if (auto sharedType = mlir::dyn_cast(type)) - { - valueInterfaces(sharedType.getElementType(), found, depth + 1); - } - } - - // Every interface a type mentions anywhere, a class's fields and a function's parameters - // included: what another module handed a value of this type could reach. With `classes`, the - // classes it mentions too (a base class is its derived class's first field). - static void anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, - llvm::StringSet<> *classes = nullptr) - { - if (!type || !seen.insert(type).second) - { - return; - } - - if (auto interfaceType = mlir::dyn_cast(type)) - { - found.insert(interfaceType.getName().getValue()); - return; - } - - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) - { - anyInterfaces(info.type, found, seen, classes); - } - }; - - // the types whose fields are not parameters of the type - if (auto classStorageType = mlir::dyn_cast(type)) - { - if (classes) - { - classes->insert(classStorageType.getName().getValue()); - } - - fields(classStorageType.getFields()); - return; - } - - if (auto objectStorageType = mlir::dyn_cast(type)) - { - fields(objectStorageType.getFields()); - return; - } - - if (auto tupleType = mlir::dyn_cast(type)) - { - fields(tupleType.getFields()); - return; - } - - if (auto constTupleType = mlir::dyn_cast(type)) - { - fields(constTupleType.getFields()); - return; - } - - type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { anyInterfaces(sub, found, seen, classes); }); - } - - // The function values a value of this type holds: the type itself, a union's or an optional's - // member, a tuple's field, an array's element. Not what a reference points to (a vtable's - // address seen as an opaque pointer is the compiler's own, not a value). - static void functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth = 0) - { - if (!type || depth > 16) - { - return; - } - - if (mlir::isa(type)) - { - found.push_back(type); - return; - } - - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) - { - functionValues(info.type, found, depth + 1); - } - }; - - if (auto tupleType = mlir::dyn_cast(type)) - { - fields(tupleType.getFields()); - } - else if (auto constTupleType = mlir::dyn_cast(type)) - { - fields(constTupleType.getFields()); - } - else if (auto unionType = mlir::dyn_cast(type)) - { - for (auto member : unionType.getTypes()) - { - functionValues(member, found, depth + 1); - } - } - else if (auto optionalType = mlir::dyn_cast(type)) - { - functionValues(optionalType.getElementType(), found, depth + 1); - } - else if (auto arrayType = mlir::dyn_cast(type)) - { - functionValues(arrayType.getElementType(), found, depth + 1); - } - else if (auto constArrayType = mlir::dyn_cast(type)) - { - functionValues(constArrayType.getElementType(), found, depth + 1); - } - else if (auto sharedType = mlir::dyn_cast(type)) - { - functionValues(sharedType.getElementType(), found, depth + 1); - } - } - - // A function type's parameters and results, whatever kind of function it is. - static std::pair, llvm::SmallVector> signatureOf(mlir::Type type) - { - llvm::SmallVector inputs; - llvm::SmallVector results; - if (auto functionType = mlir::dyn_cast(type)) - { - inputs.append(functionType.getInputs().begin(), functionType.getInputs().end()); - results.append(functionType.getResults().begin(), functionType.getResults().end()); - } - else if (auto boundType = mlir::dyn_cast(type)) - { - inputs.append(boundType.getInputs().begin(), boundType.getInputs().end()); - results.append(boundType.getResults().begin(), boundType.getResults().end()); - } - else if (auto hybridType = mlir::dyn_cast(type)) - { - inputs.append(hybridType.getInputs().begin(), hybridType.getInputs().end()); - results.append(hybridType.getResults().begin(), hybridType.getResults().end()); - } - else - { - // a kind this does not read: never the same signature as another - inputs.push_back(type); - } - - return {inputs, results}; - } - - // Does a cast reinterpret a function value - from `any` or `unknown`, or as a function of another - // signature? Then an interface value its parameters or - // results hold crosses unseen (a `(o: Op2) => ...` called as `(o: Op) => ...`): `interfaces` - // gets every interface either side's signatures mention, and `boxed` the input's when it is put - // into `any`. - static bool castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing) - { - // `!ts.opaque` is the compiler's own (a vtable slot read as the `.instanceOf` it holds), not - // a value a program can put a function into - auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; - llvm::SmallVector inFunctions; - llvm::SmallVector outFunctions; - functionValues(in, inFunctions); - functionValues(out, outFunctions); - boxing = false; - if ((inFunctions.empty() && outFunctions.empty()) || in == out) - { - return false; - } - - auto mention = [&](llvm::ArrayRef types) { - for (auto type : types) - { - llvm::DenseSet seen; - anyInterfaces(type, interfaces, seen); - } - }; - - if (isOpaque(out)) - { - boxing = true; - mention(inFunctions); - return false; - } - - // the same signatures, but for `this` seen as an opaque pointer on one side, or bound on one - // side only (a closure's box, a bound function's object: the caller supplies neither) - auto sameSignature = [](mlir::Type a, mlir::Type b) { - auto [aInputs, aResults] = signatureOf(a); - auto [bInputs, bResults] = signatureOf(b); - auto dropBoundThis = [](mlir::Type type, llvm::SmallVector &inputs) { - if (!inputs.empty() && (mlir::isa(type) || mlir::isa(inputs.front()))) - { - inputs.erase(inputs.begin()); - } - }; - - if (aInputs.size() == bInputs.size() + 1) - { - dropBoundThis(a, aInputs); - } - else if (bInputs.size() == aInputs.size() + 1) - { - dropBoundThis(b, bInputs); - } - - if (aResults != bResults || aInputs.size() != bInputs.size()) - { - return false; - } - - for (auto [index, pair] : llvm::enumerate(llvm::zip(aInputs, bInputs))) - { - auto [aInput, bInput] = pair; - if (aInput != bInput && - !(index == 0 && (mlir::isa(aInput) || mlir::isa(bInput)))) - { - return false; - } - } - - return true; - }; - - // a function out of `any`, or one function seen as another; a function seen as something - // that is no function (a vtable slot's reference to it) is the compiler's own - auto reinterprets = isOpaque(in) && !outFunctions.empty(); - if (!inFunctions.empty() && !outFunctions.empty()) - { - reinterprets = reinterprets || inFunctions.size() != outFunctions.size(); - for (auto [inFunction, outFunction] : llvm::zip(inFunctions, outFunctions)) - { - reinterprets = reinterprets || !sameSignature(inFunction, outFunction); - } - } - - if (reinterprets) - { - mention(inFunctions); - mention(outFunctions); - } - - return reinterprets; - } - - // The interfaces an interface member's type mentions: a method's or a function field's - // parameters and result, a field's type, anything they hold (a class's fields included). - static void memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found) - { - llvm::DenseSet seen; - anyInterfaces(type, found, seen); - } - - static bool isNullish(mlir::Type type) - { - return mlir::isa(type); - } - - // May a cast from `in` to `out` make an interface value out of something that was not that - // interface's value already? A union or optional narrowed to its interface member (whose other - // members are null or undefined) or widened from it, null given a nullable interface type, a - // class or object seen as another (the same block, its fields as they are) and a function seen as - // another are not; a cast from another interface, `any`, an opaque pointer, or a tuple or an - // array whose element is not the same interface is. - static bool castMakesInterface(mlir::Type in, mlir::Type out, int depth = 0) - { - // null seen as an interface value has no vtable: a call through it reaches nothing - if (in == out || depth > 16 || isNullish(in)) - { - return false; - } - - llvm::StringSet<> made; - valueInterfaces(out, made); - if (made.empty()) - { - return false; - } - - if (mlir::isa(out)) - { - return !holdsAsIs(in, out, /*onlyNullable=*/true); - } - - if (mlir::isa(out)) - { - if (isNullish(in) || holdsAsIs(out, in)) - { - return false; - } - - llvm::SmallVector members; - if (auto unionType = mlir::dyn_cast(in)) - { - members.append(unionType.getTypes().begin(), unionType.getTypes().end()); - } - else if (auto optionalType = mlir::dyn_cast(in)) - { - members.push_back(optionalType.getElementType()); - } - else - { - return true; - } - - return !llvm::all_of(members, [&](mlir::Type member) { return isNullish(member) || holdsAsIs(out, member); }); - } - - auto tupleFields = [](mlir::Type type) -> std::optional> { - if (auto tupleType = mlir::dyn_cast(type)) - { - return tupleType.getFields(); - } - - if (auto constTupleType = mlir::dyn_cast(type)) - { - return constTupleType.getFields(); - } - - return std::nullopt; - }; - - if (auto outFields = tupleFields(out)) - { - auto inFields = tupleFields(in); - if (!inFields || inFields->size() != outFields->size()) - { - return true; - } - - for (auto [inField, outField] : llvm::zip(*inFields, *outFields)) - { - if (castMakesInterface(inField.type, outField.type, depth + 1)) - { - return true; - } - } - - return false; - } - - auto elementOf = [](mlir::Type type) -> mlir::Type { - if (auto arrayType = mlir::dyn_cast(type)) - { - return arrayType.getElementType(); - } - - if (auto constArrayType = mlir::dyn_cast(type)) - { - return constArrayType.getElementType(); - } - - return {}; - }; - - if (auto outElement = elementOf(out)) - { - auto inElement = elementOf(in); - return !inElement || castMakesInterface(inElement, outElement, depth + 1); - } - - // a reference seen as another (`!ts.bound_ref` as `!ts.ref`): what it points to - auto pointee = [](mlir::Type type) -> mlir::Type { - if (auto refType = mlir::dyn_cast(type)) - { - return refType.getElementType(); - } - - if (auto valueRefType = mlir::dyn_cast(type)) - { - return valueRefType.getElementType(); - } - - if (auto boundRefType = mlir::dyn_cast(type)) - { - return boundRefType.getElementType(); - } - - return {}; - }; - - if (auto outPointee = pointee(out)) - { - auto inPointee = pointee(in); - return !inPointee || castMakesInterface(inPointee, outPointee, depth + 1); - } - - // anything else reinterpreted: what is read out of it was never checked - return true; - } - - // The function a vtable slot holds: `ts.SymbolRef`, seen through casts. Null for anything else. - static mlir::StringAttr slotFunction(mlir::Value value) - { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) - { - value = castOp.getIn(); - } - - auto symbolRefOp = value ? value.getDefiningOp() : mlir_ts::SymbolRefOp(); - return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); - } - - static mlir_ts::AddressOfOp addressOfThroughCasts(mlir::Value value) - { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) - { - value = castOp.getIn(); - } - - return value ? value.getDefiningOp() : mlir_ts::AddressOfOp(); - } - - // The words of the module's exported declarations (`__decls*` globals): what another module - // can name. - static llvm::StringSet<> exportedWords(mlir::ModuleOp module) - { - llvm::StringSet<> words; - module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!globalOp.getSymName().starts_with("__decls")) - { - return; - } - - globalOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { - auto text = mlir::dyn_cast(constantOp.getValue()); - if (!text) - { - return; - } - - llvm::StringRef rest = text.getValue(); - while (!rest.empty()) - { - auto isWordChar = [](char c) { return llvm::isAlnum(c) || c == '_' || c == '$'; }; - rest = rest.drop_while([&](char c) { return !isWordChar(c); }); - auto word = rest.take_while(isWordChar); - if (!word.empty()) - { - words.insert(word); - } - - rest = rest.drop_front(word.size()); - } - }); - }); - - return words; - } - - // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a - // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the - // slots of a class's own vtable that hold the address of one of its interface vtables - struct InterfaceVTable - { - mlir_ts::TupleType type; - llvm::DenseMap slots; - // a field's slot: the object literal's field it is the offset of - llvm::DenseMap> fields; - bool written = false; - }; - - // What collectInterfaceFamilies learns of the module, one step after another. - struct InterfaceScan - { - // the interfaces open whatever their vtables hold - llvm::StringSet<> open; - llvm::StringMap vtables; - // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) - llvm::SmallVector unread; - // interface name -> the interface vtables class vtables hold for it, read through an - // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) - llvm::StringMap> classHeld; - // interface name -> the vtables its values are made with - llvm::StringMap> madeWith; - // An interface value made out of another one reaches the other's members: both are open. One - // made out of `any` or an opaque pointer may be any interface value put into one: every - // interface so boxed is open with it, and every interface at all when it is untraceable. - bool allOpen = false; - llvm::StringSet<> boxed; - llvm::StringSet<> unboxedInto; - bool unboxedFunction = false; - // the interfaces a cast makes a value of out of another interface's or an opaque one, and - // the ones it came from: closed over their members' types below - llvm::StringSet<> castOpened; - // interface -> the interfaces its members' types mention: its vtables' slots and what is - // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields - // included - llvm::StringMap> memberInterfaces; - llvm::DenseSet tied; - - void openAll(const llvm::StringSet<> &names) - { - for (auto &entry : names) - { - open.insert(entry.getKey()); - } - } - }; - - void collectInterfaceFamilies(mlir::ModuleOp module) - { - InterfaceScan scan; - readInterfaceVTables(module, scan); - traceInterfaceValues(module, scan); - openExposedInterfaces(module, scan); - openThroughMembers(scan); - openLooseVTables(scan); - buildInterfaceFamilies(scan); - collectFieldFunctions(module); - markInterfaceReached(scan); - } - - // The vtables: each interface vtable's slots, and the interface vtables a class's own vtable holds. - void readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan) - { - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto name = globalOp.getSymName(); - if (!name.ends_with("..vtbl")) - { - return; - } - - auto owner = name.drop_back(llvm::StringRef("..vtbl").size()); - llvm::DenseMap values; - globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { - if (insertOp.getPosition().size() == 1) - { - values[insertOp.getPosition()[0]] = insertOp.getValue(); - } - }); - - auto header = slotFunction(values.lookup(0)); - auto tupleType = mlir::dyn_cast(globalOp.getType()); - if (!header || !tupleType) - { - scan.unread.push_back(name); - return; - } - - if (header.getValue() == (owner + "..instanceOf").str()) - { - // an exported class (not in an executable, whose exports no module sees) - auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); - ownClasses.insert(owner); - if (exposed) - { - nameableClasses.insert(owner); - } - - for (auto &[position, value] : values) - { - auto addressOfOp = addressOfThroughCasts(value); - auto id = position < static_cast(tupleType.size()) - ? mlir::dyn_cast_or_null(tupleType.getFieldInfo(position).id) - : mlir::StringAttr(); - if (!addressOfOp || !id) - { - continue; - } - - scan.classHeld[id.getValue()].insert(addressOfOp.getGlobalName()); - if (exposed) - { - scan.open.insert(id.getValue()); // another module may extend the class - } - } - - return; - } - - if (!header.getValue().ends_with("..instanceOf") && header.getValue() != ".instanceOf.none") - { - scan.unread.push_back(name); - return; - } - - auto &vtable = scan.vtables[name]; - vtable.type = tupleType; - for (auto &[position, value] : values) - { - if (position > 0) - { - vtable.slots[position] = slotFunction(value); - if (auto field = slotField(value); field.first) - { - vtable.fields[position] = field; - } - } - } - }); - } - - // What makes an interface value, out of which vtable; the casts that box, unbox or reinterpret one; - // the interfaces its members' types mention; the vtables written. - void traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan) - { - auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { - // the global itself - if (auto addressOfOp = addressOfThroughCasts(value)) - { - return scan.vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); - } - - // a copy of it on the heap, its absent members' slots patched - while (auto castOp = value.getDefiningOp()) - { - value = castOp.getIn(); - } - - if (auto newOp = value.getDefiningOp()) - { - llvm::StringRef copied; - for (auto *user : newOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() - ? storeOp.getValue().getDefiningOp() - : mlir_ts::LoadOp(); - auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); - if (addressOfOp && scan.vtables.count(addressOfOp.getGlobalName())) - { - if (!copied.empty()) - { - return {}; - } + std::pair slotField(mlir::Value value); - copied = addressOfOp.getGlobalName(); - } - } - - return copied; - } - - return {}; - }; - - // an object's class vtable read at run time: `ts.VTableOffsetRef(ts.Load(ts.PropertyRef(object, 0)), k)` - auto readFromClass = [](mlir::Value value) { - auto offsetRefOp = value.getDefiningOp(); - auto loadOp = offsetRefOp ? offsetRefOp.getVtable().getDefiningOp() : mlir_ts::LoadOp(); - auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - return propertyRefOp && propertyRefOp.getPosition() == 0 && - mlir::isa(propertyRefOp.getObjectRef().getType()); - }; - - // what reads a vtable's address: making an interface value, a class vtable's slot, the copy - std::function onlyMakesValues = [&](mlir::Value address) { - return llvm::all_of(address.getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (auto newInterfaceOp = mlir::dyn_cast(user)) - { - return newInterfaceOp.getInterfaceVTable() == use.get(); - } - - if (mlir::isa(user)) - { - return onlyMakesValues(user->getResult(0)); - } - - if (mlir::isa(user)) - { - return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; - } - - // the copy: read whole, and stored into a block `ts.New` made - if (auto loadOp = mlir::dyn_cast(user)) - { - return llvm::all_of(loadOp->getUsers(), [&](mlir::Operation *loadUser) { - auto storeOp = mlir::dyn_cast(loadUser); - return storeOp && storeOp.getValue() == loadOp.getResult() && - storeOp.getReference().getDefiningOp(); - }); - } - - return false; - }); - }; + // What every object-literal field an interface's field slot may be holds (InterfaceFieldFamily). + void collectFieldFunctions(mlir::ModuleOp module); - auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; - module.walk([&](mlir::Operation *op) { - if (auto newInterfaceOp = mlir::dyn_cast(op)) - { - auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); - auto vtable = newInterfaceOp.getInterfaceVTable(); - if (auto found = vtableOf(vtable); !found.empty()) - { - scan.madeWith[name].insert(found); - } - else if (readFromClass(vtable)) - { - // any class's vtable for the interface (classHeld, all in its families below) - if (!scan.classHeld.count(name)) - { - scan.open.insert(name); - } - } - else if (auto extractOp = vtable.getDefiningOp()) - { - scan.castOpened.insert(name); - valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); - } - else - { - scan.allOpen = true; - } + // The interfaces mentioned by a type as values: the interface itself, a union or optional, a + // tuple's fields, an array's elements, what a reference points to. Not a class's or an object's + // fields (a reference to a block made elsewhere), nor a function's parameters and result. + static void valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth = 0); - return; - } + // Every interface a type mentions anywhere, a class's fields and a function's parameters + // included: what another module handed a value of this type could reach. With `classes`, the + // classes it mentions too (a base class is its derived class's first field). + static void anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, + llvm::StringSet<> *classes = nullptr); - if (auto castOp = mlir::dyn_cast(op)) - { - auto in = castOp.getIn().getType(); - auto out = castOp.getType(); - if (isOpaque(out)) - { - valueInterfaces(in, scan.boxed); - } + // The function values a value of this type holds: the type itself, a union's or an optional's + // member, a tuple's field, an array's element. Not what a reference points to (a vtable's + // address seen as an opaque pointer is the compiler's own, not a value). + static void functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth = 0); - if (castMakesInterface(in, out)) - { - valueInterfaces(out, scan.castOpened); - valueInterfaces(in, scan.castOpened); - if (isOpaque(in)) - { - valueInterfaces(out, scan.unboxedInto); - } - } + // A function type's parameters and results, whatever kind of function it is. + static std::pair, llvm::SmallVector> signatureOf(mlir::Type type); - // a function value reinterpreted carries the interface values of its parameters and - // results across unseen (review6 x1c, x1r) - llvm::StringSet<> signatures; - auto boxing = false; - if (castReinterpretsFunction(in, out, signatures, boxing)) - { - scan.openAll(signatures); - if (isOpaque(in)) - { - scan.unboxedFunction = true; - } - } - else if (boxing) - { - for (auto &entry : signatures) - { - scan.boxed.insert(entry.getKey()); - } - } + // Does a cast reinterpret a function value - from `any` or `unknown`, or as a function of another + // signature? Then an interface value its parameters or + // results hold crosses unseen (a `(o: Op2) => ...` called as `(o: Op) => ...`): `interfaces` + // gets every interface either side's signatures mention, and `boxed` the input's when it is put + // into `any`. + static bool castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing); - return; - } + // The interfaces an interface member's type mentions: a method's or a function field's + // parameters and result, a field's type, anything they hold (a class's fields included). + static void memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found); - if (auto interfaceRefOp = mlir::dyn_cast(op)) - { - if (!interfaceRefOp->use_empty()) - { - scan.open.insert(interfaceRefOp.getIdentifier()); - } + static bool isNullish(mlir::Type type); - return; - } + // May a cast from `in` to `out` make an interface value out of something that was not that + // interface's value already? A union or optional narrowed to its interface member (whose other + // members are null or undefined) or widened from it, null given a nullable interface type, a + // class or object seen as another (the same block, its fields as they are) and a function seen as + // another are not; a cast from another interface, `any`, an opaque pointer, or a tuple or an + // array whose element is not the same interface is. + static bool castMakesInterface(mlir::Type in, mlir::Type out, int depth = 0); - if (auto symbolRefOp = mlir::dyn_cast(op)) - { - if (auto interfaceType = mlir::dyn_cast(symbolRefOp.getInterfaceVal().getType())) - { - memberTypeInterfaces(symbolRefOp.getType(), scan.memberInterfaces[interfaceType.getName().getValue()]); - } + // The function a vtable slot holds: `ts.SymbolRef`, seen through casts. Null for anything else. + static mlir::StringAttr slotFunction(mlir::Value value); - return; - } + static mlir_ts::AddressOfOp addressOfThroughCasts(mlir::Value value); - if (auto addressOfOp = mlir::dyn_cast(op)) - { - if (auto found = scan.vtables.find(addressOfOp.getGlobalName()); found != scan.vtables.end() && !onlyMakesValues(addressOfOp.getResult())) - { - found->second.written = true; - } - } - }); - } + // The words of the module's exported declarations (`__decls*` globals): what another module + // can name. + static llvm::StringSet<> exportedWords(mlir::ModuleOp module); - // The interfaces another module sees, and the ones a value boxed into `any` may be unboxed into. - void openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan) + // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a + // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the + // slots of a class's own vtable that hold the address of one of its interface vtables + struct InterfaceVTable { - // seen from another module: a word of its exported declarations, or in a reachable type. An - // executable's exports are seen by no other module (USER decision, spec 31): only what it - // imports - a declared function, an imported global - comes from outside. - if (!executable) - { - exportWords = exportedWords(module); - } - - llvm::StringSet<> exposed; - llvm::DenseSet seen; - for (auto &entry : functions) - { - // a `public` method of a class no other module can name is not one: only a declaration - // re-parsed from `__decls` names a function of another module - auto exported = !executable && (entry.second->hasAttr("export") || entry.second->hasAttr(DLL_EXPORT)); - if (entry.second.isDeclaration() || exported) - { - anyInterfaces(entry.second.getFunctionType(), exposed, seen, &nameableClasses); - } - } - - for (auto &entry : globals) - { - auto globalOp = entry.second; - // another module's global, declared here with no initializer (an import of a .ts module - // carries `export`) - auto imported = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || - globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); - auto exported = !globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT); - if (imported || (exported && !executable)) - { - anyInterfaces(globalOp.getType(), exposed, seen, &nameableClasses); - } - } - - scan.openAll(exposed); - if (!scan.unboxedInto.empty() || scan.unboxedFunction) - { - scan.openAll(scan.boxed); - for (auto &entry : scan.boxed) - { - scan.castOpened.insert(entry.getKey()); - } - } - } + mlir_ts::TupleType type; + llvm::DenseMap slots; + // a field's slot: the object literal's field it is the offset of + llvm::DenseMap> fields; + bool written = false; + }; - // Every interface the members of one made out of another mention, recursively. - void openThroughMembers(InterfaceScan &scan) + // What collectInterfaceFamilies learns of the module, one step after another. + struct InterfaceScan { - // An interface value made out of another one (or out of `any`) is called with the other's - // members: `HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` hands an `Op` to a - // function that calls through `Op2` (review6r1 v5, v11f). So every interface either side's - // members mention is open too, and theirs, recursively (controller ruling, spec 31). - // - // A class's vtables for its interfaces are its members' families whether or not this module - // makes an interface value of the class (a method called directly then keeps its facts), and - // their slots' types are the interface's members' types: an interface whose values are all - // made from class objects is read through them (review6r2 d1-d3). - for (auto &[name, held] : scan.classHeld) - { - scan.madeWith[name].insert(held.begin(), held.end()); - } + // the interfaces open whatever their vtables hold + llvm::StringSet<> open; + llvm::StringMap vtables; + // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) + llvm::SmallVector unread; + // interface name -> the interface vtables class vtables hold for it, read through an + // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) + llvm::StringMap> classHeld; + // interface name -> the vtables its values are made with + llvm::StringMap> madeWith; + // An interface value made out of another one reaches the other's members: both are open. One + // made out of `any` or an opaque pointer may be any interface value put into one: every + // interface so boxed is open with it, and every interface at all when it is untraceable. + bool allOpen = false; + llvm::StringSet<> boxed; + llvm::StringSet<> unboxedInto; + bool unboxedFunction = false; + // the interfaces a cast makes a value of out of another interface's or an opaque one, and + // the ones it came from: closed over their members' types below + llvm::StringSet<> castOpened; + // interface -> the interfaces its members' types mention: its vtables' slots and what is + // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields + // included + llvm::StringMap> memberInterfaces; + llvm::DenseSet tied; - for (auto &[name, made] : scan.madeWith) + void openAll(const llvm::StringSet<> &names) { - for (auto vtableName : made) + for (auto &entry : names) { - if (auto found = scan.vtables.find(vtableName); found != scan.vtables.end() && found->second.type) - { - for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) - { - if (position > 0) - { - memberTypeInterfaces(field.type, scan.memberInterfaces[name]); - } - } - } + open.insert(entry.getKey()); } } + }; - llvm::SmallVector work; - for (auto &entry : scan.castOpened) - { - work.push_back(entry.getKey().str()); - } + void collectInterfaceFamilies(mlir::ModuleOp module); - llvm::StringSet<> expanded; - while (!work.empty()) - { - auto name = work.pop_back_val(); - if (!expanded.insert(name).second) - { - continue; - } + // The vtables: each interface vtable's slots, and the interface vtables a class's own vtable holds. + void readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan); - scan.open.insert(name); - for (auto &member : scan.memberInterfaces[name]) - { - work.push_back(member.getKey().str()); - } - } - } + // What makes an interface value, out of which vtable; the casts that box, unbox or reinterpret one; + // the interfaces its members' types mention; the vtables written. + void traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan); - // An interface with a vtable this pass could not tie to its values. - void openLooseVTables(InterfaceScan &scan) - { - // A vtable this pass could not read, or one no value of any interface is made with, that is - // named for an interface (`...vtbl`, `...vtbl`): the interface may have an - // implementation outside its families. Belt and braces - every way to make an interface - // value is traced above - but a missed member would get its family's facts. - llvm::DenseSet made; - for (auto &entry : scan.madeWith) - { - made.insert(entry.second.begin(), entry.second.end()); - } + // The interfaces another module sees, and the ones a value boxed into `any` may be unboxed into. + void openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan); - llvm::SmallVector loose(scan.unread.begin(), scan.unread.end()); - for (auto &entry : scan.vtables) - { - if (!made.contains(entry.getKey())) - { - loose.push_back(entry.getKey()); - } - } + // Every interface the members of one made out of another mention, recursively. + void openThroughMembers(InterfaceScan &scan); - for (auto &entry : scan.madeWith) - { - auto name = entry.getKey().str(); - if (llvm::any_of(loose, [&](llvm::StringRef vtableName) { - return vtableName.ends_with("." + name + "..vtbl") || vtableName.starts_with(name + "."); - })) - { - scan.open.insert(entry.getKey()); - } - } - } + // An interface with a vtable this pass could not tie to its values. + void openLooseVTables(InterfaceScan &scan); // The families of each interface's slots - its methods and its function-typed fields - closed // where nothing opened the interface. - void buildInterfaceFamilies(InterfaceScan &scan) - { - auto &words = exportWords; - auto isOpen = [&](llvm::StringRef name) { - // a namespaced interface is declared by its last name - auto last = name.rsplit('.').second; - return scan.allOpen || scan.open.contains(name) || words.contains(name) || (!last.empty() && words.contains(last)); - }; - - // the families - for (auto &[name, made] : scan.madeWith) - { - // a class vtable's slot holding a vtable this pass did not read (another module's) - auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return scan.vtables.count(vtableName); }); - auto closed = !isOpen(name) && allRead; - for (auto vtableName : made) - { - if (!scan.vtables.count(vtableName)) - { - continue; - } - - if (allRead) - { - interfaceVTableTypes[name].push_back(scan.vtables[vtableName].type); - } - - scan.tied.insert(vtableName); - auto &vtable = scan.vtables[vtableName]; - for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) - { - if (position == 0) - { - continue; - } - - auto &family = interfaceFamilies[{name.str(), static_cast(position)}]; - auto id = mlir::dyn_cast_or_null(field.id); - auto member = vtable.slots.lookup(position); - auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp(); - if (!family.field) - { - family.field = id; - } - - // a member that is not private (a `public` method) is open (findOpen): it has no - // facts to give, and gives the family none - family.closed = family.closed && closed && !vtable.written && id && family.field == id && funcOp && - !funcOp.isDeclaration() && !declaresCallerFacts(funcOp); - if (member && !llvm::is_contained(family.members, member)) - { - family.members.push_back(member); - } - - // the same slot as a field: an object literal's field of function type - auto &fieldFamily = interfaceFieldFamilies[{name.str(), static_cast(position)}]; - auto target = vtable.fields.lookup(position); - auto refType = mlir::dyn_cast(field.type); - if (!fieldFamily.field) - { - fieldFamily.field = id; - } - - fieldFamily.closed = fieldFamily.closed && closed && !vtable.written && id && fieldFamily.field == id && - target.first && refType && mlir::isa(refType.getElementType()); - if (target.first && !llvm::is_contained(fieldFamily.targets, target)) - { - fieldFamily.targets.push_back(target); - } - } - } - } - } + void buildInterfaceFamilies(InterfaceScan &scan); // What an open family holds, and what a vtable no interface value is made with holds. - void markInterfaceReached(InterfaceScan &scan) - { - // what an open family holds, and a vtable no interface value is made with, is reached - // through calls that are not resolved - auto reached = [&](mlir::StringAttr member) { - if (auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp()) - { - interfaceReached.insert(funcOp); - } - }; - - for (auto &[key, family] : interfaceFamilies) - { - if (!family.closed) - { - llvm::for_each(family.members, reached); - } - } - - for (auto &entry : scan.vtables) - { - if (!scan.tied.contains(entry.getKey())) - { - for (auto &slot : entry.second.slots) - { - reached(slot.second); - } - } - } - } + void markInterfaceReached(InterfaceScan &scan); static constexpr const char *INTERFACE_REACHED_WHY = "it is reached through an interface, whose calls are not given its facts"; From c4f6e775f6afb790f2c9964b0cba8618e5641f48 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:16:08 +0100 Subject: [PATCH 10/25] own passes: OwnershipSignatureResolve.cpp out of OwnershipSignaturePass.h (I1) A call resolved to its callees - class vtables and their families, imported declarations and the C-scalar surface (spec 31). A pure move of collectClassVTables, storageTupleOf, classVTableEntries, isClassVTableEntry, methodName, familyOf, addDefined, addImported, scalarType, scalarSignature, scalarDeclaration, addImportedByAddress, resolve, resolveValue, resolveFamily, isInstanceOfSlot. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../lib/TypeScript/OwnershipSignaturePass.h | 353 +---------------- .../TypeScript/OwnershipSignatureResolve.cpp | 364 ++++++++++++++++++ 3 files changed, 381 insertions(+), 337 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignatureResolve.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 004c82b6a..71a6f82cd 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -36,6 +36,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipSignaturePass.cpp OwnershipSignatureGlobals.cpp OwnershipSignatureInterfaces.cpp + OwnershipSignatureResolve.cpp OwnedReturnConsumptionPass.cpp StackPromotionPass.cpp BorrowFactsExportPass.cpp diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 6538f1204..ee847b40f 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -872,171 +872,43 @@ class OwnershipSignaturePass : public mlir::PassWrapper..vtbl` that holds its `..instanceOf`. An // interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call // through an interface does not name what it reaches. - void collectClassVTables(mlir::ModuleOp module) - { - llvm::SmallVector>>> vtables; - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto entries = classVTableEntries(globalOp); - if (!entries.empty()) - { - auto name = globalOp.getSymName(); - classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); - vtables.push_back({name, std::move(entries)}); - } - }); - - for (auto &[name, entries] : vtables) - { - for (auto [position, symbol] : entries) - { - families[{position, methodName(symbol.getValue())}].push_back(symbol); - } - } - } + void collectClassVTables(mlir::ModuleOp module); // Does a method reached through an open interface keep a parameter or return a borrow // (interfaceReached)? Then its callers through the interface cannot be told, and it has no facts // at all. bool factsThroughInterface(mlir::Operation *funcOp, const Facts &facts); - mlir_ts::TupleType storageTupleOf(mlir::Type type) - { - return own_facts::storageTupleOf(&getContext(), type); - } + mlir_ts::TupleType storageTupleOf(mlir::Type type); // The functions a class vtable holds, by position; empty for any other global. - static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) - { - llvm::SmallVector> entries; - if (!globalOp.getSymName().ends_with("..vtbl")) - { - return entries; - } + static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp); - auto isClass = false; - globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { - auto symbolRefOp = insertOp.getValue().getDefiningOp(); - if (!symbolRefOp || insertOp.getPosition().size() != 1) - { - return; - } - - auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); - isClass = isClass || symbol.getValue().ends_with("..instanceOf"); - entries.push_back({insertOp.getPosition()[0], symbol}); - }); - - if (!isClass) - { - entries.clear(); - } - - return entries; - } - - static bool isClassVTableEntry(mlir::Operation *op) - { - auto globalOp = op->getParentOfType(); - return globalOp && !classVTableEntries(globalOp).empty(); - } + static bool isClassVTableEntry(mlir::Operation *op); // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An // override keeps its base's method name, which is what puts both in one family. - std::string methodName(llvm::StringRef symbol) - { - size_t best = 0; - for (auto &entry : classNames) - { - auto name = entry.getKey(); - if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && - symbol[name.size()] == '.') - { - best = name.size() + 1; - } - } + std::string methodName(llvm::StringRef symbol); - return symbol.drop_front(best).str(); - } - - llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier) - { - llvm::SmallVector members; - if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) - { - members = found->second; - } - - if (!llvm::is_contained(members, identifier)) - { - members.push_back(identifier); - } - - return members; - } + llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier); // Adds `symbol`'s function to `callees`; false when it is not defined in this module. - bool addDefined(mlir::StringAttr symbol, Callees &callees) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || funcOp.isDeclaration()) - { - return false; - } - - if (!llvm::is_contained(callees.funcs, funcOp)) - { - callees.funcs.push_back(funcOp); - } - - return true; - } + bool addDefined(mlir::StringAttr symbol, Callees &callees); // A function declared here and defined in a library that exported its `__own_no_drops`: one // linked with this module, through its import library. Or a C function declared // `@nodrops` (OWN_DECLARED_NO_DROPS_ATTR_NAME), or one whose parameters and result hold no // heap block (scalarDeclaration). - bool addImported(mlir::StringAttr symbol, Callees &callees) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || !funcOp.isDeclaration() || - !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME) || - (scalarDeclaration(funcOp) && !functionsEscape))) - { - return false; - } - - callees.imported = true; - callees.importedName = symbol.getValue(); - return true; - } + bool addImported(mlir::StringAttr symbol, Callees &callees); // A number, a boolean, a character, or an enum or a literal of one: a value that holds no block // and no function of this module, so given to foreign code it hands it neither. Not an opaque // pointer, a reference, a string, a class, an interface, an object, a tuple, an array, a union // with such a member, a function, `any` or `unknown`. - static bool scalarType(mlir::Type type) - { - if (auto literalType = mlir::dyn_cast(type)) - { - return scalarType(literalType.getElementType()); - } - - if (auto enumType = mlir::dyn_cast(type)) - { - return scalarType(enumType.getElementType()); - } - - return mlir::isa(type); - } + static bool scalarType(mlir::Type type); // Every parameter and result scalar (a void result too), and no rest parameter. - static bool scalarSignature(mlir_ts::FunctionType functionType) - { - return !functionType.isVarArg() && llvm::all_of(functionType.getInputs(), scalarType) && - llvm::all_of(functionType.getResults(), - [](mlir::Type type) { return mlir::isa(type) || scalarType(type); }); - } + static bool scalarSignature(mlir_ts::FunctionType functionType); // A body-less `declare function` of C (no `export` or import marker: not another TypeScript // module's, whose declarations carry `export`) whose parameters and result are scalar @@ -1046,185 +918,19 @@ class OwnershipSignaturePass : public mlir::PassWrapperhasAttr("export") && !funcOp->hasAttr("import") && - !funcOp->hasAttr(DLL_EXPORT) && !funcOp->hasAttr(DLL_IMPORT) && scalarSignature(funcOp.getFunctionType()); - } + static bool scalarDeclaration(mlir_ts::FuncOp funcOp); // `ts.Load(ts.AddressOf @f)`, where the global `f` holds the address of a function exported // by a library loaded at run time, looked up by name: that function, if the library exported // its `__own_no_drops`. - bool addImportedByAddress(mlir::Value callee, Callees &callees) - { - auto loadOp = callee.getDefiningOp(); - auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - if (!addressOfOp) - { - return false; - } - - auto globalOp = globals.lookup(addressOfOp.getGlobalName()); - if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) - { - return false; - } - - // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` - auto ®ion = globalOp.getInitializerRegion(); - if (!region.hasOneBlock()) - { - return false; - } - - auto resultOp = mlir::dyn_cast(region.front().getTerminator()); - auto castOp = resultOp && resultOp->getNumOperands() == 1 - ? resultOp->getOperand(0).getDefiningOp() - : mlir_ts::CastOp(); - auto searchOp = castOp ? castOp.getIn().getDefiningOp() - : mlir_ts::SearchForAddressOfSymbolOp(); - auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); - auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); - if (!name || !importedNoDrops.contains(name.getValue())) - { - return false; - } - - callees.imported = true; - callees.importedName = name.getValue(); - return true; - } + bool addImportedByAddress(mlir::Value callee, Callees &callees); - Callees resolve(mlir::Operation *op) - { - Callees callees; - if (auto callee = directCallee(op)) - { - callees.named.push_back(callee.getAttr()); - callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); - } - else if (mlir::isa(op)) - { - resolveValue(calleeValue(op), callees); - } - - if (!callees.known) - { - callees.funcs.clear(); - } - - return callees; - } + Callees resolve(mlir::Operation *op); // The function value a call goes through: a method of a class, virtual or not, a symbol, or // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an // interface's method - is unknown. - void resolveValue(mlir::Value callee, Callees &callees) - { - // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function - // keeps; a method of a class imported so is bound to its object first - auto function = callee; - if (auto getMethodOp = function.getDefiningOp()) - { - function = getMethodOp.getBoundFunc(); - } - - if (auto boundOp = function.getDefiningOp()) - { - function = boundOp.getFunc(); - } - - auto loadOp = function.getDefiningOp(); - if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) - { - callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); - } - - if (addImportedByAddress(callee, callees)) - { - callees.known = true; - return; - } - - if (auto getMethodOp = callee.getDefiningOp()) - { - callee = getMethodOp.getBoundFunc(); - } - - auto *def = callee.getDefiningOp(); - mlir::StringAttr identifier; - int64_t index = -1; - if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (isInstanceOfSlot(def)) - { - callees.known = true; - callees.instanceOf = true; - return; - } - else if (auto symbol = fieldFunctions.of(def)) - { - callees.known = addDefined(symbol, callees); - callees.fieldFunction = callees.known; - return; - } - else if (auto interfaceRefOp = mlir::dyn_cast_or_null(def)) - { - resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); - return; - } - else if (auto loadOp = mlir::dyn_cast_or_null(def)) - { - resolveCallback(loadOp, callees); - return; - } - else if (auto boundOp = mlir::dyn_cast_or_null(def)) - { - // a function-typed field of an interface, called: `ts.CreateBoundFunction( - // ts.ExtractInterfaceThis(i), ts.Load(ts.InterfaceSymbolRef(i, f)))` - auto loadOp = boundOp.getFunc().getDefiningOp(); - auto refOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::InterfaceSymbolRefOp(); - auto thisOp = boundOp.getThisVal().getDefiningOp(); - if (refOp && isInterfaceField(refOp.getResult()) && thisOp && thisOp.getInterfaceVal() == refOp.getInterfaceVal()) - { - resolveInterfaceSlot(refOp, interfaceFieldFamilies, callees); - } - - return; - } - - if (!identifier) - { - return; - } - - if (index < 0) - { - callees.named.push_back(identifier); - callees.known = addDefined(identifier, callees) || addImported(identifier, callees); - return; - } - - callees.named = familyOf(index, identifier); - resolveFamily(index, identifier, callees); - } + void resolveValue(mlir::Value callee, Callees &callees); // A method of a closed interface (interfaceFamilies), `ts.InterfaceSymbolRef(value, index)`, or a // function-typed field of one (interfaceFieldFamilies), read and called: the family of the slot @@ -1620,41 +1326,14 @@ class OwnershipSignaturePass : public mlir::PassWrapper(def); - auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); - if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) - { - return false; - } - - // a class vtable, read out of an object's first word, or an interface's own vtable, - // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) - auto *vtable = slot.getVtable().getDefiningOp(); - if (auto classVTable = mlir::dyn_cast_or_null(vtable)) - { - return classVTable.getIndex() == 0; - } - - return mlir::isa_and_nonnull(vtable); - } + static bool isInstanceOfSlot(mlir::Operation *def); // Is every use of this function value the callee of a call - through `ts.GetMethod`, with // `ts.GetThis` reading the object beside it? diff --git a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp new file mode 100644 index 000000000..2fa6e57e7 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp @@ -0,0 +1,364 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): a call resolved to its callees - class +// vtables and their families, imported declarations and the C-scalar surface (spec 31). + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + void OwnershipSignaturePass::collectClassVTables(mlir::ModuleOp module) + { + llvm::SmallVector>>> vtables; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto entries = classVTableEntries(globalOp); + if (!entries.empty()) + { + auto name = globalOp.getSymName(); + classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); + vtables.push_back({name, std::move(entries)}); + } + }); + + for (auto &[name, entries] : vtables) + { + for (auto [position, symbol] : entries) + { + families[{position, methodName(symbol.getValue())}].push_back(symbol); + } + } + } + + mlir_ts::TupleType OwnershipSignaturePass::storageTupleOf(mlir::Type type) + { + return own_facts::storageTupleOf(&getContext(), type); + } + + llvm::SmallVector> OwnershipSignaturePass::classVTableEntries(mlir_ts::GlobalOp globalOp) + { + llvm::SmallVector> entries; + if (!globalOp.getSymName().ends_with("..vtbl")) + { + return entries; + } + + auto isClass = false; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + auto symbolRefOp = insertOp.getValue().getDefiningOp(); + if (!symbolRefOp || insertOp.getPosition().size() != 1) + { + return; + } + + auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); + isClass = isClass || symbol.getValue().ends_with("..instanceOf"); + entries.push_back({insertOp.getPosition()[0], symbol}); + }); + + if (!isClass) + { + entries.clear(); + } + + return entries; + } + + bool OwnershipSignaturePass::isClassVTableEntry(mlir::Operation *op) + { + auto globalOp = op->getParentOfType(); + return globalOp && !classVTableEntries(globalOp).empty(); + } + + std::string OwnershipSignaturePass::methodName(llvm::StringRef symbol) + { + size_t best = 0; + for (auto &entry : classNames) + { + auto name = entry.getKey(); + if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && + symbol[name.size()] == '.') + { + best = name.size() + 1; + } + } + + return symbol.drop_front(best).str(); + } + + llvm::SmallVector OwnershipSignaturePass::familyOf(int64_t index, mlir::StringAttr identifier) + { + llvm::SmallVector members; + if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) + { + members = found->second; + } + + if (!llvm::is_contained(members, identifier)) + { + members.push_back(identifier); + } + + return members; + } + + bool OwnershipSignaturePass::addDefined(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || funcOp.isDeclaration()) + { + return false; + } + + if (!llvm::is_contained(callees.funcs, funcOp)) + { + callees.funcs.push_back(funcOp); + } + + return true; + } + + bool OwnershipSignaturePass::addImported(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || !funcOp.isDeclaration() || + !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME) || + (scalarDeclaration(funcOp) && !functionsEscape))) + { + return false; + } + + callees.imported = true; + callees.importedName = symbol.getValue(); + return true; + } + + bool OwnershipSignaturePass::scalarType(mlir::Type type) + { + if (auto literalType = mlir::dyn_cast(type)) + { + return scalarType(literalType.getElementType()); + } + + if (auto enumType = mlir::dyn_cast(type)) + { + return scalarType(enumType.getElementType()); + } + + return mlir::isa(type); + } + + bool OwnershipSignaturePass::scalarSignature(mlir_ts::FunctionType functionType) + { + return !functionType.isVarArg() && llvm::all_of(functionType.getInputs(), scalarType) && + llvm::all_of(functionType.getResults(), + [](mlir::Type type) { return mlir::isa(type) || scalarType(type); }); + } + + bool OwnershipSignaturePass::scalarDeclaration(mlir_ts::FuncOp funcOp) + { + return funcOp.isDeclaration() && !funcOp->hasAttr("export") && !funcOp->hasAttr("import") && + !funcOp->hasAttr(DLL_EXPORT) && !funcOp->hasAttr(DLL_IMPORT) && scalarSignature(funcOp.getFunctionType()); + } + + bool OwnershipSignaturePass::addImportedByAddress(mlir::Value callee, Callees &callees) + { + auto loadOp = callee.getDefiningOp(); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!addressOfOp) + { + return false; + } + + auto globalOp = globals.lookup(addressOfOp.getGlobalName()); + if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + { + return false; + } + + // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` + auto ®ion = globalOp.getInitializerRegion(); + if (!region.hasOneBlock()) + { + return false; + } + + auto resultOp = mlir::dyn_cast(region.front().getTerminator()); + auto castOp = resultOp && resultOp->getNumOperands() == 1 + ? resultOp->getOperand(0).getDefiningOp() + : mlir_ts::CastOp(); + auto searchOp = castOp ? castOp.getIn().getDefiningOp() + : mlir_ts::SearchForAddressOfSymbolOp(); + auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); + auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); + if (!name || !importedNoDrops.contains(name.getValue())) + { + return false; + } + + callees.imported = true; + callees.importedName = name.getValue(); + return true; + } + + OwnershipSignaturePass::Callees OwnershipSignaturePass::resolve(mlir::Operation *op) + { + Callees callees; + if (auto callee = directCallee(op)) + { + callees.named.push_back(callee.getAttr()); + callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); + } + else if (mlir::isa(op)) + { + resolveValue(calleeValue(op), callees); + } + + if (!callees.known) + { + callees.funcs.clear(); + } + + return callees; + } + + void OwnershipSignaturePass::resolveValue(mlir::Value callee, Callees &callees) + { + // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function + // keeps; a method of a class imported so is bound to its object first + auto function = callee; + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + auto loadOp = function.getDefiningOp(); + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) + { + callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); + } + + if (addImportedByAddress(callee, callees)) + { + callees.known = true; + return; + } + + if (auto getMethodOp = callee.getDefiningOp()) + { + callee = getMethodOp.getBoundFunc(); + } + + auto *def = callee.getDefiningOp(); + mlir::StringAttr identifier; + int64_t index = -1; + if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (isInstanceOfSlot(def)) + { + callees.known = true; + callees.instanceOf = true; + return; + } + else if (auto symbol = fieldFunctions.of(def)) + { + callees.known = addDefined(symbol, callees); + callees.fieldFunction = callees.known; + return; + } + else if (auto interfaceRefOp = mlir::dyn_cast_or_null(def)) + { + resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); + return; + } + else if (auto loadOp = mlir::dyn_cast_or_null(def)) + { + resolveCallback(loadOp, callees); + return; + } + else if (auto boundOp = mlir::dyn_cast_or_null(def)) + { + // a function-typed field of an interface, called: `ts.CreateBoundFunction( + // ts.ExtractInterfaceThis(i), ts.Load(ts.InterfaceSymbolRef(i, f)))` + auto loadOp = boundOp.getFunc().getDefiningOp(); + auto refOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::InterfaceSymbolRefOp(); + auto thisOp = boundOp.getThisVal().getDefiningOp(); + if (refOp && isInterfaceField(refOp.getResult()) && thisOp && thisOp.getInterfaceVal() == refOp.getInterfaceVal()) + { + resolveInterfaceSlot(refOp, interfaceFieldFamilies, callees); + } + + return; + } + + if (!identifier) + { + return; + } + + if (index < 0) + { + callees.named.push_back(identifier); + callees.known = addDefined(identifier, callees) || addImported(identifier, callees); + return; + } + + callees.named = familyOf(index, identifier); + resolveFamily(index, identifier, callees); + } + + void OwnershipSignaturePass::resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) + { + callees.known = true; + for (auto member : familyOf(index, identifier)) + { + callees.known = addDefined(member, callees) && callees.known; + } + + callees.known = callees.known && llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp funcOp) { return !callableFromOutside(funcOp); }); + } + + bool OwnershipSignaturePass::isInstanceOfSlot(mlir::Operation *def) + { + auto castOp = mlir::dyn_cast_or_null(def); + auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); + if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) + { + return false; + } + + // a class vtable, read out of an object's first word, or an interface's own vtable, + // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) + auto *vtable = slot.getVtable().getDefiningOp(); + if (auto classVTable = mlir::dyn_cast_or_null(vtable)) + { + return classVTable.getIndex() == 0; + } + + return mlir::isa_and_nonnull(vtable); + } + +} // namespace own_signature From a71b9530c291424ffe0d9681366079d751605f8e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:17:04 +0100 Subject: [PATCH 11/25] own passes: OwnershipSignatureClosedWorld.cpp out of OwnershipSignaturePass.h (I1) The closed world - which functions another module can call, which escape as values, and the families that are open. A pure move of callableFromOutside, holdsFunction, cppClassOfMethod, addressType, findEscapingFunctions, collectClassStorages, onlyCalled, findOpen, openFamily. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipSignatureClosedWorld.cpp | 452 ++++++++++++++++++ .../lib/TypeScript/OwnershipSignaturePass.h | 441 +---------------- 3 files changed, 462 insertions(+), 432 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 71a6f82cd..efb3b888a 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -34,6 +34,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipVerifierPass.cpp OwnershipInferencePass.cpp OwnershipSignaturePass.cpp + OwnershipSignatureClosedWorld.cpp OwnershipSignatureGlobals.cpp OwnershipSignatureInterfaces.cpp OwnershipSignatureResolve.cpp diff --git a/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp new file mode 100644 index 000000000..749d60f0d --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp @@ -0,0 +1,452 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): the closed world - which functions another +// module can call, which escape as values, and the families that are open. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::callableFromOutside(mlir_ts::FuncOp funcOp) + { + if (!funcOp.isPrivate() && (funcOp.isDeclaration() || funcOp->hasAttr("export") || funcOp->hasAttr(DLL_EXPORT))) + { + return true; + } + + // its class: the longest own class name that prefixes its symbol + auto symbol = funcOp.getSymName(); + llvm::StringRef owner; + for (auto &entry : ownClasses) + { + auto name = entry.getKey(); + if (name.size() > owner.size() && symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.') + { + owner = name; + } + } + + if (owner.empty()) + { + return !funcOp.isPrivate(); + } + + auto nameable = [&](llvm::StringRef name) { + auto last = name.rsplit('.').second; + return nameableClasses.contains(name) || exportWords.contains(name) || (!last.empty() && exportWords.contains(last)); + }; + + if (!funcOp.isPrivate() && nameable(owner)) + { + return true; + } + + // An override of a method of a nameable class, or of one another module defines (no own + // vtable here), fills a slot of that class's vtable: another module calls it through the + // base (review6r1 p5, p6, p7; controller ruling: any base of that kind on the chain). The + // base is the first field of a derived class's storage. + auto storage = classStorages.lookup(owner); + if (!storage) + { + // its storage is not known here: no telling what it extends + return !funcOp.isPrivate(); + } + + for (auto depth = 0; depth < 64; ++depth) + { + auto fields = storage.getFields(); + auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); + if (!base) + { + return false; + } + + auto name = base.getName().getValue(); + if (nameable(name) || !ownClasses.contains(name)) + { + return true; + } + + storage = base; + } + + return true; + } + + bool OwnershipSignaturePass::holdsFunction(mlir::Type type, bool fields, SeenTypes &seen) + { + if (!type || !seen.insert({type, fields}).second) + { + return false; + } + + if (mlir::isa(type)) + { + return true; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + auto found = interfaceVTableTypes.find(interfaceType.getName().getValue()); + if (found == interfaceVTableTypes.end()) + { + return true; + } + + return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { + return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), + [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); + }); + } + + auto any = [&](llvm::ArrayRef infos) { + return llvm::any_of(infos, [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, fields, seen); }); + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + return any(tupleType.getFields()); + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + return any(constTupleType.getFields()); + } + + if (auto classStorageType = mlir::dyn_cast(type)) + { + return fields && any(classStorageType.getFields()); + } + + if (auto objectStorageType = mlir::dyn_cast(type)) + { + return fields && any(objectStorageType.getFields()); + } + + if (mlir::isa(type) && !fields) + { + return false; + } + + auto found = false; + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { found = found || holdsFunction(sub, fields, seen); }); + return found; + } + + mlir_ts::ClassStorageType OwnershipSignaturePass::cppClassOfMethod(mlir_ts::FuncOp funcOp) + { + auto inputs = funcOp.getFunctionType().getInputs(); + auto classType = inputs.empty() ? mlir_ts::ClassType() : mlir::dyn_cast(inputs.front()); + auto storage = classType ? mlir::dyn_cast(classType.getStorageType()) : mlir_ts::ClassStorageType(); + if (!storage || !storage.isCpp()) + { + return {}; + } + + auto name = storage.getName().getValue(); + auto symbol = funcOp.getSymName(); + return symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.' ? storage : mlir_ts::ClassStorageType(); + } + + bool OwnershipSignaturePass::addressType(mlir::Type type) + { + return mlir::isa(type); + } + + void OwnershipSignaturePass::findEscapingFunctions(mlir::ModuleOp module) + { + // every body-less function: C's, another module's, a `declare class`'s (its methods, its + // `..instanceOf`, its `..new`), a `@cpp` class's slot + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() && !scalarSignature(funcOp.getFunctionType())) + { + functionsEscape = true; + return; + } + + // a method of a `@cpp` class: C++ calls it through the class's C++ vtable + if (cppClassOfMethod(funcOp)) + { + functionsEscape = true; + return; + } + } + + // every global declared with no initializer or imported: a `declare var`, a `declare + // class`'s vtable (functions) and type name (a string) + for (auto &entry : globals) + { + auto globalOp = entry.second; + auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || + globalOp.getInitializerRegion().front().empty(); + if (foreign && !scalarType(globalOp.getType())) + { + functionsEscape = true; + return; + } + } + + // every `@cpp` class's fields; a base (the first field, a storage) is a `@cpp` class of its + // own, judged on its own + for (auto &entry : classStorages) + { + auto storage = entry.second; + if (!storage.isCpp()) + { + continue; + } + + for (auto [index, field] : llvm::enumerate(storage.getFields())) + { + auto base = index == 0 ? mlir::dyn_cast(field.type) : mlir_ts::ClassStorageType(); + if (base ? !base.isCpp() : !scalarType(field.type)) + { + functionsEscape = true; + return; + } + } + } + + module.walk([&](mlir::Operation *op) { + if (functionsEscape) + { + return; + } + + // a function found in a library at run time: scalar through every cast of its address + if (mlir::isa(op)) + { + functionsEscape = !llvm::all_of(op->getResult(0).getUsers(), [](mlir::Operation *user) { + auto castOp = mlir::dyn_cast(user); + auto functionType = castOp ? mlir::dyn_cast(castOp.getType()) : mlir_ts::FunctionType(); + return functionType && scalarSignature(functionType); + }); + return; + } + + // `inline_asm(...)` and `call_intrinsic(...)`: a function given to either is data + // of no type (review6r5 h1, h2: `inline_asm("mov $1, $0", "=r,r", onSig)`) + if (mlir::isa(op)) + { + functionsEscape = llvm::any_of(op->getOperands(), [&](mlir::Value operand) { + SeenTypes seen; + return holdsFunction(operand.getType(), /*fields=*/true, seen); + }); + return; + } + + auto castOp = mlir::dyn_cast(op); + if (!castOp) + { + return; + } + + // a vtable's address (a class's, an interface's, a thunk's cell) seen as an opaque + // pointer or an integer: the compiler's own, inside a vtable or storing an object's + // vtable, never a value a program gives C + auto vtableOp = op->getParentOfType(); + auto addressOfOp = addressOfThroughCasts(castOp.getIn()); + auto isVTable = [](llvm::StringRef name) { return name.ends_with("..vtbl") || name.contains("..vtbl."); }; + if ((vtableOp && isVTable(vtableOp.getSymName())) || (addressOfOp && isVTable(addressOfOp.getGlobalName()))) + { + return; + } + + // an opaque pointer or a reference made out of a number: an address of foreign memory + if (mlir::isa(castOp.getType()) && !addressType(castOp.getIn().getType())) + { + functionsEscape = true; + return; + } + + // a function seen as something that holds none; a boolean cannot carry one. A class or + // object takes its fields along, seen as anything (`any`, an opaque pointer, an integer, + // a reference: `>k`, then a load of the field, review6r5 g2) but a class + // (its base) or an interface (a view of it, read through its vtable only) + if (mlir::isa(castOp.getType())) + { + return; + } + + auto fields = !mlir::isa(castOp.getType()); + SeenTypes seenIn; + SeenTypes seenOut; + functionsEscape = holdsFunction(castOp.getIn().getType(), fields, seenIn) && + !holdsFunction(castOp.getType(), /*fields=*/true, seenOut); + }); + } + + void OwnershipSignaturePass::collectClassStorages(mlir::ModuleOp module) + { + llvm::DenseSet seen; + std::function visit = [&](mlir::Type type) { + if (!type || !seen.insert(type).second) + { + return; + } + + if (auto storage = mlir::dyn_cast(type)) + { + classStorages[storage.getName().getValue()] = storage; + for (auto &field : storage.getFields()) + { + visit(field.type); + } + + return; + } + + type.walkImmediateSubElements([](mlir::Attribute) {}, visit); + }; + + for (auto &entry : functions) + { + visit(entry.second.getFunctionType()); + } + + module.walk([&](mlir::Operation *op) { + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + visit(result.getType()); + } + } + }); + } + + bool OwnershipSignaturePass::onlyCalled(mlir::Value value) + { + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) && + calleeValue(user) == use.get() && use.getOperandNumber() == 0) + { + continue; + } + + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && onlyCalled(user->getResult(0))) + { + continue; + } + + return false; + } + + return true; + } + + void OwnershipSignaturePass::findOpen(mlir::ModuleOp module) + { + for (auto &entry : functions) + { + if (callableFromOutside(entry.second)) + { + open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR + "' would say it in its signature"); + } + } + + // the module is a symbol table, which getSymbolUses does not look inside: walk its body + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + // an op this pass cannot read symbols through: nothing is closed + for (auto &entry : functions) + { + open.try_emplace(entry.second, "its callers cannot all be found"); + } + + return; + } + + for (auto &use : *uses) + { + auto symbol = use.getSymbolRef().getRootReference(); + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp) + { + continue; + } + + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && isClassVTableEntry(user)) + { + continue; + } + + // passed only to a parameter whose family is closed: its calls are resolved (spec 33) + if (callbackPassing.contains(user)) + { + continue; + } + + int64_t index = -1; + if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + + auto inFunction = !!user->getParentOfType(); + if (inFunction && mlir::isa(user) && + onlyCalled(user->getResult(0))) + { + // a virtual call this pass cannot resolve reaches its family without the facts + if (index >= 0) + { + Callees callees; + resolveFamily(index, symbol, callees); + if (!callees.known) + { + openFamily(index, symbol, "an override may be defined in another module"); + } + } + + continue; + } + + open.try_emplace(funcOp, "it is used other than by a call"); + if (index >= 0) + { + openFamily(index, symbol, "it is used other than by a call"); + } + } + } + + void OwnershipSignaturePass::openFamily(int64_t index, mlir::StringAttr symbol, const char *why) + { + for (auto member : familyOf(index, symbol)) + { + if (auto memberOp = functions.lookup(member.getValue())) + { + open.try_emplace(memberOp, why); + } + } + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index ee847b40f..d3e113ffa 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -971,71 +971,7 @@ class OwnershipSignaturePass : public mlir::PassWrapperhasAttr("export") || funcOp->hasAttr(DLL_EXPORT))) - { - return true; - } - - // its class: the longest own class name that prefixes its symbol - auto symbol = funcOp.getSymName(); - llvm::StringRef owner; - for (auto &entry : ownClasses) - { - auto name = entry.getKey(); - if (name.size() > owner.size() && symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.') - { - owner = name; - } - } - - if (owner.empty()) - { - return !funcOp.isPrivate(); - } - - auto nameable = [&](llvm::StringRef name) { - auto last = name.rsplit('.').second; - return nameableClasses.contains(name) || exportWords.contains(name) || (!last.empty() && exportWords.contains(last)); - }; - - if (!funcOp.isPrivate() && nameable(owner)) - { - return true; - } - - // An override of a method of a nameable class, or of one another module defines (no own - // vtable here), fills a slot of that class's vtable: another module calls it through the - // base (review6r1 p5, p6, p7; controller ruling: any base of that kind on the chain). The - // base is the first field of a derived class's storage. - auto storage = classStorages.lookup(owner); - if (!storage) - { - // its storage is not known here: no telling what it extends - return !funcOp.isPrivate(); - } - - for (auto depth = 0; depth < 64; ++depth) - { - auto fields = storage.getFields(); - auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); - if (!base) - { - return false; - } - - auto name = base.getName().getValue(); - if (nameable(name) || !ownClasses.contains(name)) - { - return true; - } - - storage = base; - } - - return true; - } + bool callableFromOutside(mlir_ts::FuncOp funcOp); // Can foreign code learn of a function of this module, to call it back during a call of a // scalar declaration (scalarDeclaration)? It can only through the module's foreign surface, or @@ -1067,262 +1003,22 @@ class OwnershipSignaturePass : public mlir::PassWrapper(type)) - { - return true; - } - - if (auto interfaceType = mlir::dyn_cast(type)) - { - auto found = interfaceVTableTypes.find(interfaceType.getName().getValue()); - if (found == interfaceVTableTypes.end()) - { - return true; - } - - return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { - return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), - [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); - }); - } - - auto any = [&](llvm::ArrayRef infos) { - return llvm::any_of(infos, [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, fields, seen); }); - }; - - if (auto tupleType = mlir::dyn_cast(type)) - { - return any(tupleType.getFields()); - } - - if (auto constTupleType = mlir::dyn_cast(type)) - { - return any(constTupleType.getFields()); - } - - if (auto classStorageType = mlir::dyn_cast(type)) - { - return fields && any(classStorageType.getFields()); - } - - if (auto objectStorageType = mlir::dyn_cast(type)) - { - return fields && any(objectStorageType.getFields()); - } - - if (mlir::isa(type) && !fields) - { - return false; - } - - auto found = false; - type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { found = found || holdsFunction(sub, fields, seen); }); - return found; - } + bool holdsFunction(mlir::Type type, bool fields, SeenTypes &seen); // The `@cpp` class storage a method's `this` is of, if any: the storage of its first // parameter's class, when the method's symbol is `Class.name`. - mlir_ts::ClassStorageType cppClassOfMethod(mlir_ts::FuncOp funcOp) - { - auto inputs = funcOp.getFunctionType().getInputs(); - auto classType = inputs.empty() ? mlir_ts::ClassType() : mlir::dyn_cast(inputs.front()); - auto storage = classType ? mlir::dyn_cast(classType.getStorageType()) : mlir_ts::ClassStorageType(); - if (!storage || !storage.isCpp()) - { - return {}; - } - - auto name = storage.getName().getValue(); - auto symbol = funcOp.getSymName(); - return symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.' ? storage : mlir_ts::ClassStorageType(); - } + mlir_ts::ClassStorageType cppClassOfMethod(mlir_ts::FuncOp funcOp); // Does a value of this type carry an address of memory this module made - so a cast of it to // an opaque pointer or a reference points into this module, not into foreign memory? - static bool addressType(mlir::Type type) - { - return mlir::isa(type); - } + static bool addressType(mlir::Type type); - void findEscapingFunctions(mlir::ModuleOp module) - { - // every body-less function: C's, another module's, a `declare class`'s (its methods, its - // `..instanceOf`, its `..new`), a `@cpp` class's slot - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() && !scalarSignature(funcOp.getFunctionType())) - { - functionsEscape = true; - return; - } - - // a method of a `@cpp` class: C++ calls it through the class's C++ vtable - if (cppClassOfMethod(funcOp)) - { - functionsEscape = true; - return; - } - } - - // every global declared with no initializer or imported: a `declare var`, a `declare - // class`'s vtable (functions) and type name (a string) - for (auto &entry : globals) - { - auto globalOp = entry.second; - auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || - globalOp.getInitializerRegion().front().empty(); - if (foreign && !scalarType(globalOp.getType())) - { - functionsEscape = true; - return; - } - } - - // every `@cpp` class's fields; a base (the first field, a storage) is a `@cpp` class of its - // own, judged on its own - for (auto &entry : classStorages) - { - auto storage = entry.second; - if (!storage.isCpp()) - { - continue; - } - - for (auto [index, field] : llvm::enumerate(storage.getFields())) - { - auto base = index == 0 ? mlir::dyn_cast(field.type) : mlir_ts::ClassStorageType(); - if (base ? !base.isCpp() : !scalarType(field.type)) - { - functionsEscape = true; - return; - } - } - } - - module.walk([&](mlir::Operation *op) { - if (functionsEscape) - { - return; - } - - // a function found in a library at run time: scalar through every cast of its address - if (mlir::isa(op)) - { - functionsEscape = !llvm::all_of(op->getResult(0).getUsers(), [](mlir::Operation *user) { - auto castOp = mlir::dyn_cast(user); - auto functionType = castOp ? mlir::dyn_cast(castOp.getType()) : mlir_ts::FunctionType(); - return functionType && scalarSignature(functionType); - }); - return; - } - - // `inline_asm(...)` and `call_intrinsic(...)`: a function given to either is data - // of no type (review6r5 h1, h2: `inline_asm("mov $1, $0", "=r,r", onSig)`) - if (mlir::isa(op)) - { - functionsEscape = llvm::any_of(op->getOperands(), [&](mlir::Value operand) { - SeenTypes seen; - return holdsFunction(operand.getType(), /*fields=*/true, seen); - }); - return; - } - - auto castOp = mlir::dyn_cast(op); - if (!castOp) - { - return; - } - - // a vtable's address (a class's, an interface's, a thunk's cell) seen as an opaque - // pointer or an integer: the compiler's own, inside a vtable or storing an object's - // vtable, never a value a program gives C - auto vtableOp = op->getParentOfType(); - auto addressOfOp = addressOfThroughCasts(castOp.getIn()); - auto isVTable = [](llvm::StringRef name) { return name.ends_with("..vtbl") || name.contains("..vtbl."); }; - if ((vtableOp && isVTable(vtableOp.getSymName())) || (addressOfOp && isVTable(addressOfOp.getGlobalName()))) - { - return; - } - - // an opaque pointer or a reference made out of a number: an address of foreign memory - if (mlir::isa(castOp.getType()) && !addressType(castOp.getIn().getType())) - { - functionsEscape = true; - return; - } - - // a function seen as something that holds none; a boolean cannot carry one. A class or - // object takes its fields along, seen as anything (`any`, an opaque pointer, an integer, - // a reference: `>k`, then a load of the field, review6r5 g2) but a class - // (its base) or an interface (a view of it, read through its vtable only) - if (mlir::isa(castOp.getType())) - { - return; - } - - auto fields = !mlir::isa(castOp.getType()); - SeenTypes seenIn; - SeenTypes seenOut; - functionsEscape = holdsFunction(castOp.getIn().getType(), fields, seenIn) && - !holdsFunction(castOp.getType(), /*fields=*/true, seenOut); - }); - } + void findEscapingFunctions(mlir::ModuleOp module); // class name -> its storage, from every class type a function's signature or a global mentions llvm::StringMap classStorages; - void collectClassStorages(mlir::ModuleOp module) - { - llvm::DenseSet seen; - std::function visit = [&](mlir::Type type) { - if (!type || !seen.insert(type).second) - { - return; - } - - if (auto storage = mlir::dyn_cast(type)) - { - classStorages[storage.getName().getValue()] = storage; - for (auto &field : storage.getFields()) - { - visit(field.type); - } - - return; - } - - type.walkImmediateSubElements([](mlir::Attribute) {}, visit); - }; - - for (auto &entry : functions) - { - visit(entry.second.getFunctionType()); - } - - module.walk([&](mlir::Operation *op) { - for (auto result : op->getResults()) - { - if (mlir::isa(result.getType())) - { - visit(result.getType()); - } - } - }); - } + void collectClassStorages(mlir::ModuleOp module); // A class another module can see may be extended there, and its override is a candidate this // module never sees: every member has to be private and defined here. @@ -1337,134 +1033,15 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user) && - calleeValue(user) == use.get() && use.getOperandNumber() == 0) - { - continue; - } - - if (mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) && onlyCalled(user->getResult(0))) - { - continue; - } - - return false; - } - - return true; - } + static bool onlyCalled(mlir::Value value); // The closed world. A function is open when a caller this pass cannot see may reach it: it is // not private (another module may call it), or its symbol is used other than by a direct call, // a class vtable's entry, or a method reference used only as a callee. A virtual reference // that escapes opens its whole family. - void findOpen(mlir::ModuleOp module) - { - for (auto &entry : functions) - { - if (callableFromOutside(entry.second)) - { - open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR - "' would say it in its signature"); - } - } - - // the module is a symbol table, which getSymbolUses does not look inside: walk its body - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - // an op this pass cannot read symbols through: nothing is closed - for (auto &entry : functions) - { - open.try_emplace(entry.second, "its callers cannot all be found"); - } - - return; - } + void findOpen(mlir::ModuleOp module); - for (auto &use : *uses) - { - auto symbol = use.getSymbolRef().getRootReference(); - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp) - { - continue; - } - - auto *user = use.getUser(); - if (mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) && isClassVTableEntry(user)) - { - continue; - } - - // passed only to a parameter whose family is closed: its calls are resolved (spec 33) - if (callbackPassing.contains(user)) - { - continue; - } - - int64_t index = -1; - if (auto refOp = mlir::dyn_cast(user)) - { - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast(user)) - { - index = refOp.getIndex(); - } - - auto inFunction = !!user->getParentOfType(); - if (inFunction && mlir::isa(user) && - onlyCalled(user->getResult(0))) - { - // a virtual call this pass cannot resolve reaches its family without the facts - if (index >= 0) - { - Callees callees; - resolveFamily(index, symbol, callees); - if (!callees.known) - { - openFamily(index, symbol, "an override may be defined in another module"); - } - } - - continue; - } - - open.try_emplace(funcOp, "it is used other than by a call"); - if (index >= 0) - { - openFamily(index, symbol, "it is used other than by a call"); - } - } - } - - void openFamily(int64_t index, mlir::StringAttr symbol, const char *why) - { - for (auto member : familyOf(index, symbol)) - { - if (auto memberOp = functions.lookup(member.getValue())) - { - open.try_emplace(memberOp, why); - } - } - } + void openFamily(int64_t index, mlir::StringAttr symbol, const char *why); // ---- Callbacks (spec 33) ---- // From f2da2ca76b64913dd624ce8f87b820762b0902f1 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:17:56 +0100 Subject: [PATCH 12/25] own passes: OwnershipSignatureGenerators.cpp out of OwnershipSignaturePass.h (I1) Generators - the iterator protocol, the fields a generator yields, and states that borrow their arguments (phase 7b). A pure move of isIteratorNext, findImportedStates, makesGeneratorState, storageTupleOfType, isRecordFieldStore, findYieldedFields, findBoundedForwarders, fieldIndex, boxStoredIntoState, calledOnlyDirectly, isParameterCell, readsParameterSlot, readsParameterField, findBorrowingStates, readsBorrowed. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipSignatureGenerators.cpp | 749 ++++++++++++++++++ .../lib/TypeScript/OwnershipSignaturePass.h | 738 +---------------- 3 files changed, 765 insertions(+), 723 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index efb3b888a..a1bedb092 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -35,6 +35,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferencePass.cpp OwnershipSignaturePass.cpp OwnershipSignatureClosedWorld.cpp + OwnershipSignatureGenerators.cpp OwnershipSignatureGlobals.cpp OwnershipSignatureInterfaces.cpp OwnershipSignatureResolve.cpp diff --git a/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp b/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp new file mode 100644 index 000000000..231728223 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp @@ -0,0 +1,749 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): generators - the iterator protocol, the +// fields a generator yields, and states that borrow their arguments (phase 7b). + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::isIteratorNext(mlir::Operation *op) + { + if (op->getNumResults() != 1) + { + return false; + } + + auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); + if (!tupleType || tupleType.getFields().size() != 2) + { + return false; + } + + auto fieldName = [&](int index) { + auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); + return id ? id.getValue() : llvm::StringRef(); + }; + if (fieldName(0) != "value" || fieldName(1) != "done") + { + return false; + } + + auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; + if (auto callee = directCallee(op)) + { + return isNext(callee.getValue()); + } + + auto function = calleeValue(op); + if (!function) + { + return false; + } + + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + if (auto loadOp = function.getDefiningOp()) + { + if (auto refOp = loadOp.getReference().getDefiningOp()) + { + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + return identifier && isNext(identifier.getValue()); + } + + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + { + auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); + if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) + { + return false; + } + + auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); + return id && isNext(id.getValue()); + } + + return false; + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + return false; + } + + void OwnershipSignaturePass::findImportedStates(mlir::ModuleOp module) + { + llvm::MapVector states; + for (auto &call : calls) + { + if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) + { + continue; + } + + auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); + auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); + states[tupleType] = states.lookup(tupleType) || borrows; + } + + if (states.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + entries.append(existing.begin(), existing.end()); + } + + for (auto &[type, borrows] : states) + { + auto tupleType = mlir::cast(type); + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + // the box first, when it borrows; -1 names no field, so a box that owns is released + llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) + { + fields.push_back(static_cast(index)); + } + } + + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + bool OwnershipSignaturePass::makesGeneratorState(mlir::Operation *op) + { + if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) + { + return false; + } + + auto tupleType = storageTupleOfType(op->getResult(0).getType()); + if (!tupleType) + { + return false; + } + + auto hasStep = false; + auto hasNext = false; + for (auto field : tupleType.getFields()) + { + if (auto id = mlir::dyn_cast_or_null(field.id)) + { + hasStep = hasStep || id.getValue() == ".step"; + hasNext = hasNext || id.getValue() == "next"; + } + } + + return hasStep && hasNext; + } + + mlir_ts::TupleType OwnershipSignaturePass::storageTupleOfType(mlir::Type type) + { + if (auto objectType = mlir::dyn_cast(type)) + { + if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) + { + return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); + } + + // an object a declaration names by its shape (another module's generator) + return mlir::dyn_cast(objectType.getStorageType()); + } + + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + type = valueRefType.getElementType(); + } + + return mlir::dyn_cast(type); + } + + bool OwnershipSignaturePass::isRecordFieldStore(mlir::Operation *user, mlir::Value value) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == value + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); + } + + void OwnershipSignaturePass::findYieldedFields(mlir::ModuleOp module) + { + // (state type, field) -> its reads and its stores + struct FieldUses + { + llvm::SmallVector reads; + llvm::SmallVector stores; + bool other = false; + }; + + llvm::MapVector, FieldUses> fields; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); + auto position = static_cast(propertyRefOp.getPosition()); + if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || + !mlir::isa(propertyRefOp.getType()) || + !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) + { + return; + } + + auto &uses = fields[{tupleType, position}]; + for (auto *user : propertyRefOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + uses.reads.push_back(loadOp); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) + { + uses.stores.push_back(storeOp); + } + else if (!mlir::isa(user)) + { + uses.other = true; + } + } + }); + + // a field the signature pass already found borrowing is the caller's, not the generator's + llvm::DenseSet> borrowing; + if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + for (auto entry : entries.getAsRange()) + { + for (auto index : mlir::cast(entry[1]).asArrayRef()) + { + borrowing.insert({mlir::cast(entry[0]).getValue(), index}); + } + } + } + + llvm::MapVector> yielded; + for (auto &[key, uses] : fields) + { + if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) + { + continue; + } + + // read only to be put into a yield's record + auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { + auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { + return !mlir::isa(user); + }); + return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); + }); + + // each value stored is yielded later in its block: nothing between stops the generator + auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { + return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { + return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); + }); + }); + + if (!onlyYielded || !eachYielded) + { + continue; + } + + yielded[key.first].push_back(key.second); + for (auto loadOp : uses.reads) + { + loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + } + + if (yielded.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, indices] : yielded) + { + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); + } + + module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + void OwnershipSignaturePass::findBoundedForwarders() + { + llvm::DenseMap calleesOf; + for (auto &call : calls) + { + calleesOf[call.op] = &call.callees; + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) + { + continue; + } + + llvm::SmallVector found; + auto forwards = true; + for (auto value : returnedValues(funcOp)) + { + if (holdsNoBlock(value)) + { + continue; + } + + auto *call = rootOf(value).getDefiningOp(); + auto known = call ? calleesOf.find(call) : calleesOf.end(); + if (known == calleesOf.end()) + { + forwards = false; + break; + } + + llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); + if (bounded.empty() && known->second->funcs.size() == 1 && !known->second->viaCallback) + { + auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); + bounded.assign(made.begin(), made.end()); + } + + if (bounded.empty()) + { + forwards = false; + break; + } + + auto args = callArgs(call); + for (auto index : bounded) + { + auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; + if (param < 0) + { + forwards = false; + break; + } + + if (!llvm::is_contained(found, param)) + { + found.push_back(param); + } + } + } + + if (forwards && !found.empty()) + { + llvm::sort(found); + borrowsFound[funcOp] = found; + } + } + } + + int32_t OwnershipSignaturePass::fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name) + { + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (auto id = mlir::dyn_cast_or_null(field.id); id && id.getValue() == name) + { + return static_cast(index); + } + } + + return -1; + } + + mlir::Value OwnershipSignaturePass::boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured) + { + mlir_ts::StoreOp fill; + for (auto *user : newOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) + { + fill = storeOp; + } + } + + auto loadOp = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); + auto stateOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp) + { + return {}; + } + + mlir::Value box; + for (auto *user : stateOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp || propertyRefOp.getPosition() != captured) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || box) + { + return {}; + } + + box = storeOp.getValue(); + } + } + + auto boxOp = box ? box.getDefiningOp() : mlir_ts::VariableOp(); + return boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); + } + + bool OwnershipSignaturePass::calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo) + { + if (!funcOp.isPrivate() && !publicToo) + { + return false; + } + + auto uses = mlir::SymbolTable::getSymbolUses(funcOp.getSymNameAttr(), &module.getBodyRegion()); + if (!uses) + { + return false; + } + + for (auto &use : *uses) + { + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + // a method whose signature gives its callers the bound: called through its class's + // vtable or a reference to it, which the facts reach too (declaredByCallees) + if (publicToo && + (isClassVTableEntry(user) || + (mlir::isa(user) && + onlyCalled(user->getResult(0))))) + { + continue; + } + + auto symbolRefOp = mlir::dyn_cast(user); + if (!symbolRefOp) + { + return false; + } + + for (auto *refUser : symbolRefOp->getUsers()) + { + auto boundOp = mlir::dyn_cast(refUser); + if (!boundOp || !llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { + return mlir::isa(boundUser); + })) + { + return false; + } + } + } + + return true; + } + + bool OwnershipSignaturePass::isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) + { + auto argument = cellOp.getInitializer() ? mlir::dyn_cast(cellOp.getInitializer()) + : mlir::BlockArgument(); + if (!cellOp.getCaptured().value_or(false) || isOwningVariable(cellOp) || !argument || + !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + + bool OwnershipSignaturePass::readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + int position = -1; + if (!varOp || !isParameterSlot(varOp, position) || varOp->getParentOfType() != maker) + { + return false; + } + + index = static_cast(position); + return true; + } + + bool OwnershipSignaturePass::readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp->getOperand(0)) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + + void OwnershipSignaturePass::findBorrowingStates(mlir::ModuleOp module) + { + llvm::MapVector states; + llvm::DenseMap made; + module.walk([&](mlir_ts::NewOp newOp) { + auto valueRefType = mlir::dyn_cast(newOp.getType()); + auto tupleType = valueRefType ? mlir::dyn_cast(valueRefType.getElementType()) + : mlir_ts::TupleType(); + if (!tupleType) + { + return; + } + + ++made[tupleType]; + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + auto maker = newOp->getParentOfType(); + // an open maker has callers this pass cannot give the bound to, unless its signature + // gives it to them (`@borrows`) + if (captured < 0 || !maker || + !calledOnlyDirectly(module, maker, /*publicToo=*/maker->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME))) + { + return; + } + + auto box = boxStoredIntoState(newOp, captured); + if (!box) + { + return; + } + + BorrowingState state{maker, captured}; + for (auto *user : box.getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp) + { + return; + } + + auto value = storeOp.getValue(); + int32_t index = -1; + if (auto cellOp = value.getDefiningOp(); cellOp && isParameterCell(cellOp, maker, index)) + { + auto cellType = mlir::cast(cellOp.getType()); + // a handle parameter's cell holds a count of its own (spec 23.2), which only + // the box's release gives back: a box holding one owns what it holds + if (MLIRTypeHelper::isSharedHandleType(cellType.getElementType())) + { + return; + } + + if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) + { + state.bounded.push_back(index); + } + + continue; + } + + // a copy of what a parameter holds in a field: `.map`'s maker is a closure body + // that copies its own box's fields into the generator's + if (!ownsHeap(value.getLoc(), value.getType())) + { + continue; + } + + // `this`, which the box holds by value (`.this`), not in a cell: read out of + // the parameter's slot + if (readsParameterSlot(value, maker, index)) + { + if (MLIRTypeHelper::isSharedHandleType(value.getType())) + { + return; + } + + if (!llvm::is_contained(state.bounded, index)) + { + state.bounded.push_back(index); + } + + continue; + } + + if (!readsParameterField(value, maker, index)) + { + return; // a box with anything but what the parameters hold owns what it holds + } + + if (!llvm::is_contained(state.bounded, index)) + { + state.bounded.push_back(index); + } + } + } + + if (!state.bounded.empty()) + { + states[tupleType] = state; + } + }); + + // made in one place only: the maker is the one function a bound can come from + states.remove_if([&](auto &entry) { return made[entry.first] != 1; }); + for (auto &[type, state] : states) + { + auto bounded = state.bounded; + llvm::sort(bounded); + borrowsFound[state.maker] = bounded; + } + if (states.empty()) + { + return; + } + + // Each store into a field of a state type, by field: what the field may be given. + llvm::DenseMap, llvm::SmallVector> stored; + module.walk([&](mlir_ts::StoreOp storeOp) { + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); + if (tupleType && states.count(tupleType)) + { + stored[{tupleType, propertyRefOp.getPosition()}].push_back(storeOp.getValue()); + } + }); + + for (auto &[type, state] : states) + { + auto tupleType = mlir::cast(type); + // least fixpoint: a field borrows once everything stored into it is a read of a borrowing + // cell or field + for (auto changed = true; changed;) + { + changed = false; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + auto position = static_cast(index); + auto found = stored.find({type, position}); + if (position == state.captured || state.fields.contains(position) || found == stored.end() || + !ownsHeap(state.maker.getLoc(), field.type)) + { + continue; + } + + if (llvm::all_of(found->second, [&](mlir::Value value) { return readsBorrowed(value, tupleType, state); })) + { + state.fields.insert(position); + changed = true; + } + } + } + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, state] : states) + { + llvm::SmallVector fields{state.captured}; + fields.append(state.fields.begin(), state.fields.end()); + entries.push_back(mlir::ArrayAttr::get( + context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded)); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + bool OwnershipSignaturePass::readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state) + { + auto loadOp = rootOf(value).getDefiningOp(); + if (!loadOp) + { + return false; + } + + auto isStateField = [&](mlir::Value ref, int32_t &position) { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp || storageTupleOf(propertyRefOp->getOperand(0).getType()) != tupleType) + { + return false; + } + + position = propertyRefOp.getPosition(); + return true; + }; + + int32_t position = -1; + if (isStateField(loadOp.getReference(), position)) + { + return state.fields.contains(position); + } + + // a field of the box the state holds, copied in by value, or a cell read out of it + auto isBoxField = [&](mlir::Value ref) { + auto fieldRef = ref.getDefiningOp(); + auto boxLoad = fieldRef ? fieldRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; + }; + + if (isBoxField(loadOp.getReference())) + { + return true; + } + + auto cellLoad = loadOp.getReference().getDefiningOp(); + return cellLoad && isBoxField(cellLoad.getReference()); + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index d3e113ffa..239f3d572 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -466,95 +466,7 @@ class OwnershipSignaturePass : public mlir::PassWrappergetNumResults() != 1) - { - return false; - } - - auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); - if (!tupleType || tupleType.getFields().size() != 2) - { - return false; - } - - auto fieldName = [&](int index) { - auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); - return id ? id.getValue() : llvm::StringRef(); - }; - if (fieldName(0) != "value" || fieldName(1) != "done") - { - return false; - } - - auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; - if (auto callee = directCallee(op)) - { - return isNext(callee.getValue()); - } - - auto function = calleeValue(op); - if (!function) - { - return false; - } - - if (auto getMethodOp = function.getDefiningOp()) - { - function = getMethodOp.getBoundFunc(); - } - - if (auto boundOp = function.getDefiningOp()) - { - function = boundOp.getFunc(); - } - - if (auto loadOp = function.getDefiningOp()) - { - if (auto refOp = loadOp.getReference().getDefiningOp()) - { - auto identifier = mlir::dyn_cast(refOp.getIdentifier()); - return identifier && isNext(identifier.getValue()); - } - - if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) - { - auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); - if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) - { - return false; - } - - auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); - return id && isNext(id.getValue()); - } - - return false; - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - return false; - } + static bool isIteratorNext(mlir::Operation *op); // A generator another module made: its state object's type is only its shape here, and what each // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is @@ -562,108 +474,15 @@ class OwnershipSignaturePass : public mlir::PassWrapper states; - for (auto &call : calls) - { - if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) - { - continue; - } - - auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); - auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); - states[tupleType] = states.lookup(tupleType) || borrows; - } - - if (states.empty()) - { - return; - } - - auto *context = &getContext(); - llvm::SmallVector entries; - if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) - { - entries.append(existing.begin(), existing.end()); - } - - for (auto &[type, borrows] : states) - { - auto tupleType = mlir::cast(type); - auto captured = fieldIndex(tupleType, CAPTURED_NAME); - // the box first, when it borrows; -1 names no field, so a box that owns is released - llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) - { - fields.push_back(static_cast(index)); - } - } - - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); - } - - module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } + void findImportedStates(mlir::ModuleOp module); // A call whose result is a generator's state object (an object whose fields are `.step` and // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the // caller owns - in another module too, as the iteration that follows needs. - static bool makesGeneratorState(mlir::Operation *op) - { - if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) - { - return false; - } - - auto tupleType = storageTupleOfType(op->getResult(0).getType()); - if (!tupleType) - { - return false; - } - - auto hasStep = false; - auto hasNext = false; - for (auto field : tupleType.getFields()) - { - if (auto id = mlir::dyn_cast_or_null(field.id)) - { - hasStep = hasStep || id.getValue() == ".step"; - hasNext = hasNext || id.getValue() == "next"; - } - } - - return hasStep && hasNext; - } + static bool makesGeneratorState(mlir::Operation *op); // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple - static mlir_ts::TupleType storageTupleOfType(mlir::Type type) - { - if (auto objectType = mlir::dyn_cast(type)) - { - if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) - { - return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); - } - - // an object a declaration names by its shape (another module's generator) - return mlir::dyn_cast(objectType.getStorageType()); - } - - if (auto refType = mlir::dyn_cast(type)) - { - type = refType.getElementType(); - } - else if (auto valueRefType = mlir::dyn_cast(type)) - { - type = valueRefType.getElementType(); - } - - return mlir::dyn_cast(type); - } + static mlir_ts::TupleType storageTupleOfType(mlir::Type type); // ---- Fields a generator yields ---- // @@ -676,191 +495,14 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user); - auto propertyRefOp = storeOp && storeOp.getValue() == value - ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); - } - - void findYieldedFields(mlir::ModuleOp module) - { - // (state type, field) -> its reads and its stores - struct FieldUses - { - llvm::SmallVector reads; - llvm::SmallVector stores; - bool other = false; - }; - - llvm::MapVector, FieldUses> fields; - module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { - auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); - auto position = static_cast(propertyRefOp.getPosition()); - if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || - !mlir::isa(propertyRefOp.getType()) || - !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) - { - return; - } - - auto &uses = fields[{tupleType, position}]; - for (auto *user : propertyRefOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - uses.reads.push_back(loadOp); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) - { - uses.stores.push_back(storeOp); - } - else if (!mlir::isa(user)) - { - uses.other = true; - } - } - }); - - // a field the signature pass already found borrowing is the caller's, not the generator's - llvm::DenseSet> borrowing; - if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) - { - for (auto entry : entries.getAsRange()) - { - for (auto index : mlir::cast(entry[1]).asArrayRef()) - { - borrowing.insert({mlir::cast(entry[0]).getValue(), index}); - } - } - } - - llvm::MapVector> yielded; - for (auto &[key, uses] : fields) - { - if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) - { - continue; - } - - // read only to be put into a yield's record - auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { - auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { - return !mlir::isa(user); - }); - return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); - }); - - // each value stored is yielded later in its block: nothing between stops the generator - auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { - return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { - return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); - }); - }); - - if (!onlyYielded || !eachYielded) - { - continue; - } - - yielded[key.first].push_back(key.second); - for (auto loadOp : uses.reads) - { - loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); - } - } + static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value); - if (yielded.empty()) - { - return; - } - - auto *context = &getContext(); - llvm::SmallVector entries; - for (auto &[type, indices] : yielded) - { - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); - } - - module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } + void findYieldedFields(mlir::ModuleOp module); // A function that says `@borrows` and makes no state itself, but returns what a call that has // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters // the call's bounded arguments are, or are read out of. - void findBoundedForwarders() - { - llvm::DenseMap calleesOf; - for (auto &call : calls) - { - calleesOf[call.op] = &call.callees; - } - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) - { - continue; - } - - llvm::SmallVector found; - auto forwards = true; - for (auto value : returnedValues(funcOp)) - { - if (holdsNoBlock(value)) - { - continue; - } - - auto *call = rootOf(value).getDefiningOp(); - auto known = call ? calleesOf.find(call) : calleesOf.end(); - if (known == calleesOf.end()) - { - forwards = false; - break; - } - - llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); - if (bounded.empty() && known->second->funcs.size() == 1 && !known->second->viaCallback) - { - auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); - bounded.assign(made.begin(), made.end()); - } - - if (bounded.empty()) - { - forwards = false; - break; - } - - auto args = callArgs(call); - for (auto index : bounded) - { - auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; - if (param < 0) - { - forwards = false; - break; - } - - if (!llvm::is_contained(found, param)) - { - found.push_back(param); - } - } - } - - if (forwards && !found.empty()) - { - llvm::sort(found); - borrowsFound[funcOp] = found; - } - } - } + void findBoundedForwarders(); // A body that does other than its signature says, and an override that says other than the // method it overrides: either way a caller that trusts the signature frees twice or leaks. @@ -1904,386 +1546,36 @@ class OwnershipSignaturePass : public mlir::PassWrapper fields; }; - static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name) - { - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - if (auto id = mlir::dyn_cast_or_null(field.id); id && id.getValue() == name) - { - return static_cast(index); - } - } - - return -1; - } + static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name); // The box the maker stores into the `.captured` field of the initial state it copies into // `newOp`'s block: `ts.Store(box, ts.PropertyRef(state, captured))`, `state` a local whose only // read fills the block. - static mlir::Value boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured) - { - mlir_ts::StoreOp fill; - for (auto *user : newOp->getUsers()) - { - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) - { - fill = storeOp; - } - } - - auto loadOp = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); - auto stateOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!stateOp) - { - return {}; - } - - mlir::Value box; - for (auto *user : stateOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp || propertyRefOp.getPosition() != captured) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || box) - { - return {}; - } - - box = storeOp.getValue(); - } - } - - auto boxOp = box ? box.getDefiningOp() : mlir_ts::VariableOp(); - return boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); - } + static mlir::Value boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured); // Is every caller of `funcOp` a direct call this pass sees? It is private, and each use of its // symbol is a call, or a closure over it that is only retained and released - `.map(f)` builds // one and calls its body straight through the box. - bool calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo = false) - { - if (!funcOp.isPrivate() && !publicToo) - { - return false; - } - - auto uses = mlir::SymbolTable::getSymbolUses(funcOp.getSymNameAttr(), &module.getBodyRegion()); - if (!uses) - { - return false; - } - - for (auto &use : *uses) - { - auto *user = use.getUser(); - if (mlir::isa(user)) - { - continue; - } - - // a method whose signature gives its callers the bound: called through its class's - // vtable or a reference to it, which the facts reach too (declaredByCallees) - if (publicToo && - (isClassVTableEntry(user) || - (mlir::isa(user) && - onlyCalled(user->getResult(0))))) - { - continue; - } - - auto symbolRefOp = mlir::dyn_cast(user); - if (!symbolRefOp) - { - return false; - } - - for (auto *refUser : symbolRefOp->getUsers()) - { - auto boundOp = mlir::dyn_cast(refUser); - if (!boundOp || !llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { - return mlir::isa(boundUser); - })) - { - return false; - } - } - } - - return true; - } + bool calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo = false); // A parameter's cell: a captured local declared from an argument of `maker`'s entry block, that // owns nothing (its value is the caller's). - static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) - { - auto argument = cellOp.getInitializer() ? mlir::dyn_cast(cellOp.getInitializer()) - : mlir::BlockArgument(); - if (!cellOp.getCaptured().value_or(false) || isOwningVariable(cellOp) || !argument || - !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) - { - return false; - } - - index = static_cast(argument.getArgNumber()); - return true; - } + static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index); // `ts.Load` of one of `maker`'s parameter slots (isParameterSlot): `index` is its position. - static bool readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - int position = -1; - if (!varOp || !isParameterSlot(varOp, position) || varOp->getParentOfType() != maker) - { - return false; - } - - index = static_cast(position); - return true; - } + static bool readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index); // `ts.Load(ts.PropertyRef(argument, i))` of one of `maker`'s parameters: `index` is its position. - static bool readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp->getOperand(0)) : mlir::BlockArgument(); - if (!argument || !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) - { - return false; - } - - index = static_cast(argument.getArgNumber()); - return true; - } + static bool readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index); bool ownsHeap(mlir::Location location, mlir::Type type); - void findBorrowingStates(mlir::ModuleOp module) - { - llvm::MapVector states; - llvm::DenseMap made; - module.walk([&](mlir_ts::NewOp newOp) { - auto valueRefType = mlir::dyn_cast(newOp.getType()); - auto tupleType = valueRefType ? mlir::dyn_cast(valueRefType.getElementType()) - : mlir_ts::TupleType(); - if (!tupleType) - { - return; - } - - ++made[tupleType]; - auto captured = fieldIndex(tupleType, CAPTURED_NAME); - auto maker = newOp->getParentOfType(); - // an open maker has callers this pass cannot give the bound to, unless its signature - // gives it to them (`@borrows`) - if (captured < 0 || !maker || - !calledOnlyDirectly(module, maker, /*publicToo=*/maker->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME))) - { - return; - } - - auto box = boxStoredIntoState(newOp, captured); - if (!box) - { - return; - } - - BorrowingState state{maker, captured}; - for (auto *user : box.getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp) - { - return; - } - - auto value = storeOp.getValue(); - int32_t index = -1; - if (auto cellOp = value.getDefiningOp(); cellOp && isParameterCell(cellOp, maker, index)) - { - auto cellType = mlir::cast(cellOp.getType()); - // a handle parameter's cell holds a count of its own (spec 23.2), which only - // the box's release gives back: a box holding one owns what it holds - if (MLIRTypeHelper::isSharedHandleType(cellType.getElementType())) - { - return; - } - - if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) - { - state.bounded.push_back(index); - } - - continue; - } - - // a copy of what a parameter holds in a field: `.map`'s maker is a closure body - // that copies its own box's fields into the generator's - if (!ownsHeap(value.getLoc(), value.getType())) - { - continue; - } - - // `this`, which the box holds by value (`.this`), not in a cell: read out of - // the parameter's slot - if (readsParameterSlot(value, maker, index)) - { - if (MLIRTypeHelper::isSharedHandleType(value.getType())) - { - return; - } - - if (!llvm::is_contained(state.bounded, index)) - { - state.bounded.push_back(index); - } - - continue; - } - - if (!readsParameterField(value, maker, index)) - { - return; // a box with anything but what the parameters hold owns what it holds - } - - if (!llvm::is_contained(state.bounded, index)) - { - state.bounded.push_back(index); - } - } - } - - if (!state.bounded.empty()) - { - states[tupleType] = state; - } - }); - - // made in one place only: the maker is the one function a bound can come from - states.remove_if([&](auto &entry) { return made[entry.first] != 1; }); - for (auto &[type, state] : states) - { - auto bounded = state.bounded; - llvm::sort(bounded); - borrowsFound[state.maker] = bounded; - } - if (states.empty()) - { - return; - } - - // Each store into a field of a state type, by field: what the field may be given. - llvm::DenseMap, llvm::SmallVector> stored; - module.walk([&](mlir_ts::StoreOp storeOp) { - auto propertyRefOp = storeOp.getReference().getDefiningOp(); - auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); - if (tupleType && states.count(tupleType)) - { - stored[{tupleType, propertyRefOp.getPosition()}].push_back(storeOp.getValue()); - } - }); - - for (auto &[type, state] : states) - { - auto tupleType = mlir::cast(type); - // least fixpoint: a field borrows once everything stored into it is a read of a borrowing - // cell or field - for (auto changed = true; changed;) - { - changed = false; - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - auto position = static_cast(index); - auto found = stored.find({type, position}); - if (position == state.captured || state.fields.contains(position) || found == stored.end() || - !ownsHeap(state.maker.getLoc(), field.type)) - { - continue; - } - - if (llvm::all_of(found->second, [&](mlir::Value value) { return readsBorrowed(value, tupleType, state); })) - { - state.fields.insert(position); - changed = true; - } - } - } - } - - auto *context = &getContext(); - llvm::SmallVector entries; - for (auto &[type, state] : states) - { - llvm::SmallVector fields{state.captured}; - fields.append(state.fields.begin(), state.fields.end()); - entries.push_back(mlir::ArrayAttr::get( - context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); - state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded)); - } - - module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } + void findBorrowingStates(mlir::ModuleOp module); // Is `value` a read of what a state of `tupleType` borrows: the value in one of its box's cells // (`ts.Load` of `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`), or a // borrowing field's (`ts.Load(ts.PropertyRef(state, p))`)? - bool readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state) - { - auto loadOp = rootOf(value).getDefiningOp(); - if (!loadOp) - { - return false; - } - - auto isStateField = [&](mlir::Value ref, int32_t &position) { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp || storageTupleOf(propertyRefOp->getOperand(0).getType()) != tupleType) - { - return false; - } - - position = propertyRefOp.getPosition(); - return true; - }; - - int32_t position = -1; - if (isStateField(loadOp.getReference(), position)) - { - return state.fields.contains(position); - } - - // a field of the box the state holds, copied in by value, or a cell read out of it - auto isBoxField = [&](mlir::Value ref) { - auto fieldRef = ref.getDefiningOp(); - auto boxLoad = fieldRef ? fieldRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); - return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; - }; - - if (isBoxField(loadOp.getReference())) - { - return true; - } - - auto cellLoad = loadOp.getReference().getDefiningOp(); - return cellLoad && isBoxField(cellLoad.getReference()); - } + bool readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state); // ---- Globals given a borrow ---- From 7bfebf6714cb05daffea839564af6dac3ce9d956 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:18:33 +0100 Subject: [PATCH 13/25] own passes: OwnershipSignatureCallbacks.cpp out of OwnershipSignaturePass.h (I1) Callbacks (spec 33) - the functions passed for a parameter, and the facts their families agree on. A pure move of isFunctionValueType, onlyCalledParam, passedFunction, passedOnly, collectCallbackFamilies, resolveCallback, shiftFacts, canReleaseParam, meetCallbacks. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipSignatureCallbacks.cpp | 423 ++++++++++++++++++ .../lib/TypeScript/OwnershipSignaturePass.h | 412 +---------------- 3 files changed, 433 insertions(+), 403 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index a1bedb092..24c38f712 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -34,6 +34,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipVerifierPass.cpp OwnershipInferencePass.cpp OwnershipSignaturePass.cpp + OwnershipSignatureCallbacks.cpp OwnershipSignatureClosedWorld.cpp OwnershipSignatureGenerators.cpp OwnershipSignatureGlobals.cpp diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp new file mode 100644 index 000000000..39fb5e4a6 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -0,0 +1,423 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): callbacks (spec 33) - the functions passed +// for a parameter, and the facts their families agree on. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::isFunctionValueType(mlir::Type type) + { + return mlir::isa(type); + } + + bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument) + { + for (auto *user : argument.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index)) + { + return false; + } + + for (auto *slotUser : varOp->getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + auto loadOp = mlir::dyn_cast(slotUser); + if (!loadOp) + { + return false; + } + + for (auto &use : loadOp->getUses()) + { + auto *call = use.getOwner(); + if (!isCall(call) || calleeValue(call) != use.get() || + llvm::is_contained(callArgs(call), use.get())) + { + return false; + } + } + } + } + + return true; + } + + mlir_ts::SymbolRefOp OwnershipSignaturePass::passedFunction(mlir::Value value, int32_t &shift) + { + while (auto castOp = value.getDefiningOp()) + { + if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) + { + return {}; + } + + value = castOp.getIn(); + } + + if (auto refOp = value.getDefiningOp()) + { + shift = 0; + return refOp; + } + + auto boundOp = value.getDefiningOp(); + if (!boundOp || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return {}; + } + + shift = 1; + return boundOp.getFunc().getDefiningOp(); + } + + bool OwnershipSignaturePass::passedOnly(mlir::Value value, llvm::function_ref passes, int depth) + { + if (depth > 8) + { + return false; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || passes(use)) + { + continue; + } + + auto castOp = mlir::dyn_cast(user); + auto boundOp = mlir::dyn_cast(user); + if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) + { + continue; + } + + if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && + passedOnly(boundOp.getResult(), passes, depth + 1)) + { + continue; + } + + return false; + } + + return true; + } + + void OwnershipSignaturePass::collectCallbackFamilies(mlir::ModuleOp module) + { + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + return; + } + + llvm::DenseMap> usersOf; + for (auto &use : *uses) + { + if (auto funcOp = functions.lookup(use.getSymbolRef().getRootReference().getValue())) + { + usersOf[funcOp].push_back(use.getUser()); + } + } + + auto callsOnly = [&](mlir_ts::FuncOp funcOp) { + auto &users = usersOf[funcOp]; + return !users.empty() && llvm::all_of(users, [&](mlir::Operation *user) { + auto callee = directCallee(user); + return callee && callee.getValue() == funcOp.getSymName(); + }); + }; + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || callableFromOutside(funcOp) || declaresFacts(funcOp) || !callsOnly(funcOp)) + { + continue; + } + + for (auto argument : funcOp.getBody().front().getArguments()) + { + if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) + { + continue; + } + + auto index = static_cast(argument.getArgNumber()); + CallbackFamily family; + for (auto *user : usersOf[funcOp]) + { + auto args = callArgs(user); + auto shift = 0; + auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) + : mlir_ts::SymbolRefOp(); + auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); + if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) + { + family.closed = false; + break; + } + + auto symbol = member.getSymNameAttr(); + auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); + if (same != family.members.end() && same->shift != shift) + { + family.closed = false; + break; + } + + if (same == family.members.end()) + { + family.members.push_back({symbol, shift}); + } + + family.passing.push_back(refOp); + } + + callbackFamilies[{funcOp, index}] = std::move(family); + } + } + + // A member's symbol used anywhere else - a call through another value, a field, a vtable, a + // global - reaches callers this cannot see; so does one passed to a family that is not closed. + // Each family that has such a member is open, until nothing changes. + auto passesToClosed = [&](mlir::OpOperand &use) { + auto *call = use.getOwner(); + auto callee = directCallee(call); + auto calleeOp = callee ? functions.lookup(callee.getValue()) : mlir_ts::FuncOp(); + if (!calleeOp) + { + return false; + } + + // the argument this use is, by its operand's position + auto args = callArgs(call); + if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || + use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + { + return false; + } + + auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); + auto found = callbackFamilies.find({calleeOp, index}); + return found != callbackFamilies.end() && found->second.closed; + }; + + for (auto changed = true; changed;) + { + changed = false; + for (auto &[key, family] : callbackFamilies) + { + if (!family.closed) + { + continue; + } + + for (auto &member : family.members) + { + auto memberOp = functions.lookup(member.symbol.getValue()); + auto escapes = llvm::any_of(usersOf[memberOp], [&](mlir::Operation *user) { + if (auto callee = directCallee(user); callee && callee.getValue() == member.symbol.getValue()) + { + return false; + } + + auto refOp = mlir::dyn_cast(user); + return !refOp || !user->getParentOfType() || + !passedOnly(refOp.getResult(), passesToClosed); + }); + + if (escapes) + { + family.closed = false; + changed = true; + break; + } + } + } + } + + for (auto &[key, family] : callbackFamilies) + { + if (family.closed) + { + callbackPassing.insert(family.passing.begin(), family.passing.end()); + } + } + } + + void OwnershipSignaturePass::resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) + { + auto varOp = loadOp.getReference().getDefiningOp(); + auto funcOp = loadOp->getParentOfType(); + auto index = -1; + if (!varOp || !funcOp || !isParameterSlot(varOp, index)) + { + return; + } + + auto found = callbackFamilies.find({funcOp.getOperation(), index}); + if (found == callbackFamilies.end() || !found->second.closed || found->second.members.empty()) + { + return; + } + + callees.viaCallback = true; + callees.known = true; + for (auto &member : found->second.members) + { + callees.named.push_back(member.symbol); + callees.known = addDefined(member.symbol, callees) && callees.known; + callees.shifts.push_back(member.shift); + } + + callees.known = callees.known && callees.shifts.size() == callees.funcs.size(); + } + + bool OwnershipSignaturePass::shiftFacts(const Facts &facts, int32_t shift, Facts &shifted) + { + shifted = Facts(); + shifted.record = facts.record; + for (auto [from, to] : {std::make_pair(&facts.owned, &shifted.owned), std::make_pair(&facts.borrows, &shifted.borrows)}) + { + for (auto index : *from) + { + if (index < shift) + { + return false; + } + + to->push_back(index - shift); + } + } + + return true; + } + + bool OwnershipSignaturePass::canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index) + { + if (funcOp.getBody().empty() || static_cast(index) >= funcOp.getNumArguments()) + { + return false; + } + + auto argument = funcOp.getBody().front().getArgument(index); + if (isHandle(argument) || isCopyableString(argument.getType()) || holdsNoBlock(argument)) + { + return false; + } + + return llvm::any_of(argument.getUsers(), [&](mlir::Operation *user) { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + return varOp && isParameterSlot(varOp, position) && position == index; + }); + } + + bool OwnershipSignaturePass::meetCallbacks(llvm::DenseSet &demoted) + { + auto changed = false; + for (auto &[key, family] : callbackFamilies) + { + if (!family.closed) + { + continue; + } + + llvm::SmallVector members; + llvm::SmallVector seen; + auto shiftable = true; + for (auto &member : family.members) + { + auto funcOp = functions.lookup(member.symbol.getValue()); + Facts shifted; + shiftable = shiftable && funcOp && shiftFacts(factsEffective.lookup(funcOp), member.shift, shifted); + members.push_back(funcOp); + seen.push_back(shifted); + } + + if (shiftable && llvm::all_equal(seen)) + { + continue; + } + + // a parameter some member keeps, and each other one only borrows: kept by all + llvm::SmallVector kept; + for (auto &facts : seen) + { + kept.append(facts.owned.begin(), facts.owned.end()); + } + + llvm::sort(kept); + kept.erase(std::unique(kept.begin(), kept.end()), kept.end()); + auto promotable = shiftable && llvm::all_of(seen, [&](const Facts &facts) { + return facts.borrows == seen.front().borrows && facts.record == seen.front().record && + llvm::none_of(kept, [&](int32_t index) { return llvm::is_contained(facts.borrows, index); }); + }); + + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + // a member another caller reaches without the facts is given none (findOpen) + for (auto index : kept) + { + promotable = promotable && !demoted.contains(funcOp) && !open.contains(funcOp) && + !interfaceReached.contains(funcOp) && + (llvm::is_contained(seen[position].owned, index) || + canReleaseParam(funcOp, index + member.shift)); + } + } + + if (promotable) + { + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + auto facts = factsEffective.lookup(funcOp); + for (auto index : kept) + { + auto param = index + member.shift; + if (!llvm::is_contained(facts.owned, param)) + { + facts.owned.insert(llvm::upper_bound(facts.owned, param), param); + } + } + + if (facts != factsEffective.lookup(funcOp)) + { + factsEffective[funcOp] = facts; + changed = true; + } + } + + continue; + } + + for (auto funcOp : members) + { + if (funcOp && !declaresCallerFacts(funcOp)) + { + demoted.insert(funcOp); + if (factsEffective.erase(funcOp)) + { + changed = true; + } + } + } + } + + return changed; + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 239f3d572..2bbc84406 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -724,431 +724,37 @@ class OwnershipSignaturePass : public mlir::PassWrapper callbackPassing; - static bool isFunctionValueType(mlir::Type type) - { - return mlir::isa(type); - } + static bool isFunctionValueType(mlir::Type type); // Is every use of this parameter - through its slot, never assigned or captured - the function // a call goes through? - static bool onlyCalledParam(mlir::BlockArgument argument) - { - for (auto *user : argument.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) - { - return false; - } - - for (auto *slotUser : varOp->getUsers()) - { - if (mlir::isa(slotUser)) - { - continue; - } - - auto loadOp = mlir::dyn_cast(slotUser); - if (!loadOp) - { - return false; - } - - for (auto &use : loadOp->getUses()) - { - auto *call = use.getOwner(); - if (!isCall(call) || calleeValue(call) != use.get() || - llvm::is_contained(callArgs(call), use.get())) - { - return false; - } - } - } - } - - return true; - } + static bool onlyCalledParam(mlir::BlockArgument argument); // The function a caller passes as `value`: its symbol, seen as a function value through casts // between function types, or a closure over a capture box built of it. Null for anything else. - static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift) - { - while (auto castOp = value.getDefiningOp()) - { - if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) - { - return {}; - } - - value = castOp.getIn(); - } - - if (auto refOp = value.getDefiningOp()) - { - shift = 0; - return refOp; - } - - auto boundOp = value.getDefiningOp(); - if (!boundOp || closureOfBox(boundOp.getThisVal()) != boundOp) - { - return {}; - } - - shift = 1; - return boundOp.getFunc().getDefiningOp(); - } + static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift); // Does the function value `value` go only where `passes` allows - as an argument of a call - and // through casts between function types, a closure built of it, and that closure's own retains // and releases? - static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0) - { - if (depth > 8) - { - return false; - } - - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || passes(use)) - { - continue; - } - - auto castOp = mlir::dyn_cast(user); - auto boundOp = mlir::dyn_cast(user); - if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) - { - continue; - } - - if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && - passedOnly(boundOp.getResult(), passes, depth + 1)) - { - continue; - } - - return false; - } - - return true; - } - - void collectCallbackFamilies(mlir::ModuleOp module) - { - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - return; - } - - llvm::DenseMap> usersOf; - for (auto &use : *uses) - { - if (auto funcOp = functions.lookup(use.getSymbolRef().getRootReference().getValue())) - { - usersOf[funcOp].push_back(use.getUser()); - } - } - - auto callsOnly = [&](mlir_ts::FuncOp funcOp) { - auto &users = usersOf[funcOp]; - return !users.empty() && llvm::all_of(users, [&](mlir::Operation *user) { - auto callee = directCallee(user); - return callee && callee.getValue() == funcOp.getSymName(); - }); - }; - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() || callableFromOutside(funcOp) || declaresFacts(funcOp) || !callsOnly(funcOp)) - { - continue; - } - - for (auto argument : funcOp.getBody().front().getArguments()) - { - if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) - { - continue; - } - - auto index = static_cast(argument.getArgNumber()); - CallbackFamily family; - for (auto *user : usersOf[funcOp]) - { - auto args = callArgs(user); - auto shift = 0; - auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) - : mlir_ts::SymbolRefOp(); - auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); - if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) - { - family.closed = false; - break; - } - - auto symbol = member.getSymNameAttr(); - auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); - if (same != family.members.end() && same->shift != shift) - { - family.closed = false; - break; - } - - if (same == family.members.end()) - { - family.members.push_back({symbol, shift}); - } - - family.passing.push_back(refOp); - } - - callbackFamilies[{funcOp, index}] = std::move(family); - } - } - - // A member's symbol used anywhere else - a call through another value, a field, a vtable, a - // global - reaches callers this cannot see; so does one passed to a family that is not closed. - // Each family that has such a member is open, until nothing changes. - auto passesToClosed = [&](mlir::OpOperand &use) { - auto *call = use.getOwner(); - auto callee = directCallee(call); - auto calleeOp = callee ? functions.lookup(callee.getValue()) : mlir_ts::FuncOp(); - if (!calleeOp) - { - return false; - } + static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0); - // the argument this use is, by its operand's position - auto args = callArgs(call); - if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || - use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) - { - return false; - } - - auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); - auto found = callbackFamilies.find({calleeOp, index}); - return found != callbackFamilies.end() && found->second.closed; - }; - - for (auto changed = true; changed;) - { - changed = false; - for (auto &[key, family] : callbackFamilies) - { - if (!family.closed) - { - continue; - } - - for (auto &member : family.members) - { - auto memberOp = functions.lookup(member.symbol.getValue()); - auto escapes = llvm::any_of(usersOf[memberOp], [&](mlir::Operation *user) { - if (auto callee = directCallee(user); callee && callee.getValue() == member.symbol.getValue()) - { - return false; - } - - auto refOp = mlir::dyn_cast(user); - return !refOp || !user->getParentOfType() || - !passedOnly(refOp.getResult(), passesToClosed); - }); - - if (escapes) - { - family.closed = false; - changed = true; - break; - } - } - } - } - - for (auto &[key, family] : callbackFamilies) - { - if (family.closed) - { - callbackPassing.insert(family.passing.begin(), family.passing.end()); - } - } - } + void collectCallbackFamilies(mlir::ModuleOp module); // A call through a parameter whose family is closed: its members. - void resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) - { - auto varOp = loadOp.getReference().getDefiningOp(); - auto funcOp = loadOp->getParentOfType(); - auto index = -1; - if (!varOp || !funcOp || !isParameterSlot(varOp, index)) - { - return; - } - - auto found = callbackFamilies.find({funcOp.getOperation(), index}); - if (found == callbackFamilies.end() || !found->second.closed || found->second.members.empty()) - { - return; - } - - callees.viaCallback = true; - callees.known = true; - for (auto &member : found->second.members) - { - callees.named.push_back(member.symbol); - callees.known = addDefined(member.symbol, callees) && callees.known; - callees.shifts.push_back(member.shift); - } - - callees.known = callees.known && callees.shifts.size() == callees.funcs.size(); - } + void resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees); // A member's facts as a call through its family sees them: each parameter k as argument // k - shift. False when a fact is about the capture box itself, which the call does not pass. - static bool shiftFacts(const Facts &facts, int32_t shift, Facts &shifted) - { - shifted = Facts(); - shifted.record = facts.record; - for (auto [from, to] : {std::make_pair(&facts.owned, &shifted.owned), std::make_pair(&facts.borrows, &shifted.borrows)}) - { - for (auto index : *from) - { - if (index < shift) - { - return false; - } - - to->push_back(index - shift); - } - } - - return true; - } + static bool shiftFacts(const Facts &facts, int32_t shift, Facts &shifted); // Can the body of `funcOp` be given parameter `index` to keep and release it where it does not // move it: a plain slot (isParameterSlot), a block, not a handle, not a string? - bool canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index) - { - if (funcOp.getBody().empty() || static_cast(index) >= funcOp.getNumArguments()) - { - return false; - } - - auto argument = funcOp.getBody().front().getArgument(index); - if (isHandle(argument) || isCopyableString(argument.getType()) || holdsNoBlock(argument)) - { - return false; - } - - return llvm::any_of(argument.getUsers(), [&](mlir::Operation *user) { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - return varOp && isParameterSlot(varOp, position) && position == index; - }); - } + bool canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index); // The members of each closed callback family agree (the section above), by promotion of a kept // parameter or by demotion. True when any member's facts changed. - bool meetCallbacks(llvm::DenseSet &demoted) - { - auto changed = false; - for (auto &[key, family] : callbackFamilies) - { - if (!family.closed) - { - continue; - } - - llvm::SmallVector members; - llvm::SmallVector seen; - auto shiftable = true; - for (auto &member : family.members) - { - auto funcOp = functions.lookup(member.symbol.getValue()); - Facts shifted; - shiftable = shiftable && funcOp && shiftFacts(factsEffective.lookup(funcOp), member.shift, shifted); - members.push_back(funcOp); - seen.push_back(shifted); - } - - if (shiftable && llvm::all_equal(seen)) - { - continue; - } - - // a parameter some member keeps, and each other one only borrows: kept by all - llvm::SmallVector kept; - for (auto &facts : seen) - { - kept.append(facts.owned.begin(), facts.owned.end()); - } - - llvm::sort(kept); - kept.erase(std::unique(kept.begin(), kept.end()), kept.end()); - auto promotable = shiftable && llvm::all_of(seen, [&](const Facts &facts) { - return facts.borrows == seen.front().borrows && facts.record == seen.front().record && - llvm::none_of(kept, [&](int32_t index) { return llvm::is_contained(facts.borrows, index); }); - }); - - for (auto [position, member] : llvm::enumerate(family.members)) - { - auto funcOp = members[position]; - // a member another caller reaches without the facts is given none (findOpen) - for (auto index : kept) - { - promotable = promotable && !demoted.contains(funcOp) && !open.contains(funcOp) && - !interfaceReached.contains(funcOp) && - (llvm::is_contained(seen[position].owned, index) || - canReleaseParam(funcOp, index + member.shift)); - } - } - - if (promotable) - { - for (auto [position, member] : llvm::enumerate(family.members)) - { - auto funcOp = members[position]; - auto facts = factsEffective.lookup(funcOp); - for (auto index : kept) - { - auto param = index + member.shift; - if (!llvm::is_contained(facts.owned, param)) - { - facts.owned.insert(llvm::upper_bound(facts.owned, param), param); - } - } - - if (facts != factsEffective.lookup(funcOp)) - { - factsEffective[funcOp] = facts; - changed = true; - } - } - - continue; - } - - for (auto funcOp : members) - { - if (funcOp && !declaresCallerFacts(funcOp)) - { - demoted.insert(funcOp); - if (factsEffective.erase(funcOp)) - { - changed = true; - } - } - } - } - - return changed; - } + bool meetCallbacks(llvm::DenseSet &demoted); // ---- Results that borrow an argument ---- From a424ff9a262349ca83788f760330ae67f7b988d1 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:20:04 +0100 Subject: [PATCH 14/25] own passes: OwnershipSignatureRecords.cpp out of OwnershipSignaturePass.h (I1) Borrowing records (spec 32) - the boxes an object literal is made into, and the parameters a record result borrows. A pure move of classifyRecordBoxes, classifyRecordBox, recordParamsOf. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../lib/TypeScript/OwnershipSignaturePass.h | 290 +---------------- .../TypeScript/OwnershipSignatureRecords.cpp | 301 ++++++++++++++++++ 3 files changed, 305 insertions(+), 287 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipSignatureRecords.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 24c38f712..56decb8ad 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -39,6 +39,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipSignatureGenerators.cpp OwnershipSignatureGlobals.cpp OwnershipSignatureInterfaces.cpp + OwnershipSignatureRecords.cpp OwnershipSignatureResolve.cpp OwnedReturnConsumptionPass.cpp StackPromotionPass.cpp diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 2bbc84406..231a21ac1 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -801,191 +801,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper recordBoxes; - void classifyRecordBoxes() - { - recordBoxes.clear(); - getOperation().walk([&](mlir_ts::NewOp newOp) { - newOp->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); - for (auto *user : newOp->getUsers()) - { - for (auto *viewUser : user->getUsers()) - { - if (mlir::isa(viewUser)) - { - viewUser->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); - } - } - } - - classifyRecordBox(newOp); - }); - } - - void classifyRecordBox(mlir_ts::NewOp newOp) - { - auto boxOp = recordBoxOf(newOp.getResult()); - auto varOp = boxOp == newOp ? recordLocalOf(newOp.getResult()) : mlir_ts::VariableOp(); - if (!varOp) - { - return; - } - - // the record read once, and only into its box (rc's retain of it beside) - mlir_ts::LoadOp read; - for (auto *user : varOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - if (read) - { - return; - } - - read = loadOp; - } - } - - for (auto *user : read->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (!mlir::isa(user) && !(storeOp && storeOp.getReference() == newOp.getResult())) - { - return; - } - } - - // the box seen only as interfaces: every release of it reads their tag - llvm::SmallVector interfaces; - for (auto &use : newOp->getUses()) - { - auto *user = use.getOwner(); - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) - { - continue; - } - - auto castOp = mlir::dyn_cast(user); - if (!castOp || !isView(castOp)) - { - return; - } - - for (auto *castUser : castOp->getUsers()) - { - auto newInterfaceOp = mlir::dyn_cast(castUser); - if (!newInterfaceOp || newInterfaceOp.getThisVal() != castOp.getResult()) - { - return; - } - - interfaces.push_back(castUser); - } - } - - if (interfaces.empty()) - { - return; - } - - // each field that holds a block: owned (made here, or nothing) or borrowed, never both - RecordBox info; - llvm::SmallVector borrowed; - auto mth = typeHelper(&getContext()); - for (auto *user : varOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - auto field = propertyRefOp.getResult(); - auto refType = mlir::dyn_cast(field.getType()); - if (!refType || !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) - { - continue; - } - - auto owns = false; - auto borrows = false; - for (auto *fieldUser : field.getUsers()) - { - if (mlir::isa(fieldUser)) - { - continue; - } - - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != field) - { - return; - } - - auto value = storeOp.getValue(); - switch (recordFieldSource(value)) - { - case RecordFieldSource::NoBlock: - continue; - case RecordFieldSource::Fresh: - owns = true; - continue; - case RecordFieldSource::Unknown: - return; - case RecordFieldSource::Immortal: - borrows = true; - continue; - case RecordFieldSource::Param: - case RecordFieldSource::Place: - if (auto param = borrowedParam(value); param >= 0) - { - info.params.push_back(param); - } - else - { - info.returnable = false; - } - - borrows = true; - continue; - case RecordFieldSource::Slot: - case RecordFieldSource::Cell: - info.returnable = false; - borrows = true; - continue; - } - } - - // a string is copied, not borrowed (spec 22) - if ((owns && borrows) || (borrows && isCopyableString(refType.getElementType()))) - { - return; - } - - if (borrows) - { - borrowed.push_back(propertyRefOp.getPosition()); - } - } + void classifyRecordBoxes(); - if (borrowed.empty()) - { - return; - } - - llvm::sort(borrowed); - borrowed.erase(std::unique(borrowed.begin(), borrowed.end()), borrowed.end()); - llvm::sort(info.params); - info.params.erase(std::unique(info.params.begin(), info.params.end()), info.params.end()); - - auto attr = mlir::DenseI32ArrayAttr::get(&getContext(), borrowed); - newOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); - for (auto *newInterfaceOp : interfaces) - { - newInterfaceOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); - } - - recordBoxes[newOp.getOperation()] = std::move(info); - } + void classifyRecordBox(mlir_ts::NewOp newOp); enum class RecordFlow { @@ -997,109 +815,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper ¶ms) - { - llvm::SmallVector work{value}; - llvm::SmallPtrSet seen; - auto record = false; - auto other = false; - while (!work.empty()) - { - // what holds no block, by its own type (a box's `value_ref` owns nothing by type, but - // the object its view shows does) - auto next = work.pop_back_val(); - auto current = rootOf(next); - if (!seen.insert(current).second || holdsNoBlock(next)) - { - continue; - } - - auto *def = current.getDefiningOp(); - if (auto found = def ? recordBoxes.find(def) : recordBoxes.end(); found != recordBoxes.end()) - { - if (!found->second.returnable) - { - return RecordFlow::Bad; - } - - params.append(found->second.params.begin(), found->second.params.end()); - record = true; - continue; - } - - if (def && isCall(def) && isRecordResult(def)) - { - auto args = callArgs(def); - for (auto index : resultBorrowsAll(def)) - { - if (static_cast(index) >= args.size()) - { - return RecordFlow::Bad; - } - - auto arg = args[index]; - if (argumentHoldsNoBlock(arg) || isImmortalGlobalRead(arg)) - { - continue; - } - - auto param = borrowedParam(arg); - if (param < 0) - { - return RecordFlow::Bad; - } - - params.push_back(param); - } - - record = true; - continue; - } - - if (auto argument = mlir::dyn_cast(current)) - { - llvm::SmallVector merged; - if (mergedInto(argument, merged)) - { - work.append(merged.begin(), merged.end()); - continue; - } - - other = true; - continue; - } - - // what an owning local holds: its initializer and every value stored into it - auto loadOp = mlir::dyn_cast_or_null(def); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) - { - if (varOp.getInitializer()) - { - work.push_back(varOp.getInitializer()); - } - - for (auto *user : varOp->getUsers()) - { - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == varOp.getResult()) - { - work.push_back(storeOp.getValue()); - } - } - - continue; - } - - other = true; - } - - if (!record) - { - return RecordFlow::None; - } - - return other ? RecordFlow::Bad : RecordFlow::Record; - } + RecordFlow recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms); // ---- Parameters the callee keeps ---- diff --git a/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp b/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp new file mode 100644 index 000000000..909ea7bb0 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp @@ -0,0 +1,301 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): borrowing records (spec 32) - the boxes an +// object literal is made into, and the parameters a record result borrows. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + void OwnershipSignaturePass::classifyRecordBoxes() + { + recordBoxes.clear(); + getOperation().walk([&](mlir_ts::NewOp newOp) { + newOp->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + for (auto *user : newOp->getUsers()) + { + for (auto *viewUser : user->getUsers()) + { + if (mlir::isa(viewUser)) + { + viewUser->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + } + } + } + + classifyRecordBox(newOp); + }); + } + + void OwnershipSignaturePass::classifyRecordBox(mlir_ts::NewOp newOp) + { + auto boxOp = recordBoxOf(newOp.getResult()); + auto varOp = boxOp == newOp ? recordLocalOf(newOp.getResult()) : mlir_ts::VariableOp(); + if (!varOp) + { + return; + } + + // the record read once, and only into its box (rc's retain of it beside) + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + if (read) + { + return; + } + + read = loadOp; + } + } + + for (auto *user : read->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (!mlir::isa(user) && !(storeOp && storeOp.getReference() == newOp.getResult())) + { + return; + } + } + + // the box seen only as interfaces: every release of it reads their tag + llvm::SmallVector interfaces; + for (auto &use : newOp->getUses()) + { + auto *user = use.getOwner(); + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) + { + continue; + } + + auto castOp = mlir::dyn_cast(user); + if (!castOp || !isView(castOp)) + { + return; + } + + for (auto *castUser : castOp->getUsers()) + { + auto newInterfaceOp = mlir::dyn_cast(castUser); + if (!newInterfaceOp || newInterfaceOp.getThisVal() != castOp.getResult()) + { + return; + } + + interfaces.push_back(castUser); + } + } + + if (interfaces.empty()) + { + return; + } + + // each field that holds a block: owned (made here, or nothing) or borrowed, never both + RecordBox info; + llvm::SmallVector borrowed; + auto mth = typeHelper(&getContext()); + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + auto field = propertyRefOp.getResult(); + auto refType = mlir::dyn_cast(field.getType()); + if (!refType || !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) + { + continue; + } + + auto owns = false; + auto borrows = false; + for (auto *fieldUser : field.getUsers()) + { + if (mlir::isa(fieldUser)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != field) + { + return; + } + + auto value = storeOp.getValue(); + switch (recordFieldSource(value)) + { + case RecordFieldSource::NoBlock: + continue; + case RecordFieldSource::Fresh: + owns = true; + continue; + case RecordFieldSource::Unknown: + return; + case RecordFieldSource::Immortal: + borrows = true; + continue; + case RecordFieldSource::Param: + case RecordFieldSource::Place: + if (auto param = borrowedParam(value); param >= 0) + { + info.params.push_back(param); + } + else + { + info.returnable = false; + } + + borrows = true; + continue; + case RecordFieldSource::Slot: + case RecordFieldSource::Cell: + info.returnable = false; + borrows = true; + continue; + } + } + + // a string is copied, not borrowed (spec 22) + if ((owns && borrows) || (borrows && isCopyableString(refType.getElementType()))) + { + return; + } + + if (borrows) + { + borrowed.push_back(propertyRefOp.getPosition()); + } + } + + if (borrowed.empty()) + { + return; + } + + llvm::sort(borrowed); + borrowed.erase(std::unique(borrowed.begin(), borrowed.end()), borrowed.end()); + llvm::sort(info.params); + info.params.erase(std::unique(info.params.begin(), info.params.end()), info.params.end()); + + auto attr = mlir::DenseI32ArrayAttr::get(&getContext(), borrowed); + newOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + for (auto *newInterfaceOp : interfaces) + { + newInterfaceOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + } + + recordBoxes[newOp.getOperation()] = std::move(info); + } + + OwnershipSignaturePass::RecordFlow OwnershipSignaturePass::recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms) + { + llvm::SmallVector work{value}; + llvm::SmallPtrSet seen; + auto record = false; + auto other = false; + while (!work.empty()) + { + // what holds no block, by its own type (a box's `value_ref` owns nothing by type, but + // the object its view shows does) + auto next = work.pop_back_val(); + auto current = rootOf(next); + if (!seen.insert(current).second || holdsNoBlock(next)) + { + continue; + } + + auto *def = current.getDefiningOp(); + if (auto found = def ? recordBoxes.find(def) : recordBoxes.end(); found != recordBoxes.end()) + { + if (!found->second.returnable) + { + return RecordFlow::Bad; + } + + params.append(found->second.params.begin(), found->second.params.end()); + record = true; + continue; + } + + if (def && isCall(def) && isRecordResult(def)) + { + auto args = callArgs(def); + for (auto index : resultBorrowsAll(def)) + { + if (static_cast(index) >= args.size()) + { + return RecordFlow::Bad; + } + + auto arg = args[index]; + if (argumentHoldsNoBlock(arg) || isImmortalGlobalRead(arg)) + { + continue; + } + + auto param = borrowedParam(arg); + if (param < 0) + { + return RecordFlow::Bad; + } + + params.push_back(param); + } + + record = true; + continue; + } + + if (auto argument = mlir::dyn_cast(current)) + { + llvm::SmallVector merged; + if (mergedInto(argument, merged)) + { + work.append(merged.begin(), merged.end()); + continue; + } + + other = true; + continue; + } + + // what an owning local holds: its initializer and every value stored into it + auto loadOp = mlir::dyn_cast_or_null(def); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) + { + if (varOp.getInitializer()) + { + work.push_back(varOp.getInitializer()); + } + + for (auto *user : varOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == varOp.getResult()) + { + work.push_back(storeOp.getValue()); + } + } + + continue; + } + + other = true; + } + + if (!record) + { + return RecordFlow::None; + } + + return other ? RecordFlow::Bad : RecordFlow::Record; + } + +} // namespace own_signature From 989b08aec9ee3d4b5e33e3c838bfd3ae742953bc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:20:38 +0100 Subject: [PATCH 15/25] own passes: OwnershipInferencePass into OwnershipInferencePass.h (I1) The class moves out of its anonymous namespace into a private header (namespace own_inference), its comments staying with the declarations, as MLIRGenImpl.h did. OwnershipInferencePass.cpp keeps the pass itself - each function analyzed, its retains decided and erased, string copies made. The other methods stay inline in the header until the next commits give each analysis a file of its own. A pure move: checked as line multisets, the only lines not carried over verbatim are the declarations, the qualified definitions and the anonymous namespace. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 6903 +--------------- .../lib/TypeScript/OwnershipInferencePass.h | 6909 +++++++++++++++++ 2 files changed, 6930 insertions(+), 6882 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferencePass.h diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index ec5977112..b08000cd3 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1,72 +1,14 @@ -#include "mlir/Pass/Pass.h" -#include "mlir/IR/Dominance.h" -#include "mlir/Dialect/LLVMIR/LLVMAttrs.h" -#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" -#include "mlir/Interfaces/ControlFlowInterfaces.h" -#include "mlir/Interfaces/FunctionInterfaces.h" +// OwnershipInferencePass (see OwnershipInferencePass.h): the pass itself - each function analyzed, +// its retains decided and erased, string copies made. -#include "TypeScript/TypeScriptDialect.h" -#include "TypeScript/TypeScriptOps.h" -#include "TypeScript/TypeScriptFunctionPass.h" -#include "TypeScript/Passes.h" -#include "TypeScript/Defines.h" -#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" - -#include "OwnershipFacts.h" - -#include "llvm/ADT/MapVector.h" -#include "llvm/ADT/ScopeExit.h" -#include "llvm/ADT/SetVector.h" -#include "llvm/ADT/SmallPtrSet.h" -#include "llvm/ADT/SmallVector.h" -#include "llvm/ADT/STLFunctionalExtras.h" -#include "llvm/ADT/StringSet.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include +#include "OwnershipInferencePass.h" #define DEBUG_TYPE "own" -namespace mlir_ts = mlir::typescript; - -namespace -{ - -using namespace own_facts; - -// This pass's own mark, for its own reading: a store of a value into the capture box of a closure -// that borrows what it captures (phase 5). The store is a read, not a taker. -#define OWN_CAPTURE_BORROW_ATTR_NAME "__own_capture_borrow" - -// Ownership inference for -mm=own, phases 0 and 1. See -// docs/superpowers/specs/2026-09-24-own-memory-model-design.md, sections 4.2, 10.6, 11 and 12. -// -// A fresh heap value may have at most one owner: one use that takes it - a local's -// declaration, a store into a field, element, global or return slot, an insertion into an -// array, a return - or, with none of those, the temporary itself, which rc gives back with a -// `ts.Release`. That owner's release is then its destroy, and every retain rc planted on the -// way is erased. Owners are counted from the uses themselves, not from rc's retains: rc also -// transfers ownership by *consuming* a value (`__owned_consumed`, a push, a field store), -// which leaves no retain behind to count. -// -// A move is decided by reachability on the affine CFG (spec 4.2 step 3): nothing may use the value -// after its taker, a taker that control comes back to without the value being made again is a -// move on every loop iteration (2.6), and each release the move reaches must be dominated by it -// and is erased. A release the move cannot reach stays - the owner on the paths that did not -// move. A local's release that paths which moved it and paths which did not both reach is placed -// instead on the edges where the latter join the former (spec 30). Anything else is an error at -// its location. -// -// It runs on every function: a `ts.Func`, and the `func.func` the async lowering outlines an -// `await`'s continuation or a `for await` body into, whose arguments are the values it captures. -class OwnershipInferencePass - : public mlir::PassWrapper> +namespace own_inference { - public: - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipInferencePass) - void runOnOperation() override + void OwnershipInferencePass::runOnOperation() { if (!getFunction().isExternal()) { @@ -74,12 +16,12 @@ class OwnershipInferencePass } } - mlir::FunctionOpInterface getFunction() + mlir::FunctionOpInterface OwnershipInferencePass::getFunction() { return getOperation(); } - void runOnFunction() + void OwnershipInferencePass::runOnFunction() { // the dry run (spec 22.3): does today's analysis pass as it is? { @@ -109,9 +51,7 @@ class OwnershipInferencePass analyze(toErase); } - // The analysis of one function; true when it passed. In a dry run it erases nothing and strips - // no facts. - bool analyze(llvm::SetVector &toErase) + bool OwnershipInferencePass::analyze(llvm::SetVector &toErase) { auto f = getFunction(); mlir::DominanceInfo dominanceInfo(f); @@ -187,8 +127,7 @@ class OwnershipInferencePass return true; } - // Each analysis starts from nothing this function has not set up again. - void resetFunctionState() + void OwnershipInferencePass::resetFunctionState() { auto f = getFunction(); drops.clear(); @@ -208,35 +147,7 @@ class OwnershipInferencePass recordStoreUses.clear(); } - // What the ownership operations of one function name, in the order the walk meets them - // (collectOwnershipOps). - struct FunctionOps - { - // every value some ownership operation names, in the order the walk meets them - llvm::SetVector candidates; - llvm::SmallVector retains; - // reads out of fields and elements, and results that borrow an argument, and what may - // destroy what they read (spec 2.3, 2.4) - llvm::SmallVector placeReads; - // reads of what a mortal global holds (isGlobalRead) - llvm::SmallVector globalReads; - // calls whose callee keeps an argument: each such argument moves into the call - llvm::SmallVector keepingCalls; - }; - - struct SlotReceiver - { - mlir::Operation *retain; - mlir::Value value; - mlir_ts::LoadOp load; - }; - - // the receivers of each owning local, or of each parameter this function keeps - using Receivers = llvm::MapVector>; - - // Every op of the function that names an ownership: the values to check, the retains to - // decide, the reads out of places and globals, the calls that keep an argument, and the drops. - void collectOwnershipOps(FunctionOps &ops, llvm::SetVector &toErase) + void OwnershipInferencePass::collectOwnershipOps(FunctionOps &ops, llvm::SetVector &toErase) { auto f = getFunction(); // A view of a block is that block: the candidate is always the root. @@ -354,9 +265,7 @@ class OwnershipInferencePass }); } - // The owner check of each value, then of each read out of a place and of a mortal global. The - // fresh values proven, whose retains go. - llvm::DenseSet checkOwnershipOps(const FunctionOps &ops, llvm::SetVector &toErase) + llvm::DenseSet OwnershipInferencePass::checkOwnershipOps(const FunctionOps &ops, llvm::SetVector &toErase) { // Every value gets the owner check, fresh or not: rc can hand a value it did not make to // two consuming declarations without a single retain (the result of an indirect call @@ -385,9 +294,7 @@ class OwnershipInferencePass return proven; } - // Each retain: erased, given to the receivers of an owning local or of a kept parameter, or - // reported. - void decideRetains(const FunctionOps &ops, const llvm::DenseSet &proven, Receivers &slotReceivers, + void OwnershipInferencePass::decideRetains(const FunctionOps &ops, const llvm::DenseSet &proven, Receivers &slotReceivers, Receivers ¶mReceivers, llvm::SetVector &toErase) { // What is proven loses its retains; any other retain is a second reference nobody @@ -506,9 +413,7 @@ class OwnershipInferencePass } } - // The arguments a callee keeps, given to the receivers of the local or kept parameter they come - // from. - void giveKeptArguments(const FunctionOps &ops, Receivers &slotReceivers, Receivers ¶mReceivers) + void OwnershipInferencePass::giveKeptArguments(const FunctionOps &ops, Receivers &slotReceivers, Receivers ¶mReceivers) { // An argument a callee keeps moves into the call (spec 2.4): out of an owning local or a // parameter this function keeps, like any other taker; a fresh value is decided with the @@ -544,8 +449,7 @@ class OwnershipInferencePass } } - // The facts were for this pass only. - void stripFacts() + void OwnershipInferencePass::stripFacts() { returnedParams.clear(); getFunction().walk([](mlir::Operation *op) { @@ -561,30 +465,7 @@ class OwnershipInferencePass }); } - private: - mlir::DominanceInfo *dominance = nullptr; - - // What in this function may destroy a value held by a field or an element: an overwrite of a - // field or an element, an array op that removes elements, a call. - llvm::SmallVector drops; - - // derivedFrom's answer for each value it was asked about in this function; held by pointer, so - // an answer stays put while the next one is added - llvm::DenseMap>> derivedCache; - - // While non-zero, verdicts are tried without reporting: the next verdict gets its turn first. - unsigned quiet = 0; - - // The dry run (spec 22.3): the whole analysis, but nothing reported and nothing changed. A - // report it would make is counted in `failures`. - bool dryRun = false; - unsigned failures = 0; - // attributes the dry run set on ops that did not have them, taken off again after it - llvm::SmallVector> dryRunAttrs; - - // Whether a report is to be emitted: not under `quietly`'s trial, and not in the dry run, which - // only counts it. - bool reporting() + bool OwnershipInferencePass::reporting() { if (quiet) { @@ -600,8 +481,7 @@ class OwnershipInferencePass return true; } - // `op->setAttr(name, unit)`, undone after the dry run when the op did not have it - void setAttrTracked(mlir::Operation *op, llvm::StringRef name) + void OwnershipInferencePass::setAttrTracked(mlir::Operation *op, llvm::StringRef name) { auto attrName = mlir::StringAttr::get(&getContext(), name); if (dryRun && !op->hasAttr(attrName)) @@ -612,9 +492,7 @@ class OwnershipInferencePass op->setAttr(attrName, mlir::UnitAttr::get(&getContext())); } - // Strings as values (spec 22.3): each string retain the dry run did not erase is rewritten, where - // its one use can be found, into a copy for that use. - void makeStringCopies(const llvm::SetVector &erased) + void OwnershipInferencePass::makeStringCopies(const llvm::SetVector &erased) { llvm::SmallVector retains; getFunction()->walk([&](mlir::Operation *op) { @@ -631,20 +509,13 @@ class OwnershipInferencePass } } - // The value a parameter of this function is, when its callers were told this function keeps it - // or returns a borrow of it (spec 15): copying it would leak what the caller gave up. - bool borrowsKnownParam(mlir::Value value) + bool OwnershipInferencePass::borrowsKnownParam(mlir::Value value) { auto param = borrowedParam(value); return param >= 0 && (llvm::is_contained(ownedParams(getFunction()), param) || param == returnsBorrowOf); } - // Rewrites one string retain into a copy for the use it was made for (spec 22.3), and erases it; - // false, leaving it for the real run to report, where there is no single such use in its block, - // or the value is not a string or is a known parameter's (spec 22.4). Its use walk is takerOf's - // without the twinReadAfter fallback: a retain whose value has no use of its own is not copied, - // so its error stays. - bool copyForRetain(mlir::Operation *retain) + bool OwnershipInferencePass::copyForRetain(mlir::Operation *retain) { // `let x = v`: the local starts from a copy if (auto retainSlotOp = mlir::dyn_cast(retain)) @@ -724,6743 +595,11 @@ class OwnershipInferencePass return true; } - // The parameter this function's result borrows (`__own_result_borrows`), or -1; and the - // parameters (or views of them) whose rc reference for the caller was already decided. - int returnsBorrowOf = -1; - llvm::DenseSet returnedParams; - - // The parameters this function's result is a borrowing record of (OWN_RESULT_RECORD_ATTR_NAME, - // spec 32); empty for none. - llvm::SmallVector returnsRecordOf; - - // ---- Closures (spec 2.5, phase 5) ---- - // - // A closure is `ts.CreateBoundFunction(box, @f) {__owns_capture}`; its box holds a cell for each - // variable it captures by reference and a copy of each it captures by value. One that does not - // escape borrows all of it: rc's references for the box go, the box frees itself only - // (OWN_BORROWS_CAPTURES_ATTR_NAME), and nothing that may run the closure may come after anything - // it borrows is given back. One that escapes would own all of it, which is not done yet. - struct Closure - { - mlir_ts::CreateBoundFunctionOp op; - mlir::Value box; - // each store that fills the box, with the field it fills - llvm::SmallVector> fills; - bool hasCells = false; - // the first use that takes the closure out of this function's hands - mlir::Operation *escape = nullptr; - // what may run the closure: calls given it, called through it, or through its box - llvm::SmallVector uses; - // the calls through the box itself, made without the closure value (a folded `const f`) - llvm::SmallVector boxUses; - // where the closure value is given back: its releases, and those of the `let`s holding it - llvm::SmallVector ends; - // rc's references for the box: `ts.RetainCell` of each cell, `ts.Retain` of each copy - llvm::SmallVector retains; - // held by a local that owns nothing (`let f: () => number; f = () => ...`): an alias, so - // the closure's own releases stay its owner - bool aliased = false; - // a generator's box whose state object borrows from the generator's arguments - // (OWN_BORROWING_FIELDS_ATTR_NAME): a parameter's cell moves in, and the box frees the cell only - bool borrowsValues = false; - // copies only, and given to a generator's maker that borrows from it (`.map(f)`): the box - // borrows its copies, as a box with cells would - bool borrowsCopies = false; - }; - - llvm::SmallVector> closures; - // a copy stored into a borrowing box -> its closure - llvm::DenseMap captureStores; - // rc's references for boxes, which the closures decide - llvm::DenseSet captureRetains; - // each closure over a cell, by its `ts.CreateBoundFunction` - llvm::DenseMap closureOf; - // each generator's capture box, which escapes with its state object (classifyStateBoxes) - llvm::DenseMap stateBoxes; - // rc's retains for stores into borrowing fields, and those fields' releases (classifyBorrowingFields) - llvm::DenseSet fieldBorrowRetains; - llvm::SmallVector fieldBorrowReleases; - - // Finds the closures over cells and what they borrow, before anything else: whether a box's - // stores are reads or takers depends on it. A closure with copies only keeps what phase 4 does - // with it - its box owns them, and each copy moves in. - void classifyClosures() - { - getFunction().walk([&](mlir_ts::CreateBoundFunctionOp boundOp) { - if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME) || closureOfBox(boundOp.getThisVal()) != boundOp) - { - return; - } - - auto closure = std::make_shared(); - closure->op = boundOp; - closure->box = boundOp.getThisVal(); - for (auto *user : closure->box.getUsers()) - { - if (user == boundOp.getOperation()) - { - continue; - } - - if (auto propertyRefOp = mlir::dyn_cast(user)) - { - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - closure->escape = closure->escape ? closure->escape : fieldUser; - continue; - } - - closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); - closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); - } - - continue; - } - - if (isCall(user)) - { - closure->uses.push_back(user); - closure->boxUses.push_back(user); - continue; - } - - closure->escape = closure->escape ? closure->escape : user; - } - - if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); - escape && !closure->escape) - { - closure->escape = escape; - } - - // copies only: the box owns them (phase 4); only an alias needs deciding - unless the box - // is given to a generator's maker whose result borrows from it - if (!closure->hasCells) - { - // ...and only when every copy has an owner of its own: the box frees itself alone, so a - // value made for it (a callback that captures, `a.filter((x) => x > n)`) would be - // nobody's - closure->borrowsCopies = - !closure->escape && - llvm::any_of(closure->boxUses, - [](mlir::Operation *use) { return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); }) && - llvm::none_of(closure->fills, [&](auto &fill) { - auto value = fill.second.getValue(); - return !holdsNoBlock(value) && isFresh(rootOf(value)); - }); - if (!closure->borrowsCopies) - { - if (!closure->escape && closure->aliased) - { - closures.push_back(std::move(closure)); - } - - return; - } - } - - for (auto &fill : closure->fills) - { - auto storeOp = fill.second; - auto isCell = mlir::isa(storeOp.getValue().getType()); - // an escaping closure's copies move into its box as in phase 4, rc's retain with them - if (closure->escape && !isCell) - { - continue; - } - - if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - - if (!closure->escape && !isCell) - { - setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); - captureStores[storeOp] = closure.get(); - } - } - - if (!closure->escape) - { - setAttrTracked(boundOp, OWN_BORROWS_CAPTURES_ATTR_NAME); - } - - closureOf[boundOp.getOperation()] = closure.get(); - closures.push_back(std::move(closure)); - }); - } - - // The fields of a generator's state object of this type that borrow from its arguments - // (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first. - llvm::SmallVector borrowingFieldsOf(mlir::Type recordType) - { - llvm::SmallVector fields; - if (auto objectType = mlir::dyn_cast(recordType)) - { - recordType = objectType.getStorageType(); - } - - if (auto storageType = mlir::dyn_cast(recordType)) - { - recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); - } - - auto module = getFunction()->getParentOfType(); - auto entries = module ? module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); - if (!entries) - { - return fields; - } - - for (auto entry : entries.getAsRange()) - { - if (mlir::cast(entry[0]).getValue() == recordType) - { - auto indices = mlir::cast(entry[1]).asArrayRef(); - fields.append(indices.begin(), indices.end()); - } - } - - return fields; - } - - // Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)? - bool isYieldedField(mlir::Value ref) - { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp) - { - return false; - } - - auto recordType = propertyRefOp->getOperand(0).getType(); - if (auto objectType = mlir::dyn_cast(recordType)) - { - recordType = objectType.getStorageType(); - } - - if (auto storageType = mlir::dyn_cast(recordType)) - { - recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); - } - - auto module = getFunction()->getParentOfType(); - auto entries = module ? module->getAttrOfType(OWN_YIELDED_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); - if (!entries) - { - return false; - } - - for (auto entry : entries.getAsRange()) - { - if (mlir::cast(entry[0]).getValue() == recordType && - llvm::is_contained(mlir::cast(entry[1]).asArrayRef(), - static_cast(propertyRefOp.getPosition()))) - { - return true; - } - } - - return false; - } - - // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? - bool isBorrowingField(mlir::Value ref) - { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp) - { - return false; - } - - auto fields = borrowingFieldsOf(propertyRefOp->getOperand(0).getType()); - return fields.size() > 1 && llvm::is_contained(llvm::ArrayRef(fields).drop_front(), - static_cast(propertyRefOp.getPosition())); - } - - // A store into a borrowing field holds what it stores without owning it, as a borrowing box's - // copy does (OWN_CAPTURE_BORROW_ATTR_NAME), and rc's retain for it goes; its release, when the - // field is overwritten, gives nothing back. - void classifyBorrowingFields() - { - getFunction().walk([&](mlir::Operation *op) { - if (auto storeOp = mlir::dyn_cast(op); storeOp && isBorrowingField(storeOp.getReference())) - { - setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); - if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) - { - fieldBorrowRetains.insert(retain); - } - } - else if (auto releaseSlotOp = mlir::dyn_cast(op); - releaseSlotOp && isBorrowingField(releaseSlotOp.getSlot())) - { - fieldBorrowReleases.push_back(op); - } - }); - } - - // Is `ref` a cell read out of the `.captured` box of a generator's state object that borrows: - // `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`? - bool isBorrowingCell(mlir::Value ref) - { - auto cellLoad = ref.getDefiningOp(); - return cellLoad && isBorrowingBoxField(cellLoad.getReference()); - } - - // Is `ref` a field of the `.captured` box of a generator's state object that borrows: - // `ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i)`? A field copied in by value holds - // the caller's value itself, a cell field the caller's cell. - bool isBorrowingBoxField(mlir::Value ref) - { - auto cellRef = ref.getDefiningOp(); - auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); - auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - if (!boxRef) - { - return false; - } - - auto fields = borrowingFieldsOf(boxRef->getOperand(0).getType()); - return !fields.empty() && fields.front() == static_cast(boxRef.getPosition()); - } - - // What a generator reads out of a borrowing cell or field is the caller's: every use of it must - // be a borrow - a store into another borrowing field is one (classifyBorrowingFields). A yield of - // it, a push, a return, a store anywhere else would give the caller's value a second owner, and - // rc retains nothing for some of these, so nothing else would see it. - void checkBorrowedReads() - { - getFunction().walk([&](mlir_ts::LoadOp loadOp) { - auto ref = loadOp.getReference(); - if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingBoxField(ref) || isBorrowingField(ref))) - { - return; - } - - auto reported = false; - forEachUse(loadOp.getResult(), [&](mlir::Operation *user, mlir::Value used) { - if (reported || isBorrow(user, used) || fieldBorrowRetains.contains(user)) - { - return; - } - - reportPlaceBorrowEscapes(user, ownerName(loadOp.getResult()), "the generator's argument"); - reported = true; - }); - }); - } - - // A call whose result borrows from its arguments (OWN_RESULT_BOUNDED_ATTR_NAME): a generator over - // a parameter's block. The result is owned as any fresh value is, but nothing may use it after - // one of those arguments is given back - released, assigned, or dropped from the place it was - // read from - and it may not be kept anywhere that could outlive them. Its releases are not - // uses: the state object's release gives back nothing it borrows. - void checkBoundedResults() - { - getFunction().walk([&](mlir::Operation *call) { - auto bounded = call->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); - if (!bounded || !isCall(call) || call->getNumResults() != 1) - { - return; - } - - auto name = ownerName(call->getResult(0)); - llvm::SmallVector uses; - if (auto *kept = boundedUses(call->getResult(0), uses)) - { - reportPlaceBorrowEscapes(kept, name, "its generator's arguments"); - return; - } - - auto args = callArgs(call); - for (auto index : bounded.asArrayRef()) - { - if (static_cast(index) >= args.size()) - { - continue; - } - - // a closure's box (`.map(f)` calls its maker with it): it lives while the closures on - // it do, and what it holds while their owners do. A reference to storage, so first. - auto root = rootOf(args[index]); - if (auto boxOp = root.getDefiningOp(); boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) - { - if (!checkBoundedByBox(call, root, uses, name)) - { - return; - } - - continue; - } - - if (holdsNoBlock(args[index]) || isImmortalLiteral(args[index])) - { - continue; - } - - if (borrowedParam(root) >= 0) - { - continue; // the caller's caller's: alive for all of this function - } - - Chain chain; - auto *read = placeReadOf(root); - if (read) - { - chain = chainOf(read); - } - else if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - chain.roots.push_back({root, Chain::NotOwned}); - } - - // the root's own ends; and, for a read out of a place, whatever may destroy what the - // place holds (a local or a made value is not destroyed by a call it is given) - auto ends = rootEnds(chain); - for (auto *drop : read ? llvm::ArrayRef(drops) : llvm::ArrayRef()) - { - if (drop != call && dropsChain(drop, chain, read)) - { - ends.push_back(drop); - } - } - - auto owner = ownerName(root); - for (auto *end : ends) - { - if (end == call) - { - continue; - } - - for (auto *use : uses) - { - if (use != end && reachableAfter(end, use, call)) - { - reportBorrowOutlives(use, end, name, owner); - return; - } - } - } - } - }); - } - - // The ends of a capture box given to a call with a bounded result: each release of a closure on - // it (the box goes with its last), and each end of what a field of it was filled from. Reports - // and returns false when a use of the result comes after one. - bool checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, - llvm::StringRef name) - { - llvm::SmallVector ends; - for (auto *user : box.getUsers()) - { - if (auto boundOp = mlir::dyn_cast(user); boundOp && boundOp.getThisVal() == box) - { - llvm::SmallVector closureUses; - auto aliased = false; - if (followClosure(boundOp.getResult(), closureUses, ends, aliased)) - { - reportPlaceBorrowEscapes(call, name, "a closure that escapes"); // the box may go anywhere - return false; - } - - continue; - } - - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult() || holdsNoBlock(storeOp.getValue())) - { - continue; - } - - auto root = rootOf(storeOp.getValue()); - Chain chain; - if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (borrowedParam(root) >= 0 || isImmortalLiteral(root)) - { - continue; - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - reportPlaceBorrowEscapes(call, name, "a value this function cannot bound"); - return false; - } - - auto rootEndsOf = rootEnds(chain); - ends.append(rootEndsOf.begin(), rootEndsOf.end()); - } - } - - for (auto *end : ends) - { - for (auto *use : uses) - { - if (end != call && use != end && reachableAfter(end, use, call)) - { - reportBorrowOutlives(use, end, name, "the generator's source"); - return false; - } - } - } - - return true; - } - - // Every use of a bounded result, through its views, the locals that hold it, and the function - // and object a call through it is made with. Answers the first use that keeps it - anything - // that is not a read, a call, a local, or rc's bookkeeping - or null. - mlir::Operation *boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses) - { - llvm::SmallVector values{result}; - llvm::SmallPtrSet seen; - mlir::Operation *kept = nullptr; - while (!values.empty() && !kept) - { - auto value = values.pop_back_val(); - if (!seen.insert(value).second) - { - continue; - } - - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (kept || mlir::isa(user)) - { - return; - } - - mlir::Value slot; - if (auto varOp = mlir::dyn_cast(user)) - { - slot = varOp.getResult(); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) - { - slot = storeOp.getReference(); - } - - if (slot) - { - auto slotVar = slot.getDefiningOp(); - if (!slotVar || slotVar.getCaptured().value_or(false)) - { - kept = user; - return; - } - - for (auto *slotUser : slot.getUsers()) - { - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - values.push_back(loadOp.getResult()); - } - } - - return; - } - - // returned by a function that says so (`@borrows`): its callers hold the result to - // the same bound, and the signature pass checked the arguments are its own - if (mlir::isa(user) && getFunction()->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) - { - return; - } - - uses.push_back(user); - if (isCall(user)) - { - if (isKeptArgument(user, used)) - { - kept = user; - } - - return; - } - - // `it.next()`: the field read, the bound function, its `this` - if (mlir::isa(user)) - { - for (auto partResult : user->getResults()) - { - values.push_back(partResult); - } - - return; - } - - if (!isBorrow(user, used)) - { - kept = user; - } - }); - } - - return kept; - } - - // A generator's maker puts what the generator captures - its parameters, an outer local - into - // a box, and the box into its state object's `.captured` field: the box of a closure that - // escapes, with the state object, at that store. There is no closure value, so no use runs it - // here and nothing calls through it. rc's `ts.RetainCell` of the box is the state object's - // reference, which moves in with it. A box used any other way is left to the RetainCell error. - void classifyStateBoxes() - { - getFunction().walk([&](mlir_ts::VariableOp boxOp) { - auto box = boxOp.getResult(); - if (!boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) || closureOfBox(box)) - { - return; - } - - auto closure = std::make_shared(); - closure->box = box; - llvm::SmallVector boxRetains; - for (auto *user : box.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(user)) - { - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - return; - } - - closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); - closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); - } - - continue; - } - - if (mlir::isa(user)) - { - boxRetains.push_back(user); - continue; - } - - if (!closure->escape && isStoreIntoState(user, box)) - { - closure->escape = user; - continue; - } - - return; - } - - if (!closure->escape) - { - return; - } - - auto stateRef = mlir::cast(closure->escape).getReference().getDefiningOp(); - auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); - auto borrowing = borrowingFieldsOf(stateType); - closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); - // `.map`'s maker copies what its parameter box holds into the generator's box: a borrow - if (closure->borrowsValues) - { - for (auto &fill : closure->fills) - { - if (!mlir::isa(fill.second.getValue().getType())) - { - setAttrTracked(fill.second, OWN_CAPTURE_BORROW_ATTR_NAME); - } - } - } - - for (auto &fill : closure->fills) - { - // a parameter this function does not own, in a box that does not borrow: the box - // cannot take rc's reference for it, the caller's still holds it; left to the - // second-reference error, which says the callers would have to give it up - auto param = borrowedParam(fill.second.getValue()); - if (!closure->borrowsValues && param >= 0 && !llvm::is_contained(ownedParams(getFunction()), param)) - { - continue; - } - - if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - } - - for (auto *retain : boxRetains) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - - stateBoxes[box] = closure.get(); - closures.push_back(std::move(closure)); - }); - } - - // `ts.Store(box, ts.PropertyRef(state, i))`, where `state` is a local a `ts.Constant` - // initializes whose every read fills a block `ts.New` made: a generator's initial state. - static bool isStoreIntoState(mlir::Operation *user, mlir::Value box) - { - auto storeOp = mlir::dyn_cast(user); - auto propertyRefOp = storeOp && storeOp.getValue() == box - ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto stateOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - if (!stateOp || !isConstantRecordLocal(stateOp)) - { - return false; - } - - auto reads = 0; - for (auto *stateUser : stateOp->getUsers()) - { - auto loadOp = mlir::dyn_cast(stateUser); - if (!loadOp) - { - continue; - } - - ++reads; - for (auto *readUser : loadOp->getUsers()) - { - // rc's retain of the initial state, for fields that own (holdsConstantsOrMoves) - if (mlir::isa(readUser)) - { - continue; - } - - auto fillOp = mlir::dyn_cast(readUser); - if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) - { - return false; - } - } - } - - return reads > 0; - } - - // rc's reference for the box's copy of `value`: the retain of it nearest before the store that - // fills the box, in the same block. - mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store) - { - // rc's retain of an earlier read of the same place (twinReadAfter) - if (auto loadOp = value.getDefiningOp()) - { - for (auto *user : loadOp.getReference().getUsers()) - { - auto earlier = mlir::dyn_cast(user); - auto *retain = earlier ? soleUser(earlier.getResult()) : nullptr; - if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && - twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && - retain->isBeforeInBlock(store)) - { - return retain; - } - } - } - - mlir::Operation *found = nullptr; - for (auto *user : value.getUsers()) - { - if (!mlir::isa(user) || user->getBlock() != store->getBlock() || - !user->isBeforeInBlock(store) || captureRetains.contains(user)) - { - continue; - } - - if (!found || found->isBeforeInBlock(user)) - { - found = user; - } - } - - return found; - } - - // Where a closure value goes: through its views and the owning `let`s that hold it, each use - // that may run it joins `uses`, and each release `ends`. Answers the first use that takes it - // anywhere else - returned, stored, kept by a callee, captured - or null. Anything this does - // not know is such a use. - mlir::Operation *followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, - llvm::SmallVectorImpl &ends, bool &aliased) - { - llvm::SmallVector values{closure}; - llvm::SmallPtrSet seen; - mlir::Operation *escape = nullptr; - auto followSlot = [&](mlir::Value slot) { - for (auto *slotUser : slot.getUsers()) - { - if (mlir::isa(slotUser)) - { - continue; - } - - if (mlir::isa(slotUser)) - { - ends.push_back(slotUser); - } - else if (auto loadOp = mlir::dyn_cast(slotUser)) - { - values.push_back(loadOp.getResult()); - } - else if (auto storeOp = mlir::dyn_cast(slotUser); !storeOp || storeOp.getReference() != slot) - { - escape = escape ? escape : slotUser; - } - } - }; - - while (!values.empty() && !escape) - { - auto value = values.pop_back_val(); - if (!seen.insert(value).second) - { - continue; - } - - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (escape || mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - ends.push_back(user); - return; - } - - if (isCall(user)) - { - if (isKeptArgument(user, used)) - { - escape = user; - } - else - { - uses.push_back(user); - } - - return; - } - - // the function and the box a call through the closure is made with - if (mlir::isa(user)) - { - for (auto *partUser : user->getResult(0).getUsers()) - { - if (isCall(partUser) && !isKeptArgument(partUser, user->getResult(0))) - { - uses.push_back(partUser); - } - else - { - escape = escape ? escape : partUser; - } - } - - return; - } - - mlir::Value slot; - if (auto varOp = mlir::dyn_cast(user)) - { - slot = varOp.getResult(); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) - { - slot = storeOp.getReference(); - } - - // a local, owning or not (`let f: () => number;` owns nothing): its reads are the - // closure; a function's result slot is a local too, and its read is returned - auto slotVar = slot ? slot.getDefiningOp() : mlir_ts::VariableOp(); - if (slotVar && !slotVar.getCaptured().value_or(false)) - { - aliased = aliased || !isOwningVariable(slotVar); - followSlot(slot); - return; - } - - escape = user; - }); - } - - return escape; - } - - // Does a call given this value give it a closure, which may run and touch what its box holds? - static bool holdsCaptures(mlir::Value value) - { - if (mlir::isa(value.getType())) - { - return true; - } - - auto root = rootOf(value); - if (auto varOp = root.getDefiningOp()) - { - return varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); - } - - // a closure's `this` is its box, a reference; a bound method's is the object - auto getThisOp = root.getDefiningOp(); - return getThisOp && mlir::isa(getThisOp.getType()); - } - - // The verdicts on the closures over cells: an escape is an error; a borrow must not be run after - // anything it borrows is given back - a cell this function owns at its releases, a copy at its - // owner's ends. A cell an enclosing closure holds and a parameter end nowhere here; a copy of a - // read out of a container is bounded by the read's own check, which sees the closure's uses - // (walkBorrowed). - void decideClosures(llvm::SetVector &toErase) - { - for (auto &closure : closures) - { - if (!closure->op) - { - decideEscaping(*closure, toErase); // a generator's box - continue; - } - - auto *boundOp = closure->op.getOperation(); - auto name = ownerName(closure->op.getResult()); - auto ok = true; - // Held by a local that owns nothing: rc's birth reference reads to the move logic as the - // store's, which would leave the box with no owner. The closure keeps its releases, and - // nothing may run it after them. - auto keepAliasedReleases = [&]() { - for (auto *end : closure->ends) - { - if (!mlir::isa(end)) - { - continue; - } - - toErase.remove(end); - for (auto *use : closure->uses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, name); - ok = false; - } - } - } - }; - - if (!closure->hasCells && !closure->borrowsCopies) - { - keepAliasedReleases(); - continue; - } - - if (closure->escape) - { - decideEscaping(*closure, toErase); - continue; - } - - for (auto &fill : closure->fills) - { - auto value = fill.second.getValue(); - llvm::SmallVector ends; - llvm::StringRef captured = "this value"; - if (mlir::isa(value.getType())) - { - if (isCellVariable(value)) - { - captured = varName(value.getDefiningOp()); - for (auto *user : value.getUsers()) - { - if (mlir::isa(user) || movesCellAway(user)) - { - ends.push_back(user); - } - } - } - else if (!isLoadedCell(value)) - { - reportCaptureNotOwned(fill.second, captured); - ok = false; - continue; - } - } - else - { - auto root = rootOf(value); - captured = ownerName(root); - Chain chain; - if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0 || isHandle(root)) - { - // nothing to own, a read the read's check bounds, a parameter, or a handle, - // whose copy in the box is counted (spec 23.2) - } - else if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - reportCaptureNotOwned(fill.second, captured); - ok = false; - continue; - } - - ends = rootEnds(chain); - } - - for (auto *end : ends) - { - if (captureRetains.contains(end) || captureStores.count(end)) - { - continue; // a box's own copy, this one's or another borrowing closure's - } - - for (auto *use : closure->uses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, captured); - ok = false; - } - } - } - } - - if (closure->aliased) - { - keepAliasedReleases(); - } - - // a call through the box, after the closure value that owns the box is given back - for (auto *end : closure->ends) - { - for (auto *use : closure->boxUses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, name); - ok = false; - } - } - } - - toErase.insert(closure->retains.begin(), closure->retains.end()); - } - } - - // Is this the store that moves a cell into the box of a closure that escapes? - bool movesCellAway(mlir::Operation *op) - { - auto storeOp = mlir::dyn_cast(op); - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - if (!propertyRefOp || !mlir::isa(storeOp.getValue().getType())) - { - return false; - } - - if (stateBoxes.count(propertyRefOp.getObjectRef())) - { - return true; - } - - auto boundOp = closureOfBox(propertyRefOp.getObjectRef()); - auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); - return found != closureOf.end() && found->second->escape; - } - - // A closure that escapes owns what its box holds (spec 2.5, phase 5b). Its copies moved in as - // in phase 4. Each cell moves in where the box takes it: that store is the move, rc's - // `ts.RetainCell` for it goes, and so does each of the frame's `ts.ReleaseCell`s the move reaches, - // which it must dominate. Nothing in the frame may use the variable after the move - read it, - // assign it, capture it again - and the move may not come round a loop to a cell made outside - // it. A cell that borrows its value (a parameter's) or one an enclosing closure holds has - // nothing to move. And nothing may call through the box once the closure has gone. - void decideEscaping(Closure &closure, llvm::SetVector &toErase) - { - auto ok = true; - for (auto &fill : closure.fills) - { - auto value = fill.second.getValue(); - mlir::Operation *move = fill.second; - if (!mlir::isa(value.getType())) - { - continue; - } - - if (isLoadedCell(value)) - { - reportEscapingCellNotOwned(move, closure.escape, "it is captured from an enclosing function"); - ok = false; - continue; - } - - auto varOp = isCellVariable(value) ? value.getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp) - { - reportCaptureNotOwned(move, "this value"); - ok = false; - continue; - } - - auto name = varName(varOp); - auto elementType = mlir::cast(value.getType()).getElementType(); - // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) - if (!isOwningVariable(varOp) && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) && - !MLIRTypeHelper::isSharedHandleType(elementType)) - { - reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); - ok = false; - continue; - } - - if (takerLoops(move, varOp)) - { - reportMovedInLoop(move, name); - ok = false; - continue; - } - - llvm::SmallVector moved; - for (auto *user : value.getUsers()) - { - if (user == move || llvm::is_contained(closure.retains, user) || !reachableAfter(move, user, varOp)) - { - continue; - } - - if (!mlir::isa(user)) - { - reportUsedAfterEscapingCapture(user, move, name); - ok = false; - break; - } - - if (!dominance->properlyDominates(move, user)) - { - reportMovedOnSomePaths(move, user, name); - ok = false; - break; - } - - moved.push_back(user); - } - - toErase.insert(moved.begin(), moved.end()); - } - - // A call through the box itself after the closure was given away or given back. followClosure - // stops at the escape, so `ends` may miss releases past it; that is enough while the only - // calls through a box are those of a folded `const f`, whose release ends its block. A - // generator's box has no closure value and no such call. - if (!closure.op) - { - toErase.insert(closure.retains.begin(), closure.retains.end()); - return; - } - - auto name = ownerName(closure.op.getResult()); - llvm::SmallVector gone(closure.ends.begin(), closure.ends.end()); - gone.push_back(closure.escape); - for (auto *end : gone) - { - for (auto *use : closure.boxUses) - { - if (ok && use != end && reachableAfter(end, use, closure.op.getOperation())) - { - reportUseAfterMove(use, end, name); - ok = false; - } - } - } - - toErase.insert(closure.retains.begin(), closure.retains.end()); - } - - // A captured variable whose value this function does not own - a parameter, `this` - has a cell - // that borrows the value: the cell's release frees the cell only (OWN_CELL_BORROWS_ATTR_NAME), - // and nothing may assign the variable, here or in a closure over it, since the old value is the - // caller's. A variable whose value nobody can be said to own is an error. - void decideCells() - { - getFunction().walk([&](mlir_ts::VariableOp varOp) { - auto cell = varOp.getResult(); - if (!isCellVariable(cell) || isOwningVariable(varOp)) - { - return; - } - - auto elementType = mlir::cast(cell.getType()).getElementType(); - if (!ownsHeap(varOp.getLoc(), elementType)) - { - return; - } - - // A handle's cell holds a count of its own, taken where it is made (spec 23.2): its - // release gives that back, as under rc, and an assignment is a counted copy. - if (MLIRTypeHelper::isSharedHandleType(elementType)) - { - return; - } - - auto name = varName(varOp); - auto init = varOp.getInitializer(); - auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); - if (!argument || !argument.getOwner()->isEntryBlock() || - !mlir::isa(argument.getOwner()->getParentOp())) - { - reportCaptureNotOwned(varOp, name); - return; - } - - for (auto *user : cell.getUsers()) - { - if (mlir::isa(user)) - { - setAttrTracked(user, OWN_CELL_BORROWS_ATTR_NAME); - continue; - } - - auto storeOp = mlir::dyn_cast(user); - if (mlir::isa(user) || (storeOp && storeOp.getReference() == cell)) - { - reportCapturedParamAssigned(user, name); - return; - } - - // captured: the closure's body must not assign it either - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); - if (!boundOp) - { - continue; - } - - auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); - if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) - { - reportCapturedParamAssigned(boundOp, name); - return; - } - } - }); - } - - // An argument read out of a local is lent to the call, but the arguments are read in order: a - // later one may give the local a new value before the call takes the first - `g(n, (n = mk(), 0))`, - // in a conditional argument too - or call a closure that assigns it, when it is captured. The - // assignment drops the block the argument is, and the callee reads freed memory. rc holds such an - // argument across the call instead (OwnedReturnConsumptionPass, holdGlobalArguments). A read of a - // global is checkGlobalRead's, and a store into a place in the local the place borrows' (a field - // read `g(n.v, (n.v = mk(), 0))`); a store into the local's slot ends `n.v` too. - void checkArgumentsAssigned() - { - getFunction().walk([&](mlir::Operation *call) { - if (!isCall(call)) - { - return mlir::WalkResult::advance(); - } - - for (auto argument : callArgs(call)) - { - // a method's object: `this` of the method bound to it - if (auto getThisOp = argument.getDefiningOp()) - { - auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); - if (mlir::isa_and_nonnull(boundOp)) - { - argument = boundOp->getOperand(0); - } - } - - auto *read = argument.getDefiningOp(); - auto local = mlir::dyn_cast_or_null(slotOf(argument)); - if (!read || !local || !ownsHeap(argument) || isHandle(argument)) - { - continue; - } - - auto assignedByClosure = closureAssigns(local); - auto *write = findBetween(read, call, [&](mlir::Operation *op) { - if (assignedByClosure && isCall(op)) - { - return true; - } - - auto storeOp = mlir::dyn_cast(op); - auto releaseSlotOp = mlir::dyn_cast(op); - return (storeOp && storeOp.getReference() == local.getResult()) || - (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); - }); - if (!write) - { - continue; - } - - if (reporting()) - { - auto name = varName(local); - auto callee = calleeName(call); - auto diag = call->emitError("'") - << name << "' is lent to '" << (callee.empty() ? "a function" : callee) - << "', but is given a new value before the call takes it; -mm=own cannot prove " - "it outlives the call"; - diag.attachNote(write->getLoc()) << "'" << name << "' is given a new value here"; - signalPassFailure(); - } - - return mlir::WalkResult::interrupt(); - } - - return mlir::WalkResult::advance(); - }); - } - - // Does a closure built over this captured local assign it (OWN_ASSIGNS_CAPTURES_ATTR_NAME)? Yes too - // when its cell goes where this does not follow. - bool closureAssigns(mlir_ts::VariableOp local) - { - if (!local.getCaptured().value_or(false)) - { - return false; - } - - for (auto *user : local->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (mlir::isa(user) || - (storeOp && storeOp.getReference() == local.getResult())) - { - continue; - } - - // the cell put into a closure's capture box - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); - if (!boundOp) - { - return true; - } - - auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); - if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) - { - return true; - } - } - - return false; - } - - template bool quietly(F check) - { - ++quiet; - auto result = check(); - --quiet; - return result; - } - - // Move, else borrow, else error (spec 2.2) for the receivers of one owning local. - void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) - { - auto owner = varName(slot.getDefiningOp()); - if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers)) - { - // a move on some paths only is placed when it is the local's one receiver (spec 30) - for (auto &receiver : receivers) - { - decideSlotReceiver(slot, receiver, owner, toErase, receivers.size() == 1); - } - - return; - } - - decideSlotReceivers(slot, receivers, owner, toErase); - } - - void decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, - llvm::SetVector &toErase, bool sole) - { - if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); })) - { - // rc's retain goes with the move; a call that keeps the argument is the move itself - if (!isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - - return; - } - - if (auto borrower = borrowerOf(receiver.retain)) - { - borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase); - return; - } - - checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); // reports - } - - // Receivers of one owning local that each read a value of its own (#528): `h.c = a; a = new C(); - // g.c = a`. An assignment comes between every two of their reads on every path from one to the - // other, so each is decided alone, as the one receiver of the value it reads. So are receivers - // no path joins at all, assigned or not: `if (c) return a; return a;` moves `a` once on each - // way out. - bool takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers) - { - auto assignments = slotAssignments(slot); - llvm::SmallVector kills(assignments.begin(), assignments.end()); - if (auto varOp = slot.getDefiningOp()) - { - kills.push_back(varOp); - } - - for (auto &from : receivers) - { - for (auto &to : receivers) - { - if (&from != &to && reachableAfter(from.load, to.load, kills)) - { - return false; - } - } - } - - return true; - } - - // ---- A kept parameter released where it is not moved (spec 33) ---- - // - // `return v.x > 2 ? acc : V.plus(acc, v)`: `acc` is kept (OwnershipSignaturePass), so the caller - // moved it in; the path that returns it moves it out, and the others must release it. So must a - // callback that only borrows the accumulator its family gives it to keep. MLIRGen made no owner - // of a parameter, so its releases are put in here, before each way out of the body - a return, - // a throw that leaves the function - as MLIRGen puts an owning local's. The move's own check then - // erases those it reaches, and places the release on the edges into the join that did not move - // it (spec 30), statically: no run-time flag. Unwinding out of the function (a call that throws - // outside a `try`, a `finally` passed on the way out) gives nothing back: it leaks, as an owning - // local does there. - - // the releases put in for this run - llvm::SmallVector exitReleases; - - // The slot of parameter `index` (isParameterSlot), or null. - mlir_ts::VariableOp parameterSlot(int32_t index) - { - auto &body = getFunction().getFunctionBody(); - if (body.empty() || static_cast(index) >= body.front().getNumArguments()) - { - return {}; - } - - for (auto *user : body.front().getArgument(index).getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - if (varOp && isParameterSlot(varOp, position) && position == index) - { - return varOp; - } - } - - return {}; - } - - // Is `slot` a parameter this function keeps and releases where it does not move it? - bool isReleasedParam(mlir::Value slot) - { - auto f = getFunction(); - auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); - auto varOp = slot.getDefiningOp(); - auto index = -1; - return attr && varOp && isParameterSlot(varOp, index) && llvm::is_contained(attr.asArrayRef(), index) && - llvm::is_contained(ownedParams(f), index); - } - - // Does `value` come from a read of `slot`: computed from one, through any op? - static bool readsSlot(mlir::Value value, mlir::Value slot, int depth = 0) - { - auto *def = value.getDefiningOp(); - if (!def) - { - return false; - } - - // too deep to tell: as if it did - if (depth > 16) - { - return true; - } - - if (auto loadOp = mlir::dyn_cast(def); loadOp && loadOp.getReference() == slot) - { - return true; - } - - return llvm::any_of(def->getOperands(), [&](mlir::Value operand) { return readsSlot(operand, slot, depth + 1); }); - } - - void releaseKeptParamsAtExits() - { - exitReleases.clear(); - auto f = getFunction(); - auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); - if (!attr) - { - return; - } - - for (auto index : attr.asArrayRef()) - { - auto varOp = parameterSlot(index); - if (!varOp || !llvm::is_contained(ownedParams(f), index)) - { - continue; - } - - for (auto &block : f.getFunctionBody()) - { - auto *exit = block.empty() ? nullptr : &block.back(); - // a thrown value made of the parameter takes it with it: no release there (a leak, - // if it does not) - auto leaves = mlir::isa_and_nonnull(exit) || - (mlir::isa_and_nonnull(exit) && - !readsSlot(exit->getOperand(0), varOp.getResult())); - if (!leaves) - { - continue; - } - - mlir::OpBuilder builder(exit); - exitReleases.push_back(builder.create(exit->getLoc(), varOp.getResult())); - } - } - } - - // A parameter this function keeps (`__own_params`): its caller moved the argument in, and this - // function has no release for it, so exactly one taker must move it on, on every way out of - // the function, and nothing may read it after that. - void decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) - { - auto name = varName(slot.getDefiningOp()); - auto &receiver = receivers.front(); - auto *taker = takerOf(receiver.retain, receiver.value); - if (receivers.size() > 1) - { - auto &second = receivers[1]; - auto *secondTaker = takerOf(second.retain, second.value); - reportUseAfterMove(secondTaker ? secondTaker : second.retain, taker ? taker : receiver.retain, name); - return; - } - - // one the body releases on the paths that do not move it (spec 33): the move's own check - // erases the releases at the exits it reaches, and places one on each edge into where the - // paths join that did not move it (spec 30) - auto released = isReleasedParam(slot); - if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, released)) - { - return; - } - - if (released) - { - if (!isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - - return; - } - - auto &body = getFunction().getFunctionBody(); - auto *moved = body.findAncestorOpInRegion(*taker); - auto everyPath = true; - getFunction().walk([&](mlir_ts::ReturnInternalOp returnOp) { - auto *exit = body.findAncestorOpInRegion(*returnOp); - if (everyPath && (!moved || !exit || !dominance->dominates(moved, exit))) - { - reportKeptOnSomePaths(taker, returnOp, name); - everyPath = false; - } - }); - - // rc's retain goes with the move - a `ts.RetainSlot` too, for an owning local the parameter - // moves into (`let r = init`, spec 33); a call that keeps it is the move itself - if (everyPath && !isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - } - - // The thrown value, read out of the exception object by a catch's copy thunk (`.eh.copy.*`, - // which the C++ runtime calls to copy the exception into the catch). rc retains it for the - // copy; own moves it: the exception object never gives its reference back (a throw hands the - // thrower's reference to it, and nothing releases it - rc leaks it the same way), and each - // exception is copied into one catch, since a TypeScript rethrow is a new throw. - bool isReadOutOfException(mlir::Value value) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); - return argument && argument.getOwner()->isEntryBlock() && getFunction().getName().starts_with(".eh.copy."); - } - - // The read of a parameter this function keeps, through its views. None for anything else. - mlir_ts::LoadOp keptParamLoadOf(mlir::Value value) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index) || !llvm::is_contained(ownedParams(getFunction()), index)) - { - return {}; - } - - return loadOp; - } - - // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, so - // none may move it. A receiver that is not a `let` reports its move error - another - // receiver's reads come after it - or, if its move alone would pass, that a borrow pins it. - void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, - llvm::SetVector &toErase) - { - auto ends = slotEnds(slot); - mlir_ts::VariableOp someBorrower; - for (auto &receiver : receivers) - { - if (auto borrower = borrowerOf(receiver.retain)) - { - someBorrower = borrower; - borrowWith(borrower, receiver.retain, ends, owner, toErase); - } - } - - for (auto &receiver : receivers) - { - if (borrowerOf(receiver.retain)) - { - continue; - } - - llvm::SetVector unused; - if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) - { - reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); - } - } - } - - // A `let` that could borrow: owning, declared from a value, holding a reference it took - // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. With - // `consumed`, also one that took over the reference rc thought the value carried: a call's - // result that turned out to borrow an argument carries none. - static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition, bool consumed = false) - { - mlir_ts::VariableOp varOp; - if (auto retainSlotOp = mlir::dyn_cast(acquisition)) - { - varOp = retainSlotOp.getSlot().getDefiningOp(); - } - else if (auto retainOp = mlir::dyn_cast(acquisition)) - { - // rc's reference for a `let` that takes the value over: `let ri = raw` retains the - // interface and declares `ri` consuming it - varOp = consumingLetOf(retainOp.getReference()); - consumed = true; - } - else - { - varOp = mlir::dyn_cast(acquisition); - } - - if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - return {}; - } - - if (varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) - { - return consumed ? varOp : mlir_ts::VariableOp(); - } - - auto retains = llvm::any_of(varOp.getResult().getUsers(), - [](mlir::Operation *user) { return mlir::isa(user); }); - return retains ? varOp : mlir_ts::VariableOp(); - } - - // tryBorrow for the `let` an acquisition gives a reference: a `ts.Retain` of the value a - // consuming `let` takes over is that `let`'s, and goes with the borrow; a `ts.RetainSlot` is the - // `let`'s own bookkeeping already. - bool borrowWith(mlir_ts::VariableOp borrower, mlir::Operation *acquisition, llvm::ArrayRef ends, - llvm::StringRef owner, llvm::SetVector &toErase) - { - if (!tryBorrow(borrower, ends, owner, toErase)) - { - return false; - } - - if (mlir::isa(acquisition)) - { - toErase.insert(acquisition); - } - - return true; - } - - // The owning `let` declared from `value` that takes over its reference, or none. - static mlir_ts::VariableOp consumingLetOf(mlir::Value value) - { - for (auto *user : value.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - if (varOp && varOp.getInitializer() == value && varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) - { - return varOp; - } - } - - return {}; - } - - // Where an owning local stops holding its value: each release of its slot and each store - // into it. - static llvm::SmallVector slotEnds(mlir::Value slot) - { - llvm::SmallVector ends; - for (auto *user : slot.getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - } - - return ends; - } - - // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its - // retain and every release of its slot go, and the owner keeps its own. Sound while no use of - // the borrower can run after any of `ends` - the owner's releases and assignments - and while - // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use - // through the borrower's own declaration is a new borrow, not this one. Reports and returns - // false otherwise. - bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, - llvm::SetVector &toErase) - { - auto name = varName(borrower); - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - mlir::Operation *declaration = borrower; - if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) - { - if (isAssignmentOf(kept, borrower)) - { - reportBorrowerAssigned(kept, name, owner); - } - else - { - reportBorrowEscapes(kept, name, owner); - } - - return false; - } - - for (auto *end : ends) - { - for (auto &use : uses) - { - if (reachableAfter(end, use.op, use.kills)) - { - reportBorrowOutlives(use.op, end, name, owner); - return false; - } - } - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - return true; - } - - // A use of a borrowed value, and where the value it sees is made again: a path from a release - // to the use through one of `kills` reaches a new value, not this one. For the borrow's own - // uses that is the read or the borrowing `let`; for a use through a merge it is the merge's - // block, and through a local that owns nothing, that local's declaration and assignments. - struct BorrowUse - { - mlir::Operation *op; - llvm::SmallVector kills; - // the function value an indirect call goes through: read when the call starts, not while - // the callee runs - bool callee = false; - }; - - // A borrowing `let`'s slot: each read, and everything it leads to (walkBorrowed), joins - // `uses`, made again at `kills`; its `ts.RetainSlot` and `ts.ReleaseSlot`s are `bookkeeping`, - // which a borrow erases. Answers the first use that would keep what the slot holds - an - // assignment, or anything else that is not a read - or null. - mlir::Operation *collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, - llvm::SmallVectorImpl &uses, - llvm::SmallVectorImpl &bookkeeping) - { - for (auto *user : borrower.getResult().getUsers()) - { - if (mlir::isa(user)) - { - bookkeeping.push_back(user); - continue; - } - - auto readOp = mlir::dyn_cast(user); - if (!readOp) - { - return user; - } - - uses.push_back({user, {kills.begin(), kills.end()}}); - if (auto *kept = walkBorrowed(readOp.getResult(), kills, uses)) - { - return kept; - } - } - - return nullptr; - } - - static bool isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp) - { - auto storeOp = mlir::dyn_cast(op); - return storeOp && storeOp.getReference() == varOp.getResult(); - } - - // What `use` hands the very value it uses on to, as that value: a cast or a view of it, the - // argument of the block a branch passes it to (a merge: `a ?? b`), or the reads of a local - // that owns nothing and is assigned it (`for (x of arr)` into an outer `let x`). Answers - // whether the use is one of those, even where nothing comes out of it. Where the value lands - // somewhere that is replaced - a block argument, a local - `renewedAt` gets what replaces it: - // the block's entry, the local's declaration (when it has an initializer: `let l!: V` stores - // nothing, and in a loop the slot keeps the last round's value) and assignments. - static bool passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, - llvm::SmallVectorImpl *renewedAt = nullptr) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || isView(user)) - { - values.push_back(user->getResult(0)); - return true; - } - - if (auto branchOp = mlir::dyn_cast(user)) - { - if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) - { - values.push_back(*argument); - if (renewedAt) - { - renewedAt->push_back(&argument->getOwner()->front()); - } - - return true; - } - - return false; - } - - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == use.get()) - { - if (auto varOp = storeOp.getReference().getDefiningOp(); - varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) - { - if (renewedAt && varOp.getInitializer()) - { - renewedAt->push_back(varOp); - } - - for (auto *varUser : varOp.getResult().getUsers()) - { - if (auto loadOp = mlir::dyn_cast(varUser)) - { - values.push_back(loadOp.getResult()); - } - else if (renewedAt && isAssignmentOf(varUser, varOp)) - { - renewedAt->push_back(varUser); - } - } - - return true; - } - } - - return false; - } - - // Where a borrowed value goes. Every use of it, and of what it produces that may still point - // into the block, joins `uses`, made again at `kills`. The value itself - through what passesOn - // hands it on to - must not be kept anywhere; what it produces - a field reference, a value - // loaded through one, a bound method, a catch clause's non-owning local - still points into - // the borrowed block, so each of its uses is a use of the borrow too. A value that cannot point - // anywhere (a number, a boolean) ends the walk. Where passesOn hands the value to something - // that is replaced, the uses beyond are made again where it is replaced instead; the op that - // hands it on is itself a use, bounded by `kills`. `isBorrower` accepts a use of the value - // itself that the caller follows on its own (a borrowing `let`), made again at the kills it is - // given. Answers the first use that would keep the value. - // - // `keeping` names what rc planted around a borrow that it took for an owner and that goes - // with it: the temporary release of a result that borrows an argument (the start's, or that of - // a call it is passed to, which is a borrow of it too), and, in a function whose result - // borrows this very value's parameter, the retain rc made for the caller. Those join - // `bookkeeping`, not `uses`, and the return itself is a use, not a keep. - struct Keeping - { - // no default member initializers: GCC rejects them in a default argument of the class - // that encloses the struct - llvm::SmallVectorImpl *bookkeeping; - bool returns; - }; - - mlir::Operation *walkBorrowed( - mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, - llvm::function_ref)> isBorrower = nullptr, - Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false, bool startBorrowed = true) - { - struct Pending - { - mlir::Value value; - bool borrowed; - llvm::SmallVector kills; - }; - - llvm::SmallVector values; - // `startBorrowed` false: `start` is what a read produced (a field's reference), whose uses - // are uses of the borrow but not the borrowed value itself - values.push_back({start, startBorrowed, {kills.begin(), kills.end()}}); - llvm::SmallPtrSet seen{start}; - while (!values.empty()) - { - auto pending = values.pop_back_val(); - for (auto &use : pending.value.getUses()) - { - auto *user = use.getOwner(); - if (pending.borrowed && keeping.bookkeeping && - ((mlir::isa(user) && borrowingCall(rootOf(pending.value))) || - (keeping.returns && mlir::isa(user)))) - { - keeping.bookkeeping->push_back(user); - continue; - } - - // rc's reference for a borrowing box's copy goes with the closure (decideClosures), and - // one for a borrowing field's with the field (classifyBorrowingFields) - if (captureRetains.contains(user) || fieldBorrowRetains.contains(user)) - { - continue; - } - - uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); - // copied into a borrowing box: whatever runs the closure reads it - if (auto found = captureStores.find(user); found != captureStores.end()) - { - for (auto *closureUse : found->second->uses) - { - uses.push_back({closureUse, pending.kills}); - } - } - - // stored into a field a borrowing record borrows (spec 32): whatever uses the record - // reads it - if (auto found = recordStoreUses.find(user); found != recordStoreUses.end()) - { - for (auto &recordUse : found->second->walk.uses) - { - uses.push_back({recordUse.op, pending.kills, recordUse.callee}); - } - } - - if (pending.borrowed && keeping.returns && mlir::isa(user)) - { - continue; - } - - if (pending.borrowed) - { - // a handle read that rc counts (spec 23.3): a return or an `any` box rc - // retained for is a counted copy of its own; passesOn would hand them on - if (countedHandle && isCountedHandOff(user, pending.value)) - { - continue; - } - - llvm::SmallVector same; - llvm::SmallVector renewedAt; - if (passesOn(use, same, &renewedAt)) - { - for (auto value : same) - { - if (mayPointInto(value) && seen.insert(value).second) - { - values.push_back({value, true, renewedAt.empty() ? pending.kills : renewedAt}); - } - } - - continue; - } - - // a handle read that rc counts (spec 23.3): its retain, and what takes the value - // it retains for, are a counted copy of their own, as a string copy is; the - // read's other uses are the borrow's (Ruling 7) - if (countedHandle && isCountedHandleReceiver(user, pending.value)) - { - continue; - } - - if (isBorrower && isBorrower(user, pending.kills)) - { - continue; - } - - if (mlir::isa(user) || !isBorrow(user, pending.value)) - { - return user; - } - } - - // a copy (spec 22.2) reads the borrow into a block of its own, which points into - // nothing, and so does a concatenation: `s + t` is a new string whatever s and t are - // (a `+` of two strings lowers to ts.StringConcat, which always allocates) - if (mlir::isa(user) || - (mlir::isa(user) && mlir::isa(user->getResult(0).getType()))) - { - continue; - } - - // a call whose result is the caller's own (callResultIsOwn) ends the walk at the call - if (callResultIsOwn(user)) - { - continue; - } - - // A result that borrows what it was given borrows this too, and is walked as it. - for (auto result : user->getResults()) - { - if (mayPointInto(result) && seen.insert(result).second) - { - values.push_back({result, !!borrowingCall(result), pending.kills}); - } - } - } - } - - return nullptr; - } - - // Whether a call hands back a block of the caller's own, which points into nothing it was given: - // walkBorrowed then ends a borrow's walk at the call (the call itself is still a use for as long - // as it runs). Never a generator's state that borrows its arguments (`__own_result_bounded`). - // - A call known to make its result (`__own_fresh_result`: an iterator's `next()`, a field that - // only ever holds one function, another own module's `..new`) - the `{value, done}` record of - // `it.next()` points nowhere into `it`, and what it does after its next call cannot be a use of - // the method read out of `it`. - // - A direct call of a function this module defines (isFresh): its result points into nothing it - // was given but what its facts name - a borrow of an argument (`__own_result_borrows`, never - // fresh), a borrowing record (isFresh, but bounded by what it borrows), a generator's state - // that borrows its arguments - and a body whose callers cannot all know such a fact is an - // error. `getPoint(x, scene.camera)` makes a new vector: a call after it that may destroy - // `scene.camera` does not end that vector (spec 34). - static bool callResultIsOwn(mlir::Operation *call) - { - if (!isCall(call) || call->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) - { - return false; - } - - return call->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) || - (call->getNumResults() == 1 && !isRecordResult(call) && isFresh(call->getResult(0))); - } - - // Is this what an indirect call goes through: its callee value, or, for a method read out of - // a field of an object (`o.m()`, `m` a field holding a function), the object it is bound to, - // which is the container of what was read, not what was borrowed. A method of a class bound - // to a borrowed instance (`h.c.m()`) is not one: its `this` is the borrow. - static bool isCalleeOperand(mlir::OpOperand &use) - { - auto *user = use.getOwner(); - auto callee = isCall(user) ? calleeValue(user) : mlir::Value(); - if (!callee) - { - return false; - } - - if (use.getOperandNumber() == 0) - { - return true; - } - - auto getThisOp = use.get().getDefiningOp(); - auto getMethodOp = callee.getDefiningOp(); - auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); - return use.getOperandNumber() == 1 && getMethodOp && methodField && - getThisOp.getOperand() == getMethodOp.getBoundFunc() && - mlir::isa(methodField.getReference().getType()); - } - - // ---- Borrowing records (spec 32) ---- - // - // An object literal boxed as an interface that holds values it does not own - `this`, a parameter, - // a field read, an owning `let`'s value, a captured variable's - beside fresh values, which move - // in. The signature pass marked its box (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME): its release frees - // the box and the fields it owns, and leaves the others to their owners. So the box has one owner - // like any fresh block - the box's own check - and what it borrows bounds every use of it: a read - // out of a place is checked with the record's uses as its own (walkBorrowed), a local's value - // against the local's ends, a captured variable's against whatever may assign it. Returned, the - // record is the caller's to own and to bound (checkRecordResults), which the function's facts - // must say. Stored anywhere else that may outlive what it borrows, it is an error. - struct RecordWalk - { - llvm::SmallVector uses; - // the first use that would keep the record itself: a store into a place, a callee that keeps - // it, a capture - mlir::Operation *kept = nullptr; - llvm::SmallVector returns; - }; - - struct BorrowedField - { - mlir_ts::StoreOp store; - RecordFieldSource source; - }; - - struct BorrowingRecord - { - mlir_ts::NewOp box; - mlir_ts::VariableOp local; - llvm::SmallVector interfaces; - llvm::SmallVector borrowed; - RecordWalk walk; - }; - - llvm::SmallVector> records; - // a store of a place read into a borrowing record's field -> the record's uses, which are the - // read's uses too (walkBorrowed) - llvm::DenseMap recordStoreUses; - - // Can a value of this type point into a heap block - a reference into one, a block of its own, - // or a method bound to one? A number or a boolean read out of a borrowed block cannot. - bool mayPointInto(mlir::Value value) - { - auto type = value.getType(); - return mlir::isa(type) || - ownsHeap(value); - } - - // The borrowing record built in `varOp` (classifyRecords), or null. - BorrowingRecord *borrowingRecordOf(mlir_ts::VariableOp varOp) - { - for (auto &record : records) - { - if (record->local == varOp) - { - return record.get(); - } - } - - return nullptr; - } - - // The boxes the signature pass marked as borrowing records, where every field still holds what it - // was marked for: a borrowed field a value this pass bounds (recordFieldSource), any other one a - // fresh value or nothing. A store into a borrowed field takes nothing, and whatever uses the record - // reads what it stored (walkBorrowed, through recordStoreUses). A box that is not what it was - // marked for is left to the record's retain, which is then an error. - void classifyRecords() - { - getFunction().walk([&](mlir_ts::NewOp newOp) { - auto fields = recordBorrowedFields(newOp); - auto varOp = fields.empty() ? mlir_ts::VariableOp() : recordLocalOf(newOp.getResult()); - if (!varOp) - { - return; - } - - auto record = std::make_shared(); - record->box = newOp; - record->local = varOp; - for (auto *user : varOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - auto borrows = llvm::is_contained(fields, static_cast(propertyRefOp.getPosition())); - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - continue; - } - - auto source = recordFieldSource(storeOp.getValue()); - if (source == RecordFieldSource::NoBlock) - { - continue; - } - - if (source == RecordFieldSource::Unknown || borrows != (source != RecordFieldSource::Fresh)) - { - return; - } - - if (borrows) - { - record->borrowed.push_back({storeOp, source}); - } - } - } - - for (auto *user : newOp->getUsers()) - { - if (auto castOp = mlir::dyn_cast(user)) - { - for (auto *castUser : castOp->getUsers()) - { - if (auto newInterfaceOp = mlir::dyn_cast(castUser)) - { - record->interfaces.push_back(newInterfaceOp.getResult()); - } - } - } - } - - // a path back to a use through the record's making is another record - llvm::SmallVector kills{newOp.getOperation(), varOp.getOperation()}; - walkRecord(record->interfaces, kills, record->walk); - for (auto &field : record->borrowed) - { - setAttrTracked(field.store, OWN_CAPTURE_BORROW_ATTR_NAME); - recordStoreUses[field.store.getOperation()] = record.get(); - } - - records.push_back(std::move(record)); - }); - } - - // Every use of a borrowing record (spec 32), and of what is read out of it - each may read what - // the record borrows - through its views, merges, and the locals that hold it, made again at - // `kills`. Its own retains and releases are the box's, which its own check decides. The record - // itself is `kept` when it is stored anywhere but a local, given to a callee that keeps it, or - // captured; returned, it is in `returns`. A call given it hands back its own block, unless that - // is a borrowing record that holds it, whose uses are then this one's as well. - void walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk) - { - struct Pending - { - mlir::Value value; - bool record; - }; - - llvm::SmallVector work; - llvm::SmallPtrSet seen; - auto push = [&](mlir::Value value, bool record) { - if ((record || mayPointInto(value)) && seen.insert(value).second) - { - work.push_back({value, record}); - } - }; - - auto followSlot = [&](mlir_ts::VariableOp varOp, bool record) { - for (auto *slotUser : varOp->getUsers()) - { - // its releases and its other values are the slot's own - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - push(loadOp.getResult(), record); - } - } - }; - - auto keep = [&](mlir::Operation *user) { walk.kept = walk.kept ? walk.kept : user; }; - - for (auto start : starts) - { - push(start, true); - } - - while (!work.empty()) - { - auto pending = work.pop_back_val(); - auto value = pending.value; - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || - (pending.record && mlir::isa(user))) - { - continue; - } - - // the same value: a view, a cast, a merge - if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) - { - push(user->getResult(0), pending.record); - continue; - } - - if (auto branchOp = mlir::dyn_cast(user)) - { - if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) - { - push(*argument, pending.record); - } - - continue; - } - - // a local that holds it: its reads - auto varOp = mlir::dyn_cast(user); - auto storeOp = mlir::dyn_cast(user); - if (varOp || (storeOp && storeOp.getValue() == value)) - { - auto slotOp = varOp ? varOp : storeOp.getReference().getDefiningOp(); - if (slotOp && !slotOp.getCaptured().value_or(false)) - { - followSlot(slotOp, pending.record); - continue; - } - - // a place, a global, a captured variable: what is read out of the record is checked - // where it is read (checkPlaceRead) - walk.uses.push_back({user, {kills.begin(), kills.end()}}); - if (pending.record) - { - keep(user); - } - - continue; - } - - walk.uses.push_back({user, {kills.begin(), kills.end()}, isCall(user) && isCalleeOperand(use)}); - if (mlir::isa(user)) - { - if (pending.record) - { - walk.returns.push_back(user); - } - - continue; - } - - if (isCall(user)) - { - if (pending.record && isKeptArgument(user, value)) - { - keep(user); - continue; - } - - // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that - // holds it - points where it points - auto args = callArgs(user); - auto given = [&](llvm::ArrayRef indices) { - return llvm::any_of(indices, [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - }); - }; - - if (user->getNumResults() == 1 && given(resultBorrowsAll(user))) - { - push(user->getResult(0), pending.record || isRecordResult(user)); - } - - // ...and a generator's state that borrows it (OWN_RESULT_BOUNDED_ATTR_NAME) - if (auto bounded = user->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); - bounded && user->getNumResults() == 1 && - given(llvm::SmallVector(bounded.asArrayRef().begin(), bounded.asArrayRef().end()))) - { - push(user->getResult(0), false); - } - - continue; - } - - if (pending.record && !isBorrow(user, value)) - { - keep(user); - continue; - } - - // a field written through the record: its old value may be one the record borrows, - // which the overwrite would release, and the field may be one its release leaves - if (pending.record && mlir::isa(user)) - { - for (auto *fieldUser : user->getResult(0).getUsers()) - { - auto fieldStore = mlir::dyn_cast(fieldUser); - if (mlir::isa(fieldUser) || - (fieldStore && fieldStore.getReference() == user->getResult(0))) - { - keep(fieldUser); - } - } - } - - // what it produces - a field's reference, a value read out of it, a bound method - may - // still point into what the record borrows - for (auto result : user->getResults()) - { - push(result, false); - } - } - } - } - - // The name the record goes by: the local that holds it, or a folded `const`'s. - llvm::StringRef recordName(mlir::Value value) - { - llvm::StringRef name = ownerName(value); - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (auto varOp = mlir::dyn_cast(user); varOp && varOp.getInitializer() == used) - { - name = varName(varOp); - } - }); - - return name; - } - - // What a borrowed field holds, for an error: `'ray'`, `'this.pos'`, `'livec'`, ... - std::string describeRecordSource(BorrowedField field) - { - auto value = field.store.getValue(); - auto quoted = [](llvm::StringRef name) { return "'" + name.str() + "'"; }; - switch (field.source) - { - case RecordFieldSource::Param: - return quoted(paramName(borrowedParam(value))); - case RecordFieldSource::Place: - if (auto loadOp = rootOf(value).getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - return describePlace(loadOp.getReference()); - } - - return describeCall(rootOf(value).getDefiningOp()); - case RecordFieldSource::Slot: - return quoted(varName(slotLoadOf(value).getReference().getDefiningOp())); - case RecordFieldSource::Cell: - return "a captured variable"; - default: - return "a value it does not own"; - } - } - - // A record this function makes (spec 32): kept, it is an error; returned, each field it borrows - // must be a parameter's that this function's facts lend (`__own_result_record`), so that its - // callers bound it; and no use of it may come after what a field borrows may be released or - // overwritten - an owning local's ends, and whatever may assign a captured variable. A parameter - // and `this` are the caller's for the whole call; a read out of a place has the record's uses as - // its own (checkPlaceRead). - void checkBorrowingRecords() - { - for (auto &record : records) - { - auto name = recordName(record->interfaces.empty() ? record->box.getResult() : record->interfaces.front()); - if (record->walk.kept) - { - reportPlaceBorrowEscapes(record->walk.kept, name, - record->borrowed.empty() ? "a value it does not own" - : describeRecordSource(record->borrowed.front())); - continue; - } - - auto ok = true; - for (auto *returned : record->walk.returns) - { - for (auto &field : record->borrowed) - { - if (!ok || field.source == RecordFieldSource::Immortal) - { - continue; - } - - auto param = field.source == RecordFieldSource::Param || field.source == RecordFieldSource::Place - ? borrowedParam(field.store.getValue()) - : -1; - if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) - { - reportPlaceBorrowEscapes(returned, name, describeRecordSource(field)); - ok = false; - } - } - } - - for (auto &field : record->borrowed) - { - if (!ok) - { - break; - } - - auto value = field.store.getValue(); - Chain chain; - if (field.source == RecordFieldSource::Slot) - { - chain.roots.push_back({slotLoadOf(value).getReference(), Chain::Slot}); - } - else if (auto load = field.source == RecordFieldSource::Param ? keptParamLoadOf(value) : mlir_ts::LoadOp()) - { - // a parameter this function keeps is its own to move away, as a local's value is - chain.roots.push_back({load.getReference(), Chain::Slot}); - } - else if (field.source == RecordFieldSource::Cell) - { - chain.roots.push_back({rootOf(value), Chain::NotOwned}); - } - else - { - continue; - } - - auto ends = rootEnds(chain); - if (field.source == RecordFieldSource::Cell) - { - for (auto *drop : drops) - { - if (dropsChain(drop, chain, nullptr, value)) - { - ends.push_back(drop); - } - } - } - - for (auto *end : ends) - { - for (auto &use : record->walk.uses) - { - if (ok && (use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) - { - if (field.source == RecordFieldSource::Slot) - { - reportBorrowOutlives(use.op, end, name, varName(chain.roots.front().first.getDefiningOp())); - } - else - { - reportPlaceBorrowOutlives(use.op, end, name, describeRecordSource(field)); - } - - ok = false; - } - } - } - } - } - } - - // A call whose result is a borrowing record (OWN_RESULT_RECORD_ATTR_NAME, spec 32): the box is - // this function's to own - it is fresh, and its own check decides its moves and releases - but - // it may hold a borrow of each argument the facts name, as a call's result that borrows them - // does (checkPlaceRead): somewhere under each. So no use of it, through the locals it moves - // into, may come after one of those arguments may be released or overwritten, and it may not be - // kept. Returned, the arguments must be parameters this function's own facts lend. Its releases - // are not uses: they free only what the record owns. - void checkRecordResults() - { - getFunction().walk([&](mlir::Operation *call) { - if (!isCall(call) || !isRecordResult(call) || call->getNumResults() != 1) - { - return; - } - - auto result = call->getResult(0); - auto place = describeCall(call); - auto name = recordName(result); - RecordWalk walk; - llvm::SmallVector kills{call}; - walkRecord({result}, kills, walk); - if (walk.kept) - { - reportPlaceBorrowEscapes(walk.kept, name, place); - return; - } - - auto args = callArgs(call); - for (auto *returned : walk.returns) - { - for (auto index : resultBorrowsAll(call)) - { - if (static_cast(index) >= args.size() || argumentHoldsNoBlock(args[index]) || - isImmortalLiteral(args[index])) - { - continue; - } - - auto param = borrowedParam(args[index]); - if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) - { - reportPlaceBorrowEscapes(returned, name, place); - return; - } - } - } - - auto chain = chainOf(call); - // rc's retain of a fresh argument right before the call is no end: the box's own check - // decides it, and the use that takes it is an end of its own - llvm::SmallVector ends; - for (auto *end : rootEnds(chain)) - { - auto ownedRetain = mlir::isa(end) && llvm::any_of(chain.roots, [&](auto &root) { - return root.second == Chain::Owned && rootOf(end->getOperand(0)) == root.first; - }); - if (!ownedRetain) - { - ends.push_back(end); - } - } - - for (auto *drop : drops) - { - if (drop != call && dropsChain(drop, chain, call)) - { - ends.push_back(drop); - } - } - - for (auto *end : ends) - { - for (auto &use : walk.uses) - { - if ((use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) - { - reportPlaceBorrowOutlives(use.op, end, name, place); - return; - } - } - } - }); - } - - // ---- Reads out of containers (spec 2.3, phase 3) ---- - // - // `h.c` and `arr[i]` read a value the container owns. Such a read is a borrow: it may not be - // kept, and none of its uses - nor any use of what it produces - may run after something that - // may destroy it (spec 2.2's dropping mutations): an overwrite of a place on the way from the - // read up to its root, an array op that removes elements, an end of the root itself, or a call - // that may reach the root. A path back to a use through the read itself is a new borrow. - - // A read of a heap value out of a field or an element, or a call whose result borrows an - // argument (spec 2.4, phase 4), seen through its views. Null for anything else. - mlir::Operation *placeReadOf(mlir::Value value) - { - auto root = rootOf(value); - auto *def = root.getDefiningOp(); - // a handle read out of a place that rc gives an owner of its own is a counted copy, not a - // borrow (spec 23.3); any other is a borrow of the place, as a field read is - if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) - { - return nullptr; - } - - if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) - { - // a field of a generator's borrowing box: only its maker fills it, and nothing a call - // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) - if (isBorrowingBoxField(loadOp.getReference())) - { - return nullptr; - } - - return def; - } - - return borrowingCall(root); - } - - // How a handle read out of a place is used (spec 23.3), seen through casts, merges and locals - // that own nothing. Every use is classified, and nothing falls through (Ruling 9): - // - `counted`: a receiver that takes a count rc made for that very use - // (isCountedHandleReceiver) - a store, a push or an unshift, `new Shared`, an array - // literal's element, a return, an `any` box, or an owning local's initializer; - // - passed on: a cast or a view that is still a handle, a merge, a local that owns nothing - - // whose own uses are classified in turn; - // - `borrowed`: everything else. A use that reads through the place (`f(p, p.child)`, a - // `.value` read, `Shared.count`), and every use that is neither of the above: a tuple's - // element, an object literal's field, a capture, a `yield`, a receiver rc made no retain - // for, any op not named here. The read is then a borrow, and tenure-checked - // (checkPlaceRead). - // rc's retains count for nothing on their own: each is the count of the receiver right after - // it. `cur = cur.value.next` is counted only. - struct HandleReadUses - { - bool counted; - bool borrowed; - }; - - static HandleReadUses handleReadUses(mlir::Value value) - { - HandleReadUses result{false, false}; - llvm::SmallVector values{value}; - llvm::SmallPtrSet seen{value}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto &use : current.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - continue; - } - - // a return and an `any` box take the value, though passesOn would hand them on - if (isHandOff(user, current)) - { - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; - continue; - } - - llvm::SmallVector same; - if (passesOn(use, same)) - { - for (auto next : same) - { - // what is no longer a handle is no receiver: a test (`if (p.child)`) only - // reads it - if (!isHandle(next)) - { - result.borrowed = true; - } - else if (seen.insert(next).second) - { - values.push_back(next); - } - } - - continue; - } - - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; - } - } - - return result; - } - - // A handle read out of a place that rc gives a count of its own is a counted copy, not a borrow - // (spec 23.3) - but only when every use of it is one rc counts (Ruling 7), and a use that is - // not known to be one is not (Ruling 9). A read with one use that reads through the place as - // well (`f(p, q, q.child = p.child)`), or that keeps it with no count (`[(q.child = p.child), - // 1]`), is a borrow, and that use is tenure-checked; the counted uses are copies - // (checkPlaceRead). - static bool isCountedHandleRead(mlir::Value value) - { - auto uses = handleReadUses(value); - return uses.counted && !uses.borrowed; - } - - // A use that gives a handle an owner rc counts, with rc's retain for that very use: rc's - // retain itself, a store into a field, an element, a global, an owning local or the result - // slot (a return) that rc retained for (countedStoreRetain); a push or an unshift, `new - // Shared`, an array literal's element, an `any` box, a `ts.ReturnVal`, each right after rc's - // retain of what it takes (retainedRightBefore); or the initializer of an owning local, which - // takes a count of its own (`ts.RetainSlot`). Any of them without its retain is not one: it - // keeps the read without a count (`{ s: (q.child = p.child) }`'s field). - static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return true; - } - - if (auto varOp = mlir::dyn_cast(user)) - { - return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { - return mlir::isa(varUser); - }); - } - - if (auto storeOp = mlir::dyn_cast(user)) - { - return storeOp.getValue() == value && countedStoreRetain(storeOp); - } - - if (auto pushOp = mlir::dyn_cast(user)) - { - return retainedRightBefore(user, pushOp.getItems(), value); - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return retainedRightBefore(user, unshiftOp.getItems(), value); - } - - if (auto castOp = mlir::dyn_cast(user)) - { - return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); - } - - // `ts.ReturnVal` is the return before the affine lowering, which makes it a store into - // the result slot - if (mlir::isa(user)) - { - return retainedRightBefore(user, user->getOperands(), value); - } - - return false; - } - - // A use that passesOn hands on but that takes the value: a return's store into the function's - // result slot (isResultSlot), and a cast that boxes it into `any`. - static bool isHandOff(mlir::Operation *user, mlir::Value value) - { - if (auto storeOp = mlir::dyn_cast(user)) - { - auto slot = storeOp.getReference().getDefiningOp(); - return storeOp.getValue() == value && slot && isResultSlot(slot); - } - - auto castOp = mlir::dyn_cast(user); - return castOp && castOp.getIn() == value && mlir::isa(castOp.getType()); - } - - // A return or an `any` box of a handle that rc counts (walkBorrowed). - static bool isCountedHandOff(mlir::Operation *user, mlir::Value value) - { - return isHandOff(user, value) && isCountedHandleReceiver(user, value); - } - - // rc's retains for what `op` takes: MLIRGen puts them in a row right before it - // (mlirGenRetainCaptured, retainInsertedElements), one for each operand it does not take - // over. Answers whether that row retains `value` once for each time `operands` hold it. A - // retain is right before one op only, so no other receiver can count it as well. - static bool retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value) - { - auto needed = llvm::count(operands, value); - if (needed == 0) - { - return false; - } - - for (auto *previous = op->getPrevNode(); needed > 0 && previous; previous = previous->getPrevNode()) - { - auto retainOp = mlir::dyn_cast(previous); - if (!retainOp) - { - break; - } - - if (retainOp.getReference() == value) - { - --needed; - } - } - - return needed == 0; - } - - // The releases of the old value that rc's counted stores of a handle read make: `ts.Retain` of - // the value, `ts.ReleaseSlot` of the slot, `ts.Store` of the value into it, in a row. Seen - // through the read's casts, merges and locals that own nothing, as handleReadUses sees it. - static void collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases) - { - llvm::SmallVector values{value}; - llvm::SmallPtrSet seen{value}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto &use : current.getUses()) - { - llvm::SmallVector same; - if (passesOn(use, same)) - { - for (auto next : same) - { - if (isHandle(next) && seen.insert(next).second) - { - values.push_back(next); - } - } - - continue; - } - - auto storeOp = mlir::dyn_cast(use.getOwner()); - if (!storeOp || storeOp.getValue() != current || !countedStoreRetain(storeOp)) - { - continue; - } - - if (auto releaseSlotOp = mlir::dyn_cast_or_null(storeOp->getPrevNode())) - { - releases.insert(releaseSlotOp); - } - } - } - } - - // rc's retain for a store of a value, when it made one: `ts.Retain` of the value right before - // the store, or before the `ts.ReleaseSlot` of the old value right before it - - // `Retain(v)`, [`ReleaseSlot(slot)`], `Store(v, slot)`. A return's store into the result - // slot comes after the scope exit's releases of the frame's locals and their slots: - // `Retain(v)`, [`Release(..)`, `ReleaseSlot(..)`...], `Store(v, result)`. Null for a store rc - // counts nothing for. - static mlir::Operation *countedStoreRetain(mlir_ts::StoreOp storeOp) - { - auto *previous = storeOp->getPrevNode(); - if (auto slot = storeOp.getReference().getDefiningOp(); slot && isResultSlot(slot)) - { - // the scope exit's releases, of locals and of their slots, in any order - while (mlir::isa_and_nonnull(previous) && - previous->getOperand(0) != storeOp.getValue()) - { - previous = previous->getPrevNode(); - } - } - else if (auto releaseSlotOp = mlir::dyn_cast_or_null(previous)) - { - previous = releaseSlotOp.getSlot() == storeOp.getReference() ? releaseSlotOp->getPrevNode() : nullptr; - } - - auto retainOp = mlir::dyn_cast_or_null(previous); - return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; - } - - // The call a value is the result of, when that result is a plain borrow of an argument. A borrowing - // record (spec 32) is not one: the caller owns the box (checkRecordResults). - static mlir::Operation *borrowingCall(mlir::Value value) - { - auto *def = value.getDefiningOp(); - if (!def || !isCall(def) || resultBorrows(def) < 0) - { - return nullptr; - } - - auto args = callArgs(def); - auto indices = resultBorrowsAll(def); - return llvm::all_of(indices, [&](int index) { return static_cast(index) < args.size(); }) ? def : nullptr; - } - - // The places from a read up to its roots, and the roots: the values the function reached the - // first container through. A container reached through a merge (`(c ? a : b).x`) has a root - // on each way in. - struct Chain - { - enum Kind - { - Slot, // an owning local, or a value-type local reached by reference - Owned, // a value this function made - NotOwned // a parameter, `this`, a global, anything else - }; - - // a field's position and reference type; an element is position -1, a handle's payload - // (`s.value`) is SharedValue, a field reached through an interface is InterfaceField: its - // position in the object is not known here (spec 28) - llvm::SmallVector> places; - static constexpr int64_t SharedValue = -2; - static constexpr int64_t InterfaceField = -3; - llvm::SmallVector> roots; - // The roots that are handles - what a `s.value` was read through - each also NotOwned - // among `roots` (spec 23.3). Known from the op, not the root's type: a view may hide it. - llvm::SmallVector handles; - // Somewhere under a root, but where is not known: a result that borrows an argument may - // be any field or element under it. Every overwrite and every removal drops it. - bool anyPlace = false; - - bool owned() const - { - return llvm::none_of(roots, [](auto &root) { return root.second == NotOwned; }); - } - }; - - // With `place`, the chain of that place itself: where a store into it moves a value (spec 29). - Chain chainOf(mlir::Operation *read, mlir::Value place = {}) - { - Chain chain; - llvm::SmallVector refs; - llvm::SmallVector first; - if (place) - { - refs.push_back(place); - } - else if (auto loadOp = mlir::dyn_cast(read)) - { - refs.push_back(loadOp.getReference()); - } - else - { - // a call's result that borrows its arguments - a plain borrow, or a borrowing record's box - // (spec 32), which may hold a borrow of each - is somewhere under each of them - chain.anyPlace = true; - auto args = callArgs(read); - for (auto index : resultBorrowsAll(read)) - { - if (static_cast(index) < args.size() && !argumentHoldsNoBlock(args[index]) && - !isImmortalLiteral(args[index])) - { - first.push_back(rootOf(args[index])); - } - } - } - - llvm::SmallPtrSet seen; - // the bases a `s.value` was read through, and what merges into them - llvm::SmallPtrSet handleBases; - while (!refs.empty() || !first.empty()) - { - llvm::SmallVector bases; - if (!first.empty()) - { - bases.swap(first); - } - else - { - auto ref = refs.pop_back_val(); - auto place = placeBase(ref); - assert(place && "chainOf walks places only"); - switch (place.kind) - { - case PlaceKind::Field: - chain.places.push_back({ref.getDefiningOp().getPosition(), ref.getType()}); - break; - case PlaceKind::SharedValue: - chain.places.push_back({Chain::SharedValue, ref.getType()}); - handleBases.insert(rootOf(place.base)); - break; - case PlaceKind::InterfaceField: - chain.places.push_back({Chain::InterfaceField, ref.getType()}); - break; - default: - chain.places.push_back({-1, ref.getType()}); - break; - } - - bases.push_back(rootOf(place.base)); - } - - while (!bases.empty()) - { - auto base = bases.pop_back_val(); - if (!seen.insert(base).second) - { - continue; - } - - // A handle (spec 23.3): other handles may reach its block, so this function owns - // nothing under it. A handle read out of a place still has that place above it, and - // one merged from several has each of them. - if (handleBases.contains(base) || isHandle(base)) - { - llvm::SmallVector merged; - if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, merged)) - { - handleBases.insert(merged.begin(), merged.end()); - bases.append(merged.begin(), merged.end()); - continue; - } - - chain.roots.push_back({base, Chain::NotOwned}); - chain.handles.push_back(base); - if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - refs.push_back(loadOp.getReference()); - } - - continue; - } - - if (isPlace(base)) - { - refs.push_back(base); // a field of a value-type field: the same object - continue; - } - - if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - refs.push_back(loadOp.getReference()); - continue; - } - - // the object a method is called on - if (auto object = boundThis(base)) - { - bases.push_back(rootOf(object)); - continue; - } - - // a result that borrows an argument is somewhere under the argument - if (auto *call = borrowingCall(base)) - { - chain.anyPlace = true; - auto args = callArgs(call); - for (auto index : resultBorrowsAll(call)) - { - if (static_cast(index) < args.size()) - { - bases.push_back(rootOf(args[index])); - } - } - - continue; - } - - if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, bases)) - { - continue; - } - - if (base.getDefiningOp()) - { - chain.roots.push_back({base, Chain::Slot}); - } - else if (auto slotLoad = slotLoadOf(base)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else - { - chain.roots.push_back({base, isFresh(base) ? Chain::Owned : Chain::NotOwned}); - } - } - } - - return chain; - } - - // Where the roots stop holding what the chain reads: a local's releases and assignments and - // every declaration or retain that may move it away; a made value's releases and every use - // that takes it; a global's assignments. - llvm::SmallVector rootEnds(const Chain &chain) - { - llvm::SmallVector ends; - for (auto [root, kind] : chain.roots) - { - // A handle (spec 23.3), never owned: where this one is given back or overwritten - a - // local's release and assignments, a temporary's release; a global's assignments are - // below. A copy of it is counted, and moves nothing away. - if (llvm::is_contained(chain.handles, root)) - { - auto loadOp = root.getDefiningOp(); - if (auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp()) - { - for (auto *user : varOp->getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - } - } - else if (!loadOp) - { - forEachUse(root, [&](mlir::Operation *use, mlir::Value) { - if (mlir::isa(use)) - { - ends.push_back(use); - } - }); - } - } - - if (kind == Chain::Slot) - { - for (auto *user : root.getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - else if (auto loadOp = mlir::dyn_cast(user)) - { - forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use) || (varOp && isOwningVariable(varOp)) || - (isCall(use) && isKeptArgument(use, used))) - { - ends.push_back(use); - } - }); - } - } - } - else if (kind == Chain::Owned) - { - forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { - if (mlir::isa(use) || !isBorrow(use, used)) - { - ends.push_back(use); - } - }); - } - else if (auto loadOp = root.getDefiningOp()) - { - if (auto global = loadOp.getReference().getDefiningOp()) - { - getFunction().walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto releaseSlotOp = mlir::dyn_cast(op)) - { - slot = releaseSlotOp.getSlot(); - } - else if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - - auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); - if (other && other.getGlobalName() == global.getGlobalName()) - { - ends.push_back(op); - } - }); - } - } - } - - return ends; - } - - // What a call has to be given to reach a root: the root, and where the root is a local - // declared from another (a borrow, or a move that left the other dead), that one too. - static llvm::SmallVector rootSources(const Chain &chain) - { - llvm::SmallVector sources; - for (auto &root : chain.roots) - { - auto value = root.first; - sources.push_back(value); - for (auto steps = 0; steps < 8; ++steps) - { - auto varOp = value.getDefiningOp(); - if (!varOp || !varOp.getInitializer()) - { - break; - } - - auto initializer = rootOf(varOp.getInitializer()); - auto slotLoad = slotLoadOf(initializer); - value = slotLoad ? slotLoad.getReference() : initializer; - sources.push_back(value); - } - } - - return sources; - } - - // Everything that may point into what `start` points to: the forward closure over users' - // results that may point anywhere. A call's result is not followed: the call hands back a - // value it owns. - const llvm::DenseSet &derivedFrom(mlir::Value start) - { - if (auto found = derivedCache.find(start); found != derivedCache.end()) - { - return *found->second; - } - - auto &slot = derivedCache[start]; - slot = std::make_shared>(); - auto &derived = *slot; - llvm::SmallVector work{start}; - while (!work.empty()) - { - auto value = work.pop_back_val(); - if (!derived.insert(value).second) - { - continue; - } - - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (isCall(user)) - { - // ...but a borrowing record given this value holds it (spec 32) - auto args = callArgs(user); - if (isRecordResult(user) && llvm::any_of(resultBorrowsAll(user), [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - })) - { - work.push_back(user->getResult(0)); - } - - continue; - } - - llvm::SmallVector next(user->getResults().begin(), user->getResults().end()); - passesOn(use, next); - for (auto result : next) - { - if (mayPointInto(result)) - { - work.push_back(result); - } - } - } - } - - return derived; - } - - // May this op of the function's drops destroy what the chain reads? An overwrite of a place - // on it, matched by position and type (two references to one field of one object always - // agree on both; an unrelated field that happens to agree only adds an error); an array op - // that removes elements of the type of an element on it; a call whose callee may destroy - // anything (`__own_no_drops`, phase 4) - any such call, for a root this function does not - // own, since the owner may be a global the callee assigns; else one given something derived - // from the root that the read did not produce. - // `borrowed`, when given, is the value the borrow holds in place of the read's result (a value - // moved into the place, spec 29), and `read` may then be null. - bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed = {}) - { - if (read && drop == read) - { - return false; // a call that returns a borrow has not destroyed it by returning it - } - - if (chain.anyPlace && !isCall(drop)) - { - return true; - } - - if (auto releaseSlotOp = mlir::dyn_cast(drop)) - { - // An assignment to a captured variable destroys its old value, which is reached only - // through a read of the variable's cell: a root this function does not own. - auto slot = releaseSlotOp.getSlot(); - if (isCellVariable(slot) || isLoadedCell(slot)) - { - return !chain.owned(); - } - - // An assignment to a global destroys its old value and all that value owns. A root this - // function does not own - `this`, a parameter, a global's or a field's value - may be - // that very block, or under it (spec 29). A root this function made cannot. An immortal - // global's one store is its initializer, whose old value is the empty one (spec 25). - if (auto addressOfOp = slot.getDefiningOp()) - { - return !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && !chain.owned(); - } - - // `x.value = ...` through any handle, of any type, may replace the payload another - // handle reads (spec 23.3): a handle converts to one of a base class's type - if (slot.getDefiningOp()) - { - return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); - } - - auto isField = [](int64_t position) { return position >= 0 || position == Chain::InterfaceField; }; - int64_t position = -1; - if (auto propertyRefOp = slot.getDefiningOp()) - { - position = propertyRefOp.getPosition(); - } - else if (isInterfaceField(slot)) - { - position = Chain::InterfaceField; - } - - // A field reached through an interface may be any field of the object under it, of any - // type (a class may declare the field narrower than the interface): an overwrite through - // an interface drops every field on the chain, and any field's overwrite drops a field - // read through an interface (spec 28). - return llvm::any_of(chain.places, [&](auto &place) { - if (isField(position) && isField(place.first) && - (position == Chain::InterfaceField || place.first == Chain::InterfaceField)) - { - return true; - } - - return place.first == position && place.second == slot.getType(); - }); - } - - if (isCall(drop)) - { - if (callNoDrops(drop)) - { - return false; - } - - // a call given a closure may run it, and its box may hold anything - if (!chain.owned() || llvm::any_of(drop->getOperands(), holdsCaptures)) - { - return true; - } - - auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); - auto sources = rootSources(chain); - return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { - return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { - return derivedFrom(source).contains(operand); - }); - }); - } - - // pop, shift, splice, `length =`; push and unshift on an array reached through a handle - auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); - auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); - if (!arrayType) - { - return true; - } - - return llvm::any_of(chain.places, [&](auto &place) { - auto placeType = mlir::dyn_cast(place.second); - return place.first == -1 && placeType && placeType.getElementType() == arrayType.getElementType(); - }); - } - - // A read out of a container is a borrow of it (spec 2.3), and so is a `let` declared from one: - // its retain and releases go when the check passes, as for any borrowing `let` (phase 2). - // Reports and returns false when the read is kept, or used after something that may destroy - // it. - bool checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase) - { - auto loadOp = mlir::dyn_cast(read); - auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); - auto result = read->getResult(0); - // A handle read rc counts (spec 23.3): its counted uses are copies (walkBorrowed), and the - // release of the old value a counted store makes, right before it stores the retained - // read, cannot free the block read: it holds the retain's count by then - // (`Shared.count(q.child = p.child)`). - auto countedHandle = isHandle(result) && handleReadUses(result).counted; - llvm::SmallPtrSet countedStoreReleases; - if (countedHandle) - { - collectCountedStoreReleases(result, countedStoreReleases); - } - - llvm::StringRef name = ownerName(result); - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - mlir_ts::VariableOp letKeeps; - mlir::Operation *letKept = nullptr; - // A result that borrows arrives with rc's temporary release, or rc's consuming `let`, and - // in a function returning a borrow of the same parameter it may go out with rc's retain. - Keeping keeping{&bookkeeping, returnsBorrowOf >= 0 && borrowedParam(result) == returnsBorrowOf}; - auto *kept = walkBorrowed(result, read, uses, [&](mlir::Operation *user, - llvm::ArrayRef kills) { - auto borrower = borrowerOf(user, /*consumed=*/true); - if (!borrower || borrower.getOperation() != user) - { - return false; - } - - name = varName(borrower); - if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) - { - letKept = keeps; - letKeeps = borrower; - } - - return true; - }, keeping, countedHandle); - - if (kept || letKept) - { - if (!kept && isAssignmentOf(letKept, letKeeps)) - { - reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); - } - else - { - reportPlaceBorrowEscapes(kept ? kept : letKept, name, place); - } - - return false; - } - - auto chain = chainOf(read); - // A call given the borrow is a use of it for as long as the callee runs, so a call that - // may destroy it is a use after that, whatever the order of its arguments. The function - // value it goes through is only read as it starts. - auto outlives = [&](mlir::Operation *drop) { - for (auto &use : uses) - { - if ((use.op != drop || !use.callee) && reachableAfter(drop, use.op, use.kills)) - { - reportPlaceBorrowOutlives(use.op, drop, name, place); - return true; - } - } - - return false; - }; - - for (auto *end : rootEnds(chain)) - { - if (outlives(end)) - { - return false; - } - } - - if (keptParamOutlives(chain, outlives)) - { - return false; - } - - for (auto *drop : drops) - { - if (countedStoreReleases.contains(drop)) - { - continue; - } - - if (dropsChain(drop, chain, read) && outlives(drop)) - { - return false; - } - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - return true; - } - - // A local that holds a borrow through `user`, and the field path under it that holds it (empty: - // the local's value itself): a store of it into the local, or into a field of the local's value - // tuple at any depth (an object literal built in place), or the local declared from it. - struct BorrowHolder - { - mlir_ts::VariableOp local; - llvm::SmallVector path; - }; - - static std::optional borrowHolderOf(mlir::Operation *user) - { - BorrowHolder holder; - mlir::Value ref; - if (auto storeOp = mlir::dyn_cast(user)) - { - ref = storeOp.getReference(); - while (auto propertyRefOp = ref.getDefiningOp()) - { - holder.path.insert(holder.path.begin(), propertyRefOp.getPosition()); - ref = propertyRefOp.getObjectRef(); - } - } - else if (auto varOp = mlir::dyn_cast(user)) - { - ref = varOp.getResult(); - } - - auto varOp = ref ? ref.getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp || varOp.getCaptured().value_or(false) || isCellVariable(varOp.getResult())) - { - return std::nullopt; - } - - holder.local = varOp; - return holder; - } - - // Where the holder is given a new value: the local's declaration when it has an initializer - // (`let l!: V` stores nothing, and in a loop the slot keeps the last round's value: fix-up - // re-review N4), and every store into the field path that holds the borrow or into a prefix of - // it (the whole local, `o.p` for `o.p.v`). A store into a sibling field, or below the path, - // gives the path no new value. - static llvm::SmallVector holderRenewals(BorrowHolder holder) - { - llvm::SmallVector renewals; - if (holder.local.getInitializer()) - { - renewals.push_back(holder.local); - } - - llvm::SmallVector> refs{{holder.local.getResult(), 0}}; - while (!refs.empty()) - { - auto [ref, depth] = refs.pop_back_val(); - for (auto *refUser : ref.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(refUser)) - { - if (depth < holder.path.size() && propertyRefOp.getPosition() == holder.path[depth]) - { - refs.push_back({propertyRefOp.getResult(), depth + 1}); - } - } - else if (auto storeOp = mlir::dyn_cast(refUser); storeOp && storeOp.getReference() == ref) - { - renewals.push_back(storeOp); - } - } - } - - return renewals; - } - - // Whether one field path is on the way to the other: a read of either may reach the other's value. - static bool pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b) - { - auto common = std::min(a.size(), b.size()); - return a.take_front(common) == b.take_front(common); - } - - // A read of what a mortal global holds (spec 25: an immortal one's one store is its - // initializer): a heap value it owns, not a handle (spec 23.3 counts those). - bool isGlobalRead(mlir_ts::LoadOp loadOp) - { - auto addressOfOp = loadOp.getReference().getDefiningOp(); - auto result = loadOp.getResult(); - return addressOfOp && !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && ownsHeap(result) && !isHandle(result); - } - - // Such a read is a borrow of the value the global owns - `const c = K.w` names the read itself, - // with no retain - and an assignment of the global releases it. So no use of the read, nor of - // what it produces (walkBorrowed), may run after one in this function. A path back to a use - // through the read itself is a new read. A call that may assign the global, itself or through - // its callees, is such an assignment too (follow-up 11: `const c = G; reset(); print(c.x)`); the - // read given to that call as an argument is #639's. (C1 of the fix-up review: `const c = K.w; K.w = new V(i + 1); print(c.x)` in a loop - // read the block the assignment freed.) - // - // Where the walk meets a keep, the read is either followed into what holds it or rejected - - // never let go unchecked (fix-up re-review N2: a kept read is not always another check's). A - // local holds it (borrowHolderOf): an object literal built in place (`const o = { v: c }`, - // `{ p: { v: K.w } }`), a `let` or a destructured local declared or assigned from it - // (`let l = o.v`, `let { v } = o`). Every use of that local under the path that holds the borrow - // - its reads, a copy, a spread (`ts.DeconstructTuple`), what those lead to - is a use of the - // read, made again only where the local is given a new value (holderRenewals): never at the - // read, which renews `c` and not `o.v` - a holder declared before a loop keeps the old borrow - // across the back edge until it is stored again (fix-up re-review N3). The local's own retain - // and releases are its bookkeeping, which the owner checks decide. A sibling field never given - // the borrow is not followed. A value made from the borrow and stored into another local makes - // that one a holder too. Any other keep - a field of an object, an array, a global, a callee - // that keeps it, a capture, a return - cannot be bounded here and is an error. A string read is - // copied where it is kept (spec 22), so its keeps are not this check's. - void checkGlobalRead(mlir_ts::LoadOp read) - { - // each access of a global is a `ts.AddressOf` of its own - auto global = read.getReference().getDefiningOp().getGlobalName(); - llvm::SmallVector uses; - llvm::StringSet<> followed; - mlir::Operation *keptAt = nullptr; - // rc's temporary release of a call's result that borrows an argument goes with that result - // (walkBorrowed's `keeping`); the owner checks decide it - llvm::SmallVector bookkeeping; - Keeping keeping{&bookkeeping, false}; - std::function)> intoLocal; - - // the uses of what `holder` holds, made again at its renewals: those depend on the holder - // alone, not on the way the walk reached it, so a holder is followed once. A value loaded - // out of it is made again only by a path back through that load (the load itself is a use, - // bounded by the renewals): a store into the path after the load gives the loaded value - // nothing new. - auto followHolder = [&](BorrowHolder holder) { - std::string key; - llvm::raw_string_ostream keyOut(key); - keyOut << holder.local.getOperation(); - for (auto position : holder.path) - { - keyOut << "." << position; - } - - if (!followed.insert(keyOut.str()).second) - { - return true; - } - - auto localKills = holderRenewals(holder); - llvm::SmallVector>> refs{{holder.local.getResult(), {}}}; - while (!refs.empty()) - { - auto [ref, path] = refs.pop_back_val(); - for (auto *refUser : ref.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(refUser)) - { - auto fieldPath = path; - fieldPath.push_back(propertyRefOp.getPosition()); - if (pathsMeet(fieldPath, holder.path)) - { - refs.push_back({propertyRefOp.getResult(), fieldPath}); - } - - continue; - } - - if (ref == holder.local.getResult() && - mlir::isa(refUser)) - { - continue; - } - - uses.push_back({refUser, {localKills.begin(), localKills.end()}}); - if (auto loadOp = mlir::dyn_cast(refUser)) - { - if (auto *kept = walkBorrowed(loadOp.getResult(), loadOp.getOperation(), uses, intoLocal, keeping)) - { - keptAt = keptAt ? keptAt : kept; - return false; - } - } - } - } - - return true; - }; - - intoLocal = [&](mlir::Operation *user, llvm::ArrayRef kills) { - // a tuple taken apart (`{ ...b }`, a destructuring): each field is the borrow again - if (mlir::isa(user)) - { - for (auto result : user->getResults()) - { - if (!mayPointInto(result)) - { - continue; - } - - if (auto *kept = walkBorrowed(result, kills, uses, intoLocal, keeping)) - { - keptAt = keptAt ? keptAt : kept; - return false; - } - } - - return true; - } - - auto holder = borrowHolderOf(user); - return holder && followHolder(*holder); - }; - - auto *kept = walkBorrowed(read.getResult(), read.getOperation(), uses, intoLocal, keeping); - - // a value made from the borrow (a field of a tuple read out of a holder, `{ ...b }`) stored - // into another local, or declaring one: that local holds the borrow too - for (size_t index = 0; !kept && !keptAt && index < uses.size(); index++) - { - auto *op = uses[index].op; - auto storeOp = mlir::dyn_cast(op); - auto varOp = mlir::dyn_cast(op); - auto value = storeOp ? storeOp.getValue() : varOp ? varOp.getInitializer() : mlir::Value(); - if (!value || !mayPointInto(value)) - { - continue; - } - - if (auto holder = borrowHolderOf(op)) - { - followHolder(*holder); - } - } - - if (kept || keptAt) - { - if (mlir::isa(read.getType())) - { - return; - } - - if (reporting()) - { - auto *at = keptAt ? keptAt : kept; - at->emitError("the value read from '") - << global << "' is kept where its end at an assignment of '" << global - << "' cannot be checked; -mm=own cannot prove it outlives the global's value"; - signalPassFailure(); - } - - return; - } - - llvm::SmallVector ends; - getFunction().walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto releaseSlotOp = mlir::dyn_cast(op)) - { - slot = releaseSlotOp.getSlot(); - } - else if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - - auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); - if (other && other.getGlobalName() == global) - { - ends.push_back(op); - } - - // a call that may give the global a new value (OwnershipSignaturePass, pinAssignedGlobals): - // `const c = G; reset(); print(c.x)` - if (isCall(op)) - { - auto assigned = op->getAttrOfType(OWN_ASSIGNS_GLOBALS_ATTR_NAME); - if (op->hasAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME) || - (assigned && llvm::any_of(assigned.getAsRange(), - [&](mlir::StringAttr name) { return name.getValue() == global; }))) - { - ends.push_back(op); - } - } - }); - - for (auto *end : ends) - { - for (auto &use : uses) - { - if (use.op != end && reachableAfter(end, use.op, use.kills)) - { - reportBorrowOutlives(use.op, end, ownerName(read.getResult()), global); - return; - } - } - } - } - - // A borrow read out of a parameter this function keeps, taken before the parameter moves into a - // new owner (spec 33, review fix): from the move on it is bounded by that owner, as a read through - // the owner would be (spec 29) - by the owner's ends and the drops of its place after the move. - // `const inner = p.v; let r = p; r = new N(...); inner.x` reads what the assignment freed. A move - // into anything that is not such a place (a call that keeps it, a return, a push) ends the borrow - // there. So do the releases of the parameter's slot (spec 33's exit releases). True when some use - // of the borrow comes after one of these (`outlives` reported it). - bool keptParamOutlives(const Chain &chain, llvm::function_ref outlives) - { - for (auto &root : chain.roots) - { - auto load = keptParamLoadOf(root.first); - if (!load) - { - continue; - } - - // the moves of the parameter: what takes a read of its slot - llvm::SmallVector> takers; - for (auto *user : load.getReference().getUsers()) - { - if (mlir::isa(user)) - { - if (outlives(user)) - { - return true; - } - - continue; - } - - auto readOp = mlir::dyn_cast(user); - if (!readOp) - { - continue; - } - - forEachUse(readOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use)) - { - auto *taker = takerOf(use, used); - takers.push_back({taker ? taker : use, used}); - } - else if ((varOp && isOwningVariable(varOp)) || (isCall(use) && isKeptArgument(use, used))) - { - takers.push_back({use, used}); - } - }); - } - - for (auto [taker, moved] : takers) - { - Chain owner; - if (!destinationChain(taker, owner)) - { - if (outlives(taker)) - { - return true; - } - - continue; - } - - auto afterMove = [&](mlir::Operation *end) { - return end != taker && reachableAfter(taker, end, llvm::ArrayRef{}) && outlives(end); - }; - - for (auto *end : rootEnds(owner)) - { - if (afterMove(end)) - { - return true; - } - } - - for (auto *drop : drops) - { - if (dropsChain(drop, owner, nullptr, moved) && afterMove(drop)) - { - return true; - } - } - } - } - - return false; - } - - // A parameter, or a view of one (`u`, the payload `ts.Unbox` reads out of an `any`), that - // this function returns as a borrow (`__own_result_borrows`). rc retained it for the caller, - // who now takes no reference: the retain goes, and nothing may keep it. The caller's own - // borrow of the argument bounds it; nothing here can end that but a call, and a call that may - // is the caller's use of what it passed (spec 2.4, phase 4). - void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase) - { - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - if (auto *kept = walkBorrowed(value, {}, uses, nullptr, Keeping{&bookkeeping, true})) - { - reportBorrowEscapes(kept, ownerName(value), "a parameter"); - return; - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - } - - // `the result of 'first'`, for a call whose result borrows an argument. - static std::string describeCall(mlir::Operation *call) - { - auto callee = calleeName(call); - return callee.empty() ? "a call's result" : "the result of '" + callee + "'"; - } - - // The function a call names - directly, or the method it calls through a vtable - or empty. - static std::string calleeName(mlir::Operation *call) - { - mlir::StringAttr callee; - auto value = calleeValue(call); - if (auto symbol = directCallee(call)) - { - callee = symbol.getAttr(); - } - else if (auto calleeOp = value ? value.getDefiningOp() : nullptr) - { - if (auto getMethodOp = mlir::dyn_cast(calleeOp)) - { - calleeOp = getMethodOp.getBoundFunc().getDefiningOp(); - } - - if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - // a method reached through an interface (spec 31): `.` - auto method = mlir::dyn_cast(refOp.getIdentifier()); - auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); - if (method && interfaceType) - { - return (interfaceType.getName().getValue() + "." + method.getValue()).str(); - } - } - } - - return callee ? callee.getValue().str() : std::string(); - } - - // `'h.c'`, `'arr[]'`, `'h.c.v'` - or `a field`, `an element` when the root has no name (a - // temporary, or no --di). - std::string describePlace(mlir::Value ref) - { - auto path = pathOf(ref); - if (path.empty()) - { - return ref.getDefiningOp() ? "an element" : "a field"; - } - - return "'" + path + "'"; - } - - std::string pathOf(mlir::Value ref) - { - auto place = placeBase(ref); - std::string step; - switch (place.kind) - { - case PlaceKind::Field: { - auto field = fieldName(ref.getDefiningOp()); - if (field.empty()) - { - return {}; - } - - step = "." + field; - break; - } - case PlaceKind::Element: - step = "[]"; - break; - case PlaceKind::SharedValue: - step = ".value"; - break; - case PlaceKind::InterfaceField: { - auto field = mlir::dyn_cast(ref.getDefiningOp().getIdentifier()); - if (!field) - { - return {}; - } - - step = "." + field.getValue().str(); - break; - } - case PlaceKind::None: - return {}; - } - - auto base = rootOf(place.base); - std::string prefix; - if (isPlace(base)) - { - prefix = pathOf(base); - } - else if (auto loadOp = base.getDefiningOp()) - { - auto from = loadOp.getReference(); - if (isPlace(from)) - { - prefix = pathOf(from); - } - else if (auto varOp = from.getDefiningOp()) - { - prefix = varName(varOp).str(); - } - else if (auto global = from.getDefiningOp()) - { - prefix = global.getGlobalName().str(); - } - } - else if (auto varOp = base.getDefiningOp()) - { - prefix = varName(varOp).str(); - } - else - { - prefix = ownerName(base).str(); - } - - // a name MLIRGen made up (`.a`, the array a `for...of` walks) means nothing to the reader - if (prefix.empty() || prefix == "this value" || prefix.front() == '.') - { - return {}; - } - - return prefix + step; - } - - static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp) - { - auto type = propertyRefOp.getObjectRef().getType(); - if (auto refType = mlir::dyn_cast(type)) - { - type = refType.getElementType(); - } - - if (auto classType = mlir::dyn_cast(type)) - { - type = classType.getStorageType(); - } - - auto position = propertyRefOp.getPosition(); - mlir::Attribute id; - if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) - { - id = storageType.getId(position); - } - else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) - { - id = tupleType.getId(position); - } - - if (auto name = mlir::dyn_cast_or_null(id)) - { - return name.str(); - } - - if (auto name = mlir::dyn_cast_or_null(id)) - { - return name.getValue().str(); - } - - return {}; - } - - // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it - // has a ts.DebugVariable, whose location carries the name. - static llvm::StringRef ownerName(mlir::Value value) - { - llvm::StringRef name = "this value"; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user)) - { - name = nameAt(user->getLoc()); - } - }); - - return name; - } - - // A `ts.RetainSlot`'s slot holds its initializer where the retain is. MLIRGen puts the retain at - // the declaration (takeOwnershipOfLocal), and nothing between the two may write the slot: in - // their block, no op is given the slot, no call runs, and no op has regions (a store could hide - // in them). What is stored into the slot later is decided at its own store; this retain takes - // only the initializer. No closure captures it. - static bool slotHoldsInitializerAt(mlir::Operation *op) - { - auto slot = mlir::cast(op).getSlot(); - auto varOp = slot.getDefiningOp(); - if (!varOp || !varOp.getInitializer() || isCellVariable(slot) || varOp->getBlock() != op->getBlock()) - { - return false; - } - - for (auto *between = varOp->getNextNode(); between; between = between->getNextNode()) - { - if (between == op) - { - return true; - } - - if (llvm::is_contained(between->getOperands(), slot) || isCall(between) || between->getNumRegions() > 0) - { - return false; - } - } - - return false; - } - - // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer, - // seen through its views. None for a variable with no initializer - its storage was hoisted - // in front of a try and its value arrives by a store this phase does not follow. - static mlir::Value retainedValue(mlir::Operation *op) - { - if (auto retainOp = mlir::dyn_cast(op)) - { - return rootOf(retainOp.getReference()); - } - - auto varOp = mlir::cast(op).getSlot().getDefiningOp(); - return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); - } - - // Does a value of this type own a block that a release would destroy? - bool ownsHeap(mlir::Value value) - { - // a `value_ref` points at storage it does not own, but one `ts.New` made and seen as its - // object is the object's block: the root of a generator's state object or an object literal - if (value.getDefiningOp() && mlir::isa(value.getType()) && - llvm::any_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user) && isView(user); })) - { - return true; - } - - return ownsHeap(value.getLoc(), value.getType()); - } - - bool ownsHeap(mlir::Location location, mlir::Type type) - { - return own_facts::ownsHeap(location, type); - } - - // A tuple read out of a local that a `ts.Constant` initializes, whose owning fields hold what - // that constant put there - null, a number, an immortal literal - or a fresh value moved in: - // the initial state a generator's maker copies into its state object, or a record built to be - // returned (`return {value: new C(), done: false}`, a `yield`'s result). rc's retain of it is - // its reference for the values moved in, which own moves instead (recordRetainOf). - bool holdsConstantsOrMoves(mlir::Value value) - { - auto loadOp = value.getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - // a borrowing record (spec 32) takes nothing for what it borrows, and its fresh values move in - // as an owning record's do - if (!varOp || !isConstantRecordLocal(varOp) || !(isOwningRecord(value) || borrowingRecordOf(varOp))) - { - return false; - } - - // a fresh value moved in, its move decided by its own check against this retain - auto movesIn = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - return false; - } - - // a method's field is a `!ts.bound_ref`, which holds a function, not a block - auto refType = mlir::dyn_cast(propertyRefOp.getType()); - if (refType && !ownsHeap(propertyRefOp.getLoc(), refType.getElementType())) - { - return false; - } - - return llvm::any_of(propertyRefOp->getUsers(), - [](mlir::Operation *fieldUser) { return mlir::isa(fieldUser); }); - }); - - return !movesIn || recordRetainOf(varOp) != nullptr; - } - - // rc's one `ts.Retain` of a record local's one read, where the read is only returned: stored - // into a local that owns nothing and is read only by returns (the result slot), or copied into - // the box it is given in as an object or an interface (spec 27). It is the reference rc gives - // each fresh value stored into the record, so it is where they move in. Null for any other - // record. - static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp) - { - if (!isConstantRecordLocal(varOp)) - { - return nullptr; - } - - mlir_ts::LoadOp read; - for (auto *user : varOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - if (read) - { - return nullptr; - } - - read = loadOp; - } - } - - if (!read) - { - return nullptr; - } - - mlir::Operation *retain = nullptr; - auto boxed = false; - auto other = false; - for (auto *user : read->getUsers()) - { - if (mlir::isa(user)) - { - if (retain) - { - return nullptr; - } - - retain = user; - continue; - } - - // copied into the block it is given in as an object or an interface (spec 27): the box - // owns what the record held, and its own check decides where the box goes - auto storeOp = mlir::dyn_cast(user); - auto box = storeOp && storeOp.getValue() == read.getResult() ? recordBoxOf(storeOp.getReference()) - : mlir_ts::NewOp(); - if (box && box.getResult() == storeOp.getReference()) - { - if (boxed) - { - return nullptr; - } - - boxed = true; - continue; - } - - other = true; - if (mlir::isa(user)) - { - continue; - } - - // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's - // `ts.RetainSlot` of that local is the record's retain - if (auto ownerOp = mlir::dyn_cast(user)) - { - auto *slotRetain = ownerOp.getInitializer() == read.getResult() && isOwningVariable(ownerOp) - ? onlyRetainSlotOf(ownerOp) - : nullptr; - if (!slotRetain || retain) - { - return nullptr; - } - - retain = slotRetain; - continue; - } - - // stored into the result slot, or into an owning local (`a = { item: new C() }`) - auto slot = storeOp && storeOp.getValue() == read.getResult() - ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - if (!slot || !(isResultSlot(slot) || isOwningVariable(slot))) - { - return nullptr; - } - } - - // a boxed record goes nowhere else: the box is its one owner - if (boxed && other) - { - return nullptr; - } - - return retain; - } - - // `const b = [...a, ...a]`: MLIRGen builds the array in a local of its own that owns nothing, - // starting from a fresh one and pushing into it, and reads it once, when it is done, into the - // owning local it is made for. That local's `ts.RetainSlot` (`retain`) is the reference: the - // fresh array moves into the builder and then into the owner, and nothing else holds it. - bool isBuiltArrayRead(mlir::Operation *retain, mlir::Value value) - { - auto read = value.getDefiningOp(); - auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); - auto owner = mlir::cast(retain).getSlot().getDefiningOp(); - if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || - !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp()) || - !owner || owner.getInitializer() != read.getResult() || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain) - { - return false; - } - - // only filled, and only before it is read - return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (user == read) - { - return true; - } - - return mlir::isa(user) && use.getOperandNumber() == 0 && - !reachableAfter(read, user, builder); - }); - } - - // `return [...a]`: the builder of isBuiltArrayRead, read once, when it is done, and returned. - // The return's `ts.Retain` (`retain`) is the reference the caller gets: the fresh array moves - // into the builder and then out of the function, and nothing else holds it. The builder's - // other reads only bind its `push` (`ts.ThisSymbolRef @__array_push`) or are not used. A store - // into a field is the same move, into a container: `h.c = [...a]`, and a generator's local - // (`const groups = [...result]`), which is a field of its state object. - bool isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value) - { - auto read = value.getDefiningOp(); - auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || - !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp())) - { - return false; - } - - // the read is retained and returned - stored into the function's result slot - and nothing else - auto returned = false; - for (auto *user : read->getUsers()) - { - if (user == retain) - { - continue; - } - - auto storeOp = mlir::dyn_cast(user); - auto resultSlot = storeOp && storeOp.getValue() == read.getResult() - ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - auto intoField = storeOp && storeOp.getValue() == read.getResult() && - storeOp.getReference().getDefiningOp(); - if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) - { - return false; - } - - returned = true; - } - - if (!returned) - { - return false; - } - - // only filled, and only before it is read - return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (user == read) - { - return true; - } - - if (reachableAfter(read, user, builder)) - { - return false; - } - - if (auto loadOp = mlir::dyn_cast(user)) - { - return llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { - return mlir::isa(loadUser) && loadUser->use_empty(); - }); - } - - return mlir::isa(user) && use.getOperandNumber() == 0; - }); - } - - // The one `ts.RetainSlot` of a local; null when there are none or several. - static mlir::Operation *onlyRetainSlotOf(mlir_ts::VariableOp varOp) - { - mlir::Operation *found = nullptr; - for (auto *user : varOp->getUsers()) - { - if (mlir::isa(user)) - { - if (found) - { - return nullptr; - } - - found = user; - } - } - - return found; - } - - // A local that owns nothing, is never captured, and is read only to be returned. - static bool isResultSlot(mlir_ts::VariableOp varOp) - { - if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - return false; - } - - return llvm::all_of(varOp->getUsers(), [&](mlir::Operation *user) { - if (auto storeOp = mlir::dyn_cast(user)) - { - return storeOp.getReference() == varOp.getResult(); - } - - auto loadOp = mlir::dyn_cast(user); - return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { - return mlir::isa(loadUser); - }); - }); - } - - // The record whose field `taker` stores into, and rc's retain that moves the stored value in; - // null when `taker` is not such a store. - static mlir::Operation *recordRetainOfStore(mlir::Operation *taker) - { - auto storeOp = mlir::dyn_cast(taker); - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto varOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - auto *retain = varOp ? recordRetainOf(varOp) : nullptr; - if (!retain || retain->getBlock() != taker->getBlock() || !taker->isBeforeInBlock(retain)) - { - return nullptr; - } - - return retain; - } - - // A string constant cast to `string` is the global itself, which a release skips; so is a read - // of an immortal global (spec 25). - static bool isImmortalLiteral(mlir::Value value) - { - if (isImmortalGlobalRead(value)) - { - return true; - } - - auto castOp = rootOf(value).getDefiningOp(); - return castOp && mlir::isa(castOp.getType()) && - castOp.getIn().getDefiningOp(); - } - - // A use that reads the value without keeping it. Kept deliberately short: anything not listed - // is treated as taking the value, which can only turn a program into an error. - static bool isBorrow(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return true; - } - - // `assert(c, message)` prints the message when it fails, and keeps it either way - if (mlir::isa(user)) - { - return true; - } - - // `s.value`'s place and `Shared.count(s)` read the handle and keep nothing (spec 23.1) - if (mlir::isa(user)) - { - return true; - } - - // `--di`'s record of a folded const's value for the debugger: names it, keeps nothing - if (mlir::isa(user)) - { - return true; - } - - // a test - `if (d)`, `d?.x`, `typeof u` - asks about the value, and keeps nothing - if (mlir::isa(user)) - { - return true; - } - - // ...and so does a cast to `null` or `undefined`, which holds no block: `--di` records the - // `r` that `if (r == null)` narrows as one - if (auto castOp = mlir::dyn_cast(user)) - { - return mlir::isa(castOp.getType()) || - castOp.getType().isInteger(1) || isBoundForCall(castOp); - } - - if (mlir::isa(user)) - { - return isBoundForCall(user); - } - - // arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee - // with no facts that keeps one retains it, and that retain is its own error - if (isCall(user)) - { - return !isKeptArgument(user, value); - } - - // the array being pushed onto is written, not taken; what is inserted is taken - if (auto pushOp = mlir::dyn_cast(user)) - { - return pushOp.getOp() == value && !llvm::is_contained(pushOp.getItems(), value); - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return unshiftOp.getOp() == value && !llvm::is_contained(unshiftOp.getItems(), value); - } - - if (auto spliceOp = mlir::dyn_cast(user)) - { - return spliceOp.getOp() == value && !llvm::is_contained(spliceOp.getItems(), value); - } - - // a declaration that does not own what it holds only reads it - if (auto varOp = mlir::dyn_cast(user)) - { - return !isOwningVariable(varOp); - } - - // the box of a closure that borrows what it captures holds a copy that owns nothing; the - // closure's own uses are the borrow's (walkBorrowed, decideClosures) - if (mlir::isa(user) && user->hasAttr(OWN_CAPTURE_BORROW_ATTR_NAME)) - { - return true; - } - - // a store through a `ts.New`'d `value_ref` fills its block and keeps nothing of it - if (auto storeOp = mlir::dyn_cast(user)) - { - return mlir::isa(value.getType()) && storeOp.getReference() == value && - storeOp.getValue() != value; - } - - return false; - } - - // `ts.CreateBoundFunction(ts.Cast(object), method)` split straight back into `ts.GetThis` and - // `ts.GetMethod` for a call: a method of another module's class - its constructor, when it is - // built with `new` - called on an object here. It reads the object as `ts.ThisSymbolRef` does - // for a method of this module; nothing else may use the cast or the bound function, and the - // object comes out again only as a call's argument, which is a borrow of its own. An - // interface's `this` (`ts.ExtractInterfaceThis`) is bound so for a call of a function-typed - // field (`i.toString()` where `toString: () => string`). - static bool isBoundForCall(mlir::Operation *thisOp) - { - if (thisOp->getNumResults() != 1 || !mlir::isa(thisOp->getResult(0).getType()) || - thisOp->use_empty()) - { - return false; - } - - return llvm::all_of(thisOp->getUses(), [](mlir::OpOperand &use) { - auto boundOp = mlir::dyn_cast(use.getOwner()); - if (!boundOp || boundOp.getThisVal() != use.get()) - { - return false; - } - - return llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { - if (mlir::isa(boundUser)) - { - return true; - } - - return mlir::isa(boundUser) && - llvm::all_of(boundUser->getUsers(), [](mlir::Operation *thisUser) { return isCall(thisUser); }); - }); - }); - } - - // Is `value` an argument the call's callee keeps? - static bool isKeptArgument(mlir::Operation *call, mlir::Value value) - { - auto args = callArgs(call); - return llvm::any_of(ownedParams(call), [&](int32_t index) { - return static_cast(index) < args.size() && args[index] == value; - }); - } - - // The moves out of a fresh or unknown SSA value. Each taking use (spec 11.1) is a move: - // - nothing else may use the value after it - another taker included - and a taker that - // control comes back to without the value being made again is a move on every iteration - // of a loop (spec 2.6); - // - where the temporary is also released (`__owned_result` rc gives back at block end), the - // taker must be one rc gave its own reference (takerAcquires), and each release the move - // reaches must be dominated by it - that release then has nothing to give back and is - // erased. A release the move cannot reach stays: it is the owner on the other paths. - // Reports and returns false otherwise. - bool checkValueMoves(mlir::Value value, llvm::SetVector &toErase) - { - // Nothing to move: a number owns no block, and a string literal is the immortal global, - // which any number of places may hold. Under --opt, CSE merges identical literals before - // this pass, so one such value is routinely stored into several places. A read out of a - // container owns nothing either: checkPlaceRead decides it. Nor does other data nothing - // owns: an empty optional, a function, a tuple of literals (isConstantData). - if (!ownsHeap(value) || isImmortalLiteral(value) || isConstantData(value) || placeReadOf(value)) - { - return true; - } - - // A temporary's releases are one owner, one release on each way out. Two on one path are - // two owners: each view rc gave a reference of its own (`use(o); use(o)`, with `o` a - // folded object literal and `use` taking an interface: two `ts.NewInterface`s of one block). - if (!checkReleasedOnce(value)) - { - return false; - } - - if (quietly([&] { return movesOf(value, toErase); })) - { - return true; - } - - // Not a move. Every taker a `let` that can borrow, and the temporary's own release the - // owner that bounds them (spec 2.2, verdict 2) - else the move's error stands. - llvm::SmallVector takers; - llvm::SmallVector releases; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - releases.push_back(user); - } - else if (!isBorrow(user, used)) - { - takers.push_back(user); - } - }); - - if (!releases.empty() && !takers.empty() && - llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) - { - auto owner = ownerName(value); - auto ok = true; - for (auto *taker : takers) - { - ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; - } - - return ok; - } - - return movesOf(value, toErase); - } - - // No release of `value` comes after another on any path. Reports and returns false otherwise. - bool checkReleasedOnce(mlir::Value value) - { - llvm::SmallVector releases; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user)) - { - releases.push_back(user); - } - }); - - auto *definition = value.getDefiningOp(); - for (auto *first : releases) - { - for (auto *second : releases) - { - if (first != second && reachableAfter(first, second, definition)) - { - // the view the second release gives back, where rc took its reference - auto *view = second->getOperand(0).getDefiningOp(); - reportSecondReference(view ? view : second, value); - return false; - } - } - } - - return true; - } - - // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. - bool movesOf(mlir::Value value, llvm::SetVector &toErase) - { - llvm::SmallVector takers; - llvm::SmallVector releases; - llvm::SmallVector reads; - llvm::DenseMap readOf; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - releases.push_back(user); - } - else if (isBorrow(user, used)) - { - reads.push_back(user); - readOf.try_emplace(user, used); - } - else - { - takers.push_back(user); - } - }); - - auto *definition = value.getDefiningOp(); - auto name = describe(value); - llvm::SmallVector moved; - for (auto *taker : takers) - { - if (takerLoops(taker, definition)) - { - reportMovedInLoop(taker, name); - return false; - } - - llvm::SmallVector made; - if (definition) - { - made.push_back(definition); - } - - // the call that keeps it, given it again through another operand - if (auto *again = callGivenAgain(taker, value, {value})) - { - reportUseAfterMove(again, taker, name); - return false; - } - - for (auto *use : llvm::concat(takers, reads)) - { - if (use != taker && reachableAfter(taker, use, definition)) - { - // a read through the new owner, while it holds the value (spec 29) - if (auto found = readOf.find(use); - found != readOf.end() && readsThroughOwner(taker, found->second, use, made)) - { - continue; - } - - reportUseAfterMove(use, taker, name); - return false; - } - } - - if (releases.empty()) - { - continue; // rc handed the taker the value's own reference: nothing to cancel - } - - if (!takerAcquires(taker, value)) - { - reportSecondReference(taker, value); - return false; - } - - auto *acquire = acquirePoint(taker, value); - for (auto *release : releases) - { - if (!reachableAfter(acquire, release, definition)) - { - continue; - } - - if (!dominance->properlyDominates(acquire, release)) - { - reportMovedOnSomePaths(taker, release, name); - return false; - } - - moved.push_back(release); - } - } - - toErase.insert(moved.begin(), moved.end()); - return true; - } - - // A call that keeps an argument and is given the same value again through another operand - the - // value itself, a view of it, something read out of it, another read of its slot (`f(n, n)`, - // `f(h, n, n.v)` with only the first `n` kept): the callee takes the value while it borrows it - // through the other operand, and may free it under that borrow, so the other operand is a use - // after the move (spec 33, review). `sources` are what the moved value is: the value, or every - // read of the owning slot it came from. The call itself, or null. - mlir::Operation *callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources) - { - if (!isCall(taker) || ownedParams(taker).empty()) - { - return nullptr; - } - - auto args = callArgs(taker); - auto kept = ownedParams(taker); - auto movedRoot = rootOf(moved); - // one kept position is the move itself; the value at a second one (`f(n, n)` with both - // kept) is a second owner, a use after the first move like any other operand - auto skipped = false; - for (auto &operand : taker->getOpOperands()) - { - // the moved argument itself, where the callee keeps it - auto number = operand.getOperandNumber(); - if (!skipped && !args.empty() && number >= args.getBeginOperandIndex() && - number < args.getBeginOperandIndex() + args.size() && - llvm::is_contained(kept, static_cast(number - args.getBeginOperandIndex())) && - rootOf(operand.get()) == movedRoot) - { - skipped = true; - continue; - } - - for (auto source : sources) - { - if (derivedFrom(source).contains(operand.get())) - { - return taker; - } - } - } - - return nullptr; - } - - // Does control come back round to `taker` without making the value again? Only then is it a - // move on every iteration of a loop. - bool takerLoops(mlir::Operation *taker, mlir::Operation *definition) - { - auto &body = getFunction().getFunctionBody(); - auto *top = body.findAncestorOpInRegion(*taker); - if (!top) - { - return true; - } - - auto *kill = definition ? body.findAncestorOpInRegion(*definition) : nullptr; - llvm::SmallPtrSet seen; - llvm::SmallVector work(top->getBlock()->getSuccessors().begin(), - top->getBlock()->getSuccessors().end()); - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - if (kill && kill->getBlock() == block && - (block != top->getBlock() || kill->isBeforeInBlock(top))) - { - continue; // the value is made again before the taker runs again - } - - if (block == top->getBlock()) - { - return true; - } - - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - - return false; - } - - // Can control reach `to` after `from` has run, without passing `kill` on the way? `kill` is - // the source's definition - the producer of an SSA value, or a local's `ts.Variable` - and a - // path through it is a new value, not this one. Straight-line order within a block, then the - // CFG from the block's successors; a path back into `from`'s own block reaches every op in it. - // - // Ops nested in a region that is not the function body are compared by their ancestors in the - // body. That answers "reachable" whenever the two share an ancestor, which can only turn a - // program into an error. (By this pass async bodies and lambdas are functions of their own; - // after the affine lowering no op of a body holds a region, so the case is kept for safety.) - bool reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill) - { - return reachableAfter(from, to, kill ? llvm::ArrayRef(kill) : llvm::ArrayRef()); - } - - // Whether reachableAfter can order `from` and `to` at all: both inside the function body and not - // one op of it (nested in the same statement). A check whose safe answer is "not reachable" must - // refuse what it cannot order, where reachableAfter would answer "reachable". - bool orderedInBody(mlir::Operation *from, mlir::Operation *to) - { - auto &body = getFunction().getFunctionBody(); - from = body.findAncestorOpInRegion(*from); - to = body.findAncestorOpInRegion(*to); - return from && to && from != to; - } - - // The same, where a path through any of `kills` is a new value. A use that is itself a kill - // runs after the value is made again. - bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) - { - if (llvm::is_contained(kills, to)) - { - return false; - } - - auto &body = getFunction().getFunctionBody(); - from = body.findAncestorOpInRegion(*from); - to = body.findAncestorOpInRegion(*to); - llvm::SmallVector killAt; - for (auto *kill : kills) - { - if (auto *top = body.findAncestorOpInRegion(*kill)) - { - killAt.push_back(top); - } - } - - if (!from || !to) - { - return true; - } - - if (from == to) - { - // Two distinct ops that share one ancestor statement: the order inside it is not - // known here, and "reachable" can only turn a program into an error. - return true; - } - - // is there a kill in `block` after `after` (from its start when null) and before `before` - // (to its end when null)? - auto killsIn = [&](mlir::Block *block, mlir::Operation *after, mlir::Operation *before) { - return llvm::any_of(killAt, [&](mlir::Operation *kill) { - return kill->getBlock() == block && (!after || after->isBeforeInBlock(kill)) && - (!before || kill->isBeforeInBlock(before)); - }); - }; - - auto *fromBlock = from->getBlock(); - auto *toBlock = to->getBlock(); - if (fromBlock == toBlock && from->isBeforeInBlock(to)) - { - return !killsIn(fromBlock, from, to); - } - - // every way out of this block runs a kill first - if (killsIn(fromBlock, from, nullptr)) - { - return false; - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work(fromBlock->getSuccessors().begin(), fromBlock->getSuccessors().end()); - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - if (block == toBlock && !killsIn(block, nullptr, to)) - { - return true; - } - - if (killsIn(block, nullptr, nullptr)) - { - continue; - } - - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - - return false; - } - - // The use that takes `value` for this retain: the declaration a `ts.RetainSlot` belongs to, - // or the one use of the value, or of a view of it, after a `ts.Retain` that is not a read. - static mlir::Operation *takerOf(mlir::Operation *retain, mlir::Value value) - { - if (auto retainSlotOp = mlir::dyn_cast(retain)) - { - return retainSlotOp.getSlot().getDefiningOp(); - } - - // a call that keeps the argument: rc's retain for it is in the callee - if (isCall(retain)) - { - return retain; - } - - mlir::Operation *taker = nullptr; - auto several = false; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user) || isBorrow(user, used)) - { - return; - } - - several = several || taker; - taker = user; - }); - - // rc retained this read and hands the receiver the next read of the same place - if (!taker && !several) - { - if (auto twin = twinReadAfter(retain, value)) - { - return takerOf(retain, twin); - } - } - - return several ? nullptr : taker; // two takers of one read: not a move this phase proves - } - - // rc reads a place twice for one copy - `.map(f)` boxes its array so - and retains the first - // read, whose only use is that retain: the read of the same place that follows it in its block, - // with nothing stored into the place between, is the same value. Null when there is none. - // The one op that uses `value`, but `--di`'s ts.DebugVariable, which only names it; or null. - static mlir::Operation *soleUser(mlir::Value value) - { - mlir::Operation *found = nullptr; - for (auto *user : value.getUsers()) - { - if (mlir::isa(user)) - { - continue; - } - - if (found) - { - return nullptr; - } - - found = user; - } - - return found; - } - - static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value) - { - auto first = value.getDefiningOp(); - if (!first || !mlir::isa(retain) || soleUser(value) != retain || retain->getBlock() != first->getBlock()) - { - return {}; - } - - auto place = first.getReference(); - for (auto *op = retain->getNextNode(); op; op = op->getNextNode()) - { - if (auto loadOp = mlir::dyn_cast(op); loadOp && loadOp.getReference() == place) - { - return loadOp.getResult(); - } - - // anything else given the place may write through it - a store, a push - and a call may - // write anything - if (llvm::is_contained(op->getOperands(), place) || isCall(op)) - { - return {}; - } - } - - return {}; - } - - // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and - // retained what it read for the receiver; the move erases that retain and every release of - // the slot the move reaches, which must be dominated by it - or, with `placeReleases`, is - // placed on the paths that did not move it (spec 30). Any other use of the slot the - // move reaches - a read, an assignment's own read of it - is a use after the move. Reports and - // returns false when it is not a move. - // - // An assignment the move reaches gives the slot a new value to own (#528): a path through it - // is a new value, as a path through the declaration is. Its release before the store goes - // with the move - what the slot held is the receiver's - and the releases only a path through - // an assignment reaches stay, to destroy the new value. - bool checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, - llvm::SetVector &toErase, bool placeReleases = false) - { - auto slot = load.getReference(); - auto varOp = slot.getDefiningOp(); - auto name = varName(varOp); - - auto *taker = takerOf(retain, value); - if (!taker) - { - reportSecondReference(retain); - return false; - } - - auto assignments = slotAssignments(slot); - llvm::SmallVector kills(assignments.begin(), assignments.end()); - if (varOp) - { - kills.push_back(varOp); - } - - // the move's own read, met again around a loop the slot was declared outside of - if (reachableAfter(taker, load, kills)) - { - reportMovedInLoop(taker, name); - return false; - } - - // the slot given another value on some path between the move's read and its taker - inside - // the call's own argument list, `f(n, (n = mk(7), 0))`, on one arm of it, `f(n, c ? (n = - // mk(7), 0) : 0)`: the assignment's release frees what the read holds before the callee takes - // it, so the assignment is a use of the slot after the move. The taker may be an assignment - // of the slot itself (`n = n`): it is the destination, not between. A read and a taker - // reachableAfter cannot order (one statement kept as a region, or outside the body) refuse. - { - llvm::SmallVector readAgain{load.getOperation()}; - if (varOp) - { - readAgain.push_back(varOp); - } - - for (auto *assignment : assignments) - { - if (assignment != taker && reachableAfter(load, assignment, varOp) && - reachableAfter(assignment, taker, readAgain)) - { - reportUseAfterMove(assignment, taker, name); - return false; - } - } - - llvm::SmallVector killsBetween(kills); - llvm::erase(killsBetween, taker); - if (!orderedInBody(load, taker) || !reachableAfter(load, taker, killsBetween)) - { - reportUseAfterMove(taker, taker, name); - return false; - } - } - - // the call that keeps it, given the slot's value again through another operand: a read of - // the slot that reaches the call without crossing an assignment of it (not one made before - // an assignment gave the slot its current value, `const arr = [n]; n = mk(5); f(n, arr)`); - // since the move's own read reaches the call so too, such a read holds the moved value - llvm::SmallVector slotReads; - for (auto *user : slot.getUsers()) - { - if (auto readOp = mlir::dyn_cast(user); readOp && reachableAfter(readOp, taker, kills)) - { - slotReads.push_back(readOp.getResult()); - } - } - - if (auto *again = callGivenAgain(taker, value, slotReads)) - { - reportUseAfterMove(again, taker, name); - return false; - } - - // Every read of the slot is a use of `a`, and so is every use of what a read returned: a - // `const b = a` is folded into its load, so `b.x` after the move reads the moved value - // through a load that came before it. A read through the new owner, while it holds the - // value, is not such a use (spec 29). - auto throughOwner = [&](mlir::Operation *user, mlir::Value used) { - return readsThroughOwner(taker, used, user, kills); - }; - - for (auto *use : slot.getUsers()) - { - if (mlir::isa(use)) - { - continue; - } - - if (use != load.getOperation() && reachableAfter(taker, use, kills)) - { - auto readOp = mlir::dyn_cast(use); - if (!readOp || !onlyReadThroughOwner(readOp.getResult(), throughOwner)) - { - reportUseAfterMove(use, taker, name); - return false; - } - - continue; - } - - if (auto readOp = mlir::dyn_cast(use)) - { - if (auto *late = readUsedAfter(readOp, retain, taker, varOp, throughOwner)) - { - reportUseAfterMove(late, taker, name); - return false; - } - } - } - - auto *acquire = mlir::isa(retain) ? taker : retain; - llvm::SmallVector moved; - for (auto *use : slot.getUsers()) - { - if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) - { - continue; - } - - // a release that one path reaches holding the moved value and another holding a new - // one an assignment after the move stored: there is no single answer for it - auto someMoved = !dominance->properlyDominates(acquire, use); - for (auto *assignment : assignments) - { - llvm::SmallVector movedAgain{load.getOperation()}; - if (varOp) - { - movedAgain.push_back(varOp); - } - - someMoved = someMoved || - (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)); - } - - if (someMoved) - { - // ...but the paths that did not move it can release it before they join the - // others (spec 30) - if (placeReleases && placeOnUnmovedEdges(varOp, acquire, retain, taker, assignments, toErase)) - { - return true; - } - - reportMovedOnSomePaths(taker, use, name, placeReleases ? joinedUse : nullptr); - return false; - } - - moved.push_back(use); - } - - toErase.insert(moved.begin(), moved.end()); - return true; - } - - // ---- A move on some paths only (spec 30) ---- - // - // `if (c) { best = v; }`: after the paths join, `v` holds its value on one and a moved one on - // the other, and its one release there has no single answer. No run-time record of the move is - // kept (spec 2.2); instead, where nothing uses the value after the join, the release is placed - // on each edge into the join that did not move it - freeing the value earlier, where it is - // already dead - and the releases after the join go. Each path frees the value once: through - // its new owner where it moved, through the edge where it did not. - - // What the slot of an owning local holds at a point: nothing yet or any more (declared later, - // released), its value, or a value moved out of it. A point some paths reach holding the value - // and others having moved it is where the paths join. - enum SlotState : unsigned - { - SlotDead = 1, - SlotLive = 2, - SlotMoved = 4 - }; - - static bool isJoined(unsigned state) - { - return (state & SlotLive) && (state & SlotMoved); - } - - // A value of this type can point into a block: anything but a number, a boolean, a literal or - // an empty value. - static bool carriesPointer(mlir::Type type) - { - return !mlir::isa(type); - } - - // A release on an edge into the join that did not move the local's value (spec 30), put in after - // the analysis. - struct EdgeRelease - { - mlir::Value slot; - mlir::Block *from; - unsigned successor; - mlir::Location loc; - }; - - llvm::SmallVector edgeReleases; - - // The use that kept placeOnUnmovedEdges from placing the releases, for the error's note. - mlir::Operation *joinedUse = nullptr; - - // Can control reach `to` from the start of `start`, without passing `kills` on the way? - bool reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills) - { - auto &body = getFunction().getFunctionBody(); - to = body.findAncestorOpInRegion(*to); - if (!to) - { - return true; - } - - llvm::SmallVector killAt; - for (auto *kill : kills) - { - if (auto *top = body.findAncestorOpInRegion(*kill)) - { - killAt.push_back(top); - } - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work{start}; - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - auto killed = [&](mlir::Operation *before) { - return llvm::any_of(killAt, [&](mlir::Operation *kill) { - return kill->getBlock() == block && (!before || kill->isBeforeInBlock(before) || kill == before); - }); - }; - - if (to->getBlock() == block && !killed(to)) - { - return true; - } - - if (!killed(nullptr)) - { - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - } - - return false; - } - - // The blocks of an exception handler: from a landing pad to the end of its catch. A release there - // would be a call inside an exception funclet. - llvm::SmallPtrSet handlerBlocks() - { - llvm::SmallPtrSet handler; - llvm::SmallVector work; - for (auto &block : getFunction().getFunctionBody()) - { - if (!block.empty() && mlir::isa(block.front())) - { - work.push_back(&block); - } - } - - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!handler.insert(block).second) - { - continue; - } - - auto ends = llvm::any_of(*block, [](mlir::Operation &op) { - return mlir::isa(op); - }); - if (!ends) - { - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - } - - return handler; - } - - // The move at `acquire` out of the owning local `varOp` reaches a release that other paths reach - // holding the value. Places a release of the slot on every edge into the join that holds the value - // and has not moved it, and erases every release a moved value reaches - when nothing uses the - // value, through the slot or through anything read out of it, where the paths have joined. False, - // with nothing placed or erased, when it cannot. - // The slot of an owning local through the function (spec 30): its state at the entry and the exit - // of each block, and before each op of the body. - struct SlotStates - { - llvm::DenseMap entry; - llvm::DenseMap exit; - llvm::DenseMap before; - }; - - bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, - mlir::Operation *taker, llvm::ArrayRef assignments, - llvm::SetVector &toErase) - { - joinedUse = nullptr; - llvm::SmallVector releases; - if (!slotReleasesInBody(varOp, acquire, assignments, releases)) - { - return false; - } - - auto states = slotStates(varOp, acquire, assignments, releases); - - // every release a moved value reaches goes; one some path reaches with nothing in the slot - // is not this rule's - llvm::SmallVector erased; - for (auto *release : releases) - { - auto state = states.before.lookup(release); - if (state & SlotMoved) - { - if (state & SlotDead) - { - return false; - } - - erased.push_back(release); - } - } - - llvm::SmallVector placed; - llvm::SetVector releasedInto; - if (!unmovedEdges(varOp, states, placed, releasedInto)) - { - return false; - } - - if (!noUseWhereJoined(varOp, retain, taker, assignments, states, releasedInto)) - { - return false; - } - - toErase.insert(erased.begin(), erased.end()); - edgeReleases.append(placed.begin(), placed.end()); - return true; - } - - // The releases of the slot, when the local, the move, its assignments and its releases are all in - // the function's body. False when one is not. - bool slotReleasesInBody(mlir_ts::VariableOp varOp, mlir::Operation *acquire, - llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases) - { - auto &body = getFunction().getFunctionBody(); - if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || - acquire->getParentRegion() != &body || - llvm::any_of(assignments, [&](mlir::Operation *op) { return op->getParentRegion() != &body; })) - { - return false; - } - - auto slot = varOp.getResult(); - for (auto *user : slot.getUsers()) - { - if (mlir::isa(user)) - { - if (user->getParentRegion() != &body) - { - return false; - } - - releases.push_back(user); - } - } - - return true; - } - - // The slot's state through the function, forward from its entry: assigned, moved at `acquire`, - // released. - SlotStates slotStates(mlir_ts::VariableOp varOp, mlir::Operation *acquire, llvm::ArrayRef assignments, - llvm::ArrayRef releases) - { - auto &body = getFunction().getFunctionBody(); - auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { - if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) - { - return SlotLive; - } - - if (op == acquire) - { - return SlotMoved; - } - - if (llvm::is_contained(releases, op)) - { - return SlotDead; - } - - return state; - }; - - SlotStates states; - auto &entry = states.entry; - auto exitOf = [&](mlir::Block *block) { - auto state = entry.lookup(block); - for (auto &op : *block) - { - state = step(&op, state); - } - - return state; - }; - - entry[&body.front()] = SlotDead; - llvm::SmallVector work{&body.front()}; - while (!work.empty()) - { - auto *block = work.pop_back_val(); - auto state = exitOf(block); - for (auto *successor : block->getSuccessors()) - { - auto &into = entry[successor]; - if ((into | state) != into) - { - into |= state; - work.push_back(successor); - } - } - } - - // ...and before each op of the body - for (auto &block : body) - { - auto state = entry.lookup(&block); - for (auto &op : block) - { - states.before[&op] = state; - state = step(&op, state); - } - - states.exit[&block] = state; - } - - return states; - } - - // Places a release of the slot on every edge into the join that holds the value and has not moved - // it. False, with nothing placed, when an edge cannot take one, or there is none. - bool unmovedEdges(mlir_ts::VariableOp varOp, const SlotStates &states, llvm::SmallVectorImpl &placed, - llvm::SetVector &releasedInto) - { - auto &body = getFunction().getFunctionBody(); - auto slot = varOp.getResult(); - auto &entry = states.entry; - - // the edges into the join that hold the value and did not move it - auto handler = handlerBlocks(); - for (auto &block : body) - { - if (!entry.count(&block)) - { - continue; // unreachable - } - - auto state = states.exit.lookup(&block); - for (auto index : llvm::seq(0, block.getNumSuccessors())) - { - auto into = entry.lookup(block.getSuccessor(index)); - if (!isJoined(into)) - { - continue; - } - - // a join some path reaches with nothing in the slot is not this rule's - if (into & SlotDead) - { - return false; - } - - // an edge that moved it, or from where the paths have joined already - if (state != SlotLive) - { - continue; - } - - auto *terminator = block.getTerminator(); - if (!mlir::isa(terminator) || handler.contains(&block) || - !dominance->dominates(varOp.getOperation(), terminator)) - { - return false; - } - - placed.push_back({slot, &block, static_cast(index), terminator->getLoc()}); - releasedInto.insert(block.getSuccessor(index)); - } - } - - return !placed.empty(); - } - - // Whether nothing uses the value where the paths have joined, or after an edge released it. - // False, with the use in `joinedUse` where there is one, when the release cannot be placed. - bool noUseWhereJoined(mlir_ts::VariableOp varOp, mlir::Operation *retain, mlir::Operation *taker, - llvm::ArrayRef assignments, const SlotStates &states, - const llvm::SetVector &releasedInto) - { - auto &body = getFunction().getFunctionBody(); - auto slot = varOp.getResult(); - - auto joinedAt = [&](mlir::Operation *user) { - auto *top = body.findAncestorOpInRegion(*user); - return !top || isJoined(states.before.lookup(top)); - }; - - // Nothing uses the value where the paths have joined - a read there, on the path that did - // not move it, needs what the edge released - neither through the slot nor through anything - // read out of it: a field, a view, a borrowing local, a closure. The move itself hands the - // value to its new owner, whose reads are its own. A value the slot is given - its - // initializer, an assignment's - is the slot's only once it is stored: a use before that (a - // constructor's, or one around a loop before it is made again) is not a use of it. - // - // Past the joined points too: what was read out of the value before an edge released it - // may not be used after that edge - after a new value is stored into the slot, or after its - // scope ends - unless it was read out again (spec 30). A read of the slot after a new value - // is stored reads that one. - struct Origin - { - mlir::Operation *stored; - mlir::Operation *made; - }; - - llvm::SmallVector origins{{nullptr, nullptr}}; - llvm::SmallVector> values; - llvm::DenseSet> seen; - auto taint = [&](mlir::Value value, unsigned origin) { - if (carriesPointer(value.getType()) && seen.insert({value, origin}).second) - { - values.push_back({value, origin}); - } - }; - - auto given = [&](mlir::Value value, mlir::Operation *stored) { - origins.push_back({stored, value.getDefiningOp()}); - taint(value, origins.size() - 1); - }; - - taint(slot, 0); - if (varOp.getInitializer()) - { - given(varOp.getInitializer(), varOp); - } - - for (auto *assignment : assignments) - { - given(mlir::cast(assignment).getValue(), assignment); - } - - // reached from an edge that released the value, before the read it came from (or the op - // that made it) runs again - auto usedAfterRelease = [&](mlir::Operation *made, mlir::Operation *user) { - llvm::SmallVector madeAgain; - if (made) - { - madeAgain.push_back(made); - } - - return llvm::any_of(releasedInto, [&](mlir::Block *join) { return reachableFrom(join, user, madeAgain); }); - }; - - auto heldAt = [&](unsigned origin, mlir::Operation *user) { - auto &from = origins[origin]; - if (!from.stored) - { - return true; - } - - llvm::SmallVector madeAgain; - if (from.made) - { - madeAgain.push_back(from.made); - } - - return reachableAfter(from.stored, user, madeAgain); - }; - - while (!values.empty()) - { - auto [value, origin] = values.pop_back_val(); - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || user == retain) - { - continue; - } - - // the slot's own releases and assignments are decided above - if (value == slot && (mlir::isa(user) || llvm::is_contained(assignments, user))) - { - continue; - } - - if (joinedAt(user) && heldAt(origin, user)) - { - joinedUse = user; - return false; - } - - if (value != slot && usedAfterRelease(origins[origin].made, user)) - { - joinedUse = user; - return false; - } - - if (user == taker) - { - continue; - } - - // a store of it into a local makes every read of that local one; one into a place - // is not followed, and the release is not placed - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == value) - { - auto into = storeOp.getReference().getDefiningOp(); - if (!into || into.getCaptured().value_or(false)) - { - return false; - } - - taint(into.getResult(), origin); - continue; - } - - if (auto branch = mlir::dyn_cast(user)) - { - for (auto index : llvm::seq(0, user->getNumSuccessors())) - { - auto operands = branch.getSuccessorOperands(index); - for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) - { - if (operand == value) - { - taint(user->getSuccessor(index)->getArgument(operands.getProducedOperandCount() + position), - origin); - } - } - } - } - - // a call's result is its own block, unless its facts say it borrows this argument - a - // plain borrow, or a borrowing record that holds it (spec 32) - auto borrows = true; - if (isCall(user)) - { - auto args = callArgs(user); - borrows = llvm::any_of(resultBorrowsAll(user), [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - }); - } - - // a read of the slot is what its uses came from - auto from = origin; - if (value == slot && user->getNumResults() > 0) - { - origins.push_back({nullptr, user}); - from = origins.size() - 1; - } - - for (auto result : user->getResults()) - { - if (borrows) - { - taint(result, from); - } - } - - // inside a region - a closure, an async body - it is the op that holds the region - // that hands it on - if (user->getParentRegion() != &body) - { - for (auto result : body.findAncestorOpInRegion(*user)->getResults()) - { - taint(result, origin); - } - } - } - } - - return true; - } - - // Puts in the releases placeOnUnmovedEdges decided: at the end of a block that goes only to the - // join, else in a block of its own on the edge. - void placeEdgeReleases() - { - for (auto &edge : edgeReleases) - { - auto *terminator = edge.from->getTerminator(); - mlir::OpBuilder builder(terminator); - if (terminator->getNumSuccessors() == 1) - { - builder.create(edge.loc, edge.slot); - continue; - } - - auto *join = terminator->getSuccessor(edge.successor); - auto operands = mlir::cast(terminator).getSuccessorOperands(edge.successor); - llvm::SmallVector forwarded(operands.getForwardedOperands()); - auto *split = new mlir::Block(); - split->insertAfter(edge.from); - builder.setInsertionPointToEnd(split); - builder.create(edge.loc, edge.slot); - builder.create(edge.loc, join, forwarded); - if (!forwarded.empty()) - { - operands.erase(0, forwarded.size()); - } - - terminator->setSuccessor(split, edge.successor); - } - - edgeReleases.clear(); - } - - // The stores of the assignments to an owning local: rc releases what the slot holds right - // before it stores the new value (an assignment, and a `var` declaration met again). - static llvm::SmallVector slotAssignments(mlir::Value slot) - { - llvm::SmallVector assignments; - for (auto *user : slot.getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - auto releaseOp = storeOp ? mlir::dyn_cast_or_null(storeOp->getPrevNode()) - : mlir_ts::ReleaseSlotOp(); - if (releaseOp && storeOp.getReference() == slot && releaseOp.getSlot() == slot) - { - assignments.push_back(storeOp); - } - } - - return assignments; - } - - // A use of what `read` returned - directly or through casts - that the move at `taker` can - // reach, other than the move's own retain and taker. None if there is none. A path back - // through `read` itself reaches a use of a value read again: the read, met after the move, - // is the slot's own use to check (around a loop, of a value an assignment stored). - // `isRead` accepts a use that reads the value through its new owner (spec 29). - mlir::Operation *readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, - mlir::Operation *kill, - llvm::function_ref isRead = nullptr) - { - llvm::SmallVector values{read.getResult()}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto *user : current.getUsers()) - { - if (user == retain || user == taker || mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) || isView(user)) - { - values.push_back(user->getResult(0)); - continue; - } - - llvm::SmallVector kills{read.getOperation()}; - if (kill) - { - kills.push_back(kill); - } - - if (reachableAfter(taker, user, kills) && !(isRead && isRead(user, current))) - { - return user; - } - } - } - - return nullptr; - } - - // ---- Reads after a move, through the new owner (spec 29) ---- - // - // `h.c = c; print(c.x)`, `best = v; s += v.x`: after the move the value is the new owner's, and - // a read of it through the old name is a borrow of the place it moved into, as a read of - // `h.c` would be. It is bounded the way checkPlaceRead bounds such a read - by the ends of the - // place's roots and by the drops that may destroy what the place holds (an overwrite, through an - // interface view too; a call whose facts may drop; a call given a closure) - on a path from the - // move to the read on which the value is not made again or moved again. - - // Every use of `value`, through its views, is a read `isRead` accepts. A retain - rc's reference - // for an owner - is not. - static bool onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead) - { - auto all = true; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - all = all && (mlir::isa(user) || - (!mlir::isa(user) && isRead(user, used))); - }); - - return all; - } - - // A use that only reads the value: a field, element or length read through it, a method reached - // through it, a comparison or arithmetic, a print, an assert, a test, or an argument the callee - // borrows. Anything else - a store, an owning `let`, a keeping call, a return - would be a second - // owner. - static bool isReadAfterMove(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return isBorrow(user, value); - } - - return isCall(user) && !isKeptArgument(user, value); - } - - // The place a taker moves its value into, as a chain checkPlaceRead would read it: an owning - // local (a declaration, or an assignment), or a field or element - of a local or a parameter - // (`this`). False for anything else: a call that keeps it, a push, a return, a global, a - // captured variable, a handle's payload. - bool destinationChain(mlir::Operation *taker, Chain &chain) - { - auto ownSlot = [](mlir_ts::VariableOp varOp) { - return varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false); - }; - - if (auto varOp = mlir::dyn_cast(taker)) - { - if (!ownSlot(varOp)) - { - return false; - } - - chain.roots.push_back({varOp.getResult(), Chain::Slot}); - return true; - } - - auto storeOp = mlir::dyn_cast(taker); - if (!storeOp) - { - return false; - } - - auto ref = storeOp.getReference(); - if (auto varOp = ref.getDefiningOp()) - { - if (!ownSlot(varOp)) - { - return false; - } - - chain.roots.push_back({ref, Chain::Slot}); - return true; - } - - if (!isPlace(ref)) - { - return false; - } - - chain = chainOf(nullptr, ref); - if (!chain.handles.empty() || chain.roots.empty()) - { - return false; - } - - // a global's field, or a captured variable's: not a place of this function - return llvm::none_of(chain.roots, [](auto &root) { - auto loadOp = root.first.template getDefiningOp(); - return loadOp && (loadOp.getReference().template getDefiningOp() || - isLoadedCell(loadOp.getReference())); - }); - } - - // Is `use`, a use of `value` after `taker` moved it, a read through the place it moved into - // (spec 29)? `made` is where the value is made again (its definition, the local's declaration - // and assignments). Sound when no path from the move through an end or a drop of the place to - // the read - or to anything the read produced - makes the value again or moves it again: such a - // path is the only way the place can stop holding the value before the read. - bool readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, - llvm::ArrayRef made) - { - if (!isReadAfterMove(use, value)) - { - return false; - } - - Chain chain; - if (!destinationChain(taker, chain)) - { - return false; - } - - // A read again of the old name after a drop still finds the moved value - the slot is not - // emptied by a move - so neither the read nor the slot's read is where it is made again. - llvm::SmallVector kills(made.begin(), made.end()); - kills.push_back(taker); - llvm::SmallVector uses; - uses.push_back({use, {kills.begin(), kills.end()}}); - for (auto result : use->getResults()) - { - if (!mayPointInto(result)) - { - continue; - } - - // what the read produced - a field's reference, a bound method - still points into the - // block; a call's result that borrows its argument is the value again and may not be kept - if (walkBorrowed(result, kills, uses, nullptr, Keeping{nullptr, false}, false, !!borrowingCall(result))) - { - return false; - } - } - - // a method reached through it whose callee keeps its `this`: a second owner - for (auto &borrowUse : uses) - { - if (!isCall(borrowUse.op)) - { - continue; - } - - auto args = callArgs(borrowUse.op); - for (auto index : ownedParams(borrowUse.op)) - { - if (static_cast(index) < args.size() && - mlir::isa_and_nonnull(args[index].getDefiningOp())) - { - return false; - } - } - } - - auto outlives = [&](mlir::Operation *end) { - if (end == taker || !reachableAfter(taker, end, made)) - { - return false; - } - - return llvm::any_of(uses, [&](BorrowUse &borrowUse) { - return (borrowUse.op != end || !borrowUse.callee) && reachableAfter(end, borrowUse.op, borrowUse.kills); - }); - }; - - for (auto *end : rootEnds(chain)) - { - if (outlives(end)) - { - return false; - } - } - - for (auto *drop : drops) - { - if (dropsChain(drop, chain, nullptr, value) && outlives(drop)) - { - return false; - } - } - - return true; - } - - // Where rc takes the receiver's reference: its `ts.Retain` of the value, when that comes - // first in the taker's block, else the taker itself (a declaration, whose `ts.RetainSlot` - // follows it). A release between the two - a `return`'s scope exit runs after its retain - // and before the store into the return slot - is after the move. - static mlir::Operation *acquirePoint(mlir::Operation *taker, mlir::Value value) - { - // a store into a record, which rc retains as a whole after it is filled - if (auto *retain = recordRetainOfStore(taker)) - { - return retain; - } - - mlir::Operation *first = taker; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user) && user->getBlock() == taker->getBlock() && - user->isBeforeInBlock(first)) - { - first = user; - } - }); - - return first; - } - - // Did rc give this taker a reference of its own - a `ts.Retain` of the value in front of it, - // or the `ts.RetainSlot` of an owning local declared from it? Only then is there a release on - // the taker's side to give the value back once it has moved. `ts.NewInterface` is a taker to - // this pass but a view to rc, which retains nothing for it and relies on the instance's own - // release; moving the instance into it would leave nothing to release it at all. - static bool takerAcquires(mlir::Operation *taker, mlir::Value value) - { - // a callee that keeps its argument retained it in its own body under rc - if (isCall(taker)) - { - return true; - } - - if (recordRetainOfStore(taker)) - { - return true; - } - - if (auto varOp = mlir::dyn_cast(taker)) - { - return llvm::any_of(varOp.getResult().getUsers(), - [](mlir::Operation *user) { return mlir::isa(user); }); - } - - auto acquires = false; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && - user->isBeforeInBlock(taker)); - }); - - return acquires; - } - - // The declaration's name survives only as debug metadata (`--di`). At this level MLIRGen's - // form of it is a NameLoc first inside the location fused with the scope - what LowerToLLVM's - // preserveTypesForDebugInfo reads - and after that pass it is a DILocalVariable. Without - // `--di` the error still points at the right place. - static llvm::StringRef varName(mlir_ts::VariableOp varOp) - { - return nameAt(varOp.getLoc()); - } - - static llvm::StringRef nameAt(mlir::Location location) - { - if (auto fused = mlir::dyn_cast>(location)) - { - if (auto name = fused.getMetadata().getName()) - { - return name.getValue(); - } - } - - if (auto scoped = mlir::dyn_cast>(location)) - { - if (auto named = mlir::dyn_cast_or_null(scoped.getLocations().front())) - { - return named.getName().getValue(); - } - } - - return "this value"; - } - - static llvm::StringRef describe(mlir::Value value) - { - llvm::StringRef name = "this value"; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (auto varOp = mlir::dyn_cast(user); varOp && name == "this value") - { - name = varName(varOp); - } - }); - - return name; - } - - // A second reference nobody proved safe. `value` is the value it is to, when the caller knows - // it; a retain names its own. The shapes this pass recognises get the rule they break (spec - // 21.2); anything else, the catch-all. - void reportSecondReference(mlir::Operation *op, mlir::Value value = {}) - { - if (!reporting()) - { - return; - } - - llvm::StringRef name = "this value"; - if (auto retainSlotOp = mlir::dyn_cast(op)) - { - if (auto varOp = retainSlotOp.getSlot().getDefiningOp()) - { - name = varName(varOp); - } - } - else if (auto varOp = mlir::dyn_cast(op)) - { - name = varName(varOp); - } - - if (!value && mlir::isa(op)) - { - value = retainedValue(op); - } - - if (!value || !reportShapeOfSecondReference(op, rootOf(value), name)) - { - auto diag = op->emitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; - noteLostFacts(diag); - } - - signalPassFailure(); - } - - // The named shapes of a second reference to `root` (spec 21.2). False when it is none of them. - bool reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name) - { - auto f = getFunction(); - - // returned, where another return gives a borrow of an argument and this one does not, or - // the other way round: the callers cannot be told which they get - if (isReturned(root)) - { - auto param = borrowedParam(root); - if (auto *other = otherReturn(param)) - { - auto diag = op->emitError("'") << f.getName() - << "' returns a borrow of its argument on some paths and another value on " - "others; -mm=own cannot tell its callers whether they own the result"; - diag.attachNote(other->getLoc()) - << (param >= 0 ? "returns another value here" : "returns a borrow of its argument here"); - return true; - } - } - - // a parameter's value, or a part of it, returned or kept: a fact its callers must know - // (spec 15), and noteLostFacts says why they cannot - if (auto param = borrowedParam(root); param >= 0) - { - auto diag = op->emitError("'") << (name == "this value" ? paramName(param) : name); - if (isReturned(root)) - { - diag << "' is a parameter, returned here, but -mm=own cannot tell the callers of '" << f.getName() - << "' that the result borrows it"; - } - else - { - diag << "' is a parameter, kept here, but -mm=own cannot make the callers of '" << f.getName() - << "' give it up"; - } - - noteLostFacts(diag); - return true; - } - - // a store into a local whose value is not this function's to replace: a parameter's slot, or - // a local that owns nothing (MLIRGen's takeOwnershipOfLocal) - auto storeOp = mlir::dyn_cast(op); - if (auto slot = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - slot && !isOwningVariable(slot) && !isResultSlot(slot)) - { - // a parameter's slot, assigned (isParameterSlot is the unassigned one) - auto argument = mlir::dyn_cast_or_null(slot.getInitializer()); - if (argument && argument.getOwner()->isEntryBlock() && - mlir::isa(argument.getOwner()->getParentOp())) - { - op->emitError("'") << varName(slot) << "' is a parameter and cannot be assigned under -mm=own yet"; - return true; - } - - // MLIRGen makes no owner of a local declared without a value, nor of one declared in a - // catch or finally clause, whose release would be a call inside an exception funclet - if (!slot.getInitializer()) - { - op->emitError("'") << varName(slot) - << "' owns nothing under -mm=own (it is declared without a value, or in a catch or " - "finally clause), so it cannot be given a new value here"; - return true; - } - } - - // merged from branches or loop iterations - if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) - { - op->emitError("'") << name - << "' is merged from several branches (`?:`, `&&`, `||`, `??`, a loop); -mm=own cannot " - "prove which one owns it yet"; - return true; - } - - auto loadOp = root.getDefiningOp(); - - // a read of a global - if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp()) - { - // a local that reads it: phase 3's borrow, which a call or an assignment of the global - // may end - if (mlir::isa(op)) - { - op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() - << "', but -mm=own cannot prove the global keeps its value while '" << name - << "' is used: a call or an assignment may overwrite it"; - return true; - } - - op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() - << "' and cannot be stored, returned or captured"; - return true; - } - - // a read of a captured variable, through its cell - if (loadOp && isLoadedCell(loadOp.getReference())) - { - op->emitError("'") << name << "' borrows a captured variable and cannot be stored, returned or captured"; - return true; - } - - // an object literal built in a local, holding a value it owns: moved only into an owning - // local or the result (recordRetainOf) - auto record = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!record && storeOp) - { - auto propertyRefOp = storeOp.getReference().getDefiningOp(); - record = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - } - - if (record && !isOwningVariable(record) && record.getInitializer() && - record.getInitializer().getDefiningOp()) - { - op->emitError("an object literal that holds a value it owns is moved only into a local or " - "returned as it is; -mm=own cannot move it here yet"); - return true; - } - - return false; - } - - // The name of parameter `index`, as its slot carries it (under `--di`). - llvm::StringRef paramName(int index) - { - llvm::StringRef name = "this value"; - getFunction()->walk([&](mlir_ts::VariableOp varOp) { - auto argument = mlir::dyn_cast_or_null(varOp.getInitializer()); - if (argument && argument.getOwner()->isEntryBlock() && static_cast(argument.getArgNumber()) == index) - { - name = varName(varOp); - } - }); - - return name; - } - - // Is `root`, or a view of it, stored into the result slot? - static bool isReturned(mlir::Value root) - { - auto returned = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value used) { - auto storeOp = mlir::dyn_cast(user); - auto slot = storeOp && storeOp.getValue() == used ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - returned = returned || (slot && isResultSlot(slot)); - }); - - return returned; - } - - // A return that disagrees with one of a borrow of parameter `param` (with -1, with one that - // borrows nothing): a store into the result slot of a value that holds a block and borrows - // another parameter or none - or, for -1, borrows a parameter. Null when there is none. - mlir::Operation *otherReturn(int param) - { - mlir::Operation *found = nullptr; - getFunction()->walk([&](mlir_ts::StoreOp storeOp) { - auto slot = storeOp.getReference().getDefiningOp(); - if (found || !slot || !isResultSlot(slot) || holdsNoBlock(storeOp.getValue())) - { - return; - } - - auto borrows = borrowedParam(storeOp.getValue()); - if (param >= 0 ? borrows != param : borrows >= 0) - { - found = storeOp; - } - }); - - return found; - } - - // Where the body would have been fine had its callers known a fact about it, say why they - // cannot. - void noteLostFacts(mlir::InFlightDiagnostic &diag) - { - auto f = getFunction(); - if (auto why = f->getAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) - { - diag.attachNote(f.getLoc()) << "'" << f.getName() << "' could return a borrow or keep a parameter, but " - << why.getValue(); - } - } - - void reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = move->emitError("'") << name - << "' is moved here on some paths only; -mm=own cannot release it " - "on the others yet"; - diag.attachNote(exit->getLoc()) << "returns here without moving it; the caller gave it up"; - signalPassFailure(); - } - - // A handle made into a union whose other members include one that owns a block (spec 23.2): - // `Shared | string` given a Shared. gc, none and rc accept it. - void reportHandleInOwningUnion(mlir::Operation *op) - { - auto handleIn = llvm::any_of(op->getOperandTypes(), [](mlir::Type type) { - return MLIRTypeHelper::isSharedHandleType(type); - }); - if (!handleIn) - { - return; - } - - for (auto resultType : op->getResultTypes()) - { - if (!MLIRTypeHelper::isSharedInOwningUnionType(resultType)) - { - continue; - } - - if (!reporting()) - { - return; - } - - auto unionType = resultType; - if (auto optionalType = mlir::dyn_cast(unionType)) - { - unionType = optionalType.getElementType(); - } - - mlir::Type shared; - mlir::Type owning; - for (auto member : mlir::cast(unionType).getTypes()) - { - if (mlir::isa(member)) - { - shared = shared ? shared : member; - } - else if (!owning && MLIRTypeHelper::ownsHeapMemoryOfUnionMember(member)) - { - owning = member; - } - } - - auto diag = op->emitError("a ") << printed(shared) << " in a union with "; - if (owning) - { - diag << "'" << printed(owning) << "', which -mm=own does not count,"; - } - else - { - diag << "a member -mm=own does not count,"; - } - - diag << " is not supported"; - signalPassFailure(); - return; - } - } - - static std::string printed(mlir::Type type) - { - llvm::SmallString<128> text; - llvm::raw_svector_ostream out(text); - MLIRPrinter printer{}; - printer.printType(out, type); - return text.str().str(); - } - - void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) - { - if (!reporting()) - { - return; - } - - llvm::StringRef name = ownerName(arg); - if (auto loadOp = rootOf(arg).getDefiningOp()) - { - if (auto varOp = loadOp.getReference().getDefiningOp()) - { - name = varName(varOp); - } - } - - auto callee = calleeName(call); - auto diag = call->emitError("argument '") << name << "' is given to '" << (callee.empty() ? "a function" : callee) - << "', which keeps it, but -mm=own cannot move it here: this " - "function does not own it"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' is used here after its value was moved"; - diag.attachNote(move->getLoc()) << "value moved here"; - signalPassFailure(); - } - - void reportMovedInLoop(mlir::Operation *move, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - move->emitError("'") << name - << "' is moved inside a loop but was made outside it; only a borrow could do " - "that, and -mm=own cannot prove one yet"; - signalPassFailure(); - } - - void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' borrows '" << owner << "' but is used here after '" << owner - << "' is released or overwritten"; - diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; - signalPassFailure(); - } - - void reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, - llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' borrows " << place - << " but is used here after it may be released or overwritten"; - diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; - signalPassFailure(); - } - - void reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name << "' borrows " << place << " and cannot be assigned; -mm=own cannot prove that yet"; - signalPassFailure(); - } - - void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - auto diag = op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << owner << "' is moved here while '" << borrower - << "' borrows it; a borrowed value cannot be moved"; - signalPassFailure(); - } - - void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } - - auto diag = op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name << "' borrows '" << owner << "' and cannot be assigned; -mm=own cannot prove that yet"; - signalPassFailure(); - } - - void reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why) - { - if (!reporting()) - { - return; - } - - auto diag = capture->emitError("a closure that escapes owns what it captures, but -mm=own cannot move this " - "variable into it: ") - << why; - diag.attachNote(escape->getLoc()) << "the closure escapes here"; - signalPassFailure(); - } - - void reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' is captured by a closure that escapes, and is still used here"; - diag.attachNote(capture->getLoc()) << "the closure takes it here"; - signalPassFailure(); - } - - void reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, - llvm::StringRef captured) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << closure << "' borrows '" << captured << "', which it captures, but runs here after '" - << captured << "' is released"; - diag.attachNote(end->getLoc()) << "'" << captured << "' is released here"; - signalPassFailure(); - } - - void reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << captured << "' is captured by a closure, but -mm=own cannot tell who owns its value here"; - signalPassFailure(); - } - - void reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name - << "' is a captured parameter and cannot be assigned under -mm=own: its value is the " - "caller's"; - signalPassFailure(); - } - - void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, - mlir::Operation *usedAt = nullptr) - { - if (!reporting()) - { - return; - } - - auto diag = move->emitError("'") << name - << "' is moved here on some paths only; -mm=own cannot release it " - "on the others yet"; - diag.attachNote(release->getLoc()) << "released here on every path"; - if (usedAt) - { - diag.attachNote(usedAt->getLoc()) - << "used here, after the paths join, so it cannot be released before they do"; - } - - signalPassFailure(); - } -}; - -} // end anonymous namespace +} // namespace own_inference #undef DEBUG_TYPE std::unique_ptr mlir_ts::createOwnershipInferencePass() { - return std::make_unique(); + return std::make_unique(); } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h new file mode 100644 index 000000000..05f837f49 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -0,0 +1,6909 @@ +// OwnershipInferencePass, the per-function inference of -mm=own. Its methods are defined across +// the lib/TypeScript/OwnershipInference*.cpp files, one analysis each; templates stay here. + +#ifndef MLIR_TYPESCRIPT_OWNERSHIPINFERENCEPASS_H +#define MLIR_TYPESCRIPT_OWNERSHIPINFERENCEPASS_H + +#include "mlir/Pass/Pass.h" +#include "mlir/IR/Dominance.h" +#include "mlir/Dialect/LLVMIR/LLVMAttrs.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Interfaces/ControlFlowInterfaces.h" +#include "mlir/Interfaces/FunctionInterfaces.h" + +#include "TypeScript/TypeScriptDialect.h" +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/TypeScriptFunctionPass.h" +#include "TypeScript/Passes.h" +#include "TypeScript/Defines.h" +#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" + +#include "OwnershipFacts.h" + +#include "llvm/ADT/MapVector.h" +#include "llvm/ADT/ScopeExit.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/STLFunctionalExtras.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace mlir_ts = mlir::typescript; + +namespace own_inference +{ + +using namespace own_facts; + +// This pass's own mark, for its own reading: a store of a value into the capture box of a closure +// that borrows what it captures (phase 5). The store is a read, not a taker. +#define OWN_CAPTURE_BORROW_ATTR_NAME "__own_capture_borrow" + +// Ownership inference for -mm=own, phases 0 and 1. See +// docs/superpowers/specs/2026-09-24-own-memory-model-design.md, sections 4.2, 10.6, 11 and 12. +// +// A fresh heap value may have at most one owner: one use that takes it - a local's +// declaration, a store into a field, element, global or return slot, an insertion into an +// array, a return - or, with none of those, the temporary itself, which rc gives back with a +// `ts.Release`. That owner's release is then its destroy, and every retain rc planted on the +// way is erased. Owners are counted from the uses themselves, not from rc's retains: rc also +// transfers ownership by *consuming* a value (`__owned_consumed`, a push, a field store), +// which leaves no retain behind to count. +// +// A move is decided by reachability on the affine CFG (spec 4.2 step 3): nothing may use the value +// after its taker, a taker that control comes back to without the value being made again is a +// move on every loop iteration (2.6), and each release the move reaches must be dominated by it +// and is erased. A release the move cannot reach stays - the owner on the paths that did not +// move. A local's release that paths which moved it and paths which did not both reach is placed +// instead on the edges where the latter join the former (spec 30). Anything else is an error at +// its location. +// +// It runs on every function: a `ts.Func`, and the `func.func` the async lowering outlines an +// `await`'s continuation or a `for await` body into, whose arguments are the values it captures. +class OwnershipInferencePass + : public mlir::PassWrapper> +{ + public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipInferencePass) + + void runOnOperation() override; + + mlir::FunctionOpInterface getFunction(); + + void runOnFunction(); + + // The analysis of one function; true when it passed. In a dry run it erases nothing and strips + // no facts. + bool analyze(llvm::SetVector &toErase); + + // Each analysis starts from nothing this function has not set up again. + void resetFunctionState(); + + // What the ownership operations of one function name, in the order the walk meets them + // (collectOwnershipOps). + struct FunctionOps + { + // every value some ownership operation names, in the order the walk meets them + llvm::SetVector candidates; + llvm::SmallVector retains; + // reads out of fields and elements, and results that borrow an argument, and what may + // destroy what they read (spec 2.3, 2.4) + llvm::SmallVector placeReads; + // reads of what a mortal global holds (isGlobalRead) + llvm::SmallVector globalReads; + // calls whose callee keeps an argument: each such argument moves into the call + llvm::SmallVector keepingCalls; + }; + + struct SlotReceiver + { + mlir::Operation *retain; + mlir::Value value; + mlir_ts::LoadOp load; + }; + + // the receivers of each owning local, or of each parameter this function keeps + using Receivers = llvm::MapVector>; + + // Every op of the function that names an ownership: the values to check, the retains to + // decide, the reads out of places and globals, the calls that keep an argument, and the drops. + void collectOwnershipOps(FunctionOps &ops, llvm::SetVector &toErase); + + // The owner check of each value, then of each read out of a place and of a mortal global. The + // fresh values proven, whose retains go. + llvm::DenseSet checkOwnershipOps(const FunctionOps &ops, llvm::SetVector &toErase); + + // Each retain: erased, given to the receivers of an owning local or of a kept parameter, or + // reported. + void decideRetains(const FunctionOps &ops, const llvm::DenseSet &proven, Receivers &slotReceivers, + Receivers ¶mReceivers, llvm::SetVector &toErase); + + // The arguments a callee keeps, given to the receivers of the local or kept parameter they come + // from. + void giveKeptArguments(const FunctionOps &ops, Receivers &slotReceivers, Receivers ¶mReceivers); + + // The facts were for this pass only. + void stripFacts(); + + private: + mlir::DominanceInfo *dominance = nullptr; + + // What in this function may destroy a value held by a field or an element: an overwrite of a + // field or an element, an array op that removes elements, a call. + llvm::SmallVector drops; + + // derivedFrom's answer for each value it was asked about in this function; held by pointer, so + // an answer stays put while the next one is added + llvm::DenseMap>> derivedCache; + + // While non-zero, verdicts are tried without reporting: the next verdict gets its turn first. + unsigned quiet = 0; + + // The dry run (spec 22.3): the whole analysis, but nothing reported and nothing changed. A + // report it would make is counted in `failures`. + bool dryRun = false; + unsigned failures = 0; + // attributes the dry run set on ops that did not have them, taken off again after it + llvm::SmallVector> dryRunAttrs; + + // Whether a report is to be emitted: not under `quietly`'s trial, and not in the dry run, which + // only counts it. + bool reporting(); + + // `op->setAttr(name, unit)`, undone after the dry run when the op did not have it + void setAttrTracked(mlir::Operation *op, llvm::StringRef name); + + // Strings as values (spec 22.3): each string retain the dry run did not erase is rewritten, where + // its one use can be found, into a copy for that use. + void makeStringCopies(const llvm::SetVector &erased); + + // The value a parameter of this function is, when its callers were told this function keeps it + // or returns a borrow of it (spec 15): copying it would leak what the caller gave up. + bool borrowsKnownParam(mlir::Value value); + + // Rewrites one string retain into a copy for the use it was made for (spec 22.3), and erases it; + // false, leaving it for the real run to report, where there is no single such use in its block, + // or the value is not a string or is a known parameter's (spec 22.4). Its use walk is takerOf's + // without the twinReadAfter fallback: a retain whose value has no use of its own is not copied, + // so its error stays. + bool copyForRetain(mlir::Operation *retain); + + // The parameter this function's result borrows (`__own_result_borrows`), or -1; and the + // parameters (or views of them) whose rc reference for the caller was already decided. + int returnsBorrowOf = -1; + llvm::DenseSet returnedParams; + + // The parameters this function's result is a borrowing record of (OWN_RESULT_RECORD_ATTR_NAME, + // spec 32); empty for none. + llvm::SmallVector returnsRecordOf; + + // ---- Closures (spec 2.5, phase 5) ---- + // + // A closure is `ts.CreateBoundFunction(box, @f) {__owns_capture}`; its box holds a cell for each + // variable it captures by reference and a copy of each it captures by value. One that does not + // escape borrows all of it: rc's references for the box go, the box frees itself only + // (OWN_BORROWS_CAPTURES_ATTR_NAME), and nothing that may run the closure may come after anything + // it borrows is given back. One that escapes would own all of it, which is not done yet. + struct Closure + { + mlir_ts::CreateBoundFunctionOp op; + mlir::Value box; + // each store that fills the box, with the field it fills + llvm::SmallVector> fills; + bool hasCells = false; + // the first use that takes the closure out of this function's hands + mlir::Operation *escape = nullptr; + // what may run the closure: calls given it, called through it, or through its box + llvm::SmallVector uses; + // the calls through the box itself, made without the closure value (a folded `const f`) + llvm::SmallVector boxUses; + // where the closure value is given back: its releases, and those of the `let`s holding it + llvm::SmallVector ends; + // rc's references for the box: `ts.RetainCell` of each cell, `ts.Retain` of each copy + llvm::SmallVector retains; + // held by a local that owns nothing (`let f: () => number; f = () => ...`): an alias, so + // the closure's own releases stay its owner + bool aliased = false; + // a generator's box whose state object borrows from the generator's arguments + // (OWN_BORROWING_FIELDS_ATTR_NAME): a parameter's cell moves in, and the box frees the cell only + bool borrowsValues = false; + // copies only, and given to a generator's maker that borrows from it (`.map(f)`): the box + // borrows its copies, as a box with cells would + bool borrowsCopies = false; + }; + + llvm::SmallVector> closures; + // a copy stored into a borrowing box -> its closure + llvm::DenseMap captureStores; + // rc's references for boxes, which the closures decide + llvm::DenseSet captureRetains; + // each closure over a cell, by its `ts.CreateBoundFunction` + llvm::DenseMap closureOf; + // each generator's capture box, which escapes with its state object (classifyStateBoxes) + llvm::DenseMap stateBoxes; + // rc's retains for stores into borrowing fields, and those fields' releases (classifyBorrowingFields) + llvm::DenseSet fieldBorrowRetains; + llvm::SmallVector fieldBorrowReleases; + + // Finds the closures over cells and what they borrow, before anything else: whether a box's + // stores are reads or takers depends on it. A closure with copies only keeps what phase 4 does + // with it - its box owns them, and each copy moves in. + void classifyClosures() + { + getFunction().walk([&](mlir_ts::CreateBoundFunctionOp boundOp) { + if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME) || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return; + } + + auto closure = std::make_shared(); + closure->op = boundOp; + closure->box = boundOp.getThisVal(); + for (auto *user : closure->box.getUsers()) + { + if (user == boundOp.getOperation()) + { + continue; + } + + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + closure->escape = closure->escape ? closure->escape : fieldUser; + continue; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (isCall(user)) + { + closure->uses.push_back(user); + closure->boxUses.push_back(user); + continue; + } + + closure->escape = closure->escape ? closure->escape : user; + } + + if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); + escape && !closure->escape) + { + closure->escape = escape; + } + + // copies only: the box owns them (phase 4); only an alias needs deciding - unless the box + // is given to a generator's maker whose result borrows from it + if (!closure->hasCells) + { + // ...and only when every copy has an owner of its own: the box frees itself alone, so a + // value made for it (a callback that captures, `a.filter((x) => x > n)`) would be + // nobody's + closure->borrowsCopies = + !closure->escape && + llvm::any_of(closure->boxUses, + [](mlir::Operation *use) { return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); }) && + llvm::none_of(closure->fills, [&](auto &fill) { + auto value = fill.second.getValue(); + return !holdsNoBlock(value) && isFresh(rootOf(value)); + }); + if (!closure->borrowsCopies) + { + if (!closure->escape && closure->aliased) + { + closures.push_back(std::move(closure)); + } + + return; + } + } + + for (auto &fill : closure->fills) + { + auto storeOp = fill.second; + auto isCell = mlir::isa(storeOp.getValue().getType()); + // an escaping closure's copies move into its box as in phase 4, rc's retain with them + if (closure->escape && !isCell) + { + continue; + } + + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + if (!closure->escape && !isCell) + { + setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); + captureStores[storeOp] = closure.get(); + } + } + + if (!closure->escape) + { + setAttrTracked(boundOp, OWN_BORROWS_CAPTURES_ATTR_NAME); + } + + closureOf[boundOp.getOperation()] = closure.get(); + closures.push_back(std::move(closure)); + }); + } + + // The fields of a generator's state object of this type that borrow from its arguments + // (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first. + llvm::SmallVector borrowingFieldsOf(mlir::Type recordType) + { + llvm::SmallVector fields; + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return fields; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType) + { + auto indices = mlir::cast(entry[1]).asArrayRef(); + fields.append(indices.begin(), indices.end()); + } + } + + return fields; + } + + // Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)? + bool isYieldedField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto recordType = propertyRefOp->getOperand(0).getType(); + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_YIELDED_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return false; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType && + llvm::is_contained(mlir::cast(entry[1]).asArrayRef(), + static_cast(propertyRefOp.getPosition()))) + { + return true; + } + } + + return false; + } + + // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? + bool isBorrowingField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto fields = borrowingFieldsOf(propertyRefOp->getOperand(0).getType()); + return fields.size() > 1 && llvm::is_contained(llvm::ArrayRef(fields).drop_front(), + static_cast(propertyRefOp.getPosition())); + } + + // A store into a borrowing field holds what it stores without owning it, as a borrowing box's + // copy does (OWN_CAPTURE_BORROW_ATTR_NAME), and rc's retain for it goes; its release, when the + // field is overwritten, gives nothing back. + void classifyBorrowingFields() + { + getFunction().walk([&](mlir::Operation *op) { + if (auto storeOp = mlir::dyn_cast(op); storeOp && isBorrowingField(storeOp.getReference())) + { + setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + fieldBorrowRetains.insert(retain); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast(op); + releaseSlotOp && isBorrowingField(releaseSlotOp.getSlot())) + { + fieldBorrowReleases.push_back(op); + } + }); + } + + // Is `ref` a cell read out of the `.captured` box of a generator's state object that borrows: + // `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`? + bool isBorrowingCell(mlir::Value ref) + { + auto cellLoad = ref.getDefiningOp(); + return cellLoad && isBorrowingBoxField(cellLoad.getReference()); + } + + // Is `ref` a field of the `.captured` box of a generator's state object that borrows: + // `ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i)`? A field copied in by value holds + // the caller's value itself, a cell field the caller's cell. + bool isBorrowingBoxField(mlir::Value ref) + { + auto cellRef = ref.getDefiningOp(); + auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + if (!boxRef) + { + return false; + } + + auto fields = borrowingFieldsOf(boxRef->getOperand(0).getType()); + return !fields.empty() && fields.front() == static_cast(boxRef.getPosition()); + } + + // What a generator reads out of a borrowing cell or field is the caller's: every use of it must + // be a borrow - a store into another borrowing field is one (classifyBorrowingFields). A yield of + // it, a push, a return, a store anywhere else would give the caller's value a second owner, and + // rc retains nothing for some of these, so nothing else would see it. + void checkBorrowedReads() + { + getFunction().walk([&](mlir_ts::LoadOp loadOp) { + auto ref = loadOp.getReference(); + if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingBoxField(ref) || isBorrowingField(ref))) + { + return; + } + + auto reported = false; + forEachUse(loadOp.getResult(), [&](mlir::Operation *user, mlir::Value used) { + if (reported || isBorrow(user, used) || fieldBorrowRetains.contains(user)) + { + return; + } + + reportPlaceBorrowEscapes(user, ownerName(loadOp.getResult()), "the generator's argument"); + reported = true; + }); + }); + } + + // A call whose result borrows from its arguments (OWN_RESULT_BOUNDED_ATTR_NAME): a generator over + // a parameter's block. The result is owned as any fresh value is, but nothing may use it after + // one of those arguments is given back - released, assigned, or dropped from the place it was + // read from - and it may not be kept anywhere that could outlive them. Its releases are not + // uses: the state object's release gives back nothing it borrows. + void checkBoundedResults() + { + getFunction().walk([&](mlir::Operation *call) { + auto bounded = call->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + if (!bounded || !isCall(call) || call->getNumResults() != 1) + { + return; + } + + auto name = ownerName(call->getResult(0)); + llvm::SmallVector uses; + if (auto *kept = boundedUses(call->getResult(0), uses)) + { + reportPlaceBorrowEscapes(kept, name, "its generator's arguments"); + return; + } + + auto args = callArgs(call); + for (auto index : bounded.asArrayRef()) + { + if (static_cast(index) >= args.size()) + { + continue; + } + + // a closure's box (`.map(f)` calls its maker with it): it lives while the closures on + // it do, and what it holds while their owners do. A reference to storage, so first. + auto root = rootOf(args[index]); + if (auto boxOp = root.getDefiningOp(); boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) + { + if (!checkBoundedByBox(call, root, uses, name)) + { + return; + } + + continue; + } + + if (holdsNoBlock(args[index]) || isImmortalLiteral(args[index])) + { + continue; + } + + if (borrowedParam(root) >= 0) + { + continue; // the caller's caller's: alive for all of this function + } + + Chain chain; + auto *read = placeReadOf(root); + if (read) + { + chain = chainOf(read); + } + else if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + chain.roots.push_back({root, Chain::NotOwned}); + } + + // the root's own ends; and, for a read out of a place, whatever may destroy what the + // place holds (a local or a made value is not destroyed by a call it is given) + auto ends = rootEnds(chain); + for (auto *drop : read ? llvm::ArrayRef(drops) : llvm::ArrayRef()) + { + if (drop != call && dropsChain(drop, chain, read)) + { + ends.push_back(drop); + } + } + + auto owner = ownerName(root); + for (auto *end : ends) + { + if (end == call) + { + continue; + } + + for (auto *use : uses) + { + if (use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, owner); + return; + } + } + } + } + }); + } + + // The ends of a capture box given to a call with a bounded result: each release of a closure on + // it (the box goes with its last), and each end of what a field of it was filled from. Reports + // and returns false when a use of the result comes after one. + bool checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, + llvm::StringRef name) + { + llvm::SmallVector ends; + for (auto *user : box.getUsers()) + { + if (auto boundOp = mlir::dyn_cast(user); boundOp && boundOp.getThisVal() == box) + { + llvm::SmallVector closureUses; + auto aliased = false; + if (followClosure(boundOp.getResult(), closureUses, ends, aliased)) + { + reportPlaceBorrowEscapes(call, name, "a closure that escapes"); // the box may go anywhere + return false; + } + + continue; + } + + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult() || holdsNoBlock(storeOp.getValue())) + { + continue; + } + + auto root = rootOf(storeOp.getValue()); + Chain chain; + if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (borrowedParam(root) >= 0 || isImmortalLiteral(root)) + { + continue; + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + reportPlaceBorrowEscapes(call, name, "a value this function cannot bound"); + return false; + } + + auto rootEndsOf = rootEnds(chain); + ends.append(rootEndsOf.begin(), rootEndsOf.end()); + } + } + + for (auto *end : ends) + { + for (auto *use : uses) + { + if (end != call && use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, "the generator's source"); + return false; + } + } + } + + return true; + } + + // Every use of a bounded result, through its views, the locals that hold it, and the function + // and object a call through it is made with. Answers the first use that keeps it - anything + // that is not a read, a call, a local, or rc's bookkeeping - or null. + mlir::Operation *boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses) + { + llvm::SmallVector values{result}; + llvm::SmallPtrSet seen; + mlir::Operation *kept = nullptr; + while (!values.empty() && !kept) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (kept || mlir::isa(user)) + { + return; + } + + mlir::Value slot; + if (auto varOp = mlir::dyn_cast(user)) + { + slot = varOp.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) + { + slot = storeOp.getReference(); + } + + if (slot) + { + auto slotVar = slot.getDefiningOp(); + if (!slotVar || slotVar.getCaptured().value_or(false)) + { + kept = user; + return; + } + + for (auto *slotUser : slot.getUsers()) + { + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + } + + return; + } + + // returned by a function that says so (`@borrows`): its callers hold the result to + // the same bound, and the signature pass checked the arguments are its own + if (mlir::isa(user) && getFunction()->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + return; + } + + uses.push_back(user); + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + kept = user; + } + + return; + } + + // `it.next()`: the field read, the bound function, its `this` + if (mlir::isa(user)) + { + for (auto partResult : user->getResults()) + { + values.push_back(partResult); + } + + return; + } + + if (!isBorrow(user, used)) + { + kept = user; + } + }); + } + + return kept; + } + + // A generator's maker puts what the generator captures - its parameters, an outer local - into + // a box, and the box into its state object's `.captured` field: the box of a closure that + // escapes, with the state object, at that store. There is no closure value, so no use runs it + // here and nothing calls through it. rc's `ts.RetainCell` of the box is the state object's + // reference, which moves in with it. A box used any other way is left to the RetainCell error. + void classifyStateBoxes() + { + getFunction().walk([&](mlir_ts::VariableOp boxOp) { + auto box = boxOp.getResult(); + if (!boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) || closureOfBox(box)) + { + return; + } + + auto closure = std::make_shared(); + closure->box = box; + llvm::SmallVector boxRetains; + for (auto *user : box.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + return; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (mlir::isa(user)) + { + boxRetains.push_back(user); + continue; + } + + if (!closure->escape && isStoreIntoState(user, box)) + { + closure->escape = user; + continue; + } + + return; + } + + if (!closure->escape) + { + return; + } + + auto stateRef = mlir::cast(closure->escape).getReference().getDefiningOp(); + auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); + auto borrowing = borrowingFieldsOf(stateType); + closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); + // `.map`'s maker copies what its parameter box holds into the generator's box: a borrow + if (closure->borrowsValues) + { + for (auto &fill : closure->fills) + { + if (!mlir::isa(fill.second.getValue().getType())) + { + setAttrTracked(fill.second, OWN_CAPTURE_BORROW_ATTR_NAME); + } + } + } + + for (auto &fill : closure->fills) + { + // a parameter this function does not own, in a box that does not borrow: the box + // cannot take rc's reference for it, the caller's still holds it; left to the + // second-reference error, which says the callers would have to give it up + auto param = borrowedParam(fill.second.getValue()); + if (!closure->borrowsValues && param >= 0 && !llvm::is_contained(ownedParams(getFunction()), param)) + { + continue; + } + + if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + } + + for (auto *retain : boxRetains) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + stateBoxes[box] = closure.get(); + closures.push_back(std::move(closure)); + }); + } + + // `ts.Store(box, ts.PropertyRef(state, i))`, where `state` is a local a `ts.Constant` + // initializes whose every read fills a block `ts.New` made: a generator's initial state. + static bool isStoreIntoState(mlir::Operation *user, mlir::Value box) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == box + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto stateOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp || !isConstantRecordLocal(stateOp)) + { + return false; + } + + auto reads = 0; + for (auto *stateUser : stateOp->getUsers()) + { + auto loadOp = mlir::dyn_cast(stateUser); + if (!loadOp) + { + continue; + } + + ++reads; + for (auto *readUser : loadOp->getUsers()) + { + // rc's retain of the initial state, for fields that own (holdsConstantsOrMoves) + if (mlir::isa(readUser)) + { + continue; + } + + auto fillOp = mlir::dyn_cast(readUser); + if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) + { + return false; + } + } + } + + return reads > 0; + } + + // rc's reference for the box's copy of `value`: the retain of it nearest before the store that + // fills the box, in the same block. + mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store) + { + // rc's retain of an earlier read of the same place (twinReadAfter) + if (auto loadOp = value.getDefiningOp()) + { + for (auto *user : loadOp.getReference().getUsers()) + { + auto earlier = mlir::dyn_cast(user); + auto *retain = earlier ? soleUser(earlier.getResult()) : nullptr; + if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && + twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && + retain->isBeforeInBlock(store)) + { + return retain; + } + } + } + + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (!mlir::isa(user) || user->getBlock() != store->getBlock() || + !user->isBeforeInBlock(store) || captureRetains.contains(user)) + { + continue; + } + + if (!found || found->isBeforeInBlock(user)) + { + found = user; + } + } + + return found; + } + + // Where a closure value goes: through its views and the owning `let`s that hold it, each use + // that may run it joins `uses`, and each release `ends`. Answers the first use that takes it + // anywhere else - returned, stored, kept by a callee, captured - or null. Anything this does + // not know is such a use. + mlir::Operation *followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &ends, bool &aliased) + { + llvm::SmallVector values{closure}; + llvm::SmallPtrSet seen; + mlir::Operation *escape = nullptr; + auto followSlot = [&](mlir::Value slot) { + for (auto *slotUser : slot.getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + if (mlir::isa(slotUser)) + { + ends.push_back(slotUser); + } + else if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + else if (auto storeOp = mlir::dyn_cast(slotUser); !storeOp || storeOp.getReference() != slot) + { + escape = escape ? escape : slotUser; + } + } + }; + + while (!values.empty() && !escape) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (escape || mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + ends.push_back(user); + return; + } + + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + escape = user; + } + else + { + uses.push_back(user); + } + + return; + } + + // the function and the box a call through the closure is made with + if (mlir::isa(user)) + { + for (auto *partUser : user->getResult(0).getUsers()) + { + if (isCall(partUser) && !isKeptArgument(partUser, user->getResult(0))) + { + uses.push_back(partUser); + } + else + { + escape = escape ? escape : partUser; + } + } + + return; + } + + mlir::Value slot; + if (auto varOp = mlir::dyn_cast(user)) + { + slot = varOp.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) + { + slot = storeOp.getReference(); + } + + // a local, owning or not (`let f: () => number;` owns nothing): its reads are the + // closure; a function's result slot is a local too, and its read is returned + auto slotVar = slot ? slot.getDefiningOp() : mlir_ts::VariableOp(); + if (slotVar && !slotVar.getCaptured().value_or(false)) + { + aliased = aliased || !isOwningVariable(slotVar); + followSlot(slot); + return; + } + + escape = user; + }); + } + + return escape; + } + + // Does a call given this value give it a closure, which may run and touch what its box holds? + static bool holdsCaptures(mlir::Value value) + { + if (mlir::isa(value.getType())) + { + return true; + } + + auto root = rootOf(value); + if (auto varOp = root.getDefiningOp()) + { + return varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); + } + + // a closure's `this` is its box, a reference; a bound method's is the object + auto getThisOp = root.getDefiningOp(); + return getThisOp && mlir::isa(getThisOp.getType()); + } + + // The verdicts on the closures over cells: an escape is an error; a borrow must not be run after + // anything it borrows is given back - a cell this function owns at its releases, a copy at its + // owner's ends. A cell an enclosing closure holds and a parameter end nowhere here; a copy of a + // read out of a container is bounded by the read's own check, which sees the closure's uses + // (walkBorrowed). + void decideClosures(llvm::SetVector &toErase) + { + for (auto &closure : closures) + { + if (!closure->op) + { + decideEscaping(*closure, toErase); // a generator's box + continue; + } + + auto *boundOp = closure->op.getOperation(); + auto name = ownerName(closure->op.getResult()); + auto ok = true; + // Held by a local that owns nothing: rc's birth reference reads to the move logic as the + // store's, which would leave the box with no owner. The closure keeps its releases, and + // nothing may run it after them. + auto keepAliasedReleases = [&]() { + for (auto *end : closure->ends) + { + if (!mlir::isa(end)) + { + continue; + } + + toErase.remove(end); + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + }; + + if (!closure->hasCells && !closure->borrowsCopies) + { + keepAliasedReleases(); + continue; + } + + if (closure->escape) + { + decideEscaping(*closure, toErase); + continue; + } + + for (auto &fill : closure->fills) + { + auto value = fill.second.getValue(); + llvm::SmallVector ends; + llvm::StringRef captured = "this value"; + if (mlir::isa(value.getType())) + { + if (isCellVariable(value)) + { + captured = varName(value.getDefiningOp()); + for (auto *user : value.getUsers()) + { + if (mlir::isa(user) || movesCellAway(user)) + { + ends.push_back(user); + } + } + } + else if (!isLoadedCell(value)) + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + } + else + { + auto root = rootOf(value); + captured = ownerName(root); + Chain chain; + if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0 || isHandle(root)) + { + // nothing to own, a read the read's check bounds, a parameter, or a handle, + // whose copy in the box is counted (spec 23.2) + } + else if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + + ends = rootEnds(chain); + } + + for (auto *end : ends) + { + if (captureRetains.contains(end) || captureStores.count(end)) + { + continue; // a box's own copy, this one's or another borrowing closure's + } + + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, captured); + ok = false; + } + } + } + } + + if (closure->aliased) + { + keepAliasedReleases(); + } + + // a call through the box, after the closure value that owns the box is given back + for (auto *end : closure->ends) + { + for (auto *use : closure->boxUses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + + toErase.insert(closure->retains.begin(), closure->retains.end()); + } + } + + // Is this the store that moves a cell into the box of a closure that escapes? + bool movesCellAway(mlir::Operation *op) + { + auto storeOp = mlir::dyn_cast(op); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + if (!propertyRefOp || !mlir::isa(storeOp.getValue().getType())) + { + return false; + } + + if (stateBoxes.count(propertyRefOp.getObjectRef())) + { + return true; + } + + auto boundOp = closureOfBox(propertyRefOp.getObjectRef()); + auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); + return found != closureOf.end() && found->second->escape; + } + + // A closure that escapes owns what its box holds (spec 2.5, phase 5b). Its copies moved in as + // in phase 4. Each cell moves in where the box takes it: that store is the move, rc's + // `ts.RetainCell` for it goes, and so does each of the frame's `ts.ReleaseCell`s the move reaches, + // which it must dominate. Nothing in the frame may use the variable after the move - read it, + // assign it, capture it again - and the move may not come round a loop to a cell made outside + // it. A cell that borrows its value (a parameter's) or one an enclosing closure holds has + // nothing to move. And nothing may call through the box once the closure has gone. + void decideEscaping(Closure &closure, llvm::SetVector &toErase) + { + auto ok = true; + for (auto &fill : closure.fills) + { + auto value = fill.second.getValue(); + mlir::Operation *move = fill.second; + if (!mlir::isa(value.getType())) + { + continue; + } + + if (isLoadedCell(value)) + { + reportEscapingCellNotOwned(move, closure.escape, "it is captured from an enclosing function"); + ok = false; + continue; + } + + auto varOp = isCellVariable(value) ? value.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp) + { + reportCaptureNotOwned(move, "this value"); + ok = false; + continue; + } + + auto name = varName(varOp); + auto elementType = mlir::cast(value.getType()).getElementType(); + // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) + if (!isOwningVariable(varOp) && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) && + !MLIRTypeHelper::isSharedHandleType(elementType)) + { + reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); + ok = false; + continue; + } + + if (takerLoops(move, varOp)) + { + reportMovedInLoop(move, name); + ok = false; + continue; + } + + llvm::SmallVector moved; + for (auto *user : value.getUsers()) + { + if (user == move || llvm::is_contained(closure.retains, user) || !reachableAfter(move, user, varOp)) + { + continue; + } + + if (!mlir::isa(user)) + { + reportUsedAfterEscapingCapture(user, move, name); + ok = false; + break; + } + + if (!dominance->properlyDominates(move, user)) + { + reportMovedOnSomePaths(move, user, name); + ok = false; + break; + } + + moved.push_back(user); + } + + toErase.insert(moved.begin(), moved.end()); + } + + // A call through the box itself after the closure was given away or given back. followClosure + // stops at the escape, so `ends` may miss releases past it; that is enough while the only + // calls through a box are those of a folded `const f`, whose release ends its block. A + // generator's box has no closure value and no such call. + if (!closure.op) + { + toErase.insert(closure.retains.begin(), closure.retains.end()); + return; + } + + auto name = ownerName(closure.op.getResult()); + llvm::SmallVector gone(closure.ends.begin(), closure.ends.end()); + gone.push_back(closure.escape); + for (auto *end : gone) + { + for (auto *use : closure.boxUses) + { + if (ok && use != end && reachableAfter(end, use, closure.op.getOperation())) + { + reportUseAfterMove(use, end, name); + ok = false; + } + } + } + + toErase.insert(closure.retains.begin(), closure.retains.end()); + } + + // A captured variable whose value this function does not own - a parameter, `this` - has a cell + // that borrows the value: the cell's release frees the cell only (OWN_CELL_BORROWS_ATTR_NAME), + // and nothing may assign the variable, here or in a closure over it, since the old value is the + // caller's. A variable whose value nobody can be said to own is an error. + void decideCells() + { + getFunction().walk([&](mlir_ts::VariableOp varOp) { + auto cell = varOp.getResult(); + if (!isCellVariable(cell) || isOwningVariable(varOp)) + { + return; + } + + auto elementType = mlir::cast(cell.getType()).getElementType(); + if (!ownsHeap(varOp.getLoc(), elementType)) + { + return; + } + + // A handle's cell holds a count of its own, taken where it is made (spec 23.2): its + // release gives that back, as under rc, and an assignment is a counted copy. + if (MLIRTypeHelper::isSharedHandleType(elementType)) + { + return; + } + + auto name = varName(varOp); + auto init = varOp.getInitializer(); + auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || + !mlir::isa(argument.getOwner()->getParentOp())) + { + reportCaptureNotOwned(varOp, name); + return; + } + + for (auto *user : cell.getUsers()) + { + if (mlir::isa(user)) + { + setAttrTracked(user, OWN_CELL_BORROWS_ATTR_NAME); + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (mlir::isa(user) || (storeOp && storeOp.getReference() == cell)) + { + reportCapturedParamAssigned(user, name); + return; + } + + // captured: the closure's body must not assign it either + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp) + { + continue; + } + + auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); + if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) + { + reportCapturedParamAssigned(boundOp, name); + return; + } + } + }); + } + + // An argument read out of a local is lent to the call, but the arguments are read in order: a + // later one may give the local a new value before the call takes the first - `g(n, (n = mk(), 0))`, + // in a conditional argument too - or call a closure that assigns it, when it is captured. The + // assignment drops the block the argument is, and the callee reads freed memory. rc holds such an + // argument across the call instead (OwnedReturnConsumptionPass, holdGlobalArguments). A read of a + // global is checkGlobalRead's, and a store into a place in the local the place borrows' (a field + // read `g(n.v, (n.v = mk(), 0))`); a store into the local's slot ends `n.v` too. + void checkArgumentsAssigned() + { + getFunction().walk([&](mlir::Operation *call) { + if (!isCall(call)) + { + return mlir::WalkResult::advance(); + } + + for (auto argument : callArgs(call)) + { + // a method's object: `this` of the method bound to it + if (auto getThisOp = argument.getDefiningOp()) + { + auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); + if (mlir::isa_and_nonnull(boundOp)) + { + argument = boundOp->getOperand(0); + } + } + + auto *read = argument.getDefiningOp(); + auto local = mlir::dyn_cast_or_null(slotOf(argument)); + if (!read || !local || !ownsHeap(argument) || isHandle(argument)) + { + continue; + } + + auto assignedByClosure = closureAssigns(local); + auto *write = findBetween(read, call, [&](mlir::Operation *op) { + if (assignedByClosure && isCall(op)) + { + return true; + } + + auto storeOp = mlir::dyn_cast(op); + auto releaseSlotOp = mlir::dyn_cast(op); + return (storeOp && storeOp.getReference() == local.getResult()) || + (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); + }); + if (!write) + { + continue; + } + + if (reporting()) + { + auto name = varName(local); + auto callee = calleeName(call); + auto diag = call->emitError("'") + << name << "' is lent to '" << (callee.empty() ? "a function" : callee) + << "', but is given a new value before the call takes it; -mm=own cannot prove " + "it outlives the call"; + diag.attachNote(write->getLoc()) << "'" << name << "' is given a new value here"; + signalPassFailure(); + } + + return mlir::WalkResult::interrupt(); + } + + return mlir::WalkResult::advance(); + }); + } + + // Does a closure built over this captured local assign it (OWN_ASSIGNS_CAPTURES_ATTR_NAME)? Yes too + // when its cell goes where this does not follow. + bool closureAssigns(mlir_ts::VariableOp local) + { + if (!local.getCaptured().value_or(false)) + { + return false; + } + + for (auto *user : local->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (mlir::isa(user) || + (storeOp && storeOp.getReference() == local.getResult())) + { + continue; + } + + // the cell put into a closure's capture box + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp) + { + return true; + } + + auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); + if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) + { + return true; + } + } + + return false; + } + + template bool quietly(F check) + { + ++quiet; + auto result = check(); + --quiet; + return result; + } + + // Move, else borrow, else error (spec 2.2) for the receivers of one owning local. + void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto owner = varName(slot.getDefiningOp()); + if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers)) + { + // a move on some paths only is placed when it is the local's one receiver (spec 30) + for (auto &receiver : receivers) + { + decideSlotReceiver(slot, receiver, owner, toErase, receivers.size() == 1); + } + + return; + } + + decideSlotReceivers(slot, receivers, owner, toErase); + } + + void decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, + llvm::SetVector &toErase, bool sole) + { + if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); })) + { + // rc's retain goes with the move; a call that keeps the argument is the move itself + if (!isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + + return; + } + + if (auto borrower = borrowerOf(receiver.retain)) + { + borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase); + return; + } + + checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); // reports + } + + // Receivers of one owning local that each read a value of its own (#528): `h.c = a; a = new C(); + // g.c = a`. An assignment comes between every two of their reads on every path from one to the + // other, so each is decided alone, as the one receiver of the value it reads. So are receivers + // no path joins at all, assigned or not: `if (c) return a; return a;` moves `a` once on each + // way out. + bool takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers) + { + auto assignments = slotAssignments(slot); + llvm::SmallVector kills(assignments.begin(), assignments.end()); + if (auto varOp = slot.getDefiningOp()) + { + kills.push_back(varOp); + } + + for (auto &from : receivers) + { + for (auto &to : receivers) + { + if (&from != &to && reachableAfter(from.load, to.load, kills)) + { + return false; + } + } + } + + return true; + } + + // ---- A kept parameter released where it is not moved (spec 33) ---- + // + // `return v.x > 2 ? acc : V.plus(acc, v)`: `acc` is kept (OwnershipSignaturePass), so the caller + // moved it in; the path that returns it moves it out, and the others must release it. So must a + // callback that only borrows the accumulator its family gives it to keep. MLIRGen made no owner + // of a parameter, so its releases are put in here, before each way out of the body - a return, + // a throw that leaves the function - as MLIRGen puts an owning local's. The move's own check then + // erases those it reaches, and places the release on the edges into the join that did not move + // it (spec 30), statically: no run-time flag. Unwinding out of the function (a call that throws + // outside a `try`, a `finally` passed on the way out) gives nothing back: it leaks, as an owning + // local does there. + + // the releases put in for this run + llvm::SmallVector exitReleases; + + // The slot of parameter `index` (isParameterSlot), or null. + mlir_ts::VariableOp parameterSlot(int32_t index) + { + auto &body = getFunction().getFunctionBody(); + if (body.empty() || static_cast(index) >= body.front().getNumArguments()) + { + return {}; + } + + for (auto *user : body.front().getArgument(index).getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + if (varOp && isParameterSlot(varOp, position) && position == index) + { + return varOp; + } + } + + return {}; + } + + // Is `slot` a parameter this function keeps and releases where it does not move it? + bool isReleasedParam(mlir::Value slot) + { + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + auto varOp = slot.getDefiningOp(); + auto index = -1; + return attr && varOp && isParameterSlot(varOp, index) && llvm::is_contained(attr.asArrayRef(), index) && + llvm::is_contained(ownedParams(f), index); + } + + // Does `value` come from a read of `slot`: computed from one, through any op? + static bool readsSlot(mlir::Value value, mlir::Value slot, int depth = 0) + { + auto *def = value.getDefiningOp(); + if (!def) + { + return false; + } + + // too deep to tell: as if it did + if (depth > 16) + { + return true; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && loadOp.getReference() == slot) + { + return true; + } + + return llvm::any_of(def->getOperands(), [&](mlir::Value operand) { return readsSlot(operand, slot, depth + 1); }); + } + + void releaseKeptParamsAtExits() + { + exitReleases.clear(); + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + if (!attr) + { + return; + } + + for (auto index : attr.asArrayRef()) + { + auto varOp = parameterSlot(index); + if (!varOp || !llvm::is_contained(ownedParams(f), index)) + { + continue; + } + + for (auto &block : f.getFunctionBody()) + { + auto *exit = block.empty() ? nullptr : &block.back(); + // a thrown value made of the parameter takes it with it: no release there (a leak, + // if it does not) + auto leaves = mlir::isa_and_nonnull(exit) || + (mlir::isa_and_nonnull(exit) && + !readsSlot(exit->getOperand(0), varOp.getResult())); + if (!leaves) + { + continue; + } + + mlir::OpBuilder builder(exit); + exitReleases.push_back(builder.create(exit->getLoc(), varOp.getResult())); + } + } + } + + // A parameter this function keeps (`__own_params`): its caller moved the argument in, and this + // function has no release for it, so exactly one taker must move it on, on every way out of + // the function, and nothing may read it after that. + void decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto name = varName(slot.getDefiningOp()); + auto &receiver = receivers.front(); + auto *taker = takerOf(receiver.retain, receiver.value); + if (receivers.size() > 1) + { + auto &second = receivers[1]; + auto *secondTaker = takerOf(second.retain, second.value); + reportUseAfterMove(secondTaker ? secondTaker : second.retain, taker ? taker : receiver.retain, name); + return; + } + + // one the body releases on the paths that do not move it (spec 33): the move's own check + // erases the releases at the exits it reaches, and places one on each edge into where the + // paths join that did not move it (spec 30) + auto released = isReleasedParam(slot); + if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, released)) + { + return; + } + + if (released) + { + if (!isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + + return; + } + + auto &body = getFunction().getFunctionBody(); + auto *moved = body.findAncestorOpInRegion(*taker); + auto everyPath = true; + getFunction().walk([&](mlir_ts::ReturnInternalOp returnOp) { + auto *exit = body.findAncestorOpInRegion(*returnOp); + if (everyPath && (!moved || !exit || !dominance->dominates(moved, exit))) + { + reportKeptOnSomePaths(taker, returnOp, name); + everyPath = false; + } + }); + + // rc's retain goes with the move - a `ts.RetainSlot` too, for an owning local the parameter + // moves into (`let r = init`, spec 33); a call that keeps it is the move itself + if (everyPath && !isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + } + + // The thrown value, read out of the exception object by a catch's copy thunk (`.eh.copy.*`, + // which the C++ runtime calls to copy the exception into the catch). rc retains it for the + // copy; own moves it: the exception object never gives its reference back (a throw hands the + // thrower's reference to it, and nothing releases it - rc leaks it the same way), and each + // exception is copied into one catch, since a TypeScript rethrow is a new throw. + bool isReadOutOfException(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); + return argument && argument.getOwner()->isEntryBlock() && getFunction().getName().starts_with(".eh.copy."); + } + + // The read of a parameter this function keeps, through its views. None for anything else. + mlir_ts::LoadOp keptParamLoadOf(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index) || !llvm::is_contained(ownedParams(getFunction()), index)) + { + return {}; + } + + return loadOp; + } + + // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, so + // none may move it. A receiver that is not a `let` reports its move error - another + // receiver's reads come after it - or, if its move alone would pass, that a borrow pins it. + void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto ends = slotEnds(slot); + mlir_ts::VariableOp someBorrower; + for (auto &receiver : receivers) + { + if (auto borrower = borrowerOf(receiver.retain)) + { + someBorrower = borrower; + borrowWith(borrower, receiver.retain, ends, owner, toErase); + } + } + + for (auto &receiver : receivers) + { + if (borrowerOf(receiver.retain)) + { + continue; + } + + llvm::SetVector unused; + if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) + { + reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); + } + } + } + + // A `let` that could borrow: owning, declared from a value, holding a reference it took + // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. With + // `consumed`, also one that took over the reference rc thought the value carried: a call's + // result that turned out to borrow an argument carries none. + static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition, bool consumed = false) + { + mlir_ts::VariableOp varOp; + if (auto retainSlotOp = mlir::dyn_cast(acquisition)) + { + varOp = retainSlotOp.getSlot().getDefiningOp(); + } + else if (auto retainOp = mlir::dyn_cast(acquisition)) + { + // rc's reference for a `let` that takes the value over: `let ri = raw` retains the + // interface and declares `ri` consuming it + varOp = consumingLetOf(retainOp.getReference()); + consumed = true; + } + else + { + varOp = mlir::dyn_cast(acquisition); + } + + if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return {}; + } + + if (varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return consumed ? varOp : mlir_ts::VariableOp(); + } + + auto retains = llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + return retains ? varOp : mlir_ts::VariableOp(); + } + + // tryBorrow for the `let` an acquisition gives a reference: a `ts.Retain` of the value a + // consuming `let` takes over is that `let`'s, and goes with the borrow; a `ts.RetainSlot` is the + // `let`'s own bookkeeping already. + bool borrowWith(mlir_ts::VariableOp borrower, mlir::Operation *acquisition, llvm::ArrayRef ends, + llvm::StringRef owner, llvm::SetVector &toErase) + { + if (!tryBorrow(borrower, ends, owner, toErase)) + { + return false; + } + + if (mlir::isa(acquisition)) + { + toErase.insert(acquisition); + } + + return true; + } + + // The owning `let` declared from `value` that takes over its reference, or none. + static mlir_ts::VariableOp consumingLetOf(mlir::Value value) + { + for (auto *user : value.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + if (varOp && varOp.getInitializer() == value && varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return varOp; + } + } + + return {}; + } + + // Where an owning local stops holding its value: each release of its slot and each store + // into it. + static llvm::SmallVector slotEnds(mlir::Value slot) + { + llvm::SmallVector ends; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + + return ends; + } + + // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its + // retain and every release of its slot go, and the owner keeps its own. Sound while no use of + // the borrower can run after any of `ends` - the owner's releases and assignments - and while + // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use + // through the borrower's own declaration is a new borrow, not this one. Reports and returns + // false otherwise. + bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto name = varName(borrower); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + mlir::Operation *declaration = borrower; + if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) + { + if (isAssignmentOf(kept, borrower)) + { + reportBorrowerAssigned(kept, name, owner); + } + else + { + reportBorrowEscapes(kept, name, owner); + } + + return false; + } + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, name, owner); + return false; + } + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + // A use of a borrowed value, and where the value it sees is made again: a path from a release + // to the use through one of `kills` reaches a new value, not this one. For the borrow's own + // uses that is the read or the borrowing `let`; for a use through a merge it is the merge's + // block, and through a local that owns nothing, that local's declaration and assignments. + struct BorrowUse + { + mlir::Operation *op; + llvm::SmallVector kills; + // the function value an indirect call goes through: read when the call starts, not while + // the callee runs + bool callee = false; + }; + + // A borrowing `let`'s slot: each read, and everything it leads to (walkBorrowed), joins + // `uses`, made again at `kills`; its `ts.RetainSlot` and `ts.ReleaseSlot`s are `bookkeeping`, + // which a borrow erases. Answers the first use that would keep what the slot holds - an + // assignment, or anything else that is not a read - or null. + mlir::Operation *collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, + llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &bookkeeping) + { + for (auto *user : borrower.getResult().getUsers()) + { + if (mlir::isa(user)) + { + bookkeeping.push_back(user); + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + return user; + } + + uses.push_back({user, {kills.begin(), kills.end()}}); + if (auto *kept = walkBorrowed(readOp.getResult(), kills, uses)) + { + return kept; + } + } + + return nullptr; + } + + static bool isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp) + { + auto storeOp = mlir::dyn_cast(op); + return storeOp && storeOp.getReference() == varOp.getResult(); + } + + // What `use` hands the very value it uses on to, as that value: a cast or a view of it, the + // argument of the block a branch passes it to (a merge: `a ?? b`), or the reads of a local + // that owns nothing and is assigned it (`for (x of arr)` into an outer `let x`). Answers + // whether the use is one of those, even where nothing comes out of it. Where the value lands + // somewhere that is replaced - a block argument, a local - `renewedAt` gets what replaces it: + // the block's entry, the local's declaration (when it has an initializer: `let l!: V` stores + // nothing, and in a loop the slot keeps the last round's value) and assignments. + static bool passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, + llvm::SmallVectorImpl *renewedAt = nullptr) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || isView(user)) + { + values.push_back(user->getResult(0)); + return true; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + values.push_back(*argument); + if (renewedAt) + { + renewedAt->push_back(&argument->getOwner()->front()); + } + + return true; + } + + return false; + } + + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == use.get()) + { + if (auto varOp = storeOp.getReference().getDefiningOp(); + varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) + { + if (renewedAt && varOp.getInitializer()) + { + renewedAt->push_back(varOp); + } + + for (auto *varUser : varOp.getResult().getUsers()) + { + if (auto loadOp = mlir::dyn_cast(varUser)) + { + values.push_back(loadOp.getResult()); + } + else if (renewedAt && isAssignmentOf(varUser, varOp)) + { + renewedAt->push_back(varUser); + } + } + + return true; + } + } + + return false; + } + + // Where a borrowed value goes. Every use of it, and of what it produces that may still point + // into the block, joins `uses`, made again at `kills`. The value itself - through what passesOn + // hands it on to - must not be kept anywhere; what it produces - a field reference, a value + // loaded through one, a bound method, a catch clause's non-owning local - still points into + // the borrowed block, so each of its uses is a use of the borrow too. A value that cannot point + // anywhere (a number, a boolean) ends the walk. Where passesOn hands the value to something + // that is replaced, the uses beyond are made again where it is replaced instead; the op that + // hands it on is itself a use, bounded by `kills`. `isBorrower` accepts a use of the value + // itself that the caller follows on its own (a borrowing `let`), made again at the kills it is + // given. Answers the first use that would keep the value. + // + // `keeping` names what rc planted around a borrow that it took for an owner and that goes + // with it: the temporary release of a result that borrows an argument (the start's, or that of + // a call it is passed to, which is a borrow of it too), and, in a function whose result + // borrows this very value's parameter, the retain rc made for the caller. Those join + // `bookkeeping`, not `uses`, and the return itself is a use, not a keep. + struct Keeping + { + // no default member initializers: GCC rejects them in a default argument of the class + // that encloses the struct + llvm::SmallVectorImpl *bookkeeping; + bool returns; + }; + + mlir::Operation *walkBorrowed( + mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, + llvm::function_ref)> isBorrower = nullptr, + Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false, bool startBorrowed = true) + { + struct Pending + { + mlir::Value value; + bool borrowed; + llvm::SmallVector kills; + }; + + llvm::SmallVector values; + // `startBorrowed` false: `start` is what a read produced (a field's reference), whose uses + // are uses of the borrow but not the borrowed value itself + values.push_back({start, startBorrowed, {kills.begin(), kills.end()}}); + llvm::SmallPtrSet seen{start}; + while (!values.empty()) + { + auto pending = values.pop_back_val(); + for (auto &use : pending.value.getUses()) + { + auto *user = use.getOwner(); + if (pending.borrowed && keeping.bookkeeping && + ((mlir::isa(user) && borrowingCall(rootOf(pending.value))) || + (keeping.returns && mlir::isa(user)))) + { + keeping.bookkeeping->push_back(user); + continue; + } + + // rc's reference for a borrowing box's copy goes with the closure (decideClosures), and + // one for a borrowing field's with the field (classifyBorrowingFields) + if (captureRetains.contains(user) || fieldBorrowRetains.contains(user)) + { + continue; + } + + uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); + // copied into a borrowing box: whatever runs the closure reads it + if (auto found = captureStores.find(user); found != captureStores.end()) + { + for (auto *closureUse : found->second->uses) + { + uses.push_back({closureUse, pending.kills}); + } + } + + // stored into a field a borrowing record borrows (spec 32): whatever uses the record + // reads it + if (auto found = recordStoreUses.find(user); found != recordStoreUses.end()) + { + for (auto &recordUse : found->second->walk.uses) + { + uses.push_back({recordUse.op, pending.kills, recordUse.callee}); + } + } + + if (pending.borrowed && keeping.returns && mlir::isa(user)) + { + continue; + } + + if (pending.borrowed) + { + // a handle read that rc counts (spec 23.3): a return or an `any` box rc + // retained for is a counted copy of its own; passesOn would hand them on + if (countedHandle && isCountedHandOff(user, pending.value)) + { + continue; + } + + llvm::SmallVector same; + llvm::SmallVector renewedAt; + if (passesOn(use, same, &renewedAt)) + { + for (auto value : same) + { + if (mayPointInto(value) && seen.insert(value).second) + { + values.push_back({value, true, renewedAt.empty() ? pending.kills : renewedAt}); + } + } + + continue; + } + + // a handle read that rc counts (spec 23.3): its retain, and what takes the value + // it retains for, are a counted copy of their own, as a string copy is; the + // read's other uses are the borrow's (Ruling 7) + if (countedHandle && isCountedHandleReceiver(user, pending.value)) + { + continue; + } + + if (isBorrower && isBorrower(user, pending.kills)) + { + continue; + } + + if (mlir::isa(user) || !isBorrow(user, pending.value)) + { + return user; + } + } + + // a copy (spec 22.2) reads the borrow into a block of its own, which points into + // nothing, and so does a concatenation: `s + t` is a new string whatever s and t are + // (a `+` of two strings lowers to ts.StringConcat, which always allocates) + if (mlir::isa(user) || + (mlir::isa(user) && mlir::isa(user->getResult(0).getType()))) + { + continue; + } + + // a call whose result is the caller's own (callResultIsOwn) ends the walk at the call + if (callResultIsOwn(user)) + { + continue; + } + + // A result that borrows what it was given borrows this too, and is walked as it. + for (auto result : user->getResults()) + { + if (mayPointInto(result) && seen.insert(result).second) + { + values.push_back({result, !!borrowingCall(result), pending.kills}); + } + } + } + } + + return nullptr; + } + + // Whether a call hands back a block of the caller's own, which points into nothing it was given: + // walkBorrowed then ends a borrow's walk at the call (the call itself is still a use for as long + // as it runs). Never a generator's state that borrows its arguments (`__own_result_bounded`). + // - A call known to make its result (`__own_fresh_result`: an iterator's `next()`, a field that + // only ever holds one function, another own module's `..new`) - the `{value, done}` record of + // `it.next()` points nowhere into `it`, and what it does after its next call cannot be a use of + // the method read out of `it`. + // - A direct call of a function this module defines (isFresh): its result points into nothing it + // was given but what its facts name - a borrow of an argument (`__own_result_borrows`, never + // fresh), a borrowing record (isFresh, but bounded by what it borrows), a generator's state + // that borrows its arguments - and a body whose callers cannot all know such a fact is an + // error. `getPoint(x, scene.camera)` makes a new vector: a call after it that may destroy + // `scene.camera` does not end that vector (spec 34). + static bool callResultIsOwn(mlir::Operation *call) + { + if (!isCall(call) || call->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) + { + return false; + } + + return call->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) || + (call->getNumResults() == 1 && !isRecordResult(call) && isFresh(call->getResult(0))); + } + + // Is this what an indirect call goes through: its callee value, or, for a method read out of + // a field of an object (`o.m()`, `m` a field holding a function), the object it is bound to, + // which is the container of what was read, not what was borrowed. A method of a class bound + // to a borrowed instance (`h.c.m()`) is not one: its `this` is the borrow. + static bool isCalleeOperand(mlir::OpOperand &use) + { + auto *user = use.getOwner(); + auto callee = isCall(user) ? calleeValue(user) : mlir::Value(); + if (!callee) + { + return false; + } + + if (use.getOperandNumber() == 0) + { + return true; + } + + auto getThisOp = use.get().getDefiningOp(); + auto getMethodOp = callee.getDefiningOp(); + auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); + return use.getOperandNumber() == 1 && getMethodOp && methodField && + getThisOp.getOperand() == getMethodOp.getBoundFunc() && + mlir::isa(methodField.getReference().getType()); + } + + // ---- Borrowing records (spec 32) ---- + // + // An object literal boxed as an interface that holds values it does not own - `this`, a parameter, + // a field read, an owning `let`'s value, a captured variable's - beside fresh values, which move + // in. The signature pass marked its box (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME): its release frees + // the box and the fields it owns, and leaves the others to their owners. So the box has one owner + // like any fresh block - the box's own check - and what it borrows bounds every use of it: a read + // out of a place is checked with the record's uses as its own (walkBorrowed), a local's value + // against the local's ends, a captured variable's against whatever may assign it. Returned, the + // record is the caller's to own and to bound (checkRecordResults), which the function's facts + // must say. Stored anywhere else that may outlive what it borrows, it is an error. + struct RecordWalk + { + llvm::SmallVector uses; + // the first use that would keep the record itself: a store into a place, a callee that keeps + // it, a capture + mlir::Operation *kept = nullptr; + llvm::SmallVector returns; + }; + + struct BorrowedField + { + mlir_ts::StoreOp store; + RecordFieldSource source; + }; + + struct BorrowingRecord + { + mlir_ts::NewOp box; + mlir_ts::VariableOp local; + llvm::SmallVector interfaces; + llvm::SmallVector borrowed; + RecordWalk walk; + }; + + llvm::SmallVector> records; + // a store of a place read into a borrowing record's field -> the record's uses, which are the + // read's uses too (walkBorrowed) + llvm::DenseMap recordStoreUses; + + // Can a value of this type point into a heap block - a reference into one, a block of its own, + // or a method bound to one? A number or a boolean read out of a borrowed block cannot. + bool mayPointInto(mlir::Value value) + { + auto type = value.getType(); + return mlir::isa(type) || + ownsHeap(value); + } + + // The borrowing record built in `varOp` (classifyRecords), or null. + BorrowingRecord *borrowingRecordOf(mlir_ts::VariableOp varOp) + { + for (auto &record : records) + { + if (record->local == varOp) + { + return record.get(); + } + } + + return nullptr; + } + + // The boxes the signature pass marked as borrowing records, where every field still holds what it + // was marked for: a borrowed field a value this pass bounds (recordFieldSource), any other one a + // fresh value or nothing. A store into a borrowed field takes nothing, and whatever uses the record + // reads what it stored (walkBorrowed, through recordStoreUses). A box that is not what it was + // marked for is left to the record's retain, which is then an error. + void classifyRecords() + { + getFunction().walk([&](mlir_ts::NewOp newOp) { + auto fields = recordBorrowedFields(newOp); + auto varOp = fields.empty() ? mlir_ts::VariableOp() : recordLocalOf(newOp.getResult()); + if (!varOp) + { + return; + } + + auto record = std::make_shared(); + record->box = newOp; + record->local = varOp; + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + auto borrows = llvm::is_contained(fields, static_cast(propertyRefOp.getPosition())); + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + continue; + } + + auto source = recordFieldSource(storeOp.getValue()); + if (source == RecordFieldSource::NoBlock) + { + continue; + } + + if (source == RecordFieldSource::Unknown || borrows != (source != RecordFieldSource::Fresh)) + { + return; + } + + if (borrows) + { + record->borrowed.push_back({storeOp, source}); + } + } + } + + for (auto *user : newOp->getUsers()) + { + if (auto castOp = mlir::dyn_cast(user)) + { + for (auto *castUser : castOp->getUsers()) + { + if (auto newInterfaceOp = mlir::dyn_cast(castUser)) + { + record->interfaces.push_back(newInterfaceOp.getResult()); + } + } + } + } + + // a path back to a use through the record's making is another record + llvm::SmallVector kills{newOp.getOperation(), varOp.getOperation()}; + walkRecord(record->interfaces, kills, record->walk); + for (auto &field : record->borrowed) + { + setAttrTracked(field.store, OWN_CAPTURE_BORROW_ATTR_NAME); + recordStoreUses[field.store.getOperation()] = record.get(); + } + + records.push_back(std::move(record)); + }); + } + + // Every use of a borrowing record (spec 32), and of what is read out of it - each may read what + // the record borrows - through its views, merges, and the locals that hold it, made again at + // `kills`. Its own retains and releases are the box's, which its own check decides. The record + // itself is `kept` when it is stored anywhere but a local, given to a callee that keeps it, or + // captured; returned, it is in `returns`. A call given it hands back its own block, unless that + // is a borrowing record that holds it, whose uses are then this one's as well. + void walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk) + { + struct Pending + { + mlir::Value value; + bool record; + }; + + llvm::SmallVector work; + llvm::SmallPtrSet seen; + auto push = [&](mlir::Value value, bool record) { + if ((record || mayPointInto(value)) && seen.insert(value).second) + { + work.push_back({value, record}); + } + }; + + auto followSlot = [&](mlir_ts::VariableOp varOp, bool record) { + for (auto *slotUser : varOp->getUsers()) + { + // its releases and its other values are the slot's own + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + push(loadOp.getResult(), record); + } + } + }; + + auto keep = [&](mlir::Operation *user) { walk.kept = walk.kept ? walk.kept : user; }; + + for (auto start : starts) + { + push(start, true); + } + + while (!work.empty()) + { + auto pending = work.pop_back_val(); + auto value = pending.value; + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || + (pending.record && mlir::isa(user))) + { + continue; + } + + // the same value: a view, a cast, a merge + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + push(user->getResult(0), pending.record); + continue; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + push(*argument, pending.record); + } + + continue; + } + + // a local that holds it: its reads + auto varOp = mlir::dyn_cast(user); + auto storeOp = mlir::dyn_cast(user); + if (varOp || (storeOp && storeOp.getValue() == value)) + { + auto slotOp = varOp ? varOp : storeOp.getReference().getDefiningOp(); + if (slotOp && !slotOp.getCaptured().value_or(false)) + { + followSlot(slotOp, pending.record); + continue; + } + + // a place, a global, a captured variable: what is read out of the record is checked + // where it is read (checkPlaceRead) + walk.uses.push_back({user, {kills.begin(), kills.end()}}); + if (pending.record) + { + keep(user); + } + + continue; + } + + walk.uses.push_back({user, {kills.begin(), kills.end()}, isCall(user) && isCalleeOperand(use)}); + if (mlir::isa(user)) + { + if (pending.record) + { + walk.returns.push_back(user); + } + + continue; + } + + if (isCall(user)) + { + if (pending.record && isKeptArgument(user, value)) + { + keep(user); + continue; + } + + // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that + // holds it - points where it points + auto args = callArgs(user); + auto given = [&](llvm::ArrayRef indices) { + return llvm::any_of(indices, [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + }; + + if (user->getNumResults() == 1 && given(resultBorrowsAll(user))) + { + push(user->getResult(0), pending.record || isRecordResult(user)); + } + + // ...and a generator's state that borrows it (OWN_RESULT_BOUNDED_ATTR_NAME) + if (auto bounded = user->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + bounded && user->getNumResults() == 1 && + given(llvm::SmallVector(bounded.asArrayRef().begin(), bounded.asArrayRef().end()))) + { + push(user->getResult(0), false); + } + + continue; + } + + if (pending.record && !isBorrow(user, value)) + { + keep(user); + continue; + } + + // a field written through the record: its old value may be one the record borrows, + // which the overwrite would release, and the field may be one its release leaves + if (pending.record && mlir::isa(user)) + { + for (auto *fieldUser : user->getResult(0).getUsers()) + { + auto fieldStore = mlir::dyn_cast(fieldUser); + if (mlir::isa(fieldUser) || + (fieldStore && fieldStore.getReference() == user->getResult(0))) + { + keep(fieldUser); + } + } + } + + // what it produces - a field's reference, a value read out of it, a bound method - may + // still point into what the record borrows + for (auto result : user->getResults()) + { + push(result, false); + } + } + } + } + + // The name the record goes by: the local that holds it, or a folded `const`'s. + llvm::StringRef recordName(mlir::Value value) + { + llvm::StringRef name = ownerName(value); + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (auto varOp = mlir::dyn_cast(user); varOp && varOp.getInitializer() == used) + { + name = varName(varOp); + } + }); + + return name; + } + + // What a borrowed field holds, for an error: `'ray'`, `'this.pos'`, `'livec'`, ... + std::string describeRecordSource(BorrowedField field) + { + auto value = field.store.getValue(); + auto quoted = [](llvm::StringRef name) { return "'" + name.str() + "'"; }; + switch (field.source) + { + case RecordFieldSource::Param: + return quoted(paramName(borrowedParam(value))); + case RecordFieldSource::Place: + if (auto loadOp = rootOf(value).getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + return describePlace(loadOp.getReference()); + } + + return describeCall(rootOf(value).getDefiningOp()); + case RecordFieldSource::Slot: + return quoted(varName(slotLoadOf(value).getReference().getDefiningOp())); + case RecordFieldSource::Cell: + return "a captured variable"; + default: + return "a value it does not own"; + } + } + + // A record this function makes (spec 32): kept, it is an error; returned, each field it borrows + // must be a parameter's that this function's facts lend (`__own_result_record`), so that its + // callers bound it; and no use of it may come after what a field borrows may be released or + // overwritten - an owning local's ends, and whatever may assign a captured variable. A parameter + // and `this` are the caller's for the whole call; a read out of a place has the record's uses as + // its own (checkPlaceRead). + void checkBorrowingRecords() + { + for (auto &record : records) + { + auto name = recordName(record->interfaces.empty() ? record->box.getResult() : record->interfaces.front()); + if (record->walk.kept) + { + reportPlaceBorrowEscapes(record->walk.kept, name, + record->borrowed.empty() ? "a value it does not own" + : describeRecordSource(record->borrowed.front())); + continue; + } + + auto ok = true; + for (auto *returned : record->walk.returns) + { + for (auto &field : record->borrowed) + { + if (!ok || field.source == RecordFieldSource::Immortal) + { + continue; + } + + auto param = field.source == RecordFieldSource::Param || field.source == RecordFieldSource::Place + ? borrowedParam(field.store.getValue()) + : -1; + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, describeRecordSource(field)); + ok = false; + } + } + } + + for (auto &field : record->borrowed) + { + if (!ok) + { + break; + } + + auto value = field.store.getValue(); + Chain chain; + if (field.source == RecordFieldSource::Slot) + { + chain.roots.push_back({slotLoadOf(value).getReference(), Chain::Slot}); + } + else if (auto load = field.source == RecordFieldSource::Param ? keptParamLoadOf(value) : mlir_ts::LoadOp()) + { + // a parameter this function keeps is its own to move away, as a local's value is + chain.roots.push_back({load.getReference(), Chain::Slot}); + } + else if (field.source == RecordFieldSource::Cell) + { + chain.roots.push_back({rootOf(value), Chain::NotOwned}); + } + else + { + continue; + } + + auto ends = rootEnds(chain); + if (field.source == RecordFieldSource::Cell) + { + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value)) + { + ends.push_back(drop); + } + } + } + + for (auto *end : ends) + { + for (auto &use : record->walk.uses) + { + if (ok && (use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + if (field.source == RecordFieldSource::Slot) + { + reportBorrowOutlives(use.op, end, name, varName(chain.roots.front().first.getDefiningOp())); + } + else + { + reportPlaceBorrowOutlives(use.op, end, name, describeRecordSource(field)); + } + + ok = false; + } + } + } + } + } + } + + // A call whose result is a borrowing record (OWN_RESULT_RECORD_ATTR_NAME, spec 32): the box is + // this function's to own - it is fresh, and its own check decides its moves and releases - but + // it may hold a borrow of each argument the facts name, as a call's result that borrows them + // does (checkPlaceRead): somewhere under each. So no use of it, through the locals it moves + // into, may come after one of those arguments may be released or overwritten, and it may not be + // kept. Returned, the arguments must be parameters this function's own facts lend. Its releases + // are not uses: they free only what the record owns. + void checkRecordResults() + { + getFunction().walk([&](mlir::Operation *call) { + if (!isCall(call) || !isRecordResult(call) || call->getNumResults() != 1) + { + return; + } + + auto result = call->getResult(0); + auto place = describeCall(call); + auto name = recordName(result); + RecordWalk walk; + llvm::SmallVector kills{call}; + walkRecord({result}, kills, walk); + if (walk.kept) + { + reportPlaceBorrowEscapes(walk.kept, name, place); + return; + } + + auto args = callArgs(call); + for (auto *returned : walk.returns) + { + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) >= args.size() || argumentHoldsNoBlock(args[index]) || + isImmortalLiteral(args[index])) + { + continue; + } + + auto param = borrowedParam(args[index]); + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, place); + return; + } + } + } + + auto chain = chainOf(call); + // rc's retain of a fresh argument right before the call is no end: the box's own check + // decides it, and the use that takes it is an end of its own + llvm::SmallVector ends; + for (auto *end : rootEnds(chain)) + { + auto ownedRetain = mlir::isa(end) && llvm::any_of(chain.roots, [&](auto &root) { + return root.second == Chain::Owned && rootOf(end->getOperand(0)) == root.first; + }); + if (!ownedRetain) + { + ends.push_back(end); + } + } + + for (auto *drop : drops) + { + if (drop != call && dropsChain(drop, chain, call)) + { + ends.push_back(drop); + } + } + + for (auto *end : ends) + { + for (auto &use : walk.uses) + { + if ((use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, end, name, place); + return; + } + } + } + }); + } + + // ---- Reads out of containers (spec 2.3, phase 3) ---- + // + // `h.c` and `arr[i]` read a value the container owns. Such a read is a borrow: it may not be + // kept, and none of its uses - nor any use of what it produces - may run after something that + // may destroy it (spec 2.2's dropping mutations): an overwrite of a place on the way from the + // read up to its root, an array op that removes elements, an end of the root itself, or a call + // that may reach the root. A path back to a use through the read itself is a new borrow. + + // A read of a heap value out of a field or an element, or a call whose result borrows an + // argument (spec 2.4, phase 4), seen through its views. Null for anything else. + mlir::Operation *placeReadOf(mlir::Value value) + { + auto root = rootOf(value); + auto *def = root.getDefiningOp(); + // a handle read out of a place that rc gives an owner of its own is a counted copy, not a + // borrow (spec 23.3); any other is a borrow of the place, as a field read is + if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) + { + return nullptr; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) + { + // a field of a generator's borrowing box: only its maker fills it, and nothing a call + // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) + if (isBorrowingBoxField(loadOp.getReference())) + { + return nullptr; + } + + return def; + } + + return borrowingCall(root); + } + + // How a handle read out of a place is used (spec 23.3), seen through casts, merges and locals + // that own nothing. Every use is classified, and nothing falls through (Ruling 9): + // - `counted`: a receiver that takes a count rc made for that very use + // (isCountedHandleReceiver) - a store, a push or an unshift, `new Shared`, an array + // literal's element, a return, an `any` box, or an owning local's initializer; + // - passed on: a cast or a view that is still a handle, a merge, a local that owns nothing - + // whose own uses are classified in turn; + // - `borrowed`: everything else. A use that reads through the place (`f(p, p.child)`, a + // `.value` read, `Shared.count`), and every use that is neither of the above: a tuple's + // element, an object literal's field, a capture, a `yield`, a receiver rc made no retain + // for, any op not named here. The read is then a borrow, and tenure-checked + // (checkPlaceRead). + // rc's retains count for nothing on their own: each is the count of the receiver right after + // it. `cur = cur.value.next` is counted only. + struct HandleReadUses + { + bool counted; + bool borrowed; + }; + + static HandleReadUses handleReadUses(mlir::Value value) + { + HandleReadUses result{false, false}; + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + // a return and an `any` box take the value, though passesOn would hand them on + if (isHandOff(user, current)) + { + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + continue; + } + + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + // what is no longer a handle is no receiver: a test (`if (p.child)`) only + // reads it + if (!isHandle(next)) + { + result.borrowed = true; + } + else if (seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + } + } + + return result; + } + + // A handle read out of a place that rc gives a count of its own is a counted copy, not a borrow + // (spec 23.3) - but only when every use of it is one rc counts (Ruling 7), and a use that is + // not known to be one is not (Ruling 9). A read with one use that reads through the place as + // well (`f(p, q, q.child = p.child)`), or that keeps it with no count (`[(q.child = p.child), + // 1]`), is a borrow, and that use is tenure-checked; the counted uses are copies + // (checkPlaceRead). + static bool isCountedHandleRead(mlir::Value value) + { + auto uses = handleReadUses(value); + return uses.counted && !uses.borrowed; + } + + // A use that gives a handle an owner rc counts, with rc's retain for that very use: rc's + // retain itself, a store into a field, an element, a global, an owning local or the result + // slot (a return) that rc retained for (countedStoreRetain); a push or an unshift, `new + // Shared`, an array literal's element, an `any` box, a `ts.ReturnVal`, each right after rc's + // retain of what it takes (retainedRightBefore); or the initializer of an owning local, which + // takes a count of its own (`ts.RetainSlot`). Any of them without its retain is not one: it + // keeps the read without a count (`{ s: (q.child = p.child) }`'s field). + static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return true; + } + + if (auto varOp = mlir::dyn_cast(user)) + { + return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { + return mlir::isa(varUser); + }); + } + + if (auto storeOp = mlir::dyn_cast(user)) + { + return storeOp.getValue() == value && countedStoreRetain(storeOp); + } + + if (auto pushOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, pushOp.getItems(), value); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, unshiftOp.getItems(), value); + } + + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); + } + + // `ts.ReturnVal` is the return before the affine lowering, which makes it a store into + // the result slot + if (mlir::isa(user)) + { + return retainedRightBefore(user, user->getOperands(), value); + } + + return false; + } + + // A use that passesOn hands on but that takes the value: a return's store into the function's + // result slot (isResultSlot), and a cast that boxes it into `any`. + static bool isHandOff(mlir::Operation *user, mlir::Value value) + { + if (auto storeOp = mlir::dyn_cast(user)) + { + auto slot = storeOp.getReference().getDefiningOp(); + return storeOp.getValue() == value && slot && isResultSlot(slot); + } + + auto castOp = mlir::dyn_cast(user); + return castOp && castOp.getIn() == value && mlir::isa(castOp.getType()); + } + + // A return or an `any` box of a handle that rc counts (walkBorrowed). + static bool isCountedHandOff(mlir::Operation *user, mlir::Value value) + { + return isHandOff(user, value) && isCountedHandleReceiver(user, value); + } + + // rc's retains for what `op` takes: MLIRGen puts them in a row right before it + // (mlirGenRetainCaptured, retainInsertedElements), one for each operand it does not take + // over. Answers whether that row retains `value` once for each time `operands` hold it. A + // retain is right before one op only, so no other receiver can count it as well. + static bool retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value) + { + auto needed = llvm::count(operands, value); + if (needed == 0) + { + return false; + } + + for (auto *previous = op->getPrevNode(); needed > 0 && previous; previous = previous->getPrevNode()) + { + auto retainOp = mlir::dyn_cast(previous); + if (!retainOp) + { + break; + } + + if (retainOp.getReference() == value) + { + --needed; + } + } + + return needed == 0; + } + + // The releases of the old value that rc's counted stores of a handle read make: `ts.Retain` of + // the value, `ts.ReleaseSlot` of the slot, `ts.Store` of the value into it, in a row. Seen + // through the read's casts, merges and locals that own nothing, as handleReadUses sees it. + static void collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases) + { + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + if (isHandle(next) && seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + auto storeOp = mlir::dyn_cast(use.getOwner()); + if (!storeOp || storeOp.getValue() != current || !countedStoreRetain(storeOp)) + { + continue; + } + + if (auto releaseSlotOp = mlir::dyn_cast_or_null(storeOp->getPrevNode())) + { + releases.insert(releaseSlotOp); + } + } + } + } + + // rc's retain for a store of a value, when it made one: `ts.Retain` of the value right before + // the store, or before the `ts.ReleaseSlot` of the old value right before it - + // `Retain(v)`, [`ReleaseSlot(slot)`], `Store(v, slot)`. A return's store into the result + // slot comes after the scope exit's releases of the frame's locals and their slots: + // `Retain(v)`, [`Release(..)`, `ReleaseSlot(..)`...], `Store(v, result)`. Null for a store rc + // counts nothing for. + static mlir::Operation *countedStoreRetain(mlir_ts::StoreOp storeOp) + { + auto *previous = storeOp->getPrevNode(); + if (auto slot = storeOp.getReference().getDefiningOp(); slot && isResultSlot(slot)) + { + // the scope exit's releases, of locals and of their slots, in any order + while (mlir::isa_and_nonnull(previous) && + previous->getOperand(0) != storeOp.getValue()) + { + previous = previous->getPrevNode(); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast_or_null(previous)) + { + previous = releaseSlotOp.getSlot() == storeOp.getReference() ? releaseSlotOp->getPrevNode() : nullptr; + } + + auto retainOp = mlir::dyn_cast_or_null(previous); + return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; + } + + // The call a value is the result of, when that result is a plain borrow of an argument. A borrowing + // record (spec 32) is not one: the caller owns the box (checkRecordResults). + static mlir::Operation *borrowingCall(mlir::Value value) + { + auto *def = value.getDefiningOp(); + if (!def || !isCall(def) || resultBorrows(def) < 0) + { + return nullptr; + } + + auto args = callArgs(def); + auto indices = resultBorrowsAll(def); + return llvm::all_of(indices, [&](int index) { return static_cast(index) < args.size(); }) ? def : nullptr; + } + + // The places from a read up to its roots, and the roots: the values the function reached the + // first container through. A container reached through a merge (`(c ? a : b).x`) has a root + // on each way in. + struct Chain + { + enum Kind + { + Slot, // an owning local, or a value-type local reached by reference + Owned, // a value this function made + NotOwned // a parameter, `this`, a global, anything else + }; + + // a field's position and reference type; an element is position -1, a handle's payload + // (`s.value`) is SharedValue, a field reached through an interface is InterfaceField: its + // position in the object is not known here (spec 28) + llvm::SmallVector> places; + static constexpr int64_t SharedValue = -2; + static constexpr int64_t InterfaceField = -3; + llvm::SmallVector> roots; + // The roots that are handles - what a `s.value` was read through - each also NotOwned + // among `roots` (spec 23.3). Known from the op, not the root's type: a view may hide it. + llvm::SmallVector handles; + // Somewhere under a root, but where is not known: a result that borrows an argument may + // be any field or element under it. Every overwrite and every removal drops it. + bool anyPlace = false; + + bool owned() const + { + return llvm::none_of(roots, [](auto &root) { return root.second == NotOwned; }); + } + }; + + // With `place`, the chain of that place itself: where a store into it moves a value (spec 29). + Chain chainOf(mlir::Operation *read, mlir::Value place = {}) + { + Chain chain; + llvm::SmallVector refs; + llvm::SmallVector first; + if (place) + { + refs.push_back(place); + } + else if (auto loadOp = mlir::dyn_cast(read)) + { + refs.push_back(loadOp.getReference()); + } + else + { + // a call's result that borrows its arguments - a plain borrow, or a borrowing record's box + // (spec 32), which may hold a borrow of each - is somewhere under each of them + chain.anyPlace = true; + auto args = callArgs(read); + for (auto index : resultBorrowsAll(read)) + { + if (static_cast(index) < args.size() && !argumentHoldsNoBlock(args[index]) && + !isImmortalLiteral(args[index])) + { + first.push_back(rootOf(args[index])); + } + } + } + + llvm::SmallPtrSet seen; + // the bases a `s.value` was read through, and what merges into them + llvm::SmallPtrSet handleBases; + while (!refs.empty() || !first.empty()) + { + llvm::SmallVector bases; + if (!first.empty()) + { + bases.swap(first); + } + else + { + auto ref = refs.pop_back_val(); + auto place = placeBase(ref); + assert(place && "chainOf walks places only"); + switch (place.kind) + { + case PlaceKind::Field: + chain.places.push_back({ref.getDefiningOp().getPosition(), ref.getType()}); + break; + case PlaceKind::SharedValue: + chain.places.push_back({Chain::SharedValue, ref.getType()}); + handleBases.insert(rootOf(place.base)); + break; + case PlaceKind::InterfaceField: + chain.places.push_back({Chain::InterfaceField, ref.getType()}); + break; + default: + chain.places.push_back({-1, ref.getType()}); + break; + } + + bases.push_back(rootOf(place.base)); + } + + while (!bases.empty()) + { + auto base = bases.pop_back_val(); + if (!seen.insert(base).second) + { + continue; + } + + // A handle (spec 23.3): other handles may reach its block, so this function owns + // nothing under it. A handle read out of a place still has that place above it, and + // one merged from several has each of them. + if (handleBases.contains(base) || isHandle(base)) + { + llvm::SmallVector merged; + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, merged)) + { + handleBases.insert(merged.begin(), merged.end()); + bases.append(merged.begin(), merged.end()); + continue; + } + + chain.roots.push_back({base, Chain::NotOwned}); + chain.handles.push_back(base); + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + } + + continue; + } + + if (isPlace(base)) + { + refs.push_back(base); // a field of a value-type field: the same object + continue; + } + + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + continue; + } + + // the object a method is called on + if (auto object = boundThis(base)) + { + bases.push_back(rootOf(object)); + continue; + } + + // a result that borrows an argument is somewhere under the argument + if (auto *call = borrowingCall(base)) + { + chain.anyPlace = true; + auto args = callArgs(call); + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) < args.size()) + { + bases.push_back(rootOf(args[index])); + } + } + + continue; + } + + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, bases)) + { + continue; + } + + if (base.getDefiningOp()) + { + chain.roots.push_back({base, Chain::Slot}); + } + else if (auto slotLoad = slotLoadOf(base)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else + { + chain.roots.push_back({base, isFresh(base) ? Chain::Owned : Chain::NotOwned}); + } + } + } + + return chain; + } + + // Where the roots stop holding what the chain reads: a local's releases and assignments and + // every declaration or retain that may move it away; a made value's releases and every use + // that takes it; a global's assignments. + llvm::SmallVector rootEnds(const Chain &chain) + { + llvm::SmallVector ends; + for (auto [root, kind] : chain.roots) + { + // A handle (spec 23.3), never owned: where this one is given back or overwritten - a + // local's release and assignments, a temporary's release; a global's assignments are + // below. A copy of it is counted, and moves nothing away. + if (llvm::is_contained(chain.handles, root)) + { + auto loadOp = root.getDefiningOp(); + if (auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp()) + { + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + } + else if (!loadOp) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value) { + if (mlir::isa(use)) + { + ends.push_back(use); + } + }); + } + } + + if (kind == Chain::Slot) + { + for (auto *user : root.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + else if (auto loadOp = mlir::dyn_cast(user)) + { + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use) || (varOp && isOwningVariable(varOp)) || + (isCall(use) && isKeptArgument(use, used))) + { + ends.push_back(use); + } + }); + } + } + } + else if (kind == Chain::Owned) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { + if (mlir::isa(use) || !isBorrow(use, used)) + { + ends.push_back(use); + } + }); + } + else if (auto loadOp = root.getDefiningOp()) + { + if (auto global = loadOp.getReference().getDefiningOp()) + { + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global.getGlobalName()) + { + ends.push_back(op); + } + }); + } + } + } + + return ends; + } + + // What a call has to be given to reach a root: the root, and where the root is a local + // declared from another (a borrow, or a move that left the other dead), that one too. + static llvm::SmallVector rootSources(const Chain &chain) + { + llvm::SmallVector sources; + for (auto &root : chain.roots) + { + auto value = root.first; + sources.push_back(value); + for (auto steps = 0; steps < 8; ++steps) + { + auto varOp = value.getDefiningOp(); + if (!varOp || !varOp.getInitializer()) + { + break; + } + + auto initializer = rootOf(varOp.getInitializer()); + auto slotLoad = slotLoadOf(initializer); + value = slotLoad ? slotLoad.getReference() : initializer; + sources.push_back(value); + } + } + + return sources; + } + + // Everything that may point into what `start` points to: the forward closure over users' + // results that may point anywhere. A call's result is not followed: the call hands back a + // value it owns. + const llvm::DenseSet &derivedFrom(mlir::Value start) + { + if (auto found = derivedCache.find(start); found != derivedCache.end()) + { + return *found->second; + } + + auto &slot = derivedCache[start]; + slot = std::make_shared>(); + auto &derived = *slot; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto value = work.pop_back_val(); + if (!derived.insert(value).second) + { + continue; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (isCall(user)) + { + // ...but a borrowing record given this value holds it (spec 32) + auto args = callArgs(user); + if (isRecordResult(user) && llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + })) + { + work.push_back(user->getResult(0)); + } + + continue; + } + + llvm::SmallVector next(user->getResults().begin(), user->getResults().end()); + passesOn(use, next); + for (auto result : next) + { + if (mayPointInto(result)) + { + work.push_back(result); + } + } + } + } + + return derived; + } + + // May this op of the function's drops destroy what the chain reads? An overwrite of a place + // on it, matched by position and type (two references to one field of one object always + // agree on both; an unrelated field that happens to agree only adds an error); an array op + // that removes elements of the type of an element on it; a call whose callee may destroy + // anything (`__own_no_drops`, phase 4) - any such call, for a root this function does not + // own, since the owner may be a global the callee assigns; else one given something derived + // from the root that the read did not produce. + // `borrowed`, when given, is the value the borrow holds in place of the read's result (a value + // moved into the place, spec 29), and `read` may then be null. + bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed = {}) + { + if (read && drop == read) + { + return false; // a call that returns a borrow has not destroyed it by returning it + } + + if (chain.anyPlace && !isCall(drop)) + { + return true; + } + + if (auto releaseSlotOp = mlir::dyn_cast(drop)) + { + // An assignment to a captured variable destroys its old value, which is reached only + // through a read of the variable's cell: a root this function does not own. + auto slot = releaseSlotOp.getSlot(); + if (isCellVariable(slot) || isLoadedCell(slot)) + { + return !chain.owned(); + } + + // An assignment to a global destroys its old value and all that value owns. A root this + // function does not own - `this`, a parameter, a global's or a field's value - may be + // that very block, or under it (spec 29). A root this function made cannot. An immortal + // global's one store is its initializer, whose old value is the empty one (spec 25). + if (auto addressOfOp = slot.getDefiningOp()) + { + return !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && !chain.owned(); + } + + // `x.value = ...` through any handle, of any type, may replace the payload another + // handle reads (spec 23.3): a handle converts to one of a base class's type + if (slot.getDefiningOp()) + { + return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); + } + + auto isField = [](int64_t position) { return position >= 0 || position == Chain::InterfaceField; }; + int64_t position = -1; + if (auto propertyRefOp = slot.getDefiningOp()) + { + position = propertyRefOp.getPosition(); + } + else if (isInterfaceField(slot)) + { + position = Chain::InterfaceField; + } + + // A field reached through an interface may be any field of the object under it, of any + // type (a class may declare the field narrower than the interface): an overwrite through + // an interface drops every field on the chain, and any field's overwrite drops a field + // read through an interface (spec 28). + return llvm::any_of(chain.places, [&](auto &place) { + if (isField(position) && isField(place.first) && + (position == Chain::InterfaceField || place.first == Chain::InterfaceField)) + { + return true; + } + + return place.first == position && place.second == slot.getType(); + }); + } + + if (isCall(drop)) + { + if (callNoDrops(drop)) + { + return false; + } + + // a call given a closure may run it, and its box may hold anything + if (!chain.owned() || llvm::any_of(drop->getOperands(), holdsCaptures)) + { + return true; + } + + auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); + auto sources = rootSources(chain); + return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { + return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { + return derivedFrom(source).contains(operand); + }); + }); + } + + // pop, shift, splice, `length =`; push and unshift on an array reached through a handle + auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); + auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); + if (!arrayType) + { + return true; + } + + return llvm::any_of(chain.places, [&](auto &place) { + auto placeType = mlir::dyn_cast(place.second); + return place.first == -1 && placeType && placeType.getElementType() == arrayType.getElementType(); + }); + } + + // A read out of a container is a borrow of it (spec 2.3), and so is a `let` declared from one: + // its retain and releases go when the check passes, as for any borrowing `let` (phase 2). + // Reports and returns false when the read is kept, or used after something that may destroy + // it. + bool checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase) + { + auto loadOp = mlir::dyn_cast(read); + auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); + auto result = read->getResult(0); + // A handle read rc counts (spec 23.3): its counted uses are copies (walkBorrowed), and the + // release of the old value a counted store makes, right before it stores the retained + // read, cannot free the block read: it holds the retain's count by then + // (`Shared.count(q.child = p.child)`). + auto countedHandle = isHandle(result) && handleReadUses(result).counted; + llvm::SmallPtrSet countedStoreReleases; + if (countedHandle) + { + collectCountedStoreReleases(result, countedStoreReleases); + } + + llvm::StringRef name = ownerName(result); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + mlir_ts::VariableOp letKeeps; + mlir::Operation *letKept = nullptr; + // A result that borrows arrives with rc's temporary release, or rc's consuming `let`, and + // in a function returning a borrow of the same parameter it may go out with rc's retain. + Keeping keeping{&bookkeeping, returnsBorrowOf >= 0 && borrowedParam(result) == returnsBorrowOf}; + auto *kept = walkBorrowed(result, read, uses, [&](mlir::Operation *user, + llvm::ArrayRef kills) { + auto borrower = borrowerOf(user, /*consumed=*/true); + if (!borrower || borrower.getOperation() != user) + { + return false; + } + + name = varName(borrower); + if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) + { + letKept = keeps; + letKeeps = borrower; + } + + return true; + }, keeping, countedHandle); + + if (kept || letKept) + { + if (!kept && isAssignmentOf(letKept, letKeeps)) + { + reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); + } + else + { + reportPlaceBorrowEscapes(kept ? kept : letKept, name, place); + } + + return false; + } + + auto chain = chainOf(read); + // A call given the borrow is a use of it for as long as the callee runs, so a call that + // may destroy it is a use after that, whatever the order of its arguments. The function + // value it goes through is only read as it starts. + auto outlives = [&](mlir::Operation *drop) { + for (auto &use : uses) + { + if ((use.op != drop || !use.callee) && reachableAfter(drop, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, drop, name, place); + return true; + } + } + + return false; + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + if (keptParamOutlives(chain, outlives)) + { + return false; + } + + for (auto *drop : drops) + { + if (countedStoreReleases.contains(drop)) + { + continue; + } + + if (dropsChain(drop, chain, read) && outlives(drop)) + { + return false; + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + // A local that holds a borrow through `user`, and the field path under it that holds it (empty: + // the local's value itself): a store of it into the local, or into a field of the local's value + // tuple at any depth (an object literal built in place), or the local declared from it. + struct BorrowHolder + { + mlir_ts::VariableOp local; + llvm::SmallVector path; + }; + + static std::optional borrowHolderOf(mlir::Operation *user) + { + BorrowHolder holder; + mlir::Value ref; + if (auto storeOp = mlir::dyn_cast(user)) + { + ref = storeOp.getReference(); + while (auto propertyRefOp = ref.getDefiningOp()) + { + holder.path.insert(holder.path.begin(), propertyRefOp.getPosition()); + ref = propertyRefOp.getObjectRef(); + } + } + else if (auto varOp = mlir::dyn_cast(user)) + { + ref = varOp.getResult(); + } + + auto varOp = ref ? ref.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || varOp.getCaptured().value_or(false) || isCellVariable(varOp.getResult())) + { + return std::nullopt; + } + + holder.local = varOp; + return holder; + } + + // Where the holder is given a new value: the local's declaration when it has an initializer + // (`let l!: V` stores nothing, and in a loop the slot keeps the last round's value: fix-up + // re-review N4), and every store into the field path that holds the borrow or into a prefix of + // it (the whole local, `o.p` for `o.p.v`). A store into a sibling field, or below the path, + // gives the path no new value. + static llvm::SmallVector holderRenewals(BorrowHolder holder) + { + llvm::SmallVector renewals; + if (holder.local.getInitializer()) + { + renewals.push_back(holder.local); + } + + llvm::SmallVector> refs{{holder.local.getResult(), 0}}; + while (!refs.empty()) + { + auto [ref, depth] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + if (depth < holder.path.size() && propertyRefOp.getPosition() == holder.path[depth]) + { + refs.push_back({propertyRefOp.getResult(), depth + 1}); + } + } + else if (auto storeOp = mlir::dyn_cast(refUser); storeOp && storeOp.getReference() == ref) + { + renewals.push_back(storeOp); + } + } + } + + return renewals; + } + + // Whether one field path is on the way to the other: a read of either may reach the other's value. + static bool pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b) + { + auto common = std::min(a.size(), b.size()); + return a.take_front(common) == b.take_front(common); + } + + // A read of what a mortal global holds (spec 25: an immortal one's one store is its + // initializer): a heap value it owns, not a handle (spec 23.3 counts those). + bool isGlobalRead(mlir_ts::LoadOp loadOp) + { + auto addressOfOp = loadOp.getReference().getDefiningOp(); + auto result = loadOp.getResult(); + return addressOfOp && !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && ownsHeap(result) && !isHandle(result); + } + + // Such a read is a borrow of the value the global owns - `const c = K.w` names the read itself, + // with no retain - and an assignment of the global releases it. So no use of the read, nor of + // what it produces (walkBorrowed), may run after one in this function. A path back to a use + // through the read itself is a new read. A call that may assign the global, itself or through + // its callees, is such an assignment too (follow-up 11: `const c = G; reset(); print(c.x)`); the + // read given to that call as an argument is #639's. (C1 of the fix-up review: `const c = K.w; K.w = new V(i + 1); print(c.x)` in a loop + // read the block the assignment freed.) + // + // Where the walk meets a keep, the read is either followed into what holds it or rejected - + // never let go unchecked (fix-up re-review N2: a kept read is not always another check's). A + // local holds it (borrowHolderOf): an object literal built in place (`const o = { v: c }`, + // `{ p: { v: K.w } }`), a `let` or a destructured local declared or assigned from it + // (`let l = o.v`, `let { v } = o`). Every use of that local under the path that holds the borrow + // - its reads, a copy, a spread (`ts.DeconstructTuple`), what those lead to - is a use of the + // read, made again only where the local is given a new value (holderRenewals): never at the + // read, which renews `c` and not `o.v` - a holder declared before a loop keeps the old borrow + // across the back edge until it is stored again (fix-up re-review N3). The local's own retain + // and releases are its bookkeeping, which the owner checks decide. A sibling field never given + // the borrow is not followed. A value made from the borrow and stored into another local makes + // that one a holder too. Any other keep - a field of an object, an array, a global, a callee + // that keeps it, a capture, a return - cannot be bounded here and is an error. A string read is + // copied where it is kept (spec 22), so its keeps are not this check's. + void checkGlobalRead(mlir_ts::LoadOp read) + { + // each access of a global is a `ts.AddressOf` of its own + auto global = read.getReference().getDefiningOp().getGlobalName(); + llvm::SmallVector uses; + llvm::StringSet<> followed; + mlir::Operation *keptAt = nullptr; + // rc's temporary release of a call's result that borrows an argument goes with that result + // (walkBorrowed's `keeping`); the owner checks decide it + llvm::SmallVector bookkeeping; + Keeping keeping{&bookkeeping, false}; + std::function)> intoLocal; + + // the uses of what `holder` holds, made again at its renewals: those depend on the holder + // alone, not on the way the walk reached it, so a holder is followed once. A value loaded + // out of it is made again only by a path back through that load (the load itself is a use, + // bounded by the renewals): a store into the path after the load gives the loaded value + // nothing new. + auto followHolder = [&](BorrowHolder holder) { + std::string key; + llvm::raw_string_ostream keyOut(key); + keyOut << holder.local.getOperation(); + for (auto position : holder.path) + { + keyOut << "." << position; + } + + if (!followed.insert(keyOut.str()).second) + { + return true; + } + + auto localKills = holderRenewals(holder); + llvm::SmallVector>> refs{{holder.local.getResult(), {}}}; + while (!refs.empty()) + { + auto [ref, path] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + auto fieldPath = path; + fieldPath.push_back(propertyRefOp.getPosition()); + if (pathsMeet(fieldPath, holder.path)) + { + refs.push_back({propertyRefOp.getResult(), fieldPath}); + } + + continue; + } + + if (ref == holder.local.getResult() && + mlir::isa(refUser)) + { + continue; + } + + uses.push_back({refUser, {localKills.begin(), localKills.end()}}); + if (auto loadOp = mlir::dyn_cast(refUser)) + { + if (auto *kept = walkBorrowed(loadOp.getResult(), loadOp.getOperation(), uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + } + } + + return true; + }; + + intoLocal = [&](mlir::Operation *user, llvm::ArrayRef kills) { + // a tuple taken apart (`{ ...b }`, a destructuring): each field is the borrow again + if (mlir::isa(user)) + { + for (auto result : user->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + if (auto *kept = walkBorrowed(result, kills, uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + + return true; + } + + auto holder = borrowHolderOf(user); + return holder && followHolder(*holder); + }; + + auto *kept = walkBorrowed(read.getResult(), read.getOperation(), uses, intoLocal, keeping); + + // a value made from the borrow (a field of a tuple read out of a holder, `{ ...b }`) stored + // into another local, or declaring one: that local holds the borrow too + for (size_t index = 0; !kept && !keptAt && index < uses.size(); index++) + { + auto *op = uses[index].op; + auto storeOp = mlir::dyn_cast(op); + auto varOp = mlir::dyn_cast(op); + auto value = storeOp ? storeOp.getValue() : varOp ? varOp.getInitializer() : mlir::Value(); + if (!value || !mayPointInto(value)) + { + continue; + } + + if (auto holder = borrowHolderOf(op)) + { + followHolder(*holder); + } + } + + if (kept || keptAt) + { + if (mlir::isa(read.getType())) + { + return; + } + + if (reporting()) + { + auto *at = keptAt ? keptAt : kept; + at->emitError("the value read from '") + << global << "' is kept where its end at an assignment of '" << global + << "' cannot be checked; -mm=own cannot prove it outlives the global's value"; + signalPassFailure(); + } + + return; + } + + llvm::SmallVector ends; + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global) + { + ends.push_back(op); + } + + // a call that may give the global a new value (OwnershipSignaturePass, pinAssignedGlobals): + // `const c = G; reset(); print(c.x)` + if (isCall(op)) + { + auto assigned = op->getAttrOfType(OWN_ASSIGNS_GLOBALS_ATTR_NAME); + if (op->hasAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME) || + (assigned && llvm::any_of(assigned.getAsRange(), + [&](mlir::StringAttr name) { return name.getValue() == global; }))) + { + ends.push_back(op); + } + } + }); + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (use.op != end && reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, ownerName(read.getResult()), global); + return; + } + } + } + } + + // A borrow read out of a parameter this function keeps, taken before the parameter moves into a + // new owner (spec 33, review fix): from the move on it is bounded by that owner, as a read through + // the owner would be (spec 29) - by the owner's ends and the drops of its place after the move. + // `const inner = p.v; let r = p; r = new N(...); inner.x` reads what the assignment freed. A move + // into anything that is not such a place (a call that keeps it, a return, a push) ends the borrow + // there. So do the releases of the parameter's slot (spec 33's exit releases). True when some use + // of the borrow comes after one of these (`outlives` reported it). + bool keptParamOutlives(const Chain &chain, llvm::function_ref outlives) + { + for (auto &root : chain.roots) + { + auto load = keptParamLoadOf(root.first); + if (!load) + { + continue; + } + + // the moves of the parameter: what takes a read of its slot + llvm::SmallVector> takers; + for (auto *user : load.getReference().getUsers()) + { + if (mlir::isa(user)) + { + if (outlives(user)) + { + return true; + } + + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + continue; + } + + forEachUse(readOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use)) + { + auto *taker = takerOf(use, used); + takers.push_back({taker ? taker : use, used}); + } + else if ((varOp && isOwningVariable(varOp)) || (isCall(use) && isKeptArgument(use, used))) + { + takers.push_back({use, used}); + } + }); + } + + for (auto [taker, moved] : takers) + { + Chain owner; + if (!destinationChain(taker, owner)) + { + if (outlives(taker)) + { + return true; + } + + continue; + } + + auto afterMove = [&](mlir::Operation *end) { + return end != taker && reachableAfter(taker, end, llvm::ArrayRef{}) && outlives(end); + }; + + for (auto *end : rootEnds(owner)) + { + if (afterMove(end)) + { + return true; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, owner, nullptr, moved) && afterMove(drop)) + { + return true; + } + } + } + } + + return false; + } + + // A parameter, or a view of one (`u`, the payload `ts.Unbox` reads out of an `any`), that + // this function returns as a borrow (`__own_result_borrows`). rc retained it for the caller, + // who now takes no reference: the retain goes, and nothing may keep it. The caller's own + // borrow of the argument bounds it; nothing here can end that but a call, and a call that may + // is the caller's use of what it passed (spec 2.4, phase 4). + void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase) + { + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + if (auto *kept = walkBorrowed(value, {}, uses, nullptr, Keeping{&bookkeeping, true})) + { + reportBorrowEscapes(kept, ownerName(value), "a parameter"); + return; + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + } + + // `the result of 'first'`, for a call whose result borrows an argument. + static std::string describeCall(mlir::Operation *call) + { + auto callee = calleeName(call); + return callee.empty() ? "a call's result" : "the result of '" + callee + "'"; + } + + // The function a call names - directly, or the method it calls through a vtable - or empty. + static std::string calleeName(mlir::Operation *call) + { + mlir::StringAttr callee; + auto value = calleeValue(call); + if (auto symbol = directCallee(call)) + { + callee = symbol.getAttr(); + } + else if (auto calleeOp = value ? value.getDefiningOp() : nullptr) + { + if (auto getMethodOp = mlir::dyn_cast(calleeOp)) + { + calleeOp = getMethodOp.getBoundFunc().getDefiningOp(); + } + + if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + // a method reached through an interface (spec 31): `.` + auto method = mlir::dyn_cast(refOp.getIdentifier()); + auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); + if (method && interfaceType) + { + return (interfaceType.getName().getValue() + "." + method.getValue()).str(); + } + } + } + + return callee ? callee.getValue().str() : std::string(); + } + + // `'h.c'`, `'arr[]'`, `'h.c.v'` - or `a field`, `an element` when the root has no name (a + // temporary, or no --di). + std::string describePlace(mlir::Value ref) + { + auto path = pathOf(ref); + if (path.empty()) + { + return ref.getDefiningOp() ? "an element" : "a field"; + } + + return "'" + path + "'"; + } + + std::string pathOf(mlir::Value ref) + { + auto place = placeBase(ref); + std::string step; + switch (place.kind) + { + case PlaceKind::Field: { + auto field = fieldName(ref.getDefiningOp()); + if (field.empty()) + { + return {}; + } + + step = "." + field; + break; + } + case PlaceKind::Element: + step = "[]"; + break; + case PlaceKind::SharedValue: + step = ".value"; + break; + case PlaceKind::InterfaceField: { + auto field = mlir::dyn_cast(ref.getDefiningOp().getIdentifier()); + if (!field) + { + return {}; + } + + step = "." + field.getValue().str(); + break; + } + case PlaceKind::None: + return {}; + } + + auto base = rootOf(place.base); + std::string prefix; + if (isPlace(base)) + { + prefix = pathOf(base); + } + else if (auto loadOp = base.getDefiningOp()) + { + auto from = loadOp.getReference(); + if (isPlace(from)) + { + prefix = pathOf(from); + } + else if (auto varOp = from.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else if (auto global = from.getDefiningOp()) + { + prefix = global.getGlobalName().str(); + } + } + else if (auto varOp = base.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else + { + prefix = ownerName(base).str(); + } + + // a name MLIRGen made up (`.a`, the array a `for...of` walks) means nothing to the reader + if (prefix.empty() || prefix == "this value" || prefix.front() == '.') + { + return {}; + } + + return prefix + step; + } + + static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp) + { + auto type = propertyRefOp.getObjectRef().getType(); + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + + if (auto classType = mlir::dyn_cast(type)) + { + type = classType.getStorageType(); + } + + auto position = propertyRefOp.getPosition(); + mlir::Attribute id; + if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) + { + id = storageType.getId(position); + } + else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) + { + id = tupleType.getId(position); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.str(); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.getValue().str(); + } + + return {}; + } + + // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it + // has a ts.DebugVariable, whose location carries the name. + static llvm::StringRef ownerName(mlir::Value value) + { + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) + { + name = nameAt(user->getLoc()); + } + }); + + return name; + } + + // A `ts.RetainSlot`'s slot holds its initializer where the retain is. MLIRGen puts the retain at + // the declaration (takeOwnershipOfLocal), and nothing between the two may write the slot: in + // their block, no op is given the slot, no call runs, and no op has regions (a store could hide + // in them). What is stored into the slot later is decided at its own store; this retain takes + // only the initializer. No closure captures it. + static bool slotHoldsInitializerAt(mlir::Operation *op) + { + auto slot = mlir::cast(op).getSlot(); + auto varOp = slot.getDefiningOp(); + if (!varOp || !varOp.getInitializer() || isCellVariable(slot) || varOp->getBlock() != op->getBlock()) + { + return false; + } + + for (auto *between = varOp->getNextNode(); between; between = between->getNextNode()) + { + if (between == op) + { + return true; + } + + if (llvm::is_contained(between->getOperands(), slot) || isCall(between) || between->getNumRegions() > 0) + { + return false; + } + } + + return false; + } + + // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer, + // seen through its views. None for a variable with no initializer - its storage was hoisted + // in front of a try and its value arrives by a store this phase does not follow. + static mlir::Value retainedValue(mlir::Operation *op) + { + if (auto retainOp = mlir::dyn_cast(op)) + { + return rootOf(retainOp.getReference()); + } + + auto varOp = mlir::cast(op).getSlot().getDefiningOp(); + return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); + } + + // Does a value of this type own a block that a release would destroy? + bool ownsHeap(mlir::Value value) + { + // a `value_ref` points at storage it does not own, but one `ts.New` made and seen as its + // object is the object's block: the root of a generator's state object or an object literal + if (value.getDefiningOp() && mlir::isa(value.getType()) && + llvm::any_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user) && isView(user); })) + { + return true; + } + + return ownsHeap(value.getLoc(), value.getType()); + } + + bool ownsHeap(mlir::Location location, mlir::Type type) + { + return own_facts::ownsHeap(location, type); + } + + // A tuple read out of a local that a `ts.Constant` initializes, whose owning fields hold what + // that constant put there - null, a number, an immortal literal - or a fresh value moved in: + // the initial state a generator's maker copies into its state object, or a record built to be + // returned (`return {value: new C(), done: false}`, a `yield`'s result). rc's retain of it is + // its reference for the values moved in, which own moves instead (recordRetainOf). + bool holdsConstantsOrMoves(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + // a borrowing record (spec 32) takes nothing for what it borrows, and its fresh values move in + // as an owning record's do + if (!varOp || !isConstantRecordLocal(varOp) || !(isOwningRecord(value) || borrowingRecordOf(varOp))) + { + return false; + } + + // a fresh value moved in, its move decided by its own check against this retain + auto movesIn = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + return false; + } + + // a method's field is a `!ts.bound_ref`, which holds a function, not a block + auto refType = mlir::dyn_cast(propertyRefOp.getType()); + if (refType && !ownsHeap(propertyRefOp.getLoc(), refType.getElementType())) + { + return false; + } + + return llvm::any_of(propertyRefOp->getUsers(), + [](mlir::Operation *fieldUser) { return mlir::isa(fieldUser); }); + }); + + return !movesIn || recordRetainOf(varOp) != nullptr; + } + + // rc's one `ts.Retain` of a record local's one read, where the read is only returned: stored + // into a local that owns nothing and is read only by returns (the result slot), or copied into + // the box it is given in as an object or an interface (spec 27). It is the reference rc gives + // each fresh value stored into the record, so it is where they move in. Null for any other + // record. + static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp) + { + if (!isConstantRecordLocal(varOp)) + { + return nullptr; + } + + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + if (read) + { + return nullptr; + } + + read = loadOp; + } + } + + if (!read) + { + return nullptr; + } + + mlir::Operation *retain = nullptr; + auto boxed = false; + auto other = false; + for (auto *user : read->getUsers()) + { + if (mlir::isa(user)) + { + if (retain) + { + return nullptr; + } + + retain = user; + continue; + } + + // copied into the block it is given in as an object or an interface (spec 27): the box + // owns what the record held, and its own check decides where the box goes + auto storeOp = mlir::dyn_cast(user); + auto box = storeOp && storeOp.getValue() == read.getResult() ? recordBoxOf(storeOp.getReference()) + : mlir_ts::NewOp(); + if (box && box.getResult() == storeOp.getReference()) + { + if (boxed) + { + return nullptr; + } + + boxed = true; + continue; + } + + other = true; + if (mlir::isa(user)) + { + continue; + } + + // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's + // `ts.RetainSlot` of that local is the record's retain + if (auto ownerOp = mlir::dyn_cast(user)) + { + auto *slotRetain = ownerOp.getInitializer() == read.getResult() && isOwningVariable(ownerOp) + ? onlyRetainSlotOf(ownerOp) + : nullptr; + if (!slotRetain || retain) + { + return nullptr; + } + + retain = slotRetain; + continue; + } + + // stored into the result slot, or into an owning local (`a = { item: new C() }`) + auto slot = storeOp && storeOp.getValue() == read.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + if (!slot || !(isResultSlot(slot) || isOwningVariable(slot))) + { + return nullptr; + } + } + + // a boxed record goes nowhere else: the box is its one owner + if (boxed && other) + { + return nullptr; + } + + return retain; + } + + // `const b = [...a, ...a]`: MLIRGen builds the array in a local of its own that owns nothing, + // starting from a fresh one and pushing into it, and reads it once, when it is done, into the + // owning local it is made for. That local's `ts.RetainSlot` (`retain`) is the reference: the + // fresh array moves into the builder and then into the owner, and nothing else holds it. + bool isBuiltArrayRead(mlir::Operation *retain, mlir::Value value) + { + auto read = value.getDefiningOp(); + auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto owner = mlir::cast(retain).getSlot().getDefiningOp(); + if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || + !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp()) || + !owner || owner.getInitializer() != read.getResult() || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain) + { + return false; + } + + // only filled, and only before it is read + return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (user == read) + { + return true; + } + + return mlir::isa(user) && use.getOperandNumber() == 0 && + !reachableAfter(read, user, builder); + }); + } + + // `return [...a]`: the builder of isBuiltArrayRead, read once, when it is done, and returned. + // The return's `ts.Retain` (`retain`) is the reference the caller gets: the fresh array moves + // into the builder and then out of the function, and nothing else holds it. The builder's + // other reads only bind its `push` (`ts.ThisSymbolRef @__array_push`) or are not used. A store + // into a field is the same move, into a container: `h.c = [...a]`, and a generator's local + // (`const groups = [...result]`), which is a field of its state object. + bool isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value) + { + auto read = value.getDefiningOp(); + auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || + !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp())) + { + return false; + } + + // the read is retained and returned - stored into the function's result slot - and nothing else + auto returned = false; + for (auto *user : read->getUsers()) + { + if (user == retain) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + auto resultSlot = storeOp && storeOp.getValue() == read.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + auto intoField = storeOp && storeOp.getValue() == read.getResult() && + storeOp.getReference().getDefiningOp(); + if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) + { + return false; + } + + returned = true; + } + + if (!returned) + { + return false; + } + + // only filled, and only before it is read + return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (user == read) + { + return true; + } + + if (reachableAfter(read, user, builder)) + { + return false; + } + + if (auto loadOp = mlir::dyn_cast(user)) + { + return llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser) && loadUser->use_empty(); + }); + } + + return mlir::isa(user) && use.getOperandNumber() == 0; + }); + } + + // The one `ts.RetainSlot` of a local; null when there are none or several. + static mlir::Operation *onlyRetainSlotOf(mlir_ts::VariableOp varOp) + { + mlir::Operation *found = nullptr; + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + if (found) + { + return nullptr; + } + + found = user; + } + } + + return found; + } + + // A local that owns nothing, is never captured, and is read only to be returned. + static bool isResultSlot(mlir_ts::VariableOp varOp) + { + if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [&](mlir::Operation *user) { + if (auto storeOp = mlir::dyn_cast(user)) + { + return storeOp.getReference() == varOp.getResult(); + } + + auto loadOp = mlir::dyn_cast(user); + return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser); + }); + }); + } + + // The record whose field `taker` stores into, and rc's retain that moves the stored value in; + // null when `taker` is not such a store. + static mlir::Operation *recordRetainOfStore(mlir::Operation *taker) + { + auto storeOp = mlir::dyn_cast(taker); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto varOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + auto *retain = varOp ? recordRetainOf(varOp) : nullptr; + if (!retain || retain->getBlock() != taker->getBlock() || !taker->isBeforeInBlock(retain)) + { + return nullptr; + } + + return retain; + } + + // A string constant cast to `string` is the global itself, which a release skips; so is a read + // of an immortal global (spec 25). + static bool isImmortalLiteral(mlir::Value value) + { + if (isImmortalGlobalRead(value)) + { + return true; + } + + auto castOp = rootOf(value).getDefiningOp(); + return castOp && mlir::isa(castOp.getType()) && + castOp.getIn().getDefiningOp(); + } + + // A use that reads the value without keeping it. Kept deliberately short: anything not listed + // is treated as taking the value, which can only turn a program into an error. + static bool isBorrow(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return true; + } + + // `assert(c, message)` prints the message when it fails, and keeps it either way + if (mlir::isa(user)) + { + return true; + } + + // `s.value`'s place and `Shared.count(s)` read the handle and keep nothing (spec 23.1) + if (mlir::isa(user)) + { + return true; + } + + // `--di`'s record of a folded const's value for the debugger: names it, keeps nothing + if (mlir::isa(user)) + { + return true; + } + + // a test - `if (d)`, `d?.x`, `typeof u` - asks about the value, and keeps nothing + if (mlir::isa(user)) + { + return true; + } + + // ...and so does a cast to `null` or `undefined`, which holds no block: `--di` records the + // `r` that `if (r == null)` narrows as one + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) || + castOp.getType().isInteger(1) || isBoundForCall(castOp); + } + + if (mlir::isa(user)) + { + return isBoundForCall(user); + } + + // arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee + // with no facts that keeps one retains it, and that retain is its own error + if (isCall(user)) + { + return !isKeptArgument(user, value); + } + + // the array being pushed onto is written, not taken; what is inserted is taken + if (auto pushOp = mlir::dyn_cast(user)) + { + return pushOp.getOp() == value && !llvm::is_contained(pushOp.getItems(), value); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return unshiftOp.getOp() == value && !llvm::is_contained(unshiftOp.getItems(), value); + } + + if (auto spliceOp = mlir::dyn_cast(user)) + { + return spliceOp.getOp() == value && !llvm::is_contained(spliceOp.getItems(), value); + } + + // a declaration that does not own what it holds only reads it + if (auto varOp = mlir::dyn_cast(user)) + { + return !isOwningVariable(varOp); + } + + // the box of a closure that borrows what it captures holds a copy that owns nothing; the + // closure's own uses are the borrow's (walkBorrowed, decideClosures) + if (mlir::isa(user) && user->hasAttr(OWN_CAPTURE_BORROW_ATTR_NAME)) + { + return true; + } + + // a store through a `ts.New`'d `value_ref` fills its block and keeps nothing of it + if (auto storeOp = mlir::dyn_cast(user)) + { + return mlir::isa(value.getType()) && storeOp.getReference() == value && + storeOp.getValue() != value; + } + + return false; + } + + // `ts.CreateBoundFunction(ts.Cast(object), method)` split straight back into `ts.GetThis` and + // `ts.GetMethod` for a call: a method of another module's class - its constructor, when it is + // built with `new` - called on an object here. It reads the object as `ts.ThisSymbolRef` does + // for a method of this module; nothing else may use the cast or the bound function, and the + // object comes out again only as a call's argument, which is a borrow of its own. An + // interface's `this` (`ts.ExtractInterfaceThis`) is bound so for a call of a function-typed + // field (`i.toString()` where `toString: () => string`). + static bool isBoundForCall(mlir::Operation *thisOp) + { + if (thisOp->getNumResults() != 1 || !mlir::isa(thisOp->getResult(0).getType()) || + thisOp->use_empty()) + { + return false; + } + + return llvm::all_of(thisOp->getUses(), [](mlir::OpOperand &use) { + auto boundOp = mlir::dyn_cast(use.getOwner()); + if (!boundOp || boundOp.getThisVal() != use.get()) + { + return false; + } + + return llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { + if (mlir::isa(boundUser)) + { + return true; + } + + return mlir::isa(boundUser) && + llvm::all_of(boundUser->getUsers(), [](mlir::Operation *thisUser) { return isCall(thisUser); }); + }); + }); + } + + // Is `value` an argument the call's callee keeps? + static bool isKeptArgument(mlir::Operation *call, mlir::Value value) + { + auto args = callArgs(call); + return llvm::any_of(ownedParams(call), [&](int32_t index) { + return static_cast(index) < args.size() && args[index] == value; + }); + } + + // The moves out of a fresh or unknown SSA value. Each taking use (spec 11.1) is a move: + // - nothing else may use the value after it - another taker included - and a taker that + // control comes back to without the value being made again is a move on every iteration + // of a loop (spec 2.6); + // - where the temporary is also released (`__owned_result` rc gives back at block end), the + // taker must be one rc gave its own reference (takerAcquires), and each release the move + // reaches must be dominated by it - that release then has nothing to give back and is + // erased. A release the move cannot reach stays: it is the owner on the other paths. + // Reports and returns false otherwise. + bool checkValueMoves(mlir::Value value, llvm::SetVector &toErase) + { + // Nothing to move: a number owns no block, and a string literal is the immortal global, + // which any number of places may hold. Under --opt, CSE merges identical literals before + // this pass, so one such value is routinely stored into several places. A read out of a + // container owns nothing either: checkPlaceRead decides it. Nor does other data nothing + // owns: an empty optional, a function, a tuple of literals (isConstantData). + if (!ownsHeap(value) || isImmortalLiteral(value) || isConstantData(value) || placeReadOf(value)) + { + return true; + } + + // A temporary's releases are one owner, one release on each way out. Two on one path are + // two owners: each view rc gave a reference of its own (`use(o); use(o)`, with `o` a + // folded object literal and `use` taking an interface: two `ts.NewInterface`s of one block). + if (!checkReleasedOnce(value)) + { + return false; + } + + if (quietly([&] { return movesOf(value, toErase); })) + { + return true; + } + + // Not a move. Every taker a `let` that can borrow, and the temporary's own release the + // owner that bounds them (spec 2.2, verdict 2) - else the move's error stands. + llvm::SmallVector takers; + llvm::SmallVector releases; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (!isBorrow(user, used)) + { + takers.push_back(user); + } + }); + + if (!releases.empty() && !takers.empty() && + llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) + { + auto owner = ownerName(value); + auto ok = true; + for (auto *taker : takers) + { + ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; + } + + return ok; + } + + return movesOf(value, toErase); + } + + // No release of `value` comes after another on any path. Reports and returns false otherwise. + bool checkReleasedOnce(mlir::Value value) + { + llvm::SmallVector releases; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) + { + releases.push_back(user); + } + }); + + auto *definition = value.getDefiningOp(); + for (auto *first : releases) + { + for (auto *second : releases) + { + if (first != second && reachableAfter(first, second, definition)) + { + // the view the second release gives back, where rc took its reference + auto *view = second->getOperand(0).getDefiningOp(); + reportSecondReference(view ? view : second, value); + return false; + } + } + } + + return true; + } + + // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. + bool movesOf(mlir::Value value, llvm::SetVector &toErase) + { + llvm::SmallVector takers; + llvm::SmallVector releases; + llvm::SmallVector reads; + llvm::DenseMap readOf; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (isBorrow(user, used)) + { + reads.push_back(user); + readOf.try_emplace(user, used); + } + else + { + takers.push_back(user); + } + }); + + auto *definition = value.getDefiningOp(); + auto name = describe(value); + llvm::SmallVector moved; + for (auto *taker : takers) + { + if (takerLoops(taker, definition)) + { + reportMovedInLoop(taker, name); + return false; + } + + llvm::SmallVector made; + if (definition) + { + made.push_back(definition); + } + + // the call that keeps it, given it again through another operand + if (auto *again = callGivenAgain(taker, value, {value})) + { + reportUseAfterMove(again, taker, name); + return false; + } + + for (auto *use : llvm::concat(takers, reads)) + { + if (use != taker && reachableAfter(taker, use, definition)) + { + // a read through the new owner, while it holds the value (spec 29) + if (auto found = readOf.find(use); + found != readOf.end() && readsThroughOwner(taker, found->second, use, made)) + { + continue; + } + + reportUseAfterMove(use, taker, name); + return false; + } + } + + if (releases.empty()) + { + continue; // rc handed the taker the value's own reference: nothing to cancel + } + + if (!takerAcquires(taker, value)) + { + reportSecondReference(taker, value); + return false; + } + + auto *acquire = acquirePoint(taker, value); + for (auto *release : releases) + { + if (!reachableAfter(acquire, release, definition)) + { + continue; + } + + if (!dominance->properlyDominates(acquire, release)) + { + reportMovedOnSomePaths(taker, release, name); + return false; + } + + moved.push_back(release); + } + } + + toErase.insert(moved.begin(), moved.end()); + return true; + } + + // A call that keeps an argument and is given the same value again through another operand - the + // value itself, a view of it, something read out of it, another read of its slot (`f(n, n)`, + // `f(h, n, n.v)` with only the first `n` kept): the callee takes the value while it borrows it + // through the other operand, and may free it under that borrow, so the other operand is a use + // after the move (spec 33, review). `sources` are what the moved value is: the value, or every + // read of the owning slot it came from. The call itself, or null. + mlir::Operation *callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources) + { + if (!isCall(taker) || ownedParams(taker).empty()) + { + return nullptr; + } + + auto args = callArgs(taker); + auto kept = ownedParams(taker); + auto movedRoot = rootOf(moved); + // one kept position is the move itself; the value at a second one (`f(n, n)` with both + // kept) is a second owner, a use after the first move like any other operand + auto skipped = false; + for (auto &operand : taker->getOpOperands()) + { + // the moved argument itself, where the callee keeps it + auto number = operand.getOperandNumber(); + if (!skipped && !args.empty() && number >= args.getBeginOperandIndex() && + number < args.getBeginOperandIndex() + args.size() && + llvm::is_contained(kept, static_cast(number - args.getBeginOperandIndex())) && + rootOf(operand.get()) == movedRoot) + { + skipped = true; + continue; + } + + for (auto source : sources) + { + if (derivedFrom(source).contains(operand.get())) + { + return taker; + } + } + } + + return nullptr; + } + + // Does control come back round to `taker` without making the value again? Only then is it a + // move on every iteration of a loop. + bool takerLoops(mlir::Operation *taker, mlir::Operation *definition) + { + auto &body = getFunction().getFunctionBody(); + auto *top = body.findAncestorOpInRegion(*taker); + if (!top) + { + return true; + } + + auto *kill = definition ? body.findAncestorOpInRegion(*definition) : nullptr; + llvm::SmallPtrSet seen; + llvm::SmallVector work(top->getBlock()->getSuccessors().begin(), + top->getBlock()->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + if (kill && kill->getBlock() == block && + (block != top->getBlock() || kill->isBeforeInBlock(top))) + { + continue; // the value is made again before the taker runs again + } + + if (block == top->getBlock()) + { + return true; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + return false; + } + + // Can control reach `to` after `from` has run, without passing `kill` on the way? `kill` is + // the source's definition - the producer of an SSA value, or a local's `ts.Variable` - and a + // path through it is a new value, not this one. Straight-line order within a block, then the + // CFG from the block's successors; a path back into `from`'s own block reaches every op in it. + // + // Ops nested in a region that is not the function body are compared by their ancestors in the + // body. That answers "reachable" whenever the two share an ancestor, which can only turn a + // program into an error. (By this pass async bodies and lambdas are functions of their own; + // after the affine lowering no op of a body holds a region, so the case is kept for safety.) + bool reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill) + { + return reachableAfter(from, to, kill ? llvm::ArrayRef(kill) : llvm::ArrayRef()); + } + + // Whether reachableAfter can order `from` and `to` at all: both inside the function body and not + // one op of it (nested in the same statement). A check whose safe answer is "not reachable" must + // refuse what it cannot order, where reachableAfter would answer "reachable". + bool orderedInBody(mlir::Operation *from, mlir::Operation *to) + { + auto &body = getFunction().getFunctionBody(); + from = body.findAncestorOpInRegion(*from); + to = body.findAncestorOpInRegion(*to); + return from && to && from != to; + } + + // The same, where a path through any of `kills` is a new value. A use that is itself a kill + // runs after the value is made again. + bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) + { + if (llvm::is_contained(kills, to)) + { + return false; + } + + auto &body = getFunction().getFunctionBody(); + from = body.findAncestorOpInRegion(*from); + to = body.findAncestorOpInRegion(*to); + llvm::SmallVector killAt; + for (auto *kill : kills) + { + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } + } + + if (!from || !to) + { + return true; + } + + if (from == to) + { + // Two distinct ops that share one ancestor statement: the order inside it is not + // known here, and "reachable" can only turn a program into an error. + return true; + } + + // is there a kill in `block` after `after` (from its start when null) and before `before` + // (to its end when null)? + auto killsIn = [&](mlir::Block *block, mlir::Operation *after, mlir::Operation *before) { + return llvm::any_of(killAt, [&](mlir::Operation *kill) { + return kill->getBlock() == block && (!after || after->isBeforeInBlock(kill)) && + (!before || kill->isBeforeInBlock(before)); + }); + }; + + auto *fromBlock = from->getBlock(); + auto *toBlock = to->getBlock(); + if (fromBlock == toBlock && from->isBeforeInBlock(to)) + { + return !killsIn(fromBlock, from, to); + } + + // every way out of this block runs a kill first + if (killsIn(fromBlock, from, nullptr)) + { + return false; + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(fromBlock->getSuccessors().begin(), fromBlock->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + if (block == toBlock && !killsIn(block, nullptr, to)) + { + return true; + } + + if (killsIn(block, nullptr, nullptr)) + { + continue; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + return false; + } + + // The use that takes `value` for this retain: the declaration a `ts.RetainSlot` belongs to, + // or the one use of the value, or of a view of it, after a `ts.Retain` that is not a read. + static mlir::Operation *takerOf(mlir::Operation *retain, mlir::Value value) + { + if (auto retainSlotOp = mlir::dyn_cast(retain)) + { + return retainSlotOp.getSlot().getDefiningOp(); + } + + // a call that keeps the argument: rc's retain for it is in the callee + if (isCall(retain)) + { + return retain; + } + + mlir::Operation *taker = nullptr; + auto several = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user) || isBorrow(user, used)) + { + return; + } + + several = several || taker; + taker = user; + }); + + // rc retained this read and hands the receiver the next read of the same place + if (!taker && !several) + { + if (auto twin = twinReadAfter(retain, value)) + { + return takerOf(retain, twin); + } + } + + return several ? nullptr : taker; // two takers of one read: not a move this phase proves + } + + // rc reads a place twice for one copy - `.map(f)` boxes its array so - and retains the first + // read, whose only use is that retain: the read of the same place that follows it in its block, + // with nothing stored into the place between, is the same value. Null when there is none. + // The one op that uses `value`, but `--di`'s ts.DebugVariable, which only names it; or null. + static mlir::Operation *soleUser(mlir::Value value) + { + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + continue; + } + + if (found) + { + return nullptr; + } + + found = user; + } + + return found; + } + + static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value) + { + auto first = value.getDefiningOp(); + if (!first || !mlir::isa(retain) || soleUser(value) != retain || retain->getBlock() != first->getBlock()) + { + return {}; + } + + auto place = first.getReference(); + for (auto *op = retain->getNextNode(); op; op = op->getNextNode()) + { + if (auto loadOp = mlir::dyn_cast(op); loadOp && loadOp.getReference() == place) + { + return loadOp.getResult(); + } + + // anything else given the place may write through it - a store, a push - and a call may + // write anything + if (llvm::is_contained(op->getOperands(), place) || isCall(op)) + { + return {}; + } + } + + return {}; + } + + // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and + // retained what it read for the receiver; the move erases that retain and every release of + // the slot the move reaches, which must be dominated by it - or, with `placeReleases`, is + // placed on the paths that did not move it (spec 30). Any other use of the slot the + // move reaches - a read, an assignment's own read of it - is a use after the move. Reports and + // returns false when it is not a move. + // + // An assignment the move reaches gives the slot a new value to own (#528): a path through it + // is a new value, as a path through the declaration is. Its release before the store goes + // with the move - what the slot held is the receiver's - and the releases only a path through + // an assignment reaches stay, to destroy the new value. + bool checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, + llvm::SetVector &toErase, bool placeReleases = false) + { + auto slot = load.getReference(); + auto varOp = slot.getDefiningOp(); + auto name = varName(varOp); + + auto *taker = takerOf(retain, value); + if (!taker) + { + reportSecondReference(retain); + return false; + } + + auto assignments = slotAssignments(slot); + llvm::SmallVector kills(assignments.begin(), assignments.end()); + if (varOp) + { + kills.push_back(varOp); + } + + // the move's own read, met again around a loop the slot was declared outside of + if (reachableAfter(taker, load, kills)) + { + reportMovedInLoop(taker, name); + return false; + } + + // the slot given another value on some path between the move's read and its taker - inside + // the call's own argument list, `f(n, (n = mk(7), 0))`, on one arm of it, `f(n, c ? (n = + // mk(7), 0) : 0)`: the assignment's release frees what the read holds before the callee takes + // it, so the assignment is a use of the slot after the move. The taker may be an assignment + // of the slot itself (`n = n`): it is the destination, not between. A read and a taker + // reachableAfter cannot order (one statement kept as a region, or outside the body) refuse. + { + llvm::SmallVector readAgain{load.getOperation()}; + if (varOp) + { + readAgain.push_back(varOp); + } + + for (auto *assignment : assignments) + { + if (assignment != taker && reachableAfter(load, assignment, varOp) && + reachableAfter(assignment, taker, readAgain)) + { + reportUseAfterMove(assignment, taker, name); + return false; + } + } + + llvm::SmallVector killsBetween(kills); + llvm::erase(killsBetween, taker); + if (!orderedInBody(load, taker) || !reachableAfter(load, taker, killsBetween)) + { + reportUseAfterMove(taker, taker, name); + return false; + } + } + + // the call that keeps it, given the slot's value again through another operand: a read of + // the slot that reaches the call without crossing an assignment of it (not one made before + // an assignment gave the slot its current value, `const arr = [n]; n = mk(5); f(n, arr)`); + // since the move's own read reaches the call so too, such a read holds the moved value + llvm::SmallVector slotReads; + for (auto *user : slot.getUsers()) + { + if (auto readOp = mlir::dyn_cast(user); readOp && reachableAfter(readOp, taker, kills)) + { + slotReads.push_back(readOp.getResult()); + } + } + + if (auto *again = callGivenAgain(taker, value, slotReads)) + { + reportUseAfterMove(again, taker, name); + return false; + } + + // Every read of the slot is a use of `a`, and so is every use of what a read returned: a + // `const b = a` is folded into its load, so `b.x` after the move reads the moved value + // through a load that came before it. A read through the new owner, while it holds the + // value, is not such a use (spec 29). + auto throughOwner = [&](mlir::Operation *user, mlir::Value used) { + return readsThroughOwner(taker, used, user, kills); + }; + + for (auto *use : slot.getUsers()) + { + if (mlir::isa(use)) + { + continue; + } + + if (use != load.getOperation() && reachableAfter(taker, use, kills)) + { + auto readOp = mlir::dyn_cast(use); + if (!readOp || !onlyReadThroughOwner(readOp.getResult(), throughOwner)) + { + reportUseAfterMove(use, taker, name); + return false; + } + + continue; + } + + if (auto readOp = mlir::dyn_cast(use)) + { + if (auto *late = readUsedAfter(readOp, retain, taker, varOp, throughOwner)) + { + reportUseAfterMove(late, taker, name); + return false; + } + } + } + + auto *acquire = mlir::isa(retain) ? taker : retain; + llvm::SmallVector moved; + for (auto *use : slot.getUsers()) + { + if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) + { + continue; + } + + // a release that one path reaches holding the moved value and another holding a new + // one an assignment after the move stored: there is no single answer for it + auto someMoved = !dominance->properlyDominates(acquire, use); + for (auto *assignment : assignments) + { + llvm::SmallVector movedAgain{load.getOperation()}; + if (varOp) + { + movedAgain.push_back(varOp); + } + + someMoved = someMoved || + (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)); + } + + if (someMoved) + { + // ...but the paths that did not move it can release it before they join the + // others (spec 30) + if (placeReleases && placeOnUnmovedEdges(varOp, acquire, retain, taker, assignments, toErase)) + { + return true; + } + + reportMovedOnSomePaths(taker, use, name, placeReleases ? joinedUse : nullptr); + return false; + } + + moved.push_back(use); + } + + toErase.insert(moved.begin(), moved.end()); + return true; + } + + // ---- A move on some paths only (spec 30) ---- + // + // `if (c) { best = v; }`: after the paths join, `v` holds its value on one and a moved one on + // the other, and its one release there has no single answer. No run-time record of the move is + // kept (spec 2.2); instead, where nothing uses the value after the join, the release is placed + // on each edge into the join that did not move it - freeing the value earlier, where it is + // already dead - and the releases after the join go. Each path frees the value once: through + // its new owner where it moved, through the edge where it did not. + + // What the slot of an owning local holds at a point: nothing yet or any more (declared later, + // released), its value, or a value moved out of it. A point some paths reach holding the value + // and others having moved it is where the paths join. + enum SlotState : unsigned + { + SlotDead = 1, + SlotLive = 2, + SlotMoved = 4 + }; + + static bool isJoined(unsigned state) + { + return (state & SlotLive) && (state & SlotMoved); + } + + // A value of this type can point into a block: anything but a number, a boolean, a literal or + // an empty value. + static bool carriesPointer(mlir::Type type) + { + return !mlir::isa(type); + } + + // A release on an edge into the join that did not move the local's value (spec 30), put in after + // the analysis. + struct EdgeRelease + { + mlir::Value slot; + mlir::Block *from; + unsigned successor; + mlir::Location loc; + }; + + llvm::SmallVector edgeReleases; + + // The use that kept placeOnUnmovedEdges from placing the releases, for the error's note. + mlir::Operation *joinedUse = nullptr; + + // Can control reach `to` from the start of `start`, without passing `kills` on the way? + bool reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills) + { + auto &body = getFunction().getFunctionBody(); + to = body.findAncestorOpInRegion(*to); + if (!to) + { + return true; + } + + llvm::SmallVector killAt; + for (auto *kill : kills) + { + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + auto killed = [&](mlir::Operation *before) { + return llvm::any_of(killAt, [&](mlir::Operation *kill) { + return kill->getBlock() == block && (!before || kill->isBeforeInBlock(before) || kill == before); + }); + }; + + if (to->getBlock() == block && !killed(to)) + { + return true; + } + + if (!killed(nullptr)) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return false; + } + + // The blocks of an exception handler: from a landing pad to the end of its catch. A release there + // would be a call inside an exception funclet. + llvm::SmallPtrSet handlerBlocks() + { + llvm::SmallPtrSet handler; + llvm::SmallVector work; + for (auto &block : getFunction().getFunctionBody()) + { + if (!block.empty() && mlir::isa(block.front())) + { + work.push_back(&block); + } + } + + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!handler.insert(block).second) + { + continue; + } + + auto ends = llvm::any_of(*block, [](mlir::Operation &op) { + return mlir::isa(op); + }); + if (!ends) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return handler; + } + + // The move at `acquire` out of the owning local `varOp` reaches a release that other paths reach + // holding the value. Places a release of the slot on every edge into the join that holds the value + // and has not moved it, and erases every release a moved value reaches - when nothing uses the + // value, through the slot or through anything read out of it, where the paths have joined. False, + // with nothing placed or erased, when it cannot. + // The slot of an owning local through the function (spec 30): its state at the entry and the exit + // of each block, and before each op of the body. + struct SlotStates + { + llvm::DenseMap entry; + llvm::DenseMap exit; + llvm::DenseMap before; + }; + + bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, + mlir::Operation *taker, llvm::ArrayRef assignments, + llvm::SetVector &toErase) + { + joinedUse = nullptr; + llvm::SmallVector releases; + if (!slotReleasesInBody(varOp, acquire, assignments, releases)) + { + return false; + } + + auto states = slotStates(varOp, acquire, assignments, releases); + + // every release a moved value reaches goes; one some path reaches with nothing in the slot + // is not this rule's + llvm::SmallVector erased; + for (auto *release : releases) + { + auto state = states.before.lookup(release); + if (state & SlotMoved) + { + if (state & SlotDead) + { + return false; + } + + erased.push_back(release); + } + } + + llvm::SmallVector placed; + llvm::SetVector releasedInto; + if (!unmovedEdges(varOp, states, placed, releasedInto)) + { + return false; + } + + if (!noUseWhereJoined(varOp, retain, taker, assignments, states, releasedInto)) + { + return false; + } + + toErase.insert(erased.begin(), erased.end()); + edgeReleases.append(placed.begin(), placed.end()); + return true; + } + + // The releases of the slot, when the local, the move, its assignments and its releases are all in + // the function's body. False when one is not. + bool slotReleasesInBody(mlir_ts::VariableOp varOp, mlir::Operation *acquire, + llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases) + { + auto &body = getFunction().getFunctionBody(); + if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || + acquire->getParentRegion() != &body || + llvm::any_of(assignments, [&](mlir::Operation *op) { return op->getParentRegion() != &body; })) + { + return false; + } + + auto slot = varOp.getResult(); + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + if (user->getParentRegion() != &body) + { + return false; + } + + releases.push_back(user); + } + } + + return true; + } + + // The slot's state through the function, forward from its entry: assigned, moved at `acquire`, + // released. + SlotStates slotStates(mlir_ts::VariableOp varOp, mlir::Operation *acquire, llvm::ArrayRef assignments, + llvm::ArrayRef releases) + { + auto &body = getFunction().getFunctionBody(); + auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { + if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) + { + return SlotLive; + } + + if (op == acquire) + { + return SlotMoved; + } + + if (llvm::is_contained(releases, op)) + { + return SlotDead; + } + + return state; + }; + + SlotStates states; + auto &entry = states.entry; + auto exitOf = [&](mlir::Block *block) { + auto state = entry.lookup(block); + for (auto &op : *block) + { + state = step(&op, state); + } + + return state; + }; + + entry[&body.front()] = SlotDead; + llvm::SmallVector work{&body.front()}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + auto state = exitOf(block); + for (auto *successor : block->getSuccessors()) + { + auto &into = entry[successor]; + if ((into | state) != into) + { + into |= state; + work.push_back(successor); + } + } + } + + // ...and before each op of the body + for (auto &block : body) + { + auto state = entry.lookup(&block); + for (auto &op : block) + { + states.before[&op] = state; + state = step(&op, state); + } + + states.exit[&block] = state; + } + + return states; + } + + // Places a release of the slot on every edge into the join that holds the value and has not moved + // it. False, with nothing placed, when an edge cannot take one, or there is none. + bool unmovedEdges(mlir_ts::VariableOp varOp, const SlotStates &states, llvm::SmallVectorImpl &placed, + llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + auto &entry = states.entry; + + // the edges into the join that hold the value and did not move it + auto handler = handlerBlocks(); + for (auto &block : body) + { + if (!entry.count(&block)) + { + continue; // unreachable + } + + auto state = states.exit.lookup(&block); + for (auto index : llvm::seq(0, block.getNumSuccessors())) + { + auto into = entry.lookup(block.getSuccessor(index)); + if (!isJoined(into)) + { + continue; + } + + // a join some path reaches with nothing in the slot is not this rule's + if (into & SlotDead) + { + return false; + } + + // an edge that moved it, or from where the paths have joined already + if (state != SlotLive) + { + continue; + } + + auto *terminator = block.getTerminator(); + if (!mlir::isa(terminator) || handler.contains(&block) || + !dominance->dominates(varOp.getOperation(), terminator)) + { + return false; + } + + placed.push_back({slot, &block, static_cast(index), terminator->getLoc()}); + releasedInto.insert(block.getSuccessor(index)); + } + } + + return !placed.empty(); + } + + // Whether nothing uses the value where the paths have joined, or after an edge released it. + // False, with the use in `joinedUse` where there is one, when the release cannot be placed. + bool noUseWhereJoined(mlir_ts::VariableOp varOp, mlir::Operation *retain, mlir::Operation *taker, + llvm::ArrayRef assignments, const SlotStates &states, + const llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + + auto joinedAt = [&](mlir::Operation *user) { + auto *top = body.findAncestorOpInRegion(*user); + return !top || isJoined(states.before.lookup(top)); + }; + + // Nothing uses the value where the paths have joined - a read there, on the path that did + // not move it, needs what the edge released - neither through the slot nor through anything + // read out of it: a field, a view, a borrowing local, a closure. The move itself hands the + // value to its new owner, whose reads are its own. A value the slot is given - its + // initializer, an assignment's - is the slot's only once it is stored: a use before that (a + // constructor's, or one around a loop before it is made again) is not a use of it. + // + // Past the joined points too: what was read out of the value before an edge released it + // may not be used after that edge - after a new value is stored into the slot, or after its + // scope ends - unless it was read out again (spec 30). A read of the slot after a new value + // is stored reads that one. + struct Origin + { + mlir::Operation *stored; + mlir::Operation *made; + }; + + llvm::SmallVector origins{{nullptr, nullptr}}; + llvm::SmallVector> values; + llvm::DenseSet> seen; + auto taint = [&](mlir::Value value, unsigned origin) { + if (carriesPointer(value.getType()) && seen.insert({value, origin}).second) + { + values.push_back({value, origin}); + } + }; + + auto given = [&](mlir::Value value, mlir::Operation *stored) { + origins.push_back({stored, value.getDefiningOp()}); + taint(value, origins.size() - 1); + }; + + taint(slot, 0); + if (varOp.getInitializer()) + { + given(varOp.getInitializer(), varOp); + } + + for (auto *assignment : assignments) + { + given(mlir::cast(assignment).getValue(), assignment); + } + + // reached from an edge that released the value, before the read it came from (or the op + // that made it) runs again + auto usedAfterRelease = [&](mlir::Operation *made, mlir::Operation *user) { + llvm::SmallVector madeAgain; + if (made) + { + madeAgain.push_back(made); + } + + return llvm::any_of(releasedInto, [&](mlir::Block *join) { return reachableFrom(join, user, madeAgain); }); + }; + + auto heldAt = [&](unsigned origin, mlir::Operation *user) { + auto &from = origins[origin]; + if (!from.stored) + { + return true; + } + + llvm::SmallVector madeAgain; + if (from.made) + { + madeAgain.push_back(from.made); + } + + return reachableAfter(from.stored, user, madeAgain); + }; + + while (!values.empty()) + { + auto [value, origin] = values.pop_back_val(); + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || user == retain) + { + continue; + } + + // the slot's own releases and assignments are decided above + if (value == slot && (mlir::isa(user) || llvm::is_contained(assignments, user))) + { + continue; + } + + if (joinedAt(user) && heldAt(origin, user)) + { + joinedUse = user; + return false; + } + + if (value != slot && usedAfterRelease(origins[origin].made, user)) + { + joinedUse = user; + return false; + } + + if (user == taker) + { + continue; + } + + // a store of it into a local makes every read of that local one; one into a place + // is not followed, and the release is not placed + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == value) + { + auto into = storeOp.getReference().getDefiningOp(); + if (!into || into.getCaptured().value_or(false)) + { + return false; + } + + taint(into.getResult(), origin); + continue; + } + + if (auto branch = mlir::dyn_cast(user)) + { + for (auto index : llvm::seq(0, user->getNumSuccessors())) + { + auto operands = branch.getSuccessorOperands(index); + for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) + { + if (operand == value) + { + taint(user->getSuccessor(index)->getArgument(operands.getProducedOperandCount() + position), + origin); + } + } + } + } + + // a call's result is its own block, unless its facts say it borrows this argument - a + // plain borrow, or a borrowing record that holds it (spec 32) + auto borrows = true; + if (isCall(user)) + { + auto args = callArgs(user); + borrows = llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + } + + // a read of the slot is what its uses came from + auto from = origin; + if (value == slot && user->getNumResults() > 0) + { + origins.push_back({nullptr, user}); + from = origins.size() - 1; + } + + for (auto result : user->getResults()) + { + if (borrows) + { + taint(result, from); + } + } + + // inside a region - a closure, an async body - it is the op that holds the region + // that hands it on + if (user->getParentRegion() != &body) + { + for (auto result : body.findAncestorOpInRegion(*user)->getResults()) + { + taint(result, origin); + } + } + } + } + + return true; + } + + // Puts in the releases placeOnUnmovedEdges decided: at the end of a block that goes only to the + // join, else in a block of its own on the edge. + void placeEdgeReleases() + { + for (auto &edge : edgeReleases) + { + auto *terminator = edge.from->getTerminator(); + mlir::OpBuilder builder(terminator); + if (terminator->getNumSuccessors() == 1) + { + builder.create(edge.loc, edge.slot); + continue; + } + + auto *join = terminator->getSuccessor(edge.successor); + auto operands = mlir::cast(terminator).getSuccessorOperands(edge.successor); + llvm::SmallVector forwarded(operands.getForwardedOperands()); + auto *split = new mlir::Block(); + split->insertAfter(edge.from); + builder.setInsertionPointToEnd(split); + builder.create(edge.loc, edge.slot); + builder.create(edge.loc, join, forwarded); + if (!forwarded.empty()) + { + operands.erase(0, forwarded.size()); + } + + terminator->setSuccessor(split, edge.successor); + } + + edgeReleases.clear(); + } + + // The stores of the assignments to an owning local: rc releases what the slot holds right + // before it stores the new value (an assignment, and a `var` declaration met again). + static llvm::SmallVector slotAssignments(mlir::Value slot) + { + llvm::SmallVector assignments; + for (auto *user : slot.getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + auto releaseOp = storeOp ? mlir::dyn_cast_or_null(storeOp->getPrevNode()) + : mlir_ts::ReleaseSlotOp(); + if (releaseOp && storeOp.getReference() == slot && releaseOp.getSlot() == slot) + { + assignments.push_back(storeOp); + } + } + + return assignments; + } + + // A use of what `read` returned - directly or through casts - that the move at `taker` can + // reach, other than the move's own retain and taker. None if there is none. A path back + // through `read` itself reaches a use of a value read again: the read, met after the move, + // is the slot's own use to check (around a loop, of a value an assignment stored). + // `isRead` accepts a use that reads the value through its new owner (spec 29). + mlir::Operation *readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, + mlir::Operation *kill, + llvm::function_ref isRead = nullptr) + { + llvm::SmallVector values{read.getResult()}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto *user : current.getUsers()) + { + if (user == retain || user == taker || mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) || isView(user)) + { + values.push_back(user->getResult(0)); + continue; + } + + llvm::SmallVector kills{read.getOperation()}; + if (kill) + { + kills.push_back(kill); + } + + if (reachableAfter(taker, user, kills) && !(isRead && isRead(user, current))) + { + return user; + } + } + } + + return nullptr; + } + + // ---- Reads after a move, through the new owner (spec 29) ---- + // + // `h.c = c; print(c.x)`, `best = v; s += v.x`: after the move the value is the new owner's, and + // a read of it through the old name is a borrow of the place it moved into, as a read of + // `h.c` would be. It is bounded the way checkPlaceRead bounds such a read - by the ends of the + // place's roots and by the drops that may destroy what the place holds (an overwrite, through an + // interface view too; a call whose facts may drop; a call given a closure) - on a path from the + // move to the read on which the value is not made again or moved again. + + // Every use of `value`, through its views, is a read `isRead` accepts. A retain - rc's reference + // for an owner - is not. + static bool onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead) + { + auto all = true; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + all = all && (mlir::isa(user) || + (!mlir::isa(user) && isRead(user, used))); + }); + + return all; + } + + // A use that only reads the value: a field, element or length read through it, a method reached + // through it, a comparison or arithmetic, a print, an assert, a test, or an argument the callee + // borrows. Anything else - a store, an owning `let`, a keeping call, a return - would be a second + // owner. + static bool isReadAfterMove(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return isBorrow(user, value); + } + + return isCall(user) && !isKeptArgument(user, value); + } + + // The place a taker moves its value into, as a chain checkPlaceRead would read it: an owning + // local (a declaration, or an assignment), or a field or element - of a local or a parameter + // (`this`). False for anything else: a call that keeps it, a push, a return, a global, a + // captured variable, a handle's payload. + bool destinationChain(mlir::Operation *taker, Chain &chain) + { + auto ownSlot = [](mlir_ts::VariableOp varOp) { + return varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false); + }; + + if (auto varOp = mlir::dyn_cast(taker)) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({varOp.getResult(), Chain::Slot}); + return true; + } + + auto storeOp = mlir::dyn_cast(taker); + if (!storeOp) + { + return false; + } + + auto ref = storeOp.getReference(); + if (auto varOp = ref.getDefiningOp()) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({ref, Chain::Slot}); + return true; + } + + if (!isPlace(ref)) + { + return false; + } + + chain = chainOf(nullptr, ref); + if (!chain.handles.empty() || chain.roots.empty()) + { + return false; + } + + // a global's field, or a captured variable's: not a place of this function + return llvm::none_of(chain.roots, [](auto &root) { + auto loadOp = root.first.template getDefiningOp(); + return loadOp && (loadOp.getReference().template getDefiningOp() || + isLoadedCell(loadOp.getReference())); + }); + } + + // Is `use`, a use of `value` after `taker` moved it, a read through the place it moved into + // (spec 29)? `made` is where the value is made again (its definition, the local's declaration + // and assignments). Sound when no path from the move through an end or a drop of the place to + // the read - or to anything the read produced - makes the value again or moves it again: such a + // path is the only way the place can stop holding the value before the read. + bool readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, + llvm::ArrayRef made) + { + if (!isReadAfterMove(use, value)) + { + return false; + } + + Chain chain; + if (!destinationChain(taker, chain)) + { + return false; + } + + // A read again of the old name after a drop still finds the moved value - the slot is not + // emptied by a move - so neither the read nor the slot's read is where it is made again. + llvm::SmallVector kills(made.begin(), made.end()); + kills.push_back(taker); + llvm::SmallVector uses; + uses.push_back({use, {kills.begin(), kills.end()}}); + for (auto result : use->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + // what the read produced - a field's reference, a bound method - still points into the + // block; a call's result that borrows its argument is the value again and may not be kept + if (walkBorrowed(result, kills, uses, nullptr, Keeping{nullptr, false}, false, !!borrowingCall(result))) + { + return false; + } + } + + // a method reached through it whose callee keeps its `this`: a second owner + for (auto &borrowUse : uses) + { + if (!isCall(borrowUse.op)) + { + continue; + } + + auto args = callArgs(borrowUse.op); + for (auto index : ownedParams(borrowUse.op)) + { + if (static_cast(index) < args.size() && + mlir::isa_and_nonnull(args[index].getDefiningOp())) + { + return false; + } + } + } + + auto outlives = [&](mlir::Operation *end) { + if (end == taker || !reachableAfter(taker, end, made)) + { + return false; + } + + return llvm::any_of(uses, [&](BorrowUse &borrowUse) { + return (borrowUse.op != end || !borrowUse.callee) && reachableAfter(end, borrowUse.op, borrowUse.kills); + }); + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value) && outlives(drop)) + { + return false; + } + } + + return true; + } + + // Where rc takes the receiver's reference: its `ts.Retain` of the value, when that comes + // first in the taker's block, else the taker itself (a declaration, whose `ts.RetainSlot` + // follows it). A release between the two - a `return`'s scope exit runs after its retain + // and before the store into the return slot - is after the move. + static mlir::Operation *acquirePoint(mlir::Operation *taker, mlir::Value value) + { + // a store into a record, which rc retains as a whole after it is filled + if (auto *retain = recordRetainOfStore(taker)) + { + return retain; + } + + mlir::Operation *first = taker; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user) && user->getBlock() == taker->getBlock() && + user->isBeforeInBlock(first)) + { + first = user; + } + }); + + return first; + } + + // Did rc give this taker a reference of its own - a `ts.Retain` of the value in front of it, + // or the `ts.RetainSlot` of an owning local declared from it? Only then is there a release on + // the taker's side to give the value back once it has moved. `ts.NewInterface` is a taker to + // this pass but a view to rc, which retains nothing for it and relies on the instance's own + // release; moving the instance into it would leave nothing to release it at all. + static bool takerAcquires(mlir::Operation *taker, mlir::Value value) + { + // a callee that keeps its argument retained it in its own body under rc + if (isCall(taker)) + { + return true; + } + + if (recordRetainOfStore(taker)) + { + return true; + } + + if (auto varOp = mlir::dyn_cast(taker)) + { + return llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + } + + auto acquires = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && + user->isBeforeInBlock(taker)); + }); + + return acquires; + } + + // The declaration's name survives only as debug metadata (`--di`). At this level MLIRGen's + // form of it is a NameLoc first inside the location fused with the scope - what LowerToLLVM's + // preserveTypesForDebugInfo reads - and after that pass it is a DILocalVariable. Without + // `--di` the error still points at the right place. + static llvm::StringRef varName(mlir_ts::VariableOp varOp) + { + return nameAt(varOp.getLoc()); + } + + static llvm::StringRef nameAt(mlir::Location location) + { + if (auto fused = mlir::dyn_cast>(location)) + { + if (auto name = fused.getMetadata().getName()) + { + return name.getValue(); + } + } + + if (auto scoped = mlir::dyn_cast>(location)) + { + if (auto named = mlir::dyn_cast_or_null(scoped.getLocations().front())) + { + return named.getName().getValue(); + } + } + + return "this value"; + } + + static llvm::StringRef describe(mlir::Value value) + { + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (auto varOp = mlir::dyn_cast(user); varOp && name == "this value") + { + name = varName(varOp); + } + }); + + return name; + } + + // A second reference nobody proved safe. `value` is the value it is to, when the caller knows + // it; a retain names its own. The shapes this pass recognises get the rule they break (spec + // 21.2); anything else, the catch-all. + void reportSecondReference(mlir::Operation *op, mlir::Value value = {}) + { + if (!reporting()) + { + return; + } + + llvm::StringRef name = "this value"; + if (auto retainSlotOp = mlir::dyn_cast(op)) + { + if (auto varOp = retainSlotOp.getSlot().getDefiningOp()) + { + name = varName(varOp); + } + } + else if (auto varOp = mlir::dyn_cast(op)) + { + name = varName(varOp); + } + + if (!value && mlir::isa(op)) + { + value = retainedValue(op); + } + + if (!value || !reportShapeOfSecondReference(op, rootOf(value), name)) + { + auto diag = op->emitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + noteLostFacts(diag); + } + + signalPassFailure(); + } + + // The named shapes of a second reference to `root` (spec 21.2). False when it is none of them. + bool reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name) + { + auto f = getFunction(); + + // returned, where another return gives a borrow of an argument and this one does not, or + // the other way round: the callers cannot be told which they get + if (isReturned(root)) + { + auto param = borrowedParam(root); + if (auto *other = otherReturn(param)) + { + auto diag = op->emitError("'") << f.getName() + << "' returns a borrow of its argument on some paths and another value on " + "others; -mm=own cannot tell its callers whether they own the result"; + diag.attachNote(other->getLoc()) + << (param >= 0 ? "returns another value here" : "returns a borrow of its argument here"); + return true; + } + } + + // a parameter's value, or a part of it, returned or kept: a fact its callers must know + // (spec 15), and noteLostFacts says why they cannot + if (auto param = borrowedParam(root); param >= 0) + { + auto diag = op->emitError("'") << (name == "this value" ? paramName(param) : name); + if (isReturned(root)) + { + diag << "' is a parameter, returned here, but -mm=own cannot tell the callers of '" << f.getName() + << "' that the result borrows it"; + } + else + { + diag << "' is a parameter, kept here, but -mm=own cannot make the callers of '" << f.getName() + << "' give it up"; + } + + noteLostFacts(diag); + return true; + } + + // a store into a local whose value is not this function's to replace: a parameter's slot, or + // a local that owns nothing (MLIRGen's takeOwnershipOfLocal) + auto storeOp = mlir::dyn_cast(op); + if (auto slot = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + slot && !isOwningVariable(slot) && !isResultSlot(slot)) + { + // a parameter's slot, assigned (isParameterSlot is the unassigned one) + auto argument = mlir::dyn_cast_or_null(slot.getInitializer()); + if (argument && argument.getOwner()->isEntryBlock() && + mlir::isa(argument.getOwner()->getParentOp())) + { + op->emitError("'") << varName(slot) << "' is a parameter and cannot be assigned under -mm=own yet"; + return true; + } + + // MLIRGen makes no owner of a local declared without a value, nor of one declared in a + // catch or finally clause, whose release would be a call inside an exception funclet + if (!slot.getInitializer()) + { + op->emitError("'") << varName(slot) + << "' owns nothing under -mm=own (it is declared without a value, or in a catch or " + "finally clause), so it cannot be given a new value here"; + return true; + } + } + + // merged from branches or loop iterations + if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) + { + op->emitError("'") << name + << "' is merged from several branches (`?:`, `&&`, `||`, `??`, a loop); -mm=own cannot " + "prove which one owns it yet"; + return true; + } + + auto loadOp = root.getDefiningOp(); + + // a read of a global + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp()) + { + // a local that reads it: phase 3's borrow, which a call or an assignment of the global + // may end + if (mlir::isa(op)) + { + op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() + << "', but -mm=own cannot prove the global keeps its value while '" << name + << "' is used: a call or an assignment may overwrite it"; + return true; + } + + op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() + << "' and cannot be stored, returned or captured"; + return true; + } + + // a read of a captured variable, through its cell + if (loadOp && isLoadedCell(loadOp.getReference())) + { + op->emitError("'") << name << "' borrows a captured variable and cannot be stored, returned or captured"; + return true; + } + + // an object literal built in a local, holding a value it owns: moved only into an owning + // local or the result (recordRetainOf) + auto record = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!record && storeOp) + { + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + record = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + } + + if (record && !isOwningVariable(record) && record.getInitializer() && + record.getInitializer().getDefiningOp()) + { + op->emitError("an object literal that holds a value it owns is moved only into a local or " + "returned as it is; -mm=own cannot move it here yet"); + return true; + } + + return false; + } + + // The name of parameter `index`, as its slot carries it (under `--di`). + llvm::StringRef paramName(int index) + { + llvm::StringRef name = "this value"; + getFunction()->walk([&](mlir_ts::VariableOp varOp) { + auto argument = mlir::dyn_cast_or_null(varOp.getInitializer()); + if (argument && argument.getOwner()->isEntryBlock() && static_cast(argument.getArgNumber()) == index) + { + name = varName(varOp); + } + }); + + return name; + } + + // Is `root`, or a view of it, stored into the result slot? + static bool isReturned(mlir::Value root) + { + auto returned = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value used) { + auto storeOp = mlir::dyn_cast(user); + auto slot = storeOp && storeOp.getValue() == used ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + returned = returned || (slot && isResultSlot(slot)); + }); + + return returned; + } + + // A return that disagrees with one of a borrow of parameter `param` (with -1, with one that + // borrows nothing): a store into the result slot of a value that holds a block and borrows + // another parameter or none - or, for -1, borrows a parameter. Null when there is none. + mlir::Operation *otherReturn(int param) + { + mlir::Operation *found = nullptr; + getFunction()->walk([&](mlir_ts::StoreOp storeOp) { + auto slot = storeOp.getReference().getDefiningOp(); + if (found || !slot || !isResultSlot(slot) || holdsNoBlock(storeOp.getValue())) + { + return; + } + + auto borrows = borrowedParam(storeOp.getValue()); + if (param >= 0 ? borrows != param : borrows >= 0) + { + found = storeOp; + } + }); + + return found; + } + + // Where the body would have been fine had its callers known a fact about it, say why they + // cannot. + void noteLostFacts(mlir::InFlightDiagnostic &diag) + { + auto f = getFunction(); + if (auto why = f->getAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) + { + diag.attachNote(f.getLoc()) << "'" << f.getName() << "' could return a borrow or keep a parameter, but " + << why.getValue(); + } + } + + void reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = move->emitError("'") << name + << "' is moved here on some paths only; -mm=own cannot release it " + "on the others yet"; + diag.attachNote(exit->getLoc()) << "returns here without moving it; the caller gave it up"; + signalPassFailure(); + } + + // A handle made into a union whose other members include one that owns a block (spec 23.2): + // `Shared | string` given a Shared. gc, none and rc accept it. + void reportHandleInOwningUnion(mlir::Operation *op) + { + auto handleIn = llvm::any_of(op->getOperandTypes(), [](mlir::Type type) { + return MLIRTypeHelper::isSharedHandleType(type); + }); + if (!handleIn) + { + return; + } + + for (auto resultType : op->getResultTypes()) + { + if (!MLIRTypeHelper::isSharedInOwningUnionType(resultType)) + { + continue; + } + + if (!reporting()) + { + return; + } + + auto unionType = resultType; + if (auto optionalType = mlir::dyn_cast(unionType)) + { + unionType = optionalType.getElementType(); + } + + mlir::Type shared; + mlir::Type owning; + for (auto member : mlir::cast(unionType).getTypes()) + { + if (mlir::isa(member)) + { + shared = shared ? shared : member; + } + else if (!owning && MLIRTypeHelper::ownsHeapMemoryOfUnionMember(member)) + { + owning = member; + } + } + + auto diag = op->emitError("a ") << printed(shared) << " in a union with "; + if (owning) + { + diag << "'" << printed(owning) << "', which -mm=own does not count,"; + } + else + { + diag << "a member -mm=own does not count,"; + } + + diag << " is not supported"; + signalPassFailure(); + return; + } + } + + static std::string printed(mlir::Type type) + { + llvm::SmallString<128> text; + llvm::raw_svector_ostream out(text); + MLIRPrinter printer{}; + printer.printType(out, type); + return text.str().str(); + } + + void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) + { + if (!reporting()) + { + return; + } + + llvm::StringRef name = ownerName(arg); + if (auto loadOp = rootOf(arg).getDefiningOp()) + { + if (auto varOp = loadOp.getReference().getDefiningOp()) + { + name = varName(varOp); + } + } + + auto callee = calleeName(call); + auto diag = call->emitError("argument '") << name << "' is given to '" << (callee.empty() ? "a function" : callee) + << "', which keeps it, but -mm=own cannot move it here: this " + "function does not own it"; + noteLostFacts(diag); + signalPassFailure(); + } + + void reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' is used here after its value was moved"; + diag.attachNote(move->getLoc()) << "value moved here"; + signalPassFailure(); + } + + void reportMovedInLoop(mlir::Operation *move, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + move->emitError("'") << name + << "' is moved inside a loop but was made outside it; only a borrow could do " + "that, and -mm=own cannot prove one yet"; + signalPassFailure(); + } + + void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows '" << owner << "' but is used here after '" << owner + << "' is released or overwritten"; + diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; + signalPassFailure(); + } + + void reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, + llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows " << place + << " but is used here after it may be released or overwritten"; + diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; + signalPassFailure(); + } + + void reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name << "' borrows " << place << " and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + + void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + auto diag = op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; + noteLostFacts(diag); + signalPassFailure(); + } + + void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << owner << "' is moved here while '" << borrower + << "' borrows it; a borrowed value cannot be moved"; + signalPassFailure(); + } + + void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + auto diag = op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; + noteLostFacts(diag); + signalPassFailure(); + } + + void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name << "' borrows '" << owner << "' and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + + void reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why) + { + if (!reporting()) + { + return; + } + + auto diag = capture->emitError("a closure that escapes owns what it captures, but -mm=own cannot move this " + "variable into it: ") + << why; + diag.attachNote(escape->getLoc()) << "the closure escapes here"; + signalPassFailure(); + } + + void reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' is captured by a closure that escapes, and is still used here"; + diag.attachNote(capture->getLoc()) << "the closure takes it here"; + signalPassFailure(); + } + + void reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, + llvm::StringRef captured) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << closure << "' borrows '" << captured << "', which it captures, but runs here after '" + << captured << "' is released"; + diag.attachNote(end->getLoc()) << "'" << captured << "' is released here"; + signalPassFailure(); + } + + void reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << captured << "' is captured by a closure, but -mm=own cannot tell who owns its value here"; + signalPassFailure(); + } + + void reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name + << "' is a captured parameter and cannot be assigned under -mm=own: its value is the " + "caller's"; + signalPassFailure(); + } + + void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, + mlir::Operation *usedAt = nullptr) + { + if (!reporting()) + { + return; + } + + auto diag = move->emitError("'") << name + << "' is moved here on some paths only; -mm=own cannot release it " + "on the others yet"; + diag.attachNote(release->getLoc()) << "released here on every path"; + if (usedAt) + { + diag.attachNote(usedAt->getLoc()) + << "used here, after the paths join, so it cannot be released before they do"; + } + + signalPassFailure(); + } +}; + +} // namespace own_inference + +#endif // MLIR_TYPESCRIPT_OWNERSHIPINFERENCEPASS_H From 06e7259d324403bfcfc441e42a168e67b861a59f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:21:12 +0100 Subject: [PATCH 16/25] own passes: OwnershipInferenceClosures.cpp out of OwnershipInferencePass.h (I1) Closures (spec 2.5, phase 5) - captures, cells, generator state boxes and the fields they borrow, and the slots a closure takes. A pure move of classifyClosures, borrowingFieldsOf, isYieldedField, isBorrowingField, classifyBorrowingFields, isBorrowingCell, isBorrowingBoxField, checkBorrowedReads, checkBoundedResults, checkBoundedByBox, boundedUses, classifyStateBoxes, isStoreIntoState, retainBefore, followClosure, holdsCaptures, decideClosures, movesCellAway, decideEscaping, decideCells, decideSlot, decideSlotReceiver, takeValuesOfTheirOwn. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../TypeScript/OwnershipInferenceClosures.cpp | 1216 +++++++++++++++ .../lib/TypeScript/OwnershipInferencePass.h | 1300 +---------------- 3 files changed, 1289 insertions(+), 1228 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceClosures.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 56decb8ad..73a698340 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -33,6 +33,7 @@ add_mlir_dialect_library(MLIRTypeScript RelocateConstantPass.cpp OwnershipVerifierPass.cpp OwnershipInferencePass.cpp + OwnershipInferenceClosures.cpp OwnershipSignaturePass.cpp OwnershipSignatureCallbacks.cpp OwnershipSignatureClosedWorld.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp new file mode 100644 index 000000000..36b48de93 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -0,0 +1,1216 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): closures (spec 2.5, phase 5) - captures, +// cells, generator state boxes and the fields they borrow, and the slots a closure takes. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + void OwnershipInferencePass::classifyClosures() + { + getFunction().walk([&](mlir_ts::CreateBoundFunctionOp boundOp) { + if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME) || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return; + } + + auto closure = std::make_shared(); + closure->op = boundOp; + closure->box = boundOp.getThisVal(); + for (auto *user : closure->box.getUsers()) + { + if (user == boundOp.getOperation()) + { + continue; + } + + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + closure->escape = closure->escape ? closure->escape : fieldUser; + continue; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (isCall(user)) + { + closure->uses.push_back(user); + closure->boxUses.push_back(user); + continue; + } + + closure->escape = closure->escape ? closure->escape : user; + } + + if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); + escape && !closure->escape) + { + closure->escape = escape; + } + + // copies only: the box owns them (phase 4); only an alias needs deciding - unless the box + // is given to a generator's maker whose result borrows from it + if (!closure->hasCells) + { + // ...and only when every copy has an owner of its own: the box frees itself alone, so a + // value made for it (a callback that captures, `a.filter((x) => x > n)`) would be + // nobody's + closure->borrowsCopies = + !closure->escape && + llvm::any_of(closure->boxUses, + [](mlir::Operation *use) { return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); }) && + llvm::none_of(closure->fills, [&](auto &fill) { + auto value = fill.second.getValue(); + return !holdsNoBlock(value) && isFresh(rootOf(value)); + }); + if (!closure->borrowsCopies) + { + if (!closure->escape && closure->aliased) + { + closures.push_back(std::move(closure)); + } + + return; + } + } + + for (auto &fill : closure->fills) + { + auto storeOp = fill.second; + auto isCell = mlir::isa(storeOp.getValue().getType()); + // an escaping closure's copies move into its box as in phase 4, rc's retain with them + if (closure->escape && !isCell) + { + continue; + } + + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + if (!closure->escape && !isCell) + { + setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); + captureStores[storeOp] = closure.get(); + } + } + + if (!closure->escape) + { + setAttrTracked(boundOp, OWN_BORROWS_CAPTURES_ATTR_NAME); + } + + closureOf[boundOp.getOperation()] = closure.get(); + closures.push_back(std::move(closure)); + }); + } + + llvm::SmallVector OwnershipInferencePass::borrowingFieldsOf(mlir::Type recordType) + { + llvm::SmallVector fields; + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return fields; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType) + { + auto indices = mlir::cast(entry[1]).asArrayRef(); + fields.append(indices.begin(), indices.end()); + } + } + + return fields; + } + + bool OwnershipInferencePass::isYieldedField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto recordType = propertyRefOp->getOperand(0).getType(); + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_YIELDED_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return false; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType && + llvm::is_contained(mlir::cast(entry[1]).asArrayRef(), + static_cast(propertyRefOp.getPosition()))) + { + return true; + } + } + + return false; + } + + bool OwnershipInferencePass::isBorrowingField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto fields = borrowingFieldsOf(propertyRefOp->getOperand(0).getType()); + return fields.size() > 1 && llvm::is_contained(llvm::ArrayRef(fields).drop_front(), + static_cast(propertyRefOp.getPosition())); + } + + void OwnershipInferencePass::classifyBorrowingFields() + { + getFunction().walk([&](mlir::Operation *op) { + if (auto storeOp = mlir::dyn_cast(op); storeOp && isBorrowingField(storeOp.getReference())) + { + setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + fieldBorrowRetains.insert(retain); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast(op); + releaseSlotOp && isBorrowingField(releaseSlotOp.getSlot())) + { + fieldBorrowReleases.push_back(op); + } + }); + } + + bool OwnershipInferencePass::isBorrowingCell(mlir::Value ref) + { + auto cellLoad = ref.getDefiningOp(); + return cellLoad && isBorrowingBoxField(cellLoad.getReference()); + } + + bool OwnershipInferencePass::isBorrowingBoxField(mlir::Value ref) + { + auto cellRef = ref.getDefiningOp(); + auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + if (!boxRef) + { + return false; + } + + auto fields = borrowingFieldsOf(boxRef->getOperand(0).getType()); + return !fields.empty() && fields.front() == static_cast(boxRef.getPosition()); + } + + void OwnershipInferencePass::checkBorrowedReads() + { + getFunction().walk([&](mlir_ts::LoadOp loadOp) { + auto ref = loadOp.getReference(); + if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingBoxField(ref) || isBorrowingField(ref))) + { + return; + } + + auto reported = false; + forEachUse(loadOp.getResult(), [&](mlir::Operation *user, mlir::Value used) { + if (reported || isBorrow(user, used) || fieldBorrowRetains.contains(user)) + { + return; + } + + reportPlaceBorrowEscapes(user, ownerName(loadOp.getResult()), "the generator's argument"); + reported = true; + }); + }); + } + + void OwnershipInferencePass::checkBoundedResults() + { + getFunction().walk([&](mlir::Operation *call) { + auto bounded = call->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + if (!bounded || !isCall(call) || call->getNumResults() != 1) + { + return; + } + + auto name = ownerName(call->getResult(0)); + llvm::SmallVector uses; + if (auto *kept = boundedUses(call->getResult(0), uses)) + { + reportPlaceBorrowEscapes(kept, name, "its generator's arguments"); + return; + } + + auto args = callArgs(call); + for (auto index : bounded.asArrayRef()) + { + if (static_cast(index) >= args.size()) + { + continue; + } + + // a closure's box (`.map(f)` calls its maker with it): it lives while the closures on + // it do, and what it holds while their owners do. A reference to storage, so first. + auto root = rootOf(args[index]); + if (auto boxOp = root.getDefiningOp(); boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) + { + if (!checkBoundedByBox(call, root, uses, name)) + { + return; + } + + continue; + } + + if (holdsNoBlock(args[index]) || isImmortalLiteral(args[index])) + { + continue; + } + + if (borrowedParam(root) >= 0) + { + continue; // the caller's caller's: alive for all of this function + } + + Chain chain; + auto *read = placeReadOf(root); + if (read) + { + chain = chainOf(read); + } + else if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + chain.roots.push_back({root, Chain::NotOwned}); + } + + // the root's own ends; and, for a read out of a place, whatever may destroy what the + // place holds (a local or a made value is not destroyed by a call it is given) + auto ends = rootEnds(chain); + for (auto *drop : read ? llvm::ArrayRef(drops) : llvm::ArrayRef()) + { + if (drop != call && dropsChain(drop, chain, read)) + { + ends.push_back(drop); + } + } + + auto owner = ownerName(root); + for (auto *end : ends) + { + if (end == call) + { + continue; + } + + for (auto *use : uses) + { + if (use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, owner); + return; + } + } + } + } + }); + } + + bool OwnershipInferencePass::checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, + llvm::StringRef name) + { + llvm::SmallVector ends; + for (auto *user : box.getUsers()) + { + if (auto boundOp = mlir::dyn_cast(user); boundOp && boundOp.getThisVal() == box) + { + llvm::SmallVector closureUses; + auto aliased = false; + if (followClosure(boundOp.getResult(), closureUses, ends, aliased)) + { + reportPlaceBorrowEscapes(call, name, "a closure that escapes"); // the box may go anywhere + return false; + } + + continue; + } + + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult() || holdsNoBlock(storeOp.getValue())) + { + continue; + } + + auto root = rootOf(storeOp.getValue()); + Chain chain; + if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (borrowedParam(root) >= 0 || isImmortalLiteral(root)) + { + continue; + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + reportPlaceBorrowEscapes(call, name, "a value this function cannot bound"); + return false; + } + + auto rootEndsOf = rootEnds(chain); + ends.append(rootEndsOf.begin(), rootEndsOf.end()); + } + } + + for (auto *end : ends) + { + for (auto *use : uses) + { + if (end != call && use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, "the generator's source"); + return false; + } + } + } + + return true; + } + + mlir::Operation *OwnershipInferencePass::boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses) + { + llvm::SmallVector values{result}; + llvm::SmallPtrSet seen; + mlir::Operation *kept = nullptr; + while (!values.empty() && !kept) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (kept || mlir::isa(user)) + { + return; + } + + mlir::Value slot; + if (auto varOp = mlir::dyn_cast(user)) + { + slot = varOp.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) + { + slot = storeOp.getReference(); + } + + if (slot) + { + auto slotVar = slot.getDefiningOp(); + if (!slotVar || slotVar.getCaptured().value_or(false)) + { + kept = user; + return; + } + + for (auto *slotUser : slot.getUsers()) + { + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + } + + return; + } + + // returned by a function that says so (`@borrows`): its callers hold the result to + // the same bound, and the signature pass checked the arguments are its own + if (mlir::isa(user) && getFunction()->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + return; + } + + uses.push_back(user); + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + kept = user; + } + + return; + } + + // `it.next()`: the field read, the bound function, its `this` + if (mlir::isa(user)) + { + for (auto partResult : user->getResults()) + { + values.push_back(partResult); + } + + return; + } + + if (!isBorrow(user, used)) + { + kept = user; + } + }); + } + + return kept; + } + + void OwnershipInferencePass::classifyStateBoxes() + { + getFunction().walk([&](mlir_ts::VariableOp boxOp) { + auto box = boxOp.getResult(); + if (!boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) || closureOfBox(box)) + { + return; + } + + auto closure = std::make_shared(); + closure->box = box; + llvm::SmallVector boxRetains; + for (auto *user : box.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + return; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (mlir::isa(user)) + { + boxRetains.push_back(user); + continue; + } + + if (!closure->escape && isStoreIntoState(user, box)) + { + closure->escape = user; + continue; + } + + return; + } + + if (!closure->escape) + { + return; + } + + auto stateRef = mlir::cast(closure->escape).getReference().getDefiningOp(); + auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); + auto borrowing = borrowingFieldsOf(stateType); + closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); + // `.map`'s maker copies what its parameter box holds into the generator's box: a borrow + if (closure->borrowsValues) + { + for (auto &fill : closure->fills) + { + if (!mlir::isa(fill.second.getValue().getType())) + { + setAttrTracked(fill.second, OWN_CAPTURE_BORROW_ATTR_NAME); + } + } + } + + for (auto &fill : closure->fills) + { + // a parameter this function does not own, in a box that does not borrow: the box + // cannot take rc's reference for it, the caller's still holds it; left to the + // second-reference error, which says the callers would have to give it up + auto param = borrowedParam(fill.second.getValue()); + if (!closure->borrowsValues && param >= 0 && !llvm::is_contained(ownedParams(getFunction()), param)) + { + continue; + } + + if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + } + + for (auto *retain : boxRetains) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + stateBoxes[box] = closure.get(); + closures.push_back(std::move(closure)); + }); + } + + bool OwnershipInferencePass::isStoreIntoState(mlir::Operation *user, mlir::Value box) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == box + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto stateOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp || !isConstantRecordLocal(stateOp)) + { + return false; + } + + auto reads = 0; + for (auto *stateUser : stateOp->getUsers()) + { + auto loadOp = mlir::dyn_cast(stateUser); + if (!loadOp) + { + continue; + } + + ++reads; + for (auto *readUser : loadOp->getUsers()) + { + // rc's retain of the initial state, for fields that own (holdsConstantsOrMoves) + if (mlir::isa(readUser)) + { + continue; + } + + auto fillOp = mlir::dyn_cast(readUser); + if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) + { + return false; + } + } + } + + return reads > 0; + } + + mlir::Operation *OwnershipInferencePass::retainBefore(mlir::Value value, mlir::Operation *store) + { + // rc's retain of an earlier read of the same place (twinReadAfter) + if (auto loadOp = value.getDefiningOp()) + { + for (auto *user : loadOp.getReference().getUsers()) + { + auto earlier = mlir::dyn_cast(user); + auto *retain = earlier ? soleUser(earlier.getResult()) : nullptr; + if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && + twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && + retain->isBeforeInBlock(store)) + { + return retain; + } + } + } + + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (!mlir::isa(user) || user->getBlock() != store->getBlock() || + !user->isBeforeInBlock(store) || captureRetains.contains(user)) + { + continue; + } + + if (!found || found->isBeforeInBlock(user)) + { + found = user; + } + } + + return found; + } + + mlir::Operation *OwnershipInferencePass::followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &ends, bool &aliased) + { + llvm::SmallVector values{closure}; + llvm::SmallPtrSet seen; + mlir::Operation *escape = nullptr; + auto followSlot = [&](mlir::Value slot) { + for (auto *slotUser : slot.getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + if (mlir::isa(slotUser)) + { + ends.push_back(slotUser); + } + else if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + else if (auto storeOp = mlir::dyn_cast(slotUser); !storeOp || storeOp.getReference() != slot) + { + escape = escape ? escape : slotUser; + } + } + }; + + while (!values.empty() && !escape) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (escape || mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + ends.push_back(user); + return; + } + + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + escape = user; + } + else + { + uses.push_back(user); + } + + return; + } + + // the function and the box a call through the closure is made with + if (mlir::isa(user)) + { + for (auto *partUser : user->getResult(0).getUsers()) + { + if (isCall(partUser) && !isKeptArgument(partUser, user->getResult(0))) + { + uses.push_back(partUser); + } + else + { + escape = escape ? escape : partUser; + } + } + + return; + } + + mlir::Value slot; + if (auto varOp = mlir::dyn_cast(user)) + { + slot = varOp.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) + { + slot = storeOp.getReference(); + } + + // a local, owning or not (`let f: () => number;` owns nothing): its reads are the + // closure; a function's result slot is a local too, and its read is returned + auto slotVar = slot ? slot.getDefiningOp() : mlir_ts::VariableOp(); + if (slotVar && !slotVar.getCaptured().value_or(false)) + { + aliased = aliased || !isOwningVariable(slotVar); + followSlot(slot); + return; + } + + escape = user; + }); + } + + return escape; + } + + bool OwnershipInferencePass::holdsCaptures(mlir::Value value) + { + if (mlir::isa(value.getType())) + { + return true; + } + + auto root = rootOf(value); + if (auto varOp = root.getDefiningOp()) + { + return varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); + } + + // a closure's `this` is its box, a reference; a bound method's is the object + auto getThisOp = root.getDefiningOp(); + return getThisOp && mlir::isa(getThisOp.getType()); + } + + void OwnershipInferencePass::decideClosures(llvm::SetVector &toErase) + { + for (auto &closure : closures) + { + if (!closure->op) + { + decideEscaping(*closure, toErase); // a generator's box + continue; + } + + auto *boundOp = closure->op.getOperation(); + auto name = ownerName(closure->op.getResult()); + auto ok = true; + // Held by a local that owns nothing: rc's birth reference reads to the move logic as the + // store's, which would leave the box with no owner. The closure keeps its releases, and + // nothing may run it after them. + auto keepAliasedReleases = [&]() { + for (auto *end : closure->ends) + { + if (!mlir::isa(end)) + { + continue; + } + + toErase.remove(end); + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + }; + + if (!closure->hasCells && !closure->borrowsCopies) + { + keepAliasedReleases(); + continue; + } + + if (closure->escape) + { + decideEscaping(*closure, toErase); + continue; + } + + for (auto &fill : closure->fills) + { + auto value = fill.second.getValue(); + llvm::SmallVector ends; + llvm::StringRef captured = "this value"; + if (mlir::isa(value.getType())) + { + if (isCellVariable(value)) + { + captured = varName(value.getDefiningOp()); + for (auto *user : value.getUsers()) + { + if (mlir::isa(user) || movesCellAway(user)) + { + ends.push_back(user); + } + } + } + else if (!isLoadedCell(value)) + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + } + else + { + auto root = rootOf(value); + captured = ownerName(root); + Chain chain; + if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0 || isHandle(root)) + { + // nothing to own, a read the read's check bounds, a parameter, or a handle, + // whose copy in the box is counted (spec 23.2) + } + else if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + + ends = rootEnds(chain); + } + + for (auto *end : ends) + { + if (captureRetains.contains(end) || captureStores.count(end)) + { + continue; // a box's own copy, this one's or another borrowing closure's + } + + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, captured); + ok = false; + } + } + } + } + + if (closure->aliased) + { + keepAliasedReleases(); + } + + // a call through the box, after the closure value that owns the box is given back + for (auto *end : closure->ends) + { + for (auto *use : closure->boxUses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + + toErase.insert(closure->retains.begin(), closure->retains.end()); + } + } + + bool OwnershipInferencePass::movesCellAway(mlir::Operation *op) + { + auto storeOp = mlir::dyn_cast(op); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + if (!propertyRefOp || !mlir::isa(storeOp.getValue().getType())) + { + return false; + } + + if (stateBoxes.count(propertyRefOp.getObjectRef())) + { + return true; + } + + auto boundOp = closureOfBox(propertyRefOp.getObjectRef()); + auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); + return found != closureOf.end() && found->second->escape; + } + + void OwnershipInferencePass::decideEscaping(Closure &closure, llvm::SetVector &toErase) + { + auto ok = true; + for (auto &fill : closure.fills) + { + auto value = fill.second.getValue(); + mlir::Operation *move = fill.second; + if (!mlir::isa(value.getType())) + { + continue; + } + + if (isLoadedCell(value)) + { + reportEscapingCellNotOwned(move, closure.escape, "it is captured from an enclosing function"); + ok = false; + continue; + } + + auto varOp = isCellVariable(value) ? value.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp) + { + reportCaptureNotOwned(move, "this value"); + ok = false; + continue; + } + + auto name = varName(varOp); + auto elementType = mlir::cast(value.getType()).getElementType(); + // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) + if (!isOwningVariable(varOp) && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) && + !MLIRTypeHelper::isSharedHandleType(elementType)) + { + reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); + ok = false; + continue; + } + + if (takerLoops(move, varOp)) + { + reportMovedInLoop(move, name); + ok = false; + continue; + } + + llvm::SmallVector moved; + for (auto *user : value.getUsers()) + { + if (user == move || llvm::is_contained(closure.retains, user) || !reachableAfter(move, user, varOp)) + { + continue; + } + + if (!mlir::isa(user)) + { + reportUsedAfterEscapingCapture(user, move, name); + ok = false; + break; + } + + if (!dominance->properlyDominates(move, user)) + { + reportMovedOnSomePaths(move, user, name); + ok = false; + break; + } + + moved.push_back(user); + } + + toErase.insert(moved.begin(), moved.end()); + } + + // A call through the box itself after the closure was given away or given back. followClosure + // stops at the escape, so `ends` may miss releases past it; that is enough while the only + // calls through a box are those of a folded `const f`, whose release ends its block. A + // generator's box has no closure value and no such call. + if (!closure.op) + { + toErase.insert(closure.retains.begin(), closure.retains.end()); + return; + } + + auto name = ownerName(closure.op.getResult()); + llvm::SmallVector gone(closure.ends.begin(), closure.ends.end()); + gone.push_back(closure.escape); + for (auto *end : gone) + { + for (auto *use : closure.boxUses) + { + if (ok && use != end && reachableAfter(end, use, closure.op.getOperation())) + { + reportUseAfterMove(use, end, name); + ok = false; + } + } + } + + toErase.insert(closure.retains.begin(), closure.retains.end()); + } + + void OwnershipInferencePass::decideCells() + { + getFunction().walk([&](mlir_ts::VariableOp varOp) { + auto cell = varOp.getResult(); + if (!isCellVariable(cell) || isOwningVariable(varOp)) + { + return; + } + + auto elementType = mlir::cast(cell.getType()).getElementType(); + if (!ownsHeap(varOp.getLoc(), elementType)) + { + return; + } + + // A handle's cell holds a count of its own, taken where it is made (spec 23.2): its + // release gives that back, as under rc, and an assignment is a counted copy. + if (MLIRTypeHelper::isSharedHandleType(elementType)) + { + return; + } + + auto name = varName(varOp); + auto init = varOp.getInitializer(); + auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || + !mlir::isa(argument.getOwner()->getParentOp())) + { + reportCaptureNotOwned(varOp, name); + return; + } + + for (auto *user : cell.getUsers()) + { + if (mlir::isa(user)) + { + setAttrTracked(user, OWN_CELL_BORROWS_ATTR_NAME); + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (mlir::isa(user) || (storeOp && storeOp.getReference() == cell)) + { + reportCapturedParamAssigned(user, name); + return; + } + + // captured: the closure's body must not assign it either + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp) + { + continue; + } + + auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); + if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) + { + reportCapturedParamAssigned(boundOp, name); + return; + } + } + }); + } + + void OwnershipInferencePass::decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto owner = varName(slot.getDefiningOp()); + if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers)) + { + // a move on some paths only is placed when it is the local's one receiver (spec 30) + for (auto &receiver : receivers) + { + decideSlotReceiver(slot, receiver, owner, toErase, receivers.size() == 1); + } + + return; + } + + decideSlotReceivers(slot, receivers, owner, toErase); + } + + void OwnershipInferencePass::decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, + llvm::SetVector &toErase, bool sole) + { + if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); })) + { + // rc's retain goes with the move; a call that keeps the argument is the move itself + if (!isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + + return; + } + + if (auto borrower = borrowerOf(receiver.retain)) + { + borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase); + return; + } + + checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); // reports + } + + bool OwnershipInferencePass::takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers) + { + auto assignments = slotAssignments(slot); + llvm::SmallVector kills(assignments.begin(), assignments.end()); + if (auto varOp = slot.getDefiningOp()) + { + kills.push_back(varOp); + } + + for (auto &from : receivers) + { + for (auto &to : receivers) + { + if (&from != &to && reachableAfter(from.load, to.load, kills)) + { + return false; + } + } + } + + return true; + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 05f837f49..0967a5c74 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -232,1027 +232,90 @@ class OwnershipInferencePass // Finds the closures over cells and what they borrow, before anything else: whether a box's // stores are reads or takers depends on it. A closure with copies only keeps what phase 4 does // with it - its box owns them, and each copy moves in. - void classifyClosures() - { - getFunction().walk([&](mlir_ts::CreateBoundFunctionOp boundOp) { - if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME) || closureOfBox(boundOp.getThisVal()) != boundOp) - { - return; - } - - auto closure = std::make_shared(); - closure->op = boundOp; - closure->box = boundOp.getThisVal(); - for (auto *user : closure->box.getUsers()) - { - if (user == boundOp.getOperation()) - { - continue; - } - - if (auto propertyRefOp = mlir::dyn_cast(user)) - { - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - closure->escape = closure->escape ? closure->escape : fieldUser; - continue; - } - - closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); - closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); - } - - continue; - } - - if (isCall(user)) - { - closure->uses.push_back(user); - closure->boxUses.push_back(user); - continue; - } - - closure->escape = closure->escape ? closure->escape : user; - } - - if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); - escape && !closure->escape) - { - closure->escape = escape; - } - - // copies only: the box owns them (phase 4); only an alias needs deciding - unless the box - // is given to a generator's maker whose result borrows from it - if (!closure->hasCells) - { - // ...and only when every copy has an owner of its own: the box frees itself alone, so a - // value made for it (a callback that captures, `a.filter((x) => x > n)`) would be - // nobody's - closure->borrowsCopies = - !closure->escape && - llvm::any_of(closure->boxUses, - [](mlir::Operation *use) { return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); }) && - llvm::none_of(closure->fills, [&](auto &fill) { - auto value = fill.second.getValue(); - return !holdsNoBlock(value) && isFresh(rootOf(value)); - }); - if (!closure->borrowsCopies) - { - if (!closure->escape && closure->aliased) - { - closures.push_back(std::move(closure)); - } - - return; - } - } - - for (auto &fill : closure->fills) - { - auto storeOp = fill.second; - auto isCell = mlir::isa(storeOp.getValue().getType()); - // an escaping closure's copies move into its box as in phase 4, rc's retain with them - if (closure->escape && !isCell) - { - continue; - } - - if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - - if (!closure->escape && !isCell) - { - setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); - captureStores[storeOp] = closure.get(); - } - } - - if (!closure->escape) - { - setAttrTracked(boundOp, OWN_BORROWS_CAPTURES_ATTR_NAME); - } - - closureOf[boundOp.getOperation()] = closure.get(); - closures.push_back(std::move(closure)); - }); - } + void classifyClosures(); // The fields of a generator's state object of this type that borrow from its arguments // (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first. - llvm::SmallVector borrowingFieldsOf(mlir::Type recordType) - { - llvm::SmallVector fields; - if (auto objectType = mlir::dyn_cast(recordType)) - { - recordType = objectType.getStorageType(); - } - - if (auto storageType = mlir::dyn_cast(recordType)) - { - recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); - } - - auto module = getFunction()->getParentOfType(); - auto entries = module ? module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); - if (!entries) - { - return fields; - } - - for (auto entry : entries.getAsRange()) - { - if (mlir::cast(entry[0]).getValue() == recordType) - { - auto indices = mlir::cast(entry[1]).asArrayRef(); - fields.append(indices.begin(), indices.end()); - } - } - - return fields; - } - - // Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)? - bool isYieldedField(mlir::Value ref) - { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp) - { - return false; - } - - auto recordType = propertyRefOp->getOperand(0).getType(); - if (auto objectType = mlir::dyn_cast(recordType)) - { - recordType = objectType.getStorageType(); - } - - if (auto storageType = mlir::dyn_cast(recordType)) - { - recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); - } - - auto module = getFunction()->getParentOfType(); - auto entries = module ? module->getAttrOfType(OWN_YIELDED_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); - if (!entries) - { - return false; - } - - for (auto entry : entries.getAsRange()) - { - if (mlir::cast(entry[0]).getValue() == recordType && - llvm::is_contained(mlir::cast(entry[1]).asArrayRef(), - static_cast(propertyRefOp.getPosition()))) - { - return true; - } - } - - return false; - } - - // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? - bool isBorrowingField(mlir::Value ref) - { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp) - { - return false; - } - - auto fields = borrowingFieldsOf(propertyRefOp->getOperand(0).getType()); - return fields.size() > 1 && llvm::is_contained(llvm::ArrayRef(fields).drop_front(), - static_cast(propertyRefOp.getPosition())); - } - - // A store into a borrowing field holds what it stores without owning it, as a borrowing box's - // copy does (OWN_CAPTURE_BORROW_ATTR_NAME), and rc's retain for it goes; its release, when the - // field is overwritten, gives nothing back. - void classifyBorrowingFields() - { - getFunction().walk([&](mlir::Operation *op) { - if (auto storeOp = mlir::dyn_cast(op); storeOp && isBorrowingField(storeOp.getReference())) - { - setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); - if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) - { - fieldBorrowRetains.insert(retain); - } - } - else if (auto releaseSlotOp = mlir::dyn_cast(op); - releaseSlotOp && isBorrowingField(releaseSlotOp.getSlot())) - { - fieldBorrowReleases.push_back(op); - } - }); - } - - // Is `ref` a cell read out of the `.captured` box of a generator's state object that borrows: - // `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`? - bool isBorrowingCell(mlir::Value ref) - { - auto cellLoad = ref.getDefiningOp(); - return cellLoad && isBorrowingBoxField(cellLoad.getReference()); - } - - // Is `ref` a field of the `.captured` box of a generator's state object that borrows: - // `ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i)`? A field copied in by value holds - // the caller's value itself, a cell field the caller's cell. - bool isBorrowingBoxField(mlir::Value ref) - { - auto cellRef = ref.getDefiningOp(); - auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); - auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - if (!boxRef) - { - return false; - } - - auto fields = borrowingFieldsOf(boxRef->getOperand(0).getType()); - return !fields.empty() && fields.front() == static_cast(boxRef.getPosition()); - } - - // What a generator reads out of a borrowing cell or field is the caller's: every use of it must - // be a borrow - a store into another borrowing field is one (classifyBorrowingFields). A yield of - // it, a push, a return, a store anywhere else would give the caller's value a second owner, and - // rc retains nothing for some of these, so nothing else would see it. - void checkBorrowedReads() - { - getFunction().walk([&](mlir_ts::LoadOp loadOp) { - auto ref = loadOp.getReference(); - if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingBoxField(ref) || isBorrowingField(ref))) - { - return; - } - - auto reported = false; - forEachUse(loadOp.getResult(), [&](mlir::Operation *user, mlir::Value used) { - if (reported || isBorrow(user, used) || fieldBorrowRetains.contains(user)) - { - return; - } - - reportPlaceBorrowEscapes(user, ownerName(loadOp.getResult()), "the generator's argument"); - reported = true; - }); - }); - } - - // A call whose result borrows from its arguments (OWN_RESULT_BOUNDED_ATTR_NAME): a generator over - // a parameter's block. The result is owned as any fresh value is, but nothing may use it after - // one of those arguments is given back - released, assigned, or dropped from the place it was - // read from - and it may not be kept anywhere that could outlive them. Its releases are not - // uses: the state object's release gives back nothing it borrows. - void checkBoundedResults() - { - getFunction().walk([&](mlir::Operation *call) { - auto bounded = call->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); - if (!bounded || !isCall(call) || call->getNumResults() != 1) - { - return; - } - - auto name = ownerName(call->getResult(0)); - llvm::SmallVector uses; - if (auto *kept = boundedUses(call->getResult(0), uses)) - { - reportPlaceBorrowEscapes(kept, name, "its generator's arguments"); - return; - } - - auto args = callArgs(call); - for (auto index : bounded.asArrayRef()) - { - if (static_cast(index) >= args.size()) - { - continue; - } - - // a closure's box (`.map(f)` calls its maker with it): it lives while the closures on - // it do, and what it holds while their owners do. A reference to storage, so first. - auto root = rootOf(args[index]); - if (auto boxOp = root.getDefiningOp(); boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) - { - if (!checkBoundedByBox(call, root, uses, name)) - { - return; - } - - continue; - } - - if (holdsNoBlock(args[index]) || isImmortalLiteral(args[index])) - { - continue; - } - - if (borrowedParam(root) >= 0) - { - continue; // the caller's caller's: alive for all of this function - } - - Chain chain; - auto *read = placeReadOf(root); - if (read) - { - chain = chainOf(read); - } - else if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - chain.roots.push_back({root, Chain::NotOwned}); - } - - // the root's own ends; and, for a read out of a place, whatever may destroy what the - // place holds (a local or a made value is not destroyed by a call it is given) - auto ends = rootEnds(chain); - for (auto *drop : read ? llvm::ArrayRef(drops) : llvm::ArrayRef()) - { - if (drop != call && dropsChain(drop, chain, read)) - { - ends.push_back(drop); - } - } - - auto owner = ownerName(root); - for (auto *end : ends) - { - if (end == call) - { - continue; - } - - for (auto *use : uses) - { - if (use != end && reachableAfter(end, use, call)) - { - reportBorrowOutlives(use, end, name, owner); - return; - } - } - } - } - }); - } - - // The ends of a capture box given to a call with a bounded result: each release of a closure on - // it (the box goes with its last), and each end of what a field of it was filled from. Reports - // and returns false when a use of the result comes after one. - bool checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, - llvm::StringRef name) - { - llvm::SmallVector ends; - for (auto *user : box.getUsers()) - { - if (auto boundOp = mlir::dyn_cast(user); boundOp && boundOp.getThisVal() == box) - { - llvm::SmallVector closureUses; - auto aliased = false; - if (followClosure(boundOp.getResult(), closureUses, ends, aliased)) - { - reportPlaceBorrowEscapes(call, name, "a closure that escapes"); // the box may go anywhere - return false; - } - - continue; - } - - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult() || holdsNoBlock(storeOp.getValue())) - { - continue; - } - - auto root = rootOf(storeOp.getValue()); - Chain chain; - if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (borrowedParam(root) >= 0 || isImmortalLiteral(root)) - { - continue; - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - reportPlaceBorrowEscapes(call, name, "a value this function cannot bound"); - return false; - } - - auto rootEndsOf = rootEnds(chain); - ends.append(rootEndsOf.begin(), rootEndsOf.end()); - } - } - - for (auto *end : ends) - { - for (auto *use : uses) - { - if (end != call && use != end && reachableAfter(end, use, call)) - { - reportBorrowOutlives(use, end, name, "the generator's source"); - return false; - } - } - } - - return true; - } - - // Every use of a bounded result, through its views, the locals that hold it, and the function - // and object a call through it is made with. Answers the first use that keeps it - anything - // that is not a read, a call, a local, or rc's bookkeeping - or null. - mlir::Operation *boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses) - { - llvm::SmallVector values{result}; - llvm::SmallPtrSet seen; - mlir::Operation *kept = nullptr; - while (!values.empty() && !kept) - { - auto value = values.pop_back_val(); - if (!seen.insert(value).second) - { - continue; - } - - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (kept || mlir::isa(user)) - { - return; - } - - mlir::Value slot; - if (auto varOp = mlir::dyn_cast(user)) - { - slot = varOp.getResult(); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) - { - slot = storeOp.getReference(); - } - - if (slot) - { - auto slotVar = slot.getDefiningOp(); - if (!slotVar || slotVar.getCaptured().value_or(false)) - { - kept = user; - return; - } - - for (auto *slotUser : slot.getUsers()) - { - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - values.push_back(loadOp.getResult()); - } - } - - return; - } - - // returned by a function that says so (`@borrows`): its callers hold the result to - // the same bound, and the signature pass checked the arguments are its own - if (mlir::isa(user) && getFunction()->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) - { - return; - } - - uses.push_back(user); - if (isCall(user)) - { - if (isKeptArgument(user, used)) - { - kept = user; - } - - return; - } - - // `it.next()`: the field read, the bound function, its `this` - if (mlir::isa(user)) - { - for (auto partResult : user->getResults()) - { - values.push_back(partResult); - } - - return; - } - - if (!isBorrow(user, used)) - { - kept = user; - } - }); - } - - return kept; - } - - // A generator's maker puts what the generator captures - its parameters, an outer local - into - // a box, and the box into its state object's `.captured` field: the box of a closure that - // escapes, with the state object, at that store. There is no closure value, so no use runs it - // here and nothing calls through it. rc's `ts.RetainCell` of the box is the state object's - // reference, which moves in with it. A box used any other way is left to the RetainCell error. - void classifyStateBoxes() - { - getFunction().walk([&](mlir_ts::VariableOp boxOp) { - auto box = boxOp.getResult(); - if (!boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) || closureOfBox(box)) - { - return; - } - - auto closure = std::make_shared(); - closure->box = box; - llvm::SmallVector boxRetains; - for (auto *user : box.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(user)) - { - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - return; - } - - closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); - closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); - } - - continue; - } - - if (mlir::isa(user)) - { - boxRetains.push_back(user); - continue; - } - - if (!closure->escape && isStoreIntoState(user, box)) - { - closure->escape = user; - continue; - } - - return; - } - - if (!closure->escape) - { - return; - } - - auto stateRef = mlir::cast(closure->escape).getReference().getDefiningOp(); - auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); - auto borrowing = borrowingFieldsOf(stateType); - closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); - // `.map`'s maker copies what its parameter box holds into the generator's box: a borrow - if (closure->borrowsValues) - { - for (auto &fill : closure->fills) - { - if (!mlir::isa(fill.second.getValue().getType())) - { - setAttrTracked(fill.second, OWN_CAPTURE_BORROW_ATTR_NAME); - } - } - } - - for (auto &fill : closure->fills) - { - // a parameter this function does not own, in a box that does not borrow: the box - // cannot take rc's reference for it, the caller's still holds it; left to the - // second-reference error, which says the callers would have to give it up - auto param = borrowedParam(fill.second.getValue()); - if (!closure->borrowsValues && param >= 0 && !llvm::is_contained(ownedParams(getFunction()), param)) - { - continue; - } - - if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - } - - for (auto *retain : boxRetains) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - - stateBoxes[box] = closure.get(); - closures.push_back(std::move(closure)); - }); - } - - // `ts.Store(box, ts.PropertyRef(state, i))`, where `state` is a local a `ts.Constant` - // initializes whose every read fills a block `ts.New` made: a generator's initial state. - static bool isStoreIntoState(mlir::Operation *user, mlir::Value box) - { - auto storeOp = mlir::dyn_cast(user); - auto propertyRefOp = storeOp && storeOp.getValue() == box - ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto stateOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - if (!stateOp || !isConstantRecordLocal(stateOp)) - { - return false; - } - - auto reads = 0; - for (auto *stateUser : stateOp->getUsers()) - { - auto loadOp = mlir::dyn_cast(stateUser); - if (!loadOp) - { - continue; - } - - ++reads; - for (auto *readUser : loadOp->getUsers()) - { - // rc's retain of the initial state, for fields that own (holdsConstantsOrMoves) - if (mlir::isa(readUser)) - { - continue; - } - - auto fillOp = mlir::dyn_cast(readUser); - if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) - { - return false; - } - } - } - - return reads > 0; - } - - // rc's reference for the box's copy of `value`: the retain of it nearest before the store that - // fills the box, in the same block. - mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store) - { - // rc's retain of an earlier read of the same place (twinReadAfter) - if (auto loadOp = value.getDefiningOp()) - { - for (auto *user : loadOp.getReference().getUsers()) - { - auto earlier = mlir::dyn_cast(user); - auto *retain = earlier ? soleUser(earlier.getResult()) : nullptr; - if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && - twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && - retain->isBeforeInBlock(store)) - { - return retain; - } - } - } - - mlir::Operation *found = nullptr; - for (auto *user : value.getUsers()) - { - if (!mlir::isa(user) || user->getBlock() != store->getBlock() || - !user->isBeforeInBlock(store) || captureRetains.contains(user)) - { - continue; - } - - if (!found || found->isBeforeInBlock(user)) - { - found = user; - } - } - - return found; - } - - // Where a closure value goes: through its views and the owning `let`s that hold it, each use - // that may run it joins `uses`, and each release `ends`. Answers the first use that takes it - // anywhere else - returned, stored, kept by a callee, captured - or null. Anything this does - // not know is such a use. - mlir::Operation *followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, - llvm::SmallVectorImpl &ends, bool &aliased) - { - llvm::SmallVector values{closure}; - llvm::SmallPtrSet seen; - mlir::Operation *escape = nullptr; - auto followSlot = [&](mlir::Value slot) { - for (auto *slotUser : slot.getUsers()) - { - if (mlir::isa(slotUser)) - { - continue; - } - - if (mlir::isa(slotUser)) - { - ends.push_back(slotUser); - } - else if (auto loadOp = mlir::dyn_cast(slotUser)) - { - values.push_back(loadOp.getResult()); - } - else if (auto storeOp = mlir::dyn_cast(slotUser); !storeOp || storeOp.getReference() != slot) - { - escape = escape ? escape : slotUser; - } - } - }; - - while (!values.empty() && !escape) - { - auto value = values.pop_back_val(); - if (!seen.insert(value).second) - { - continue; - } - - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (escape || mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - ends.push_back(user); - return; - } - - if (isCall(user)) - { - if (isKeptArgument(user, used)) - { - escape = user; - } - else - { - uses.push_back(user); - } - - return; - } - - // the function and the box a call through the closure is made with - if (mlir::isa(user)) - { - for (auto *partUser : user->getResult(0).getUsers()) - { - if (isCall(partUser) && !isKeptArgument(partUser, user->getResult(0))) - { - uses.push_back(partUser); - } - else - { - escape = escape ? escape : partUser; - } - } - - return; - } - - mlir::Value slot; - if (auto varOp = mlir::dyn_cast(user)) - { - slot = varOp.getResult(); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) - { - slot = storeOp.getReference(); - } - - // a local, owning or not (`let f: () => number;` owns nothing): its reads are the - // closure; a function's result slot is a local too, and its read is returned - auto slotVar = slot ? slot.getDefiningOp() : mlir_ts::VariableOp(); - if (slotVar && !slotVar.getCaptured().value_or(false)) - { - aliased = aliased || !isOwningVariable(slotVar); - followSlot(slot); - return; - } - - escape = user; - }); - } - - return escape; - } - - // Does a call given this value give it a closure, which may run and touch what its box holds? - static bool holdsCaptures(mlir::Value value) - { - if (mlir::isa(value.getType())) - { - return true; - } - - auto root = rootOf(value); - if (auto varOp = root.getDefiningOp()) - { - return varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); - } - - // a closure's `this` is its box, a reference; a bound method's is the object - auto getThisOp = root.getDefiningOp(); - return getThisOp && mlir::isa(getThisOp.getType()); - } - - // The verdicts on the closures over cells: an escape is an error; a borrow must not be run after - // anything it borrows is given back - a cell this function owns at its releases, a copy at its - // owner's ends. A cell an enclosing closure holds and a parameter end nowhere here; a copy of a - // read out of a container is bounded by the read's own check, which sees the closure's uses - // (walkBorrowed). - void decideClosures(llvm::SetVector &toErase) - { - for (auto &closure : closures) - { - if (!closure->op) - { - decideEscaping(*closure, toErase); // a generator's box - continue; - } + llvm::SmallVector borrowingFieldsOf(mlir::Type recordType); - auto *boundOp = closure->op.getOperation(); - auto name = ownerName(closure->op.getResult()); - auto ok = true; - // Held by a local that owns nothing: rc's birth reference reads to the move logic as the - // store's, which would leave the box with no owner. The closure keeps its releases, and - // nothing may run it after them. - auto keepAliasedReleases = [&]() { - for (auto *end : closure->ends) - { - if (!mlir::isa(end)) - { - continue; - } + // Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)? + bool isYieldedField(mlir::Value ref); - toErase.remove(end); - for (auto *use : closure->uses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, name); - ok = false; - } - } - } - }; + // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? + bool isBorrowingField(mlir::Value ref); - if (!closure->hasCells && !closure->borrowsCopies) - { - keepAliasedReleases(); - continue; - } + // A store into a borrowing field holds what it stores without owning it, as a borrowing box's + // copy does (OWN_CAPTURE_BORROW_ATTR_NAME), and rc's retain for it goes; its release, when the + // field is overwritten, gives nothing back. + void classifyBorrowingFields(); - if (closure->escape) - { - decideEscaping(*closure, toErase); - continue; - } + // Is `ref` a cell read out of the `.captured` box of a generator's state object that borrows: + // `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`? + bool isBorrowingCell(mlir::Value ref); - for (auto &fill : closure->fills) - { - auto value = fill.second.getValue(); - llvm::SmallVector ends; - llvm::StringRef captured = "this value"; - if (mlir::isa(value.getType())) - { - if (isCellVariable(value)) - { - captured = varName(value.getDefiningOp()); - for (auto *user : value.getUsers()) - { - if (mlir::isa(user) || movesCellAway(user)) - { - ends.push_back(user); - } - } - } - else if (!isLoadedCell(value)) - { - reportCaptureNotOwned(fill.second, captured); - ok = false; - continue; - } - } - else - { - auto root = rootOf(value); - captured = ownerName(root); - Chain chain; - if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0 || isHandle(root)) - { - // nothing to own, a read the read's check bounds, a parameter, or a handle, - // whose copy in the box is counted (spec 23.2) - } - else if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - reportCaptureNotOwned(fill.second, captured); - ok = false; - continue; - } + // Is `ref` a field of the `.captured` box of a generator's state object that borrows: + // `ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i)`? A field copied in by value holds + // the caller's value itself, a cell field the caller's cell. + bool isBorrowingBoxField(mlir::Value ref); - ends = rootEnds(chain); - } + // What a generator reads out of a borrowing cell or field is the caller's: every use of it must + // be a borrow - a store into another borrowing field is one (classifyBorrowingFields). A yield of + // it, a push, a return, a store anywhere else would give the caller's value a second owner, and + // rc retains nothing for some of these, so nothing else would see it. + void checkBorrowedReads(); - for (auto *end : ends) - { - if (captureRetains.contains(end) || captureStores.count(end)) - { - continue; // a box's own copy, this one's or another borrowing closure's - } + // A call whose result borrows from its arguments (OWN_RESULT_BOUNDED_ATTR_NAME): a generator over + // a parameter's block. The result is owned as any fresh value is, but nothing may use it after + // one of those arguments is given back - released, assigned, or dropped from the place it was + // read from - and it may not be kept anywhere that could outlive them. Its releases are not + // uses: the state object's release gives back nothing it borrows. + void checkBoundedResults(); - for (auto *use : closure->uses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, captured); - ok = false; - } - } - } - } + // The ends of a capture box given to a call with a bounded result: each release of a closure on + // it (the box goes with its last), and each end of what a field of it was filled from. Reports + // and returns false when a use of the result comes after one. + bool checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, + llvm::StringRef name); - if (closure->aliased) - { - keepAliasedReleases(); - } + // Every use of a bounded result, through its views, the locals that hold it, and the function + // and object a call through it is made with. Answers the first use that keeps it - anything + // that is not a read, a call, a local, or rc's bookkeeping - or null. + mlir::Operation *boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses); - // a call through the box, after the closure value that owns the box is given back - for (auto *end : closure->ends) - { - for (auto *use : closure->boxUses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, name); - ok = false; - } - } - } + // A generator's maker puts what the generator captures - its parameters, an outer local - into + // a box, and the box into its state object's `.captured` field: the box of a closure that + // escapes, with the state object, at that store. There is no closure value, so no use runs it + // here and nothing calls through it. rc's `ts.RetainCell` of the box is the state object's + // reference, which moves in with it. A box used any other way is left to the RetainCell error. + void classifyStateBoxes(); - toErase.insert(closure->retains.begin(), closure->retains.end()); - } - } + // `ts.Store(box, ts.PropertyRef(state, i))`, where `state` is a local a `ts.Constant` + // initializes whose every read fills a block `ts.New` made: a generator's initial state. + static bool isStoreIntoState(mlir::Operation *user, mlir::Value box); - // Is this the store that moves a cell into the box of a closure that escapes? - bool movesCellAway(mlir::Operation *op) - { - auto storeOp = mlir::dyn_cast(op); - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - if (!propertyRefOp || !mlir::isa(storeOp.getValue().getType())) - { - return false; - } + // rc's reference for the box's copy of `value`: the retain of it nearest before the store that + // fills the box, in the same block. + mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store); - if (stateBoxes.count(propertyRefOp.getObjectRef())) - { - return true; - } + // Where a closure value goes: through its views and the owning `let`s that hold it, each use + // that may run it joins `uses`, and each release `ends`. Answers the first use that takes it + // anywhere else - returned, stored, kept by a callee, captured - or null. Anything this does + // not know is such a use. + mlir::Operation *followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &ends, bool &aliased); - auto boundOp = closureOfBox(propertyRefOp.getObjectRef()); - auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); - return found != closureOf.end() && found->second->escape; - } + // Does a call given this value give it a closure, which may run and touch what its box holds? + static bool holdsCaptures(mlir::Value value); + + // The verdicts on the closures over cells: an escape is an error; a borrow must not be run after + // anything it borrows is given back - a cell this function owns at its releases, a copy at its + // owner's ends. A cell an enclosing closure holds and a parameter end nowhere here; a copy of a + // read out of a container is bounded by the read's own check, which sees the closure's uses + // (walkBorrowed). + void decideClosures(llvm::SetVector &toErase); + + // Is this the store that moves a cell into the box of a closure that escapes? + bool movesCellAway(mlir::Operation *op); // A closure that escapes owns what its box holds (spec 2.5, phase 5b). Its copies moved in as // in phase 4. Each cell moves in where the box takes it: that store is the move, rc's @@ -1261,176 +324,13 @@ class OwnershipInferencePass // assign it, capture it again - and the move may not come round a loop to a cell made outside // it. A cell that borrows its value (a parameter's) or one an enclosing closure holds has // nothing to move. And nothing may call through the box once the closure has gone. - void decideEscaping(Closure &closure, llvm::SetVector &toErase) - { - auto ok = true; - for (auto &fill : closure.fills) - { - auto value = fill.second.getValue(); - mlir::Operation *move = fill.second; - if (!mlir::isa(value.getType())) - { - continue; - } - - if (isLoadedCell(value)) - { - reportEscapingCellNotOwned(move, closure.escape, "it is captured from an enclosing function"); - ok = false; - continue; - } - - auto varOp = isCellVariable(value) ? value.getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp) - { - reportCaptureNotOwned(move, "this value"); - ok = false; - continue; - } - - auto name = varName(varOp); - auto elementType = mlir::cast(value.getType()).getElementType(); - // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) - if (!isOwningVariable(varOp) && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) && - !MLIRTypeHelper::isSharedHandleType(elementType)) - { - reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); - ok = false; - continue; - } - - if (takerLoops(move, varOp)) - { - reportMovedInLoop(move, name); - ok = false; - continue; - } - - llvm::SmallVector moved; - for (auto *user : value.getUsers()) - { - if (user == move || llvm::is_contained(closure.retains, user) || !reachableAfter(move, user, varOp)) - { - continue; - } - - if (!mlir::isa(user)) - { - reportUsedAfterEscapingCapture(user, move, name); - ok = false; - break; - } - - if (!dominance->properlyDominates(move, user)) - { - reportMovedOnSomePaths(move, user, name); - ok = false; - break; - } - - moved.push_back(user); - } - - toErase.insert(moved.begin(), moved.end()); - } - - // A call through the box itself after the closure was given away or given back. followClosure - // stops at the escape, so `ends` may miss releases past it; that is enough while the only - // calls through a box are those of a folded `const f`, whose release ends its block. A - // generator's box has no closure value and no such call. - if (!closure.op) - { - toErase.insert(closure.retains.begin(), closure.retains.end()); - return; - } - - auto name = ownerName(closure.op.getResult()); - llvm::SmallVector gone(closure.ends.begin(), closure.ends.end()); - gone.push_back(closure.escape); - for (auto *end : gone) - { - for (auto *use : closure.boxUses) - { - if (ok && use != end && reachableAfter(end, use, closure.op.getOperation())) - { - reportUseAfterMove(use, end, name); - ok = false; - } - } - } - - toErase.insert(closure.retains.begin(), closure.retains.end()); - } + void decideEscaping(Closure &closure, llvm::SetVector &toErase); // A captured variable whose value this function does not own - a parameter, `this` - has a cell // that borrows the value: the cell's release frees the cell only (OWN_CELL_BORROWS_ATTR_NAME), // and nothing may assign the variable, here or in a closure over it, since the old value is the // caller's. A variable whose value nobody can be said to own is an error. - void decideCells() - { - getFunction().walk([&](mlir_ts::VariableOp varOp) { - auto cell = varOp.getResult(); - if (!isCellVariable(cell) || isOwningVariable(varOp)) - { - return; - } - - auto elementType = mlir::cast(cell.getType()).getElementType(); - if (!ownsHeap(varOp.getLoc(), elementType)) - { - return; - } - - // A handle's cell holds a count of its own, taken where it is made (spec 23.2): its - // release gives that back, as under rc, and an assignment is a counted copy. - if (MLIRTypeHelper::isSharedHandleType(elementType)) - { - return; - } - - auto name = varName(varOp); - auto init = varOp.getInitializer(); - auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); - if (!argument || !argument.getOwner()->isEntryBlock() || - !mlir::isa(argument.getOwner()->getParentOp())) - { - reportCaptureNotOwned(varOp, name); - return; - } - - for (auto *user : cell.getUsers()) - { - if (mlir::isa(user)) - { - setAttrTracked(user, OWN_CELL_BORROWS_ATTR_NAME); - continue; - } - - auto storeOp = mlir::dyn_cast(user); - if (mlir::isa(user) || (storeOp && storeOp.getReference() == cell)) - { - reportCapturedParamAssigned(user, name); - return; - } - - // captured: the closure's body must not assign it either - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); - if (!boundOp) - { - continue; - } - - auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); - if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) - { - reportCapturedParamAssigned(boundOp, name); - return; - } - } - }); - } + void decideCells(); // An argument read out of a local is lent to the call, but the arguments are read in order: a // later one may give the local a new value before the call takes the first - `g(n, (n = mk(), 0))`, @@ -1549,73 +449,17 @@ class OwnershipInferencePass } // Move, else borrow, else error (spec 2.2) for the receivers of one owning local. - void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) - { - auto owner = varName(slot.getDefiningOp()); - if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers)) - { - // a move on some paths only is placed when it is the local's one receiver (spec 30) - for (auto &receiver : receivers) - { - decideSlotReceiver(slot, receiver, owner, toErase, receivers.size() == 1); - } - - return; - } - - decideSlotReceivers(slot, receivers, owner, toErase); - } + void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase); void decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, - llvm::SetVector &toErase, bool sole) - { - if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); })) - { - // rc's retain goes with the move; a call that keeps the argument is the move itself - if (!isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - - return; - } - - if (auto borrower = borrowerOf(receiver.retain)) - { - borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase); - return; - } - - checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); // reports - } + llvm::SetVector &toErase, bool sole); // Receivers of one owning local that each read a value of its own (#528): `h.c = a; a = new C(); // g.c = a`. An assignment comes between every two of their reads on every path from one to the // other, so each is decided alone, as the one receiver of the value it reads. So are receivers // no path joins at all, assigned or not: `if (c) return a; return a;` moves `a` once on each // way out. - bool takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers) - { - auto assignments = slotAssignments(slot); - llvm::SmallVector kills(assignments.begin(), assignments.end()); - if (auto varOp = slot.getDefiningOp()) - { - kills.push_back(varOp); - } - - for (auto &from : receivers) - { - for (auto &to : receivers) - { - if (&from != &to && reachableAfter(from.load, to.load, kills)) - { - return false; - } - } - } - - return true; - } + bool takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers); // ---- A kept parameter released where it is not moved (spec 33) ---- // From 98eb693b85481c5c4d18f7104def5a1333a04540 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:22:16 +0100 Subject: [PATCH 17/25] own passes: OwnershipInferenceKeptParams.cpp out of OwnershipInferencePass.h (I1) A kept parameter released where it is not moved (spec 33). A pure move of parameterSlot, isReleasedParam, readsSlot, releaseKeptParamsAtExits, decideParam, isReadOutOfException, keptParamLoadOf, decideSlotReceivers. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipInferenceKeptParams.cpp | 202 ++++++++++++++++++ .../lib/TypeScript/OwnershipInferencePass.h | 190 +--------------- 3 files changed, 211 insertions(+), 182 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 73a698340..c27472054 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -34,6 +34,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipVerifierPass.cpp OwnershipInferencePass.cpp OwnershipInferenceClosures.cpp + OwnershipInferenceKeptParams.cpp OwnershipSignaturePass.cpp OwnershipSignatureCallbacks.cpp OwnershipSignatureClosedWorld.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp new file mode 100644 index 000000000..2d7c6c060 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp @@ -0,0 +1,202 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): a kept parameter released where it is +// not moved (spec 33). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + mlir_ts::VariableOp OwnershipInferencePass::parameterSlot(int32_t index) + { + auto &body = getFunction().getFunctionBody(); + if (body.empty() || static_cast(index) >= body.front().getNumArguments()) + { + return {}; + } + + for (auto *user : body.front().getArgument(index).getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + if (varOp && isParameterSlot(varOp, position) && position == index) + { + return varOp; + } + } + + return {}; + } + + bool OwnershipInferencePass::isReleasedParam(mlir::Value slot) + { + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + auto varOp = slot.getDefiningOp(); + auto index = -1; + return attr && varOp && isParameterSlot(varOp, index) && llvm::is_contained(attr.asArrayRef(), index) && + llvm::is_contained(ownedParams(f), index); + } + + bool OwnershipInferencePass::readsSlot(mlir::Value value, mlir::Value slot, int depth) + { + auto *def = value.getDefiningOp(); + if (!def) + { + return false; + } + + // too deep to tell: as if it did + if (depth > 16) + { + return true; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && loadOp.getReference() == slot) + { + return true; + } + + return llvm::any_of(def->getOperands(), [&](mlir::Value operand) { return readsSlot(operand, slot, depth + 1); }); + } + + void OwnershipInferencePass::releaseKeptParamsAtExits() + { + exitReleases.clear(); + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + if (!attr) + { + return; + } + + for (auto index : attr.asArrayRef()) + { + auto varOp = parameterSlot(index); + if (!varOp || !llvm::is_contained(ownedParams(f), index)) + { + continue; + } + + for (auto &block : f.getFunctionBody()) + { + auto *exit = block.empty() ? nullptr : &block.back(); + // a thrown value made of the parameter takes it with it: no release there (a leak, + // if it does not) + auto leaves = mlir::isa_and_nonnull(exit) || + (mlir::isa_and_nonnull(exit) && + !readsSlot(exit->getOperand(0), varOp.getResult())); + if (!leaves) + { + continue; + } + + mlir::OpBuilder builder(exit); + exitReleases.push_back(builder.create(exit->getLoc(), varOp.getResult())); + } + } + } + + void OwnershipInferencePass::decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto name = varName(slot.getDefiningOp()); + auto &receiver = receivers.front(); + auto *taker = takerOf(receiver.retain, receiver.value); + if (receivers.size() > 1) + { + auto &second = receivers[1]; + auto *secondTaker = takerOf(second.retain, second.value); + reportUseAfterMove(secondTaker ? secondTaker : second.retain, taker ? taker : receiver.retain, name); + return; + } + + // one the body releases on the paths that do not move it (spec 33): the move's own check + // erases the releases at the exits it reaches, and places one on each edge into where the + // paths join that did not move it (spec 30) + auto released = isReleasedParam(slot); + if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, released)) + { + return; + } + + if (released) + { + if (!isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + + return; + } + + auto &body = getFunction().getFunctionBody(); + auto *moved = body.findAncestorOpInRegion(*taker); + auto everyPath = true; + getFunction().walk([&](mlir_ts::ReturnInternalOp returnOp) { + auto *exit = body.findAncestorOpInRegion(*returnOp); + if (everyPath && (!moved || !exit || !dominance->dominates(moved, exit))) + { + reportKeptOnSomePaths(taker, returnOp, name); + everyPath = false; + } + }); + + // rc's retain goes with the move - a `ts.RetainSlot` too, for an owning local the parameter + // moves into (`let r = init`, spec 33); a call that keeps it is the move itself + if (everyPath && !isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + } + + bool OwnershipInferencePass::isReadOutOfException(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); + return argument && argument.getOwner()->isEntryBlock() && getFunction().getName().starts_with(".eh.copy."); + } + + mlir_ts::LoadOp OwnershipInferencePass::keptParamLoadOf(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index) || !llvm::is_contained(ownedParams(getFunction()), index)) + { + return {}; + } + + return loadOp; + } + + void OwnershipInferencePass::decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto ends = slotEnds(slot); + mlir_ts::VariableOp someBorrower; + for (auto &receiver : receivers) + { + if (auto borrower = borrowerOf(receiver.retain)) + { + someBorrower = borrower; + borrowWith(borrower, receiver.retain, ends, owner, toErase); + } + } + + for (auto &receiver : receivers) + { + if (borrowerOf(receiver.retain)) + { + continue; + } + + llvm::SetVector unused; + if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) + { + reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); + } + } + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 0967a5c74..8f50e55a1 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -477,210 +477,36 @@ class OwnershipInferencePass llvm::SmallVector exitReleases; // The slot of parameter `index` (isParameterSlot), or null. - mlir_ts::VariableOp parameterSlot(int32_t index) - { - auto &body = getFunction().getFunctionBody(); - if (body.empty() || static_cast(index) >= body.front().getNumArguments()) - { - return {}; - } - - for (auto *user : body.front().getArgument(index).getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - if (varOp && isParameterSlot(varOp, position) && position == index) - { - return varOp; - } - } - - return {}; - } + mlir_ts::VariableOp parameterSlot(int32_t index); // Is `slot` a parameter this function keeps and releases where it does not move it? - bool isReleasedParam(mlir::Value slot) - { - auto f = getFunction(); - auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); - auto varOp = slot.getDefiningOp(); - auto index = -1; - return attr && varOp && isParameterSlot(varOp, index) && llvm::is_contained(attr.asArrayRef(), index) && - llvm::is_contained(ownedParams(f), index); - } + bool isReleasedParam(mlir::Value slot); // Does `value` come from a read of `slot`: computed from one, through any op? - static bool readsSlot(mlir::Value value, mlir::Value slot, int depth = 0) - { - auto *def = value.getDefiningOp(); - if (!def) - { - return false; - } + static bool readsSlot(mlir::Value value, mlir::Value slot, int depth = 0); - // too deep to tell: as if it did - if (depth > 16) - { - return true; - } - - if (auto loadOp = mlir::dyn_cast(def); loadOp && loadOp.getReference() == slot) - { - return true; - } - - return llvm::any_of(def->getOperands(), [&](mlir::Value operand) { return readsSlot(operand, slot, depth + 1); }); - } - - void releaseKeptParamsAtExits() - { - exitReleases.clear(); - auto f = getFunction(); - auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); - if (!attr) - { - return; - } - - for (auto index : attr.asArrayRef()) - { - auto varOp = parameterSlot(index); - if (!varOp || !llvm::is_contained(ownedParams(f), index)) - { - continue; - } - - for (auto &block : f.getFunctionBody()) - { - auto *exit = block.empty() ? nullptr : &block.back(); - // a thrown value made of the parameter takes it with it: no release there (a leak, - // if it does not) - auto leaves = mlir::isa_and_nonnull(exit) || - (mlir::isa_and_nonnull(exit) && - !readsSlot(exit->getOperand(0), varOp.getResult())); - if (!leaves) - { - continue; - } - - mlir::OpBuilder builder(exit); - exitReleases.push_back(builder.create(exit->getLoc(), varOp.getResult())); - } - } - } + void releaseKeptParamsAtExits(); // A parameter this function keeps (`__own_params`): its caller moved the argument in, and this // function has no release for it, so exactly one taker must move it on, on every way out of // the function, and nothing may read it after that. - void decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) - { - auto name = varName(slot.getDefiningOp()); - auto &receiver = receivers.front(); - auto *taker = takerOf(receiver.retain, receiver.value); - if (receivers.size() > 1) - { - auto &second = receivers[1]; - auto *secondTaker = takerOf(second.retain, second.value); - reportUseAfterMove(secondTaker ? secondTaker : second.retain, taker ? taker : receiver.retain, name); - return; - } - - // one the body releases on the paths that do not move it (spec 33): the move's own check - // erases the releases at the exits it reaches, and places one on each edge into where the - // paths join that did not move it (spec 30) - auto released = isReleasedParam(slot); - if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, released)) - { - return; - } - - if (released) - { - if (!isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - - return; - } - - auto &body = getFunction().getFunctionBody(); - auto *moved = body.findAncestorOpInRegion(*taker); - auto everyPath = true; - getFunction().walk([&](mlir_ts::ReturnInternalOp returnOp) { - auto *exit = body.findAncestorOpInRegion(*returnOp); - if (everyPath && (!moved || !exit || !dominance->dominates(moved, exit))) - { - reportKeptOnSomePaths(taker, returnOp, name); - everyPath = false; - } - }); - - // rc's retain goes with the move - a `ts.RetainSlot` too, for an owning local the parameter - // moves into (`let r = init`, spec 33); a call that keeps it is the move itself - if (everyPath && !isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - } + void decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase); // The thrown value, read out of the exception object by a catch's copy thunk (`.eh.copy.*`, // which the C++ runtime calls to copy the exception into the catch). rc retains it for the // copy; own moves it: the exception object never gives its reference back (a throw hands the // thrower's reference to it, and nothing releases it - rc leaks it the same way), and each // exception is copied into one catch, since a TypeScript rethrow is a new throw. - bool isReadOutOfException(mlir::Value value) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); - return argument && argument.getOwner()->isEntryBlock() && getFunction().getName().starts_with(".eh.copy."); - } + bool isReadOutOfException(mlir::Value value); // The read of a parameter this function keeps, through its views. None for anything else. - mlir_ts::LoadOp keptParamLoadOf(mlir::Value value) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index) || !llvm::is_contained(ownedParams(getFunction()), index)) - { - return {}; - } - - return loadOp; - } + mlir_ts::LoadOp keptParamLoadOf(mlir::Value value); // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, so // none may move it. A receiver that is not a `let` reports its move error - another // receiver's reads come after it - or, if its move alone would pass, that a borrow pins it. void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, - llvm::SetVector &toErase) - { - auto ends = slotEnds(slot); - mlir_ts::VariableOp someBorrower; - for (auto &receiver : receivers) - { - if (auto borrower = borrowerOf(receiver.retain)) - { - someBorrower = borrower; - borrowWith(borrower, receiver.retain, ends, owner, toErase); - } - } - - for (auto &receiver : receivers) - { - if (borrowerOf(receiver.retain)) - { - continue; - } - - llvm::SetVector unused; - if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) - { - reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); - } - } - } + llvm::SetVector &toErase); // A `let` that could borrow: owning, declared from a value, holding a reference it took // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. With From 1a375e9cbf3be4ffb21a5d88f565bad1d94515e4 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:23:22 +0100 Subject: [PATCH 18/25] own passes: OwnershipInferenceBorrowers.cpp out of OwnershipInferencePass.h (I1) Locals that borrow - the borrower of a value, and every use of the borrow up to where the owner ends; a local lent to a call and given a new value before the call takes it. A pure move of borrowerOf, borrowWith, consumingLetOf, slotEnds, tryBorrow, collectBorrowerUses, isAssignmentOf, passesOn, walkBorrowed, callResultIsOwn, isCalleeOperand, checkArgumentsAssigned, closureAssigns. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipInferenceBorrowers.cpp | 483 ++++++++++++++++++ .../lib/TypeScript/OwnershipInferencePass.h | 463 +---------------- 3 files changed, 497 insertions(+), 450 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index c27472054..65768d62e 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -33,6 +33,7 @@ add_mlir_dialect_library(MLIRTypeScript RelocateConstantPass.cpp OwnershipVerifierPass.cpp OwnershipInferencePass.cpp + OwnershipInferenceBorrowers.cpp OwnershipInferenceClosures.cpp OwnershipInferenceKeptParams.cpp OwnershipSignaturePass.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp new file mode 100644 index 000000000..e93ff2f03 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -0,0 +1,483 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): locals that borrow - the borrower of a +// value, and every use of the borrow up to where the owner ends; a local lent to a call and +// given a new value before the call takes it. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + void OwnershipInferencePass::checkArgumentsAssigned() + { + getFunction().walk([&](mlir::Operation *call) { + if (!isCall(call)) + { + return mlir::WalkResult::advance(); + } + + for (auto argument : callArgs(call)) + { + // a method's object: `this` of the method bound to it + if (auto getThisOp = argument.getDefiningOp()) + { + auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); + if (mlir::isa_and_nonnull(boundOp)) + { + argument = boundOp->getOperand(0); + } + } + + auto *read = argument.getDefiningOp(); + auto local = mlir::dyn_cast_or_null(slotOf(argument)); + if (!read || !local || !ownsHeap(argument) || isHandle(argument)) + { + continue; + } + + auto assignedByClosure = closureAssigns(local); + auto *write = findBetween(read, call, [&](mlir::Operation *op) { + if (assignedByClosure && isCall(op)) + { + return true; + } + + auto storeOp = mlir::dyn_cast(op); + auto releaseSlotOp = mlir::dyn_cast(op); + return (storeOp && storeOp.getReference() == local.getResult()) || + (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); + }); + if (!write) + { + continue; + } + + if (reporting()) + { + auto name = varName(local); + auto callee = calleeName(call); + auto diag = call->emitError("'") + << name << "' is lent to '" << (callee.empty() ? "a function" : callee) + << "', but is given a new value before the call takes it; -mm=own cannot prove " + "it outlives the call"; + diag.attachNote(write->getLoc()) << "'" << name << "' is given a new value here"; + signalPassFailure(); + } + + return mlir::WalkResult::interrupt(); + } + + return mlir::WalkResult::advance(); + }); + } + + bool OwnershipInferencePass::closureAssigns(mlir_ts::VariableOp local) + { + if (!local.getCaptured().value_or(false)) + { + return false; + } + + for (auto *user : local->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (mlir::isa(user) || + (storeOp && storeOp.getReference() == local.getResult())) + { + continue; + } + + // the cell put into a closure's capture box + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp) + { + return true; + } + + auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); + if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) + { + return true; + } + } + + return false; + } + + mlir_ts::VariableOp OwnershipInferencePass::borrowerOf(mlir::Operation *acquisition, bool consumed) + { + mlir_ts::VariableOp varOp; + if (auto retainSlotOp = mlir::dyn_cast(acquisition)) + { + varOp = retainSlotOp.getSlot().getDefiningOp(); + } + else if (auto retainOp = mlir::dyn_cast(acquisition)) + { + // rc's reference for a `let` that takes the value over: `let ri = raw` retains the + // interface and declares `ri` consuming it + varOp = consumingLetOf(retainOp.getReference()); + consumed = true; + } + else + { + varOp = mlir::dyn_cast(acquisition); + } + + if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return {}; + } + + if (varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return consumed ? varOp : mlir_ts::VariableOp(); + } + + auto retains = llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + return retains ? varOp : mlir_ts::VariableOp(); + } + + bool OwnershipInferencePass::borrowWith(mlir_ts::VariableOp borrower, mlir::Operation *acquisition, llvm::ArrayRef ends, + llvm::StringRef owner, llvm::SetVector &toErase) + { + if (!tryBorrow(borrower, ends, owner, toErase)) + { + return false; + } + + if (mlir::isa(acquisition)) + { + toErase.insert(acquisition); + } + + return true; + } + + mlir_ts::VariableOp OwnershipInferencePass::consumingLetOf(mlir::Value value) + { + for (auto *user : value.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + if (varOp && varOp.getInitializer() == value && varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return varOp; + } + } + + return {}; + } + + llvm::SmallVector OwnershipInferencePass::slotEnds(mlir::Value slot) + { + llvm::SmallVector ends; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + + return ends; + } + + bool OwnershipInferencePass::tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto name = varName(borrower); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + mlir::Operation *declaration = borrower; + if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) + { + if (isAssignmentOf(kept, borrower)) + { + reportBorrowerAssigned(kept, name, owner); + } + else + { + reportBorrowEscapes(kept, name, owner); + } + + return false; + } + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, name, owner); + return false; + } + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + mlir::Operation *OwnershipInferencePass::collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, + llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &bookkeeping) + { + for (auto *user : borrower.getResult().getUsers()) + { + if (mlir::isa(user)) + { + bookkeeping.push_back(user); + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + return user; + } + + uses.push_back({user, {kills.begin(), kills.end()}}); + if (auto *kept = walkBorrowed(readOp.getResult(), kills, uses)) + { + return kept; + } + } + + return nullptr; + } + + bool OwnershipInferencePass::isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp) + { + auto storeOp = mlir::dyn_cast(op); + return storeOp && storeOp.getReference() == varOp.getResult(); + } + + bool OwnershipInferencePass::passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, + llvm::SmallVectorImpl *renewedAt) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || isView(user)) + { + values.push_back(user->getResult(0)); + return true; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + values.push_back(*argument); + if (renewedAt) + { + renewedAt->push_back(&argument->getOwner()->front()); + } + + return true; + } + + return false; + } + + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == use.get()) + { + if (auto varOp = storeOp.getReference().getDefiningOp(); + varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) + { + if (renewedAt && varOp.getInitializer()) + { + renewedAt->push_back(varOp); + } + + for (auto *varUser : varOp.getResult().getUsers()) + { + if (auto loadOp = mlir::dyn_cast(varUser)) + { + values.push_back(loadOp.getResult()); + } + else if (renewedAt && isAssignmentOf(varUser, varOp)) + { + renewedAt->push_back(varUser); + } + } + + return true; + } + } + + return false; + } + + mlir::Operation *OwnershipInferencePass::walkBorrowed( + mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, + llvm::function_ref)> isBorrower, + Keeping keeping, bool countedHandle, bool startBorrowed) + { + struct Pending + { + mlir::Value value; + bool borrowed; + llvm::SmallVector kills; + }; + + llvm::SmallVector values; + // `startBorrowed` false: `start` is what a read produced (a field's reference), whose uses + // are uses of the borrow but not the borrowed value itself + values.push_back({start, startBorrowed, {kills.begin(), kills.end()}}); + llvm::SmallPtrSet seen{start}; + while (!values.empty()) + { + auto pending = values.pop_back_val(); + for (auto &use : pending.value.getUses()) + { + auto *user = use.getOwner(); + if (pending.borrowed && keeping.bookkeeping && + ((mlir::isa(user) && borrowingCall(rootOf(pending.value))) || + (keeping.returns && mlir::isa(user)))) + { + keeping.bookkeeping->push_back(user); + continue; + } + + // rc's reference for a borrowing box's copy goes with the closure (decideClosures), and + // one for a borrowing field's with the field (classifyBorrowingFields) + if (captureRetains.contains(user) || fieldBorrowRetains.contains(user)) + { + continue; + } + + uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); + // copied into a borrowing box: whatever runs the closure reads it + if (auto found = captureStores.find(user); found != captureStores.end()) + { + for (auto *closureUse : found->second->uses) + { + uses.push_back({closureUse, pending.kills}); + } + } + + // stored into a field a borrowing record borrows (spec 32): whatever uses the record + // reads it + if (auto found = recordStoreUses.find(user); found != recordStoreUses.end()) + { + for (auto &recordUse : found->second->walk.uses) + { + uses.push_back({recordUse.op, pending.kills, recordUse.callee}); + } + } + + if (pending.borrowed && keeping.returns && mlir::isa(user)) + { + continue; + } + + if (pending.borrowed) + { + // a handle read that rc counts (spec 23.3): a return or an `any` box rc + // retained for is a counted copy of its own; passesOn would hand them on + if (countedHandle && isCountedHandOff(user, pending.value)) + { + continue; + } + + llvm::SmallVector same; + llvm::SmallVector renewedAt; + if (passesOn(use, same, &renewedAt)) + { + for (auto value : same) + { + if (mayPointInto(value) && seen.insert(value).second) + { + values.push_back({value, true, renewedAt.empty() ? pending.kills : renewedAt}); + } + } + + continue; + } + + // a handle read that rc counts (spec 23.3): its retain, and what takes the value + // it retains for, are a counted copy of their own, as a string copy is; the + // read's other uses are the borrow's (Ruling 7) + if (countedHandle && isCountedHandleReceiver(user, pending.value)) + { + continue; + } + + if (isBorrower && isBorrower(user, pending.kills)) + { + continue; + } + + if (mlir::isa(user) || !isBorrow(user, pending.value)) + { + return user; + } + } + + // a copy (spec 22.2) reads the borrow into a block of its own, which points into + // nothing, and so does a concatenation: `s + t` is a new string whatever s and t are + // (a `+` of two strings lowers to ts.StringConcat, which always allocates) + if (mlir::isa(user) || + (mlir::isa(user) && mlir::isa(user->getResult(0).getType()))) + { + continue; + } + + // a call whose result is the caller's own (callResultIsOwn) ends the walk at the call + if (callResultIsOwn(user)) + { + continue; + } + + // A result that borrows what it was given borrows this too, and is walked as it. + for (auto result : user->getResults()) + { + if (mayPointInto(result) && seen.insert(result).second) + { + values.push_back({result, !!borrowingCall(result), pending.kills}); + } + } + } + } + + return nullptr; + } + + bool OwnershipInferencePass::callResultIsOwn(mlir::Operation *call) + { + if (!isCall(call) || call->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) + { + return false; + } + + return call->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) || + (call->getNumResults() == 1 && !isRecordResult(call) && isFresh(call->getResult(0))); + } + + bool OwnershipInferencePass::isCalleeOperand(mlir::OpOperand &use) + { + auto *user = use.getOwner(); + auto callee = isCall(user) ? calleeValue(user) : mlir::Value(); + if (!callee) + { + return false; + } + + if (use.getOperandNumber() == 0) + { + return true; + } + + auto getThisOp = use.get().getDefiningOp(); + auto getMethodOp = callee.getDefiningOp(); + auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); + return use.getOperandNumber() == 1 && getMethodOp && methodField && + getThisOp.getOperand() == getMethodOp.getBoundFunc() && + mlir::isa(methodField.getReference().getType()); + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 8f50e55a1..0ed018573 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -339,106 +339,11 @@ class OwnershipInferencePass // argument across the call instead (OwnedReturnConsumptionPass, holdGlobalArguments). A read of a // global is checkGlobalRead's, and a store into a place in the local the place borrows' (a field // read `g(n.v, (n.v = mk(), 0))`); a store into the local's slot ends `n.v` too. - void checkArgumentsAssigned() - { - getFunction().walk([&](mlir::Operation *call) { - if (!isCall(call)) - { - return mlir::WalkResult::advance(); - } - - for (auto argument : callArgs(call)) - { - // a method's object: `this` of the method bound to it - if (auto getThisOp = argument.getDefiningOp()) - { - auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); - if (mlir::isa_and_nonnull(boundOp)) - { - argument = boundOp->getOperand(0); - } - } - - auto *read = argument.getDefiningOp(); - auto local = mlir::dyn_cast_or_null(slotOf(argument)); - if (!read || !local || !ownsHeap(argument) || isHandle(argument)) - { - continue; - } - - auto assignedByClosure = closureAssigns(local); - auto *write = findBetween(read, call, [&](mlir::Operation *op) { - if (assignedByClosure && isCall(op)) - { - return true; - } - - auto storeOp = mlir::dyn_cast(op); - auto releaseSlotOp = mlir::dyn_cast(op); - return (storeOp && storeOp.getReference() == local.getResult()) || - (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); - }); - if (!write) - { - continue; - } - - if (reporting()) - { - auto name = varName(local); - auto callee = calleeName(call); - auto diag = call->emitError("'") - << name << "' is lent to '" << (callee.empty() ? "a function" : callee) - << "', but is given a new value before the call takes it; -mm=own cannot prove " - "it outlives the call"; - diag.attachNote(write->getLoc()) << "'" << name << "' is given a new value here"; - signalPassFailure(); - } - - return mlir::WalkResult::interrupt(); - } - - return mlir::WalkResult::advance(); - }); - } + void checkArgumentsAssigned(); // Does a closure built over this captured local assign it (OWN_ASSIGNS_CAPTURES_ATTR_NAME)? Yes too // when its cell goes where this does not follow. - bool closureAssigns(mlir_ts::VariableOp local) - { - if (!local.getCaptured().value_or(false)) - { - return false; - } - - for (auto *user : local->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (mlir::isa(user) || - (storeOp && storeOp.getReference() == local.getResult())) - { - continue; - } - - // the cell put into a closure's capture box - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); - if (!boundOp) - { - return true; - } - - auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); - if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) - { - return true; - } - } - - return false; - } + bool closureAssigns(mlir_ts::VariableOp local); template bool quietly(F check) { @@ -512,89 +417,20 @@ class OwnershipInferencePass // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. With // `consumed`, also one that took over the reference rc thought the value carried: a call's // result that turned out to borrow an argument carries none. - static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition, bool consumed = false) - { - mlir_ts::VariableOp varOp; - if (auto retainSlotOp = mlir::dyn_cast(acquisition)) - { - varOp = retainSlotOp.getSlot().getDefiningOp(); - } - else if (auto retainOp = mlir::dyn_cast(acquisition)) - { - // rc's reference for a `let` that takes the value over: `let ri = raw` retains the - // interface and declares `ri` consuming it - varOp = consumingLetOf(retainOp.getReference()); - consumed = true; - } - else - { - varOp = mlir::dyn_cast(acquisition); - } - - if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - return {}; - } - - if (varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) - { - return consumed ? varOp : mlir_ts::VariableOp(); - } - - auto retains = llvm::any_of(varOp.getResult().getUsers(), - [](mlir::Operation *user) { return mlir::isa(user); }); - return retains ? varOp : mlir_ts::VariableOp(); - } + static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition, bool consumed = false); // tryBorrow for the `let` an acquisition gives a reference: a `ts.Retain` of the value a // consuming `let` takes over is that `let`'s, and goes with the borrow; a `ts.RetainSlot` is the // `let`'s own bookkeeping already. bool borrowWith(mlir_ts::VariableOp borrower, mlir::Operation *acquisition, llvm::ArrayRef ends, - llvm::StringRef owner, llvm::SetVector &toErase) - { - if (!tryBorrow(borrower, ends, owner, toErase)) - { - return false; - } - - if (mlir::isa(acquisition)) - { - toErase.insert(acquisition); - } - - return true; - } + llvm::StringRef owner, llvm::SetVector &toErase); // The owning `let` declared from `value` that takes over its reference, or none. - static mlir_ts::VariableOp consumingLetOf(mlir::Value value) - { - for (auto *user : value.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - if (varOp && varOp.getInitializer() == value && varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) - { - return varOp; - } - } - - return {}; - } + static mlir_ts::VariableOp consumingLetOf(mlir::Value value); // Where an owning local stops holding its value: each release of its slot and each store // into it. - static llvm::SmallVector slotEnds(mlir::Value slot) - { - llvm::SmallVector ends; - for (auto *user : slot.getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - } - - return ends; - } + static llvm::SmallVector slotEnds(mlir::Value slot); // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its // retain and every release of its slot go, and the owner keeps its own. Sound while no use of @@ -603,41 +439,7 @@ class OwnershipInferencePass // through the borrower's own declaration is a new borrow, not this one. Reports and returns // false otherwise. bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, - llvm::SetVector &toErase) - { - auto name = varName(borrower); - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - mlir::Operation *declaration = borrower; - if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) - { - if (isAssignmentOf(kept, borrower)) - { - reportBorrowerAssigned(kept, name, owner); - } - else - { - reportBorrowEscapes(kept, name, owner); - } - - return false; - } - - for (auto *end : ends) - { - for (auto &use : uses) - { - if (reachableAfter(end, use.op, use.kills)) - { - reportBorrowOutlives(use.op, end, name, owner); - return false; - } - } - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - return true; - } + llvm::SetVector &toErase); // A use of a borrowed value, and where the value it sees is made again: a path from a release // to the use through one of `kills` reaches a new value, not this one. For the borrow's own @@ -658,37 +460,9 @@ class OwnershipInferencePass // assignment, or anything else that is not a read - or null. mlir::Operation *collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, - llvm::SmallVectorImpl &bookkeeping) - { - for (auto *user : borrower.getResult().getUsers()) - { - if (mlir::isa(user)) - { - bookkeeping.push_back(user); - continue; - } + llvm::SmallVectorImpl &bookkeeping); - auto readOp = mlir::dyn_cast(user); - if (!readOp) - { - return user; - } - - uses.push_back({user, {kills.begin(), kills.end()}}); - if (auto *kept = walkBorrowed(readOp.getResult(), kills, uses)) - { - return kept; - } - } - - return nullptr; - } - - static bool isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp) - { - auto storeOp = mlir::dyn_cast(op); - return storeOp && storeOp.getReference() == varOp.getResult(); - } + static bool isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp); // What `use` hands the very value it uses on to, as that value: a cast or a view of it, the // argument of the block a branch passes it to (a merge: `a ?? b`), or the reads of a local @@ -698,59 +472,7 @@ class OwnershipInferencePass // the block's entry, the local's declaration (when it has an initializer: `let l!: V` stores // nothing, and in a loop the slot keeps the last round's value) and assignments. static bool passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, - llvm::SmallVectorImpl *renewedAt = nullptr) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || isView(user)) - { - values.push_back(user->getResult(0)); - return true; - } - - if (auto branchOp = mlir::dyn_cast(user)) - { - if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) - { - values.push_back(*argument); - if (renewedAt) - { - renewedAt->push_back(&argument->getOwner()->front()); - } - - return true; - } - - return false; - } - - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == use.get()) - { - if (auto varOp = storeOp.getReference().getDefiningOp(); - varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) - { - if (renewedAt && varOp.getInitializer()) - { - renewedAt->push_back(varOp); - } - - for (auto *varUser : varOp.getResult().getUsers()) - { - if (auto loadOp = mlir::dyn_cast(varUser)) - { - values.push_back(loadOp.getResult()); - } - else if (renewedAt && isAssignmentOf(varUser, varOp)) - { - renewedAt->push_back(varUser); - } - } - - return true; - } - } - - return false; - } + llvm::SmallVectorImpl *renewedAt = nullptr); // Where a borrowed value goes. Every use of it, and of what it produces that may still point // into the block, joins `uses`, made again at `kills`. The value itself - through what passesOn @@ -779,137 +501,7 @@ class OwnershipInferencePass mlir::Operation *walkBorrowed( mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, llvm::function_ref)> isBorrower = nullptr, - Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false, bool startBorrowed = true) - { - struct Pending - { - mlir::Value value; - bool borrowed; - llvm::SmallVector kills; - }; - - llvm::SmallVector values; - // `startBorrowed` false: `start` is what a read produced (a field's reference), whose uses - // are uses of the borrow but not the borrowed value itself - values.push_back({start, startBorrowed, {kills.begin(), kills.end()}}); - llvm::SmallPtrSet seen{start}; - while (!values.empty()) - { - auto pending = values.pop_back_val(); - for (auto &use : pending.value.getUses()) - { - auto *user = use.getOwner(); - if (pending.borrowed && keeping.bookkeeping && - ((mlir::isa(user) && borrowingCall(rootOf(pending.value))) || - (keeping.returns && mlir::isa(user)))) - { - keeping.bookkeeping->push_back(user); - continue; - } - - // rc's reference for a borrowing box's copy goes with the closure (decideClosures), and - // one for a borrowing field's with the field (classifyBorrowingFields) - if (captureRetains.contains(user) || fieldBorrowRetains.contains(user)) - { - continue; - } - - uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); - // copied into a borrowing box: whatever runs the closure reads it - if (auto found = captureStores.find(user); found != captureStores.end()) - { - for (auto *closureUse : found->second->uses) - { - uses.push_back({closureUse, pending.kills}); - } - } - - // stored into a field a borrowing record borrows (spec 32): whatever uses the record - // reads it - if (auto found = recordStoreUses.find(user); found != recordStoreUses.end()) - { - for (auto &recordUse : found->second->walk.uses) - { - uses.push_back({recordUse.op, pending.kills, recordUse.callee}); - } - } - - if (pending.borrowed && keeping.returns && mlir::isa(user)) - { - continue; - } - - if (pending.borrowed) - { - // a handle read that rc counts (spec 23.3): a return or an `any` box rc - // retained for is a counted copy of its own; passesOn would hand them on - if (countedHandle && isCountedHandOff(user, pending.value)) - { - continue; - } - - llvm::SmallVector same; - llvm::SmallVector renewedAt; - if (passesOn(use, same, &renewedAt)) - { - for (auto value : same) - { - if (mayPointInto(value) && seen.insert(value).second) - { - values.push_back({value, true, renewedAt.empty() ? pending.kills : renewedAt}); - } - } - - continue; - } - - // a handle read that rc counts (spec 23.3): its retain, and what takes the value - // it retains for, are a counted copy of their own, as a string copy is; the - // read's other uses are the borrow's (Ruling 7) - if (countedHandle && isCountedHandleReceiver(user, pending.value)) - { - continue; - } - - if (isBorrower && isBorrower(user, pending.kills)) - { - continue; - } - - if (mlir::isa(user) || !isBorrow(user, pending.value)) - { - return user; - } - } - - // a copy (spec 22.2) reads the borrow into a block of its own, which points into - // nothing, and so does a concatenation: `s + t` is a new string whatever s and t are - // (a `+` of two strings lowers to ts.StringConcat, which always allocates) - if (mlir::isa(user) || - (mlir::isa(user) && mlir::isa(user->getResult(0).getType()))) - { - continue; - } - - // a call whose result is the caller's own (callResultIsOwn) ends the walk at the call - if (callResultIsOwn(user)) - { - continue; - } - - // A result that borrows what it was given borrows this too, and is walked as it. - for (auto result : user->getResults()) - { - if (mayPointInto(result) && seen.insert(result).second) - { - values.push_back({result, !!borrowingCall(result), pending.kills}); - } - } - } - } - - return nullptr; - } + Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false, bool startBorrowed = true); // Whether a call hands back a block of the caller's own, which points into nothing it was given: // walkBorrowed then ends a borrow's walk at the call (the call itself is still a use for as long @@ -924,42 +516,13 @@ class OwnershipInferencePass // that borrows its arguments - and a body whose callers cannot all know such a fact is an // error. `getPoint(x, scene.camera)` makes a new vector: a call after it that may destroy // `scene.camera` does not end that vector (spec 34). - static bool callResultIsOwn(mlir::Operation *call) - { - if (!isCall(call) || call->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) - { - return false; - } - - return call->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) || - (call->getNumResults() == 1 && !isRecordResult(call) && isFresh(call->getResult(0))); - } + static bool callResultIsOwn(mlir::Operation *call); // Is this what an indirect call goes through: its callee value, or, for a method read out of // a field of an object (`o.m()`, `m` a field holding a function), the object it is bound to, // which is the container of what was read, not what was borrowed. A method of a class bound // to a borrowed instance (`h.c.m()`) is not one: its `this` is the borrow. - static bool isCalleeOperand(mlir::OpOperand &use) - { - auto *user = use.getOwner(); - auto callee = isCall(user) ? calleeValue(user) : mlir::Value(); - if (!callee) - { - return false; - } - - if (use.getOperandNumber() == 0) - { - return true; - } - - auto getThisOp = use.get().getDefiningOp(); - auto getMethodOp = callee.getDefiningOp(); - auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); - return use.getOperandNumber() == 1 && getMethodOp && methodField && - getThisOp.getOperand() == getMethodOp.getBoundFunc() && - mlir::isa(methodField.getReference().getType()); - } + static bool isCalleeOperand(mlir::OpOperand &use); // ---- Borrowing records (spec 32) ---- // From 67ab92fb5f605ae978a5ca23d5f174fddb1092f7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:24:35 +0100 Subject: [PATCH 19/25] own passes: OwnershipInferenceRecords.cpp out of OwnershipInferencePass.h (I1) Borrowing records (spec 32). A pure move of mayPointInto, borrowingRecordOf, classifyRecords, walkRecord, recordName, describeRecordSource, checkBorrowingRecords, checkRecordResults. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../lib/TypeScript/OwnershipInferencePass.h | 469 +---------------- .../TypeScript/OwnershipInferenceRecords.cpp | 479 ++++++++++++++++++ 3 files changed, 488 insertions(+), 461 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceRecords.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 65768d62e..0bc6e7ec9 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -36,6 +36,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferenceBorrowers.cpp OwnershipInferenceClosures.cpp OwnershipInferenceKeptParams.cpp + OwnershipInferenceRecords.cpp OwnershipSignaturePass.cpp OwnershipSignatureCallbacks.cpp OwnershipSignatureClosedWorld.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 0ed018573..dffa4e3d8 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -566,107 +566,17 @@ class OwnershipInferencePass // Can a value of this type point into a heap block - a reference into one, a block of its own, // or a method bound to one? A number or a boolean read out of a borrowed block cannot. - bool mayPointInto(mlir::Value value) - { - auto type = value.getType(); - return mlir::isa(type) || - ownsHeap(value); - } + bool mayPointInto(mlir::Value value); // The borrowing record built in `varOp` (classifyRecords), or null. - BorrowingRecord *borrowingRecordOf(mlir_ts::VariableOp varOp) - { - for (auto &record : records) - { - if (record->local == varOp) - { - return record.get(); - } - } - - return nullptr; - } + BorrowingRecord *borrowingRecordOf(mlir_ts::VariableOp varOp); // The boxes the signature pass marked as borrowing records, where every field still holds what it // was marked for: a borrowed field a value this pass bounds (recordFieldSource), any other one a // fresh value or nothing. A store into a borrowed field takes nothing, and whatever uses the record // reads what it stored (walkBorrowed, through recordStoreUses). A box that is not what it was // marked for is left to the record's retain, which is then an error. - void classifyRecords() - { - getFunction().walk([&](mlir_ts::NewOp newOp) { - auto fields = recordBorrowedFields(newOp); - auto varOp = fields.empty() ? mlir_ts::VariableOp() : recordLocalOf(newOp.getResult()); - if (!varOp) - { - return; - } - - auto record = std::make_shared(); - record->box = newOp; - record->local = varOp; - for (auto *user : varOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - auto borrows = llvm::is_contained(fields, static_cast(propertyRefOp.getPosition())); - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - continue; - } - - auto source = recordFieldSource(storeOp.getValue()); - if (source == RecordFieldSource::NoBlock) - { - continue; - } - - if (source == RecordFieldSource::Unknown || borrows != (source != RecordFieldSource::Fresh)) - { - return; - } - - if (borrows) - { - record->borrowed.push_back({storeOp, source}); - } - } - } - - for (auto *user : newOp->getUsers()) - { - if (auto castOp = mlir::dyn_cast(user)) - { - for (auto *castUser : castOp->getUsers()) - { - if (auto newInterfaceOp = mlir::dyn_cast(castUser)) - { - record->interfaces.push_back(newInterfaceOp.getResult()); - } - } - } - } - - // a path back to a use through the record's making is another record - llvm::SmallVector kills{newOp.getOperation(), varOp.getOperation()}; - walkRecord(record->interfaces, kills, record->walk); - for (auto &field : record->borrowed) - { - setAttrTracked(field.store, OWN_CAPTURE_BORROW_ATTR_NAME); - recordStoreUses[field.store.getOperation()] = record.get(); - } - - records.push_back(std::move(record)); - }); - } + void classifyRecords(); // Every use of a borrowing record (spec 32), and of what is read out of it - each may read what // the record borrows - through its views, merges, and the locals that hold it, made again at @@ -674,207 +584,13 @@ class OwnershipInferencePass // itself is `kept` when it is stored anywhere but a local, given to a callee that keeps it, or // captured; returned, it is in `returns`. A call given it hands back its own block, unless that // is a borrowing record that holds it, whose uses are then this one's as well. - void walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk) - { - struct Pending - { - mlir::Value value; - bool record; - }; - - llvm::SmallVector work; - llvm::SmallPtrSet seen; - auto push = [&](mlir::Value value, bool record) { - if ((record || mayPointInto(value)) && seen.insert(value).second) - { - work.push_back({value, record}); - } - }; - - auto followSlot = [&](mlir_ts::VariableOp varOp, bool record) { - for (auto *slotUser : varOp->getUsers()) - { - // its releases and its other values are the slot's own - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - push(loadOp.getResult(), record); - } - } - }; - - auto keep = [&](mlir::Operation *user) { walk.kept = walk.kept ? walk.kept : user; }; - - for (auto start : starts) - { - push(start, true); - } - - while (!work.empty()) - { - auto pending = work.pop_back_val(); - auto value = pending.value; - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || - (pending.record && mlir::isa(user))) - { - continue; - } - - // the same value: a view, a cast, a merge - if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) - { - push(user->getResult(0), pending.record); - continue; - } - - if (auto branchOp = mlir::dyn_cast(user)) - { - if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) - { - push(*argument, pending.record); - } - - continue; - } - - // a local that holds it: its reads - auto varOp = mlir::dyn_cast(user); - auto storeOp = mlir::dyn_cast(user); - if (varOp || (storeOp && storeOp.getValue() == value)) - { - auto slotOp = varOp ? varOp : storeOp.getReference().getDefiningOp(); - if (slotOp && !slotOp.getCaptured().value_or(false)) - { - followSlot(slotOp, pending.record); - continue; - } - - // a place, a global, a captured variable: what is read out of the record is checked - // where it is read (checkPlaceRead) - walk.uses.push_back({user, {kills.begin(), kills.end()}}); - if (pending.record) - { - keep(user); - } - - continue; - } - - walk.uses.push_back({user, {kills.begin(), kills.end()}, isCall(user) && isCalleeOperand(use)}); - if (mlir::isa(user)) - { - if (pending.record) - { - walk.returns.push_back(user); - } - - continue; - } - - if (isCall(user)) - { - if (pending.record && isKeptArgument(user, value)) - { - keep(user); - continue; - } - - // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that - // holds it - points where it points - auto args = callArgs(user); - auto given = [&](llvm::ArrayRef indices) { - return llvm::any_of(indices, [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - }); - }; - - if (user->getNumResults() == 1 && given(resultBorrowsAll(user))) - { - push(user->getResult(0), pending.record || isRecordResult(user)); - } - - // ...and a generator's state that borrows it (OWN_RESULT_BOUNDED_ATTR_NAME) - if (auto bounded = user->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); - bounded && user->getNumResults() == 1 && - given(llvm::SmallVector(bounded.asArrayRef().begin(), bounded.asArrayRef().end()))) - { - push(user->getResult(0), false); - } - - continue; - } - - if (pending.record && !isBorrow(user, value)) - { - keep(user); - continue; - } - - // a field written through the record: its old value may be one the record borrows, - // which the overwrite would release, and the field may be one its release leaves - if (pending.record && mlir::isa(user)) - { - for (auto *fieldUser : user->getResult(0).getUsers()) - { - auto fieldStore = mlir::dyn_cast(fieldUser); - if (mlir::isa(fieldUser) || - (fieldStore && fieldStore.getReference() == user->getResult(0))) - { - keep(fieldUser); - } - } - } - - // what it produces - a field's reference, a value read out of it, a bound method - may - // still point into what the record borrows - for (auto result : user->getResults()) - { - push(result, false); - } - } - } - } + void walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk); // The name the record goes by: the local that holds it, or a folded `const`'s. - llvm::StringRef recordName(mlir::Value value) - { - llvm::StringRef name = ownerName(value); - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (auto varOp = mlir::dyn_cast(user); varOp && varOp.getInitializer() == used) - { - name = varName(varOp); - } - }); - - return name; - } + llvm::StringRef recordName(mlir::Value value); // What a borrowed field holds, for an error: `'ray'`, `'this.pos'`, `'livec'`, ... - std::string describeRecordSource(BorrowedField field) - { - auto value = field.store.getValue(); - auto quoted = [](llvm::StringRef name) { return "'" + name.str() + "'"; }; - switch (field.source) - { - case RecordFieldSource::Param: - return quoted(paramName(borrowedParam(value))); - case RecordFieldSource::Place: - if (auto loadOp = rootOf(value).getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - return describePlace(loadOp.getReference()); - } - - return describeCall(rootOf(value).getDefiningOp()); - case RecordFieldSource::Slot: - return quoted(varName(slotLoadOf(value).getReference().getDefiningOp())); - case RecordFieldSource::Cell: - return "a captured variable"; - default: - return "a value it does not own"; - } - } + std::string describeRecordSource(BorrowedField field); // A record this function makes (spec 32): kept, it is an error; returned, each field it borrows // must be a parameter's that this function's facts lend (`__own_result_record`), so that its @@ -882,101 +598,7 @@ class OwnershipInferencePass // overwritten - an owning local's ends, and whatever may assign a captured variable. A parameter // and `this` are the caller's for the whole call; a read out of a place has the record's uses as // its own (checkPlaceRead). - void checkBorrowingRecords() - { - for (auto &record : records) - { - auto name = recordName(record->interfaces.empty() ? record->box.getResult() : record->interfaces.front()); - if (record->walk.kept) - { - reportPlaceBorrowEscapes(record->walk.kept, name, - record->borrowed.empty() ? "a value it does not own" - : describeRecordSource(record->borrowed.front())); - continue; - } - - auto ok = true; - for (auto *returned : record->walk.returns) - { - for (auto &field : record->borrowed) - { - if (!ok || field.source == RecordFieldSource::Immortal) - { - continue; - } - - auto param = field.source == RecordFieldSource::Param || field.source == RecordFieldSource::Place - ? borrowedParam(field.store.getValue()) - : -1; - if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) - { - reportPlaceBorrowEscapes(returned, name, describeRecordSource(field)); - ok = false; - } - } - } - - for (auto &field : record->borrowed) - { - if (!ok) - { - break; - } - - auto value = field.store.getValue(); - Chain chain; - if (field.source == RecordFieldSource::Slot) - { - chain.roots.push_back({slotLoadOf(value).getReference(), Chain::Slot}); - } - else if (auto load = field.source == RecordFieldSource::Param ? keptParamLoadOf(value) : mlir_ts::LoadOp()) - { - // a parameter this function keeps is its own to move away, as a local's value is - chain.roots.push_back({load.getReference(), Chain::Slot}); - } - else if (field.source == RecordFieldSource::Cell) - { - chain.roots.push_back({rootOf(value), Chain::NotOwned}); - } - else - { - continue; - } - - auto ends = rootEnds(chain); - if (field.source == RecordFieldSource::Cell) - { - for (auto *drop : drops) - { - if (dropsChain(drop, chain, nullptr, value)) - { - ends.push_back(drop); - } - } - } - - for (auto *end : ends) - { - for (auto &use : record->walk.uses) - { - if (ok && (use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) - { - if (field.source == RecordFieldSource::Slot) - { - reportBorrowOutlives(use.op, end, name, varName(chain.roots.front().first.getDefiningOp())); - } - else - { - reportPlaceBorrowOutlives(use.op, end, name, describeRecordSource(field)); - } - - ok = false; - } - } - } - } - } - } + void checkBorrowingRecords(); // A call whose result is a borrowing record (OWN_RESULT_RECORD_ATTR_NAME, spec 32): the box is // this function's to own - it is fresh, and its own check decides its moves and releases - but @@ -985,82 +607,7 @@ class OwnershipInferencePass // into, may come after one of those arguments may be released or overwritten, and it may not be // kept. Returned, the arguments must be parameters this function's own facts lend. Its releases // are not uses: they free only what the record owns. - void checkRecordResults() - { - getFunction().walk([&](mlir::Operation *call) { - if (!isCall(call) || !isRecordResult(call) || call->getNumResults() != 1) - { - return; - } - - auto result = call->getResult(0); - auto place = describeCall(call); - auto name = recordName(result); - RecordWalk walk; - llvm::SmallVector kills{call}; - walkRecord({result}, kills, walk); - if (walk.kept) - { - reportPlaceBorrowEscapes(walk.kept, name, place); - return; - } - - auto args = callArgs(call); - for (auto *returned : walk.returns) - { - for (auto index : resultBorrowsAll(call)) - { - if (static_cast(index) >= args.size() || argumentHoldsNoBlock(args[index]) || - isImmortalLiteral(args[index])) - { - continue; - } - - auto param = borrowedParam(args[index]); - if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) - { - reportPlaceBorrowEscapes(returned, name, place); - return; - } - } - } - - auto chain = chainOf(call); - // rc's retain of a fresh argument right before the call is no end: the box's own check - // decides it, and the use that takes it is an end of its own - llvm::SmallVector ends; - for (auto *end : rootEnds(chain)) - { - auto ownedRetain = mlir::isa(end) && llvm::any_of(chain.roots, [&](auto &root) { - return root.second == Chain::Owned && rootOf(end->getOperand(0)) == root.first; - }); - if (!ownedRetain) - { - ends.push_back(end); - } - } - - for (auto *drop : drops) - { - if (drop != call && dropsChain(drop, chain, call)) - { - ends.push_back(drop); - } - } - - for (auto *end : ends) - { - for (auto &use : walk.uses) - { - if ((use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) - { - reportPlaceBorrowOutlives(use.op, end, name, place); - return; - } - } - } - }); - } + void checkRecordResults(); // ---- Reads out of containers (spec 2.3, phase 3) ---- // diff --git a/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp new file mode 100644 index 000000000..51c66ccd6 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp @@ -0,0 +1,479 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): borrowing records (spec 32). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::mayPointInto(mlir::Value value) + { + auto type = value.getType(); + return mlir::isa(type) || + ownsHeap(value); + } + + OwnershipInferencePass::BorrowingRecord *OwnershipInferencePass::borrowingRecordOf(mlir_ts::VariableOp varOp) + { + for (auto &record : records) + { + if (record->local == varOp) + { + return record.get(); + } + } + + return nullptr; + } + + void OwnershipInferencePass::classifyRecords() + { + getFunction().walk([&](mlir_ts::NewOp newOp) { + auto fields = recordBorrowedFields(newOp); + auto varOp = fields.empty() ? mlir_ts::VariableOp() : recordLocalOf(newOp.getResult()); + if (!varOp) + { + return; + } + + auto record = std::make_shared(); + record->box = newOp; + record->local = varOp; + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + auto borrows = llvm::is_contained(fields, static_cast(propertyRefOp.getPosition())); + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + continue; + } + + auto source = recordFieldSource(storeOp.getValue()); + if (source == RecordFieldSource::NoBlock) + { + continue; + } + + if (source == RecordFieldSource::Unknown || borrows != (source != RecordFieldSource::Fresh)) + { + return; + } + + if (borrows) + { + record->borrowed.push_back({storeOp, source}); + } + } + } + + for (auto *user : newOp->getUsers()) + { + if (auto castOp = mlir::dyn_cast(user)) + { + for (auto *castUser : castOp->getUsers()) + { + if (auto newInterfaceOp = mlir::dyn_cast(castUser)) + { + record->interfaces.push_back(newInterfaceOp.getResult()); + } + } + } + } + + // a path back to a use through the record's making is another record + llvm::SmallVector kills{newOp.getOperation(), varOp.getOperation()}; + walkRecord(record->interfaces, kills, record->walk); + for (auto &field : record->borrowed) + { + setAttrTracked(field.store, OWN_CAPTURE_BORROW_ATTR_NAME); + recordStoreUses[field.store.getOperation()] = record.get(); + } + + records.push_back(std::move(record)); + }); + } + + void OwnershipInferencePass::walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk) + { + struct Pending + { + mlir::Value value; + bool record; + }; + + llvm::SmallVector work; + llvm::SmallPtrSet seen; + auto push = [&](mlir::Value value, bool record) { + if ((record || mayPointInto(value)) && seen.insert(value).second) + { + work.push_back({value, record}); + } + }; + + auto followSlot = [&](mlir_ts::VariableOp varOp, bool record) { + for (auto *slotUser : varOp->getUsers()) + { + // its releases and its other values are the slot's own + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + push(loadOp.getResult(), record); + } + } + }; + + auto keep = [&](mlir::Operation *user) { walk.kept = walk.kept ? walk.kept : user; }; + + for (auto start : starts) + { + push(start, true); + } + + while (!work.empty()) + { + auto pending = work.pop_back_val(); + auto value = pending.value; + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || + (pending.record && mlir::isa(user))) + { + continue; + } + + // the same value: a view, a cast, a merge + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + push(user->getResult(0), pending.record); + continue; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + push(*argument, pending.record); + } + + continue; + } + + // a local that holds it: its reads + auto varOp = mlir::dyn_cast(user); + auto storeOp = mlir::dyn_cast(user); + if (varOp || (storeOp && storeOp.getValue() == value)) + { + auto slotOp = varOp ? varOp : storeOp.getReference().getDefiningOp(); + if (slotOp && !slotOp.getCaptured().value_or(false)) + { + followSlot(slotOp, pending.record); + continue; + } + + // a place, a global, a captured variable: what is read out of the record is checked + // where it is read (checkPlaceRead) + walk.uses.push_back({user, {kills.begin(), kills.end()}}); + if (pending.record) + { + keep(user); + } + + continue; + } + + walk.uses.push_back({user, {kills.begin(), kills.end()}, isCall(user) && isCalleeOperand(use)}); + if (mlir::isa(user)) + { + if (pending.record) + { + walk.returns.push_back(user); + } + + continue; + } + + if (isCall(user)) + { + if (pending.record && isKeptArgument(user, value)) + { + keep(user); + continue; + } + + // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that + // holds it - points where it points + auto args = callArgs(user); + auto given = [&](llvm::ArrayRef indices) { + return llvm::any_of(indices, [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + }; + + if (user->getNumResults() == 1 && given(resultBorrowsAll(user))) + { + push(user->getResult(0), pending.record || isRecordResult(user)); + } + + // ...and a generator's state that borrows it (OWN_RESULT_BOUNDED_ATTR_NAME) + if (auto bounded = user->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + bounded && user->getNumResults() == 1 && + given(llvm::SmallVector(bounded.asArrayRef().begin(), bounded.asArrayRef().end()))) + { + push(user->getResult(0), false); + } + + continue; + } + + if (pending.record && !isBorrow(user, value)) + { + keep(user); + continue; + } + + // a field written through the record: its old value may be one the record borrows, + // which the overwrite would release, and the field may be one its release leaves + if (pending.record && mlir::isa(user)) + { + for (auto *fieldUser : user->getResult(0).getUsers()) + { + auto fieldStore = mlir::dyn_cast(fieldUser); + if (mlir::isa(fieldUser) || + (fieldStore && fieldStore.getReference() == user->getResult(0))) + { + keep(fieldUser); + } + } + } + + // what it produces - a field's reference, a value read out of it, a bound method - may + // still point into what the record borrows + for (auto result : user->getResults()) + { + push(result, false); + } + } + } + } + + llvm::StringRef OwnershipInferencePass::recordName(mlir::Value value) + { + llvm::StringRef name = ownerName(value); + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (auto varOp = mlir::dyn_cast(user); varOp && varOp.getInitializer() == used) + { + name = varName(varOp); + } + }); + + return name; + } + + std::string OwnershipInferencePass::describeRecordSource(BorrowedField field) + { + auto value = field.store.getValue(); + auto quoted = [](llvm::StringRef name) { return "'" + name.str() + "'"; }; + switch (field.source) + { + case RecordFieldSource::Param: + return quoted(paramName(borrowedParam(value))); + case RecordFieldSource::Place: + if (auto loadOp = rootOf(value).getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + return describePlace(loadOp.getReference()); + } + + return describeCall(rootOf(value).getDefiningOp()); + case RecordFieldSource::Slot: + return quoted(varName(slotLoadOf(value).getReference().getDefiningOp())); + case RecordFieldSource::Cell: + return "a captured variable"; + default: + return "a value it does not own"; + } + } + + void OwnershipInferencePass::checkBorrowingRecords() + { + for (auto &record : records) + { + auto name = recordName(record->interfaces.empty() ? record->box.getResult() : record->interfaces.front()); + if (record->walk.kept) + { + reportPlaceBorrowEscapes(record->walk.kept, name, + record->borrowed.empty() ? "a value it does not own" + : describeRecordSource(record->borrowed.front())); + continue; + } + + auto ok = true; + for (auto *returned : record->walk.returns) + { + for (auto &field : record->borrowed) + { + if (!ok || field.source == RecordFieldSource::Immortal) + { + continue; + } + + auto param = field.source == RecordFieldSource::Param || field.source == RecordFieldSource::Place + ? borrowedParam(field.store.getValue()) + : -1; + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, describeRecordSource(field)); + ok = false; + } + } + } + + for (auto &field : record->borrowed) + { + if (!ok) + { + break; + } + + auto value = field.store.getValue(); + Chain chain; + if (field.source == RecordFieldSource::Slot) + { + chain.roots.push_back({slotLoadOf(value).getReference(), Chain::Slot}); + } + else if (auto load = field.source == RecordFieldSource::Param ? keptParamLoadOf(value) : mlir_ts::LoadOp()) + { + // a parameter this function keeps is its own to move away, as a local's value is + chain.roots.push_back({load.getReference(), Chain::Slot}); + } + else if (field.source == RecordFieldSource::Cell) + { + chain.roots.push_back({rootOf(value), Chain::NotOwned}); + } + else + { + continue; + } + + auto ends = rootEnds(chain); + if (field.source == RecordFieldSource::Cell) + { + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value)) + { + ends.push_back(drop); + } + } + } + + for (auto *end : ends) + { + for (auto &use : record->walk.uses) + { + if (ok && (use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + if (field.source == RecordFieldSource::Slot) + { + reportBorrowOutlives(use.op, end, name, varName(chain.roots.front().first.getDefiningOp())); + } + else + { + reportPlaceBorrowOutlives(use.op, end, name, describeRecordSource(field)); + } + + ok = false; + } + } + } + } + } + } + + void OwnershipInferencePass::checkRecordResults() + { + getFunction().walk([&](mlir::Operation *call) { + if (!isCall(call) || !isRecordResult(call) || call->getNumResults() != 1) + { + return; + } + + auto result = call->getResult(0); + auto place = describeCall(call); + auto name = recordName(result); + RecordWalk walk; + llvm::SmallVector kills{call}; + walkRecord({result}, kills, walk); + if (walk.kept) + { + reportPlaceBorrowEscapes(walk.kept, name, place); + return; + } + + auto args = callArgs(call); + for (auto *returned : walk.returns) + { + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) >= args.size() || argumentHoldsNoBlock(args[index]) || + isImmortalLiteral(args[index])) + { + continue; + } + + auto param = borrowedParam(args[index]); + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, place); + return; + } + } + } + + auto chain = chainOf(call); + // rc's retain of a fresh argument right before the call is no end: the box's own check + // decides it, and the use that takes it is an end of its own + llvm::SmallVector ends; + for (auto *end : rootEnds(chain)) + { + auto ownedRetain = mlir::isa(end) && llvm::any_of(chain.roots, [&](auto &root) { + return root.second == Chain::Owned && rootOf(end->getOperand(0)) == root.first; + }); + if (!ownedRetain) + { + ends.push_back(end); + } + } + + for (auto *drop : drops) + { + if (drop != call && dropsChain(drop, chain, call)) + { + ends.push_back(drop); + } + } + + for (auto *end : ends) + { + for (auto &use : walk.uses) + { + if ((use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, end, name, place); + return; + } + } + } + }); + } + +} // namespace own_inference From 7df6df8b97c0249a7d221ae9a4d0b40e154b1189 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:25:41 +0100 Subject: [PATCH 20/25] own passes: OwnershipInferencePlaceReads.cpp out of OwnershipInferencePass.h (I1) Reads out of containers (spec 2.3, phase 3) - handle reads, the chain from a read to its roots, and what may drop it. A pure move of placeReadOf, handleReadUses, isCountedHandleRead, isCountedHandleReceiver, isHandOff, isCountedHandOff, retainedRightBefore, collectCountedStoreReleases, countedStoreRetain, borrowingCall, chainOf, rootEnds, rootSources, derivedFrom, dropsChain, checkPlaceRead, keptParamOutlives, checkReturnedParam. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../lib/TypeScript/OwnershipInferencePass.h | 856 +---------------- .../OwnershipInferencePlaceReads.cpp | 867 ++++++++++++++++++ 3 files changed, 886 insertions(+), 838 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 0bc6e7ec9..967fce9fc 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -36,6 +36,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferenceBorrowers.cpp OwnershipInferenceClosures.cpp OwnershipInferenceKeptParams.cpp + OwnershipInferencePlaceReads.cpp OwnershipInferenceRecords.cpp OwnershipSignaturePass.cpp OwnershipSignatureCallbacks.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index dffa4e3d8..0d8b601ac 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -619,31 +619,7 @@ class OwnershipInferencePass // A read of a heap value out of a field or an element, or a call whose result borrows an // argument (spec 2.4, phase 4), seen through its views. Null for anything else. - mlir::Operation *placeReadOf(mlir::Value value) - { - auto root = rootOf(value); - auto *def = root.getDefiningOp(); - // a handle read out of a place that rc gives an owner of its own is a counted copy, not a - // borrow (spec 23.3); any other is a borrow of the place, as a field read is - if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) - { - return nullptr; - } - - if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) - { - // a field of a generator's borrowing box: only its maker fills it, and nothing a call - // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) - if (isBorrowingBoxField(loadOp.getReference())) - { - return nullptr; - } - - return def; - } - - return borrowingCall(root); - } + mlir::Operation *placeReadOf(mlir::Value value); // How a handle read out of a place is used (spec 23.3), seen through casts, merges and locals // that own nothing. Every use is classified, and nothing falls through (Ruling 9): @@ -665,55 +641,7 @@ class OwnershipInferencePass bool borrowed; }; - static HandleReadUses handleReadUses(mlir::Value value) - { - HandleReadUses result{false, false}; - llvm::SmallVector values{value}; - llvm::SmallPtrSet seen{value}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto &use : current.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - continue; - } - - // a return and an `any` box take the value, though passesOn would hand them on - if (isHandOff(user, current)) - { - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; - continue; - } - - llvm::SmallVector same; - if (passesOn(use, same)) - { - for (auto next : same) - { - // what is no longer a handle is no receiver: a test (`if (p.child)`) only - // reads it - if (!isHandle(next)) - { - result.borrowed = true; - } - else if (seen.insert(next).second) - { - values.push_back(next); - } - } - - continue; - } - - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; - } - } - - return result; - } + static HandleReadUses handleReadUses(mlir::Value value); // A handle read out of a place that rc gives a count of its own is a counted copy, not a borrow // (spec 23.3) - but only when every use of it is one rc counts (Ruling 7), and a use that is @@ -721,11 +649,7 @@ class OwnershipInferencePass // well (`f(p, q, q.child = p.child)`), or that keeps it with no count (`[(q.child = p.child), // 1]`), is a borrow, and that use is tenure-checked; the counted uses are copies // (checkPlaceRead). - static bool isCountedHandleRead(mlir::Value value) - { - auto uses = handleReadUses(value); - return uses.counted && !uses.borrowed; - } + static bool isCountedHandleRead(mlir::Value value); // A use that gives a handle an owner rc counts, with rc's retain for that very use: rc's // retain itself, a store into a field, an element, a global, an owning local or the result @@ -734,138 +658,25 @@ class OwnershipInferencePass // retain of what it takes (retainedRightBefore); or the initializer of an owning local, which // takes a count of its own (`ts.RetainSlot`). Any of them without its retain is not one: it // keeps the read without a count (`{ s: (q.child = p.child) }`'s field). - static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return true; - } - - if (auto varOp = mlir::dyn_cast(user)) - { - return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { - return mlir::isa(varUser); - }); - } - - if (auto storeOp = mlir::dyn_cast(user)) - { - return storeOp.getValue() == value && countedStoreRetain(storeOp); - } - - if (auto pushOp = mlir::dyn_cast(user)) - { - return retainedRightBefore(user, pushOp.getItems(), value); - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return retainedRightBefore(user, unshiftOp.getItems(), value); - } - - if (auto castOp = mlir::dyn_cast(user)) - { - return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); - } - - // `ts.ReturnVal` is the return before the affine lowering, which makes it a store into - // the result slot - if (mlir::isa(user)) - { - return retainedRightBefore(user, user->getOperands(), value); - } - - return false; - } + static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value); // A use that passesOn hands on but that takes the value: a return's store into the function's // result slot (isResultSlot), and a cast that boxes it into `any`. - static bool isHandOff(mlir::Operation *user, mlir::Value value) - { - if (auto storeOp = mlir::dyn_cast(user)) - { - auto slot = storeOp.getReference().getDefiningOp(); - return storeOp.getValue() == value && slot && isResultSlot(slot); - } - - auto castOp = mlir::dyn_cast(user); - return castOp && castOp.getIn() == value && mlir::isa(castOp.getType()); - } + static bool isHandOff(mlir::Operation *user, mlir::Value value); // A return or an `any` box of a handle that rc counts (walkBorrowed). - static bool isCountedHandOff(mlir::Operation *user, mlir::Value value) - { - return isHandOff(user, value) && isCountedHandleReceiver(user, value); - } + static bool isCountedHandOff(mlir::Operation *user, mlir::Value value); // rc's retains for what `op` takes: MLIRGen puts them in a row right before it // (mlirGenRetainCaptured, retainInsertedElements), one for each operand it does not take // over. Answers whether that row retains `value` once for each time `operands` hold it. A // retain is right before one op only, so no other receiver can count it as well. - static bool retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value) - { - auto needed = llvm::count(operands, value); - if (needed == 0) - { - return false; - } - - for (auto *previous = op->getPrevNode(); needed > 0 && previous; previous = previous->getPrevNode()) - { - auto retainOp = mlir::dyn_cast(previous); - if (!retainOp) - { - break; - } - - if (retainOp.getReference() == value) - { - --needed; - } - } - - return needed == 0; - } + static bool retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value); // The releases of the old value that rc's counted stores of a handle read make: `ts.Retain` of // the value, `ts.ReleaseSlot` of the slot, `ts.Store` of the value into it, in a row. Seen // through the read's casts, merges and locals that own nothing, as handleReadUses sees it. - static void collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases) - { - llvm::SmallVector values{value}; - llvm::SmallPtrSet seen{value}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto &use : current.getUses()) - { - llvm::SmallVector same; - if (passesOn(use, same)) - { - for (auto next : same) - { - if (isHandle(next) && seen.insert(next).second) - { - values.push_back(next); - } - } - - continue; - } - - auto storeOp = mlir::dyn_cast(use.getOwner()); - if (!storeOp || storeOp.getValue() != current || !countedStoreRetain(storeOp)) - { - continue; - } - - if (auto releaseSlotOp = mlir::dyn_cast_or_null(storeOp->getPrevNode())) - { - releases.insert(releaseSlotOp); - } - } - } - } + static void collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases); // rc's retain for a store of a value, when it made one: `ts.Retain` of the value right before // the store, or before the `ts.ReleaseSlot` of the old value right before it - @@ -873,41 +684,11 @@ class OwnershipInferencePass // slot comes after the scope exit's releases of the frame's locals and their slots: // `Retain(v)`, [`Release(..)`, `ReleaseSlot(..)`...], `Store(v, result)`. Null for a store rc // counts nothing for. - static mlir::Operation *countedStoreRetain(mlir_ts::StoreOp storeOp) - { - auto *previous = storeOp->getPrevNode(); - if (auto slot = storeOp.getReference().getDefiningOp(); slot && isResultSlot(slot)) - { - // the scope exit's releases, of locals and of their slots, in any order - while (mlir::isa_and_nonnull(previous) && - previous->getOperand(0) != storeOp.getValue()) - { - previous = previous->getPrevNode(); - } - } - else if (auto releaseSlotOp = mlir::dyn_cast_or_null(previous)) - { - previous = releaseSlotOp.getSlot() == storeOp.getReference() ? releaseSlotOp->getPrevNode() : nullptr; - } - - auto retainOp = mlir::dyn_cast_or_null(previous); - return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; - } + static mlir::Operation *countedStoreRetain(mlir_ts::StoreOp storeOp); // The call a value is the result of, when that result is a plain borrow of an argument. A borrowing // record (spec 32) is not one: the caller owns the box (checkRecordResults). - static mlir::Operation *borrowingCall(mlir::Value value) - { - auto *def = value.getDefiningOp(); - if (!def || !isCall(def) || resultBorrows(def) < 0) - { - return nullptr; - } - - auto args = callArgs(def); - auto indices = resultBorrowsAll(def); - return llvm::all_of(indices, [&](int index) { return static_cast(index) < args.size(); }) ? def : nullptr; - } + static mlir::Operation *borrowingCall(mlir::Value value); // The places from a read up to its roots, and the roots: the values the function reached the // first container through. A container reached through a merge (`(c ? a : b).x`) has a root @@ -942,332 +723,21 @@ class OwnershipInferencePass }; // With `place`, the chain of that place itself: where a store into it moves a value (spec 29). - Chain chainOf(mlir::Operation *read, mlir::Value place = {}) - { - Chain chain; - llvm::SmallVector refs; - llvm::SmallVector first; - if (place) - { - refs.push_back(place); - } - else if (auto loadOp = mlir::dyn_cast(read)) - { - refs.push_back(loadOp.getReference()); - } - else - { - // a call's result that borrows its arguments - a plain borrow, or a borrowing record's box - // (spec 32), which may hold a borrow of each - is somewhere under each of them - chain.anyPlace = true; - auto args = callArgs(read); - for (auto index : resultBorrowsAll(read)) - { - if (static_cast(index) < args.size() && !argumentHoldsNoBlock(args[index]) && - !isImmortalLiteral(args[index])) - { - first.push_back(rootOf(args[index])); - } - } - } - - llvm::SmallPtrSet seen; - // the bases a `s.value` was read through, and what merges into them - llvm::SmallPtrSet handleBases; - while (!refs.empty() || !first.empty()) - { - llvm::SmallVector bases; - if (!first.empty()) - { - bases.swap(first); - } - else - { - auto ref = refs.pop_back_val(); - auto place = placeBase(ref); - assert(place && "chainOf walks places only"); - switch (place.kind) - { - case PlaceKind::Field: - chain.places.push_back({ref.getDefiningOp().getPosition(), ref.getType()}); - break; - case PlaceKind::SharedValue: - chain.places.push_back({Chain::SharedValue, ref.getType()}); - handleBases.insert(rootOf(place.base)); - break; - case PlaceKind::InterfaceField: - chain.places.push_back({Chain::InterfaceField, ref.getType()}); - break; - default: - chain.places.push_back({-1, ref.getType()}); - break; - } - - bases.push_back(rootOf(place.base)); - } - - while (!bases.empty()) - { - auto base = bases.pop_back_val(); - if (!seen.insert(base).second) - { - continue; - } - - // A handle (spec 23.3): other handles may reach its block, so this function owns - // nothing under it. A handle read out of a place still has that place above it, and - // one merged from several has each of them. - if (handleBases.contains(base) || isHandle(base)) - { - llvm::SmallVector merged; - if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, merged)) - { - handleBases.insert(merged.begin(), merged.end()); - bases.append(merged.begin(), merged.end()); - continue; - } - - chain.roots.push_back({base, Chain::NotOwned}); - chain.handles.push_back(base); - if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - refs.push_back(loadOp.getReference()); - } - - continue; - } - - if (isPlace(base)) - { - refs.push_back(base); // a field of a value-type field: the same object - continue; - } - - if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - refs.push_back(loadOp.getReference()); - continue; - } - - // the object a method is called on - if (auto object = boundThis(base)) - { - bases.push_back(rootOf(object)); - continue; - } - - // a result that borrows an argument is somewhere under the argument - if (auto *call = borrowingCall(base)) - { - chain.anyPlace = true; - auto args = callArgs(call); - for (auto index : resultBorrowsAll(call)) - { - if (static_cast(index) < args.size()) - { - bases.push_back(rootOf(args[index])); - } - } - - continue; - } - - if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, bases)) - { - continue; - } - - if (base.getDefiningOp()) - { - chain.roots.push_back({base, Chain::Slot}); - } - else if (auto slotLoad = slotLoadOf(base)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else - { - chain.roots.push_back({base, isFresh(base) ? Chain::Owned : Chain::NotOwned}); - } - } - } - - return chain; - } + Chain chainOf(mlir::Operation *read, mlir::Value place = {}); // Where the roots stop holding what the chain reads: a local's releases and assignments and // every declaration or retain that may move it away; a made value's releases and every use // that takes it; a global's assignments. - llvm::SmallVector rootEnds(const Chain &chain) - { - llvm::SmallVector ends; - for (auto [root, kind] : chain.roots) - { - // A handle (spec 23.3), never owned: where this one is given back or overwritten - a - // local's release and assignments, a temporary's release; a global's assignments are - // below. A copy of it is counted, and moves nothing away. - if (llvm::is_contained(chain.handles, root)) - { - auto loadOp = root.getDefiningOp(); - if (auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp()) - { - for (auto *user : varOp->getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - } - } - else if (!loadOp) - { - forEachUse(root, [&](mlir::Operation *use, mlir::Value) { - if (mlir::isa(use)) - { - ends.push_back(use); - } - }); - } - } - - if (kind == Chain::Slot) - { - for (auto *user : root.getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - else if (auto loadOp = mlir::dyn_cast(user)) - { - forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use) || (varOp && isOwningVariable(varOp)) || - (isCall(use) && isKeptArgument(use, used))) - { - ends.push_back(use); - } - }); - } - } - } - else if (kind == Chain::Owned) - { - forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { - if (mlir::isa(use) || !isBorrow(use, used)) - { - ends.push_back(use); - } - }); - } - else if (auto loadOp = root.getDefiningOp()) - { - if (auto global = loadOp.getReference().getDefiningOp()) - { - getFunction().walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto releaseSlotOp = mlir::dyn_cast(op)) - { - slot = releaseSlotOp.getSlot(); - } - else if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - - auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); - if (other && other.getGlobalName() == global.getGlobalName()) - { - ends.push_back(op); - } - }); - } - } - } - - return ends; - } + llvm::SmallVector rootEnds(const Chain &chain); // What a call has to be given to reach a root: the root, and where the root is a local // declared from another (a borrow, or a move that left the other dead), that one too. - static llvm::SmallVector rootSources(const Chain &chain) - { - llvm::SmallVector sources; - for (auto &root : chain.roots) - { - auto value = root.first; - sources.push_back(value); - for (auto steps = 0; steps < 8; ++steps) - { - auto varOp = value.getDefiningOp(); - if (!varOp || !varOp.getInitializer()) - { - break; - } - - auto initializer = rootOf(varOp.getInitializer()); - auto slotLoad = slotLoadOf(initializer); - value = slotLoad ? slotLoad.getReference() : initializer; - sources.push_back(value); - } - } - - return sources; - } + static llvm::SmallVector rootSources(const Chain &chain); // Everything that may point into what `start` points to: the forward closure over users' // results that may point anywhere. A call's result is not followed: the call hands back a // value it owns. - const llvm::DenseSet &derivedFrom(mlir::Value start) - { - if (auto found = derivedCache.find(start); found != derivedCache.end()) - { - return *found->second; - } - - auto &slot = derivedCache[start]; - slot = std::make_shared>(); - auto &derived = *slot; - llvm::SmallVector work{start}; - while (!work.empty()) - { - auto value = work.pop_back_val(); - if (!derived.insert(value).second) - { - continue; - } - - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (isCall(user)) - { - // ...but a borrowing record given this value holds it (spec 32) - auto args = callArgs(user); - if (isRecordResult(user) && llvm::any_of(resultBorrowsAll(user), [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - })) - { - work.push_back(user->getResult(0)); - } - - continue; - } - - llvm::SmallVector next(user->getResults().begin(), user->getResults().end()); - passesOn(use, next); - for (auto result : next) - { - if (mayPointInto(result)) - { - work.push_back(result); - } - } - } - } - - return derived; - } + const llvm::DenseSet &derivedFrom(mlir::Value start); // May this op of the function's drops destroy what the chain reads? An overwrite of a place // on it, matched by position and type (two references to one field of one object always @@ -1278,212 +748,13 @@ class OwnershipInferencePass // from the root that the read did not produce. // `borrowed`, when given, is the value the borrow holds in place of the read's result (a value // moved into the place, spec 29), and `read` may then be null. - bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed = {}) - { - if (read && drop == read) - { - return false; // a call that returns a borrow has not destroyed it by returning it - } - - if (chain.anyPlace && !isCall(drop)) - { - return true; - } - - if (auto releaseSlotOp = mlir::dyn_cast(drop)) - { - // An assignment to a captured variable destroys its old value, which is reached only - // through a read of the variable's cell: a root this function does not own. - auto slot = releaseSlotOp.getSlot(); - if (isCellVariable(slot) || isLoadedCell(slot)) - { - return !chain.owned(); - } - - // An assignment to a global destroys its old value and all that value owns. A root this - // function does not own - `this`, a parameter, a global's or a field's value - may be - // that very block, or under it (spec 29). A root this function made cannot. An immortal - // global's one store is its initializer, whose old value is the empty one (spec 25). - if (auto addressOfOp = slot.getDefiningOp()) - { - return !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && !chain.owned(); - } - - // `x.value = ...` through any handle, of any type, may replace the payload another - // handle reads (spec 23.3): a handle converts to one of a base class's type - if (slot.getDefiningOp()) - { - return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); - } - - auto isField = [](int64_t position) { return position >= 0 || position == Chain::InterfaceField; }; - int64_t position = -1; - if (auto propertyRefOp = slot.getDefiningOp()) - { - position = propertyRefOp.getPosition(); - } - else if (isInterfaceField(slot)) - { - position = Chain::InterfaceField; - } - - // A field reached through an interface may be any field of the object under it, of any - // type (a class may declare the field narrower than the interface): an overwrite through - // an interface drops every field on the chain, and any field's overwrite drops a field - // read through an interface (spec 28). - return llvm::any_of(chain.places, [&](auto &place) { - if (isField(position) && isField(place.first) && - (position == Chain::InterfaceField || place.first == Chain::InterfaceField)) - { - return true; - } - - return place.first == position && place.second == slot.getType(); - }); - } - - if (isCall(drop)) - { - if (callNoDrops(drop)) - { - return false; - } - - // a call given a closure may run it, and its box may hold anything - if (!chain.owned() || llvm::any_of(drop->getOperands(), holdsCaptures)) - { - return true; - } - - auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); - auto sources = rootSources(chain); - return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { - return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { - return derivedFrom(source).contains(operand); - }); - }); - } - - // pop, shift, splice, `length =`; push and unshift on an array reached through a handle - auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); - auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); - if (!arrayType) - { - return true; - } - - return llvm::any_of(chain.places, [&](auto &place) { - auto placeType = mlir::dyn_cast(place.second); - return place.first == -1 && placeType && placeType.getElementType() == arrayType.getElementType(); - }); - } + bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed = {}); // A read out of a container is a borrow of it (spec 2.3), and so is a `let` declared from one: // its retain and releases go when the check passes, as for any borrowing `let` (phase 2). // Reports and returns false when the read is kept, or used after something that may destroy // it. - bool checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase) - { - auto loadOp = mlir::dyn_cast(read); - auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); - auto result = read->getResult(0); - // A handle read rc counts (spec 23.3): its counted uses are copies (walkBorrowed), and the - // release of the old value a counted store makes, right before it stores the retained - // read, cannot free the block read: it holds the retain's count by then - // (`Shared.count(q.child = p.child)`). - auto countedHandle = isHandle(result) && handleReadUses(result).counted; - llvm::SmallPtrSet countedStoreReleases; - if (countedHandle) - { - collectCountedStoreReleases(result, countedStoreReleases); - } - - llvm::StringRef name = ownerName(result); - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - mlir_ts::VariableOp letKeeps; - mlir::Operation *letKept = nullptr; - // A result that borrows arrives with rc's temporary release, or rc's consuming `let`, and - // in a function returning a borrow of the same parameter it may go out with rc's retain. - Keeping keeping{&bookkeeping, returnsBorrowOf >= 0 && borrowedParam(result) == returnsBorrowOf}; - auto *kept = walkBorrowed(result, read, uses, [&](mlir::Operation *user, - llvm::ArrayRef kills) { - auto borrower = borrowerOf(user, /*consumed=*/true); - if (!borrower || borrower.getOperation() != user) - { - return false; - } - - name = varName(borrower); - if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) - { - letKept = keeps; - letKeeps = borrower; - } - - return true; - }, keeping, countedHandle); - - if (kept || letKept) - { - if (!kept && isAssignmentOf(letKept, letKeeps)) - { - reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); - } - else - { - reportPlaceBorrowEscapes(kept ? kept : letKept, name, place); - } - - return false; - } - - auto chain = chainOf(read); - // A call given the borrow is a use of it for as long as the callee runs, so a call that - // may destroy it is a use after that, whatever the order of its arguments. The function - // value it goes through is only read as it starts. - auto outlives = [&](mlir::Operation *drop) { - for (auto &use : uses) - { - if ((use.op != drop || !use.callee) && reachableAfter(drop, use.op, use.kills)) - { - reportPlaceBorrowOutlives(use.op, drop, name, place); - return true; - } - } - - return false; - }; - - for (auto *end : rootEnds(chain)) - { - if (outlives(end)) - { - return false; - } - } - - if (keptParamOutlives(chain, outlives)) - { - return false; - } - - for (auto *drop : drops) - { - if (countedStoreReleases.contains(drop)) - { - continue; - } - - if (dropsChain(drop, chain, read) && outlives(drop)) - { - return false; - } - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - return true; - } + bool checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase); // A local that holds a borrow through `user`, and the field path under it that holds it (empty: // the local's value itself): a store of it into the local, or into a field of the local's value @@ -1785,105 +1056,14 @@ class OwnershipInferencePass // into anything that is not such a place (a call that keeps it, a return, a push) ends the borrow // there. So do the releases of the parameter's slot (spec 33's exit releases). True when some use // of the borrow comes after one of these (`outlives` reported it). - bool keptParamOutlives(const Chain &chain, llvm::function_ref outlives) - { - for (auto &root : chain.roots) - { - auto load = keptParamLoadOf(root.first); - if (!load) - { - continue; - } - - // the moves of the parameter: what takes a read of its slot - llvm::SmallVector> takers; - for (auto *user : load.getReference().getUsers()) - { - if (mlir::isa(user)) - { - if (outlives(user)) - { - return true; - } - - continue; - } - - auto readOp = mlir::dyn_cast(user); - if (!readOp) - { - continue; - } - - forEachUse(readOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use)) - { - auto *taker = takerOf(use, used); - takers.push_back({taker ? taker : use, used}); - } - else if ((varOp && isOwningVariable(varOp)) || (isCall(use) && isKeptArgument(use, used))) - { - takers.push_back({use, used}); - } - }); - } - - for (auto [taker, moved] : takers) - { - Chain owner; - if (!destinationChain(taker, owner)) - { - if (outlives(taker)) - { - return true; - } - - continue; - } - - auto afterMove = [&](mlir::Operation *end) { - return end != taker && reachableAfter(taker, end, llvm::ArrayRef{}) && outlives(end); - }; - - for (auto *end : rootEnds(owner)) - { - if (afterMove(end)) - { - return true; - } - } - - for (auto *drop : drops) - { - if (dropsChain(drop, owner, nullptr, moved) && afterMove(drop)) - { - return true; - } - } - } - } - - return false; - } + bool keptParamOutlives(const Chain &chain, llvm::function_ref outlives); // A parameter, or a view of one (`u`, the payload `ts.Unbox` reads out of an `any`), that // this function returns as a borrow (`__own_result_borrows`). rc retained it for the caller, // who now takes no reference: the retain goes, and nothing may keep it. The caller's own // borrow of the argument bounds it; nothing here can end that but a call, and a call that may // is the caller's use of what it passed (spec 2.4, phase 4). - void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase) - { - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - if (auto *kept = walkBorrowed(value, {}, uses, nullptr, Keeping{&bookkeeping, true})) - { - reportBorrowEscapes(kept, ownerName(value), "a parameter"); - return; - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - } + void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase); // `the result of 'first'`, for a call whose result borrows an argument. static std::string describeCall(mlir::Operation *call) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp new file mode 100644 index 000000000..7a004e6e2 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -0,0 +1,867 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): reads out of containers (spec 2.3, +// phase 3) - handle reads, the chain from a read to its roots, and what may drop it. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + mlir::Operation *OwnershipInferencePass::placeReadOf(mlir::Value value) + { + auto root = rootOf(value); + auto *def = root.getDefiningOp(); + // a handle read out of a place that rc gives an owner of its own is a counted copy, not a + // borrow (spec 23.3); any other is a borrow of the place, as a field read is + if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) + { + return nullptr; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) + { + // a field of a generator's borrowing box: only its maker fills it, and nothing a call + // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) + if (isBorrowingBoxField(loadOp.getReference())) + { + return nullptr; + } + + return def; + } + + return borrowingCall(root); + } + + OwnershipInferencePass::HandleReadUses OwnershipInferencePass::handleReadUses(mlir::Value value) + { + HandleReadUses result{false, false}; + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + // a return and an `any` box take the value, though passesOn would hand them on + if (isHandOff(user, current)) + { + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + continue; + } + + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + // what is no longer a handle is no receiver: a test (`if (p.child)`) only + // reads it + if (!isHandle(next)) + { + result.borrowed = true; + } + else if (seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + } + } + + return result; + } + + bool OwnershipInferencePass::isCountedHandleRead(mlir::Value value) + { + auto uses = handleReadUses(value); + return uses.counted && !uses.borrowed; + } + + bool OwnershipInferencePass::isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return true; + } + + if (auto varOp = mlir::dyn_cast(user)) + { + return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { + return mlir::isa(varUser); + }); + } + + if (auto storeOp = mlir::dyn_cast(user)) + { + return storeOp.getValue() == value && countedStoreRetain(storeOp); + } + + if (auto pushOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, pushOp.getItems(), value); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, unshiftOp.getItems(), value); + } + + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); + } + + // `ts.ReturnVal` is the return before the affine lowering, which makes it a store into + // the result slot + if (mlir::isa(user)) + { + return retainedRightBefore(user, user->getOperands(), value); + } + + return false; + } + + bool OwnershipInferencePass::isHandOff(mlir::Operation *user, mlir::Value value) + { + if (auto storeOp = mlir::dyn_cast(user)) + { + auto slot = storeOp.getReference().getDefiningOp(); + return storeOp.getValue() == value && slot && isResultSlot(slot); + } + + auto castOp = mlir::dyn_cast(user); + return castOp && castOp.getIn() == value && mlir::isa(castOp.getType()); + } + + bool OwnershipInferencePass::isCountedHandOff(mlir::Operation *user, mlir::Value value) + { + return isHandOff(user, value) && isCountedHandleReceiver(user, value); + } + + bool OwnershipInferencePass::retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value) + { + auto needed = llvm::count(operands, value); + if (needed == 0) + { + return false; + } + + for (auto *previous = op->getPrevNode(); needed > 0 && previous; previous = previous->getPrevNode()) + { + auto retainOp = mlir::dyn_cast(previous); + if (!retainOp) + { + break; + } + + if (retainOp.getReference() == value) + { + --needed; + } + } + + return needed == 0; + } + + void OwnershipInferencePass::collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases) + { + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + if (isHandle(next) && seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + auto storeOp = mlir::dyn_cast(use.getOwner()); + if (!storeOp || storeOp.getValue() != current || !countedStoreRetain(storeOp)) + { + continue; + } + + if (auto releaseSlotOp = mlir::dyn_cast_or_null(storeOp->getPrevNode())) + { + releases.insert(releaseSlotOp); + } + } + } + } + + mlir::Operation *OwnershipInferencePass::countedStoreRetain(mlir_ts::StoreOp storeOp) + { + auto *previous = storeOp->getPrevNode(); + if (auto slot = storeOp.getReference().getDefiningOp(); slot && isResultSlot(slot)) + { + // the scope exit's releases, of locals and of their slots, in any order + while (mlir::isa_and_nonnull(previous) && + previous->getOperand(0) != storeOp.getValue()) + { + previous = previous->getPrevNode(); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast_or_null(previous)) + { + previous = releaseSlotOp.getSlot() == storeOp.getReference() ? releaseSlotOp->getPrevNode() : nullptr; + } + + auto retainOp = mlir::dyn_cast_or_null(previous); + return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; + } + + mlir::Operation *OwnershipInferencePass::borrowingCall(mlir::Value value) + { + auto *def = value.getDefiningOp(); + if (!def || !isCall(def) || resultBorrows(def) < 0) + { + return nullptr; + } + + auto args = callArgs(def); + auto indices = resultBorrowsAll(def); + return llvm::all_of(indices, [&](int index) { return static_cast(index) < args.size(); }) ? def : nullptr; + } + + OwnershipInferencePass::Chain OwnershipInferencePass::chainOf(mlir::Operation *read, mlir::Value place) + { + Chain chain; + llvm::SmallVector refs; + llvm::SmallVector first; + if (place) + { + refs.push_back(place); + } + else if (auto loadOp = mlir::dyn_cast(read)) + { + refs.push_back(loadOp.getReference()); + } + else + { + // a call's result that borrows its arguments - a plain borrow, or a borrowing record's box + // (spec 32), which may hold a borrow of each - is somewhere under each of them + chain.anyPlace = true; + auto args = callArgs(read); + for (auto index : resultBorrowsAll(read)) + { + if (static_cast(index) < args.size() && !argumentHoldsNoBlock(args[index]) && + !isImmortalLiteral(args[index])) + { + first.push_back(rootOf(args[index])); + } + } + } + + llvm::SmallPtrSet seen; + // the bases a `s.value` was read through, and what merges into them + llvm::SmallPtrSet handleBases; + while (!refs.empty() || !first.empty()) + { + llvm::SmallVector bases; + if (!first.empty()) + { + bases.swap(first); + } + else + { + auto ref = refs.pop_back_val(); + auto place = placeBase(ref); + assert(place && "chainOf walks places only"); + switch (place.kind) + { + case PlaceKind::Field: + chain.places.push_back({ref.getDefiningOp().getPosition(), ref.getType()}); + break; + case PlaceKind::SharedValue: + chain.places.push_back({Chain::SharedValue, ref.getType()}); + handleBases.insert(rootOf(place.base)); + break; + case PlaceKind::InterfaceField: + chain.places.push_back({Chain::InterfaceField, ref.getType()}); + break; + default: + chain.places.push_back({-1, ref.getType()}); + break; + } + + bases.push_back(rootOf(place.base)); + } + + while (!bases.empty()) + { + auto base = bases.pop_back_val(); + if (!seen.insert(base).second) + { + continue; + } + + // A handle (spec 23.3): other handles may reach its block, so this function owns + // nothing under it. A handle read out of a place still has that place above it, and + // one merged from several has each of them. + if (handleBases.contains(base) || isHandle(base)) + { + llvm::SmallVector merged; + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, merged)) + { + handleBases.insert(merged.begin(), merged.end()); + bases.append(merged.begin(), merged.end()); + continue; + } + + chain.roots.push_back({base, Chain::NotOwned}); + chain.handles.push_back(base); + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + } + + continue; + } + + if (isPlace(base)) + { + refs.push_back(base); // a field of a value-type field: the same object + continue; + } + + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + continue; + } + + // the object a method is called on + if (auto object = boundThis(base)) + { + bases.push_back(rootOf(object)); + continue; + } + + // a result that borrows an argument is somewhere under the argument + if (auto *call = borrowingCall(base)) + { + chain.anyPlace = true; + auto args = callArgs(call); + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) < args.size()) + { + bases.push_back(rootOf(args[index])); + } + } + + continue; + } + + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, bases)) + { + continue; + } + + if (base.getDefiningOp()) + { + chain.roots.push_back({base, Chain::Slot}); + } + else if (auto slotLoad = slotLoadOf(base)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else + { + chain.roots.push_back({base, isFresh(base) ? Chain::Owned : Chain::NotOwned}); + } + } + } + + return chain; + } + + llvm::SmallVector OwnershipInferencePass::rootEnds(const Chain &chain) + { + llvm::SmallVector ends; + for (auto [root, kind] : chain.roots) + { + // A handle (spec 23.3), never owned: where this one is given back or overwritten - a + // local's release and assignments, a temporary's release; a global's assignments are + // below. A copy of it is counted, and moves nothing away. + if (llvm::is_contained(chain.handles, root)) + { + auto loadOp = root.getDefiningOp(); + if (auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp()) + { + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + } + else if (!loadOp) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value) { + if (mlir::isa(use)) + { + ends.push_back(use); + } + }); + } + } + + if (kind == Chain::Slot) + { + for (auto *user : root.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + else if (auto loadOp = mlir::dyn_cast(user)) + { + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use) || (varOp && isOwningVariable(varOp)) || + (isCall(use) && isKeptArgument(use, used))) + { + ends.push_back(use); + } + }); + } + } + } + else if (kind == Chain::Owned) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { + if (mlir::isa(use) || !isBorrow(use, used)) + { + ends.push_back(use); + } + }); + } + else if (auto loadOp = root.getDefiningOp()) + { + if (auto global = loadOp.getReference().getDefiningOp()) + { + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global.getGlobalName()) + { + ends.push_back(op); + } + }); + } + } + } + + return ends; + } + + llvm::SmallVector OwnershipInferencePass::rootSources(const Chain &chain) + { + llvm::SmallVector sources; + for (auto &root : chain.roots) + { + auto value = root.first; + sources.push_back(value); + for (auto steps = 0; steps < 8; ++steps) + { + auto varOp = value.getDefiningOp(); + if (!varOp || !varOp.getInitializer()) + { + break; + } + + auto initializer = rootOf(varOp.getInitializer()); + auto slotLoad = slotLoadOf(initializer); + value = slotLoad ? slotLoad.getReference() : initializer; + sources.push_back(value); + } + } + + return sources; + } + + const llvm::DenseSet &OwnershipInferencePass::derivedFrom(mlir::Value start) + { + if (auto found = derivedCache.find(start); found != derivedCache.end()) + { + return *found->second; + } + + auto &slot = derivedCache[start]; + slot = std::make_shared>(); + auto &derived = *slot; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto value = work.pop_back_val(); + if (!derived.insert(value).second) + { + continue; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (isCall(user)) + { + // ...but a borrowing record given this value holds it (spec 32) + auto args = callArgs(user); + if (isRecordResult(user) && llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + })) + { + work.push_back(user->getResult(0)); + } + + continue; + } + + llvm::SmallVector next(user->getResults().begin(), user->getResults().end()); + passesOn(use, next); + for (auto result : next) + { + if (mayPointInto(result)) + { + work.push_back(result); + } + } + } + } + + return derived; + } + + bool OwnershipInferencePass::dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed) + { + if (read && drop == read) + { + return false; // a call that returns a borrow has not destroyed it by returning it + } + + if (chain.anyPlace && !isCall(drop)) + { + return true; + } + + if (auto releaseSlotOp = mlir::dyn_cast(drop)) + { + // An assignment to a captured variable destroys its old value, which is reached only + // through a read of the variable's cell: a root this function does not own. + auto slot = releaseSlotOp.getSlot(); + if (isCellVariable(slot) || isLoadedCell(slot)) + { + return !chain.owned(); + } + + // An assignment to a global destroys its old value and all that value owns. A root this + // function does not own - `this`, a parameter, a global's or a field's value - may be + // that very block, or under it (spec 29). A root this function made cannot. An immortal + // global's one store is its initializer, whose old value is the empty one (spec 25). + if (auto addressOfOp = slot.getDefiningOp()) + { + return !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && !chain.owned(); + } + + // `x.value = ...` through any handle, of any type, may replace the payload another + // handle reads (spec 23.3): a handle converts to one of a base class's type + if (slot.getDefiningOp()) + { + return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); + } + + auto isField = [](int64_t position) { return position >= 0 || position == Chain::InterfaceField; }; + int64_t position = -1; + if (auto propertyRefOp = slot.getDefiningOp()) + { + position = propertyRefOp.getPosition(); + } + else if (isInterfaceField(slot)) + { + position = Chain::InterfaceField; + } + + // A field reached through an interface may be any field of the object under it, of any + // type (a class may declare the field narrower than the interface): an overwrite through + // an interface drops every field on the chain, and any field's overwrite drops a field + // read through an interface (spec 28). + return llvm::any_of(chain.places, [&](auto &place) { + if (isField(position) && isField(place.first) && + (position == Chain::InterfaceField || place.first == Chain::InterfaceField)) + { + return true; + } + + return place.first == position && place.second == slot.getType(); + }); + } + + if (isCall(drop)) + { + if (callNoDrops(drop)) + { + return false; + } + + // a call given a closure may run it, and its box may hold anything + if (!chain.owned() || llvm::any_of(drop->getOperands(), holdsCaptures)) + { + return true; + } + + auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); + auto sources = rootSources(chain); + return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { + return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { + return derivedFrom(source).contains(operand); + }); + }); + } + + // pop, shift, splice, `length =`; push and unshift on an array reached through a handle + auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); + auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); + if (!arrayType) + { + return true; + } + + return llvm::any_of(chain.places, [&](auto &place) { + auto placeType = mlir::dyn_cast(place.second); + return place.first == -1 && placeType && placeType.getElementType() == arrayType.getElementType(); + }); + } + + bool OwnershipInferencePass::checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase) + { + auto loadOp = mlir::dyn_cast(read); + auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); + auto result = read->getResult(0); + // A handle read rc counts (spec 23.3): its counted uses are copies (walkBorrowed), and the + // release of the old value a counted store makes, right before it stores the retained + // read, cannot free the block read: it holds the retain's count by then + // (`Shared.count(q.child = p.child)`). + auto countedHandle = isHandle(result) && handleReadUses(result).counted; + llvm::SmallPtrSet countedStoreReleases; + if (countedHandle) + { + collectCountedStoreReleases(result, countedStoreReleases); + } + + llvm::StringRef name = ownerName(result); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + mlir_ts::VariableOp letKeeps; + mlir::Operation *letKept = nullptr; + // A result that borrows arrives with rc's temporary release, or rc's consuming `let`, and + // in a function returning a borrow of the same parameter it may go out with rc's retain. + Keeping keeping{&bookkeeping, returnsBorrowOf >= 0 && borrowedParam(result) == returnsBorrowOf}; + auto *kept = walkBorrowed(result, read, uses, [&](mlir::Operation *user, + llvm::ArrayRef kills) { + auto borrower = borrowerOf(user, /*consumed=*/true); + if (!borrower || borrower.getOperation() != user) + { + return false; + } + + name = varName(borrower); + if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) + { + letKept = keeps; + letKeeps = borrower; + } + + return true; + }, keeping, countedHandle); + + if (kept || letKept) + { + if (!kept && isAssignmentOf(letKept, letKeeps)) + { + reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); + } + else + { + reportPlaceBorrowEscapes(kept ? kept : letKept, name, place); + } + + return false; + } + + auto chain = chainOf(read); + // A call given the borrow is a use of it for as long as the callee runs, so a call that + // may destroy it is a use after that, whatever the order of its arguments. The function + // value it goes through is only read as it starts. + auto outlives = [&](mlir::Operation *drop) { + for (auto &use : uses) + { + if ((use.op != drop || !use.callee) && reachableAfter(drop, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, drop, name, place); + return true; + } + } + + return false; + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + if (keptParamOutlives(chain, outlives)) + { + return false; + } + + for (auto *drop : drops) + { + if (countedStoreReleases.contains(drop)) + { + continue; + } + + if (dropsChain(drop, chain, read) && outlives(drop)) + { + return false; + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + bool OwnershipInferencePass::keptParamOutlives(const Chain &chain, llvm::function_ref outlives) + { + for (auto &root : chain.roots) + { + auto load = keptParamLoadOf(root.first); + if (!load) + { + continue; + } + + // the moves of the parameter: what takes a read of its slot + llvm::SmallVector> takers; + for (auto *user : load.getReference().getUsers()) + { + if (mlir::isa(user)) + { + if (outlives(user)) + { + return true; + } + + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + continue; + } + + forEachUse(readOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use)) + { + auto *taker = takerOf(use, used); + takers.push_back({taker ? taker : use, used}); + } + else if ((varOp && isOwningVariable(varOp)) || (isCall(use) && isKeptArgument(use, used))) + { + takers.push_back({use, used}); + } + }); + } + + for (auto [taker, moved] : takers) + { + Chain owner; + if (!destinationChain(taker, owner)) + { + if (outlives(taker)) + { + return true; + } + + continue; + } + + auto afterMove = [&](mlir::Operation *end) { + return end != taker && reachableAfter(taker, end, llvm::ArrayRef{}) && outlives(end); + }; + + for (auto *end : rootEnds(owner)) + { + if (afterMove(end)) + { + return true; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, owner, nullptr, moved) && afterMove(drop)) + { + return true; + } + } + } + } + + return false; + } + + void OwnershipInferencePass::checkReturnedParam(mlir::Value value, llvm::SetVector &toErase) + { + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + if (auto *kept = walkBorrowed(value, {}, uses, nullptr, Keeping{&bookkeeping, true})) + { + reportBorrowEscapes(kept, ownerName(value), "a parameter"); + return; + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + } + +} // namespace own_inference From a4f37d41fe581b4fb5fcceb244d574615560bc47 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:36:24 +0100 Subject: [PATCH 21/25] own passes: OwnershipInferenceGlobalReads.cpp out of OwnershipInferencePass.h (I1) A read of a mortal global, ended at the global's assignment, and the locals that hold it (spec 25). A pure move of borrowHolderOf, holderRenewals, pathsMeet, isGlobalRead, checkGlobalRead. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipInferenceGlobalReads.cpp | 265 ++++++++++++++++++ .../lib/TypeScript/OwnershipInferencePass.h | 254 +---------------- 3 files changed, 271 insertions(+), 249 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceGlobalReads.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 967fce9fc..dcc988141 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -35,6 +35,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferencePass.cpp OwnershipInferenceBorrowers.cpp OwnershipInferenceClosures.cpp + OwnershipInferenceGlobalReads.cpp OwnershipInferenceKeptParams.cpp OwnershipInferencePlaceReads.cpp OwnershipInferenceRecords.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferenceGlobalReads.cpp b/tslang/lib/TypeScript/OwnershipInferenceGlobalReads.cpp new file mode 100644 index 000000000..bcbbcee57 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceGlobalReads.cpp @@ -0,0 +1,265 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): a read of a mortal global, ended at the +// global's assignment, and the locals that hold it (spec 25). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + std::optional OwnershipInferencePass::borrowHolderOf(mlir::Operation *user) + { + BorrowHolder holder; + mlir::Value ref; + if (auto storeOp = mlir::dyn_cast(user)) + { + ref = storeOp.getReference(); + while (auto propertyRefOp = ref.getDefiningOp()) + { + holder.path.insert(holder.path.begin(), propertyRefOp.getPosition()); + ref = propertyRefOp.getObjectRef(); + } + } + else if (auto varOp = mlir::dyn_cast(user)) + { + ref = varOp.getResult(); + } + + auto varOp = ref ? ref.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || varOp.getCaptured().value_or(false) || isCellVariable(varOp.getResult())) + { + return std::nullopt; + } + + holder.local = varOp; + return holder; + } + + llvm::SmallVector OwnershipInferencePass::holderRenewals(BorrowHolder holder) + { + llvm::SmallVector renewals; + if (holder.local.getInitializer()) + { + renewals.push_back(holder.local); + } + + llvm::SmallVector> refs{{holder.local.getResult(), 0}}; + while (!refs.empty()) + { + auto [ref, depth] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + if (depth < holder.path.size() && propertyRefOp.getPosition() == holder.path[depth]) + { + refs.push_back({propertyRefOp.getResult(), depth + 1}); + } + } + else if (auto storeOp = mlir::dyn_cast(refUser); storeOp && storeOp.getReference() == ref) + { + renewals.push_back(storeOp); + } + } + } + + return renewals; + } + + bool OwnershipInferencePass::pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b) + { + auto common = std::min(a.size(), b.size()); + return a.take_front(common) == b.take_front(common); + } + + bool OwnershipInferencePass::isGlobalRead(mlir_ts::LoadOp loadOp) + { + auto addressOfOp = loadOp.getReference().getDefiningOp(); + auto result = loadOp.getResult(); + return addressOfOp && !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && ownsHeap(result) && !isHandle(result); + } + + void OwnershipInferencePass::checkGlobalRead(mlir_ts::LoadOp read) + { + // each access of a global is a `ts.AddressOf` of its own + auto global = read.getReference().getDefiningOp().getGlobalName(); + llvm::SmallVector uses; + llvm::StringSet<> followed; + mlir::Operation *keptAt = nullptr; + // rc's temporary release of a call's result that borrows an argument goes with that result + // (walkBorrowed's `keeping`); the owner checks decide it + llvm::SmallVector bookkeeping; + Keeping keeping{&bookkeeping, false}; + std::function)> intoLocal; + + // the uses of what `holder` holds, made again at its renewals: those depend on the holder + // alone, not on the way the walk reached it, so a holder is followed once. A value loaded + // out of it is made again only by a path back through that load (the load itself is a use, + // bounded by the renewals): a store into the path after the load gives the loaded value + // nothing new. + auto followHolder = [&](BorrowHolder holder) { + std::string key; + llvm::raw_string_ostream keyOut(key); + keyOut << holder.local.getOperation(); + for (auto position : holder.path) + { + keyOut << "." << position; + } + + if (!followed.insert(keyOut.str()).second) + { + return true; + } + + auto localKills = holderRenewals(holder); + llvm::SmallVector>> refs{{holder.local.getResult(), {}}}; + while (!refs.empty()) + { + auto [ref, path] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + auto fieldPath = path; + fieldPath.push_back(propertyRefOp.getPosition()); + if (pathsMeet(fieldPath, holder.path)) + { + refs.push_back({propertyRefOp.getResult(), fieldPath}); + } + + continue; + } + + if (ref == holder.local.getResult() && + mlir::isa(refUser)) + { + continue; + } + + uses.push_back({refUser, {localKills.begin(), localKills.end()}}); + if (auto loadOp = mlir::dyn_cast(refUser)) + { + if (auto *kept = walkBorrowed(loadOp.getResult(), loadOp.getOperation(), uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + } + } + + return true; + }; + + intoLocal = [&](mlir::Operation *user, llvm::ArrayRef kills) { + // a tuple taken apart (`{ ...b }`, a destructuring): each field is the borrow again + if (mlir::isa(user)) + { + for (auto result : user->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + if (auto *kept = walkBorrowed(result, kills, uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + + return true; + } + + auto holder = borrowHolderOf(user); + return holder && followHolder(*holder); + }; + + auto *kept = walkBorrowed(read.getResult(), read.getOperation(), uses, intoLocal, keeping); + + // a value made from the borrow (a field of a tuple read out of a holder, `{ ...b }`) stored + // into another local, or declaring one: that local holds the borrow too + for (size_t index = 0; !kept && !keptAt && index < uses.size(); index++) + { + auto *op = uses[index].op; + auto storeOp = mlir::dyn_cast(op); + auto varOp = mlir::dyn_cast(op); + auto value = storeOp ? storeOp.getValue() : varOp ? varOp.getInitializer() : mlir::Value(); + if (!value || !mayPointInto(value)) + { + continue; + } + + if (auto holder = borrowHolderOf(op)) + { + followHolder(*holder); + } + } + + if (kept || keptAt) + { + if (mlir::isa(read.getType())) + { + return; + } + + if (reporting()) + { + auto *at = keptAt ? keptAt : kept; + at->emitError("the value read from '") + << global << "' is kept where its end at an assignment of '" << global + << "' cannot be checked; -mm=own cannot prove it outlives the global's value"; + signalPassFailure(); + } + + return; + } + + llvm::SmallVector ends; + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global) + { + ends.push_back(op); + } + + // a call that may give the global a new value (OwnershipSignaturePass, pinAssignedGlobals): + // `const c = G; reset(); print(c.x)` + if (isCall(op)) + { + auto assigned = op->getAttrOfType(OWN_ASSIGNS_GLOBALS_ATTR_NAME); + if (op->hasAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME) || + (assigned && llvm::any_of(assigned.getAsRange(), + [&](mlir::StringAttr name) { return name.getValue() == global; }))) + { + ends.push_back(op); + } + } + }); + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (use.op != end && reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, ownerName(read.getResult()), global); + return; + } + } + } + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 0d8b601ac..820bb90a2 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -765,85 +765,21 @@ class OwnershipInferencePass llvm::SmallVector path; }; - static std::optional borrowHolderOf(mlir::Operation *user) - { - BorrowHolder holder; - mlir::Value ref; - if (auto storeOp = mlir::dyn_cast(user)) - { - ref = storeOp.getReference(); - while (auto propertyRefOp = ref.getDefiningOp()) - { - holder.path.insert(holder.path.begin(), propertyRefOp.getPosition()); - ref = propertyRefOp.getObjectRef(); - } - } - else if (auto varOp = mlir::dyn_cast(user)) - { - ref = varOp.getResult(); - } - - auto varOp = ref ? ref.getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp || varOp.getCaptured().value_or(false) || isCellVariable(varOp.getResult())) - { - return std::nullopt; - } - - holder.local = varOp; - return holder; - } + static std::optional borrowHolderOf(mlir::Operation *user); // Where the holder is given a new value: the local's declaration when it has an initializer // (`let l!: V` stores nothing, and in a loop the slot keeps the last round's value: fix-up // re-review N4), and every store into the field path that holds the borrow or into a prefix of // it (the whole local, `o.p` for `o.p.v`). A store into a sibling field, or below the path, // gives the path no new value. - static llvm::SmallVector holderRenewals(BorrowHolder holder) - { - llvm::SmallVector renewals; - if (holder.local.getInitializer()) - { - renewals.push_back(holder.local); - } - - llvm::SmallVector> refs{{holder.local.getResult(), 0}}; - while (!refs.empty()) - { - auto [ref, depth] = refs.pop_back_val(); - for (auto *refUser : ref.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(refUser)) - { - if (depth < holder.path.size() && propertyRefOp.getPosition() == holder.path[depth]) - { - refs.push_back({propertyRefOp.getResult(), depth + 1}); - } - } - else if (auto storeOp = mlir::dyn_cast(refUser); storeOp && storeOp.getReference() == ref) - { - renewals.push_back(storeOp); - } - } - } - - return renewals; - } + static llvm::SmallVector holderRenewals(BorrowHolder holder); // Whether one field path is on the way to the other: a read of either may reach the other's value. - static bool pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b) - { - auto common = std::min(a.size(), b.size()); - return a.take_front(common) == b.take_front(common); - } + static bool pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b); // A read of what a mortal global holds (spec 25: an immortal one's one store is its // initializer): a heap value it owns, not a handle (spec 23.3 counts those). - bool isGlobalRead(mlir_ts::LoadOp loadOp) - { - auto addressOfOp = loadOp.getReference().getDefiningOp(); - auto result = loadOp.getResult(); - return addressOfOp && !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && ownsHeap(result) && !isHandle(result); - } + bool isGlobalRead(mlir_ts::LoadOp loadOp); // Such a read is a borrow of the value the global owns - `const c = K.w` names the read itself, // with no retain - and an assignment of the global releases it. So no use of the read, nor of @@ -867,187 +803,7 @@ class OwnershipInferencePass // that one a holder too. Any other keep - a field of an object, an array, a global, a callee // that keeps it, a capture, a return - cannot be bounded here and is an error. A string read is // copied where it is kept (spec 22), so its keeps are not this check's. - void checkGlobalRead(mlir_ts::LoadOp read) - { - // each access of a global is a `ts.AddressOf` of its own - auto global = read.getReference().getDefiningOp().getGlobalName(); - llvm::SmallVector uses; - llvm::StringSet<> followed; - mlir::Operation *keptAt = nullptr; - // rc's temporary release of a call's result that borrows an argument goes with that result - // (walkBorrowed's `keeping`); the owner checks decide it - llvm::SmallVector bookkeeping; - Keeping keeping{&bookkeeping, false}; - std::function)> intoLocal; - - // the uses of what `holder` holds, made again at its renewals: those depend on the holder - // alone, not on the way the walk reached it, so a holder is followed once. A value loaded - // out of it is made again only by a path back through that load (the load itself is a use, - // bounded by the renewals): a store into the path after the load gives the loaded value - // nothing new. - auto followHolder = [&](BorrowHolder holder) { - std::string key; - llvm::raw_string_ostream keyOut(key); - keyOut << holder.local.getOperation(); - for (auto position : holder.path) - { - keyOut << "." << position; - } - - if (!followed.insert(keyOut.str()).second) - { - return true; - } - - auto localKills = holderRenewals(holder); - llvm::SmallVector>> refs{{holder.local.getResult(), {}}}; - while (!refs.empty()) - { - auto [ref, path] = refs.pop_back_val(); - for (auto *refUser : ref.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(refUser)) - { - auto fieldPath = path; - fieldPath.push_back(propertyRefOp.getPosition()); - if (pathsMeet(fieldPath, holder.path)) - { - refs.push_back({propertyRefOp.getResult(), fieldPath}); - } - - continue; - } - - if (ref == holder.local.getResult() && - mlir::isa(refUser)) - { - continue; - } - - uses.push_back({refUser, {localKills.begin(), localKills.end()}}); - if (auto loadOp = mlir::dyn_cast(refUser)) - { - if (auto *kept = walkBorrowed(loadOp.getResult(), loadOp.getOperation(), uses, intoLocal, keeping)) - { - keptAt = keptAt ? keptAt : kept; - return false; - } - } - } - } - - return true; - }; - - intoLocal = [&](mlir::Operation *user, llvm::ArrayRef kills) { - // a tuple taken apart (`{ ...b }`, a destructuring): each field is the borrow again - if (mlir::isa(user)) - { - for (auto result : user->getResults()) - { - if (!mayPointInto(result)) - { - continue; - } - - if (auto *kept = walkBorrowed(result, kills, uses, intoLocal, keeping)) - { - keptAt = keptAt ? keptAt : kept; - return false; - } - } - - return true; - } - - auto holder = borrowHolderOf(user); - return holder && followHolder(*holder); - }; - - auto *kept = walkBorrowed(read.getResult(), read.getOperation(), uses, intoLocal, keeping); - - // a value made from the borrow (a field of a tuple read out of a holder, `{ ...b }`) stored - // into another local, or declaring one: that local holds the borrow too - for (size_t index = 0; !kept && !keptAt && index < uses.size(); index++) - { - auto *op = uses[index].op; - auto storeOp = mlir::dyn_cast(op); - auto varOp = mlir::dyn_cast(op); - auto value = storeOp ? storeOp.getValue() : varOp ? varOp.getInitializer() : mlir::Value(); - if (!value || !mayPointInto(value)) - { - continue; - } - - if (auto holder = borrowHolderOf(op)) - { - followHolder(*holder); - } - } - - if (kept || keptAt) - { - if (mlir::isa(read.getType())) - { - return; - } - - if (reporting()) - { - auto *at = keptAt ? keptAt : kept; - at->emitError("the value read from '") - << global << "' is kept where its end at an assignment of '" << global - << "' cannot be checked; -mm=own cannot prove it outlives the global's value"; - signalPassFailure(); - } - - return; - } - - llvm::SmallVector ends; - getFunction().walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto releaseSlotOp = mlir::dyn_cast(op)) - { - slot = releaseSlotOp.getSlot(); - } - else if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - - auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); - if (other && other.getGlobalName() == global) - { - ends.push_back(op); - } - - // a call that may give the global a new value (OwnershipSignaturePass, pinAssignedGlobals): - // `const c = G; reset(); print(c.x)` - if (isCall(op)) - { - auto assigned = op->getAttrOfType(OWN_ASSIGNS_GLOBALS_ATTR_NAME); - if (op->hasAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME) || - (assigned && llvm::any_of(assigned.getAsRange(), - [&](mlir::StringAttr name) { return name.getValue() == global; }))) - { - ends.push_back(op); - } - } - }); - - for (auto *end : ends) - { - for (auto &use : uses) - { - if (use.op != end && reachableAfter(end, use.op, use.kills)) - { - reportBorrowOutlives(use.op, end, ownerName(read.getResult()), global); - return; - } - } - } - } + void checkGlobalRead(mlir_ts::LoadOp read); // A borrow read out of a parameter this function keeps, taken before the parameter moves into a // new owner (spec 33, review fix): from the move on it is bounded by that owner, as a read through From ad5bc5b8d8e6623a97c7298dce19a2f09daffd2f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:36:59 +0100 Subject: [PATCH 22/25] own passes: OwnershipInferenceMoves.cpp out of OwnershipInferencePass.h (I1) Moves - the one taker of a fresh value, the retains that are its bookkeeping, and the paths that use it after the move. A pure move of slotHoldsInitializerAt, retainedValue, ownsHeap, holdsConstantsOrMoves, recordRetainOf, isBuiltArrayRead, isBuiltArrayReturned, onlyRetainSlotOf, isResultSlot, recordRetainOfStore, isImmortalLiteral, isBorrow, isBoundForCall, isKeptArgument, checkValueMoves, checkReleasedOnce, movesOf, callGivenAgain, takerLoops, reachableAfter, orderedInBody, takerOf, soleUser, twinReadAfter, checkSlotMove. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../TypeScript/OwnershipInferenceMoves.cpp | 1081 +++++++++++++++++ .../lib/TypeScript/OwnershipInferencePass.h | 1071 +--------------- 3 files changed, 1110 insertions(+), 1043 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceMoves.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index dcc988141..f0f6cb12f 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -37,6 +37,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferenceClosures.cpp OwnershipInferenceGlobalReads.cpp OwnershipInferenceKeptParams.cpp + OwnershipInferenceMoves.cpp OwnershipInferencePlaceReads.cpp OwnershipInferenceRecords.cpp OwnershipSignaturePass.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp new file mode 100644 index 000000000..7bfcbde41 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -0,0 +1,1081 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): moves - the one taker of a fresh value, +// the retains that are its bookkeeping, and the paths that use it after the move. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::slotHoldsInitializerAt(mlir::Operation *op) + { + auto slot = mlir::cast(op).getSlot(); + auto varOp = slot.getDefiningOp(); + if (!varOp || !varOp.getInitializer() || isCellVariable(slot) || varOp->getBlock() != op->getBlock()) + { + return false; + } + + for (auto *between = varOp->getNextNode(); between; between = between->getNextNode()) + { + if (between == op) + { + return true; + } + + if (llvm::is_contained(between->getOperands(), slot) || isCall(between) || between->getNumRegions() > 0) + { + return false; + } + } + + return false; + } + + mlir::Value OwnershipInferencePass::retainedValue(mlir::Operation *op) + { + if (auto retainOp = mlir::dyn_cast(op)) + { + return rootOf(retainOp.getReference()); + } + + auto varOp = mlir::cast(op).getSlot().getDefiningOp(); + return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); + } + + bool OwnershipInferencePass::ownsHeap(mlir::Value value) + { + // a `value_ref` points at storage it does not own, but one `ts.New` made and seen as its + // object is the object's block: the root of a generator's state object or an object literal + if (value.getDefiningOp() && mlir::isa(value.getType()) && + llvm::any_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user) && isView(user); })) + { + return true; + } + + return ownsHeap(value.getLoc(), value.getType()); + } + + bool OwnershipInferencePass::ownsHeap(mlir::Location location, mlir::Type type) + { + return own_facts::ownsHeap(location, type); + } + + bool OwnershipInferencePass::holdsConstantsOrMoves(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + // a borrowing record (spec 32) takes nothing for what it borrows, and its fresh values move in + // as an owning record's do + if (!varOp || !isConstantRecordLocal(varOp) || !(isOwningRecord(value) || borrowingRecordOf(varOp))) + { + return false; + } + + // a fresh value moved in, its move decided by its own check against this retain + auto movesIn = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + return false; + } + + // a method's field is a `!ts.bound_ref`, which holds a function, not a block + auto refType = mlir::dyn_cast(propertyRefOp.getType()); + if (refType && !ownsHeap(propertyRefOp.getLoc(), refType.getElementType())) + { + return false; + } + + return llvm::any_of(propertyRefOp->getUsers(), + [](mlir::Operation *fieldUser) { return mlir::isa(fieldUser); }); + }); + + return !movesIn || recordRetainOf(varOp) != nullptr; + } + + mlir::Operation *OwnershipInferencePass::recordRetainOf(mlir_ts::VariableOp varOp) + { + if (!isConstantRecordLocal(varOp)) + { + return nullptr; + } + + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + if (read) + { + return nullptr; + } + + read = loadOp; + } + } + + if (!read) + { + return nullptr; + } + + mlir::Operation *retain = nullptr; + auto boxed = false; + auto other = false; + for (auto *user : read->getUsers()) + { + if (mlir::isa(user)) + { + if (retain) + { + return nullptr; + } + + retain = user; + continue; + } + + // copied into the block it is given in as an object or an interface (spec 27): the box + // owns what the record held, and its own check decides where the box goes + auto storeOp = mlir::dyn_cast(user); + auto box = storeOp && storeOp.getValue() == read.getResult() ? recordBoxOf(storeOp.getReference()) + : mlir_ts::NewOp(); + if (box && box.getResult() == storeOp.getReference()) + { + if (boxed) + { + return nullptr; + } + + boxed = true; + continue; + } + + other = true; + if (mlir::isa(user)) + { + continue; + } + + // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's + // `ts.RetainSlot` of that local is the record's retain + if (auto ownerOp = mlir::dyn_cast(user)) + { + auto *slotRetain = ownerOp.getInitializer() == read.getResult() && isOwningVariable(ownerOp) + ? onlyRetainSlotOf(ownerOp) + : nullptr; + if (!slotRetain || retain) + { + return nullptr; + } + + retain = slotRetain; + continue; + } + + // stored into the result slot, or into an owning local (`a = { item: new C() }`) + auto slot = storeOp && storeOp.getValue() == read.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + if (!slot || !(isResultSlot(slot) || isOwningVariable(slot))) + { + return nullptr; + } + } + + // a boxed record goes nowhere else: the box is its one owner + if (boxed && other) + { + return nullptr; + } + + return retain; + } + + bool OwnershipInferencePass::isBuiltArrayRead(mlir::Operation *retain, mlir::Value value) + { + auto read = value.getDefiningOp(); + auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto owner = mlir::cast(retain).getSlot().getDefiningOp(); + if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || + !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp()) || + !owner || owner.getInitializer() != read.getResult() || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain) + { + return false; + } + + // only filled, and only before it is read + return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (user == read) + { + return true; + } + + return mlir::isa(user) && use.getOperandNumber() == 0 && + !reachableAfter(read, user, builder); + }); + } + + bool OwnershipInferencePass::isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value) + { + auto read = value.getDefiningOp(); + auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || + !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp())) + { + return false; + } + + // the read is retained and returned - stored into the function's result slot - and nothing else + auto returned = false; + for (auto *user : read->getUsers()) + { + if (user == retain) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + auto resultSlot = storeOp && storeOp.getValue() == read.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + auto intoField = storeOp && storeOp.getValue() == read.getResult() && + storeOp.getReference().getDefiningOp(); + if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) + { + return false; + } + + returned = true; + } + + if (!returned) + { + return false; + } + + // only filled, and only before it is read + return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (user == read) + { + return true; + } + + if (reachableAfter(read, user, builder)) + { + return false; + } + + if (auto loadOp = mlir::dyn_cast(user)) + { + return llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser) && loadUser->use_empty(); + }); + } + + return mlir::isa(user) && use.getOperandNumber() == 0; + }); + } + + mlir::Operation *OwnershipInferencePass::onlyRetainSlotOf(mlir_ts::VariableOp varOp) + { + mlir::Operation *found = nullptr; + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + if (found) + { + return nullptr; + } + + found = user; + } + } + + return found; + } + + bool OwnershipInferencePass::isResultSlot(mlir_ts::VariableOp varOp) + { + if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [&](mlir::Operation *user) { + if (auto storeOp = mlir::dyn_cast(user)) + { + return storeOp.getReference() == varOp.getResult(); + } + + auto loadOp = mlir::dyn_cast(user); + return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser); + }); + }); + } + + mlir::Operation *OwnershipInferencePass::recordRetainOfStore(mlir::Operation *taker) + { + auto storeOp = mlir::dyn_cast(taker); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto varOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + auto *retain = varOp ? recordRetainOf(varOp) : nullptr; + if (!retain || retain->getBlock() != taker->getBlock() || !taker->isBeforeInBlock(retain)) + { + return nullptr; + } + + return retain; + } + + bool OwnershipInferencePass::isImmortalLiteral(mlir::Value value) + { + if (isImmortalGlobalRead(value)) + { + return true; + } + + auto castOp = rootOf(value).getDefiningOp(); + return castOp && mlir::isa(castOp.getType()) && + castOp.getIn().getDefiningOp(); + } + + bool OwnershipInferencePass::isBorrow(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return true; + } + + // `assert(c, message)` prints the message when it fails, and keeps it either way + if (mlir::isa(user)) + { + return true; + } + + // `s.value`'s place and `Shared.count(s)` read the handle and keep nothing (spec 23.1) + if (mlir::isa(user)) + { + return true; + } + + // `--di`'s record of a folded const's value for the debugger: names it, keeps nothing + if (mlir::isa(user)) + { + return true; + } + + // a test - `if (d)`, `d?.x`, `typeof u` - asks about the value, and keeps nothing + if (mlir::isa(user)) + { + return true; + } + + // ...and so does a cast to `null` or `undefined`, which holds no block: `--di` records the + // `r` that `if (r == null)` narrows as one + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) || + castOp.getType().isInteger(1) || isBoundForCall(castOp); + } + + if (mlir::isa(user)) + { + return isBoundForCall(user); + } + + // arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee + // with no facts that keeps one retains it, and that retain is its own error + if (isCall(user)) + { + return !isKeptArgument(user, value); + } + + // the array being pushed onto is written, not taken; what is inserted is taken + if (auto pushOp = mlir::dyn_cast(user)) + { + return pushOp.getOp() == value && !llvm::is_contained(pushOp.getItems(), value); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return unshiftOp.getOp() == value && !llvm::is_contained(unshiftOp.getItems(), value); + } + + if (auto spliceOp = mlir::dyn_cast(user)) + { + return spliceOp.getOp() == value && !llvm::is_contained(spliceOp.getItems(), value); + } + + // a declaration that does not own what it holds only reads it + if (auto varOp = mlir::dyn_cast(user)) + { + return !isOwningVariable(varOp); + } + + // the box of a closure that borrows what it captures holds a copy that owns nothing; the + // closure's own uses are the borrow's (walkBorrowed, decideClosures) + if (mlir::isa(user) && user->hasAttr(OWN_CAPTURE_BORROW_ATTR_NAME)) + { + return true; + } + + // a store through a `ts.New`'d `value_ref` fills its block and keeps nothing of it + if (auto storeOp = mlir::dyn_cast(user)) + { + return mlir::isa(value.getType()) && storeOp.getReference() == value && + storeOp.getValue() != value; + } + + return false; + } + + bool OwnershipInferencePass::isBoundForCall(mlir::Operation *thisOp) + { + if (thisOp->getNumResults() != 1 || !mlir::isa(thisOp->getResult(0).getType()) || + thisOp->use_empty()) + { + return false; + } + + return llvm::all_of(thisOp->getUses(), [](mlir::OpOperand &use) { + auto boundOp = mlir::dyn_cast(use.getOwner()); + if (!boundOp || boundOp.getThisVal() != use.get()) + { + return false; + } + + return llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { + if (mlir::isa(boundUser)) + { + return true; + } + + return mlir::isa(boundUser) && + llvm::all_of(boundUser->getUsers(), [](mlir::Operation *thisUser) { return isCall(thisUser); }); + }); + }); + } + + bool OwnershipInferencePass::isKeptArgument(mlir::Operation *call, mlir::Value value) + { + auto args = callArgs(call); + return llvm::any_of(ownedParams(call), [&](int32_t index) { + return static_cast(index) < args.size() && args[index] == value; + }); + } + + bool OwnershipInferencePass::checkValueMoves(mlir::Value value, llvm::SetVector &toErase) + { + // Nothing to move: a number owns no block, and a string literal is the immortal global, + // which any number of places may hold. Under --opt, CSE merges identical literals before + // this pass, so one such value is routinely stored into several places. A read out of a + // container owns nothing either: checkPlaceRead decides it. Nor does other data nothing + // owns: an empty optional, a function, a tuple of literals (isConstantData). + if (!ownsHeap(value) || isImmortalLiteral(value) || isConstantData(value) || placeReadOf(value)) + { + return true; + } + + // A temporary's releases are one owner, one release on each way out. Two on one path are + // two owners: each view rc gave a reference of its own (`use(o); use(o)`, with `o` a + // folded object literal and `use` taking an interface: two `ts.NewInterface`s of one block). + if (!checkReleasedOnce(value)) + { + return false; + } + + if (quietly([&] { return movesOf(value, toErase); })) + { + return true; + } + + // Not a move. Every taker a `let` that can borrow, and the temporary's own release the + // owner that bounds them (spec 2.2, verdict 2) - else the move's error stands. + llvm::SmallVector takers; + llvm::SmallVector releases; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (!isBorrow(user, used)) + { + takers.push_back(user); + } + }); + + if (!releases.empty() && !takers.empty() && + llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) + { + auto owner = ownerName(value); + auto ok = true; + for (auto *taker : takers) + { + ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; + } + + return ok; + } + + return movesOf(value, toErase); + } + + bool OwnershipInferencePass::checkReleasedOnce(mlir::Value value) + { + llvm::SmallVector releases; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) + { + releases.push_back(user); + } + }); + + auto *definition = value.getDefiningOp(); + for (auto *first : releases) + { + for (auto *second : releases) + { + if (first != second && reachableAfter(first, second, definition)) + { + // the view the second release gives back, where rc took its reference + auto *view = second->getOperand(0).getDefiningOp(); + reportSecondReference(view ? view : second, value); + return false; + } + } + } + + return true; + } + + bool OwnershipInferencePass::movesOf(mlir::Value value, llvm::SetVector &toErase) + { + llvm::SmallVector takers; + llvm::SmallVector releases; + llvm::SmallVector reads; + llvm::DenseMap readOf; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (isBorrow(user, used)) + { + reads.push_back(user); + readOf.try_emplace(user, used); + } + else + { + takers.push_back(user); + } + }); + + auto *definition = value.getDefiningOp(); + auto name = describe(value); + llvm::SmallVector moved; + for (auto *taker : takers) + { + if (takerLoops(taker, definition)) + { + reportMovedInLoop(taker, name); + return false; + } + + llvm::SmallVector made; + if (definition) + { + made.push_back(definition); + } + + // the call that keeps it, given it again through another operand + if (auto *again = callGivenAgain(taker, value, {value})) + { + reportUseAfterMove(again, taker, name); + return false; + } + + for (auto *use : llvm::concat(takers, reads)) + { + if (use != taker && reachableAfter(taker, use, definition)) + { + // a read through the new owner, while it holds the value (spec 29) + if (auto found = readOf.find(use); + found != readOf.end() && readsThroughOwner(taker, found->second, use, made)) + { + continue; + } + + reportUseAfterMove(use, taker, name); + return false; + } + } + + if (releases.empty()) + { + continue; // rc handed the taker the value's own reference: nothing to cancel + } + + if (!takerAcquires(taker, value)) + { + reportSecondReference(taker, value); + return false; + } + + auto *acquire = acquirePoint(taker, value); + for (auto *release : releases) + { + if (!reachableAfter(acquire, release, definition)) + { + continue; + } + + if (!dominance->properlyDominates(acquire, release)) + { + reportMovedOnSomePaths(taker, release, name); + return false; + } + + moved.push_back(release); + } + } + + toErase.insert(moved.begin(), moved.end()); + return true; + } + + mlir::Operation *OwnershipInferencePass::callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources) + { + if (!isCall(taker) || ownedParams(taker).empty()) + { + return nullptr; + } + + auto args = callArgs(taker); + auto kept = ownedParams(taker); + auto movedRoot = rootOf(moved); + // one kept position is the move itself; the value at a second one (`f(n, n)` with both + // kept) is a second owner, a use after the first move like any other operand + auto skipped = false; + for (auto &operand : taker->getOpOperands()) + { + // the moved argument itself, where the callee keeps it + auto number = operand.getOperandNumber(); + if (!skipped && !args.empty() && number >= args.getBeginOperandIndex() && + number < args.getBeginOperandIndex() + args.size() && + llvm::is_contained(kept, static_cast(number - args.getBeginOperandIndex())) && + rootOf(operand.get()) == movedRoot) + { + skipped = true; + continue; + } + + for (auto source : sources) + { + if (derivedFrom(source).contains(operand.get())) + { + return taker; + } + } + } + + return nullptr; + } + + bool OwnershipInferencePass::takerLoops(mlir::Operation *taker, mlir::Operation *definition) + { + auto &body = getFunction().getFunctionBody(); + auto *top = body.findAncestorOpInRegion(*taker); + if (!top) + { + return true; + } + + auto *kill = definition ? body.findAncestorOpInRegion(*definition) : nullptr; + llvm::SmallPtrSet seen; + llvm::SmallVector work(top->getBlock()->getSuccessors().begin(), + top->getBlock()->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + if (kill && kill->getBlock() == block && + (block != top->getBlock() || kill->isBeforeInBlock(top))) + { + continue; // the value is made again before the taker runs again + } + + if (block == top->getBlock()) + { + return true; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + return false; + } + + bool OwnershipInferencePass::reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill) + { + return reachableAfter(from, to, kill ? llvm::ArrayRef(kill) : llvm::ArrayRef()); + } + + bool OwnershipInferencePass::orderedInBody(mlir::Operation *from, mlir::Operation *to) + { + auto &body = getFunction().getFunctionBody(); + from = body.findAncestorOpInRegion(*from); + to = body.findAncestorOpInRegion(*to); + return from && to && from != to; + } + + bool OwnershipInferencePass::reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) + { + if (llvm::is_contained(kills, to)) + { + return false; + } + + auto &body = getFunction().getFunctionBody(); + from = body.findAncestorOpInRegion(*from); + to = body.findAncestorOpInRegion(*to); + llvm::SmallVector killAt; + for (auto *kill : kills) + { + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } + } + + if (!from || !to) + { + return true; + } + + if (from == to) + { + // Two distinct ops that share one ancestor statement: the order inside it is not + // known here, and "reachable" can only turn a program into an error. + return true; + } + + // is there a kill in `block` after `after` (from its start when null) and before `before` + // (to its end when null)? + auto killsIn = [&](mlir::Block *block, mlir::Operation *after, mlir::Operation *before) { + return llvm::any_of(killAt, [&](mlir::Operation *kill) { + return kill->getBlock() == block && (!after || after->isBeforeInBlock(kill)) && + (!before || kill->isBeforeInBlock(before)); + }); + }; + + auto *fromBlock = from->getBlock(); + auto *toBlock = to->getBlock(); + if (fromBlock == toBlock && from->isBeforeInBlock(to)) + { + return !killsIn(fromBlock, from, to); + } + + // every way out of this block runs a kill first + if (killsIn(fromBlock, from, nullptr)) + { + return false; + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(fromBlock->getSuccessors().begin(), fromBlock->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + if (block == toBlock && !killsIn(block, nullptr, to)) + { + return true; + } + + if (killsIn(block, nullptr, nullptr)) + { + continue; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + return false; + } + + mlir::Operation *OwnershipInferencePass::takerOf(mlir::Operation *retain, mlir::Value value) + { + if (auto retainSlotOp = mlir::dyn_cast(retain)) + { + return retainSlotOp.getSlot().getDefiningOp(); + } + + // a call that keeps the argument: rc's retain for it is in the callee + if (isCall(retain)) + { + return retain; + } + + mlir::Operation *taker = nullptr; + auto several = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user) || isBorrow(user, used)) + { + return; + } + + several = several || taker; + taker = user; + }); + + // rc retained this read and hands the receiver the next read of the same place + if (!taker && !several) + { + if (auto twin = twinReadAfter(retain, value)) + { + return takerOf(retain, twin); + } + } + + return several ? nullptr : taker; // two takers of one read: not a move this phase proves + } + + mlir::Operation *OwnershipInferencePass::soleUser(mlir::Value value) + { + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + continue; + } + + if (found) + { + return nullptr; + } + + found = user; + } + + return found; + } + + mlir::Value OwnershipInferencePass::twinReadAfter(mlir::Operation *retain, mlir::Value value) + { + auto first = value.getDefiningOp(); + if (!first || !mlir::isa(retain) || soleUser(value) != retain || retain->getBlock() != first->getBlock()) + { + return {}; + } + + auto place = first.getReference(); + for (auto *op = retain->getNextNode(); op; op = op->getNextNode()) + { + if (auto loadOp = mlir::dyn_cast(op); loadOp && loadOp.getReference() == place) + { + return loadOp.getResult(); + } + + // anything else given the place may write through it - a store, a push - and a call may + // write anything + if (llvm::is_contained(op->getOperands(), place) || isCall(op)) + { + return {}; + } + } + + return {}; + } + + bool OwnershipInferencePass::checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, + llvm::SetVector &toErase, bool placeReleases) + { + auto slot = load.getReference(); + auto varOp = slot.getDefiningOp(); + auto name = varName(varOp); + + auto *taker = takerOf(retain, value); + if (!taker) + { + reportSecondReference(retain); + return false; + } + + auto assignments = slotAssignments(slot); + llvm::SmallVector kills(assignments.begin(), assignments.end()); + if (varOp) + { + kills.push_back(varOp); + } + + // the move's own read, met again around a loop the slot was declared outside of + if (reachableAfter(taker, load, kills)) + { + reportMovedInLoop(taker, name); + return false; + } + + // the slot given another value on some path between the move's read and its taker - inside + // the call's own argument list, `f(n, (n = mk(7), 0))`, on one arm of it, `f(n, c ? (n = + // mk(7), 0) : 0)`: the assignment's release frees what the read holds before the callee takes + // it, so the assignment is a use of the slot after the move. The taker may be an assignment + // of the slot itself (`n = n`): it is the destination, not between. A read and a taker + // reachableAfter cannot order (one statement kept as a region, or outside the body) refuse. + { + llvm::SmallVector readAgain{load.getOperation()}; + if (varOp) + { + readAgain.push_back(varOp); + } + + for (auto *assignment : assignments) + { + if (assignment != taker && reachableAfter(load, assignment, varOp) && + reachableAfter(assignment, taker, readAgain)) + { + reportUseAfterMove(assignment, taker, name); + return false; + } + } + + llvm::SmallVector killsBetween(kills); + llvm::erase(killsBetween, taker); + if (!orderedInBody(load, taker) || !reachableAfter(load, taker, killsBetween)) + { + reportUseAfterMove(taker, taker, name); + return false; + } + } + + // the call that keeps it, given the slot's value again through another operand: a read of + // the slot that reaches the call without crossing an assignment of it (not one made before + // an assignment gave the slot its current value, `const arr = [n]; n = mk(5); f(n, arr)`); + // since the move's own read reaches the call so too, such a read holds the moved value + llvm::SmallVector slotReads; + for (auto *user : slot.getUsers()) + { + if (auto readOp = mlir::dyn_cast(user); readOp && reachableAfter(readOp, taker, kills)) + { + slotReads.push_back(readOp.getResult()); + } + } + + if (auto *again = callGivenAgain(taker, value, slotReads)) + { + reportUseAfterMove(again, taker, name); + return false; + } + + // Every read of the slot is a use of `a`, and so is every use of what a read returned: a + // `const b = a` is folded into its load, so `b.x` after the move reads the moved value + // through a load that came before it. A read through the new owner, while it holds the + // value, is not such a use (spec 29). + auto throughOwner = [&](mlir::Operation *user, mlir::Value used) { + return readsThroughOwner(taker, used, user, kills); + }; + + for (auto *use : slot.getUsers()) + { + if (mlir::isa(use)) + { + continue; + } + + if (use != load.getOperation() && reachableAfter(taker, use, kills)) + { + auto readOp = mlir::dyn_cast(use); + if (!readOp || !onlyReadThroughOwner(readOp.getResult(), throughOwner)) + { + reportUseAfterMove(use, taker, name); + return false; + } + + continue; + } + + if (auto readOp = mlir::dyn_cast(use)) + { + if (auto *late = readUsedAfter(readOp, retain, taker, varOp, throughOwner)) + { + reportUseAfterMove(late, taker, name); + return false; + } + } + } + + auto *acquire = mlir::isa(retain) ? taker : retain; + llvm::SmallVector moved; + for (auto *use : slot.getUsers()) + { + if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) + { + continue; + } + + // a release that one path reaches holding the moved value and another holding a new + // one an assignment after the move stored: there is no single answer for it + auto someMoved = !dominance->properlyDominates(acquire, use); + for (auto *assignment : assignments) + { + llvm::SmallVector movedAgain{load.getOperation()}; + if (varOp) + { + movedAgain.push_back(varOp); + } + + someMoved = someMoved || + (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)); + } + + if (someMoved) + { + // ...but the paths that did not move it can release it before they join the + // others (spec 30) + if (placeReleases && placeOnUnmovedEdges(varOp, acquire, retain, taker, assignments, toErase)) + { + return true; + } + + reportMovedOnSomePaths(taker, use, name, placeReleases ? joinedUse : nullptr); + return false; + } + + moved.push_back(use); + } + + toErase.insert(moved.begin(), moved.end()); + return true; + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 820bb90a2..525d96bd5 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -1017,234 +1017,37 @@ class OwnershipInferencePass // their block, no op is given the slot, no call runs, and no op has regions (a store could hide // in them). What is stored into the slot later is decided at its own store; this retain takes // only the initializer. No closure captures it. - static bool slotHoldsInitializerAt(mlir::Operation *op) - { - auto slot = mlir::cast(op).getSlot(); - auto varOp = slot.getDefiningOp(); - if (!varOp || !varOp.getInitializer() || isCellVariable(slot) || varOp->getBlock() != op->getBlock()) - { - return false; - } - - for (auto *between = varOp->getNextNode(); between; between = between->getNextNode()) - { - if (between == op) - { - return true; - } - - if (llvm::is_contained(between->getOperands(), slot) || isCall(between) || between->getNumRegions() > 0) - { - return false; - } - } - - return false; - } + static bool slotHoldsInitializerAt(mlir::Operation *op); // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer, // seen through its views. None for a variable with no initializer - its storage was hoisted // in front of a try and its value arrives by a store this phase does not follow. - static mlir::Value retainedValue(mlir::Operation *op) - { - if (auto retainOp = mlir::dyn_cast(op)) - { - return rootOf(retainOp.getReference()); - } - - auto varOp = mlir::cast(op).getSlot().getDefiningOp(); - return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); - } + static mlir::Value retainedValue(mlir::Operation *op); // Does a value of this type own a block that a release would destroy? - bool ownsHeap(mlir::Value value) - { - // a `value_ref` points at storage it does not own, but one `ts.New` made and seen as its - // object is the object's block: the root of a generator's state object or an object literal - if (value.getDefiningOp() && mlir::isa(value.getType()) && - llvm::any_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user) && isView(user); })) - { - return true; - } - - return ownsHeap(value.getLoc(), value.getType()); - } + bool ownsHeap(mlir::Value value); - bool ownsHeap(mlir::Location location, mlir::Type type) - { - return own_facts::ownsHeap(location, type); - } + bool ownsHeap(mlir::Location location, mlir::Type type); // A tuple read out of a local that a `ts.Constant` initializes, whose owning fields hold what // that constant put there - null, a number, an immortal literal - or a fresh value moved in: // the initial state a generator's maker copies into its state object, or a record built to be // returned (`return {value: new C(), done: false}`, a `yield`'s result). rc's retain of it is // its reference for the values moved in, which own moves instead (recordRetainOf). - bool holdsConstantsOrMoves(mlir::Value value) - { - auto loadOp = value.getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - // a borrowing record (spec 32) takes nothing for what it borrows, and its fresh values move in - // as an owning record's do - if (!varOp || !isConstantRecordLocal(varOp) || !(isOwningRecord(value) || borrowingRecordOf(varOp))) - { - return false; - } - - // a fresh value moved in, its move decided by its own check against this retain - auto movesIn = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - return false; - } - - // a method's field is a `!ts.bound_ref`, which holds a function, not a block - auto refType = mlir::dyn_cast(propertyRefOp.getType()); - if (refType && !ownsHeap(propertyRefOp.getLoc(), refType.getElementType())) - { - return false; - } - - return llvm::any_of(propertyRefOp->getUsers(), - [](mlir::Operation *fieldUser) { return mlir::isa(fieldUser); }); - }); - - return !movesIn || recordRetainOf(varOp) != nullptr; - } + bool holdsConstantsOrMoves(mlir::Value value); // rc's one `ts.Retain` of a record local's one read, where the read is only returned: stored // into a local that owns nothing and is read only by returns (the result slot), or copied into // the box it is given in as an object or an interface (spec 27). It is the reference rc gives // each fresh value stored into the record, so it is where they move in. Null for any other // record. - static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp) - { - if (!isConstantRecordLocal(varOp)) - { - return nullptr; - } - - mlir_ts::LoadOp read; - for (auto *user : varOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - if (read) - { - return nullptr; - } - - read = loadOp; - } - } - - if (!read) - { - return nullptr; - } - - mlir::Operation *retain = nullptr; - auto boxed = false; - auto other = false; - for (auto *user : read->getUsers()) - { - if (mlir::isa(user)) - { - if (retain) - { - return nullptr; - } - - retain = user; - continue; - } - - // copied into the block it is given in as an object or an interface (spec 27): the box - // owns what the record held, and its own check decides where the box goes - auto storeOp = mlir::dyn_cast(user); - auto box = storeOp && storeOp.getValue() == read.getResult() ? recordBoxOf(storeOp.getReference()) - : mlir_ts::NewOp(); - if (box && box.getResult() == storeOp.getReference()) - { - if (boxed) - { - return nullptr; - } - - boxed = true; - continue; - } - - other = true; - if (mlir::isa(user)) - { - continue; - } - - // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's - // `ts.RetainSlot` of that local is the record's retain - if (auto ownerOp = mlir::dyn_cast(user)) - { - auto *slotRetain = ownerOp.getInitializer() == read.getResult() && isOwningVariable(ownerOp) - ? onlyRetainSlotOf(ownerOp) - : nullptr; - if (!slotRetain || retain) - { - return nullptr; - } - - retain = slotRetain; - continue; - } - - // stored into the result slot, or into an owning local (`a = { item: new C() }`) - auto slot = storeOp && storeOp.getValue() == read.getResult() - ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - if (!slot || !(isResultSlot(slot) || isOwningVariable(slot))) - { - return nullptr; - } - } - - // a boxed record goes nowhere else: the box is its one owner - if (boxed && other) - { - return nullptr; - } - - return retain; - } + static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp); // `const b = [...a, ...a]`: MLIRGen builds the array in a local of its own that owns nothing, // starting from a fresh one and pushing into it, and reads it once, when it is done, into the // owning local it is made for. That local's `ts.RetainSlot` (`retain`) is the reference: the // fresh array moves into the builder and then into the owner, and nothing else holds it. - bool isBuiltArrayRead(mlir::Operation *retain, mlir::Value value) - { - auto read = value.getDefiningOp(); - auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); - auto owner = mlir::cast(retain).getSlot().getDefiningOp(); - if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || - !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp()) || - !owner || owner.getInitializer() != read.getResult() || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain) - { - return false; - } - - // only filled, and only before it is read - return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (user == read) - { - return true; - } - - return mlir::isa(user) && use.getOperandNumber() == 0 && - !reachableAfter(read, user, builder); - }); - } + bool isBuiltArrayRead(mlir::Operation *retain, mlir::Value value); // `return [...a]`: the builder of isBuiltArrayRead, read once, when it is done, and returned. // The return's `ts.Retain` (`retain`) is the reference the caller gets: the fresh array moves @@ -1252,235 +1055,25 @@ class OwnershipInferencePass // other reads only bind its `push` (`ts.ThisSymbolRef @__array_push`) or are not used. A store // into a field is the same move, into a container: `h.c = [...a]`, and a generator's local // (`const groups = [...result]`), which is a field of its state object. - bool isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value) - { - auto read = value.getDefiningOp(); - auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || - !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp())) - { - return false; - } - - // the read is retained and returned - stored into the function's result slot - and nothing else - auto returned = false; - for (auto *user : read->getUsers()) - { - if (user == retain) - { - continue; - } - - auto storeOp = mlir::dyn_cast(user); - auto resultSlot = storeOp && storeOp.getValue() == read.getResult() - ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - auto intoField = storeOp && storeOp.getValue() == read.getResult() && - storeOp.getReference().getDefiningOp(); - if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) - { - return false; - } - - returned = true; - } - - if (!returned) - { - return false; - } - - // only filled, and only before it is read - return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (user == read) - { - return true; - } - - if (reachableAfter(read, user, builder)) - { - return false; - } - - if (auto loadOp = mlir::dyn_cast(user)) - { - return llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { - return mlir::isa(loadUser) && loadUser->use_empty(); - }); - } - - return mlir::isa(user) && use.getOperandNumber() == 0; - }); - } + bool isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value); // The one `ts.RetainSlot` of a local; null when there are none or several. - static mlir::Operation *onlyRetainSlotOf(mlir_ts::VariableOp varOp) - { - mlir::Operation *found = nullptr; - for (auto *user : varOp->getUsers()) - { - if (mlir::isa(user)) - { - if (found) - { - return nullptr; - } - - found = user; - } - } - - return found; - } + static mlir::Operation *onlyRetainSlotOf(mlir_ts::VariableOp varOp); // A local that owns nothing, is never captured, and is read only to be returned. - static bool isResultSlot(mlir_ts::VariableOp varOp) - { - if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - return false; - } - - return llvm::all_of(varOp->getUsers(), [&](mlir::Operation *user) { - if (auto storeOp = mlir::dyn_cast(user)) - { - return storeOp.getReference() == varOp.getResult(); - } - - auto loadOp = mlir::dyn_cast(user); - return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { - return mlir::isa(loadUser); - }); - }); - } + static bool isResultSlot(mlir_ts::VariableOp varOp); // The record whose field `taker` stores into, and rc's retain that moves the stored value in; // null when `taker` is not such a store. - static mlir::Operation *recordRetainOfStore(mlir::Operation *taker) - { - auto storeOp = mlir::dyn_cast(taker); - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto varOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - auto *retain = varOp ? recordRetainOf(varOp) : nullptr; - if (!retain || retain->getBlock() != taker->getBlock() || !taker->isBeforeInBlock(retain)) - { - return nullptr; - } - - return retain; - } + static mlir::Operation *recordRetainOfStore(mlir::Operation *taker); // A string constant cast to `string` is the global itself, which a release skips; so is a read // of an immortal global (spec 25). - static bool isImmortalLiteral(mlir::Value value) - { - if (isImmortalGlobalRead(value)) - { - return true; - } - - auto castOp = rootOf(value).getDefiningOp(); - return castOp && mlir::isa(castOp.getType()) && - castOp.getIn().getDefiningOp(); - } + static bool isImmortalLiteral(mlir::Value value); // A use that reads the value without keeping it. Kept deliberately short: anything not listed // is treated as taking the value, which can only turn a program into an error. - static bool isBorrow(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return true; - } - - // `assert(c, message)` prints the message when it fails, and keeps it either way - if (mlir::isa(user)) - { - return true; - } - - // `s.value`'s place and `Shared.count(s)` read the handle and keep nothing (spec 23.1) - if (mlir::isa(user)) - { - return true; - } - - // `--di`'s record of a folded const's value for the debugger: names it, keeps nothing - if (mlir::isa(user)) - { - return true; - } - - // a test - `if (d)`, `d?.x`, `typeof u` - asks about the value, and keeps nothing - if (mlir::isa(user)) - { - return true; - } - - // ...and so does a cast to `null` or `undefined`, which holds no block: `--di` records the - // `r` that `if (r == null)` narrows as one - if (auto castOp = mlir::dyn_cast(user)) - { - return mlir::isa(castOp.getType()) || - castOp.getType().isInteger(1) || isBoundForCall(castOp); - } - - if (mlir::isa(user)) - { - return isBoundForCall(user); - } - - // arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee - // with no facts that keeps one retains it, and that retain is its own error - if (isCall(user)) - { - return !isKeptArgument(user, value); - } - - // the array being pushed onto is written, not taken; what is inserted is taken - if (auto pushOp = mlir::dyn_cast(user)) - { - return pushOp.getOp() == value && !llvm::is_contained(pushOp.getItems(), value); - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return unshiftOp.getOp() == value && !llvm::is_contained(unshiftOp.getItems(), value); - } - - if (auto spliceOp = mlir::dyn_cast(user)) - { - return spliceOp.getOp() == value && !llvm::is_contained(spliceOp.getItems(), value); - } - - // a declaration that does not own what it holds only reads it - if (auto varOp = mlir::dyn_cast(user)) - { - return !isOwningVariable(varOp); - } - - // the box of a closure that borrows what it captures holds a copy that owns nothing; the - // closure's own uses are the borrow's (walkBorrowed, decideClosures) - if (mlir::isa(user) && user->hasAttr(OWN_CAPTURE_BORROW_ATTR_NAME)) - { - return true; - } - - // a store through a `ts.New`'d `value_ref` fills its block and keeps nothing of it - if (auto storeOp = mlir::dyn_cast(user)) - { - return mlir::isa(value.getType()) && storeOp.getReference() == value && - storeOp.getValue() != value; - } - - return false; - } + static bool isBorrow(mlir::Operation *user, mlir::Value value); // `ts.CreateBoundFunction(ts.Cast(object), method)` split straight back into `ts.GetThis` and // `ts.GetMethod` for a call: a method of another module's class - its constructor, when it is @@ -1489,41 +1082,10 @@ class OwnershipInferencePass // object comes out again only as a call's argument, which is a borrow of its own. An // interface's `this` (`ts.ExtractInterfaceThis`) is bound so for a call of a function-typed // field (`i.toString()` where `toString: () => string`). - static bool isBoundForCall(mlir::Operation *thisOp) - { - if (thisOp->getNumResults() != 1 || !mlir::isa(thisOp->getResult(0).getType()) || - thisOp->use_empty()) - { - return false; - } - - return llvm::all_of(thisOp->getUses(), [](mlir::OpOperand &use) { - auto boundOp = mlir::dyn_cast(use.getOwner()); - if (!boundOp || boundOp.getThisVal() != use.get()) - { - return false; - } - - return llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { - if (mlir::isa(boundUser)) - { - return true; - } - - return mlir::isa(boundUser) && - llvm::all_of(boundUser->getUsers(), [](mlir::Operation *thisUser) { return isCall(thisUser); }); - }); - }); - } + static bool isBoundForCall(mlir::Operation *thisOp); // Is `value` an argument the call's callee keeps? - static bool isKeptArgument(mlir::Operation *call, mlir::Value value) - { - auto args = callArgs(call); - return llvm::any_of(ownedParams(call), [&](int32_t index) { - return static_cast(index) < args.size() && args[index] == value; - }); - } + static bool isKeptArgument(mlir::Operation *call, mlir::Value value); // The moves out of a fresh or unknown SSA value. Each taking use (spec 11.1) is a move: // - nothing else may use the value after it - another taker included - and a taker that @@ -1534,196 +1096,13 @@ class OwnershipInferencePass // reaches must be dominated by it - that release then has nothing to give back and is // erased. A release the move cannot reach stays: it is the owner on the other paths. // Reports and returns false otherwise. - bool checkValueMoves(mlir::Value value, llvm::SetVector &toErase) - { - // Nothing to move: a number owns no block, and a string literal is the immortal global, - // which any number of places may hold. Under --opt, CSE merges identical literals before - // this pass, so one such value is routinely stored into several places. A read out of a - // container owns nothing either: checkPlaceRead decides it. Nor does other data nothing - // owns: an empty optional, a function, a tuple of literals (isConstantData). - if (!ownsHeap(value) || isImmortalLiteral(value) || isConstantData(value) || placeReadOf(value)) - { - return true; - } + bool checkValueMoves(mlir::Value value, llvm::SetVector &toErase); - // A temporary's releases are one owner, one release on each way out. Two on one path are - // two owners: each view rc gave a reference of its own (`use(o); use(o)`, with `o` a - // folded object literal and `use` taking an interface: two `ts.NewInterface`s of one block). - if (!checkReleasedOnce(value)) - { - return false; - } - - if (quietly([&] { return movesOf(value, toErase); })) - { - return true; - } - - // Not a move. Every taker a `let` that can borrow, and the temporary's own release the - // owner that bounds them (spec 2.2, verdict 2) - else the move's error stands. - llvm::SmallVector takers; - llvm::SmallVector releases; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - releases.push_back(user); - } - else if (!isBorrow(user, used)) - { - takers.push_back(user); - } - }); - - if (!releases.empty() && !takers.empty() && - llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) - { - auto owner = ownerName(value); - auto ok = true; - for (auto *taker : takers) - { - ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; - } - - return ok; - } - - return movesOf(value, toErase); - } - - // No release of `value` comes after another on any path. Reports and returns false otherwise. - bool checkReleasedOnce(mlir::Value value) - { - llvm::SmallVector releases; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user)) - { - releases.push_back(user); - } - }); - - auto *definition = value.getDefiningOp(); - for (auto *first : releases) - { - for (auto *second : releases) - { - if (first != second && reachableAfter(first, second, definition)) - { - // the view the second release gives back, where rc took its reference - auto *view = second->getOperand(0).getDefiningOp(); - reportSecondReference(view ? view : second, value); - return false; - } - } - } - - return true; - } + // No release of `value` comes after another on any path. Reports and returns false otherwise. + bool checkReleasedOnce(mlir::Value value); // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. - bool movesOf(mlir::Value value, llvm::SetVector &toErase) - { - llvm::SmallVector takers; - llvm::SmallVector releases; - llvm::SmallVector reads; - llvm::DenseMap readOf; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - releases.push_back(user); - } - else if (isBorrow(user, used)) - { - reads.push_back(user); - readOf.try_emplace(user, used); - } - else - { - takers.push_back(user); - } - }); - - auto *definition = value.getDefiningOp(); - auto name = describe(value); - llvm::SmallVector moved; - for (auto *taker : takers) - { - if (takerLoops(taker, definition)) - { - reportMovedInLoop(taker, name); - return false; - } - - llvm::SmallVector made; - if (definition) - { - made.push_back(definition); - } - - // the call that keeps it, given it again through another operand - if (auto *again = callGivenAgain(taker, value, {value})) - { - reportUseAfterMove(again, taker, name); - return false; - } - - for (auto *use : llvm::concat(takers, reads)) - { - if (use != taker && reachableAfter(taker, use, definition)) - { - // a read through the new owner, while it holds the value (spec 29) - if (auto found = readOf.find(use); - found != readOf.end() && readsThroughOwner(taker, found->second, use, made)) - { - continue; - } - - reportUseAfterMove(use, taker, name); - return false; - } - } - - if (releases.empty()) - { - continue; // rc handed the taker the value's own reference: nothing to cancel - } - - if (!takerAcquires(taker, value)) - { - reportSecondReference(taker, value); - return false; - } - - auto *acquire = acquirePoint(taker, value); - for (auto *release : releases) - { - if (!reachableAfter(acquire, release, definition)) - { - continue; - } - - if (!dominance->properlyDominates(acquire, release)) - { - reportMovedOnSomePaths(taker, release, name); - return false; - } - - moved.push_back(release); - } - } - - toErase.insert(moved.begin(), moved.end()); - return true; - } + bool movesOf(mlir::Value value, llvm::SetVector &toErase); // A call that keeps an argument and is given the same value again through another operand - the // value itself, a view of it, something read out of it, another read of its slot (`f(n, n)`, @@ -1731,83 +1110,11 @@ class OwnershipInferencePass // through the other operand, and may free it under that borrow, so the other operand is a use // after the move (spec 33, review). `sources` are what the moved value is: the value, or every // read of the owning slot it came from. The call itself, or null. - mlir::Operation *callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources) - { - if (!isCall(taker) || ownedParams(taker).empty()) - { - return nullptr; - } - - auto args = callArgs(taker); - auto kept = ownedParams(taker); - auto movedRoot = rootOf(moved); - // one kept position is the move itself; the value at a second one (`f(n, n)` with both - // kept) is a second owner, a use after the first move like any other operand - auto skipped = false; - for (auto &operand : taker->getOpOperands()) - { - // the moved argument itself, where the callee keeps it - auto number = operand.getOperandNumber(); - if (!skipped && !args.empty() && number >= args.getBeginOperandIndex() && - number < args.getBeginOperandIndex() + args.size() && - llvm::is_contained(kept, static_cast(number - args.getBeginOperandIndex())) && - rootOf(operand.get()) == movedRoot) - { - skipped = true; - continue; - } - - for (auto source : sources) - { - if (derivedFrom(source).contains(operand.get())) - { - return taker; - } - } - } - - return nullptr; - } + mlir::Operation *callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources); // Does control come back round to `taker` without making the value again? Only then is it a // move on every iteration of a loop. - bool takerLoops(mlir::Operation *taker, mlir::Operation *definition) - { - auto &body = getFunction().getFunctionBody(); - auto *top = body.findAncestorOpInRegion(*taker); - if (!top) - { - return true; - } - - auto *kill = definition ? body.findAncestorOpInRegion(*definition) : nullptr; - llvm::SmallPtrSet seen; - llvm::SmallVector work(top->getBlock()->getSuccessors().begin(), - top->getBlock()->getSuccessors().end()); - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - if (kill && kill->getBlock() == block && - (block != top->getBlock() || kill->isBeforeInBlock(top))) - { - continue; // the value is made again before the taker runs again - } - - if (block == top->getBlock()) - { - return true; - } - - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - - return false; - } + bool takerLoops(mlir::Operation *taker, mlir::Operation *definition); // Can control reach `to` after `from` has run, without passing `kill` on the way? `kill` is // the source's definition - the producer of an SSA value, or a local's `ts.Variable` - and a @@ -1818,193 +1125,28 @@ class OwnershipInferencePass // body. That answers "reachable" whenever the two share an ancestor, which can only turn a // program into an error. (By this pass async bodies and lambdas are functions of their own; // after the affine lowering no op of a body holds a region, so the case is kept for safety.) - bool reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill) - { - return reachableAfter(from, to, kill ? llvm::ArrayRef(kill) : llvm::ArrayRef()); - } + bool reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill); // Whether reachableAfter can order `from` and `to` at all: both inside the function body and not // one op of it (nested in the same statement). A check whose safe answer is "not reachable" must // refuse what it cannot order, where reachableAfter would answer "reachable". - bool orderedInBody(mlir::Operation *from, mlir::Operation *to) - { - auto &body = getFunction().getFunctionBody(); - from = body.findAncestorOpInRegion(*from); - to = body.findAncestorOpInRegion(*to); - return from && to && from != to; - } + bool orderedInBody(mlir::Operation *from, mlir::Operation *to); // The same, where a path through any of `kills` is a new value. A use that is itself a kill // runs after the value is made again. - bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) - { - if (llvm::is_contained(kills, to)) - { - return false; - } - - auto &body = getFunction().getFunctionBody(); - from = body.findAncestorOpInRegion(*from); - to = body.findAncestorOpInRegion(*to); - llvm::SmallVector killAt; - for (auto *kill : kills) - { - if (auto *top = body.findAncestorOpInRegion(*kill)) - { - killAt.push_back(top); - } - } - - if (!from || !to) - { - return true; - } - - if (from == to) - { - // Two distinct ops that share one ancestor statement: the order inside it is not - // known here, and "reachable" can only turn a program into an error. - return true; - } - - // is there a kill in `block` after `after` (from its start when null) and before `before` - // (to its end when null)? - auto killsIn = [&](mlir::Block *block, mlir::Operation *after, mlir::Operation *before) { - return llvm::any_of(killAt, [&](mlir::Operation *kill) { - return kill->getBlock() == block && (!after || after->isBeforeInBlock(kill)) && - (!before || kill->isBeforeInBlock(before)); - }); - }; - - auto *fromBlock = from->getBlock(); - auto *toBlock = to->getBlock(); - if (fromBlock == toBlock && from->isBeforeInBlock(to)) - { - return !killsIn(fromBlock, from, to); - } - - // every way out of this block runs a kill first - if (killsIn(fromBlock, from, nullptr)) - { - return false; - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work(fromBlock->getSuccessors().begin(), fromBlock->getSuccessors().end()); - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - if (block == toBlock && !killsIn(block, nullptr, to)) - { - return true; - } - - if (killsIn(block, nullptr, nullptr)) - { - continue; - } - - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - - return false; - } + bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills); // The use that takes `value` for this retain: the declaration a `ts.RetainSlot` belongs to, // or the one use of the value, or of a view of it, after a `ts.Retain` that is not a read. - static mlir::Operation *takerOf(mlir::Operation *retain, mlir::Value value) - { - if (auto retainSlotOp = mlir::dyn_cast(retain)) - { - return retainSlotOp.getSlot().getDefiningOp(); - } - - // a call that keeps the argument: rc's retain for it is in the callee - if (isCall(retain)) - { - return retain; - } - - mlir::Operation *taker = nullptr; - auto several = false; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user) || isBorrow(user, used)) - { - return; - } - - several = several || taker; - taker = user; - }); - - // rc retained this read and hands the receiver the next read of the same place - if (!taker && !several) - { - if (auto twin = twinReadAfter(retain, value)) - { - return takerOf(retain, twin); - } - } - - return several ? nullptr : taker; // two takers of one read: not a move this phase proves - } + static mlir::Operation *takerOf(mlir::Operation *retain, mlir::Value value); // rc reads a place twice for one copy - `.map(f)` boxes its array so - and retains the first // read, whose only use is that retain: the read of the same place that follows it in its block, // with nothing stored into the place between, is the same value. Null when there is none. // The one op that uses `value`, but `--di`'s ts.DebugVariable, which only names it; or null. - static mlir::Operation *soleUser(mlir::Value value) - { - mlir::Operation *found = nullptr; - for (auto *user : value.getUsers()) - { - if (mlir::isa(user)) - { - continue; - } - - if (found) - { - return nullptr; - } - - found = user; - } + static mlir::Operation *soleUser(mlir::Value value); - return found; - } - - static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value) - { - auto first = value.getDefiningOp(); - if (!first || !mlir::isa(retain) || soleUser(value) != retain || retain->getBlock() != first->getBlock()) - { - return {}; - } - - auto place = first.getReference(); - for (auto *op = retain->getNextNode(); op; op = op->getNextNode()) - { - if (auto loadOp = mlir::dyn_cast(op); loadOp && loadOp.getReference() == place) - { - return loadOp.getResult(); - } - - // anything else given the place may write through it - a store, a push - and a call may - // write anything - if (llvm::is_contained(op->getOperands(), place) || isCall(op)) - { - return {}; - } - } - - return {}; - } + static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value); // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and // retained what it read for the receiver; the move erases that retain and every release of @@ -2018,164 +1160,7 @@ class OwnershipInferencePass // with the move - what the slot held is the receiver's - and the releases only a path through // an assignment reaches stay, to destroy the new value. bool checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, - llvm::SetVector &toErase, bool placeReleases = false) - { - auto slot = load.getReference(); - auto varOp = slot.getDefiningOp(); - auto name = varName(varOp); - - auto *taker = takerOf(retain, value); - if (!taker) - { - reportSecondReference(retain); - return false; - } - - auto assignments = slotAssignments(slot); - llvm::SmallVector kills(assignments.begin(), assignments.end()); - if (varOp) - { - kills.push_back(varOp); - } - - // the move's own read, met again around a loop the slot was declared outside of - if (reachableAfter(taker, load, kills)) - { - reportMovedInLoop(taker, name); - return false; - } - - // the slot given another value on some path between the move's read and its taker - inside - // the call's own argument list, `f(n, (n = mk(7), 0))`, on one arm of it, `f(n, c ? (n = - // mk(7), 0) : 0)`: the assignment's release frees what the read holds before the callee takes - // it, so the assignment is a use of the slot after the move. The taker may be an assignment - // of the slot itself (`n = n`): it is the destination, not between. A read and a taker - // reachableAfter cannot order (one statement kept as a region, or outside the body) refuse. - { - llvm::SmallVector readAgain{load.getOperation()}; - if (varOp) - { - readAgain.push_back(varOp); - } - - for (auto *assignment : assignments) - { - if (assignment != taker && reachableAfter(load, assignment, varOp) && - reachableAfter(assignment, taker, readAgain)) - { - reportUseAfterMove(assignment, taker, name); - return false; - } - } - - llvm::SmallVector killsBetween(kills); - llvm::erase(killsBetween, taker); - if (!orderedInBody(load, taker) || !reachableAfter(load, taker, killsBetween)) - { - reportUseAfterMove(taker, taker, name); - return false; - } - } - - // the call that keeps it, given the slot's value again through another operand: a read of - // the slot that reaches the call without crossing an assignment of it (not one made before - // an assignment gave the slot its current value, `const arr = [n]; n = mk(5); f(n, arr)`); - // since the move's own read reaches the call so too, such a read holds the moved value - llvm::SmallVector slotReads; - for (auto *user : slot.getUsers()) - { - if (auto readOp = mlir::dyn_cast(user); readOp && reachableAfter(readOp, taker, kills)) - { - slotReads.push_back(readOp.getResult()); - } - } - - if (auto *again = callGivenAgain(taker, value, slotReads)) - { - reportUseAfterMove(again, taker, name); - return false; - } - - // Every read of the slot is a use of `a`, and so is every use of what a read returned: a - // `const b = a` is folded into its load, so `b.x` after the move reads the moved value - // through a load that came before it. A read through the new owner, while it holds the - // value, is not such a use (spec 29). - auto throughOwner = [&](mlir::Operation *user, mlir::Value used) { - return readsThroughOwner(taker, used, user, kills); - }; - - for (auto *use : slot.getUsers()) - { - if (mlir::isa(use)) - { - continue; - } - - if (use != load.getOperation() && reachableAfter(taker, use, kills)) - { - auto readOp = mlir::dyn_cast(use); - if (!readOp || !onlyReadThroughOwner(readOp.getResult(), throughOwner)) - { - reportUseAfterMove(use, taker, name); - return false; - } - - continue; - } - - if (auto readOp = mlir::dyn_cast(use)) - { - if (auto *late = readUsedAfter(readOp, retain, taker, varOp, throughOwner)) - { - reportUseAfterMove(late, taker, name); - return false; - } - } - } - - auto *acquire = mlir::isa(retain) ? taker : retain; - llvm::SmallVector moved; - for (auto *use : slot.getUsers()) - { - if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) - { - continue; - } - - // a release that one path reaches holding the moved value and another holding a new - // one an assignment after the move stored: there is no single answer for it - auto someMoved = !dominance->properlyDominates(acquire, use); - for (auto *assignment : assignments) - { - llvm::SmallVector movedAgain{load.getOperation()}; - if (varOp) - { - movedAgain.push_back(varOp); - } - - someMoved = someMoved || - (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)); - } - - if (someMoved) - { - // ...but the paths that did not move it can release it before they join the - // others (spec 30) - if (placeReleases && placeOnUnmovedEdges(varOp, acquire, retain, taker, assignments, toErase)) - { - return true; - } - - reportMovedOnSomePaths(taker, use, name, placeReleases ? joinedUse : nullptr); - return false; - } - - moved.push_back(use); - } - - toErase.insert(moved.begin(), moved.end()); - return true; - } + llvm::SetVector &toErase, bool placeReleases = false); // ---- A move on some paths only (spec 30) ---- // From c2fb987b79a8509c5c96dcb7cac64351b5ea29c0 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:37:55 +0100 Subject: [PATCH 23/25] own passes: OwnershipInferenceEdgeReleases.cpp out of OwnershipInferencePass.h (I1) A move on some paths only (spec 30) - the release placed on the edges where the paths that did not move join the ones that did. A pure move of isJoined, carriesPointer, reachableFrom, handlerBlocks, placeOnUnmovedEdges, slotReleasesInBody, slotStates, unmovedEdges, noUseWhereJoined, placeEdgeReleases, slotAssignments, readUsedAfter. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipInferenceEdgeReleases.cpp | 571 ++++++++++++++++++ .../lib/TypeScript/OwnershipInferencePass.h | 551 +---------------- 3 files changed, 584 insertions(+), 539 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index f0f6cb12f..793db9840 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -35,6 +35,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferencePass.cpp OwnershipInferenceBorrowers.cpp OwnershipInferenceClosures.cpp + OwnershipInferenceEdgeReleases.cpp OwnershipInferenceGlobalReads.cpp OwnershipInferenceKeptParams.cpp OwnershipInferenceMoves.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp new file mode 100644 index 000000000..678e7f158 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp @@ -0,0 +1,571 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): a move on some paths only (spec 30) - +// the release placed on the edges where the paths that did not move join the ones that did. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::isJoined(unsigned state) + { + return (state & SlotLive) && (state & SlotMoved); + } + + bool OwnershipInferencePass::carriesPointer(mlir::Type type) + { + return !mlir::isa(type); + } + + bool OwnershipInferencePass::reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills) + { + auto &body = getFunction().getFunctionBody(); + to = body.findAncestorOpInRegion(*to); + if (!to) + { + return true; + } + + llvm::SmallVector killAt; + for (auto *kill : kills) + { + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + auto killed = [&](mlir::Operation *before) { + return llvm::any_of(killAt, [&](mlir::Operation *kill) { + return kill->getBlock() == block && (!before || kill->isBeforeInBlock(before) || kill == before); + }); + }; + + if (to->getBlock() == block && !killed(to)) + { + return true; + } + + if (!killed(nullptr)) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return false; + } + + llvm::SmallPtrSet OwnershipInferencePass::handlerBlocks() + { + llvm::SmallPtrSet handler; + llvm::SmallVector work; + for (auto &block : getFunction().getFunctionBody()) + { + if (!block.empty() && mlir::isa(block.front())) + { + work.push_back(&block); + } + } + + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!handler.insert(block).second) + { + continue; + } + + auto ends = llvm::any_of(*block, [](mlir::Operation &op) { + return mlir::isa(op); + }); + if (!ends) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return handler; + } + + bool OwnershipInferencePass::placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, + mlir::Operation *taker, llvm::ArrayRef assignments, + llvm::SetVector &toErase) + { + joinedUse = nullptr; + llvm::SmallVector releases; + if (!slotReleasesInBody(varOp, acquire, assignments, releases)) + { + return false; + } + + auto states = slotStates(varOp, acquire, assignments, releases); + + // every release a moved value reaches goes; one some path reaches with nothing in the slot + // is not this rule's + llvm::SmallVector erased; + for (auto *release : releases) + { + auto state = states.before.lookup(release); + if (state & SlotMoved) + { + if (state & SlotDead) + { + return false; + } + + erased.push_back(release); + } + } + + llvm::SmallVector placed; + llvm::SetVector releasedInto; + if (!unmovedEdges(varOp, states, placed, releasedInto)) + { + return false; + } + + if (!noUseWhereJoined(varOp, retain, taker, assignments, states, releasedInto)) + { + return false; + } + + toErase.insert(erased.begin(), erased.end()); + edgeReleases.append(placed.begin(), placed.end()); + return true; + } + + bool OwnershipInferencePass::slotReleasesInBody(mlir_ts::VariableOp varOp, mlir::Operation *acquire, + llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases) + { + auto &body = getFunction().getFunctionBody(); + if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || + acquire->getParentRegion() != &body || + llvm::any_of(assignments, [&](mlir::Operation *op) { return op->getParentRegion() != &body; })) + { + return false; + } + + auto slot = varOp.getResult(); + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + if (user->getParentRegion() != &body) + { + return false; + } + + releases.push_back(user); + } + } + + return true; + } + + OwnershipInferencePass::SlotStates OwnershipInferencePass::slotStates(mlir_ts::VariableOp varOp, mlir::Operation *acquire, llvm::ArrayRef assignments, + llvm::ArrayRef releases) + { + auto &body = getFunction().getFunctionBody(); + auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { + if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) + { + return SlotLive; + } + + if (op == acquire) + { + return SlotMoved; + } + + if (llvm::is_contained(releases, op)) + { + return SlotDead; + } + + return state; + }; + + SlotStates states; + auto &entry = states.entry; + auto exitOf = [&](mlir::Block *block) { + auto state = entry.lookup(block); + for (auto &op : *block) + { + state = step(&op, state); + } + + return state; + }; + + entry[&body.front()] = SlotDead; + llvm::SmallVector work{&body.front()}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + auto state = exitOf(block); + for (auto *successor : block->getSuccessors()) + { + auto &into = entry[successor]; + if ((into | state) != into) + { + into |= state; + work.push_back(successor); + } + } + } + + // ...and before each op of the body + for (auto &block : body) + { + auto state = entry.lookup(&block); + for (auto &op : block) + { + states.before[&op] = state; + state = step(&op, state); + } + + states.exit[&block] = state; + } + + return states; + } + + bool OwnershipInferencePass::unmovedEdges(mlir_ts::VariableOp varOp, const SlotStates &states, llvm::SmallVectorImpl &placed, + llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + auto &entry = states.entry; + + // the edges into the join that hold the value and did not move it + auto handler = handlerBlocks(); + for (auto &block : body) + { + if (!entry.count(&block)) + { + continue; // unreachable + } + + auto state = states.exit.lookup(&block); + for (auto index : llvm::seq(0, block.getNumSuccessors())) + { + auto into = entry.lookup(block.getSuccessor(index)); + if (!isJoined(into)) + { + continue; + } + + // a join some path reaches with nothing in the slot is not this rule's + if (into & SlotDead) + { + return false; + } + + // an edge that moved it, or from where the paths have joined already + if (state != SlotLive) + { + continue; + } + + auto *terminator = block.getTerminator(); + if (!mlir::isa(terminator) || handler.contains(&block) || + !dominance->dominates(varOp.getOperation(), terminator)) + { + return false; + } + + placed.push_back({slot, &block, static_cast(index), terminator->getLoc()}); + releasedInto.insert(block.getSuccessor(index)); + } + } + + return !placed.empty(); + } + + bool OwnershipInferencePass::noUseWhereJoined(mlir_ts::VariableOp varOp, mlir::Operation *retain, mlir::Operation *taker, + llvm::ArrayRef assignments, const SlotStates &states, + const llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + + auto joinedAt = [&](mlir::Operation *user) { + auto *top = body.findAncestorOpInRegion(*user); + return !top || isJoined(states.before.lookup(top)); + }; + + // Nothing uses the value where the paths have joined - a read there, on the path that did + // not move it, needs what the edge released - neither through the slot nor through anything + // read out of it: a field, a view, a borrowing local, a closure. The move itself hands the + // value to its new owner, whose reads are its own. A value the slot is given - its + // initializer, an assignment's - is the slot's only once it is stored: a use before that (a + // constructor's, or one around a loop before it is made again) is not a use of it. + // + // Past the joined points too: what was read out of the value before an edge released it + // may not be used after that edge - after a new value is stored into the slot, or after its + // scope ends - unless it was read out again (spec 30). A read of the slot after a new value + // is stored reads that one. + struct Origin + { + mlir::Operation *stored; + mlir::Operation *made; + }; + + llvm::SmallVector origins{{nullptr, nullptr}}; + llvm::SmallVector> values; + llvm::DenseSet> seen; + auto taint = [&](mlir::Value value, unsigned origin) { + if (carriesPointer(value.getType()) && seen.insert({value, origin}).second) + { + values.push_back({value, origin}); + } + }; + + auto given = [&](mlir::Value value, mlir::Operation *stored) { + origins.push_back({stored, value.getDefiningOp()}); + taint(value, origins.size() - 1); + }; + + taint(slot, 0); + if (varOp.getInitializer()) + { + given(varOp.getInitializer(), varOp); + } + + for (auto *assignment : assignments) + { + given(mlir::cast(assignment).getValue(), assignment); + } + + // reached from an edge that released the value, before the read it came from (or the op + // that made it) runs again + auto usedAfterRelease = [&](mlir::Operation *made, mlir::Operation *user) { + llvm::SmallVector madeAgain; + if (made) + { + madeAgain.push_back(made); + } + + return llvm::any_of(releasedInto, [&](mlir::Block *join) { return reachableFrom(join, user, madeAgain); }); + }; + + auto heldAt = [&](unsigned origin, mlir::Operation *user) { + auto &from = origins[origin]; + if (!from.stored) + { + return true; + } + + llvm::SmallVector madeAgain; + if (from.made) + { + madeAgain.push_back(from.made); + } + + return reachableAfter(from.stored, user, madeAgain); + }; + + while (!values.empty()) + { + auto [value, origin] = values.pop_back_val(); + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || user == retain) + { + continue; + } + + // the slot's own releases and assignments are decided above + if (value == slot && (mlir::isa(user) || llvm::is_contained(assignments, user))) + { + continue; + } + + if (joinedAt(user) && heldAt(origin, user)) + { + joinedUse = user; + return false; + } + + if (value != slot && usedAfterRelease(origins[origin].made, user)) + { + joinedUse = user; + return false; + } + + if (user == taker) + { + continue; + } + + // a store of it into a local makes every read of that local one; one into a place + // is not followed, and the release is not placed + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == value) + { + auto into = storeOp.getReference().getDefiningOp(); + if (!into || into.getCaptured().value_or(false)) + { + return false; + } + + taint(into.getResult(), origin); + continue; + } + + if (auto branch = mlir::dyn_cast(user)) + { + for (auto index : llvm::seq(0, user->getNumSuccessors())) + { + auto operands = branch.getSuccessorOperands(index); + for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) + { + if (operand == value) + { + taint(user->getSuccessor(index)->getArgument(operands.getProducedOperandCount() + position), + origin); + } + } + } + } + + // a call's result is its own block, unless its facts say it borrows this argument - a + // plain borrow, or a borrowing record that holds it (spec 32) + auto borrows = true; + if (isCall(user)) + { + auto args = callArgs(user); + borrows = llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + } + + // a read of the slot is what its uses came from + auto from = origin; + if (value == slot && user->getNumResults() > 0) + { + origins.push_back({nullptr, user}); + from = origins.size() - 1; + } + + for (auto result : user->getResults()) + { + if (borrows) + { + taint(result, from); + } + } + + // inside a region - a closure, an async body - it is the op that holds the region + // that hands it on + if (user->getParentRegion() != &body) + { + for (auto result : body.findAncestorOpInRegion(*user)->getResults()) + { + taint(result, origin); + } + } + } + } + + return true; + } + + void OwnershipInferencePass::placeEdgeReleases() + { + for (auto &edge : edgeReleases) + { + auto *terminator = edge.from->getTerminator(); + mlir::OpBuilder builder(terminator); + if (terminator->getNumSuccessors() == 1) + { + builder.create(edge.loc, edge.slot); + continue; + } + + auto *join = terminator->getSuccessor(edge.successor); + auto operands = mlir::cast(terminator).getSuccessorOperands(edge.successor); + llvm::SmallVector forwarded(operands.getForwardedOperands()); + auto *split = new mlir::Block(); + split->insertAfter(edge.from); + builder.setInsertionPointToEnd(split); + builder.create(edge.loc, edge.slot); + builder.create(edge.loc, join, forwarded); + if (!forwarded.empty()) + { + operands.erase(0, forwarded.size()); + } + + terminator->setSuccessor(split, edge.successor); + } + + edgeReleases.clear(); + } + + llvm::SmallVector OwnershipInferencePass::slotAssignments(mlir::Value slot) + { + llvm::SmallVector assignments; + for (auto *user : slot.getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + auto releaseOp = storeOp ? mlir::dyn_cast_or_null(storeOp->getPrevNode()) + : mlir_ts::ReleaseSlotOp(); + if (releaseOp && storeOp.getReference() == slot && releaseOp.getSlot() == slot) + { + assignments.push_back(storeOp); + } + } + + return assignments; + } + + mlir::Operation *OwnershipInferencePass::readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, + mlir::Operation *kill, + llvm::function_ref isRead) + { + llvm::SmallVector values{read.getResult()}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto *user : current.getUsers()) + { + if (user == retain || user == taker || mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) || isView(user)) + { + values.push_back(user->getResult(0)); + continue; + } + + llvm::SmallVector kills{read.getOperation()}; + if (kill) + { + kills.push_back(kill); + } + + if (reachableAfter(taker, user, kills) && !(isRead && isRead(user, current))) + { + return user; + } + } + } + + return nullptr; + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 525d96bd5..72d032b77 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -1181,18 +1181,11 @@ class OwnershipInferencePass SlotMoved = 4 }; - static bool isJoined(unsigned state) - { - return (state & SlotLive) && (state & SlotMoved); - } + static bool isJoined(unsigned state); // A value of this type can point into a block: anything but a number, a boolean, a literal or // an empty value. - static bool carriesPointer(mlir::Type type) - { - return !mlir::isa(type); - } + static bool carriesPointer(mlir::Type type); // A release on an edge into the join that did not move the local's value (spec 30), put in after // the analysis. @@ -1210,87 +1203,11 @@ class OwnershipInferencePass mlir::Operation *joinedUse = nullptr; // Can control reach `to` from the start of `start`, without passing `kills` on the way? - bool reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills) - { - auto &body = getFunction().getFunctionBody(); - to = body.findAncestorOpInRegion(*to); - if (!to) - { - return true; - } - - llvm::SmallVector killAt; - for (auto *kill : kills) - { - if (auto *top = body.findAncestorOpInRegion(*kill)) - { - killAt.push_back(top); - } - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work{start}; - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - auto killed = [&](mlir::Operation *before) { - return llvm::any_of(killAt, [&](mlir::Operation *kill) { - return kill->getBlock() == block && (!before || kill->isBeforeInBlock(before) || kill == before); - }); - }; - - if (to->getBlock() == block && !killed(to)) - { - return true; - } - - if (!killed(nullptr)) - { - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - } - - return false; - } + bool reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills); // The blocks of an exception handler: from a landing pad to the end of its catch. A release there // would be a call inside an exception funclet. - llvm::SmallPtrSet handlerBlocks() - { - llvm::SmallPtrSet handler; - llvm::SmallVector work; - for (auto &block : getFunction().getFunctionBody()) - { - if (!block.empty() && mlir::isa(block.front())) - { - work.push_back(&block); - } - } - - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!handler.insert(block).second) - { - continue; - } - - auto ends = llvm::any_of(*block, [](mlir::Operation &op) { - return mlir::isa(op); - }); - if (!ends) - { - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - } - - return handler; - } + llvm::SmallPtrSet handlerBlocks(); // The move at `acquire` out of the owning local `varOp` reaches a release that other paths reach // holding the value. Places a release of the slot on every edge into the join that holds the value @@ -1308,447 +1225,36 @@ class OwnershipInferencePass bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, mlir::Operation *taker, llvm::ArrayRef assignments, - llvm::SetVector &toErase) - { - joinedUse = nullptr; - llvm::SmallVector releases; - if (!slotReleasesInBody(varOp, acquire, assignments, releases)) - { - return false; - } - - auto states = slotStates(varOp, acquire, assignments, releases); - - // every release a moved value reaches goes; one some path reaches with nothing in the slot - // is not this rule's - llvm::SmallVector erased; - for (auto *release : releases) - { - auto state = states.before.lookup(release); - if (state & SlotMoved) - { - if (state & SlotDead) - { - return false; - } - - erased.push_back(release); - } - } - - llvm::SmallVector placed; - llvm::SetVector releasedInto; - if (!unmovedEdges(varOp, states, placed, releasedInto)) - { - return false; - } - - if (!noUseWhereJoined(varOp, retain, taker, assignments, states, releasedInto)) - { - return false; - } - - toErase.insert(erased.begin(), erased.end()); - edgeReleases.append(placed.begin(), placed.end()); - return true; - } + llvm::SetVector &toErase); // The releases of the slot, when the local, the move, its assignments and its releases are all in // the function's body. False when one is not. bool slotReleasesInBody(mlir_ts::VariableOp varOp, mlir::Operation *acquire, - llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases) - { - auto &body = getFunction().getFunctionBody(); - if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || - acquire->getParentRegion() != &body || - llvm::any_of(assignments, [&](mlir::Operation *op) { return op->getParentRegion() != &body; })) - { - return false; - } - - auto slot = varOp.getResult(); - for (auto *user : slot.getUsers()) - { - if (mlir::isa(user)) - { - if (user->getParentRegion() != &body) - { - return false; - } - - releases.push_back(user); - } - } - - return true; - } + llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases); // The slot's state through the function, forward from its entry: assigned, moved at `acquire`, // released. SlotStates slotStates(mlir_ts::VariableOp varOp, mlir::Operation *acquire, llvm::ArrayRef assignments, - llvm::ArrayRef releases) - { - auto &body = getFunction().getFunctionBody(); - auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { - if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) - { - return SlotLive; - } - - if (op == acquire) - { - return SlotMoved; - } - - if (llvm::is_contained(releases, op)) - { - return SlotDead; - } - - return state; - }; - - SlotStates states; - auto &entry = states.entry; - auto exitOf = [&](mlir::Block *block) { - auto state = entry.lookup(block); - for (auto &op : *block) - { - state = step(&op, state); - } - - return state; - }; - - entry[&body.front()] = SlotDead; - llvm::SmallVector work{&body.front()}; - while (!work.empty()) - { - auto *block = work.pop_back_val(); - auto state = exitOf(block); - for (auto *successor : block->getSuccessors()) - { - auto &into = entry[successor]; - if ((into | state) != into) - { - into |= state; - work.push_back(successor); - } - } - } - - // ...and before each op of the body - for (auto &block : body) - { - auto state = entry.lookup(&block); - for (auto &op : block) - { - states.before[&op] = state; - state = step(&op, state); - } - - states.exit[&block] = state; - } - - return states; - } + llvm::ArrayRef releases); // Places a release of the slot on every edge into the join that holds the value and has not moved // it. False, with nothing placed, when an edge cannot take one, or there is none. bool unmovedEdges(mlir_ts::VariableOp varOp, const SlotStates &states, llvm::SmallVectorImpl &placed, - llvm::SetVector &releasedInto) - { - auto &body = getFunction().getFunctionBody(); - auto slot = varOp.getResult(); - auto &entry = states.entry; - - // the edges into the join that hold the value and did not move it - auto handler = handlerBlocks(); - for (auto &block : body) - { - if (!entry.count(&block)) - { - continue; // unreachable - } - - auto state = states.exit.lookup(&block); - for (auto index : llvm::seq(0, block.getNumSuccessors())) - { - auto into = entry.lookup(block.getSuccessor(index)); - if (!isJoined(into)) - { - continue; - } - - // a join some path reaches with nothing in the slot is not this rule's - if (into & SlotDead) - { - return false; - } - - // an edge that moved it, or from where the paths have joined already - if (state != SlotLive) - { - continue; - } - - auto *terminator = block.getTerminator(); - if (!mlir::isa(terminator) || handler.contains(&block) || - !dominance->dominates(varOp.getOperation(), terminator)) - { - return false; - } - - placed.push_back({slot, &block, static_cast(index), terminator->getLoc()}); - releasedInto.insert(block.getSuccessor(index)); - } - } - - return !placed.empty(); - } + llvm::SetVector &releasedInto); // Whether nothing uses the value where the paths have joined, or after an edge released it. // False, with the use in `joinedUse` where there is one, when the release cannot be placed. bool noUseWhereJoined(mlir_ts::VariableOp varOp, mlir::Operation *retain, mlir::Operation *taker, llvm::ArrayRef assignments, const SlotStates &states, - const llvm::SetVector &releasedInto) - { - auto &body = getFunction().getFunctionBody(); - auto slot = varOp.getResult(); - - auto joinedAt = [&](mlir::Operation *user) { - auto *top = body.findAncestorOpInRegion(*user); - return !top || isJoined(states.before.lookup(top)); - }; - - // Nothing uses the value where the paths have joined - a read there, on the path that did - // not move it, needs what the edge released - neither through the slot nor through anything - // read out of it: a field, a view, a borrowing local, a closure. The move itself hands the - // value to its new owner, whose reads are its own. A value the slot is given - its - // initializer, an assignment's - is the slot's only once it is stored: a use before that (a - // constructor's, or one around a loop before it is made again) is not a use of it. - // - // Past the joined points too: what was read out of the value before an edge released it - // may not be used after that edge - after a new value is stored into the slot, or after its - // scope ends - unless it was read out again (spec 30). A read of the slot after a new value - // is stored reads that one. - struct Origin - { - mlir::Operation *stored; - mlir::Operation *made; - }; - - llvm::SmallVector origins{{nullptr, nullptr}}; - llvm::SmallVector> values; - llvm::DenseSet> seen; - auto taint = [&](mlir::Value value, unsigned origin) { - if (carriesPointer(value.getType()) && seen.insert({value, origin}).second) - { - values.push_back({value, origin}); - } - }; - - auto given = [&](mlir::Value value, mlir::Operation *stored) { - origins.push_back({stored, value.getDefiningOp()}); - taint(value, origins.size() - 1); - }; - - taint(slot, 0); - if (varOp.getInitializer()) - { - given(varOp.getInitializer(), varOp); - } - - for (auto *assignment : assignments) - { - given(mlir::cast(assignment).getValue(), assignment); - } - - // reached from an edge that released the value, before the read it came from (or the op - // that made it) runs again - auto usedAfterRelease = [&](mlir::Operation *made, mlir::Operation *user) { - llvm::SmallVector madeAgain; - if (made) - { - madeAgain.push_back(made); - } - - return llvm::any_of(releasedInto, [&](mlir::Block *join) { return reachableFrom(join, user, madeAgain); }); - }; - - auto heldAt = [&](unsigned origin, mlir::Operation *user) { - auto &from = origins[origin]; - if (!from.stored) - { - return true; - } - - llvm::SmallVector madeAgain; - if (from.made) - { - madeAgain.push_back(from.made); - } - - return reachableAfter(from.stored, user, madeAgain); - }; - - while (!values.empty()) - { - auto [value, origin] = values.pop_back_val(); - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || user == retain) - { - continue; - } - - // the slot's own releases and assignments are decided above - if (value == slot && (mlir::isa(user) || llvm::is_contained(assignments, user))) - { - continue; - } - - if (joinedAt(user) && heldAt(origin, user)) - { - joinedUse = user; - return false; - } - - if (value != slot && usedAfterRelease(origins[origin].made, user)) - { - joinedUse = user; - return false; - } - - if (user == taker) - { - continue; - } - - // a store of it into a local makes every read of that local one; one into a place - // is not followed, and the release is not placed - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == value) - { - auto into = storeOp.getReference().getDefiningOp(); - if (!into || into.getCaptured().value_or(false)) - { - return false; - } - - taint(into.getResult(), origin); - continue; - } - - if (auto branch = mlir::dyn_cast(user)) - { - for (auto index : llvm::seq(0, user->getNumSuccessors())) - { - auto operands = branch.getSuccessorOperands(index); - for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) - { - if (operand == value) - { - taint(user->getSuccessor(index)->getArgument(operands.getProducedOperandCount() + position), - origin); - } - } - } - } - - // a call's result is its own block, unless its facts say it borrows this argument - a - // plain borrow, or a borrowing record that holds it (spec 32) - auto borrows = true; - if (isCall(user)) - { - auto args = callArgs(user); - borrows = llvm::any_of(resultBorrowsAll(user), [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - }); - } - - // a read of the slot is what its uses came from - auto from = origin; - if (value == slot && user->getNumResults() > 0) - { - origins.push_back({nullptr, user}); - from = origins.size() - 1; - } - - for (auto result : user->getResults()) - { - if (borrows) - { - taint(result, from); - } - } - - // inside a region - a closure, an async body - it is the op that holds the region - // that hands it on - if (user->getParentRegion() != &body) - { - for (auto result : body.findAncestorOpInRegion(*user)->getResults()) - { - taint(result, origin); - } - } - } - } - - return true; - } + const llvm::SetVector &releasedInto); // Puts in the releases placeOnUnmovedEdges decided: at the end of a block that goes only to the // join, else in a block of its own on the edge. - void placeEdgeReleases() - { - for (auto &edge : edgeReleases) - { - auto *terminator = edge.from->getTerminator(); - mlir::OpBuilder builder(terminator); - if (terminator->getNumSuccessors() == 1) - { - builder.create(edge.loc, edge.slot); - continue; - } - - auto *join = terminator->getSuccessor(edge.successor); - auto operands = mlir::cast(terminator).getSuccessorOperands(edge.successor); - llvm::SmallVector forwarded(operands.getForwardedOperands()); - auto *split = new mlir::Block(); - split->insertAfter(edge.from); - builder.setInsertionPointToEnd(split); - builder.create(edge.loc, edge.slot); - builder.create(edge.loc, join, forwarded); - if (!forwarded.empty()) - { - operands.erase(0, forwarded.size()); - } - - terminator->setSuccessor(split, edge.successor); - } - - edgeReleases.clear(); - } + void placeEdgeReleases(); // The stores of the assignments to an owning local: rc releases what the slot holds right // before it stores the new value (an assignment, and a `var` declaration met again). - static llvm::SmallVector slotAssignments(mlir::Value slot) - { - llvm::SmallVector assignments; - for (auto *user : slot.getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - auto releaseOp = storeOp ? mlir::dyn_cast_or_null(storeOp->getPrevNode()) - : mlir_ts::ReleaseSlotOp(); - if (releaseOp && storeOp.getReference() == slot && releaseOp.getSlot() == slot) - { - assignments.push_back(storeOp); - } - } - - return assignments; - } + static llvm::SmallVector slotAssignments(mlir::Value slot); // A use of what `read` returned - directly or through casts - that the move at `taker` can // reach, other than the move's own retain and taker. None if there is none. A path back @@ -1757,40 +1263,7 @@ class OwnershipInferencePass // `isRead` accepts a use that reads the value through its new owner (spec 29). mlir::Operation *readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, mlir::Operation *kill, - llvm::function_ref isRead = nullptr) - { - llvm::SmallVector values{read.getResult()}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto *user : current.getUsers()) - { - if (user == retain || user == taker || mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) || isView(user)) - { - values.push_back(user->getResult(0)); - continue; - } - - llvm::SmallVector kills{read.getOperation()}; - if (kill) - { - kills.push_back(kill); - } - - if (reachableAfter(taker, user, kills) && !(isRead && isRead(user, current))) - { - return user; - } - } - } - - return nullptr; - } + llvm::function_ref isRead = nullptr); // ---- Reads after a move, through the new owner (spec 29) ---- // From d39c533de8b39a1ce89d7da964d5c303ce0788d1 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:38:33 +0100 Subject: [PATCH 24/25] own passes: OwnershipInferenceReadsAfterMove.cpp out of OwnershipInferencePass.h (I1) Reads after a move, through the new owner (spec 29). A pure move of onlyReadThroughOwner, isReadAfterMove, destinationChain, readsThroughOwner, acquirePoint, takerAcquires. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../lib/TypeScript/OwnershipInferencePass.h | 209 +---------------- .../OwnershipInferenceReadsAfterMove.cpp | 221 ++++++++++++++++++ 3 files changed, 228 insertions(+), 203 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 793db9840..4e45d80ee 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -40,6 +40,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferenceKeptParams.cpp OwnershipInferenceMoves.cpp OwnershipInferencePlaceReads.cpp + OwnershipInferenceReadsAfterMove.cpp OwnershipInferenceRecords.cpp OwnershipSignaturePass.cpp OwnershipSignatureCallbacks.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 72d032b77..3acef361d 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -1276,91 +1276,19 @@ class OwnershipInferencePass // Every use of `value`, through its views, is a read `isRead` accepts. A retain - rc's reference // for an owner - is not. - static bool onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead) - { - auto all = true; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - all = all && (mlir::isa(user) || - (!mlir::isa(user) && isRead(user, used))); - }); - - return all; - } + static bool onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead); // A use that only reads the value: a field, element or length read through it, a method reached // through it, a comparison or arithmetic, a print, an assert, a test, or an argument the callee // borrows. Anything else - a store, an owning `let`, a keeping call, a return - would be a second // owner. - static bool isReadAfterMove(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return isBorrow(user, value); - } - - return isCall(user) && !isKeptArgument(user, value); - } + static bool isReadAfterMove(mlir::Operation *user, mlir::Value value); // The place a taker moves its value into, as a chain checkPlaceRead would read it: an owning // local (a declaration, or an assignment), or a field or element - of a local or a parameter // (`this`). False for anything else: a call that keeps it, a push, a return, a global, a // captured variable, a handle's payload. - bool destinationChain(mlir::Operation *taker, Chain &chain) - { - auto ownSlot = [](mlir_ts::VariableOp varOp) { - return varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false); - }; - - if (auto varOp = mlir::dyn_cast(taker)) - { - if (!ownSlot(varOp)) - { - return false; - } - - chain.roots.push_back({varOp.getResult(), Chain::Slot}); - return true; - } - - auto storeOp = mlir::dyn_cast(taker); - if (!storeOp) - { - return false; - } - - auto ref = storeOp.getReference(); - if (auto varOp = ref.getDefiningOp()) - { - if (!ownSlot(varOp)) - { - return false; - } - - chain.roots.push_back({ref, Chain::Slot}); - return true; - } - - if (!isPlace(ref)) - { - return false; - } - - chain = chainOf(nullptr, ref); - if (!chain.handles.empty() || chain.roots.empty()) - { - return false; - } - - // a global's field, or a captured variable's: not a place of this function - return llvm::none_of(chain.roots, [](auto &root) { - auto loadOp = root.first.template getDefiningOp(); - return loadOp && (loadOp.getReference().template getDefiningOp() || - isLoadedCell(loadOp.getReference())); - }); - } + bool destinationChain(mlir::Operation *taker, Chain &chain); // Is `use`, a use of `value` after `taker` moved it, a read through the place it moved into // (spec 29)? `made` is where the value is made again (its definition, the local's declaration @@ -1368,145 +1296,20 @@ class OwnershipInferencePass // the read - or to anything the read produced - makes the value again or moves it again: such a // path is the only way the place can stop holding the value before the read. bool readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, - llvm::ArrayRef made) - { - if (!isReadAfterMove(use, value)) - { - return false; - } - - Chain chain; - if (!destinationChain(taker, chain)) - { - return false; - } - - // A read again of the old name after a drop still finds the moved value - the slot is not - // emptied by a move - so neither the read nor the slot's read is where it is made again. - llvm::SmallVector kills(made.begin(), made.end()); - kills.push_back(taker); - llvm::SmallVector uses; - uses.push_back({use, {kills.begin(), kills.end()}}); - for (auto result : use->getResults()) - { - if (!mayPointInto(result)) - { - continue; - } - - // what the read produced - a field's reference, a bound method - still points into the - // block; a call's result that borrows its argument is the value again and may not be kept - if (walkBorrowed(result, kills, uses, nullptr, Keeping{nullptr, false}, false, !!borrowingCall(result))) - { - return false; - } - } - - // a method reached through it whose callee keeps its `this`: a second owner - for (auto &borrowUse : uses) - { - if (!isCall(borrowUse.op)) - { - continue; - } - - auto args = callArgs(borrowUse.op); - for (auto index : ownedParams(borrowUse.op)) - { - if (static_cast(index) < args.size() && - mlir::isa_and_nonnull(args[index].getDefiningOp())) - { - return false; - } - } - } - - auto outlives = [&](mlir::Operation *end) { - if (end == taker || !reachableAfter(taker, end, made)) - { - return false; - } - - return llvm::any_of(uses, [&](BorrowUse &borrowUse) { - return (borrowUse.op != end || !borrowUse.callee) && reachableAfter(end, borrowUse.op, borrowUse.kills); - }); - }; - - for (auto *end : rootEnds(chain)) - { - if (outlives(end)) - { - return false; - } - } - - for (auto *drop : drops) - { - if (dropsChain(drop, chain, nullptr, value) && outlives(drop)) - { - return false; - } - } - - return true; - } + llvm::ArrayRef made); // Where rc takes the receiver's reference: its `ts.Retain` of the value, when that comes // first in the taker's block, else the taker itself (a declaration, whose `ts.RetainSlot` // follows it). A release between the two - a `return`'s scope exit runs after its retain // and before the store into the return slot - is after the move. - static mlir::Operation *acquirePoint(mlir::Operation *taker, mlir::Value value) - { - // a store into a record, which rc retains as a whole after it is filled - if (auto *retain = recordRetainOfStore(taker)) - { - return retain; - } - - mlir::Operation *first = taker; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user) && user->getBlock() == taker->getBlock() && - user->isBeforeInBlock(first)) - { - first = user; - } - }); - - return first; - } + static mlir::Operation *acquirePoint(mlir::Operation *taker, mlir::Value value); // Did rc give this taker a reference of its own - a `ts.Retain` of the value in front of it, // or the `ts.RetainSlot` of an owning local declared from it? Only then is there a release on // the taker's side to give the value back once it has moved. `ts.NewInterface` is a taker to // this pass but a view to rc, which retains nothing for it and relies on the instance's own // release; moving the instance into it would leave nothing to release it at all. - static bool takerAcquires(mlir::Operation *taker, mlir::Value value) - { - // a callee that keeps its argument retained it in its own body under rc - if (isCall(taker)) - { - return true; - } - - if (recordRetainOfStore(taker)) - { - return true; - } - - if (auto varOp = mlir::dyn_cast(taker)) - { - return llvm::any_of(varOp.getResult().getUsers(), - [](mlir::Operation *user) { return mlir::isa(user); }); - } - - auto acquires = false; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && - user->isBeforeInBlock(taker)); - }); - - return acquires; - } + static bool takerAcquires(mlir::Operation *taker, mlir::Value value); // The declaration's name survives only as debug metadata (`--di`). At this level MLIRGen's // form of it is a NameLoc first inside the location fused with the scope - what LowerToLLVM's diff --git a/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp b/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp new file mode 100644 index 000000000..d5699e09e --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp @@ -0,0 +1,221 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): reads after a move, through the new +// owner (spec 29). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead) + { + auto all = true; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + all = all && (mlir::isa(user) || + (!mlir::isa(user) && isRead(user, used))); + }); + + return all; + } + + bool OwnershipInferencePass::isReadAfterMove(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return isBorrow(user, value); + } + + return isCall(user) && !isKeptArgument(user, value); + } + + bool OwnershipInferencePass::destinationChain(mlir::Operation *taker, Chain &chain) + { + auto ownSlot = [](mlir_ts::VariableOp varOp) { + return varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false); + }; + + if (auto varOp = mlir::dyn_cast(taker)) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({varOp.getResult(), Chain::Slot}); + return true; + } + + auto storeOp = mlir::dyn_cast(taker); + if (!storeOp) + { + return false; + } + + auto ref = storeOp.getReference(); + if (auto varOp = ref.getDefiningOp()) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({ref, Chain::Slot}); + return true; + } + + if (!isPlace(ref)) + { + return false; + } + + chain = chainOf(nullptr, ref); + if (!chain.handles.empty() || chain.roots.empty()) + { + return false; + } + + // a global's field, or a captured variable's: not a place of this function + return llvm::none_of(chain.roots, [](auto &root) { + auto loadOp = root.first.template getDefiningOp(); + return loadOp && (loadOp.getReference().template getDefiningOp() || + isLoadedCell(loadOp.getReference())); + }); + } + + bool OwnershipInferencePass::readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, + llvm::ArrayRef made) + { + if (!isReadAfterMove(use, value)) + { + return false; + } + + Chain chain; + if (!destinationChain(taker, chain)) + { + return false; + } + + // A read again of the old name after a drop still finds the moved value - the slot is not + // emptied by a move - so neither the read nor the slot's read is where it is made again. + llvm::SmallVector kills(made.begin(), made.end()); + kills.push_back(taker); + llvm::SmallVector uses; + uses.push_back({use, {kills.begin(), kills.end()}}); + for (auto result : use->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + // what the read produced - a field's reference, a bound method - still points into the + // block; a call's result that borrows its argument is the value again and may not be kept + if (walkBorrowed(result, kills, uses, nullptr, Keeping{nullptr, false}, false, !!borrowingCall(result))) + { + return false; + } + } + + // a method reached through it whose callee keeps its `this`: a second owner + for (auto &borrowUse : uses) + { + if (!isCall(borrowUse.op)) + { + continue; + } + + auto args = callArgs(borrowUse.op); + for (auto index : ownedParams(borrowUse.op)) + { + if (static_cast(index) < args.size() && + mlir::isa_and_nonnull(args[index].getDefiningOp())) + { + return false; + } + } + } + + auto outlives = [&](mlir::Operation *end) { + if (end == taker || !reachableAfter(taker, end, made)) + { + return false; + } + + return llvm::any_of(uses, [&](BorrowUse &borrowUse) { + return (borrowUse.op != end || !borrowUse.callee) && reachableAfter(end, borrowUse.op, borrowUse.kills); + }); + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value) && outlives(drop)) + { + return false; + } + } + + return true; + } + + mlir::Operation *OwnershipInferencePass::acquirePoint(mlir::Operation *taker, mlir::Value value) + { + // a store into a record, which rc retains as a whole after it is filled + if (auto *retain = recordRetainOfStore(taker)) + { + return retain; + } + + mlir::Operation *first = taker; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user) && user->getBlock() == taker->getBlock() && + user->isBeforeInBlock(first)) + { + first = user; + } + }); + + return first; + } + + bool OwnershipInferencePass::takerAcquires(mlir::Operation *taker, mlir::Value value) + { + // a callee that keeps its argument retained it in its own body under rc + if (isCall(taker)) + { + return true; + } + + if (recordRetainOfStore(taker)) + { + return true; + } + + if (auto varOp = mlir::dyn_cast(taker)) + { + return llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + } + + auto acquires = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && + user->isBeforeInBlock(taker)); + }); + + return acquires; + } + +} // namespace own_inference From 4438c059ab021f85cbf1dd53e0cda4330f9785bc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 12:45:07 +0100 Subject: [PATCH 25/25] own passes: OwnershipInferenceDiagnostics.cpp out of OwnershipInferencePass.h (I1) Names and diagnostics - what a value, a place or a call is called in an error, and the errors themselves. A pure move of describeCall, calleeName, describePlace, pathOf, fieldName, ownerName, varName, nameAt, describe, reportSecondReference, reportShapeOfSecondReference, paramName, isReturned, otherReturn, noteLostFacts, reportKeptOnSomePaths, reportHandleInOwningUnion, printed, reportGivenNotOwned, reportUseAfterMove, reportMovedInLoop, reportBorrowOutlives, reportPlaceBorrowOutlives, reportPlaceBorrowerAssigned, reportPlaceBorrowEscapes, reportMovedWhileBorrowed, reportBorrowEscapes, reportBorrowerAssigned, reportEscapingCellNotOwned, reportUsedAfterEscapingCapture, reportClosureOutlives, reportCaptureNotOwned, reportCapturedParamAssigned, reportMovedOnSomePaths. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/CMakeLists.txt | 1 + .../OwnershipInferenceDiagnostics.cpp | 748 ++++++++++++++++++ .../lib/TypeScript/OwnershipInferencePass.h | 734 +---------------- 3 files changed, 783 insertions(+), 700 deletions(-) create mode 100644 tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 4e45d80ee..988e91784 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -35,6 +35,7 @@ add_mlir_dialect_library(MLIRTypeScript OwnershipInferencePass.cpp OwnershipInferenceBorrowers.cpp OwnershipInferenceClosures.cpp + OwnershipInferenceDiagnostics.cpp OwnershipInferenceEdgeReleases.cpp OwnershipInferenceGlobalReads.cpp OwnershipInferenceKeptParams.cpp diff --git a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp new file mode 100644 index 000000000..f913bbf25 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp @@ -0,0 +1,748 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): names and diagnostics - what a value, +// a place or a call is called in an error, and the errors themselves. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + std::string OwnershipInferencePass::describeCall(mlir::Operation *call) + { + auto callee = calleeName(call); + return callee.empty() ? "a call's result" : "the result of '" + callee + "'"; + } + + std::string OwnershipInferencePass::calleeName(mlir::Operation *call) + { + mlir::StringAttr callee; + auto value = calleeValue(call); + if (auto symbol = directCallee(call)) + { + callee = symbol.getAttr(); + } + else if (auto calleeOp = value ? value.getDefiningOp() : nullptr) + { + if (auto getMethodOp = mlir::dyn_cast(calleeOp)) + { + calleeOp = getMethodOp.getBoundFunc().getDefiningOp(); + } + + if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + // a method reached through an interface (spec 31): `.` + auto method = mlir::dyn_cast(refOp.getIdentifier()); + auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); + if (method && interfaceType) + { + return (interfaceType.getName().getValue() + "." + method.getValue()).str(); + } + } + } + + return callee ? callee.getValue().str() : std::string(); + } + + std::string OwnershipInferencePass::describePlace(mlir::Value ref) + { + auto path = pathOf(ref); + if (path.empty()) + { + return ref.getDefiningOp() ? "an element" : "a field"; + } + + return "'" + path + "'"; + } + + std::string OwnershipInferencePass::pathOf(mlir::Value ref) + { + auto place = placeBase(ref); + std::string step; + switch (place.kind) + { + case PlaceKind::Field: { + auto field = fieldName(ref.getDefiningOp()); + if (field.empty()) + { + return {}; + } + + step = "." + field; + break; + } + case PlaceKind::Element: + step = "[]"; + break; + case PlaceKind::SharedValue: + step = ".value"; + break; + case PlaceKind::InterfaceField: { + auto field = mlir::dyn_cast(ref.getDefiningOp().getIdentifier()); + if (!field) + { + return {}; + } + + step = "." + field.getValue().str(); + break; + } + case PlaceKind::None: + return {}; + } + + auto base = rootOf(place.base); + std::string prefix; + if (isPlace(base)) + { + prefix = pathOf(base); + } + else if (auto loadOp = base.getDefiningOp()) + { + auto from = loadOp.getReference(); + if (isPlace(from)) + { + prefix = pathOf(from); + } + else if (auto varOp = from.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else if (auto global = from.getDefiningOp()) + { + prefix = global.getGlobalName().str(); + } + } + else if (auto varOp = base.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else + { + prefix = ownerName(base).str(); + } + + // a name MLIRGen made up (`.a`, the array a `for...of` walks) means nothing to the reader + if (prefix.empty() || prefix == "this value" || prefix.front() == '.') + { + return {}; + } + + return prefix + step; + } + + std::string OwnershipInferencePass::fieldName(mlir_ts::PropertyRefOp propertyRefOp) + { + auto type = propertyRefOp.getObjectRef().getType(); + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + + if (auto classType = mlir::dyn_cast(type)) + { + type = classType.getStorageType(); + } + + auto position = propertyRefOp.getPosition(); + mlir::Attribute id; + if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) + { + id = storageType.getId(position); + } + else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) + { + id = tupleType.getId(position); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.str(); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.getValue().str(); + } + + return {}; + } + + llvm::StringRef OwnershipInferencePass::ownerName(mlir::Value value) + { + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) + { + name = nameAt(user->getLoc()); + } + }); + + return name; + } + + llvm::StringRef OwnershipInferencePass::varName(mlir_ts::VariableOp varOp) + { + return nameAt(varOp.getLoc()); + } + + llvm::StringRef OwnershipInferencePass::nameAt(mlir::Location location) + { + if (auto fused = mlir::dyn_cast>(location)) + { + if (auto name = fused.getMetadata().getName()) + { + return name.getValue(); + } + } + + if (auto scoped = mlir::dyn_cast>(location)) + { + if (auto named = mlir::dyn_cast_or_null(scoped.getLocations().front())) + { + return named.getName().getValue(); + } + } + + return "this value"; + } + + llvm::StringRef OwnershipInferencePass::describe(mlir::Value value) + { + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (auto varOp = mlir::dyn_cast(user); varOp && name == "this value") + { + name = varName(varOp); + } + }); + + return name; + } + + void OwnershipInferencePass::reportSecondReference(mlir::Operation *op, mlir::Value value) + { + if (!reporting()) + { + return; + } + + llvm::StringRef name = "this value"; + if (auto retainSlotOp = mlir::dyn_cast(op)) + { + if (auto varOp = retainSlotOp.getSlot().getDefiningOp()) + { + name = varName(varOp); + } + } + else if (auto varOp = mlir::dyn_cast(op)) + { + name = varName(varOp); + } + + if (!value && mlir::isa(op)) + { + value = retainedValue(op); + } + + if (!value || !reportShapeOfSecondReference(op, rootOf(value), name)) + { + auto diag = op->emitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + noteLostFacts(diag); + } + + signalPassFailure(); + } + + bool OwnershipInferencePass::reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name) + { + auto f = getFunction(); + + // returned, where another return gives a borrow of an argument and this one does not, or + // the other way round: the callers cannot be told which they get + if (isReturned(root)) + { + auto param = borrowedParam(root); + if (auto *other = otherReturn(param)) + { + auto diag = op->emitError("'") << f.getName() + << "' returns a borrow of its argument on some paths and another value on " + "others; -mm=own cannot tell its callers whether they own the result"; + diag.attachNote(other->getLoc()) + << (param >= 0 ? "returns another value here" : "returns a borrow of its argument here"); + return true; + } + } + + // a parameter's value, or a part of it, returned or kept: a fact its callers must know + // (spec 15), and noteLostFacts says why they cannot + if (auto param = borrowedParam(root); param >= 0) + { + auto diag = op->emitError("'") << (name == "this value" ? paramName(param) : name); + if (isReturned(root)) + { + diag << "' is a parameter, returned here, but -mm=own cannot tell the callers of '" << f.getName() + << "' that the result borrows it"; + } + else + { + diag << "' is a parameter, kept here, but -mm=own cannot make the callers of '" << f.getName() + << "' give it up"; + } + + noteLostFacts(diag); + return true; + } + + // a store into a local whose value is not this function's to replace: a parameter's slot, or + // a local that owns nothing (MLIRGen's takeOwnershipOfLocal) + auto storeOp = mlir::dyn_cast(op); + if (auto slot = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + slot && !isOwningVariable(slot) && !isResultSlot(slot)) + { + // a parameter's slot, assigned (isParameterSlot is the unassigned one) + auto argument = mlir::dyn_cast_or_null(slot.getInitializer()); + if (argument && argument.getOwner()->isEntryBlock() && + mlir::isa(argument.getOwner()->getParentOp())) + { + op->emitError("'") << varName(slot) << "' is a parameter and cannot be assigned under -mm=own yet"; + return true; + } + + // MLIRGen makes no owner of a local declared without a value, nor of one declared in a + // catch or finally clause, whose release would be a call inside an exception funclet + if (!slot.getInitializer()) + { + op->emitError("'") << varName(slot) + << "' owns nothing under -mm=own (it is declared without a value, or in a catch or " + "finally clause), so it cannot be given a new value here"; + return true; + } + } + + // merged from branches or loop iterations + if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) + { + op->emitError("'") << name + << "' is merged from several branches (`?:`, `&&`, `||`, `??`, a loop); -mm=own cannot " + "prove which one owns it yet"; + return true; + } + + auto loadOp = root.getDefiningOp(); + + // a read of a global + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp()) + { + // a local that reads it: phase 3's borrow, which a call or an assignment of the global + // may end + if (mlir::isa(op)) + { + op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() + << "', but -mm=own cannot prove the global keeps its value while '" << name + << "' is used: a call or an assignment may overwrite it"; + return true; + } + + op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() + << "' and cannot be stored, returned or captured"; + return true; + } + + // a read of a captured variable, through its cell + if (loadOp && isLoadedCell(loadOp.getReference())) + { + op->emitError("'") << name << "' borrows a captured variable and cannot be stored, returned or captured"; + return true; + } + + // an object literal built in a local, holding a value it owns: moved only into an owning + // local or the result (recordRetainOf) + auto record = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!record && storeOp) + { + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + record = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + } + + if (record && !isOwningVariable(record) && record.getInitializer() && + record.getInitializer().getDefiningOp()) + { + op->emitError("an object literal that holds a value it owns is moved only into a local or " + "returned as it is; -mm=own cannot move it here yet"); + return true; + } + + return false; + } + + llvm::StringRef OwnershipInferencePass::paramName(int index) + { + llvm::StringRef name = "this value"; + getFunction()->walk([&](mlir_ts::VariableOp varOp) { + auto argument = mlir::dyn_cast_or_null(varOp.getInitializer()); + if (argument && argument.getOwner()->isEntryBlock() && static_cast(argument.getArgNumber()) == index) + { + name = varName(varOp); + } + }); + + return name; + } + + bool OwnershipInferencePass::isReturned(mlir::Value root) + { + auto returned = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value used) { + auto storeOp = mlir::dyn_cast(user); + auto slot = storeOp && storeOp.getValue() == used ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + returned = returned || (slot && isResultSlot(slot)); + }); + + return returned; + } + + mlir::Operation *OwnershipInferencePass::otherReturn(int param) + { + mlir::Operation *found = nullptr; + getFunction()->walk([&](mlir_ts::StoreOp storeOp) { + auto slot = storeOp.getReference().getDefiningOp(); + if (found || !slot || !isResultSlot(slot) || holdsNoBlock(storeOp.getValue())) + { + return; + } + + auto borrows = borrowedParam(storeOp.getValue()); + if (param >= 0 ? borrows != param : borrows >= 0) + { + found = storeOp; + } + }); + + return found; + } + + void OwnershipInferencePass::noteLostFacts(mlir::InFlightDiagnostic &diag) + { + auto f = getFunction(); + if (auto why = f->getAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) + { + diag.attachNote(f.getLoc()) << "'" << f.getName() << "' could return a borrow or keep a parameter, but " + << why.getValue(); + } + } + + void OwnershipInferencePass::reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = move->emitError("'") << name + << "' is moved here on some paths only; -mm=own cannot release it " + "on the others yet"; + diag.attachNote(exit->getLoc()) << "returns here without moving it; the caller gave it up"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportHandleInOwningUnion(mlir::Operation *op) + { + auto handleIn = llvm::any_of(op->getOperandTypes(), [](mlir::Type type) { + return MLIRTypeHelper::isSharedHandleType(type); + }); + if (!handleIn) + { + return; + } + + for (auto resultType : op->getResultTypes()) + { + if (!MLIRTypeHelper::isSharedInOwningUnionType(resultType)) + { + continue; + } + + if (!reporting()) + { + return; + } + + auto unionType = resultType; + if (auto optionalType = mlir::dyn_cast(unionType)) + { + unionType = optionalType.getElementType(); + } + + mlir::Type shared; + mlir::Type owning; + for (auto member : mlir::cast(unionType).getTypes()) + { + if (mlir::isa(member)) + { + shared = shared ? shared : member; + } + else if (!owning && MLIRTypeHelper::ownsHeapMemoryOfUnionMember(member)) + { + owning = member; + } + } + + auto diag = op->emitError("a ") << printed(shared) << " in a union with "; + if (owning) + { + diag << "'" << printed(owning) << "', which -mm=own does not count,"; + } + else + { + diag << "a member -mm=own does not count,"; + } + + diag << " is not supported"; + signalPassFailure(); + return; + } + } + + std::string OwnershipInferencePass::printed(mlir::Type type) + { + llvm::SmallString<128> text; + llvm::raw_svector_ostream out(text); + MLIRPrinter printer{}; + printer.printType(out, type); + return text.str().str(); + } + + void OwnershipInferencePass::reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) + { + if (!reporting()) + { + return; + } + + llvm::StringRef name = ownerName(arg); + if (auto loadOp = rootOf(arg).getDefiningOp()) + { + if (auto varOp = loadOp.getReference().getDefiningOp()) + { + name = varName(varOp); + } + } + + auto callee = calleeName(call); + auto diag = call->emitError("argument '") << name << "' is given to '" << (callee.empty() ? "a function" : callee) + << "', which keeps it, but -mm=own cannot move it here: this " + "function does not own it"; + noteLostFacts(diag); + signalPassFailure(); + } + + void OwnershipInferencePass::reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' is used here after its value was moved"; + diag.attachNote(move->getLoc()) << "value moved here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportMovedInLoop(mlir::Operation *move, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + move->emitError("'") << name + << "' is moved inside a loop but was made outside it; only a borrow could do " + "that, and -mm=own cannot prove one yet"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows '" << owner << "' but is used here after '" << owner + << "' is released or overwritten"; + diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, + llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows " << place + << " but is used here after it may be released or overwritten"; + diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name << "' borrows " << place << " and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + auto diag = op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; + noteLostFacts(diag); + signalPassFailure(); + } + + void OwnershipInferencePass::reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << owner << "' is moved here while '" << borrower + << "' borrows it; a borrowed value cannot be moved"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + auto diag = op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; + noteLostFacts(diag); + signalPassFailure(); + } + + void OwnershipInferencePass::reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name << "' borrows '" << owner << "' and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why) + { + if (!reporting()) + { + return; + } + + auto diag = capture->emitError("a closure that escapes owns what it captures, but -mm=own cannot move this " + "variable into it: ") + << why; + diag.attachNote(escape->getLoc()) << "the closure escapes here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' is captured by a closure that escapes, and is still used here"; + diag.attachNote(capture->getLoc()) << "the closure takes it here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, + llvm::StringRef captured) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << closure << "' borrows '" << captured << "', which it captures, but runs here after '" + << captured << "' is released"; + diag.attachNote(end->getLoc()) << "'" << captured << "' is released here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << captured << "' is captured by a closure, but -mm=own cannot tell who owns its value here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name + << "' is a captured parameter and cannot be assigned under -mm=own: its value is the " + "caller's"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, + mlir::Operation *usedAt) + { + if (!reporting()) + { + return; + } + + auto diag = move->emitError("'") << name + << "' is moved here on some paths only; -mm=own cannot release it " + "on the others yet"; + diag.attachNote(release->getLoc()) << "released here on every path"; + if (usedAt) + { + diag.attachNote(usedAt->getLoc()) + << "used here, after the paths join, so it cannot be released before they do"; + } + + signalPassFailure(); + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 3acef361d..31fd373bb 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -822,195 +822,22 @@ class OwnershipInferencePass void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase); // `the result of 'first'`, for a call whose result borrows an argument. - static std::string describeCall(mlir::Operation *call) - { - auto callee = calleeName(call); - return callee.empty() ? "a call's result" : "the result of '" + callee + "'"; - } + static std::string describeCall(mlir::Operation *call); // The function a call names - directly, or the method it calls through a vtable - or empty. - static std::string calleeName(mlir::Operation *call) - { - mlir::StringAttr callee; - auto value = calleeValue(call); - if (auto symbol = directCallee(call)) - { - callee = symbol.getAttr(); - } - else if (auto calleeOp = value ? value.getDefiningOp() : nullptr) - { - if (auto getMethodOp = mlir::dyn_cast(calleeOp)) - { - calleeOp = getMethodOp.getBoundFunc().getDefiningOp(); - } - - if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - // a method reached through an interface (spec 31): `.` - auto method = mlir::dyn_cast(refOp.getIdentifier()); - auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); - if (method && interfaceType) - { - return (interfaceType.getName().getValue() + "." + method.getValue()).str(); - } - } - } - - return callee ? callee.getValue().str() : std::string(); - } + static std::string calleeName(mlir::Operation *call); // `'h.c'`, `'arr[]'`, `'h.c.v'` - or `a field`, `an element` when the root has no name (a // temporary, or no --di). - std::string describePlace(mlir::Value ref) - { - auto path = pathOf(ref); - if (path.empty()) - { - return ref.getDefiningOp() ? "an element" : "a field"; - } - - return "'" + path + "'"; - } + std::string describePlace(mlir::Value ref); - std::string pathOf(mlir::Value ref) - { - auto place = placeBase(ref); - std::string step; - switch (place.kind) - { - case PlaceKind::Field: { - auto field = fieldName(ref.getDefiningOp()); - if (field.empty()) - { - return {}; - } - - step = "." + field; - break; - } - case PlaceKind::Element: - step = "[]"; - break; - case PlaceKind::SharedValue: - step = ".value"; - break; - case PlaceKind::InterfaceField: { - auto field = mlir::dyn_cast(ref.getDefiningOp().getIdentifier()); - if (!field) - { - return {}; - } - - step = "." + field.getValue().str(); - break; - } - case PlaceKind::None: - return {}; - } + std::string pathOf(mlir::Value ref); - auto base = rootOf(place.base); - std::string prefix; - if (isPlace(base)) - { - prefix = pathOf(base); - } - else if (auto loadOp = base.getDefiningOp()) - { - auto from = loadOp.getReference(); - if (isPlace(from)) - { - prefix = pathOf(from); - } - else if (auto varOp = from.getDefiningOp()) - { - prefix = varName(varOp).str(); - } - else if (auto global = from.getDefiningOp()) - { - prefix = global.getGlobalName().str(); - } - } - else if (auto varOp = base.getDefiningOp()) - { - prefix = varName(varOp).str(); - } - else - { - prefix = ownerName(base).str(); - } - - // a name MLIRGen made up (`.a`, the array a `for...of` walks) means nothing to the reader - if (prefix.empty() || prefix == "this value" || prefix.front() == '.') - { - return {}; - } - - return prefix + step; - } - - static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp) - { - auto type = propertyRefOp.getObjectRef().getType(); - if (auto refType = mlir::dyn_cast(type)) - { - type = refType.getElementType(); - } - - if (auto classType = mlir::dyn_cast(type)) - { - type = classType.getStorageType(); - } - - auto position = propertyRefOp.getPosition(); - mlir::Attribute id; - if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) - { - id = storageType.getId(position); - } - else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) - { - id = tupleType.getId(position); - } - - if (auto name = mlir::dyn_cast_or_null(id)) - { - return name.str(); - } - - if (auto name = mlir::dyn_cast_or_null(id)) - { - return name.getValue().str(); - } - - return {}; - } + static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp); // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it // has a ts.DebugVariable, whose location carries the name. - static llvm::StringRef ownerName(mlir::Value value) - { - llvm::StringRef name = "this value"; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user)) - { - name = nameAt(user->getLoc()); - } - }); - - return name; - } + static llvm::StringRef ownerName(mlir::Value value); // A `ts.RetainSlot`'s slot holds its initializer where the retain is. MLIRGen puts the retain at // the declaration (takeOwnershipOfLocal), and nothing between the two may write the slot: in @@ -1315,570 +1142,77 @@ class OwnershipInferencePass // form of it is a NameLoc first inside the location fused with the scope - what LowerToLLVM's // preserveTypesForDebugInfo reads - and after that pass it is a DILocalVariable. Without // `--di` the error still points at the right place. - static llvm::StringRef varName(mlir_ts::VariableOp varOp) - { - return nameAt(varOp.getLoc()); - } - - static llvm::StringRef nameAt(mlir::Location location) - { - if (auto fused = mlir::dyn_cast>(location)) - { - if (auto name = fused.getMetadata().getName()) - { - return name.getValue(); - } - } - - if (auto scoped = mlir::dyn_cast>(location)) - { - if (auto named = mlir::dyn_cast_or_null(scoped.getLocations().front())) - { - return named.getName().getValue(); - } - } + static llvm::StringRef varName(mlir_ts::VariableOp varOp); - return "this value"; - } + static llvm::StringRef nameAt(mlir::Location location); - static llvm::StringRef describe(mlir::Value value) - { - llvm::StringRef name = "this value"; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (auto varOp = mlir::dyn_cast(user); varOp && name == "this value") - { - name = varName(varOp); - } - }); - - return name; - } + static llvm::StringRef describe(mlir::Value value); // A second reference nobody proved safe. `value` is the value it is to, when the caller knows // it; a retain names its own. The shapes this pass recognises get the rule they break (spec // 21.2); anything else, the catch-all. - void reportSecondReference(mlir::Operation *op, mlir::Value value = {}) - { - if (!reporting()) - { - return; - } - - llvm::StringRef name = "this value"; - if (auto retainSlotOp = mlir::dyn_cast(op)) - { - if (auto varOp = retainSlotOp.getSlot().getDefiningOp()) - { - name = varName(varOp); - } - } - else if (auto varOp = mlir::dyn_cast(op)) - { - name = varName(varOp); - } - - if (!value && mlir::isa(op)) - { - value = retainedValue(op); - } - - if (!value || !reportShapeOfSecondReference(op, rootOf(value), name)) - { - auto diag = op->emitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; - noteLostFacts(diag); - } - - signalPassFailure(); - } + void reportSecondReference(mlir::Operation *op, mlir::Value value = {}); // The named shapes of a second reference to `root` (spec 21.2). False when it is none of them. - bool reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name) - { - auto f = getFunction(); - - // returned, where another return gives a borrow of an argument and this one does not, or - // the other way round: the callers cannot be told which they get - if (isReturned(root)) - { - auto param = borrowedParam(root); - if (auto *other = otherReturn(param)) - { - auto diag = op->emitError("'") << f.getName() - << "' returns a borrow of its argument on some paths and another value on " - "others; -mm=own cannot tell its callers whether they own the result"; - diag.attachNote(other->getLoc()) - << (param >= 0 ? "returns another value here" : "returns a borrow of its argument here"); - return true; - } - } - - // a parameter's value, or a part of it, returned or kept: a fact its callers must know - // (spec 15), and noteLostFacts says why they cannot - if (auto param = borrowedParam(root); param >= 0) - { - auto diag = op->emitError("'") << (name == "this value" ? paramName(param) : name); - if (isReturned(root)) - { - diag << "' is a parameter, returned here, but -mm=own cannot tell the callers of '" << f.getName() - << "' that the result borrows it"; - } - else - { - diag << "' is a parameter, kept here, but -mm=own cannot make the callers of '" << f.getName() - << "' give it up"; - } - - noteLostFacts(diag); - return true; - } - - // a store into a local whose value is not this function's to replace: a parameter's slot, or - // a local that owns nothing (MLIRGen's takeOwnershipOfLocal) - auto storeOp = mlir::dyn_cast(op); - if (auto slot = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - slot && !isOwningVariable(slot) && !isResultSlot(slot)) - { - // a parameter's slot, assigned (isParameterSlot is the unassigned one) - auto argument = mlir::dyn_cast_or_null(slot.getInitializer()); - if (argument && argument.getOwner()->isEntryBlock() && - mlir::isa(argument.getOwner()->getParentOp())) - { - op->emitError("'") << varName(slot) << "' is a parameter and cannot be assigned under -mm=own yet"; - return true; - } - - // MLIRGen makes no owner of a local declared without a value, nor of one declared in a - // catch or finally clause, whose release would be a call inside an exception funclet - if (!slot.getInitializer()) - { - op->emitError("'") << varName(slot) - << "' owns nothing under -mm=own (it is declared without a value, or in a catch or " - "finally clause), so it cannot be given a new value here"; - return true; - } - } - - // merged from branches or loop iterations - if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) - { - op->emitError("'") << name - << "' is merged from several branches (`?:`, `&&`, `||`, `??`, a loop); -mm=own cannot " - "prove which one owns it yet"; - return true; - } - - auto loadOp = root.getDefiningOp(); - - // a read of a global - if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp()) - { - // a local that reads it: phase 3's borrow, which a call or an assignment of the global - // may end - if (mlir::isa(op)) - { - op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() - << "', but -mm=own cannot prove the global keeps its value while '" << name - << "' is used: a call or an assignment may overwrite it"; - return true; - } - - op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() - << "' and cannot be stored, returned or captured"; - return true; - } - - // a read of a captured variable, through its cell - if (loadOp && isLoadedCell(loadOp.getReference())) - { - op->emitError("'") << name << "' borrows a captured variable and cannot be stored, returned or captured"; - return true; - } - - // an object literal built in a local, holding a value it owns: moved only into an owning - // local or the result (recordRetainOf) - auto record = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!record && storeOp) - { - auto propertyRefOp = storeOp.getReference().getDefiningOp(); - record = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - } - - if (record && !isOwningVariable(record) && record.getInitializer() && - record.getInitializer().getDefiningOp()) - { - op->emitError("an object literal that holds a value it owns is moved only into a local or " - "returned as it is; -mm=own cannot move it here yet"); - return true; - } - - return false; - } + bool reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name); // The name of parameter `index`, as its slot carries it (under `--di`). - llvm::StringRef paramName(int index) - { - llvm::StringRef name = "this value"; - getFunction()->walk([&](mlir_ts::VariableOp varOp) { - auto argument = mlir::dyn_cast_or_null(varOp.getInitializer()); - if (argument && argument.getOwner()->isEntryBlock() && static_cast(argument.getArgNumber()) == index) - { - name = varName(varOp); - } - }); - - return name; - } + llvm::StringRef paramName(int index); // Is `root`, or a view of it, stored into the result slot? - static bool isReturned(mlir::Value root) - { - auto returned = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value used) { - auto storeOp = mlir::dyn_cast(user); - auto slot = storeOp && storeOp.getValue() == used ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - returned = returned || (slot && isResultSlot(slot)); - }); - - return returned; - } + static bool isReturned(mlir::Value root); // A return that disagrees with one of a borrow of parameter `param` (with -1, with one that // borrows nothing): a store into the result slot of a value that holds a block and borrows // another parameter or none - or, for -1, borrows a parameter. Null when there is none. - mlir::Operation *otherReturn(int param) - { - mlir::Operation *found = nullptr; - getFunction()->walk([&](mlir_ts::StoreOp storeOp) { - auto slot = storeOp.getReference().getDefiningOp(); - if (found || !slot || !isResultSlot(slot) || holdsNoBlock(storeOp.getValue())) - { - return; - } - - auto borrows = borrowedParam(storeOp.getValue()); - if (param >= 0 ? borrows != param : borrows >= 0) - { - found = storeOp; - } - }); - - return found; - } + mlir::Operation *otherReturn(int param); // Where the body would have been fine had its callers known a fact about it, say why they // cannot. - void noteLostFacts(mlir::InFlightDiagnostic &diag) - { - auto f = getFunction(); - if (auto why = f->getAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) - { - diag.attachNote(f.getLoc()) << "'" << f.getName() << "' could return a borrow or keep a parameter, but " - << why.getValue(); - } - } - - void reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name) - { - if (!reporting()) - { - return; - } + void noteLostFacts(mlir::InFlightDiagnostic &diag); - auto diag = move->emitError("'") << name - << "' is moved here on some paths only; -mm=own cannot release it " - "on the others yet"; - diag.attachNote(exit->getLoc()) << "returns here without moving it; the caller gave it up"; - signalPassFailure(); - } + void reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name); // A handle made into a union whose other members include one that owns a block (spec 23.2): // `Shared | string` given a Shared. gc, none and rc accept it. - void reportHandleInOwningUnion(mlir::Operation *op) - { - auto handleIn = llvm::any_of(op->getOperandTypes(), [](mlir::Type type) { - return MLIRTypeHelper::isSharedHandleType(type); - }); - if (!handleIn) - { - return; - } + void reportHandleInOwningUnion(mlir::Operation *op); - for (auto resultType : op->getResultTypes()) - { - if (!MLIRTypeHelper::isSharedInOwningUnionType(resultType)) - { - continue; - } - - if (!reporting()) - { - return; - } - - auto unionType = resultType; - if (auto optionalType = mlir::dyn_cast(unionType)) - { - unionType = optionalType.getElementType(); - } - - mlir::Type shared; - mlir::Type owning; - for (auto member : mlir::cast(unionType).getTypes()) - { - if (mlir::isa(member)) - { - shared = shared ? shared : member; - } - else if (!owning && MLIRTypeHelper::ownsHeapMemoryOfUnionMember(member)) - { - owning = member; - } - } - - auto diag = op->emitError("a ") << printed(shared) << " in a union with "; - if (owning) - { - diag << "'" << printed(owning) << "', which -mm=own does not count,"; - } - else - { - diag << "a member -mm=own does not count,"; - } - - diag << " is not supported"; - signalPassFailure(); - return; - } - } + static std::string printed(mlir::Type type); - static std::string printed(mlir::Type type) - { - llvm::SmallString<128> text; - llvm::raw_svector_ostream out(text); - MLIRPrinter printer{}; - printer.printType(out, type); - return text.str().str(); - } + void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg); - void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) - { - if (!reporting()) - { - return; - } + void reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name); - llvm::StringRef name = ownerName(arg); - if (auto loadOp = rootOf(arg).getDefiningOp()) - { - if (auto varOp = loadOp.getReference().getDefiningOp()) - { - name = varName(varOp); - } - } + void reportMovedInLoop(mlir::Operation *move, llvm::StringRef name); - auto callee = calleeName(call); - auto diag = call->emitError("argument '") << name << "' is given to '" << (callee.empty() ? "a function" : callee) - << "', which keeps it, but -mm=own cannot move it here: this " - "function does not own it"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' is used here after its value was moved"; - diag.attachNote(move->getLoc()) << "value moved here"; - signalPassFailure(); - } - - void reportMovedInLoop(mlir::Operation *move, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - move->emitError("'") << name - << "' is moved inside a loop but was made outside it; only a borrow could do " - "that, and -mm=own cannot prove one yet"; - signalPassFailure(); - } - - void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' borrows '" << owner << "' but is used here after '" << owner - << "' is released or overwritten"; - diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; - signalPassFailure(); - } + void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner); void reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, - llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' borrows " << place - << " but is used here after it may be released or overwritten"; - diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; - signalPassFailure(); - } - - void reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name << "' borrows " << place << " and cannot be assigned; -mm=own cannot prove that yet"; - signalPassFailure(); - } - - void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) - { - if (!reporting()) - { - return; - } + llvm::StringRef place); - auto diag = op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << owner << "' is moved here while '" << borrower - << "' borrows it; a borrowed value cannot be moved"; - signalPassFailure(); - } + void reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place); - void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } + void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place); - auto diag = op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; - noteLostFacts(diag); - signalPassFailure(); - } + void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower); - void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } + void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner); - op->emitError("'") << name << "' borrows '" << owner << "' and cannot be assigned; -mm=own cannot prove that yet"; - signalPassFailure(); - } + void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner); - void reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why) - { - if (!reporting()) - { - return; - } + void reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why); - auto diag = capture->emitError("a closure that escapes owns what it captures, but -mm=own cannot move this " - "variable into it: ") - << why; - diag.attachNote(escape->getLoc()) << "the closure escapes here"; - signalPassFailure(); - } - - void reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' is captured by a closure that escapes, and is still used here"; - diag.attachNote(capture->getLoc()) << "the closure takes it here"; - signalPassFailure(); - } + void reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name); void reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, - llvm::StringRef captured) - { - if (!reporting()) - { - return; - } + llvm::StringRef captured); - auto diag = use->emitError("'") << closure << "' borrows '" << captured << "', which it captures, but runs here after '" - << captured << "' is released"; - diag.attachNote(end->getLoc()) << "'" << captured << "' is released here"; - signalPassFailure(); - } + void reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured); - void reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << captured << "' is captured by a closure, but -mm=own cannot tell who owns its value here"; - signalPassFailure(); - } - - void reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name - << "' is a captured parameter and cannot be assigned under -mm=own: its value is the " - "caller's"; - signalPassFailure(); - } + void reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name); void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, - mlir::Operation *usedAt = nullptr) - { - if (!reporting()) - { - return; - } - - auto diag = move->emitError("'") << name - << "' is moved here on some paths only; -mm=own cannot release it " - "on the others yet"; - diag.attachNote(release->getLoc()) << "released here on every path"; - if (usedAt) - { - diag.attachNote(usedAt->getLoc()) - << "used here, after the paths join, so it cannot be released before they do"; - } - - signalPassFailure(); - } + mlir::Operation *usedAt = nullptr); }; } // namespace own_inference