From 94cc94f6384ff70b7d0506957ed2e85372026954 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sun, 19 Jul 2026 23:07:01 +0300 Subject: [PATCH] feat(selfhost): field-move skip Drop, #line maps, Array field mono Bring selfhost C backend closer to bootstrap ownership and debug quality. - Mark locals moved into struct fields / returns (no double Drop) - Mangle Array/Set/Map/Channel field types so user structs fully emit - Emit #line N from HIR line; BUX_DEBUG_FILE / BUX_NO_LINE controls --- docs/BuildAndTest.md | 7 ++- docs/QUALITY_PLAN.md | 24 ++++++-- src/c_backend.bux | 132 ++++++++++++++++++++++++++++++++++++------- src/hir_lower.bux | 13 ++++- 4 files changed, 148 insertions(+), 28 deletions(-) diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index 97b879a..50f2ed6 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -148,12 +148,13 @@ make bench-nexus # wrk throughput vs apps/nexus /api/health ```bash ./buxc build # -O0 -g, #line → .bux (gdb-friendly; bootstrap) ./buxc build --release # -O2 -DNDEBUG, no #line / -g -# Selfhost (buxc2) same --release / default -O0 -g; #line maps bootstrap-only for now +# Selfhost (buxc2): same --release / default -O0 -g; #line via HIR line numbers +export BUX_DEBUG_FILE=src/Main.bux # path embedded in #line (selfhost) +export BUX_NO_LINE=1 # disable selfhost #line maps export BUX_CFLAGS="-fno-omit-frame-pointer" # optional extra cc flags gdb --args ./build/myapp # (gdb) break Main -# (gdb) run -# (gdb) list # shows Bux source via #line (bootstrap) +# (gdb) list # Bux source via #line ``` diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index f32c6bd..a1aa11b 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 — **field-move skip Drop**, Nexus keep-alive, LSP 0.6, selfhost `-g` +> **Текущо:** v0.5.x — field-move Drop (bootstrap+**selfhost**), selfhost **#line**, Nexus KA, LSP 0.6 > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -14,7 +14,7 @@ | Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ | | HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ | | Selfhost (`src/`) | ~12k LOC, binary-identical loop, closures+tuples | ★★★★★ | -| Gradual ownership | `@[Checked]`, `&`/`&mut`, move, Drop, **lifetime elision** | ★★★★☆ | +| Gradual ownership | `@[Checked]`, move, Drop, elision, **field-move skip Drop** | ★★★★★ | | Concurrency | M:N tasks + channels + async | ★★★★☆ | | Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ | | Tooling | LSP 0.5 hover/def/outline/**refs/rename** + fmt/test/doc | ★★★★★ | @@ -594,9 +594,25 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 37 (compiler: field-move skip auto-Drop) + +1. **Root cause (formal):** auto-Drop of locals that were **moved by value** into + a struct field / let / return still ran → UAF (Nexus `headers` in `HttpRequest`). +2. **`bootstrap/hir_lower.nim`:** + - `movedOutLocals: HashSet[string]` + - `markMovedOutFromAst` on `ekStructInit` fields, `let` init, `return` value + - `shouldSkipDrop` at return / block exit / function tail +3. **Nexus:** removed zeroing workaround; error path still Drops; success path + transfers ownership; `HandleConnection` Drops `req.headers` after response +4. **Example:** `examples/move_field.bux` (Array into `Box { items }`) +5. Verified: move_field PASS; ParseRequest C has Drop only on error path; + `bench-nexus` ~88k RPS; drop_early_return still 5 drops + +--- + ## Следващи стъпки -1. Compiler: skip Drop when local is moved into a struct field / return payload -2. Selfhost `#line` maps (parity with bootstrap LIR backend) +1. Selfhost `#line` maps (parity with bootstrap LIR backend) +2. Selfhost parity for field-move skip Drop (if CBE path differs) 3. Deeper rename (type members / qualified paths) 4. LSP call hierarchy (optional) diff --git a/src/c_backend.bux b/src/c_backend.bux index 909184a..b72016f 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -2,6 +2,7 @@ // Generates C code from the HIR. module CBackend { + extern func bux_getenv(name: String) -> String; // --------------------------------------------------------------------------- // Type → C type name @@ -77,6 +78,79 @@ module CBackend { movedName7: String, tmpCounter: int, currentRetType: String, + // #line debug maps (E.4 selfhost parity) + lastDebugLine: int, + emitDebugLines: bool, + currentFile: String, + } + + /// Normalize monomorphized type spellings for C (Array → Array_int). + func CBE_NormalizeTypeName(name: String) -> String { + if String_Eq(name, "") { return name; } + if String_StartsWith(name, "Array<") && String_EndsWith(name, ">") { + let n: uint = String_Len(name); + // "Array<" + inner + ">" + let inner: String = String_Slice(name, 6, n - 7); + return String_Concat("Array_", inner); + } + if String_StartsWith(name, "Map<") && String_EndsWith(name, ">") { + // Map → Map_int_String (best-effort) + let n: uint = String_Len(name); + let inner: String = String_Slice(name, 4, n - 5); + return String_Concat("Map_", String_ReplaceAll(inner, ",", "_")); + } + if String_StartsWith(name, "Set<") && String_EndsWith(name, ">") { + let n: uint = String_Len(name); + let inner: String = String_Slice(name, 4, n - 5); + return String_Concat("Set_", inner); + } + if String_StartsWith(name, "Channel<") && String_EndsWith(name, ">") { + let n: uint = String_Len(name); + let inner: String = String_Slice(name, 8, n - 9); + return String_Concat("Channel_", inner); + } + return name; + } + + /// Mark droppable locals moved by-value (struct fields / nested). + func CBE_MarkMovedFromNode(cbe: *CEmitter, node: *HirNode) { + if node == null as *HirNode { return; } + if node.kind == hVar { + CBE_AddMoved(cbe, node.strValue); + return; + } + if node.kind == hStructInit { + var field: *HirNode = node.child1; + while field != null as *HirNode { + CBE_MarkMovedFromNode(cbe, field.child1); + field = field.child3; + } + return; + } + if node.kind == hTupleInit { + // child1/child2 + linked extras if any + CBE_MarkMovedFromNode(cbe, node.child1); + CBE_MarkMovedFromNode(cbe, node.child2); + return; + } + } + + func CBE_EmitDebugLine(cbe: *CEmitter, node: *HirNode) { + if !cbe.emitDebugLines { return; } + if node == null as *HirNode { return; } + if node.line == 0 { return; } + let ln: int = node.line as int; + if ln == cbe.lastDebugLine { return; } + cbe.lastDebugLine = ln; + // #line must start at column 0 + StringBuilder_Append(&cbe.sb, "#line "); + StringBuilder_AppendInt(&cbe.sb, ln as int64); + if !String_Eq(cbe.currentFile, "") { + StringBuilder_Append(&cbe.sb, " \""); + StringBuilder_Append(&cbe.sb, cbe.currentFile); + StringBuilder_Append(&cbe.sb, "\""); + } + StringBuilder_Append(&cbe.sb, "\n"); } func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) { @@ -406,9 +480,10 @@ module CBackend { // Return — evaluate value first, then drop live locals, then return. // (Emitting Drop before the value used to use-after-drop on `return a.id`.) if kind == hReturn { - // Track moved variables via return (skip auto-drop of moved-out locals) - if node.child1 != null as *HirNode && node.child1.kind == hVar { - CBE_AddMoved(cbe, node.child1.strValue); + CBE_EmitDebugLine(cbe, node); + // Track moved variables via return / field-move into returned struct + if node.child1 != null as *HirNode { + CBE_MarkMovedFromNode(cbe, node.child1); } if node.child1 != null as *HirNode && cbe.deferCount > 0 { // Materialize into a temp so Drop cannot clobber the returned value. @@ -762,6 +837,8 @@ module CBackend { // Struct init: ((TypeName){.field = value, ...}) if kind == hStructInit { + // Field values taken by value → skip auto-Drop of those locals + CBE_MarkMovedFromNode(cbe, node); StringBuilder_Append(&cbe.sb, "(("); StringBuilder_Append(&cbe.sb, node.strValue); StringBuilder_Append(&cbe.sb, "){"); @@ -792,6 +869,7 @@ module CBackend { child = child.child3; continue; } + CBE_EmitDebugLine(cbe, child); // Indent var sp: int = 0; while sp < cbe.indent { @@ -1158,21 +1236,22 @@ module CBackend { // --------------------------------------------------------------------------- func CBE_IsGenericTypeName(name: String) -> bool { - if String_Eq(name, "T") || String_Eq(name, "K") || String_Eq(name, "V") { return true; } - if String_Eq(name, "T*") || String_Eq(name, "K*") || String_Eq(name, "V*") { return true; } - // Any type name containing '<' is a generic instantiation or parameter - if String_Contains(name, "<") { return true; } - // Generic container structs and their pointer variants - if String_Eq(name, "Array") || String_Eq(name, "Array*") { return true; } - if String_Eq(name, "Channel") || String_Eq(name, "Channel*") { return true; } - if String_Eq(name, "Iter") || String_Eq(name, "Iter*") { return true; } - if String_Eq(name, "Set") || String_Eq(name, "Set*") { return true; } - if String_Eq(name, "SetEntry") || String_Eq(name, "SetEntry*") { return true; } - if String_Eq(name, "Map") || String_Eq(name, "Map*") { return true; } - if String_Eq(name, "MapEntry") || String_Eq(name, "MapEntry*") { return true; } - if String_Eq(name, "StringMap") || String_Eq(name, "StringMap*") { return true; } - if String_Eq(name, "StringMapEntry") || String_Eq(name, "StringMapEntry*") { return true; } - if String_Eq(name, "Slice") || String_Eq(name, "Slice*") { return true; } + let n: String = CBE_NormalizeTypeName(name); + if String_Eq(n, "T") || String_Eq(n, "K") || String_Eq(n, "V") { return true; } + if String_Eq(n, "T*") || String_Eq(n, "K*") || String_Eq(n, "V*") { return true; } + // Bare generic containers (not monomorphized Array_int etc.) + if String_Eq(n, "Array") || String_Eq(n, "Array*") { return true; } + if String_Eq(n, "Channel") || String_Eq(n, "Channel*") { return true; } + if String_Eq(n, "Iter") || String_Eq(n, "Iter*") { return true; } + if String_Eq(n, "Set") || String_Eq(n, "Set*") { return true; } + if String_Eq(n, "SetEntry") || String_Eq(n, "SetEntry*") { return true; } + if String_Eq(n, "Map") || String_Eq(n, "Map*") { return true; } + if String_Eq(n, "MapEntry") || String_Eq(n, "MapEntry*") { return true; } + if String_Eq(n, "StringMap") || String_Eq(n, "StringMap*") { return true; } + if String_Eq(n, "StringMapEntry") || String_Eq(n, "StringMapEntry*") { return true; } + if String_Eq(n, "Slice") || String_Eq(n, "Slice*") { return true; } + // Remaining angle-bracket forms (unresolved generics) + if String_Contains(n, "<") { return true; } return false; } @@ -1234,7 +1313,7 @@ module CBackend { func CBE_StructHasValueStructField(st: *HirStruct) -> bool { var fi: int = 0; while fi < st.fieldCount { - let ft: String = st.fields[fi].typeName; + let ft: String = CBE_NormalizeTypeName(st.fields[fi].typeName); if !CBE_IsPrimitiveTypeName(ft) && !String_EndsWith(ft, "*") { return true; } @@ -1251,7 +1330,7 @@ module CBackend { var fi: int = 0; while fi < st.fieldCount { StringBuilder_Append(&cbe.sb, " "); - var ft: String = st.fields[fi].typeName; + var ft: String = CBE_NormalizeTypeName(st.fields[fi].typeName); if String_Eq(ft, "int") || String_Eq(ft, "") { StringBuilder_Append(&cbe.sb, "int"); } else if String_Eq(ft, "String") { @@ -1325,6 +1404,19 @@ module CBackend { cbe.deferCount = 0; cbe.movedCount = 0; cbe.tmpCounter = 0; + cbe.lastDebugLine = 0; + cbe.emitDebugLines = true; + cbe.currentFile = ""; + // Optional: BUX_DEBUG_FILE sets the #line path for gdb (selfhost has no multi-file loc yet) + let dbgFile: String = bux_getenv("BUX_DEBUG_FILE"); + if dbgFile != null as String && !String_Eq(dbgFile, "") { + cbe.currentFile = dbgFile; + } + // BUX_NO_LINE=1 disables #line maps + let noLine: String = bux_getenv("BUX_NO_LINE"); + if noLine != null as String && !String_Eq(noLine, "") { + cbe.emitDebugLines = false; + } // Header StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n"); diff --git a/src/hir_lower.bux b/src/hir_lower.bux index c78c89c..0a89224 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -4073,7 +4073,18 @@ module HirLower { hm.structs[si].fields[fi].typeName = String_Concat(ftype.pointerPointee.typeName, "*"); } } else if !String_Eq(ftype.typeName, "") { - hm.structs[si].fields[fi].typeName = ftype.typeName; + // Monomorphize container fields: Array → Array_int + // so C backend does not skip the parent struct as "generic". + var tn: String = ftype.typeName; + if ftype.typeArgCount >= 1 && !String_Eq(ftype.typeArgName0, "") { + if String_Eq(tn, "Array") || String_Eq(tn, "Set") || + String_Eq(tn, "Channel") || String_Eq(tn, "Iter") { + tn = String_Concat(String_Concat(tn, "_"), ftype.typeArgName0); + } else if String_Eq(tn, "Map") && ftype.typeArgCount >= 2 { + tn = String_Concat(String_Concat(String_Concat("Map_", ftype.typeArgName0), "_"), ftype.typeArgName1); + } + } + hm.structs[si].fields[fi].typeName = tn; } } fi = fi + 1;