Summary
Monty collapses distinct function objects together based on their compiled function ID / representation.
That shows up in two nearby ways:
- nested functions with no defaults compare identical and equal across separate calls
- nested functions with defaults compare equal across separate calls
CPython treats all of these as distinct function objects.
Repro
import pydantic_monty
def run(code: str):
return pydantic_monty.Monty(code).run()
assert run(
"""
def outer():
def inner():
return 1
return inner
a = outer()
b = outer()
(a is b, a == b)
"""
) == (True, True)
assert run(
"""
def outer():
def inner(y=1):
return y
return inner
a = outer()
b = outer()
(a is b, a == b)
"""
) == (False, True)
Actual behavior
Monty returns:
(True, True)
(False, True)
Expected behavior
CPython returns:
(False, False)
(False, False)
These are distinct runtime function objects, so they should not compare equal or identical just because they came from the same compiled function body.
Why this looks representation-driven
In the VM:
- functions with no defaults are emitted as inline
Value::DefFunction(func_id)
- functions with defaults are emitted as heap
HeapData::FunctionDefaults { func_id, defaults }
Relevant code:
crates/monty/src/bytecode/vm/mod.rs
Equality then appears to collapse objects by func_id:
Value::DefFunction(f1) == Value::DefFunction(f2) compares f1 == f2
HeapData::FunctionDefaults(a) == HeapData::FunctionDefaults(b) compares only func_id
Relevant code:
crates/monty/src/value.rs
crates/monty/src/heap_data.rs
This also affects hash/container behavior. For example, two distinct no-default nested function objects collapse to a single set element and work as the same dict key.
Summary
Monty collapses distinct function objects together based on their compiled function ID / representation.
That shows up in two nearby ways:
CPython treats all of these as distinct function objects.
Repro
Actual behavior
Monty returns:
Expected behavior
CPython returns:
These are distinct runtime function objects, so they should not compare equal or identical just because they came from the same compiled function body.
Why this looks representation-driven
In the VM:
Value::DefFunction(func_id)HeapData::FunctionDefaults { func_id, defaults }Relevant code:
crates/monty/src/bytecode/vm/mod.rsEquality then appears to collapse objects by
func_id:Value::DefFunction(f1) == Value::DefFunction(f2)comparesf1 == f2HeapData::FunctionDefaults(a) == HeapData::FunctionDefaults(b)compares onlyfunc_idRelevant code:
crates/monty/src/value.rscrates/monty/src/heap_data.rsThis also affects hash/container behavior. For example, two distinct no-default nested function objects collapse to a single set element and work as the same dict key.