Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion compiler/astdef.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
28 changes: 23 additions & 5 deletions compiler/ccgtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -883,21 +883,39 @@ 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

if chunkStart != 0:
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
Expand Down
2 changes: 2 additions & 0 deletions compiler/condsyms.nim
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,5 @@ proc initDefines*(symbols: StringTableRef) =

defineSymbol("nimHasImplicitRangeConversion")

defineSymbol("nimHasCppExternalDefault")

2 changes: 2 additions & 0 deletions compiler/ic/enum2nif.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down Expand Up @@ -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

Expand Down
63 changes: 55 additions & 8 deletions compiler/semtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions lib/system.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>::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<float>``, N = C++ default
## var y: Buf[float, 100] # C++: ``Buf<float, 100>``, N = 100
## ```
default(T)


proc reset*[T](obj: var T) {.noSideEffect.} =
## Resets an object `obj` to its default value.
when nimvm:
Expand Down
39 changes: 39 additions & 0 deletions tests/cpp/tcppexternaldefault_basic.nim
Original file line number Diff line number Diff line change
@@ -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<T, int N = 1024 / sizeof(T) + 8>`.
# When the user writes `var x: Buf[cint]` the compiler must instantiate
# `Buf<int>` (= no second template arg), so the C++-side default
# expression `1024 / sizeof(int) + 8 = 264` takes effect.

{.emit: """/*TYPESECTION*/
template<typename T, int N = 1024 / sizeof(T) + 8>
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()
24 changes: 24 additions & 0 deletions tests/cpp/tcppexternaldefault_multi_sentinel.nim
Original file line number Diff line number Diff line change
@@ -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<typename T, int M = 4, int N = 8>
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()
25 changes: 25 additions & 0 deletions tests/cpp/tcppexternaldefault_no_apostrophe.nim
Original file line number Diff line number Diff line change
@@ -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<typename T, int N = 1024 / sizeof(T) + 8>
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()
30 changes: 30 additions & 0 deletions tests/cpp/tcppexternaldefault_proc_deduction.nim
Original file line number Diff line number Diff line change
@@ -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<typename T, int N = 1024 / sizeof(T) + 8>
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()
14 changes: 14 additions & 0 deletions tests/cpp/tcppexternaldefault_sentinel_error.nim
Original file line number Diff line number Diff line change
@@ -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()
61 changes: 61 additions & 0 deletions tests/cpp/tcppexternaldefault_typed.nim
Original file line number Diff line number Diff line change
@@ -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<typename T, bool flag = sizeof(T) >= 2>
struct WithBool { int len() const { return flag ? 8 : 1; } };

template<typename T, char tag = (char)(sizeof(T))>
struct WithChar { int len() const { return (int)tag; } };

enum class Mode { A = 0, B = 1, C = 2 };
template<typename T, Mode m = ((sizeof(T) > 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()
Loading