Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,9 @@ MEMCHECK_CASES := \
tests/cases/test_enum_scope_release \
tests/cases/test_field_reassign_release tests/cases/test_discarded_result \
tests/cases/test_discarded_value_shapes \
tests/cases/test_discarded_branch_value \
tests/cases/test_result_local_match \
tests/cases/test_result_in_holder \
tests/cases/test_global_rebind_ownership \
tests/cases/test_generic_method_return tests/cases/test_generic_fnvalue_return \
tests/cases/test_generic_method_type_args tests/cases/test_generic_owner_generic_method \
Expand Down
43 changes: 42 additions & 1 deletion cranelift/codegen/src/count_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ fn is_emitted_release(callee: &str) -> bool {
callee.contains("__tuple_cascade_") || callee.contains("__result_arg_release_") || callee.contains("__kinded_release_m")
}

/// the helper the emitter writes into each module to hand back a result box
/// the caller owns alone (`ir_emit_result_box_unshare`)
fn is_emitted_unshare(callee: &str) -> bool {
callee.contains("__result_unshare_")
}

/// a kind a generic body left unsubstituted, such as `T`: it may be counted
/// or not, so the checker can't settle a count on it
fn kind_of_text(kind: &str) -> &str {
Expand Down Expand Up @@ -1192,6 +1198,17 @@ impl<'a, 'f> FuncCheck<'a, 'f> {
st.regs.insert(dst.to_string(), Val::Plain);
return Ok(());
}
// the emitted unshare takes the count it is handed on a result box
// and hands back a box the caller owns alone: the same one when that
// count was the only one, a fresh copy otherwise
if is_emitted_unshare(callee) {
if let Some(a) = args.first() {
self.take(st, a, callee)?;
}
let o = self.new_obj(st, 1);
st.regs.insert(dst.to_string(), Val::Obj(o));
return Ok(());
}
if RETAINS.contains(&callee) {
match self.val(st, args[0]) {
Val::Obj(o) => {
Expand Down Expand Up @@ -1461,7 +1478,7 @@ fn instruction_writes(
let callee = if ins[0] == "call" { ins.get(2) } else { ins.get(3) };
let Some(callee) = callee else { return true };
// an emitted release helper drops only its own argument
if is_emitted_release(callee) || callee.contains("__tuple_stack_cascade_") {
if is_emitted_release(callee) || is_emitted_unshare(callee) || callee.contains("__tuple_stack_cascade_") {
return false;
}
if user.contains(callee) {
Expand Down Expand Up @@ -1939,6 +1956,30 @@ mark kind c Counter
endfunc
";

// a borrowed result box returned whole goes through the unshare helper,
// which takes the count the return retained and hands back an owned box
const UNSHARE_RETURN: &str = "
func first 1 tuple
param rs
load 1 rs
iconst 2 0
call 3 pith_list_get_value tuple 2 1 2
call 4 pith_struct_retain void 1 3
call 5 __result_unshare_s0_m0 tuple 1 3
ret 5
mark kind rs list
endfunc
";

#[test]
fn the_unshare_helper_takes_its_argument_and_hands_back_a_count() {
let r = check(UNSHARE_RETURN);
assert_eq!((r.passed, r.failed, r.undecided), (1, 0, 0), "{:?}", r);
// without the retain the helper takes a count the function never had
let r = check(&UNSHARE_RETURN.replace("call 4 pith_struct_retain void 1 3\n", ""));
assert_eq!(r.failed, 1, "{:?}", r);
}

#[test]
fn a_struct_field_named_ok_is_not_a_result_box() {
let r = check(STRUCT_FIELD_NAMED_OK);
Expand Down
10 changes: 10 additions & 0 deletions docs/ir-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,16 @@ count of its own, left the caller's box and the returned payload sharing one
count, and whichever was released first freed what the other still read
(#1164).

**a returned box belongs to the caller alone.** every caller that opens a
call's box takes over the payload's count, which is correct only when
nothing else holds the box. so a function that returns the box itself (the
box abi, which generic functions and interface impls always use) and returns
a box it does not fully own (a parameter, a field, an element, or a local
whose count is shared) passes it through `__result_unshare_<ok><err>`. that
helper returns the same box when the caller's count is the only one.
otherwise it returns a fresh box holding its own count on the live payload,
and drops the count on the shared box.

**optional encoding.** a `T?` is the null pointer when it is none. a Some is
a two-slot struct allocation holding 1 at offset 0 and the payload at offset
8, and it may carry a destructor that releases the payload. a none costs no
Expand Down
5 changes: 0 additions & 5 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,6 @@ stale and a fix to it is welcome.
module's structs as E306. an experimental cycle collector runs behind
`PITH_CYCLE_GC=1`, off by default, with `std.concurrent.gc_collect()` to force
a pass.
- **known leaks**. each of these strands memory on every occurrence:
- a discarded `catch`, `select` or `match` expression releases only a string
result (#1091).
- a `T!` stored in a struct field or a list element (#1167).
- a match on a result local with a tuple payload (#1150).
- **closing a channel can race a send**. a send that passes its closed check
as another task closes the channel can enqueue a value nobody reads, and the
send reports success. a channel costs about 416 bytes plus 16 bytes per
Expand Down
7 changes: 7 additions & 0 deletions docs/ownership.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,13 @@ gaps, all bounded leaks rather than dangling pointers:
and what happens to it from there is what has always happened to a
kept result (see the entry below on results bound to a name). `tests/cases/test_result_arg_callee_spellings`
reads back every spelling under valgrind with the freelist off.
a result box stored in a holder (a struct field, or a list or map
element) gets a destructor at the store: the stack cascade helper for its
signature, which releases the live payload and zeroes its slot, and
leaves the box itself to the runtime. the holder's release then drops the
payload along with the box. a cascade or a result-argument release that
runs first also zeroes the slot, so the payload is released exactly once
(#1167).
tuple-typed registers the emitter cannot tie back to a literal still
strand their box.
- **a collection literal the checker cannot type builds an untagged
Expand Down
Loading
Loading