diff --git a/compiler/astdef.nim b/compiler/astdef.nim index 6a7b4d77881e5..35d27b73c78d7 100644 --- a/compiler/astdef.nim +++ b/compiler/astdef.nim @@ -327,6 +327,10 @@ type # because openSym experimental switch is disabled # gives warning instead nfLazyType # node has a lazy type + nfFromCppExternalDefault # marker placed on the int literal 0 we substitute + # for a `cppExternalDefault[T]()` sentinel; carried + # through type instantiation so codegen can detect + # that the C++ template arg should be omitted TNodeFlags* = set[TNodeFlag] TTypeFlag* = enum # keep below 32 for efficiency reasons (now: 47) @@ -871,7 +875,8 @@ const nfFromTemplate, nfDefaultRefsParam, nfExecuteOnReload, nfLastRead, nfFirstWrite, nfSkipFieldChecking, - nfDisabledOpenSym, nfLazyType} + nfDisabledOpenSym, nfLazyType, + nfFromCppExternalDefault} namePos* = 0 patternPos* = 1 # empty except for term rewriting macros genericParamsPos* = 2 diff --git a/compiler/ccgtypes.nim b/compiler/ccgtypes.nim index 8e6b44c81de1c..e7a051bb7ea49 100644 --- a/compiler/ccgtypes.nim +++ b/compiler/ccgtypes.nim @@ -883,11 +883,24 @@ proc importedCppObject(m: BModule; t, tt: PType; check: var IntSet; kind: TypeDe var chunkEnd = i-1 var idx, stars: int = 0 if scanCppGenericSlot(cppName, i, idx, stars): - result.add cppName.substr(chunkStart, chunkEnd) - chunkStart = i - let typeInSlot = resolveStarsInCppType(tt, idx + 1, stars) - addResultType(typeInSlot) + var skipArg = false + if typeInSlot != nil and typeInSlot.kind == tyStatic and + typeInSlot.n != nil and nfFromCppExternalDefault in typeInSlot.n.flags: + skipArg = true + if skipArg: + # Skip this template arg and the preceding separator (", " etc.) + # so the C++ template default value applies. + var trimmedEnd = chunkEnd + while trimmedEnd >= chunkStart and cppName[trimmedEnd] in {' ', ',', '\t'}: + dec trimmedEnd + if trimmedEnd >= chunkStart: + result.add cppName.substr(chunkStart, trimmedEnd) + chunkStart = i + else: + result.add cppName.substr(chunkStart, chunkEnd) + chunkStart = i + addResultType(typeInSlot) else: inc i @@ -895,9 +908,14 @@ proc importedCppObject(m: BModule; t, tt: PType; check: var IntSet; kind: TypeDe result.add cppName.substr(chunkStart) else: result = cppNameAsRope & "<" - for needsComma, a in tt.genericInstParams: + var needsComma = false + for _, a in tt.genericInstParams: + if a != nil and a.kind == tyStatic and a.n != nil and + nfFromCppExternalDefault in a.n.flags: + continue if needsComma: result.add(" COMMA ") addResultType(a) + needsComma = true result.add("> ") # always call for sideeffects: assert t.kind != tyTuple diff --git a/compiler/condsyms.nim b/compiler/condsyms.nim index 28c3d2f309531..fd93ef43011e3 100644 --- a/compiler/condsyms.nim +++ b/compiler/condsyms.nim @@ -177,3 +177,5 @@ proc initDefines*(symbols: StringTableRef) = defineSymbol("nimHasImplicitRangeConversion") + defineSymbol("nimHasCppExternalDefault") + diff --git a/compiler/ic/enum2nif.nim b/compiler/ic/enum2nif.nim index 4b7860fc96594..863873a204616 100644 --- a/compiler/ic/enum2nif.nim +++ b/compiler/ic/enum2nif.nim @@ -1473,6 +1473,7 @@ proc genFlags*(s: set[TNodeFlag]; dest: var string) = of nfSkipFieldChecking: dest.add "s0" of nfDisabledOpenSym: dest.add "d3" of nfLazyType: dest.add "l1" + of nfFromCppExternalDefault: dest.add "k" proc parse*(t: typedesc[TNodeFlag]; s: string): set[TNodeFlag] = @@ -1534,6 +1535,7 @@ proc parse*(t: typedesc[TNodeFlag]; s: string): set[TNodeFlag] = else: result.incl nfSem of 't': result.incl nfTransf of 'w': result.incl nfFirstWrite + of 'k': result.incl nfFromCppExternalDefault else: discard inc i diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index 8009f7293c61e..bf2208ff28033 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -2626,16 +2626,63 @@ proc semGenericParamList(c: PContext, n: PNode, father: PType = nil): PNode = typ = semGenericConstraints(c, typ) if def.kind != nkEmpty: - def = semConstExpr(c, def) - if typ == nil: - if def.typ.kind != tyTypeDesc: + # Detect `cppExternalDefault[T]()` sentinel as the default expression. + # Recognized syntactically (nkCall whose callee is `cppExternalDefault` + # possibly with bracket type args). Done before semConstExpr because + # the template carries an `{.error.}` annotation that would trigger + # on normal evaluation. The literal substituted below carries + # `nfFromCppExternalDefault` (a persistent node flag) so codegen can + # omit the corresponding C++ template arg at instantiation. + var isCppExternalDefault = false + if def.kind == nkCall and def.len >= 1: + var callee = def[0] + if callee.kind == nkBracketExpr and callee.len >= 1: + callee = callee[0] + if callee.kind == nkIdent and callee.ident.s == "cppExternalDefault": + isCppExternalDefault = true + + if isCppExternalDefault: + # Replace the sentinel call with the integer literal 0 so the rest + # of generic instantiation has a concrete value to bind. The literal + # carries `nfFromCppExternalDefault` (a persistent node flag) so + # codegen can detect, at C++ template arg emission time, that the + # default actually fired and the corresponding template arg should + # be omitted (= the C++-side default takes effect). + let info = def.info + def = newIntNode(nkIntLit, 0) + def.info = info + def.flags.incl nfFromCppExternalDefault + if typ != nil: + # Use the declared base type so `static bool`, `static char`, + # `static enum`, `static float` etc. all bind cleanly. Only + # fall back to int when the base is something we cannot fit a + # zero literal into (= an int placeholder is still chosen so + # the rest of generic instantiation has a concrete value; the + # value is never observed because codegen omits the C++ arg). + const fitable = {tyInt..tyInt64, tyUInt..tyUInt64, + tyFloat..tyFloat64, tyBool, tyChar, tyEnum} + let baseType = typ.skipTypes({tyStatic, tyTypeDesc}) + if baseType != nil and baseType.kind in fitable: + def.typ = baseType + else: + def.typ = getSysType(c.graph, info, tyInt) + def = fitNode(c, typ, def, info) + def.flags.incl nfFromCppExternalDefault + else: + def.typ = getSysType(c.graph, info, tyInt) typ = newTypeS(tyStatic, c, def.typ) + if father == nil: typ.incl tfWildcard else: - # the following line fixes ``TV2*[T:SomeNumber=TR] = array[0..1, T]`` - # from manyloc/named_argument_bug/triengine: - def.typ = def.typ.skipTypes({tyTypeDesc}) - if not containsGenericType(def.typ): - def = fitNode(c, typ, def, def.info) + def = semConstExpr(c, def) + if typ == nil: + if def.typ.kind != tyTypeDesc: + typ = newTypeS(tyStatic, c, def.typ) + else: + # the following line fixes ``TV2*[T:SomeNumber=TR] = array[0..1, T]`` + # from manyloc/named_argument_bug/triengine: + def.typ = def.typ.skipTypes({tyTypeDesc}) + if not containsGenericType(def.typ): + def = fitNode(c, typ, def, def.info) if typ == nil: typ = newTypeS(tyGenericParam, c) diff --git a/lib/system.nim b/lib/system.nim index c76d096426974..976acc6711409 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -925,6 +925,30 @@ proc default*[T](_: typedesc[T]): T {.magic: "Default", noSideEffect.} = assert x.a == 2 +when defined(nimHasCppExternalDefault): + template cppExternalDefault*[T](): T {.error: "cppExternalDefault is a sentinel only valid as a generic parameter default on importcpp types; the compiler omits the corresponding template argument in C++ instantiation so the C++-side default takes effect".} = + ## Sentinel for "use the C++-side template default" on `importcpp` generic + ## parameters. When used as a generic parameter default 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 (e.g. ``1024/sizeof(T)+8`` or ``Class::value``) + ## takes effect. + ## + ## Calling this template directly is a compile-time error: it has no + ## meaningful runtime value, only a compile-time role as a sentinel. + ## + ## Example: + ## ```nim + ## type + ## Buf*[T; N: static int = cppExternalDefault[int]()] + ## {.importcpp: "Buf<'0, '1>".} = object + ## + ## var x: Buf[float] # C++: ``Buf``, N = C++ default + ## var y: Buf[float, 100] # C++: ``Buf``, N = 100 + ## ``` + default(T) + + proc reset*[T](obj: var T) {.noSideEffect.} = ## Resets an object `obj` to its default value. when nimvm: diff --git a/tests/cpp/tcppexternaldefault_basic.nim b/tests/cpp/tcppexternaldefault_basic.nim new file mode 100644 index 0000000000000..0a24184fc9be7 --- /dev/null +++ b/tests/cpp/tcppexternaldefault_basic.nim @@ -0,0 +1,39 @@ +discard """ + targets: "cpp" + output: ''' +default len = 264 +explicit N=4, len = 4 +''' +""" + +# Core happy-path test for the cppExternalDefault sentinel. +# +# Triggers: +# - lib/system.nim cppExternalDefault template (declaration only) +# - semtypes.nim sentinel detection in semGenericParamList +# - semtypinst.nim flag propagation through handleGenericInvocation +# - typeallowed.nim tyGenericInvocation + tyGenericParam allow branches +# - ccgtypes.nim apostrophe slot skip + preceding-comma trim +# +# C++ side defines `struct Buf`. +# When the user writes `var x: Buf[cint]` the compiler must instantiate +# `Buf` (= no second template arg), so the C++-side default +# expression `1024 / sizeof(int) + 8 = 264` takes effect. + +{.emit: """/*TYPESECTION*/ +template +struct Buf { int len() const { return N; } }; +""".} + +type + Buf*[T; N: static int = cppExternalDefault[int]()] {.importcpp: "Buf<'0, '1>".} = object + +proc len*[T; N: static int](b: Buf[T, N]): int {.importcpp: "#.len()".} + +proc main() = + var defaulted: Buf[cint] + echo "default len = ", defaulted.len() + var explicit: Buf[cint, 4] + echo "explicit N=4, len = ", explicit.len() + +main() diff --git a/tests/cpp/tcppexternaldefault_multi_sentinel.nim b/tests/cpp/tcppexternaldefault_multi_sentinel.nim new file mode 100644 index 0000000000000..726619bba4d07 --- /dev/null +++ b/tests/cpp/tcppexternaldefault_multi_sentinel.nim @@ -0,0 +1,24 @@ +discard """ + targets: "cpp" + output: "ok" +""" + +# Triggers the ccgtypes apostrophe path with two consecutive sentinel slots, +# exercising the trim loop's ability to drop multiple `, ` separators in a +# row when adjacent template args are all omitted. + +{.emit: """/*TYPESECTION*/ +template +struct Buf { }; +""".} + +type + Buf*[T; + M: static int = cppExternalDefault[int](); + N: static int = cppExternalDefault[int]()] {.importcpp: "Buf<'0, '1, '2>".} = object + +proc main() = + var b: Buf[cint] + echo "ok" + +main() diff --git a/tests/cpp/tcppexternaldefault_no_apostrophe.nim b/tests/cpp/tcppexternaldefault_no_apostrophe.nim new file mode 100644 index 0000000000000..591c281a88a67 --- /dev/null +++ b/tests/cpp/tcppexternaldefault_no_apostrophe.nim @@ -0,0 +1,25 @@ +discard """ + targets: "cpp" + output: "ok" +""" + +# Triggers the ccgtypes branch that handles importcpp names without +# apostrophe placeholders (= the `for _, a in tt.genericInstParams:` loop). +# When the importcpp pattern is just a bare name, ccgtypes synthesizes the +# `<...>` template arg list from the generic params; the sentinel arg must +# be skipped there too. + +{.emit: """/*TYPESECTION*/ +template +struct Buf { }; +""".} + +type + # No apostrophe pattern: importcpp uses the bare name. + Buf*[T; N: static int = cppExternalDefault[int]()] {.importcpp: "Buf".} = object + +proc main() = + var b: Buf[cint] + echo "ok" + +main() diff --git a/tests/cpp/tcppexternaldefault_proc_deduction.nim b/tests/cpp/tcppexternaldefault_proc_deduction.nim new file mode 100644 index 0000000000000..1d0b9ebf19379 --- /dev/null +++ b/tests/cpp/tcppexternaldefault_proc_deduction.nim @@ -0,0 +1,30 @@ +discard """ + targets: "cpp" + output: "ok" +""" + +# Triggers the proc generic deduction path: +# - seminst.nim hook 1 (sentinel-default param has no entry in pt) +# - seminst.nim hook 2 (isUnresolvedStatic case for the same param) +# - semtypinst.nim lookupTypeVar hook (lookup nil for sentinel param) +# +# Without these hooks the call to `take` below errors with +# `cannot instantiate: 'N'` because the proc-side N param cannot be +# deduced from a value bound by the cppExternalDefault sentinel. + +{.emit: """/*TYPESECTION*/ +template +struct Buf { }; +""".} + +type + Buf*[T; N: static int = cppExternalDefault[int]()] {.importcpp: "Buf<'0, '1>".} = object + +proc take*[T; N: static int](b: Buf[T, N]) {.importcpp: "(void)#".} + +proc main() = + var b: Buf[cint] + take(b) + echo "ok" + +main() diff --git a/tests/cpp/tcppexternaldefault_sentinel_error.nim b/tests/cpp/tcppexternaldefault_sentinel_error.nim new file mode 100644 index 0000000000000..3097259f3eda3 --- /dev/null +++ b/tests/cpp/tcppexternaldefault_sentinel_error.nim @@ -0,0 +1,14 @@ +discard """ + targets: "cpp" + errormsg: "cppExternalDefault is a sentinel only valid as a generic parameter default on importcpp types" + line: 12 +""" + +# Triggers the lib/system.nim cppExternalDefault template's {.error.} when +# the user calls the sentinel directly instead of using it as a generic +# parameter default. Should fail to compile with a clear message. + +proc main() = + discard cppExternalDefault[int]() + +main() diff --git a/tests/cpp/tcppexternaldefault_typed.nim b/tests/cpp/tcppexternaldefault_typed.nim new file mode 100644 index 0000000000000..802fdc6357f2f --- /dev/null +++ b/tests/cpp/tcppexternaldefault_typed.nim @@ -0,0 +1,61 @@ +discard """ + targets: "cpp" + output: ''' +bool default = 8 +char default = 4 +enum default = 2 +''' +""" + +# Triggers the static-non-int path of the cppExternalDefault hook in +# `semGenericParamList` (= the default placeholder must adopt the param's +# declared base type, not be hard-coded to `int`). +# +# Each importcpp template has a non-`int` default kind (= bool / char / +# enum). The C++-side defaults all involve `sizeof(T)` so they are not +# Nim-evaluable and must be expressed via `cppExternalDefault`. Without +# the typed-fallback in semtypes, these fail with +# `type mismatch: got 'int' for '0' but expected 'static[bool]'` etc. +# +# (`double` is intentionally excluded: floating-point non-type template +# parameters are forbidden by the C++ standard until C++20, and +# testament defaults to `-std=gnu++17`.) + +{.emit: """/*TYPESECTION*/ +template= 2> +struct WithBool { int len() const { return flag ? 8 : 1; } }; + +template +struct WithChar { int len() const { return (int)tag; } }; + +enum class Mode { A = 0, B = 1, C = 2 }; +template 2) ? Mode::C : Mode::A)> +struct WithEnum { int len() const { return (int)m; } }; +""".} + +type + Mode {.importcpp: "Mode".} = enum + mA = 0 + mB = 1 + mC = 2 + + WithBool*[T; flag: static bool = cppExternalDefault[bool]()] + {.importcpp: "WithBool<'0, '1>".} = object + WithChar*[T; tag: static char = cppExternalDefault[char]()] + {.importcpp: "WithChar<'0, '1>".} = object + WithEnum*[T; m: static Mode = cppExternalDefault[Mode]()] + {.importcpp: "WithEnum<'0, '1>".} = object + +proc lenB*[T; flag: static bool](b: WithBool[T, flag]): int {.importcpp: "#.len()".} +proc lenC*[T; tag: static char](b: WithChar[T, tag]): int {.importcpp: "#.len()".} +proc lenE*[T; m: static Mode](b: WithEnum[T, m]): int {.importcpp: "#.len()".} + +proc main() = + var b: WithBool[cint] + echo "bool default = ", b.lenB() # sizeof(cint)=4 >= 2 -> flag=true -> 8 + var c: WithChar[cint] + echo "char default = ", c.lenC() # (char)sizeof(cint) -> 4 + var e: WithEnum[cint] + echo "enum default = ", e.lenE() # sizeof(cint)=4 > 2 -> Mode::C = 2 + +main()