mirror of
https://github.com/nim-lang/Nim.git
synced 2026-04-21 14:55:24 +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.
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"
|