Skip to content

cl: generate logical classes for MethodCheck with AsXxx conversion (#948) - #949

Merged
xushiwei merged 1 commit into
mainfrom
fennoai/issue-948-1791283299
Oct 6, 2026
Merged

xushiwei merged 1 commit into
mainfrom
fennoai/issue-948-1791283299

Conversation

@fennoai

@fennoai fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by @xushiwei

Fixes #948.

MethodCheck can tell which logical type a C function belongs to (e.g. PyList_GetItem belongs to List), but every such method was still attached to the physical base class Object, polluting the base API and inviting name conflicts (PyList_GetItem and PyDict_GetItem both wanted to be Item).

What changed

  • cl/logical.go (new): logical-class machinery. logicalClassOf lazily emits type <Class> struct { <Base> } and an As<Class> conversion method on the base class (func (o *Object) AsList() *List { return (*List)(unsafe.Pointer(o)) }). It reuses an existing package-level type of the same name, and reports a diagnostic if As<Class> collides with an existing base method.
  • cl/func.go: when a C global function resolves (via MethodCheck) to a logical class distinct from its physical receiver type, the method is emitted on the logical class instead of the base.
  • cl/ctx.go / cl/compile.go: added and initialized the logicals map on pkgCtx.
  • tool/_testc/python-3.14.8: added PyDict_GetItem to the header (name-collision case) and updated the expected pythread.go.

Behavior (matches the acceptance criteria)

  • PyList_GetItem → (*List).Item; Object gets AsList() only because List has a method.
  • PyDict_GetItem → (*Dict).Item + (*Object).AsDict() — no conflict with List.Item.
  • PyObject_IsTrue stays (*Object).IsTrue with no AsObject; Py_IsTrue stays a package function IsTrue.
  • Logical classes are generated lazily and in deterministic (source) order.

Verification

  • go build ./cl/ and go vet ./cl/... ./tool/... type-check clean. The expected pythread.go is gofmt-canonical.
  • Note: the full test binaries (go test) link against libclang/llgo, which isn't available in this sandbox, so the end-to-end TestPython run (darwin-only in the harness) was not executed here. The generator logic and the updated golden file were verified by code analysis against gogen's emission/formatting behavior.

When MethodCheck resolves a C function to a logical type (e.g. PyList_GetItem
-> List) that differs from its physical receiver (Object), emit the method on
a new logical class that embeds the base class, and generate an As<Class>
conversion method on the base (e.g. (*Object).AsList() *List). The conversion
only reinterprets the pointer.

Logical classes are generated lazily - only when at least one function resolves
to them. When the logical type is the physical type itself (PyObject_IsTrue ->
Object) the method stays on Object with no AsObject; functions with an empty
logical type (Py_IsTrue) remain plain package functions. An existing
package-level type of the same name is reused rather than regenerated, and an
As<Class> that collides with an existing base method reports a diagnostic.

tool test: TestPython now covers List, Dict (PyDict_GetItem collision case),
and the Object/package-function cases.

Fixes #948
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.72093% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.95%. Comparing base (08ec1a5) to head (4773729).

Files with missing lines Patch % Lines
cl/logical.go 81.08% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #949      +/-   ##
==========================================
- Coverage   88.02%   87.95%   -0.07%     
==========================================
  Files          22       23       +1     
  Lines        2622     2665      +43     
==========================================
+ Hits         2308     2344      +36     
- Misses        314      321       +7     
Flag Coverage Δ
llgo-tests 87.95% <83.72%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: logical classes (issue #948)

This is a clean, well-documented addition. The lazy generation is correctly guarded (cls != "" && cls != typName), the logicals map is properly initialized, emission order is deterministic (driven by function compile order, not map iteration), and the golden output matches the described behavior (PyObject_IsTrue correctly stays on Object with no AsObject). The package-level doc comment explaining the why (method-name conflicts on the base) is excellent.

A few findings below, mostly around the doc/behavior mismatch on the collision path and two latent edge cases on the type-reuse path. None are blocking.

Comment thread cl/logical.go
Comment on lines +80 to +81
// As<Class> conversion; genAsMethod itself keeps any pre-existing method of
// that name and reports a diagnostic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 genAsMethod calls p.errorf(...) on a name collision, which increments p.errCnt; compile.go:287 then turns any errCnt > 0 into fmt.Errorf("compilation failed with %d errors", ...), and under FailFast checkFailFast can os.Exit(1). So the outcome is a compilation error that fails generation (the As<Class> method is simply skipped), not a tolerant warning. Only the narrow "existing method is kept" sub-claim is true. Suggest rewording to state the collision is reported as a compilation error and the conversion is skipped.

Comment thread cl/logical.go
// 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 {

Copy link
Copy Markdown
Contributor Author

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

logicalClassOf caches by goName only (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's base) with no diagnostic. At the call site in func.go the 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 when p.logicals[goName].base != base.

Comment thread cl/logical.go
// 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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

newLogicalType reuses any package-level type found by Scope().Lookup(goName) (e.g. one from headers or TypeAlias) without checking it is a struct whose first field is the embedded base. genAsMethod then unconditionally emits return (*List)(unsafe.Pointer(o)). That reinterpret cast is only memory-safe when the reused type actually begins with the base layout; the freshly generated path guarantees this (single embedded field at offset 0), but the reuse path does not. A same-named type with a different layout would produce a silently incorrect, memory-unsafe conversion. Recommend validating the reused type's first field is base (skip/diagnose otherwise) before adding the conversion, and documenting the layout assumption.

Comment thread cl/logical.go
sig := types.NewSignatureType(recv, nil, nil, nil, results, false)

f, err := pkg.NewFuncWith(goNodePos(p, decl), name, sig, nil)
if err != nil {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] genAsMethod aborts whole run via panicf instead of errorf

On a NewFuncWith error, genAsMethod calls p.panicf (→ log.Panicf), tearing down the entire generation run. Note findMember only checks the base's own methods/direct fields, not names promoted through the base's embedded types, so a promoted-name collision would fall through to NewFuncWith and hit this panic rather than the graceful errorf path above it. For consistency with the rest of the file's error handling, consider errorf + return so one malformed/colliding symbol doesn't abort the whole package.

@xushiwei
xushiwei merged commit b00c3ec into main Oct 6, 2026
2 of 4 checks passed
@fennoai
fennoai Bot deleted the fennoai/issue-948-1791283299 branch October 6, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

llcppg: generate logical classes for MethodCheck and convert from the base class

1 participant