3

I am doing a Z80 emulator in Ada. I am implementing the JR (Jump relative) family, But I am not satisfied with my code:

with Ada.Text_IO;

procedure main is
    type UInt16 is mod 2 ** 16;
    type UInt8  is mod 2 **  8;
    type Int8   is range -128 .. 127;

    package UInt16_IO is new Ada.Text_IO.Modular_IO (UInt16);

    function Two_Complement(N : UInt8) return Int8 is
    begin
        if N <= 127 then
            return Int8 (N);
        end if;
        return Int8 (Integer (N) - 256);
    end Two_Complement;

    -- Relative jump
    function Jr (Address : UInt16; D: UInt8) return UInt16 is
    begin
        return UInt16 (Integer (Address) + Integer (Two_Complement (D) + 2));
    end Jr;

    Address : UInt16;
begin
    Address := 16#683#;
    UInt16_IO.Put (Item => Jr (Address, 16#F1#), Base => 16); -- Get    16#676# which is good !
end main;

It seems to work, but I find that there are too many types conversions. Do you have some advice ?

Thanks,

Olivier.

ols
  • 173
  • 7
  • With: return Address + UInt16 (Two_Complement (D) + 2); I have a CONSTRAINT_ERROR, which is normal as Two_Complement can return negative numbers. "Too many": because I have a C/C++ background and I try to write good Ada ! – ols May 27 '17 at 10:49
  • I have not noticed that it returns int8. – 0___________ May 27 '17 at 12:52
  • 1
    Side note: if you use type `Integer` from the Ada standard library and not a type of your own (preferably with range and `'Size` specified), then the former type may well be a 16 bit type. With Janus/Ada, it is, and so an attempted conversion from `UInt16` might raise a range constraint error. – B98 May 27 '17 at 14:59

4 Answers4

6

You could look at

function Jr (Address : UInt16; D: UInt8) return UInt16 is
   Offset : constant Uint16
     := Uint16 (D) + (if D >= 16#80# then 16#ff00# else 0);
begin
   return Address + Offset + 2;
end Jr;

but it rather depends on what you need to happen when - for instance - Address is 0 and D is, say, 16#80 (the code above returns 16#ff82#).

Simon Wright
  • 25,108
  • 2
  • 35
  • 62
  • Nice ! For the case of address 0, I need to check in a Z80 handbook, or do some tests on a real one. I am pretty sure we have a modulo effect. – ols May 27 '17 at 16:48
1

In case two integer types are very closely related, at least form a certain point of view, if they only differ in the subset of values but not function, consider subtypes.

I suspect that choosing subtypes might blur matters, though, from a conceptual point of view. So, if I may speculate, using your knowledge about the purpose of those integers to evolve names like Offset (guessing) will increase the value of names by conveying their purpose: what they mean, not just how many bits they have, or whether they are signed. Maybe that softens the experience of type conversions, too, because then parameters “become” objects of the so named types = notions. The run-time (or compile-time) effects will be the same.

In C terms, a type alias is a possibility for XintNN_t, too; the alias might even include the int-ness if that's desirable.

B98
  • 1,229
  • 12
  • 20
  • I agree ! But it is not easy to find good "domain" names to increase the abstraction level, as the Z80 domain emulation is very technical with lots of bits and bytes. The code I provide is not the real code. I simplify it to focus on my problem. In the real one, I have: subtype Register8 is UInt8; subtype Register16 is UInt16; subtype Address16 is UInt16; And so on ! – ols May 27 '17 at 16:57
1

Since Ada focuses on the type safety, the following two type definitions are not directly compatible as seen by the Ada compiler:

type UInt8 is mod 2 ** 8;
type UInt_8 is mod 2 ** 8;

This is why a type conversion is needed when they are used in the same expression. One way to solve this issue is to define a common type. For example,

type Int32 is range -2 ** 31 .. 2 ** 31 - 1;

subtype UInt8 is Int32 range       0 .. 2 ** 8 - 1;
subtype  Int8 is Int32 range -2 ** 7 .. 2 ** 7 - 1;

Then you would not need so many conversions as the compiler will use the Int32 type as the base type for the computation. For instance, the statement return Int8 (Integer (N) - 256); in the Two_Сomplement procedure, can then be simplified to return Int8 (N - 256);.

As a side note, you can also use the Interfaces library to insure the proper sizes for types. In addition, the library has convenient operations such as Shift_Left, Shift_Right etc.

NeoSer
  • 527
  • 3
  • 16
  • If you make `UInt8` a subtype of the signed `Int32` then you won’t get modular arithmetic; `UInt8’(255) + 1` will be 256, not 0, and you may well get CE, depending on what you assign it to. And OP doesn’t have `UInt_8` anywhere, I think you meant `UInt16`. The `Interfaces` suggestion is a good one. – Simon Wright May 27 '17 at 16:07
  • Indeed, modular arithmetic will not be available in this case. On other hand, I do not think that the use of modular arithmetic will have any benefit in this code. On contrary, if two unsigned integers are subtracted `UInt8'(125) - 130`, the result will wrap-around, will not fit into the type and the compiler will not be able to "see" this. Hence, a CE will be raised, as well. I think that if no modular arithmetic is available and in your example `UInt8'(255) + 1`, the compiler will give at least a warning (or an error if configured) about the result. – NeoSer May 27 '17 at 16:43
  • I willl have a look on the `Interfaces` package. – ols May 27 '17 at 17:02
  • The point in this example is that modular arithmetic is what the Z80 MPU does in the JR instruction, so it's what the emulator requires. Type safety helps draw attention to the corner cases, like addresses that may wrap round from 16#FFFF# to 0 - it's been a long time but I think the Z80 does wrap, as Simon suggests. –  May 28 '17 at 10:25
1

I suspect that a little alteration in naming might help things out here.

You could use this:

Subtype Address is UInt16;

Function "+"( Location : Address; Offset: Int8 ) return Address is
  (if Offset < 0 then Location - UInt16(ABS Offset)
   else Location + UInt16(Offset) );

Which would allow you to reformulate Jr to this:

-- Relative jump
function Jr (Location : Address; D: UInt8) return UInt16 is
    Offset : Constant Int8 := Two_Complement(D) + 2;
begin
    return Location + Offset;
end Jr;
Shark8
  • 4,095
  • 1
  • 17
  • 31