From 81281cbb1159947514ebdbe8036a5406f4a5c222 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 9 Jun 2026 17:25:28 +0300 Subject: [PATCH] =?UTF-8?q?feat(selfhost):=20@[Checked]=20borrow=20checker?= =?UTF-8?q?=20=E2=80=94=20basic=20write-through-ptr=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/ast.bux | 1 + src/parser.bux | 32 ++++++++++++++++++++++++++++++-- src/sema.bux | 27 +++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/ast.bux b/src/ast.bux index df3d593..4caaa6a 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -250,6 +250,7 @@ struct Decl { column: uint32, isPublic: bool, isAsync: bool, + isChecked: int, // @[Checked] attribute (0/1) // Names strValue: String, // decl name strValue2: String, // interface name, dll name, module path diff --git a/src/parser.bux b/src/parser.bux index c283688..a054fc1 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -1361,14 +1361,42 @@ func parserParseDecl(p: *Parser) -> *Decl { discard parserAdvance(p); } 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); if kind == tkAsync && parserPeek(p, 1) == tkFunc { 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 { - 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 == tkEnum { return parserParseEnumDecl(p, isPublic); } diff --git a/src/sema.bux b/src/sema.bux index b213b3d..0c59df4 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -14,6 +14,9 @@ struct Sema { diags: *SemaDiag; hasError: bool; 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 { @@ -325,6 +328,14 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { let right: int = Sema_CheckExpr(sema, expr.child2); 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 if op == tkAssign || (op >= tkPlusAssign && op <= tkShrAssign) { return left; @@ -360,7 +371,11 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { if kind == ekAssign { let target: int = Sema_CheckExpr(sema, expr.child1); 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; } @@ -895,9 +910,16 @@ func Sema_Analyze(mod: *Module) -> *Sema { if decl.retType != null as *TypeExpr { s.currentRetType = Sema_ResolveType(s, decl.retType); } 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 var stmt: *Stmt = decl.refBody.firstStmt; while stmt != null as *Stmt { @@ -905,6 +927,7 @@ func Sema_Analyze(mod: *Module) -> *Sema { stmt = stmt.nextStmt; } + s.checkedFunc = wasChecked; s.scope = prevScope; } decl = decl.childDecl2;