fix(compiler): skip auto-Drop after field/let/return moves

Track locals moved by value into struct fields, let bindings, or return
values so Array/Drop types are not freed while still owned by the target.

- hir_lower: movedOutLocals + markMovedOutFromAst + shouldSkipDrop
- Nexus: drop zeroing workaround; free headers after each request
- examples/move_field.bux regression for Box { items: arr }
This commit is contained in:
2026-07-19 23:03:05 +03:00
parent adc5c743a6
commit cfb89dd72f
7 changed files with 106 additions and 23 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ SRC := bootstrap/main.nim
OUT := buxc OUT := buxc
BUILD_DIR := build BUILD_DIR := build
EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked drop_early_return lifetime_elision ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked drop_early_return lifetime_elision ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow move_field
.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf .PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf
+3 -8
View File
@@ -100,8 +100,8 @@ module Parser {
// Find header/body boundary // Find header/body boundary
let boundary: String = bux_strstr(raw, "\r\n\r\n"); let boundary: String = bux_strstr(raw, "\r\n\r\n");
// Only one allocation path — avoid Array_New then overwrite (leak) and // Single assignment path (no Array_New then overwrite). Field-move of
// suppress auto-drop after moving into HttpRequest (use-after-free). // `headers` into HttpRequest skips auto-Drop (compiler movedOutLocals).
var headers: Array<HeaderEntry>; var headers: Array<HeaderEntry>;
var body: String = ""; var body: String = "";
if String_Len(boundary) > 0 { if String_Len(boundary) > 0 {
@@ -116,7 +116,7 @@ module Parser {
} }
if String_Eq(path, "") { if String_Eq(path, "") {
// auto-drop of `headers` runs on return // auto-drop of `headers` runs on error return
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest }); return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
} }
@@ -127,11 +127,6 @@ module Parser {
body: body, body: body,
headers: headers, headers: headers,
}; };
// Ownership transferred into req — zero local shell so auto-drop is a no-op.
// (Compiler does not yet treat field-move as a move-out of the local.)
headers.data = null as *HeaderEntry;
headers.len = 0;
headers.cap = 0;
return ParseResult_NewOk(req); return ParseResult_NewOk(req);
} }
+4 -1
View File
@@ -4,8 +4,9 @@ module Server {
import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
import Std::String::{String_Len, String_StartsWith}; import Std::String::{String_Len, String_StartsWith};
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv}; import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
import Std::Array::{Array_Drop};
import Config::{ServerConfig}; import Config::{ServerConfig};
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, Request_WantsKeepAlive}; import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, Request_WantsKeepAlive, HeaderEntry};
import Errors::{ParseResult}; import Errors::{ParseResult};
import Parser::{ParseRequest}; import Parser::{ParseRequest};
import Router::{Router, Router_Dispatch}; import Router::{Router, Router_Dispatch};
@@ -95,6 +96,8 @@ module Server {
} }
let resp: HttpResponse = Router_Dispatch(router, req); let resp: HttpResponse = Router_Dispatch(router, req);
Net_Send(fd, BuildResponse(resp, keepAlive)); Net_Send(fd, BuildResponse(resp, keepAlive));
// Free header buffer (moved into req at parse; no Drop on nested fields)
Array_Drop<HeaderEntry>(&req.headers);
} else { } else {
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request"); let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
Net_Send(fd, BuildResponse(resp, false)); Net_Send(fd, BuildResponse(resp, false));
+64 -8
View File
@@ -35,6 +35,9 @@ type
patternBoundNames*: HashSet[string] patternBoundNames*: HashSet[string]
## Active renames: source pattern name → unique C local (for shadowing) ## Active renames: source pattern name → unique C local (for shadowing)
patternRenames*: Table[string, string] patternRenames*: Table[string, string]
## Locals whose value was moved into another owner (struct field, let, return).
## Auto-Drop is skipped for these (session 37 — field-move ownership).
movedOutLocals*: HashSet[string]
proc freshName(ctx: var LowerCtx): string = proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter inc ctx.varCounter
@@ -57,15 +60,53 @@ proc ensureDropMono(ctx: var LowerCtx, dropBase: string, freeBase: string, typeA
discard ctx.generateMethodInstance(freeBase, typeArgs) discard ctx.generateMethodInstance(freeBase, typeArgs)
discard ctx.generateMethodInstance(dropBase, typeArgs) discard ctx.generateMethodInstance(dropBase, typeArgs)
proc dropTargetsVar(n: HirNode, name: string): bool = proc dropTargetName(n: HirNode): string =
## True if n is Type_Drop(&name) / collection Drop of that local. ## Local name targeted by Type_Drop(&name), or "".
if n == nil or name.len == 0: return false if n == nil: return ""
if n.kind == hCall and n.callArgs.len >= 1: if n.kind == hCall and n.callArgs.len >= 1:
let a = n.callArgs[0] let a = n.callArgs[0]
if a != nil and a.kind == hUnary and a.unaryOp == tkAmp and if a != nil and a.kind == hUnary and a.unaryOp == tkAmp and
a.unaryOperand != nil and a.unaryOperand.kind == hVar: a.unaryOperand != nil and a.unaryOperand.kind == hVar:
return a.unaryOperand.varName == name return a.unaryOperand.varName
return false return ""
proc dropTargetsVar(n: HirNode, name: string): bool =
## True if n is Type_Drop(&name) / collection Drop of that local.
name.len > 0 and dropTargetName(n) == name
proc hasPendingDrop(ctx: LowerCtx, name: string): bool =
if name.len == 0: return false
for d in ctx.deferStmts:
if dropTargetsVar(d, name): return true
false
proc markMovedOutLocal(ctx: var LowerCtx, name: string) =
## Record that `name` no longer owns its heap (moved into another value).
if name.len > 0 and ctx.hasPendingDrop(name):
ctx.movedOutLocals.incl(name)
proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
## Mark droppable locals used by-value in ownership-taking contexts.
if expr == nil: return
case expr.kind
of ekIdent:
ctx.markMovedOutLocal(expr.exprIdent)
of ekStructInit:
for f in expr.exprStructInitFields:
ctx.markMovedOutFromAst(f.value)
of ekTuple:
for e in expr.exprTupleElements:
ctx.markMovedOutFromAst(e)
else:
discard
proc shouldSkipDrop(ctx: LowerCtx, dropNode: HirNode, skipName: string): bool =
## Skip Drop for explicit skipName or any moved-out local.
let target = dropTargetName(dropNode)
if target.len == 0: return false
if skipName.len > 0 and target == skipName: return true
if target in ctx.movedOutLocals: return true
false
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string = proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string =
## Return `Type_Drop` if this type should be auto-dropped, else "". ## Return `Type_Drop` if this type should be auto-dropped, else "".
@@ -474,6 +515,7 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.seenFatTypes = @[] result.seenFatTypes = @[]
result.patternBoundNames = initHashSet[string]() result.patternBoundNames = initHashSet[string]()
result.patternRenames = initTable[string, string]() result.patternRenames = initTable[string, string]()
result.movedOutLocals = initHashSet[string]()
proc sanitizeFatPart(s: string): string = proc sanitizeFatPart(s: string): string =
result = s.replace("const char*", "cstr").replace("unsigned int", "uint") result = s.replace("const char*", "cstr").replace("unsigned int", "uint")
@@ -1420,6 +1462,8 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
typ: makeVoid(), loc: loc) typ: makeVoid(), loc: loc)
of ekStructInit: of ekStructInit:
# Field values are taken by value → move ownership out of droppable locals
ctx.markMovedOutFromAst(expr)
var structName = expr.exprStructInitName var structName = expr.exprStructInitName
if expr.exprStructInitTypeArgs.len > 0: if expr.exprStructInitTypeArgs.len > 0:
var suffix = "" var suffix = ""
@@ -1787,6 +1831,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if initHir != nil: if initHir != nil:
let store = hirStore(varNode, initHir, loc) let store = hirStore(varNode, initHir, loc)
stmts.add(store) stmts.add(store)
# Move: `let a = b` takes ownership of droppable local `b`
if stmt.stmtLetInit != nil:
ctx.markMovedOutFromAst(stmt.stmtLetInit)
# Auto-Drop: @[Drop] types and Array/Map/etc. with TypeName_Drop # Auto-Drop: @[Drop] types and Array/Map/etc. with TypeName_Drop
let dropName = ctx.autoDropFuncName(allocaType) let dropName = ctx.autoDropFuncName(allocaType)
if dropName.len > 0: if dropName.len > 0:
@@ -1798,6 +1845,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
return hirBlock(stmts, nil, makeVoid(), loc) return hirBlock(stmts, nil, makeVoid(), loc)
of skReturn: of skReturn:
# Mark moves before lowering so struct-field moves are recorded
if stmt.stmtReturnValue != nil:
ctx.markMovedOutFromAst(stmt.stmtReturnValue)
let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil
var stmts = ctx.pendingStmts var stmts = ctx.pendingStmts
ctx.pendingStmts = @[] ctx.pendingStmts = @[]
@@ -1805,6 +1855,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
var skipDrop = "" var skipDrop = ""
if value != nil and value.kind == hVar: if value != nil and value.kind == hVar:
skipDrop = value.varName skipDrop = value.varName
ctx.markMovedOutLocal(value.varName)
# Materialize the return value BEFORE drops so `return a.id` is not # Materialize the return value BEFORE drops so `return a.id` is not
# use-after-drop (drops are separate stmts; LIR evaluates return expr last). # use-after-drop (drops are separate stmts; LIR evaluates return expr last).
var retVal = value var retVal = value
@@ -1817,7 +1868,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
retVal = hirVar(tmp, retTy, loc) retVal = hirVar(tmp, retTy, loc)
# Add defers in reverse order (LIFO); snapshot full stack for every return path # Add defers in reverse order (LIFO); snapshot full stack for every return path
for i in countdown(ctx.deferStmts.len - 1, 0): for i in countdown(ctx.deferStmts.len - 1, 0):
if not dropTargetsVar(ctx.deferStmts[i], skipDrop): if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i]) stmts.add(ctx.deferStmts[i])
stmts.add(hirReturn(retVal, loc)) stmts.add(hirReturn(retVal, loc))
return hirBlock(stmts, nil, makeVoid(), loc) return hirBlock(stmts, nil, makeVoid(), loc)
@@ -2116,13 +2167,14 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
stmts[^1] = hirBlock(last.blockStmts, nil, makeVoid(), last.loc) stmts[^1] = hirBlock(last.blockStmts, nil, makeVoid(), last.loc)
expr = last.blockExpr expr = last.blockExpr
# Scope exit: Drop locals introduced in this block (not outer ones). # Scope exit: Drop locals introduced in this block (not outer ones).
# Skip Drop for a local that is the block result (move into expr / caller). # Skip Drop for block result and any moved-out locals (field / let / return move).
var skipDrop = "" var skipDrop = ""
if expr != nil and expr.kind == hVar: if expr != nil and expr.kind == hVar:
skipDrop = expr.varName skipDrop = expr.varName
ctx.markMovedOutLocal(expr.varName)
if ctx.deferStmts.len > deferBase: if ctx.deferStmts.len > deferBase:
for i in countdown(ctx.deferStmts.len - 1, deferBase): for i in countdown(ctx.deferStmts.len - 1, deferBase):
if not dropTargetsVar(ctx.deferStmts[i], skipDrop): if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i]) stmts.add(ctx.deferStmts[i])
ctx.deferStmts.setLen(deferBase) ctx.deferStmts.setLen(deferBase)
let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid() let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid()
@@ -2174,7 +2226,9 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
ctx.patternBoundNames = initHashSet[string]() ctx.patternBoundNames = initHashSet[string]()
ctx.patternRenames = initTable[string, string]() ctx.patternRenames = initTable[string, string]()
let oldDefers = ctx.deferStmts let oldDefers = ctx.deferStmts
let oldMovedOut = ctx.movedOutLocals
ctx.deferStmts = @[] ctx.deferStmts = @[]
ctx.movedOutLocals = initHashSet[string]()
# Add parameters to varTypeExprs after clearing so they are visible in the body. # Add parameters to varTypeExprs after clearing so they are visible in the body.
for p in funcParams: for p in funcParams:
if p.ptype != nil: if p.ptype != nil:
@@ -2194,8 +2248,10 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
hasReturn = true hasReturn = true
if not hasReturn: if not hasReturn:
for i in countdown(ctx.deferStmts.len - 1, 0): for i in countdown(ctx.deferStmts.len - 1, 0):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], ""):
body.blockStmts.add(ctx.deferStmts[i]) body.blockStmts.add(ctx.deferStmts[i])
ctx.deferStmts = oldDefers ctx.deferStmts = oldDefers
ctx.movedOutLocals = oldMovedOut
ctx.currentFuncDecl = oldFuncDecl ctx.currentFuncDecl = oldFuncDecl
ctx.currentFuncRetType = oldFuncRetType ctx.currentFuncRetType = oldFuncRetType
+1 -1
View File
@@ -1,7 +1,7 @@
# Bux — План към „добър“ език (v0.5 → v1.0) # Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-19 > **Дата:** 2026-07-19
> **Текущо:** v0.5.x — Nexus keep-alive, **header Array ownership fix**, LSP 0.6 workspace/symbol, selfhost `-g` > **Текущо:** v0.5.x — **field-move skip Drop**, Nexus keep-alive, LSP 0.6, selfhost `-g`
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
--- ---
+29
View File
@@ -0,0 +1,29 @@
// Field-move ownership: Array moved into a struct must not be auto-dropped.
import Std::Io::{PrintLine};
import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get};
import Std::String::{String_FromInt, String_Concat};
import Std::Test::{Test_AssertTrue, Test_Pass};
struct Box {
items: Array<int>;
}
func MakeBox() -> Box {
var items: Array<int> = Array_New<int>(4);
Array_Push<int>(&items, 10);
Array_Push<int>(&items, 20);
// Move `items` into the field — compiler skips Drop of `items`
let b: Box = Box { items: items };
return b;
}
func Main() -> int {
let b: Box = MakeBox();
Test_AssertTrue(Array_Len<int>(&b.items) == 2);
Test_AssertTrue(Array_Get<int>(&b.items, 0) == 10);
Test_AssertTrue(Array_Get<int>(&b.items, 1) == 20);
PrintLine(String_Concat("sum=", String_FromInt(
(Array_Get<int>(&b.items, 0) + Array_Get<int>(&b.items, 1)) as int64)));
Test_Pass("move_field");
return 0;
}