Summary
The Python bindings export the same logical external-function value differently depending on whether the callable's __name__ was already interned by the compiled Monty source.
This means a tiny source change like mentioning foo earlier in the program changes the final Python result for an unrelated input binding bar.
Repro
import pydantic_monty
def run(code: str, *, inputs=None):
input_names = list(inputs.keys()) if inputs else None
return pydantic_monty.Monty(code, inputs=input_names).run(inputs=inputs)
def foo():
pass
print(run("bar; bar", inputs={"foo": foo, "bar": foo}))
print(run("foo; bar", inputs={"foo": foo, "bar": foo}))
Actual behavior
foo
<function 'foo' external>
The final value in both cases is logically the same external function value, but it exports differently.
I also checked a few nearby cases:
run("bar", inputs={"foo": foo, "bar": foo}) # 'foo'
run("foo", inputs={"foo": foo, "bar": foo}) # "<function 'foo' external>"
run("bar, bar", inputs={"foo": foo, "bar": foo}) # ('foo', 'foo')
run("foo, bar", inputs={"foo": foo, "bar": foo}) # ("<function 'foo' external>", "<function 'foo' external>")
If I rename the Python callable:
foo.__name__ = "baz"
print(run("bar; bar", inputs={"foo": foo, "bar": foo}))
print(run("foo; bar", inputs={"foo": foo, "bar": foo}))
both results become bare 'baz', which suggests the trigger is the callable's own name being interned, not the input binding name.
Expected behavior
The same logical external-function value should export consistently regardless of whether its name happened to appear in the source code.
I would expect one of these:
- always export the same placeholder string/repr-like value
- always export a dedicated Python-side wrapper/object
- reject raw external-function final outputs consistently
But it should not depend on whether the function name was interned during compilation.
Likely cause
Python callables passed through inputs= are converted to MontyObject::Function { name, .. } using callable.__name__.
When that enters the VM, MontyObject::Function is lowered in two different ways:
- if
vm.interns.get_string_id_by_name(&name) succeeds, it becomes inline Value::ExtFunction(StringId)
- otherwise it becomes heap
HeapData::ExtFunction(String)
On the way back out, those two runtime representations are handled differently:
HeapData::ExtFunction(String) is converted back to MontyObject::Function { name, .. }, which the Python bindings export as the bare name string
Value::ExtFunction(StringId) does not have a dedicated MontyObject arm and falls through to repr_or_error(...), which eventually exports as "<function 'foo' external>"
Relevant code:
crates/monty-python/src/convert.rs
crates/monty/src/object.rs
This seems separate from #339. #339 is about dispatch using callable.__name__; this issue is about inconsistent final output/export of the resulting external-function value depending on name interning.
Summary
The Python bindings export the same logical external-function value differently depending on whether the callable's
__name__was already interned by the compiled Monty source.This means a tiny source change like mentioning
fooearlier in the program changes the final Python result for an unrelated input bindingbar.Repro
Actual behavior
The final value in both cases is logically the same external function value, but it exports differently.
I also checked a few nearby cases:
If I rename the Python callable:
both results become bare
'baz', which suggests the trigger is the callable's own name being interned, not the input binding name.Expected behavior
The same logical external-function value should export consistently regardless of whether its name happened to appear in the source code.
I would expect one of these:
But it should not depend on whether the function name was interned during compilation.
Likely cause
Python callables passed through
inputs=are converted toMontyObject::Function { name, .. }usingcallable.__name__.When that enters the VM,
MontyObject::Functionis lowered in two different ways:vm.interns.get_string_id_by_name(&name)succeeds, it becomes inlineValue::ExtFunction(StringId)HeapData::ExtFunction(String)On the way back out, those two runtime representations are handled differently:
HeapData::ExtFunction(String)is converted back toMontyObject::Function { name, .. }, which the Python bindings export as the bare name stringValue::ExtFunction(StringId)does not have a dedicatedMontyObjectarm and falls through torepr_or_error(...), which eventually exports as"<function 'foo' external>"Relevant code:
crates/monty-python/src/convert.rscrates/monty/src/object.rsThis seems separate from #339. #339 is about dispatch using
callable.__name__; this issue is about inconsistent final output/export of the resulting external-function value depending on name interning.