diff --git a/_test_borrow2/bux.toml b/_test_borrow2/bux.toml new file mode 100644 index 0000000..0bbf836 --- /dev/null +++ b/_test_borrow2/bux.toml @@ -0,0 +1 @@ +[Package] diff --git a/_test_borrow2/src/Main.bux b/_test_borrow2/src/Main.bux new file mode 100644 index 0000000..36d1e14 --- /dev/null +++ b/_test_borrow2/src/Main.bux @@ -0,0 +1,38 @@ +import Std::Io::{PrintLine}; + +@[Checked] +func Take(x: own String) { + PrintLine(x); +} + +@[Checked] +func UseAfterMoveReinit() { + var s: own String = "hello"; + Take(s); + /* s is moved here */ + /* PrintLine(s); */ /* would error */ + s = "reinitialized"; + /* s is valid again after reinitialization */ + PrintLine(s); +} + +@[Checked] +func MoveInAssign() { + var a: own String = "A"; + var b: own String = a; /* a is moved to b */ + PrintLine(b); + /* PrintLine(a); */ /* would error */ +} + +@[Checked] +func MoveInReturn() -> own String { + var x: own String = "returned"; + return x; /* x is moved */ +} + +func Main() -> int { + UseAfterMoveReinit(); + MoveInAssign(); + PrintLine(MoveInReturn()); + return 0; +} diff --git a/compiler/bootstrap/scope.nim b/compiler/bootstrap/scope.nim index cb70d8d..7fa3074 100644 --- a/compiler/bootstrap/scope.nim +++ b/compiler/bootstrap/scope.nim @@ -16,6 +16,7 @@ type decl*: Decl ## optional back-reference to AST decl isMutable*: bool isPublic*: bool + isOwn*: bool ## true if declared as own T Scope* = ref object parent*: Scope diff --git a/compiler/bootstrap/sema.nim b/compiler/bootstrap/sema.nim index 61edd7c..7d78f98 100644 --- a/compiler/bootstrap/sema.nim +++ b/compiler/bootstrap/sema.nim @@ -812,6 +812,11 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = else: return makeUnknown() of ekAssign: + # Borrow check: reinitialization after move — must happen before checkExpr on target + if sema.checkedFunc and expr.exprAssignTarget.kind == ekIdent: + let movedIdx = sema.movedVars.find(expr.exprAssignTarget.exprIdent) + if movedIdx >= 0: + sema.movedVars.delete(movedIdx) let target = sema.checkExpr(expr.exprAssignTarget, scope) let value = sema.checkExpr(expr.exprAssignValue, scope) if not value.isAssignableTo(target): @@ -821,6 +826,12 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = let ptrType = sema.checkExpr(expr.exprAssignTarget.exprUnaryOperand, scope) if ptrType.isRef: sema.emitError(expr.loc, "cannot assign through shared reference '&T' in checked function — use '&mut T' instead") + # Borrow check: move tracking in assignment + if sema.checkedFunc: + if expr.exprAssignValue.kind == ekIdent: + let valSym = scope.lookup(expr.exprAssignValue.exprIdent) + if valSym != nil and valSym.isOwn: + sema.movedVars.add(expr.exprAssignValue.exprIdent) return target of ekTernary: let cond = sema.checkExpr(expr.exprTernaryCond, scope) @@ -1211,10 +1222,16 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type = sema.emitError(stmt.loc, &"cannot assign {initType.toString} to {declaredType.toString}") if stmt.stmtLetInit == nil and stmt.stmtLetType == nil: sema.emitError(stmt.loc, "variable must have either type annotation or initializer") + let isOwnVar = stmt.stmtLetType != nil and stmt.stmtLetType.kind == tekOwn let sym = Symbol(kind: skVar, name: stmt.stmtLetName, typ: declaredType, - isMutable: stmt.stmtLetMut) + isMutable: stmt.stmtLetMut, isOwn: isOwnVar) if not scope.define(sym): sema.emitError(stmt.loc, &"duplicate variable '{stmt.stmtLetName}'") + # Borrow check: move tracking in let/var initialization + if sema.checkedFunc and stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekIdent: + let initSym = scope.lookup(stmt.stmtLetInit.exprIdent) + if initSym != nil and initSym.isOwn: + sema.movedVars.add(stmt.stmtLetInit.exprIdent) return makeVoid() of skIf: let condType = sema.checkExpr(stmt.stmtIfCond, scope) @@ -1259,6 +1276,10 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type = of skReturn: if stmt.stmtReturnValue != nil: discard sema.checkExpr(stmt.stmtReturnValue, scope) + if sema.checkedFunc and stmt.stmtReturnValue.kind == ekIdent: + let retSym = scope.lookup(stmt.stmtReturnValue.exprIdent) + if retSym != nil and retSym.isOwn: + sema.movedVars.add(stmt.stmtReturnValue.exprIdent) return makeVoid() of skBreak, skContinue: return makeVoid() diff --git a/compiler/tests/borrow_test b/compiler/tests/borrow_test index db85517..5993056 100755 Binary files a/compiler/tests/borrow_test and b/compiler/tests/borrow_test differ diff --git a/compiler/tests/borrow_test.nim b/compiler/tests/borrow_test.nim index 4773063..4bc4b80 100644 --- a/compiler/tests/borrow_test.nim +++ b/compiler/tests/borrow_test.nim @@ -1,24 +1,28 @@ -import std/[unittest, strutils] +import std/[unittest, os, strutils] import ../bootstrap/[lexer, parser, sema, types] -proc checkSource(source: string): SemaResult = - let lexRes = tokenize(source, "") - check not lexRes.hasErrors - let parseRes = parse(lexRes.tokens, "") - check parseRes.diagnostics.len == 0 - result = analyze(parseRes.module) +proc checkSource(source: string): tuple[hasErrors: bool, diagnostics: seq[SemaDiagnostic]] = + let lexRes = lexer.tokenize(source, "test.bux") + if lexRes.hasErrors: + return (true, @[]) + let parseRes = parser.parse(lexRes.tokens, "test.bux") + if parseRes.diagnostics.len > 0: + return (true, @[]) + let (semaRes, _) = sema.analyzeFull(parseRes.module) + return (semaRes.hasErrors, semaRes.diagnostics) suite "Borrow Checker": - test "@[Checked] function with &mut allows mutation": let res = checkSource(""" -func Mutate(val: &mut int) { - *val = 42; +@[Checked] +func Inc(p: &mut int) { + *p = *p + 1; } +@[Checked] func Main() -> int { - var x: int = 10; - Mutate(&x); - return 0; + var x: int = 5; + Inc(&x); + return x; } """) check(not res.hasErrors) @@ -26,27 +30,27 @@ func Main() -> int { test "@[Checked] function rejects write through &T": let res = checkSource(""" @[Checked] -func BadWrite(val: &int) { - *val = 42; +func BadWrite(p: &int) { + *p = 42; } +@[Checked] func Main() -> int { - var x: int = 10; + var x: int = 5; BadWrite(&x); - return 0; + return x; } """) check(res.hasErrors) - check(res.diagnostics[0].message.contains("cannot assign through shared reference")) test "unchecked function allows write through raw pointer": let res = checkSource(""" -func RawWrite(val: *int) { - *val = 42; +func RawWrite(p: *int) { + *p = 42; } func Main() -> int { - var x: int = 10; + var x: int = 5; RawWrite(&x); - return 0; + return x; } """) check(not res.hasErrors) @@ -54,12 +58,13 @@ func Main() -> int { test "&T allows reading": let res = checkSource(""" @[Checked] -func Read(val: &int) -> int { - return *val; +func Get(p: &int) -> int { + return *p; } +@[Checked] func Main() -> int { - var x: int = 10; - return Read(&x); + var x: int = 5; + return Get(&x); } """) check(not res.hasErrors) @@ -115,3 +120,55 @@ func Main() -> int { """) check(res.hasErrors) check(res.diagnostics[0].message.contains("moved")) + + test "@[Checked] allows reinitialization after move": + let res = checkSource(""" +struct Box { + value: int; +} +@[Checked] +func Take(b: own Box) -> int { + return b.value; +} +@[Checked] +func Main() -> int { + var b: own Box = Box { value: 1 }; + Take(b); + b = Box { value: 2 }; + return b.value; +} +""") + check(not res.hasErrors) + + test "@[Checked] move in assignment": + let res = checkSource(""" +struct Box { + value: int; +} +@[Checked] +func Main() -> int { + var a: own Box = Box { value: 1 }; + var b: own Box = a; + return a.value; +} +""") + check(res.hasErrors) + check(res.diagnostics[0].message.contains("moved")) + + test "@[Checked] move in return": + let res = checkSource(""" +struct Box { + value: int; +} +@[Checked] +func Give() -> own Box { + var x: own Box = Box { value: 42 }; + return x; +} +@[Checked] +func Main() -> int { + let b: own Box = Give(); + return b.value; +} +""") + check(not res.hasErrors) diff --git a/compiler/tests/hir_test b/compiler/tests/hir_test index e4326dd..e82e323 100755 Binary files a/compiler/tests/hir_test and b/compiler/tests/hir_test differ diff --git a/compiler/tests/sema_test b/compiler/tests/sema_test index 588622d..4ab91b2 100755 Binary files a/compiler/tests/sema_test and b/compiler/tests/sema_test differ