From 8e637c89e7ddc6a4aae35a759378be006bb4696d Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 30 May 2026 21:36:42 +0300 Subject: [PATCH] feat: semantic analysis + type checker (Phase 2) --- src/cli.nim | 11 +- src/parser.nim | 26 ++- src/scope.nim | 47 +++++ src/sema.nim | 486 ++++++++++++++++++++++++++++++++++++++++++++ src/types.nim | 176 ++++++++++++++++ tests/sema_test.nim | 85 ++++++++ 6 files changed, 828 insertions(+), 3 deletions(-) create mode 100644 src/scope.nim create mode 100644 src/sema.nim create mode 100644 src/types.nim create mode 100644 tests/sema_test.nim diff --git a/src/cli.nim b/src/cli.nim index 17e7990..ba64eac 100644 --- a/src/cli.nim +++ b/src/cli.nim @@ -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 diff --git a/src/parser.nim b/src/parser.nim index 3bf2700..ced96f8 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -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: diff --git a/src/scope.nim b/src/scope.nim new file mode 100644 index 0000000..2dceb4a --- /dev/null +++ b/src/scope.nim @@ -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 diff --git a/src/sema.nim b/src/sema.nim new file mode 100644 index 0000000..34518c9 --- /dev/null +++ b/src/sema.nim @@ -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) diff --git a/src/types.nim b/src/types.nim new file mode 100644 index 0000000..ebcf9dc --- /dev/null +++ b/src/types.nim @@ -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 diff --git a/tests/sema_test.nim b/tests/sema_test.nim new file mode 100644 index 0000000..a9701d7 --- /dev/null +++ b/tests/sema_test.nim @@ -0,0 +1,85 @@ +import std/[unittest, strutils] +import ../src/[lexer, parser, sema, types] + +proc checkSource(source: string): SemaResult = + let lexRes = tokenize(source, "") + check not lexRes.hasErrors + let parseRes = parse(lexRes.tokens, "") + 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