From 618582a40e368a56324e0759002b0cdde2f7cb2e Mon Sep 17 00:00:00 2001 From: Stephen Olesen Date: Tue, 11 Aug 2026 17:01:29 -0600 Subject: [PATCH 1/2] fix(tasks): reject cancelled value results I make task result publication explicit and preserve exact-once cleanup for unconsumed owned values. --- hew-codegen-rs/src/llvm.rs | 18 ++ hew-codegen-rs/src/runtime_abi.rs | 13 ++ hew-codegen-rs/src/suspend.rs | 189 ++++++++++++++--- hew-codegen-rs/src/thunks.rs | 80 +++++++- .../task_entry_cancel_composite_emission.rs | 58 ++++++ hew-runtime/src/reply_channel.rs | 41 ++++ hew-runtime/src/task_scope.rs | 193 ++++++++++++++++++ scripts/ffi-ownership-ratchet.toml | 2 +- .../forced_cancel_composite_probe.hew | 69 +++++-- scripts/forced-cancel-composite-check.sh | 58 +++--- scripts/jit-symbol-classification.toml | 6 + 11 files changed, 640 insertions(+), 87 deletions(-) diff --git a/hew-codegen-rs/src/llvm.rs b/hew-codegen-rs/src/llvm.rs index 50f66e4303..5b5c6dbb25 100644 --- a/hew-codegen-rs/src/llvm.rs +++ b/hew-codegen-rs/src/llvm.rs @@ -1682,6 +1682,10 @@ pub(crate) struct CoroState<'ctx> { /// were inlined). `None` for a coroutine that carries an explicit final /// Suspend (a generator) — its `Return` arm just `ret`s the handle. pub(crate) final_suspend_block: Option>, + /// The shared block that emits the coroutine's sole final suspend. Normal + /// completion reaches it after depositing a reply; cancellation reaches it + /// without a reply so the scheduler resolves the caller as an error. + pub(crate) final_suspend_emit_block: Option>, /// Whether this coroutine is a GENERATOR body (its MIR carries /// `Terminator::Yield`). A generator's value channel is the explicit /// out-pointer parameter the body publishes each yield into (NOT the @@ -33950,6 +33954,11 @@ fn lower_function<'ctx>( } else { Some(ctx.append_basic_block(llvm_fn, "coro.final.suspend")) }; + let final_suspend_emit_block = if has_explicit_final_suspend { + None + } else { + Some(ctx.append_basic_block(llvm_fn, "coro.final.suspend.emit")) + }; Some(CoroState { handle: cc.handle, id_token: cc.id_token, @@ -33958,6 +33967,7 @@ fn lower_function<'ctx>( logical_return_ty, has_explicit_final_suspend, final_suspend_block, + final_suspend_emit_block, is_generator, }) } else { @@ -35180,6 +35190,14 @@ fn lower_function<'ctx>( ) .llvm_ctx("coro hew_reply call")?; } + let final_suspend_emit_block = coro.final_suspend_emit_block.ok_or_else(|| { + CodegenError::Llvm("coroutine final suspend has no emit block".into()) + })?; + fn_ctx + .builder + .build_unconditional_branch(final_suspend_emit_block) + .llvm_ctx("coro final reply -> final suspend")?; + fn_ctx.builder.position_at_end(final_suspend_emit_block); cc.emit_suspend( coro.cleanup_block, coro.cleanup_block, diff --git a/hew-codegen-rs/src/runtime_abi.rs b/hew-codegen-rs/src/runtime_abi.rs index 56e8bd67fd..f7c88333b6 100644 --- a/hew-codegen-rs/src/runtime_abi.rs +++ b/hew-codegen-rs/src/runtime_abi.rs @@ -5448,6 +5448,19 @@ pub(crate) fn intern_runtime_decl<'ctx>( // if Done, null otherwise. Called after hew_task_await_blocking // when the producer needs to read the result separately. "hew_task_get_result" => ptr_ty.fn_type(&[ptr_ty.into()], false), + "hew_task_take_result" => ptr_ty.fn_type(&[ptr_ty.into()], false), + "hew_task_set_result_drop_fn" => ctx + .void_type() + .fn_type(&[ptr_ty.into(), ptr_ty.into()], false), + "hew_task_result_publication_checkpoint" => { + ctx.void_type().fn_type(&[ptr_ty.into()], false) + } + "hew_reply_channel_publish_cancelled" => { + ctx.void_type().fn_type(&[ptr_ty.into()], false) + } + "hew_reply_channel_publish_task_failed" => { + ctx.void_type().fn_type(&[ptr_ty.into()], false) + } "hew_task_completion_observe" => i32_ty.fn_type( &[ptr_ty.into(), ptr_ty.into(), ptr_ty.into(), ptr_ty.into()], false, diff --git a/hew-codegen-rs/src/suspend.rs b/hew-codegen-rs/src/suspend.rs index a4a533c734..2ac321554f 100644 --- a/hew-codegen-rs/src/suspend.rs +++ b/hew-codegen-rs/src/suspend.rs @@ -197,7 +197,7 @@ pub(crate) struct SuspendingCallClosureEmit<'a> { pub(crate) struct SuspendingTaskAwaitEmit { pub(crate) scope: Place, pub(crate) task: Place, - /// `Some(slot)` reads the task result via `hew_task_get_result` on the bind + /// `Some(slot)` takes the task result via `hew_task_take_result` on the bind /// edge and copies it into the slot at the `T` element width (value task); /// `None` for a unit task (nothing to bind). pub(crate) result_dest: Option, @@ -3193,12 +3193,15 @@ fn emit_suspending_channel_recv_bind<'ctx>( /// abandon: ; parked cont destroyed /// hew_task_detach_await(scope, task, slot); br shared cleanup /// bind: ; already-done OR resumed -/// (value task) result = hew_task_get_result(task); store -> result_dest +/// error = hew_task_get_error(task) +/// error == Cancelled -> publish cancellation; final suspend without a value +/// other error != 0 -> publish task failure; final suspend without a value +/// (value task) result = hew_task_take_result(task); store -> result_dest /// hew_read_slot_free(slot) ; release the creator ref /// br resume_bb /// ``` /// The task is BORROWED across the suspend (the scope-join owns its free). On -/// resume the task is `Done` and `hew_task_get_result` deep-reads its result — +/// resume the task is `Done` and `hew_task_take_result` moves its result — /// the value channel is the task's own result buffer, NOT the gen out-pointer. /// Slot refs mirror the channel-recv ramp: `new` (+1 creator); /// `hew_task_await_suspend` takes the observer's in-flight ref only on the park @@ -3232,7 +3235,7 @@ pub(crate) fn emit_suspending_task_await_terminator<'ctx>( // A value-returning task carries `result_dest` — the slot the child's `T` // is read into on the bind edge. The task body published its result through // `hew_task_set_result` (the codegen task wrapper); on the resume / - // immediate-ready edge the task is `Done` and `hew_task_get_result` returns + // immediate-ready edge the task is `Done` and `hew_task_take_result` returns // the result buffer, which the bind edge copies into `result_dest` at the // `T` element width. A unit task carries `None` and binds nothing. let actor_self_fn = intern_runtime_decl( @@ -3346,41 +3349,165 @@ pub(crate) fn emit_suspending_task_await_terminator<'ctx>( Ok(()) }, || { - // Bind: already-done OR resumed. The task is `Done`. For a value - // task, read the published result and copy it into `result_dest` at - // the `T` element width: `hew_task_get_result` returns the + // Bind: already-done OR resumed. The task is `Done`. Its terminal + // error is authoritative: cancellation takes the value-free final + // suspend, leaving any published owned result for task teardown. + let task_error = fn_ctx.call_runtime_int( + "hew_task_get_error", + &[task_ptr.into()], + "suspending_task_await_error", + "hew_task_get_error (bind) call", + )?; + let success_bb = fn_ctx + .ctx + .append_basic_block(parent, "suspending_task_await_success"); + let cancelled_bb = fn_ctx + .ctx + .append_basic_block(parent, "suspending_task_await_cancelled"); + let failed_bb = fn_ctx + .ctx + .append_basic_block(parent, "suspending_task_await_failed"); + let non_success_bb = fn_ctx + .ctx + .append_basic_block(parent, "suspending_task_await_non_success"); + let is_success = fn_ctx + .builder + .build_int_compare( + IntPredicate::EQ, + task_error, + task_error.get_type().const_zero(), + "suspending_task_await_is_success", + ) + .llvm_ctx("value-task success compare")?; + fn_ctx + .builder + .build_conditional_branch(is_success, success_bb, non_success_bb) + .llvm_ctx("value-task terminal-status branch")?; + + fn_ctx.builder.position_at_end(non_success_bb); + let is_cancelled = fn_ctx + .builder + .build_int_compare( + IntPredicate::EQ, + task_error, + task_error.get_type().const_int(1, false), + "suspending_task_await_is_cancelled", + ) + .llvm_ctx("value-task cancellation compare")?; + fn_ctx + .builder + .build_conditional_branch(is_cancelled, cancelled_bb, failed_bb) + .llvm_ctx("value-task failure-kind branch")?; + + fn_ctx.builder.position_at_end(cancelled_bb); + fn_ctx + .builder + .build_call( + slot_free, + &[slot.into()], + "suspending_task_await_cancel_free", + ) + .llvm_ctx("hew_read_slot_free (cancel bind) call")?; + crate::llvm::emit_elab_drops( + fn_ctx, + fn_ctx.suspend_abandon_block.get(), + fn_ctx.drop_plans, + )?; + let reply_channel = intern_runtime_decl( + fn_ctx.ctx, + fn_ctx.llvm_mod, + &mut fn_ctx.runtime_decls.borrow_mut(), + "hew_get_reply_channel", + )?; + let reply_cancel = intern_runtime_decl( + fn_ctx.ctx, + fn_ctx.llvm_mod, + &mut fn_ctx.runtime_decls.borrow_mut(), + "hew_reply_channel_publish_cancelled", + )?; + let ch = fn_ctx + .builder + .build_call(reply_channel, &[], "suspending_task_cancel_reply_channel") + .llvm_ctx("hew_get_reply_channel (task cancel) call")? + .try_as_basic_value() + .basic() + .ok_or_else(|| { + CodegenError::FailClosed("hew_get_reply_channel returned void".into()) + })? + .into_pointer_value(); + fn_ctx + .builder + .build_call(reply_cancel, &[ch.into()], "suspending_task_cancel_reply") + .llvm_ctx("hew_reply_channel_publish_cancelled (task cancel) call")?; + let final_suspend_emit = coro.final_suspend_emit_block.ok_or_else(|| { + CodegenError::FailClosed( + "task-await cancellation requires a shared final suspend".into(), + ) + })?; + fn_ctx + .builder + .build_unconditional_branch(final_suspend_emit) + .llvm_ctx("task cancellation -> final suspend")?; + + fn_ctx.builder.position_at_end(failed_bb); + fn_ctx + .builder + .build_call( + slot_free, + &[slot.into()], + "suspending_task_await_failure_free", + ) + .llvm_ctx("hew_read_slot_free (failure bind) call")?; + crate::llvm::emit_elab_drops( + fn_ctx, + fn_ctx.suspend_abandon_block.get(), + fn_ctx.drop_plans, + )?; + let ch = fn_ctx + .builder + .build_call(reply_channel, &[], "suspending_task_failure_reply_channel") + .llvm_ctx("hew_get_reply_channel (task failure) call")? + .try_as_basic_value() + .basic() + .ok_or_else(|| { + CodegenError::FailClosed("hew_get_reply_channel returned void".into()) + })? + .into_pointer_value(); + let reply_failed = intern_runtime_decl( + fn_ctx.ctx, + fn_ctx.llvm_mod, + &mut fn_ctx.runtime_decls.borrow_mut(), + "hew_reply_channel_publish_task_failed", + )?; + fn_ctx + .builder + .build_call(reply_failed, &[ch.into()], "suspending_task_fail_reply") + .llvm_ctx("hew_reply_channel_publish_task_failed call")?; + fn_ctx + .builder + .build_unconditional_branch(final_suspend_emit) + .llvm_ctx("task failure -> final suspend")?; + + fn_ctx.builder.position_at_end(success_bb); + // For a value task, take the published result and copy it into + // `result_dest` at the `T` element width. `hew_task_take_result` + // returns the // task-owned result buffer (the bytes `hew_task_set_result` deep- // copied from the body's return); the load+store MOVES the value // representation out of the buffer into the awaiter's binding slot. // The task frees the buffer (as raw bytes) at scope join, so a // managed handle lives exactly once — in `result_dest`. // - // Null-buffer guard: `hew_task_get_result` returns `t.result`, which - // is NULL whenever the task reached `Done` WITHOUT publishing a result - // — i.e. a `Done(Cancelled)` task whose body never ran to - // `hew_task_set_result` (the runtime documents this null contract on - // `hew_task_await_suspend`). On a null buffer the bind edge copies - // nothing rather than dereferencing null: the awaiter is being torn - // down by the same scope cancel, so the binding is never observed — - // exactly the unit-task discipline. Mirrors the suspending-ask bind - // edge, which null-checks `reply_ptr` before the load. - // - // WHY a guard for a path the current surface cannot reach: no v0.6 - // user construct cancels a sibling task while another value-awaits in - // the same scope (the empty-body `scope … after(d)` deadline arms but - // joins the forked task rather than preempting its await — verified), - // so this branch is presently unexercised. It honours the runtime's - // explicit null-return contract so the value path is correct the day a - // cancelling-await surface lands (await-cancel deadline on the task, a - // failing-sibling scope-cancel), rather than shipping a latent null - // deref behind a future feature. WHEN obsolete: never — the runtime - // contract permits a null result buffer regardless of surface. + // A successful value task must have a written, unconsumed result. + // A null take is therefore an invalid completion fact and routes to + // the same payload-free failure edge instead of exposing an + // uninitialized destination. if let Some(result_dest) = term.result_dest { let result_buf = fn_ctx.call_runtime_ptr( - "hew_task_get_result", + "hew_task_take_result", &[task_ptr.into()], "suspending_task_await_result", - "hew_task_get_result (bind) call", + "hew_task_take_result (bind) call", )?; let copy_bb = fn_ctx .ctx @@ -3392,10 +3519,10 @@ pub(crate) fn emit_suspending_task_await_terminator<'ctx>( .builder .build_is_null(result_buf, "suspending_task_await_result_is_null") .llvm_ctx("value-task result null compare")?; - // null (cancelled / no result) → skip copy; non-null → copy. + // null (invalid success) → fail closed; non-null → copy. fn_ctx .builder - .build_conditional_branch(result_is_null, bind_join_bb, copy_bb) + .build_conditional_branch(result_is_null, failed_bb, copy_bb) .llvm_ctx("value-task result null branch")?; fn_ctx.builder.position_at_end(copy_bb); diff --git a/hew-codegen-rs/src/thunks.rs b/hew-codegen-rs/src/thunks.rs index 23424317f2..5061f09c57 100644 --- a/hew-codegen-rs/src/thunks.rs +++ b/hew-codegen-rs/src/thunks.rs @@ -48,6 +48,7 @@ fn task_closure_wrapper_name(fn_symbol: &str) -> String { pub(crate) fn get_or_create_task_wrapper<'ctx>( fn_ctx: &FnCtx<'_, 'ctx>, callee_symbol: &str, + result_resolved_ty: &ResolvedTy, ) -> CodegenResult> { let wrapper_name = task_wrapper_name(callee_symbol); if let Some(existing) = fn_ctx.llvm_mod.get_function(&wrapper_name) { @@ -86,13 +87,29 @@ pub(crate) fn get_or_create_task_wrapper<'ctx>( let task_param = wrapper.get_nth_param(1).ok_or_else(|| { CodegenError::FailClosed("task wrapper missing HewTask* parameter".into()) })?; + if !callee_returns_unit { + let result_drop_fn = ask_reply_drop_thunk_ptr(fn_ctx, result_resolved_ty)?; + let set_drop_fn = intern_runtime_decl( + fn_ctx.ctx, + fn_ctx.llvm_mod, + &mut fn_ctx.runtime_decls.borrow_mut(), + "hew_task_set_result_drop_fn", + )?; + builder + .build_call( + set_drop_fn, + &[task_param.into(), result_drop_fn.into()], + "hew_task_set_result_drop_fn_call", + ) + .llvm_ctx("hew_task_set_result_drop_fn call")?; + } let body_call = builder .build_call(callee_value, &[ctx_param.into()], "task_body_call") .llvm_ctx("task wrapper body call")?; // Value-returning task: publish the body's `T` result through the task's // own result buffer BEFORE marking the task complete, so the awaiter's - // resume edge reads it via `hew_task_get_result`. The adapter + // resume edge moves it via `hew_task_take_result`. The adapter // (`__hew_task_entry_`, TaskEntry call-conv) returns its body's `T`; a // unit task returns the i8 unit stand-in and publishes nothing. // @@ -120,6 +137,40 @@ pub(crate) fn get_or_create_task_wrapper<'ctx>( let (size, _align) = abi_size_align(callee_return_ty, Some(fn_ctx.target_data))?; let size_ty = runtime_size_ty(fn_ctx.ctx, fn_ctx.llvm_mod); let size_val = size_ty.const_int(size, false); + // The adapter's cancellation edge must return a type-correct value to + // satisfy its LLVM ABI, but that value was not produced by the task + // body. Re-observe the monotonic cancellation token before publishing: + // only the non-cancelled edge establishes that the result slot was + // written. A cancellation racing after this check is ordered against + // `hew_task_complete_threaded`; the terminal task status still wins. + let cooperate = intern_runtime_decl( + fn_ctx.ctx, + fn_ctx.llvm_mod, + &mut fn_ctx.runtime_decls.borrow_mut(), + "hew_actor_cooperate", + )?; + let signal = builder + .build_call(cooperate, &[], "task_result_cooperate") + .llvm_ctx("task result cooperate call")? + .try_as_basic_value() + .basic() + .ok_or_else(|| CodegenError::FailClosed("hew_actor_cooperate returned void".into()))? + .into_int_value(); + let publish_bb = fn_ctx.ctx.append_basic_block(wrapper, "publish_result"); + let complete_bb = fn_ctx.ctx.append_basic_block(wrapper, "complete_task"); + let is_cancelled = builder + .build_int_compare( + IntPredicate::EQ, + signal, + signal.get_type().const_int(2, false), + "task_result_cancelled", + ) + .llvm_ctx("task result cancellation compare")?; + builder + .build_conditional_branch(is_cancelled, complete_bb, publish_bb) + .llvm_ctx("task result publication branch")?; + + builder.position_at_end(publish_bb); let set_result = intern_runtime_decl( fn_ctx.ctx, fn_ctx.llvm_mod, @@ -133,6 +184,23 @@ pub(crate) fn get_or_create_task_wrapper<'ctx>( "hew_task_set_result_call", ) .llvm_ctx("hew_task_set_result call")?; + let publication_checkpoint = intern_runtime_decl( + fn_ctx.ctx, + fn_ctx.llvm_mod, + &mut fn_ctx.runtime_decls.borrow_mut(), + "hew_task_result_publication_checkpoint", + )?; + builder + .build_call( + publication_checkpoint, + &[task_param.into()], + "hew_task_result_publication_checkpoint_call", + ) + .llvm_ctx("hew_task_result_publication_checkpoint call")?; + builder + .build_unconditional_branch(complete_bb) + .llvm_ctx("task result publish -> complete")?; + builder.position_at_end(complete_bb); } let complete = intern_runtime_decl( @@ -175,7 +243,15 @@ pub(crate) fn emit_spawn_task_direct( // by-value copy). A non-coroutine handler keeps the cheaper spilled param. let parent_ctx = live_execution_context_ptr(fn_ctx, spilled_ctx)?; let task_ptr = load_duplex_handle(fn_ctx, task, "SpawnTaskDirect task")?; - let wrapper = get_or_create_task_wrapper(fn_ctx, callee_symbol)?; + let task_result_ty = match place_resolved_ty(fn_ctx, task)? { + ResolvedTy::Task(result) => result.as_ref().clone(), + other => { + return Err(CodegenError::FailClosed(format!( + "SpawnTaskDirect task place has non-task type {other:?}" + ))); + } + }; + let wrapper = get_or_create_task_wrapper(fn_ctx, callee_symbol, &task_result_ty)?; let spawn = intern_runtime_decl( fn_ctx.ctx, fn_ctx.llvm_mod, diff --git a/hew-codegen-rs/tests/emission/task_entry_cancel_composite_emission.rs b/hew-codegen-rs/tests/emission/task_entry_cancel_composite_emission.rs index 0467583b8f..7577e9bfea 100644 --- a/hew-codegen-rs/tests/emission/task_entry_cancel_composite_emission.rs +++ b/hew-codegen-rs/tests/emission/task_entry_cancel_composite_emission.rs @@ -178,3 +178,61 @@ fn task_entry_composite_cancel_exit_never_loads_return_slot() { --- cancel_exit block ---\n{cancel_exit_block}" ); } + +#[test] +fn task_wrapper_publishes_only_after_non_cancelled_completion() { + let ll = emit_ll_text( + &pipeline_from_source(COMPOSITE_FORK_SOURCE), + "composite_fork_result_authority", + ); + + let wrapper_define = ll + .lines() + .find(|line| { + line.trim_start().starts_with("define") + && line.contains("__hew_task_wrapper___hew_task_entry_") + && line.contains("compute") + }) + .unwrap_or_else(|| panic!("no compute task wrapper definition; IR:\n{ll}")); + let wrapper_start = ll.find(wrapper_define).expect("wrapper define offset"); + let wrapper_end = ll[wrapper_start..] + .find("\n}\n") + .map_or(ll.len(), |offset| wrapper_start + offset); + let wrapper = &ll[wrapper_start..wrapper_end]; + + let cooperate = wrapper + .find("@hew_actor_cooperate") + .expect("task wrapper must re-observe cancellation before publication"); + let cancellation_branch = wrapper + .find("task_result_cancelled") + .expect("task wrapper must branch on the cancellation signal"); + let publish = wrapper + .find("@hew_task_set_result(") + .expect("task wrapper must publish successful values"); + assert!( + cooperate < cancellation_branch && cancellation_branch < publish, + "the cancellation observation and branch must dominate result publication;\n--- wrapper ---\n{wrapper}" + ); + assert!( + wrapper.contains("publish_result:") && wrapper.contains("complete_task:"), + "publication and terminal completion need distinct control-flow facts;\n--- wrapper ---\n{wrapper}" + ); + + let await_error = ll + .find("@hew_task_get_error(") + .expect("task await must read terminal status"); + let await_take = ll + .find("@hew_task_take_result(") + .expect("successful task await must consume the published result"); + assert!( + await_error < await_take, + "task await must reject cancellation before consuming payload bytes" + ); + assert!( + ll.contains("suspending_task_await_cancelled:") + && ll.contains("@hew_reply_channel_publish_cancelled(") + && ll.contains("suspending_task_await_failed:") + && ll.contains("@hew_reply_channel_publish_task_failed("), + "task await must keep cancellation and non-cancellation failure distinct" + ); +} diff --git a/hew-runtime/src/reply_channel.rs b/hew-runtime/src/reply_channel.rs index 5619c34b7d..c6cb554d14 100644 --- a/hew-runtime/src/reply_channel.rs +++ b/hew-runtime/src/reply_channel.rs @@ -937,6 +937,47 @@ pub unsafe extern "C" fn hew_reply_channel_cancel(ch: *mut HewReplyChannel) { } } +/// Publish cancellation as a terminal, payload-free reply. +/// +/// This is the sender-side completion path for a handler that cannot produce +/// its promised value because an awaited child task was cancelled. It marks +/// the channel cancelled before publishing the null sentinel, so the waiter +/// observes a status-bearing cancellation error rather than a value. +/// +/// # Safety +/// +/// `ch` must be null or the live sender-side reference for the current reply. +#[no_mangle] +pub unsafe extern "C" fn hew_reply_channel_publish_cancelled(ch: *mut HewReplyChannel) { + if ch.is_null() { + return; + } + // SAFETY: caller guarantees the live sender reference and single writer. + unsafe { + crate::scheduler::mark_current_reply_channel_consumed(ch.cast()); + (*ch).cancelled.store(true, Ordering::Release); + publish_reply_from_sender_ref(ch, ptr::null_mut(), 0); + } +} + +/// Publish a non-cancellation task failure as a terminal, payload-free reply. +/// +/// # Safety +/// +/// `ch` must be null or the live sender-side reference for the current reply. +#[no_mangle] +pub unsafe extern "C" fn hew_reply_channel_publish_task_failed(ch: *mut HewReplyChannel) { + if ch.is_null() { + return; + } + // SAFETY: caller guarantees the live sender reference and single writer. + unsafe { + crate::scheduler::mark_current_reply_channel_consumed(ch.cast()); + hew_reply_channel_mark_failed(ch, crate::internal::types::HEW_REPLY_FAIL_HANDLER_TRAPPED); + publish_reply_from_sender_ref(ch, ptr::null_mut(), 0); + } +} + /// Return the attached suspended-await status for a reply channel. /// /// When no common registration is attached, this reports the legacy channel diff --git a/hew-runtime/src/task_scope.rs b/hew-runtime/src/task_scope.rs index 1ae87ce630..eac8ac31ed 100644 --- a/hew-runtime/src/task_scope.rs +++ b/hew-runtime/src/task_scope.rs @@ -308,6 +308,12 @@ pub struct HewTask { pub result: *mut c_void, /// Size of `result` in bytes. pub result_size: usize, + /// True only after the worker explicitly publishes a result. + result_written: bool, + /// True after an awaiter moves the published representation out. + result_consumed: bool, + /// Typed destructor for an unconsumed published result. + result_drop_fn: Option, /// Parent scope (structured lifetime). pub scope: *mut HewTaskScope, /// Cancellation token owned by this task. @@ -634,6 +640,9 @@ pub unsafe extern "C" fn hew_task_new() -> *mut HewTask { error: HewTaskError::None, result: ptr::null_mut(), result_size: 0, + result_written: false, + result_consumed: false, + result_drop_fn: None, scope: ptr::null_mut(), cancel_token: ptr::null_mut(), next: ptr::null_mut(), @@ -658,6 +667,13 @@ pub unsafe extern "C" fn hew_task_free(task: *mut HewTask) { // SAFETY: Caller guarantees `task` was Box-allocated. let t = unsafe { Box::from_raw(task) }; if !t.result.is_null() { + if t.result_written && !t.result_consumed { + if let Some(drop_fn) = t.result_drop_fn { + // SAFETY: the callback was registered for the result's exact + // representation and the buffer has not been consumed. + unsafe { drop_fn(t.result) }; + } + } // SAFETY: result was malloc'd by hew_task_set_result. unsafe { libc::free(t.result) }; } @@ -746,6 +762,27 @@ pub unsafe extern "C" fn hew_task_get_result(task: *mut HewTask) -> *mut c_void t.result } +/// Move a completed task's result representation to its awaiter. +/// +/// The returned buffer remains task-owned and is freed with the task. Calling +/// this function only transfers ownership of values embedded in its bytes, so +/// task teardown does not run the registered typed destructor afterward. +/// +/// # Safety +/// +/// `task` must be a valid pointer returned by [`hew_task_new`]. +#[no_mangle] +pub unsafe extern "C" fn hew_task_take_result(task: *mut HewTask) -> *mut c_void { + cabi_guard!(task.is_null(), ptr::null_mut()); + // SAFETY: caller guarantees `task` is valid. + let t = unsafe { &mut *task }; + if t.load_state() != HewTaskState::Done || !t.result_written || t.result_consumed { + return ptr::null_mut(); + } + t.result_consumed = true; + t.result +} + /// Register a one-shot callback fired when `task` reaches `Done`. /// /// The callback is invoked outside the task completion lock. If the task is @@ -1267,9 +1304,30 @@ pub unsafe extern "C" fn hew_task_set_result(task: *mut HewTask, result: *mut c_ unsafe { ptr::copy_nonoverlapping(result.cast::(), buf.cast::(), size) }; t.result = buf; t.result_size = size; + t.result_written = true; } } +/// Register the typed destructor for a published task result. +/// +/// The destructor runs at task teardown only when the result was written but +/// never consumed by an awaiter. Passing null clears the destructor for a +/// bit-copy result. +/// +/// # Safety +/// +/// - `task` must be a valid pointer returned by [`hew_task_new`]. +/// - `drop_fn`, when present, must accept the exact result representation. +#[no_mangle] +pub unsafe extern "C" fn hew_task_set_result_drop_fn( + task: *mut HewTask, + drop_fn: Option, +) { + cabi_guard!(task.is_null()); + // SAFETY: caller guarantees `task` is valid. + unsafe { (*task).result_drop_fn = drop_fn }; +} + /// Get the task's error code. /// /// # Safety @@ -1862,6 +1920,7 @@ mod forced_cancel_test_hook { /// spawn consumes only the entry whose scope address matches its own /// task's owning scope; an unrelated scope's spawn leaves it untouched. static ARMED: Mutex> = Mutex::new(Vec::new()); + static RESULT_ARMED: Mutex> = Mutex::new(Vec::new()); /// Paused spawns waiting for release: `(scope address, generation, /// open)`. `release_paused_spawn(scope)` opens only entries whose scope @@ -1884,6 +1943,17 @@ mod forced_cancel_test_hook { ARMED.lock_or_recover().push((scope as usize, generation)); } + /// Arm a pause after the next task in the current scope publishes its + /// result but before it marks itself terminal. + #[no_mangle] + pub extern "C" fn hew_test_pause_next_task_result_until_scope_cancel() { + let scope = super::current_task_scope(); + let generation = NEXT_GENERATION.fetch_add(1, Ordering::SeqCst); + RESULT_ARMED + .lock_or_recover() + .push((scope as usize, generation)); + } + /// Consume the arm registered for `child_scope`, if any, and publish its /// paused-spawn gate to `GATES` before returning — this always runs on /// the spawning (parent) thread, strictly before the new worker thread @@ -1899,6 +1969,16 @@ mod forced_cancel_test_hook { Some(generation) } + pub(super) fn consume_result_pause(child_scope: *mut HewTaskScope) -> Option { + let key = child_scope as usize; + let mut armed = RESULT_ARMED.lock_or_recover(); + let index = armed.iter().position(|&(scope, _)| scope == key)?; + let (_, generation) = armed.swap_remove(index); + drop(armed); + GATES.lock_or_recover().push((key, generation, false)); + Some(generation) + } + /// Block the calling (newly spawned task) thread until /// `release_paused_spawn` opens this exact `(scope, generation)` gate. /// 10s bound: a trigger that never fires is a test-authoring bug, not a @@ -1949,9 +2029,32 @@ mod forced_cancel_test_hook { } } +#[cfg(any(test, feature = "forced-cancel-test"))] +pub use forced_cancel_test_hook::hew_test_pause_next_task_result_until_scope_cancel; #[cfg(any(test, feature = "forced-cancel-test"))] pub use forced_cancel_test_hook::hew_test_pause_next_task_spawn_until_scope_cancel; +/// Test checkpoint immediately after a task result is published. +/// +/// Production builds are a no-op. The forced-cancellation proving build can +/// pause the selected task here until its owning scope is cancelled. +/// +/// # Safety +/// +/// `task` must be null or a valid pointer returned by [`hew_task_new`]. +#[no_mangle] +pub unsafe extern "C" fn hew_task_result_publication_checkpoint(task: *mut HewTask) { + cabi_guard!(task.is_null()); + #[cfg(any(test, feature = "forced-cancel-test"))] + { + // SAFETY: caller guarantees `task` is valid. + let scope = unsafe { (*task).scope }; + if let Some(generation) = forced_cancel_test_hook::consume_result_pause(scope) { + forced_cancel_test_hook::wait_for_release(scope as usize, generation); + } + } +} + /// Cancel all non-terminal tasks in the scope. /// /// Cancelled tasks transition to `Done` with error `Cancelled`. @@ -4163,6 +4266,96 @@ mod tests { } } + #[test] + fn zero_result_is_written_and_consumed_as_success() { + // SAFETY: the test owns the task and its copied result buffer. + unsafe { + let task = hew_task_new(); + (*task).store_state(HewTaskState::Running, Ordering::Relaxed); + let zero = 0_i64; + hew_task_set_result(task, (&raw const zero).cast_mut().cast(), size_of::()); + hew_task_complete_threaded(task); + + assert_eq!((*task).error, HewTaskError::None); + assert!((*task).result_written); + let result = hew_task_take_result(task); + assert!(!result.is_null()); + assert_eq!(*result.cast::(), 0); + assert!(hew_task_take_result(task).is_null()); + hew_task_free(task); + } + } + + #[test] + fn cancelled_published_owned_result_drops_exactly_once() { + static DROPS: AtomicUsize = AtomicUsize::new(0); + + unsafe extern "C" fn drop_boxed_word(slot: *mut c_void) { + // SAFETY: `slot` points to the copied pointer representation and + // this callback is invoked only for its one unconsumed owner. + let owned = unsafe { *slot.cast::<*mut usize>() }; + if !owned.is_null() { + // SAFETY: the test minted this pointer with Box::into_raw. + drop(unsafe { Box::from_raw(owned) }); + DROPS.fetch_add(1, Ordering::SeqCst); + } + } + + DROPS.store(0, Ordering::SeqCst); + // SAFETY: the test owns the task, token, and boxed result. + unsafe { + let task = hew_task_new(); + (*task).store_state(HewTaskState::Running, Ordering::Relaxed); + hew_task_set_result_drop_fn(task, Some(drop_boxed_word)); + let owned = Box::into_raw(Box::new(41_usize)); + hew_task_set_result( + task, + (&raw const owned).cast_mut().cast(), + size_of::<*mut usize>(), + ); + let token = hew_cancel_token_new_child(ptr::null_mut()); + hew_task_set_cancel_token(task, token); + hew_cancel_token_cancel(token, HewTaskError::Cancelled as i32); + hew_task_complete_threaded(task); + + assert_eq!((*task).error, HewTaskError::Cancelled); + assert!((*task).result_written); + assert_eq!(DROPS.load(Ordering::SeqCst), 0); + hew_task_free(task); + assert_eq!(DROPS.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn consumed_owned_result_is_not_dropped_by_task_teardown() { + static TASK_DROPS: AtomicUsize = AtomicUsize::new(0); + + unsafe extern "C" fn count_task_drop(_slot: *mut c_void) { + TASK_DROPS.fetch_add(1, Ordering::SeqCst); + } + + TASK_DROPS.store(0, Ordering::SeqCst); + // SAFETY: the test owns the task and manually consumes the moved box. + unsafe { + let task = hew_task_new(); + (*task).store_state(HewTaskState::Running, Ordering::Relaxed); + hew_task_set_result_drop_fn(task, Some(count_task_drop)); + let owned = Box::into_raw(Box::new(73_usize)); + hew_task_set_result( + task, + (&raw const owned).cast_mut().cast(), + size_of::<*mut usize>(), + ); + hew_task_complete_threaded(task); + let result = hew_task_take_result(task); + let moved = *result.cast::<*mut usize>(); + hew_task_free(task); + assert_eq!(TASK_DROPS.load(Ordering::SeqCst), 0); + assert_eq!(*moved, 73); + drop(Box::from_raw(moved)); + } + } + /// Stress test: spawn many tasks concurrently and verify every one /// reaches `Done` with the correct result visible to the joining thread. #[test] diff --git a/scripts/ffi-ownership-ratchet.toml b/scripts/ffi-ownership-ratchet.toml index 2f03b2cd92..53c43cd732 100644 --- a/scripts/ffi-ownership-ratchet.toml +++ b/scripts/ffi-ownership-ratchet.toml @@ -2,4 +2,4 @@ # # This value must exactly match the verifier's computed count. Any classification # or contract change that changes the count must deliberately update this ratchet. -unclassified = 825 +unclassified = 831 diff --git a/scripts/fixtures/forced-cancel-gate/forced_cancel_composite_probe.hew b/scripts/fixtures/forced-cancel-gate/forced_cancel_composite_probe.hew index a6ee6be82d..48848c0ff5 100644 --- a/scripts/fixtures/forced-cancel-gate/forced_cancel_composite_probe.hew +++ b/scripts/fixtures/forced-cancel-gate/forced_cancel_composite_probe.hew @@ -8,10 +8,9 @@ // // Forces a specific task's FIRST FunctionEntry cooperate check to observe // cancellation deterministically (via the feature-gated runtime hook, armed -// through the extern block below), then drives the value through the -// awaiter's bind edge (suspend.rs's "Null-buffer guard" path) to observe -// whatever `emit_cancel_trap_or_return`'s composite arm published into -// `out`. Post-fix, `out` must be the well-defined zero `Point{x:0,y:0}`. +// through the extern block below), then proves the awaiter reports cancellation +// without consuming the adapter's ABI-only zero return. A separate successful +// task returns `Point{x:0,y:0}` to prove payload bytes are not the discriminator. // // Uses the LEGACY `hew_task_scope_cancel_after_ns` live trigger (empty // `after(d) {}` body) rather than the modern `SuspendingScopeDeadline` @@ -39,36 +38,52 @@ fn compute() -> Point { return Point { x: 11, y: 22 }; } -// Keep the supported heap-string owner on a separate unit task: the current -// fork surface supports argument-bearing unit tasks only for BitCopy + string, -// while the Point task below remains the no-argument value-task shape that -// exercises the TaskEntry composite cancel-return path. Both tasks are paused -// at their own entry cooperate check and cancelled by one scope deadline. -fn consume_cancelled(label: string) { - if label.len() == -1 { - panic("cancelled task unexpectedly entered its body"); - } +fn compute_zero() -> Point { + return Point { x: 0, y: 0 }; +} + +fn compute_owned() -> string { + return "cancelled-owned-result".to_upper(); } extern "C" { fn hew_test_pause_next_task_spawn_until_scope_cancel(); + fn hew_test_pause_next_task_result_until_scope_cancel(); } actor Driver { - receive fn run_value() -> Point { + receive fn run_cancelled() -> Point { var out: Point = Point { x: 0, y: 0 }; scope { unsafe { hew_test_pause_next_task_spawn_until_scope_cancel() }; fork t = compute(); + after(1ms) {}; + let v = await t; + out = v; + }; + return out; + } + + receive fn run_cancelled_owned() -> string { + var out: string = "unset".to_upper(); + scope { unsafe { - hew_test_pause_next_task_spawn_until_scope_cancel() + hew_test_pause_next_task_result_until_scope_cancel() }; - let label = "forced-cancel-owned-string".to_upper(); - fork cleanup = consume_cancelled(label); + fork owned = compute_owned(); after(1ms) {}; - await cleanup; + let owned_value = await owned; + out = owned_value; + }; + return out; + } + + receive fn run_zero() -> Point { + var out: Point = Point { x: 9, y: 9 }; + scope { + fork t = compute_zero(); let v = await t; out = v; }; @@ -78,9 +93,19 @@ actor Driver { fn main() { let d = spawn Driver; - let r = await d.run_value(); - match r { - Ok(p) => println(f"x={p.x} y={p.y}"), - Err(_) => println("failed"), + let zero = await d.run_zero(); + match zero { + Ok(p) => println(f"zero x={p.x} y={p.y}"), + Err(_) => println("zero failed"), + } + let cancelled = await d.run_cancelled(); + match cancelled { + Ok(p) => println(f"fabricated x={p.x} y={p.y}"), + Err(_) => println("cancelled"), + } + let owned_cancelled = await d.run_cancelled_owned(); + match owned_cancelled { + Ok(value) => println(f"fabricated owned={value}"), + Err(_) => println("owned cancelled"), } } diff --git a/scripts/forced-cancel-composite-check.sh b/scripts/forced-cancel-composite-check.sh index a9ce31ba91..a5435f7564 100755 --- a/scripts/forced-cancel-composite-check.sh +++ b/scripts/forced-cancel-composite-check.sh @@ -3,20 +3,18 @@ # # What this gate proves # ────────────────────── -# `emit_cancel_trap_or_return`'s composite-return arm (hew-codegen-rs/src/ -# llvm.rs, TaskEntry adapter) must synthesize a well-defined zero value on -# the cancel-exit edge, never load the unstored `return_slot` alloca. A Rust -# unit test over the codegen helper in isolation cannot prove this — the -# defect is only observable through the compiled ABI boundary a real -# `.hew` program's runtime reads. A plain `cargo nextest` run cannot exercise -# it either: the trigger requires a task's OWN entry-block cooperate check to -# observe cancellation before it stores anything, which a normal build cannot -# force deterministically (see `hew-runtime`'s `forced-cancel-test` feature). +# A TaskEntry adapter must not turn its ABI-only cancellation return into a +# published task result. A Rust unit test over the codegen helper in isolation +# cannot prove this — the defect is only observable through the compiled ABI +# boundary a real `.hew` program's runtime reads. A plain `cargo nextest` run +# cannot exercise it either: the trigger requires a task's OWN entry-block +# cooperate check to observe cancellation before it stores anything, which a +# normal build cannot force deterministically. # # This script builds `hew` + `libhew.a` with `--features # hew-runtime/forced-cancel-test`, compiles+links the probe fixture against -# that build, and asserts the fixture observes the FIXED (well-defined zero) -# value — not the pre-fix uninitialized-stack garbage. +# that build, and asserts the fixture observes cancellation while a separate +# genuine all-zero return remains a successful value. # # Approach # ──────── @@ -26,9 +24,9 @@ # 2. Compile the probe fixture to a relocatable object (`hew build # --emit-obj`), then link with clang against the feature-enabled # `libhew.a` (mirrors `asan-fixture-check.sh`'s manual-link pattern). -# 3. Run the binary; assert the cancelled value task publishes `x=0 y=0` -# (the fixed zero-initialized composite) and the separately paused -# heap-string task has a real cancellation release in the emitted IR. +# 3. Run the binary; assert the cancelled value task reports cancellation, the +# genuine zero task reports `x=0 y=0`, and a post-publication cancellation +# releases an owned string result exactly once. # # WHEN OBSOLETE: if a future construct needs a general deterministic # actor-cancellation test harness, this narrow gate is superseded by that — @@ -86,25 +84,24 @@ if [[ ! -f "${PROBE_LL}" ]]; then exit 1 fi -# The unit task's entry cooperate check sees cancellation BEFORE it loads the -# string out of its fork environment. The actual owner is therefore the -# task-attached environment Rc destructor, not a normal lexical Drop in the -# skipped shim body. Prove both halves of that cancellation cleanup contract: -# the task installs the exact destructor at `hew_rc_new`, and that destructor -# releases the one string field. +# The post-publication cancellation case leaves an owned string in the task +# result buffer. The wrapper must register its exact in-place destructor, and +# that destructor must release the embedded string. Runtime task teardown calls +# it only when the awaiter did not consume the buffer; the leak oracle below +# proves the dynamic leg. if ! grep -Eq \ - 'hew_rc_new\(.*ptr @__hew_spawn_env_rc_drop___hew_fork_entry_.*\)' \ + 'call void @hew_task_set_result_drop_fn\(ptr %[^,]+, ptr @__hew_reply_drop_string\)' \ "${PROBE_LL}"; then - echo "FAIL forced-cancel-composite-check: fork environment is not wired to its string cleanup destructor" >&2 + echo "FAIL forced-cancel-composite-check: owned task result has no typed destructor" >&2 exit 1 fi if ! awk ' - /define private void @__hew_spawn_env_rc_drop___hew_fork_entry_/ { in_drop = 1 } + /define internal void @__hew_reply_drop_string/ { in_drop = 1 } in_drop && /call void @hew_string_drop\(/ { released = 1 } in_drop && /^}/ { in_drop = 0 } END { exit released ? 0 : 1 } ' "${PROBE_LL}"; then - echo "FAIL forced-cancel-composite-check: cancelled fork environment destructor does not release its heap string" >&2 + echo "FAIL forced-cancel-composite-check: owned task-result destructor does not release its string" >&2 exit 1 fi @@ -129,9 +126,9 @@ echo "=== forced-cancel-composite-check: running gate ===" actual_exit=0 actual_stdout="$("${PROBE_BIN}")" || actual_exit=$? -# The cancelled TaskEntry adapter must publish a zeroed Point before its body -# starts. The independently paused string task releases silently. -expected_stdout='x=0 y=0' +# Cancellation must surface without a payload; an independently completed zero +# result must still be delivered. The owned result releases silently. +expected_stdout=$'zero x=0 y=0\ncancelled\nowned cancelled' if [[ "${actual_exit}" -ne 0 ]]; then echo "FAIL forced-cancel-composite-check: expected exit 0, got ${actual_exit}" >&2 @@ -140,8 +137,7 @@ fi if [[ "${actual_stdout}" != "${expected_stdout}" ]]; then echo "FAIL forced-cancel-composite-check: expected stdout '${expected_stdout}', got '${actual_stdout}'" >&2 - echo " (a pre-fix build observes non-deterministic uninitialized-stack" >&2 - echo " garbage here instead of the well-defined zero composite)" >&2 + echo " (a pre-fix build reports the cancelled task as a successful zero value)" >&2 exit 1 fi @@ -152,10 +148,10 @@ if [[ "$(uname -s)" == "Darwin" ]] && command -v leaks >/dev/null 2>&1; then exit 1 } if ! grep -Eq '0 leaks for 0 total leaked bytes\.' <<<"${leaks_output}"; then - echo "FAIL forced-cancel-composite-check: forced cancellation leaked the fork-owned string" >&2 + echo "FAIL forced-cancel-composite-check: forced cancellation leaked the owned result" >&2 echo "${leaks_output}" >&2 exit 1 fi fi -echo "PASS forced-cancel-composite-check: fork-env string cleanup wired; zero composite observed; exit ${actual_exit}" +echo "PASS forced-cancel-composite-check: cancellation surfaced; genuine zero preserved; owned result cleanup wired; exit ${actual_exit}" diff --git a/scripts/jit-symbol-classification.toml b/scripts/jit-symbol-classification.toml index 3f6b258546..4456613019 100644 --- a/scripts/jit-symbol-classification.toml +++ b/scripts/jit-symbol-classification.toml @@ -550,6 +550,8 @@ stable = [ "hew_registry_unregister", "hew_reply", "hew_reply_channel_cancel", + "hew_reply_channel_publish_cancelled", + "hew_reply_channel_publish_task_failed", "hew_reply_channel_failure_kind", "hew_reply_channel_free", "hew_reply_channel_is_orphaned", @@ -708,8 +710,10 @@ stable = [ "hew_task_get_env", "hew_task_get_error", "hew_task_get_result", + "hew_task_take_result", "hew_task_is_cancelled", "hew_task_new", + "hew_task_result_publication_checkpoint", "hew_task_scope_cancel", "hew_task_scope_cancel_after_ns", "hew_task_scope_cancel_token", @@ -730,6 +734,7 @@ stable = [ "hew_task_set_cancel_cleanup_fn", "hew_task_set_cancel_token", "hew_task_set_env", + "hew_task_set_result_drop_fn", "hew_task_set_result", "hew_task_spawn_thread", "hew_task_spawn_thread_with_inherited_context", @@ -1708,6 +1713,7 @@ internal = [ # Cargo feature — absent from a default build. Not user-callable; not # JIT-reachable. "hew_test_pause_next_task_spawn_until_scope_cancel", + "hew_test_pause_next_task_result_until_scope_cancel", "hew_trace_clear", "hew_trace_drain", "hew_trace_reset", From 70361c6b4b46ed38ae23910b4e9d4a77c6c01985 Mon Sep 17 00:00:00 2001 From: Stephen Olesen Date: Tue, 11 Aug 2026 23:32:02 -0600 Subject: [PATCH 2/2] test(codegen): refresh final suspend IR goldens I record the intentional shared final-suspend block in the native and WASM byte-identity corpus. --- .../corpus/golden/native/mir_suspend_carrier_drop.ll | 7 +++++-- .../golden/native/owned_vec_cross_function_release.ll | 7 +++++-- .../corpus/golden/wasm32/mir_suspend_carrier_drop.ll | 7 +++++-- .../golden/wasm32/owned_vec_cross_function_release.ll | 7 +++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/ll-oracle/corpus/golden/native/mir_suspend_carrier_drop.ll b/tests/ll-oracle/corpus/golden/native/mir_suspend_carrier_drop.ll index ea61674be9..58d3eea4cc 100644 --- a/tests/ll-oracle/corpus/golden/native/mir_suspend_carrier_drop.ll +++ b/tests/ll-oracle/corpus/golden/native/mir_suspend_carrier_drop.ll @@ -534,17 +534,20 @@ coro.begin: ; preds = %dyn.alloc, %entry %coro.handle = call ptr @llvm.coro.begin(token %coro.id, ptr %coro.mem) br label %alloca.prologue -coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend, %bb2, %frame_cleanup_registered +coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend.emit, %bb2, %frame_cleanup_registered call void @llvm.coro.end(ptr %coro.handle, i1 false, token none) call void @hew_cont_frame_handoff(ptr %coro.handle) ret ptr %coro.handle -coro.cleanup: ; preds = %coro.final.suspend, %coro.final.suspend, %helper_crash_cleanup_retire_merge8, %helper_crash_cleanup_retire_merge +coro.cleanup: ; preds = %coro.final.suspend.emit, %coro.final.suspend.emit, %helper_crash_cleanup_retire_merge8, %helper_crash_cleanup_retire_merge %coro.freemem = call ptr @llvm.coro.free(token %coro.id, ptr %coro.handle) %coro.freemem.isnull = icmp eq ptr %coro.freemem, null br i1 %coro.freemem.isnull, label %coro.suspend.return, label %coro.dyn.free coro.final.suspend: ; preds = %helper_crash_cleanup_return_merge_4 + br label %coro.final.suspend.emit + +coro.final.suspend.emit: ; preds = %coro.final.suspend %coro.final.save = call token @llvm.coro.save(ptr %coro.handle) %coro.final.s = call i8 @llvm.coro.suspend(token %coro.final.save, i1 true) switch i8 %coro.final.s, label %coro.suspend.return [ diff --git a/tests/ll-oracle/corpus/golden/native/owned_vec_cross_function_release.ll b/tests/ll-oracle/corpus/golden/native/owned_vec_cross_function_release.ll index db41bd4270..0b62ee7c80 100644 --- a/tests/ll-oracle/corpus/golden/native/owned_vec_cross_function_release.ll +++ b/tests/ll-oracle/corpus/golden/native/owned_vec_cross_function_release.ll @@ -453,17 +453,20 @@ coro.begin: ; preds = %dyn.alloc, %entry %coro.handle = call ptr @llvm.coro.begin(token %coro.id, ptr %coro.mem) br label %alloca.prologue -coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend, %bb7 +coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend.emit, %bb7 call void @llvm.coro.end(ptr %coro.handle, i1 false, token none) call void @hew_cont_frame_handoff(ptr %coro.handle) ret ptr %coro.handle -coro.cleanup: ; preds = %coro.final.suspend, %coro.final.suspend, %gen_yield_abandon +coro.cleanup: ; preds = %coro.final.suspend.emit, %coro.final.suspend.emit, %gen_yield_abandon %coro.freemem = call ptr @llvm.coro.free(token %coro.id, ptr %coro.handle) %coro.freemem.isnull = icmp eq ptr %coro.freemem, null br i1 %coro.freemem.isnull, label %coro.suspend.return, label %coro.dyn.free coro.final.suspend: ; preds = %bb4 + br label %coro.final.suspend.emit + +coro.final.suspend.emit: ; preds = %coro.final.suspend %coro.final.save = call token @llvm.coro.save(ptr %coro.handle) %coro.final.s = call i8 @llvm.coro.suspend(token %coro.final.save, i1 true) switch i8 %coro.final.s, label %coro.suspend.return [ diff --git a/tests/ll-oracle/corpus/golden/wasm32/mir_suspend_carrier_drop.ll b/tests/ll-oracle/corpus/golden/wasm32/mir_suspend_carrier_drop.ll index 152b1e3031..7b8e42fc02 100644 --- a/tests/ll-oracle/corpus/golden/wasm32/mir_suspend_carrier_drop.ll +++ b/tests/ll-oracle/corpus/golden/wasm32/mir_suspend_carrier_drop.ll @@ -525,17 +525,20 @@ coro.begin: ; preds = %dyn.alloc, %entry %coro.handle = call ptr @llvm.coro.begin(token %coro.id, ptr %coro.mem) br label %alloca.prologue -coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend, %bb2, %frame_cleanup_registered +coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend.emit, %bb2, %frame_cleanup_registered call void @llvm.coro.end(ptr %coro.handle, i1 false, token none) call void @hew_cont_frame_handoff(ptr %coro.handle) ret ptr %coro.handle -coro.cleanup: ; preds = %coro.final.suspend, %coro.final.suspend, %helper_crash_cleanup_retire_merge8, %helper_crash_cleanup_retire_merge +coro.cleanup: ; preds = %coro.final.suspend.emit, %coro.final.suspend.emit, %helper_crash_cleanup_retire_merge8, %helper_crash_cleanup_retire_merge %coro.freemem = call ptr @llvm.coro.free(token %coro.id, ptr %coro.handle) %coro.freemem.isnull = icmp eq ptr %coro.freemem, null br i1 %coro.freemem.isnull, label %coro.suspend.return, label %coro.dyn.free coro.final.suspend: ; preds = %helper_crash_cleanup_return_merge_4 + br label %coro.final.suspend.emit + +coro.final.suspend.emit: ; preds = %coro.final.suspend %coro.final.save = call token @llvm.coro.save(ptr %coro.handle) %coro.final.s = call i8 @llvm.coro.suspend(token %coro.final.save, i1 true) switch i8 %coro.final.s, label %coro.suspend.return [ diff --git a/tests/ll-oracle/corpus/golden/wasm32/owned_vec_cross_function_release.ll b/tests/ll-oracle/corpus/golden/wasm32/owned_vec_cross_function_release.ll index 1650ed78fd..44db7ab752 100644 --- a/tests/ll-oracle/corpus/golden/wasm32/owned_vec_cross_function_release.ll +++ b/tests/ll-oracle/corpus/golden/wasm32/owned_vec_cross_function_release.ll @@ -453,17 +453,20 @@ coro.begin: ; preds = %dyn.alloc, %entry %coro.handle = call ptr @llvm.coro.begin(token %coro.id, ptr %coro.mem) br label %alloca.prologue -coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend, %bb7 +coro.suspend.return: ; preds = %coro.dyn.free, %coro.cleanup, %coro.final.suspend.emit, %bb7 call void @llvm.coro.end(ptr %coro.handle, i1 false, token none) call void @hew_cont_frame_handoff(ptr %coro.handle) ret ptr %coro.handle -coro.cleanup: ; preds = %coro.final.suspend, %coro.final.suspend, %gen_yield_abandon +coro.cleanup: ; preds = %coro.final.suspend.emit, %coro.final.suspend.emit, %gen_yield_abandon %coro.freemem = call ptr @llvm.coro.free(token %coro.id, ptr %coro.handle) %coro.freemem.isnull = icmp eq ptr %coro.freemem, null br i1 %coro.freemem.isnull, label %coro.suspend.return, label %coro.dyn.free coro.final.suspend: ; preds = %bb4 + br label %coro.final.suspend.emit + +coro.final.suspend.emit: ; preds = %coro.final.suspend %coro.final.save = call token @llvm.coro.save(ptr %coro.handle) %coro.final.s = call i8 @llvm.coro.suspend(token %coro.final.save, i1 true) switch i8 %coro.final.s, label %coro.suspend.return [