Compilation Error: Complex Classes Exceed Branch Quota During Discovery
First, let me just say thank you for creating Ziggy-Pydust, it's a fantastic package! The overall summary of this issue is "many of my Python classes work so well that the ones which don't seem to be able to be implemented in Ziggy-Pydust make me sad."
Environment
- Pydust Version: 0.26.0
- Zig Version: 0.15.2
- Platform: Windows (x86_64)
- Python Version: 3.11.9
- Build Type: Both Debug and ReleaseFast
Summary
When implementing moderately complex Python classes (10+ fields, 4+ methods) using pydust, compilation fails with "evaluation exceeded 10000 backwards branches" during the compile-time discovery phase. This occurs even when following best practices (struct-based parameters, simple init signatures).
The issue appears to be in pydust's discovery.zig module, which recursively analyzes all type information at compile time, causing combinatorial explosion with complex class definitions.
Expected Behavior
Classes with 10-15 fields and 4-5 methods should compile successfully, as they represent common real-world use cases for Python extension modules.
Actual Behavior
Compilation fails during the discovery phase with:
vendor\pydust\pydust\src\discovery.zig:39:20: error: evaluation exceeded 10000 backwards branches
inline for (info.fields) |f| {
~~~~~~~^~~
vendor\pydust\pydust\src\discovery.zig:39:20: note: use @setEvalBranchQuota() to raise the branch limit from 10000
This error occurs even when:
- Using struct-based parameters for
__init__ (only 2 total parameters: self + single struct)
- Keeping individual methods simple
- Following the patterns used in simpler working examples
Minimal Reproduction
Classes That Work ✅
Simple classes with 2-7 fields and 2-3 methods compile successfully:
pub const SimpleHeader = py.class(struct {
const Self = @This();
// 7 fields - typical message header
field_a: py.PyBytes,
field_b: u8,
field_c: u8,
field_d: u32,
field_e: u16,
field_f: u16,
field_g: u32,
pub fn __init__(self: *Self, args: struct {
field_a: py.PyBytes,
field_b: u8,
field_c: u8,
field_d: u32,
field_e: u16,
field_f: u16,
field_g: u32,
}) !void {
self.field_a = args.field_a;
self.field_b = args.field_b;
self.field_c = args.field_c;
self.field_d = args.field_d;
self.field_e = args.field_e;
self.field_f = args.field_f;
self.field_g = args.field_g;
}
pub fn from_bytes(args: struct { raw_bytes: py.PyBytes }) !*Self {
const bytes = try args.raw_bytes.asSlice();
// Parse bytes into fields...
return try py.init(root, Self, .{
.field_a = try py.PyBytes.create(bytes[0..2]),
.field_b = bytes[2],
.field_c = bytes[3],
// ... etc
});
}
pub fn to_bytes(self: *Self) !py.PyBytes {
var buffer: [16]u8 = undefined;
// Serialize fields to buffer...
return try py.PyBytes.create(&buffer);
}
});
Classes That Fail ❌
Classes with 10+ fields and 4+ methods fail compilation:
pub const ComplexDataBlock = py.class(struct {
const Self = @This();
// 10 fields - typical data structure with multiple related values
id: u16,
flag_x: bool,
value_x: i32,
flag_y: bool,
value_y: i32,
flag_z: bool,
value_z: i32,
rate_x: i32,
rate_y: i32,
rate_z: i32,
// Standard __init__ with struct parameter (following best practices)
pub fn __init__(self: *Self, args: struct {
id: u16,
flag_x: bool,
value_x: i32,
flag_y: bool,
value_y: i32,
flag_z: bool,
value_z: i32,
rate_x: i32,
rate_y: i32,
rate_z: i32,
}) !void {
self.id = args.id;
self.flag_x = args.flag_x;
self.value_x = args.value_x;
self.flag_y = args.flag_y;
self.value_y = args.value_y;
self.flag_z = args.flag_z;
self.value_z = args.value_z;
self.rate_x = args.rate_x;
self.rate_y = args.rate_y;
self.rate_z = args.rate_z;
}
// Factory method following py.init() pattern
pub fn from_bytes(args: struct {
raw_bytes: py.PyBytes,
config: py.PyString
}) !*Self {
const bytes_slice = try args.raw_bytes.asSlice();
const config_slice = try args.config.asSlice();
// Parse variable-length binary data based on config
// Extract 10 fields using bit manipulation (~20 lines)
return try py.init(root, Self, .{
.id = id,
.flag_x = flag_x,
.value_x = value_x,
// ... 7 more field assignments
});
}
// Serialization method
pub fn to_bytes(self: *Self, args: struct {
config: py.PyString
}) !py.PyBytes {
// Serialize 10 fields to variable-length format
// Based on config parameter (~30 lines)
return try py.PyBytes.create(&buffer);
}
// String representation
pub fn __repr__(self: *const Self) !py.PyString {
// Format string with all 10 fields (~5 lines)
}
});
Error when exporting this class:
vendor\pydust\pydust\src\discovery.zig:39:20: error: evaluation exceeded 10000 backwards branches
inline for (info.fields) |f| {
~~~~~~~^~~
Analysis
Root Cause
The issue is in pydust's compile-time discovery system, which recursively analyzes:
- All struct fields
- All methods and their parameters
- All nested types within parameters (e.g., the struct in
args: struct { ... })
- All declarations within the class
For each field/method, the analysis:
- Calls
countDefinitions() recursively
- Calls
getIdentifiers() for each declaration
- Iterates through all nested type information
With 10 fields + 4 methods + nested struct parameters:
- Each method's struct parameter adds ~10 more "fields" to analyze
- Total declarations: ~50-70 items
- Recursive depth: 3-4 levels
- Approximate branches: 10,000+ (exceeds quota)
Why Simple Classes Work
Classes with 3-5 fields and 2-3 methods stay under the branch quota because:
- Fewer total declarations (~15-20)
- Shallower recursion depth
- Smaller parameter structs
Attempted Workarounds
We tried several approaches, all unsuccessful:
- ✗ Struct-based parameters: Still exceeds quota (this is already best practice)
- ✗ Splitting into multiple files: Doesn't help - analysis happens per-class
- ✗ Simplifying methods: Even with minimal logic, declaration count is the issue
- ✗ Removing helper functions: Marginal improvement, still exceeds quota
- ✗ Using @setEvalBranchQuota(): Not available to user code calling pydust
Impact
This limitation significantly restricts pydust's usefulness for real-world applications:
What We Can't Implement
- Data structures with many fields: Common in binary protocols, network packets, file formats
- Classes with rich APIs: Multiple conversion methods (from_bytes, to_bytes, from_json, etc.)
- Domain objects: Business logic objects often have 10-20 fields
What We Can Implement
- Simple message headers: 3-7 fields (works great, 30-150x speedup achieved)
- Wrapper classes: Thin wrappers around single values
- Stateless utility functions: Functions without class context
Comparison with Working Classes
We successfully implemented 8 classes that work perfectly:
| Class |
Fields |
Methods |
Status |
| MessageHeaderA |
7 |
3 |
✅ Works (30-66x speedup) |
| MessageHeaderB |
4 |
3 |
✅ Works (32-52x speedup) |
| MessageHeaderC |
6 |
3 |
✅ Works (32-68x speedup) |
| StatusBlock |
3 |
3 |
✅ Works (21-34x speedup) |
| DescriptorBlock |
7 |
3 |
✅ Works (43-85x speedup) |
| TimestampBlock |
3 |
3 |
✅ Works (18-34x speedup) |
| ComplexDataBlock |
10 |
4 |
❌ Fails (branch quota exceeded) |
| ConfigBlock |
2 |
4 |
❌ Fails (branch quota exceeded) |
Note: Even ConfigBlock with only 2 fields fails when it has 4 methods, suggesting the method count contributes significantly to branch usage.
Proposed Solutions
Option 1: Increase Branch Quota in Discovery Module
Add @setEvalBranchQuota() in discovery.zig:
// In discovery.zig, before expensive loops
pub fn getAllIdentifiers(definition: type) []const Identifier {
@setEvalBranchQuota(50000); // Increase from default 10000
// ... existing code ...
}
Pros: Simple fix, allows more complex classes
Cons: May hide deeper performance issues
Option 2: Optimize Discovery Algorithm
Reduce redundant analysis:
- Cache type information that's analyzed multiple times
- Skip analyzing standard library types
- Implement iterative instead of recursive approach where possible
Pros: More sustainable, better performance overall
Cons: Requires significant refactoring
Option 3: Lazy Discovery
Defer some type analysis until actually needed:
- Only analyze methods that are actually called from Python
- Use lazy evaluation for nested types
Pros: Scales better with complex classes
Cons: More complex implementation, potential runtime overhead
Option 4: Exclude Internal Details
Allow marking fields/methods to skip during discovery:
// Hypothetical attribute to skip discovery
pub fn internalHelper() void comptime_exclude {}
Pros: Gives users control over complexity
Cons: Requires new language features or conventions
Request
Could you please:
- Increase the branch quota in discovery.zig as a short-term fix (Option 1)
- Consider optimizing the discovery algorithm for long-term scalability (Option 2)
- Document the complexity limits so users understand what's feasible
Workaround
For now, we're keeping complex classes in Python and only using pydust for simple message headers (which still provides excellent 30-150x speedup). However, this limits pydust's applicability to a subset of potential use cases.
Additional Context
- Use case: Binary protocol parser for network communications
- Successfully built 8 simpler classes with great results (30-150x speedup)
- Blocked on implementing 7 more complex data structures
- All code follows pydust best practices (struct-based parameters, py.init pattern)
- Testing with Zig 0.15.2 and Pydust 0.26.0
Related Issues
- Similar to any existing issues about compile-time evaluation limits
- Related to general compile-time evaluation performance
This Issue was primarily written by Claude Sonnet 4.5, and reviewed and edited by myself.
Compilation Error: Complex Classes Exceed Branch Quota During Discovery
First, let me just say thank you for creating Ziggy-Pydust, it's a fantastic package! The overall summary of this issue is "many of my Python classes work so well that the ones which don't seem to be able to be implemented in Ziggy-Pydust make me sad."
Environment
Summary
When implementing moderately complex Python classes (10+ fields, 4+ methods) using pydust, compilation fails with "evaluation exceeded 10000 backwards branches" during the compile-time discovery phase. This occurs even when following best practices (struct-based parameters, simple init signatures).
The issue appears to be in pydust's
discovery.zigmodule, which recursively analyzes all type information at compile time, causing combinatorial explosion with complex class definitions.Expected Behavior
Classes with 10-15 fields and 4-5 methods should compile successfully, as they represent common real-world use cases for Python extension modules.
Actual Behavior
Compilation fails during the discovery phase with:
This error occurs even when:
__init__(only 2 total parameters:self+ single struct)Minimal Reproduction
Classes That Work ✅
Simple classes with 2-7 fields and 2-3 methods compile successfully:
Classes That Fail ❌
Classes with 10+ fields and 4+ methods fail compilation:
Error when exporting this class:
Analysis
Root Cause
The issue is in pydust's compile-time discovery system, which recursively analyzes:
args: struct { ... })For each field/method, the analysis:
countDefinitions()recursivelygetIdentifiers()for each declarationWith 10 fields + 4 methods + nested struct parameters:
Why Simple Classes Work
Classes with 3-5 fields and 2-3 methods stay under the branch quota because:
Attempted Workarounds
We tried several approaches, all unsuccessful:
Impact
This limitation significantly restricts pydust's usefulness for real-world applications:
What We Can't Implement
What We Can Implement
Comparison with Working Classes
We successfully implemented 8 classes that work perfectly:
Note: Even ConfigBlock with only 2 fields fails when it has 4 methods, suggesting the method count contributes significantly to branch usage.
Proposed Solutions
Option 1: Increase Branch Quota in Discovery Module
Add
@setEvalBranchQuota()indiscovery.zig:Pros: Simple fix, allows more complex classes
Cons: May hide deeper performance issues
Option 2: Optimize Discovery Algorithm
Reduce redundant analysis:
Pros: More sustainable, better performance overall
Cons: Requires significant refactoring
Option 3: Lazy Discovery
Defer some type analysis until actually needed:
Pros: Scales better with complex classes
Cons: More complex implementation, potential runtime overhead
Option 4: Exclude Internal Details
Allow marking fields/methods to skip during discovery:
Pros: Gives users control over complexity
Cons: Requires new language features or conventions
Request
Could you please:
Workaround
For now, we're keeping complex classes in Python and only using pydust for simple message headers (which still provides excellent 30-150x speedup). However, this limits pydust's applicability to a subset of potential use cases.
Additional Context
Related Issues
This Issue was primarily written by Claude Sonnet 4.5, and reviewed and edited by myself.