The Goose Programming Language · Goose

A memory-safe techniques language with no heap allocator, rubbish
collector, or lifetime annotations. In this repository’s benchmarks,
Goose runs quicker than C++ and Rust whereas utilizing much less reminiscence.
Tutorial · Reference · Specification · Packages · Samples · Benchmarks · Standard library
Goose has syntax acquainted to C and Rust programmers, however
there isn’t a heap. Every dynamic worth lives inline on a
information stack managed by the compiler. Growth advances a pointer, and
reminiscence is freed when its scope ends. The language’s kind and lifelong
guidelines make this reminiscence mannequin protected to make use of.
Why Goose
- Faster than C++ and than protected Rust, whereas reminiscence
protected. Over sixteen benchmarks Goose runs at 3.3x the velocity of
idiomatic C++, 1.16x hand-optimized C++ and 1.12x the most effective protected Rust, on
1.9x, 1.3x and 1.2x much less reminiscence (results). These positive factors come from Goose’s
information layouts and reminiscence mannequin. - No allocator, no GC, no reference counting, no
destructors. Memory is a handful of information stacks that the
compiler assigns statically. Freeing a million-element construction is one
retailer, nonetheless deeply it nests. - Growth preserves references. A reference right into a
rising array stays legitimate for so long as the array does. You hold typed
references the place C++ shouldreserveand protected Rust makes use of
u32indices within the corresponding benchmarks. - Memory protected with zero annotations. No lifetime
syntax, no aliasing or exclusivity guidelines, nounsafe. The
compiler infers what each reference is rooted in and rejects references
that might outlive their homeowners. - Nested information stays inline. A string, an array of
strings, a document with variable-size fields and an array of these
data are every one contiguous block with no pointer in it. A document
that’s 160 bytes and an allocation in C++ is 29 bytes and none in
Goose. - Enums that value what they maintain. Variable-mode ADTs
give every worth its personal variant’s dimension slightly than the most important one’s: 4x
much less reminiscence and 2x the velocity of a Rustenumon the
benchmark that workouts it. - Links narrower than pointers. A relative reference
shops a typed, checked hyperlink as a 1, 2 or 4-byte offset. Structures
constructed from them are place unbiased, so your information construction is
already its file format: saving is a write, loading is a learn plus a
verification go that rejects hostile bytes. - Everything is inbuilt place, assured. A price
is constructed at its last vacation spot by any depth of calls.
gadgets.push(parse(line))writes the parsed document straight
into the array, and returning a growable array by worth prices
nothing. - Returns throughout a number of calls.
return err from loadreturns from a perform any variety of
frames up, statically checked, with no unwinder, noResult
kind and no?on each name. - Threads that share nothing. A employee is compiled as
a separate program with its personal reminiscence, and flat values cross typed
queues as amemcpy. Data races, locks, atomics and reminiscence
orderings don’t exist within the language. - Generics and higher-order features with no
overhead. An untyped parameter is generic. Function values are
compile-time entities, soxs.filter() { it > 0 }
compiles to the loop it seems like and builds its outcome straight into
its vacation spot. - Plain C in, plain C out. Goose compiles to at least one C
file, so it runs wherever a C compiler does and calls C instantly by
extern fn. The bundled TinyCC backend compiles and runs a
program in-process, with no construct step.
The tutorial walks by all of
this by instance, the reference covers
the entire language subject by subject, the specification has the precise guidelines, and
the benchmarks have the numbers, losses
included.
Features
This part focuses on what distinguishes Goose. The tutorial explains these options in additional
element, and the samples present them in
thirty-one full packages.
One reminiscence
mannequin: stacks, and scope exit is the free
A program has the native name stack, static information and N information stacks,
the place the compiler works out N. A knowledge stack is a big address-space
reservation with a bump pointer, and there’s no different reminiscence. At most
one resizable worth is stay per stack and it’s at all times on prime, so progress
by no means strikes something and by no means checks a capability. All of that is proved
at compile time; the runtime tracks solely the bump pointers.
for round in 3 {
var scratch: u8[>..] = []; // grow-only: growth is a pointer bump
for i in 100000 { scratch.push((i % 256) as! u8); }
print("round ", round, ": ", scratch.len, " bytes");
} // the free: one store to the stack top
Nothing moves, so
references survive growth
struct Item { id: i32, weight: f32 }
var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 }); // a reference to element 0
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5; // still valid, a million pushes later
push returns a reference to the new element, so a
program can link elements while building the array. A
vector or Vec may reallocate
and cannot provide this guarantee. Stable references account for several
benchmark gains.
Safe references, no
annotations
Every reference and slice has a static root: the variable
that bounds its target’s lifetime. A reference must not outlive the
variable that owns its target or access that target through an
incompatible type. The compiler infers roots and specializes functions
for them, so no annotations are needed. There are no aliasing or
exclusivity rules. When a lifetime check fails, the error names the
reference’s root and its destination:
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); // error: storing a reference rooted at inner,
} // which does not outlive the destination (§9.2)
A slice T[:] lets a function accept a range from any
array kind without copying its elements. For example, split
returns slices into its input, and a dictionary keyed by
u8[:] stores views of strings rather than copies.
Nested data stays inline
There is no single array type. There is a family that differs only in
what happens to the size, and every member is
[metadata][elements...], inline and packed, never a pointer
to an element block:
| Spelling | What it is |
|---|---|
T[k] |
fixed size, known at compile time |
T[], T[varint] |
sized at construction, frozen after; a fixed or variable-width length |
T[..k], T[..] |
capacity inline; grows and shrinks within it |
T[>..] |
grow-only: the workhorse; references into it stay valid |
T[>..<] |
grow-shrink: stacks, queues, heaps |
Strings are just u8 arrays: u8[>..] is a
builder, u8[] a finished string stored inline in whatever
holds it, u8[..16] a small string inside a struct,
u8[:] a view. A struct may contain variable-size parts, and
they sit inline in declaration order, so a record is a run of bytes with
nothing indirect in it:
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 }] });
That order is 29 bytes and 0 allocations. As C++
std::string + std::vector it is
160 bytes and 1 allocation, and as Rust String +
Vec 153 bytes and 4. A whole order book is one
array that streams through the cache. The price is that an array of
variable-size elements is sequential: you can iterate it but not index
it (unless you keep a second array of references into it as an
index).
Enums in two sizes
Algebraic data types are the only dynamic polymorphism: no
inheritance, no vtables. Every ADT can be stored two ways, chosen at the
point of use. Fixed mode (Shape) is a tag plus room for the
largest payload, indexable and overwritable. Variable mode
(Shape..) gives each value exactly its variant’s size; the
array becomes sequential and a value never changes variant, and in
exchange you may take references into a payload:
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }
var packed: Shape..[>..] = []; // 9, 17 and 1 bytes, not 17 each
packed.push(Shape.Circle { r: 1.0 });
packed.push(Shape.Rect { w: 2.0, h: 3.0 });
packed.push(Shape.Dot);
for s in packed {
match s { Rect &r => { r.w += 1.0; }, _ => {} } // edits the payload in place, inside the array
}
Case functions provide another way to dispatch on a variant:
define one overload per variant. Calls dispatch on the tag through a
jump table, and the compiler checks that the overloads cover every
variant:
fn area(s: Shape.Circle) -> f64 { 3.14159 * s.r * s.r }
fn area(s: Shape.Rect) -> f64 { s.w * s.h }
fn area(s: Shape.Dot) -> f64 { 0.0 }
for s in packed { print(s, " area ", area(s)); }
Links narrower than pointers
A T& is a machine address: eight bytes, never
dangling. A relative reference is the same link stored as a
narrow offset: T& is measured from the field
itself to a target in the same array, so the structure is position
independent, and T& from a named
pool’s base, so other arrays can link into the pool.
T& uses offset 0 as null. Here is a binary
search tree in one grow-only array, with 12-byte nodes:
struct Node { key: i32, left: Node&<u32>?, right: Node&<u32>? } // 12 bytes, links included
fn insert(pool: Node[>..]&, key: i32) {
if pool.len == 0 { pool.push(Node { key: key }); return; }
var cur .= pool[0]; // .= binds a reference; = would copy the node
loop {
if key == cur.key { return; }
let next = if key < cur.key { cur.left } else { cur.right };
if next { cur .= next; continue; } // narrowed by `if`: next is a Node&
if key < cur.key { cur.left .= pool.push(Node { key: key }); }
else { cur.right .= pool.push(Node { key: key }); }
return;
}
}
References are transparent (no *, no
->), .= binds or retargets one, and
Node? is a nullable reference that if,
guard and assert narrow. The insert is one
push and one store, and the push cannot invalidate cur.
When lifetimes are not nested, a reusable pool pairs a
grow-only array with a hidden freelist: alloc_ref reuses a
slot or pushes, free hands one back, and since nothing is
ever actually freed, a stale slot reads a different value of the
correct type rather than corrupting memory.
Your data is already a file
format
var image: u8[>..] = tree.to_bytes(); // a framed byte image of the whole array
var loaded, ok = from_bytes<Node[>..]>(image); // verified before it becomes a value
image[image.len - 3] = 200; // tamper with a link ...
var bad, bok = from_bytes<Node[>..]>(image); // ... and it is false and an empty array
The tree’s links are self-relative, so it means the same thing
wherever it sits: saving is writing its bytes and loading is reading
them back, with no serializer, no schema and no pointer fixups.
from_bytes checks the framing, every tag, every length and
every link before the bytes become a value, so a corrupt or hostile file
is a false, never a wild reference.
Built in place, always
The copy-free construction guarantee (spec
§4.3) says a constructed worth is at all times inbuilt its last residence,
propagated top-down by calls. A perform returning a grow-only
array by worth writes its components straight into the caller’s variable,
or right into a discipline of the document being constructed inside one other array, so
out-parameters principally don’t seem.
words.push(str("word", i)); // formatted straight into the new element
let evens = xs.filter() { it % 2 == 0 }; // built straight into `evens`: no temporary
book.push(Order { id: 1001, customer: "alice", items: parse_items("SKU-441:2:1999;SKU-7:1:500") });
Returning errors across
calls
struct User { name: u8[..16], age: i32 }
fn load(text: u8[:]) -> User[>..], u8[] {
var users: User[>..] = [];
each_split(text, 'n') { users.push(parse_user(it)); };
return users, "";
}
fn parse_user(line: u8[:]) -> User { // returns a User: no Result, no error parameter
let comma = find(line, ",");
guard comma >= 0 else { return [], str("expected name,age: ", line) from load; }
let age, ok = parse_int(line[comma + 1..]);
guard ok else { return [], str("bad age: ", line) from load; }
let name = trim(line[..comma]);
return User { name: name, age: age as i32 };
}
return E from f returns E as the result of
the innermost active call of f, however many frames up, and
every function in between keeps its plain signature. It is checked
statically, so every call of parse_user must lie inside a
load call and nothing is uncaught at runtime. It is a
hidden discriminant checked per frame rather than an unwinder, and the
message is built directly where load‘s caller wants it. The
parsers in the samples use it for every syntax error.
Generics and function blocks
fn twice(x) { x + x } // an untyped parameter is a generic one
fn each_pair<T, F>(xs: T[:]) { // F is a function value: a compile-time entity
for i in 0..xs.len - 1 { F(xs[i], xs[i + 1]); }
}
fn first_gap(xs: i64[:]) -> i64 {
each_pair(xs) { a, b => if b - a > 1 { return a; } }; // returns from first_gap, not each_pair
return -1;
}
let evens = xs.filter() { it % 2 == 0 };
let total = fold(xs, 0) { acc, x => acc + x };
sort(xs) { a, b => a > b };
Everything is monomorphized. Type arguments are inferred from
argument types; parameters absent from those types can be supplied
explicitly. Function values are passed as generic parameters: every call
is direct and inlinable, they cannot escape, and there are no closure
objects or function pointers, so a higher-order function compiles to
exactly the loop it looks like. A block may return from its
lexically enclosing function, and nested functions see the enclosing
function’s locals.
Threads that share nothing
thread_fn worker() { // a separate program with its own memory
loop {
let job = qget<Job>(); // blocks on the typed queue for Job
guard job.n >= 0 else { break; } // -1 means stop
qput(Result { n: job.n, digits: count_digits(job.n) });
}
}
for i in n { ids.push(thread_spawn(worker)); }
There is no shared mutable memory. A thread_fn and
everything it calls is compiled as its own program with its own data
stacks and its own copies of the globals. Values cross through typed
queues, one per type, and must be flat, with no references at
any depth. That is cheap because a flat Goose value is contiguous: a job
or a result carries real data, pixels included, and crossing is a
memcpy.
A failed check or abort in a worker started with
thread_spawn_contained ends only that worker. Its message
goes to whoever waits for it, not to stderr, so a supervisor can log it
and start another:
var why: u8[>..] = [];
while !thread_wait(thread_spawn_contained(serve, attempt), why) {
log(why); // "assert failed (serve.goose:12)"
why.clear();
attempt++;
}
The first thread_wait(id, msg) for a worker takes the
message; a caller that doesn’t ask for it loses it.
Plain C in, plain C out
extern fn hypot(x: f64, y: f64) -> f64; // straight from libm
extern fn crc32_bytes(s: const u8[:]) -> u32; // a slice crosses as { data, len }
extern fn stats_of(xs: i32[:], out: Stats&); // a struct filled through a pointer
An extern fn binds a Goose signature to a C symbol. The
math and os modules use this interface. Only
types with supported C representations may cross the boundary;
unsupported signatures are rejected at the declaration. The compiler
emits one C file for the whole program. The bundled TinyCC backend can
compile and run that C inside the Goose compiler’s process.
Where the speed comes from
Much of the speed comes from the memory model: no heap allocation or
per-object teardown, contiguous data, narrow links, and enums sized for
the variant they contain. Every index is bounds-checked, and the
compiler removes checks it can prove unnecessary. An assert
on a slice length often lets a kernel eliminate its remaining checks and
vectorize. --bce-lines reports the checks that remain.
checkfree fn rejects a kernel if any bounds check survives;
index types let graph edges retain validation done during construction.
Benchmark measurements use whole-process wall-clock time, including
teardown.
| Geometric mean over 16 benchmarks | vs idiomatic C++ | vs hand-optimized C++ | vs best safe Rust |
|---|---|---|---|
| speed, MSVC backend | 3.51x | 1.24x | 1.13x |
| speed, clang backend | 3.54x | 1.26x | 1.19x |
| peak memory | 1.95x less | 1.32x less | 1.25x less |
The summary explains the positive factors and
losses; results.md has each
measurement, and design.md explains what
the suite assessments.
What it prices you
- You take into consideration the place information lives: who owns this, and the way lengthy does
its scope final. Usually the reply is “the perform that builds it”, and
that’s free; when it isn’t, it’s areusablepool. - Recursive features can not hold growable information throughout a recursive
name; scratch ends earlier than the decision, and what outlives it’s handed in as
a pool. - Arrays of variable-size components iterate however don’t index (wanting
an index of references stored beside them), a fixed-mode enum can’t be
pointed into, and a variable-mode one can’t be overwritten. You select
per container. - No escaping closures, no perform pointers, no dynamic dispatch
past ADT tags, and whole-program compilation solely. - A slot handed again to a
reusablepool and nonetheless named
reads no matter its subsequent proprietor put there: a logic bug, by no means reminiscence
corruption.
Tutorial §18 is
the complete listing.
Install a launch
Prebuilt compilers for Windows (x64), Linux (x64) and macOS (Apple
silicon and Intel) are on the releases page,
with the graphics, audio, ui and physics packages compiled in. Unpack
one anyplace and put its listing on PATH: the compiler finds the
commonplace library, TinyCC’s runtime and the packages beside itself, and
retains what it builds underneath ~/.goose
(GOOSE_HOME). The archive additionally holds these docs, the
samples and the VS Code extension.
Running a program in-process (goose file.goose) wants
nothing else on Windows. On Linux it wants the C library’s headers
(libc6-dev, or glibc-devel); on macOS, the SDK
of the Xcode command line instruments (xcode-select --install).
goose construct additionally wants a C compiler: Visual Studio 2022 or
later, or clang, on Windows; gcc or clang elsewhere. The Linux compiler
wants glibc 2.35 or later (Ubuntu 22.04, Debian 12), the macOS one macOS
13.3 or later. macOS retains a downloaded archive’s information from operating
till xattr -dr com.apple.quarantine is run on the unpacked
listing.
Build and run
You want CMake 3.27 or later, a C++20 compiler (MSVC, clang or gcc)
and Python 3 for the check and pattern runners. TinyCC, which the
in-process backend is constructed from, is a part of the tree. The graphics,
audio, ui and physics modules are packages in repositories of their very own
(goose_gfx and goose_physics;
docs/design/packages.md):
-DGOOSE_BUILTIN_LIBS=all fetches them on the pinned tags
(git, at configure time) and compiles them into the compiler, so their
packages run with nothing else put in. Without it the compiler builds
alone, and a program importing one among them has it constructed into
~/.goose the primary time, which wants git, cmake and a C
compiler.
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
GOOSE_BUILTIN_LIBS can also name some of the packages
(physics, or gfx, which carries
audio and ui too). On Linux, SDL needs the X11
or Wayland development packages (third_party/SDL/docs/README-linux.md);
with out them the construct leaves the gfx module out and says
what to put in.
The runner scripts desire construct/Release, then
construct/RelWithDebInfo, then construct, then
construct/Debug. Pass --exe to pick a particular
compiler; use an express Debug executable when testing with compiler
assertions enabled. The compiler is construct/goose
(construct/Release/goose.exe with the Visual Studio generator),
and it finds the usual library within the supply tree it was constructed from.
On Windows 10 model 1903 and later, its embedded manifest selects a
UTF-8 course of code web page so compiler paths don’t rely on the system
locale. Earlier Windows variations retain their legacy code web page.
Generated native executables retain their very own course of settings.
Run a program straight from supply, in-process
(goose run says the identical explicitly, and
goose examine solely typechecks):
build/goose samples/01_tour.goose
Or build an executable, which lands beside the source
(-o puts it elsewhere). The compiler finds a C compiler on
the machine (goose toolchain says which: MSVC or clang on
Windows, cc, gcc or clang elsewhere; --cc chooses) and
links the generated C with the runtime and whatever native modules the
program uses, graphics included. Programs that use threads need this
route, since TinyCC cannot place thread-local storage.
build/goose build samples/01_tour.goose && samples/01_tour
build/goose build samples/27_gfx_cube.goose && samples/27_gfx_cube
--debug turns on the overflow, range and tag checks in
the generated C, each abort naming its source line (and an overflow or a
cast the values involved), and builds the C with debug information;
--dry-run prints the commands a build would run, for a
build system of your own; -v prints them as they run.
Intermediates go under ~/.goose (GOOSE_HOME),
the runtime object compiled once per C compiler. Useful flags of both
forms: -O0/-O1/-O2 set the
inlining level (a build defaults to -O2, a run to
-O1), --bce-lines reports the bounds checks
kept per line, and -DGS_DEBUG=1 is what
--debug defines. Every resizable value lives on a data
stack of 2 GB of reserved address space, committed as it is used:
--stack-reserve 16G changes that for a program,
--stacks reports how many data stacks the program and each
of its workers can take and the cap that puts on
hardware_threads(), and -DGS_STACK_STATS=1
makes a program report its actual use as it ends.
A program with C of its own names it in a sidecar beside the source,
foo.build beside foo.goose:
c_sources = ["impl.c"];, with c_include,
c_defines, c_libs and _windows,
_linux or _macos variants of each for one
platform’s part. Both forms build it: goose build links it
in, and goose run builds it into a library the in-process
run loads, so an extern fn reaches it either way. A library
does the same with a sidecar beside its entry (or names the static
library target of its own CMake project,
cmake_target = "x";, which the compiler builds through
cmake), and a program takes a library with
import name from "source"; in its root file: a directory
("./lib"), or a git repository at a tag or commit
("owner/repo@v1" on GitHub,
"host/owner/repo@ref"), fetched into ~/.goose
and built there once per C compiler
(docs/design/packages.md).
Underneath, goose -o tour.c tour.goose writes the C,
which holds the program and the part of the runtime that inlines into
it; the rest of the runtime (the OS layer, data stack reservation,
threads, printing) is one more C file, which
goose --emit-runtime goose_runtime.c writes, to be compiled
once and linked with every program
(cc -O2 tour.c goose_runtime.o -o tour -lm -pthread; on
Windows cl /O2 tour.c goose_runtime.obj). Build the runtime
with the compiler and flags the programs use; it takes no
-D of its own, and a program from another version of the
compiler fails to link against it. --standalone writes a C
file that holds the whole runtime instead, and builds on its own. A
program using audio, gfx, physics
or ui also links that module’s layer, which
goose --audio-link msvc|cc, --gfx-link,
--physics-link and --ui-link name in a
response file
(cc cube.c goose_runtime.o -o cube @$(goose --gfx-link cc),
a ui program’s before the gfx one it draws through).
goose build --dry-run prints all of this for the program at
hand.
A program using audio plays procedural mono/stereo PCM
through SDL3 without opening a graphics window. See the audio reference.
The check suite and the samples run on Windows, macOS and Linux:
python test/run_tests.py
python samples/run_samples.py
For editing, the VS Code extension
provides syntax highlighting, the compiler’s language server
(goose lsp: errors as you kind, hover, go to definition,
completion, rename, symbols) and one-key runs by the JIT:
code --install-extension vscode/goose-language.vsix
Other editors can run goose lsp themselves; editors has the configuration for Neovim,
Helix and Emacs.
Documentation
- Documentation index: the map of
every little thing under, and the basis of the documentation web site constructed from
these information (how). - Tutorial: the pleasant
introduction, by instance. Read this primary. - Language reference: each function,
subject by subject, with a compiling instance for every: the entire language,
written to be learn. - Language specification: the precise
guidelines, whenever you need to know why one thing didn’t compile. - Samples: thirty-one full
packages in studying order, from a tour of the language to a JSON parser,
a threaded Mandelbrot, a file tree constructed from two swimming pools, a spinning dice
on the GPU, hundreds of bins raining right into a heap and a to-do listing in
home windows of widgets. - Standard library: fourteen modules,
all readable Goose underneathstdlib/, together with
gfx, graphics on SDL3’s GPU API (how
it is built),physics, inflexible physique physics on Box3D (how
it is built), andui, fonts, text rendering and
game HUDs, home windows and widgets on Nuklear, drawn by gfx (how
it is built). - Building programs and using
packages:goose construct, libraries from git repositories
or directories, a program’s personal C, and how you can make a bundle of your
personal. - Benchmarks: the numbers, with the
full results and the design behind them. - Implementation notes: how the
compiler works, go by go, and how it is
tested.
Status
Goose is new. What exists immediately is a whole-program compiler of about
thirty thousand strains of C++, the specification, the usual library,
the samples, and a check suite that CI runs on Windows, macOS and Linux
with an additional sanitizer job. Deliberately out of scope for now: strikes
for resizable values, a couple of resizable per struct, labeled
break, namespace privateness, and extra OS/library entry. Open
gadgets are tracked within the specification’s
Appendix B.
License
Goose is licensed underneath the Apache License,
Version 2.0.
Authors
Wouter van Oortmerssen: Language Design, Compiler
Design, Coding requirements. home web page,
twitter.
Claude Fable: Compiler implementation, Benchmarking,
Sample & Doc writing.
History & Use of AI
This repo was produced virtually totally with AI, from a human language
design. I had designed Goose a number of years in the past, and had began to
implement it, however operating a sport startup (which is constructed on one other
programming language of mine, Lobster) there was no time to
end it. Which was unhappy, as a result of I knew Goose might do issues different
languages cannot, and it ought to exist.
I had not anticipated AI to assist with this till Fable got here out. I
thought it is perhaps succesful sufficient to do a very good job. I made it
primarily clone the type and construction of my different current compiler
(Lobster), which is why when you take a look at the code, it seems slightly related
to that. My preliminary design had left a lot of issues unspecified, and much
of forwards and backwards with Fable made me determine on all of these, and it’s
now a greater language for it.
It can be an experiment in utilizing AI to implement a brand new language,
slightly than reproduce an present one.
You might surprise why I had it work in C++, if clearly I might have used
any language, like Rust, or my very own Lobster, or.. Goose itself (that will
nonetheless occur). With Fable, it was my commentary that it’s virtually
equally succesful in any language, and the sturdy guardrails of Rust or
different languages aren’t as urgent as they as soon as had been. Since the
compiler emits C, you seemingly already must have a C/C++ compiler
round, so sticking inside one ecosystem would appear to simplify
deployment and adoption. I’ve additionally built-in libtcc and who is aware of what
different C libraries sooner or later. And, like I mentioned, to have the compiler
code mimic my present Lobster compiler appeared enjoyable, at the very least I can learn
it like it’s my very own.
