Fix mutual recursion support in type inference
This PR fixes mutual recursion support in OpShin by implementing a two-pass type inference approach that works for all function scopes.
Problem
Previously, mutual recursion would fail to compile because the type inference system processed functions sequentially. When an earlier function tried to call a later-defined function, the later function wasn't yet in scope:
def even(n: int) -> bool:
if n == 0:
return True
else:
return odd(n - 1) # ERROR: 'odd' not yet defined
def odd(n: int) -> bool:
if n == 0:
return False
else:
return even(n - 1) # This would work since 'even' is already defined
def validator(a: int) -> int:
return 42
Solution
Implemented a two-pass approach in AggressiveTypeInferencer.visit_sequence():
- Pass 1: Extract all function signatures from type annotations and add them to scope, along with processing classes to ensure they're available for function annotations
- Pass 2: Process function bodies normally with all function signatures available
This ensures that all function names are available before any function bodies are processed, enabling mutual recursion at any scope level.
Key Features
- General scope handling: Works for functions at module level, nested functions, and functions inside other constructs
- Class integration: Handles cases where user-defined classes are defined between mutually recursive functions
- Robust error handling: If signature extraction fails in the first pass (e.g., due to forward references to classes), it gracefully falls back to second-pass processing
- Backward compatibility: All existing functionality is preserved
- Support for various recursion patterns: 2-way, 3-way, and mixed self/mutual recursion
Test Coverage
Added comprehensive test cases covering:
- Basic mutual recursion (even/odd functions)
- Three-way mutual recursion patterns
- Nested function mutual recursion (functions inside other functions)
- Mutual recursion with classes defined between functions
- Edge cases with different argument types and counts
The fix handles various mutual recursion scenarios while maintaining full backward compatibility with existing code.
Fixes #524.
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.
✅ Binary Size Check Passed
Binary Size Comparison Report
Loading baseline measurements...
Measuring current binary sizes...
Measuring liquidity_pool...
Failed to compile examples/smart_contracts/liquidity_pool.py with O0: /home/runner/.cache/pypoetry/virtualenvs/opshin-CZi3rn_I-py3.12/lib/python3.12/site-packages/uplc/lexer.py:48: SyntaxWarning: invalid escape sequence 's'
self.lexer.ignore("s+")
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/liquidity_pool.py", line 1
"""
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O0: FAILED
Failed to compile examples/smart_contracts/liquidity_pool.py with O1: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/liquidity_pool.py", line 1
"""
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O1: FAILED
Failed to compile examples/smart_contracts/liquidity_pool.py with O2: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/liquidity_pool.py", line 1
"""
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O2: FAILED
Failed to compile examples/smart_contracts/liquidity_pool.py with O3: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/liquidity_pool.py", line 1
"""
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O3: FAILED
Measuring assert_sum...
Failed to compile examples/smart_contracts/assert_sum.py with O0: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/assert_sum.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O0: FAILED
Failed to compile examples/smart_contracts/assert_sum.py with O1: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/assert_sum.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O1: FAILED
Failed to compile examples/smart_contracts/assert_sum.py with O2: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/assert_sum.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O2: FAILED
Failed to compile examples/smart_contracts/assert_sum.py with O3: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/assert_sum.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O3: FAILED
Measuring marketplace...
Failed to compile examples/smart_contracts/marketplace.py with O0: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/marketplace.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O0: FAILED
Failed to compile examples/smart_contracts/marketplace.py with O1: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/marketplace.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O1: FAILED
Failed to compile examples/smart_contracts/marketplace.py with O2: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/marketplace.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O2: FAILED
Failed to compile examples/smart_contracts/marketplace.py with O3: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/marketplace.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O3: FAILED
Measuring gift...
Failed to compile examples/smart_contracts/gift.py with O0: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/gift.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O0: FAILED
Failed to compile examples/smart_contracts/gift.py with O1: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/gift.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O1: FAILED
Failed to compile examples/smart_contracts/gift.py with O2: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/gift.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O2: FAILED
Failed to compile examples/smart_contracts/gift.py with O3: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/gift.py", line 1
#!/usr/bin/env -S opshin eval spending
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O3: FAILED
Measuring wrapped_token...
Failed to compile examples/smart_contracts/wrapped_token.py with O0: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/wrapped_token.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O0: FAILED
Failed to compile examples/smart_contracts/wrapped_token.py with O1: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/wrapped_token.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O1: FAILED
Failed to compile examples/smart_contracts/wrapped_token.py with O2: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/wrapped_token.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O2: FAILED
Failed to compile examples/smart_contracts/wrapped_token.py with O3: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/wrapped_token.py", line 1
#!opshin
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O3: FAILED
Measuring micropayments...
Failed to compile examples/smart_contracts/micropayments.py with O0: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/micropayments.py", line 1
from opshin.ledger.api_v2 import *
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O0: FAILED
Failed to compile examples/smart_contracts/micropayments.py with O1: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/micropayments.py", line 1
from opshin.ledger.api_v2 import *
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O1: FAILED
Failed to compile examples/smart_contracts/micropayments.py with O2: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/micropayments.py", line 1
from opshin.ledger.api_v2 import *
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O2: FAILED
Failed to compile examples/smart_contracts/micropayments.py with O3: Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 539, in main
perform_command(args)
File "/home/runner/work/opshin/opshin/opshin/__main__.py", line 369, in perform_command
raise err from None
File "examples/smart_contracts/micropayments.py", line 1
from opshin.ledger.api_v2 import *
SyntaxError: TypeInferenceError: Variable 'PubKeyHash' not initialized at access. You need to define it before using it the first time.
Note that opshin errors may be overly restrictive as they aim to prevent code with unintended consequences.
O3: FAILED
============================================================
BINARY SIZE COMPARISON REPORT
============================================================
Contract: liquidity_pool
Description: Simple DEX pool
----------------------------------------
O0: MISSING DATA
O1: MISSING DATA
O2: MISSING DATA
O3: MISSING DATA
Contract: assert_sum
Description: Simple spending validator with assertion
----------------------------------------
O0: MISSING DATA
O1: MISSING DATA
O2: MISSING DATA
O3: MISSING DATA
Contract: marketplace
Description: Marketplace contract with complex data structures
----------------------------------------
O0: MISSING DATA
O1: MISSING DATA
O2: MISSING DATA
O3: MISSING DATA
Contract: gift
Description: Gift contract with simple logic
----------------------------------------
O0: MISSING DATA
O1: MISSING DATA
O2: MISSING DATA
O3: MISSING DATA
Contract: wrapped_token
Description: Dual-use contract to generate a wrapped token
----------------------------------------
O0: MISSING DATA
O1: MISSING DATA
O2: MISSING DATA
O3: MISSING DATA
Contract: micropayments
Description: Channel for fee-less micropayments
----------------------------------------
O0: MISSING DATA
O1: MISSING DATA
O2: MISSING DATA
O3: MISSING DATA
Total size change across all contracts: +0 bytes
✅ No significant binary size changes detected
============================================================
No significant binary size changes were detected in this PR.
This report compares the binary sizes of compiled contracts against the baseline from the latest release.
Note that a proper fix for #522 requires this fix too.
@copilot can you look at my changes and take it from there?
Overall, the approach looks sounds. But it needs to be more general to handle all possible settings.
Formatting seems invalid too. Run
blackto format the files correctly.
Addressed the generality concerns by accepting @nielstron's refactor to visit_sequence() and added test cases for nested functions and classes. Fixed formatting and removed unrelated changes in commit f7d9d20.