From f76aa99566096c63f6f03e641e5d5178fb175cfc Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 24 May 2026 13:28:16 +0300 Subject: [PATCH] =?UTF-8?q?v2:=20ownership=20checker=20=E2=80=94=20add=20D?= =?UTF-8?q?rop=20marker=20interface=20and=20per-fn=20drop=20schedule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the `Drop` built-in interface alongside `Copy`/`Owned`. Structs that declare `implements Drop` MUST provide a `drop(mut self)` method; the checker validates this contract once per build and emits a diagnostic pointing at the struct decl when violated. Every owned binding whose static type implements `Drop` is recorded in `Checker.drop_schedule[fn_name]`. This is the scope-exit cleanup hook that the transformer / backends will lower to actual destructor calls in a follow-up — the AST is `pub:` read-only today, so emitting calls requires a separate AST-mutability change. --- vlib/v2/types/checker.v | 23 ++++ vlib/v2/types/checker_ownership.v | 150 +++++++++++++++++++++++++ vlib/v2/types/checker_ownership_test.v | 104 +++++++++++++++++ vlib/v2/types/universe.v | 11 +- 4 files changed, 287 insertions(+), 1 deletion(-) diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index ce965c8a4..eb93ac52d 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -595,6 +595,16 @@ mut: ownership_cur_fn string // Borrow tracking: variables currently borrowed via & borrowed_vars map[string][]BorrowInfo // var name -> list of active borrows + // Drop schedule: per-function list of owned bindings implementing the + // `Drop` interface. Each entry is the destructor call codegen must emit + // before the binding's owning scope exits. Populated as `Drop`-typed + // values are bound; pruned (via the `moved_vars` view) when they are + // moved or returned. Exposed so the transformer / backends can lower it. + drop_schedule map[string][]DropEntry + // Source positions for `implements Drop` struct declarations, indexed + // by struct name. Used to point the "missing drop method" diagnostic at + // the struct decl rather than at an unrelated use site. + ownership_drop_decl_positions map[string]token.Pos } pub fn Checker.new(prefs &pref.Preferences, file_set &token.FileSet, env &Environment) &Checker { @@ -1045,6 +1055,7 @@ pub fn (mut c Checker) check_files(files []ast.File) { c.process_pending_const_fields() $if ownership ? { c.ownership_prescan_fn_bodies() + c.ownership_validate_drop_impls() } c.process_pending_fn_bodies() c.check_struct_field_defaults(files) @@ -3656,6 +3667,17 @@ fn (mut c Checker) process_pending_struct_decls() { implements_names << fallback_name } } + // Stash the struct declaration position keyed by name so that the + // ownership validator can attach the "missing drop method" diagnostic + // to the decl site instead of an arbitrary use site. + $if ownership ? { + for impl_name in implements_names { + if impl_name == 'Drop' || impl_name.all_after_last('__') == 'Drop' { + c.ownership_drop_decl_positions[pending.decl.name] = pending.decl.pos + break + } + } + } mut fields := []Field{} for field in pending.decl.fields { field_typ := c.decl_field_type(field.typ) @@ -3994,6 +4016,7 @@ pub fn (mut c Checker) process_all_deferred() { c.process_pending_const_fields() $if ownership ? { c.ownership_prescan_fn_bodies() + c.ownership_validate_drop_impls() } c.process_pending_fn_bodies() } diff --git a/vlib/v2/types/checker_ownership.v b/vlib/v2/types/checker_ownership.v index b2d65794c..4212c30ea 100644 --- a/vlib/v2/types/checker_ownership.v +++ b/vlib/v2/types/checker_ownership.v @@ -730,12 +730,162 @@ fn ownership_type_display(t Type) string { // - a non-Copy value is created (struct/array/map literal, fn returning // non-Copy, etc.) // - a string is explicitly upgraded via `.to_owned()` +// +// If `typ` implements the `Drop` interface, this also schedules a destructor +// call to be emitted at scope exit (see `ownership_schedule_drop`). fn (mut c Checker) ownership_mark_owned(name string, typ Type, pos token.Pos) { if name.len == 0 || name == '_' { return } c.owned_vars[name] = pos c.owned_var_types[name] = ownership_type_display(typ) + c.ownership_schedule_drop(name, typ, pos) +} + +// is_drop_type reports whether `t` (or its alias / option / result base) is a +// struct that declares `implements Drop`. Drop types must provide a +// `drop(mut self)` method; the checker schedules a call to it at scope exit +// for every owned, non-moved binding via `drop_schedule`. +pub fn (c &Checker) is_drop_type(t Type) bool { + return c.is_drop_type_impl(t, 0) +} + +fn (c &Checker) is_drop_type_impl(t Type, depth int) bool { + if depth > 16 { + return false + } + match t { + Alias { + return c.is_drop_type_impl(t.base_type, depth + 1) + } + OptionType { + return c.is_drop_type_impl(t.base_type, depth + 1) + } + ResultType { + return c.is_drop_type_impl(t.base_type, depth + 1) + } + Struct { + return c.struct_marker_matches(t, 'Drop') + } + else { + return false + } + } +} + +// type_has_drop_method reports whether the type (or its base after unwrapping +// alias / option / result) has a registered `drop(...)` method. +fn (c &Checker) type_has_drop_method(t Type) bool { + mut cur := t + for _ in 0 .. 16 { + match cur { + Alias { + cur = (cur as Alias).base_type + continue + } + OptionType { + cur = (cur as OptionType).base_type + continue + } + ResultType { + cur = (cur as ResultType).base_type + continue + } + else { + break + } + } + } + name := cur.name() + if name.len == 0 { + return false + } + c.env.lookup_method(name, 'drop') or { return false } + return true +} + +// DropEntry records a scheduled scope-exit destructor call. The codegen +// integration (a follow-up) reads `Checker.drop_schedule` and emits +// `var_name.drop()` immediately before each binding's owning scope ends. +pub struct DropEntry { +pub: + var_name string // local binding to drop + type_name string // displayed type, for diagnostics / debug output + fn_name string // enclosing function (key into drop_schedule) + pos token.Pos // position where the binding became owned +} + +// ownership_schedule_drop records that `name` of type `typ` needs a +// destructor call before its enclosing scope exits. No-op if the type does +// not implement `Drop`. Safe to call multiple times for the same binding — +// only the first registration sticks (later reassignments don't add new +// drop entries, since the old value is overwritten and dropped by the +// assignment-time hook). +fn (mut c Checker) ownership_schedule_drop(name string, typ Type, pos token.Pos) { + if c.ownership_cur_fn.len == 0 { + return + } + if !c.is_drop_type(typ) { + return + } + existing := c.drop_schedule[c.ownership_cur_fn] or { []DropEntry{} } + for entry in existing { + if entry.var_name == name { + return + } + } + mut list := existing.clone() + list << DropEntry{ + var_name: name + type_name: ownership_type_display(typ) + fn_name: c.ownership_cur_fn + pos: pos + } + c.drop_schedule[c.ownership_cur_fn] = list +} + +// ownership_validate_drop_impls verifies that every struct declaring +// `implements Drop` actually provides a `drop(mut self)` method. Runs after +// `preregister_all_fn_signatures` (so methods are visible) and before the +// pending fn bodies are walked, so the diagnostic fires once per build. +fn (mut c Checker) ownership_validate_drop_impls() { + mut to_check := []Struct{} + rlock c.env.scopes { + for _, mod_scope in c.env.scopes { + for _, obj in mod_scope.objects { + if obj is Type { + typ := obj as Type + if typ is Struct { + st := typ as Struct + if c.struct_marker_matches(st, 'Drop') { + to_check << st + } + } + } + } + } + } + for st in to_check { + if !c.type_has_drop_method(Type(st)) { + pos := c.ownership_drop_decl_positions[st.name] or { token.Pos{} } + c.ownership_emit_drop_method_missing(st.name, pos) + } + } +} + +// ownership_emit_drop_method_missing prints the "missing drop method" +// diagnostic. Kept separate so tests / callers can call it directly with a +// synthesized position when needed. +fn (mut c Checker) ownership_emit_drop_method_missing(struct_name string, pos token.Pos) { + if pos.id > 0 { + file := c.file_set.file(pos) + errors.error('struct `${struct_name}` implements `Drop` but does not provide a `drop(mut self)` method', errors.details(file, + file.position(pos), 2), .error, file.position(pos)) + } else { + eprintln('error: struct `${struct_name}` implements `Drop` but does not provide a `drop(mut self)` method') + } + eprintln(' --> add `fn (mut s ${struct_name}) drop() { ... }` to satisfy the Drop contract') + exit(1) } // ownership_is_borrow_or_ref_rhs returns true if the RHS expression is itself diff --git a/vlib/v2/types/checker_ownership_test.v b/vlib/v2/types/checker_ownership_test.v index f13577355..9e0abe7f1 100644 --- a/vlib/v2/types/checker_ownership_test.v +++ b/vlib/v2/types/checker_ownership_test.v @@ -704,3 +704,107 @@ fn main() { assert output.contains('has type `Widget`'), 'diagnostic should name Widget, got: ${output}' assert !output.contains('has type `string`'), 'diagnostic should not say string, got: ${output}' } + +// === Drop interface tests === + +fn test_drop_struct_with_method_ok() { + // A struct that implements Drop and provides a `drop()` method compiles. + code := ' +struct Resource implements Owned, Drop { +mut: + handle int +} + +fn (mut r Resource) drop() { + r.handle = 0 +} + +fn main() { + r := Resource{handle: 42} + println(r.handle) +} +' + exit_code, output := run_ownership_check(code) + assert exit_code == 0, 'Drop with drop() method should compile: ${output}' +} + +fn test_drop_struct_missing_method_errors() { + // `implements Drop` without a `drop()` method must fail with a clear + // diagnostic pointing at the contract. + code := ' +struct Leaky implements Owned, Drop { + handle int +} + +fn main() { + l := Leaky{handle: 1} + println(l.handle) +} +' + exit_code, output := run_ownership_check(code) + assert exit_code != 0, 'missing drop() should fail' + assert output.contains('struct `Leaky` implements `Drop`'), 'got: ${output}' + assert output.contains('drop(mut self)'), 'diagnostic should reference required signature, got: ${output}' +} + +fn test_drop_only_marker_still_requires_method() { + // Drop without Owned still requires drop(); both marker interfaces are + // independent and the Drop contract stands on its own. + code := ' +struct Handle implements Drop { + fd int +} + +fn main() { + h := Handle{fd: 3} + println(h.fd) +} +' + exit_code, output := run_ownership_check(code) + assert exit_code != 0, 'Drop without method should fail even without Owned' + assert output.contains('struct `Handle` implements `Drop`'), 'got: ${output}' +} + +fn test_drop_unrelated_struct_unaffected() { + // A struct with neither Owned nor Drop should keep compiling as before; + // the new validator must not affect unmarked types. + code := ' +struct Plain { + x int +} + +fn main() { + p := Plain{x: 1} + println(p.x) +} +' + exit_code, output := run_ownership_check(code) + assert exit_code == 0, 'unmarked struct must not be affected: ${output}' +} + +fn test_drop_with_move_still_diagnoses_use() { + // A Drop+Owned struct that gets moved still produces the standard + // "use of moved value" diagnostic. Drop scheduling and move tracking + // must coexist on the same binding. + code := ' +struct Conn implements Owned, Drop { +mut: + socket int +} + +fn (mut c Conn) drop() { + c.socket = -1 +} + +fn main() { + c := Conn{socket: 7} + c2 := c + println(c.socket) + _ = c2 +} +' + exit_code, output := run_ownership_check(code) + assert exit_code != 0, 'should fail: c moved into c2' + assert output.contains('use of moved value: `c`'), 'got: ${output}' + assert output.contains('has type `Conn`'), 'got: ${output}' +} diff --git a/vlib/v2/types/universe.v b/vlib/v2/types/universe.v index c350d807e..3915bf739 100644 --- a/vlib/v2/types/universe.v +++ b/vlib/v2/types/universe.v @@ -186,12 +186,21 @@ pub fn init_universe() &Scope { // literal nesting, and pass-by-value to a function all // transfer ownership; reusing the source afterwards is a // compile error. - // Both are empty interfaces (no required methods). + // * `Drop` — explicit "needs cleanup" marker. A struct that implements + // `Drop` MUST provide a `drop(mut self)` method. The checker + // schedules `var.drop()` for every owned, non-moved binding + // of such a type at scope exit (the schedule is exposed for + // codegen via `drop_schedule`). + // All three are empty interfaces except `Drop`, which is satisfied by the + // presence of a `drop(mut self)` method. universe_.insert('Copy', Type(Interface{ name: 'Copy' })) universe_.insert('Owned', Type(Interface{ name: 'Owned' })) + universe_.insert('Drop', Type(Interface{ + name: 'Drop' + })) return universe_ } -- 2.39.5