From a813b7f801d8f40d41ec83c85c31d89449b97f59 Mon Sep 17 00:00:00 2001 From: Emery Conrad Date: Tue, 25 Aug 2026 12:51:26 -0500 Subject: [PATCH 1/2] [interop] Pass a negative literal as a template argument value is_integral rejects a leading minus, so the comma-split fallback in AppendTypesSlow reads a negative literal as a type name. Accept the sign. Co-developed-with-the-help-of: Claude Code (Opus 5, human in the loop) --- src/interop/interop_wrapper.cxx | 4 +++- test/test_templates.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index 1b5b0eb..6eb1eb0 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -74,7 +74,9 @@ static inline bool is_integral(std::string& s) { s = "1"; return true; } - return !s.empty() && std::find_if(s.begin(), s.end(), [](unsigned char c) { + // allow a leading minus (negative literal) + auto begin = s.begin() + (s.size() > 1 && s[0] == '-' ? 1 : 0); + return !s.empty() && std::find_if(begin, s.end(), [](unsigned char c) { return !std::isdigit(c); }) == s.end(); } diff --git a/test/test_templates.py b/test/test_templates.py index 127ec7f..a27f342 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -84,6 +84,10 @@ def test02_non_type_template_args(self): assert cppjit.gbl.nt_templ_args[1]() == 1 assert cppjit.gbl.nt_templ_args[256]() == 256 + # negative literals are values, not types + assert cppjit.gbl.nt_templ_args[-1]() == -1 + assert cppjit.gbl.nt_templ_args[-256]() == -256 + def test03_templated_function(self): """Templated global and static functions lookup and calls""" From 5d9ceb725ed689cfb2f09de12a46e0a95d17b7f5 Mon Sep 17 00:00:00 2001 From: Emery Conrad Date: Tue, 25 Aug 2026 14:42:11 -0500 Subject: [PATCH 2/2] [interop] Pass template names and named constants as template arguments A class or alias template name becomes a template-template argument. The argument carries the name and a null type. A constexpr variable or an enum constant goes through as its qualified name, so Sema builds an expression. true and false become bool values. The by-name paths need a CppInterOp that resolves the name. An older one reads the name as an integer literal and instantiates a garbage value. Probe for CppInterOp's SupportsNamedTemplateArguments export, and skip the test when it is absent. Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop) --- src/interop/interop_wrapper.cxx | 59 ++++++++++++++++++++++++++++++++- test/support.py | 22 ++++++++++++ test/test_templates.py | 48 +++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index 6eb1eb0..fa30b18 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -460,6 +460,22 @@ static bool is_identifier(std::string_view s) { std::all_of(s.begin() + 1, s.end(), is_valid_body); }; +// A template argument carried by name needs a CppInterOp that resolves the +// name; such a CppInterOp exports SupportsNamedTemplateArguments. +static bool supportsNamedTemplateArgs() { +#ifdef _WIN32 + return false; // no dlsym; enable once the pin guarantees the capability +#else + static const bool Supported = [] { + // CppInterOp is dlopen'ed RTLD_LOCAL; its exports need its own handle. + void* handle = dlopen(cppinterop_paths().Library.c_str(), + RTLD_LOCAL | RTLD_NOW | RTLD_NOLOAD); + return handle && dlsym(handle, "cppinterop_SupportsNamedTemplateArguments"); + }(); + return Supported; +#endif +} + // returns true if no new type was added. bool interop::AppendTypesSlow(const std::string& name, std::vector& types, @@ -494,6 +510,32 @@ bool interop::AppendTypesSlow(const std::string& name, // outside the query scope, e.g. `typedef Foo Bar;` at TU consulted // from a method on Foo). if (is_identifier(name)) { + // true/false are identifier-shaped value literals. + if (name == "true" || name == "false") { + types.emplace_back(Cpp::GetType("bool").data, + strdup(name == "true" ? "1" : "0")); + return false; + } + if (supportsNamedTemplateArgs()) { + TCppScope_t named = parent ? Cpp::GetNamed(name, parent) : nullptr; + if (!named) + named = Cpp::GetNamed(name); + // The identifier may name a non-type entity (constexpr variable, enum + // constant); pass its qualified name so Sema gets an expression, not the + // entity's type. + if (named && (Cpp::IsVariable(named) || Cpp::IsEnumConstant(named))) { + types.emplace_back( + Cpp::GetTypeFromScope(named).data, + strdup(Cpp::GetQualifiedCompleteName(named).c_str())); + return false; + } + // Template name (template-template arg): no type; carried by name. + if (named && Cpp::IsTemplate(named)) { + types.emplace_back( + nullptr, strdup(Cpp::GetQualifiedCompleteName(named).c_str())); + return false; + } + } TCppType_t type = parent ? Cpp::GetType(name, parent) : nullptr; if (!type) type = Cpp::GetType(name); @@ -563,16 +605,31 @@ bool interop::AppendTypesSlow(const std::string& name, } if (!type) { + // Qualified template name (template-template arg). + if (supportsNamedTemplateArgs()) { + if (TCppScope_t named = GetEnumFromCompleteName(i)) { + if (Cpp::IsTemplate(named)) { + types.emplace_back( + nullptr, strdup(Cpp::GetQualifiedCompleteName(named).c_str())); + continue; + } + } + } types.clear(); return true; } if (is_integral(i)) integral_value = strdup(i.c_str()); - if (TCppScope_t scope = GetEnumFromCompleteName(i)) + if (TCppScope_t scope = GetEnumFromCompleteName(i)) { if (Cpp::IsEnumConstant(scope)) integral_value = strdup(std::to_string(Cpp::GetEnumConstantValue(scope)).c_str()); + // A variable is a non-type argument; pass its name (see the identifier + // path). + else if (supportsNamedTemplateArgs() && Cpp::IsVariable(scope)) + integral_value = strdup(Cpp::GetQualifiedCompleteName(scope).c_str()); + } types.emplace_back(type.data, integral_value); } return false; diff --git a/test/support.py b/test/support.py index de2532d..5424689 100644 --- a/test/support.py +++ b/test/support.py @@ -1,5 +1,6 @@ from __future__ import print_function +import ctypes import os import subprocess import sys @@ -116,3 +117,24 @@ def setup_make(targetname): == 1 ) IS_VALGRIND = True if os.getenv("IS_VALGRIND") else False + + +def _has_named_template_args(): + """Whether a template argument may name a constant. + + Look for the same CppInterOp export that cppjit gates on. A rejected + instantiation leaves interpreter state that changes later tests. + """ + + libname = { + "win32": "clangCppInterOp.dll", + "darwin": "libclangCppInterOp.dylib", + }.get(sys.platform, "libclangCppInterOp.so") + lib = os.path.join(os.path.dirname(cppjit.__file__), "interop", "lib", libname) + try: + return hasattr(ctypes.CDLL(lib), "cppinterop_SupportsNamedTemplateArguments") + except OSError: + return False + + +HAS_NAMED_TEMPLATE_ARGS = _has_named_template_args() diff --git a/test/test_templates.py b/test/test_templates.py index a27f342..0eb74a6 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -1,6 +1,7 @@ import py from pytest import mark, raises from support import ( + HAS_NAMED_TEMPLATE_ARGS, IS_CLANG_REPL, IS_CLING, IS_LINUX_ARM, @@ -88,6 +89,53 @@ def test02_non_type_template_args(self): assert cppjit.gbl.nt_templ_args[-1]() == -1 assert cppjit.gbl.nt_templ_args[-256]() == -256 + # true/false are identifier-shaped value literals + cppjit.cppdef("template bool nt_templ_bool() { return b; };") + assert cppjit.gbl.nt_templ_bool["true"]() is True + assert cppjit.gbl.nt_templ_bool["false"]() is False + + @mark.skipif( + not HAS_NAMED_TEMPLATE_ARGS, + reason="needs a CppInterOp that resolves named template arguments", + ) + def test02a_named_template_args(self): + """Use of template names and named constants as template arguments""" + + import cppjit + + cppjit.cppdef("""\ + template struct NtPlain {}; + constexpr int kNtThree = 3; + enum NtEnum { kNtFour = 4 }; + namespace ntarg { + template using Alias = NtPlain; + namespace inner { template struct Nested {}; } + template