Repository navigation
cl: generate logical classes for MethodCheck with AsXxx conversion (#948) #949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| /* | ||
| * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package cl | ||
|
|
||
| import ( | ||
| "go/token" | ||
| "go/types" | ||
|
|
||
| "github.com/goplus/llcppg/clang" | ||
| ) | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
|
|
||
| // Logical classes. | ||
| // | ||
| // MethodCheck can tell which logical type a C function belongs to (for example | ||
| // PyList_GetItem belongs to List even though its receiver is the physical base | ||
| // class Object). Instead of attaching every such function to the physical base | ||
| // class - which pollutes the base API surface and invites method-name conflicts | ||
| // (PyList_GetItem and PyDict_GetItem would both want to be Item) - llcppg emits | ||
| // each method on its own logical class that embeds the physical base class, plus | ||
| // a conversion method on the base class named As<Class> that reinterprets the | ||
| // pointer. | ||
| // | ||
| // type List struct { | ||
| // Object | ||
| // } | ||
| // | ||
| // func (o *Object) AsList() *List { return (*List)(unsafe.Pointer(o)) } | ||
| // | ||
| // // llgo:link (*List).Item C.PyList_GetItem | ||
| // func (self *List) Item(index *Object) *Object { return nil } | ||
| // | ||
| // A logical class is generated lazily: only when at least one function actually | ||
| // resolves to it. If the logical type is the physical type itself (for example | ||
| // PyObject_IsTrue resolves to Object), the method stays on Object and no class | ||
| // or AsObject is generated. See https://github.com/goplus/llcppg/issues/948. | ||
|
|
||
| const asMethodPrefix = "As" | ||
|
|
||
| type logicalClass struct { | ||
| named *types.Named // the logical class type (List) | ||
| base *types.Named // the physical base class it embeds (Object) | ||
| } | ||
|
|
||
| // logicalClassOf returns the logical class named goName whose base is the | ||
| // physical type base, creating it (and its As<Class> conversion method on base) | ||
| // on first use. It is lazy and deterministic: the type and the conversion method | ||
| // are emitted in the order functions first resolve to the logical class. | ||
| // | ||
| // If a package-level type with the same name already exists (generated from the | ||
| // headers or provided through TypeAlias), it is reused instead of generating a | ||
| // second one, and the As<Class> conversion is still added when it is missing. | ||
| func (p *pkgCtx) logicalClassOf(decl clang.Cursor, goName string, base *types.Named) *types.Named { | ||
| if lc, ok := p.logicals[goName]; ok { | ||
| return lc.named | ||
| } | ||
|
|
||
| named := p.newLogicalType(decl, goName, base) | ||
| lc := &logicalClass{named: named, base: base} | ||
| p.logicals[goName] = lc | ||
|
|
||
| // genAsMethod runs once per logical class (subsequent resolutions hit the | ||
| // cache above). It is attempted whether the type was freshly emitted or an | ||
| // existing package-level type was reused, so a reused type still gets its | ||
| // As<Class> conversion; genAsMethod itself keeps any pre-existing method of | ||
| // that name and reports a diagnostic. | ||
|
Comment on lines
+80
to
+81
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Doc comment calls a hard compilation error a soft "diagnostic" The comment says "genAsMethod itself keeps any pre-existing method of that name and reports a diagnostic" (also lines 66-67 and 111-112). In practice |
||
| p.genAsMethod(decl, lc) | ||
| return named | ||
| } | ||
|
|
||
| // newLogicalType returns the named type for the logical class, reusing an | ||
| // existing package-level type when one is present and otherwise emitting a fresh | ||
| // "type <goName> struct { <base> }". | ||
| func (p *pkgCtx) newLogicalType(decl clang.Cursor, goName string, base *types.Named) *types.Named { | ||
| if o := p.pkg.Types.Scope().Lookup(goName); o != nil { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Reused pre-existing type not verified to embed base at offset 0
|
||
| if tn, ok := o.(*types.TypeName); ok { | ||
| if n, ok := tn.Type().(*types.Named); ok { | ||
| return n | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pkg := p.pkg | ||
| pkgTypes := pkg.Types | ||
| typDecl := newType(p, decl, "", goName) | ||
| embed := types.NewField(goNodePos(p, decl), pkgTypes, base.Obj().Name(), base, true) | ||
| typDecl.InitType(pkg, types.NewStruct([]*types.Var{embed}, nil)) | ||
| return typDecl.Type() | ||
| } | ||
|
|
||
| // genAsMethod emits the conversion method on the base class, for example | ||
| // | ||
| // func (o *Object) AsList() *List { return (*List)(unsafe.Pointer(o)) } | ||
| // | ||
| // The conversion only reinterprets the pointer; it does not call into C and does | ||
| // not check the object's real type. If a method of the same name already exists | ||
| // on the base class, the existing method is kept and a diagnostic is reported. | ||
| func (p *pkgCtx) genAsMethod(decl clang.Cursor, lc *logicalClass) { | ||
| name := asMethodPrefix + lc.named.Obj().Name() | ||
| if pos, _, exists := findMember(lc.base, name); exists { | ||
| p.errorf(decl, "%s redeclared in this block\n\t%v: other declaration of %s", name, p.position(pos), name) | ||
| return | ||
| } | ||
|
|
||
| pkg := p.pkg | ||
| pkgTypes := pkg.Types | ||
| recvType := types.NewPointer(lc.base) | ||
| retType := types.NewPointer(lc.named) | ||
| recv := types.NewParam(goNodePos(p, decl), pkgTypes, "o", recvType) | ||
| results := types.NewTuple(types.NewParam(token.NoPos, pkgTypes, "", retType)) | ||
| sig := types.NewSignatureType(recv, nil, nil, nil, results, false) | ||
|
|
||
| f, err := pkg.NewFuncWith(goNodePos(p, decl), name, sig, nil) | ||
| if err != nil { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] genAsMethod aborts whole run via panicf instead of errorf On a |
||
| p.panicf(decl, "logical class %s: genAsMethod failed - %v", lc.named.Obj().Name(), err) | ||
| } | ||
| cb := f.BodyStart(pkg) | ||
| // return (*List)(unsafe.Pointer(o)) | ||
| cb.Typ(retType). | ||
| Typ(p.unsafePointer()).VarVal("o"). | ||
| Call(1). | ||
| Call(1). | ||
| Return(1).End() | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Logical-class cache ignores base; differing receiver silently absorbed
logicalClassOfcaches bygoNameonly (p.logicals[goName]). If two C functions resolve via MethodCheck to the same logical class name but have different physical receiver types, the second reuses the first's logical class (which embeds the first function'sbase) with no diagnostic. At the call site infunc.gothe receiver is then unconditionally rewritten to that cached logical type, so the emitted method would embed/convert through the wrong base relative to its actual C receiver. Latent given typical MethodCheck patterns, but there is no guard — consider keying on(goName, base)or reporting a diagnostic whenp.logicals[goName].base != base.