最終更新: 2026-09-08
設計ステータス: 次期仕様確定・未実装
現行の language-spec.md は、実装済みの観測可能な挙動を規定する正本である。本書はそれを書き換えるものではなく、監査バックログで未決定だった意味論、品質改善、将来機能を実装するための次revisionの実装仕様である。
本書と現行実装が異なる間は、利用者が実際に依存できる挙動は language-spec.md に従う。本書の各受入基準を満たし、tree-walk版とVM版のpaired testが通過した時点で、該当事項を language-spec.md へ統合し、仕様revisionを更新する。部分実装を「次revision対応」と表示してはならない。
新規6設計文書間では、本書が次期言語挙動、CLI文法、canonical errorとscript catch可否の正本である。Phase 3/4の公開API・状態機械・budget・transactionは実行予算・協調実行仕様、Phase 5/6のaudit schemaとfail-closed契約は決定性・実行時監査仕様、Phase 0の責任境界は脅威モデルを正本とする。組み込みAPI仕様とCapability Model仕様のPhase 1/2先行実装はこれらの最終契約の内部subsetとして実装し、同じbuildへ旧型・旧event・旧状態機械を併存させない。
本書で用いる規範語は次の意味を持つ。
- MUST / 必須: 実装・テスト・文書のすべてで満たす。
- MUST NOT / 禁止: 実装してはならない。
- SHOULD / 原則: 例外には設計記録と回帰テストを必要とする。
- 観測可能: 戻り値、標準出力・標準エラー、終了コード、error kind/message/line/trace、外部効果、REPL次入力から見える状態を含む。
対象backendはデフォルトのtree-walk版と --vm 版である。明示的に「内部リファクタ」とした項目を除き、両backendは同じ観測可能な結果を返さなければならない。
- 規範意味論は一つ: backend固有の期待ファイルは、次期revisionで明示的に残す非適合を除き作らない。本書の対象事項については差を残さない。
- 実行context単位の状態: 変数、closure cell、import marker、FunctionId、capability、budget、audit識別子は
ExecutionContextに属する。process-globalな可変意味論を増やさない。 - 失敗は構造化する: script入力からhost panic、abort、unchecked cast、index panicへ到達させない。
- 左から右の評価: 本書に別の優先順位を定義しない限り、receiver/callee、引数、index、代入値はsource順に一度だけ評価する。
- lineとtraceを意味論に含める: kindとmessageだけ一致していても、lineまたはtraceが異なればbackend非互換とする。
- 外部効果はlanguage-stateと分離する: language-stateをrollbackしても、filesystem、stdio、process、network等の完了済み外部効果は戻らない。外部効果の有無はauditで観測可能にする。
- IDを再利用しない: FunctionId等の観測可能な同一性に使う単調増加IDは、rollback後も再利用しない。
- 上限は境界を含めて定義する: 「128」のような値だけでなく、何を数え、どの時点で拒否するかを共有定数のdoc contractに含める。
runtime errorは、operationごとの共通constructorから kind、canonical message、source lineを生成する。tree/VMが個別にmessageを組み立てる方式と、message文字列からkindを推測する方式を廃止する。classify_runtime_error() は互換移行中のprivate fallbackに限定し、次期revision対象経路から呼んではならない。
- backendごとの自然な診断を許す: golden testとhost側分岐がbackend依存になるため却下する。
- messageの部分一致だけを保証する: 値や型による誤分類、trace欠落、余分な副作用を検出できないため却下する。
- すべてを
runtimekindへ集約する:try/catchとembedding hostが失敗原因を安全に識別できないため却下する。 - OSエラー文字列をcanonical messageへ埋め込む: OS・locale差が規範出力になるため却下する。詳細は非規範のcauseまたはaudit metadataへ保持する。
runtime errorは次の論理構造を持つ。
RuntimeError {
kind: ErrorKind,
message: String,
line: usize,
trace: Vec<TraceFrame>,
cause: Option<HostCause>, # scriptへは非公開、audit/host API向け
}
TraceFrame {
name: String,
line: usize, # 呼び出し元のcall-site行
}
既存kindに加え、host boundary実装時に capability、budget、timeout、cancelled、host を追加する。budget、timeout、cancelled は停止要求でありscriptの try/catch では捕捉できない。その他のruntime errorは原則として捕捉できる。
message内の型名は Int、Float、Str、Bool、Null、List、Dict、Fn、Error、Class、Instance、BoundMethod を用いる。{actual} へ値全体やsecretを埋め込まず、型または安全な整数境界だけを表示する。
以下のテンプレートを完全一致の正本とする。波括弧は実行時に置換する部分である。
| operation / 条件 | kind | canonical message |
|---|---|---|
| 変数read・callee名が未定義 | name |
未定義の変数または関数: {name} |
| 未定義変数への通常代入 | name |
未定義の変数に代入: {name} |
let / 関数値 / class値のID割当不能 |
internal |
内部エラー: {id_kind} を割り当てできません |
| Intの0除算・剰余 | zero_division |
ゼロ除算 |
| 算術演算の対象型不正 | type |
演算子 {op} は {left_type} と {right_type} に適用できません |
| 大小比較の対象型不正 | type |
比較演算子 {op} は {left_type} と {right_type} に適用できません |
| List indexがIntでない | type |
List のインデックスは Int である必要があります: {actual_type} |
| Str indexがIntでない | type |
Str のインデックスは Int である必要があります: {actual_type} |
| Dict keyがStrでない | type |
Dict のキーは Str である必要があります: {actual_type} |
| List index範囲外 | index |
List のインデックスが範囲外です: {index} (長さ: {len}) |
| Str index範囲外 | index |
Str のインデックスが範囲外です: {index} (長さ: {len}) |
| index read非対応型 | type |
インデックスアクセスできない型です: {actual_type} |
| index assignment非対応型 | type |
インデックス代入できない型です: {actual_type} |
| 反復非対応型 | iteration |
反復できない型です: {actual_type} |
| 非callableの呼び出し | type |
呼び出せない型です: {actual_type} |
| user function / method arity | argument |
{callable} の引数個数が一致しません: 期待 {expected}, 実際 {actual} |
| builtin arity | argument |
{builtin} の引数個数が一致しません: 期待 {expected}, 実際 {actual} |
| variadic builtin最小arity | argument |
{builtin} の引数が不足しています: 最小 {minimum}, 実際 {actual} |
| builtin argument型 | builtin_type |
{builtin} の第 {position} 引数は {expected} である必要があります: {actual_type} |
| callbackが非callable | type |
{builtin} のコールバックは呼び出し可能である必要があります: {actual_type} |
| callback arity | argument |
{builtin} のコールバック引数個数が一致しません: 期待 1, 実際 {actual} |
push / pop の第1引数が変数でない |
builtin_type |
{builtin} の第 1 引数には List 変数を指定してください |
空Listへのpop |
builtin_type |
pop は空の List には使用できません |
| builtin固有の状態不正 | builtin_type |
{builtin} は {state} には使用できません |
| collection上限 | collection_limit |
コレクション要素数が上限を超えました: {requested} (上限: {limit}) |
| user function depth上限 | overflow |
スタックオーバーフロー: 再帰が深すぎます (上限: 128) |
| AST深度上限 | overflow |
AST のネストが深すぎます (上限: 256) |
| import chain深度上限 | import |
import 失敗: ネストが深すぎます (上限: 128) |
| import先が存在しない・読めない | import |
import に失敗しました: モジュールを読み込めません: {module} |
| import先のparse失敗 | import |
import に失敗しました: モジュールの構文が不正です: {module} |
loop外のbreak |
control_flow |
break はループの中でのみ使用できます |
loop外のcontinue |
control_flow |
continue はループの中でのみ使用できます |
| step/fuel上限 | limit |
ステップ上限に達しました (上限: {limit}) |
| 整数演算overflow | int_overflow |
整数オーバーフロー: {operation} |
| 文字列等からIntへ変換不能 | conversion |
to_int で Int に変換できません: {reason} |
| Float→Int不能 | conversion |
{builtin} で Int に変換できません: {reason} |
| file sizeがi64範囲外 | int_overflow |
ファイルサイズを Int で表現できません |
exit code範囲外 |
argument |
exit の終了コードは 0 から 255 の範囲で指定してください: {code} |
path_join非Str |
builtin_type |
path_join の第 {position} 引数は Str である必要があります: {actual_type} |
| property read対象がInstanceでない | type |
プロパティアクセスできない型です: {actual_type} |
| property write対象がInstanceでない | type |
プロパティ代入できない型です: {actual_type} |
| property不存在 | name |
プロパティが見つかりません: {class_name}.{property} |
initがNull以外をreturn |
type |
{class_name}.init は Null 以外を返せません: {actual_type} |
| filesystem capability拒否 | sandbox |
ファイル操作が許可されていません: {operation} |
| host function capability拒否 | capability |
host function の実行が許可されていません: {name} |
| host adapter失敗 | host |
host function の実行に失敗しました: {name} ({category}) |
| deadline超過 | timeout |
実行期限を超過しました |
| cancellation | cancelled |
実行がキャンセルされました |
| host/heap/I/O budget超過 | budget |
{budget_name} の上限を超えました (上限: {limit}) |
| stdout書込失敗 | io |
標準出力への書き込みに失敗しました |
| VM/Compiler不変条件違反 | internal |
内部エラー: {stable_detail} |
{state}、{reason}、{operation}、{category} は各BuiltinSpecまたはoperation constructorが列挙型から選び、任意のOS文字列を入れない。import先のparse error詳細はcauseとして保持し、CLIはwrapperに続けて同じcanonical parse diagnosticsをsource順に表示する。
この表は言語core runtime error constructorの全inventoryである。新しいcore constructorを追加するPRは、既存行へ対応付けるか新しいcanonical行を追加しなければCIを通過してはならない。core外のHostFunction adapterは、登録時に静的 HostErrorSpec extension table(kind、message template、placeholder列挙、catch可否)を同時登録し、同じinventory testへ連結する。HTTP adapterのextension正本は16.12節であり、未登録adapterのerror constructorは実行できない。filesystem builtinのうち仕様上Null/Falseを返すOS失敗はruntime error constructorではないため表の対象外だが、capability拒否は必ず表のfilesystem capability拒否を使う。
expected が複数候補の場合はregistryに保持した文字列(例: 1 または 2)を用いる。callbackで実際のuser function bodyへ入った後に発生したエラーは、callback専用messageへ包み直さず、元のkind/message/lineを保持する。
| 失敗位置 | canonical line |
|---|---|
| parse error | 不正tokenまたはEOF tokenの行 |
| 二項・単項演算 | 演算子を含む式の開始行 |
| index read | [ を含むindex式の開始行 |
| index assignment | assignment文の開始行 |
| callee/arity/capability検査 | call式の開始行 |
| 引数式内部 | その引数で実際に失敗した式の行 |
| builtin本体の型・境界検査 | call式の開始行 |
| callback body | callback内で実際に失敗した文・式の行 |
| property read/write | . を含む式または代入文の開始行 |
initの不正return |
return 文の行 |
| import解決 | rootから見た import 文の行 |
| host adapter内部 | host functionのcall式の行 |
| defensive VMのline table不正 | 安全に取得できれば該当命令行、取得不能なら1 |
- traceはエラー発生地点に最も近いuser function/methodから外側へ並べる。
- 各frameのlineは、そのframeを呼び出したcaller側call-siteの行とする。root script frameは含めない。
- call validationがframe作成前に失敗した場合、不正calleeのframeは追加しない。
- callbackへ入った場合はcallbackのuser function frameを通常関数と同じように追加する。
map等のbuiltin自体はframeに追加しない。 - bound methodのframe名は
{Class}.{method}、initは{Class}.initとする。 - host adapter内部frame、Rust関数名、VM instruction offsetをscript traceへ出さない。
- catchされたError値の
lineはorigin lineを保持する。次期revisionではError値へtraceを公開しないが、再throw相当の仕組みを将来追加してもorigin traceを破棄しない。
| operation | 検査・評価順 |
|---|---|
| user function / method call | callee評価 → callable分類 → user callableならstep・user frame深度 → callable/arity → 引数を左から右 → frame作成 → body |
context builtin (input/args/exit/push/pop/map/filter/each) |
builtin選択 → arity → mutation target構文条件 → 引数を左から右 → 型・状態 → 実行 |
| pure/core builtin | callee選択 → 引数を左から右 → registry arity → 型・境界 → 実行 |
| host function | symbol/arity → host functionの粗粒度capability → host-call回数budget/deadline/cancel → 引数を左から右 → 型・shape → 引数値依存policy(URL/宛先等)→ audit start → adapter外部処理 |
| index read | object → index → object型 → index/key型 → 境界 → read |
| index assignment | target binding存在 → index → value → targetの最新値の型 → index/key型 → 境界/size → mutation |
| property read | receiver → Instance検査 → field lookup → method lookup → missing error |
| property assignment | receiver → Instance検査 → value → field mutation |
| class construction | class/init arity → 引数を左から右 → Instance生成 → init user frame → Instance返却 |
| callback builtin | outer引数を左から右 → collection型 → 要素ごとにcallback callable/arity → callback body |
src/error.rs: operation別constructor、追加kind、canonical rendererを定義する。src/eval.rs,src/builtin.rs:runtime()のmessage推測をやめ、共通constructorとline規則を使う。src/vm.rs:eval_index、call/callback、iteration、trace生成を共通constructorへ移す。src/builtin_core.rs: builtin型・境界errorをregistry metadataと共通constructorへ統合する。src/compiler.rs: call-site、property、callbackのsource lineをopcodeへ欠落なく保持する。- CLIは
Displayのcanonical出力だけをstderrへ書く。backend名を追加しない。
error messageやkindを文字列比較するscript・hostは更新が必要になる。移行期間に旧messageを併記してはならない。release noteに operation→旧/新kind/messageの対応表を掲載し、e["type"] を使うことを推奨する。
| 軸 | ケース |
|---|---|
| backend | tree / VM |
| phase | parse / link / call validation / argument / body / host boundary |
| call | direct / closure / callback / bound method / constructor |
| index | List / Dict / Str / 非対応型 / 負index / 範囲外 |
| trace | top-level / 1段 / 多段 / callback / method→function→method |
| catch | 未捕捉 / 同一frame catch / caller catch |
| line | 単行 / 複数行 / import先 / f-string内 |
全ケースで終了コード、stdout、stderr、kind、message、line、traceを完全一致比較する。index_read_lowering.expected.vm のような本項目由来のbackend別期待値は削除する。
- 代表例ではなくoperation表の全行に少なくとも1つのpaired testがある。
- runtime error constructor inventory testが、表または登録済みHostErrorSpec extensionに対応しないconstructorと任意文字列からのkind推測を拒否する。
- error生成の対象経路で
classify_runtime_error()を呼ばない。 - tree/VMのstdout・stderr・終了コード・Error値が完全一致する。
- callback、import、REPL、defensive Chunkを含めhost panicがない。
実装状況(2026-09-06): ✅ 実装完了。VM Compilerの
Stmt::Let再宣言でのslot再利用を廃止し、全scopeで新slotを割り当てるよう統一した。観測挙動を変更したためlanguage-spec.mdへ規範として反映し、仕様revisionを0.14へ上げた。
同一scopeで同名を let 再宣言した場合も、必ず新しいcellを作る。そのscopeの名前解決は新cellへ切り替わるが、再宣言前に作られたclosureは旧cellを保持し、旧値を見る。現行tree-walk版へ統一する。
let x = 1
let before = fn() x end
let x = 2
let after = fn() x end
print(before()) # 1
print(after()) # 2
print(x) # 2
関数定義・class定義も名前を現在scopeへbindingする宣言であり、同じfresh-cell規則を使う。
- 既存cellの値だけを更新する:
letと通常代入の区別が失われ、過去のclosureの意味が再宣言で変わるため却下する。 - 同一scope再宣言を構文エラーにする: 現行sourceとの互換性を不必要に壊すため却下する。
- closureだけ旧値をsnapshotする: 参照captureモデルと矛盾するため却下する。
scopeは name -> CellId/SharedValue のmapである。
Declare(name, value):
old = scope[name] # あってもよい
fresh = new Cell(value)
scope[name] = fresh
oldは既存closureが参照している限り生存
Assign(name, value):
visible scopeを検索
解決した既存Cellの内容を更新
REPL transactionが未捕捉errorでrollbackした場合、scope mapは入力開始時のcellへ戻し、入力中に作ったfresh cellを公開しない。FunctionIdは再利用しない。
- tree:
Env::setの現行fresh-cell挙動を規範として維持し、unit testを追加する。 - VM:
Compiler::compile_stmt(Stmt::Let/FnDef/ClassDef)はfind_local_in_current_scopeによるslot再利用をやめ、同一scopeでも新slotを割り当てる。name resolverとtop-level global registryだけを新slotへ付け替える。 - VM: 旧slotと
locals_cellsをpopしてはならない。escaping closureが旧cellを保持する。 - REPL: 正常commit後は最新slotを次入力の名前解決対象にし、rollback時は旧slot mappingへ戻す。
再宣言自体はerrorにしない。cell/slot割当不能は internal / 内部エラー: binding cell を割り当てできません とし、宣言行をlineにする。
VMで「再宣言後に過去のclosureも新値を見る」挙動へ依存したsourceは変わる。共有更新が必要な場合は let ではなく通常代入を使う。
| scope | 宣言種別 | closure作成位置 | 実行形態 |
|---|---|---|---|
| top-level / function / block / REPL跨ぎ | let / fn / class |
再宣言前 / 後 / 両方 | direct / callback / import |
旧closure、最新binding、通常代入、shadowing、未捕捉error rollback、cell化前後をtree/VMで比較する。
- すべてのscopeで再宣言がfresh cellになる。
- 既存closureは旧cell、再宣言後のreadと新closureは新cellを見る。
- VMのslot再利用による差がなく、REPL失敗時にstale slotが残らない。
MAX_USER_CALL_DEPTH = 128 を src/limits.rs に一度だけ定義する。数えるのはroot frameを除く、現在activeなuser定義関数・lambda・method・initのframeである。通常callと map/filter/each callbackは同じ規則を使う。host functionとcore builtin自体はuser frameへ数えない。
128個目のuser frameは実行可能で、129個目を作る直前に拒否する。動的calleeは先に一度だけ評価・分類し、user callable(Fn、BoundMethod、initを持つClass)の場合だけ、引数評価より前にstepと深度を検査する。builtin、host function、initなしClassにはuser-frame深度検査を適用しない。constructor allocationはframeに数えず、実際に実行する init は1 user frameとして数える。host functionがhost内部で行うRust callやHTTP redirectも数えないが、別のhost-call budgetで制御する。
- VM root frameを1つとして数える: backendの実装詳細がsourceの許容深度へ漏れるため却下する。
- callbackだけ別上限にする:
map経由で上限を迂回できるため却下する。 - host functionもuser frameに数える: scriptが制御できないadapter実装深度で結果が変わるため却下する。
active_user_frames: usize
BeforeUserCall:
if active_user_frames >= MAX_USER_CALL_DEPTH: error
active_user_frames += 1
AfterUserCall / unwind:
active_user_frames -= 1
unwind、catch、return、cancellationの全経路で減算する。VMは frames.len() - root_frame_count を直接各所で計算せず、active_user_frame_count() を唯一の計数APIにする。
src/limits.rs:MAX_USER_CALL_DEPTHと本節のdoc contractを追加する。src/eval.rs: ローカル定数を削除し、通常callで共有定数を使う。src/builtin.rs: callback callで同じ計数APIを使う。src/vm.rs: ローカル定数を削除し、root frameを除くhelperをPrepareCall、defensiveCall、callbackへ適用する。- class実装時はmethod/initも同じhelperを通す。
上限超過は overflow、messageは スタックオーバーフロー: 再帰が深すぎます (上限: 128)。lineは拒否された129個目のcall式。traceには既にactiveな128 user frameだけを内側から外側へ出す。
VMで許容されるuser frameが127から128へ1つ増える。また深度上限到達時もcallee式の評価までは行われ、その結果をuser callableと分類した後、引数評価前に拒否する。callee式の副作用順に依存する場合はこの規則へ移行する。tree/VMともbuiltin・host function呼び出しはuser depth 128でも拒否しない。MAX_CALL_DEPTH を直接参照していたlibrary利用コードは、公開する場合 MAX_USER_CALL_DEPTH へ移行する。
- direct recursion 127 / 128 / 129
- mutual recursion
- lambda recursion
map/filter/eachcallback recursion- method/init recursion
- user→host→user再入が可能になった場合のuser frame計数
- REPLでcatch済み上限error後の回復
- defensive Chunkが
PrepareCallを迂回した場合
- tree/VMとも128 user frameを許可し、129個目を同じline/message/traceで拒否する。
- callback・method・initで迂回できない。
- root/host/core builtin frameは数えない。
MAX_USER_CALL_DEPTHの定義はlimits.rsの1か所だけに存在する。
CLI grammarを次で固定する。Capability/profile optionもすべてOPTIONSに属し、別文書に独立したgrammarを定義しない。
tsumugi [OPTIONS] [SCRIPT [ARGS...]]
OPTIONS:
--vm
--profile safe|legacy
--allow-env KEY
--allow-clock
--allow-script-stdin
--allow-exit
--deny-stdout
--fs-root NAME=PATH
--fs-op NAME=OP[,OP...]
--allow-import-root NAME=PATH
--help
--version
--
複数回指定できるoptionは--allow-env、--fs-root、--fs-op、--allow-import-rootだけであり、詳細な値検証とsafe/legacy profileの構築規則はCapability Model仕様第14節に従う。safe profileはprofile builderがstdout capabilityを明示grantする既定構成であり、process stdioへのambient accessではない。--deny-stdoutはその明示grantを除去する。REPL promptはscript capability外のCLI I/Oである。
option解析中の最初のpositionalを SCRIPT とし、それ以後のtokenは既知option、未知option、--を含めて一切再解釈せず、そのままscript argsとする。-- はoption解析を終了し、次のtokenがあればSCRIPTとして扱う。--だけでEOFならSCRIPTなしとしてREPLを起動する。- はstdinからscript source全体を読む特別なSCRIPTである。SCRIPTがなければREPLを起動する。
例:
tsumugi --vm app.tsg a --vm # VMで実行、args() == ["a", "--vm"]
tsumugi -- app.tsg --help # treeで実行、args() == ["--help"]
tsumugi - a b # stdin script、args() == ["a", "b"]
tsumugi -- # SCRIPTなしなのでREPL
- argv全体から
--vmをfilterする: scriptへ同名文字列を渡せないため却下する。 - 2個目以降のpositionalをusage errorにする:
args()の公開契約を満たせないため却下する。 - process-global argvをbuiltinから読む: embedding APIとテストが実行単位で引数を注入できないため却下する。
-をREPLとして扱う: pipeされた完全scriptと対話入力を区別できないため却下する。
Options --(--vm / profile / capability option)--> Options(updated configuration)
Options --(--)-----> ExpectScript
Options --(-)------> Script(stdin), Args
Options --(other positional)--> Script(path), Args
Args --(any UTF-8 token)-------> append verbatim
Options/ExpectScript --(EOF)---> REPL
--help と --version はOptions状態でのみterminal actionとなり、表示して0で終了する。--vm は複数回指定してもidempotent。profile/boolean option重複、unknown option、同NAME・同KEY重複、option値欠落、profileとの不正な組合せは、いずれも診断とusageをstderrへ出し1で終了する。SCRIPT確定後は同じtokenを含めてすべてArgsへ渡す。
全argvを副作用前にUTF-8へ変換する。SCRIPTまたはARGSを含む1つでも非UTF-8なら エラー: コマンドライン引数はUTF-8で指定してください をstderrへ出し、終了コード1とする。
CliInvocation {
backend: Tree | Vm,
source: Repl | Stdin | File(PathBuf),
script_args: Vec<String>,
profile: Safe | Legacy,
capability_options: CliCapabilityOptions,
}
script_args は ExecutionRequest.arguments へsnapshotとして注入し、args() はそのcloneを返す。tree/VMとも std::env::args*() を実行中に読まない。stdin scriptのsource nameは <stdin>、相対import基準はCLI起動時cwdとする。profileとcapability optionはSCRIPT確定前にfreezeし、safe profileのstdoutを含む全grantをCapabilitySetBuilderで明示構築する。
src/main.rs: argv parsingを単一関数へ分離し、tree/VMへ同じCliInvocationを渡す。stdin scriptとREPLを分離する。src/engine.rs:ExecutionContextのhost inputへscript argsを追加する。src/builtin.rs:args()をcontext snapshotから返す。src/vm.rs: process argv参照を削除し、VM execution contextから返す。- help/usageを新grammarへ更新する。
CLI parse errorはTsumugi runtime errorではなく、traceなしのCLI診断である。help/versionはstdout・0、unknown option、missing file、非UTF-8、stdin read失敗はstderr・1。stdin scriptのparse/runtime errorは通常scriptと同じcanonical errorをstderrへ出し1で終了する。
従来usage errorだった追加positionalがscript argsになる。script path後の --vm はbackend optionではなくなるため、VM指定はSCRIPTより前へ移す。引数を取りたいembedding hostはprocess argvではなくcontextへ明示注入する。
| source | option位置 | args | backend |
|---|---|---|---|
| file / stdin / REPL | 前 / --後 / script後 |
空 / 通常 / --vm / --help / - / Unicode |
tree / VM |
help、version、unknown option、missing file、stdin read error、非UTF-8が実行前に副作用0で終了することも検証する。
- grammar表の全例でtree/VMの
args()が一致する。 - script後の全tokenが順序・内容を保つ。
-は非対話script、scriptなしはREPLになる。- runtime中にprocess-global argvを参照しない。
- 非UTF-8でpanicせず診断+1になる。
実装状況(2026-09-07): ✅ 実装完了。tree/VM両engineで、未捕捉ランタイムエラーで 終了したREPL入力が変更した language-state(binding、cell の値、index 代入、push/pop、 import marker)を入力開始時点へ巻き戻すようにした。正常完了と catch されて完了した入力は commit する。stdout・ファイル書き込み等の外部効果は巻き戻さない。FunctionId は rollback しても再利用しない(AUD-048)。observable挙動が変わるため
language-spec.mdへ反映し、 仕様revisionを0.15へ上げた。7.5節の journal 方式(first-write undo log、cell は Rc ポインタ同一性で1回だけ記録)で実装し、記録量は変更した箇所の数に比例する。deadline/ budget/cancellation は未実装のため、それらに伴う rollback は本 phase の対象外。
未捕捉runtime errorで終了したREPL入力は、その入力が変更した全language-stateを入力開始時点へrollbackする。正常完了と、入力内でcatchされ最終的に正常完了したerrorはcommitする。外部I/Oはrollbackせず、rollback後も「partial effectsあり」とauditへ記録する。
- errorまでのprefixをcommitする: Compiler/Envの宣言状態と値の公開状態を一貫させにくく、backend差を残すため却下する。
- stack slotだけrollbackする: captured cell、upvalue、List/Dict、Instance field、import markerが漏れるため却下する。
- 外部I/Oもtransaction化する: stdout、任意filesystem、HTTP等に一般的なrollbackを提供できないため却下する。
- context全体をdeep cloneする: REPL状態量に比例する時間・一時メモリを毎入力で必要とするため却下する。
Idle
-> Begin(submission_id)
-> Link/Compile
parse/link/compile failure -> AbortWithoutExecution -> Idle
-> Execute
completed / Exited -> Commit -> Idle
caught error then completed -> Commit -> Idle
uncaught runtime error -> RollbackLanguageState -> AuditPartialEffects -> Idle
cancellation/deadline/budget abort -> RollbackLanguageState -> AuditAbort -> Idle
rollback対象は次を含む。
- scopeのname→cell mapping、新規・再宣言binding
- 既存cellの値、captured cell、upvalue
- List/Dictの要素・キー、nested collection
- class導入後のInstance field
- VM stack slot、locals_cells、global name→slot、frame、handler
- Compiler locals/scope/loop状態とModuleLoaderのloaded/loading marker、base directory
- import先が作成・変更したlanguage binding
- step/fuelの入力内消費状態(次入力は新予算)
rollback対象外は次である。
- FunctionId、将来のClassId等の単調増加identity counter
- stdout/stderr/input消費
- filesystem/process/network/host functionの完了済み効果
- audit event自体
- hostがcontext外で管理するrate limit、circuit breaker、secret使用回数
transactionはcopy-on-write journalを使う。
SubmissionJournal {
undo_log: Vec<UndoEntry>,
seen_scope_entries: Set<(ScopeId, Name)>,
seen_cells: Set<CellId>,
seen_instance_fields: Set<(InstanceIdentity, FieldName)>,
compiler_checkpoint,
loader_checkpoint,
vm_checkpoint,
external_effect_count,
}
UndoEntry =
ScopeEntry { scope, name, original: Option<Cell> }
| CellValue { cell, original: Value }
| InstanceField { instance, field, original: Option<Value> }
同じscope entry、cell、instance fieldを1入力で複数回変更しても元値は最初の1回だけ undo_log へ追加する。InstanceはFieldMap全体ではなく変更field単位で、不存在も None として記録する。rollbackは undo_log の逆順、commitはjournal破棄で行う。AUD-047のList/Dict COWにより Value journal cloneはO(1)のhandle cloneになる。これによりrollbackの記録量は変更したentry/cell/field数に比例し、Instance全field数には比例しない。
- tree:
ExecutionContextがsubmission transactionを開始し、Env::set/update、index mutation、push/pop、upvalue cell、将来のproperty mutationをjournal経由にする。 - tree:
Evaluator::runのmodule marker rollbackと同じtransactionへ統合する。 - VM: 現行
ReplStackCheckpointを拡張し、SharedValueへのSetLocal/SetGlobal/SetUpvalueとindex mutationもfirst-write journalへ記録する。 - VM: Compiler/ModuleLoader checkpointをsubmission objectへ統合し、値rollbackと名前解決rollbackを同時に行う。
- host boundary: 外部操作開始/完了時にsubmission idとeffect countをauditへ通知する。
元のruntime errorのkind/message/line/traceを変更しない。rollback自体に失敗した場合は元errorをcauseとして保持し、internal / 内部エラー: REPL state を復元できません を返してcontextをpoison状態にする。poison状態のcontextは再利用禁止。
外部効果が1件以上完了してからrollbackした場合、決定性・実行時監査仕様の最終AuditEvent::Terminalへcontext_committed = false、host_effects_may_remain = trueと終了理由を記録する。別名のcompletion fieldや独自eventを追加せず、script stderrへ追加warningも出さない。
未捕捉error後に途中代入やList変更が残ることへ依存した対話操作は変わる。変更を残したい場合は try/catch で処理し、入力を正常完了させる。
- binding: 新規let / 再宣言 / 通常代入 / fn / import
- cell: local / global / captured / 多段upvalue / cell化前後
- collection: List / Dict / nested / push / pop / index assignment
- control: direct error / function / callback / try内で未捕捉 / caught
- external: print / file write / fake host call後のerror
- backend: tree / VM、同一processで次入力から確認
- failure: rollback中internal failureをfault injection
- 未捕捉runtime error後の全language-stateが入力開始時と観測上同一になる。
- caught errorと正常入力はcommitされる。
- import再試行、closure identity、最新binding mappingが壊れない。
- 外部効果は戻さず、auditがpartial effectsを欠落なく示す。
- rollbackコストは変更量に比例し、保持中全state量には比例しない。
継続入力bufferが空でない状態でEOFを受けた場合、bufferを破棄して正常終了してはならない。通常のLexer/ParserへEOFを渡し、parse診断をstderrへ出して終了コード1でREPLを終了する。tree/VM共通である。
bufferが空のEOFだけを正常終了(0)とする。
- 無言でbufferを破棄する: typoやpipe切断を成功と誤認するため却下する。
- EOF後もpromptを出し続ける: 入力sourceが閉じているため進行不能になる。
- runtime errorとして扱う: 実行開始前の構文不完結なのでparse errorである。
Reading(buffer="") + EOF -> Completed(0)
Reading(buffer!="") + EOF -> Parse(buffer + EOF) -> Diagnostic -> Failed(1)
lineはLexerが生成するEOF tokenの行。未閉じblockのcanonical messageは 入力が未完結です: end が必要です。未閉じ文字列、f-string、括弧は各parser/lexerの既存canonical parse messageを使う。
src/main.rs のtree/VM REPL loopで共通 finish_repl_at_eof(buffer) を使う。is_incomplete の判定だけでmessageを合成せず、実Parserの結果を表示する。
未完結入力をpipeしていた自動処理の終了コードが0から1へ変わる。
if/fn/while/for/try/class、複数行lambda、括弧、List/Dict、string、f-string、commentのみ、空bufferをtree/VMのstdin subprocessで確認する。
- 非空未完結buffer+EOFはparse診断・stderr・1。
- 空buffer+EOFは出力を壊さず0。
- tree/VMで診断全文と終了コードが一致する。
実装状況: ✅ 実装完了(仕様revision 0.16で
language-spec.mdへ統合済み)。src/builtin_core.rsのbuiltin_path_joinが全引数を左から右へStr検査し、最初の非Strでbuiltin_typeエラーを返す。tree/VMはAUD-049の共有registry/handlerを使うため差はない。tests/canonical_error_inventory.rs(tree/VM一致)とtests/path_join_contract.rs(OS依存期待値をRustPathBufから構築)で固定した。
path_join は可変長引数で、全引数がStrでなければならない。
- 0引数:
"" - 1引数: Rust
PathBufへその要素をpushした結果 - N引数: 空の
PathBufへ左から順にPathBuf::pushした結果 - filesystemへのアクセス、canonicalize、存在確認は行わない
- separator、absolute component、prefixの扱いは実行OSのRust
PathBufと同じ
全引数の型を先に左から右へ検査し、非Strを1つでも見つけたら結果を返さない。
- 非Strを無言でskipする: typoとデータ欠落を成功扱いするため却下する。
to_strで暗黙変換する: pathに意図しない値表現が入るため却下する。- 常に
/で結合する: host filesystem APIとしてWindowsの意味論とずれるため却下する。
BuiltinSpec(path_join) は arity=Variadic(0)、argument_contract=All(Str) を持つ。src/builtin_core.rs の実装は検査済み &[Value] からのみPathBufを構築する。tree/VMはAUD-049の同じregistry/handlerを使い、固有処理を持たない。
最初の非Str引数について builtin_type、path_join の第 {position} 引数は Str である必要があります: {actual_type}。lineはcall式。後続引数は既に言語式として評価済みだが、結合処理は開始しない。
非Strが無視される挙動に依存したsourceはerrorになる。明示的に to_str を呼ぶ。
0/1/N、空文字、absolute component、./..、Unicode、separatorを含むcomponent、各positionの非StrをOS別に確認する。OS依存期待値はRust PathBufでtest側も構築し、固定separator文字列にしない。
- 全正常caseがPathBuf相当となる。
- 非Strを一切skipしない。
- tree/VMのkind/message/lineが一致する。
実装状況(2026-09-09): 🟡 部分実装。Float→Int 変換(
to_int/floor/ceil/round)と file_size の u64→i64 変換を checked helper へ集約し、NaN/±Infinity/ i64 範囲外を lossy なascast に頼らずconversion/int_overflowエラーにした。src/builtin_core.rsのchecked_float_to_i64(RoundMode)とchecked_file_size_to_i64を tree/VM 共有 handler として実装し、観測挙動が変わるためlanguage-spec.mdへ反映し仕様revisionを0.17へ上げた。paired test はtests/canonical_error_inventory.rs(tree/VM 一致)・tests/checked_conversion_contract.rs(丸めモード・境界)・builtin_coreのaud_036_tests(private file_size helper のi64::MAX+1/u64::MAX)で固定した。未実装(本節の残り):
exitの構造化ExecutionOutcome::Exited { code, usage }化。 これはBudgetUsageと REV-023(exit()の process 終了廃止・Phase 3 budget 基盤)に 依存するため、本節の他項目とは切り離し、REV-023 と同一マイルストーンで実装する。 それまでexitは従来どおり process を終了する(language-spec.mdの記述は据え置き)。
Float入力はfiniteで、各演算後の数学値が [-2^63, 2^63) に入る場合だけIntへ変換する。
to_int: 0方向へ切り捨てfloor: 負の無限大方向ceil: 正の無限大方向round: 最も近い整数、ちょうど中間は0から遠い側
NaN、±Infinity、演算後にi64範囲外となる値は conversion error。-0.0 は0になる。Float以外に対する各builtinの既存受付型は変えない。
OSの u64 sizeを i64::try_from 相当で検査し、i64::MAX を超えた場合は int_overflow error。負値やwrapを返さない。
exit は0個または1個の引数を受ける。0引数の exit() は終了code 0、1引数ではIntだけを受け、値域は 0..=255。2個以上はcanonical arity error、1引数が非Intなら builtin_type、範囲外Intは argument error。coreはprocessを直接終了せず ExecutionOutcome::Exited { code: u8, usage: BudgetUsage } を返し、CLIだけが対応するprocess exit codeへ変換する。
- Rust
ascastの飽和・0化・wrapを仕様化する: 入力ミスと境界超過が成功値になるため却下する。 - すべて
int_overflowにする: 値表現変換失敗、filesystem境界、引数範囲の区別を失うため却下する。 - exitをi32全域にする: OS間で観測終了コードが一致しないため却下する。
- embedding時にもprocess exitする: host processの安定性に反するため却下する。
共通helper checked_float_to_i64(value, mode) がfinite検査→丸め→半開区間検査→変換を行う。i64::MAX as f64 は2^63へ丸められるため上端として受理しない。file sizeは共通checked helperへ分離し、sparse fileなしでもunit test可能にする。
exit(valid) はruntime errorではなくterminal outcomeであり、REPLではlanguage-stateをcommitして該当sessionを終了する。exit(invalid) は通常runtime errorで、未捕捉ならAUD-024に従いrollbackする。
src/builtin_core.rs: checked Float helper、file size helperを共有する。src/builtin.rs,src/vm.rs: exitの重複cast/process exitを削除し、同じoutcomeへ変換する。src/engine.rs:ExecutionOutcome::Exited { code: u8, usage: BudgetUsage }を追加する。src/main.rs: outcomeをCLI exit codeへ変換する。
- NaN:
conversion/{builtin} で Int に変換できません: NaN - Infinity:
conversion/{builtin} で Int に変換できません: 非有限値 - 範囲外:
conversion/{builtin} で Int に変換できません: i64 範囲外 - file size:
int_overflow/ファイルサイズを Int で表現できません - exit非Int: AUD-019のbuiltin argument型
- exit範囲外:
argument/exit の終了コードは 0 から 255 の範囲で指定してください: {code}
NaN/Infinity/範囲外が0や端値になっていたsourceはerrorになる。exit() は従来どおり0を要求するためsource移行不要。範囲外exit codeに依存したshell scriptは0..255へ修正する。embedding hostは Exited を明示処理する。
各Float builtinについて0、±0.5、±1.5、i64最小、2^63直前の表現可能値、±2^63、NaN、±Infinityを確認する。file size helperは i64::MAX と i64::MAX+1、exitは0引数、型違い、2引数、-1、0、255、256、i64極値を確認する。
- 対象経路にlossy
as i64/as i32がない。 - tree/VMの値またはerrorが全境界で一致する。
exitがlibrary host processを終了しない。
Value表現を次へ変更する。
List = Rc<Vec<Value>>
Dict = Rc<BTreeMap<String, Value>>
全mutationは Rc::make_mut を通す。cloneはhandle共有、書き込み時だけbacking collectionを複製する。言語の観測可能な値snapshot意味論を維持し、参照alias意味論へ変えない。
let b = aでList/Dictを代入した直後は内部backingを共有してよい。b[0] = x、push(b, x)等でbを変更するとdetachし、aは変化しない。- 同じbinding cellをclosureが共有している場合、そのcellへのmutationは全closureから見える。
- forは開始時のRc snapshotを保持する。loop中に元bindingを変更するとmutation側がdetachし、反復列は開始時の要素を維持する。
- nested collectionも各階層で同じ値意味論を持つ。
- equality、display、iteration order、collection limitは変えない。
- List/Dictを共有可変objectにする:
let b=a後のmutationがaへ漏れ、現行意味論を破壊するため却下する。 - AUD-041の専用opcodeだけを増やす: callを含むindex、upvalue、REPL journal等のdeep cloneが残るため却下する。
- unsafeなinterior mutation: snapshotとrollbackを破壊するため禁止する。
Read/Clone: Rc::clone(backing)
Write(binding):
value = bindingの現在Value
vec/map = Rc::make_mut(backing) # strong_count > 1ならdetach
検査後にmutation
同じbindingへValueを書き戻す、またはcell内で置換
collection上限とindex/key検査はmutation前に完了し、error時にdetach済みの同値backingを残しても観測結果は変えない。可能なら検査後に make_mut して不要copyを避ける。
src/value.rs: variant、Clone/PartialEq/Display/Debug/truthiness/typeを更新する。src/builtin_core.rs:assign_index、push/pop、sort/reverse等の全mutationをmake_mut化する。src/eval.rs,src/builtin.rs: pattern matchとfor snapshot、index readを更新する。src/vm.rs: constant/local/upvalue clone、index、collection opcode、REPL journalを更新する。src/compiler.rs: AUD-041 opcodeは正当な高速pathとして残せるが、意味論の正しさを専用opcodeへ依存させない。- heap budget実装時は同じbacking allocationを参照数分重複計上せず、detach時に新規allocationを課金する。
新しいerrorは追加しない。allocation failureをcatch可能なscript errorへ変換できないRust allocator環境では、総heap budgetでallocation前に拒否することをclass/HTTPより先に実装する。
sourceの観測挙動は変えない。libraryで Value::List(Vec<_>) / Dict(BTreeMap<...>) を直接構築・matchするコードはRc表現へ追従する。
- alias分離: List/Dict/nested、function argument、return、closure、global
- mutation: index、push/pop、新規Dict key、既存key、callback内
- snapshot: for中の再代入・push/pop・index write
- equality/display: shared/detachedで同値
- rollback: 未捕捉error前のmutation
- scaling:
d[to_str(i)]、upvalue read、REPL checkpointの確保量が線形
- 全mutation経路が
Rc::make_mutまたはそれを包む共通helperを通る。 - alias分離とfor snapshotが現行規範どおり。
- AUD-041で残った複雑index/upvalueの二次確保を解消する。
- paired goldenとallocation scaling gateが通る。
✅ 実装済み。src/value.rs の Value::List を Rc<Vec<Value>>、Value::Dict を
Rc<BTreeMap<String, Value>> へ変更した。全 mutation 経路(builtin_core::assign_index・
builtin_push / builtin_pop / builtin_pop_update、VM の ListPush / DictInsert
opcode)は検査後に Rc::make_mut で detach する。PartialEq に Rc::ptr_eq の高速パスを
足したが等価性の結果は変えていない。for は (**list).clone() で開始時点の要素を snapshot
する。観測挙動は不変で、既存の paired golden・error inventory・defensive test はすべて通る。
受入基準の alias 分離と snapshot は golden fixture collection_cow_alias(代入・引数・
返り値・nested・closure 共有 cell・equality)と既存の for_iteration_snapshot で固定し、
d[to_str(i)](関数呼び出しを含む index 読み)と upvalue 経由 xs[i] の確保量が線形に
収まることを tests/scaling.rs の cow_read_allocation_stays_linear_in_both_engines で
固定した。総 heap budget の allocation 課金統合は Phase 3(実行予算・協調実行仕様)で別途扱う。
関数値は関数式・関数定義を実行して値を生成するたびに、ExecutionContext内で単調増加する FunctionId(u64) を1つ割り当てる。clone、変数代入、引数渡し、collection格納は同じIDを保持する。tree/VMの関数等価性はFunctionIdだけで判定する。
同じsource上の fn をloopやfactoryで複数回評価した値は、captureの有無に関係なく異なる。aliasは等しい。
- AST/Chunk pointer identity: VMのcaptureなし関数で生成instanceを区別できないため却下する。
- capture cell列も比較する: closure identityと環境の構造比較を混同するため却下する。
- 関数は常に不等:
f == fの反射律を壊すため却下する。 - rollback時にcounterを戻す: 失敗入力とauditで一度使ったIDを再利用するため却下する。
ExecutionContext {
next_function_id: u64,
}
FunctionValue {
id: FunctionId,
code: TreeFnDef | VmChunk,
captures: ...,
}
InstantiateFunction(prototype, captures):
id = allocate_monotonic()
if overflow: internal error before binding公開
return FunctionValue(id, ...)
VM constant tableにはID付き関数値ではなく FunctionPrototype を置く。captureが0件でも実行時に必ず InstantiateFunction / MakeClosure(0) を通す。class methodもclass定義実行時にsource順でIDを割り当てる。rollbackしても next_function_id は戻さない。
src/value.rs: tree/VM関数値へ共通FunctionIdを追加し、PartialEqをID比較だけにする。src/engine.rs: allocatorをExecutionContextへ置く。src/eval.rs: FnDef/Lambda評価時に割り当てる。src/compiler.rs: function constantをprototype化し、captureなしでもruntime instantiate opcodeを生成する。src/opcode.rs,src/vm.rs: FunctionId allocator参照とinstantiate処理を追加する。
u64を割り当てた後さらに必要になりoverflowする場合、internal / 内部エラー: FunctionId を割り当てできません。lineは値を生成しようとした関数式/定義行。通常運用で到達不能でもfault injection testを持つ。
VMでcaptureなしの同じfn式を複数回評価した値が true だった非適合は false になる。comparison_semantics.expected.vm の本件差分を削除する。
named/lambda、capture有/無、factory、loop、import、REPL、clone/alias、List contains、rollback後の再生成、counter overflow、tree/VMを確認する。
- 関数等価性の実装はFunctionId比較だけである。
- captureなしVM関数も評価ごとに新IDを得る。
- cloneはIDを保持し、rollbackでIDを再利用しない。
- AUD-048のbackend別期待ファイルが不要になる。
language-visible builtinを単一の静的 BuiltinSpec registryで定義し、treeの名前解決・dispatch、Compilerのbuiltin判定/lowering、VM dispatch、arity、context metadataをすべてそこから導出する。
概念モデル:
BuiltinSpec {
id: BuiltinId,
name: &'static str,
arity: Exact | OneOf | Variadic { min },
execution: PureCore | Context,
evaluation: EvaluateThenValidate | ValidateBeforeArgs,
mutation_target: None | FirstArgIdentifier,
lowering: Generic | Print | Push | Pop | HigherOrder | Input | Args | Exit,
handler: CoreHandler | ContextHandler,
}
print はlexer上の予約tokenであっても同じregistry entryへ解決する。内部命令 __pop_update はpublic registryへ置かず、別の InternalBuiltinSpec または専用opcodeへ置く。
- macroで3つのmatchを生成するだけ: metadataとdispatchが再び分離しやすいため却下する。
- string名をVM opcodeへ保持する: typoをruntimeまで持ち越すため却下する。opcodeはBuiltinIdを持つ。
- 内部builtinもpublic registryへ混在: scriptから到達可能になる危険があるため却下する。
compile/name resolutionは lookup_public(name) -> Option<&BuiltinSpec> を唯一の入口にする。runtimeは BuiltinId からspec/handlerを引く。arityとpre-evaluation validationはspecが決め、handlerが独自に別messageを作らない。
HostFunction registryは別物であり、BuiltinSpecへHTTP等を追加しない。名前解決はuser binding → public builtin → host registryの優先順を別途固定する。host登録名がbuiltinと衝突した場合は登録時errorとする。
src/builtin_core.rsまたは新規src/builtin_registry.rs: registryとBuiltinIdを定義。src/builtin.rs: 公開名matchを削除しspec dispatchを使う。src/compiler.rs:is_builtin()の名前列挙を削除しregistry lookupを使う。src/opcode.rs,src/vm.rs:CallBuiltin等をBuiltinIdベースにする。- internal pop更新はprivate registry/専用opcodeへ移す。
arity/type messageはAUD-019のregistry由来テンプレートを使う。registry重複、ID重複、公開名に __ prefix、handler欠落はbuild/test時に失敗させ、runtimeなら internal とする。
source意味論は変えない。新builtin追加手順は「handler実装+BuiltinSpec 1 entry+tests」に一本化する。
全public specを列挙し、treeで名前解決、VMでcompile、正しいarity metadata、handler到達、user binding優先、print予約token、internal名非到達、重複なしを自動検査する。
- public builtin名の正本が1つだけである。
- compiler/tree/VMに手書きの公開名一覧が残らない。
__pop_updateをsourceから呼べない。- registry全entryの自動contract testがある。
以下4件は原則として観測可能な意味論を変えない。意味論変更が必要になった場合は、該当AUDまたは別のsemantic decisionとして先に承認する。
ステータス: 実装契約確定・未実装。
採用判断: run_frames を命令fetch、line検査、IP進行、unwindのownerとして残し、巨大な dispatch をカテゴリ別private handlerへ分ける。第一段階では制御型を変えず、外側のexhaustive matchから次へ委譲する。
- arithmetic/comparison
- binding/upvalue/global
- control-flow
- call/closure/class
- stack/constant
- collection/property
- builtin/host boundary
却下案: 一度にdirect-threaded VMへ変更、unsafe dispatch table、return/tryと全opcodeの同時再設計は、意味論退行の切り分けを困難にするため却下する。
状態・不変条件: IPは命令ごとに一度だけ進める。stack effect、line、try handler、call frame、REPL journal、step count、defensive Chunk検査を変えない。各handlerは不正variantを unreachable!() へ送らず、外側matchがvariantを一意に分類する。
変更箇所: src/vm.rs 内helper化を先行し、安定後に src/vm/dispatch/*.rs へ物理分割する。src/opcode.rs は意味論変更時以外触らない。
error: kind/message/line/traceはbyte-for-byte不変。内部不変条件違反は従来どおり構造化 internal。
移行: public API/source変更なし。
テスト行列: 全Opcodeの正常stack effect、不足stack、範囲外operand、line table欠落、try/return、callback再入、REPL rollbackをtable-drivenで検証する。
受入基準: Vm::dispatch の単一巨大matchをカテゴリへ分割し、tests/defensive_vm.rs、全paired fixture、scaling testが無変更で通る。新panic経路がない。
ステータス: 実装契約確定・未実装。
採用判断: exec_stmt をexhaustive routerとして残し、binding、index/property assignment、if、while、for、fn/class、try/catchへprivate helperを分離する。最初は同じファイル内で分割し、visibilityが安定してからmodule分割する。
却下案: AST visitor frameworkへの全面置換、Compilerとの共通HIR導入を同じPRで行う案は、scope/control-flow退行の原因を増やすため却下する。
状態・不変条件: EvalResult::{Val,Return,Break,Continue} とruntime errorを全経路で保持し、scopeは正常/error/return/break/continueで必ずpopする。index assignmentの評価順、loop step count、import internal guard、closure captureを変えない。
変更箇所: src/eval.rs のhelper化。必要なら後続で src/eval/stmt.rs。AST/valueは変更しない。
error / 移行: 観測出力とpublic APIは不変。
テスト行列: statement variant×正常/return/break/continue/error、nested scope、caught error、closure escape、REPLをtree単体とpaired fixtureで検証する。
受入基準: router以外に巨大なStmt matchを複製せず、全scope cleanup testとgoldenが無変更で通る。
ステータス: 実装契約確定・未実装。Phase 2 capabilityの先行縦切り。
採用判断: process-global OnceLock を廃止し、immutableな FilesystemCapability を実行requestへ注入する。libraryのCapabilitySet::empty()はfilesystem deny-by-defaultであり、CLIもsafe/legacy profile builderが必要なDirectoryHandle / FileHandle authorityを明示構築する。ExecutionContextは前executionのcapabilityを保持しない。
operation対応はCapability Model仕様第8.4節を唯一の正本とする。要点は次のとおりである。
| source operation | 必要capability |
|---|---|
source import |
resolverに付与したImport。runtime Readから導出しない |
read_file, read_lines |
Read |
write_file, append_file |
Write + Create |
mkdir |
Create |
list_dir |
Metadata + List |
path_exists, file_size, is_file, is_dir |
Metadata |
remove, remove_dir |
Delete |
rename |
sourceのDelete + destinationのCreate、置換時destinationのDelete |
path_join |
不要(filesystemへ触れない) |
root scriptはhostが選択して読む入力であり、script filesystem capabilityへ暗黙追加しない。script pathは@MOUNT/componentまたはdefault mountへrouteする検証済みRelativePathだけを受理し、absolute path、.、..、空component、backslash、drive prefix、UNCをhost pathへ変換する前に拒否する。
path認可とI/Oは同じroot-bound directory/file handleへbindする。canonicalizeや文字列prefixで検査した後に元pathをstd::fsへ渡すcheck/use分離は禁止する。中間・final・dangling symlinkの扱いはSymlinkPolicyごとにadapterが原子的に保証し、platformが保証できなければSecureResolutionUnsupportedとしてfail closedにする。存在path/不存在pathを許可外rootのoracleにせず、mount・operation不足ではOS call前に同じcapability denialを返す。renameは両endpointのauthorityを検査してから単一adapter callを行う。
CLI互換profileはTSUMUGI_SANDBOX等をCLI起動時に一度だけ読み、coreからambient参照しない。safe profileは明示optionで指定されたrootだけをDenyAll policyのhandleとしてgrantする。legacy profileが広いauthorityを構築する場合も、文字列fallbackではなく同じhandle契約を使い、security warningをstderrへ出す。secure handleを提供できないplatformではprofile構築を拒否する。
却下案: OnceLockをreset可能にする、thread-localへ移す、環境変数をI/Oごとに読む、canonicalize後に元pathを再利用する案は、実行単位のauthorityまたはTOCTOU耐性を満たさないため却下する。
状態・不変条件: 同一processの2 executionが異なるpolicyを持てる。capabilityはstart時にfreezeし、cancel以外の途中revokeはない。read/writeとfinal directory entry操作のsymlink意味論を混同せず、secure resolution不能時に文字列checkへfallbackしない。
tree / VM変更箇所: src/sandbox.rs のprocess-global stateを廃止し、src/engine.rsのrequest、src/eval.rs/builtin.rs、src/builtin_core.rs、src/module.rs、src/vm.rs、src/main.rsへ同じfrozen capabilityを伝播する。詳細型をこれらの箇所で再定義せず、capability文書のadapterを共有する。
error: lexical path不正はcatch可能なcanonical argument error、script操作中のauthority不足とsecure adapter失敗は第3節のcatch可能なcapability / host errorとする。link中にhandlerが存在しないresolver denial/host failureだけは構造化terminal Denied / HostErrorとなる。path全文やhost absolute pathをmessageへ出さない。
移行: embeddingの既定requestはfilesystem denyとなる。CLI safe profileは明示optionだけをgrantし、legacy互換もcoreの環境参照や文字列path検査を復活させない。
テスト行列: 同一processでallow/deny policyを交互実行し、各operation、qualified/default/missing mount、absolute/./../separator/drive/UNC、存在/不存在target、intermediate/final/dangling symlink、全SymlinkPolicy、rename両endpointと置換、import専用authority、tree/VM、並列executionを検証する。secure resolution不能adapterとsymlink交換raceでroot外変更0を確認する。
受入基準: SANDBOX_PATHS/OnceLockがなく、core filesystem/import経路が明示capabilityなしにadapter/OSを呼ばない。全path操作がroot-bound handle契約を通り、文字列check/use fallbackが0件である。
ステータス: 実装契約確定・未実装。
採用判断: 通常testと分離した fuzz/ workspaceを作り、次のtargetを段階導入する。
frontend: bounded UTF-8 bytes → Lexer → Parsercompiler: parse成功Program → Compilervm_chunk: bounded arbitrary Chunk → VMdifferential_pure: 副作用なし生成Programのtree/VM比較evaluator: capability deny、有限budget、fake I/O下での実行
依存は導入時にexact versionでpinし、PR smokeとschedule/manual long runを分ける。crash artifactは最小化し、通常unit/integration regressionへ昇格してからartifactを整理する。
却下案: 無制限sourceをそのままprocess I/O付きCLIでfuzzする、PRごとに長時間fuzzする、crash corpusだけをCI artifactに残して回帰testへしない案は却下する。
状態・安全契約: input bytes、AST node、instruction、constant、collection、step、wall time、memoryをboundedにする。exit/input/filesystem/network はfakeまたはdeny。panic、abort、hang、OOMを失敗とし、成功または構造化errorだけを許す。
変更箇所: 新規 fuzz/Cargo.toml、fuzz/fuzz_targets/*、seed corpus、CIの独立job。root packageの通常runtime依存へfuzzer依存を入れない。
error / 移行: 言語error契約は変えない。既知の意図的差はallowlistではなく、本書対象の差を先に解消してからdifferential targetを有効にする。
テスト行列: random token/UTF-8、深いunary/elif/f-string、巨大operand、欠損line table、不正jump、try/call/closure、limit境界をseedに含める。
受入基準: short smokeがCIで決定時間内に通り、長時間jobを手動/schedule実行できる。発見したcrashは再現可能な通常testへ昇格する。外部I/Oやprocess exitへ到達しない。
設計ステータス: 設計済み・低優先度
データと操作を束ねる最小クラスを導入する。単一・多重を問わず継承、super、override chain、metaclass、static/class methodは導入しない。機能共有はfieldへ別instanceを持つ合成で行う。
class_def = "class" IDENT NEWLINE class_member* "end" NEWLINE ;
class_member = method_def ;
method_def = "fn" IDENT "(" [ params ] ")" NEWLINE block "end" NEWLINE ;
primary = ... | "self" ;
postfix = primary { call_suffix | index_suffix | property_suffix } ;
property_suffix = "." IDENT ;
assignment_stmt = postfix "=" expr NEWLINE ;Parserは左辺postfixを最後まで構築した後、次tokenが = なら完成したASTをassignable targetとして検査する。root nodeが Expr::GetProperty { object, name } なら Stmt::PropertyAssign へ分解する。これにより a.b = v はreceiver a、name b、a.b.c = v はreceiver a.b、name c と一意に決まる。rootがGetPropertyでない場合は、既存のIdent assignmentまたはIdent直下のIndexAssign規則だけを許し、それ以外をparse errorにする。
- class bodyに置けるのはmethod定義、空行、commentだけ。field宣言や実行文は置けない。
classとselfは予約語。initは通常のidentifierだがmethod名として特別なconstructor契約を持つ。- method parameterに
selfを明記しない。暗黙bindingとして提供する。 selfはmethod本体と、その内側で定義されたclosureから利用できる。class外ではparse error。self = valueはparse error。self.field = valueは有効。a.b.c、a.b().c、a[i].bを左結合postfixとして許す。a.b = valueのreceiveraは任意のpostfix式でよいが、一度だけ評価する。class Child < Parent、class Child(Parent)等の継承構文は継承はサポートされていませんというparse errorにする。
Stmt::ClassDef {
name: String,
methods: Vec<MethodDef>,
line: usize,
}
Stmt::PropertyAssign {
object: Expr,
name: String,
value: Expr,
line: usize,
}
Expr::SelfRef { line }
Expr::GetProperty { object: Box<Expr>, name: String, line }
MethodDef {
name: String,
params: Vec<String>,
body: Vec<Stmt>,
line: usize,
}
Stmt::line、AST depth preflight、referenced_names、is_side_effect_free、f-string子Parserを全variantへ対応させる。SelfRef は外側Env名としてcaptureせず、method call時の特別localを参照する。method内closureはそのlocal cellを通常upvalueとしてcaptureできる。
同一class内のmethod名重複は2つ目の定義行でparse error。init は0個または1個だけ。
Class {
name: String,
methods: BTreeMap<String, FunctionValue>,
}
Instance {
inner: Rc<RefCell<InstanceData>>,
}
InstanceData {
class: Rc<Class>,
fields: BTreeMap<String, Value>,
}
BoundMethod {
receiver: Instance,
method: FunctionValue,
}
- Classは定義実行ごとに新しいidentityを持つimmutable値。
- Instanceはreference semantics。
let b = a; b.x = 1はaからも見える。 - BoundMethodはreceiverをstrong参照し、元のinstance変数がscopeを抜けても呼べる。
- ClassとInstanceは同じidentityだけ等しい。BoundMethodは同じInstance identityかつ同じmethod FunctionIdの場合だけ等しい。
- Class/Instanceを構造比較しない。
- truthinessはすべてtrue。
type()はそれぞれ"class"、"instance"、"bound_method"。- displayは
<class {Name}>、<{Name} instance>、<bound method {Name}.{method}>。field値を自動展開しない。
BoundMethodまたはselfをinstance自身のfield/containerへ保存するとRc cycleを構成し得る。cycle collector導入までは既知の保持として扱い、class実装のproduction有効化前に総heap budgetを必須とする。receiverをWeakにしてbound methodの寿命を壊す案は採らない。
ClassDef実行は宣言全体をatomicに扱う。最初に現在scopeへAUD-016のfresh class cellを予約し、値を内部非公開の UninitializedClass sentinelにする。次にmethodをsource順で評価して各methodへAUD-048のFunctionIdを割り当てる。methodは予約済みclass cellを含む、class定義地点のlexical bindingを通常関数と同じ規則でcaptureする。self はcapture対象外。全methodからClassを構築できた後、同じ予約cellをClass値で置換して宣言を公開する。
method生成、FunctionId、BuildClassのいずれかが失敗した場合は、通常file実行を含めscope mappingをClassDef開始前へ戻し、sentinelを外部へ公開しない。再宣言では旧class cellを置換せず、新しい予約cellを作るため、旧classのmethodは旧cell、新classのmethodは新cellを参照する。これによりmethod内の宣言class名参照と再宣言identityが一意になる。VMは新slotを先にcell化してsentinelを置き、method closureをそのcellへcaptureさせ、BuildClass成功後に同じcellへ書く。top-level global registryへの公開は成功後に行う。
import先のClassDefは既存source importと同様にcaller top-levelへ展開される。native moduleやnamespaceをclass importに便乗して導入しない。
Class値はcallableである。
- classに
initがなければ引数0個だけを許す。 initがあればsource上のparameter数をconstructor arityとする。暗黙selfは数えない。- arity検査後、引数を左から右へ評価する。
- 空fieldsのInstanceを生成する。
initをBoundMethodとして1 user frameで呼ぶ。initが暗黙終了またはreturn nullなら、class callはInstanceを返す。- Null以外をreturnした場合はtype errorとし、Instanceを返さない。
obj.init(...) を通常methodとして直接呼ぶこともでき、その戻り値はNullである。これにより return null をearly returnへ使える。
property readは次の順序。
- receiverを評価しInstanceであることを検査。
- fieldsに同名があればfield値を返す。
- Class methodsに同名があればfresh BoundMethodを返す。
- なければ
nameerror。
fieldはmethodをshadowできる。property assignmentは存在しないfieldも作成でき、method名と同名でも許す。field削除は提供しない。Class値自体へのproperty access、static field、methodの差替えは提供しない。
property assignmentはreceiver評価→Instance検査→value評価→first-write journal→field更新の順。receiverが不正ならvalueを評価しない。AUD-024のREPL rollback対象である。
既存の「名前付き関数が宣言名で自身を参照するself-binding」と、予約語 self は別である。
- methodのslot/binding 0: 現在のBoundMethod(宣言名での再帰用。receiverを保持)
- methodのslot/binding 1: slot 0と同じreceiverの暗黙self
- 以後: source parameter
parameter名で self は使えない。treeはmethod frameへ宣言名→現在のBoundMethodをbindingし、VMはBoundMethodをslot 0へ置いて、そのunderlying FunctionValueのcodeを実行する。したがってmethod bodyから宣言名を呼んでもreceiverを失わず、通常のBoundMethod callとしてslot 1へ同じselfが入る。methodをfieldから取り出したBoundMethodはreceiverを保持し、後で呼んでも同じselfを使う。self.method() は通常のproperty lookupを行うため、同名fieldがmethodをshadowしていればそのfieldをcallしようとする。
src/lexer.rs: Class/Self/Dot token。src/parser.rs: class body、method context、postfix dot、PropertyAssign、重複method、継承拒否。src/ast.rs: 新variantと全非再帰traversal。src/value.rs: Class/Instance/BoundMethod、equality/display/type/truthiness。src/eval.rs: ClassDef、GetProperty、PropertyAssign、汎用callable dispatch。src/env.rs: self special bindingは通常cellとしてframeへ注入し、closure capture可能にする。src/main.rs:is_incompleteがclass/endを数える。
CompilerはmethodをFunctionPrototypeとしてcompileし、class定義実行時に全methodをinstantiateする。
BuildClass { name, method_names } # stack上のN methodをpopしClassをpush
GetProperty { name } # receiverをpopしfield/BoundMethodをpush
SetProperty { name } # receiver,valueをpopしmutation、valueをpush
ClassDef statementはfresh slot/cellへ UninitializedClass を置き、method closure生成と BuildClass に成功した後で同じcellへClassを書き、通常宣言として公開する。失敗時はslot/global mappingを宣言前へ戻す。PropertyAssign statementは SetProperty 後のvalueをPopする。
ValidateCall / Call はFnに加えてClass/BoundMethodを扱う。BoundMethod callは物理frameを [bound_method, receiver, args...] とし、CallFrameは bound_method.method のcodeを実行する。これによりcompilerのslot 0/1規則と宣言名再帰を同時に満たす。Class callはInstanceを生成し、init BoundMethod frameに ReturnMode::Constructor(instance) を設定する。initの正常Null return後はcallerへInstanceをpushする。try unwind、trace、depth、step、REPL journalは通常user callと同じ経路を使う。
不正ChunkのBuildClass method数、property name constant、stack不足はpanicせず internal error。
| 条件 | kind | message / line |
|---|---|---|
| class外のself | parse | self はメソッドの中でのみ使用できます / self行 |
| selfへの代入 | parse | self には代入できません / assignment行 |
| class bodyの非method | parse | クラス本体にはメソッドのみ定義できます / 該当行 |
| method重複 | parse | メソッドが重複しています: {name} / 2つ目 |
| 継承構文 | parse | 継承はサポートされていません / class行 |
| 非Instance property read/write | AUD-019のtype |
dot/assignment行 |
| missing property | name |
プロパティが見つかりません: {Class}.{name} / dot行 |
| constructor/method arity | argument |
AUD-019 callable template / call行 |
| init non-Null return | type |
{Class}.init は Null 以外を返せません: {type} / return行 |
BuildClassのmethod stack不足 |
internal |
内部エラー: BuildClass のメソッドスタックが不正です / opcode行 |
BuildClassへ非関数method値 |
internal |
内部エラー: BuildClass に関数以外のメソッド値が渡されました / opcode行 |
| property名constant不正 | internal |
内部エラー: プロパティ名定数が不正です / opcode行 |
| constructor frame戻り状態不正 | internal |
内部エラー: constructor frame の戻り状態が不正です / call行 |
| Instance生成前のlogical heap budget不足 | budget |
heap の上限を超えました (上限: {limit}) / class call行 |
trace frame名は {Class}.{method}。constructor arity失敗ではinit frameを追加しない。
class と self が予約語になるため、既存の同名変数・関数はrenameが必要。. は従来Unknown tokenだったため既存の有効sourceとは衝突しない。List/Dictは値意味論、Instanceはreference意味論であることをmigration guideへ明記する。
- parser: 空class、複数method、重複、禁止body、継承拒否、self文脈、dot chain、property assignment
- construction: init有無、0/N args、implicit/
return null、non-Null return - fields: create/read/update、method shadow、missing、receiver/value評価順
- methods: direct、extracted BoundMethod、receiver生存、宣言名によるdirect/extracted method再帰、method同士、closure capture、self capture
- identity: Class/Instance/BoundMethod alias・別instance、FunctionIdとの関係、contains
- scope: class名のmethod内参照、class再宣言時の旧/new class cell、class構築失敗時の予約cell rollback、block、REPL、import
- control: try/catch、return、callback、depth128、stack trace
- VM defensive: stack不足、invalid method table/property constant
- lifecycle: intentional cycleがheap budgetで停止し、host OOMへ至らない
- grammar、AST、tree、Compiler/VM、error、REPL、importが同一PR系列で完成し、片backendだけをreleaseしない。
- paired matrixでstdout/stderr/value/identity/traceが一致する。
- 継承に関するruntime fieldやopcodeを持たない。
- BoundMethodを取り出してもreceiverが生存する。
- Instance mutationがAUD-024でrollbackでき、総heap budgetでcycle保持を制御できる。
設計ステータス: 設計済み・着手禁止
次をすべて満たし、具体的ユースケースが設計レビューで承認されるまで、HTTP client依存追加、adapter実装、DNS接続testを開始してはならない。
- Roadmap Phase 1: tree/VMを包含するstable Engine/CompiledScript/ExecutionContext/Outcome。
- Phase 2: deny-by-default capabilityとHostFunction registry。
- Phase 3: host call数、request/response bytes、総heap、deadlineを含む実行budget。
- Phase 4: cancellation、同時実行上限、backpressure。
- Phase 5: host注入resolver/clock/I/Oと正式backend境界。
- Phase 6: 決定性・実行時監査仕様の
ExecutionStarted、CapabilityDecision、HostCallStarted、HostCallFinished、BudgetCharged、Yielded、Resumed、Terminalとfail-closed sinkを実装済みである。HTTP専用の別event enumを定義しない。 - 承認対象ユースケースについて、宛先、method、data分類、secret owner、rate、SLO、失敗時処理、運用責任者が文書化されている。
- threat reviewでSSRF、DNS rebinding、redirect、secret漏洩、response size、timeoutが承認されている。
Phase 7のcapability/budget/audit/fuzz matrix通過はrelease gateとする。
HTTPは言語core builtinでもsource import moduleでもなく、core外のoptional host adapterとして提供する。coreはHTTP client crateへ依存しない。hostがadapterのHostFunctionをregistryへ登録し、ExecutionContextへ対応capabilityを付与した場合だけ利用できる。登録と権限付与は別の二鍵とする。
canonical adapter surfaceは1引数のhost functionとする。
let response = http_request({
"method": "GET",
"url": "https://example.invalid/data",
"headers": {"accept": "application/json"},
"body": null
})
request Dict:
| key | 型 | 必須 | 契約 |
|---|---|---|---|
method |
Str | yes | ASCII token、uppercase正規化後policy検査 |
url |
Str | yes | absolute URL |
headers |
Dict<Str,Str> | no | default {}、禁止headerあり |
body |
Str または Null | no | UTF-8 bytes、default Null |
unknown keyは argument error。responseは次のDict。
{
"status": Int,
"headers": Dict<Str, List<Str>>,
"body": Str,
"url": Str
}
header名はASCII lowercase、重複値は受信順List、url は最終URL。response bodyが有効UTF-8でなければ host errorとし、lossy変換しない。binary bodyは次期Bytes型または別adapterまで対象外。JSONの自動parseは行わない。
http_getをbuiltin_coreへ追加: capability、budget、auditを迂回し依存をcoreへ固定するため却下する。- 現行
import "http"をnative moduleに流用: filesystem source importとhost registryの責務が混ざるため却下する。 - URL allow-listだけでSSRF対策完了とする: DNS解決後IP、redirect、proxyで迂回できるため却下する。
- TLS検証無効化optionをscriptへ渡す: secretと通信内容を保護できないため禁止する。
- responseを上限確認後に一括readする: header詐称・chunked responseでOOMし得るため却下する。
- 自動retry: 非冪等requestの重複外部効果を起こすため初版では行わない。
HostFunction dispatcher
-> resolve symbol / validate arity
-> reserve host-call count / deadline / cancellation
-> evaluate the single argument
-> validate request shape/type and header syntax
-> normalize URL and evaluate coarse/target capability
-> reserve request-header/body, redirect budget, audit lifecycle 3 events
-> append+ack HostCallStarted
-> append+ack CapabilityDecision(Allow | Deny)
-> Denyならadapter call 0でHostCallFinished(Denied)をappend+ackしcanonical capability error
-> Allowなら最初のDNS query以前に外部処理を開始
-> resolve DNS via injected resolver
-> validate every resolved address
-> execute through injected transport
-> validate every redirect hop
-> stream/charge headers and body
-> append+ack HostCallFinished(Success | HostError | Cancelled | Detached)
-> return Value or canonical error
symbol/arity、host-call回数budgetは引数評価より前に検査する。URL等の引数値に依存するpolicyはshape/type検査後、外部処理前に検査する。粗粒度またはURL policyの拒否でも、同じcall IDのHostCallStarted、CapabilityDecision(Deny)、HostCallFinished(Denied)を順に記録し、adapter/DNS/transport callを0回にする。allowではCapabilityDecision(Allow)のsink ack後だけ外部効果を開始し、DNS failure/timeout/cancelを含むすべての終了をHostCallFinishedの対応outcomeで1回だけ閉じる。
adapterはEvaluator、Env、VM stackへアクセスしない。共通dispatcherが認可・引数評価・shape検査・audit開始を担当し、adapterへ渡すのは認可済みrequestと狭いcontrolだけである。概念interfaceは次。
HttpAdapter::execute(
approved_request,
HostCallControl { deadline, cancellation, budgets, audit_context }
) -> Result<HttpResponse, SanitizedHostError>
resolver、clock、transport、rate limiterを注入可能にし、unit testで実networkを使わない。
HttpCapability は少なくとも次を持つ。
- 許可method
- 許可scheme(defaultは
httpsのみ、httpは明示grant) - IDNA/ASCII正規化後host allow-list
- port allow-list
- 任意のpath prefix
- redirect許可と最大hop
- request/response byte上限
- connect/read/total timeout上限
- secret injection policy
- proxy policy
URLはabsoluteのみ。userinfoとfragmentを拒否する。曖昧なIPv4表記、zone id、scheme-relative URLを拒否する。host名比較は末尾dotとcaseを正規化し、suffix文字列一致ではなくlabel境界で行う。
- hostnameはinjected resolverでA/AAAA解決し、返った全addressを検査する。
- loopback、private、link-local、unspecified、multicast、carrier-grade NAT、benchmark/documentation、cloud metadataとしてpolicyが禁止するrangeをdefault denyする。
- IP literalも同じ検査を通す。
- 許可済みSocketAddrへconnect先をpinし、TLS SNI/hostname検証にはcanonical hostnameを使う。再解決した未検査addressへtransportが接続してはならない。
- 接続retryで別addressを使う場合も、事前検査済み集合内だけにする。
- proxyはdefault無効。hostが明示構成する場合もproxy先と最終宛先を別々に認可し、環境変数proxyをambientに読まない。
- certificate chainとhostname検証を必須とする。
- scriptからverification disable、任意CA、client certificate pathを指定できない。
- custom trust storeとmTLS secretはhost設定だけで注入し、対象originへbindする。
- TLS error詳細はsecret/pathを除去して
host (... tls)に正規化する。
redirectはdefault無効。許可時も各hopでscheme/host/port/path、DNS/IP、secret、budgetを最初から再検査する。
- 最大hopはcapabilityとbudgetの小さい方。
- 303はGETへ変更しbodyを破棄。
- 307/308はmethod/bodyを維持。
- 301/302でGET/HEAD以外を別methodへ暗黙変更せず、
host (... redirect_policy)error。 - originを跨ぐhopへAuthorization、Cookie、host secretを転送しない。
- redirect loopは
host (... redirect_loop)。
request header名はASCII case-insensitiveで扱い、検査前にlowercaseへ正規化する。nameはRFC token相当のASCIIだけを許し、valueはCR、LF、NULとその他の禁止control characterを拒否する。正規化後のAuthorization、Proxy-Authorization、Cookie、Host、Content-Length、Transfer-Encoding、Connection等のsensitive/hop-by-hop headerはargument-dependent capability denialとし、外部処理前にCapabilityDecision(Deny)とHostCallFinished(Denied)を記録してcanonical capability errorを返す。header name/valueのsyntax不正は argument errorであり、host call lifecycle開始前に判定する。検査順はheader名のcodepoint順とし、各entryでname syntax→forbidden name→value syntaxの順に最初の失敗を返す。
host secretはpolicyにより特定origin・path・methodへbindし、adapterがtransport直前に注入する。secret値はTsumugi Value、error、trace、audit、debug logへ入れない。
responseの set-cookie は初版ではscriptへ返さない。response headerに禁止control characterがあれば、同じcall IDのHostCallFinished(HostError)を記録して host (... protocol) errorとする。
- request header bytes、request body bytes、response status/header count/header bytes/body bytesを個別に制限する。
- responseはstreamし、chunk受信ごとにbudgetを消費する。上限超過時は接続を中断し、残りを読み続けない。
- effective total deadlineはexecution deadline、capability上限、request policyの最小値。
- connect/read timeoutはtotal deadlineを超えない。
- cancellation tokenをDNS、connect、TLS、write、readの各待機へ伝播し、可能な限りtransportをabortする。
- host/tenant/origin単位のtoken bucketと同時実行semaphoreをhostが提供する。rate waitがdeadlineを超える場合は待たずにbudget/timeout error。
- 自動retryは初版なし。将来追加する場合はmethod idempotency、retry回数、追加budget、auditを別決定する。
HTTPは独自eventを定義せず、決定性・実行時監査仕様第7〜11節のhost call lifecycleを使う。各requestは同じoperation ID/call IDのHostCallStarted、CapabilityDecision(Allow | Deny)、HostCallFinished(Success | Denied | HostError | Cancelled | Detached)をこの順で持ち、denyを含め必ずpairを閉じる。allow decisionがackされる前にDNS・transport等の外部効果を開始せず、finishedがackされる前にresponseをscriptへ公開しない。ExecutionStarted、集約BudgetCharged、必要なYielded/Resumed、最後のTerminalも同じschemaを使い、sink failureはFailClosedとする。
eventはexecution ID、script hash、host function名、capability policy、method、redacted origin/path、redirect数、request/response byte、duration、終了categoryを既存fieldへ写像する。query、userinfo、Authorization/Cookie、request/response body、secretは既定で記録しない。
HTTP request完了後に同じexecutionが未捕捉errorでrollbackしてもHTTP効果は戻らない。AUD-024に従い、最終AuditEvent::Terminalへcontext_committed = false、host_effects_may_remain = trueを記録する。host call completionへ独自のlanguage-state fieldを追加しない。
| 条件 | kind | canonical message |
|---|---|---|
| adapter未登録 | name |
未定義の変数または関数: http_request |
| coarse/target capability拒否 | capability |
host function の実行が許可されていません: http_request |
| 第1引数がDictでない | builtin_type |
http_request の第 1 引数は Dict である必要があります: {actual_type} |
必須key method / url がない |
argument |
http_request の必須キーがありません: {key} |
| unknown request key | argument |
http_request に未対応のキーがあります: {key} |
method / url がStrでない |
builtin_type |
http_request.{key} は Str である必要があります: {actual_type} |
headers がDictでない |
builtin_type |
http_request.headers は Dict である必要があります: {actual_type} |
| header valueがStrでない | builtin_type |
http_request.headers の値は Str である必要があります: {actual_type} |
| request header name syntax不正 | argument |
http_request.headers の名前が不正です: {header} |
| request header valueに禁止文字 | argument |
http_request.headers の値が不正です: {header} |
| sensitive/hop-by-hop header | capability |
host function の実行が許可されていません: http_request |
body がStr/Nullでない |
builtin_type |
http_request.body は Str または Null である必要があります: {actual_type} |
| method token / URL syntax不正 | argument |
http_request.{key} が不正です: {reason} |
| DNS/IP/redirect/TLS/protocol/UTF-8/transport | host |
host function の実行に失敗しました: http_request ({category}) |
| byte/call/concurrency budget | budget |
{budget_name} の上限を超えました (上限: {limit}) |
| deadline | timeout |
実行期限を超過しました |
| cancellation | cancelled |
実行がキャンセルされました |
{reason} は invalid_method、invalid_url のいずれか、{category} は dns、address_policy、redirect_policy、redirect_loop、tls、protocol、utf8、transport のいずれかに限定する。OS/libraryの生messageを埋め込まない。request Dictの検査順は method存在→url存在→unknown keyのcodepoint順→method型→url型→headers型/各値のStr型→body型→method token→URL syntax→header名のcodepoint順にname syntax/forbidden name/value syntax→method/scheme/host/port/path policy とし、最初の失敗だけを返す。{header} はlowercase正規化後のheader名で、control characterを含む未正規化値をmessageへ出さない。
lineは http_request(...) call式。host内部traceは出さない。budget/timeout/cancelled はscriptでcatch不能、capability/host/argument/builtin_type はcatch可能とする。
HTTPは新規optional機能であり、adapter未登録contextでは名前未定義のまま。core package、CLI、default ExecutionContextがnetwork権限を暗黙付与してはならない。利用hostはcapability、budget、audit、secret policyを明示設定する。
- registry/capability: 未登録、登録deny、grant、名前衝突、deny時の引数副作用なし
- request shape: 非Dict、必須key欠落、unknown key、各key/nested header/body型、validation precedence
- URL: scheme、userinfo、IDNA、末尾dot、IP literal、曖昧IP、port、path
- DNS: public/private混在、rebind、複数A/AAAA、resolver failure/timeout/cancelと
HostCallStarted→CapabilityDecision→HostCallFinishedのcorrelation - redirect: same/cross origin、loop、hop上限、301/302/303/307/308、secret除去
- TLS: valid、expired、hostname mismatch、unknown CA、mTLS host injection
- header/secret: forbidden request header、control char、set-cookie除外、audit redaction
- body: content-length有無、chunked、exact limit、limit+1、invalid UTF-8
- control: connect/read/total timeout、cancel各phase、rate、concurrency、deadline
- semantics: caught host error、uncatchable timeout、REPL rollback後partial effect audit
- backend: fake adapterでtree/VMの引数順、error、audit一致
実network統合testは隔離したlocal test server/resolverで行い、public Internetへ依存しない。
- 着手gateの承認記録が存在するまでHTTP依存・実装がrepositoryへ入らない。
- core crateはHTTP clientへ依存しない。
- 全接続先がURL policyとDNS後IP policyの両方を通る。
- redirect全hop、secret、stream body、deadline/cancel/rateがfake transportで決定的に検証される。
- canonical host call lifecycle、capability decision、terminalのaudit欠落がなく、sink failure時にfail-closedとなる。
- default context/CLIからnetworkへ到達できない。
本節は semantic-review/Tsumugi-detailed-review-20260907.md の指摘のうち、レビュー時点で設計が不足していた(設計欄が △ または ×)6件の次期実装仕様を定める。レビューが「検討◎/設計◎、実装のみ未」とした指摘(REV-001/002/006/007/013/014/015/016/017/018/019/020/022/023/024/025)は、既存の各設計文書と本書の該当節を正本とし、実装バックログへ直結させる。それらへ本節で新しい決定を追加しない。
レビュー第9.3節の方針に従い、既存 AUD 番号を再割り当てせず、本節の各項目を REV 由来の新規追跡項目として扱う。REV-012 だけは既存の第5節(AUD-017)が正本であり、本節は第5節との整合と status/doc drift の解消のみを扱う。
Int–Float の比較と等価判定は、Int を f64 へ丸めず、数学的に厳密な順序を返す。tree・VM・PartialEq・関係演算子・contains・min・max・sort key 生成が、共通の 1 実装 NumericOrder を経由する。
NumericOrder::compare(&Value, &Value) -> Result<NumericOrdering, TypeError>
NumericOrdering = Less | Equal | Greater | UnorderedNaN
規範挙動は次のとおり。
Int(a)とInt(b)は整数として比較する。Float(a)とFloat(b)は IEEE 754 binary64 として比較し、NaN が絡む比較はUnorderedNaN。Int(a)とFloat(b)は、Float を「符号・指数・仮数」へ分解して、丸めなしで整数側と厳密比較する。有限 Float は数学的実数値として、±Infinityは全ての有限 Int より大小が確定、NaN はUnorderedNaN。- 等価は
compareがEqualを返す場合だけ true。UnorderedNaNは等価でも大小でもない。 min/maxは比較後に選択された元の operand を、その型のまま返す(Int を Float へ変換して返さない)。同値時は第 1 引数を返す。入力に NaN を含む場合は canonical NaN(Float(f64::NAN))を返す。
これにより 2^53 近傍で a == f かつ f == b だが a != b となる非推移性(レビュー REV-003 の例)が解消し、等価は対称・推移的、比較は反対称になる。
- 現行どおり
i64 as f64で比較する:2^53超で桁落ちし、等価の推移性と比較の反対称性が崩れるため却下する。 - Int を常に Float 化した値で
min/maxの結果も返す: 正確な Int operand の値を失うため却下する。 f64 as i64で Float 側を丸めてから整数比較する: 小数部・範囲外・NaN/Inf の順序が壊れるため却下する。
i64 と f64 の厳密比較は次で行う。丸め・浮動小数演算を経由しない。
compare_int_float(i: i64, f: f64) -> NumericOrdering:
f が NaN -> UnorderedNaN
f が +Infinity -> Less # i < +inf
f が -Infinity -> Greater # i > -inf
f を (sign, mantissa_int, exponent) へ分解し、
f == trunc(f) でなければ、trunc(f) と i を整数比較して、
等しければ小数部の符号で決着(正の小数部なら f>trunc、負なら f<trunc)
f == trunc(f) なら trunc(f) を i128/u128 magnitude で表現し i と厳密比較
実装は f64::to_bits() から sign / exponent / significand を取り出し、f.fract() と f.trunc() を使って整数部と小数部を分離してよい(丸めではなく分離であり厳密)。i64 は i128 へ widen して比較する。中間に f64 へ戻す経路を持たない。
src/value.rs:PartialEqのInt×Float/Float×Int分岐をNumericOrder::compareのEqual判定へ置換する。- 新規
NumericOrder(src/value.rsもしくは専用 module): tree・VM・builtin から参照する単一実装。 src/eval.rs: 関係演算子(<,<=,>,>=)の混合数値比較を共通 helper へ。src/vm.rs: relational opcode の混合数値比較を同じ helper へ。src/builtin_core.rs:containsの要素一致、min/maxの比較と戻り値選択、sort key 生成の数値比較を共通 helper へ。
比較不能な型組合せ(数値でない型どうしの大小比較など)は既存の第3.4節「大小比較の対象型不正」の type error を維持する。等価比較は従来どおり型エラーにせず、異型は非等価。NaN 比較は error ではなく false(大小・等価とも)。
2^53 を超える Int と Float を等価・大小比較していた source は結果が変わる(従来 true だったものが false になり得る)。厳密値へ移行する。min / max の戻り値型が「Int を渡せば Int のまま」返るようになる点も移行対象。
- 値境界:
±(2^53-1),±2^53,±(2^53+1),i64::MIN,i64::MAX - 各整数近傍の
next_up/next_down相当 Float -0.0,0.0, NaN,±Infinity- property: 等価の対称性・推移性、comparator の反対称性
min/maxの戻り値型と tie 規則、NaN 入力- tree / VM /
PartialEq/ relational / contains / min / max / sort で結果一致
- 対象経路に
i64 as f64/f64 as i64の混合比較 cast がない。 - 等価が対称・推移的、比較が反対称であることを property test が示す。
- tree / VM の値・error・sort 順が全境界で一致する。
Chunk の可変 builder API(emit / emit_jump / patch_jump / add_constant 等)を公開 panic 面から外す。二段構えとする。
- 最終形:
ChunkBuilderをpub(crate)にし、公開するChunkは immutable かつ検証済み(REV-005/006 のVerifiedChunkへ接続)とする。raw builder はunstable-bytecodefeature でのみ公開する。 - 最小修正(先行可):
patch_jumpをpanic!しないResultへ変更する。
patch_jump(&mut self, offset: usize) -> Result<(), ChunkBuildError>
code.get_mut(offset) が None -> Err(BadOffset)
対象が Jump 系 opcode でない -> Err(NotAJump)
それ以外 -> addr を現在位置へ設定して Ok
ChunkBuildError は BadOffset / NotAJump を持つ列挙型とし、builder エラーは呼び出し元 compiler が内部エラーとして扱う(script からは到達しない)。
patch_jumpの panic を維持する: 公開 API から host を panic させられ、tests/defensive_vm.rsの「公開 API へ任意 Chunk を渡しても host panic しない」方針に反するため却下する。offsetを無検査 index のまま assert だけ足す: release build で assert が消え未定義寄りの挙動になるため却下する。
src/chunk.rs:patch_jumpをResult化。builder 系メソッドの可視性を段階的にpub(crate)へ縮小する。src/compiler.rs:patch_jumpの呼び出しを?伝播へ変更し、builder エラーを compiler 内部エラーへ写像する。src/lib.rs:chunkmodule の公開範囲を縮小し、raw builder は feature gate 下へ移す(REV-018 と共通の封印作業)。
builder エラーが VM 実行時まで漏れた場合(防御的経路)は第3.4節「VM/Compiler 不変条件違反」の internal / 内部エラー: {stable_detail} を用いる。{stable_detail} に offset 生値を入れない。
offset == len,usize::MAX, 通常 opcode, 全 jump variantcatch_unwindで panic が発生しないこと- builder error 発生時に
code/linesが部分破損しないこと
- 公開経路から
patch_jumpで panic しない。 - 不正 offset / 非 jump opcode が
Result::Errになる。 - builder 縮小後、通常 compile 経路の観測挙動が変わらない。
closure の capture を、MakeClosure 直前の隣接 opcode 列から逆算する現行方式をやめ、関数プロトタイプが capture 記述子を明示的に持つ方式へ変更する。
FunctionPrototype {
chunk: Rc<VerifiedChunk>,
captures: Box<[CaptureDesc]>,
}
CaptureDesc = Local(u32) | Upvalue(u32)
MakeClosure はプロトタイプ index だけを operand に持ち、capture の解釈は記述子から直接行う。現行の (true, usize::MAX) フォールバック(不正 operand を Null cell として黙認する経路)を廃止し、記述子が local/upvalue 範囲外・不整合なら即 internal error とする。範囲検証は REV-006 の verifier が担い、VM 実行時は defense-in-depth として再検査する。
- 隣接 opcode からの逆算を維持し、不一致だけ error にする: 命令列の並びに依存する暗黙契約が残り、verifier と二重管理になるため却下する。
- 不正記述子を現行どおり Null cell で握りつぶす: 壊れた capture を持つ closure が「成功」してしまい、観測挙動が未定義化するため却下する。
compiler は各関数定義について capture 集合を確定し、FunctionPrototype.captures へ格納する。VM は MakeClosure(proto_index) で:
proto = current_chunk.prototypes[proto_index] # 範囲は verifier 済み
for cap in proto.captures:
Local(slot) -> 親フレームの local cell を共有(slot は verifier 済み)
Upvalue(index) -> 親フレームの upvalue cell を共有(index は verifier 済み)
Null フォールバックは持たない
src/opcode.rs:MakeClosureの operand を「upvalue 数」から「プロトタイプ index」へ変更する。src/value.rs(VmFn 表現): 関数値がプロトタイプ / capture 記述子を参照する形へ変更する。src/compiler.rs: capture 記述子を明示生成し、GetLocal/GetUpvalue列の暗黙契約を廃止する。src/vm.rs:MakeClosure処理を記述子ベースへ置換し、usize::MAXフォールバックと Null cell 生成を削除する。tree 版 closure 生成と観測挙動を一致させる。
これは opcode と関数値表現の変更を伴うため、REV-006(verifier / VerifiedChunk)と同一マイルストーンで実施する。
記述子不正が実行時まで到達した場合は第3.4節「VM/Compiler 不変条件違反」の internal を用いる。capture 範囲外は verifier が compile/link 段で拒否する。
- 正常系: 0 capture / 単一 local / 単一 upvalue / 多段 upvalue / 複数 capture
- 異常系(raw bytecode): 記述子の local/upvalue 範囲外、記述子順序不正、対応する cell が無い
- 不正列が「成功」も「Null 補完」もされないこと
- tree / VM で closure の観測挙動一致
- capture が隣接 opcode 列でなく明示記述子で決まる。
- 不正記述子が Null cell へ黙殺されず、verifier または内部 error で止まる。
- 正常な多段 capture が tree / VM 一致する。
list_dir は、ディレクトリ列挙開始後の個別 entry 取得失敗を黙殺しない。safe profile では entry error が 1 件でもあれば、部分結果を成功 List として返さず、構造化した host error にする。非 UTF-8 の entry 名を lossy 変換して同一視しない。
- 個別 entry 取得失敗:
hosterror(category はdirectory_read)。 - 非 UTF-8 の entry 名: safe profile では entry 名を String として返せないため、
hosterror(category はinvalid_encoding)とする。将来 opaque entry ID / Bytes 型を導入する場合はそちらで表現する。 - legacy profile では現行互換(失敗時 Null、lossy 名)を adapter 層で提供してよいが、capability denial を Null へ畳まない。
- 列挙は snapshot とし、ordering・件数・bytes budget を adapter 契約に含める(budget は REV-015 / execution-control 側の実装に接続)。
これは次期 Capability Model の DirectoryEntry.name(検証済み String, capability-model.md)とも整合させる。
entries.flatten()のまま部分結果を返す: 「存在しない」という誤認を成功として返すため却下する。- 非 UTF-8 名を
to_string_lossyで返し続ける: 異なる byte 名が同一表示へ写像され、path identity・audit・replay が壊れるため却下する。 - 部分結果を
{ entries, incomplete }として通常 List に見せる: 通常 List への偽装であり、業務処理が incomplete を見落とすため却下する(構造化する場合は明示型で返す)。
list_dir(path):
authorize(path) # capability 拒否は sandbox/capability error(Null にしない)
read_dir 失敗 -> safe: host(directory_read) / legacy: Null
各 entry:
取得失敗 -> safe: host(directory_read) / legacy: skip
file_name が非 UTF-8 -> safe: host(invalid_encoding) / legacy: lossy 文字列
件数/bytes budget 超過 -> collection_limit / budget(既存契約)
snapshot を確定し ordering 済み List を返す
src/builtin_core.rs:builtin_list_dirのentries.flatten()を、各 entry のResultを検査する形へ変更する。to_string_lossyを廃し、非 UTF-8 は profile に応じて error か legacy 変換にする。- profile 判定は capability / ExecutionContext 側の safe/legacy に接続する(REV-014 の request 化と同一基盤)。
- tree / VM は同じ registry / handler(AUD-049)を共有し固有処理を持たない。
- entry 取得失敗:
host/host function の実行に失敗しました: list_dir (directory_read) - 非 UTF-8 名(safe):
host/host function の実行に失敗しました: list_dir (invalid_encoding) - line は
list_dir(...)call 式。OS の生 message を埋め込まない。
第3.4節の host error inventory へ上記 category を追記する。
「一部 entry が欠けた List が成功で返る」挙動、および非 UTF-8 名が置換文字列で返る挙動に依存した source は変わる。legacy profile で当面の互換を取る。
- 個別 entry error を 2 件目だけ注入(adapter fake)
- Unix の異なる invalid-byte 名が同じ lossy 文字列になる case
- partial success を成功 List として返さないこと
- ordering と budget N / N+1
- safe / legacy profile 差
- tree / VM 一致
- entry error が 1 件でもあれば safe profile で成功 List を返さない。
- 非 UTF-8 名を lossy 変換して同一視しない。
- capability denial を Null へ畳まない。
- tree / VM の結果・error・ordering が一致する。
user call の最終評価順の正本は第5節(AUD-017)§5.1 および第3.7節 error precedence 表であり、本節はそれらを新しく変更しない。REV-012 が指摘するのは実装ではなく、次の drift である。
- 現行
language-spec.md・現行実装は「step/depth 検査 → callee 評価」(callee 評価前に検査)である。 src/limits.rsの doc comment と本書 §5.1/§5.6・第3.7節は「callee を先に評価・分類し、user callable だけ step/depth 検査」(callee 先行)を次期契約とする。roadmap.mdは AUD-017 / AUD-050 を「完了」と表示するが、これは深度計数値(127→128)の統一を指し、error precedence の callee 先行化は未実装である。
つまり「境界値の統一は実装済み」「callee 先行への precedence 変更は未実装」の 2 つが 1 つの完了表示に畳まれていることが問題である。
- 最終意味論は第5節どおり callee 先行を維持する(builtin/host function を user 深度で拒否せず、動的 callee の種類を確定してから適切な budget を選べるため)。これは callee の副作用が limit error より前に発生し得る破壊的変更であり、
LanguageRevisionを上げて実施する。 src/limits.rsの doc comment は「current(callee 評価前検査)」と「target(callee 先行)」を混在させず、どちらの revision の契約かを明記する。実装が現行のうちは current を、切替 commit で target へ更新する。- roadmap の AUD-017 / AUD-050 status を、単一の「完了」から次の 2 sub-status へ分割する。
depth-counting(128 user frame 統一): verifiedcallee-precedence(callee 先行への precedence 変更): planned
- tree / compiler / VM / callback の評価順切替は同一 commit で行い、backend 差を残さない。
status 表現は §19(文書全体の最終受入基準)および REV-011(メタデータ単一正本、roadmap.md)の機械可読 status で管理し、手作業同期の再 drift を避ける。
src/limits.rs: doc comment を current/target 明記へ修正する。src/eval.rs/src/compiler.rs/src/vm.rs: callee 先行化を実施する commit で、callee 評価 → callable 分類 → user callable だけ step/depth → arity → args 左→右 → frame/body の順へ揃える。docs/roadmap.md: AUD-017 / AUD-050 の status をdepth-counting=verified/callee-precedence=plannedへ分割する。
precedence 変更後も error kind/message/line は第3.4・3.5・3.7節どおり。深度上限は overflow / スタックオーバーフロー: 再帰が深すぎます (上限: 128)、line は拒否された call 式。
callee の副作用が limit error より前に発生する(従来は検査で先に止まっていた)。callee 式の副作用順に依存する source はこの規則へ移行する。builtin / host function 呼び出しは user depth 128 でも拒否されない点も §5.6 のとおり。
- callee に counter 更新を持つ式(budget 0 / depth 128 で callee 副作用の有無)
- non-callable / wrong arity で argument 非評価
- builtin / host function / callback の分類
- tree / VM の stdout・state・error kind/message/line 完全一致
- tree / VM が callee 先行の同一順序で評価する。
limits.rsdoc comment が current/target を混在させない。- roadmap の AUD-017 / AUD-050 status が 2 sub-status へ分かれ、
callee-precedenceが verified になるまで「完了」と表示しない。
現行 remove_dir は std::fs::remove_dir_all(中身ごと再帰削除)だが、次期 Capability Model は remove_dir を RemoveKind::EmptyDirectory へ割り当てている。この不整合を、EmptyDirectory 権限で再帰削除を許してしまう(過大権限)/非空で突然失敗する(契約違反)のどちらにもしないため、操作を分割する。
- 次期 revision で
remove_dir(path)を空ディレクトリのみ削除へ変更する(std::fs::remove_dir相当)。非空なら error。必要 capability はEmptyDirectory。 - 再帰削除は新規 builtin
remove_tree(path)として分離し、専用 capabilityRecursiveDeleteを要求する。remove_dirへEmptyDirectoryから暗黙昇格させない。 remove_treeは entry 件数 / depth / bytes / fuel / deadline / cancel / audit を持つ(budget・cancel は REV-015 / execution-control 側に接続)。final symlink と中間 symlink の扱いを明示する(symlink 自体を削除し、リンク先を辿って再帰削除しない)。
互換を優先する代替として「RemoveKind::RecursiveTree を明示追加して remove_dir へ割り当てる」案もあるが、remove_dir の名前と再帰削除の危険度が一致しないため、分割案を第一候補とする。
- 現行
remove_dir_allのままEmptyDirectorycapability へ接続する: 空ディレクトリ権限で再帰削除を許す過大権限になるため却下する。 remove_dirを空のみへ変えるが再帰削除手段を用意しない: 既存の再帰削除ユースケースを失うため、remove_treeを同時に用意する。EmptyDirectoryへ暗黙で再帰権限を含める: capability の最小付与原則に反するため却下する。
remove_dir(path):
authorize(path, EmptyDirectory)
std::fs::remove_dir(safe_path) # 非空なら OS error -> 契約に従い false もしくは host error
symlink は remove_dir で扱わず remove(FileOrSymlink)側の責務
remove_tree(path):
authorize(path, RecursiveDelete)
walk しながら entry 件数/depth/bytes/fuel/deadline/cancel を消費
中間・final symlink はリンクを辿らずリンク自体を削除
audit に削除対象数と partial effect を記録
戻り値契約(Null/false vs 構造化 error)は REV-022 の profile 方針に従い、safe profile では denial と OS 失敗を区別する。
src/builtin_core.rs:builtin_remove_dirをremove_dir_allからremove_dir(空のみ)へ変更する。新規builtin_remove_treeを追加する。- capability 層(次期
FilesystemCapability):remove_dir→EmptyDirectory、remove_tree→RecursiveDeleteを割り当てる。 docs/capability-model.md/docs/language-spec.md:remove_dirの意味論変更とremove_tree追加を反映する(実装完了・受入通過後)。- tree / VM は同じ registry / handler を共有する。
- 非空ディレクトリへの
remove_dir: safe profile ではhost(categorydirectory_not_empty)。legacy では現行どおり false。 - capability 拒否: 第3.4節「filesystem capability 拒否」の
sandbox。 remove_treeの budget / cancel:budget/cancelled(catch 不能)。
第3.4節の host error inventory へ directory_not_empty category を追記する。
remove_dir が非空ディレクトリを再帰削除しなくなる。再帰削除が必要な source は remove_tree へ移行し、host は RecursiveDelete capability を明示付与する。これは破壊的変更のため LanguageRevision を上げる。
- 空 / 非空ディレクトリ
- nested tree(
remove_tree) - final symlink と中間 symlink
- partial failure / permission change / cancel(
remove_tree) - capability 隣接操作の deny(
EmptyDirectoryでremove_tree不可、その逆も) - audit で削除対象数と partial effect を記録
- tree / VM 一致
remove_dirが空ディレクトリのみを削除し、EmptyDirectorycapability で再帰削除できない。remove_treeがRecursiveDeletecapability を要求し、budget / cancel / audit を持つ。- symlink をリンク先まで辿って削除しない。
- tree / VM の結果・error が一致する。
設計ステータス: 次期仕様確定・未実装。§17.2(REV-004)・§17.3(REV-005)・REV-018 と同一マイルストーン(第18節ステップ7)で実装する。本節はそれらが「範囲検証は verifier が担う」と委譲している検証層の正本である。
VM が実行する bytecode を、検証を通過した VerifiedChunk に限定する。あわせて、任意の bytecode に対して停止性(step 予算での必ずの終了)が成り立つよう、step 課金を「1 命令 dispatch ごとに 1」へ変更する。この 2 層で REV-006 の 2 つの穴(Jump(0) 等による step 課金迂回、PrepareCall を省いた raw Call による call 課金迂回)を塞ぐ。
- 層1(durable guarantee・bytecode 形状に非依存): VM のメインループは、fetch した全命令の dispatch 直前に無条件で
count_stepを呼ぶ。これにより、検証を通っていない chunk が防御的に VM へ届いた場合でも、任意の命令列は有限 step でlimiterror になる。Jump(0)の自己ループも 1 周ごとに 1 命令以上を dispatch するため必ず課金される。現行の Loop / PrepareCall / callback の 3 箇所だけの課金は廃止する(二重課金しない)。 - 層2(gate・早期拒否と不変条件の確立):
ChunkをVerifiedChunkへ昇格させる際に static verifier を通す。verifier はホストが構築・改変した bytecode の構造的健全性を compile/link 時に一度だけ検査し、VM 実行時の防御的分岐(AUD-023 のrequire_*ヘルパー)が「起こり得るが正常運用では到達しない」経路として残る前提を確立する。
VM の公開入口は VerifiedChunk だけを受け取る。raw Chunk から VM を直接動かす現行の公開経路(tsumugi::vm::Vm::new(chunk) / run_repl_chunk(chunk) と pub mod chunk/opcode/vm)は封印する(REV-018 と共通作業)。
停止性の正本は層1(per-instruction 課金)とする。verifier は「早期拒否・不変条件の明文化・defense-in-depth」であって、停止性を verifier だけに依存させない。verifier を通していない chunk が防御的に実行されても、ホストは panic せず有限 step で停止しなければならない。
- verifier だけで停止性を保証し、課金は現状(Loop/PrepareCall のみ)を維持する: verifier に単一の見落としがあると無期限実行へ直結する。停止性を静的解析の完全性へ依存させるのは脆いため却下する。層1の無条件課金を正本にする。
- 後方ジャンプの到達可能性を解析して「課金経路を必ず通る」ことを保証する: 一般の bytecode で「必ず charged 命令を通る」ことの静的証明は制御フロー解析が重く、生成器の変更に脆い。per-instruction 課金なら形状に依存せず自明に停止するため却下する。
Jumpにだけ課金を追加する:Jump以外の後方遷移(不正なLoopoperand 等)や、分岐で自己参照する構成を取りこぼす。命令種別ごとの課金は網羅が難しいため却下する。- raw
Chunk経路を公開したまま実行時チェックだけ足す: 未検証 bytecode が VM 内部不変条件へ到達し続け、verifier で確立したい「VM は検証済み前提」を崩す。公開入口をVerifiedChunkへ限定する。 - 1 命令 = 1 step で既存 fixture の step 予算が変わることを避けるため、命令に重み付けする: 重み表が opcode 追加ごとに保守対象になり、観測挙動(
limit到達点)の説明が複雑化する。等重み(1 命令 1 step)とし、必要ならTSUMUGI_MAX_STEPSの既定値だけを調整する。
VerifiedChunk(inner: Chunk) # 生成は verify() 経由のみ。inner は非公開。
- VM が参照する code / constants / lines / prototypes への読み取り専用アクセサを持つ
- Clone は検証状態を保つ(再検証不要)
- PartialEq は inner の等価で定義(test 用)
verify(chunk: Chunk) -> Result<VerifiedChunk, ChunkVerifyError>
以下を、関数プロトタイプを含め chunk 木の全 code に対して検査する:
(V1) 行番号表整合: lines.len() == code.len()
(V2) 定数参照: LoadConst(i) の i < constants.len()
(V3) local slot: Get/SetLocal(s) / LenLocal(s) / IndexLocal(s) /
SetIndex(Local(s)) の s が、その chunk の宣言 local 数の上限内
(V4) upvalue index: Get/SetUpvalue(i) / SetIndex(Upvalue(i)) の i が
プロトタイプの capture 数(§17.3 の captures.len())内
(V5) capture 記述子: §17.3 の CaptureDesc の Local(slot)/Upvalue(index) が
親プロトタイプの範囲内(REV-005 の範囲検証をここで実施)
(V6) jump target 範囲: Jump / JumpIfFalse / JumpIfFalseKeep / JumpIfTrueKeep /
Loop / SetupTry / JumpIfGlobalDefined の target が
0 ..= code.len()(= 末尾=暗黙 return 位置を許可)
(V7) builtin id: CallBuiltin(id, _) の id が registry に存在
(AUD-049 の BuiltinId は型で保証されるが、防御的に再確認)
(V8) stack operand: PopN(n) / Print(n) / FStrConcat(n) / CallBuiltin(_, n) /
Call(n) / ValidateCall(n) の n が u32→usize 変換可能
(32-bit 迂回は REV-010 と整合。負の到達は型で排除)
(V9) try 構造: SetupTry の target が catch 先頭として範囲内、
TeardownTry が対応する SetupTry を持つ入れ子であること
検査は一度だけ。成功なら VerifiedChunk、失敗なら最初の違反を ChunkVerifyError で返す。
ChunkVerifyError {
kind: BadLineTable | BadConstant | BadLocalSlot | BadUpvalue
| BadCapture | BadJumpTarget | UnknownBuiltin | BadOperand | BadTryStructure,
// 生値・オフセットは stable_detail へ含めず、種別と位置種別のみ保持する
}
停止性(層1)の状態遷移:
run_frames dispatch loop:
fetch instruction at ip
ip += 1
count_step(line)? # ★ 全命令共通・無条件(REV-006)
dispatch(instruction) # Loop / PrepareCall / callback では追加課金しない
count_step / max_steps / TSUMUGI_MAX_STEPS / step_limit error の定義は変更しない。変わるのは「どこで呼ぶか」だけ(3 箇所限定 → 全命令 1 回)。深度上限(MAX_USER_CALL_DEPTH)は現行どおり PrepareCall と Call の両方で検査し、call 課金は per-instruction 課金に吸収する(Call 命令自体が 1 step 課金されるため、PrepareCall を省いた raw Call でも課金を迂回できない)。
VerifiedChunk は VM・関数プロトタイプ・REPL 差し替えの各所で Chunk を置き換える。VM 内部の require_* ヘルパー(AUD-023)は削除せず defense-in-depth として残す。verifier を通した chunk では発火しないことをテストで確認する。
- 新規
src/verifier.rs(またはsrc/chunk.rs内verify):verify(Chunk) -> Result<VerifiedChunk, ChunkVerifyError>とVerifiedChunkを定義する。プロトタイプ木を再帰的に検査する。 src/chunk.rs:VerifiedChunkの読み取り専用アクセサを定義し、builder(emit/patch_jump等)は §17.2 に従いpub(crate)へ縮小する。src/opcode.rs: §17.3 のMakeClosure(proto_index)とFunctionPrototypeを導入する(同一マイルストーン)。src/vm.rs:- メインループの
count_stepを全命令 dispatch 前の 1 回へ移す。Loop/PrepareCall/call_fn_valueの個別count_stepを削除する。 Vm::new/run_repl_chunkの受け取りをVerifiedChunkへ変更する。VM 内部が参照する code / constants / lines / prototypes をVerifiedChunkのアクセサ経由にする。require_*ヘルパーは残す(防御的経路)。
- メインループの
src/compiler.rs:compile/compile_repl_lineの戻り値をVerifiedChunkにする(生成直後にverifyを通す)か、compiler が生成する chunk は不変条件を満たすため verify を skip してVerifiedChunkを直接構築するpub(crate)経路を用意する。どちらでも「VM へ渡る型はVerifiedChunk」を守る。src/lib.rs:chunk/opcode/vmmodule の raw 公開を封印する(REV-018 と共通)。公開 facade はEngine系のみ。raw builder / raw VM 経路はunstable-bytecodefeature 下でだけ露出する。src/module.rs/src/engine.rs: link 済みプログラムを compile→verify→VM の順に流す経路をVerifiedChunkで通す。
- verifier 拒否(compile/link 段): 第3.4節「VM/Compiler 不変条件違反」の
internal/内部エラー: {stable_detail}を用いる。{stable_detail}はChunkVerifyError.kindに対応する固定文字列(例:bytecode 検証に失敗しました: jump target が範囲外です)とし、オフセットや operand の生値・スタック内容を含めない。script からは到達しないため trace は付けない。 - 防御的に未検証 chunk が VM 実行時の
require_*へ到達した場合: 従来どおりinternal(AUD-023 の各メッセージ)を維持し、host panic させない。 - step 上限到達: 既存の
limit/ステップ上限に達しました (上限: {limit})を変更しない。per-instruction 課金化により到達点(step 数)は変わるが、kind・message template・line 規則は不変。
TSUMUGI_MAX_STEPSの既定値と、step 数に依存する fixture(error_step_limit等)は、per-instruction 課金で到達点が変わるため再調整する。観測される kind・message は変えない。- ホストが
tsumugi::chunk::Chunkを raw 構築してtsumugi::vm::Vmへ渡していた利用は、封印によりunstable-bytecodefeature が必要になる。安定利用はEnginefacade へ移行する。 tests/defensive_vm.rsは「未検証 chunk を防御的経路(unstable-bytecodeまたはテスト内部 API)で VM へ渡しても host panic しない」方針を維持する。加えて REV-006 の 2 ケース(Jump(0)自己ループ・PrepareCallなし rawCallループ)を追加し、いずれもlimiterror で有限停止することを固定する。
| 軸 | ケース |
|---|---|
| 停止性(層1) | Jump(0) 自己ループ / 後方 Loop 迂回 / PrepareCall なし raw Call ループ / 巨大 operand なし純命令ループ。いずれも limit で有限停止 |
| verifier 拒否(層2) | V1〜V9 各違反を最低 1 ケース。最初の違反種別が ChunkVerifyError になる |
| verifier 通過 | 正規 compiler 出力(if/while/for/関数/closure/try/f-string/builtin)が verify を通り、観測挙動が現行と一致 |
| defense-in-depth | verify を通した chunk では require_* が発火しないこと(fault injection でのみ発火) |
| backend | per-instruction 課金後の step 到達点を tree(該当する範囲)と VM で説明可能にし、VM の limit error が message/line 一致 |
| 公開境界 | unstable-bytecode feature 無効時に raw Chunk→VM 経路がコンパイル不能/到達不能であること |
全ケースで終了コード・stdout・stderr・kind・message・line を検査する。既存 golden の step 到達点変化は fixture 側で吸収し、backend 別期待値を新設しない。
- VM の公開入口が
VerifiedChunkだけを受け取り、安定 API から rawChunkを VM へ渡せない。 - 全命令 dispatch ごとに 1 step 課金され、
Jump(0)自己ループ・PrepareCallなし rawCallループが有限 step でlimiterror になる。 - verifier が V1〜V9 の構造違反を compile/link 段で
internalとして拒否する。 - verifier を通していない chunk が防御的に実行されても host が panic せず有限停止する(停止性は verifier に依存しない)。
- 正規 compiler 出力の観測挙動(step 到達点を除く)が変わらず、tree / VM で kind・message・line が一致する。
- §17.2(REV-004)builder 封印・§17.3(REV-005)capture 記述子と同一 build へ統合し、旧 API・旧フォールバックを併存させない。
次期revisionは以下の順で進める。各段階でfmt、Clippy、全test、paired golden、defensive testを通し、意味論変更と内部リファクタを同じcommitへ混在させない。
- 基準固定: 本書のerror templates・line/trace・評価順をtest helperへ定義する。現行非適合fixtureを明示する。
- 内部リファクタ: VM dispatch分割、tree
exec_stmt分割。観測挙動は変えない。 - 上限統合: AUD-050の
limits.rs集約とAUD-017の128 user frame統一。§17.5(REV-012)のcallee先行precedenceへの切替とlimits.rsdoc comment のcurrent/target明記、roadmap statusの2分割もここへ含める。 - Builtin/error基盤: AUD-049単一registry、AUD-019共通error constructorと完全一致test。§17.1(REV-003)の
NumericOrder集約、および §17.4(REV-009)・§17.6(REV-021)で追加するhost error category(directory_read/invalid_encoding/directory_not_empty)のinventory登録もここで行う。 - 値表現: AUD-047 List/Dict COW、続けてAUD-048 FunctionId。scaling/identity testを先に追加する。
- binding/transaction: AUD-016 VM fresh cell、AUD-024全language-state REPL journal。FunctionId counterをrollback対象外に固定する。
- bytecode検証面: §17.7(REV-006)の
VerifiedChunk/verifier と per-instruction step 課金を軸に、§17.2(REV-004)のpatch_jumpfallible化とbuilder封印、§17.3(REV-005)のMakeClosurecapture記述子化、REV-018のraw module封印を同一マイルストーンで実施する。VM入口をVerifiedChunkへ限定し、停止性はper-instruction課金(verifier非依存)で担保する。opcodeと関数値表現の変更を伴うため境界挙動より前に置く。 - 境界挙動:
AUD-034(✅ 完了、revision 0.16)、AUD-036 checked変換/Exited、AUD-018 CLI args/stdin、AUD-033 EOF診断。§17.1(REV-003)の混合数値比較の観測挙動変更もここでrevisionを上げる。path_join - capability縦切り: sandbox OnceLockをExecutionContext FilesystemCapabilityへ移し、tree/VM/importを同じpolicyへ接続する。§17.4(REV-009)の
list_dir部分失敗/非UTF-8、§17.6(REV-021)のremove_dir(空のみ)/remove_tree(RecursiveDelete)分割をこのcapability面で実装する。 - 検証基盤: cargo-fuzzのfrontend/compiler/vm_chunk、続いてcapability完成後にevaluator/differential target。
- 次期仕様反映: 全受入基準通過後にだけ
language-spec.md、LANG_GUIDE.md、設計文書、revisionを更新する。 - クラス: 上記基盤と総heap budget完成後、低優先度機能としてlexer→AST→tree→VM→paired testの順で実装する。継承は含めない。
- HTTP: 現在の実装順には含めない。Phase 1–6完了と具体ユースケース承認後にgateを再評価し、承認された場合だけ別計画を作る。
- AUD-016/017/018/019/024/033/034/036/047/048/049/050の採用判断、却下案、規範挙動、状態、変更箇所、error、移行、test、受入基準が実装PRから追跡できる。
- §17のREV-003/004/005/006/009/012/021についても、同じ8観点(採用判断・却下案・データモデル・変更箇所・error・移行・test・受入基準)が実装PRから追跡できる。REV-012は既存の第5節(AUD-017)を正本とし、本書で新しい意味論を追加しない。REV-006(§17.7)は停止性をper-instruction課金で担保し、verifierを唯一の停止性根拠にしない。
- tree/VMで意図しないbackend別期待値が残らない。
- 現行
language-spec.mdは実装完了まで変更せず、現行挙動の正本として維持する。 - クラスは「設計済み・低優先度」、HTTPは「設計済み・着手禁止」として扱われる。
- 本書の承認だけをもってコード実装済み、security sandbox完成、HTTP利用可能とは表示しない。