From cfb89dd72fdc38bf8961fa9e0cd4fc116e618763 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sun, 19 Jul 2026 23:03:05 +0300 Subject: [PATCH] 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 } --- Makefile | 2 +- apps/nexus/src/Http.bux | 6 ++-- apps/nexus/src/Parser.bux | 11 ++---- apps/nexus/src/Server.bux | 5 ++- bootstrap/hir_lower.nim | 74 ++++++++++++++++++++++++++++++++++----- docs/QUALITY_PLAN.md | 2 +- examples/move_field.bux | 29 +++++++++++++++ 7 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 examples/move_field.bux diff --git a/Makefile b/Makefile index 7c7f750..f1ca549 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/apps/nexus/src/Http.bux b/apps/nexus/src/Http.bux index 54c7b84..224ccf3 100644 --- a/apps/nexus/src/Http.bux +++ b/apps/nexus/src/Http.bux @@ -153,13 +153,13 @@ module Http { /// Fallback when only raw bytes are available. pub func RawRequest_WantsKeepAlive(raw: String) -> bool { if String_Contains(raw, "Connection: close") || String_Contains(raw, "connection: close") || - String_Contains(raw, "CONNECTION: CLOSE") { + String_Contains(raw, "CONNECTION: CLOSE") { return false; } if String_Contains(raw, "HTTP/1.0") { if String_Contains(raw, "Connection: keep-alive") || - String_Contains(raw, "Connection: Keep-Alive") || - String_Contains(raw, "connection: keep-alive") { + String_Contains(raw, "Connection: Keep-Alive") || + String_Contains(raw, "connection: keep-alive") { return true; } return false; diff --git a/apps/nexus/src/Parser.bux b/apps/nexus/src/Parser.bux index 7bf0746..69236f1 100644 --- a/apps/nexus/src/Parser.bux +++ b/apps/nexus/src/Parser.bux @@ -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; 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); } diff --git a/apps/nexus/src/Server.bux b/apps/nexus/src/Server.bux index 6b65bba..20446fb 100644 --- a/apps/nexus/src/Server.bux +++ b/apps/nexus/src/Server.bux @@ -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(&req.headers); } else { let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request"); Net_Send(fd, BuildResponse(resp, false)); diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index e7dd5a4..84c5e89 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -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): - body.blockStmts.add(ctx.deferStmts[i]) + 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 diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 2be5d1f..f32c6bd 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -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. --- diff --git a/examples/move_field.bux b/examples/move_field.bux new file mode 100644 index 0000000..b54c2a5 --- /dev/null +++ b/examples/move_field.bux @@ -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; +} + +func MakeBox() -> Box { + var items: Array = Array_New(4); + Array_Push(&items, 10); + Array_Push(&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(&b.items) == 2); + Test_AssertTrue(Array_Get(&b.items, 0) == 10); + Test_AssertTrue(Array_Get(&b.items, 1) == 20); + PrintLine(String_Concat("sum=", String_FromInt( + (Array_Get(&b.items, 0) + Array_Get(&b.items, 1)) as int64))); + Test_Pass("move_field"); + return 0; +}