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
This commit is contained in:
2026-07-19 23:07:01 +03:00
parent cfb89dd72f
commit 94cc94f638
4 changed files with 148 additions and 28 deletions
+4 -3
View File
@@ -148,12 +148,13 @@ make bench-nexus # wrk throughput vs apps/nexus /api/health
```bash ```bash
./buxc build # -O0 -g, #line → .bux (gdb-friendly; bootstrap) ./buxc build # -O0 -g, #line → .bux (gdb-friendly; bootstrap)
./buxc build --release # -O2 -DNDEBUG, no #line / -g ./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 export BUX_CFLAGS="-fno-omit-frame-pointer" # optional extra cc flags
gdb --args ./build/myapp gdb --args ./build/myapp
# (gdb) break Main # (gdb) break Main
# (gdb) run # (gdb) list # Bux source via #line
# (gdb) list # shows Bux source via #line (bootstrap)
``` ```
+20 -4
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 — **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. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
--- ---
@@ -14,7 +14,7 @@
| Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ | | Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ |
| HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ | | HIR → C | Tuples + fat `func` ABI в bootstrap **и** selfhost | ★★★★☆ |
| Selfhost (`src/`) | ~12k LOC, binary-identical loop, closures+tuples | ★★★★★ | | 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 | ★★★★☆ | | Concurrency | M:N tasks + channels + async | ★★★★☆ |
| Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ | | Stdlib | Array/Map/Set/String/Iter HOF разширени | ★★★★☆ |
| Tooling | LSP 0.5 hover/def/outline/**refs/rename** + fmt/test/doc | ★★★★★ | | 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 1. Selfhost `#line` maps (parity with bootstrap LIR backend)
2. 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) 3. Deeper rename (type members / qualified paths)
4. LSP call hierarchy (optional) 4. LSP call hierarchy (optional)
+112 -20
View File
@@ -2,6 +2,7 @@
// Generates C code from the HIR. // Generates C code from the HIR.
module CBackend { module CBackend {
extern func bux_getenv(name: String) -> String;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Type → C type name // Type → C type name
@@ -77,6 +78,79 @@ module CBackend {
movedName7: String, movedName7: String,
tmpCounter: int, tmpCounter: int,
currentRetType: String, currentRetType: String,
// #line debug maps (E.4 selfhost parity)
lastDebugLine: int,
emitDebugLines: bool,
currentFile: String,
}
/// Normalize monomorphized type spellings for C (Array<int> → 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<int,String> → 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) { func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) {
@@ -406,9 +480,10 @@ module CBackend {
// Return — evaluate value first, then drop live locals, then return. // Return — evaluate value first, then drop live locals, then return.
// (Emitting Drop before the value used to use-after-drop on `return a.id`.) // (Emitting Drop before the value used to use-after-drop on `return a.id`.)
if kind == hReturn { if kind == hReturn {
// Track moved variables via return (skip auto-drop of moved-out locals) CBE_EmitDebugLine(cbe, node);
if node.child1 != null as *HirNode && node.child1.kind == hVar { // Track moved variables via return / field-move into returned struct
CBE_AddMoved(cbe, node.child1.strValue); if node.child1 != null as *HirNode {
CBE_MarkMovedFromNode(cbe, node.child1);
} }
if node.child1 != null as *HirNode && cbe.deferCount > 0 { if node.child1 != null as *HirNode && cbe.deferCount > 0 {
// Materialize into a temp so Drop cannot clobber the returned value. // Materialize into a temp so Drop cannot clobber the returned value.
@@ -762,6 +837,8 @@ module CBackend {
// Struct init: ((TypeName){.field = value, ...}) // Struct init: ((TypeName){.field = value, ...})
if kind == hStructInit { 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, "((");
StringBuilder_Append(&cbe.sb, node.strValue); StringBuilder_Append(&cbe.sb, node.strValue);
StringBuilder_Append(&cbe.sb, "){"); StringBuilder_Append(&cbe.sb, "){");
@@ -792,6 +869,7 @@ module CBackend {
child = child.child3; child = child.child3;
continue; continue;
} }
CBE_EmitDebugLine(cbe, child);
// Indent // Indent
var sp: int = 0; var sp: int = 0;
while sp < cbe.indent { while sp < cbe.indent {
@@ -1158,21 +1236,22 @@ module CBackend {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func CBE_IsGenericTypeName(name: String) -> bool { func CBE_IsGenericTypeName(name: String) -> bool {
if String_Eq(name, "T") || String_Eq(name, "K") || String_Eq(name, "V") { return true; } let n: String = CBE_NormalizeTypeName(name);
if String_Eq(name, "T*") || String_Eq(name, "K*") || String_Eq(name, "V*") { return true; } if String_Eq(n, "T") || String_Eq(n, "K") || String_Eq(n, "V") { return true; }
// Any type name containing '<' is a generic instantiation or parameter if String_Eq(n, "T*") || String_Eq(n, "K*") || String_Eq(n, "V*") { return true; }
if String_Contains(name, "<") { return true; } // Bare generic containers (not monomorphized Array_int etc.)
// Generic container structs and their pointer variants if String_Eq(n, "Array") || String_Eq(n, "Array*") { return true; }
if String_Eq(name, "Array") || String_Eq(name, "Array*") { return true; } if String_Eq(n, "Channel") || String_Eq(n, "Channel*") { return true; }
if String_Eq(name, "Channel") || String_Eq(name, "Channel*") { return true; } if String_Eq(n, "Iter") || String_Eq(n, "Iter*") { return true; }
if String_Eq(name, "Iter") || String_Eq(name, "Iter*") { return true; } if String_Eq(n, "Set") || String_Eq(n, "Set*") { return true; }
if String_Eq(name, "Set") || String_Eq(name, "Set*") { return true; } if String_Eq(n, "SetEntry") || String_Eq(n, "SetEntry*") { return true; }
if String_Eq(name, "SetEntry") || String_Eq(name, "SetEntry*") { return true; } if String_Eq(n, "Map") || String_Eq(n, "Map*") { return true; }
if String_Eq(name, "Map") || String_Eq(name, "Map*") { return true; } if String_Eq(n, "MapEntry") || String_Eq(n, "MapEntry*") { return true; }
if String_Eq(name, "MapEntry") || String_Eq(name, "MapEntry*") { return true; } if String_Eq(n, "StringMap") || String_Eq(n, "StringMap*") { return true; }
if String_Eq(name, "StringMap") || String_Eq(name, "StringMap*") { return true; } if String_Eq(n, "StringMapEntry") || String_Eq(n, "StringMapEntry*") { return true; }
if String_Eq(name, "StringMapEntry") || String_Eq(name, "StringMapEntry*") { return true; } if String_Eq(n, "Slice") || String_Eq(n, "Slice*") { return true; }
if String_Eq(name, "Slice") || String_Eq(name, "Slice*") { return true; } // Remaining angle-bracket forms (unresolved generics)
if String_Contains(n, "<") { return true; }
return false; return false;
} }
@@ -1234,7 +1313,7 @@ module CBackend {
func CBE_StructHasValueStructField(st: *HirStruct) -> bool { func CBE_StructHasValueStructField(st: *HirStruct) -> bool {
var fi: int = 0; var fi: int = 0;
while fi < st.fieldCount { 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, "*") { if !CBE_IsPrimitiveTypeName(ft) && !String_EndsWith(ft, "*") {
return true; return true;
} }
@@ -1251,7 +1330,7 @@ module CBackend {
var fi: int = 0; var fi: int = 0;
while fi < st.fieldCount { while fi < st.fieldCount {
StringBuilder_Append(&cbe.sb, " "); 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, "") { if String_Eq(ft, "int") || String_Eq(ft, "") {
StringBuilder_Append(&cbe.sb, "int"); StringBuilder_Append(&cbe.sb, "int");
} else if String_Eq(ft, "String") { } else if String_Eq(ft, "String") {
@@ -1325,6 +1404,19 @@ module CBackend {
cbe.deferCount = 0; cbe.deferCount = 0;
cbe.movedCount = 0; cbe.movedCount = 0;
cbe.tmpCounter = 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 // Header
StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n"); StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n");
+12 -1
View File
@@ -4073,7 +4073,18 @@ module HirLower {
hm.structs[si].fields[fi].typeName = String_Concat(ftype.pointerPointee.typeName, "*"); hm.structs[si].fields[fi].typeName = String_Concat(ftype.pointerPointee.typeName, "*");
} }
} else if !String_Eq(ftype.typeName, "") { } else if !String_Eq(ftype.typeName, "") {
hm.structs[si].fields[fi].typeName = ftype.typeName; // Monomorphize container fields: Array<int> → 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; fi = fi + 1;