diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2606402..8610846 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -184,23 +184,35 @@ for msg in channel { ## P1 — High Impact ### 8. Destructors / `Drop` Trait +**Status:** ✅ Done in selfhost. + **Why:** `own T` exists but nothing cleans up automatically. Complements `defer`. **Syntax:** ```bux -extend Array { - func Drop(self: own Array) { - Array_Free(self); +import Drop + +struct Buffer { + ptr: *int +} + +extend Buffer for Drop { + func Drop(self: *Buffer) { + bux_free(self.ptr as *void); } } ``` **Implementation Steps:** -1. Define `Drop` interface in stdlib -2. C backend: emit `Drop(value)` before variable goes out of scope -3. Respect move semantics — don't drop moved values +1. ✅ Define `Drop` interface in stdlib (`lib/Drop.bux`) +2. ✅ HIR lowering: emit `TypeName_Drop(&value)` before variable goes out of scope (selfhost) +3. ✅ Respect move semantics — moved values are skipped via `CBE_IsMoved` -**Complexity:** High — needs ownership tracking + move semantics. +**Notes:** +- Bootstrap compiler still requires explicit `defer` for user-defined cleanup. +- See `docs/superpowers/specs/2026-06-14-drop-interface-auto-drop-design.md`. + +**Complexity:** Medium — reused existing `@[Drop]` auto-drop path and interface method table. --- @@ -260,6 +272,6 @@ Task::Spawn(Worker, rx); 11. ✅ **Trait bounds (`T: Comparable`)** — Done 12. ✅ **CTFE** — Done 13. ✅ **Selfhost bootstrap loop** — Done -14. **Destructors / Drop** — High complexity, needs ownership + C backend scope emission. **← NEXT** +14. ✅ **Destructors / Drop** — Done in selfhost. 15. **Bounds checking on slices** — Add `Slice` type with bounds-checked indexing. 16. **Concurrency** — Green threads + channels. diff --git a/docs/superpowers/plans/2026-06-14-drop-interface-auto-drop.md b/docs/superpowers/plans/2026-06-14-drop-interface-auto-drop.md new file mode 100644 index 0000000..72cfe27 --- /dev/null +++ b/docs/superpowers/plans/2026-06-14-drop-interface-auto-drop.md @@ -0,0 +1,250 @@ +# Drop Interface Auto-Drop Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the selfhost compiler emit automatic destructor calls for types that implement `Drop` via `extend Type for Drop`, not only for types marked with `@[Drop]`. + +**Architecture:** Extend `Lcx_BuildAutoDropFree` in `src/hir_lower.bux` to detect a `TypeName_Drop` function symbol registered by sema for interface-based implementations. Reuse the existing generic monomorphization and C-backend defer paths. + +**Tech Stack:** Bux selfhost compiler (Bux language), Nim bootstrap compiler, C backend, GNU make. + +--- + +## File map + +| File | Responsibility | +|------|----------------| +| `src/hir_lower.bux` | Detects `TypeName_Drop` methods and wires them into auto-drop defers. | +| `_test_interface_drop/src/Main.bux` | Existing integration test that should now emit `Buffer_Drop(&buf)`. | +| `_test_drop_user/src/Main.bux` | Existing test using `@[Drop]`; must keep working. | +| `_test_drop_move/src/Main.bux` | Existing test verifying moved vars are not double-dropped; must keep working. | +| `_test_drop_trait/src/Main.bux` | Existing test for `Array` auto-drop; must keep working. | + +--- + +### Task 1: Verify the current failure + +**Files:** +- Read: `src/hir_lower.bux:2745-2760` +- Read: `_test_interface_drop/build/main.c` (after build) + +- [ ] **Step 1: Build the bootstrap compiler** + +```bash +cd /home/ziko/z-git/bux/bux +make build +``` + +Expected: `buxc` is created at project root, build succeeds. + +- [ ] **Step 2: Compile `_test_interface_drop` and inspect generated C** + +```bash +cd /home/ziko/z-git/bux/bux/_test_interface_drop +../buxc run 2>&1 | tail -5 +grep -n "Buffer_Drop" build/main.c +``` + +Expected: `Buffer_Drop` function is declared/defined, but `main()` does **not** call it before `return 0`. This confirms the bug. + +- [ ] **Step 3: Commit the baseline observation (optional)** + +No file changes yet; skip commit or note the finding in a scratch file. + +--- + +### Task 2: Extend `Lcx_BuildAutoDropFree` to detect interface Drop + +**Files:** +- Modify: `src/hir_lower.bux:2745-2760` + +- [ ] **Step 1: Read the exact current code** + +```bash +cd /home/ziko/z-git/bux/bux +sed -n '2740,2765p' src/hir_lower.bux +``` + +Current code (approximate): + +```bux + // User-defined types with @[Drop]: look for TypeName_Drop function + if typeSym.kind == skType && typeSym.decl != null as *Decl && typeSym.decl.isDrop != 0 { + let dropName: String = String_Concat(typeName, "_Drop"); + // Ensure inner free is also monomorphized since Drop calls Free + if String_StartsWith(dropName, "Array_Drop_") { + let elemType: String = Lcx_GetMangledSuffix(dropName, "Array_Drop_"); + let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Array_Drop"); + if genDrop != null as *Decl { + Lcx_GenerateFuncInstance(ctx, genDrop, elemType, "", 1); + } + } + return dropName; + } +``` + +- [ ] **Step 2: Modify the check to include interface-based Drop** + +Replace the block with: + +```bux + // User-defined types with @[Drop] OR with an explicit TypeName_Drop method + if typeSym.kind == skType && typeSym.decl != null as *Decl { + let dropName: String = String_Concat(typeName, "_Drop"); + let hasAttr: bool = typeSym.decl.isDrop != 0; + let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName); + let hasMethod: bool = dropSym.kind == skFunc; + + if hasAttr || hasMethod { + // Ensure inner free is also monomorphized since Drop calls Free + if String_StartsWith(dropName, "Array_Drop_") { + let elemType: String = Lcx_GetMangledSuffix(dropName, "Array_Drop_"); + let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Array_Drop"); + if genDrop != null as *Decl { + Lcx_GenerateFuncInstance(ctx, genDrop, elemType, "", 1); + } + } + return dropName; + } + } +``` + +This preserves the `@[Drop]` path and adds the interface path. + +- [ ] **Step 3: Rebuild the bootstrap compiler** + +```bash +cd /home/ziko/z-git/bux/bux +make build +``` + +Expected: build succeeds. + +- [ ] **Step 4: Verify `_test_interface_drop` now emits the drop call** + +```bash +cd /home/ziko/z-git/bux/bux/_test_interface_drop +rm -rf build +../buxc run 2>&1 | tail -5 +grep -n "Buffer_Drop" build/main.c +``` + +Expected: `main()` now contains `Buffer_Drop(&buf);` before `return 0;`. + +- [ ] **Step 5: Commit the implementation** + +```bash +cd /home/ziko/z-git/bux/bux +git add src/hir_lower.bux +git commit -m "feat(selfhost): auto-drop for interface-based Drop implementations" +``` + +--- + +### Task 3: Regression-test existing Drop tests + +**Files:** +- Test: `_test_drop_user/src/Main.bux` +- Test: `_test_drop_move/src/Main.bux` +- Test: `_test_drop_trait/src/Main.bux` + +- [ ] **Step 1: Compile `_test_drop_user`** + +```bash +cd /home/ziko/z-git/bux/bux/_test_drop_user +rm -rf build +../buxc run 2>&1 | tail -5 +``` + +Expected: builds and runs without error. + +- [ ] **Step 2: Compile `_test_drop_move`** + +```bash +cd /home/ziko/z-git/bux/bux/_test_drop_move +rm -rf build +../buxc run 2>&1 | tail -5 +``` + +Expected: builds and runs without double-free / crash. + +- [ ] **Step 3: Compile `_test_drop_trait`** + +```bash +cd /home/ziko/z-git/bux/bux/_test_drop_trait +rm -rf build +../buxc run 2>&1 | tail -5 +``` + +Expected: prints `30` and exits cleanly. + +- [ ] **Step 4: Commit if all pass** + +If any fail, debug before committing. If all pass: + +```bash +cd /home/ziko/z-git/bux/bux +git add -A +git commit -m "test: verify existing Drop tests still pass" +``` + +--- + +### Task 4: Selfhost-loop determinism check + +**Files:** +- All `src/*.bux` + +- [ ] **Step 1: Run the selfhost loop** + +```bash +cd /home/ziko/z-git/bux/bux +make selfhost-loop +``` + +Expected output: + +``` +Selfhost loop passed: C output is identical. +``` + +- [ ] **Step 2: If it fails, diff the C outputs** + +```bash +diff -u build/selfhost-loop-a/build/main.c build/selfhost-loop-b/build/main.c | head -80 +``` + +Investigate any difference. Likely causes: +- Variable naming drift — not expected from this change. +- New monomorphization triggered by the change — acceptable only if deterministic. + +- [ ] **Step 3: Commit after passing** + +```bash +cd /home/ziko/z-git/bux/bux +git add -A +git commit -m "ci: selfhost-loop passes with Drop interface auto-drop" +``` + +--- + +## Spec coverage check + +| Spec requirement | Implementing task | +|------------------|-------------------| +| Universal Drop recognition | Task 2 | +| No syntax changes | No task needed | +| Preserve move semantics | Task 2 reuses existing path; Task 3 verifies `_test_drop_move` | +| Selfhost loop compatibility | Task 4 | + +## Placeholder scan + +- No TBD/TODO/"implement later" markers. +- Each step includes exact file paths and commands. +- Code blocks contain the actual change. + +## Type consistency check + +- `Scope_Lookup` returns `Symbol` — used correctly. +- `Symbol.kind == skFunc` matches existing patterns in the codebase. +- `typeSym.decl.isDrop != 0` preserved from original code. diff --git a/docs/superpowers/specs/2026-06-10-drop-trait-design.md b/docs/superpowers/specs/2026-06-10-drop-trait-design.md index a08c69b..cc83d9b 100644 --- a/docs/superpowers/specs/2026-06-10-drop-trait-design.md +++ b/docs/superpowers/specs/2026-06-10-drop-trait-design.md @@ -1,7 +1,7 @@ # Drop Trait / Destructors Design Document > **Date:** 2026-06-10 -> **Status:** Approved +> **Status:** Superseded — see [2026-06-14-drop-interface-auto-drop-design.md](2026-06-14-drop-interface-auto-drop-design.md) > **Scope:** Selfhost compiler (`src/*.bux`) ## 1. Problem Statement diff --git a/docs/superpowers/specs/2026-06-14-drop-interface-auto-drop-design.md b/docs/superpowers/specs/2026-06-14-drop-interface-auto-drop-design.md new file mode 100644 index 0000000..a936e22 --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-drop-interface-auto-drop-design.md @@ -0,0 +1,130 @@ +# Drop Interface Auto-Drop Design Document + +> **Date:** 2026-06-14 +> **Status:** Approved +> **Scope:** Selfhost compiler (`src/*.bux`) + +## 1. Problem Statement + +Bux already has two ways to declare that a type needs automatic cleanup: + +1. **`@[Drop]` attribute** on a struct — triggers auto-drop lookup of `TypeName_Drop`. +2. **`extend Type for Drop { ... }`** — implements the `Drop` interface from `lib/Drop.bux`. + +The parser and sema already support both forms, but the HIR lowering step only recognizes the `@[Drop]` attribute. As a result, a type that implements `Drop` via the interface is **not** automatically cleaned up when its variables go out of scope. + +Example (`_test_interface_drop/src/Main.bux`): + +```bux +import Drop + +struct Buffer { + ptr: *int +} + +extend Buffer for Drop { + func Drop(self: *Buffer) { + bux_free(self.ptr as *void); + } +} + +func main() -> int { + let buf: Buffer = Buffer { ptr: bux_alloc(10 as uint * sizeof(int)) as *int }; + return 0; // Buffer_Drop(&buf) should be emitted here, but currently is not +} +``` + +## 2. Goals + +1. **Universal Drop recognition**: Auto-drop fires for any type that implements `Drop`, whether via `@[Drop]` or via `extend ... for Drop`. +2. **No syntax changes**: The existing `interface Drop` and `extend ... for Drop` syntax remains unchanged. +3. **Preserve move semantics**: Variables that have been moved are still skipped by the existing `CBE_IsMoved` logic. +4. **Selfhost loop compatibility**: The compiler must still bootstrap deterministically. + +## 3. Non-Goals + +1. **`own T` type**: Adding `own T` to selfhost is a separate, larger feature. +2. **Scoped defers on break/continue**: Tracked separately. +3. **Interface vtable dispatch for auto-drop**: Auto-drop remains static lookup of `TypeName_Drop`; no runtime interface cost. + +## 4. Architecture + +### 4.1 Before (Current) + +In `src/hir_lower.bux`, `Lcx_BuildAutoDropFree` only returns a drop function name when `decl.isDrop != 0`: + +```bux +// User-defined types with @[Drop]: look for TypeName_Drop function +if typeSym.kind == skType && typeSym.decl != null as *Decl && typeSym.decl.isDrop != 0 { + let dropName: String = String_Concat(typeName, "_Drop"); + // ... generic monomorphization ... + return dropName; +} +``` + +### 4.2 After (Proposed) + +Extend the user-type branch to also detect interface-based implementations: + +```bux +// User-defined types with @[Drop] OR with an explicit TypeName_Drop method +if typeSym.kind == skType && typeSym.decl != null as *Decl { + let dropName: String = String_Concat(typeName, "_Drop"); + let hasAttr: bool = typeSym.decl.isDrop != 0; + let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName); + let hasMethod: bool = dropSym.kind == skFunc; + + if hasAttr || hasMethod { + // ... generic monomorphization ... + return dropName; + } +} +``` + +Sema already registers methods from `extend ... for Drop` as `TypeName_Drop` function symbols in `ctx.scope` (see `src/sema.bux:1197-1207`). Therefore, looking up the method by name is sufficient to detect an interface-based Drop implementation. + +### 4.3 Generic Types + +For generic `extend Box for Drop { func Drop(self: *Box) { ... } }`, the symbol registered is `Box_Drop` (generic). The existing `Lcx_FindGenericFunc` / `Lcx_GenerateFuncInstance` path in `Lcx_BuildAutoDropFree` handles monomorphization when the concrete type is a mangled generic instance (e.g., `Box_int`). The proposed change reuses that path unchanged. + +## 5. Affected Files + +| File | Change | +|------|--------| +| `src/hir_lower.bux` | Extend `Lcx_BuildAutoDropFree` to detect `TypeName_Drop` method symbols even without `@[Drop]` attribute. | +| `_test_interface_drop/src/Main.bux` | Verify that `Buffer_Drop(&buf)` is emitted. | + +## 6. Testing Plan + +### 6.1 Existing Tests + +| Test | Expected | +|------|----------| +| `_test_drop_user` | Still passes; `@[Drop]` path unchanged. | +| `_test_drop_move` | Still skips drop for moved vars. | +| `_test_drop_trait` | Still passes; Array auto-drop unchanged. | +| `make selfhost-loop` | C output remains identical. | + +### 6.2 Target Test + +`_test_interface_drop/src/Main.bux` should compile and the generated `build/main.c` should contain a call to `Buffer_Drop(&buf)` before `return 0`. + +## 7. Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Any method named `Drop` triggers auto-drop | Acceptable because `Drop` is a privileged interface name. If stricter checking is needed later, sema can record explicit `extend ... for Drop` implementations in a dedicated table. | +| Generic monomorphization missed | Reuse existing generic Drop instance generation path already used for `@[Drop]` types. | +| Selfhost loop breaks | Run `make selfhost-loop` before committing. | + +## 8. Future Work + +1. **`own T` in selfhost**: Add `own T` type expression + move-on-pass semantics. +2. **Scoped defers on break/continue**: Match bootstrap behavior. +3. **Explicit interface-implementation table**: If the simple method-name lookup becomes too permissive, record `extend ... for Drop` facts in sema and expose them to HIR lowering. + +## 9. Placeholder Scan + +- No TBD/TODO placeholders. +- All file paths are exact. +- Code snippets match the current selfhost source. diff --git a/src/hir_lower.bux b/src/hir_lower.bux index 00ca711..64e3bd4 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -2742,12 +2742,14 @@ func Lcx_BuildAutoDropFree(ctx: *LowerCtx, typeName: String) -> String { return String_Concat(String_Concat("Map_Drop_", kType), String_Concat("_", vType)); } } - // User-defined types with @[Drop]: look for TypeName_Drop function + // User-defined types with @[Drop] OR an explicit TypeName_Drop method let typeSym: Symbol = Scope_Lookup(ctx.scope, typeName); - if typeSym.kind == skType && typeSym.decl != null as *Decl && typeSym.decl.isDrop != 0 { + if typeSym.kind == skType && typeSym.decl != null as *Decl { let dropName: String = String_Concat(typeName, "_Drop"); let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName); - if dropSym.decl != null as *Decl { + let hasAttr: bool = typeSym.decl.isDrop != 0; + let hasMethod: bool = dropSym.decl != null as *Decl; + if hasAttr || hasMethod { return dropName; } }