package typechecker import ( "path/filepath" "strings" "github.com/marzeq/quokka/parser" "github.com/marzeq/quokka/shared" "github.com/marzeq/quokka/tokeniser" ) type ( Node = parser.Node Type = shared.Type ) type VarSig struct { Type Type Mutable bool } type FunctionSig struct { Name string ArgTypes []shared.Pair[string, Type] RetType Type ImplicitReturn bool } type ( VarTable = *shared.SymbolTable[*VarSig] FuncTable = *shared.SymbolTable[*FunctionSig] ) type TypeChecker struct { VarTable VarTable FuncTable FuncTable } func NewTypeChecker() *TypeChecker { return &TypeChecker{ VarTable: shared.NewSymbolTable[*VarSig](), FuncTable: shared.NewSymbolTable[*FunctionSig](), } } func (tc *TypeChecker) TypeCheck(root *Node) (*Node, map[string]*FunctionSig, error) { loaded := make(map[string]*Node) recStack := make(map[string]bool) mergedRoot, err := tc.processImports(root, loaded, recStack) if err != nil { return nil, nil, err } root = mergedRoot for _, node := range root.Children { if node.Type != parser.NODE_TYPE_FUNCTION_DEF { continue } fsig, err := ExtractFunctionSig(node) if err != nil { return nil, nil, err } if ok := tc.FuncTable.Define(fsig.Name, fsig); !ok { return nil, nil, shared.NewError(node.Loc, "function '%s' is already defined", fsig.Name) } if fsig.Name == "main" && fsig.RetType != shared.BUILTIN_I32 { return nil, nil, shared.NewError(node.Loc, "'main' function must be of '%s' return type", shared.BUILTIN_I32) } } for _, node := range root.Children { if node.Type != parser.NODE_TYPE_FUNCTION_DEF { continue } name := IdentToStr(node.Value.(*parser.FunctionValue).Name) sig, _ := tc.FuncTable.Lookup(name) tc.enterScope() if err := tc.typeCheckFunction(node, sig); err != nil { return nil, nil, err } tc.exitScope() } return mergedRoot, tc.FuncTable.GetScope(), nil } func (tc *TypeChecker) processImports(root *Node, loaded map[string]*Node, recStack map[string]bool) (*Node, error) { filePath := root.Loc.FilePath if recStack[filePath] { return nil, shared.NewError(root.Loc, "import cycle detected for file '%s'", filePath) } if merged, ok := loaded[filePath]; ok { return merged, nil } recStack[filePath] = true merged := &Node{ Type: root.Type, Loc: root.Loc, Children: []*Node{}, } for _, node := range root.Children { if node.Type != parser.NODE_TYPE_IMPORT { continue } importPath := node.Right.Value.(string) resolvedPath := filepath.Join(filepath.Dir(filePath), importPath) t, err := tokeniser.NewTokeniserFromFile(resolvedPath) if err != nil { switch err.(type) { case shared.Error: return nil, err default: return nil, shared.NewError(node.Loc, "import failed: %v", err) } } toks, err := t.Tokenise() if err != nil { return nil, err } p := parser.NewParser(toks) ast, err := p.Parse() if err != nil { return nil, err } importedMerged, err := tc.processImports(ast, loaded, recStack) if err != nil { return nil, err } merged.Children = append(merged.Children, importedMerged.Children...) } for _, node := range root.Children { if node.Type != parser.NODE_TYPE_IMPORT { merged.Children = append(merged.Children, node) } } loaded[filePath] = merged delete(recStack, filePath) return merged, nil } func (tc *TypeChecker) enterScope() { tc.VarTable.EnterScope() tc.FuncTable.EnterScope() } func (tc *TypeChecker) exitScope() { tc.VarTable.ExitScope() tc.FuncTable.ExitScope() } func (tc *TypeChecker) typeCheckFunction(funcNode *Node, sig *FunctionSig) error { val := funcNode.Value.(*parser.FunctionValue) body := funcNode.Children[0] name := IdentToStr(funcNode.Value.(*parser.FunctionValue).Name) for _, arg := range sig.ArgTypes { tc.VarTable.Define(arg.L, &VarSig{ Type: arg.R, Mutable: false, }) } if body.Type == parser.NODE_TYPE_BLOCK { _, err := tc.typeCheckBlock(body, sig, false, false) return err } exprType, err := tc.typeCheckExpression(body, sig.RetType) if err != nil { return err } if !shared.CanCoerceTo(exprType, sig.RetType) { return shared.NewError(val.RetType.Loc, "function '%s' expects return type '%s' but returns '%s'", name, sig.RetType, exprType) } if exprType == shared.BUILTIN_UNTYPED_INT { body.ExprType = sig.RetType } return nil } func (tc *TypeChecker) typeCheckBlock(blockNode *Node, sig *FunctionSig, isLoop bool, isMainBody bool) (bool, error) { // (returns, error) foundReturn := false for i, node := range blockNode.Children { switch node.Type { case parser.NODE_TYPE_DECLARATION: varName, varSig, err := tc.typeCheckDeclaration(node) if err != nil { return false, err } if ok := tc.VarTable.Define(varName, varSig); !ok { return false, shared.NewError(node.Left.Loc, "variable '%s' is already declared in this scope", varName) } case parser.NODE_TYPE_FUNCTION_DEF: fsig, err := ExtractFunctionSig(node) if err != nil { return false, err } if ok := tc.FuncTable.Define(fsig.Name, fsig); !ok { return false, shared.NewError(node.Loc, "function '%s' is already defined", fsig.Name) } tc.enterScope() if err := tc.typeCheckFunction(node, fsig); err != nil { return false, err } tc.exitScope() case parser.NODE_TYPE_ASSIGNMENT: if err := tc.typeCheckAssignment(node); err != nil { return false, err } case parser.NODE_TYPE_FUNCTION_CALL: if _, err := tc.typeCheckFunctionCall(node); err != nil { return false, err } case parser.NODE_TYPE_CONTROL_KEYWORD: kw := node.Value.(string) switch kw { case "return": retType := shared.BUILTIN_VOID if node.Right != nil { var err error retType, err = tc.typeCheckExpression(node.Right, sig.RetType) if err != nil { return false, err } if !shared.CanCoerceTo(retType, sig.RetType) { return false, shared.NewError(node.Loc, "wrong return type for function, expected '%s' got '%s'", sig.RetType, retType) } if retType == shared.BUILTIN_UNTYPED_INT { node.Right.ExprType = sig.RetType } } else if sig.RetType != shared.BUILTIN_VOID { return false, shared.NewError(node.Loc, "wrong return type for function, expected '%s' got void", sig.RetType) } foundReturn = true if i != len(blockNode.Children)-1 { return false, shared.NewError(blockNode.Children[i+1].Loc, "dead code following return statement") } case "break", "continue": if !isLoop { return false, shared.NewError(node.Loc, "'%s' statement outside a loop", kw) } } case parser.NODE_TYPE_BLOCK: tc.enterScope() returns, err := tc.typeCheckBlock(node, sig, isLoop, false) if err != nil { return false, err } tc.exitScope() if i == len(blockNode.Children)-1 && !foundReturn { foundReturn = returns } case parser.NODE_TYPE_FOR: if err := tc.typeCheckForLoop(node, sig); err != nil { return false, err } case parser.NODE_TYPE_IF: tc.enterScope() returns, err := tc.typeCheckIfStatement(node, sig, isLoop) if err != nil { return false, err } tc.exitScope() if i == len(blockNode.Children)-1 && !foundReturn { foundReturn = returns } default: panic("unexpected node in block") } } if isMainBody { if !foundReturn && sig.RetType != shared.BUILTIN_VOID { return false, shared.NewError(blockNode.Loc, "function with return type '%s' is missing a return statement", sig.RetType) } sig.ImplicitReturn = !foundReturn } return foundReturn, nil } func (tc *TypeChecker) typeCheckExpression(exprNode *Node, expectedType Type) (Type, error) { exprNode.ExprType = expectedType switch exprNode.Type { case parser.NODE_TYPE_IDENTIFIER: varName := exprNode.Value.(string) varSig, ok := tc.VarTable.Lookup(varName) if !ok { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "undefined variable '%s'", varName) } exprNode.ExprType = varSig.Type return varSig.Type, nil case parser.NODE_TYPE_NUMBER_LITERAL: exprType := shared.BUILTIN_UNTYPED_INT if expectedType != shared.BUILTIN_VOID && shared.CanCoerceTo(exprType, expectedType) { exprNode.ExprType = expectedType return expectedType, nil } exprNode.ExprType = exprType return exprType, nil case parser.NODE_TYPE_BOOL_LITERAL: exprNode.ExprType = shared.BUILTIN_BOOL return shared.BUILTIN_BOOL, nil case parser.NODE_TYPE_FUNCTION_CALL: return tc.typeCheckFunctionCall(exprNode) case parser.NODE_TYPE_UNARY_OP: op := exprNode.Value.(string) operandType, err := tc.typeCheckExpression(exprNode.Right, shared.BUILTIN_VOID) if err != nil { return shared.BUILTIN_VOID, err } if expectedType != shared.BUILTIN_VOID && operandType == shared.BUILTIN_UNTYPED_INT && shared.IsNumericType(expectedType) { exprNode.Right.ExprType = expectedType operandType = expectedType } switch op { case "not": if operandType != shared.BUILTIN_BOOL { return shared.BUILTIN_VOID, shared.NewError(exprNode.Right.Loc, "unary operator 'not' expects a boolean operand, found '%s'", operandType) } return shared.BUILTIN_BOOL, nil case "-": if !shared.IsNumericType(operandType) { return shared.BUILTIN_VOID, shared.NewError(exprNode.Right.Loc, "unary operator '-' requires numeric operand, found '%s'", operandType) } return operandType, nil default: return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "unknown unary operator '%s'", op) } case parser.NODE_TYPE_BINARY_OP: op := exprNode.Value.(string) leftType, err := tc.typeCheckExpression(exprNode.Left, shared.BUILTIN_VOID) if err != nil { return shared.BUILTIN_VOID, err } rightType, err := tc.typeCheckExpression(exprNode.Right, shared.BUILTIN_VOID) if err != nil { return shared.BUILTIN_VOID, err } if expectedType != shared.BUILTIN_VOID { if leftType == shared.BUILTIN_UNTYPED_INT && shared.IsNumericType(expectedType) { exprNode.Left.ExprType = expectedType leftType = expectedType } if rightType == shared.BUILTIN_UNTYPED_INT && shared.IsNumericType(expectedType) { exprNode.Right.ExprType = expectedType rightType = expectedType } } switch op { case "and", "or": if leftType != shared.BUILTIN_BOOL || rightType != shared.BUILTIN_BOOL { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "operator '%s' expects boolean operands, found '%s' and '%s'", op, leftType, rightType) } exprNode.ExprType = shared.BUILTIN_BOOL return shared.BUILTIN_BOOL, nil case "==", "!=", "<", ">", "<=", ">=": compatible, commonType := shared.AreCompatibleNumericTypes(leftType, rightType) if !compatible { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "operator '%s' cannot be applied to operands of type '%s' and '%s'", op, leftType, rightType) } exprNode.Left.ExprType = commonType exprNode.Right.ExprType = commonType exprNode.ExprType = shared.BUILTIN_BOOL return shared.BUILTIN_BOOL, nil case "+", "-", "*", "/", "%": compatible, commonType := shared.AreCompatibleNumericTypes(leftType, rightType) if !compatible { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "operator '%s' requires numeric operands, found '%s' and '%s'", op, leftType, rightType) } exprNode.Left.ExprType = commonType exprNode.Right.ExprType = commonType exprNode.ExprType = commonType return commonType, nil default: return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "unknown binary operator '%s'", op) } case parser.NODE_TYPE_CAST: tpeName := IdentToStr(exprNode.Left) tpe, ok := shared.ResolveType(tpeName) if !ok { return shared.BUILTIN_VOID, shared.NewError(exprNode.Left.Loc, "no such type '%s'", tpeName) } exTpe, err := tc.typeCheckExpression(exprNode.Right, shared.BUILTIN_VOID) if err != nil { return shared.BUILTIN_VOID, err } if exTpe == shared.BUILTIN_UNTYPED_INT { exprNode.Right.ExprType = tpe } compatible, _ := shared.AreCompatibleTypes(tpe, exTpe) if compatible { exprNode.ExprType = tpe return tpe, nil } else { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "cannot cast type '%s' to '%s'", exTpe, tpe) } case parser.NODE_TYPE_IF_EXPR: ifExpr := exprNode.Value.(*parser.IfNodeValue) if ifExpr.IfBranch == nil { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "if expression missing if branch") } condType, err := tc.typeCheckExpression(ifExpr.IfBranch.Condition, shared.BUILTIN_BOOL) if err != nil { return shared.BUILTIN_VOID, err } if condType != shared.BUILTIN_BOOL { return shared.BUILTIN_VOID, shared.NewError(ifExpr.IfBranch.Condition.Loc, "if condition must be boolean, found '%s'", condType) } ifType, err := tc.typeCheckExpression(ifExpr.IfBranch.Node, expectedType) if err != nil { return shared.BUILTIN_VOID, err } commonType := ifType for _, elseIf := range ifExpr.ElseIfBranches { elseIfCondType, err := tc.typeCheckExpression(elseIf.Condition, shared.BUILTIN_BOOL) if err != nil { return shared.BUILTIN_VOID, err } if elseIfCondType != shared.BUILTIN_BOOL { return shared.BUILTIN_VOID, shared.NewError(elseIf.Condition.Loc, "else-if condition must be boolean, found '%s'", elseIfCondType) } elseIfType, err := tc.typeCheckExpression(elseIf.Node, expectedType) if err != nil { return shared.BUILTIN_VOID, err } if elseIfType != commonType { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "all branches must return same type, expected '%s' but found '%s'", commonType, elseIfType) } } if ifExpr.ElseBranch == nil { return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "if expression requires else branch") } elseType, err := tc.typeCheckExpression(ifExpr.ElseBranch.Node, expectedType) if err != nil { return shared.BUILTIN_VOID, err } if elseType != commonType { return shared.BUILTIN_VOID, shared.NewError(ifExpr.ElseBranch.Node.Loc, "else branch must match type '%s', found '%s'", commonType, elseType) } exprNode.ExprType = commonType return commonType, nil default: return shared.BUILTIN_VOID, shared.NewError(exprNode.Loc, "unsupported expression type: '%s'", exprNode.Type) } } func (tc *TypeChecker) typeCheckFunctionCall(funccallNode *Node) (Type, error) { nameNode := funccallNode.Value.(*Node) fname := IdentToStr(nameNode) // TODO: REMOVE THIS if fname == "_print" { if len(funccallNode.Children) != 1 { return shared.BUILTIN_VOID, shared.NewError(funccallNode.Loc, "temporary '_print' function expects exactly one argument") } _, err := tc.typeCheckExpression(funccallNode.Children[0], shared.BUILTIN_VOID) if err != nil { return shared.BUILTIN_VOID, err } return shared.BUILTIN_VOID, nil } fsig, ok := tc.FuncTable.Lookup(fname) if !ok { return shared.BUILTIN_VOID, shared.NewError(nameNode.Loc, "undefined function '%s'", fname) } for i, arg := range funccallNode.Children { fsigArgType := fsig.ArgTypes[i].R argType, err := tc.typeCheckExpression(arg, fsigArgType) if err != nil { return shared.BUILTIN_VOID, err } if fsigArgType != argType { return shared.BUILTIN_VOID, shared.NewError(arg.Loc, "argument %d of function '%s' has type '%s' but expected '%s'", i+1, fname, argType, fsig.ArgTypes[i].R, ) } } funccallNode.ExprType = fsig.RetType return fsig.RetType, nil } func (tc *TypeChecker) typeCheckDeclaration(declNode *Node) (string, *VarSig, error) { varName := IdentToStr(declNode.Left) if strings.HasPrefix(varName, "___") { return "", nil, shared.NewError(declNode.Loc, "variables starting with '___' are reserved for the compiler") } info := declNode.Value.(*parser.DeclarationValue) mutable := info.Mutable varType := shared.BUILTIN_VOID if info.Type != nil { varTypeStr := IdentToStr(info.Type) vt, ok := shared.ResolveType(varTypeStr) if !ok { return "", nil, shared.NewError(info.Type.Loc, "variable '%s' has undefined type '%s'", varName, varTypeStr) } varType = vt } exprType, err := tc.typeCheckExpression(declNode.Right, varType) if err != nil { return "", nil, err } if varType == shared.BUILTIN_VOID { varType = exprType } else if !shared.CanCoerceTo(exprType, varType) { return "", nil, shared.NewError(declNode.Loc, "cannot assign value of type '%s' to variable '%s' of type '%s'", exprType, varName, varType, ) } if varType == shared.BUILTIN_UNTYPED_INT { return "", nil, shared.NewError(declNode.Loc, "ambiguous number type, specify type explicitly") } if varType == shared.BUILTIN_VOID { return "", nil, shared.NewError(declNode.Loc, "a variable cannot be of type void") } declNode.ExprType = exprType return varName, &VarSig{ Type: varType, Mutable: mutable, }, nil } func (tc *TypeChecker) typeCheckAssignment(asNode *Node) error { varName := IdentToStr(asNode.Left) varSig, ok := tc.VarTable.Lookup(varName) if !ok { return shared.NewError(asNode.Left.Loc, "undefined variable '%s'", varName) } if !varSig.Mutable { return shared.NewError(asNode.Loc, "cannot assign to immutable variable '%s'", varName) } exprType, err := tc.typeCheckExpression(asNode.Right, varSig.Type) if err != nil { return err } if exprType != varSig.Type { return shared.NewError(asNode.Loc, "cannot assign value of type '%s' to variable '%s' of type '%s'", exprType, varName, varSig.Type, ) } asNode.ExprType = varSig.Type return nil } func (tc *TypeChecker) typeCheckForLoop(loopNode *Node, sig *FunctionSig) error { value := loopNode.Value.(*parser.ForLoopValue) tc.enterScope() if len(value.ExprsOrStmts) == 1 { expr := value.ExprsOrStmts[0] if !expr.Type.IsExpression() { return shared.NewError(expr.Loc, "for loop condition must be a boolean expression") } exprType, err := tc.typeCheckExpression(expr, shared.BUILTIN_BOOL) if err != nil { return err } if exprType != shared.BUILTIN_BOOL { return shared.NewError(expr.Loc, "for loop condition must be a boolean expression, found '%s'", exprType) } } else if len(value.ExprsOrStmts) == 3 { init := value.ExprsOrStmts[0] if init.Type == parser.NODE_TYPE_DECLARATION { varName, varSig, err := tc.typeCheckDeclaration(init) if err != nil { return err } tc.VarTable.Define(varName, varSig) } else if init.Type == parser.NODE_TYPE_ASSIGNMENT { if err := tc.typeCheckAssignment(init); err != nil { return err } } else { return shared.NewError(init.Loc, "for loop initialiser must be a declaration or assignment statement") } cond := value.ExprsOrStmts[1] if !cond.Type.IsExpression() { return shared.NewError(cond.Loc, "for loop condition must be a boolean expression") } exprType, err := tc.typeCheckExpression(cond, shared.BUILTIN_BOOL) if err != nil { return err } if exprType != shared.BUILTIN_BOOL { return shared.NewError(cond.Loc, "for loop condition must be a boolean expression, found '%s'", exprType) } reass := value.ExprsOrStmts[2] if reass.Type != parser.NODE_TYPE_ASSIGNMENT { return shared.NewError(reass.Loc, "for loop 'after' step must be an assignment statement") } if err := tc.typeCheckAssignment(reass); err != nil { return err } } else if len(value.ExprsOrStmts) != 0 { return shared.NewError(loopNode.Loc, "for loop must have either:\n - no conditions\n - a condition\n - an initialiser, a condition and an 'after' assignment") } if _, err := tc.typeCheckBlock(value.Body, sig, true, false); err != nil { return err } tc.exitScope() return nil } func (tc *TypeChecker) typeCheckIfStatement(ifNode *Node, sig *FunctionSig, isLoop bool) (bool, error) { value := ifNode.Value.(*parser.IfNodeValue) ifCondType, err := tc.typeCheckExpression(value.IfBranch.Condition, shared.BUILTIN_BOOL) if err != nil { return false, err } if ifCondType != shared.BUILTIN_BOOL { return false, shared.NewError(value.ElseBranch.Condition.Loc, "if condition must be boolean, found '%s'", ifCondType) } ifReturns, err := tc.typeCheckBlock(value.IfBranch.Node, sig, isLoop, false) if err != nil { return false, err } alwaysReturns := ifReturns for _, elseIfBranch := range value.ElseIfBranches { elseIfCondType, err := tc.typeCheckExpression(elseIfBranch.Condition, shared.BUILTIN_BOOL) if err != nil { return false, err } if elseIfCondType != shared.BUILTIN_BOOL { return false, shared.NewError(elseIfBranch.Condition.Loc, "else-if condition must be boolean, found '%s'", elseIfCondType) } elseIfReturns, err := tc.typeCheckBlock(elseIfBranch.Node, sig, isLoop, false) if err != nil { return false, err } if !alwaysReturns { alwaysReturns = elseIfReturns } } if value.ElseBranch != nil { elseReturns, err := tc.typeCheckBlock(value.ElseBranch.Node, sig, isLoop, false) if err != nil { return false, err } if !alwaysReturns { alwaysReturns = elseReturns } } return alwaysReturns, nil } func ExtractFunctionSig(functionNode *Node) (*FunctionSig, error) { functionVal := functionNode.Value.(*parser.FunctionValue) name := IdentToStr(functionVal.Name) retTypeStr := IdentToStr(functionVal.RetType) retType, ok := shared.ResolveType(retTypeStr) if !ok { return nil, shared.NewError(functionVal.RetType.Loc, "function '%s' has undefined return type '%s'", name, retTypeStr) } argTypes := make([]shared.Pair[string, Type], len(functionVal.Args)) for i, arg := range functionVal.Args { tpe := IdentToStr(arg.R) resolved, ok := shared.ResolveType(tpe) if !ok { return nil, shared.NewError(arg.R.Loc, "parameter '%s' has undefined type '%s'", IdentToStr(arg.L), tpe) } argTypes[i] = shared.Pair[string, Type]{L: IdentToStr(arg.L), R: resolved} } return &FunctionSig{ ArgTypes: argTypes, RetType: retType, Name: name, }, nil } func IdentToStr(identNode *Node) string { return identNode.Value.(string) }