Skip to content

erts: Add guard BIF erlang:is_between/3 #9995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

lucioleKi
Copy link
Contributor

Add guard BIF erlang:is_between/3 according to EEP-16. This BIF takes in 3 parameters, Term, LowerBound, and UpperBound. It returns true if Term, LowerBound, and UpperBound are all integers, and LowerBound =< Term =< UpperBound; otherwise, it returns false.

Failure: badarg if LowerBound or UpperBound does not evaluate to an integer.

Example:

1> is_between(2, 1, 10).
true
2> is_between(11, 1, 10).
false
3> is_between(1, 1.0, 10.0).
** exception error: bad argument
     in function  is_between/3
        called as is_between(1,1.0,10.0)

The PR is ready for review, but it can't be merged until it's approved by an OTB meeting.

Add guard BIF `erlang:is_between/3` according to EEP-16. This BIF
takes in 3 parameters, `Term`, `LowerBound`, and `UpperBound`.
It returns `true` if `Term`, `LowerBound`, and `UpperBound` are
all integers, and `LowerBound =< Term =< UpperBound`; otherwise,
it returns false.

Failure: `badarg` if `LowerBound` or `UpperBound` does not evaluate to
an integer.

Example:

````
1> is_between(2, 1, 10).
true
2> is_between(11, 1, 10).
false
3> is_between(1, 1.0, 10.0).
** exception error: bad argument
     in function  is_between/3
        called as is_between(1,1.0,10.0)
````

Co-authored-by: Björn Gustavsson <[email protected]>
Co-authored-by: John Högberg <[email protected]>
@lucioleKi lucioleKi self-assigned this Jun 25, 2025
@lucioleKi lucioleKi added the team:VM Assigned to OTP team VM label Jun 25, 2025
Copy link
Contributor

github-actions bot commented Jun 25, 2025

CT Test Results

     7 files     640 suites   3h 14m 10s ⏱️
 6 168 tests  5 817 ✅ 350 💤 1 ❌
12 557 runs  12 119 ✅ 437 💤 1 ❌

For more details on these failures, see this check.

Results for commit a049aef.

♻️ This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@michalmuskala
Copy link
Contributor

I think the behaviour with regards to types seems somewhat inconsistent.

First, I find it a bit odd, it works with just integers, but even then, for bounds it raises an error, if the value is not an integer, while for the middle value it just returns false - I think the behaviour should be consistent.

I also wonder, in terms of implementation, if it wouldn't be better, if this was desugared in core to the basic comparisons - the compiler has quite a bit of infrastructure of optimising comparisons that we might be losing here.

@lucioleKi
Copy link
Contributor Author

lucioleKi commented Jun 26, 2025

It works only with integers because this has the widest use case. is_between/3 is here to prevent users from reinventing the wheel, which is chained comparison for integers, like 0<=X<=1024. Chained comparison for floats and other types are much more rarer than integers. If we relax the input type, is_between/3 becomes less useful, as in most cases, the user wants to be sure that the chained comparison only contains integers.

Having different error behaviors for non-integer bounds and non-integer argument makes the BIF convey more meaning in its result. is_between/3 returns false means the bounds are valid, but the tested argument does not lie between the bounds. Exception means that the comparison can't be done at all. At least one of the bounds cannot work for any tested argument. Another BIF that does something similar is is_function(Term, Arity). Throwing an exception in certain cases makes the BIF's intention clearer.

About the implementation: A BIF must have a C implementation so that it can work in scenarios like apply/3. For the cases where we can safely rewrite is_between/3 or provide type info in the compiler, this implementation already does that, so that the JIT can generate efficient code. (I actually wrote a version that rewrites is_between/3 in v3_core, and John wrote a version that does magic in JIT. Many different approaches can achieve the same result.)

@michalmuskala
Copy link
Contributor

Having different error behaviors for non-integer bounds and non-integer argument makes the BIF convey more meaning in its result. is_between/3 returns false means the bounds are valid, but the tested argument does not lie between the bounds. Exception means that the comparison can't be done at all. At least one of the bounds cannot work for any tested argument.

That's not true for something like is_between(5.0, 1, 10) will just return false - this seems very confusing. In my mind, if we want the function to work with just integers, it should do that - work with just integers for all arguments.

This actually raises another concern for me - the order of arguments of Term, LowerBound, UpperBound seems quite awkward and error prone. Why not LowerBound, Term, UpperBound - this seems like a more natural order to express something is between two values.

About the implementation: A BIF must have a C implementation so that it can work in scenarios like apply/3.

It could work with a pure-erlang implementation in the erlang module for those cases.

@lucioleKi
Copy link
Contributor Author

I don't have much preference about if we should badarg or return false when the first argument is not an integer. The argument order is historical, at least from what I heard from Richard O'Keefe. I'll bring those arguments to our OTB meeting in fall.

Well my statement about C implementation is incorrect, but I don't see how a pure Erlang implementation would be more elegant than the current one. If the BIF never throws exception or there's no JIT, desugaring everything would be nice. But now we have the tradeoff between desugaring the BIF in various contexts differently, or calling the BIF through different routes and desugaring when possible. As we don't lose optimizations either way, I prefer the latter because the structure is neater.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team:VM Assigned to OTP team VM
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants