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
+443
View File
@@ -0,0 +1,443 @@
import source_location, token
type
CallingConvention* = enum
ccDefault
ccWin64
IntrinsicKind* = enum
ikLine
ikColumn
ikFile
ikFunction
ikDate
ikTime
ikModule
UseKind* = enum
ukSingle
ukGlob
ukMulti
# ---------------------------------------------------------------------------
# Type expressions
# ---------------------------------------------------------------------------
TypeExprKind* = enum
tekNamed
tekPath
tekSlice
tekPointer
tekOwn ## own T — owned value (gradual ownership)
tekRef ## &T — shared reference (gradual ownership)
tekMutRef ## &mut T — mutable reference
tekDynRef ## &dyn Trait — trait object (fat pointer)
tekTuple
tekSelf
TypeExpr* = ref object
loc*: SourceLocation
case kind*: TypeExprKind
of tekNamed:
typeName*: string
typeArgs*: seq[TypeExpr]
of tekPath:
pathSegments*: seq[string]
of tekSlice:
sliceElement*: TypeExpr
sliceSize*: Expr ## nil for unsized slices T[]
of tekOwn, tekPointer, tekRef, tekMutRef:
pointerPointee*: TypeExpr
refLifetime*: string ## only meaningful for tekRef/tekMutRef
of tekDynRef:
dynInterface*: string
of tekTuple:
tupleElements*: seq[TypeExpr]
of tekSelf:
discard
# ---------------------------------------------------------------------------
# Patterns
# ---------------------------------------------------------------------------
PatternKind* = enum
pkWildcard
pkLiteral
pkIdent
pkRange
pkEnum
pkStruct
pkTuple
pkGuarded
Pattern* = ref object
loc*: SourceLocation
case kind*: PatternKind
of pkWildcard:
discard
of pkLiteral:
patLit*: Token
of pkIdent:
patIdent*: string
of pkRange:
patRangeLo*: Pattern
patRangeHi*: Pattern
patRangeInclusive*: bool
of pkEnum:
patEnumPath*: seq[string]
patEnumArgs*: seq[Pattern]
patEnumNamed*: seq[tuple[name: string, pattern: Pattern]]
of pkStruct:
patStructName*: string
patStructFields*: seq[tuple[name: string, pattern: Pattern]]
of pkTuple:
patTupleElements*: seq[Pattern]
of pkGuarded:
patGuardedInner*: Pattern
patGuardedExpr*: Expr
# ---------------------------------------------------------------------------
# Expressions
# ---------------------------------------------------------------------------
ExprKind* = enum
ekLiteral
ekIdent
ekSelf
ekPath
ekSizeOf
ekIntrinsic
ekUnary
ekPostfix
ekBinary
ekAssign
ekTernary
ekRange
ekCall
ekGenericCall
ekIndex
ekField
ekStructInit
ekSlice
ekSpread
ekTuple
ekCast
ekIs
ekTry
ekUnwrap ## expr! — unwrap or panic
ekSpawn ## spawn expr — create a new task
ekAwait ## expr.await — suspend until future resolves
ekBlock
ekMatch
MatchArm* = object
loc*: SourceLocation
pattern*: Pattern
body*: Expr
Expr* = ref object
loc*: SourceLocation
case kind*: ExprKind
of ekLiteral:
exprLit*: Token
of ekIdent:
exprIdent*: string
of ekSelf:
discard
of ekPath:
exprPath*: seq[string]
of ekSizeOf:
exprSizeOfType*: TypeExpr
of ekIntrinsic:
exprIntrinsic*: IntrinsicKind
of ekUnary:
exprUnaryOp*: TokenKind
exprUnaryOperand*: Expr
of ekPostfix:
exprPostfixOp*: TokenKind
exprPostfixOperand*: Expr
of ekBinary:
exprBinaryOp*: TokenKind
exprBinaryLeft*: Expr
exprBinaryRight*: Expr
of ekAssign:
exprAssignOp*: TokenKind
exprAssignTarget*: Expr
exprAssignValue*: Expr
of ekTernary:
exprTernaryCond*: Expr
exprTernaryThen*: Expr
exprTernaryElse*: Expr
of ekRange:
exprRangeLo*: Expr
exprRangeHi*: Expr
exprRangeInclusive*: bool
of ekCall:
exprCallCallee*: Expr
exprCallArgs*: seq[Expr]
exprCallInferredTypeArgs*: seq[TypeExpr] ## filled by sema for inferred generic calls
of ekGenericCall:
exprGenericCallee*: string
exprGenericTypeArgs*: seq[TypeExpr]
of ekIndex:
exprIndexObj*: Expr
exprIndexIdx*: Expr
exprIndexBoundsCheck*: bool
of ekField:
exprFieldObj*: Expr
exprFieldName*: string
of ekStructInit:
exprStructInitName*: string
exprStructInitTypeArgs*: seq[TypeExpr]
exprStructInitFields*: seq[tuple[name: string, value: Expr]]
of ekSlice:
exprSliceElements*: seq[Expr]
of ekSpread:
exprSpreadOperand*: Expr
of ekTuple:
exprTupleElements*: seq[Expr]
of ekCast:
exprCastOperand*: Expr
exprCastType*: TypeExpr
of ekIs:
exprIsOperand*: Expr
exprIsType*: TypeExpr
of ekTry:
exprTryOperand*: Expr
exprTryType*: TypeExpr # nil for Result?, or explicit target type
of ekUnwrap:
exprUnwrapOperand*: Expr
of ekSpawn:
exprSpawnCallee*: Expr
exprSpawnArgs*: seq[Expr]
exprSpawnAsync*: bool
of ekAwait:
exprAwaitOperand*: Expr
of ekBlock:
exprBlock*: Block
of ekMatch:
exprMatchSubject*: Expr
exprMatchArms*: seq[MatchArm]
# ---------------------------------------------------------------------------
# Statements
# ---------------------------------------------------------------------------
StmtKind* = enum
skExpr
skLet
skIf
skWhile
skDoWhile
skLoop
skFor
skMatch
skReturn
skBreak
skContinue
skStaticAssert
skComptime
skEmit
skDecl
ElseIf* = object
loc*: SourceLocation
cond*: Expr
blk*: Block
Block* = ref object
loc*: SourceLocation
stmts*: seq[Stmt]
Stmt* = ref object
loc*: SourceLocation
case kind*: StmtKind
of skExpr:
stmtExpr*: Expr
of skLet:
stmtLetMut*: bool
stmtLetName*: string
stmtLetPattern*: Pattern
stmtLetType*: TypeExpr ## nil if inferred
stmtLetInit*: Expr
of skIf:
stmtIfCond*: Expr
stmtIfThen*: Block
stmtIfElseIfs*: seq[ElseIf]
stmtIfElse*: Block ## nil if no else
of skWhile:
stmtWhileLabel*: string
stmtWhileCond*: Expr
stmtWhileBody*: Block
of skDoWhile:
stmtDoWhileLabel*: string
stmtDoWhileBody*: Block
stmtDoWhileCond*: Expr
of skLoop:
stmtLoopLabel*: string
stmtLoopBody*: Block
of skFor:
stmtForLabel*: string
stmtForVar*: string
stmtForIter*: Expr
stmtForBody*: Block
of skMatch:
stmtMatchSubject*: Expr
stmtMatchArms*: seq[MatchArm]
of skReturn:
stmtReturnValue*: Expr ## nil for bare return
of skBreak:
stmtBreakLabel*: string
of skContinue:
stmtContinueLabel*: string
of skStaticAssert:
stmtStaticAssertCond*: Expr
stmtStaticAssertMsg*: Expr
of skComptime:
stmtComptimeBlock*: Block
of skEmit:
stmtEmitExpr*: Expr
stmtEmitEvaluated*: string ## filled by sema CTFE
of skDecl:
stmtDecl*: Decl
# ---------------------------------------------------------------------------
# Type Parameters (for generics with trait bounds)
# ---------------------------------------------------------------------------
TypeParam* = object
name*: string
bounds*: seq[string] ## e.g. ["Comparable"] for <T: Comparable>
isLifetime*: bool ## true for lifetime params like 'a
# ---------------------------------------------------------------------------
# Declarations
# ---------------------------------------------------------------------------
DeclKind* = enum
dkFunc
dkStruct
dkEnum
dkUnion
dkInterface
dkImpl
dkModule
dkUse
dkConst
dkTypeAlias
dkExternFunc
dkExternVar
dkExternBlock
Param* = object
loc*: SourceLocation
name*: string
ptype*: TypeExpr
isVariadic*: bool
defaultValue*: Expr
StructField* = object
loc*: SourceLocation
isPublic*: bool
name*: string
ftype*: TypeExpr
EnumVariant* = object
loc*: SourceLocation
name*: string
fields*: seq[TypeExpr]
namedFields*: seq[tuple[name: string, ftype: TypeExpr]]
discriminant*: string
UnionField* = object
loc*: SourceLocation
name*: string
ftype*: TypeExpr
Decl* = ref object
loc*: SourceLocation
isPublic*: bool
declAttrs*: seq[string] ## attributes: @[Checked], @[Inline], etc.
case kind*: DeclKind
of dkFunc:
declFuncAsm*: bool
declFuncCallConv*: CallingConvention
declFuncConst*: bool ## const func — evaluable at compile time
declFuncIsAsync*: bool ## async func — returns Future<T>
declFuncName*: string
declFuncTypeParams*: seq[TypeParam]
declFuncParams*: seq[Param]
declFuncReturnType*: TypeExpr ## nil if void/inferred
declFuncBody*: Block ## nil for signature-only
of dkStruct:
declStructName*: string
declStructTypeParams*: seq[TypeParam]
declStructFields*: seq[StructField]
of dkEnum:
declEnumName*: string
declEnumBaseType*: TypeExpr
declEnumVariants*: seq[EnumVariant]
of dkUnion:
declUnionName*: string
declUnionFields*: seq[UnionField]
of dkInterface:
declInterfaceName*: string
declInterfaceAssocTypes*: seq[string] ## associated type names: type Output;
declInterfaceMethods*: seq[Decl] ## FuncDecl signatures only
of dkImpl:
declImplTypeName*: string
declImplTypeParams*: seq[TypeParam] ## type parameters for generic impl: extend Box<T>
declImplInterface*: string ## empty if not for interface
declImplAssocTypes*: seq[tuple[name: string, typ: TypeExpr]] ## type Output = int;
declImplMethods*: seq[Decl]
of dkModule:
declModuleName*: string
declModulePath*: seq[string]
declModuleItems*: seq[Decl]
of dkUse:
declUsePath*: seq[string]
declUseKind*: UseKind
declUseNames*: seq[string] ## for multi-import
declUseTargetOs*: string
of dkConst:
declConstName*: string
declConstType*: TypeExpr
declConstValue*: Expr
of dkTypeAlias:
declAliasName*: string
declAliasType*: TypeExpr
of dkExternFunc:
declExtFuncName*: string
declExtFuncDll*: string
declExtFuncCallConv*: CallingConvention
declExtFuncParams*: seq[Param]
declExtFuncVariadic*: bool
declExtFuncReturnType*: TypeExpr
of dkExternVar:
declExtVarName*: string
declExtVarType*: TypeExpr
of dkExternBlock:
declExtBlockDll*: string
declExtBlockCallConv*: CallingConvention
declExtBlockItems*: seq[Decl]
# ---------------------------------------------------------------------------
# Module (AST root)
# ---------------------------------------------------------------------------
Module* = ref object
name*: string
path*: seq[string]
items*: seq[Decl]
# Convenience constructors
proc newModule*(name: string, path: seq[string] = @[]): Module =
result = Module(name: name, path: path)
proc newBlock*(loc: SourceLocation): Block =
result = Block(loc: loc)
proc newLiteralExpr*(tok: Token): Expr =
result = Expr(kind: ekLiteral, loc: tok.loc, exprLit: tok)
proc newIdentExpr*(name: string, loc: SourceLocation): Expr =
result = Expr(kind: ekIdent, loc: loc, exprIdent: name)
proc newBinaryExpr*(op: TokenKind, left, right: Expr, loc: SourceLocation): Expr =
result = Expr(kind: ekBinary, loc: loc, exprBinaryOp: op, exprBinaryLeft: left, exprBinaryRight: right)
proc newUnaryExpr*(op: TokenKind, operand: Expr, loc: SourceLocation): Expr =
result = Expr(kind: ekUnary, loc: loc, exprUnaryOp: op, exprUnaryOperand: operand)
+708
View File
@@ -0,0 +1,708 @@
import std/[strformat, strutils, tables]
import hir, types, token, source_location
type
CBackend* = object
output*: string
indent*: int
varCounter*: int
declaredVars*: seq[string]
sliceTypeDefs*: seq[tuple[name: string, elem: string]] ## Generated Slice_<T> typedefs
proc cEscape(s: string): string =
## Escape a string for use as a C string literal.
result = ""
for c in s:
case c
of '\\': result.add("\\\\")
of '"': result.add("\\\"")
of '\n': result.add("\\n")
of '\r': result.add("\\r")
of '\t': result.add("\\t")
of '\0': result.add("\\0")
else: result.add(c)
proc initCBackend*(): CBackend =
result.output = ""
result.indent = 0
result.varCounter = 0
result.declaredVars = @[]
proc emit(be: var CBackend, s: string) =
be.output.add(s)
proc emitLine(be: var CBackend, s: string) =
for i in 0..<be.indent:
be.output.add(" ")
be.output.add(s)
be.output.add("\n")
proc emitIndent(be: var CBackend) =
for i in 0..<be.indent:
be.output.add(" ")
proc freshVar(be: var CBackend): string =
inc be.varCounter
result = &"__tmp_{be.varCounter}"
# Type conversion: Bux Type → C type string
proc typeToC*(be: var CBackend, typ: Type): string =
if typ == nil: return "void"
case typ.kind
of tkVoid: return "void"
of tkBool: return "bool"
of tkBool8: return "bool"
of tkBool16: return "bool"
of tkBool32: return "bool"
of tkChar8: return "char"
of tkChar16: return "char16_t"
of tkChar32: return "char32_t"
of tkStr: return "const char*"
of tkInt8: return "int8_t"
of tkInt16: return "int16_t"
of tkInt32: return "int32_t"
of tkInt64: return "int64_t"
of tkInt: return "int"
of tkUInt8: return "uint8_t"
of tkUInt16: return "uint16_t"
of tkUInt32: return "uint32_t"
of tkUInt64: return "uint64_t"
of tkUInt: return "unsigned int"
of tkFloat32: return "float"
of tkFloat64: return "double"
of tkPointer, tkRef, tkMutRef:
if typ.inner.len > 0:
return typeToC(be, typ.inner[0]) & "*"
return "void*"
of tkDynRef:
return typ.name & "_FatPtr"
of tkSlice:
let elemName = if typ.inner.len > 0: typeToC(be, typ.inner[0]) else: "void"
let sliceName = "Slice_" & elemName.replace(" ", "_").replace("*", "Ptr")
var alreadyDefined = false
for d in be.sliceTypeDefs:
if d.name == sliceName:
alreadyDefined = true
break
if not alreadyDefined:
be.sliceTypeDefs.add((name: sliceName, elem: elemName))
return sliceName
of tkNamed:
# Map common Bux type names to C types
case typ.name
of "String", "str": return "const char*"
of "int": return "int"
of "int8": return "int8_t"
of "int16": return "int16_t"
of "int32": return "int32_t"
of "int64": return "int64_t"
of "uint": return "unsigned int"
of "uint8": return "uint8_t"
of "uint16": return "uint16_t"
of "uint32": return "uint32_t"
of "uint64": return "uint64_t"
of "float32": return "float"
of "float64": return "double"
of "bool": return "bool"
else: return typ.name
of tkTuple: return "void*" # TODO: proper tuple struct
of tkFunc: return "void*" # TODO: function pointer
else: return "int"
proc operatorToC(op: TokenKind): string =
case op
of tkPlus: return "+"
of tkMinus: return "-"
of tkStar: return "*"
of tkSlash: return "/"
of tkPercent: return "%"
of tkAmp: return "&"
of tkPipe: return "|"
of tkCaret: return "^"
of tkShl: return "<<"
of tkShr: return ">>"
of tkAmpAmp: return "&&"
of tkPipePipe: return "||"
of tkEq: return "=="
of tkNe: return "!="
of tkLt: return "<"
of tkLe: return "<="
of tkGt: return ">"
of tkGe: return ">="
of tkBang: return "!"
of tkTilde: return "~"
of tkPlusPlus: return "++"
of tkMinusMinus: return "--"
of tkAssign: return "="
of tkPlusAssign: return "+="
of tkMinusAssign: return "-="
of tkStarAssign: return "*="
of tkSlashAssign: return "/="
of tkPercentAssign: return "%="
of tkAmpAssign: return "&="
of tkPipeAssign: return "|="
of tkCaretAssign: return "^="
of tkShlAssign: return "<<="
of tkShrAssign: return ">>="
else: return "?"
# Forward declaration
proc emitExpr(be: var CBackend, node: HirNode): string
proc emitStmt(be: var CBackend, node: HirNode)
proc emitExpr(be: var CBackend, node: HirNode): string =
if node == nil: return "0"
case node.kind
of hLit:
case node.litToken.kind
of tkBoolLiteral:
if node.litToken.text == "true": return "true"
else: return "false"
of tkStringLiteral:
var text = node.litToken.text
# Backtick raw string: strip backticks, escape content for C
if text.len >= 2 and text[0] == '`' and text[text.len-1] == '`':
text = "\"" & cEscape(text[1 ..< text.len-1]) & "\""
# If text has no surrounding quotes, it's from constFoldConstDecl (already unescaped)
elif text.len >= 2 and text[0] == '"' and text[text.len-1] == '"':
# Strip c8" c16" c32" prefixes — in C they are just regular string literals
if text.startsWith("c32\""):
text = "\"" & cEscape(text[4 ..< text.len-1]) & "\""
elif text.startsWith("c16\""):
text = "\"" & cEscape(text[4 ..< text.len-1]) & "\""
elif text.startsWith("c8\""):
text = "\"" & cEscape(text[3 ..< text.len-1]) & "\""
else:
text = "\"" & cEscape(text[1 ..< text.len-1]) & "\""
elif text.len >= 2 and text[0] == '"':
# Partial quote — escape anyway
text = "\"" & cEscape(text[1 ..< text.len]) & "\""
else:
# No quotes — from constFoldConstDecl, needs wrapping and escaping
text = "\"" & cEscape(text) & "\""
return text
of tkNull:
return "NULL"
else:
return node.litToken.text
of hVar:
return node.varName
of hSelf:
return "self"
of hUnary:
let operand = be.emitExpr(node.unaryOperand)
let op = operatorToC(node.unaryOp)
if node.unaryOp == tkStar:
return &"(*{operand})"
elif node.unaryOp == tkAmp:
return &"(&{operand})"
else:
return &"({op}{operand})"
of hBinary:
let left = be.emitExpr(node.binaryLeft)
let right = be.emitExpr(node.binaryRight)
let op = operatorToC(node.binaryOp)
return &"({left} {op} {right})"
of hCall:
var args: seq[string] = @[]
for arg in node.callArgs:
args.add(be.emitExpr(arg))
let argsStr = args.join(", ")
return &"{node.callCallee}({argsStr})"
of hCallIndirect:
let callee = be.emitExpr(node.callIndirectCallee)
var args: seq[string] = @[]
for arg in node.callIndirectArgs:
args.add(be.emitExpr(arg))
let argsStr = args.join(", ")
return &"({callee})({argsStr})"
of hLoad:
# Optimize: load(field_ptr(base, field)) → base.field (avoids & on temporaries)
if node.loadPtr != nil and node.loadPtr.kind == hFieldPtr:
let base = be.emitExpr(node.loadPtr.fieldPtrBase)
return &"({base}.{node.loadPtr.fieldName})"
# Optimize: load(arrow_field(base, field)) → base->field
if node.loadPtr != nil and node.loadPtr.kind == hArrowField:
let base = be.emitExpr(node.loadPtr.arrowFieldBase)
return &"({base}->{node.loadPtr.arrowFieldName})"
# Optimize: load(index_ptr(base, idx)) → base[idx]
if node.loadPtr != nil and node.loadPtr.kind == hIndexPtr:
let base = be.emitExpr(node.loadPtr.indexPtrBase)
let idx = be.emitExpr(node.loadPtr.indexPtrIndex)
return &"({base}[{idx}])"
let ptrExpr = be.emitExpr(node.loadPtr)
return &"(*{ptrExpr})"
of hFieldPtr:
let base = be.emitExpr(node.fieldPtrBase)
return &"(&({base}.{node.fieldName}))"
of hArrowField:
let base = be.emitExpr(node.arrowFieldBase)
return &"(&({base}->{node.arrowFieldName}))"
of hIndexPtr:
let base = be.emitExpr(node.indexPtrBase)
let idx = be.emitExpr(node.indexPtrIndex)
return &"(&({base}[{idx}]))"
of hStructInit:
# C99 compound literal: (StructName){.field1 = val1, .field2 = val2}
var fields: seq[string] = @[]
for f in node.structInitFields:
let val = be.emitExpr(f.value)
fields.add(&".{f.name} = {val}")
let fieldsStr = fields.join(", ")
return &"(({node.structInitName}){{{fieldsStr}}})"
of hSliceInit:
let sliceName = typeToC(be, node.typ)
var elems: seq[string] = @[]
for e in node.sliceInitElements:
elems.add(be.emitExpr(e))
let elemsStr = elems.join(", ")
let elemType = if node.typ.inner.len > 0: typeToC(be, node.typ.inner[0]) else: "void"
return &"({sliceName}){{.data = ({elemType}[]){{{elemsStr}}}, .len = {node.sliceInitLen}}}"
of hSliceIndex:
let base = be.emitExpr(node.sliceIndexBase)
let idx = be.emitExpr(node.sliceIndexIndex)
if node.sliceIndexBoundsCheck:
return &"(bux_bounds_check((size_t)({idx}), ({base}).len), ({base}).data[{idx}])"
else:
return &"({base}).data[{idx}]"
of hTupleInit:
var elems: seq[string] = @[]
for e in node.tupleInitElements:
elems.add(be.emitExpr(e))
return &"{{{elems.join(\", \")}}}"
of hCast:
let operand = be.emitExpr(node.castOperand)
let typ = typeToC(be, node.castType)
return &"(({typ}){operand})"
of hIs:
return "true" # TODO: proper type checking
of hSizeOf:
let typ = typeToC(be, node.sizeOfType)
return &"sizeof({typ})"
of hSpawn:
if node.spawnAsync:
# Async coroutine spawn
return &"bux_async_spawn({node.spawnCallee})"
else:
# OS thread spawn
var argsStr = "NULL"
if node.spawnArgs.len > 0:
argsStr = &"(void*){be.emitExpr(node.spawnArgs[0])}"
return &"bux_task_spawn((void* (*)(void*)){node.spawnCallee}, {argsStr})"
of hDynRef:
let data = be.emitExpr(node.dynRefData)
let iface = node.dynRefInterface
let concrete = node.dynRefConcreteType
return &"({iface}_FatPtr){{.data = {data}, .vtable = &{concrete}_{iface}_VTable}}"
of hDynCall:
let receiver = be.emitExpr(node.dynCallReceiver)
let methodName = node.dynCallMethod
var args: seq[string] = @[]
args.add(&"{receiver}.data")
for i in 1 ..< node.dynCallArgs.len:
args.add(be.emitExpr(node.dynCallArgs[i]))
let argsStr = args.join(", ")
return &"({receiver}.vtable->{methodName}({argsStr}))"
of hIf:
# Ternary expression
let cond = be.emitExpr(node.ifCond)
let thenE = be.emitExpr(node.ifThen)
let elseE = be.emitExpr(node.ifElse)
return &"({cond} ? {thenE} : {elseE})"
of hAssign:
let target = be.emitExpr(node.assignTarget)
let value = be.emitExpr(node.assignValue)
let op = operatorToC(node.assignOp)
return &"({target} {op} {value})"
of hBlock:
# For block expressions, just emit the last expression
if node.blockExpr != nil:
return be.emitExpr(node.blockExpr)
elif node.blockStmts.len > 0:
return be.emitExpr(node.blockStmts[^1])
return "0"
of hMatch:
return "0" # TODO: match expression lowering
else:
return "0"
proc emitStmt(be: var CBackend, node: HirNode) =
if node == nil: return
case node.kind
of hReturn:
if node.returnValue != nil:
let val = be.emitExpr(node.returnValue)
be.emitLine(&"return {val};")
else:
be.emitLine("return;")
of hIf:
let cond = be.emitExpr(node.ifCond)
be.emitLine(&"if ({cond}) {{")
inc be.indent
be.emitStmt(node.ifThen)
dec be.indent
if node.ifElse != nil:
be.emitLine("} else {")
inc be.indent
be.emitStmt(node.ifElse)
dec be.indent
be.emitLine("}")
of hWhile:
let cond = be.emitExpr(node.whileCond)
be.emitLine(&"while ({cond}) {{")
inc be.indent
be.emitStmt(node.whileBody)
dec be.indent
be.emitLine("}")
of hLoop:
be.emitLine("while (1) {")
inc be.indent
be.emitStmt(node.loopBody)
dec be.indent
be.emitLine("}")
of hBreak:
be.emitLine("break;")
of hContinue:
be.emitLine("continue;")
of hEmit:
be.emitLine(node.emitCode)
of hBlock:
if node.isScope:
be.emitLine("{")
inc be.indent
for stmt in node.blockStmts:
be.emitStmt(stmt)
if node.blockExpr != nil:
let val = be.emitExpr(node.blockExpr)
be.emitLine(&"{val};")
if node.isScope:
dec be.indent
be.emitLine("}")
of hAlloca:
let typ = typeToC(be, node.allocaType)
be.emitLine(&"{typ} {node.allocaName};")
of hStore:
let ptrExpr = be.emitExpr(node.storePtr)
let val = be.emitExpr(node.storeValue)
be.emitLine(&"{ptrExpr} = {val};")
of hAssign:
let target = be.emitExpr(node.assignTarget)
let value = be.emitExpr(node.assignValue)
let op = operatorToC(node.assignOp)
be.emitLine(&"{target} {op} {value};")
of hCall:
let expr = be.emitExpr(node)
be.emitLine(&"{expr};")
of hCallIndirect:
let expr = be.emitExpr(node)
be.emitLine(&"{expr};")
else:
# Expression statement
let expr = be.emitExpr(node)
be.emitLine(&"{expr};")
proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
let retType = typeToC(be, hfunc.retType)
var params: seq[string] = @[]
for p in hfunc.params:
params.add(&"{typeToC(be, p.typ)} {p.name}")
if params.len == 0:
params.add("void")
let paramsStr = params.join(", ")
be.emitLine(&"{retType} {hfunc.name}({paramsStr}) {{")
inc be.indent
if hfunc.body != nil:
if hfunc.body.kind == hBlock:
for stmt in hfunc.body.blockStmts:
be.emitStmt(stmt)
if hfunc.body.blockExpr != nil and hfunc.retType.kind != tkVoid:
let val = be.emitExpr(hfunc.body.blockExpr)
be.emitLine(&"return {val};")
else:
be.emitStmt(hfunc.body)
dec be.indent
be.emitLine("}")
be.emitLine("")
proc emitStruct*(be: var CBackend, name: string, fields: seq[tuple[name: string, typ: Type]]) =
be.emitLine(&"typedef struct {name} {{")
inc be.indent
for f in fields:
let typ = typeToC(be, f.typ)
be.emitLine(&"{typ} {f.name};")
dec be.indent
be.emitLine(&"}} {name};")
be.emitLine("")
proc emitEnum*(be: var CBackend, name: string, variants: seq[HirEnumVariant]) =
# Check if this is a simple enum (no data) or algebraic enum (with data)
var hasData = false
for v in variants:
if v.fields.len > 0 or v.namedFields.len > 0:
hasData = true
break
if not hasData:
# Simple enum - generate as before
be.emitLine(&"typedef enum {{")
inc be.indent
for i, v in variants:
if i < variants.len - 1:
be.emitLine(&"{name}_{v.name},")
else:
be.emitLine(&"{name}_{v.name}")
dec be.indent
be.emitLine(&"}} {name};")
be.emitLine("")
else:
# Algebraic enum - generate tagged union
# 1. Generate tag enum
be.emitLine(&"typedef enum {{")
inc be.indent
for i, v in variants:
if i < variants.len - 1:
be.emitLine(&"{name}_{v.name},")
else:
be.emitLine(&"{name}_{v.name}")
dec be.indent
be.emitLine(&"}} {name}_Tag;")
be.emitLine("")
# 2. Generate union for data
be.emitLine(&"typedef union {{")
inc be.indent
for v in variants:
if v.fields.len > 0:
# Positional fields
for i, f in v.fields:
let typ = typeToC(be, f)
be.emitLine(&"{typ} {v.name}_{i};")
elif v.namedFields.len > 0:
# Named fields - generate as struct
be.emitLine(&"struct {{")
inc be.indent
for nf in v.namedFields:
let typ = typeToC(be, nf.typ)
be.emitLine(&"{typ} {nf.name};")
dec be.indent
be.emitLine(&"}} {v.name};")
dec be.indent
be.emitLine(&"}} {name}_Data;")
be.emitLine("")
# 3. Generate main struct with tag + union
be.emitLine(&"typedef struct {{")
inc be.indent
be.emitLine(&"{name}_Tag tag;")
be.emitLine(&"{name}_Data data;")
dec be.indent
be.emitLine(&"}} {name};")
be.emitLine("")
proc emitExternDecl*(be: var CBackend, efunc: HirFunc) =
let retType = typeToC(be, efunc.retType)
var params: seq[string] = @[]
for p in efunc.params:
params.add(&"{typeToC(be, p.typ)} {p.name}")
if params.len == 0:
params.add("void")
let paramsStr = params.join(", ")
be.emitLine(&"extern {retType} {efunc.name}({paramsStr});")
proc collectSliceTypes(module: HirModule): seq[tuple[name: string, elem: string]] =
## Pre-pass: collect all slice types used in the module.
var dummyBe = initCBackend()
for f in module.funcs:
discard typeToC(dummyBe, f.retType)
for p in f.params:
discard typeToC(dummyBe, p.typ)
for ef in module.externFuncs:
discard typeToC(dummyBe, ef.retType)
for p in ef.params:
discard typeToC(dummyBe, p.typ)
for s in module.structs:
for f in s.fields:
discard typeToC(dummyBe, f.typ)
for c in module.consts:
if c.value != nil:
discard typeToC(dummyBe, c.value.typ)
return dummyBe.sliceTypeDefs
proc emitModule*(be: var CBackend, module: HirModule): string =
# Header
be.emitLine("/* Generated by Bux Compiler */")
be.emitLine("#include <stdio.h>")
be.emitLine("#include <stdlib.h>")
be.emitLine("#include <stdint.h>")
be.emitLine("#include <stdbool.h>")
be.emitLine("#include <string.h>")
be.emitLine("")
# Pre-collect slice types so we can emit forward declarations early
let sliceTypes = collectSliceTypes(module)
# Forward declarations
for s in module.structs:
be.emitLine(&"typedef struct {s.name} {s.name};")
if module.structs.len > 0:
be.emitLine("")
# Forward declarations for trait object fat pointers
for iface in module.interfaces:
if not iface.hasAssocTypes:
be.emitLine(&"typedef struct {iface.name}_FatPtr {iface.name}_FatPtr;")
if module.interfaces.len > 0:
be.emitLine("")
# Extern function declarations
if module.externFuncs.len > 0:
be.emitLine("/* Extern function declarations */")
for ef in module.externFuncs:
be.emitExternDecl(ef)
be.emitLine("")
# Const declarations as #define
if module.consts.len > 0:
be.emitLine("/* Constants */")
for c in module.consts:
let val = c.value
if val != nil and val.kind == hLit:
let tok = val.litToken
case tok.kind
of tkIntLiteral:
be.emitLine(&"#define {c.name} {tok.text}")
of tkStringLiteral:
be.emitLine(&"#define {c.name} \"{cEscape(tok.text)}\"")
of tkBoolLiteral:
be.emitLine(&"#define {c.name} {tok.text}")
else:
be.emitLine(&"/* const {c.name} (unsupported literal kind) */")
else:
be.emitLine(&"/* const {c.name} (complex expression) */")
be.emitLine("")
# Struct definitions
for s in module.structs:
be.emitStruct(s.name, s.fields)
# Enum definitions
for e in module.enums:
be.emitEnum(e.name, e.variants)
# Slice fat-pointer typedefs
if sliceTypes.len > 0:
be.emitLine("/* Slice types */")
for st in sliceTypes:
be.emitLine(&"typedef struct {{ {st.elem}* data; size_t len; }} {st.name};")
be.emitLine("")
# Forward declarations for all functions
for f in module.funcs:
let retType = typeToC(be, f.retType)
var params: seq[string] = @[]
for p in f.params:
params.add(typeToC(be, p.typ) & " " & p.name)
if params.len == 0:
params.add("void")
be.emitLine(retType & " " & f.name & "(" & params.join(", ") & ");")
be.emitLine("")
# Trait object vtable and fat pointer struct definitions
for iface in module.interfaces:
if iface.hasAssocTypes:
continue # Skip vtables for interfaces with associated types (not yet supported)
let ifaceName = iface.name
# VTable struct
be.emitLine(&"typedef struct {ifaceName}_VTable {{")
inc be.indent
for m in iface.methods:
var paramCtypes: seq[string] = @[]
for i, p in m.params:
if i == 0:
paramCtypes.add("void* self") # First param is always self (erased)
else:
paramCtypes.add(typeToC(be, p) & " param")
if paramCtypes.len == 0:
paramCtypes.add("void")
let ret = typeToC(be, m.ret)
let paramsStr = paramCtypes.join(", ")
be.emitLine(&"{ret} (*{m.name})({paramsStr});")
dec be.indent
be.emitLine(&"}} {ifaceName}_VTable;")
# Fat pointer struct
be.emitLine(&"typedef struct {ifaceName}_FatPtr {{")
inc be.indent
be.emitLine("void* data;")
be.emitLine(&"{ifaceName}_VTable* vtable;")
dec be.indent
be.emitLine(&"}} {ifaceName}_FatPtr;")
be.emitLine("")
# VTable instances
for vt in module.vtables:
if vt.hasAssocTypes:
continue # Skip vtables for interfaces with associated types
let varName = vt.concreteType & "_" & vt.interfaceName & "_VTable"
be.emitLine(&"{vt.interfaceName}_VTable {varName} = {{")
inc be.indent
for m in vt.methodNames:
be.emitLine(&".{m} = (void*){vt.concreteType}_{m},")
dec be.indent
be.emitLine("};")
be.emitLine("")
# Function definitions
var hasMain = false
for f in module.funcs:
be.emitFunc(f)
if f.name == "Main":
hasMain = true
# Generate C main wrapper if Bux Main exists
if hasMain:
be.emitLine("/* C entry point wrapper */")
be.emitLine("extern int g_argc;")
be.emitLine("extern char** g_argv;")
be.emitLine("int main(int argc, char** argv) {")
be.emitLine(" g_argc = argc;")
be.emitLine(" g_argv = argv;")
be.emitLine(" return Main();")
be.emitLine("}")
be.emitLine("")
return be.output
+608
View File
@@ -0,0 +1,608 @@
import std/[os, strutils, terminal, strformat, osproc, sets]
import lexer, parser, ast, sema, manifest, hir, hir_lower, c_backend, types, scope
type
ColorMode* = enum
cmAuto
cmOn
cmOff
GlobalOptions* = object
color*: ColorMode
quiet*: bool
verbose*: bool
proc printUsage*() =
echo """Bux Programming Language (bootstrap compiler)
Usage: bux [options] <command> [command-options]
Commands:
new <name> Create a new Bux package
init Initialize a Bux package in the current directory
add <name> [ver] Add a dependency (--path, --git)
install Resolve and install dependencies
build Build the current package
run Build and run the current package
check Type-check the current package
clean Remove build artifacts
help Show this help message
version Show version
Global options:
--color <auto|on|off> Control colored output (default: auto)
-q, --quiet Suppress non-error output
-v, --verbose Verbose output
"""
proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq[string], ok: bool] =
result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false)
result.rest = @[]
result.ok = true
var i = 0
while i < args.len:
let arg = args[i]
if arg == "--color":
if i + 1 >= args.len:
stderr.writeLine("error: --color requires an argument")
result.ok = false
return
inc i
case args[i].toLowerAscii()
of "auto": result.opts.color = cmAuto
of "on": result.opts.color = cmOn
of "off": result.opts.color = cmOff
else:
stderr.writeLine(&"error: unknown --color value '{args[i]}'")
result.ok = false
return
elif arg == "-q" or arg == "--quiet":
result.opts.quiet = true
elif arg == "-v" or arg == "--verbose":
result.opts.verbose = true
else:
result.rest.add(arg)
inc i
proc shouldUseColor(opts: GlobalOptions): bool =
case opts.color
of cmOn: true
of cmOff: false
of cmAuto: terminal.isatty(stdout)
proc printError(msg: string, useColor: bool) =
if useColor:
stdout.setForegroundColor(fgRed)
stdout.write("error: ")
stdout.resetAttributes()
stdout.writeLine(msg)
else:
stderr.writeLine("error: " & msg)
proc printInfo(msg: string, useColor: bool) =
if useColor:
stdout.setForegroundColor(fgCyan)
stdout.write("info: ")
stdout.resetAttributes()
stdout.writeLine(msg)
else:
echo("info: " & msg)
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
proc cmdNew*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
if args.len < 1:
printError("'new' requires a package name", useColor)
return 1
let name = args[0]
let root = getCurrentDir() / name
if dirExists(root):
printError(&"directory '{name}' already exists", useColor)
return 1
createDir(root / "src")
writeFile(root / "bux.toml", &"""[Package]
Name = "{name}"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
""")
writeFile(root / "src" / "Main.bux", """import Std::Io::PrintLine;
func Main() -> int {
PrintLine(c8"Hello, Bux!");
return 0;
}
""")
if not opts.quiet:
printInfo(&"Created Bux package '{name}'", useColor)
return 0
proc cmdInit*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = getCurrentDir()
if fileExists(root / "bux.toml"):
printError("bux.toml already exists", useColor)
return 1
let name = splitPath(root).tail
writeFile(root / "bux.toml", &"""[Package]
Name = "{name}"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
""")
if not dirExists(root / "src"):
createDir(root / "src")
if not opts.quiet:
printInfo(&"Initialized Bux package '{name}'", useColor)
return 0
proc cmdAdd*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = getCurrentDir()
let manifestPath = root / "bux.toml"
if not fileExists(manifestPath):
printError("no bux.toml found", useColor)
return 1
if args.len == 0:
printError("usage: bux add <name> [version] [--path <path>] [--git <url>]", useColor)
return 1
let depName = args[0]
var version = "*"
var path = ""
var gitUrl = ""
var i = 1
while i < args.len:
case args[i]
of "--path":
if i + 1 < args.len:
path = args[i + 1]
inc i
else:
printError("--path requires a value", useColor)
return 1
of "--git":
if i + 1 < args.len:
gitUrl = args[i + 1]
inc i
else:
printError("--git requires a value", useColor)
return 1
else:
version = args[i]
inc i
# Append to bux.toml
var depLine = ""
if path.len > 0:
depLine = &"{depName} = {{ Path = \"{path}\" }}"
elif gitUrl.len > 0:
depLine = &"{depName} = {{ Version = \"{version}\", Source = \"{gitUrl}\" }}"
else:
depLine = &"{depName} = \"{version}\""
var content = readFile(manifestPath)
# Ensure [Dependencies] section exists
if content.find("[Dependencies]") < 0:
content.add("\n[Dependencies]\n")
# Append dependency line
content.add(depLine & "\n")
writeFile(manifestPath, content)
if not opts.quiet:
printInfo(&"Added dependency '{depName}' to bux.toml", useColor)
return 0
proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = getCurrentDir()
let manifestPath = root / "bux.toml"
if not fileExists(manifestPath):
printError("no bux.toml found", useColor)
return 1
let man = loadManifest(manifestPath)
var lock = Lockfile(entries: @[])
let cacheDir = getHomeDir() / ".bux" / "packages"
if not dirExists(cacheDir):
createDir(cacheDir)
# Resolve each dependency
for dep in man.dependencies:
case dep.kind
of dkPath:
let absPath = if dep.path.isAbsolute: dep.path else: root / dep.path
if not dirExists(absPath):
printError(&"path dependency not found: {absPath}", useColor)
return 1
# Read dependency manifest
let depManifestPath = absPath / "bux.toml"
if fileExists(depManifestPath):
let depMan = loadManifest(depManifestPath)
lock.entries.add(LockEntry(name: dep.name, version: depMan.version, source: absPath))
else:
lock.entries.add(LockEntry(name: dep.name, version: "0.0.0", source: absPath))
if not opts.quiet:
printInfo(&"Resolved path dependency '{dep.name}' from {absPath}", useColor)
of dkGit:
let depDir = cacheDir / dep.name
if not dirExists(depDir):
if not opts.quiet:
printInfo(&"Cloning '{dep.name}' from {dep.gitUrl}...", useColor)
let (outp, code) = execCmdEx(&"git clone {dep.gitUrl} {depDir} 2>&1")
if code != 0:
printError(&"failed to clone {dep.gitUrl}: {outp}", useColor)
return 1
else:
if not opts.quiet:
printInfo(&"Using cached '{dep.name}' from {depDir}", useColor)
lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl))
of dkVersion:
# For version-based deps without a registry, we just record them
# TODO: lookup in registry
lock.entries.add(LockEntry(name: dep.name, version: dep.versionReq, source: "registry"))
if not opts.quiet:
printInfo(&"Recorded dependency '{dep.name}' = {dep.versionReq}", useColor)
# Save lockfile
let lockPath = root / "bux.lock"
saveLockfile(lockPath, lock)
if not opts.quiet:
printInfo(&"Generated {lockPath}", useColor)
return 0
proc collectStdlibDecls(stdlibDir: string): seq[Decl]
proc getDeclName(d: Decl): string
proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl]
proc collectDepDecls(lock: Lockfile, root: string, opts: GlobalOptions): seq[Decl]
proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = if args.len > 0: absolutePath(args[0]) else: getCurrentDir()
let manifestPath = root / "bux.toml"
if not fileExists(manifestPath):
printError("no bux.toml found", useColor)
return 1
let man = loadManifest(manifestPath)
let srcDir = root / "src"
if not dirExists(srcDir):
printError("no src/ directory found", useColor)
return 1
# Phase 1: Parse all .bux files and collect declarations
var allModuleItems: seq[Decl] = @[]
var foundMain = false
for kind, path in walkDir(srcDir):
if kind == pcFile and path.endsWith(".bux"):
let source = readFile(path)
let lexRes = tokenize(source, path)
if lexRes.hasErrors:
printError(&"lex errors in {path}", useColor)
for d in lexRes.diagnostics:
echo $d
return 1
let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0:
printError(&"parse errors in {path}", useColor)
for d in parseRes.diagnostics:
echo &"error: {d.message} at {d.loc}"
return 1
# Flatten declarations from module wrappers
for decl in parseRes.module.items:
if decl.kind == dkModule:
for sub in decl.declModuleItems:
allModuleItems.add(sub)
else:
allModuleItems.add(decl)
if splitFile(path).name == "Main":
foundMain = true
if not foundMain:
printError("no Main.bux found in src/", useColor)
return 1
# Phase 2: Merge with stdlib and check
var stdlibDir = ""
let stdlibSearchPaths = @[
getAppDir() / ".." / "library",
getAppDir() / "library",
getCurrentDir() / "library",
"/home/ziko/z-git/bux/bux/library",
]
for path in stdlibSearchPaths:
if dirExists(path):
stdlibDir = path
break
let stdlibDecls = collectStdlibDecls(stdlibDir)
let lock = loadLockfile(root / "bux.lock")
let depDecls = collectDepDecls(lock, root, opts)
let stdlibAndDeps = mergeDecls(stdlibDecls, depDecls)
let mergedItems = mergeDecls(stdlibAndDeps, allModuleItems)
var unifiedModule = newModule("main")
unifiedModule.items = mergedItems
let semaRes = analyze(unifiedModule)
if semaRes.hasErrors:
printError("type errors in project", useColor)
for d in semaRes.diagnostics:
let sev = if d.severity == sdsError: "error" else: "warning"
echo &"{sev}: {d.message} at {d.loc}"
return 1
if not opts.quiet:
printInfo("check passed", useColor)
return 0
proc collectStdlibDecls(stdlibDir: string): seq[Decl] =
result = @[]
if not dirExists(stdlibDir): return
for path in walkDirRec(stdlibDir):
if path.endsWith(".bux"):
let source = readFile(path)
let lexRes = tokenize(source, path)
if lexRes.hasErrors: continue
let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0: continue
for item in parseRes.module.items:
if item.kind == dkModule:
for sub in item.declModuleItems:
result.add(sub)
else:
result.add(item)
proc getDeclName(d: Decl): string =
case d.kind
of dkFunc: d.declFuncName
of dkExternFunc: d.declExtFuncName
of dkStruct: d.declStructName
of dkEnum: d.declEnumName
of dkUnion: d.declUnionName
of dkInterface: d.declInterfaceName
of dkConst: d.declConstName
of dkTypeAlias: d.declAliasName
else: ""
proc collectDepDecls(lock: Lockfile, root: string, opts: GlobalOptions): seq[Decl] =
## Collect declarations from all locked dependencies.
let cacheDir = getHomeDir() / ".bux" / "packages"
let useColor = shouldUseColor(opts)
for entry in lock.entries:
var depSrcDir = ""
if dirExists(entry.source):
# Path-based dependency
depSrcDir = entry.source / "src"
elif entry.source.startsWith("http") or entry.source.startsWith("git@"):
# Git-based dependency in cache
depSrcDir = cacheDir / entry.name / "src"
if depSrcDir == "" or not dirExists(depSrcDir):
continue
for kind, path in walkDir(depSrcDir):
if kind == pcFile and path.endsWith(".bux"):
let source = readFile(path)
let lexRes = tokenize(source, path)
if lexRes.hasErrors:
continue
let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0:
continue
for decl in parseRes.module.items:
if decl.kind == dkModule:
for sub in decl.declModuleItems:
result.add(sub)
else:
result.add(decl)
if not opts.quiet:
printInfo(&"Loaded dependency '{entry.name}' from {depSrcDir}", useColor)
proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] =
## Merge stdlib and user declarations.
## User funcs shadow stdlib funcs with the same name (simple overload avoidance).
var userNames: HashSet[string]
for d in userDecls:
let name = getDeclName(d)
if name != "":
userNames.incl(name)
result = @[]
for d in stdlibDecls:
let name = getDeclName(d)
if name == "" or name notin userNames:
result.add(d)
for d in userDecls:
result.add(d)
proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = if args.len > 0: absolutePath(args[0]) else: getCurrentDir()
let manifestPath = root / "bux.toml"
if not fileExists(manifestPath):
printError("no bux.toml found", useColor)
return 1
let man = loadManifest(manifestPath)
let srcDir = root / "src"
if not dirExists(srcDir):
printError("no src/ directory found", useColor)
return 1
# Create build directory
let buildDir = root / "build"
if not dirExists(buildDir):
createDir(buildDir)
# Find stdlib directory
var stdlibDir = ""
let stdlibSearchPaths = @[
getAppDir() / ".." / "library",
getAppDir() / "library",
root / "library",
"/home/ziko/z-git/bux/bux/library",
]
for path in stdlibSearchPaths:
if dirExists(path):
stdlibDir = path
break
# Collect stdlib declarations once
let stdlibDecls = collectStdlibDecls(stdlibDir)
# Collect dependency declarations from lockfile
let lock = loadLockfile(root / "bux.lock")
let depDecls = collectDepDecls(lock, root, opts)
# Phase 1: Parse all .bux files and collect declarations
var allModuleItems: seq[Decl] = @[]
var foundMain = false
for kind, path in walkDir(srcDir):
if kind == pcFile and path.endsWith(".bux"):
let source = readFile(path)
let lexRes = tokenize(source, path)
if lexRes.hasErrors:
printError(&"lex errors in {path}", useColor)
for d in lexRes.diagnostics:
echo $d
return 1
let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0:
printError(&"parse errors in {path}", useColor)
for d in parseRes.diagnostics:
echo &"error: {d.message} at {d.loc}"
return 1
# Flatten: extract declarations from module wrappers
for decl in parseRes.module.items:
if decl.kind == dkModule:
for sub in decl.declModuleItems:
allModuleItems.add(sub)
else:
allModuleItems.add(decl)
if splitFile(path).name == "Main":
foundMain = true
if not foundMain:
printError("no Main.bux found in src/", useColor)
return 1
# Phase 2: Merge stdlib + deps + project (later shadow earlier)
let stdlibAndDeps = mergeDecls(stdlibDecls, depDecls)
let mergedItems = mergeDecls(stdlibAndDeps, allModuleItems)
# Create unified module
var unifiedModule = newModule("main")
unifiedModule.items = mergedItems
# Phase 3: Sema + HIR + C codegen on unified module
let (semaRes, semaCtx) = analyzeFull(unifiedModule)
if semaRes.hasErrors:
printError("type errors in project", useColor)
for d in semaRes.diagnostics:
let sev = if d.severity == sdsError: "error" else: "warning"
echo &"{sev}: {d.message} at {d.loc}"
return 1
let hirMod = lowerModule(unifiedModule, semaCtx)
var cbe = initCBackend()
var allCCode = cbe.emitModule(hirMod)
# Write C file
let cFile = buildDir / "main.c"
writeFile(cFile, allCCode)
# Copy runtime and stdlib files
let runtimeDst = buildDir / "runtime.c"
let ioDst = buildDir / "io.c"
if stdlibDir == "":
printError("stdlib directory not found", useColor)
return 1
# Copy runtime.c
let runtimeSrc = stdlibDir / "runtime" / "runtime.c"
if fileExists(runtimeSrc):
copyFile(runtimeSrc, runtimeDst)
else:
printError("runtime.c not found in library/runtime/", useColor)
return 1
# Copy io.c
let ioSrc = stdlibDir / "runtime" / "io.c"
if fileExists(ioSrc):
copyFile(ioSrc, ioDst)
else:
printError("io.c not found in library/runtime/", useColor)
return 1
# Compile with cc
let outputName = if man.name != "": man.name else: "bux_out"
let outputFile = buildDir / outputName
let ccCmd = &"cc -O2 -pthread -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm 2>&1"
if opts.verbose:
printInfo(&"running: {ccCmd}", useColor)
let (output, exitCode) = execCmdEx(ccCmd)
if exitCode != 0:
printError("C compilation failed:", useColor)
echo output
return 1
if not opts.quiet:
printInfo(&"build: {outputFile}", useColor)
return 0
proc cmdRun*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let buildRes = cmdBuild(args, opts)
if buildRes != 0:
return buildRes
let root = if args.len > 0: absolutePath(args[0]) else: getCurrentDir()
let man = loadManifest(root / "bux.toml")
let outputName = if man.name != "": man.name else: "bux_out"
let outputFile = root / "build" / outputName
if not fileExists(outputFile):
printError("executable not found after build", useColor)
return 1
let exitCode = execCmd(outputFile)
return exitCode
proc cmdClean*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = getCurrentDir()
let buildDir = root / "build"
if dirExists(buildDir):
removeDir(buildDir)
if not opts.quiet:
printInfo("clean: build directory removed", useColor)
return 0
proc cmdVersion*(args: seq[string], opts: GlobalOptions): int =
echo "bux 0.1.0 (bootstrap)"
return 0
proc runCli*(args: seq[string]): int =
let (opts, rest, ok) = parseGlobalOptions(args)
if not ok:
return 1
if rest.len == 0:
printUsage()
return 0
let cmd = rest[0]
let cmdArgs = if rest.len > 1: rest[1..^1] else: @[]
case cmd
of "new": return cmdNew(cmdArgs, opts)
of "init": return cmdInit(cmdArgs, opts)
of "add": return cmdAdd(cmdArgs, opts)
of "install": return cmdInstall(cmdArgs, opts)
of "build": return cmdBuild(cmdArgs, opts)
of "run": return cmdRun(cmdArgs, opts)
of "check": return cmdCheck(cmdArgs, opts)
of "clean": return cmdClean(cmdArgs, opts)
of "version", "--version", "-v": return cmdVersion(cmdArgs, opts)
of "help", "--help", "-h":
printUsage()
return 0
else:
let useColor = shouldUseColor(opts)
printError(&"unknown command '{cmd}'", useColor)
return 1
+224
View File
@@ -0,0 +1,224 @@
import ast, types, token, source_location
type
HirKind* = enum
# Literals
hLit
hVar
hSelf
# Operations
hUnary
hBinary
hAssign
# Control flow
hIf
hWhile
hLoop
hBreak
hContinue
hReturn
# Memory
hAlloca
hLoad
hStore
hFieldPtr
hArrowField
hIndexPtr
hSliceIndex
# Functions
hCall
hCallIndirect
# Type operations
hCast
hIs
hSizeOf
# Concurrency
hSpawn
# Compile-time code generation
hEmit
# Trait objects (dynamic dispatch)
hDynRef
hDynCall
# Composite
hBlock
hStructInit
hSliceInit
hRange
hTupleInit
# Match (desugared to switch/branch later)
hMatch
HirNode* = ref object
loc*: SourceLocation
typ*: Type
case kind*: HirKind
of hLit:
litToken*: Token
of hVar:
varName*: string
of hSelf:
discard
of hUnary:
unaryOp*: TokenKind
unaryOperand*: HirNode
of hBinary:
binaryOp*: TokenKind
binaryLeft*: HirNode
binaryRight*: HirNode
of hAssign:
assignOp*: TokenKind
assignTarget*: HirNode
assignValue*: HirNode
of hIf:
ifCond*: HirNode
ifThen*: HirNode
ifElse*: HirNode
of hWhile:
whileCond*: HirNode
whileBody*: HirNode
of hLoop:
loopBody*: HirNode
of hBreak:
breakLabel*: string
of hContinue:
continueLabel*: string
of hReturn:
returnValue*: HirNode
of hAlloca:
allocaType*: Type
allocaName*: string
of hLoad:
loadPtr*: HirNode
of hStore:
storePtr*: HirNode
storeValue*: HirNode
of hFieldPtr:
fieldPtrBase*: HirNode
fieldName*: string
of hArrowField:
arrowFieldBase*: HirNode
arrowFieldName*: string
of hIndexPtr:
indexPtrBase*: HirNode
indexPtrIndex*: HirNode
of hCall:
callCallee*: string
callArgs*: seq[HirNode]
of hCallIndirect:
callIndirectCallee*: HirNode
callIndirectArgs*: seq[HirNode]
of hCast:
castOperand*: HirNode
castType*: Type
of hIs:
isOperand*: HirNode
isType*: Type
of hSizeOf:
sizeOfType*: Type
of hSpawn:
spawnCallee*: string
spawnArgs*: seq[HirNode]
spawnAsync*: bool
of hEmit:
emitCode*: string
of hDynRef:
dynRefData*: HirNode
dynRefInterface*: string
dynRefConcreteType*: string
of hDynCall:
dynCallReceiver*: HirNode
dynCallMethod*: string
dynCallArgs*: seq[HirNode]
of hBlock:
blockStmts*: seq[HirNode]
blockExpr*: HirNode
isScope*: bool
of hStructInit:
structInitName*: string
structInitFields*: seq[tuple[name: string, value: HirNode]]
of hSliceInit:
sliceInitElements*: seq[HirNode]
sliceInitLen*: int
of hSliceIndex:
sliceIndexBase*: HirNode
sliceIndexIndex*: HirNode
sliceIndexBoundsCheck*: bool
of hRange:
rangeLo*: HirNode
rangeHi*: HirNode
rangeInclusive*: bool
of hTupleInit:
tupleInitElements*: seq[HirNode]
of hMatch:
matchSubject*: HirNode
matchArms*: seq[HirMatchArm]
HirMatchArm* = object
pattern*: Pattern
body*: HirNode
HirFunc* = object
name*: string
params*: seq[tuple[name: string, typ: Type]]
retType*: Type
body*: HirNode
isPublic*: bool
HirEnumVariant* = object
name*: string
fields*: seq[Type] # Positional fields for algebraic enums
namedFields*: seq[tuple[name: string, typ: Type]] # Named fields
HirModule* = object
funcs*: seq[HirFunc]
externFuncs*: seq[HirFunc] # Functions declared with extern (no body)
structs*: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]]
enums*: seq[tuple[name: string, variants: seq[HirEnumVariant]]]
consts*: seq[tuple[name: string, typ: Type, value: HirNode]]
interfaces*: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]]
vtables*: seq[tuple[interfaceName: string, concreteType: string, methodNames: seq[string], hasAssocTypes: bool]]
# Constructor helpers
proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hLit, litToken: tok, typ: typ, loc: loc)
proc hirVar*(name: string, typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hVar, varName: name, typ: typ, loc: loc)
proc hirSelf*(typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hSelf, typ: typ, loc: loc)
proc hirUnary*(op: TokenKind, operand: HirNode, typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hUnary, unaryOp: op, unaryOperand: operand, typ: typ, loc: loc)
proc hirBinary*(op: TokenKind, left, right: HirNode, typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hBinary, binaryOp: op, binaryLeft: left, binaryRight: right, typ: typ, loc: loc)
proc hirCall*(callee: string, args: seq[HirNode], typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hCall, callCallee: callee, callArgs: args, typ: typ, loc: loc)
proc hirReturn*(value: HirNode, loc: SourceLocation): HirNode =
HirNode(kind: hReturn, returnValue: value, typ: makeVoid(), loc: loc)
proc hirBlock*(stmts: seq[HirNode], expr: HirNode, typ: Type, loc: SourceLocation, isScope: bool = false): HirNode =
HirNode(kind: hBlock, blockStmts: stmts, blockExpr: expr, typ: typ, loc: loc, isScope: isScope)
proc hirAlloca*(name: string, typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hAlloca, allocaType: typ, allocaName: name, typ: makePointer(typ), loc: loc)
proc hirStore*(ptrNode, value: HirNode, loc: SourceLocation): HirNode =
HirNode(kind: hStore, storePtr: ptrNode, storeValue: value, typ: makeVoid(), loc: loc)
proc hirLoad*(ptrNode: HirNode, typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hLoad, loadPtr: ptrNode, typ: typ, loc: loc)
proc hirEmit*(code: string, loc: SourceLocation): HirNode =
HirNode(kind: hEmit, emitCode: code, typ: makeVoid(), loc: loc)
proc hirDynRef*(data: HirNode, interfaceName, concreteType: string, loc: SourceLocation): HirNode =
HirNode(kind: hDynRef, dynRefData: data, dynRefInterface: interfaceName,
dynRefConcreteType: concreteType, typ: makeDynRef(interfaceName), loc: loc)
proc hirDynCall*(receiver: HirNode, methodName: string, args: seq[HirNode], typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hDynCall, dynCallReceiver: receiver, dynCallMethod: methodName,
dynCallArgs: args, typ: typ, loc: loc)
File diff suppressed because it is too large Load Diff
+619
View File
@@ -0,0 +1,619 @@
import std/[strutils, strformat]
import token, source_location
type
LexerDiagnosticSeverity* = enum
ldsWarning
ldsError
LexerDiagnostic* = object
severity*: LexerDiagnosticSeverity
loc*: SourceLocation
message*: string
LexerResult* = object
tokens*: seq[Token]
diagnostics*: seq[LexerDiagnostic]
proc hasErrors*(res: LexerResult): bool =
for d in res.diagnostics:
if d.severity == ldsError:
return true
return false
proc `$`*(d: LexerDiagnostic): string =
let sev = if d.severity == ldsError: "error" else: "warning"
result = &"{sev}: {d.message} at {d.loc}"
type
Lexer* = object
source: string
sourceName: string
pos: int
line: uint32
col: uint32
tokens: seq[Token]
diagnostics: seq[LexerDiagnostic]
proc initLexer*(source, sourceName: string): Lexer =
result.source = source
result.sourceName = sourceName
result.pos = 0
result.line = 1
result.col = 1
proc isAtEnd(lex: Lexer): bool =
lex.pos >= lex.source.len
proc peek(lex: Lexer, ahead: int = 0): char =
let i = lex.pos + ahead
if i < lex.source.len:
return lex.source[i]
return '\0'
proc advance(lex: var Lexer): char =
result = lex.peek()
if not lex.isAtEnd():
inc lex.pos
if result == '\n':
inc lex.line
lex.col = 1
else:
inc lex.col
proc match(lex: var Lexer, expected: char): bool =
if lex.isAtEnd(): return false
if lex.peek() != expected: return false
discard lex.advance()
return true
proc matchStr(lex: var Lexer, s: string): bool =
for i, c in s:
if lex.peek(i) != c:
return false
for _ in s:
discard lex.advance()
return true
proc currentLocation(lex: Lexer): SourceLocation =
result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos))
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
proc emitWarning(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsWarning, loc: loc, message: message))
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
let text = lex.source[startPos ..< lex.pos]
result = Token(kind: kind, text: text, loc: startLoc)
# ---------------------------------------------------------------------------
# Whitespace / comments
# ---------------------------------------------------------------------------
proc skipLineComment(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() != '\n':
discard lex.advance()
proc skipBlockComment(lex: var Lexer) =
let startLoc = lex.currentLocation()
var depth = 1
while not lex.isAtEnd() and depth > 0:
if lex.peek() == '/' and lex.peek(1) == '*':
discard lex.advance()
discard lex.advance()
inc depth
elif lex.peek() == '*' and lex.peek(1) == '/':
discard lex.advance()
discard lex.advance()
dec depth
else:
discard lex.advance()
if depth > 0:
lex.emitError(startLoc, "unterminated block comment")
proc skipWhitespace(lex: var Lexer) =
while not lex.isAtEnd():
let c = lex.peek()
if c in {' ', '\t', '\r'}:
discard lex.advance()
elif c == '/' and lex.peek(1) == '/':
lex.skipLineComment()
elif c == '/' and lex.peek(1) == '*':
discard lex.advance()
discard lex.advance()
lex.skipBlockComment()
else:
break
# ---------------------------------------------------------------------------
# Identifiers
# ---------------------------------------------------------------------------
proc isIdentStart(c: char): bool =
c in {'a'..'z', 'A'..'Z', '_'}
proc isIdentChar(c: char): bool =
c in {'a'..'z', 'A'..'Z', '0'..'9', '_'}
proc scanIdent(lex: var Lexer, startLoc: SourceLocation): Token =
let startPos = lex.pos
while not lex.isAtEnd() and isIdentChar(lex.peek()):
discard lex.advance()
let text = lex.source[startPos ..< lex.pos]
var kind = keywordKind(text)
if text == "_":
kind = tkUnderscore
result = Token(kind: kind, text: text, loc: startLoc)
# ---------------------------------------------------------------------------
# Numbers
# ---------------------------------------------------------------------------
proc scanIntSuffix(lex: var Lexer) =
# i8, i16, i32, i64, u8, u16, u32, u64, f32, f64
if lex.isAtEnd(): return
let c = lex.peek()
if c in {'i', 'u', 'f'}:
discard lex.advance()
while not lex.isAtEnd() and lex.peek() in {'0'..'9'}:
discard lex.advance()
proc scanHexDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0'..'9', 'a'..'f', 'A'..'F'}:
discard lex.advance()
proc scanBinDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0', '1'}:
discard lex.advance()
proc scanOctDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0'..'7'}:
discard lex.advance()
proc scanDecDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0'..'9'}:
discard lex.advance()
proc scanNumber(lex: var Lexer, startLoc: SourceLocation): Token =
let startPos = lex.pos
var isFloat = false
if lex.peek() == '0' and lex.peek(1) in {'x', 'X', 'b', 'B', 'o', 'O'}:
discard lex.advance() # '0'
let prefix = lex.advance()
case prefix
of 'x', 'X': lex.scanHexDigits()
of 'b', 'B': lex.scanBinDigits()
of 'o', 'O': lex.scanOctDigits()
else: discard
lex.scanIntSuffix()
return lex.makeToken(tkIntLiteral, startLoc, startPos)
lex.scanDecDigits()
# Fractional part
if lex.peek() == '.' and lex.peek(1) in {'0'..'9'}:
isFloat = true
discard lex.advance() # '.'
lex.scanDecDigits()
# Exponent
if lex.peek() in {'e', 'E'}:
isFloat = true
discard lex.advance()
if lex.peek() in {'+', '-'}:
discard lex.advance()
if lex.peek() notin {'0'..'9'}:
lex.emitError(lex.currentLocation(), "expected digits in exponent")
else:
lex.scanDecDigits()
if isFloat:
# Optional f32/f64 suffix
if lex.peek() == 'f' and lex.peek(1) in {'3', '6'}:
discard lex.advance()
discard lex.advance()
result = lex.makeToken(tkFloatLiteral, startLoc, startPos)
else:
lex.scanIntSuffix()
result = lex.makeToken(tkIntLiteral, startLoc, startPos)
# ---------------------------------------------------------------------------
# Strings and chars
# ---------------------------------------------------------------------------
proc scanEscapeSequence(lex: var Lexer): string =
if lex.isAtEnd():
return ""
let c = lex.advance()
case c
of '\\': result = "\\"
of '"': result = "\""
of '\'': result = "\'"
of 'n': result = "\n"
of 'r': result = "\r"
of 't': result = "\t"
of '0': result = "\0"
of 'x':
var hexVal = ""
for _ in 0..<2:
if lex.isAtEnd() or lex.peek() notin {'0'..'9', 'a'..'f', 'A'..'F'}:
lex.emitError(lex.currentLocation(), "expected two hex digits after \\x")
break
hexVal.add(lex.advance())
if hexVal.len == 2:
try:
let code = parseHexInt(hexVal)
result = $chr(code)
except ValueError:
lex.emitError(lex.currentLocation(), &"invalid hex escape \\x{hexVal}")
result = ""
else:
result = ""
else:
lex.emitError(lex.currentLocation(), &"unknown escape sequence \\\\{c}")
result = $c
proc scanString(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token =
let startPos = lex.pos - prefixLen
# prefixLen characters before the opening quote were already consumed by caller
# but we need to handle the quote itself
# Collect resolved string content to properly handle escape sequences
var resolved = ""
if lex.peek() == '"':
discard lex.advance()
while not lex.isAtEnd() and lex.peek() != '"':
if lex.peek() == '\n':
lex.emitError(lex.currentLocation(), "unterminated string literal")
break
if lex.peek() == '\\':
discard lex.advance()
resolved.add(lex.scanEscapeSequence())
else:
resolved.add(lex.advance())
if lex.isAtEnd():
lex.emitError(startLoc, "unterminated string literal")
else:
discard lex.advance() # closing "
# Rebuild text with resolved escapes: prefix + " + resolved + "
var text = lex.source[startPos ..< startPos + prefixLen]
text.add('"')
text.add(resolved)
text.add('"')
result = Token(kind: tkStringLiteral, text: text, loc: startLoc)
proc scanBacktickString(lex: var Lexer, startLoc: SourceLocation): Token =
## Scan a backtick-delimited raw string literal: content is literal,
## no escape processing, newlines are preserved.
let startPos = lex.pos - 1 # include the opening backtick
while not lex.isAtEnd() and lex.peek() != '`':
discard lex.advance()
if lex.isAtEnd():
lex.emitError(startLoc, "unterminated backtick string literal")
else:
discard lex.advance() # closing backtick
result = lex.makeToken(tkStringLiteral, startLoc, startPos)
proc scanChar(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token =
let startPos = lex.pos - prefixLen
# Collect resolved char content to properly handle escape sequences
var resolved = ""
if lex.peek() == '\'':
discard lex.advance()
if lex.isAtEnd():
lex.emitError(startLoc, "unterminated char literal")
elif lex.peek() == '\n':
lex.emitError(lex.currentLocation(), "newline in char literal")
elif lex.peek() == '\\':
discard lex.advance()
resolved.add(lex.scanEscapeSequence())
else:
resolved.add(lex.advance())
if lex.isAtEnd() or lex.peek() != '\'':
lex.emitError(lex.currentLocation(), "expected closing ' for char literal")
else:
discard lex.advance()
# Rebuild text with resolved escape: prefix + ' + resolved + '
var text = lex.source[startPos ..< startPos + prefixLen]
text.add('\'')
text.add(resolved)
text.add('\'')
result = Token(kind: tkCharLiteral, text: text, loc: startLoc)
# ---------------------------------------------------------------------------
# Symbols / operators
# ---------------------------------------------------------------------------
proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
let startPos = lex.pos
let c1 = lex.advance()
template check2(c2: char, kind2: TokenKind, kind1: TokenKind) =
if lex.peek() == c2:
discard lex.advance()
return lex.makeToken(kind2, startLoc, startPos)
else:
return lex.makeToken(kind1, startLoc, startPos)
template check3(c2: char, kind2: TokenKind, c3: char, kind3: TokenKind, kind1: TokenKind) =
if lex.peek() == c2:
discard lex.advance()
if lex.peek() == c3:
discard lex.advance()
return lex.makeToken(kind3, startLoc, startPos)
return lex.makeToken(kind2, startLoc, startPos)
else:
return lex.makeToken(kind1, startLoc, startPos)
template checkEq(c2: char, kind2: TokenKind, kind1: TokenKind) =
check2(c2, kind2, kind1)
case c1
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
of '{': return lex.makeToken(tkLBrace, startLoc, startPos)
of '}': return lex.makeToken(tkRBrace, startLoc, startPos)
of '[': return lex.makeToken(tkLBracket, startLoc, startPos)
of ']': return lex.makeToken(tkRBracket, startLoc, startPos)
of ',': return lex.makeToken(tkComma, startLoc, startPos)
of ';': return lex.makeToken(tkSemicolon, startLoc, startPos)
of '@': return lex.makeToken(tkAt, startLoc, startPos)
of '?': return lex.makeToken(tkQuestion, startLoc, startPos)
of '~': return lex.makeToken(tkTilde, startLoc, startPos)
of ':': check2(':', tkColonColon, tkColon)
of '.':
if lex.peek() == '.' and lex.peek(1) == '.':
discard lex.advance()
discard lex.advance()
return lex.makeToken(tkDotDotDot, startLoc, startPos)
elif lex.peek() == '.' and lex.peek(1) == '=':
discard lex.advance()
discard lex.advance()
return lex.makeToken(tkDotDotEqual, startLoc, startPos)
elif lex.peek() == '.':
discard lex.advance()
return lex.makeToken(tkDotDot, startLoc, startPos)
else:
return lex.makeToken(tkDot, startLoc, startPos)
of '-':
if lex.peek() == '>':
discard lex.advance()
return lex.makeToken(tkArrow, startLoc, startPos)
elif lex.peek() == '-':
discard lex.advance()
return lex.makeToken(tkMinusMinus, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkMinusAssign, startLoc, startPos)
else:
return lex.makeToken(tkMinus, startLoc, startPos)
of '+':
if lex.peek() == '+':
discard lex.advance()
return lex.makeToken(tkPlusPlus, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkPlusAssign, startLoc, startPos)
else:
return lex.makeToken(tkPlus, startLoc, startPos)
of '*':
if lex.peek() == '*':
discard lex.advance()
return lex.makeToken(tkStarStar, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkStarAssign, startLoc, startPos)
else:
return lex.makeToken(tkStar, startLoc, startPos)
of '/':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkSlashAssign, startLoc, startPos)
else:
return lex.makeToken(tkSlash, startLoc, startPos)
of '%':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkPercentAssign, startLoc, startPos)
else:
return lex.makeToken(tkPercent, startLoc, startPos)
of '=':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkEq, startLoc, startPos)
elif lex.peek() == '>':
discard lex.advance()
return lex.makeToken(tkFatArrow, startLoc, startPos)
else:
return lex.makeToken(tkAssign, startLoc, startPos)
of '!':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkNe, startLoc, startPos)
else:
return lex.makeToken(tkBang, startLoc, startPos)
of '<':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkLe, startLoc, startPos)
elif lex.peek() == '<':
discard lex.advance()
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkShlAssign, startLoc, startPos)
return lex.makeToken(tkShl, startLoc, startPos)
else:
return lex.makeToken(tkLt, startLoc, startPos)
of '>':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkGe, startLoc, startPos)
elif lex.peek() == '>':
discard lex.advance()
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkShrAssign, startLoc, startPos)
return lex.makeToken(tkShr, startLoc, startPos)
else:
return lex.makeToken(tkGt, startLoc, startPos)
of '&':
if lex.peek() == '&':
discard lex.advance()
return lex.makeToken(tkAmpAmp, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkAmpAssign, startLoc, startPos)
else:
return lex.makeToken(tkAmp, startLoc, startPos)
of '|':
if lex.peek() == '|':
discard lex.advance()
return lex.makeToken(tkPipePipe, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkPipeAssign, startLoc, startPos)
else:
return lex.makeToken(tkPipe, startLoc, startPos)
of '^':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkCaretAssign, startLoc, startPos)
else:
return lex.makeToken(tkCaret, startLoc, startPos)
of '#':
# Check for intrinsics: #line, #column, #file, #function, #date, #time, #module
let afterHash = lex.peek()
if afterHash == 'l' and lex.matchStr("line"):
return lex.makeToken(tkHashLine, startLoc, startPos)
elif afterHash == 'c' and lex.matchStr("column"):
return lex.makeToken(tkHashColumn, startLoc, startPos)
elif afterHash == 'f' and lex.peek(1) == 'i' and lex.peek(2) == 'l' and lex.peek(3) == 'e':
discard lex.matchStr("file")
return lex.makeToken(tkHashFile, startLoc, startPos)
elif afterHash == 'f' and lex.peek(1) == 'u' and lex.peek(2) == 'n':
discard lex.matchStr("function")
return lex.makeToken(tkHashFunction, startLoc, startPos)
elif afterHash == 'd' and lex.matchStr("date"):
return lex.makeToken(tkHashDate, startLoc, startPos)
elif afterHash == 't' and lex.matchStr("time"):
return lex.makeToken(tkHashTime, startLoc, startPos)
elif afterHash == 'm' and lex.matchStr("module"):
return lex.makeToken(tkHashModule, startLoc, startPos)
elif afterHash == 'e' and lex.matchStr("emit"):
return lex.makeToken(tkHashEmit, startLoc, startPos)
else:
return lex.makeToken(tkHash, startLoc, startPos)
else:
lex.emitError(startLoc, &"unexpected character '{c1}'")
return lex.makeToken(tkUnknown, startLoc, startPos)
# ---------------------------------------------------------------------------
# Main scanning loop
# ---------------------------------------------------------------------------
proc nextToken(lex: var Lexer): Token =
lex.skipWhitespace()
let startLoc = lex.currentLocation()
let startPos = lex.pos
if lex.isAtEnd():
return Token(kind: tkEndOfFile, text: "", loc: startLoc)
let c = lex.peek()
if c == '\n':
discard lex.advance()
return Token(kind: tkNewLine, text: "\n", loc: startLoc)
# String prefixes: c8" c16" c32" — must come before ident check
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
let d = lex.peek(1)
if d == '8' and lex.peek(2) == '"':
discard lex.advance() # c
discard lex.advance() # 8
return lex.scanString(startLoc, 2)
elif d == '1' and lex.peek(2) == '6' and lex.peek(3) == '"':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanString(startLoc, 3)
elif d == '3' and lex.peek(2) == '2' and lex.peek(3) == '"':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanString(startLoc, 3)
if c == '"':
return lex.scanString(startLoc, 0)
# Backtick-delimited raw string: `...`
if c == '`':
discard lex.advance()
return lex.scanBacktickString(startLoc)
# Char prefixes: c8' c16' c32' — must come before ident check
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
let d = lex.peek(1)
if d == '8' and lex.peek(2) == '\'':
discard lex.advance()
discard lex.advance()
return lex.scanChar(startLoc, 2)
elif d == '1' and lex.peek(2) == '6' and lex.peek(3) == '\'':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanChar(startLoc, 3)
elif d == '3' and lex.peek(2) == '2' and lex.peek(3) == '\'':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanChar(startLoc, 3)
if c == '\'':
# Check if this is a lifetime (e.g., 'a) or char literal (e.g., 'a')
# Lifetime: ' followed by identifier chars, no closing '
# Char literal: ' followed by one char/escape, then closing '
let afterQuote = lex.peek(1)
if isIdentStart(afterQuote):
# Could be lifetime or char literal like 'x'
# If next char after ident start is NOT ', it's a lifetime
# (for char literals, the char/escape is consumed and then ')
# Simple heuristic: if peek(2) is ', it's a char literal; else lifetime
if lex.peek(2) == '\'':
return lex.scanChar(startLoc, 0)
elif lex.peek(2) == '\0':
return lex.scanChar(startLoc, 0)
else:
# Lifetime: consume ' and then identifier chars
discard lex.advance() # '
while not lex.isAtEnd() and isIdentChar(lex.peek()):
discard lex.advance()
return lex.makeToken(tkLifetime, startLoc, startPos)
else:
return lex.scanChar(startLoc, 0)
if isIdentStart(c):
return lex.scanIdent(startLoc)
if c in {'0'..'9'}:
return lex.scanNumber(startLoc)
return lex.scanSymbol(startLoc)
proc tokenize*(lex: var Lexer): LexerResult =
while true:
let tok = lex.nextToken()
lex.tokens.add(tok)
if tok.kind == tkEndOfFile:
break
result = LexerResult(tokens: lex.tokens, diagnostics: lex.diagnostics)
proc tokenize*(source, sourceName: string): LexerResult =
var lex = initLexer(source, sourceName)
result = lex.tokenize()
proc dumpTokens*(res: LexerResult): string =
for tok in res.tokens:
result.add($tok & "\n")
+6
View File
@@ -0,0 +1,6 @@
import std/os
import cli
when isMainModule:
let args = commandLineParams()
quit(runCli(args))
+274
View File
@@ -0,0 +1,274 @@
import std/[strutils, os, tables, sequtils, strformat]
type
PackageType* = enum
ptExecutable
ptSharedLibrary
ptStaticLibrary
ptSource
DepKind* = enum
dkVersion ## "1.0" or "*"
dkPath ## { Path = "../Lib" }
dkGit ## { Version = "1.4", Source = "https://..." }
Dependency* = object
name*: string
case kind*: DepKind
of dkVersion:
versionReq*: string
of dkPath:
path*: string
of dkGit:
gitUrl*: string
gitVersion*: string
Manifest* = object
name*: string
version*: string
pkgType*: PackageType
output*: string
dependencies*: seq[Dependency]
devDependencies*: seq[Dependency]
buildDependencies*: seq[Dependency]
# ---------------------------------------------------------------------------
# Extended TOML parser (supports inline tables and arrays)
# ---------------------------------------------------------------------------
type
TomlValueKind = enum
tvkString, tvkTable, tvkArray, tvkInlineTable
TomlValue = object
case kind*: TomlValueKind
of tvkString:
strVal*: string
of tvkTable:
tableVal*: OrderedTableRef[string, TomlValue]
of tvkArray:
arrayVal*: seq[TomlValue]
of tvkInlineTable:
inlineVal*: OrderedTableRef[string, TomlValue]
proc parseInlineTable(s: string): OrderedTableRef[string, TomlValue] =
result = newOrderedTable[string, TomlValue]()
var content = s.strip()
if content.len >= 2 and content[0] == '{' and content[^1] == '}':
content = content[1 ..< ^1].strip()
var i = 0
while i < content.len:
# skip whitespace
while i < content.len and content[i] in Whitespace:
inc i
if i >= content.len: break
# parse key
var keyStart = i
while i < content.len and content[i] notin {'=', ' ', '\t'}:
inc i
let key = content[keyStart ..< i].strip()
# skip to =
while i < content.len and content[i] in Whitespace:
inc i
if i >= content.len or content[i] != '=': break
inc i
while i < content.len and content[i] in Whitespace:
inc i
# parse value
var valStart = i
if i < content.len and content[i] == '"':
inc i
while i < content.len and content[i] != '"':
inc i
if i < content.len: inc i
let val = content[valStart + 1 ..< i - 1]
result[key] = TomlValue(kind: tvkString, strVal: val)
elif i < content.len and content[i] == '{':
# nested inline table (skip for now)
var braceCount = 1
inc i
while i < content.len and braceCount > 0:
if content[i] == '{': inc braceCount
elif content[i] == '}': dec braceCount
inc i
let val = content[valStart ..< i]
result[key] = TomlValue(kind: tvkInlineTable, inlineVal: newOrderedTable[string, TomlValue]())
else:
while i < content.len and content[i] notin {',', ' ', '\t'}:
inc i
let val = content[valStart ..< i].strip()
result[key] = TomlValue(kind: tvkString, strVal: val)
# skip to comma or end
while i < content.len and content[i] in Whitespace:
inc i
if i < content.len and content[i] == ',':
inc i
proc parseArray(s: string): seq[TomlValue] =
result = @[]
var content = s.strip()
if content.len >= 2 and content[0] == '[' and content[^1] == ']':
content = content[1 ..< ^1].strip()
if content.len == 0: return
for item in content.split(','):
let val = item.strip()
if val.len >= 2 and val[0] == '"' and val[^1] == '"':
result.add(TomlValue(kind: tvkString, strVal: val[1 ..< ^1]))
else:
result.add(TomlValue(kind: tvkString, strVal: val))
proc parseValue(valStr: string): TomlValue =
let val = valStr.strip()
if val.len >= 2 and val[0] == '"' and val[^1] == '"':
return TomlValue(kind: tvkString, strVal: val[1 ..< ^1])
elif val.len >= 2 and val[0] == '[' and val[^1] == ']':
return TomlValue(kind: tvkArray, arrayVal: parseArray(val))
elif val.len >= 2 and val[0] == '{' and val[^1] == '}':
return TomlValue(kind: tvkInlineTable, inlineVal: parseInlineTable(val))
else:
return TomlValue(kind: tvkString, strVal: val)
proc parseToml*(path: string): OrderedTableRef[string, TomlValue] =
result = newOrderedTable[string, TomlValue]()
if not fileExists(path):
return result
let content = readFile(path)
var currentTable = ""
var currentArrayTable = ""
for rawLine in content.splitLines():
let line = rawLine.strip()
if line.len == 0 or line.startsWith("#"):
continue
if line.startsWith("[[") and line.endsWith("]]"):
# Array of tables
currentArrayTable = line[2 ..< ^2]
currentTable = ""
let newTable = TomlValue(kind: tvkTable, tableVal: newOrderedTable[string, TomlValue]())
if not result.hasKey(currentArrayTable):
result[currentArrayTable] = TomlValue(kind: tvkArray, arrayVal: @[newTable])
else:
result[currentArrayTable].arrayVal.add(newTable)
elif line.startsWith("[") and line.endsWith("]"):
currentTable = line[1 ..< ^1]
currentArrayTable = ""
if not result.hasKey(currentTable):
result[currentTable] = TomlValue(kind: tvkTable, tableVal: newOrderedTable[string, TomlValue]())
else:
let eqIdx = line.find('=')
if eqIdx >= 0:
let key = line[0 ..< eqIdx].strip()
let valStr = line[eqIdx + 1 .. ^1].strip()
let val = parseValue(valStr)
if currentArrayTable != "" and result.hasKey(currentArrayTable):
let arr = result[currentArrayTable].arrayVal
if arr.len > 0:
arr[^1].tableVal[key] = val
elif currentTable != "" and result.hasKey(currentTable):
result[currentTable].tableVal[key] = val
else:
result[key] = val
proc parseDepTable(table: OrderedTableRef[string, TomlValue]): seq[Dependency] =
result = @[]
for name, val in table:
case val.kind
of tvkString:
result.add(Dependency(name: name, kind: dkVersion, versionReq: val.strVal))
of tvkInlineTable:
let t = val.inlineVal
if t.hasKey("Path"):
result.add(Dependency(name: name, kind: dkPath, path: t["Path"].strVal))
elif t.hasKey("Source"):
var ver = "*"
if t.hasKey("Version"):
ver = t["Version"].strVal
result.add(Dependency(name: name, kind: dkGit, gitUrl: t["Source"].strVal, gitVersion: ver))
elif t.hasKey("Version"):
result.add(Dependency(name: name, kind: dkVersion, versionReq: t["Version"].strVal))
else:
result.add(Dependency(name: name, kind: dkVersion, versionReq: "*"))
else:
discard
# ---------------------------------------------------------------------------
# Lockfile
# ---------------------------------------------------------------------------
type
LockEntry* = object
name*: string
version*: string
source*: string
checksum*: string
Lockfile* = object
entries*: seq[LockEntry]
proc loadLockfile*(path: string): Lockfile =
result.entries = @[]
if not fileExists(path):
return result
let data = parseToml(path)
if data.hasKey("Package"):
let arr = data["Package"]
if arr.kind == tvkArray:
for item in arr.arrayVal:
if item.kind == tvkTable:
let t = item.tableVal
var entry = LockEntry()
if t.hasKey("Name"): entry.name = t["Name"].strVal
if t.hasKey("Version"): entry.version = t["Version"].strVal
if t.hasKey("Source"): entry.source = t["Source"].strVal
if t.hasKey("Checksum"): entry.checksum = t["Checksum"].strVal
result.entries.add(entry)
proc saveLockfile*(path: string, lock: Lockfile) =
var lines: seq[string] = @[]
for entry in lock.entries:
lines.add("[[Package]]")
lines.add(&"Name = \"{entry.name}\"")
lines.add(&"Version = \"{entry.version}\"")
lines.add(&"Source = \"{entry.source}\"")
if entry.checksum.len > 0:
lines.add(&"Checksum = \"{entry.checksum}\"")
lines.add("")
writeFile(path, lines.join("\n"))
# ---------------------------------------------------------------------------
# Manifest loader
# ---------------------------------------------------------------------------
proc loadManifest*(path: string): Manifest =
let data = parseToml(path)
result.name = ""
result.version = "0.1.0"
result.pkgType = ptExecutable
result.output = "Bin"
result.dependencies = @[]
result.devDependencies = @[]
result.buildDependencies = @[]
if data.hasKey("Package"):
let pkg = data["Package"].tableVal
if pkg.hasKey("Name"):
result.name = pkg["Name"].strVal
if pkg.hasKey("Version"):
result.version = pkg["Version"].strVal
if pkg.hasKey("Type"):
case pkg["Type"].strVal.toLowerAscii()
of "bin", "executable": result.pkgType = ptExecutable
of "shared", "sharedlibrary", "dll", "so", "dylib": result.pkgType = ptSharedLibrary
of "static", "staticlibrary", "lib", "a": result.pkgType = ptStaticLibrary
of "source": result.pkgType = ptSource
else: result.pkgType = ptExecutable
if data.hasKey("Build"):
let bld = data["Build"].tableVal
if bld.hasKey("Output"):
result.output = bld["Output"].strVal
if data.hasKey("Dependencies"):
result.dependencies = parseDepTable(data["Dependencies"].tableVal)
if data.hasKey("DevDependencies"):
result.devDependencies = parseDepTable(data["DevDependencies"].tableVal)
if data.hasKey("BuildDependencies"):
result.buildDependencies = parseDepTable(data["BuildDependencies"].tableVal)
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
import std/tables
import types, ast, source_location
type
SymbolKind* = enum
skVar
skFunc
skType
skConst
skModule
Symbol* = ref object
kind*: SymbolKind
name*: string
typ*: Type
decl*: Decl ## optional back-reference to AST decl
isMutable*: bool
isPublic*: bool
Scope* = ref object
parent*: Scope
table*: Table[string, Symbol] ## O(1) lookup via hash table
proc newScope*(parent: Scope = nil): Scope =
result = Scope(parent: parent)
proc define*(scope: Scope, sym: Symbol): bool =
## Returns false if name already exists in this scope
if scope.table.hasKey(sym.name):
return false
scope.table[sym.name] = sym
return true
proc lookup*(scope: Scope, name: string): Symbol =
var cur = scope
while cur != nil:
if cur.table.hasKey(name):
return cur.table[name]
cur = cur.parent
return nil
proc lookupLocal*(scope: Scope, name: string): Symbol =
if scope.table.hasKey(name):
return scope.table[name]
return nil
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
type
SourceLocation* = object
line*: uint32 ## 1-based
column*: uint32 ## 1-based (UTF-8 byte offset in line)
offset*: uint32 ## byte offset from start of file
proc `$`*(loc: SourceLocation): string =
$loc.line & ":" & $loc.column
+336
View File
@@ -0,0 +1,336 @@
import source_location
type
TokenKind* = enum
##Literals
tkIntLiteral # 42 0xFF 0b1010 0o77
tkFloatLiteral # 3.14 1.0e-9
tkStringLiteral # "hello" c8"hello" c16"hello" c32"hello" `raw multi-line`
tkCharLiteral # 'A' c8'A' c16'A' c32'A'
tkBoolLiteral # true false
##Identifiers
tkIdent # foo Bar _x
tkUnderscore # _
##Control flow keywords
tkIf # if
tkElse # else
tkWhile # while
tkDo # do
tkLoop # loop
tkFor # for
tkIn # in
tkBreak # break
tkContinue # continue
tkReturn # return
tkMatch # match
##Declaration keywords
tkFunc # func
tkLet # let
tkVar # var
tkConst # const
tkType # type
tkStruct # struct
tkEnum # enum
tkUnion # union
tkInterface # interface
tkExtend # extend
tkModule # module
tkImport # import
tkPub # pub
tkExtern # extern
##Other keywords
tkAs # as
tkIs # is
tkNull # null
tkSelf # self
tkSuper # super
tkSizeOf # sizeof
tkOwn # own (gradual ownership transfer)
tkMut # mut (mutable reference)
tkDiscard # discard (evaluate and throw away)
tkAsync # async
tkAwait # await
tkSpawn # spawn
tkStaticAssert # static_assert
tkComptime # comptime
tkDyn # dyn
tkLifetime # 'a (lifetime parameter)
##Punctuation
tkLParen # (
tkRParen # )
tkLBrace # {
tkRBrace # }
tkLBracket # [
tkRBracket # ]
tkComma # ,
tkSemicolon # ;
tkColon # :
tkColonColon # ::
tkDot # .
tkDotDot # ..
tkDotDotDot # ...
tkDotDotEqual # ..=
tkArrow # ->
tkFatArrow # =>
tkAt # @
tkHash # #
tkQuestion # ?
##Arithmetic operators
tkPlus # +
tkMinus # -
tkStar # *
tkSlash # /
tkPercent # %
tkStarStar # **
tkPlusPlus # ++
tkMinusMinus # --
##Bitwise operators
tkAmp # &
tkPipe # |
tkCaret # ^
tkTilde # ~
tkShl # <<
tkShr # >>
##Logical operators
tkAmpAmp # &&
tkPipePipe # ||
tkBang # !
##Comparison operators
tkEq # ==
tkNe # !=
tkLt # <
tkLe # <=
tkGt # >
tkGe # >=
##Assignment operators
tkAssign # =
tkPlusAssign # +=
tkMinusAssign # -=
tkStarAssign # *=
tkSlashAssign # /=
tkPercentAssign # %=
tkAmpAssign # &=
tkPipeAssign # |=
tkCaretAssign # ^=
tkShlAssign # <<=
tkShrAssign # >>=
##Compile-time intrinsics
tkHashLine # #line
tkHashColumn # #column
tkHashFile # #file
tkHashFunction # #function
tkHashDate # #date
tkHashTime # #time
tkHashModule # #module
tkHashEmit # #emit
##Special
tkNewLine # significant newline (if grammar uses them)
tkEndOfFile # end of file
tkUnknown # unrecognized character
Token* = object
kind*: TokenKind
text*: string # original source spelling
loc*: SourceLocation
proc isKeyword*(kind: TokenKind): bool =
case kind
of tkIf, tkElse, tkWhile, tkDo, tkLoop, tkFor, tkIn,
tkBreak, tkContinue, tkReturn, tkMatch,
tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum,
tkUnion, tkInterface, tkExtend, tkModule, tkImport,
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn:
true
else:
false
proc isLiteral*(kind: TokenKind): bool =
case kind
of tkIntLiteral, tkFloatLiteral, tkStringLiteral, tkCharLiteral, tkBoolLiteral:
true
else:
false
proc isOperator*(kind: TokenKind): bool =
case kind
of tkPlus..tkShrAssign:
true
else:
false
proc isEof*(kind: TokenKind): bool = kind == tkEndOfFile
proc keywordKind*(text: string): TokenKind =
case text
of "func": tkFunc
of "let": tkLet
of "var": tkVar
of "const": tkConst
of "type": tkType
of "struct": tkStruct
of "enum": tkEnum
of "union": tkUnion
of "interface": tkInterface
of "extend": tkExtend
of "module": tkModule
of "import": tkImport
of "pub": tkPub
of "extern": tkExtern
of "if": tkIf
of "else": tkElse
of "while": tkWhile
of "do": tkDo
of "loop": tkLoop
of "for": tkFor
of "in": tkIn
of "break": tkBreak
of "continue": tkContinue
of "return": tkReturn
of "match": tkMatch
of "as": tkAs
of "is": tkIs
of "null": tkNull
of "self": tkSelf
of "super": tkSuper
of "sizeof": tkSizeOf
of "own": tkOwn
of "mut": tkMut
of "discard": tkDiscard
of "async": tkAsync
of "await": tkAwait
of "spawn": tkSpawn
of "static_assert": tkStaticAssert
of "comptime": tkComptime
of "dyn": tkDyn
of "true", "false": tkBoolLiteral
else: tkIdent
proc tokenKindName*(kind: TokenKind): string =
case kind
of tkIntLiteral: "integer literal"
of tkFloatLiteral: "float literal"
of tkStringLiteral: "string literal"
of tkCharLiteral: "char literal"
of tkBoolLiteral: "boolean literal"
of tkIdent: "identifier"
of tkUnderscore: "'_'"
of tkSizeOf: "'sizeof'"
of tkIf: "'if'"
of tkElse: "'else'"
of tkWhile: "'while'"
of tkDo: "'do'"
of tkLoop: "'loop'"
of tkFor: "'for'"
of tkIn: "'in'"
of tkBreak: "'break'"
of tkContinue: "'continue'"
of tkReturn: "'return'"
of tkMatch: "'match'"
of tkFunc: "'func'"
of tkLet: "'let'"
of tkVar: "'var'"
of tkConst: "'const'"
of tkType: "'type'"
of tkStruct: "'struct'"
of tkEnum: "'enum'"
of tkUnion: "'union'"
of tkInterface: "'interface'"
of tkExtend: "'extend'"
of tkModule: "'module'"
of tkImport: "'import'"
of tkPub: "'pub'"
of tkExtern: "'extern'"
of tkAs: "'as'"
of tkIs: "'is'"
of tkNull: "'null'"
of tkSelf: "'self'"
of tkSuper: "'super'"
of tkOwn: "'own'"
of tkMut: "'mut'"
of tkDiscard: "'discard'"
of tkAsync: "'async'"
of tkAwait: "'await'"
of tkSpawn: "'spawn'"
of tkStaticAssert: "'static_assert'"
of tkComptime: "'comptime'"
of tkDyn: "'dyn'"
of tkLifetime: "lifetime"
of tkLParen: "'('"
of tkRParen: "')'"
of tkLBrace: "'{'"
of tkRBrace: "'}'"
of tkLBracket: "'['"
of tkRBracket: "']'"
of tkComma: "','"
of tkSemicolon: "';'"
of tkColon: "':'"
of tkColonColon: "'::'"
of tkDot: "'.'"
of tkDotDot: "'..'"
of tkDotDotDot: "'...'"
of tkDotDotEqual: "'..='"
of tkArrow: "'->'"
of tkFatArrow: "'=>'"
of tkAt: "'@'"
of tkHash: "'#'"
of tkQuestion: "'?'"
of tkPlus: "'+'"
of tkMinus: "'-'"
of tkStar: "'*'"
of tkSlash: "'/'"
of tkPercent: "'%'"
of tkStarStar: "'**'"
of tkPlusPlus: "'++'"
of tkMinusMinus: "'--'"
of tkAmp: "'&'"
of tkPipe: "'|'"
of tkCaret: "'^'"
of tkTilde: "'~'"
of tkShl: "'<<'"
of tkShr: "'>>'"
of tkAmpAmp: "'&&'"
of tkPipePipe: "'||'"
of tkBang: "'!'"
of tkEq: "'=='"
of tkNe: "'!='"
of tkLt: "'<'"
of tkLe: "'<='"
of tkGt: "'>'"
of tkGe: "'>='"
of tkAssign: "'='"
of tkPlusAssign: "'+='"
of tkMinusAssign: "'-='"
of tkStarAssign: "'*='"
of tkSlashAssign: "'/='"
of tkPercentAssign: "'%='"
of tkAmpAssign: "'&='"
of tkPipeAssign: "'|='"
of tkCaretAssign: "'^='"
of tkShlAssign: "'<<='"
of tkShrAssign: "'>>='"
of tkHashLine: "'#line'"
of tkHashColumn: "'#column'"
of tkHashFile: "'#file'"
of tkHashFunction: "'#function'"
of tkHashDate: "'#date'"
of tkHashTime: "'#time'"
of tkHashModule: "'#module'"
of tkHashEmit: "'#emit'"
of tkNewLine: "newline"
of tkEndOfFile: "end of file"
of tkUnknown: "unknown token"
proc `$`*(tok: Token): string =
result = tokenKindName(tok.kind) & " '" & tok.text & "' @ " & $tok.loc
+231
View File
@@ -0,0 +1,231 @@
import std/[sequtils, strformat, strutils]
type
TypeKind* = enum
tkUnknown
tkVoid
tkBool
tkBool8
tkBool16
tkBool32
tkChar8
tkChar16
tkChar32
tkStr
tkInt8
tkInt16
tkInt32
tkInt64
tkInt
tkUInt8
tkUInt16
tkUInt32
tkUInt64
tkUInt
tkFloat32
tkFloat64
tkPointer
tkRef
tkMutRef
tkDynRef
tkSlice
tkRange
tkTuple
tkNamed
tkTypeParam
tkFunc
Type* = ref object
kind*: TypeKind
name*: string
inner*: seq[Type] ## for Pointer(pointee), Slice(element), Tuple(elements), Func(params+ret)
# Factories
proc makeUnknown*(): Type = Type(kind: tkUnknown)
proc makeVoid*(): Type = Type(kind: tkVoid)
proc makeBool*(): Type = Type(kind: tkBool)
proc makeBool8*(): Type = Type(kind: tkBool8)
proc makeBool16*(): Type = Type(kind: tkBool16)
proc makeBool32*(): Type = Type(kind: tkBool32)
proc makeChar8*(): Type = Type(kind: tkChar8)
proc makeChar16*(): Type = Type(kind: tkChar16)
proc makeChar32*(): Type = Type(kind: tkChar32)
proc makeStr*(): Type = Type(kind: tkStr)
proc makeInt8*(): Type = Type(kind: tkInt8)
proc makeInt16*(): Type = Type(kind: tkInt16)
proc makeInt32*(): Type = Type(kind: tkInt32)
proc makeInt64*(): Type = Type(kind: tkInt64)
proc makeInt*(): Type = Type(kind: tkInt)
proc makeUInt8*(): Type = Type(kind: tkUInt8)
proc makeUInt16*(): Type = Type(kind: tkUInt16)
proc makeUInt32*(): Type = Type(kind: tkUInt32)
proc makeUInt64*(): Type = Type(kind: tkUInt64)
proc makeUInt*(): Type = Type(kind: tkUInt)
proc makeFloat32*(): Type = Type(kind: tkFloat32)
proc makeFloat64*(): Type = Type(kind: tkFloat64)
proc makePointer*(pointee: Type): Type =
Type(kind: tkPointer, inner: @[pointee])
proc makeRef*(pointee: Type): Type =
Type(kind: tkRef, inner: @[pointee])
proc makeMutRef*(pointee: Type): Type =
Type(kind: tkMutRef, inner: @[pointee])
proc makeDynRef*(interfaceName: string): Type =
Type(kind: tkDynRef, name: interfaceName)
proc makeSlice*(element: Type): Type =
Type(kind: tkSlice, inner: @[element])
proc makeRange*(element: Type): Type =
Type(kind: tkRange, inner: @[element])
proc makeTuple*(elems: seq[Type]): Type =
Type(kind: tkTuple, inner: elems)
proc makeNamed*(name: string): Type =
Type(kind: tkNamed, name: name)
proc makeTypeParam*(name: string): Type =
Type(kind: tkTypeParam, name: name)
proc makeFunc*(params: seq[Type], ret: Type): Type =
Type(kind: tkFunc, inner: params & @[ret])
# Predicates
proc isUnknown*(t: Type): bool = t.kind == tkUnknown
proc isVoid*(t: Type): bool = t.kind == tkVoid
proc isBool*(t: Type): bool = t.kind in {tkBool, tkBool8, tkBool16, tkBool32}
proc isNumeric*(t: Type): bool =
if t.kind in {tkUnknown, tkNamed, tkTypeParam}: return true
t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt,
tkUInt8, tkUInt16, tkUInt32, tkUInt64, tkUInt,
tkFloat32, tkFloat64}
proc isInteger*(t: Type): bool =
if t.kind in {tkUnknown, tkNamed, tkTypeParam}: return true
t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt,
tkUInt8, tkUInt16, tkUInt32, tkUInt64, tkUInt}
proc isFloat*(t: Type): bool =
if t.kind in {tkUnknown, tkNamed, tkTypeParam}: return true
t.kind in {tkFloat32, tkFloat64}
proc isSigned*(t: Type): bool =
if t.kind in {tkUnknown, tkNamed, tkTypeParam}: return true
t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt}
proc isPointer*(t: Type): bool = t.kind in {tkPointer, tkRef, tkMutRef, tkDynRef}
proc isRawPointer*(t: Type): bool = t.kind == tkPointer
proc isRef*(t: Type): bool = t.kind == tkRef
proc isMutRef*(t: Type): bool = t.kind == tkMutRef
proc isDynRef*(t: Type): bool = t.kind == tkDynRef
proc isSlice*(t: Type): bool = t.kind == tkSlice
# Comparison
proc `==`*(a, b: Type): bool =
if a.isNil or b.isNil:
return a.isNil and b.isNil
if a.kind != b.kind: return false
if a.kind in {tkNamed, tkTypeParam} and a.name != b.name: return false
if a.inner.len != b.inner.len: return false
for i in 0 ..< a.inner.len:
if a.inner[i] != b.inner[i]: return false
return true
proc `!=`*(a, b: Type): bool = not (a == b)
# Assignment compatibility
proc isAssignableTo*(a, b: Type): bool =
if a.isUnknown or b.isUnknown: return true
if b.kind == tkTypeParam: return true
if a == b: return true
# float32 -> float64
if a.kind == tkFloat32 and b.kind == tkFloat64: return true
# int64 <-> int (on x64)
if a.kind == tkInt64 and b.kind == tkInt: return true
if a.kind == tkInt and b.kind == tkInt64: return true
if a.kind == tkUInt64 and b.kind == tkUInt: return true
if a.kind == tkUInt and b.kind == tkUInt64: return true
# int32 <-> int (for convenience in bootstrap)
if a.kind == tkInt32 and b.kind == tkInt: return true
if a.kind == tkInt and b.kind == tkInt32: return true
if a.kind == tkUInt32 and b.kind == tkInt: return true
if a.kind == tkInt and b.kind == tkUInt32: return true
if a.kind == tkUInt32 and b.kind == tkUInt: return true
if a.kind == tkUInt and b.kind == tkUInt32: return true
if a.kind == tkInt32 and b.kind == tkUInt32: return true
if a.kind == tkUInt32 and b.kind == tkInt32: return true
# smaller int -> int/uint
if b.kind == tkInt and a.kind in {tkInt8, tkInt16, tkInt32}: return true
if b.kind == tkUInt and a.kind in {tkUInt8, tkUInt16, tkUInt32}: return true
# int <-> uint (for convenience in bootstrap)
if a.kind == tkInt and b.kind == tkUInt: return true
if a.kind == tkUInt and b.kind == tkInt: return true
# *char8 -> String (C string literal to Bux String)
if a.kind == tkPointer and b.kind == tkStr:
if a.inner.len > 0 and a.inner[0].kind == tkChar8:
return true
# numeric exact match required otherwise
if a.isNumeric and b.isNumeric: return false
# bool across widths
if a.isBool and b.isBool: return true
# pointer to opaque / null pointer
if a.isPointer and b.isPointer:
if a.inner.len > 0 and a.inner[0].isUnknown:
return true
if b.inner.len > 0 and b.inner[0].isUnknown:
return true
# &mut T -> &T (mutable ref can coerce to shared ref)
if a.isMutRef and b.isRef:
if a.inner.len > 0 and b.inner.len > 0 and a.inner[0].isAssignableTo(b.inner[0]):
return true
# &mut T -> *T (mutable ref can coerce to raw pointer)
if a.isMutRef and b.isRawPointer:
if a.inner.len > 0 and b.inner.len > 0 and a.inner[0].isAssignableTo(b.inner[0]):
return true
# &T -> *T (shared ref can coerce to raw pointer)
if a.isRef and b.isRawPointer:
if a.inner.len > 0 and b.inner.len > 0 and a.inner[0].isAssignableTo(b.inner[0]):
return true
# &Concrete -> &dyn Trait (trait object coercion)
if b.isDynRef and a.isRef:
return true
if b.isDynRef and a.isMutRef:
return true
return false
# String representation
proc toString*(t: Type): string =
case t.kind
of tkUnknown: "?"
of tkVoid: "void"
of tkBool: "bool"
of tkBool8: "bool8"
of tkBool16: "bool16"
of tkBool32: "bool32"
of tkChar8: "char8"
of tkChar16: "char16"
of tkChar32: "char32"
of tkStr: "String"
of tkInt8: "int8"
of tkInt16: "int16"
of tkInt32: "int32"
of tkInt64: "int64"
of tkInt: "int"
of tkUInt8: "uint8"
of tkUInt16: "uint16"
of tkUInt32: "uint32"
of tkUInt64: "uint64"
of tkUInt: "uint"
of tkFloat32: "float32"
of tkFloat64: "float64"
of tkPointer: "*" & t.inner[0].toString
of tkRef: "&" & t.inner[0].toString
of tkMutRef: "&mut " & t.inner[0].toString
of tkDynRef: "&dyn " & t.name
of tkSlice:
if t.inner.len > 0: t.inner[0].toString & "[]"
else: "Slice<?>"
of tkRange:
if t.inner.len > 0: "Range<" & t.inner[0].toString & ">"
else: "Range<?>"
of tkTuple:
"(" & t.inner.mapIt(it.toString).join(", ") & ")"
of tkNamed: t.name
of tkTypeParam: t.name
of tkFunc:
if t.inner.len == 0: "func()"
else:
let params = t.inner[0..^2].mapIt(it.toString).join(", ")
let ret = t.inner[^1].toString
"func(" & params & ") -> " & ret