From 8af3174b04f18d858653ad70669dced71f5eaa99 Mon Sep 17 00:00:00 2001 From: zinc Date: Wed, 14 Oct 2026 23:10:49 +0330 Subject: destructors: heap(v), drop(v, f) / v.drop(f) / v.drop = f, struct drop methods, collector drops after the sweep (once, resurrection), freed blocks forget their drop, temporaries drop at statement end with a hint, actor send check; o3: no collection inside a region (pending until the outermost exit), no drops set or ended inside o3; spec 3.11.6, 4.10.3-4, 6.15 --- tests/actor/reject/drop_message.zc | 13 ++++++++ tests/actor/reject/drop_value.zc | 12 +++++++ .../6-ownership/drop_ends_in_o3_rejected.zc | 9 +++++ tests/conformance/6-ownership/drop_forms.zc | 14 ++++++++ .../6-ownership/drop_free_in_o3_rejected.zc | 10 ++++++ tests/conformance/6-ownership/drop_gc_once.zc | 17 ++++++++++ tests/conformance/6-ownership/drop_gc_resurrect.zc | 21 ++++++++++++ tests/conformance/6-ownership/drop_heap_values.zc | 15 +++++++++ .../conformance/6-ownership/drop_in_o3_rejected.zc | 9 +++++ .../6-ownership/drop_parent_before_child.zc | 17 ++++++++++ .../conformance/6-ownership/drop_reused_address.zc | 22 ++++++++++++ tests/conformance/6-ownership/drop_set_late.zc | 10 ++++++ tests/conformance/6-ownership/drop_set_twice.zc | 10 ++++++ .../6-ownership/drop_temporary_statement.zc | 13 ++++++++ .../conformance/6-ownership/drop_value_and_type.zc | 11 ++++++ tests/conformance/8-runtime/o3_no_gc_inside.zc | 39 ++++++++++++++++++++++ tests/data/drop_hint.zc | 2 ++ tests/drivers/actor_test.py | 2 +- tests/test.sh | 1 + 19 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 tests/actor/reject/drop_message.zc create mode 100644 tests/actor/reject/drop_value.zc create mode 100644 tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc create mode 100644 tests/conformance/6-ownership/drop_forms.zc create mode 100644 tests/conformance/6-ownership/drop_free_in_o3_rejected.zc create mode 100644 tests/conformance/6-ownership/drop_gc_once.zc create mode 100644 tests/conformance/6-ownership/drop_gc_resurrect.zc create mode 100644 tests/conformance/6-ownership/drop_heap_values.zc create mode 100644 tests/conformance/6-ownership/drop_in_o3_rejected.zc create mode 100644 tests/conformance/6-ownership/drop_parent_before_child.zc create mode 100644 tests/conformance/6-ownership/drop_reused_address.zc create mode 100644 tests/conformance/6-ownership/drop_set_late.zc create mode 100644 tests/conformance/6-ownership/drop_set_twice.zc create mode 100644 tests/conformance/6-ownership/drop_temporary_statement.zc create mode 100644 tests/conformance/6-ownership/drop_value_and_type.zc create mode 100644 tests/conformance/8-runtime/o3_no_gc_inside.zc create mode 100644 tests/data/drop_hint.zc (limited to 'tests') diff --git a/tests/actor/reject/drop_message.zc b/tests/actor/reject/drop_message.zc new file mode 100644 index 0000000..fb37d5f --- /dev/null +++ b/tests/actor/reject/drop_message.zc @@ -0,0 +1,13 @@ +// destructors 7: copying a value with a drop into an actor message is an error; two processes would drop it twice +namespace; +include rt, actor; +struct res { int fd; void drop() { rt.put_s("closed"); } } +actor Keeper { int total; int handle(res* r) { return r.fd; } } +fn main() { + Keeper* k = rt.new(Keeper); + res* r = rt.new(res); r.fd = 3; + k.spawn(); + k.send(r); + k.close(); + return 0; +} diff --git a/tests/actor/reject/drop_value.zc b/tests/actor/reject/drop_value.zc new file mode 100644 index 0000000..10e941d --- /dev/null +++ b/tests/actor/reject/drop_value.zc @@ -0,0 +1,12 @@ +// destructors 7: a value whose own drop is set can't be copied into an actor message either +namespace; +include rt, actor; +actor Echo { int handle(int* p) { return *p; } } +fn main() { + Echo* e = rt.new(Echo); + int* p = rt.new(int); p.drop = fn() { rt.put_s("dropped"); }; + e.spawn(); + e.send(p); + e.close(); + return 0; +} diff --git a/tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc b/tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc new file mode 100644 index 0000000..4f29537 --- /dev/null +++ b/tests/conformance/6-ownership/drop_ends_in_o3_rejected.zc @@ -0,0 +1,9 @@ +// expect error: r: has drop and can't end inside an o3 region +// destructors, o3: a droppable value's lifetime can't end inside an o3 scope +include rt; +struct res { int fd; void drop() { rt.put_s("closed"); } } +fn main() { + o3 { res* r = rt.new(res); r.fd = 1; } + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_forms.zc b/tests/conformance/6-ownership/drop_forms.zc new file mode 100644 index 0000000..e9e4c07 --- /dev/null +++ b/tests/conformance/6-ownership/drop_forms.zc @@ -0,0 +1,14 @@ +// expect: 15i'm free as a bird! 15/body y7 x g +// destructors 2: drop(v, f) and v.drop(f) return v, v.drop = f; is a statement; this is the value. the spec example drops its temporary at the end of the statement +include rt; +fn main() { + rt.put_n(*heap(15).drop(fn() { rt.put_s("i'm free as a bird! "); rt.put_n(*this); })); + rt.put_c(10); + let g = drop(heap(1), fn() { rt.put_s("g "); }); + let x = rt.new(int).drop(fn() { rt.put_s("x "); }); + let y = rt.new(int); y.drop = fn() { rt.put_s("y"); rt.put_n(*this); rt.put_s(" "); }; + *y = 7; + rt.put_s("body "); + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_free_in_o3_rejected.zc b/tests/conformance/6-ownership/drop_free_in_o3_rejected.zc new file mode 100644 index 0000000..b318d1f --- /dev/null +++ b/tests/conformance/6-ownership/drop_free_in_o3_rejected.zc @@ -0,0 +1,10 @@ +// expect error: x: has drop and can't end inside an o3 region +// destructors, o3: an explicit free of a droppable value inside o3 is an error +include rt; +fn main() { + int* x = rt.new(int); + x.drop = fn() { rt.put_s("a"); }; + o3 { rt.free(x); } + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_gc_once.zc b/tests/conformance/6-ownership/drop_gc_once.zc new file mode 100644 index 0000000..abe1d95 --- /dev/null +++ b/tests/conformance/6-ownership/drop_gc_once.zc @@ -0,0 +1,17 @@ +// expect: 0 drop 7 1 1 1 0/ +// destructors 6: the collector runs an unreachable value's drop after the sweep, exactly once; later collections never run it again +include rt; +int n; +word g; +fn make() { g = drop(heap(7), fn() { n++; rt.put_s("drop "); rt.put_n(*this); rt.put_c(32); }); return 0; } +fn main() { + make(); + rt.gc(); rt.put_n(n); rt.put_c(32); + g = 0; + rt.gc(); rt.put_n(n); rt.put_c(32); + rt.gc(); rt.put_n(n); rt.put_c(32); + rt.gc(); rt.put_n(n); rt.put_c(32); + rt.put_n(rt.dcount); rt.put_c(10); // its entry is gone with the block + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_gc_resurrect.zc b/tests/conformance/6-ownership/drop_gc_resurrect.zc new file mode 100644 index 0000000..d964eba --- /dev/null +++ b/tests/conformance/6-ownership/drop_gc_resurrect.zc @@ -0,0 +1,21 @@ +// expect: drop 8 alive 8 1 gone 1/ +// destructors 6: a drop that stores this somewhere reachable revives the value; it stays usable, its drop never runs again, and a later collection reclaims it +include rt; +int n; +word g, saved; +unchecked int keep(word p) { saved = p; return 0; } +fn make() { g = drop(heap(8), fn() { n++; keep(this); rt.put_s("drop "); rt.put_n(*this); rt.put_c(32); }); return 0; } +fn main() { + make(); + g = 0; + rt.gc(); + rt.gc(); + unchecked { word p = saved; rt.put_s("alive "); rt.put_n(p[0]); rt.put_c(32); } + rt.put_n(n); rt.put_c(32); + saved = 0; + rt.gc(); rt.gc(); + rt.put_s("gone "); rt.put_n(n); + rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_heap_values.zc b/tests/conformance/6-ownership/drop_heap_values.zc new file mode 100644 index 0000000..63d07c9 --- /dev/null +++ b/tests/conformance/6-ownership/drop_heap_values.zc @@ -0,0 +1,15 @@ +// expect: 10 hi 2 4 9 / +// destructors 1: heap(v) boxes an int, a string and a struct into a fresh owned block sized to the value +include rt; +struct foo { int bar; int baz; } +fn main() { + int* a = heap(10); + char* s = heap("hi"); + foo v; v.bar = 4; v.baz = 9; + foo* f = heap(v); + rt.put_n(*a); rt.put_c(32); rt.put_s(s); rt.put_c(32); rt.put_n(rt.len_w(s)); rt.put_c(32); + rt.put_n(f.bar); rt.put_c(32); rt.put_n(f.baz); rt.put_c(32); + rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_in_o3_rejected.zc b/tests/conformance/6-ownership/drop_in_o3_rejected.zc new file mode 100644 index 0000000..9095269 --- /dev/null +++ b/tests/conformance/6-ownership/drop_in_o3_rejected.zc @@ -0,0 +1,9 @@ +// expect error: a drop can't be set inside an o3 region +// destructors, o3: setting a drop inside o3 is an error +include rt; +fn main() { + int* x = rt.new(int); + o3 { x.drop = fn() { rt.put_s("a"); }; } + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_parent_before_child.zc b/tests/conformance/6-ownership/drop_parent_before_child.zc new file mode 100644 index 0000000..9f68c52 --- /dev/null +++ b/tests/conformance/6-ownership/drop_parent_before_child.zc @@ -0,0 +1,17 @@ +// expect: parent sees 5 child / +// destructors 3: a child owned by a field drops after its parent's drop returns, so the parent still sees it +include rt; +struct node { int* kid; } +int* seen; +fn main() { + { + node* p = rt.new(node); + p.drop = fn() { rt.put_s("parent sees "); rt.put_n(*this.kid); rt.put_c(32); }; + int* k = rt.new(int); k.drop = fn() { rt.put_s("child "); }; + *k = 5; + p.kid = k; + } + rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_reused_address.zc b/tests/conformance/6-ownership/drop_reused_address.zc new file mode 100644 index 0000000..97ef21d --- /dev/null +++ b/tests/conformance/6-ownership/drop_reused_address.zc @@ -0,0 +1,22 @@ +// expect: drop 1 same 1 1 0/ +// destructors 9: a freed block's drop and its bits are cleared, so a block reused at the same address runs no old drop +include rt; +int n, first; +int addr(int* p) { return p as int; } +int clean(int* p) { unbounded { return (p[-4] & (rt.F_HASDROP | rt.F_DROPPED)) == 0; } } +// the drop is set in a helper, so main has no closure and its locals take no cells from the heap +int* .u armed() { int* p = rt.new(int); p.drop = fn() { n++; rt.put_s("drop "); }; return p; } +fn main() { + { int* p = armed(); *p = 1; first = addr(p); } + rt.put_n(n); rt.put_c(32); + { + int* q = rt.new(int); *q = 2; + if (addr(q) == first) rt.put_s("same "); else rt.put_s("moved "); + rt.put_n(clean(q)); rt.put_c(32); + } + rt.put_n(n); rt.put_c(32); + rt.put_n(rt.dcount); + rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_set_late.zc b/tests/conformance/6-ownership/drop_set_late.zc new file mode 100644 index 0000000..9bd8310 --- /dev/null +++ b/tests/conformance/6-ownership/drop_set_late.zc @@ -0,0 +1,10 @@ +// expect error: x: drop set after the value was used +// destructors 2: the drop must be set before the value is first read, reassigned or passed +include rt; +fn main() { + int* x = rt.new(int); + *x = 4; + x.drop = fn() { rt.put_s("a"); }; + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_set_twice.zc b/tests/conformance/6-ownership/drop_set_twice.zc new file mode 100644 index 0000000..843694a --- /dev/null +++ b/tests/conformance/6-ownership/drop_set_twice.zc @@ -0,0 +1,10 @@ +// expect error: x: drop is already set +// destructors 2: a value's drop can be set only once +include rt; +fn main() { + int* x = rt.new(int); + x.drop = fn() { rt.put_s("a"); }; + x.drop = fn() { rt.put_s("b"); }; + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_temporary_statement.zc b/tests/conformance/6-ownership/drop_temporary_statement.zc new file mode 100644 index 0000000..fd27623 --- /dev/null +++ b/tests/conformance/6-ownership/drop_temporary_statement.zc @@ -0,0 +1,13 @@ +// expect: unlocked a b unlocked c / +// destructors 3: a value nothing holds drops at the end of its full statement; a bound guard drops at block end +include rt; +void unlock() { rt.put_s("unlocked "); } +fn main() { + heap(0).drop = fn() { unlock(); }; + rt.put_s("a "); + { let guard = drop(heap(0), fn() { unlock(); }); rt.put_s("b "); } + rt.put_s("c "); + rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/conformance/6-ownership/drop_value_and_type.zc b/tests/conformance/6-ownership/drop_value_and_type.zc new file mode 100644 index 0000000..2979e4f --- /dev/null +++ b/tests/conformance/6-ownership/drop_value_and_type.zc @@ -0,0 +1,11 @@ +// expect: body value type only-type / +// destructors 2: a struct body's drop is the default for its type; a value's own drop runs first, then the type's +include rt; +struct res { int fd; void drop() { if (this.fd) rt.put_s("type "); else rt.put_s("only-type "); } } +fn main() { + { res* r = rt.new(res); r.drop = fn() { rt.put_s("value "); }; r.fd = 1; rt.put_s("body "); } + { res* q = rt.new(res); q.fd = 0; } + rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/conformance/8-runtime/o3_no_gc_inside.zc b/tests/conformance/8-runtime/o3_no_gc_inside.zc new file mode 100644 index 0000000..532765c --- /dev/null +++ b/tests/conformance/8-runtime/o3_no_gc_inside.zc @@ -0,0 +1,39 @@ +// expect: 0 1 0 1 0 1/ +// spec 4.10.3: no collection runs inside an o3 region; an allocation that would collect grows the heap, and leaving the outermost region runs the pending collection once +include rt; +word sink; +int inside; +o3 int churn(int n) { + int i = 0; + while (i < n) { sink = rt.new(64); i++; } + rt.gc(); + return 0; +} +int early(int base) { + o3 { + int i = 0; + while (i < 20000) { sink = rt.new(64); i++; } + rt.gc(); + inside = rt.gc_runs - base; + return 0; + } + return 1; +} +fn main() { + int before = rt.gc_runs; + o3 { + int i = 0; + while (i < 20000) { sink = rt.new(64); i++; } + inside = rt.gc_runs - before; + } + rt.put_n(inside); rt.put_c(32); rt.put_n(rt.gc_runs - before); rt.put_c(32); + before = rt.gc_runs; + churn(20000); + rt.put_n(0); rt.put_c(32); rt.put_n(rt.gc_runs - before); rt.put_c(32); + before = rt.gc_runs; + early(before); + rt.put_n(inside); rt.put_c(32); rt.put_n(rt.gc_runs - before); + rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/data/drop_hint.zc b/tests/data/drop_hint.zc new file mode 100644 index 0000000..c690bc5 --- /dev/null +++ b/tests/data/drop_hint.zc @@ -0,0 +1,2 @@ +include rt; +fn main() { heap(0).drop = fn() { rt.put_s("u"); }; return 0; } diff --git a/tests/drivers/actor_test.py b/tests/drivers/actor_test.py index 0cfa51f..929bbf4 100644 --- a/tests/drivers/actor_test.py +++ b/tests/drivers/actor_test.py @@ -23,7 +23,7 @@ for case in sorted((root / 'tests/actor').glob('*/*.zc')): proc = subprocess.run([str(root / args.vm)], input=source, text=True, capture_output=True, cwd=root, timeout=15) elapsed = time.monotonic() - start if case.parent.name == 'reject': - required = {'no_include': "actor: isn't a type", 'input_type': 'type error:', 'output_type': 'type error:', 'explicit_this': 'this is implicit', 'nested_missing_import': 'module access requires `include rt;`', 'generic_input': 'type error:', 'generic_method_return': 'type error:'}[case.stem] + required = {'no_include': "actor: isn't a type", 'input_type': 'type error:', 'output_type': 'type error:', 'explicit_this': 'this is implicit', 'nested_missing_import': 'module access requires `include rt;`', 'generic_input': 'type error:', 'generic_method_return': 'type error:', 'drop_message': "r: has drop and can't be copied into a message", 'drop_value': "p: has drop and can't be copied into a message"}[case.stem] assert required in proc.stdout, f'{case}: expected {required!r}, got {proc.stdout!r}' else: actual = proc.stdout diff --git a/tests/test.sh b/tests/test.sh index 1be4557..bd731ed 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -578,6 +578,7 @@ s_lsp() { case "$h" in *"$w"*) chk 1 1 "lsp hint: $w";; *) chk 0 1 "lsp hint: $w";; esac done chk "$($lsp < examples/struct_closure_opt.zc | grep -c error)" "0" "lsp: methods and closures check clean" + case "$($lsp < tests/data/drop_hint.zc)" in *"value with drop is never bound; it drops at the end of this statement"*) chk 1 1 "lsp hint: a value with drop that is never bound";; *) chk 0 1 "lsp hint: a value with drop that is never bound";; esac # a file of a larger build checks on its own: its bodyless declarations are not errors in check mode for f in src/runtime/rt.zc src/runtime/run.zc src/lsp/server.zc; do chk "$($lsp < $f | grep -c ': error:')" "0" "lsp: $f checks alone"; done # the compiler is several files that only make sense together: check them as one document -- cgit v1.2.3