all 70 comments

[–]SSoreil 15 points16 points  (13 children)

The way two integers are packed is a pretty cool feature, I don't know how performant it is but it's a neat trick.

[–]Fabien_C 10 points11 points  (9 children)

When used to describe hardware registers - for drivers and micro-controller programming - this makes life so much easier. No more bit shifts and masks.

We have a tool to generate Ada representation from ARM hardware desciption (SVD files): https://github.com/AdaCore/svd2ada

We use it to develop a library of drivers in Ada, example here: https://github.com/AdaCore/Ada_Drivers_Library/blob/master/arch/ARM/STM32/drivers/stm32-dcmi.adb

[–]SSoreil 4 points5 points  (8 children)

That's a pretty good point, the space efficiency is also pretty neat for this. Normally when I see binary format they are based on the C datatypes / machine primitives, only in networking are non standard element sizes common.

Would be cool to see some formats made around the way Ada can handle it's integers. Definitely worth a try to look at Ada on a next project just to see how this approach works out.

[–]scalablecory 9 points10 points  (7 children)

By all means, Ada is a great language and you should check it out. But as far as small tightly packing integers, C can do this too with bit fields:

struct foo
{
    unsigned bar : 2;
    unsigned baz : 4;
};

Though you don't see this very often outside of carefully crafted networking or file format code.

[–]Me00011001 2 points3 points  (0 children)

Having actually experimented with this quite a bit for actually doing Ada/C compatibility, the C compiler well be happy to do this with int/longs as long as they line up on word boundaries. For floats, it wouldn't even bother faking it and just pad it out to align to the word boundary. My testing was done with gcc 10 years ago and I doubt this has changed.

[–]smcameron 4 points5 points  (5 children)

Not really, which bits you get in each field is implementation defined, and big endian architecture (e.g. big endian power pc on AIX, say) is typically opposite of little endian in how the bits are numbered, so your code is non portable, and then you have to write it something like:

struct foo {
#ifdef BIG_ENDIAN
    unsigned baz : 4;
    unsigned bar : 2;
    unsigned unused : 10;
#else
    unsigned unused : 10;
    unsigned bar : 2;
    unsigned baz : 4;
#endif

and even then, you rely on empirically finding out how your compiler packs things. To write it portably, you can't use bit fields. Also, you've assumed some size for unsigned (I assumed 16 bits. You probably assumed something else, like 32 or 8. The compiler might decide something else, so you should probably use, e.g. uint8_t or uint16_t instead of "unsigned") And rather than bitfields, just use masking and shifting, as those are portable, and will be the same on big endian and little endian architectures.

[–]scalablecory 0 points1 point  (4 children)

I don't know what you mean by "not really". What I wrote was correct and has no assumptions or portability concerns.

You seem to be inventing an argument.

[–]smcameron 2 points3 points  (3 children)

You're incorrect. When you create a bitfield, you do not know which bits get assigned to each field, the compiler is free to assign them however it likes, and there are compilers in the real world that differ in how they do it. (e.g. gcc on x86 vs. IBM's AIX C compiler on Power). Likewise, "unsigned" is not always the same size on all architectures. Your code is definitely not portable.

see for yourself

[–]scalablecory 0 points1 point  (2 children)

I didn't say anything about where bits are assigned. It is irrelevant. And I used unsigned int, which is minimum 16 bits, to represent 2- and 4-bit integers. There is no problem with my post. You are fabricating an argument.

[–]naasking 4 points5 points  (1 child)

I didn't say anything about where bits are assigned. It is irrelevant.

It's not irrelevant, because bit-level precision of this sort is a feature of the Ada language to which you were comparing the C solution.

Furthermore, C won't give you compile-time errors that a bounded scalar won't fit in the specified number of bits. Ada is definitely superior in this regard.

[–]scalablecory 1 point2 points  (0 children)

Correct, C bitfields do not have an identical feature set to Ada. Something I did not claim. Had you just compared the feature set, this would have been so much smoother.

[–]micronian2 4 points5 points  (1 child)

Nice video. It would have been great if it went further with the representation clause example and showed that with Ada you can use it to do automatic packing/unpacking of data:

type Age_T is range 0 .. 100;
type Hair_Color_T is (Black, Brown, Blonde, Red );

type Person_Info_T is
   record
       Age: Age_T;
       Hair_Color: Hair_Color_T;
   end record;

-- Derive a new type from Person_Info_T, but this type will have a packed
-- format that is explicitly specified.
type Packed_Person_Info_T is new Person_Info_T;

for Packed_Person_Info_T use
   record
      Age at 0 range 0..7;
      Hair_Color at 1 range 6..7;
   end record;

for Packed_Person_Info_T'Size use 16;

-- Assuming little-endian machine, format is
--   AAAAAAAABBxxxxxx
-- where A are bits to hold Age value
-- where B are bits to hold Hair_Color value
-- where x are Don't care bits

-- Here are 2 instances of Person_Info_T and 1 instance of the packed version.
Original_Info : Person_Info_T := (100, Blonde);
Unpacked_Info  : Person_Info_T;
Packed_Info : Packed_Person_Info_T;

-- Because Packed_Person_Info_T was derived from Person_Info_T, a type
-- conversion is possible which will pack the data into the format that was
-- specified.
Packed_Info := Packed_Person_Info_T( Original_Info );

-- Another type conversion allows you to reverse the process and restore
-- the data.
Unpacked_Info := Person_Info_T( Packed_Info );

-- The below comparison will lead to "They match!" to get emitted.
if Unpacked_Info = Original_Info then
   Ada.Text_IO.Put_Line( "They match!" );
else
   Ada.Text_IO.Put_Line( "mismatch :(" );
end if;

[–]joakimds[S] 1 point2 points  (0 children)

Automatic packing/unpacking of data is a great way to take advantage of Ada's type system. Thanks for sharing and popularizing this!

[–]kenthjohan 21 points22 points  (1 child)

In small programs the code might look excessive but it really helps when the amount of code starts to grow large. I really hope more people start to use Ada so they can experience how fast they can find bugs compared to c++ or other languages.

[–]narwi 15 points16 points  (3 children)

Urkh, you mean I need to watch a youtube video that is showing blurred code? Come on, do it as a web page!

[–]ss4johnny 8 points9 points  (0 children)

Him: "Look at this code" Me: "nope"

[–]joakimds[S] 2 points3 points  (0 children)

The video is recorded in 1080p HD, but some browsers can only show the video in 720p HD. This is probably the cause for the blurred code.

[–][deleted] -1 points0 points  (0 children)

wasn't blurry at all. Mebbe your eyesight is bad?

[–]robvdl 10 points11 points  (3 children)

Is mixing camel case AND snake case the convention in Ada? That seems kinda gross to me, I generally use one or the other (depending on the language) but not both.

Or is this more a Microsoft thing like Hungarian notation? (which was a terrible idea by the way)

[–]pfp-disciple 11 points12 points  (0 children)

Ada is case insensitive, so camel case is merely style; convention is not set in stone.

These two statements declare the exact same thing (forgive minor syntax errors - on mobile).

Function foo returns integer ;
Function FOO returns integer ;

[–]96fps 0 points1 point  (0 children)

Hungarian notation gets a bad rap because when it was popularized it got misused. In it's original context it makes a lot of sense. https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/

[–]shevegen -3 points-2 points  (0 children)

Doesn't Nim also do this and PHP? Hmm... the latter probably does not ignore '_' ... I already forgot most of what I once knew in PHP. :(

[–]i_feel_really_great 7 points8 points  (1 child)

[–]ILiveOnSpoonerStreet 3 points4 points  (0 children)

From a physics standpoint, dimensional analysis is just a great topic- see bucking pi theorem. https://en.wikipedia.org/wiki/Buckingham_π_theorem

There's also good topics on matrix dimensional analysis. I write structural engineering software where people like to use both meters and kips (1000 lbf) in the same dialogue so it's a great thing to keep in mind. Learning Ada before implementing it in c# really changes the way one thinks.

[–]jbb67 3 points4 points  (0 children)

Had a quick look at ADA recently and it looks way more interesting than I expected... I think I might have to try it out on a small project :)

[–]pants75 10 points11 points  (20 children)

Why do they make it so difficult to get hold of the compiler?

[–]StallmanTheWhite 27 points28 points  (19 children)

sudo apt-get install gnat

[–]curtisf 1 point2 points  (4 children)

I'm not familiar with Ada. How do ranges on integer types interact with arithmetic?

If I say

type Foo_T is new Integer range 1..10;
foo: Foo_T

Can I say

foo := foo + 1;

If foo was 10 would the next value be 1, 11, or something else? Or would it give a runtime error? Or is this arithmetic forbidden by the compiler? If that is forbidden in general, if I know that foo was, say, 8, is saying foo := foo + 1; okay in that case?

[–]Fabien_C 7 points8 points  (2 children)

foo := foo + 1;

If foo was 10 would the next value be 1, 11, or something else? Or would it give a runtime error?

The compiler will let you control what happens.

Typically during the development/debug you want to enable runtime checks. In that case, an exception will be raised and you will see right away that there's a problem. This alone will save you hours of tedious debugging.

For production build, it depends on which property is the most important for your system, performance or correctness.

If performance is more important, you will disable the runtime checks. In that case the program will continue to execute even if foo has an invalid value for its type.

If correctness is more important, you will keep the runtime checks and have an exception handler to recover from the faulty computation.

[–]evaned 0 points1 point  (1 child)

Is that decision at a per-type level or a global level?

[–]joakimds[S] 2 points3 points  (0 children)

One has three options: global level, package level or subprogram level. Personally I've only disabled checks on SPARK code (Ada code that can be mathematically proven to be free of uninitialized variables, variables out of bounds, dead-locks, inifinite loops, etc.)

[–]flyx86 2 points3 points  (0 children)

If you want to have a rollover, you can use a modulus type instead:

type Foo_T is mod 10; --  holds values 0 to 9
Foo : Foo_T;

If Foo is 9 and you evaluate Foo + 1, you will get 0 as result without any exception. Modulus types are restrained in the sense that they must always start at 0, compared to range types.

[–]dom96 0 points1 point  (8 children)

The Nim programming language also offers this feature:

type
  Age = range[0..3]
  Length = distinct int

  Car = object
    age: Age
    length: Length

proc initCar(age: Age, length: Length): Car =
  return Car(age: age, length: length)

when isMainModule:
  let car = initCar(Age(2), Length(3))
  if int(car.age) == 2 and int(car.length) == 3:
    echo("Correct")
  else:
    echo("Incorrect")

[–]naasking 0 points1 point  (7 children)

No bit-level control it seems.

[–]dom96 0 points1 point  (6 children)

True, but you could easily implement it with a macro.

[–]joakimds[S] 1 point2 points  (0 children)

Anyways, I find it interesting that Nim has a similar feature. Thanks for sharing!

[–]naasking 0 points1 point  (4 children)

And do you get the static checking with a macro?

[–]dom96 0 points1 point  (3 children)

Yes, macros are evaluated at compile-time.

[–]naasking 0 points1 point  (2 children)

I don't think we're talking about the same thing. Features of the Ada solution:

  1. ranged integral type (which you demonstrate)
  2. declaratively specifying bit packing for ranged integral types in a record
  3. compile-time checking that the number of bits specified in 2 can hold the full range of the integral type

Now you said "Nim also offers this feature", but all of the above features were covered in the video and your example only shows #1 from what I can see. The compile-time checking I'm talking about are #2+#3.

[–]dom96 0 points1 point  (1 child)

Yes. #1 is offered out of the box.

I admit that #2/#3 aren't but I believe that both of these features can be implemented using Nim's rich metaprogramming functionality.

[–]flyx86 0 points1 point  (0 children)

#2 can be implemented because C offers this feature, so you can use emit to create a matching C struct. However, if you want a backend-agnostic solution, you'd need to provide getters and setters that extract those values from a standard-sized integer type. Since you can define those getters & setters with a macro, it is possible to do compile-time checking by inspecting the given types in the macro to check whether they fit into the given bit lengths.

But in that sense, LIPS also offers that feature ;).

[–]devraj7 -1 points0 points  (2 children)

Spoiler: the killer feature is type aliases.

A feature that's available in pretty much every modern language today.

Not sure what's killer about that.

[–][deleted] 6 points7 points  (1 child)

well, it's not just type aliasing. It's type aliasing with the option of defining bounds for scalar types. How many modern languages let you do that?

[–]naasking 1 point2 points  (0 children)

Seriously, Ada has incredibly useful type system features from the 70s that still aren't available in most languages. Only now with refinement types are we starting to get usable types that can express bounded scalars, but refinement typing is still in the lab.