neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
Goose 프로그래밍 언어이다
The Goose Programming Language
개발 도구C++ · PythonGitHub @aardappel직접 설치운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
SNS에서 화제 · 지금 30위 · 출처 1곳
10월 30일 04:46 확인 · 점수는 각 플랫폼이 보여 주는 값을 마지막 확인 시각에 옮긴 것입니다. 글 본문과 댓글은 옮기지 않고, 원문이 지워지면 여기서도 빠집니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
설치형 프로젝트는 배포 도메인 없이도 확인할 수 있습니다. 개인 저장소는 GitHub 로그인으로, 조직 저장소는 위의 저장소 확인 파일로 직접 확인해 주세요.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A memory-safe systems language with no heap allocator, garbage collector, or lifetime annotations. In this repository's benchmarks, Goose runs faster than C++ and Rust while using less memory.
Tutorial · Reference · Specification · Command line · Packages · Samples · Benchmarks · Standard library
Goose has syntax familiar to C and Rust programmers, but there is no heap. Every dynamic value lives inline on data stacks managed by the compiler. Growth advances a pointer, and memory is freed when its scope ends. The language's type and lifetime rules make this memory model safe to use.
reserve and safe Rust uses u32 indices.unsafe. The compiler
infers what every reference is rooted at and rejects references that could
outlive their owners.enum
and run at 2x its speed.items.push(parse(line))
writes the parsed record directly into the array, and returning a growable
array by value does not copy it.return err from load returns from a
function any number of frames up the call stack. It is checked statically,
and needs no unwinder, no Result type and no ? on every call.The tutorial walks through all of this by example, the reference covers the whole language topic by topic, the specification has the exact rules, and the benchmarks have the numbers, including the cases where Goose loses.
This section focuses on what distinguishes Goose. The tutorial explains these features in more detail, and the samples show them in thirty-one complete programs.
A program has the native call stack, static data and N data stacks, where the compiler works out N. A data stack is a large address-space reservation with a bump pointer, and there is no other memory. At most one resizable value is live per stack and it is always on top, so growth never moves anything and never checks a capacity. All of this is proved at compile time; the runtime tracks only 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
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<T> or Vec<T> may reallocate and cannot
provide this guarantee. Stable references account for several benchmark gains.
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.
There is no single array type. Instead there is a family of array types that
differ only in how their size can change. Every member is laid out as
[metadata][elements...], inline and packed, never as a pointer to a separate
block of elements:
| Type | 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 most commonly used kind; references into it stay valid |
T[>..<] | grow-shrink: stacks, queues, heaps |
Strings are 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, which are stored
inline in declaration order, so a record is a sequence of bytes with no
indirection 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<Item> it is 160 bytes and 1 allocation, and as Rust String +
Vec<Item> it is 153 bytes and 4 allocations. An entire order book is a single
array, so reading it streams through the cache. The trade-off 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).
Algebraic data types are the only form of dynamic polymorphism; there is no
inheritance and there are no vtables. Every ADT can be stored in two ways,
chosen at the point of use. Fixed mode (Shape) stores a tag plus room for
the largest payload, so values can be indexed and overwritten. Variable mode
(Shape..) gives each value exactly the size of its variant. An array of
such values becomes sequential and a value never changes variant, but 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)); }
A T& is a machine address: it takes eight bytes and never dangles. A
relative reference is the same link stored as a narrow offset. A T&<u32>
is measured from the field itself to a target in the same array, so the
structure is position independent. A T&<u32 in pool> is measured from the
base of a named pool, so other arrays can link into the pool. T&<u32>? 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 (there is no * or ->). .= binds or retargets
a reference, 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 free slot or
pushes a new one, and free returns a slot to the pool. Since a slot's memory
is never actually freed, a stale slot reads a different value of the correct
type rather than corrupting memory.
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 the tree means the same thing wherever
it is stored. Saving it writes its bytes and loading it reads 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 makes it return false, never a wild
reference.
The copy-free construction guarantee (spec §4.3) says a constructed value is always built at its final location, which is passed down through calls. A function returning a grow-only array by value writes its elements directly into the caller's variable, or into a field of the record being built inside another array, so out-parameters are rarely needed.
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") });
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 that call is. Every function in between keeps its
plain signature. The construct is checked statically: every call of
parse_user must lie inside a load call, so nothing can go uncaught at
runtime. It is implemented as a hidden discriminant checked in each frame
rather than as 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.
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;
type parameters that do not appear in those types can be supplied explicitly.
Function values are passed as generic parameters. Every call of a function
value is direct and can be inlined, function values 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 can use the enclosing
function's locals.
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 are passed between threads through typed queues, one per
type, and must be flat, with no references at any depth. Passing them is
cheap because a flat Goose value is contiguous: a job or a result can carry
real data, such as pixels, and passing it is a single 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.
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.
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 the
compiler remove the remaining checks in a kernel, so that the kernel can be
vectorized. --bce-lines reports the checks that remain. checkfree fn
rejects a kernel if any bounds check remains in it; index types let graph
edges keep the validation done while the graph was built.
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 gains and losses; results.md has every measurement, and design.md explains what the suite tests.
reusable pool.reusable pool but is still referenced
reads whatever its next owner put there. That is a logic bug, but never
memory corruption.Tutorial §18 is the full list.
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
anywhere and put its directory on PATH: the compiler finds the standard
library, TinyCC's runtime and the packages beside itself, and keeps what it
builds under ~/.goose (GOOSE_HOME). The archive also holds these docs, the
samples and the VS Code extension.
Running a program in-process (goose file.goose) needs nothing else on
Windows. On Linux it needs the C library's headers (libc6-dev, or
glibc-devel); on macOS, the SDK of the Xcode command line tools
(xcode-select --install). goose build also needs a C compiler: Visual
Studio 2022 or later, or clang, on Windows; gcc or clang elsewhere. The Linux
compiler needs glibc 2.35 or later (Ubuntu 22.04, Debian 12), and the macOS
compiler needs macOS 13.3 or later. macOS keeps a downloaded archive's files
from running until xattr -dr com.apple.quarantine is run on the unpacked
directory.
You need CMake 3.27 or later, a C++20 compiler (MSVC, clang or gcc) and Python
3 for the test and sample runners. TinyCC, which the in-process backend is
built from, is part of the tree. The graphics, audio, ui and physics
modules are packages in repositories of their own
(goose_gfx and
goose_physics;
docs/design/packages.md). -DGOOSE_BUILTIN_LIBS=all fetches them with git
at configure time, at the pinned tags, and compiles them into the compiler,
so programs that use them run with nothing else installed. Without that
option the compiler is built on its own, and the first time a program
imports one of these packages, the package is built into ~/.goose, which
needs 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 includes audio and ui too). On
Linux, SDL needs the X11 or Wayland development packages
(third_party/SDL/docs/README-linux.md);
without them the build leaves the gfx module out and says what to install.
The runner scripts prefer build/Release, then build/RelWithDebInfo, then
build, then build/Debug. Pass --exe to select a specific compiler; use an
explicit Debug executable when testing with compiler assertions enabled.
The compiler is build/goose (build/Release/goose.exe with the Visual Studio
generator), and it finds the standard library in the source tree it was built
from. On Windows 10 version 1903 and later, its embedded manifest selects a
UTF-8 process code page so compiler paths do not depend on the system locale.
Earlier Windows versions retain their legacy code page. Generated native
executables retain their own process settings.
Run a program directly from source, in-process, with no C compiler and no
build step (goose run is the explicit form of the same command):
build/goose samples/01_tour.goose
Or build an executable, which is written beside the source. The compiler
finds a C compiler on the machine (goose toolchain shows which one) and
links the generated C with the runtime and any native modules the program
uses, including graphics. Programs that use threads must be built this way,
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
goose check only typechecks. --debug turns on the overflow, range and
tag checks, each of which names its source line when it fails, and
--bce-lines reports the bounds checks that remain on each line. Every
resizable value lives on a data stack of 2 GB of reserved address space,
committed as it is used, and --stack-reserve 16G changes that size.
The command line describes every command and
option, including how to write the C for a build system or a C host of your
own (goose -o tour.c tour.goose).
A program with C of its own lists that C in a sidecar file beside the
source, foo.build beside foo.goose: c_sources = ["impl.c"];. Both
forms build that C, so an extern fn can call it either way. A program uses
a library by writing import name from "source"; in its root file. The
source is a directory ("./lib"), or a git repository at a tag or commit
("owner/repo@v1" on GitHub, "host/owner/repo@ref"), which is fetched
into ~/.goose and built there once per C compiler
(packages).
A program using audio plays procedural mono/stereo PCM through SDL3 without
opening a graphics window. See the audio reference.
The test 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 gives syntax
highlighting, the compiler's language server (goose lsp: errors as you
type, hover, go to definition, completion, rename, symbols) and one-key runs
through 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.
stdlib/. They include gfx for graphics on SDL3's GPU API (how
it is built)
and physics for rigid body physics on Box3D (how it is
built).
The ui module provides fonts, text rendering and game
HUDs. It also provides windows
and widgets on Nuklear, drawn through gfx (how it is
built).Goose is new. What exists today is a whole-program compiler of about thirty
thousand lines of C++, the specification, the standard library, the samples,
and a test suite that CI runs on Windows, macOS and Linux with an extra
sanitizer job. These are deliberately out of scope for now: moves for
resizable values, more than one resizable per struct, labeled break,
namespace privacy, and more OS/library access. Open items are tracked in the
specification's Appendix B.
Goose is licensed under the Apache License, Version 2.0.
Wouter van Oortmerssen: Language Design, Compiler Design, Coding standards. home page, twitter.
Claude Fable: Compiler implementation, Benchmarking, Sample & Doc writing.
This repo was produced almost entirely with AI, from a human language design. I had designed Goose several years ago, and had started to implement it, but running a game startup (which is built on another programming language of mine, Lobster) there was no time to finish it. Which was sad, because I knew Goose could do things other languages can't, and it should exist.
I had not expected AI to help with this until Fable came out. I thought it might be capable enough to do a good job. I made it essentially clone the style and structure of my other recent compiler (Lobster), which is why if you look at the code, it looks rather similar to that. My initial design had left lots of things unspecified, and lots of back and forth with Fable made me decide on all of those, and it is now a better language for it.
It is also an experiment in using AI to implement a new language, rather than reproduce an existing one.
You may wonder why I had it work in C++, if clearly I could have used any language, like Rust, or my own Lobster, or.. Goose itself (that may still happen). With Fable, it was my observation that it is almost equally capable in any language, and the strong guardrails of Rust or other languages are not as pressing as they once were. Since the compiler emits C, you likely already need to have a C/C++ compiler around, so sticking inside one ecosystem would seem to simplify deployment and adoption. I've also integrated libtcc and who knows what other C libraries in the future. And, like I said, to have the compiler code mimic my existing Lobster compiler seemed fun, at least I can read it like it is my own.
memcpyxs.filter() { it > 0 } compiles to what the handwrittend loop would looks like and builds its
result directly in its destination.extern fn. The bundled
TinyCC backend compiles and runs a program in-process, with no build step.goose