feat(drop): enforce @[Drop] attribute and add basic move semantics

- Auto-drop now only triggers for user-defined types explicitly marked
  with @[Drop] on the struct declaration.
- Added move tracking in C backend to skip auto-drop for variables
  that have been moved (via let y = x, return x, or assignment).
- Prevents double-free when ownership is transferred.
- Selfhost bootstrap loop verified: C output is deterministic.
This commit is contained in:
2026-06-10 19:08:15 +03:00
parent 7392013301
commit 8f8708c5df
2 changed files with 110 additions and 10 deletions
+103 -6
View File
@@ -86,6 +86,15 @@ struct CEmitter {
defer5: *HirNode, defer5: *HirNode,
defer6: *HirNode, defer6: *HirNode,
defer7: *HirNode, defer7: *HirNode,
movedCount: int,
movedName0: String,
movedName1: String,
movedName2: String,
movedName3: String,
movedName4: String,
movedName5: String,
movedName6: String,
movedName7: String,
} }
func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) { func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) {
@@ -100,16 +109,76 @@ func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) {
cbe.deferCount = cbe.deferCount + 1; cbe.deferCount = cbe.deferCount + 1;
} }
func CBE_AddMoved(cbe: *CEmitter, name: String) {
if cbe.movedCount >= 8 { return; }
if cbe.movedCount == 0 { cbe.movedName0 = name; }
else if cbe.movedCount == 1 { cbe.movedName1 = name; }
else if cbe.movedCount == 2 { cbe.movedName2 = name; }
else if cbe.movedCount == 3 { cbe.movedName3 = name; }
else if cbe.movedCount == 4 { cbe.movedName4 = name; }
else if cbe.movedCount == 5 { cbe.movedName5 = name; }
else if cbe.movedCount == 6 { cbe.movedName6 = name; }
else if cbe.movedCount == 7 { cbe.movedName7 = name; }
cbe.movedCount = cbe.movedCount + 1;
}
func CBE_IsMoved(cbe: *CEmitter, name: String) -> bool {
if cbe.movedCount > 0 && String_Eq(cbe.movedName0, name) { return true; }
if cbe.movedCount > 1 && String_Eq(cbe.movedName1, name) { return true; }
if cbe.movedCount > 2 && String_Eq(cbe.movedName2, name) { return true; }
if cbe.movedCount > 3 && String_Eq(cbe.movedName3, name) { return true; }
if cbe.movedCount > 4 && String_Eq(cbe.movedName4, name) { return true; }
if cbe.movedCount > 5 && String_Eq(cbe.movedName5, name) { return true; }
if cbe.movedCount > 6 && String_Eq(cbe.movedName6, name) { return true; }
if cbe.movedCount > 7 && String_Eq(cbe.movedName7, name) { return true; }
return false;
}
func CBE_RemoveMoved(cbe: *CEmitter, name: String) {
var found: int = -1;
if cbe.movedCount > 0 && String_Eq(cbe.movedName0, name) { found = 0; }
else if cbe.movedCount > 1 && String_Eq(cbe.movedName1, name) { found = 1; }
else if cbe.movedCount > 2 && String_Eq(cbe.movedName2, name) { found = 2; }
else if cbe.movedCount > 3 && String_Eq(cbe.movedName3, name) { found = 3; }
else if cbe.movedCount > 4 && String_Eq(cbe.movedName4, name) { found = 4; }
else if cbe.movedCount > 5 && String_Eq(cbe.movedName5, name) { found = 5; }
else if cbe.movedCount > 6 && String_Eq(cbe.movedName6, name) { found = 6; }
else if cbe.movedCount > 7 && String_Eq(cbe.movedName7, name) { found = 7; }
if found >= 0 {
var i: int = found;
while i < cbe.movedCount - 1 {
if i == 0 { cbe.movedName0 = cbe.movedName1; }
else if i == 1 { cbe.movedName1 = cbe.movedName2; }
else if i == 2 { cbe.movedName2 = cbe.movedName3; }
else if i == 3 { cbe.movedName3 = cbe.movedName4; }
else if i == 4 { cbe.movedName4 = cbe.movedName5; }
else if i == 5 { cbe.movedName5 = cbe.movedName6; }
else if i == 6 { cbe.movedName6 = cbe.movedName7; }
i = i + 1;
}
cbe.movedCount = cbe.movedCount - 1;
}
}
func CBE_GetAutoDropVarName(node: *HirNode) -> String {
if node == null as *HirNode { return ""; }
// node is the inner expression stored by CBE_PushDefer (hCall for auto-drop)
let callNode: *HirNode = node;
if callNode.kind != hCall { return ""; }
let addrNode: *HirNode = callNode.child1;
if addrNode == null as *HirNode { return ""; }
if addrNode.kind != hUnary { return ""; }
if addrNode.intValue != tkAmp { return ""; }
let varNode: *HirNode = addrNode.child1;
if varNode == null as *HirNode { return ""; }
if varNode.kind != hVar { return ""; }
return varNode.strValue;
}
func CBE_EmitDefers(cbe: *CEmitter) -> int { func CBE_EmitDefers(cbe: *CEmitter) -> int {
if cbe.deferCount == 0 { return 0; } if cbe.deferCount == 0 { return 0; }
var i: int = cbe.deferCount - 1; var i: int = cbe.deferCount - 1;
while i >= 0 { while i >= 0 {
StringBuilder_Append(&cbe.sb, "\n");
var sp: int = 0;
while sp < cbe.indent {
StringBuilder_Append(&cbe.sb, " ");
sp = sp + 1;
}
var dn: *HirNode = null as *HirNode; var dn: *HirNode = null as *HirNode;
if i == 0 { dn = cbe.defer0; } if i == 0 { dn = cbe.defer0; }
if i == 1 { dn = cbe.defer1; } if i == 1 { dn = cbe.defer1; }
@@ -119,6 +188,18 @@ func CBE_EmitDefers(cbe: *CEmitter) -> int {
if i == 5 { dn = cbe.defer5; } if i == 5 { dn = cbe.defer5; }
if i == 6 { dn = cbe.defer6; } if i == 6 { dn = cbe.defer6; }
if i == 7 { dn = cbe.defer7; } if i == 7 { dn = cbe.defer7; }
// Skip auto-drop for moved variables
let deferVarName: String = CBE_GetAutoDropVarName(dn);
if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) {
i = i - 1;
continue;
}
StringBuilder_Append(&cbe.sb, "\n");
var sp: int = 0;
while sp < cbe.indent {
StringBuilder_Append(&cbe.sb, " ");
sp = sp + 1;
}
CBE_EmitExpr(cbe, dn); CBE_EmitExpr(cbe, dn);
StringBuilder_Append(&cbe.sb, ";"); StringBuilder_Append(&cbe.sb, ";");
i = i - 1; i = i - 1;
@@ -316,6 +397,10 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
// Return // Return
if kind == hReturn { if kind == hReturn {
// Track moved variables via return
if node.child1 != null as *HirNode && node.child1.kind == hVar {
CBE_AddMoved(cbe, node.child1.strValue);
}
let hadDefers: int = CBE_EmitDefers(cbe); let hadDefers: int = CBE_EmitDefers(cbe);
if hadDefers != 0 { if hadDefers != 0 {
StringBuilder_Append(&cbe.sb, "\n"); StringBuilder_Append(&cbe.sb, "\n");
@@ -345,6 +430,14 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
// Store: combine alloca + value into single declaration // Store: combine alloca + value into single declaration
if kind == hStore { if kind == hStore {
// Track moved variables via assignment/let
if node.child2 != null as *HirNode && node.child2.kind == hVar {
CBE_AddMoved(cbe, node.child2.strValue);
}
// Reinitialization removes moved status
if node.child1 != null as *HirNode && node.child1.kind == hVar {
CBE_RemoveMoved(cbe, node.child1.strValue);
}
if node.child1 != null as *HirNode && node.child1.kind == hAlloca { if node.child1 != null as *HirNode && node.child1.kind == hAlloca {
// Declaration with initializer: Type x = value; // Declaration with initializer: Type x = value;
var ct: String = CBackend_TypeToC(node.child1.intValue); var ct: String = CBackend_TypeToC(node.child1.intValue);
@@ -833,6 +926,8 @@ func CBackend_Generate(mod: *HirModule) -> String {
cbe.sb = StringBuilder_NewCap(8192); cbe.sb = StringBuilder_NewCap(8192);
cbe.indent = 0; cbe.indent = 0;
cbe.mod = mod; cbe.mod = mod;
cbe.deferCount = 0;
cbe.movedCount = 0;
// Header // Header
StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n"); StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n");
@@ -1084,6 +1179,8 @@ func CBackend_Generate(mod: *HirModule) -> String {
StringBuilder_Append(&cbe.sb, " {\n"); StringBuilder_Append(&cbe.sb, " {\n");
// Body // Body
cbe.checkedFunc = mod.funcs[i].checkedFunc; cbe.checkedFunc = mod.funcs[i].checkedFunc;
cbe.deferCount = 0;
cbe.movedCount = 0;
var hasReturn: bool = false; var hasReturn: bool = false;
cbe.indent = 1; cbe.indent = 1;
CBE_EmitExpr(cbe, body); CBE_EmitExpr(cbe, body);
+3
View File
@@ -2264,11 +2264,14 @@ func Lcx_BuildAutoDropFree(ctx: *LowerCtx, typeName: String) -> String {
} }
} }
// User-defined types with @[Drop]: look for TypeName_Drop function // User-defined types with @[Drop]: look for TypeName_Drop function
let typeSym: Symbol = Scope_Lookup(ctx.scope, typeName);
if typeSym.kind == skType && typeSym.decl != null as *Decl && typeSym.decl.isDrop != 0 {
let dropName: String = String_Concat(typeName, "_Drop"); let dropName: String = String_Concat(typeName, "_Drop");
let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName); let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName);
if dropSym.decl != null as *Decl { if dropSym.decl != null as *Decl {
return dropName; return dropName;
} }
}
return ""; return "";
} }