feat: semantic analysis + type checker (Phase 2)

This commit is contained in:
2026-05-30 21:36:42 +03:00
parent 713ab8e4d6
commit 8e637c89e7
6 changed files with 828 additions and 3 deletions
+9 -2
View File
@@ -1,5 +1,5 @@
import std/[os, strutils, terminal, strformat]
import lexer, parser, manifest
import lexer, parser, sema, manifest
type
ColorMode* = enum
@@ -170,8 +170,15 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
for d in parseRes.diagnostics:
echo &"error: {d.message} at {d.loc}"
return 1
let semaRes = analyze(parseRes.module)
if semaRes.hasErrors:
printError(&"type errors in {path}", 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 opts.verbose:
printInfo(&"parsed {path} ({lexRes.tokens.len} tokens, {parseRes.module.items.len} top-level declarations)", useColor)
printInfo(&"checked {path} ({lexRes.tokens.len} tokens, {parseRes.module.items.len} decls)", useColor)
if splitFile(path).name == "Main":
foundMain = true
+25 -1
View File
@@ -1,4 +1,4 @@
import std/[strformat, sequtils]
import std/[strformat, sequtils, strutils]
import token, source_location, lexer, ast
type
@@ -367,7 +367,9 @@ proc parsePrimary(p: var Parser): Expr =
return Expr(kind: ekSlice, loc: loc, exprSliceElements: elems)
of tkMatch:
discard p.advance()
p.structInitAllowed = false
let subject = p.parseExpr()
p.structInitAllowed = true
discard p.expect(tkLBrace, "expected '{' to start match")
var arms: seq[MatchArm] = @[]
while not p.check(tkRBrace) and not p.isAtEnd:
@@ -460,6 +462,22 @@ proc parsePostfix(p: var Parser): Expr =
discard p.advance()
let ty = p.parseType()
left = Expr(kind: ekIs, loc: loc, exprIsOperand: left, exprIsType: ty)
of tkLBrace:
if p.structInitAllowed and left.kind in {ekIdent, ekPath}:
discard p.advance()
var fields: seq[tuple[name: string, value: Expr]] = @[]
while not p.check(tkRBrace) and not p.isAtEnd:
let fieldName = p.expect(tkIdent, "expected field name").text
discard p.expect(tkColon, "expected ':'")
let fieldValue = p.parseExpr()
fields.add((fieldName, fieldValue))
if p.check(tkComma):
discard p.advance()
discard p.expect(tkRBrace, "expected '}'")
let typeName = if left.kind == ekIdent: left.exprIdent else: left.exprPath.join("::")
left = Expr(kind: ekStructInit, loc: loc, exprStructInitName: typeName, exprStructInitFields: fields)
else:
break
else:
break
return left
@@ -645,7 +663,9 @@ proc parseStmt(p: var Parser): Stmt =
stmtLetPattern: pat, stmtLetType: ty, stmtLetInit: initExpr)
of tkIf:
discard p.advance()
p.structInitAllowed = false
let cond = p.parseExpr()
p.structInitAllowed = true
let thenBlk = p.parseBlock()
var elseIfs: seq[ElseIf] = @[]
var elseBlk: Block = nil
@@ -664,7 +684,9 @@ proc parseStmt(p: var Parser): Stmt =
stmtIfElseIfs: elseIfs, stmtIfElse: elseBlk)
of tkWhile:
discard p.advance()
p.structInitAllowed = false
let cond = p.parseExpr()
p.structInitAllowed = true
let body = p.parseBlock()
return Stmt(kind: skWhile, loc: loc, stmtWhileCond: cond, stmtWhileBody: body)
of tkDo:
@@ -683,7 +705,9 @@ proc parseStmt(p: var Parser): Stmt =
discard p.advance()
let varName = p.expect(tkIdent, "expected loop variable name").text
discard p.expect(tkIn, "expected 'in' in for loop")
p.structInitAllowed = false
let iter = p.parseExpr()
p.structInitAllowed = true
let body = p.parseBlock()
return Stmt(kind: skFor, loc: loc, stmtForVar: varName, stmtForIter: iter, stmtForBody: body)
of tkMatch:
+47
View File
@@ -0,0 +1,47 @@
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
symbols*: seq[Symbol]
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
for s in scope.symbols:
if s.name == sym.name:
return false
scope.symbols.add(sym)
return true
proc lookup*(scope: Scope, name: string): Symbol =
var cur = scope
while cur != nil:
for s in cur.symbols:
if s.name == name:
return s
cur = cur.parent
return nil
proc lookupLocal*(scope: Scope, name: string): Symbol =
for s in scope.symbols:
if s.name == name:
return s
return nil
+486
View File
@@ -0,0 +1,486 @@
import std/[strformat, tables, sequtils, strutils]
import ast, types, scope, source_location, token
type
SemaDiagnosticSeverity* = enum
sdsWarning
sdsError
SemaDiagnostic* = object
severity*: SemaDiagnosticSeverity
loc*: SourceLocation
message*: string
SemaResult* = object
diagnostics*: seq[SemaDiagnostic]
Sema* = object
module*: Module
globalScope*: Scope
diagnostics*: seq[SemaDiagnostic]
# Built-in type mapping from name to Type
typeTable*: Table[string, Type]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
proc emitError(sema: var Sema, loc: SourceLocation, message: string) =
sema.diagnostics.add(SemaDiagnostic(severity: sdsError, loc: loc, message: message))
proc emitWarning(sema: var Sema, loc: SourceLocation, message: string) =
sema.diagnostics.add(SemaDiagnostic(severity: sdsWarning, loc: loc, message: message))
proc hasErrors*(res: SemaResult): bool =
for d in res.diagnostics:
if d.severity == sdsError:
return true
return false
# ---------------------------------------------------------------------------
# Type resolution from AST TypeExpr
# ---------------------------------------------------------------------------
proc resolveType(sema: var Sema, te: TypeExpr): Type =
if te == nil:
return makeUnknown()
case te.kind
of tekNamed:
let name = te.typeName
case name
of "void": return makeVoid()
of "bool": return makeBool()
of "bool8": return makeBool8()
of "bool16": return makeBool16()
of "bool32": return makeBool32()
of "char8": return makeChar8()
of "char16": return makeChar16()
of "char32": return makeChar32()
of "String", "str": return makeStr()
of "int8": return makeInt8()
of "int16": return makeInt16()
of "int32": return makeInt32()
of "int64": return makeInt64()
of "int": return makeInt()
of "uint8": return makeUInt8()
of "uint16": return makeUInt16()
of "uint32": return makeUInt32()
of "uint64": return makeUInt64()
of "uint": return makeUInt()
of "float32": return makeFloat32()
of "float64": return makeFloat64()
of "float": return makeFloat64()
else:
if sema.typeTable.hasKey(name):
return sema.typeTable[name]
return makeNamed(name)
of tekPath:
let fullName = te.pathSegments.join("::")
return makeNamed(fullName)
of tekPointer:
return makePointer(sema.resolveType(te.pointerPointee))
of tekSlice:
let elemType = sema.resolveType(te.sliceElement)
return makeSlice(elemType)
of tekTuple:
var elems: seq[Type] = @[]
for e in te.tupleElements:
elems.add(sema.resolveType(e))
return makeTuple(elems)
of tekSelf:
return makeNamed("self")
# ---------------------------------------------------------------------------
# First pass: collect global symbols
# ---------------------------------------------------------------------------
proc collectGlobals(sema: var Sema) =
for decl in sema.module.items:
case decl.kind
of dkFunc:
let sym = Symbol(kind: skFunc, name: decl.declFuncName, decl: decl,
isPublic: decl.isPublic)
# Build function type from params and return
var params: seq[Type] = @[]
for p in decl.declFuncParams:
params.add(sema.resolveType(p.ptype))
let retType = if decl.declFuncReturnType != nil: sema.resolveType(decl.declFuncReturnType) else: makeVoid()
sym.typ = makeFunc(params, retType)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declFuncName}'")
of dkStruct:
let t = makeNamed(decl.declStructName)
let sym = Symbol(kind: skType, name: decl.declStructName, typ: t,
decl: decl, isPublic: decl.isPublic)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declStructName}'")
sema.typeTable[decl.declStructName] = t
of dkEnum:
let t = makeNamed(decl.declEnumName)
let sym = Symbol(kind: skType, name: decl.declEnumName, typ: t,
decl: decl, isPublic: decl.isPublic)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declEnumName}'")
sema.typeTable[decl.declEnumName] = t
of dkUnion:
let t = makeNamed(decl.declUnionName)
let sym = Symbol(kind: skType, name: decl.declUnionName, typ: t,
decl: decl, isPublic: decl.isPublic)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declUnionName}'")
sema.typeTable[decl.declUnionName] = t
of dkConst:
let sym = Symbol(kind: skConst, name: decl.declConstName,
typ: sema.resolveType(decl.declConstType),
decl: decl, isPublic: decl.isPublic)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declConstName}'")
of dkTypeAlias:
let t = sema.resolveType(decl.declAliasType)
let sym = Symbol(kind: skType, name: decl.declAliasName, typ: t,
decl: decl, isPublic: decl.isPublic)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declAliasName}'")
sema.typeTable[decl.declAliasName] = t
of dkUse:
# Imports: for now just register the last segment as a module symbol
if decl.declUsePath.len > 0:
let name = decl.declUsePath[^1]
let sym = Symbol(kind: skModule, name: name, typ: makeUnknown(), isPublic: true)
discard sema.globalScope.define(sym)
else:
discard
# ---------------------------------------------------------------------------
# Expression type checking
# ---------------------------------------------------------------------------
proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type
proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type
proc checkExprList(sema: var Sema, exprs: seq[Expr], scope: Scope): seq[Type] =
for e in exprs:
result.add(sema.checkExpr(e, scope))
proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
if expr == nil:
return makeUnknown()
case expr.kind
of ekLiteral:
case expr.exprLit.kind
of tkIntLiteral: return makeInt()
of tkFloatLiteral: return makeFloat64()
of tkStringLiteral: return makeStr()
of tkCharLiteral: return makeChar32()
of tkBoolLiteral: return makeBool()
of tkNull: return makePointer(makeUnknown())
else: return makeUnknown()
of ekIdent:
let sym = scope.lookup(expr.exprIdent)
if sym == nil:
sema.emitError(expr.loc, &"undeclared identifier '{expr.exprIdent}'")
return makeUnknown()
if sym.typ == nil:
return makeUnknown()
return sym.typ
of ekSelf:
return makeNamed("self")
of ekPath:
let fullName = expr.exprPath.join("::")
let sym = scope.lookup(fullName)
if sym != nil:
return sym.typ
# Try looking up the first segment
let first = scope.lookup(expr.exprPath[0])
if first == nil:
sema.emitError(expr.loc, &"undeclared identifier '{expr.exprPath[0]}'")
return makeUnknown()
return first.typ
of ekUnary:
let operandType = sema.checkExpr(expr.exprUnaryOperand, scope)
case expr.exprUnaryOp
of tkBang:
if not operandType.isBool:
sema.emitError(expr.loc, "'!' requires bool operand")
return makeBool()
of tkMinus, tkTilde:
if not operandType.isNumeric:
sema.emitError(expr.loc, "unary '-' requires numeric operand")
return operandType
of tkStar:
if not operandType.isPointer:
sema.emitError(expr.loc, "dereference requires pointer operand")
return makeUnknown()
return operandType.inner[0]
of tkAmp:
return makePointer(operandType)
else:
return operandType
of ekPostfix:
let operandType = sema.checkExpr(expr.exprPostfixOperand, scope)
case expr.exprPostfixOp
of tkPlusPlus, tkMinusMinus:
if not operandType.isNumeric:
sema.emitError(expr.loc, "increment/decrement requires numeric operand")
return operandType
else:
return operandType
of ekBinary:
let left = sema.checkExpr(expr.exprBinaryLeft, scope)
let right = sema.checkExpr(expr.exprBinaryRight, scope)
case expr.exprBinaryOp
of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar:
if not left.isNumeric or not right.isNumeric:
sema.emitError(expr.loc, &"arithmetic operator requires numeric operands ({left.toString}, {right.toString})")
return makeUnknown()
# Result type is the wider of the two
if left.isFloat or right.isFloat:
if left.kind == tkFloat64 or right.kind == tkFloat64:
return makeFloat64()
return makeFloat32()
return left
of tkAmp, tkPipe, tkCaret, tkShl, tkShr:
if not left.isInteger or not right.isInteger:
sema.emitError(expr.loc, "bitwise operator requires integer operands")
return left
of tkAmpAmp, tkPipePipe:
if not left.isBool or not right.isBool:
sema.emitError(expr.loc, "logical operator requires bool operands")
return makeBool()
of tkEq, tkNe, tkLt, tkLe, tkGt, tkGe:
if not left.isAssignableTo(right) and not right.isAssignableTo(left):
sema.emitError(expr.loc, &"cannot compare types {left.toString} and {right.toString}")
return makeBool()
else:
return makeUnknown()
of ekAssign:
let target = sema.checkExpr(expr.exprAssignTarget, scope)
let value = sema.checkExpr(expr.exprAssignValue, scope)
if not value.isAssignableTo(target):
sema.emitError(expr.loc, &"cannot assign {value.toString} to {target.toString}")
return target
of ekTernary:
let cond = sema.checkExpr(expr.exprTernaryCond, scope)
if not cond.isBool:
sema.emitError(expr.loc, "ternary condition must be bool")
let thenType = sema.checkExpr(expr.exprTernaryThen, scope)
let elseType = sema.checkExpr(expr.exprTernaryElse, scope)
if thenType != elseType:
sema.emitError(expr.loc, "ternary branches must have same type")
return thenType
of ekRange:
let lo = sema.checkExpr(expr.exprRangeLo, scope)
let hi = sema.checkExpr(expr.exprRangeHi, scope)
if lo != hi:
sema.emitError(expr.loc, "range bounds must have same type")
return makeRange(lo)
of ekCall:
if expr.exprCallCallee == nil:
sema.emitError(expr.loc, "internal error: nil callee in call expression")
return makeUnknown()
let calleeType = sema.checkExpr(expr.exprCallCallee, scope)
var argTypes = sema.checkExprList(expr.exprCallArgs, scope)
if calleeType.kind == tkFunc:
let expectedParams = calleeType.inner[0..^2]
if argTypes.len != expectedParams.len:
sema.emitError(expr.loc, &"expected {expectedParams.len} arguments, got {argTypes.len}")
else:
for i in 0 ..< argTypes.len:
if not argTypes[i].isAssignableTo(expectedParams[i]):
sema.emitError(expr.loc, &"argument {i+1}: expected {expectedParams[i].toString}, got {argTypes[i].toString}")
return calleeType.inner[^1]
elif calleeType.kind == tkUnknown:
return makeUnknown()
else:
sema.emitError(expr.loc, &"cannot call non-function type {calleeType.toString}")
return makeUnknown()
of ekIndex:
let obj = sema.checkExpr(expr.exprIndexObj, scope)
let idx = sema.checkExpr(expr.exprIndexIdx, scope)
if not idx.isInteger:
sema.emitError(expr.loc, "index must be integer")
if obj.isSlice:
return obj.inner[0]
elif obj.isPointer:
return obj.inner[0]
else:
sema.emitError(expr.loc, "cannot index non-slice/non-pointer type")
return makeUnknown()
of ekField:
let obj = sema.checkExpr(expr.exprFieldObj, scope)
if obj.kind == tkNamed:
let sym = sema.globalScope.lookup(obj.name)
if sym != nil and sym.decl != nil and sym.decl.kind == dkStruct:
for f in sym.decl.declStructFields:
if f.name == expr.exprFieldName:
return sema.resolveType(f.ftype)
sema.emitError(expr.loc, &"struct '{obj.name}' has no field '{expr.exprFieldName}'")
else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
return makeUnknown()
of ekStructInit:
let sym = sema.globalScope.lookup(expr.exprStructInitName)
if sym == nil or sym.kind != skType:
sema.emitError(expr.loc, &"unknown struct type '{expr.exprStructInitName}'")
return makeUnknown()
return makeNamed(expr.exprStructInitName)
of ekSlice:
if expr.exprSliceElements.len == 0:
return makeSlice(makeUnknown())
let firstType = sema.checkExpr(expr.exprSliceElements[0], scope)
for i in 1 ..< expr.exprSliceElements.len:
let t = sema.checkExpr(expr.exprSliceElements[i], scope)
if t != firstType:
sema.emitError(expr.loc, "slice elements must have same type")
return makeSlice(firstType)
of ekTuple:
var elems: seq[Type] = @[]
for e in expr.exprTupleElements:
elems.add(sema.checkExpr(e, scope))
return makeTuple(elems)
of ekCast:
discard sema.checkExpr(expr.exprCastOperand, scope)
return sema.resolveType(expr.exprCastType)
of ekIs:
discard sema.checkExpr(expr.exprIsOperand, scope)
return makeBool()
of ekBlock:
var blockScope = newScope(scope)
var lastType = makeVoid()
for stmt in expr.exprBlock.stmts:
lastType = sema.checkStmt(stmt, blockScope)
return lastType
of ekMatch:
let subjectType = sema.checkExpr(expr.exprMatchSubject, scope)
var resultType = makeUnknown()
for arm in expr.exprMatchArms:
let armType = sema.checkExpr(arm.body, scope)
if resultType.isUnknown:
resultType = armType
elif armType != resultType and not armType.isUnknown:
sema.emitError(arm.body.loc, "match arm type mismatch")
return resultType
of ekSizeOf:
return makeInt()
of ekIntrinsic:
case expr.exprIntrinsic
of ikLine, ikColumn: return makeInt()
of ikFile, ikFunction, ikDate, ikTime, ikModule: return makeStr()
of ekSpread:
return sema.checkExpr(expr.exprSpreadOperand, scope)
# ---------------------------------------------------------------------------
# Statement type checking
# ---------------------------------------------------------------------------
proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
if stmt == nil:
return makeVoid()
case stmt.kind
of skExpr:
return sema.checkExpr(stmt.stmtExpr, scope)
of skLet:
let initType = sema.checkExpr(stmt.stmtLetInit, scope)
let declaredType = if stmt.stmtLetType != nil: sema.resolveType(stmt.stmtLetType) else: initType
if stmt.stmtLetType != nil and not initType.isAssignableTo(declaredType):
sema.emitError(stmt.loc, &"cannot assign {initType.toString} to {declaredType.toString}")
let sym = Symbol(kind: skVar, name: stmt.stmtLetName, typ: declaredType,
isMutable: stmt.stmtLetMut)
if not scope.define(sym):
sema.emitError(stmt.loc, &"duplicate variable '{stmt.stmtLetName}'")
return makeVoid()
of skIf:
let condType = sema.checkExpr(stmt.stmtIfCond, scope)
if not condType.isBool:
sema.emitError(stmt.loc, "if condition must be bool")
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtIfThen.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtIfThen.loc, exprBlock: stmt.stmtIfThen)), scope)
for elifBranch in stmt.stmtIfElseIfs:
let elifCond = sema.checkExpr(elifBranch.cond, scope)
if not elifCond.isBool:
sema.emitError(elifBranch.cond.loc, "else-if condition must be bool")
discard sema.checkStmt(Stmt(kind: skExpr, loc: elifBranch.blk.loc, stmtExpr: Expr(kind: ekBlock, loc: elifBranch.blk.loc, exprBlock: elifBranch.blk)), scope)
if stmt.stmtIfElse != nil:
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtIfElse.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtIfElse.loc, exprBlock: stmt.stmtIfElse)), scope)
return makeVoid()
of skWhile:
let condType = sema.checkExpr(stmt.stmtWhileCond, scope)
if not condType.isBool:
sema.emitError(stmt.loc, "while condition must be bool")
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtWhileBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtWhileBody.loc, exprBlock: stmt.stmtWhileBody)), scope)
return makeVoid()
of skDoWhile:
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtDoWhileBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtDoWhileBody.loc, exprBlock: stmt.stmtDoWhileBody)), scope)
let condType = sema.checkExpr(stmt.stmtDoWhileCond, scope)
if not condType.isBool:
sema.emitError(stmt.loc, "do-while condition must be bool")
return makeVoid()
of skLoop:
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtLoopBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtLoopBody.loc, exprBlock: stmt.stmtLoopBody)), scope)
return makeVoid()
of skFor:
discard sema.checkExpr(stmt.stmtForIter, scope)
var forScope = newScope(scope)
let iterSym = Symbol(kind: skVar, name: stmt.stmtForVar, typ: makeUnknown(), isMutable: true)
discard forScope.define(iterSym)
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtForBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtForBody.loc, exprBlock: stmt.stmtForBody)), forScope)
return makeVoid()
of skMatch:
discard sema.checkExpr(stmt.stmtMatchSubject, scope)
for arm in stmt.stmtMatchArms:
discard sema.checkExpr(arm.body, scope)
return makeVoid()
of skReturn:
if stmt.stmtReturnValue != nil:
discard sema.checkExpr(stmt.stmtReturnValue, scope)
return makeVoid()
of skBreak, skContinue:
return makeVoid()
of skDecl:
# Local declaration inside block
case stmt.stmtDecl.kind
of dkFunc:
sema.emitError(stmt.loc, "nested functions not yet supported")
else:
discard
return makeVoid()
# ---------------------------------------------------------------------------
# Function body checking
# ---------------------------------------------------------------------------
proc checkFunc(sema: var Sema, decl: Decl) =
if decl.declFuncBody == nil:
return
var funcScope = newScope(sema.globalScope)
# Add parameters
for p in decl.declFuncParams:
let pType = sema.resolveType(p.ptype)
let sym = Symbol(kind: skVar, name: p.name, typ: pType, isMutable: false)
discard funcScope.define(sym)
# Check body statements
for stmt in decl.declFuncBody.stmts:
discard sema.checkStmt(stmt, funcScope)
# ---------------------------------------------------------------------------
# Second pass: check all function bodies
# ---------------------------------------------------------------------------
proc checkBodies(sema: var Sema) =
for decl in sema.module.items:
case decl.kind
of dkFunc:
sema.checkFunc(decl)
else:
discard
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
proc analyze*(modu: Module): SemaResult =
var sema = Sema(module: modu, globalScope: newScope())
sema.collectGlobals()
sema.checkBodies()
result = SemaResult(diagnostics: sema.diagnostics)
+176
View File
@@ -0,0 +1,176 @@
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
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 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 =
t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt,
tkUInt8, tkUInt16, tkUInt32, tkUInt64, tkUInt,
tkFloat32, tkFloat64}
proc isInteger*(t: Type): bool =
t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt,
tkUInt8, tkUInt16, tkUInt32, tkUInt64, tkUInt}
proc isFloat*(t: Type): bool = t.kind in {tkFloat32, tkFloat64}
proc isSigned*(t: Type): bool =
t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt}
proc isPointer*(t: Type): bool = t.kind == tkPointer
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 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
# 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
# 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
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 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
+85
View File
@@ -0,0 +1,85 @@
import std/[unittest, strutils]
import ../src/[lexer, parser, sema, types]
proc checkSource(source: string): SemaResult =
let lexRes = tokenize(source, "<test>")
check not lexRes.hasErrors
let parseRes = parse(lexRes.tokens, "<test>")
check parseRes.diagnostics.len == 0
result = analyze(parseRes.module)
suite "Sema":
test "valid function with correct types":
let res = checkSource("func Main() -> int { return 0; }")
check not res.hasErrors
test "undeclared identifier":
let res = checkSource("func Main() -> int { return x; }")
check res.hasErrors
check "undeclared" in res.diagnostics[0].message
test "duplicate function":
let res = checkSource("func Main() -> int { return 0; } func Main() -> int { return 1; }")
check res.hasErrors
check "duplicate" in res.diagnostics[0].message
test "type mismatch in assignment":
let res = checkSource("func Main() -> int { let x: int32 = c8\"hello\"; return 0; }")
check res.hasErrors
check "cannot assign" in res.diagnostics[0].message
test "valid arithmetic":
let res = checkSource("func Main() -> int { return 1 + 2 * 3; }")
check not res.hasErrors
test "arithmetic on strings fails":
let res = checkSource("func Main() -> int { return c8\"a\" + c8\"b\"; }")
check res.hasErrors
test "valid function call":
let res = checkSource("func Add(a: int, b: int) -> int { return a + b; } func Main() -> int { return Add(1, 2); }")
check not res.hasErrors
test "wrong number of arguments":
let res = checkSource("func Add(a: int32, b: int32) -> int32 { return a + b; } func Main() -> int { return Add(1); }")
check res.hasErrors
check "expected 2 arguments" in res.diagnostics[0].message
test "wrong argument type":
let res = checkSource("func Add(a: int32, b: int32) -> int32 { return a + b; } func Main() -> int { return Add(c8\"a\", 2); }")
check res.hasErrors
check "argument 1" in res.diagnostics[0].message
test "if condition must be bool":
let res = checkSource("func Main() -> int { if 1 { return 0; } return 0; }")
check res.hasErrors
check "bool" in res.diagnostics[0].message
test "valid if with bool":
let res = checkSource("func Main() -> int { if true { return 0; } return 0; }")
check not res.hasErrors
test "pointer dereference":
let res = checkSource("func Main() -> int32 { let p: *int32 = null; return *p; }")
check not res.hasErrors
test "struct field access":
let res = checkSource("struct Point { x: float64; y: float64; } func Main() -> int32 { let p = Point { x: 1.0, y: 2.0 }; return 0; }")
check not res.hasErrors
test "unknown struct field":
let res = checkSource("struct Point { x: float64; y: float64; } func Main() -> int32 { let p = Point { x: 1.0, y: 2.0 }; return p.z; }")
check res.hasErrors
check "no field" in res.diagnostics[0].message
test "valid comparison":
let res = checkSource("func Main() -> int { return 1 == 2; }")
check not res.hasErrors
test "valid slice literal":
let res = checkSource("func Main() -> int { let arr = [1, 2, 3]; return 0; }")
check not res.hasErrors
test "slice element type mismatch":
let res = checkSource("func Main() -> int { let arr = [1, c8\"a\"]; return 0; }")
check res.hasErrors