github.com/goplus/llgo@v0.8.3/chore/llpyg/llpyg.go (about)

     1  /*
     2   * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"go/ast"
    24  	"go/token"
    25  	"go/types"
    26  	"log"
    27  	"os"
    28  	"os/exec"
    29  	"strings"
    30  
    31  	"github.com/goplus/gogen"
    32  	"github.com/goplus/llgo/chore/llpyg/pysig"
    33  	"github.com/goplus/llgo/ssa"
    34  )
    35  
    36  type symbol struct {
    37  	Name string `json:"name"`
    38  	Type string `json:"type"`
    39  	Doc  string `json:"doc"`
    40  	Sig  string `json:"sig"`
    41  	URL  string `json:"url"`
    42  }
    43  
    44  type module struct {
    45  	Name  string    `json:"name"`
    46  	Items []*symbol `json:"items"`
    47  }
    48  
    49  func pydump(pyLib string) (mod module) {
    50  	var out bytes.Buffer
    51  	cmd := exec.Command("pydump", pyLib)
    52  	cmd.Stdout = &out
    53  	cmd.Stderr = os.Stderr
    54  	cmd.Run()
    55  
    56  	json.Unmarshal(out.Bytes(), &mod)
    57  	return
    58  }
    59  
    60  func pysigfetch(pyLib string, names []string) (mod module) {
    61  	var out bytes.Buffer
    62  	cmd := exec.Command("pysigfetch", pyLib, "-")
    63  	cmd.Stdin = strings.NewReader(strings.Join(names, " "))
    64  	cmd.Stdout = &out
    65  	cmd.Stderr = os.Stderr
    66  	cmd.Run()
    67  
    68  	json.Unmarshal(out.Bytes(), &mod)
    69  	return
    70  }
    71  
    72  func main() {
    73  	if len(os.Args) < 2 {
    74  		fmt.Fprintln(os.Stderr, "Usage: llpyg <pythonLibPath>")
    75  		return
    76  	}
    77  	pyLib := os.Args[1]
    78  
    79  	mod := pydump(pyLib)
    80  	if mod.Name != pyLib {
    81  		log.Printf("import module %s failed\n", pyLib)
    82  		os.Exit(1)
    83  	}
    84  	pkg := gogen.NewPackage("", pyLib, nil)
    85  	pkg.Import("unsafe").MarkForceUsed(pkg)       // import _ "unsafe"
    86  	py := pkg.Import("github.com/goplus/llgo/py") // import "github.com/goplus/llgo/py"
    87  
    88  	f := func(cb *gogen.CodeBuilder) int {
    89  		cb.Val("py." + mod.Name)
    90  		return 1
    91  	}
    92  	defs := pkg.NewConstDefs(pkg.Types.Scope())
    93  	defs.New(f, 0, 0, nil, "LLGoPackage")
    94  
    95  	obj := py.Ref("Object").(*types.TypeName).Type().(*types.Named)
    96  	objPtr := types.NewPointer(obj)
    97  	ret := types.NewTuple(pkg.NewParam(0, "", objPtr))
    98  
    99  	ctx := &context{pkg, obj, objPtr, ret, nil, py}
   100  	ctx.genMod(pkg, &mod)
   101  	skips := ctx.skips
   102  	if n := len(skips); n > 0 {
   103  		log.Printf("==> There are %d signatures not found, fetch from doc site\n", n)
   104  		mod = pysigfetch(pyLib, skips)
   105  		ctx.skips = skips[:0]
   106  		ctx.genMod(pkg, &mod)
   107  		if len(mod.Items) > 0 {
   108  			skips = ctx.skips
   109  		}
   110  		if n := len(skips); n > 0 {
   111  			log.Printf("==> Skip %d symbols:\n%v\n", n, skips)
   112  		}
   113  	}
   114  
   115  	pkg.WriteTo(os.Stdout)
   116  }
   117  
   118  type context struct {
   119  	pkg    *gogen.Package
   120  	obj    *types.Named
   121  	objPtr *types.Pointer
   122  	ret    *types.Tuple
   123  	skips  []string
   124  	py     gogen.PkgRef
   125  }
   126  
   127  func (ctx *context) genMod(pkg *gogen.Package, mod *module) {
   128  	for _, sym := range mod.Items {
   129  		switch sym.Type {
   130  		case "builtin_function_or_method", "function", "ufunc", "method-wrapper":
   131  			ctx.genFunc(pkg, sym)
   132  		case "str", "float", "bool", "type", "dict", "tuple", "list", "object", "module",
   133  			"int", "set", "frozenset", "flags", "bool_", "pybind11_type", "layout",
   134  			"memory_format", "qscheme", "dtype", "tensortype": // skip
   135  		case "": // pysigfetch: page not found
   136  			ctx.skips = append(ctx.skips, sym.Name)
   137  		default:
   138  			t := sym.Type
   139  			if len(t) > 0 && (t[0] >= 'a' && t[0] <= 'z') && !strings.HasSuffix(t, "_info") {
   140  				log.Panicln("unsupport type:", sym.Type)
   141  			}
   142  		}
   143  	}
   144  }
   145  
   146  func (ctx *context) genFunc(pkg *gogen.Package, sym *symbol) {
   147  	name, symSig := sym.Name, sym.Sig
   148  	if len(name) == 0 || name[0] == '_' {
   149  		return
   150  	}
   151  	if symSig == "<NULL>" {
   152  		ctx.skips = append(ctx.skips, name)
   153  		return
   154  	}
   155  	params, variadic := ctx.genParams(pkg, symSig)
   156  	name = genName(name, -1)
   157  	sig := types.NewSignatureType(nil, nil, nil, params, ctx.ret, variadic)
   158  	fn := pkg.NewFuncDecl(token.NoPos, name, sig)
   159  	list := ctx.genDoc(sym.Doc)
   160  	if sym.URL != "" {
   161  		if len(list) > 0 {
   162  			list = append(list, emptyCommentLine)
   163  		}
   164  		list = append(list, genSee(sym.URL))
   165  	}
   166  	if len(list) > 0 {
   167  		list = append(list, emptyCommentLine)
   168  	}
   169  	list = append(list, ctx.genLinkname(name, sym))
   170  	fn.SetComments(pkg, &ast.CommentGroup{List: list})
   171  	// fn.BodyStart(pkg).End()
   172  }
   173  
   174  func (ctx *context) genParams(pkg *gogen.Package, sig string) (*types.Tuple, bool) {
   175  	args := pysig.Parse(sig)
   176  	if len(args) == 0 {
   177  		return nil, false
   178  	}
   179  	n := len(args)
   180  	objPtr := ctx.objPtr
   181  	list := make([]*types.Var, 0, n)
   182  	for i := 0; i < n; i++ {
   183  		name := args[i].Name
   184  		if name == "/" {
   185  			continue
   186  		}
   187  		if name == "*" || name == "\\*" {
   188  			break
   189  		}
   190  		if strings.HasPrefix(name, "*") {
   191  			if name[1] != '*' {
   192  				list = append(list, ssa.VArg())
   193  				return types.NewTuple(list...), true
   194  			}
   195  			return types.NewTuple(list...), false
   196  		}
   197  		list = append(list, pkg.NewParam(0, genName(name, 0), objPtr))
   198  	}
   199  	return types.NewTuple(list...), false
   200  }
   201  
   202  func genName(name string, idxDontTitle int) string {
   203  	parts := strings.Split(name, "_")
   204  	for i, part := range parts {
   205  		if i != idxDontTitle && part != "" {
   206  			if c := part[0]; c >= 'a' && c <= 'z' {
   207  				part = string(c+'A'-'a') + part[1:]
   208  			}
   209  			parts[i] = part
   210  		}
   211  	}
   212  	name = strings.Join(parts, "")
   213  	switch name {
   214  	case "default", "func", "var", "":
   215  		name += "_"
   216  	}
   217  	return name
   218  }
   219  
   220  func (ctx *context) genLinkname(name string, sym *symbol) *ast.Comment {
   221  	return &ast.Comment{Text: "//go:linkname " + name + " py." + sym.Name}
   222  }
   223  
   224  func (ctx *context) genDoc(doc string) []*ast.Comment {
   225  	if doc == "" {
   226  		return make([]*ast.Comment, 0, 4)
   227  	}
   228  	lines := strings.Split(doc, "\n")
   229  	list := make([]*ast.Comment, len(lines), len(lines)+4)
   230  	for i, line := range lines {
   231  		list[i] = &ast.Comment{Text: "// " + line}
   232  	}
   233  	return list
   234  }
   235  
   236  func genSee(url string) *ast.Comment {
   237  	return &ast.Comment{Text: "// See " + url}
   238  }
   239  
   240  var (
   241  	emptyCommentLine = &ast.Comment{Text: "//"}
   242  )