// Insert inserts the given declaration into the current scope. func (s *Scope) Insert(decl ast.Decl) error { // Early return for first-time declarations. ident := decl.Name() if ident == nil { // Anonymous function parameter declaration. return nil } name := ident.String() prev, ok := s.Decls[name] if !ok { s.Decls[name] = decl return nil } prevIdent := prev.Name() if prevIdent.NamePos == ident.NamePos { // Identifier already added to scope. return nil } // Previously declared. if !types.Equal(prev.Type(), decl.Type()) { return errors.Newf(ident.Start(), "redefinition of %q with type %q instead of %q", name, decl.Type(), prev.Type()) } // The last tentative definition becomes the definition, unless defined // explicitly (e.g. having an initializer or function body). if !s.IsDef(prev) { s.Decls[name] = decl return nil } // Definition already present in scope. if s.IsDef(decl) { // TODO: Consider adding support for multiple errors (and potentially // warnings and notifications). // // If support for notifications are added, add a note of the previous declaration. // errors.Notef(prevIdent.Start(), "previous definition of %q", name) return errors.Newf(ident.Start(), "redefinition of %q", name) } // Declaration of previously declared identifier. return nil }
// checkNestedFunctions reports an error if the given function contains any // nested function definitions. func checkNestedFunctions(fn *ast.FuncDecl) error { if !astutil.IsDef(fn) { return nil } check := func(n ast.Node) error { if n, ok := n.(*ast.FuncDecl); ok { return errors.Newf(n.FuncName.Start(), "nested functions not allowed") } return nil } nop := func(ast.Node) error { return nil } if err := astutil.WalkBeforeAfter(fn.Body, check, nop); err != nil { return errutil.Err(err) } return nil }
// check type-checks the given file. func check(file *ast.File, exprTypes map[ast.Expr]types.Type) error { // funcs is a stack of function declarations, where the top-most entry // represents the currently active function. var funcs []*types.Func // check type-checks the given node. check := func(n ast.Node) error { switch n := n.(type) { case *ast.BlockStmt: // Verify that array declarations have an explicit size or an // initializer. for _, item := range n.Items { switch item := item.(type) { case *ast.VarDecl: typ := item.Type() if typ, ok := typ.(*types.Array); ok { if typ.Len == 0 && item.Val == nil { return errors.Newf(item.VarName.NamePos, "array size or initializer missing for %q", item.VarName) } } } } case *ast.VarDecl: typ := n.Type() // TODO: Evaluate if the type-checking of identifiers could be made // more generic. Consider if a new variable declaration type was added // alongside of ast.VarDecl, e.g. ast.Field. Then the type-checking // code would have to be duplicated to ast.Field. A more generic // approach that may be worth exploring is to do the type-checking // directly on *ast.Ident. A naive implementation has been attempted // using *ast.Ident, which failed since "void" refers to itself as a // VarDecl, whos types is "void". if n.VarName != nil && types.IsVoid(typ) { return errors.Newf(n.VarName.NamePos, `%q has invalid type "void"`, n.VarName) } if typ, ok := typ.(*types.Array); ok { if types.IsVoid(typ.Elem) { return errors.Newf(n.VarName.NamePos, `invalid element type "void" of array %q`, n.VarName) } } case *ast.FuncDecl: if astutil.IsDef(n) { // push function declaration. funcs = append(funcs, n.Type().(*types.Func)) // Verify that parameter names are not obmitted in function // definitions. for _, param := range n.FuncType.Params { if !types.IsVoid(param.Type()) && param.VarName == nil { return errors.Newf(param.VarType.Start(), "parameter name obmitted") } } // Verify that non-void functions end with return statement. if !types.IsVoid(n.Type().(*types.Func).Result) { // endsWithReturn reports whether the given block item ends with // a return statement. The following two terminating statements // are supported, recursively. // // return; // // if (cond) { // return; // } else { // return; // } var endsWithReturn func(ast.Node) bool endsWithReturn = func(node ast.Node) bool { last := node if block, ok := node.(*ast.BlockStmt); ok { items := block.Items if len(items) == 0 { // node ends without a return statement. return false } last = items[len(items)-1] } switch last := last.(type) { case *ast.ReturnStmt: // node ends with a return statement. return true case *ast.IfStmt: if last.Else == nil { // node may ends without a return statement if the else- // branch is invoked. return false } return endsWithReturn(last.Body) && endsWithReturn(last.Else) default: // node may end without return statement. return false } } missing := !endsWithReturn(n.Body) // Missing return statements are valid from the main function. // // NOTE: "reaching the } that terminates the main function // returns a value of 0." (see §5.1.2.2.3 in the C11 spec) if missing && n.FuncName.String() != "main" { return errors.Newf(n.Body.Rbrace, "missing return at end of non-void function %q", n.FuncName) } } } case *ast.ReturnStmt: // Verify result type. curFunc := funcs[len(funcs)-1] var resultType types.Type resultType = &types.Basic{Kind: types.Void} if n.Result != nil { resultType = exprTypes[n.Result] } if !isCompatible(resultType, curFunc.Result) { resultPos := n.Start() if n.Result != nil { resultPos = n.Result.Start() } return errors.Newf(resultPos, "returning %q from a function with incompatible result type %q", resultType, curFunc.Result) } case *ast.CallExpr: funcType, ok := n.Name.Decl.Type().(*types.Func) if !ok { return errors.Newf(n.Lparen, "cannot call non-function %q of type %q", n.Name, funcType) } // TODO: Implement support for functions with variable arguments (i.e. // ellipsis). // Verify call without arguments. if len(n.Args) == 0 && len(funcType.Params) == 1 && types.IsVoid(funcType.Params[0].Type) { return nil } // Check number of arguments. if len(n.Args) < len(funcType.Params) { return errors.Newf(n.Lparen, "calling %q with too few arguments; expected %d, got %d", n.Name, len(funcType.Params), len(n.Args)) } if len(n.Args) > len(funcType.Params) { return errors.Newf(n.Lparen, "calling %q with too many arguments; expected %d, got %d", n.Name, len(funcType.Params), len(n.Args)) } // Check that call argument types match the function parameter types. for i, param := range funcType.Params { arg := n.Args[i] argType := exprTypes[arg] paramType := param.Type if !isCompatibleArg(argType, paramType) { return errors.Newf(arg.Start(), "calling %q with incompatible argument type %q to parameter of type %q", n.Name, argType, paramType) } } case *ast.FuncType: for _, param := range n.Params { paramType := param.Type() if len(n.Params) > 1 && types.IsVoid(paramType) { return errors.Newf(n.Lparen, `"void" must be the only parameter`) } } case *ast.IndexExpr: indexType, ok := exprTypes[n.Index] if !ok { panic(fmt.Sprintf("unable to locate type of expression %v", n.Index)) } if !types.IsInteger(indexType) { return errors.Newf(n.Index.Start(), "invalid array index; expected integer, got %q", indexType) } default: // TODO: Implement type-checking for remaining node types. //log.Printf("not type-checked: %T\n", n) } return nil } // after reverts to the outer function after traversing function definitions. after := func(n ast.Node) error { switch n := n.(type) { case *ast.FuncDecl: if astutil.IsDef(n) { // pop function declaration. funcs = funcs[:len(funcs)-1] } } return nil } // Walk the AST of the given file to perform type-checking. if err := astutil.WalkBeforeAfter(file, check, after); err != nil { return errutil.Err(err) } return nil }
// resolve performs identifier resolution, mapping identifiers to corresponding // declarations. func resolve(file *ast.File, scopes map[ast.Node]*Scope) error { // TODO: Verify that type keywords cannot be redeclared. // Pre-pass, add keyword types and universe scope. universe := NewScope(nil) charIdent := &ast.Ident{NamePos: universePos, Name: "char"} charDecl := &ast.TypeDef{DeclType: charIdent, TypeName: charIdent, Val: &types.Basic{Kind: types.Char}} charIdent.Decl = charDecl intIdent := &ast.Ident{NamePos: universePos, Name: "int"} intDecl := &ast.TypeDef{DeclType: intIdent, TypeName: intIdent, Val: &types.Basic{Kind: types.Int}} intIdent.Decl = intDecl voidIdent := &ast.Ident{NamePos: universePos, Name: "void"} voidDecl := &ast.TypeDef{DeclType: voidIdent, TypeName: voidIdent, Val: &types.Basic{Kind: types.Void}} voidIdent.Decl = voidDecl universeDecls := []*ast.TypeDef{ charDecl, intDecl, voidDecl, } for _, decl := range universeDecls { if err := universe.Insert(decl); err != nil { return errutil.Err(err) } } // First pass, add global declarations to file scope. fileScope := NewScope(universe) scopes[file] = fileScope fileScope.IsDef = func(decl ast.Decl) bool { // Consider variable declarations as tentative definitions; i.e. return // false, unless variable definition. return decl.Value() != nil } for _, decl := range file.Decls { if err := fileScope.Insert(decl); err != nil { return errutil.Err(err) } } // skip specifies that the block statement body of a function declaration // should skip creating a nested scope, as it has already been created by its // function declaration, so that function parameters are placed within the // correct scope. skip := false // scope specifies the current lexical scope. scope := fileScope // resolve performs identifier resolution, mapping identifiers to the // corresponding declarations of the closest lexical scope. resolve := func(n ast.Node) error { switch n := n.(type) { case ast.Decl: // Insert declaration into the scope if not already added by the // file scope pre-pass. if scope != fileScope { if err := scope.Insert(n); err != nil { return errutil.Err(err) } } // Create nested scope for function definitions. if fn, ok := n.(*ast.FuncDecl); ok { if astutil.IsDef(fn) { skip = true } scope = NewScope(scope) scopes[fn] = scope } case *ast.BlockStmt: if !skip { scope = NewScope(scope) scopes[n] = scope } skip = false case *ast.Ident: decl, ok := scope.Lookup(n.Name) if !ok { return errors.Newf(n.Start(), "undeclared identifier %q", n) } n.Decl = decl } return nil } // after reverts to the outer scope after traversing block statements. after := func(n ast.Node) error { if _, ok := n.(*ast.BlockStmt); ok { scope = scope.Outer } else if fn, ok := n.(*ast.FuncDecl); ok && !astutil.IsDef(fn) { scope = scope.Outer } return nil } // Walk the AST of the given file to resolve identifiers. if err := astutil.WalkBeforeAfter(file, resolve, after); err != nil { return errutil.Err(err) } return nil }
// typeOf returns the type of the given expression. func typeOf(n ast.Expr) (types.Type, error) { switch n := n.(type) { case *ast.BasicLit: // "The type of an integer constant is the first of the corresponding // list in which its value can be represented." [C99 draft 6.4.4.1.5] switch n.Kind { case token.CharLit: // "An integer character constant has type int." [C99 draft 6.4.4.4.10] return &types.Basic{Kind: types.Int}, nil case token.IntLit: return &types.Basic{Kind: types.Int}, nil default: panic(fmt.Sprintf("support for basic literal type %v not yet implemented", n.Kind)) } case *ast.BinaryExpr: // See [C99 draft 6.3.1.8 Usual arithmetic conversions] xType, err := typeOf(n.X) if err != nil { return nil, errutil.Err(err) } yType, err := typeOf(n.Y) if err != nil { return nil, errutil.Err(err) } if n.Op == token.Assign { if !isAssignable(n.X) { return nil, errors.Newf(n.OpPos, "cannot assign to %q of type %q", n.X, xType) } if !isCompatible(xType, yType) { return nil, errors.Newf(n.OpPos, "cannot assign to %q (type mismatch between %q and %q)", n.X, xType, yType) } // TODO: !types.Equal(higherPrecision(xType, yType), xType) could be // used for loss of percision warning. return xType, nil } if types.IsVoid(xType) || types.IsVoid(yType) { return nil, errors.Newf(n.OpPos, "invalid operands to binary expression: %v (%q and %q)", n, xType, yType) } if !isCompatible(xType, yType) { return nil, errors.Newf(n.OpPos, "invalid operation: %v (type mismatch between %q and %q)", n, xType, yType) } // TODO: Implement better implicit conversion. Future: Make sure to // promote types early when implementing signed/unsigned types and // types need to be promoted anyway later. Be careful of bug: // https://youtu.be/Ux0YnVEaI6A?t=279 // Implement according to [C99 draft 6.3.1.8 Usual arithmetic conversions] return higherPrecision(xType, yType), nil case *ast.CallExpr: typ := n.Name.Decl.Type() if typ, ok := typ.(*types.Func); ok { return typ.Result, nil } return nil, errors.Newf(n.Lparen, "cannot call non-function %q of type %q", n.Name, typ) case *ast.Ident: return n.Decl.Type(), nil case *ast.IndexExpr: typ := n.Name.Decl.Type() if typ, ok := typ.(*types.Array); ok { return typ.Elem, nil } return nil, errors.Newf(n.Lbracket, "invalid operation: %v (type %q does not support indexing)", n, typ) case *ast.ParenExpr: return typeOf(n.X) case *ast.UnaryExpr: // TODO: Add support for pointers. return typeOf(n.X) default: panic(fmt.Sprintf("support for type %T not yet implemented.", n)) } }