feat(release): add @[Release] attribute and --release CLI flag

- @[Release] disables borrow checking and bounds checking in the function
- --release passes -O3 -flto to the C compiler for optimized builds
- Selfhost loop still deterministic
This commit is contained in:
2026-06-11 09:39:11 +03:00
parent 224542df7f
commit 290cbc8f98
5 changed files with 66 additions and 24 deletions
+1
View File
@@ -275,6 +275,7 @@ struct Decl {
isAsync: bool,
isChecked: int, // @[Checked] attribute (0/1)
isDrop: int, // @[Drop] attribute (0/1)
isRelease: int, // @[Release] attribute (0/1)
// Names
strValue: String, // decl name
strValue2: String, // interface name, dll name, module path
+42 -15
View File
@@ -190,11 +190,11 @@ func Cli_Compile(source: String, sourceName: String, targetTriple: String) -> St
// Build command
// ---------------------------------------------------------------------------
func Cli_Build(srcPath: String, outPath: String, targetTriple: String) -> int {
func Cli_Build(srcPath: String, outPath: String, targetTriple: String, isRelease: bool) -> int {
// If building the standard project entry point, use full project build
// which merges stdlib and supports multi-file projects
if String_Eq(srcPath, "src/Main.bux") || String_Eq(srcPath, "src/main.bux") {
let rc: int = Cli_BuildProject(".", targetTriple);
let rc: int = Cli_BuildProject(".", targetTriple, isRelease);
if rc == 0 && !String_Eq(outPath, "build/main") {
// Rename output if custom path was requested
bux_system(String_Concat(String_Concat("mv build/main ", outPath), " 2>/dev/null || true"));
@@ -248,12 +248,20 @@ func Cli_Build(srcPath: String, outPath: String, targetTriple: String) -> int {
// Compile with cc or clang for cross-compilation
PrintLine("Compiling C...");
var cmdBuf: StringBuilder = StringBuilder_NewCap(512);
var optFlags: String = "-O2";
if isRelease {
optFlags = "-O3 -flto";
}
if !String_Eq(targetTriple, "") {
StringBuilder_Append(&cmdBuf, "clang -O2 -pthread -target ");
StringBuilder_Append(&cmdBuf, "clang ");
StringBuilder_Append(&cmdBuf, optFlags);
StringBuilder_Append(&cmdBuf, " -pthread -target ");
StringBuilder_Append(&cmdBuf, targetTriple);
StringBuilder_Append(&cmdBuf, " ");
} else {
StringBuilder_Append(&cmdBuf, "cc -O2 -pthread ");
StringBuilder_Append(&cmdBuf, "cc ");
StringBuilder_Append(&cmdBuf, optFlags);
StringBuilder_Append(&cmdBuf, " -pthread ");
}
StringBuilder_Append(&cmdBuf, "-o ");
StringBuilder_Append(&cmdBuf, outPath);
@@ -860,7 +868,7 @@ func Cli_Test(projectDir: String) -> int {
Print("Testing project: ");
PrintLine(projectDir);
// Build the project first
let rc: int = Cli_BuildProject(projectDir, "");
let rc: int = Cli_BuildProject(projectDir, "", false);
if rc != 0 {
PrintLine("Test build failed");
return rc;
@@ -886,7 +894,7 @@ func Cli_Test(projectDir: String) -> int {
return result;
}
func Cli_BuildProject(projectDir: String, targetTriple: String) -> int {
func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool) -> int {
let man: Manifest = Manifest_Load(bux_path_join(projectDir, "bux.toml"));
var outName: String = man.name;
if String_Eq(outName, "") {
@@ -1147,12 +1155,20 @@ func Cli_BuildProject(projectDir: String, targetTriple: String) -> int {
PrintLine("Compiling C...");
let outBin: String = bux_path_join(buildDir, outName);
var ccBuf: StringBuilder = StringBuilder_NewCap(512);
var optFlags2: String = "-O2";
if isRelease {
optFlags2 = "-O3 -flto";
}
if !String_Eq(targetTriple, "") {
StringBuilder_Append(&ccBuf, "clang -O2 -pthread -target ");
StringBuilder_Append(&ccBuf, "clang ");
StringBuilder_Append(&ccBuf, optFlags2);
StringBuilder_Append(&ccBuf, " -pthread -target ");
StringBuilder_Append(&ccBuf, targetTriple);
StringBuilder_Append(&ccBuf, " ");
} else {
StringBuilder_Append(&ccBuf, "cc -O2 -pthread ");
StringBuilder_Append(&ccBuf, "cc ");
StringBuilder_Append(&ccBuf, optFlags2);
StringBuilder_Append(&ccBuf, " -pthread ");
}
StringBuilder_Append(&ccBuf, "-o ");
StringBuilder_Append(&ccBuf, outBin);
@@ -1184,8 +1200,8 @@ func Cli_BuildProject(projectDir: String, targetTriple: String) -> int {
// Run command — build project and execute
// ---------------------------------------------------------------------------
func Cli_RunProject(projectDir: String, targetTriple: String) -> int {
let rc: int = Cli_BuildProject(projectDir, targetTriple);
func Cli_RunProject(projectDir: String, targetTriple: String, isRelease: bool) -> int {
let rc: int = Cli_BuildProject(projectDir, targetTriple, isRelease);
if rc != 0 {
return rc;
}
@@ -1205,8 +1221,9 @@ func Cli_RunProject(projectDir: String, targetTriple: String) -> int {
// ---------------------------------------------------------------------------
func Cli_Run(args: *String, argCount: int) -> int {
/* Scan for --target flag before processing command */
/* Scan for --target and --release flags before processing command */
var targetTriple: String = "";
var isRelease: bool = false;
var ai: int = 1;
while ai < argCount {
if String_Eq(args[ai], "--target") && ai + 1 < argCount {
@@ -1218,7 +1235,17 @@ func Cli_Run(args: *String, argCount: int) -> int {
j = j + 1;
}
argCount = argCount - 2;
break;
ai = ai - 1;
} else if String_Eq(args[ai], "--release") {
isRelease = true;
/* Remove --release from args by shifting */
var j: int = ai;
while j + 1 < argCount {
args[j] = args[j + 1];
j = j + 1;
}
argCount = argCount - 1;
ai = ai - 1;
}
ai = ai + 1;
}
@@ -1301,13 +1328,13 @@ func Cli_Run(args: *String, argCount: int) -> int {
let out: String = "build/main";
if argCount >= 3 { src = args[2]; }
if argCount >= 4 { out = args[3]; }
return Cli_Build(src, out, targetTriple);
return Cli_Build(src, out, targetTriple, isRelease);
}
if String_Eq(cmd, "project") {
let dir: String = ".";
if argCount >= 3 { dir = args[2]; }
return Cli_BuildProject(dir, targetTriple);
return Cli_BuildProject(dir, targetTriple, isRelease);
}
if String_Eq(cmd, "test") {
@@ -1319,7 +1346,7 @@ func Cli_Run(args: *String, argCount: int) -> int {
if String_Eq(cmd, "run") {
let dir: String = ".";
if argCount >= 3 { dir = args[2]; }
return Cli_RunProject(dir, targetTriple);
return Cli_RunProject(dir, targetTriple, isRelease);
}
Print("Unknown command: ");
+6 -2
View File
@@ -33,6 +33,7 @@ struct LowerCtx {
substArg1: String,
// Borrow checker state
checkedFunc: bool,
releaseFunc: bool,
}
// ---------------------------------------------------------------------------
@@ -775,7 +776,7 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
// Index: arr[idx]
if kind == ekIndex {
// In @[Checked] functions, Array access goes through Array_Get with bounds check
if ctx.checkedFunc && expr.child1 != null as *Expr && Lcx_IsArrayTypeExpr(expr.child1.refType) {
if ctx.checkedFunc && !ctx.releaseFunc && expr.child1 != null as *Expr && Lcx_IsArrayTypeExpr(expr.child1.refType) {
let elemType: String = Lcx_GetArrayElemType(expr.child1.refType);
if !String_Eq(elemType, "") {
let baseNode: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
@@ -906,7 +907,7 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
if kind == ekAssign {
// Array bounds-checking for write in @[Checked]: arr[idx] = val → Array_Set_T(&arr, idx, val)
// Only for plain assignment (=), not compound operators (+=, -=, etc.)
if expr.intValue == tkAssign && ctx.checkedFunc && expr.child1 != null as *Expr && expr.child1.kind == ekIndex && expr.child1.child1 != null as *Expr && Lcx_IsArrayTypeExpr(expr.child1.child1.refType) {
if expr.intValue == tkAssign && ctx.checkedFunc && !ctx.releaseFunc && expr.child1 != null as *Expr && expr.child1.kind == ekIndex && expr.child1.child1 != null as *Expr && Lcx_IsArrayTypeExpr(expr.child1.child1.refType) {
let elemType: String = Lcx_GetArrayElemType(expr.child1.child1.refType);
if !String_Eq(elemType, "") {
let baseNode: *HirNode = Lcx_LowerExpr(ctx, expr.child1.child1);
@@ -1944,6 +1945,8 @@ func Lcx_LowerParam(out: *HirParam, p: *Param, ctx: *LowerCtx) {
func Lcx_LowerFunc(ctx: *LowerCtx, decl: *Decl) -> *HirFunc {
let oldChecked: bool = ctx.checkedFunc;
ctx.checkedFunc = decl.isChecked != 0;
let oldRelease: bool = ctx.releaseFunc;
ctx.releaseFunc = decl.isRelease != 0;
let f: *HirFunc = bux_alloc(sizeof(HirFunc)) as *HirFunc;
f.name = decl.strValue;
@@ -2029,6 +2032,7 @@ func Lcx_LowerFunc(ctx: *LowerCtx, decl: *Decl) -> *HirFunc {
}
ctx.scope = prevScope;
ctx.checkedFunc = oldChecked;
ctx.releaseFunc = oldRelease;
return f;
}
+7 -1
View File
@@ -1571,9 +1571,10 @@ func parserParseDecl(p: *Parser) -> *Decl {
}
let isPublic: bool = parserMatch(p, tkPub);
// Parse @[Checked] / @[Drop] attribute
// Parse @[Checked] / @[Drop] / @[Release] attribute
var isChecked: int = 0;
var isDrop: int = 0;
var isRelease: int = 0;
if parserCheck(p, tkAt) {
discard parserAdvance(p); // @
if parserCheck(p, tkLBracket) {
@@ -1586,6 +1587,9 @@ func parserParseDecl(p: *Parser) -> *Decl {
if String_Eq(attrName.text, "Drop") {
isDrop = 1;
}
if String_Eq(attrName.text, "Release") {
isRelease = 1;
}
discard parserAdvance(p); // attribute name
}
if parserCheck(p, tkRBracket) {
@@ -1604,11 +1608,13 @@ func parserParseDecl(p: *Parser) -> *Decl {
discard parserAdvance(p); // async
let d: *Decl = parserParseFuncDecl(p, isPublic, false, true);
d.isChecked = isChecked;
d.isRelease = isRelease;
return d;
}
if kind == tkFunc {
let d: *Decl = parserParseFuncDecl(p, isPublic, false, false);
d.isChecked = isChecked;
d.isRelease = isRelease;
return d;
}
if kind == tkStruct {
+10 -6
View File
@@ -20,6 +20,7 @@ struct Sema {
hasError: bool;
currentRetType: int; // return type of the function being checked
checkedFunc: bool; // true inside @[Checked] function
releaseFunc: bool; // true inside @[Release] function
movedCount: int; // number of moved variables
movedName0: String; // inline moved var names (up to 8)
movedName1: String;
@@ -424,7 +425,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
// Identifier — look up in scope
if kind == ekIdent {
// Borrow check: use-after-move in @[Checked] functions
if sema.checkedFunc && Sema_IsMoved(sema, expr.strValue) {
if sema.checkedFunc && !sema.releaseFunc && Sema_IsMoved(sema, expr.strValue) {
Sema_EmitError(sema, expr.line, expr.column,
String_Concat("use of moved value '", String_Concat(expr.strValue, "'")));
}
@@ -473,7 +474,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
let op: int = expr.intValue;
// Borrow check: reject assignment through deref in @[Checked] functions
if sema.checkedFunc && op == tkAssign {
if sema.checkedFunc && !sema.releaseFunc && op == tkAssign {
if expr.child1 != null as *Expr && expr.child1.kind == ekUnary && expr.child1.intValue == tkStar {
Sema_EmitError(sema, expr.line, expr.column,
"cannot assign through raw pointer in checked function — use '&mut T' instead");
@@ -481,7 +482,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
}
// Borrow check: reinitialization after move
if sema.checkedFunc && op == tkAssign {
if sema.checkedFunc && !sema.releaseFunc && op == tkAssign {
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
Sema_RemoveMoved(sema, expr.child1.strValue);
}
@@ -567,7 +568,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
let target: int = Sema_CheckExpr(sema, expr.child1);
let value: int = Sema_CheckExpr(sema, expr.child2);
// Borrow check: reject assignment through deref in @[Checked] functions
if sema.checkedFunc && expr.child1 != null as *Expr && expr.child1.kind == ekUnary && expr.child1.intValue == tkStar {
if sema.checkedFunc && !sema.releaseFunc && expr.child1 != null as *Expr && expr.child1.kind == ekUnary && expr.child1.intValue == tkStar {
Sema_EmitError(sema, expr.line, expr.column,
"cannot assign through raw pointer in checked function — use '&mut T' instead");
}
@@ -584,7 +585,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
arg = arg.next;
}
// Borrow check: reject double mutable borrow in @[Checked] functions
if sema.checkedFunc {
if sema.checkedFunc && !sema.releaseFunc {
var a: *ExprList = expr.callArgs;
var ai: int = 0;
while a != null as *ExprList {
@@ -853,7 +854,7 @@ func Sema_CheckStmt(sema: *Sema, stmt: *Stmt) {
if kind == skLet {
let initType: int = Sema_CheckExpr(sema, stmt.child1);
// Borrow check: move tracking
if sema.checkedFunc && stmt.child1 != null as *Expr && stmt.child1.kind == ekIdent {
if sema.checkedFunc && !sema.releaseFunc && stmt.child1 != null as *Expr && stmt.child1.kind == ekIdent {
Sema_AddMoved(sema, stmt.child1.strValue);
}
// Register variable in scope
@@ -1515,6 +1516,8 @@ func Sema_Analyze(mod: *Module) -> *Sema {
// Enable borrow checking for @[Checked] functions
let wasChecked: bool = s.checkedFunc;
s.checkedFunc = decl.isChecked != 0;
let wasRelease: bool = s.releaseFunc;
s.releaseFunc = decl.isRelease != 0;
// Check body statements
var stmt: *Stmt = decl.refBody.firstStmt;
@@ -1524,6 +1527,7 @@ func Sema_Analyze(mod: *Module) -> *Sema {
}
s.checkedFunc = wasChecked;
s.releaseFunc = wasRelease;
s.scope = prevScope;
}
decl = decl.childDecl2;