c3c icon indicating copy to clipboard operation
c3c copied to clipboard

Casting bitstruct to wider base type

Open cbuttner opened this issue 3 weeks ago • 7 comments

bitstruct Foo : ushort {
  bool a;
}

fn void main() {
  Foo foo;
  // Currently
  ulong a = (ulong)(ushort)foo;
  // Potential improvement?
  ulong b = (ulong)foo;
}

cbuttner avatar Dec 04 '25 13:12 cbuttner

Are there any safety issues one should consider here?

lerno avatar Dec 04 '25 13:12 lerno

Explicitly casting wider? Probably not. Explicitly casting to a narrower type is always a safety issue, but also it's already allowed for all the other types. I guess for an enum, with it being sequential and starting at 0, the likelihood of creating problems by casting away significant bits is lower than the other types, but that's about it.

Maybe there could be a rule that if the type is not inline and the cast is narrowing, actually require to cast to the base type (or wider) first? I don't know if that would improve anything though.

I've compiled a bit of an overview of the minimum required casts for myself. Two things that stick out is that bitstruct is

  1. the only type that requires this kind of cast and
  2. doesn't have the inline option.
bitstruct Bitstruct : uint { bool a; }
enum Enum : uint { A }
enum Inline_Enum : inline uint { A }
enum Const_Enum : const uint { A }
enum Const_Inline_Enum : const inline uint { A }
typedef Typedef = uint;
typedef Inline_Typedef = inline uint;

fn void main() {
  Bitstruct foo_bitstruct;
  Enum foo_enum;
  Inline_Enum foo_inline_enum;
  Const_Enum foo_const_enum;
  Const_Inline_Enum foo_const_inline_enum;
  Typedef foo_typedef;
  Inline_Typedef foo_inline_typedef;

  ulong bitstruct_wide            = (ulong)(uint)foo_bitstruct;
  ulong enum_wide                 = (ulong)foo_enum;
  ulong inline_enum_wide          = foo_inline_enum;
  ulong const_enum_wide           = (ulong)foo_const_enum;
  ulong const_inline_enum_wide    = foo_const_inline_enum;
  ulong typedef_wide              = (ulong)foo_typedef;
  ulong inline_typedef_wide       = foo_inline_typedef;

  ushort bitstruct_narrow         = (ushort)(uint)foo_bitstruct;
  ushort enum_narrow              = (ushort)foo_enum;
  ushort inline_enum_narrow       = (ushort)foo_inline_enum;
  ushort const_enum_narrow        = (ushort)foo_const_enum;
  ushort const_inline_enum_narrow = (ushort)foo_const_inline_enum;
  ushort typedef_narrow           = (ushort)foo_typedef;
  ushort inline_typedef_narrow    = (ushort)foo_inline_typedef;
}

cbuttner avatar Dec 04 '25 14:12 cbuttner

I am not super happy about how you can accidentally narrow things today. For the ptr -> int conversion, you can't narrow without an extra step, but I suppose widening should be fine. With inline presumable narrowing with explicit cast should be allowed to?

lerno avatar Dec 04 '25 22:12 lerno

What about this:

  ulong bitstruct_wide            = (ulong)foo_bitstruct;
  ulong enum_wide                 = (ulong)foo_enum;
  ulong inline_enum_wide          = foo_inline_enum;
  ulong const_enum_wide           = (ulong)foo_const_enum;
  ulong const_inline_enum_wide    = foo_const_inline_enum;
  ulong typedef_wide              = (ulong)foo_typedef;
  ulong inline_typedef_wide       = foo_inline_typedef;

  ushort bitstruct_narrow         = (ushort)(uint)foo_bitstruct;
  ushort enum_narrow              = (ushort)(uint)foo_enum;
  ushort inline_enum_narrow       = (ushort)foo_inline_enum;
  ushort const_enum_narrow        = (ushort)(uint)foo_const_enum;
  ushort const_inline_enum_narrow = (ushort)foo_const_inline_enum;
  ushort typedef_narrow           = (ushort)(uint)foo_typedef;
  ushort inline_typedef_narrow    = (ushort)foo_inline_typedef;

lerno avatar Dec 05 '25 19:12 lerno

Yeah, maybe. I can see this making it a bit safer, for example when you have some code where you need the raw integer

uint a = (uint)foo_bitstruct;

but then later decide foo_bitstruct needs more bits, this now unintentional narrowing cast would go undetected.

What would the reverse look like then, like this?

  ulong wide;
  ushort narrow;

  foo_bitstruct         = (Bitstruct)(uint)wide;
  foo_enum              = (Enum)(uint)wide;
  foo_inline_enum       = (Inline_Enum)(uint)wide;
  foo_const_enum        = (Const_Enum)(uint)wide;
  foo_const_inline_enum = (Const_Inline_Enum)(uint)wide;
  foo_typedef           = (Typedef)(uint)wide;
  foo_inline_typedef    = (Inline_Typedef)(uint)wide;

  foo_bitstruct         = (Bitstruct)narrow;
  foo_enum              = (Enum)narrow;
  foo_inline_enum       = (Inline_Enum)narrow;
  foo_const_enum        = (Const_Enum)narrow;
  foo_const_inline_enum = (Const_Inline_Enum)narrow;
  foo_typedef           = (Typedef)narrow;
  foo_inline_typedef    = (Inline_Typedef)narrow;

cbuttner avatar Dec 05 '25 21:12 cbuttner

Perhaps. It would be in line with having to do

char a = 123;
iptr b = 123;
void* x = (void*)(iptr)a;
void* x = (void*)b;

lerno avatar Dec 05 '25 23:12 lerno

This is already required BTW. Same the other way around, you need to do (char)(uptr)&foo. Again, this is to protect from accidentally doing casts that silence errors you didn't expect.

For example in this situation:

// This works on 32 bits
// But it would be a compile time error in 
// current C3 on 64 bits
uint x = (uint)&a;
void* y = (void*)x;

This would be what would be needed to cast to uint on 64 bits for C3 today:

uint x = (uint)(uptr)&a;
void* y = (void*)(uptr)x;

And I think this makes sense, even though it's a bit ugly.

lerno avatar Dec 05 '25 23:12 lerno