Gooseプログラミング言語
The Goose Programming Language (aardappel.github.io)
要約
Gooseは、ヒープアロケータ、ガベージコレクタ、ライフタイム注釈なしでメモリ安全なシステムプログラミング言語です。C++やRustよりも高速でメモリ使用量が少ないとベンチマークで示されています。この言語は、コンパイラが管理するデータスタック上にすべての動的値をインラインで配置するメモリモデルを採用しており、メモリ管理を簡素化しつつ安全性を確保しています。
全文翻訳
Gooseプログラミング言語
[画像: Gooseプログラミング言語]
ヒープアロケータ、ガベージコレクタ、ライフタイム注釈なしのメモリ安全なシステム言語。このリポジトリのベンチマークでは、GooseはC++およびRustよりも高速に動作し、メモリ使用量も少なくなっています。
チュートリアル · リファレンス · 仕様 · パッケージ · サンプル · ベンチマーク · 標準ライブラリ
GooseはCおよびRustプログラマーに馴染みのある構文ですが、ヒープはありません。すべての動的値は、コンパイラによって管理されるデータスタック上にインラインで配置されます。成長はポインタを進め、スコープが終了するとメモリは解放されます。言語の型とライフタイムの規則により、このメモリモデルは安全に使用できます。
[画像: Discordチャット] [画像: Twitterフォロー]
なぜ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バイトで済みます。
- コストに見合ったサイズのエナム。 可変モードのADTは、各値に最大のペイロードサイズではなく、そのバリアント固有のサイズを使用します。これを活用したベンチマークでは、Rustの
enumと比較してメモリ使用量が4分の1、速度が2倍になります。 - ポインタよりも狭いリンク。 相対参照は、型チェックされたリンクを1、2、または4バイトのオフセットとして格納します。それらから構築された構造体は位置独立であるため、データ構造はすでにファイル形式になっています。保存は書き込み、読み込みは検証パスを伴う読み込みであり、悪意のあるバイトを拒否します。
- すべてがその場で構築され、保証される。 値は、あらゆる深さの呼び出しを通じて、最終的な場所に構築されます。
items.push(parse(line))は、解析されたレコードを配列に直接書き込み、可変長配列を値として返すのにコストはかかりません。 - 複数回の呼び出しをまたぐ戻り値。
return err from loadは、アンワインダーやResult型、すべての呼び出しでの?なしで、静的にチェックされた任意の数のフレームを上に戻って関数から戻ります。 - 何も共有しないスレッド。 ワーカーは、独自のメモリを持つ別のプログラムとしてコンパイルされ、フラットな値は型付きキューを
memcpyとして横断します。データ競合、ロック、アトミック、メモリ順序は言語に存在しません。 - オーバーヘッドなしのジェネリクスと高階関数。 型指定されていないパラメータはジェネリックです。関数値はコンパイル時のエンティティであるため、
xs.filter() { it > 0 }は見た目通りのループにコンパイルされ、結果を直接宛先に構築します。 - プレーンなCの入出力。 Gooseは単一のCファイルにコンパイルされるため、Cコンパイラが動作する場所ならどこでも実行でき、
extern fnを介して直接Cを呼び出します。バンドルされたTinyCCバックエンドは、インプロセスでプログラムをコンパイルおよび実行でき、ビルドステップは不要です。
チュートリアルではこれらの機能を例を挙げて説明し、リファレンスでは言語全体をトピックごとに網羅し、仕様には正確なルール、ベンチマークには損失を含めた数値が記載されています。
特徴
このセクションでは、Gooseの独自性に焦点を当てます。チュートリアルでこれらの機能をより詳細に説明し、サンプルでは31の完全なプログラムでそれらを示しています。
単一のメモリモデル:スタック、スコープ終了が解放
プログラムは、ネイティブコールスタック、静的データ、およびコンパイラが決定するN個のデータスタックを持ちます。データスタックは大きなアドレス空間予約とバンプポインタで構成され、それ以外のメモリはありません。各スタックには最大1つのリサイズ可能な値しか存在せず、常に最上位にあるため、成長によって何も移動せず、容量チェックも行われません。これらすべてはコンパイル時に証明され、ランタイムはバンプポインタのみを追跡します。
for round in 3 {
var scratch: u8[>..] = []; // 成長専用:成長はポインタのバンプ
for i in 100000 { scratch.push((i % 256) as! u8); }
print("round ", round, ": ", scratch.len, " bytes");
} // 解放:スタックトップへの1回のストア
何も移動しないため、参照は成長後も有効
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; // 100万回のプッシュ後も有効
pushは新しい要素への参照を返すため、プログラムは配列を構築しながら要素をリンクできます。C++のvector<T>やRustの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); // エラー:innerに由来する参照を格納しています。
} // これは宛先よりも長く存続しません(§9.2)
スライスT[:]により、関数は要素をコピーせずに任意の配列の種類からの範囲を受け入れることができます。たとえば、splitは入力のスライスを返し、u8[:]をキーとする辞書はコピーではなく文字列のビューを格納します。
ネストされたデータはインラインのまま
単一の配列型はありません。サイズがどのように扱われるかによって異なるファミリーがあり、各メンバーは[メタデータ][要素...]としてインラインでパックされ、要素ブロックへのポインタは決してありません。
| スペル | 内容 |
|---|---|
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バイトで、割り当ては0回です。C++のstd::string + std::vector<Item>では160バイトと1回の割り当て、RustのString + Vec<Item>では153バイトと4回の割り当てが必要です。注文全体のブックは、キャッシュをストリームする単一の配列になります。代償として、可変サイズの要素の配列はシーケンシャルになります。反復処理はできますが、インデックス付けはできません(参照の別の配列をインデックスとして保持しない限り)。
2つのサイズの列挙型
代数的データ型は唯一の動的ポリモーフィズムです。継承やvtableはありません。各ADTは2つの方法で格納でき、使用時に選択されます。固定モード(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; }, // 配列内のペイロードをインプレースで編集
_ => {}
}
}
ケース関数は、バリアントでディスパッチする別の方法を提供します。各バリアントに対して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バイト
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;
}
}
参照は透明(*や->なし)で、.=は1つを束縛または再ターゲットし、Node?はif、guard、assertで絞り込まれるnull許容参照です。挿入は1回のプッシュと1回のストアで完了し、プッシュはcurを無効にしません。ライフタイムがネストされていない場合、reusableプールは成長専用配列と非表示のフリーリストをペアにします。alloc_refはスロットを再利用するかプッシュし、freeはスロットを返しますが、実際には何も解放されないため、古いスロットはメモリを破損するのではなく、正しい型の別の値を読み取ります。
あなたのデータはすでにファイル形式です
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); // ...そしてそれはfalseと空の配列になります
ツリーのリンクは自己相対的なため、どこに配置されても同じ意味を持ちます。保存はバイトの書き込み、読み込みはそれらを再度読み込むことであり、シリアライザ、スキーマ、ポインタの修正はありません。from_bytesは、フレーム、すべてのタグ、すべての長さ、およびすべてのリンクをバイトが値になる前にチェックするため、破損または悪意のあるファイルはワイルドな参照ではなくfalseになります。
常にその場で構築
コピーフリー構築保証(仕様 §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") });
複数呼び出しをまたぐエラーの返却
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 { // 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 };
}
return E from fは、fの最も内側のアクティブな呼び出しの結果としてEを返します。これは何フレーム上であっても同様で、間のすべての関数は通常のシグネチャを維持します。これは静的にチェックされるため、parse_userのすべての呼び出しはload呼び出し内にある必要があり、実行時に未キャッチのものはありません。これはアンワインダーではなく、フレームごとにチェックされる隠された判別子であり、メッセージはloadの呼び出し元が望む場所に直接構築されます。サンプル内のパーサーは、すべての構文エラーにこれを使用します。
ジェネリクスと関数ブロック
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; } }; // first_gapから返され、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() { // 独自のメモリを持つ別のプログラム
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_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; // libmから直接
extern fn crc32_bytes(s: const u8[:]) -> u32; // スライスは{data, len}として渡される
extern fn stats_of(xs: i32[:], out: Stats&); // ポインタを介して埋められる構造体
extern fnはGooseシグネチャをCシンボルにバインドします。mathおよびosモジュールはこのインターフェースを使用します。C表現をサポートする型のみが境界を越えることができ、サポートされていないシグネチャは宣言時に拒否されます。コンパイラはプログラム全体に対して1つの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は、それらをピン留めされたタグ(設定時)で取得し、コンパイラにコンパイルするため、それらのプログラムは他に何もインストールせずに実行できます。これがない場合、コンパイラは単独でビルドされ、いずれかのモジュールをインポートするプログラムは、最初の実行時に~/.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、またはオーディオとUIを含むgfx)の名前を指定することもできます。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.gooseの隣のfoo.build: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")またはgitリポジトリ(タグまたはコミット)(GitHubの"owner/repo@v1"、"host/owner/repo@ref")でライブラリを取得し、~/.gooseにフェッチしてCコンパイラごとに1回ビルドします(docs/design/packages.md)。
下では、goose -o tour.c tour.gooseがCを書き出します。これにはプログラムと、インライン化されるランタイムの一部が含まれます。ランタイムの残りの部分(OSレイヤー、データスタック予約、スレッド、印刷)は別の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プログラムは、それが描画するgfxプログラムの前にあります)。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コード。これには、SDL3のGPU API上のグラフィックス(構築方法)であるgfx、Box3D上のリジッドボディ物理学(構築方法)であるphysics、およびフォント、テキストレンダリングとゲームHUD、Nuklear上のウィンドウとウィジェット(gfx経由で描画)(構築方法)であるuiが含まれます。 - プログラムのビルドとパッケージの使用:
goose build、gitリポジトリまたはディレクトリからのライブラリ、プログラム独自のC、および独自のパッケージの作成方法。 - ベンチマーク:数値、完全な結果とそれらの背後にある設計付き。
- 実装ノート:コンパイラがパスごとにどのように機能するか、およびテスト方法。
ステータス
Gooseは新しい言語です。現在存在するものは、約3万行のC++で書かれた完全プログラムコンパイラ、仕様、標準ライブラリ、サンプル、およびCIがWindows、macOS、Linuxで追加のサニタイザージョブで実行するテストスイートです。現時点では意図的に範囲外となっているもの:リサイズ可能な値の移動、構造体あたりのリサイズ可能値の複数、ラベル付きbreak、名前空間のプライバシー、およびOS/ライブラリへのさらなるアクセス。未解決の項目は、仕様の付録Bで追跡されています。
ライセンス
GooseはApache License, Version 2.0の下でライセンスされています。
著者
Wouter van Oortmerssen:言語設計、コンパイラ設計、コーディング標準。ホームページ、Twitter。
Claude Fable:コンパイラ実装、ベンチマーク、サンプル&ドキュメント作成。
歴史とAIの使用
このリポジトリは、人間の言語設計から、ほぼ完全にAIによって生成されました。私は数年前にGooseを設計し、実装を開始しましたが、ゲームスタートアップ(私の別のプログラミング言語であるLobsterに基づいています)を運営していたため、完成させる時間がありませんでした。それは残念でした。なぜなら、Gooseが他の言語にはできないことをできると知っていたからです。そして、それは存在するべきでした。
AIがこれに役立つとは予想していませんでしたが、Fableが登場しました。十分な能力があれば良い仕事ができるだろうと思いました。私はそれを私の他の最近のコンパイラ(Lobster)のスタイルと構造を実質的にクローンするようにしました。そのため、コードを見ると、それに非常に似ています。私の初期の設計では、多くのことが未指定のままでしたが、Fableとの多くのやり取りにより、それらすべてを決定することができ、その結果、言語はさらに良くなりました。
また、既存の言語を再現するのではなく、AIを使用して新しい言語を実装する実験でもあります。
なぜRust、私のLobster、あるいはGoose自体のような任意の言語ではなくC++で作業させたのか疑問に思うかもしれません(それはまだ起こるかもしれません)。Fableを使用した場合、どの言語でもほぼ同等の能力があり、Rustなどの言語の強力なガードレールは、かつてほど差し迫ったものではないと観察しました。コンパイラはCを出力するため、おそらくすでにC/C++コンパイラを用意する必要があるため、1つのエコシステム内に留まることは、デプロイメントと採用を簡素化するように思われます。また、libtccや将来の他のCライブラリも統合しました。そして、前述のように、コンパイラコードが私の既存のLobsterコンパイラを模倣するようにしたのは、少なくとも私自身のように読めるので、楽しいと思いました。