
May 18, 2026
Most Ruby developers use ranges every day:
(1..5)('a'..'z')(1...)(..10)
They feel lightweight, expressive, and almost deceptively simple.
But under the hood, Ruby’s Range implementation is one of the most optimized and feature-rich parts of CRuby.
Inside range.c, the Ruby core team handles:
- endless ranges
- beginless ranges
- arithmetic sequences
- binary search
- Fixnum optimizations
- Bignum overflow
- float stepping
- string iteration
- symbol iteration
- enumerator integration
All behind a syntax most developers learn on day one.
Let’s open the hood.
How Ruby Stores a Range
Internally, a Range stores only three values:
#define RANGE_SET_BEG(r, v) (RSTRUCT_SET(r, 0, v))#define RANGE_SET_END(r, v) (RSTRUCT_SET(r, 1, v))#define RANGE_SET_EXCL(r, v) (RSTRUCT_SET(r, 2, v))
That means Ruby ranges are essentially:
- begin
- end
- exclude_end?
For example:
(1..5)
becomes conceptually:
begin: 1end: 5exclude_end?: false
While:
(1...5)
becomes:
begin: 1end: 5exclude_end?: true
The implementation is surprisingly compact considering how much behavior Ruby builds on top of it.
Ranges Are Immutable
One subtle detail hidden in range.c:
if (CLASS_OF(range) == rb_cRange) { rb_obj_freeze(range);}
Ruby freezes ranges automatically.
That means this:
r = (1..5)r.frozen?# => true
isn’t accidental it’s a deliberate design choice inside CRuby.
Why?
Because immutable objects:
- are safer
- simplify optimizations
- avoid synchronization problems
- make iteration logic predictable
This is especially important once endless and beginless ranges enter the picture.
Inclusive vs Exclusive Ranges
Ruby internally tracks exclusivity using:
#define EXCL(r) RTEST(RANGE_EXCL(r))
That tiny flag changes iteration behavior everywhere.
Inclusive:
(1..5).to_a# => [1, 2, 3, 4, 5]
Exclusive:
(1...5).to_a# => [1, 2, 3, 4]
Internally, iteration paths constantly branch based on this flag:
if (EXCL(range)) {
One boolean influences:
- iteration
- size
- max
- step
- bsearch
- arithmetic sequences
- array slicing
Fast Paths for Integers
Ruby aggressively optimizes integer ranges.
This section is one of the most important:
else if (FIXNUM_P(b) && FIXNUM_P(e) && FIXNUM_P(step)) { /* fixnums are special: summation is performed in C for performance */
Instead of generic Ruby method dispatch, CRuby runs tight C loops directly.
For example:
(1..1_000_000).each
does not repeatedly call Ruby methods for every iteration.
Ruby executes optimized C loops:
for (long i = FIX2LONG(beg); i < lim; i++) { rb_yield(LONG2FIX(i));}
This dramatically reduces overhead.
Without these fast paths, range iteration would be much slower.
Endless Ranges Are First-Class Citizens
Endless ranges aren’t syntax sugar.
Ruby deeply integrates them into the runtime.
Example:
(1..)
Internally:
if (NIL_P(RANGE_END(range))) {
Ruby literally stores nil as the end boundary.
That affects everything.
Converting to arrays becomes illegal:
(1..).to_a# RangeError
because internally:
rb_raise(rb_eRangeError, "cannot convert endless range to an array");
But iteration works forever:
(1..).each
using dedicated infinite loops:
for (;;)
Ruby even optimizes endless integer iteration separately.
Beginless Ranges Behave Differently
Beginless ranges:
(..10)
also store nil internally but for the beginning.
Some operations become meaningless:
(..10).first# RangeError
because CRuby explicitly rejects it:
rb_raise(rb_eRangeError, "cannot get the first element of beginless range");
This distinction matters:
Range TypeValid(1..) iteration✅(..10).reverse_each✅(..10).first❌(1..).last❌
Ruby carefully defines semantics for each combination.
Range#step Is Far More Advanced Than Most Developers Realize
This is where range.c becomes especially interesting.
Ruby supports:
(1..10).step(2)
negative stepping:
(10..1).step(-2)
float stepping:
(1.0..5.0).step(0.5)
time stepping:
(start_time..end_time).step(3600)
and even string stepping:
('a'..'z').step(2)
Internally, CRuby dynamically detects iteration direction:
if (unit < 0) {
and decides whether iteration should move forward or backward.
Even more interesting:
Ruby preserves backward compatibility for string ranges:
// backwards compatibility behavior for String only
Meaning old Ruby behaviors are intentionally preserved deep inside the runtime.
Arithmetic Sequences
When no block is provided:
(1..10).step(2)
Ruby doesn’t immediately iterate.
Instead, it creates:
Enumerator::ArithmeticSequence
Internally:
return rb_arith_seq_new(...)
This allows lazy-style behavior and advanced slicing tricks:
array[(0..) % 2]
which selects every second element.
Most Rubyists use this feature without realizing an entirely different internal object is being created.
Binary Search Internals Are Surprisingly Sophisticated
Range#bsearch is one of the most technically fascinating parts of range.c.
Ruby supports binary searching over:
- integers
- floats
- endless ranges
- beginless ranges
The float implementation is especially clever.
CRuby maps floats to signed 64-bit integers:
union int64_double { int64_t i; double d;};
Why?
Because binary search fundamentally works best over ordered integers.
Ruby transforms floating-point values into comparable integer representations while preserving ordering semantics.
That’s an advanced systems-level optimization hidden behind:
(0.0..10.0).bsearch
Ruby Carefully Handles Overflow
Large integer ranges transition between Fixnum and Bignum logic.
Example:
/* then switch to Bignum API */
CRuby starts with fast native integer operations, then transparently switches to arbitrary precision arithmetic when limits are exceeded.
This prevents overflow while maintaining performance for common cases.
Most developers never notice the transition happening.
Strings and Symbols Have Dedicated Iteration Logic
Ruby ranges support:
('a'..'z')(:aa..:zz)
These are not generic implementations.
CRuby has dedicated string iteration functions:
rb_str_upto_each
and symbol-specific handling:
sym_each_i
This is why string ranges behave naturally despite strings not being numeric.
Ruby internally uses succ semantics to generate subsequent values.
The Real Lesson from range.c
Ruby’s Range looks tiny on the surface:
1..5
But internally it’s:
- highly optimized
- deeply polymorphic
- overflow-aware
- enumerable-aware
- lazy-compatible
- infinite-range capable
- backward-compatible
range.c is a perfect example of CRuby’s philosophy:
simple syntax backed by serious engineering.
The elegance Ruby developers experience daily exists because the runtime absorbs enormous implementation complexity behind the scenes.
And once you read the source, ranges stop looking like syntax sugar and start looking like one of Ruby’s most sophisticated core abstractions.
