diff --git a/_xtool/pydump/class.go b/_xtool/pydump/class.go new file mode 100644 index 0000000..4fe3e23 --- /dev/null +++ b/_xtool/pydump/class.go @@ -0,0 +1,174 @@ +package main + +import ( + "fmt" + "strings" + "github.com/goplus/lib/c" + "github.com/goplus/lib/py" + "github.com/goplus/lib/py/inspect" + "github.com/goplus/llpyg/symbol" +) + +func parseClass(class *py.Object, sym *symbol.Symbol) (*symbol.Class, error) { + cls := &symbol.Class{ + Name: sym.Name, + Doc: sym.Doc, + } + // bases + bases, err := parseBases(class, sym.Name) + if err != nil { + return nil, err + } + cls.Bases = bases + // methods, properties, etc. + cls, err = parseClassDict(class, cls) + if err != nil { + return nil, err + } + return cls, nil +} + +// get class parents +func parseBases(class *py.Object, name string) ([]*symbol.Base, error) { + basesObj := class.GetAttrString(c.Str("__bases__")) // tuple + if basesObj == nil { + return nil, fmt.Errorf("can't get __bases__ from %s", name) + } + bases := make([]*symbol.Base, 0) + for i, n := 0, basesObj.TupleLen(); i < n; i++ { + baseObj := basesObj.TupleItem(i) + base := &symbol.Base{ + Name: c.GoString(baseObj.GetAttrString(c.Str("__name__")).CStr()), + Module: c.GoString(baseObj.GetAttrString(c.Str("__module__")).CStr()), + } + bases = append(bases, base) + } + return bases, nil +} + +func parseClassDict(class *py.Object, cls *symbol.Class) (*symbol.Class, error) { + items, err := getRealDictItems(class, cls.Name) + if err != nil { + return nil, err + } + for i, n := 0, items.ListLen(); i < n; i++ { + item := items.ListItem(i) + name := c.GoString(item.TupleItem(0).CStr()) + val := item.TupleItem(1) + typeName := c.GoString(val.Type().TypeName().CStr()) + typeName = strings.TrimSpace(typeName) + // init method + if name == "__init__" { + sym := &symbol.Symbol{ + Name: name, + Type: typeName, + } + sig, err := getSignature(val, sym, true) + if err != nil { + return nil, err + } + sym.Sig = sig + cls.InitMethod = sym + continue + } + // instance method + if inspect.Isfunction(val).IsTrue() == 1 { + sym, err := parseMethod(val, name, typeName, true) + if err != nil { + return nil, err + } + cls.InstanceMethods = append(cls.InstanceMethods, sym) + continue + } + // hard-code + switch typeName { + case "classmethod": + val = val.GetAttrString(c.Str("__func__")) + if val == nil { + return nil, fmt.Errorf("can't get __func__ of %s", name) + } + sym, err := parseMethod(val, name, typeName, true) + if err != nil { + return nil, err + } + cls.ClassMethods = append(cls.ClassMethods, sym) + case "staticmethod": + val = val.GetAttrString(c.Str("__func__")) + if val == nil { + return nil, fmt.Errorf("can't get __func__ of %s", name) + } + sym, err := parseMethod(val, name, typeName, false) + if err != nil { + return nil, err + } + cls.StaticMethods = append(cls.StaticMethods, sym) + case "property": + property, err := parseProperty(val, name) + if err != nil { + return nil, err + } + cls.Properties = append(cls.Properties, property) + default: + // TODO: others + } + } + return cls, nil +} + +func parseMethod(val *py.Object, name string, typeName string, skipFirst bool) (*symbol.Symbol, error) { + sym := &symbol.Symbol{ + Name: name, + Type: typeName, + } + doc := val.GetAttrString(c.Str("__doc__")) + if doc != nil && doc.IsTrue() == 1 { + sym.Doc = c.GoString(doc.Str().CStr()) + } + sig, err := getSignature(val, sym, skipFirst) + if err != nil { + return nil, err + } + sym.Sig = sig + return sym, nil +} + +func parseProperty(val *py.Object, name string) (*symbol.Property, error) { + property := &symbol.Property{ + Name: name, + } + getter := val.GetAttrString(c.Str("fget")) + if getter != nil { + // (self) -> value + property.Getter = "()" + } + setter := val.GetAttrString(c.Str("fset")) + if setter != nil { + sym := &symbol.Symbol{ + Name: name, + Type: "property", + } + // (self, value) -> None + sig, err := getSignature(setter, sym, true) + if err != nil { + return nil, err + } + property.Setter = sig + } + return property, nil +} + +func getRealDictItems(class *py.Object, name string) (*py.Object, error) { + dict := class.GetAttrString(c.Str("__dict__")) + if dict == nil { + return nil, fmt.Errorf("can't get __dict__ of %s", name) + } + dictTypeName := c.GoString(dict.Type().TypeName().CStr()) + if dictTypeName != "mappingproxy" { + return nil, fmt.Errorf("__dict__ of %s is not a mappingproxy", name) + } + realDict := dict.CallMethod(c.Str("copy"), nil) + if realDict == nil { + return nil, fmt.Errorf("failed to copy real dict of %s", name) + } + return realDict.DictItems(), nil +} diff --git a/_xtool/pydump/pydump.go b/_xtool/pydump/pydump.go index 7b8be43..15303f5 100644 --- a/_xtool/pydump/pydump.go +++ b/_xtool/pydump/pydump.go @@ -38,29 +38,39 @@ func extractSignatureFromDoc(doc, funcName string) string { return strings.Join(fields, " ") } -func getSignature(val *py.Object, sym *symbol.Symbol) string { - // function, method, class, or implement __call__ +func getSignature(val *py.Object, sym *symbol.Symbol, skipFirst bool) (string, error) { + // which has __call__ if val.Callable() == 0 { - return "" + return "", fmt.Errorf("%s is not callable", sym.Name) } - // get signature from inspect - sigFromInspect := inspect.Signature(val) - if sigFromInspect != nil { - sig := c.GoString(sigFromInspect.Str().CStr()) - if sig != "(*args, **kwargs)" { - return sig + // use inspect + sigObj := inspect.Signature(val) + if sigObj != nil { + sig := c.GoString(sigObj.Str().CStr()) + if skipFirst { + sig = removeFirstParam(sig) } + return sig, nil } - // get signature from doc - sigFromDoc := extractSignatureFromDoc(sym.Doc, sym.Name) - if sigFromDoc != "" { - return sigFromDoc + // parse doc + sig := extractSignatureFromDoc(sym.Doc, sym.Name) + if sig != "" { + return sig, nil } // Paradigms if pyFuncTypes[sym.Type] { - return "(*args, **kwargs)" + return "(*args, **kwargs)", nil + } + return "", fmt.Errorf("failed to get signature of %s", sym.Name) +} + +// (self, ...) -> (...) +func removeFirstParam(sig string) string { + idx := strings.Index(sig, ",") + if idx == -1 { + return "()" } - return "" + return "(" + strings.TrimSpace(sig[idx+1:]) } // moduleName: Python module name @@ -70,11 +80,8 @@ func pydump(moduleName string) (*symbol.Module, error) { if mod == nil { return nil, fmt.Errorf("failed to import module %s", moduleName) } - // get dict, python list Object + // get object dict keys := mod.ModuleGetDict().DictKeys() - if keys == nil { - return nil, fmt.Errorf("failed to get dict keys of %s", moduleName) - } // create module instance modInstance := &symbol.Module{ Name: moduleName, @@ -96,10 +103,23 @@ func pydump(moduleName string) (*symbol.Module, error) { } // functions if pyFuncTypes[sym.Type] { - sym.Sig = getSignature(val, sym) + sig, err := getSignature(val, sym, false) + if err != nil { + return nil, err + } + sym.Sig = sig modInstance.Functions = append(modInstance.Functions, sym) + continue + } + // classes + if inspect.Isclass(val).IsTrue() == 1 { + cls, err := parseClass(val, sym) + if err != nil { + return nil, err + } + modInstance.Classes = append(modInstance.Classes, cls) + continue } - // TODO: variables, classes, etc. } return modInstance, nil } diff --git a/symbol/symbol.go b/symbol/symbol.go index e0e5d2b..d7078e0 100644 --- a/symbol/symbol.go +++ b/symbol/symbol.go @@ -10,5 +10,31 @@ type Symbol struct { type Module struct { Name string `json:"name"` // python module name Functions []*Symbol `json:"functions"` // package functions - // TODO: variables, classes, etc. + Classes []*Class `json:"classes"` +} + +// base class +type Base struct { + Name string `json:"name"` + Module string `json:"module"` +} + +// @property +type Property struct { + Name string `json:"name"` + Getter string `json:"getter"` + Setter string `json:"setter"` +} + +// Python class +type Class struct { + Name string `json:"name"` + Doc string `json:"doc"` + Bases []*Base `json:"bases"` + InitMethod *Symbol `json:"initMethod"` + InstanceMethods []*Symbol `json:"instanceMethods"` // include override special methods + ClassMethods []*Symbol `json:"classMethods"` + StaticMethods []*Symbol `json:"staticMethods"` + Properties []*Property `json:"properties"` + // TODO: attributes } diff --git a/tool/pygen/genclass.go b/tool/pygen/genclass.go new file mode 100644 index 0000000..48e844f --- /dev/null +++ b/tool/pygen/genclass.go @@ -0,0 +1,119 @@ +package pygen + +import ( + "github.com/goplus/gogen" + "github.com/goplus/llpyg/symbol" + "go/ast" + "go/token" + "go/types" + "log" + "strings" +) + +func (ctx *context) genClasses(pkg *gogen.Package, classes []*symbol.Class) { + for _, cls := range classes { + if len(cls.Bases) == 0 { + log.Fatalf("Class %s has no base", cls.Name) + } + // now inherit from object only + // TODO: support other bases + ctx.genStruct(pkg, cls.Name) + if cls.InitMethod != nil { + ctx.genConstructor(pkg, cls) + } + ctx.genMethods(pkg, cls) + ctx.genProperties(pkg, cls) + } +} + +func (ctx *context) genStruct(pkg *gogen.Package, name string) { + structType := pkg.NewTypeDefs().NewType(ctx.genName(name, -1)) + // TODO: support parents embed + structType.InitType(pkg, types.NewStruct( + []*types.Var{ + types.NewVar(0, pkg.Types, "", ctx.obj), + }, + nil, + )) + ctx.structs[name] = structType.Type() +} + +func (ctx *context) genConstructor(pkg *gogen.Package, cls *symbol.Class) { + // TODO: convert to XGO struct.New + funcName := "New" + ctx.genName(cls.Name, -1) + params, variadic := ctx.genParams(pkg, cls.InitMethod.Sig) + // return struct pointer + ret := types.NewTuple(pkg.NewParam(0, "", types.NewPointer(ctx.structs[cls.Name]))) + sig := types.NewSignatureType(nil, nil, nil, params, ret, variadic) + fn := pkg.NewFuncDecl(token.NoPos, funcName, sig) + // doc + docList := ctx.genDoc(cls.Doc) + if len(docList) > 0 { + docList = append(docList, emptyCommentLine) + } + // linkname + goLinkname := "//go:linkname " + funcName + " py." + cls.Name + docList = append(docList, &ast.Comment{Text: goLinkname}) + fn.SetComments(pkg, &ast.CommentGroup{List: docList}) +} + +func (ctx *context) genMethods(pkg *gogen.Package, cls *symbol.Class) { + for _, method := range cls.InstanceMethods { + name := method.Name + // TODO: XGO magic method style + if strings.HasPrefix(name, "__") && strings.HasSuffix(name, "__") { + name = name[2 : len(name)-2] + } + funcName := ctx.genName(name, -1) + // full name + method.Name = cls.Name + "." + method.Name + ctx.genMethod(pkg, method, cls.Name, funcName, true) // return *py.Object + } + // TODO: class methods, static methods to XGO static methods +} + +func (ctx *context) genProperties(pkg *gogen.Package, cls *symbol.Class) { + for _, property := range cls.Properties { + name := cls.Name + property.Name + if property.Getter != "" { + sym := &symbol.Symbol{Name: name + ".__get__", Sig: property.Getter} + funcName := ctx.genName(property.Name, -1) + ctx.genMethod(pkg, sym, cls.Name, funcName, true) + } + if property.Setter != "" { + sym := &symbol.Symbol{Name: name + ".__set__", Sig: property.Setter} + funcName := ctx.genName("set_"+property.Name, -1) + ctx.genMethod(pkg, sym, cls.Name, funcName, false) + } + } +} +func (ctx *context) genMethod(pkg *gogen.Package, method *symbol.Symbol, clsName, funcName string, ret bool) { + cls := ctx.structs[clsName] + recv := types.NewVar(0, pkg.Types, "", types.NewPointer(cls)) + // signature + params, variadic := ctx.genParams(pkg, method.Sig) + var retType *types.Tuple + if ret { + retType = ctx.ret + } + sig := types.NewSignatureType(recv, nil, nil, params, retType, variadic) + fn, err := pkg.NewFuncWith(token.NoPos, funcName, sig, nil) + if err != nil { + log.Fatal(err) + } + if ret { + fn.BodyStart(pkg).ZeroLit(ctx.objPtr).Return(1).End() + } else { + fn.BodyStart(pkg).End() + } + // doc + docList := ctx.genDoc(method.Doc) + if len(docList) > 0 { + docList = append(docList, emptyCommentLine) + } + //llgo:link + funcName = "(*" + cls.Obj().Name() + ")." + funcName + link := "//llgo:link " + funcName + " py." + method.Name + docList = append(docList, &ast.Comment{Text: link}) + fn.SetComments(pkg, &ast.CommentGroup{List: docList}) +} diff --git a/tool/pygen/pygen.go b/tool/pygen/pygen.go index 905398d..18eb9d6 100644 --- a/tool/pygen/pygen.go +++ b/tool/pygen/pygen.go @@ -24,6 +24,7 @@ type context struct { objPtr *types.Pointer ret *types.Tuple py gogen.PkgRef + structs map[string]*types.Named skips []symbol.Symbol } @@ -89,7 +90,7 @@ func createGoPackage(mod symbol.Module) (ctx *context) { obj := py.Ref("Object").(*types.TypeName).Type().(*types.Named) objPtr := types.NewPointer(obj) ret := types.NewTuple(pkg.NewParam(0, "", objPtr)) // return *py.Object - ctx = &context{pkg, obj, objPtr, ret, py, nil} + ctx = &context{pkg, obj, objPtr, ret, py, make(map[string]*types.Named), nil} return ctx } @@ -103,7 +104,8 @@ func (ctx *context) genMod(pkg *gogen.Package, mod *symbol.Module) { funcMap[sym.Name] = true ctx.genFunc(pkg, sym) } - // TODO: class, variable, etc. + // classes + ctx.genClasses(pkg, mod.Classes) } diff --git a/tool/pygen/pygen_test.go b/tool/pygen/pygen_test.go index 2a4d8ec..2490d7e 100644 --- a/tool/pygen/pygen_test.go +++ b/tool/pygen/pygen_test.go @@ -41,6 +41,21 @@ func TestGenFunc(t *testing.T) { t.Logf("test gen func pass") } +func TestGenClass(t *testing.T) { + prepareEnv("./testdata/class") + mod, err := pydump("demo") + if err != nil { + t.Fatal(err) + } + ctx := createGoPackage(mod) + ctx.genClasses(ctx.pkg, mod.Classes) + err = compareWithExpected(t, ctx, "testdata/class/expect.go") + if err != nil { + t.Fatalf("test gen class failed: %v", err) + } + t.Logf("test gen class pass") +} + func compareWithExpected(t *testing.T, ctx *context, expectedPath string) error { outFilePath := "./temp/actual_git.go" dir := filepath.Dir(outFilePath) diff --git a/tool/pygen/testdata/class/demo.py b/tool/pygen/testdata/class/demo.py new file mode 100644 index 0000000..5bd9920 --- /dev/null +++ b/tool/pygen/testdata/class/demo.py @@ -0,0 +1,36 @@ +class Animal: + def __init__(self, name): + self._name = name + + def speak(self): + pass + +class Dog(Animal): + + DOG_NAME = "Dog" + + def __init__(self, name, age): + super().__init__(name) + self._age = age + + def speak(self): + print(f"Dog {self._name} is speaking") + + @property + def age(self): + return self._age + + @age.setter + def age(self, age): + self._age = age + + @classmethod + def bark(cls, msg): + print(f"Dog is barking {msg}") + + @staticmethod + def sleep(): + print("Dog is sleeping") + + def __str__(self): + return f"Dog {self._name} is {self._age} years old" diff --git a/tool/pygen/testdata/class/expect.go b/tool/pygen/testdata/class/expect.go new file mode 100644 index 0000000..2a421a4 --- /dev/null +++ b/tool/pygen/testdata/class/expect.go @@ -0,0 +1,46 @@ +package demo + +import ( + "github.com/goplus/lib/py" + _ "unsafe" +) + +const LLGoPackage = "py.demo" + +type Animal struct { + py.Object +} + +//go:linkname NewAnimal py.Animal +func NewAnimal(name *py.Object) *Animal + +//llgo:link (*Animal).Speak py.Animal.speak +func (*Animal) Speak() *py.Object { + return nil +} + +type Dog struct { + py.Object +} + +//go:linkname NewDog py.Dog +func NewDog(name *py.Object, age *py.Object) *Dog + +//llgo:link (*Dog).Speak py.Dog.speak +func (*Dog) Speak() *py.Object { + return nil +} + +//llgo:link (*Dog).Str py.Dog.__str__ +func (*Dog) Str() *py.Object { + return nil +} + +//llgo:link (*Dog).Age py.Dogage.__get__ +func (*Dog) Age() *py.Object { + return nil +} + +//llgo:link (*Dog).SetAge py.Dogage.__set__ +func (*Dog) SetAge(age *py.Object) { +}