Skip to content

Commit

Permalink
add switch, warning, and bind support for new generic injection beh…
Browse files Browse the repository at this point in the history
…avior (#23102)

refs #23091, especially post merge comments

Unsure if `experimental` and `bind` are the perfect constructs to use
but they seem to get the job done here. Symbol nodes do not get marked
`nfOpenSym` if the `bind` statement is used for their symbol, and
`nfOpenSym` nodes do not get replaced by new local symbols if the
experimental switch is not enabled in the local context (meaning it also
works with `push experimental`). However this incurs a warning as the
fact that the node is marked `nfOpenSym` means we did not `bind` it, so
we might want to do that or turn on the experimental switch if we didn't
intend to bind it.

The experimental switch name is arbitrary and could be changed.

---------

Co-authored-by: Andreas Rumpf <[email protected]>
  • Loading branch information
metagn and Araq authored Dec 22, 2023
1 parent df3c95d commit 4b1a841
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 6 deletions.
35 changes: 35 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,41 @@ slots when enlarging a sequence.
let (a, (b, c)): (byte, (float, cstring)) = (1, (2, "abc"))
```

- An experimental option `genericsOpenSym` has been added to allow captured
symbols in generic routine bodies to be replaced by symbols injected locally
by templates/macros at instantiation time. `bind` may be used to keep the
captured symbols over the injected ones regardless of enabling the option.

Since this change may affect runtime behavior, the experimental switch
`genericsOpenSym` needs to be enabled, and a warning is given in the case
where an injected symbol would replace a captured symbol not bound by `bind`
and the experimental switch isn't enabled.

```nim
const value = "captured"
template foo(x: int, body: untyped) =
let value {.inject.} = "injected"
body
proc old[T](): string =
foo(123):
return value # warning: a new `value` has been injected, use `bind` or turn on `experimental:genericsOpenSym`
echo old[int]() # "captured"
{.experimental: "genericsOpenSym".}
proc bar[T](): string =
foo(123):
return value
assert bar[int]() == "injected" # previously it would be "captured"
proc baz[T](): string =
bind value
foo(123):
return value
assert baz[int]() == "captured"
```

## Compiler changes

- `--nimcache` using a relative path as the argument in a config file is now relative to the config file instead of the current directory.
Expand Down
2 changes: 2 additions & 0 deletions compiler/lineinfos.nim
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ type
warnStmtListLambda = "StmtListLambda",
warnBareExcept = "BareExcept",
warnImplicitDefaultValue = "ImplicitDefaultValue",
warnGenericsIgnoredInjection = "GenericsIgnoredInjection",
warnStdPrefix = "StdPrefix"
warnUser = "User",
# hints
Expand Down Expand Up @@ -196,6 +197,7 @@ const
warnStmtListLambda: "statement list expression assumed to be anonymous proc; this is deprecated, use `do (): ...` or `proc () = ...` instead",
warnBareExcept: "$1",
warnImplicitDefaultValue: "$1",
warnGenericsIgnoredInjection: "$1",
warnStdPrefix: "$1 needs the 'std' prefix",
warnUser: "$1",
hintSuccess: "operation successful: $#",
Expand Down
1 change: 1 addition & 0 deletions compiler/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ type
strictDefs,
strictCaseObjects,
inferGenericTypes,
genericsOpenSym,
vtables

LegacyFeature* = enum
Expand Down
14 changes: 12 additions & 2 deletions compiler/semexprs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3076,8 +3076,18 @@ proc semExpr(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType
var o = s2.owner
while o != nil:
if o == c.p.owner:
result = semExpr(c, id, flags, expectedType)
return
if genericsOpenSym in c.features:
result = semExpr(c, id, flags, expectedType)
return
else:
message(c.config, n.info, warnGenericsIgnoredInjection,
"a new symbol '" & s.name.s & "' has been injected during " &
"instantiation of " & c.p.owner.name.s & ", " &
"however " & getSymRepr(c.config, s) & " captured at " &
"the proc declaration will be used instead; " &
"either enable --experimental:genericsOpenSym to use the " &
"injected symbol or `bind` this captured symbol explicitly")
break
o = o.owner
# because of the changed symbol binding, this does not mean that we
# don't have to check the symbol for semantics here again!
Expand Down
11 changes: 7 additions & 4 deletions compiler/semgnrc.nim
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ template isMixedIn(sym): bool =
s.magic == mNone and
s.kind in OverloadableSyms)

template canOpenSym(s): bool =
{withinMixin, withinConcept} * flags == {withinMixin} and s.id notin ctx.toBind

proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym,
ctx: var GenericCtx; flags: TSemGenericFlags,
fromDotExpr=false): PNode =
Expand All @@ -69,7 +72,7 @@ proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym,
result.transitionSonsKind(nkClosedSymChoice)
else:
result = symChoice(c, n, s, scOpen)
if {withinMixin, withinConcept} * flags == {withinMixin} and result.kind == nkSym:
if result.kind == nkSym and canOpenSym(result.sym):
result.flags.incl nfOpenSym
result.typ = nil
case s.kind
Expand Down Expand Up @@ -99,7 +102,7 @@ proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym,
result = n
else:
result = newSymNodeTypeDesc(s, c.idgen, n.info)
if {withinMixin, withinConcept} * flags == {withinMixin}:
if canOpenSym(result.sym):
result.flags.incl nfOpenSym
result.typ = nil
onUse(n.info, s)
Expand All @@ -110,15 +113,15 @@ proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym,
if (s.typ != nil) and
(s.typ.flags * {tfGenericTypeParam, tfImplicitTypeParam} == {}):
result = newSymNodeTypeDesc(s, c.idgen, n.info)
if {withinMixin, withinConcept} * flags == {withinMixin}:
if canOpenSym(result.sym):
result.flags.incl nfOpenSym
result.typ = nil
else:
result = n
onUse(n.info, s)
else:
result = newSymNode(s, n.info)
if {withinMixin, withinConcept} * flags == {withinMixin}:
if canOpenSym(result.sym):
result.flags.incl nfOpenSym
result.typ = nil
onUse(n.info, s)
Expand Down
40 changes: 40 additions & 0 deletions doc/manual_experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -2521,6 +2521,46 @@ NimFunctor()(1)
Notice we use the overload of `()` to have the same semantics in Nim, but on the `importcpp` we import the functor as a function.
This allows to easy interop with functions that accepts for example a `const` operator in its signature.


Injected symbols in generic procs
=================================

With the experimental option `genericsOpenSym`, captured symbols in generic
routine bodies may be replaced by symbols injected locally by templates/macros
at instantiation time. `bind` may be used to keep the captured symbols over
the injected ones regardless of enabling the option.

Since this change may affect runtime behavior, the experimental switch
`genericsOpenSym` needs to be enabled, and a warning is given in the case
where an injected symbol would replace a captured symbol not bound by `bind`
and the experimental switch isn't enabled.
```nim
const value = "captured"
template foo(x: int, body: untyped) =
let value {.inject.} = "injected"
body
proc old[T](): string =
foo(123):
return value # warning: a new `value` has been injected, use `bind` or turn on `experimental:genericsOpenSym`
echo old[int]() # "captured"
{.experimental: "genericsOpenSym".}
proc bar[T](): string =
foo(123):
return value
assert bar[int]() == "injected" # previously it would be "captured"
proc baz[T](): string =
bind value
foo(123):
return value
assert baz[int]() == "captured"
```
VTable for methods
==================
Expand Down
29 changes: 29 additions & 0 deletions tests/generics/tmacroinjectedsym.nim
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{.experimental: "genericsOpenSym".}

block: # issue #22605, normal call syntax
const error = "bad"

Expand All @@ -16,6 +18,15 @@ block: # issue #22605, normal call syntax

doAssert g(int) == "good"

proc g2(T: type): string =
bind error # use the bad version on purpose
let x = valueOr 123:
return $error

"ok"

doAssert g2(int) == "bad"

block: # issue #22605, method call syntax
const error = "bad"

Expand All @@ -34,6 +45,15 @@ block: # issue #22605, method call syntax

doAssert g(int) == "good"

proc g2(T: type): string =
bind error # use the bad version on purpose
let x = 123.valueOr:
return $error

"ok"

doAssert g2(int) == "bad"

block: # issue #22605, original complex example
type Xxx = enum
error
Expand Down Expand Up @@ -84,3 +104,12 @@ block: # issue #22605, original complex example
"ok"

doAssert g(int) == "f"

proc g2(T: type): string =
bind error # use the bad version on purpose
let x = f().valueOr:
return $error

"ok"

doAssert g2(int) == "error"
50 changes: 50 additions & 0 deletions tests/generics/tmacroinjectedsymwarning.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
type Xxx = enum
error
value

type
Result[T, E] = object
when T is void:
when E is void:
oResultPrivate*: bool
else:
case oResultPrivate*: bool
of false:
eResultPrivate*: E
of true:
discard
else:
when E is void:
case oResultPrivate*: bool
of false:
discard
of true:
vResultPrivate*: T
else:
case oResultPrivate*: bool
of false:
eResultPrivate*: E
of true:
vResultPrivate*: T

template valueOr[T: not void, E](self: Result[T, E], def: untyped): untyped =
let s = (self) # TODO avoid copy
case s.oResultPrivate
of true:
s.vResultPrivate
of false:
when E isnot void:
template error: untyped {.used, inject.} = s.eResultPrivate
def

proc f(): Result[int, cstring] =
Result[int, cstring](oResultPrivate: false, eResultPrivate: "f")

proc g(T: type): string =
let x = f().valueOr:
return $error #[tt.Warning
^ a new symbol 'error' has been injected during instantiation of g, however 'error' [enumField declared in tmacroinjectedsymwarning.nim(2, 3)] captured at the proc declaration will be used instead; either enable --experimental:genericsOpenSym to use the injected symbol or `bind` this captured symbol explicitly [GenericsIgnoredInjection]]#

"ok"

discard g(int)

0 comments on commit 4b1a841

Please sign in to comment.