🕑

ChatGPT PlusのProjectsだけでプログラミング言語を作り始めて早2週間

に公開

はじめに

Aneというプログラミング言語を作り始めて、2週間が経過しました。

https://gitlab.com/tkithrta/ane
https://zenn.dev/tkithrta/articles/2b0134084a45d6

前回の記事では、ChatGPT PlusのProjectsだけを作業場にして、Python製のStage0コンパイラ、仕様、例、失敗するべき入力、検査スクリプトを一緒に育てている話を書きました。そこからさらに1週間が経ち、Aneはまた少し言語らしくなりました。

この1週間でいちばん大きかったのは、ジェネリクスです。型エイリアス、レコード、enum、関数が、かなり素直に型パラメータを持てるようになりました。Option[T]Result[T, E]が普通のコアモジュールとして見えるようになり、Vec[T]Buffer[T]のような小さな所有しない記述子も、特別な魔法ではなく普通の型として書けるようになっています。

そして、それに合わせてハードルールも1つ増えました。Aneはランタイム型を持たない、というルールです。

この記事では、まず追加したハードルールを説明し、そのあとTypeScript、Rust、Goと比べながら、今のAneのジェネリクスがどの位置にあるのかを書きます。あわせて、この1週間で進んだテスト環境の改善についても、現在の開発フローを支える仕組みとして触れます。

ぼくのかんがえたさいきょうのジェネリクス

前回の時点で、Aneには5つのハードルールがありました。

隠れた制御フローを持たない。隠れたメモリアロケーションを持たない。プリプロセッサもマクロも持たない。型以外の綴りの別名を持たない。改行に意味を持たせない。

ここに、6つ目のルールを追加しました。

ランタイム型を持たない

型、ジェネリックパラメータ、エフェクト、レイアウト情報は、ランタイム値ではなくコンパイル時の事実です。ジェネリック宣言は、バックエンド出力の前に解決されます。コア言語には、リフレクション、ランタイム型オブジェクト、暗黙の型辞書、隠れたジェネリックディスパッチ、型に基づくランタイム検索はありません。

なぜこのルールを追加したのか

このルールは、ジェネリクスを入れたからこそ必要になりました。

たとえば、Box[i32]Box[u8]があるとします。Aneでは、これらを「実行時に型引数を持っているBox」として扱うのではなく、コンパイル時に別々の具体的なレイアウトとして解決します。

Ane
type Box<T> = {
    value: T,
}

fn unbox_i32(b: Box[i32]) -> i32
    return b.value
end

fn unbox_u8(b: Box[u8]) -> u8
    return b.value
end

ここでTは実行時の値ではありません。b.valueを読むときに、実行時の型オブジェクトを見てフィールドを探すわけでもありません。Box[i32]ならvaluei32Box[u8]ならvalueu8です。これは型検査と具体化の段階で決まります。

AneはC99へ落とす言語です。低レイヤーの足場にしたいので、生成されたCの側にも、隠れた型辞書や型情報テーブルを持ち込みたくありません。もちろん、将来デバッグ情報やメタデータを別の層で扱う可能性はあります。しかし、コア言語の値としてランタイム型を持つ方向には進めない、という線をここで引きました。

このルールは、Aneのジェネリクスを説明するうえでかなり重要です。Aneのジェネリクスは、ソースを書き換えるマクロではありません。実行時に型を調べるリフレクションでもありません。コンパイル時に型を受け取り、具体的な型と具体的な呼び出しへ落としていく仕組みです。

Generics

AneのGenericsはどこまで来たのか

現在のAneのStage0では、ジェネリックな型エイリアス、ジェネリックなレコード、ジェネリックなenum、制約なしのジェネリック関数を扱えるようになりました。

この4つが入ったことで、Option[T]Result[T, E]のような型が、キーワードでもマクロでもなく、普通のAneコードとして見えるようになりました。さらに、生成Cの手前でモノモーフィゼーションされるため、低レイヤーのターゲットへ持っていくときにも、ランタイム型や型辞書を要求しません。

ここからは、今のAneのジェネリクスを6つに分けて見ていきます。Generic type alias、Generic record、Generic enum、Generic function、Monomorphization、そして型引数の構文です。

Generic type alias

まずは、ジェネリックな型エイリアスです。

Ane
module core.arrays

type Array2<T> = [2]T
type Array3<T> = [3]T
type Array4<T> = [4]T

Array4[T]は、長さ4の配列らしき何かではありません。[4]Tへの型エイリアスです。Array4[u8]なら[4]u8Array4[i32]なら[4]i32として、生成Cの前に具体化されます。

タプルも同じです。

Ane
module core.tuples

type Pair<T, U> = (T, U)
type Triple<T, U, V> = (T, U, V)
type Quadruple<T, U, V, W> = (T, U, V, W)

Pair[T, U]もランタイムの可変長タプルではありません。固定長の値型に対する別名です。ここまででも、コアモジュールの見通しはかなりよくなりました。

本当は、もっと一般化したくなります。たとえば、Array[N, T]のような型を作るにはConst generics(type-level integer parameters)が欲しくなります。タプルについては、Tuple[Ts...]のような形を作るためにVariadic generics(type packs)が欲しくなります。さらに、値の側でも可変個数の引数を扱うVariadic generics(value packs)が欲しくなる場面があります。

ただ、今のAneでは実装していません。型レベル整数、型パック、値パックを入れると、型引数の個数、評価順、診断、再帰的な具体化、ABIへの落とし方、エフェクトとの関係をまとめて設計する必要があります。便利ではありますが、Stage0のシンプルさを維持しづらくなります。

そこで現在は、よく使いそうな固定長配列と固定長タプルを、普通のジェネリック型エイリアスとして置いています。小さいですが、何が起きているかを説明しやすい形です。

Generic record

次に、ジェネリックなレコードです。

Aneのレコードは、今かなり便利になっています。フィールド名が見えて、レイアウトが型として見えて、ジェネリクスと組み合わせても特別な構文を増やさずに済みます。

Ane
type Box<T> = {
    value: T,
}

type Entry<K, V> = {
    key: K,
    value: V,
}

type Span<T> = {
    ptr: *T,
    len: usize,
}

Box[i32]ならvaluei32です。Entry[string, i32]ならkeystringvaluei32です。Span[u8]ならptr*u8lenusizeです。

レコードリテラルは、宛先型からチェックされます。

Ane
fn make_entry() -> Entry[string, i32]
    return { key = "answer", value = 42 }
end

fn make_span(ptr: *u8, len: usize) -> Span[u8]
    return { ptr = ptr, len = len }
end

この書き方は、ジェネリクスと相性がよいです。型引数はレコード名の側で分かり、フィールドの対応は名前で分かります。呼び出し側から見ると、余計な記号を増やさずに、Entry[string, i32]という型と{ key = ..., value = ... }という値の形を対応させられます。

また、.anedataで型付きデータを扱う方針とも合っています。Aneでは、波かっこで囲まれたデータを、勝手に動的なオブジェクトにしたいわけではありません。宛先型があり、その型に沿ってフィールドを検査する。ジェネリックなレコードでも、この方針は変わりません。

Generic enum

ジェネリックなenumが入ると、Option[T]Result[T, E]を普通のコアモジュールとして書けます。

Ane
module core.option

type Option<T> = enum
    Some(T)
    None
end

fn unwrap_or<T>(value: Option[T], fallback: T) -> T
    if value == Option.Some then
        return value.Some
    else
        return fallback
    end
end

このvalue == Option.Someは、Some(T)の中に入っている値まで比較しているわけではありません。Aneでは、このような比較をタグテストとして扱います。

Option[T]の値が今どのvariantなのか、つまりタグがSomeなのかNoneなのかだけを確認します。Someの中身であるTは、この比較には使われません。

そのため、Tが比較できる型かどうかは関係ありません。Option[Uncomparable]のような型であっても、value == Option.Someというタグテスト自体は成立します。ここで比較しているのはペイロードではなく、enumのタグだからです。

ペイロードを読むときは、タグテストでそのvariantだと分かっている範囲の中で、value.Someのように射影します。この射影は、隠れた分岐や実行時の型検索ではなく、フロー検査で許可されたフィールドアクセスです。

Option[T]はキーワードではありません。nilでもありません。普通のgeneric enumです。Option.Noneも、暗黙のnullではなく明示的なvariantです。

Ane
module core.result

type Result<T, E> = enum
    Ok(T)
    Err(E)
end

fn unwrap_or<T, E>(value: Result[T, E], fallback: T) -> T
    if value == Result.Ok then
        return value.Ok
    else
        return fallback
    end
end

Aneには例外も、暗黙のpanic経路も、隠れたreturnもありません。回復可能な失敗は、Result[T, E]のような値として見える形にします。ここはRustの影響を感じるところですが、AneではResultそのものを特別扱いしないのが大事です。普通のモジュールにある普通のgeneric enumです。

Generic function

Aneのジェネリック関数は、制約なしの小さな範囲から始まっています。

Ane
module core.slices

fn len<T>(s: []T) -> usize
    return #s
end

fn ptr<T>(s: []T) -> *T
    return s.ptr
end

呼び出し側では、引数の型からTを推論できます。

Ane
fn count_bytes(s: []u8) -> usize
    return slices.len(s)
end

明示的に型引数を書くこともできます。

Ane
fn first_ptr(s: []u8) -> *u8
    return slices.ptr[u8](s)
end

推論は便利ですが、魔法にはしません。型パラメータが引数にも宛先の戻り値型にも現れないなら、推論できないので拒否します。戻り値だけに型パラメータがある場合は、宛先の型から分かるときだけ使えます。

このあたりは、TypeScriptやRustやGoのように成熟した言語と比べると、かなり小さいです。しかし、小さいからこそ、何が起きているかを説明しやすいです。呼び出し引数と宛先型から分かる範囲で具体化する。分からなければ拒否する。隠れたランタイム検索には逃げない。

Monomorphization

今回の主役は、実はここかもしれません。Aneのジェネリクスは、生成Cの手前でモノモーフィゼーションされます。日本語で言うなら、型引数ごとに具体化する、ということです。

https://en.wikipedia.org/wiki/Monomorphization

たとえば、Ane側では次のように書けます。

Ane
use module core.result { Result }
use module core.option { Option }
use module core.unit { Unit }

fn demo(flag: bool) -> Result[Option[i32], Unit]
    if flag then
        return Result.Ok(Option.Some(42))
    else
        return Result.Ok(Option.None)
    end
end

ここでは、Result[Option[i32], Unit]という入れ子のジェネリック型が出てきます。Aneはこれを、実行時の型オブジェクトとして持ち回りません。Option[i32]Result[Option[i32], Unit]という具体的な型として、バックエンド出力の前に解決します。

この方針のおかげで、Option[T]Result[T, E]はフリースタンディングターゲットでも使えます。ヒープ、libc、例外、unwinding、panic builtin、リフレクション、ランタイム型オブジェクト、隠れたディスパッチを要求しません。

ここが、新しく追加したハードルールとつながります。型、ジェネリックパラメータ、エフェクト、レイアウト情報は、ランタイム値ではなくコンパイル時の事実です。つまり、Tは実行時に取り出せる値ではありません。Tに応じて実行時に型テーブルを引くわけでもありません。使われた具体的な型ごとに、Cへ出る前に形を決めます。

もちろん、そのぶん制限もあります。制約付きジェネリクスはまだありません。比較、ハッシュ、アロケーションのような能力を暗黙に要求する仕組みもありません。比較したいなら比較関数を渡す、アロケーションしたいならアロケータを明示する、という方向を先に考える必要があります。

でも、今のAneにはこれくらいで十分だと思っています。むしろ、ここで止めたことに価値があります。Option[T]Result[T, E]を普通に書けるだけで、エラー処理やUTF-8処理や整数変換の見通しがかなりよくなりました。

型引数の構文

Aneでは、型パラメータの宣言には山かっこを使います。

Ane
type Box<T> = {
    value: T,
}

fn make_box<T>(value: T) -> Box[T]
    return { value = value }
end

一方、型の適用には角かっこを使います。

Ane
let b: Box[i32] = make_box(123)

ここはRustやTypeScriptの見た目と少し違います。宣言側はBox<T>、使用側はBox[i32]です。Aneの式文法では、<>は比較演算子としても使います。型の適用を角かっこに分けることで、式と型の境界を追いやすくしています。

ジェネリック関数へ明示的に型引数を書くときも、適用側なので角かっこを使います。

Ane
fn first_ptr(s: []u8) -> *u8
    return slices.ptr[u8](s)
end

山かっこは「型パラメータを導入する場所」、角かっこは「すでにある型や関数に型引数を渡す場所」です。この使い分けは、他の言語との比較でも重要になります。

TypeScriptと比べる

TypeScriptのジェネリクスは、アプリケーションを書くときにはとても気持ちのよい仕組みです。値の形を保ちながら型だけを一般化できます。

TypeScript
function identity<T>(value: T): T {
  return value;
}

const n = identity<number>(123);
const s = identity<string>("abc");

Aneと少し似て見えるところとして、TypeScriptには.d.tsがあります。実装とは別に、外から見える型の形を書けます。Aneでも、外部境界や型付きデータの形をコード本体から分けたい気持ちがあります。

ただし、TypeScriptの型の世界はかなり強力です。.d.ts自体は実行される本文を書く場所ではありませんが、型レベルでは条件型のような分岐を持てます。

TypeScript
type Payload<T> = T extends "int" ? number : string;

declare function read<T extends "int" | "text">(kind: T): Payload<T>;

const a = read("int");
const b = read("text");

この例では、T"int"ならnumber、そうでなければstringという型レベルの分岐があります。さらにTypeScriptには、mapped typesやinferを使った型レベルの計算もあります。これは便利ですが、Aneがいま入れたいものとはだいぶ違います。

Aneには、型レベルのifも、型引数に応じて別の型を選ぶ条件型もありません。Result[i32, ParseError]のような型は、実行時の型オブジェクトでも、型レベルプログラムの結果でもなく、バックエンド出力の前に具体化される型です。

ここでのAneのデメリットは、TypeScriptほど柔らかく型を変形できないことです。入力の形から戻り値型を高度に変えるAPIは、TypeScriptのほうがかなり書きやすいです。一方で、Aneでは「どの型がCへ出る前に具体化されるのか」を追いやすくしたい。型の便利な制御文を入れすぎると、低レイヤーへ出す前に何が起きているのかを説明しづらくなります。

TypeScriptのジェネリクスが「JavaScriptに型の道具を重ねる」方向だとすると、Aneのジェネリクスは「低レイヤーへ出す前に型を具体化する」方向です。

Rustと比べる

Rustのジェネリクスは、Aneから見るとかなり近い面と、かなり遠い面があります。

Rust
fn identity<T>(value: T) -> T {
    value
}

fn max<T: Ord>(a: T, b: T) -> T {
    if a >= b { a } else { b }
}

近いのは、具体的な型に対してコンパイル時に解決していく感覚です。Option<i32>Result<T, E>が普通の型として扱われ、低レイヤーでも使いやすいところは、Aneが目指している気持ちに近いです。

ただし、Rustにはtrait bound、所有権、ライフタイム、借用、標準ライブラリ、パターンマッチなど、巨大で強力な設計があります。Aneは今そこへ一気に行こうとしていません。

特に分かりやすい違いはmatchです。Rustでは、enumを分解しながら網羅的に分岐できます。

Rust
fn unwrap_or<T>(value: Option<T>, fallback: T) -> T {
    match value {
        Some(x) => x,
        None => fallback,
    }
}

これはとても強力です。分岐、分解、束縛、網羅性検査が1つの構文にまとまっています。Result<T, E>の処理も、matchがあるとかなり読みやすくなります。

では、なぜ今のAneにはmatchキーワードがないのか。理由は、Aneがパターンマッチを嫌っているからではありません。Stage0の時点では、分岐とvariantの取り出しを、もっと単純な形で表面に出しておきたいからです。

Ane
fn unwrap_or<T>(value: Option[T], fallback: T) -> T
    if value == Option.Some then
        return value.Some
    else
        return fallback
    end
end

ここでも、value == Option.Someはタグテストです。Some(T)の中身を比較しているのではなく、この値がSome variantかどうかだけを見ています。そのうえで、then側ではvalue.Someとしてペイロードを読めます。

この書き方はRustのmatchより冗長です。網羅性も、パターンの分解も、見た目の美しさもRustに負けます。ここは明確にAne側のデメリットです。

その代わり、今のAneでは「どこで分岐しているか」「どこでvariantを読んでいるか」がかなり素朴に見えます。matchを入れるなら、パターンの文法、束縛のスコープ、ガード、網羅性、ネストした分解、失敗するパターンの扱いまで一緒に決める必要があります。Aneはまだそこへ行かず、まずgeneric enumそのものと、Cへ落とす具体化を固めています。

現在のAneには、trait boundに相当する仕組みもありません。比較できる型だけを受け取る、ハッシュできる型だけを受け取る、アロケータを暗黙に要求する、というような一般制約はまだありません。だから、次のようなRust風の書き方をAneにそのまま持ち込む段階ではありません。

Rust
fn contains<T: Eq>(xs: &[T], needle: T) -> bool {
    for x in xs {
        if *x == needle {
            return true;
        }
    }
    false
}

Aneで同じ方向へ進むなら、比較関数や比較可能性をどう渡すかを先に設計する必要があります。暗黙の比較制約や暗黙のディスパッチを入れると、「隠れた制御フローなし」「ランタイム型なし」のルールに触れます。

今のAneは、Rustのような強い抽象化へ憧れつつ、まずはもっと小さい場所から始めています。Option[T]Result[T, E]Box[T]Span[T]のように、型の形を具体化できる範囲を先に固める。制約やmatchはあとで必要になってから考える。これは、Stage0の小ささを守るための順番です。

Goと比べる

Goのジェネリクスは、言語全体の簡潔さを保ちながら型パラメータを入れた仕組みとして、とても参考になります。

Go
func Identity[T any](value T) T {
    return value
}

type Pair[T any, U any] struct {
    First  T
    Second U
}

Goは型パラメータに山かっこを使いません。角かっこを使います。Aneは、宣言側では山かっこ、型の適用側では角かっこを使い分けています。

Ane
type Pair<T, U> = (T, U)

type Entry<K, V> = {
    key: K,
    value: V,
}

let p: Pair[i32, string] = (1, "one")
let e: Entry[string, i32] = { key = "one", value = 1 }

この違いは小さく見えますが、読み味にはかなり効きます。Goでは、型パラメータの宣言、スライス、配列、インデックス、型セットが、同じ角かっこの見た目に近い場所へ集まります。

Go
func Map[S ~[]E, E any](s S, f func(E) E) S {
    out := make(S, len(s))
    for i, v := range s {
        out[i] = f(v)
    }
    return out
}

これはGoとしてはよく整理された構文です。ただ、Aneから見ると、S ~[]Eのような型セット、[S ...]の型パラメータ、[]Eのスライス、out[i]のインデックスが、かなり近い記号で並びます。山かっこを使わないことのデメリットは、型パラメータ宣言が他の角かっこ構文と視覚的に混ざりやすいことです。

Aneでは、型パラメータを宣言する場所は<T>、型を適用する場所はBox[i32]、固定配列は[4]T、スライスは[]Tです。角かっこをまったく使わないわけではありません。むしろ使います。ただし、宣言と適用の記号を分けることで、「ここで型パラメータを導入している」のか「ここで既存の型に型引数を渡している」のかを見分けやすくしています。

Goには制約付き型パラメータがあります。

Go
type Ordered interface {
    ~int | ~int64 | ~float64 | ~string
}

func Min[T Ordered](a T, b T) T {
    if a < b {
        return a
    }
    return b
}

これは便利です。T<を使えることが制約で分かります。Aneは、今のところここまで進めていません。Tに対して<が使えるのか、==が使えるのか、コピーしてよいのか、アロケーションが必要なのか、といった話は、いずれ必要になります。

ここでのAneのデメリットは、Goよりも汎用的な関数を書きづらいことです。Min[T]のような関数は、Aneではまだ素直に書けません。その代わり、暗黙の演算子能力や型セットを入れる前に、エフェクト、メモリ、ABI、診断を小さく保っています。

Goは実用的なホスト環境、ランタイム、標準ライブラリと一緒に使う言語です。Aneは、まずフリースタンディングに持っていける小さな核を作りたい言語です。この違いは大きいです。

Aneのジェネリクスは、Goのような使いやすい制約付きジェネリクスをいきなり目指すのではなく、制約なしで安全に具体化できる部分を先に実装しています。これは地味ですが、C99に落とすStage0コンパイラには合っています。

テスト環境の改善

この1週間で、テスト環境もかなり変わりました。前回は、通るべき例と落ちるべき例を、fixture配下の個別ファイルとして増やしていく形でした。.aneファイルだけでも150個近くあり、失敗するべき入力はファイル名やディレクトリ構成から読み取る部分が大きくなっていました。

現在は、fixtureをTOMLマニフェストに寄せています。物理的なfixtureファイルは、レイヤーごとの少数のマニフェストにまとまりました。その一方で、case数はむしろ増えています。入力ソース、付随する.anetype.anedata、期待する成功または失敗、Cを出すかどうか、生成Cに含まれてほしい文字列、含まれてほしくない文字列まで、1つのcaseに閉じ込められるようになりました。

[group]
name = "l4"
layer = "L4"

[defaults]
target = "x86_64-none-elf"
expect = "ok"

[[case]]
name = "enum_constructor"
entry = "enum_constructor.ane"

[case.files]
"enum_constructor.ane" = '''
type Code = enum
    Ok
    Err(i32)
end

export fn kmain() -> Code
    return Code.Ok
end
'''

[case.checks]
ane_compile = true
emit_c = false
cc_compile = false

この変更は、ChatGPT PlusのProjectsで作業するうえでもかなり効きました。前回の形だと、zipを展開してfixtureを読むだけで大量の小さなファイルへアクセスする必要がありました。Advanced Data AnalysisのPython実行環境で全体を確認すると、ファイル読み込みの数が増え、作業中にタイムアウトしやすくなります。

マニフェスト化すると、読むべき物理ファイルの数が劇的に減ります。しかも、caseの中にソースと期待結果がまとまっているので、「この入力は何を検査しているのか」を追いやすくなります。失敗するべきcaseも、expect = "error"として明示できます。ファイル名にinvalid_と付けて察するだけではなく、マニフェスト上の期待値として固定できます。

もうひとつよかったのは、テストの粒度を上げやすくなったことです。以前は、新しい失敗例を足すたびに新しい.aneファイルを増やしていました。現在は、同じレイヤーのマニフェストにcaseを足せます。小さな入力、小さな期待値、小さな生成Cチェックを並べられるので、ジェネリクスのように細かい境界が多い機能と相性がよいです。

また、group.layerによって、そのcase群がどのレイヤーの言語機能として検査されるのかも分かりやすくなりました。ジェネリクスは、L3のrecordレイアウト、L4のenumコンストラクタ、L5のunsafeエフェクトフェンスにまたがります。だからこそ、構文だけでなく、具体化、型検査、フロー検査、C生成まで含めて少しずつ固める必要があります。

マニフェストにレイヤーとcaseを一緒に置けると、「この入力はどの段階で通すべきか」「この入力はどの段階では拒否すべきか」を記録しやすくなります。

ChatGPTと一緒に言語処理系を育てる場合、テストは単なる安全網ではありません。仕様を固定する杭です。人間が「これは通す」「これは落とす」「この出力はこうなる」と決め、その杭をマニフェストに残す。そうすると、次の会話で実装を進めても、前に決めた境界へ戻りやすくなります。

おわりに

ChatGPT PlusのProjectsだけでプログラミング言語を作り始めて、2週間が経ちました。1週間前は、Aneが小さなシステム言語として形を持ち始めたことが主役でした。今回は、ジェネリクスが入ったことで、コアモジュールの見え方が一段変わりました。

Option[T]Result[T, E]Array4[T]Pair[T, U]Vec[T]Buffer[T]。こうした型が、キーワードでもマクロでもランタイム型でもなく、普通のAneコードとして見えるようになってきました。

そして、その裏側で「ランタイム型を持たない」というハードルールを追加しました。これは、今後Aneが便利な抽象化を増やしていくときのブレーキになります。ジェネリクスを入れたからこそ、型を実行時の便利な箱にしない。エフェクトを入れたからこそ、エフェクトを値や暗黙のディスパッチにしない。便利さを増やすたびに、どこまでがコンパイル時の事実で、どこからが実行時の値なのかを分ける必要があります。

今回の記事では、かなりジェネリクスに寄せました。そのため、紹介できなかった内容もたくさんあります。

たとえば、core.charcore.asciicore.utf8が入ったことで、文字とバイトとUTF-8の境界はさらに面白くなっています。core.ptrcore.memcore.aligncore.layoutcore.alloccore.errnoのような低レイヤー寄りのコアモジュールも増えています。with unsafeだけでなく、with unsafe + Allocのようなエフェクト合成も見えるようになりました。C生成器やIRの整理、module-owned type名、生成Cの決定的な名前付けもかなり大事です。

このあたりは、来週以降に紹介したいと思います。特に、UTF-8、エフェクト、メモリ、C99出力の話は、ジェネリクスとは別の軸でAneらしさが出てきています。

ただ、その前に一度、zip内のMarkdownファイルのコンテキストを掃除するかもしれません。SPEC.md、README.md、TODO.md、CORE.md、SKILL.mdのような文書が増えてくると、ChatGPT PlusのProjectsに置く文脈としては便利な一方で、重複した説明や古い前提も混ざりやすくなります。次の1週間は、新機能を足すだけでなく、どの文書が何を担当するのかを整理する時間にもなりそうです。

今のところAneは、まだ小さなStage0言語です。ただ、2週間前に比べると、かなり「言語を作っている」という手触りになってきました。コードを書くだけではなく、仕様を書き、拒否する入力を書き、マニフェストを書き、生成Cを確認し、ハードルールを増やす。ChatGPTに丸投げしているというより、人間が文脈と検査を設計し、そのうえでChatGPTと一緒に小さく進めている感覚です。

次に書く記事では、今回紹介できなかった低レイヤーのコアモジュールやエフェクトの話に進むかもしれません。その前に、未来の自分とChatGPTが迷わないように、まずは文書の掃除からやっていきたいと思います。

Discussion