# Culebra 標準ライブラリ
本書はCulebraの**組み込みライブラリのAPIリファレンス**です。
ランタイムユーティリティをまとめた名前空間オブジェクト
(`Math`, `IO`, `Sys`, `FS`, `Time`, `Args`, `Random`, `String`)
を対象とします。ここに記載のものは`import`文なしで利用できます。
実例つきの導入とイディオムは [`handbook.ja.md` §14](handbook.ja.md#14-標準ライブラリ)
を参照してください。
言語レベルの組み込み関数(`to_long`, `to_float`, `to_string`,
`type_of`, `range`, `iota`, `repeat`)は [言語仕様 §19](language.ja.md)
を参照してください。matcher一族 (`assert_true` / `assert_eq` /
`assert_throws`等) は [§13 Matchers](#13-matchers) で扱います。
組み込み型(`String`, `Array`, `Object`)のメソッドは
[言語仕様 §18](language.ja.md) に規定されています。
これに加えて、`inspect`・`print`・`println`が`IO.inspect` / `IO.print` /
`IO.println`のエイリアスとしてグローバルに束縛されます([言語仕様
§22](language.ja.md)参照)。これは無条件なので、埋め込みで動かす
プログラムからも、スクリプトと同じように見えます。
本書で用いる記法:
* 型注釈は[言語仕様 §14](language.ja.md) に従います。`Any`は任意の
値を示します。
* 例外条項は`type error at L:C.`等の実行時エラーを示します。
[言語仕様 §15](language.ja.md) を参照。
* 名前空間は**閉じて**います:名前空間が持たないメンバーを読むと、`nil`を
返すのではなくアクセス地点で
`AttributeError: namespace 'IO' has no member 'read_all'`を送出します。
これにより、タイプミスや廃止APIの使用が、後続の呼び出しでの分かりにくい
失敗ではなく、即座に捕捉可能なエラーになります。(プレーンなdictは寛容な
ルールのまま — 未定義キーは`nil`。)dictの組み込みメソッド
`keys`/`values`/`has`/`get`/`size`は名前空間でも従来通り使えます。唯一の
例外は`Path`で、これは名前空間ではなくクラスなので、未定義プロパティは
他のクラスと同様に`nil`を返します。
## 目次
1. [`Math`](#1-math) — 数値ユーティリティ・定数・整数列
2. [`IO`](#2-io) — 出力・標準入力(ファイルI/Oは`FS`)
3. [`FS`](#3-fs) — パス操作・ファイル/ディレクトリ問い合わせ・更新
4. [`File`](#4-file) — ストリーミング読み書き/seekの状態付きハンドル
5. [`Time`](#5-time) — `Instant` / `Duration`クラス、ISO 8601、カレンダー算術、ナノ秒精度
6. [`Random`](#6-random) — シード可能なPRNG(uniform / gauss / shuffle / choice / weighted_choice)
7. [`Sys`](#7-sys) — argv / exit / env / executable。`GC`ヒープ情報の取得も同節
8. [`Tensor`](#8-tensor) — N次元数値テンソル、BLAS対応lazy graph
9. [`JSON`](#9-json) — stringify / parseの相互変換
10. [`Args`](#10-args) — 宣言的なCLI引数パーサ (positional / option / subcommand / `--help`)
11. [`Proc`](#11-proc) — 外部コマンドを同期実行しstdout/stderr/終了コードを取得
12. [`Isolate`](#12-isolate) — クロージャを別スレッド(独立ヒープ)で実行、値は境界でコピー。`Channel` / `Parallel` / `Signal`(Ctrl+Cをチャネルへ振り向け)/ `SharedBuffer`(zero-copy共有する固定レイアウトデータ)/ `Shared`(参照共有するimmutable値)も同節
13. [Matchers](#13-matchers) — `assert_true` / `assert_eq` / `assert_throws` / `assert_close`一族
14. [`Regex`](#14-regex) — 線形時間・grapheme単位の正規表現
15. [`Http`](#15-http) — 同期HTTP/HTTPSクライアント(get/post/put/delete/head/request)、サーバー(ルーティング・静的ファイル・WebSocket)、Server-Sent Events
16. [`Encoding`](#16-encoding) — スキーム別のテキストコーデック(`Encoding.html`、`Encoding.base64`、`Encoding.hex`、`Encoding.url`、`Encoding.utf16`)
17. [`Compress`](#17-compress) — データ・ファイルのgzip / deflate圧縮/展開と、ファイルをZIPアーカイブに詰めること
18. [`Hash`](#18-hash) — SHA-256/SHA-1/SHA-512/MD5ダイジェストとHMAC(hex出力)
19. [`CSV`](#19-csv) — RFC 4180流のCSVをparse / stringify
20. [`Env`](#20-env) — dotenv形式の`.env`をparse / load
21. [`UUID`](#21-uuid) — v4(ランダム)/ v7(時刻順)UUID生成
22. [`Term`](#22-term) — TUI向けの端末の色・カーソル制御・サイズ・キー/マウス入力
23. [`Log`](#23-log) — stderrへのレベル付き構造化ログ(text / JSON、child logger)
24. [`TOML`](#24-toml) — TOML設定をparse / stringify
25. [`XML`](#25-xml): XMLをparse / 検索(XPathの部分集合) / stringify。大きな文書向けの遅延イベント列
26. [`SQLite`](#26-sqlite) — 組み込みSQLデータベース(query / execute / プリペアド文 / トランザクション)
27. [`Canvas`](#27-canvas) — ゲーム向けイミディエイトモード2Dフレームバッファ(図形 / スプライト / オフスクリーン描画先 / テキスト / キー・マウス・ゲームパッド / ウィンドウ制御)
28. [`Scene`](#28-scene) — 手続きジオメトリ向けのretained-mode 3Dレンダラ
29. [`Net`](#29-net) — 生のTCP / UDPソケットと名前解決(`Http`の下位レイヤ)
30. [`Desktop` / `Webview`](#30-desktop--webview) — ネイティブWebViewのデスクトップアプリ: ローカルHTTPサーバ + ウィンドウを1呼び出しで
31. [`Vector2`](#31-vector2) — グラフィックス/ゲーム向けの最小限の2D floatベクトル(「Point」の代わりも兼ねる)
32. [`Vector3`](#32-vector3) — `Vector2`の3D版
33. [`Deque`](#33-deque) — 両端キュー、両端のpush/popが償却O(1)
34. [`PriorityQueue`](#34-priorityqueue) — 二分ヒープ、push/popがO(log n)
35. [`PEG`](#35-peg) — PEGパーサジェネレータ。文法を書くと構文木が返る
36. [`CodeGen`](#36-codegen) — 小さな言語のIRを手で組み立てて実行する
37. [`StateMachine`](#37-statemachine) — 入れ子にできる状態機械。テキストでも書ける
38. [`FST`](#38-fst) — 書き換えない辞書を圧縮して持つ。前方一致・補完・あいまい検索
39. [`Search`](#39-search) — 自分の文書を全文検索して順位をつける
40. [`Audio`](#40-audio) — ウィンドウの有無によらず使える音: WASM-4のtone、効果音、ストリーム再生の音楽、合成するPCM、マイク
41. [`Dir`](#41-dir) — ディスク・メモリ・バイナリへの焼き込み・ZIPアーカイブのどこにあるファイルの集まりも、同じ読み方で(`Dir.disk` / `Dir.memory` / `Dir.embedded` / `Dir.zip`と`Dir` trait)
42. [設計上の注記](#42-設計上の注記)
43. [未収録(将来検討)](#43-未収録将来検討)
**目的別索引**
| やりたいこと | 参照先 |
|---|---|
| 定数(π、e、inf、nan) | [§1 Math 定数](#math-pi) |
| スカラー演算(abs / min / max / log / exp / sqrt / floor / ceil / round) | [§1 Math](#1-math) |
| 三角関数(sin / cos / tan / asin / acos / atan / atan2、ラジアン) | [§1 Math](#1-math) |
| 標準出力 | `IO.inspect`(改行 + クォート付き) / `IO.println`(改行、生) / `IO.print`(生、改行なし) |
| 呼び出しが印字した内容を読み戻す | [§2 IO](#2-io) — `IO.capture(fn)` |
| ファイル全体を読む | `FS.read`(失敗時throw) |
| ファイルをストリーム(行 / チャンク / seek) | [§4 File](#4-file) — `File.open` / `File.with` |
| パス操作(join / basename / dirname / stem / extension) | [§3 FS](#3-fs);流暢な`Path`ラッパ: [§3 `Path`](#path--流暢なラッパ) |
| stat / walk / glob / copy / rename / symlink / chmod / chown | [§3 FS](#3-fs) |
| ディレクトリの変更監視 | [§3 FS](#3-fs) — `FS.watch` |
| ディレクトリ列挙・作成・削除 | `FS.list_dir`、`FS.mkdir`、`FS.remove` |
| ZIPアーカイブ、バイナリに焼き込んだアセット、メモリ上のファイルを同じやり方で読む | [§41 Dir](#41-dir) — `Dir.zip(path)` / `Dir.embedded("assets")` / `Dir.memory({...})` |
| `Instant` / `Duration`クラス、ISO 8601、カレンダー算術 | [§5 Time](#5-time) |
| 負になりうるインデックスを`0..n`に巻き戻す | [§1 Math](#1-math) — `Math.wrap(i, n)`(`%`は切り捨てなので負のまま) |
| 乱数 | `Random.int`、`.uniform`、`.gauss`、`.shuffle`、`.choice`、`.weighted_choice` |
| CLI引数解析 | [§10 Args](#10-args) |
| プロセス情報 | `Sys.argv`、`Sys.exit`、`Sys.env`、`Sys.set_env`、`Sys.getcwd`、`Sys.chdir`、`Sys.executable`、`Sys.script` |
| プロセスを越えて残すデータの保存先 | [§7 Sys](#7-sys) — `Sys.data_dir("myapp")`(プラットフォームごとのユーザーデータディレクトリ) |
| 外部コマンド実行 | [§11 Proc](#11-proc) — `Proc.run(["git", "status"])` |
| HTTP/HTTPS APIを呼ぶ | [§15 Http](#15-http) — `Http.get("https://api.example/x")` |
| HTTPを提供する(ルーティング・静的ファイル・WebSocket) | [§15 `Http.server()`](#httpserver---object) — `Http.server().get("/", h).listen(8080)` |
| 生のTCP / UDPを話す、ホスト名を解決する | [§29 Net](#29-net) — `Net.connect(host, port)` / `Net.listen(port)` / `Net.udp()` / `Net.resolve(host)` |
| HTMLエンティティのescape / unescape | [§16 Encoding](#16-encoding) — `Encoding.html.unescape("a & b")` |
| base64 / hex / urlのエンコード・デコード | [§16 Encoding](#16-encoding) — `Encoding.base64.encode(s)` |
| データ・ファイルのgzip / gunzip | [§17 Compress](#17-compress) — `Compress.gzip(s)` / `Compress.gunzip(z)` |
| ファイルをZIPアーカイブに詰める | [§17 Compress](#17-compress) — `Compress.zip(files)` |
| gzipの封筒なしで圧縮 | [§17 Compress](#17-compress) — `Compress.deflate(s, level: 9)`(展開は`Compress.gunzip`) |
| ハッシュ / チェックサム / HMAC | [§18 Hash](#18-hash) — `Hash.sha256(s)` / `Hash.hmac_sha256(key, s)` |
| CSVのパース / 生成 | [§19 CSV](#19-csv) — `CSV.parse(text)` / `CSV.stringify(rows)` |
| TOMLのパース / 生成 | [§24 TOML](#24-toml) — `TOML.parse(text)` / `TOML.stringify(obj)` |
| XMLのパース / 検索 / 生成 | [§25 XML](#25-xml): `XML.parse(text)` / `root.find_all(".//item")` / `el.stringify()` |
| `.env`設定ファイルの読込 | [§20 Env](#20-env) — `Env.load(".env")` / `Env.parse(text)` |
| UUIDの生成 | [§21 UUID](#21-uuid) — `UUID.v4()` / `UUID.v7()` |
| レベル付き / 構造化ログ | [§23 Log](#23-log) — `Log.info("msg", {k: v})` / `Log.with({req: id})` |
| 別スレッドで処理を実行(CPU並列) | [§12 Isolate](#12-isolate) — `Isolate.spawn(\|\| fib(40))` |
| 固定レイアウトデータをスレッド/プロセス間で共有(zero copy) | [§12 SharedBuffer](#sharedbuffer--zero-copy-で共有する固定レイアウトデータ) — `SharedBuffer.new(n, FloatPair)` / `.file` / `.shared` |
| 可変長のread-onlyデータをスレッド間で共有(コピーなし) | [§12 Shared](#shared--参照共有する-immutable-値) — `Shared.new(value)` |
| Ctrl+C / SIGINTを綺麗に扱う | [§12 Signal](#signal--signalnotify--signalreset) — `Signal.notify(tx)` / `Signal.reset()` |
| デスクトップGUI(ネイティブWebView + ローカルサーバ) | [§30 Desktop](#30-desktop--webview) — `Desktop.run({title, assets, routes})` |
| 音を鳴らす(tone・効果音・音楽)、音を合成する | [§40 Audio](#40-audio) — `Audio.tone(440, 10)` / `Audio.Sound.new(bytes).play()` / `Audio.Music.new(bytes)` / `Audio.PCM.new(44100, 1, 1024)` / `Audio.Capture.new(44100)` |
| ヒープ情報・リークチェック | [§7 GC](#gc--ヒープ情報の取得) — `GC.stat()` → `{live_objects, rc_objects, heap_bytes}` |
| 2D/3Dベクトル演算(dot、length、normalize、distance) | [§31 `Vector2`](#31-vector2) / [§32 `Vector3`](#32-vector3) |
| FIFOキュー、スライディングウィンドウ、前後両端のスタック | [§33 `Deque`](#33-deque) — `Deque.new()` — `push_back`/`pop_front` |
| 優先度スケジューリング、イベントシミュレーション、最短経路探索 | [§34 `PriorityQueue`](#34-priorityqueue) — `PriorityQueue.new()` — `push`/`pop` |
| 自前の言語・設定フォーマットをパースする | [§35 PEG](#35-peg) — PEG文法を書いて`PEG.parser(grammar)`、`enable_ast()`、`parse(text)`。返る木は`match`で分解できる |
| 手順・通信手順・画面のモードを状態とイベントで表す | [§37 `StateMachine`](#37-statemachine) — `StateMachine.parse(text)`、または記述用の`Object` |
| 変わらない大きな語彙から前方一致で補完する | [§38 FST](#38-fst) — `FST.Set.new(FST.compile_set(words))` → `.predictive_search("hel")` |
| 綴りの直し・あいまい検索 | [§38 FST](#38-fst) — `.edit_distance_search(word, 1)`/`.suggest(word)` |
| たくさんの文書を語で検索して順位をつける | [§39 Search](#39-search) — `Search.Index.new()` → `.add(key, text)` → `.search("quick -dog")` |
| 行列・テンソル演算(BLAS対応) | [§8 Tensor](#8-tensor) |
| String / Array / Objectのメソッド | [言語仕様 §18](language.ja.md) |
| 整数列(`range`, `iota`) | [言語仕様 §19](language.ja.md) |
| `n`個の値で`Array`を埋める | [言語仕様 §19](language.ja.md) — `repeat(n, value)` |
| 変換(`to_long`、`to_float`、`to_string`、`type_of`) | [言語仕様 §19](language.ja.md) |
---
## 1. `Math`
数値ユーティリティ群。整数専用ルーチン(`pow`・`sign`・
`wrap`)は`Long`入力を保ち、`clamp`と浮動小数点ルーチン(`log`ほか)は
`Long` / `Float`のいずれかを受け取ります。`Long`と`Float`の相互作用は
言語仕様 §4 / §7を参照。
このセクションのサブグループ: **定数**(`Math.pi`、`Math.e`、
`Math.inf`、`Math.nan`) — **スカラー演算**(`abs`、`min`、`max`、
`log`、`exp`、`sqrt`、`floor`、`ceil`、`round`、`pow`、`sign`、
`clamp`、`wrap`) — **三角関数**(`sin`、`cos`、`tan`、`asin`、`acos`、
`atan`、`atan2`、ラジアン)。整数列ファクトリ`range` / `iota`は
言語コアグローバルで、[言語仕様 §19](language.ja.md#19-コア組み込み関数) を参照。
### 定数
`Math.pi` / `Math.e` / `Math.inf` / `Math.nan`は`Float`の
プロパティです。`--jit`でもコンパイル時定数として展開されます。
#### `Math.pi`
`π` ≈ `3.141592653589793`。
#### `Math.e`
ネイピア数 ≈ `2.718281828459045`。
#### `Math.inf`
正の無限大(`Math.inf > 1e308 == true`)。負の場合は`-Math.inf`。
#### `Math.nan`
quiet NaN。`Math.nan == Math.nan`はIEEE-754通り`false`。
```culebra
inspect(Math.pi) # => 3.141592653589793
inspect(Math.e) # => 2.718281828459045
inspect(Math.inf > 1e308) # => true
inspect(Math.nan == Math.nan) # => false
```
### スカラー演算
### `Math.abs(x: Long|Float) -> Long|Float`
絶対値。`Long`入力なら`Long`、`Float`入力なら`Float`を返します。
```culebra
inspect(Math.abs(-7)) # => 7
inspect(Math.abs(-7.5)) # => 7.5
```
### `Math.min(a, b, ...) -> Long|Float`、`Math.max(a, b, ...) -> Long|Float`
2つ以上の数値引数から最小 / 最大を取ります。全て`Long`なら`Long`、
1つでも`Float`が含まれれば結果は`Float`。引数1個以下、または
数値以外が混じれば`type error`。
```culebra
inspect(Math.min(3, 1, 4, 1, 5)) # => 1
inspect(Math.max(1.5, 2, 0.5)) # => 2.0
```
### `Math.log(x: Long|Float) -> Float`
自然対数。`Math.log(0)`は`-inf`、負の値は`nan`を返します。
整数値を返す場合でも戻り値は常に`Float`。
### `Math.exp(x: Long|Float) -> Float`
`e`の`x`乗。
### `Math.sqrt(x: Long|Float) -> Float`
主平方根。`Math.sqrt(-1.0)`は`nan`。
### `Math.sin(x) -> Float`、`Math.cos(x) -> Float`、`Math.tan(x) -> Float`
三角関数。`x`は**ラジアン**(`Long`または`Float`)。
```culebra
inspect(Math.sin(Math.pi / 2)) # => 1.0
inspect(Math.cos(0)) # => 1.0
```
### `Math.asin(x) -> Float`、`Math.acos(x) -> Float`、`Math.atan(x) -> Float`、`Math.atan2(y, x) -> Float`
逆三角関数。戻り値はラジアン。`asin` / `acos`は`x`が`[-1, 1]`
範囲(外は`nan`)。`Math.atan2(y, x)`は`y / x`の象限を考慮した
逆正接。
```culebra
inspect(Math.atan2(1.0, 1.0)) # => 0.7853981633974483
# (= pi/4)
```
### `Math.floor(x: Long|Float) -> Long`、`Math.ceil(...) -> Long`、`Math.round(...) -> Long`
整数への丸め。`Long`入力はそのまま返します。`Math.floor`は
`-∞`方向、`Math.ceil`は`+∞`方向、`Math.round`は
**偶数丸め(bankers' rounding)**。結果がLongに収まらないFloat
(NaN、無限大、±2⁶³を超える値)は`ValueError`。
```culebra
inspect(Math.floor(-1.5)) # => -2
inspect(Math.ceil(-1.5)) # => -1
# ちょうど半分は偶数側へ丸めるので 2.5 も 3.5 も偶数になる:
inspect(Math.round(2.5)) # => 2
inspect(Math.round(3.5)) # => 4
```
### `Math.f32(x: Long|Float) -> Float`
最も近い`float`(IEEE binary32)に丸めて`Float`として返します。
単精度で書かれた計算(ゲームの乱数生成器、32bit値を格納するファイル形式
など)を`Float`のまま再現するためのものです。`float`の最大値をわずかに
超える値はその最大値に戻り、さらに外側は`±inf`、`nan`は`nan`のままです。
```culebra
inspect(Math.f32(0.1)) # => 0.10000000149011612
inspect(Math.f32(16777217)) # => 1.6777216e+07
```
### `Math.pow(base: Long, exp: Long) -> Long`
整数累乗。繰り返し二乗法で`base ** exp`を計算します。
`Math.pow(x, 0)`は`x`に関わらず`1`(`0`を含む)。
**例外**: `exp < 0`のとき`type error at L:C.`。
後方互換のため残してあります。**基本は`**`演算子を使ってください**
(`Float`・負指数も扱えます。言語仕様 §7)。
```culebra
inspect(Math.pow(2, 10)) # => 1024
inspect(Math.pow(7, 0)) # => 1
inspect(Math.pow(-3, 3)) # => -27
```
### `Math.sign(x: Long) -> Long`
負数で`-1`、ゼロで`0`、正数で`1`を返します。
```culebra
inspect(Math.sign(-5)) # => -1
inspect(Math.sign(0)) # => 0
inspect(Math.sign(42)) # => 1
```
### `Math.clamp(x: Long|Float, lo: Long|Float, hi: Long|Float) -> Long|Float`
`x`を閉区間`[lo, hi]`に収めます。`lo > hi`の場合はエラーに
ならず`hi`を返します。`x`・`lo`・`hi`が全て`Long`なら`Long`、
いずれかが`Float`なら`Float`を返します(`Math.min`/`Math.max`と
同じ昇格ルール)。
```culebra
inspect(Math.clamp(5, 0, 10)) # => 5
inspect(Math.clamp(-5, 0, 10)) # => 0
inspect(Math.clamp(15, 0, 10)) # => 10
inspect(Math.clamp(0.5, 0.0, 1.0)) # => 0.5
inspect(Math.clamp(-3.0, 0.0, 10.0)) # => 0.0
```
### `Math.wrap(x: Long, n: Long) -> Long`
`x`を幅`n`に巻き戻します。`%`では得られない **floor剰余** です
(`%`は切り捨てなので結果は`x`の符号を持つ。[言語仕様 §7](language.ja.md#算術))。
`Math.wrap`の結果は`n`の符号を持つので、`n`が正なら必ず`[0, n)`
に入ります。巡回インデックスが欲しいのはこちらで、インデックス0の
1つ手前は負の添字ではなく末尾の要素になります。
```culebra
inspect(Math.wrap(3, 320)) # => 3
inspect(Math.wrap(-3, 320)) # => 317
inspect(-3 % 320) # => -3
inspect(Math.wrap(320, 320)) # => 0
```
`x`が非負の範囲では両者は一致するので、`Math.wrap`が効くのは`x`が
負になりうるとき — スクロール量、折り返すタイル座標、逆向きに進めた
角度 — だけです。
```culebra
let frames = ['a', 'b', 'c']
let prev = fn (i) {
frames[Math.wrap(i - 1, frames.size())]
}
inspect(prev(0)) # => 'c'
```
`n`が負なら全体が反転し、結果は`(n, 0]`に入ります。`n`が`0`の
場合は`x % 0`と同じく`divide by 0 error`を送出します。
---
## 2. `IO`
出力と標準入力。ファイルの読み書きは`FS`(`FS.read` / `FS.write` /
`FS.exists`)にあります。
### `IO.inspect(x: Any) -> Nil`
`x`を改行付きで標準出力に書き出します。参照型は
`Array`/`Object`の既定の`to_string`と同じ書式で整形され、
文字列は**シングルクォートで囲んで**出力されます。
```culebra
IO.inspect('hi') # → 'hi'
IO.inspect(42) # → 42
IO.inspect([1, 'a']) # → [1, 'a']
```
### `IO.print(x: Any) -> Nil`
`x`を**末尾改行なし**で標準出力へ書き出します。書式は`to_string`
と同じで、文字列は**引用符なし**で出力されます。複数回の書き込みで
1行を組み立てたい場合に便利です。
```culebra
IO.print('Hello, ')
IO.print('world!')
IO.println('') # → Hello, world!
```
### `IO.println(x: Any = '') -> Nil`
`x`を改行付きで標準出力に書き出します。書式は`to_string`と同じで、
文字列は**引用符なし**で出力されます — `inspect`の生表示版であり、
`print`の改行付き版です。`x`は`''`がデフォルトなので、`println()`単体は
空行だけを出力します。
```culebra
IO.println('hi') # → hi
IO.println(42) # → 42
IO.println() # → (空行)
```
### `IO.input() -> String`
標準入力から1行読み取ります。末尾の改行は除かれます。EOFのとき
`''`(空文字列)を返します。
```culebra
# doctest: skip — stdin を読む
println('name?')
name = IO.input()
println("Hello, {name}")
```
### `IO.stdin() -> reader`
標準入力に対する読み取り専用ハンドルを返します。`File`ハンドルと同じreader
インターフェース(だからソース非依存のコードが両方で動く):
| メソッド | 結果 |
| --- | --- |
| `.read()` | 標準入力をEOFまで全て(`FS.read("/dev/stdin")`の移植可能な代替。POSIX専用ではない)。即EOFなら空文字列 |
| `.read(n: Long)` | 最大`n`バイト(EOF時のみ少なく返る) |
| `.lines()` | 入力行の遅延iterator。末尾改行を除去し、EOFで停止 |
`.lines()`は定数メモリでストリーミングする = 巨大・無限入力(`tail -f \| script`)に対する
Unixフィルタのイディオム。読み取りはブロッキングかつ割り込み可能(Ctrl+C 1回で待機解除)。
標準入力は単一消費者で、各メソッドは内部バッファを共有するので`.read(n)`の後の`.lines()`
はバイト読みの続きから続行します。
```culebra
# doctest: skip — stdin を読む
# フィルタ: "error" を含む行を大文字化
for line in IO.stdin().lines() {
if line.contains("error") {
IO.println(line.upper())
}
}
let src = if IO.stdin_is_terminal() {
read_clipboard()
} else {
IO.stdin().read()
}
```
### `IO.einspect(x: Any) -> Nil` / `IO.eprint(x: Any) -> Nil` / `IO.eprintln(x: Any) -> Nil`
標準エラーへ書き出します(`inspect` / `print` / `println`の双子)。
`einspect`は文字列をクォートし改行を付けます(`inspect`同様)、
`eprint`は末尾改行なしの生の表示形(`print`同様)、`eprintln`は
改行付きの生の表示形(`println`同様)。stdoutに混ぜたくない診断
出力に使います。
```culebra
# doctest: skip
IO.einspect("warning: retrying") # → stderr
if !ok {
IO.eprint("error: {msg}\n")
}
IO.eprintln("done")
```
### `IO.stdin_is_terminal() -> Bool` / `IO.stdout_is_terminal() -> Bool` / `IO.stderr_is_terminal() -> Bool`
指定した標準ストリームが端末に接続されているか(POSIX `isatty`)を返し
ます。対話性に応じた分岐に使えます。stdinならプロンプト表示かパイプ
読み取りか、stdout / stderrなら色付けかプレーン出力か。スト
リームがファイルやパイプにリダイレクトされている場合は`false`を返し
ます。
```culebra
# doctest: skip
let src = if IO.stdin_is_terminal() {
read_clipboard()
} else {
FS.read("/dev/stdin")
}
if IO.stdout_is_terminal() {
println(colorize(msg))
} else {
println(msg)
}
```
### `IO.capture(f: Function) -> String`
`f`をstdoutをバッファに向けた状態で実行し、書き出された内容を返します。
`f`の戻り値は捨てられます(返す値ではなく印字する値が目的なので)。
「返さずに出力する」関数をテストで検証する手段です:
```culebra
let out = IO.capture(fn () {
println('hello')
})
assert_eq(out, "hello\n")
```
リダイレクトは**呼び出したスレッドのもの**です。`f`の実行中に別スレッドが
印字したものはstdoutへ出ますし、2つのスレッドが同時にcaptureしても互いの
出力を奪いません。`f`がthrowしてもリダイレクトは元に戻り、throwはそのまま
伝播します。入れ子にもでき、内側のcaptureは端末ではなく外側のバッファに
戻します。`IO.eprint`系はstderrに書くので捕捉されません。
`IO`は標準ストリームとコンソールの名前空間です。ファイルの読み書きは
`FS`(`FS.read` / `FS.write` / `FS.exists`)にあります。
---
## 3. `FS`
ファイルシステムのパス操作とディレクトリ操作。実装は
`std::filesystem`。変更系の呼び出しは失敗時に構造化された
`IOError`(`{kind: "IOError", message, line, col}`)をthrowし、
埋め込み側でソース位置に紐づけてエラー処理できます。
### パス操作
#### `FS.join(parts...: String) -> String`
プラットフォームの区切り文字(POSIXでは`/`、Windowsでは`\` — 下の
`FS.sep()`参照)でパス要素を結合します。引数0個は`""`を返します。
`std::filesystem::path::operator/=`と同じく、途中要素の末尾区切り文字は
尊重されます。
```culebra
inspect(FS.join('a', 'b', 'c.txt')) # => 'a/b/c.txt'
inspect(FS.join('/usr', 'local', 'bin')) # => '/usr/local/bin'
inspect(FS.join()) # => ''
```
上の例はPOSIXの区切り文字での表示。結合したパスをリテラルと比較する
プログラムは、`/`を決め打ちせず`FS.sep()`で組み立てるか、パスの
構成要素同士を比較すること。
#### `FS.sep() -> String`
プラットフォームのパス区切り文字 — `FS.join`と`Path`の`/`が要素間に
挿むもの。POSIXでは`/`、Windowsでは`\`。
```culebra
inspect(FS.join('a', 'b') == "a{FS.sep()}b") # => true
```
#### `FS.basename(path: String) -> String`
最終要素(ファイル名+拡張子)。末尾区切り文字のみのパスは`""`。
```culebra
inspect(FS.basename('a/b/c.txt')) # => 'c.txt'
inspect(FS.basename('/')) # => ''
```
#### `FS.dirname(path: String) -> String`
親パス。親が無い場合は`""`(`'c.txt' -> ''`)。
#### `FS.extension(path: String) -> String`
拡張子(先頭ドット込み)。無ければ`""`。ドットファイル
(`.hidden`)は拡張子なしとして扱います — `std::filesystem`仕様
通り。
```culebra
inspect(FS.extension('a/b/c.txt')) # => '.txt'
inspect(FS.extension('.hidden')) # => ''
```
#### `FS.stem(path: String) -> String`
拡張子を除いたbasename。
```culebra
inspect(FS.stem('a/b/c.txt')) # => 'c'
```
### ファイル全体の読み書き
#### `FS.read(path: String) -> String`
`path`のファイル全体を`String`として読み込みます(open + read +
closeを1呼び出しで)。常にバイナリ: 戻り値は任意の内容を往復できる
byte string。逐次/ストリーミング読みは`File`ハンドルを使います。
ファイルが存在しない・読込不可・ディレクトリの場合`IOError`をthrow。
```culebra
# doctest: skip
contents = FS.read('data.txt')
```
#### `FS.write(path: String, content: String) -> Nil`
`content`を`path`に書き込みます(作成または上書き)。バイナリ、
改行変換なし。親ディレクトリが無い・書込不可の場合`IOError`をthrow。
```culebra
# doctest: skip — 隣にファイルを書く
FS.write('out.txt', 'hello\n')
```
### 問い合わせ
#### `FS.exists(path: String) -> Bool`
`path`に何かが存在するか。ファイル/ディレクトリ/シンボリック
リンクの区別なし。空文字列や不正なパスは`false`。
#### `FS.is_file(path: String) -> Bool`
`path`が通常ファイルならtrue。シンボリックリンクはfollow。
#### `FS.is_dir(path: String) -> Bool`
`path`がディレクトリならtrue。シンボリックリンクはfollow。
#### `FS.size(path: String) -> Long`
ファイルサイズ(バイト)。`path`が存在しない/通常ファイルで
ない場合は`IOError`をthrow。
### ディレクトリ変更系
#### `FS.list_dir(path: String) -> Array`
`path`の直下エントリをファイル名(プレフィックスなし、`.` /
`..`除く)の配列で返します。順序はファイルシステム任せなので
必要なら明示的にソートしてください。`path`がディレクトリでなけ
れば`IOError`をthrow。
```culebra
# doctest: skip
let names = FS.list_dir('/tmp/build')
assert_true(names.contains('out.o'))
```
#### `FS.mkdir(path: String) -> Nil`
ディレクトリを作成。途中の親ディレクトリも含めて作成
(`mkdir -p`セマンティクス)。既存ならno-op。パスがファイル
として存在する/作成に失敗した場合は`IOError`をthrow。
#### `FS.temp_dir() -> String`
このマシンが一時ファイルを置くディレクトリ。POSIXでは`$TMPDIR`と
そのフォールバック、Windowsでは`GetTempPath`。末尾に区切り文字を
付けないので`FS.join`で二重にならない。プラットフォームが返した
パスがディレクトリでない場合は`IOError`をthrow。
```culebra
assert_true(FS.is_dir(FS.temp_dir()))
```
#### `FS.mkdtemp(prefix: String) -> String`
`FS.temp_dir()`の下に`prefix`で始まる名前の新しいディレクトリを
作成し、そのパスを返す。名前を決めるだけでなく実際に**作成**するので、
同時に走る2つのプログラムが同じディレクトリを受け取ることはない
(サフィックスは共有エンジンではなくOSから引くので、`Random.seed`で
同じ種を与えた2つの実行でも一致しない)。削除は呼び出し側の責任。
`prefix`はパスではなく名前。存在しない親ディレクトリを含む場合は
暗黙の`mkdir -p`ではなく`IOError`。
```culebra
let dir = FS.mkdtemp('build-')
assert_true(FS.is_dir(dir))
assert_eq(FS.dirname(dir), FS.temp_dir())
FS.remove(dir, recursive: true)
```
#### `FS.remove(path: String, recursive: Bool = false) -> Nil`
ファイルまたは空ディレクトリを削除(既定)。非空ディレクトリは
`IOError`。`recursive: true`でディレクトリツリーを削除(`rm -rf`)。
対象が存在しない/削除できない場合は`IOError`。
```culebra
# doctest: skip
FS.remove('/tmp/build/out.o')
FS.remove('/tmp/build', recursive: true)
```
#### `FS.rename(src: String, dst: String) -> Nil`
`src`を`dst`にリネーム/移動(同一ファイルシステム内はatomic)。
失敗時`IOError`。
#### `FS.copy(src: String, dst: String, recursive: Bool = false) -> Nil`
ファイルをコピー(`dst`既存なら上書き)。`recursive: true`で
ディレクトリツリーをコピー。失敗時`IOError`。
#### `FS.chmod(path: String, mode: Long) -> Nil`
`path`の権限ビットを`mode`(整数、通常は8進リテラル`0o755`/`0o644`)に
設定する。`mode`は下位12ビット(所有者/グループ/その他の`rwx` +
setuid/setgid/sticky)にマスクされ、既存ビットを置き換える。現在のビットは
`FS.stat(path).mode`で読める。存在しない、または権限変更に失敗すると
`IOError`。
プラットフォームをまたいで通用するのは所有者のwriteビットだけです。Windows
は権限ビットではなく読み取り専用属性を1つ持つので、どのファイルも`0o666`か
`0o444`として読め、modeは往復しません(`FS.chmod(p, 0o755)`の後の
`FS.stat(p).mode`は`0o666`)。所有者writeを落とす(`0o444`)・再び立てる、
という操作だけがどこでも同じように振る舞います。
```culebra
# doctest: skip
FS.chmod('deploy.sh', 0o755) # 実行可能にする
inspect(FS.stat('deploy.sh').mode) # 493 (0o755)
```
#### `FS.chown(path: String, owner = nil, group = nil) -> Nil`
`path`の所有者・グループを変更する。`owner`/`group`はそれぞれ名前(`String`)・
数値id(`Long`)・`nil`(その項目は変更しない)を受ける。現在のidは
`FS.stat(path).uid` / `.gid`で読める。所有者の変更は通常rootが必要、グループは
所属グループへなら一般ユーザでも可。存在しないパス・不明な名前・権限失敗で
`IOError`、String/Long/Nil以外の引数は`TypeError`。
```culebra
# doctest: skip
FS.chown('app.log', group: 'staff') # グループだけ名前で設定、所有者は維持
FS.chown('data', 'deploy', 'deploy') # 両方を名前で(root)
```
### stat / メタデータ
#### `FS.stat(path: String) -> Object`
`{size, is_dir, is_file, is_symlink, mtime, mode, uid, gid}`を返す。`size`は
バイト(非通常ファイルは0)、`mtime`はUnix epoch秒、`mode`は権限ビットの整数
(8進と比較: `st.mode == 0o644`)、`uid`/`gid`は所有者・グループid、
`is_symlink`は
リンク自体を、他フィールドはリンク先を見ます。存在しなければ
`IOError`。
WindowsにはPOSIXの所有者モデルが無いので、`uid`と`gid`は代替値を持つのでは
なく**フィールドごと存在しません**。`st.uid`は`nil`になり、そのプラット
フォームが所有者idを持つかは`st.has('uid')`で問います。`mode`はどこにでも
ありますが、Windowsは権限ビットではなく読み取り専用属性を1つ持つだけなので
意味が薄くなります(下の`FS.chmod`を参照)。
```culebra
# doctest: skip
let st = FS.stat('config.toml')
inspect(st.size)
```
### 再帰探索
#### `FS.walk(path: String) -> Array`
`path`配下の全パスを再帰・深さ優先で。各要素はフルパス。
読む権限の無いディレクトリは要素には入るが、その中へは降りない。
`path`がディレクトリでないとき、`path`自体が読めないとき、配下の読み込みが
それ以外の理由で失敗したときは`IOError`。
#### `FS.glob(pattern: String) -> Array`
glob `pattern`にマッチするパス(ソート済み)。セグメント単位で
`*` / `?` / `[...]`、`**`で再帰下降。1文字はコードポイントなので、
`?`は`é`をまるごと1文字として合わせます。シェル流globで`Regex`とは
別物です。
```culebra
# doctest: skip — 作業ディレクトリを読む
let sources = FS.glob('src/**/*.cul')
```
### 変更の監視
#### `FS.watch(path: String, recursive: Bool = true, match: Array? = nil) -> WatchHandle`
ディレクトリの変更を監視します。ハンドルの反復は次の変更までブロック
し、`{path, kind}`を返します。`path`は絶対・正規化済み(シンボリック
リンク解決済みなので、macOSでは`/tmp`配下が`/private/tmp`配下として
報告される)、`kind`は`'created'` / `'modified'` / `'deleted'`。
`match`は指定拡張子で終わるパスだけを通します(先頭ドットは有無どちら
でも可 — `'cul'`と`'.cul'`は同じフィルタ)。判定はイベントをキューに積む
前に行うので、弾かれた変更はキューに入りません。
ディレクトリでなければ`IOError`、`match`が文字列配列でなければ
`TypeError`。
`recursive: false`は監視をそのディレクトリ直下の項目に絞ります。サブ
ディレクトリの*下*で起きた変更は報告されませんが、サブディレクトリ項目
そのものの変更は報告されることがあります — 中のファイルが書かれたとき
にタイムスタンプが動いたことをWindowsは報告し、inotifyとFSEventsは報告
しません。
バックエンドはmacOSがFSEvents、Linuxがinotify、Windowsが
`ReadDirectoryChangesW`。
```culebra
# doctest: skip — 割り込みまで監視を続ける
for e in FS.watch('src', match: ['.cul']) {
println("{e.kind} {e.path}")
}
```
イベントは監視を開いた時点から積まれるので、最初の取り出しより前に起き
た変更も届きます。ハンドルはそのまま反復でき、`break`しても使えるまま
です。名前付きハンドルは再度反復できます:
```culebra
# doctest: skip — 割り込みまで監視を続ける
let w = FS.watch('src')
for e in w { break } # ハンドルは開いたまま
for e in w { break } # 前のループの続きから
w.close()
```
`close()`で監視を止めます。スコープを抜けたハンドルは自分で閉じるので、
匿名の`for e in FS.watch(p)`はループ終了時に停止します。監視は開いた
スレッドに属し、isolateへ送ることはできません。
意図的に約束していないことが3点あります。**kindはOSの粒度でのbest
effort**です。FSEventsは短い窓の中で同一パスへの複数の変更を1イベント
に畳み、inotifyは個別に報告するため、同じ編集が片方では1イベント、もう
片方では2イベントになりえます。**renameはペアになりません** — 旧パスの
`deleted`と新パスの`created`として別々に届きます。**重複排除も
debounceもせず**、キューは無制限です。木の変化より遅い消費者はイベント
を落とすのではなくメモリを消費します。
待機中の反復を終わらせられるのは`break`・Ctrl+C・実行中のisolateの
キャンセルだけです。タイムアウトやノンブロッキング取得はありません。
### パス解決
#### `FS.abspath(path: String) -> String`
`path`の絶対・正規化形(カレントディレクトリ基準)。シンボリック
リンクは解決しません。
#### `FS.realpath(path: String) -> String`
シンボリックリンクを解決した正準パス(`weakly_canonical`、末尾の
存在しない要素は保持)。
#### `FS.normpath(path: String) -> String`
ファイルシステムに触れず字句的に正規化(`.` / `..` / 重複区切りを
畳む)。
#### `FS.is_abs(path: String) -> Bool`
`path`が絶対パスか。
### シンボリックリンク
#### `FS.symlink(target: String, link: String) -> Nil`
`target`を指すシンボリックリンクを`link`に作成。
#### `FS.readlink(path: String) -> String`
シンボリックリンクの参照先を読む。`path`がリンクでなければ
`IOError`。
#### `FS.is_symlink(path: String) -> Bool`
`path`がシンボリックリンク自体か(参照先でなく)。
### `Path` — 流暢なラッパ
`Path`は`FS.*`ヘルパの上の薄い**不変**ラッパです。`FS`が原始層
(生の`String`パスを受け、`String`/値を返す)で、`Path`はパスを持ち
回る糖衣層 — パス文字列を手で引き回さずに済みます。各操作は新しい
`Path`(成分は`String`)を返し、`Path`は決して破壊的に変更されません。
短いスクリプトでは`FS.*`を、パスを段階的に組み立てたり複数の操作へ
渡したりする所では`Path`を。`/`演算子とプロパティ連鎖の方が読みやすく
なります:
```culebra
# doctest: skip
let root = Path.new(FS.dirname(Sys.script)).resolve()
for src in root.glob("*/content.src.js") {
let dst = src.parent / "content.js" # vs FS.join(FS.dirname(src), …)
dst.write(transform(src.read()))
IO.print("{src.parent.name}/content.js\n") # vs FS.basename(FS.dirname(src))
}
```
`Path.new(s)`で構築します(`s`は`String`または別の`Path`)。パスを
取るメソッド(`join`, `/`, `rename`, `==`)はいずれも`String`か`Path`
を受けます。文字列補間(`"{p}"`)と`to_string(p)`は生のパス文字列を
返します。
| メンバー | 返り値 | 委譲先 |
|---|---|---|
| `p / other` / `p.join(other)` | `Path` | `FS.join` |
| `p.name` | `String`(末尾成分) | `FS.basename` |
| `p.stem` | `String`(拡張子なしの名前) | `FS.stem` |
| `p.suffix` | `String`(ドット込み拡張子) | `FS.extension` |
| `p.parent` | `Path` | `FS.dirname` |
| `p.resolve()` | `Path`(絶対) | `FS.abspath` |
| `p.exists()` / `p.is_file()` / `p.is_dir()` | `Bool` | `FS.*` |
| `p.read()` / `p.write(content)` | `String` / `Nil` | `FS.read` / `FS.write` |
| `p.mkdir()` | `Nil`(親も作成) | `FS.mkdir` |
| `p.remove(recursive=false)` | `Nil` | `FS.remove` |
| `p.rename(dst)` | `Path`(移動先) | `FS.rename` |
| `p.list()` | `Array`(ディレクトリ項目) | `FS.list_dir` |
| `p.glob(pattern)` | `Array` | `FS.glob` |
| `p.walk()` | `Array`(再帰) | `FS.walk` |
| `p.str()` | `String`(エスケープハッチ) | — |
`name` / `stem` / `suffix` / `parent`は **getter**(純粋な文字列導出)で、
括弧なしで読みます — `p.parent().name()`ではなく`p.parent.name`(括弧付き
の呼び出しも動きます)。下段のファイルシステム操作はI/Oを行いthrowし
うるためメソッドのままです。
2つの`Path`は内部パス文字列で比較します: `==`(`String`相手も可)と
`<` / `<=` / `>` / `>=`。よって`Path`配列はソート可能(`paths.sorted()`)で、
`min` / `max` / `Set`でも使えます。非パスとの順序比較は`TypeError`(意味のある
答えがないため)、一方`==`の非パス相手は単に`false`です。
`FS.*`ヘルパと`File.open` / `File.with`は、パスを取る所ならどこでも
`Path`を受けます(パス引数の型は`String | Path`)。よって`Path`は
`.str()`なしでそのまま流し込めます:
```culebra
# doctest: skip
let cfg = Path.new("/etc") / "app.conf"
let text = FS.read(cfg) # FS.read(String | Path)
for line in File.open(cfg).lines() {
} # File.open(String | Path)
```
こうしてopt-inするのは*パスを取る*標準ライブラリ関数だけです。それ
以外では`Path`と`String`は**別の型**のまま — ふつうの`fn(x: String)`
は`Path`を受け付けないので型境界は意味を保ちます。`String`専用APIに
生文字列を渡すには`p.str()`(または`"{p}"`)を使ってください。
---
## 4. `File`
`File`はストリーミングI/O用の**状態を持つハンドル**で、`FS`の
一発の全読み書き(`FS.read` / `FS.write`)と対になります。`File.open`
またはスコープ付きの`File.with`で開きます。I/Oは常にバイナリ
(テキストモードの改行変換なし)で、`String`はbyte stringなので
任意の内容が往復します。
ハンドル(以下`f`)は4つのメソッド群を実装します — **Reader**(`read` /
`lines` / `chunks`)、**Writer**(`write` / `flush`)、**Seekable**
(`seek` / `tell`)、**Closeable**(`close`)。どれが有効かはopen
モードに依ります。
### 開く
#### `File.open(path: String, mode: String = "r") -> File`
`path`を開く。`mode`は`"r"`(読み)/ `"w"`(切り詰め+書き)/
`"a"`(追記)。それ以外は`ValueError`、開けなければ`IOError`。
#### `File.with(path: String, mode: String = "r", fn: Function) -> Any`
`path`を開き`fn(handle)`を呼び、あらゆる脱出経路(正常・`return`・
例外)でハンドルを閉じます。`fn`の戻り値を返します。`open` +
`defer { close }`のnative版で、ハンドルの寿命が1ブロックに収まる
時に最も明快です。
```culebra
# doctest: skip
let head = File.with('big.log', 'r', fn (f) {
f.read(256)
})
```
### 資源安全 — 閉じる3つの方法
| パターン | 使う場面 |
|---|---|
| `File.with(p, m, fn (f) { … })` | ハンドルの寿命が1ブロックに収まる |
| `let f = File.open(p, m); defer { f.close() }` | 寿命が広い/他のロジックと混ざる |
| `for line in File.open(p).lines() { … }` | ストリーミング。iteratorがループ脱出(`break`含む)で閉じる |
明示的に閉じられなかったハンドルはGCバックストップが閉じますが、
それに頼らず上記3つのどれかを使ってください。
### Reader メソッド
#### `f.read() -> String` / `f.read(n: Long) -> String`
現在位置からのストリーミング読み: `read()`は残り全部、`read(n)`
は最大`n`バイト(EOFでは少なくなる)。ハンドル不要の一発全読みは
`FS.read(path)`を使います。
#### `f.lines() -> Iterator`
行を反復、各行は末尾改行を剥がします(`\n` / `\r\n` / `\r`全対応)。
iteratorがハンドルを所有し、ループ終了・breakで閉じます。
```culebra
# doctest: skip
for line in File.open('access.log').lines() {
if line.contains('ERROR') {
inspect(line)
}
}
```
#### `f.chunks(n: Long) -> Iterator`
最大`n`バイトの固定長チャンクを反復(最後は短いことあり)。
`lines()`と同じclose-on-exit契約。
### Writer メソッド
#### `f.write(data: String) -> Nil`
現在位置に`data`を書き込み(生バイト、改行変換なし)。読み取り
専用ハンドルでは`IOError`。
#### `f.flush() -> Nil`
バッファした書き込みをOSにフラッシュ。
### Seekable メソッド
#### `f.seek(offset: Long, whence: String = "set") -> Nil`
カーソル移動。`whence`は`"set"`(先頭から)/ `"cur"`(相対)/
`"end"`(末尾から;負の`offset`を使う)。
#### `f.tell() -> Long`
現在のバイトオフセット。
### Closeable
#### `f.close() -> Nil`
ハンドルを閉じ、書き込みをフラッシュ。冪等 — 二重closeはno-op。
閉じたハンドルへの操作は`IOError`。
---
## 5. `Time`
Wall-clock + monotonic時刻、ISO 8601入出力、カレンダー算術。
モジュールが提供する2つのクラス — `Instant`(時点)と
`Duration`(時間幅)— 内部表現はUnix epoch起点の`i64`
ナノ秒(範囲 ±292年、完全ナノ秒精度)。この範囲を超える結果は
桁あふれせず`ValueError`になる。
タイムゾーンは **UTC + localのみ**(`Asia/Tokyo`等の名前付き
ゾーンは将来対応)。各methodはkw-only `utc:`フラグを取り、
`iso`は`utc: true`がデフォルト(Z付きISO 8601がinteropの
wire formのため)、それ以外は`utc: false`(local)デフォルト。
### 取得
#### `Time.now() -> Instant`
現在のwall-clock時刻。NTPや手動時計調整の影響を受けるので、
経過計測には`Time.monotonic`を使うこと。
#### `Time.monotonic() -> Float`
最初の呼出(プロセス起動)からの経過秒数(sub-秒精度)。厳密に
非減少、壁時計変更の影響を受けない。ベンチやtimeoutの基本ツー
ル。
```culebra
# doctest: skip
let t0 = Time.monotonic()
do_work()
inspect("elapsed: {Time.monotonic() - t0} s")
```
#### `Time.sleep(secs: Float) -> Nil`
現在スレッドを`secs`秒以上ブロック。負値や0はno-op。
### `Instant` コンストラクタ
#### `Time.from_iso(s: String) -> Instant`
ISO 8601タイムスタンプをparse。受け付けるvariant:
- `2026-05-20T15:30:00Z`
- `2026-05-20T15:30:00.123Z`
- `2026-05-20T15:30:00.000123456Z`(ns精度の入力)
- `2026-05-20T15:30:00+09:00`
- `2026-05-20T15:30:00-0900`
- `2026-05-20`(日付のみ — UTC 0:00として扱う)
- `2026-05-20T15:30`(秒省略)
不正な入力は`ValueError`をthrow。暦の範囲外の値(`2026-13-01`、
`2025-02-29`、`T25:00`)も不正な入力で、翌月や翌日へ繰り上げはしない。
#### `Time.from_unix(secs: Long|Float) -> Instant`
Unix epoch秒から構築。Longなら正確に、Floatならsub-秒精度で、
最も近いナノ秒に丸める。
#### `Time.from_parts(p: Object, utc: Bool = false) -> Instant`
parts dictからtimestampを組み立て — `Instant.parts`の逆操作。
認識キー: `year`、`month`、`day`、`hour`、`minute`、`second`、
`nanosecond`(デフォルト: `year=1970`、`month=1`、`day=1`、その他0)。それ
以外のキーは無視。認識キーの値はLongでなければ`TypeError`、暦の範囲
(`month`は1〜12、`day`はその月の日数まで、`hour`は0〜23、`minute`と
`second`は0〜59、`nanosecond`は10⁹未満)を外れると`ValueError`。
#### `Time.parse(s: String, fmt: String) -> Instant`
非ISO入力向けの厳格strftime parse。formatはPOSIX `strptime`
準拠。`s`が`fmt`に一致しないとき、`fmt`を読み終えても文字が
残るとき、項目が暦の範囲(`Time.from_parts`と同じ)を外れるときは
`ValueError`をthrow(`2026-02-30`は3月に繰り上げずに拒否)。`fmt`に
ない項目は`Time.from_parts`と同じ既定値(`year=1970`、`month=1`、
`day=1`、その他0)。結果はlocal timeとして解釈。
```culebra
Time.parse("2026/05/20 15:30:00", "%Y/%m/%d %H:%M:%S")
```
### `Instant` method
#### `t.iso(utc: true) -> String`
ISO 8601形式、完全ナノ秒精度(小数部が0の場合は省略)。デフォ
ルトUTC(`...Z`)、`utc: false`でlocal time + `±HH:MM` offset。
#### `t.format(fmt: String, utc: false) -> String`
strftime formatで整形。デフォルトはlocal time。
```culebra
# doctest: skip
t.format("%Y-%m-%d %H:%M:%S") # local
t.format("%Y%m%d", utc: true) # 20260520
```
#### `t.parts(utc: false) -> Object`
`{year, month, day, hour, minute, second, nanosecond, weekday,
dayofyear}`に分解。`weekday`はISO 8601起点(`0=Mon`、`6=Sun`)、
`dayofyear`は1-based(`1..366`)。
```culebra
let p = Time.now().parts()
if p.hour >= 9 && p.hour < 17 {
inspect("business hours")
}
```
#### `t.weekday(utc: false) -> Long`
weekday単体(0=Mon..6=Sun)。`parts()`のObject allocationを
避けたい場合用。
#### `t.add(years=0, months=0, days=0, hours=0, minutes=0, seconds=0, utc: false) -> Instant`
カレンダー算術。`years` / `months`は **月末clamp** セマンティ
クス: `2026-01-31 + 1ヶ月 → 2026-02-28`、
`2024-01-31 + 1ヶ月 → 2024-02-29`(うるう年)。日以下の場は単純
加算。
```culebra
let next_month = Time.now().add(months: 1)
let next_quarter = Time.now().add(months: 3)
let next_year = Time.now().add(years: 1)
```
#### `t.start_of(unit: String, utc: false) -> Instant`
カレンダー単位の先頭に丸める。`unit` ∈ `"year"` / `"month"` /
`"day"` / `"hour"` / `"minute"`。それ以外は`ValueError`。
```culebra
# doctest: skip
let day_bucket = t.start_of("day")
let hour_bucket = t.start_of("hour")
```
#### `t.unix() -> Float`、`t.unix_nanos() -> Long`
Unix epochをFloat秒(現在時点で ~400ns精度)またはLong ns
(loss-less)で取得。
### `Duration` コンストラクタ
```culebra
# doctest: skip
Time.seconds(n) # n 秒
Time.milliseconds(n)
Time.minutes(n)
Time.hours(n)
Time.days(n)
```
`n`はLongでもFloatでも可 — Longは正確に換算し、小数単位は最も近い
ナノ秒に丸める。
### `Duration` method
#### `d.seconds() / .milliseconds() / .minutes() / .hours() / .days() -> Float`
指定単位での値(小数単位をround-tripするため常にFloat)。
#### `d.abs() -> Duration`
絶対値(負のdurationを正に)。
### 演算子オーバーロード
```culebra
# doctest: skip
let t = Time.now()
let one_hour = Time.hours(1)
t + one_hour # Instant + Duration → Instant
t - one_hour # Instant - Duration → Instant
t1 - t2 # Instant - Instant → Duration
Time.minutes(1) + Time.seconds(30) # → Duration (90s)
one_hour * 2 # → Duration(Longは正確)
one_hour / 2 # → Duration(Longは切り捨て)
-one_hour # → Duration
a < b, a <= b, a == b # 両クラスで自然な順序
```
どちらもナノ秒を包むクラスですが、互換ではありません。意味を持たない
組み合わせ(`t1 + t2`、`t < one_hour`、どちらかが素の数値)は、黙って
計算せずに受け取った型を明示した`TypeError`になります。等値比較だけは
例外で、他の型との`==`はエラーではなく`false`です。
---
## 6. `Random`
乱数生成。プロセスごとに単一のMersenne-Twister-64エンジンを
持ち、VMとJITで共有しています。`Random.seed(n)`は
エンジンをリセットし、以降の呼び出しを1回のプログラム実行内で
再現可能にします。`seed`を呼ばなければ`std::random_device`で
初期化されます。
### `Random.seed(n: Long) -> Nil`
PRNGを再シード。同じ`n` → 同じ系列。
```culebra
Random.seed(42)
```
### `Random.int(lo: Long, hi: Long) -> Long`
半開区間`[lo, hi)`からの一様整数。`hi > lo`必須、
違反すると`type error`。
```culebra
Random.seed(0)
inspect(Random.int(0, 10)) # 0..9
```
### `Random.uniform(lo: Float, hi: Float) -> Float`
半開区間`[lo, hi)`からの一様実数。`Long`引数も受け付け、
`Float`に昇格します。
### `Random.gauss(mu: Float, sigma: Float) -> Float`
平均`mu`、標準偏差`sigma`のガウス分布から1サンプル。
`Long`引数は`Float`に昇格します。
```culebra
Random.gauss(0.0, 1.0) # 標準正規
```
### `Random.shuffle(a: Array) -> Nil`
Fisher–Yatesによるインプレース置換。`nil`を返し、引数は破壊的に
並び替えられます。
### `Random.weighted_choice(pop: Array, weights: Array) -> Any`
対応する`weights`に比例する確率で`pop`から1要素を取り出します。
`weights`はすべて数値かつ`pop`と同じ長さである必要があります。
空または長さ不一致は`type error`。重み`0`は選ばれません。
```culebra
Random.weighted_choice(['hit', 'miss'], [1, 9]) # ~10% 'hit'
```
### `Random.choice(pop: Array) -> Any`
`pop`から一様な確率で1要素を取り出します。`pop`が空なら`type error`。
```culebra
Random.seed(0)
Random.choice(['rock', 'paper', 'scissors'])
```
---
## 7. `Sys`
プロセスレベルの情報。
### `Sys.argv -> Array`
スクリプトにコマンドラインで渡された`String`引数の配列。最初の
非フラグ引数がスクリプトパスで、**それより後ろ**がすべて`argv`
として取り込まれます。culebra自身のフラグ
(`--jit`・`--debug`など)はスクリプトパスより前に置く必要があり
ます。末尾引数が無い場合やREPL実行時は空配列です。
```culebra
# doctest: skip — マシン固有の値を印字する
# $ culebra run.cul hello world
inspect(Sys.argv) # ['hello', 'world']
# $ culebra --jit run.cul hello → ['hello'] (--jit は culebra 用)
```
単独の`--`は任意のescape hatchです。フラグ解析を打ち切るので、
ダッシュで始まるファイル名でも次の引数をスクリプトにできます
(例: `culebra -- -weird.cul`)。通常は不要です。
`culebra build`で作ったバイナリも同じ形で(プログラム名を同様に除いて)
実行時の引数を返します。値はプロセス全体で共有されるので、
[isolate](#12-isolate) やworkerスレッド上のHTTPハンドラでも
メインスレッドと同じ配列が読めます。
### `Sys.exit(code: Long) -> Nil`
指定の終了コードでプロセスを即座に終了します。呼び出しは戻らず、
保留中の`defer`文も**実行されません**。
```culebra
# doctest: skip
if error_occurred {
Sys.exit(1)
}
```
### `Sys.env(name: String, fallback = '') -> Any`
環境変数`name`の値を返します。未設定の場合は`fallback`をそのまま返します。
既定値が`''`なので、1引数形式は従来どおり`String`を返します。
`fallback`は型を問わずそのまま返るため、`nil`を渡すことで「未設定」と
「空文字列が設定されている」を区別できます。1引数形式はどちらも`''`に
なります。
```culebra
# doctest: skip — マシン固有の値を印字する
inspect(Sys.env('HOME')) # '/Users/alice'
inspect(Sys.env('NOT_A_VAR')) # ''
inspect(Sys.env('PORT', '8080')) # '8080'(PORT未設定時)
inspect(Sys.env('NOT_A_VAR', nil)) # nil — 未設定
inspect(Sys.env('SET_BUT_EMPTY', nil)) # '' — 設定済みで空文字列
```
### `Sys.set_env(name: String, value: String) -> Nil`
環境変数`name`を`value`に設定します(既存の値は上書き)。変更はこのプロセス
(`Sys.env`経由)と、以降に起動する子プロセス(例: `Proc.run`)から見えます。
失敗時(不正な変数名など)は`IOError`を送出します。
```culebra
Sys.set_env('CULEBRA_MODE', 'fast')
inspect(Sys.env('CULEBRA_MODE')) # => 'fast'
```
### `Sys.getcwd() -> String`
現在の作業ディレクトリの絶対パスを返します。パス中のシンボリックリンクは
解決されます。ディレクトリを特定できない場合(プロセスの足元でディレクトリが
削除された等)は`IOError`を送出します。
```culebra
# doctest: skip — マシン固有の値を印字する
inspect(Sys.getcwd()) # '/Users/alice/project'
```
### `Sys.chdir(path: String) -> Nil`
プロセスの作業ディレクトリを`path`に変更します。パスが存在しない、または
ディレクトリでない場合は`IOError`を送出します。
```culebra
# doctest: skip — マシン固有の値を印字する
Sys.chdir('/tmp')
inspect(Sys.getcwd()) # '/tmp'(または解決後のパス)
```
### `Sys.data_dir(app: String) -> String`
`app`がプロセスを越えて残すデータ(セーブファイル、ハイスコア、設定)を
置くユーザーごとのディレクトリを返します。規約はプラットフォームのものに
従います:
| プラットフォーム | ディレクトリ |
|---|---|
| macOS | `~/Library/Application Support/` |
| Windows | `%APPDATA%\`(未設定なら`%USERPROFILE%\AppData\Roaming\`) |
| その他 | `$XDG_DATA_HOME/`、未設定なら`~/.local/share/` |
ディレクトリは**作成しません** — 最初に書き込むときに`FS.mkdir`が親ごと作ります。
`app`は単一のパスセグメントでなければならず、空文字列・`/`や`\`や`:`を含む
名前・`.`・`..`はいずれも`ValueError`になります。したがって戻り値がベース
ディレクトリの外を指すことはありません(Windowsのドライブレターは、付け足す
のではなくベースを置き換えてしまうため)。プラットフォームのベースディレクトリを
決定できない場合(`HOME`が無い、`APPDATA`も`USERPROFILE`も無い)は`IOError`を
送出します。
```culebra
# doctest: skip
let dir = Sys.data_dir('samegame')
FS.mkdir(dir)
FS.write(FS.join(dir, 'scores.json'), JSON.stringify(scores))
inspect(dir) # '/Users/alice/Library/Application Support/samegame'
```
### `Sys.executable -> String`
実行中のculebraバイナリの絶対パス。`culebra`が`PATH`にあることに頼らず、
culebraのワーカーコピーを起動するのに使う — 例
`Proc.run([Sys.executable, "worker.cul"], ...)`。(AOTビルドされたプログラムでは
その単体バイナリ自身のパスになる。)
```culebra
# doctest: skip — マシン固有の値を印字する
inspect(Sys.executable) # '/usr/local/bin/culebra'
```
### `Sys.script -> String?`
実行中スクリプトの絶対パス(`__file__`相当)。カレントディレクトリに頼らず、
スクリプトの隣にあるファイルを解決するのに使う:
`FS.join(FS.dirname(Sys.script), "data.txt")`。実行時にソースファイルが存在しない
場合 — REPL・パイプした`stdin`・AOTビルドされたバイナリ(`.cul`を持たない。その
場合は`Sys.executable`を使う)— では`nil`。
```culebra
# doctest: skip — マシン固有の値を印字する
inspect(Sys.script) # '/Users/alice/project/build.cul'
```
### `Sys.time() -> Float`
**単調時計**の経過秒数。原点はプロセス内での最初の呼び出し。壁時計の
補正で巻き戻ることがないので、コード区間の計測にはこちらを使います
(日時が必要なら`Time.now()`、§5)。`Time.monotonic()`(§5)も同じ
単調時計を読みますが、原点はそれぞれの関数の初回呼び出しなので、
2つを混ぜず同じ関数の2点の差を取ってください。
```culebra
let t0 = Sys.time()
let sum = range(1000).reduce(0, |a, x| a + x)
inspect(Sys.time() - t0 >= 0.0) # => true
```
### `GC` — ヒープ情報の取得
`GC.stat()`はフルコレクションを実行し、その直後の生きたヒープを表す
`Object`を返す:
| キー | 型 | 意味 |
|---|---|---|
| `live_objects` | `Long` | 到達可能なヒープオブジェクト数 |
| `rc_objects` | `Long` | 到達可能な *参照カウント* オブジェクト数(traced-onlyなString/StringViewを除く) |
| `heap_bytes` | `Long` | それらが占めるバイト数 |
先にコレクションを走らせるので、数値はsweep待ちの循環残渣ではなく
*到達可能な* 状態を表す。呼び出し自体が結果`Object`を確保するため、
連続して読むと小さな定数分だけ差が出る — 絶対値ではなく対象コード前後の
差分(delta)を測ること。
このコレクションは根をスタック走査ではなく参照カウント(CPythonの
`gc_refs`)から決めるので、プログラムのどのオブジェクトが生き残るか、
つまりどの孤児の`drop`が発火するかは参照カウントだけで決まり、どの
backendでも毎回同じになる。したがって`rc_objects`は同じプログラムなら
決定的な値で、executorと`--jit`は同じ値を返す。
`live_objects`は保守的なスタック走査でトレースされるStringも数えるので
数個ぶれることがある。2回の呼び出しの間に走るバックグラウンドの
コレクションも保守的で、より安価である。
```culebra
# doctest: skip
let base = GC.stat().live_objects
build_some_structure()
inspect(GC.stat().live_objects - base) # 構造が保持しているオブジェクト数
```
これはリーク回帰テストの土台になる: 多数の反復をまたいでdeltaが有界に
留まることをassertする。メモリはそれ以外は自動管理 — メモリモデルと
確定的`drop`は言語仕様を参照。
`GC.refcount(value)`はヒープ値が開いている参照の数を返す。参照カウントを
持たない値では`nil` — `Long`・`Float`・`Bool`・`nil`は即値で、`String`は
カウントでなくトレースで管理される。これは`GC.stat()`では答えられない
問いに答える: 決して回収されない値 — 例えばnamespaceから到達可能なもの — は、
参照がいくら積み上がってもオブジェクト数を1つも増やさないので、そうした値に
対して漏れた参照はオブジェクト数の計測には写らない。
```culebra
let obj = {a: 1}
let base = GC.refcount(obj)
for _ in range(100) {
let alias = obj
}
inspect(GC.refcount(obj) - base) # => 0
```
絶対値ではなく必ずdeltaとして読むこと: 答えを返している間その呼び出し自身が
参照を1つ持っているし、ランタイムがrootとして保持している値は、プログラムから
見える数よりはるかに大きな値を返す。
---
## 8. `Tensor`
N次元数値テンソル。lazy計算グラフを構築し、`Tensor.eval(...)`で
BLAS / vDSP経由のカーネルを起動して値を確定します。dtypeは
`Float32`(デフォルト)と`Float64`、形状はvariadicか`[m, n]`
配列で指定。`transpose` / `slice` / `reshape`はzero-copy view。
```culebra
let A = Tensor.from([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # [2, 3]
let B = Tensor.from([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]) # [3, 2]
let C = A.dot(B) + 1.0 # lazy: グラフを作るだけ
Tensor.eval(C) # ここで BLAS GEMM が走る
inspect(C.shape()) # => [2, 2]
inspect(C.to_array()) # => [[5.0, 6.0], [11.0, 12.0]]
```
### 構築(名前空間関数)
#### `Tensor.zeros(...) -> Tensor` / `Tensor.ones(...)` / `Tensor.randn(...)`
形状をvariadic(`Tensor.zeros(3, 4)`)またはArray
(`Tensor.zeros([3, 4])`)で受け取ります。dtypeは文字列タグを
**第一引数**に置くJulia流。dtypeは`"f32"`のみです(float64は
GPUバックエンドに高速パスがないため):
```culebra
let a = Tensor.zeros(3, 4) # F32 default
let a32 = Tensor.zeros("f32", 3, 4) # 明示
let dims = [3, 4]
let b = Tensor.zeros(dims) # 計算済み形状
let r = Tensor.randn(2, 3) # 標準正規
```
#### `Tensor.from(arr: Array) -> Tensor`
ネストされたCulebra配列をTensorに変換します。1D(`[1.0, 2.0]`)
または2D(`[[1.0, 2.0], [3.0, 4.0]]`)を受け、F32で格納:
```culebra
let v = Tensor.from([1.0, 2.0, 3.0, 4.0]) # [4]
let m = Tensor.from([[1.0, 2.0], [3.0, 4.0]]) # [2, 2]
```
#### `Tensor.concat(parts: Array, axis: Long = 0) -> Tensor`
Tensorを`axis`方向(既定は軸0=行)に積み重ね、1つのTensorに
します。すべてのpartはdtype・rank、`axis`以外の次元が一致して
いる必要があります。結果の`axis`方向の長さは各partの合計です。
微分可能 — 勾配は各partに対応する`axis`方向のwindowに切り分けて
戻されます。
```culebra
let a = Tensor.from([[1.0, 2.0], [3.0, 4.0]]) # [2, 2]
let b = Tensor.from([[5.0, 6.0]]) # [1, 2]
let c = Tensor.concat([a, b]) # [3, 2]
let k1 = Tensor.randn(2, 4, 8) # [B, T, D]
let k2 = Tensor.randn(2, 1, 8) # 新しい1ポジション分
let kv = Tensor.concat([k1, k2], 1) # [2, 5, 8] — KVキャッシュへの追記
```
#### `Tensor.where(cond: Tensor, a, b) -> Tensor`
要素ごとの選択で、3つのオペランド全てを(他のbinopと同じ規則で)
broadcastします: `cond != 0 ? a : b`。`a`/`b`はスカラーでもよく、
`cond`のdtypeに合わせてliftされます。masking(attention mask、
padding mask)の基礎部品です。微分可能 — `da = g*cond`、
`db = g*(1-cond)`。`cond`自体には勾配が流れません
(numpy/PyTorch自身の`where()`と同じ規約)。
```culebra
let mask = Tensor.from([[1.0, 0.0, 1.0]]) # [1, 3]、行方向にbroadcast
let scores = Tensor.randn(4, 3)
let masked = Tensor.where(mask, scores, -1.0e9) # padding maskのパターン
```
#### `Tensor.adam_step(p, m, v, g, lr, beta1, beta2, eps, step) -> Nil`
Adamの更新を1パラメータ分その場で行います: モーメント`m`と`v`が勾配`g`で進み、
続いて`p`がバイアス補正済みの比の分だけ動きます。4つは同じ形・同じdtypeで、
`p`・`m`・`v`は書き換えられるのでviewであってはならず、3つとも評価済みである
必要があります。`step`は1始まりで、バイアス補正の指数です。
式に開くと`m = m*beta1 + g*(1-beta1)`、`v = v*beta2 + g*g*(1-beta2)`、
`p -= m/(1-beta1^step) * lr / (sqrt(v/(1-beta2^step)) + eps)`となり、
1パラメータあたり15個前後の演算と、そのそれぞれがパラメータと同じ大きさの
バッファを確保します。融合したこの形なら4つを1回なめるだけで、CUDAでは
カーネル起動も1回です。
```culebra
# doctest: skip
for i in range(params.size()) {
let p = params[i]
Tensor.adam_step(p, moments[i], velocities[i], p.grad(),
lr: 3.0e-4, beta1: 0.9, beta2: 0.95, eps: 1.0e-8,
step: t)
}
```
#### `Tensor.index_add(indices: Tensor, values: Tensor, target_shape: Array) -> Tensor`
`.index_select()`の正確な双対: `t.index_select(indices)`のような形の
`values`を、`target_shape`の新規ゼロバッファへscatter-addします。
同じインデックスが重複すれば累積されます——embeddingテーブルの勾配は
これです。`values`について微分可能(入ってきた勾配に対する
`.index_select(indices)`)。`indices`には勾配が流れません。
```culebra
let idx = Tensor.from([1.0, 1.0, 0.0]) # 行1が2回ヒット: 累積される
let values = Tensor.from([[10.0, 20.0], [30.0, 40.0], [1.0, 2.0]])
let grad = Tensor.index_add(idx, values, [4, 2]) # [[1,2],[40,60],[0,0],[0,0]]
```
#### `Tensor.scatter_to_axis(indices: Tensor, values: Tensor, size: Long) -> Tensor`
新規の末尾軸(長さ`size`)へのone-hot scatter: `indices[...] == k`のとき
`out[..., k] = values[...]`、それ以外は0。`indices`/`values`は同じ形状
である必要があります。poolingレイヤー自身の手書きbackwardが`.fold()`と
組み合わせてpadded input勾配を再構築するネイティブプリミティブです
——`.max(axis)`/`.argmax(axis)`がwindowから1要素選ぶのに対し、これは
勾配を同じwindow形状へscatterして戻します。`values`について微分可能:
各値はちょうど1つのスロットに入るので、勾配はそのスロットから取り出す
だけです。`indices`には勾配が付きません。
```culebra
let idx = Tensor.from([2.0, 0.0, 1.0])
let values = Tensor.from([5.0, 7.0, 9.0])
let out = Tensor.scatter_to_axis(idx, values, 4)
# [[0,0,5,0], [7,0,0,0], [0,9,0,0]]
```
#### `Tensor.from_csv(path: String) -> Tensor`
CSVファイルを直接contiguousなTensorに読み込みます。常に
**rank-2** を返す — 単列CSVは`[N, 1]`(biasベクトル形式)。
nested Arrayを経由しないので、`Tensor.from(load_2d(path))`
パターンより3-5x速い(MNIST規模で実測):
```culebra
# doctest: skip
let W1 = Tensor.from_csv("W1.csv") # [30, 784]
let b1 = Tensor.from_csv("b1.csv") # [30, 1]
let X = Tensor.from_csv("X.csv") # [N, 784]
```
#### `Tensor.eval(t1, t2, ...) -> Nil`
可変長のTensorを受け、依存グラフをtopological順に評価します。
共有部分式は一度だけ計算されます。学習ループのmini-batch境界で
**必ず一度呼ぶ**(呼ばないとグラフが累積してメモリが膨張)。
```culebra
# doctest: skip
W2 -= d2.dot(a1.transpose()) * lr
b2 -= d2.sum(1).reshape([N_OUT, 1]) * lr
W1 -= d1.dot(xb.transpose()) * lr
b1 -= d1.sum(1).reshape([N_HID, 1]) * lr
Tensor.eval(W1, b1, W2, b2) # 4 つを 1 パスで評価
```
### 活性化関数
Tensorのインスタンスメソッドです。ユーザのクラスが独自に`relu` /
`sigmoid` / `softmax`を定義していても(microgptの`Value.relu()`
など)、メソッド解決はクラスメソッドをビルトインより優先するため衝突
しません。
```culebra
# doctest: skip
let h = z.sigmoid() # 1/(1+exp(-z)) elementwise
let r = x.relu() # max(0, x)
let p = logits.softmax() # 最終軸で online stable
let l = p.log() # 自然対数、elementwise
let t = z.tanh() # 双曲線正接
let s = z.sin() # elementwise sin/cos
let c = z.cos()
let m = z.clamp(-1.0, 1.0) # elementwise clip、[lo, hi]に収める
```
### Tensor のメソッド
形状・線形代数・reductionはメソッド構文:
| メソッド | 戻り値 | 説明 |
|---|---|---|
| `.shape() -> Array` | Array of Long | 形状をArrayで返す |
| `.dot(other: Tensor) -> Tensor` | lazy | 行列積。rank-1/2、またはbatched(rank>=3、先頭次元が完全一致) |
| `.linear_sigmoid(x, b) -> Tensor` | lazy | 融合`sigmoid(self @ x + b)` |
| `.pow(exp) -> Tensor` | lazy | elementwise冪、expはTensorまたはscalar |
| `.gt(other) / .lt(other) / .ge(other) / .le(other) / .eq(other) / .ne(other) -> Tensor` | lazy | elementwise比較、`1.0`/`0.0`のマスクを返す。otherはTensorまたはscalar |
| `.tanh() / .sin() / .cos() -> Tensor` | lazy | elementwiseな三角関数・双曲線正接 |
| `.clamp(lo, hi) -> Tensor` | lazy | `[lo, hi]`へのelementwise clip |
| `.rope(pos: Long, base) -> Tensor` | lazy | 回転位置埋め込み(half-split方式)、最後の軸に適用。selfは`[H, D]`または`[H, T, D]`、行rの位置は`pos + (r % T)` |
| `.causal_attention(k, v) -> Tensor` | lazy | 因果的なself-attention: `softmax(self·kᵀ / sqrt(D))·v`、行`t`は行`0..t`だけを見る。`self`・`k`・`v`は同じ形で`[H, T, D]`または`[T, D]` — 下記参照 |
| `.layer_norm(gamma, beta) -> Tensor` | lazy | 最後の軸についてのlayer normalization: 行ごとに`(self - mean) / sqrt(var + 1e-5) · gamma + beta`。`gamma`・`beta`は`[D]`または`[1, D]` — 下記参照 |
| `.transpose() -> Tensor` | view | 全軸逆順(rank-2で行列転置) |
| `.permute(axes: Array) -> Tensor` | view | 任意軸並べ替え。`axes[i]`が結果の軸`i`に対応する自分自身の軸を指定 |
| `.slice(start, end) -> Tensor` | view | 軸0を`[start, end)`で切り出し |
| `.narrow(params: Array) -> Tensor` | view | `[axis, start, end]`。`.slice()`を任意軸に一般化 |
| `.reshape(dims: Array) -> Tensor` | view | 連続入力のみ。新形状 |
| `.unfold(params: Array) -> Tensor` | view | `[axis, win, step]`。`axis`沿いのスライディングウィンドウ、末尾に長さ`win`の軸を追加 |
| `.pad(params: Array) -> Tensor` | new buffer | `[axis, before, after]`。`axis`沿いにゼロパディング |
| `.fold(params: Array) -> Tensor` | new buffer | `[axis, orig_size, step]`。`.unfold()`の逆——重なる窓をscatter-addで`axis`へ戻す |
| `.sum() -> Float` | scalar | 全要素和(暗黙eval) |
| `.sum(axis: Long?, keepdims: Bool = false) -> Tensor` | lazy | 軸を1つ畳む。axisがnilなら軸指定なし=スカラー形と同じ。`keepdims: true`なら畳んだ軸を長さ1で残すので、結果はそのまま入力にブロードキャストできる: `x - x.mean(1, keepdims: true)` |
| `.mean() / .mean(axis, keepdims:)` | Float / Tensor | 同様 |
| `.max() / .max(axis, keepdims:)` | Float / Tensor | 同様 |
| `.argmax(axis: Long, keepdims: Bool = false) -> Tensor` | lazy | 軸を畳んでインデックスをFloatで格納 |
| `.index_select(indices: Tensor) -> Tensor` | lazy | 軸0方向の行gather: `out[i] = self[indices[i]]`——embeddingテーブルのlookup |
| `.softmax_cross_entropy(targets: Tensor) -> Tensor` | lazy | `[N, C]`のlogitsの行ごとにsoftmax + cross-entropyを融合。行あたり1つのloss(`[N]`) |
| `.to_array() -> Array` | eager | Culebra Arrayへ変換(暗黙eval) |
| `.item() -> Float` | eager | 唯一の要素をFloatとして取り出す。要素数が1でない(任意rank)場合は例外 |
`.item()`はスカラーの取り出し口で、`.to_array()`(形状を持つデータ用)と対をなす。
lossなど単一要素の結果をreshapeせず読むのに使う。`loss.item()`は
`to_float(loss.to_array()[0])`の置き換え。
#### `.softmax_cross_entropy(targets: Tensor) -> Tensor`
クラス方向のsoftmaxと`targets`に対するcross-entropyを1つのopに融合したもの。
`self`は`[N, C]`のlogits、`targets`は行あたり1つのクラスid(`[N]`、
`.index_select()`のindicesと同じくFloat値。要素値の範囲検査をしない点も同じ)。
結果は行ごとのlossなので、どう畳むかは呼び手が決めます:
```culebra
# doctest: skip
let rows = logits.softmax_cross_entropy(targets)
let loss = rows.mean(0) # 素直なバッチ平均
let sft = (rows * mask).sum(0) * (1.0 / kept) # 一部の行だけ採点する場合
```
自前で合成しても値は同じですが、道のりが長くなります。合成した場合、
forwardは`[N, C]`のsoftmaxとone-hotとclampしたlogを作ってから行ごとの
lossに畳み、`.backward()`はその全部を遡ります。融合すると、forwardは
各行を`logsumexp(self) - self[target]`に畳むだけで`[N, C]`のものを
何も作らず、VJPは閉形`softmax(self) - onehot`の1パスになります。
RTX 3090の`[512, 1000]`で、forwardが91 µsに対し47 µs、backwardが
203 µsに対し59 µsです。
合成版に必要なclampはop内部にあります。確率が0にアンダーフローした
クラスは、無限大のlossではなく`-log(1e-15)`を寄与します。dezero自身の
融合`SoftmaxCrossEntropy`と同じく、backwardは閉形を取り、このclampは
微分しません。
#### `.causal_attention(k, v) -> Tensor`
因果的なself-attentionを1つのopで計算します。各headの各行`t`について
`softmax(self[t]·k[0..t]ᵀ / sqrt(D))·v[0..t]` — 行は自分と自分より前の
行だけを見て、後ろの行は見ません。`self`・`k`・`v`は同じ形で、
`[H, T, D]`(head数、行数、headの幅)か、1 headなら`[T, D]`です。係数は
`1/sqrt(D)`に固定です。別の係数にしたいときは`self`を先にスケールして
ください。積は双線形なので、それで同じopになります。
```culebra
# doctest: skip
let q = x.dot(Wq).reshape([T, H, D]).permute([1, 0, 2]) # [H, T, D]
let k = x.dot(Wk).reshape([T, H, D]).permute([1, 0, 2])
let v = x.dot(Wv).reshape([T, H, D]).permute([1, 0, 2])
let ctx = q.causal_attention(k, v) # [H, T, D]
```
同じ積を手で書くと、`.dot()`、`[T, T]`のスコアへのマスクの加算、
`.softmax()`、もう1つの`.dot()`になります。融合版は、CUDAで`D`が64か
128のときforwardが1つのカーネルになり、query行のブロックごとに`k`と
`v`を流し込んで`[T, T]`のスコアを一度も書き出しません。それ以外では
同じ結果を返す参照ループです。backwardは融合前のopで確率を作り直して
attentionの標準的なpullbackを当てるので、融合したforwardも合成版と
同じように学習できます。
#### `.layer_norm(gamma, beta) -> Tensor`
最後の軸についてのlayer normalizationを1つのopで計算します。行ごとに
平均`mean`と分散`var`(`D`で割ったもの)を求め、
`(x - mean) / sqrt(var + 1e-5) · gamma + beta`を返します。`gamma`と
`beta`は最後の軸の要素1つにつき重みを1つ持ち、形は`[D]`か`[1, D]`です。
`self`のrankは1以上なら何でも構いません。epsilonは`1e-5`に固定です。
```culebra
# doctest: skip
let gamma = Tensor.ones(1, D)
let beta = Tensor.zeros(1, D)
let h = x.layer_norm(gamma, beta) # x: [T, D] または [B, T, D]
```
同じ結果を手で書くと、`.mean(last, keepdims: true)`、引き算、2乗、
もう1回の平均、`+ 1e-5`、`.pow(-0.5)`、2つの積と足し算の9個のopになり、
そのどれもが`self`と同じ大きさのバッファを新しく書きます。融合版は
CPUでは行ごとに1回なめるだけ、GPUでは1つのカーネルで済み、書き出すのは
結果だけです。backwardは閉形の式で勾配を求めます。CUDAでは3つのカーネルが
行ごとの統計量を計算し直して3つの勾配を直接書き、それ以外では融合前のopで
正規化した行を作り直します。どちらでも合成版と同じ勾配になります。
### 自動微分(reverse-mode)
Tensorプリミティブはネイティブなreverse-mode自動微分エンジンを
持ちます。forwardグラフがそのままtapeを兼ね、`.backward()`が
C++ 側でそれを辿ります。スクリプト側のラッパは不要 — 値を計算する
op自身がvector-Jacobian productを知っています。tapeが記録される
のは`requires_grad`な葉に繋がるopのみ。forward-onlyな処理
(推論、あるいはbackwardを自前で書く学習ループ)はtapeを一切
記録しないので、下層のテンソルライブラリと同等のコストで済みます。
| メソッド | 戻り値 | 説明 |
|---|---|---|
| `.requires_grad() -> Tensor` | self | 葉に勾配累積を要求。チェーン可 |
| `.backward() -> Nil` | — | `dL/dself = 1`を起点に全葉へ伝播 |
| `.grad() -> Tensor` | Tensor | 累積勾配(`backward`前はzeros) |
| `.zero_grad() -> Nil` | — | 次ステップ前に勾配をクリア |
| `.detach() -> Tensor` | Tensor | グラフも勾配も持たないmaterializedコピー |
`requires_grad`はforwardに伝播します — 勾配追跡する入力を持つop
の出力も勾配を追跡します。微分可能なopは`+ - * /`、`.pow()`(底に
ついて)、`.dot()`、軸`.sum()` / `.mean()`、`.relu()`、`.sigmoid()`、
`.softmax()`、`.log()`、`.tanh()`、`.sin()`、`.cos()`、`.clamp()`、
`.transpose()`、`.permute()`、`.reshape()`、`.slice()`、`.narrow()`、
`.pad()`、`.unfold()`、`.fold()`、
`.rope()`、`.causal_attention()`、`.layer_norm()`、`.softmax_cross_entropy()`、
`Tensor.concat()`、`Tensor.where()`、`Tensor.scatter_to_axis()`、
`.index_select()` /
`Tensor.index_add()`(互いが相手のVJP)。勾配は自動でun-broadcastされる
ので、バッチ越しに加えたbiasは元の形状に和を取って戻ります。
`.permute()`のVJPは逆置換です。これによりattentionのhead分割
(`[B, C, H, D]`から`[B, H, C, D]`)が学習できます——`.transpose()`は
*全*軸を反転するので、rank 3以上では別の操作であって、これの短縮形では
ありません。置換はノード自身のスカラースロットに1軸4ビットで収めるため、
rank 16以上の`.permute()`を通した`.backward()`は、切り詰めた置換を
保存するのではなく例外を投げます。`.rope()`も何も保存しません。
回転行列の逆は自身の転置であり、head次元の後ろ半分の符号を反転して
から回して戻せばその転置になるので、backwardは順方向をもう一度
走らせるだけで済みます——使ったcos/sinの表を持ち回る必要がありません。
im2colの3つは2組の双対として微分されます: `.pad()`のVJPは`.narrow()`が
行うcropで、`.narrow()`のVJPがそのpad。`.unfold()`のVJPは`.fold()`で、
`.fold()`のVJPは`.unfold()`です。各ノードが持つのは軸とstepだけで、
windowサイズと復元する長さは前後の形状から取れます。つまりpad -> unfold
-> reshape -> `.dot()`と書いた畳み込みは、backwardを一行も書かずに
`.backward()`で学習できます——`examples/tensor/mnist_conv.cul`を参照。
windowが重なる位置では`.fold()`がコピーを足し合わせるので、これは逆関数
ではなく転置です。
`.gt()` / `.lt()` / `.ge()` / `.le()` / `.eq()` / `.ne()`(および`.max()` /
`.argmax()`)は恒久的に微分不可能です——比較を通した`.backward()`は常に
例外を投げます(PyTorch/numpyと同じ)。0/1マスクは`*`で合成してください
(ReLU系のbackward gateが具体例)。Tanh/Sin/Cos/Clipのbackwardを
これらのopから自前で組み立てるやり方も、ネイティブVJPと共存する
選択の一つとして今も有効です——どちらの経路でも動きます。
```culebra
let w = Tensor.from([[2.0, 0.0], [0.0, 3.0]]).requires_grad()
let x = Tensor.from([[1.0], [1.0]]).requires_grad()
let y = w.dot(x) # [2, 1]
let loss = (y * y).sum(0).sum(0)
loss.backward()
Tensor.eval(w.grad(), x.grad())
let gw = w.grad().to_array() # dL/dw
```
典型的な学習ステップは、勾配をゼロ化 → forward → `.backward()` →
optimizer更新のため`.grad()`を読む → 新しい重みを`.detach()`して
次ステップをクリーンな葉から始める、という流れです。
`benchmarks/microgpt/microgpt_tensor.cul`のtransformerが完全な実例
(embedding、KVキャッシュ付きattention、RMSNorm、MLP、交差エントロピー、
Adam)で、すべてこれらのメソッドだけで構築されています。
`.backward()`はforwardバッファから勾配を読むためlossの
`Tensor.eval`を伴います。`.grad()`は他と同じTensorを返すので、
`.to_array()`の前に`Tensor.eval`でmaterializeしてください。
勾配を保持するのは葉だけです。中間ノードの勾配は、そのノードの入力へ
渡した時点で解放されるので、`.backward()`後のそのノードの`.grad()`はzerosを返します。
同じグラフに2回目の`.backward()`を通すと、葉にはその回の寄与だけが
加算されます。
`Tensor.no_grad(fn) -> Any`は勾配追跡を抑制して`fn`を実行します。
内部の演算はautogradグラフを作らず(テープも`requires_grad`伝播も
発生しない)、`fn`の戻り値をそのまま返します。推論など、逆伝播しない
forwardに使います。
```culebra
# doctest: skip
let logits = Tensor.no_grad(fn () {
model_forward(x)
})
```
### 演算子オーバーロード
`+ - * /`はブロードキャストelementwise(numpy / silarray規則)。
スカラーとの混在も自動:
```culebra
let M = Tensor.ones(3, 4)
let v = Tensor.ones(4) # → [3, 4] にブロードキャスト
let r = Tensor.ones(3, 1) # → [3, 4] にブロードキャスト
Tensor.eval(M + v, M + r, M + 1.0, M * 2.0)
```
`@`演算子は未実装(`.dot()`を使う)。
### 複合代入 (`+=` `-=` `*=` `/=` `**=`) と in-place 書き込み
複合代入は左辺のTensorバッファに直接書き戻します(新規Tensorを
確保しない)。条件は「LHSが自前バッファを所有していて、形が右辺の
broadcast結果と一致する」こと — view・未評価グラフノード・形状不一致
の場合は通常経路(新規Tensor)に自動でフォールバックします。
```culebra
# doctest: skip
mut W = Tensor.randn('f32', 1024, 256)
let alias = W
W -= grad * lr # W のバッファを直接書き換え
Tensor.eval(alias) # alias の to_array() でも更新後の値が見える
```
SGD形式の重み更新で`W = W - grad * lr`と書くと毎ステップWサイズ
分の確保が起きます。`-=`ならその確保が消えるので、巨大重みでループが
回るほど差が広がります(実測で5000ステップ・1024×256 f32のloopで
plain `=` 5.5s → `-=` 3.6s)。
サポートop: `+= -= *= /= **=`(`%=`と`@=`は対象外 — `%`は
Tensorで意味づけしておらず、`@`は出力形状が変わるためin-place不可)。
### dtype / 形状の制約
- dtypeはF32のみ(float64はGPUバックエンドに高速パスがない。
`.item()` / `.to_array()`などスカラー出口は`Float`を返す)
- `.dot()`はbatched matmul対応(rank 3以上)——両オペランドの先頭
次元が一致していれば、それぞれの末尾2軸同士でmatmulが走る
(GPU上でもbatchのsliceごとにdispatchされる)
- `.reshape()`・`.softmax()`とも非連続入力(`.transpose()`や
`.permute()`の結果など)をそのまま受け付ける——明示的な
materialize手順は不要。`.reshape()`は元が非連続なら内部で
連続バッファへclone、`.softmax()`はstrided入力を直接読む
### バックエンドとデバイス選択
評価はvendoredな`cpp-tensorlib`エンジンに委譲されます。lazy graph・カーネル融合・デバイスバックエンドはすべて
そちら側の責務です:
- **CPU** — ベクトル化カーネル(AVX2 / NEON)。macOSではBLAS形状の
カーネルにAccelerateを使用
- **GPU** — macOSはMetal、Linux / WindowsはCUDA
デバイスはプロセスグローバル(VM / JIT / AOTで共有)で、実行時に
切り替えられます:
| 関数 | 効果 |
| --- | --- |
| `Tensor.use_cpu() -> Nil` | すべての演算をCPUで評価 |
| `Tensor.use_gpu() -> Nil` | GPUバックエンドで評価 |
| `Tensor.use_auto() -> Nil` | 演算ごとに問題サイズで選択(デフォルト) |
| `Tensor.gpu_available() -> Bool` | GPUバックエンドがビルドに含まれ到達可能か |
| `Tensor.device() -> String` | 現在の選択: `'cpu'` / `'gpu'` / `'auto'` |
```culebra
inspect(type_of(Tensor.gpu_available())) # => 'Bool'
inspect(Tensor.device()) # => 'auto'
```
`device()`が返すのは選択であって、個々の演算がどこで走ったかではありま
せん。`'auto'`では演算ごとに決まりますし、GPUに送った演算も到達できな
ければCPUで実行されます。
`use_auto`がデフォルトなのは、小さいテンソルがカーネル起動コストに
負けるからです。小さい演算はCPUに留め、移送コストに見合う大きさの
演算だけをGPUに送ります。行列積をどの大きさからGPUに送るかは、
`auto`での最初の評価のときにそのマシン上で測って決めます(正方行列の
積を数個、両デバイスで計時します。一度きりで数ミリ秒)。別のマシンで
測った表ではなく、実際に載っているCPUとGPUの力関係で分かれます。
`use_cpu()` / `use_gpu()`を呼べば — 最初のテンソルを作る前でも —
以降の演算はそのデバイスに固定されます。
GPUが無いビルドで`use_gpu()`を呼んでもthrowせずCPU経路に
フォールバックするので、プログラムは可搬なままです。選択が結果を
左右する場合は`gpu_available()`を確認してください。格納はどのデバイス
でもF32のまま(上のdtype制約参照)。
Metalはビルド時に何も要りません。CUDAは`nvcc`が見つかったときに
組み込まれます(`CULEBRA_TENSOR_CUDA=AUTO`がデフォルト。`ON`は`nvcc`
が無ければconfigureエラー、`OFF`はバックエンドを外す)。CUDAドライバ
自体は実行時にロードされるので、CUDAを有効にしたバイナリはGPUの無い
マシンでもそのまま動き、`gpu_available()`が`false`を返すだけです。
### プロファイル
| 関数 | 効果 |
| --- | --- |
| `Tensor.profile(fn) -> Array` | エンジンのプロファイラを有効にして`fn`を実行し、記録した行を返す |
| `Tensor.profile_scope(label, fn) -> Any` | `label`という名前のスコープの下で`fn`を実行し、`fn`の戻り値を返す |
`Tensor.profile`が返すのは`fn`の実行中にプロファイラが記録した行で、
`fn`自身の戻り値は捨てられます。セッション中、エンジンは実行する演算ごとに
その名前のスコープ(`dot`・`sum_to`・`layer_norm`・`adam_step`など)を開き、
`.backward()`は`backward`と、VJPごとにCulebra側の演算名(`Dot`・`LayerNorm`)
のスコープを開きます。入れ子のスコープは`/`でつないだパスになります。
バックエンドが行うカーネル起動・ホストとデバイス間のコピー・ブロックする
待ちは、そのとき一番内側で開いているスコープに帰属します。
`Tensor.profile_scope`は自分で名付けたスコープ(学習のフェーズ、層など)を
`fn`の周りに足し、`fn`の戻り値をそのまま返します。セッションの外では単に
`fn`を呼ぶだけです。
各行はObjectです:
| フィールド | 意味 |
| --- | --- |
| `kind` | スコープ自身の行は`'scope'`。その下の仕事は`'launch'`・`'transfer'`・`'wait'` |
| `path` | スコープのパス(`'step/backward/Dot/dot'`) |
| `kernel` | 起動はカーネル名。転送は`'h2d'` / `'d2h'`。それ以外は`''` |
| `count` | スコープに入った回数、または起動 / 転送 / 待ちの回数 |
| `host_ms` | スコープ: 中身を含む経過時間。転送と待ち: ホストがブロックされた時間 |
| `device_ms` | 起動: デバイス上のカーネル時間の合計(バックエンドが計れる場合) |
| `device_timed` | その行の起動のうち`device_ms`に含まれる数 |
| `bytes` | 転送: 転送したバイト数 |
行はパスごとにまとまり、パスはスコープの経過時間の降順、各パスの中は
スコープ自身の行が先で、起動はデバイス時間の降順です。`device_ms`は
CUDAでは起動ごとに計ります。Metalは起動ではなくコマンドバッファ単位でしか
計れないので、起動の行は`device_timed`が0で、バッチの時間は行に含まれ
ません。WebGPUは起動回数だけを数えます。`device_timed`が`count`より小さい
行は計れなかったのであって、速いのではありません。
```culebra
let w = Tensor.from([[1.0, 2.0], [3.0, 4.0]]).requires_grad()
let rows = Tensor.profile(fn () {
Tensor.profile_scope('step', fn () {
w.dot(w).sum(0).sum(0).backward()
})
})
let paths = rows.map(|r| r.path)
inspect(paths.filter(|p| p.starts_with('step/backward/Dot')).size() > 0) # => true
```
環境変数`TL_PROFILE=1`を付けると、プログラム全体を最初の評価から
プロファイルし、終了時に同じ行を表としてstderrに出します。コードの
変更は要りません。
---
## 9. `JSON`
Culebraの値とJSONテキストの相互変換。両バックエンドで同じAPI
を提供します。
### `JSON.stringify(v, indent=0, sort_keys=false, lines=false) -> String`
`v`をJSON文字列にシリアライズします。
* `indent > 0`でそのスペース数でインデントしpretty-printします
(カンマの後に改行、`":"`の代わりに`": "`)。`indent <= 0`は
コンパクト出力。結果が確保できないほど大きくなる`indent`は
`ValueError`(`JSON.stringify() result is too large`)。
* `sort_keys=true`で`Object`のキーを挿入順ではなく辞書順で
出力します。diff / ハッシュ向けの決定論的出力に有用。
* `lines=true`で **JSON Lines** を出力します。`Array` / `Tuple` /
`Set`の各要素をそれぞれ独立した行としてcompact形式で出力し、
末尾`\n`付き。空コレクションは空文字列を返します。`indent > 0`
との併用や、Array/Tuple/Set以外への指定はどちらも`TypeError`。
旧APIとの互換: 位置引数2番目を`indent`として受けます。
`JSON.stringify(v, 2)`は`JSON.stringify(v, indent: 2)`と等価。
サポート対象:
| Culebra | JSON |
|--------------------|-----------------------------------|
| `Nil` | `null` |
| `Bool` | `true` / `false` |
| `Long`, `Float` | 数値(非有限Floatは`ValueError`を投げる) |
| `String` | クォート文字列、`\n`/`\t`/`\r`/`\"`/`\\`/`\u00xx`エスケープ |
| `Array` | JSON配列 |
| `Tuple` | JSON配列(`Array`と同じ形) |
| `Set` | JSON配列、メンバーは挿入順 |
| `Object`(Stringキーのみ)| JSONオブジェクト、キーは挿入順 |
`Function`, `Tensor`、および非Stringキーを持つObjectは
シリアライズ不可で`TypeError`を投げます。
### `JSON.parse(s, lines=false, number_mode='auto', jsonc=false) -> Any`
JSON文字列をCulebraの値に変換します。
* `number_mode='auto'`(既定): 小数点や指数を含まない数値は`Long`、
それ以外は`Float`。`Long`に収まらない整数も`Float`として読む(JSON は
数の大きさを制限しない)。`Float`にも収まらなければ`ValueError`。
* `number_mode='float'`: すべての数値を`Float`に。生産者側が
数値型を統一している場合のround-trip安全性向上に。
* `lines=true`: 入力を`\n`で分割し、空でない各行を独立したJSON
値として解析、`Array`を返します。
* `jsonc=true`: **JSONC** として解析します。`//`行コメント・`/* */`
ブロックコメント・オブジェクト/配列の末尾カンマを許容するため、
既存の設定ファイル(`tsconfig.json`、VSCodeの`settings.json`等)を
事前に除去せず読めます。既定は厳格なJSONで、コメントや末尾カンマは
`ValueError`で拒否します。
不正な入力には`ValueError`が投げられ、構造化Errorの
`e.line` / `e.col`(共に1-based、エラー位置の文字)にJSON内部の
位置が乗ります:
```culebra
let r = try {
JSON.parse('{"a": ,}')
nil
} catch e {
e
}
inspect(r.message) # => 'JSON.parse: expected value'
inspect("{r.line}:{r.col}") # => '1:7'
```
`\uXXXX`エスケープはUTF-8に復号される。BMP外の文字はJSONの規定どおり
UTF-16のサロゲートペアで書かれ、2つで1文字に合成される。片方だけでは
どの文字も指さないので`ValueError`。`stringify`は制御バイトを`\u00xx`で
出すので、自身の出力は常に読み戻せる。
```culebra
# Goのencoding/jsonは`<`・`>`・`&`をこの形でエスケープするため、Go製
# サービスの応答にはこれが含まれる。
inspect(JSON.parse('"\u003cb\u003e"')) # => ''
inspect(JSON.parse('"\uD83D\uDE00"')) # => '😀'
```
ネストの深さには上限がある: コンテナのネストが1000段を超えると、Cスタックを
使い果たす代わりに`ValueError`(`nesting too deep (limit 1000)`)を投げる。
`stringify`も走査する値の木に同じ上限を適用する。
例:
```culebra
let v = {name: 'alice', age: 30, tags: ['admin', 'staff']}
# 既定はコンパクト。`sort_keys` はキーを辞書順に並べる。
inspect(JSON.stringify(v)) # => '{"name":"alice","age":30,"tags":["admin","staff"]}'
inspect(JSON.stringify(v, sort_keys: true)) # => '{"age":30,"name":"alice","tags":["admin","staff"]}'
let back = JSON.parse(JSON.stringify(v))
inspect(back.name) # => 'alice'
let arr = JSON.parse("1\n2\n3\n", lines: true)
inspect(arr) # => [1, 2, 3]
let cfg = JSON.parse(
'{
// コメントと末尾カンマを許容
"port": 8080,
}',
jsonc: true,
)
inspect(cfg.port) # => 8080
```
`indent`は整形出力、`lines`はJSON Linesを出します。どちらも複数行に
なります:
```culebra
let v = {name: 'alice', age: 30, tags: ['admin', 'staff']}
inspect(JSON.stringify(v, indent: 2))
inspect(JSON.stringify([1, 2, 3], lines: true))
# => |
# '{
# "name": "alice",
# "age": 30,
# "tags": [
# "admin",
# "staff"
# ]
# }'
# '1
# 2
# 3
# '
```
JITメモ: ビルトインの`JSON.{stringify, parse}`は他の名前空間
メソッドと同じ正準呼び出しリゾルバを経由するため、すべての呼び出し
形がVMと同一に振る舞います。位置引数の束縛
(`JSON.stringify(v, 2)`は`indent`、`JSON.parse(s, true)`は
`lines`)、キーワード引数、リテラル`**{...}`と動的`**variable`
splatの両方。第一級の値として使った場合も束縛は同じです
(`let f = JSON.stringify; f(v, indent: 2)`)。
---
## 10. `Args`
宣言的なCLI引数パーサ。specはculebra `Object`でpositional / option /
subcommandを列挙し、`Args.parse`はparse結果を`Object`で返す。
`--help`指定でhelpをstdoutに出して`Sys.exit(0)`、パースエラー時は
stderrにerror表示 + `Sys.exit(2)`。プログラム制御したい場合は
`Args.try_parse`を使うと`ArgParseError`が発生し、`--help`では
`{kind: "ArgParseHelp", help}`がthrowされる。
### `Args.parse(argv: Array, spec: Object) -> Object`
`argv` (通常`Sys.argv`) を`spec`に従ってparse。`--help` / `-h`でhelp
をstdout出力 + `Sys.exit(0)`、パースエラーでstderr出力 + `Sys.exit(2)`。
### `Args.try_parse(argv, spec) -> Object`
同じエンジンだがexitせず例外をthrow。テストや独自UX構築用。
### `Args.help(spec: Object) -> String`
parse / exitせずにhelp文字列だけ取得。広めのメッセージに埋め込みたい
時など。
### Spec 形式
各引数は次のフィールドを持つ`Object`:
| field | 型 | デフォルト | 意味 |
|---|---|---|---|
| `name` | `String` | (必須) | parse結果のkey |
| `type` | `String` | `"String"` | `"String"` / `"Long"` / `"Float"` / `"Bool"` |
| `short` | `String` | (なし) | 短縮形 (`"v"` → `-v`)。指定するとoption扱い。 |
| `default` | `Any` | (なし) | 省略時の値。指定するとoptional。 |
| `doc` | `String` | `""` | help用説明文 |
| `repeated` | `Bool` | `false` | 複数指定可、`Array`で集約 |
| `positional` | `Bool` | (推論) | `true`でpositional、`false`でoptionに固定 |
`type: "Bool"`の場合は値を取らない **flag** (`--verbose` / `-v`)。それ以外
の型は次のトークンを値として消費する (`--count 5` / `--count=5`)。
`short`も`default`も無い引数は **positional** 扱い。また`default`を持つか
`repeated`か`Bool` flagのいずれかであれば **省略可能**、そうでなければ必須。
`positional`はこの2つの規則が食い違う場合にどちらかを明示する — 省略可能な
positionalは「省略時の値」を言うために`default`が要るが、それだけだと
optionと見なされてしまうため。
```culebra
{name: "file", positional: true, default: "-"} # cat []
{name: "file", positional: true, default: nil} # 同じ。省略時はnil
{name: "out", positional: false} # 必須の--out
```
positionalはspec順にマッチするが、省略可能なものは「後続の必須positionalが
必要とする数」より多くトークンが残っている時だけ受け取る。つまり
`[{name: "files", repeated: true}, {name: "dest"}]`は`a b d`を
`files == ["a", "b"]` / `dest == "d"`とparseする。
### 例
```culebra
# doctest: skip
let spec = {
name: "wc-lite",
doc: "count lines and words",
args: [
{name: "input", type: "String", doc: "input file"},
{
name: "lines",
short: "l",
type: "Bool",
default: false,
doc: "count lines",
},
{
name: "words",
short: "w",
type: "Bool",
default: false,
doc: "count words",
},
{name: "encoding", type: "String", default: "utf-8"},
],
}
let args = Args.parse(Sys.argv, spec)
inspect(args.input)
if args.lines {
inspect("lines: ...")
}
if args.words {
inspect("words: ...")
}
inspect("encoding: {args.encoding}")
```
```
$ ./wc-lite -l file.txt
lines: ...
encoding: utf-8
$ ./wc-lite --help
wc-lite - count lines and words
Usage: wc-lite [options]
Arguments:
input input file
Options:
-l, --lines count lines
-w, --words count words
--encoding
-h, --help show this help and exit
```
### サブコマンド
`spec.subcommands`は`Array', got ''
inspect("{r.line}:{r.col}") # => '2:6'
```
### `XML.events(text, keep_space=false) -> Iterator`
`parse`と同じパーサを1イベントずつ使う。木として持つには大きすぎる文書や、
一部だけが必要な場合に向く。各イベントは`Object`:
| `kind` | ほかのフィールド |
| --- | --- |
| `'start'` | `tag`、`ns`、`attrs` |
| `'end'` | `tag`、`ns` |
| `'text'` | `text` |
``は`start`と`end`を1つずつ生む。テキストのまとめ方と空白の扱いは`parse`と
まったく同じで、コメントと処理命令は何も生まない。
呼び出した時点では何も読まない。各ステップは次のイベントまでしかparseしないので、
不正な文書は反復が問題箇所に達したときに、それより前のイベントをすべて返した後で
`XML.events:`で始まるメッセージの例外を投げる。`for`ループを`break`で途中で抜けると
イテレータが閉じてパーサが解放され、残りのテキストは読まれない。
```culebra
let texts = XML.events('boot ready')
.filter(|ev| ev.kind == 'text')
.map(|ev| ev.text)
.collect()
inspect(texts) # => ['boot', 'ready']
# The tail is not XML, but the loop stops before reaching it.
for ev in XML.events('boot<<<') {
inspect(ev) # => {kind: 'start', tag: 'log', ns: nil, attrs: {}}
break
}
```
### `el.find(path, namespaces={}) -> Object | Nil`, `XML.find(el, path, namespaces={}) -> Object | Nil`
### `el.find_all(path, namespaces={}) -> Array`, `XML.find_all(el, path, namespaces={}) -> Array`
### `el.find_iter(path, namespaces={}) -> Iterator`, `XML.find_iter(el, path, namespaces={}) -> Iterator`
`el`以下の木を`path`で検索する。`find_all`はすべての一致を文書順に重複なく返し、
`find`はその最初(無ければ`nil`)、`find_iter`は`find_all`と同じ一致を`Iterator`で
返す。返るのは要素そのもので、コピーではない。
パス言語はXPathの部分集合で、XPath 1.0の省略形のロケーションパスに、後の版の名前の
形式を2つ加えたもの(XPath 2.0の`*:tag`とXPath 3.0の`Q{uri}tag`)。各形式の意味は
XPathでの意味と同じ。パスは`el`からの相対で、ステップは`/`で区切る。
| ステップ | 選ぶもの |
| --- | --- |
| `tag` | 名前が`tag`の子要素 |
| `p:tag` | `p`に束縛された名前空間にある、名前が`tag`の子要素 |
| `Q{uri}tag` | 名前空間`uri`にある、名前が`tag`の子要素(`Q{}tag`は名前空間なし) |
| `*:tag` | 名前空間の有無を問わず、名前が`tag`の子要素 |
| `*`、`p:*`、`Q{uri}*` | すべての子要素(その名前空間のもの) |
| `.` | 要素自身 |
| `..` | 親(`el`より上には何も無い) |
| `a//b` | `a`の下の任意の深さにある`b`要素(`.//b`は`el`の下の任意の位置) |
| 述語 | 残す要素 |
| --- | --- |
| `[@attr]` | 属性`attr`を持つ |
| `[@attr='v']` | 属性`attr`が`v`に等しい(`"v"`も可) |
| `[p]` | 相対パス`p`がその要素から選ぶ要素がある(`[title]`、`[pitch/step]`、`[.//b]`、`[../note]`) |
| `[p='v']` | `p`が選ぶ要素のうちテキストが`v`のものがある(`[title='B']`) |
| `[p/@attr]`、`[p/@attr='v']` | `p`が選ぶ要素のうち属性`attr`を持つ(`v`に等しい)ものがある。`p//@attr`はそれらの要素とその下のすべての要素を見る |
| `[n]` | 候補の`n`番目(1始まり) |
| `[last()]`、`[last()-n]` | 最後の候補、またはその`n`個前 |
述語は連ねられ(`book[@lang='en'][2]`)、それぞれが前の述語の残したものをさらに
絞る。XPathと同じく位置は1つの親の子の中で数える。`book[1]`は`book`を持つ各親の
最初の`book`で、`.//book[1]`は`el`自身と、その下の各要素の最初の`book`子要素。
述語中の属性名は文書に書かれたとおりの名前で照合する(`[@xlink:href]`)。
述語中のパスは候補の要素から始まり、外側のパスと同じステップを`.`と`..`も含めて使え、
自身の述語も持てる(`note[pitch[step='C']]`)。その`..`は`el`まで上がり、それより上へは
行かない。名前は外側のパスと同じく解決する。XPathと同じく、`[p='v']`は`p`が選ぶ要素の
どれか1つのテキストが`v`なら成り立つ。述語の入れ子は1000段まで。
**名前と名前空間。** パス中の接頭辞はまず`namespaces:`で引き、無ければ`el`自身の
`xmlns:p`宣言で引く(祖先の宣言は見ない。要素は祖先を記録していない)。どちらにも
無い接頭辞は`ValueError`。接頭辞の無い名前は、既定の`xmlns=`の下にあっても名前空間の
無い要素にしか一致しない。そうした要素に一致させるには`namespaces:`で`""`をそのURIに
対応させる(XPath 2.0の既定要素名前空間)か、`Q{uri}tag`か`*:tag`と書く。
それ以外はすべて、その構文と桁を示す`ValueError`で拒否する: 軸(`following-sibling::`)、
`last()`以外の関数、`and`、`or`、`=`以外の比較とリテラル以外との比較、和集合(`|`)、
述語中のパスの末尾以外にある属性ステップ(`a/@href`)、絶対パス(`/a`、`//a`。述語中でも
同じ: "a path is relative to the element")。
`Q`の無い波括弧の名前(`{uri}tag`、`{*}tag`)もXPathではなく、そのエラーはXPathでの
書き方(`Q{uri}tag`、`*:tag`)を示す。
```culebra
let lib = XML.parse('ABC')
let titles = |xs| xs.map(|b| b.text('title'))
inspect(titles(lib.find_all('.//book'))) # => ['A', 'B', 'C']
inspect(titles(lib.find_all(".//book[@lang='en']"))) # => ['A', 'C']
inspect(titles(lib.find_all('.//book[1]'))) # => ['A', 'C']
inspect(titles(lib.find_all('shelf/book[last()]'))) # => ['B', 'C']
inspect(lib.find_all(".//book[title='B']/..").size()) # => 1
inspect(lib.find_iter('.//title').count()) # => 3
lib.find('shelf/following-sibling::shelf') # !! unsupported path syntax 'following-sibling::' at column 7
inspect(lib.find_all("shelf[book/@lang='fr']").size()) # => 1
inspect(titles(lib.find_all(".//book[../book/title='B']"))) # => ['A', 'B']
inspect(lib.find_all("shelf[book[title='C']/@lang='en']").size()) # => 1
```
名前空間を使う場合:
```culebra
let feed = XML.parse('First')
inspect(feed.ns) # => 'urn:ex:feed'
inspect(feed.children[0].children[1].ns) # => 'urn:ex:media'
let ns = {f: 'urn:ex:feed'}
inspect(feed.text('f:entry/f:title', namespaces: ns)) # => 'First'
# `m` is not in `ns`; the root's own xmlns:m binds it.
inspect(feed.find('f:entry/m:image', namespaces: ns).attrs.href) # => 'a.png'
inspect(feed.find('entry')) # => nil
inspect(feed.find_all('entry', namespaces: {'': 'urn:ex:feed'}).size()) # => 1
inspect(feed.find_all('*:entry').size()) # => 1
```
### `el.text(path=".", namespaces={}) -> String | Nil`, `XML.text(el, path=".", namespaces={}) -> String | Nil`
`path`に最初に一致した要素のテキスト。XPathのstring-value、つまりその下のすべての
テキストを文書順につないだもの。一致が無ければ`nil`、テキストの無い要素なら`''`。
`parse`が要素の間で捨てた空白は含まない。残すには`keep_space: true`で読む。
```culebra
inspect(XML.parse('a bold word
').text()) # => 'a bold word'
inspect(XML.parse('
').text('q')) # => ''
inspect(XML.parse('').text('q')) # => nil
```
### `el.stringify(indent=0, declaration=false) -> String`, `XML.stringify(el, indent=0, declaration=false) -> String`
要素をXMLテキストとして書く。`tag`、順序どおりの`attrs`、`children`の順に出力する。
`ns`は読まない。名前空間宣言は`attrs`から出る。
* `tag`は`String`でなければならない。`attrs`と`children`は省略(または`nil`)できる。
ある場合、`attrs`は値が`String`・`Long`・`Float`・`Bool`の`Object`、`children`は
要素と`String`の`Array`。それ以外は`TypeError`。
* タグと`attrs`の各キーは`parse`が受け付ける名前(修飾名: 名前文字で書き、`:`は
1つまでで両端には置かない)でなければならず、そうでなければ`TypeError`。
テキストと属性値にUTF-8でないバイト列や、XML文書に含められない文字(大半の
制御文字)があれば`ValueError`。
* 子の無い要素は``と書く。
* テキストは`&`、`<`、`>`と復帰(`
`)をエスケープする。属性値は`"`で囲み、
`&`、`<`、`"`をエスケープし、タブ・改行・復帰を文字参照で書くので、どれも読み戻すと
元のままになる。
* `indent > 0`は子要素を1つずつインデントした行に置くが、子がすべて要素である要素に
限る。テキストを1つでも持つ要素は中身全体を1行に保つので、インデントがテキストを
変えることはない。
* `declaration: true`は出力の先頭に``と改行を
置く。
* 1000段を超えて入れ子になった要素は`parse`と同じく`ValueError`になる。
`parse`が返した任意の`e`について、2回のparseが同じ`keep_space`を使えば、`indent`の
有無にかかわらず`XML.parse(XML.stringify(e))`は`e`に等しい。
```culebra
inspect(XML.stringify({tag: 'point', attrs: {x: 1, y: 2.5, note: 'a "b"'}})) # => ''
let page = XML.parse('HiSay hi
')
println(page.stringify(indent: 2, declaration: true))
# => |
#
#
#
# Hi
#
# Say hi
#
```
---
## 26. `SQLite`
[SQLite](https://sqlite.org) による組み込みSQLデータベース(amalgamationを
vendor同梱・コンパイル済みで、システムライブラリ不要)。`SQLite.open`は
ステートフルな **Database** ハンドルを返す。高レベルの`execute` / `query` /
`transaction`が日常的なCRUDを、`prepare`が返す再利用可能な **Statement**
ハンドルがホットループを担う。どちらのハンドルもスコープを抜けると確定的に
クローズされるので、明示的な`close` / `finalize`は任意。
| 関数 | 結果 |
| --- | --- |
| `SQLite.open(path: String) -> Database` | DBを開く(無ければ作成)。`":memory:"`でインメモリ |
| `SQLite.version() -> String` | リンクされたSQLiteライブラリのバージョン(例`"3.53.2"`) |
### Database
| メソッド | 結果 |
| --- | --- |
| `db.execute(sql: String, params = nil) -> Long` | 文を1つ実行。影響行数を返す |
| `db.query(sql: String, params = nil) -> Array` | クエリを実行。各行は列名をキーとする`Object` |
| `db.prepare(sql: String) -> Statement` | 再利用可能な文をコンパイル |
| `db.transaction(fn: Function) -> Any` | `BEGIN` → `fn`実行 → `COMMIT`。throwされると`ROLLBACK`して再送出 |
| `db.close()` | 接続を閉じる(スコープ離脱時にも自動実行) |
### Statement
| メソッド | 結果 |
| --- | --- |
| `stmt.run(params = nil) -> Long` | 実行(INSERT/UPDATE/DELETE)。影響行数を返す |
| `stmt.query(params = nil) -> Array` | クエリを実行し行を収集 |
| `stmt.finalize()` | 文を解放(スコープ離脱時にも自動実行) |
### パラメータ
`params`はSQLのプレースホルダを束縛する。**Array** は位置プレースホルダ
`?`を左から順に、**Object** は名前付きプレースホルダ`:name`(`@name` /
`$name`も可)をキーで束縛する:
```culebra
# doctest: skip
db.execute("INSERT INTO users VALUES (?, ?)", [1, "Alice"])
db.query("SELECT * FROM users WHERE id = :id", {id: 1})
```
### 型マッピング
値は読み出し時は列の実行時型で、書き込み時はculebra値の型で対応づけられる:
| SQLite | Culebra |
| --- | --- |
| INTEGER | `Long` |
| REAL | `Float` |
| TEXT | `String` |
| BLOB | `String`(生バイト列) |
| NULL | `nil` |
書き込み側では`Bool`は`0` / `1`として束縛される。それ以外の型(`Array`・
`Object`・関数など)の束縛は`TypeError`。SQLエラーや制約違反はSQLite自身の
メッセージを載せた`SQLiteError`を送出する:
```culebra
let db = SQLite.open(":memory:")
db.execute("CREATE TABLE users (id INTEGER, name TEXT)")
db.execute("INSERT INTO users VALUES (?, ?)", [1, "Alice"])
let rows = db.query("SELECT * FROM users")
inspect(rows[0]["name"]) # => 'Alice'
# 再利用可能なプリペアド文
let ins = db.prepare("INSERT INTO users VALUES (?, ?)")
for u in [[2, "Bob"], [3, "Carol"]] {
ins.run(u)
}
ins.finalize()
# all-or-nothing
db.transaction(fn () {
db.execute("UPDATE users SET name = 'Bob!' WHERE id = 2")
})
let r = try {
db.query("SELECT * FROM missing")
nil
} catch e {
e
}
inspect(r.kind) # => 'SQLiteError'
db.close()
```
Database / Statementハンドルは生成したスレッド(isolate)に紐づく。`Sendable`
ではなく、`Isolate` / `Channel`境界を越えて渡せない。トランザクションはネスト
できない(必要なら`SAVEPOINT`を直接使う)。
---
## 27. `Canvas`
小さなゲームやピクセルグラフィックス向けのイミディエイトモード2Dフレーム
バッファ。フレームを描き、`present`で提示し、入力をポーリングして繰り返す。
色はpacked RGBA `Long`、バッファは任意サイズ(WASM-4流の160×160が典型)。
WASM PlaygroundではCanvasプログラムは **Canvasタブ**で動く — フレームは
`