Skip to content

Conversation

sabiwara
Copy link
Contributor

@sabiwara sabiwara commented Sep 4, 2025

Close #14746

The second commit skips calling fun altogether for empty enumerables, which is what is being done for other slicing functions like drop / slice (and which is why the Enum.slice tests were passing).

To be backported.

@sabiwara sabiwara changed the title Fix Enum.take(empty_date_range, -1) Fix infinite loop in Enum.take(empty_date_range, -1) Sep 4, 2025
@sabiwara sabiwara changed the title Fix infinite loop in Enum.take(empty_date_range, -1) Fix infinite loop when enumerating empty Date.Range Sep 4, 2025

defp slice(current, _step, 1, calendar) do
[date_from_iso_days(current, calendar)]
defp slice(_current, _step, 0, _calendar) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say this:

The `start` position is a number `>= 0` and guaranteed to
exist in the `enumerable`. The length is a number `>= 1`
in a way that `start + length * step <= count`, where
`count` is the maximum amount of elements in the enumerable.

So I think this fix is not necessary. Instead we should indeed avoid calling it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine reverting it, but just mentioning that I personally prefer the new version of the code even if not to fix a bug:

  • consistency with the Range impl
  • the recursion feels simpler stopping on the empty list clause, less redundancy (double date_from_iso_days(current, calendar))

But it's really a nit, your call 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you prefer! Ending on 1 can help us catch future bugs though. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want catching, perhaps a guard on > 1 on the other clause could lead to a nicer way to fail than an infinite loop? But that might have an impact on perf, and I'm too lazy to bench it 😄

I was also wondering if the type system could have caught such a bug in a defimpl, but since integers don't have a subset I don't think there's an easy way to catch it at compile-time, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, can’t catch at compile time. I think performance cost is negligible. Pick whatever approach you prefer!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, went with making both Range and Date.Range consistent and added a defensive guard: aef73cb.

Copy link
Member

@josevalim josevalim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we only need the second commit. Ship it!

@sabiwara
Copy link
Contributor Author

sabiwara commented Sep 5, 2025

Also, question: after I merge, should I backport this to 1.18 as well as 1.19?

@sabiwara sabiwara merged commit 4008142 into elixir-lang:main Sep 5, 2025
13 checks passed
@sabiwara sabiwara deleted the date-range-infinite-loop branch September 5, 2025 12:24
@sabiwara
Copy link
Contributor Author

sabiwara commented Sep 5, 2025

Backported to v1.19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent behaviour of %Date.Range{} |> Enum.take/2 with negative amount

2 participants