WebAssembly
A ghūl program can be compiled to WebAssembly as well as to .NET. The compiler writes a WasmGC module and a small JavaScript loader beside it, and the program runs under Node.js 22 or newer. The same source builds for either target.
work in progress
The WebAssembly target is new and doesn't yet compile the whole language. Small programs work today; the WebAssembly epic lists what is supported and what is still to come.
installing
A WebAssembly 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-compilerghul builds with the newest compiler it has installed. Each compiler release adds to what the WebAssembly target compiles, so run ghul install-compiler again to update. Running the result needs Node.js 22 or newer.
hello world
A WebAssembly program is a project: a directory holding a ghul-project.json manifest that names the project, its targets and its sources. ghul new creates one, here for both targets:
ghul new hello --target dotnet,wasm
cd hello--target takes dotnet, wasm or dotnet,wasm, and is dotnet when it is left out. The new directory holds the manifest:
{
"name": "hello",
"kind": "program",
"targets": ["dotnet", "wasm"],
"sources": ["src/**/*.ghul"]
}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/<target>/ and runs it. The manifest lists two targets, so name the one to run:
ghul run --target wasm # runs out/wasm/hello.mjs under Node.js
ghul run --target dotnet # runs out/dotnet/hello.exe with dotnetThe first build installs the compiler if there isn't one, and fetches the libraries a WebAssembly build compiles in. Later builds reuse them. A project's sources don't get any implicit imports: each file uses what it calls, or starts with use default for write_line, the pipes and the collections.
ghul build builds without running. The ghul-cli README covers the manifest in full.
what works today
With the latest compiler, a WebAssembly build compiles classes, structs, traits and unions, with virtual and trait calls, case over a union, generics, arrays, tuples, function values and closures, strings and string interpolation with alignment and formats, bigint, the scalar conversions such as double(n), list comprehensions, slicing, throw, try, catch and finally, generators, reading standard input, and for loops over 0..n and 1::n.
Not yet:
- asynchronous functions and
await decimal- an interpolated number formatted other than by the standard formats
D,E,F,G,N,RandXor a custom pattern of0,#,.and,, and an interpolated enum given any format - files and directories.
File,Directoryand the readers and writers over a file compile, but act on an empty file system that cannot be changed:existsanswers false, reading a file throwsIO.FileNotFoundException, and creating, writing or deleting one throwsGhul.NotSupportedException.Pathworks as it does on .NET, and standard input and output are read and written as on .NET
The WebAssembly epic tracks each of these. Run ghul install-compiler to pick up each release as it adds to the list.
the same program on .NET
The program above is ordinary ghūl, and the dotnet target builds it into an ordinary .NET executable. On WebAssembly there is no .NET base class library. What it supplies comes from two libraries compiled into the module from source: ghul-core, which gives the built-in types their members and declares the collections, and ghul-runtime, which supplies the pipes and the rest of Ghul. A program that calls .NET APIs directly builds only for the dotnet target, unless it keeps those calls to the dotnet build.
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.
code for one target
@IF.dotnet() written before a definition or a statement keeps it only in a dotnet build, and @IF.wasm() keeps it only in a wasm build. That gives each target a version of its own where they need to differ:
use IO.Std.write_line
entry() is
write_line(target_name())
@IF.wasm()
write_line("a statement kept only for wasm")
si
@IF.dotnet()
target_name() -> string => "dotnet"
@IF.wasm()
target_name() -> string => "wasm"@IF.not.wasm() keeps what it marks everywhere except a wasm build. Whatever a build leaves out is never compiled, so a @IF.dotnet() declaration can call a .NET API the wasm target doesn't have. To leave a whole file out of one target, mark its namespace.
The same pragma works with names of your own, which the compiler's --define <name> option turns on. A name nothing turns on is off, and dotnet and wasm are always decided by the target being built.
errors you may see
A .NET API the core library doesn't declare doesn't exist on WebAssembly, so a call to one is reported the way any other missing name is:
main.ghul: 11,23..11,34: error: member Environment not found in SystemSomething the WebAssembly target can't support at all is reported where it's written, such as a pointer type or an IL. pragma:
main.ghul: 20,15..20,22: error: pointer types not supported on the wasm target
main.ghul: 4,6..4,13: error: pragma IL.name not supported on the wasm targetSomething the target will support but can't compile yet is reported at the function holding it:
main.ghul: 3,1..3,6: error: code generation for a decimal literal is not supported on the wasm target