diff --git a/vlib/builtin/closure/closure.c.v b/vlib/builtin/closure/closure.c.v index 6f22139ca70d6c..088adcc8690f72 100644 --- a/vlib/builtin/closure/closure.c.v +++ b/vlib/builtin/closure/closure.c.v @@ -9,6 +9,8 @@ const ppc64_architecture = int(11) type ClosureGetDataFn = fn () voidptr +type ClosureInitFn = fn () + struct ClosurePage { mut: next &ClosurePage = unsafe { nil } @@ -17,21 +19,71 @@ mut: struct ClosureLiveInfo { mut: - ctx voidptr - owns_data bool + ctx voidptr + owns_data bool + generation u64 +} + +struct ClosureLifetimeRecord { +mut: + exec_ptr voidptr + generation u64 +} + +struct ClosureLifetimeFrame { +mut: + start int + end int +} + +@[heap] +struct ClosureLifetimeState { +mut: + owner_thread u64 + active bool + disposed bool + suspended int + frame_start int + frame_gen u64 + generation u64 + frame_generation u64 + records []ClosureLifetimeRecord + frames []ClosureLifetimeFrame + next_free &ClosureLifetimeState = unsafe { nil } +} + +// Lifetime owns a set of tracked closure callbacks that can be reclaimed together. +pub struct Lifetime { +mut: + state &ClosureLifetimeState = unsafe { nil } + generation u64 + disposed bool +} + +struct FrameToken { +mut: + state &ClosureLifetimeState = unsafe { nil } + thread_id u64 + state_generation u64 + generation u64 } @[heap] struct Closure { ClosureMutex mut: - closure_ptr voidptr - closure_get_data ClosureGetDataFn = unsafe { nil } - closure_cap int - free_closure_ptr voidptr - pages &ClosurePage = unsafe { nil } - v_page_size int = int(0x4000) - live map[voidptr]ClosureLiveInfo + closure_ptr voidptr + closure_get_data ClosureGetDataFn = unsafe { nil } + closure_cap int + free_closure_ptr voidptr + pages &ClosurePage = unsafe { nil } + v_page_size int = int(0x4000) + live map[voidptr]ClosureLiveInfo + active_lifetimes map[u64]&ClosureLifetimeState + next_generation u64 + free_lifetime_states &ClosureLifetimeState = unsafe { nil } + next_lifetime_generation u64 + lifetime_state_allocs u64 } __global g_closure = Closure{} @@ -303,23 +355,363 @@ fn closure_is_managed(exec_ptr voidptr) bool { } fn closure_live_set(exec_ptr voidptr, data voidptr, owns_data bool) { - if isnil(data) { - return - } + g_closure.next_generation++ g_closure.live[exec_ptr] = ClosureLiveInfo{ - ctx: data - owns_data: owns_data + ctx: data + owns_data: owns_data + generation: g_closure.next_generation } } -fn closure_live_delete(exec_ptr voidptr) bool { +fn closure_live_delete(exec_ptr voidptr) ClosureLiveInfo { if info := g_closure.live[exec_ptr] { - owns_data := info.owns_data g_closure.live[exec_ptr] = ClosureLiveInfo{} g_closure.live.delete(exec_ptr) - return owns_data + return info } - return false + return ClosureLiveInfo{} +} + +fn new_closure_lifetime_state_no_lock() &ClosureLifetimeState { + mut state := g_closure.free_lifetime_states + if !isnil(state) { + g_closure.free_lifetime_states = state.next_free + } else { + unsafe { + state = &ClosureLifetimeState(malloc(sizeof(ClosureLifetimeState))) + } + g_closure.lifetime_state_allocs++ + } + g_closure.next_lifetime_generation++ + unsafe { + *state = ClosureLifetimeState{ + owner_thread: closure_current_thread_id_platform() + generation: g_closure.next_lifetime_generation + } + } + return state +} + +fn new_closure_lifetime_state() &ClosureLifetimeState { + closure_mtx_lock_platform() + state := new_closure_lifetime_state_no_lock() + closure_mtx_unlock_platform() + return state +} + +fn closure_lifetime_recycle_state_no_lock(mut state &ClosureLifetimeState) { + state.disposed = true + state.active = false + state.suspended = 0 + state.frame_start = 0 + state.frame_gen = 0 + state.frame_generation = 0 + unsafe { + state.records.free() + state.frames.free() + } + state.records = []ClosureLifetimeRecord{} + state.frames = []ClosureLifetimeFrame{} + state.next_free = g_closure.free_lifetime_states + g_closure.free_lifetime_states = state +} + +fn closure_lifetime_error(state &ClosureLifetimeState, generation u64, thread_id u64) string { + if state.disposed || state.generation != generation { + return 'closure lifetime used after dispose' + } + if state.owner_thread != thread_id { + return 'closure lifetime used from a different thread' + } + return '' +} + +fn (mut lifetime Lifetime) ensure_state() !&ClosureLifetimeState { + closure_ensure_initialized() + if isnil(lifetime.state) { + if lifetime.disposed { + return error('closure lifetime used after dispose') + } + lifetime.state = new_closure_lifetime_state() + lifetime.generation = lifetime.state.generation + return lifetime.state + } + closure_mtx_lock_platform() + state := lifetime.state + if lifetime.disposed || state.disposed || state.generation != lifetime.generation { + closure_mtx_unlock_platform() + return error('closure lifetime used after dispose') + } + closure_mtx_unlock_platform() + return state +} + +fn closure_lifetime_track_no_lock(exec_ptr voidptr) { + thread_id := closure_current_thread_id_platform() + mut state := g_closure.active_lifetimes[thread_id] or { return } + if state.suspended > 0 { + return + } + info := g_closure.live[exec_ptr] or { return } + state.records << ClosureLifetimeRecord{ + exec_ptr: exec_ptr + generation: info.generation + } +} + +@[direct_array_access] +fn closure_slot_data(exec_ptr voidptr) voidptr { + unsafe { + mut p := closure_slot_meta(exec_ptr) + if is_ppc64() { + return p[2] + } + return p[0] + } +} + +@[direct_array_access] +fn closure_release_no_lock(exec_ptr voidptr, generation u64) bool { + if !closure_is_managed(exec_ptr) { + return false + } + info := g_closure.live[exec_ptr] or { return false } + if generation != 0 && info.generation != generation { + return false + } + data := closure_slot_data(exec_ptr) + _ := closure_live_delete(exec_ptr) + if info.owns_data && !isnil(data) { + unsafe { free(data) } + } + unsafe { + mut p := closure_slot_meta(exec_ptr) + p[0] = g_closure.free_closure_ptr + if is_ppc64() { + p[1] = nil + p[2] = nil + p[3] = nil + } else { + p[1] = nil + } + g_closure.free_closure_ptr = exec_ptr + } + return true +} + +fn closure_lifetime_release_records_no_lock(records []ClosureLifetimeRecord, start int, end int) { + for i in start .. end { + record := records[i] + closure_release_no_lock(record.exec_ptr, record.generation) + } +} + +fn closure_lifetime_reclaim_no_lock(mut state ClosureLifetimeState, retain int) { + keep := if retain < 0 { 0 } else { retain } + if state.frames.len <= keep { + return + } + reclaim_count := state.frames.len - keep + mut cutoff := 0 + for i in 0 .. reclaim_count { + frame := state.frames[i] + closure_lifetime_release_records_no_lock(state.records, frame.start, frame.end) + cutoff = frame.end + } + state.frames.delete_many(0, reclaim_count) + if cutoff > 0 { + state.records.delete_many(0, cutoff) + for mut frame in state.frames { + frame.start -= cutoff + frame.end -= cutoff + } + } +} + +fn closure_ensure_initialized() { + closure_init_once_platform() +} + +// new_lifetime creates a lifetime object for tracking closure callbacks created inside frames. +pub fn new_lifetime() Lifetime { + closure_ensure_initialized() + state := new_closure_lifetime_state() + return Lifetime{ + state: state + generation: state.generation + } +} + +// lifetime_state_allocs writes the number of allocated closure lifetime bookkeeping states to out. +@[if track_heap ?] +pub fn lifetime_state_allocs(out &u64) { + closure_ensure_initialized() + closure_mtx_lock_platform() + unsafe { + *out = g_closure.lifetime_state_allocs + } + closure_mtx_unlock_platform() +} + +fn (mut lifetime Lifetime) begin_frame() !FrameToken { + mut state := lifetime.ensure_state()! + thread_id := closure_current_thread_id_platform() + closure_mtx_lock_platform() + err := closure_lifetime_error(state, lifetime.generation, thread_id) + if err != '' { + closure_mtx_unlock_platform() + return error(err) + } + if state.active { + closure_mtx_unlock_platform() + return error('closure lifetime frames can not be nested') + } + if state.suspended > 0 { + closure_mtx_unlock_platform() + return error('closure lifetime frame while suspended') + } + if _ := g_closure.active_lifetimes[thread_id] { + closure_mtx_unlock_platform() + return error('another closure lifetime is already active on this thread') + } + state.frame_generation++ + state.active = true + state.frame_start = state.records.len + state.frame_gen = state.frame_generation + g_closure.active_lifetimes[thread_id] = state + closure_mtx_unlock_platform() + return FrameToken{ + state: state + thread_id: thread_id + state_generation: lifetime.generation + generation: state.frame_generation + } +} + +fn (mut lifetime Lifetime) end_frame(token FrameToken) ! { + if isnil(token.state) { + return error('invalid closure lifetime frame token') + } + mut state := token.state + thread_id := closure_current_thread_id_platform() + closure_mtx_lock_platform() + err := closure_lifetime_error(state, token.state_generation, thread_id) + if err != '' { + closure_mtx_unlock_platform() + return error(err) + } + if token.thread_id != thread_id || token.generation != state.frame_gen || !state.active { + closure_mtx_unlock_platform() + return error('invalid closure lifetime frame token') + } + state.frames << ClosureLifetimeFrame{ + start: state.frame_start + end: state.records.len + } + state.active = false + state.frame_start = 0 + state.frame_gen = 0 + g_closure.active_lifetimes[thread_id] = unsafe { nil } + g_closure.active_lifetimes.delete(thread_id) + closure_mtx_unlock_platform() +} + +// frame runs work while tracking closure callbacks created by that work in this lifetime. +// The work callback itself is borrowed; only closures allocated while the frame is active +// are owned by the lifetime and later released by reclaim or dispose. +pub fn (mut lifetime Lifetime) frame(work fn ()) ! { + token := lifetime.begin_frame()! + mut ended := false + defer { + if !ended { + lifetime.end_frame(token) or {} + } + } + // Terminal panic can abort before scoped cleanup runs. + work() + lifetime.end_frame(token)! + ended = true +} + +// reclaim releases tracked closure callbacks from old frames while retaining the newest retain frames. +pub fn (mut lifetime Lifetime) reclaim(retain int) ! { + mut state := lifetime.ensure_state()! + thread_id := closure_current_thread_id_platform() + closure_mtx_lock_platform() + err := closure_lifetime_error(state, lifetime.generation, thread_id) + if err != '' { + closure_mtx_unlock_platform() + return error(err) + } + if state.active { + closure_mtx_unlock_platform() + return error('closure lifetime reclaim while a frame is active') + } + closure_lifetime_reclaim_no_lock(mut state, retain) + closure_mtx_unlock_platform() +} + +// reclaim_all releases all tracked closure callbacks owned by this lifetime. +pub fn (mut lifetime Lifetime) reclaim_all() ! { + lifetime.reclaim(0)! +} + +// dispose releases all tracked closure callbacks and invalidates this lifetime. +pub fn (mut lifetime Lifetime) dispose() ! { + mut state := lifetime.ensure_state()! + thread_id := closure_current_thread_id_platform() + closure_mtx_lock_platform() + err := closure_lifetime_error(state, lifetime.generation, thread_id) + if err != '' { + closure_mtx_unlock_platform() + return error(err) + } + if state.active { + closure_mtx_unlock_platform() + return error('closure lifetime dispose while a frame is active') + } + if state.suspended > 0 { + closure_mtx_unlock_platform() + return error('closure lifetime dispose while suspended') + } + closure_lifetime_reclaim_no_lock(mut state, 0) + lifetime.state = unsafe { nil } + lifetime.disposed = true + closure_lifetime_recycle_state_no_lock(mut state) + closure_mtx_unlock_platform() +} + +// suspend runs work without tracking closure callbacks in the active frame of this lifetime. +// The work callback is borrowed and is not owned or released by the lifetime. +pub fn (mut lifetime Lifetime) suspend(work fn ()) ! { + mut state := lifetime.ensure_state()! + thread_id := closure_current_thread_id_platform() + closure_mtx_lock_platform() + err := closure_lifetime_error(state, lifetime.generation, thread_id) + if err != '' { + closure_mtx_unlock_platform() + return error(err) + } + if active := g_closure.active_lifetimes[thread_id] { + if active != state { + closure_mtx_unlock_platform() + return error('another closure lifetime is already active on this thread') + } + } + state.suspended++ + closure_mtx_unlock_platform() + defer { + closure_mtx_lock_platform() + state.suspended-- + closure_mtx_unlock_platform() + } + work() +} + +// untracked runs work without tracking closure callbacks in this lifetime. +// The work callback is borrowed and is not owned or released by the lifetime. +pub fn (mut lifetime Lifetime) untracked(work fn ()) ! { + lifetime.suspend(work)! } // closure_alloc allocates executable memory pages for closures(INTERNAL COMPILER USE ONLY). @@ -348,10 +740,19 @@ fn closure_alloc() { // closure_init initializes global closure subsystem(INTERNAL COMPILER USE ONLY). fn closure_init() { + closure_ensure_initialized() +} + +fn closure_init_body() { // Determine system page size mut page_size := get_page_size_platform() g_closure.v_page_size = page_size // Store calculated size g_closure.live = map[voidptr]ClosureLiveInfo{} + g_closure.active_lifetimes = map[u64]&ClosureLifetimeState{} + g_closure.next_generation = 0 + g_closure.free_lifetime_states = unsafe { nil } + g_closure.next_lifetime_generation = 0 + g_closure.lifetime_state_allocs = 0 // Initialize thread-safety lock closure_mtx_lock_init_platform() @@ -395,6 +796,7 @@ fn closure_create(func voidptr, data voidptr) voidptr { // closure_create_with_data creates closure objects with explicit context ownership(INTERNAL COMPILER USE ONLY). @[direct_array_access] fn closure_create_with_data(func voidptr, data voidptr, owns_data bool) voidptr { + closure_ensure_initialized() closure_mtx_lock_platform() mut curr_closure := g_closure.free_closure_ptr @@ -436,6 +838,7 @@ fn closure_create_with_data(func voidptr, data voidptr, owns_data bool) voidptr } } closure_live_set(curr_closure, data, owns_data) + closure_lifetime_track_no_lock(curr_closure) closure_mtx_unlock_platform() // Return executable closure object @@ -455,39 +858,15 @@ fn closure_data(closure voidptr) voidptr { } } -// closure_try_destroy frees a managed closure slot and its owned context when the closure is known to be temporary. +// Legacy compiler hook for one-shot local cleanup. Scoped lifetime reclaim uses generation checks. @[direct_array_access] fn closure_try_destroy(closure voidptr) { if isnil(closure) { return } + closure_ensure_initialized() exec_ptr := closure_exec_ptr(closure) closure_mtx_lock_platform() - if !closure_is_managed(exec_ptr) { - closure_mtx_unlock_platform() - return - } - unsafe { - mut p := closure_slot_meta(exec_ptr) - mut data := nil - if is_ppc64() { - data = p[2] - } else { - data = p[0] - } - owns_data := closure_live_delete(exec_ptr) - if owns_data && !isnil(data) { - free(data) - } - p[0] = g_closure.free_closure_ptr - if is_ppc64() { - p[1] = nil - p[2] = nil - p[3] = nil - } else { - p[1] = nil - } - g_closure.free_closure_ptr = exec_ptr - } + closure_release_no_lock(exec_ptr, 0) closure_mtx_unlock_platform() } diff --git a/vlib/builtin/closure/closure_nix.c.v b/vlib/builtin/closure/closure_nix.c.v index 5e01c4d83a35f9..4e8e1d2acf4a1d 100644 --- a/vlib/builtin/closure/closure_nix.c.v +++ b/vlib/builtin/closure/closure_nix.c.v @@ -2,13 +2,18 @@ module closure $if !freestanding && !vinix { #include -} + #insert "@VEXEROOT/vlib/builtin/closure/closure_once_nix.h" -@[typedef] -pub struct C.pthread_mutex_t {} + fn C.v_closure_init_once(ClosureInitFn) +} struct ClosureMutex { - closure_mtx C.pthread_mutex_t + closure_mtx [128]u8 +} + +@[inline] +fn closure_mtx_ptr_platform() voidptr { + return unsafe { voidptr(&g_closure.closure_mtx[0]) } } @[inline] @@ -65,20 +70,39 @@ fn get_page_size_platform() int { @[inline] fn closure_mtx_lock_init_platform() { $if !freestanding || vinix { - C.pthread_mutex_init(&g_closure.closure_mtx, 0) + C.pthread_mutex_init(closure_mtx_ptr_platform(), 0) } } @[inline] fn closure_mtx_lock_platform() { $if !freestanding || vinix { - C.pthread_mutex_lock(&g_closure.closure_mtx) + C.pthread_mutex_lock(closure_mtx_ptr_platform()) } } @[inline] fn closure_mtx_unlock_platform() { $if !freestanding || vinix { - C.pthread_mutex_unlock(&g_closure.closure_mtx) + C.pthread_mutex_unlock(closure_mtx_ptr_platform()) + } +} + +@[inline] +fn closure_current_thread_id_platform() u64 { + $if !freestanding { + return u64(C.pthread_self()) + } + return u64(0) +} + +@[inline] +fn closure_init_once_platform() { + $if freestanding || vinix { + if isnil(g_closure.closure_ptr) { + closure_init_body() + } + } $else { + C.v_closure_init_once(closure_init_body) } } diff --git a/vlib/builtin/closure/closure_once_nix.h b/vlib/builtin/closure/closure_once_nix.h new file mode 100644 index 00000000000000..51022200919956 --- /dev/null +++ b/vlib/builtin/closure/closure_once_nix.h @@ -0,0 +1,28 @@ +#ifndef V_CLOSURE_ONCE_NIX_H +#define V_CLOSURE_ONCE_NIX_H + +#include + +typedef void (*v_closure_init_fn)(void); + +#ifndef V_CLOSURE_STATIC_INLINE +# ifdef _MSC_VER +# define V_CLOSURE_STATIC_INLINE static __inline +# else +# define V_CLOSURE_STATIC_INLINE static inline +# endif +#endif + +static pthread_mutex_t v_closure_once_mutex = PTHREAD_MUTEX_INITIALIZER; +static int v_closure_once_done = 0; + +V_CLOSURE_STATIC_INLINE void v_closure_init_once(v_closure_init_fn init_fn) { + pthread_mutex_lock(&v_closure_once_mutex); + if (!v_closure_once_done) { + init_fn(); + v_closure_once_done = 1; + } + pthread_mutex_unlock(&v_closure_once_mutex); +} + +#endif diff --git a/vlib/builtin/closure/closure_once_windows.h b/vlib/builtin/closure/closure_once_windows.h new file mode 100644 index 00000000000000..edfe3a00be51b6 --- /dev/null +++ b/vlib/builtin/closure/closure_once_windows.h @@ -0,0 +1,28 @@ +#ifndef V_CLOSURE_ONCE_WINDOWS_H +#define V_CLOSURE_ONCE_WINDOWS_H + +#include + +typedef void (*v_closure_init_fn)(void); + +#ifndef V_CLOSURE_STATIC_INLINE +# ifdef _MSC_VER +# define V_CLOSURE_STATIC_INLINE static __inline +# else +# define V_CLOSURE_STATIC_INLINE static inline +# endif +#endif + +static SRWLOCK v_closure_once_lock; +static int v_closure_once_done = 0; + +V_CLOSURE_STATIC_INLINE void v_closure_init_once(v_closure_init_fn init_fn) { + AcquireSRWLockExclusive(&v_closure_once_lock); + if (!v_closure_once_done) { + init_fn(); + v_closure_once_done = 1; + } + ReleaseSRWLockExclusive(&v_closure_once_lock); +} + +#endif diff --git a/vlib/builtin/closure/closure_windows.c.v b/vlib/builtin/closure/closure_windows.c.v index c4b24b79c8b1e4..4a6cc847ba2775 100644 --- a/vlib/builtin/closure/closure_windows.c.v +++ b/vlib/builtin/closure/closure_windows.c.v @@ -1,6 +1,9 @@ module closure #include +#insert "@VEXEROOT/vlib/builtin/closure/closure_once_windows.h" + +fn C.v_closure_init_once(ClosureInitFn) struct ClosureMutex { closure_mtx C.SRWLOCK @@ -51,3 +54,13 @@ fn closure_mtx_lock_platform() { fn closure_mtx_unlock_platform() { C.ReleaseSRWLockExclusive(&g_closure.closure_mtx) } + +@[inline] +fn closure_current_thread_id_platform() u64 { + return u64(C.GetCurrentThreadId()) +} + +@[inline] +fn closure_init_once_platform() { + C.v_closure_init_once(closure_init_body) +} diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index f303859cbd4687..0be4fb58244669 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -209,6 +209,7 @@ mut: right_is_opt bool // right hand side on assignment is an option assign_ct_type map[int]ast.Type // left hand side resolved comptime type expected_rhs_type_by_pos map[int]ast.Type // expected value type for local RHS expressions + closure_frame_arg_tmps map[int]string indent int empty_line bool assign_op token.Kind // *=, =, etc (for array_set) @@ -431,6 +432,7 @@ pub fn gen(files []&ast.File, mut table ast.Table, pref_ &pref.Preferences) GenO boehm_keep_decl: map[string]bool{} boehm_keep_gen: map[string]bool{} boehm_keep_busy: map[string]bool{} + closure_frame_arg_tmps: map[int]string{} generic_parts_cache: []i8{len: table.type_symbols.len} unwrap_generic_cache: map[u64]ast.Type{} resolved_scope_var_type_cache: map[u64]ast.Type{} @@ -1089,6 +1091,7 @@ fn cgen_process_one_file_cb(mut p pool.PoolProcessor, idx int, wid int) voidptr boehm_keep_decl: map[string]bool{} boehm_keep_gen: map[string]bool{} boehm_keep_busy: map[string]bool{} + closure_frame_arg_tmps: map[int]string{} generic_parts_cache: []i8{len: global_g.table.type_symbols.len} unwrap_generic_cache: map[u64]ast.Type{} resolved_scope_var_type_cache: map[u64]ast.Type{} @@ -11138,6 +11141,11 @@ fn (mut g Gen) return_stmt(node ast.Return) { } } return_needs_local_closure_cleanup := g.return_needs_local_closure_cleanup(node) + return_expr0 := unwrap_paren_call_expr(expr0) + return_call_needs_closure_lifetime_arg_tmp := match return_expr0 { + ast.CallExpr { g.call_needs_closure_lifetime_arg_tmp(return_expr0) } + else { false } + } if exprs_len > 0 { // `$veb.html()` expands to statements, so the Result return @@ -11229,16 +11237,17 @@ fn (mut g Gen) return_stmt(node ast.Return) { ret_typ := g.ret_styp(fn_ret_type) // `return fn_call_opt()` - if exprs_len == 1 && (fn_return_is_option || fn_return_is_result) && expr0 is ast.CallExpr - && expr0.or_block.kind == .absent { - mut resolved_call_return_type := g.resolve_return_type(expr0) + if exprs_len == 1 && (fn_return_is_option || fn_return_is_result) + && return_expr0 is ast.CallExpr && return_expr0.or_block.kind == .absent { + mut resolved_call_return_type := g.resolve_return_type(return_expr0) if resolved_call_return_type == ast.void_type { - resolved_call_return_type = expr0.return_type + resolved_call_return_type = return_expr0.return_type } if g.unwrap_generic(g.recheck_concrete_type(resolved_call_return_type)) == ret_type { - if g.defer_stmts.len > 0 || return_needs_local_closure_cleanup { + if g.defer_stmts.len > 0 || return_needs_local_closure_cleanup + || return_call_needs_closure_lifetime_arg_tmp { g.write('${ret_typ} ${tmpvar} = ') - g.expr(expr0) + g.expr(return_expr0) g.writeln(';') g.write_defer_stmts_when_needed(node.scope, true, node.pos) if return_needs_local_closure_cleanup { @@ -11248,7 +11257,7 @@ fn (mut g Gen) return_stmt(node ast.Return) { } else { g.write_defer_stmts_when_needed(node.scope, true, node.pos) g.write('return ') - g.expr(expr0) + g.expr(return_expr0) g.writeln(';') } return @@ -11256,6 +11265,7 @@ fn (mut g Gen) return_stmt(node ast.Return) { } mut use_tmp_var := g.defer_stmts.len > 0 || g.defer_profile_code.len > 0 || g.cur_lock.lockeds.len > 0 || return_needs_local_closure_cleanup + || return_call_needs_closure_lifetime_arg_tmp || (fn_return_is_multi && exprs_len >= 1 && fn_return_is_option) || fn_return_is_fixed_array_non_result || (fn_return_is_multi && ret_expr_types.any(g.table.final_sym(it).kind == .array_fixed)) diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index f4c3c6000dc287..40839d4f7809ef 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -2312,6 +2312,12 @@ fn (mut g Gen) call_expr(node ast.CallExpr) { return } } + closure_frame_arg_tmps := g.prepare_closure_lifetime_arg_tmps(node) + defer { + for pos in closure_frame_arg_tmps.keys() { + g.closure_frame_arg_tmps.delete(pos) + } + } old_inside_call := g.inside_call g.inside_call = true // Reset inside_selector_lhs so that receiver expressions inside method @@ -2444,6 +2450,13 @@ fn (mut g Gen) call_expr(node ast.CallExpr) { } else { g.fn_call(node) } + if closure_frame_arg_tmps.len > 0 { + g.writeln(';') + for _, tmp in closure_frame_arg_tmps { + g.writeln('builtin__closure__closure_try_destroy((voidptr)${tmp});') + } + g.set_current_pos_as_last_stmt_pos() + } if needs_tmp_fn_result_cleanup && !gen_or && !gen_keep_alive && !g.inside_curry_call { if node.return_type == ast.void_type { g.writeln(';') @@ -2527,6 +2540,105 @@ fn (mut g Gen) call_expr(node ast.CallExpr) { } } +fn (mut g Gen) is_closure_lifetime_callback_call(node ast.CallExpr) bool { + method_name := node.name.all_after_last('.') + if !node.is_method || method_name !in ['frame', 'suspend', 'untracked'] || node.args.len != 1 { + return false + } + mut receiver_type := g.unwrap_generic(node.receiver_type) + if receiver_type == 0 { + receiver_type = g.unwrap_generic(node.left_type) + } + if receiver_type == 0 { + return false + } + if receiver_type.is_ptr() { + receiver_type = receiver_type.deref() + } + return g.table.sym(receiver_type).name == 'builtin.closure.Lifetime' +} + +fn (mut g Gen) closure_lifetime_arg_needs_tmp(expr ast.Expr) bool { + unwrapped_expr := expr.remove_par() + return match unwrapped_expr { + ast.AnonFn { + unwrapped_expr.inherited_vars.len > 0 + } + ast.SelectorExpr { + unwrapped_expr.has_hidden_receiver + } + else { + false + } + } +} + +fn (mut g Gen) call_needs_closure_lifetime_arg_tmp(node ast.CallExpr) bool { + return g.is_closure_lifetime_callback_call(node) + && g.closure_lifetime_arg_needs_tmp(node.args[0].expr) +} + +fn (mut g Gen) closure_lifetime_fn_type_tmp_signature(fn_types []ast.Type, tmp string) ?string { + for raw_fn_typ in fn_types { + fn_typ := g.unwrap_generic(g.recheck_concrete_type(raw_fn_typ)) + if fn_typ == 0 { + continue + } + fn_sym := g.table.final_sym(fn_typ) + if fn_sym.info is ast.FnType { + func := fn_sym.info.func + return g.fn_var_signature(ast.void_type, func.return_type, func.params.map(it.typ), tmp) + } + } + return none +} + +fn (mut g Gen) closure_lifetime_arg_tmp_signature(node ast.CallExpr, arg ast.CallArg, tmp string) ?string { + unwrapped_expr := arg.expr.remove_par() + match unwrapped_expr { + ast.AnonFn { + return g.fn_var_signature(ast.void_type, unwrapped_expr.decl.return_type, + unwrapped_expr.decl.params.map(it.typ), tmp) + } + ast.SelectorExpr { + mut fn_types := []ast.Type{} + if arg.typ != 0 { + fn_types << arg.typ + } + if unwrapped_expr.typ != 0 { + fn_types << unwrapped_expr.typ + } + if node.expected_arg_types.len > 0 && node.expected_arg_types[0] != 0 { + fn_types << node.expected_arg_types[0] + } + return g.closure_lifetime_fn_type_tmp_signature(fn_types, tmp) + } + else {} + } + + return none +} + +fn (mut g Gen) prepare_closure_lifetime_arg_tmps(node ast.CallExpr) map[int]string { + mut tmps := map[int]string{} + if !g.call_needs_closure_lifetime_arg_tmp(node) { + return tmps + } + arg := node.args[0] + tmp := g.new_tmp_var() + fn_type := g.closure_lifetime_arg_tmp_signature(node, arg, tmp) or { return tmps } + line := g.go_before_last_stmt().trim_space() + g.empty_line = true + g.write('${fn_type} = ') + g.expr(ast.Expr(arg.expr)) + g.writeln(';') + g.set_current_pos_as_last_stmt_pos() + g.write(line) + tmps[arg.pos.pos] = tmp + g.closure_frame_arg_tmps[arg.pos.pos] = tmp + return tmps +} + fn (mut g Gen) conversion_function_call(prefix string, postfix string, node ast.CallExpr) { g.write('${prefix}( (') g.expr(node.left) @@ -6410,6 +6522,13 @@ fn (mut g Gen) call_args(node ast.CallExpr) { if is_variadic && i == expected_types.len - 1 { break } + if tmp := g.closure_frame_arg_tmps[arg.pos.pos] { + g.write(tmp) + if i < args.len - 1 || is_variadic { + g.write(', ') + } + continue + } mut is_smartcast := false if arg.expr is ast.Ident { if arg.expr.obj is ast.Var { diff --git a/vlib/v/tests/fns/closure_context_skip_unused_test.v b/vlib/v/tests/fns/closure_context_skip_unused_test.v new file mode 100644 index 00000000000000..cd911ecf1046aa --- /dev/null +++ b/vlib/v/tests/fns/closure_context_skip_unused_test.v @@ -0,0 +1,87 @@ +import os + +const vexe = @VEXE + +fn missing_boehm_leak_lib(output string) bool { + return output.contains('libgc') && (output.contains('was not found') + || output.contains('cannot find')) +} + +fn closure_skip_unused_source() string { + return [ + 'module main', + 'import builtin.closure', + '', + 'fn make_cb(seed int) fn () int {', + '\tvalues := []int{len: 8, init: seed + index}', + '\treturn fn [values] () int {', + '\t\treturn values[0] + values[7]', + '\t}', + '}', + '', + 'fn no_capture_work() {}', + '', + 'fn consume(i int) int {', + '\th := fn [i] (x int) int {', + '\t\treturn i + x', + '\t}', + '\treturn h(i + 1)', + '}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut total := 0', + '\tlifetime.frame(fn () {', + '\t\tcb := make_cb(20)', + '\t\tassert cb() == 47', + '\t})!', + '\tlifetime.suspend(fn () {', + '\t\tcb := make_cb(30)', + '\t\tassert cb() == 67', + '\t})!', + '\tlifetime.suspend(no_capture_work)!', + '\tfor i in 0 .. 64 {', + '\t\ttotal += consume(i)', + '\t}', + '\tlifetime.reclaim_all()!', + '\tlifetime.dispose()!', + '\tassert total == 4096', + '\tprintln(total)', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') +} + +fn run_closure_skip_unused_case(tmp_dir string, mode string) { + source_path := os.join_path(tmp_dir, 'closure_skip_unused_${mode}.v') + binary_path := os.join_path(tmp_dir, 'closure_skip_unused_${mode}') + os.write_file(source_path, closure_skip_unused_source()) or { panic(err) } + compile_cmd := '${os.quoted_path(vexe)} -skip-unused -gc ${mode} -o ${os.quoted_path(binary_path)} ${os.quoted_path(source_path)}' + compile_res := os.execute(compile_cmd) + if mode == 'boehm_leak' && compile_res.exit_code != 0 + && missing_boehm_leak_lib(compile_res.output) { + eprintln('skipping boehm_leak closure skip-unused test: missing libgc') + return + } + assert compile_res.exit_code == 0, compile_res.output + if mode == 'boehm_leak' { + return + } + run_res := os.execute(os.quoted_path(binary_path)) + assert run_res.exit_code == 0, run_res.output + assert run_res.output.contains('4096'), run_res.output +} + +fn test_closure_context_helpers_are_kept_with_skip_unused() { + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_context_skip_unused_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + for mode in ['boehm', 'boehm_leak', 'none'] { + run_closure_skip_unused_case(tmp_dir, mode) + } +} diff --git a/vlib/v/tests/fns/closure_lifetime_api_test.v b/vlib/v/tests/fns/closure_lifetime_api_test.v new file mode 100644 index 00000000000000..8b1fc1c548a90b --- /dev/null +++ b/vlib/v/tests/fns/closure_lifetime_api_test.v @@ -0,0 +1,1260 @@ +import os + +const vexe = @VEXE + +fn missing_boehm_leak_lib(output string) bool { + return output.contains('libgc') && (output.contains('was not found') + || output.contains('cannot find')) +} + +fn count_occurrences(haystack string, needle string) int { + mut pos := 0 + mut count := 0 + for { + idx := haystack[pos..].index(needle) or { break } + count++ + pos += idx + needle.len + } + return count +} + +fn write_program(tmp_dir string, name string, source string) string { + source_path := os.join_path(tmp_dir, '${name}.v') + os.write_file(source_path, source) or { panic(err) } + return source_path +} + +fn run_program_with_gc(tmp_dir string, name string, source string, mode string) os.Result { + source_path := write_program(tmp_dir, '${name}_${mode}', source) + return os.execute('${os.quoted_path(vexe)} -gc ${mode} run ${os.quoted_path(source_path)}') +} + +fn compile_program_with_gc(tmp_dir string, name string, source string, mode string) os.Result { + source_path := write_program(tmp_dir, '${name}_${mode}', source) + binary_path := os.join_path(tmp_dir, '${name}_${mode}') + return os.execute('${os.quoted_path(vexe)} -gc ${mode} -o ${os.quoted_path(binary_path)} ${os.quoted_path(source_path)}') +} + +fn compile_freestanding_object(tmp_dir string, name string, source string) os.Result { + source_path := write_program(tmp_dir, name, source) + object_path := os.join_path(tmp_dir, '${name}.o') + return os.execute('${os.quoted_path(vexe)} -gc none -freestanding -no-std -is_o -o ${os.quoted_path(object_path)} ${os.quoted_path(source_path)}') +} + +fn run_program_with_track_heap(tmp_dir string, name string, source string) os.Result { + source_path := write_program(tmp_dir, name, source) + return os.execute('${os.quoted_path(vexe)} -gc none -d track_heap run ${os.quoted_path(source_path)}') +} + +fn c_output_for_program(tmp_dir string, name string, source string) os.Result { + source_path := write_program(tmp_dir, name, source) + return os.execute('${os.quoted_path(vexe)} -o - ${os.quoted_path(source_path)}') +} + +fn assert_boehm_leak_compile_or_missing_lib(tmp_dir string, name string, source string) { + res := compile_program_with_gc(tmp_dir, name, source, 'boehm_leak') + if res.exit_code != 0 && missing_boehm_leak_lib(res.output) { + eprintln('skipping boehm_leak compile for ${name}: missing libgc') + return + } + assert res.exit_code == 0, res.output +} + +fn assert_boehm_leak_runtime_or_compile_only(tmp_dir string, name string, source string) { + res := run_program_with_gc(tmp_dir, name, source, 'boehm_leak') + if res.exit_code == 0 { + return + } + if missing_boehm_leak_lib(res.output) { + eprintln('skipping boehm_leak runtime for ${name}: missing libgc') + return + } + if res.output.contains('leaked objects') || res.output.contains('Found ') { + eprintln('boehm_leak runtime reported runtime allocations for ${name}; keeping compile-only coverage') + assert_boehm_leak_compile_or_missing_lib(tmp_dir, '${name}_compile_only', source) + return + } + assert false, res.output +} + +fn no_captured_closure_lifetime_source() string { + return [ + 'module main', + 'import builtin.closure', + '', + 'fn no_capture_work() {', + '\tassert true', + '}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(no_capture_work)!', + '\tlifetime.suspend(no_capture_work)!', + '\tlifetime.untracked(no_capture_work)!', + '\tlifetime.reclaim_all()!', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') +} + +fn closure_lifetime_runtime_source() string { + return [ + 'module main', + 'import builtin.closure', + '', + 'fn no_capture_work() {}', + '', + '@[heap]', + 'struct CallbackBox {', + 'mut:', + '\tcb fn () int = fn () int { return -1 }', + '}', + '', + '@[heap]', + 'struct IntBox {', + 'mut:', + '\tvalue int', + '}', + '', + 'fn make_cb(value int) fn () int {', + '\tpayload := []int{len: 64, init: value + index}', + '\treturn fn [payload] () int {', + '\t\treturn payload[0] + payload[payload.len - 1]', + '\t}', + '}', + '', + 'fn call_cb(cb fn () int) int {', + '\treturn cb()', + '}', + '', + 'fn collect_and_churn() {', + '\tgc_collect()', + '\tfor _ in 0 .. 512 {', + '\t\tunsafe {', + '\t\t\tp := malloc(32)', + '\t\t\tvmemset(p, 0x55, 32)', + '\t\t}', + '\t}', + '\tgc_collect()', + '}', + '', + 'fn outside_lifetime_closure_survives_lifetime_reclaim() ! {', + '\toutside := make_cb(10)', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(fn () {', + '\t\tinside := make_cb(100)', + '\t\tassert inside() == 263', + '\t})!', + '\tlifetime.reclaim_all()!', + '\tcollect_and_churn()', + '\tassert outside() == 83', + '\tlifetime.dispose()!', + '}', + '', + 'fn reclaim_keeps_requested_recent_frame() ! {', + '\tmut first := &CallbackBox{}', + '\tmut second := &CallbackBox{}', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(fn [mut first] () {', + '\t\tfirst.cb = make_cb(1)', + '\t\tassert first.cb() == 65', + '\t})!', + '\tlifetime.frame(fn [mut second] () {', + '\t\tsecond.cb = make_cb(2)', + '\t\tassert second.cb() == 67', + '\t})!', + '\tlifetime.reclaim(1)!', + '\tcollect_and_churn()', + '\tassert second.cb() == 67', + '\tlifetime.reclaim_all()!', + '\tlifetime.dispose()!', + '}', + '', + 'fn two_lifetimes_reclaim_independently() ! {', + '\tmut first := &CallbackBox{}', + '\tmut second := &CallbackBox{}', + '\tmut lifetime_a := closure.new_lifetime()', + '\tmut lifetime_b := closure.new_lifetime()', + '\tlifetime_a.frame(fn [mut first] () {', + '\t\tfirst.cb = make_cb(20)', + '\t\tassert first.cb() == 103', + '\t})!', + '\tlifetime_b.frame(fn [mut second] () {', + '\t\tsecond.cb = make_cb(30)', + '\t\tassert second.cb() == 123', + '\t})!', + '\tlifetime_a.reclaim_all()!', + '\tcollect_and_churn()', + '\tassert second.cb() == 123', + '\tlifetime_b.reclaim_all()!', + '\tlifetime_a.dispose()!', + '\tlifetime_b.dispose()!', + '}', + '', + 'fn suspended_persistent_callbacks_survive_reclaim_and_spawn() ! {', + '\tmut persisted := &CallbackBox{}', + '\tmut spawned_value := &IntBox{value: -1}', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(fn [mut lifetime, mut persisted, mut spawned_value] () {', + '\t\tshort := make_cb(5)', + '\t\tassert short() == 73', + '\t\tlifetime.suspend(fn [mut persisted, mut spawned_value] () {', + '\t\t\tpersisted.cb = make_cb(40)', + '\t\t\tth := spawn call_cb(persisted.cb)', + '\t\t\tspawned_value.value = th.wait()', + '\t\t}) or { panic(err) }', + '\t})!', + '\tlifetime.reclaim_all()!', + '\tcollect_and_churn()', + '\tassert persisted.cb() == 143', + '\tassert spawned_value.value == 143', + '\tlifetime.dispose()!', + '}', + '', + 'fn stale_reused_slot_does_not_release_suspended_newer_closure() ! {', + '\tmut survivor := &CallbackBox{}', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(fn [mut lifetime, mut survivor] () {', + '\t\tfor i in 0 .. 96 {', + '\t\t\th := fn [i] () int { return i }', + '\t\t\tassert h() == i', + '\t\t}', + '\t\tlifetime.suspend(fn [mut survivor] () {', + '\t\t\tsurvivor.cb = make_cb(70)', + '\t\t\tassert survivor.cb() == 203', + '\t\t}) or { panic(err) }', + '\t})!', + '\tlifetime.reclaim_all()!', + '\tcollect_and_churn()', + '\tassert survivor.cb() == 203', + '\tlifetime.dispose()!', + '}', + '', + 'fn direct_untracked_callback_is_public_api() ! {', + '\tmut marker := &IntBox{value: -1}', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(fn [mut lifetime, mut marker] () {', + '\t\tlifetime.untracked(fn [mut marker] () {', + '\t\t\tmarker.value = 91', + '\t\t}) or { panic(err) }', + '\t})!', + '\tlifetime.reclaim_all()!', + '\tassert marker.value == 91', + '\tlifetime.dispose()!', + '}', + '', + 'fn local_cleanup_and_lifetime_reclaim_do_not_double_release() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tfor i in 0 .. 1024 {', + '\t\tlifetime.frame(fn [i] () {', + '\t\t\th := fn [i] () int { return i + 1 }', + '\t\t\tassert h() == i + 1', + '\t\t})!', + '\t\tlifetime.reclaim_all()!', + '\t}', + '\tlifetime.dispose()!', + '}', + '', + 'fn lifetime_reclaim_does_not_accumulate_owned_escaping_closures() ! {', + '\t$if gcboehm ? {', + '\t\tmut cb := &CallbackBox{}', + '\t\tmut lifetime := closure.new_lifetime()', + '\t\tgc_collect()', + '\t\tstart_mb := gc_memory_use() / 1024 / 1024', + '\t\tfor n in 0 .. 20_000 {', + '\t\t\tlifetime.frame(fn [mut cb, n] () {', + '\t\t\t\tbig := []int{len: 512, init: n + index}', + '\t\t\t\tcb.cb = fn [big] () int {', + '\t\t\t\t\treturn big[0] + big[big.len - 1]', + '\t\t\t\t}', + '\t\t\t\tassert cb.cb() == 2 * n + 511', + '\t\t\t})!', + '\t\t\tlifetime.reclaim_all()!', + '\t\t\tif n % 5000 == 0 {', + '\t\t\t\tgc_collect()', + '\t\t\t}', + '\t\t}', + '\t\tgc_collect()', + '\t\tend_mb := gc_memory_use() / 1024 / 1024', + '\t\tassert end_mb <= start_mb + 24', + '\t}', + '}', + '', + 'fn external_captured_frame_callback_survives_reclaim_all() ! {', + '\tpayload := []int{len: 64, init: 200 + index}', + '\tcb := fn [payload] () {', + '\t\tassert payload[0] == 200', + '\t\tassert payload[payload.len - 1] == 263', + '\t}', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(cb)!', + '\tlifetime.reclaim_all()!', + '\tcollect_and_churn()', + '\tcb()', + '\tlifetime.dispose()!', + '}', + '', + 'fn parenthesized_frame_return_with_captured_callback() ! {', + '\tx := 37', + '\tmut lifetime := closure.new_lifetime()', + '\tdefer {', + '\t\tlifetime.dispose() or {}', + '\t}', + '\treturn (lifetime.frame(fn [x] () {', + '\t\tassert x == 37', + '\t}))', + '}', + '', + 'fn frame_end_uses_original_token_state_after_lifetime_reassign() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut original := lifetime', + '\tlifetime.frame(fn [mut lifetime] () {', + '\t\tlifetime = closure.new_lifetime()', + '\t})!', + '\tmut next := closure.new_lifetime()', + '\tnext.frame(no_capture_work)!', + '\tnext.dispose()!', + '\toriginal.dispose()!', + '}', + '', + 'fn lifetime_copies_after_dispose_report_error() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut copy := lifetime', + '\tlifetime.dispose()!', + '\tcollect_and_churn()', + '\tmut replacement := closure.new_lifetime()', + '\treplacement.frame(no_capture_work)!', + '\tmut saw_frame := false', + '\tmut saw_reclaim := false', + '\tmut saw_suspend := false', + '\tmut saw_untracked := false', + '\tmut saw_dispose := false', + '\tcopy.frame(no_capture_work) or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_frame = true', + '\t}', + '\tcopy.reclaim_all() or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_reclaim = true', + '\t}', + '\tcopy.suspend(no_capture_work) or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_suspend = true', + '\t}', + '\tcopy.untracked(no_capture_work) or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_untracked = true', + '\t}', + '\tcopy.dispose() or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_dispose = true', + '\t}', + '\tassert saw_frame', + '\tassert saw_reclaim', + '\tassert saw_suspend', + '\tassert saw_untracked', + '\tassert saw_dispose', + '\treplacement.dispose()!', + '}', + '', + 'fn run() ! {', + '\toutside_lifetime_closure_survives_lifetime_reclaim()!', + '\treclaim_keeps_requested_recent_frame()!', + '\ttwo_lifetimes_reclaim_independently()!', + '\tsuspended_persistent_callbacks_survive_reclaim_and_spawn()!', + '\tstale_reused_slot_does_not_release_suspended_newer_closure()!', + '\tdirect_untracked_callback_is_public_api()!', + '\tlocal_cleanup_and_lifetime_reclaim_do_not_double_release()!', + '\tlifetime_reclaim_does_not_accumulate_owned_escaping_closures()!', + '\texternal_captured_frame_callback_survives_reclaim_all()!', + '\tparenthesized_frame_return_with_captured_callback()!', + '\tframe_end_uses_original_token_state_after_lifetime_reassign()!', + '\tlifetime_copies_after_dispose_report_error()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') +} + +fn boehm_leak_clean_lifetime_source() string { + return [ + 'module main', + 'import builtin.closure', + '', + 'fn make_cb(value int) fn () int {', + '\treturn fn [value] () int {', + '\t\treturn value + 1', + '\t}', + '}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tfor n in 0 .. 128 {', + '\t\tlifetime.frame(fn [n] () {', + '\t\t\tcb := make_cb(n)', + '\t\t\tassert cb() == n + 1', + '\t\t})!', + '\t\tlifetime.reclaim_all()!', + '\t}', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') +} + +fn gc_none_lifetime_reclaim_memory_source() string { + return [ + 'module main', + 'import builtin.closure', + 'import runtime', + '', + 'fn used_mb() u64 {', + '\tused := runtime.used_memory() or { return 0 }', + '\treturn used / 1024 / 1024', + '}', + '', + 'fn frame_callback_context_is_reclaimed() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut start_mb := u64(0)', + '\tfor n in 0 .. 220000 {', + '\t\ta0 := n', + '\t\ta1 := n + 1', + '\t\ta2 := n + 2', + '\t\ta3 := n + 3', + '\t\ta4 := n + 4', + '\t\ta5 := n + 5', + '\t\ta6 := n + 6', + '\t\ta7 := n + 7', + '\t\ta8 := n + 8', + '\t\ta9 := n + 9', + '\t\ta10 := n + 10', + '\t\ta11 := n + 11', + '\t\ta12 := n + 12', + '\t\ta13 := n + 13', + '\t\ta14 := n + 14', + '\t\ta15 := n + 15', + '\t\tlifetime.frame(fn [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15] () {', + '\t\t\tassert a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 == 8 * a0 + 28', + '\t\t\tassert a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 == 8 * a0 + 92', + '\t\t})!', + '\t\tlifetime.reclaim_all()!', + '\t\tif n == 2048 {', + '\t\t\tstart_mb = used_mb()', + '\t\t}', + '\t}', + '\tend_mb := used_mb()', + '\tif start_mb > 0 && end_mb > start_mb + 64 {', + "\t\tpanic('closure frame callback memory grew too much')", + '\t}', + '\tlifetime.dispose()!', + '}', + '', + 'fn run() ! {', + '\tframe_callback_context_is_reclaimed()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') +} + +fn gc_none_lifetime_bookkeeping_track_heap_source(header_path string) string { + c_header_path := header_path.replace('\\', '/') + return [ + 'module main', + 'import builtin.closure', + '', + '#include "${c_header_path}"', + '', + 'fn no_capture_work() {}', + '', + 'fn exercise(rounds int) ! {', + '\tfor _ in 0 .. rounds {', + '\t\tmut lifetime := closure.new_lifetime()', + '\t\tmut copy := lifetime', + '\t\tlifetime.dispose()!', + '\t\tcopy.frame(no_capture_work) or {', + '\t\t\tcontinue', + '\t\t}', + "\t\treturn error('disposed lifetime copy was accepted')", + '\t}', + '}', + '', + 'fn main() {', + '\tmut warmup := closure.new_lifetime()', + '\twarmup.dispose() or { panic(err) }', + '\texercise(128) or { panic(err) }', + '\tmut start := u64(0)', + '\tunsafe { closure.lifetime_state_allocs(&start) }', + '\texercise(1000) or { panic(err) }', + '\tmut end := u64(0)', + '\tunsafe { closure.lifetime_state_allocs(&end) }', + '\tif end != start {', + "\t\tpanic('leaked lifetime state recycling bookkeeping: start=\${start} end=\${end}')", + '\t}', + '}', + ].join('\n') +} + +fn lazy_concurrent_lifetime_init_source() string { + return [ + 'module main', + 'import builtin.closure', + '', + '@[heap]', + 'struct IntBox {', + 'mut:', + '\tvalue int', + '}', + '', + 'fn worker(id int, ready chan bool, start chan bool) ! {', + '\tready <- true', + "\t_ := <-start or { return error('start channel closed') }", + '\tfor round in 0 .. 32 {', + '\t\tmut box := &IntBox{}', + '\t\texpected := id * 1000 + round', + '\t\tmut lifetime := closure.new_lifetime()', + '\t\tlifetime.frame(fn [mut box, expected] () {', + '\t\t\tmake_value := fn [expected] () int { return expected }', + '\t\t\tbox.value = make_value()', + '\t\t})!', + '\t\tassert box.value == expected', + '\t\tlifetime.dispose()!', + '\t}', + '}', + '', + 'fn run() ! {', + '\tthread_count := 16', + '\tready := chan bool{cap: thread_count}', + '\tstart := chan bool{cap: thread_count}', + '\tmut threads := []thread !{cap: thread_count}', + '\tfor i in 0 .. thread_count {', + '\t\tthreads << spawn worker(i, ready, start)', + '\t}', + '\tfor _ in 0 .. thread_count {', + "\t\t_ := <-ready or { return error('ready channel closed') }", + '\t}', + '\tfor _ in 0 .. thread_count {', + '\t\tstart <- true', + '\t}', + '\tthreads.wait()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') +} + +fn assert_misuse_program_passes(tmp_dir string, name string, source string) { + source_path := write_program(tmp_dir, name, source) + res := os.execute('${os.quoted_path(vexe)} run ${os.quoted_path(source_path)}') + assert res.exit_code == 0, res.output +} + +fn test_lifetime_public_api_without_captured_closure() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_no_capture_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source := no_captured_closure_lifetime_source() + c_res := c_output_for_program(tmp_dir, 'no_captured_lifetime_codegen', source) + assert c_res.exit_code == 0, c_res.output + assert !c_res.output.contains('builtin__closure__closure_create') + assert !c_res.output.contains('_V_closure_main__') + for mode in ['boehm', 'none'] { + res := run_program_with_gc(tmp_dir, 'no_captured_lifetime', source, mode) + assert res.exit_code == 0, res.output + } + assert_boehm_leak_compile_or_missing_lib(tmp_dir, 'no_captured_lifetime', source) +} + +fn test_closure_lifetime_runtime_api_contract() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_runtime_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source := closure_lifetime_runtime_source() + for mode in ['boehm', 'none'] { + res := run_program_with_gc(tmp_dir, 'closure_lifetime_runtime', source, mode) + assert res.exit_code == 0, res.output + } + assert_boehm_leak_compile_or_missing_lib(tmp_dir, 'closure_lifetime_runtime', source) +} + +fn test_closure_lifetime_boehm_leak_runtime_without_persistent_callbacks() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_boehm_leak_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + assert_boehm_leak_runtime_or_compile_only(tmp_dir, 'closure_lifetime_boehm_leak_clean', + boehm_leak_clean_lifetime_source()) +} + +fn test_closure_lifetime_gc_none_does_not_leak_frame_callback_or_bookkeeping() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_gc_none_memory_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + res := run_program_with_gc(tmp_dir, 'closure_lifetime_gc_none_memory', + gc_none_lifetime_reclaim_memory_source(), 'none') + assert res.exit_code == 0, res.output +} + +fn test_closure_lifetime_gc_none_reuses_disposed_state_bookkeeping() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_gc_none_state_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + header_path := os.join_path(tmp_dir, 'track_heap_hooks.h') + os.write_file(header_path, [ + 'void vheap_alloc(void* p, unsigned long long n) { (void)p; (void)n; }', + 'void vheap_free(void* p) { (void)p; }', + ].join('\n')) or { panic(err) } + res := run_program_with_track_heap(tmp_dir, 'closure_lifetime_gc_none_state', + gc_none_lifetime_bookkeeping_track_heap_source(header_path)) + assert res.exit_code == 0, res.output +} + +fn test_closure_lifetime_dispose_recycles_state_for_later_lifetimes() { + source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/closure.c.v') + source := os.read_file(source_path) or { panic(err) } + start := source.index('pub fn (mut lifetime Lifetime) dispose() !') or { + panic('missing dispose helper') + } + end := source[start..].index('pub fn (mut lifetime Lifetime) suspend') or { + panic('missing dispose helper end') + } + helper := source[start..start + end] + assert helper.contains('closure_lifetime_recycle_state_no_lock(mut state)') + assert helper.contains('lifetime.state = unsafe { nil }') + assert helper.contains('lifetime.disposed = true') + assert !helper.contains('free(state)') +} + +fn test_closure_lifetime_freestanding_no_std_object_compile() { + $if windows { + return + } + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_freestanding_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source := [ + 'module main', + 'import builtin.closure', + '', + 'fn main() {', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.dispose() or { panic(err) }', + '}', + ].join('\n') + res := compile_freestanding_object(tmp_dir, 'closure_lifetime_freestanding', source) + assert res.exit_code == 0, res.output +} + +fn test_closure_lifetime_lazy_concurrent_runtime_init() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_lazy_init_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source := lazy_concurrent_lifetime_init_source() + for mode in ['none', 'boehm'] { + res := run_program_with_gc(tmp_dir, 'closure_lifetime_lazy_init', source, mode) + assert res.exit_code == 0, res.output + } +} + +fn test_closure_lifetime_reclaim_helper_reuses_buffers_without_clone() { + source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/closure.c.v') + source := os.read_file(source_path) or { panic(err) } + start := source.index('fn closure_lifetime_reclaim_no_lock') or { + panic('missing reclaim helper') + } + end := source[start..].index('fn closure_ensure_initialized') or { + panic('missing reclaim helper end') + } + helper := source[start..start + end] + assert !helper.contains('.clone()') + assert helper.contains('delete_many(0, reclaim_count)') + assert helper.contains('delete_many(0, cutoff)') +} + +fn test_closure_lifetime_dispose_frees_bookkeeping_buffers_but_keeps_state() { + source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/closure.c.v') + source := os.read_file(source_path) or { panic(err) } + start := source.index('fn closure_lifetime_recycle_state_no_lock') or { + panic('missing recycle helper') + } + end := source[start..].index('fn closure_lifetime_error') or { + panic('missing recycle helper end') + } + helper := source[start..start + end] + assert helper.contains('state.disposed = true') + assert helper.contains('state.records.free()') + assert helper.contains('state.frames.free()') + assert helper.contains('state.records = []ClosureLifetimeRecord{}') + assert helper.contains('state.frames = []ClosureLifetimeFrame{}') + assert helper.contains('state.next_free = g_closure.free_lifetime_states') + assert helper.contains('g_closure.free_lifetime_states = state') + assert !helper.contains('free(state)') +} + +fn test_closure_lifetime_once_headers_keep_helper_internal() { + for header_name in ['closure_once_nix.h', 'closure_once_windows.h'] { + source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/${header_name}') + source := os.read_file(source_path) or { panic(err) } + assert source.contains('V_CLOSURE_STATIC_INLINE void v_closure_init_once') + assert !source.contains('\nvoid v_closure_init_once') + } +} + +fn test_closure_lifetime_codegen_emits_single_local_destroy() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_codegen_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source := [ + 'module main', + 'import builtin.closure', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(fn () {', + '\t\tvalue := 7', + '\t\th := fn [value] () int { return value }', + '\t\tassert h() == 7', + '\t})!', + '\tlifetime.reclaim_all()!', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') + res := c_output_for_program(tmp_dir, 'closure_lifetime_single_destroy', source) + assert res.exit_code == 0, res.output + assert count_occurrences(res.output, 'builtin__closure__closure_try_destroy((voidptr)h);') == 1 +} + +fn test_closure_lifetime_inline_callback_codegen_cleanup() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_inline_codegen_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source := [ + 'module main', + 'import builtin.closure', + '', + 'struct View {', + '\tvalue int', + '}', + '', + 'fn (view View) draw() {', + '\tassert view.value == 41', + '}', + '', + 'fn frame_bound_method() ! {', + '\tview := View{value: 41}', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(view.draw)!', + '\tlifetime.dispose()!', + '}', + '', + 'fn outside_suspend_untracked() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tvalue := 7', + '\tlifetime.suspend(fn [value] () {', + '\t\tassert value == 7', + '\t})!', + '\tlifetime.untracked(fn [value] () {', + '\t\tassert value == 7', + '\t})!', + '\tlifetime.dispose()!', + '}', + '', + 'fn parenthesized_inline_frame() ! {', + '\tvalue := 12', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame((fn [value] () {', + '\t\tassert value == 12', + '\t}))!', + '\tlifetime.dispose()!', + '}', + '', + 'fn tail_return_frame(value int) ! {', + '\tmut lifetime := closure.new_lifetime()', + '\treturn lifetime.frame(fn [value] () {', + '\t\tassert value == 9', + '\t})', + '}', + '', + 'fn main() {', + '\tframe_bound_method() or { panic(err) }', + '\toutside_suspend_untracked() or { panic(err) }', + '\tparenthesized_inline_frame() or { panic(err) }', + '\ttail_return_frame(9) or { panic(err) }', + '}', + ].join('\n') + res := c_output_for_program(tmp_dir, 'closure_lifetime_inline_cleanup', source) + assert res.exit_code == 0, res.output + destroy_count := count_occurrences(res.output, + 'builtin__closure__closure_try_destroy((voidptr)') + assert destroy_count == 5, res.output + parenthesized_start := res.output.index('VV_LOC _result_void main__parenthesized_inline_frame(void) {') or { + panic(res.output) + } + parenthesized_end := if parenthesized_start + 1200 < res.output.len { + parenthesized_start + 1200 + } else { + res.output.len + } + parenthesized_fn := res.output[parenthesized_start..parenthesized_end] + frame_pos := parenthesized_fn.index('builtin__closure__Lifetime_frame') or { + panic(parenthesized_fn) + } + destroy_pos := parenthesized_fn.index('builtin__closure__closure_try_destroy((voidptr)') or { + panic(parenthesized_fn) + } + assert destroy_pos > frame_pos, parenthesized_fn + assert !res.output.contains('return builtin__closure__Lifetime_frame(') +} + +fn test_closure_lifetime_borrowed_callback_result_is_not_destroyed() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_borrowed_callback_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source := [ + 'module main', + 'import builtin.closure', + '', + 'fn identity(cb fn ()) fn () {', + '\treturn cb', + '}', + '', + 'fn run() ! {', + '\tpayload := []int{len: 32, init: index}', + '\tstored := fn [payload] () {', + '\t\tassert payload[0] == 0', + '\t\tassert payload[payload.len - 1] == 31', + '\t}', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.frame(identity(stored))!', + '\tlifetime.dispose()!', + '\tstored()', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n') + c_res := c_output_for_program(tmp_dir, 'closure_lifetime_borrowed_callback_cgen', source) + assert c_res.exit_code == 0, c_res.output + run_start := c_res.output.index('VV_LOC _result_void main__run(void) {') or { + panic(c_res.output) + } + run_end := c_res.output.index_after('VV_LOC void main__main(void) {', run_start) or { + panic(c_res.output) + } + run_fn := c_res.output[run_start..run_end] + assert !run_fn.contains('builtin__closure__closure_try_destroy((voidptr)'), run_fn + res := run_program_with_gc(tmp_dir, 'closure_lifetime_borrowed_callback', source, 'none') + assert res.exit_code == 0, res.output +} + +fn test_lifetime_rejects_misuse_with_errors() { + $if gcboehm_leak ? { + return + } + tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_api_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + assert_misuse_program_passes(tmp_dir, 'wrong_thread_reclaim', [ + 'import builtin.closure', + '', + 'fn reclaim_from_thread(lifetime closure.Lifetime) ! {', + '\tmut local := lifetime', + '\tlocal.reclaim_all()!', + '}', + '', + 'fn main() {', + '\tlifetime := closure.new_lifetime()', + '\tth := spawn reclaim_from_thread(lifetime)', + '\tth.wait() or {', + "\t\tassert err.msg().contains('different thread')", + '\t\treturn', + '\t}', + '\tassert false', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'wrong_thread_dispose', [ + 'import builtin.closure', + '', + 'fn dispose_from_thread(lifetime closure.Lifetime) ! {', + '\tmut local := lifetime', + '\tlocal.dispose()!', + '}', + '', + 'fn main() {', + '\tlifetime := closure.new_lifetime()', + '\tth := spawn dispose_from_thread(lifetime)', + '\tth.wait() or {', + "\t\tassert err.msg().contains('different thread')", + '\t\treturn', + '\t}', + '\tassert false', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'wrong_thread_suspend', [ + 'import builtin.closure', + '', + 'fn no_capture_work() {}', + '', + 'fn suspend_from_thread(lifetime closure.Lifetime) ! {', + '\tmut local := lifetime', + '\tlocal.suspend(no_capture_work)!', + '}', + '', + 'fn main() {', + '\tlifetime := closure.new_lifetime()', + '\tth := spawn suspend_from_thread(lifetime)', + '\tth.wait() or {', + "\t\tassert err.msg().contains('different thread')", + '\t\treturn', + '\t}', + '\tassert false', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'wrong_thread_frame', [ + 'import builtin.closure', + '', + 'fn no_capture_work() {}', + '', + 'fn frame_from_thread(lifetime closure.Lifetime) ! {', + '\tmut local := lifetime', + '\tlocal.frame(no_capture_work)!', + '}', + '', + 'fn main() {', + '\tlifetime := closure.new_lifetime()', + '\tth := spawn frame_from_thread(lifetime)', + '\tth.wait() or {', + "\t\tassert err.msg().contains('different thread')", + '\t\treturn', + '\t}', + '\tassert false', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'nested_same_lifetime_frame', [ + 'import builtin.closure', + '', + '@[heap]', + 'struct BoolBox {', + 'mut:', + '\tvalue bool', + '}', + '', + 'fn no_capture_work() {}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut saw := &BoolBox{}', + '\tlifetime.frame(fn [mut lifetime, mut saw] () {', + '\t\tlifetime.frame(no_capture_work) or {', + "\t\t\tassert err.msg().contains('active') || err.msg().contains('nested')", + '\t\t\tsaw.value = true', + '\t\t\treturn', + '\t\t}', + '\t\tassert false', + '\t})!', + '\tassert saw.value', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'second_active_lifetime', [ + 'import builtin.closure', + '', + '@[heap]', + 'struct BoolBox {', + 'mut:', + '\tvalue bool', + '}', + '', + 'fn no_capture_work() {}', + '', + 'fn run() ! {', + '\tmut first := closure.new_lifetime()', + '\tmut second := closure.new_lifetime()', + '\tmut saw := &BoolBox{}', + '\tfirst.frame(fn [mut second, mut saw] () {', + '\t\tsecond.frame(no_capture_work) or {', + "\t\t\tassert err.msg().contains('active') || err.msg().contains('another')", + '\t\t\tsaw.value = true', + '\t\t\treturn', + '\t\t}', + '\t\tassert false', + '\t})!', + '\tassert saw.value', + '\tfirst.dispose()!', + '\tsecond.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'second_suspend_inside_first_frame', [ + 'import builtin.closure', + '', + '@[heap]', + 'struct BoolBox {', + 'mut:', + '\tvalue bool', + '}', + '', + 'fn no_capture_work() {}', + '', + 'fn run() ! {', + '\tmut first := closure.new_lifetime()', + '\tmut second := closure.new_lifetime()', + '\tmut saw := &BoolBox{}', + '\tfirst.frame(fn [mut second, mut saw] () {', + '\t\tsecond.suspend(no_capture_work) or {', + "\t\t\tassert err.msg().contains('active') || err.msg().contains('another')", + '\t\t\tsaw.value = true', + '\t\t\treturn', + '\t\t}', + '\t\tassert false', + '\t})!', + '\tassert saw.value', + '\tfirst.dispose()!', + '\tsecond.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'reclaim_dispose_while_active', [ + 'import builtin.closure', + '', + '@[heap]', + 'struct BoolBox {', + 'mut:', + '\tvalue bool', + '}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut saw_reclaim := &BoolBox{}', + '\tmut saw_dispose := &BoolBox{}', + '\tlifetime.frame(fn [mut lifetime, mut saw_reclaim, mut saw_dispose] () {', + '\t\tlifetime.reclaim_all() or {', + "\t\t\tassert err.msg().contains('active')", + '\t\t\tsaw_reclaim.value = true', + '\t\t}', + '\t\tlifetime.dispose() or {', + "\t\t\tassert err.msg().contains('active')", + '\t\t\tsaw_dispose.value = true', + '\t\t\treturn', + '\t\t}', + '\t\tassert false', + '\t})!', + '\tassert saw_reclaim.value', + '\tassert saw_dispose.value', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'dispose_while_suspended', [ + 'import builtin.closure', + '', + '@[heap]', + 'struct BoolBox {', + 'mut:', + '\tvalue bool', + '}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut saw := &BoolBox{}', + '\tlifetime.suspend(fn [mut lifetime, mut saw] () {', + '\t\tlifetime.dispose() or {', + "\t\t\tassert err.msg().contains('suspend')", + '\t\t\tsaw.value = true', + '\t\t\treturn', + '\t\t}', + '\t\tassert false', + '\t})!', + '\tassert saw.value', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'dispose_while_untracked', [ + 'import builtin.closure', + '', + '@[heap]', + 'struct BoolBox {', + 'mut:', + '\tvalue bool', + '}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut saw := &BoolBox{}', + '\tlifetime.untracked(fn [mut lifetime, mut saw] () {', + '\t\tlifetime.dispose() or {', + "\t\t\tassert err.msg().contains('suspend')", + '\t\t\tsaw.value = true', + '\t\t\treturn', + '\t\t}', + '\t\tassert false', + '\t})!', + '\tassert saw.value', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'frame_while_suspended', [ + 'import builtin.closure', + '', + '@[heap]', + 'struct BoolBox {', + 'mut:', + '\tvalue bool', + '}', + '', + 'fn no_capture_work() {}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tmut saw := &BoolBox{}', + '\tlifetime.suspend(fn [mut lifetime, mut saw] () {', + '\t\tlifetime.frame(no_capture_work) or {', + "\t\t\tassert err.msg().contains('suspend')", + '\t\t\tsaw.value = true', + '\t\t\treturn', + '\t\t}', + '\t\tassert false', + '\t})!', + '\tassert saw.value', + '\tlifetime.dispose()!', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) + assert_misuse_program_passes(tmp_dir, 'use_after_dispose', [ + 'import builtin.closure', + '', + 'fn no_capture_work() {}', + '', + 'fn run() ! {', + '\tmut lifetime := closure.new_lifetime()', + '\tlifetime.dispose()!', + '\tmut saw_frame := false', + '\tmut saw_reclaim := false', + '\tmut saw_suspend := false', + '\tmut saw_untracked := false', + '\tmut saw_dispose := false', + '\tlifetime.frame(no_capture_work) or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_frame = true', + '\t}', + '\tlifetime.reclaim_all() or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_reclaim = true', + '\t}', + '\tlifetime.suspend(no_capture_work) or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_suspend = true', + '\t}', + '\tlifetime.untracked(no_capture_work) or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_untracked = true', + '\t}', + '\tlifetime.dispose() or {', + "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')", + '\t\tsaw_dispose = true', + '\t}', + '\tassert saw_frame', + '\tassert saw_reclaim', + '\tassert saw_suspend', + '\tassert saw_untracked', + '\tassert saw_dispose', + '}', + '', + 'fn main() {', + '\trun() or { panic(err) }', + '}', + ].join('\n')) +}