goose

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

92posts · 30d
Nov 33 posts
  1. Reddit · · r/BestGitHubRepos · u/company_url_finder

    Goose: A memory-safe systems language with no heap allocator, garbage collector, or lifetime annotations (2.6k stars) What it does All text below is quoted directly from the project 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. Why Goose: - Faster than C++ and than safe Rust, while memory safe. Over sixteen benchmarks Goose runs at 3.3x the speed of idiomatic C++, 1.16x hand-optimized C++ and 1.12x the best safe Rust, and uses 1.9x, 1.3x and 1.2x less memory. - No allocator, no GC, no reference counting, no destructors. Memory is a handful of data stacks that the compiler assigns statically. - Growth preserves references. A reference into a growing array stays valid for as long as the array does. - Memory safe with zero annotations. There is no lifetime syntax, there are no aliasing or exclusivity rules, and there is no unsafe. - Nested data stays inline. A string, an array of strings, a record with variable-size fields and an array of those records are each stored as one contiguous block that contains no pointers. - Variable sized Enums. Variable-mode ADTs store each value at the size of its own variant rather than that of the largest variant. - Everything is built in place, guaranteed. A value is constructed at its final destination through any depth of calls. >Goose has syntax familiar to C and Rust programmers, but there is no heap. Repository aardappel/goose Repository stats |Metric|Value| |:-|:-| |Stars|2,586| |Forks|105| |Last release|v2026.3 (2026-10-21)| |Languages|C++ 73.1%, Python 14.8%, C 6%| |Contributors|25| |AI contributor accounts (Claude / Codex / Cursor / Copilot)|@claude (30 commits), @Copilot (1 commits)| |AI co-authored commits|Claude: 30, Copilot: 1| Main contributor |Field|Value| |:-|:-| |Name|Wouter van Oortmerssen| |GitHub|@aardappel| |Commits to this repo|983 (top contributor)| |Company / Location|VoxRay Games / San Francisco| |X / Twitter|@wvo| |Followers|1.2k| ────────────────────────────── 📌 If you want to browse more repos like this, we've organized them by category and rankings here: bestgithubrepositories.com 🗂️ We're also archiving repos where possible, so useful projects don't disappear when they're removed from GitHub.

    23 points · 1 comment
  2. Zenn · · @akari1106

    Goose言語はなぜシリアライザを必要としないのか? ! この記事は実測記録です。2026年10月27日に発表記事で知った言語を、翌日に Windows で動かした結果と、そこから考えたことを書いています。主役は、シリアライザを書かずにセーブデータを保存して読み戻す実験です。速度の数値も載せますが、脇役です。 あとなんか知りませんが、Zennの絵文字でGoose選んだら猫の絵文字になる仕様?バグ?を見つけました。なんでだ…。 はじめに 2026年10月27日に、GIGAZINE で Goose というプログラミング言語の記事を見ました。見出しは「メモリ安全で、C++ より速く、メモリも少ない」で、そこまでなら最近よく見る発表です。目が止まったのは、作者の名前と README の一文でした。 作者は Wouter van Oortmerssen さんです。FlatBuffers の作者で、今は自分のゲームスタジオを経営しています。README の「History & Use of AI」には、数年前に Goose を設計して実装を始めたが、ゲーム会社の経営で時間がなくなり、Claude Fable が出て実装できるようになった、とあります。設計は人間、コンパイラの実装とドキュメントは Fable、という分担です。とても現代的ですよね。 FlatBuffers は、バイト列をパース(デコード)せずにそのまま読むためのシリアライズ形式です。JSON のように読むたびにオブジェクトを作り直すのではなく、バイト列の中の相対オフセットをたどって値を直接読みます。 FlatBuffers とは 2014年に Google が公開したシリアライズ形式で、設計したのが Wouter van Oortmerssen さんです。もともとはゲーム開発のために作られました。 解きたかった問題は、ゲームのアセット(マップ、モデル、設定)の読み込みです。JSON や Protocol Buffers は、ファイルを読んだあとに全体を解析して、メモリ上にオブジェクトを組み立て直します。アセットが大きいと、この組み立てに時間とメモリを使います。 FlatBuffers はこれをやめました。ファイルの中身は、スキーマ(.fbs ファイル)から生成したコードが作る1つのバイト列で、値はすべて決まった位置か、その位置からの相対オフセットで見つかります。読む側は、バイト列をメモリに置いたまま、必要な値だけを直接読みます。コピーもアロケーションもありません。ファイルから読んでも、ネットワークから受け取っても、メモリにマップしても、同じバイト列をそのまま使えます。 外から来たバイト列は信用できないので、verifier が「オフセットがすべてバッファの範囲内を指しているか」を検査します。値の中身が正しいかは検査しません。 Google 以外では、Facebook が 2015 年に Android アプリのデータ読み込みに採用した例が知られています。 flatbuffers.dev この形式を作った人が言語を作ったら、言語の中のデータはどうなるのか。気になって触ってみたら、Goose では 「プログラムが計算に使っているメモリ上のバイト列」と「ファイルに書くバイト列」が同じもの でした。シリアライザが要らないのは、そのためです。セーブデータを書いて読む実験で、それを確かめました。 比べる相手として、同じプログラムを Rust と C++ でも書きました。Goose の構文がどの言語から来ているか、シリアライザを書かずに済むとは何が減ることなのか、を並べて見るためです。 先に、この記事の範囲を書いておきます。触ったのは1日で、バージョンは v2026.3 です。 Goose とは 公式ページの説明を、短くまとめておきます(2026年10月28日時点)。 aardappel.github.io Goose はシステムプログラミング向けの言語です。メモリ安全性を保ちながら、ヒープアロケーター、ガベージコレクター、参照カウント、デストラクタ、ライフタイム注釈のどれも持ちません。動的な値はすべて、コンパイラが管理するデータスタックの上に inline で置かれ、スコープを抜けた時点で解放されます。コンパイラは C++ で書かれていて、出力は1つの C ファイルです。ライセンスは Apache-2.0 で、リポジトリは下記です。 aardappel/goose 公式ページの数値は、自分たちのベンチマーク16本をまとめた値(幾何平均)です。速度は、普通に書いた C++ の 3.51 倍、手で最適化した C++ の 1.24 倍、unsafe を使わない Rust の 1.13 倍。ピークメモリは、それぞれ 1.95 倍、1.32 倍、1.25 倍少ないとしています。負けるケースも同じページに載っています。なお、GIGAZINE の記事にある 3.3 倍と 1.12 倍は、公式ページの現在の値とは違います。この記事では公式ページの値を使います。 作者の経歴をまとめておきます。1990年代に Amiga E という言語を作り、Cube と Sauerbraten というオープンソースの FPS エンジンを作り、Google で FlatBuffers を作り、自分のゲーム用に Lobster という言語を作りました。README によると、今のスタジオ(VoxRay Games)は Lobster の上に成り立っています。ずっと ゲームを速く動かすためのデータの置き方を追い続けている人 です。Goose の設計は数年前に済んでいて、AI 向けに設計したものではないそうです。 この記事で実験することは3つです。1つ目は、「ライフタイムの注釈を書かなくても、メモリ安全性が保たれる」という公式の説明を、他の言語ならメモリを壊すコードを書いて、コンパイルエラーになるかで試します。2つ目は、タイトルの問いです。ゲームのセーブデータを、シリアライザを書かずにファイルへ書いて読み戻し、壊したファイルも読ませます。3つ目は、「普通に書いた C++ の 3.51 倍速い」という公式の数値について、同じベンチマークは手元にないので、自分で書いたプログラムで Rust と C++ と比べます。2つ目と3つ目の間に、その3言語のプログラムを並べて構文を比べる節を挟みます。 環境と手順 実行環境は次のとおりです。実行日は 2026年10月28日です。 項目 値 OS Windows 11 Goose v2026.3(2026-10-21 リリース)の Windows x64 ビルド済みバイナリ。goose version は goose runtime 4d3da949e563 C コンパイラ MSVC 19.44(Visual Studio 2022 Build Tools) Rust rustc 1.99.1 CPU / メモリ AMD Ryzen 7 5700X / 32 GB GitHub Releases の zip を展開するだけで、goose file.goose でプログラムをその場で実行できました。同梱の TinyCC が、生成した C をプロセスの中でコンパイルして実行します。Windows では追加のツールは要りません。実行ファイルを作る goose build には C コンパイラが要ります。goose toolchain というコマンドが、PATH に cl がない状態でも Visual Studio Build Tools の MSVC を見つけてくれました。 goose samples/01_tour.goose goose build samples/01_tour.goose 速度の計測は、goose build(-O2)、cargo build --release、cl /O2 で作った実行ファイルを、Python からプロセスごと起動して壁時計時間とピークワーキングセットを取りました。各入力を10回(大きい入力は5回)実行し、表には最良値を載せています。起動時間を含むので、小さい入力の数値は比較に使えません。 同梱の samples/ には31本のプログラムがあり、docs/ にはチュートリアル、仕様書、リファレンス、設計メモが入っています。この記事で「設計文書」と書いているのは、docs/design/ の中のファイルです。 この記事で書いたコードと実行結果は、次のリポジトリに置いておきます。 akari-iku/goose-playground まず、わざとエラーを出してみる 1つ目の実験です。「ライフタイムの注釈を書かなくても、メモリ安全性が保たれる」という説明を確かめます。どこでコンパイラが止めてくれるのかを見るために、言語の説明を読む前に、コンパイルエラーになるはずのコードをわざと書いてみました。最初は、ローカルの配列への参照を返す関数です。 fn make() -> i64& { var xs: i64[>..] = []; xs.push(1); return xs[0]; } i64[>..] は、要素が移動しないことを保証する配列の型です。末尾に追加しても既存の要素の場所が変わらないので、参照が無効になりません(配列の種類は、セーブデータの節の前に整理します)。コンパイルすると、次のエラーが出ました。 01_dangling_return.goose:8: error: returning a reference rooted in xs, which dies with this function (§9.2) return xs[0]; 行番号は実験ファイルのもので、掲載したコードは抜粋です。エラーメッセージが、参照の root(寿命を決める変数、ここでは xs)を名指しして、仕様書の節番号を添えています。docs/goose_spec.md の §9.2 には、参照には必ず root があり、参照を返したり保存したりできるのは、root が保存先より長く生きるときだけ、という規則が書いてあります。xs は関数と一緒に消えるので、返せません。 もう1つ試しました。配列の一部を指す参照(スライス)w をあとで使うのに、その前に元の配列を空にする例です。 var line: u8[>..] = []; line.append("hello world"); let w = line[..5]; line.clear(); print(w); 02_clear_while_sliced.goose:8: error: cannot clear line while w is still used: it may hold a reference or slice into it (§5.1) w の最後の使用が clear() より後にあることまで見ています。print(w) を clear() の前に動かすと通ります。 逆に、通ってほしいものも確かめました。配列の要素への参照を取ったあと、配列を伸ばし続けても、その参照がまだ使えるか、です。参照を取ってから10万回 push して、最初の参照に書き込んでみました。Item は id と weight を持つ構造体です。Goose の = はコピーで、参照を持つには .= を使います。Rust なら、first を持ったまま items.push するとコンパイルエラーになる書き方です。 var items: Item[>..] = []; let first .= items.push(Item { 1, 0.5 }); for i in 2..100001 { items.push(Item { i as i32, i * 0.5 }); } first.weight = 99.5; print(items.len, " items, first weight ", items[0].weight); 100000 items, first weight 99.5 [>..] の配列は要素を移動しないので、push が返した参照はそのまま有効 です。移動しないのは、伸びる配列が1本ずつ専用のデータスタック(あらかじめ予約したアドレス範囲)に置かれ、末尾に積むだけで伸びるからです。std::vector や Vec は、容量が足りなくなると要素を別の領域にまとめてコピーするので、古い領域を指していた参照は使えなくなります。Goose の [>..] ではそれが起きません。 公式ページが制限事項として認めている穴も再現しました。reusable と宣言したプールはスロットを再利用するので、解放済みのスロットへの参照を持ち続けると、そのスロットにあとから入った別の値が読めます。 reusable var pool: Item[>..] = []; let stale: Item& = pool.alloc_ref(Item { 10, 1.0 }); pool.free(pool.index_of(stale)); let next: Item& = pool.alloc_ref(Item { 12, 2.0 }); print(stale.id); // 12 型に Item& と書いた変数は、= でも参照として束縛されます。コンパイルは通り、stale.id は 12 でした。公式はこれを「型は合っているのでメモリ破壊ではなく、論理バグ」と説明しています。その説明のとおりでした。 ここで見た「要素が移動しない配列」と「参照の root」が、次の実験でセーブデータの木を相対オフセットで持てる前提になります。 セーブデータを書いて、読んで、壊す 2つ目の実験で、タイトルの問いに答えます。作者はゲーム開発者で、公式のパッケージ(gfx、audio、ui、physics)もゲーム向けなので、題材はゲームのセーブデータにしました。セーブデータは「書いて、読んで、たどる」というゲームのデータの典型で、固定サイズの値、可変サイズの値、木構造の3つがそろいます。Goose でファイルに書いて読み戻し、同じことを C++ で、手書きのシリアライザで書いて比べました。 Rust は比べていません。serde を使えばシリアライザは書かずに済みますが、読んだあとにオブジェクトを作り直す点は C++ と同じです。Rust の rkyv は相対ポインタと検証器で Goose と同じ形式をライブラリとして実現していますが、読み込んだ値は専用の Archived 型で、普通の値のまま push や書き換えはできません。この違いは、あとの「FlatBuffers と Goose の対応」で触れます。 データは3種類です。プレイヤー、持ち物、そしてクエスト(ゲーム内の課題)です。クエストは、大きな目標の下に小さな手順がぶら下がる親子関係を持つので、木構造にしました。たとえば「村の池を守る」の下に「葦を集める」、さらにその下に「鎌を探す」、という形です。Goose はガチョウなので、題材は池にしました。 先に、配列の型名 セーブデータの構造体を読む前に、配列の型名を整理しておきます。Goose 独自の部分です。配列の型名は、長さの決まり方と伸び方の組み合わせで8種類あります。代表的な6種類を図にします。「この値がどう伸びて、どこに置かれるか」を型で宣言する、と考えると読めます。 このほかに、配列の一部を指す参照であるスライス T[:] があります(1つ目の実験の line[..5] がそれです)。Rust では参照どうしの寿命の関係を注釈で書きます。Goose では、値の置き場所と伸び方を配列の種類として型に書き、寿命はコンパイラが root から推論します。 3種類のデータ struct Vec2 { x: f32, y: f32 } struct Player { name: u8[..16], hp: i32, gold: i32, pos: Vec2 } struct Item { name: u8[varint], qty: varint, cents: varint } struct Quest { id: u32, title: u8[..24], done: bool = false, first_child: Quest&<u32>?, next: Quest&<u32>?, } Player は固定サイズです。名前は容量16の inline 配列なので、Player は1つの平らな値で、Player の配列は添字で引けます。 Item は可変サイズです。名前は必要なバイト数だけ持ち、数値は LEB128(小さい値ほど短くなる整数の表現)で持ちます。 Quest は木です。first_child と next の型 Quest&<u32>? は「同じ配列の中の別の要素への、4バイトの相対オフセット」です。格納される値は、参照先のアドレスからこのフィールド自身のアドレスを引いた差です。ポインタ(メモリ上の絶対アドレス)は1つもありません。ポインタをファイルに書くと、次に実行したときには別の場所を指してしまいますが、相対オフセットは配列ごと別の場所に置いても同じ要素を指します。 保存は、3つの配列を順にバイト列に追記するだけです。 var file: u8[>..] = []; file.append("GSAV"); players.to_bytes(file); bag.to_bytes(file); quests.to_bytes(file); write_file(path, file); to_bytes は、配列がメモリ上で占めているバイト列を、長さの前置きつきで追記します。シリアライザを書く必要がありません。理由は3つです。メモリ上の配列にポインタが1つもないこと、木のリンクが相対オフセットであること、そのリンクを張れるのが要素の移動しない [>..] の配列の中だけであること(1つ目の実験で見た性質)です。この3つがそろっているので、バイト列をそのまま別の場所に置いても意味が変わりません。これがタイトルの問いへの答えです。 読み戻しは from_bytes です。image は、ファイルから切り出した Quest セクションのバイト列です。ファイルから来たバイト列は、コンパイラが組み立てた値のようには信用できないので、読む前に検証します。 var lq, ok = from_bytes<Quest[>..]>(image); 検証に通ると、普通の配列が返ってきます。ファイルから読んだデータと、プログラムが使うデータの区別がありません。読み戻した lq に新しいクエストを push することも、相対オフセットで子をたどることもできました。C++ でこれをやるには、ファイルを読んだあとに std::vector<Quest> を組み立て直す処理が要ります。Goose では、その処理がありません。 サイズ C++ 版は、std::string と std::vector を使う一般的な書き方にしました。C と同じ平らな構造体(char title[24] と添字)で書けば C++ でもファイルにそのまま書けますが、添字の範囲検査と「リンクが同じ配列の中を指している」ことの保証は手書きになります。この記事では、添字の範囲検査とリンク先の配列の検査を誰が書くか、という点で比べます。Goose ではコンパイラと from_bytes が行います。 Goose では、メモリ上のサイズがそのままファイル上のサイズです。C++ 版はメモリ上の値とファイルの形が別なので、表はメモリ上のサイズで比べます。 データ Goose(メモリ上=ファイル上) C++ メモリ上(MSVC) Player 1件 33 バイト 48 バイト。名前が16文字以上なら、別途ヒープに領域を確保する Item 3件 35 バイト 144 バイト Quest 1件 38 バイト 72 バイト。子の配列と長いタイトルは別途ヒープに確保(7件で5回) Goose の構造体は、フィールドを宣言順に詰めて置きます。C++ のコンパイラが入れるアライメント用の空きバイトもありません。アラインされていない読み書きは安価とみなす設計で、必要なら pad を自分で書きます(仕様書 §3.2)。Quest の 38 バイトは、id 4、title 1 + 24、done 1、リンク 4 × 2 です。 ファイル全体は Goose が 375 バイト、C++ が 369 バイトでほぼ同じです。C++ はメモリ上の値を詰め直してファイルに書くので、ファイルは 369 バイトですが、メモリ上では大きくなります。Goose はメモリ上も 375 バイトのままです。 書いた量 シリアライザは、Goose では書いていません。to_bytes の呼び出しが1行ずつあるだけです。C++ では Writer と Reader、型ごとの write と read で 78 行書きました。読み込み時の検証も、Goose では from_bytes が組み込みで行い、C++ では長さのチェックと、子の添字が範囲内かの検査を手書きしました。Goose 側で自分で書いたのは、セクションの区切りを読む8行だけです。長さの前置きを読んで、その分だけ切り出す処理です。 ファイルの中身 Goose が書いた save.gsav の先頭です。 00000000: 4753 4156 4203 6775 7300 0000 0000 0000 GSAVB.gus....... 00000010: 0000 0000 0000 4600 0000 e204 0000 0000 ......F......... 00000020: 4841 0000 40c0 076d 616c 6c61 7264 0000 HA..@..mallard.. 47 53 41 56 は自分で書いたタグ "GSAV" です。42 は LEB128 の 66 で、Player セクションの本体が 66 バイトあることを表します。03 67 75 73 は名前で、長さ 3 と "gus"、残り13バイトは未使用です。46 00 00 00 は hp = 70、e2 04 00 00 は gold = 1250、続く8バイトは pos の f32 が2つです。07 6d 61 6c 6c 61 72 64 から2人目の "mallard" が始まります。 ここに並んでいるのは、プログラムが計算に使っていたメモリ上のバイト列そのものです。設計文書(docs/design/serialization.md)の言葉では、Goose のデータは「それ自体がシリアライズ済みの形」です。保存では要素の領域をそのまま書き出し、読み込みではそのまま読み込みます。読み込んだあとにポインタを書き換える処理はありません。 壊したファイルを読ませる セーブデータは、ディスクの故障や改造ツールで壊れることがあります。from_bytes の検証が何を拒否して何を通すのか、境目を見るために、ファイルを4通りに壊して読ませました。 操作 Goose from_bytes C++ 手書き Reader リンク(相対オフセット)の1バイトを反転 拒否 拒否(添字の範囲検査を書いたので) データ(done フラグ)の1バイトを書き換え 受理。木の根のクエストの done が true になる 受理 末尾を1バイト切る 拒否 拒否 Item セクションのバイト列を Quest として読む 拒否 未実施 設計文書は、この線引きをはっきり書いています。from_bytes が検証するのは安全性であって、整合性ではありません。壊れたリンクは「配列の外のメモリを指す参照」になり得るので拒否します。数値が壊れていても、リンクがすべて配列の中を指していれば、木としてはたどれるので受理します。値の正しさは from_bytes では確かめません。確かめるにはチェックサムを使う、と設計文書は書いています。チェックサムは、データ全体から計算する検査用の値で、1バイトでも変わると値が変わります。 FlatBuffers の verifier も同じです。オフセットがバッファの範囲内を指しているかだけを確かめ、中の値が正しいかは見ません。 C++ 側で同じ安全性を得るには、Reader に長さのチェックを入れ、読み終わったあとに添字の範囲を検査する必要があります。どちらも書き忘れると、壊れたファイルは範囲外アクセスになります。 1番 Goose らしいエラー この実験で1番印象に残ったのは、動いた結果ではなく、途中で出たコンパイルエラーです。最初、クエストの木に子を足す関数を、親を参照で受け取る形で書きました。 fn add_child(pool: Quest[>..]&, parent: Quest&, ...) { let node .= pool.push(Quest { ... }); ... cur.next .= node; // cur は parent からたどった参照 } error: a relative reference must point within the same root as its location (§3.9); this reference is rooted at pool 相対オフセットは「同じ配列の中」でしか意味を持ちません。そのため、リンクを張る2つの参照が同じ配列から来ていることを、コンパイラが静的に確認します。parent を Quest& で受け取ると、その参照がどの配列に根ざしているかコンパイラには見えません。直し方は、親を添字(i64)で受け取り、関数の中で pool[parent_at] と解決することでした。 fn add_child(pool: Quest[>..]&, parent_at: i64, ...) -> i64 { let node .= pool.push(Quest { ... }); let parent .= pool[parent_at]; ... } 「ポインタではなく添字で持つ」は、ゲーム開発系の C++ ではよくやることかと思います。Goose では、それをコンパイラが要求してきます。 もう1つあります。相対参照を含む配列は、値としてコピーできません。配列全体を同じ並びのままファイルへ書くのとは違い、一部の値だけを別の場所へコピーすると、相対オフセットの指す先がずれるためです。失敗時に空の配列を返そうとして var empty: T = []; return empty, false; と書いたら、copying a value of type Quest[>..], which contains self-relative references, is not supported; construct it in place と言われました。return from_bytes<T>(""); と書くと、空のバイト列から「空の配列と false」をその場で構築して返せます。 FlatBuffers と Goose の対応 タイトルの問いへの答えは、セーブデータの節で見たとおりです。ここでは、その仕組みが FlatBuffers のどの部分にあたるかを並べます。 FlatBuffers Goose バッファ内の相対オフセット Quest&<u32> のような相対参照。格納値は「target − フィールド自身のアドレス」で、両端が一緒に動くので位置独立 ネストした文字列や配列を inline で持つ u8[..16] や u8[varint] が構造体の中に inline で入る 読み込み時に verifier を通す from_bytes が同じ線引き(安全性のみ)で検証する バッファをそのまま write 「Goose のデータはそれ自体がシリアライズ済みの形」 スキーマにフィールドを足しても古いバッファを読める 未確認。from_bytes は型のバイト配置で検証するので、構造体を変えると古いファイルは拒否されると読めるが、試していない 他の言語から同じバッファを読める 未確認。バイト配置は仕様書の付録にあるので読む側を書くことはできるが、生成コードはない 違いは2つです。FlatBuffers はスキーマ言語とコード生成を必要とし、基本は読み取り専用です。Rust の rkyv も、読み込んだ値は専用の型です。Goose では、言語の普通の値が可変のまま同じ性質を持ちます。var quests: Quest[>..] と宣言した配列に push して、参照で書き換えて、そのままファイルに書けます。 その代償が、配列の種類を型で宣言することです。要素が移動しない配列([>..])だからこそ、相対オフセットが壊れません。公式ページも、制限事項の先頭に「データがどこにあり誰が所有するかを考える必要がある」と挙げています。 data-oriented design との対応 ゲーム業界で独自エンジンを作る人たちがたどり着いた設計の考え方を、data-oriented design と呼びます。Insomniac Games の Mike Acton さんの CppCon 2014 の講演で広まり、Unity は DOTS としてエンジンに取り込み、Richard Fabian さんが本にまとめています。 youtube.com unity.com dataorienteddesign.com 要素は4つです。 オブジェクトをポインタでつながず、配列に連続で並べる ポインタの代わりに、添字やハンドルで参照する 構造体の配列ではなく、フィールドごとの配列(SoA)にする アセットはロード時に組み立て直さず、ファイルの形のままメモリに置く Goose は、このうち1、2、4を言語機能にしています。3については docs/design/soa.md に「SoA を配列の種類として足す」という未実装の提案があります。 同じプログラムを3言語で書く ここからはタイトルの問いを離れて、Goose の書き心地を見ます。構文がどこから来ているかを見るために、L-system で SVG を書き出すプログラムを Goose、Rust、C++ で書きました。L-system は、文字列の書き換え規則で図形を作る仕組みです。規則を何回か適用して長い文字列を作り、その1文字ずつを「前に進む」「曲がる」の命令として読んで線を引くと、植物や曲線になります。 関数の分け方と出力の形式をそろえたので、4つのプリセットすべてで、3言語の出力がバイト単位で一致しました。 プリセット 深さ 線分数 SVG のサイズ plant(植物) 6 6,048 182,943 バイト koch(コッホ曲線) 4 625 17,517 バイト dragon(ドラゴン曲線) 12 4,096 112,031 バイト sierpinski(シェルピンスキー) 6 2,187 68,397 バイト plant を深さ5で描いたものです(線分 1,488 本)。 4つのプリセットの SVG は、リポジトリの notes/images/ に置いてあります。 骨格は Rust、細部はいろいろな言語から 関数定義は fn rewrite(src: const u8[:], out: u8[>..]&) と書きます。Rust の fn rewrite(src: &[u8], out: &mut Vec<u8>) とほぼ同じ形で、const の位置だけ C++ です。match、if が式であること、as、:: も Rust のままです。 違うのは参照の渡し方です。Goose では、書き換えてもらう配列を渡すときに、呼び出し側は rewrite(cur, next) とそのまま書きます。Rust のように &mut next と書く必要はありません。これは C++ の参照と同じです。参照を変数に束縛するときは let first .= items.push(...) と .= を使い、= は左辺の型が参照型でなければコピー、.== は同じ対象を指しているかの判定です。この3つの区別は Goose 独自です。 ほかの言語から来ている書き方もあります。複数の戻り値 -> f64, f64 と let depth, ok = parse_int(s) は Go の多値返却、Preset? と guard ... else は Swift と Kotlin、末尾から数える添字 xs[^2..] は C# の形です。 文字列型はありません。u8(1バイトの整数)の配列なら、どの種類でも文字列として使います。伸ばすなら u8[>..]、構造体の中なら u8[..16] や u8[varint] です。"hello" のようなリテラルは読み取り専用のスライス const u8[:] で、どの種類の配列にもコピーできます。C の char* に近い感覚です。 配列の型名は、セーブデータの節で整理したとおりです。構造体の中に to: u8[..32] と書くと、容量32の配列が構造体の中に inline で入ります。C の char to[32] に近いですが、長さを持っています。 Rust と Goose の両方を書く人が踏む罠 for &x in xs の意味が、Rust と Goose で逆です。 書き方 Goose Rust for x in xs 要素のコピーで回す xs がスライスや &Vec<T> なら &T、Vec<T> そのものなら所有権を移して T for &x in xs 要素への参照で回す。x に書き込むと元の配列が変わる 参照をはがして、値で回す 記号が同じで意味が逆です。Goose の & は、引数の型 out: T& でも束縛の let r .= ... でも、一貫して「この名前は参照である」という宣言です。C++ の for (auto& x : xs) と同じ読み方をすれば合います。 書いている途中で踏んだ構文エラーは3つで、どれもエラーメッセージだけで直せました。 踏んだ構文エラー3つ match の腕に文を書くには {} が必須。'+' => t.heading += p.angle, は構文エラー pad は構造体のパディング用のキーワードで、変数名に使えない for depth { ... } は書けず、for _ in depth { ... } と書く 速度も測った 3つ目の実験です。「普通に書いた C++ の 3.51 倍速い」という公式の数値を、自分のプログラムで見ます。公式と同じベンチマークは手元にないので、L-system のプログラムで Rust と C++ と比べました。計測条件は「環境と手順」のとおりです。 最初に測ったとき、plant depth 10 で Goose は C++ の8倍速く見えました。ただし、この差はメモリ管理の差ではありません。この入力の出力は 55 MB の SVG で、処理のほとんどは f64 を小数2桁の文字列にする部分です。差がついたのは、Goose の format_flt、Rust の {:.2}、MSVC の snprintf("%.2f") の速さでした。 そこで、C++ と Rust にも、Goose の format_flt と同じ「100倍して四捨五入し、桁を1文字ずつ書く」フォーマッタを手書きして測り直しました。この丸め方は %.2f の丸め方(2進数の真の値に最も近い10進数を選ぶ)と一般には一致しませんが、この4プリセットの入力では出力が3言語でバイト単位で一致しました。C++ なら標準の std::to_chars を使う手もありますが、Goose と同じ処理に合わせるために手書きにしています。 各セルは「壁時計時間 / ピークワーキングセット」です。 入力 Goose Rust 標準 Rust 手書き C++ 標準 C++ 手書き plant depth 8 17 ms / 11 MB 39 ms / 16 MB 19 ms / 15 MB 98 ms / 14 MB 25 ms / 13 MB plant depth 10 167 ms / 113 MB 520 ms / 162 MB 205 ms / 161 MB 1459 ms / 141 MB 298 ms / 141 MB 差は 8 倍から 1.8 倍に縮みました。Rust との差は 1.2 倍です。残った差が何によるものかは、この記事では追っていません。数値を引用するなら「手書き」の列です。公式の 3.51 倍という数値そのものは、この記事では確かめられていません。確かめられたのは、「自分で書いた1本のプログラムでは、文字列化の差を除くと C++ の 1.8 倍、Rust の 1.2 倍だった」ことまでです。 誰向けの言語か Goose が諦めているものを、公式ページの制限事項から拾います。 関数の外へ持ち出せるクロージャ(escaping closure)がない。TypeScript でいう、setTimeout や addEventListener にコールバックを渡して後で呼んでもらう書き方ができない 関数ポインタがない。関数を変数に入れて持ち回れない 動的ディスパッチは、enum のタグで分岐する match だけ。インターフェイスに対して実装を差し替える書き方がない プログラム全体を一度にコンパイルする。プラグインを後から読み込めない 標準ライブラリに、ネットワークとサブプロセスがない(v1 の範囲外。必要なら extern fn で C を呼ぶ) そのほかの制限 再帰関数は、伸びる配列を再帰呼び出しをまたいで保持できない 可変サイズの要素を持つ配列は、添字で引けない(先頭から順にたどる) 正規表現は ASCII のみ WebAssembly は設計メモの段階で未実装 この制限を、ゲームとビジネス系で分けて読むと、向き不向きがはっきりします。 ビジネス系・便利系 ゲーム 時間の制約 要求ごとの応答時間。平均で良い 16.6 ms(60 fps の1フレーム)の中に全部収める メモリの制約 足せる 上限が固定 困るもの 仕様変更、外部連携、例外的な入力 GC の停止、断片化、キャッシュミス 欲しいもの 変更しやすさ、プラグイン構造 毎フレーム同じ時間で終わること、連続したデータ Goose が諦めたもののうち、関数ポインタと動的ディスパッチは、ゲームの中核ループの内側(1要素ごとに仮想呼び出しをする場所)では避けるものです。ジョブの投入やイベントのコールバックには使うので、Goose ではそこを enum のタグと match で書き換えることになります。プラグイン構造や動的ディスパッチを多用するビジネス系では、設計の前提が成り立ちません。 「向いていない」にも2種類あります。ネットワークがないのは「今の v1 で標準にない」話で、extern fn やパッケージで埋まる可能性があります。動的ディスパッチがないのは「設計上そもそも合わない」話で、今後も変わらないはずです。 向いているのは、「構造を持つデータを作って、保存して、読み戻して、たどる」が処理の中心にあるものです。ゲームの中核(シミュレーション、セーブとロード)はその典型です。ゲーム以外で、触ってみて合いそうだと感じた領域を挙げておきます。試したわけではないので、見立てです。 領域 合いそうな理由 インデックスや組み込みの KVS 木やハッシュ表を相対参照のプールで作り、そのままファイルにできる。同梱の samples/25_serialize.goose が単語インデックスを保存して読み戻す例 エミュレータのセーブステート 機械の状態を1つの構造体にすれば、to_bytes の出力がそのままセーブステート コンパイラやインタプリタ AST を相対参照の木で持てば、パース結果をバイト列のまま保存して、次回はパースせずに読める ログやテレメトリの記録と再生 追記だけの配列にレコードを積んで書き出す。途中で切れたファイルは検証で弾ける 通信のペイロード定義 FlatBuffers のもともとの用途。構造体の定義がそのままワイヤ形式になる。ソケットは extern fn で C を呼ぶ 映像の処理は、フレームが最初から平らな u8 の配列なので、Goose の強み(ポインタなしの構造化データ)が関係せず、スレッド間の受け渡しが memcpy になる点も不利です(公式 README の記述で、コストは測っていません)。合うのはフレームの外側で、動画編集ソフトのプロジェクトファイル(タイムライン、クリップ、エフェクトの設定)はセーブデータと同じ性質です。用途としてありえるのかも?という感覚です。 公式には gfx、audio、ui、physics のパッケージがあります。PC 向けのゲームに要るもののうち、標準で覆う範囲とそうでない範囲は次のように分かれます。 Web アプリは、ネットワークが標準にない時点で対象外です。スマホアプリは、ストア配布、課金、OS の SDK 連携が前提なので、Swift や Kotlin か、Unity や Flutter に収束します。PC(Steam)なら、配布と課金を Steam が受け持ち、ウィンドウ、描画、音、入力、物理は Goose のパッケージが覆います。OS の通知やアカウント連携は残りますが、PC のゲームでは必須ではありません。Goose が現実に選択肢になるのは PC 向けに、自分でエンジンを持ちたい小さなチーム で、これは作者自身の立場と一致しています。 ビジネス系の読者には、別の読み方を勧めます。FlatBuffers と data-oriented design の考え方を、コンパイラのエラーメッセージで教わる教材として読む、という読み方です。「同じ root の中でしか相対参照を張れない」というエラーは、どの言語で書いていても、データを連続で置くときに守っている規則そのものです。Redux で状態を正規化して、オブジェクトの参照ではなく ID で持つやり方と同じ発想です。 まとめ Goose は、いろいろな言語の良いところを集めた言語ではありません。FlatBuffers の考え方を言語の値に昇格させ、構文は読者が慣れた形を借りた言語です。一言でまとめると、「FlatBuffers のバッファを、そのまま変数として扱える言語」です。シリアライザが要らないのはそのためで、その代わりに、配列の種類を型で宣言します。触って1番伝わったのは速度ではなく、シリアライザを書かずにセーブデータが往復し、壊れたリンクだけが拒否されること でした。 「はじめに」で触れたとおり、Goose は数年前の人間の設計を AI が実装して世に出た言語です。その設計は、書き手に注釈を書かせず、コンパイラに推論させる方向のものでした。この記事で踏んだコンパイルエラーは、構文の3つも相対参照の root も、メッセージに書かれた仕様書の節番号を読めば直せました。注釈を書かせず、間違いを具体的なエラーで返す設計は、AI がコードを書いて直す回し方と相性がよいはずです。数年前の設計が、その時代とたまたま噛み合っている。この記事を書いていて1番面白かったのは、その巡り合わせでした。 実験に使ったコードと実行結果は、次のリポジトリにあります。Goose の実験4本、L-system の3言語版、セーブデータの Goose 版と C++ 版、計測スクリプト、検証メモが入っています。 akari-iku/goose-playground 参考 公式ページ: aardappel.github.io GitHub: aardappel/goose 設計文書: docs/design/relative_references.md、docs/design/serialization.md(リポジトリ内) 作者: strlen.com 、x.com GIGAZINE の記事: gigazine.net この記事のコードと実行結果: akari-iku/goose-playground

  3. Bluesky · · @spacegirl.ing

    what's the word on aardappel/goose? i feel like i don't quite understand the theoretical improvements well enough to evaluate it

  4. C++やRustよりも高速でメモリも少ないらしい 後で見る aardappel/goose

    10 likes · 1 repost · 1,639 views
  5. Hacker News · · bpierre

    The Goose Programming Language

    1 point · 1 comment
  6. Just as a sidenote: I followed aardappels work before this and I was very curious about this new thing, since all the other previous things are amazing. I also read most of the readme as LLMisms. There might be technical value there, but it's pretty grating for me reading this, personally. And the page itself seems to mention: > Claude Fable: Compiler implementation, Benchmarking, Sample & Doc writing. And at the bottom it says: Built from README.md. And so it seems that there has been some nice AI-isms cleanups since then: github.com/aardappel/goose/commit/543f2b4dba719af… I'll be looking forward to see how it develops, might be more interesting things coming out of it.

    4 points
  7. C++ code that does what Goose does is a soup of pointer manipulation, casts and byte stuffing. You could write that, but Goose proves it to be safe at compile time, with simple readable code. The moment you write "reasonable" code in C++, it is actually a lot slower than Goose. For details on how, see aardappel/goose

  8. DEV · · @jamilxt

    Goose vs Rust: Can a New Language Beat Rust at Memory-Safe Performance? A new programming language can attract attention by promising to be faster than Rust. That claim is easy to make and hard to evaluate. The more interesting question is what the language changes in order to get its results, and whether that change fits the software you need to build. Goose is a systems language from Wouter van Oortmerssen, also known for Lobster. Its current benchmark summary reports that, across sixteen large benchmark cases, Goose's selected implementations run at a geometric-mean 1.13x to 1.19x the speed of the best safe Rust rows, depending on the C compiler backend. The same summary says Goose uses less peak memory in aggregate. Those are meaningful results, but they are not proof that Goose is a drop-in, faster replacement for Rust. The central idea is not a magic optimizer. Goose makes compact, flat, pointer-light data layouts a natural way to represent programs. Rust can express similar layouts, but often with more explicit arena indices or other trade-offs. That difference is what makes Goose worth examining. It is also why the headline comparison needs more context. This is a source-based comparison, not a report from a hands-on migration. I did not run the benchmark suite or build a production application in Goose. The practical verdict below is based on the language reference, the published benchmark notes, and Rust's ownership model. The useful question is not “Is Goose faster?” A language benchmark compresses several choices into one number: the algorithm, data representation, compiler, flags, memory strategy, and the skill of the person writing each version. A result can be valid and still answer a narrower question than its headline suggests. The Goose benchmark summary is unusually helpful because it describes some of those choices. Its October 16, 2026 run covers sixteen benchmark programs at large sizes. The reported geometric means compare the fastest Goose implementation for each benchmark against specified C++ and Rust rows. The page also lists cases where Goose loses or comes close to Rust, rather than presenting only the wins. The benchmark summary says the aggregate favors Goose on the selected rows, while calc and some lru and scene comparisons remain losses or near-ties, depending on compiler. That changes how to read the claim. It is evidence that Goose's design can perform well on this benchmark suite. It is not evidence that a random Rust service, command-line tool, or web backend becomes faster when rewritten in Goose. A fair comparison should ask three things: - Are the workloads relevant? A compact tree, graph, or record-processing benchmark may resemble a game engine or parser more than a typical CRUD service. - Are the representations equivalent? If one version stores references and another stores integer handles, the benchmark is partly about the data model. - Can the result be reproduced? Compiler versions, build flags, generated code, and measurement procedure matter as much as the language name. The benchmark page provides substantial row-level detail, but a reader should still inspect the source and run a workload that resembles their own application before making an adoption decision. Goose and Rust make different memory trade-offs Rust's defining constraint is ownership: values have clear ownership, references have lifetimes, and the compiler rejects many unsafe memory patterns before a program runs. The Rust Book's ownership chapter explains the model and the compile-time errors it prevents. This is powerful, especially when a project has many contributors and long-lived maintenance needs. Goose makes a different set of choices. Its language reference describes roots, stores, writability, reusable pools, and relative references as part of the model. Instead of making the programmer manually manage every object with general-purpose heap allocation, Goose encourages allocating related data into typed regions and using references that fit that allocation structure. The language reference and specification define the rules. This design aims to make compact layouts and stable typed references convenient. It can avoid the overhead of separately allocated objects and can keep related data close together. In some benchmark comparisons, the Goose author contrasts typed references into growing storage with safe Rust versions using integer indices into an arena. That is not an apples-to-apples language-only contest: it is a comparison between two ways of representing and navigating data. It is still useful, because representation is a major part of performance engineering. The practical difference is about what the language asks you to reason about. Rust makes ownership and borrowing visible throughout the code. Goose asks you to understand allocation regions, reference validity, and the constraints of its storage model. Neither is free complexity. One may fit a particular program's shape better than the other. What the benchmark does show The report's best argument is not a single multiplier. It is the variety of workloads and the presence of wins, losses, and caveats. For example, the report says Goose's records benchmark is 2.02 to 2.26 times faster than the Rust row while using 3.98 times less memory. It attributes this to variable-mode enums that store each record's payload, including inline text. In the interp case, it reports a smaller but still notable speed advantage and about half the memory use. Those results support a specific thesis: data structures that can be represented compactly and contiguously may benefit from Goose's design. The same report also explains that the headline aggregate selects the fastest Goose implementation separately for each comparison tier, and the smallest implementation separately for memory. So the aggregate speed and memory figures do not necessarily describe one single implementation configuration. That's reasonable for a survey of best achievable results, but it should not be mistaken for the performance of one default program. It also documents cases where Goose is not ahead. The calc row is slower than the Rust arena version in the listed run. lru loses under MSVC and wins under clang. scene loses under MSVC and is approximately level under clang. The flat blur case illustrates another important point: proving that a function is free of bounds checks does not guarantee the backend will generate the fastest code. Compiler optimization still matters. These details make the benchmark more credible as an engineering report, not as a universal verdict. Readers should preserve the distinctions the report itself makes: compiler backend, benchmark row, memory layout, and selected implementation. Where Rust still has the safer adoption profile Rust's advantage is not only language safety. Its ecosystem, tooling, documentation, package conventions, and community experience reduce the risk of building and maintaining ordinary production software. Goose's reference is extensive and gives a direct goose example.goose workflow, but a language reference is not the same thing as a mature third-party library ecosystem or years of operational experience. That matters for teams. A language is a long-term dependency on compilers, build tools, debuggers, libraries, contributors, and future maintainers. If a project depends on a rare database driver, cryptography library, operating-system binding, or deployment target, benchmark speed may not be the bottleneck. Missing integrations or unfamiliar failure modes can cost far more than a few milliseconds saved in a benchmark. There is also a safety nuance. Goose's own specification and design claims need to be read carefully rather than translated into “same guarantees as Rust.” A language can prohibit certain unsafe behaviors and still differ in how it achieves that result, how its compiler is tested, and what guarantees its users can rely on. The project README says Goose compiles to one C file, so it is sensible to inspect generated-code behavior and the language specification before treating the safety story as equivalent to Rust's established model. This is a review question, not a claim that Goose is unsafe. For a small project or a performance-sensitive component, evaluating Goose may be worthwhile. For a safety-critical system, a large team, or a product whose dependencies are already Rust-native, the burden of proof should be higher. A practical decision checklist Use this checklist before choosing between Goose and Rust: - Start with the bottleneck. Profile the application first. If time is spent in network waits, database queries, serialization, or inefficient algorithms, a language rewrite may not help. - Match the workload. Reimplement one representative hot path, not just a benchmark toy. Include input validation, allocations, error handling, and realistic data sizes. - Compare equivalent designs. Record whether each implementation uses arenas, indices, inline values, or separate allocations. The representation may explain more than the language label. - Measure more than runtime. Track peak memory, binary size, build time, compile errors, debugging effort, and the cost of integrating required libraries. - Review the safety contract. Read the language specification and generated-code strategy. Test boundary cases, invalid inputs, concurrency, and memory growth behavior. - Estimate ecosystem risk. Confirm that the language has the platform support, library coverage, and maintenance outlook your project needs. - Set a stop condition. Decide in advance what improvement would justify migration and what missing capability would end the experiment. For most teams today, Rust is the lower-risk default. Goose is an interesting candidate when compact data representation is central to the workload and the project can tolerate ecosystem risk. The benchmark numbers justify curiosity and a controlled evaluation, not a wholesale rewrite. The takeaway Goose is not best understood as “Rust, but faster.” It is a language that makes a different memory layout strategy central, then demonstrates that strategy on a set of benchmark programs. The reported advantages are strongest where the data structures can stay compact and contiguous. The report also records losses, compiler-dependent results, and implementation choices that shape its aggregate. If you maintain a system where memory layout is already a major performance concern, Goose deserves a small, reproducible experiment. If your priority is broad tooling, library availability, and a well-established safety model, Rust remains the more practical choice for now. The responsible next step is not to trust either camp's headline. It is to test both approaches on the same workload, with equivalent requirements and transparent measurements. I write practical software comparisons and engineering guides. If that is useful, subscribe for the next one. It's free. Have you tried Goose or another Rust alternative? Which workload would you trust a new language with first? Save-worthy takeaway: Benchmark results are a reason to profile and prototype, not a migration plan. Compare data layouts, safety guarantees, ecosystem costs, and real application workloads before choosing a language.

  9. aardappel/goose It’s like they took every pattern you try to stick to for performant code and just made it a first class language feature. Very cool language and idea.

    2 likes · 1 repost
  10. DEV · · @lu1tr0n

    Goose: el lenguaje sin heap, 3.3x más rápido que C++ según sus benchmarks El lenguaje Goose compila a un único archivo en C y, según sus propios benchmarks, corre más rápido que C++ optimizado a mano y que la versión más rápida de Rust seguro: sin heap, sin recolector de basura y sin una sola anotación de vida útil en el código. Lo creó Wouter van Oortmerssen, conocido como aardappel, el mismo desarrollador de Lobster y coautor de FlatBuffers en Google. La promesa central es simple de enunciar y mucho más difícil de lograr: velocidad de C con memoria dinámica segura y sin sintaxis adicional para conseguirlo. Acá va cómo lo logra, qué garantías ofrece y en qué se diferencia de C++ y de Rust. TL;DR - Goose compila a C y elimina heap, recolector de basura y anotaciones de vida útil sin perder seguridad de memoria.- Las estructuras crecen con un simple avance de puntero sobre una pila de datos, sin reservar memoria dinámica.- El compilador rastrea la raíz de cada referencia y rechaza accesos que podrían sobrevivir a su dueño.- Los enums de Goose ocupan el tamaño de su propia variante, no el de la más grande como en un enum de Rust.- Cada hilo de Goose corre como programa aislado y comparte datos por copia, sin locks ni condiciones de carrera. ¿Qué es el lenguaje Goose? Goose es un lenguaje de programación de sistemas, con sintaxis cercana a C y Rust, que elimina el heap, el recolector de basura y las anotaciones de vida útil. Cada valor dinámico vive en una pila de datos que gestiona el compilador, y el lenguaje Goose asegura memoria segura sin esa sintaxis adicional. El lenguaje nació como evolución de las ideas de Lobster, un proyecto anterior del mismo autor que ya proponía manejar la memoria con pilas en lugar de un heap tradicional. Goose lleva esa idea al extremo: en vez de contar referencias o rastrear punteros con un recolector de basura, el compilador prueba en tiempo de compilación que ninguna referencia puede sobrevivir a la variable que la contiene. > 💭 Clave: FlatBuffers, la librería de serialización binaria que Google usa en Android y en varios de sus motores de juego, nació del mismo autor que Goose. La idea de que una estructura en memoria sea también su formato de archivo no es nueva en su trabajo, Goose la lleva al lenguaje entero. La sintaxis es deliberadamente familiar. Quien conozca C o Rust puede leer un programa en Goose sin aprender un paradigma nuevo: hay structs, funciones, bucles for, operadores aritméticos y arreglos. La diferencia está debajo, en cómo se reserva y libera cada byte. Las referencias en Goose siguen válidas incluso después de que el arreglo crezca un millón de veces. Por qué importa Los lenguajes de sistemas llevan tres décadas discutiendo la misma tensión: C y C++ dan control total sobre la memoria pero permiten use-after-free, buffer overflows y otros bugs que generan la mayoría de las vulnerabilidades críticas de software. Rust resolvió ese problema con el borrow checker, pero a cambio pidió aprender ownership, lifetimes explícitos y, en casos complejos, aceptar índices numéricos en lugar de referencias directas porque un Vec<T> puede reallocar en cualquier momento. Goose ataca la misma tensión desde otro ángulo. En lugar de rastrear ownership con anotaciones, el compilador infiere automáticamente la raíz de cada referencia (la variable que delimita su vida útil) y rechaza el programa si detecta un acceso que podría salir de esa raíz. El desarrollador no escribe una sola anotación de vida útil, y aun así el compilador puede dar un error preciso: nombra la referencia y su destino cuando algo no cuadra. El otro punto que importa es el rendimiento. Sobre dieciséis benchmarks, el proyecto reporta que Goose corre a 3,3 veces la velocidad de C++ idiomático, 1,16 veces la de C++ optimizado a mano y 1,12 veces la del mejor Rust seguro equivalente, usando 1,9, 1,3 y 1,2 veces menos memoria respectivamente, según los benchmarks publicados por el proyecto. Son números del propio equipo de Goose, no de un tercero independiente, pero la metodología (dieciséis programas comparables, mismas entradas) está publicada junto con el código fuente de cada prueba. Cómo funciona en detalle Un programa en Goose tiene memoria nativa de pila (la del call stack normal), datos estáticos y N pilas de datos, donde N lo calcula el compilador según cuántos flujos de ejecución necesita el programa. Cada pila de datos es una reserva grande de espacio de direcciones con un puntero de avance (bump pointer): no hay otra memoria dinámica en el sistema. La regla clave es que, en cualquier momento, hay como mucho un valor de tamaño variable activo por pila, y ese valor siempre está en la cima. Esto significa que crecer nunca mueve nada ni revisa una capacidad como hace std::vector::push_back en C++: el compilador ya probó en tiempo de compilación que hay espacio, así que crecer es solo avanzar el puntero. flowchart TD A["Pila de datos reservada"] --> B["Puntero de avance (bump pointer)"] B --> C["Valor dinamico en la cima"] C --> D["Fin del scope"] D --> E["Un solo store libera todo"] Liberar memoria es igual de directo. Cuando el scope donde vive una pila termina, el compilador emite una sola instrucción: devolver el puntero de avance a donde estaba antes de entrar al scope. No importa si la estructura tiene mil elementos o diez millones: liberar un arreglo gigante cuesta lo mismo que liberar un entero, porque no hay que recorrer nada ni invocar destructores en cascada. La otra pieza del modelo son las referencias relativas. En lugar de un puntero de memoria absoluto de 8 bytes, una referencia dentro de una estructura de Goose puede guardarse como un offset de 1, 2 o 4 bytes relativo a su contenedor. El beneficio no es solo ahorro de espacio: una estructura construida así es independiente de la posición en memoria, lo que significa que guardarla en disco es un simple write, y cargarla es un read más una pasada de verificación que rechaza bytes manipulados. El formato en memoria es, literalmente, el formato de archivo. Ejemplos prácticos La mejor forma de ver el modelo en acción es con el propio código de ejemplo que publica el proyecto. Partimos de lo más simple y escalamos hasta el caso que demuestra por qué las referencias no se rompen cuando el arreglo crece. Un programa mínimo en Goose imprime texto con print, que acepta varios argumentos separados por coma y los concatena: fn main() { print("Hola, Goose"); } La salida es literalmente la cadena que se pasó: Hola, Goose. No hay heap involucrado ni siquiera en este ejemplo trivial: la cadena vive en la pila estática del programa. El segundo ejemplo, tomado directo del repositorio oficial, muestra un arreglo que crece cien mil veces dentro de un bucle y se libera solo al cerrar el bloque: for round in 3 { var scratch: u8[>..] = []; // crece solo con un avance de puntero for i in 100000 { scratch.push((i % 256) as! u8); } print("round ", round, ": ", scratch.len, " bytes"); } // el free: un solo store al tope de la pila Cada ronda declara scratch desde cero, lo llena con cien mil bytes y lo imprime. La salida esperada, literal, es: round 0: 100000 bytes round 1: 100000 bytes round 2: 100000 bytes Ese número no cambia entre rondas porque cada ronda vuelve a declarar scratch vacío: no hay acumulación entre iteraciones, y liberar los cien mil bytes al cerrar cada ronda cuesta la misma instrucción que liberar un byte. El tercer ejemplo es el que de verdad separa a Goose de un Vec<T> de Rust o un std::vector de C++: una referencia al primer elemento de un arreglo sigue siendo válida después de empujarle un millón de elementos más. struct Item { id: i32, weight: f32 } var items: Item[>..] = []; let first .= items.push(Item { 1, 0.5 }); for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); } first.weight = 99.5; // sigue siendo valida, un millon de pushes despues print(first.weight); sequenceDiagram participant P as Programa participant S as Pila de datos P->>S: items.push(Item) S-->>P: referencia al nuevo elemento P->>S: push 1000000 elementos mas Note over P,S: la referencia inicial sigue valida push devuelve una referencia al elemento recién insertado, y esa referencia (first) sigue apuntando al mismo elemento aunque el arreglo haya crecido un millón de veces. La última línea imprime 99.5, el valor que se le asignó al final. En C++ esa misma operación requeriría reservar capacidad por adelantado con reserve(); en Rust seguro, lo habitual es guardar un índice u32 en vez de una referencia directa, precisamente porque un Vec puede reubicarse en memoria al crecer. Cómo empezar Goose todavía no publica binarios de release estables, así que la forma más directa de probarlo es clonar el repositorio oficial y seguir el tutorial paso a paso que lo acompaña. Antes de eso, necesitás un compilador de C instalado (gcc o clang en Linux/macOS, o el que trae MSVC en Windows), porque Goose traduce cada programa a un único archivo .c antes de ejecutarlo; el propio proyecto incluye además un backend con TinyCC embebido que compila y corre un programa en el mismo proceso, sin paso de build aparte. Linux y macOS: git clone https://github.com/aardappel/goose.git cd goose Windows (PowerShell): git clone https://github.com/aardappel/goose.git cd goose Desde ahí, el tutorial oficial indica paso a paso cómo compilar el propio compilador de Goose y ejecutar los primeros programas con el backend de TinyCC, sin necesidad de instalar un toolchain de build adicional para los ejemplos. No repetimos acá los comandos exactos del CLI porque el proyecto los puede cambiar entre commits mientras no exista una release etiquetada; la fuente de verdad es siempre el repositorio. Cada programa de Goose se traduce a un único archivo en lenguaje C antes de ejecutarse. Casos de uso reales Goose es, a fecha de hoy, un proyecto de investigación y compilador experimental, no una base de código con historial de producción. Donde el modelo de memoria rinde mejor es en programas con muchas estructuras dinámicas de vida corta y predecible: parsers que construyen un árbol y lo descartan entero al final, simuladores que recrean su estado en cada paso, o motores de juego donde cada frame reserva y libera las mismas formas de datos. > ⚠️ Ojo: Sin binarios de release ni versión 1.0 anunciada, usar Goose hoy significa aceptar que la sintaxis y el CLI pueden cambiar entre commits; no es una apuesta razonable todavía para un proyecto con fecha de entrega. El caso de las referencias relativas también lo acerca a un uso muy concreto: estructuras que son, a la vez, su propio formato de archivo. Un árbol de escena de un videojuego o un dataset binario construido con offsets relativos puede escribirse a disco con un solo write y volver a cargarse con un read más una verificación, sin un paso de serialización intermedio como el que necesitan JSON o protobuf. El modelo de hilos de Goose refuerza esa misma filosofía: cada worker se compila como un programa separado con su propia memoria, y los valores planos cruzan colas tipadas con un memcpy. No hay locks, atómicos ni memory ordering que razonar porque, directamente, no existen en el lenguaje: dos hilos de Goose nunca comparten una dirección de memoria. Errores comunes y buenas prácticas El error más común al migrar la cabeza desde C++ o Rust es asumir que Goose tiene algún escape hatch tipo unsafe. No lo tiene. Si el compilador determina que una referencia podría sobrevivir a la variable que la contiene, el programa simplemente no compila, y no hay forma de forzarlo con una anotación. Un patrón típico que dispara ese rechazo es intentar que una referencia declarada en un scope externo apunte a datos creados dentro de un bloque anidado que todavía no cerró. El ejemplo de la documentación oficial usa una función longest que devuelve la más larga de dos franjas (u8[:]) y luego intenta asignar el resultado, cuando una de las dos franjas vive en un bloque interior, a una variable del scope exterior. El compilador rechaza esa asignación y en el error nombra tanto la raíz de la referencia como el destino que la violaría, así que el mensaje señala exactamente qué variable hay que mover o qué scope hay que cambiar. Otro gotcha, menos sobre errores de compilación que sobre expectativas: una pila de datos reserva un rango grande de espacio de direcciones por adelantado. Eso es gratis en sistemas de 64 bits con memoria virtual, pero es la razón por la que Goose apunta a sistemas con ese modelo de memoria y no, todavía, a microcontroladores con direccionamiento de 16 bits. La buena práctica que se deriva de todo esto es diseñar las estructuras de datos pensando en qué pila las va a contener y por cuánto tiempo, igual que en C uno piensa en qué arena o qué buffer va a usar un dato, pero sin tener que escribir esa decisión como código: el compilador la calcula a partir de dónde declarás cada variable. Comparativa con alternativas A diferencia de C++ y Rust, el lenguaje Goose no exige elegir entre velocidad y seguridad, pero tampoco llega gratis: cambia anotaciones explícitas por restricciones más rígidas sobre cómo se puede estructurar un programa. OpciónCuándo usarlaVentajaLimitaciónC++ (manual)Control total de layout y ya existe disciplina de memory safety en el equipoMáximo control sobre cada byteSin garantías del compilador: use-after-free y overflows posiblesRust (seguro)Ecosistema maduro y tolerancia al costo de ownership explícitoSeguridad de memoria probada en producción a gran escalaExige lifetimes anotados y a veces índices u32 en vez de referenciasGooseProgramas con muchas estructuras inline de vida corta y foco en rendimientoCero anotaciones de vida útil, referencias estables tras crecerLenguaje nuevo, sin ecosistema ni librerías maduras todavía La fila de Rust merece un matiz: el borrow checker de Rust es más expresivo que el inferidor de raíces de Goose en casos con aliasing complejo (varias referencias mutables condicionadas), justamente porque Rust sí permite anotar esos casos a mano. Goose apuesta a que la mayoría de los programas reales no necesitan esa expresividad extra, y a cambio no pide escribir ninguna anotación. flowchart LR subgraph ClasicoCPP["C++ / Rust"] A1["Heap"] --> A2["Allocator"] A2 --> A3["Posible reallocation"] end subgraph GooseLang["Goose"] B1["Pila de datos"] --> B2["Bump pointer"] B2 --> B3["Nunca se mueve"] end Profundizando Las referencias relativas no son solo una optimización de espacio. Al guardar un offset de 1, 2 o 4 bytes en vez de un puntero absoluto, toda la estructura queda position independent: no importa en qué dirección de memoria termine montada. Eso es lo que permite tratar una estructura en memoria como su propio formato de archivo, algo que ni C++ ni Rust resuelven de forma nativa, donde lo normal es escribir un serializador aparte, como protobuf o FlatBuffers. Los enums de Goose (variable-mode ADTs, en la terminología del proyecto) le dan a cada valor el tamaño de su propia variante en lugar del tamaño de la variante más grande, que es como funciona un enum de Rust. El proyecto reporta, sobre el benchmark que ejercita esto específicamente, 4 veces menos memoria y 2 veces la velocidad frente al enum de Rust equivalente. Los genéricos de Goose no tienen costo en runtime: un parámetro sin tipo declarado es genérico, y las funciones de orden superior son entidades de tiempo de compilación. Por eso xs.filter() { it > 0 } compila exactamente al bucle que parece ser en el código fuente, sin boxing, sin vtable y construyendo el resultado directo en su destino final. Otra pieza poco común es el retorno a través de múltiples llamadas: return err from load retorna desde una función cualquier número de niveles hacia arriba, verificado en tiempo de compilación, sin un unwinder de excepciones, sin un tipo Result y sin un operador ? en cada llamada intermedia. Es, en esencia, el mismo resultado que logra la propagación de errores de Rust, pero resuelto en el chequeo estático en lugar de con un tipo contenedor que hay que desenvolver manualmente en cada paso. Como todo programa de Goose termina siendo un único archivo en C, hay una forma concreta de confirmar que el modelo de memoria funciona como describe el proyecto: generar ese archivo .c intermedio y buscar en él llamadas a malloc, calloc o free con un simple grep. Si el programa no usa extern fn hacia una librería de C que sí reserve memoria dinámica, esas llamadas no deberían aparecer. No es una garantía formal, no sustituye la prueba de tipos del propio compilador, pero es la forma más directa de verificarlo desde afuera sin instrumentación adicional. 📖 Resumen en Telegram: Ver resumen Tu próximo paso: cloná el repositorio de Goose, abrí el ejemplo de items.push con el millón de inserciones, cambiá el tipo de first por un índice numérico como haría Rust y compará cuántas líneas hacen falta para lograr lo mismo que una sola referencia. Preguntas frecuentes ¿El lenguaje Goose ya es apto para producción? No según el propio proyecto: no hay binarios de release estables ni una versión 1.0 anunciada, así que la sintaxis y el CLI pueden cambiar entre commits del repositorio. ¿Goose necesita un recolector de basura en segundo plano? No. La memoria dinámica vive entera en pilas de datos con un puntero de avance; liberar un scope es una sola instrucción, sin ciclo de recolección ni pausas. ¿Cómo evita Goose los errores de memoria sin anotaciones como las de Rust? El compilador infiere la raíz, la variable dueña, de cada referencia y rechaza cualquier acceso que pudiera sobrevivir a esa raíz, sin que el programador escriba lifetimes a mano. ¿Goose reemplaza a Rust o a C++? Todavía no para proyectos con historial de producción: Rust y C++ tienen ecosistemas, librerías y herramientas maduras que Goose, como proyecto nuevo, no tiene aún. ¿Qué significa que Goose compile a C? Que cada programa en Goose se traduce a un único archivo fuente en lenguaje C antes de ejecutarse, lo que le permite correr en cualquier plataforma con compilador de C y llamar código C directo con extern fn. ¿Qué pasa con los hilos en Goose? Cada hilo se compila como un programa separado con su propia memoria; los datos que cruzan entre hilos lo hacen por copia (memcpy) a través de colas tipadas, así que no existen locks, atómicos ni condiciones de carrera en el lenguaje. Referencias - aardappel.github.io: sitio oficial del proyecto, con el tutorial, la referencia del lenguaje y los benchmarks citados en este artículo.- aardappel/goose: repositorio del código fuente y punto de partida para clonar y probar el compilador.- doc.rust-lang.org: capítulo de ownership del libro oficial de Rust, usado como referencia para la comparación de modelos de memoria.- en.wikipedia.org: definición general de seguridad de memoria y de las clases de bugs que estos modelos buscan evitar. 📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

Scan pending.

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
aardappel/goose mentions in the last 30 days