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:
@@ -3,7 +3,7 @@ SRC := bootstrap/main.nim
|
||||
OUT := buxc
|
||||
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
|
||||
|
||||
|
||||
@@ -100,8 +100,8 @@ module Parser {
|
||||
|
||||
// Find header/body boundary
|
||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||
// Only one allocation path — avoid Array_New then overwrite (leak) and
|
||||
// suppress auto-drop after moving into HttpRequest (use-after-free).
|
||||
// Single assignment path (no Array_New then overwrite). Field-move of
|
||||
// `headers` into HttpRequest skips auto-Drop (compiler movedOutLocals).
|
||||
var headers: Array<HeaderEntry>;
|
||||
var body: String = "";
|
||||
if String_Len(boundary) > 0 {
|
||||
@@ -116,7 +116,7 @@ module Parser {
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -127,11 +127,6 @@ module Parser {
|
||||
body: body,
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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::String::{String_Len, String_StartsWith};
|
||||
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||
import Std::Array::{Array_Drop};
|
||||
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 Parser::{ParseRequest};
|
||||
import Router::{Router, Router_Dispatch};
|
||||
@@ -95,6 +96,8 @@ module Server {
|
||||
}
|
||||
let resp: HttpResponse = Router_Dispatch(router, req);
|
||||
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 {
|
||||
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
|
||||
Net_Send(fd, BuildResponse(resp, false));
|
||||
|
||||
+64
-8
@@ -35,6 +35,9 @@ type
|
||||
patternBoundNames*: HashSet[string]
|
||||
## Active renames: source pattern name → unique C local (for shadowing)
|
||||
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 =
|
||||
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(dropBase, typeArgs)
|
||||
|
||||
proc dropTargetsVar(n: HirNode, name: string): bool =
|
||||
## True if n is Type_Drop(&name) / collection Drop of that local.
|
||||
if n == nil or name.len == 0: return false
|
||||
proc dropTargetName(n: HirNode): string =
|
||||
## Local name targeted by Type_Drop(&name), or "".
|
||||
if n == nil: return ""
|
||||
if n.kind == hCall and n.callArgs.len >= 1:
|
||||
let a = n.callArgs[0]
|
||||
if a != nil and a.kind == hUnary and a.unaryOp == tkAmp and
|
||||
a.unaryOperand != nil and a.unaryOperand.kind == hVar:
|
||||
return a.unaryOperand.varName == name
|
||||
return false
|
||||
return a.unaryOperand.varName
|
||||
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 =
|
||||
## 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.patternBoundNames = initHashSet[string]()
|
||||
result.patternRenames = initTable[string, string]()
|
||||
result.movedOutLocals = initHashSet[string]()
|
||||
|
||||
proc sanitizeFatPart(s: string): string =
|
||||
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)
|
||||
|
||||
of ekStructInit:
|
||||
# Field values are taken by value → move ownership out of droppable locals
|
||||
ctx.markMovedOutFromAst(expr)
|
||||
var structName = expr.exprStructInitName
|
||||
if expr.exprStructInitTypeArgs.len > 0:
|
||||
var suffix = ""
|
||||
@@ -1787,6 +1831,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
if initHir != nil:
|
||||
let store = hirStore(varNode, initHir, loc)
|
||||
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
|
||||
let dropName = ctx.autoDropFuncName(allocaType)
|
||||
if dropName.len > 0:
|
||||
@@ -1798,6 +1845,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
return hirBlock(stmts, nil, makeVoid(), loc)
|
||||
|
||||
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
|
||||
var stmts = ctx.pendingStmts
|
||||
ctx.pendingStmts = @[]
|
||||
@@ -1805,6 +1855,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
var skipDrop = ""
|
||||
if value != nil and value.kind == hVar:
|
||||
skipDrop = value.varName
|
||||
ctx.markMovedOutLocal(value.varName)
|
||||
# Materialize the return value BEFORE drops so `return a.id` is not
|
||||
# use-after-drop (drops are separate stmts; LIR evaluates return expr last).
|
||||
var retVal = value
|
||||
@@ -1817,7 +1868,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
retVal = hirVar(tmp, retTy, loc)
|
||||
# Add defers in reverse order (LIFO); snapshot full stack for every return path
|
||||
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(hirReturn(retVal, 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)
|
||||
expr = last.blockExpr
|
||||
# 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 = ""
|
||||
if expr != nil and expr.kind == hVar:
|
||||
skipDrop = expr.varName
|
||||
ctx.markMovedOutLocal(expr.varName)
|
||||
if ctx.deferStmts.len > 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])
|
||||
ctx.deferStmts.setLen(deferBase)
|
||||
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.patternRenames = initTable[string, string]()
|
||||
let oldDefers = ctx.deferStmts
|
||||
let oldMovedOut = ctx.movedOutLocals
|
||||
ctx.deferStmts = @[]
|
||||
ctx.movedOutLocals = initHashSet[string]()
|
||||
# Add parameters to varTypeExprs after clearing so they are visible in the body.
|
||||
for p in funcParams:
|
||||
if p.ptype != nil:
|
||||
@@ -2194,8 +2248,10 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
|
||||
hasReturn = true
|
||||
if not hasReturn:
|
||||
for i in countdown(ctx.deferStmts.len - 1, 0):
|
||||
if not ctx.shouldSkipDrop(ctx.deferStmts[i], ""):
|
||||
body.blockStmts.add(ctx.deferStmts[i])
|
||||
ctx.deferStmts = oldDefers
|
||||
ctx.movedOutLocals = oldMovedOut
|
||||
|
||||
ctx.currentFuncDecl = oldFuncDecl
|
||||
ctx.currentFuncRetType = oldFuncRetType
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||
|
||||
> **Дата:** 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user