Skip to content

Add bit shifting built-ins #13001

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

spacekitteh
Copy link

Motivation

I want to shift bits without my nixos configuration taking a minute just to evaluate

Context

Partially addresses #13000 by adding bit shifting operations


Add 👍 to pull requests you find important.

The Nix maintainer team uses a GitHub project board to schedule and track reviews.

@spacekitteh spacekitteh force-pushed the add-bitshift-primops branch from 6271ec7 to 4eed1b3 Compare April 10, 2025 23:54
Copy link
Member

@lf- lf- left a comment

Choose a reason for hiding this comment

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

Please address the design issue that these functions don't have specified and safe edge case behaviour in their specification.

@RaitoBezarius iirc we didn't want these in lix at all (though they aren't of terribly high maintenance cost), but do you remember more context? Also, should these be namespaced somehow rather than in the top level builtins attrset?

issues: [13000]
---

Added left and right bit shifting built-ins.
Copy link
Member

Choose a reason for hiding this comment

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

Please give an example.

auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitShiftLeft");
auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitShiftLeft");

v.mkInt(i1.value << i2.value);
Copy link
Member

Choose a reason for hiding this comment

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

This contains UB if i2 is too large. Do it some other way that is safe. Maybe there's an intrinsic.

Copy link
Member

Choose a reason for hiding this comment

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

A brief look at the rust source code reveals that you have to check that i2 is in range: https://doc.rust-lang.org/stable/src/core/num/int_macros.rs.html#1222. Probably don't have to do anything else. You should add this to the checked-arithmetic library used by the other arithmetic.

You also have to make sure the shift amount is positive. BUT this reveals that perhaps the correct design is a single bitShift builtin, see haskell: https://hackage.haskell.org/package/base-4.21.0.0/docs/GHC-Bits.html#v:shift

Choose a reason for hiding this comment

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

Personally I think it would be easier and more understandable if we split the two implementations and enforce a positive shift amount. I think most of the time you know beforehand which direction you want to shift and this way we'll catch errors if that assumption is broken.

Copy link
Member

Choose a reason for hiding this comment

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

The reason I'm suggesting one of them is that it has clear and continuous behaviour over its domain: it's got less partial behaviour.

If there's two, negative shift amounts are always illegal on both.

Choose a reason for hiding this comment

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

Yeah good point. But then we might have to implement a logical right shift not the current arithmetic one. Else it might lead to a scenario where shift (shift x 1) -1 != x

Copy link
Member

Choose a reason for hiding this comment

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

Right. Do we want to allow shifting left into the sign bit to change the sign of a number? i dunno! but these are the questions that need to be answered in the spec.

Builtins are permanent parts of derivation ABI that cannot be changed since we don't have a good language versioning story.

The lix position is that builtins need:

  1. fully specified behaviour and a spec
  2. knowledge of edge cases and how they're handled and tests
  3. demonstrated need (though that is somewhat shaky here, doing IFD to achieve this probably isn't a great answer either)
  4. consideration of back and forward compatibility: if a built-in has an attrset argument how do you detect supported features in it? if it could accept more args in the future how is it accommodated?

We need to write these rules down somewhere because there's multiple sunk built-in projects that regrettably wasted a lot of people's time.

Choose a reason for hiding this comment

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

Honestly after talking with some other people about it I'm less convinced this builtin is strictly needed than before. I still think it's nice to have and would simplify things a bit, but I can see that there is no way of implementing it currently that wouldn't leak the internal integer representation, or wouldn't have some weird/unintuitive edge case behavior.
I'm still open to try and implement it with a proper spec, tests and anything else that's needed, but if the powers that be understandably say they rather not have another semi useful, somewhat obscure niche builtin, I needn't spend more time on this.

So I guess the main question is if this would contain proper specified behavior, handling of edge cases and tests, would you or anyone else be open to merge it or would you rather not?

auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitShiftRight");
auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitShiftRight");

v.mkInt(i1.value >> i2.value);
Copy link
Member

Choose a reason for hiding this comment

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

this probably contains UB if i2 is too large

.name = "__bitShiftLeft",
.args = {"e1", "e2"},
.doc = R"(
Return the integer *e1* shifted left by *e2*.
Copy link
Member

Choose a reason for hiding this comment

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

Insufficiently specific, what happens with negative operands? Are those invalid? What about negative shift values?

I suspect the implementation here does UB with them.

@PatrickDaG
Copy link

Didn't know these were intentionally not in lix so I opened https://gerrit.lix.systems/c/lix/+/2975 yesterday. I am very much of the opinion that these are a good idea and needed in the language. Some things I want to do are just impossible without (mainly arithmetic on IP/MAC addresses). If possible I would propose to try and coordinate these two requests to keep the two implementations on par. I'll try and implement the suggestions from here over on the lix CL as well.

@PatrickDaG
Copy link

For comparison I've implemented a logical left/right shift using current nix/lix https://gist.github.com/PatrickDaG/c075f5ef8a7cba59b0999d8a0dd7a7ce. As far as I can tell it works and perfectly mirrors C shift semantics on uint64_t.

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

Successfully merging this pull request may close these issues.

3 participants