Skip to content

drive for loops with user-defined iterators (next protocol)#203

Merged
kacy merged 1 commit into
mainfrom
feat/iterator-protocol
May 27, 2026
Merged

drive for loops with user-defined iterators (next protocol)#203
kacy merged 1 commit into
mainfrom
feat/iterator-protocol

Conversation

@kacy
Copy link
Copy Markdown
Owner

@kacy kacy commented May 27, 2026

summary

a struct with a next() -> T? method is now iterable. for x in it: calls
next() each iteration, binds x to the yielded value, and stops when next()
returns none:

struct Counter:
    cur: Int
    hi: Int

impl Counter:
    fn next() -> Int?:
        if self.cur >= self.hi:
            return none
        v := self.cur
        self.cur = self.cur + 1
        return v

for x in Counter(0, 5):   # 0 1 2 3 4

break/continue and the optional value, index binding work (the index is
the 0-based position). existing list / string / map / set / range loops are
unchanged.

this is the consumption half of the iterator protocol. it builds on struct
field assignment (#202) for the mutable cursor, and the lazy range / map /
filter adapters land on top of it next.

how

  • checker: iterator_element_type derives the loop variable's type from the
    struct's next() return (the optional's inner type); check_for_statement
    falls back to it for non-collection iterables.
  • ir emitter: ir_emit_for_iterator_stmt drives the loop by calling
    next(), branching on the optional's is_some flag (field 0) and reading the
    value (field 8), reusing the existing break/continue label scaffolding.

what was tested

  • new regression tests/cases/test_for_iterator.pith: Int iterator sum,
    break/continue, value, index binding, and a String-element iterator
  • existing for-loops over list/string/map/range confirmed unchanged
  • full suite: regressions (96), invalid checker, examples native + self-hosted
    (83 each), and bootstrap fixed point — all 0 failures

design note

the iterator detection reads a module-global map. The same read inlined into
the larger check_for_statement returned stale results — a codegen sensitivity
in the self-hosted compiler with module-global access in big functions — so it
lives in a small dedicated helper, which compiles correctly.

a struct with a next() -> T? method is now iterable: for x in it calls
next() each iteration, binding x to the yielded value and stopping when
next() returns none.

    struct Counter:
        cur: Int
        hi: Int
    impl Counter:
        fn next() -> Int?:
            if self.cur >= self.hi:
                return none
            v := self.cur
            self.cur = self.cur + 1
            return v

    for x in Counter(0, 5):   # 0 1 2 3 4

break/continue and the optional value, index binding work; existing list,
string, map, set and range loops are unchanged. this is the consumption
half of the iterator protocol — lazy range/map/filter adapters build on it.

checker: iterator_element_type derives the loop variable's type from the
iterator's next() return. ir emitter: ir_emit_for_iterator_stmt drives the
loop via next(), reading the is_some flag and value out of the optional.

the iterator detection reads a module-global map; it's isolated in a small
helper because the same read inside the larger check_for_statement returned
stale results (a codegen sensitivity in the self-hosted compiler).
@kacy kacy merged commit d15c782 into main May 27, 2026
2 checks passed
@kacy kacy deleted the feat/iterator-protocol branch May 27, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant