// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bytes" "flag" "fmt" "go/ast" "go/format" "go/parser" "go/token" "log" "os" "strings" "golang.org/x/tools/go/ast/astutil" internalastutil "runtime/_mkmalloc/astutil" ) var stdout = flag.Bool("stdout", false, "write sizeclasses source to stdout instead of sizeclasses.go") func makeSizeToSizeClass(classes []class) []uint8 { sc := uint8(0) ret := make([]uint8, smallScanNoHeaderMax+1) for i := range ret { if i > classes[sc].size { sc++ } ret[i] = sc } return ret } func main() { log.SetFlags(0) log.SetPrefix("mkmalloc: ") classes := makeClasses() sizeToSizeClass := makeSizeToSizeClass(classes) if *stdout { if _, err := os.Stdout.Write(mustFormat(generateSizeClasses(classes))); err != nil { log.Fatal(err) } return } sizeclasesesfile := "../../internal/runtime/gc/sizeclasses.go" if err := os.WriteFile(sizeclasesesfile, mustFormat(generateSizeClasses(classes)), 0666); err != nil { log.Fatal(err) } outfile := "../malloc_generated.go" if err := os.WriteFile(outfile, mustFormat(inline(specializedMallocConfig(classes, sizeToSizeClass))), 0666); err != nil { log.Fatal(err) } tablefile := "../malloc_tables_generated.go" if err := os.WriteFile(tablefile, mustFormat(generateTable(sizeToSizeClass)), 0666); err != nil { log.Fatal(err) } } // withLineNumbers returns b with line numbers added to help debugging. func withLineNumbers(b []byte) []byte { var buf bytes.Buffer i := 1 for line := range bytes.Lines(b) { fmt.Fprintf(&buf, "%d: %s", i, line) i++ } return buf.Bytes() } // mustFormat formats the input source, or exits if there's an error. func mustFormat(b []byte) []byte { formatted, err := format.Source(b) if err != nil { log.Fatalf("error formatting source: %v\nsource:\n%s\n", err, withLineNumbers(b)) } return formatted } // generatorConfig is the configuration for the generator. It uses the given file to find // its templates, and generates each of the functions specified by specs. type generatorConfig struct { file string specs []spec } // spec is the specification for a function for the inliner to produce. The function gets // the given name, and is produced by starting with the function with the name given by // templateFunc and applying each of the ops. type spec struct { name string templateFunc string ops []op } // replacementKind specifies the operation to ben done by a op. type replacementKind int const ( inlineFunc = replacementKind(iota) subBasicLit ) // op is a single inlining operation for the inliner. Any calls to the function // from are replaced with the inlined body of to. For non-functions, uses of from are // replaced with the basic literal expression given by to. type op struct { kind replacementKind from string to string } func smallScanNoHeaderSCFuncName(sc, scMax uint8) string { if sc == 0 || sc > scMax { return "mallocPanic" } return fmt.Sprintf("mallocgcSmallScanNoHeaderSC%d", sc) } func tinyFuncName(size uintptr) string { if size == 0 || size > smallScanNoHeaderMax { return "mallocPanic" } return fmt.Sprintf("mallocTiny%d", size) } func smallNoScanSCFuncName(sc, scMax uint8) string { if sc < 2 || sc > scMax { return "mallocPanic" } return fmt.Sprintf("mallocgcSmallNoScanSC%d", sc) } // specializedMallocConfig produces an inlining config to stamp out the definitions of the size-specialized // malloc functions to be written by mkmalloc. func specializedMallocConfig(classes []class, sizeToSizeClass []uint8) generatorConfig { config := generatorConfig{file: "../malloc_stubs.go"} // Only generate specialized functions for sizes that don't have // a header on 64-bit platforms. (They may have a header on 32-bit, but // we will fall back to the non-specialized versions in that case) scMax := sizeToSizeClass[smallScanNoHeaderMax] str := fmt.Sprint // allocations with pointer bits { const noscan = 0 for sc := uint8(0); sc <= scMax; sc++ { if sc == 0 { continue } name := smallScanNoHeaderSCFuncName(sc, scMax) elemsize := classes[sc].size config.specs = append(config.specs, spec{ templateFunc: "mallocStub", name: name, ops: []op{ {inlineFunc, "inlinedMalloc", "smallScanNoHeaderStub"}, {inlineFunc, "heapSetTypeNoHeaderStub", "heapSetTypeNoHeaderStub"}, {inlineFunc, "nextFreeFastStub", "nextFreeFastStub"}, {inlineFunc, "writeHeapBitsSmallStub", "writeHeapBitsSmallStub"}, {subBasicLit, "elemsize_", str(elemsize)}, {subBasicLit, "sizeclass_", str(sc)}, {subBasicLit, "noscanint_", str(noscan)}, }, }) } } // allocations without pointer bits { const noscan = 1 // tiny tinySizeClass := sizeToSizeClass[tinySize] for s := range uintptr(16) { if s == 0 { continue } name := tinyFuncName(s) elemsize := classes[tinySizeClass].size config.specs = append(config.specs, spec{ templateFunc: "mallocStub", name: name, ops: []op{ {inlineFunc, "inlinedMalloc", "tinyStub"}, {inlineFunc, "nextFreeFastTiny", "nextFreeFastTiny"}, {subBasicLit, "elemsize_", str(elemsize)}, {subBasicLit, "sizeclass_", str(tinySizeClass)}, {subBasicLit, "size_", str(s)}, {subBasicLit, "noscanint_", str(noscan)}, }, }) } // non-tiny for sc := uint8(tinySizeClass); sc <= scMax; sc++ { name := smallNoScanSCFuncName(sc, scMax) elemsize := classes[sc].size config.specs = append(config.specs, spec{ templateFunc: "mallocStub", name: name, ops: []op{ {inlineFunc, "inlinedMalloc", "smallNoScanStub"}, {inlineFunc, "nextFreeFastStub", "nextFreeFastStub"}, {subBasicLit, "elemsize_", str(elemsize)}, {subBasicLit, "sizeclass_", str(sc)}, {subBasicLit, "noscanint_", str(noscan)}, }, }) } } return config } // inline applies the inlining operations given by the config. func inline(config generatorConfig) []byte { var out bytes.Buffer // Read the template file in. fset := token.NewFileSet() f, err := parser.ParseFile(fset, config.file, nil, 0) if err != nil { log.Fatalf("parsing %s: %v", config.file, err) } // Collect the function and import declarations. The function // declarations in the template file provide both the templates // that will be stamped out, and the functions that will be inlined // into them. The imports from the template file will be copied // straight to the output. funcDecls := map[string]*ast.FuncDecl{} importDecls := []*ast.GenDecl{} for _, decl := range f.Decls { switch decl := decl.(type) { case *ast.FuncDecl: funcDecls[decl.Name.Name] = decl case *ast.GenDecl: if decl.Tok.String() == "import" { importDecls = append(importDecls, decl) continue } } } // Write out the package and import declarations. out.WriteString("// Code generated by mkmalloc.go; DO NOT EDIT.\n\n") out.WriteString("package " + f.Name.Name + "\n\n") for _, importDecl := range importDecls { out.Write(mustFormatNode(fset, importDecl)) out.WriteString("\n\n") } // Produce each of the inlined functions specified by specs. for _, spec := range config.specs { // Start with a renamed copy of the template function. containingFuncCopy := internalastutil.CloneNode(funcDecls[spec.templateFunc]) if containingFuncCopy == nil { log.Fatal("did not find", spec.templateFunc) } containingFuncCopy.Name.Name = spec.name // Apply each of the ops given by the specs stamped := ast.Node(containingFuncCopy) for _, repl := range spec.ops { if toDecl, ok := funcDecls[repl.to]; ok { stamped = inlineFunction(stamped, repl.from, toDecl) } else { stamped = substituteWithBasicLit(stamped, repl.from, repl.to) } } out.Write(mustFormatNode(fset, stamped)) out.WriteString("\n\n") } return out.Bytes() } // substituteWithBasicLit recursively renames identifiers in the provided AST // according to 'from' and 'to'. func substituteWithBasicLit(node ast.Node, from, to string) ast.Node { // The op is a substitution of an identifier with an basic literal. toExpr, err := parser.ParseExpr(to) if err != nil { log.Fatalf("parsing expr %q: %v", to, err) } if _, ok := toExpr.(*ast.BasicLit); !ok { log.Fatalf("op 'to' expr %q is not a basic literal", to) } return astutil.Apply(node, func(cursor *astutil.Cursor) bool { if isIdentWithName(cursor.Node(), from) { cursor.Replace(toExpr) } return true }, nil) } // inlineFunction recursively replaces calls to the function 'from' with the body of the function // 'toDecl'. All calls to 'from' must appear in assignment statements. // The replacement is very simple: it doesn't substitute the arguments for the parameters, so the // arguments to the function call must be the same identifier as the parameters to the function // declared by 'toDecl'. If there are any calls to from where that's not the case there will be a fatal error. func inlineFunction(node ast.Node, from string, toDecl *ast.FuncDecl) ast.Node { return astutil.Apply(node, func(cursor *astutil.Cursor) bool { switch node := cursor.Node().(type) { case *ast.AssignStmt: // TODO(matloob) CHECK function args have same name // as parameters (or parameter is "_"). if len(node.Rhs) == 1 && isCallTo(node.Rhs[0], from) { args := node.Rhs[0].(*ast.CallExpr).Args if !argsMatchParameters(args, toDecl.Type.Params) { log.Fatalf("applying op: arguments to %v don't match parameter names of %v: %v", from, toDecl.Name, debugPrint(args...)) } replaceAssignment(cursor, node, toDecl) } return false case *ast.CallExpr: // double check that all calls to from appear within an assignment if isCallTo(node, from) { if _, ok := cursor.Parent().(*ast.AssignStmt); !ok { log.Fatalf("applying op: all calls to function %q being replaced must appear in an assignment statement, appears in %T", from, cursor.Parent()) } } } return true }, nil) } // argsMatchParameters reports whether the arguments given by args are all identifiers // whose names are the same as the corresponding parameters in params. func argsMatchParameters(args []ast.Expr, params *ast.FieldList) bool { var paramIdents []*ast.Ident for _, f := range params.List { paramIdents = append(paramIdents, f.Names...) } if len(args) != len(paramIdents) { return false } for i := range args { if !isIdentWithName(args[i], paramIdents[i].Name) { return false } } return true } // isIdentWithName reports whether the expression is an identifier with the given name. func isIdentWithName(expr ast.Node, name string) bool { ident, ok := expr.(*ast.Ident) if !ok { return false } return ident.Name == name } // isCallTo reports whether the expression is a call expression to the function with the given name. func isCallTo(expr ast.Expr, name string) bool { callexpr, ok := expr.(*ast.CallExpr) if !ok { return false } return isIdentWithName(callexpr.Fun, name) } // replaceAssignment replaces an assignment statement where the right hand side is a function call // whose arguments have the same names as the parameters to funcdecl with the body of funcdecl. // It sets the left hand side of the assignment to the return values of the function. func replaceAssignment(cursor *astutil.Cursor, assign *ast.AssignStmt, funcdecl *ast.FuncDecl) { if !hasTerminatingReturn(funcdecl.Body) { log.Fatal("function being inlined must have a return at the end") } body := internalastutil.CloneNode(funcdecl.Body) if hasTerminatingAndNonterminatingReturn(funcdecl.Body) { // The function has multiple return points. Add the code that we'd continue with in the caller // after each of the return points. The calling function must have a terminating return // so we don't continue execution in the replaced function after we finish executing the // continue block that we add. body = addContinues(cursor, assign, body, everythingFollowingInParent(cursor)).(*ast.BlockStmt) } if len(body.List) < 1 { log.Fatal("replacing with empty bodied function") } // The op happens in two steps: first we insert the body of the function being inlined (except for // the final return) before the assignment, and then we change the assignment statement to replace the function call // with the expressions being returned. // Determine the expressions being returned. beforeReturn, ret := body.List[:len(body.List)-1], body.List[len(body.List)-1] returnStmt, ok := ret.(*ast.ReturnStmt) if !ok { log.Fatal("last stmt in function we're replacing with should be a return") } results := returnStmt.Results // Insert the body up to the final return. for _, stmt := range beforeReturn { cursor.InsertBefore(stmt) } // Rewrite the assignment statement. replaceWithAssignment(cursor, assign.Lhs, results, assign.Tok) } // hasTerminatingReturn reparts whether the block ends in a return statement. func hasTerminatingReturn(block *ast.BlockStmt) bool { _, ok := block.List[len(block.List)-1].(*ast.ReturnStmt) return ok } // hasTerminatingAndNonterminatingReturn reports whether the block ends in a return // statement, and also has a return elsewhere in it. func hasTerminatingAndNonterminatingReturn(block *ast.BlockStmt) bool { if !hasTerminatingReturn(block) { return false } var ret bool for i := range block.List[:len(block.List)-1] { ast.Inspect(block.List[i], func(node ast.Node) bool { _, ok := node.(*ast.ReturnStmt) if ok { ret = true return false } return true }) } return ret } // everythingFollowingInParent returns a block with everything in the parent block node of the cursor after // the cursor itself. The cursor must point to an element in a block node's list. func everythingFollowingInParent(cursor *astutil.Cursor) *ast.BlockStmt { parent := cursor.Parent() block, ok := parent.(*ast.BlockStmt) if !ok { log.Fatal("internal error: in everythingFollowingInParent, cursor doesn't point to element in block list") } blockcopy := internalastutil.CloneNode(block) // get a clean copy blockcopy.List = blockcopy.List[cursor.Index()+1:] // and remove everything before and including stmt if _, ok := blockcopy.List[len(blockcopy.List)-1].(*ast.ReturnStmt); !ok { log.Printf("%s", mustFormatNode(token.NewFileSet(), blockcopy)) log.Fatal("internal error: parent doesn't end in a return") } return blockcopy } // in the case that there's a return in the body being inlined (toBlock), addContinues // replaces those returns that are not at the end of the function with the code in the // caller after the function call that execution would continue with after the return. // The block being added must end in a return. func addContinues(cursor *astutil.Cursor, assignNode *ast.AssignStmt, toBlock *ast.BlockStmt, continueBlock *ast.BlockStmt) ast.Node { if !hasTerminatingReturn(continueBlock) { log.Fatal("the block being continued to in addContinues must end in a return") } applyFunc := func(cursor *astutil.Cursor) bool { ret, ok := cursor.Node().(*ast.ReturnStmt) if !ok { return true } if cursor.Parent() == toBlock && cursor.Index() == len(toBlock.List)-1 { return false } // This is the opposite of replacing a function call with the body. First // we replace the return statement with the assignment from the caller, and // then add the code we continue with. replaceWithAssignment(cursor, assignNode.Lhs, ret.Results, assignNode.Tok) cursor.InsertAfter(internalastutil.CloneNode(continueBlock)) return false } return astutil.Apply(toBlock, applyFunc, nil) } // debugPrint prints out the expressions given by nodes for debugging. func debugPrint(nodes ...ast.Expr) string { var b strings.Builder for i, node := range nodes { b.Write(mustFormatNode(token.NewFileSet(), node)) if i != len(nodes)-1 { b.WriteString(", ") } } return b.String() } // mustFormatNode produces the formatted Go code for the given node. func mustFormatNode(fset *token.FileSet, node any) []byte { var buf bytes.Buffer format.Node(&buf, fset, node) return buf.Bytes() } // mustMatchExprs makes sure that the expression lists have the same length, // and returns the lists of the expressions on the lhs and rhs where the // identifiers are not the same. These are used to produce assignment statements // where the expressions on the right are assigned to the identifiers on the left. func mustMatchExprs(lhs []ast.Expr, rhs []ast.Expr) ([]ast.Expr, []ast.Expr) { if len(lhs) != len(rhs) { log.Fatal("exprs don't match", debugPrint(lhs...), debugPrint(rhs...)) } var newLhs, newRhs []ast.Expr for i := range lhs { lhsIdent, ok1 := lhs[i].(*ast.Ident) rhsIdent, ok2 := rhs[i].(*ast.Ident) if ok1 && ok2 && lhsIdent.Name == rhsIdent.Name { continue } newLhs = append(newLhs, lhs[i]) newRhs = append(newRhs, rhs[i]) } return newLhs, newRhs } // replaceWithAssignment replaces the node pointed to by the cursor with an assignment of the // left hand side to the righthand side, removing any redundant assignments of a variable to itself, // and replacing an assignment to a single basic literal with a constant declaration. func replaceWithAssignment(cursor *astutil.Cursor, lhs, rhs []ast.Expr, tok token.Token) { newLhs, newRhs := mustMatchExprs(lhs, rhs) if len(newLhs) == 0 { cursor.Delete() return } if len(newRhs) == 1 { if lit, ok := newRhs[0].(*ast.BasicLit); ok { constDecl := &ast.DeclStmt{ Decl: &ast.GenDecl{ Tok: token.CONST, Specs: []ast.Spec{ &ast.ValueSpec{ Names: []*ast.Ident{newLhs[0].(*ast.Ident)}, Values: []ast.Expr{lit}, }, }, }, } cursor.Replace(constDecl) return } } newAssignment := &ast.AssignStmt{ Lhs: newLhs, Rhs: newRhs, Tok: tok, } cursor.Replace(newAssignment) } // generateTable generates the file with the jump tables for the specialized malloc functions. func generateTable(sizeToSizeClass []uint8) []byte { scMax := sizeToSizeClass[smallScanNoHeaderMax] var b bytes.Buffer fmt.Fprintln(&b, `// Code generated by mkmalloc.go; DO NOT EDIT. //go:build !plan9 package runtime import "unsafe" var mallocScanTable = [513]func(size uintptr, typ *_type, needzero bool) unsafe.Pointer{`) for i := range uintptr(smallScanNoHeaderMax + 1) { fmt.Fprintf(&b, "%s,\n", smallScanNoHeaderSCFuncName(sizeToSizeClass[i], scMax)) } fmt.Fprintln(&b, ` } var mallocNoScanTable = [513]func(size uintptr, typ *_type, needzero bool) unsafe.Pointer{`) for i := range uintptr(smallScanNoHeaderMax + 1) { if i < 16 { fmt.Fprintf(&b, "%s,\n", tinyFuncName(i)) } else { fmt.Fprintf(&b, "%s,\n", smallNoScanSCFuncName(sizeToSizeClass[i], scMax)) } } fmt.Fprintln(&b, ` }`) return b.Bytes() }