Skip to content

Instantly share code, notes, and snippets.

@aandvalenzuela
Created December 11, 2025 15:15
Show Gist options
  • Select an option

  • Save aandvalenzuela/09ff451ccc01f16a4bcdc00bcdd5bb02 to your computer and use it in GitHub Desktop.

Select an option

Save aandvalenzuela/09ff451ccc01f16a4bcdc00bcdd5bb02 to your computer and use it in GitHub Desktop.

ADA Use-Case

We consider an Ada program compiled with its native GCC-based toolchain (GNAT). The Ada front end parses and type-checks the source, and GCC lowers the program to GIMPLE-IR as part of its normal optimization pipeline. Instead of continuing to RTL and generating code for a GCC-supported architecture, we intercept the compilation at the GIMPLE level and feed the GIMPLE dump to our model IRIS.

IRIS translates the GIMPLE-IR into semantically equivalent LLVM-IR, which we then pass to the LLVM toolchain and compile to WebAssembly, a target not currently supported by GCC. At the time of writing, either GNAT LLVM provide direct support for compiling Ada to WebAssembly.

This pipeline effectively enables Ada code to run in WebAssembly environments without modifying the Ada front end or implementing a dedicated Ada-to-WebAssembly backend. It illustrates how IR-to-IR translation can decouple language front ends from specific back ends and open new deployment targets with minimal changes to existing compiler infrastructure.

  • We start from the following illustrative Ada snippet:
with Ada.Text_IO; use Ada.Text_IO;
procedure Hello is
begin
   Put_Line ("Hello World. Welcome to GNAT");
end;

  • GCC dumps the GIMPLE IR:
_GLOBAL.SZ0.ada_hello (positive___XDLU_1__2147483647 p0, positive___XDLU_1__2147483647 p1)
{
  bitsizetype D.4598;
  bitsizetype iftmp.0;

  if (p1 <= p0) goto <D.4600>; else goto <D.4601>;
  <D.4600>:
  _1 = (sizetype) p0;
  _2 = (sizetype) p1;
  _3 = _1 - _2;
  _4 = _3 + 1;
  _5 = (bitsizetype) _4;
  iftmp.0 = _5 * 8;
  goto <D.4602>;
  <D.4601>:
  iftmp.0 = 0;
  <D.4602>:
  D.4598 = iftmp.0;
  return D.4598;
}


_GLOBAL.SZ1.ada_hello (positive___XDLU_1__2147483647 p0, positive___XDLU_1__2147483647 p1)
{
  sizetype D.4603;
  sizetype iftmp.1;

  if (p1 <= p0) goto <D.4605>; else goto <D.4606>;
  <D.4605>:
  _1 = (sizetype) p0;
  _2 = (sizetype) p1;
  _3 = _1 - _2;
  iftmp.1 = _3 + 1;
  goto <D.4607>;
  <D.4606>:
  iftmp.1 = 0;
  <D.4607>:
  D.4603 = iftmp.1;
  return D.4603;
}


hello ()
{
  struct  D.4608;

  {
    typedef signed long struct signed long;
    typedef character hello__T1b[1:28];

    D.4608.P_ARRAY = "Hello World. Welcome to GNAT";
    D.4608.P_BOUNDS = &*.LC0;
    ada.text_io.put_line (D.4608);
  }
  return;
}
  • The IRIS model translates this GIMPLE IR to LLVM IR:
; ModuleID = 'main.c'
source_filename = "main.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

%struct.anon = type { i8*, i8* }

@.str = private unnamed_addr constant [29 x i8] c"Hello World. Welcome to GNAT\00", align 1
@.str.1 = private unnamed_addr constant [2 x i8] c"\0A\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i64 @SZ0(i32 noundef %0, i32 noundef %1) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %4, align 4
  %6 = load i32, i32* %3, align 4
  %7 = icmp ugt i32 %5, %6
  br i1 %7, label %8, label %9

8:                                                ; preds = %2
  br label %17

9:                                                ; preds = %2
  %10 = load i32, i32* %3, align 4
  %11 = zext i32 %10 to i64
  %12 = getelementptr inbounds i8, i8* null, i64 %11
  %13 = load i32, i32* %4, align 4
  %14 = zext i32 %13 to i64
  %15 = sub i64 0, %14
  %16 = getelementptr inbounds i8, i8* %12, i64 %15
  br label %17

17:                                               ; preds = %9, %8
  %18 = phi i8* [ null, %8 ], [ %16, %9 ]
  %19 = ptrtoint i8* %18 to i64
  %20 = sub i64 %19, 0
  ret i64 %20
}

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i64 @SZ1(i32 noundef %0, i32 noundef %1) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %4, align 4
  %6 = load i32, i32* %3, align 4
  %7 = icmp ugt i32 %5, %6
  br i1 %7, label %8, label %9

8:                                                ; preds = %2
  br label %16

9:                                                ; preds = %2
  %10 = load i32, i32* %3, align 4
  %11 = load i32, i32* %4, align 4
  %12 = zext i32 %11 to i64
  %13 = sub i64 0, %12
  %14 = getelementptr inbounds i8, i8* null, i64 %13
  %15 = zext i32 %10 to i64
  br label %16

16:                                               ; preds = %9, %8
  %17 = phi i8* [ null, %8 ], [ %14, %9 ]
  %18 = ptrtoint i8* %17 to i64
  %19 = sub i64 %18, 0
  ret i64 %19
}

; Function Attrs: noinline nounwind optnone uwtable
define dso_local void @hello() #0 {
  %1 = alloca %struct.anon, align 8
  %2 = getelementptr inbounds %struct.anon, %struct.anon* %1, i32 0, i32 0
  store i8* getelementptr inbounds ([29 x i8], [29 x i8]* @.str, i64 0, i64 0), i8** %2, align 8
  %3 = getelementptr inbounds %struct.anon, %struct.anon* %1, i32 0, i32 1
  store i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.1, i64 0, i64 0), i8** %3, align 8
  call void @put_line(%struct.anon* noundef byval(%struct.anon) align 8 %1)
  ret void
}

declare void @put_line(%struct.anon* noundef byval(%struct.anon) align 8) #1

attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }

!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 1}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"Ubuntu clang version 14.0.0-1ubuntu1.1"}

  • When we try to compile the generated LLVM IR, we get the following error:
> clang hello.ll -o hello_binary -O0 -nostartfiles -Wl,-e,hello -lgnat-10
  /usr/bin/ld: /scratch/tmp/hello-d7da56.o: in function `hello':
  main.c:(.text+0xc3): undefined reference to `put_line'
  clang: error: linker command failed with exit code 1 (use -v to see invocation)
  • As a workaround, we define our own put_line.c:
#include <stdio.h>

void put_line(char *s1, char *s2) {
    (void)s2;
    puts(s1);
}
  • Change the put_line() call in the LLVM IR so that the function signatures match:
79c79,81
<   call void @put_line(%struct.anon* noundef byval(%struct.anon) align 8 %1)
---
>   %4 = load i8*, i8** %2, align 8
>   %5 = load i8*, i8** %3, align 8
>   call void @put_line(i8* noundef %4, i8* noundef %5)
83c85
< declare void @put_line(%struct.anon* noundef byval(%struct.anon) align 8) #1
---
> declare dso_local void @put_line(i8* noundef, i8* noundef) #1
  • And define a driver.c to specify the entry-point:
void hello(void);

int main(void) {
    hello();
    return 0;
}
  • Finally, compiling with clang hello-modif.ll put_line.o driver.c -O0 -lgnat-10 -o hello_paper works:
avalenz1@BSC-8488191241:~/ada/paper-sample/test/paper$ ./hello_paper
Hello World. Welcome to GNAT

We are not sure about:

  1. Do we need the driver.c?

  2. Can we link correctly agains libgnat-10.so so that we do not need to define our own put_line.c to get the missing symbols?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment