Skip to content

add cppExternalDefault sentinel for C++ template defaults (#25805, #25806)#25808

Open
puffball1567 wants to merge 1 commit into
nim-lang:develfrom
puffball1567:cpp-external-default
Open

add cppExternalDefault sentinel for C++ template defaults (#25805, #25806)#25808
puffball1567 wants to merge 1 commit into
nim-lang:develfrom
puffball1567:cpp-external-default

Conversation

@puffball1567
Copy link
Copy Markdown
Contributor

@puffball1567 puffball1567 commented May 11, 2026

Summary

Closes #25805 and #25806.

C++ allows generic types and member traits to default a template parameter to an expression that has no Nim equivalent:

There is currently no way to express this on importcpp types, so bindings either drop the parameter (= losing the type) or fabricate a Nim-side default that diverges from the C++-side one. This PR adds a sentinel template that delegates the default back to the C++ side.

Design

A new sentinel template cppExternalDefault[T](): T is added to lib/system.nim, gated on the new nimHasCppExternalDefault symbol. It carries an {.error.} annotation so calling it directly is a compile-time error — it has no meaningful runtime value, only a compile-time role as a sentinel.

When used as a generic parameter default on an importcpp type and the user omits the parameter at instantiation, the compiler omits the corresponding template argument in the emitted C++ instantiation, so the C++-side default expression takes effect.

type Buf*[T; N: static int = cppExternalDefault[int]()]
  {.importcpp: "Buf<'0, '1>".} = object

var x: Buf[float]        # C++: Buf<float>      — N = C++ default
var y: Buf[float, 100]   # C++: Buf<float, 100> — N = 100
discard cppExternalDefault[int]()  # compile-time error

The user-visible API is exactly:

  • one template (cppExternalDefault) usable only as a generic-param default
  • one feature symbol (nimHasCppExternalDefault) for compatibility-shimmed bindings

Implementation

The intervention is intentionally narrow — kept to detection at one sem stage and emission at one codegen stage:

  • semtypes.semGenericParamList detects the sentinel call syntactically (= nkCall whose callee is cppExternalDefault, possibly with bracket type args) before semConstExpr is run. Direct semConstExpr would trip the {.error.}. The default expression is replaced with the integer literal 0 carrying a new persistent node flag nfFromCppExternalDefault.
  • ccgtypes.importedCppObject checks nfFromCppExternalDefault on each generic argument's static value and skips emitting the corresponding template arg, trimming the preceding , separator. Both branches of the codegen path (= apostrophe pattern like Buf<'0, '1> and the synthesized fallback for bare importcpp names) are updated.
  • PersistentNodeFlags in astdef.nim is extended with nfFromCppExternalDefault so the flag survives copyTree through default-argument substitution. Without this the flag is dropped during sigmatch's default-fill path and the codegen check never fires.

The rest of generic instantiation, type validation, and proc generic deduction stay on their existing paths: the bound argument is a normal static int 0, only carrying an extra node flag that codegen inspects. No new type flag, no special branches in seminst, semtypinst, sigmatch.matches, or typeallowed.

Diff:

  • lib/system.nim — sentinel template
  • compiler/condsyms.nimnimHasCppExternalDefault feature symbol
  • compiler/astdef.nimnfFromCppExternalDefault node flag (added to PersistentNodeFlags)
  • compiler/ic/enum2nif.nim — serialize the new node flag
  • compiler/semtypes.nim — sentinel detection + literal substitution + flag set
  • compiler/ccgtypes.nim — skip flagged template arg in both emit paths

Comparison to C++ semantics

C++ template default Express in Nim with this PR Notes
template<typename T, int N = 1024/sizeof(T)+8> [T; N: static int = cppExternalDefault[int]()] C++-side 1024/sizeof(T)+8 applies (= e.g. Buf<float> → N = 264)
template<typename T> struct Outer { template<typename U = T> struct Inner; }; sentinel on the inner-template default Inner default references outer T — Nim cannot reach it, sentinel delegates
template<typename T, typename Alloc = std::allocator<T>> sentinel on Alloc Alloc default references T — same situation as above
template<typename T> struct S { static constexpr int v = traits<T>::value; }; then [N = S<T>::v] sentinel on N S<T>::value is a SFINAE/trait helper; not expressible in Nim
template<int N> (no default) unchanged: [N: static int] Sentinel has no effect when there is no C++-side default — should not be used
template<int N = 4> (= constant default) both work: explicit [N: static int = 4] or sentinel Explicit is clearer when the default is a plain constant; sentinel is the right answer when the default depends on other params or external traits

When the user provides an explicit value at instantiation (= Buf[float, 100]), the explicit value wins as expected — the sentinel only fires when the parameter is omitted.

Tests

tests/cpp/tcppexternaldefault_*.nim (5 files) cover:

  • _basic.nim — happy path: default fires when omitted, explicit value wins when given
  • _multi_sentinel.nim — multiple consecutive sentinel slots (= consecutive separator trim)
  • _no_apostrophe.nim — bare importcpp name (= synthesized template arg list path)
  • _proc_deduction.nim — proc generic deduction through a sentinel-default type (= no extra hooks needed in seminst / semtypinst)
  • _sentinel_error.nim — direct call → {.error.} triggers

Local regression: full tests/cpp/ runs clean (= the only failure is tasync_cpp.nim's cannot open file: jester, which also fails on unmodified devel due to missing local nimble dep; unrelated to this change).

Test plan

  • bin/testament --targets:cpp pat "tests/cpp/tcppexternaldefault*" — 9/9 PASS (= 5 files × megatest variants)
  • bin/testament --targets:cpp cat cpp — same pass set as unmodified devel (only pre-existing env-gated failure)
  • CI — green across targets

Drafted; ready for review on green CI.

@puffball1567
Copy link
Copy Markdown
Contributor Author

CI failures look unrelated to this change:

  • macOS batch 1_3 / 2_3: cannot open file: ginger (= ggplotnim's transitive dep), cannot open file: stew/shims/macros (= chronicles' transitive dep)
  • packages Linux_amd64: same pattern with cannot open file: sdl2 (= nimes' transitive dep) and a generic PackageFileParsed failure

These all look like transient nimble dep resolution issues — the failed import lines reference packages that were never installed (= the failure happens before any compiler-level work runs). PRs opened a few hours earlier (#25807, #25804, #25799) had the same package categories pass clean.

This PR's touched surface is narrow: a sentinel template in lib/system.nim, syntactic detection in semtypes.semGenericParamList, and template-arg skip in ccgtypes.importedCppObject (= 6 compiler/lib files + 5 tests, ~234 lines). It doesn't touch nimble or any package-install path.

Will mark ready for review once CI looks healthy.

Side note: is there a contributor-friendly way to retrigger failed jobs on this repo (= I don't have admin rights), or is this typically maintainer-driven? Happy to follow whichever convention you prefer. cc @Araq @ringabout @metagn

@puffball1567 puffball1567 marked this pull request as ready for review May 11, 2026 22:53
…5805, nim-lang#25806)

C++ allows generic types and member traits to default a template parameter
to an expression that has no Nim equivalent: ``sizeof(T)`` on an opaque
template argument, or ``Class<T>::value`` from a SFINAE/trait helper.
There is currently no way to express this on `importcpp` types, so
bindings either drop the parameter (= losing the type) or fabricate a
Nim-side default that diverges from the C++-side one.

Introduce a sentinel template ``cppExternalDefault[T](): T`` (= in
`lib/system.nim`, gated on the new `nimHasCppExternalDefault` symbol) that
is only meaningful as a generic parameter default on `importcpp` types.
Calling it directly is a compile-time error; placed as a default it tells
the compiler to emit no template argument at the corresponding slot in
the C++ instantiation, so the C++-side default takes effect.

Implementation
--------------
The sentinel is detected syntactically in `semGenericParamList` (= no
`semConstExpr` is run, which would trip the `{.error.}`). The default
expression is replaced with the integer literal ``0`` carrying a new
persistent node flag ``nfFromCppExternalDefault``. Codegen
(`importedCppObject` in `ccgtypes`) checks that flag on each generic
argument and skips emitting the corresponding template arg, trimming the
preceding ``,`` separator.

This keeps the rest of generic instantiation, type validation, and proc
generic deduction on their existing paths: the bound argument is a normal
``static int`` value, only carrying an extra node flag that codegen
inspects. Adding the flag to `PersistentNodeFlags` is what makes it
survive ``copyTree`` through default-argument substitution.

Behavior
--------
\`\`\`nim
type Buf*[T; N: static int = cppExternalDefault[int]()]
  {.importcpp: \"Buf<'0, '1>\".} = object

var x: Buf[float]        # C++: Buf<float>      (= N = C++ default)
var y: Buf[float, 100]   # C++: Buf<float, 100> (= N = 100)
discard cppExternalDefault[int]()  # compile error
\`\`\`

Fixes nim-lang#25805. Fixes nim-lang#25806.

Tests
-----
\`tests/cpp/tcppexternaldefault_*.nim\` cover the apostrophe pattern,
multiple consecutive sentinel slots (= consecutive separator trim), the
no-apostrophe importcpp form (= synthesized template arg list), proc
generic deduction through a sentinel-default type, and the direct-call
\`{.error.}\`. Existing \`tests/cpp/\` continues to pass.
@puffball1567 puffball1567 force-pushed the cpp-external-default branch from c57e510 to 4f36aac Compare May 12, 2026 12:27
@puffball1567 puffball1567 marked this pull request as draft May 12, 2026 15:31
@puffball1567 puffball1567 marked this pull request as ready for review May 13, 2026 15:13
@puffball1567
Copy link
Copy Markdown
Contributor Author

For context on cases where the overload-alias workaround doesn't reach (= trait/SFINAE-dependent defaults, member-type-dependent defaults, combinatorial explosion in STL-shaped APIs, variadic template defaults), see the discussion at #25805 (comment): #25805 (comment)

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.

[C++] sizeof on opaque importcpp template parameter prevents using C++ template defaults like sizeof(T)

1 participant