feat: restructure repo, borrow checker, expanded stdlib

- Reorganize repository to Rust-style layout:
  compiler/bootstrap/  compiler/selfhost/  compiler/tests/
  library/std/  library/runtime/  tests/  tools/
- Add buxs/ Windows-compatible project root
- Add borrow checker tests and implement:
  - Alias analysis (double mutable borrow detection)
  - Use-after-move detection for own T
- Expand standard library:
  - Std::Os: Args, Env, Cwd, Chdir
  - Std::Time: NowMs, NowUs, SleepMs
  - Std::Process: Run, Output
  - Std::Io: PrintInt64 (fixes 32-bit truncation bug)
- Add examples: os_time.bux, process.bux
- Fix PrintInt to use int64_t in C runtime
This commit is contained in:
2026-06-05 20:08:17 +03:00
parent a45a2ecd49
commit 9c6b516453
75 changed files with 581 additions and 79 deletions
BIN
View File
Binary file not shown.
+117
View File
@@ -0,0 +1,117 @@
import std/[unittest, strutils]
import ../bootstrap/[lexer, parser, sema, types]
proc checkSource(source: string): SemaResult =
let lexRes = tokenize(source, "<test>")
check not lexRes.hasErrors
let parseRes = parse(lexRes.tokens, "<test>")
check parseRes.diagnostics.len == 0
result = analyze(parseRes.module)
suite "Borrow Checker":
test "@[Checked] function with &mut allows mutation":
let res = checkSource("""
func Mutate(val: &mut int) {
*val = 42;
}
func Main() -> int {
var x: int = 10;
Mutate(&x);
return 0;
}
""")
check(not res.hasErrors)
test "@[Checked] function rejects write through &T":
let res = checkSource("""
@[Checked]
func BadWrite(val: &int) {
*val = 42;
}
func Main() -> int {
var x: int = 10;
BadWrite(&x);
return 0;
}
""")
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 Main() -> int {
var x: int = 10;
RawWrite(&x);
return 0;
}
""")
check(not res.hasErrors)
test "&T allows reading":
let res = checkSource("""
@[Checked]
func Read(val: &int) -> int {
return *val;
}
func Main() -> int {
var x: int = 10;
return Read(&x);
}
""")
check(not res.hasErrors)
test "own T type parses and resolves":
let res = checkSource("""
struct Box {
value: int;
}
func TakeOwn(b: own Box) -> int {
return b.value;
}
func Main() -> int {
var b: Box = Box { value: 42 };
return TakeOwn(b);
}
""")
check(not res.hasErrors)
test "@[Checked] rejects double mutable borrow in call":
let res = checkSource("""
@[Checked]
func Swap(a: &mut int, b: &mut int) {
let tmp = *a;
*a = *b;
*b = tmp;
}
@[Checked]
func Main() -> int {
var x: int = 10;
Swap(&x, &x);
return 0;
}
""")
check(res.hasErrors)
check(res.diagnostics[0].message.contains("mutable borrow"))
test "@[Checked] rejects use after move":
let res = checkSource("""
struct Box {
value: int;
}
@[Checked]
func Consume(b: own Box) -> int {
return b.value;
}
@[Checked]
func Main() -> int {
var b: Box = Box { value: 42 };
let x: int = Consume(b);
return b.value;
}
""")
check(res.hasErrors)
check(res.diagnostics[0].message.contains("moved"))
BIN
View File
Binary file not shown.
+104
View File
@@ -0,0 +1,104 @@
import std/[unittest, strformat]
import ../bootstrap/[lexer, parser, sema, hir, hir_lower, types, scope]
proc lowerSource(source: string): HirModule =
let lexRes = tokenize(source, "<test>")
check not lexRes.hasErrors
let parseRes = parse(lexRes.tokens, "<test>")
check parseRes.diagnostics.len == 0
# Create Sema and populate global scope + method table
var s = Sema(module: parseRes.module, globalScope: newScope())
s.collectGlobals()
result = lowerModule(parseRes.module, s)
suite "HIR Lowering":
test "simple function":
let src = "func Main() -> int { return 0; }"
let hir = lowerSource(src)
check hir.funcs.len == 1
check hir.funcs[0].name == "Main"
check hir.funcs[0].body != nil
check hir.funcs[0].body.kind == hBlock
echo " PASS: simple function"
test "function with arithmetic":
let src = "func Add(a: int, b: int) -> int { return a + b; }"
let hir = lowerSource(src)
check hir.funcs.len == 1
check hir.funcs[0].params.len == 2
echo " PASS: function with arithmetic"
test "struct lowering":
let src = """
struct Point { x: float64; y: float64; }
func Main() -> int { return 0; }
"""
let hir = lowerSource(src)
check hir.structs.len == 1
check hir.structs[0].name == "Point"
check hir.structs[0].fields.len == 2
echo " PASS: struct lowering"
test "method lowering":
let src = """
struct Point { x: float64; }
extend Point {
func GetX(self: Point) -> float64 { return 0.0; }
}
func Main() -> int { return 0; }
"""
let hir = lowerSource(src)
check hir.funcs.len == 2 # GetX (mangled) + Main
var foundMangled = false
for f in hir.funcs:
if f.name == "Point_GetX":
foundMangled = true
check foundMangled
echo " PASS: method lowering"
test "if statement":
let src = """
func Main() -> int {
if true { return 1; }
return 0;
}
"""
let hir = lowerSource(src)
let body = hir.funcs[0].body
check body.kind == hBlock
check body.blockStmts.len >= 1
echo " PASS: if statement"
test "while loop":
let src = """
func Main() -> int {
while true { break; }
return 0;
}
"""
let hir = lowerSource(src)
let body = hir.funcs[0].body
check body.kind == hBlock
echo " PASS: while loop"
test "let statement":
let src = """
func Main() -> int {
let x: int = 42;
return x;
}
"""
let hir = lowerSource(src)
check hir.funcs.len == 1
echo " PASS: let statement"
test "enum lowering":
let src = """
enum Color { Red, Green, Blue }
func Main() -> int { return 0; }
"""
let hir = lowerSource(src)
check hir.enums.len == 1
check hir.enums[0].name == "Color"
check hir.enums[0].variants.len == 3
echo " PASS: enum lowering"
BIN
View File
Binary file not shown.
+124
View File
@@ -0,0 +1,124 @@
import std/[unittest, os, strutils]
import ../bootstrap/[lexer, token, source_location]
proc tokenKinds(source: string): seq[TokenKind] =
let res = tokenize(source, "<test>")
for t in res.tokens:
result.add(t.kind)
proc tokenTexts(source: string): seq[string] =
let res = tokenize(source, "<test>")
for t in res.tokens:
result.add(t.text)
suite "Lexer":
test "empty source":
let res = tokenize("", "<test>")
check res.tokens.len == 1
check res.tokens[0].kind == tkEndOfFile
check not res.hasErrors
test "simple identifiers":
let kinds = tokenKinds("foo bar _x Baz123")
check kinds == @[tkIdent, tkIdent, tkIdent, tkIdent, tkEndOfFile]
test "keywords":
let kinds = tokenKinds("func let var if else while for return match struct enum")
check kinds == @[tkFunc, tkLet, tkVar, tkIf, tkElse, tkWhile, tkFor, tkReturn, tkMatch, tkStruct, tkEnum, tkEndOfFile]
test "bool literals":
let kinds = tokenKinds("true false")
check kinds == @[tkBoolLiteral, tkBoolLiteral, tkEndOfFile]
let texts = tokenTexts("true false")
check texts == @["true", "false", ""]
test "integers":
let kinds = tokenKinds("42 0xFF 0b1010 0o77")
check kinds == @[tkIntLiteral, tkIntLiteral, tkIntLiteral, tkIntLiteral, tkEndOfFile]
test "floats":
let kinds = tokenKinds("3.14 1.0e-9 2.5E+3")
check kinds == @[tkFloatLiteral, tkFloatLiteral, tkFloatLiteral, tkEndOfFile]
test "operators":
let kinds = tokenKinds("+ - * / % ** ++ --")
check kinds == @[tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar, tkPlusPlus, tkMinusMinus, tkEndOfFile]
test "comparison operators":
let kinds = tokenKinds("== != < <= > >=")
check kinds == @[tkEq, tkNe, tkLt, tkLe, tkGt, tkGe, tkEndOfFile]
test "assignment operators":
let kinds = tokenKinds("= += -= *= /= %= &= |= ^= <<= >>=")
check kinds == @[tkAssign, tkPlusAssign, tkMinusAssign, tkStarAssign, tkSlashAssign, tkPercentAssign, tkAmpAssign, tkPipeAssign, tkCaretAssign, tkShlAssign, tkShrAssign, tkEndOfFile]
test "punctuation":
let kinds = tokenKinds("( ) { } [ ] , ; : :: . .. ... ..= -> => @ # ?")
check kinds == @[tkLParen, tkRParen, tkLBrace, tkRBrace, tkLBracket, tkRBracket, tkComma, tkSemicolon, tkColon, tkColonColon, tkDot, tkDotDot, tkDotDotDot, tkDotDotEqual, tkArrow, tkFatArrow, tkAt, tkHash, tkQuestion, tkEndOfFile]
test "logical operators":
let kinds = tokenKinds("&& || !")
check kinds == @[tkAmpAmp, tkPipePipe, tkBang, tkEndOfFile]
test "bitwise operators":
let kinds = tokenKinds("& | ^ ~ << >>")
check kinds == @[tkAmp, tkPipe, tkCaret, tkTilde, tkShl, tkShr, tkEndOfFile]
test "string literals":
let kinds = tokenKinds("\"hello\" c8\"world\"")
check kinds == @[tkStringLiteral, tkStringLiteral, tkEndOfFile]
test "char literals":
let kinds = tokenKinds("'A' c8'B'")
check kinds == @[tkCharLiteral, tkCharLiteral, tkEndOfFile]
test "line comments are skipped":
let kinds = tokenKinds("let x = 42 // this is a comment")
check kinds == @[tkLet, tkIdent, tkAssign, tkIntLiteral, tkEndOfFile]
test "block comments are skipped":
let kinds = tokenKinds("let /* inline comment */ x = 42")
check kinds == @[tkLet, tkIdent, tkAssign, tkIntLiteral, tkEndOfFile]
test "nested block comments":
let kinds = tokenKinds("let /* outer /* inner */ still outer */ x = 42")
check kinds == @[tkLet, tkIdent, tkAssign, tkIntLiteral, tkEndOfFile]
test "intrinsics":
let kinds = tokenKinds("#line #column #file #function #date #time #module")
check kinds == @[tkHashLine, tkHashColumn, tkHashFile, tkHashFunction, tkHashDate, tkHashTime, tkHashModule, tkEndOfFile]
test "newline tokens":
let kinds = tokenKinds("let x = 42\nlet y = 10")
check kinds == @[tkLet, tkIdent, tkAssign, tkIntLiteral, tkNewLine, tkLet, tkIdent, tkAssign, tkIntLiteral, tkEndOfFile]
test "paths":
let kinds = tokenKinds("Std::Io::PrintLine")
check kinds == @[tkIdent, tkColonColon, tkIdent, tkColonColon, tkIdent, tkEndOfFile]
test "unterminated string error":
let res = tokenize("\"hello", "<test>")
check res.hasErrors
test "unterminated char error":
let res = tokenize("'a", "<test>")
check res.hasErrors
test "escape sequences in string":
let res = tokenize("\"hello\\nworld\\t!\"", "<test>")
check not res.hasErrors
check res.tokens[0].kind == tkStringLiteral
test "full function":
let src = "func Main() -> int {\n let x: int32 = 42;\n return x;\n}\n"
let res = tokenize(src, "<test>")
check not res.hasErrors
check res.tokens[0].kind == tkFunc
check res.tokens[1].kind == tkIdent
check res.tokens[1].text == "Main"
test "dumpTokens produces output":
let res = tokenize("let x = 42", "<test>")
let dump = dumpTokens(res)
check "let" in dump
check "42" in dump
BIN
View File
Binary file not shown.
+122
View File
@@ -0,0 +1,122 @@
import std/[unittest, os, strutils]
import ../bootstrap/[lexer, parser, ast, token]
proc parseSource(source: string): ParseResult =
let lexRes = tokenize(source, "<test>")
check not lexRes.hasErrors
result = parse(lexRes.tokens, "<test>")
suite "Parser":
test "empty module":
let res = parseSource("")
check res.diagnostics.len == 0
check res.module.items.len == 0
test "simple function":
let res = parseSource("func Main() -> int { return 0; }")
check res.diagnostics.len == 0
check res.module.items.len == 1
check res.module.items[0].kind == dkFunc
check res.module.items[0].declFuncName == "Main"
test "function with parameters":
let res = parseSource("func Add(a: int32, b: int32) -> int32 { return a + b; }")
check res.diagnostics.len == 0
check res.module.items[0].declFuncParams.len == 2
check res.module.items[0].declFuncParams[0].name == "a"
check res.module.items[0].declFuncParams[1].name == "b"
test "struct declaration":
let res = parseSource("struct Point { x: float64; y: float64; }")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkStruct
check res.module.items[0].declStructName == "Point"
check res.module.items[0].declStructFields.len == 2
test "enum declaration":
let res = parseSource("enum Color { Red, Green, Blue }")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkEnum
check res.module.items[0].declEnumName == "Color"
check res.module.items[0].declEnumVariants.len == 3
test "import declaration":
let res = parseSource("import Std::Io::PrintLine;")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkUse
check res.module.items[0].declUsePath == @["Std", "Io", "PrintLine"]
test "const declaration":
let res = parseSource("const PI: float64 = 3.14;")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkConst
check res.module.items[0].declConstName == "PI"
test "type alias":
let res = parseSource("type MyInt = int32;")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkTypeAlias
check res.module.items[0].declAliasName == "MyInt"
test "let statement in function":
let res = parseSource("func Main() -> int { let x: int32 = 42; return x; }")
check res.diagnostics.len == 0
check res.module.items[0].declFuncBody.stmts.len == 2
check res.module.items[0].declFuncBody.stmts[0].kind == skLet
check res.module.items[0].declFuncBody.stmts[0].stmtLetName == "x"
test "if statement":
let res = parseSource("func Main() -> int { if true { return 1; } else { return 0; } }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skIf
check body.stmts[0].stmtIfThen.stmts.len == 1
check body.stmts[0].stmtIfElse.stmts.len == 1
test "while loop":
let res = parseSource("func Main() -> int { while true { break; } return 0; }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skWhile
check body.stmts[0].stmtWhileBody.stmts[0].kind == skBreak
test "for loop":
let res = parseSource("func Main() -> int { for i in items { continue; } return 0; }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skFor
check body.stmts[0].stmtForVar == "i"
test "match expression":
let res = parseSource("func Main() -> int { let x = match 1 { 1 => 10, _ => 20 }; return x; }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skLet
check body.stmts[0].stmtLetInit.kind == ekMatch
test "binary expression":
let res = parseSource("func Main() -> int { return 1 + 2 * 3; }")
check res.diagnostics.len == 0
let ret = res.module.items[0].declFuncBody.stmts[0]
check ret.kind == skReturn
check ret.stmtReturnValue.kind == ekBinary
# 1 + (2 * 3)
check ret.stmtReturnValue.exprBinaryOp == tkPlus
test "call expression":
let res = parseSource("func Main() -> int { return Add(10, 20); }")
check res.diagnostics.len == 0
let ret = res.module.items[0].declFuncBody.stmts[0]
check ret.stmtReturnValue.kind == ekCall
check ret.stmtReturnValue.exprCallCallee.kind == ekIdent
test "full sample file":
let source = readFile(currentSourcePath.parentDir / "testdata" / "sample.bux")
let lexRes = tokenize(source, "sample.bux")
check not lexRes.hasErrors
let parseRes = parse(lexRes.tokens, "sample.bux")
check parseRes.diagnostics.len == 0
check parseRes.module.items.len == 3
check parseRes.module.items[0].kind == dkUse
check parseRes.module.items[1].kind == dkFunc
check parseRes.module.items[2].kind == dkFunc
BIN
View File
Binary file not shown.
+139
View File
@@ -0,0 +1,139 @@
import std/[unittest, strutils]
import ../bootstrap/[lexer, parser, sema, types]
proc checkSource(source: string): SemaResult =
let lexRes = tokenize(source, "<test>")
check not lexRes.hasErrors
let parseRes = parse(lexRes.tokens, "<test>")
check parseRes.diagnostics.len == 0
result = analyze(parseRes.module)
suite "Sema":
test "valid function with correct types":
let res = checkSource("func Main() -> int { return 0; }")
check not res.hasErrors
test "undeclared identifier":
let res = checkSource("func Main() -> int { return x; }")
check res.hasErrors
check "undeclared" in res.diagnostics[0].message
test "duplicate function":
let res = checkSource("func Main() -> int { return 0; } func Main() -> int { return 1; }")
check res.hasErrors
check "duplicate" in res.diagnostics[0].message
test "type mismatch in assignment":
let res = checkSource("func Main() -> int { let x: int32 = c8\"hello\"; return 0; }")
check res.hasErrors
check "cannot assign" in res.diagnostics[0].message
test "valid arithmetic":
let res = checkSource("func Main() -> int { return 1 + 2 * 3; }")
check not res.hasErrors
test "arithmetic on strings fails":
let res = checkSource("func Main() -> int { return c8\"a\" + c8\"b\"; }")
check res.hasErrors
test "valid function call":
let res = checkSource("func Add(a: int, b: int) -> int { return a + b; } func Main() -> int { return Add(1, 2); }")
check not res.hasErrors
test "wrong number of arguments":
let res = checkSource("func Add(a: int32, b: int32) -> int32 { return a + b; } func Main() -> int { return Add(1); }")
check res.hasErrors
check "expected 2 arguments" in res.diagnostics[0].message
test "wrong argument type":
let res = checkSource("func Add(a: int32, b: int32) -> int32 { return a + b; } func Main() -> int { return Add(c8\"a\", 2); }")
check res.hasErrors
check "argument 1" in res.diagnostics[0].message
test "if condition must be bool":
let res = checkSource("func Main() -> int { if 1 { return 0; } return 0; }")
check res.hasErrors
check "bool" in res.diagnostics[0].message
test "valid if with bool":
let res = checkSource("func Main() -> int { if true { return 0; } return 0; }")
check not res.hasErrors
test "pointer dereference":
let res = checkSource("func Main() -> int32 { let p: *int32 = null; return *p; }")
check not res.hasErrors
test "struct field access":
let res = checkSource("struct Point { x: float64; y: float64; } func Main() -> int32 { let p = Point { x: 1.0, y: 2.0 }; return 0; }")
check not res.hasErrors
test "unknown struct field":
let res = checkSource("struct Point { x: float64; y: float64; } func Main() -> int32 { let p = Point { x: 1.0, y: 2.0 }; return p.z; }")
check res.hasErrors
check "no field" in res.diagnostics[0].message
test "valid comparison":
let res = checkSource("func Main() -> int { return 1 == 2; }")
check not res.hasErrors
test "valid slice literal":
let res = checkSource("func Main() -> int { let arr = [1, 2, 3]; return 0; }")
check not res.hasErrors
test "slice element type mismatch":
let res = checkSource("func Main() -> int { let arr = [1, c8\"a\"]; return 0; }")
check res.hasErrors
test "method call with extend":
let src = """
struct Point { x: float64; y: float64; }
extend Point {
func Distance(self: Point) -> float64 { return 0.0; }
}
func Main() -> int {
let p = Point { x: 1.0, y: 2.0 };
let d = p.Distance();
return 0;
}
"""
let res = checkSource(src)
check not res.hasErrors
test "method call with wrong arguments":
let src = """
struct Point { x: float64; y: float64; }
extend Point {
func Add(self: Point, other: Point) -> Point { return self; }
}
func Main() -> int {
let p = Point { x: 1.0, y: 2.0 };
let q = p.Add();
return 0;
}
"""
let res = checkSource(src)
check res.hasErrors
check "too few arguments" in res.diagnostics[0].message
test "interface declaration":
let src = """
interface Display {
func ToString(self: Self) -> String;
}
"""
let res = checkSource(src)
check not res.hasErrors
test "extend for interface":
let src = """
struct Point { x: float64; y: float64; }
interface Display {
func ToString(self: Self) -> String;
}
extend Point for Display {
func ToString(self: Point) -> String { return c8"Point"; }
}
func Main() -> int { return 0; }
"""
let res = checkSource(src)
check not res.hasErrors
+15
View File
@@ -0,0 +1,15 @@
import "../bootstrap/lexer", "../bootstrap/parser", "../bootstrap/ast"
let source = readFile("../_selfhost/src/ast.bux")
let lexRes = tokenize(source, "ast.bux")
let res = parse(lexRes.tokens, "ast.bux")
if res.module != nil:
echo "Module items: ", res.module.items.len
for decl in res.module.items:
if decl.kind == dkStruct:
echo "Struct: ", decl.declStructName, " fields: ", decl.declStructFields.len
elif decl.kind == dkModule:
echo "Inner module: ", decl.declModuleName, " items: ", decl.declModuleItems.len
for inner in decl.declModuleItems:
if inner.kind == dkStruct:
echo " Struct: ", inner.declStructName, " fields: ", inner.declStructFields.len
+16
View File
@@ -0,0 +1,16 @@
import "../bootstrap/lexer", "../bootstrap/parser", "../bootstrap/ast", "../bootstrap/token"
import std/os
let source = readFile("../_selfhost/src/ast.bux")
let lexRes = tokenize(source, "ast.bux")
echo "Tokens: ", lexRes.tokens.len
echo "Errors: ", lexRes.hasErrors
let res = parse(lexRes.tokens, "ast.bux")
if res.module == nil:
echo "Parse failed, diagnostics: ", res.diagnostics.len
for d in res.diagnostics:
echo " ", d.message
else:
echo "Parsed OK, items: ", res.module.items.len
for i, decl in res.module.items:
echo "Decl ", i, " kind=", decl.kind, " name=", decl.declFuncName
+11
View File
@@ -0,0 +1,11 @@
import Std::Io::PrintLine;
func Add(a: int32, b: int32) -> int32 {
return a + b;
}
func Main() -> int {
let result: int32 = Add(10, 20);
PrintLine(c8"Hello from Bux!");
return 0;
}