Skip to content

Builtin iterator methods without the default library: a.map(f).map(g) - #443

Merged
ASDAlexander77 merged 1 commit into
mainfrom
builtin-iterator-methods
Oct 2, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
builtin-iterator-methods

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Part of #442: the first of its two bugs. With --no-default-lib, a.map(f).map(g) failed to compile under every memory model.

Cause

Without the default library, an array's map, filter, forEach, every and some are MLIRGen builtins. map and filter build a generator and return it, and a generator had none of these methods. With the default library the same code works: its namespace __Iterator gives every iterator these methods as extension functions.

Change

  • Any iterator gets the builtins. An iterator here is any object whose next() returns {value, done}: the result of map or filter, or a generator of your own.
    • The new fallback is in mlirGenPropertyAccessExpressionBaseLogic, and applies only under enableBuiltins && noDefaultLib.
    • It reuses the array builtins unchanged: each one is already a for...of over its receiver.
    • It runs after extension functions, so a user's own map still wins, as it does with the default library.
  • reduce stays array-only. Its builtin is a generic function that takes T[].
  • Refactors:
    • The builtin's symbol construction is moved out of MLIRPropertyAccessCodeLogic::Array into CustomMethod(elementType), so arrays and iterators share it.
    • hasIterator now wraps a new getIteratorValueType, which returns next()'s value type.
  • A second bug the fix exposed: in one expression, a.map(f).map(g) then failed with "redefinition of symbol ... .next".
    • The generator was named after where its call starts, and both calls start at the same place.
    • LocationHelper::getSpan has no real span end to tell them apart.
    • The generator is now also named after its callback's location. A location gives the same name in MLIRGen's dummy and real runs, which a counter might not.

Not in this PR

Verification

Tests

tests/00iterator_methods.ts covers:

  • a two-call and a three-call chain in one expression;
  • forEach through a variable;
  • every and some, both true and false;
  • the methods on a generator of your own;
  • next() on a map result;
  • strings.

It is registered for AOT and JIT, and in TSLANG_CORPUS, so it also runs under rc and none.

🤖 Generated with Claude Code

With --no-default-lib, an array's map/filter/forEach/every/some are
builtins, and what map and filter give is a generator, which had none of
them: `a.map(f).map(g)` failed to compile under every model. An iterator
(anything whose next() returns {value, done}) now has the same builtins,
as the default library's __Iterator gives them; each is a for...of over
its receiver. The fallback comes after extension functions, so a user's
own `map` still wins. reduce stays array-only: its builtin takes `T[]`.

The generator map/filter build was named by where the call starts, and
`a.map(f).map(g)`'s two calls start together: "redefinition of symbol
... .next". It is named by its callback's location too.

Part of #442.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77

Copy link
Copy Markdown
Owner Author

Rechecked -mm=own with #441 merged. This branch merges into main cleanly; I built the merge locally and ran each case with --no-default-lib --di.

  • Compiles and runs correctly: a single builtin on a generator, e.g. const m = a.map(f); m.forEach(print) (prints 2 4 6), g().map(f), a.map(f).every(p).
  • Rejected, as it should be: reassigning the source while the generator is in use, e.g. const m = a.map(f); a = [7, 8, 9]; m.forEach(print). The error is "'m' borrows 'the generator's source' but is used here after 'the generator's source' is released or overwritten", for both map and filter.
  • Still rejected, by -mm=own phase 7b: a generator over a parameter's block borrows it; .map and .filter #441's documented limits (spec §19.5):
    • Chains, e.g. a.map(f).map(g) or m.map(g): "'m' borrows its generator's arguments and cannot be stored, returned or captured". The outer map's closure box captures the inner generator. Accepting this would need a box to inherit its contents' bound, which is new inference work.
    • A capturing callback on a generator, e.g. m.forEach(x => { s += x; }): "takes a second reference".

So 00iterator_methods.ts stays in the gc/rc/none tiers only, and needs no change for own.

@ASDAlexander77
ASDAlexander77 merged commit d59302b into main Oct 2, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the builtin-iterator-methods branch October 2, 2026 15:25
ASDAlexander77 added a commit that referenced this pull request Oct 4, 2026
…n too (#442) (#499)

`if (o.foo)` on an object with no `foo` reported only "the condition
has no value", and `const v = o.foo` only "can't resolve name: v",
neither naming the property. The failure happens in the run that
infers the function's return type (allowPartialResolve), where
mlirGenPropertyAccessExpression gives success with no value and no
message, so that a sibling method an object literal has not registered
yet can resolve in a later run; the compile then fails in that run on
what the caller makes of no value, and the run that would have said
"Can't resolve property" is never reached.

The access now emits "Can't resolve property 'foo' of type {x:s32}" in
that run as well, still returning success. A message is shown only if
the module fails to compile, and each retry cycle starts with none, so
a property that resolves later costs nothing. The caller's own message
("the condition has no value") follows it.

Part 1 of the issue (a.map(f).map(g) without the default library) was
fixed by #443.

Closes #442

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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