Skip to content

fix(nitrogen): handle cyclic struct references#1074

Open
puckey wants to merge 9 commits into
mrousavy:mainfrom
puckey:fix/self-referential-structs
Open

fix(nitrogen): handle cyclic struct references#1074
puckey wants to merge 9 commits into
mrousavy:mainfrom
puckey:fix/self-referential-structs

Conversation

@puckey

@puckey puckey commented Dec 2, 2025

Copy link
Copy Markdown
Contributor

Summary

  • Fixes stack overflow during code generation for structs with cyclic references
  • Throws a clear error for recursive struct types (struct containing itself directly or indirectly through other structs)
  • Adds support for structs with callbacks that reference the same struct type
  • Detects cyclic dependencies between struct and function types at code generation time
  • Generates separate .hpp/.cpp files for types with cyclic dependencies to avoid circular includes (Android)
  • Uses forward declarations in headers, with full type definitions only in implementation files
  • Adds visited set parameter to prevent infinite recursion in type walkers (affects all platforms)
  • Documents cyclic struct reference behavior in the docs

Test plan

  • Added SelfReferentialStruct test case with callback that returns the same struct type
  • Added TreeNode and TreeNodeMap test cases for tree-like data structures
  • Verified Android build succeeds
  • Verified iOS build succeeds
  • On-device tests pass for self-referential struct operations (Android + iOS)

Note

This PR was implemented with Claude Code. I'm not deeply familiar with all the C++/JNI internals here, but did my best to follow along and verify the changes make sense. Also tested it in library code I am working on.

Previously, struct types that reference themselves (e.g., `Node { node?: Node }`)
or have indirect cycles (e.g., `Foo { bar: Bar }`, `Bar { foo: Foo }`) would cause
"Maximum call stack size exceeded" during code generation.

This fix introduces:
- Cycle detection in createType.ts using a processingTypes set
- Lazy initialization in StructType to break cyclic dependencies
- visited Set parameter in getRequiredImports/getExtraFiles to prevent infinite recursion

Includes test cases for both direct self-reference and indirect cycles.
Adds a struct with a callback that references the same struct type.

Currently fails with "Maximum call stack size exceeded" due to
infinite recursion in nitrogen's type walker.
Add cycle detection for struct types that directly or indirectly reference
themselves. Allow self-references through reference types (arrays, maps,
callbacks, promises) since these use heap allocation.

Removes Cyclic.nitro.ts test file and adds proper test cases (TreeNode,
TreeNodeMap, SelfReferentialStruct) to TestObject.nitro.ts.
Explain that direct/indirect cyclic struct references are not supported,
but self-references through arrays, maps, or functions are allowed since
these are reference types using heap allocation.
Generated files for TreeNode, TreeNodeMap, and SelfReferentialStruct
test cases demonstrating allowed self-reference patterns.
When a struct contains a function that references the same struct
(e.g., `transform?: (config: MyStruct) => Promise<MyStruct>`), the
generated JNI headers would have circular includes.

Generate valid C++ by using forward declarations, deferring includes
until after class declarations, and defining methods out-of-line.

Extract shared cyclic detection logic into detectCyclicDependencies.ts.
Add runtime tests for bounceSelfReferentialStruct, bounceTreeNode,
and bounceTreeNodeMap to verify the cyclic struct handling works
correctly on device.
Properly separate declarations from implementations for cyclic struct/function
dependencies by generating separate .hpp and .cpp files instead of inline
deferred definitions. Also consolidate import partitioning logic into a single
`partitionAndTransformImports` helper function.
@vercel

vercel Bot commented Dec 2, 2025

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Margelo Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Dec 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
nitro-docs Ready Ready Preview, Comment Dec 18, 2025 9:45am

@mrousavy

Copy link
Copy Markdown
Owner

Hey - I think overall the change is good - it's making DX better. I really appreciate you taking the time to work on this. ❤️

I am just not a big fan of how complex this is to implement - not only does it touch a lot of the codebase, but also it passes the Sets down very far where which many functions write to - it's a bit too complex for this small quality of life change imo.

We can leave this PR open for future reference, but I was hoping for a much smaller change...

puckey added a commit to puckey/nitro that referenced this pull request Mar 2, 2026
Structs that reference themselves through reference types (arrays,
callbacks, promises, records) previously caused a stack overflow
during code generation.

Break the cycle by tracking structs being initialized and using
lazy property resolution in StructType. When a struct is encountered
while it's already being processed, the cached instance is returned
instead of recursing infinitely.

Follows up on PR mrousavy#1074 with a simpler approach per mrousavy's
feedback about reducing complexity.

Fixes mrousavy#1070
@puckey

puckey commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I've reworked this into two smaller, focused PRs:

Happy to close this one in favour of those.

puckey added a commit to puckey/nitro that referenced this pull request Mar 2, 2026
Structs that reference themselves through reference types (arrays,
callbacks, promises, records) previously caused a stack overflow
during code generation.

Break the cycle by tracking structs being initialized and using
lazy property resolution in StructType. When a struct is encountered
while it's already being processed, the cached instance is returned
instead of recursing infinitely.

Follows up on PR mrousavy#1074 with a simpler approach per mrousavy's
feedback about reducing complexity.

Fixes mrousavy#1070
puckey added a commit to puckey/nitro that referenced this pull request Mar 2, 2026
Structs that reference themselves through reference types (arrays,
callbacks, promises, records) previously caused a stack overflow
during code generation.

Break the cycle by tracking structs being initialized and using
lazy property resolution in StructType. When a struct is encountered
while it's already being processed, the cached instance is returned
instead of recursing infinitely.

Follows up on PR mrousavy#1074 with a simpler approach per mrousavy's
feedback about reducing complexity.

Fixes mrousavy#1070
puckey added a commit to puckey/nitro that referenced this pull request Mar 4, 2026
Structs that reference themselves through reference types (arrays,
callbacks, promises, records) previously caused a stack overflow
during code generation.

Break the cycle by tracking structs being initialized and using
lazy property resolution in StructType. When a struct is encountered
while it's already being processed, the cached instance is returned
instead of recursing infinitely.

Follows up on PR mrousavy#1074 with a simpler approach per mrousavy's
feedback about reducing complexity.

Fixes mrousavy#1070
puckey added a commit to puckey/nitro that referenced this pull request Jun 12, 2026
Structs that reference themselves through reference types (arrays,
callbacks, promises, records) previously caused a stack overflow
during code generation.

Break the cycle by tracking structs being initialized and using
lazy property resolution in StructType. When a struct is encountered
while it's already being processed, the cached instance is returned
instead of recursing infinitely.

Follows up on PR mrousavy#1074 with a simpler approach per mrousavy's
feedback about reducing complexity.

Fixes mrousavy#1070
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.

2 participants