Skip to content

[cpyrt] Deduce a template instantiation per annotated Python callable - #154

Open
guitargeek wants to merge 1 commit into
compiler-research:mainfrom
guitargeek:fix-annotated-callable-instantiation
Open

guitargeek wants to merge 1 commit into
compiler-research:mainfrom
guitargeek:fix-annotated-callable-instantiation

Conversation

@guitargeek

@guitargeek guitargeek commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

An implicit template call with an annotated Python function deduced the template argument from the annotations only the first time. A later call with a differently annotated callable reused that first instantiation, silently wrapping the new callable with the old signature (e.g. truncating a float argument to int). The signature hash behind the memoized dispatch only mixes in the Python type, which all functions share, and the function pointer converter wrapped any callable regardless of its annotations.

Mix the annotation values of Python functions and bound methods into HashSignature. Give the function pointer converter the type it converts to and have it, in the strict round, accept an annotated callable only if the annotations declare that function type: the declared signature is resolved to a type once per spelling and compared through CppInterOp, so the spelling need not match the parameter's. A mismatch defers the callable to the implicit round, as for other implicit conversions. The std::function converter skips the check, as a std::function takes any compatible callable. A None annotation now reads as void.

Fixes #151

Comment thread src/cpyrt/Converters.cxx Outdated
return name;
}

static bool AnnotationsMatch(PyObject* pyobject, const std::string& rettype,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid string comparisons and hook to the underlying clang/CppInterOp types. That will give us some level of memoization stability where we can cache the actual function prototype from the AST.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The converter now holds the parameter's AST type and the annotated signature is resolved to a type (cached per spelling), compared with IsSimilarFnTypes. Added a test with a typedef annotation that the string comparison got wrong.

@Vipul-Cariappa Vipul-Cariappa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look at commit 425cada in #150 for my approach to fix this. It is a more concise fix than this.

The underlying issue is that the hash for the cache is the same for all different Python functions. This is fixed by taking the annotation into account while computing the hash; if the annotation is not available, then we use the Python function object itself as the hash.

Comment thread test/test_templates.py
Comment on lines +1503 to +1504
# an unannotated callable is wrapped by the known instantiation
assert ns.call(D) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not be supported with the new overload resolution. We do not have type information on either the Python or C++ side.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads up! That line records the behavior on main today. It can be dropped together with the new overload resolution if that no longer supports it.

An implicit template call with an annotated Python function deduced the
template argument from the annotations only the first time. A later call
with a differently annotated callable reused that first instantiation,
silently wrapping the new callable with the old signature (e.g.
truncating a float argument to int). The signature hash behind the
memoized dispatch only mixes in the Python type, which all functions
share, and the function pointer converter wrapped any callable
regardless of its annotations.

Mix the annotation values of Python functions and bound methods into
HashSignature. Give the function pointer converter the type it converts
to and have it, in the strict round, accept an annotated callable only
if the annotations declare that function type: the declared signature is
resolved to a type once per spelling and compared through CppInterOp,
so the spelling need not match the parameter's. A mismatch defers the
callable to the implicit round, as for other implicit conversions. The
std::function converter skips the check, as a std::function takes any
compatible callable. A None annotation now reads as void.

Fixes compiler-research#151
@guitargeek
guitargeek force-pushed the fix-annotated-callable-instantiation branch from 09d1368 to 34f16c5 Compare October 14, 2026 11:19
@guitargeek

Copy link
Copy Markdown
Collaborator Author

Please look at commit 425cada in #150 for my approach to fix this. It is a more concise fix than this.

The underlying issue is that the hash for the cache is the same for all different Python functions. This is fixed by taking the annotation into account while computing the hash; if the annotation is not available, then we use the Python function object itself as the hash.

This branch already hashes the annotations into HashSignature. On main the converter check is still needed, because the pass over known instantiations precedes deduction and would otherwise wrap a differently annotated callable with the first instantiation. We should revisit once #150 lands, but I'd propose we don't let this PR wait for the new overload resolution, since it is addressing a reported issue.

@vgvassilev

Copy link
Copy Markdown
Contributor

What happens on a hash collision? Do we call the wrong thing?

This branch has not been deployed

No deployments
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.

Implicit template instantiation from an annotated Python callable reuses the signature of an earlier callable

3 participants