Goose: C++より1.16倍、安全なRustより1.12倍高速でメモリ安全
Goose: 1.16x faster than C++ and 1.12x than safe Rust, while memory safe (github.com)
要約
Gooseは、メモリ安全性を持ちながらC++や安全なRustよりも高速で、より少ないメモリ使用量で動作する新しいシステムプログラミング言語です。ヒープやアロケータ、GCを持たず、コンパイラ管理下のデータスタックに全ての動的値を配置することで、パフォーマンスとメモリ安全性を両立させています。ライフタイムアノテーションやunsafeブロックも不要で、コンパイラが参照の生存期間を静的に管理します。
全文翻訳
The Goose Programming Language
[画像: The Goose programming language]
ヒープアロケータ、ガベージコレクタ、ライフタイム注釈なしのメモリ安全なシステム言語。このリポジトリのベンチマークでは、GooseはC++やRustよりも高速に動作し、メモリ使用量も少なくなっています。
チュートリアル · リファレンス · 仕様 · パッケージ · サンプル · ベンチマーク · 標準ライブラリ
GooseはCやRustプログラマには馴染みのある構文ですが、ヒープはありません。すべての動的値は、コンパイラによって管理されるデータスタック上にインラインで配置されます。成長はポインタを進め、スコープが終了するとメモリは解放されます。言語の型とライフタイムの規則により、このメモリモデルは安全に使用できます。
[画像: Discord Chat] [画像: Twitter Follow]
なぜGooseか
- メモリ安全でありながら、C++や安全なRustよりも高速。 16以上のベンチマークで、Gooseは慣用的なC++の3.3倍、手作業で最適化されたC++の1.16倍、そして最良の安全なRustの1.12倍の速度で動作し、メモリ使用量はそれぞれ1.9倍、1.3倍、1.2倍少なくなっています(結果)。これらの向上は、Gooseのデータレイアウトとメモリモデルによるものです。
- アロケータなし、GCなし、参照カウントなし、デストラクタなし。 メモリは、コンパイラが静的に割り当てる少数のデータスタックです。百万要素の構造体を解放するのは、どれだけ深くネストしていても、1回のストア操作で済みます。
- 成長は参照を保持する。 成長する配列への参照は、配列が存在する限り有効なままです。C++が
reserveする必要がある場所や、安全なRustが対応するベンチマークでu32インデックスを使用する場所で、型付き参照を保持できます。 - 注釈なしでメモリ安全。 ライフタイム構文なし、エイリアシングや排他性のルールなし、
unsafeなし。コンパイラは、すべての参照が何に由来するかを推論し、所有者よりも長く存在する可能性のある参照を拒否します。 - ネストされたデータはインラインのまま。 文字列、文字列の配列、可変サイズのフィールドを持つレコード、そしてそれらのレコードの配列は、ポインタを含まない単一の連続したブロックになります。C++では160バイトの割り当てが必要なレコードが、Gooseでは29バイトで済みます。
- Enumは保持するものをコストとする。 可変モードのADTは、各値が最大のバリアントのサイズではなく、自身のバリアントのサイズを取ります。これにより、メモリ使用量が4分の1、速度が2倍になります(それを実行するベンチマークにおいて、Rustの
enumと比較して)。 - ポインタより細いリンク。 相対参照は、型チェックされたリンクを1、2、または4バイトのオフセットとして格納します。それらから構築された構造体は位置独立であるため、データ構造はすでにファイル形式となっています。保存は書き込み、読み込みは検証パスによる拒否を伴う読み込みです。
- すべてがその場で構築され、保証される。 値は、呼び出しの深さに関係なく、最終的な場所に構築されます。
items.push(parse(line))は、解析されたレコードを配列に直接書き込み、可変長配列を値として返すのにコストはかかりません。 - 複数の呼び出しをまたいだ戻り値。
return err from loadは、アンワインダーやResult型、すべての呼び出しでの?なしで、静的にチェックされた任意の数のフレーム上まで関数から戻ります。 - 何も共有しないスレッド。 ワーカーは、独自のメモリを持つ別のプログラムとしてコンパイルされます。フラットな値は、
memcpyとして型付きキューを横断します。データ競合、ロック、アトミック、メモリ順序は言語には存在しません。 - オーバーヘッドなしのジェネリクスと高階関数。 型指定のないパラメータはジェネリックです。関数値はコンパイル時のエンティティであるため、
xs.filter() { it > 0 }は見た目通りのループにコンパイルされ、結果を直接宛先に構築します。 - プレーンCの入出力。 Gooseは単一のCファイルにコンパイルされるため、Cコンパイラが存在する場所ならどこでも実行でき、Cと直接
extern fn経由で呼び出せます。バンドルされたTinyCCバックエンドは、ビルドステップなしでプログラムをインプロセスでコンパイル・実行できます。
チュートリアルは例を挙げてこれらすべてを解説し、リファレンスはトピックごとに言語全体をカバーし、仕様には正確なルールがあり、ベンチマークには損失を含めた数値があります。
特徴
このセクションでは、Gooseを際立たせる点に焦点を当てます。チュートリアルでこれらの機能をより詳しく説明し、サンプルでは31の完全なプログラムでそれらを示しています。
1つのメモリモデル:スタック、そしてスコープ終了が解放
プログラムは、ネイティブコールスタック、静的データ、およびコンパイラがNを決定するN個のデータスタックを持ちます。データスタックは大きなアドレス空間予約で、バンプポインタがあり、それ以外のメモリはありません。各スタックには、最大で1つのリサイズ可能な値しか存在せず、常にトップにあるため、成長は何も移動させず、容量をチェックすることもありません。これらすべてはコンパイル時に証明され、ランタイムはバンプポインタのみを追跡します。
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は新しい要素への参照を返すため、プログラムは配列を構築しながら要素をリンクできます。vector<T>やVec<T>は再割り当てを行う可能性があり、この保証を提供できません。安定した参照は、いくつかのベンチマークの向上に寄与しています。
安全な参照、注釈なし
すべての参照とスライスには静的なルートがあります。これは、ターゲットのライフタイムを制限する変数です。参照は、その所有者をバインドする変数を超えて存続したり、互換性のない型を介してそのターゲットにアクセスしたりすることはできません。コンパイラはルートを推論し、それらのために関数を特化するため、注釈は不要です。エイリアシングや排他性のルールはありません。ライフタイムチェックが失敗した場合、エラーは参照のルートとその宛先を名前で示します。
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)
スライスT[:]により、関数は要素をコピーせずに任意の配列種別からの範囲を受け入れることができます。たとえば、splitは入力のスライスを返し、u8[:]でキー付けされた辞書はコピーではなく文字列のビューを格納します。
ネストされたデータはインラインのまま
単一の配列型はありません。サイズがどのように扱われるかによってのみ異なるファミリーがあり、各メンバーは[metadata][elements...]であり、インラインでパックされており、要素ブロックへのポインタではありません。
| スペル | 内容 |
|---|---|
T[k] | コンパイル時に既知の固定サイズ |
T[], T[varint] | 構築時にサイズが決まり、その後固定。固定または可変幅の長さ。 |
T[..k], T[..] | インラインの容量。その中で成長・縮小する。 |
T[>..] | 成長専用:ワークホース。これへの参照は有効なまま。 |
T[>..<] | 成長・縮小:スタック、キュー、ヒープ。 |
文字列は単なるu8配列です。u8[>..]はビルダー、u8[]はそれを保持するものにインラインで格納される完成した文字列、u8[..16]は構造体内の小さな文字列、u8[:]はビューです。構造体は可変サイズのパーツを含むことができ、それらは宣言順にインラインに配置されるため、レコードはポインタを含まないバイトの連続です。
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バイトで割り当てなしです。C++のstd::string + std::vector<Item>では160バイトと1回の割り当て、RustのString + Vec<Item>では153バイトと4回の割り当てです。注文全体のブックは、キャッシュをストリームする単一の配列になります。欠点は、可変サイズ要素の配列はシーケンシャルであることです。反復処理はできますが、インデックス付けはできません(参照の別の配列をインデックスとして保持しない限り)。
2つのサイズのエナム
代数的データ型(ADT)は、唯一の動的ポリモーフィズムです。継承やvtableはありません。各ADTは2つの方法で格納でき、使用時に選択します。固定モード(Shape)はタグと最大のペイロード用のスペースで、インデックス可能で上書き可能です。可変モード(Shape..)は、各値にそのバリアントのサイズのみを正確に割り当てます。配列はシーケンシャルになり、値はバリアントを変更しません。その交換として、ペイロード内の参照を取ることができます。
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }
var packed: Shape..[>..] = []; // 9, 17, 1バイト、それぞれ17バイトではない
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
}
ケース関数は、バリアントでディスパッチする別の方法を提供します。バリアントごとに1つのオーバーロードを定義します。呼び出しはジャンプテーブルを介してタグでディスパッチされ、コンパイラはオーバーロードがすべてのバリアントを網羅していることをチェックします。
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)); }
ポインタより細いリンク
T&はマシンアドレスです。8バイトで、決してダングリングしません。相対参照は、細いオフセットとして格納される同じリンクです。T&<u32>は、同じ配列内のターゲットへのフィールド自体からのオフセットで測定されるため、構造体は位置独立になります。T&<u32 in pool>は、名前付きプールのベースからのオフセットであり、他の配列がプールにリンクできます。T&<u32>?は、オフセット0をnullとして使用します。ここに、12バイトのノードを持つ、単一の成長専用配列内の二分探索木を示します。
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;
}
}
参照は透明(*や->なし)で、.=は1つをバインドまたはリターゲットし、Node?はnull許容参照で、if、guard、assertによって絞り込まれます。挿入は1回のプッシュと1回のストアで、プッシュはcurを無効にしません。ライフタイムがネストされていない場合、reusableプールは成長専用配列と非表示のフリーリストをペアにします。alloc_refはスロットを再利用するかプッシュし、freeはそれを返しますが、実際には何も解放されないため、古いスロットはメモリを破損するのではなく、正しい型の別の値を読み取ります。
あなたのデータはすでにファイル形式
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
ツリーのリンクは自己相対的なため、どこに配置されても同じ意味を持ちます。保存はバイトの書き込み、読み込みはそれらを再度読み込むことで、シリアライザ、スキーマ、ポインタの修正なしで行えます。from_bytesは、バイトが値になる前に、フレーム、すべてのタグ、すべての長さ、およびすべてのリンクをチェックするため、破損または悪意のあるファイルはfalseになり、ワイルドな参照になることはありません。
常にその場で構築
コピーフリー構築保証(仕様 §4.3)は、構築された値が常に最終的な場所に構築され、呼び出しを通じてトップダウンで伝播されることを示しています。値として成長専用配列を返す関数は、要素を呼び出し元の変数に直接書き込むか、別の配列内で構築されているレコードのフィールドに書き込むため、アウトパラメータはほとんど現れません。
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は、fの最も内側のアクティブな呼び出しの結果としてEを返します。これはフレームをいくつまたいでも可能で、中間のすべての関数は通常のシグネチャを維持します。これは静的にチェックされるため、parse_userのすべての呼び出しはloadの呼び出し内にある必要があり、実行時に捕捉されないものはありません。これはアンワインダーではなく、フレームごとにチェックされる隠された判別子であり、メッセージはloadの呼び出し元が望む場所に直接構築されます。サンプル内のパーサーは、すべての構文エラーにこれを使用します。
ジェネリクスと関数ブロック
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 };
すべてがモノモーフィゼーションされます。型引数は引数の型から推論されます。それらの型にないパラメータは明示的に指定できます。関数値はジェネリックパラメータとして渡されます。すべての呼び出しは直接的でインライン化可能であり、エスケープできず、クロージャオブジェクトや関数ポインタがないため、高階関数は見た目通りのループにコンパイルされます。ブロックは、レキシカルに囲む関数からreturnでき、ネストされた関数は囲む関数のローカル変数を見ることができます。
何も共有しないスレッド
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)); }
共有ミュータブルメモリはありません。thread_fnとそのすべての呼び出しは、独自のデータスタックとグローバルの独自のコピーを持つ、独自のプログラムとしてコンパイルされます。値は、型ごとに1つずつ、型付きキューを介して渡され、フラットである必要があります。つまり、どの深さにも参照を含まない必要があります。これは安価です。なぜなら、フラットなGoose値は連続しているからです。ジョブまたは結果は、ピクセルを含め、実際のデータを運び、横断はmemcpyです。
thread_spawn_containedで開始されたワーカーでのチェック失敗またはabortは、そのワーカーのみを終了させます。そのメッセージは標準エラー出力ではなく、待機している人に送られるため、スーパーバイザーはそれをログに記録して別のワーカーを開始できます。
var why: u8[>..] = [];
while !thread_wait(thread_spawn_contained(serve, attempt), why) {
log(why); // "assert failed (serve.goose:12)"
why.clear();
attempt++;
}
最初のthread_wait(id, msg)はワーカーのためにメッセージを受け取ります。それを要求しない呼び出し元はそれを失います。
プレーンCの入出力
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
extern fnは、GooseシグネチャをCシンボルにバインドします。mathおよびosモジュールはこのインターフェースを使用します。サポートされているC表現を持つ型のみが境界を越えることができます。サポートされていないシグネチャは、宣言時に拒否されます。コンパイラはプログラム全体に対して単一のCファイルを生成します。バンドルされたTinyCCバックエンドは、そのCをGooseコンパイラプロセス内でコンパイルして実行できます。
スピードの源泉
速度の多くはメモリモデルに由来します。ヒープ割り当てやオブジェクトごとのティアダウンなし、連続したデータ、細いリンク、そして含まれるバリアントに合わせてサイズ設定されたエナムです。すべてのインデックスは境界チェックされ、コンパイラは証明可能な不要なチェックを削除します。スライス長に対するassertは、カーネルが残りのチェックを削除してベクトル化するのに役立つことがよくあります。--bce-linesは、残っているチェックを報告します。checkfree fnは、境界チェックが残っている場合、カーネルを拒否します。インデックス型により、グラフエッジは構築中に実行された検証を保持できます。ベンチマーク測定は、ティアダウンを含むプロセス全体の壁時計時間を使用します。
| 16ベンチマークの幾何平均 | 対慣用C++ | 対手作業最適化C++ | 対最良安全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はテストスイートが何をテストしているかを説明しています。
あなたが失うもの
- データがどこに配置されるかを考える必要があります。これは誰が所有しており、そのスコープはどれくらい持続するかです。通常、答えは「それを構築する関数」であり、それは無料です。そうでない場合は、
reusableプールです。 - 再帰関数は、再帰呼び出しをまたいで可変長データを保持できません。スクラッチは呼び出しの前に終了し、それを超えて存続するものはプールとして渡されます。
- 可変サイズ要素の配列は反復処理できますが、インデックス付けはできません(それらを横に保持する参照のインデックスがない限り)。固定モードのエナムはポインタを挿入できず、可変モードのエナムは上書きできません。コンテナごとに選択します。
- エスケープするクロージャなし、関数ポインタなし、ADTタグを超える動的ディスパッチなし、そして全体プログラムコンパイルのみ。
reusableプールに返され、まだ名前が付いているスロットは、次の所有者がそこに置いたものを読み取ります。メモリ破損ではなく、論理バグです。
チュートリアル §18は完全なリストです。
リリースをインストール
Windows (x64)、Linux (x64)、macOS (Apple siliconおよびIntel) 用のプリビルドコンパイラがリリースページにあります。グラフィックス、オーディオ、UI、物理パッケージがコンパイルされています。いずれかの場所に解凍し、そのディレクトリをPATHに追加してください。コンパイラは標準ライブラリ、TinyCCのランタイム、およびそれらに隣接するパッケージを見つけ、構築したものを~/.goose(GOOSE_HOME)の下に保持します。アーカイブには、これらのドキュメント、サンプル、およびVS Code拡張機能も含まれています。
インプロセスでプログラムを実行する(goose file.goose)には、Windowsでは他に何も必要ありません。LinuxではCライブラリのヘッダー(libc6-devまたはglibc-devel)が必要です。macOSでは、XcodeコマンドラインツールのSDK(xcode-select --install)が必要です。goose buildにはCコンパイラも必要です。WindowsではVisual Studio 2022以降、またはclang。それ以外ではgccまたはclang。Linuxコンパイラにはglibc 2.35以降(Ubuntu 22.04、Debian 12)、macOSコンパイラにはmacOS 13.3以降が必要です。macOSは、解凍されたディレクトリに対してxattr -dr com.apple.quarantineを実行するまで、ダウンロードされたアーカイブのファイルを保持します。
ビルドと実行
CMake 3.27以降、C++20コンパイラ(MSVC、clang、またはgcc)、およびPython 3がテストおよびサンプルランナーに必要です。インプロセスバックエンドが構築されているTinyCCは、ツリーの一部です。グラフィックス、オーディオ、UI、物理モジュールは、独自のリポジトリ(goose_gfxおよびgoose_physics;docs/design/packages.md)のパッケージです。-DGOOSE_BUILTIN_LIBS=allは、コンパイル時にピン留めされたタグ(git)でそれらを取得し、コンパイラにコンパイルするため、それらのプログラムは他に何もインストールせずに実行できます。これがない場合、コンパイラは単独でビルドされ、いずれかのモジュールをインポートするプログラムは、最初の起動時に~/.gooseにビルドされ、git、cmake、およびCコンパイラが必要です。
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(オーディオとUIも含む))の名前を指定することもできます。Linuxでは、SDLはX11またはWayland開発パッケージ(third_party/SDL/docs/README-linux.md)を必要とします。それらがない場合、ビルドはgfxモジュールを除外し、インストールするものを通知します。
ランナースクリプトはbuild/Releaseを優先し、次にbuild/RelWithDebInfo、次にbuild、次にbuild/Debugを優先します。--exeを渡して特定のコンパイラを選択します。コンパイラアサーションが有効なテストには、明示的なデバッグ実行可能ファイルを使用します。コンパイラはbuild/goose(Visual Studioジェネレータを使用する場合はbuild/Release/goose.exe)であり、ビルドされたソースツリー内の標準ライブラリを見つけます。Windows 10バージョン1903以降では、組み込みマニフェストはUTF-8プロセスコードページを選択するため、コンパイラパスはシステムロケールに依存しません。それ以前のWindowsバージョンは、レガシーコードページを維持します。生成されたネイティブ実行可能ファイルは、独自のプロセス設定を維持します。
ソースから直接プログラムを実行する(goose runはこれを明示的に示し、goose checkは型チェックのみを行います):
build/goose samples/01_tour.goose
または実行可能ファイルをビルドします。これはソースの隣に配置されます(-oで別の場所に配置)。コンパイラはマシン上のCコンパイラを見つけ(goose toolchainがどれかを示します:WindowsではMSVCまたはclang、それ以外ではcc、gcc、またはclang;--ccで選択)、生成されたCをランタイムとプログラムが使用するネイティブモジュール(グラフィックスを含む)とリンクします。スレッドを使用するプログラムにはこのルートが必要です。TinyCCはスレッドローカルストレージを配置できないためです。
build/goose build samples/01_tour.goose && samples/01_tour
build/goose build samples/27_gfx_cube.goose && samples/27_gfx_cube
--debugは生成されたCのオーバーフロー、範囲、タグチェックをオンにします。各アボートはソース行(およびオーバーフローまたはキャストは関連する値を名前で示します)を名前で示し、デバッグ情報付きでCをビルドします。--dry-runは、ビルドシステム用のビルドが実行するコマンドを表示します。-vは実行中にコマンドを表示します。中間ファイルは~/.goose(GOOSE_HOME)の下に置かれ、ランタイムオブジェクトはCコンパイラごとに1回コンパイルされます。両方の形式の便利なフラグ:-O0/-O1/-O2はインライン化レベルを設定します(ビルドはデフォルトで-O2、実行は-O1)、--bce-linesは行ごとに保持される境界チェックを報告し、-DGS_DEBUG=1は--debugが定義するものです。すべてのリサイズ可能な値は、2GBの予約済みアドレス空間のデータスタック上に存在し、使用されるにつれてコミットされます。--stack-reserve 16Gはプログラムに対してそれを変更し、--stacksはプログラムとその各ワーカーが取得できるデータスタックの数とhardware_threads()に課されるキャップを報告し、-DGS_STACK_STATS=1はプログラムが終了時に実際の使用量を報告するようにします。
独自のCを使用するプログラムは、ソースの隣にあるサイドカーにそれを名前で指定します。foo.buildはfoo.gooseの隣にあります:c_sources = ["impl.c"];。各プラットフォームの部分に対して、c_include、c_defines、c_libs、および_windows、_linux、_macosバリアントがあります。両方の形式がそれをビルドします:goose buildはそれをリンクし、goose runはそれをインプロセス実行がロードするライブラリにビルドするため、extern fnはどちらの場合でもそれに到達します。ライブラリは、エントリの隣にあるサイドカーで同様のことを行います(または、独自のCMakeプロジェクトの静的ライブラリターゲットの名前を付けます。cmake_target = "x";。コンパイラはそれをcmake経由でビルドします)。プログラムは、ルートファイル("./lib")またはタグまたはコミット(GitHubの"owner/repo@v1"、"host/owner/repo@ref")にあるgitリポジトリでライブラリを取得します。これは~/.gooseにフェッチされ、Cコンパイラごとに1回ビルドされます(docs/design/packages.md)。
下位レベルでは、goose -o tour.c tour.gooseはCを書き出します。これにはプログラムと、インライン化されるランタイムの一部が含まれます。ランタイムの残りの部分(OSレイヤー、データスタック予約、スレッド、印刷)はもう1つのCファイルであり、goose --emit-runtime goose_runtime.cで書き出され、一度コンパイルされてすべてのプログラムとリンクされます(cc -O2 tour.c goose_runtime.o -o tour -lm -pthread;Windowsではcl /O2 tour.c goose_runtime.obj)。プログラムが使用するコンパイラとフラグでランタイムをビルドします。独自の-Dは取らず、別のバージョンのコンパイラからのプログラムはそれとリンクできません。--standaloneは、完全なランタイムを含むCファイルを書き出し、単独でビルドします。audio、gfx、physics、またはuiを使用するプログラムは、そのモジュールのレイヤーもリンクします。これは、goose --audio-link msvc|cc、--gfx-link、--physics-link、および--ui-linkが応答ファイルで名前を指定します(cc cube.c goose_runtime.o -o cube @$(goose --gfx-link cc)、グラフィックスを描画するUIプログラムの前に)。goose build --dry-runは、対象のプログラムのこれらすべてを印刷します。
audioを使用するプログラムは、グラフィックスウィンドウを開かずに、SDL3を介してプロシージャルなモノラル/ステレオPCMを再生します。オーディオリファレンスを参照してください。
テストスイートとサンプルはWindows、macOS、Linuxで実行されます。
python test/run_tests.py
python samples/run_samples.py
編集のために、VS Code拡張機能は、構文ハイライト、コンパイラの言語サーバー(タイプ中のエラー、ホバー、定義へ移動、補完、名前変更、シンボル)、およびJITを介したワンキー実行を提供します。
code --install-extension vscode/goose-language.vsix
他のエディタは、goose lspを自分で実行できます。エディタには、Neovim、Helix、Emacsの設定があります。
ドキュメント
- ドキュメントインデックス:以下のすべてへのマップであり、ドキュメントサイトのルートです(方法)。
- チュートリアル:例による親しみやすい入門。まずこれを読んでください。
- 言語リファレンス:各トピックごとにすべての機能、コンパイル可能な例付き。言語全体を読んで理解できるように書かれています。
- 言語仕様:正確なルール。なぜコンパイルされなかったのかを知りたい場合。
- サンプル:31の完全なプログラムを読み取り順に。言語ツアーからJSONパーサー、スレッド化されたマンデルブロ集合、2つのプールから構築されたファイルツリー、GPU上の回転するキューブ、ヒープに降り注ぐ何千ものボックス、およびウィジェットウィンドウのToDoリストまで。
- 標準ライブラリ:14モジュール。すべて
stdlib/の下にある読み取り可能なGooseコード。gfx(SDL3のGPU API上のグラフィックス、構築方法)、physics(Box3D上の剛体物理学、構築方法)、およびui(フォント、テキストレンダリングとゲームHUD)、Nuklear上のウィンドウとウィジェット(gfx経由で描画、構築方法)が含まれます。 - プログラムのビルドとパッケージの使用:
goose build、gitリポジトリまたはディレクトリからのライブラリ、プログラム独自のC、および独自のパッケージの作成方法。 - ベンチマーク:数値、完全な結果とそれらの背後にある設計付き。
- 実装ノート:コンパイラがどのように動作するか、パスごと、およびどのようにテストされるか。
状況
Gooseは新しい言語です。現在存在するものは、約3万行のC++で書かれた全プログラムコンパイラ、仕様、標準ライブラリ、サンプル、およびCIがWindows、macOS、Linuxで追加のサニタイザージョブで実行するテストスイートです。現時点では意図的にスコープ外となっているもの:リサイズ可能な値の移動、構造体あたりのリサイズ可能値は1つのみ、ラベル付きbreak、名前空間のプライバシー、およびOS/ライブラリへのより多くのアクセス。未解決の項目は、仕様の付録Bで追跡されています。
ライセンス
GooseはApache License, Version 2.0の下でライセンスされています。
著者
Wouter van Oortmerssen:言語設計、コンパイラ設計、コーディング標準。ホームページ、Twitter。
Claude Fable:コンパイラ実装、ベンチマーク、サンプルとドキュメント作成。
歴史とAIの使用
このリポジトリは、人間の言語設計から、ほぼすべてAIによって生成されました。私は数年前にGooseを設計し、実装を開始しましたが、ゲームスタートアップ(私の別のプログラミング言語であるLobsterに基づいています)を実行していたため、完成させる時間がありませんでした。それは悲しいことでした。なぜなら、Gooseが他の言語にはできないことをできると知っていたからです。そして、それは存在するべきでした。
Fableが出てくるまで、AIがこれに役立つとは予想していませんでした。十分な能力があるだろうと考えました。私はそれを、私の他の最近のコンパイラ(Lobster)のスタイルと構造を実質的にクローンするようにしました。そのため、コードを見ると、それにかなり似ています。私の初期設計では、多くのことが未指定のままでした。Fableとの多くのやり取りにより、それらすべてを決定することになり、その結果、言語はさらに良くなりました。
これは、既存の言語を再現するのではなく、新しい言語を実装するためにAIを使用する実験でもあります。
なぜC++で作業させたのか疑問に思うかもしれません。明らかに、Rust、私の独自のLobster、または… Goose自体のようなどの言語でも使用できたのに。Fableの場合、どの言語でもほぼ同等の能力があるという私の観察があり、Rustのような他の言語の強力なガードレールは、かつてほど差し迫ったものではありませんでした。コンパイラはCを出力するため、おそらくすでにC/C++コンパイラが必要なので、1つのエコシステムにとどまることは、デプロイメントと採用を簡素化するように思われます。libtccや将来の他のCライブラリも統合しました。そして、言ったように、コンパイラコードが私の既存のLobsterコンパイラを模倣するようにしたのは楽しかったです。少なくとも、それは私のもののように読むことができます。