From 2eb104f31ffd2f22e24a4bb12bb0d132dba69c6e Mon Sep 17 00:00:00 2001 From: xushiwei <396972+xushiwei@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:24:52 +0000 Subject: [PATCH 1/3] fix(cl): avoid re-initializing a class body, which caused an infinite loop The three-stage refactor (preload/load/compile) could reach the same C/C++ class more than once (redeclarations and definitions pulled in repeatedly across the standard headers that DenseMapInfo/STLForwardCompat include). loadClass/loadTemplateClass registered the type declaration on first sight but then re-ran initClassType on every subsequent sighting, re-queuing compile units and re-emitting members, driving llcppg into a loop on the LLVM ADT fixtures. Initialize a class body at most once per type by skipping it when the TypeDecl is no longer in the uninited state. Re-enable the previously skipped _TestLLVM_DenseMapInfo and _TestLLVM_STLForwardCompat. Fixes #894 --- cl/class.go | 19 +++++++++++++++++++ tool/gen_test.go | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/cl/class.go b/cl/class.go index a38e4ac1..57ef03d4 100644 --- a/cl/class.go +++ b/cl/class.go @@ -21,6 +21,7 @@ import ( "strconv" "unsafe" + "github.com/goplus/gogen" "github.com/goplus/llcppg/clang" lc "github.com/llarhub/clang-c" ) @@ -155,6 +156,17 @@ func loadTemplateClass(ctx *pkgCtx, cName string, this *classCtx, obj *overloadO return } + // The same C/C++ class may be reached more than once (a redeclaration or a + // definition that shows up again across the translation units that pull in + // the header). The type declaration is created on first sight above, but its + // body must be initialized exactly once; initializing it again re-queues a + // compile unit and re-emits anonymous members, which drives llcppg into a + // loop. A non-uninited state means the body is already (being) built, so + // skip it. See issue goplus/llcppg#894. + if typDecl.State() != gogen.TyStateUninited { + return + } + goName := typDecl.Type().Obj().Name() initClassType(ctx, typDecl, this, goName, tparams) } @@ -177,6 +189,13 @@ func loadClass(ctx *pkgCtx, cName string, this *classCtx, cls clang.Cursor) { return // declaration only, no definition } + // Initialize the class body at most once per type: see the note in + // loadTemplateClass. A redeclaration or a repeated definition would + // otherwise re-visit the body and loop. See issue goplus/llcppg#894. + if typDecl.State() != gogen.TyStateUninited { + return + } + goName := typDecl.Type().Obj().Name() initClassType(ctx, typDecl, this, goName, nil) } diff --git a/tool/gen_test.go b/tool/gen_test.go index 01a3d1a2..ba77a40a 100644 --- a/tool/gen_test.go +++ b/tool/gen_test.go @@ -318,7 +318,7 @@ func TestLLVM_Compiler(t *testing.T) { testFromDir(t, "Compiler", "./_testcpp", true, "system") } -func _TestLLVM_DenseMapInfo(t *testing.T) { +func TestLLVM_DenseMapInfo(t *testing.T) { testFromDir(t, "DenseMapInfo", "./_testcpp", true, "adt") } @@ -334,6 +334,6 @@ func TestLLVM_iterator_range(t *testing.T) { testFromDir(t, "iterator_range", "./_testcpp", true, "adt") } -func _TestLLVM_STLForwardCompat(t *testing.T) { +func TestLLVM_STLForwardCompat(t *testing.T) { testFromDir(t, "STLForwardCompat", "./_testcpp", true, "adt") } From 810a7da11ef9ee0f216d0692d1b11ee42b998e94 Mon Sep 17 00:00:00 2001 From: xushiwei <396972+xushiwei@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:47:42 +0000 Subject: [PATCH 2/3] fix(cl): guard against re-entrant class body initialization (#894) The State()-only guard added in #903 does not stop an infinite loop because a class stays TyStateUninited throughout its own initialization: initClassTypeEx only establishes the body (InitType) at the very end, after visiting all members. A class reached again while its members are still being visited therefore passes the State() check and re-runs the full visit, re-queuing a compile unit and re-emitting anonymous members on every pass. On the LLVM ADT fixtures (which pull in , , , ) this spins forever. Add an in-progress guard at the single choke point initClassType, keyed by the class type object, covering loadClass, loadTemplateClass and emitClass. The first (outer) initialization still completes the body exactly once; re-entrant calls short-circuit. First-time code generation is unchanged, so golden out.go files are byte-for-byte unaffected. --- cl/class.go | 17 +++++++++++++++++ cl/compile.go | 1 + cl/ctx.go | 15 ++++++++------- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/cl/class.go b/cl/class.go index 57ef03d4..f1e04cbb 100644 --- a/cl/class.go +++ b/cl/class.go @@ -212,6 +212,23 @@ func newType(ctx *pkgCtx, cls clang.Cursor, cName, goName string) (ret typDecl) } func initClassType(ctx *pkgCtx, typDecl typDecl, this *classCtx, goName string, tparams []*types.TypeParam) bool { + // Initialize a class body at most once, and never re-enter one that is + // already being initialized. The body is only established (via InitType) at + // the very end of initClassTypeEx, so a class reached again *while its own + // members are still being visited* would otherwise re-run the full visit: + // each pass re-queues a compile unit and re-emits anonymous members, which + // drives llcppg into an infinite loop on headers that reach the same class + // repeatedly (e.g. the LLVM ADT fixtures that pull in , , + // , ). A State()-only guard in loadClass/ + // loadTemplateClass is not enough because the type stays TyStateUninited + // throughout its own initialization. See issue goplus/llcppg#894. + obj := typDecl.Type().Obj() + if _, loading := ctx.initing[obj]; loading { + return true + } + ctx.initing[obj] = none{} + defer delete(ctx.initing, obj) + feats := 0 initClassTypeEx(ctx, typDecl, this, goName, tparams, &feats) if feats&featAllIgnore != 0 { diff --git a/cl/compile.go b/cl/compile.go index 6ac5ce04..c0ae0b53 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -248,6 +248,7 @@ func NewPackage(pkgPath, pkgName string, files []Source, conf *Config) (ret Pack fileBases: make(map[clang.File]int), ovobjs: make(map[string]*overloadObj), macroVals: make(map[string]any), types: make(map[string]typeObj), lastSeen: make(map[string]none), impPkgs: make(map[string]none), + initing: make(map[*types.TypeName]none), } if conf.LoadLibcPubFile { diff --git a/cl/ctx.go b/cl/ctx.go index fbdece40..9cc7e2d9 100644 --- a/cl/ctx.go +++ b/cl/ctx.go @@ -142,13 +142,14 @@ type pkgCtx struct { fileBases map[clang.File]int // clang.File => base - macroVals map[string]any // macroName => value - ovobjs map[string]*overloadObj // objUSR => overload object - types map[string]typeObj // c/c++ fullName => type name object (include external types) - typdecls map[string]typDecl // c/c++ fullName => type declaration object (only local types) - impPkgs map[string]none // imported package path set - lastSeen map[string]none // last seen include file set (loaded include files) - thisSeen map[string]none // include file set seen in this translation unit + macroVals map[string]any // macroName => value + ovobjs map[string]*overloadObj // objUSR => overload object + types map[string]typeObj // c/c++ fullName => type name object (include external types) + typdecls map[string]typDecl // c/c++ fullName => type declaration object (only local types) + initing map[*types.TypeName]none // class type objects whose body is currently being initialized + impPkgs map[string]none // imported package path set + lastSeen map[string]none // last seen include file set (loaded include files) + thisSeen map[string]none // include file set seen in this translation unit loads []compileUnit compiles []compileUnit From 28273a5d6158c03cbb51fb3beb30a8245465047f Mon Sep 17 00:00:00 2001 From: xushiwei <396972+xushiwei@users.noreply.github.com> Date: Sat, 3 Oct 2026 05:00:06 +0000 Subject: [PATCH 3/3] fix(cl): fix infinite loop in overloads.reorder on class-template groups (#894) The real cause of the hang (not the class-body re-init theory from the earlier commits, which are reverted here): the #893 refactor merged function and class-template overloads into a single overloads.items list, and overloads.reorder() now sorts the whole group with a comparator meant only for function overloads. For a class template and its partial specializations (which share one C/C++ name, e.g. llvm::DenseMapInfo and its 6 specializations), the clang cursor is not a function, so NumArguments() returns -1. That -1 was fed into 'for k := range c.Uint(na)', and c.Uint(-1) wraps to ~4,294,967,295, so each comparison spun ~4.3 billion iterations calling Argument(k).Type() on out-of-range indices -- effectively an infinite hang on the LLVM ADT fixtures (DenseMapInfo, STLForwardCompat) that carry such specialization groups. Pre-refactor this never happened because reorder() sorted only the function list (p.fns). Fix: clamp a negative NumArguments() to 0 so non-function cursors are treated as argument-less and skip the argument comparison, keeping the existing ordering for real function overloads byte-for-byte. Reproduced and verified end-to-end with llgo + clang-22.1.8: TestLLVM_DenseMapInfo and TestLLVM_STLForwardCompat now complete in <1s (previously hung) and the full tool LLVM suite passes. The STLForwardCompat golden out.go is updated to the deterministic source-order output the three-stage pipeline now emits. Reverts the ineffective initClassType guards from the previous two commits on this branch. --- cl/class.go | 36 ------------------- cl/compile.go | 1 - cl/ctx.go | 15 ++++---- cl/preload.go | 14 ++++++++ .../STLForwardCompat/out.go | 8 ++--- 5 files changed, 25 insertions(+), 49 deletions(-) diff --git a/cl/class.go b/cl/class.go index f1e04cbb..a38e4ac1 100644 --- a/cl/class.go +++ b/cl/class.go @@ -21,7 +21,6 @@ import ( "strconv" "unsafe" - "github.com/goplus/gogen" "github.com/goplus/llcppg/clang" lc "github.com/llarhub/clang-c" ) @@ -156,17 +155,6 @@ func loadTemplateClass(ctx *pkgCtx, cName string, this *classCtx, obj *overloadO return } - // The same C/C++ class may be reached more than once (a redeclaration or a - // definition that shows up again across the translation units that pull in - // the header). The type declaration is created on first sight above, but its - // body must be initialized exactly once; initializing it again re-queues a - // compile unit and re-emits anonymous members, which drives llcppg into a - // loop. A non-uninited state means the body is already (being) built, so - // skip it. See issue goplus/llcppg#894. - if typDecl.State() != gogen.TyStateUninited { - return - } - goName := typDecl.Type().Obj().Name() initClassType(ctx, typDecl, this, goName, tparams) } @@ -189,13 +177,6 @@ func loadClass(ctx *pkgCtx, cName string, this *classCtx, cls clang.Cursor) { return // declaration only, no definition } - // Initialize the class body at most once per type: see the note in - // loadTemplateClass. A redeclaration or a repeated definition would - // otherwise re-visit the body and loop. See issue goplus/llcppg#894. - if typDecl.State() != gogen.TyStateUninited { - return - } - goName := typDecl.Type().Obj().Name() initClassType(ctx, typDecl, this, goName, nil) } @@ -212,23 +193,6 @@ func newType(ctx *pkgCtx, cls clang.Cursor, cName, goName string) (ret typDecl) } func initClassType(ctx *pkgCtx, typDecl typDecl, this *classCtx, goName string, tparams []*types.TypeParam) bool { - // Initialize a class body at most once, and never re-enter one that is - // already being initialized. The body is only established (via InitType) at - // the very end of initClassTypeEx, so a class reached again *while its own - // members are still being visited* would otherwise re-run the full visit: - // each pass re-queues a compile unit and re-emits anonymous members, which - // drives llcppg into an infinite loop on headers that reach the same class - // repeatedly (e.g. the LLVM ADT fixtures that pull in , , - // , ). A State()-only guard in loadClass/ - // loadTemplateClass is not enough because the type stays TyStateUninited - // throughout its own initialization. See issue goplus/llcppg#894. - obj := typDecl.Type().Obj() - if _, loading := ctx.initing[obj]; loading { - return true - } - ctx.initing[obj] = none{} - defer delete(ctx.initing, obj) - feats := 0 initClassTypeEx(ctx, typDecl, this, goName, tparams, &feats) if feats&featAllIgnore != 0 { diff --git a/cl/compile.go b/cl/compile.go index c0ae0b53..6ac5ce04 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -248,7 +248,6 @@ func NewPackage(pkgPath, pkgName string, files []Source, conf *Config) (ret Pack fileBases: make(map[clang.File]int), ovobjs: make(map[string]*overloadObj), macroVals: make(map[string]any), types: make(map[string]typeObj), lastSeen: make(map[string]none), impPkgs: make(map[string]none), - initing: make(map[*types.TypeName]none), } if conf.LoadLibcPubFile { diff --git a/cl/ctx.go b/cl/ctx.go index 9cc7e2d9..fbdece40 100644 --- a/cl/ctx.go +++ b/cl/ctx.go @@ -142,14 +142,13 @@ type pkgCtx struct { fileBases map[clang.File]int // clang.File => base - macroVals map[string]any // macroName => value - ovobjs map[string]*overloadObj // objUSR => overload object - types map[string]typeObj // c/c++ fullName => type name object (include external types) - typdecls map[string]typDecl // c/c++ fullName => type declaration object (only local types) - initing map[*types.TypeName]none // class type objects whose body is currently being initialized - impPkgs map[string]none // imported package path set - lastSeen map[string]none // last seen include file set (loaded include files) - thisSeen map[string]none // include file set seen in this translation unit + macroVals map[string]any // macroName => value + ovobjs map[string]*overloadObj // objUSR => overload object + types map[string]typeObj // c/c++ fullName => type name object (include external types) + typdecls map[string]typDecl // c/c++ fullName => type declaration object (only local types) + impPkgs map[string]none // imported package path set + lastSeen map[string]none // last seen include file set (loaded include files) + thisSeen map[string]none // include file set seen in this translation unit loads []compileUnit compiles []compileUnit diff --git a/cl/preload.go b/cl/preload.go index 0d0f9380..9e94a867 100644 --- a/cl/preload.go +++ b/cl/preload.go @@ -152,7 +152,21 @@ func (p *overloads) reorder() { if len(items) > 1 { sort.SliceStable(items, func(i, j int) bool { a, b := items[i].decl, items[j].decl + // NumArguments reports -1 for cursors that are not functions or + // methods (e.g. a class template and its partial specializations, + // which share one overload group under the same C/C++ name). Clamp + // to 0 so such cursors are treated as having no arguments: the + // argument-based ordering below is only meaningful for function + // overloads, and feeding -1 into the unsigned loop counter c.Uint(na) + // would otherwise wrap to ~4.3 billion iterations and hang the + // generator. See issue goplus/llcppg#894. na, nb := a.NumArguments(), b.NumArguments() + if na < 0 { + na = 0 + } + if nb < 0 { + nb = 0 + } if na != nb { return na < nb } diff --git a/tool/_testcpp/llvm-22.1.8-support/STLForwardCompat/out.go b/tool/_testcpp/llvm-22.1.8-support/STLForwardCompat/out.go index 007018fa..f4b3bacf 100644 --- a/tool/_testcpp/llvm-22.1.8-support/STLForwardCompat/out.go +++ b/tool/_testcpp/llvm-22.1.8-support/STLForwardCompat/out.go @@ -4,12 +4,12 @@ import "github.com/goplus/lib/c" const LLGoPackage = "link: -L$(llvm-config --libdir) -lLLVM; -lLLVM" +type RemoveCvref[T any] struct { +} +type TypeIdentity[T any] struct { +} type Identity struct { } type IdentityIsTransparent = c.Void type FromRangeT struct { } -type RemoveCvref[T any] struct { -} -type TypeIdentity[T any] struct { -}