本文へ移動
Hikari 仕様

std:fs — ファイルシステム

標準ライブラリモジュール (index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../language-spec.md を参照。

import fs := "std:fs"read / read_bytes / write / write_bytes / mkdir / mkdtemp / readdir / walk / remove / remove_all / rename / exists / stat / open / open_rw の 15 slot と、型メンバー File を持つ namespace object を fs に束縛する。名前 destructure (language-spec.md §6.3) で個別に取り出すか、namespace のまま slot アクセスする:

# 個別に引く
import { read, write } := "std:fs"
content := read("foo.hika")!.unwrap!

# namespace のまま使う
import fs := "std:fs"
content := fs.read("foo.hika")!.unwrap!
名前 効果 意味
read read(path) Fs path のファイルを UTF-8 テキストとして読み、Future (Result(String)) を返す
read_bytes read_bytes(path) Fs path のファイルを Bytes として読み (デコードなし)、Future (Result(Bytes)) を返す
write write(path, content) Fs path に content を UTF-8 テキストとして書き出し (上書き)、Future (Result(Unit)) を返す
write_bytes write_bytes(path, content) Fs path に Bytes content をそのまま書き出し (上書き)、Future (Result(Unit)) を返す
mkdir mkdir(path) Fs path を親ごと再帰作成 (冪等)、Future (Result(Unit)) を返す
mkdtemp mkdtemp(prefix) Fs ホストの一時ディレクトリ直下に prefix で始まる一意な dir を作り、その完全パスを Future (Result(String)) で返す
readdir readdir(path) Fs path 直下のエントリ ({name, is_dir} の List) を読み、Future (Result(List)) を返す
walk walk(path) Fs path 配下の通常ファイルのパス (String の List) を再帰的に集め、Future (Result(List)) を返す
remove remove(path) Fs path (ファイル or 空ディレクトリ) を 1 つ削除、Future (Result(Unit)) を返す
remove_all remove_all(path) Fs path を中身ごと再帰削除 (冪等)、Future (Result(Unit)) を返す
rename rename(old, new) Fs old を new へ改名/移動、Future (Result(Unit)) を返す
exists exists(path) Fs path の存在を確かめ、Future (Result(Bool)) を返す
stat stat(path) Fs path のメタ情報 ({size, mtime, is_dir}) を読み、Future (Result) を返す

以下の例では import fs := "std:fs" で束縛したものとして fs.read 等で記す。全関数は Future を返し、! で resolve 値 (Result) を待つ。非 String 引数は呼び出し位置で Error (panic 型, language-spec.md §16)。

1. fs.read

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • 戻り値: Future! (= ()) で resolve 値を待つ (prelude.md §9.4 のブロッキング I/O 規則に乗る)
  • resolve 値は Result(String):
    • 成功時: Ok(content) — content はファイル内容の String (UTF-8 として読む)
    • 失敗時: Err(e) — e は回復可能エラーオブジェクト {kind, message} (§14)。kind は機械可読な種別 String、message は人間可読 String
match fs.read("foo.hika")! {
  Ok(content) => use(content)
  Err(e) => match e.kind {
    "not_found" => print("no such file")
    _ => print(e.message)  # default = 未知 kind
  }
}

ファイル内容に不正な UTF-8 バイト列が含まれる場合の挙動は実装定義 (バイト列のまま String として渡す可能性があり、後続の文字列処理が壊れることがある)。バイナリやエンコーディングの不確かなデータを扱うなら fs.read_bytes を使うこと。

2. fs.read_bytes

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • 戻り値: Future! (= ()) で resolve 値を待つ
  • resolve 値は Result(Bytes):
    • 成功時: Ok(content) — content はファイルの生バイト列 (Bytes、デコードなし)
    • 失敗時: Err(e) — e は回復可能エラーオブジェクト {kind, message} (§14)。kind は機械可読な種別 String、message は人間可読 String
match fs.read_bytes("image.png")! {
  Ok(content) => print(content.length!)  # content.0 は先頭バイト (Int 0〜255)
  Err(e) => print(e.message)
}

UTF-8 として解釈したい場合は content.to_string! を呼ぶ。不正 UTF-8 なら None が返る。生バイト列をそのまま別ファイルへ書くなら fs.write_bytes (§4) と対で使う。

3. fs.write

  • 引数: (path, content) 2-tuple。両方 String。非 String を渡すと呼び出し位置で Error
  • 既存ファイルは 上書き (truncate 後に書き込み)
  • 親ディレクトリが存在しない場合は失敗 (mkdir はしない。先に fs.mkdir で作る)
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(Unit):
    • 成功時: Ok(()) — success に値は無い (Result(Unit))
    • 失敗時: Err(e) — e は回復可能エラーオブジェクト {kind, message} (§14)。kind は機械可読な種別 String、message は人間可読 String
match fs.write("out.txt", "hello")! {
  Ok(_) => ()
  Err(e) => print(e.message)
}

バイト列をそのまま書くなら fs.write_bytes (§4) を使う。

4. fs.write_bytes

  • 引数: (path, content) 2-tuple。path は String、contentBytes。型違いは呼び出し位置で Error
  • content のバイト列を デコード/変換せずそのまま 書く。fs.read_bytes (§2) で読んだ生バイト列をそのまま書き戻せる
  • 既存ファイルは 上書き (truncate 後に書き込み)
  • 親ディレクトリが存在しない場合は失敗 (mkdir はしない)
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(Unit):
    • 成功時: Ok(())
    • 失敗時: Err(e){kind, message} (§14)
match fs.read_bytes("a.png")! {
  Ok(bytes) => fs.write_bytes("b.png", bytes)!.unwrap!  # 生バイト列をそのままコピー
  Err(e) => print(e.message)
}

テキストを書くなら fs.write (§3)。

5. fs.mkdir

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • 親ディレクトリごと 再帰的に作成 し、冪等 (既に同じ dir が存在しても成功する。mkdir -p 相当)
  • 経路上の構成要素に 同名の非ディレクトリ (既存ファイル等) があると失敗
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(Unit):
    • 成功時: Ok(())
    • 失敗時: Err(e){kind, message} (§14)。経路上に非ディレクトリがあるときは io_error
match fs.mkdir("data/2026-06-19")! {
  Ok(_) => ()
  Err(e) => print(e.message)
}

5.1 fs.mkdtemp

  • 引数: prefix (String) 1 個。非 String を渡すと呼び出し位置で Error
  • ホストの一時ディレクトリ (TMPDIR 等。どこかは実装定義) の直下に、prefix で始まる一意な名前のディレクトリを作り、その完全パスを返す
  • 名前の残りは実装が選ぶ。同じ prefix で何度呼んでも衝突しない — 作成は排他的で、既にあるディレクトリのパスを返すことはない
  • unix では 0700 で作る。一時ディレクトリは他の利用者と共有する場所なので、umask 任せにすると中身を差し替えられる
  • 自動では消えない。 プロセスが終わっても残る。後始末は呼び手の責任で、相手が §7.1 fs.remove_all である
  • prefix にパス区切りを含めてはならない (含むと io_error)。置き場所を選ぶ口ではない — 場所を選びたいなら mkdir で自分の名前を作る
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(String):
    • 成功時: Ok(path) — 作ったディレクトリの完全パス
    • 失敗時: Err(e){kind, message} (§14)。一時ディレクトリへ書けないときは permission_denied / io_error
dir := fs.mkdtemp("hikari-build-")!.unwrap!
fs.write(path.join([dir, "out.txt"]), body)!?
fs.remove_all(dir)!?

6. fs.readdir

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(List):
    • 成功時: Ok(entries)entriespath 直下のエントリの List。各要素はスロット {name, is_dir} を持つ closed・immutable なレコード:
      • name — エントリの basename (String。path 自体は含まない)
      • is_dir — ディレクトリなら true、それ以外は false (Bool)
    • 失敗時: Err(e){kind, message} (§14)。path 不在は not_foundpath がディレクトリでないときは io_error
  • 列挙順は name の昇順で決定的
  • . / .. は含めない。ドットファイル (.trash 等) は含む
  • is_dirリンクを辿らずに決める (§12.1)。ディレクトリへのリンクは is_dir=false である
match fs.readdir(".")! {
  Ok(entries) => entries.each { e | print(e.name) }
  Err(e) => print(e.message)
}

6.1 fs.walk

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • readdir再帰版path 配下の通常ファイルのパスを深さ優先で集める。ディレクトリ自身は結果に含めない
  • 集めるのは通常ファイルだけである。ディレクトリ・シンボリックリンク・名前付きパイプ・ソケット・デバイスは結果に載せない — 通常ファイルでないものを混ぜると、受け取った側が read して止まる
  • ツリーの中のリンクは辿らない (§12.1)。ディレクトリへのリンクの下へは降りず、壊れたリンクがあっても歩き続ける。path 自身がリンクならそれは辿る
  • path が通常ファイルなら [path] を返す (単一要素)
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(List):
    • 成功時: Ok(paths)pathsString の List。各要素は path を接頭にした完全パス (readdir の basename とは異なる)
    • 失敗時: Err(e){kind, message} (§14)。path 不在は not_found
  • 列挙順は パスの昇順で決定的。ドットファイルも含む
match fs.walk("docs")! {
  Ok(files) => files.each { p | print(p) }  # docs/specs/std/fs.md ...
  Err(e) => print(e.message)
}

7. fs.remove

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • ファイル または空のディレクトリ を 1 つ削除する。再帰削除はしない
  • 非空のディレクトリを渡すと失敗 (io_error)。ディレクトリツリーを消すなら §7.1 fs.remove_all を使う
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(Unit):
    • 成功時: Ok(())
    • 失敗時: Err(e){kind, message} (§14)。path 不在は not_found、非空ディレクトリは io_error
match fs.remove("out.txt")! {
  Ok(_) => ()
  Err(e) => print(e.message)
}

7.1 fs.remove_all

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • remove再帰版path がファイルならそれを、ディレクトリなら中身ごと消す (rm -rf 相当)
  • 冪等path が不在でも Ok(()) を返す。remove (不在は not_found) とわざと違える。後始末が求めているのは「無くなっていること」であって、既に無いのは失敗ではない。mkdir が「既にあっても成功」なのと同じ向きである
  • シンボリックリンクを辿らない。 path 自身がリンクならリンクを消し、リンク先には触れない。ツリーの中のリンクも同じ — 辿ると消す対象の外まで消えてしまう。path 自身も辿らない点だけ §12.1 の既定と違う (消す操作なので、名指されたリンクのを消すわけにはいかない)
  • 途中で失敗したときにどこまで消えたかは実装定義。部分的に消えたツリーが残りうるので、失敗を握り潰して先へ進んではならない
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(Unit):
    • 成功時: Ok(())path が無くなった (元から無かった場合を含む)
    • 失敗時: Err(e){kind, message} (§14)。権限不足は permission_denied、それ以外は io_error
match fs.remove_all("build/stage")! {
  Ok(_) => ()
  Err(e) => print(e.message)
}

8. fs.rename

  • 引数: (old, new) 2-tuple。両方 String。非 String を渡すと呼び出し位置で Error
  • oldnew へ改名・移動する
  • 既存の 宛先ファイルは上書き する。宛先が非空ディレクトリのときは失敗 (io_error)
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(Unit):
    • 成功時: Ok(())
    • 失敗時: Err(e){kind, message} (§14)。old 不在・new の親ディレクトリ不在は not_found、ファイルシステムを跨ぐ移動 (EXDEV) は io_error (自動コピーはしない)
match fs.rename("a.md", ".trash/a.md")! {
  Ok(_) => ()
  Err(e) => print(e.message)
}

9. fs.exists

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result(Bool):
    • 存在する: Ok(true)
    • 確実に存在しない: Ok(false)
    • 判定自体が失敗 (権限不足など): Err(e){kind, message} (§14)
  • 不在 (not_found 相当) は エラーにせず Ok(false) へ倒す。Err は「存在を確かめられなかった」場合に限る
match fs.exists("data")! {
  Ok(true) => ()
  Ok(false) => fs.mkdir("data")!.unwrap!
  Err(e) => print(e.message)
}

10. fs.stat

  • 引数: path (String) 1 個。非 String を渡すと呼び出し位置で Error
  • 戻り値: Future! で resolve 値を待つ
  • resolve 値は Result:
    • 成功時: Ok(info)info はスロット {size, mtime, is_dir} を持つ closed・immutable なレコード:
      • size — バイト数 (Int)。ディレクトリに対する値は OS 定義で、意味を持たない
      • mtime — 最終更新時刻を epoch ミリ秒 で表す Int (now() と同じ単位・基点)
      • is_dir — ディレクトリなら true、それ以外は false (Bool)
    • 失敗時: Err(e){kind, message} (§14)。path 不在は not_found
match fs.stat("note.md")! {
  Ok(info) => print(info.size)  # info.mtime はミリ秒 Int、info.is_dir は Bool
  Err(e) => print(e.message)
}

11. ファイルハンドル File — 位置指定の部分読み書き

全体読み書き (read / write 等) がファイル 1 個 = 値 1 個の写像であるのに対し、File ハンドルはファイルの一部分をオフセット指定で読み書きする (SQLite のようなページ構造のバイナリファイルを直接扱う用途)。カーソル (seek) は持たず、位置は毎回明示する (POSIX の pread/pwrite に対応)。

import fs := "std:fs"

f := fs.open_rw("data.db")!.unwrap_or_else { e | panic(e.message) }
page := f.read_at(4096, 4096)!      # 2 ページ目を読む (Result(Bytes))
f.write_at(0, header)!              # 先頭に書く
f.sync()!                           # fsync (耐久性の確定)
f.close()!

コンストラクター

名前 効果 意味
open fs.open(path) Fs 読み取り専用で開く。不在は Err(not_found)。→ Future(Result(File))
open_rw fs.open_rw(path) Fs 読み書きで開く (不在なら作成)。→ Future(Result(File))

非 String 引数は呼び出し位置で Error (panic 型, language-spec.md §16) になる。

「作成して空にする」は open_rw + truncate(0) の合成で表す。

File のメソッド

I/O メソッドは他の fs slot と同じく Future(Result(...)) を返す (prelude.md §9.4 のブロッキング I/O 契約)。

メソッド 効果 意味
read_at f.read_at(offset, n) Fs オフセットから最大 n bytes 読む → Ok(Bytes)。EOF 越えは短い Bytes (長さで判定)。offset は非負 Int (負は Error)。n0..2^30 (1 GiB) の Int (0 は空 Bytes・範囲外は Error)
write_at f.write_at(offset, bytes) Fs オフセットに全量書く → Ok(())。末尾を越える位置への書き込みはファイルを拡張する
size f.size() Fs 現在のサイズ (bytes) → Ok(Int)
truncate f.truncate(n) Fs サイズを n に切り詰め / 拡張 → Ok(())
sync f.sync() Fs OS バッファーをディスクへ確定 (fsync) → Ok(())
close f.close() Fs 閉じる → Ok(())。呼んだ時点でハンドルは封印され、以降どのメソッドを呼んでも Error (use after close。array.md §2.1 の frozen 封印と同じ動的規律)

ハンドル規律

Filearray.mdArrayrandom.md のジェネレーターと同じ可変 scratch ハンドルである。束縛自体は immutable (f = other は再代入エラー)、内部状態 (OS ファイル記述子) だけがメソッドで前進する。値型ではないため == / compare を持たず、std:map / std:set のキー / 要素にできない。

Filestd:fs型メンバーとして export され、型注釈に書ける (fs.File、または import { File } := "std:fs")。不透明型であり内部表現は露出しない。

File非 Copyable である (../language-spec.md の複製の節)。ホストのファイル記述子を持つ可変ハンドルで、作り直しても同じものにならないためである。copy は panic し、spawn / std:parallel の捕獲検査も弾く。

FileExactlyOnce の多重度型である (../language-spec.md の多重度型の節)。closeConsuming メソッドで、read_at / write_at は借用である。閉じ忘れは記述子の漏れなので消費義務を課す。

将来枠

  • ファイルロック (flock / byte-range lock) — 複数プロセスから同一 DB ファイルへ安全に書くには必要になる
  • ストリーム読み (行単位イテレーション等)

12. パス解決

  • 起点は プロセスの cwdfs.read "foo.hika" は cwd 配下の foo.hika を見る
  • 絶対パスはそのまま使う
  • ファイル import (import m := "x.hika"、caller file 相対、language-spec.md §13.4) とは規則が異なるstd:fs の各関数はデータファイルの取り回し (cwd 起点)、ファイル import はソース解決 (caller 相対) という用途の違いに対応した使い分け
  • 渡したパスは正規化しない。 . / .. を含んでいてもその綴りのままホストへ渡し、パス区切りの解釈もホストに委ねる。まとめてから渡したいなら path.mdnormalize を通す

12.1 シンボリックリンク

リンクを辿るかどうかは渡したパス自身と、ツリーの中で見つけたもので分ける

  • 渡したパス自身は辿る。 呼び手がその綴りを名指しているので、リンクであることは呼び手が承知している。read / write / stat / exists / readdir / walk / open はいずれもリンク先を見る (stat("link-to-dir")is_dirtrue、壊れたリンクの existsfalse)
  • ツリーの中で見つけたものは辿らない。 readdiris_dir (§6)・walk の再帰 (§6.1)・remove_all の再帰 (§7.1) は、リンクをリンクとして扱う。辿ると輪で止まらなくなり・同じファイルを 2 度数え・ツリーの外へ出るremove_all なら消す対象の外まで消える)
  • 壊れたリンクはツリーを歩く妨げにならない。 リンク先が無いことは、そのリンクを含むツリーが読めないことではない。walk は壊れたリンクを結果に載せずに歩き続ける

この分け方は「呼び手が名指したものには従い、こちらが見つけたものは疑う」という 1 つの規律である。リンクそのものを作る口・読む口 (symlink / readlink) と、リンクを辿らずにメタ情報を読む口 (lstat 相当) は持たない。リンクかどうかを知る必要があるときは、readdiris_dirstatis_dir が食い違うことで間接的に分かる。

13. embed バイナリでの挙動

hikari build (build.md) が生成する embed バイナリ内でも std:fs の各関数は ホスト FS のみ を見る。embed FS (バイナリに同梱されたソースツリー) はファイル import 専用で、std:fs からはアクセスできない。std:fs モジュール自体 (ホストコード) は embed バイナリにも常に含まれ、std:fsimport は常に解決可能 (language-spec.md §13.4)。

14. エラーモデル

失敗時の err は回復可能エラーオブジェクト {kind, message} (language-spec.md §16)。std:fs が返す正規 kind (閉じた一覧。全 15 関数で共通):

kind 意味
not_found パス (またはその親) が存在しない
permission_denied 権限不足でアクセスできない
io_error その他の I/O 失敗

新しい kind は設けず、上記 3 種で全関数を表す。種別が紛れやすい代表的な対応:

  • remove非空ディレクトリio_error (再帰削除は §7.1 remove_all)
  • remove_all不在のパスkind ではなく Ok(()) で表す (§7.1remove とは違う)
  • mkdtempprefix にパス区切りio_error (§5.1)
  • readdirpathディレクトリでないio_error
  • renameファイルシステムを跨ぐ移動 (EXDEV) → io_error (自動コピーはしない)
  • mkdir経路上に同名の非ディレクトリio_error
  • exists の不在は kind ではなく Ok(false) で表す (§9)

messagefs.mkdir: ... のように関数名を含む人間可読文字列。分岐は match res { Ok(v) => … Err(e) => match e.kind {...} } で行い、message の文字列マッチに依存しないこと。

Future / scheduler との関係は prelude.md §9.4 と同じ。root リテラル本体の評価が完了した時点で未解決の fs Future は language-spec.md §13.3 のとおり放棄される。別フローで進み続ける I/O は無い — fs はホストのスレッドへ仕事を渡さないので、放棄した時点で止めるべき走行中の仕事は残らない。