native executables
A ghūl program can be compiled to a native executable for Linux on x86-64, as well as to .NET and WebAssembly. The compiler writes an LLVM module and links it into a single statically linked file, which runs on its own with nothing else installed.
work in progress
The native target is new and doesn't yet compile the whole language. Small programs work today; the native target epic lists what is supported and what is still to come.
installing
A native build is driven by the ghul command, the ghul.cli .NET tool, which needs the .NET 10 SDK to install and run. Install it, then bring its compiler up to the latest release:
dotnet tool install -g ghul.cli
ghul install-compilerThe first native build also fetches ghul-llvm, the linker and the runtime library a native program links against, into ~/.cache/ghul-cli. The executables it builds need an x86-64-v3 processor (AVX2 and BMI2, from about 2013 on).
hello world
ghul new creates a project for the native target:
ghul new hello --target llvm
cd helloThe new directory holds a ghul-project.json manifest listing the llvm target, a .gitignore for the build output in out/, and the program, as src/main.ghul:
use IO.Std.write_line
entry() is
write_line("hello")
sighul run builds the project into out/llvm/ and runs the executable it built:
ghul runghul build builds without running, and out/llvm/hello is then an ordinary executable that can be copied to another Linux machine and run there. A project can list more than one target, as in ghul new hello --target dotnet,llvm; name the one to build with --target.
debugging
--debug builds with debug information, into out/llvm-debug/ beside the ordinary build:
ghul build --debugA debug build is unoptimised and carries line tables and variables, so a debugger and a crash's backtrace name ghūl files, lines and variables. objdump shows the source beside the machine code, and gdb steps through it:
objdump -d -S out/llvm-debug/hello
gdb out/llvm-debug/hello--optimise <0-3> sets the optimisation level, which is otherwise 2, or 0 for a debug build. A build at another level goes into a directory of its own, such as out/llvm-O3/, or out/llvm-debug-O2/ for an optimised build with debug information. The manifest's options can name a default level, as "optimise": 3.
what works today
With the latest compiler, a native build compiles classes, structs, traits and unions, with virtual and trait calls, type tests and casts, generics, arrays, tuples, optional value types, function values and closures, strings and string interpolation, bigint, generators, asynchronous functions and await, exceptions with try, catch and finally, loops, writing to the console, reading and writing files, and an entry taking the command-line arguments.
A native program fails the way a .NET one does: reading through a null reference throws NullReferenceException, a failed cast throws InvalidCastException, an array index out of range throws IndexOutOfRangeException, and an integer division by zero throws DivideByZeroException, each of which a try can catch.
Strings, numbers and dates behave as they do in a .NET program configured for invariant globalization: strings compare and sort by UTF-16 code unit, and numbers and dates format the same way whatever the culture of the machine. A .NET program gets the same behaviour by setting <InvariantGlobalization>true</InvariantGlobalization> in its project.
Not yet:
decimal
The native target epic tracks what is still to come. Run ghul install-compiler to pick up each release as it adds to the list.
code for one target
@IF.llvm() written before a definition or a statement keeps it only in a native build, and @IF.source_core() keeps it in both a native and a WebAssembly build, the two targets whose core library is compiled from source rather than taken from .NET:
use IO.Std.write_line
entry() is
write_line(target_name())
si
@IF.dotnet()
target_name() -> string => "dotnet"
@IF.llvm()
target_name() -> string => "native"Code for one target on the WebAssembly page covers the pragma in full.
errors you may see
A .NET API the core library doesn't declare doesn't exist in a native build, so a call to one is reported the way any other missing name is. Something the native target will support but can't compile yet is reported at the function holding it, but only in code the program reaches:
main.ghul: 3,1..3,6: error: code generation for a decimal literal is not supported on the llvm targetLLVM keeps names starting llvm. for itself, so a namespace named llvm at the root of a program is reported as an error. To use the name, nest it inside another namespace, as Outer.llvm.