De Goose Programmeertaal
Goose heeft een syntaxis die bekend voorkomt bij C- en Rust-programmeurs, maar er is geen heap. Elke dynamische waarde leeft inline op een datastack die door de compiler wordt beheerd. Groei vindt plaats door een pointer te verplaatsen, en geheugen wordt vrijgegeven zodra de scope eindigt. De type- en lifetime-regels van de taal zorgen ervoor dat dit geheugenmodel veilig is in gebruik.
Waarom Goose?
Goose is sneller dan C++ en veilige Rust, terwijl het toch geheugenveilig is. Over zestien benchmarks draait Goose met een snelheid van 3,3x die van idiomatische C++, 1,16x die van hand-geoptimaliseerde C++ en 1,12x die van de beste veilige Rust, met een geheugengebruik dat respectievelijk 1,9x, 1,3x en 1,2x lager ligt. Deze winst komt voort uit de datalayouts en het geheugenmodel van Goose.
De belangrijkste voordelen zijn:
- Geen allocator, GC of reference counting: Er zijn geen destructors of garbage collectors. Geheugen bestaat uit een handvol datastacks die statisch door de compiler worden toegewezen. Het vrijgeven van een structuur met een miljoen elementen is slechts één bewerking, ongeacht hoe diep deze genest is.
- Referenties overleven groei: Een referentie naar een groeiende array blijft geldig zolang de array bestaat. Waar C++ reserves moet maken en veilige Rust
u32-indexen gebruikt, behoudt Goose getypeerde referenties. - Geheugenveilig zonder annotaties: Er is geen lifetime-syntaxis, geen aliasing- of exclusiviteitsregels en geen
unsafe. De compiler leidt af waar elke referentie in geworteld is en weigert referenties die langer zouden kunnen leven dan hun eigenaren. - Geneste data blijft inline: Strings, arrays van strings en records met variabel-geformatteerde velden vormen één aaneengesloten blok zonder pointers. Een record dat in C++ 160 bytes en één allocatie zou kosten, kost in Goose 29 bytes en nul allocaties.
- Efficiënte Enums: Variabele ADT's (Algebraic Data Types) geven elke waarde de grootte van zijn eigen variant in plaats van de grootste variant. Dit resulteert in 4x minder geheugengebruik en 2x de snelheid van een Rust-enum in specifieke benchmarks.
- Compacte links: Een relatieve referentie slaat een getypeerde, gecontroleerde link op als een offset van 1, 2 of 4 bytes. Hierdoor zijn structuren positie-onafhankelijk; het opslaan van data is een simpele write-operatie en het laden is een read-operatie gevolgd door een verificatiepass.
- Altijd ter plekke gebouwd: Waarden worden direct op hun uiteindelijke bestemming geconstrueerd, ongeacht de diepte van de functieaanroepen.
items.push(parse(line))schrijft het geparseerde record direct in de array. - Fouten teruggeven over meerdere calls: De instructie
return err from loadkeert terug vanuit een functie over een willekeurig aantal frames omhoog. Dit wordt statisch gecontroleerd zonder unwinder,Result-type of de noodzaak voor een?bij elke aanroep. - Threads die niets delen: Een worker wordt gecompileerd als een apart programma met eigen geheugen. Platte waarden passeren via getypeerde queues via een
memcpy. Data races, locks en atomics bestaan niet in de taal. - Generics zonder overhead: Niet-getypeerde parameters zijn generiek. Functiewaarden zijn compile-time entiteiten, waardoor
xs.filter() { it > 0 }compileert tot een eenvoudige loop en het resultaat direct in de bestemming bouwt. - C-compatibiliteit: Goose compileert naar één C-bestand, waardoor het overal draait waar een C-compiler beschikbaar is. Via
extern fnkan C direct worden aangeroepen.
Belangrijkste Kenmerken
Eén geheugenmodel: stacks en scope-exit
Een programma heeft de native call stack, statische data en $N$ datastacks (waarbij $N$ door de compiler wordt bepaald). Een datastack is een grote reservering in de adresruimte met een bump pointer. Er is maximaal één resizable waarde live per stack, die altijd bovenaan staat. Hierdoor verplaatst groei nooit bestaande data en is er geen capaciteitscontrole nodig tijdens runtime; dit wordt op compile-time bewezen.
for round in 3 {
var scratch: u8[>..] = []; // grow-only: groei is een pointer bump
for i in 100000 { scratch.push((i % 256) as! u8); }
print("round ", round, ": ", scratch.len, " bytes");
} // de free: één store naar de stack top
Referenties overleven groei
Omdat data niet verplaatst wordt, blijven referenties geldig.
struct Item { id: i32, weight: f32 }
var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 }); // referentie naar element 0
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5; // nog steeds geldig, een miljoen pushes later
In tegenstelling tot vector<T> of Vec<T> kan een Goose-array niet heralloceren, waardoor deze garantie mogelijk is.
Veilige referenties zonder annotaties
Elke referentie en slice heeft een statische wortel: de variabele die de lifetime van het doel begrenst. De compiler leidt deze wortels af en specialiseert functies, waardoor annotaties overbodig zijn.
Voorbeeld van een lifetime-fout:
fn longest(a: u8[:], b: u8[:]) -> u8[:] { if a.len >= b.len { a } else { b } }
var outer: u8[>..] = [];
var w = outer[..];
{
var inner: u8[>..] = [];
format(inner, "inner text");
w = longest(outer, inner); // FOUT: referentie geworteld in 'inner',
} // which does not outlive the destination (§9.2)
Geneste data blijft inline
Goose gebruikt een familie van array-types die verschillen in hoe ze met grootte omgaan:
| Type | Beschrijving |
|---|---|
T[k] | Vaste grootte, bekend op compile-time |
T[], T[varint] | Grootte bepaald bij constructie, daarna bevroren |
T[..k], T[..] | Capaciteit inline; kan groeien en krimpen binnen die limiet |
T[>..] | Grow-only; referenties blijven geldig |
T[>..<] | Grow-shrink; gebruikt voor stacks, queues, heaps |
Strings zijn simpelweg u8-arrays. Structs kunnen variabel-geformatteerde delen bevatten die inline in de declaratievolgorde staan.
struct Item { sku: u8[varint], qty: varint, cents: varint }
struct Order { id: varint, customer: u8[varint], items: Item[varint] }
var book: Order[>..] = [];
book.push(Order { id: 1001, customer: "alice",
items: [Item { sku: "SKU-441", qty: 2, cents: 1999 },
Item { sku: "SKU-7", qty: 1, cents: 500 }] });
Bovenstaande Order is 29 bytes en vereist 0 allocaties.
Enums in twee formaten
ADT's kunnen op twee manieren worden opgeslagen:
- Fixed mode (
Shape): Een tag plus ruimte voor de grootste payload. Indexeerbaar en overschrijfbaar. - Variable mode (
Shape..): Elke waarde heeft precies de grootte van zijn variant. De array wordt sequentieel en waarden kunnen niet van variant veranderen, maar je kunt wel referenties naar de payload maken.
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }
var packed: Shape..[>..] = []; // 9, 17 en 1 bytes, niet 17 per stuk
packed.push(Shape.Circle { r: 1.0 });
packed.push(Shape.Rect { w: 2.0, h: 3.0 });
packed.push(Shape.Dot);
Links smaller dan pointers
Een standaard referentie T& is een machine-adres (8 bytes). Een relatieve referentie T&<u32> slaat een smalle offset op vanaf het veld zelf naar een doel in dezelfde array. Dit maakt structuren positie-onafhankelijk.
Voorbeeld van een binaire zoekboom met 12-byte nodes:
struct Node { key: i32, left: Node&<u32>?, right: Node&<u32>? }
Gegevens als bestandsformaat
Door de positie-onafhankelijke links is data direct een bestandsformaat. Opslaan is een write, laden is een read plus een verificatiepass.
var image: u8[>..] = tree.to_bytes(); // framed byte image van de hele array
var loaded, ok = from_bytes<Node[>..]>(image); // geverifieerd voordat het een waarde wordt
Altijd ter plekke gebouwd
Waarden worden direct op hun eindbestemming gebouwd. Een functie die een grow-only array retourneert, schrijft de elementen direct in de variabele van de aanroeper.
Fouten teruggeven over meerdere calls
Goose staat toe om fouten terug te sturen naar een specifieke hogere functie in de call stack.
fn parse_user(line: u8[:]) -> User {
let comma = find(line, ",");
guard comma >= 0 else { return [], str("expected name,age: ", line) from load; }
// ...
}
De instructie return ... from load keert terug naar de load functie, ongeacht hoeveel frames er tussen zitten. Dit wordt statisch gecontroleerd.
Generics en functieblokken
Niet-getypeerde parameters zijn generiek. Functiewaarden zijn compile-time entiteiten, waardoor higher-order functies (zoals filter of fold) compileren tot de exacte loop die ze beschrijven, zonder closure-objecten of functiepointers.
Threads die niets delen
thread_fn creëert een apart programma met eigen geheugen en datastacks. Communicatie verloopt via getypeerde queues. Omdat Goose-waarden aaneengesloten zijn, is het versturen van data een eenvoudige memcpy.
C-interoperabiliteit
Goose compileert naar één C-bestand. Via extern fn kan Goose direct C-functies aanroepen.
extern fn hypot(x: f64, y: f64) -> f64;
Waar de snelheid vandaan komt
De snelheid komt voort uit het geheugenmodel: geen heap-allocaties, geen per-object afbraak, aaneengesloten data, smalle links en compacte enums. De compiler verwijdert bounds-checks die bewezen overbodig zijn.
Gemiddelde prestaties over 16 benchmarks:
| Metric | vs idiomatische C++ | vs hand-geoptimaliseerde C++ | vs beste veilige Rust |
|---|---|---|---|
| Snelheid (MSVC backend) | 3.51x | 1.24x | 1.13x |
| Snelheid (clang backend) | 3.54x | 1.26x | 1.19x |
| Piekgeheugen | 1.95x minder | 1.32x minder | 1.25x minder |
De keerzijde (Wat het kost)
- Geheugenbeheer: De programmeur moet nadenken over waar data leeft, wie de eigenaar is en hoe lang de scope duurt.
- Recursie: Recursieve functies kunnen geen growable data behouden over een recursieve aanroep heen.
- Beperkingen in arrays: Arrays met variabel-geformatteerde elementen kunnen worden geïtereerd, maar niet direct geïndexeerd (tenzij er een aparte index van referenties wordt bijgehouden).
- Geen dynamische dispatch: Er zijn geen escaping closures, functiepointers of dynamische dispatch buiten ADT-tags.
- Whole-program compilation: Alleen volledige programma-compilatie is ondersteund.
Installatie en Gebruik
Een release installeren
Voorbouwde compilers voor Windows, Linux en macOS zijn beschikbaar op de releases-pagina. Na installatie moet de directory aan de PATH worden toegevoegd. Op macOS is het nodig om xattr -dr com.apple.quarantine uit te voeren op de uitgepakte directory.
Bouwen en uitvoeren
Voor het bouwen van de compiler zelf zijn CMake 3.27+, een C++20 compiler en Python 3 nodig.
Build stappen:
git clone https://github.com/aardappel/goose
cd goose
cmake -B build -DCMAKE_BUILD_TYPE=Release -DGOOSE_BUILTIN_LIBS=all
cmake --build build --config Release
Programma's uitvoeren:
- In-process (JIT):
build/goose samples/01_tour.goose - Native executable bouwen:
build/goose build samples/01tour.goose && samples/01tour
Handige vlaggen:
-O0/-O1/-O2: Inlining niveau (standaard-O2).--debug: Schakelt overflow-, range- en tag-checks in.--bce-lines: Rapporteert welke bounds-checks per regel bewaard blijven.
Documentatie
- Tutorial: Een vriendelijke introductie aan de hand van voorbeelden.
- Language Reference: Uitgebreide beschrijving van elke feature met voorbeelden.
- Language Specification: De exacte regels van de taal.
- Samples: Een verzameling van 31 volledige programma's.
- Standard Library: Veertien modules, waaronder
gfx(SDL3),physics(Box3D) enui(Nuklear).
Status
Goose is nieuw. De huidige versie bestaat uit een whole-program compiler van ongeveer 30.000 regels C++. Functionaliteiten die momenteel buiten scope vallen zijn: moves voor resizable waarden, meerdere resizable waarden per struct, labeled breaks en uitgebreidere OS-toegang.
Licentie
Goose is gelicentieerd onder de Apache License, Version 2.0.
Auteurs
- Wouter van Oortmerssen: Taalontwerp, Compilerontwerp, Codestandaarden.
- Claude Fable: Compiler-implementatie, Benchmarking, Samples & Documentatie.
Geschiedenis en gebruik van AI
Dit project is bijna volledig geproduceerd met behulp van AI, gebaseerd op een menselijk taalontwerp. De auteur had Goose jaren geleden ontworpen, maar had geen tijd voor de implementatie. Met de komst van AI (met name Fable) werd het mogelijk om de implementatie te versnellen.
De compiler is geschreven in C++ om de deployment te vereenvoudigen, aangezien de compiler zelf C-code genereert en vaak gebruikmaakt van C-bibliotheken. De structuur van de code is gemodelleerd naar een andere compiler van de auteur (Lobster).
Groetjes,