feat: semantic analysis + type checker (Phase 2)
This commit is contained in:
+9
-2
@@ -1,5 +1,5 @@
|
|||||||
import std/[os, strutils, terminal, strformat]
|
import std/[os, strutils, terminal, strformat]
|
||||||
import lexer, parser, manifest
|
import lexer, parser, sema, manifest
|
||||||
|
|
||||||
type
|
type
|
||||||
ColorMode* = enum
|
ColorMode* = enum
|
||||||
@@ -170,8 +170,15 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
for d in parseRes.diagnostics:
|
for d in parseRes.diagnostics:
|
||||||
echo &"error: {d.message} at {d.loc}"
|
echo &"error: {d.message} at {d.loc}"
|
||||||
return 1
|
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:
|
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":
|
if splitFile(path).name == "Main":
|
||||||
foundMain = true
|
foundMain = true
|
||||||
|
|
||||||
|
|||||||
+25
-1
@@ -1,4 +1,4 @@
|
|||||||
import std/[strformat, sequtils]
|
import std/[strformat, sequtils, strutils]
|
||||||
import token, source_location, lexer, ast
|
import token, source_location, lexer, ast
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -367,7 +367,9 @@ proc parsePrimary(p: var Parser): Expr =
|
|||||||
return Expr(kind: ekSlice, loc: loc, exprSliceElements: elems)
|
return Expr(kind: ekSlice, loc: loc, exprSliceElements: elems)
|
||||||
of tkMatch:
|
of tkMatch:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
|
p.structInitAllowed = false
|
||||||
let subject = p.parseExpr()
|
let subject = p.parseExpr()
|
||||||
|
p.structInitAllowed = true
|
||||||
discard p.expect(tkLBrace, "expected '{' to start match")
|
discard p.expect(tkLBrace, "expected '{' to start match")
|
||||||
var arms: seq[MatchArm] = @[]
|
var arms: seq[MatchArm] = @[]
|
||||||
while not p.check(tkRBrace) and not p.isAtEnd:
|
while not p.check(tkRBrace) and not p.isAtEnd:
|
||||||
@@ -460,6 +462,22 @@ proc parsePostfix(p: var Parser): Expr =
|
|||||||
discard p.advance()
|
discard p.advance()
|
||||||
let ty = p.parseType()
|
let ty = p.parseType()
|
||||||
left = Expr(kind: ekIs, loc: loc, exprIsOperand: left, exprIsType: ty)
|
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:
|
else:
|
||||||
break
|
break
|
||||||
return left
|
return left
|
||||||
@@ -645,7 +663,9 @@ proc parseStmt(p: var Parser): Stmt =
|
|||||||
stmtLetPattern: pat, stmtLetType: ty, stmtLetInit: initExpr)
|
stmtLetPattern: pat, stmtLetType: ty, stmtLetInit: initExpr)
|
||||||
of tkIf:
|
of tkIf:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
|
p.structInitAllowed = false
|
||||||
let cond = p.parseExpr()
|
let cond = p.parseExpr()
|
||||||
|
p.structInitAllowed = true
|
||||||
let thenBlk = p.parseBlock()
|
let thenBlk = p.parseBlock()
|
||||||
var elseIfs: seq[ElseIf] = @[]
|
var elseIfs: seq[ElseIf] = @[]
|
||||||
var elseBlk: Block = nil
|
var elseBlk: Block = nil
|
||||||
@@ -664,7 +684,9 @@ proc parseStmt(p: var Parser): Stmt =
|
|||||||
stmtIfElseIfs: elseIfs, stmtIfElse: elseBlk)
|
stmtIfElseIfs: elseIfs, stmtIfElse: elseBlk)
|
||||||
of tkWhile:
|
of tkWhile:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
|
p.structInitAllowed = false
|
||||||
let cond = p.parseExpr()
|
let cond = p.parseExpr()
|
||||||
|
p.structInitAllowed = true
|
||||||
let body = p.parseBlock()
|
let body = p.parseBlock()
|
||||||
return Stmt(kind: skWhile, loc: loc, stmtWhileCond: cond, stmtWhileBody: body)
|
return Stmt(kind: skWhile, loc: loc, stmtWhileCond: cond, stmtWhileBody: body)
|
||||||
of tkDo:
|
of tkDo:
|
||||||
@@ -683,7 +705,9 @@ proc parseStmt(p: var Parser): Stmt =
|
|||||||
discard p.advance()
|
discard p.advance()
|
||||||
let varName = p.expect(tkIdent, "expected loop variable name").text
|
let varName = p.expect(tkIdent, "expected loop variable name").text
|
||||||
discard p.expect(tkIn, "expected 'in' in for loop")
|
discard p.expect(tkIn, "expected 'in' in for loop")
|
||||||
|
p.structInitAllowed = false
|
||||||
let iter = p.parseExpr()
|
let iter = p.parseExpr()
|
||||||
|
p.structInitAllowed = true
|
||||||
let body = p.parseBlock()
|
let body = p.parseBlock()
|
||||||
return Stmt(kind: skFor, loc: loc, stmtForVar: varName, stmtForIter: iter, stmtForBody: body)
|
return Stmt(kind: skFor, loc: loc, stmtForVar: varName, stmtForIter: iter, stmtForBody: body)
|
||||||
of tkMatch:
|
of tkMatch:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user