
2026/10/25 4:06
Goose プログラミング言語
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
Goose は、動的値をコンパイラ管理のスタック上にインライン保持することで、ヒープ割当とガベージコレクションを排除し、メモリ安全性を実現するシステム言語です。リソースの解放は手動のデアロケーションではなくスコープ終了経由で行われ、単一スタックメモリモデルによって再割当なしでの成長がサポートされ、参照がポインタが進むにつれて拡大する配列内を維持します。コンパイラはすべての参照に対する静的ルートを自動的に推論し、所有者よりも長生きする可能性があるものは却下し、ライフタイムアノテーションやアンセーフブロックの必要はありません。
パフォーマンスベンチマークでは、Goose が標準的な C++、手動最適化された C++、そして安全な Rust に比べて速度とメモリ使用量でともに優れています:標準的な C++ の場合最高 3.3 倍高速に動作し、メモリ使用量は約半分(1.9 倍削減)です。アーキテクチャ上の革新であるバリアントモード enums(各バリアントの正確なサイズを格納)、ナローリンク(位置非依存参照のために 1/2/4 バイトのオフセットを使用)、およびインプレース構築は、オーバーヘッドが著しく低い連続したネスト型データ構造を可能にします。エラーは関数フレーム間を静的に返すことができ、アンワインダーや Result タイプを必要とせず、スレッドは分離されたメモリ空間で動作し、memcpy によってフラットな値が転送されることでデータ競合とロックを回避します。全体として、Goose は組み込みシステムや高频度取引といったクリティカルドメインにおいて高いパフォーマンスと安全性を提供するとともに、開発ライフサイクルを簡素化します。
本文
Goose: メモリーセーフなシステムプログラミング言語
Goose は、ヒープアロケーター、ガベージコレクション、ライフタイム注釈を一切使用しないメモリーセーフなシステムプログラミング言語です。ベンチマーク結果によると、C++ よりも高速でありながら、使用するメモリー量も少ないのが特徴です。
- チュートリアル
- 参考資料
- 仕様書
- パッケージ
- サンプルコード
- ベンチマーク
- 標準ライブラリ
なぜ Goose か?
Goose は C++ よりも高速で、メモリー安全性を保証しつつ Rust のようなメモリーセーフな性質を持ちます。16 つのベンチマークにおいて、以下の性能を示しています。
- 慣用的な C++: 3.3 倍高速
- 手動最適化された C++: 1.16 倍高速
- 最も安全な Rust の実装: 1.12 倍高速
- メモリー削減: 上記と比較し、それぞれ 1.9 倍、1.3 倍、1.2 少ないメモリー使用量
これらの利点は、Goose が採用しているデータ配置とメモリーモデルによって生み出されています。
主要な特徴
-
アロケーターも GC もない メモリはコンパイラが静的に割り当てる数段のデータスタックに過ぎません。百万要素の構造体を解放する操作は、ネストが何重にあろうとも単一のストア(ストア指令)で完了します。
-
拡張に伴う参照の不変性 配列が増大しても、その中への参照は無効化されません。C++ では予約を迫られるのに対し、Rust では
インデックスを使用するのに対して、Goose は型付けされた参照を保持し続けます。u32 -
注釈ゼロでのメモリー安全性 ライフタイム構文なし、アライニングや排他性のルールなし、
キーワードなしです。コンパイラは各参照が何の根拠(ルートの変数)を持っているかを推論し、所有者を超えて生き延びる可能性がある参照を拒否します。unsafe -
ネストしたデータはインラインのまま 文字列、文字列配列、可変サイズのフィールドを持つ構造体などは、いずれも連続したブロックであり、ポインターを含みません。C++ ではアロケーションが必要になるものが、Goose ではアロケーション自体が存在しません。
-
ADT は保持するサイズにのみコストがかかる 変長モードの代数型データ(ADT)は、各値が自身のバリアントのサイズを持つように設計されています。Rust の
と比較するとメモリー使用量は 4 分の 1、速度は 2 倍になります。_enum_ -
ポインターより狭いリンク 相対参照は、1 バイト、2 バイト、または 4 バイトのオフセットとして型付けされ検証済みのリンクを格納します。これらで作成された構造体は位置非依存であり、データ構造自体がファイルフォーマットとなります。
-
すべては元のリポジトリで構築 値は、呼び出しの深い階層を通じて最終目的地で構築されます。
は解析済みの構造体をそのまま配列に書き込み、成長可能な配列を値として返すコストはゼロです。items.push(parse(line)) -
複数回の呼び出しを超えたエラーの返却
関数からload
とする際、静的に検証された状態で任意のフレーム数を上に遡って返却できます。アンワインダーも、return err
タイプも、各呼び出しに対するResult
も不要です。? -
共有しないスレッド ワーカーは独自のメモリーを持つ独立したプログラムとしてコンパイルされます。フラットな値が型付けされたキューを介してコピー(memcpy)として渡されます。データレース、ロック、アトミック演算、メモリー順序化は言語の設計上存在しません。
-
オーバーヘッドなしでジェネリクスと higher-order 関数 非型付けされたパラメータはジェネリックです。関数値はコンパイル時間のエンティティとなるため、
は見た目通りのループにコンパイルされ、結果をその目的地に直接構築します。xs.filter() { it > 0 } -
インプットもアウトプットも純粋な C Goose は一つの C ファイルへコンパイルするため、C コンパイラが動作するどこでも動作し、
を通じて C に直接呼び出せます。bundled TinyCC バックエンドはプロセス内でプログラムをコンパイル・実行するため、ビルドステップは不要です。extern fn
ドキュメントリソースの概要
- チュートリアル: 例を用いてすべてを解説します。
- 参考資料 (Reference): 言語全体をトピック別に網羅しています。
- 仕様書 (Specification): 厳密なルールが記載されています。
- ベンチマーク: データと損失率も含まれています。
特徴詳細
このセクションは Goose を特徴づける点を焦点としています。チュートリアルではこれらの機能を詳しく説明し、サンプルコードでは 31 つの完全なプログラムでそれらが示されています。
一つのメモリーモデル:スタックのみ、スコープ出口での解放
プログラムにはネイティブの呼び出しスタック、静的データ、そしてコンパイラが導き出すデータスタックがあります。データスタックは大きなアドレス空間予約とインクリメントポインタ(バンプポインタ)を持ち、それ以外のメモリーはありません。
一つの実行例:
for round in 3 { var scratch: u8[>..] = []; // 成長専用:成長はポインタのインクリメント for i in 100000 { scratch.push((i % 256) as! u8); } print("round ", round, ": ", scratch.len, " bytes"); } // 解放:スタックのトップへの一つのストアで完了
データは移動しないため、参照も成長に伴う
配列が増大しても、既存要素への参照は無効化されません。
vector<T> や Vec<T> は再アロケートが必要となる場合がありますが、Goose では安定した参照を維持します。
struct Item { id: i32, weight: f32 } var items: Item[>..] = []; let first .= items.push(Item { 1, 0.5 }); // エレメント 0 への参照 for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); } first.weight = 99.5; // まだ有効、百万回のプッシュの後でも
安全な参照、注釈なし
すべての参照とスライスは静的なルート(対象のライフタイムを束縛する変数)を持ちます。参照は、そのターゲットを所有する変数の寿命を超えてはなりません。コンパイラはルートを推論し、それらに特化した関数を生成するため、注釈は不要です。
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); // エラー:inner にルートを有する参照を、lifetime の短い目的地に代入 (§9.2) }
ネストしたデータはインラインのまま
単一の配列タイプはありません。サイズに対して何が起こるかによって異なるファミリーがあり、各メンバーはインラインでパックされており、要素ブロックへのポインターではありません:
| 表記法 | 内容 |
|---|---|
| コンパイル時に固定サイズ |
| 構築時にサイズが決まり、その後凍結される;固定または可変幅の長さ |
| キャパシティがインライン;その中で成長・縮小 |
| 成長専用:主力;参照は有効性を保つ |
| 成長・縮小兼用:スタック、キュー、ヒープ |
構造体が可変サイズの部分を含むことができ、それらは宣言順序でインライン配置されるため、レコードはバイトの連続であり、間接参照は一切含まれません:
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 }] });
この注文レコードは29 バイトであり、アロケーションは 0 です。C++ の
std::string + std::vector<Item> では 160 バイトで 1 アロケーション必要、Rust の String + Vec<Item> では 153 バイトで 4 アロケーション必要です。
二つのサイズを持つ ENUMS
代数型データ(ADT)だけが動的多態性を提供します:継承も仮想テーブル(vtables)もありません。すべての ADT は使用時点で選択される两种方式で格納できます。
- 固定モード (
): タグと最大ペイロード分のスペースを持ち、インデックス可能かつ書き換え可能です。Shape - 変長モード (
): 各値にバリアントごとの正確なサイズを与え、配列は順序的になり、値はバリアントを変更しないため、代わりにペイロードへの参照を取得できます。Shape..
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot } var packed: Shape..[>..] = []; // 各 17 バイトではなく、9, 17, 1 バイト 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; }, _ => {} } // 配列内のペイロードを現地で編集 }
ポインターより狭いリンク
T& はマシンアドレス(8 バイト)であり、決してダングリングしません。相対参照は同じリンクを狭いオフセットとして格納します:
: 同じ配列内のフィールド自体からターゲットまでの距離T&<u32>
: 名前付きプールのベースからの距離T&<u32 in pool>
: オフセット 0 を NULL と扱うT&<u32>?
以下は 12 バイトのノードを持つ単一の成長専用配列でのバイナリ検索ツリーです:
struct Node { key: i32, left: Node&<u32>?, right: Node&<u32>? } // 12 バイト、リンク込み fn insert(pool: Node[>..]&, key: i32) { if pool.len == 0 { pool.push(Node { key: key }); return; } var cur .= pool[0]; // .= は参照をバインド;= はコピーする loop { if key == cur.key { return; } let next = if key < cur.key { cur.left } else { cur.right }; if next { cur .= next; continue; } // `if` で狭められた:next は Node& 型 if key < cur.key { cur.left .= pool.push(Node { key: key }); } else { cur.right .= pool.push(Node { key: key }); } return; } }
あなたのデータはすでにファイルフォーマットです
ツリーのリンクは自己相対的であるため、どこに置かれても同じ意味を持ちます。保存はそのバイトを書き込むこと、読み込みはそれを読み戻すことです(シリアライザー、スキーマ、ポインター修正なし)。
var image: u8[>..] = tree.to_bytes(); // 全体の配列のフレーム付きバイトイメージ var loaded, ok = from_bytes<Node[>..]>(image); // バリューになる前に検証済み image[image.len - 3] = 200; // リンクを改ざん ... var bad, bok = from_bytes<Node[>..]>(image); // ... これは偽で空配列となる
常に元のリポジトリで構築
コピーフリーな構築の保証(仕様 §4.3)によれば、構築された値は常にその最終的な住処で構築され、呼び出しを介して下から上に伝播します。成長専用配列を値として返す関数は、その要素を直接呼び出し元の変数に書き込むため、アウトパラメータはほとんど出ません。
words.push(str("word", i)); // フォーマットされたデータが新しい要素に直接書き込まれる let evens = xs.filter() { it % 2 == 0 }; // 一時的なしで `evens` に直接構築される book.push(Order { id: 1001, customer: "alice", items: parse_items("SKU-441:2:1999;SKU-7:1:500") });
エラーを呼び出しを超えて返却する
return E from f は、最も内側のアクティブな呼び出しの結果として E を返し、その間にいくつのフレームがあろうとも返却されます。これは静的に検証されるため、各呼び出しは必ず親呼び出しの内側にある必要があります。
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 { // Result 型やエラーパラメータなし 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 }; }
ジェネリクスと関数ブロック
非型付けされたパラメータはジェネリックとなります。関数値はコンパイル時間のエンティティとなり、すべての呼び出しは直接的かつインライン可能です。
fn twice(x) { x + x } // 非型付けパラメータはジェネリックとなる fn each_pair<T, F>(xs: T[:]) { // F は関数値:コンパイル時間のエンティティ 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; } }; // `each_pair` からではなく `first_gap` から返却される return -1; } let evens = xs.filter() { it % 2 == 0 }; let total = fold(xs, 0) { acc, x => acc + x }; sort(xs) { a, b => a > b };
スレッドと共有不変メモリー
共有不変メモリーはありません。
thread_fn とその呼び出しすべては、独自のデータスタックとグローバルのコピーを持つ独自のプログラムとしてコンパイルされます。値はタイプ付けされたキュー(各タイプごとに一つ)を通じて渡され、あらゆる深さで参照を持たずフラットである必要があります。
thread_fn worker() { // 独自のメモリーを持つ独立したプログラム loop { let job = qget<Job>(); // Job タイプのキューでブロックする guard job.n >= 0 else { break; } // -1 は停止の意味 qput(Result { n: job.n, digits: count_digits(job.n) }); } } for i in n { ids.push(thread_spawn(worker)); }
thread_wait(id, msg) がメッセージを取得します。チェック失敗や中止は、そのワーカーのみを終了させます。スーパーバイザーはログに記録して別の作業を開始できます:
var why: u8[>..] = []; while !thread_wait(thread_spawn_contained(serve, attempt), why) { log(why); // "assert failed (serve.goose:12)" why.clear(); attempt++; }
インプット・アウトプットともに純粋な C
extern fn hypot(x: f64, y: f64) -> f64; // 直接 libm から extern fn crc32_bytes(s: const u8[:]) -> u32; // スライスは { data, len } として渡される extern fn stats_of(xs: i32[:], out: Stats&); // ポインタを通じて構造体を埋める
extern fn は Goose の署名を C のシンボルにバインドします。境界を越えられるのは、サポートされた C 表現を持つ型のみです。コンパイラはプログラム全体の C ファイル一個を出力します。bundled TinyCC バックエンドは Goose コンパイラのプロセス内でその C をコンパイル・実行できます。
速度の源泉
速度の多くはメモリーモデルに由来します:ヒープアロケーションやオブジェクトごとのデトーチなし、連続したデータ、狭いリンク、バリアントに適合した ENUMS です。すべてのインデックスは境界チェックされていますが、コンパイラは無用であることが証明されるチェックを削除します。
ベンチマーク測定(16 のベンチにおける幾何平均)
| メトリック | vs 慣用的 C++ | vs 手動最適化 C++ | vs ベスト safe Rust |
|---|---|---|---|
| 速度 (MSVC) | 3.51 倍 | 1.24 倍 | 1.13 倍 |
| 速度 (clang) | 3.54 倍 | 1.26 倍 | 1.19 倍 |
| ピークメモリー | 1.95 倍少ない | 1.32 倍少ない | 1.25 倍少ない |
詳細な測定値は
results.md に記載されています。また design.md ではスイートが何をテストしているか説明されています。
あなたに求めるコスト
- データの居場所: データの所有者とスコープについて考える必要があります(通常「構築する関数」)。そうではない場合、再利用可能なプールとなります。
- 再帰関数: 呼び出し間で成長データを保持できません。スケッチは呼び出し前に終わり、それを超えて生き残るものはプールとして渡されます。
- インデックスアクセス: 可変サイズの要素の配列は反復処理できますが、インデックスアクセスはできません(参照の配列を並べて保持しない限り)。固定モード ENUM はポインター指向できず、変長モード ENUM も書き換えられません。
- 動的ディスパッチ: エスケープするクロージャーも、関数ポインターも、ADT タグを超えた動的ディスパッチも、ホールド全体のコンパイルもありません。
- 再利用可能なプール: スロットは再利用されるため、論理上のバグ(メモリー破損ではありません)を引き起こす可能性があります。
チュートリアル §18 は完全なリストです。
インストール
リリース版のインストール
Windows (x64)、Linux (x64)、macOS 用のプリビルトコンパイラはリリースページにあります。グラフィックス、オーディオ、UI、物理シミュレーションのパッケージもコンパイル済みです。どこかであらかじめ展開し、そのディレクトリを
PATH に追加します。
- Linux: C ライブラリのヘッダー (
) が必須です。libc6-dev - macOS: Xcode コマンドラインツールの SDK (
) が必須です。xcode-select --install - Windows: Visual Studio 2022 以降または clang、あるいは gcc/clang(他プラットフォーム)が必要です。
ビルドと実行
CMake 3.27 以降、C++20 コンパイラー、Python 3 が必要です。TinyCC はプロセス内バックエンドからビルドされます。
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 はパッケージ(physics、gfx など)も指定できます。
ランナーは
build/Release を優先します。特定のコンパイラーを選択するには --exe を渡してください。
ソースから直接プログラムを実行
goose run は明示的に実行し、goose check は型チェックのみを行います:
build/goose samples/01_tour.goose
ビルドした実行可能ファイル(
-o で別場所)を使用する場合:
build/goose build samples/01_tour.goose && samples/01_tour
有用なフラグ:
/-O0
/-O1
: インライン化レベル(デフォルトは-O2
)-O2
: 行ごとの境界チェックを報告--bce-lines
: オーバーフロー、範囲、およびタグチェックをオンに--debug
: ビルドシステムの実行コマンドを表示--dry-run
C コードとのリンク
独自の C を持つプログラムは
foo.goose の横に foo.build が配置されます。goose build でリンクされ、goose run でプロセス内の実行にロードされます。
c_sources = ["impl.c"]; // ... c_include, c_defines 等の設定
テストとサンプルの実行
テストスイートとサンプルは Windows、macOS、および Linux で動作します:
python test/run_tests.py python samples/run_samples.py
VS Code 拡張機能は構文ハイライトとコンパイラの言語サーバー (
goose lsp) を提供します。
ドキュメント
- ドキュメントインデックス: 以下のすべてへの地図です。
- チュートリアル: 例による友好的な入門です。これを最初に読むべきです。
- 言語参照: すべての機能、トピックごとに網羅されています。
- 言語仕様書: 厳密なルールが記載されています。
- サンプル: 読書順序の 31 つの完全なプログラム(JSON パーサー、マンデルブロ集計など)。
- 標準ライブラリ:
以下の Goose モジュール 14 つ (stdlib/
,gfx
,physics
など)。ui
ステータス
Goose は新しいものです。現在存在するのは約 3 万行の C++ で構成されたホールド全体のコンパイラ、仕様書、標準ライブラリ、サンプルコード、そして CI が実行するテストスイートです。意図的に範囲外とされているもの:可変サイズ値への移動、構造体ごとの複数の可変サイズ値、ラベル付き
break、ネームスペースプライバシー、そしてより多くの OS/ライブラリアクセスなど。
ライセンス
Goose は Apache License, Version 2.0 の下でライセンスされています。
著者
- Wouter van Oortmerssen: 言語設計、コンパイラー設計、コーディング基準。
- Claude Fable: コンパイラー実装、ベンチマーク、サンプルおよびドキュメントの執筆。