feat(selfhost): @[Checked] borrow checker — basic write-through-ptr detection

- Add isChecked field to Decl struct (ast.bux)
- Parse @[Checked] attribute in parserParseDecl (parser.bux)
- Add checkedFunc flag and movedVars to Sema struct (sema.bux)
- Enable borrow checking per-function in Sema_Analyze
- Reject assignment through raw pointer (*T) in @[Checked] functions
- Skip newlines between @[Checked] attribute and the declaration keyword
  to avoid parser seeing the attribute and function as separate decls
This commit is contained in:
2026-06-09 17:25:28 +03:00
parent c14578d9dd
commit 81281cbb11
3 changed files with 56 additions and 4 deletions
+1
View File
@@ -250,6 +250,7 @@ struct Decl {
column: uint32, column: uint32,
isPublic: bool, isPublic: bool,
isAsync: bool, isAsync: bool,
isChecked: int, // @[Checked] attribute (0/1)
// Names // Names
strValue: String, // decl name strValue: String, // decl name
strValue2: String, // interface name, dll name, module path strValue2: String, // interface name, dll name, module path
+30 -2
View File
@@ -1361,14 +1361,42 @@ func parserParseDecl(p: *Parser) -> *Decl {
discard parserAdvance(p); discard parserAdvance(p);
} }
let isPublic: bool = parserMatch(p, tkPub); let isPublic: bool = parserMatch(p, tkPub);
// Parse @[Checked] attribute
var isChecked: int = 0;
if parserCheck(p, tkAt) {
discard parserAdvance(p); // @
if parserCheck(p, tkLBracket) {
discard parserAdvance(p); // [
if parserCheck(p, tkIdent) {
let attrName: LexToken = parserCurToken(p);
if String_Eq(attrName.text, "Checked") {
isChecked = 1;
}
discard parserAdvance(p); // attribute name
}
if parserCheck(p, tkRBracket) {
discard parserAdvance(p); // ]
}
}
// Skip newlines after attribute before the declaration
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
discard parserAdvance(p);
}
}
let kind: int = parserPeek(p, 0); let kind: int = parserPeek(p, 0);
if kind == tkAsync && parserPeek(p, 1) == tkFunc { if kind == tkAsync && parserPeek(p, 1) == tkFunc {
discard parserAdvance(p); // async discard parserAdvance(p); // async
return parserParseFuncDecl(p, isPublic, false, true); let d: *Decl = parserParseFuncDecl(p, isPublic, false, true);
d.isChecked = isChecked;
return d;
} }
if kind == tkFunc { if kind == tkFunc {
return parserParseFuncDecl(p, isPublic, false, false); let d: *Decl = parserParseFuncDecl(p, isPublic, false, false);
d.isChecked = isChecked;
return d;
} }
if kind == tkStruct { return parserParseStructDecl(p, isPublic); } if kind == tkStruct { return parserParseStructDecl(p, isPublic); }
if kind == tkEnum { return parserParseEnumDecl(p, isPublic); } if kind == tkEnum { return parserParseEnumDecl(p, isPublic); }
+24 -1
View File
@@ -14,6 +14,9 @@ struct Sema {
diags: *SemaDiag; diags: *SemaDiag;
hasError: bool; hasError: bool;
currentRetType: int; // return type of the function being checked currentRetType: int; // return type of the function being checked
checkedFunc: bool; // true inside @[Checked] function
movedCount: int; // number of moved variables
movedNames: *String; // array of moved variable names
} }
struct SemaDiag { struct SemaDiag {
@@ -325,6 +328,14 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
let right: int = Sema_CheckExpr(sema, expr.child2); let right: int = Sema_CheckExpr(sema, expr.child2);
let op: int = expr.intValue; let op: int = expr.intValue;
// Borrow check: reject assignment through deref in @[Checked] functions
if sema.checkedFunc && 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");
}
}
// Assignment operators return target type // Assignment operators return target type
if op == tkAssign || (op >= tkPlusAssign && op <= tkShrAssign) { if op == tkAssign || (op >= tkPlusAssign && op <= tkShrAssign) {
return left; return left;
@@ -360,7 +371,11 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
if kind == ekAssign { if kind == ekAssign {
let target: int = Sema_CheckExpr(sema, expr.child1); let target: int = Sema_CheckExpr(sema, expr.child1);
let value: int = Sema_CheckExpr(sema, expr.child2); let value: int = Sema_CheckExpr(sema, expr.child2);
// Basic type compatibility (permissive for now) // 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 {
Sema_EmitError(sema, expr.line, expr.column,
"cannot assign through raw pointer in checked function — use '&mut T' instead");
}
return target; return target;
} }
@@ -896,8 +911,15 @@ func Sema_Analyze(mod: *Module) -> *Sema {
s.currentRetType = Sema_ResolveType(s, decl.retType); s.currentRetType = Sema_ResolveType(s, decl.retType);
} else { } else {
s.currentRetType = tyVoid; s.currentRetType = tyVoid;
s.checkedFunc = false;
s.movedCount = 0;
s.movedNames = bux_alloc(64 as uint * 256) as *String; // up to 64 moved var names
} }
// Enable borrow checking for @[Checked] functions
let wasChecked: bool = s.checkedFunc;
s.checkedFunc = decl.isChecked != 0;
// Check body statements // Check body statements
var stmt: *Stmt = decl.refBody.firstStmt; var stmt: *Stmt = decl.refBody.firstStmt;
while stmt != null as *Stmt { while stmt != null as *Stmt {
@@ -905,6 +927,7 @@ func Sema_Analyze(mod: *Module) -> *Sema {
stmt = stmt.nextStmt; stmt = stmt.nextStmt;
} }
s.checkedFunc = wasChecked;
s.scope = prevScope; s.scope = prevScope;
} }
decl = decl.childDecl2; decl = decl.childDecl2;