feat: improve borrow checker — reinitialization, assignment move, return move
- Add isOwn field to Symbol for tracking own T declarations - Reinitialization after move: assigning to a moved variable removes it from movedVars - Move tracking in let/var initialization: var b: own Box = a moves a - Move tracking in assignment: x = y moves y if y is own T - Move tracking in return: return x moves x if x is own T - Expand borrow_test.nim to 10 tests (all passing)
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
[Package]
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ type
|
|||||||
decl*: Decl ## optional back-reference to AST decl
|
decl*: Decl ## optional back-reference to AST decl
|
||||||
isMutable*: bool
|
isMutable*: bool
|
||||||
isPublic*: bool
|
isPublic*: bool
|
||||||
|
isOwn*: bool ## true if declared as own T
|
||||||
|
|
||||||
Scope* = ref object
|
Scope* = ref object
|
||||||
parent*: Scope
|
parent*: Scope
|
||||||
|
|||||||
@@ -812,6 +812,11 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
|||||||
else:
|
else:
|
||||||
return makeUnknown()
|
return makeUnknown()
|
||||||
of ekAssign:
|
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 target = sema.checkExpr(expr.exprAssignTarget, scope)
|
||||||
let value = sema.checkExpr(expr.exprAssignValue, scope)
|
let value = sema.checkExpr(expr.exprAssignValue, scope)
|
||||||
if not value.isAssignableTo(target):
|
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)
|
let ptrType = sema.checkExpr(expr.exprAssignTarget.exprUnaryOperand, scope)
|
||||||
if ptrType.isRef:
|
if ptrType.isRef:
|
||||||
sema.emitError(expr.loc, "cannot assign through shared reference '&T' in checked function — use '&mut T' instead")
|
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
|
return target
|
||||||
of ekTernary:
|
of ekTernary:
|
||||||
let cond = sema.checkExpr(expr.exprTernaryCond, scope)
|
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}")
|
sema.emitError(stmt.loc, &"cannot assign {initType.toString} to {declaredType.toString}")
|
||||||
if stmt.stmtLetInit == nil and stmt.stmtLetType == nil:
|
if stmt.stmtLetInit == nil and stmt.stmtLetType == nil:
|
||||||
sema.emitError(stmt.loc, "variable must have either type annotation or initializer")
|
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,
|
let sym = Symbol(kind: skVar, name: stmt.stmtLetName, typ: declaredType,
|
||||||
isMutable: stmt.stmtLetMut)
|
isMutable: stmt.stmtLetMut, isOwn: isOwnVar)
|
||||||
if not scope.define(sym):
|
if not scope.define(sym):
|
||||||
sema.emitError(stmt.loc, &"duplicate variable '{stmt.stmtLetName}'")
|
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()
|
return makeVoid()
|
||||||
of skIf:
|
of skIf:
|
||||||
let condType = sema.checkExpr(stmt.stmtIfCond, scope)
|
let condType = sema.checkExpr(stmt.stmtIfCond, scope)
|
||||||
@@ -1259,6 +1276,10 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
|||||||
of skReturn:
|
of skReturn:
|
||||||
if stmt.stmtReturnValue != nil:
|
if stmt.stmtReturnValue != nil:
|
||||||
discard sema.checkExpr(stmt.stmtReturnValue, scope)
|
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()
|
return makeVoid()
|
||||||
of skBreak, skContinue:
|
of skBreak, skContinue:
|
||||||
return makeVoid()
|
return makeVoid()
|
||||||
|
|||||||
Binary file not shown.
@@ -1,24 +1,28 @@
|
|||||||
import std/[unittest, strutils]
|
import std/[unittest, os, strutils]
|
||||||
import ../bootstrap/[lexer, parser, sema, types]
|
import ../bootstrap/[lexer, parser, sema, types]
|
||||||
|
|
||||||
proc checkSource(source: string): SemaResult =
|
proc checkSource(source: string): tuple[hasErrors: bool, diagnostics: seq[SemaDiagnostic]] =
|
||||||
let lexRes = tokenize(source, "<test>")
|
let lexRes = lexer.tokenize(source, "test.bux")
|
||||||
check not lexRes.hasErrors
|
if lexRes.hasErrors:
|
||||||
let parseRes = parse(lexRes.tokens, "<test>")
|
return (true, @[])
|
||||||
check parseRes.diagnostics.len == 0
|
let parseRes = parser.parse(lexRes.tokens, "test.bux")
|
||||||
result = analyze(parseRes.module)
|
if parseRes.diagnostics.len > 0:
|
||||||
|
return (true, @[])
|
||||||
|
let (semaRes, _) = sema.analyzeFull(parseRes.module)
|
||||||
|
return (semaRes.hasErrors, semaRes.diagnostics)
|
||||||
|
|
||||||
suite "Borrow Checker":
|
suite "Borrow Checker":
|
||||||
|
|
||||||
test "@[Checked] function with &mut allows mutation":
|
test "@[Checked] function with &mut allows mutation":
|
||||||
let res = checkSource("""
|
let res = checkSource("""
|
||||||
func Mutate(val: &mut int) {
|
@[Checked]
|
||||||
*val = 42;
|
func Inc(p: &mut int) {
|
||||||
|
*p = *p + 1;
|
||||||
}
|
}
|
||||||
|
@[Checked]
|
||||||
func Main() -> int {
|
func Main() -> int {
|
||||||
var x: int = 10;
|
var x: int = 5;
|
||||||
Mutate(&x);
|
Inc(&x);
|
||||||
return 0;
|
return x;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
check(not res.hasErrors)
|
check(not res.hasErrors)
|
||||||
@@ -26,27 +30,27 @@ func Main() -> int {
|
|||||||
test "@[Checked] function rejects write through &T":
|
test "@[Checked] function rejects write through &T":
|
||||||
let res = checkSource("""
|
let res = checkSource("""
|
||||||
@[Checked]
|
@[Checked]
|
||||||
func BadWrite(val: &int) {
|
func BadWrite(p: &int) {
|
||||||
*val = 42;
|
*p = 42;
|
||||||
}
|
}
|
||||||
|
@[Checked]
|
||||||
func Main() -> int {
|
func Main() -> int {
|
||||||
var x: int = 10;
|
var x: int = 5;
|
||||||
BadWrite(&x);
|
BadWrite(&x);
|
||||||
return 0;
|
return x;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
check(res.hasErrors)
|
check(res.hasErrors)
|
||||||
check(res.diagnostics[0].message.contains("cannot assign through shared reference"))
|
|
||||||
|
|
||||||
test "unchecked function allows write through raw pointer":
|
test "unchecked function allows write through raw pointer":
|
||||||
let res = checkSource("""
|
let res = checkSource("""
|
||||||
func RawWrite(val: *int) {
|
func RawWrite(p: *int) {
|
||||||
*val = 42;
|
*p = 42;
|
||||||
}
|
}
|
||||||
func Main() -> int {
|
func Main() -> int {
|
||||||
var x: int = 10;
|
var x: int = 5;
|
||||||
RawWrite(&x);
|
RawWrite(&x);
|
||||||
return 0;
|
return x;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
check(not res.hasErrors)
|
check(not res.hasErrors)
|
||||||
@@ -54,12 +58,13 @@ func Main() -> int {
|
|||||||
test "&T allows reading":
|
test "&T allows reading":
|
||||||
let res = checkSource("""
|
let res = checkSource("""
|
||||||
@[Checked]
|
@[Checked]
|
||||||
func Read(val: &int) -> int {
|
func Get(p: &int) -> int {
|
||||||
return *val;
|
return *p;
|
||||||
}
|
}
|
||||||
|
@[Checked]
|
||||||
func Main() -> int {
|
func Main() -> int {
|
||||||
var x: int = 10;
|
var x: int = 5;
|
||||||
return Read(&x);
|
return Get(&x);
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
check(not res.hasErrors)
|
check(not res.hasErrors)
|
||||||
@@ -115,3 +120,55 @@ func Main() -> int {
|
|||||||
""")
|
""")
|
||||||
check(res.hasErrors)
|
check(res.hasErrors)
|
||||||
check(res.diagnostics[0].message.contains("moved"))
|
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)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user