Note for subscribers: if you are interested in my
Ada programming articles only, you can use
this RSS feed link.
HAC (the HAC Ada Compiler) was since the first of its previous lives, from Pascal-S, in 1973, to recent commits, translating high-level language exclusively to the machine code of a fictitious machine (a Virtual Machine, abbreviated VM).
For writing a tiny compiler, a VM is very convenient:
- You (the compiler creator) can model and adapt it to your needs.
- You don't depend on hardware.
- You don't need to rewrite the code generation for each new hardware.
- You can make the VM running on many hardwares (hence the success of the JVM and .NET around year 2000).
The HAC VM and its users are enjoying all these advantages.
However, there is a flip side:
- A VM is much slower than a real machine (but till year 2000, it didn't matter; you could say: don't worry, just wait a few months for a more powerful computer).
- Since year 2000, the speed of microprocessors stopped doubling every six months (hence the declining interest in Virtual Machines). Despite the improvements in cache, RAM, and the multiplication of cores, the per-core performance improvement is slower and slower.
- Supporting only a VM gives the impression that your compiler can only compile to a VM.
Well, so far, the latter blame was objectively correct regarding HAC until a few weeks ago.
Now it is changing!
We have begun an abstract framework for emitting machine code.
With that framework, HAC can, on demand, emit code for its captive VM or for any implemented machine.
So you read well, it is not a just-in-time compiler translating VM instructions to native ones.
We are implementing a direct Ada-to-native compiler - and cross-compiler, since HAC doesn't know on which machine it is running!
The framework looks like this:
with HAC_Sys.Defs;
package HAC_Sys.Targets is
type Machine is limited interface;
type Abstract_Machine_Reference is access Machine'Class;
--------------------
-- Informations --
--------------------
function Name (m : Machine) return String is abstract;
function CPU (m : Machine) return String is abstract;
function OS (m : Machine) return String is abstract;
function Null_Terminated_String_Literals (m : Machine) return Boolean is abstract;
----------------------------
-- Machine Instructions --
----------------------------
procedure Emit_Arithmetic_Binary_Instruction
(m : in out Machine;
operator : Defs.Arithmetic_Binary_Operator;
base_typ : Defs.Numeric_Typ) is abstract;
...
The code emission in the compiler is being changed (very slowly, be patient!) for going through this new abstract machine mechanism.
So far we have two implementations:
- The HAC VM - that way, we ensure HAC-for-HAC-VM works exactly as previously and can pass the test suite.
- A real target: the AMD64, running under Windows; the machine code is emitted in Assembler form, for the Flat Assembler (FASM).
The development for multiple targets is embryonic so far.
However, we can already compile a "hello world"-style program:
with HAT;
procedure Native is
use HAT;
a : Integer;
begin
-- a := 1; -- Variables: TBD.
Put_Line ("Hello ...");
Put_Line ("... world!");
Put_Line (12340 + 5);
Put_Line (12350 - 5);
Put_Line (2469 * 5);
Put_Line (61725 / 5);
end Native;
With the command:
hac -tamd64_windows_console_fasm native.adb
HAC produces this:
; Assembler file for the Flat Assembler - https://flatassembler.net/
format PE64 console
entry _start
include 'include\win64a.inc'
section '.code' code readable executable
_start:
push 9
push 1
push -1
push -1
pop r14
pop r13
pop r12
pop r11
add r12, _hac_strings_pool
ccall [printf], r12
ccall [printf], _hac_end_of_line
push 10
push 11
push -1
push -1
pop r14
pop r13
pop r12
pop r11
add r12, _hac_strings_pool
ccall [printf], r12
ccall [printf], _hac_end_of_line
push 12340
push 5
pop r11
pop rax
add rax, r11
push rax
push 20
push 10
push -1
pop r14
pop r13
pop r12
pop r11
ccall [printf], _hac_decimal_format, r11
ccall [printf], _hac_end_of_line
push 12350
push 5
pop r11
pop rax
sub rax, r11
push rax
push 20
push 10
push -1
pop r14
pop r13
pop r12
pop r11
ccall [printf], _hac_decimal_format, r11
ccall [printf], _hac_end_of_line
push 2469
push 5
pop r11
pop rax
imul rax, r11
push rax
push 20
push 10
push -1
pop r14
pop r13
pop r12
pop r11
ccall [printf], _hac_decimal_format, r11
ccall [printf], _hac_end_of_line
push 61725
push 5
pop r11
pop rax
xor rdx, rdx
idiv r11
push rax
push 20
push 10
push -1
pop r14
pop r13
pop r12
pop r11
ccall [printf], _hac_decimal_format, r11
ccall [printf], _hac_end_of_line
stdcall [ExitProcess],0
section '.data' data readable writeable
_hac_end_of_line db 10, 0
_hac_decimal_format db "%d", 0
_hac_strings_pool db "XHello ...", \
0, "... world!", 0
section '.idata' import data readable
library kernel,'kernel32.dll',\
msvcrt,'msvcrt.dll'
import kernel,\
ExitProcess,'ExitProcess'
import msvcrt,\
printf,'printf'
As you can see, the assembler code needs badly some simplification, but, anyway: it works.
FASM produces from it a relatively small 2048-byte executable which writes
Hello ...
... world!
12345
12345
12345
12345
The executable is full of zeroes, due to alignments. The non-zero bytes (more or less, the actual machine code and data) take 605 bytes.
Some Web links for HAC:
Main URL: https://hacadacompiler.sourceforge.io/
Sources, site #1: HAC Ada Compiler download | SourceForge.net
Sources, site #2: GitHub - zertovitch/hac: HAC Ada Compiler - a small, quick Ada compiler fully in Ada
Alire Crate: Alire - Hac