mirror of
https://github.com/nim-lang/Nim.git
synced 2026-04-18 05:20:31 +00:00
This fixes type resolution for `iterable[T]`.
I want to proceed with RFC
[#562](https://github.com/nim-lang/RFCs/issues/562) and this is the main
blocker for composability.
Fixes #22098 and, arguably, #19206
```nim
import std/strutils
template collect[T](it: iterable[T]): seq[T] =
block:
var res: seq[T] = @[]
for x in it:
res.add x
res
const text = "a b c d"
let words = text.split.collect()
doAssert words == @[ "a", "b", "c", "d" ]
```
In cases like `strutils.split`, where both proc and iterator overload
exists, the compiler resolves to the `func` overload causing a type
mismatch.
The old mode resolved `text.split` to `seq[string]` before the
surrounding `iterable[T]` requirement was applied, so the argument no
longer matched this template.
It should be noted that, compared to older sequtils templates,
composable chains based on `iterable[T]` require an iterator-producing
expression, e.g. `"foo".items.iterableTmpl()` rather than just
`"foo".iterableTmpl()`. This is actually desirable: it keeps the
iteration boundary explicit and makes iterable-driven templates
intentionally not directly interchangeable with older
untyped/loosely-typed templates like those in `sequtils`, whose internal
iterator setup we have zero control over (e.g. hard-coding adapters like
`items`).
Also, I noticed in `semstmts` that anonymous iterators are always
`closure`, which is not that surprising if you think about it, but still
I added a paragraph to the manual.
Regarding implementation:
From what I gathered, the root cause is that `semOpAux` eagerly
pre-types all arguments with plain flags before overload resolution
begins, so by the time `prepareOperand` processes `split` against the
`iterable[T]`, the wrong overload has already won.
The fix touches a few places:
- `prepareOperand` in `sigmatch.nim`:
When `formal.kind == tyIterable` and the argument was already typed as
something else, it's re-semchecked with the
`efPreferIteratorForIterable` flag. The recheck is limited to direct
calls (`a[0].kind in {nkIdent, nkAccQuoted, nkSym, nkOpenSym}`) to avoid
recursing through `semIndirectOp`/`semOpAux` again.
- `iteratorPreference` field `TCandidate`, checked before
`genericMatches` in `cmpCandidates`, gives the iterator overload a win
without touching the existing iterator heuristic used by `for` loops.
**Limitations:**
The implementation is still flag-driven rather than purely
formal-driven, so the behaviour is a bit too broad `efWantIterable` can
cause iterator results to be wrapped as `tyIterable` in
iterable-admitting contexts, not only when `iterable[T]` match is being
processed.
`iterable[T]` still does not accept closure iterator values such
as`iterator(): T {.closure.}`. It only matches the compiler's internal
`tyIterable`, not arbitrary iterator-typed values.
The existing iterator-preference heuristic is still in place, because
when I tried to remove it, some loosely-related regressions happened. In
particular, ordinary iterator-admitting contexts and iterator chains
still rely on early iterator preference during semchecking, before the
compiler has enough surrounding context to distinguish between
value/iterator producing overloads. Full heuristic removal would require
a broader refactor of dot-chain/intermediate-expression semchecking,
which is just too much for me ATM. This PR narrows only the
tyIterable-specific cases.
**Future work:**
Rework overload resolution to preserve additional information of
matching iterator overloads for calls up to the point where the
iterator-requiring context is established, to avoid re-sem in
`prepareOperand`.
Currently there's no good channel to store that information. Nodes can
get rewritten, TCandidate doesn't live long enough, storing in Context
or some side-table raises the question how to properly key that info.
(cherry picked from commit be29bcd402)
51 lines
1.1 KiB
Nim
51 lines
1.1 KiB
Nim
discard """
|
|
action: "run"
|
|
"""
|
|
|
|
import std/[assertions, options, strutils]
|
|
from std/sequtils import toSeq
|
|
|
|
# block: # TODO: make iterable accept closure iterators?
|
|
# template mymap[T, U](s: iterable[T], f: proc(x: T): U): untyped =
|
|
# let res = iterator (): U =
|
|
# for val in s:
|
|
# yield f(val)
|
|
# res
|
|
|
|
# proc foo(x: string): string = x & "0"
|
|
|
|
# let a = "1\n2\n3\n4".splitLines().mymap(foo).toSeq()
|
|
# echo a
|
|
# echo typeof(a)
|
|
|
|
block splitIterable: # #22098
|
|
template collect[T](it: iterable[T]): seq[T] =
|
|
var res: seq[T] = @[]
|
|
for x in it:
|
|
res.add x
|
|
res
|
|
|
|
const text = "a b c d"
|
|
let words = text.split.collect()
|
|
doAssert words == @["a", "b", "c", "d"]
|
|
|
|
block optionElements:
|
|
iterator its(_: int; default: Option[string] = none(string)): Option[string] =
|
|
yield some("x")
|
|
|
|
var fromCall = none(string)
|
|
for x in its(0):
|
|
fromCall = x
|
|
doAssert fromCall == some("x")
|
|
|
|
var fromDot = none(string)
|
|
for x in 0.its:
|
|
fromDot = x
|
|
doAssert fromDot == some("x")
|
|
|
|
block closureIteratorCallsStayCallable:
|
|
let next = iterator (): string =
|
|
yield "x"
|
|
|
|
doAssert next() == "x"
|