feat(selfhost): auto-drop for interface-based Drop implementations
- Extend Lcx_BuildAutoDropFree to detect TypeName_Drop methods registered by sema for extend Type for Drop impl blocks. - Preserve existing @[Drop] attribute path and move semantics. - Add design doc and implementation plan. - Mark Destructors/Drop roadmap item done in selfhost.
This commit is contained in:
@@ -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<int>` 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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<T> for Drop { func Drop(self: *Box<T>) { ... } }`, 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.
|
||||
Reference in New Issue
Block a user