From bd46d888cf3e6674af831eda244bb417c367904a Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 00:15:03 +0900 Subject: [PATCH 01/25] =?UTF-8?q?=E3=82=B9=E3=83=9E=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=83=9D=E3=82=A4=E3=83=B3=E3=82=BF=EF=BC=88std::mem::smart=20?= =?UTF-8?q?=E2=80=94=20UniquePtr/SharedPtr=EF=BC=89=E3=82=92=E3=82=BB?= =?UTF-8?q?=E3=83=AB=E3=83=95=E3=83=9B=E3=82=B9=E3=83=88=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=83=96=E3=83=A9=E3=83=AA=E3=81=A8=E3=81=97=E3=81=A6=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=80=81v0.17.2=E3=82=BB=E3=83=AB=E3=83=95?= =?UTF-8?q?=E3=83=9B=E3=82=B9=E3=83=86=E3=82=A3=E3=83=B3=E3=82=B0stdlib?= =?UTF-8?q?=E6=95=B4=E5=82=99=E8=A8=88=E7=94=BB=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAII(スコープ末尾デストラクタ)でヒープ解放を自動化するUniquePtr(単独所有・move専用、get/set/raw/release/reset/is_null)とSharedPtr(参照カウント共有・明示clone、use_count)をコンパイラ変更なしのCm実装で追加した。 確保はstd::mem::alloc/dealloc経由でカスタムアロケータ差し替えが効く。対応はnative系(jit/native/wasm/uefi/baremetal)で、JS/TSはポインタ禁止(GC)・SVは対象外。 所有規律(移動はmove・共有はclone・所有型の返却はreturn move。暗黙コピーは二重解放)はプロトタイプ実測に基づき設計文書とチュートリアルja/enに明記した。 integrationテスト4件(unique基本・move移動/factory/関数渡し・shared clone/参照カウント/可視性・構造体ペイロード)をtests/common/stdlib/smartへ追加し、jit/native/wasmで検証した(platform: !js|sv)。 セルフホスティングに向けた標準ライブラリの棚卸しとv0.17.2計画(Arenaアロケータ・文字分類・数値parse集約・HashSet/TreeSet・Vector強化・fs行読み・Stopwatch・WeakPtr/Atomic版、および言語側課題としてdtor持ちstruct暗黙コピー警告とreturn move検出)をdocs/design/v0.17.2/selfhosting-stdlib.mdへまとめた。 スマートポインタの設計文書は実装完了としてdocs/archive/v0.17.1/へ収載した。新しい構文・キーワードは追加していないためVSCode拡張の変更はない。 --- docs/archive/v0.17.1/smart-pointers.md | 54 +++++++ docs/design/index.md | 7 + docs/design/v0.17.2/selfhosting-stdlib.md | 72 +++++++++ docs/releases/v0.17.1.md | 21 +++ docs/tutorials/en/stdlib/mem.md | 47 ++++++ docs/tutorials/ja/stdlib/mem.md | 47 ++++++ libs/std/mem/smart.cm | 153 ++++++++++++++++++ tests/common/stdlib/smart/move_transfer.cm | 39 +++++ .../common/stdlib/smart/move_transfer.expect | 4 + tests/common/stdlib/smart/shared.cm | 32 ++++ tests/common/stdlib/smart/shared.expect | 7 + tests/common/stdlib/smart/struct_payload.cm | 36 +++++ .../common/stdlib/smart/struct_payload.expect | 3 + tests/common/stdlib/smart/unique.cm | 34 ++++ tests/common/stdlib/smart/unique.expect | 6 + 15 files changed, 562 insertions(+) create mode 100644 docs/archive/v0.17.1/smart-pointers.md create mode 100644 docs/design/v0.17.2/selfhosting-stdlib.md create mode 100644 libs/std/mem/smart.cm create mode 100644 tests/common/stdlib/smart/move_transfer.cm create mode 100644 tests/common/stdlib/smart/move_transfer.expect create mode 100644 tests/common/stdlib/smart/shared.cm create mode 100644 tests/common/stdlib/smart/shared.expect create mode 100644 tests/common/stdlib/smart/struct_payload.cm create mode 100644 tests/common/stdlib/smart/struct_payload.expect create mode 100644 tests/common/stdlib/smart/unique.cm create mode 100644 tests/common/stdlib/smart/unique.expect diff --git a/docs/archive/v0.17.1/smart-pointers.md b/docs/archive/v0.17.1/smart-pointers.md new file mode 100644 index 00000000..d66fc5c2 --- /dev/null +++ b/docs/archive/v0.17.1/smart-pointers.md @@ -0,0 +1,54 @@ +# スマートポインタ(std::mem::smart — UniquePtr / SharedPtr) + +## 目的 + +ヒープ確保した値の解放をRAIIで自動化するスマートポインタを、コンパイラ変更なしのセルフホストライブラリ(Cm実装)として提供する。 +将来のセルフホスティング(AST等の所有権管理)の基盤となる。 + +## 実現可能性の検証結果(プロトタイプ実測) + +- ジェネリックstruct+`impl`のコンストラクタ/デストラクタ・`__sizeof__(T)`・`std::mem::alloc/dealloc`・スコープ末尾dtorはすべて動作する(`Vector`が同型の先行実装)。 +- `move` は元のdtorを無効化して所有権を移す(1回だけ解放される)。関数への `move` 渡し・`return move local;` も正しく動作する。 +- **暗黙コピー(`b = a;`)は浅いコピーで両方のdtorが走る**(二重解放)。コピーコンストラクタのフックは存在しない。 +- **`return local;` はローカルのdtorが先に走りdanglingになる**。所有型を返す関数は必ず `return move local;` と書く必要がある。 +- 単項 `*`/`->` のオーバーロードは不可のため、アクセスは `get()/set()/raw()` メソッドで提供する。 +- JS/TSターゲットはポインタ操作禁止(GCのため不要)、SVは対象外。native系(jit/native/wasm/uefi/baremetal)限定モジュールとする。 + +## API設計 + +```cm +import std::mem::smart::*; + +// UniquePtr: 単独所有。共有不可・移動のみ +UniquePtr u(42); // 値をヒープへ移して所有 +int v = u.get(); // 読み出し(コピー) +u.set(43); // 書き換え +int* p = u.raw(); // 借用ビュー(所有権は移動しない) +UniquePtr u2 = move u; // 所有権の移動(uは無効化) +int* released = u2.release(); // 所有権を放棄(解放は呼び出し側の責任) +u2.reset(); // 即時解放してnull化 + +// SharedPtr: 参照カウントによる共有。共有は明示clone(Rustと同じ流儀) +SharedPtr a(42); +SharedPtr b = a.clone(); // rc=2 +int n = a.use_count(); +// 最後の所有者のdtorでペイロードと制御カウントを解放 +``` + +## 設計判断 + +- **共有は明示 `clone()`**: 暗黙コピーはコピーフックが無く参照カウントを増やせないため、`=` での共有は二重解放になる。Rustの `Arc::clone` と同じ明示規律とし、チュートリアル・docコメントで「代入は `move` 必須・共有は `clone()` 必須」を明記する。 +- **制御カウントは独立ヒープセル(int*)**: ペイロードと別確保のシンプルな構成(weak対応時に制御ブロック構造体へ拡張する)。 +- **確保は `std::mem::alloc/dealloc` 経由**: `set_allocator_fns` によるカスタムアロケータ差し替えを透過させる(Vectorと同方針)。 +- **デストラクタ内は解放処理をインライン**: dtorからのメソッド呼び出しへの依存を避ける。 +- 宣言だけの `UniquePtr u;` はフィールドゼロ初期化(v0.17.0確定)によりnull状態で、dtorは何もしない。 + +## 既知の制約(言語側の将来課題) + +- dtorを持つstructの暗黙コピー(`b = a;`)はコンパイラが検出しない(v0.17.2で警告/エラー化を検討 — selfhosting-stdlib設計参照)。 +- `return move` を忘れた所有型の返却はdanglingになる(同上の検出候補)。 + +## テスト計画 + +- `tests/common/stdlib/smart/`(`//! platform: !js|sv`): unique基本(get/set/raw/スコープ解放)、move移動・関数渡し・factory返却、release/reset、shared clone/use_count/段階的解放、構造体ペイロード。 +- 解放順・回数はdtor内printlnの出力順で検証する。 diff --git a/docs/design/index.md b/docs/design/index.md index 66008b34..3ed43308 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -18,11 +18,18 @@ Cm言語コンパイラの設計文書の一覧。実装が完了した設計文 | [v1.0.0ロードマップ](roadmap_v1.0.0.html) | v1.0.0までのマイルストーン別計画 | | [v0.16.0 SVバックエンド拡充(アーカイブ)](../archive/v0.16.0/roadmap.html) | v0.16.0開発時のロードマップ(Verilogギャップ分析・tcl/cst統合検討) | +## v0.17.2 設計 + +| ドキュメント | 内容 | +|---|---| +| [セルフホスティング向け標準ライブラリ整備計画](v0.17.2/selfhosting-stdlib.html) | コンパイラ実装に必要なstdの棚卸しとArena・文字分類・parse・Set等の実装計画 | + ## v0.17.1 設計(実装済み・アーカイブ済み) | ドキュメント | 内容 | |---|---| | [ネスト型宣言](../archive/v0.17.1/nested-type-declarations.html) | struct/enum本体内のstruct/enum宣言と `Outer::Inner` / `Outer::Inner::MEM` 修飾アクセスチェーンの設計(実装完了) | +| [スマートポインタ](../archive/v0.17.1/smart-pointers.html) | UniquePtr/SharedPtr(std::mem::smart)のRAII設計・move/clone所有規律・実測に基づく制約(実装完了) | ## v0.17.0 設計(全件完遂・アーカイブ済み) diff --git a/docs/design/v0.17.2/selfhosting-stdlib.md b/docs/design/v0.17.2/selfhosting-stdlib.md new file mode 100644 index 00000000..e31fbd72 --- /dev/null +++ b/docs/design/v0.17.2/selfhosting-stdlib.md @@ -0,0 +1,72 @@ +# v0.17.2 セルフホスティング向け標準ライブラリ整備計画 + +## 目的 + +Cmコンパイラ自身をCmで記述する(セルフホスティング)ための土台として、コンパイラ実装に必要な標準ライブラリを棚卸しし、v0.17.2で整備する範囲を定める。 +コンパイラの各フェーズ(字句解析→構文解析→AST→型検査→コード生成)が要求するデータ構造・ユーティリティを基準に不足を洗い出す。 + +## 現状の棚卸し(v0.17.1時点) + +| 分類 | 提供済み | 備考 | +|---|---|---| +| コレクション | `Vector`・`HashMap`・`TreeMap`・`Queue` | Set系・Stack/Dequeなし。Vector.sortはバブル・プリミティブ限定 | +| 文字列 | `StringBuilder`・`split/lines`・`from_bytes`・組み込みメソッド(startsWith/endsWith/includes/indexOf/substring/replace/trim/repeat/toLowerCase/toUpperCase/charAt/len/byte_at/codepoint_at) | 文字単位の分類(is_digit等)なし | +| 数値変換 | `parse_int`(`std::io`内・`Option`返し) | 配置が不自然(io内)。parse_float/基数指定なし | +| メモリ | `alloc/dealloc/size_of`・`Allocator`インターフェース・グローバルアロケータ差し替え・`UniquePtr/SharedPtr`(v0.17.1) | Arena/Poolなし・WeakPtrなし | +| イテレータ | `std::iter`(traits/adapters/range/array) | | +| OS連携 | `fs`・`path`・`env`・`process`・`bytes`・args | 行単位の逐次読みなし(全読みのみ) | +| その他 | `json`・`core`(min/max/clamp/abs/panic)・`core::time`(now_ms/sleep)・native系(http/net/sync/gpu/math) | | +| エラー処理 | 組み込み`Result/Option`・`?`演算子・panic | 十分 | + +## ギャップ分析(コンパイラのフェーズ別) + +- **字句解析**: 文字分類(is_digit/is_alpha等)が無く、比較演算の手書きが必要になる。数値リテラル解析にparse_int/parse_floatの基数対応が要る。 +- **構文解析・AST**: ノードの所有管理はUniquePtr/SharedPtrで可能になったが、コンパイラ用途では**Arenaアロケータ**(フェーズ一括解放・個別free不要)が定石で、move規律の負担も減る。 +- **型検査**: 集合(訪問済み・シンボル集合)にSetが無くHashMapで代用が必要。スコープスタックはVectorで代用可能(push/pop既存)。 +- **コード生成**: StringBuilder・fsは既存で足りる。 +- **横断**: ソートの計算量(バブル)と比較関数対応、計測用Stopwatch。 + +## v0.17.2 実装提案(優先度順) + +### P0: セルフホスティングの直接前提 + +1. **`std::mem::arena` — Arenaアロケータ** + - `Arena`(可変長チャンクの単方向リスト): `alloc(size)`・`reset()`・dtorで一括解放。`Allocator`インターフェース実装で`Vector`等にも注入可能。 + - AST構築の定石(個別freeなし・フェーズ終了で一括破棄)。UniquePtr/SharedPtrのmove規律が不要になる領域を広げる。 +2. **`std::strings::chars` — 文字分類・変換** + - `is_digit/is_alpha/is_alnum/is_space/is_upper/is_lower/is_hex_digit(char)`・`to_upper/to_lower(char)`・`digit_value(char, int base) -> Option`。 + - ASCII範囲を対象(識別子・リテラルはASCIIで十分。非ASCIIはcodepoint_atで別途扱う)。 +3. **`std::strings::parse` — 数値解析の集約** + - `parse_int(string) -> Option` を`std::io`から移設(io側は再エクスポートで互換維持)。 + - `parse_int_radix(string, int base)`・`parse_long`・`parse_float` を追加(リテラル解析の基盤)。 +4. **`std::collections::HashSet / TreeSet`** + - 既存HashMap/TreeMapのキーのみ薄ラッパー(insert/contains/remove/len/clear)。 + +### P1: 品質・利便性 + +5. **`Vector`の強化**: `sortBy(比較ラムダ)`対応と挿入ソート化(小規模前提の改善)・`last()/top()`(スタック用途の明確化)・`reserve(n)`。 +6. **`std::fs`の行読み**: `read_lines(path) -> Vector`(全読み+split流用)と、大ファイル向け`FileReader`(チャンク読み+行イテレータ)。 +7. **`std::core::time::Stopwatch`**: `start()/elapsed_ms()`(now_msの薄ラッパー。コンパイラのフェーズ計測用)。 + +### P2: スマートポインタ第2弾(言語ガードと連動) + +8. **`WeakPtr`**: 制御ブロックをstrong/weak二重カウント構造体へ拡張(循環参照対策)。 +9. **Atomic参照カウント版SharedPtr**: `std::sync`(native)のAtomicで`AtomicSharedPtr`。単一スレッド版と別型として提供。 + +## 言語側の前提課題(stdlib外・別設計で扱う) + +- **dtorを持つstructの暗黙コピーを警告/エラー化**: `b = a;` の浅いコピーで両dtorが走る二重解放をコンパイラが検出する(UniquePtr/SharedPtrのmove/clone規律の強制)。 +- **所有型の`return local;`検出**: dtor持ちローカルの非move返却(dangling)を警告する。 +- 上記2つはスマートポインタ設計(archive/v0.17.1/smart-pointers.md)の既知制約であり、v0.17.2の言語側設計として別文書化する。 + +## テスト計画 + +- 各モジュールに `tests/common/stdlib/` 配下のintegrationテストを追加(native系。ポインタを使うもの=arena/smartは `//! platform: !js|sv`、文字分類・parse・Set等の純粋ロジックは全バックエンド)。 +- Arenaは確保・reset・一括解放・Allocator注入(Vector連携)を検証。 +- parse系は境界(空文字・符号・オーバーフロー・基数16)をResult/Optionで検証。 + +## 非対象(明示) + +- 正規表現・Unicode正規化: コンパイラ実装に不要。 +- `std::fmt`: 文字列補間で充足。 +- 診断・ソース位置管理: コンパイラ本体のドメインでありstdへは置かない。 diff --git a/docs/releases/v0.17.1.md b/docs/releases/v0.17.1.md index 63d6412d..ed8c0838 100644 --- a/docs/releases/v0.17.1.md +++ b/docs/releases/v0.17.1.md @@ -95,6 +95,27 @@ struct Outer { - 末尾`;`省略構文(`struct X {}`)の直後に来る通常宣言(`P make()` 等)と区別するため、宣言子は「識別子の直後が `;` か `,`」の場合のみ受理する。 - 制限: 宣言子への初期化子は未対応(別文で書く)・ジェネリック型宣言への宣言子不可・enum本体内では宣言子不可。 +### スマートポインタ(std::mem::smart — UniquePtr / SharedPtr) + +ヒープ確保した値の解放を自動化する手段がなく、`alloc/dealloc` の手動対応が必要だった。 +コンパイラ変更なしのセルフホストライブラリ(Cm実装)として、RAIIベースのスマートポインタを追加した。 + +```cm +import std::mem::smart::*; + +UniquePtr u(42); // 単独所有。スコープを抜けると自動解放 +UniquePtr v = move u; // 所有権の移動は必ずmove + +SharedPtr a(100); +SharedPtr b = a.clone(); // 共有は必ずclone()(参照カウント+1) +println("rc={a.use_count()}"); // 2 +``` + +- API: `get/set/raw/is_null`、UniquePtrは `release/reset`、SharedPtrは `clone/use_count`。 +- 確保は `std::mem::alloc/dealloc` 経由でカスタムアロケータ差し替えが効く。 +- 対応はnative系(jit/native/wasm/uefi/baremetal)。JS/TSはポインタ禁止(GCのため不要)、SVは対象外。 +- 所有規律: 暗黙コピー(`b = a;`)は両dtorが走り二重解放になるため、移動は `move`・共有は `clone()`・所有型の返却は `return move` を必須とする(コンパイラによる強制はv0.17.2の言語側課題として設計済み)。 + ## 🔧 改善 ### VSCode拡張のハイライトをC++/Rust標準スコープへ統一 diff --git a/docs/tutorials/en/stdlib/mem.md b/docs/tutorials/en/stdlib/mem.md index 5793c79f..5284d35e 100644 --- a/docs/tutorials/en/stdlib/mem.md +++ b/docs/tutorials/en/stdlib/mem.md @@ -113,6 +113,53 @@ The registered functions must have the signatures `void*(long)` / `void(void*)` --- +## Smart Pointers (std::mem::smart, v0.17.1) + +Automate freeing heap-allocated values with RAII (destructor at scope exit). + +```cm +import std::mem::smart::*; + +int main() { + // UniquePtr: sole ownership; freed automatically at scope exit + UniquePtr u(42); + println("{u.get()}"); // 42 + u.set(43); + int* p = u.raw(); // borrowed view (ownership is not transferred) + + // Always transfer ownership with move + UniquePtr v = move u; + + // SharedPtr: reference-counted sharing; always share via clone() + SharedPtr a(100); + SharedPtr b = a.clone(); // rc=2 + println("rc={a.use_count()}"); // 2 + b.set(200); // visible to all sharers + return 0; +} + +// Functions returning an owning type must use return move +UniquePtr make(int v) { + UniquePtr b(v); + return move b; +} +``` + +**Ownership discipline (important):** + +- An implicit copy (`b = a;`) is a shallow copy and both destructors run, causing a **double free**. Always use `move` for transfers and `clone()` for SharedPtr sharing. +- Functions returning an owning type must write `return move local;` (`return local;` runs the local's destructor first and returns a dangling pointer). +- Never let the raw pointer from `raw()` outlive the smart pointer. + +| API | UniquePtr | SharedPtr | +|---|---|---| +| Construct | `UniquePtr u(value);` | `SharedPtr a(value);` | +| Read/Write | `get()` / `set(v)` / `raw()` | `get()` / `set(v)` / `raw()` | +| Share | Not allowed (move only) | `clone()` (refcount +1) | +| Others | `release()` / `reset()` / `is_null()` | `use_count()` / `is_null()` | + +--- + ## libc FFI libc functions available internally: diff --git a/docs/tutorials/ja/stdlib/mem.md b/docs/tutorials/ja/stdlib/mem.md index af9993e0..9427d934 100644 --- a/docs/tutorials/ja/stdlib/mem.md +++ b/docs/tutorials/ja/stdlib/mem.md @@ -115,6 +115,53 @@ int main() { --- +## スマートポインタ(std::mem::smart・v0.17.1) + +ヒープ確保した値の解放をRAII(スコープ末尾のデストラクタ)で自動化します。 + +```cm +import std::mem::smart::*; + +int main() { + // UniquePtr: 単独所有。スコープを抜けると自動解放 + UniquePtr u(42); + println("{u.get()}"); // 42 + u.set(43); + int* p = u.raw(); // 借用ビュー(所有権は移動しない) + + // 所有権の移動は必ず move で行う + UniquePtr v = move u; + + // SharedPtr: 参照カウント共有。共有は必ず clone() で行う + SharedPtr a(100); + SharedPtr b = a.clone(); // rc=2 + println("rc={a.use_count()}"); // 2 + b.set(200); // 全共有者から見える + return 0; +} + +// 所有型を返す関数は必ず return move で書く +UniquePtr make(int v) { + UniquePtr b(v); + return move b; +} +``` + +**所有規律(重要):** + +- 暗黙コピー(`b = a;`)は浅いコピーで両方のデストラクタが走り**二重解放**になります。移動は `move`、SharedPtrの共有は `clone()` を必ず使ってください。 +- 所有型を関数から返すときは `return move local;` と書きます(`return local;` はローカルのデストラクタが先に走りdanglingになります)。 +- `raw()` の生ポインタはスマートポインタより長生きさせないでください。 + +| API | UniquePtr | SharedPtr | +|---|---|---| +| 構築 | `UniquePtr u(value);` | `SharedPtr a(value);` | +| 読み書き | `get()` / `set(v)` / `raw()` | `get()` / `set(v)` / `raw()` | +| 共有 | 不可(moveのみ) | `clone()`(参照カウント+1) | +| その他 | `release()` / `reset()` / `is_null()` | `use_count()` / `is_null()` | + +--- + ## libc FFI 内部的に使用可能なlibc関数: diff --git a/libs/std/mem/smart.cm b/libs/std/mem/smart.cm new file mode 100644 index 00000000..287adac8 --- /dev/null +++ b/libs/std/mem/smart.cm @@ -0,0 +1,153 @@ +// std/mem/smart.cm - スマートポインタ(UniquePtr / SharedPtr) +// ヒープ確保した値の解放をRAII(スコープ末尾のデストラクタ)で自動化する。 +// +// 所有規律(重要): +// - 所有権の移動は必ず move で行う(UniquePtr b = move a;)。 +// 暗黙コピー(b = a;)は浅いコピーで両方のデストラクタが走り二重解放になる。 +// - SharedPtrの共有は必ず clone() で行う(参照カウントを増やす唯一の手段)。 +// - 所有型を関数から返すときは必ず return move local; と書く。 +// +// 対応ターゲット: native系(jit/native/wasm/uefi/baremetal)。 +// JS/TSはポインタ操作が禁止(GCのため不要)、SVは対象外。 +module std.mem.smart; + +import std::mem::{alloc, dealloc}; + +// ============================================================= +// UniquePtr - 単独所有スマートポインタ(移動のみ・共有不可) +// ============================================================= + +export struct UniquePtr { + T* ptr; +} + +export impl UniquePtr { + // 値をヒープへ移して所有する + self(T value) { + void* raw = alloc(__sizeof__(T) as int); + self.ptr = raw as T*; + *self.ptr = value; + } + + // 中身の読み出し(コピーを返す) + T get() { + return *self.ptr; + } + + // 中身の書き換え + void set(T value) { + *self.ptr = value; + } + + // 生ポインタの借用ビュー(所有権は移動しない。UniquePtrより長生きさせないこと) + T* raw() { + return self.ptr; + } + + // 無効(null)状態か(移動済み・release/reset済み・宣言のみのゼロ初期化状態) + bool is_null() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + return p == null_ptr; + } + + // 所有権を放棄して生ポインタを返す(以後デストラクタは何もしない。解放は呼び出し側の責任) + T* release() { + T* p = self.ptr; + void* null_ptr = 0 as void*; + self.ptr = null_ptr as T*; + return p; + } + + // いま所有している値を即時解放してnull状態にする + void reset() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + dealloc(p); + self.ptr = null_ptr as T*; + } + } + + // デストラクタ: 所有中なら解放する(解放処理はメソッドに依存せずインラインで行う) + ~self() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + dealloc(p); + self.ptr = null_ptr as T*; + } + } +} + +// ============================================================= +// SharedPtr - 参照カウント共有スマートポインタ(共有は明示clone) +// ============================================================= + +export struct SharedPtr { + T* ptr; + int* count; +} + +export impl SharedPtr { + // 値をヒープへ移して所有する(参照カウント=1) + self(T value) { + void* raw = alloc(__sizeof__(T) as int); + self.ptr = raw as T*; + *self.ptr = value; + void* craw = alloc(4); + self.count = craw as int*; + *self.count = 1; + } + + // 共有: 参照カウントを増やした複製を返す(共有はこのメソッドのみで行うこと) + SharedPtr clone() { + *self.count = *self.count + 1; + SharedPtr copy; + copy.ptr = self.ptr; + copy.count = self.count; + return move copy; + } + + // 中身の読み出し(コピーを返す) + T get() { + return *self.ptr; + } + + // 中身の書き換え(全共有者から見える) + void set(T value) { + *self.ptr = value; + } + + // 生ポインタの借用ビュー(所有権は移動しない) + T* raw() { + return self.ptr; + } + + // 現在の参照カウント + int use_count() { + return *self.count; + } + + // 無効(null)状態か(移動済み・宣言のみのゼロ初期化状態) + bool is_null() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + return p == null_ptr; + } + + // デストラクタ: 参照カウントを減らし、最後の所有者ならペイロードとカウントを解放する + ~self() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + *self.count = *self.count - 1; + if (*self.count == 0) { + dealloc(p); + dealloc(self.count as void*); + } + self.ptr = null_ptr as T*; + self.count = null_ptr as int*; + } + } +} diff --git a/tests/common/stdlib/smart/move_transfer.cm b/tests/common/stdlib/smart/move_transfer.cm new file mode 100644 index 00000000..d3009e77 --- /dev/null +++ b/tests/common/stdlib/smart/move_transfer.cm @@ -0,0 +1,39 @@ +//! platform: !js|sv +// UniquePtrの所有権移動: move代入・move関数渡し・return moveによるfactory +import std::io::println; +import std::mem::smart::*; + +int consume(UniquePtr p) { + return p.get() * 2; +} + +UniquePtr make(int v) { + UniquePtr b(v); + return move b; +} + +UniquePtr pass_through(UniquePtr p) { + p.set(p.get() + 1); + return move p; +} + +int main() { + // move代入 + UniquePtr a(1); + UniquePtr b = move a; + println("{b.get()}"); + + // moveで関数へ所有権を渡す(関数側のdtorが解放する) + UniquePtr c(21); + println("{consume(move c)}"); + + // factory(return move) + UniquePtr u = make(7); + println("{u.get()}"); + + // 関数を経由した往復 + UniquePtr d(10); + UniquePtr e = pass_through(move d); + println("{e.get()}"); + return 0; +} diff --git a/tests/common/stdlib/smart/move_transfer.expect b/tests/common/stdlib/smart/move_transfer.expect new file mode 100644 index 00000000..38b4caf7 --- /dev/null +++ b/tests/common/stdlib/smart/move_transfer.expect @@ -0,0 +1,4 @@ +1 +42 +7 +11 diff --git a/tests/common/stdlib/smart/shared.cm b/tests/common/stdlib/smart/shared.cm new file mode 100644 index 00000000..57c9d9bb --- /dev/null +++ b/tests/common/stdlib/smart/shared.cm @@ -0,0 +1,32 @@ +//! platform: !js|sv +// SharedPtr: 明示cloneによる共有・参照カウント・共有者間の可視性・段階的解放 +import std::io::println; +import std::mem::smart::*; + +int main() { + SharedPtr a(42); + println("rc={a.use_count()}"); + + { + SharedPtr b = a.clone(); + println("b={b.get()} rc={b.use_count()}"); + + // 書き換えは全共有者から見える + b.set(100); + println("a={a.get()}"); + + { + SharedPtr c = b.clone(); + println("rc={c.use_count()}"); + } + // cのdtorでrcが1減る + println("rc={a.use_count()}"); + } + // bのdtorでrcが1減る + println("rc={a.use_count()}"); + + // moveは参照カウントを変えない(所有スロットの移動のみ) + SharedPtr d = move a; + println("d={d.get()} rc={d.use_count()}"); + return 0; +} diff --git a/tests/common/stdlib/smart/shared.expect b/tests/common/stdlib/smart/shared.expect new file mode 100644 index 00000000..42ae9a7b --- /dev/null +++ b/tests/common/stdlib/smart/shared.expect @@ -0,0 +1,7 @@ +rc=1 +b=42 rc=2 +a=100 +rc=3 +rc=2 +rc=1 +d=100 rc=1 diff --git a/tests/common/stdlib/smart/struct_payload.cm b/tests/common/stdlib/smart/struct_payload.cm new file mode 100644 index 00000000..3587e03b --- /dev/null +++ b/tests/common/stdlib/smart/struct_payload.cm @@ -0,0 +1,36 @@ +//! platform: !js|sv +// 構造体ペイロード: 構造体値の所有・フィールド更新・SharedPtr共有 +import std::io::println; +import std::mem::smart::*; + +struct Point { + int x; + int y; +} + +int main() { + Point p; + p.x = 3; + p.y = 4; + + UniquePtr u(p); + Point got = u.get(); + println("{got.x},{got.y}"); + + // 生ポインタ経由でフィールドを直接更新 + Point* raw = u.raw(); + raw->x = 30; + println("{u.get().x}"); + + Point q; + q.x = 1; + q.y = 2; + SharedPtr s(q); + SharedPtr t = s.clone(); + Point r; + r.x = 9; + r.y = 9; + t.set(r); + println("{s.get().x},{s.get().y} rc={s.use_count()}"); + return 0; +} diff --git a/tests/common/stdlib/smart/struct_payload.expect b/tests/common/stdlib/smart/struct_payload.expect new file mode 100644 index 00000000..4ecff1c4 --- /dev/null +++ b/tests/common/stdlib/smart/struct_payload.expect @@ -0,0 +1,3 @@ +3,4 +30 +9,9 rc=2 diff --git a/tests/common/stdlib/smart/unique.cm b/tests/common/stdlib/smart/unique.cm new file mode 100644 index 00000000..68a75e34 --- /dev/null +++ b/tests/common/stdlib/smart/unique.cm @@ -0,0 +1,34 @@ +//! platform: !js|sv +// UniquePtr基本: 確保・読み書き・生ポインタ借用・release/reset・null状態 +import std::io::println; +import std::mem::smart::*; +import std::mem::{dealloc}; + +int main() { + UniquePtr u(42); + println("{u.get()}"); + + u.set(43); + println("{u.get()}"); + + // 借用ビュー経由の書き換えは所有者から見える + int* p = u.raw(); + *p = 44; + println("{u.get()}"); + + // release: 所有権を放棄して手動解放する + UniquePtr r(7); + int* raw = r.release(); + println("released={*raw} null={r.is_null()}"); + dealloc(raw as void*); + + // reset: 即時解放してnull化 + UniquePtr s(8); + s.reset(); + println("after reset null={s.is_null()}"); + + // 宣言のみはゼロ初期化でnull状態(dtorは何もしない) + UniquePtr empty; + println("empty null={empty.is_null()}"); + return 0; +} diff --git a/tests/common/stdlib/smart/unique.expect b/tests/common/stdlib/smart/unique.expect new file mode 100644 index 00000000..7a4779de --- /dev/null +++ b/tests/common/stdlib/smart/unique.expect @@ -0,0 +1,6 @@ +42 +43 +44 +released=7 null=true +after reset null=true +empty null=true From 41f06a7a78df53fc79590e223d57319c4c92221a Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 00:22:44 +0900 Subject: [PATCH 02/25] =?UTF-8?q?v0.17.2=E3=81=AE=E9=96=8B=E7=99=BA?= =?UTF-8?q?=E3=82=92=E9=96=8B=E5=A7=8B:=20VERSION/=E3=83=89=E3=82=AD?= =?UTF-8?q?=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=83=90=E3=83=83=E3=82=B8?= =?UTF-8?q?/VSCode=E6=8B=A1=E5=BC=B5=E3=81=AE=E3=83=90=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E6=95=B4=E5=90=88=E3=81=A8=E3=83=AA=E3=83=AA?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=83=8E=E3=83=BC=E3=83=88=E9=AA=A8=E5=AD=90?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit スマートポインタはv0.17.1タグ後の実装のためv0.17.2の収載へ変更し、設計文書のarchive位置(archive/v0.17.2)と各所のバージョン表記を追従した。 リリース一覧にv0.17.2(開発中)を追加し、v0.17.1をリリース済み(2026-08-14)へ更新した。 --- VERSION | 2 +- .../{v0.17.1 => v0.17.2}/smart-pointers.md | 0 docs/design/index.md | 2 +- docs/design/v0.17.2/selfhosting-stdlib.md | 4 +- docs/en/index.md | 2 +- docs/index.md | 2 +- docs/releases/README.md | 3 +- docs/releases/index.md | 3 +- docs/releases/v0.17.1.md | 21 ---------- docs/releases/v0.17.2.md | 41 +++++++++++++++++++ docs/tutorials/en/basics/index.md | 2 +- docs/tutorials/en/compiler/js/index.md | 2 +- docs/tutorials/en/compiler/native/uefi.md | 2 +- docs/tutorials/en/index.md | 10 ++--- docs/tutorials/en/stdlib/mem.md | 2 +- docs/tutorials/ja/basics/index.md | 2 +- docs/tutorials/ja/compiler/js/index.md | 2 +- docs/tutorials/ja/compiler/native/index.md | 2 +- docs/tutorials/ja/compiler/native/uefi.md | 2 +- docs/tutorials/ja/compiler/wasm/index.md | 2 +- docs/tutorials/ja/index.md | 12 +++--- docs/tutorials/ja/stdlib/mem.md | 2 +- vscode-extension/package.json | 2 +- 23 files changed, 73 insertions(+), 51 deletions(-) rename docs/archive/{v0.17.1 => v0.17.2}/smart-pointers.md (100%) create mode 100644 docs/releases/v0.17.2.md diff --git a/VERSION b/VERSION index 7cca7711..c3d16c16 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.17.1 +0.17.2 diff --git a/docs/archive/v0.17.1/smart-pointers.md b/docs/archive/v0.17.2/smart-pointers.md similarity index 100% rename from docs/archive/v0.17.1/smart-pointers.md rename to docs/archive/v0.17.2/smart-pointers.md diff --git a/docs/design/index.md b/docs/design/index.md index 3ed43308..0e1b77c1 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -29,7 +29,7 @@ Cm言語コンパイラの設計文書の一覧。実装が完了した設計文 | ドキュメント | 内容 | |---|---| | [ネスト型宣言](../archive/v0.17.1/nested-type-declarations.html) | struct/enum本体内のstruct/enum宣言と `Outer::Inner` / `Outer::Inner::MEM` 修飾アクセスチェーンの設計(実装完了) | -| [スマートポインタ](../archive/v0.17.1/smart-pointers.html) | UniquePtr/SharedPtr(std::mem::smart)のRAII設計・move/clone所有規律・実測に基づく制約(実装完了) | +| [スマートポインタ](../archive/v0.17.2/smart-pointers.html) | UniquePtr/SharedPtr(std::mem::smart)のRAII設計・move/clone所有規律・実測に基づく制約(実装完了・v0.17.2収載) | ## v0.17.0 設計(全件完遂・アーカイブ済み) diff --git a/docs/design/v0.17.2/selfhosting-stdlib.md b/docs/design/v0.17.2/selfhosting-stdlib.md index e31fbd72..0f107c71 100644 --- a/docs/design/v0.17.2/selfhosting-stdlib.md +++ b/docs/design/v0.17.2/selfhosting-stdlib.md @@ -12,7 +12,7 @@ Cmコンパイラ自身をCmで記述する(セルフホスティング)た | コレクション | `Vector`・`HashMap`・`TreeMap`・`Queue` | Set系・Stack/Dequeなし。Vector.sortはバブル・プリミティブ限定 | | 文字列 | `StringBuilder`・`split/lines`・`from_bytes`・組み込みメソッド(startsWith/endsWith/includes/indexOf/substring/replace/trim/repeat/toLowerCase/toUpperCase/charAt/len/byte_at/codepoint_at) | 文字単位の分類(is_digit等)なし | | 数値変換 | `parse_int`(`std::io`内・`Option`返し) | 配置が不自然(io内)。parse_float/基数指定なし | -| メモリ | `alloc/dealloc/size_of`・`Allocator`インターフェース・グローバルアロケータ差し替え・`UniquePtr/SharedPtr`(v0.17.1) | Arena/Poolなし・WeakPtrなし | +| メモリ | `alloc/dealloc/size_of`・`Allocator`インターフェース・グローバルアロケータ差し替え・`UniquePtr/SharedPtr`(v0.17.2先行実装済み) | Arena/Poolなし・WeakPtrなし | | イテレータ | `std::iter`(traits/adapters/range/array) | | | OS連携 | `fs`・`path`・`env`・`process`・`bytes`・args | 行単位の逐次読みなし(全読みのみ) | | その他 | `json`・`core`(min/max/clamp/abs/panic)・`core::time`(now_ms/sleep)・native系(http/net/sync/gpu/math) | | @@ -57,7 +57,7 @@ Cmコンパイラ自身をCmで記述する(セルフホスティング)た - **dtorを持つstructの暗黙コピーを警告/エラー化**: `b = a;` の浅いコピーで両dtorが走る二重解放をコンパイラが検出する(UniquePtr/SharedPtrのmove/clone規律の強制)。 - **所有型の`return local;`検出**: dtor持ちローカルの非move返却(dangling)を警告する。 -- 上記2つはスマートポインタ設計(archive/v0.17.1/smart-pointers.md)の既知制約であり、v0.17.2の言語側設計として別文書化する。 +- 上記2つはスマートポインタ設計(archive/v0.17.2/smart-pointers.md)の既知制約であり、v0.17.2の言語側設計として別文書化する。 ## テスト計画 diff --git a/docs/en/index.md b/docs/en/index.md index 63be8543..611758cb 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -80,6 +80,6 @@ impl Circle for Drawable { --- -**Last Updated:** v0.17.1 (2026-08-14) +**Last Updated:** v0.17.2 (2026-08-15) © 2025-2026 Cm Language Project diff --git a/docs/index.md b/docs/index.md index a198e6a3..d898b311 100644 --- a/docs/index.md +++ b/docs/index.md @@ -157,7 +157,7 @@ ctest --test-dir build --- -**最終更新:** v0.17.1 (2026-08-14) +**最終更新:** v0.17.2 (2026-08-15) © 2025-2026 Cm Language Project diff --git a/docs/releases/README.md b/docs/releases/README.md index 6225547f..212afac5 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -13,7 +13,8 @@ has_children: true | バージョン | リリース日 | 状態 | 主要機能 | |-----------|-----------|------|---------| -| [v0.17.1](v0.17.1.html) | - | 🚧 開発中 | ネスト型宣言(`Outer::Inner`)、C/C++スタイル単一宣言(匿名struct/enum+宣言子)、VSCodeハイライトのC++/Rust標準スコープ化 | +| [v0.17.2](v0.17.2.html) | - | 🚧 開発中 | セルフホスティング向けstdlib整備(スマートポインタ・Arena・文字分類・Set等) | +| [v0.17.1](v0.17.1.html) | 2026-08-14 | ✅ リリース済み | ネスト型宣言(`Outer::Inner`)、C/C++スタイル単一宣言(匿名struct/enum+宣言子)、VSCodeハイライトのC++/Rust標準スコープ化 | | [v0.17.0](v0.17.0.html) | 2026-08-12 | ✅ リリース済み | TypeScript出力とCm完結Web開発、js/tsのBigInt化、標準ライブラリ拡充(TreeMap/JSON/OS連携)、文字列基盤刷新、モジュールシステム構造化 | | [v0.16.2](v0.16.2.html) | 2026-07-19 | ✅ リリース済み | サニタイザ(--sanitize)導入、SVのinterface/impl対応、メッセージi18n集約 | | [v0.16.1](v0.16.1.html) | 2026-07-16 | ✅ リリース済み | implメソッドのデフォルト引数修正、ビルド警告ゼロ化 | diff --git a/docs/releases/index.md b/docs/releases/index.md index 226587cc..483fb965 100644 --- a/docs/releases/index.md +++ b/docs/releases/index.md @@ -14,7 +14,8 @@ has_children: true | バージョン | リリース日 | 状態 | 主要機能 | |-----------|-----------|------|---------| -| [v0.17.1](v0.17.1.html) | - | 🚧 開発中 | ネスト型宣言(`Outer::Inner`)、C/C++スタイル単一宣言(匿名struct/enum+宣言子)、VSCodeハイライトのC++/Rust標準スコープ化 | +| [v0.17.2](v0.17.2.html) | - | 🚧 開発中 | セルフホスティング向けstdlib整備(スマートポインタ・Arena・文字分類・Set等) | +| [v0.17.1](v0.17.1.html) | 2026-08-14 | ✅ リリース済み | ネスト型宣言(`Outer::Inner`)、C/C++スタイル単一宣言(匿名struct/enum+宣言子)、VSCodeハイライトのC++/Rust標準スコープ化 | | [v0.17.0](v0.17.0.html) | 2026-08-12 | ✅ リリース済み | TypeScript出力とCm完結Web開発、js/tsのBigInt化、標準ライブラリ拡充(TreeMap/JSON/OS連携)、文字列基盤刷新、モジュールシステム構造化 | | [v0.16.2](v0.16.2.html) | 2026-07-19 | ✅ リリース済み | サニタイザ(--sanitize)導入、SVのinterface/impl対応、メッセージi18n集約 | | [v0.16.1](v0.16.1.html) | 2026-07-16 | ✅ リリース済み | implメソッドのデフォルト引数修正、ビルド警告ゼロ化 | diff --git a/docs/releases/v0.17.1.md b/docs/releases/v0.17.1.md index ed8c0838..63d6412d 100644 --- a/docs/releases/v0.17.1.md +++ b/docs/releases/v0.17.1.md @@ -95,27 +95,6 @@ struct Outer { - 末尾`;`省略構文(`struct X {}`)の直後に来る通常宣言(`P make()` 等)と区別するため、宣言子は「識別子の直後が `;` か `,`」の場合のみ受理する。 - 制限: 宣言子への初期化子は未対応(別文で書く)・ジェネリック型宣言への宣言子不可・enum本体内では宣言子不可。 -### スマートポインタ(std::mem::smart — UniquePtr / SharedPtr) - -ヒープ確保した値の解放を自動化する手段がなく、`alloc/dealloc` の手動対応が必要だった。 -コンパイラ変更なしのセルフホストライブラリ(Cm実装)として、RAIIベースのスマートポインタを追加した。 - -```cm -import std::mem::smart::*; - -UniquePtr u(42); // 単独所有。スコープを抜けると自動解放 -UniquePtr v = move u; // 所有権の移動は必ずmove - -SharedPtr a(100); -SharedPtr b = a.clone(); // 共有は必ずclone()(参照カウント+1) -println("rc={a.use_count()}"); // 2 -``` - -- API: `get/set/raw/is_null`、UniquePtrは `release/reset`、SharedPtrは `clone/use_count`。 -- 確保は `std::mem::alloc/dealloc` 経由でカスタムアロケータ差し替えが効く。 -- 対応はnative系(jit/native/wasm/uefi/baremetal)。JS/TSはポインタ禁止(GCのため不要)、SVは対象外。 -- 所有規律: 暗黙コピー(`b = a;`)は両dtorが走り二重解放になるため、移動は `move`・共有は `clone()`・所有型の返却は `return move` を必須とする(コンパイラによる強制はv0.17.2の言語側課題として設計済み)。 - ## 🔧 改善 ### VSCode拡張のハイライトをC++/Rust標準スコープへ統一 diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md new file mode 100644 index 00000000..f6788373 --- /dev/null +++ b/docs/releases/v0.17.2.md @@ -0,0 +1,41 @@ +--- +title: v0.17.2 +parent: Release Notes +nav_order: -5 +--- +{% raw %} + +# Cm v0.17.2 リリースノート + +**前バージョン:** v0.17.1 + +各項目は「何が問題だったか→どうなったか」の要約です。 + +## 🎯 テーマ: セルフホスティング向け標準ライブラリ整備 + +Cmコンパイラ自身をCmで記述するための土台として、コンパイラ実装が要求する標準ライブラリを整備する([設計文書](../archive/v0.17.2/selfhosting-stdlib.html))。 + +## ✨ 新機能 + +### スマートポインタ(std::mem::smart — UniquePtr / SharedPtr) + +ヒープ確保した値の解放を自動化する手段がなく、`alloc/dealloc` の手動対応が必要だった。 +コンパイラ変更なしのセルフホストライブラリ(Cm実装)として、RAIIベースのスマートポインタを追加した。 + +```cm +import std::mem::smart::*; + +UniquePtr u(42); // 単独所有。スコープを抜けると自動解放 +UniquePtr v = move u; // 所有権の移動は必ずmove + +SharedPtr a(100); +SharedPtr b = a.clone(); // 共有は必ずclone()(参照カウント+1) +println("rc={a.use_count()}"); // 2 +``` + +- API: `get/set/raw/is_null`、UniquePtrは `release/reset`、SharedPtrは `clone/use_count`。 +- 確保は `std::mem::alloc/dealloc` 経由でカスタムアロケータ差し替えが効く。 +- 対応はnative系(jit/native/wasm/uefi/baremetal)。JS/TSはポインタ禁止(GCのため不要)、SVは対象外。 +- 所有規律: 暗黙コピー(`b = a;`)は両dtorが走り二重解放になるため、移動は `move`・共有は `clone()`・所有型の返却は `return move` を必須とする。 + +{% endraw %} diff --git a/docs/tutorials/en/basics/index.md b/docs/tutorials/en/basics/index.md index c65fb08a..dd88353e 100644 --- a/docs/tutorials/en/basics/index.md +++ b/docs/tutorials/en/basics/index.md @@ -32,4 +32,4 @@ Tutorials covering the fundamentals of the Cm language. Estimated time: 3-4 hour --- -← Prev: [Cm Language Tutorials v0.17.1](../index.html) | [Contents](../index.html) | Next: [Basics - Introduction](introduction.html) → +← Prev: [Cm Language Tutorials v0.17.2](../index.html) | [Contents](../index.html) | Next: [Basics - Introduction](introduction.html) → diff --git a/docs/tutorials/en/compiler/js/index.md b/docs/tutorials/en/compiler/js/index.md index fb4d468e..accda754 100644 --- a/docs/tutorials/en/compiler/js/index.md +++ b/docs/tutorials/en/compiler/js/index.md @@ -191,7 +191,7 @@ make tjp node output.js ``` -### v0.17.1 Test Results +### v0.17.2 Test Results | Item | Value | |------|-------| diff --git a/docs/tutorials/en/compiler/native/uefi.md b/docs/tutorials/en/compiler/native/uefi.md index effed1df..9defa513 100644 --- a/docs/tutorials/en/compiler/native/uefi.md +++ b/docs/tutorials/en/compiler/native/uefi.md @@ -6,7 +6,7 @@ nav_order: 9 # UEFI Bare-Metal Development Tutorial -**Target version:** v0.17.1 **Level:** 🔴 Advanced **Prerequisites:** Inline assembly, pointer operations +**Target version:** v0.17.2 **Level:** 🔴 Advanced **Prerequisites:** Inline assembly, pointer operations --- diff --git a/docs/tutorials/en/index.md b/docs/tutorials/en/index.md index fe40c856..4d5d3134 100644 --- a/docs/tutorials/en/index.md +++ b/docs/tutorials/en/index.md @@ -7,10 +7,10 @@ has_children: true [日本語](../ja/) -# Cm Language Tutorials v0.17.1 +# Cm Language Tutorials v0.17.2 -**Target Version:** v0.17.1 -**Last Updated:** v0.17.1 (2026-08-14) +**Target Version:** v0.17.2 +**Last Updated:** v0.17.2 (2026-08-15) A comprehensive collection of tutorials to learn all features of the Cm language step-by-step. @@ -128,11 +128,11 @@ Estimated Time: 3 hours **Total Tutorials:** 43 files **Estimated Time:** 18-22 hours -**Target Version:** v0.17.1 +**Target Version:** v0.17.2 --- -**Last Updated:** v0.17.1 (2026-08-14) +**Last Updated:** v0.17.2 (2026-08-15) **Author:** Cm Language Development Team diff --git a/docs/tutorials/en/stdlib/mem.md b/docs/tutorials/en/stdlib/mem.md index 5284d35e..aff257c8 100644 --- a/docs/tutorials/en/stdlib/mem.md +++ b/docs/tutorials/en/stdlib/mem.md @@ -113,7 +113,7 @@ The registered functions must have the signatures `void*(long)` / `void(void*)` --- -## Smart Pointers (std::mem::smart, v0.17.1) +## Smart Pointers (std::mem::smart, v0.17.2) Automate freeing heap-allocated values with RAII (destructor at scope exit). diff --git a/docs/tutorials/ja/basics/index.md b/docs/tutorials/ja/basics/index.md index c0576063..4208751d 100644 --- a/docs/tutorials/ja/basics/index.md +++ b/docs/tutorials/ja/basics/index.md @@ -32,4 +32,4 @@ Cm言語の基礎を学ぶチュートリアル集です。推定学習時間: 3 --- -← 前: [Cm言語チュートリアル v0.17.1](../index.html) | [目次](../index.html) | 次: [はじめに - Cm言語とは](introduction.html) → +← 前: [Cm言語チュートリアル v0.17.2](../index.html) | [目次](../index.html) | 次: [はじめに - Cm言語とは](introduction.html) → diff --git a/docs/tutorials/ja/compiler/js/index.md b/docs/tutorials/ja/compiler/js/index.md index c44a39d3..e763b139 100644 --- a/docs/tutorials/ja/compiler/js/index.md +++ b/docs/tutorials/ja/compiler/js/index.md @@ -191,7 +191,7 @@ make tjp node output.js ``` -### v0.17.1 テスト結果 +### v0.17.2 テスト結果 | 項目 | 値 | |------|-----| diff --git a/docs/tutorials/ja/compiler/native/index.md b/docs/tutorials/ja/compiler/native/index.md index 5661ac45..c415cbf0 100644 --- a/docs/tutorials/ja/compiler/native/index.md +++ b/docs/tutorials/ja/compiler/native/index.md @@ -17,7 +17,7 @@ parent: Tutorials - 高度な最適化 - デバッグ情報生成 -## サポート状況(v0.17.1) +## サポート状況(v0.17.2) | 機能 | 状態 | |------|------| diff --git a/docs/tutorials/ja/compiler/native/uefi.md b/docs/tutorials/ja/compiler/native/uefi.md index 8001d41e..cfb18c69 100644 --- a/docs/tutorials/ja/compiler/native/uefi.md +++ b/docs/tutorials/ja/compiler/native/uefi.md @@ -6,7 +6,7 @@ nav_order: 9 # UEFIベアメタル開発チュートリアル -**対象バージョン:** v0.17.1 +**対象バージョン:** v0.17.2 **難易度:** 🔴 上級 **前提知識:** インラインアセンブリ、ポインタ操作 diff --git a/docs/tutorials/ja/compiler/wasm/index.md b/docs/tutorials/ja/compiler/wasm/index.md index 953cbdac..a3279ac1 100644 --- a/docs/tutorials/ja/compiler/wasm/index.md +++ b/docs/tutorials/ja/compiler/wasm/index.md @@ -16,7 +16,7 @@ parent: Tutorials - ブラウザで実行可能 - サーバーレス環境対応 -## サポート状況(v0.17.1) +## サポート状況(v0.17.2) | 機能 | 状態 | |------|------| diff --git a/docs/tutorials/ja/index.md b/docs/tutorials/ja/index.md index be99c838..eb876a31 100644 --- a/docs/tutorials/ja/index.md +++ b/docs/tutorials/ja/index.md @@ -7,10 +7,10 @@ has_children: true [English](../en/) -# Cm言語チュートリアル v0.17.1 +# Cm言語チュートリアル v0.17.2 -**対象バージョン:** v0.17.1 -**最終更新:** v0.17.1 (2026-08-14) +**対象バージョン:** v0.17.2 +**最終更新:** v0.17.2 (2026-08-15) Cm言語の全機能を段階的に学べる包括的なチュートリアル集です。 @@ -138,7 +138,7 @@ Cm言語の全機能を段階的に学べる包括的なチュートリアル集 --- -## ✅ 実装状況一覧(v0.17.1) +## ✅ 実装状況一覧(v0.17.2) | カテゴリ | 機能 | LLVM | WASM | JS | チュートリアル | |---------|------|------|------|-----|---------------| @@ -209,11 +209,11 @@ Cm言語の全機能を段階的に学べる包括的なチュートリアル集 - [テストケース](https://github.com/shadowlink0122/Cm/tree/main/tests/common/) - 全バックエンド共通の機能テスト **推定学習時間:** 18-22時間 -**対象バージョン:** v0.17.1 +**対象バージョン:** v0.17.2 --- -**最終更新:** v0.17.1 (2026-08-14) +**最終更新:** v0.17.2 (2026-08-15) **著者:** Cm Language Development Team diff --git a/docs/tutorials/ja/stdlib/mem.md b/docs/tutorials/ja/stdlib/mem.md index 9427d934..801c4c08 100644 --- a/docs/tutorials/ja/stdlib/mem.md +++ b/docs/tutorials/ja/stdlib/mem.md @@ -115,7 +115,7 @@ int main() { --- -## スマートポインタ(std::mem::smart・v0.17.1) +## スマートポインタ(std::mem::smart・v0.17.2) ヒープ確保した値の解放をRAII(スコープ末尾のデストラクタ)で自動化します。 diff --git a/vscode-extension/package.json b/vscode-extension/package.json index dd9c91bb..87cd3219 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "cm-language", "displayName": "Cm Language Support", "description": "Syntax highlighting and language support for the Cm programming language", - "version": "0.17.1", + "version": "0.17.2", "publisher": "cm-lang", "engines": { "vscode": "^1.125.0" From 584062ef9585ca8c9cab0460e916015f2dcc72c1 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 13:00:33 +0900 Subject: [PATCH 03/25] =?UTF-8?q?v0.17.2:=20=E3=82=BB=E3=83=AB=E3=83=95?= =?UTF-8?q?=E3=83=9B=E3=82=B9=E3=83=86=E3=82=A3=E3=83=B3=E3=82=B0=E5=90=91?= =?UTF-8?q?=E3=81=91=E6=A8=99=E6=BA=96=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9?= =?UTF-8?q?=E3=83=AA=E6=95=B4=E5=82=99=E3=81=A8=E3=82=B3=E3=83=B3=E3=83=91?= =?UTF-8?q?=E3=82=A4=E3=83=A9=E3=83=90=E3=82=B04=E4=BB=B6=E3=83=BB?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9=E3=83=AA=E3=83=90=E3=82=B0?= =?UTF-8?q?2=E4=BB=B6=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 標準ライブラリの追加・強化: - 集合型を追加: TreeSet(平衡二分木=AVL木ベース・昇順走査対応)と HashSet(ハッシュ表ベース・平均O(1)) - TreeMapへAVL削除(回転による平衡維持・空きスロット再利用)・keys_in_order(明示スタックの反復in-order走査)・clearを追加 - std::mem::arena を新設(バンプアロケータ・8バイト整列・チャンク自動拡張・reset一括解放、ベンチで個別alloc/dealloc比約4倍) - std::mem::smart へ WeakPtr(downgrade/upgrade/is_alive・循環参照の切断)と SharedPtr.reset()/weak_count() を追加し、smart.cm を smart/mod.cm へ再構成 - std::mem::smart::atomic を新設(AtomicSharedPtr・参照カウントをアトミック命令で増減、native/jit専用) - std::strings::chars を新設(is_digit等の文字分類・to_upper/to_lower・digit_value(c, base)) - 数値解析を std::io から std::strings::parse へ移設し、基数指定版 parse_int_radix/parse_long_radix(2〜36進)を追加(io側は再エクスポートで互換維持) - Vectorへ reserve/last/sortBy(比較関数ソート)を追加、sortは挿入ソート化 - std::fs::read_lines(行単位読み・LF/CRLF対応)を追加 コンパイラバグ修正(新stdlibが最初の実利用者となり顕在化した潜在バグ、記録はdocs/archive/v0.17.2/): - フィールドレシーバのメソッド呼び出しが一時コピーに実行され変異が消える問題を修正(self引数処理へlower_placeによる場所化分岐を追加) - ジェネリック関数の関数ポインタ引数 int*(T, T) が型置換されずLLVM検証エラーになる問題を修正(正準API・mono側の両実装へFunction型再帰を追加) - ジェネリックメソッドが返す K[] スライスの要素幅がずれる問題を修正(mono特殊化でターミネータにも型置換を適用し、cm_slice_new/cm_array_to_sliceの要素サイズ定数を置換後の型から再計算) - ポインタ経由でしか参照されない構造体がプログラムDCEで削除されAOTだけ不正IRになる問題を修正(使用中struct収集を型ノードの再帰走査に変更) ライブラリバグ修正: - HashSetコンストラクタの二重解放(dtor持ちHashMapの暗黙コピー)による非決定的SIGABRTを修正し、所有権移動後のポインタ無効化で対処 - ユーザ定義Optionとの衝突がlibs内部の選択importの即時型検査で無関係なプログラムへ波及する問題を、ワイルドカードimportへの変更で回避 - JSバックエンドのスライスポインタ引数(K[]*)未対応をTreeMap走査の反復化で回避(バックエンド側は残課題として記録) テスト・ドキュメント: - 機能テスト(Set/TreeMap削除/chars/parse/Arena/Vector強化/fs行読み/WeakPtr/Atomic)とバグ修正回帰4件を追加 - パフォーマンステスト(HashSet 10000件スモーク・Arenaチャーン・TreeSet平衡性の高さ検証)とベンチマーク3本(TreeSet/HashSet/Arena)を追加 - チュートリアル(ja/en)へ集合・文字分類/数値解析ページを新設し、mem/treemap/vector/io/索引を更新 - リリースノートを完成させ、設計文書とバグ記録6件を docs/archive/v0.17.2/ へ移動 - 全スイート通過を確認: interpreter 742・llvm 783・js 682・sv 147・wasm 729・unit/regression VSCode拡張の更新は新構文の追加がないため非該当(ライブラリAPIの追加のみで文法・ハイライト対象の変更なし) --- docs/archive/v0.17.2/bugfix-aot-struct-dce.md | 37 +++ docs/archive/v0.17.2/bugfix-generic-mir.md | 49 ++++ .../v0.17.2/bugfix-hashset-double-free.md | 46 ++++ .../v0.17.2/bugfix-js-slice-pointer-arg.md | 34 +++ .../bugfix-option-namespace-collision.md | 30 ++ .../v0.17.2/selfhosting-stdlib.md | 0 docs/design/index.md | 11 +- docs/releases/v0.17.2.md | 139 ++++++++++ docs/tutorials/en/stdlib/collections/sets.md | 106 ++++++++ .../en/stdlib/collections/treemap.md | 5 +- docs/tutorials/en/stdlib/mem.md | 51 +++- .../en/stdlib/strings/chars-parse.md | 78 ++++++ docs/tutorials/ja/index.md | 3 + docs/tutorials/ja/stdlib/collections/sets.md | 116 ++++++++ .../ja/stdlib/collections/treemap.md | 7 +- .../tutorials/ja/stdlib/collections/vector.md | 17 +- docs/tutorials/ja/stdlib/index.md | 6 +- docs/tutorials/ja/stdlib/io.md | 1 + docs/tutorials/ja/stdlib/json.md | 2 +- docs/tutorials/ja/stdlib/mem.md | 94 ++++++- .../ja/stdlib/strings/chars-parse.md | 97 +++++++ libs/std/collections/hashset.cm | 49 ++++ libs/std/collections/mod.cm | 4 +- libs/std/collections/treemap.cm | 123 ++++++++- libs/std/collections/treeset.cm | 59 ++++ libs/std/collections/vector.cm | 59 +++- libs/std/fs/mod.cm | 8 + libs/std/io/console/input.cm | 161 +---------- libs/std/mem/arena.cm | 114 ++++++++ libs/std/mem/smart.cm | 153 ----------- libs/std/mem/smart/atomic.cm | 85 ++++++ libs/std/mem/smart/mod.cm | 256 ++++++++++++++++++ libs/std/strings/chars.cm | 95 +++++++ libs/std/strings/mod.cm | 5 +- libs/std/strings/parse.cm | 247 +++++++++++++++++ src/internal/mir/lowering/expr/call.cpp | 18 +- src/internal/mir/lowering/mono/internal.cpp | 26 ++ src/internal/mir/lowering/mono/specialize.cpp | 70 +++++ .../mir/passes/cleanup/program_dce.cpp | 46 ++-- src/internal/syntax/ast/types.cpp | 12 +- tests/benchmarks/cm/08_treeset_ops.cm | 39 +++ tests/benchmarks/cm/09_hashset_ops.cm | 41 +++ tests/benchmarks/cm/10_arena_vs_alloc.cm | 40 +++ .../common/generics/functions/fnptr_param.cm | 27 ++ .../generics/functions/fnptr_param.expect | 3 + tests/common/generics/slices/method_return.cm | 51 ++++ .../generics/slices/method_return.expect | 3 + .../stdlib/collections/hashset_basic.cm | 25 ++ .../stdlib/collections/hashset_basic.expect | 5 + .../common/stdlib/collections/hashset_load.cm | 34 +++ .../stdlib/collections/hashset_load.expect | 3 + .../stdlib/collections/treemap_remove.cm | 35 +++ .../stdlib/collections/treemap_remove.expect | 7 + .../stdlib/collections/treeset_balance.cm | 36 +++ .../stdlib/collections/treeset_balance.expect | 5 + .../stdlib/collections/treeset_basic.cm | 36 +++ .../stdlib/collections/treeset_basic.expect | 6 + .../stdlib/collections/vector_enhance.cm | 35 +++ .../stdlib/collections/vector_enhance.expect | 6 + tests/common/stdlib/core/parse_option_test.cm | 2 +- tests/common/stdlib/fs/lines.cm | 18 ++ tests/common/stdlib/fs/lines.expect | 2 + tests/common/stdlib/mem/arena_alloc.cm | 47 ++++ tests/common/stdlib/mem/arena_alloc.expect | 6 + tests/common/stdlib/mem/arena_churn.cm | 23 ++ tests/common/stdlib/mem/arena_churn.expect | 2 + tests/common/stdlib/smart/atomic_shared.cm | 23 ++ .../common/stdlib/smart/atomic_shared.expect | 5 + tests/common/stdlib/smart/weak.cm | 32 +++ tests/common/stdlib/smart/weak.expect | 7 + tests/common/stdlib/strings/chars_classify.cm | 17 ++ .../stdlib/strings/chars_classify.expect | 10 + tests/common/stdlib/strings/parse_radix.cm | 41 +++ .../common/stdlib/strings/parse_radix.expect | 7 + tests/common/structs/field_receiver.cm | 66 +++++ tests/common/structs/field_receiver.expect | 4 + tests/common/structs/ptr_only_struct.cm | 57 ++++ tests/common/structs/ptr_only_struct.expect | 2 + 78 files changed, 2967 insertions(+), 360 deletions(-) create mode 100644 docs/archive/v0.17.2/bugfix-aot-struct-dce.md create mode 100644 docs/archive/v0.17.2/bugfix-generic-mir.md create mode 100644 docs/archive/v0.17.2/bugfix-hashset-double-free.md create mode 100644 docs/archive/v0.17.2/bugfix-js-slice-pointer-arg.md create mode 100644 docs/archive/v0.17.2/bugfix-option-namespace-collision.md rename docs/{design => archive}/v0.17.2/selfhosting-stdlib.md (100%) create mode 100644 docs/tutorials/en/stdlib/collections/sets.md create mode 100644 docs/tutorials/en/stdlib/strings/chars-parse.md create mode 100644 docs/tutorials/ja/stdlib/collections/sets.md create mode 100644 docs/tutorials/ja/stdlib/strings/chars-parse.md create mode 100644 libs/std/collections/hashset.cm create mode 100644 libs/std/collections/treeset.cm create mode 100644 libs/std/mem/arena.cm delete mode 100644 libs/std/mem/smart.cm create mode 100644 libs/std/mem/smart/atomic.cm create mode 100644 libs/std/mem/smart/mod.cm create mode 100644 libs/std/strings/chars.cm create mode 100644 libs/std/strings/parse.cm create mode 100644 tests/benchmarks/cm/08_treeset_ops.cm create mode 100644 tests/benchmarks/cm/09_hashset_ops.cm create mode 100644 tests/benchmarks/cm/10_arena_vs_alloc.cm create mode 100644 tests/common/generics/functions/fnptr_param.cm create mode 100644 tests/common/generics/functions/fnptr_param.expect create mode 100644 tests/common/generics/slices/method_return.cm create mode 100644 tests/common/generics/slices/method_return.expect create mode 100644 tests/common/stdlib/collections/hashset_basic.cm create mode 100644 tests/common/stdlib/collections/hashset_basic.expect create mode 100644 tests/common/stdlib/collections/hashset_load.cm create mode 100644 tests/common/stdlib/collections/hashset_load.expect create mode 100644 tests/common/stdlib/collections/treemap_remove.cm create mode 100644 tests/common/stdlib/collections/treemap_remove.expect create mode 100644 tests/common/stdlib/collections/treeset_balance.cm create mode 100644 tests/common/stdlib/collections/treeset_balance.expect create mode 100644 tests/common/stdlib/collections/treeset_basic.cm create mode 100644 tests/common/stdlib/collections/treeset_basic.expect create mode 100644 tests/common/stdlib/collections/vector_enhance.cm create mode 100644 tests/common/stdlib/collections/vector_enhance.expect create mode 100644 tests/common/stdlib/fs/lines.cm create mode 100644 tests/common/stdlib/fs/lines.expect create mode 100644 tests/common/stdlib/mem/arena_alloc.cm create mode 100644 tests/common/stdlib/mem/arena_alloc.expect create mode 100644 tests/common/stdlib/mem/arena_churn.cm create mode 100644 tests/common/stdlib/mem/arena_churn.expect create mode 100644 tests/common/stdlib/smart/atomic_shared.cm create mode 100644 tests/common/stdlib/smart/atomic_shared.expect create mode 100644 tests/common/stdlib/smart/weak.cm create mode 100644 tests/common/stdlib/smart/weak.expect create mode 100644 tests/common/stdlib/strings/chars_classify.cm create mode 100644 tests/common/stdlib/strings/chars_classify.expect create mode 100644 tests/common/stdlib/strings/parse_radix.cm create mode 100644 tests/common/stdlib/strings/parse_radix.expect create mode 100644 tests/common/structs/field_receiver.cm create mode 100644 tests/common/structs/field_receiver.expect create mode 100644 tests/common/structs/ptr_only_struct.cm create mode 100644 tests/common/structs/ptr_only_struct.expect diff --git a/docs/archive/v0.17.2/bugfix-aot-struct-dce.md b/docs/archive/v0.17.2/bugfix-aot-struct-dce.md new file mode 100644 index 00000000..fda1f603 --- /dev/null +++ b/docs/archive/v0.17.2/bugfix-aot-struct-dce.md @@ -0,0 +1,37 @@ +# v0.17.2 バグ修正: ポインタ経由参照のみの構造体がプログラムDCEで削除される(AOT限定の不正IR) + +セルフホスティング向け標準ライブラリの実装中に発見した6件目の不具合の記録。 +`AtomicSharedPtr`(制御ブロック `AtomicSharedCount` をポインタでのみ保持する)が最初の顕在化点で、v0.17.2で修正済み。 + +## 症状 + +`cm run`(JIT)では正常動作するプログラムが、`cm compile`(AOT)だけ「LLVM module verification failed: Load operand must be a pointer」で失敗する。 +生成IRを見ると、`&self.count->strong` のような二段ポインタ越しのフィールドアドレス計算から最終フィールドGEPとstoreが**黙って脱落**し、未初期化ローカルの読み出しや `load ptr, i32 1` のような不正命令が残る。 + +## 真因 + +コンパイルパイプラインの2段の問題の合成。 + +1. **プログラムDCEの使用中struct収集がポインタを辿らない**(`mir/passes/cleanup/program_dce.cpp`): 使用中構造体の収集が「値型のローカル」と「構造体フィールドの値型」だけを見ており、`Ctr*` のようにポインタ・型引数経由でしか現れない構造体を「未使用」と誤判定して定義ごと削除していた。 +2. **codegenのフィールド投影が型解決失敗を黙って飲む**(`codegen/llvm/core/operand.cpp`): struct定義が消えているためフィールド投影のstruct型解決が失敗し、`getPlaceAddress` がnullptrを返して当該文の命令生成がスキップされる(エラーはデバッグログのみ)。結果として不正IRが検証段まで進む。 + +プログラムDCEはAOT(`cm compile`)のパイプラインのみで走るため、JITでは顕在化しない。 +`SharedPtr` が同じ構造(`SharedCount*`)でも動いていたのは、他の生存判定経路で偶然structが残っていたため。 + +## 修正 + +DCEの使用中struct収集に型ノードの再帰走査を導入した(`collect_struct_names_from_type`)。 +ポインタ(`element_type`)・配列・型引数(`type_args`)を再帰的に辿り、Struct/Generic種別の名前を全て「使用中」として収集する。 +フィールド走査側も同じ再帰ヘルパーで統一し、`Ctr*` フィールドのようなポインタ経由参照も生存扱いになる。 + +デバッグの決め手は `CM_DUMP_IR=1` によるJIT/AOTのIR直接diff(AOTだけGEP+storeが欠落)と、フィールド投影失敗点への一時診断出力(`structName='Ctr'` の解決失敗を直接確認)。 + +## 残課題(設計課題) + +codegen側の「型解決失敗で命令を黙ってスキップする」挙動は不正IRの温床であり、ハードエラー化(該当関数名・投影位置付きの診断)が望ましい。 +v0.17.2ではDCE側の修正でIR不正の実経路は塞がったため、codegen側のエラー化は今後の課題として記録する。 + +## 回帰テスト + +- `tests/common/structs/ptr_only_struct.cm` — ポインタ経由でのみ参照される構造体(ジェネリックimpl内の `Ctr*` フィールド・二段デリファレンスの読み書き)の最小再現(本修正で新規追加) +- `tests/common/stdlib/smart/atomic_shared.cm` — 顕在化点の実利用テスト(llvmスイートでAOT経路を通る) diff --git a/docs/archive/v0.17.2/bugfix-generic-mir.md b/docs/archive/v0.17.2/bugfix-generic-mir.md new file mode 100644 index 00000000..4061d3c4 --- /dev/null +++ b/docs/archive/v0.17.2/bugfix-generic-mir.md @@ -0,0 +1,49 @@ +# v0.17.2 バグ修正: ジェネリック特殊化とメソッドレシーバのMIR実体参照 + +セルフホスティング向け標準ライブラリの実装中に発見した3件のコンパイラバグの記録。 +いずれもstdの新モジュール(TreeSet/HashSet/Vector.sortBy等)が最初の実利用者となって顕在化した潜在バグで、v0.17.2で修正済み。 + +## バグ1: フィールドレシーバのメソッド呼び出しがコピーに対して実行される + +**症状**: `w.c.bump()` のように構造体フィールドをレシーバにしたメソッド呼び出しで、メソッド内の `self` フィールド変異が呼び出し元に反映されない(非ジェネリックでも発生)。 +HashSet/TreeSetのような「内部にマップを持つラッパー型」が全滅する致命的な問題だった(`self.map.insert()` の件数更新が消え、ヒープ経由の書き込みだけが偶然残る)。 + +**真因**: MIRのメソッド呼び出しlowering(`expr/call.cpp` のself引数処理)が、レシーバ式の場所化を変数参照(HirVarRef)とポインタデリファレンスの2形だけ特別扱いし、フィールドアクセス(HirMember)等はフォールバックで「一時コピーへの参照」を渡していた。 +場所化API `lower_place`(メンバ・インデックス・デリファレンスのチェーンをMirPlaceへ写す唯一の機構)は存在したが、self引数経路から使われていなかった(`resolve_receiver_place` は定義のみのデッドコードだった)。 + +**修正**: self引数処理へ場所化分岐を追加し、`lower_place` が解決できるレシーバは実体のアドレス(投影付きPlaceへの `MirRvalue::ref`)を渡すようにした。右辺値レシーバ(関数戻り値への直接呼び出し等)のみ従来のコピー渡しへフォールバックする。 + +## バグ2: ジェネリック関数の関数ポインタ引数でシグネチャが未置換 + +**症状**: ` void f(int*(T, T) cmp, ...)` やジェネリックimplメソッドの `int*(T, T)` 引数経由の間接呼び出しが、LLVM検証エラー「Call parameter type does not match function signature」で失敗する。 +`Vector.sortBy(比較ラムダ)` が最初の顕在化点。 + +**真因**: 型パラメータ置換が2実装とも関数型の内側を辿っていなかった。 + +- 正準API `ast::substitute_type_params` は element_type / type_args のみ再帰し、`param_types` / `return_type` を辿らず、さらに「リーフ共有」早期リターンの判定にも関数型フィールドが含まれていなかった(未置換のまま共有返却)。 +- mono側の `substitute_type_in_type` にはFunction型の分岐自体が無かった。 + +**修正**: 両実装にFunction型の再帰(param_types / return_type)を追加した。 + +## バグ3: ジェネリックメソッドが返す `K[]` スライスの要素幅ずれ + +**症状**: ジェネリックimplメソッド内で構築した `K[]`(ローカルまたはフィールド)を返すと、呼び出し側の要素読みが交互にゴミ値になる(`[1, ゴミ, 2, ゴミ]`)。メソッド内での読みは正常。 +TreeMapの `keys_in_order()`(in-order走査の返却)が最初の顕在化点。 + +**真因**: ジェネリック関数のMIRはスライス生成の要素サイズを「未解決のK(構造体扱いの既定幅)」で**定数として焼き込む**(`cm_slice_new(elem_size, cap)` の第1引数・`cm_array_to_slice(ptr, len, elem_size)` の第3引数)。 +mono特殊化はローカルの型は置換するが、この埋め込み定数と、そもそもターミネータ(呼び出しの引数・戻り値格納先)のplace型を置換対象にしていなかった。 +その結果ヘッダの `elem_size` が過大なまま残り、blob規約のpush/getはヘッダ基準で整合する一方、呼び出し側の静的ストライド(int=4)と食い違って読みがずれていた。 + +**修正**(`mono/specialize.cpp`): + +1. ターミネータ(Call引数・destination・SwitchInt判別値)にも型置換を適用する(従来はstatementのみ)。 +2. `cm_slice_new` / `cm_array_to_slice` の要素サイズ引数を、置換後の宛先スライス型から `layout::slice_elem_stride_of`(集約サイズは `calculate_specialized_type_size`)で再計算して差し替える。blob系push/getはヘッダの `elem_size` を参照するサイズ非依存の規約のため、ヘッダサイズの補正だけで全経路が整合する。 + +既存の `sizeof_for_T` マーカー(ジェネリックsizeofの遅延解決)と同じ「ジェネリック依存の定数はmonoで再計算する」方針への追従であり、スライス生成の2関数がこの方針から漏れていた。 + +## 回帰テスト + +- `tests/common/structs/field_receiver.cm`: フィールドレシーバ変異(直接・main経由・非ジェネリックimpl経由・ジェネリックimpl経由) +- `tests/common/generics/functions/fnptr_param.cm`: ジェネリック自由関数・implメソッドの関数ポインタ引数 +- `tests/common/generics/slices/method_return.cm`: `K[]` 返却(ローカル構築・フィールド構築・部分修飾)と要素値の完全一致 +- TreeMap/TreeSetのin-order走査テスト(stdlib側)も同経路を常時検証する diff --git a/docs/archive/v0.17.2/bugfix-hashset-double-free.md b/docs/archive/v0.17.2/bugfix-hashset-double-free.md new file mode 100644 index 00000000..24ffbc5f --- /dev/null +++ b/docs/archive/v0.17.2/bugfix-hashset-double-free.md @@ -0,0 +1,46 @@ +# v0.17.2 バグ修正: HashSetコンストラクタの二重解放(dtor持ち構造体の暗黙コピー) + +セルフホスティング向け標準ライブラリの実装中に発見した4件目の不具合の記録。 +コンパイラ本体の欠陥ではなく、Cmの現行セマンティクス(構造体代入は浅いコピー・コピー時に所有権の概念なし)とdtor持ち構造体の組み合わせで生じるライブラリ側の二重解放で、v0.17.2でライブラリ側を修正済み。 + +## 症状 + +`HashSet` を使うプログラムが**非決定的に**SIGABRT(`pointer being freed was not allocated`)またはSIGTRAPで異常終了する。 +同一バイナリ・同一プログラムでも実行ごとに完走・途中クラッシュが揺れ(10回中3回程度失敗)、ヒープの再利用状況に依存するため再現条件が安定しない。 +表面上は「大量挿入でハングする」ように見え、当初はパフォーマンス問題やコンパイラのループと誤認しやすい。 + +## 真因 + +`HashSet.self()` がローカルで `HashMap m()` を構築し `self.map = m;` で代入していた。 +Cmの構造体代入は浅いコピーであり、コンストラクタ終端で `m` のデストラクタが走って `entries` バッファを解放するため、`self.map.entries` は最初からdanglingポインタになる。 +以後の挿入は解放済みメモリへの書き込みとして偶然動作し、`grow()` が旧バッファを `dealloc` した時点で二重解放としてmallocに検出される(検出されない実行では完走する)。 + +デバッグの決め手はハング中プロセスの `sample` によるスタック採取で、JITフレーム内からの `___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED` が直接得られた。 + +## 修正 + +ライブラリ側(`libs/std/collections/hashset.cm`)で、代入後にローカル `m` の保持ポインタを明示的に無効化して所有権移動を表現した。 +`HashMap` のデストラクタはnullポインタを解放しないため、`m` のdtorは無害化される。 + +```cm +self() { + HashMap m(); + self.map = m; + // mのdtorによる二重解放を防ぐ(所有権はself.mapへ移動済み) + m.entries = 0 as Entry*; +} +``` + +同型のラッパーである `TreeSet`(`TreeMap` を内包)は、`TreeMap` がスライス(ランタイム管理)ベースでデストラクタを持たないため影響しない。 +`libs/std` 全体を「ローカル構築→フィールド代入」パターンで横断確認し、dtor持ち構造体の該当箇所は `HashSet` のみだった。 + +## 言語側のフォローアップ(未実装・設計課題) + +この事故はライブラリ利用者が同じパターン(dtor持ち構造体を値としてコピー・フィールドへ代入)を書けば誰でも踏む。 +`selfhosting-stdlib.md` の言語側前提課題に挙げた「dtor持ち構造体の暗黙コピーへの警告」および「move代入(`self.map = move m;`)のサポート」が恒久対策であり、v0.17.2ではスコープ外として設計課題に留める。 + +## 回帰テスト + +- `tests/common/stdlib/collections/hashset_basic.cm` — 機能検証(既存) +- `tests/common/stdlib/collections/hashset_load.cm` — 10000件の挿入・参照・半数削除のパフォーマンススモーク(本修正で新規追加。修正前は非決定的クラッシュ) +- `tests/benchmarks/cm/09_hashset_ops.cm` — 50000件のベンチマーク diff --git a/docs/archive/v0.17.2/bugfix-js-slice-pointer-arg.md b/docs/archive/v0.17.2/bugfix-js-slice-pointer-arg.md new file mode 100644 index 00000000..89a011ab --- /dev/null +++ b/docs/archive/v0.17.2/bugfix-js-slice-pointer-arg.md @@ -0,0 +1,34 @@ +# v0.17.2 バグ記録: JSバックエンドのスライスポインタ引数(K[]*)未対応 + +セルフホスティング向け標準ライブラリの実装中に発見した7件目の不具合の記録。 +`TreeMap.keys_in_order()` の内部再帰ヘルパー `collect_keys(int node, K[]* out)` が最初の顕在化点で、v0.17.2ではライブラリ側の書き換えで回避した(バックエンド側は未修正の既知制限として記録)。 + +## 症状 + +スライスへのポインタ(`K[]*`)を関数引数として渡し、呼び出し先で `(*out).push(...)` すると、JSバックエンドだけ実行時エラーになる。 + +``` +TypeError: Cannot read properties of undefined (reading 'undefined') + at TreeMap__int__bool__collect_keys (...:776:38) +``` + +native(JIT/AOT)・interpreterでは正常動作する。 + +## 真因 + +JSバックエンドのポインタ表現は `{__arr, __idx}`(配列+添字のセル参照)だが、スライス(JS配列)そのものを指すポインタのデリファレンス+メソッド呼び出し(`__cm_unwrap(out.__arr[out.__idx]).push(...)`)で参照セルの解決が壊れる。 +ローカル変数のアドレス渡し一般では動くが、再帰呼び出しへ引き回されるスライスポインタで `__arr` が未定義になる。 + +## 回避(ライブラリ側) + +`TreeMap.keys_in_order()` を再帰ヘルパー+出力ポインタ方式から、明示スタックの反復in-order走査へ書き換えた(`libs/std/collections/treemap.cm` に制約コメントあり)。 +挙動・計算量(O(n))は同一で、全バックエンドで同一結果になる。 + +## 残課題(バックエンド側) + +JSバックエンドのスライスポインタ引数の参照セル解決の修正は今後の課題として記録する。 +それまで、libsおよびユーザコードでJS対象のコードは「スライスを関数で育てる」場合に出力ポインタ引数でなく戻り値返し・反復走査を使うこと。 + +## 回帰テスト + +- `tests/common/stdlib/collections/treeset_basic.cm` / `treemap_remove.cm` / `treeset_balance.cm` — jsスイートで反復走査版keys_in_orderの動作を担保 diff --git a/docs/archive/v0.17.2/bugfix-option-namespace-collision.md b/docs/archive/v0.17.2/bugfix-option-namespace-collision.md new file mode 100644 index 00000000..988e601b --- /dev/null +++ b/docs/archive/v0.17.2/bugfix-option-namespace-collision.md @@ -0,0 +1,30 @@ +# v0.17.2 バグ修正: ユーザ定義Optionとプレリュード衝突のライブラリ波及(選択importの即時型検査) + +セルフホスティング向け標準ライブラリの実装中に発見した5件目の不具合の記録。 +パース関数の `std::strings::parse` への移設に伴い、ローカルで `enum Option { None, Some(int) }` を再定義している既存プログラム4件(`tests/common/types/enum/`)が「libs内のコード」の型エラーでコンパイル不能になった。 +v0.17.2ではライブラリ側の構成で回避し、コンパイラ側の2つの潜在問題は設計課題として記録する。 + +## 症状 + +`import std::io::println;` しかしていないプログラムで、ユーザが `Option` を独自enumとして再定義すると、`libs/std/strings/parse.cm` 内部の `Option::Some(true)` が「expected int, got bool」の型エラーになりコンパイルが失敗する。 +ユーザのコードにもimportにも `parse` は登場しないため、エラー位置がlibs内部を指し原因が追いにくい。 + +## 真因(2つの潜在問題の合成) + +1. **プレリュードOptionのフラット上書き**: ユーザ定義の `Option` / `Result` は組み込み型をプログラム全体のフラット名前空間で上書きする仕様(`typedecl.cpp` のenum登録)。そのため、コンパイル対象に含まれたlibsモジュールの `Option` 参照もユーザ型へ解決される。これはHEAD時点から存在する挙動で、`import std::io::*;` とローカルOption再定義の組み合わせでは移設前から同じエラーになる(今回顕在化しただけの既存問題)。 +2. **選択importの即時型検査**: `import std::strings::parse::{parse_int, ...};` のような選択importは、対象関数の本体をその場で型検査する。一方 `import std::strings::parse::*;`(ワイルドカード)と `export import` は利用時まで検査を遅延する。移設時にinput.cmへ内部利用向けの選択importを書いたため、printlnしか使わないプログラムにまでparse.cmの本体検査(=衝突1の顕在化)が波及した。 + +## 修正(ライブラリ構成での回避) + +- `libs/std/io/console/input.cm` の内部importを選択importからワイルドカード(`import std::strings::parse::*;`)へ変更した。`input_int` 等の解決は維持され、parse本体の検査は実際に使うプログラムまで遅延される。 +- `libs/std/strings/mod.cm` のファサード(`export import std.strings.chars / parse`)は遅延のため安全であり維持する。両ファイルに「chars/parseを内部利用する際は選択importを使わない」制約コメントを残した。 + +## 言語側のフォローアップ(未実装・設計課題) + +- 型解決のモジュール分離: libsモジュール内の `Option` 参照はユーザ再定義の影響を受けずプレリュード定義へ解決されるべき(`__prelude` 属性による区別は登録済みで、解決側の分離が未実装)。 +- 選択importとワイルドカードimportで型検査のタイミングが異なる非一貫性の解消。 + +## 回帰テスト + +- `tests/common/types/enum/{simple_compare, variable, guard_condition, match_extract}.cm` — ローカルOption再定義プログラムが引き続きコンパイル・実行できることを担保する既存テスト(本件の検出元) +- `tests/common/stdlib/core/parse_option_test.cm`・`tests/common/stdlib/strings/parse_radix.cm` — 移設後のparse APIの機能検証 diff --git a/docs/design/v0.17.2/selfhosting-stdlib.md b/docs/archive/v0.17.2/selfhosting-stdlib.md similarity index 100% rename from docs/design/v0.17.2/selfhosting-stdlib.md rename to docs/archive/v0.17.2/selfhosting-stdlib.md diff --git a/docs/design/index.md b/docs/design/index.md index 0e1b77c1..d61054fe 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -18,18 +18,23 @@ Cm言語コンパイラの設計文書の一覧。実装が完了した設計文 | [v1.0.0ロードマップ](roadmap_v1.0.0.html) | v1.0.0までのマイルストーン別計画 | | [v0.16.0 SVバックエンド拡充(アーカイブ)](../archive/v0.16.0/roadmap.html) | v0.16.0開発時のロードマップ(Verilogギャップ分析・tcl/cst統合検討) | -## v0.17.2 設計 +## v0.17.2 設計(実装済み・アーカイブ済み) | ドキュメント | 内容 | |---|---| -| [セルフホスティング向け標準ライブラリ整備計画](v0.17.2/selfhosting-stdlib.html) | コンパイラ実装に必要なstdの棚卸しとArena・文字分類・parse・Set等の実装計画 | +| [セルフホスティング向け標準ライブラリ整備計画](../archive/v0.17.2/selfhosting-stdlib.html) | コンパイラ実装に必要なstdの棚卸しとArena・文字分類・parse・Set等の実装計画(実装完了) | +| [スマートポインタ](../archive/v0.17.2/smart-pointers.html) | UniquePtr/SharedPtr(std::mem::smart)のRAII設計・move/clone所有規律・実測に基づく制約(実装完了) | +| [バグ修正: ジェネリック特殊化とメソッドレシーバ](../archive/v0.17.2/bugfix-generic-mir.html) | 標準ライブラリ実装で顕在化した3件のコンパイラバグ(フィールドレシーバのコピー・関数ポインタ引数の未置換・スライス要素幅ずれ)の記録(修正完了) | +| [バグ修正: HashSetの二重解放](../archive/v0.17.2/bugfix-hashset-double-free.html) | dtor持ち構造体の暗黙コピーによる非決定的クラッシュの真因と修正・言語側の設計課題の記録(修正完了) | +| [バグ修正: ユーザ定義Optionの衝突波及](../archive/v0.17.2/bugfix-option-namespace-collision.html) | 選択importの即時型検査とプレリュードOptionのフラット上書きの合成問題の記録(ライブラリ構成で回避) | +| [バグ修正: ポインタ経由参照のみの構造体のDCE誤削除](../archive/v0.17.2/bugfix-aot-struct-dce.html) | AOT限定で不正IRになるプログラムDCEの使用中struct収集漏れの記録(修正完了) | +| [バグ記録: JSのスライスポインタ引数未対応](../archive/v0.17.2/bugfix-js-slice-pointer-arg.html) | K[]*引数の再帰渡しがJSバックエンドで壊れる制限の記録(ライブラリ側は反復走査で回避・バックエンド側は残課題) | ## v0.17.1 設計(実装済み・アーカイブ済み) | ドキュメント | 内容 | |---|---| | [ネスト型宣言](../archive/v0.17.1/nested-type-declarations.html) | struct/enum本体内のstruct/enum宣言と `Outer::Inner` / `Outer::Inner::MEM` 修飾アクセスチェーンの設計(実装完了) | -| [スマートポインタ](../archive/v0.17.2/smart-pointers.html) | UniquePtr/SharedPtr(std::mem::smart)のRAII設計・move/clone所有規律・実測に基づく制約(実装完了・v0.17.2収載) | ## v0.17.0 設計(全件完遂・アーカイブ済み) diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index f6788373..01a3ca30 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -38,4 +38,143 @@ println("rc={a.use_count()}"); // 2 - 対応はnative系(jit/native/wasm/uefi/baremetal)。JS/TSはポインタ禁止(GCのため不要)、SVは対象外。 - 所有規律: 暗黙コピー(`b = a;`)は両dtorが走り二重解放になるため、移動は `move`・共有は `clone()`・所有型の返却は `return move` を必須とする。 +### WeakPtr - 弱参照(std::mem::smart) + +SharedPtrだけでは相互参照する構造(親子リンク等)が参照カウントの循環で解放されなかった。 +`SharedPtr.downgrade()` で作る弱参照 `WeakPtr` を追加し、循環参照を切断できるようにした。 + +```cm +SharedPtr a(42); +WeakPtr w = a.downgrade(); // strongは増えない +SharedPtr up = w.upgrade(); // 生存中はSharedPtrへ昇格、死後はnull +println("alive={w.is_alive()}"); +``` + +- API: `is_alive/upgrade/clone`。制御ブロックはstrong=0でペイロード解放後もweakが残る限り維持され、最後のWeakPtr破棄で解放される。 +- 併せてSharedPtrへ `weak_count()` と `reset()` を追加した(`move` はエイリアスで破棄タイミングが移動元スコープ末尾のため、決定的に手放す手段として `reset()` を使う)。 + +### AtomicSharedPtr - スレッド安全な参照カウント(std::mem::smart::atomic) + +SharedPtrの参照カウントは非アトミックで、clone済みインスタンスを別スレッドへ渡すと増減が競合し得た。 +参照カウントの増減をアトミック命令(ネイティブC++ランタイムの``)で行う `AtomicSharedPtr` を追加した。 +API・所有規律はSharedPtrと同一で、対応はnative/jitのみ(ペイロード自体の同期は別途Mutex等で守る)。 + +### Arenaアロケータ(std::mem::arena) + +短命な大量オブジェクト(パーサのAST等)を1件ずつ `alloc/dealloc` すると管理コストが支配的だった。 +「まとめて確保・まとめて解放」のバンプアロケータ `Arena` を追加した(ベンチマークで個別alloc/dealloc比 約4倍)。 + +```cm +Arena a(); // 既定チャンク4096バイト +int* x = a.alloc_bytes(4) as int*; // 8バイト整列のバンプ確保 +a.reset(); // 全チャンク一括解放 +``` + +- チャンク不足時は自動拡張し、チャンク超の大型要求は専用チャンクで受ける。 +- `allocated_bytes()` で総確保バイトを取得できる。 + +### 集合型 TreeSet / HashSet(std::collections) + +重複しない値の集まりを扱うには `TreeMap/HashMap` へダミー値を詰める必要があった。 +順序付き集合 `TreeSet`(平衡二分木=AVL木ベース)と順序なし集合 `HashSet`(ハッシュ表ベース)を追加した。 + +```cm +TreeSet s(); +s.insert(30); s.insert(10); s.insert(10); // 重複は無視 +int[] xs = s.values_in_order(); // 昇順走査 [10, 30] + +HashSet h(); +h.insert(100); +println("{h.contains(100)}"); // 平均O(1) +``` + +- TreeSetは `insert/contains/remove/len/is_empty/clear/values_in_order`(全てO(log n)・走査はO(n))。 +- 20000要素で木高15(log2(20000)≈14.3)となり平衡を維持することをテストで確認済み。 +- HashSetは平均O(1)の `insert/contains/remove` を提供する。 + +### TreeMapの削除・順序走査・clear(std::collections::treemap) + +TreeMapは挿入・探索のみで、削除や全キーの順序取得ができなかった。 +AVL削除(回転による平衡維持)・in-order走査・clearを追加した。 + +- `remove(key)`: 葉・子1・子2の全ケースをO(log n)で削除し、空きスロットはfree listで再利用する。 +- `keys_in_order()`: 全キーを昇順の `K[]` で返す。 +- `clear()`: 全要素を削除する。 + +### 文字分類(std::strings::chars) + +字句解析に必要な文字種判定が標準ライブラリになく、セルフホスティングの前提を満たせなかった。 +`is_digit/is_alpha/is_alnum/is_space/is_upper/is_lower/is_hex_digit/is_ident_start/is_ident_continue/to_upper/to_lower/digit_value(c, base)` を追加した。 + +### 数値解析の移設と基数対応(std::strings::parse) + +`parse_int` 等の数値解析が入出力モジュール(std::io)に同居しており、文字列処理として独立していなかった。 +`std::strings::parse` へ移設し、基数指定版 `parse_int_radix/parse_long_radix`(2〜36進)を追加した。 +`import std::io::*;` 経由の既存コードは再エクスポートで引き続き動作する。 + +### Vectorの強化(reserve / last / sortBy) + +Vectorは容量の事前確保・末尾参照・任意順序のソートができなかった。 +`reserve(n)`(縮小なしの事前拡張)・`last()`(popせず末尾参照)・`sortBy(cmp)`(比較関数`int*(T, T)`によるソート)を追加した。 + +```cm +v.sortBy((int a, int b) => b - a); // 降順 +``` + +### ファイルの行読み(std::fs::read_lines) + +ファイルを行単位で処理するには `read_file` + `lines` の組み合わせを毎回書く必要があった。 +`read_lines(path)` を追加した(LF/CRLF対応・失敗時は空スライス)。 + +## 🐛 コンパイラバグ修正(標準ライブラリ実装で顕在化) + +新しい標準ライブラリが最初の実利用者となり、計4件の潜在コンパイラバグを発見・修正した([ジェネリック特殊化まわり3件の記録](../archive/v0.17.2/bugfix-generic-mir.html)・[DCE誤削除の記録](../archive/v0.17.2/bugfix-aot-struct-dce.html))。 + +### フィールドレシーバのメソッド呼び出しがコピーに実行される + +`w.c.bump()` のようにフィールドをレシーバにすると、メソッド内の `self` 変異が呼び出し元へ反映されなかった(HashSet/TreeSetのようなラッパー型が全滅する問題)。 +MIRのself引数処理へ場所化分岐(`lower_place`)を追加し、実体のアドレスを渡すようにした。 + +### ジェネリック関数の関数ポインタ引数が未置換 + +`` 関数の `int*(T, T)` 引数がmono特殊化で置換されず、LLVM検証エラーになった(`Vector.sortBy` が顕在化点)。 +型置換の2実装(正準API・mono側)へFunction型の再帰を追加した。 + +### ジェネリックメソッドが返すスライスの要素幅ずれ + +ジェネリックメソッドから `K[]` を返すと要素読みが交互にゴミ値になった(`TreeMap.keys_in_order` が顕在化点)。 +mono特殊化でターミネータにも型置換を適用し、スライス生成の要素サイズ定数を置換後の型から再計算するようにした。 + +### ポインタ経由参照のみの構造体がAOTで消え不正IRになる + +`cm run` では動くプログラムが `cm compile` だけ「Load operand must be a pointer」の検証エラーで失敗した(`AtomicSharedPtr` の制御ブロックが顕在化点。[記録](../archive/v0.17.2/bugfix-aot-struct-dce.html))。 +AOT限定のプログラムDCEが使用中構造体の収集でポインタ・型引数を辿らず、`Ctr*` のようにポインタでしか参照されない構造体定義を削除し、codegenのフィールド投影が型解決失敗で命令を黙って落としていた。 +DCEの収集を型ノードの再帰走査(ポインタ・配列・型引数)に改めて修正した。 + +## 🐛 標準ライブラリのバグ修正 + +### HashSetの非決定的クラッシュ(二重解放) + +`HashSet` を使うプログラムが実行ごとに完走・SIGABRTが揺れる非決定的クラッシュを起こした([記録](../archive/v0.17.2/bugfix-hashset-double-free.html))。 +コンストラクタの `self.map = m;` が浅いコピーで、ローカル `m` のデストラクタが `entries` を解放してdanglingになるのが真因(ハング中プロセスのスタック採取でmallocの二重解放検出を直接確認)。 +所有権移動後にローカルの保持ポインタを無効化して修正した。 +恒久対策として「dtor持ち構造体の暗黙コピーへの警告」「move代入」を言語側の設計課題に記録した。 + +### JSバックエンドのスライスポインタ引数の制限回避(TreeMap走査) + +`K[]*` 引数を再帰ヘルパーへ引き回す `TreeMap.keys_in_order()` がJSバックエンドだけ実行時エラーになった([記録](../archive/v0.17.2/bugfix-js-slice-pointer-arg.html))。 +JSのポインタ表現(配列+添字のセル参照)がスライスポインタのデリファレンスで壊れる既知制限のため、明示スタックの反復in-order走査へ書き換えて回避した(バックエンド側の修正は残課題として記録)。 + +### ユーザ定義Optionとプレリュード衝突のライブラリ波及 + +ローカルで `enum Option` を再定義しているプログラムが、importしていない `std::strings::parse` の型エラーでコンパイル不能になった([記録](../archive/v0.17.2/bugfix-option-namespace-collision.html))。 +選択import(`::{名前}`)が対象関数本体を即時型検査する挙動と、ユーザ定義Optionのフラット名前空間上書きの合成が真因。 +ライブラリ内部の利用をワイルドカードimport(検査が利用時まで遅延)へ変更して回避し、型解決のモジュール分離を言語側の設計課題に記録した。 + +## 🧪 テスト + +- 機能テスト: スマートポインタ4件+WeakPtr/Atomic、TreeSet/HashSet/TreeMap削除、chars/parse、Arena、Vector強化、fs行読みを追加。 +- パフォーマンステスト: HashSet 10000件の挿入・参照・削除スモーク(修正前の非決定的クラッシュの回帰検知を兼ねる)、Arenaのチャーン耐性。 +- ベンチマーク: `08_treeset_ops`(20000要素・木高検証)、`09_hashset_ops`(50000要素)、`10_arena_vs_alloc`(200000確保の対alloc比較)を追加。 + {% endraw %} diff --git a/docs/tutorials/en/stdlib/collections/sets.md b/docs/tutorials/en/stdlib/collections/sets.md new file mode 100644 index 00000000..02ba90a4 --- /dev/null +++ b/docs/tutorials/en/stdlib/collections/sets.md @@ -0,0 +1,106 @@ +--- +title: TreeSet / HashSet +--- + +[日本語](../../../ja/stdlib/collections/sets.html) + +# std::collections - Sets (TreeSet / HashSet) + +Two set types for collections of unique values (added in v0.17.2). +`TreeSet` is an **ordered set** backed by a self-balancing binary search tree (AVL), and `HashSet` is an **unordered set** backed by a hash table. + +> **Supported backends:** TreeSet runs on JIT / Native / WASM / JS / TS (same slice-arena representation as TreeMap); HashSet is Native (LLVM) only (heap allocation like HashMap). + +--- + +## TreeSet - ordered set + +```cm +import std::collections::treeset::*; +import std::io::println; + +int main() { + TreeSet s(); + + s.insert(30); + s.insert(10); + s.insert(20); + s.insert(10); // duplicates are ignored + + println("len: {s.len()}"); // 3 + println("has 20: {s.contains(20)}"); // true + + // all elements in ascending order + int[] xs = s.values_in_order(); + for (int i = 0; i < xs.len(); i++) { + println("{xs[i]}"); // 10, 20, 30 + } + + s.remove(20); + println("len: {s.len()}"); // 2 + return 0; +} +``` + +| Method | Returns | Description | +|--------|---------|-------------| +| `insert(value)` | `void` | Add a value (duplicates ignored). O(log n) | +| `contains(value)` | `bool` | Membership test. O(log n) | +| `remove(value)` | `void` | Delete a value (rebalances with AVL rotations). O(log n) | +| `len()` / `is_empty()` | `int` / `bool` | Element count / emptiness | +| `clear()` | `void` | Remove all elements | +| `values_in_order()` | `T[]` | All elements in ascending order (in-order traversal) | + +The element type `T` needs ordered comparison (`<` and `==`); structs require `with Ord, Eq`. + +--- + +## HashSet - unordered set + +```cm +import std::collections::hashset::*; +import std::io::println; + +int main() { + HashSet s(); + s.insert(100); + s.insert(200); + s.insert(100); // duplicates are ignored + + println("len: {s.len()}"); // 2 + println("has 200: {s.contains(200)}"); // true + + s.remove(100); + println("has 100: {s.contains(100)}"); // false + return 0; +} +``` + +| Method | Returns | Description | +|--------|---------|-------------| +| `insert(value)` / `remove(value)` | `void` | Add / delete a value. Average O(1) | +| `contains(value)` | `bool` | Membership test. Average O(1) | +| `len()` / `is_empty()` | `int` / `bool` | Element count / emptiness | +| `clear()` | `void` | Remove all elements | + +Element types castable to `int` are recommended (same simple hash as HashMap). + +--- + +## Choosing between them + +| | TreeSet | HashSet | +|---|---------|---------| +| Search / insert / delete | O(log n) | Average O(1) | +| Element order | Kept ascending (`values_in_order`) | None | +| Element constraint | Ordered comparison (`<`, `==`) | Castable to `int` recommended | +| Backends | All | Native only | + +--- + +**See also:** [TreeMap](treemap.html) + +--- + + +← Prev: [TreeMap](treemap.html) | [Contents](../../index.html) | Next: [std::json — JSON parser](../json.html) → diff --git a/docs/tutorials/en/stdlib/collections/treemap.md b/docs/tutorials/en/stdlib/collections/treemap.md index 264699e4..62728282 100644 --- a/docs/tutorials/en/stdlib/collections/treemap.md +++ b/docs/tutorials/en/stdlib/collections/treemap.md @@ -50,6 +50,9 @@ int main() { | `get(key)` | `Option` | Fetch (`Some(value)` if present, `None` otherwise). O(log n) | | `get_or(key, default)` | `V` | Fetch with an explicit default for absent keys. O(log n) | | `contains(key)` | `bool` | Key existence check. O(log n) | +| `remove(key)` | `void` | Delete a key (rebalances with AVL rotations, ignores absent keys, recycles free slots). O(log n) (v0.17.2) | +| `keys_in_order()` | `K[]` | All keys in ascending order (in-order traversal) (v0.17.2) | +| `clear()` | `void` | Remove all entries (v0.17.2) | | `size()` | `int` | Number of entries | --- @@ -67,4 +70,4 @@ The value type `V` is unconstrained. --- -[Contents](../../index.html) | Next: [std::json — JSON parser](../json.html) → +[Contents](../../index.html) | Next: [TreeSet / HashSet - Sets](sets.html) → diff --git a/docs/tutorials/en/stdlib/mem.md b/docs/tutorials/en/stdlib/mem.md index aff257c8..d8003e12 100644 --- a/docs/tutorials/en/stdlib/mem.md +++ b/docs/tutorials/en/stdlib/mem.md @@ -156,7 +156,56 @@ UniquePtr make(int v) { | Construct | `UniquePtr u(value);` | `SharedPtr a(value);` | | Read/Write | `get()` / `set(v)` / `raw()` | `get()` / `set(v)` / `raw()` | | Share | Not allowed (move only) | `clone()` (refcount +1) | -| Others | `release()` / `reset()` / `is_null()` | `use_count()` / `is_null()` | +| Others | `release()` / `reset()` / `is_null()` | `use_count()` / `weak_count()` / `reset()` / `downgrade()` / `is_null()` | + +`move` is an alias (destruction happens at the end of the source variable's scope), so moving into an inner scope does not release earlier. Use `reset()` to release at a deterministic point (v0.17.2). + +### WeakPtr - non-owning reference (v0.17.2) + +Created with `SharedPtr.downgrade()`; does not increment the strong count, so it breaks reference cycles. + +```cm +SharedPtr a(42); +WeakPtr w = a.downgrade(); +SharedPtr up = w.upgrade(); // Some-like: null SharedPtr once all strongs are gone +``` + +| API | Description | +|-----|-------------| +| `SharedPtr.downgrade()` | Create a `WeakPtr` (weak +1) | +| `is_alive()` | Whether any strong reference remains | +| `upgrade()` | `SharedPtr` (strong +1) while alive; a null SharedPtr afterwards | +| `clone()` | Duplicate the WeakPtr itself (weak +1) | + +The control block outlives the payload while weak references remain and is freed with the last WeakPtr. + +### AtomicSharedPtr - thread-safe refcount (v0.17.2) + +A variant whose refcount updates use atomic instructions (native C++ `` runtime); pass `clone()`d instances to other threads. Same API and ownership discipline as `SharedPtr`; guard the payload itself with a Mutex. Import `std::mem::smart::atomic::*` (Native / JIT only). + +--- + +## Arena allocator (std::mem::arena, v0.17.2) + +A bump allocator for many short-lived objects: allocate in bulk, free in bulk. +It skips per-object bookkeeping, so it is faster than per-object `alloc`/`dealloc` (about 4x in the benchmark). + +```cm +import std::mem::arena::*; + +Arena a(); // default 4096-byte chunks (Arena a(65536) to override) +int* x = a.alloc_bytes(4) as int*; +a.reset(); // free every chunk at once (Arena stays reusable) +``` + +| API | Description | +|-----|-------------| +| `Arena a();` / `Arena a(chunk_bytes);` | Construct (data-section chunk size optional) | +| `alloc_bytes(size)` | Bump-allocate, 8-byte aligned (auto-grows; oversized requests get a dedicated chunk) | +| `reset()` | Free every chunk and return to empty | +| `allocated_bytes()` | Total allocated data bytes (statistics) | + +There is no per-object `dealloc` — suited to objects with a common lifetime (parser ASTs, per-request scratch space). --- diff --git a/docs/tutorials/en/stdlib/strings/chars-parse.md b/docs/tutorials/en/stdlib/strings/chars-parse.md new file mode 100644 index 00000000..197b70cc --- /dev/null +++ b/docs/tutorials/en/stdlib/strings/chars-parse.md @@ -0,0 +1,78 @@ +--- +title: Character Classification & Number Parsing +--- + +[日本語](../../../ja/stdlib/strings/chars-parse.html) + +# std::strings::chars / parse - Character Classification & Number Parsing + +Building blocks for lexers and text processing: single-character classification/conversion (`chars`) and string-to-number parsing (`parse`), added in v0.17.2 (the parse functions moved from std::io). + +> **Supported backends:** chars runs everywhere (`digit_value` excluded on SV); parse runs everywhere except SV (Option-returning APIs). + +--- + +## std::strings::chars + +```cm +import std::io::println; +import std::strings::chars::*; + +int main() { + println("{is_digit('7')}"); // true + println("{is_hex_digit('F')}"); // true + println("{is_ident_start('_')}"); // true + println("{to_upper('a')}"); // A + + int v = digit_value('f', 16).unwrap_or(-1); + println("{v}"); // 15 + return 0; +} +``` + +| Function | Returns | Description | +|----------|---------|-------------| +| `is_digit(c)` / `is_alpha(c)` / `is_alnum(c)` | `bool` | Digit / letter / alphanumeric | +| `is_space(c)` | `bool` | Whitespace (space, tab, LF, CR) | +| `is_upper(c)` / `is_lower(c)` | `bool` | Upper / lower case | +| `is_hex_digit(c)` | `bool` | Hex digit (0-9 / a-f / A-F) | +| `is_ident_start(c)` / `is_ident_continue(c)` | `bool` | Identifier head / continuation character | +| `to_upper(c)` / `to_lower(c)` | `char` | ASCII case conversion (others unchanged) | +| `digit_value(c, base)` | `Option` | Digit value in base 2-36 (`None` when out of range; not on SV) | + +--- + +## std::strings::parse + +String-to-number parsing that reports failure via `Option`. +Moved from `std::io` in v0.17.2; existing code importing via `import std::io::*;` keeps working through a re-export. + +```cm +import std::io::println; +import std::strings::parse::*; + +int main() { + int a = parse_int("-123").unwrap_or(0); + int hex = parse_int_radix("ff", 16).unwrap_or(-1); // 255 + bool bad = parse_int("abc").is_none(); // true + println("{a} {hex} {bad}"); + return 0; +} +``` + +| Function | Returns | Description | +|----------|---------|-------------| +| `parse_int(s)` / `parse_long(s)` | `Option` / `Option` | Decimal integer parsing (leading `-` supported, stops at non-digits) | +| `parse_int_radix(s, base)` / `parse_long_radix(s, base)` | `Option` / `Option` | Integer parsing in base 2-36 (v0.17.2) | +| `parse_double(s)` | `Option` | Floating-point parsing | +| `parse_bool(s)` | `Option` | `true/1/yes` → `Some(true)`, `false/0/no` → `Some(false)`, otherwise `None` | + +**Note:** when using `chars` / `parse` from inside library modules, use wildcard imports (`import std::strings::parse::*;`) — selective imports type-check the target bodies eagerly and leak type collisions into programs that redefine `Option`. + +--- + +**See also:** [StringBuilder](builder.html) · [String Length](length.html) + +--- + +[Contents](../../index.html) diff --git a/docs/tutorials/ja/index.md b/docs/tutorials/ja/index.md index eb876a31..5f7a8cc7 100644 --- a/docs/tutorials/ja/index.md +++ b/docs/tutorials/ja/index.md @@ -79,6 +79,9 @@ Cm言語の全機能を段階的に学べる包括的なチュートリアル集 - [Vector](stdlib/collections/vector.html) - 動的配列・Vector\\> - [Queue](stdlib/collections/queue.html) - FIFOキュー - [HashMap](stdlib/collections/hashmap.html) - 連想配列 + - [TreeMap](stdlib/collections/treemap.html) - 順序付きマップ(AVL木) + - [TreeSet / HashSet](stdlib/collections/sets.html) - 集合(順序付き/ハッシュ・v0.17.2) + - [文字分類と数値解析](stdlib/strings/chars-parse.html) - chars/parse(v0.17.2) - [HTTP通信](stdlib/http.html) - HttpClient/HttpServer/HTTPS - [TCP/UDP通信](stdlib/network/tcp.html) - ソケット/DNS/poll - [並行処理](stdlib/concurrency/) - スレッド/Mutex/Channel/Atomic diff --git a/docs/tutorials/ja/stdlib/collections/sets.md b/docs/tutorials/ja/stdlib/collections/sets.md new file mode 100644 index 00000000..c053d660 --- /dev/null +++ b/docs/tutorials/ja/stdlib/collections/sets.md @@ -0,0 +1,116 @@ +--- +title: TreeSet / HashSet +--- + +[English](../../../en/stdlib/collections/sets.html) + +# std::collections - 集合(TreeSet / HashSet) + +重複しない値の集まりを扱う2つの集合型です(v0.17.2で追加)。 +`TreeSet` は自己平衡二分探索木(AVL木)による**順序付き集合**、`HashSet` はハッシュ表による**順序なし集合**です。 + +> **対応バックエンド:** TreeSetはJIT / Native / WASM / JS / TS(TreeMapと同じスライスのアリーナ表現)、HashSetはNative (LLVM) のみ(HashMapと同じヒープ確保) + +--- + +## TreeSet - 順序付き集合 + +```cm +import std::collections::treeset::*; +import std::io::println; + +int main() { + TreeSet s(); + + s.insert(30); + s.insert(10); + s.insert(20); + s.insert(10); // 重複は無視される + + println("len: {s.len()}"); // 3 + println("has 20: {s.contains(20)}"); // true + + // 昇順で全要素を取得 + int[] xs = s.values_in_order(); + for (int i = 0; i < xs.len(); i++) { + println("{xs[i]}"); // 10, 20, 30 + } + + s.remove(20); + println("len: {s.len()}"); // 2 + return 0; +} +``` + +### API一覧 + +| メソッド | 戻り値 | 説明 | +|---------|--------|------| +| `insert(value)` | `void` | 値を追加(重複は無視)。O(log n) | +| `contains(value)` | `bool` | 値が含まれるか。O(log n) | +| `remove(value)` | `void` | 値を削除(AVL回転で平衡を維持)。O(log n) | +| `len()` | `int` | 要素数 | +| `is_empty()` | `bool` | 空か | +| `clear()` | `void` | 全要素を削除 | +| `values_in_order()` | `T[]` | 昇順の全要素(in-order走査) | + +値型 `T` は順序比較(`<` と `==`)が必要です(構造体は `with Ord, Eq`)。 + +--- + +## HashSet - 順序なし集合 + +```cm +import std::collections::hashset::*; +import std::io::println; + +int main() { + HashSet s(); + + s.insert(100); + s.insert(200); + s.insert(100); // 重複は無視される + + println("len: {s.len()}"); // 2 + println("has 200: {s.contains(200)}"); // true + + s.remove(100); + println("has 100: {s.contains(100)}"); // false + return 0; +} +``` + +### API一覧 + +| メソッド | 戻り値 | 説明 | +|---------|--------|------| +| `insert(value)` | `void` | 値を追加(重複は無視)。平均O(1) | +| `contains(value)` | `bool` | 値が含まれるか。平均O(1) | +| `remove(value)` | `void` | 値を削除。平均O(1) | +| `len()` | `int` | 要素数 | +| `is_empty()` | `bool` | 空か | +| `clear()` | `void` | 全要素を削除 | + +値型 `T` は `int` にキャスト可能な型が推奨です(HashMapと同じ簡易ハッシュ)。 + +--- + +## 使い分け + +| | TreeSet | HashSet | +|---|---------|---------| +| 探索・挿入・削除 | O(log n) | 平均O(1) | +| 要素の順序 | 昇順を維持(`values_in_order`) | なし | +| 値の制約 | 順序比較(`<`・`==`) | `int` にキャスト可能な型が推奨 | +| 対応バックエンド | 全バックエンド | Nativeのみ | + +順序付き走査や範囲的な用途はTreeSet、存在判定だけを高速に行いたい場合はHashSetが向いています。 + +--- + +**関連:** [TreeMap](treemap.html) · [HashMap](hashmap.html) · [Vector](vector.html) + +--- + + +← 前: [std::collections::treemap - 順序付きマップ](treemap.html) | [目次](../index.html) | 次: [std::json — JSONパーサ](../json.html) → diff --git a/docs/tutorials/ja/stdlib/collections/treemap.md b/docs/tutorials/ja/stdlib/collections/treemap.md index 503c917b..28d0cd85 100644 --- a/docs/tutorials/ja/stdlib/collections/treemap.md +++ b/docs/tutorials/ja/stdlib/collections/treemap.md @@ -50,6 +50,9 @@ int main() { | `get(key)` | `Option` | 値を取得(存在すれば`Some(値)`、不在なら`None`)。O(log n) | | `get_or(key, default)` | `V` | 値を取得(不在時は`default`を返す非Option版)。O(log n) | | `contains(key)` | `bool` | キーが存在するか。O(log n) | +| `remove(key)` | `void` | キーを削除(AVL回転で平衡を維持・不在キーは無視・空きスロットは再利用)。O(log n)(v0.17.2) | +| `keys_in_order()` | `K[]` | 全キーを昇順で取得(in-order走査)(v0.17.2) | +| `clear()` | `void` | 全要素を削除(v0.17.2) | | `size()` | `int` | 要素数 | --- @@ -73,7 +76,7 @@ int main() { | 探索・挿入 | O(log n) | 平均O(1)(線形探索の衝突解決) | | キーの制約 | 順序比較(`<`・`==`) | `int` にキャスト可能な型が推奨 | | 内部構造 | AVL木(挿入ごとに平衡化) | オープンアドレス法 | -| 容量 | 自動拡張(スライスのアリーナ) | 固定16エントリ(自動リサイズ未実装) | +| 容量 | 自動拡張(スライスのアリーナ) | 負荷率50%で自動2倍拡張 | キー数が多い・キーが文字列・容量を事前に見積もれない場合はTreeMapが向いています。 @@ -84,4 +87,4 @@ int main() { --- -← 前: [std::collections::hashmap - 連想配列](hashmap.html) | [目次](../index.html) | 次: [std::json — JSONパーサ](../json.html) → +← 前: [std::collections::hashmap - 連想配列](hashmap.html) | [目次](../index.html) | 次: [TreeSet / HashSet - 集合](sets.html) → diff --git a/docs/tutorials/ja/stdlib/collections/vector.md b/docs/tutorials/ja/stdlib/collections/vector.md index 7b748592..062e4f20 100644 --- a/docs/tutorials/ja/stdlib/collections/vector.md +++ b/docs/tutorials/ja/stdlib/collections/vector.md @@ -50,7 +50,10 @@ int main() { | `capacity()` | `int` | 確保済み容量 | | `is_empty()` | `bool` | 空かどうか | | `clear()` | `void` | 全要素削除(メモリは保持) | -| `sort()` | `void` | 昇順ソート(プリミティブ型のみ) | +| `reserve(n)` | `void` | 容量をn以上へ事前拡張(縮小はしない)(v0.17.2) | +| `last()` | `T*` | 末尾要素へのポインタ(popせず参照)(v0.17.2) | +| `sort()` | `void` | 昇順ソート(挿入ソート・プリミティブ型のみ) | +| `sortBy(cmp)` | `void` | 比較関数`int*(T, T)`によるソート(v0.17.2) | --- @@ -135,6 +138,18 @@ println("{*v.get(1)}"); // 20 println("{*v.get(2)}"); // 30 ``` +比較関数を渡す `sortBy` で任意の順序にできます(負=aが先・正=bが先。v0.17.2)。 + +```cm +v.sortBy((int a, int b) => b - a); // 降順 +println("{*v.get(0)}"); // 30 + +Vector d(); +d.push(2.5); +d.push(0.5); +d.sortBy((double a, double b) => (a < b) ? -1 : 1); // 昇順 +``` + --- ## メモリ管理 diff --git a/docs/tutorials/ja/stdlib/index.md b/docs/tutorials/ja/stdlib/index.md index 5725050d..4e2c6765 100644 --- a/docs/tutorials/ja/stdlib/index.md +++ b/docs/tutorials/ja/stdlib/index.md @@ -20,7 +20,7 @@ Cm標準ライブラリのモジュール群です。 |-----------|------|------------| | `std::io` | 入出力 (println, input) | [入出力](io.html) | | `std::fs` | ファイルシステム操作 (read_file/write_file・Result API) | [入出力](io.html) | -| `std::mem` | メモリ管理 (alloc, size_of, Allocator) | [メモリ管理](mem.html) | +| `std::mem` | メモリ管理 (alloc, size_of, Allocator, スマートポインタ, Arena) | [メモリ管理](mem.html) | | `std::math` | 数学関数 (sin, sqrt, PI, gcd等) | [数学関数](math.html) | | `std::core` | ユーティリティ (min, max, clamp, 型エイリアス) | [コア](core-utils.html) | | `std::env` / `std::process` | 環境変数・args・サブプロセス (Native/JIT) | [OS連携](os.html) | @@ -70,7 +70,8 @@ Cm標準ライブラリのモジュール群です。 | `std::collections::vector` | `Vector` 動的配列 | [Vector](collections/vector.html) | | `std::collections::queue` | `Queue` FIFO | [Queue](collections/queue.html) | | `std::collections::hashmap` | `HashMap` ハッシュマップ(getはOption返し) | [HashMap](collections/hashmap.html) | -| `std::collections::treemap` | `TreeMap` 順序付きマップ(AVL木・O(log n)) | [TreeMap](collections/treemap.html) | +| `std::collections::treemap` | `TreeMap` 順序付きマップ(AVL木・O(log n)・remove/keys_in_order対応) | [TreeMap](collections/treemap.html) | +| `std::collections::treeset` / `hashset` | `TreeSet` 順序付き集合・`HashSet` ハッシュ集合(v0.17.2) | [TreeSet / HashSet](collections/sets.html) | --- @@ -79,6 +80,7 @@ Cm標準ライブラリのモジュール群です。 | モジュール | 説明 | ドキュメント | |-----------|------|------------| | `std::strings::builder` | `StringBuilder` 可変文字列バッファ(償却O(1)追記) | [StringBuilder](strings/builder.html) | +| `std::strings::chars` / `parse` | 文字分類・変換と数値解析(基数対応・Option返し。v0.17.2) | [文字分類と数値解析](strings/chars-parse.html) | | 文字列組み込み | `len()`(コードポイント数)/ `byte_len()`(バイト数) | [文字列の長さ](strings/length.html) | --- diff --git a/docs/tutorials/ja/stdlib/io.md b/docs/tutorials/ja/stdlib/io.md index 5247ce59..7ec4e835 100644 --- a/docs/tutorials/ja/stdlib/io.md +++ b/docs/tutorials/ja/stdlib/io.md @@ -109,6 +109,7 @@ int main() { | 関数 | 戻り値 | 説明 | |------|--------|------| | `read_file(path)` | `string` | ファイル全体を読み込み(失敗時は空文字列) | +| `read_lines(path)` | `string[]` | 行単位で読み込み(改行区切り・失敗時は空スライス。v0.17.2) | | `write_file(path, content)` | `bool` | 上書き書き込み | | `append_file(path, content)` | `bool` | 追記 | | `exists(path)` | `bool` | 存在確認 | diff --git a/docs/tutorials/ja/stdlib/json.md b/docs/tutorials/ja/stdlib/json.md index 4d4e2654..59fbf1eb 100644 --- a/docs/tutorials/ja/stdlib/json.md +++ b/docs/tutorials/ja/stdlib/json.md @@ -87,4 +87,4 @@ int main() { --- -← 前: [TreeMap](collections/treemap.html) | [目次](index.html) | 次: [標準ライブラリの拡張方法](extending.html) → +← 前: [TreeSet / HashSet - 集合](collections/sets.html) | [目次](index.html) | 次: [標準ライブラリの拡張方法](extending.html) → diff --git a/docs/tutorials/ja/stdlib/mem.md b/docs/tutorials/ja/stdlib/mem.md index 801c4c08..c005eaf3 100644 --- a/docs/tutorials/ja/stdlib/mem.md +++ b/docs/tutorials/ja/stdlib/mem.md @@ -158,7 +158,99 @@ UniquePtr make(int v) { | 構築 | `UniquePtr u(value);` | `SharedPtr a(value);` | | 読み書き | `get()` / `set(v)` / `raw()` | `get()` / `set(v)` / `raw()` | | 共有 | 不可(moveのみ) | `clone()`(参照カウント+1) | -| その他 | `release()` / `reset()` / `is_null()` | `use_count()` / `is_null()` | +| その他 | `release()` / `reset()` / `is_null()` | `use_count()` / `weak_count()` / `reset()` / `downgrade()` / `is_null()` | + +`move` はエイリアス(破棄は移動元スコープの末尾)のため、内側スコープへmoveしても破棄は早まりません。決定的なタイミングで手放したい場合は `reset()` を使ってください(v0.17.2)。 + +### WeakPtr - 弱参照(v0.17.2) + +`SharedPtr.downgrade()` で作る「所有しない参照」です。参照カウントを増やさないため、循環参照の切断に使います。 + +```cm +import std::io::println; +import std::mem::smart::*; + +int main() { + SharedPtr a(42); + WeakPtr w = a.downgrade(); // strongは増えない + println("alive={w.is_alive()}"); // true + + // 生きていればSharedPtrへ昇格(strong+1)、死んでいればnullのSharedPtr + SharedPtr up = w.upgrade(); + println("up={up.get()}"); // 42 + up.reset(); + + a.reset(); // 最後のstrongが消える → ペイロード解放 + println("alive={w.is_alive()}"); // false + SharedPtr dead = w.upgrade(); + println("null={dead.is_null()}"); // true + return 0; +} +``` + +| API | 説明 | +|-----|------| +| `SharedPtr.downgrade()` | `WeakPtr` を作る(weakカウント+1) | +| `is_alive()` | strongが1以上残っているか | +| `upgrade()` | 生存中なら `SharedPtr`(strong+1)、死後はnullのSharedPtr | +| `clone()` | WeakPtr自体の複製(weak+1) | + +制御ブロックはstrong=0でペイロード解放後も、weakが残る限り維持され、最後のWeakPtrの破棄で解放されます。 + +### AtomicSharedPtr - スレッド安全な参照カウント(v0.17.2) + +参照カウントの増減をアトミック命令(ネイティブC++ランタイムの``)で行う版です。`clone()` した各インスタンスを別スレッドへ渡す用途に使います(API・所有規律は`SharedPtr`と同一。ペイロード自体の同期は別途Mutex等で守ってください)。 + +```cm +import std::mem::smart::atomic::*; + +AtomicSharedPtr a(42); +AtomicSharedPtr b = a.clone(); // アトミックにrc+1 +``` + +> **対応バックエンド:** Native / JITのみ(アトミックランタイムがネイティブ実装のため) + +--- + +## Arenaアロケータ(std::mem::arena・v0.17.2) + +短命な大量オブジェクトを「まとめて確保・まとめて解放」するバンプアロケータです。 +個々の解放を追跡しないため、1件ごとの `alloc`/`dealloc` よりも高速です(ベンチマークで約4倍)。 + +```cm +import std::mem::arena::*; +import std::io::println; + +struct Node { + int value; + long tag; +} + +int main() { + Arena a(); // 既定チャンク4096バイト(Arena a(65536)でサイズ指定) + + int* x = a.alloc_bytes(4) as int*; + *x = 42; + + Node* n = a.alloc_bytes(16) as Node*; + n->value = 7; + + println("used={a.allocated_bytes()}"); + + a.reset(); // 全チャンクを一括解放(Arena自体は再利用可能) + return 0; +} +// スコープ終了時はデストラクタが全チャンクを解放する +``` + +| API | 説明 | +|-----|------| +| `Arena a();` / `Arena a(chunk_bytes);` | 構築(チャンクのデータ部サイズを指定可能) | +| `alloc_bytes(size)` | `void*` を返すバンプ確保(8バイト整列。チャンク不足時は自動拡張・チャンク超の大型要求は専用チャンク) | +| `reset()` | 全チャンクを一括解放して空へ戻す | +| `allocated_bytes()` | データ部の総確保バイト(統計) | + +個々の `dealloc` は不要です(というより、できません)。寿命が揃ったオブジェクト群(パーサのAST・リクエスト単位の作業領域など)に向いています。 --- diff --git a/docs/tutorials/ja/stdlib/strings/chars-parse.md b/docs/tutorials/ja/stdlib/strings/chars-parse.md new file mode 100644 index 00000000..34d41070 --- /dev/null +++ b/docs/tutorials/ja/stdlib/strings/chars-parse.md @@ -0,0 +1,97 @@ +--- +title: 文字分類と数値解析 +--- + +[English](../../../en/stdlib/strings/chars-parse.html) + +# std::strings::chars / parse - 文字分類と数値解析 + +字句解析やテキスト処理の基礎部品として、文字1つの分類・変換(`chars`)と文字列から数値への解析(`parse`)を提供します(v0.17.2で追加。parse系はstd::ioから移設)。 + +> **対応バックエンド:** charsは全バックエンド(`digit_value`のみSV除く)、parseはSV以外(`Option`返しAPIのため) + +--- + +## std::strings::chars - 文字分類・変換 + +```cm +import std::io::println; +import std::strings::chars::*; + +int main() { + println("{is_digit('7')}"); // true + println("{is_alpha('a')}"); // true + println("{is_alnum('_')}"); // false + println("{is_space('\t')}"); // true + println("{is_hex_digit('F')}"); // true + + // 識別子の字種判定(字句解析向け) + println("{is_ident_start('_')}"); // true + println("{is_ident_continue('9')}"); // true + + // 大文字・小文字変換(ASCII) + println("{to_upper('a')}"); // A + println("{to_lower('Z')}"); // z + + // 基数付きの数字値(失敗はNone) + int v = digit_value('f', 16).unwrap_or(-1); + println("{v}"); // 15 + return 0; +} +``` + +| 関数 | 戻り値 | 説明 | +|------|--------|------| +| `is_digit(c)` / `is_alpha(c)` / `is_alnum(c)` | `bool` | 数字 / 英字 / 英数字か | +| `is_space(c)` | `bool` | 空白文字か(スペース・タブ・改行・CR) | +| `is_upper(c)` / `is_lower(c)` | `bool` | 大文字 / 小文字か | +| `is_hex_digit(c)` | `bool` | 16進数字か(0-9 / a-f / A-F) | +| `is_ident_start(c)` / `is_ident_continue(c)` | `bool` | 識別子の先頭 / 継続文字か(英字・`_` / 英数字・`_`) | +| `to_upper(c)` / `to_lower(c)` | `char` | ASCII大文字化 / 小文字化(対象外はそのまま) | +| `digit_value(c, base)` | `Option` | 基数base(2〜36)での数字値(範囲外は`None`。SV除く) | + +--- + +## std::strings::parse - 数値解析 + +失敗を `Option` で返す文字列→数値の解析関数です。 +v0.17.2で `std::io` から移設されました(`import std::io::*;` 経由の従来コードは再エクスポートで引き続き動作します)。 + +```cm +import std::io::println; +import std::strings::parse::*; + +int main() { + int a = parse_int("-123").unwrap_or(0); // -123 + long b = parse_long("9000000000").unwrap_or(0 as long); + double c = parse_double("3.14").unwrap_or(0.0); + bool d = parse_bool("yes").unwrap_or(false); // true + + // 基数指定(2〜36。v0.17.2で追加) + int hex = parse_int_radix("ff", 16).unwrap_or(-1); // 255 + int bin = parse_int_radix("1010", 2).unwrap_or(-1); // 10 + long big = parse_long_radix("ffffffff", 16).unwrap_or(-1 as long); + + // 失敗はNone + bool bad = parse_int("abc").is_none(); // true + println("{a} {hex} {bin} {bad}"); + return 0; +} +``` + +| 関数 | 戻り値 | 説明 | +|------|--------|------| +| `parse_int(s)` / `parse_long(s)` | `Option` / `Option` | 10進整数の解析(先頭の`-`対応・数字以外で停止) | +| `parse_int_radix(s, base)` / `parse_long_radix(s, base)` | `Option` / `Option` | 基数base(2〜36)での整数解析(v0.17.2) | +| `parse_double(s)` | `Option` | 小数の解析 | +| `parse_bool(s)` | `Option` | `true/1/yes` → `Some(true)`、`false/0/no` → `Some(false)`、他は`None` | + +**注意:** ライブラリ内部から `chars` / `parse` を利用する場合はワイルドカードimport(`import std::strings::parse::*;`)を使ってください(選択importは本体の即時型検査により、ユーザ定義`Option`を持つプログラムへ型衝突を波及させます)。 + +--- + +**関連:** [StringBuilder](builder.html) · [文字列の長さ](length.html) + +--- + +[目次](../index.html) diff --git a/libs/std/collections/hashset.cm b/libs/std/collections/hashset.cm new file mode 100644 index 00000000..9685a041 --- /dev/null +++ b/libs/std/collections/hashset.cm @@ -0,0 +1,49 @@ +// std::collections::hashset - ハッシュ集合 +// HashMapのキーのみ薄ラッパー。挿入・存在判定・削除をO(1)期待で行う +module std.collections.hashset; + +import std::collections::hashmap::*; + +export struct HashSet { + HashMap map; +} + +export impl HashSet { + // 空の集合を作る + self() { + HashMap m(); + self.map = m; + // dtor持ち構造体の代入は浅いコピーのため、mのdtorがself.map.entriesも解放してしまう(二重解放)。所有権をself.mapへ移した印としてmの保持ポインタを無効化する + m.entries = 0 as Entry*; + } + + // 値を追加する(既存なら何もしない) + void insert(T value) { + self.map.insert(value, true); + } + + // 値が含まれるか + bool contains(T value) { + return self.map.contains(value); + } + + // 値を削除する(存在しなければ何もしない) + void remove(T value) { + self.map.remove(value); + } + + // 要素数 + int len() { + return self.map.len(); + } + + // 空か + bool is_empty() { + return self.map.len() == 0; + } + + // 全要素を削除する + void clear() { + self.map.clear(); + } +} diff --git a/libs/std/collections/mod.cm b/libs/std/collections/mod.cm index d16945da..a86ee747 100644 --- a/libs/std/collections/mod.cm +++ b/libs/std/collections/mod.cm @@ -1,8 +1,10 @@ // std::collections - コレクション型 -// Vector, HashMap, Queue, TreeMap(平衡二分探索木・O(log n)) +// Vector, HashMap, Queue, TreeMap(平衡二分探索木・O(log n)), HashSet, TreeSet module std.collections; export import std.collections.vector; export import std.collections.hashmap; export import std.collections.queue; export import std.collections.treemap; +export import std.collections.hashset; +export import std.collections.treeset; diff --git a/libs/std/collections/treemap.cm b/libs/std/collections/treemap.cm index f5d418a8..db4f5fb6 100644 --- a/libs/std/collections/treemap.cm +++ b/libs/std/collections/treemap.cm @@ -27,7 +27,9 @@ export struct TreeMap { int[] left; // 左の子のindex(-1で無し) int[] right; // 右の子のindex(-1で無し) int[] height; // 部分木の高さ(AVL平衡用) - int count; // 確保済みノード数 + int[] free_slots; // remove済みノードindex(alloc_nodeが再利用する) + int count; // 確保済みノード数(スロット総数) + int live; // 実要素数(remove対応でcountと分離) int root; // 根のindex(-1で空) } @@ -39,7 +41,9 @@ export impl TreeMap { self.left = []; self.right = []; self.height = []; + self.free_slots = []; self.count = 0; + self.live = 0; self.root = -1; } @@ -89,8 +93,18 @@ export impl TreeMap { return y; } - // 新しいノードを確保してindexを返す。スライスへpushするので容量は自動拡張される。 + // 新しいノードを確保してindexを返す。remove済みスロットがあれば再利用し、無ければスライスへpushする(容量は自動拡張)。 int alloc_node(K k, V v) { + self.live = self.live + 1; + if (self.free_slots.len() > 0) { + int idx = self.free_slots.pop(); + self.keys[idx] = k; + self.vals[idx] = v; + self.left[idx] = -1; + self.right[idx] = -1; + self.height[idx] = 1; + return idx; + } int idx = self.count; self.keys.push(k); self.vals.push(v); @@ -101,6 +115,64 @@ export impl TreeMap { return idx; } + // 部分木の最小キーノードindexを返す(remove時の後継探索用)。 + int min_index(int node) { + int cur = node; + while (self.left[cur] >= 0) { + cur = self.left[cur]; + } + return cur; + } + + // 削除後の平衡化(削除は挿入と違いキーでなく子の平衡係数で回転方向を決める)。 + int rebalance_after_remove(int node) { + self.update_height(node); + int b = self.balance_factor(node); + if (b > 1) { + if (self.balance_factor(self.left[node]) >= 0) { + return self.rotate_right(node); + } + self.left[node] = self.rotate_left(self.left[node]); + return self.rotate_right(node); + } + if (b < -1) { + if (self.balance_factor(self.right[node]) <= 0) { + return self.rotate_left(node); + } + self.right[node] = self.rotate_right(self.right[node]); + return self.rotate_left(node); + } + return node; + } + + // node を根とする部分木から k を削除し、平衡化後の新しい根indexを返す(kの存在は呼び出し側で確認済み)。 + int remove_at(int node, K k) { + if (node < 0) { + return -1; + } + if (k < self.keys[node]) { + self.left[node] = self.remove_at(self.left[node], k); + } else if (self.keys[node] < k) { + self.right[node] = self.remove_at(self.right[node], k); + } else { + // 子が1つ以下: そのまま子を持ち上げ、スロットをfree listへ返す + if (self.left[node] < 0 || self.right[node] < 0) { + int child = self.left[node]; + if (child < 0) { + child = self.right[node]; + } + self.free_slots.push(node); + return child; + } + // 子が2つ: 右部分木の最小(後継)のキー/値をコピーし、後継を右部分木から削除する + int succ = self.min_index(self.right[node]); + self.keys[node] = self.keys[succ]; + self.vals[node] = self.vals[succ]; + self.right[node] = self.remove_at(self.right[node], self.keys[node]); + } + return self.rebalance_after_remove(node); + } + // node を根とする部分木へ (k, v) を挿入し、平衡化後の新しい根indexを返す。 int insert_at(int node, K k, V v) { if (node < 0) { @@ -185,8 +257,53 @@ export impl TreeMap { return self.vals[idx]; } + // キーを削除する(存在しなければ何もしない)。O(log n)。 + void remove(K key) { + if (self.find_index(key) < 0) { + return; + } + self.root = self.remove_at(self.root, key); + self.live = self.live - 1; + } + + // 昇順(in-order)の全キーを返す。O(n)。 + // 明示スタックの反復in-order走査(JSバックエンドがスライスポインタ引数(K[]*)の再帰渡しに未対応のため、再帰ヘルパーを使わない) + K[] keys_in_order() { + K[] out = []; + int[] stack = []; + int cur = self.root; + while (cur >= 0 || stack.len() > 0) { + while (cur >= 0) { + stack.push(cur); + cur = self.left[cur]; + } + cur = stack.pop(); + out.push(self.keys[cur]); + cur = self.right[cur]; + } + return out; + } + + // 全要素を削除して空にする。 + void clear() { + self.keys = []; + self.vals = []; + self.left = []; + self.right = []; + self.height = []; + self.free_slots = []; + self.count = 0; + self.live = 0; + self.root = -1; + } + + // 木の高さ(平衡性の検証用。AVLなら高さ <= 1.44*log2(n)+2 に収まる)。 + int tree_height() { + return self.node_height(self.root); + } + // 保持している要素数。 int size() { - return self.count; + return self.live; } } diff --git a/libs/std/collections/treeset.cm b/libs/std/collections/treeset.cm new file mode 100644 index 00000000..aee3251a --- /dev/null +++ b/libs/std/collections/treeset.cm @@ -0,0 +1,59 @@ +// std::collections::treeset - 順序付き集合(平衡二分探索木) +// TreeMap(AVL木)のキーのみ薄ラッパー。挿入・存在判定・削除をO(log n)で行い、 +// values_in_order()で昇順の全要素を取り出せる(コンパイラのシンボル集合・訪問済み集合向け)。 +// 要素型Tは順序比較(< と ==)が必要(int/string等は組み込み、構造体は with Ord, Eq を付ける) +module std.collections.treeset; + +import std::collections::treemap::*; + +export struct TreeSet { + TreeMap map; +} + +export impl TreeSet { + // 空の集合を作る + self() { + TreeMap m(); + self.map = m; + } + + // 値を追加する(既存なら何もしない)。O(log n) + void insert(T value) { + self.map.put(value, true); + } + + // 値が含まれるか。O(log n) + bool contains(T value) { + return self.map.contains(value); + } + + // 値を削除する(存在しなければ何もしない)。O(log n) + void remove(T value) { + self.map.remove(value); + } + + // 要素数 + int len() { + return self.map.size(); + } + + // 空か + bool is_empty() { + return self.map.size() == 0; + } + + // 全要素を削除する + void clear() { + self.map.clear(); + } + + // 昇順(in-order)の全要素を返す。O(n) + T[] values_in_order() { + return self.map.keys_in_order(); + } + + // 内部AVL木の高さ(平衡性の検証用) + int tree_height() { + return self.map.tree_height(); + } +} diff --git a/libs/std/collections/vector.cm b/libs/std/collections/vector.cm index f9901cda..e00c8dd5 100644 --- a/libs/std/collections/vector.cm +++ b/libs/std/collections/vector.cm @@ -89,23 +89,66 @@ export impl Vector { return self.size == 0; } + // 容量をn要素ぶん先行確保する(既に足りていれば何もしない。push連打の再確保を避ける) + void reserve(int n) { + if (n <= self.cap) { + return; + } + const long elem_size = __sizeof__(T) as long; + const long alloc_size = (n as long) * elem_size; + void* raw_ptr = alloc(alloc_size as int); + T* new_data = raw_ptr as T*; + if (self.size > 0) { + void* src = self.data as void*; + void* dst = new_data as void*; + const long copy_size = (self.size as long) * elem_size; + memcpy(dst, src, copy_size as int); + } + void* old_ptr = self.data as void*; + void* null_ptr = 0 as void*; + if (old_ptr != null_ptr) { + dealloc(old_ptr); + } + self.data = new_data; + self.cap = n; + } + + // 末尾要素への参照を返す(スタック用途のtop相当。空のときは呼ばないこと) + T* last() { + return &self.data[self.size - 1]; + } + // クリア(データは保持、サイズのみリセット) void clear() { self.size = 0; } - // ソート(昇順、バブルソート) + // ソート(昇順、挿入ソート) // プリミティブ型(int, long, double等)で動作 void sort() { const int n = self.size; - for (int i = 0; i < n - 1; i++) { - for (int j = 0; j < n - i - 1; j++) { - if (self.data[j] > self.data[j + 1]) { - const T temp = self.data[j]; - self.data[j] = self.data[j + 1]; - self.data[j + 1] = temp; - } + for (int i = 1; i < n; i++) { + const T key = self.data[i]; + int j = i - 1; + while (j >= 0 && self.data[j] > key) { + self.data[j + 1] = self.data[j]; + j = j - 1; + } + self.data[j + 1] = key; + } + } + + // 比較関数によるソート(挿入ソート。cmp(a,b)<0でaがbより前) + void sortBy(int*(T, T) cmp) { + const int n = self.size; + for (int i = 1; i < n; i++) { + const T key = self.data[i]; + int j = i - 1; + while (j >= 0 && cmp(self.data[j], key) > 0) { + self.data[j + 1] = self.data[j]; + j = j - 1; } + self.data[j + 1] = key; } } diff --git a/libs/std/fs/mod.cm b/libs/std/fs/mod.cm index 5794a9a6..150cb781 100644 --- a/libs/std/fs/mod.cm +++ b/libs/std/fs/mod.cm @@ -3,6 +3,8 @@ // JS/WASMバックエンドは未対応(バックエンド対応マトリクス参照) module std.fs; +import std::strings::split::{lines}; + use libc { string cm_file_read_all(string path); bool cm_file_write_all(string path, string content); @@ -127,6 +129,12 @@ export Result write_all(string path, string content) { return Result::Err("failed to write: " + path); } +/// ファイルを読み込んで行の配列を返す(改行コードはLF/CRLF両対応。ファイル全体を読むため大ファイルは注意) +export string[] read_lines(string path) { + string content = read_file(path); + return lines(content); +} + /// 文字列をファイル末尾へ追記する export Result append_all(string path, string content) { if (cm_file_append(path, content)) { diff --git a/libs/std/io/console/input.cm b/libs/std/io/console/input.cm index 14331877..7cbb8d92 100644 --- a/libs/std/io/console/input.cm +++ b/libs/std/io/console/input.cm @@ -90,161 +90,12 @@ export string input_string() { } // ============================================================ -// 文字列パース関数(純粋Cm実装) +// 文字列パース関数はstd::strings::parseへ移設した(v0.17.2)。 +// 既存コードとの互換のため再エクスポートを維持する(移設先は全関数#[target(!sv)]のためstd::ioファサード経由でSVプログラムへ混入しない)。 +// export importは自モジュール内の呼び出し解決には効かないため、input_*向けの内部importも併記する。 +// 内部importは選択import(::{名前})でなくワイルドカード必須: 選択importは対象関数本体を即時型検査するため、ユーザ定義Optionを持つprintlnのみのプログラムにまでプレリュードOptionとのフラット名衝突を波及させる(ワイルドカードは利用時まで遅延) // ============================================================ -/// 文字列を整数に変換(M17: 失敗を0で握り潰さずOptionで返す。 -/// 空文字列・符号のみ・非数字先頭はNone、先頭の数値部分をパースしてSome) -export Option parse_int(string s) { - int result = 0; - bool negative = false; - int i = 0; - int len = s.len(); - - if (len == 0) { - return Option::None; - } - - // 符号チェック - if (s[0] == '-') { - negative = true; - i = 1; - } else if (s[0] == '+') { - i = 1; - } - - // 先頭が数字でなければ失敗 - if (i >= len || (s[i] as int) < 48 || (s[i] as int) > 57) { - return Option::None; - } - - // 数字を処理 - while (i < len) { - int c = s[i] as int; - if (c >= 48 && c <= 57) { // '0' = 48, '9' = 57 - result = result * 10 + (c - 48); - } else { - // 非数字文字で終了 - break; - } - i = i + 1; - } - - if (negative) { - result = -result; - } - - return Option::Some(result); -} - -/// 文字列をboolに変換("true"/"1"/"yes"はtrue、"false"/"0"/"no"はfalse、それ以外はNone) -export Option parse_bool(string s) { - if (s == "true" || s == "1" || s == "yes") { - return Option::Some(true); - } - if (s == "false" || s == "0" || s == "no") { - return Option::Some(false); - } - return Option::None; -} - -/// 文字列をlongに変換(M17: 失敗はNone) -export Option parse_long(string s) { - long result = 0; - bool negative = false; - int i = 0; - int len = s.len(); +import std::strings::parse::*; - if (len == 0) { - return Option::None; - } - - // 符号チェック - if (s[0] == '-') { - negative = true; - i = 1; - } else if (s[0] == '+') { - i = 1; - } - - // 先頭が数字でなければ失敗 - if (i >= len || (s[i] as int) < 48 || (s[i] as int) > 57) { - return Option::None; - } - - // 数字を処理 - while (i < len) { - int c = s[i] as int; - if (c >= 48 && c <= 57) { - result = result * 10 + (c - 48); - } else { - break; - } - i = i + 1; - } - - if (negative) { - result = -result; - } - - return Option::Some(result); -} - -/// 文字列をdoubleに変換(簡易実装。M17: 失敗はNone) -export Option parse_double(string s) { - // 整数部と小数部を分離 - double result = 0.0; - bool negative = false; - int i = 0; - int len = s.len(); - - if (len == 0) { - return Option::None; - } - - // 符号チェック - if (s[0] == '-') { - negative = true; - i = 1; - } else if (s[0] == '+') { - i = 1; - } - - // 先頭が数字か小数点でなければ失敗 - if (i >= len || (((s[i] as int) < 48 || (s[i] as int) > 57) && (s[i] as int) != 46)) { - return Option::None; - } - - // 整数部 - while (i < len) { - int c = s[i] as int; - if (c >= 48 && c <= 57) { - result = result * 10.0 + (c - 48) as double; - } else if (c == 46) { // '.' - i = i + 1; - break; - } else { - break; - } - i = i + 1; - } - - // 小数部 - double fraction = 0.1; - while (i < len) { - int c = s[i] as int; - if (c >= 48 && c <= 57) { - result = result + ((c - 48) as double) * fraction; - fraction = fraction * 0.1; - } else { - break; - } - i = i + 1; - } - - if (negative) { - result = -result; - } - - return Option::Some(result); -} +export import std.strings.parse::{parse_int, parse_long, parse_double, parse_bool}; diff --git a/libs/std/mem/arena.cm b/libs/std/mem/arena.cm new file mode 100644 index 00000000..9715740b --- /dev/null +++ b/libs/std/mem/arena.cm @@ -0,0 +1,114 @@ +// std/mem/arena.cm - Arenaアロケータ(バンプアロケータ+チャンク連結) +// フェーズ単位で大量の小オブジェクトを確保し、個別解放せずreset()/デストラクタで一括解放する +// (AST構築などコンパイラ実装の定石。個別freeが不要なためUniquePtr等のmove規律も不要になる)。 +// +// チャンク構造: 先頭にメタデータ(next/used/cap の3ワード)を置き、続くデータ部をバンプ確保する。 +// [next: long][used: long][cap: long][data ...] +// 要求サイズがチャンクの既定サイズを超える場合は要求サイズちょうどの専用チャンクを作る。 +// 確保は8バイト整列。対応ターゲットはnative系(JS/TSはポインタ禁止のため対象外) +module std.mem.arena; + +import std::mem::{alloc, dealloc}; + +// チャンクメタデータのワード数(next/used/cap)とバイト数 +const int ARENA_META_WORDS = 3; +const long ARENA_META_BYTES = 24; + +export struct Arena { + void* head; // 現在のチャンク(未確保ならnull) + long chunk_size; // データ部の既定サイズ(バイト) + long total; // データ部の総確保バイト(統計用) +} + +export impl Arena { + // 既定チャンクサイズ4KiBで作る + self() { + void* null_ptr = 0 as void*; + self.head = null_ptr; + self.chunk_size = 4096; + self.total = 0; + } + + // チャンクサイズを指定して作る(1KiB未満は1KiBへ切り上げ) + overload self(int chunk_bytes) { + void* null_ptr = 0 as void*; + self.head = null_ptr; + long size = chunk_bytes as long; + if (size < 1024) { + size = 1024; + } + self.chunk_size = size; + self.total = 0; + } + + // 新しいチャンクを確保して先頭へ連結する(data_bytesはデータ部のサイズ) + void grow(long data_bytes) { + void* raw = alloc((ARENA_META_BYTES + data_bytes) as int); + long* meta = raw as long*; + meta[0] = self.head as long; // next + meta[1] = 0; // used + meta[2] = data_bytes; // cap + self.head = raw; + } + + // sizeバイトを確保して先頭ポインタを返す(8バイト整列・ゼロ初期化はしない) + void* alloc_bytes(int size) { + // 8バイト整列へ切り上げ + long need = ((size as long) + 7) / 8 * 8; + void* null_ptr = 0 as void*; + if (self.head == null_ptr) { + long data = self.chunk_size; + if (need > data) { + data = need; + } + self.grow(data); + } + long* meta = self.head as long*; + if (meta[1] + need > meta[2]) { + long data = self.chunk_size; + if (need > data) { + data = need; + } + self.grow(data); + meta = self.head as long*; + } + long base = self.head as long; + void* p = (base + ARENA_META_BYTES + meta[1]) as void*; + meta[1] = meta[1] + need; + self.total = self.total + need; + return p; + } + + // 全チャンクを解放して空へ戻す(確保済みポインタはすべて無効になる) + void reset() { + void* null_ptr = 0 as void*; + void* cur = self.head; + while (cur != null_ptr) { + long* meta = cur as long*; + void* next = meta[0] as void*; + dealloc(cur); + cur = next; + } + self.head = null_ptr; + self.total = 0; + } + + // データ部の総確保バイト(統計用) + long allocated_bytes() { + return self.total; + } + + // デストラクタ: 全チャンクを一括解放する + ~self() { + void* null_ptr = 0 as void*; + void* cur = self.head; + while (cur != null_ptr) { + long* meta = cur as long*; + void* next = meta[0] as void*; + dealloc(cur); + cur = next; + } + self.head = null_ptr; + self.total = 0; + } +} diff --git a/libs/std/mem/smart.cm b/libs/std/mem/smart.cm deleted file mode 100644 index 287adac8..00000000 --- a/libs/std/mem/smart.cm +++ /dev/null @@ -1,153 +0,0 @@ -// std/mem/smart.cm - スマートポインタ(UniquePtr / SharedPtr) -// ヒープ確保した値の解放をRAII(スコープ末尾のデストラクタ)で自動化する。 -// -// 所有規律(重要): -// - 所有権の移動は必ず move で行う(UniquePtr b = move a;)。 -// 暗黙コピー(b = a;)は浅いコピーで両方のデストラクタが走り二重解放になる。 -// - SharedPtrの共有は必ず clone() で行う(参照カウントを増やす唯一の手段)。 -// - 所有型を関数から返すときは必ず return move local; と書く。 -// -// 対応ターゲット: native系(jit/native/wasm/uefi/baremetal)。 -// JS/TSはポインタ操作が禁止(GCのため不要)、SVは対象外。 -module std.mem.smart; - -import std::mem::{alloc, dealloc}; - -// ============================================================= -// UniquePtr - 単独所有スマートポインタ(移動のみ・共有不可) -// ============================================================= - -export struct UniquePtr { - T* ptr; -} - -export impl UniquePtr { - // 値をヒープへ移して所有する - self(T value) { - void* raw = alloc(__sizeof__(T) as int); - self.ptr = raw as T*; - *self.ptr = value; - } - - // 中身の読み出し(コピーを返す) - T get() { - return *self.ptr; - } - - // 中身の書き換え - void set(T value) { - *self.ptr = value; - } - - // 生ポインタの借用ビュー(所有権は移動しない。UniquePtrより長生きさせないこと) - T* raw() { - return self.ptr; - } - - // 無効(null)状態か(移動済み・release/reset済み・宣言のみのゼロ初期化状態) - bool is_null() { - void* p = self.ptr as void*; - void* null_ptr = 0 as void*; - return p == null_ptr; - } - - // 所有権を放棄して生ポインタを返す(以後デストラクタは何もしない。解放は呼び出し側の責任) - T* release() { - T* p = self.ptr; - void* null_ptr = 0 as void*; - self.ptr = null_ptr as T*; - return p; - } - - // いま所有している値を即時解放してnull状態にする - void reset() { - void* p = self.ptr as void*; - void* null_ptr = 0 as void*; - if (p != null_ptr) { - dealloc(p); - self.ptr = null_ptr as T*; - } - } - - // デストラクタ: 所有中なら解放する(解放処理はメソッドに依存せずインラインで行う) - ~self() { - void* p = self.ptr as void*; - void* null_ptr = 0 as void*; - if (p != null_ptr) { - dealloc(p); - self.ptr = null_ptr as T*; - } - } -} - -// ============================================================= -// SharedPtr - 参照カウント共有スマートポインタ(共有は明示clone) -// ============================================================= - -export struct SharedPtr { - T* ptr; - int* count; -} - -export impl SharedPtr { - // 値をヒープへ移して所有する(参照カウント=1) - self(T value) { - void* raw = alloc(__sizeof__(T) as int); - self.ptr = raw as T*; - *self.ptr = value; - void* craw = alloc(4); - self.count = craw as int*; - *self.count = 1; - } - - // 共有: 参照カウントを増やした複製を返す(共有はこのメソッドのみで行うこと) - SharedPtr clone() { - *self.count = *self.count + 1; - SharedPtr copy; - copy.ptr = self.ptr; - copy.count = self.count; - return move copy; - } - - // 中身の読み出し(コピーを返す) - T get() { - return *self.ptr; - } - - // 中身の書き換え(全共有者から見える) - void set(T value) { - *self.ptr = value; - } - - // 生ポインタの借用ビュー(所有権は移動しない) - T* raw() { - return self.ptr; - } - - // 現在の参照カウント - int use_count() { - return *self.count; - } - - // 無効(null)状態か(移動済み・宣言のみのゼロ初期化状態) - bool is_null() { - void* p = self.ptr as void*; - void* null_ptr = 0 as void*; - return p == null_ptr; - } - - // デストラクタ: 参照カウントを減らし、最後の所有者ならペイロードとカウントを解放する - ~self() { - void* p = self.ptr as void*; - void* null_ptr = 0 as void*; - if (p != null_ptr) { - *self.count = *self.count - 1; - if (*self.count == 0) { - dealloc(p); - dealloc(self.count as void*); - } - self.ptr = null_ptr as T*; - self.count = null_ptr as int*; - } - } -} diff --git a/libs/std/mem/smart/atomic.cm b/libs/std/mem/smart/atomic.cm new file mode 100644 index 00000000..5c93c398 --- /dev/null +++ b/libs/std/mem/smart/atomic.cm @@ -0,0 +1,85 @@ +// std/mem/smart/atomic.cm - Atomic参照カウント版SharedPtr +// 制御ブロックの増減をアトミック命令(C++ backing)で行い、 +// スレッド間で共有できる参照カウントを提供する(clone済みの各インスタンスを別スレッドへ渡す用途)。 +// API・所有規律は単一スレッド版SharedPtrと同一(共有はclone・移動はmove・返却はreturn move)。 +// 対応ターゲット: native(jit/native。アトミックランタイムはネイティブC++実装のためwasm/js/svは対象外) +module std.mem.smart.atomic; + +import std::mem::{alloc, dealloc}; + +// アトミック操作ランタイム(native.syncと同じC++ backing) +extern "C" int cm_atomic_load_i32(int* ptr); +extern "C" int cm_atomic_fetch_add_i32(int* ptr, int value); +extern "C" int cm_atomic_fetch_sub_i32(int* ptr, int value); + +// 制御ブロック(strong/weakをアトミックに増減する) +export struct AtomicSharedCount { + int strong; + int weak; +} + +export struct AtomicSharedPtr { + T* ptr; + AtomicSharedCount* count; +} + +export impl AtomicSharedPtr { + // 値をヒープへ移して所有する(strong=1, weak=0) + self(T value) { + void* raw = alloc(__sizeof__(T) as int); + self.ptr = raw as T*; + *self.ptr = value; + void* craw = alloc(8); + self.count = craw as AtomicSharedCount*; + self.count->strong = 1; + self.count->weak = 0; + } + + // 共有: 参照カウントをアトミックに増やした複製を返す + AtomicSharedPtr clone() { + cm_atomic_fetch_add_i32(&self.count->strong, 1); + AtomicSharedPtr copy; + copy.ptr = self.ptr; + copy.count = self.count; + return move copy; + } + + // 中身の読み出し(コピーを返す。ペイロード自体の同期は別途Mutexなどで守ること) + T get() { + return *self.ptr; + } + + // 中身の書き換え(ペイロード自体の同期は別途Mutexなどで守ること) + void set(T value) { + *self.ptr = value; + } + + // 現在の強参照カウント(アトミックload) + int use_count() { + return cm_atomic_load_i32(&self.count->strong); + } + + // 無効(null)状態か + bool is_null() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + return p == null_ptr; + } + + // デストラクタ: strongをアトミックに減らし、最後の所有者ならペイロードと制御ブロックを解放する + ~self() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + const int prev = cm_atomic_fetch_sub_i32(&self.count->strong, 1); + if (prev == 1) { + dealloc(p); + if (cm_atomic_load_i32(&self.count->weak) == 0) { + dealloc(self.count as void*); + } + } + self.ptr = null_ptr as T*; + self.count = null_ptr as AtomicSharedCount*; + } + } +} diff --git a/libs/std/mem/smart/mod.cm b/libs/std/mem/smart/mod.cm new file mode 100644 index 00000000..3c97b93f --- /dev/null +++ b/libs/std/mem/smart/mod.cm @@ -0,0 +1,256 @@ +// std/mem/smart.cm - スマートポインタ(UniquePtr / SharedPtr) +// ヒープ確保した値の解放をRAII(スコープ末尾のデストラクタ)で自動化する。 +// +// 所有規律(重要): +// - 所有権の移動は必ず move で行う(UniquePtr b = move a;)。 +// 暗黙コピー(b = a;)は浅いコピーで両方のデストラクタが走り二重解放になる。 +// - SharedPtrの共有は必ず clone() で行う(参照カウントを増やす唯一の手段)。 +// - 所有型を関数から返すときは必ず return move local; と書く。 +// - moveはエイリアス(真のゼロコスト移動)であり、デストラクタは移動元変数のスコープ末尾で1回だけ走る。 +// 内側スコープへmoveしても破棄は早まらない。決定的に手放すにはreset()を使う。 +// +// 対応ターゲット: native系(jit/native/wasm/uefi/baremetal)。 +// JS/TSはポインタ操作が禁止(GCのため不要)、SVは対象外。 +module std.mem.smart; + +import std::mem::{alloc, dealloc}; + +// ============================================================= +// UniquePtr - 単独所有スマートポインタ(移動のみ・共有不可) +// ============================================================= + +export struct UniquePtr { + T* ptr; +} + +export impl UniquePtr { + // 値をヒープへ移して所有する + self(T value) { + void* raw = alloc(__sizeof__(T) as int); + self.ptr = raw as T*; + *self.ptr = value; + } + + // 中身の読み出し(コピーを返す) + T get() { + return *self.ptr; + } + + // 中身の書き換え + void set(T value) { + *self.ptr = value; + } + + // 生ポインタの借用ビュー(所有権は移動しない。UniquePtrより長生きさせないこと) + T* raw() { + return self.ptr; + } + + // 無効(null)状態か(移動済み・release/reset済み・宣言のみのゼロ初期化状態) + bool is_null() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + return p == null_ptr; + } + + // 所有権を放棄して生ポインタを返す(以後デストラクタは何もしない。解放は呼び出し側の責任) + T* release() { + T* p = self.ptr; + void* null_ptr = 0 as void*; + self.ptr = null_ptr as T*; + return p; + } + + // いま所有している値を即時解放してnull状態にする + void reset() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + dealloc(p); + self.ptr = null_ptr as T*; + } + } + + // デストラクタ: 所有中なら解放する(解放処理はメソッドに依存せずインラインで行う) + ~self() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + dealloc(p); + self.ptr = null_ptr as T*; + } + } +} + +// ============================================================= +// SharedPtr - 参照カウント共有スマートポインタ(共有は明示clone) +// ============================================================= + +// 制御ブロック: strong(SharedPtrの数)とweak(WeakPtrの数)を保持する。 +// ペイロードはstrong==0で解放、制御ブロック自体はstrong==0かつweak==0で解放する +export struct SharedCount { + int strong; + int weak; +} + +export struct SharedPtr { + T* ptr; + SharedCount* count; +} + +export impl SharedPtr { + // 値をヒープへ移して所有する(strong=1, weak=0) + self(T value) { + void* raw = alloc(__sizeof__(T) as int); + self.ptr = raw as T*; + *self.ptr = value; + void* craw = alloc(8); + self.count = craw as SharedCount*; + self.count->strong = 1; + self.count->weak = 0; + } + + // 共有: 参照カウントを増やした複製を返す(共有はこのメソッドのみで行うこと) + SharedPtr clone() { + self.count->strong = self.count->strong + 1; + SharedPtr copy; + copy.ptr = self.ptr; + copy.count = self.count; + return move copy; + } + + // 弱参照を作る(strongは増やさない。循環参照の切断用) + WeakPtr downgrade() { + self.count->weak = self.count->weak + 1; + WeakPtr w; + w.ptr = self.ptr; + w.count = self.count; + return move w; + } + + // 中身の読み出し(コピーを返す) + T get() { + return *self.ptr; + } + + // 中身の書き換え(全共有者から見える) + void set(T value) { + *self.ptr = value; + } + + // 生ポインタの借用ビュー(所有権は移動しない) + T* raw() { + return self.ptr; + } + + // 現在の強参照カウント + int use_count() { + return self.count->strong; + } + + // 現在の弱参照カウント + int weak_count() { + return self.count->weak; + } + + // 無効(null)状態か(移動済み・reset済み・宣言のみのゼロ初期化状態) + bool is_null() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + return p == null_ptr; + } + + // この強参照を即時手放してnull状態にする(最後の所有者ならペイロードを解放する)。 + // moveはエイリアス(破棄は移動元スコープ末尾)のため、決定的なタイミングで手放したい場合はresetを使う + void reset() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + self.count->strong = self.count->strong - 1; + if (self.count->strong == 0) { + dealloc(p); + if (self.count->weak == 0) { + dealloc(self.count as void*); + } + } + self.ptr = null_ptr as T*; + self.count = null_ptr as SharedCount*; + } + } + + // デストラクタ: strongを減らし、0でペイロード解放。weakも0なら制御ブロックも解放する + ~self() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + self.count->strong = self.count->strong - 1; + if (self.count->strong == 0) { + dealloc(p); + if (self.count->weak == 0) { + dealloc(self.count as void*); + } + } + self.ptr = null_ptr as T*; + self.count = null_ptr as SharedCount*; + } + } +} + +// ============================================================= +// WeakPtr - 弱参照(所有しない参照。循環参照の切断用) +// ============================================================= + +export struct WeakPtr { + T* ptr; + SharedCount* count; +} + +export impl WeakPtr { + // 対象がまだ生きているか(strong > 0) + bool is_alive() { + void* c = self.count as void*; + void* null_ptr = 0 as void*; + if (c == null_ptr) { + return false; + } + return self.count->strong > 0; + } + + // 強参照へ昇格する。対象が解放済みならnull状態のSharedPtrを返す(is_null()で判定) + SharedPtr upgrade() { + SharedPtr result; + if (self.is_alive()) { + self.count->strong = self.count->strong + 1; + result.ptr = self.ptr; + result.count = self.count; + } + return move result; + } + + // 複製(weakを増やす) + WeakPtr clone() { + void* c = self.count as void*; + void* null_ptr = 0 as void*; + WeakPtr w; + if (c != null_ptr) { + self.count->weak = self.count->weak + 1; + w.ptr = self.ptr; + w.count = self.count; + } + return move w; + } + + // デストラクタ: weakを減らし、strongもweakも0なら制御ブロックを解放する + ~self() { + void* c = self.count as void*; + void* null_ptr = 0 as void*; + if (c != null_ptr) { + self.count->weak = self.count->weak - 1; + if (self.count->strong == 0 && self.count->weak == 0) { + dealloc(self.count as void*); + } + self.ptr = null_ptr as T*; + self.count = null_ptr as SharedCount*; + } + } +} diff --git a/libs/std/strings/chars.cm b/libs/std/strings/chars.cm new file mode 100644 index 00000000..eeeb2cfa --- /dev/null +++ b/libs/std/strings/chars.cm @@ -0,0 +1,95 @@ +// std/strings/chars.cm - 文字分類・変換(ASCII範囲) +// 字句解析の基盤となるC標準のctype相当。対象はASCII範囲で、非ASCIIはすべてfalse/恒等を返す +// (識別子・数値リテラルはASCIIで十分。コードポイント単位の処理はstringのcodepoint_atを使う) +module std.strings.chars; + +// 10進数字('0'-'9')か +export bool is_digit(char c) { + const int v = c as int; + return v >= 48 && v <= 57; +} + +// 英字('A'-'Z' / 'a'-'z')か +export bool is_alpha(char c) { + const int v = c as int; + return (v >= 65 && v <= 90) || (v >= 97 && v <= 122); +} + +// 英数字か +export bool is_alnum(char c) { + return is_alpha(c) || is_digit(c); +} + +// 空白(スペース・タブ・改行・復帰・改ページ・垂直タブ)か +export bool is_space(char c) { + const int v = c as int; + return v == 32 || (v >= 9 && v <= 13); +} + +// 大文字('A'-'Z')か +export bool is_upper(char c) { + const int v = c as int; + return v >= 65 && v <= 90; +} + +// 小文字('a'-'z')か +export bool is_lower(char c) { + const int v = c as int; + return v >= 97 && v <= 122; +} + +// 16進数字('0'-'9' / 'A'-'F' / 'a'-'f')か +export bool is_hex_digit(char c) { + const int v = c as int; + return (v >= 48 && v <= 57) || (v >= 65 && v <= 70) || (v >= 97 && v <= 102); +} + +// 識別子の先頭文字(英字または'_')か +export bool is_ident_start(char c) { + return is_alpha(c) || (c as int) == 95; +} + +// 識別子の継続文字(英数字または'_')か +export bool is_ident_continue(char c) { + return is_alnum(c) || (c as int) == 95; +} + +// 大文字化('a'-'z'のみ変換、それ以外は恒等) +export char to_upper(char c) { + const int v = c as int; + if (v >= 97 && v <= 122) { + return (v - 32) as char; + } + return c; +} + +// 小文字化('A'-'Z'のみ変換、それ以外は恒等) +export char to_lower(char c) { + const int v = c as int; + if (v >= 65 && v <= 90) { + return (v + 32) as char; + } + return c; +} + +// 基数base(2〜36)における数字の値を返す('0'-'9'→0-9、'a'/'A'-'z'/'Z'→10-35。基数外はNone)。 +// SVターゲットはOption(タグ付きユニオン)が非合成のため除外する +#[target(!sv)] +export Option digit_value(char c, int base) { + if (base < 2 || base > 36) { + return Option::None; + } + const int v = c as int; + int d = -1; + if (v >= 48 && v <= 57) { + d = v - 48; + } else if (v >= 65 && v <= 90) { + d = v - 55; + } else if (v >= 97 && v <= 122) { + d = v - 87; + } + if (d < 0 || d >= base) { + return Option::None; + } + return Option::Some(d); +} diff --git a/libs/std/strings/mod.cm b/libs/std/strings/mod.cm index 659ecd40..1d1aa24f 100644 --- a/libs/std/strings/mod.cm +++ b/libs/std/strings/mod.cm @@ -1,7 +1,10 @@ // std::strings - 文字列ユーティリティ -// StringBuilder(可変文字列バッファ・償却O(1)追記)と分割(split/lines) +// StringBuilder(可変文字列バッファ・償却O(1)追記)・分割(split/lines)・文字分類(chars)・数値解析(parse) +// chars/parseはOption返しAPIを含むため、他モジュールから内部利用する際は選択import(::{名前})でなくワイルドカードimportにすること(選択importは対象関数本体を即時型検査し、ユーザ定義Optionとプレリュード Option のフラット名衝突を無関係なプログラムへ波及させる。export importとワイルドカードは利用時まで遅延) module std.strings; export import std.strings.builder; export import std.strings.split; export import std.strings.from_bytes; +export import std.strings.chars; +export import std.strings.parse; diff --git a/libs/std/strings/parse.cm b/libs/std/strings/parse.cm new file mode 100644 index 00000000..117fd4e9 --- /dev/null +++ b/libs/std/strings/parse.cm @@ -0,0 +1,247 @@ +// std/strings/parse.cm - 文字列→数値/真偽値の解析 +// std::io::console::inputに置かれていたparse系を文字列モジュールへ集約し、基数指定版を追加した +// (std::io側は互換のため再エクスポートを維持する)。 +// 方針: 空文字列・符号のみ・非数字先頭はNone、先頭の数値部分をパースしてSome(M17のOption契約を踏襲)。 +// SVターゲットはOption(タグ付きユニオン)が非合成のため全関数を#[target(!sv)]で除外する +module std.strings.parse; + +import std::strings::chars::{digit_value}; + +/// 文字列を整数に変換(失敗はNone。先頭の数値部分をパース) +#[target(!sv)] +export Option parse_int(string s) { + int result = 0; + bool negative = false; + int i = 0; + int len = s.len(); + + if (len == 0) { + return Option::None; + } + + // 符号チェック + if (s[0] == '-') { + negative = true; + i = 1; + } else if (s[0] == '+') { + i = 1; + } + + // 先頭が数字でなければ失敗 + if (i >= len || (s[i] as int) < 48 || (s[i] as int) > 57) { + return Option::None; + } + + // 数字を処理 + while (i < len) { + int c = s[i] as int; + if (c >= 48 && c <= 57) { + result = result * 10 + (c - 48); + } else { + break; + } + i = i + 1; + } + + if (negative) { + result = -result; + } + + return Option::Some(result); +} + +/// 基数base(2〜36)で文字列を整数に変換(失敗はNone。0x等のプレフィックスは扱わない) +#[target(!sv)] +export Option parse_int_radix(string s, int base) { + if (base < 2 || base > 36) { + return Option::None; + } + int result = 0; + bool negative = false; + int i = 0; + int len = s.len(); + + if (len == 0) { + return Option::None; + } + + if (s[0] == '-') { + negative = true; + i = 1; + } else if (s[0] == '+') { + i = 1; + } + + // 先頭が基数内の数字でなければ失敗 + if (i >= len || digit_value(s[i], base).is_none()) { + return Option::None; + } + + while (i < len) { + Option d = digit_value(s[i], base); + if (d.is_none()) { + break; + } + result = result * base + d.unwrap(); + i = i + 1; + } + + if (negative) { + result = -result; + } + + return Option::Some(result); +} + +/// 基数base(2〜36)で文字列をlongに変換(失敗はNone) +#[target(!sv)] +export Option parse_long_radix(string s, int base) { + if (base < 2 || base > 36) { + return Option::None; + } + long result = 0; + bool negative = false; + int i = 0; + int len = s.len(); + + if (len == 0) { + return Option::None; + } + + if (s[0] == '-') { + negative = true; + i = 1; + } else if (s[0] == '+') { + i = 1; + } + + if (i >= len || digit_value(s[i], base).is_none()) { + return Option::None; + } + + while (i < len) { + Option d = digit_value(s[i], base); + if (d.is_none()) { + break; + } + result = result * (base as long) + (d.unwrap() as long); + i = i + 1; + } + + if (negative) { + result = -result; + } + + return Option::Some(result); +} + +/// 文字列をboolに変換("true"/"1"/"yes"はtrue、"false"/"0"/"no"はfalse、それ以外はNone) +#[target(!sv)] +export Option parse_bool(string s) { + if (s == "true" || s == "1" || s == "yes") { + return Option::Some(true); + } + if (s == "false" || s == "0" || s == "no") { + return Option::Some(false); + } + return Option::None; +} + +/// 文字列をlongに変換(失敗はNone) +#[target(!sv)] +export Option parse_long(string s) { + long result = 0; + bool negative = false; + int i = 0; + int len = s.len(); + + if (len == 0) { + return Option::None; + } + + if (s[0] == '-') { + negative = true; + i = 1; + } else if (s[0] == '+') { + i = 1; + } + + if (i >= len || (s[i] as int) < 48 || (s[i] as int) > 57) { + return Option::None; + } + + while (i < len) { + int c = s[i] as int; + if (c >= 48 && c <= 57) { + result = result * 10 + (c - 48); + } else { + break; + } + i = i + 1; + } + + if (negative) { + result = -result; + } + + return Option::Some(result); +} + +/// 文字列をdoubleに変換(簡易実装。失敗はNone。指数表記は未対応) +/// SVターゲットは浮動小数非対応(SV004)のため宣言ごと除外する +#[target(!sv)] +export Option parse_double(string s) { + double result = 0.0; + bool negative = false; + int i = 0; + int len = s.len(); + + if (len == 0) { + return Option::None; + } + + if (s[0] == '-') { + negative = true; + i = 1; + } else if (s[0] == '+') { + i = 1; + } + + // 先頭が数字か小数点でなければ失敗 + if (i >= len || (((s[i] as int) < 48 || (s[i] as int) > 57) && (s[i] as int) != 46)) { + return Option::None; + } + + // 整数部 + while (i < len) { + int c = s[i] as int; + if (c >= 48 && c <= 57) { + result = result * 10.0 + (c - 48) as double; + } else if (c == 46) { + i = i + 1; + break; + } else { + break; + } + i = i + 1; + } + + // 小数部 + double fraction = 0.1; + while (i < len) { + int c = s[i] as int; + if (c >= 48 && c <= 57) { + result = result + ((c - 48) as double) * fraction; + fraction = fraction * 0.1; + } else { + break; + } + i = i + 1; + } + + if (negative) { + result = -result; + } + + return Option::Some(result); +} diff --git a/src/internal/mir/lowering/expr/call.cpp b/src/internal/mir/lowering/expr/call.cpp index a3bdf06b..50689510 100644 --- a/src/internal/mir/lowering/expr/call.cpp +++ b/src/internal/mir/lowering/expr/call.cpp @@ -208,7 +208,23 @@ LocalId ExprLowering::lower_call(const hir::HirCall& call, const hir::TypePtr& r } } - // フォールバック: 通常のlower_expressionを使用 + // フィールド・インデックス・デリファレンスのチェーンがレシーバの場合は、 + // 場所化(lower_place)して実体のアドレスをselfとして渡す。 + // 従来は下のフォールバックで一時コピーへの参照が渡り、w.c.bump()のような + // フィールドレシーバのメソッド内フィールド変異が呼び出し元に反映されなかった + { + MirPlace recv_place{0}; + hir::TypePtr recv_type = nullptr; + if (lower_place(arg.get(), ctx, recv_place, recv_type)) { + LocalId ref_temp = ctx.new_temp(hir::make_pointer(arg_type)); + ctx.push_statement(MirStatement::assign(MirPlace{ref_temp}, + MirRvalue::ref(recv_place, false))); + args.push_back(MirOperand::copy(MirPlace{ref_temp})); + continue; + } + } + + // フォールバック: 通常のlower_expressionを使用(関数戻り値等の右辺値レシーバ) LocalId arg_local = lower_expression(*arg, ctx); LocalId ref_temp = ctx.new_temp(hir::make_pointer(arg_type)); ctx.push_statement(MirStatement::assign( diff --git a/src/internal/mir/lowering/mono/internal.cpp b/src/internal/mir/lowering/mono/internal.cpp index 47db7af1..88326cdc 100644 --- a/src/internal/mir/lowering/mono/internal.cpp +++ b/src/internal/mir/lowering/mono/internal.cpp @@ -200,6 +200,32 @@ hir::TypePtr substitute_type_in_type( } } + // 2.6 関数ポインタ型の場合(int*(T, T) → int*(int, int)) + // param_types/return_typeを再帰的に置換する。これがないとジェネリック関数/メソッドの + // 関数ポインタ引数のシグネチャが未置換(Tのまま)で残り、間接呼び出しがLLVM検証エラー + // (Call parameter type does not match function signature)になる + if (type->kind == hir::TypeKind::Function) { + bool fn_changed = false; + auto new_fn = std::make_shared(*type); + for (auto& pt : new_fn->param_types) { + auto sub = substitute_type_in_type(pt, type_subst, mono); + if (sub && (sub != pt || (pt && sub->name != pt->name))) { + fn_changed = true; + } + pt = sub; + } + if (new_fn->return_type) { + auto sub = substitute_type_in_type(new_fn->return_type, type_subst, mono); + if (sub && (sub != new_fn->return_type || sub->name != new_fn->return_type->name)) { + fn_changed = true; + } + new_fn->return_type = sub; + } + if (fn_changed) { + return new_fn; + } + } + // 3a. $エンコード名に型パラメータが埋め込まれている場合(Container$1$1$T → 復号・置換・再エンコード) if ((type->kind == hir::TypeKind::Struct || type->kind == hir::TypeKind::TypeAlias) && ast::typekey::is_encoded_key(type->name)) { diff --git a/src/internal/mir/lowering/mono/specialize.cpp b/src/internal/mir/lowering/mono/specialize.cpp index a903a7ad..0d3508bc 100644 --- a/src/internal/mir/lowering/mono/specialize.cpp +++ b/src/internal/mir/lowering/mono/specialize.cpp @@ -4,6 +4,7 @@ #include "internal/base/target.hpp" #include "internal/mir/lowering/context.hpp" #include "internal/mir/lowering/expr.hpp" +#include "internal/mir/lowering/layout.hpp" #include "internal/mir/lowering/mono/internal.hpp" #include "internal/mir/lowering/mono/monomorphization.hpp" #include "internal/mir/lowering/mono/utils.hpp" @@ -375,6 +376,75 @@ void Monomorphization::generate_generic_specializations( } } + // ターミネータ(呼び出しの引数・戻り値格納先・分岐判別値)の型も置換する。 + // 従来はstatementの場所・オペランドのみが対象で、terminator側のplace型 + // (フィールド宛先のresult_type等)が未置換のまま残っていた + for (auto& block : specialized->basic_blocks) { + if (!block || !block->terminator) + continue; + if (block->terminator->kind == MirTerminator::Call) { + auto& term_call = std::get(block->terminator->data); + for (auto& term_arg : term_call.args) { + substitute_operand_types(term_arg); + } + if (term_call.destination) { + substitute_place_types(*term_call.destination); + } + } else if (block->terminator->kind == MirTerminator::SwitchInt) { + auto& term_sw = std::get(block->terminator->data); + substitute_operand_types(term_sw.discriminant); + } + } + + // cm_slice_new の要素サイズ定数を置換後の宛先スライス型から再計算する。 + // ジェネリック関数のMIRは要素サイズを未解決のK(blob幅の既定値)で定数化しており、 + // 特殊化後もヘッダelem_sizeが誤ったままだと呼び出し側の静的ストライドと食い違い、 + // K[]戻り値の要素読みがずれる(交互にゴミ値が混入)。blob系push/getはサイズ非依存の + // 呼び出し規約(アドレス渡し+ヘッダelem_sizeバイトのインラインコピー)のため、 + // ヘッダサイズの補正だけで全経路が整合する + for (auto& block : specialized->basic_blocks) { + if (!block || !block->terminator) + continue; + if (block->terminator->kind != MirTerminator::Call) + continue; + auto& call_data = std::get(block->terminator->data); + if (!call_data.func || call_data.func->kind != MirOperand::FunctionRef) + continue; + const std::string& slice_fn = std::get(call_data.func->data); + const bool is_slice_new = slice_fn == "cm_slice_new"; + const bool is_array_to_slice = slice_fn == "cm_array_to_slice"; + if (!is_slice_new && !is_array_to_slice) + continue; + // elem_size引数の位置: cm_slice_new(elem_size, cap)は第1引数、 + // cm_array_to_slice(ptr, len, elem_size)は第3引数 + const size_t size_arg_idx = is_slice_new ? 0 : 2; + if (call_data.args.size() <= size_arg_idx || !call_data.destination) + continue; + // 宛先スライス型を解決する: 投影付きplace(self.tmp等のフィールド宛先)は最後の投影の + // result_type / placeのtype、投影なしはローカル型を使う(いずれもmono置換済み) + hir::TypePtr dest_type = nullptr; + if (!call_data.destination->projections.empty()) { + dest_type = call_data.destination->projections.back().result_type; + if (!dest_type) { + dest_type = call_data.destination->type; + } + } else if (call_data.destination->local < specialized->locals.size()) { + dest_type = specialized->locals[call_data.destination->local].type; + } + if (!dest_type || dest_type->kind != hir::TypeKind::Array || + dest_type->array_size.has_value() || !dest_type->element_type) + continue; + const int64_t stride = layout::slice_elem_stride_of( + dest_type->element_type, + [this](const hir::TypePtr& t) { return calculate_specialized_type_size(t); }); + MirConstant stride_const; + stride_const.value = stride; + stride_const.type = hir::make_long(); + call_data.args[size_arg_idx] = MirOperand::constant(stride_const); + debug_msg("MONO", "Recomputed " + slice_fn + " elem_size=" + std::to_string(stride) + + " in specialized function " + specialized_name); + } + // メソッド呼び出しの self 引数に対する参照修正 // 構造体メソッド呼び出しで、第1引数が値型の場合に &ref を追加 for (auto& block : specialized->basic_blocks) { diff --git a/src/internal/mir/passes/cleanup/program_dce.cpp b/src/internal/mir/passes/cleanup/program_dce.cpp index cc1352ed..c2d054aa 100644 --- a/src/internal/mir/passes/cleanup/program_dce.cpp +++ b/src/internal/mir/passes/cleanup/program_dce.cpp @@ -258,10 +258,29 @@ bool ProgramDeadCodeElimination::remove_unused_functions(MirProgram& program, return changed; } +// 型ノードからポインタ・配列・型引数を再帰的に辿り、参照される構造体名を収集する。 +// 従来は値型の構造体(と配列直下)だけを見ており、Ctr*のようにポインタ経由でしか現れない構造体が「未使用」と誤判定されて定義ごと削除され、AOTのフィールド投影がstruct型を解決できず命令が黙って脱落していた(JITはプログラムDCEを通らないため顕在化しない) +static void collect_struct_names_from_type(const ast::TypePtr& type, std::set& used, + std::queue& worklist) { + if (!type) + return; + if (type->kind == ast::TypeKind::Struct || type->kind == ast::TypeKind::Generic) { + if (!type->name.empty() && used.insert(type->name).second) { + worklist.push(type->name); + } + } + collect_struct_names_from_type(type->element_type, used, worklist); + for (const auto& targ : type->type_args) { + collect_struct_names_from_type(targ, used, worklist); + } +} + void ProgramDeadCodeElimination::collect_used_structs(const MirProgram& program, std::set& used, const std::set& used_functions) { - // 使用される関数の引数・戻り値・ローカル変数から構造体を収集 + std::queue worklist; + + // 使用される関数の引数・戻り値・ローカル変数から構造体を収集(ポインタ・配列・型引数の中も辿る) for (const auto& func : program.functions) { if (!func) continue; @@ -269,25 +288,11 @@ void ProgramDeadCodeElimination::collect_used_structs(const MirProgram& program, continue; for (const auto& local : func->locals) { - if (local.type && local.type->kind == ast::TypeKind::Struct) { - used.insert(local.type->name); - } - // 配列の要素型も検査 - if (local.type && local.type->kind == ast::TypeKind::Array && - local.type->element_type) { - if (local.type->element_type->kind == ast::TypeKind::Struct) { - used.insert(local.type->element_type->name); - } - } + collect_struct_names_from_type(local.type, used, worklist); } } - // 構造体のフィールドからも収集(再帰的) - std::queue worklist; - for (const auto& s : used) { - worklist.push(s); - } - + // 構造体のフィールドからも収集(再帰的。フィールドのポインタ・配列型の中も辿る) while (!worklist.empty()) { std::string current = worklist.front(); worklist.pop(); @@ -297,12 +302,7 @@ void ProgramDeadCodeElimination::collect_used_structs(const MirProgram& program, continue; for (const auto& field : st->fields) { - if (field.type && field.type->kind == ast::TypeKind::Struct) { - if (used.find(field.type->name) == used.end()) { - used.insert(field.type->name); - worklist.push(field.type->name); - } - } + collect_struct_names_from_type(field.type, used, worklist); } } } diff --git a/src/internal/syntax/ast/types.cpp b/src/internal/syntax/ast/types.cpp index ef01e8ba..619f0295 100644 --- a/src/internal/syntax/ast/types.cpp +++ b/src/internal/syntax/ast/types.cpp @@ -219,9 +219,10 @@ TypePtr substitute_type_params(const TypePtr& type, return it->second; } - // 置換対象(element_type・type_args)を含まないリーフはそのまま共有する。 + // 置換対象(element_type・type_args・関数型のparam_types/return_type)を含まないリーフはそのまま共有する。 // クローンするとUnionType等の派生ノードがスライスされ変種情報を失うため、コピー自体を避ける - if (!type->element_type && type->type_args.empty()) + if (!type->element_type && type->type_args.empty() && type->param_types.empty() && + !type->return_type) return type; auto result = std::make_shared(*type); @@ -229,6 +230,13 @@ TypePtr substitute_type_params(const TypePtr& type, result->element_type = substitute_type_params(type->element_type, subst); for (auto& arg : result->type_args) arg = substitute_type_params(arg, subst); + // 関数ポインタ型(int*(T, T)等)の引数・戻り値も置換する。 + // 従来は未置換のまま共有され、ジェネリック関数/メソッドの関数ポインタ引数経由の間接呼び出しが + // LLVM検証エラー(Call parameter type does not match)になっていた + for (auto& pt : result->param_types) + pt = substitute_type_params(pt, subst); + if (type->return_type) + result->return_type = substitute_type_params(type->return_type, subst); // "Box" 表記が名前に残っている場合は基底名へ正規化する(type_argsが真実) if (!result->type_args.empty()) { diff --git a/tests/benchmarks/cm/08_treeset_ops.cm b/tests/benchmarks/cm/08_treeset_ops.cm new file mode 100644 index 00000000..15faabd5 --- /dev/null +++ b/tests/benchmarks/cm/08_treeset_ops.cm @@ -0,0 +1,39 @@ +// ベンチマーク8: TreeSet(平衡二分探索木)の挿入・検索・順序走査 +// 20,000要素の昇順挿入(素朴なBSTの最悪ケース)+ 全件検索 + in-order走査 + +import std::io; +import std::collections::treeset::*; +import std::core::time::*; + +int main() { + const int n = 20000; + Timer t = Timer::start(); + + TreeSet s(); + for (int i = 0; i < n; i++) { + s.insert(i); + } + ulong insert_ms = t.elapsed_ms(); + + t.reset(); + int hit = 0; + for (int i = 0; i < n; i++) { + if (s.contains(i)) { + hit = hit + 1; + } + } + ulong lookup_ms = t.elapsed_ms(); + + t.reset(); + int[] v = s.values_in_order(); + ulong iter_ms = t.elapsed_ms(); + + println("TreeSet n={n} height={s.tree_height()} hit={hit} iter_n={v.len()}"); + println("insert={insert_ms}ms lookup={lookup_ms}ms iterate={iter_ms}ms"); + if (hit == n && v.len() == n && s.tree_height() <= 22) { + println("Successfully verified balanced TreeSet with {n} elements"); + return 0; + } + println("verification failed"); + return 1; +} diff --git a/tests/benchmarks/cm/09_hashset_ops.cm b/tests/benchmarks/cm/09_hashset_ops.cm new file mode 100644 index 00000000..1359fe9f --- /dev/null +++ b/tests/benchmarks/cm/09_hashset_ops.cm @@ -0,0 +1,41 @@ +// ベンチマーク9: HashSetの挿入・検索・削除 +// 50,000要素の挿入 + 全件検索 + 半数削除 + +import std::io; +import std::collections::hashset::*; +import std::core::time::*; + +int main() { + const int n = 50000; + Timer t = Timer::start(); + + HashSet s(); + for (int i = 0; i < n; i++) { + s.insert(i * 3); + } + ulong insert_ms = t.elapsed_ms(); + + t.reset(); + int hit = 0; + for (int i = 0; i < n; i++) { + if (s.contains(i * 3)) { + hit = hit + 1; + } + } + ulong lookup_ms = t.elapsed_ms(); + + t.reset(); + for (int i = 0; i < n; i = i + 2) { + s.remove(i * 3); + } + ulong remove_ms = t.elapsed_ms(); + + println("HashSet n={n} hit={hit} after_remove={s.len()}"); + println("insert={insert_ms}ms lookup={lookup_ms}ms remove={remove_ms}ms"); + if (hit == n && s.len() == n / 2) { + println("Successfully verified HashSet with {n} elements"); + return 0; + } + println("verification failed"); + return 1; +} diff --git a/tests/benchmarks/cm/10_arena_vs_alloc.cm b/tests/benchmarks/cm/10_arena_vs_alloc.cm new file mode 100644 index 00000000..44e9962e --- /dev/null +++ b/tests/benchmarks/cm/10_arena_vs_alloc.cm @@ -0,0 +1,40 @@ +// ベンチマーク10: Arenaアロケータ vs 都度alloc/dealloc +// 200,000件の小口確保をArenaのバンプ確保と、グローバルアロケータの都度確保・解放で比較 + +import std::io; +import std::mem::arena::*; +import std::mem::{alloc, dealloc}; +import std::core::time::*; + +int main() { + const int n = 200000; + + Timer t = Timer::start(); + Arena a(65536); + long arena_sum = 0; + for (int i = 0; i < n; i++) { + int* p = a.alloc_bytes(4) as int*; + *p = i; + arena_sum = arena_sum + (*p as long); + } + a.reset(); + ulong arena_ms = t.elapsed_ms(); + + t.reset(); + long alloc_sum = 0; + for (int i = 0; i < n; i++) { + int* p = alloc(4) as int*; + *p = i; + alloc_sum = alloc_sum + (*p as long); + dealloc(p as void*); + } + ulong alloc_ms = t.elapsed_ms(); + + println("arena={arena_ms}ms alloc/dealloc={alloc_ms}ms n={n}"); + if (arena_sum == alloc_sum) { + println("Successfully verified {n} allocations (checksum match)"); + return 0; + } + println("verification failed"); + return 1; +} diff --git a/tests/common/generics/functions/fnptr_param.cm b/tests/common/generics/functions/fnptr_param.cm new file mode 100644 index 00000000..f2304466 --- /dev/null +++ b/tests/common/generics/functions/fnptr_param.cm @@ -0,0 +1,27 @@ +// v0.17.2バグ修正回帰: ジェネリック関数/implメソッドの関数ポインタ引数(int*(T, T))のシグネチャ置換 +// (旧: param_types/return_typeが未置換のまま共有され、間接呼び出しがLLVM検証エラーになっていた) +import std::io::println; + + int call_cmp(int*(T, T) f, T a, T b) { + return f(a, b); +} + +struct Box { + T v; +} + +impl Box { + int apply(int*(T, T) f, T other) { + return f(self.v, other); + } +} + +int main() { + println("{call_cmp((int x, int y) => x - y, 10, 3)}"); + println("{call_cmp((double x, double y) => (x < y) ? -1 : 1, 1.5, 2.5)}"); + + Box b; + b.v = 21; + println("{b.apply((int a, int c) => a * c, 2)}"); + return 0; +} diff --git a/tests/common/generics/functions/fnptr_param.expect b/tests/common/generics/functions/fnptr_param.expect new file mode 100644 index 00000000..3b5152a6 --- /dev/null +++ b/tests/common/generics/functions/fnptr_param.expect @@ -0,0 +1,3 @@ +7 +-1 +42 diff --git a/tests/common/generics/slices/method_return.cm b/tests/common/generics/slices/method_return.cm new file mode 100644 index 00000000..51fa6d19 --- /dev/null +++ b/tests/common/generics/slices/method_return.cm @@ -0,0 +1,51 @@ +// v0.17.2バグ修正回帰: ジェネリックメソッドが構築して返すK[]スライスの要素幅 +// (旧: cm_slice_new/cm_array_to_sliceの要素サイズ定数が未解決K幅のまま残り、呼び出し側の読みが交互にゴミ化していた) +import std::io::println; + +struct Holder { + K[] keys; + V[] vals; + K[] tmp; +} + +impl Holder { + // ローカル構築して返す + K[] copy_keys() { + K[] out = []; + for (int i = 0; i < self.keys.len(); i++) { + out.push(self.keys[i]); + } + return out; + } + + // フィールドへ構築して返す + K[] via_field() { + self.tmp = []; + for (int i = 0; i < self.keys.len(); i++) { + self.tmp.push(self.keys[i]); + } + return self.tmp; + } +} + +int main() { + Holder h; + h.keys = [10, 20, 30, 40]; + h.vals = [1, 2, 3, 4]; + h.tmp = []; + + int[] a = h.copy_keys(); + println("local: {a[0]} {a[1]} {a[2]} {a[3]} n={a.len()}"); + + int[] b = h.via_field(); + println("field: {b[0]} {b[1]} {b[2]} {b[3]} n={b.len()}"); + + // long要素(8バイト)でも同様に一致する + Holder hl; + hl.keys = [100 as long, 200 as long]; + hl.vals = [1, 2]; + hl.tmp = []; + long[] c = hl.copy_keys(); + println("long: {c[0]} {c[1]} n={c.len()}"); + return 0; +} diff --git a/tests/common/generics/slices/method_return.expect b/tests/common/generics/slices/method_return.expect new file mode 100644 index 00000000..ddddf517 --- /dev/null +++ b/tests/common/generics/slices/method_return.expect @@ -0,0 +1,3 @@ +local: 10 20 30 40 n=4 +field: 10 20 30 40 n=4 +long: 100 200 n=2 diff --git a/tests/common/stdlib/collections/hashset_basic.cm b/tests/common/stdlib/collections/hashset_basic.cm new file mode 100644 index 00000000..7f14aee1 --- /dev/null +++ b/tests/common/stdlib/collections/hashset_basic.cm @@ -0,0 +1,25 @@ +//! platform: !js|sv +// HashSet: 挿入・重複・存在判定・削除・clear +import std::io::println; +import std::collections::hashset::*; + +int main() { + HashSet s(); + s.insert(3); + s.insert(1); + s.insert(4); + s.insert(3); + println("len={s.len()} c3={s.contains(3)} c2={s.contains(2)}"); + + s.remove(3); + println("len={s.len()} c3={s.contains(3)}"); + s.remove(99); + println("len={s.len()}"); + + s.clear(); + println("cleared len={s.len()} empty={s.is_empty()}"); + + s.insert(7); + println("reuse len={s.len()} c7={s.contains(7)}"); + return 0; +} diff --git a/tests/common/stdlib/collections/hashset_basic.expect b/tests/common/stdlib/collections/hashset_basic.expect new file mode 100644 index 00000000..9b872d38 --- /dev/null +++ b/tests/common/stdlib/collections/hashset_basic.expect @@ -0,0 +1,5 @@ +len=3 c3=true c2=false +len=2 c3=false +len=2 +cleared len=0 empty=true +reuse len=1 c7=true diff --git a/tests/common/stdlib/collections/hashset_load.cm b/tests/common/stdlib/collections/hashset_load.cm new file mode 100644 index 00000000..698b66cc --- /dev/null +++ b/tests/common/stdlib/collections/hashset_load.cm @@ -0,0 +1,34 @@ +//! platform: !js|sv +// パフォーマンススモーク: HashSetの大量挿入・参照・削除がスイートのタイムアウト内で完了する +// (O(1)期待の操作が線形化するとタイムアウトで検出される) +import std::io::println; +import std::collections::hashset::*; + +int main() { + HashSet s(); + const int n = 10000; + for (int i = 0; i < n; i++) { + s.insert(i * 7); + } + println("len={s.len()}"); + + int hit = 0; + for (int i = 0; i < n; i++) { + if (s.contains(i * 7)) { + hit = hit + 1; + } + } + int miss = 0; + for (int i = 0; i < 100; i++) { + if (s.contains(i * 7 + 1)) { + miss = miss + 1; + } + } + println("hit={hit} miss={miss}"); + + for (int i = 0; i < n; i = i + 2) { + s.remove(i * 7); + } + println("after remove len={s.len()}"); + return 0; +} diff --git a/tests/common/stdlib/collections/hashset_load.expect b/tests/common/stdlib/collections/hashset_load.expect new file mode 100644 index 00000000..fb93962f --- /dev/null +++ b/tests/common/stdlib/collections/hashset_load.expect @@ -0,0 +1,3 @@ +len=10000 +hit=10000 miss=0 +after remove len=5000 diff --git a/tests/common/stdlib/collections/treemap_remove.cm b/tests/common/stdlib/collections/treemap_remove.cm new file mode 100644 index 00000000..55f1b7f6 --- /dev/null +++ b/tests/common/stdlib/collections/treemap_remove.cm @@ -0,0 +1,35 @@ +// TreeMap: AVL削除(葉・子1・子2)・スロット再利用・順序維持・clear +import std::io::println; +import std::collections::treemap::*; + +int main() { + TreeMap m(); + for (int i = 0; i < 20; i++) { + m.put(i, i * 10); + } + println("size={m.size()}"); + + // 葉・内部・根寄りの削除 + m.remove(0); + m.remove(10); + m.remove(19); + println("size={m.size()} c10={m.contains(10)} c11={m.contains(11)}"); + + // 順序が維持される + int[] ks = m.keys_in_order(); + println("first={ks[0]} last={ks[ks.len()-1]} n={ks.len()}"); + + // 削除済みキーの再挿入(free listのスロット再利用) + m.put(10, 999); + println("size={m.size()} v10={m.get_or(10, -1)}"); + + // 存在しないキーの削除は無視 + m.remove(1000); + println("size={m.size()}"); + + m.clear(); + println("cleared size={m.size()} h={m.tree_height()}"); + m.put(1, 1); + println("reuse size={m.size()}"); + return 0; +} diff --git a/tests/common/stdlib/collections/treemap_remove.expect b/tests/common/stdlib/collections/treemap_remove.expect new file mode 100644 index 00000000..95a31736 --- /dev/null +++ b/tests/common/stdlib/collections/treemap_remove.expect @@ -0,0 +1,7 @@ +size=20 +size=17 c10=false c11=true +first=1 last=18 n=17 +size=18 v10=999 +size=18 +cleared size=0 h=0 +reuse size=1 diff --git a/tests/common/stdlib/collections/treeset_balance.cm b/tests/common/stdlib/collections/treeset_balance.cm new file mode 100644 index 00000000..8e73aa5a --- /dev/null +++ b/tests/common/stdlib/collections/treeset_balance.cm @@ -0,0 +1,36 @@ +// パフォーマンス特性テスト: AVL平衡性の決定的検証 +// 昇順4096件の連続挿入(素朴なBSTなら高さ4096に退化する最悪ケース)で、 +// 平衡二分探索木の高さがAVL上界(1.44*log2(n)+2 ≒ 20)へ収まることを検証する +import std::io::println; +import std::collections::treeset::*; + +int main() { + TreeSet s(); + const int n = 4096; + for (int i = 0; i < n; i++) { + s.insert(i); + } + println("len={s.len()}"); + int h = s.tree_height(); + println("height_ok={h >= 12 && h <= 20} logn_bound=20"); + + // 全件ヒットと境界の不在 + bool all = true; + for (int i = 0; i < n; i++) { + if (!s.contains(i)) { + all = false; + } + } + println("all={all} miss={s.contains(n)}"); + + // 半分削除しても平衡が保たれる(偶数のみ残す) + for (int i = 1; i < n; i = i + 2) { + s.remove(i); + } + int h2 = s.tree_height(); + println("len={s.len()} height2_ok={h2 >= 11 && h2 <= 20}"); + + int[] v = s.values_in_order(); + println("first={v[0]} second={v[1]} last={v[v.len()-1]}"); + return 0; +} diff --git a/tests/common/stdlib/collections/treeset_balance.expect b/tests/common/stdlib/collections/treeset_balance.expect new file mode 100644 index 00000000..04c71b2e --- /dev/null +++ b/tests/common/stdlib/collections/treeset_balance.expect @@ -0,0 +1,5 @@ +len=4096 +height_ok=true logn_bound=20 +all=true miss=false +len=2048 height2_ok=true +first=0 second=2 last=4094 diff --git a/tests/common/stdlib/collections/treeset_basic.cm b/tests/common/stdlib/collections/treeset_basic.cm new file mode 100644 index 00000000..77153bb9 --- /dev/null +++ b/tests/common/stdlib/collections/treeset_basic.cm @@ -0,0 +1,36 @@ +// TreeSet(平衡二分探索木): 挿入・存在判定・削除・昇順走査・clear +import std::io::println; +import std::collections::treeset::*; + +int main() { + TreeSet s(); + s.insert(5); + s.insert(1); + s.insert(9); + s.insert(5); + s.insert(3); + println("len={s.len()} c5={s.contains(5)} c2={s.contains(2)}"); + + // 昇順走査 + int[] v = s.values_in_order(); + println("{v[0]} {v[1]} {v[2]} {v[3]} n={v.len()}"); + + // 削除(葉・子1つ・子2つのケースを通す) + s.remove(1); + s.remove(5); + println("len={s.len()} c1={s.contains(1)} c5={s.contains(5)} c9={s.contains(9)}"); + int[] w = s.values_in_order(); + println("{w[0]} {w[1]} n={w.len()}"); + + // 文字列要素 + TreeSet t(); + t.insert("banana"); + t.insert("apple"); + t.insert("cherry"); + string[] u = t.values_in_order(); + println("{u[0]} {u[1]} {u[2]}"); + + s.clear(); + println("cleared len={s.len()} empty={s.is_empty()}"); + return 0; +} diff --git a/tests/common/stdlib/collections/treeset_basic.expect b/tests/common/stdlib/collections/treeset_basic.expect new file mode 100644 index 00000000..397e1849 --- /dev/null +++ b/tests/common/stdlib/collections/treeset_basic.expect @@ -0,0 +1,6 @@ +len=4 c5=true c2=false +1 3 5 9 n=4 +len=2 c1=false c5=false c9=true +3 9 n=2 +apple banana cherry +cleared len=0 empty=true diff --git a/tests/common/stdlib/collections/vector_enhance.cm b/tests/common/stdlib/collections/vector_enhance.cm new file mode 100644 index 00000000..88b18c73 --- /dev/null +++ b/tests/common/stdlib/collections/vector_enhance.cm @@ -0,0 +1,35 @@ +//! platform: !js|sv +// Vector強化: reserve/last/挿入ソート/sortBy(比較ラムダ) +import std::io::println; +import std::collections::vector::*; + +int main() { + Vector v(); + v.reserve(64); + println("cap={v.capacity()} len={v.len()}"); + + v.push(5); + v.push(2); + v.push(9); + v.push(1); + println("last={*v.last()}"); + + v.sort(); + println("{*v.get(0)} {*v.get(1)} {*v.get(2)} {*v.get(3)}"); + + v.sortBy((int a, int b) => b - a); + println("{*v.get(0)} {*v.get(1)} {*v.get(2)} {*v.get(3)}"); + + // reserveは縮小しない + v.reserve(4); + println("cap={v.capacity() >= 64}"); + + // doubleでもsortByが動く + Vector d(); + d.push(2.5); + d.push(0.5); + d.push(1.5); + d.sortBy((double a, double b) => (a < b) ? -1 : 1); + println("{*d.get(0)} {*d.get(1)} {*d.get(2)}"); + return 0; +} diff --git a/tests/common/stdlib/collections/vector_enhance.expect b/tests/common/stdlib/collections/vector_enhance.expect new file mode 100644 index 00000000..81b09975 --- /dev/null +++ b/tests/common/stdlib/collections/vector_enhance.expect @@ -0,0 +1,6 @@ +cap=64 len=0 +last=1 +1 2 5 9 +9 5 2 1 +cap=true +0.5 1.5 2.5 diff --git a/tests/common/stdlib/core/parse_option_test.cm b/tests/common/stdlib/core/parse_option_test.cm index d932577d..a131d970 100644 --- a/tests/common/stdlib/core/parse_option_test.cm +++ b/tests/common/stdlib/core/parse_option_test.cm @@ -1,7 +1,7 @@ // M17回帰: std::ioのパース関数がOption返しになり、失敗を0で握り潰さないことを検証する // (空文字列・非数字先頭はNone、有効入力はSome。チェーン形unwrap_orのレシーバ退避も検証) import std::io::println; -import std::io::console::input::*; +import std::strings::parse::*; int main() { println(parse_int("123").unwrap_or(-1)); diff --git a/tests/common/stdlib/fs/lines.cm b/tests/common/stdlib/fs/lines.cm new file mode 100644 index 00000000..548dd035 --- /dev/null +++ b/tests/common/stdlib/fs/lines.cm @@ -0,0 +1,18 @@ +//! platform: !wasm|js|sv +// std::fs::read_lines: 行分割読み(LF・末尾改行あり/なし) +import std::io::println; +import std::fs::{write_file, read_lines, remove}; + +int main() { + const string path = "/tmp/cm_read_lines_test.txt"; + write_file(path, "alpha\nbeta\ngamma\n"); + string[] ls = read_lines(path); + println("n={ls.len()} l0={ls[0]} l1={ls[1]} l2={ls[2]}"); + + write_file(path, "solo"); + string[] one = read_lines(path); + println("n={one.len()} l0={one[0]}"); + + remove(path); + return 0; +} diff --git a/tests/common/stdlib/fs/lines.expect b/tests/common/stdlib/fs/lines.expect new file mode 100644 index 00000000..c6b7422c --- /dev/null +++ b/tests/common/stdlib/fs/lines.expect @@ -0,0 +1,2 @@ +n=3 l0=alpha l1=beta l2=gamma +n=1 l0=solo diff --git a/tests/common/stdlib/mem/arena_alloc.cm b/tests/common/stdlib/mem/arena_alloc.cm new file mode 100644 index 00000000..d5eb7454 --- /dev/null +++ b/tests/common/stdlib/mem/arena_alloc.cm @@ -0,0 +1,47 @@ +//! platform: !js|sv +// Arenaアロケータ: バンプ確保・整列・チャンク拡張・大型確保・reset・統計 +import std::io::println; +import std::mem::arena::*; + +struct Node { + int value; + long tag; +} + +int main() { + Arena a(); + + // 基本確保と書き込み + int* x = a.alloc_bytes(4) as int*; + *x = 42; + long* y = a.alloc_bytes(8) as long*; + *y = 99; + println("{*x} {*y}"); + + // 8バイト整列(4バイト要求でも8単位で進む) + println("aligned={a.allocated_bytes()}"); + + // 構造体の確保 + Node* n = a.alloc_bytes(16) as Node*; + n->value = 7; + n->tag = 77; + println("{n->value} {n->tag}"); + + // 既定チャンク(4096)を跨ぐ大量確保と、チャンク超過の大型確保 + for (int i = 0; i < 1000; i++) { + int* p = a.alloc_bytes(8) as int*; + *p = i; + } + void* big = a.alloc_bytes(10000); + long bigv = 123456789; + *(big as long*) = bigv; + println("big={*(big as long*)} total={a.allocated_bytes()}"); + + // reset後は統計が0へ戻り、再確保できる + a.reset(); + println("reset={a.allocated_bytes()}"); + int* z = a.alloc_bytes(4) as int*; + *z = 5; + println("{*z}"); + return 0; +} diff --git a/tests/common/stdlib/mem/arena_alloc.expect b/tests/common/stdlib/mem/arena_alloc.expect new file mode 100644 index 00000000..ba9a3680 --- /dev/null +++ b/tests/common/stdlib/mem/arena_alloc.expect @@ -0,0 +1,6 @@ +42 99 +aligned=16 +7 77 +big=123456789 total=18032 +reset=0 +5 diff --git a/tests/common/stdlib/mem/arena_churn.cm b/tests/common/stdlib/mem/arena_churn.cm new file mode 100644 index 00000000..c434bd6f --- /dev/null +++ b/tests/common/stdlib/mem/arena_churn.cm @@ -0,0 +1,23 @@ +//! platform: !js|sv +// パフォーマンススモーク: Arenaの大量小口確保(10万件)がタイムアウト内で完了し、 +// reset一括解放後も再利用できる(個別freeなしのフェーズ運用) +import std::io::println; +import std::mem::arena::*; + +int main() { + Arena a(65536); + const int rounds = 10; + const int per_round = 10000; + long checksum = 0; + for (int r = 0; r < rounds; r++) { + for (int i = 0; i < per_round; i++) { + int* p = a.alloc_bytes(4) as int*; + *p = i; + checksum = checksum + (*p as long); + } + a.reset(); + } + println("checksum={checksum}"); + println("after final reset={a.allocated_bytes()}"); + return 0; +} diff --git a/tests/common/stdlib/mem/arena_churn.expect b/tests/common/stdlib/mem/arena_churn.expect new file mode 100644 index 00000000..2cef17b7 --- /dev/null +++ b/tests/common/stdlib/mem/arena_churn.expect @@ -0,0 +1,2 @@ +checksum=499950000 +after final reset=0 diff --git a/tests/common/stdlib/smart/atomic_shared.cm b/tests/common/stdlib/smart/atomic_shared.cm new file mode 100644 index 00000000..0c8a035d --- /dev/null +++ b/tests/common/stdlib/smart/atomic_shared.cm @@ -0,0 +1,23 @@ +//! platform: !wasm|js|sv +// AtomicSharedPtr: 単一スレッドでの機能検証(clone/use_count/共有可視性/段階的解放)。 +// アトミック増減の実効性はネイティブC++ランタイム()に依存し、マルチスレッドの +// 競合検証はstd::syncのスレッドAPIと合わせた将来のストレステストで扱う +import std::io::println; +import std::mem::smart::atomic::*; + +int main() { + AtomicSharedPtr a(42); + println("rc={a.use_count()}"); + + { + AtomicSharedPtr b = a.clone(); + println("b={b.get()} rc={b.use_count()}"); + b.set(100); + println("a={a.get()}"); + } + println("rc={a.use_count()}"); + + AtomicSharedPtr c = move a; + println("c={c.get()} rc={c.use_count()}"); + return 0; +} diff --git a/tests/common/stdlib/smart/atomic_shared.expect b/tests/common/stdlib/smart/atomic_shared.expect new file mode 100644 index 00000000..00d446dc --- /dev/null +++ b/tests/common/stdlib/smart/atomic_shared.expect @@ -0,0 +1,5 @@ +rc=1 +b=42 rc=2 +a=100 +rc=1 +c=100 rc=1 diff --git a/tests/common/stdlib/smart/weak.cm b/tests/common/stdlib/smart/weak.cm new file mode 100644 index 00000000..1c3f5a4f --- /dev/null +++ b/tests/common/stdlib/smart/weak.cm @@ -0,0 +1,32 @@ +//! platform: !js|sv +// WeakPtr: downgrade/upgrade/生存判定/reset連動/制御ブロックの遅延解放 +import std::io::println; +import std::mem::smart::*; + +int main() { + // 生存中のupgrade + SharedPtr a(42); + WeakPtr w = a.downgrade(); + println("alive={w.is_alive()} strong={a.use_count()} weak={a.weak_count()}"); + + SharedPtr up = w.upgrade(); + println("up={up.get()} strong={a.use_count()}"); + up.reset(); + println("after up.reset strong={a.use_count()}"); + + // weakのclone + WeakPtr w2 = w.clone(); + println("weak={a.weak_count()}"); + + // 共有を増やしてから段階的にreset(moveのエイリアス破棄タイミングに依存しない決定的検証) + SharedPtr b = a.clone(); + a.reset(); + println("alive={w.is_alive()} strong={b.use_count()}"); + b.reset(); + println("alive={w.is_alive()}"); + + // 全strong消滅後のupgradeはnull + SharedPtr dead = w.upgrade(); + println("dead null={dead.is_null()}"); + return 0; +} diff --git a/tests/common/stdlib/smart/weak.expect b/tests/common/stdlib/smart/weak.expect new file mode 100644 index 00000000..09d5efe5 --- /dev/null +++ b/tests/common/stdlib/smart/weak.expect @@ -0,0 +1,7 @@ +alive=true strong=1 weak=1 +up=42 strong=2 +after up.reset strong=1 +weak=2 +alive=true strong=1 +alive=false +dead null=true diff --git a/tests/common/stdlib/strings/chars_classify.cm b/tests/common/stdlib/strings/chars_classify.cm new file mode 100644 index 00000000..9af1a192 --- /dev/null +++ b/tests/common/stdlib/strings/chars_classify.cm @@ -0,0 +1,17 @@ +// std::strings::chars: 文字分類・変換・基数付き数字値 +import std::io::println; +import std::strings::chars::*; + +int main() { + println("{is_digit('0')} {is_digit('9')} {is_digit('a')}"); + println("{is_alpha('A')} {is_alpha('z')} {is_alpha('1')}"); + println("{is_alnum('x')} {is_alnum('5')} {is_alnum('_')}"); + println("{is_space(' ')} {is_space('a')}"); + println("{is_upper('Q')} {is_upper('q')} {is_lower('q')} {is_lower('Q')}"); + println("{is_hex_digit('f')} {is_hex_digit('F')} {is_hex_digit('g')}"); + println("{is_ident_start('_')} {is_ident_start('9')} {is_ident_continue('9')} {is_ident_continue('-')}"); + println("{to_upper('a')} {to_upper('Z')} {to_lower('B')} {to_lower('#')}"); + println("{digit_value('7', 10).unwrap_or(-1)} {digit_value('f', 16).unwrap_or(-1)} {digit_value('z', 36).unwrap_or(-1)}"); + println("{digit_value('9', 8).unwrap_or(-1)} {digit_value('a', 10).unwrap_or(-1)} {digit_value('0', 1).unwrap_or(-1)}"); + return 0; +} diff --git a/tests/common/stdlib/strings/chars_classify.expect b/tests/common/stdlib/strings/chars_classify.expect new file mode 100644 index 00000000..6fa42df7 --- /dev/null +++ b/tests/common/stdlib/strings/chars_classify.expect @@ -0,0 +1,10 @@ +true true false +true true false +true true false +true false +true false true false +true true false +true false true false +A Z b # +7 15 35 +-1 -1 -1 diff --git a/tests/common/stdlib/strings/parse_radix.cm b/tests/common/stdlib/strings/parse_radix.cm new file mode 100644 index 00000000..970c3ba8 --- /dev/null +++ b/tests/common/stdlib/strings/parse_radix.cm @@ -0,0 +1,41 @@ +// std::strings::parse: 基数指定の数値解析と境界値 +import std::io::println; +import std::strings::parse::*; + +int main() { + // 基数16/2/8/36 + int hex = parse_int_radix("ff", 16).unwrap_or(-1); + int neg = parse_int_radix("-ff", 16).unwrap_or(-1); + int bin = parse_int_radix("1010", 2).unwrap_or(-1); + int oct = parse_int_radix("777", 8).unwrap_or(-1); + int b36 = parse_int_radix("zz", 36).unwrap_or(-1); + println("{hex} {neg} {bin} {oct} {b36}"); + + // 失敗系: 空・符号のみ・基数外の先頭・不正基数 + bool e1 = parse_int_radix("", 16).is_none(); + bool e2 = parse_int_radix("-", 16).is_none(); + bool e3 = parse_int_radix("g", 16).is_none(); + bool e4 = parse_int_radix("1", 1).is_none(); + bool e5 = parse_int_radix("1", 37).is_none(); + println("{e1} {e2} {e3} {e4} {e5}"); + + // 途中の基数外文字で停止(先頭の数値部分をパース) + int partial = parse_int_radix("1f2g", 16).unwrap_or(-1); + println("{partial}"); + + // long版(32bit超) + long big = parse_long_radix("ffffffff", 16).unwrap_or(-1 as long); + println("{big}"); + + // 移設後も既存関数が同一挙動 + int m1 = parse_int("-123").unwrap_or(0); + int m2 = parse_int("12ab").unwrap_or(0); + bool m3 = parse_int("ab").is_none(); + println("{m1} {m2} {m3}"); + long l1 = parse_long("9000000000").unwrap_or(0 as long); + println("{l1}"); + bool b1 = parse_bool("yes").unwrap_or(false); + bool b2 = parse_bool("maybe").is_none(); + println("{b1} {b2}"); + return 0; +} diff --git a/tests/common/stdlib/strings/parse_radix.expect b/tests/common/stdlib/strings/parse_radix.expect new file mode 100644 index 00000000..d87002cf --- /dev/null +++ b/tests/common/stdlib/strings/parse_radix.expect @@ -0,0 +1,7 @@ +255 -255 10 511 1295 +true true false true true +498 +4294967295 +-123 12 true +9000000000 +true true diff --git a/tests/common/structs/field_receiver.cm b/tests/common/structs/field_receiver.cm new file mode 100644 index 00000000..abeb1fd5 --- /dev/null +++ b/tests/common/structs/field_receiver.cm @@ -0,0 +1,66 @@ +// v0.17.2バグ修正回帰: フィールドをレシーバにしたメソッド呼び出しの変異が呼び出し元へ反映される +// (旧: HirMemberレシーバがフォールバックの一時コピー参照に落ち、selfフィールド変異が消えていた) +import std::io::println; + +struct Counter { + int n; +} + +impl Counter { + void bump() { + self.n = self.n + 1; + } +} + +struct Wrap { + Counter c; +} + +impl Wrap { + void bump_via_field() { + self.c.bump(); + } +} + +struct GWrap { + Counter c; + T dummy; +} + +impl GWrap { + void bump_via_field() { + self.c.bump(); + } +} + +struct Deep { + Wrap w; +} + +int main() { + // mainからフィールド経由 + Wrap w; + w.c.n = 0; + w.c.bump(); + w.c.bump(); + println("{w.c.n}"); + + // 非ジェネリックimplのメソッド内からフィールド経由 + w.c.n = 10; + w.bump_via_field(); + println("{w.c.n}"); + + // ジェネリックimplのメソッド内からフィールド経由 + GWrap g; + g.c.n = 100; + g.dummy = 0; + g.bump_via_field(); + println("{g.c.n}"); + + // 2段ネストのフィールドレシーバ + Deep d; + d.w.c.n = 5; + d.w.c.bump(); + println("{d.w.c.n}"); + return 0; +} diff --git a/tests/common/structs/field_receiver.expect b/tests/common/structs/field_receiver.expect new file mode 100644 index 00000000..686d5f5d --- /dev/null +++ b/tests/common/structs/field_receiver.expect @@ -0,0 +1,4 @@ +2 +11 +101 +6 diff --git a/tests/common/structs/ptr_only_struct.cm b/tests/common/structs/ptr_only_struct.cm new file mode 100644 index 00000000..9101f998 --- /dev/null +++ b/tests/common/structs/ptr_only_struct.cm @@ -0,0 +1,57 @@ +//! platform: !js|sv +// v0.17.2バグ修正回帰: ポインタ経由でしか参照されない構造体がプログラムDCEで削除されない +// (旧: 値型ローカルだけを「使用中」と数えるため、Ctr*のようなポインタ・フィールド経由のみの +// 構造体定義がAOTで消え、フィールド投影の型解決失敗で命令が黙って脱落し不正IRになっていた) +import std::io::println; +import std::mem::{alloc, dealloc}; + +// ポインタ経由でのみ参照される構造体(値型のローカル・フィールドを作らない) +struct Ctr { + int strong; + int weak; +} + +struct Box { + T* ptr; + Ctr* count; +} + +impl Box { + self(T value) { + void* raw = alloc(__sizeof__(T) as int); + self.ptr = raw as T*; + *self.ptr = value; + void* craw = alloc(8); + self.count = craw as Ctr*; + self.count->strong = 1; + self.count->weak = 0; + } + + int strong() { + return self.count->strong; + } + + void inc() { + self.count->strong = self.count->strong + 1; + } + + ~self() { + void* p = self.ptr as void*; + void* null_ptr = 0 as void*; + if (p != null_ptr) { + dealloc(p); + dealloc(self.count as void*); + self.ptr = null_ptr as T*; + self.count = null_ptr as Ctr*; + } + } +} + +int main() { + Box b(42); + println("v={*b.ptr} rc={b.strong()}"); + b.inc(); + b.inc(); + println("rc={b.strong()} weak={b.count->weak}"); + return 0; +} diff --git a/tests/common/structs/ptr_only_struct.expect b/tests/common/structs/ptr_only_struct.expect new file mode 100644 index 00000000..4bbae9ec --- /dev/null +++ b/tests/common/structs/ptr_only_struct.expect @@ -0,0 +1,2 @@ +v=42 rc=1 +rc=3 weak=0 From e23d7e4f4875eb224a464daa5b1f71761715d8a6 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 13:20:25 +0900 Subject: [PATCH 04/25] =?UTF-8?q?stdlib=E3=81=AE=E5=86=85=E9=83=A8?= =?UTF-8?q?=E3=83=98=E3=83=AB=E3=83=91=E3=83=BC=E3=83=BB=E5=86=85=E9=83=A8?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=90=E3=81=B8private=E4=BF=AE=E9=A3=BE?= =?UTF-8?q?=E5=AD=90=E3=82=92=E4=BB=98=E4=B8=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TreeMap: AVL内部ヘルパー11個(rotate/alloc_node/insert_at/remove_at等)と全フィールド(keys/vals/left/right/height/free_slots/count/live/root)をprivate化 - HashMap: grow()とcap/sizeをprivate化(entriesはHashSetコンストラクタの所有権移動処理が直接触るためpublic維持を理由コメント付きで明記) - HashSet/TreeSet: 内包マップフィールドをprivate化 - Vector: data/size/capをprivate化 - Arena: grow()とhead/chunk_size/totalをprivate化 - UniquePtr: ptrをprivate化、AtomicSharedPtr: ptr/countをprivate化 - SharedPtr/WeakPtr/SharedCount/AtomicSharedCountのフィールドは相互構築(downgrade/upgrade)や制御ブロック共有で他型implから直接アクセスされるためpublic維持とし、理由コメントを追加(privateはimpl粒度の検査のため) - 全バックエンドスイートで通過を確認: interpreter 747・llvm 791・js 756・wasm 747(いずれもFailed 0) ライブラリ内部の可視性変更のみで公開APIと文法に変更はないため、チュートリアル・VSCode拡張・リリースノートの更新は非該当 --- libs/std/collections/hashmap.cm | 7 +++--- libs/std/collections/hashset.cm | 2 +- libs/std/collections/treemap.cm | 40 ++++++++++++++++----------------- libs/std/collections/treeset.cm | 2 +- libs/std/collections/vector.cm | 6 ++--- libs/std/mem/arena.cm | 8 +++---- libs/std/mem/smart/atomic.cm | 5 +++-- libs/std/mem/smart/mod.cm | 5 ++++- 8 files changed, 40 insertions(+), 35 deletions(-) diff --git a/libs/std/collections/hashmap.cm b/libs/std/collections/hashmap.cm index c19f7eca..7762a408 100644 --- a/libs/std/collections/hashmap.cm +++ b/libs/std/collections/hashmap.cm @@ -18,9 +18,10 @@ export struct Entry { } export struct HashMap { + // entriesはHashSetコンストラクタの所有権移動処理(dtor無効化のためのnull代入)が直接触るためpublicのまま Entry* entries; - int cap; - int size; + private int cap; + private int size; } export impl HashMap { @@ -40,7 +41,7 @@ export impl HashMap { } // 容量を2倍へ拡張し全エントリを再ハッシュする(Q7: 従来はresize未実装で、満杯時のinsertが黙って喪失していた) - void grow() { + private void grow() { const int old_cap = self.cap; Entry* old_entries = self.entries; const int new_cap = old_cap * 2; diff --git a/libs/std/collections/hashset.cm b/libs/std/collections/hashset.cm index 9685a041..1f72fa1a 100644 --- a/libs/std/collections/hashset.cm +++ b/libs/std/collections/hashset.cm @@ -5,7 +5,7 @@ module std.collections.hashset; import std::collections::hashmap::*; export struct HashSet { - HashMap map; + private HashMap map; } export impl HashSet { diff --git a/libs/std/collections/treemap.cm b/libs/std/collections/treemap.cm index db4f5fb6..9c50cc7b 100644 --- a/libs/std/collections/treemap.cm +++ b/libs/std/collections/treemap.cm @@ -22,15 +22,15 @@ module std.collections.treemap; // ジェネリック平衡二分探索木マップ。 // ノードはパラレルな動的配列(keys/vals/left/right/height)+indexで表す。容量はpushで自動拡張される。 export struct TreeMap { - K[] keys; // ノードのキー - V[] vals; // ノードの値 - int[] left; // 左の子のindex(-1で無し) - int[] right; // 右の子のindex(-1で無し) - int[] height; // 部分木の高さ(AVL平衡用) - int[] free_slots; // remove済みノードindex(alloc_nodeが再利用する) - int count; // 確保済みノード数(スロット総数) - int live; // 実要素数(remove対応でcountと分離) - int root; // 根のindex(-1で空) + private K[] keys; // ノードのキー + private V[] vals; // ノードの値 + private int[] left; // 左の子のindex(-1で無し) + private int[] right; // 右の子のindex(-1で無し) + private int[] height; // 部分木の高さ(AVL平衡用) + private int[] free_slots; // remove済みノードindex(alloc_nodeが再利用する) + private int count; // 確保済みノード数(スロット総数) + private int live; // 実要素数(remove対応でcountと分離) + private int root; // 根のindex(-1で空) } export impl TreeMap { @@ -50,7 +50,7 @@ export impl TreeMap { // ---- 内部ヘルパ ---- // ノードの高さ(無しは0)。 - int node_height(int node) { + private int node_height(int node) { if (node < 0) { return 0; } @@ -58,7 +58,7 @@ export impl TreeMap { } // ノードの高さを子から再計算する。 - void update_height(int node) { + private void update_height(int node) { int hl = self.node_height(self.left[node]); int hr = self.node_height(self.right[node]); int m = hl; @@ -69,12 +69,12 @@ export impl TreeMap { } // 平衡係数(左の高さ - 右の高さ)。 - int balance_factor(int node) { + private int balance_factor(int node) { return self.node_height(self.left[node]) - self.node_height(self.right[node]); } // 右回転して新しい部分木の根indexを返す。 - int rotate_right(int y) { + private int rotate_right(int y) { int x = self.left[y]; self.left[y] = self.right[x]; self.right[x] = y; @@ -84,7 +84,7 @@ export impl TreeMap { } // 左回転して新しい部分木の根indexを返す。 - int rotate_left(int x) { + private int rotate_left(int x) { int y = self.right[x]; self.right[x] = self.left[y]; self.left[y] = x; @@ -94,7 +94,7 @@ export impl TreeMap { } // 新しいノードを確保してindexを返す。remove済みスロットがあれば再利用し、無ければスライスへpushする(容量は自動拡張)。 - int alloc_node(K k, V v) { + private int alloc_node(K k, V v) { self.live = self.live + 1; if (self.free_slots.len() > 0) { int idx = self.free_slots.pop(); @@ -116,7 +116,7 @@ export impl TreeMap { } // 部分木の最小キーノードindexを返す(remove時の後継探索用)。 - int min_index(int node) { + private int min_index(int node) { int cur = node; while (self.left[cur] >= 0) { cur = self.left[cur]; @@ -125,7 +125,7 @@ export impl TreeMap { } // 削除後の平衡化(削除は挿入と違いキーでなく子の平衡係数で回転方向を決める)。 - int rebalance_after_remove(int node) { + private int rebalance_after_remove(int node) { self.update_height(node); int b = self.balance_factor(node); if (b > 1) { @@ -146,7 +146,7 @@ export impl TreeMap { } // node を根とする部分木から k を削除し、平衡化後の新しい根indexを返す(kの存在は呼び出し側で確認済み)。 - int remove_at(int node, K k) { + private int remove_at(int node, K k) { if (node < 0) { return -1; } @@ -174,7 +174,7 @@ export impl TreeMap { } // node を根とする部分木へ (k, v) を挿入し、平衡化後の新しい根indexを返す。 - int insert_at(int node, K k, V v) { + private int insert_at(int node, K k, V v) { if (node < 0) { return self.alloc_node(k, v); } @@ -211,7 +211,7 @@ export impl TreeMap { } // キーに対応するノードindexを返す(無ければ -1)。O(log n)。 - int find_index(K k) { + private int find_index(K k) { int cur = self.root; while (cur >= 0) { if (k == self.keys[cur]) { diff --git a/libs/std/collections/treeset.cm b/libs/std/collections/treeset.cm index aee3251a..c997ca0f 100644 --- a/libs/std/collections/treeset.cm +++ b/libs/std/collections/treeset.cm @@ -7,7 +7,7 @@ module std.collections.treeset; import std::collections::treemap::*; export struct TreeSet { - TreeMap map; + private TreeMap map; } export impl TreeSet { diff --git a/libs/std/collections/vector.cm b/libs/std/collections/vector.cm index e00c8dd5..551d4a5c 100644 --- a/libs/std/collections/vector.cm +++ b/libs/std/collections/vector.cm @@ -14,9 +14,9 @@ use libc { // ============================================================= export struct Vector { - T* data; - int size; - int cap; + private T* data; + private int size; + private int cap; } export impl Vector { diff --git a/libs/std/mem/arena.cm b/libs/std/mem/arena.cm index 9715740b..ababdad6 100644 --- a/libs/std/mem/arena.cm +++ b/libs/std/mem/arena.cm @@ -15,9 +15,9 @@ const int ARENA_META_WORDS = 3; const long ARENA_META_BYTES = 24; export struct Arena { - void* head; // 現在のチャンク(未確保ならnull) - long chunk_size; // データ部の既定サイズ(バイト) - long total; // データ部の総確保バイト(統計用) + private void* head; // 現在のチャンク(未確保ならnull) + private long chunk_size; // データ部の既定サイズ(バイト) + private long total; // データ部の総確保バイト(統計用) } export impl Arena { @@ -42,7 +42,7 @@ export impl Arena { } // 新しいチャンクを確保して先頭へ連結する(data_bytesはデータ部のサイズ) - void grow(long data_bytes) { + private void grow(long data_bytes) { void* raw = alloc((ARENA_META_BYTES + data_bytes) as int); long* meta = raw as long*; meta[0] = self.head as long; // next diff --git a/libs/std/mem/smart/atomic.cm b/libs/std/mem/smart/atomic.cm index 5c93c398..94e7a649 100644 --- a/libs/std/mem/smart/atomic.cm +++ b/libs/std/mem/smart/atomic.cm @@ -13,14 +13,15 @@ extern "C" int cm_atomic_fetch_add_i32(int* ptr, int value); extern "C" int cm_atomic_fetch_sub_i32(int* ptr, int value); // 制御ブロック(strong/weakをアトミックに増減する) +// フィールドはAtomicSharedPtrのimplがcount->経由でアトミック操作へ渡すためpublicのまま(privateはimpl粒度の検査のため他型implから触れなくなる) export struct AtomicSharedCount { int strong; int weak; } export struct AtomicSharedPtr { - T* ptr; - AtomicSharedCount* count; + private T* ptr; + private AtomicSharedCount* count; } export impl AtomicSharedPtr { diff --git a/libs/std/mem/smart/mod.cm b/libs/std/mem/smart/mod.cm index 3c97b93f..af808498 100644 --- a/libs/std/mem/smart/mod.cm +++ b/libs/std/mem/smart/mod.cm @@ -20,7 +20,7 @@ import std::mem::{alloc, dealloc}; // ============================================================= export struct UniquePtr { - T* ptr; + private T* ptr; } export impl UniquePtr { @@ -88,11 +88,13 @@ export impl UniquePtr { // 制御ブロック: strong(SharedPtrの数)とweak(WeakPtrの数)を保持する。 // ペイロードはstrong==0で解放、制御ブロック自体はstrong==0かつweak==0で解放する +// フィールドはSharedPtr/WeakPtr両方のimplがcount->経由で増減するためpublicのまま(privateはimpl粒度の検査のため他型implから触れなくなる) export struct SharedCount { int strong; int weak; } +// フィールドはWeakPtr.upgrade()がSharedPtrを構築する際に直接書くためpublicのまま export struct SharedPtr { T* ptr; SharedCount* count; @@ -200,6 +202,7 @@ export impl SharedPtr { // WeakPtr - 弱参照(所有しない参照。循環参照の切断用) // ============================================================= +// フィールドはSharedPtr.downgrade()がWeakPtrを構築する際に直接書くためpublicのまま export struct WeakPtr { T* ptr; SharedCount* count; From 48f16503b9af1188f76db639ec1e87c1b1285cae Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 14:26:42 +0900 Subject: [PATCH 05/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=86=E3=82=A3=E3=83=B3=E3=82=B0=E8=A3=9C=E5=BC=B7?= =?UTF-8?q?:=20=E6=96=87=E5=AD=97=E5=88=97=E5=86=85=E5=AE=B9=E3=83=8F?= =?UTF-8?q?=E3=83=83=E3=82=B7=E3=83=A5=E3=81=AEStringMap/StringSet?= =?UTF-8?q?=E3=83=BB=E5=9F=BA=E6=95=B0=E3=83=95=E3=82=A9=E3=83=BC=E3=83=9E?= =?UTF-8?q?=E3=83=83=E3=82=BF=E3=83=BB=E3=83=92=E3=83=BC=E3=83=97=E3=82=BD?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=83=BBInterner=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=80=81mono=E7=B7=8F=E7=A7=B0=E3=83=98=E3=83=AB=E3=83=91?= =?UTF-8?q?=E3=83=BC=E9=80=A3=E9=8E=96=E3=81=AE=E5=9E=8B=E6=B1=9A=E6=9F=93?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 充足度評価(native/jit)で見つかった不足の実装(P0=正しさ→P1=性能・欠落API→P2=周辺整備、計画docはarchive/v0.17.2/stdlib-hardening.md): P0(正しさ): - HashMapはハッシュがkey as int(文字列では実質ポインタ値)のため実行時生成キーが外れる(実測2000件中ヒット1件)。キーを文字列に固定し内容ハッシュ(FNV-1a)で探索するStringMap/StringSetを新設した(std::collections::strmap/strset) - スライスバックでdtorを持たない構造(dtor持ち暗黙コピー問題を構造的に回避)。密配列+墓石削除・スロット再利用・負荷率50%成長。実行時生成キー10000件全ヒットのパフォーマンステストで担保 - ジェネリックHashMap内での型分岐が現行言語で書けないこと(フリー関数オーバーロード非対応・__typename__(T)のmono時解決なし・デッドブランチが型検査不能)を実験で確認し、設計判断と言語側課題として記録 - std::strings::hashにhash_string(FNV-1a 32bit)を公開 - HashMap/HashSetはintキャスト可能キー専用の制約をヘッダ・チュートリアルに明記 P1(性能・欠落API): - std::strings::formatを新設: to_radix(2〜36進・負数・long最小値も正確)/to_hex/to_bin/to_oct/pad_left/pad_right(parseの逆方向。コード生成の16進出力・桁揃え用) - Vector.sort/sortByを挿入ソート(O(n²))からヒープソート(O(n log n)・in-place・非安定)へ変更し、スライス向けstd::slices(sort/sort_by)を新設。20000要素逆順のパフォーマンステストで担保 P2(周辺整備): - std::strings::internにInterner(文字列→一意id・name_of逆引き)を追加(シンボルテーブルの基礎部品) - 大出力の書き込み経路(StringBuilder一括→write_fileとBufWriter逐次追記)のパフォーマンステストを追加 コンパイラバグ修正(6件目・記録はarchive/v0.17.2/bugfix-mono-generic-helper.md): - importモジュール内の総称free関数から総称ヘルパーを呼ぶとint版が呼ばれソートが無言で無効になる問題を修正 - 真因は総称本体スキャンの推論不能→既定intの偽特殊化リクエストと、生成直後の即時書き換えによるclone前本体の汚染、特殊化名の辞書順による発現(同一ファイルでは偶然無事) - mono不動点ループで総称本体をスキャン対象から外した(呼び出し元の特殊化後に正しい実型で要求される既存機構に委ねる) テスト・ドキュメント: - 機能テスト8件(strmap/strset/hash/format/intern/slices sort/総称ヘルパー連鎖回帰)とパフォーマンステスト3件(strmap 10000件・ソート20000件・書き込み8000行×2経路)を追加 - チュートリアル(ja/en)へStringMap/StringSet・format/hash/intern・ソート変更を反映、リリースノートへ新機能5節とバグ修正1節を追記 - 全スイート通過: interpreter 757・llvm 801・js 766・wasm 757・sv 147・unit/regression VSCode拡張の更新は新構文の追加がないため非該当(ライブラリAPIの追加のみ) --- .../v0.17.2/bugfix-mono-generic-helper.md | 30 +++ docs/archive/v0.17.2/stdlib-hardening.md | 86 +++++++++ docs/design/index.md | 2 + docs/releases/v0.17.2.md | 58 +++++- docs/tutorials/en/stdlib/collections/sets.md | 2 +- .../en/stdlib/strings/chars-parse.md | 57 +++++- docs/tutorials/ja/index.md | 2 +- .../ja/stdlib/collections/hashmap.md | 39 ++++ docs/tutorials/ja/stdlib/collections/sets.md | 2 +- .../tutorials/ja/stdlib/collections/vector.md | 4 +- docs/tutorials/ja/stdlib/index.md | 4 +- .../ja/stdlib/strings/chars-parse.md | 63 +++++- libs/std/collections/hashmap.cm | 1 + libs/std/collections/mod.cm | 4 +- libs/std/collections/strmap.cm | 179 ++++++++++++++++++ libs/std/collections/strset.cm | 53 ++++++ libs/std/collections/vector.cm | 70 +++++-- libs/std/slices/mod.cm | 68 +++++++ libs/std/strings/format.cm | 80 ++++++++ libs/std/strings/hash.cm | 14 ++ libs/std/strings/intern.cm | 50 +++++ libs/std/strings/mod.cm | 3 + src/internal/mir/lowering/mono/driver.cpp | 5 + .../generics/functions/module_helper_chain.cm | 20 ++ .../functions/module_helper_chain.expect | 3 + .../common/stdlib/collections/strmap_basic.cm | 40 ++++ .../stdlib/collections/strmap_basic.expect | 8 + .../common/stdlib/collections/strmap_load.cm | 32 ++++ .../stdlib/collections/strmap_load.expect | 3 + .../common/stdlib/collections/strset_basic.cm | 22 +++ .../stdlib/collections/strset_basic.expect | 4 + tests/common/stdlib/fs/write_perf.cm | 48 +++++ tests/common/stdlib/fs/write_perf.expect | 2 + tests/common/stdlib/slices/sort.cm | 29 +++ tests/common/stdlib/slices/sort.expect | 5 + tests/common/stdlib/slices/sort_load.cm | 20 ++ tests/common/stdlib/slices/sort_load.expect | 1 + tests/common/stdlib/strings/format_radix.cm | 20 ++ .../common/stdlib/strings/format_radix.expect | 7 + tests/common/stdlib/strings/hash_fnv.cm | 21 ++ tests/common/stdlib/strings/hash_fnv.expect | 3 + tests/common/stdlib/strings/intern_basic.cm | 21 ++ .../common/stdlib/strings/intern_basic.expect | 4 + 43 files changed, 1158 insertions(+), 31 deletions(-) create mode 100644 docs/archive/v0.17.2/bugfix-mono-generic-helper.md create mode 100644 docs/archive/v0.17.2/stdlib-hardening.md create mode 100644 libs/std/collections/strmap.cm create mode 100644 libs/std/collections/strset.cm create mode 100644 libs/std/slices/mod.cm create mode 100644 libs/std/strings/format.cm create mode 100644 libs/std/strings/hash.cm create mode 100644 libs/std/strings/intern.cm create mode 100644 tests/common/generics/functions/module_helper_chain.cm create mode 100644 tests/common/generics/functions/module_helper_chain.expect create mode 100644 tests/common/stdlib/collections/strmap_basic.cm create mode 100644 tests/common/stdlib/collections/strmap_basic.expect create mode 100644 tests/common/stdlib/collections/strmap_load.cm create mode 100644 tests/common/stdlib/collections/strmap_load.expect create mode 100644 tests/common/stdlib/collections/strset_basic.cm create mode 100644 tests/common/stdlib/collections/strset_basic.expect create mode 100644 tests/common/stdlib/fs/write_perf.cm create mode 100644 tests/common/stdlib/fs/write_perf.expect create mode 100644 tests/common/stdlib/slices/sort.cm create mode 100644 tests/common/stdlib/slices/sort.expect create mode 100644 tests/common/stdlib/slices/sort_load.cm create mode 100644 tests/common/stdlib/slices/sort_load.expect create mode 100644 tests/common/stdlib/strings/format_radix.cm create mode 100644 tests/common/stdlib/strings/format_radix.expect create mode 100644 tests/common/stdlib/strings/hash_fnv.cm create mode 100644 tests/common/stdlib/strings/hash_fnv.expect create mode 100644 tests/common/stdlib/strings/intern_basic.cm create mode 100644 tests/common/stdlib/strings/intern_basic.expect diff --git a/docs/archive/v0.17.2/bugfix-mono-generic-helper.md b/docs/archive/v0.17.2/bugfix-mono-generic-helper.md new file mode 100644 index 00000000..ac5d9e03 --- /dev/null +++ b/docs/archive/v0.17.2/bugfix-mono-generic-helper.md @@ -0,0 +1,30 @@ +# v0.17.2 バグ修正: 総称本体スキャンの既定int偽特殊化が総称ヘルパー連鎖を汚染する + +セルフホスティング補強(stdlib-hardening)の実装中に発見した8件目の不具合の記録。 +`std::slices::sort`(importしたモジュール内の総称free関数から別の総称ヘルパー`sift_lt`を呼ぶ連鎖)が最初の顕在化点で、v0.17.2で修正済み。 + +## 症状 + +importしたモジュールの総称関数 `sort` を文字列で使うと、内部ヘルパー `sift_lt` が**int版として特殊化・呼び出しされ、ソートが無言で無効になる**(配列が並び替わらない・エラーなし)。 +同じコードをプログラムと同一ファイルに置くと正しく動くため、モジュール分割が引き金に見える。 + +## 真因(3要素の合成) + +1. **総称本体のスキャンによる既定int偽リクエスト**: モノモーフィゼーションのpass 0は全関数(未特殊化の総称本体を含む)をスキャンする。総称 `sort` 本体内の `sift_lt(xs, ...)` は引数型が `T[]`(未確定)のため構造的単一化が失敗し、「推論できなかった場合の既定int」で `sift_lt__int` の特殊化リクエストが作られる(呼び出しサイトは総称`sort`本体)。 +2. **生成直後の即時呼び出し書き換え**: `generate_generic_specializations` は各特殊化を生成した直後にそのリクエストの呼び出しサイトを書き換える。偽リクエストの書き換えが**まだcloneされていない総称 `sort` 本体を `sift_lt__int` 呼びへ汚染**する。 +3. **生成順序が辞書順**: リクエスト表は特殊化名キーのmapで、モジュールprivate改名 `__cm_priv_mod_0_sift_lt__int` は `sort__string` より辞書順で先に処理される。汚染後に `sort__string` がcloneされ、int版ヘルパー呼びを引き継ぐ。同一ファイル版はヘルパー名 `sift__int` が `hsort__string` より後になり、偶然clone が先行して無事だった(=アルファベット順の運で挙動が変わる)。 + +汚染されたclone内の呼び出しは具体名(`__int`)のため次パスのスキャン対象にならず、`sift_lt__string` は永遠に生成されない。 + +## 修正 + +`mono/driver.cpp` の不動点ループで、**総称本体そのものをスキャン対象から外した**。 +総称本体内の総称呼び出しは、呼び出し元が特殊化された後(実型が確定したclone)を次パスで走査したときに正しい型で要求される(既存のpass反復機構がそのまま担う)。 +これにより偽リクエスト(既定int)の発生源が消え、生成順序依存の汚染も起きなくなる。 + +デバッグの決め手は `-d=debug` のMONOログ(`WARNING: Could not infer T, defaulting to int` → `Scanned call in sort ... type args: int` の並び)と、pass 1のスキャンが `sort__string` 内で具体名 `__cm_priv_mod_0_sift_lt__int` を見ている(=総称名が既に消えている)ことを確認する一時診断だった。 + +## 回帰テスト + +- `tests/common/generics/functions/module_helper_chain.cm` — importモジュール内の総称free関数→総称ヘルパー連鎖をstring/long/比較ラムダで検証(本修正で新規追加) +- `tests/common/stdlib/slices/sort.cm` — string[]を含む全型のソート(顕在化点) diff --git a/docs/archive/v0.17.2/stdlib-hardening.md b/docs/archive/v0.17.2/stdlib-hardening.md new file mode 100644 index 00000000..563d067f --- /dev/null +++ b/docs/archive/v0.17.2/stdlib-hardening.md @@ -0,0 +1,86 @@ +# v0.17.2 セルフホスティング補強: 文字列ハッシュ・基数フォーマッタ・ソート・Interner + +標準ライブラリ充足度評価(native/jit)で見つかった不足の実装計画。 +優先度はP0(正しさ)→P1(性能・欠落API)→P2(周辺整備)。 + +## 背景(評価で判明した問題) + +- **P0**: `HashMap` / `HashSet` はハッシュが `key as int`(文字列では実質ポインタ値)のため、実行時生成キーのルックアップが外れる(実測2000件中ヒット1件)。リテラルキー同士はintern共有で偶然一致するため既存テストでは顕在化しない。シンボルテーブル用途が直撃する。 +- **P1**: 数値→基数文字列(to_hex等)が無い(parseの逆方向)。Vector.sort/sortByが挿入ソート(O(n²))で大規模入力に弱く、スライスにはソートAPI自体が無い。 +- **P2**: 大出力向けの書き込み経路(コード生成の出力性能)が未検証。文字列interning(Symbol化)の部品が無い。 + +## 設計判断: 文字列ハッシュはジェネリック分岐でなく専用型で提供する + +ジェネリックな `HashMap` 内でKの型により内容ハッシュへ分岐する案は、現行言語では成立しないことを実験で確認した。 + +- フリー関数オーバーロードは非対応(専用診断で拒否)。 +- `__typename__(T)` はジェネリック文脈ではcheck時に文字列 "T" へ解決され、mono後の実型名にならない(`__sizeof__(T)` のようなmonoマーカー機構が無い)。 +- 仮に実型名で分岐できても、K=intの特殊化で文字列分岐側の `key as string` が型検査を通らない(`int as string` は不可)ため、デッドブランチがコンパイルできない。 + +よって専用型 `StringMap` / `StringSet`(キー型を文字列に固定)で内容ハッシュ(FNV-1a)を提供する。 +`HashMap` の是正(`__typename__` のmono時解決+デッドブランチ剪定、または組み込みハッシュintrinsic)は言語側の設計課題として記録する。 + +## P0: std::strings::hash と StringMap / StringSet + +### std::strings::hash(新設) + +- `export int hash_string(string s)` — FNV-1a 32bit。`byte_len()`/`byte_at(i)` で内容を走査し、long演算+0xFFFFFFFFマスクで32bitラップを再現、最上位ビットを落として非負intを返す。 + +### std::collections::strmap(新設・StringMap) + +HashSetの二重解放(dtor持ち構造体の暗黙コピー)の教訓から、**スライスバックでdtor無し**の構造にする(TreeMapで実証済みのパターン)。 + +- 構造: 密配列 `string[] keys` / `V[] vals` / `bool[] alive` + 探索表 `int[] slots`(密配列indexを保持、-1=空、-2=墓石)+ `int[] free_idx`(削除済み密indexの再利用)。 +- 探索表は線形探索のオープンアドレッシング、負荷率50%で2倍成長(成長時はaliveキーのみ再挿入、密配列は不変)。 +- API: `insert(key, val)` / `get(key) -> Option` / `get_or(key, default)` / `contains` / `remove` / `len` / `is_empty` / `clear` / `keys() -> string[]`(生存キーの挿入順)。 +- 削除は墓石方式+密スロット再利用(vals[idx]への上書き代入で確保済みスロットを再利用し、デフォルト値V()を要求しない)。 + +### std::collections::strset(新設・StringSet) + +`StringMap` の薄ラッパー(insert/contains/remove/len/is_empty/clear/values)。 +StringMapはdtorを持たないため、コンストラクタの `self.map = m;` はTreeSetと同じく安全。 + +### 既存HashMap/HashSetの扱い + +実装は変更しない(intキャスト可能なキー専用として維持)。 +モジュールヘッダとチュートリアルへ「文字列キーは StringMap / StringSet を使う」制約を明記する。 + +## P1: std::strings::format と ヒープソート + +### std::strings::format(新設) + +- `export string to_radix(long v, int base)` — 基数2〜36(小文字、負数は'-'前置)。ulong経由でlong最小値も正しく扱う。 +- `export string to_hex(long v)` / `to_bin(long v)` / `to_oct(long v)` — to_radixの別名。 +- `export string pad_left(string s, int width, char fill)` / `pad_right` — 幅揃え(コード生成の桁揃え用)。 +- 実装は `utiny[]` へ逆順に桁を詰めて反転し `from_bytes` で文字列化(O(桁数))。 + +### ソートのO(n log n)化 + +- `Vector.sort()` / `sortBy(cmp)` をヒープソートへ変更(in-place・追加確保なし・非安定)。`sort()` は `<` 比較、`sortBy` は比較関数。private sift-downヘルパーを比較方式ごとに持つ。 +- `std::slices`(新設): `export void sort(T[] xs)` / `sort_by(T[] xs, int*(T, T) cmp)` — スライス向けの同アルゴリズム。 +- 非安定ソートであることと、所有権型(dtor持ち)要素は要素コピーが発生するため対象外であることをコメントで明記。 + +## P2: 書き込み性能の検証 と Interner + +### 大出力の書き込み検証 + +- StringBuilder(償却O(1)追記)で数MB級のテキストを構築し `write_file` で書き出すパフォーマンステストを追加(コード生成の出力経路の回帰検知)。 +- `BufWriter`(native::io::stream)の行追記経路も同量で検証する。 + +### std::strings::intern(新設・Interner) + +- `export struct Interner { private StringMap ids; private string[] names; }` +- `intern(s) -> int`(既存なら同じid、新規なら採番)/ `name_of(id) -> string` / `len()`。 +- コンパイラのシンボルテーブル・識別子管理の基礎部品。 + +## テスト計画 + +- 機能: strmap(挿入・上書き・削除・墓石再利用・成長・keys順序)、strset、hash_string(既知値・空文字列)、format(基数・負数・パディング・long境界)、slices/Vectorソート(逆順・重複・既ソート・1要素・空)、intern(重複intern・name_of往復)。 +- 正しさ回帰: **実行時生成キー**(補間で構築)でのStringMapルックアップ2000件全ヒット(P0バグの再現条件)。 +- パフォーマンス: StringMap 10000件挿入・参照スモーク、ソート20000件(挿入ソートではタイムアウトする規模)、書き込み数MB。 +- 全バックエンドスイート(interpreter/llvm/js/wasm/sv)通過。 + +## 言語側の設計課題(今回はスコープ外として記録) + +- `__typename__(T)` のmono時解決(sizeof_for_Tマーカーと同型の機構)と、mono時定数条件によるデッドブランチ剪定。両方が揃うと `HashMap` 内での型分岐ハッシュが書けるようになり、StringMapをHashMapへ統合できる。 +- フリー関数オーバーロード(意図的に非対応としている現状仕様の再検討)。 diff --git a/docs/design/index.md b/docs/design/index.md index d61054fe..dcbfa6fb 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -29,6 +29,8 @@ Cm言語コンパイラの設計文書の一覧。実装が完了した設計文 | [バグ修正: ユーザ定義Optionの衝突波及](../archive/v0.17.2/bugfix-option-namespace-collision.html) | 選択importの即時型検査とプレリュードOptionのフラット上書きの合成問題の記録(ライブラリ構成で回避) | | [バグ修正: ポインタ経由参照のみの構造体のDCE誤削除](../archive/v0.17.2/bugfix-aot-struct-dce.html) | AOT限定で不正IRになるプログラムDCEの使用中struct収集漏れの記録(修正完了) | | [バグ記録: JSのスライスポインタ引数未対応](../archive/v0.17.2/bugfix-js-slice-pointer-arg.html) | K[]*引数の再帰渡しがJSバックエンドで壊れる制限の記録(ライブラリ側は反復走査で回避・バックエンド側は残課題) | +| [セルフホスティング補強: 文字列ハッシュ・フォーマッタ・ソート・Interner](../archive/v0.17.2/stdlib-hardening.html) | StringMap/StringSet(内容ハッシュ)・to_radix/pad・ヒープソート・Interner・書き込み性能検証の実装計画(実装完了) | +| [バグ修正: 総称ヘルパー連鎖の既定int汚染](../archive/v0.17.2/bugfix-mono-generic-helper.html) | 総称本体スキャンの偽特殊化がclone前の本体を汚染しint版ヘルパーが呼ばれる問題の記録(修正完了) | ## v0.17.1 設計(実装済み・アーカイブ済み) diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index 01a3ca30..febd2100 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -121,6 +121,56 @@ Vectorは容量の事前確保・末尾参照・任意順序のソートがで v.sortBy((int a, int b) => b - a); // 降順 ``` +### 文字列キーの連想配列と集合 StringMap / StringSet(std::collections) + +`HashMap` はハッシュが `key as int`(文字列では実質ポインタ値)のため、実行時生成キーのルックアップが外れる正しさ問題があった(実測2000件中ヒット1件。リテラル同士はintern共有で偶然一致するため既存テストでは顕在化しない)。 +キーを文字列に固定し内容ハッシュ(FNV-1a)で探索する `StringMap` / `StringSet` を追加した([設計判断の記録](../archive/v0.17.2/stdlib-hardening.html))。 + +```cm +StringMap m(); +m.insert("symbol_{i}", i); // 実行時生成キーで +int v = m.get_or("symbol_{i}", -1); // 正しくヒットする +``` + +- スライスバックでdtorを持たない構造(dtor持ち構造体の暗黙コピー問題を構造的に回避)。密配列+墓石方式の削除・スロット再利用・負荷率50%自動成長。 +- `insert/get/get_or/contains/remove/len/is_empty/clear/keys_in_order`(StringSetは values_in_order)。 +- HashMap/HashSetは「intへキャスト可能なキー専用」としてヘッダとチュートリアルに制約を明記した。 +- ジェネリックHashMap内での型分岐(K=stringで内容ハッシュへ)は現行言語では書けないことを確認し(フリー関数オーバーロード非対応・`__typename__(T)`のmono時解決なし)、言語側の設計課題として記録した。 + +### 文字列の内容ハッシュ(std::strings::hash) + +`export int hash_string(string s)` — FNV-1a 32bitの内容ハッシュ(非負int)。StringMap/Internerの基礎部品で、単体でも利用できる。 + +### 数値→基数文字列(std::strings::format) + +数値解析(parse)の逆方向が無く、コード生成の16進出力・桁揃えができなかった。 +`to_radix(v, base)`(2〜36進・負数対応・long最小値も正確)・`to_hex/to_bin/to_oct`・`pad_left/pad_right` を追加した。 + +```cm +to_hex(255) // "ff" +pad_left(to_hex(255), 4, '0') // "00ff" +to_radix(-9223372036854775807 - 1, 16) // "-8000000000000000" +``` + +### ソートのO(n log n)化とスライスソート(std::slices / Vector) + +Vectorのsort/sortByは挿入ソート(O(n²))で大規模入力に弱く、スライスにはソートAPI自体が無かった。 +両者をヒープソート(in-place・追加確保なし・非安定)へ統一し、スライス向けに `std::slices` を新設した。 + +```cm +import std::slices::*; +int[] xs = [5, 1, 4]; +sort(xs); // 昇順 +sort_by(xs, (int a, int b) => b - a); // 比較関数 +``` + +20000要素の逆順ソート(挿入ソートでは約2億比較で退化する規模)のパフォーマンステストで担保する。 + +### 文字列interning Interner(std::strings::intern) + +コンパイラのシンボルテーブルの基礎部品として、同一内容の文字列へ一意な整数idを割り当てる `Interner` を追加した(`intern/contains/name_of/len`)。 +idの比較は整数比較になり、`name_of` で内容を引き戻せる。 + ### ファイルの行読み(std::fs::read_lines) ファイルを行単位で処理するには `read_file` + `lines` の組み合わせを毎回書く必要があった。 @@ -128,7 +178,7 @@ v.sortBy((int a, int b) => b - a); // 降順 ## 🐛 コンパイラバグ修正(標準ライブラリ実装で顕在化) -新しい標準ライブラリが最初の実利用者となり、計4件の潜在コンパイラバグを発見・修正した([ジェネリック特殊化まわり3件の記録](../archive/v0.17.2/bugfix-generic-mir.html)・[DCE誤削除の記録](../archive/v0.17.2/bugfix-aot-struct-dce.html))。 +新しい標準ライブラリが最初の実利用者となり、計5件の潜在コンパイラバグを発見・修正した([ジェネリック特殊化まわり3件の記録](../archive/v0.17.2/bugfix-generic-mir.html)・[DCE誤削除の記録](../archive/v0.17.2/bugfix-aot-struct-dce.html)・[総称ヘルパー連鎖の記録](../archive/v0.17.2/bugfix-mono-generic-helper.html))。 ### フィールドレシーバのメソッド呼び出しがコピーに実行される @@ -145,6 +195,12 @@ MIRのself引数処理へ場所化分岐(`lower_place`)を追加し、実体 ジェネリックメソッドから `K[]` を返すと要素読みが交互にゴミ値になった(`TreeMap.keys_in_order` が顕在化点)。 mono特殊化でターミネータにも型置換を適用し、スライス生成の要素サイズ定数を置換後の型から再計算するようにした。 +### 総称ヘルパー連鎖が既定intの偽特殊化で汚染される + +importしたモジュール内の総称関数(`sort`)から別の総称ヘルパーを呼ぶと、ヘルパーがint版として特殊化されソートが無言で無効になった([記録](../archive/v0.17.2/bugfix-mono-generic-helper.html))。 +総称本体のスキャンが型未確定の呼び出しへ「既定int」の偽リクエストを作り、生成直後の即時書き換えがclone前の総称本体を汚染していた(同一ファイルでは特殊化名の辞書順の偶然で動いていた)。 +モノモーフィゼーションの不動点ループで総称本体をスキャン対象から外して修正した(呼び出し元の特殊化後に正しい実型で要求される)。 + ### ポインタ経由参照のみの構造体がAOTで消え不正IRになる `cm run` では動くプログラムが `cm compile` だけ「Load operand must be a pointer」の検証エラーで失敗した(`AtomicSharedPtr` の制御ブロックが顕在化点。[記録](../archive/v0.17.2/bugfix-aot-struct-dce.html))。 diff --git a/docs/tutorials/en/stdlib/collections/sets.md b/docs/tutorials/en/stdlib/collections/sets.md index 02ba90a4..5abafa09 100644 --- a/docs/tutorials/en/stdlib/collections/sets.md +++ b/docs/tutorials/en/stdlib/collections/sets.md @@ -83,7 +83,7 @@ int main() { | `len()` / `is_empty()` | `int` / `bool` | Element count / emptiness | | `clear()` | `void` | Remove all elements | -Element types castable to `int` are recommended (same simple hash as HashMap). +The element type must be castable to `int` (same simple hash as HashMap). For strings use the content-hashing `StringSet` (`std::collections::strset`, same API plus `values_in_order`, v0.17.2). --- diff --git a/docs/tutorials/en/stdlib/strings/chars-parse.md b/docs/tutorials/en/stdlib/strings/chars-parse.md index 197b70cc..fa233f1b 100644 --- a/docs/tutorials/en/stdlib/strings/chars-parse.md +++ b/docs/tutorials/en/stdlib/strings/chars-parse.md @@ -1,12 +1,12 @@ --- -title: Character Classification & Number Parsing +title: Character Classification, Parsing & Formatting --- [日本語](../../../ja/stdlib/strings/chars-parse.html) -# std::strings::chars / parse - Character Classification & Number Parsing +# std::strings - Classification, Parsing, Formatting, Hashing & Interning -Building blocks for lexers and text processing: single-character classification/conversion (`chars`) and string-to-number parsing (`parse`), added in v0.17.2 (the parse functions moved from std::io). +Building blocks for lexers and text processing: single-character classification/conversion (`chars`), string-to-number parsing (`parse`), number-to-radix-string formatting (`format`), content hashing (`hash`) and string interning (`intern`), added in v0.17.2 (the parse functions moved from std::io). > **Supported backends:** chars runs everywhere (`digit_value` excluded on SV); parse runs everywhere except SV (Option-returning APIs). @@ -71,6 +71,57 @@ int main() { --- +## std::strings::format + +The reverse of `parse`: number-to-string in a given radix, plus width padding for code generation output. + +```cm +import std::strings::format::*; + +to_hex(255) // "ff" +to_radix(35 as long, 36) // "z" +pad_left(to_hex(255), 4, '0') // "00ff" +``` + +| Function | Description | +|----------|-------------| +| `to_radix(v, base)` | Radix 2-36 (lowercase, `-` prefix for negatives, exact for long min; empty string for invalid radix) | +| `to_hex(v)` / `to_bin(v)` / `to_oct(v)` | Hex / binary / octal | +| `pad_left(s, width, fill)` / `pad_right` | Width padding (returned unchanged when already wide enough) | + +--- + +## std::strings::hash + +FNV-1a 32-bit content hash; identical contents hash identically regardless of how the string was built. The foundation of `StringMap` / `StringSet` / `Interner`. + +```cm +import std::strings::hash::*; +int h = hash_string("hello"); // non-negative, content-based +``` + +--- + +## std::strings::intern + +Assigns a unique integer id (symbol) to each distinct string content — the basis for compiler symbol tables; id comparison is O(1). + +```cm +import std::strings::intern::*; + +Interner it(); +const int a = it.intern("foo"); // 0 (newly numbered) +const int b = it.intern("fo" + "o"); // 0 (same content, same id) +string s = it.name_of(a); // "foo" +``` + +| Method | Description | +|--------|-------------| +| `intern(s)` / `contains(s)` | Get-or-assign id / check without interning | +| `name_of(id)` / `len()` | Reverse lookup (empty when out of range) / symbol count | + +--- + **See also:** [StringBuilder](builder.html) · [String Length](length.html) --- diff --git a/docs/tutorials/ja/index.md b/docs/tutorials/ja/index.md index 5f7a8cc7..71f0680a 100644 --- a/docs/tutorials/ja/index.md +++ b/docs/tutorials/ja/index.md @@ -81,7 +81,7 @@ Cm言語の全機能を段階的に学べる包括的なチュートリアル集 - [HashMap](stdlib/collections/hashmap.html) - 連想配列 - [TreeMap](stdlib/collections/treemap.html) - 順序付きマップ(AVL木) - [TreeSet / HashSet](stdlib/collections/sets.html) - 集合(順序付き/ハッシュ・v0.17.2) - - [文字分類と数値解析](stdlib/strings/chars-parse.html) - chars/parse(v0.17.2) + - [文字分類・数値解析・フォーマット](stdlib/strings/chars-parse.html) - chars/parse/format/hash/intern(v0.17.2) - [HTTP通信](stdlib/http.html) - HttpClient/HttpServer/HTTPS - [TCP/UDP通信](stdlib/network/tcp.html) - ソケット/DNS/poll - [並行処理](stdlib/concurrency/) - スレッド/Mutex/Channel/Atomic diff --git a/docs/tutorials/ja/stdlib/collections/hashmap.md b/docs/tutorials/ja/stdlib/collections/hashmap.md index 27b2b97b..5a7e2b11 100644 --- a/docs/tutorials/ja/stdlib/collections/hashmap.md +++ b/docs/tutorials/ja/stdlib/collections/hashmap.md @@ -6,6 +6,8 @@ title: HashMap `HashMap` はジェネリックなキー・バリュー連想配列です。 +> **キー型の制約:** ハッシュは `key as int` のため、**intへキャスト可能なキー専用**です。文字列キーはポインタ値ハッシュになり実行時生成キーが外れるため、[StringMap / StringSet](#stringmap---文字列キー連想配列v0172) を使ってください。 + > **対応バックエンド:** Native (LLVM) のみ **最終更新:** 2026-02-08 @@ -123,5 +125,42 @@ entries: [ ][K1:V1][ ][ ][K2:V2][ ][K3:V3][ ]... --- +## StringMap - 文字列キー連想配列(v0.17.2) + +キーを文字列に固定し、内容ハッシュ(FNV-1a)で探索する連想配列です。同一内容の文字列は生成方法(リテラル・連結・補間)によらず正しくヒットします。 + +```cm +import std::collections::strmap::*; +import std::io::println; + +int main() { + StringMap m(); + for (int i = 0; i < 100; i++) { + m.insert("symbol_{i}", i); // 実行時生成キー + } + println("{m.get_or(\"symbol_42\", -1)}"); // 42 + println("{m.contains(\"sym\" + \"bol_7\")}"); // true(内容ハッシュ) + + m.remove("symbol_0"); + string[] ks = m.keys_in_order(); // 生存キーの挿入順 + println("len={m.len()} keys={ks.len()}"); + return 0; +} +``` + +| メソッド | 戻り値 | 説明 | +|---------|--------|------| +| `insert(key, value)` | `void` | 挿入(既存キーは上書き)。平均O(1)・負荷率50%で自動拡張 | +| `get(key)` | `Option` | 取得(不在は`None`) | +| `get_or(key, default)` | `V` | 取得(不在時は`default`) | +| `contains(key)` / `remove(key)` | `bool` / `void` | 存在確認 / 削除(墓石方式・スロット再利用) | +| `keys_in_order()` | `string[]` | 生存キーを挿入順で返す | +| `len()` / `is_empty()` / `clear()` | - | 要素数 / 空判定 / 全削除 | + +スライスバックでデストラクタを持たないため、他の構造体のフィールドとしても安全に使えます(全バックエンド対応)。 +集合が欲しい場合は `StringSet`(`std::collections::strset`)を使います。 + +--- + ← 前: [std::collections::queue - FIFOキュー](queue.html) | [目次](../index.html) | 次: [TreeMap - 順序付きマップ](treemap.html) → diff --git a/docs/tutorials/ja/stdlib/collections/sets.md b/docs/tutorials/ja/stdlib/collections/sets.md index c053d660..bb4a68aa 100644 --- a/docs/tutorials/ja/stdlib/collections/sets.md +++ b/docs/tutorials/ja/stdlib/collections/sets.md @@ -91,7 +91,7 @@ int main() { | `is_empty()` | `bool` | 空か | | `clear()` | `void` | 全要素を削除 | -値型 `T` は `int` にキャスト可能な型が推奨です(HashMapと同じ簡易ハッシュ)。 +値型 `T` は `int` にキャスト可能な型**専用**です(HashMapと同じ簡易ハッシュ)。文字列は内容ハッシュの `StringSet`(`std::collections::strset`・API同一+`values_in_order`)を使ってください(v0.17.2)。 --- diff --git a/docs/tutorials/ja/stdlib/collections/vector.md b/docs/tutorials/ja/stdlib/collections/vector.md index 062e4f20..d6040dbf 100644 --- a/docs/tutorials/ja/stdlib/collections/vector.md +++ b/docs/tutorials/ja/stdlib/collections/vector.md @@ -52,8 +52,8 @@ int main() { | `clear()` | `void` | 全要素削除(メモリは保持) | | `reserve(n)` | `void` | 容量をn以上へ事前拡張(縮小はしない)(v0.17.2) | | `last()` | `T*` | 末尾要素へのポインタ(popせず参照)(v0.17.2) | -| `sort()` | `void` | 昇順ソート(挿入ソート・プリミティブ型のみ) | -| `sortBy(cmp)` | `void` | 比較関数`int*(T, T)`によるソート(v0.17.2) | +| `sort()` | `void` | 昇順ソート(ヒープソートO(n log n)・非安定・プリミティブ型のみ) | +| `sortBy(cmp)` | `void` | 比較関数`int*(T, T)`によるソート(ヒープソート・非安定。v0.17.2) | --- diff --git a/docs/tutorials/ja/stdlib/index.md b/docs/tutorials/ja/stdlib/index.md index 4e2c6765..60d10b42 100644 --- a/docs/tutorials/ja/stdlib/index.md +++ b/docs/tutorials/ja/stdlib/index.md @@ -72,6 +72,8 @@ Cm標準ライブラリのモジュール群です。 | `std::collections::hashmap` | `HashMap` ハッシュマップ(getはOption返し) | [HashMap](collections/hashmap.html) | | `std::collections::treemap` | `TreeMap` 順序付きマップ(AVL木・O(log n)・remove/keys_in_order対応) | [TreeMap](collections/treemap.html) | | `std::collections::treeset` / `hashset` | `TreeSet` 順序付き集合・`HashSet` ハッシュ集合(v0.17.2) | [TreeSet / HashSet](collections/sets.html) | +| `std::collections::strmap` / `strset` | `StringMap` / `StringSet` 文字列キー・内容ハッシュ(v0.17.2) | [HashMap内のStringMap節](collections/hashmap.html) | +| `std::slices` | スライスのヒープソート sort / sort_by(v0.17.2) | [Vector内のソート節](collections/vector.html) | --- @@ -80,7 +82,7 @@ Cm標準ライブラリのモジュール群です。 | モジュール | 説明 | ドキュメント | |-----------|------|------------| | `std::strings::builder` | `StringBuilder` 可変文字列バッファ(償却O(1)追記) | [StringBuilder](strings/builder.html) | -| `std::strings::chars` / `parse` | 文字分類・変換と数値解析(基数対応・Option返し。v0.17.2) | [文字分類と数値解析](strings/chars-parse.html) | +| `std::strings::chars` / `parse` / `format` / `hash` / `intern` | 文字分類・数値解析・基数フォーマット・内容ハッシュ・interning(v0.17.2) | [文字分類・数値解析・フォーマット](strings/chars-parse.html) | | 文字列組み込み | `len()`(コードポイント数)/ `byte_len()`(バイト数) | [文字列の長さ](strings/length.html) | --- diff --git a/docs/tutorials/ja/stdlib/strings/chars-parse.md b/docs/tutorials/ja/stdlib/strings/chars-parse.md index 34d41070..fe12314a 100644 --- a/docs/tutorials/ja/stdlib/strings/chars-parse.md +++ b/docs/tutorials/ja/stdlib/strings/chars-parse.md @@ -1,12 +1,12 @@ --- -title: 文字分類と数値解析 +title: 文字分類・数値解析・フォーマット --- [English](../../../en/stdlib/strings/chars-parse.html) -# std::strings::chars / parse - 文字分類と数値解析 +# std::strings - 文字分類・数値解析・フォーマット・ハッシュ・interning -字句解析やテキスト処理の基礎部品として、文字1つの分類・変換(`chars`)と文字列から数値への解析(`parse`)を提供します(v0.17.2で追加。parse系はstd::ioから移設)。 +字句解析やテキスト処理の基礎部品として、文字1つの分類・変換(`chars`)、文字列から数値への解析(`parse`)、数値→基数文字列(`format`)、内容ハッシュ(`hash`)、文字列interning(`intern`)を提供します(v0.17.2で追加。parse系はstd::ioから移設)。 > **対応バックエンド:** charsは全バックエンド(`digit_value`のみSV除く)、parseはSV以外(`Option`返しAPIのため) @@ -90,6 +90,63 @@ int main() { --- +## std::strings::format - 数値→基数文字列と幅揃え + +`parse` の逆方向です。コード生成の16進出力・桁揃えに使います。 + +```cm +import std::strings::format::*; + +to_hex(255) // "ff" +to_hex(0 - 255) // "-ff" +to_bin(10) // "1010" +to_radix(35 as long, 36) // "z" +pad_left(to_hex(255), 4, '0') // "00ff" +pad_right("ab", 5, '.') // "ab..." +``` + +| 関数 | 説明 | +|------|------| +| `to_radix(v, base)` | 基数2〜36の文字列(小文字・負数は'-'前置・long最小値も正確。基数範囲外は空文字列) | +| `to_hex(v)` / `to_bin(v)` / `to_oct(v)` | 16進 / 2進 / 8進 | +| `pad_left(s, width, fill)` / `pad_right` | 幅揃え(既にwidth以上ならそのまま) | + +--- + +## std::strings::hash - 内容ハッシュ + +FNV-1a 32bitの内容ハッシュです。同一内容の文字列は生成方法(リテラル・連結・補間)によらず同じ値を返します。`StringMap` / `StringSet` / `Interner` の基礎部品です。 + +```cm +import std::strings::hash::*; + +int h = hash_string("hello"); // 非負int(内容ベース) +``` + +--- + +## std::strings::intern - 文字列interning(Symbol化) + +同一内容の文字列へ一意な整数id(symbol)を割り当てます。コンパイラのシンボルテーブル・識別子管理の基礎部品で、idの比較は整数比較(O(1))になります。 + +```cm +import std::strings::intern::*; + +Interner it(); +const int a = it.intern("foo"); // 0(新規採番) +const int b = it.intern("fo" + "o"); // 0(同一内容 → 同じid) +string s = it.name_of(a); // "foo" +``` + +| メソッド | 説明 | +|---------|------| +| `intern(s)` | idを返す(既存なら同じid、新規なら連番採番) | +| `contains(s)` | internせず登録済みかを確認 | +| `name_of(id)` | idから文字列を引き戻す(範囲外は空文字列) | +| `len()` | 登録済みシンボル数 | + +--- + **関連:** [StringBuilder](builder.html) · [文字列の長さ](length.html) --- diff --git a/libs/std/collections/hashmap.cm b/libs/std/collections/hashmap.cm index 7762a408..e80295ac 100644 --- a/libs/std/collections/hashmap.cm +++ b/libs/std/collections/hashmap.cm @@ -1,5 +1,6 @@ // std::collections::hashmap - ジェネリック連想配列 // HashMap - キーと値の両方がジェネリック +// 制約: ハッシュは `key as int` のため、intへキャスト可能なキー専用。文字列キーはポインタ値ハッシュになり実行時生成キーが外れるため、StringMap / StringSet(std::collections::strmap / strset)を使うこと module std.collections.hashmap; // 確保はstd::mem経由にして、set_allocator_fnsで登録したカスタムアロケータを通す(R9。従来は生malloc直呼びで素通しだった) diff --git a/libs/std/collections/mod.cm b/libs/std/collections/mod.cm index a86ee747..b62593b3 100644 --- a/libs/std/collections/mod.cm +++ b/libs/std/collections/mod.cm @@ -1,5 +1,5 @@ // std::collections - コレクション型 -// Vector, HashMap, Queue, TreeMap(平衡二分探索木・O(log n)), HashSet, TreeSet +// Vector, HashMap, Queue, TreeMap(平衡二分探索木・O(log n)), HashSet, TreeSet, StringMap/StringSet(文字列キー・内容ハッシュ) module std.collections; export import std.collections.vector; @@ -8,3 +8,5 @@ export import std.collections.queue; export import std.collections.treemap; export import std.collections.hashset; export import std.collections.treeset; +export import std.collections.strmap; +export import std.collections.strset; diff --git a/libs/std/collections/strmap.cm b/libs/std/collections/strmap.cm new file mode 100644 index 00000000..534591e3 --- /dev/null +++ b/libs/std/collections/strmap.cm @@ -0,0 +1,179 @@ +// std::collections::strmap - 文字列キー連想配列 +// StringMap - キーを文字列に固定し、内容ハッシュ(FNV-1a)で探索する。 +// HashMapのハッシュ(key as int)は文字列では実質ポインタ値になり実行時生成キーが外れるため、文字列キーは本型を使う。 +// スライスバックでデストラクタを持たない構造(dtor持ち構造体の暗黙コピーによる二重解放を構造的に避ける。TreeMapで実証済みのパターン) +module std.collections.strmap; + +import std::strings::hash::*; + +export struct StringMap { + private string[] keys; // 密配列: キー(挿入順・削除スロットは再利用) + private V[] vals; // 密配列: 値 + private bool[] alive; // 密配列: 生存フラグ(remove済みはfalse) + private int[] slots; // 探索表: 密配列index(-1=空、-2=墓石) + private int[] free_idx; // 削除済み密indexの再利用リスト + private int cap; // 探索表のスロット数 + private int size; // 生存要素数 +} + +export impl StringMap { + // 空のマップを作る(探索表16スロット) + self() { + self.keys = []; + self.vals = []; + self.alive = []; + self.slots = []; + self.free_idx = []; + self.cap = 16; + self.size = 0; + for (int i = 0; i < 16; i++) { + self.slots.push(-1); + } + } + + // keyの現在スロット位置(生存一致)を返す。不在は-1 + private int find_slot(string key) { + const int h = hash_string(key); + for (int i = 0; i < self.cap; i++) { + const int pos = (h + i) % self.cap; + const int idx = self.slots[pos]; + if (idx == -1) { + return -1; + } + if (idx >= 0 && self.alive[idx] && self.keys[idx] == key) { + return pos; + } + } + return -1; + } + + // 探索表を2倍へ拡張し、生存キーのみ再挿入する(密配列は不変) + private void grow() { + const int new_cap = self.cap * 2; + int[] new_slots = []; + for (int i = 0; i < new_cap; i++) { + new_slots.push(-1); + } + for (int idx = 0; idx < self.keys.len(); idx++) { + if (!self.alive[idx]) { + continue; + } + const int h = hash_string(self.keys[idx]); + for (int i = 0; i < new_cap; i++) { + const int pos = (h + i) % new_cap; + if (new_slots[pos] == -1) { + new_slots[pos] = idx; + break; + } + } + } + self.slots = new_slots; + self.cap = new_cap; + } + + // 挿入(既存キーは上書き)。負荷率50%で自動拡張 + void insert(string key, V value) { + const int existing = self.find_slot(key); + if (existing >= 0) { + self.vals[self.slots[existing]] = value; + return; + } + if ((self.size + 1) * 2 >= self.cap) { + self.grow(); + } + // 密スロットを確保(削除済みがあれば上書き再利用、無ければ末尾へ追加) + int idx = -1; + if (self.free_idx.len() > 0) { + idx = self.free_idx.pop(); + self.keys[idx] = key; + self.vals[idx] = value; + self.alive[idx] = true; + } else { + idx = self.keys.len(); + self.keys.push(key); + self.vals.push(value); + self.alive.push(true); + } + // 探索表へ登録(空きまたは墓石を再利用) + const int h = hash_string(key); + for (int i = 0; i < self.cap; i++) { + const int pos = (h + i) % self.cap; + if (self.slots[pos] == -1 || self.slots[pos] == -2) { + self.slots[pos] = idx; + break; + } + } + self.size = self.size + 1; + } + + // 取得(不在はNone) + Option get(string key) { + const int pos = self.find_slot(key); + if (pos < 0) { + return Option::None; + } + return Option::Some(self.vals[self.slots[pos]]); + } + + // 取得(不在時はdefault_valを返す非Option版) + V get_or(string key, V default_val) { + const int pos = self.find_slot(key); + if (pos < 0) { + return default_val; + } + return self.vals[self.slots[pos]]; + } + + // 存在確認 + bool contains(string key) { + return self.find_slot(key) >= 0; + } + + // 削除(不在キーは無視)。スロットは墓石化し、密indexは再利用リストへ + void remove(string key) { + const int pos = self.find_slot(key); + if (pos < 0) { + return; + } + const int idx = self.slots[pos]; + self.slots[pos] = -2; + self.alive[idx] = false; + self.free_idx.push(idx); + self.size = self.size - 1; + } + + // 生存キーを挿入順で返す + string[] keys_in_order() { + string[] out = []; + for (int idx = 0; idx < self.keys.len(); idx++) { + if (self.alive[idx]) { + out.push(self.keys[idx]); + } + } + return out; + } + + // 生存要素数 + int len() { + return self.size; + } + + // 空か + bool is_empty() { + return self.size == 0; + } + + // 全要素を削除する + void clear() { + self.keys = []; + self.vals = []; + self.alive = []; + self.free_idx = []; + self.slots = []; + self.cap = 16; + self.size = 0; + for (int i = 0; i < 16; i++) { + self.slots.push(-1); + } + } +} diff --git a/libs/std/collections/strset.cm b/libs/std/collections/strset.cm new file mode 100644 index 00000000..338883bd --- /dev/null +++ b/libs/std/collections/strset.cm @@ -0,0 +1,53 @@ +// std::collections::strset - 文字列集合 +// StringMapのキーのみ薄ラッパー。内容ハッシュで挿入・存在判定・削除を行う +// (HashSetはハッシュがポインタ値になり実行時生成キーが外れるため、文字列は本型を使う) +module std.collections.strset; + +import std::collections::strmap::*; + +export struct StringSet { + private StringMap map; +} + +export impl StringSet { + // 空の集合を作る(StringMapはdtorを持たないスライスバック構造のため、値コピーで安全に初期化できる) + self() { + StringMap m(); + self.map = m; + } + + // 値を追加する(既存なら何もしない) + void insert(string value) { + self.map.insert(value, true); + } + + // 値が含まれるか + bool contains(string value) { + return self.map.contains(value); + } + + // 値を削除する(存在しなければ何もしない) + void remove(string value) { + self.map.remove(value); + } + + // 要素数 + int len() { + return self.map.len(); + } + + // 空か + bool is_empty() { + return self.map.is_empty(); + } + + // 全要素を削除する + void clear() { + self.map.clear(); + } + + // 生存要素を挿入順で返す + string[] values_in_order() { + return self.map.keys_in_order(); + } +} diff --git a/libs/std/collections/vector.cm b/libs/std/collections/vector.cm index 551d4a5c..22ed8b6b 100644 --- a/libs/std/collections/vector.cm +++ b/libs/std/collections/vector.cm @@ -123,32 +123,68 @@ export impl Vector { self.size = 0; } - // ソート(昇順、挿入ソート) + // 内部: data[root..end) をsift-down(<比較・最大ヒープ) + private void sift_lt(int root, int end) { + int r = root; + while (r * 2 + 1 < end) { + int child = r * 2 + 1; + if (child + 1 < end && self.data[child] < self.data[child + 1]) { + child = child + 1; + } + if (self.data[child] < self.data[r]) { + return; + } + const T tmp = self.data[r]; + self.data[r] = self.data[child]; + self.data[child] = tmp; + r = child; + } + } + + // ソート(昇順、ヒープソート: O(n log n)・追加確保なし・非安定) // プリミティブ型(int, long, double等)で動作 void sort() { const int n = self.size; - for (int i = 1; i < n; i++) { - const T key = self.data[i]; - int j = i - 1; - while (j >= 0 && self.data[j] > key) { - self.data[j + 1] = self.data[j]; - j = j - 1; + for (int i = n / 2 - 1; i >= 0; i--) { + self.sift_lt(i, n); + } + for (int end = n - 1; end > 0; end--) { + const T tmp = self.data[0]; + self.data[0] = self.data[end]; + self.data[end] = tmp; + self.sift_lt(0, end); + } + } + + // 内部: 比較関数版sift-down(cmpが負=aが先) + private void sift_cmp(int root, int end, int*(T, T) cmp) { + int r = root; + while (r * 2 + 1 < end) { + int child = r * 2 + 1; + if (child + 1 < end && cmp(self.data[child], self.data[child + 1]) < 0) { + child = child + 1; } - self.data[j + 1] = key; + if (cmp(self.data[child], self.data[r]) < 0) { + return; + } + const T tmp = self.data[r]; + self.data[r] = self.data[child]; + self.data[child] = tmp; + r = child; } } - // 比較関数によるソート(挿入ソート。cmp(a,b)<0でaがbより前) + // 比較関数によるソート(ヒープソート。cmp(a,b)<0でaがbより前・非安定) void sortBy(int*(T, T) cmp) { const int n = self.size; - for (int i = 1; i < n; i++) { - const T key = self.data[i]; - int j = i - 1; - while (j >= 0 && cmp(self.data[j], key) > 0) { - self.data[j + 1] = self.data[j]; - j = j - 1; - } - self.data[j + 1] = key; + for (int i = n / 2 - 1; i >= 0; i--) { + self.sift_cmp(i, n, cmp); + } + for (int end = n - 1; end > 0; end--) { + const T tmp = self.data[0]; + self.data[0] = self.data[end]; + self.data[end] = tmp; + self.sift_cmp(0, end, cmp); } } diff --git a/libs/std/slices/mod.cm b/libs/std/slices/mod.cm new file mode 100644 index 00000000..5cf7d8da --- /dev/null +++ b/libs/std/slices/mod.cm @@ -0,0 +1,68 @@ +// std::slices - スライスユーティリティ +// スライス(T[])のin-placeソート。ヒープソート(O(n log n)・追加確保なし・非安定) +// 要素はスワップ時にコピーされるため、所有権型(dtor持ち構造体)の要素は対象外 +module std.slices; + +// 内部: xs[root..end) をsift-down(<比較・最大ヒープ) + void sift_lt(T[] xs, int root, int end) { + int r = root; + while (r * 2 + 1 < end) { + int child = r * 2 + 1; + if (child + 1 < end && xs[child] < xs[child + 1]) { + child = child + 1; + } + if (xs[child] < xs[r]) { + return; + } + T tmp = xs[r]; + xs[r] = xs[child]; + xs[child] = tmp; + r = child; + } +} + +// 昇順ソート(<比較が定義された型) +export void sort(T[] xs) { + const int n = xs.len(); + for (int i = n / 2 - 1; i >= 0; i--) { + sift_lt(xs, i, n); + } + for (int end = n - 1; end > 0; end--) { + T tmp = xs[0]; + xs[0] = xs[end]; + xs[end] = tmp; + sift_lt(xs, 0, end); + } +} + +// 内部: 比較関数版sift-down(cmpが負=aが先) + void sift_cmp(T[] xs, int root, int end, int*(T, T) cmp) { + int r = root; + while (r * 2 + 1 < end) { + int child = r * 2 + 1; + if (child + 1 < end && cmp(xs[child], xs[child + 1]) < 0) { + child = child + 1; + } + if (cmp(xs[child], xs[r]) < 0) { + return; + } + T tmp = xs[r]; + xs[r] = xs[child]; + xs[child] = tmp; + r = child; + } +} + +// 比較関数によるソート(負=aが先・正=bが先) +export void sort_by(T[] xs, int*(T, T) cmp) { + const int n = xs.len(); + for (int i = n / 2 - 1; i >= 0; i--) { + sift_cmp(xs, i, n, cmp); + } + for (int end = n - 1; end > 0; end--) { + T tmp = xs[0]; + xs[0] = xs[end]; + xs[end] = tmp; + sift_cmp(xs, 0, end, cmp); + } +} diff --git a/libs/std/strings/format.cm b/libs/std/strings/format.cm new file mode 100644 index 00000000..9f7f1612 --- /dev/null +++ b/libs/std/strings/format.cm @@ -0,0 +1,80 @@ +// std/strings/format.cm - 数値→基数文字列と幅揃え +// std::strings::parse(文字列→数値)の逆方向。コード生成の16進出力・桁揃え等に使う +module std.strings.format; + +import std::strings::from_bytes::*; + +// 基数の数字文字(10以上は小文字・モジュール内部用) +const string FORMAT_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"; + +// 値vを基数base(2〜36)の文字列にする(負数は'-'前置。基数が範囲外なら空文字列) +// long最小値はulong経由で絶対値を取り正しく扱う +export string to_radix(long v, int base) { + if (base < 2 || base > 36) { + return ""; + } + if (v == 0) { + return "0"; + } + const bool neg = v < 0; + ulong mag = v as ulong; + if (neg) { + mag = (0 as ulong) - mag; + } + const ulong ubase = base as ulong; + utiny[] rev = []; + while (mag > (0 as ulong)) { + const int d = (mag % ubase) as int; + rev.push(FORMAT_DIGITS.byte_at(d) as utiny); + mag = mag / ubase; + } + utiny[] buf = []; + if (neg) { + buf.push(45 as utiny); // '-' + } + for (int i = rev.len() - 1; i >= 0; i--) { + buf.push(rev[i]); + } + return from_bytes(buf); +} + +// 16進文字列(小文字・prefix無し) +export string to_hex(long v) { + return to_radix(v, 16); +} + +// 2進文字列 +export string to_bin(long v) { + return to_radix(v, 2); +} + +// 8進文字列 +export string to_oct(long v) { + return to_radix(v, 8); +} + +// 幅widthまで左側をfillで埋める(既にwidth以上ならそのまま) +export string pad_left(string s, int width, char fill) { + int need = width - s.len(); + if (need <= 0) { + return s; + } + utiny[] buf = []; + for (int i = 0; i < need; i++) { + buf.push(fill as utiny); + } + return from_bytes(buf) + s; +} + +// 幅widthまで右側をfillで埋める(既にwidth以上ならそのまま) +export string pad_right(string s, int width, char fill) { + int need = width - s.len(); + if (need <= 0) { + return s; + } + utiny[] buf = []; + for (int i = 0; i < need; i++) { + buf.push(fill as utiny); + } + return s + from_bytes(buf); +} diff --git a/libs/std/strings/hash.cm b/libs/std/strings/hash.cm new file mode 100644 index 00000000..882e8b68 --- /dev/null +++ b/libs/std/strings/hash.cm @@ -0,0 +1,14 @@ +// std/strings/hash.cm - 文字列の内容ハッシュ +// StringMap/StringSet/Internerの基礎部品。同一内容の文字列は生成方法(リテラル・連結・補間)によらず同じ値を返す +module std.strings.hash; + +// FNV-1a 32bitの内容ハッシュ(非負intを返す) +// long演算+0xFFFFFFFFマスクで32bitラップを再現し、最上位ビットを落として非負に揃える +export int hash_string(string s) { + long h = 2166136261; + const int n = s.byte_len(); + for (int i = 0; i < n; i++) { + h = ((h ^ (s.byte_at(i) as long)) * 16777619) & 4294967295; + } + return (h & 2147483647) as int; +} diff --git a/libs/std/strings/intern.cm b/libs/std/strings/intern.cm new file mode 100644 index 00000000..4d0f3524 --- /dev/null +++ b/libs/std/strings/intern.cm @@ -0,0 +1,50 @@ +// std/strings/intern.cm - 文字列interning(Symbol化) +// 同一内容の文字列へ一意な整数id(symbol)を割り当てる。コンパイラのシンボルテーブル・識別子管理の基礎部品 +// idの比較は整数比較(O(1))になり、id→文字列はname_ofで引き戻せる +module std.strings.intern; + +import std::collections::strmap::*; + +export struct Interner { + private StringMap ids; // 内容→id + private string[] names; // id→内容(idは採番順の連番) +} + +export impl Interner { + // 空のInternerを作る(StringMapはdtor無しのスライスバック構造のため値コピーで安全に初期化できる) + self() { + StringMap m(); + self.ids = m; + self.names = []; + } + + // 文字列をintern して idを返す(既存の内容は同じid、新規は連番を採番) + int intern(string s) { + const int found = self.ids.get_or(s, -1); + if (found >= 0) { + return found; + } + const int id = self.names.len(); + self.ids.insert(s, id); + self.names.push(s); + return id; + } + + // 登録済みかを返す(internせず確認のみ) + bool contains(string s) { + return self.ids.contains(s); + } + + // idから文字列を引き戻す(範囲外は空文字列) + string name_of(int id) { + if (id < 0 || id >= self.names.len()) { + return ""; + } + return self.names[id]; + } + + // 登録済みシンボル数 + int len() { + return self.names.len(); + } +} diff --git a/libs/std/strings/mod.cm b/libs/std/strings/mod.cm index 1d1aa24f..e99f809d 100644 --- a/libs/std/strings/mod.cm +++ b/libs/std/strings/mod.cm @@ -8,3 +8,6 @@ export import std.strings.split; export import std.strings.from_bytes; export import std.strings.chars; export import std.strings.parse; +export import std.strings.hash; +export import std.strings.format; +export import std.strings.intern; diff --git a/src/internal/mir/lowering/mono/driver.cpp b/src/internal/mir/lowering/mono/driver.cpp index 174fec68..8bbcd2fe 100644 --- a/src/internal/mir/lowering/mono/driver.cpp +++ b/src/internal/mir/lowering/mono/driver.cpp @@ -82,6 +82,11 @@ void Monomorphization::monomorphize( // 2パス目: 新規生成された特殊化関数のみスキャン if (pass > 0 && all_generated.count(func->name) == 0) continue; + // 総称本体そのものはスキャンしない。総称本体内の総称呼び出しは型パラメータが未確定で + // 推論が既定intへ退化した偽リクエストを作り、その即時書き換えがclone前の総称本体を汚染する + // (呼び出し先は特殊化された呼び出し元を次パスで走査したときに正しい実型で要求される) + if (generic_funcs.count(func->name) > 0) + continue; scan_generic_calls(func.get(), generic_funcs, hir_functions, needed); } diff --git a/tests/common/generics/functions/module_helper_chain.cm b/tests/common/generics/functions/module_helper_chain.cm new file mode 100644 index 00000000..dab80523 --- /dev/null +++ b/tests/common/generics/functions/module_helper_chain.cm @@ -0,0 +1,20 @@ +// v0.17.2バグ修正回帰: importしたモジュール内の総称free関数から別の総称ヘルパーを呼ぶ連鎖の型引数伝播 +// (旧: 総称本体のスキャンが推論不能→既定intの偽特殊化を作り、その即時書き換えがclone前の総称本体を汚染して +// 呼び出し元の実型(string等)でなくint版ヘルパーが呼ばれ、ソート等が無言で無効化されていた) +import std::io::println; +import std::slices::*; + +int main() { + // stringはintと辞書順が異なるため、int版siftが呼ばれると並びが壊れる + string[] s = ["delta", "alpha", "charlie", "bravo"]; + sort(s); + println("{s[0]} {s[1]} {s[2]} {s[3]}"); + + long[] l = [30, 10, 20]; + sort(l); + println("{l[0]} {l[1]} {l[2]}"); + + sort_by(s, (string a, string b) => (a < b) ? 1 : -1); + println("{s[0]} {s[3]}"); + return 0; +} diff --git a/tests/common/generics/functions/module_helper_chain.expect b/tests/common/generics/functions/module_helper_chain.expect new file mode 100644 index 00000000..4926057a --- /dev/null +++ b/tests/common/generics/functions/module_helper_chain.expect @@ -0,0 +1,3 @@ +alpha bravo charlie delta +10 20 30 +delta alpha diff --git a/tests/common/stdlib/collections/strmap_basic.cm b/tests/common/stdlib/collections/strmap_basic.cm new file mode 100644 index 00000000..29f42a22 --- /dev/null +++ b/tests/common/stdlib/collections/strmap_basic.cm @@ -0,0 +1,40 @@ +// StringMap: 挿入・上書き・削除・墓石スロット再利用・成長・挿入順keys +import std::io::println; +import std::collections::strmap::*; + +int main() { + StringMap m(); + m.insert("apple", 1); + m.insert("banana", 2); + m.insert("cherry", 3); + println("len={m.len()} a={m.get_or(\"apple\", -1)} x={m.get_or(\"nope\", -1)}"); + + // 実行時生成キーでも内容ハッシュで一致する(P0バグの再現条件) + string dyn = "app" + "le"; + int i = 2; + println("dyn={m.get_or(dyn, -1)} interp={m.contains(\"banana\")} get={m.get(\"cherry\").unwrap_or(-1)} i{i}=ok"); + + // 上書き + m.insert("apple", 100); + println("overwrite={m.get_or(\"apple\", -1)} len={m.len()}"); + + // 削除と墓石スロット再利用 + m.remove("banana"); + println("removed={m.contains(\"banana\")} len={m.len()}"); + m.insert("date", 4); + println("reuse len={m.len()} date={m.get_or(\"date\", -1)}"); + + // 挿入順keys(生存のみ。bananaの密スロットはdateが再利用) + string[] ks = m.keys_in_order(); + println("keys n={ks.len()} k0={ks[0]} k1={ks[1]} k2={ks[2]}"); + + // 成長をまたぐ挿入(cap16 → 負荷率50%超で拡張) + for (int j = 0; j < 40; j++) { + m.insert("grow_key_{j}", j); + } + println("grown len={m.len()} g0={m.get_or(\"grow_key_0\", -1)} g39={m.get_or(\"grow_key_39\", -1)} a={m.get_or(\"apple\", -1)}"); + + m.clear(); + println("cleared len={m.len()} empty={m.is_empty()}"); + return 0; +} diff --git a/tests/common/stdlib/collections/strmap_basic.expect b/tests/common/stdlib/collections/strmap_basic.expect new file mode 100644 index 00000000..5b93ea8b --- /dev/null +++ b/tests/common/stdlib/collections/strmap_basic.expect @@ -0,0 +1,8 @@ +len=3 a=1 x=-1 +dyn=1 interp=true get=3 i2=ok +overwrite=100 len=3 +removed=false len=2 +reuse len=3 date=4 +keys n=3 k0=apple k1=date k2=cherry +grown len=43 g0=0 g39=39 a=100 +cleared len=0 empty=true diff --git a/tests/common/stdlib/collections/strmap_load.cm b/tests/common/stdlib/collections/strmap_load.cm new file mode 100644 index 00000000..e0849349 --- /dev/null +++ b/tests/common/stdlib/collections/strmap_load.cm @@ -0,0 +1,32 @@ +// パフォーマンス+正しさスモーク: 実行時生成キー10000件の全ヒット(旧HashMapは2000件中1件しか当たらない) +import std::io::println; +import std::collections::strmap::*; + +int main() { + StringMap m(); + const int n = 10000; + for (int i = 0; i < n; i++) { + m.insert("symbol_table_entry_{i}", i); + } + println("len={m.len()}"); + + int hit = 0; + for (int i = 0; i < n; i++) { + if (m.get_or("symbol_table_entry_{i}", -1) == i) { + hit = hit + 1; + } + } + int miss = 0; + for (int i = 0; i < 100; i++) { + if (m.contains("absent_key_{i}")) { + miss = miss + 1; + } + } + println("hit={hit} miss={miss}"); + + for (int i = 0; i < n; i = i + 2) { + m.remove("symbol_table_entry_{i}"); + } + println("after remove len={m.len()}"); + return 0; +} diff --git a/tests/common/stdlib/collections/strmap_load.expect b/tests/common/stdlib/collections/strmap_load.expect new file mode 100644 index 00000000..fb93962f --- /dev/null +++ b/tests/common/stdlib/collections/strmap_load.expect @@ -0,0 +1,3 @@ +len=10000 +hit=10000 miss=0 +after remove len=5000 diff --git a/tests/common/stdlib/collections/strset_basic.cm b/tests/common/stdlib/collections/strset_basic.cm new file mode 100644 index 00000000..961afe29 --- /dev/null +++ b/tests/common/stdlib/collections/strset_basic.cm @@ -0,0 +1,22 @@ +// StringSet: 挿入・重複・存在判定・削除・挿入順values・clear +import std::io::println; +import std::collections::strset::*; + +int main() { + StringSet s(); + s.insert("red"); + s.insert("green"); + s.insert("blue"); + s.insert("red"); // 重複は無視 + println("len={s.len()} has={s.contains(\"green\")} dyn={s.contains(\"re\" + \"d\")}"); + + s.remove("green"); + println("removed={s.contains(\"green\")} len={s.len()}"); + + string[] vs = s.values_in_order(); + println("v0={vs[0]} v1={vs[1]} n={vs.len()}"); + + s.clear(); + println("cleared len={s.len()} empty={s.is_empty()}"); + return 0; +} diff --git a/tests/common/stdlib/collections/strset_basic.expect b/tests/common/stdlib/collections/strset_basic.expect new file mode 100644 index 00000000..40ede643 --- /dev/null +++ b/tests/common/stdlib/collections/strset_basic.expect @@ -0,0 +1,4 @@ +len=3 has=true dyn=true +removed=false len=2 +v0=red v1=blue n=2 +cleared len=0 empty=true diff --git a/tests/common/stdlib/fs/write_perf.cm b/tests/common/stdlib/fs/write_perf.cm new file mode 100644 index 00000000..7aa5c75c --- /dev/null +++ b/tests/common/stdlib/fs/write_perf.cm @@ -0,0 +1,48 @@ +//! platform: !wasm|js|sv +// パフォーマンススモーク: コード生成級の大出力(StringBuilder一括構築→write_fileと、BufWriterの逐次追記)がタイムアウト内で完了する +import std::io::println; +import std::strings::builder::*; +import std::fs::{write_file, read_lines, remove, size}; +import native::io::stream::*; + +use libc { + int open(string path, int flags, int mode); + int close(int fd); +} + +int main() { + const int n = 8000; + const string path1 = "/tmp/cm_write_perf_sb.txt"; + const string path2 = "/tmp/cm_write_perf_bw.txt"; + + // 1) StringBuilderで一括構築してwrite_file(コード生成の主経路) + StringBuilder sb(); + for (int i = 0; i < n; i++) { + sb.append("line_{i}: emitted code text\n"); + } + const string content = sb.to_string(); + write_file(path1, content); + string[] ls = read_lines(path1); + println("sb lines={ls.len()} first={ls[0]} last={ls[n - 1]}"); + + // 2) BufWriterで行単位追記(バッファリング書き込みの経路) + // O_WRONLY|O_CREAT|O_TRUNC = 0x0601, mode 0644 + const int fd = open(path2, 1537, 420); + BufWriter w = BufWriter::from_fd(fd); + utiny[] line = []; + const string text = "buffered line payload\n"; + for (int i = 0; i < text.byte_len(); i++) { + line.push(text.byte_at(i) as utiny); + } + for (int i = 0; i < n; i++) { + w.write(line); + } + w.flush(); + close(fd); + const long expect_size = (text.byte_len() as long) * (n as long); + println("bw size_ok={size(path2) == expect_size}"); + + remove(path1); + remove(path2); + return 0; +} diff --git a/tests/common/stdlib/fs/write_perf.expect b/tests/common/stdlib/fs/write_perf.expect new file mode 100644 index 00000000..3b63f765 --- /dev/null +++ b/tests/common/stdlib/fs/write_perf.expect @@ -0,0 +1,2 @@ +sb lines=8000 first=line_0: emitted code text last=line_7999: emitted code text +bw size_ok=true diff --git a/tests/common/stdlib/slices/sort.cm b/tests/common/stdlib/slices/sort.cm new file mode 100644 index 00000000..0613c19c --- /dev/null +++ b/tests/common/stdlib/slices/sort.cm @@ -0,0 +1,29 @@ +// std::slices: ヒープソートの正しさ(逆順・重複・既ソート・空・1要素・比較関数・double・string) +import std::io::println; +import std::slices::*; + +int main() { + int[] a = [5, 1, 4, 1, 3, 9, 2, 6, 5, 3]; + sort(a); + println("{a[0]} {a[1]} {a[2]} {a[3]} {a[4]} {a[5]} {a[6]} {a[7]} {a[8]} {a[9]}"); + + int[] e = []; + sort(e); + int[] one = [42]; + sort(one); + int[] sorted = [1, 2, 3]; + sort(sorted); + println("e={e.len()} one={one[0]} s={sorted[0]}{sorted[1]}{sorted[2]}"); + + sort_by(a, (int x, int y) => y - x); + println("desc {a[0]} {a[9]}"); + + double[] d = [2.5, 0.5, 1.5]; + sort(d); + println("{d[0]} {d[1]} {d[2]}"); + + string[] s = ["banana", "apple", "cherry"]; + sort(s); + println("{s[0]} {s[1]} {s[2]}"); + return 0; +} diff --git a/tests/common/stdlib/slices/sort.expect b/tests/common/stdlib/slices/sort.expect new file mode 100644 index 00000000..5ae231f8 --- /dev/null +++ b/tests/common/stdlib/slices/sort.expect @@ -0,0 +1,5 @@ +1 1 2 3 3 4 5 5 6 9 +e=0 one=42 s=123 +desc 9 1 +0.5 1.5 2.5 +apple banana cherry diff --git a/tests/common/stdlib/slices/sort_load.cm b/tests/common/stdlib/slices/sort_load.cm new file mode 100644 index 00000000..5d271d39 --- /dev/null +++ b/tests/common/stdlib/slices/sort_load.cm @@ -0,0 +1,20 @@ +// パフォーマンススモーク: 20000要素の逆順ソートがタイムアウト内に完了する(挿入ソートO(n²)では約2億比較で退化する規模) +import std::io::println; +import std::slices::*; + +int main() { + const int n = 20000; + int[] xs = []; + for (int i = 0; i < n; i++) { + xs.push(n - i); + } + sort(xs); + bool ok = true; + for (int i = 0; i < n; i++) { + if (xs[i] != i + 1) { + ok = false; + } + } + println("sorted={ok} first={xs[0]} last={xs[n - 1]}"); + return 0; +} diff --git a/tests/common/stdlib/slices/sort_load.expect b/tests/common/stdlib/slices/sort_load.expect new file mode 100644 index 00000000..068e4802 --- /dev/null +++ b/tests/common/stdlib/slices/sort_load.expect @@ -0,0 +1 @@ +sorted=true first=1 last=20000 diff --git a/tests/common/stdlib/strings/format_radix.cm b/tests/common/stdlib/strings/format_radix.cm new file mode 100644 index 00000000..74fb56bd --- /dev/null +++ b/tests/common/stdlib/strings/format_radix.cm @@ -0,0 +1,20 @@ +// std::strings::format: 基数変換・負数・パディング・long境界 +import std::io::println; +import std::strings::format::*; + +int main() { + println("{to_hex(255)} {to_hex(0 - 255)} {to_hex(0)} {to_hex(4096)}"); + println("{to_bin(10)} {to_oct(64)} {to_radix(35 as long, 36)} {to_radix(36 as long, 36)}"); + println("{to_radix(4294967295, 16)}"); + + // long最小値・最大値 + println("{to_hex(-9223372036854775807 - 1)} {to_hex(9223372036854775807)}"); + + // 不正基数は空文字列 + println("[{to_radix(1 as long, 1)}] [{to_radix(1 as long, 37)}]"); + + // パディング(幅超過はそのまま) + println("[{pad_left(\"7f\", 8, '0')}] [{pad_right(\"ab\", 5, '.')}] [{pad_left(\"long\", 2, 'x')}]"); + println("[{pad_left(to_hex(255), 4, '0')}]"); + return 0; +} diff --git a/tests/common/stdlib/strings/format_radix.expect b/tests/common/stdlib/strings/format_radix.expect new file mode 100644 index 00000000..13c69d57 --- /dev/null +++ b/tests/common/stdlib/strings/format_radix.expect @@ -0,0 +1,7 @@ +ff -ff 0 1000 +1010 100 z 10 +ffffffff +-8000000000000000 7fffffffffffffff +[] [] +[0000007f] [ab...] [long] +[00ff] diff --git a/tests/common/stdlib/strings/hash_fnv.cm b/tests/common/stdlib/strings/hash_fnv.cm new file mode 100644 index 00000000..54303987 --- /dev/null +++ b/tests/common/stdlib/strings/hash_fnv.cm @@ -0,0 +1,21 @@ +// std::strings::hash: FNV-1a 32bitの既知値と内容同一性 +import std::io::println; +import std::strings::hash::*; + +int main() { + // FNV-1a既知値(0x7FFFFFFFマスク後): ""=2166136261-2^31, "a"=0xE40C292C-2^31 + println("{hash_string(\"\")} {hash_string(\"a\")}"); + + // 生成方法によらず同一内容は同値 + int n = 42; + const int h1 = hash_string("num_42"); + const int h2 = hash_string("num_" + "42"); + const int h3 = hash_string("num_{n}"); + println("{h1 == h2} {h2 == h3}"); + + // 異なる内容は(この例では)異なる値・非負 + const int ha = hash_string("abc"); + const int hb = hash_string("abd"); + println("{ha != hb} {ha >= 0} {hb >= 0}"); + return 0; +} diff --git a/tests/common/stdlib/strings/hash_fnv.expect b/tests/common/stdlib/strings/hash_fnv.expect new file mode 100644 index 00000000..5fa24a81 --- /dev/null +++ b/tests/common/stdlib/strings/hash_fnv.expect @@ -0,0 +1,3 @@ +18652613 1678518572 +true true +true true true diff --git a/tests/common/stdlib/strings/intern_basic.cm b/tests/common/stdlib/strings/intern_basic.cm new file mode 100644 index 00000000..227f7752 --- /dev/null +++ b/tests/common/stdlib/strings/intern_basic.cm @@ -0,0 +1,21 @@ +// std::strings::intern: 同一内容の同id・採番順・name_of往復 +import std::io::println; +import std::strings::intern::*; + +int main() { + Interner it(); + const int a = it.intern("foo"); + const int b = it.intern("bar"); + const int c = it.intern("foo"); // 同一内容 → 同じid + const int d = it.intern("fo" + "o"); // 実行時生成でも同じid + println("a={a} b={b} c={c} d={d} len={it.len()}"); + println("names={it.name_of(a)},{it.name_of(b)} oob=[{it.name_of(99)}]"); + println("has={it.contains(\"bar\")} nohas={it.contains(\"baz\")}"); + + // 大量intern(重複50%)でも件数はユニーク数 + for (int i = 0; i < 200; i++) { + it.intern("sym_{i % 100}"); + } + println("bulk len={it.len()}"); + return 0; +} diff --git a/tests/common/stdlib/strings/intern_basic.expect b/tests/common/stdlib/strings/intern_basic.expect new file mode 100644 index 00000000..86edf6d1 --- /dev/null +++ b/tests/common/stdlib/strings/intern_basic.expect @@ -0,0 +1,4 @@ +a=0 b=1 c=0 d=0 len=2 +names=foo,bar oob=[] +has=true nohas=false +bulk len=102 From 6d8e551c52d00d064b8864f8de87666e0e2e65bd Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 17:26:55 +0900 Subject: [PATCH 06/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E3=81=AE=E3=82=B5?= =?UTF-8?q?=E3=83=B3=E3=83=97=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0=EF=BC=88?= =?UTF-8?q?examples/08=5Fselfhost=5Fparser=E3=83=BB=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E5=88=A59=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E5=88=86?= =?UTF-8?q?=E5=89=B2=E3=83=BB#[test]=E5=8D=98=E4=BD=93=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E4=BB=98=E3=81=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.17.2で整備した標準ライブラリの実証として、Cmで書いたCmソースの宣言レベルパーサを追加した。 動作: - コンパイル可能な構文なら各種定義一覧(module/import/use/typedef/const/struct/enum/impl/関数)を出力する - 構文エラーならエラー箇所(メッセージ・ファイル:行:桁・キャレット付きソース行)を出力して終了コード1を返す - 失敗し得る解析関数はすべてOption型で失敗を伝播し、診断(メッセージ/行/桁)はLexer/Parser状態に保持する(最初のエラーのみ保持) 構成(機能・定義ごとにファイル分割): - token.cm(トークン種別+TokenStream)/ lexer.cm(字句解析・StringSetキーワード表・::融合・コメント/文字列/文字/数値) - parser/state.cm(状態・基本操作・レコーダ)/ modules.cm(module/import/use)/ types.cm(struct/enum・ネスト型・無名宣言子)/ impls.cm(impl/interface・self/~self/private/static)/ funcs.cm(関数/typedef/const/extern)/ decl.cm(ディスパッチ) - 各ファイルに#[test]ディレクティブの単体テスト(*_test.cm・8ファイル38テスト)を付属 検証: - パーサ自身の全9ソースを自己解析できることを確認 - 正常サンプル(全宣言種別)とエラーサンプル(メソッド本体欠落を13行目として報告)を同梱 - CI(check_examples.sh)へ単体テスト・正常/エラーサンプル・自己解析の検証を追加(sample_error.cmは意図的に不正なためcm check対象から除外) 計画doc・チュートリアル・VSCode拡張は非該当(言語機能の追加ではなくサンプル実装のため。examples/README.mdとリリースノートのサンプル節で案内) --- docs/releases/v0.17.2.md | 9 + examples/08_selfhost_parser/README.md | 57 ++++ examples/08_selfhost_parser/lexer.cm | 278 ++++++++++++++++ examples/08_selfhost_parser/lexer_test.cm | 63 ++++ examples/08_selfhost_parser/main.cm | 95 ++++++ examples/08_selfhost_parser/parser/decl.cm | 86 +++++ .../08_selfhost_parser/parser/decl_test.cm | 52 +++ examples/08_selfhost_parser/parser/funcs.cm | 124 ++++++++ .../08_selfhost_parser/parser/funcs_test.cm | 62 ++++ examples/08_selfhost_parser/parser/impls.cm | 135 ++++++++ .../08_selfhost_parser/parser/impls_test.cm | 46 +++ examples/08_selfhost_parser/parser/modules.cm | 81 +++++ .../08_selfhost_parser/parser/modules_test.cm | 52 +++ examples/08_selfhost_parser/parser/state.cm | 299 ++++++++++++++++++ .../08_selfhost_parser/parser/state_test.cm | 69 ++++ examples/08_selfhost_parser/parser/types.cm | 153 +++++++++ .../08_selfhost_parser/parser/types_test.cm | 53 ++++ examples/08_selfhost_parser/sample_error.cm | 17 + examples/08_selfhost_parser/sample_ok.cm | 66 ++++ examples/08_selfhost_parser/token.cm | 70 ++++ examples/08_selfhost_parser/token_test.cm | 25 ++ examples/README.md | 5 + scripts/ci/check_examples.sh | 18 +- 23 files changed, 1914 insertions(+), 1 deletion(-) create mode 100644 examples/08_selfhost_parser/README.md create mode 100644 examples/08_selfhost_parser/lexer.cm create mode 100644 examples/08_selfhost_parser/lexer_test.cm create mode 100644 examples/08_selfhost_parser/main.cm create mode 100644 examples/08_selfhost_parser/parser/decl.cm create mode 100644 examples/08_selfhost_parser/parser/decl_test.cm create mode 100644 examples/08_selfhost_parser/parser/funcs.cm create mode 100644 examples/08_selfhost_parser/parser/funcs_test.cm create mode 100644 examples/08_selfhost_parser/parser/impls.cm create mode 100644 examples/08_selfhost_parser/parser/impls_test.cm create mode 100644 examples/08_selfhost_parser/parser/modules.cm create mode 100644 examples/08_selfhost_parser/parser/modules_test.cm create mode 100644 examples/08_selfhost_parser/parser/state.cm create mode 100644 examples/08_selfhost_parser/parser/state_test.cm create mode 100644 examples/08_selfhost_parser/parser/types.cm create mode 100644 examples/08_selfhost_parser/parser/types_test.cm create mode 100644 examples/08_selfhost_parser/sample_error.cm create mode 100644 examples/08_selfhost_parser/sample_ok.cm create mode 100644 examples/08_selfhost_parser/token.cm create mode 100644 examples/08_selfhost_parser/token_test.cm diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index febd2100..f7a43aa8 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -227,6 +227,15 @@ JSのポインタ表現(配列+添字のセル参照)がスライスポイ 選択import(`::{名前}`)が対象関数本体を即時型検査する挙動と、ユーザ定義Optionのフラット名前空間上書きの合成が真因。 ライブラリ内部の利用をワイルドカードimport(検査が利用時まで遅延)へ変更して回避し、型解決のモジュール分離を言語側の設計課題に記録した。 +## 📦 サンプル + +### セルフホストパーサ(examples/08_selfhost_parser) + +v0.17.2で整備した標準ライブラリの実証として、Cmで書いたCmソースの宣言レベルパーサを追加した。 +成功なら定義一覧(module/import/use/typedef/const/struct/enum/impl/関数)を、失敗ならエラー箇所(行:桁・キャレット付きソース行)を出力し、失敗し得る解析関数はすべてOption型で失敗を伝播する。 +機能別に9ファイルへ分割し、各ファイルに `#[test]` ディレクティブの単体テスト(8ファイル・38テスト)が付属する。 +パーサ自身の全ソースを自己解析でき、CIが単体テスト・正常/エラーサンプル・自己解析を検証する。 + ## 🧪 テスト - 機能テスト: スマートポインタ4件+WeakPtr/Atomic、TreeSet/HashSet/TreeMap削除、chars/parse、Arena、Vector強化、fs行読みを追加。 diff --git a/examples/08_selfhost_parser/README.md b/examples/08_selfhost_parser/README.md new file mode 100644 index 00000000..32400912 --- /dev/null +++ b/examples/08_selfhost_parser/README.md @@ -0,0 +1,57 @@ +# セルフホストパーサ(宣言リスタ) + +Cmで書いたCmソースの宣言レベルパーサです。 +セルフホスティングへ向けた実証として、v0.17.2で整備した標準ライブラリ(StringSet・chars相当の字句分類・format・スライスバック構造)だけでコンパイラのフロントエンド前段を構成しています。 + +- コンパイル可能な構文なら**各種定義一覧**(module / import / use / typedef / const / struct / enum / impl / 関数)を出力します +- 構文エラーなら**エラー箇所**(メッセージ・`ファイル:行:桁`・キャレット付きソース行)を出力して終了コード1を返します +- 失敗し得る解析関数はすべて **`Option`型** を返し(`None`=失敗)、位置・メッセージはLexer/Parserが保持します + +## 使い方 + +```bash +# 任意のCmソースを解析 +cm run examples/08_selfhost_parser/main.cm -- path/to/file.cm [more.cm ...] + +# 自己解析(パーサが自分自身のソースを解析する) +cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/main.cm + +# 正常系サンプル: 定義一覧が出る +cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/sample_ok.cm + +# エラー系サンプル: 13行目のメソッド本体欠落を位置付きで報告する +cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/sample_error.cm +``` + +## ファイル構成(機能ごとに分割) + +| ファイル | 役割 | +|---|---| +| `token.cm` | トークン種別定数と`TokenStream`(パラレルスライス) | +| `lexer.cm` | 字句解析器(コメント/文字列/文字/数値/`::`融合、キーワード判定は`StringSet`) | +| `parser/state.cm` | `Parser`状態・基本操作(expect/skip/ジェネリクス読取)・エラー診断・定義レコーダ | +| `parser/modules.cm` | module / import(選択・相対・ワイルドカード)/ use libc | +| `parser/types.cm` | struct / enum(ネスト型・無名struct宣言子・with句・ペイロード付きバリアント) | +| `parser/impls.cm` | impl / interface(self・~self・private・static・overload) | +| `parser/funcs.cm` | 関数 / typedef / const / extern "C" | +| `parser/decl.cm` | トップレベル宣言のディスパッチと`parse_program` | +| `main.cm` | CLI(引数解析・読み込み・結果出力) | + +関数本体・メソッド本体は波括弧の対応で読み飛ばす「定義リスタ」としての解析です(文字列リテラル内の波括弧は字句解析済みのため誤検出しません)。 + +## 単体テスト + +各ファイルに`#[test]`ディレクティブの単体テスト(`*_test.cm`)が付属します。 + +```bash +cm test examples/08_selfhost_parser/lexer_test.cm +cm test examples/08_selfhost_parser/parser/types_test.cm +# ...(token / state / modules / impls / funcs / decl も同様) +``` + +CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、8ファイルの単体テスト・正常/エラーサンプル・自己解析を検証します。 + +## エラーハンドリングの設計 + +`Option`はペイロードに失敗情報を持てないため、「失敗=`Option::None`を返す・診断(メッセージ/行/桁)は状態側に保持」という規約で統一しています。 +最初のエラーだけを保持するため、深い再帰の途中で失敗しても報告位置が上書きされません。 diff --git a/examples/08_selfhost_parser/lexer.cm b/examples/08_selfhost_parser/lexer.cm new file mode 100644 index 00000000..246b69e6 --- /dev/null +++ b/examples/08_selfhost_parser/lexer.cm @@ -0,0 +1,278 @@ +// セルフホストパーサ: 字句解析器 +// Cmソースをトークン列へ分解する。 +// エラーハンドリングはOption型で行い、失敗(未終端文字列・未終端コメント等)の位置はLexer側に保持する +module lexer; + +import std::collections::strset::*; +import std::strings::from_bytes::*; +import ./token::*; + +// 字句解析器本体 +// トークン列は内部に保持し、run()成功後にtake_tokens()で受け取る +// (Optionのような大きな構造体ペイロードのユニオンを避け、Optionは成否のみを運ぶ) +export struct Lexer { + private string src; + private int pos; + private int line; + private int col; + private StringSet keywords; + private TokenStream tokens; + // 失敗時の診断(run()がNoneを返したときに参照する) + private string err_msg; + private int err_line; + private int err_col; +} + +export impl Lexer { + self(string source) { + self.src = source; + self.pos = 0; + self.line = 1; + self.col = 1; + StringSet kw(); + kw.insert("module"); + kw.insert("import"); + kw.insert("export"); + kw.insert("use"); + kw.insert("struct"); + kw.insert("enum"); + kw.insert("impl"); + kw.insert("interface"); + kw.insert("typedef"); + kw.insert("const"); + kw.insert("static"); + kw.insert("private"); + kw.insert("overload"); + kw.insert("extern"); + kw.insert("operator"); + kw.insert("for"); + kw.insert("with"); + kw.insert("self"); + kw.insert("return"); + kw.insert("if"); + kw.insert("else"); + kw.insert("while"); + kw.insert("match"); + kw.insert("switch"); + kw.insert("move"); + kw.insert("as"); + kw.insert("is"); + self.keywords = kw; + TokenStream ts(); + self.tokens = ts; + self.err_msg = ""; + self.err_line = 0; + self.err_col = 0; + } + + // 失敗時の診断アクセサ + string error_message() { + return self.err_msg; + } + + int error_line() { + return self.err_line; + } + + int error_col() { + return self.err_col; + } + + // 現在位置のバイト(終端は-1) + private int peek_byte() { + if (self.pos >= self.src.byte_len()) { + return -1; + } + return self.src.byte_at(self.pos); + } + + private int peek_byte2() { + if (self.pos + 1 >= self.src.byte_len()) { + return -1; + } + return self.src.byte_at(self.pos + 1); + } + + // 1バイト進める(改行で行・桁を更新) + private void advance() { + if (self.pos < self.src.byte_len()) { + if (self.src.byte_at(self.pos) == 10) { + self.line = self.line + 1; + self.col = 1; + } else { + self.col = self.col + 1; + } + self.pos = self.pos + 1; + } + } + + private bool is_digit_b(int b) { + return b >= 48 && b <= 57; + } + + private bool is_ident_start_b(int b) { + return (b >= 97 && b <= 122) || (b >= 65 && b <= 90) || b == 95; + } + + private bool is_ident_cont_b(int b) { + return self.is_ident_start_b(b) || self.is_digit_b(b); + } + + // 空白とコメントを読み飛ばす(未終端ブロックコメントはNone) + private Option skip_trivia() { + while (true) { + const int b = self.peek_byte(); + if (b == 32 || b == 9 || b == 13 || b == 10) { + self.advance(); + } else if (b == 47 && self.peek_byte2() == 47) { + // 行コメント + while (self.peek_byte() != 10 && self.peek_byte() != -1) { + self.advance(); + } + } else if (b == 47 && self.peek_byte2() == 42) { + // ブロックコメント + const int start_line = self.line; + const int start_col = self.col; + self.advance(); + self.advance(); + bool closed = false; + while (self.peek_byte() != -1) { + if (self.peek_byte() == 42 && self.peek_byte2() == 47) { + self.advance(); + self.advance(); + closed = true; + break; + } + self.advance(); + } + if (!closed) { + self.err_msg = "unterminated block comment"; + self.err_line = start_line; + self.err_col = start_col; + return Option::None; + } + } else { + return Option::Some(true); + } + } + return Option::Some(true); + } + + // src[start..pos) をバイト単位で文字列へ切り出す + // (substringはコードポイント添字のため、バイト位置ベースの字句切り出しにはfrom_bytesを使う) + private string slice_text(int start) { + utiny[] buf = []; + for (int i = start; i < self.pos; i++) { + buf.push(self.src.byte_at(i) as utiny); + } + return from_bytes(buf); + } + + // 成功後にトークン列を受け取る(スライスバックの値コピーで安全) + TokenStream take_tokens() { + return self.tokens; + } + + // 全体を字句解析する(成功はSome(true)、失敗はNoneで位置はerror_line/error_col) + Option run() { + while (true) { + const Option trivia = self.skip_trivia(); + if (trivia.is_none()) { + return Option::None; + } + const int b = self.peek_byte(); + if (b == -1) { + self.tokens.push_token(TK_EOF, "", self.line, self.col); + return Option::Some(true); + } + const int tok_line = self.line; + const int tok_col = self.col; + const int start = self.pos; + + if (self.is_ident_start_b(b)) { + // 識別子または予約語 + while (self.is_ident_cont_b(self.peek_byte())) { + self.advance(); + } + const string text = self.slice_text(start); + if (self.keywords.contains(text)) { + self.tokens.push_token(TK_KEYWORD, text, tok_line, tok_col); + } else { + self.tokens.push_token(TK_IDENT, text, tok_line, tok_col); + } + } else if (self.is_digit_b(b)) { + // 数値リテラル(10進・16進・小数・指数をまとめて消費する) + while (true) { + const int c = self.peek_byte(); + if (self.is_ident_cont_b(c)) { + self.advance(); + } else if (c == 46 && self.is_digit_b(self.peek_byte2())) { + // 小数点(直後が数字のときのみ) + self.advance(); + } else if ((c == 43 || c == 45) && self.pos > start && + (self.src.byte_at(self.pos - 1) == 101 || + self.src.byte_at(self.pos - 1) == 69)) { + // 指数の符号(e/Eの直後のみ) + self.advance(); + } else { + break; + } + } + self.tokens.push_token(TK_NUMBER, self.slice_text(start), tok_line, tok_col); + } else if (b == 34) { + // 文字列リテラル(エスケープ対応・未終端はエラー) + self.advance(); + bool closed = false; + while (self.peek_byte() != -1) { + const int c = self.peek_byte(); + if (c == 92) { + self.advance(); + self.advance(); + } else if (c == 34) { + self.advance(); + closed = true; + break; + } else if (c == 10) { + break; + } else { + self.advance(); + } + } + if (!closed) { + self.err_msg = "unterminated string literal"; + self.err_line = tok_line; + self.err_col = tok_col; + return Option::None; + } + self.tokens.push_token(TK_STRING, self.slice_text(start), tok_line, tok_col); + } else if (b == 39) { + // 文字リテラル(未終端はエラー) + self.advance(); + if (self.peek_byte() == 92) { + self.advance(); + self.advance(); + } else if (self.peek_byte() != -1 && self.peek_byte() != 39) { + self.advance(); + } + if (self.peek_byte() != 39) { + self.err_msg = "unterminated char literal"; + self.err_line = tok_line; + self.err_col = tok_col; + return Option::None; + } + self.advance(); + self.tokens.push_token(TK_CHAR, self.slice_text(start), tok_line, tok_col); + } else if (b == 58 && self.peek_byte2() == 58) { + // "::" は1トークンに融合する(import経路・修飾名の解析用) + self.advance(); + self.advance(); + self.tokens.push_token(TK_SYMBOL, "::", tok_line, tok_col); + } else { + // その他は1文字記号 + self.advance(); + self.tokens.push_token(TK_SYMBOL, self.slice_text(start), tok_line, tok_col); + } + } + return Option::None; + } +} diff --git a/examples/08_selfhost_parser/lexer_test.cm b/examples/08_selfhost_parser/lexer_test.cm new file mode 100644 index 00000000..b088f5e8 --- /dev/null +++ b/examples/08_selfhost_parser/lexer_test.cm @@ -0,0 +1,63 @@ +// lexer.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ./token::*; +import ./lexer::*; + +// テスト用: ソースを字句解析してトークン列を返す(成功前提) +TokenStream lex_ok(string src) { + Lexer lx(src); + const Option r = lx.run(); + assert(r.is_some(), "lex should succeed"); + return lx.take_tokens(); +} + +#[test] +void tokenizes_idents_keywords_and_symbols() { + TokenStream ts = lex_ok("struct Point { int x; }"); + assert(ts.kind_at(0) == TK_KEYWORD, "struct is keyword"); + assert(ts.text_at(0) == "struct", "struct text"); + assert(ts.kind_at(1) == TK_IDENT, "Point is ident"); + assert(ts.kind_at(2) == TK_SYMBOL, "brace is symbol"); + assert(ts.kind_at(ts.len() - 1) == TK_EOF, "stream ends with EOF"); +} + +#[test] +void merges_double_colon() { + TokenStream ts = lex_ok("std::io::println"); + assert(ts.text_at(1) == "::", "double colon merged"); + assert(ts.text_at(3) == "::", "second double colon merged"); + assert(ts.len() == 6, "5 tokens + EOF"); +} + +#[test] +void skips_comments_and_tracks_lines() { + TokenStream ts = lex_ok("// comment\n/* block\ncomment */ answer"); + assert(ts.text_at(0) == "answer", "comments skipped"); + assert(ts.line_at(0) == 3, "line number after comments"); +} + +#[test] +void lexes_string_char_and_number_literals() { + TokenStream ts = lex_ok("\"a\\\"b\" 'x' 0x1f 1.5e3"); + assert(ts.kind_at(0) == TK_STRING, "string literal with escape"); + assert(ts.kind_at(1) == TK_CHAR, "char literal"); + assert(ts.kind_at(2) == TK_NUMBER, "hex number"); + assert(ts.text_at(3) == "1.5e3", "float with exponent"); +} + +#[test] +void reports_unterminated_string_position() { + Lexer lx("int x;\n\"never closed"); + const Option r = lx.run(); + assert(r.is_none(), "unterminated string is a lex error"); + assert(lx.error_line() == 2, "error line"); + assert(lx.error_col() == 1, "error col"); +} + +#[test] +void reports_unterminated_block_comment() { + Lexer lx("/* no end"); + const Option r = lx.run(); + assert(r.is_none(), "unterminated block comment is a lex error"); + assert(lx.error_line() == 1, "error line"); +} diff --git a/examples/08_selfhost_parser/main.cm b/examples/08_selfhost_parser/main.cm new file mode 100644 index 00000000..4045639a --- /dev/null +++ b/examples/08_selfhost_parser/main.cm @@ -0,0 +1,95 @@ +// セルフホストパーサ: エントリポイント +// Cmソースファイルを字句解析→宣言解析し、成功なら各種定義一覧を、失敗ならエラー箇所(行:桁とキャレット付きソース行)を出力する。 +// 実行例: cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/main.cm +import std::io::println; +import std::env::args; +import std::fs::{read_file, exists}; +import std::strings::split::{lines}; +import std::strings::format::*; +import ./lexer::*; +import ./parser/state::*; +import ./parser/decl::*; + +// エラー位置のソース行をキャレット付きで表示する +void print_error_context(string path, string src, int line, int col) { + string[] ls = lines(src); + if (line >= 1 && line <= ls.len()) { + const string ln = to_radix(line as long, 10); + println("{pad_left(ln, 4, ' ')} | {ls[line - 1]}"); + println(" | {pad_left(\"^\", col, ' ')}"); + } +} + +// 定義一覧のセクションを見出し付きで出力する +void print_section(string title, string[] items) { + if (items.len() == 0) { + return; + } + println(" {title} ({items.len()}):"); + for (int i = 0; i < items.len(); i++) { + println(" {items[i]}"); + } +} + +int main() { + const string[] argv = args(); + if (argv.len() < 2) { + println("usage: selfhost_parser [more files...]"); + println(" Cmソースの宣言を解析し、成功なら定義一覧を、失敗ならエラー箇所を出力します"); + return 1; + } + + int failures = 0; + for (int ai = 1; ai < argv.len(); ai++) { + const string path = argv[ai]; + if (!exists(path)) { + println("error: file not found: {path}"); + failures = failures + 1; + continue; + } + const string src = read_file(path); + + // 1. 字句解析(失敗はOption::Noneで返り、位置はLexerが保持) + Lexer lx(src); + const Option lexed = lx.run(); + if (lexed.is_none()) { + println("== {path}"); + println("lex error: {lx.error_message()}"); + println(" --> {path}:{lx.error_line()}:{lx.error_col()}"); + print_error_context(path, src, lx.error_line(), lx.error_col()); + failures = failures + 1; + continue; + } + + // 2. 宣言解析(失敗はOption::Noneで返り、位置はParserが保持) + Parser ps(lx.take_tokens()); + const Option parsed = parse_program(&ps); + if (parsed.is_none()) { + println("== {path}"); + println("syntax error: {ps.error_message()}"); + println(" --> {path}:{ps.error_line()}:{ps.error_col()}"); + print_error_context(path, src, ps.error_line(), ps.error_col()); + failures = failures + 1; + continue; + } + + // 3. 定義一覧の出力 + println("== {path}: OK"); + if (ps.module_of() != "") { + println(" module {ps.module_of()}"); + } + print_section("imports", ps.imports_of()); + print_section("use", ps.uses_of()); + print_section("typedefs", ps.typedefs_of()); + print_section("consts", ps.consts_of()); + print_section("structs", ps.structs_of()); + print_section("enums", ps.enums_of()); + print_section("impls", ps.impls_of()); + print_section("functions", ps.funcs_of()); + } + + if (failures > 0) { + return 1; + } + return 0; +} diff --git a/examples/08_selfhost_parser/parser/decl.cm b/examples/08_selfhost_parser/parser/decl.cm new file mode 100644 index 00000000..d630d2a7 --- /dev/null +++ b/examples/08_selfhost_parser/parser/decl.cm @@ -0,0 +1,86 @@ +// セルフホストパーサ: トップレベル宣言のディスパッチ +// 先頭トークンで宣言種別を判定し、機能別モジュール(modules/types/impls/funcs)へ振り分ける +module parser.decl; + +import ../token::*; +import ./state::*; +import ./modules::*; +import ./types::*; +import ./impls::*; +import ./funcs::*; + +// トップレベル宣言を1つ解析する +export Option parse_decl(Parser* p) { + // 属性は読み飛ばす + if (p->at_text("#")) { + return p->skip_attribute(); + } + // exportは接頭辞として無視して次を見る + if (p->at_text("export")) { + p->bump(); + return parse_decl(p); + } + if (p->at_text("module")) { + return parse_module_decl(p); + } + if (p->at_text("import")) { + return parse_import_decl(p); + } + if (p->at_text("use")) { + return parse_use_decl(p); + } + if (p->at_text("typedef")) { + return parse_typedef(p); + } + if (p->at_text("const")) { + return parse_const(p); + } + if (p->at_text("extern")) { + return parse_extern(p); + } + if (p->at_text("struct")) { + const Option r = parse_struct(p, ""); + if (r.is_none()) { + return Option::None; + } + // 無名/宣言子付きの "STR;" 形式が続く場合は消費する + if (p->at_text(";")) { + p->bump(); + } else if (p->at_ident()) { + const Option d = p->skip_to_semicolon(); + if (d.is_none()) { + return Option::None; + } + } + return Option::Some(true); + } + if (p->at_text("enum")) { + const Option r = parse_enum(p, ""); + if (r.is_none()) { + return Option::None; + } + if (p->at_text(";")) { + p->bump(); + } + return Option::Some(true); + } + if (p->at_text("impl")) { + return parse_impl(p); + } + if (p->at_text("interface")) { + return parse_interface(p); + } + // それ以外は関数またはグローバル定義とみなす + return parse_func_or_var(p); +} + +// プログラム全体を解析する(成功はSome(true)、失敗はNone) +export Option parse_program(Parser* p) { + while (!p->at_eof()) { + const Option d = parse_decl(p); + if (d.is_none()) { + return Option::None; + } + } + return Option::Some(true); +} diff --git a/examples/08_selfhost_parser/parser/decl_test.cm b/examples/08_selfhost_parser/parser/decl_test.cm new file mode 100644 index 00000000..6a3c0b24 --- /dev/null +++ b/examples/08_selfhost_parser/parser/decl_test.cm @@ -0,0 +1,52 @@ +// parser/decl.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ../token::*; +import ../lexer::*; +import ./state::*; +import ./decl::*; + +// テスト用: ソースから解析器を組み立てる +Parser parser_of(string src) { + Lexer lx(src); + const Option r = lx.run(); + assert(r.is_some(), "lex should succeed"); + Parser p(lx.take_tokens()); + return p; +} + +#[test] +void parses_full_program() { + Parser p = parser_of( + "module demo; import std::io::println; #[derive(Debug)] struct P { int x; } enum E { A, B } impl P { self() { } } int main() { return 0; }"); + assert(parse_program(&p).is_some(), "program parses"); + assert(p.module_of() == "demo", "module recorded"); + assert(p.imports_of().len() == 1, "import recorded"); + assert(p.structs_of().len() == 1, "struct recorded"); + assert(p.enums_of().len() == 1, "enum recorded"); + assert(p.impls_of().len() == 1, "impl recorded"); + assert(p.funcs_of().len() == 1, "function recorded"); +} + +#[test] +void export_prefix_is_transparent() { + Parser p = parser_of("export struct S { int v; } export int f() { return 1; }"); + assert(parse_program(&p).is_some(), "exported decls parse"); + assert(p.structs_of().len() == 1, "exported struct recorded"); + assert(p.funcs_of().len() == 1, "exported function recorded"); +} + +#[test] +void anonymous_struct_with_declarator() { + Parser p = parser_of("struct { int a; } CONFIG;"); + assert(parse_program(&p).is_some(), "anonymous struct with declarator parses"); + const string expected = "(anonymous) {" + " a " + "}"; + assert(p.structs_of()[0] == expected, "anonymous display"); +} + +#[test] +void error_position_propagates() { + Parser p = parser_of("struct Ok { int v; }\nstruct Broken {\n int x\n}"); + assert(parse_program(&p).is_none(), "missing field semicolon is an error"); + assert(p.error_line() == 4, "error reported at closing line"); + assert(p.structs_of().len() == 1, "first struct was already recorded"); +} diff --git a/examples/08_selfhost_parser/parser/funcs.cm b/examples/08_selfhost_parser/parser/funcs.cm new file mode 100644 index 00000000..0e6f1bba --- /dev/null +++ b/examples/08_selfhost_parser/parser/funcs.cm @@ -0,0 +1,124 @@ +// セルフホストパーサ: 関数・定数・typedef・extern宣言の解析 +// トップレベルの「型トークン列から始まる宣言」(関数 / グローバル定義)と、typedef・const・extern "C" を扱う +module parser.funcs; + +import ../token::*; +import ./state::*; + +// typedef宣言("typedef"の上から) +export Option parse_typedef(Parser* p) { + p->bump(); + const Option name = p->expect_ident(); + if (name.is_none()) { + return Option::None; + } + const Option d = p->skip_to_semicolon(); + if (d.is_none()) { + return Option::None; + } + p->record_typedef(name.unwrap()); + return Option::Some(true); +} + +// グローバル定数宣言("const"の上から): "const TYPE NAME = ...;" のNAME("="直前の識別子)を収集 +export Option parse_const(Parser* p) { + p->bump(); + string last_ident = ""; + while (!p->at_text("=")) { + if (p->at_eof() || p->at_text(";")) { + return p->fail("expected '=' in const declaration but found " + p->found_text()); + } + if (p->at_ident()) { + last_ident = p->text(); + } + p->bump(); + } + const Option d = p->skip_to_semicolon(); + if (d.is_none()) { + return Option::None; + } + if (last_ident == "") { + return p->fail("could not determine const name"); + } + p->record_const(last_ident); + return Option::Some(true); +} + +// トップレベルの関数またはグローバル変数(型トークン列から始まる宣言) +export Option parse_func_or_var(Parser* p) { + // ""付きの総称free関数 + const Option gp = p->parse_generic_params(); + if (gp.is_none()) { + return Option::None; + } + // "("の直前の識別子が関数名。それより前が戻り値型 + string ret = ""; + string name = ""; + while (!p->at_text("(")) { + if (p->at_eof()) { + return p->fail("unexpected end of file in declaration"); + } + if (p->at_text(";") || p->at_text("=")) { + // 関数でなくグローバル変数宣言だった + const Option d = p->skip_to_semicolon(); + if (d.is_none()) { + return Option::None; + } + if (name == "") { + return p->fail("could not determine declaration name"); + } + p->record_const(name); + return Option::Some(true); + } + if (p->at_text("{") || p->at_text("}")) { + return p->fail("expected declaration but found " + p->found_text()); + } + if (p->at_ident()) { + if (name != "") { + if (ret != "") { + ret = ret + " "; + } + ret = ret + name; + } + name = p->text(); + } else { + // 型の記号(* [ ] :: 等)は戻り値型の一部として保持 + if (name != "") { + ret = ret + name + p->text(); + name = ""; + } else { + ret = ret + p->text(); + } + } + p->bump(); + } + if (name == "") { + return p->fail("could not determine function name before '('"); + } + const Option params = p->skip_balanced("(", ")"); + if (params.is_none()) { + return Option::None; + } + if (p->at_text("{")) { + const Option b = p->skip_balanced("{", "}"); + if (b.is_none()) { + return Option::None; + } + } else if (p->at_text(";")) { + p->bump(); + } else { + return p->fail("expected '{' or ';' after function signature but found " + + p->found_text()); + } + p->record_func(ret + " " + name + gp.unwrap() + "()"); + return Option::Some(true); +} + +// extern宣言("extern"の上から): extern "C" の関数宣言を収集 +export Option parse_extern(Parser* p) { + p->bump(); + if (p->at_string()) { + p->bump(); + } + return parse_func_or_var(p); +} diff --git a/examples/08_selfhost_parser/parser/funcs_test.cm b/examples/08_selfhost_parser/parser/funcs_test.cm new file mode 100644 index 00000000..12bf04cc --- /dev/null +++ b/examples/08_selfhost_parser/parser/funcs_test.cm @@ -0,0 +1,62 @@ +// parser/funcs.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ../token::*; +import ../lexer::*; +import ./state::*; +import ./funcs::*; + +// テスト用: ソースから解析器を組み立てる +Parser parser_of(string src) { + Lexer lx(src); + const Option r = lx.run(); + assert(r.is_some(), "lex should succeed"); + Parser p(lx.take_tokens()); + return p; +} + +#[test] +void parses_plain_function() { + Parser p = parser_of("int main() { return 0; }"); + assert(parse_func_or_var(&p).is_some(), "function parses"); + assert(p.funcs_of()[0] == "int main()", "return type and name"); +} + +#[test] +void parses_generic_function_with_complex_return() { + Parser p = parser_of(" Option find(T[] xs) { return Option::None; }"); + assert(parse_func_or_var(&p).is_some(), "generic function parses"); + assert(p.funcs_of()[0] == "Option find()", "generics and return type kept"); +} + +#[test] +void parses_typedef_and_const() { + Parser p = parser_of("typedef Meters = double;"); + assert(parse_typedef(&p).is_some(), "typedef parses"); + assert(p.typedefs_of()[0] == "Meters", "typedef name"); + + Parser q = parser_of("const int LIMIT = 100;"); + assert(parse_const(&q).is_some(), "const parses"); + assert(q.consts_of()[0] == "LIMIT", "const name is the ident before '='"); +} + +#[test] +void parses_extern_c_declaration() { + Parser p = parser_of("extern \"C\" int cm_now(int* out);"); + assert(parse_extern(&p).is_some(), "extern decl parses"); + assert(p.funcs_of()[0] == "int cm_now()", "extern function recorded"); +} + +#[test] +void function_without_body_or_semicolon_is_error() { + Parser p = parser_of("int broken()"); + assert(parse_func_or_var(&p).is_none(), "missing body is an error"); + assert(p.error_message() == + "expected '{' or ';' after function signature but found end of file", + "error message"); +} + +#[test] +void const_without_assign_is_error() { + Parser p = parser_of("const int NO_VALUE;"); + assert(parse_const(&p).is_none(), "const without '=' is an error"); +} diff --git a/examples/08_selfhost_parser/parser/impls.cm b/examples/08_selfhost_parser/parser/impls.cm new file mode 100644 index 00000000..bdb510e5 --- /dev/null +++ b/examples/08_selfhost_parser/parser/impls.cm @@ -0,0 +1,135 @@ +// セルフホストパーサ: 実装ブロック(impl / interface)の解析 +// impl Name (for Iface)? { メソッド... } のメソッド名収集と、interface宣言を扱う。 +// メソッドはコンストラクタ self / デストラクタ ~self / private / static / overload / operator に対応する +module parser.impls; + +import ../token::*; +import ./state::*; + +// impl内の1メソッドを解析し、メソッド名を返す +export Option parse_method(Parser* p) { + // 修飾子・属性を読み飛ばす + while (true) { + if (p->at_text("#")) { + const Option a = p->skip_attribute(); + if (a.is_none()) { + return Option::None; + } + continue; + } + if (p->at_text("private") || p->at_text("static") || p->at_text("overload") || + p->at_text("export")) { + p->bump(); + continue; + } + break; + } + // シグネチャ: "("直前の識別子(self / ~self / operator込み)がメソッド名 + string name = ""; + while (!p->at_text("(")) { + if (p->at_eof() || p->at_text("{") || p->at_text("}")) { + return p->fail_str("expected method signature but found " + p->found_text()); + } + if (p->at_text("~")) { + name = "~"; + p->bump(); + continue; + } + if (p->at_ident() || p->at_text("self") || p->at_text("operator")) { + if (name == "~") { + name = "~" + p->text(); + } else { + name = p->text(); + } + } + p->bump(); + } + if (name == "") { + return p->fail_str("could not determine method name before '('"); + } + const Option params = p->skip_balanced("(", ")"); + if (params.is_none()) { + return Option::None; + } + // 本体 or 宣言のみ + if (p->at_text("{")) { + const Option b = p->skip_balanced("{", "}"); + if (b.is_none()) { + return Option::None; + } + } else if (p->at_text(";")) { + p->bump(); + } else { + return p->fail_str("expected '{' or ';' after method signature but found " + + p->found_text()); + } + return Option::Some(name); +} + +// implブロック("impl"の上から)。メソッド名の一覧を収集する +export Option parse_impl(Parser* p) { + p->bump(); + const Option gp = p->parse_generic_params(); + if (gp.is_none()) { + return Option::None; + } + const Option tname = p->expect_ident(); + if (tname.is_none()) { + return Option::None; + } + const Option targs = p->parse_generic_params(); + if (targs.is_none()) { + return Option::None; + } + string iface = ""; + if (p->at_text("for")) { + p->bump(); + const Option iname = p->expect_ident(); + if (iname.is_none()) { + return Option::None; + } + const Option iargs = p->parse_generic_params(); + if (iargs.is_none()) { + return Option::None; + } + iface = " for " + iname.unwrap() + iargs.unwrap(); + } + const Option o = p->expect_text("{"); + if (o.is_none()) { + return Option::None; + } + string[] methods = []; + while (!p->at_text("}")) { + if (p->at_eof()) { + return p->fail("unexpected end of file in impl body"); + } + const Option m = parse_method(p); + if (m.is_none()) { + return Option::None; + } + methods.push(m.unwrap()); + } + p->bump(); + p->record_impl("impl " + tname.unwrap() + targs.unwrap() + iface + " { " + + join_names(methods) + " }"); + return Option::Some(true); +} + +// interface宣言("interface"の上から)。名前だけ収集する +export Option parse_interface(Parser* p) { + p->bump(); + const Option name = p->expect_ident(); + if (name.is_none()) { + return Option::None; + } + const Option gp = p->parse_generic_params(); + if (gp.is_none()) { + return Option::None; + } + const Option b = p->skip_balanced("{", "}"); + if (b.is_none()) { + return Option::None; + } + p->record_impl("interface " + name.unwrap() + gp.unwrap()); + return Option::Some(true); +} diff --git a/examples/08_selfhost_parser/parser/impls_test.cm b/examples/08_selfhost_parser/parser/impls_test.cm new file mode 100644 index 00000000..81ff95d6 --- /dev/null +++ b/examples/08_selfhost_parser/parser/impls_test.cm @@ -0,0 +1,46 @@ +// parser/impls.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ../token::*; +import ../lexer::*; +import ./state::*; +import ./impls::*; + +// テスト用: ソースから解析器を組み立てる +Parser parser_of(string src) { + Lexer lx(src); + const Option r = lx.run(); + assert(r.is_some(), "lex should succeed"); + Parser p(lx.take_tokens()); + return p; +} + +#[test] +void parses_impl_with_ctor_dtor_and_modifiers() { + Parser p = parser_of( + "impl Counter { self() { self.n = 0; } ~self() { } private int helper() { return 1; } static Counter zero() { Counter c(); return c; } }"); + assert(parse_impl(&p).is_some(), "impl parses"); + assert(p.impls_of()[0] == "impl Counter { self, ~self, helper, zero }", + "ctor/dtor/private/static methods collected"); +} + +#[test] +void parses_generic_impl_for_interface() { + Parser p = parser_of("impl Box for Container { void put(T v) { } T take() { } }"); + assert(parse_impl(&p).is_some(), "generic impl-for parses"); + assert(p.impls_of()[0] == "impl Box for Container { put, take }", "display form"); +} + +#[test] +void parses_interface_decl() { + Parser p = parser_of("interface Shape { double area(); }"); + assert(parse_interface(&p).is_some(), "interface parses"); + assert(p.impls_of()[0] == "interface Shape", "interface recorded"); +} + +#[test] +void method_without_body_or_semicolon_is_error() { + Parser p = parser_of("impl X { int broken() }"); + assert(parse_impl(&p).is_none(), "missing body is an error"); + assert(p.error_message() == "expected '{' or ';' after method signature but found '}'", + "error message"); +} diff --git a/examples/08_selfhost_parser/parser/modules.cm b/examples/08_selfhost_parser/parser/modules.cm new file mode 100644 index 00000000..e737da0c --- /dev/null +++ b/examples/08_selfhost_parser/parser/modules.cm @@ -0,0 +1,81 @@ +// セルフホストパーサ: モジュール系宣言の解析 +// module宣言・import(選択/ワイルドカード/再エクスポート)・use libc ブロックを扱う +module parser.modules; + +import ../token::*; +import ./state::*; + +// "::"区切りのパス(import経路等)を";"まで表示形で読み取る +export Option parse_path_to_semicolon(Parser* p) { + string out = ""; + while (!p->at_text(";")) { + if (p->at_eof()) { + return p->fail_str("unexpected end of file in import path"); + } + if (p->at_text("{")) { + // 選択import "{a, b}" は中身ごと表示形へ + out = out + "{"; + p->bump(); + bool first = true; + while (!p->at_text("}")) { + if (p->at_eof()) { + return p->fail_str("unexpected end of file in import list"); + } + if (p->at_text(",")) { + p->bump(); + continue; + } + if (!first) { + out = out + ", "; + } + out = out + p->text(); + first = false; + p->bump(); + } + out = out + "}"; + p->bump(); + } else { + out = out + p->text(); + p->bump(); + } + } + p->bump(); + return Option::Some(out); +} + +// module宣言("module"の上から) +export Option parse_module_decl(Parser* p) { + p->bump(); + const Option path = parse_path_to_semicolon(p); + if (path.is_none()) { + return Option::None; + } + p->record_module(path.unwrap()); + return Option::Some(true); +} + +// import宣言("import"の上から) +export Option parse_import_decl(Parser* p) { + p->bump(); + const Option path = parse_path_to_semicolon(p); + if (path.is_none()) { + return Option::None; + } + p->record_import(path.unwrap()); + return Option::Some(true); +} + +// use宣言 "use libc { ... }"("use"の上から) +export Option parse_use_decl(Parser* p) { + p->bump(); + const Option target = p->expect_ident(); + if (target.is_none()) { + return Option::None; + } + const Option b = p->skip_balanced("{", "}"); + if (b.is_none()) { + return Option::None; + } + p->record_use(target.unwrap()); + return Option::Some(true); +} diff --git a/examples/08_selfhost_parser/parser/modules_test.cm b/examples/08_selfhost_parser/parser/modules_test.cm new file mode 100644 index 00000000..84061b38 --- /dev/null +++ b/examples/08_selfhost_parser/parser/modules_test.cm @@ -0,0 +1,52 @@ +// parser/modules.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ../token::*; +import ../lexer::*; +import ./state::*; +import ./modules::*; + +// テスト用: ソースから解析器を組み立てる +Parser parser_of(string src) { + Lexer lx(src); + const Option r = lx.run(); + assert(r.is_some(), "lex should succeed"); + Parser p(lx.take_tokens()); + return p; +} + +#[test] +void parses_module_path() { + Parser p = parser_of("module std.collections.strmap;"); + assert(parse_module_decl(&p).is_some(), "module decl parses"); + assert(p.module_of() == "std.collections.strmap", "dotted path display"); +} + +#[test] +void parses_selective_import() { + Parser p = parser_of("import std::fs::{read_file, exists};"); + assert(parse_import_decl(&p).is_some(), "import parses"); + assert(p.imports_of().len() == 1, "one import recorded"); + assert(p.imports_of()[0] == "std::fs::{read_file, exists}", "selective list display"); +} + +#[test] +void parses_wildcard_and_relative_import() { + Parser p = parser_of("import ../token::*;"); + assert(parse_import_decl(&p).is_some(), "relative import parses"); + assert(p.imports_of()[0] == "../token::*", "relative path display"); +} + +#[test] +void parses_use_libc_block() { + Parser p = parser_of("use libc { void* malloc(int size); void free(void* p); }"); + assert(parse_use_decl(&p).is_some(), "use block parses"); + assert(p.uses_of()[0] == "libc", "use target recorded"); + assert(p.at_eof(), "block consumed"); +} + +#[test] +void unterminated_import_is_error() { + Parser p = parser_of("import std::io"); + assert(parse_import_decl(&p).is_none(), "missing semicolon is an error"); + assert(p.error_message() == "unexpected end of file in import path", "error message"); +} diff --git a/examples/08_selfhost_parser/parser/state.cm b/examples/08_selfhost_parser/parser/state.cm new file mode 100644 index 00000000..7158f9ef --- /dev/null +++ b/examples/08_selfhost_parser/parser/state.cm @@ -0,0 +1,299 @@ +// セルフホストパーサ: 解析器の状態と基本操作 +// トークン列の読み取り・期待・読み飛ばしと、エラー診断・定義収集の置き場。 +// 各宣言の解析は機能別モジュール(modules/types/impls/funcs/decl)のフリー関数が本モジュールの公開APIで行う +module parser.state; + +import ../token::*; + +// 解析器本体(収集結果もここに持つ。全てスライスバックでデストラクタ無し) +export struct Parser { + private TokenStream toks; + private int pos; + // エラー診断(Noneを返した関数がセットする。最初のエラーを保持) + private bool failed; + private string err_msg; + private int err_line; + private int err_col; + // 収集した定義(表示用に整形済みの文字列) + private string module_name; + private string[] imports; + private string[] uses; + private string[] typedefs; + private string[] consts; + private string[] structs; + private string[] enums; + private string[] impls; + private string[] funcs; +} + +export impl Parser { + self(TokenStream tokens) { + self.toks = tokens; + self.pos = 0; + self.failed = false; + self.err_msg = ""; + self.err_line = 0; + self.err_col = 0; + self.module_name = ""; + self.imports = []; + self.uses = []; + self.typedefs = []; + self.consts = []; + self.structs = []; + self.enums = []; + self.impls = []; + self.funcs = []; + } + + // ---- エラー診断アクセサ ---- + + string error_message() { + return self.err_msg; + } + + int error_line() { + return self.err_line; + } + + int error_col() { + return self.err_col; + } + + // ---- 収集結果アクセサ ---- + + string module_of() { + return self.module_name; + } + + string[] imports_of() { + return self.imports; + } + + string[] uses_of() { + return self.uses; + } + + string[] typedefs_of() { + return self.typedefs; + } + + string[] consts_of() { + return self.consts; + } + + string[] structs_of() { + return self.structs; + } + + string[] enums_of() { + return self.enums; + } + + string[] impls_of() { + return self.impls; + } + + string[] funcs_of() { + return self.funcs; + } + + // ---- 収集レコーダ(機能別モジュールが呼ぶ) ---- + + void record_module(string path) { + self.module_name = path; + } + + void record_import(string path) { + self.imports.push(path); + } + + void record_use(string target) { + self.uses.push(target); + } + + void record_typedef(string name) { + self.typedefs.push(name); + } + + void record_const(string name) { + self.consts.push(name); + } + + void record_struct(string display) { + self.structs.push(display); + } + + void record_enum(string display) { + self.enums.push(display); + } + + void record_impl(string display) { + self.impls.push(display); + } + + void record_func(string display) { + self.funcs.push(display); + } + + // ---- トークン読み取りの基本操作 ---- + + int kind() { + return self.toks.kind_at(self.pos); + } + + string text() { + return self.toks.text_at(self.pos); + } + + void bump() { + self.pos = self.pos + 1; + } + + bool at_text(string t) { + return self.text() == t && self.kind() != TK_EOF; + } + + bool at_eof() { + return self.kind() == TK_EOF; + } + + bool at_ident() { + return self.kind() == TK_IDENT; + } + + bool at_string() { + return self.kind() == TK_STRING; + } + + // 失敗を記録してNoneを返すための共通処理(最初のエラーだけを保持する) + Option fail(string msg) { + if (!self.failed) { + self.failed = true; + self.err_msg = msg; + self.err_line = self.toks.line_at(self.pos); + self.err_col = self.toks.col_at(self.pos); + } + return Option::None; + } + + // fail()のOption版 + Option fail_str(string msg) { + const Option e = self.fail(msg); + if (e.is_none()) { + return Option::None; + } + return Option::None; + } + + // 現在トークンの表示形(エラーメッセージ用) + string found_text() { + if (self.at_eof()) { + return "end of file"; + } + return "'" + self.text() + "'"; + } + + // 指定の字句を要求して読み進める(違えばNone) + Option expect_text(string t) { + if (!self.at_text(t)) { + return self.fail("expected '" + t + "' but found " + self.found_text()); + } + self.bump(); + return Option::Some(true); + } + + // 識別子を要求して返す(予約語・記号ならNone) + Option expect_ident() { + if (self.kind() != TK_IDENT) { + return self.fail_str("expected identifier but found " + self.found_text()); + } + const string name = self.text(); + self.bump(); + return Option::Some(name); + } + + // open/closeの対応を取って読み飛ばす(現在位置はopenの上にあること) + Option skip_balanced(string open, string close) { + const Option o = self.expect_text(open); + if (o.is_none()) { + return Option::None; + } + int depth = 1; + while (depth > 0) { + if (self.at_eof()) { + return self.fail("unexpected end of file while looking for '" + close + "'"); + } + if (self.at_text(open)) { + depth = depth + 1; + } else if (self.at_text(close)) { + depth = depth - 1; + } + self.bump(); + } + return Option::Some(true); + } + + // ";"まで読み飛ばす(初期化式内の"{...}"は対応を取って飛ばす) + Option skip_to_semicolon() { + while (!self.at_text(";")) { + if (self.at_eof()) { + return self.fail("unexpected end of file while looking for ';'"); + } + if (self.at_text("{")) { + const Option b = self.skip_balanced("{", "}"); + if (b.is_none()) { + return Option::None; + } + } else { + self.bump(); + } + } + self.bump(); + return Option::Some(true); + } + + // ジェネリックパラメータ "" を読み、表示形を返す(無ければ空文字列) + Option parse_generic_params() { + if (!self.at_text("<")) { + return Option::Some(""); + } + self.bump(); + string out = "<"; + bool first = true; + while (!self.at_text(">")) { + if (self.at_eof()) { + return self.fail_str("unexpected end of file in generic parameter list"); + } + if (self.at_text(",")) { + self.bump(); + continue; + } + if (!first) { + out = out + ", "; + } + out = out + self.text(); + first = false; + self.bump(); + } + self.bump(); + return Option::Some(out + ">"); + } + + // 属性 "#[...]" を読み飛ばす(現在位置は"#"の上) + Option skip_attribute() { + self.bump(); + return self.skip_balanced("[", "]"); + } +} + +// 名前一覧を", "で連結する(表示用) +export string join_names(string[] names) { + string out = ""; + for (int i = 0; i < names.len(); i++) { + if (i > 0) { + out = out + ", "; + } + out = out + names[i]; + } + return out; +} diff --git a/examples/08_selfhost_parser/parser/state_test.cm b/examples/08_selfhost_parser/parser/state_test.cm new file mode 100644 index 00000000..1f53604e --- /dev/null +++ b/examples/08_selfhost_parser/parser/state_test.cm @@ -0,0 +1,69 @@ +// parser/state.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ../token::*; +import ../lexer::*; +import ./state::*; + +// テスト用: ソースから解析器を組み立てる +Parser parser_of(string src) { + Lexer lx(src); + const Option r = lx.run(); + assert(r.is_some(), "lex should succeed"); + Parser p(lx.take_tokens()); + return p; +} + +#[test] +void expect_text_consumes_or_fails() { + Parser p = parser_of("( )"); + assert(p.expect_text("(").is_some(), "matching text consumed"); + assert(p.expect_text("]").is_none(), "mismatch returns None"); + assert(p.error_message() == "expected ']' but found ')'", "error message"); + assert(p.error_line() == 1, "error line"); + assert(p.error_col() == 3, "error col points at the found token"); +} + +#[test] +void expect_ident_rejects_keyword() { + Parser p = parser_of("struct"); + assert(p.expect_ident().is_none(), "keyword is not an identifier"); +} + +#[test] +void skip_balanced_handles_nesting_and_eof() { + Parser p = parser_of("{ a { b } c }"); + assert(p.skip_balanced("{", "}").is_some(), "nested braces"); + assert(p.at_eof(), "consumed to end"); + + Parser q = parser_of("{ never closed"); + assert(q.skip_balanced("{", "}").is_none(), "unclosed block is an error"); +} + +#[test] +void skip_to_semicolon_jumps_over_blocks() { + Parser p = parser_of("int[] xs = { 1, 2 };"); + assert(p.skip_to_semicolon().is_some(), "initializer block skipped"); + assert(p.at_eof(), "consumed to end"); +} + +#[test] +void generic_params_display() { + Parser p = parser_of(" rest"); + const Option gp = p.parse_generic_params(); + assert(gp.is_some(), "generic list parsed"); + assert(gp.unwrap() == "", "display form"); + + Parser q = parser_of("plain"); + assert(q.parse_generic_params().unwrap() == "", "no generics yields empty string"); +} + +#[test] +void recorders_accumulate() { + Parser p = parser_of(""); + p.record_struct("A(x)"); + p.record_struct("B(y)"); + p.record_func("int main()"); + assert(p.structs_of().len() == 2, "two structs recorded"); + assert(p.funcs_of().len() == 1, "one function recorded"); + assert(join_names(p.structs_of()) == "A(x), B(y)", "join_names order"); +} diff --git a/examples/08_selfhost_parser/parser/types.cm b/examples/08_selfhost_parser/parser/types.cm new file mode 100644 index 00000000..bab33498 --- /dev/null +++ b/examples/08_selfhost_parser/parser/types.cm @@ -0,0 +1,153 @@ +// セルフホストパーサ: 型定義(struct / enum)の解析 +// ネスト型(struct内struct・enum内enum、v0.17.1)と無名struct宣言子・with句・ジェネリクスに対応する +module parser.types; + +import ../token::*; +import ./state::*; + +// struct宣言("struct"の上から)。prefixは"Outer::"のようなネスト名の接頭辞 +export Option parse_struct(Parser* p, string prefix) { + p->bump(); + string name = ""; + if (p->at_ident()) { + name = p->text(); + p->bump(); + } + const Option gp = p->parse_generic_params(); + if (gp.is_none()) { + return Option::None; + } + // "with Trait, ..." は"{"まで読み飛ばす + while (!p->at_text("{")) { + if (p->at_eof() || p->at_text(";") || p->at_text("}")) { + return p->fail("expected '{' in struct declaration but found " + p->found_text()); + } + p->bump(); + } + p->bump(); + // 本体: フィールドとネスト型を収集 + string[] fields = []; + while (!p->at_text("}")) { + if (p->at_eof()) { + return p->fail("unexpected end of file in struct body"); + } + if (p->at_text("#")) { + const Option a = p->skip_attribute(); + if (a.is_none()) { + return Option::None; + } + continue; + } + if (p->at_text("private")) { + p->bump(); + continue; + } + if (p->at_text("struct")) { + const Option n = parse_struct(p, prefix + name + "::"); + if (n.is_none()) { + return Option::None; + } + // ネスト型直後の宣言子付き ";" または "NAME;" を消費 + const Option d = p->skip_to_semicolon(); + if (d.is_none()) { + return Option::None; + } + continue; + } + if (p->at_text("enum")) { + const Option n = parse_enum(p, prefix + name + "::"); + if (n.is_none()) { + return Option::None; + } + const Option d = p->skip_to_semicolon(); + if (d.is_none()) { + return Option::None; + } + continue; + } + // 通常フィールド: ";"までの最後の識別子がフィールド名 + string last_ident = ""; + while (!p->at_text(";")) { + if (p->at_eof()) { + return p->fail("unexpected end of file in struct field"); + } + if (p->at_ident()) { + last_ident = p->text(); + } + p->bump(); + } + p->bump(); + if (last_ident != "") { + fields.push(last_ident); + } + } + p->bump(); + // 無名structの宣言子("struct {...} NAME;")は呼び出し元が拾う + string display = prefix + name + gp.unwrap(); + if (name == "") { + display = prefix + "(anonymous)" + gp.unwrap(); + } + p->record_struct(display + " { " + join_names(fields) + " }"); + return Option::Some(true); +} + +// enum宣言("enum"の上から)。ペイロード付きバリアント・明示値・ネストenumに対応 +export Option parse_enum(Parser* p, string prefix) { + p->bump(); + string name = ""; + if (p->at_ident()) { + name = p->text(); + p->bump(); + } + const Option gp = p->parse_generic_params(); + if (gp.is_none()) { + return Option::None; + } + const Option o = p->expect_text("{"); + if (o.is_none()) { + return Option::None; + } + string[] variants = []; + while (!p->at_text("}")) { + if (p->at_eof()) { + return p->fail("unexpected end of file in enum body"); + } + if (p->at_text(",")) { + p->bump(); + continue; + } + if (p->at_text("enum")) { + // ネストenum(v0.17.1) + const Option n = parse_enum(p, prefix + name + "::"); + if (n.is_none()) { + return Option::None; + } + continue; + } + if (!p->at_ident()) { + return p->fail("expected enum variant name but found " + p->found_text()); + } + variants.push(p->text()); + p->bump(); + // ペイロード "(...)" と明示値 "= n" を読み飛ばす + if (p->at_text("(")) { + const Option pay = p->skip_balanced("(", ")"); + if (pay.is_none()) { + return Option::None; + } + } + if (p->at_text("=")) { + p->bump(); + while (!p->at_text(",") && !p->at_text("}")) { + if (p->at_eof()) { + return p->fail("unexpected end of file in enum value"); + } + p->bump(); + } + } + } + p->bump(); + string display = prefix + name + gp.unwrap(); + p->record_enum(display + " { " + join_names(variants) + " }"); + return Option::Some(true); +} diff --git a/examples/08_selfhost_parser/parser/types_test.cm b/examples/08_selfhost_parser/parser/types_test.cm new file mode 100644 index 00000000..c9c7945e --- /dev/null +++ b/examples/08_selfhost_parser/parser/types_test.cm @@ -0,0 +1,53 @@ +// parser/types.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ../token::*; +import ../lexer::*; +import ./state::*; +import ./types::*; + +// テスト用: ソースから解析器を組み立てる +Parser parser_of(string src) { + Lexer lx(src); + const Option r = lx.run(); + assert(r.is_some(), "lex should succeed"); + Parser p(lx.take_tokens()); + return p; +} + +#[test] +void parses_struct_with_fields() { + Parser p = parser_of("struct Point { double x; double y; }"); + assert(parse_struct(&p, "").is_some(), "struct parses"); + assert(p.structs_of()[0] == "Point {{ x, y }}", "name and field list"); +} + +#[test] +void parses_generic_struct_with_traits_and_private() { + Parser p = parser_of("struct Pair with Debug { private K key; V val; }"); + assert(parse_struct(&p, "").is_some(), "generic struct with with-clause parses"); + assert(p.structs_of()[0] == "Pair {{ key, val }}", "generics kept, private stripped"); +} + +#[test] +void parses_nested_struct_and_enum() { + Parser p = parser_of("struct Outer { struct Inner { int v; } in; enum Mode { A, B } m; int t; }"); + assert(parse_struct(&p, "").is_some(), "nested types parse"); + assert(p.structs_of().len() == 2, "outer and inner recorded"); + assert(p.structs_of()[0] == "Outer::Inner {{ v }}", "nested name is qualified"); + assert(p.enums_of()[0] == "Outer::Mode {{ A, B }}", "nested enum qualified"); +} + +#[test] +void parses_enum_with_payload_and_value() { + Parser p = parser_of("enum Shape { Dot, Circle(double), Rect(double, double), Alias = 7 }"); + assert(parse_enum(&p, "").is_some(), "enum parses"); + assert(p.enums_of()[0] == "Shape {{ Dot, Circle, Rect, Alias }}", "variant names collected"); +} + +#[test] +void struct_without_brace_is_error() { + Parser p = parser_of("struct Broken;"); + assert(parse_struct(&p, "").is_none(), "missing body is an error"); + assert(p.error_message() == "expected '{' in struct declaration but found ';'", + "error message"); +} diff --git a/examples/08_selfhost_parser/sample_error.cm b/examples/08_selfhost_parser/sample_error.cm new file mode 100644 index 00000000..9054d72f --- /dev/null +++ b/examples/08_selfhost_parser/sample_error.cm @@ -0,0 +1,17 @@ +// セルフホストパーサの入力サンプル(エラー系): 12行目のメソッドに本体が無い +module sample.error; + +struct Ok { + int fine; +} + +impl Ok { + int works() { + return 1; + } + int broken() +} + +int main() { + return 0; +} diff --git a/examples/08_selfhost_parser/sample_ok.cm b/examples/08_selfhost_parser/sample_ok.cm new file mode 100644 index 00000000..11fa03b7 --- /dev/null +++ b/examples/08_selfhost_parser/sample_ok.cm @@ -0,0 +1,66 @@ +// セルフホストパーサの入力サンプル(正常系): 主要な宣言種別を一通り含む +module sample.ok; + +import std::io::println; +import std::collections::strmap::*; + +use libc { + void* malloc(int size); + void free(void* ptr); +} + +typedef Meters = double; + +const int LIMIT = 100; + +#[derive(Debug)] +struct Point with Clone { + T x; + T y; + private int cached; +} + +struct Outer { + struct Inner { + int value; + } inner; + int tag; +} + +enum Shape { + Dot, + Circle(double), + Named(string) +} + +interface Area { + double area(); +} + +impl Point { + self() { + self.cached = 0; + } + + ~self() { + } + + private int helper() { + return self.cached; + } + + static int origin_count() { + return 1; + } +} + +extern "C" long cm_now_ms(); + + T pick_first(T[] xs) { + return xs[0]; +} + +int main() { + println("sample"); + return 0; +} diff --git a/examples/08_selfhost_parser/token.cm b/examples/08_selfhost_parser/token.cm new file mode 100644 index 00000000..9f54e854 --- /dev/null +++ b/examples/08_selfhost_parser/token.cm @@ -0,0 +1,70 @@ +// セルフホストパーサ: トークン定義 +// トークン種別定数と、字句解析結果のトークン列(種別・字句・行・桁のパラレルスライス)を提供する +module token; + +// トークン種別(パーサはこの定数とトークン字句で判定する) +export const int TK_EOF = 0; +export const int TK_IDENT = 1; // 識別子 +export const int TK_KEYWORD = 2; // 予約語(識別子のうちキーワード表に載っているもの) +export const int TK_NUMBER = 3; // 数値リテラル +export const int TK_STRING = 4; // 文字列リテラル(引用符ごと保持) +export const int TK_CHAR = 5; // 文字リテラル +export const int TK_SYMBOL = 6; // 記号("::"のみ2文字、他は1文字) + +// 字句解析の結果トークン列(スライスバックでデストラクタ無し) +export struct TokenStream { + private int[] kinds; + private string[] texts; + private int[] lines; + private int[] cols; +} + +export impl TokenStream { + self() { + self.kinds = []; + self.texts = []; + self.lines = []; + self.cols = []; + } + + void push_token(int kind, string text, int line, int col) { + self.kinds.push(kind); + self.texts.push(text); + self.lines.push(line); + self.cols.push(col); + } + + // トークン数(終端のTK_EOFを含む) + int len() { + return self.kinds.len(); + } + + // 範囲外はEOF扱いで安全に読める + int kind_at(int i) { + if (i < 0 || i >= self.kinds.len()) { + return TK_EOF; + } + return self.kinds[i]; + } + + string text_at(int i) { + if (i < 0 || i >= self.texts.len()) { + return ""; + } + return self.texts[i]; + } + + int line_at(int i) { + if (i < 0 || i >= self.lines.len()) { + return 0; + } + return self.lines[i]; + } + + int col_at(int i) { + if (i < 0 || i >= self.cols.len()) { + return 0; + } + return self.cols[i]; + } +} diff --git a/examples/08_selfhost_parser/token_test.cm b/examples/08_selfhost_parser/token_test.cm new file mode 100644 index 00000000..3870a510 --- /dev/null +++ b/examples/08_selfhost_parser/token_test.cm @@ -0,0 +1,25 @@ +// token.cm の単体テスト(#[test] 関数を `cm test` が実行する) +import std::debug::assert; +import ./token::*; + +#[test] +void push_and_read_back() { + TokenStream ts(); + ts.push_token(TK_IDENT, "foo", 1, 5); + ts.push_token(TK_SYMBOL, "::", 1, 8); + assert(ts.len() == 2, "len after two pushes"); + assert(ts.kind_at(0) == TK_IDENT, "kind of first token"); + assert(ts.text_at(0) == "foo", "text of first token"); + assert(ts.line_at(1) == 1, "line of second token"); + assert(ts.col_at(1) == 8, "col of second token"); +} + +#[test] +void out_of_range_is_eof() { + TokenStream ts(); + ts.push_token(TK_NUMBER, "42", 3, 1); + assert(ts.kind_at(99) == TK_EOF, "past-end kind is EOF"); + assert(ts.kind_at(-1) == TK_EOF, "negative index kind is EOF"); + assert(ts.text_at(99) == "", "past-end text is empty"); + assert(ts.line_at(99) == 0, "past-end line is 0"); +} diff --git a/examples/README.md b/examples/README.md index e1458150..da8a05dd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -37,6 +37,11 @@ Cmの機能を示す公式サンプル集です。 - `main.cm` - コンパイラの縮図となるCLIツール。`env::args()`で引数解析、`fs::read_bytes`で読み込み、`strings::lines/split`でトークン数を集計、`bytes::push_u32_le`+`fs::write_bytes`でバイナリ成果物を出力、`process::run`で外部コマンド呼び出し - `sample_input.cm` - 入力サンプル。`cm run examples/07_selfhost_drill/main.cm -- examples/07_selfhost_drill/sample_input.cm -o out.bin` で実行 +### 08_selfhost_parser - セルフホストパーサ(宣言リスタ) +- Cmで書いたCmソースの宣言レベルパーサ。成功なら定義一覧(struct/enum/impl/関数等)、失敗ならエラー箇所(行:桁+キャレット)を出力 +- 機能別にファイル分割(token/lexer/parser/state・modules・types・impls・funcs・decl)し、各ファイルに`#[test]`単体テストが付属 +- `cm run examples/08_selfhost_parser/main.cm -- ` で実行。自分自身の全ソースを自己解析できる + ### uefi - UEFI アプリケーション - `hello_world.cm` - UEFI Hello World(画面出力) - `memory_test.cm` - メモリ管理テスト(AllocatePool/FreePool) diff --git a/scripts/ci/check_examples.sh b/scripts/ci/check_examples.sh index 889e77bf..99201390 100755 --- a/scripts/ci/check_examples.sh +++ b/scripts/ci/check_examples.sh @@ -10,7 +10,7 @@ while IFS= read -r file; do ./cm check "$file" 2>&1 | head -20 || true fail=1 fi -done < <(find examples -name '*.cm' -not -path 'examples/uefi/util/*' -not -path 'examples/uefi/libs/*') +done < <(find examples -name '*.cm' -not -path 'examples/uefi/util/*' -not -path 'examples/uefi/libs/*' -not -path 'examples/08_selfhost_parser/sample_error.cm') if [ "$fail" -eq 0 ]; then echo "✅ examples check passed" @@ -25,4 +25,20 @@ DRILL=examples/07_selfhost_drill cmp /tmp/drill_jit.bin /tmp/drill_native.bin echo "✅ selfhost drill passed (jit/native artifacts identical)" +# セルフホストパーサ(examples/08_selfhost_parser)の検証: +# 各モジュールの#[test]単体テスト → 正常サンプルの定義一覧出力(rc=0) → エラーサンプルの位置報告(rc=1) → パーサ自身の全ソース自己解析 +SHP=examples/08_selfhost_parser +for t in token_test lexer_test parser/state_test parser/modules_test parser/types_test parser/impls_test parser/funcs_test parser/decl_test; do + ./cm test "$SHP/$t.cm" >/dev/null +done +./cm run "$SHP/main.cm" -- "$SHP/sample_ok.cm" >/dev/null +if ./cm run "$SHP/main.cm" -- "$SHP/sample_error.cm" >/dev/null 2>&1; then + echo "::error::selfhost parser should report an error for sample_error.cm" + exit 1 +fi +./cm run "$SHP/main.cm" -- "$SHP/main.cm" "$SHP/token.cm" "$SHP/lexer.cm" \ + "$SHP/parser/state.cm" "$SHP/parser/modules.cm" "$SHP/parser/types.cm" \ + "$SHP/parser/impls.cm" "$SHP/parser/funcs.cm" "$SHP/parser/decl.cm" >/dev/null +echo "✅ selfhost parser passed (unit tests / ok-sample / error-sample / self-parse)" + exit "$fail" From 02e106b9f98dd214651ab31a0c8392666a33d453 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 17:32:12 +0900 Subject: [PATCH 07/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E3=81=AE=E9=9A=8E?= =?UTF-8?q?=E5=B1=A4=E3=82=92=E7=B4=B0=E5=88=86=E5=8C=96=EF=BC=88=E3=83=88?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=81=AFmain.cm=E3=81=AE=E3=81=BF=E3=83=BB?= =?UTF-8?q?=E6=AE=B5=E9=9A=8E=E3=81=94=E3=81=A8=E3=81=AE=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AB=E3=83=80=E6=A7=8B=E6=88=90=E3=81=B8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit フラットに置かれていたモジュールを処理段階ごとのディレクトリへ再配置した: - lexer/(token.cm=トークン定義・scan.cm=字句解析器、旧lexer.cmを改名) - parser/state.cm(解析器の状態・基本操作) - parser/decl/(宣言種別ごとの解析: modules・types・impls・funcs・dispatch、旧decl.cmはディレクトリ名との重複を避けdispatch.cmへ改名) - samples/(ok.cm・error.cm、旧sample_*.cmはフォルダ名の重複を避け改名) - 各*_test.cmは対象モジュールと同じディレクトリへ同伴 モジュール宣言(module lexer.scan等)と相対importを新パスへ追従し、READMEのディレクトリ構成表とCI(check_examples.sh)の単体テスト・サンプル・自己解析パスを更新した。 再配置後も全単体テスト38件・正常/エラーサンプル・自己解析9ファイルの通過を確認済み --- examples/08_selfhost_parser/README.md | 40 +++++++++++-------- .../{lexer.cm => lexer/scan.cm} | 2 +- .../{lexer_test.cm => lexer/scan_test.cm} | 2 +- .../08_selfhost_parser/{ => lexer}/token.cm | 2 +- .../{ => lexer}/token_test.cm | 0 examples/08_selfhost_parser/main.cm | 4 +- .../parser/{decl.cm => decl/dispatch.cm} | 6 +-- .../{decl_test.cm => decl/dispatch_test.cm} | 8 ++-- .../parser/{ => decl}/funcs.cm | 6 +-- .../parser/{ => decl}/funcs_test.cm | 6 +-- .../parser/{ => decl}/impls.cm | 6 +-- .../parser/{ => decl}/impls_test.cm | 6 +-- .../parser/{ => decl}/modules.cm | 6 +-- .../parser/{ => decl}/modules_test.cm | 6 +-- .../parser/{ => decl}/types.cm | 6 +-- .../parser/{ => decl}/types_test.cm | 6 +-- examples/08_selfhost_parser/parser/state.cm | 2 +- .../08_selfhost_parser/parser/state_test.cm | 4 +- .../{sample_error.cm => samples/error.cm} | 0 .../{sample_ok.cm => samples/ok.cm} | 0 examples/README.md | 2 +- scripts/ci/check_examples.sh | 16 ++++---- 22 files changed, 71 insertions(+), 65 deletions(-) rename examples/08_selfhost_parser/{lexer.cm => lexer/scan.cm} (99%) rename examples/08_selfhost_parser/{lexer_test.cm => lexer/scan_test.cm} (99%) rename examples/08_selfhost_parser/{ => lexer}/token.cm (99%) rename examples/08_selfhost_parser/{ => lexer}/token_test.cm (100%) rename examples/08_selfhost_parser/parser/{decl.cm => decl/dispatch.cm} (96%) rename examples/08_selfhost_parser/parser/{decl_test.cm => decl/dispatch_test.cm} (95%) rename examples/08_selfhost_parser/parser/{ => decl}/funcs.cm (98%) rename examples/08_selfhost_parser/parser/{ => decl}/funcs_test.cm (96%) rename examples/08_selfhost_parser/parser/{ => decl}/impls.cm (98%) rename examples/08_selfhost_parser/parser/{ => decl}/impls_test.cm (95%) rename examples/08_selfhost_parser/parser/{ => decl}/modules.cm (96%) rename examples/08_selfhost_parser/parser/{ => decl}/modules_test.cm (95%) rename examples/08_selfhost_parser/parser/{ => decl}/types.cm (98%) rename examples/08_selfhost_parser/parser/{ => decl}/types_test.cm (96%) rename examples/08_selfhost_parser/{sample_error.cm => samples/error.cm} (100%) rename examples/08_selfhost_parser/{sample_ok.cm => samples/ok.cm} (100%) diff --git a/examples/08_selfhost_parser/README.md b/examples/08_selfhost_parser/README.md index 32400912..be8a03d1 100644 --- a/examples/08_selfhost_parser/README.md +++ b/examples/08_selfhost_parser/README.md @@ -17,25 +17,31 @@ cm run examples/08_selfhost_parser/main.cm -- path/to/file.cm [more.cm ...] cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/main.cm # 正常系サンプル: 定義一覧が出る -cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/sample_ok.cm +cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/samples/ok.cm # エラー系サンプル: 13行目のメソッド本体欠落を位置付きで報告する -cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/sample_error.cm +cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/samples/error.cm ``` -## ファイル構成(機能ごとに分割) +## ディレクトリ構成(トップは`main.cm`のみ・段階ごとにフォルダ分割) -| ファイル | 役割 | -|---|---| -| `token.cm` | トークン種別定数と`TokenStream`(パラレルスライス) | -| `lexer.cm` | 字句解析器(コメント/文字列/文字/数値/`::`融合、キーワード判定は`StringSet`) | -| `parser/state.cm` | `Parser`状態・基本操作(expect/skip/ジェネリクス読取)・エラー診断・定義レコーダ | -| `parser/modules.cm` | module / import(選択・相対・ワイルドカード)/ use libc | -| `parser/types.cm` | struct / enum(ネスト型・無名struct宣言子・with句・ペイロード付きバリアント) | -| `parser/impls.cm` | impl / interface(self・~self・private・static・overload) | -| `parser/funcs.cm` | 関数 / typedef / const / extern "C" | -| `parser/decl.cm` | トップレベル宣言のディスパッチと`parse_program` | -| `main.cm` | CLI(引数解析・読み込み・結果出力) | +``` +main.cm CLI(引数解析・読み込み・結果出力) +lexer/ 字句解析 + token.cm トークン種別定数とTokenStream(パラレルスライス) + scan.cm 字句解析器(コメント/文字列/文字/数値/::融合、キーワード判定はStringSet) +parser/ 構文解析 + state.cm Parser状態・基本操作(expect/skip/ジェネリクス読取)・エラー診断・定義レコーダ + decl/ 宣言種別ごとの解析 + modules.cm module / import(選択・相対・ワイルドカード)/ use libc + types.cm struct / enum(ネスト型・無名struct宣言子・with句・ペイロード付きバリアント) + impls.cm impl / interface(self・~self・private・static・overload) + funcs.cm 関数 / typedef / const / extern "C" + dispatch.cm トップレベル宣言のディスパッチとparse_program +samples/ パーサへ入力するサンプル + ok.cm 正常系(全宣言種別を含む) + error.cm エラー系(メソッド本体欠落) +``` 関数本体・メソッド本体は波括弧の対応で読み飛ばす「定義リスタ」としての解析です(文字列リテラル内の波括弧は字句解析済みのため誤検出しません)。 @@ -44,9 +50,9 @@ cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/sample 各ファイルに`#[test]`ディレクティブの単体テスト(`*_test.cm`)が付属します。 ```bash -cm test examples/08_selfhost_parser/lexer_test.cm -cm test examples/08_selfhost_parser/parser/types_test.cm -# ...(token / state / modules / impls / funcs / decl も同様) +cm test examples/08_selfhost_parser/lexer/scan_test.cm +cm test examples/08_selfhost_parser/parser/decl/types_test.cm +# ...(lexer/token / parser/state / parser/decl/{modules, impls, funcs, dispatch} も同様) ``` CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、8ファイルの単体テスト・正常/エラーサンプル・自己解析を検証します。 diff --git a/examples/08_selfhost_parser/lexer.cm b/examples/08_selfhost_parser/lexer/scan.cm similarity index 99% rename from examples/08_selfhost_parser/lexer.cm rename to examples/08_selfhost_parser/lexer/scan.cm index 246b69e6..7684ce6e 100644 --- a/examples/08_selfhost_parser/lexer.cm +++ b/examples/08_selfhost_parser/lexer/scan.cm @@ -1,7 +1,7 @@ // セルフホストパーサ: 字句解析器 // Cmソースをトークン列へ分解する。 // エラーハンドリングはOption型で行い、失敗(未終端文字列・未終端コメント等)の位置はLexer側に保持する -module lexer; +module lexer.scan; import std::collections::strset::*; import std::strings::from_bytes::*; diff --git a/examples/08_selfhost_parser/lexer_test.cm b/examples/08_selfhost_parser/lexer/scan_test.cm similarity index 99% rename from examples/08_selfhost_parser/lexer_test.cm rename to examples/08_selfhost_parser/lexer/scan_test.cm index b088f5e8..5dd6f93f 100644 --- a/examples/08_selfhost_parser/lexer_test.cm +++ b/examples/08_selfhost_parser/lexer/scan_test.cm @@ -1,7 +1,7 @@ // lexer.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; import ./token::*; -import ./lexer::*; +import ./scan::*; // テスト用: ソースを字句解析してトークン列を返す(成功前提) TokenStream lex_ok(string src) { diff --git a/examples/08_selfhost_parser/token.cm b/examples/08_selfhost_parser/lexer/token.cm similarity index 99% rename from examples/08_selfhost_parser/token.cm rename to examples/08_selfhost_parser/lexer/token.cm index 9f54e854..fa3d0db2 100644 --- a/examples/08_selfhost_parser/token.cm +++ b/examples/08_selfhost_parser/lexer/token.cm @@ -1,6 +1,6 @@ // セルフホストパーサ: トークン定義 // トークン種別定数と、字句解析結果のトークン列(種別・字句・行・桁のパラレルスライス)を提供する -module token; +module lexer.token; // トークン種別(パーサはこの定数とトークン字句で判定する) export const int TK_EOF = 0; diff --git a/examples/08_selfhost_parser/token_test.cm b/examples/08_selfhost_parser/lexer/token_test.cm similarity index 100% rename from examples/08_selfhost_parser/token_test.cm rename to examples/08_selfhost_parser/lexer/token_test.cm diff --git a/examples/08_selfhost_parser/main.cm b/examples/08_selfhost_parser/main.cm index 4045639a..d4ea7fd8 100644 --- a/examples/08_selfhost_parser/main.cm +++ b/examples/08_selfhost_parser/main.cm @@ -6,9 +6,9 @@ import std::env::args; import std::fs::{read_file, exists}; import std::strings::split::{lines}; import std::strings::format::*; -import ./lexer::*; +import ./lexer/scan::*; import ./parser/state::*; -import ./parser/decl::*; +import ./parser/decl/dispatch::*; // エラー位置のソース行をキャレット付きで表示する void print_error_context(string path, string src, int line, int col) { diff --git a/examples/08_selfhost_parser/parser/decl.cm b/examples/08_selfhost_parser/parser/decl/dispatch.cm similarity index 96% rename from examples/08_selfhost_parser/parser/decl.cm rename to examples/08_selfhost_parser/parser/decl/dispatch.cm index d630d2a7..3a5c2c08 100644 --- a/examples/08_selfhost_parser/parser/decl.cm +++ b/examples/08_selfhost_parser/parser/decl/dispatch.cm @@ -1,9 +1,9 @@ // セルフホストパーサ: トップレベル宣言のディスパッチ // 先頭トークンで宣言種別を判定し、機能別モジュール(modules/types/impls/funcs)へ振り分ける -module parser.decl; +module parser.decl.dispatch; -import ../token::*; -import ./state::*; +import ../../lexer/token::*; +import ../state::*; import ./modules::*; import ./types::*; import ./impls::*; diff --git a/examples/08_selfhost_parser/parser/decl_test.cm b/examples/08_selfhost_parser/parser/decl/dispatch_test.cm similarity index 95% rename from examples/08_selfhost_parser/parser/decl_test.cm rename to examples/08_selfhost_parser/parser/decl/dispatch_test.cm index 6a3c0b24..696d46fa 100644 --- a/examples/08_selfhost_parser/parser/decl_test.cm +++ b/examples/08_selfhost_parser/parser/decl/dispatch_test.cm @@ -1,9 +1,9 @@ // parser/decl.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../token::*; -import ../lexer::*; -import ./state::*; -import ./decl::*; +import ../../lexer/token::*; +import ../../lexer/scan::*; +import ../state::*; +import ./dispatch::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { diff --git a/examples/08_selfhost_parser/parser/funcs.cm b/examples/08_selfhost_parser/parser/decl/funcs.cm similarity index 98% rename from examples/08_selfhost_parser/parser/funcs.cm rename to examples/08_selfhost_parser/parser/decl/funcs.cm index 0e6f1bba..beedfce9 100644 --- a/examples/08_selfhost_parser/parser/funcs.cm +++ b/examples/08_selfhost_parser/parser/decl/funcs.cm @@ -1,9 +1,9 @@ // セルフホストパーサ: 関数・定数・typedef・extern宣言の解析 // トップレベルの「型トークン列から始まる宣言」(関数 / グローバル定義)と、typedef・const・extern "C" を扱う -module parser.funcs; +module parser.decl.funcs; -import ../token::*; -import ./state::*; +import ../../lexer/token::*; +import ../state::*; // typedef宣言("typedef"の上から) export Option parse_typedef(Parser* p) { diff --git a/examples/08_selfhost_parser/parser/funcs_test.cm b/examples/08_selfhost_parser/parser/decl/funcs_test.cm similarity index 96% rename from examples/08_selfhost_parser/parser/funcs_test.cm rename to examples/08_selfhost_parser/parser/decl/funcs_test.cm index 12bf04cc..d85ab320 100644 --- a/examples/08_selfhost_parser/parser/funcs_test.cm +++ b/examples/08_selfhost_parser/parser/decl/funcs_test.cm @@ -1,8 +1,8 @@ // parser/funcs.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../token::*; -import ../lexer::*; -import ./state::*; +import ../../lexer/token::*; +import ../../lexer/scan::*; +import ../state::*; import ./funcs::*; // テスト用: ソースから解析器を組み立てる diff --git a/examples/08_selfhost_parser/parser/impls.cm b/examples/08_selfhost_parser/parser/decl/impls.cm similarity index 98% rename from examples/08_selfhost_parser/parser/impls.cm rename to examples/08_selfhost_parser/parser/decl/impls.cm index bdb510e5..bf783386 100644 --- a/examples/08_selfhost_parser/parser/impls.cm +++ b/examples/08_selfhost_parser/parser/decl/impls.cm @@ -1,10 +1,10 @@ // セルフホストパーサ: 実装ブロック(impl / interface)の解析 // impl Name (for Iface)? { メソッド... } のメソッド名収集と、interface宣言を扱う。 // メソッドはコンストラクタ self / デストラクタ ~self / private / static / overload / operator に対応する -module parser.impls; +module parser.decl.impls; -import ../token::*; -import ./state::*; +import ../../lexer/token::*; +import ../state::*; // impl内の1メソッドを解析し、メソッド名を返す export Option parse_method(Parser* p) { diff --git a/examples/08_selfhost_parser/parser/impls_test.cm b/examples/08_selfhost_parser/parser/decl/impls_test.cm similarity index 95% rename from examples/08_selfhost_parser/parser/impls_test.cm rename to examples/08_selfhost_parser/parser/decl/impls_test.cm index 81ff95d6..968bfb00 100644 --- a/examples/08_selfhost_parser/parser/impls_test.cm +++ b/examples/08_selfhost_parser/parser/decl/impls_test.cm @@ -1,8 +1,8 @@ // parser/impls.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../token::*; -import ../lexer::*; -import ./state::*; +import ../../lexer/token::*; +import ../../lexer/scan::*; +import ../state::*; import ./impls::*; // テスト用: ソースから解析器を組み立てる diff --git a/examples/08_selfhost_parser/parser/modules.cm b/examples/08_selfhost_parser/parser/decl/modules.cm similarity index 96% rename from examples/08_selfhost_parser/parser/modules.cm rename to examples/08_selfhost_parser/parser/decl/modules.cm index e737da0c..94e12f9e 100644 --- a/examples/08_selfhost_parser/parser/modules.cm +++ b/examples/08_selfhost_parser/parser/decl/modules.cm @@ -1,9 +1,9 @@ // セルフホストパーサ: モジュール系宣言の解析 // module宣言・import(選択/ワイルドカード/再エクスポート)・use libc ブロックを扱う -module parser.modules; +module parser.decl.modules; -import ../token::*; -import ./state::*; +import ../../lexer/token::*; +import ../state::*; // "::"区切りのパス(import経路等)を";"まで表示形で読み取る export Option parse_path_to_semicolon(Parser* p) { diff --git a/examples/08_selfhost_parser/parser/modules_test.cm b/examples/08_selfhost_parser/parser/decl/modules_test.cm similarity index 95% rename from examples/08_selfhost_parser/parser/modules_test.cm rename to examples/08_selfhost_parser/parser/decl/modules_test.cm index 84061b38..803b4750 100644 --- a/examples/08_selfhost_parser/parser/modules_test.cm +++ b/examples/08_selfhost_parser/parser/decl/modules_test.cm @@ -1,8 +1,8 @@ // parser/modules.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../token::*; -import ../lexer::*; -import ./state::*; +import ../../lexer/token::*; +import ../../lexer/scan::*; +import ../state::*; import ./modules::*; // テスト用: ソースから解析器を組み立てる diff --git a/examples/08_selfhost_parser/parser/types.cm b/examples/08_selfhost_parser/parser/decl/types.cm similarity index 98% rename from examples/08_selfhost_parser/parser/types.cm rename to examples/08_selfhost_parser/parser/decl/types.cm index bab33498..77796bef 100644 --- a/examples/08_selfhost_parser/parser/types.cm +++ b/examples/08_selfhost_parser/parser/decl/types.cm @@ -1,9 +1,9 @@ // セルフホストパーサ: 型定義(struct / enum)の解析 // ネスト型(struct内struct・enum内enum、v0.17.1)と無名struct宣言子・with句・ジェネリクスに対応する -module parser.types; +module parser.decl.types; -import ../token::*; -import ./state::*; +import ../../lexer/token::*; +import ../state::*; // struct宣言("struct"の上から)。prefixは"Outer::"のようなネスト名の接頭辞 export Option parse_struct(Parser* p, string prefix) { diff --git a/examples/08_selfhost_parser/parser/types_test.cm b/examples/08_selfhost_parser/parser/decl/types_test.cm similarity index 96% rename from examples/08_selfhost_parser/parser/types_test.cm rename to examples/08_selfhost_parser/parser/decl/types_test.cm index c9c7945e..cea2d2bf 100644 --- a/examples/08_selfhost_parser/parser/types_test.cm +++ b/examples/08_selfhost_parser/parser/decl/types_test.cm @@ -1,8 +1,8 @@ // parser/types.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../token::*; -import ../lexer::*; -import ./state::*; +import ../../lexer/token::*; +import ../../lexer/scan::*; +import ../state::*; import ./types::*; // テスト用: ソースから解析器を組み立てる diff --git a/examples/08_selfhost_parser/parser/state.cm b/examples/08_selfhost_parser/parser/state.cm index 7158f9ef..eca24543 100644 --- a/examples/08_selfhost_parser/parser/state.cm +++ b/examples/08_selfhost_parser/parser/state.cm @@ -3,7 +3,7 @@ // 各宣言の解析は機能別モジュール(modules/types/impls/funcs/decl)のフリー関数が本モジュールの公開APIで行う module parser.state; -import ../token::*; +import ../lexer/token::*; // 解析器本体(収集結果もここに持つ。全てスライスバックでデストラクタ無し) export struct Parser { diff --git a/examples/08_selfhost_parser/parser/state_test.cm b/examples/08_selfhost_parser/parser/state_test.cm index 1f53604e..a5c59933 100644 --- a/examples/08_selfhost_parser/parser/state_test.cm +++ b/examples/08_selfhost_parser/parser/state_test.cm @@ -1,7 +1,7 @@ // parser/state.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../token::*; -import ../lexer::*; +import ../lexer/token::*; +import ../lexer/scan::*; import ./state::*; // テスト用: ソースから解析器を組み立てる diff --git a/examples/08_selfhost_parser/sample_error.cm b/examples/08_selfhost_parser/samples/error.cm similarity index 100% rename from examples/08_selfhost_parser/sample_error.cm rename to examples/08_selfhost_parser/samples/error.cm diff --git a/examples/08_selfhost_parser/sample_ok.cm b/examples/08_selfhost_parser/samples/ok.cm similarity index 100% rename from examples/08_selfhost_parser/sample_ok.cm rename to examples/08_selfhost_parser/samples/ok.cm diff --git a/examples/README.md b/examples/README.md index da8a05dd..6b4c96e0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,7 +39,7 @@ Cmの機能を示す公式サンプル集です。 ### 08_selfhost_parser - セルフホストパーサ(宣言リスタ) - Cmで書いたCmソースの宣言レベルパーサ。成功なら定義一覧(struct/enum/impl/関数等)、失敗ならエラー箇所(行:桁+キャレット)を出力 -- 機能別にファイル分割(token/lexer/parser/state・modules・types・impls・funcs・decl)し、各ファイルに`#[test]`単体テストが付属 +- トップは`main.cm`のみで、段階ごとに`lexer/`(token・scan)・`parser/`(state)・`parser/decl/`(modules・types・impls・funcs・dispatch)・`samples/`へ分割し、各モジュールに`#[test]`単体テストが付属 - `cm run examples/08_selfhost_parser/main.cm -- ` で実行。自分自身の全ソースを自己解析できる ### uefi - UEFI アプリケーション diff --git a/scripts/ci/check_examples.sh b/scripts/ci/check_examples.sh index 99201390..149c6a65 100755 --- a/scripts/ci/check_examples.sh +++ b/scripts/ci/check_examples.sh @@ -10,7 +10,7 @@ while IFS= read -r file; do ./cm check "$file" 2>&1 | head -20 || true fail=1 fi -done < <(find examples -name '*.cm' -not -path 'examples/uefi/util/*' -not -path 'examples/uefi/libs/*' -not -path 'examples/08_selfhost_parser/sample_error.cm') +done < <(find examples -name '*.cm' -not -path 'examples/uefi/util/*' -not -path 'examples/uefi/libs/*' -not -path 'examples/08_selfhost_parser/samples/error.cm') if [ "$fail" -eq 0 ]; then echo "✅ examples check passed" @@ -28,17 +28,17 @@ echo "✅ selfhost drill passed (jit/native artifacts identical)" # セルフホストパーサ(examples/08_selfhost_parser)の検証: # 各モジュールの#[test]単体テスト → 正常サンプルの定義一覧出力(rc=0) → エラーサンプルの位置報告(rc=1) → パーサ自身の全ソース自己解析 SHP=examples/08_selfhost_parser -for t in token_test lexer_test parser/state_test parser/modules_test parser/types_test parser/impls_test parser/funcs_test parser/decl_test; do +for t in lexer/token_test lexer/scan_test parser/state_test parser/decl/modules_test parser/decl/types_test parser/decl/impls_test parser/decl/funcs_test parser/decl/dispatch_test; do ./cm test "$SHP/$t.cm" >/dev/null done -./cm run "$SHP/main.cm" -- "$SHP/sample_ok.cm" >/dev/null -if ./cm run "$SHP/main.cm" -- "$SHP/sample_error.cm" >/dev/null 2>&1; then - echo "::error::selfhost parser should report an error for sample_error.cm" +./cm run "$SHP/main.cm" -- "$SHP/samples/ok.cm" >/dev/null +if ./cm run "$SHP/main.cm" -- "$SHP/samples/error.cm" >/dev/null 2>&1; then + echo "::error::selfhost parser should report an error for samples/error.cm" exit 1 fi -./cm run "$SHP/main.cm" -- "$SHP/main.cm" "$SHP/token.cm" "$SHP/lexer.cm" \ - "$SHP/parser/state.cm" "$SHP/parser/modules.cm" "$SHP/parser/types.cm" \ - "$SHP/parser/impls.cm" "$SHP/parser/funcs.cm" "$SHP/parser/decl.cm" >/dev/null +./cm run "$SHP/main.cm" -- "$SHP/main.cm" "$SHP/lexer/token.cm" "$SHP/lexer/scan.cm" \ + "$SHP/parser/state.cm" "$SHP/parser/decl/modules.cm" "$SHP/parser/decl/types.cm" \ + "$SHP/parser/decl/impls.cm" "$SHP/parser/decl/funcs.cm" "$SHP/parser/decl/dispatch.cm" >/dev/null echo "✅ selfhost parser passed (unit tests / ok-sample / error-sample / self-parse)" exit "$fail" From 0f7978ecc7ad93d3f105c4e59074b3a064674bdf Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 17:42:50 +0900 Subject: [PATCH 08/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E3=81=AE=E5=93=81?= =?UTF-8?q?=E8=B3=AA=E6=94=B9=E5=96=84:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E5=AE=9A=E7=BE=A9?= =?UTF-8?q?=E3=81=AE=E5=88=86=E9=9B=A2=E3=83=BB=E3=83=88=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=B3=E7=A8=AE=E5=88=A5=E3=81=AEenum=E5=8C=96=E3=83=BB?= =?UTF-8?q?=E3=83=9E=E3=82=B8=E3=83=83=E3=82=AF=E3=83=8A=E3=83=B3=E3=83=90?= =?UTF-8?q?=E3=83=BC=E6=8E=92=E9=99=A4=E3=83=BB=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E6=8B=A1=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - エラーメッセージをdiag/messages.cmへ分離(Cmコンパイラのi18nメッセージ表と同様に文面をロジックから一元化。テストも同じビルダーを参照するため文面変更に自動追従) - トークン種別をint定数からenum TokenKind(Eof/Ident/Keyword/Number/StringLit/CharLit/Symbol)へ変更し、TokenStreamの格納・比較も型付きにした - キーワード定義をstring[]リテラルの一覧宣言とStringSetへの登録ループに分離した - 字句解析のバイト値マジックナンバーを全廃し、文字リテラルのキャスト(const int BYTE_LF = '\n' as int等)による定数定義へ置き換えた。main.cmの表示幅・基数も定数化 - テストを38件から56件へ拡充: キーワード前方一致の識別子判定・単独コロン・空入力・エスケープ付き文字リテラルの桁追跡・最初のエラー保持・EOFでのexpect・空struct/末尾カンマenum・フィールド属性・overloadコンストラクタ・空impl・関数ポインタ引数の括弧対応・非constグローバル・再エクスポート宣言列・2宣言目のエラー位置・メッセージカタログ整合 - 検証: 全56テスト・正常/エラーサンプル・自己解析・CI 3項目すべて通過 --- docs/releases/v0.17.2.md | 2 +- examples/08_selfhost_parser/README.md | 9 +- examples/08_selfhost_parser/diag/messages.cm | 44 ++++++ examples/08_selfhost_parser/lexer/scan.cm | 144 ++++++++++-------- .../08_selfhost_parser/lexer/scan_test.cm | 54 ++++++- examples/08_selfhost_parser/lexer/token.cm | 32 ++-- .../08_selfhost_parser/lexer/token_test.cm | 12 +- examples/08_selfhost_parser/main.cm | 9 +- .../parser/decl/dispatch_test.cm | 20 +++ .../08_selfhost_parser/parser/decl/funcs.cm | 16 +- .../parser/decl/funcs_test.cm | 23 +++ .../08_selfhost_parser/parser/decl/impls.cm | 10 +- .../parser/decl/impls_test.cm | 20 +++ .../08_selfhost_parser/parser/decl/modules.cm | 5 +- .../parser/decl/modules_test.cm | 8 + .../08_selfhost_parser/parser/decl/types.cm | 13 +- .../parser/decl/types_test.cm | 25 +++ examples/08_selfhost_parser/parser/state.cm | 23 +-- .../08_selfhost_parser/parser/state_test.cm | 21 ++- 19 files changed, 358 insertions(+), 132 deletions(-) create mode 100644 examples/08_selfhost_parser/diag/messages.cm diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index f7a43aa8..476de78c 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -233,7 +233,7 @@ JSのポインタ表現(配列+添字のセル参照)がスライスポイ v0.17.2で整備した標準ライブラリの実証として、Cmで書いたCmソースの宣言レベルパーサを追加した。 成功なら定義一覧(module/import/use/typedef/const/struct/enum/impl/関数)を、失敗ならエラー箇所(行:桁・キャレット付きソース行)を出力し、失敗し得る解析関数はすべてOption型で失敗を伝播する。 -機能別に9ファイルへ分割し、各ファイルに `#[test]` ディレクティブの単体テスト(8ファイル・38テスト)が付属する。 +段階別ディレクトリ(diag/lexer/parser/decl/samples)へ分割し、各モジュールに `#[test]` ディレクティブの単体テスト(8ファイル・56テスト)が付属する。トークン種別はenum、エラーメッセージは診断モジュールへ分離、バイト値は文字キャストの定数で定義している。 パーサ自身の全ソースを自己解析でき、CIが単体テスト・正常/エラーサンプル・自己解析を検証する。 ## 🧪 テスト diff --git a/examples/08_selfhost_parser/README.md b/examples/08_selfhost_parser/README.md index be8a03d1..f85a3c89 100644 --- a/examples/08_selfhost_parser/README.md +++ b/examples/08_selfhost_parser/README.md @@ -27,9 +27,11 @@ cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/sample ``` main.cm CLI(引数解析・読み込み・結果出力) +diag/ 診断 + messages.cm エラーメッセージ定義(Cmコンパイラのi18n表と同様に文面をロジックから分離) lexer/ 字句解析 - token.cm トークン種別定数とTokenStream(パラレルスライス) - scan.cm 字句解析器(コメント/文字列/文字/数値/::融合、キーワード判定はStringSet) + token.cm トークン種別enum(TokenKind)とTokenStream(パラレルスライス) + scan.cm 字句解析器(コメント/文字列/文字/数値/::融合、キーワード判定はStringSet、バイト値は文字キャスト定数) parser/ 構文解析 state.cm Parser状態・基本操作(expect/skip/ジェネリクス読取)・エラー診断・定義レコーダ decl/ 宣言種別ごとの解析 @@ -55,7 +57,8 @@ cm test examples/08_selfhost_parser/parser/decl/types_test.cm # ...(lexer/token / parser/state / parser/decl/{modules, impls, funcs, dispatch} も同様) ``` -CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、8ファイルの単体テスト・正常/エラーサンプル・自己解析を検証します。 +CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、8ファイル・56テストの単体実行・正常/エラーサンプル・自己解析を検証します。 +エラーメッセージの検証はテストも`diag/messages.cm`のビルダーを参照するため、文面変更に自動で追従します。 ## エラーハンドリングの設計 diff --git a/examples/08_selfhost_parser/diag/messages.cm b/examples/08_selfhost_parser/diag/messages.cm new file mode 100644 index 00000000..fd353968 --- /dev/null +++ b/examples/08_selfhost_parser/diag/messages.cm @@ -0,0 +1,44 @@ +// セルフホストパーサ: 診断メッセージ定義 +// Cmコンパイラのi18nメッセージ表と同じく、エラーメッセージの文面を解析ロジックから分離して一元管理する。 +// 文面の変更・多言語化はこのモジュールだけで完結し、テストも同じビルダーを参照するため文面変更に追従する +module diag.messages; + +// 「expected '<期待字句>' but found <実際>」 +export string msg_expected(string expected, string found) { + return "expected '" + expected + "' but found " + found; +} + +// 「expected <期待物> but found <実際>」(字句でなく概念を期待する場合) +export string msg_expected_thing(string what, string found) { + return "expected " + what + " but found " + found; +} + +// 「expected '<期待字句>' in <文脈> but found <実際>」 +export string msg_expected_in(string expected, string context, string found) { + return "expected '" + expected + "' in " + context + " but found " + found; +} + +// 「expected '{' or ';' after <対象> but found <実際>」(本体または宣言終端の欠落) +export string msg_expected_body_or_semi(string after, string found) { + return "expected '{' or ';' after " + after + " but found " + found; +} + +// 「unexpected end of file in <文脈>」 +export string msg_eof_in(string context) { + return "unexpected end of file in " + context; +} + +// 「unexpected end of file while looking for '<区切り>'」 +export string msg_eof_looking_for(string delimiter) { + return "unexpected end of file while looking for '" + delimiter + "'"; +} + +// 「unterminated <対象>」(未終端リテラル・コメント) +export string msg_unterminated(string what) { + return "unterminated " + what; +} + +// 「could not determine <対象>」(名前の特定失敗) +export string msg_cannot_determine(string what) { + return "could not determine " + what; +} diff --git a/examples/08_selfhost_parser/lexer/scan.cm b/examples/08_selfhost_parser/lexer/scan.cm index 7684ce6e..8601d6fe 100644 --- a/examples/08_selfhost_parser/lexer/scan.cm +++ b/examples/08_selfhost_parser/lexer/scan.cm @@ -6,6 +6,33 @@ module lexer.scan; import std::collections::strset::*; import std::strings::from_bytes::*; import ./token::*; +import ../diag/messages::*; + +// ---- バイト値定数(マジックナンバー排除。文字はキャストで定義する) ---- + +const int BYTE_EOF = -1; // 入力終端の番兵 +const int BYTE_TAB = '\t' as int; +const int BYTE_LF = '\n' as int; +const int BYTE_CR = '\r' as int; +const int BYTE_SPACE = ' ' as int; +const int BYTE_DQUOTE = '"' as int; +const int BYTE_SQUOTE = '\'' as int; +const int BYTE_BACKSLASH = '\\' as int; +const int BYTE_SLASH = '/' as int; +const int BYTE_STAR = '*' as int; +const int BYTE_COLON = ':' as int; +const int BYTE_UNDERSCORE = '_' as int; +const int BYTE_DOT = '.' as int; +const int BYTE_PLUS = '+' as int; +const int BYTE_MINUS = '-' as int; +const int BYTE_LOWER_E = 'e' as int; +const int BYTE_UPPER_E = 'E' as int; +const int BYTE_DIGIT_0 = '0' as int; +const int BYTE_DIGIT_9 = '9' as int; +const int BYTE_LOWER_A = 'a' as int; +const int BYTE_LOWER_Z = 'z' as int; +const int BYTE_UPPER_A = 'A' as int; +const int BYTE_UPPER_Z = 'Z' as int; // 字句解析器本体 // トークン列は内部に保持し、run()成功後にtake_tokens()で受け取る @@ -29,34 +56,17 @@ export impl Lexer { self.pos = 0; self.line = 1; self.col = 1; + // キーワード定義: 一覧の宣言と登録ループを分離する + string[] keywords = [ + "module", "import", "export", "use", "struct", "enum", "impl", "interface", + "typedef", "const", "static", "private", "overload", "extern", "operator", + "for", "with", "self", "return", "if", "else", "while", "match", "switch", + "move", "as", "is" + ]; StringSet kw(); - kw.insert("module"); - kw.insert("import"); - kw.insert("export"); - kw.insert("use"); - kw.insert("struct"); - kw.insert("enum"); - kw.insert("impl"); - kw.insert("interface"); - kw.insert("typedef"); - kw.insert("const"); - kw.insert("static"); - kw.insert("private"); - kw.insert("overload"); - kw.insert("extern"); - kw.insert("operator"); - kw.insert("for"); - kw.insert("with"); - kw.insert("self"); - kw.insert("return"); - kw.insert("if"); - kw.insert("else"); - kw.insert("while"); - kw.insert("match"); - kw.insert("switch"); - kw.insert("move"); - kw.insert("as"); - kw.insert("is"); + for (int i = 0; i < keywords.len(); i++) { + kw.insert(keywords[i]); + } self.keywords = kw; TokenStream ts(); self.tokens = ts; @@ -78,17 +88,17 @@ export impl Lexer { return self.err_col; } - // 現在位置のバイト(終端は-1) + // 現在位置のバイト(終端はBYTE_EOF) private int peek_byte() { if (self.pos >= self.src.byte_len()) { - return -1; + return BYTE_EOF; } return self.src.byte_at(self.pos); } private int peek_byte2() { if (self.pos + 1 >= self.src.byte_len()) { - return -1; + return BYTE_EOF; } return self.src.byte_at(self.pos + 1); } @@ -96,7 +106,7 @@ export impl Lexer { // 1バイト進める(改行で行・桁を更新) private void advance() { if (self.pos < self.src.byte_len()) { - if (self.src.byte_at(self.pos) == 10) { + if (self.src.byte_at(self.pos) == BYTE_LF) { self.line = self.line + 1; self.col = 1; } else { @@ -107,11 +117,12 @@ export impl Lexer { } private bool is_digit_b(int b) { - return b >= 48 && b <= 57; + return b >= BYTE_DIGIT_0 && b <= BYTE_DIGIT_9; } private bool is_ident_start_b(int b) { - return (b >= 97 && b <= 122) || (b >= 65 && b <= 90) || b == 95; + return (b >= BYTE_LOWER_A && b <= BYTE_LOWER_Z) || (b >= BYTE_UPPER_A && b <= BYTE_UPPER_Z) || + b == BYTE_UNDERSCORE; } private bool is_ident_cont_b(int b) { @@ -122,22 +133,22 @@ export impl Lexer { private Option skip_trivia() { while (true) { const int b = self.peek_byte(); - if (b == 32 || b == 9 || b == 13 || b == 10) { + if (b == BYTE_SPACE || b == BYTE_TAB || b == BYTE_CR || b == BYTE_LF) { self.advance(); - } else if (b == 47 && self.peek_byte2() == 47) { + } else if (b == BYTE_SLASH && self.peek_byte2() == BYTE_SLASH) { // 行コメント - while (self.peek_byte() != 10 && self.peek_byte() != -1) { + while (self.peek_byte() != BYTE_LF && self.peek_byte() != BYTE_EOF) { self.advance(); } - } else if (b == 47 && self.peek_byte2() == 42) { + } else if (b == BYTE_SLASH && self.peek_byte2() == BYTE_STAR) { // ブロックコメント const int start_line = self.line; const int start_col = self.col; self.advance(); self.advance(); bool closed = false; - while (self.peek_byte() != -1) { - if (self.peek_byte() == 42 && self.peek_byte2() == 47) { + while (self.peek_byte() != BYTE_EOF) { + if (self.peek_byte() == BYTE_STAR && self.peek_byte2() == BYTE_SLASH) { self.advance(); self.advance(); closed = true; @@ -146,7 +157,7 @@ export impl Lexer { self.advance(); } if (!closed) { - self.err_msg = "unterminated block comment"; + self.err_msg = msg_unterminated("block comment"); self.err_line = start_line; self.err_col = start_col; return Option::None; @@ -181,8 +192,8 @@ export impl Lexer { return Option::None; } const int b = self.peek_byte(); - if (b == -1) { - self.tokens.push_token(TK_EOF, "", self.line, self.col); + if (b == BYTE_EOF) { + self.tokens.push_token(TokenKind::Eof, "", self.line, self.col); return Option::Some(true); } const int tok_line = self.line; @@ -196,9 +207,9 @@ export impl Lexer { } const string text = self.slice_text(start); if (self.keywords.contains(text)) { - self.tokens.push_token(TK_KEYWORD, text, tok_line, tok_col); + self.tokens.push_token(TokenKind::Keyword, text, tok_line, tok_col); } else { - self.tokens.push_token(TK_IDENT, text, tok_line, tok_col); + self.tokens.push_token(TokenKind::Ident, text, tok_line, tok_col); } } else if (self.is_digit_b(b)) { // 数値リテラル(10進・16進・小数・指数をまとめて消費する) @@ -206,71 +217,74 @@ export impl Lexer { const int c = self.peek_byte(); if (self.is_ident_cont_b(c)) { self.advance(); - } else if (c == 46 && self.is_digit_b(self.peek_byte2())) { + } else if (c == BYTE_DOT && self.is_digit_b(self.peek_byte2())) { // 小数点(直後が数字のときのみ) self.advance(); - } else if ((c == 43 || c == 45) && self.pos > start && - (self.src.byte_at(self.pos - 1) == 101 || - self.src.byte_at(self.pos - 1) == 69)) { + } else if ((c == BYTE_PLUS || c == BYTE_MINUS) && self.pos > start && + (self.src.byte_at(self.pos - 1) == BYTE_LOWER_E || + self.src.byte_at(self.pos - 1) == BYTE_UPPER_E)) { // 指数の符号(e/Eの直後のみ) self.advance(); } else { break; } } - self.tokens.push_token(TK_NUMBER, self.slice_text(start), tok_line, tok_col); - } else if (b == 34) { + self.tokens.push_token(TokenKind::Number, self.slice_text(start), tok_line, tok_col); + } else if (b == BYTE_DQUOTE) { // 文字列リテラル(エスケープ対応・未終端はエラー) self.advance(); bool closed = false; - while (self.peek_byte() != -1) { + while (self.peek_byte() != BYTE_EOF) { const int c = self.peek_byte(); - if (c == 92) { + if (c == BYTE_BACKSLASH) { self.advance(); self.advance(); - } else if (c == 34) { + } else if (c == BYTE_DQUOTE) { self.advance(); closed = true; break; - } else if (c == 10) { + } else if (c == BYTE_LF) { break; } else { self.advance(); } } if (!closed) { - self.err_msg = "unterminated string literal"; + self.err_msg = msg_unterminated("string literal"); self.err_line = tok_line; self.err_col = tok_col; return Option::None; } - self.tokens.push_token(TK_STRING, self.slice_text(start), tok_line, tok_col); - } else if (b == 39) { + self.tokens.push_token(TokenKind::StringLit, self.slice_text(start), tok_line, + tok_col); + } else if (b == BYTE_SQUOTE) { // 文字リテラル(未終端はエラー) self.advance(); - if (self.peek_byte() == 92) { + if (self.peek_byte() == BYTE_BACKSLASH) { self.advance(); self.advance(); - } else if (self.peek_byte() != -1 && self.peek_byte() != 39) { + } else if (self.peek_byte() != BYTE_EOF && self.peek_byte() != BYTE_SQUOTE) { self.advance(); } - if (self.peek_byte() != 39) { - self.err_msg = "unterminated char literal"; + if (self.peek_byte() != BYTE_SQUOTE) { + self.err_msg = msg_unterminated("char literal"); self.err_line = tok_line; self.err_col = tok_col; return Option::None; } self.advance(); - self.tokens.push_token(TK_CHAR, self.slice_text(start), tok_line, tok_col); - } else if (b == 58 && self.peek_byte2() == 58) { + self.tokens.push_token(TokenKind::CharLit, self.slice_text(start), tok_line, + tok_col); + } else if (b == BYTE_COLON && self.peek_byte2() == BYTE_COLON) { // "::" は1トークンに融合する(import経路・修飾名の解析用) self.advance(); self.advance(); - self.tokens.push_token(TK_SYMBOL, "::", tok_line, tok_col); + self.tokens.push_token(TokenKind::Symbol, "::", tok_line, tok_col); } else { // その他は1文字記号 self.advance(); - self.tokens.push_token(TK_SYMBOL, self.slice_text(start), tok_line, tok_col); + self.tokens.push_token(TokenKind::Symbol, self.slice_text(start), tok_line, + tok_col); } } return Option::None; diff --git a/examples/08_selfhost_parser/lexer/scan_test.cm b/examples/08_selfhost_parser/lexer/scan_test.cm index 5dd6f93f..9ba2ed78 100644 --- a/examples/08_selfhost_parser/lexer/scan_test.cm +++ b/examples/08_selfhost_parser/lexer/scan_test.cm @@ -2,6 +2,7 @@ import std::debug::assert; import ./token::*; import ./scan::*; +import ../diag/messages::*; // テスト用: ソースを字句解析してトークン列を返す(成功前提) TokenStream lex_ok(string src) { @@ -14,11 +15,11 @@ TokenStream lex_ok(string src) { #[test] void tokenizes_idents_keywords_and_symbols() { TokenStream ts = lex_ok("struct Point { int x; }"); - assert(ts.kind_at(0) == TK_KEYWORD, "struct is keyword"); + assert(ts.kind_at(0) == TokenKind::Keyword, "struct is keyword"); assert(ts.text_at(0) == "struct", "struct text"); - assert(ts.kind_at(1) == TK_IDENT, "Point is ident"); - assert(ts.kind_at(2) == TK_SYMBOL, "brace is symbol"); - assert(ts.kind_at(ts.len() - 1) == TK_EOF, "stream ends with EOF"); + assert(ts.kind_at(1) == TokenKind::Ident, "Point is ident"); + assert(ts.kind_at(2) == TokenKind::Symbol, "brace is symbol"); + assert(ts.kind_at(ts.len() - 1) == TokenKind::Eof, "stream ends with EOF"); } #[test] @@ -39,9 +40,9 @@ void skips_comments_and_tracks_lines() { #[test] void lexes_string_char_and_number_literals() { TokenStream ts = lex_ok("\"a\\\"b\" 'x' 0x1f 1.5e3"); - assert(ts.kind_at(0) == TK_STRING, "string literal with escape"); - assert(ts.kind_at(1) == TK_CHAR, "char literal"); - assert(ts.kind_at(2) == TK_NUMBER, "hex number"); + assert(ts.kind_at(0) == TokenKind::StringLit, "string literal with escape"); + assert(ts.kind_at(1) == TokenKind::CharLit, "char literal"); + assert(ts.kind_at(2) == TokenKind::Number, "hex number"); assert(ts.text_at(3) == "1.5e3", "float with exponent"); } @@ -61,3 +62,42 @@ void reports_unterminated_block_comment() { assert(r.is_none(), "unterminated block comment is a lex error"); assert(lx.error_line() == 1, "error line"); } + +#[test] +void keyword_prefix_is_ident() { + TokenStream ts = lex_ok("selfish structure important"); + assert(ts.kind_at(0) == TokenKind::Ident, "selfish is not the self keyword"); + assert(ts.kind_at(1) == TokenKind::Ident, "structure is not the struct keyword"); + assert(ts.kind_at(2) == TokenKind::Ident, "important is not the import keyword"); +} + +#[test] +void single_colon_stays_single() { + TokenStream ts = lex_ok("a : b"); + assert(ts.text_at(1) == ":", "single colon is one symbol"); + assert(ts.len() == 4, "3 tokens + EOF"); +} + +#[test] +void empty_source_yields_only_eof() { + TokenStream ts = lex_ok(""); + assert(ts.len() == 1, "only EOF token"); + assert(ts.kind_at(0) == TokenKind::Eof, "EOF kind"); + assert(ts.line_at(0) == 1, "EOF at line 1"); +} + +#[test] +void escaped_char_literal_and_columns() { + TokenStream ts = lex_ok("'\\n' x"); + assert(ts.kind_at(0) == TokenKind::CharLit, "escaped char literal"); + assert(ts.col_at(0) == 1, "char literal column"); + assert(ts.col_at(1) == 6, "column after 4-byte literal + space"); +} + +#[test] +void unterminated_message_comes_from_catalog() { + Lexer lx("\"open"); + const Option r = lx.run(); + assert(r.is_none(), "unterminated string fails"); + assert(lx.error_message() == msg_unterminated("string literal"), "catalog message"); +} diff --git a/examples/08_selfhost_parser/lexer/token.cm b/examples/08_selfhost_parser/lexer/token.cm index fa3d0db2..a9de2129 100644 --- a/examples/08_selfhost_parser/lexer/token.cm +++ b/examples/08_selfhost_parser/lexer/token.cm @@ -1,19 +1,21 @@ // セルフホストパーサ: トークン定義 -// トークン種別定数と、字句解析結果のトークン列(種別・字句・行・桁のパラレルスライス)を提供する +// トークン種別enumと、字句解析結果のトークン列(種別・字句・行・桁のパラレルスライス)を提供する module lexer.token; -// トークン種別(パーサはこの定数とトークン字句で判定する) -export const int TK_EOF = 0; -export const int TK_IDENT = 1; // 識別子 -export const int TK_KEYWORD = 2; // 予約語(識別子のうちキーワード表に載っているもの) -export const int TK_NUMBER = 3; // 数値リテラル -export const int TK_STRING = 4; // 文字列リテラル(引用符ごと保持) -export const int TK_CHAR = 5; // 文字リテラル -export const int TK_SYMBOL = 6; // 記号("::"のみ2文字、他は1文字) +// トークン種別(パーサはこの種別とトークン字句で判定する) +export enum TokenKind { + Eof, // 入力終端 + Ident, // 識別子 + Keyword, // 予約語(識別子のうちキーワード表に載っているもの) + Number, // 数値リテラル + StringLit, // 文字列リテラル(引用符ごと保持) + CharLit, // 文字リテラル + Symbol // 記号("::"のみ2文字、他は1文字) +} // 字句解析の結果トークン列(スライスバックでデストラクタ無し) export struct TokenStream { - private int[] kinds; + private TokenKind[] kinds; private string[] texts; private int[] lines; private int[] cols; @@ -27,22 +29,22 @@ export impl TokenStream { self.cols = []; } - void push_token(int kind, string text, int line, int col) { + void push_token(TokenKind kind, string text, int line, int col) { self.kinds.push(kind); self.texts.push(text); self.lines.push(line); self.cols.push(col); } - // トークン数(終端のTK_EOFを含む) + // トークン数(終端のEofを含む) int len() { return self.kinds.len(); } - // 範囲外はEOF扱いで安全に読める - int kind_at(int i) { + // 範囲外はEof扱いで安全に読める + TokenKind kind_at(int i) { if (i < 0 || i >= self.kinds.len()) { - return TK_EOF; + return TokenKind::Eof; } return self.kinds[i]; } diff --git a/examples/08_selfhost_parser/lexer/token_test.cm b/examples/08_selfhost_parser/lexer/token_test.cm index 3870a510..234a55ea 100644 --- a/examples/08_selfhost_parser/lexer/token_test.cm +++ b/examples/08_selfhost_parser/lexer/token_test.cm @@ -5,10 +5,10 @@ import ./token::*; #[test] void push_and_read_back() { TokenStream ts(); - ts.push_token(TK_IDENT, "foo", 1, 5); - ts.push_token(TK_SYMBOL, "::", 1, 8); + ts.push_token(TokenKind::Ident, "foo", 1, 5); + ts.push_token(TokenKind::Symbol, "::", 1, 8); assert(ts.len() == 2, "len after two pushes"); - assert(ts.kind_at(0) == TK_IDENT, "kind of first token"); + assert(ts.kind_at(0) == TokenKind::Ident, "kind of first token"); assert(ts.text_at(0) == "foo", "text of first token"); assert(ts.line_at(1) == 1, "line of second token"); assert(ts.col_at(1) == 8, "col of second token"); @@ -17,9 +17,9 @@ void push_and_read_back() { #[test] void out_of_range_is_eof() { TokenStream ts(); - ts.push_token(TK_NUMBER, "42", 3, 1); - assert(ts.kind_at(99) == TK_EOF, "past-end kind is EOF"); - assert(ts.kind_at(-1) == TK_EOF, "negative index kind is EOF"); + ts.push_token(TokenKind::Number, "42", 3, 1); + assert(ts.kind_at(99) == TokenKind::Eof, "past-end kind is EOF"); + assert(ts.kind_at(-1) == TokenKind::Eof, "negative index kind is EOF"); assert(ts.text_at(99) == "", "past-end text is empty"); assert(ts.line_at(99) == 0, "past-end line is 0"); } diff --git a/examples/08_selfhost_parser/main.cm b/examples/08_selfhost_parser/main.cm index d4ea7fd8..376e0eec 100644 --- a/examples/08_selfhost_parser/main.cm +++ b/examples/08_selfhost_parser/main.cm @@ -10,12 +10,17 @@ import ./lexer/scan::*; import ./parser/state::*; import ./parser/decl/dispatch::*; +// エラー表示の行番号欄の幅 +const int LINE_NUMBER_WIDTH = 4; +// 10進表示の基数 +const int RADIX_DECIMAL = 10; + // エラー位置のソース行をキャレット付きで表示する void print_error_context(string path, string src, int line, int col) { string[] ls = lines(src); if (line >= 1 && line <= ls.len()) { - const string ln = to_radix(line as long, 10); - println("{pad_left(ln, 4, ' ')} | {ls[line - 1]}"); + const string ln = to_radix(line as long, RADIX_DECIMAL); + println("{pad_left(ln, LINE_NUMBER_WIDTH, ' ')} | {ls[line - 1]}"); println(" | {pad_left(\"^\", col, ' ')}"); } } diff --git a/examples/08_selfhost_parser/parser/decl/dispatch_test.cm b/examples/08_selfhost_parser/parser/decl/dispatch_test.cm index 696d46fa..1cdfa04c 100644 --- a/examples/08_selfhost_parser/parser/decl/dispatch_test.cm +++ b/examples/08_selfhost_parser/parser/decl/dispatch_test.cm @@ -4,6 +4,7 @@ import ../../lexer/token::*; import ../../lexer/scan::*; import ../state::*; import ./dispatch::*; +import ../../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -50,3 +51,22 @@ void error_position_propagates() { assert(p.error_line() == 4, "error reported at closing line"); assert(p.structs_of().len() == 1, "first struct was already recorded"); } + +#[test] +void reexport_and_decl_sequence() { + Parser p = parser_of( + "export import std::strings::parse::*; use libc { void free(void* p); } typedef Id = int; const int N = 1;"); + assert(parse_program(&p).is_some(), "re-export and decl sequence parses"); + assert(p.imports_of()[0] == "std::strings::parse::*", "re-export recorded as import"); + assert(p.uses_of().len() == 1, "use recorded"); + assert(p.typedefs_of().len() == 1, "typedef recorded"); + assert(p.consts_of().len() == 1, "const recorded"); +} + +#[test] +void second_decl_error_position() { + Parser p = parser_of("int ok() { return 1; }\nstruct Broken;"); + assert(parse_program(&p).is_none(), "second decl fails"); + assert(p.error_line() == 2, "error on second line"); + assert(p.funcs_of().len() == 1, "first decl already recorded"); +} diff --git a/examples/08_selfhost_parser/parser/decl/funcs.cm b/examples/08_selfhost_parser/parser/decl/funcs.cm index beedfce9..fa2ddce2 100644 --- a/examples/08_selfhost_parser/parser/decl/funcs.cm +++ b/examples/08_selfhost_parser/parser/decl/funcs.cm @@ -4,6 +4,7 @@ module parser.decl.funcs; import ../../lexer/token::*; import ../state::*; +import ../../diag/messages::*; // typedef宣言("typedef"の上から) export Option parse_typedef(Parser* p) { @@ -26,7 +27,7 @@ export Option parse_const(Parser* p) { string last_ident = ""; while (!p->at_text("=")) { if (p->at_eof() || p->at_text(";")) { - return p->fail("expected '=' in const declaration but found " + p->found_text()); + return p->fail(msg_expected_in("=", "const declaration", p->found_text())); } if (p->at_ident()) { last_ident = p->text(); @@ -38,7 +39,7 @@ export Option parse_const(Parser* p) { return Option::None; } if (last_ident == "") { - return p->fail("could not determine const name"); + return p->fail(msg_cannot_determine("const name")); } p->record_const(last_ident); return Option::Some(true); @@ -56,7 +57,7 @@ export Option parse_func_or_var(Parser* p) { string name = ""; while (!p->at_text("(")) { if (p->at_eof()) { - return p->fail("unexpected end of file in declaration"); + return p->fail(msg_eof_in("declaration")); } if (p->at_text(";") || p->at_text("=")) { // 関数でなくグローバル変数宣言だった @@ -65,13 +66,13 @@ export Option parse_func_or_var(Parser* p) { return Option::None; } if (name == "") { - return p->fail("could not determine declaration name"); + return p->fail(msg_cannot_determine("declaration name")); } p->record_const(name); return Option::Some(true); } if (p->at_text("{") || p->at_text("}")) { - return p->fail("expected declaration but found " + p->found_text()); + return p->fail(msg_expected_thing("declaration", p->found_text())); } if (p->at_ident()) { if (name != "") { @@ -93,7 +94,7 @@ export Option parse_func_or_var(Parser* p) { p->bump(); } if (name == "") { - return p->fail("could not determine function name before '('"); + return p->fail(msg_cannot_determine("function name before '('")); } const Option params = p->skip_balanced("(", ")"); if (params.is_none()) { @@ -107,8 +108,7 @@ export Option parse_func_or_var(Parser* p) { } else if (p->at_text(";")) { p->bump(); } else { - return p->fail("expected '{' or ';' after function signature but found " + - p->found_text()); + return p->fail(msg_expected_body_or_semi("function signature", p->found_text())); } p->record_func(ret + " " + name + gp.unwrap() + "()"); return Option::Some(true); diff --git a/examples/08_selfhost_parser/parser/decl/funcs_test.cm b/examples/08_selfhost_parser/parser/decl/funcs_test.cm index d85ab320..a25a0ce8 100644 --- a/examples/08_selfhost_parser/parser/decl/funcs_test.cm +++ b/examples/08_selfhost_parser/parser/decl/funcs_test.cm @@ -4,6 +4,7 @@ import ../../lexer/token::*; import ../../lexer/scan::*; import ../state::*; import ./funcs::*; +import ../../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -60,3 +61,25 @@ void const_without_assign_is_error() { Parser p = parser_of("const int NO_VALUE;"); assert(parse_const(&p).is_none(), "const without '=' is an error"); } + +#[test] +void function_pointer_parameter_nests_parens() { + Parser p = parser_of("void sort_by(int[] xs, int*(int, int) cmp) { }"); + assert(parse_func_or_var(&p).is_some(), "fnptr parameter parses"); + assert(p.funcs_of()[0] == "void sort_by()", "name extracted before first paren"); +} + +#[test] +void global_var_without_const_is_recorded() { + Parser p = parser_of("int counter = 0;"); + assert(parse_func_or_var(&p).is_some(), "global var parses"); + assert(p.consts_of()[0] == "counter", "declaration name recorded"); +} + +#[test] +void func_error_uses_catalog_message() { + Parser p = parser_of("int broken()"); + assert(parse_func_or_var(&p).is_none(), "missing body fails"); + assert(p.error_message() == msg_expected_body_or_semi("function signature", "end of file"), + "catalog message"); +} diff --git a/examples/08_selfhost_parser/parser/decl/impls.cm b/examples/08_selfhost_parser/parser/decl/impls.cm index bf783386..ae1a4fe8 100644 --- a/examples/08_selfhost_parser/parser/decl/impls.cm +++ b/examples/08_selfhost_parser/parser/decl/impls.cm @@ -5,6 +5,7 @@ module parser.decl.impls; import ../../lexer/token::*; import ../state::*; +import ../../diag/messages::*; // impl内の1メソッドを解析し、メソッド名を返す export Option parse_method(Parser* p) { @@ -28,7 +29,7 @@ export Option parse_method(Parser* p) { string name = ""; while (!p->at_text("(")) { if (p->at_eof() || p->at_text("{") || p->at_text("}")) { - return p->fail_str("expected method signature but found " + p->found_text()); + return p->fail_str(msg_expected_thing("method signature", p->found_text())); } if (p->at_text("~")) { name = "~"; @@ -45,7 +46,7 @@ export Option parse_method(Parser* p) { p->bump(); } if (name == "") { - return p->fail_str("could not determine method name before '('"); + return p->fail_str(msg_cannot_determine("method name before '('")); } const Option params = p->skip_balanced("(", ")"); if (params.is_none()) { @@ -60,8 +61,7 @@ export Option parse_method(Parser* p) { } else if (p->at_text(";")) { p->bump(); } else { - return p->fail_str("expected '{' or ';' after method signature but found " + - p->found_text()); + return p->fail_str(msg_expected_body_or_semi("method signature", p->found_text())); } return Option::Some(name); } @@ -101,7 +101,7 @@ export Option parse_impl(Parser* p) { string[] methods = []; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail("unexpected end of file in impl body"); + return p->fail(msg_eof_in("impl body")); } const Option m = parse_method(p); if (m.is_none()) { diff --git a/examples/08_selfhost_parser/parser/decl/impls_test.cm b/examples/08_selfhost_parser/parser/decl/impls_test.cm index 968bfb00..a8f34678 100644 --- a/examples/08_selfhost_parser/parser/decl/impls_test.cm +++ b/examples/08_selfhost_parser/parser/decl/impls_test.cm @@ -4,6 +4,7 @@ import ../../lexer/token::*; import ../../lexer/scan::*; import ../state::*; import ./impls::*; +import ../../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -44,3 +45,22 @@ void method_without_body_or_semicolon_is_error() { assert(p.error_message() == "expected '{' or ';' after method signature but found '}'", "error message"); } + +#[test] +void overload_ctor_and_empty_impl() { + Parser p = parser_of("impl A { self() { } overload self(int n) { } }"); + assert(parse_impl(&p).is_some(), "overload ctor parses"); + const string expected = "impl A {" + " self, self " + "}"; + assert(p.impls_of()[0] == expected, "both ctors listed"); + + Parser q = parser_of("impl B { }"); + assert(parse_impl(&q).is_some(), "empty impl parses"); +} + +#[test] +void method_error_uses_catalog_message() { + Parser p = parser_of("impl X { int broken() }"); + assert(parse_impl(&p).is_none(), "missing body fails"); + assert(p.error_message() == msg_expected_body_or_semi("method signature", "'}'"), + "catalog message"); +} diff --git a/examples/08_selfhost_parser/parser/decl/modules.cm b/examples/08_selfhost_parser/parser/decl/modules.cm index 94e12f9e..2cf7619b 100644 --- a/examples/08_selfhost_parser/parser/decl/modules.cm +++ b/examples/08_selfhost_parser/parser/decl/modules.cm @@ -4,13 +4,14 @@ module parser.decl.modules; import ../../lexer/token::*; import ../state::*; +import ../../diag/messages::*; // "::"区切りのパス(import経路等)を";"まで表示形で読み取る export Option parse_path_to_semicolon(Parser* p) { string out = ""; while (!p->at_text(";")) { if (p->at_eof()) { - return p->fail_str("unexpected end of file in import path"); + return p->fail_str(msg_eof_in("import path")); } if (p->at_text("{")) { // 選択import "{a, b}" は中身ごと表示形へ @@ -19,7 +20,7 @@ export Option parse_path_to_semicolon(Parser* p) { bool first = true; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail_str("unexpected end of file in import list"); + return p->fail_str(msg_eof_in("import list")); } if (p->at_text(",")) { p->bump(); diff --git a/examples/08_selfhost_parser/parser/decl/modules_test.cm b/examples/08_selfhost_parser/parser/decl/modules_test.cm index 803b4750..eb48ac15 100644 --- a/examples/08_selfhost_parser/parser/decl/modules_test.cm +++ b/examples/08_selfhost_parser/parser/decl/modules_test.cm @@ -4,6 +4,7 @@ import ../../lexer/token::*; import ../../lexer/scan::*; import ../state::*; import ./modules::*; +import ../../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -50,3 +51,10 @@ void unterminated_import_is_error() { assert(parse_import_decl(&p).is_none(), "missing semicolon is an error"); assert(p.error_message() == "unexpected end of file in import path", "error message"); } + +#[test] +void import_error_uses_catalog_message() { + Parser p = parser_of("import std::io"); + assert(parse_import_decl(&p).is_none(), "missing semicolon fails"); + assert(p.error_message() == msg_eof_in("import path"), "catalog message"); +} diff --git a/examples/08_selfhost_parser/parser/decl/types.cm b/examples/08_selfhost_parser/parser/decl/types.cm index 77796bef..d6be2291 100644 --- a/examples/08_selfhost_parser/parser/decl/types.cm +++ b/examples/08_selfhost_parser/parser/decl/types.cm @@ -4,6 +4,7 @@ module parser.decl.types; import ../../lexer/token::*; import ../state::*; +import ../../diag/messages::*; // struct宣言("struct"の上から)。prefixは"Outer::"のようなネスト名の接頭辞 export Option parse_struct(Parser* p, string prefix) { @@ -20,7 +21,7 @@ export Option parse_struct(Parser* p, string prefix) { // "with Trait, ..." は"{"まで読み飛ばす while (!p->at_text("{")) { if (p->at_eof() || p->at_text(";") || p->at_text("}")) { - return p->fail("expected '{' in struct declaration but found " + p->found_text()); + return p->fail(msg_expected_in("{", "struct declaration", p->found_text())); } p->bump(); } @@ -29,7 +30,7 @@ export Option parse_struct(Parser* p, string prefix) { string[] fields = []; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail("unexpected end of file in struct body"); + return p->fail(msg_eof_in("struct body")); } if (p->at_text("#")) { const Option a = p->skip_attribute(); @@ -69,7 +70,7 @@ export Option parse_struct(Parser* p, string prefix) { string last_ident = ""; while (!p->at_text(";")) { if (p->at_eof()) { - return p->fail("unexpected end of file in struct field"); + return p->fail(msg_eof_in("struct field")); } if (p->at_ident()) { last_ident = p->text(); @@ -110,7 +111,7 @@ export Option parse_enum(Parser* p, string prefix) { string[] variants = []; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail("unexpected end of file in enum body"); + return p->fail(msg_eof_in("enum body")); } if (p->at_text(",")) { p->bump(); @@ -125,7 +126,7 @@ export Option parse_enum(Parser* p, string prefix) { continue; } if (!p->at_ident()) { - return p->fail("expected enum variant name but found " + p->found_text()); + return p->fail(msg_expected_thing("enum variant name", p->found_text())); } variants.push(p->text()); p->bump(); @@ -140,7 +141,7 @@ export Option parse_enum(Parser* p, string prefix) { p->bump(); while (!p->at_text(",") && !p->at_text("}")) { if (p->at_eof()) { - return p->fail("unexpected end of file in enum value"); + return p->fail(msg_eof_in("enum value")); } p->bump(); } diff --git a/examples/08_selfhost_parser/parser/decl/types_test.cm b/examples/08_selfhost_parser/parser/decl/types_test.cm index cea2d2bf..5914a9eb 100644 --- a/examples/08_selfhost_parser/parser/decl/types_test.cm +++ b/examples/08_selfhost_parser/parser/decl/types_test.cm @@ -4,6 +4,7 @@ import ../../lexer/token::*; import ../../lexer/scan::*; import ../state::*; import ./types::*; +import ../../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -51,3 +52,27 @@ void struct_without_brace_is_error() { assert(p.error_message() == "expected '{' in struct declaration but found ';'", "error message"); } + +#[test] +void empty_struct_and_trailing_comma_enum() { + Parser p = parser_of("struct Empty { }"); + assert(parse_struct(&p, "").is_some(), "empty struct parses"); + Parser q = parser_of("enum E { A, B, }"); + assert(parse_enum(&q, "").is_some(), "trailing comma enum parses"); +} + +#[test] +void field_attribute_is_skipped() { + Parser p = parser_of("struct S { #[target(!sv)] int x; int y; }"); + assert(parse_struct(&p, "").is_some(), "attributed field parses"); + const string expected = "S {" + " x, y " + "}"; + assert(p.structs_of()[0] == expected, "both fields collected"); +} + +#[test] +void struct_error_uses_catalog_message() { + Parser p = parser_of("struct Broken;"); + assert(parse_struct(&p, "").is_none(), "missing body fails"); + assert(p.error_message() == msg_expected_in("{", "struct declaration", "';'"), + "catalog message"); +} diff --git a/examples/08_selfhost_parser/parser/state.cm b/examples/08_selfhost_parser/parser/state.cm index eca24543..af6ed4fd 100644 --- a/examples/08_selfhost_parser/parser/state.cm +++ b/examples/08_selfhost_parser/parser/state.cm @@ -4,6 +4,7 @@ module parser.state; import ../lexer/token::*; +import ../diag/messages::*; // 解析器本体(収集結果もここに持つ。全てスライスバックでデストラクタ無し) export struct Parser { @@ -137,7 +138,7 @@ export impl Parser { // ---- トークン読み取りの基本操作 ---- - int kind() { + TokenKind kind() { return self.toks.kind_at(self.pos); } @@ -150,19 +151,19 @@ export impl Parser { } bool at_text(string t) { - return self.text() == t && self.kind() != TK_EOF; + return self.text() == t && self.kind() != TokenKind::Eof; } bool at_eof() { - return self.kind() == TK_EOF; + return self.kind() == TokenKind::Eof; } bool at_ident() { - return self.kind() == TK_IDENT; + return self.kind() == TokenKind::Ident; } bool at_string() { - return self.kind() == TK_STRING; + return self.kind() == TokenKind::StringLit; } // 失敗を記録してNoneを返すための共通処理(最初のエラーだけを保持する) @@ -196,7 +197,7 @@ export impl Parser { // 指定の字句を要求して読み進める(違えばNone) Option expect_text(string t) { if (!self.at_text(t)) { - return self.fail("expected '" + t + "' but found " + self.found_text()); + return self.fail(msg_expected(t, self.found_text())); } self.bump(); return Option::Some(true); @@ -204,8 +205,8 @@ export impl Parser { // 識別子を要求して返す(予約語・記号ならNone) Option expect_ident() { - if (self.kind() != TK_IDENT) { - return self.fail_str("expected identifier but found " + self.found_text()); + if (self.kind() != TokenKind::Ident) { + return self.fail_str(msg_expected_thing("identifier", self.found_text())); } const string name = self.text(); self.bump(); @@ -221,7 +222,7 @@ export impl Parser { int depth = 1; while (depth > 0) { if (self.at_eof()) { - return self.fail("unexpected end of file while looking for '" + close + "'"); + return self.fail(msg_eof_looking_for(close)); } if (self.at_text(open)) { depth = depth + 1; @@ -237,7 +238,7 @@ export impl Parser { Option skip_to_semicolon() { while (!self.at_text(";")) { if (self.at_eof()) { - return self.fail("unexpected end of file while looking for ';'"); + return self.fail(msg_eof_looking_for(";")); } if (self.at_text("{")) { const Option b = self.skip_balanced("{", "}"); @@ -262,7 +263,7 @@ export impl Parser { bool first = true; while (!self.at_text(">")) { if (self.at_eof()) { - return self.fail_str("unexpected end of file in generic parameter list"); + return self.fail_str(msg_eof_in("generic parameter list")); } if (self.at_text(",")) { self.bump(); diff --git a/examples/08_selfhost_parser/parser/state_test.cm b/examples/08_selfhost_parser/parser/state_test.cm index a5c59933..a4aa60ca 100644 --- a/examples/08_selfhost_parser/parser/state_test.cm +++ b/examples/08_selfhost_parser/parser/state_test.cm @@ -3,6 +3,7 @@ import std::debug::assert; import ../lexer/token::*; import ../lexer/scan::*; import ./state::*; +import ../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -18,7 +19,7 @@ void expect_text_consumes_or_fails() { Parser p = parser_of("( )"); assert(p.expect_text("(").is_some(), "matching text consumed"); assert(p.expect_text("]").is_none(), "mismatch returns None"); - assert(p.error_message() == "expected ']' but found ')'", "error message"); + assert(p.error_message() == msg_expected("]", "')'"), "catalog message"); assert(p.error_line() == 1, "error line"); assert(p.error_col() == 3, "error col points at the found token"); } @@ -67,3 +68,21 @@ void recorders_accumulate() { assert(p.funcs_of().len() == 1, "one function recorded"); assert(join_names(p.structs_of()) == "A(x), B(y)", "join_names order"); } + +#[test] +void first_error_is_preserved() { + Parser p = parser_of("a"); + assert(p.expect_text("(").is_none(), "first failure"); + const string first = p.error_message(); + const int first_col = p.error_col(); + assert(p.expect_text(")").is_none(), "second failure"); + assert(p.error_message() == first, "message not overwritten"); + assert(p.error_col() == first_col, "position not overwritten"); +} + +#[test] +void expect_at_eof_reports_eof() { + Parser p = parser_of(""); + assert(p.expect_text(";").is_none(), "expect at EOF fails"); + assert(p.error_message() == msg_expected(";", "end of file"), "EOF wording"); +} From 0f9abbe1544e1af78740ba2324e29ea44196c815 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 17:46:20 +0900 Subject: [PATCH 09/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E3=81=AE=E8=A8=BA?= =?UTF-8?q?=E6=96=AD=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92?= =?UTF-8?q?i18n=E5=AF=BE=E5=BF=9C=E5=BD=A2=E5=BC=8F=E3=81=B8=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cmコンパイラ本体と同じ構成(enum MsgId×enum Langの2次元テンプレート表)でdiag/messages.cmを再構成した - テンプレートは{0}〜{2}プレースホルダを持つ完結した1文とし、語順が言語ごとに異なるため断片連結による文組み立てを廃止した(msgf1/msgf2/msgf3が置換で埋める) - 言語は環境変数CM_LANGで切替(ja=日本語、既定は英語)。日本語は語順に合わせてプレースホルダ位置を入れ替えたテンプレートを持つ - main.cmの見出し(構文エラー/字句エラー/ファイル不在)もカタログ経由へ統一した - diag/messages_test.cmを追加(既定英語・CM_LANG=ja切替・言語ごとの語順・見出しのローカライズ、4テスト)し、CIの単体テスト対象へ組み込んだ(計9ファイル60テスト) - READMEへi18n構成とCM_LANG=jaの実行例を追記 --- docs/releases/v0.17.2.md | 2 +- examples/08_selfhost_parser/README.md | 15 +- examples/08_selfhost_parser/diag/messages.cm | 164 ++++++++++++++++-- .../08_selfhost_parser/diag/messages_test.cm | 50 ++++++ examples/08_selfhost_parser/main.cm | 7 +- scripts/ci/check_examples.sh | 2 +- 6 files changed, 222 insertions(+), 18 deletions(-) create mode 100644 examples/08_selfhost_parser/diag/messages_test.cm diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index 476de78c..a4f222a2 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -233,7 +233,7 @@ JSのポインタ表現(配列+添字のセル参照)がスライスポイ v0.17.2で整備した標準ライブラリの実証として、Cmで書いたCmソースの宣言レベルパーサを追加した。 成功なら定義一覧(module/import/use/typedef/const/struct/enum/impl/関数)を、失敗ならエラー箇所(行:桁・キャレット付きソース行)を出力し、失敗し得る解析関数はすべてOption型で失敗を伝播する。 -段階別ディレクトリ(diag/lexer/parser/decl/samples)へ分割し、各モジュールに `#[test]` ディレクティブの単体テスト(8ファイル・56テスト)が付属する。トークン種別はenum、エラーメッセージは診断モジュールへ分離、バイト値は文字キャストの定数で定義している。 +段階別ディレクトリ(diag/lexer/parser/decl/samples)へ分割し、各モジュールに `#[test]` ディレクティブの単体テスト(9ファイル・60テスト)が付属する。トークン種別はenum、バイト値は文字キャストの定数で定義し、エラーメッセージはコンパイラ本体と同じi18n構成(enum MsgId×Langのテンプレート表・`{0}`プレースホルダ・CM_LANG切替)で診断モジュールへ分離している。 パーサ自身の全ソースを自己解析でき、CIが単体テスト・正常/エラーサンプル・自己解析を検証する。 ## 🧪 テスト diff --git a/examples/08_selfhost_parser/README.md b/examples/08_selfhost_parser/README.md index f85a3c89..81b0147a 100644 --- a/examples/08_selfhost_parser/README.md +++ b/examples/08_selfhost_parser/README.md @@ -28,7 +28,7 @@ cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/sample ``` main.cm CLI(引数解析・読み込み・結果出力) diag/ 診断 - messages.cm エラーメッセージ定義(Cmコンパイラのi18n表と同様に文面をロジックから分離) + messages.cm エラーメッセージ定義(i18n対応: enum MsgId×Langのテンプレート表・{0}プレースホルダ・CM_LANG=jaで日本語) lexer/ 字句解析 token.cm トークン種別enum(TokenKind)とTokenStream(パラレルスライス) scan.cm 字句解析器(コメント/文字列/文字/数値/::融合、キーワード判定はStringSet、バイト値は文字キャスト定数) @@ -57,9 +57,20 @@ cm test examples/08_selfhost_parser/parser/decl/types_test.cm # ...(lexer/token / parser/state / parser/decl/{modules, impls, funcs, dispatch} も同様) ``` -CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、8ファイル・56テストの単体実行・正常/エラーサンプル・自己解析を検証します。 +CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、9ファイル・60テストの単体実行・正常/エラーサンプル・自己解析を検証します。 エラーメッセージの検証はテストも`diag/messages.cm`のビルダーを参照するため、文面変更に自動で追従します。 +## エラーメッセージのi18n + +Cmコンパイラ本体と同じ構成で、メッセージID(`enum MsgId`)×言語(`enum Lang`)の2次元テンプレート表に文面を集約しています。 +テンプレートは`{0}`〜`{2}`のプレースホルダを持つ完結した1文で、語順が言語ごとに異なるため断片連結による文組み立ては行いません。 + +```bash +# 日本語診断(既定は英語) +CM_LANG=ja cm run examples/08_selfhost_parser/main.cm -- examples/08_selfhost_parser/samples/error.cm +# → 構文エラー: method signature の後には '{' または ';' が必要ですが '}' が見つかりました +``` + ## エラーハンドリングの設計 `Option`はペイロードに失敗情報を持てないため、「失敗=`Option::None`を返す・診断(メッセージ/行/桁)は状態側に保持」という規約で統一しています。 diff --git a/examples/08_selfhost_parser/diag/messages.cm b/examples/08_selfhost_parser/diag/messages.cm index fd353968..3c42db4b 100644 --- a/examples/08_selfhost_parser/diag/messages.cm +++ b/examples/08_selfhost_parser/diag/messages.cm @@ -1,44 +1,186 @@ -// セルフホストパーサ: 診断メッセージ定義 -// Cmコンパイラのi18nメッセージ表と同じく、エラーメッセージの文面を解析ロジックから分離して一元管理する。 -// 文面の変更・多言語化はこのモジュールだけで完結し、テストも同じビルダーを参照するため文面変更に追従する +// セルフホストパーサ: 診断メッセージ定義(i18n対応) +// Cmコンパイラ本体のi18n構成と同じく、メッセージID(enum MsgId)×言語(enum Lang)の2次元テンプレート表で文面を一元管理する。 +// テンプレートは "{0}" "{1}" のプレースホルダを持つ完結した1文で、断片連結による文組み立ては行わない(語順が言語ごとに異なるため)。 +// 言語は環境変数 CM_LANG("ja"で日本語、それ以外は英語)で切り替える module diag.messages; +import std::env::*; + +// メッセージID(診断の種類ごとに1つ) +export enum MsgId { + ExpectedButFound, // 期待字句と実際: expected '{0}' but found {1} + ExpectedThing, // 期待概念と実際: expected {0} but found {1} + ExpectedIn, // 文脈付き期待: expected '{0}' in {1} but found {2} + ExpectedBodyOrSemi, // 本体または終端の欠落: expected '{' or ';' after {0} but found {1} + EofIn, // 文脈内での入力終端: unexpected end of file in {0} + EofLookingFor, // 区切り探索中の入力終端: unexpected end of file while looking for '{0}' + Unterminated, // 未終端リテラル・コメント: unterminated {0} + CannotDetermine, // 名前の特定失敗: could not determine {0} + LexErrorLabel, // 字句エラー行の見出し: lex error: {0} + SyntaxErrorLabel, // 構文エラー行の見出し: syntax error: {0} + FileNotFound // 入力ファイル不在: error: file not found: {0} +} + +// 対応言語 +export enum Lang { + En, + Ja +} + +// 現在の言語(環境変数CM_LANGが"ja"なら日本語、既定は英語) +export Lang current_lang() { + const Option v = get("CM_LANG"); + if (v.is_some() && v.unwrap() == "ja") { + return Lang::Ja; + } + return Lang::En; +} + +// メッセージID×言語のテンプレート表(プレースホルダ{0}〜{2}を持つ完結した1文) +string template_of(MsgId id, Lang lang) { + if (lang == Lang::Ja) { + if (id == MsgId::ExpectedButFound) { + return "'{0}' が必要ですが {1} が見つかりました"; + } + if (id == MsgId::ExpectedThing) { + return "{0} が必要ですが {1} が見つかりました"; + } + if (id == MsgId::ExpectedIn) { + return "{1} には '{0}' が必要ですが {2} が見つかりました"; + } + if (id == MsgId::ExpectedBodyOrSemi) { + return "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"; + } + if (id == MsgId::EofIn) { + return "{0} の途中で入力が終端しました"; + } + if (id == MsgId::EofLookingFor) { + return "'{0}' を探索中に入力が終端しました"; + } + if (id == MsgId::Unterminated) { + return "{0} が終端していません"; + } + if (id == MsgId::CannotDetermine) { + return "{0} を特定できません"; + } + if (id == MsgId::LexErrorLabel) { + return "字句エラー: {0}"; + } + if (id == MsgId::SyntaxErrorLabel) { + return "構文エラー: {0}"; + } + if (id == MsgId::FileNotFound) { + return "エラー: ファイルが見つかりません: {0}"; + } + return ""; + } + if (id == MsgId::ExpectedButFound) { + return "expected '{0}' but found {1}"; + } + if (id == MsgId::ExpectedThing) { + return "expected {0} but found {1}"; + } + if (id == MsgId::ExpectedIn) { + return "expected '{0}' in {1} but found {2}"; + } + if (id == MsgId::ExpectedBodyOrSemi) { + return "expected '{' or ';' after {0} but found {1}"; + } + if (id == MsgId::EofIn) { + return "unexpected end of file in {0}"; + } + if (id == MsgId::EofLookingFor) { + return "unexpected end of file while looking for '{0}'"; + } + if (id == MsgId::Unterminated) { + return "unterminated {0}"; + } + if (id == MsgId::CannotDetermine) { + return "could not determine {0}"; + } + if (id == MsgId::LexErrorLabel) { + return "lex error: {0}"; + } + if (id == MsgId::SyntaxErrorLabel) { + return "syntax error: {0}"; + } + if (id == MsgId::FileNotFound) { + return "error: file not found: {0}"; + } + return ""; +} + +// ---- フォーマッタ(プレースホルダをこの場で埋める。引数数ごとに提供) ---- + +// 引数1つのメッセージを組み立てる +export string msgf1(MsgId id, string a0) { + return template_of(id, current_lang()).replace("{0}", a0); +} + +// 引数2つのメッセージを組み立てる +export string msgf2(MsgId id, string a0, string a1) { + return template_of(id, current_lang()).replace("{0}", a0).replace("{1}", a1); +} + +// 引数3つのメッセージを組み立てる +export string msgf3(MsgId id, string a0, string a1, string a2) { + return template_of(id, current_lang()).replace("{0}", a0).replace("{1}", a1).replace("{2}", + a2); +} + +// ---- 診断ビルダー(呼び出し側の公開API。IDと引数の対応をここで固定する) ---- + // 「expected '<期待字句>' but found <実際>」 export string msg_expected(string expected, string found) { - return "expected '" + expected + "' but found " + found; + return msgf2(MsgId::ExpectedButFound, expected, found); } // 「expected <期待物> but found <実際>」(字句でなく概念を期待する場合) export string msg_expected_thing(string what, string found) { - return "expected " + what + " but found " + found; + return msgf2(MsgId::ExpectedThing, what, found); } // 「expected '<期待字句>' in <文脈> but found <実際>」 export string msg_expected_in(string expected, string context, string found) { - return "expected '" + expected + "' in " + context + " but found " + found; + return msgf3(MsgId::ExpectedIn, expected, context, found); } // 「expected '{' or ';' after <対象> but found <実際>」(本体または宣言終端の欠落) export string msg_expected_body_or_semi(string after, string found) { - return "expected '{' or ';' after " + after + " but found " + found; + return msgf2(MsgId::ExpectedBodyOrSemi, after, found); } // 「unexpected end of file in <文脈>」 export string msg_eof_in(string context) { - return "unexpected end of file in " + context; + return msgf1(MsgId::EofIn, context); } // 「unexpected end of file while looking for '<区切り>'」 export string msg_eof_looking_for(string delimiter) { - return "unexpected end of file while looking for '" + delimiter + "'"; + return msgf1(MsgId::EofLookingFor, delimiter); } // 「unterminated <対象>」(未終端リテラル・コメント) export string msg_unterminated(string what) { - return "unterminated " + what; + return msgf1(MsgId::Unterminated, what); } // 「could not determine <対象>」(名前の特定失敗) export string msg_cannot_determine(string what) { - return "could not determine " + what; + return msgf1(MsgId::CannotDetermine, what); +} + +// 「lex error: <詳細>」(エラー出力の見出し行) +export string msg_lex_error(string detail) { + return msgf1(MsgId::LexErrorLabel, detail); +} + +// 「syntax error: <詳細>」(エラー出力の見出し行) +export string msg_syntax_error(string detail) { + return msgf1(MsgId::SyntaxErrorLabel, detail); +} + +// 「error: file not found: <パス>」 +export string msg_file_not_found(string path) { + return msgf1(MsgId::FileNotFound, path); } diff --git a/examples/08_selfhost_parser/diag/messages_test.cm b/examples/08_selfhost_parser/diag/messages_test.cm new file mode 100644 index 00000000..f5600d76 --- /dev/null +++ b/examples/08_selfhost_parser/diag/messages_test.cm @@ -0,0 +1,50 @@ +// diag/messages.cm の単体テスト(#[test] 関数を `cm test` が実行する) +// メッセージID×言語のテンプレート表とプレースホルダ埋め、CM_LANGによる言語切替を検証する +import std::debug::assert; +import std::env::*; +import ./messages::*; + +#[test] +void english_is_default() { + set("CM_LANG", "en"); + assert(msg_expected(";", "end of file") == "expected ';' but found end of file", + "en template with two placeholders"); + assert(msg_eof_in("import path") == "unexpected end of file in import path", + "en template with one placeholder"); + assert(msg_expected_in("{", "struct declaration", "';'") == + "expected '{' in struct declaration but found ';'", + "en template with three placeholders"); +} + +#[test] +void japanese_via_env() { + set("CM_LANG", "ja"); + assert(msg_expected(";", "end of file") == "';' が必要ですが end of file が見つかりました", + "ja template"); + assert(msg_unterminated("string literal") == "string literal が終端していません", + "ja unterminated"); + set("CM_LANG", "en"); +} + +#[test] +void placeholder_positions_differ_per_language() { + // 語順が違う言語でも断片連結でなくテンプレート置換なので正しく並ぶ + set("CM_LANG", "ja"); + const string ja = msg_expected_in("=", "const declaration", "';'"); + assert(ja == "const declaration には '=' が必要ですが ';' が見つかりました", + "ja word order puts context first"); + set("CM_LANG", "en"); + const string en = msg_expected_in("=", "const declaration", "';'"); + assert(en == "expected '=' in const declaration but found ';'", + "en word order puts expected first"); +} + +#[test] +void error_labels_are_localized() { + set("CM_LANG", "ja"); + assert(msg_syntax_error("x") == "構文エラー: x", "ja syntax label"); + assert(msg_lex_error("y") == "字句エラー: y", "ja lex label"); + set("CM_LANG", "en"); + assert(msg_syntax_error("x") == "syntax error: x", "en syntax label"); + assert(msg_file_not_found("a.cm") == "error: file not found: a.cm", "en file not found"); +} diff --git a/examples/08_selfhost_parser/main.cm b/examples/08_selfhost_parser/main.cm index 376e0eec..76faf21a 100644 --- a/examples/08_selfhost_parser/main.cm +++ b/examples/08_selfhost_parser/main.cm @@ -6,6 +6,7 @@ import std::env::args; import std::fs::{read_file, exists}; import std::strings::split::{lines}; import std::strings::format::*; +import ./diag/messages::*; import ./lexer/scan::*; import ./parser/state::*; import ./parser/decl/dispatch::*; @@ -48,7 +49,7 @@ int main() { for (int ai = 1; ai < argv.len(); ai++) { const string path = argv[ai]; if (!exists(path)) { - println("error: file not found: {path}"); + println("{msg_file_not_found(path)}"); failures = failures + 1; continue; } @@ -59,7 +60,7 @@ int main() { const Option lexed = lx.run(); if (lexed.is_none()) { println("== {path}"); - println("lex error: {lx.error_message()}"); + println("{msg_lex_error(lx.error_message())}"); println(" --> {path}:{lx.error_line()}:{lx.error_col()}"); print_error_context(path, src, lx.error_line(), lx.error_col()); failures = failures + 1; @@ -71,7 +72,7 @@ int main() { const Option parsed = parse_program(&ps); if (parsed.is_none()) { println("== {path}"); - println("syntax error: {ps.error_message()}"); + println("{msg_syntax_error(ps.error_message())}"); println(" --> {path}:{ps.error_line()}:{ps.error_col()}"); print_error_context(path, src, ps.error_line(), ps.error_col()); failures = failures + 1; diff --git a/scripts/ci/check_examples.sh b/scripts/ci/check_examples.sh index 149c6a65..42368d5e 100755 --- a/scripts/ci/check_examples.sh +++ b/scripts/ci/check_examples.sh @@ -28,7 +28,7 @@ echo "✅ selfhost drill passed (jit/native artifacts identical)" # セルフホストパーサ(examples/08_selfhost_parser)の検証: # 各モジュールの#[test]単体テスト → 正常サンプルの定義一覧出力(rc=0) → エラーサンプルの位置報告(rc=1) → パーサ自身の全ソース自己解析 SHP=examples/08_selfhost_parser -for t in lexer/token_test lexer/scan_test parser/state_test parser/decl/modules_test parser/decl/types_test parser/decl/impls_test parser/decl/funcs_test parser/decl/dispatch_test; do +for t in diag/messages_test lexer/token_test lexer/scan_test parser/state_test parser/decl/modules_test parser/decl/types_test parser/decl/impls_test parser/decl/funcs_test parser/decl/dispatch_test; do ./cm test "$SHP/$t.cm" >/dev/null done ./cm run "$SHP/main.cm" -- "$SHP/samples/ok.cm" >/dev/null From 54ef8c50a61fe9cb21874f01c2cec3ace7e9ce02 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 17:53:52 +0900 Subject: [PATCH 10/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E3=81=AE=E5=86=97?= =?UTF-8?q?=E9=95=B7=E3=81=AAif=E9=80=A3=E9=8E=96=E3=82=92match/switch?= =?UTF-8?q?=E3=81=B8=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - diag/messages.cmのテンプレート表: 言語→IDの二重if連鎖をmatch (Lang)×match (MsgId)の二段matchへ変更(網羅が構文上明示され、ID追加漏れが読み取りやすくなる) - parser/decl/dispatch.cmの宣言分岐: 先頭字句の12連ifをswitch (p->text())のcase("struct")等へ変更 - lexer/scan.cmのバイト分岐はifを維持し、理由をコメントで明記(switchのcase(const変数)は現状値が解決されず全て0として重複caseの誤コンパイルになるため、リテラル以外をcaseに使えない。範囲判定・2バイト先読みもifが適切) - 全60テスト・正常/エラーサンプル・CM_LANG=ja出力・自己解析・CI 3項目の通過を確認 --- examples/08_selfhost_parser/diag/messages.cm | 139 +++++++++--------- examples/08_selfhost_parser/lexer/scan.cm | 1 + .../parser/decl/dispatch.cm | 111 +++++++------- 3 files changed, 132 insertions(+), 119 deletions(-) diff --git a/examples/08_selfhost_parser/diag/messages.cm b/examples/08_selfhost_parser/diag/messages.cm index 3c42db4b..da5cec62 100644 --- a/examples/08_selfhost_parser/diag/messages.cm +++ b/examples/08_selfhost_parser/diag/messages.cm @@ -38,74 +38,81 @@ export Lang current_lang() { // メッセージID×言語のテンプレート表(プレースホルダ{0}〜{2}を持つ完結した1文) string template_of(MsgId id, Lang lang) { - if (lang == Lang::Ja) { - if (id == MsgId::ExpectedButFound) { - return "'{0}' が必要ですが {1} が見つかりました"; + match (lang) { + Lang::Ja => { + match (id) { + MsgId::ExpectedButFound => { + return "'{0}' が必要ですが {1} が見つかりました"; + } + MsgId::ExpectedThing => { + return "{0} が必要ですが {1} が見つかりました"; + } + MsgId::ExpectedIn => { + return "{1} には '{0}' が必要ですが {2} が見つかりました"; + } + MsgId::ExpectedBodyOrSemi => { + return "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"; + } + MsgId::EofIn => { + return "{0} の途中で入力が終端しました"; + } + MsgId::EofLookingFor => { + return "'{0}' を探索中に入力が終端しました"; + } + MsgId::Unterminated => { + return "{0} が終端していません"; + } + MsgId::CannotDetermine => { + return "{0} を特定できません"; + } + MsgId::LexErrorLabel => { + return "字句エラー: {0}"; + } + MsgId::SyntaxErrorLabel => { + return "構文エラー: {0}"; + } + MsgId::FileNotFound => { + return "エラー: ファイルが見つかりません: {0}"; + } + } } - if (id == MsgId::ExpectedThing) { - return "{0} が必要ですが {1} が見つかりました"; + Lang::En => { + match (id) { + MsgId::ExpectedButFound => { + return "expected '{0}' but found {1}"; + } + MsgId::ExpectedThing => { + return "expected {0} but found {1}"; + } + MsgId::ExpectedIn => { + return "expected '{0}' in {1} but found {2}"; + } + MsgId::ExpectedBodyOrSemi => { + return "expected '{' or ';' after {0} but found {1}"; + } + MsgId::EofIn => { + return "unexpected end of file in {0}"; + } + MsgId::EofLookingFor => { + return "unexpected end of file while looking for '{0}'"; + } + MsgId::Unterminated => { + return "unterminated {0}"; + } + MsgId::CannotDetermine => { + return "could not determine {0}"; + } + MsgId::LexErrorLabel => { + return "lex error: {0}"; + } + MsgId::SyntaxErrorLabel => { + return "syntax error: {0}"; + } + MsgId::FileNotFound => { + return "error: file not found: {0}"; + } + } } - if (id == MsgId::ExpectedIn) { - return "{1} には '{0}' が必要ですが {2} が見つかりました"; - } - if (id == MsgId::ExpectedBodyOrSemi) { - return "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"; - } - if (id == MsgId::EofIn) { - return "{0} の途中で入力が終端しました"; - } - if (id == MsgId::EofLookingFor) { - return "'{0}' を探索中に入力が終端しました"; - } - if (id == MsgId::Unterminated) { - return "{0} が終端していません"; - } - if (id == MsgId::CannotDetermine) { - return "{0} を特定できません"; - } - if (id == MsgId::LexErrorLabel) { - return "字句エラー: {0}"; - } - if (id == MsgId::SyntaxErrorLabel) { - return "構文エラー: {0}"; - } - if (id == MsgId::FileNotFound) { - return "エラー: ファイルが見つかりません: {0}"; - } - return ""; - } - if (id == MsgId::ExpectedButFound) { - return "expected '{0}' but found {1}"; - } - if (id == MsgId::ExpectedThing) { - return "expected {0} but found {1}"; - } - if (id == MsgId::ExpectedIn) { - return "expected '{0}' in {1} but found {2}"; - } - if (id == MsgId::ExpectedBodyOrSemi) { - return "expected '{' or ';' after {0} but found {1}"; - } - if (id == MsgId::EofIn) { - return "unexpected end of file in {0}"; - } - if (id == MsgId::EofLookingFor) { - return "unexpected end of file while looking for '{0}'"; - } - if (id == MsgId::Unterminated) { - return "unterminated {0}"; - } - if (id == MsgId::CannotDetermine) { - return "could not determine {0}"; - } - if (id == MsgId::LexErrorLabel) { - return "lex error: {0}"; - } - if (id == MsgId::SyntaxErrorLabel) { - return "syntax error: {0}"; - } - if (id == MsgId::FileNotFound) { - return "error: file not found: {0}"; } return ""; } diff --git a/examples/08_selfhost_parser/lexer/scan.cm b/examples/08_selfhost_parser/lexer/scan.cm index 8601d6fe..3cc81bca 100644 --- a/examples/08_selfhost_parser/lexer/scan.cm +++ b/examples/08_selfhost_parser/lexer/scan.cm @@ -185,6 +185,7 @@ export impl Lexer { } // 全体を字句解析する(成功はSome(true)、失敗はNoneで位置はerror_line/error_col) + // バイト値の分岐はifで行う(switchのcase(const変数)は現状値が解決されないため、リテラル以外をcaseに使えない。範囲判定・2バイト先読みもあるためifが適切) Option run() { while (true) { const Option trivia = self.skip_trivia(); diff --git a/examples/08_selfhost_parser/parser/decl/dispatch.cm b/examples/08_selfhost_parser/parser/decl/dispatch.cm index 3a5c2c08..11dd7d76 100644 --- a/examples/08_selfhost_parser/parser/decl/dispatch.cm +++ b/examples/08_selfhost_parser/parser/decl/dispatch.cm @@ -9,68 +9,73 @@ import ./types::*; import ./impls::*; import ./funcs::*; -// トップレベル宣言を1つ解析する +// トップレベル宣言を1つ解析する(先頭字句のswitchで宣言種別ごとの解析へ振り分ける) export Option parse_decl(Parser* p) { - // 属性は読み飛ばす - if (p->at_text("#")) { - return p->skip_attribute(); - } - // exportは接頭辞として無視して次を見る - if (p->at_text("export")) { - p->bump(); - return parse_decl(p); - } - if (p->at_text("module")) { - return parse_module_decl(p); - } - if (p->at_text("import")) { - return parse_import_decl(p); - } - if (p->at_text("use")) { - return parse_use_decl(p); - } - if (p->at_text("typedef")) { - return parse_typedef(p); - } - if (p->at_text("const")) { - return parse_const(p); - } - if (p->at_text("extern")) { - return parse_extern(p); - } - if (p->at_text("struct")) { - const Option r = parse_struct(p, ""); - if (r.is_none()) { - return Option::None; + switch (p->text()) { + case("#") { + // 属性は読み飛ばす + return p->skip_attribute(); } - // 無名/宣言子付きの "STR;" 形式が続く場合は消費する - if (p->at_text(";")) { + case("export") { + // exportは接頭辞として無視して次を見る p->bump(); - } else if (p->at_ident()) { - const Option d = p->skip_to_semicolon(); - if (d.is_none()) { + return parse_decl(p); + } + case("module") { + return parse_module_decl(p); + } + case("import") { + return parse_import_decl(p); + } + case("use") { + return parse_use_decl(p); + } + case("typedef") { + return parse_typedef(p); + } + case("const") { + return parse_const(p); + } + case("extern") { + return parse_extern(p); + } + case("struct") { + const Option r = parse_struct(p, ""); + if (r.is_none()) { return Option::None; } + // 無名/宣言子付きの "STR;" 形式が続く場合は消費する + if (p->at_text(";")) { + p->bump(); + } else if (p->at_ident()) { + const Option d = p->skip_to_semicolon(); + if (d.is_none()) { + return Option::None; + } + } + return Option::Some(true); } - return Option::Some(true); - } - if (p->at_text("enum")) { - const Option r = parse_enum(p, ""); - if (r.is_none()) { - return Option::None; + case("enum") { + const Option r = parse_enum(p, ""); + if (r.is_none()) { + return Option::None; + } + if (p->at_text(";")) { + p->bump(); + } + return Option::Some(true); } - if (p->at_text(";")) { - p->bump(); + case("impl") { + return parse_impl(p); + } + case("interface") { + return parse_interface(p); + } + else { + // それ以外は関数またはグローバル定義とみなす + return parse_func_or_var(p); } - return Option::Some(true); - } - if (p->at_text("impl")) { - return parse_impl(p); - } - if (p->at_text("interface")) { - return parse_interface(p); } - // それ以外は関数またはグローバル定義とみなす return parse_func_or_var(p); } From bb28db42e6bccac0b9f00155db3f57a8dc2dd68c Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 17:56:21 +0900 Subject: [PATCH 11/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E3=81=AEi18n?= =?UTF-8?q?=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88=E8=A1=A8?= =?UTF-8?q?=E3=81=AEmatch=E9=A0=86=E3=82=92=E3=83=A1=E3=83=83=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=82=B8ID=E2=86=92=E8=A8=80=E8=AA=9E=E3=81=B8?= =?UTF-8?q?=E5=85=A5=E3=82=8C=E6=9B=BF=E3=81=88=EF=BC=881=E3=81=A4?= =?UTF-8?q?=E3=81=AE=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=AE?= =?UTF-8?q?=E5=85=A8=E5=AF=BE=E8=A8=B3=E3=81=8C1=E7=AE=87=E6=89=80?= =?UTF-8?q?=E3=81=AB=E9=9B=86=E3=81=BE=E3=82=8A=E3=80=81ID=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E6=99=82=E3=81=AB=E5=AF=BE=E8=A8=B3=E3=82=92=E4=B8=A6?= =?UTF-8?q?=E3=81=B9=E3=81=A6=E7=AE=A1=E7=90=86=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E6=A7=8B=E6=88=90=E3=81=AB=E3=81=99=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/08_selfhost_parser/diag/messages.cm | 135 ++++++++++++------- 1 file changed, 86 insertions(+), 49 deletions(-) diff --git a/examples/08_selfhost_parser/diag/messages.cm b/examples/08_selfhost_parser/diag/messages.cm index da5cec62..19fef73f 100644 --- a/examples/08_selfhost_parser/diag/messages.cm +++ b/examples/08_selfhost_parser/diag/messages.cm @@ -37,80 +37,117 @@ export Lang current_lang() { } // メッセージID×言語のテンプレート表(プレースホルダ{0}〜{2}を持つ完結した1文) +// 外側をメッセージID・内側を言語のmatchにして、1つのメッセージの全対訳を1箇所へ集める(ID追加時は対訳を並べて書く) string template_of(MsgId id, Lang lang) { - match (lang) { - Lang::Ja => { - match (id) { - MsgId::ExpectedButFound => { - return "'{0}' が必要ですが {1} が見つかりました"; - } - MsgId::ExpectedThing => { - return "{0} が必要ですが {1} が見つかりました"; - } - MsgId::ExpectedIn => { - return "{1} には '{0}' が必要ですが {2} が見つかりました"; - } - MsgId::ExpectedBodyOrSemi => { - return "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"; - } - MsgId::EofIn => { - return "{0} の途中で入力が終端しました"; - } - MsgId::EofLookingFor => { - return "'{0}' を探索中に入力が終端しました"; - } - MsgId::Unterminated => { - return "{0} が終端していません"; - } - MsgId::CannotDetermine => { - return "{0} を特定できません"; - } - MsgId::LexErrorLabel => { - return "字句エラー: {0}"; - } - MsgId::SyntaxErrorLabel => { - return "構文エラー: {0}"; + match (id) { + MsgId::ExpectedButFound => { + match (lang) { + Lang::En => { + return "expected '{0}' but found {1}"; } - MsgId::FileNotFound => { - return "エラー: ファイルが見つかりません: {0}"; + Lang::Ja => { + return "'{0}' が必要ですが {1} が見つかりました"; } } } - Lang::En => { - match (id) { - MsgId::ExpectedButFound => { - return "expected '{0}' but found {1}"; - } - MsgId::ExpectedThing => { + MsgId::ExpectedThing => { + match (lang) { + Lang::En => { return "expected {0} but found {1}"; } - MsgId::ExpectedIn => { + Lang::Ja => { + return "{0} が必要ですが {1} が見つかりました"; + } + } + } + MsgId::ExpectedIn => { + match (lang) { + Lang::En => { return "expected '{0}' in {1} but found {2}"; } - MsgId::ExpectedBodyOrSemi => { + Lang::Ja => { + return "{1} には '{0}' が必要ですが {2} が見つかりました"; + } + } + } + MsgId::ExpectedBodyOrSemi => { + match (lang) { + Lang::En => { return "expected '{' or ';' after {0} but found {1}"; } - MsgId::EofIn => { + Lang::Ja => { + return "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"; + } + } + } + MsgId::EofIn => { + match (lang) { + Lang::En => { return "unexpected end of file in {0}"; } - MsgId::EofLookingFor => { + Lang::Ja => { + return "{0} の途中で入力が終端しました"; + } + } + } + MsgId::EofLookingFor => { + match (lang) { + Lang::En => { return "unexpected end of file while looking for '{0}'"; } - MsgId::Unterminated => { + Lang::Ja => { + return "'{0}' を探索中に入力が終端しました"; + } + } + } + MsgId::Unterminated => { + match (lang) { + Lang::En => { return "unterminated {0}"; } - MsgId::CannotDetermine => { + Lang::Ja => { + return "{0} が終端していません"; + } + } + } + MsgId::CannotDetermine => { + match (lang) { + Lang::En => { return "could not determine {0}"; } - MsgId::LexErrorLabel => { + Lang::Ja => { + return "{0} を特定できません"; + } + } + } + MsgId::LexErrorLabel => { + match (lang) { + Lang::En => { return "lex error: {0}"; } - MsgId::SyntaxErrorLabel => { + Lang::Ja => { + return "字句エラー: {0}"; + } + } + } + MsgId::SyntaxErrorLabel => { + match (lang) { + Lang::En => { return "syntax error: {0}"; } - MsgId::FileNotFound => { + Lang::Ja => { + return "構文エラー: {0}"; + } + } + } + MsgId::FileNotFound => { + match (lang) { + Lang::En => { return "error: file not found: {0}"; } + Lang::Ja => { + return "エラー: ファイルが見つかりません: {0}"; + } } } } From 72b98ba3e1b7cecacdb0a893c5ecfb37605b2633 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 17:58:01 +0900 Subject: [PATCH 12/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E3=81=AEi18n?= =?UTF-8?q?=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88=E8=A1=A8?= =?UTF-8?q?=E3=82=92=E5=AF=BE=E8=A8=B3=E9=85=8D=E5=88=97=E5=BD=A2=E5=BC=8F?= =?UTF-8?q?=E3=81=B8=E7=B0=A1=E7=B4=A0=E5=8C=96=EF=BC=88=E8=A8=80=E8=AA=9E?= =?UTF-8?q?=E3=81=AEmatch=E3=82=92=E5=BB=83=E6=AD=A2=E3=81=97=E3=80=81?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8ID=E3=81=94?= =?UTF-8?q?=E3=81=A8=E3=81=AB[=E8=8B=B1=E8=AA=9E,=20=E6=97=A5=E6=9C=AC?= =?UTF-8?q?=E8=AA=9E]=E3=81=AE=E9=85=8D=E5=88=97=E3=83=AA=E3=83=86?= =?UTF-8?q?=E3=83=A9=E3=83=AB1=E3=81=A4=E3=81=A7=E5=85=A8=E5=AF=BE?= =?UTF-8?q?=E8=A8=B3=E3=82=92=E7=AE=A1=E7=90=86=E3=80=82=E6=B7=BB=E5=AD=97?= =?UTF-8?q?=E3=81=AFLang=E3=81=AE=E3=82=BF=E3=82=B0=E5=80=A4=E3=81=A7?= =?UTF-8?q?=E3=80=81=E5=AF=BE=E8=A8=B3=E6=9C=AA=E6=95=B4=E5=82=99=E3=81=AE?= =?UTF-8?q?=E8=A8=80=E8=AA=9E=E3=81=AF=E8=8B=B1=E8=AA=9E=E3=81=B8=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=BC=E3=83=AB=E3=83=90=E3=83=83=E3=82=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/08_selfhost_parser/diag/messages.cm | 117 ++++--------------- 1 file changed, 25 insertions(+), 92 deletions(-) diff --git a/examples/08_selfhost_parser/diag/messages.cm b/examples/08_selfhost_parser/diag/messages.cm index 19fef73f..af91f985 100644 --- a/examples/08_selfhost_parser/diag/messages.cm +++ b/examples/08_selfhost_parser/diag/messages.cm @@ -36,122 +36,55 @@ export Lang current_lang() { return Lang::En; } -// メッセージID×言語のテンプレート表(プレースホルダ{0}〜{2}を持つ完結した1文) -// 外側をメッセージID・内側を言語のmatchにして、1つのメッセージの全対訳を1箇所へ集める(ID追加時は対訳を並べて書く) -string template_of(MsgId id, Lang lang) { +// メッセージID→対訳配列の表(プレースホルダ{0}〜{2}を持つ完結した1文) +// 添字はLangのタグ値(En=0, Ja=1)。言語のmatchは持たず、1メッセージの全対訳を1つの配列リテラルで管理する +string[] templates_of(MsgId id) { match (id) { MsgId::ExpectedButFound => { - match (lang) { - Lang::En => { - return "expected '{0}' but found {1}"; - } - Lang::Ja => { - return "'{0}' が必要ですが {1} が見つかりました"; - } - } + return ["expected '{0}' but found {1}", "'{0}' が必要ですが {1} が見つかりました"]; } MsgId::ExpectedThing => { - match (lang) { - Lang::En => { - return "expected {0} but found {1}"; - } - Lang::Ja => { - return "{0} が必要ですが {1} が見つかりました"; - } - } + return ["expected {0} but found {1}", "{0} が必要ですが {1} が見つかりました"]; } MsgId::ExpectedIn => { - match (lang) { - Lang::En => { - return "expected '{0}' in {1} but found {2}"; - } - Lang::Ja => { - return "{1} には '{0}' が必要ですが {2} が見つかりました"; - } - } + return ["expected '{0}' in {1} but found {2}", "{1} には '{0}' が必要ですが {2} が見つかりました"]; } MsgId::ExpectedBodyOrSemi => { - match (lang) { - Lang::En => { - return "expected '{' or ';' after {0} but found {1}"; - } - Lang::Ja => { - return "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"; - } - } + return ["expected '{' or ';' after {0} but found {1}", "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"]; } MsgId::EofIn => { - match (lang) { - Lang::En => { - return "unexpected end of file in {0}"; - } - Lang::Ja => { - return "{0} の途中で入力が終端しました"; - } - } + return ["unexpected end of file in {0}", "{0} の途中で入力が終端しました"]; } MsgId::EofLookingFor => { - match (lang) { - Lang::En => { - return "unexpected end of file while looking for '{0}'"; - } - Lang::Ja => { - return "'{0}' を探索中に入力が終端しました"; - } - } + return ["unexpected end of file while looking for '{0}'", "'{0}' を探索中に入力が終端しました"]; } MsgId::Unterminated => { - match (lang) { - Lang::En => { - return "unterminated {0}"; - } - Lang::Ja => { - return "{0} が終端していません"; - } - } + return ["unterminated {0}", "{0} が終端していません"]; } MsgId::CannotDetermine => { - match (lang) { - Lang::En => { - return "could not determine {0}"; - } - Lang::Ja => { - return "{0} を特定できません"; - } - } + return ["could not determine {0}", "{0} を特定できません"]; } MsgId::LexErrorLabel => { - match (lang) { - Lang::En => { - return "lex error: {0}"; - } - Lang::Ja => { - return "字句エラー: {0}"; - } - } + return ["lex error: {0}", "字句エラー: {0}"]; } MsgId::SyntaxErrorLabel => { - match (lang) { - Lang::En => { - return "syntax error: {0}"; - } - Lang::Ja => { - return "構文エラー: {0}"; - } - } + return ["syntax error: {0}", "構文エラー: {0}"]; } MsgId::FileNotFound => { - match (lang) { - Lang::En => { - return "error: file not found: {0}"; - } - Lang::Ja => { - return "エラー: ファイルが見つかりません: {0}"; - } - } + return ["error: file not found: {0}", "エラー: ファイルが見つかりません: {0}"]; } } - return ""; + return [""]; +} + +// テンプレートの取り出し(対訳が未整備の言語は英語=添字0へフォールバックする) +string template_of(MsgId id, Lang lang) { + const string[] t = templates_of(id); + const int li = lang as int; + if (li < 0 || li >= t.len()) { + return t[0]; + } + return t[li]; } // ---- フォーマッタ(プレースホルダをこの場で埋める。引数数ごとに提供) ---- From f7d56b292a1fdea82cc9d8f4dd3544605e5e4374 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 18:16:34 +0900 Subject: [PATCH 13/25] =?UTF-8?q?make=E3=82=BF=E3=83=BC=E3=82=B2=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=82=92install-vscode-extension=E3=81=B8=E6=94=B9?= =?UTF-8?q?=E5=90=8D=EF=BC=88=E6=93=8D=E4=BD=9C=E7=B3=BB=E3=81=AF=E5=8B=95?= =?UTF-8?q?=E8=A9=9E=E5=85=88=E9=A0=AD=E3=83=BB=E6=88=90=E6=9E=9C=E7=89=A9?= =?UTF-8?q?=E3=81=AF=E5=90=8D=E8=A9=9E=E3=81=AE=E5=91=BD=E5=90=8D=E8=A6=8F?= =?UTF-8?q?=E7=B4=84=E3=81=B8=E7=B5=B1=E4=B8=80=E3=80=82=E6=97=A7=E5=90=8D?= =?UTF-8?q?vscode-extension-install=E3=81=AF=E9=9D=9E=E6=8E=A8=E5=A5=A8?= =?UTF-8?q?=E3=82=A8=E3=82=A4=E3=83=AA=E3=82=A2=E3=82=B9=E3=81=A8=E3=81=97?= =?UTF-8?q?=E3=81=A6=E6=A1=88=E5=86=85=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E4=BB=98=E3=81=8D=E3=81=A7=E6=AE=8B=E3=81=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 10 ++++++++-- docs/releases/v0.17.2.md | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 02320c2e..9af682c3 100644 --- a/Makefile +++ b/Makefile @@ -267,8 +267,8 @@ vscode-extension: @echo "✅ VSCode拡張ビルド完了:" @ls -lh vscode-extension/cm-language-*.vsix | awk '{print " " $$9 " (" $$5 ")"}' -.PHONY: vscode-extension-install -vscode-extension-install: vscode-extension +.PHONY: install-vscode-extension +install-vscode-extension: vscode-extension @VERSION=$$(cat VERSION | tr -d '[:space:]'); \ CODE_BIN=$$(command -v code || true); \ if [ -z "$$CODE_BIN" ] && [ -x "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" ]; then \ @@ -283,6 +283,12 @@ vscode-extension-install: vscode-extension "$$CODE_BIN" --install-extension "vscode-extension/cm-language-$$VERSION.vsix" && \ echo "✅ VSCode拡張をインストールしました (cm-language-$$VERSION)" +# 旧名の互換エイリアス(操作ターゲットは動詞先頭の命名規約に合わせinstall-vscode-extensionへ改名済み) +.PHONY: vscode-extension-install +vscode-extension-install: + @echo "⚠ 'make vscode-extension-install' は非推奨です。'make install-vscode-extension' を使ってください" + @$(MAKE) install-vscode-extension + .PHONY: dist dist: release @VERSION=$$(cat VERSION | tr -d '[:space:]'); \ diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index a4f222a2..90c53830 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -227,6 +227,13 @@ JSのポインタ表現(配列+添字のセル参照)がスライスポイ 選択import(`::{名前}`)が対象関数本体を即時型検査する挙動と、ユーザ定義Optionのフラット名前空間上書きの合成が真因。 ライブラリ内部の利用をワイルドカードimport(検査が利用時まで遅延)へ変更して回避し、型解決のモジュール分離を言語側の設計課題に記録した。 +## 🔧 ツーリング + +### makeターゲット名の統一(install-vscode-extension) + +操作系ターゲットは動詞先頭(`install` / `test-*` / `update-docs-version`)、成果物ターゲットは名詞(`release` / `dist` / `vscode-extension`)という命名規約に対し、`vscode-extension-install` だけが操作なのに名詞先頭だった。 +`make install-vscode-extension` へ改名した(旧名は非推奨の互換エイリアスとして案内メッセージ付きで残る)。 + ## 📦 サンプル ### セルフホストパーサ(examples/08_selfhost_parser) From 410408a66cbbd505aeb02ed62efcbf1b0eb83762 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 18:18:37 +0900 Subject: [PATCH 14/25] =?UTF-8?q?install-vscode-extension=E3=81=AE?= =?UTF-8?q?=E9=9D=9E=E6=8E=A8=E5=A5=A8=E3=82=A8=E3=82=A4=E3=83=AA=E3=82=A2?= =?UTF-8?q?=E3=82=B9=EF=BC=88=E6=97=A7=E5=90=8Dvscode-extension-install?= =?UTF-8?q?=EF=BC=89=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 6 ------ docs/releases/v0.17.2.md | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 9af682c3..ee9e94ae 100644 --- a/Makefile +++ b/Makefile @@ -283,12 +283,6 @@ install-vscode-extension: vscode-extension "$$CODE_BIN" --install-extension "vscode-extension/cm-language-$$VERSION.vsix" && \ echo "✅ VSCode拡張をインストールしました (cm-language-$$VERSION)" -# 旧名の互換エイリアス(操作ターゲットは動詞先頭の命名規約に合わせinstall-vscode-extensionへ改名済み) -.PHONY: vscode-extension-install -vscode-extension-install: - @echo "⚠ 'make vscode-extension-install' は非推奨です。'make install-vscode-extension' を使ってください" - @$(MAKE) install-vscode-extension - .PHONY: dist dist: release @VERSION=$$(cat VERSION | tr -d '[:space:]'); \ diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index 90c53830..156fb842 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -232,7 +232,7 @@ JSのポインタ表現(配列+添字のセル参照)がスライスポイ ### makeターゲット名の統一(install-vscode-extension) 操作系ターゲットは動詞先頭(`install` / `test-*` / `update-docs-version`)、成果物ターゲットは名詞(`release` / `dist` / `vscode-extension`)という命名規約に対し、`vscode-extension-install` だけが操作なのに名詞先頭だった。 -`make install-vscode-extension` へ改名した(旧名は非推奨の互換エイリアスとして案内メッセージ付きで残る)。 +`make install-vscode-extension` へ改名した。 ## 📦 サンプル From 2bbc70f415b00eb0c253e20fa9532cab19ecb217 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 18:37:35 +0900 Subject: [PATCH 15/25] =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=BC=E3=83=9E?= =?UTF-8?q?=E3=83=83=E3=82=BF:=20=E6=96=87=E3=83=AC=E3=83=99=E3=83=AB?= =?UTF-8?q?=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E3=83=81=E3=82=A7=E3=83=BC?= =?UTF-8?q?=E3=83=B3=E3=81=AETS=E9=A2=A8=E6=8A=98=E3=82=8A=E8=BF=94?= =?UTF-8?q?=E3=81=97=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wrap_long_linesの折り返し候補はカンマ直後と二項演算子直前のみで、文レベルのメソッドチェーンが最大行幅を超えると最後の呼び出しの引数リスト内カンマで折れてチェーン構造が読めない整形になっていた。 .method(...)の呼び出しが2つ以上連なる超過行は、カンマ・演算子折り返しより優先して各チェーンの.直前で折り返すようにした(TypeScript/Prettier風・継続行1段インデント)。 フィールド参照の.や数値リテラルの小数点、括弧内のチェーンは候補にせず、折り返し後もなお長いセグメントは再帰適用でカンマ折り返しへ委ねる(各セグメントのチェーン.は高々1つなので停止する)。 回帰テスト(wrap/method_chain: チェーン折り返し・フィールド参照のみの行は従来のカンマ折り返し・冪等性)とチュートリアル(ja/en 2.2.1節)・リリースノートを追加し、設計文書はarchiveへ収めた。 VSCode拡張は構文追加がないため対象外。 --- .../v0.17.2/formatter-method-chain-wrap.md | 40 +++++++++++++ docs/releases/v0.17.2.md | 14 +++++ .../tutorials/en/compiler/common/formatter.md | 17 ++++++ .../tutorials/ja/compiler/common/formatter.md | 17 ++++++ src/internal/fmt/formatter/wrap.cpp | 56 +++++++++++++++++++ .../formatter/wrap/method_chain.expected.cm | 26 +++++++++ .../cases/formatter/wrap/method_chain.input | 18 ++++++ tests/regression/formatter_test.cpp | 5 ++ 8 files changed, 193 insertions(+) create mode 100644 docs/archive/v0.17.2/formatter-method-chain-wrap.md create mode 100644 tests/regression/cases/formatter/wrap/method_chain.expected.cm create mode 100644 tests/regression/cases/formatter/wrap/method_chain.input diff --git a/docs/archive/v0.17.2/formatter-method-chain-wrap.md b/docs/archive/v0.17.2/formatter-method-chain-wrap.md new file mode 100644 index 00000000..f1fa3146 --- /dev/null +++ b/docs/archive/v0.17.2/formatter-method-chain-wrap.md @@ -0,0 +1,40 @@ +# フォーマッタ: メソッドチェーンのTS風折り返し + +## 問題 + +`wrap_long_lines` の折り返し候補はカンマ直後と二項演算子直前のみで、メソッドチェーンの `.` は候補にない。 +そのため文レベルのチェーン式が最大行幅(100桁)を超えると、最後の呼び出しの引数リスト内のカンマで折れてしまい、チェーン構造が読み取れない整形になる。 + +```cm +// 現状の整形結果(最後の引数だけが折り返される) +return template_of(id, current_lang()).replace("{0}", a0).replace("{1}", a1).replace("{2}", + a2); +``` + +## 方針 + +文レベル(丸括弧・角括弧・波括弧の外)に `.メソッド名(` 形のチェーン呼び出しが2つ以上ある超過行は、カンマ・演算子折り返しより優先して各チェーンの `.` 直前で折り返す(TypeScript/Prettier風)。 + +```cm +// 改修後の整形結果 +return template_of(id, current_lang()) + .replace("{0}", a0) + .replace("{1}", a1) + .replace("{2}", a2); +``` + +## 設計 + +- 折り返し候補の走査ループ(文字列・文字リテラル・行末コメントを除外済み)で、括弧スタックが空の位置の `.` を収集する。 +- チェーン呼び出しの `.` とみなす条件: 直後が識別子(英字または `_` 開始)で、識別子の直後が `(` であること。フィールド参照(`self.err_msg = ...`)や数値リテラルの小数点は候補にしない。 +- 候補が2つ以上ある場合のみチェーン折り返しを採用する(単一呼び出しの長い行は従来どおりカンマ・演算子折り返しに委ねる)。 +- 継続行のインデントは1段(`indent_width_`)を仮置きし、後段の `normalize_indentation` 再実行(継続行+1段の規則)と一致させて冪等性を保つ。 +- 折り返し後もなお超過するセグメント(長い引数リスト等)はブロック展開と同様に `wrap_long_lines` を再帰適用してカンマ折り返しへ委ねる。各セグメントに残るチェーン `.` は高々1つなので再帰でチェーン折り返しが再発せず停止する。 +- `if (...)` ヘッダ内などの括弧内チェーンは括弧スタックが非空のため対象外(文レベルのチェーンのみ折り返す)。 + +## テスト計画 + +- regression(`tests/regression/cases/formatter/wrap/method_chain.*`): + - 3連チェーンの超過行が各 `.` 直前で折り返されること(入力→期待値)。 + - フィールド参照の `.` しかない超過行はチェーン折り返しされず、従来のカンマ折り返し(引数展開)になること。 + - 期待値ファイルへの再適用で変化しないこと(既存ハーネスの冪等性検証)。 diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index 156fb842..b4b805e0 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -229,6 +229,20 @@ JSのポインタ表現(配列+添字のセル参照)がスライスポイ ## 🔧 ツーリング +### フォーマッタ: メソッドチェーンのTS風折り返し + +`cm fmt` の折り返し候補はカンマと二項演算子のみで、文レベルのメソッドチェーンが最大行幅(100桁)を超えると最後の呼び出しの引数リスト内で折れてしまい、チェーン構造が読めない整形になっていた([設計文書](../archive/v0.17.2/formatter-method-chain-wrap.html))。 +`.method(...)` の呼び出しが2つ以上連なる超過行は、カンマ・演算子折り返しより優先して各チェーンの `.` 直前で折り返すようにした(TypeScript/Prettier風。継続行は1段インデント)。 + +```cm +return template_text() + .replace("{0}", first_argument) + .replace("{1}", second_argument) + .replace("{2}", third_argument); +``` + +フィールド参照の `.` や数値リテラルの小数点、`if (...)` ヘッダなど括弧内のチェーンは対象にせず、従来の折り返しを維持する。 + ### makeターゲット名の統一(install-vscode-extension) 操作系ターゲットは動詞先頭(`install` / `test-*` / `update-docs-version`)、成果物ターゲットは名詞(`release` / `dist` / `vscode-extension`)という命名規約に対し、`vscode-extension-install` だけが操作なのに名詞先頭だった。 diff --git a/docs/tutorials/en/compiler/common/formatter.md b/docs/tutorials/en/compiler/common/formatter.md index b6d27f4e..4ad4ff9a 100644 --- a/docs/tutorials/en/compiler/common/formatter.md +++ b/docs/tutorials/en/compiler/common/formatter.md @@ -105,6 +105,23 @@ out = (q0 | (q1 << 1) | (q2 << 2) | (1 << 8)) as ushort; ``` +### 2.2.1 Method-chain wrapping (v0.17.2) + +When a statement-level method chain (two or more `.method(...)` calls) exceeds the maximum line width (100 columns), it is wrapped right before each chained `.`, taking priority over comma/operator wrapping (TypeScript/Prettier style): + +```cm +// Before +return template_text().replace("{0}", first_argument).replace("{1}", second_argument).replace("{2}", third_argument); + +// After +return template_text() + .replace("{0}", first_argument) + .replace("{1}", second_argument) + .replace("{2}", third_argument); +``` + +Field-access dots (such as `self.value`) and chains inside parentheses (such as an `if (...)` header) are not affected and keep the conventional comma/operator wrapping. + ### 2.3 Trailing-comment position (v0.16.0) Manually adjusted trailing-comment positions are respected. The gap between code and comment is widened to two spaces only when it is smaller than that: diff --git a/docs/tutorials/ja/compiler/common/formatter.md b/docs/tutorials/ja/compiler/common/formatter.md index 5792d77f..9ab8ee05 100644 --- a/docs/tutorials/ja/compiler/common/formatter.md +++ b/docs/tutorials/ja/compiler/common/formatter.md @@ -105,6 +105,23 @@ out = (q0 | (q1 << 1) | (q2 << 2) | (1 << 8)) as ushort; ``` +### 2.2.1 メソッドチェーンの折り返し(v0.17.2) + +文レベルのメソッドチェーン(`.method(...)` の呼び出しが2つ以上)が最大行幅(100桁)を超える場合は、カンマ・演算子の折り返しより優先して各チェーンの `.` 直前で折り返されます(TypeScript/Prettier風): + +```cm +// Before +return template_text().replace("{0}", first_argument).replace("{1}", second_argument).replace("{2}", third_argument); + +// After +return template_text() + .replace("{0}", first_argument) + .replace("{1}", second_argument) + .replace("{2}", third_argument); +``` + +フィールド参照の `.`(`self.value` 等)や `if (...)` ヘッダなど括弧内のチェーンは対象にならず、従来どおりカンマ・演算子で折り返されます。 + ### 2.3 行末コメントの位置(v0.16.0) 行末コメントの位置は手動調整が尊重されます。コードとコメントの間隔が2スペース未満の場合のみ、2スペースへ広げられます: diff --git a/src/internal/fmt/formatter/wrap.cpp b/src/internal/fmt/formatter/wrap.cpp index eb661a82..5c54863c 100644 --- a/src/internal/fmt/formatter/wrap.cpp +++ b/src/internal/fmt/formatter/wrap.cpp @@ -169,6 +169,7 @@ std::string Formatter::wrap_long_lines(const std::string& code, size_t& changes) }; std::vector cands; std::map close_of; // 開き括弧位置 → 対応する閉じ括弧位置 + std::vector chain_dots; // 文レベルの .method( チェーン呼び出しの . 位置 size_t comment_start = std::string::npos; { bool in_string = false; @@ -214,6 +215,20 @@ std::string Formatter::wrap_long_lines(const std::string& code, size_t& changes) size_t group_open = bracket_stack.empty() ? std::string::npos : bracket_stack.back().second; cands.push_back({i + 1, depth, true, group, group_open}); + } else if (c == '.' && bracket_stack.empty()) { + // 文レベルの .method( をチェーン折り返し候補として収集する(直後が識別子+開き括弧の場合のみ。フィールド参照や数値リテラルの小数点は除外) + size_t j = i + 1; + if (j < line.size() && + (std::isalpha(static_cast(line[j])) || line[j] == '_')) { + while ( + j < line.size() && + (std::isalnum(static_cast(line[j])) || line[j] == '_')) { + ++j; + } + if (j < line.size() && line[j] == '(') { + chain_dots.push_back(i); + } + } } else if (c == ' ' && i + 1 < line.size()) { // " op " の形の二項演算子: 演算子の直前で折り返す for (const char* op : kWrapOps) { @@ -287,6 +302,47 @@ std::string Formatter::wrap_long_lines(const std::string& code, size_t& changes) } } + // 文レベルのチェーン呼び出し(.method(...))が2つ以上ある行は、カンマ・演算子折り返しより優先して各チェーンの . 直前で折り返す(TS風。最後の呼び出しの引数リスト途中で折れるのを防ぐ) + { + std::vector dots; + for (size_t d : chain_dots) { + if (d < code_end) { + dots.push_back(d); + } + } + if (dots.size() >= 2) { + // 継続行は1段深く仮置きする(後段のインデント正規化の継続行+1段の規則と一致し冪等になる) + size_t cont_indent = content_start + static_cast(indent_width_); + std::vector out_lines; + size_t seg_start = 0; + for (size_t dot : dots) { + std::string seg = line.substr(seg_start, dot - seg_start); + while (!seg.empty() && seg.back() == ' ') + seg.pop_back(); + out_lines.push_back(seg_start == 0 ? seg : std::string(cont_indent, ' ') + seg); + seg_start = dot; + } + std::string tail = line.substr(seg_start, code_end - seg_start); + while (!tail.empty() && tail.back() == ' ') + tail.pop_back(); + if (comment_start != std::string::npos) { + tail += " " + line.substr(comment_start); + } + out_lines.push_back(std::string(cont_indent, ' ') + tail); + std::string joined; + for (size_t li = 0; li < out_lines.size(); ++li) { + if (li > 0) + joined += '\n'; + joined += out_lines[li]; + } + // 折り返し後もなお長いセグメント(長い引数リスト等)はカンマ折り返しへ委ねる。 + // 各セグメントのチェーン . は高々1つなので再帰でチェーン折り返しは再発しない + result << wrap_long_lines(joined, changes); + changes++; + continue; + } + } + // 折り返し位置の選択: より浅い括弧深さの候補を優先し、同深さならカンマを優先する(深い位置のカンマで呼び出しの途中を折るより、浅い演算子で折るほうが読みやすい) bool has_comma = false; bool has_op = false; diff --git a/tests/regression/cases/formatter/wrap/method_chain.expected.cm b/tests/regression/cases/formatter/wrap/method_chain.expected.cm new file mode 100644 index 00000000..cb696adb --- /dev/null +++ b/tests/regression/cases/formatter/wrap/method_chain.expected.cm @@ -0,0 +1,26 @@ +module wrap_chain; + +string template_text() { + return "expected '{0}' in {1} but found {2}"; +} + +string localized(string first_argument, string second_argument, string third_argument) { + return template_text() + .replace("{0}", first_argument) + .replace("{1}", second_argument) + .replace("{2}", third_argument); +} + +struct Reporter { + string error_message_text; + string template_source; + + void remember(string first_argument_text, string second_argument_text, string third_text) { + self.error_message_text = build_message( + self.template_source, + first_argument_text, + second_argument_text, + third_text + ); + } +} diff --git a/tests/regression/cases/formatter/wrap/method_chain.input b/tests/regression/cases/formatter/wrap/method_chain.input new file mode 100644 index 00000000..159c1795 --- /dev/null +++ b/tests/regression/cases/formatter/wrap/method_chain.input @@ -0,0 +1,18 @@ +module wrap_chain; + +string template_text() { + return "expected '{0}' in {1} but found {2}"; +} + +string localized(string first_argument, string second_argument, string third_argument) { + return template_text().replace("{0}", first_argument).replace("{1}", second_argument).replace("{2}", third_argument); +} + +struct Reporter { + string error_message_text; + string template_source; + + void remember(string first_argument_text, string second_argument_text, string third_text) { + self.error_message_text = build_message(self.template_source, first_argument_text, second_argument_text, third_text); + } +} diff --git a/tests/regression/formatter_test.cpp b/tests/regression/formatter_test.cpp index 0e05401f..e952b91a 100644 --- a/tests/regression/formatter_test.cpp +++ b/tests/regression/formatter_test.cpp @@ -74,6 +74,11 @@ TEST_F(FormatterIntegrationTest, WrapExplodesLongArgs) { expect_format_case("wrap/long_args"); } +// 文レベルのメソッドチェーン(.method(...) が2つ以上)はカンマ折り返しより優先して各チェーンの . 直前で折り返す(TS風)。フィールド参照の . しかない長い行は従来どおりカンマ折り返しになる +TEST_F(FormatterIntegrationTest, WrapBreaksMethodChain) { + expect_format_case("wrap/method_chain"); +} + // 1行に詰め込まれた文ブロック({ 文; 文; })はブロック展開する(演算子折り返しだと if ヘッダの途中で折れ、`; }` 継続行が次回実行のセミコロン改行で再分割されて冪等性が崩れるため) TEST_F(FormatterIntegrationTest, WrapExpandsInlineBlock) { expect_format_case("wrap/inline_block"); From 536a1baa5820eb4a9403b3e461f40617f2dfbeaa Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 18:37:48 +0900 Subject: [PATCH 16/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E4=BE=8B:=20msgf1?= =?UTF-8?q?/2/3=E3=82=92msgf(MsgId,=20string[])=E3=81=B8=E4=B8=80=E6=9C=AC?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引数数ごとのmsgf1/msgf2/msgf3は本質的に同じ置換処理の重複で、msgf3はチェーン式の折り返しも読みにくかった。 C++側i18n::msgfと同じ方式で、テンプレート中の{i}をargsの各要素で順に置換するmsgf(MsgId, string[]) 1本に統一し、診断ビルダーは配列リテラルで呼び出す形にした。 プレースホルダ番号はstd::strings::formatのto_radixで組み立てるため引数数の上限がない。 新フォーマッタ適用で100バイト超のテンプレート対訳配列は1要素1行へ展開した。cm test(diag/messages_test 4件)とscripts/ci/check_examples.shの通過を確認済み。 チュートリアル・VSCode拡張は例の内部整理のため対象外。 --- docs/releases/v0.17.2.md | 1 + examples/08_selfhost_parser/diag/messages.cm | 76 +++++++++++--------- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index b4b805e0..bd0ea500 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -255,6 +255,7 @@ return template_text() v0.17.2で整備した標準ライブラリの実証として、Cmで書いたCmソースの宣言レベルパーサを追加した。 成功なら定義一覧(module/import/use/typedef/const/struct/enum/impl/関数)を、失敗ならエラー箇所(行:桁・キャレット付きソース行)を出力し、失敗し得る解析関数はすべてOption型で失敗を伝播する。 段階別ディレクトリ(diag/lexer/parser/decl/samples)へ分割し、各モジュールに `#[test]` ディレクティブの単体テスト(9ファイル・60テスト)が付属する。トークン種別はenum、バイト値は文字キャストの定数で定義し、エラーメッセージはコンパイラ本体と同じi18n構成(enum MsgId×Langのテンプレート表・`{0}`プレースホルダ・CM_LANG切替)で診断モジュールへ分離している。 +プレースホルダ埋めは引数数別の関数を設けず、C++側 `i18n::msgf` と同じ方式の `msgf(MsgId, string[])` 1本が `{i}` をループ置換する。 パーサ自身の全ソースを自己解析でき、CIが単体テスト・正常/エラーサンプル・自己解析を検証する。 ## 🧪 テスト diff --git a/examples/08_selfhost_parser/diag/messages.cm b/examples/08_selfhost_parser/diag/messages.cm index af91f985..55adfbe3 100644 --- a/examples/08_selfhost_parser/diag/messages.cm +++ b/examples/08_selfhost_parser/diag/messages.cm @@ -5,6 +5,7 @@ module diag.messages; import std::env::*; +import std::strings::format::*; // メッセージID(診断の種類ごとに1つ) export enum MsgId { @@ -41,22 +42,37 @@ export Lang current_lang() { string[] templates_of(MsgId id) { match (id) { MsgId::ExpectedButFound => { - return ["expected '{0}' but found {1}", "'{0}' が必要ですが {1} が見つかりました"]; + return [ + "expected '{0}' but found {1}", + "'{0}' が必要ですが {1} が見つかりました" + ]; } MsgId::ExpectedThing => { - return ["expected {0} but found {1}", "{0} が必要ですが {1} が見つかりました"]; + return [ + "expected {0} but found {1}", + "{0} が必要ですが {1} が見つかりました" + ]; } MsgId::ExpectedIn => { - return ["expected '{0}' in {1} but found {2}", "{1} には '{0}' が必要ですが {2} が見つかりました"]; + return [ + "expected '{0}' in {1} but found {2}", + "{1} には '{0}' が必要ですが {2} が見つかりました" + ]; } MsgId::ExpectedBodyOrSemi => { - return ["expected '{' or ';' after {0} but found {1}", "{0} の後には '{' または ';' が必要ですが {1} が見つかりました"]; + return [ + "expected '{' or ';' after {0} but found {1}", + "{0} の後には '{' または ';' が必要ですが {1} が見つかりました" + ]; } MsgId::EofIn => { return ["unexpected end of file in {0}", "{0} の途中で入力が終端しました"]; } MsgId::EofLookingFor => { - return ["unexpected end of file while looking for '{0}'", "'{0}' を探索中に入力が終端しました"]; + return [ + "unexpected end of file while looking for '{0}'", + "'{0}' を探索中に入力が終端しました" + ]; } MsgId::Unterminated => { return ["unterminated {0}", "{0} が終端していません"]; @@ -71,7 +87,10 @@ string[] templates_of(MsgId id) { return ["syntax error: {0}", "構文エラー: {0}"]; } MsgId::FileNotFound => { - return ["error: file not found: {0}", "エラー: ファイルが見つかりません: {0}"]; + return [ + "error: file not found: {0}", + "エラー: ファイルが見つかりません: {0}" + ]; } } return [""]; @@ -87,77 +106,70 @@ string template_of(MsgId id, Lang lang) { return t[li]; } -// ---- フォーマッタ(プレースホルダをこの場で埋める。引数数ごとに提供) ---- +// ---- フォーマッタ(C++側 i18n::msgf と同じ方式で、引数数によらず1本で埋める) ---- -// 引数1つのメッセージを組み立てる -export string msgf1(MsgId id, string a0) { - return template_of(id, current_lang()).replace("{0}", a0); -} - -// 引数2つのメッセージを組み立てる -export string msgf2(MsgId id, string a0, string a1) { - return template_of(id, current_lang()).replace("{0}", a0).replace("{1}", a1); -} - -// 引数3つのメッセージを組み立てる -export string msgf3(MsgId id, string a0, string a1, string a2) { - return template_of(id, current_lang()).replace("{0}", a0).replace("{1}", a1).replace("{2}", - a2); +// テンプレート中の {0} {1} ... を args の各要素で順に置換してメッセージを組み立てる +export string msgf(MsgId id, string[] args) { + string text = template_of(id, current_lang()); + for (int i = 0; i < args.len(); i++) { + text = text.replace("{" + to_radix(i, 10) + "}", args[i]); + } + return text; } // ---- 診断ビルダー(呼び出し側の公開API。IDと引数の対応をここで固定する) ---- // 「expected '<期待字句>' but found <実際>」 export string msg_expected(string expected, string found) { - return msgf2(MsgId::ExpectedButFound, expected, found); + return msgf(MsgId::ExpectedButFound, [expected, found]); } // 「expected <期待物> but found <実際>」(字句でなく概念を期待する場合) export string msg_expected_thing(string what, string found) { - return msgf2(MsgId::ExpectedThing, what, found); + return msgf(MsgId::ExpectedThing, [what, found]); } // 「expected '<期待字句>' in <文脈> but found <実際>」 export string msg_expected_in(string expected, string context, string found) { - return msgf3(MsgId::ExpectedIn, expected, context, found); + return msgf(MsgId::ExpectedIn, [expected, context, found]); } // 「expected '{' or ';' after <対象> but found <実際>」(本体または宣言終端の欠落) export string msg_expected_body_or_semi(string after, string found) { - return msgf2(MsgId::ExpectedBodyOrSemi, after, found); + return msgf(MsgId::ExpectedBodyOrSemi, [after, found]); } // 「unexpected end of file in <文脈>」 export string msg_eof_in(string context) { - return msgf1(MsgId::EofIn, context); + return msgf(MsgId::EofIn, [context]); } // 「unexpected end of file while looking for '<区切り>'」 export string msg_eof_looking_for(string delimiter) { - return msgf1(MsgId::EofLookingFor, delimiter); + return msgf(MsgId::EofLookingFor, [delimiter]); } // 「unterminated <対象>」(未終端リテラル・コメント) export string msg_unterminated(string what) { - return msgf1(MsgId::Unterminated, what); + return msgf(MsgId::Unterminated, [what]); } // 「could not determine <対象>」(名前の特定失敗) export string msg_cannot_determine(string what) { - return msgf1(MsgId::CannotDetermine, what); + return msgf(MsgId::CannotDetermine, [what]); } // 「lex error: <詳細>」(エラー出力の見出し行) export string msg_lex_error(string detail) { - return msgf1(MsgId::LexErrorLabel, detail); + return msgf(MsgId::LexErrorLabel, [detail]); } // 「syntax error: <詳細>」(エラー出力の見出し行) export string msg_syntax_error(string detail) { - return msgf1(MsgId::SyntaxErrorLabel, detail); + return msgf(MsgId::SyntaxErrorLabel, [detail]); } // 「error: file not found: <パス>」 export string msg_file_not_found(string path) { - return msgf1(MsgId::FileNotFound, path); + return msgf(MsgId::FileNotFound, [path]); } From 39dd2eea3f670e7e46226c1e08b93e74ed4cb8be Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 18:50:56 +0900 Subject: [PATCH 17/25] =?UTF-8?q?=E3=82=BB=E3=83=AB=E3=83=95=E3=83=9B?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=91=E3=83=BC=E3=82=B5=E4=BE=8B:=20?= =?UTF-8?q?=E8=A8=BA=E6=96=AD=E3=83=93=E3=83=AB=E3=83=80=E3=83=BC=E5=BB=83?= =?UTF-8?q?=E6=AD=A2=E3=81=A8tests/=E3=83=95=E3=82=A9=E3=83=AB=E3=83=80?= =?UTF-8?q?=E9=9B=86=E7=B4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit msg_expected等の診断ビルダーはmsgfへの引数固定だけの薄い転送で、msgf(MsgId, string[])一本化後は不要になった。 ビルダーを全廃し、呼び出し側がmsgf(MsgId::ExpectedButFound, [expected, found])のようにIDと引数配列を直接渡す形に統一した(MsgIdのenumコメントがプレースホルダ対応を文書化する)。 また各モジュールの隣に並んでいた*_test.cmはフォルダを雑然とさせるため、tests/フォルダへ集約して相対importを張り替えた(対象モジュール名+_test.cmで命名)。 scripts/ci/check_examples.shはtests/配下のグロブ実行へ変更し、README・リリースノートを追従した。全60テスト・正常/エラーサンプル・自己解析の通過を確認済み。 チュートリアル・VSCode拡張は例の内部整理のため対象外。 --- docs/releases/v0.17.2.md | 4 +- examples/08_selfhost_parser/README.md | 14 +++-- examples/08_selfhost_parser/diag/messages.cm | 57 ------------------- .../08_selfhost_parser/diag/messages_test.cm | 50 ---------------- examples/08_selfhost_parser/lexer/scan.cm | 28 +++++---- examples/08_selfhost_parser/main.cm | 6 +- .../08_selfhost_parser/parser/decl/funcs.cm | 14 ++--- .../08_selfhost_parser/parser/decl/impls.cm | 10 ++-- .../08_selfhost_parser/parser/decl/modules.cm | 4 +- .../08_selfhost_parser/parser/decl/types.cm | 12 ++-- examples/08_selfhost_parser/parser/state.cm | 10 ++-- .../{parser/decl => tests}/dispatch_test.cm | 10 ++-- .../{parser/decl => tests}/funcs_test.cm | 19 ++++--- .../{parser/decl => tests}/impls_test.cm | 18 +++--- .../08_selfhost_parser/tests/messages_test.cm | 56 ++++++++++++++++++ .../{parser/decl => tests}/modules_test.cm | 12 ++-- .../{lexer => tests}/scan_test.cm | 6 +- .../{parser => tests}/state_test.cm | 6 +- .../{lexer => tests}/token_test.cm | 2 +- .../{parser/decl => tests}/types_test.cm | 16 +++--- scripts/ci/check_examples.sh | 6 +- 21 files changed, 159 insertions(+), 201 deletions(-) delete mode 100644 examples/08_selfhost_parser/diag/messages_test.cm rename examples/08_selfhost_parser/{parser/decl => tests}/dispatch_test.cm (95%) rename examples/08_selfhost_parser/{parser/decl => tests}/funcs_test.cm (87%) rename examples/08_selfhost_parser/{parser/decl => tests}/impls_test.cm (86%) create mode 100644 examples/08_selfhost_parser/tests/messages_test.cm rename examples/08_selfhost_parser/{parser/decl => tests}/modules_test.cm (89%) rename examples/08_selfhost_parser/{lexer => tests}/scan_test.cm (95%) rename examples/08_selfhost_parser/{parser => tests}/state_test.cm (92%) rename examples/08_selfhost_parser/{lexer => tests}/token_test.cm (97%) rename examples/08_selfhost_parser/{parser/decl => tests}/types_test.cm (90%) diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index bd0ea500..badbf462 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -254,8 +254,8 @@ return template_text() v0.17.2で整備した標準ライブラリの実証として、Cmで書いたCmソースの宣言レベルパーサを追加した。 成功なら定義一覧(module/import/use/typedef/const/struct/enum/impl/関数)を、失敗ならエラー箇所(行:桁・キャレット付きソース行)を出力し、失敗し得る解析関数はすべてOption型で失敗を伝播する。 -段階別ディレクトリ(diag/lexer/parser/decl/samples)へ分割し、各モジュールに `#[test]` ディレクティブの単体テスト(9ファイル・60テスト)が付属する。トークン種別はenum、バイト値は文字キャストの定数で定義し、エラーメッセージはコンパイラ本体と同じi18n構成(enum MsgId×Langのテンプレート表・`{0}`プレースホルダ・CM_LANG切替)で診断モジュールへ分離している。 -プレースホルダ埋めは引数数別の関数を設けず、C++側 `i18n::msgf` と同じ方式の `msgf(MsgId, string[])` 1本が `{i}` をループ置換する。 +段階別ディレクトリ(diag/lexer/parser/decl/samples)へ分割し、`#[test]` ディレクティブの単体テストはソースと混在しないよう `tests/` フォルダへ集約した。トークン種別はenum、バイト値は文字キャストの定数で定義し、エラーメッセージはコンパイラ本体と同じi18n構成(enum MsgId×Langのテンプレート表・`{0}`プレースホルダ・CM_LANG切替)で診断モジュールへ分離している。 +プレースホルダ埋めは引数数別の関数や診断ビルダーを設けず、C++側 `i18n::msgf` と同じ方式の `msgf(MsgId, string[])` 1本へ集約し、呼び出し側が `msgf(MsgId::ExpectedButFound, [expected, found])` のようにIDと引数配列を直接渡す。 パーサ自身の全ソースを自己解析でき、CIが単体テスト・正常/エラーサンプル・自己解析を検証する。 ## 🧪 テスト diff --git a/examples/08_selfhost_parser/README.md b/examples/08_selfhost_parser/README.md index 81b0147a..91303075 100644 --- a/examples/08_selfhost_parser/README.md +++ b/examples/08_selfhost_parser/README.md @@ -43,27 +43,29 @@ parser/ 構文解析 samples/ パーサへ入力するサンプル ok.cm 正常系(全宣言種別を含む) error.cm エラー系(メソッド本体欠落) +tests/ 単体テスト(#[test]。対象モジュール名 + _test.cm) ``` 関数本体・メソッド本体は波括弧の対応で読み飛ばす「定義リスタ」としての解析です(文字列リテラル内の波括弧は字句解析済みのため誤検出しません)。 ## 単体テスト -各ファイルに`#[test]`ディレクティブの単体テスト(`*_test.cm`)が付属します。 +`tests/`フォルダに`#[test]`ディレクティブの単体テスト(対象モジュール名 + `_test.cm`)を集約しています。 ```bash -cm test examples/08_selfhost_parser/lexer/scan_test.cm -cm test examples/08_selfhost_parser/parser/decl/types_test.cm -# ...(lexer/token / parser/state / parser/decl/{modules, impls, funcs, dispatch} も同様) +cm test examples/08_selfhost_parser/tests/scan_test.cm +cm test examples/08_selfhost_parser/tests/types_test.cm +# ...(messages / token / state / modules / impls / funcs / dispatch も同様) ``` -CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、9ファイル・60テストの単体実行・正常/エラーサンプル・自己解析を検証します。 -エラーメッセージの検証はテストも`diag/messages.cm`のビルダーを参照するため、文面変更に自動で追従します。 +CI(`scripts/ci/check_examples.sh`)は全`.cm`の型検査に加えて、`tests/`配下の単体実行・正常/エラーサンプル・自己解析を検証します。 +エラーメッセージの検証はテストも`diag/messages.cm`の`msgf`とテンプレート表を参照するため、文面変更に自動で追従します。 ## エラーメッセージのi18n Cmコンパイラ本体と同じ構成で、メッセージID(`enum MsgId`)×言語(`enum Lang`)の2次元テンプレート表に文面を集約しています。 テンプレートは`{0}`〜`{2}`のプレースホルダを持つ完結した1文で、語順が言語ごとに異なるため断片連結による文組み立ては行いません。 +プレースホルダ埋めはC++側`i18n::msgf`と同じ方式の`msgf(MsgId, string[])`1本で、呼び出し側が`msgf(MsgId::ExpectedButFound, [expected, found])`のようにIDと引数配列を直接渡します。 ```bash # 日本語診断(既定は英語) diff --git a/examples/08_selfhost_parser/diag/messages.cm b/examples/08_selfhost_parser/diag/messages.cm index 55adfbe3..018f6dd5 100644 --- a/examples/08_selfhost_parser/diag/messages.cm +++ b/examples/08_selfhost_parser/diag/messages.cm @@ -116,60 +116,3 @@ export string msgf(MsgId id, string[] args) { } return text; } - -// ---- 診断ビルダー(呼び出し側の公開API。IDと引数の対応をここで固定する) ---- - -// 「expected '<期待字句>' but found <実際>」 -export string msg_expected(string expected, string found) { - return msgf(MsgId::ExpectedButFound, [expected, found]); -} - -// 「expected <期待物> but found <実際>」(字句でなく概念を期待する場合) -export string msg_expected_thing(string what, string found) { - return msgf(MsgId::ExpectedThing, [what, found]); -} - -// 「expected '<期待字句>' in <文脈> but found <実際>」 -export string msg_expected_in(string expected, string context, string found) { - return msgf(MsgId::ExpectedIn, [expected, context, found]); -} - -// 「expected '{' or ';' after <対象> but found <実際>」(本体または宣言終端の欠落) -export string msg_expected_body_or_semi(string after, string found) { - return msgf(MsgId::ExpectedBodyOrSemi, [after, found]); -} - -// 「unexpected end of file in <文脈>」 -export string msg_eof_in(string context) { - return msgf(MsgId::EofIn, [context]); -} - -// 「unexpected end of file while looking for '<区切り>'」 -export string msg_eof_looking_for(string delimiter) { - return msgf(MsgId::EofLookingFor, [delimiter]); -} - -// 「unterminated <対象>」(未終端リテラル・コメント) -export string msg_unterminated(string what) { - return msgf(MsgId::Unterminated, [what]); -} - -// 「could not determine <対象>」(名前の特定失敗) -export string msg_cannot_determine(string what) { - return msgf(MsgId::CannotDetermine, [what]); -} - -// 「lex error: <詳細>」(エラー出力の見出し行) -export string msg_lex_error(string detail) { - return msgf(MsgId::LexErrorLabel, [detail]); -} - -// 「syntax error: <詳細>」(エラー出力の見出し行) -export string msg_syntax_error(string detail) { - return msgf(MsgId::SyntaxErrorLabel, [detail]); -} - -// 「error: file not found: <パス>」 -export string msg_file_not_found(string path) { - return msgf(MsgId::FileNotFound, [path]); -} diff --git a/examples/08_selfhost_parser/diag/messages_test.cm b/examples/08_selfhost_parser/diag/messages_test.cm deleted file mode 100644 index f5600d76..00000000 --- a/examples/08_selfhost_parser/diag/messages_test.cm +++ /dev/null @@ -1,50 +0,0 @@ -// diag/messages.cm の単体テスト(#[test] 関数を `cm test` が実行する) -// メッセージID×言語のテンプレート表とプレースホルダ埋め、CM_LANGによる言語切替を検証する -import std::debug::assert; -import std::env::*; -import ./messages::*; - -#[test] -void english_is_default() { - set("CM_LANG", "en"); - assert(msg_expected(";", "end of file") == "expected ';' but found end of file", - "en template with two placeholders"); - assert(msg_eof_in("import path") == "unexpected end of file in import path", - "en template with one placeholder"); - assert(msg_expected_in("{", "struct declaration", "';'") == - "expected '{' in struct declaration but found ';'", - "en template with three placeholders"); -} - -#[test] -void japanese_via_env() { - set("CM_LANG", "ja"); - assert(msg_expected(";", "end of file") == "';' が必要ですが end of file が見つかりました", - "ja template"); - assert(msg_unterminated("string literal") == "string literal が終端していません", - "ja unterminated"); - set("CM_LANG", "en"); -} - -#[test] -void placeholder_positions_differ_per_language() { - // 語順が違う言語でも断片連結でなくテンプレート置換なので正しく並ぶ - set("CM_LANG", "ja"); - const string ja = msg_expected_in("=", "const declaration", "';'"); - assert(ja == "const declaration には '=' が必要ですが ';' が見つかりました", - "ja word order puts context first"); - set("CM_LANG", "en"); - const string en = msg_expected_in("=", "const declaration", "';'"); - assert(en == "expected '=' in const declaration but found ';'", - "en word order puts expected first"); -} - -#[test] -void error_labels_are_localized() { - set("CM_LANG", "ja"); - assert(msg_syntax_error("x") == "構文エラー: x", "ja syntax label"); - assert(msg_lex_error("y") == "字句エラー: y", "ja lex label"); - set("CM_LANG", "en"); - assert(msg_syntax_error("x") == "syntax error: x", "en syntax label"); - assert(msg_file_not_found("a.cm") == "error: file not found: a.cm", "en file not found"); -} diff --git a/examples/08_selfhost_parser/lexer/scan.cm b/examples/08_selfhost_parser/lexer/scan.cm index 3cc81bca..22223c36 100644 --- a/examples/08_selfhost_parser/lexer/scan.cm +++ b/examples/08_selfhost_parser/lexer/scan.cm @@ -121,8 +121,9 @@ export impl Lexer { } private bool is_ident_start_b(int b) { - return (b >= BYTE_LOWER_A && b <= BYTE_LOWER_Z) || (b >= BYTE_UPPER_A && b <= BYTE_UPPER_Z) || - b == BYTE_UNDERSCORE; + return (b >= BYTE_LOWER_A && b <= BYTE_LOWER_Z) + || (b >= BYTE_UPPER_A && b <= BYTE_UPPER_Z) || + b == BYTE_UNDERSCORE; } private bool is_ident_cont_b(int b) { @@ -157,7 +158,7 @@ export impl Lexer { self.advance(); } if (!closed) { - self.err_msg = msg_unterminated("block comment"); + self.err_msg = msgf(MsgId::Unterminated, ["block comment"]); self.err_line = start_line; self.err_col = start_col; return Option::None; @@ -222,15 +223,20 @@ export impl Lexer { // 小数点(直後が数字のときのみ) self.advance(); } else if ((c == BYTE_PLUS || c == BYTE_MINUS) && self.pos > start && - (self.src.byte_at(self.pos - 1) == BYTE_LOWER_E || - self.src.byte_at(self.pos - 1) == BYTE_UPPER_E)) { + (self.src.byte_at(self.pos - 1) == BYTE_LOWER_E || + self.src.byte_at(self.pos - 1) == BYTE_UPPER_E)) { // 指数の符号(e/Eの直後のみ) self.advance(); } else { break; } } - self.tokens.push_token(TokenKind::Number, self.slice_text(start), tok_line, tok_col); + self.tokens.push_token( + TokenKind::Number, + self.slice_text(start), + tok_line, + tok_col + ); } else if (b == BYTE_DQUOTE) { // 文字列リテラル(エスケープ対応・未終端はエラー) self.advance(); @@ -251,13 +257,13 @@ export impl Lexer { } } if (!closed) { - self.err_msg = msg_unterminated("string literal"); + self.err_msg = msgf(MsgId::Unterminated, ["string literal"]); self.err_line = tok_line; self.err_col = tok_col; return Option::None; } self.tokens.push_token(TokenKind::StringLit, self.slice_text(start), tok_line, - tok_col); + tok_col); } else if (b == BYTE_SQUOTE) { // 文字リテラル(未終端はエラー) self.advance(); @@ -268,14 +274,14 @@ export impl Lexer { self.advance(); } if (self.peek_byte() != BYTE_SQUOTE) { - self.err_msg = msg_unterminated("char literal"); + self.err_msg = msgf(MsgId::Unterminated, ["char literal"]); self.err_line = tok_line; self.err_col = tok_col; return Option::None; } self.advance(); self.tokens.push_token(TokenKind::CharLit, self.slice_text(start), tok_line, - tok_col); + tok_col); } else if (b == BYTE_COLON && self.peek_byte2() == BYTE_COLON) { // "::" は1トークンに融合する(import経路・修飾名の解析用) self.advance(); @@ -285,7 +291,7 @@ export impl Lexer { // その他は1文字記号 self.advance(); self.tokens.push_token(TokenKind::Symbol, self.slice_text(start), tok_line, - tok_col); + tok_col); } } return Option::None; diff --git a/examples/08_selfhost_parser/main.cm b/examples/08_selfhost_parser/main.cm index 76faf21a..6f8aac3b 100644 --- a/examples/08_selfhost_parser/main.cm +++ b/examples/08_selfhost_parser/main.cm @@ -49,7 +49,7 @@ int main() { for (int ai = 1; ai < argv.len(); ai++) { const string path = argv[ai]; if (!exists(path)) { - println("{msg_file_not_found(path)}"); + println(msgf(MsgId::FileNotFound, [path])); failures = failures + 1; continue; } @@ -60,7 +60,7 @@ int main() { const Option lexed = lx.run(); if (lexed.is_none()) { println("== {path}"); - println("{msg_lex_error(lx.error_message())}"); + println(msgf(MsgId::LexErrorLabel, [lx.error_message()])); println(" --> {path}:{lx.error_line()}:{lx.error_col()}"); print_error_context(path, src, lx.error_line(), lx.error_col()); failures = failures + 1; @@ -72,7 +72,7 @@ int main() { const Option parsed = parse_program(&ps); if (parsed.is_none()) { println("== {path}"); - println("{msg_syntax_error(ps.error_message())}"); + println(msgf(MsgId::SyntaxErrorLabel, [ps.error_message()])); println(" --> {path}:{ps.error_line()}:{ps.error_col()}"); print_error_context(path, src, ps.error_line(), ps.error_col()); failures = failures + 1; diff --git a/examples/08_selfhost_parser/parser/decl/funcs.cm b/examples/08_selfhost_parser/parser/decl/funcs.cm index fa2ddce2..ae515dd3 100644 --- a/examples/08_selfhost_parser/parser/decl/funcs.cm +++ b/examples/08_selfhost_parser/parser/decl/funcs.cm @@ -27,7 +27,7 @@ export Option parse_const(Parser* p) { string last_ident = ""; while (!p->at_text("=")) { if (p->at_eof() || p->at_text(";")) { - return p->fail(msg_expected_in("=", "const declaration", p->found_text())); + return p->fail(msgf(MsgId::ExpectedIn, ["=", "const declaration", p->found_text()])); } if (p->at_ident()) { last_ident = p->text(); @@ -39,7 +39,7 @@ export Option parse_const(Parser* p) { return Option::None; } if (last_ident == "") { - return p->fail(msg_cannot_determine("const name")); + return p->fail(msgf(MsgId::CannotDetermine, ["const name"])); } p->record_const(last_ident); return Option::Some(true); @@ -57,7 +57,7 @@ export Option parse_func_or_var(Parser* p) { string name = ""; while (!p->at_text("(")) { if (p->at_eof()) { - return p->fail(msg_eof_in("declaration")); + return p->fail(msgf(MsgId::EofIn, ["declaration"])); } if (p->at_text(";") || p->at_text("=")) { // 関数でなくグローバル変数宣言だった @@ -66,13 +66,13 @@ export Option parse_func_or_var(Parser* p) { return Option::None; } if (name == "") { - return p->fail(msg_cannot_determine("declaration name")); + return p->fail(msgf(MsgId::CannotDetermine, ["declaration name"])); } p->record_const(name); return Option::Some(true); } if (p->at_text("{") || p->at_text("}")) { - return p->fail(msg_expected_thing("declaration", p->found_text())); + return p->fail(msgf(MsgId::ExpectedThing, ["declaration", p->found_text()])); } if (p->at_ident()) { if (name != "") { @@ -94,7 +94,7 @@ export Option parse_func_or_var(Parser* p) { p->bump(); } if (name == "") { - return p->fail(msg_cannot_determine("function name before '('")); + return p->fail(msgf(MsgId::CannotDetermine, ["function name before '('"])); } const Option params = p->skip_balanced("(", ")"); if (params.is_none()) { @@ -108,7 +108,7 @@ export Option parse_func_or_var(Parser* p) { } else if (p->at_text(";")) { p->bump(); } else { - return p->fail(msg_expected_body_or_semi("function signature", p->found_text())); + return p->fail(msgf(MsgId::ExpectedBodyOrSemi, ["function signature", p->found_text()])); } p->record_func(ret + " " + name + gp.unwrap() + "()"); return Option::Some(true); diff --git a/examples/08_selfhost_parser/parser/decl/impls.cm b/examples/08_selfhost_parser/parser/decl/impls.cm index ae1a4fe8..2ce68b72 100644 --- a/examples/08_selfhost_parser/parser/decl/impls.cm +++ b/examples/08_selfhost_parser/parser/decl/impls.cm @@ -29,7 +29,7 @@ export Option parse_method(Parser* p) { string name = ""; while (!p->at_text("(")) { if (p->at_eof() || p->at_text("{") || p->at_text("}")) { - return p->fail_str(msg_expected_thing("method signature", p->found_text())); + return p->fail_str(msgf(MsgId::ExpectedThing, ["method signature", p->found_text()])); } if (p->at_text("~")) { name = "~"; @@ -46,7 +46,7 @@ export Option parse_method(Parser* p) { p->bump(); } if (name == "") { - return p->fail_str(msg_cannot_determine("method name before '('")); + return p->fail_str(msgf(MsgId::CannotDetermine, ["method name before '('"])); } const Option params = p->skip_balanced("(", ")"); if (params.is_none()) { @@ -61,7 +61,7 @@ export Option parse_method(Parser* p) { } else if (p->at_text(";")) { p->bump(); } else { - return p->fail_str(msg_expected_body_or_semi("method signature", p->found_text())); + return p->fail_str(msgf(MsgId::ExpectedBodyOrSemi, ["method signature", p->found_text()])); } return Option::Some(name); } @@ -101,7 +101,7 @@ export Option parse_impl(Parser* p) { string[] methods = []; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail(msg_eof_in("impl body")); + return p->fail(msgf(MsgId::EofIn, ["impl body"])); } const Option m = parse_method(p); if (m.is_none()) { @@ -111,7 +111,7 @@ export Option parse_impl(Parser* p) { } p->bump(); p->record_impl("impl " + tname.unwrap() + targs.unwrap() + iface + " { " + - join_names(methods) + " }"); + join_names(methods) + " }"); return Option::Some(true); } diff --git a/examples/08_selfhost_parser/parser/decl/modules.cm b/examples/08_selfhost_parser/parser/decl/modules.cm index 2cf7619b..2f52c58e 100644 --- a/examples/08_selfhost_parser/parser/decl/modules.cm +++ b/examples/08_selfhost_parser/parser/decl/modules.cm @@ -11,7 +11,7 @@ export Option parse_path_to_semicolon(Parser* p) { string out = ""; while (!p->at_text(";")) { if (p->at_eof()) { - return p->fail_str(msg_eof_in("import path")); + return p->fail_str(msgf(MsgId::EofIn, ["import path"])); } if (p->at_text("{")) { // 選択import "{a, b}" は中身ごと表示形へ @@ -20,7 +20,7 @@ export Option parse_path_to_semicolon(Parser* p) { bool first = true; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail_str(msg_eof_in("import list")); + return p->fail_str(msgf(MsgId::EofIn, ["import list"])); } if (p->at_text(",")) { p->bump(); diff --git a/examples/08_selfhost_parser/parser/decl/types.cm b/examples/08_selfhost_parser/parser/decl/types.cm index d6be2291..6e31e8e7 100644 --- a/examples/08_selfhost_parser/parser/decl/types.cm +++ b/examples/08_selfhost_parser/parser/decl/types.cm @@ -21,7 +21,7 @@ export Option parse_struct(Parser* p, string prefix) { // "with Trait, ..." は"{"まで読み飛ばす while (!p->at_text("{")) { if (p->at_eof() || p->at_text(";") || p->at_text("}")) { - return p->fail(msg_expected_in("{", "struct declaration", p->found_text())); + return p->fail(msgf(MsgId::ExpectedIn, ["{", "struct declaration", p->found_text()])); } p->bump(); } @@ -30,7 +30,7 @@ export Option parse_struct(Parser* p, string prefix) { string[] fields = []; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail(msg_eof_in("struct body")); + return p->fail(msgf(MsgId::EofIn, ["struct body"])); } if (p->at_text("#")) { const Option a = p->skip_attribute(); @@ -70,7 +70,7 @@ export Option parse_struct(Parser* p, string prefix) { string last_ident = ""; while (!p->at_text(";")) { if (p->at_eof()) { - return p->fail(msg_eof_in("struct field")); + return p->fail(msgf(MsgId::EofIn, ["struct field"])); } if (p->at_ident()) { last_ident = p->text(); @@ -111,7 +111,7 @@ export Option parse_enum(Parser* p, string prefix) { string[] variants = []; while (!p->at_text("}")) { if (p->at_eof()) { - return p->fail(msg_eof_in("enum body")); + return p->fail(msgf(MsgId::EofIn, ["enum body"])); } if (p->at_text(",")) { p->bump(); @@ -126,7 +126,7 @@ export Option parse_enum(Parser* p, string prefix) { continue; } if (!p->at_ident()) { - return p->fail(msg_expected_thing("enum variant name", p->found_text())); + return p->fail(msgf(MsgId::ExpectedThing, ["enum variant name", p->found_text()])); } variants.push(p->text()); p->bump(); @@ -141,7 +141,7 @@ export Option parse_enum(Parser* p, string prefix) { p->bump(); while (!p->at_text(",") && !p->at_text("}")) { if (p->at_eof()) { - return p->fail(msg_eof_in("enum value")); + return p->fail(msgf(MsgId::EofIn, ["enum value"])); } p->bump(); } diff --git a/examples/08_selfhost_parser/parser/state.cm b/examples/08_selfhost_parser/parser/state.cm index af6ed4fd..573dcde3 100644 --- a/examples/08_selfhost_parser/parser/state.cm +++ b/examples/08_selfhost_parser/parser/state.cm @@ -197,7 +197,7 @@ export impl Parser { // 指定の字句を要求して読み進める(違えばNone) Option expect_text(string t) { if (!self.at_text(t)) { - return self.fail(msg_expected(t, self.found_text())); + return self.fail(msgf(MsgId::ExpectedButFound, [t, self.found_text()])); } self.bump(); return Option::Some(true); @@ -206,7 +206,7 @@ export impl Parser { // 識別子を要求して返す(予約語・記号ならNone) Option expect_ident() { if (self.kind() != TokenKind::Ident) { - return self.fail_str(msg_expected_thing("identifier", self.found_text())); + return self.fail_str(msgf(MsgId::ExpectedThing, ["identifier", self.found_text()])); } const string name = self.text(); self.bump(); @@ -222,7 +222,7 @@ export impl Parser { int depth = 1; while (depth > 0) { if (self.at_eof()) { - return self.fail(msg_eof_looking_for(close)); + return self.fail(msgf(MsgId::EofLookingFor, [close])); } if (self.at_text(open)) { depth = depth + 1; @@ -238,7 +238,7 @@ export impl Parser { Option skip_to_semicolon() { while (!self.at_text(";")) { if (self.at_eof()) { - return self.fail(msg_eof_looking_for(";")); + return self.fail(msgf(MsgId::EofLookingFor, [";"])); } if (self.at_text("{")) { const Option b = self.skip_balanced("{", "}"); @@ -263,7 +263,7 @@ export impl Parser { bool first = true; while (!self.at_text(">")) { if (self.at_eof()) { - return self.fail_str(msg_eof_in("generic parameter list")); + return self.fail_str(msgf(MsgId::EofIn, ["generic parameter list"])); } if (self.at_text(",")) { self.bump(); diff --git a/examples/08_selfhost_parser/parser/decl/dispatch_test.cm b/examples/08_selfhost_parser/tests/dispatch_test.cm similarity index 95% rename from examples/08_selfhost_parser/parser/decl/dispatch_test.cm rename to examples/08_selfhost_parser/tests/dispatch_test.cm index 1cdfa04c..563b5a20 100644 --- a/examples/08_selfhost_parser/parser/decl/dispatch_test.cm +++ b/examples/08_selfhost_parser/tests/dispatch_test.cm @@ -1,10 +1,10 @@ // parser/decl.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../../lexer/token::*; -import ../../lexer/scan::*; -import ../state::*; -import ./dispatch::*; -import ../../diag/messages::*; +import ../lexer/token::*; +import ../lexer/scan::*; +import ../parser/state::*; +import ../parser/decl/dispatch::*; +import ../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { diff --git a/examples/08_selfhost_parser/parser/decl/funcs_test.cm b/examples/08_selfhost_parser/tests/funcs_test.cm similarity index 87% rename from examples/08_selfhost_parser/parser/decl/funcs_test.cm rename to examples/08_selfhost_parser/tests/funcs_test.cm index a25a0ce8..5a801470 100644 --- a/examples/08_selfhost_parser/parser/decl/funcs_test.cm +++ b/examples/08_selfhost_parser/tests/funcs_test.cm @@ -1,10 +1,10 @@ // parser/funcs.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../../lexer/token::*; -import ../../lexer/scan::*; -import ../state::*; -import ./funcs::*; -import ../../diag/messages::*; +import ../lexer/token::*; +import ../lexer/scan::*; +import ../parser/state::*; +import ../parser/decl/funcs::*; +import ../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -52,8 +52,8 @@ void function_without_body_or_semicolon_is_error() { Parser p = parser_of("int broken()"); assert(parse_func_or_var(&p).is_none(), "missing body is an error"); assert(p.error_message() == - "expected '{' or ';' after function signature but found end of file", - "error message"); + "expected '{' or ';' after function signature but found end of file", + "error message"); } #[test] @@ -80,6 +80,7 @@ void global_var_without_const_is_recorded() { void func_error_uses_catalog_message() { Parser p = parser_of("int broken()"); assert(parse_func_or_var(&p).is_none(), "missing body fails"); - assert(p.error_message() == msg_expected_body_or_semi("function signature", "end of file"), - "catalog message"); + assert(p.error_message() + == msgf(MsgId::ExpectedBodyOrSemi, ["function signature", "end of file"]), + "catalog message"); } diff --git a/examples/08_selfhost_parser/parser/decl/impls_test.cm b/examples/08_selfhost_parser/tests/impls_test.cm similarity index 86% rename from examples/08_selfhost_parser/parser/decl/impls_test.cm rename to examples/08_selfhost_parser/tests/impls_test.cm index a8f34678..1d1e8e9f 100644 --- a/examples/08_selfhost_parser/parser/decl/impls_test.cm +++ b/examples/08_selfhost_parser/tests/impls_test.cm @@ -1,10 +1,10 @@ // parser/impls.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../../lexer/token::*; -import ../../lexer/scan::*; -import ../state::*; -import ./impls::*; -import ../../diag/messages::*; +import ../lexer/token::*; +import ../lexer/scan::*; +import ../parser/state::*; +import ../parser/decl/impls::*; +import ../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -21,7 +21,7 @@ void parses_impl_with_ctor_dtor_and_modifiers() { "impl Counter { self() { self.n = 0; } ~self() { } private int helper() { return 1; } static Counter zero() { Counter c(); return c; } }"); assert(parse_impl(&p).is_some(), "impl parses"); assert(p.impls_of()[0] == "impl Counter { self, ~self, helper, zero }", - "ctor/dtor/private/static methods collected"); + "ctor/dtor/private/static methods collected"); } #[test] @@ -43,7 +43,7 @@ void method_without_body_or_semicolon_is_error() { Parser p = parser_of("impl X { int broken() }"); assert(parse_impl(&p).is_none(), "missing body is an error"); assert(p.error_message() == "expected '{' or ';' after method signature but found '}'", - "error message"); + "error message"); } #[test] @@ -61,6 +61,6 @@ void overload_ctor_and_empty_impl() { void method_error_uses_catalog_message() { Parser p = parser_of("impl X { int broken() }"); assert(parse_impl(&p).is_none(), "missing body fails"); - assert(p.error_message() == msg_expected_body_or_semi("method signature", "'}'"), - "catalog message"); + assert(p.error_message() == msgf(MsgId::ExpectedBodyOrSemi, ["method signature", "'}'"]), + "catalog message"); } diff --git a/examples/08_selfhost_parser/tests/messages_test.cm b/examples/08_selfhost_parser/tests/messages_test.cm new file mode 100644 index 00000000..80696332 --- /dev/null +++ b/examples/08_selfhost_parser/tests/messages_test.cm @@ -0,0 +1,56 @@ +// diag/messages.cm の単体テスト(#[test] 関数を `cm test` が実行する) +// メッセージID×言語のテンプレート表とプレースホルダ埋め、CM_LANGによる言語切替を検証する +import std::debug::assert; +import std::env::*; +import ../diag/messages::*; + +#[test] +void english_is_default() { + set("CM_LANG", "en"); + assert(msgf(MsgId::ExpectedButFound, [";", "end of file"]) + == "expected ';' but found end of file", + "en template with two placeholders"); + assert(msgf(MsgId::EofIn, ["import path"]) == "unexpected end of file in import path", + "en template with one placeholder"); + assert(msgf(MsgId::ExpectedIn, ["{", "struct declaration", "';'"]) == + "expected '{' in struct declaration but found ';'", + "en template with three placeholders"); +} + +#[test] +void japanese_via_env() { + set("CM_LANG", "ja"); + assert(msgf(MsgId::ExpectedButFound, [";", "end of file"]) + == "';' が必要ですが end of file が見つかりました", + "ja template"); + assert(msgf(MsgId::Unterminated, ["string literal"]) + == "string literal が終端していません", + "ja unterminated"); + set("CM_LANG", "en"); +} + +#[test] +void placeholder_positions_differ_per_language() { + // 語順が違う言語でも断片連結でなくテンプレート置換なので正しく並ぶ + set("CM_LANG", "ja"); + const string ja = msgf(MsgId::ExpectedIn, ["=", "const declaration", "';'"]); + assert(ja == "const declaration には '=' が必要ですが ';' が見つかりました", + "ja word order puts context first"); + set("CM_LANG", "en"); + const string en = msgf(MsgId::ExpectedIn, ["=", "const declaration", "';'"]); + assert(en == "expected '=' in const declaration but found ';'", + "en word order puts expected first"); +} + +#[test] +void error_labels_are_localized() { + set("CM_LANG", "ja"); + assert(msgf(MsgId::SyntaxErrorLabel, ["x"]) == "構文エラー: x", "ja syntax label"); + assert(msgf(MsgId::LexErrorLabel, ["y"]) == "字句エラー: y", "ja lex label"); + set("CM_LANG", "en"); + assert(msgf(MsgId::SyntaxErrorLabel, ["x"]) == "syntax error: x", "en syntax label"); + assert( + msgf(MsgId::FileNotFound, ["a.cm"]) == "error: file not found: a.cm", + "en file not found" + ); +} diff --git a/examples/08_selfhost_parser/parser/decl/modules_test.cm b/examples/08_selfhost_parser/tests/modules_test.cm similarity index 89% rename from examples/08_selfhost_parser/parser/decl/modules_test.cm rename to examples/08_selfhost_parser/tests/modules_test.cm index eb48ac15..1f6b7104 100644 --- a/examples/08_selfhost_parser/parser/decl/modules_test.cm +++ b/examples/08_selfhost_parser/tests/modules_test.cm @@ -1,10 +1,10 @@ // parser/modules.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../../lexer/token::*; -import ../../lexer/scan::*; -import ../state::*; -import ./modules::*; -import ../../diag/messages::*; +import ../lexer/token::*; +import ../lexer/scan::*; +import ../parser/state::*; +import ../parser/decl/modules::*; +import ../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -56,5 +56,5 @@ void unterminated_import_is_error() { void import_error_uses_catalog_message() { Parser p = parser_of("import std::io"); assert(parse_import_decl(&p).is_none(), "missing semicolon fails"); - assert(p.error_message() == msg_eof_in("import path"), "catalog message"); + assert(p.error_message() == msgf(MsgId::EofIn, ["import path"]), "catalog message"); } diff --git a/examples/08_selfhost_parser/lexer/scan_test.cm b/examples/08_selfhost_parser/tests/scan_test.cm similarity index 95% rename from examples/08_selfhost_parser/lexer/scan_test.cm rename to examples/08_selfhost_parser/tests/scan_test.cm index 9ba2ed78..bf0049f1 100644 --- a/examples/08_selfhost_parser/lexer/scan_test.cm +++ b/examples/08_selfhost_parser/tests/scan_test.cm @@ -1,7 +1,7 @@ // lexer.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ./token::*; -import ./scan::*; +import ../lexer/token::*; +import ../lexer/scan::*; import ../diag/messages::*; // テスト用: ソースを字句解析してトークン列を返す(成功前提) @@ -99,5 +99,5 @@ void unterminated_message_comes_from_catalog() { Lexer lx("\"open"); const Option r = lx.run(); assert(r.is_none(), "unterminated string fails"); - assert(lx.error_message() == msg_unterminated("string literal"), "catalog message"); + assert(lx.error_message() == msgf(MsgId::Unterminated, ["string literal"]), "catalog message"); } diff --git a/examples/08_selfhost_parser/parser/state_test.cm b/examples/08_selfhost_parser/tests/state_test.cm similarity index 92% rename from examples/08_selfhost_parser/parser/state_test.cm rename to examples/08_selfhost_parser/tests/state_test.cm index a4aa60ca..e8a4809a 100644 --- a/examples/08_selfhost_parser/parser/state_test.cm +++ b/examples/08_selfhost_parser/tests/state_test.cm @@ -2,7 +2,7 @@ import std::debug::assert; import ../lexer/token::*; import ../lexer/scan::*; -import ./state::*; +import ../parser/state::*; import ../diag/messages::*; // テスト用: ソースから解析器を組み立てる @@ -19,7 +19,7 @@ void expect_text_consumes_or_fails() { Parser p = parser_of("( )"); assert(p.expect_text("(").is_some(), "matching text consumed"); assert(p.expect_text("]").is_none(), "mismatch returns None"); - assert(p.error_message() == msg_expected("]", "')'"), "catalog message"); + assert(p.error_message() == msgf(MsgId::ExpectedButFound, ["]", "')'"]), "catalog message"); assert(p.error_line() == 1, "error line"); assert(p.error_col() == 3, "error col points at the found token"); } @@ -84,5 +84,5 @@ void first_error_is_preserved() { void expect_at_eof_reports_eof() { Parser p = parser_of(""); assert(p.expect_text(";").is_none(), "expect at EOF fails"); - assert(p.error_message() == msg_expected(";", "end of file"), "EOF wording"); + assert(p.error_message() == msgf(MsgId::ExpectedButFound, [";", "end of file"]), "EOF wording"); } diff --git a/examples/08_selfhost_parser/lexer/token_test.cm b/examples/08_selfhost_parser/tests/token_test.cm similarity index 97% rename from examples/08_selfhost_parser/lexer/token_test.cm rename to examples/08_selfhost_parser/tests/token_test.cm index 234a55ea..ff32f160 100644 --- a/examples/08_selfhost_parser/lexer/token_test.cm +++ b/examples/08_selfhost_parser/tests/token_test.cm @@ -1,6 +1,6 @@ // token.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ./token::*; +import ../lexer/token::*; #[test] void push_and_read_back() { diff --git a/examples/08_selfhost_parser/parser/decl/types_test.cm b/examples/08_selfhost_parser/tests/types_test.cm similarity index 90% rename from examples/08_selfhost_parser/parser/decl/types_test.cm rename to examples/08_selfhost_parser/tests/types_test.cm index 5914a9eb..56e22f63 100644 --- a/examples/08_selfhost_parser/parser/decl/types_test.cm +++ b/examples/08_selfhost_parser/tests/types_test.cm @@ -1,10 +1,10 @@ // parser/types.cm の単体テスト(#[test] 関数を `cm test` が実行する) import std::debug::assert; -import ../../lexer/token::*; -import ../../lexer/scan::*; -import ../state::*; -import ./types::*; -import ../../diag/messages::*; +import ../lexer/token::*; +import ../lexer/scan::*; +import ../parser/state::*; +import ../parser/decl/types::*; +import ../diag/messages::*; // テスト用: ソースから解析器を組み立てる Parser parser_of(string src) { @@ -50,7 +50,7 @@ void struct_without_brace_is_error() { Parser p = parser_of("struct Broken;"); assert(parse_struct(&p, "").is_none(), "missing body is an error"); assert(p.error_message() == "expected '{' in struct declaration but found ';'", - "error message"); + "error message"); } #[test] @@ -73,6 +73,6 @@ void field_attribute_is_skipped() { void struct_error_uses_catalog_message() { Parser p = parser_of("struct Broken;"); assert(parse_struct(&p, "").is_none(), "missing body fails"); - assert(p.error_message() == msg_expected_in("{", "struct declaration", "';'"), - "catalog message"); + assert(p.error_message() == msgf(MsgId::ExpectedIn, ["{", "struct declaration", "';'"]), + "catalog message"); } diff --git a/scripts/ci/check_examples.sh b/scripts/ci/check_examples.sh index 42368d5e..f5b95875 100755 --- a/scripts/ci/check_examples.sh +++ b/scripts/ci/check_examples.sh @@ -26,10 +26,10 @@ cmp /tmp/drill_jit.bin /tmp/drill_native.bin echo "✅ selfhost drill passed (jit/native artifacts identical)" # セルフホストパーサ(examples/08_selfhost_parser)の検証: -# 各モジュールの#[test]単体テスト → 正常サンプルの定義一覧出力(rc=0) → エラーサンプルの位置報告(rc=1) → パーサ自身の全ソース自己解析 +# tests/配下の#[test]単体テスト → 正常サンプルの定義一覧出力(rc=0) → エラーサンプルの位置報告(rc=1) → パーサ自身の全ソース自己解析 SHP=examples/08_selfhost_parser -for t in diag/messages_test lexer/token_test lexer/scan_test parser/state_test parser/decl/modules_test parser/decl/types_test parser/decl/impls_test parser/decl/funcs_test parser/decl/dispatch_test; do - ./cm test "$SHP/$t.cm" >/dev/null +for t in "$SHP"/tests/*_test.cm; do + ./cm test "$t" >/dev/null done ./cm run "$SHP/main.cm" -- "$SHP/samples/ok.cm" >/dev/null if ./cm run "$SHP/main.cm" -- "$SHP/samples/error.cm" >/dev/null 2>&1; then From 04e27676b7bcb0eedbfa8ad0a60750e90818493a Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 18:51:06 +0900 Subject: [PATCH 18/25] =?UTF-8?q?native::io::file=E3=81=A8write=5Fperf?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AEopen=E3=83=95=E3=83=A9?= =?UTF-8?q?=E3=82=B0=E3=82=92Linux=E5=AF=BE=E5=BF=9C=E3=81=AB=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O_WRONLY|O_CREAT|O_TRUNCとしてmacOS/BSD固有値0x0601を直書きしており、LinuxではO_CREAT(0x0040)が立たずopenが失敗していた(CIのubuntuでcommon/stdlib/fs/write_perfがjit/nativeともにOutput mismatch)。 libs/native/io/file.cmのwrite_fileは#ifdef __linux__でLinux値0x0241へ分岐し、write_perfテストはwrite_fileで空ファイルを先に作成して両OS同値のO_WRONLY(=1)だけでopenする移植可能な形へ修正した。 jit/nativeの実行と期待値一致をローカル(macOS)で確認済み。バグ修正のためチュートリアル・VSCode拡張は対象外。 --- docs/releases/v0.17.2.md | 5 +++++ libs/native/io/file.cm | 15 +++++++++------ tests/common/stdlib/fs/write_perf.cm | 6 ++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index badbf462..7c00a58b 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -209,6 +209,11 @@ DCEの収集を型ノードの再帰走査(ポインタ・配列・型引数 ## 🐛 標準ライブラリのバグ修正 +### native::io::file の open フラグがmacOS固有値だった + +`write_file` が `O_WRONLY|O_CREAT|O_TRUNC` としてmacOS/BSDの値 `0x0601` を直書きしており、Linuxでは `O_CREAT`(Linuxでは `0x0040`)が立たず新規ファイルへの書き込みが失敗していた。 +`#ifdef __linux__` でLinux値 `0x0241` に分岐するようにした。同じ値を使っていた `fs/write_perf` テスト(CIのubuntuでjit/nativeともにOutput mismatch)は、`std::fs::write_file` で空ファイルを先に作成し両OSで同値の `O_WRONLY(=1)` だけでopenする移植可能な形へ修正した。 + ### HashSetの非決定的クラッシュ(二重解放) `HashSet` を使うプログラムが実行ごとに完走・SIGABRTが揺れる非決定的クラッシュを起こした([記録](../archive/v0.17.2/bugfix-hashset-double-free.html))。 diff --git a/libs/native/io/file.cm b/libs/native/io/file.cm index 41d2e0bf..1126e119 100644 --- a/libs/native/io/file.cm +++ b/libs/native/io/file.cm @@ -23,9 +23,9 @@ use libc { // ============================================================ // O_RDONLY = 0 // O_WRONLY = 1 -// O_CREAT = 0x0200 -// O_TRUNC = 0x0400 -// O_WRONLY | O_CREAT | O_TRUNC = 0x0601 +// O_CREAT / O_TRUNC はOSごとに値が異なる: +// macOS/BSD: O_CREAT=0x0200, O_TRUNC=0x0400 → O_WRONLY|O_CREAT|O_TRUNC = 0x0601 +// Linux: O_CREAT=0x0040, O_TRUNC=0x0200 → O_WRONLY|O_CREAT|O_TRUNC = 0x0241 // SEEK_SET = 0 // SEEK_END = 2 // DEFAULT_MODE = 0x1A4 (0644 octal) @@ -73,9 +73,12 @@ export string read_file(string path) { /// ファイルに書き込み(簡易版、成功時true) export bool write_file(string path, string content) { - // O_WRONLY | O_CREAT | O_TRUNC = 0x0601 - // DEFAULT_MODE = 0x1A4 (rw-r--r--) - const int fd = open(path, 0x0601, 0x1A4); + // O_WRONLY | O_CREAT | O_TRUNC(OS別の値。DEFAULT_MODE = 0x1A4 = rw-r--r--) + #ifdef __linux__ + const int fd = open(path, 0x0241, 0x1A4); + #else + const int fd = open(path, 0x0601, 0x1A4); + #end if (fd < 0) { return false; } diff --git a/tests/common/stdlib/fs/write_perf.cm b/tests/common/stdlib/fs/write_perf.cm index 7aa5c75c..bce11d9e 100644 --- a/tests/common/stdlib/fs/write_perf.cm +++ b/tests/common/stdlib/fs/write_perf.cm @@ -26,8 +26,10 @@ int main() { println("sb lines={ls.len()} first={ls[0]} last={ls[n - 1]}"); // 2) BufWriterで行単位追記(バッファリング書き込みの経路) - // O_WRONLY|O_CREAT|O_TRUNC = 0x0601, mode 0644 - const int fd = open(path2, 1537, 420); + // O_CREAT/O_TRUNCの値はOSごとに異なる(macOS: 0x200/0x400, Linux: 0x40/0x200)ため、 + // ファイルはwrite_fileで空作成してから両OSで同値のO_WRONLY(=1)だけでopenする + write_file(path2, ""); + const int fd = open(path2, 1, 0); BufWriter w = BufWriter::from_fd(fd); utiny[] line = []; const string text = "buffered line payload\n"; From 7c7e56b306322ef199ddd00aee38941465cb7fd6 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 18:51:14 +0900 Subject: [PATCH 19/25] =?UTF-8?q?CI:=20=E3=82=A4=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=83=97=E3=83=AA=E3=82=BF=E5=AE=9F=E8=A1=8C=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=96=E3=82=92=E7=B5=B1=E5=90=88=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?matrix=E3=81=8B=E3=82=89=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interpreterスイート(MIRインタプリタ・O3)はjit系ジョブと検証範囲が重複するため、CIのジョブ数削減として統合テストmatrixから外した。 ローカルのmake testには引き続き含まれ、必要になれば1行の復元で戻せる。 CI構成変更のためテスト・チュートリアル・VSCode拡張は対象外。 --- .github/workflows/ci.yml | 3 +-- docs/releases/v0.17.2.md | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cd0aa9b..9cd21ae6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -308,8 +308,7 @@ jobs: - { id: js-o3, name: "JSコード生成+実行 O3", target: tjp3, backend: js, needs_node: true } - { id: sv-o0, name: "SV生成テスト O0", target: tsvp0, backend: sv, needs_node: false } - { id: sv-o3, name: "SV生成テスト O3", target: tsvp3, backend: sv, needs_node: false } - # make test(ローカル既定)に含まれるinterpreterスイートをCIでも検証する - - { id: interp-o3, name: "インタプリタ実行 O3", target: tip3, backend: interpreter, needs_node: false } + # interpreterスイート(MIRインタプリタ)はCIでは実行しない(jit系と検証範囲が重複するためジョブを削減。ローカルのmake testには残置) runs-on: ${{ matrix.os }} timeout-minutes: 45 diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index 7c00a58b..33ecac03 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -248,6 +248,11 @@ return template_text() フィールド参照の `.` や数値リテラルの小数点、`if (...)` ヘッダなど括弧内のチェーンは対象にせず、従来の折り返しを維持する。 +### CIからインタプリタ実行ジョブを削除 + +統合テストのmatrixにあったinterpreterスイート(MIRインタプリタ・O3)はjit系ジョブと検証範囲が重複するため、CIジョブから外した。 +ローカルの `make test` には引き続き含まれる。 + ### makeターゲット名の統一(install-vscode-extension) 操作系ターゲットは動詞先頭(`install` / `test-*` / `update-docs-version`)、成果物ターゲットは名詞(`release` / `dist` / `vscode-extension`)という命名規約に対し、`vscode-extension-install` だけが操作なのに名詞先頭だった。 From 08a3570b8e478da9ddbd76bb9a5e34dc422f1ea6 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 19:23:33 +0900 Subject: [PATCH 20/25] =?UTF-8?q?=E8=A8=AD=E8=A8=88=E3=83=89=E3=82=AD?= =?UTF-8?q?=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88:=20C++=E3=83=BBRust?= =?UTF-8?q?=E3=81=A8=E3=81=AE=E6=AF=94=E8=BC=83=EF=BC=88=E6=9C=89=E5=8A=B9?= =?UTF-8?q?=E3=81=AA=E9=83=A8=E5=88=86=E3=81=A8=E3=83=87=E3=83=A1=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=83=88=EF=BC=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cmの採用判断・設計判断の材料として、C++・Rustに対する現状評価をliving documentとして整理した。 マルチバックエンド出力(特にSV/HDL)とツールチェーン一体型を強みに、メモリ安全性の静的保証の不在・ジェネリクスの表現力制約・エコシステム不在をデメリットとして、リポジトリ内の記録(二重解放バグ・StringMap設計経緯・ロードマップ)へのリンク付きで記載した。 docs/design/index.mdのliving documents表へ掲載。ドキュメント追加のためテスト・チュートリアル・VSCode拡張・リリースノートは対象外。 --- docs/design/index.md | 1 + docs/design/language-comparison.md | 86 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 docs/design/language-comparison.md diff --git a/docs/design/index.md b/docs/design/index.md index dcbfa6fb..fb773e9f 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -10,6 +10,7 @@ Cm言語コンパイラの設計文書の一覧。実装が完了した設計文 | [文法定義(cm_grammar)](cm_grammar.html) | BNFベースの文法定義 | | [バックエンド対応マトリクス](backend_support_matrix.html) | 構文・機能×バックエンドの対応可否とテスト方針 | | [エラー処理方針](error_handling_policy.html) | 例外/Result/診断APIの使い分け方針 | +| [C++・Rustとの比較](language-comparison.html) | Cmの有効な部分とデメリットの現状評価(採用・設計判断の材料) | ## ロードマップ diff --git a/docs/design/language-comparison.md b/docs/design/language-comparison.md new file mode 100644 index 00000000..c34115e2 --- /dev/null +++ b/docs/design/language-comparison.md @@ -0,0 +1,86 @@ +# C++・Rustとの比較(Cmの有効な部分とデメリット) + +Cmを既存のシステムプログラミング言語(C++・Rust)と比較し、現時点(v0.17.x)での強みと弱みを整理する。 +本書は採用判断・設計判断の材料とする現状評価であり、実装の進行に応じて見直す(正式な言語仕様は [CANONICAL_SPEC](CANONICAL_SPEC.html) を参照)。 + +## サマリ + +| 観点 | C++ | Rust | Cm | +|---|---|---|---| +| 構文系統 | C系 | ML影響のC系 | C系(C++スタイル宣言・fnキーワードなし) | +| メモリ管理 | 手動+RAII+スマートポインタ | 所有権+借用検査(静的保証) | 手動+RAII(dtor)+スマートポインタ(規約ベース、静的保証なし) | +| エラー処理 | 例外+エラーコード混在 | Result/Option+panic | Result/Option(例外なし)+診断は状態保持の規約 | +| 継承 | 多重継承あり | なし(trait) | なし(構造体合成+interface、設計方針として不採用) | +| ジェネリクス | テンプレート(チューリング完全) | トレイト境界付きジェネリクス | 単相化ジェネリクス(境界なし・制約あり) | +| 出力ターゲット | ネイティブ中心(コンパイラによる) | ネイティブ+WASM | JIT/ネイティブ/WASM/JS・TS/SystemVerilog/UEFI/ベアメタル | +| ツールチェーン | 外部ツール分立(cmake等) | cargo一体型 | cmバイナリ一体型(run/compile/check/fmt/lint/test) | +| エコシステム | 巨大(数十年の資産) | 大(crates.io) | なし(パッケージマネージャ未整備・標準ライブラリ整備中) | +| 成熟度 | 標準化済(ISO) | 安定版(後方互換保証) | 開発中(仕様凍結はv1.0.0予定) | + +## Cmの有効な部分 + +### 1. 単一言語からのマルチバックエンド出力(最大の差別化点) + +同じソースからJIT実行・ネイティブAOT・WASM・JS/TSコード生成・SystemVerilog(HDL)・UEFI・ベアメタルを出力できる([バックエンド対応マトリクス](backend_support_matrix.html))。 +特にソフトウェア記述とハードウェア記述(SV生成・`#[input]`等の合成向け属性・物理制約生成)を1言語で覆う点はC++にもRustにもない。 +CPU実装(本リポジトリを含むCmCPU)のように「シミュレーション・合成・ホストツール」を同一言語で書ける。 + +### 2. ツールチェーン一体型の開発体験 + +`cm run / compile / check / fmt / lint / test` がコンパイラ本体に内蔵され、cargoに近い体験をC系構文で得られる。 +`#[test]` ディレクティブによるテストがビルド設定なしで実行でき、examplesの単体テストのように伴走テストを低コストで持てる。 +診断メッセージはenum MsgId×Langのテンプレート表でi18n化されており、`CM_LANG=ja` で日本語診断が出る(英語フォールバック付き)。 + +### 3. C++に対する簡潔さ・安全側の既定値 + +- ヘッダファイルとリンク順の問題がなく、モジュールと相対import(`import ./foo::*` / `../bar::*`)で依存を書く。 +- 例外がなく、失敗し得る操作は `Option/Result` で型に現れる(nullポインタ参照をAPI設計で避けられる)。 +- 継承を持たないため、脆弱基底クラス問題・スライシング・仮想デストラクタ忘れが構造的に存在しない(多態はinterfaceの動的ディスパッチ)。 +- 数値の暗黙縮小変換は警告(`--strict` でエラー)になり、C++の暗黙変換事故を既定で検出する。 +- 文字列補間 `"{expr}"`・スライス `T[]`・`match` 式・ペイロード付きenum(ADT)など、C++が近年ようやく取り込んだ機能が最初から言語コアにある。 + +### 4. Rustに対する学習コストとFFIの軽さ + +- 借用検査・ライフタイム注釈がなく、C/C++経験者がほぼそのまま読める(本リポジトリのセルフホストパーサ例のように、Cmコンパイラの構文をCm自身で解析する程度のプログラムが素直に書ける)。 +- `use libc { int open(string path, int flags, int mode); }` の宣言だけでlibc関数を直接呼べ、bindgen相当の前処理がいらない。 +- `#ifdef __linux__ / __macos__` 等の定義済みマクロによる条件コンパイルが言語内蔵で、cfg属性より単純に書ける。 +- 言語仕様が小さいため全体像の把握が容易で、コンパイラ自体のビルド・改造の敷居も低い。 + +## Cmのデメリット + +### 1. メモリ安全性の静的保証がない(対Rust最大の差) + +move/cloneの所有規律は規約ベースで、コンパイラは違反を検出しない。 +実際に「dtor持ち構造体の暗黙コピーによる二重解放(非決定的SIGABRT)」が標準ライブラリ開発で発生しており([記録](../archive/v0.17.2/bugfix-hashset-double-free.html))、Rustならコンパイルエラーになるクラスのバグが実行時まで潜伏する。 +生ポインタ(`void*`・`as` キャスト)が通常のコードに現れ、境界検査のない領域が残る。 +データ競合の防止機構(RustのSend/Sync相当)はなく、スレッド安全性は利用者の責任になる。 + +### 2. ジェネリクス・抽象化の表現力の制約 + +トレイト境界に相当する制約記述がなく、単相化時の型エラーとして顕在化する。 +フリー関数のオーバーロードや総称関数内での型分岐ができず、`HashMap` の内容ハッシュ化を型分岐で書けずに `StringMap` を別型として追加した経緯がある([記録](../archive/v0.17.2/stdlib-hardening.html))。 +C++テンプレートのメタプログラミングやRustのマクロ・トレイト特殊化に相当する機構はない(deriveは定型実装の自動生成に限る)。 + +### 3. エコシステムと実績の不在 + +パッケージマネージャ・中央レジストリがなく、外部ライブラリはFFI(libc等)経由になる。 +標準ライブラリは整備中で、コレクション・文字列・fs・processは揃いつつあるが、非同期I/O・ネットワーク・日時などは未整備。 +IDE支援はVSCode拡張(正規表現ベースのLSP)のみで、デバッガ・プロファイラの統合はない。 +本番投入実績・コミュニティ・求人市場は存在せず、学習資料はリポジトリ内のチュートリアルに限られる。 + +### 4. 言語・処理系の成熟度 + +仕様凍結はv1.0.0予定で、現在も破壊的でない範囲の仕様追加が続いている([ロードマップ](roadmap_v1.0.0.html))。 +標準ライブラリのセルフホスト実装が最初の大規模利用者となり、ジェネリック特殊化・DCE・バックエンド差などの誤コンパイルバグが継続的に発見・修正されている段階にある。 +バックエンド間の意味論一致は対応マトリクスとテストスイートで管理しているが、全バックエンドで同じ結果を保証する段階には達していない。 +最適化はLLVM基盤(O0〜O3)に乗るためコード品質の下地はあるが、C++/Rustの成熟したチューニング実績には及ばない。 + +## 使い分けの目安 + +| 状況 | 推奨 | +|---|---| +| ソフトとHDL(SV)を同一言語で書きたい・CmCPUのような自作CPU/合成プロジェクト | Cm | +| 1つのツールをJIT/ネイティブ/WASM/JS/UEFIへ同時展開したい実験・教育用途 | Cm | +| メモリ安全性・並行安全性を静的に保証したい本番システム | Rust | +| 既存資産・外部ライブラリ・人材が必要な大規模開発 | C++(またはRust) | +| 言語処理系を学びながら言語自体へ貢献したい | Cm(コンパイラが小さく5ステップ開発フローが整備済み) | From dc91cd187d279a5ff60af7dec0e3a452b72fe809 Mon Sep 17 00:00:00 2001 From: shadowlink0122 Date: Sat, 15 Aug 2026 19:42:16 +0900 Subject: [PATCH 21/25] =?UTF-8?q?dtor=E6=8C=81=E3=81=A1=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E4=BD=93=E3=81=AE=E6=9A=97=E9=BB=99=E3=82=B3=E3=83=94=E3=83=BC?= =?UTF-8?q?=E8=A8=BA=E6=96=AD=E3=82=92=E8=BF=BD=E5=8A=A0=EF=BC=88=E4=BA=8C?= =?UTF-8?q?=E9=87=8D=E8=A7=A3=E6=94=BE=E3=83=8F=E3=82=B6=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=81=AE=E8=A8=80=E8=AA=9E=E5=81=B4=E6=81=92=E4=B9=85=E5=AF=BE?= =?UTF-8?q?=E7=AD=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 構造体代入は浅いコピーのため、デストラクタ持ち構造体を値でコピーすると両方のインスタンスでdtorが走り二重解放になる(HashSetコンストラクタで実際に発生した非決定的SIGABRTの真因。bugfix-hashset-double-free.mdの設計課題)。 受理サイト(let初期化・代入・return・構造体リテラルのフィールド初期化)でmoveなしの場所式コピーを検出するcheck_dtor_copy_policyを追加した(通常は警告、--strictではエラー。Z5縮小変換と同じ段階導入方式)。 dtor持ち型はimpl登録時にベース名を記録し、typedef解決・ジェネリック特殊化サフィックス・名前空間修飾を剥いで判定する。一時値(コンストラクタ・メソッド戻り値)とdtor無し構造体は対象外。 move代入(self.f = move x)は既存のmove機構(HIRのis_moved_from→MIRのdtor登録解除)で機能することを実験で確認し、HashSetコンストラクタのポインタ手動無効化の回避策をself.map = move mへ置き換えた。 回帰テスト(dtor_copy_diag: 4サイト警告・move/一時値/dtor無しの非警告)と統合テスト(destructor/move_assign: dtorが移動先の1回だけ走る)を追加し、libs/tests/examples全ツリーで新規警告ゼロを確認した。 チュートリアル(ja/en ownership)・リリースノート・言語比較ドキュメントを更新し、設計文書はarchiveへ収録、バグ記録のフォローアップ節を実装済みへ更新した。 VSCode拡張は新構文なし(moveは既存キーワード)のため対象外。 --- CMakeLists.txt | 18 +++++ .../v0.17.2/bugfix-hashset-double-free.md | 5 +- docs/archive/v0.17.2/dtor-copy-diagnostic.md | 48 ++++++++++++ docs/design/index.md | 1 + docs/design/language-comparison.md | 9 ++- docs/releases/v0.17.2.md | 15 ++++ docs/tutorials/en/types/ownership.md | 47 +++++++++++ docs/tutorials/ja/types/ownership.md | 47 +++++++++++ libs/std/collections/hashset.cm | 5 +- src/internal/base/messages/ids.hpp | 1 + src/internal/base/messages/messages.cpp | 3 + src/internal/types/checking/checker.hpp | 9 +++ src/internal/types/checking/decl/impl.cpp | 5 ++ src/internal/types/checking/expr/operator.cpp | 5 ++ src/internal/types/checking/expr/primary.cpp | 2 + src/internal/types/checking/stmt.cpp | 4 + .../types/checking/utils/ownership.cpp | 78 +++++++++++++++++++ tests/common/basic/destructor/move_assign.cm | 35 +++++++++ .../basic/destructor/move_assign.expect | 4 + .../cases/dtor_copy_diag/assign_copy.cm | 21 +++++ .../cases/dtor_copy_diag/field_init_copy.cm | 20 +++++ .../cases/dtor_copy_diag/let_copy.cm | 16 ++++ .../cases/dtor_copy_diag/move_ok.cm | 29 +++++++ .../cases/dtor_copy_diag/no_dtor_ok.cm | 17 ++++ .../cases/dtor_copy_diag/return_copy.cm | 20 +++++ tests/regression/dtor_copy_diag_test.cpp | 77 ++++++++++++++++++ 26 files changed, 532 insertions(+), 9 deletions(-) create mode 100644 docs/archive/v0.17.2/dtor-copy-diagnostic.md create mode 100644 src/internal/types/checking/utils/ownership.cpp create mode 100644 tests/common/basic/destructor/move_assign.cm create mode 100644 tests/common/basic/destructor/move_assign.expect create mode 100644 tests/regression/cases/dtor_copy_diag/assign_copy.cm create mode 100644 tests/regression/cases/dtor_copy_diag/field_init_copy.cm create mode 100644 tests/regression/cases/dtor_copy_diag/let_copy.cm create mode 100644 tests/regression/cases/dtor_copy_diag/move_ok.cm create mode 100644 tests/regression/cases/dtor_copy_diag/no_dtor_ok.cm create mode 100644 tests/regression/cases/dtor_copy_diag/return_copy.cm create mode 100644 tests/regression/dtor_copy_diag_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d3edf519..a816804f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,6 +271,7 @@ set(CM_TYPECHECK_SOURCES src/internal/types/checking/auto_impl.cpp src/internal/types/checking/utils/compat.cpp src/internal/types/checking/utils/conversion.cpp + src/internal/types/checking/utils/ownership.cpp src/internal/types/checking/utils/interp.cpp src/internal/types/checking/utils/diagnostics.cpp src/internal/types/checking/utils/naming.cpp @@ -845,6 +846,23 @@ if(BUILD_TESTING) target_link_libraries(narrowing_diag_test gtest_main cm_frontend) gtest_discover_tests(narrowing_diag_test PROPERTIES LABELS "regression") + # Integration tests - dtor持ち構造体の暗黙コピー診断(二重解放ハザードの言語側対策) + # (Cmプログラムを tests/regression/cases/dtor_copy_diag/ から読み込む) + add_executable(dtor_copy_diag_test + tests/regression/dtor_copy_diag_test.cpp + ${CM_LEXER_SOURCES} + ${CM_AST_SOURCES} + ${CM_PARSER_SOURCES} + ${CM_TYPECHECK_SOURCES} + ${CM_I18N_SOURCES} + ) + set_target_properties(dtor_copy_diag_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TEST_RUNTIME_OUTPUT_DIRECTORY}) + target_compile_definitions(dtor_copy_diag_test PRIVATE + CM_DTOR_COPY_DIAG_CASE_DIR="${CMAKE_SOURCE_DIR}/tests/regression/cases/dtor_copy_diag") + target_include_directories(dtor_copy_diag_test BEFORE PRIVATE ${GTEST_INCLUDE_DIR}) + target_link_libraries(dtor_copy_diag_test gtest_main cm_frontend) + gtest_discover_tests(dtor_copy_diag_test PROPERTIES LABELS "regression") + # Integration tests - MIR Lowering # (Cmプログラムを tests/regression/cases/mir_lowering/ から読み込む) add_executable(mir_lowering_test diff --git a/docs/archive/v0.17.2/bugfix-hashset-double-free.md b/docs/archive/v0.17.2/bugfix-hashset-double-free.md index 24ffbc5f..6b303206 100644 --- a/docs/archive/v0.17.2/bugfix-hashset-double-free.md +++ b/docs/archive/v0.17.2/bugfix-hashset-double-free.md @@ -34,10 +34,11 @@ self() { 同型のラッパーである `TreeSet`(`TreeMap` を内包)は、`TreeMap` がスライス(ランタイム管理)ベースでデストラクタを持たないため影響しない。 `libs/std` 全体を「ローカル構築→フィールド代入」パターンで横断確認し、dtor持ち構造体の該当箇所は `HashSet` のみだった。 -## 言語側のフォローアップ(未実装・設計課題) +## 言語側のフォローアップ(v0.17.2で実装済み) この事故はライブラリ利用者が同じパターン(dtor持ち構造体を値としてコピー・フィールドへ代入)を書けば誰でも踏む。 -`selfhosting-stdlib.md` の言語側前提課題に挙げた「dtor持ち構造体の暗黙コピーへの警告」および「move代入(`self.map = move m;`)のサポート」が恒久対策であり、v0.17.2ではスコープ外として設計課題に留める。 +恒久対策として挙げた「dtor持ち構造体の暗黙コピーへの警告」はv0.17.2内で実装した([設計文書](dtor-copy-diagnostic.html)。let初期化・代入・return・構造体リテラルフィールドで警告、--strictでエラー)。 +「move代入(`self.map = move m;`)のサポート」は既存のmove機構で既に機能することを確認し、本記録の回避策(ポインタの手動無効化)は `self.map = move m;` へ置き換えた。 ## 回帰テスト diff --git a/docs/archive/v0.17.2/dtor-copy-diagnostic.md b/docs/archive/v0.17.2/dtor-copy-diagnostic.md new file mode 100644 index 00000000..29ed7965 --- /dev/null +++ b/docs/archive/v0.17.2/dtor-copy-diagnostic.md @@ -0,0 +1,48 @@ +# dtor持ち構造体の暗黙コピー診断(二重解放ハザードの言語側対策) + +## 問題 + +Cmの構造体代入は浅いコピーで、デストラクタ持ち構造体を値としてコピーすると両方のインスタンスでdtorが走り二重解放になる。 +この事故はHashSetコンストラクタで実際に発生し([記録](../archive/v0.17.2/bugfix-hashset-double-free.html))、非決定的SIGABRTとして顕在化した。 +恒久対策として同記録に「dtor持ち構造体の暗黙コピーへの警告」と「move代入のサポート」が挙げられ、v0.17.2時点では設計課題に留まっていた。 + +## 現状調査 + +- `move` 式はパーサ上任意の単項式位置で受理され、let初期化(`T b = move a;`)だけでなく代入RHS(`self.f = move a;`)でも既にMIRまで機能する(HIRのVarRefに`is_moved_from`が立ち、MIRが移動元のdtor登録を解除する)。実験でdtorが1回だけ走ることを確認した。 +- したがって「move代入のサポート」は実装済みであり、残る恒久対策は暗黙コピーの診断のみ。 +- dtor登録はlet宣言のローカルのみ(`mir/lowering/stmt/let.cpp`)。関数パラメータはdtor登録されないため、値渡し引数は現時点で二重解放の直接原因にならない(診断対象から除外し、本書に記録する)。 + +## 方針 + +数値縮小変換(Z5)と同じ段階導入方式を採る: 通常は警告、`--strict`(check/lint)ではエラーへ昇格する。 +既存構文は引き続き受理するため破壊的変更はない([[cm-language-design-principles]]の破壊的変更回避に従う)。 + +### 診断条件 + +受理サイト(let初期化・代入・return・構造体リテラルのフィールド初期化)で、以下をすべて満たす場合に診断する。 + +- 値の式が場所式(変数参照・フィールド参照・要素参照)である(コンストラクタ呼び出しやメソッド戻り値などの一時値は所有が一意なので対象外) +- 値の式が `move` でラップされていない +- 値の型が明示的なデストラクタ(`~self()`)を持つ構造体である(typedefは解決し、ジェネリックはベース名で判定する) + +### 対象外(本書で記録する制限) + +- 関数引数の値渡し: パラメータはdtor登録されないため二重解放にならない(コピーがポインタを共有する点は残るが、診断は実害のある二重解放系に絞る) +- dtor持ち構造体をフィールドに含むだけの構造体: メンバdtorの自動合成が無いため外側の型のコピーは二重解放にならない(メンバdtor合成の導入時に再検討する) +- ジェネリック関数内の型パラメータ`T`のコピー: 型検査は単相化前のため判定できない + +## 実装 + +- `TypeChecker` にdtor持ち型名の集合を追加し、impl登録時(`impl.destructor` 非null)にベース型名を記録する +- `check_dtor_copy_policy(source_type, value_expr, span)` を `utils/ownership.cpp` に新設し、let初期化・代入(`=`)・return・構造体リテラルフィールドの4サイトから呼ぶ +- メッセージは `MsgId::TcImplicitDtorCopy`(en/ja)で、`move` による所有権移動を提案する + +## 標準ライブラリの追従 + +`HashSet.self()` の「代入後にポインタを手動無効化する」回避策を `self.map = move m;` へ置き換える(診断のセルフチェックを兼ねる)。 + +## テスト計画 + +- regression(`tests/regression/dtor_copy_diag_test.cpp` + `cases/dtor_copy_diag/`): let/代入/return/フィールド初期化の各サイトで警告1件、move使用・一時値・dtor無し構造体で警告0件 +- integration(`tests/common/basic/destructor/move_assign.cm`): move代入・move初期化でdtorが移動先の1回だけ走ることの実行検証 +- 既存スイートの警告ゼロ確認(libs・tests/commonに暗黙コピーが残っていないこと) diff --git a/docs/design/index.md b/docs/design/index.md index fb773e9f..448da3ca 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -32,6 +32,7 @@ Cm言語コンパイラの設計文書の一覧。実装が完了した設計文 | [バグ記録: JSのスライスポインタ引数未対応](../archive/v0.17.2/bugfix-js-slice-pointer-arg.html) | K[]*引数の再帰渡しがJSバックエンドで壊れる制限の記録(ライブラリ側は反復走査で回避・バックエンド側は残課題) | | [セルフホスティング補強: 文字列ハッシュ・フォーマッタ・ソート・Interner](../archive/v0.17.2/stdlib-hardening.html) | StringMap/StringSet(内容ハッシュ)・to_radix/pad・ヒープソート・Interner・書き込み性能検証の実装計画(実装完了) | | [バグ修正: 総称ヘルパー連鎖の既定int汚染](../archive/v0.17.2/bugfix-mono-generic-helper.html) | 総称本体スキャンの偽特殊化がclone前の本体を汚染しint版ヘルパーが呼ばれる問題の記録(修正完了) | +| [dtor持ち構造体の暗黙コピー診断](../archive/v0.17.2/dtor-copy-diagnostic.html) | 二重解放ハザードの言語側対策(受理サイトでの場所式コピー警告・--strictエラー・move代入の確認)(実装完了) | ## v0.17.1 設計(実装済み・アーカイブ済み) diff --git a/docs/design/language-comparison.md b/docs/design/language-comparison.md index c34115e2..2b16fd0d 100644 --- a/docs/design/language-comparison.md +++ b/docs/design/language-comparison.md @@ -8,7 +8,7 @@ Cmを既存のシステムプログラミング言語(C++・Rust)と比較 | 観点 | C++ | Rust | Cm | |---|---|---|---| | 構文系統 | C系 | ML影響のC系 | C系(C++スタイル宣言・fnキーワードなし) | -| メモリ管理 | 手動+RAII+スマートポインタ | 所有権+借用検査(静的保証) | 手動+RAII(dtor)+スマートポインタ(規約ベース、静的保証なし) | +| メモリ管理 | 手動+RAII+スマートポインタ | 所有権+借用検査(静的保証) | 手動+RAII(dtor)+スマートポインタ(move規約+危険コピー診断、借用検査なし) | | エラー処理 | 例外+エラーコード混在 | Result/Option+panic | Result/Option(例外なし)+診断は状態保持の規約 | | 継承 | 多重継承あり | なし(trait) | なし(構造体合成+interface、設計方針として不採用) | | ジェネリクス | テンプレート(チューリング完全) | トレイト境界付きジェネリクス | 単相化ジェネリクス(境界なし・制約あり) | @@ -48,10 +48,11 @@ CPU実装(本リポジトリを含むCmCPU)のように「シミュレーシ ## Cmのデメリット -### 1. メモリ安全性の静的保証がない(対Rust最大の差) +### 1. メモリ安全性の静的保証が部分的(対Rust最大の差) -move/cloneの所有規律は規約ベースで、コンパイラは違反を検出しない。 -実際に「dtor持ち構造体の暗黙コピーによる二重解放(非決定的SIGABRT)」が標準ライブラリ開発で発生しており([記録](../archive/v0.17.2/bugfix-hashset-double-free.html))、Rustならコンパイルエラーになるクラスのバグが実行時まで潜伏する。 +move/cloneの所有規律は規約ベースが基本で、Rustの借用検査のような包括的な静的保証はない。 +「dtor持ち構造体の暗黙コピーによる二重解放(非決定的SIGABRT)」が標準ライブラリ開発で実際に発生し([記録](../archive/v0.17.2/bugfix-hashset-double-free.html))、このパターンはv0.17.2でコンパイラ診断(警告・`--strict`でエラー)とmove代入により言語側で検出できるようになった([設計文書](../archive/v0.17.2/dtor-copy-diagnostic.html))。 +ただし診断は既知の危険パターン(受理サイトでの場所式コピー)に限られ、ライフタイム検証や関数境界を跨ぐ所有権追跡はない。 生ポインタ(`void*`・`as` キャスト)が通常のコードに現れ、境界検査のない領域が残る。 データ競合の防止機構(RustのSend/Sync相当)はなく、スレッド安全性は利用者の責任になる。 diff --git a/docs/releases/v0.17.2.md b/docs/releases/v0.17.2.md index 33ecac03..6c00b2e4 100644 --- a/docs/releases/v0.17.2.md +++ b/docs/releases/v0.17.2.md @@ -176,6 +176,21 @@ idの比較は整数比較になり、`name_of` で内容を引き戻せる。 ファイルを行単位で処理するには `read_file` + `lines` の組み合わせを毎回書く必要があった。 `read_lines(path)` を追加した(LF/CRLF対応・失敗時は空スライス)。 +### dtor持ち構造体の暗黙コピー診断とmove代入 + +構造体代入は浅いコピーのため、デストラクタ持ち構造体を値でコピーすると両方のインスタンスでdtorが走り二重解放になる(HashSetコンストラクタで実際に発生した非決定的SIGABRTの真因。[設計文書](../archive/v0.17.2/dtor-copy-diagnostic.html))。 +恒久対策として、受理サイト(let初期化・代入・return・構造体リテラルのフィールド初期化)でのmoveなしの場所式コピーを検出する診断を追加した(通常は警告、`--strict` ではエラー。Z5の縮小変換と同じ段階導入方式)。 + +```cm +Res a; +Res b = a; // 警告: 暗黙コピー(二重解放) +Res c = move a; // OK: 所有権移動(aのdtorは抑止される) +self.map = move m; // OK: move代入も初期化と同様に機能する +``` + +move代入(`self.f = move x;`)は既存のmove機構(HIRの`is_moved_from`→MIRのdtor登録解除)で既に機能することを確認し、HashSetコンストラクタの「代入後にポインタを手動無効化する」回避策を `self.map = move m;` へ置き換えた。 +一時値(コンストラクタ呼び出し・メソッド戻り値)の代入や、dtorを持たない構造体のコピーは診断対象外。 + ## 🐛 コンパイラバグ修正(標準ライブラリ実装で顕在化) 新しい標準ライブラリが最初の実利用者となり、計5件の潜在コンパイラバグを発見・修正した([ジェネリック特殊化まわり3件の記録](../archive/v0.17.2/bugfix-generic-mir.html)・[DCE誤削除の記録](../archive/v0.17.2/bugfix-aot-struct-dce.html)・[総称ヘルパー連鎖の記録](../archive/v0.17.2/bugfix-mono-generic-helper.html))。 diff --git a/docs/tutorials/en/types/ownership.md b/docs/tutorials/en/types/ownership.md index 170bb53b..3c1787a4 100644 --- a/docs/tutorials/en/types/ownership.md +++ b/docs/tutorials/en/types/ownership.md @@ -90,6 +90,53 @@ int main() { } ``` +### Implicit-copy diagnostic for structs with destructors (v0.17.2) + +Struct assignment is a shallow copy, so copying a struct that has a destructor (`~self()`) by value runs the destructor for both instances and causes a double free. +The compiler detects this pattern and reports a warning (an error under `cm check --strict` / `cm lint --strict`). + +```cm +struct Res { + int* p; +} + +impl Res { + ~self() { /* free self.p */ } +} + +struct Holder { + Res r; +} + +int main() { + Res a; + Res b = a; // warning: implicit copy of a struct with a destructor (double free) + Holder h; + h.r = a; // warning: assignment copy into a field as well + return 0; +} +``` + +Use `move` when you intend to transfer ownership. It works in initialization, assignment, and return, and the source's destructor no longer runs: + +```cm +int main() { + Res a; + Res b = move a; // OK: ownership transfer (a's dtor does not run) + Res c; + Holder h; + h.r = move c; // OK: move assignment + return 0; +} + +Res make() { + Res r; + return move r; // OK: return by value with return move +} +``` + +Assigning temporaries whose source does not outlive the copy (constructor calls, method return values) is not diagnosed. + --- ## Borrowing (Address-based Borrow Counting) diff --git a/docs/tutorials/ja/types/ownership.md b/docs/tutorials/ja/types/ownership.md index 9dbb4d2f..58630e80 100644 --- a/docs/tutorials/ja/types/ownership.md +++ b/docs/tutorials/ja/types/ownership.md @@ -90,6 +90,53 @@ int main() { } ``` +### デストラクタ持ち構造体の暗黙コピー診断(v0.17.2) + +構造体の代入は浅いコピーのため、デストラクタ(`~self()`)を持つ構造体を値としてコピーすると、両方のインスタンスでデストラクタが実行され二重解放になります。 +このパターンはコンパイラが検出して警告します(`cm check --strict` / `cm lint --strict` ではエラー)。 + +```cm +struct Res { + int* p; +} + +impl Res { + ~self() { /* self.p を解放 */ } +} + +struct Holder { + Res r; +} + +int main() { + Res a; + Res b = a; // 警告: dtor持ち構造体の暗黙コピー(二重解放) + Holder h; + h.r = a; // 警告: フィールドへの代入コピーも同様 + return 0; +} +``` + +所有権を渡す意図なら `move` を使います。moveは初期化・代入・returnのいずれでも使え、移動元のデストラクタは実行されなくなります: + +```cm +int main() { + Res a; + Res b = move a; // OK: 所有権移動(aのdtorは走らない) + Res c; + Holder h; + h.r = move c; // OK: move代入 + return 0; +} + +Res make() { + Res r; + return move r; // OK: 値返しはreturn moveで +} +``` + +コンストラクタ呼び出しやメソッドの戻り値など、コピー元が残らない一時値の代入は診断されません。 + --- ## 借用(アドレス取得による借用カウント) diff --git a/libs/std/collections/hashset.cm b/libs/std/collections/hashset.cm index 1f72fa1a..306a6d2c 100644 --- a/libs/std/collections/hashset.cm +++ b/libs/std/collections/hashset.cm @@ -12,9 +12,8 @@ export impl HashSet { // 空の集合を作る self() { HashMap m(); - self.map = m; - // dtor持ち構造体の代入は浅いコピーのため、mのdtorがself.map.entriesも解放してしまう(二重解放)。所有権をself.mapへ移した印としてmの保持ポインタを無効化する - m.entries = 0 as Entry*; + // 所有権をself.mapへ移動する(moveで移動元mのdtor登録が解除され二重解放しない) + self.map = move m; } // 値を追加する(既存なら何もしない) diff --git a/src/internal/base/messages/ids.hpp b/src/internal/base/messages/ids.hpp index 10aec729..f762439d 100644 --- a/src/internal/base/messages/ids.hpp +++ b/src/internal/base/messages/ids.hpp @@ -584,6 +584,7 @@ enum class MsgId : int { PsStmtAttributeOnlyCaseModifier, TcPrintAggregateUnsupported, PsTupleUnsupported, + TcImplicitDtorCopy, Count, }; diff --git a/src/internal/base/messages/messages.cpp b/src/internal/base/messages/messages.cpp index b3608358..a0777fdf 100644 --- a/src/internal/base/messages/messages.cpp +++ b/src/internal/base/messages/messages.cpp @@ -1682,6 +1682,9 @@ constexpr const char* kMessages[kMessageCount][kLangCount] = { // PsTupleUnsupported {"tuple types and destructuring assignment are not supported (use a struct to group values)", "タプル型・分解代入は未対応です(値のグループ化には構造体を使用してください)"}, + // TcImplicitDtorCopy + {"implicit copy of '{0}' which has a destructor; the destructor runs for both copies and causes a double free. Use 'move' to transfer ownership", + "デストラクタを持つ '{0}' の暗黙コピーです。両方のコピーでデストラクタが実行され二重解放になります。所有権の移動には 'move' を使用してください"}, }; // clang-format on diff --git a/src/internal/types/checking/checker.hpp b/src/internal/types/checking/checker.hpp index 38bcafa5..436bb630 100644 --- a/src/internal/types/checking/checker.hpp +++ b/src/internal/types/checking/checker.hpp @@ -200,6 +200,12 @@ class TypeChecker { // 宛先に適合するリテラルは対象外。通常は警告、--strict(check/lint)ではエラーへ昇格する(段階導入) void check_numeric_conversion_policy(const ast::TypePtr& target, const ast::TypePtr& source, const ast::Expr* value_expr, Span span); + // 型が明示的なデストラクタ(~self)を持つ構造体か(typedef解決・ジェネリックはベース名・名前空間剥ぎで判定) + bool type_has_destructor(const ast::TypePtr& type); + // 受理サイト(let初期化・代入・return・構造体リテラルのフィールド初期化)でのdtor持ち構造体の暗黙コピー診断。 + // 場所式(変数・フィールド・要素参照)のコピーのみ対象で、move式・一時値は対象外。通常は警告、--strict(check/lint)ではエラーへ昇格する + void check_dtor_copy_policy(const ast::TypePtr& source_type, const ast::Expr* value_expr, + Span span); std::vector extract_format_variables(const std::string& format_str); // 文字列リテラルの補間プレースホルダを一度だけ実ASTへ脱糖する(第4段b)。 @@ -376,6 +382,9 @@ class TypeChecker { // 特殊化時のフィールド型検証(validate_derive_instantiation)が展開後もderive規則を適用するために使う std::unordered_map> derived_generic_impls_; + // 明示的なデストラクタ(~self)を持つ構造体のベース型名(impl登録時に記録。暗黙コピー診断で参照する) + std::unordered_set types_with_destructor_; + // 組み込みインターフェースのジェネリックパラメータ std::unordered_map> builtin_interface_generic_params_; diff --git a/src/internal/types/checking/decl/impl.cpp b/src/internal/types/checking/decl/impl.cpp index 73500205..90673487 100644 --- a/src/internal/types/checking/decl/impl.cpp +++ b/src/internal/types/checking/decl/impl.cpp @@ -19,6 +19,11 @@ void TypeChecker::register_impl(ast::ImplDecl& impl) { std::string type_name = ast::type_to_string(*impl.target_type); + // dtor持ち型のベース名を記録する(暗黙コピー診断 check_dtor_copy_policy が参照) + if (impl.destructor && impl.target_type) { + types_with_destructor_.insert(impl.target_type->name); + } + // コンストラクタ/デストラクタの登録(is_ctor_implの場合) if (impl.is_ctor_impl) { for (const auto& ctor : impl.constructors) { diff --git a/src/internal/types/checking/expr/operator.cpp b/src/internal/types/checking/expr/operator.cpp index 65556ac5..26beca4e 100644 --- a/src/internal/types/checking/expr/operator.cpp +++ b/src/internal/types/checking/expr/operator.cpp @@ -199,6 +199,11 @@ ast::TypePtr TypeChecker::infer_binary(ast::BinaryExpr& binary) { ltype, rtype_before_promotion, rhs_expr_before_promotion, binary.right->span.start != 0 ? binary.right->span : current_span_); } + // dtor持ち構造体の暗黙コピーを診断(単純代入のみ。moveなしの場所式コピーは二重解放になる) + if (binary.op == ast::BinaryOp::Assign) { + check_dtor_copy_policy(rtype_before_promotion, rhs_expr_before_promotion, + binary.right->span.start != 0 ? binary.right->span : current_span_); + } switch (binary.op) { case ast::BinaryOp::Eq: diff --git a/src/internal/types/checking/expr/primary.cpp b/src/internal/types/checking/expr/primary.cpp index aeb3816e..4323b833 100644 --- a/src/internal/types/checking/expr/primary.cpp +++ b/src/internal/types/checking/expr/primary.cpp @@ -664,6 +664,8 @@ ast::TypePtr TypeChecker::infer_struct_literal(ast::StructLiteralExpr& lit) { if (field_expected && field_value_type) { check_numeric_conversion_policy(field_expected, field_value_type, field.value.get(), field.value->span); + // dtor持ち構造体の暗黙コピーもlet/代入/returnと同じ規則で診断する + check_dtor_copy_policy(field_value_type, field.value.get(), field.value->span); } } // キャプチャ付きクロージャの構造体フィールド格納は環境喪失でゴミ値になるため拒否(V6) diff --git a/src/internal/types/checking/stmt.cpp b/src/internal/types/checking/stmt.cpp index 28697ea2..7d9d6dde 100644 --- a/src/internal/types/checking/stmt.cpp +++ b/src/internal/types/checking/stmt.cpp @@ -485,6 +485,8 @@ void TypeChecker::check_let(ast::LetStmt& let) { // 数値の縮小/符号変化の暗黙変換を診断(Z5。適合リテラルは対象外、--strictではエラー昇格) if (init_type) { check_numeric_conversion_policy(resolved_type, init_type, let.init.get(), stmt_span); + // dtor持ち構造体の暗黙コピーを診断(moveなしの場所式コピーは二重解放になる) + check_dtor_copy_policy(init_type, let.init.get(), stmt_span); } // リテラル型チェック(typedef HttpMethod = "GET" | "POST" など) if (let.init) { @@ -593,6 +595,8 @@ void TypeChecker::check_return(ast::ReturnStmt& ret) { } // 数値の縮小/符号変化の暗黙変換を診断(Z5。適合リテラルは対象外、--strictではエラー昇格) check_numeric_conversion_policy(current_return_type_, val_type, ret.value.get(), stmt_span); + // dtor持ち構造体をreturnで値返しする場合もコピー元ローカルのdtorが走るため診断する(return moveを促す) + check_dtor_copy_policy(val_type, ret.value.get(), stmt_span); // ライフタイムチェック: ローカル変数への参照を返すことを禁止 // return &x の場合、xがローカル変数ならダングリングポインタになる diff --git a/src/internal/types/checking/utils/ownership.cpp b/src/internal/types/checking/utils/ownership.cpp new file mode 100644 index 00000000..4b75fb96 --- /dev/null +++ b/src/internal/types/checking/utils/ownership.cpp @@ -0,0 +1,78 @@ +// dtor持ち構造体の暗黙コピー診断(二重解放ハザードの言語側対策)。 +// Cmの構造体代入は浅いコピーで、デストラクタ持ち構造体を値としてコピーすると両方のインスタンスでdtorが走り二重解放になる +// (HashSetコンストラクタで実際に発生: docs/archive/v0.17.2/bugfix-hashset-double-free.md)。 +// 受理サイト(let初期化・代入・return・構造体リテラルのフィールド初期化)で場所式のコピーを検出し、moveによる所有権移動を提案する。 +// Z5(数値縮小変換)と同じ段階導入方式: 通常は警告、--strict(check/lint)ではエラーへ昇格する + +#include "internal/base/i18n.hpp" +#include "internal/types/type_checker.hpp" + +namespace cm { + +namespace { + +// 場所式(変数参照・フィールド参照・要素参照)か。 +// 一時値(コンストラクタ呼び出し・メソッド戻り値等)は所有が一意でコピー元が残らないため診断対象にしない +bool is_place_expr(const ast::Expr& expr) { + if (expr.as()) { + return true; + } + if (const auto* member = expr.as()) { + return !member->is_method_call; + } + if (expr.as()) { + return true; + } + return false; +} + +} // namespace + +bool TypeChecker::type_has_destructor(const ast::TypePtr& type) { + if (!type) { + return false; + } + auto t = resolve_typedef(type); + if (!t || t->kind != ast::TypeKind::Struct) { + return false; + } + if (types_with_destructor_.count(t->name)) { + return true; + } + // 特殊化サフィックス(Name<...>・Name__k)と名前空間修飾を剥がして再照合する + const std::string base = strip_spec_suffix(t->name); + if (types_with_destructor_.count(base)) { + return true; + } + const size_t pos = base.rfind("::"); + if (pos != std::string::npos && types_with_destructor_.count(base.substr(pos + 2))) { + return true; + } + return false; +} + +void TypeChecker::check_dtor_copy_policy(const ast::TypePtr& source_type, + const ast::Expr* value_expr, Span span) { + if (!value_expr) { + return; + } + // move式は所有権移動(移動元のdtor登録がMIRで解除される)なので対象外 + if (value_expr->as()) { + return; + } + if (!is_place_expr(*value_expr)) { + return; + } + if (!type_has_destructor(source_type)) { + return; + } + auto t = resolve_typedef(source_type); + const std::string msg = i18n::msgf(i18n::MsgId::TcImplicitDtorCopy, ast::type_to_string(*t)); + if (enable_naming_check_) { + error(span, msg); + } else { + warning(span, msg); + } +} + +} // namespace cm diff --git a/tests/common/basic/destructor/move_assign.cm b/tests/common/basic/destructor/move_assign.cm new file mode 100644 index 00000000..43d4b805 --- /dev/null +++ b/tests/common/basic/destructor/move_assign.cm @@ -0,0 +1,35 @@ +import std::io::println; +// move初期化・move代入で移動元のdtorが抑止され、dtorが移動先の1回だけ走ることを検証する +// (dtor持ち構造体の暗黙コピー診断の対になる正しい書き方。moveなしのコピーは両方でdtorが走り二重解放になる) +struct Resource { + int id; +} + +impl Resource { + self(int id) { + self.id = id; + println("C{self.id}"); + } + + ~self() { + println("D{self.id}"); + } +} + +struct Holder { + Resource r; +} + +int main() { + // move初期化: aのdtorは走らず、bのdtorだけがスコープ終了時に走る + Resource a(1); + Resource b = move a; + + // move代入: cのdtorは走らない(h.rはlet宣言でないためdtor登録されず、フィールドへ移した資源はここでは追跡されない) + Resource c(2); + Holder h; + h.r = move c; + + println("M"); + return 0; +} diff --git a/tests/common/basic/destructor/move_assign.expect b/tests/common/basic/destructor/move_assign.expect new file mode 100644 index 00000000..78d22dbc --- /dev/null +++ b/tests/common/basic/destructor/move_assign.expect @@ -0,0 +1,4 @@ +C1 +C2 +M +D1 diff --git a/tests/regression/cases/dtor_copy_diag/assign_copy.cm b/tests/regression/cases/dtor_copy_diag/assign_copy.cm new file mode 100644 index 00000000..043f0cb4 --- /dev/null +++ b/tests/regression/cases/dtor_copy_diag/assign_copy.cm @@ -0,0 +1,21 @@ +// dtor持ち構造体のフィールドへの代入コピー(moveなし)は警告される(HashSet二重解放と同型のパターン) +struct Res { + int* p; +} + +impl Res { + ~self() { + self.p = 0 as int*; + } +} + +struct Holder { + Res r; +} + +int main() { + Res a; + Holder h; + h.r = a; + return 0; +} diff --git a/tests/regression/cases/dtor_copy_diag/field_init_copy.cm b/tests/regression/cases/dtor_copy_diag/field_init_copy.cm new file mode 100644 index 00000000..69654a07 --- /dev/null +++ b/tests/regression/cases/dtor_copy_diag/field_init_copy.cm @@ -0,0 +1,20 @@ +// 構造体リテラルのフィールド初期化でのdtor持ち構造体コピーも警告される +struct Res { + int* p; +} + +impl Res { + ~self() { + self.p = 0 as int*; + } +} + +struct Holder { + Res r; +} + +int main() { + Res a; + Holder h = {r: a}; + return 0; +} diff --git a/tests/regression/cases/dtor_copy_diag/let_copy.cm b/tests/regression/cases/dtor_copy_diag/let_copy.cm new file mode 100644 index 00000000..2ad84cbc --- /dev/null +++ b/tests/regression/cases/dtor_copy_diag/let_copy.cm @@ -0,0 +1,16 @@ +// dtor持ち構造体のlet初期化コピー(moveなし)は警告される +struct Res { + int* p; +} + +impl Res { + ~self() { + self.p = 0 as int*; + } +} + +int main() { + Res a; + Res b = a; + return 0; +} diff --git a/tests/regression/cases/dtor_copy_diag/move_ok.cm b/tests/regression/cases/dtor_copy_diag/move_ok.cm new file mode 100644 index 00000000..20798716 --- /dev/null +++ b/tests/regression/cases/dtor_copy_diag/move_ok.cm @@ -0,0 +1,29 @@ +// move付きの所有権移動は警告されない(let初期化・代入・return move) +struct Res { + int* p; +} + +impl Res { + ~self() { + self.p = 0 as int*; + } +} + +struct Holder { + Res r; +} + +Res make() { + Res a; + return move a; +} + +int main() { + Res a; + Res b = move a; + Res c; + Holder h; + h.r = move c; + Res d = make(); + return 0; +} diff --git a/tests/regression/cases/dtor_copy_diag/no_dtor_ok.cm b/tests/regression/cases/dtor_copy_diag/no_dtor_ok.cm new file mode 100644 index 00000000..cd227d80 --- /dev/null +++ b/tests/regression/cases/dtor_copy_diag/no_dtor_ok.cm @@ -0,0 +1,17 @@ +// dtorを持たない構造体のコピーは警告されない +struct Point { + int x; + int y; +} + +struct Holder { + Point p; +} + +int main() { + Point a; + Point b = a; + Holder h; + h.p = a; + return 0; +} diff --git a/tests/regression/cases/dtor_copy_diag/return_copy.cm b/tests/regression/cases/dtor_copy_diag/return_copy.cm new file mode 100644 index 00000000..9d2bfc6b --- /dev/null +++ b/tests/regression/cases/dtor_copy_diag/return_copy.cm @@ -0,0 +1,20 @@ +// dtor持ち構造体のローカルを値返し(moveなし)するとコピー元のdtorが走るため警告される +struct Res { + int* p; +} + +impl Res { + ~self() { + self.p = 0 as int*; + } +} + +Res make() { + Res a; + return a; +} + +int main() { + Res b = make(); + return 0; +} diff --git a/tests/regression/dtor_copy_diag_test.cpp b/tests/regression/dtor_copy_diag_test.cpp new file mode 100644 index 00000000..2463ab27 --- /dev/null +++ b/tests/regression/dtor_copy_diag_test.cpp @@ -0,0 +1,77 @@ +#include "../../src/internal/syntax/lexer/lexer.hpp" +#include "../../src/internal/syntax/parser/parser.hpp" +#include "../../src/internal/types/checking/checker.hpp" + +#include +#include +#include +#include + +using namespace cm; + +// ============================================================ +// dtor持ち構造体の暗黙コピー診断のテスト(二重解放ハザードの言語側対策) +// ============================================================ +// moveなしの場所式コピーがlet初期化・代入・return・構造体リテラルフィールドで警告され、 +// move使用・一時値・dtor無し構造体では警告されないことの回帰。 +// Cmソースは tests/regression/cases/dtor_copy_diag/ の .cm ファイルに分割 +class DtorCopyDiagTest : public ::testing::Test { + protected: + std::string load_case(const std::string& name) { + std::string path = std::string(CM_DTOR_COPY_DIAG_CASE_DIR) + "/" + name + ".cm"; + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << "ケースファイルを読み込めません: " << path; + std::stringstream ss; + ss << ifs.rdbuf(); + return ss.str(); + } + + // 型検査を実行し、dtor持ち構造体の暗黙コピー警告の件数を返す + int dtor_copy_warning_count(const std::string& code) { + Lexer lex(code); + std::vector tokens = lex.tokenize(); + Parser p(tokens); + auto ast = p.parse(); + + TypeChecker checker; + checker.check(ast); + int count = 0; + for (const auto& d : checker.diagnostics()) { + if (d.severity == Severity::Warning && + d.message.find("destructor") != std::string::npos) { + ++count; + } + } + return count; + } +}; + +// let初期化のコピー(moveなし)は警告される +TEST_F(DtorCopyDiagTest, LetInitCopyWarns) { + EXPECT_EQ(dtor_copy_warning_count(load_case("let_copy")), 1); +} + +// フィールドへの代入コピー(moveなし)は警告される(HashSet二重解放と同型のパターン) +TEST_F(DtorCopyDiagTest, FieldAssignCopyWarns) { + EXPECT_EQ(dtor_copy_warning_count(load_case("assign_copy")), 1); +} + +// ローカルの値返し(moveなし)は警告される(return moveを促す) +TEST_F(DtorCopyDiagTest, ReturnCopyWarns) { + EXPECT_EQ(dtor_copy_warning_count(load_case("return_copy")), 1); +} + +// 構造体リテラルのフィールド初期化コピーは警告される +TEST_F(DtorCopyDiagTest, StructLiteralFieldCopyWarns) { + EXPECT_EQ(dtor_copy_warning_count(load_case("field_init_copy")), 1); +} + +// move付きの所有権移動(let初期化・代入・return move)は警告されない +TEST_F(DtorCopyDiagTest, MoveDoesNotWarn) { + EXPECT_EQ(dtor_copy_warning_count(load_case("move_ok")), 0); +} + +// dtorを持たない構造体のコピーは警告されない +TEST_F(DtorCopyDiagTest, NoDtorStructDoesNotWarn) { + EXPECT_EQ(dtor_copy_warning_count(load_case("no_dtor_ok")), 0); +} From 5dc9eb21a9263e39b0543c50664a9bdd974faca8 Mon Sep 17 00:00:00 2001 From: Kenta Miyajima Date: Sat, 15 Aug 2026 20:24:51 +0900 Subject: [PATCH 22/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- libs/std/mem/smart/mod.cm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/std/mem/smart/mod.cm b/libs/std/mem/smart/mod.cm index af808498..d7b6aad0 100644 --- a/libs/std/mem/smart/mod.cm +++ b/libs/std/mem/smart/mod.cm @@ -106,7 +106,7 @@ export impl SharedPtr { void* raw = alloc(__sizeof__(T) as int); self.ptr = raw as T*; *self.ptr = value; - void* craw = alloc(8); + void* craw = alloc(__sizeof__(SharedCount) as int); self.count = craw as SharedCount*; self.count->strong = 1; self.count->weak = 0; From 4b9fc57afe97d302cc9100e9d1bd2c1c2c8417cc Mon Sep 17 00:00:00 2001 From: Kenta Miyajima Date: Sat, 15 Aug 2026 20:24:59 +0900 Subject: [PATCH 23/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- libs/std/mem/smart/atomic.cm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/std/mem/smart/atomic.cm b/libs/std/mem/smart/atomic.cm index 94e7a649..5a04eabb 100644 --- a/libs/std/mem/smart/atomic.cm +++ b/libs/std/mem/smart/atomic.cm @@ -30,7 +30,7 @@ export impl AtomicSharedPtr { void* raw = alloc(__sizeof__(T) as int); self.ptr = raw as T*; *self.ptr = value; - void* craw = alloc(8); + void* craw = alloc(__sizeof__(AtomicSharedCount) as int); self.count = craw as AtomicSharedCount*; self.count->strong = 1; self.count->weak = 0; From 9cf7812a5bf313a66c59cddc2236c0b42bb185a9 Mon Sep 17 00:00:00 2001 From: Kenta Miyajima Date: Sat, 15 Aug 2026 20:25:05 +0900 Subject: [PATCH 24/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/common/structs/ptr_only_struct.cm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common/structs/ptr_only_struct.cm b/tests/common/structs/ptr_only_struct.cm index 9101f998..ddfb16e2 100644 --- a/tests/common/structs/ptr_only_struct.cm +++ b/tests/common/structs/ptr_only_struct.cm @@ -21,7 +21,7 @@ impl Box { void* raw = alloc(__sizeof__(T) as int); self.ptr = raw as T*; *self.ptr = value; - void* craw = alloc(8); + void* craw = alloc(__sizeof__(Ctr) as int); self.count = craw as Ctr*; self.count->strong = 1; self.count->weak = 0; From 1a83857b18d57c45524539f3d173c92a733b794d Mon Sep 17 00:00:00 2001 From: Kenta Miyajima Date: Sat, 15 Aug 2026 20:30:56 +0900 Subject: [PATCH 25/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/internal/mir/passes/cleanup/program_dce.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/internal/mir/passes/cleanup/program_dce.cpp b/src/internal/mir/passes/cleanup/program_dce.cpp index c2d054aa..c8265a85 100644 --- a/src/internal/mir/passes/cleanup/program_dce.cpp +++ b/src/internal/mir/passes/cleanup/program_dce.cpp @@ -270,9 +270,11 @@ static void collect_struct_names_from_type(const ast::TypePtr& type, std::setelement_type, used, worklist); - for (const auto& targ : type->type_args) { + for (const auto& targ : type->type_args) collect_struct_names_from_type(targ, used, worklist); - } + for (const auto& pt : type->param_types) + collect_struct_names_from_type(pt, used, worklist); + collect_struct_names_from_type(type->return_type, used, worklist); } void ProgramDeadCodeElimination::collect_used_structs(const MirProgram& program,