Stream: project-inline-asm

Topic: Type restrictions


Amanieu (Jan 24 2020 at 02:41, on Zulip):

It just occurred to me that we don't really need to care about what types a particular register class accepts, e.g. the SSE register class accepts 128-bit vectors but not i128.

Amanieu (Jan 24 2020 at 02:42, on Zulip):

We can just transmute any operand to a type suitable for the backend, in which case we only need to care about the register size, not the actual types.

Amanieu (Jan 24 2020 at 02:50, on Zulip):

With that in mind, should we revisit the rules for what types a register class can accept? Currently only integer/float/pointer primitive types are allowed. We could extend that to any type as long as it can fit in the register.

Amanieu (Jan 24 2020 at 02:51, on Zulip):

Let's take RISC-V's f register class which only accepts f32 and f64 as an example: should we allow an i64 operand? An i8 operand? A (u8, u8, u8) operand?

Lokathor (Jan 24 2020 at 05:05, on Zulip):

not a tuple

Lokathor (Jan 24 2020 at 05:06, on Zulip):

more generally, anything that you should not actually transmute to the register's real type should not work here.

Amanieu (Jan 24 2020 at 14:58, on Zulip):

What about struct S(u16, u16)? Should we allow this to fit in a 32-bit register?

Amanieu (Jan 24 2020 at 14:59, on Zulip):

The current RFC does allow smaller values to go into a larger register. The most common case is putting a f32 value into a 64-bit float register which supports both f32 and f64.

Amanieu (Jan 24 2020 at 15:31, on Zulip):

Hmm, I'm suddenly having an urge to redesign the register classes...

Lokathor (Jan 24 2020 at 15:31, on Zulip):

transmuting an S value to u32 would be UB in normal Rust. I can't see any possible path for allowing it into inline asm without also effectively stabilizing what the transmute would be in other contexts (since those other contexts could just use an inline asm move to do the conversion).

Any automatic same-size conversion will have to approximately follow the safe-transmute rules. ideally we could start with a few specific auto-conversions (such as f32/u32/i32) and then leave a "future extension" note for general "whatever safe transmute would let you do can now automatically be done here".

for different size conversions, having automatic sign extension or f32->f64 or whatever seems a lot easier to grasp without accidentally affecting other parts of the language. we would still need some restrictions.

Amanieu (Jan 24 2020 at 15:33, on Zulip):

The current rule is that if you pass in a smaller type, the upper bits contain an undefined value.

Amanieu (Jan 24 2020 at 15:34, on Zulip):

This works well for x86, where instructions that write to al leave the other bits unchanged, while on other architectures it clears the upper bits.

Amanieu (Jan 24 2020 at 15:34, on Zulip):

Also we want to be able to pass SIMD vectors into inline asm, for example for SSE registers.

Lokathor (Jan 24 2020 at 15:35, on Zulip):

sounds initially fine, and maybe fixable down the line too

Amanieu (Jan 24 2020 at 15:40, on Zulip):

Why is transmuting (u16, u16) to u32 UB? Is it just because of the undefined field order? (i.e. it would be fine if it was repr(C))

Lokathor (Jan 24 2020 at 15:45, on Zulip):

correct

Amanieu (Jan 24 2020 at 15:45, on Zulip):

From an implementation point of view, it feel simpler to just allow arbitrary transmutes and only checking the size of the type during type checking.

Lokathor (Jan 24 2020 at 15:48, on Zulip):

yes it would be simpler to implement, but you'd effectively be enabling a lot of accidental UB, so probably that is a poor plan.

if anything, stick to a restricted list of primitives and relax the restrictions later.

rkruppe (Jan 24 2020 at 16:17, on Zulip):

I would go even further and question the wisdom of allowing smaller types and making the highest bits undefined. Experience from C shows that people keep getting it wrong (assuming it's guaranteed to either sign- or zero-extended although neither GCC nor Clang make any guarantees about it) and it appears to work for a long time until the compiler changes and something breaks, forcing people to write patches that explicitly do the intended extension. Just this week I saw another example of this with https://www.openwall.com/lists/musl/2020/01/15/2

Josh Triplett (Jan 24 2020 at 18:44, on Zulip):

@Amanieu Just to confirm, you're suggesting manual transmutes, not automatic conversions, right?

Josh Triplett (Jan 24 2020 at 18:45, on Zulip):

I think people should be able to transmute u64 to f64, for instance, and feed that in to a floating point register. But by default feeding in a u64 should give a type error, to help reduce common errors.

Josh Triplett (Jan 24 2020 at 18:46, on Zulip):

This certainly addresses my concern about vector types vs u128 though; I don't mind having to transmute there.

Amanieu (Jan 24 2020 at 19:26, on Zulip):

@Josh Triplett Actually I was thinking of allowing any type, as long as it has the same size as the target register.

Amanieu (Jan 24 2020 at 19:28, on Zulip):

@rkruppe That's a very good point, but at the same time it can make asm! very unergonomic to use. For example you would have to explicitly cast any integer constants to u64 before passing them into a register (since integer default to i32).

Amanieu (Jan 24 2020 at 19:29, on Zulip):

Same with floats: it would suck to have to zero-extend float types to 128 bits to put them in a SSE register

Lokathor (Jan 24 2020 at 19:36, on Zulip):

if a register only accepts u64 then the literal in that place should be forced to u64.

same as

let a: u64 = 5;

makes 5 be a u64 literal

rkruppe (Jan 24 2020 at 19:48, on Zulip):

"Sizes must match exactly or it's an error" isn't the only way to resolve this. There could be rules for (register class, type) combinations where it's obvious which extensions is the right one, so that only obscure things like u64 in an XMM register need manual extension. There could also be an explicit flag on the operand for opting into the "upper bits can be anything" behavior (LLVM calls this anyext which is a good enough name IMO).

rkruppe (Jan 24 2020 at 19:51, on Zulip):

Your point about integer literals defaulting to i32 is even more reason to move away from "upper bits unspecified", because those two things together form a huge footgun.

Josh Triplett (Jan 24 2020 at 20:05, on Zulip):

@Amanieu I feel like "size matches" alone would be too error-prone, and not so common that a transmute is too much to ask.

Josh Triplett (Jan 24 2020 at 20:07, on Zulip):

If we can offer a bit of type checking, like making sure a value for rax is a u64 or i64 rather than an f64, that seems more likely to catch an error than to flag code that really did want to do that. And one transmute handles the latter case.

Josh Triplett (Jan 24 2020 at 20:08, on Zulip):

If someone has code that repeatedly has that pattern, they could always make a wrapper macro or a wrapper function.

Lokathor (Jan 24 2020 at 20:21, on Zulip):

in fact f64 and f32 already have such a method https://doc.rust-lang.org/std/primitive.f64.html#method.to_bits

Amanieu (Jan 24 2020 at 22:26, on Zulip):

So how about this then:
(x86_64)

Amanieu (Jan 24 2020 at 22:28, on Zulip):

Note that for the f32 and f64 case, the upper bits are undefined.

Lokathor (Jan 24 2020 at 22:53, on Zulip):

i hesitate with f32/f64 into simd with unknown high bits. I feel like since the simd types are in core anyway, we should just accept those types

Lokathor (Jan 24 2020 at 22:53, on Zulip):

and then people can use intrinsics to get the bits they want

Amanieu (Jan 25 2020 at 01:23, on Zulip):

Here's what I think I'm going to go with:

Lokathor (Jan 25 2020 at 01:26, on Zulip):

we've worked pretty hard to eliminate undefined data from escaping on accident. Still seems like a potential footgun.

Amanieu (Jan 25 2020 at 01:27, on Zulip):

Disallowing it completely could lead to unnecessary zero-extensions.

Amanieu (Jan 25 2020 at 01:47, on Zulip):

Hmm there's also the issue of what exact is a SIMD vector. For example this is allowed:

#[repr(simd)] struct u8x3(u8, u8, u8);
Lokathor (Jan 25 2020 at 01:58, on Zulip):

use the core::arch types

Amanieu (Jan 25 2020 at 02:22, on Zulip):

Except it's a bit difficult to check for those from within the compiler...

Amanieu (Jan 25 2020 at 02:23, on Zulip):

Hence why I was thinking of simply allowing any type as long as it fit in the register

rkruppe (Jan 25 2020 at 10:14, on Zulip):

Warnings about things that require some care but can be 100% OK and desirable are somewhat self-defeating: a project that uses the construct in question (here, relying on anyext of smaller types) more than a couple times is incentivized to disable the warning at module/crate scope, and from that point forward nobody working on the project will see the warning any more.

rkruppe (Jan 25 2020 at 10:17, on Zulip):

If you argue that anyext (instead of zero/sign extension) is useful, then I think it should require an explicit opt-in on the operand in question (as I suggested before), so that you always have a reminder of the pitfall when reading, writing, and modifying code.

Amanieu (Jan 25 2020 at 11:29, on Zulip):

@rkruppe You make a very good point. How about something like this:


rkruppe (Jan 25 2020 at 11:36, on Zulip):

IMO that is still too ambiguous about what happens to the upper bits. In C, I see mistaken assumptions about the kind of extension that is implied, rather than using smaller types by mere accident. How about this? (Exact keyword up for bike-shedding, as long as it makes clear it's neither zext nor sext)

asm!("", in(reg anyext) 42i8)
bjorn3 (Jan 25 2020 at 11:42, on Zulip):
asm!("", in(reg undefext) 42i8)
Josh Triplett (Jan 26 2020 at 17:01, on Zulip):

This seems partly caused by our unification of register sizes in register definitions. We don't really seem to distinguish between rax, eax, ax, and al.

Josh Triplett (Jan 26 2020 at 17:02, on Zulip):

On platforms that have a concept of different sizes for registers like that, we don't need to worry as much, I think.

Josh Triplett (Jan 26 2020 at 17:03, on Zulip):

It's expected that if you feed a u16 into ax you should use it as ax, or take care before using it as eax/rax.

Amanieu (Jan 30 2020 at 15:23, on Zulip):

I just pushed a big rework of how register classes work. It should also solve this issue.

Amanieu (Jan 30 2020 at 15:24, on Zulip):

The basic idea is to allow under-sized values, but then you must use template modifiers to only access the low bits of that register.

Josh Triplett (Jan 30 2020 at 20:19, on Zulip):

Interesting! I'll take a look. The template modifiers will make sure that you substitute in names like "ax" or "al" instead of "rax"?

Josh Triplett (Jan 30 2020 at 20:19, on Zulip):

(And then if you want to use the whole register, you also need to declare a whole-register clobber?)

Josh Triplett (Jan 30 2020 at 20:20, on Zulip):

Presumably you could also as u64 if you want, and then Rust will do the movzx or movsx for you?

Amanieu (Jan 30 2020 at 20:37, on Zulip):

Yes, you can sign-extend or you can use template modifiers. Either will disable the warning.

Amanieu (Jan 30 2020 at 20:39, on Zulip):

*sign/zero-extend manually

Amanieu (Jan 30 2020 at 20:59, on Zulip):

Hmm now that I think about it I forgot to take implicit registers into account (those not used in the template string)

Josh Triplett (Jan 30 2020 at 21:11, on Zulip):

@Amanieu Oh, good point. An implicit eax or al, for instance, where the register isn't named so there's nowhere to apply a modifier?

rkruppe (Jan 30 2020 at 21:16, on Zulip):

I am also skeptical that "apply the default modified on every single use" is a good way to opt into "upper bits undefined" behavior where it's needed (particular for things like scalars in vector registers). It doesn't makes clear what's actually happening, and it seems to make the asm way more annoying to read, write and modify than it has to be.

Josh Triplett (Jan 30 2020 at 21:19, on Zulip):

@rkruppe It does seem preferable to be able to write the modifier once, on the in or out, rather than repeatedly as a modifier.

Amanieu (Jan 30 2020 at 21:34, on Zulip):

@rkruppe What would you suggest instead?

Amanieu (Jan 30 2020 at 21:35, on Zulip):

I'm a bit wary of having the syntax become too complicated, especially for these sorts of edge cases that 90% of people won't care about.

Josh Triplett (Jan 30 2020 at 21:39, on Zulip):

@Amanieu "input this 32-bit value as a 32-bit register" doesn't seem like an edge case.

Josh Triplett (Jan 30 2020 at 21:39, on Zulip):

And I would much rather have Rust figure out that it's a 32-bit value and use a 32-bit register automatically, rather than forcing me to specify a modifier explicitly.

Josh Triplett (Jan 30 2020 at 21:41, on Zulip):

Also, if I'm feeding a 32-bit value into a 64-bit register, perhaps because the platform doesn't have a concept of "32-bit register", then there should be some way to avoid a gratuitous zero-extension instruction when I'm only going to use the low 32 bits.

Josh Triplett (Jan 30 2020 at 21:41, on Zulip):

Doing so might require some kind of notation, but it shouldn't be difficult...

rkruppe (Jan 30 2020 at 21:42, on Zulip):

I can appreciate that you don't want to add more bells and whistles but re-purposing operand modifiers has so many serious problems that it's worth adding a new piece of syntax IMO. It also won't actually affect people who don't have to care about this, unless they specifically go through the reference and try to understand the whole syntax of asm! and chase down what every part of the syntax means. (And those people would be reading the part about size mismatch warnings silenced by explicit modifiers too, if we went that route.)

Amanieu (Jan 30 2020 at 23:06, on Zulip):

I would really prefer if the default register formatting did not depend on the type and instead defaults to the full register width (rax or eax depending on the arch). Similarly, the register formatting does not depend on what name you chose when specifying an explicit register (in("rax") val and in("al") val are treated as equivalent).

Amanieu (Jan 30 2020 at 23:07, on Zulip):

In comparison, the GCC behavior when no template modifier is used is to only look at the type passed in. It also ignores the register name chosen for explicit registers.

rkruppe (Jan 30 2020 at 23:09, on Zulip):

As far as I can tell nobody proposed anything like that?

Amanieu (Jan 30 2020 at 23:11, on Zulip):

And I would much rather have Rust figure out that it's a 32-bit value and use a 32-bit register automatically, rather than forcing me to specify a modifier explicitly.

rkruppe (Jan 30 2020 at 23:13, on Zulip):

Ah, I missed that part, sorry.

Amanieu (Jan 31 2020 at 14:43, on Zulip):

By the way, here's what the register definitions currently look like in the code: https://github.com/Amanieu/rust/commit/0ca6992771eeb5096ed9d67d4424099067aa378c

Josh Triplett (Jan 31 2020 at 16:57, on Zulip):

@Amanieu Defaulting to the full register width seems like it would make any type smaller than the register width awkward to use.

Last update: May 27 2020 at 22:35UTC