What's Done, What's Changed, and What's Next
In my last post I went in depth on my work at Mozilla, specifically my implementation of TC39 proposals and my current work on the Range Proposal. What was remaining was setting up BigInt
support and setting a proper Prototype for the iterator returned by Iterator.range
.
My implementation followed the specification, ie - create a separate prototype that inherits from the iterator, along with the necessary implementations for toString
and next
.
Background
Initially, I had an IteratorRangeGenerator*
function (ie, step 18 of the Range proposal), that when called doesn't execute immediately, but returns a generator object which follows the iterator protocol. Inside the generator function you have yield
statements which represents where the function suspends its execution and provides value back to the caller.
The generator will pause at each yield
, and will not continue until the next method is called on the generator object that is created.
The NumericRangeIteratorPrototype
is the object that holds the iterator prototype for the Numeric range iterator.
The
next()
method is added to theNumericRangeIteratorPrototype
, when you call thenext()
method on an object created fromNumericRangeIteratorPrototype
, it doesn't directly return a value, but it makes the generator yield the next value in the series, effectively resuming the suspended generator.The first time you invoke
next()
on the generator object created viaIteratorRangeGenerator
, the generator will run up to the firstyield
statement and return the first value.When you invoke
next()
again, theNumericRangeIteratorNext()
will be called. This method usesGeneratorResume(this)
, which means the generator will pick up right where it left off, continuing to iterate the next yield statement or until iteration ends.
Generator Alternative
After discussions with my mentors, I transitioned from a generator-based implementation to a more efficient slot-based approach. This change involved defining slots to store the state necessary for computing the next value. The reasons included:
- Efficiency: Directly managing iteration state is faster than relying on generator functions.
- Simplified Implementation: A slot-based approach eliminates the need for generator-specific handling, making the code more maintainable.
-
Better Alignment with Other Iterators: Existing built-in iterators such as
StringIteratorPrototype
andArrayIteratorPrototype
do not use generators in their implementations.
Final Implementation
The final approach stores state in reserved slots rather than relying on generator suspension. This change better fits SpiderMonkey's Iterator architecture and aligns with other built-in iterators such as StringIteratorPrototype
and ArrayIteratorPrototype
.
By moving away from generators, the implementation now:
- Operates purely on numbers without additional complexity.
- Avoids cleanup operations, simplifying state management.
- Uses a dedicated prototype, ensuring consistency with other iterators.
Adding BigInt Support
Implementing BigInt
support was straightforward from a specification perspective, but I encountered two blockers:
1. Handling Infinity Checks Correctly
The specification ensures that start is either a Number
or a BigInt
in steps 3.a and 4.a. However, step 5 states:
If start is +∞ or -∞, throw a RangeError.
Despite following this, my implementation still threw an error stating that start must be finite. After investigating, I found that the issue stemmed from using a self-hosted isFinite
function.
The specification requires isFinite
to throw a TypeError
for BigInt, but the self-hosted Number_isFinite
returns false
instead. This turned out to be more of an implementation issue than a specification issue.
See Github discussion here.
Fix: Explicitly check that start is a number before calling isFinite:
// Step 5: If start is +∞ or -∞, throw a RangeError.
if (typeof start === 'number' && !Number_isFinite(start)) {
ThrowRangeError(JSMSG_ITERATOR_RANGE_START_INFINITY);
}
After making this adjustment, the implementation correctly handled infinite values.
2. Floating Point Precision Errors
When testing floating-point sequences, I encountered an issue where some decimal values were not represented exactly due to JavaScript's floating-point precision limitations. This caused incorrect test results.
There's a GitHub issue discussing this in depth. I implemented an approximatelyEqual function to compare values within a small margin of error.
Fix: Using approximatelyEqual in tests:
const resultFloat2 = Array.from(Iterator.range(0, 1, 0.2));
approximatelyEqual(resultFloat2, [0, 0.2, 0.4, 0.6, 0.8]);
This function ensures that minor precision errors do not cause test failures, improving floating-point range calculations.
This project has been an incredible learning experience, allowing me to deep-dive into SpiderMonkey's internals and improve my understanding of TC39 proposals. I'm excited to continue refining this work and collaborating with the team to ensure an optimal implementation for Iterator.range.
Stay tuned for further updates!
You may also like:
Navigating TC39 Proposals: From Error Handling to Iterator.range
Decoding Open Source: Vocabulary I've Learned on My Outreachy Journey
Top comments (2)
Beautifully written with illustrations. Weldone Serah.
Thank you.