vxx2 / vlib / v / tests / fns / closure_lifetime_api_test.v
1287 lines · 1251 sloc · 37.03 KB · 8687a933ef1596c60e58ccf41c8c8d5629b67f0b
Raw
1import os
2
3const vexe = @VEXE
4
5fn testsuite_begin() {
6 $if rv64 {
7 skip_test('closure lifetime API tests are too slow under riscv64 emulation')
8 }
9}
10
11@[noreturn]
12fn skip_test(reason string) {
13 println('skipping test, because ${reason} .')
14 exit(0)
15}
16
17fn missing_boehm_lib(output string) bool {
18 mentions_gc := output.contains('libgc') || output.contains('-lgc')
19 || output.contains("library 'gc'") || output.contains('bdw-gc')
20 return mentions_gc && (output.contains('was not found')
21 || output.contains('cannot find') || output.contains('not found')
22 || output.contains('No such file'))
23}
24
25// assert_run_succeeds_or_missing_boehm runs the program with the given gc mode and
26// asserts success, but skips gracefully when a `boehm`/`boehm_leak` build fails only
27// because the Boehm GC library is unavailable (e.g. musl images without musl libgc).
28fn assert_run_succeeds_or_missing_boehm(tmp_dir string, name string, source string, mode string) {
29 res := run_program_with_gc(tmp_dir, name, source, mode)
30 if mode != 'none' && res.exit_code != 0 && missing_boehm_lib(res.output) {
31 eprintln('skipping ${mode} run for ${name}: missing libgc')
32 return
33 }
34 assert res.exit_code == 0, res.output
35}
36
37fn count_occurrences(haystack string, needle string) int {
38 mut pos := 0
39 mut count := 0
40 for {
41 idx := haystack[pos..].index(needle) or { break }
42 count++
43 pos += idx + needle.len
44 }
45 return count
46}
47
48fn write_program(tmp_dir string, name string, source string) string {
49 source_path := os.join_path(tmp_dir, '${name}.v')
50 os.write_file(source_path, source) or { panic(err) }
51 return source_path
52}
53
54fn run_program_with_gc(tmp_dir string, name string, source string, mode string) os.Result {
55 source_path := write_program(tmp_dir, '${name}_${mode}', source)
56 return os.execute('${os.quoted_path(vexe)} -gc ${mode} run ${os.quoted_path(source_path)}')
57}
58
59fn compile_program_with_gc(tmp_dir string, name string, source string, mode string) os.Result {
60 source_path := write_program(tmp_dir, '${name}_${mode}', source)
61 binary_path := os.join_path(tmp_dir, '${name}_${mode}')
62 return os.execute('${os.quoted_path(vexe)} -gc ${mode} -o ${os.quoted_path(binary_path)} ${os.quoted_path(source_path)}')
63}
64
65fn compile_freestanding_object(tmp_dir string, name string, source string) os.Result {
66 source_path := write_program(tmp_dir, name, source)
67 object_path := os.join_path(tmp_dir, '${name}.o')
68 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)}')
69}
70
71fn run_program_with_track_heap(tmp_dir string, name string, source string) os.Result {
72 source_path := write_program(tmp_dir, name, source)
73 return os.execute('${os.quoted_path(vexe)} -gc none -d track_heap run ${os.quoted_path(source_path)}')
74}
75
76fn c_output_for_program(tmp_dir string, name string, source string) os.Result {
77 source_path := write_program(tmp_dir, name, source)
78 return os.execute('${os.quoted_path(vexe)} -o - ${os.quoted_path(source_path)}')
79}
80
81fn assert_boehm_leak_compile_or_missing_lib(tmp_dir string, name string, source string) {
82 res := compile_program_with_gc(tmp_dir, name, source, 'boehm_leak')
83 if res.exit_code != 0 && missing_boehm_lib(res.output) {
84 eprintln('skipping boehm_leak compile for ${name}: missing libgc')
85 return
86 }
87 assert res.exit_code == 0, res.output
88}
89
90fn assert_boehm_leak_runtime_or_compile_only(tmp_dir string, name string, source string) {
91 res := run_program_with_gc(tmp_dir, name, source, 'boehm_leak')
92 if res.exit_code == 0 {
93 return
94 }
95 if missing_boehm_lib(res.output) {
96 eprintln('skipping boehm_leak runtime for ${name}: missing libgc')
97 return
98 }
99 if res.output.contains('leaked objects') || res.output.contains('Found ') {
100 eprintln('boehm_leak runtime reported runtime allocations for ${name}; keeping compile-only coverage')
101 assert_boehm_leak_compile_or_missing_lib(tmp_dir, '${name}_compile_only', source)
102 return
103 }
104 assert false, res.output
105}
106
107fn no_captured_closure_lifetime_source() string {
108 return [
109 'module main',
110 'import builtin.closure',
111 '',
112 'fn no_capture_work() {',
113 '\tassert true',
114 '}',
115 '',
116 'fn run() ! {',
117 '\tmut lifetime := closure.new_lifetime()',
118 '\tlifetime.frame(no_capture_work)!',
119 '\tlifetime.suspend(no_capture_work)!',
120 '\tlifetime.untracked(no_capture_work)!',
121 '\tlifetime.reclaim_all()!',
122 '\tlifetime.dispose()!',
123 '}',
124 '',
125 'fn main() {',
126 '\trun() or { panic(err) }',
127 '}',
128 ].join('\n')
129}
130
131fn closure_lifetime_runtime_source() string {
132 return [
133 'module main',
134 'import builtin.closure',
135 '',
136 'fn no_capture_work() {}',
137 '',
138 '@[heap]',
139 'struct CallbackBox {',
140 'mut:',
141 '\tcb fn () int = fn () int { return -1 }',
142 '}',
143 '',
144 '@[heap]',
145 'struct IntBox {',
146 'mut:',
147 '\tvalue int',
148 '}',
149 '',
150 'fn make_cb(value int) fn () int {',
151 '\tpayload := []int{len: 64, init: value + index}',
152 '\treturn fn [payload] () int {',
153 '\t\treturn payload[0] + payload[payload.len - 1]',
154 '\t}',
155 '}',
156 '',
157 'fn call_cb(cb fn () int) int {',
158 '\treturn cb()',
159 '}',
160 '',
161 'fn collect_and_churn() {',
162 '\tgc_collect()',
163 '\tfor _ in 0 .. 512 {',
164 '\t\tunsafe {',
165 '\t\t\tp := malloc(32)',
166 '\t\t\tvmemset(p, 0x55, 32)',
167 '\t\t}',
168 '\t}',
169 '\tgc_collect()',
170 '}',
171 '',
172 'fn outside_lifetime_closure_survives_lifetime_reclaim() ! {',
173 '\toutside := make_cb(10)',
174 '\tmut lifetime := closure.new_lifetime()',
175 '\tlifetime.frame(fn () {',
176 '\t\tinside := make_cb(100)',
177 '\t\tassert inside() == 263',
178 '\t})!',
179 '\tlifetime.reclaim_all()!',
180 '\tcollect_and_churn()',
181 '\tassert outside() == 83',
182 '\tlifetime.dispose()!',
183 '}',
184 '',
185 'fn reclaim_keeps_requested_recent_frame() ! {',
186 '\tmut first := &CallbackBox{}',
187 '\tmut second := &CallbackBox{}',
188 '\tmut lifetime := closure.new_lifetime()',
189 '\tlifetime.frame(fn [mut first] () {',
190 '\t\tfirst.cb = make_cb(1)',
191 '\t\tassert first.cb() == 65',
192 '\t})!',
193 '\tlifetime.frame(fn [mut second] () {',
194 '\t\tsecond.cb = make_cb(2)',
195 '\t\tassert second.cb() == 67',
196 '\t})!',
197 '\tlifetime.reclaim(1)!',
198 '\tcollect_and_churn()',
199 '\tassert second.cb() == 67',
200 '\tlifetime.reclaim_all()!',
201 '\tlifetime.dispose()!',
202 '}',
203 '',
204 'fn two_lifetimes_reclaim_independently() ! {',
205 '\tmut first := &CallbackBox{}',
206 '\tmut second := &CallbackBox{}',
207 '\tmut lifetime_a := closure.new_lifetime()',
208 '\tmut lifetime_b := closure.new_lifetime()',
209 '\tlifetime_a.frame(fn [mut first] () {',
210 '\t\tfirst.cb = make_cb(20)',
211 '\t\tassert first.cb() == 103',
212 '\t})!',
213 '\tlifetime_b.frame(fn [mut second] () {',
214 '\t\tsecond.cb = make_cb(30)',
215 '\t\tassert second.cb() == 123',
216 '\t})!',
217 '\tlifetime_a.reclaim_all()!',
218 '\tcollect_and_churn()',
219 '\tassert second.cb() == 123',
220 '\tlifetime_b.reclaim_all()!',
221 '\tlifetime_a.dispose()!',
222 '\tlifetime_b.dispose()!',
223 '}',
224 '',
225 'fn suspended_persistent_callbacks_survive_reclaim_and_spawn() ! {',
226 '\tmut persisted := &CallbackBox{}',
227 '\tmut spawned_value := &IntBox{value: -1}',
228 '\tmut lifetime := closure.new_lifetime()',
229 '\tlifetime.frame(fn [mut lifetime, mut persisted, mut spawned_value] () {',
230 '\t\tshort := make_cb(5)',
231 '\t\tassert short() == 73',
232 '\t\tlifetime.suspend(fn [mut persisted, mut spawned_value] () {',
233 '\t\t\tpersisted.cb = make_cb(40)',
234 '\t\t\tth := spawn call_cb(persisted.cb)',
235 '\t\t\tspawned_value.value = th.wait()',
236 '\t\t}) or { panic(err) }',
237 '\t})!',
238 '\tlifetime.reclaim_all()!',
239 '\tcollect_and_churn()',
240 '\tassert persisted.cb() == 143',
241 '\tassert spawned_value.value == 143',
242 '\tlifetime.dispose()!',
243 '}',
244 '',
245 'fn stale_reused_slot_does_not_release_suspended_newer_closure() ! {',
246 '\tmut survivor := &CallbackBox{}',
247 '\tmut lifetime := closure.new_lifetime()',
248 '\tlifetime.frame(fn [mut lifetime, mut survivor] () {',
249 '\t\tfor i in 0 .. 96 {',
250 '\t\t\th := fn [i] () int { return i }',
251 '\t\t\tassert h() == i',
252 '\t\t}',
253 '\t\tlifetime.suspend(fn [mut survivor] () {',
254 '\t\t\tsurvivor.cb = make_cb(70)',
255 '\t\t\tassert survivor.cb() == 203',
256 '\t\t}) or { panic(err) }',
257 '\t})!',
258 '\tlifetime.reclaim_all()!',
259 '\tcollect_and_churn()',
260 '\tassert survivor.cb() == 203',
261 '\tlifetime.dispose()!',
262 '}',
263 '',
264 'fn direct_untracked_callback_is_public_api() ! {',
265 '\tmut marker := &IntBox{value: -1}',
266 '\tmut lifetime := closure.new_lifetime()',
267 '\tlifetime.frame(fn [mut lifetime, mut marker] () {',
268 '\t\tlifetime.untracked(fn [mut marker] () {',
269 '\t\t\tmarker.value = 91',
270 '\t\t}) or { panic(err) }',
271 '\t})!',
272 '\tlifetime.reclaim_all()!',
273 '\tassert marker.value == 91',
274 '\tlifetime.dispose()!',
275 '}',
276 '',
277 'fn local_cleanup_and_lifetime_reclaim_do_not_double_release() ! {',
278 '\tmut lifetime := closure.new_lifetime()',
279 '\tfor i in 0 .. 1024 {',
280 '\t\tlifetime.frame(fn [i] () {',
281 '\t\t\th := fn [i] () int { return i + 1 }',
282 '\t\t\tassert h() == i + 1',
283 '\t\t})!',
284 '\t\tlifetime.reclaim_all()!',
285 '\t}',
286 '\tlifetime.dispose()!',
287 '}',
288 '',
289 'fn lifetime_reclaim_does_not_accumulate_owned_escaping_closures() ! {',
290 '\t$if gcboehm ? {',
291 '\t\tmut cb := &CallbackBox{}',
292 '\t\tmut lifetime := closure.new_lifetime()',
293 '\t\tgc_collect()',
294 '\t\tstart_mb := gc_memory_use() / 1024 / 1024',
295 '\t\tfor n in 0 .. 20_000 {',
296 '\t\t\tlifetime.frame(fn [mut cb, n] () {',
297 '\t\t\t\tbig := []int{len: 512, init: n + index}',
298 '\t\t\t\tcb.cb = fn [big] () int {',
299 '\t\t\t\t\treturn big[0] + big[big.len - 1]',
300 '\t\t\t\t}',
301 '\t\t\t\tassert cb.cb() == 2 * n + 511',
302 '\t\t\t})!',
303 '\t\t\tlifetime.reclaim_all()!',
304 '\t\t\tif n % 5000 == 0 {',
305 '\t\t\t\tgc_collect()',
306 '\t\t\t}',
307 '\t\t}',
308 '\t\tgc_collect()',
309 '\t\tend_mb := gc_memory_use() / 1024 / 1024',
310 '\t\tassert end_mb <= start_mb + 24',
311 '\t}',
312 '}',
313 '',
314 'fn external_captured_frame_callback_survives_reclaim_all() ! {',
315 '\tpayload := []int{len: 64, init: 200 + index}',
316 '\tcb := fn [payload] () {',
317 '\t\tassert payload[0] == 200',
318 '\t\tassert payload[payload.len - 1] == 263',
319 '\t}',
320 '\tmut lifetime := closure.new_lifetime()',
321 '\tlifetime.frame(cb)!',
322 '\tlifetime.reclaim_all()!',
323 '\tcollect_and_churn()',
324 '\tcb()',
325 '\tlifetime.dispose()!',
326 '}',
327 '',
328 'fn parenthesized_frame_return_with_captured_callback() ! {',
329 '\tx := 37',
330 '\tmut lifetime := closure.new_lifetime()',
331 '\tdefer {',
332 '\t\tlifetime.dispose() or {}',
333 '\t}',
334 '\treturn (lifetime.frame(fn [x] () {',
335 '\t\tassert x == 37',
336 '\t}))',
337 '}',
338 '',
339 'fn frame_end_uses_original_token_state_after_lifetime_reassign() ! {',
340 '\tmut lifetime := closure.new_lifetime()',
341 '\tmut original := lifetime',
342 '\tlifetime.frame(fn [mut lifetime] () {',
343 '\t\tlifetime = closure.new_lifetime()',
344 '\t})!',
345 '\tmut next := closure.new_lifetime()',
346 '\tnext.frame(no_capture_work)!',
347 '\tnext.dispose()!',
348 '\toriginal.dispose()!',
349 '}',
350 '',
351 'fn lifetime_copies_after_dispose_report_error() ! {',
352 '\tmut lifetime := closure.new_lifetime()',
353 '\tmut copy := lifetime',
354 '\tlifetime.dispose()!',
355 '\tcollect_and_churn()',
356 '\tmut replacement := closure.new_lifetime()',
357 '\treplacement.frame(no_capture_work)!',
358 '\tmut saw_frame := false',
359 '\tmut saw_reclaim := false',
360 '\tmut saw_suspend := false',
361 '\tmut saw_untracked := false',
362 '\tmut saw_dispose := false',
363 '\tcopy.frame(no_capture_work) or {',
364 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
365 '\t\tsaw_frame = true',
366 '\t}',
367 '\tcopy.reclaim_all() or {',
368 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
369 '\t\tsaw_reclaim = true',
370 '\t}',
371 '\tcopy.suspend(no_capture_work) or {',
372 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
373 '\t\tsaw_suspend = true',
374 '\t}',
375 '\tcopy.untracked(no_capture_work) or {',
376 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
377 '\t\tsaw_untracked = true',
378 '\t}',
379 '\tcopy.dispose() or {',
380 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
381 '\t\tsaw_dispose = true',
382 '\t}',
383 '\tassert saw_frame',
384 '\tassert saw_reclaim',
385 '\tassert saw_suspend',
386 '\tassert saw_untracked',
387 '\tassert saw_dispose',
388 '\treplacement.dispose()!',
389 '}',
390 '',
391 'fn run() ! {',
392 '\toutside_lifetime_closure_survives_lifetime_reclaim()!',
393 '\treclaim_keeps_requested_recent_frame()!',
394 '\ttwo_lifetimes_reclaim_independently()!',
395 '\tsuspended_persistent_callbacks_survive_reclaim_and_spawn()!',
396 '\tstale_reused_slot_does_not_release_suspended_newer_closure()!',
397 '\tdirect_untracked_callback_is_public_api()!',
398 '\tlocal_cleanup_and_lifetime_reclaim_do_not_double_release()!',
399 '\tlifetime_reclaim_does_not_accumulate_owned_escaping_closures()!',
400 '\texternal_captured_frame_callback_survives_reclaim_all()!',
401 '\tparenthesized_frame_return_with_captured_callback()!',
402 '\tframe_end_uses_original_token_state_after_lifetime_reassign()!',
403 '\tlifetime_copies_after_dispose_report_error()!',
404 '}',
405 '',
406 'fn main() {',
407 '\trun() or { panic(err) }',
408 '}',
409 ].join('\n')
410}
411
412fn boehm_leak_clean_lifetime_source() string {
413 return [
414 'module main',
415 'import builtin.closure',
416 '',
417 'fn make_cb(value int) fn () int {',
418 '\treturn fn [value] () int {',
419 '\t\treturn value + 1',
420 '\t}',
421 '}',
422 '',
423 'fn run() ! {',
424 '\tmut lifetime := closure.new_lifetime()',
425 '\tfor n in 0 .. 128 {',
426 '\t\tlifetime.frame(fn [n] () {',
427 '\t\t\tcb := make_cb(n)',
428 '\t\t\tassert cb() == n + 1',
429 '\t\t})!',
430 '\t\tlifetime.reclaim_all()!',
431 '\t}',
432 '\tlifetime.dispose()!',
433 '}',
434 '',
435 'fn main() {',
436 '\trun() or { panic(err) }',
437 '}',
438 ].join('\n')
439}
440
441fn gc_none_lifetime_reclaim_memory_source() string {
442 return [
443 'module main',
444 'import builtin.closure',
445 'import runtime',
446 '',
447 'fn used_mb() u64 {',
448 '\tused := runtime.used_memory() or { return 0 }',
449 '\treturn used / 1024 / 1024',
450 '}',
451 '',
452 'fn frame_callback_context_is_reclaimed() ! {',
453 '\tmut lifetime := closure.new_lifetime()',
454 '\tmut start_mb := u64(0)',
455 '\tfor n in 0 .. 220000 {',
456 '\t\ta0 := n',
457 '\t\ta1 := n + 1',
458 '\t\ta2 := n + 2',
459 '\t\ta3 := n + 3',
460 '\t\ta4 := n + 4',
461 '\t\ta5 := n + 5',
462 '\t\ta6 := n + 6',
463 '\t\ta7 := n + 7',
464 '\t\ta8 := n + 8',
465 '\t\ta9 := n + 9',
466 '\t\ta10 := n + 10',
467 '\t\ta11 := n + 11',
468 '\t\ta12 := n + 12',
469 '\t\ta13 := n + 13',
470 '\t\ta14 := n + 14',
471 '\t\ta15 := n + 15',
472 '\t\tlifetime.frame(fn [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15] () {',
473 '\t\t\tassert a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 == 8 * a0 + 28',
474 '\t\t\tassert a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 == 8 * a0 + 92',
475 '\t\t})!',
476 '\t\tlifetime.reclaim_all()!',
477 '\t\tif n == 2048 {',
478 '\t\t\tstart_mb = used_mb()',
479 '\t\t}',
480 '\t}',
481 '\tend_mb := used_mb()',
482 '\tif start_mb > 0 && end_mb > start_mb + 64 {',
483 "\t\tpanic('closure frame callback memory grew too much')",
484 '\t}',
485 '\tlifetime.dispose()!',
486 '}',
487 '',
488 'fn run() ! {',
489 '\tframe_callback_context_is_reclaimed()!',
490 '}',
491 '',
492 'fn main() {',
493 '\trun() or { panic(err) }',
494 '}',
495 ].join('\n')
496}
497
498fn gc_none_lifetime_bookkeeping_track_heap_source(header_path string) string {
499 c_header_path := header_path.replace('\\', '/')
500 return [
501 'module main',
502 'import builtin.closure',
503 '',
504 '#include "${c_header_path}"',
505 '',
506 'fn no_capture_work() {}',
507 '',
508 'fn exercise(rounds int) ! {',
509 '\tfor _ in 0 .. rounds {',
510 '\t\tmut lifetime := closure.new_lifetime()',
511 '\t\tmut copy := lifetime',
512 '\t\tlifetime.dispose()!',
513 '\t\tcopy.frame(no_capture_work) or {',
514 '\t\t\tcontinue',
515 '\t\t}',
516 "\t\treturn error('disposed lifetime copy was accepted')",
517 '\t}',
518 '}',
519 '',
520 'fn main() {',
521 '\tmut warmup := closure.new_lifetime()',
522 '\twarmup.dispose() or { panic(err) }',
523 '\texercise(128) or { panic(err) }',
524 '\tmut start := u64(0)',
525 '\tunsafe { closure.lifetime_state_allocs(&start) }',
526 '\texercise(1000) or { panic(err) }',
527 '\tmut end := u64(0)',
528 '\tunsafe { closure.lifetime_state_allocs(&end) }',
529 '\tif end != start {',
530 "\t\tpanic('leaked lifetime state recycling bookkeeping: start=\${start} end=\${end}')",
531 '\t}',
532 '}',
533 ].join('\n')
534}
535
536fn lazy_concurrent_lifetime_init_source() string {
537 return [
538 'module main',
539 'import builtin.closure',
540 '',
541 '@[heap]',
542 'struct IntBox {',
543 'mut:',
544 '\tvalue int',
545 '}',
546 '',
547 'fn worker(id int, ready chan bool, start chan bool) ! {',
548 '\tready <- true',
549 "\t_ := <-start or { return error('start channel closed') }",
550 '\tfor round in 0 .. 32 {',
551 '\t\tmut box := &IntBox{}',
552 '\t\texpected := id * 1000 + round',
553 '\t\tmut lifetime := closure.new_lifetime()',
554 '\t\tlifetime.frame(fn [mut box, expected] () {',
555 '\t\t\tmake_value := fn [expected] () int { return expected }',
556 '\t\t\tbox.value = make_value()',
557 '\t\t})!',
558 '\t\tassert box.value == expected',
559 '\t\tlifetime.dispose()!',
560 '\t}',
561 '}',
562 '',
563 'fn run() ! {',
564 '\tthread_count := 16',
565 '\tready := chan bool{cap: thread_count}',
566 '\tstart := chan bool{cap: thread_count}',
567 '\tmut threads := []thread !{cap: thread_count}',
568 '\tfor i in 0 .. thread_count {',
569 '\t\tthreads << spawn worker(i, ready, start)',
570 '\t}',
571 '\tfor _ in 0 .. thread_count {',
572 "\t\t_ := <-ready or { return error('ready channel closed') }",
573 '\t}',
574 '\tfor _ in 0 .. thread_count {',
575 '\t\tstart <- true',
576 '\t}',
577 '\tthreads.wait()!',
578 '}',
579 '',
580 'fn main() {',
581 '\trun() or { panic(err) }',
582 '}',
583 ].join('\n')
584}
585
586fn assert_misuse_program_passes(tmp_dir string, name string, source string) {
587 source_path := write_program(tmp_dir, name, source)
588 res := os.execute('${os.quoted_path(vexe)} run ${os.quoted_path(source_path)}')
589 assert res.exit_code == 0, res.output
590}
591
592fn test_lifetime_public_api_without_captured_closure() {
593 $if gcboehm_leak ? {
594 return
595 }
596 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_no_capture_${os.getpid()}')
597 os.mkdir_all(tmp_dir) or { panic(err) }
598 defer {
599 os.rmdir_all(tmp_dir) or {}
600 }
601 source := no_captured_closure_lifetime_source()
602 c_res := c_output_for_program(tmp_dir, 'no_captured_lifetime_codegen', source)
603 assert c_res.exit_code == 0, c_res.output
604 assert !c_res.output.contains('builtin__closure__closure_create')
605 assert !c_res.output.contains('_V_closure_main__')
606 for mode in ['boehm', 'none'] {
607 assert_run_succeeds_or_missing_boehm(tmp_dir, 'no_captured_lifetime', source, mode)
608 }
609 assert_boehm_leak_compile_or_missing_lib(tmp_dir, 'no_captured_lifetime', source)
610}
611
612fn test_closure_lifetime_runtime_api_contract() {
613 $if gcboehm_leak ? {
614 return
615 }
616 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_runtime_${os.getpid()}')
617 os.mkdir_all(tmp_dir) or { panic(err) }
618 defer {
619 os.rmdir_all(tmp_dir) or {}
620 }
621 source := closure_lifetime_runtime_source()
622 for mode in ['boehm', 'none'] {
623 assert_run_succeeds_or_missing_boehm(tmp_dir, 'closure_lifetime_runtime', source, mode)
624 }
625 assert_boehm_leak_compile_or_missing_lib(tmp_dir, 'closure_lifetime_runtime', source)
626}
627
628fn test_closure_lifetime_boehm_leak_runtime_without_persistent_callbacks() {
629 $if gcboehm_leak ? {
630 return
631 }
632 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_boehm_leak_${os.getpid()}')
633 os.mkdir_all(tmp_dir) or { panic(err) }
634 defer {
635 os.rmdir_all(tmp_dir) or {}
636 }
637 assert_boehm_leak_runtime_or_compile_only(tmp_dir, 'closure_lifetime_boehm_leak_clean',
638 boehm_leak_clean_lifetime_source())
639}
640
641fn test_closure_lifetime_gc_none_does_not_leak_frame_callback_or_bookkeeping() {
642 $if gcboehm_leak ? {
643 return
644 }
645 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_gc_none_memory_${os.getpid()}')
646 os.mkdir_all(tmp_dir) or { panic(err) }
647 defer {
648 os.rmdir_all(tmp_dir) or {}
649 }
650 res := run_program_with_gc(tmp_dir, 'closure_lifetime_gc_none_memory',
651 gc_none_lifetime_reclaim_memory_source(), 'none')
652 assert res.exit_code == 0, res.output
653}
654
655fn test_closure_lifetime_gc_none_reuses_disposed_state_bookkeeping() {
656 $if gcboehm_leak ? {
657 return
658 }
659 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_gc_none_state_${os.getpid()}')
660 os.mkdir_all(tmp_dir) or { panic(err) }
661 defer {
662 os.rmdir_all(tmp_dir) or {}
663 }
664 header_path := os.join_path(tmp_dir, 'track_heap_hooks.h')
665 os.write_file(header_path, [
666 'void vheap_alloc(void* p, unsigned long long n) { (void)p; (void)n; }',
667 'void vheap_free(void* p) { (void)p; }',
668 ].join('\n')) or { panic(err) }
669 res := run_program_with_track_heap(tmp_dir, 'closure_lifetime_gc_none_state',
670 gc_none_lifetime_bookkeeping_track_heap_source(header_path))
671 assert res.exit_code == 0, res.output
672}
673
674fn test_closure_lifetime_dispose_recycles_state_for_later_lifetimes() {
675 source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/closure.c.v')
676 source := os.read_file(source_path) or { panic(err) }
677 start := source.index('pub fn (mut lifetime Lifetime) dispose() !') or {
678 panic('missing dispose helper')
679 }
680 end := source[start..].index('pub fn (mut lifetime Lifetime) suspend') or {
681 panic('missing dispose helper end')
682 }
683 helper := source[start..start + end]
684 assert helper.contains('closure_lifetime_recycle_state_no_lock(mut state)')
685 assert helper.contains('lifetime.state = unsafe { nil }')
686 assert helper.contains('lifetime.disposed = true')
687 assert !helper.contains('free(state)')
688}
689
690fn test_closure_lifetime_freestanding_no_std_object_compile() {
691 $if !linux {
692 return
693 }
694 $if !amd64 {
695 return
696 }
697 $if gcboehm_leak ? {
698 return
699 }
700 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_freestanding_${os.getpid()}')
701 os.mkdir_all(tmp_dir) or { panic(err) }
702 defer {
703 os.rmdir_all(tmp_dir) or {}
704 }
705 source := [
706 'module main',
707 'import builtin.closure',
708 '',
709 'fn main() {',
710 '\tmut lifetime := closure.new_lifetime()',
711 '\tlifetime.dispose() or { panic(err) }',
712 '}',
713 ].join('\n')
714 res := compile_freestanding_object(tmp_dir, 'closure_lifetime_freestanding', source)
715 assert res.exit_code == 0, res.output
716}
717
718fn test_closure_lifetime_lazy_concurrent_runtime_init() {
719 $if gcboehm_leak ? {
720 return
721 }
722 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_lazy_init_${os.getpid()}')
723 os.mkdir_all(tmp_dir) or { panic(err) }
724 defer {
725 os.rmdir_all(tmp_dir) or {}
726 }
727 source := lazy_concurrent_lifetime_init_source()
728 for mode in ['none', 'boehm'] {
729 assert_run_succeeds_or_missing_boehm(tmp_dir, 'closure_lifetime_lazy_init', source, mode)
730 }
731}
732
733fn test_closure_lifetime_reclaim_helper_reuses_buffers_without_clone() {
734 source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/closure.c.v')
735 source := os.read_file(source_path) or { panic(err) }
736 start := source.index('fn closure_lifetime_reclaim_no_lock') or {
737 panic('missing reclaim helper')
738 }
739 end := source[start..].index('fn closure_ensure_initialized') or {
740 panic('missing reclaim helper end')
741 }
742 helper := source[start..start + end]
743 assert !helper.contains('.clone()')
744 assert helper.contains('delete_many(0, reclaim_count)')
745 assert helper.contains('delete_many(0, cutoff)')
746}
747
748fn test_closure_lifetime_dispose_frees_bookkeeping_buffers_but_keeps_state() {
749 source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/closure.c.v')
750 source := os.read_file(source_path) or { panic(err) }
751 start := source.index('fn closure_lifetime_recycle_state_no_lock') or {
752 panic('missing recycle helper')
753 }
754 end := source[start..].index('fn closure_lifetime_error') or {
755 panic('missing recycle helper end')
756 }
757 helper := source[start..start + end]
758 assert helper.contains('state.disposed = true')
759 assert helper.contains('state.records.free()')
760 assert helper.contains('state.frames.free()')
761 assert helper.contains('state.records = []ClosureLifetimeRecord{}')
762 assert helper.contains('state.frames = []ClosureLifetimeFrame{}')
763 assert helper.contains('state.next_free = g_closure.free_lifetime_states')
764 assert helper.contains('g_closure.free_lifetime_states = state')
765 assert !helper.contains('free(state)')
766}
767
768fn test_closure_lifetime_once_headers_keep_helper_internal() {
769 for header_name in ['closure_once_nix.h', 'closure_once_windows.h'] {
770 source_path := os.join_path(os.dir(vexe), 'vlib/builtin/closure/${header_name}')
771 source := os.read_file(source_path) or { panic(err) }
772 assert source.contains('V_CLOSURE_STATIC_INLINE void v_closure_init_once')
773 assert !source.contains('\nvoid v_closure_init_once')
774 }
775}
776
777fn test_closure_lifetime_codegen_emits_single_local_destroy() {
778 $if gcboehm_leak ? {
779 return
780 }
781 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_codegen_${os.getpid()}')
782 os.mkdir_all(tmp_dir) or { panic(err) }
783 defer {
784 os.rmdir_all(tmp_dir) or {}
785 }
786 source := [
787 'module main',
788 'import builtin.closure',
789 '',
790 'fn run() ! {',
791 '\tmut lifetime := closure.new_lifetime()',
792 '\tlifetime.frame(fn () {',
793 '\t\tvalue := 7',
794 '\t\th := fn [value] () int { return value }',
795 '\t\tassert h() == 7',
796 '\t})!',
797 '\tlifetime.reclaim_all()!',
798 '\tlifetime.dispose()!',
799 '}',
800 '',
801 'fn main() {',
802 '\trun() or { panic(err) }',
803 '}',
804 ].join('\n')
805 res := c_output_for_program(tmp_dir, 'closure_lifetime_single_destroy', source)
806 assert res.exit_code == 0, res.output
807 assert count_occurrences(res.output, 'builtin__closure__closure_try_destroy((voidptr)h);') == 1
808}
809
810fn test_closure_lifetime_inline_callback_codegen_cleanup() {
811 $if gcboehm_leak ? {
812 return
813 }
814 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_inline_codegen_${os.getpid()}')
815 os.mkdir_all(tmp_dir) or { panic(err) }
816 defer {
817 os.rmdir_all(tmp_dir) or {}
818 }
819 source := [
820 'module main',
821 'import builtin.closure',
822 '',
823 'struct View {',
824 '\tvalue int',
825 '}',
826 '',
827 'fn (view View) draw() {',
828 '\tassert view.value == 41',
829 '}',
830 '',
831 'fn frame_bound_method() ! {',
832 '\tview := View{value: 41}',
833 '\tmut lifetime := closure.new_lifetime()',
834 '\tlifetime.frame(view.draw)!',
835 '\tlifetime.dispose()!',
836 '}',
837 '',
838 'fn outside_suspend_untracked() ! {',
839 '\tmut lifetime := closure.new_lifetime()',
840 '\tvalue := 7',
841 '\tlifetime.suspend(fn [value] () {',
842 '\t\tassert value == 7',
843 '\t})!',
844 '\tlifetime.untracked(fn [value] () {',
845 '\t\tassert value == 7',
846 '\t})!',
847 '\tlifetime.dispose()!',
848 '}',
849 '',
850 'fn parenthesized_inline_frame() ! {',
851 '\tvalue := 12',
852 '\tmut lifetime := closure.new_lifetime()',
853 '\tlifetime.frame((fn [value] () {',
854 '\t\tassert value == 12',
855 '\t}))!',
856 '\tlifetime.dispose()!',
857 '}',
858 '',
859 'fn tail_return_frame(value int) ! {',
860 '\tmut lifetime := closure.new_lifetime()',
861 '\treturn lifetime.frame(fn [value] () {',
862 '\t\tassert value == 9',
863 '\t})',
864 '}',
865 '',
866 'fn main() {',
867 '\tframe_bound_method() or { panic(err) }',
868 '\toutside_suspend_untracked() or { panic(err) }',
869 '\tparenthesized_inline_frame() or { panic(err) }',
870 '\ttail_return_frame(9) or { panic(err) }',
871 '}',
872 ].join('\n')
873 res := c_output_for_program(tmp_dir, 'closure_lifetime_inline_cleanup', source)
874 assert res.exit_code == 0, res.output
875 destroy_count := count_occurrences(res.output,
876 'builtin__closure__closure_try_destroy((voidptr)')
877 assert destroy_count == 5, res.output
878 parenthesized_start := res.output.index('VV_LOC _result_void main__parenthesized_inline_frame(void) {') or {
879 panic(res.output)
880 }
881 parenthesized_end := if parenthesized_start + 1200 < res.output.len {
882 parenthesized_start + 1200
883 } else {
884 res.output.len
885 }
886 parenthesized_fn := res.output[parenthesized_start..parenthesized_end]
887 frame_pos := parenthesized_fn.index('builtin__closure__Lifetime_frame') or {
888 panic(parenthesized_fn)
889 }
890 destroy_pos := parenthesized_fn.index('builtin__closure__closure_try_destroy((voidptr)') or {
891 panic(parenthesized_fn)
892 }
893 assert destroy_pos > frame_pos, parenthesized_fn
894 assert !res.output.contains('return builtin__closure__Lifetime_frame(')
895}
896
897fn test_closure_lifetime_borrowed_callback_result_is_not_destroyed() {
898 $if gcboehm_leak ? {
899 return
900 }
901 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_borrowed_callback_${os.getpid()}')
902 os.mkdir_all(tmp_dir) or { panic(err) }
903 defer {
904 os.rmdir_all(tmp_dir) or {}
905 }
906 source := [
907 'module main',
908 'import builtin.closure',
909 '',
910 'fn identity(cb fn ()) fn () {',
911 '\treturn cb',
912 '}',
913 '',
914 'fn run() ! {',
915 '\tpayload := []int{len: 32, init: index}',
916 '\tstored := fn [payload] () {',
917 '\t\tassert payload[0] == 0',
918 '\t\tassert payload[payload.len - 1] == 31',
919 '\t}',
920 '\tmut lifetime := closure.new_lifetime()',
921 '\tlifetime.frame(identity(stored))!',
922 '\tlifetime.dispose()!',
923 '\tstored()',
924 '}',
925 '',
926 'fn main() {',
927 '\trun() or { panic(err) }',
928 '}',
929 ].join('\n')
930 c_res := c_output_for_program(tmp_dir, 'closure_lifetime_borrowed_callback_cgen', source)
931 assert c_res.exit_code == 0, c_res.output
932 run_start := c_res.output.index('VV_LOC _result_void main__run(void) {') or {
933 panic(c_res.output)
934 }
935 run_end := c_res.output.index_after('VV_LOC void main__main(void) {', run_start) or {
936 panic(c_res.output)
937 }
938 run_fn := c_res.output[run_start..run_end]
939 assert !run_fn.contains('builtin__closure__closure_try_destroy((voidptr)'), run_fn
940 res := run_program_with_gc(tmp_dir, 'closure_lifetime_borrowed_callback', source, 'none')
941 assert res.exit_code == 0, res.output
942}
943
944fn test_lifetime_rejects_misuse_with_errors() {
945 $if gcboehm_leak ? {
946 return
947 }
948 tmp_dir := os.join_path(os.vtmp_dir(), 'v_closure_lifetime_api_${os.getpid()}')
949 os.mkdir_all(tmp_dir) or { panic(err) }
950 defer {
951 os.rmdir_all(tmp_dir) or {}
952 }
953 assert_misuse_program_passes(tmp_dir, 'wrong_thread_reclaim', [
954 'import builtin.closure',
955 '',
956 'fn reclaim_from_thread(lifetime closure.Lifetime) ! {',
957 '\tmut local := lifetime',
958 '\tlocal.reclaim_all()!',
959 '}',
960 '',
961 'fn main() {',
962 '\tlifetime := closure.new_lifetime()',
963 '\tth := spawn reclaim_from_thread(lifetime)',
964 '\tth.wait() or {',
965 "\t\tassert err.msg().contains('different thread')",
966 '\t\treturn',
967 '\t}',
968 '\tassert false',
969 '}',
970 ].join('\n'))
971 assert_misuse_program_passes(tmp_dir, 'wrong_thread_dispose', [
972 'import builtin.closure',
973 '',
974 'fn dispose_from_thread(lifetime closure.Lifetime) ! {',
975 '\tmut local := lifetime',
976 '\tlocal.dispose()!',
977 '}',
978 '',
979 'fn main() {',
980 '\tlifetime := closure.new_lifetime()',
981 '\tth := spawn dispose_from_thread(lifetime)',
982 '\tth.wait() or {',
983 "\t\tassert err.msg().contains('different thread')",
984 '\t\treturn',
985 '\t}',
986 '\tassert false',
987 '}',
988 ].join('\n'))
989 assert_misuse_program_passes(tmp_dir, 'wrong_thread_suspend', [
990 'import builtin.closure',
991 '',
992 'fn no_capture_work() {}',
993 '',
994 'fn suspend_from_thread(lifetime closure.Lifetime) ! {',
995 '\tmut local := lifetime',
996 '\tlocal.suspend(no_capture_work)!',
997 '}',
998 '',
999 'fn main() {',
1000 '\tlifetime := closure.new_lifetime()',
1001 '\tth := spawn suspend_from_thread(lifetime)',
1002 '\tth.wait() or {',
1003 "\t\tassert err.msg().contains('different thread')",
1004 '\t\treturn',
1005 '\t}',
1006 '\tassert false',
1007 '}',
1008 ].join('\n'))
1009 assert_misuse_program_passes(tmp_dir, 'wrong_thread_frame', [
1010 'import builtin.closure',
1011 '',
1012 'fn no_capture_work() {}',
1013 '',
1014 'fn frame_from_thread(lifetime closure.Lifetime) ! {',
1015 '\tmut local := lifetime',
1016 '\tlocal.frame(no_capture_work)!',
1017 '}',
1018 '',
1019 'fn main() {',
1020 '\tlifetime := closure.new_lifetime()',
1021 '\tth := spawn frame_from_thread(lifetime)',
1022 '\tth.wait() or {',
1023 "\t\tassert err.msg().contains('different thread')",
1024 '\t\treturn',
1025 '\t}',
1026 '\tassert false',
1027 '}',
1028 ].join('\n'))
1029 assert_misuse_program_passes(tmp_dir, 'nested_same_lifetime_frame', [
1030 'import builtin.closure',
1031 '',
1032 '@[heap]',
1033 'struct BoolBox {',
1034 'mut:',
1035 '\tvalue bool',
1036 '}',
1037 '',
1038 'fn no_capture_work() {}',
1039 '',
1040 'fn run() ! {',
1041 '\tmut lifetime := closure.new_lifetime()',
1042 '\tmut saw := &BoolBox{}',
1043 '\tlifetime.frame(fn [mut lifetime, mut saw] () {',
1044 '\t\tlifetime.frame(no_capture_work) or {',
1045 "\t\t\tassert err.msg().contains('active') || err.msg().contains('nested')",
1046 '\t\t\tsaw.value = true',
1047 '\t\t\treturn',
1048 '\t\t}',
1049 '\t\tassert false',
1050 '\t})!',
1051 '\tassert saw.value',
1052 '\tlifetime.dispose()!',
1053 '}',
1054 '',
1055 'fn main() {',
1056 '\trun() or { panic(err) }',
1057 '}',
1058 ].join('\n'))
1059 assert_misuse_program_passes(tmp_dir, 'second_active_lifetime', [
1060 'import builtin.closure',
1061 '',
1062 '@[heap]',
1063 'struct BoolBox {',
1064 'mut:',
1065 '\tvalue bool',
1066 '}',
1067 '',
1068 'fn no_capture_work() {}',
1069 '',
1070 'fn run() ! {',
1071 '\tmut first := closure.new_lifetime()',
1072 '\tmut second := closure.new_lifetime()',
1073 '\tmut saw := &BoolBox{}',
1074 '\tfirst.frame(fn [mut second, mut saw] () {',
1075 '\t\tsecond.frame(no_capture_work) or {',
1076 "\t\t\tassert err.msg().contains('active') || err.msg().contains('another')",
1077 '\t\t\tsaw.value = true',
1078 '\t\t\treturn',
1079 '\t\t}',
1080 '\t\tassert false',
1081 '\t})!',
1082 '\tassert saw.value',
1083 '\tfirst.dispose()!',
1084 '\tsecond.dispose()!',
1085 '}',
1086 '',
1087 'fn main() {',
1088 '\trun() or { panic(err) }',
1089 '}',
1090 ].join('\n'))
1091 assert_misuse_program_passes(tmp_dir, 'second_suspend_inside_first_frame', [
1092 'import builtin.closure',
1093 '',
1094 '@[heap]',
1095 'struct BoolBox {',
1096 'mut:',
1097 '\tvalue bool',
1098 '}',
1099 '',
1100 'fn no_capture_work() {}',
1101 '',
1102 'fn run() ! {',
1103 '\tmut first := closure.new_lifetime()',
1104 '\tmut second := closure.new_lifetime()',
1105 '\tmut saw := &BoolBox{}',
1106 '\tfirst.frame(fn [mut second, mut saw] () {',
1107 '\t\tsecond.suspend(no_capture_work) or {',
1108 "\t\t\tassert err.msg().contains('active') || err.msg().contains('another')",
1109 '\t\t\tsaw.value = true',
1110 '\t\t\treturn',
1111 '\t\t}',
1112 '\t\tassert false',
1113 '\t})!',
1114 '\tassert saw.value',
1115 '\tfirst.dispose()!',
1116 '\tsecond.dispose()!',
1117 '}',
1118 '',
1119 'fn main() {',
1120 '\trun() or { panic(err) }',
1121 '}',
1122 ].join('\n'))
1123 assert_misuse_program_passes(tmp_dir, 'reclaim_dispose_while_active', [
1124 'import builtin.closure',
1125 '',
1126 '@[heap]',
1127 'struct BoolBox {',
1128 'mut:',
1129 '\tvalue bool',
1130 '}',
1131 '',
1132 'fn run() ! {',
1133 '\tmut lifetime := closure.new_lifetime()',
1134 '\tmut saw_reclaim := &BoolBox{}',
1135 '\tmut saw_dispose := &BoolBox{}',
1136 '\tlifetime.frame(fn [mut lifetime, mut saw_reclaim, mut saw_dispose] () {',
1137 '\t\tlifetime.reclaim_all() or {',
1138 "\t\t\tassert err.msg().contains('active')",
1139 '\t\t\tsaw_reclaim.value = true',
1140 '\t\t}',
1141 '\t\tlifetime.dispose() or {',
1142 "\t\t\tassert err.msg().contains('active')",
1143 '\t\t\tsaw_dispose.value = true',
1144 '\t\t\treturn',
1145 '\t\t}',
1146 '\t\tassert false',
1147 '\t})!',
1148 '\tassert saw_reclaim.value',
1149 '\tassert saw_dispose.value',
1150 '\tlifetime.dispose()!',
1151 '}',
1152 '',
1153 'fn main() {',
1154 '\trun() or { panic(err) }',
1155 '}',
1156 ].join('\n'))
1157 assert_misuse_program_passes(tmp_dir, 'dispose_while_suspended', [
1158 'import builtin.closure',
1159 '',
1160 '@[heap]',
1161 'struct BoolBox {',
1162 'mut:',
1163 '\tvalue bool',
1164 '}',
1165 '',
1166 'fn run() ! {',
1167 '\tmut lifetime := closure.new_lifetime()',
1168 '\tmut saw := &BoolBox{}',
1169 '\tlifetime.suspend(fn [mut lifetime, mut saw] () {',
1170 '\t\tlifetime.dispose() or {',
1171 "\t\t\tassert err.msg().contains('suspend')",
1172 '\t\t\tsaw.value = true',
1173 '\t\t\treturn',
1174 '\t\t}',
1175 '\t\tassert false',
1176 '\t})!',
1177 '\tassert saw.value',
1178 '\tlifetime.dispose()!',
1179 '}',
1180 '',
1181 'fn main() {',
1182 '\trun() or { panic(err) }',
1183 '}',
1184 ].join('\n'))
1185 assert_misuse_program_passes(tmp_dir, 'dispose_while_untracked', [
1186 'import builtin.closure',
1187 '',
1188 '@[heap]',
1189 'struct BoolBox {',
1190 'mut:',
1191 '\tvalue bool',
1192 '}',
1193 '',
1194 'fn run() ! {',
1195 '\tmut lifetime := closure.new_lifetime()',
1196 '\tmut saw := &BoolBox{}',
1197 '\tlifetime.untracked(fn [mut lifetime, mut saw] () {',
1198 '\t\tlifetime.dispose() or {',
1199 "\t\t\tassert err.msg().contains('suspend')",
1200 '\t\t\tsaw.value = true',
1201 '\t\t\treturn',
1202 '\t\t}',
1203 '\t\tassert false',
1204 '\t})!',
1205 '\tassert saw.value',
1206 '\tlifetime.dispose()!',
1207 '}',
1208 '',
1209 'fn main() {',
1210 '\trun() or { panic(err) }',
1211 '}',
1212 ].join('\n'))
1213 assert_misuse_program_passes(tmp_dir, 'frame_while_suspended', [
1214 'import builtin.closure',
1215 '',
1216 '@[heap]',
1217 'struct BoolBox {',
1218 'mut:',
1219 '\tvalue bool',
1220 '}',
1221 '',
1222 'fn no_capture_work() {}',
1223 '',
1224 'fn run() ! {',
1225 '\tmut lifetime := closure.new_lifetime()',
1226 '\tmut saw := &BoolBox{}',
1227 '\tlifetime.suspend(fn [mut lifetime, mut saw] () {',
1228 '\t\tlifetime.frame(no_capture_work) or {',
1229 "\t\t\tassert err.msg().contains('suspend')",
1230 '\t\t\tsaw.value = true',
1231 '\t\t\treturn',
1232 '\t\t}',
1233 '\t\tassert false',
1234 '\t})!',
1235 '\tassert saw.value',
1236 '\tlifetime.dispose()!',
1237 '}',
1238 '',
1239 'fn main() {',
1240 '\trun() or { panic(err) }',
1241 '}',
1242 ].join('\n'))
1243 assert_misuse_program_passes(tmp_dir, 'use_after_dispose', [
1244 'import builtin.closure',
1245 '',
1246 'fn no_capture_work() {}',
1247 '',
1248 'fn run() ! {',
1249 '\tmut lifetime := closure.new_lifetime()',
1250 '\tlifetime.dispose()!',
1251 '\tmut saw_frame := false',
1252 '\tmut saw_reclaim := false',
1253 '\tmut saw_suspend := false',
1254 '\tmut saw_untracked := false',
1255 '\tmut saw_dispose := false',
1256 '\tlifetime.frame(no_capture_work) or {',
1257 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
1258 '\t\tsaw_frame = true',
1259 '\t}',
1260 '\tlifetime.reclaim_all() or {',
1261 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
1262 '\t\tsaw_reclaim = true',
1263 '\t}',
1264 '\tlifetime.suspend(no_capture_work) or {',
1265 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
1266 '\t\tsaw_suspend = true',
1267 '\t}',
1268 '\tlifetime.untracked(no_capture_work) or {',
1269 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
1270 '\t\tsaw_untracked = true',
1271 '\t}',
1272 '\tlifetime.dispose() or {',
1273 "\t\tassert err.msg().contains('dispose') || err.msg().contains('after')",
1274 '\t\tsaw_dispose = true',
1275 '\t}',
1276 '\tassert saw_frame',
1277 '\tassert saw_reclaim',
1278 '\tassert saw_suspend',
1279 '\tassert saw_untracked',
1280 '\tassert saw_dispose',
1281 '}',
1282 '',
1283 'fn main() {',
1284 '\trun() or { panic(err) }',
1285 '}',
1286 ].join('\n'))
1287}
1288