ヿ(koto)ドキュメント

ヿ(koto)言語仕様

この文書の範囲

この文書は、現在の構文、型とメモリ管理の規則、実装上の制限を記述する。実装のために具体化した選択は暫定仕様とし、利用上の問題や改善の根拠に応じて変更する。

実行可能なコアは、S式、スカラー値、固定幅128ビットのベクトル値とマスク、静的文字列、関数と早期脱出、宣言された失敗の伝播と処理、所有者の移動とコピー可能な値の再利用、分岐とループ、名前付きのレコードと和型、モジュール、テスト宣言と検証式を含む。メモリ管理にはregionの所有者、型付き配列、型付きのPtrと読取り用Ref、アクセス権限と借用の静的検査、早期自動解放を使う。

使い方はチュートリアル、起動ははじめに、ホストから使うAPIはホストとAPIを参照する。

言語全体の方針

ヿ製コンパイラは、この文書の実行可能なコアを使い、ブラウザのWasm上で自身を再コンパイルできる。セルフホストの検証では、再生成版にも同じ受理・拒否・実行テストを適用し、繰り返した自己コンパイルで生成バイナリが一致することを確認する。検証の定義はtest/selfhost.js、実行手順は開発にある。

ヿ製コンパイラのソース構成はsrc/compiler-sources.jsのCOMPILER_SOURCESに定義する。共有定義の正本はdefinitionsのヿソースに置き、Wasm上で実行してcompiler/generated-runtime.kotoなどの補助ソースとホスト用のsrc/generated-definitions.jsを生成する。通常起動では検証済みのWasmコンパイラを読み込む。構文解析・検査・コード生成はWasm内で行い、ホストは入出力とWasm実行を担当する。日常の再構築はprovenanceの系譜に記録した固定seedを信頼根とする。JavaScript版からの再生はアーカイブ手順として残す。共有定義の生成、seed更新、信頼モデルと起点の更新方針は開発に従う。

実行可能なコア

ソースと関数

入力は一つのソース文字列である。診断用のソース名やimportからファイルを読み込まない。複数ファイルの内容を使う場合は、ホストが改行で連結して入力する。未対応の構文を黙って無視しない。

丸括弧でS式を構成する。空白でトークンを区切り、;から行末まではコメントとする。識別子にUnicodeや.を利用できるが、空白、丸括弧、;、引用符は含められない。.は名前の一部であり、名前空間の区切りではない。単一引用符とバッククォートによるクォートは提供しない。二重引用符は文字列リテラルを開始する。

組込み構文・演算の名前と、符号を除いて数字から始まるトークンは識別子として使えない。不正なUnicodeは拒否する。名前の検査と読み取り時の入れ子の深さ制限はcompiler/compiler.kotoに定義する。

トップレベルには関数定義、型定義、テスト宣言、モジュール定義、import宣言、export宣言を置く。

(defn square
  (params (x i32))
  (returns i32)
  (i32.mul x x))

(export square)

paramsとreturnsは各関数に一度ずつ必須で、本体より前に置く。節どうしの順序は任意である。各引数には名前と型を指定し、成功時の戻り値の型を一つ指定する。regions、borrows、requiresもそれぞれ一度記述でき、省略時は空とする。回復可能な失敗を返す関数には(fails E)を一度記述し、失敗値の型Eを指定する。省略した関数は失敗を外へ伝播できない。

関数呼出しは(square 7)のように関数名と実引数を並べる。引数の個数と型を検査する。定義順に依存せず呼び出せ、所有・借用・格納に関する制限の範囲で再帰と相互再帰も扱える。関数値やラムダ式は扱わない。

(export square)でルートに直接定義した関数をホストに公開する。importした関数を直接exportすることはできず、ルートの関数から呼び出す。failsを宣言した関数のexportはE_EFFECTで拒否する。ホスト向けの関数内でcatchにより失敗を処理する。ベクトル型・マスク型はWasmのv128で表し、JavaScriptとの境界を越えられない。引数・戻り値の型に、ベクトル型・マスク型またはそれを唯一のフィールドに持つレコードを含む関数(配列要素・ポインタの先を含む)のexportはE_EXPORTで拒否する。ホストからの呼出しは借用検査の対象外なので、引数・戻り値の型にRefまたは借用パラメータを持つ名前付き型を含む関数(ポインタの先を含む)のexportもE_EXPORTで拒否する。レコード・和型のフィールドは借用パラメータを通じてだけRefを持てる。判定はcompiler/types.kotoのty-host-describableにある。重複した関数名・引数名・export、未定義関数のexportは拒否する。memoryと__koto_で始まるexport名はランタイム用に予約する。

モジュールと公開範囲

(module name declaration...)は名前付きモジュールを定義する。モジュール外の宣言は暗黙のルートに属する。モジュールの入れ子と、同名モジュールを複数回定義して内容を追加することは認めない。モジュール内には関数・型・テストの定義と、import・publicを置ける。定義と宣言の解決は記述順に依存しない。

(module pairs
  (public Pair make-pair left)
  (defrecord Pair (fields (left i32) (right i32)))
  (defn make-pair (params (a i32) (b i32)) (returns Pair)
    (record Pair (left a) (right b)))
  (defn left (params (pair Pair)) (returns i32)
    (field-copy pair left)))

(import pairs (Pair make-pair left))
(defn main (params) (returns i32)
  (left (make-pair 42 0)))
(export main)

(public name...)は、そのモジュール自身の宣言を他のモジュールへ公開する。(import module-name (name...))は公開された名前をルートまたはモジュールへ導入する。どちらも一つ以上の名前を必要とし、名前の重複、未定義・非公開の名前、同じ名前空間の既存名との衝突を拒否する。importした名前の再公開、別名、ワイルドカード、修飾名による直接参照は提供しない。ルートにはpublicを置けず、名前付きモジュールからルートの宣言は参照できない。import依存関係の循環を拒否する。

関数と型は別の名前空間に属する。同名の関数と型を定義でき、(public X)と(import module-name (X))は、公開対象に存在する両方を扱う。型の同一性は定義したモジュールと型名によって決まり、同じ綴りの型でも別モジュールの定義とは異なる。

publicに型名だけを書いた場合、その型の表現はモジュール外へ公開しない。値の受渡し・返却・コピーには通常の型・所有権規則を適用する。外部で構築、unpack・match・match-ref、field-copy・field-ref・replace-fieldを行うには、定義側で(public (representation Type))を指定する。この指定は型名と表現を公開する。型が不透明でもコピー可否や所有義務は内部の構成に従い、所有者を含む値を複製・破棄できるようにはならない。

モジュールは同じコンパイル入力内の名前と表現の公開範囲を管理する。ファイル探索、パッケージ管理、個別コンパイル、動的リンクは提供しない。名前解決はcompiler/modules.kotoに実装する。

型と式

スカラー型の定義はdefinitions/language.koto、ベクトル型とマスク型の定義は同じファイルのDefinition-vectorを参照する。暗黙の型変換はない。

型 意味
i32 32ビット符号付き整数
u8 0から255の符号なし整数
f32 IEEE 754の32ビット浮動小数点数
bool 真偽値
unit 情報を持たない値
str 静的なUTF-8文字列への読取り専用ハンドル
i8x16 16個の8ビット整数laneを持つ128ビットのベクトル
i16x8 8個の16ビット整数laneを持つ128ビットのベクトル
i32x4 4個の32ビット整数laneを持つ128ビットのベクトル
f32x4 4個のf32 laneを持つ128ビットのベクトル
m8x16 16 laneのマスク(i8x16の比較結果)
m16x8 8 laneのマスク(i16x8の比較結果)
m32x4 4 laneのマスク(i32x4・f32x4の比較結果)
(Array r T) region r内の固定要素数の配列へのハンドル
(Region r) region rの所有値
(Ptr r T) region r内の初期化済みのTへのポインタ
(Ref b T) 借用期間bに結び付くTへの読取り参照
Name、(Name r… b…) 名前付きのレコードまたは和型

i32リテラルは符号付き十進整数で、範囲は−2³¹以上2³¹−1以下。範囲外のリテラルはコンパイルエラーとする。f32リテラルには小数点または指数を含める。文法は[+-]?[0-9]+(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?であり、1.25、1.、1e3、-0.0を認める。十進値からbinary32へ直接、最近接・偶数丸めを行う。有限値の上限を越えて無限大へ丸められるリテラルはE_RANGEとし、非正規化数と符号付きゼロへのアンダーフローは認める。十六進浮動小数点、inf・nanリテラルは提供しない。

boolリテラルはtrueとfalse。(unit)はunit型の値であり、引数と戻り値にも使える。

u8.from_i32はi32をu8へ変換し、範囲外ならtrapする。i32.from_u8はu8を値を保ったままi32へ変換する。整数リテラルの型はi32であり、u8への暗黙変換や算術演算は提供しない。計算には明示的にi32へ変換する。変換の名前・型・生成先もdefinitions/language.kotoを正本とする。

組込み演算の名前・入力型・結果型・Wasm opcodeはdefinitions/language.kotoに定義する。整数演算はi32.addなどのWasm形式の名前を使い、_sは符号付き、_uは同じi32のビット列を符号なしとして扱う演算を表す。CPUに独立したu32型はない。スカラーの比較とi32.eqzの結果はヿのboolになる。bool.andとbool.orはboolを受け取り、boolを返す。

引数と演算のオペランドは左から右に評価する。論理演算も両オペランドを評価する。整数演算の意味は対応するWasm整数演算に従う。加減乗算は32ビットで折り返し、符号付き除算はゼロ方向へ切り捨てる。除算のゼロ除算・符号付きオーバーフローと、剰余のゼロ除算はtrapになる。非ゼロの符号付き剰余の符号は被除数に従う。ビット演算、シフト、回転、clz・ctz・popcntもWasmと同じ意味を持つ。シフト・回転の量は下位5ビットを使う。clz 0とctz 0は32、popcnt 0は0になる。

f32の算術・比較・丸め・平方根・符号操作は対応するWasm演算を生成する。各演算の結果をbinary32へ丸め、暗黙にi32と混ぜない。ゼロ除算はtrapせず、無限大またはNaNになる。NaNは自身とも等しくなく、大小比較も偽になる。min・maxはNaNを伝播し、±0の組合せではそれぞれ−0・+0を返す。nearestは最近接・偶数丸め、truncはゼロ方向への丸めである。NaNのペイロードの再現性は保証しない。

f32.convert_i32_s/uは符号付き/符号なしの整数を最近接・偶数丸めでf32へ変換する。i32.trunc_f32_s/uはゼロ方向へ切り捨て、NaNや切捨て後の整数が32ビットの符号付き/符号なし範囲に収まらない場合にtrapする。i32.trunc_sat_f32_s/uは同じ切捨ての後で範囲内に飽和させ、NaNを0にする。f32.reinterpret_i32とi32.reinterpret_f32は数値変換せず32ビットの表現を保って型を変える。無限大やNaNが必要な場合は演算やこのビット再解釈で作る。

局所変数は(let name expression)で束縛し、型は初期値から推論する。同じスコープでの名前の重複は拒否するが、内側のスコープでは外側と同名の束縛を作れる。

関数本体と(do ...)は順に評価する式の列で、末尾まで継続した場合は最後の式が全体の結果となる。途中の結果は、未処理の所有者を除いて捨てられる。関数本体には一つ以上の式を必要とし、空の(do)はunitを返す。引数と関数本体の局所束縛は同じスコープに属する。doで導入した名前はその外に出ない。

let、var、new-regionは関数・テスト本体、do、while、unpack、matchの各本体の直下に置く宣言で、宣言を完了した場合の結果はunitになる。

(if (condition expression) ... (else expression))は一つ以上の条件節を持つ。条件はboolで、記述順に一度ずつ評価し、最初に真になった節の式だけを実行する。各節は条件と一つの式からなり、複数式はdoでまとめる。else節は最後に一つだけ書け、すべての条件が偽のときに実行する。分岐後へ継続する各経路の結果型は一致しなければならない。elseを省略した場合、不成立時の結果はunitであり、継続する条件節の結果型にもunitを要求する。各経路の移動・借用・所有義務を検査する。

ベクトルとマスク

ベクトル型とマスク型の値はWasmのv128で表す。i8x16・i16x8・i32x4のlane型はi32、f32x4のlane型はf32とし、以下ではlane数をLと書く。lane 0は線形メモリ上で最も低いアドレスに対応する。構築式の値、ブロックロード・ストアの要素、bitmaskのビット位置も、この順序でlane 0から並ぶ。

ベクトル演算の名前は、先頭にヿの型名を置いたWasm形式の名前とする。例えばf32x4.add、i32x4.lt_s、i16x8.add_sat_s、i32x4.dot_i16x8_sである。_s・_uはスカラーの整数演算と同じく、laneのビット列を符号付き・符号なしとして扱う演算を表す。Wasmで型を区別しない命令は型ごとの名前を持つ。例えばv128.andはi8x16.and・m32x4.andなど、v128.bitselectはi32x4.bitselect・f32x4.selectなど、v128.any_trueはm8x16.anyなどになる。演算とブロックロード・ストアの名前・オペランド型・結果型・Wasm opcodeはdefinitions/language.kotoに定義する。

整数laneの演算は対応するWasm演算に従う。加減乗算はlaneのビット幅で折り返し、名前に_satを含む演算は範囲内に飽和させる。シフト量はi32で与え、laneのビット幅を法とする値を使う。例えば(i8x16.shl v 9)は1ビットのシフトになる。i8x16・i16x8のlaneへi32を入れる構築式・splat・with-lane・replace_laneは、その下位8ビット・16ビットを使い、範囲外の値でもtrapや診断を生じない。ベクトル演算はtrapしない。実行時に範囲を検査するのは、配列を読み書きするブロックロード・ストアだけである。

式 結果 条件
(f32x4 a b c d) lane 0から順に値を並べたベクトル 先頭はi8x16・i16x8・i32x4・f32x4のいずれか。値はlane型でL個
(f32x4.splat x) 全laneがxのベクトル xはlane型。4種のベクトル型にある
(lane v k) lane kの値 vはi32x4またはf32x4
(i8x16.extract_lane_s v k)、(i8x16.extract_lane_u v k) lane kを符号拡張・ゼロ拡張したi32 i16x8にも同じ演算がある
(with-lane v k x) lane kだけをxに置き換えたベクトル vは4種のベクトル型、xはlane型
(shuffle a b k...) lane jが、aのlane 0〜L−1にbのlane(番号L〜2L−1)を続けた列からj番目の添字で選んだlaneであるベクトル aとbは同じ型で、マスクも使える。添字はL個
(swizzle v s) lane jがvのlane s[j]であるi8x16。s[j]を符号なしとして16以上ならlaneは0 vとsはi8x16。sは実行時の値

(lane v k)と(with-lane v k x)は、vの型のextract_lane・replace_lane演算と同じ操作である。i8x16・i16x8のlaneは符号の扱いを明示するため、laneではなくextract_lane_s・extract_lane_uで取り出す。lane番号とshuffleの添字は、対象のベクトルのオペランドの後に書く整数リテラルで、実行時には評価しない。整数リテラル以外(束縛名、小数リテラル、式など)はE_SYNTAX、i32の範囲外のリテラルはE_RANGE、0以上L未満でないlane番号と0以上2L未満でないshuffleの添字はE_TYPEとする。構築式の値とshuffleの添字の個数がLと異なる場合はE_ARITYとする。

比較はマスクを返す。i8x16の比較はm8x16、i16x8の比較はm16x8、i32x4とf32x4の比較はm32x4になる。マスクのlaneは、真ならすべてのビットが1、偽ならすべてのビットが0であり、マスクを作る演算はこの表現を保つ。マスクMの演算は、laneごとの論理演算M.and・M.or・M.xor・M.andnot(aかつbでない)・M.notと、いずれかのlaneが真ならtrueを返すM.any、すべてのlaneが真ならtrueを返すM.all、lane jをビットjに置いたi32を返すM.bitmaskである。(V.select a b m)は、laneごとにmが真ならa、偽ならbの値を選ぶ。mは最後のオペランドで、Vと同じlane数のマスク型とする。

マスクと整数ベクトルの間には暗黙の変換も、明示的な変換演算もない。boolとi32を区別するのと同じく、真偽の表現を数値として扱わないためである。マスクから整数ベクトルを作るには(i32x4.select ones zeros m)のようにselectを使い、整数ベクトルからマスクを作るには比較を使う。マスクには構築式・splat・lane・with-laneはなく、shuffleは使える。ifの条件とassertには、M.any・M.allで得たboolを使う。

f32x4の各laneは、スカラーのf32と同じ規則に従う。演算ごとにbinary32へ丸め、ゼロ除算はtrapせず、NaNのペイロードの再現性は保証しない。i32x4.trunc_sat_f32x4_s/uはゼロ方向へ切り捨てた後で範囲内に飽和させ、NaNを0にする。trapするベクトルの切捨て変換はない。

f32x4.min・maxは、スカラーのf32.min・maxと同じくNaNを伝播し、−0を+0より小さい値として扱う。(f32x4.pmin a b)はlaneごとにb < aならb、それ以外はaを返し、(f32x4.pmax a b)はa < bならb、それ以外はaを返す。比較が偽になるNaNを含むlaneと、−0と+0の組では第1オペランドaを返す。min・maxはx86では複数の命令、ARM64では1命令になり、pmin・pmaxはx86で1命令になる。NaNと符号付きゼロの扱いが不要な計算ではpmin・pmaxを選べる。

ベクトル型とマスク型の値はコピー可能で、局所変数・引数・戻り値・var、レコードと和型のフィールド、allocしたセル、分岐やcatchの結果、失敗値に使える。フィールドとしての大きさは生成物、ランタイム、ホスト境界、exportの規則はソースと関数を参照する。配列の要素にはできない。

全laneの和のような水平方向の集計を行う組込み演算はない。shuffleでlaneを並べ替えて同じ演算を重ねるか、取り出したlaneをスカラーで計算する。

relaxed命令

名前にrelaxed_を含む演算は、WasmのRelaxed SIMD命令を生成する。結果はホストに依存し、GPUのf32と同じく環境間の再現性を保証しない。各laneの結果は次の表の許される結果のいずれかになる。実行環境は演算ごとに許される結果の集合を定め、その集合は実行中に変わらない。ただし、同じ入力で繰り返し呼び出したときに同じ結果を返すことは保証しない。表の内容はWebAssembly仕様の数値規則に従う。

演算 オペランド → 結果 許される結果(laneごと)
(i8x16.relaxed_swizzle a s) i8x16 i8x16 → i8x16 s[j]を符号なしとして、16未満ならaのlane s[j]、16以上128未満なら0またはaのlane s[j] mod 16、128以上なら0
(i32x4.relaxed_trunc_f32x4_s a) f32x4 → i32x4 範囲内のlane(有限で、ゼロ方向に切り捨てた値が−2³¹以上2³¹未満)は切り捨てた値。NaNと範囲外のlaneはホストに依存し、trunc_satの結果またはホストが選ぶ別の値になる。保証するのは範囲内のlaneだけである
(i32x4.relaxed_trunc_f32x4_u a) f32x4 → i32x4 範囲内のlane(有限で、ゼロ方向に切り捨てた値が0以上2³²未満)は切り捨てた値をi32のビット列にしたもの。NaNと範囲外のlaneはホストに依存し、trunc_satの結果またはホストが選ぶ別の値になる。保証するのは範囲内のlaneだけである
(f32x4.relaxed_madd a b c) f32x4 f32x4 f32x4 → f32x4 a·b + cを、積と和の後でそれぞれ丸めた値、または一度だけ丸めた値(融合積和)
(f32x4.relaxed_nmadd a b c) f32x4 f32x4 f32x4 → f32x4 −(a·b) + cを、二度丸めた値または一度だけ丸めた値
(V.relaxed_laneselect a b m)(Vはi8x16・i16x8・i32x4・f32x4) V V M → V マスクのlaneが真(すべてのビットが1)ならa、偽(すべてのビットが0)ならbのlane。Wasmではビットが混在するlaneの結果がホストに依存するが、ヿのマスクのlaneは常に一様であり、ホストが書き込むマスクも一様でなければならないため、結果は(V.select a b m)と等しい
(f32x4.relaxed_min a b)、(f32x4.relaxed_max a b) f32x4 f32x4 → f32x4 一方のlaneだけがNaNならNaNまたは他方のlane、両方がNaNならNaN、−0と+0の組(順序は問わない)ならaまたはbのlane、それ以外は通常の最小値・最大値
(i16x8.relaxed_q15mulr_s a b) i16x8 i16x8 → i16x8 両方のlaneが−2¹⁵なら−2¹⁵または2¹⁵−1、それ以外は(a·b + 2¹⁴) >> 15
(i16x8.relaxed_dot_i8x16_i7x16_s a b) i8x16 i8x16 → i16x8 lane jはa[2j]·b[2j] + a[2j+1]·b[2j+1]で、aのlaneは符号付きとする。ホストは二つの方式の一方を固定して使う。bのlaneを符号付きとして組の和を16ビットで折り返す方式と、bのlaneを符号なしとして組の和を符号付き16ビットの範囲に飽和させる方式である。bのlaneがすべて0以上127以下なら、両方式の結果は一致する
(i32x4.relaxed_dot_i8x16_i7x16_add_s a b c) i8x16 i8x16 i32x4 → i32x4 (i16x8.relaxed_dot_i8x16_i7x16_s a b)(同じ方式)の隣り合う2個のlaneを符号拡張して足し(i32x4.extadd_pairwise_i16x8_sと同じ)、cのlaneをi32の折り返しで加えた値

relaxed命令を生成したモジュールは、コンパイル結果に必要なWasm機能relaxed-simdを記録する。ホストは実行前にその機能への対応を確かめ、対応しない環境では明示的に失敗する。relaxed命令を通常の命令へ置き換えて実行することはない。記録の形はホストとAPIのコンパイラAPI、ホストでの失敗の形はWasmとホストの境界を参照する。

関数からの脱出と宣言された失敗

(return expression)は式を一度評価し、現在の関数を成功で終了する。値の型はreturnsと一致させる。do、分岐、ループの条件や本体の中でも関数全体から抜ける。関数本体の末尾まで継続した場合も、その末尾の値を成功結果として返す。

failsの失敗型には通常の型を指定する。値を必要としない失敗にはunitを使う。成功結果をラッパー型へ包む必要はない。

式 振る舞い
(fail expression) failsで宣言した型の値を返し、現在の関数を失敗で終了する。
(try call) 呼出しが成功したら通常の結果を返し、失敗したらその値を現在の関数から伝播する。
(catch call (name handler)) 成功時は呼出しの結果を返す。失敗時だけnameへ失敗値を束縛してhandlerを評価する。

tryとcatchの対象はfailsを宣言した関数の呼出しであり、通常の呼出し構文とregion引数を明示するcallを使える。失敗可能な呼出しには必ずどちらかを付ける。tryでは、region・借用パラメータを実引数へ代入した失敗型が、呼出し元のfailsと一致しなければならない。異なる失敗へ変換する場合はcatchしてfailする。

nameはhandler内だけの束縛で、通常の移動・コピー・所有義務の規則を適用する。継続するhandlerの結果型は呼出しの成功型と一致させる。複数の処理はdoでまとめ、returnやfailで関数から抜けるhandlerも書ける。呼出しの引数を評価する途中のtryは、その場の関数から失敗を伝播する。まだ成立していない呼出しに付けたcatchへは入らない。宣言・伝播・呼出し処理の不整合はE_EFFECTとする。

検査器は、その式から通常の評価へ戻らない状態をNoContinueとして扱う。これはソースに書ける型ではない。returnとfail、またはすべての経路で脱出する式は、外側の結果型や継続地点の状態の合流に値を提供しない。tryの成功経路と、脱出しない分岐・handlerだけが後続処理へ継続する。

検査済みの呼出し先に成功経路がなければ、その呼出しの成功側も継続しない。検査済みの呼出し先に失敗経路がなければ、その呼出しへのtryは失敗を伝播しない。再帰呼出しで検査が完了していない場合は、成功と宣言された失敗の両方を可能とみなす。

NoContinueの後に評価されるソース式はE_UNREACHABLEで拒否する。式列の後続、引数やフィールドの後続、無条件に脱出する条件の後の分岐・ループ本体、無条件に脱出する入力の後のmatch・unpack本体も対象となる。最後のオペランドが脱出する場合は、評価済みの前半を保ち、親の操作を行わずに脱出する。例えば(i32.add true (return 42))と本体のない(while (return 42))は現在の関数から42を返す。

親操作が要求する値の型や、割当・更新・移動など親操作の効果は、実行時オペランドがすべて継続した場合にだけ検査・確定する。上の加算ではtrueを評価するが加算には到達しないため、整数型を要求しない。引数の個数、名前の解決、レコードのフィールド集合、region名などの構文上の条件と、先行オペランド自身の型・効果は常に検査する。

宣言された失敗はtrapを捕捉しない。メモリ枯渇、範囲外アクセス、スタック枯渇などの実行時trap、算術のtrap、assertの失敗はcatchで回復できない。

テストと検証

(deftest "name" body...)は名前付きのテストを宣言する。名前は空でない文字列で、同じソース内では重複できない。文字列のエスケープを解釈した値で比較する。本体は一つ以上の式からなり、引数・regionパラメータ・借用パラメータ・failsを持たず、結果型はunitとする。失敗可能な呼出しはcatchで処理し、テスト本体からのfail・tryはE_EFFECTで拒否する。通常の関数本体と同じ束縛・移動・権限・借用・自動解放の規則を適用し、ローカルregionも作成できる。テスト名は関数名を宣言しないため、ソースからの呼出しやexportの対象にはならない。

(assert condition)はboolの式を一度だけ評価する。真ならunitを返し、偽なら検証位置を記録してtrapする。通常の関数でも使用でき、コンパイルモードによらず検証する。条件式を評価する途中のtrapは、条件が偽だったことによる検証失敗とは区別する。

(expect-trap expression)はdeftest本体の最後の直下の式としてだけ記述できる。対象の式を一度実行し、その評価中のtrapを期待する。実行時trapの表のどの原因(スタック枯渇を含む)とネイティブtrapも、このtrapに当たる。先行する準備処理のtrapは成功としない。対象内でassertが失敗した場合もテスト失敗とする。対象が正常終了すると、返り値を捨ててunitを返すため、未処理の所有者を返す式は通常の破棄規則により拒否する。対象も通常どおり静的に検査し、コンパイルエラーをtrapとして扱わない。

コンパイルモード"program"と"test"の両方でテスト本体を検査する。"program"は通常の関数を生成し、テスト本体を生成物から除く。"test"はテストの実行用エントリと、assertの位置とexpect-trapの対象を記録する診断状態のexportも生成する。通常の関数に含まれるassertと実行時検査の位置情報は両モードで返し、テスト本体の位置情報は"test"だけで返す。

実行器は各テストを新しいWasmインスタンスで実行し、trap後は診断状態、__koto_trap_codeと__koto_trap_siteを読むだけで、そのインスタンスの処理を再開しない。expect-trapは例外から回復する言語機能ではない。コンパイル、Wasm検証、インスタンス作成のエラーを期待したtrapとして扱わない。結果にはassertの失敗以外のtrapの原因を実行時trapの名前と文言で付ける。実行結果とソース位置のAPIはホストとAPI、判定の実装はsrc/testing.jsとsrc/traps.jsを参照する。

値の再利用、所有者の移動、可変束縛

コピー可能な値を使用しても、その束縛は消費しない。所有者を含む値の使用は移動となる。 コピー可否はcompiler/types.kotoのty-copyableが定め、値のワード数やABIには依存しない。

i32、u8、f32、bool、unit、str、ベクトル型、マスク型、Array、Ptr、Refはコピー可能で、捨てても所有義務を生じない。レコードは全フィールド、和型はすべてのケースの全フィールドがコピー可能な場合にコピー可能となる。コピー可能な値は、関数の引数、構築するフィールド、分岐・ループ、unpack・matchでも繰り返し使える。

ハンドルと参照の複製は同じ格納先を指し、内容を複製しない。参照は元の借用制約を保ち、使われない参照を束縛に保持するだけでは借用を延長しない。Regionとそれを含む値はコピーできない。移動済みの所有値を再使用するとE_MOVEDになり、未処理の所有者を含む値を単なる式結果の破棄で消すこともできない。

; 断片: 関数本体の式。xは整数、ownerは所有者の束縛とする。
; 整数は何度でも使える。
(i32.add x x)

; 所有者は移動先で扱う。
(let next owner)
(region-free next)

分岐内の移動は各経路について独立に検査する。外側の所有値については、継続する経路間で保持・移動の状態が一致しなければならない。移動した所有値は分岐後に再使用できない。catchの成功経路とhandlerにも同じ規則を適用する。条件がリテラルでも同じ検査を行う。

(var name expression)は再代入可能な束縛を作る。型は初期値から推論し、i32、u8、f32、bool、unit、str、ベクトル型、マスク型に限定する。(set name expression)はvarの束縛に同じ型の値を入れ、unitを返す。例えば(set i (i32.add i 1))は現在の値から計算して代入する。letや関数引数への再代入は認めない。

ループ

(while condition body...)は、boolの条件が真の間、本体の式列を繰り返す。本体は独立したスコープであり、正常終了する場合の結果型はunitを要求する。通常のwhileもunitを返す。breakとcontinueは提供しない。

入口で利用可能な外側の所有値は、条件評価後にも利用可能でなければならない。条件評価後に利用可能な外側の所有値は、本体の正常終了時にも利用可能でなければならない。違反はE_LOOP_MOVEとする。ループ後の利用可能性は条件評価後の状態とし、本体が一度も実行されない場合を含めて検査する。コピー可能な値の使用は、束縛の利用可能性を変えない。

本体内で生成したregionは、各反復内の安全な位置で自動解放する。外側の所有者を失ったまま次の反復へ進む操作は認めない。条件や本体からreturn・fail・tryの失敗で関数を抜ける経路には、ループへの復元を要求しない。

静的文字列

二重引用符で囲んだ文字列リテラルはstr型であり、UTF-8バイト列として静的データに配置する。エスケープは\\、\"、\n、\r、\tを扱い、それ以外のエスケープを拒否する。

(string-length text)はUTF-8のバイト数をi32で返す。(string-get text index)は0始まりの位置にあるバイトをi32で返し、範囲外ならtrapする。文字単位の索引、動的文字列の構築・連結、文字列の更新は提供しない。

strはコピー可能であり、同じ束縛を繰り返し使える。静的文字列の寿命はWasmインスタンスの寿命であり、regionの操作権限や解放を必要としない。

regionの所有と解放

(new-region (r arena))は、新鮮な静的region名rと所有者arena : (Region r)を導入する。region名は関数内で再宣言できず、スコープ外では使えない。regionの実際の寿命と静的な名前の有効範囲は区別する。

所有者は通常の値として移動・束縛・関数への受渡し・返却ができる。利用可能な所有者を持つ間、そのregionのread・write・allocを利用できる。所有者を移動すると元の束縛は権限の根拠にならない。

(region-free owner)は所有者を消費してregionを解放し、unitを返す。解放順序は作成順の逆に限定しない。Aを作成し、Bを作成し、Aを解放してからBを使い続けられる。

未移動の所有者は、型・所有義務・借用の検査後に求めた安全な位置で自動解放する。regionへのアクセス、参照の利用、所有者の移動・明示解放に必要な期間を保ち、その後の最初の配置可能な位置に解放を置く。関数が受け取った所有者にも同じ規則を適用する。

return・fail・tryによる脱出にも、その経路の自動解放を適用する。返す値を確定し、呼出し元へ渡す所有者や参照を保護してから、残った所有者を解放する。返す参照と所有者の解放が競合すればE_BORROWで拒否する。引数やレコードのフィールドを評価する途中で脱出した場合、既に評価済みでも呼出しや構築へ渡し終えていない単独のRegionは回収する。完成済みの所有者入りレコード・和型には暗黙のデストラクタを適用せず、脱出経路でも明示的な分解・移動を要求する。

解放点の選択はcompiler/lifetimes.kotoに定義する。分岐は経路ごとに扱い、ループの外側のregionは後続反復から必要になる期間を保つ。matchとunpackの入力のために必要なregionは、その式の完了まで保持する。関数へ貸した権限は呼出しが終了するまで保持する。すべての命令間で数学的に最短の寿命を求める保証ではない。trap時の後処理は保証しない。

ローカルregion名を含む値は、複合値や参照に包んでも、そのregionを宣言したスコープの外へ返せない。成功結果と失敗値の両方にこの規則を適用する。関数がregion内に結果を作って返す場合は、呼出し元が用意したregionを使う。一般の存在型や、新しいregion名を隠して所有者とハンドルを返すパッケージは提供しない。

region解放後も配列やPtrのハンドル自体は保持できる。ただし対応する権限を失うため、そのハンドルからの借用・読取り・更新・要素数取得は拒否する。別のregionの権限や、同じメモリ位置の再利用によって、古いハンドルへのアクセスが静的に有効になることはない。

配列

Arrayは作成後の要素数が固定で、要素にはi32・u8・f32・bool・str・Ptr・Arrayを使える。許可する種別はdefinitions/language.kotoのdef-array-element-*で定義する。以下のTはこの集合の借用依存を持たない型を表し、一般の型パラメータではない。Ptr・Arrayの内側もたどり、Refや借用パラメータを持つ名前付き型を含む要素は拒否する。unit・region所有者・ベクトル型・マスク型・レコード・和型そのものは要素にできない。配列ハンドルは独立した所有者ではなく、格納先regionの型情報を持つ。配列の個別解放は提供しない。

操作 入力 結果 必要な権限
array-new region名r、要素数i32、初期値T (Array r T) alloc r
array-length (Array r T) 要素数i32 read r
array-get (Array r T)、添字i32 要素のT read r
array-set (Array r T)、添字i32、新しいT unit write r

array-newは初期値の型から要素型を決め、すべての要素を初期化してからハンドルを返す。関数の引数・戻り値・代入では要素型まで一致させる。u8配列の要素は1バイト、それ以外の配列要素は1ワードずつ連続して配置する。f32要素はbinary32を4バイトで格納する。レコードのフィールドとPtr r u8の格納先でのu8は1ワードを使う。初期化にはallocだけを必要とし、既存の配列の変更にはwriteを必要とする。各入力値は通常のコピー・移動規則に従う。region名は値ではなく、Region rの所有者を渡して消費する引数ではない。

array-getは要素型の値をコピーして取得し、要素を未初期化にしない。持続する内部参照も作らない。array-setは同じ型の値で要素を置き換え、初期化済み状態を保つ。ポインタ・配列のコピーはハンドルだけを複製し、指す内容を複製しない。

配列から取り出したPtrは個別セルの識別を保持しない。コピー可能で借用依存のない内容は通常どおり借用・読取り・更新できるが、セルの所有者や格納された参照の追跡が必要な操作はE_STORAGEになる。配列を経由しても、既存セルの所有義務は失われない。外側の配列を読む権限と、取り出したポインタや内側の配列にアクセスする権限はそれぞれのregionに要求する。

Ptrにnull値はないため、ポインタ配列の初期化には有効なセルを渡す。不在を表す索引表では、番兵セルを初期値にし、不在かどうかは別の添字や件数で管理できる。型の内部に含むregionも脱出検査の対象となる。

負数の要素数、割当サイズの上限超過、確保失敗はtrapする。添字は0以上かつ要素数未満を要求し、配列の要素境界を実行時に検査する。ランタイムの上限と割当検査はdefinitions/runtime.kotoを定義とする。

ブロックロードは配列の添字iから連続するk個の要素をベクトルとして読み、ブロックストアはベクトルのlaneを連続する要素へ書く。lane jは要素i + jに対応する。すべてのオペランドを評価した後に0 ≤ iかつi + k ≤ 要素数を一度だけ検査し、満たさなければtrapする。ブロックロード・ストアは整列を要求せず、配列の要素配置はスカラーの操作と同じである。ロードにはread r、ストアにはwrite rを必要とする。

操作 入力 結果 k
i8x16.load (Array r u8)、添字i32 i8x16 16
i8x16.store (Array r u8)、添字i32、i8x16 unit 16
i32x4.load (Array r i32)、添字i32 i32x4 4
i32x4.store (Array r i32)、添字i32、i32x4 unit 4
f32x4.load (Array r f32)、添字i32 f32x4 4
f32x4.store (Array r f32)、添字i32、f32x4 unit 4

load_splatは要素iを全laneに置き、load_zeroは要素iをlane 0に置いて他のlaneを0にする。どちらもkは1である。i16x8.load_extend_s・i16x8.load_extend_uはu8配列の8要素を読み、符号拡張・ゼロ拡張して各laneに置く(kは8)。(V.load_lane array i v n)はvのlane nだけを要素iに置き換えたベクトルを返し、(V.store_lane array i v n)はvのlane nを要素iへ書く。lane番号nは整数リテラルで、kは1である。各操作がある型と配列の要素型の完全な一覧は、definitions/language.kotoのdef-vector-memory-*を正本とする。

関数のregionと権限契約

regionsは既存regionのパラメータを導入し、requiresはそのregionの操作権限を要求する。

(defn singleton
  (regions output)
  (params (value i32))
  (returns (Array output i32))
  (requires (alloc output))
  (array-new output 1 value))

通常の呼出しでは、引数の型から、ネストしたPtr・Refや名前付き型の引数も含めてregion引数を推論する。推論できないregionがある場合や明示したい場合は、(call singleton (regions r) 42)のように、宣言順にregion引数を与える。引数間の対応が矛盾する呼出しを拒否する。

操作権限は、既存値への権限なし ≤ read ≤ writeと、新規割当の権限なし ≤ allocの積とする。権限なし、read、write、alloc、read + alloc、write + allocの六つの要素を持つ。writeは読取りを含むが、allocを含まない。包含・結び・交わりの実行可能な定義はsrc/capabilities.jsに置く。

配列・セルの操作と関数呼出しで必要な権限を静的に検査する。呼出し元は利用可能な所有者、または自身のrequiresにより要求を満たす。呼出し先への操作権限の貸出しは同期呼出しの間に限定され、配列やPtrのハンドルを返すこと自体は貸出し期間を延長しない。返されたRefと、関数が格納した参照の借用は、その後の利用まで継続する。

所有・解放は操作権限の束の外にある。(owner (Region r))を引数として受け取る関数には、解放責任も移動する。同じregionに複数の所有者引数を宣言すること、所有者引数とそのregionのrequiresを併記すること、同じ呼出しで所有権を渡しながらそのregionの権限を貸すことを拒否する。

同じregionを複数のregionパラメータへ渡す呼出しも受理するため、関数内の借用検査はregionパラメータ同士が同じregionを指しうるものとして扱う。regionパラメータを通じた更新・割当・解放は、その関数内で作った借用のうち他のregionパラメータに依存するものとも衝突し、E_BORROWで拒否する。所有者引数で受け取ったregionは、前段の規則と関数の借用契約により関数が読み書き・借用できる他のregionと一致しないので、new-regionで作ったregionと同じく他のregionと区別する。借用パラメータで受け取った参照は、関数の借用契約のとおり呼出し元が検査する。

権限は操作を許可する契約であり、実際の割当の有無・量・時間や関数全体の無割当を保証しない。

レコードと和型

defrecordは名前付きフィールドのレコード、defenumは閉じたケース集合のタグ付き和型を定義する。同じフィールドを持っていても、異なる定義の名前は異なる型である。型の構文と配置の定義はcompiler/types.kotoに置く。

(defrecord Pair (fields (left i32) (right i32)))
(defenum Result
  (case Missing)
  (case Found (value Pair)))
(defrecord Link (regions r) (fields (target (Ptr r Pair))))
(defrecord View (borrows b) (fields (value (Ref b Pair))))

型定義は省略可能なregionsとborrowsを持ち、それぞれregion名と借用名を抽象化できる。型の使用時には(Link r)、(View b)のように、region引数の後に借用引数を宣言順で指定する。一般の値の型をパラメータにする機能はない。型定義の順序によらず参照でき、PtrやRefを介した再帰を認める。値を直接内包する循環は有限サイズを持たないため拒否する。

(record Pair (left 17) (right 25))と(variant Result Found (value pair))で構築する。各フィールドを過不足なく一度だけ指定する。指定順は任意で、初期化式はソースに記述した順に評価する。コピー可能な値はフィールドへコピーし、所有者を含む値は所有義務とともに移動する。構築した値Tと、それをregionへ配置した(Ptr r T)は別の型である。

(unpack value (left right) body...)は宣言順のフィールドを新しい束縛へ取り出す。コピー可能なレコードの元の束縛は引き続き利用でき、所有者を含むレコードは消費する。束縛の名前はフィールド名と同じでなくてもよい。本体の最後の式が結果となる。

(field-copy binding field-name)は、利用可能なレコード束縛の指定フィールドだけをコピーする。第1引数は関数引数または局所束縛の名前とし、任意の式を受け取らない。元の束縛と全フィールドは利用可能なまま残る。選択したフィールドにはコピー可能な型を要求するが、他のフィールドにRegionなどのコピー不可な値を含んでもよい。未知のフィールド、レコード以外、移動済みの束縛は拒否する。

field-copyは値のフィールドを取得するため、regionのアクセス権限を追加で要求しない。選択したRefの借用への依存と、Ptr・Refの格納先の同一性を保存する。選択していない参照フィールドの利用とは扱わない。取得したポインタを辿る場合は、通常どおり対象regionの権限を必要とする。定義したモジュールの外では、レコードの表現が公開されていることを要求する。

; 断片: 式の例。resultはResult型の値。
(match result
  (Missing () 0)
  (Found (pair) (unpack pair (left right) (i32.add left right))))

matchは和型を受け取り、選択されたケースのフィールドを束縛する。コピー可能な和型の元の束縛は引き続き利用でき、所有者を含む和型は消費する。すべてのケースをちょうど一度ずつ列挙し、各パターンに宣言順で全フィールドの束縛名を指定する。後続へ継続する各分岐の結果型は同一とする。網羅性やケース名の誤りはE_EXHAUSTIVEになる。Ptrの暗黙の逆参照は行わない。

match-refは(Ref b Enum)を受け取り、ケースの各フィールドを(Ref b FieldType)として束縛する。参照先の所有権は移動しない。ケースを表すタグとペイロードへの参照は、同じ読取り借用で保護する。

ポインタ、参照、借用期間

(Ptr r T)はregion r内の、型・格納場所・初期化済み状態を保つセルを指す。ポインタ自体はアクセス権限もregionの生存保証も持たない。(Ref b T)は借用期間bに結び付く読取り参照であり、Tがコピー不可でも作成できる。bはregion名とは異なる種類の静的パラメータで、検査器はその借用が依存するregionを追跡する。一つの借用が複数のregionに依存する契約も表せる。

式 結果 必要な権限・条件
(alloc r value) (Ptr r T) alloc r、value : T
(borrow pointer) 推論した期間の(Ref b T) read r、pointer : (Ptr r T)
(borrow b pointer) 指定した期間の(Ref b T) 上記と、bのorigin契約にrを含むこと
(read-copy reference) T reference : (Ref b T)、Tはコピー可能
(field-ref reference field) (Ref b FieldType) レコードへの参照が持つ証明
(replace pointer value) 古いT write r、新旧の型が同一
(replace-field pointer field value) 古いFieldType write r、レコードの同じフィールド型

borrowはポインタから読取り参照を作り、field-refは同じ借用期間の内部参照を返す。入力のポインタや参照は引き続き利用でき、参照先の値の所有権は移動しない。既存のRefの読取りには、その参照が持つ証明を使い、追加のrequires (read r)を必要としない。

read-copyは参照先の値をコピーする。参照を変数名で使った場合は、同じ格納先を指す参照をコピーする。参照先に格納されたPtrをコピーした後、そのポインタから新しく借用するには対象regionの権限が必要である。参照先が別のRefなら、コピー結果は内側の参照が持つ借用への依存を保つが、コピー元の格納スロットへの借用は引き継がない。

参照の複製・派生・保存・関数への受渡し・返却を通じて借用への依存を維持する。最後の利用まで、同じregionの既存値への更新、同じregionへの割当、そのregionの所有者のセルへの格納、regionの解放を拒否する。割当はregionの領域を倍の容量の領域へコピーして付け替えることがあり、参照は借用時の絶対アドレスを持つため、割当も対象に含める。セルへ格納した所有者は、そのセルを書き換えられる呼出し先が取り出してregionへ割り当てうるため、格納も対象に含める。別フィールド・別オブジェクトへの更新も対象となり、同じregionを指しうるregionパラメータは関数のregionと権限契約のとおり扱う。使われない参照を保持しているだけで借用を延長することはない。分岐ごとの利用とループの後続反復も検査し、実行時の参照カウントは使わない。検査の定義はcompiler/loans.kotoにある。

関数の借用契約

関数はborrowsで参照の借用期間を宣言する。入力の参照を読む・派生する関数では、regionを指定しない借用パラメータを使える。

; 断片: Pairは「レコードと和型」の例のレコード型。
(defn inspect (borrows b)
  (params (value (Ref b Pair))) (returns i32)
  (read-copy (field-ref value left)))

権限から新しい参照を作って返す場合は、(borrows (b r))のように依存するregionを明示する。複数の依存先には(borrows (b first second))を使う。

; 断片: Pairは「レコードと和型」の例のレコード型。
(defn left-view (regions r) (borrows (b r))
  (params (pointer (Ptr r Pair))) (returns (Ref b i32))
  (requires (read r))
  (field-ref (borrow b pointer) left))

呼出し時には入力型から借用の対応を推論する。入力に現れない借用には、宣言されたoriginを使って呼出しごとの借用を作る。originも入力からの対応もない借用パラメータは推論できない。借用名に実行時の値はない。

関数が要求するwriteとalloc、所有者の受取りによる解放可能性、関数内で保存した参照や移動した所有義務を、呼出し元でも検査する。allocを要求する呼出しは、実際に割り当てるかどうかによらず、そのregionへの割当として扱う。所有者と、そのregionに依存する利用中の参照を同じ呼出しへ渡すことは認めない。権限を渡すだけの呼出しと、所有者を移動する呼出しを区別する。

呼出しへ渡した参照の内側や、渡したread・write権限で辿れるポインタの先に参照を含む場合、その借用依存も呼出しまで保持する。単にポインタを受け渡すことと、その内容へアクセスする権限を渡すことを区別する。

ポインタ引数を先に評価した場合、呼出しで読む内容の借用依存は後続引数の評価中も保持する。後続引数で参照先を更新し、セルを新しい参照へ置き換えてから呼び出す処理は、保守的に拒否する場合がある。そのような更新と置換は、呼出しの前に完了させる。

複合値と格納先の所有義務

通常のデータはregionの一括解放で回収し、オブジェクトごとの暗黙のデストラクタを設けない。レコードや和型がRegionを含む場合、その所有義務を単に忘れたり上書きしたりして消してはならない。unpackやmatchで取り出し、別の値へ移すか解放する。取り出して単独の束縛になったRegionには通常の自動解放を適用する。

allocは値と所有義務を新しいセルへ移す。replaceはセルを同じ型の新しい値で置き換え、古い値を所有義務とともに返す。replace-fieldは指定したレコードのフィールドだけを置き換える。いずれも初期化済み状態を保ち、機械命令としての原子的交換は要求しない。

例えば、所有者を含むケースOwnedを所有者のないケースEmptyへ置き換えると、セルにあった義務は返り値へ移る。返り値を処理した上で、格納先のregionを解放できる。所有者が格納されたままのregionを解放するとE_STORAGEになる。同じセルを指すPtrのコピーは、同じ所有義務を参照する。

関数を通るセルの更新についても、格納された参照と所有義務の変化を追跡する。成功結果と失敗値、それぞれの経路でのregion解放・セル更新・借用依存を分けて保持する。tryの後続には成功経路を適用し、catchの後続には実際に継続する成功経路とhandlerの状態を合流する。失敗時だけ解放したregionを、成功時に返した所有者から取り上げることはない。

所有者・参照を含むセルの識別が確定しない更新、同じセルを指す可能性のある複数のポインタ引数へのそのような更新はE_STORAGEで拒否する。ループは次の反復へ進む経路で、格納された所有義務と借用の状態を復元する必要がある。条件や本体で外側のregionへ新しいセルを割り当てた場合も、次の反復に新しい所有義務や借用を残してはならない。各反復内で取り出して処理したセルや、整数など義務を持たない値の割当は扱える。

再帰呼出しの制限

再帰・相互再帰でも、Refとそれを含むレコード・和型を引数、成功結果、失敗値として扱える。借用の対応は通常の呼出しと同じく署名から決まり、返された値には対応する借用依存が残る。入れ子のRefからread-copyで取り出した参照にも、その内側の借用依存が残る。呼出し先の解析が未完成の場合、署名から求めた依存を使い、セルの同一性は仮定しない。

値として渡す引数・成功結果・失敗値にregion所有義務を含む再帰はE_RECURSIONで拒否する。レコード・和型で所有者を包む場合も含む。Refの参照先に所有者を含むことは、所有権の受渡しに当たらない。その参照からコピー可能なフィールドを読むことや、参照を返すことはできる。呼出し元のregionを解放する再帰は、現在の解析では扱わない。各呼出し内で作成・解放する作業用regionは扱える。

外部セルへ所有義務・借用依存を新たに格納する効果もE_RECURSIONになる。所有者・参照を含むセルがあるregionへのwriteを再帰へ渡す場合は、更新内容を確定できないため保守的に拒否する。引数のポインタが指すセルを変更せず読む場合は扱える。再帰で必要な解析が完了した時点でも、成功・失敗の両方について外部セルへの格納効果とregionの解放を検査する。

生成物、ランタイム、ホスト境界

生成物はimportを持たないWasmバイナリであり、指定した関数、一つの線形メモリmemory、ホスト用のランタイム操作と、trapの原因を示すglobal __koto_trap_codeと位置を示すglobal __koto_trap_site(実行時trap)をexportする。"test"モードではテストのエントリと診断状態もexportする。regionアロケータもWasmに含める。ランタイムの配置とWasmバイトコードを組み立てる実行可能な定義はdefinitions/runtime.koto、テスト実行器との共有定義はdefinitions/abi.kotoに置く。ランタイムを組み込むモジュールの生成はヿ製コンパイラが担当する。

region所有者は、regionのバイト列の外に置く記述子表の記述子を、索引と世代で指すハンドルとして表す。解放した記述子は世代を進めて再利用するため、解放済みのハンドルが後のregionを指すことはない。ArrayとPtrは、そのregionのbaseからのバイトオフセットとして表す。regionの領域は容量が2の冪のbump領域で、最初の確保でそれを収める容量を取る。足りなくなると、倍の容量の領域(倍でも収まらない確保では、それを収める2の冪の容量の領域)へ使用中の範囲をコピーしてbaseを付け替える。領域の容量は最大2^30バイトだが、線形メモリのアドレスの上限が先に効くため、倍々に成長したregionはそれより小さい容量でもメモリ枯渇になりうる。解放したregionの領域と成長で空いた旧領域は、容量ごとの空きリストから同じ容量の次の確保へ渡す。線形メモリが足りなければ、不足量と現在の大きさの1/8の大きい方だけ伸ばす。呼出しの作業領域のチャンクも同じ領域から取る。配置と上限の定義はdefinitions/runtime.kotoにある。実行時間が一定であるとは保証しない。

静的文字列とランタイムの管理情報(記述子表と空きリストの表)は通常の配列要素操作で変更できない。初期メモリ容量は静的データを収容できる容量を用意し、少なくとも1 Wasmページとする。解放しても線形メモリそのものは縮小しない。

ホストへ公開する関数のWasm引数は、regionsの宣言順にregionハンドル、その後にunitを除いた通常の引数を並べる。i32・u8・boolはWasmのi32で表し、boolは0または1とする。u8の引数は関数入口でそのWasmのi32の値を検査し、0から255の範囲外ならT_U8_RANGEでtrapする。配列・ポインタはregionのbaseからのバイトオフセット、所有者はregionハンドル、文字列は静的データのアドレスとして、Wasmのi32で表す。f32はWasmのf32で表し、JavaScriptとはNumberで受け渡す。渡したNumberはbinary32へ丸め、NaN・無限大・符号付きゼロも受け渡せる。ベクトル型とマスク型はWasmのv128で表す(JavaScriptとの境界はソースと関数のexport規則を参照)。参照と借用パラメータを持つ名前付き型は、同じexport規則によりホストへ公開する関数の型に現れない。スカラーレコードの引数と結果はフィールドのWasm値型で直接渡す。他のレコードと和型の引数は値を格納したアドレスで渡す。failsを持つ関数の呼出し規約はコンパイラ内部のもので、ホスト向けの失敗ABIは提供しない。

スカラーレコードは、フィールドがちょうど1個で、その型がi32、u8、f32、bool、str、Array、Ptr、Ref、ベクトル型、マスク型のいずれかであるレコードとする。region・借用パラメータの有無はこの判定を変えない。構築・値のコピー・field-copy・unpackでは線形メモリの作業領域を使わず、allocしたセルにはフィールドの値をそのサイズで格納する。u8フィールドの値を直接受ける関数入口では通常のu8引数と同じ範囲検査を行う。判定の正本はcompiler/types.kotoのty-scalar-recordである。unit、Region、別のレコードを1フィールドに持つ場合や、複数フィールド・和型はアドレスで表す。型の同一性・所有義務・借用の検査と、配列要素型の集合は表現から独立している。

アドレスで表す複合値を返す関数では、さらに先頭に返却先アドレスの隠れた引数を追加し、Wasmの戻り値は持たない。呼出し先は自身の作業領域で値を構築し、返却先へコピーしてから戻る。この表現上のコピーはソースの所有権を複製しない。ベクトル型とマスク型のフィールドは4ワードを占める。型のサイズとフィールド配置はcompiler/types.kotoを定義とする。ホストが線形メモリへ書き込むマスクは、boolを0または1で書くのと同じく、各laneのビットをすべて1またはすべて0にしなければならない。

region内の確保は、スカラー・ポインタ・配列・参照を4バイト、ベクトル型とマスク型を16バイトの境界に置き、regionのbaseは16の倍数とする。ベクトル型のフィールドを持つレコードも4バイト境界の詰めた配置とする。定義はcompiler/types.kotoのty-alignmentである。

アドレスで渡すレコード・和型の引数と返却先の隠れた引数は、呼出しの間に動かないメモリに置く。ホストはそれらを、その呼出しのregion引数・所有者の引数・アドレスで渡す値に含まれる所有者のいずれにも現れないregionのセルに置き、そのregionへの最後の確保の後に読んだbaseとオフセットから求めた絶対アドレスを渡す。呼出しはそのregionへ割り当てられないので、そのセルはコピー成長で移動しない。

アドレスで表す複合一時値には、関数呼出しごとに固定サイズの作業領域を確保する。構築・読取りコピー・置換・複合値を返す呼出しの各式に格納場所を割り当て、ループで同じ式を評価するとその場所を再利用する。再帰呼出しは別の作業領域を持つ。作業領域は線形メモリ上のチャンクに呼出しの順に積み、関数から戻るときに外す。チャンクが足りなければランタイムが次のチャンクを足し、足したチャンクは解放せずに再利用する。チャンクはregionと同じ容量別の領域から取る。チャンクの大きさと連結はdefinitions/runtime.kotoを定義とする。深い再帰は、エンジンのスタック枯渇か、チャンクを確保できないメモリ枯渇の実行時trapで止まる。ユーザーがallocで作るセルや配列の寿命は、明示されたregionに従う。

unitの引数と戻り値、borrowsのパラメータはWasmの値を持たない。unitを返すexportは、JavaScriptではundefinedを返す。ソースレベルでは、隠れた引数や値の内部表現を通常のi32として扱えない。

__koto_で始まるランタイムの関数exportは、ホストが領域を準備・転送・解放するための低水準操作である。__koto_region_new・__koto_region_freeはregionを作成・解放し、__koto_region_base・__koto_region_usedはregionのbaseと使用中のバイト数を返す。__koto_array_new・__koto_array_u8_newは配列を、__koto_cell_newは0で埋めたセルをregionに確保してオフセットを返す。__koto_array_length・__koto_array_u8_lengthはregionとオフセットで指定した配列の要素数を返す。正確な引数とメモリ配置はdefinitions/runtime.kotoに従う。これらのexportは任意のJavaScript入力を安全なヿの値に変換するAPIではない。strのアドレスはJavaScriptの文字列ではなく、配列のオフセットもJavaScriptの配列ではない。

ランタイムは、__koto_操作と、regionパラメータを持つ関数のexportの入口で、呼出しの副作用より前に次を検証し、満たさなければ実行時trapの行でtrapする。

ランタイムが検証しない次の事項はホストの義務とし、破った場合のインスタンスの動作は未定義とする。ネイティブtrap、表の行によるtrap、他のregionのデータの破壊のいずれも起こりうる。

regionハンドルは不透明なi32で、JavaScriptでは負の数にもなる。

ホストは、exportと__koto_操作のどの呼出しの後も、線形メモリへのビューを現在のmemory.bufferから作り直す。__koto_region_newも記述子表の拡張でメモリを伸ばしうる。exportの呼出しと確保の操作(__koto_array_new・__koto_array_u8_new・__koto_cell_new)の後は、読むregionのbaseを__koto_region_baseで読み直す。呼出しへ渡す絶対アドレスは、置き場所のregionへの最後の確保の後に読んだbaseから求める。コンパイル結果のmetadata、診断、コンパイラAPIはホストとAPIを参照する。

実行時trap

実行時trapは回復なしの致命的失敗とする。failsによる回復可能な失敗には変換せず、trap時の巻戻し・所有値の後処理は保証しない。通常終了と宣言された失敗の経路では所有義務を検査する。

生成物は、__koto_操作とexportの入口でのregionハンドル・オフセット・所有者引数、region-freeでのregionハンドル、添字と要素数、u8への変換、ブロックロード・ストアの範囲、線形メモリとregionハンドルの確保を実行時に検査し、失敗した原因のコードを可変i32のglobal __koto_trap_codeに書いてからtrapする。0はtrapなしを表す。原因の名前・文言・コードはdefinitions/abi.kotoのDefinition-trapの表を正本とする。行は利用者に示す直し方が異なる原因ごとに1つで、同じ原因を検出する複数の箇所は同じ行を使う。表のスタック枯渇の行は生成物が書かない。ホストが生成物のexport呼出し(テストのエントリ、ブラウザの実行画面のmain)だけを囲んで捕えたエンジンのスタック枯渇の例外に、この行を付ける。生成物はimportを持たないので、その呼出しから出る例外はエンジン由来である。

原因のコードもassertの位置もないWasmのtrapは「ネイティブtrap」という一つの分類とし、エンジンの文言を解析しない。整数の除算・剰余のゼロ除算と符号付き除算のオーバーフロー、検査付きのf32から整数への切捨て、ホストが渡した不正な文字列・複合値のアドレスと、ホストが線形メモリへ書いた不正な値(検出は保証せず、インスタンスの動作は未定義になる。ホスト境界を参照)、診断状態をexportしない"program"モードでのassertの失敗がこれに当たる。分類の名前と文言も同じファイルに定義する。表にないコードを残した生成物は不正な成果物として扱う。

表の行でtrapする検査は、その検査を担う式の位置を可変i32のglobal __koto_trap_siteにも書く。位置はコンパイル結果のテスト位置(種別"runtime")のIDである。要素の読み書き・ブロックロード・ストア・u8への変換・array-new・alloc・new-region・region-freeではその式、自動解放ではその解放を置いた式、u8のパラメータの検査ではそのパラメータ、呼出しの作業領域の確保では関数の定義、exportの入口の検証ではexportの宣言とする。対応はcompiler/compiler.kotoのemit-trap-siteの呼出しが定義する。__koto_操作の中のtrapは0を書く。この値は表の行によるtrapの後だけ読み、正常終了やネイティブtrapの後の値には意味がない。trapの範囲は、expect-trapの対象がtrapした場合はその対象、表の行による他のtrapでは__koto_trap_siteの位置、ネイティブtrapとスタック枯渇では実行中のテストまたは呼び出したexportとする。

ブラウザの実行画面はtrapした実行を終了して原因、表の行によるtrapではその位置、呼び出したexportを表示し、次回は新しい実行状態を使う。ホストはtrap後に__koto_trap_code・__koto_trap_siteと診断状態を読むだけで、trapした状態でヿの通常処理を再開してはならない。GPUセッションの外部資源は、後述のホスト契約に従って別途終了させる。

GPU向けコード

compile-gpuは単一の(gpu-module 名前 宣言...)を受け取り、WGSLと配置・entry・ソースマップを生成する。構文解析にはCPUコンパイラと同じ読取り器を使う。GPUの型・演算・構文は閉じた集合とし、検査・生成はcompiler/gpu.kotoが担当する。型・演算・metadata配置の共有定義はdefinitions/abi.koto、生成されたホスト用定義はsrc/generated-definitions.jsを参照する。

buffer宣言は(buffer 名前 (group 整数) (binding 整数) (array 型) (access 権限))とする。要素型はi32・u32・f32、権限はread・read_write。GPU上の要素サイズとアラインメントは4バイト。group/bindingの組合せと宣言名は重複できない。buffer-getとbuffer-setの添字、およびbuffer-lengthの結果はu32である。書込みにはread_writeが必要で、vertexからread_writeのbufferへはアクセスできない。

関数はGPUのentryとして宣言する。computeではstage、workgroup-size、params、returnsの順に節を置く。graphicsではworkgroup-sizeを置かない。

stage 入力 出力
compute 空、または(名前 (vec3 u32) (builtin global_invocation_id))一つ unit
vertex 空、または(名前 u32 (builtin vertex_index))一つ (returns (vec4 f32) (builtin position))
fragment 空 (returns (vec4 f32) (location 0))

computeの(workgroup-size x y z)は正整数3個を指定する。実行時にホストがdevice limitsを検証する。ソース上の名前は内部のWGSL名へ変換し、元の名前と対応をmetadataに保持する。

値にはunit・bool・i32・u32・f32と、u32またはf32を要素とするvec2・vec3・vec4を使える。整数リテラルはi32であり、GPU用の明示変換でu32やf32を作る。GPUコードにはCPUの小数リテラルやCPU専用の型・演算名を使えない。(vec4 f32 a b c d)はベクトルを構築し、(lane value 0)はリテラルで指定した成分を返す。対応する演算と変換は共有定義のGPU_OPERATORSを正本とする。数値の演算・変換は生成先WGSLの規則に従う。

let・var・set・do・枝を列挙するifを使える。GPUで扱う値はすべてコピー可能で、変数名を繰り返し使える。可変束縛にはsetで代入する。演算の引数は左から一度ずつ評価し、真偽値の演算も両方の引数を評価する。GPU内の一般関数呼出し、再帰、ループ、region、Ptr/Ref、ADT、文字列、組込みテストは対応しない。vertex属性・stage間varying・texture/sampler・atomic/barrierも初期範囲には含めない。

bufferのアクセス権限はshaderが利用できる操作を制約する。同時invocation間のデータ競合を排除する保証ではない。添字の範囲やinvocation同士の書込み先はGPUコードで管理する。CPUのregionの所有者や線形メモリのポインタをGPU資源へ直接渡さない。

ブラウザ外部資源の所有

初期の外部資源契約はsrc/gpu-session.jsのGPUセッションとする。セッションがdevice、buffer、pipeline、canvas設定を所有し、呼出し元へ生のGPUハンドルを返さない。typed arrayによるデータ転送、完了待ち、明示的な終了をホストAPIで扱う。ヿのCPUプログラムへ一般FFIや非同期ランタイムを追加するものではない。

一つのセッション内のGPU操作は直列化し、未完了の操作がある間の再入・転送を拒否する。通常経路では利用者がcloseを呼ぶ。停止・外部trap・device loss・GPU内部エラーでは、保留中の操作を失敗させて資源を破棄する。失敗状態から処理を再開しない。作成途中で取消された場合も、後から取得できたdeviceを破棄する。

GPU資源の終了はCPU regionの一括解放とは別の責務であり、オブジェクトごとの暗黙デストラクタを導入しない。GPU側の物理的な回収時期はブラウザとdeviceの実行に依存する。公開APIと例はホストとAPIを参照する。

コンパイラ用ライブラリ

compiler/collections.kotoは、既存の型・region・権限で実装したcollectionsモジュールである。利用側は必要な公開名を明示的にimportする。依存するconstantsモジュールはcompiler/generated-constants.kotoに生成される。両ファイルの内容を同じコンパイル入力に含める必要があり、importによる自動読込みは行わない。公開範囲と型・関数の署名はcollections.kotoの宣言を参照する。

バッファ・builder・集合の内部表現は非公開とし、操作関数で不変条件を保つ。表現を公開したsliceとソース範囲は、利用側でもfield-copyで必要なフィールドを取得できる。

IntBufferは容量と初期化済みの長さを分け、取得・更新の添字を後者の範囲で検査する。成長時には新しい配列を確保して初期化済みの値を移し、元の配列もregion解放まで有効とする。予約済み領域への追記は書込み権限だけを要求し、容量不足ではtrapする。成長は実行時の配列上限(max-array-length)を超える配列を要求しない。上限を超える追記は内容を変えずにバッファを上限超過の状態にする。この状態は解除されず、以後の成長する追記と要素の更新は無視され、要素の読取り・予約・予約済み追記はtrapする。利用側はint-buffer-exceededで確かめてからint-buffer-arrayでちょうどの長さの配列へ写す。int-buffer-appendは上限超過のバッファを追記すると追記先も上限超過にする。

ByteBuilderはu8をバイト配列へ追記し、ByteSliceとして配列内の範囲を返す。byte-slice-getはu8を返し、byte-builder-pushと予約済み追記はu8を受け取る。sliceはRefではなく配列ハンドルと範囲の値であり、読取りには所属regionの権限が必要である。既存sliceの長さは後続の追記で変化しない。ByteBuilderの成長と上限超過も同じ規則で、上限はmax-u8-array-lengthとする。byte-builder-viewは要素の読取りに当たる。byte-builder-clearは長さだけを0に戻し、上限超過の状態は保つ。byte-builder-exceeded・byte-builder-append・byte-builder-arrayはIntBufferの対応する操作と同じに振る舞う。IntSetは整数の重複を除き、cloneは独立した格納先を作り、等価比較は要素の所属で判定する。SourceSpanは非負のソース識別子と、開始位置以上の終了位置を持つ半開区間を表す。

LLM向けの操作

ブラウザ上の作業領域は順番付きの複数ファイルを保持し、内容の版を指定して検査・編集・組込みテストを実行する。編集は候補全体の静的検査を通してから一括適用する。ソース解析はヿ製Wasm、テキスト管理とMCP転送はホストが担当する。接続・位置・失敗・取消しの契約はホストとAPIを参照する。LSPは提供しない。

未実装・未確定の境界

存在型、RefMut、同じregion内の異なる場所を独立して借用する機能、regionパラメータが互いに異なることを宣言する機能は、必要性を確認してから導入を検討する。

設計の議論・判断理由・研究資料はGitHub Issues、タスクの状態はGitHub Projectで管理する。