diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d5ae2bc..86a802a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,17 +1,17 @@ -name: バグ報告 -description: 不具合の報告 +name: Bug report +description: Report a bug labels: ["bug"] body: - type: markdown attributes: value: | - バグ報告ありがとうございます。以下のフォームを埋めてください。 - 再現可能な最小コードを添えていただけると助かります。 + Thanks for reporting a bug. Please fill out the form below. + A minimal reproducible example would be greatly appreciated. - type: input id: version attributes: - label: rwc のバージョン - description: "例: v0.0.4 (rwc --version の出力)" + label: rwc version + description: "e.g. v0.0.4 (output of rwc --version)" placeholder: "v0.0.4" validations: required: true @@ -23,35 +23,35 @@ body: - Linux - macOS - Windows - - その他 + - Other validations: required: true - type: textarea id: reproduce attributes: - label: 再現手順 - description: バグを再現するための手順を記載してください。 + label: Steps to reproduce + description: Describe the steps to reproduce the bug. placeholder: | - 1. 以下のコードを `bug.rw` に保存 - 2. `rwc bug.rw` を実行 + 1. Save the following code to `bug.rw` + 2. Run `rwc bug.rw` 3. ... validations: required: true - type: textarea id: expected attributes: - label: 期待される挙動 + label: Expected behavior validations: required: true - type: textarea id: actual attributes: - label: 実際の挙動 + label: Actual behavior validations: required: true - type: textarea id: additional attributes: - label: 追加情報 - description: スタックトレース、IR ダンプ、関連ログなどがあれば貼り付けてください。 + label: Additional information + description: Paste any stack traces, IR dumps, or relevant logs if available. render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d8de9e6..242214b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - - name: 質問・ディスカッション + - name: Questions and discussions url: https://github.com/ryuichi1208/rw/discussions - about: 質問やアイデアの議論は Discussions へ + about: For questions and idea discussions, please use Discussions diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index e0c4247..11a0d2c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,34 +1,34 @@ -name: 機能要望 -description: 新機能・改善の提案 +name: Feature request +description: Propose a new feature or improvement labels: ["enhancement"] body: - type: markdown attributes: value: | - 新機能・改善提案ありがとうございます。 - 大きめの設計提案の場合は RFC テンプレートの利用もご検討ください。 + Thanks for proposing a new feature or improvement. + For larger design proposals, please consider using the RFC template. - type: textarea id: motivation attributes: - label: 背景・モチベーション - description: なぜこの機能が必要なのか、現状の課題は何かを記述してください。 + label: Background and motivation + description: Explain why this feature is needed and what the current problem is. validations: required: true - type: textarea id: proposal attributes: - label: 提案する機能 - description: 具体的な機能内容、想定される API / 構文 / 挙動などを記述してください。 + label: Proposed feature + description: Describe the concrete feature, the expected API / syntax / behavior, and so on. validations: required: true - type: textarea id: alternatives attributes: - label: 代替案 - description: 検討した他のアプローチがあれば記載してください。 + label: Alternatives + description: List any other approaches you considered. - type: textarea id: scope attributes: - label: 影響範囲 - description: "影響する領域 (lang / runtime / stdlib / tooling など) を記載してください。" - placeholder: "例: lang (parser, type checker), runtime (netpoller)" + label: Scope of impact + description: "Describe the affected areas (lang / runtime / stdlib / tooling, etc.)." + placeholder: "e.g. lang (parser, type checker), runtime (netpoller)" diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml index d1b8edc..541c366 100644 --- a/.github/ISSUE_TEMPLATE/rfc.yml +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -1,48 +1,48 @@ name: RFC -description: 設計提案 (Request For Comments) +description: Design proposal (Request For Comments) labels: ["rfc", "documentation"] body: - type: markdown attributes: value: | - 言語仕様・ランタイム・標準ライブラリなどの設計変更を提案するためのテンプレートです。 - 実装着手前に議論したい大きな変更はこちらでお願いします。 + A template for proposing design changes to the language specification, runtime, standard library, and so on. + Please use this for large changes you want to discuss before starting implementation. - type: input id: title attributes: - label: RFC タイトル - placeholder: "例: async generator の導入" + label: RFC title + placeholder: "e.g. Introduce async generators" - type: textarea id: summary attributes: - label: 要約 (TL;DR) - description: 提案内容を 2-3 行で要約してください。 + label: Summary (TL;DR) + description: Summarize the proposal in 2-3 lines. - type: textarea id: motivation attributes: - label: 動機・背景 - description: 解決したい問題、現状の制約、ユースケースなど。 + label: Motivation and background + description: The problem you want to solve, current limitations, use cases, etc. validations: required: true - type: textarea id: design attributes: - label: 詳細設計 - description: 構文 / セマンティクス / 型システム / ABI など、変更点を可能な限り詳細に。 + label: Detailed design + description: Describe the changes in as much detail as possible, including syntax / semantics / type system / ABI. validations: required: true - type: textarea id: alternatives attributes: - label: 代替案 - description: 検討した他の設計、それを採用しなかった理由。 + label: Alternatives + description: Other designs you considered and why you did not adopt them. - type: textarea id: impact attributes: - label: 影響範囲・移行戦略 - description: 既存コードへの影響、後方互換性、移行手順など。 + label: Scope of impact and migration strategy + description: Impact on existing code, backward compatibility, migration steps, etc. - type: textarea id: open_questions attributes: label: Open Questions - description: 議論したい未解決の論点を列挙してください。 + description: List any unresolved points you want to discuss. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9ea0caf..17f6e77 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,19 @@ -## 概要 +## Overview - + -## 変更点 +## Changes - -## 関連 issue +## Related issues Closes # -## チェックリスト +## Checklist -- [ ] テストを追加 / 既存テストを更新した -- [ ] ドキュメントを更新した(必要に応じて) -- [ ] ローカルで `uv run pytest` が通る -- [ ] C runtime を変更した場合 `make -C runtime test` が通る -- [ ] 破壊的変更がある場合は CHANGELOG.md / docs に記載 +- [ ] Added tests / updated existing tests +- [ ] Updated documentation (if needed) +- [ ] `uv run pytest` passes locally +- [ ] `make -C runtime test` passes if the C runtime was changed +- [ ] Documented breaking changes in CHANGELOG.md / docs, if any diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ddb209d..817613b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -# 段階適用方針: 当初は warning として運用し、PR ごとに対象を絞って fail に切り替える +# Gradual rollout policy: start as warnings, then switch to failures for targeted scopes per PR name: lint on: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7844c4d..820cd66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,39 +2,39 @@ ## [v0.0.6](https://github.com/rwlang-org/rw/compare/v0.0.5...v0.0.6) - 2026-06-21 -- ci: ruff/black/mypy による lint/format チェック (#76) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/124 -- chore: pre-commit-hooks 設定を追加 (#88) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/125 -- docs: CONTRIBUTING.md を追加 (#70) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/126 -- docs: Issue / PR テンプレートを追加 (#72) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/127 -- ci: GitHub Actions を SHA ピン留めし dependabot を追加 (#81) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/128 -- docs: CoC / SECURITY / LICENSE を整備 (#71) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/129 +- ci: add lint/format checks with ruff/black/mypy (#76) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/124 +- chore: add pre-commit-hooks configuration (#88) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/125 +- docs: add CONTRIBUTING.md (#70) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/126 +- docs: add Issue / PR templates (#72) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/127 +- ci: SHA-pin GitHub Actions and add dependabot (#81) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/128 +- docs: add CoC / SECURITY / LICENSE (#71) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/129 ## [v0.0.5](https://github.com/rwlang-org/rw/compare/v0.0.4...v0.0.5) - 2026-06-06 -- examples: TCP チャットサーバのサンプル追加 by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/113 -- 言語: 三項演算子 / 条件式 (x if cond else y) を実装 by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/114 -- docs: 条件式(三項演算子)の言語仕様を追加 by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/115 -- stdlib: ファイル I/O と fd 汎用 read/write/close (#33) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/116 -- runtime: ファイル I/O を非同期化(スレッドプールにオフロード) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/117 -- lang: 数値リテラル拡張(16進・8進・2進・指数・アンダースコア区切り) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/118 -- lang: ビット演算子 (& | ^ ~ << >>) を実装 by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/119 -- lang: break / continue 文を実装(while ループ対象) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/120 -- lang: assert 文を実装 by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/121 -- lang: 型エイリアス (type Foo = ...) を実装 by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/122 -- stdlib: math 組み込み関数を実装(LLVM intrinsic) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/123 +- examples: add a TCP chat server sample by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/113 +- lang: implement the ternary / conditional expression (x if cond else y) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/114 +- docs: add the language spec for the conditional (ternary) expression by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/115 +- stdlib: file I/O and generic fd read/write/close (#33) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/116 +- runtime: make file I/O asynchronous (offload to a thread pool) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/117 +- lang: extend numeric literals (hex, octal, binary, exponent, underscore separators) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/118 +- lang: implement bitwise operators (& | ^ ~ << >>) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/119 +- lang: implement break / continue statements (targeting while loops) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/120 +- lang: implement the assert statement by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/121 +- lang: implement type aliases (type Foo = ...) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/122 +- stdlib: implement math built-in functions (LLVM intrinsics) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/123 ## [v0.0.4](https://github.com/rwlang-org/rw/compare/v0.0.3...v0.0.4) - 2026-05-23 -- rwc + runtime: netpoller + 最小 TCP API (echo server) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/97 +- rwc + runtime: netpoller + minimal TCP API (echo server) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/97 ## [v0.0.3](https://github.com/rwlang-org/rw/compare/v0.0.2...v0.0.3) - 2026-05-23 -- rwc: 文字列の最小拡張 (len, ==, +) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/91 -- rwc: Bytes 型 (immutable, echo 最小セット) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/92 -- rwc: List[int] 型 (immutable, モノモーフ, echo 最小セット) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/93 -- rwc: Option[int] 型と最小 match 構文 (Python 3.10 風) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/94 -- rwc: Result[int, int] 型と match の Ok/Err アーム拡張 by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/95 +- rwc: minimal string extensions (len, ==, +) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/91 +- rwc: Bytes type (immutable, minimal echo set) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/92 +- rwc: List[int] type (immutable, monomorphic, minimal echo set) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/93 +- rwc: Option[int] type and minimal match syntax (Python 3.10 style) by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/94 +- rwc: Result[int, int] type and Ok/Err arm extension for match by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/95 ## [v0.0.2](https://github.com/rwlang-org/rw/compare/v0.0.1...v0.0.2) - 2026-05-20 -- ランタイムを Go 風 M:N スケジューラに進化させる by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/90 +- evolve the runtime into a Go-style M:N scheduler by @ryuichi1208 in https://github.com/rwlang-org/rw/pull/90 ## [v0.0.1](https://github.com/ryuichi1208/rw/commits/v0.0.1) - 2026-05-17 - rw v0.0.1: MVP compiler with fiber-backed runtime by @ryuichi1208 in https://github.com/ryuichi1208/rw/pull/1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 93d972c..9f7f3d5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,84 +1,132 @@ # Contributor Covenant Code of Conduct -## 私たちの誓い +## Our Pledge -私たちメンバー、コントリビューター、およびリーダーは、年齢、体型、目に見える・見えない障害、民族性、性的特徴、性自認と性表現、経験の度合い、教育、社会経済的地位、国籍、容姿、人種、カースト、肌の色、宗教、性的指向のいかんにかかわらず、誰もがハラスメントのないコミュニティへの参加体験を得られることを誓います。 +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. -私たちは、オープンで歓迎的で多様性に富み、包摂的で健全なコミュニティに貢献する形で行動し、交流することを誓います。 +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. -## 私たちの行動基準 +## Our Standards -このコミュニティにとって良い環境に貢献する行動の例: +Examples of behavior that contributes to a positive environment for our +community include: -- 他者への共感と優しさを示す -- 異なる意見・視点・経験を尊重する -- 建設的なフィードバックを与え、また快く受け入れる -- 自分の過ちの責任を受け入れ、影響を受けた人々に謝罪し、その経験から学ぶ -- 個人だけでなくコミュニティ全体にとって最善となることに焦点を当てる +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -容認されない行動の例: +Examples of unacceptable behavior include: -- 性的な言葉や図像の使用、あらゆる種類の性的な注目や誘い -- 荒らし、侮辱的・中傷的なコメント、個人的・政治的な攻撃 -- 公的・私的を問わないハラスメント -- 明示的な許可なく、物理的住所や電子メールアドレスなど他者の私的情報を公開する -- 専門的な場において合理的に不適切とみなされるその他の行為 +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting -## 施行の責任 +## Enforcement Responsibilities -コミュニティのリーダーは、容認される行動の基準を明確化し施行する責任を負い、不適切・脅迫的・攻撃的・有害とみなされる行動に対して、適切かつ公正な是正措置を講じます。 +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -コミュニティのリーダーは、この行動規範に沿わないコメント、コミット、コード、wiki の編集、issue その他の貢献を削除・編集・拒否する権利と責任を持ち、適切な場合にはモデレーションの判断理由を説明します。 +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. -## 適用範囲 +## Scope -この行動規範は、すべてのコミュニティの場において適用されます。また、個人が公の場でコミュニティを公式に代表している場合にも適用されます。 +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. -## 施行 +## Enforcement -虐待的、ハラスメント的、その他容認されない行動の事例は、施行を担当するコミュニティのリーダーに以下の連絡先で報告できます。 +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +r_watanabe@ivry.jp. +All complaints will be reviewed and investigated promptly and fairly. -`r_watanabe@ivry.jp` +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. -すべての苦情は速やかかつ公正に審査・調査されます。 +## Enforcement Guidelines -すべてのコミュニティのリーダーは、報告者のプライバシーと安全を尊重する義務を負います。 +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: -## 施行ガイドライン +### 1. Correction -コミュニティのリーダーは、この行動規範に違反するとみなした行為に対する措置を決定する際、以下のコミュニティ影響度ガイドラインに従います。 +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. -### 1. 訂正 +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. -**コミュニティへの影響**: 不適切な言葉遣い、その他コミュニティにおいて非専門的または歓迎されないとみなされる行為。 +### 2. Warning -**結果**: コミュニティのリーダーから非公開で書面による警告が行われ、違反の性質と当該行為がなぜ不適切であったかの説明が示されます。公開の謝罪が求められる場合があります。 +**Community Impact**: A violation through a single incident or series of +actions. -### 2. 警告 +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. -**コミュニティへの影響**: 単発または一連の行為による違反。 +### 3. Temporary Ban -**結果**: 継続的な行為に対する結果を伴う警告。一定期間、関係者との交流(行動規範の施行者との一方的なやり取りを含む)が禁止されます。これにはコミュニティの場および SNS などの外部チャネルでの交流回避が含まれます。違反した場合は一時的または恒久的な追放につながる可能性があります。 +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. -### 3. 一時的追放 +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. -**コミュニティへの影響**: 持続的な不適切行為を含む、コミュニティ基準への深刻な違反。 +### 4. Permanent Ban -**結果**: 一定期間、コミュニティとのあらゆる交流・公的コミュニケーションを禁止する一時的追放。この期間中は、関係者との公的・私的な交流が一切禁止されます。違反した場合は恒久的な追放につながる可能性があります。 +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. -### 4. 恒久的追放 +**Consequence**: A permanent ban from any sort of public interaction within the +community. -**コミュニティへの影響**: 持続的な不適切行為、個人へのハラスメント、特定層への攻撃や蔑視を含む、コミュニティ基準への重大な違反パターンの提示。 +## Attribution -**結果**: コミュニティ内でのあらゆる公的な交流からの恒久的追放。 +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -## 帰属 +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. -この行動規範は [Contributor Covenant][homepage] バージョン 2.1 から引用したものです。 -こちらで参照できます: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html - -コミュニティ影響度ガイドラインは [Mozilla の行動規範施行のはしご](https://github.com/mozilla/diversity) から着想を得ています。 +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org - -この行動規範に関するよくある質問への回答は https://www.contributor-covenant.org/faq を参照してください。翻訳は https://www.contributor-covenant.org/translations から入手できます。 +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a71bbc8..667312c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,27 +1,27 @@ # Contributing to rw -rw への貢献を検討いただきありがとうございます。本ドキュメントは、開発環境のセットアップから PR を出すまでの流れと、コーディング規約をまとめたものです。 +Thank you for considering a contribution to rw. This document walks through the flow from setting up your development environment to opening a PR, along with the coding conventions. -## 1. このプロジェクトについて +## 1. About this project -rw は Python-flavored, async-first, statically-typed compiled language です。 +rw is a Python-flavored, async-first, statically-typed compiled language. -- **フロントエンド**: Python 実装 (lexer / parser / sema / irgen) 。コードは `rwc/` 配下。 -- **ランタイム**: C 実装。コードは `runtime/` 配下。 -- **言語仕様の一次情報**: `docs/specs/` 以下の Markdown ドキュメントを参照してください。 +- **Frontend**: implemented in Python (lexer / parser / sema / irgen). The code lives under `rwc/`. +- **Runtime**: implemented in C. The code lives under `runtime/`. +- **Primary source for the language spec**: see the Markdown documents under `docs/specs/`. -仕様や設計に疑問が出たら、まず `docs/specs/` を確認することをおすすめします。 +Whenever you have a question about the spec or the design, we recommend checking `docs/specs/` first. -## 2. 開発環境のセットアップ +## 2. Setting up the development environment -### 必要なもの +### Prerequisites -- Python 3.11 以上 +- Python 3.11 or later - [uv](https://github.com/astral-sh/uv) -- clang (C11 をサポートするもの) +- clang (with C11 support) - make -### 手順 +### Steps ```sh git clone https://github.com/ryuichi1208/rw @@ -30,74 +30,74 @@ uv sync --extra dev make -C runtime ``` -`uv sync --extra dev` で開発用の Python 依存をすべて取得します。`make -C runtime` で C ランタイムをビルドします。 +`uv sync --extra dev` fetches all of the Python dependencies used for development. `make -C runtime` builds the C runtime. -## 3. テスト +## 3. Testing -### Python 側 +### Python side -全テストを実行する場合: +To run the full test suite: ```sh uv run pytest -v ``` -単一のテストファイルだけを実行する場合: +To run just a single test file: ```sh uv run pytest tests/test_irgen.py -v ``` -### C ランタイム +### C runtime ```sh make -C runtime test ``` -### E2E サンプル +### E2E samples -`examples/*.rw` を `rwc` でコンパイルし、実行結果を `*.rw.expected` と比較する形で E2E テストが構成されています。新しい言語機能を追加した際は、対応するサンプルと expected ファイルを追加してください。 +The E2E tests are structured by compiling `examples/*.rw` with `rwc` and comparing the results against `*.rw.expected`. When you add a new language feature, please add the corresponding sample and expected file. ## 4. lint / format (pre-commit) -本プロジェクトは [pre-commit](https://pre-commit.com/) を使ってフォーマットと lint を強制しています。 +This project enforces formatting and linting with [pre-commit](https://pre-commit.com/). -初回セットアップ: +First-time setup: ```sh -pip install pre-commit # または uv tool install pre-commit +pip install pre-commit # or: uv tool install pre-commit pre-commit install ``` -全ファイルに対して実行: +Run against all files: ```sh pre-commit run --all-files ``` -使用しているツール: +Tools in use: - **ruff** (lint + format) - **black** - **mypy** -設定は `pyproject.toml` および `.pre-commit-config.yaml` にあります。mypy は段階的に厳格化していく方針なので、既存コードでエラーが残っていても、新規に書くコードからは型を素直に通せる形で書いてください。 +The configuration lives in `pyproject.toml` and `.pre-commit-config.yaml`. Since we plan to tighten mypy incrementally, existing code may still have outstanding errors; for any new code you write, please make sure the types pass cleanly. -## 5. PR フロー +## 5. PR flow -1. feature ブランチを切ります。命名例: +1. Create a feature branch. Naming examples: - `feat/issue-NN-short-desc` - `fix/issue-NN-short-desc` - `docs/issue-NN-short-desc` -2. 変更は小さく分けてコミットしてください。 -3. PR テンプレートに従って記述し、関連 issue は `Closes #N` の形式で本文に含めます。 -4. CI が通ったらレビュー依頼を出してください。 -5. main への取り込みは **squash merge** を基本とします。 -6. リリースは [tagpr](https://github.com/Songmu/tagpr) が main への push をトリガに自動でリリース PR を生成します。リリース時は、その PR をマージするだけで OK です。 +2. Keep your changes small and split them into focused commits. +3. Follow the PR template, and reference the related issue in the body using the `Closes #N` form. +4. Once CI passes, request a review. +5. Merges into main are done with **squash merge** by default. +6. Releases are handled by [tagpr](https://github.com/Songmu/tagpr), which automatically generates a release PR triggered by pushes to main. To release, you just merge that PR. -## 6. コミットメッセージ規則 +## 6. Commit message convention -Conventional Commits 風 (ゆるめ) を採用しています。 +We follow a (loose) Conventional Commits style. ``` : @@ -105,52 +105,52 @@ Conventional Commits 風 (ゆるめ) を採用しています。 ``` -### type の例 +### Examples of type -- `feat`: 新機能 -- `fix`: バグ修正 -- `docs`: ドキュメントのみの変更 -- `chore`: 雑多な変更 (ビルド設定など) -- `ci`: CI 設定の変更 -- `refactor`: 振る舞いを変えないリファクタ -- `test`: テストの追加・修正 -- `perf`: パフォーマンス改善 +- `feat`: a new feature +- `fix`: a bug fix +- `docs`: documentation-only changes +- `chore`: miscellaneous changes (build config, etc.) +- `ci`: changes to CI configuration +- `refactor`: a refactor that does not change behavior +- `test`: adding or fixing tests +- `perf`: a performance improvement -### subject のルール +### Rules for subject -- 50 字以内 -- 命令形 (例: `add`, `fix`, `update` で始める) -- 末尾にピリオドを付けない +- 50 characters or fewer +- Imperative mood (e.g. start with `add`, `fix`, `update`) +- No trailing period -本文を書く場合は 72 字目安で wrap してください。 +When you write a body, wrap it at roughly 72 characters. -## 7. コーディング規約 +## 7. Coding conventions ### Python (`rwc/`) -- line length は 120 文字 -- ruff + black で format。pre-commit で自動整形されます -- 型ヒントを推奨。新規モジュールは mypy で素直に通る形で書いてください -- import 順は ruff の `I` ルール (isort 互換) に従います +- Line length is 120 characters +- Format with ruff + black; pre-commit formats automatically +- Type hints are recommended. Write new modules so they pass mypy cleanly +- Import ordering follows ruff's `I` rule (isort-compatible) ### C (`runtime/`) -- C11 を前提 -- インデントは 4-space -- ファイル単位で既存スタイルに揃えてください -- 公開 API は `runtime.h` に集約します -- ファイバー / スケジューラ周りの実装は、`docs/specs/05-fibers.md` および `docs/specs/06-scheduler-mn.md` を参照してください +- Assumes C11 +- Indentation is 4 spaces +- Match the existing style on a per-file basis +- Public APIs are consolidated in `runtime.h` +- For fiber / scheduler-related implementation, see `docs/specs/05-fibers.md` and `docs/specs/06-scheduler-mn.md` -## 8. ドキュメント +## 8. Documentation -- 言語仕様に関わる変更は `docs/specs/` 以下に新規 `.md` を追加するか、既存ファイルを更新してください -- 大きな変更を入れる前には、`docs/plans/YYYY-MM-DD-.md` に設計や計画を書き、それを起点に PR を出すと議論が進めやすいです +- For changes that touch the language spec, add a new `.md` under `docs/specs/` or update an existing file +- Before landing a large change, write down the design or plan in `docs/plans/YYYY-MM-DD-.md` and open a PR based on it; this makes discussion easier -## 9. issue を選ぶ +## 9. Choosing an issue -- `good first issue` ラベルが付いた issue は入門に向いています -- 着手する前に issue にコメントしてアサインを依頼してください。二重作業を避けるためです +- Issues labeled `good first issue` are a good place to get started +- Before you begin, comment on the issue to request assignment. This avoids duplicated work --- -ご質問や提案があれば、issue または PR で気軽に声をかけてください。 +If you have any questions or suggestions, feel free to reach out via an issue or a PR. diff --git a/SECURITY.md b/SECURITY.md index b5f847c..baba835 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,32 +1,32 @@ # Security Policy -## サポート対象のバージョン +## Supported Versions -最新の minor リリースのみセキュリティ修正の対象です。 +Only the latest minor release receives security fixes. | Version | Supported | | ------- | ------------------ | | 0.0.x | :white_check_mark: | -## 脆弱性の報告 +## Reporting a Vulnerability -セキュリティ上の問題を見つけた場合は、**公開 issue として報告しないでください**。代わりに次のいずれかで報告してください。 +If you find a security issue, **please do not report it as a public issue**. Instead, report it through one of the following channels. -1. (推奨) GitHub Security Advisory: +1. (Recommended) GitHub Security Advisory: https://github.com/ryuichi1208/rw/security/advisories/new -2. メールでの報告: `r_watanabe@ivry.jp` +2. By email: `r_watanabe@ivry.jp` -報告には次の情報を含めてください: +Please include the following information in your report: -- 脆弱性の概要と影響範囲 -- 再現手順 / PoC -- 影響を受けるバージョン -- 既知の緩和策(あれば) +- A summary of the vulnerability and its impact +- Reproduction steps / PoC +- Affected versions +- Any known mitigations (if applicable) -## 対応プロセス +## Response Process -- 受領: 7 日以内に確認のリプライ -- 評価: 14 日以内に深刻度の評価と対応方針の連絡 -- 修正: 重大度に応じて 30〜90 日以内にパッチをリリース予定 -- 公表: 修正リリース後、GitHub Security Advisory として公開 +- Acknowledgment: we will reply to confirm receipt within 7 days +- Assessment: we will share a severity assessment and remediation plan within 14 days +- Fix: depending on severity, we plan to release a patch within 30–90 days +- Disclosure: after the fix is released, we will publish it as a GitHub Security Advisory diff --git a/docs/plans/2026-05-21-string-builtins.md b/docs/plans/2026-05-21-string-builtins.md index 1a63ad0..05339ca 100644 --- a/docs/plans/2026-05-21-string-builtins.md +++ b/docs/plans/2026-05-21-string-builtins.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** rw 言語に最小の文字列操作 `len(s)` / `s == s` / `s + s` を追加し、後の netpoller 統合で echo server を書ける土台を作る。 +**Goal:** Add the minimal string operations `len(s)` / `s == s` / `s + s` to the rw language, laying the groundwork for writing an echo server once the netpoller is integrated later. -**Architecture:** ランタイムに C ABI ヘルパ 3 本 (`rw_str_len` / `rw_str_eq` / `rw_str_concat`) を追加し、Sema で組込み関数テーブルと二項演算子の型ルールを拡張、irgen でこれらを呼び出す IR を出す。公開 ABI (`rw_spawn_*`, `rw_str`) は不変。 +**Architecture:** Add three C ABI helpers to the runtime (`rw_str_len` / `rw_str_eq` / `rw_str_concat`), extend Sema's builtin-function table and the binary-operator type rules, and emit IR from irgen that calls these helpers. The public ABI (`rw_spawn_*`, `rw_str`) is unchanged. -**Tech Stack:** C11 (ランタイム)、Python 3.12 + llvmlite (コンパイラ)、pytest (テスト)。 +**Tech Stack:** C11 (runtime), Python 3.12 + llvmlite (compiler), pytest (tests). **Spec:** `docs/specs/07-string-builtins.md` @@ -16,29 +16,29 @@ | File | Responsibility | Action | |---|---|---| -| `runtime/runtime.h` | ABI 宣言 | 3 行追加 | -| `runtime/runtime.c` | 3 ヘルパ実装 | 関数 3 つ追加 | -| `runtime/fiber/test_str_ops.c` | C レベル単体テスト | 新規 | -| `runtime/Makefile` | (変更なし — `librw.a` は同じ OBJS) | — | -| `rwc/sema.py` | 組込み関数 / 演算子型ルール | 3 ヶ所修正 | -| `rwc/irgen.py` | ヘルパ呼び出し IR 生成 | 3 ヶ所修正 | -| `tests/test_sema.py` | 型検査の positive/negative | テスト追加 | -| `tests/test_e2e.py` | string_ops を parametrize に追加 | 1 行追加 | -| `examples/string_ops.rw` | 機能を 1 つの main で示すサンプル | 新規 | -| `examples/string_ops.rw.expected` | 期待出力 | 新規 | -| `.gitignore` | C テストバイナリ無視 | 1 行追加 | +| `runtime/runtime.h` | ABI declarations | Add 3 lines | +| `runtime/runtime.c` | 3 helper implementations | Add 3 functions | +| `runtime/fiber/test_str_ops.c` | C-level unit test | New | +| `runtime/Makefile` | (unchanged — `librw.a` uses the same OBJS) | — | +| `rwc/sema.py` | Builtin functions / operator type rules | 3 edits | +| `rwc/irgen.py` | Emit IR that calls the helpers | 3 edits | +| `tests/test_sema.py` | Positive/negative type-checking | Add tests | +| `tests/test_e2e.py` | Add string_ops to the parametrize list | Add 1 line | +| `examples/string_ops.rw` | Sample showing every feature in a single main | New | +| `examples/string_ops.rw.expected` | Expected output | New | +| `.gitignore` | Ignore the C test binary | Add 1 line | --- -## Task 1: ランタイムヘルパ 3 関数 +## Task 1: Three runtime helper functions **Files:** -- Modify: `runtime/runtime.h` (プロトタイプ追加) -- Modify: `runtime/runtime.c` (実装追加) +- Modify: `runtime/runtime.h` (add prototypes) +- Modify: `runtime/runtime.c` (add implementations) -- [ ] **Step 1.1: `runtime.h` に 3 プロトタイプを追加** +- [ ] **Step 1.1: Add the three prototypes to `runtime.h`** -`runtime/runtime.h` の `/* print */` ブロックの直下、`/* string helper */` セクションの末尾に追加: +Add them at the end of the `/* string helper */` section, immediately below the `/* print */` block in `runtime/runtime.h`: ```c /* string ops (Commit added by string-builtins PR) */ @@ -47,9 +47,9 @@ int8_t rw_str_eq (rw_str a, rw_str b); rw_str rw_str_concat(rw_str a, rw_str b); ``` -- [ ] **Step 1.2: `runtime.c` に実装を追加** +- [ ] **Step 1.2: Add the implementations to `runtime.c`** -`runtime/runtime.c` の `/* ---------- string helper ---------- */` セクションの直下、`/* ---------- lifecycle ---------- */` の上に追加: +Add them immediately below the `/* ---------- string helper ---------- */` section and above `/* ---------- lifecycle ---------- */` in `runtime/runtime.c`: ```c /* ---------- string ops ---------- */ @@ -85,13 +85,13 @@ rw_str rw_str_concat(rw_str a, rw_str b) { } ``` -- [ ] **Step 1.3: 既存 C テストが緑のまま動くか確認** +- [ ] **Step 1.3: Verify the existing C tests still pass** ```sh make -C runtime clean && make -C runtime ``` -Expected: `librw.a` がエラーなくビルドされる (新シンボル 3 つ追加、警告なし)。 +Expected: `librw.a` builds without errors (three new symbols added, no warnings). ```sh cd runtime @@ -100,9 +100,9 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_sched.c fiber/fi Expected: `total = 333833500` / `expected = 333833500`。 -- [ ] **Step 1.4: C テスト `test_str_ops.c` を書く** +- [ ] **Step 1.4: Write the C test `test_str_ops.c`** -`runtime/fiber/test_str_ops.c` を新規作成: +Create `runtime/fiber/test_str_ops.c`: ```c /* @@ -171,29 +171,29 @@ int main(void) { } ``` -- [ ] **Step 1.5: `.gitignore` に test_str_ops を追加** +- [ ] **Step 1.5: Add test_str_ops to `.gitignore`** -`/Users/ryuichi/ghq/github.com/ryuichi1208/rw/.gitignore` の `runtime/fiber/test_shutdown` の下に行追加: +Add a line below `runtime/fiber/test_shutdown` in `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/.gitignore`: ``` runtime/fiber/test_str_ops ``` -- [ ] **Step 1.6: ビルドして実行** +- [ ] **Step 1.6: Build and run** ```sh cd runtime && cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE fiber/test_str_ops.c runtime.o -o fiber/test_str_ops && ./fiber/test_str_ops ``` -Expected: `all str_ops tests passed`。 +Expected: `all str_ops tests passed`. -- [ ] **Step 1.7: 既存テスト一式が緑か確認** +- [ ] **Step 1.7: Verify the full existing test suite is green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q ``` -Expected: `66 passed`。 +Expected: `66 passed`. - [ ] **Step 1.8: Commit** @@ -224,22 +224,22 @@ EOF --- -## Task 2: コンパイラ (sema + irgen) を拡張 +## Task 2: Extend the compiler (sema + irgen) **Files:** - Modify: `rwc/sema.py` (lines around 290-345 = binop check, around 378 = call check) - Modify: `rwc/irgen.py` (lines around 68-71 = decls, 289-366 = binop, 368+ = call) - Modify: `tests/test_sema.py` -### Sema 側 +### Sema side -- [ ] **Step 2.1: Sema の現状を確認** +- [ ] **Step 2.1: Review the current state of Sema** -`rwc/sema.py:289-345` 付近に二項演算子の型チェックがある。`==` / `!=` の string 同士が `"string equality not supported in MVP"` で弾かれている (322 行付近)。`+` は int/float のみ許可。組込み関数は 378 行付近で `print` のみハードコード。 +There is a binary-operator type check around `rwc/sema.py:289-345`. `==` / `!=` between two strings is rejected with `"string equality not supported in MVP"` (around line 322). `+` is allowed only for int/float. Builtin functions are hardcoded around line 378 with `print` only. -- [ ] **Step 2.2: Sema の `+` / `==` / `!=` を string にも許可する** +- [ ] **Step 2.2: Allow `+` / `==` / `!=` on strings in Sema** -`rwc/sema.py` で「string equality not supported in MVP」のエラーを返している分岐 (sema.py:322 付近) を: +Take the branch in `rwc/sema.py` that returns the "string equality not supported in MVP" error (around sema.py:322): ```python # We could allow string equality later; disallow for MVP simplicity. @@ -249,7 +249,7 @@ EOF )) ``` -これを **削除** し、両辺 string の `==`/`!=` を bool として通すように直す。具体的には `==`/`!=` のハンドラを次の形に書き換える (前後文脈は実装時に sema.py を Read で開いて当てる): +**Delete** it and change the code so that `==`/`!=` between two strings passes as bool. Concretely, rewrite the `==`/`!=` handler into the following form (open sema.py with Read at implementation time to match the surrounding context): ```python # equality on int / float / bool / string @@ -262,7 +262,7 @@ EOF )) ``` -`+` のハンドラには新しい string 分岐を 1 つ追加: +Add one new string branch to the `+` handler: ```python if op == "+": @@ -278,9 +278,9 @@ EOF )) ``` -- [ ] **Step 2.3: Sema に `len` 組込みを追加** +- [ ] **Step 2.3: Add the `len` builtin to Sema** -`rwc/sema.py:378` の `if call.callee == "print":` ブロックの **直下** に、同じ高さで `len` の特例を追加: +Add a special case for `len` at the same level, immediately below the `if call.callee == "print":` block at `rwc/sema.py:378`: ```python if call.callee == "len": @@ -298,7 +298,7 @@ EOF return T.INT ``` -`spawn` 経路で `spawn len(x)` を禁止する分岐 (`if call.callee == "print":` のところと同じ場所、sema.py:352 付近) にも `len` を加える: +Also add `len` to the branch that forbids `spawn len(x)` in the `spawn` path (the same location as `if call.callee == "print":`, around sema.py:352): ```python if call.callee == "print": @@ -313,9 +313,9 @@ EOF )) ``` -- [ ] **Step 2.4: Sema 単体テスト (positive)** +- [ ] **Step 2.4: Sema unit tests (positive)** -`tests/test_sema.py` に追加する関数。既存ヘルパ (`analyze_str` のような形) があればそれを使う。ヘルパが無ければ次のようなものを追加: +Functions to add to `tests/test_sema.py`. Use an existing helper (something shaped like `analyze_str`) if one exists. If there is no helper, add something like this: ```python def _analyze(src: str) -> None: @@ -327,7 +327,7 @@ def _analyze(src: str) -> None: analyze(ast, filename="") ``` -その上で: +Then, on top of that: ```python def test_string_len_returns_int(): @@ -372,9 +372,9 @@ def main() -> int: """) ``` -- [ ] **Step 2.5: Sema 単体テスト (negative)** +- [ ] **Step 2.5: Sema unit tests (negative)** -`tests/test_sema.py` に追加: +Add to `tests/test_sema.py`: ```python import pytest @@ -422,21 +422,21 @@ def main() -> int: assert "len takes exactly 1 argument" in str(excinfo.value) ``` -- [ ] **Step 2.6: Sema テストを走らせる** +- [ ] **Step 2.6: Run the Sema tests** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest tests/test_sema.py -v 2>&1 | tail -20 ``` -Expected: 既存テスト + 新規 8 つ全部 PASS。 +Expected: the existing tests plus all 8 new ones PASS. -`CompileError` のフォーマットや既存 test_sema.py のスタイルが違ったら、それに合わせて補正する (`from rwc.diagnostics import CompileError` の import を追加する等)。`_analyze` ヘルパが既に test_sema.py にあれば再定義せずに使う。 +If the `CompileError` format or the style of the existing test_sema.py differs, adjust accordingly (e.g. add the `from rwc.diagnostics import CompileError` import). If the `_analyze` helper already exists in test_sema.py, use it rather than redefining it. -### irgen 側 +### irgen side -- [ ] **Step 2.7: irgen の外部関数宣言を 3 つ追加** +- [ ] **Step 2.7: Add the three external function declarations to irgen** -`rwc/irgen.py:68-71` の `self._rw_print_*` の宣言の **直下** に追加: +Add them immediately below the `self._rw_print_*` declarations at `rwc/irgen.py:68-71`: ```python self._rw_str_len = ir.Function( @@ -447,9 +447,9 @@ Expected: 既存テスト + 新規 8 つ全部 PASS。 m, ir.FunctionType(RW_STR_TY, [RW_STR_TY, RW_STR_TY]), "rw_str_concat") ``` -- [ ] **Step 2.8: irgen の `_emit_call` で `len` を扱う** +- [ ] **Step 2.8: Handle `len` in irgen's `_emit_call`** -`rwc/irgen.py:368` 付近の `_emit_call` で、`print` を処理しているブロックの **直下** に同じスタイルで: +In `_emit_call` around `rwc/irgen.py:368`, add the following in the same style immediately below the block that handles `print`: ```python if call.callee == "len": @@ -457,9 +457,9 @@ Expected: 既存テスト + 新規 8 つ全部 PASS。 return ctx.builder.call(self._rw_str_len, [v]) ``` -- [ ] **Step 2.9: irgen の `_emit_binop` で string ケースを扱う** +- [ ] **Step 2.9: Handle the string cases in irgen's `_emit_binop`** -`rwc/irgen.py:289` 付近の `_emit_binop` で、`+` / `==` / `!=` の処理に string 分岐を追加。`a.type == RW_STR_TY` または、Sema が付けた型情報を使って判別する (irgen.py を Read してどちらの方式が現状か確認してから当てる)。スケルトン: +In `_emit_binop` around `rwc/irgen.py:289`, add string branches to the handling of `+` / `==` / `!=`. Discriminate using `a.type == RW_STR_TY` or the type information attached by Sema (Read irgen.py first to confirm which approach is currently used before applying). Skeleton: ```python if op == "+": @@ -479,17 +479,17 @@ Expected: 既存テスト + 新規 8 つ全部 PASS。 ... ``` -実際の API 名 (`b.icmp_unsigned` か `b.icmp_signed` か、`not_` の存在等) は llvmlite の慣例に従う。既存 irgen.py で int の `==` 生成にどう書かれているかを見て、それに揃える。 +Follow llvmlite conventions for the actual API names (whether `b.icmp_unsigned` or `b.icmp_signed`, whether `not_` exists, etc.). Look at how the existing irgen.py emits int `==` and match that style. -- [ ] **Step 2.10: ビルドして既存 e2e が動くか確認** +- [ ] **Step 2.10: Build and verify the existing e2e tests pass** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 66 + 新規 sema 8 = 74 件全部 PASS。e2e は既存 7 件もそのまま緑。 +Expected: 66 + 8 new sema = 74 tests all PASS. The existing 7 e2e tests stay green as well. -- [ ] **Step 2.11: 単独で IR 生成して目視確認** +- [ ] **Step 2.11: Generate IR standalone and inspect it visually** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -507,9 +507,9 @@ EOF uv run rwc emit-ir /tmp/string_smoke.rw | grep -E "rw_str_(concat|eq|len)" ``` -Expected: 3 つの `call ... @rw_str_concat`, `@rw_str_eq`, `@rw_str_len` が出力に含まれる。 +Expected: the output contains the three calls `call ... @rw_str_concat`, `@rw_str_eq`, and `@rw_str_len`. -- [ ] **Step 2.12: 単独でビルド + 実行** +- [ ] **Step 2.12: Build and run standalone** ```sh uv run rwc run /tmp/string_smoke.rw @@ -559,7 +559,7 @@ EOF - Create: `examples/string_ops.rw.expected` - Modify: `tests/test_e2e.py` (parametrize list) -- [ ] **Step 3.1: `examples/string_ops.rw` を書く** +- [ ] **Step 3.1: Write `examples/string_ops.rw`** ```rw def main() -> int: @@ -576,7 +576,7 @@ def main() -> int: return 0 ``` -- [ ] **Step 3.2: `examples/string_ops.rw.expected` を書く** +- [ ] **Step 3.2: Write `examples/string_ops.rw.expected`** ``` hello, world @@ -585,9 +585,9 @@ eq ok neq ok ``` -(末尾改行ありで保存。`print` は自動改行を付けるので、行数と内容を一致させる。) +(Save with a trailing newline. `print` appends a newline automatically, so match the line count and contents accordingly.) -- [ ] **Step 3.3: 手元で実行して期待出力と一致するか確認** +- [ ] **Step 3.3: Run locally and confirm it matches the expected output** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -595,11 +595,11 @@ RW_WORKERS=1 uv run rwc run examples/string_ops.rw diff <(RW_WORKERS=1 uv run rwc run examples/string_ops.rw 2>&1) examples/string_ops.rw.expected ``` -Expected: `diff` の出力なし (バイト一致)。 +Expected: no `diff` output (byte-for-byte match). -- [ ] **Step 3.4: `tests/test_e2e.py` の parametrize に `string_ops` を追加** +- [ ] **Step 3.4: Add `string_ops` to the parametrize list in `tests/test_e2e.py`** -`tests/test_e2e.py` の以下の行 (37-40 付近): +Change the following lines in `tests/test_e2e.py` (around 37-40): ```python @pytest.mark.parametrize( @@ -608,7 +608,7 @@ Expected: `diff` の出力なし (バイト一致)。 ) ``` -を: +to: ```python @pytest.mark.parametrize( @@ -617,17 +617,15 @@ Expected: `diff` の出力なし (バイト一致)。 ) ``` -に変更。 - -- [ ] **Step 3.5: e2e テスト一式を走らせる** +- [ ] **Step 3.5: Run the full e2e test suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 全部緑 (66 + sema 新規 8 + e2e 新規 1 = 75 件)。 +Expected: all green (66 + 8 new sema + 1 new e2e = 75 tests). -- [ ] **Step 3.6: ランタイム単体テストも緑か確認** +- [ ] **Step 3.6: Verify the runtime unit tests are also green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime @@ -637,7 +635,7 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_sched.c fiber/fi cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE fiber/test_str_ops.c runtime.o -o fiber/test_str_ops && ./fiber/test_str_ops ``` -Expected: `total = 333833500` / `all str_ops tests passed`。 +Expected: `total = 333833500` / `all str_ops tests passed`. - [ ] **Step 3.7: Commit** @@ -663,22 +661,22 @@ EOF ## Self-Review -Spec カバレッジ確認: +Spec coverage check: -- `len(s) -> int` 組込み → Task 2.3 (sema) + 2.7-2.8 (irgen) + 2.4/2.5 (test) + Task 3 (e2e) +- `len(s) -> int` builtin → Task 2.3 (sema) + 2.7-2.8 (irgen) + 2.4/2.5 (test) + Task 3 (e2e) - `==` / `!=` for string → Task 2.2 (sema) + 2.9 (irgen) + 2.4-2.5 (test) + Task 3 (e2e) - `+` for string → Task 2.2 (sema) + 2.9 (irgen) + 2.4-2.5 (test) + Task 3 (e2e) -- ランタイムヘルパ 3 関数 → Task 1.1-1.2 + C unit test 1.4-1.6 -- 空文字列 / NULL ptr の扱い → Task 1.2 (concat の `len == 0` 分岐) + 1.4 (test_str_ops の empty ケース) -- 既存 e2e が壊れない → Task 2.10 + Task 3.5 で全件回す -- 公開 ABI 不変 → 新シンボル追加のみ、既存シンボル変更なし +- Three runtime helper functions → Task 1.1-1.2 + C unit test 1.4-1.6 +- Handling of empty string / NULL ptr → Task 1.2 (concat's `len == 0` branch) + 1.4 (empty cases in test_str_ops) +- Existing e2e tests don't break → run everything in Task 2.10 + Task 3.5 +- Public ABI unchanged → only new symbols added, no existing symbols modified -placeholder スキャン: 「TBD」「TODO」「(要確認)」「fill in」は plan 内に 0 件。 +Placeholder scan: zero occurrences of "TBD", "TODO", "(TBC)", or "fill in" in the plan. -type consistency: -- `rw_str_len` 引数 = `rw_str`、戻り値 = `int64_t` (= LLVM `I64`) — task 1.1, 1.2, 2.7 で一貫 -- `rw_str_eq` 引数 = `rw_str, rw_str`、戻り値 = `int8_t` (= LLVM `I8`) — 同上 -- `rw_str_concat` 引数 = `rw_str, rw_str`、戻り値 = `rw_str` (= LLVM `RW_STR_TY`) — 同上 -- Sema 内部の型定数 `T.STRING` / `T.INT` / `T.BOOL` の表記は task 2 全体で揃っている +Type consistency: +- `rw_str_len` arg = `rw_str`, return = `int64_t` (= LLVM `I64`) — consistent across tasks 1.1, 1.2, 2.7 +- `rw_str_eq` args = `rw_str, rw_str`, return = `int8_t` (= LLVM `I8`) — same +- `rw_str_concat` args = `rw_str, rw_str`, return = `rw_str` (= LLVM `RW_STR_TY`) — same +- The notation for Sema's internal type constants `T.STRING` / `T.INT` / `T.BOOL` is consistent across task 2 -リスク: irgen.py の `_emit_binop` の現状コード形 (どう型を判別しているか) は今は読まずに plan に書いたので、実装時に必ず Read で当てる。`isinstance(l.type, ir.LiteralStructType) and l.type == RW_STR_TY` の比較が等価で動くかは llvmlite 依存だが、既存 `rw_print_str` の呼び出し箇所と同じパターンに揃える前提。 +Risk: the current shape of `_emit_binop` in irgen.py (how it discriminates types) was written into the plan without reading it, so be sure to Read and match it at implementation time. Whether the comparison `isinstance(l.type, ir.LiteralStructType) and l.type == RW_STR_TY` works as an equality check is llvmlite-dependent, but the assumption is to match the pattern used at the existing `rw_print_str` call site. diff --git a/docs/plans/2026-05-22-bytes-type.md b/docs/plans/2026-05-22-bytes-type.md index 3345b79..705898b 100644 --- a/docs/plans/2026-05-22-bytes-type.md +++ b/docs/plans/2026-05-22-bytes-type.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** rw 言語に新しいプリミティブ型 `Bytes` を導入し、`len(b)` / `Bytes == Bytes` / `bytes_from_str` / `str_from_bytes` の最小 4 操作と `Future[Bytes]` を動かす。 +**Goal:** Introduce a new primitive type `Bytes` into the rw language, and get the minimal 4 operations `len(b)` / `Bytes == Bytes` / `bytes_from_str` / `str_from_bytes` plus `Future[Bytes]` working. -**Architecture:** LLVM IR レベルでは `Bytes` を既存 `string` と同じ `{i64 len, i8* ptr}` (= `RW_STR_TY`) として表現し、Sema 上だけで `T.BYTES` を別の型として区別する。ランタイムには新規関数を一切追加しない。 +**Architecture:** At the LLVM IR level, represent `Bytes` with the same `{i64 len, i8* ptr}` (= `RW_STR_TY`) as the existing `string`, and distinguish `T.BYTES` as a separate type only within Sema. Do not add any new runtime function. -**Tech Stack:** Python 3.12 + llvmlite (コンパイラ)、pytest (テスト)、C11 (ランタイム — 今回は読むだけ)。 +**Tech Stack:** Python 3.12 + llvmlite (compiler), pytest (tests), C11 (runtime — read-only this time). **Spec:** `docs/specs/08-bytes-type.md` @@ -16,59 +16,57 @@ | File | Responsibility | Action | |---|---|---| -| `rwc/types.py` | プリミティブ型定義 | `BYTES = _Primitive("Bytes")` 追加 | -| `rwc/lexer.py` | キーワード認識 | `KW_BYTES` + `KEYWORDS["Bytes"]` | -| `rwc/parser.py` | 型パース | `parse_type` の dict に 1 行 | -| `rwc/sema.py` | 型解決 + 組込み + 演算子拡張 | 4 ヶ所修正 | -| `rwc/irgen.py` | LLVM IR 生成 | 4 ヶ所修正 | -| `tests/test_sema.py` | 型検査 positive/negative | テスト追加 | -| `tests/test_e2e.py` | parametrize に bytes_basic を追加 | 1 行追加 | -| `examples/bytes_basic.rw` | 機能デモ | 新規 | -| `examples/bytes_basic.rw.expected` | 期待出力 | 新規 | - -ランタイム (`runtime/*`) と既存 fiber 関連には一切触れない。 +| `rwc/types.py` | Primitive type definitions | Add `BYTES = _Primitive("Bytes")` | +| `rwc/lexer.py` | Keyword recognition | `KW_BYTES` + `KEYWORDS["Bytes"]` | +| `rwc/parser.py` | Type parsing | 1 line in the `parse_type` dict | +| `rwc/sema.py` | Type resolution + builtins + operator extension | 4 changes | +| `rwc/irgen.py` | LLVM IR generation | 4 changes | +| `tests/test_sema.py` | Positive/negative type checking | Add tests | +| `tests/test_e2e.py` | Add bytes_basic to parametrize | 1 line added | +| `examples/bytes_basic.rw` | Feature demo | New | +| `examples/bytes_basic.rw.expected` | Expected output | New | + +Do not touch the runtime (`runtime/*`) or anything related to the existing fibers. --- -## Task 1: lexer / parser / types で `Bytes` 型名を認識 +## Task 1: Recognize the `Bytes` type name in lexer / parser / types -このタスクのゴールは「`b: Bytes = ...` の型注釈を parse + resolve できるようにする」だけ。`Bytes` を実際に使う組込みはまだ無いので、コード本体は Sema レベルで「`Bytes` という型はあるが、組み立てる手段はまだ無い」状態になる。 +The goal of this task is only to "make the `b: Bytes = ...` type annotation parse + resolve." Since there is no builtin that actually uses `Bytes` yet, the code ends up in a state at the Sema level where "the type `Bytes` exists, but there is still no way to construct it." **Files:** -- Modify: `rwc/types.py` (BYTES 追加) +- Modify: `rwc/types.py` (add BYTES) - Modify: `rwc/lexer.py` (KW_BYTES + KEYWORDS) - Modify: `rwc/parser.py` (parse_type dict) - Modify: `rwc/sema.py` (_resolve_type dict) -- [ ] **Step 1.1: `rwc/types.py` に `BYTES` プリミティブを追加** +- [ ] **Step 1.1: Add the `BYTES` primitive to `rwc/types.py`** -`rwc/types.py` の `VOID = _Primitive("void")` の **直下** に追加: +Add it **immediately below** `VOID = _Primitive("void")` in `rwc/types.py`: ```python BYTES = _Primitive("Bytes") ``` -`is_printable` / `is_numeric` には**含めない** (現状のままで OK)。 +Do **not** include it in `is_printable` / `is_numeric` (leaving them as-is is OK). -- [ ] **Step 1.2: `rwc/lexer.py` に `KW_BYTES` を追加** +- [ ] **Step 1.2: Add `KW_BYTES` to `rwc/lexer.py`** -`TokenKind` enum の `KW_STRING = auto()` の **直下** に: +Add **immediately below** `KW_STRING = auto()` in the `TokenKind` enum: ```python KW_BYTES = auto() ``` -を追加し、`KEYWORDS` dict の `"string": TokenKind.KW_STRING,` の **直下** に: +and add **immediately below** `"string": TokenKind.KW_STRING,` in the `KEYWORDS` dict: ```python "Bytes": TokenKind.KW_BYTES, ``` -を追加。 +- [ ] **Step 1.3: Make `parse_type` in `rwc/parser.py` recognize Bytes** -- [ ] **Step 1.3: `rwc/parser.py` の `parse_type` に Bytes を認識させる** - -`parse_type` メソッド内の `kind_to_name` dict (parser.py:154 付近) を以下に変更: +Change the `kind_to_name` dict inside the `parse_type` method (around parser.py:154) to the following: ```python kind_to_name = { @@ -81,9 +79,9 @@ BYTES = _Primitive("Bytes") } ``` -- [ ] **Step 1.4: `rwc/sema.py` の `_resolve_type` に Bytes を認識させる** +- [ ] **Step 1.4: Make `_resolve_type` in `rwc/sema.py` recognize Bytes** -`_resolve_type` 関数 (sema.py:39 付近) の `m` dict を以下に変更: +Change the `m` dict in the `_resolve_type` function (around sema.py:39) to the following: ```python m = { @@ -96,17 +94,17 @@ BYTES = _Primitive("Bytes") } ``` -- [ ] **Step 1.5: 既存テストが緑か確認** +- [ ] **Step 1.5: Confirm existing tests are green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `75 passed`。 +Expected: `75 passed`. -- [ ] **Step 1.6: `b: Bytes = ...` の型注釈だけが parse+resolve できることを確認するテストを追加** +- [ ] **Step 1.6: Add a test confirming that just the `b: Bytes = ...` type annotation can parse+resolve** -`tests/test_sema.py` の末尾に追加: +Add to the end of `tests/test_sema.py`: ```python def test_bytes_type_annotation_parses(): @@ -137,13 +135,13 @@ def test_unknown_type_name_still_errors(): assert "unknown type" in e.diagnostic.message ``` -- [ ] **Step 1.7: 上記テストを走らせる** +- [ ] **Step 1.7: Run the tests above** ```sh uv run pytest tests/test_sema.py::test_bytes_type_annotation_parses tests/test_sema.py::test_unknown_type_name_still_errors -v 2>&1 | tail -10 ``` -Expected: 両方 PASS。`takes_bytes` の引数型表現は `res.functions[...].params` の形式に依存するので、もし `params[0][1]` のアクセスで AttributeError が出るなら、既存テスト (`tests/test_sema.py` 内) で `params` の使われ方を確認して同じ形式に揃える。 +Expected: both PASS. The representation of `takes_bytes`'s parameter type depends on the format of `res.functions[...].params`, so if accessing `params[0][1]` raises an AttributeError, check how `params` is used in the existing tests (in `tests/test_sema.py`) and match that same format. - [ ] **Step 1.8: Commit** @@ -170,28 +168,28 @@ EOF --- -## Task 2: Sema + irgen で Bytes の操作を実装 +## Task 2: Implement Bytes operations in Sema + irgen -このタスクで以下を有効化する: +This task enables the following: -- `len(b: Bytes) -> int` (既存 len オーバーロード) +- `len(b: Bytes) -> int` (existing len overload) - `bytes_from_str(s: string) -> Bytes` - `str_from_bytes(b: Bytes) -> string` -- `Bytes == Bytes`, `Bytes != Bytes` (既存 string `==` ルートに乗せる) +- `Bytes == Bytes`, `Bytes != Bytes` (routed through the existing string `==` path) - `Future[Bytes]` (spawn / await) -`Bytes + Bytes` と `print(Bytes)` は禁止のまま (Sema エラー)。 +`Bytes + Bytes` and `print(Bytes)` remain forbidden (Sema error). **Files:** -- Modify: `rwc/sema.py` (_check_call で len 拡張 + bytes_from_str / str_from_bytes 追加、SpawnExpr 禁止リストに 2 つ追加) +- Modify: `rwc/sema.py` (extend len in _check_call + add bytes_from_str / str_from_bytes, add 2 entries to the SpawnExpr forbidden list) - Modify: `rwc/irgen.py` (llvm_type_of / _decl_spawn / _decl_await / _emit_binop / _emit_call) -- Modify: `tests/test_sema.py` (positive 5 + negative 5) +- Modify: `tests/test_sema.py` (5 positive + 5 negative) ### Sema -- [ ] **Step 2.1: Sema の `_check_call` で `len` の引数型を `string` または `Bytes` に拡張** +- [ ] **Step 2.1: In Sema's `_check_call`, extend the argument type of `len` to `string` or `Bytes`** -`sema.py:394` 付近の `if call.callee == "len":` ブロックの中で、引数型チェックを以下に変更: +Inside the `if call.callee == "len":` block (around `sema.py:394`), change the argument type check to the following: ```python # Builtin: len(string) -> int. (also len(Bytes) -> int) @@ -210,9 +208,9 @@ EOF return T.INT ``` -- [ ] **Step 2.2: Sema に `bytes_from_str` と `str_from_bytes` を追加** +- [ ] **Step 2.2: Add `bytes_from_str` and `str_from_bytes` to Sema** -`sema.py` の `len` 分岐の **直下** に追加: +Add **immediately below** the `len` branch in `sema.py`: ```python # Builtin: bytes_from_str(string) -> Bytes. @@ -245,9 +243,9 @@ EOF return T.STRING ``` -- [ ] **Step 2.3: Sema の `SpawnExpr` 禁止リストに 2 つ追加** +- [ ] **Step 2.3: Add 2 entries to Sema's `SpawnExpr` forbidden list** -`sema.py:352` 付近の `if call.callee == "print":` / `if call.callee == "len":` ブロックの **直後** に追加: +Add **immediately after** the `if call.callee == "print":` / `if call.callee == "len":` blocks (around `sema.py:352`): ```python if call.callee == "bytes_from_str": @@ -264,43 +262,41 @@ EOF ### irgen -- [ ] **Step 2.4: irgen の `llvm_type_of` に Bytes を追加** +- [ ] **Step 2.4: Add Bytes to `llvm_type_of` in irgen** -`rwc/irgen.py:40` 付近の `llvm_type_of` 関数で、`if t is T.STRING:` の **直下** に追加: +In the `llvm_type_of` function (around `rwc/irgen.py:40`), add **immediately below** `if t is T.STRING:`: ```python if t is T.BYTES: return RW_STR_TY ``` -- [ ] **Step 2.5: irgen の `_decl_spawn` / `_decl_await` で T.BYTES を string と同じ経路に** +- [ ] **Step 2.5: In irgen's `_decl_spawn` / `_decl_await`, route T.BYTES through the same path as string** -`rwc/irgen.py:93` 付近の `_decl_spawn` 内の `elif ret_ty is T.STRING:` を: +Change `elif ret_ty is T.STRING:` inside `_decl_spawn` (around `rwc/irgen.py:93`) to: ```python elif ret_ty is T.STRING or ret_ty is T.BYTES: name, ret_llvm = "rw_spawn_str", RW_STR_TY ``` -に変更。`_decl_await` 内の `elif ret_ty is T.STRING:` も同様に: +Similarly change `elif ret_ty is T.STRING:` inside `_decl_await` to: ```python elif ret_ty is T.STRING or ret_ty is T.BYTES: name, ret_llvm = "rw_await_str", RW_STR_TY ``` -に変更。 - -- [ ] **Step 2.6: irgen の `_emit_binop` で `==` / `!=` が Bytes を扱えるよう拡張** +- [ ] **Step 2.6: Extend irgen's `_emit_binop` so `==` / `!=` can handle Bytes** -`rwc/irgen.py:340` 付近の `_emit_binop` 内、`is_str = lty is T.STRING` を以下に変更: +Inside `_emit_binop` (around `rwc/irgen.py:340`), change `is_str = lty is T.STRING` to the following: ```python is_str = lty is T.STRING is_strlike = lty is T.STRING or lty is T.BYTES ``` -そして `_emit_binop` 内の `elif is_str and op in ("==", "!="):` ブロック (irgen.py 内、xor で `!=` を反転している箇所) の条件を `is_str` から `is_strlike` に変更: +Then change the condition of the `elif is_str and op in ("==", "!="):` block inside `_emit_binop` (in irgen.py, where `!=` is flipped with xor) from `is_str` to `is_strlike`: ```python elif is_strlike and op in ("==", "!="): @@ -310,13 +306,13 @@ EOF i1 = b.xor(i1, ir.Constant(ir.IntType(1), 1)) ``` -`+` の string 分岐 (`if is_str and op == "+":`) は **そのまま** で OK (= Bytes には連結が無いので、`is_str` のままで Bytes は素通り = arith op 経路で型エラー、最終的に `raise RuntimeError(f"arith op {op} on {lty}")` に落ちる)。だがそれは irgen が落ちるのでまずい — **Sema で Bytes + Bytes を弾く必要がある**。これは Step 2.2 までで `_check_expr` の `+` ハンドラを通った時点で「両辺同型なら BinOp 一般のチェックに進む → string 特例 → numeric チェック (Bytes は is_numeric=False) → "operator + requires int or float" エラー」で弾かれるはず。要確認 (Step 2.10 の negative テストで)。 +The string branch of `+` (`if is_str and op == "+":`) is **left as-is** and that's OK (= since Bytes has no concatenation, with `is_str` unchanged Bytes falls straight through = a type error on the arith op path, ultimately landing in `raise RuntimeError(f"arith op {op} on {lty}")`). But that is bad because irgen crashes — **Bytes + Bytes must be rejected in Sema**. Up through Step 2.2, once it passes through the `+` handler of `_check_expr`, it should be rejected via "if both sides are the same type, proceed to the general BinOp check → string special case → numeric check (Bytes has is_numeric=False) → 'operator + requires int or float' error." Needs verification (in the negative test of Step 2.10). -- [ ] **Step 2.7: irgen の `_emit_call` で `len`, `bytes_from_str`, `str_from_bytes` を扱う** +- [ ] **Step 2.7: Handle `len`, `bytes_from_str`, `str_from_bytes` in irgen's `_emit_call`** -`rwc/irgen.py:386` 付近の `if call.callee == "len":` ブロックは **そのまま** で OK (= 引数の SSA 値を `rw_str_len` に渡すだけ、引数の Sema 型が string でも Bytes でも IR レベルでは同じ `RW_STR_TY`)。 +The `if call.callee == "len":` block (around `rwc/irgen.py:386`) is **left as-is** and that's OK (= it just passes the argument's SSA value to `rw_str_len`; whether the argument's Sema type is string or Bytes, at the IR level it is the same `RW_STR_TY`). -`len` 分岐の **直下** に追加: +Add **immediately below** the `len` branch: ```python if call.callee in ("bytes_from_str", "str_from_bytes"): @@ -325,11 +321,11 @@ EOF return self._emit_expr(call.args[0], ctx) ``` -### テスト +### Tests -- [ ] **Step 2.8: Positive テストを追加** +- [ ] **Step 2.8: Add positive tests** -`tests/test_sema.py` の末尾に追加: +Add to the end of `tests/test_sema.py`: ```python def test_bytes_from_str_returns_bytes(): @@ -386,9 +382,9 @@ def test_future_bytes_ok(): check(src) ``` -- [ ] **Step 2.9: Negative テストを追加** +- [ ] **Step 2.9: Add negative tests** -`tests/test_sema.py` の末尾に続けて追加: +Continue adding to the end of `tests/test_sema.py`: ```python def test_print_bytes_is_type_error(): @@ -448,17 +444,17 @@ def test_cannot_spawn_bytes_from_str(): assert "cannot spawn the builtin `bytes_from_str`" in e.diagnostic.message ``` -- [ ] **Step 2.10: Sema テスト全件を走らせる** +- [ ] **Step 2.10: Run the full Sema test suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest tests/test_sema.py -v 2>&1 | tail -30 ``` -Expected: 既存 27 + Task 1 で追加した 2 + ここで追加した positive 5 + negative 5 = 39 件全部 PASS。 +Expected: existing 27 + 2 added in Task 1 + 5 positive added here + 5 negative = all 39 PASS. -`test_bytes_plus_bytes_is_type_error` で `+` のエラーが期待通り出るかは特に重要。`is_numeric(T.BYTES) == False` で `"operator + requires int or float"` メッセージが出るはず。出ない場合は `_check_expr` の BinOp 分岐 (`sema.py:298` 付近) を読み直して、Bytes が予想外のルートを通っていないか確認する。 +Whether `test_bytes_plus_bytes_is_type_error` produces the `+` error as expected is especially important. With `is_numeric(T.BYTES) == False`, the `"operator + requires int or float"` message should appear. If it does not, re-read the BinOp branch of `_check_expr` (around `sema.py:298`) and check whether Bytes is taking an unexpected route. -- [ ] **Step 2.11: 単独で IR 生成 + 実行で smoke check** +- [ ] **Step 2.11: Standalone IR generation + execution smoke check** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -477,7 +473,7 @@ echo "---" uv run rwc run /tmp/bytes_smoke.rw ``` -Expected: IR に `rw_str_len`, `rw_str_eq` の `call` が出る。実行結果: +Expected: the IR contains `call`s to `rw_str_len` and `rw_str_eq`. Execution result: ``` 5 @@ -526,11 +522,11 @@ EOF **Files:** - Create: `examples/bytes_basic.rw` - Create: `examples/bytes_basic.rw.expected` -- Modify: `tests/test_e2e.py` (parametrize に bytes_basic を追加) +- Modify: `tests/test_e2e.py` (add bytes_basic to parametrize) -- [ ] **Step 3.1: `examples/bytes_basic.rw` を書く** +- [ ] **Step 3.1: Write `examples/bytes_basic.rw`** -ファイル `examples/bytes_basic.rw`: +File `examples/bytes_basic.rw`: ```rw def main() -> int: @@ -543,9 +539,9 @@ def main() -> int: return 0 ``` -- [ ] **Step 3.2: `examples/bytes_basic.rw.expected` を書く** +- [ ] **Step 3.2: Write `examples/bytes_basic.rw.expected`** -ファイル `examples/bytes_basic.rw.expected`: +File `examples/bytes_basic.rw.expected`: ``` 5 @@ -553,42 +549,40 @@ eq ok hello ``` -(末尾改行ありで保存。) +(Save with a trailing newline.) -- [ ] **Step 3.3: 手元で実行して期待出力と一致するか確認** +- [ ] **Step 3.3: Run it locally and confirm it matches the expected output** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw diff <(RW_WORKERS=1 uv run rwc run examples/bytes_basic.rw 2>&1) examples/bytes_basic.rw.expected && echo OK ``` -Expected: `OK` だけが表示される (diff 出力なし)。 +Expected: only `OK` is printed (no diff output). -- [ ] **Step 3.4: `tests/test_e2e.py` の parametrize に `bytes_basic` を追加** +- [ ] **Step 3.4: Add `bytes_basic` to the parametrize in `tests/test_e2e.py`** -`tests/test_e2e.py:45` の以下の行: +Change the following line at `tests/test_e2e.py:45`: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops"], ``` -を: +to: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic"], ``` -に変更。 - -- [ ] **Step 3.5: 全 pytest を走らせる** +- [ ] **Step 3.5: Run the full pytest suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 既存 75 + Task 1 sema 新規 2 + Task 2 sema 新規 10 + Task 3 e2e 新規 1 = **88 件** 全緑。 +Expected: 75 existing + 2 new sema (Task 1) + 10 new sema (Task 2) + 1 new e2e (Task 3) = **88 tests** all green. -- [ ] **Step 3.6: 既存 example を回帰確認** +- [ ] **Step 3.6: Regression-check the existing examples** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -609,9 +603,9 @@ neq ok 30 ``` -- [ ] **Step 3.7: ランタイム単体テストも緑か確認** +- [ ] **Step 3.7: Confirm the runtime unit tests are also green** -ランタイムには手を入れていないが、念のため: +The runtime is untouched, but just to be safe: ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime @@ -620,7 +614,7 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_sched.c librw.a cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_str_ops.c librw.a -o fiber/test_str_ops && ./fiber/test_str_ops ``` -Expected: `total = 333833500` / `all str_ops tests passed`。 +Expected: `total = 333833500` / `all str_ops tests passed`. - [ ] **Step 3.8: Commit** @@ -651,31 +645,31 @@ EOF ### Spec coverage -| Spec 要求 | カバーするタスク | +| Spec requirement | Covering task | |---|---| -| プリミティブ型 Bytes (キーワード `Bytes`) | Task 1.1, 1.2, 1.3, 1.4 | -| 内部表現は string と共通 (`RW_STR_TY`) | Task 2.4 (llvm_type_of) | -| `len(b: Bytes) -> int` | Task 2.1 (sema), 2.7 (irgen 既存ルート流用) + test 2.8 + 2.10 | +| Primitive type Bytes (keyword `Bytes`) | Task 1.1, 1.2, 1.3, 1.4 | +| Internal representation shared with string (`RW_STR_TY`) | Task 2.4 (llvm_type_of) | +| `len(b: Bytes) -> int` | Task 2.1 (sema), 2.7 (irgen reuses the existing route) + test 2.8 + 2.10 | | `Bytes == Bytes`, `Bytes != Bytes` | Task 2.6 (irgen is_strlike) + test 2.8 + 2.10 | | `bytes_from_str(s: string) -> Bytes` | Task 2.2 (sema), 2.7 (irgen noop) + test 2.8, 2.9 | | `str_from_bytes(b: Bytes) -> string` | Task 2.2 (sema), 2.7 (irgen noop) + test 2.8 | -| `Future[Bytes]` で spawn/await | Task 2.5 + test `test_future_bytes_ok` (2.8) | -| `Bytes + Bytes` は禁止 | sema 既存ルートで自動的に numeric チェックに落ちる + test `test_bytes_plus_bytes_is_type_error` (2.9) | -| `print(Bytes)` は禁止 | `is_printable` を変えないことで自動的に弾かれる + test `test_print_bytes_is_type_error` (2.9) | -| Bytes と string の混合 `==` 禁止 | sema 既存「same type」チェックで自動 + test `test_bytes_eq_string_is_type_error` (2.9) | -| `spawn bytes_from_str(...)` 禁止 | Task 2.3 + test `test_cannot_spawn_bytes_from_str` (2.9) | -| ランタイムには手を入れない | Task 3.7 で C テスト緑を確認 | -| 既存テスト緑 | Task 3.5 (pytest 全件) + Task 3.6 (example 回帰) | +| spawn/await via `Future[Bytes]` | Task 2.5 + test `test_future_bytes_ok` (2.8) | +| `Bytes + Bytes` forbidden | Automatically falls into the numeric check via the existing sema route + test `test_bytes_plus_bytes_is_type_error` (2.9) | +| `print(Bytes)` forbidden | Automatically rejected by not changing `is_printable` + test `test_print_bytes_is_type_error` (2.9) | +| Mixed `==` between Bytes and string forbidden | Automatic via the existing sema "same type" check + test `test_bytes_eq_string_is_type_error` (2.9) | +| `spawn bytes_from_str(...)` forbidden | Task 2.3 + test `test_cannot_spawn_bytes_from_str` (2.9) | +| Do not touch the runtime | Confirmed by green C tests in Task 3.7 | +| Existing tests green | Task 3.5 (full pytest) + Task 3.6 (example regression) | -すべての spec 要求にタスクが割り当てられている。 +Every spec requirement is assigned to a task. -### Placeholder スキャン +### Placeholder scan -「TBD」「TODO」「(要確認)」「fill in」「Add appropriate」「Similar to Task N」は plan 内 0 件。Step 2.6 末尾の「要確認 (Step 2.10 の negative テストで)」は **検証手段が plan に明示されている** ので placeholder ではない (= 同じ commit 内で test を書いて確認するという指示)。 +"TBD", "TODO", "(to verify)", "fill in", "Add appropriate", and "Similar to Task N" appear 0 times in the plan. The "needs verification (in the negative test of Step 2.10)" note at the end of Step 2.6 is **not** a placeholder, because the means of verification is explicitly stated in the plan (i.e., the instruction is to write and confirm the test within the same commit). ### Type consistency -- `T.BYTES` の名前は Task 1.1, 1.4, 2.1, 2.2, 2.4, 2.5, 2.6, 2.7 で完全一致 -- LLVM 表現は `RW_STR_TY` で Task 2.4, 2.5 で揃っている -- 組込み関数名: `bytes_from_str` / `str_from_bytes` / `len` で Task 2 全体で揃っている -- ランタイム呼び出し: `rw_str_len` / `rw_str_eq` / `rw_spawn_str` / `rw_await_str` を新規追加なしで流用 (irgen 既存定義をそのまま呼ぶ) +- The name `T.BYTES` matches exactly across Tasks 1.1, 1.4, 2.1, 2.2, 2.4, 2.5, 2.6, 2.7 +- The LLVM representation is `RW_STR_TY`, consistent across Tasks 2.4 and 2.5 +- Builtin function names: `bytes_from_str` / `str_from_bytes` / `len` are consistent throughout Task 2 +- Runtime calls: `rw_str_len` / `rw_str_eq` / `rw_spawn_str` / `rw_await_str` are reused with no new additions (irgen calls the existing definitions as-is) diff --git a/docs/plans/2026-05-22-list-type.md b/docs/plans/2026-05-22-list-type.md index 9432a76..c48a34a 100644 --- a/docs/plans/2026-05-22-list-type.md +++ b/docs/plans/2026-05-22-list-type.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** rw 言語に `List[int]` プリミティブ型と 4 つの組込み (`list_new`, `list_push`, `list_at`, `len`) を導入し、`l: List[int] = list_new(); l = list_push(l, 5); print(list_at(l, 0))` のコードが動くようにする。 +**Goal:** Introduce a `List[int]` primitive type and four builtins (`list_new`, `list_push`, `list_at`, `len`) into the rw language so that code like `l: List[int] = list_new(); l = list_push(l, 5); print(list_at(l, 0))` works. -**Architecture:** `List[int]` は値型の 3 ワード fat struct `{i64 len, i64 cap, i64* data}` として LLVM IR レベルで表現し、ランタイムに 4 つの C ABI helper を追加。`list_push` は新しい配列を malloc + memcpy して新しい List を返す immutable な操作 (古い data は free しないリーク許容)。`Future[List[int]]` は今回 non-goal で Sema が禁止する。 +**Architecture:** `List[int]` is represented at the LLVM IR level as a by-value 3-word fat struct `{i64 len, i64 cap, i64* data}`, and four C ABI helpers are added to the runtime. `list_push` is an immutable operation that mallocs + memcpys into a new array and returns a new List (the old data is not freed, so leaks are tolerated). `Future[List[int]]` is a non-goal for now and is forbidden by Sema. -**Tech Stack:** C11 (ランタイム)、Python 3.12 + llvmlite (コンパイラ)、pytest (テスト)。 +**Tech Stack:** C11 (runtime), Python 3.12 + llvmlite (compiler), pytest (tests). **Spec:** `docs/specs/09-list-type.md` @@ -16,23 +16,23 @@ | File | Responsibility | Action | |---|---|---| -| `runtime/runtime.h` | ABI 宣言 | `rw_list_int` struct + 4 プロトタイプ追加 | -| `runtime/runtime.c` | 4 helper 実装 | 関数 4 つ追加 | -| `runtime/fiber/test_list_int.c` | C 単体テスト | 新規 | -| `.gitignore` | テストバイナリ | 1 行追加 | -| `rwc/types.py` | プリミティブ型定義 | `LIST_INT` 追加 | -| `rwc/lexer.py` | キーワード認識 | `KW_LIST` + `KEYWORDS["List"]` | -| `rwc/parser.py` | 型パース | `parse_type` に List 分岐 | -| `rwc/sema.py` | 型解決 + 組込み + Spawn 禁止 | 3 ヶ所修正 | -| `rwc/irgen.py` | LLVM IR 生成 | 4 ヶ所修正 | -| `tests/test_sema.py` | 型検査 positive/negative | テスト追加 | -| `tests/test_e2e.py` | parametrize | 1 行追加 | -| `examples/list_basic.rw` | デモ | 新規 | -| `examples/list_basic.rw.expected` | 期待出力 | 新規 | +| `runtime/runtime.h` | ABI declarations | Add `rw_list_int` struct + 4 prototypes | +| `runtime/runtime.c` | 4 helper implementations | Add 4 functions | +| `runtime/fiber/test_list_int.c` | C unit test | New | +| `.gitignore` | Test binary | Add 1 line | +| `rwc/types.py` | Primitive type definition | Add `LIST_INT` | +| `rwc/lexer.py` | Keyword recognition | `KW_LIST` + `KEYWORDS["List"]` | +| `rwc/parser.py` | Type parsing | Add List branch to `parse_type` | +| `rwc/sema.py` | Type resolution + builtins + spawn ban | 3 changes | +| `rwc/irgen.py` | LLVM IR generation | 4 changes | +| `tests/test_sema.py` | Type checking positive/negative | Add tests | +| `tests/test_e2e.py` | parametrize | Add 1 line | +| `examples/list_basic.rw` | Demo | New | +| `examples/list_basic.rw.expected` | Expected output | New | --- -## Task 1: ランタイムに `rw_list_int` 構造体と 4 関数を追加 +## Task 1: Add the `rw_list_int` struct and 4 functions to the runtime **Files:** - Modify: `runtime/runtime.h` @@ -40,9 +40,9 @@ - Create: `runtime/fiber/test_list_int.c` - Modify: `.gitignore` -- [ ] **Step 1.1: `runtime.h` に struct と 4 プロトタイプを追加** +- [ ] **Step 1.1: Add the struct and 4 prototypes to `runtime.h`** -`runtime/runtime.h` で `/* string ops */` セクションの直下、`/* spawn (one per return type) */` の前に追加: +In `runtime/runtime.h`, add this immediately below the `/* string ops */` section and before `/* spawn (one per return type) */`: ```c /* List[int] type and ops. */ @@ -58,9 +58,9 @@ int64_t rw_list_int_at (rw_list_int l, int64_t i); int64_t rw_list_int_len (rw_list_int l); ``` -- [ ] **Step 1.2: `runtime.c` に 4 関数の実装を追加** +- [ ] **Step 1.2: Add the implementations of the 4 functions to `runtime.c`** -`runtime.c` の `/* ---------- string ops ---------- */` セクションの **直下**、`/* ---------- lifecycle ---------- */` の前に追加: +In `runtime.c`, add this **immediately below** the `/* ---------- string ops ---------- */` section and before `/* ---------- lifecycle ---------- */`: ```c /* ---------- List[int] ops ---------- */ @@ -102,23 +102,23 @@ int64_t rw_list_int_len(rw_list_int l) { } ``` -- [ ] **Step 1.3: ランタイム再ビルドして既存テストが緑か確認** +- [ ] **Step 1.3: Rebuild the runtime and confirm existing tests are green** ```sh make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime clean make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime ``` -Expected: 警告なしビルド成功 (`librw.a` が生成される)。 +Expected: Build succeeds with no warnings (`librw.a` is generated). ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_sched.c librw.a -o fiber/test_sched && ./fiber/test_sched ``` -Expected: `total = 333833500` / `expected = 333833500`。 +Expected: `total = 333833500` / `expected = 333833500`. -- [ ] **Step 1.4: `test_list_int.c` を新規作成** +- [ ] **Step 1.4: Create `test_list_int.c`** `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime/fiber/test_list_int.c`: @@ -197,30 +197,30 @@ int main(void) { } ``` -- [ ] **Step 1.5: `.gitignore` にバイナリを追加** +- [ ] **Step 1.5: Add the binary to `.gitignore`** -`/Users/ryuichi/ghq/github.com/ryuichi1208/rw/.gitignore` の `runtime/fiber/test_str_ops` の **直下** に追加: +Add this **immediately below** `runtime/fiber/test_str_ops` in `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/.gitignore`: ``` runtime/fiber/test_list_int ``` -- [ ] **Step 1.6: ビルドして実行** +- [ ] **Step 1.6: Build and run** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_list_int.c librw.a -o fiber/test_list_int && ./fiber/test_list_int ``` -Expected: `all list_int tests passed`。 +Expected: `all list_int tests passed`. -- [ ] **Step 1.7: pytest 全件が緑か確認 (runtime のみの変更だが念のため)** +- [ ] **Step 1.7: Confirm the full pytest suite is green (runtime-only change, but just to be safe)** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `87 passed` (現状の数のまま、まだ List 関連 sema/irgen 変更はない)。 +Expected: `87 passed` (unchanged count, since there are no List-related sema/irgen changes yet). - [ ] **Step 1.8: Commit** @@ -254,7 +254,7 @@ EOF --- -## Task 2: lexer / parser / types で `List[int]` の型注釈を認識 +## Task 2: Recognize the `List[int]` type annotation in lexer / parser / types **Files:** - Modify: `rwc/types.py` @@ -263,33 +263,33 @@ EOF - Modify: `rwc/sema.py` - Modify: `tests/test_sema.py` -- [ ] **Step 2.1: `rwc/types.py` に `LIST_INT` を追加** +- [ ] **Step 2.1: Add `LIST_INT` to `rwc/types.py`** -`rwc/types.py` で `BYTES = _Primitive("Bytes")` の **直下** に追加: +Add this **immediately below** `BYTES = _Primitive("Bytes")` in `rwc/types.py`: ```python LIST_INT = _Primitive("List[int]") ``` -`is_printable` / `is_numeric` には**含めない** (= デフォルトのまま)。 +Do **not** include it in `is_printable` / `is_numeric` (i.e. leave them at their defaults). -- [ ] **Step 2.2: `rwc/lexer.py` に `KW_LIST` を追加** +- [ ] **Step 2.2: Add `KW_LIST` to `rwc/lexer.py`** -`TokenKind` enum の `KW_BYTES = auto()` の **直下** に: +**Immediately below** `KW_BYTES = auto()` in the `TokenKind` enum: ```python KW_LIST = auto() ``` -`KEYWORDS` dict の `"Bytes": TokenKind.KW_BYTES,` の **直下** に: +**Immediately below** `"Bytes": TokenKind.KW_BYTES,` in the `KEYWORDS` dict: ```python "List": TokenKind.KW_LIST, ``` -- [ ] **Step 2.3: `rwc/parser.py` の `parse_type` に List 分岐を追加** +- [ ] **Step 2.3: Add a List branch to `parse_type` in `rwc/parser.py`** -既存 `parse_type` メソッド内で、`Future` を扱う分岐 (parser.py:164-169) の **直下** に List 用の分岐を追加: +In the existing `parse_type` method, add a List branch **immediately below** the branch that handles `Future` (parser.py:164-169): ```python if t.kind == TokenKind.KW_LIST: @@ -306,9 +306,9 @@ LIST_INT = _Primitive("List[int]") return A.TypeName("List[int]", t.line, t.col) ``` -- [ ] **Step 2.4: `rwc/sema.py` の `_resolve_type` に `List[int]` を追加** +- [ ] **Step 2.4: Add `List[int]` to `_resolve_type` in `rwc/sema.py`** -`_resolve_type` 関数 (sema.py:39 付近) の `m` dict を以下に変更: +Change the `m` dict in the `_resolve_type` function (around sema.py:39) to the following: ```python m = { @@ -322,9 +322,9 @@ LIST_INT = _Primitive("List[int]") } ``` -- [ ] **Step 2.5: Sema positive テスト — 型注釈だけ parse + resolve できる** +- [ ] **Step 2.5: Sema positive test — the type annotation alone can be parsed + resolved** -`tests/test_sema.py` の末尾に追加: +Add this to the end of `tests/test_sema.py`: ```python def test_list_int_type_annotation_parses(): @@ -357,21 +357,21 @@ def test_list_with_non_int_param_is_parser_error(): assert "List[int]" in str(ei.value) or "only List" in str(ei.value) ``` -- [ ] **Step 2.6: pytest を走らせる** +- [ ] **Step 2.6: Run pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest tests/test_sema.py -q 2>&1 | tail -5 ``` -Expected: `40 passed` (現状 38 + 新規 2)。 +Expected: `40 passed` (current 38 + 2 new). -- [ ] **Step 2.7: 既存テスト全件も緑か確認** +- [ ] **Step 2.7: Confirm the full existing test suite is also green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `89 passed` (現状 87 + 新規 2)。 +Expected: `89 passed` (current 87 + 2 new). - [ ] **Step 2.8: Commit** @@ -402,7 +402,7 @@ EOF --- -## Task 3: Sema + irgen で `list_new` / `list_push` / `list_at` / `len(List[int])` を実装 +## Task 3: Implement `list_new` / `list_push` / `list_at` / `len(List[int])` in Sema + irgen **Files:** - Modify: `rwc/sema.py` @@ -411,9 +411,9 @@ EOF ### Sema -- [ ] **Step 3.1: `len` の引数型に `List[int]` を許可** +- [ ] **Step 3.1: Allow `List[int]` as an argument type to `len`** -`rwc/sema.py` の `len` ハンドラ (sema.py 内 `if call.callee == "len":` ブロック) を以下に変更: +Change the `len` handler in `rwc/sema.py` (the `if call.callee == "len":` block in sema.py) to the following: ```python # Builtin: len(string|Bytes|List[int]) -> int. @@ -432,9 +432,9 @@ EOF return T.INT ``` -- [ ] **Step 3.2: Sema に `list_new` / `list_push` / `list_at` を追加** +- [ ] **Step 3.2: Add `list_new` / `list_push` / `list_at` to Sema** -`rwc/sema.py` で `str_from_bytes` の組込みハンドラ (`if call.callee == "str_from_bytes":` ブロック) の **直下** に追加: +Add this **immediately below** the `str_from_bytes` builtin handler (the `if call.callee == "str_from_bytes":` block) in `rwc/sema.py`: ```python # Builtin: list_new() -> List[int]. @@ -487,9 +487,9 @@ EOF return T.INT ``` -- [ ] **Step 3.3: SpawnExpr の禁止リストに 3 つ追加** +- [ ] **Step 3.3: Add 3 entries to the SpawnExpr ban list** -`rwc/sema.py` の SpawnExpr 内、`if call.callee == "str_from_bytes":` の禁止分岐 の **直下** に追加: +Add this **immediately below** the `if call.callee == "str_from_bytes":` ban branch within SpawnExpr in `rwc/sema.py`: ```python if call.callee == "list_new": @@ -511,26 +511,26 @@ EOF ### irgen -- [ ] **Step 3.4: irgen に `RW_LIST_INT_TY` を定義** +- [ ] **Step 3.4: Define `RW_LIST_INT_TY` in irgen** -`rwc/irgen.py` 上部、`RW_STR_TY = ir.LiteralStructType(...)` の **直下** に追加: +Near the top of `rwc/irgen.py`, add this **immediately below** `RW_STR_TY = ir.LiteralStructType(...)`: ```python RW_LIST_INT_TY = ir.LiteralStructType([I64, I64, I64.as_pointer()]) # {len, cap, data*} ``` -- [ ] **Step 3.5: `llvm_type_of` に List[int] を追加** +- [ ] **Step 3.5: Add List[int] to `llvm_type_of`** -`rwc/irgen.py` の `llvm_type_of` で、`if t is T.BYTES:` 分岐 の **直下** に追加: +In `llvm_type_of` in `rwc/irgen.py`, add this **immediately below** the `if t is T.BYTES:` branch: ```python if t is T.LIST_INT: return RW_LIST_INT_TY ``` -- [ ] **Step 3.6: `_declare_runtime` で 4 関数を宣言** +- [ ] **Step 3.6: Declare the 4 functions in `_declare_runtime`** -`rwc/irgen.py` の `_declare_runtime` メソッド内、`self._rw_str_concat = ...` の宣言の **直下** に追加: +In the `_declare_runtime` method of `rwc/irgen.py`, add this **immediately below** the `self._rw_str_concat = ...` declaration: ```python # List[int] ops @@ -544,9 +544,9 @@ RW_LIST_INT_TY = ir.LiteralStructType([I64, I64, I64.as_pointer()]) # {len, cap m, ir.FunctionType(I64, [RW_LIST_INT_TY]), "rw_list_int_len") ``` -- [ ] **Step 3.7: `_emit_call` で List 組込みと `len(List[int])` を扱う** +- [ ] **Step 3.7: Handle the List builtins and `len(List[int])` in `_emit_call`** -`rwc/irgen.py` の `_emit_call` で、現状の `len` 分岐 (irgen.py:404 付近): +In `_emit_call` in `rwc/irgen.py`, the current `len` branch (around irgen.py:404): ```python if call.callee == "len": @@ -554,7 +554,7 @@ RW_LIST_INT_TY = ir.LiteralStructType([I64, I64, I64.as_pointer()]) # {len, cap return ctx.builder.call(self._rw_str_len, [v]) ``` -を、引数の Sema 型を見て分岐する以下の形に置き換える: +should be replaced with the following form, which branches on the argument's Sema type: ```python if call.callee == "len": @@ -566,7 +566,7 @@ RW_LIST_INT_TY = ir.LiteralStructType([I64, I64, I64.as_pointer()]) # {len, cap return ctx.builder.call(self._rw_str_len, [v]) ``` -その直下の `bytes_from_str` / `str_from_bytes` 分岐 の **直下** に List 組込み 3 つを追加: +Add the 3 List builtins **immediately below** the `bytes_from_str` / `str_from_bytes` branch that follows it: ```python if call.callee == "list_new": @@ -581,11 +581,11 @@ RW_LIST_INT_TY = ir.LiteralStructType([I64, I64, I64.as_pointer()]) # {len, cap return ctx.builder.call(self._rw_list_int_at, [lv, iv]) ``` -### テスト +### Tests -- [ ] **Step 3.8: Positive テストを追加** +- [ ] **Step 3.8: Add positive tests** -`tests/test_sema.py` の末尾に追加: +Add to the end of `tests/test_sema.py`: ```python # ---- List[int] builtin positive cases ---- @@ -631,9 +631,9 @@ def test_len_list_int_returns_int(): check(src) ``` -- [ ] **Step 3.9: Negative テストを追加** +- [ ] **Step 3.9: Add negative tests** -`tests/test_sema.py` の末尾に続けて追加: +Continue adding to the end of `tests/test_sema.py`: ```python # ---- List[int] negative cases ---- @@ -718,23 +718,24 @@ def test_cannot_spawn_list_new(): assert "cannot spawn the builtin `list_new`" in e.diagnostic.message ``` -注: `test_cannot_spawn_list_new` で `Future[List[int]]` の型注釈は **parser -が受理する** (内部 type が `List[int]`、Future の中身は何でも可)。Sema の -SpawnExpr 禁止リストに `list_new` が入っているので、`spawn` でエラーになる。 +Note: in `test_cannot_spawn_list_new`, the `Future[List[int]]` type annotation is +**accepted by the parser** (the inner type is `List[int]`, and the contents of +Future can be anything). Because `list_new` is on Sema's SpawnExpr ban list, the +`spawn` produces an error. -- [ ] **Step 3.10: pytest 全件を走らせる** +- [ ] **Step 3.10: Run the full pytest suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `100 passed` (現状 89 + Step 3.8 で 4 + Step 3.9 で 7 = 100)。 +Expected: `100 passed` (current 89 + 4 from Step 3.8 + 7 from Step 3.9 = 100). -`test_list_int_eq_list_int_is_type_error` のメッセージは現状の Sema が「内部 -比較の汎用エラー」を返すはずなので、`"=="` か `"compare"` が含まれるかを -asseert している。実際のメッセージが違ったら assertion を修正。 +The message in `test_list_int_eq_list_int_is_type_error` assumes the current Sema +returns a "generic comparison" error, so it asserts that `"=="` or `"compare"` is +contained. If the actual message differs, fix the assertion. -- [ ] **Step 3.11: 単独で smoke check** +- [ ] **Step 3.11: Standalone smoke check** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -756,9 +757,9 @@ uv run rwc run /tmp/list_smoke.rw Expected: -IR に 4 種類の `rw_list_int_*` の `declare` または `call` が含まれる。 +The IR contains `declare` or `call` for the 4 kinds of `rw_list_int_*`. -実行結果: +Execution result: ``` 3 10 @@ -806,11 +807,11 @@ EOF **Files:** - Create: `examples/list_basic.rw` - Create: `examples/list_basic.rw.expected` -- Modify: `tests/test_e2e.py` (parametrize に追加) +- Modify: `tests/test_e2e.py` (add to parametrize) -- [ ] **Step 4.1: `examples/list_basic.rw` を書く** +- [ ] **Step 4.1: Write `examples/list_basic.rw`** -ファイル `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/examples/list_basic.rw`: +File `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/examples/list_basic.rw`: ```rw def main() -> int: @@ -824,9 +825,9 @@ def main() -> int: return 0 ``` -- [ ] **Step 4.2: `examples/list_basic.rw.expected` を書く** +- [ ] **Step 4.2: Write `examples/list_basic.rw.expected`** -ファイル `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/examples/list_basic.rw.expected`: +File `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/examples/list_basic.rw.expected`: ``` 3 @@ -834,42 +835,40 @@ def main() -> int: 30 ``` -(末尾改行ありで保存。) +(Save with a trailing newline.) -- [ ] **Step 4.3: 手元で byte 一致を確認** +- [ ] **Step 4.3: Confirm a byte-for-byte match locally** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw diff <(RW_WORKERS=1 uv run rwc run examples/list_basic.rw 2>&1) examples/list_basic.rw.expected && echo OK ``` -Expected: `OK` だけが表示される。 +Expected: only `OK` is printed. -- [ ] **Step 4.4: `tests/test_e2e.py` の parametrize に `list_basic` を追加** +- [ ] **Step 4.4: Add `list_basic` to the parametrize list in `tests/test_e2e.py`** -`tests/test_e2e.py:45` の以下の行: +The following line at `tests/test_e2e.py:45`: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic"], ``` -を: +should be changed to: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic", "list_basic"], ``` -に変更。 - -- [ ] **Step 4.5: 全 pytest** +- [ ] **Step 4.5: Run the full pytest suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `101 passed` (現状 100 + e2e 新規 1)。 +Expected: `101 passed` (current 100 + 1 new e2e). -- [ ] **Step 4.6: 既存 example 回帰** +- [ ] **Step 4.6: Regression-check the existing examples** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -895,7 +894,7 @@ hello 30 ``` -- [ ] **Step 4.7: ランタイム単体テストも緑か確認** +- [ ] **Step 4.7: Confirm the runtime unit tests are green too** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime @@ -941,36 +940,37 @@ EOF ### Spec coverage -| Spec 要求 | カバーするタスク | +| Spec requirement | Covering tasks | |---|---| -| プリミティブ型 `List[int]` (キーワード `List`) | Task 2.1, 2.2, 2.3, 2.4 | -| 内部表現は 3 ワード fat struct | Task 1.1 (header), 3.4 (`RW_LIST_INT_TY`) | +| Primitive type `List[int]` (keyword `List`) | Task 2.1, 2.2, 2.3, 2.4 | +| Internal representation is a 3-word fat struct | Task 1.1 (header), 3.4 (`RW_LIST_INT_TY`) | | `list_new() -> List[int]` | Task 1.2 (runtime), 3.2 (sema), 3.6 / 3.7 (irgen), test 3.8, e2e 4.1 | | `list_push(List[int], int) -> List[int]` (immutable, malloc+memcpy) | Task 1.2, 3.2, 3.6 / 3.7, test 3.8, e2e 4.1 | -| `list_at(List[int], int) -> int` (範囲外 abort) | Task 1.2, 3.2, 3.6 / 3.7, test 3.8, e2e 4.1 | -| `len(List[int]) -> int` (既存 len オーバーロード) | Task 1.2, 3.1, 3.7, test 3.8, e2e 4.1 | -| List[T] の T が int 以外は parser エラー | Task 2.3, test 2.5 (`test_list_with_non_int_param_is_parser_error`) | -| `Future[List[int]]` 禁止 (Sema) | Task 3.3 + test `test_cannot_spawn_list_new` (3.9) | -| `print(List[int])` 禁止 | `is_printable` を変えないことで自動的に弾かれる + test (3.9) | -| `List + List` / `List == List` 禁止 | sema 既存 `+` / `==` の同型 numeric/printable チェックで自動 + test (3.9) | -| Immutable 不変条件 (push 後の元 List は変わらない) | Task 1.4 の `test_list_int.c` で `a` と `b` を別 ID で読む | -| 範囲外 abort | Task 1.2 (実装) — `at` がエラーメッセージを出す挙動。`fputs` でメッセージ確認は今回の test では行わない (実用上 abort が呼ばれることを確認すれば十分、これは別の専用テストが要るので non-goal) | -| 既存 87 テスト緑 | Task 1.7, 2.7, 3.10, 4.5 | -| 既存 example 回帰なし | Task 4.6 | - -すべての spec 要求にタスクがある。 - -### Placeholder スキャン - -「TBD」「TODO」「(要確認)」「fill in」「Add appropriate」「Similar to Task N」 -は plan 内 0 件。Step 3.10 末尾の「メッセージが違ったら assertion を修正」は -**plan で代替メッセージ (`==` か `compare`) を OR 条件で書いている** ので、 -placeholder ではない (両方の可能性を assert で許容済み)。 +| `list_at(List[int], int) -> int` (aborts out of range) | Task 1.2, 3.2, 3.6 / 3.7, test 3.8, e2e 4.1 | +| `len(List[int]) -> int` (overloads the existing len) | Task 1.2, 3.1, 3.7, test 3.8, e2e 4.1 | +| A List[T] whose T is not int is a parser error | Task 2.3, test 2.5 (`test_list_with_non_int_param_is_parser_error`) | +| `Future[List[int]]` forbidden (Sema) | Task 3.3 + test `test_cannot_spawn_list_new` (3.9) | +| `print(List[int])` forbidden | Automatically rejected by not changing `is_printable` + test (3.9) | +| `List + List` / `List == List` forbidden | Automatic via sema's existing same-type numeric/printable checks for `+` / `==` + test (3.9) | +| Immutability invariant (the original List is unchanged after a push) | `test_list_int.c` in Task 1.4 reads `a` and `b` under separate identities | +| Out-of-range abort | Task 1.2 (implementation) — `at` prints an error message and aborts. Verifying the message via `fputs` is not done in this test (in practice, confirming that abort is called is enough; that would need a separate dedicated test, so it is a non-goal) | +| Existing 87 tests green | Task 1.7, 2.7, 3.10, 4.5 | +| No regression in existing examples | Task 4.6 | + +Every spec requirement has a task. + +### Placeholder scan + +"TBD", "TODO", "(to be confirmed)", "fill in", "Add appropriate", and "Similar to +Task N" appear 0 times in the plan. The note at the end of Step 3.10 ("if the +message differs, fix the assertion") is not a placeholder, because **the plan +writes the alternative messages (`==` or `compare`) as an OR condition** (both +possibilities are already tolerated by the assert). ### Type consistency -- `T.LIST_INT` の名前は Task 2.1, 2.4, 3.1, 3.2, 3.5 で完全一致 -- LLVM 表現は `RW_LIST_INT_TY = LiteralStructType([I64, I64, I64.as_pointer()])` で Task 3.4, 3.5, 3.6 で揃っている -- 組込み関数名: `list_new` / `list_push` / `list_at` を Task 3.2 / 3.3 / 3.7 で揃って使用 -- ランタイム関数名: `rw_list_int_new` / `_push` / `_at` / `_len` を Task 1.1 (宣言), 1.2 (実装), 1.4 (test), 3.6 (irgen 宣言) で完全一致 -- Sema 型エラーメッセージのプレフィックス (`"list_push second argument must be int"`, `"list_at second argument must be int"`, `"list_new takes no arguments"`) は Sema 実装 (3.2) と negative テスト assert (3.9) で完全一致 +- The name `T.LIST_INT` matches exactly across Task 2.1, 2.4, 3.1, 3.2, 3.5 +- The LLVM representation `RW_LIST_INT_TY = LiteralStructType([I64, I64, I64.as_pointer()])` is consistent across Task 3.4, 3.5, 3.6 +- Builtin function names: `list_new` / `list_push` / `list_at` are used consistently across Task 3.2 / 3.3 / 3.7 +- Runtime function names: `rw_list_int_new` / `_push` / `_at` / `_len` match exactly across Task 1.1 (declaration), 1.2 (implementation), 1.4 (test), 3.6 (irgen declaration) +- The prefixes of the Sema type-error messages (`"list_push second argument must be int"`, `"list_at second argument must be int"`, `"list_new takes no arguments"`) match exactly between the Sema implementation (3.2) and the negative-test asserts (3.9) diff --git a/docs/plans/2026-05-22-option-type.md b/docs/plans/2026-05-22-option-type.md index 957b876..eccbe18 100644 --- a/docs/plans/2026-05-22-option-type.md +++ b/docs/plans/2026-05-22-option-type.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** rw 言語に `Option[int]` 型と最小 `match` 文 (Python 3.10 風) を追加し、`safe_div(10, 0)` のような関数が `None` を返して `match` で分解できるコードを動かす。あわせて `list_at_opt(l, i) -> Option[int]` をランタイムに追加して、範囲外を `abort` でなく `None` で返せるようにする。 +**Goal:** Add an `Option[int]` type and a minimal `match` statement (Python 3.10 style) to the rw language, so that code where a function like `safe_div(10, 0)` returns `None` and is destructured with `match` can run. Also add `list_at_opt(l, i) -> Option[int]` to the runtime so that out-of-range access returns `None` instead of `abort`. -**Architecture:** `Option[int]` は 2 ワード fat struct `{i64 tag, i64 payload}` (tag=1 = Some, 0 = None) として LLVM IR で表現。サイズ 16 バイトなので arm64 / x86_64 SysV どちらもレジスタで返せる (pointer-out ABI 不要)。新キーワード `Option` / `match` / `case` / `Some` / `None` を導入、parser は `match` を statement として 2 アーム必須でパースし、irgen は `switch` 命令で lowering する。 +**Architecture:** `Option[int]` is represented in LLVM IR as a two-word fat struct `{i64 tag, i64 payload}` (tag=1 = Some, 0 = None). At 16 bytes it can be returned in registers on both arm64 and x86_64 SysV (no pointer-out ABI needed). Introduce the new keywords `Option` / `match` / `case` / `Some` / `None`; the parser parses `match` as a statement requiring exactly two arms, and irgen lowers it with a `switch` instruction. -**Tech Stack:** C11 (ランタイム)、Python 3.12 + llvmlite (コンパイラ)、pytest (テスト)。 +**Tech Stack:** C11 (runtime), Python 3.12 + llvmlite (compiler), pytest (tests). **Spec:** `docs/specs/10-option-type.md` @@ -16,48 +16,48 @@ | File | Responsibility | Action | |---|---|---| -| `runtime/runtime.h` | ABI 宣言 | `rw_option_int` struct + `rw_list_int_at_opt` プロトタイプ | -| `runtime/runtime.c` | helper 実装 | `rw_list_int_at_opt` 追加 | -| `runtime/fiber/test_option.c` | C 単体テスト | 新規 | -| `.gitignore` | テストバイナリ | 1 行追加 | -| `rwc/types.py` | プリミティブ型定義 | `OPTION_INT` 追加 | -| `rwc/lexer.py` | キーワード認識 | `KW_OPTION` / `KW_MATCH` / `KW_CASE` / `KW_SOME` / `KW_NONE` | -| `rwc/parser.py` | 型 + 式 + 文のパース | `parse_type` の Option 分岐、Some/None 式、match 文 | -| `rwc/ast_nodes.py` | AST ノード | `SomeExpr` / `NoneExpr` / `MatchStmt` 追加 | -| `rwc/sema.py` | 型解決 + 式/文 Sema + return coverage | 4 ヶ所修正 | -| `rwc/irgen.py` | LLVM IR 生成 | `RW_OPTION_INT_TY` / Some/None/match の emit | -| `tests/test_sema.py` | 型検査 positive/negative | テスト追加 | -| `tests/test_e2e.py` | parametrize に option_basic | 1 行追加 | -| `examples/option_basic.rw` | デモ | 新規 | -| `examples/option_basic.rw.expected` | 期待出力 | 新規 | +| `runtime/runtime.h` | ABI declarations | `rw_option_int` struct + `rw_list_int_at_opt` prototype | +| `runtime/runtime.c` | helper implementation | Add `rw_list_int_at_opt` | +| `runtime/fiber/test_option.c` | C unit test | New | +| `.gitignore` | test binary | Add 1 line | +| `rwc/types.py` | primitive type definitions | Add `OPTION_INT` | +| `rwc/lexer.py` | keyword recognition | `KW_OPTION` / `KW_MATCH` / `KW_CASE` / `KW_SOME` / `KW_NONE` | +| `rwc/parser.py` | type + expression + statement parsing | Option branch in `parse_type`, Some/None expressions, match statement | +| `rwc/ast_nodes.py` | AST nodes | Add `SomeExpr` / `NoneExpr` / `MatchStmt` | +| `rwc/sema.py` | type resolution + expression/statement Sema + return coverage | 4 modifications | +| `rwc/irgen.py` | LLVM IR generation | Emit `RW_OPTION_INT_TY` / Some/None/match | +| `tests/test_sema.py` | positive/negative type checks | Add tests | +| `tests/test_e2e.py` | add option_basic to parametrize | Add 1 line | +| `examples/option_basic.rw` | demo | New | +| `examples/option_basic.rw.expected` | expected output | New | --- -## Task 1: lexer / parser / types / AST で `Option[int]` 構文を認識 +## Task 1: Recognize `Option[int]` syntax in lexer / parser / types / AST -このタスクのゴールは、`Option[int]` の型注釈、`Some(e)` / `None` の式、`match v: case Some(x): ... case None: ...` の文が **AST まで構築できる** こと。Sema / irgen は未実装のままで、`Some` を実際に評価しようとするとエラーになる。 +The goal of this task is to make the `Option[int]` type annotation, the `Some(e)` / `None` expressions, and the `match v: case Some(x): ... case None: ...` statement **buildable all the way to the AST**. Sema / irgen remain unimplemented, so actually trying to evaluate `Some` will error. **Files:** - Modify: `rwc/types.py` - Modify: `rwc/lexer.py` - Modify: `rwc/ast_nodes.py` - Modify: `rwc/parser.py` -- Modify: `rwc/sema.py` (`_resolve_type` だけ) +- Modify: `rwc/sema.py` (`_resolve_type` only) - Modify: `tests/test_sema.py` -- [ ] **Step 1.1: `rwc/types.py` に `OPTION_INT` を追加** +- [ ] **Step 1.1: Add `OPTION_INT` to `rwc/types.py`** -`rwc/types.py` で `LIST_INT = _Primitive("List[int]")` の **直下** に追加: +In `rwc/types.py`, add **directly below** `LIST_INT = _Primitive("List[int]")`: ```python OPTION_INT = _Primitive("Option[int]") ``` -`is_printable` / `is_numeric` には**含めない**。 +**Do not include** it in `is_printable` / `is_numeric`. -- [ ] **Step 1.2: `rwc/lexer.py` に 5 つのキーワードを追加** +- [ ] **Step 1.2: Add 5 keywords to `rwc/lexer.py`** -`TokenKind` enum の `KW_LIST = auto()` の **直下** に: +**Directly below** `KW_LIST = auto()` in the `TokenKind` enum: ```python KW_OPTION = auto() @@ -67,7 +67,7 @@ OPTION_INT = _Primitive("Option[int]") KW_NONE = auto() ``` -`KEYWORDS` dict の `"List": TokenKind.KW_LIST,` の **直下** に: +**Directly below** `"List": TokenKind.KW_LIST,` in the `KEYWORDS` dict: ```python "Option": TokenKind.KW_OPTION, @@ -77,9 +77,9 @@ OPTION_INT = _Primitive("Option[int]") "None": TokenKind.KW_NONE, ``` -- [ ] **Step 1.3: `rwc/ast_nodes.py` に新ノードを追加** +- [ ] **Step 1.3: Add the new nodes to `rwc/ast_nodes.py`** -`ast_nodes.py` を Read してファイル末尾の構造を確認 (`SpawnExpr` / `AwaitExpr` の定義場所を探す)。それらの **直後** に追加: +Read `ast_nodes.py` and check the structure at the end of the file (locate where `SpawnExpr` / `AwaitExpr` are defined). Add **directly after** them: ```python @dataclass @@ -100,14 +100,14 @@ class MatchStmt(Stmt): none_block: List[Stmt] ``` -`SomeExpr` / `NoneExpr` は `Expr` を継承、`MatchStmt` は `Stmt` を継承する。 -継承クラス名と既存 `Stmt`/`Expr` のフィールド (line, col 等の dataclass field -があるか) を Read で確認し、必要なら `line: int = 0`, `col: int = 0` を加える。 -**Read で見たフォーマットに合わせる**。 +`SomeExpr` / `NoneExpr` inherit from `Expr`, and `MatchStmt` inherits from `Stmt`. +Use Read to check the base class names and the fields of the existing `Stmt`/`Expr` +(whether there are dataclass fields such as line, col), and add `line: int = 0`, `col: int = 0` +if needed. **Match the format you saw with Read**. -- [ ] **Step 1.4: `rwc/parser.py` の `parse_type` に Option 分岐** +- [ ] **Step 1.4: Add an Option branch to `parse_type` in `rwc/parser.py`** -`parse_type` メソッド内、`List` 分岐の **直下** に追加: +In the `parse_type` method, add **directly below** the `List` branch: ```python if t.kind == TokenKind.KW_OPTION: @@ -124,11 +124,11 @@ class MatchStmt(Stmt): return A.TypeName("Option[int]", t.line, t.col) ``` -- [ ] **Step 1.5: `parser.py` で `Some(e)` と `None` を式として受ける** +- [ ] **Step 1.5: Accept `Some(e)` and `None` as expressions in `parser.py`** -primary expression を扱う場所 (リテラル / IDENT / `spawn` / `await` などを -処理しているメソッド) を Read で特定する。探す目印: `KW_TRUE` / `KW_FALSE` / -`KW_SPAWN` / `KW_AWAIT` の分岐。同じレベルに追加する形: +Use Read to locate where primary expressions are handled (the method that +processes literals / IDENT / `spawn` / `await`, etc.). Landmarks to look for: the +`KW_TRUE` / `KW_FALSE` / `KW_SPAWN` / `KW_AWAIT` branches. Add at the same level: ```python if t.kind == TokenKind.KW_SOME: @@ -144,20 +144,20 @@ primary expression を扱う場所 (リテラル / IDENT / `spawn` / `await` な return A.NoneExpr(line=kw.line, col=kw.col) ``` -`A.SomeExpr` / `A.NoneExpr` のコンストラクタが `line`/`col` を引数で受けるか -は Step 1.3 で決めた dataclass の形に依存する。`SpawnExpr` の生成方法と -揃える。 +Whether the `A.SomeExpr` / `A.NoneExpr` constructors take `line`/`col` as arguments +depends on the dataclass shape decided in Step 1.3. Match how `SpawnExpr` is +constructed. -- [ ] **Step 1.6: `parser.py` の `parse_stmt` に `match` 分岐** +- [ ] **Step 1.6: Add a `match` branch to `parse_stmt` in `parser.py`** -`parse_stmt` メソッド (parser.py:207 付近) の `if t.kind == TokenKind.KW_RETURN:` / `KW_IF:` / `KW_WHILE:` と同レベルに追加: +Add at the same level as `if t.kind == TokenKind.KW_RETURN:` / `KW_IF:` / `KW_WHILE:` in the `parse_stmt` method (around parser.py:207): ```python if t.kind == TokenKind.KW_MATCH: return self._parse_match() ``` -`_parse_match` メソッドを `parser.py` の `_parse_while` のすぐ後に追加: +Add the `_parse_match` method right after `_parse_while` in `parser.py`: ```python def _parse_match(self) -> A.MatchStmt: @@ -229,14 +229,14 @@ primary expression を扱う場所 (リテラル / IDENT / `spawn` / `await` な ) ``` -実際に rw の lexer が NEWLINE / INDENT / DEDENT をどう生成するか (parser が -`if` や `while` を読むときと同じシーケンスか) は parser.py:236-280 (`_parse_if` -/ `_parse_while`) を Read して合わせる。**この plan のスケルトンを必ず -既存 `_parse_if` のシーケンスに揃える**。 +How the rw lexer actually generates NEWLINE / INDENT / DEDENT (whether it is the +same sequence as when the parser reads `if` or `while`) should be matched by +reading parser.py:236-280 (`_parse_if` / `_parse_while`). **Be sure to align +this plan's skeleton with the existing `_parse_if` sequence**. -- [ ] **Step 1.7: `rwc/sema.py` の `_resolve_type` に `Option[int]` を追加** +- [ ] **Step 1.7: Add `Option[int]` to `_resolve_type` in `rwc/sema.py`** -`_resolve_type` 関数の `m` dict に 1 行追加: +Add 1 line to the `m` dict in the `_resolve_type` function: ```python m = { @@ -251,19 +251,19 @@ primary expression を扱う場所 (リテラル / IDENT / `spawn` / `await` な } ``` -- [ ] **Step 1.8: ビルドと既存テスト緑を確認** +- [ ] **Step 1.8: Confirm the build and existing tests are green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `101 passed`。Sema / irgen が新 AST を扱わないので、`Some(1)` を -**実際に使う** コードは Sema で fall through するが、まだそういうコードは -書いてない。 +Expected: `101 passed`. Since Sema / irgen do not handle the new AST, code that +**actually uses** `Some(1)` will fall through in Sema, but no such code has been +written yet. -- [ ] **Step 1.9: 型注釈と match だけ parse 通るテスト** +- [ ] **Step 1.9: Tests that only parse the type annotation and match** -`tests/test_sema.py` の末尾に追加: +Add to the end of `tests/test_sema.py`: ```python def test_option_int_type_annotation_parses(): @@ -311,17 +311,17 @@ def test_match_with_missing_arm_is_parser_error(): assert "must cover both" in str(ei.value) ``` -- [ ] **Step 1.10: pytest を走らせる** +- [ ] **Step 1.10: Run pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest tests/test_sema.py -q 2>&1 | tail -5 ``` -Expected: 既存 38 + 新規 3 = `41 passed`。`test_match_with_missing_arm` で -ParserError を期待しているが、parser が match の中で `Some(x)` の式 (= まだ -Sema 未実装) を許容する場合、別経路でエラーになる可能性がある。その場合は -代わりに `parse(tokenize(...))` で `ParserError` を期待するように形を -合わせる。 +Expected: existing 38 + 3 new = `41 passed`. `test_match_with_missing_arm` +expects a ParserError, but if the parser accepts a `Some(x)` expression inside +match (which Sema does not yet handle), the error may come from a different path. +In that case, adjust the shape to instead expect a `ParserError` from +`parse(tokenize(...))`. - [ ] **Step 1.11: Commit** @@ -353,21 +353,21 @@ EOF --- -## Task 2: Sema で `Some` / `None` / `MatchStmt` を型検査 +## Task 2: Type-check `Some` / `None` / `MatchStmt` in Sema -このタスクで Sema が新 AST を理解する。`Some(e)` の引数型、`MatchStmt` の -ターゲット型と bound 変数のスコープ、両 arm が return しているかの判定 -(return-coverage) を実装する。 +In this task Sema learns to understand the new AST. Implement the argument type +of `Some(e)`, the target type and bound-variable scope of `MatchStmt`, and the +determination of whether both arms return (return-coverage). **Files:** - Modify: `rwc/sema.py` - Modify: `tests/test_sema.py` -- [ ] **Step 2.1: Sema の `_check_expr` で `SomeExpr` / `NoneExpr` を扱う** +- [ ] **Step 2.1: Handle `SomeExpr` / `NoneExpr` in Sema's `_check_expr`** -`rwc/sema.py` の `_check_expr` メソッド (sema.py:298 付近、`isinstance(expr, -A.BinOp)` の分岐があるあたり) を Read。`isinstance(expr, A.SpawnExpr)` の -**直下** に追加: +Read the `_check_expr` method in `rwc/sema.py` (around sema.py:298, near the +`isinstance(expr, A.BinOp)` branch). Add **directly below** +`isinstance(expr, A.SpawnExpr)`: ```python if isinstance(expr, A.SomeExpr): @@ -382,12 +382,12 @@ A.BinOp)` の分岐があるあたり) を Read。`isinstance(expr, A.SpawnExpr) return T.OPTION_INT ``` -`expr.line` / `expr.col` のフィールド名は Step 1.3 の AST 定義に合わせる。 +Match the `expr.line` / `expr.col` field names to the AST definition from Step 1.3. -- [ ] **Step 2.2: Sema の `_check_stmt` で `MatchStmt` を扱う** +- [ ] **Step 2.2: Handle `MatchStmt` in Sema's `_check_stmt`** -`rwc/sema.py:153` 付近の `_check_stmt` メソッドを Read。`if isinstance(stmt, -A.WhileStmt):` の **直下** に追加: +Read the `_check_stmt` method around `rwc/sema.py:153`. Add **directly below** +`if isinstance(stmt, A.WhileStmt):`: ```python if isinstance(stmt, A.MatchStmt): @@ -407,13 +407,13 @@ A.WhileStmt):` の **直下** に追加: return some_ret and none_ret ``` -`_check_block` の戻り値は「block の末尾に到達せず return で抜けたら True」と -いう既存パターン。`if/elif/else` (sema.py:230 付近) で `then_ret` / -`else_ret` を使っている形に揃える。 +The return value of `_check_block` follows the existing convention: it is True +when the block exits via a `return` before reaching its end. Align this with how +`then_ret` / `else_ret` are used in `if/elif/else` (around sema.py:230). -- [ ] **Step 2.3: Positive テストを追加** +- [ ] **Step 2.3: Add positive tests** -`tests/test_sema.py` の末尾に追加: +Add to the end of `tests/test_sema.py`: ```python # ---- Option[int] positive cases ---- @@ -498,9 +498,9 @@ def test_match_terminates_via_both_arms_return(): check(src) ``` -- [ ] **Step 2.4: Negative テストを追加** +- [ ] **Step 2.4: Add negative tests** -`tests/test_sema.py` に続けて追加: +Continue adding to `tests/test_sema.py`: ```python # ---- Option[int] negative cases ---- @@ -555,27 +555,27 @@ def test_option_eq_is_type_error(): assert "compare" in e.diagnostic.message or "==" in e.diagnostic.message ``` -- [ ] **Step 2.5: pytest を走らせる** +- [ ] **Step 2.5: Run pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest tests/test_sema.py -q 2>&1 | tail -5 ``` -Expected: 既存 41 + positive 6 + negative 4 = `51 passed`。 -`test_match_terminates_via_both_arms_return` は return-coverage が match の -両 arm を見る実装が必要。失敗するなら Step 2.2 の `_check_stmt` の戻り値 -ロジックを再確認。 +Expected: existing 41 + 6 positive + 4 negative = `51 passed`. +`test_match_terminates_via_both_arms_return` requires the return-coverage +implementation to look at both arms of the match. If it fails, re-check the +return-value logic of `_check_stmt` in Step 2.2. -`test_print_option` のメッセージは現状の Sema が「print does not support -`Option[int]`」を返す前提。実際の文字列が違ったら assert を緩める。 +The message in `test_print_option` assumes the current Sema returns "print does +not support `Option[int]`". If the actual string differs, loosen the assert. -- [ ] **Step 2.6: 既存テスト一式も緑か** +- [ ] **Step 2.6: Confirm the full existing test suite is also green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 既存 101 + Task 1 で 3 + Task 2 で 10 = `114 passed`。 +Expected: existing 101 + 3 from Task 1 + 10 from Task 2 = `114 passed`. - [ ] **Step 2.7: Commit** @@ -610,24 +610,24 @@ EOF --- -## Task 3: ランタイム + irgen を実装し、Option 値が実行できるようにする +## Task 3: Implement the runtime + irgen so Option values can run -このタスクで Some / None / match / list_at_opt の IR を出せるようになる。 -ここで初めて rw コードが実際に Option を扱って動く。 +This task makes it possible to emit IR for Some / None / match / list_at_opt. +This is the first point where rw code actually runs while handling Option values. **Files:** - Modify: `runtime/runtime.h` - Modify: `runtime/runtime.c` - Create: `runtime/fiber/test_option.c` - Modify: `.gitignore` -- Modify: `rwc/sema.py` (`list_at_opt` の組込み追加) +- Modify: `rwc/sema.py` (add the `list_at_opt` builtin) - Modify: `rwc/irgen.py` ### Runtime -- [ ] **Step 3.1: `runtime.h` に `rw_option_int` struct と `rw_list_int_at_opt` プロトタイプ** +- [ ] **Step 3.1: Add the `rw_option_int` struct and `rw_list_int_at_opt` prototype to `runtime.h`** -`runtime/runtime.h` の `rw_list_int` struct の定義の **直下**、`rw_list_int_new` 系プロトタイプの前に追加: +In `runtime/runtime.h`, add this **immediately below** the definition of the `rw_list_int` struct, before the `rw_list_int_new` family of prototypes: ```c /* Option[int] type. Two-word fat struct: tag (0=None, 1=Some) + @@ -639,18 +639,18 @@ typedef struct { } rw_option_int; ``` -`rw_list_int_len` プロトタイプの **直下** に追加: +Add this **immediately below** the `rw_list_int_len` prototype: ```c /* List[int]: range-checked accessor returning Option[int]. */ void rw_list_int_at_opt(rw_option_int *out, const rw_list_int *l, int64_t i); ``` -`rw_list_int_at` は変更しない (`abort` のままで残す)。 +Leave `rw_list_int_at` unchanged (it still aborts). -- [ ] **Step 3.2: `runtime.c` に `rw_list_int_at_opt` 実装を追加** +- [ ] **Step 3.2: Add the `rw_list_int_at_opt` implementation to `runtime.c`** -`runtime/runtime.c` の `rw_list_int_len` の **直下** に追加: +Add this **immediately below** `rw_list_int_len` in `runtime/runtime.c`: ```c void rw_list_int_at_opt(rw_option_int *out, const rw_list_int *l, int64_t i) { @@ -664,7 +664,7 @@ void rw_list_int_at_opt(rw_option_int *out, const rw_list_int *l, int64_t i) { } ``` -- [ ] **Step 3.3: C 単体テスト `test_option.c` を新規作成** +- [ ] **Step 3.3: Create the C unit test `test_option.c`** `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime/fiber/test_option.c`: @@ -717,15 +717,15 @@ int main(void) { } ``` -- [ ] **Step 3.4: `.gitignore` にバイナリを追加** +- [ ] **Step 3.4: Add the binary to `.gitignore`** -`/Users/ryuichi/ghq/github.com/ryuichi1208/rw/.gitignore` の `runtime/fiber/test_list_int` の **直下** に: +Add this **immediately below** `runtime/fiber/test_list_int` in `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/.gitignore`: ``` runtime/fiber/test_option ``` -- [ ] **Step 3.5: ランタイムをビルドして C テストを走らせる** +- [ ] **Step 3.5: Build the runtime and run the C test** ```sh make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime clean @@ -734,9 +734,9 @@ cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_option.c librw.a -o fiber/test_option && ./fiber/test_option ``` -Expected: 警告なしビルド + `all option tests passed`。 +Expected: a warning-free build + `all option tests passed`. -- [ ] **Step 3.6: 既存 C テストが緑か念のため確認** +- [ ] **Step 3.6: Confirm the existing C tests are green, just to be safe** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime @@ -744,13 +744,13 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_list_int.c librw cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_sched.c librw.a -o fiber/test_sched && ./fiber/test_sched ``` -Expected: `all list_int tests passed` / `total = 333833500`。 +Expected: `all list_int tests passed` / `total = 333833500`. ### Sema for list_at_opt -- [ ] **Step 3.7: `list_at_opt` 組込みを Sema に追加** +- [ ] **Step 3.7: Add the `list_at_opt` builtin to Sema** -`rwc/sema.py` の `list_at` 組込み (`_check_call` 内) の **直下** に追加: +Add this **immediately below** the `list_at` builtin (within `_check_call`) in `rwc/sema.py`: ```python # Builtin: list_at_opt(List[int], int) -> Option[int]. @@ -775,7 +775,7 @@ Expected: `all list_int tests passed` / `total = 333833500`。 return T.OPTION_INT ``` -SpawnExpr 禁止リストに追加 (`list_at` 禁止分岐の隣): +Add to the SpawnExpr ban list (next to the `list_at` ban branch): ```python if call.callee == "list_at_opt": @@ -787,26 +787,26 @@ SpawnExpr 禁止リストに追加 (`list_at` 禁止分岐の隣): ### irgen -- [ ] **Step 3.8: irgen に `RW_OPTION_INT_TY` を定義** +- [ ] **Step 3.8: Define `RW_OPTION_INT_TY` in irgen** -`rwc/irgen.py` 上部、`RW_LIST_INT_TY = ...` の **直下** に: +Near the top of `rwc/irgen.py`, **immediately below** `RW_LIST_INT_TY = ...`: ```python RW_OPTION_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} ``` -- [ ] **Step 3.9: `llvm_type_of` に Option[int] を追加** +- [ ] **Step 3.9: Add Option[int] to `llvm_type_of`** -`llvm_type_of` で `if t is T.LIST_INT:` の **直下** に: +In `llvm_type_of`, **immediately below** `if t is T.LIST_INT:`: ```python if t is T.OPTION_INT: return RW_OPTION_INT_TY ``` -- [ ] **Step 3.10: `_declare_runtime` に `rw_list_int_at_opt` を追加** +- [ ] **Step 3.10: Add `rw_list_int_at_opt` to `_declare_runtime`** -`_declare_runtime` 内で List ops の宣言の **直下** に: +Inside `_declare_runtime`, **immediately below** the List ops declarations: ```python # Option[int] ops — pointer-out for the output struct, matching @@ -818,9 +818,9 @@ RW_OPTION_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} "rw_list_int_at_opt") ``` -- [ ] **Step 3.11: irgen の `_emit_expr` で `SomeExpr` / `NoneExpr` を扱う** +- [ ] **Step 3.11: Handle `SomeExpr` / `NoneExpr` in irgen's `_emit_expr`** -`_emit_expr` で既存の `if isinstance(expr, A.SpawnExpr):` 等と並列に追加: +In `_emit_expr`, add this alongside the existing branches such as `if isinstance(expr, A.SpawnExpr):`: ```python if isinstance(expr, A.SomeExpr): @@ -833,10 +833,10 @@ RW_OPTION_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} [ir.Constant(I64, 0), ir.Constant(I64, 0)]) ``` -- [ ] **Step 3.12: irgen の `_emit_stmt` で `MatchStmt` を扱う** +- [ ] **Step 3.12: Handle `MatchStmt` in irgen's `_emit_stmt`** -`_emit_stmt` (irgen.py:217 付近) の `if isinstance(stmt, A.WhileStmt):` の -**直下** に追加: +In `_emit_stmt` (around irgen.py:217), add this **immediately below** +`if isinstance(stmt, A.WhileStmt):`: ```python if isinstance(stmt, A.MatchStmt): @@ -844,7 +844,7 @@ RW_OPTION_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} return ``` -`_emit_match` を `_emit_while` の隣に追加: +Add `_emit_match` next to `_emit_while`: ```python def _emit_match(self, stmt: A.MatchStmt, ctx: "FunctionCtx") -> None: @@ -883,13 +883,13 @@ RW_OPTION_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} b.position_at_end(end_bb) ``` -`ctx.builder.block.is_terminated` の取得方法は llvmlite のバージョンに依存 -する。既存の `_emit_if` (もしあれば) でどう判定しているかを Read で確認し、 -同じ書き方に揃える。無ければ `b.block.is_terminated` を試して動かす。 +How to obtain `ctx.builder.block.is_terminated` depends on the llvmlite version. +Read the existing `_emit_if` (if any) to see how it makes this determination and +match that style. If there is none, try `b.block.is_terminated` and make it work. -- [ ] **Step 3.13: irgen の `_emit_call` で `list_at_opt` を扱う** +- [ ] **Step 3.13: Handle `list_at_opt` in irgen's `_emit_call`** -`_emit_call` 内、既存の `list_at` 分岐の **直下** に追加: +In `_emit_call`, add this **immediately below** the existing `list_at` branch: ```python if call.callee == "list_at_opt": @@ -904,7 +904,7 @@ RW_OPTION_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} ### Smoke -- [ ] **Step 3.14: 単独で IR + 実行 smoke** +- [ ] **Step 3.14: Standalone IR + execution smoke test** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -936,25 +936,25 @@ uv run rwc run /tmp/option_smoke.rw Expected: -IR に `switch`, `insertvalue`, `extractvalue` の各命令が含まれる。 +The IR contains the `switch`, `insertvalue`, and `extractvalue` instructions. -実行結果: +Execution result: ``` 5 -1 ``` -`exit != 0` (特に 139 = SIGSEGV) なら ABI 問題か match の lowering バグ。 -`Option[int]` は 16 バイトなので pointer-out 不要のはずだが、念のため -`llvm-ir-c-abi` skill の検証手順を参照。 +If `exit != 0` (especially 139 = SIGSEGV), it is either an ABI problem or a match +lowering bug. `Option[int]` is 16 bytes, so pointer-out should not be needed, but +just in case, refer to the verification steps in the `llvm-ir-c-abi` skill. -- [ ] **Step 3.15: pytest 全件** +- [ ] **Step 3.15: Run the full pytest suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 既存 114 + (このタスクでテスト追加なしなので) `114 passed`。 +Expected: existing 114 + (no tests added in this task) = `114 passed`. - [ ] **Step 3.16: Commit** @@ -1006,7 +1006,7 @@ EOF - Create: `examples/option_basic.rw.expected` - Modify: `tests/test_e2e.py` -- [ ] **Step 4.1: `examples/option_basic.rw` を書く** +- [ ] **Step 4.1: Write `examples/option_basic.rw`** `/Users/ryuichi/ghq/github.com/ryuichi1208/rw/examples/option_basic.rw`: @@ -1032,47 +1032,47 @@ def main() -> int: return 0 ``` -- [ ] **Step 4.2: `examples/option_basic.rw.expected` を書く** +- [ ] **Step 4.2: Write `examples/option_basic.rw.expected`** ``` 5 -1 ``` -(末尾改行ありで保存。) +(Save with a trailing newline.) -- [ ] **Step 4.3: 手元で byte 一致を確認** +- [ ] **Step 4.3: Confirm a byte-for-byte match locally** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw diff <(RW_WORKERS=1 uv run rwc run examples/option_basic.rw 2>&1) examples/option_basic.rw.expected && echo OK ``` -Expected: `OK` だけが表示される。 +Expected: only `OK` is printed. -- [ ] **Step 4.4: `tests/test_e2e.py` の parametrize に `option_basic` を追加** +- [ ] **Step 4.4: Add `option_basic` to the parametrize list in `tests/test_e2e.py`** -`tests/test_e2e.py:45` の以下の行: +The following line at `tests/test_e2e.py:45`: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic", "list_basic"], ``` -を以下に変更: +to the following: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic", "list_basic", "option_basic"], ``` -- [ ] **Step 4.5: 全 pytest** +- [ ] **Step 4.5: Run the full pytest suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 114 + 1 = `115 passed`。 +Expected: 114 + 1 = `115 passed`. -- [ ] **Step 4.6: 既存 example 回帰** +- [ ] **Step 4.6: Regression-check the existing examples** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -1103,7 +1103,7 @@ hello 30 ``` -- [ ] **Step 4.7: ランタイム単体テストも緑か** +- [ ] **Step 4.7: Confirm the runtime unit tests are green too** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime @@ -1114,7 +1114,7 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_sched.c librw.a ``` Expected: `all option tests passed` / `all list_int tests passed` / -`total = 333833500`。 +`total = 333833500`. - [ ] **Step 4.8: Commit** @@ -1146,38 +1146,43 @@ EOF ### Spec coverage -| Spec 要求 | カバーするタスク | +| Spec requirement | Covering tasks | |---|---| -| 新型 `Option[int]` (キーワード `Option`、parser が `[int]` 強制) | Task 1.1 / 1.2 / 1.4 | -| `Some(int) -> Option[int]` 式 | Task 1.5 (parser), 2.1 (sema), 3.11 (irgen) | -| `None` リテラル | Task 1.2 (lexer KW_NONE), 1.5 (parser), 2.1 (sema), 3.11 (irgen) | -| `match v: case Some(x): ... case None: ...` 構文 | Task 1.6 (parser), 2.2 (sema), 3.12 (irgen) | -| 2 アーム必須 (parser) | Task 1.6 (両 arm 必須チェック) + test 1.9 | -| Some arm の bound 変数が int | Task 2.2 (sema scope) + test 2.3 | -| return-coverage が match 両 arm を見る | Task 2.2 (`some_ret and none_ret`) + test 2.3 | -| `list_at_opt(List[int], int) -> Option[int]` 組込み | Task 3.1 / 3.2 (runtime), 3.7 (sema), 3.10 / 3.13 (irgen) | -| 範囲外で None を返す runtime helper | Task 3.2 + C test 3.3 | -| 内部表現は 16 バイト value 返し | Task 3.1 (struct), 3.8 (`RW_OPTION_INT_TY`) — pointer-out 不要 | -| `Option[string]` 等は parser エラー | Task 1.4 + test 1.9 | -| `match` の片側欠落は parser エラー | Task 1.6 + test 1.9 | -| `print(Option[int])` は型エラー | `is_printable` を変えないことで自動 + test 2.4 | -| `Option == Option` は型エラー | == whitelist を変えないことで自動 + test 2.4 | -| `Some(string)` は型エラー | Task 2.1 + test 2.4 | -| `match` ターゲットが Option[int] でないと型エラー | Task 2.2 + test 2.4 | -| `Future[Option[int]]` 禁止 | spec で記載、`_decl_spawn`/`_decl_await` に LIST_INT を追加しないのと同じく OPTION_INT も追加しない (Task 3 全体で「敢えて触らない」) | -| 既存 101 テスト緑 | Task 1.8, 2.6, 3.15, 4.5 | - -すべての spec 要求にタスクがある。 - -### Placeholder スキャン - -「TBD」「TODO」「(要確認)」「fill in」「Add appropriate」「Similar to Task N」は plan 内 0 件。Step 1.10 末尾の「ParserError を期待するように形を合わせる」、Step 2.5 の「実際の文字列が違ったら assert を緩める」、Step 3.12 の「llvmlite のバージョンに依存」はそれぞれ **代替手順を plan 内に明示している** ので、placeholder ではない。 +| New type `Option[int]` (keyword `Option`, parser enforces `[int]`) | Task 1.1 / 1.2 / 1.4 | +| `Some(int) -> Option[int]` expression | Task 1.5 (parser), 2.1 (sema), 3.11 (irgen) | +| `None` literal | Task 1.2 (lexer KW_NONE), 1.5 (parser), 2.1 (sema), 3.11 (irgen) | +| `match v: case Some(x): ... case None: ...` syntax | Task 1.6 (parser), 2.2 (sema), 3.12 (irgen) | +| Both arms required (parser) | Task 1.6 (mandatory-both-arms check) + test 1.9 | +| Some arm's bound variable is int | Task 2.2 (sema scope) + test 2.3 | +| Return-coverage looks at both match arms | Task 2.2 (`some_ret and none_ret`) + test 2.3 | +| `list_at_opt(List[int], int) -> Option[int]` builtin | Task 3.1 / 3.2 (runtime), 3.7 (sema), 3.10 / 3.13 (irgen) | +| Runtime helper returns None when out of range | Task 3.2 + C test 3.3 | +| Internal representation is a 16-byte value return | Task 3.1 (struct), 3.8 (`RW_OPTION_INT_TY`) — no pointer-out needed | +| `Option[string]` etc. is a parser error | Task 1.4 + test 1.9 | +| A missing `match` arm is a parser error | Task 1.6 + test 1.9 | +| `print(Option[int])` is a type error | Automatic by not changing `is_printable` + test 2.4 | +| `Option == Option` is a type error | Automatic by not changing the == whitelist + test 2.4 | +| `Some(string)` is a type error | Task 2.1 + test 2.4 | +| A `match` target that is not Option[int] is a type error | Task 2.2 + test 2.4 | +| `Future[Option[int]]` forbidden | Documented in the spec; just as LIST_INT is not added to `_decl_spawn` / `_decl_await`, OPTION_INT is likewise not added ("deliberately left untouched" throughout Task 3) | +| Existing 101 tests green | Task 1.8, 2.6, 3.15, 4.5 | + +Every spec requirement has a task. + +### Placeholder scan + +"TBD", "TODO", "(to be confirmed)", "fill in", "Add appropriate", and "Similar to +Task N" appear 0 times in the plan. The note at the end of Step 1.10 ("adjust the +shape to instead expect a ParserError"), Step 2.5 ("if the actual string differs, +loosen the assert"), and Step 3.12 ("depends on the llvmlite version") each +**explicitly spell out an alternative procedure within the plan**, so they are not +placeholders. ### Type consistency -- `T.OPTION_INT` は Task 1.1 / 1.7 / 2.1 / 2.2 / 3.7 / 3.9 で完全一致 -- LLVM 表現 `RW_OPTION_INT_TY = LiteralStructType([I64, I64])` は Task 3.8 / 3.9 / 3.10 / 3.11 / 3.12 / 3.13 で揃っている -- ランタイム関数: `rw_list_int_at_opt(out*, l*, i)` を Task 3.1 (宣言) / 3.2 (実装) / 3.3 (C test) / 3.10 (irgen 宣言) / 3.13 (irgen 呼び出し) で完全一致 -- AST ノード名: `SomeExpr` / `NoneExpr` / `MatchStmt` を Task 1.3 (定義) / 1.5 / 1.6 (parser) / 2.1 / 2.2 (sema) / 3.11 / 3.12 (irgen) で揃って使用 -- Sema 型エラーメッセージ: `"Some argument must be int"` / `"match target must be Option[int]"` / `"list_at_opt first argument must be List[int]"` / `"list_at_opt second argument must be int"` を実装 (Task 2.1, 2.2, 3.7) と negative テスト assert (Task 2.4) で完全一致 -- Match の Some arm bound 変数管理: Task 2.2 で `some_locals[stmt.some_var] = T.INT`、Task 3.12 で `ctx.locals[stmt.some_var] = slot`。同じフィールド名 `some_var` を一貫使用 +- `T.OPTION_INT` matches exactly across Task 1.1 / 1.7 / 2.1 / 2.2 / 3.7 / 3.9 +- The LLVM representation `RW_OPTION_INT_TY = LiteralStructType([I64, I64])` is consistent across Task 3.8 / 3.9 / 3.10 / 3.11 / 3.12 / 3.13 +- Runtime function: `rw_list_int_at_opt(out*, l*, i)` matches exactly across Task 3.1 (declaration) / 3.2 (implementation) / 3.3 (C test) / 3.10 (irgen declaration) / 3.13 (irgen call) +- AST node names: `SomeExpr` / `NoneExpr` / `MatchStmt` are used consistently across Task 1.3 (definition) / 1.5 / 1.6 (parser) / 2.1 / 2.2 (sema) / 3.11 / 3.12 (irgen) +- Sema type-error messages: `"Some argument must be int"` / `"match target must be Option[int]"` / `"list_at_opt first argument must be List[int]"` / `"list_at_opt second argument must be int"` match exactly between the implementation (Task 2.1, 2.2, 3.7) and the negative-test asserts (Task 2.4) +- Management of the Some arm's bound variable: `some_locals[stmt.some_var] = T.INT` in Task 2.2, `ctx.locals[stmt.some_var] = slot` in Task 3.12. The same field name `some_var` is used consistently diff --git a/docs/plans/2026-05-23-netpoller-tcp.md b/docs/plans/2026-05-23-netpoller-tcp.md index 3e9bfb9..06cb70d 100644 --- a/docs/plans/2026-05-23-netpoller-tcp.md +++ b/docs/plans/2026-05-23-netpoller-tcp.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** rw 言語に最小 TCP API (`tcp_listen` / `tcp_accept` / `tcp_read` / `tcp_write` / `tcp_close`) と、それを支える netpoller (kqueue/epoll) を追加し、`examples/tcp_echo.rw` が動く状態にする。 +**Goal:** Add a minimal TCP API (`tcp_listen` / `tcp_accept` / `tcp_read` / `tcp_write` / `tcp_close`) to the rw language, along with the netpoller (kqueue/epoll) that backs it, so that `examples/tcp_echo.rw` runs. -**Architecture:** 専用 netpoller pthread を 1 つ用意し、`kevent` / `epoll_wait` で fd readiness を ONESHOT モードで監視。fiber が `tcp_read` 等で EAGAIN を受けたら `rw_net_park_read(fd)` で netpoller に登録し WAITING、netpoller スレッドが ready 検知時に `enqueue_ready(fiber)` で起こす。main thread は fiber じゃないので `tcp_accept` を呼ぶと blocking accept で kernel sleep、worker M / netpoller は別 thread なので並行進行。 +**Architecture:** Run a single dedicated netpoller pthread that watches fd readiness in ONESHOT mode via `kevent` / `epoll_wait`. When a fiber gets EAGAIN from `tcp_read` and friends, it registers with the netpoller through `rw_net_park_read(fd)` and enters WAITING; when the netpoller thread detects readiness it wakes the fiber via `enqueue_ready(fiber)`. The main thread is not a fiber, so calling `tcp_accept` there does a blocking accept and the main thread sleeps in the kernel; the worker M's and the netpoller run on separate threads, so work proceeds concurrently. -**Tech Stack:** C11 (ランタイム、kqueue on macOS / epoll on Linux)、Python 3.12 + llvmlite (コンパイラ)、pytest (テスト)。 +**Tech Stack:** C11 (runtime; kqueue on macOS / epoll on Linux), Python 3.12 + llvmlite (compiler), pytest (tests). **Spec:** `docs/specs/12-netpoller-tcp.md` @@ -16,39 +16,39 @@ | File | Responsibility | Action | |---|---|---| -| `runtime/net/netpoller.h` | netpoller 共通 API | 新規 | -| `runtime/net/netpoller.c` | init / shutdown / park / 共通ロジック | 新規 | -| `runtime/net/netpoller_kqueue.c` | macOS 固有 (kevent) | 新規 | -| `runtime/net/netpoller_epoll.c` | Linux 固有 (epoll) | 新規 | -| `runtime/net/tcp.h` | TCP helper 宣言 | 新規 | -| `runtime/net/tcp.c` | TCP helper 実装 | 新規 | -| `runtime/runtime.h` | 5 つの tcp_* + 2 つの park プロトタイプ | 追加 | -| `runtime/runtime.c` | `rw_init` / `rw_shutdown` で netpoller 呼び出し | 変更 | -| `runtime/Makefile` | net/*.o + uname 分岐 | 変更 | -| `runtime/fiber/sched.h` | netpoller 向け sched API export | 変更 | -| `runtime/fiber/sched.c` | `rw_sched_enqueue_ready` / `rw_sched_current_fiber` を export | 変更 | -| `runtime/fiber/test_netpoller_pipe.c` | C 単体テスト (pipe) | 新規 | -| `runtime/fiber/test_tcp_loopback.c` | C 単体テスト (localhost) | 新規 | -| `.gitignore` | test バイナリ無視 | 追加 | -| `rwc/sema.py` | 5 組込み + spawn 禁止 | 変更 | -| `rwc/irgen.py` | 5 組込み emit | 変更 | -| `tests/test_sema.py` | positive 5 + negative 5 | 追加 | -| `tests/test_e2e_tcp.py` | Python から socket で echo を検証 | 新規 | -| `examples/tcp_echo.rw` | echo server デモ | 新規 | +| `runtime/net/netpoller.h` | Shared netpoller API | New | +| `runtime/net/netpoller.c` | init / shutdown / park / shared logic | New | +| `runtime/net/netpoller_kqueue.c` | macOS-specific (kevent) | New | +| `runtime/net/netpoller_epoll.c` | Linux-specific (epoll) | New | +| `runtime/net/tcp.h` | TCP helper declarations | New | +| `runtime/net/tcp.c` | TCP helper implementation | New | +| `runtime/runtime.h` | The five tcp_* + two park prototypes | Add | +| `runtime/runtime.c` | Call the netpoller from `rw_init` / `rw_shutdown` | Modify | +| `runtime/Makefile` | net/*.o + uname branching | Modify | +| `runtime/fiber/sched.h` | Export sched API for the netpoller | Modify | +| `runtime/fiber/sched.c` | Export `rw_sched_enqueue_ready` / `rw_sched_current_fiber` | Modify | +| `runtime/fiber/test_netpoller_pipe.c` | C unit test (pipe) | New | +| `runtime/fiber/test_tcp_loopback.c` | C unit test (localhost) | New | +| `.gitignore` | Ignore test binaries | Add | +| `rwc/sema.py` | Five builtins + spawn prohibition | Modify | +| `rwc/irgen.py` | Emit the five builtins | Modify | +| `tests/test_sema.py` | 5 positive + 5 negative | Add | +| `tests/test_e2e_tcp.py` | Verify echo over a socket from Python | New | +| `examples/tcp_echo.rw` | Echo server demo | New | --- -## Task 1: sched API を netpoller 向けに export +## Task 1: Export the sched API for the netpoller -netpoller スレッドから fiber を起こすため、sched.c の `enqueue_ready` と `tls_m->current` を外部に公開する。 +So the netpoller thread can wake fibers, expose sched.c's `enqueue_ready` and `tls_m->current` externally. **Files:** - Modify: `runtime/fiber/sched.h` - Modify: `runtime/fiber/sched.c` -- [ ] **Step 1.1: `runtime/fiber/sched.h` に新 API を追加** +- [ ] **Step 1.1: Add the new API to `runtime/fiber/sched.h`** -ファイル末尾の `#ifdef __cplusplus ... #endif` の **直前** に追加: +Add it **immediately before** the trailing `#ifdef __cplusplus ... #endif` at the end of the file: ```c /* ---- Exported for the netpoller (runtime/net/netpoller.c) ---- */ @@ -62,9 +62,9 @@ void rw_sched_enqueue_ready(rw_fiber_handle *h); rw_fiber_handle *rw_sched_current_fiber(void); ``` -- [ ] **Step 1.2: `runtime/fiber/sched.c` に export 関数を追加** +- [ ] **Step 1.2: Add the export functions to `runtime/fiber/sched.c`** -`enqueue_ready` (sched.c:206 付近、static) のすぐ下に export 関数を追加: +Add the export functions right below `enqueue_ready` (near sched.c:206, static): ```c /* Public wrapper for rw_sched_enqueue_ready (see sched.h). */ @@ -79,20 +79,20 @@ rw_fiber_handle *rw_sched_current_fiber(void) { } ``` -- [ ] **Step 1.3: ビルドと既存テストが緑か確認** +- [ ] **Step 1.3: Confirm the build and the existing tests are green** ```sh make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime clean make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime ``` -Expected: 警告なしビルド成功 (`librw.a` 生成)。 +Expected: Warning-free successful build (`librw.a` produced). ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `131 passed`。 +Expected: `131 passed`. - [ ] **Step 1.4: Commit** @@ -120,9 +120,9 @@ EOF --- -## Task 2: netpoller スケルトン (init/shutdown + 共通ヘッダ) +## Task 2: netpoller skeleton (init/shutdown + shared header) -netpoller スレッドの起動・停止だけを実装する。park / wake はまだ動かない。 +Implement only the startup and shutdown of the netpoller thread. park / wake does not work yet. **Files:** - Create: `runtime/net/netpoller.h` @@ -133,7 +133,7 @@ netpoller スレッドの起動・停止だけを実装する。park / wake は - Modify: `runtime/runtime.h` - Modify: `runtime/runtime.c` -- [ ] **Step 2.1: `runtime/net/netpoller.h` を新規作成** +- [ ] **Step 2.1: Create `runtime/net/netpoller.h`** ```c #ifndef RW_NETPOLLER_H @@ -185,7 +185,7 @@ int rw_netpoller_register_write(int fd, rw_fiber_handle *h); #endif /* RW_NETPOLLER_H */ ``` -- [ ] **Step 2.2: `runtime/net/netpoller.c` を新規作成** +- [ ] **Step 2.2: Create `runtime/net/netpoller.c`** ```c /* @@ -279,9 +279,9 @@ void rw_net_park_write(int fd) { } ``` -注: `rw_sched_park_current` は Task 3 で sched.c に追加する。 +Note: `rw_sched_park_current` is added to sched.c in Task 3. -- [ ] **Step 2.3: `runtime/net/netpoller_kqueue.c` を新規作成** +- [ ] **Step 2.3: Create `runtime/net/netpoller_kqueue.c`** ```c /* @@ -359,7 +359,7 @@ int rw_netpoller_register_write(int fd, rw_fiber_handle *h) { #endif /* __APPLE__ || __FreeBSD__ */ ``` -- [ ] **Step 2.4: `runtime/net/netpoller_epoll.c` を新規作成** +- [ ] **Step 2.4: Create `runtime/net/netpoller_epoll.c`** ```c /* @@ -454,9 +454,9 @@ int rw_netpoller_register_write(int fd, rw_fiber_handle *h) { #endif /* __linux__ */ ``` -- [ ] **Step 2.5: `runtime/Makefile` を更新** +- [ ] **Step 2.5: Update `runtime/Makefile`** -`OBJS` 行を更新し、新しい .o を追加: +Update the `OBJS` line to add the new .o files: ```makefile OBJS := runtime.o \ @@ -465,7 +465,7 @@ OBJS := runtime.o \ $(FIBER_ASM) ``` -`UNAME_M` ブロックの **直下** に platform 分岐を追加: +Add platform branching **directly below** the `UNAME_M` block: ```makefile UNAME_S := $(shell uname -s) @@ -478,7 +478,7 @@ else endif ``` -各 .o 用のルールを既存ルール (`fiber/park.o`) のすぐ下に追加: +Add a rule for each .o right below the existing rule (`fiber/park.o`): ```makefile net/netpoller.o: net/netpoller.c net/netpoller.h fiber/sched.h @@ -494,9 +494,9 @@ net/tcp.o: net/tcp.c net/tcp.h net/netpoller.h fiber/sched.h $(CC) $(CFLAGS) -c $< -o $@ ``` -- [ ] **Step 2.6: `runtime/runtime.h` にプロトタイプ追加** +- [ ] **Step 2.6: Add prototypes to `runtime/runtime.h`** -`/* List[int] type and ops */` ブロックのすぐ下、`/* spawn (one per return type) */` の上に追加: +Add them right below the `/* List[int] type and ops */` block and above `/* spawn (one per return type) */`: ```c /* TCP API (runtime/net/tcp.c). */ @@ -507,7 +507,7 @@ int64_t rw_tcp_write (int64_t fd, rw_str b); int64_t rw_tcp_close (int64_t fd); ``` -- [ ] **Step 2.7: `runtime/runtime.c` の `rw_init` / `rw_shutdown` に呼び出し追加** +- [ ] **Step 2.7: Add the calls to `rw_init` / `rw_shutdown` in `runtime/runtime.c`** `rw_init`: @@ -527,28 +527,28 @@ void rw_shutdown(void) { } ``` -`runtime.c` 先頭の `#include` ブロックに追加: +Add to the `#include` block at the top of `runtime.c`: ```c #include "net/netpoller.h" ``` -- [ ] **Step 2.8: ビルドが緑か確認** +- [ ] **Step 2.8: Confirm the build is green** ```sh make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime clean make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime ``` -Expected: 警告なしビルド成功。`librw.a` に `net/netpoller.o` 等が含まれる。 +Expected: Warning-free successful build. `librw.a` includes `net/netpoller.o` and the others. -- [ ] **Step 2.9: 既存テスト緑か確認** +- [ ] **Step 2.9: Confirm the existing tests are green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `131 passed`。netpoller スレッドは init/shutdown のみで実害無し。 +Expected: `131 passed`. The netpoller thread only does init/shutdown, so there is no harm. - [ ] **Step 2.10: Commit** @@ -584,19 +584,19 @@ EOF --- -## Task 3: park / wake 完成 + pipe テスト +## Task 3: Complete park / wake + pipe test -netpoller スレッドが既に動いているので、fiber を park する API を完成させ、ready 通知で fiber が起きることを C テストで確認する。 +The netpoller thread is already running, so complete the API that parks a fiber and use a C test to confirm the fiber wakes on a ready notification. **Files:** -- Modify: `runtime/fiber/sched.h` (`rw_sched_park_current` を追加) -- Modify: `runtime/fiber/sched.c` (`rw_sched_park_current` を実装) +- Modify: `runtime/fiber/sched.h` (add `rw_sched_park_current`) +- Modify: `runtime/fiber/sched.c` (implement `rw_sched_park_current`) - Create: `runtime/fiber/test_netpoller_pipe.c` - Modify: `.gitignore` -- [ ] **Step 3.1: `runtime/fiber/sched.h` に `rw_sched_park_current` を追加** +- [ ] **Step 3.1: Add `rw_sched_park_current` to `runtime/fiber/sched.h`** -Task 1 で追加した export 群の下に: +Below the exports added in Task 1: ```c /* Mark the current fiber as WAITING and swap out to the scheduler. @@ -606,9 +606,9 @@ Task 1 で追加した export 群の下に: void rw_sched_park_current(void); ``` -- [ ] **Step 3.2: `runtime/fiber/sched.c` に `rw_sched_park_current` を実装** +- [ ] **Step 3.2: Implement `rw_sched_park_current` in `runtime/fiber/sched.c`** -既存 `rw_sched_yield` のすぐ下に追加: +Add it right below the existing `rw_sched_yield`: ```c /* Park the current fiber: mark it WAITING and swap to sched_ctx. @@ -625,20 +625,20 @@ void rw_sched_park_current(void) { } ``` -worker_main の `if (state == RUNNING) enqueue_ready(g)` 判定は既存のままで、 -WAITING の fiber は自動的に re-enqueue されないので意図通り動く。 +worker_main's `if (state == RUNNING) enqueue_ready(g)` check stays as-is, and since a WAITING +fiber is not automatically re-enqueued, this works as intended. -- [ ] **Step 3.3: ビルドが緑か確認** +- [ ] **Step 3.3: Confirm the build is green** ```sh make -C /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime ``` -Expected: 警告なしビルド成功。 +Expected: Warning-free successful build. -- [ ] **Step 3.4: `runtime/fiber/test_netpoller_pipe.c` を新規作成** +- [ ] **Step 3.4: Create `runtime/fiber/test_netpoller_pipe.c`** -pipe(2) を 2 つの fiber で読み書きして、reader が park → writer が write → reader が起きる挙動を確認: +Read and write a pipe(2) across two fibers to confirm the behavior: reader parks -> writer writes -> reader wakes: ```c /* @@ -722,26 +722,26 @@ int main(void) { } ``` -- [ ] **Step 3.5: `.gitignore` に test バイナリを追加** +- [ ] **Step 3.5: Add the test binary to `.gitignore`** -`runtime/fiber/test_option` の下に追加: +Add below `runtime/fiber/test_option`: ``` runtime/fiber/test_netpoller_pipe ``` -- [ ] **Step 3.6: テストをビルドして実行** +- [ ] **Step 3.6: Build and run the test** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_netpoller_pipe.c librw.a -o fiber/test_netpoller_pipe && ./fiber/test_netpoller_pipe ``` -Expected: `netpoller pipe test ok`。 +Expected: `netpoller pipe test ok`. -タイムアウトする場合は netpoller スレッドが ready 通知を正しく `enqueue_ready` していない (Task 2 の platform 実装か Task 3.2 の park ロジックに問題)。`pkill test_netpoller_pipe` で殺してログを見直す。 +If it times out, the netpoller thread is not correctly `enqueue_ready`ing on ready notifications (a problem in the Task 2 platform implementation or the Task 3.2 park logic). Kill it with `pkill test_netpoller_pipe` and review the logs. -- [ ] **Step 3.7: 既存テストも回帰なし** +- [ ] **Step 3.7: Existing tests also stay regression-free** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 @@ -781,9 +781,9 @@ EOF --- -## Task 4: tcp_* helper + localhost loopback テスト +## Task 4: tcp_* helpers + localhost loopback test -`runtime/net/tcp.c` / `tcp.h` を実装し、localhost で listen → connect → recv/send が動くことを C で確認。 +Implement `runtime/net/tcp.c` / `tcp.h` and confirm in C that listen -> connect -> recv/send works on localhost. **Files:** - Create: `runtime/net/tcp.h` @@ -791,7 +791,7 @@ EOF - Create: `runtime/fiber/test_tcp_loopback.c` - Modify: `.gitignore` -- [ ] **Step 4.1: `runtime/net/tcp.h` を新規作成** +- [ ] **Step 4.1: Create `runtime/net/tcp.h`** ```c #ifndef RW_TCP_H @@ -816,7 +816,7 @@ extern "C" { #endif /* RW_TCP_H */ ``` -- [ ] **Step 4.2: `runtime/net/tcp.c` を新規作成** +- [ ] **Step 4.2: Create `runtime/net/tcp.c`** ```c /* @@ -931,9 +931,9 @@ int64_t rw_tcp_close(int64_t fd) { } ``` -- [ ] **Step 4.3: `runtime/fiber/test_tcp_loopback.c` を新規作成** +- [ ] **Step 4.3: Create `runtime/fiber/test_tcp_loopback.c`** -localhost で listen → 別 fiber が connect → recv/send を実施: +Listen on localhost -> a separate fiber connects -> perform recv/send: ```c /* @@ -1027,15 +1027,15 @@ int main(void) { } ``` -- [ ] **Step 4.4: `.gitignore` 更新** +- [ ] **Step 4.4: Update `.gitignore`** -`test_netpoller_pipe` の下に追加: +Add below `test_netpoller_pipe`: ``` runtime/fiber/test_tcp_loopback ``` -- [ ] **Step 4.5: ビルド + 実行** +- [ ] **Step 4.5: Build + run** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime @@ -1043,20 +1043,20 @@ make cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_tcp_loopback.c librw.a -o fiber/test_tcp_loopback && ./fiber/test_tcp_loopback ``` -Expected: `tcp loopback test ok`。 +Expected: `tcp loopback test ok`. -タイムアウトする場合は `tcp_accept` で park した fiber が起きていない、もしくは `tcp_read` / `tcp_write` の park が動いていない。`netpoller_pipe` が動いているなら netpoller 自体は健全。 +If it times out, the fiber parked in `tcp_accept` is not waking, or the park in `tcp_read` / `tcp_write` is not working. If `netpoller_pipe` works, the netpoller itself is healthy. -- [ ] **Step 4.6: pipe テストも引き続き緑か** +- [ ] **Step 4.6: The pipe test also stays green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime ./fiber/test_netpoller_pipe ``` -Expected: `netpoller pipe test ok`。 +Expected: `netpoller pipe test ok`. -- [ ] **Step 4.7: pytest も緑** +- [ ] **Step 4.7: pytest also green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 @@ -1099,16 +1099,16 @@ EOF --- -## Task 5: rwc に 5 つの組込みを追加 + sema/irgen テスト +## Task 5: Add the five builtins to rwc + sema/irgen tests **Files:** - Modify: `rwc/sema.py` - Modify: `rwc/irgen.py` - Modify: `tests/test_sema.py` -- [ ] **Step 5.1: Sema に 5 組込みを追加** +- [ ] **Step 5.1: Add the five builtins to Sema** -`rwc/sema.py` の `list_at_opt` の Sema 直下に追加: +Add them right below the `list_at_opt` Sema in `rwc/sema.py`: ```python # Builtin: tcp_listen(int) -> int. @@ -1195,9 +1195,9 @@ EOF return T.INT ``` -- [ ] **Step 5.2: SpawnExpr 禁止リストに 5 つ追加** +- [ ] **Step 5.2: Add the five to the SpawnExpr prohibition list** -`list_at_opt` の禁止分岐の **直下** に追加: +Add them **directly below** the `list_at_opt` prohibition branch: ```python if call.callee == "tcp_listen": @@ -1227,9 +1227,9 @@ EOF )) ``` -- [ ] **Step 5.3: irgen に 5 組込みの emit を追加** +- [ ] **Step 5.3: Add emit for the five builtins to irgen** -`rwc/irgen.py` の `_declare_runtime` で `rw_list_int_at_opt` の宣言の **直下** に追加: +In `rwc/irgen.py`'s `_declare_runtime`, add **directly below** the `rw_list_int_at_opt` declaration: ```python # TCP API (runtime/net/tcp.c) @@ -1247,7 +1247,7 @@ EOF m, ir.FunctionType(I64, [I64]), "rw_tcp_close") ``` -`_emit_call` で `list_at_opt` の分岐の **直下** に追加: +In `_emit_call`, add **directly below** the `list_at_opt` branch: ```python if call.callee == "tcp_listen": @@ -1271,9 +1271,9 @@ EOF return ctx.builder.call(self._rw_tcp_close, [v]) ``` -- [ ] **Step 5.4: Positive テスト** +- [ ] **Step 5.4: Positive tests** -`tests/test_sema.py` の末尾に追加: +Add to the end of `tests/test_sema.py`: ```python # ---- TCP builtins positive cases ---- @@ -1330,9 +1330,9 @@ def test_tcp_close_returns_int(): check(src) ``` -- [ ] **Step 5.5: Negative テスト** +- [ ] **Step 5.5: Negative tests** -`tests/test_sema.py` の末尾に追加: +Add to the end of `tests/test_sema.py`: ```python # ---- TCP builtins negative cases ---- @@ -1387,15 +1387,15 @@ def test_cannot_spawn_tcp_accept(): assert "cannot spawn the builtin `tcp_accept`" in e.diagnostic.message ``` -- [ ] **Step 5.6: pytest を回す** +- [ ] **Step 5.6: Run pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 既存 131 + positive 5 + negative 5 = `141 passed`。 +Expected: existing 131 + 5 positive + 5 negative = `141 passed`. -- [ ] **Step 5.7: smoke check (IR を見て build できる)** +- [ ] **Step 5.7: smoke check (inspect the IR and build)** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -1412,9 +1412,9 @@ ls -la /tmp/tcp_smoke && /tmp/tcp_smoke && echo "exit=$?" ``` Expected: -- IR に `declare i64 @"rw_tcp_listen"` と `call i64 @"rw_tcp_close"` などが見える -- ビルド成功 -- 実行は `exit=0` (listen → close、即終了) +- The IR shows `declare i64 @"rw_tcp_listen"`, `call i64 @"rw_tcp_close"`, and so on +- Build succeeds +- Execution gives `exit=0` (listen -> close, then exits immediately) - [ ] **Step 5.8: Commit** @@ -1460,7 +1460,7 @@ EOF - Create: `examples/tcp_echo.rw` - Create: `tests/test_e2e_tcp.py` -- [ ] **Step 6.1: `examples/tcp_echo.rw` を新規作成** +- [ ] **Step 6.1: Create `examples/tcp_echo.rw`** ```rw def handle_client(fd: int) -> int: @@ -1479,8 +1479,9 @@ def main() -> int: return 0 ``` -`__PORT__` プレースホルダは e2e でテスト毎に動的な空きポートに置換する。 -ユーザが手動で `rwc run examples/tcp_echo.rw` するときは `__PORT__` のままだとパースエラーになるので、**最初から `8080` を埋めておき、e2e は別の tmp .rw をビルドする** 方が手軽。修正: +The `__PORT__` placeholder is replaced with a dynamically-chosen free port per test in the e2e. +Since `__PORT__` would cause a parse error when a user manually runs `rwc run examples/tcp_echo.rw`, +it is simpler to **bake in `8080` from the start and have the e2e build a separate tmp .rw**. Revised: ```rw def handle_client(fd: int) -> int: @@ -1499,9 +1500,9 @@ def main() -> int: return 0 ``` -e2e は `examples/tcp_echo.rw` を読み、`tcp_listen(8080)` を `tcp_listen()` に sed して /tmp に保存、それをビルド+起動する。 +The e2e reads `examples/tcp_echo.rw`, seds `tcp_listen(8080)` to `tcp_listen()`, saves it to /tmp, then builds and launches it. -- [ ] **Step 6.2: `tests/test_e2e_tcp.py` を新規作成** +- [ ] **Step 6.2: Create `tests/test_e2e_tcp.py`** ```python """End-to-end tests for the TCP echo example.""" @@ -1591,7 +1592,7 @@ def test_echo_ten_concurrent_connections(): proc.wait(timeout=2.0) ``` -- [ ] **Step 6.3: 手動 smoke** +- [ ] **Step 6.3: Manual smoke** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -1603,19 +1604,19 @@ echo -n "hello" | nc -w 1 127.0.0.1 8080 kill $SERVER_PID 2>/dev/null ``` -Expected: `hello` がそのまま返ってくる。 +Expected: `hello` comes back unchanged. -- [ ] **Step 6.4: pytest を全件回す** +- [ ] **Step 6.4: Run the full pytest suite** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 既存 141 (Task 5 まで) + e2e_tcp 新規 2 = `143 passed`。 +Expected: existing 141 (through Task 5) + 2 new e2e_tcp = `143 passed`. -`test_echo_ten_concurrent_connections` が flaky な場合 (CI で `sleep 0.3` が短すぎる等) は `_start_server` の `time.sleep` を 0.5 まで上げる。 +If `test_echo_ten_concurrent_connections` is flaky (e.g. `sleep 0.3` is too short in CI), raise `_start_server`'s `time.sleep` to 0.5. -- [ ] **Step 6.5: 既存 example の回帰確認** +- [ ] **Step 6.5: Verify existing examples are regression-free** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -1624,7 +1625,7 @@ RW_WORKERS=1 uv run rwc run examples/result_basic.rw RW_WORKERS=1 uv run rwc run examples/spawn_many.rw ``` -Expected: それぞれ `5\n-1`, `5\n0`, `30`。 +Expected: `5\n-1`, `5\n0`, and `30` respectively. - [ ] **Step 6.6: Commit** @@ -1663,7 +1664,7 @@ EOF --- -## Task 7: plan ファイル commit +## Task 7: Commit the plan file - [ ] **Step 7.1: Commit** @@ -1684,39 +1685,39 @@ Co-Authored-By: Claude Opus 4.7 " ### Spec coverage -| Spec 要求 | カバーするタスク | +| Spec requirement | Covering task | |---|---| -| 専用 netpoller pthread (kqueue / epoll) | Task 2 (全 step) | +| Dedicated netpoller pthread (kqueue / epoll) | Task 2 (all steps) | | `rw_net_park_read/write` | Task 2.2 + 3.2 | | `rw_set_nonblocking` | Task 2.2 | | `rw_netpoller_init/shutdown` from `rw_init/shutdown` | Task 2.7 | | Shutdown wake-up (kqueue EVFILT_USER / epoll eventfd) | Task 2.3 / 2.4 | -| ONESHOT 監視 | Task 2.3 / 2.4 | +| ONESHOT monitoring | Task 2.3 / 2.4 | | `tcp_listen(port) -> int` | Task 4.2 (runtime) + 5.1 (sema) + 5.3 (irgen) + 5.4 (test) | -| `tcp_accept(int) -> int`、main は blocking / fiber は park | Task 4.2 + 5.1 + 5.3 + 5.4 | -| `tcp_read(int, int) -> Bytes`、len==0 で EOF/error | Task 4.2 + 5.1 + 5.3 + 5.4 | +| `tcp_accept(int) -> int`, main blocks / fiber parks | Task 4.2 + 5.1 + 5.3 + 5.4 | +| `tcp_read(int, int) -> Bytes`, len==0 for EOF/error | Task 4.2 + 5.1 + 5.3 + 5.4 | | `tcp_write(int, Bytes) -> int` | Task 4.2 + 5.1 + 5.3 + 5.4 | | `tcp_close(int) -> int` | Task 4.2 + 5.1 + 5.3 + 5.4 | -| 5 組込みすべて spawn 禁止 | Task 5.2 + test 5.5 (`test_cannot_spawn_tcp_accept`) | -| C テスト: netpoller pipe | Task 3.4 | -| C テスト: tcp loopback | Task 4.3 | -| e2e: 1 接続 + 10 並列接続 | Task 6.2 | +| All five builtins forbid spawn | Task 5.2 + test 5.5 (`test_cannot_spawn_tcp_accept`) | +| C test: netpoller pipe | Task 3.4 | +| C test: tcp loopback | Task 4.3 | +| e2e: 1 connection + 10 concurrent connections | Task 6.2 | | `examples/tcp_echo.rw` | Task 6.1 | -| 既存 131 テスト緑、既存 example 回帰なし | Task 2.9 / 3.7 / 4.7 / 6.4 / 6.5 | -| sched.h に enqueue_ready / current_fiber export | Task 1 | -| `rw_sched_park_current` 追加 | Task 3.1 + 3.2 | -| Makefile uname 分岐 | Task 2.5 | +| Existing 131 tests green, existing examples regression-free | Task 2.9 / 3.7 / 4.7 / 6.4 / 6.5 | +| Export enqueue_ready / current_fiber from sched.h | Task 1 | +| Add `rw_sched_park_current` | Task 3.1 + 3.2 | +| Makefile uname branching | Task 2.5 | -すべての spec 要求にタスクがある。 +Every spec requirement has a task. -### Placeholder スキャン +### Placeholder scan -「TBD」「TODO」「(要確認)」「fill in」「Add appropriate」「Similar to Task N」は plan 内 0 件。 +"TBD", "TODO", "(needs confirmation)", "fill in", "Add appropriate", and "Similar to Task N" appear 0 times in the plan. ### Type consistency -- `rw_netpoller_init` / `_shutdown` / `_platform_init` / `_platform_shutdown` / `_platform_run` / `_register_read` / `_register_write` のシグネチャを Task 2.1 (宣言) と Task 2.2/2.3/2.4 (実装) で完全一致 -- `rw_net_park_read` / `_write` / `rw_set_nonblocking` のシグネチャを Task 2.1 / 2.2 / 4.2 で揃って使用 -- `rw_sched_enqueue_ready` / `_current_fiber` / `_park_current` を Task 1.1 / 1.2 / 3.1 / 3.2 で揃えて宣言・実装、Task 2.2 / 2.3 / 2.4 / 4.2 で呼び出し -- `rw_tcp_listen` / `_accept` / `_read` / `_write` / `_close` のシグネチャを Task 2.6 (runtime.h) / 4.2 (実装) / 5.3 (irgen 宣言) で完全一致 -- `RW_STR_TY` を `tcp_read` の pointer-out shim で使用 (Task 5.3)、既存 string ヘルパと同じ alloca/load パターン +- The signatures of `rw_netpoller_init` / `_shutdown` / `_platform_init` / `_platform_shutdown` / `_platform_run` / `_register_read` / `_register_write` match exactly between Task 2.1 (declaration) and Task 2.2/2.3/2.4 (implementation) +- The signatures of `rw_net_park_read` / `_write` / `rw_set_nonblocking` are used consistently across Task 2.1 / 2.2 / 4.2 +- `rw_sched_enqueue_ready` / `_current_fiber` / `_park_current` are consistently declared/implemented in Task 1.1 / 1.2 / 3.1 / 3.2 and called in Task 2.2 / 2.3 / 2.4 / 4.2 +- The signatures of `rw_tcp_listen` / `_accept` / `_read` / `_write` / `_close` match exactly between Task 2.6 (runtime.h) / 4.2 (implementation) / 5.3 (irgen declaration) +- `RW_STR_TY` is used in `tcp_read`'s pointer-out shim (Task 5.3), following the same alloca/load pattern as the existing string helpers diff --git a/docs/plans/2026-05-23-result-type.md b/docs/plans/2026-05-23-result-type.md index 4e0f037..de6355b 100644 --- a/docs/plans/2026-05-23-result-type.md +++ b/docs/plans/2026-05-23-result-type.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** rw 言語に `Result[int, int]` 型と `Ok(e)` / `Err(e)` の値構築式を追加し、既存の `match` 文を「Some/None ペア」または「Ok/Err ペア」のどちらか 2 アームを受けるよう拡張する。`div_checked(a, b) -> Result[int, int]` のような関数を rw 側で書いて `match` で分解できる状態に持っていく。 +**Goal:** Add a `Result[int, int]` type and `Ok(e)` / `Err(e)` value-construction expressions to the rw language, and extend the existing `match` statement to accept either a Some/None pair or an Ok/Err pair of two arms. The target state is that a function like `div_checked(a, b) -> Result[int, int]` can be written in rw and destructured with `match`. -**Architecture:** `Result[int, int]` は `{i64 tag, i64 payload}` (tag=0 = Err, 1 = Ok) で `Option[int]` と同じ LLVM struct を持つが、Sema レベルで `T.OPTION_INT` と `T.RESULT_INT_INT` を別の型として区別する。`MatchStmt` AST に `style: "option" | "result"` フィールドを追加し、parser が最初のアームを見て style を確定する。Sema / irgen は style 別に分岐し、irgen 側は `_emit_arm` 共通 helper で Option / Result lowering を共有する。 +**Architecture:** `Result[int, int]` uses the same LLVM struct as `Option[int]` — `{i64 tag, i64 payload}` (tag=0 = Err, 1 = Ok) — but at the Sema level `T.OPTION_INT` and `T.RESULT_INT_INT` are distinguished as separate types. A `style: "option" | "result"` field is added to the `MatchStmt` AST, and the parser determines the style from the first arm. Sema / irgen branch on the style, and on the irgen side a shared `_emit_arm` helper factors out the Option / Result lowering. -**Tech Stack:** Python 3.12 + llvmlite (コンパイラ)、pytest (テスト)。ランタイム変更なし。 +**Tech Stack:** Python 3.12 + llvmlite (compiler), pytest (tests). No runtime changes. **Spec:** `docs/specs/11-result-type.md` @@ -16,49 +16,49 @@ | File | Responsibility | Action | |---|---|---| -| `rwc/types.py` | プリミティブ型定義 | `RESULT_INT_INT` 追加 | -| `rwc/lexer.py` | キーワード認識 | `KW_RESULT` / `KW_OK` / `KW_ERR` | -| `rwc/ast_nodes.py` | AST ノード | `OkExpr` / `ErrExpr` 追加、`MatchStmt` に style と Result 用フィールド追加 | -| `rwc/parser.py` | 型 + 式 + 文のパース | `parse_type` の Result 分岐、`Ok(e)` / `Err(e)` 式、`parse_match` 全面書き直し | -| `rwc/sema.py` | 型解決 + 式/文 Sema | `_resolve_type` / `OkExpr` / `ErrExpr` Sema / `MatchStmt` style 分岐 | -| `rwc/irgen.py` | LLVM IR 生成 | `RW_RESULT_INT_INT_TY` / `Ok` / `Err` emit / `MatchStmt` を `_emit_arm` 経由で style 別 lowering | -| `tests/test_sema.py` | 型検査 positive/negative | テスト追加 | -| `tests/test_e2e.py` | parametrize に result_basic | 1 行追加 | -| `examples/result_basic.rw` | デモ | 新規 | -| `examples/result_basic.rw.expected` | 期待出力 | 新規 | - -ランタイム (`runtime/*`) と fiber 関連には一切触れない。 +| `rwc/types.py` | Primitive type definitions | Add `RESULT_INT_INT` | +| `rwc/lexer.py` | Keyword recognition | `KW_RESULT` / `KW_OK` / `KW_ERR` | +| `rwc/ast_nodes.py` | AST nodes | Add `OkExpr` / `ErrExpr`; add style and Result fields to `MatchStmt` | +| `rwc/parser.py` | Type + expression + statement parsing | Result branch in `parse_type`, `Ok(e)` / `Err(e)` expressions, full rewrite of `parse_match` | +| `rwc/sema.py` | Type resolution + expr/stmt Sema | `_resolve_type` / `OkExpr` / `ErrExpr` Sema / `MatchStmt` style branching | +| `rwc/irgen.py` | LLVM IR generation | `RW_RESULT_INT_INT_TY` / `Ok` / `Err` emit / style-specific lowering of `MatchStmt` via `_emit_arm` | +| `tests/test_sema.py` | Positive/negative type checking | Add tests | +| `tests/test_e2e.py` | Add result_basic to parametrize | Add 1 line | +| `examples/result_basic.rw` | Demo | New | +| `examples/result_basic.rw.expected` | Expected output | New | + +The runtime (`runtime/*`) and anything fiber-related are left untouched. --- -## Task 1: lexer / parser / types / AST で `Result[int, int]` 構文を認識 +## Task 1: Recognize `Result[int, int]` syntax in lexer / parser / types / AST -このタスクで `Result[int, int]` の型注釈、`Ok(e)` / `Err(e)` の式、`match v: case Ok(x): ... case Err(e): ...` の文 (および既存 Some/None) が **AST まで構築できる** ようにする。`MatchStmt` の AST 構造を style 統合形に書き換える破壊的変更を含むので、既存の Option-style コード/テストが回帰しないことを確認する。 +This task makes the `Result[int, int]` type annotation, `Ok(e)` / `Err(e)` expressions, and the `match v: case Ok(x): ... case Err(e): ...` statement (as well as the existing Some/None) **constructible all the way to the AST**. It includes a breaking change that rewrites the `MatchStmt` AST structure into a style-unified form, so verify that existing Option-style code/tests do not regress. **Files:** - Modify: `rwc/types.py` - Modify: `rwc/lexer.py` - Modify: `rwc/ast_nodes.py` - Modify: `rwc/parser.py` -- Modify: `rwc/sema.py` (`_resolve_type` だけ追加 + 既存 `_check_stmt` の MatchStmt を style 分岐に対応) -- Modify: `rwc/irgen.py` (既存 `_emit_stmt` の MatchStmt を style 分岐に対応) +- Modify: `rwc/sema.py` (add only `_resolve_type` + make the existing `_check_stmt` MatchStmt handle style branching) +- Modify: `rwc/irgen.py` (make the existing `_emit_stmt` MatchStmt handle style branching) - Modify: `tests/test_sema.py` ### types / lexer -- [ ] **Step 1.1: `rwc/types.py` に `RESULT_INT_INT` を追加** +- [ ] **Step 1.1: Add `RESULT_INT_INT` to `rwc/types.py`** -`OPTION_INT = _Primitive("Option[int]")` の **直下** に追加: +Add it **directly below** `OPTION_INT = _Primitive("Option[int]")`: ```python RESULT_INT_INT = _Primitive("Result[int, int]") ``` -`is_printable` / `is_numeric` には含めない。 +Do not include it in `is_printable` / `is_numeric`. -- [ ] **Step 1.2: `rwc/lexer.py` に 3 つのキーワードを追加** +- [ ] **Step 1.2: Add three keywords to `rwc/lexer.py`** -`TokenKind` enum の `KW_NONE = auto()` の **直下** に: +**Directly below** `KW_NONE = auto()` in the `TokenKind` enum: ```python KW_RESULT = auto() @@ -66,7 +66,7 @@ RESULT_INT_INT = _Primitive("Result[int, int]") KW_ERR = auto() ``` -`KEYWORDS` dict の `"None": TokenKind.KW_NONE,` の **直下** に: +**Directly below** `"None": TokenKind.KW_NONE,` in the `KEYWORDS` dict: ```python "Result": TokenKind.KW_RESULT, @@ -76,9 +76,9 @@ RESULT_INT_INT = _Primitive("Result[int, int]") ### AST -- [ ] **Step 1.3: `rwc/ast_nodes.py` に `OkExpr` / `ErrExpr` を追加** +- [ ] **Step 1.3: Add `OkExpr` / `ErrExpr` to `rwc/ast_nodes.py`** -既存 `NoneExpr` の **直下** に: +**Directly below** the existing `NoneExpr`: ```python @dataclass @@ -95,7 +95,7 @@ class ErrExpr: col: int ``` -`Expr` Union に 2 つ追加: +Add two entries to the `Expr` Union: ```python Expr = Union[ @@ -106,9 +106,9 @@ Expr = Union[ ] ``` -- [ ] **Step 1.4: `rwc/ast_nodes.py` の `MatchStmt` を style 統合形に拡張** +- [ ] **Step 1.4: Extend `MatchStmt` in `rwc/ast_nodes.py` into the style-unified form** -現在の `MatchStmt`: +The current `MatchStmt`: ```python @dataclass @@ -121,7 +121,7 @@ class MatchStmt: col: int ``` -を以下に置き換え: +Replace it with the following: ```python @dataclass @@ -141,13 +141,13 @@ class MatchStmt: col: int ``` -`Optional` を import していなければ追加。 +Add an import for `Optional` if it is not already imported. ### parser -- [ ] **Step 1.5: `parse_type` の Option 分岐の直下に Result 分岐** +- [ ] **Step 1.5: Add the Result branch directly below the Option branch in `parse_type`** -`rwc/parser.py` の `parse_type` 内、Option 分岐の **直下** に追加: +**Directly below** the Option branch inside `parse_type` in `rwc/parser.py`: ```python if t.kind == TokenKind.KW_RESULT: @@ -172,9 +172,9 @@ class MatchStmt: return A.TypeName("Result[int, int]", t.line, t.col) ``` -- [ ] **Step 1.6: `parse_unary` で `Ok(e)` / `Err(e)` を式として受ける** +- [ ] **Step 1.6: Accept `Ok(e)` / `Err(e)` as expressions in `parse_unary`** -`KW_NONE` 分岐の **直下** に追加: +**Directly below** the `KW_NONE` branch: ```python if t.kind == TokenKind.KW_OK: @@ -191,9 +191,9 @@ class MatchStmt: return A.ErrExpr(arg, t.line, t.col) ``` -- [ ] **Step 1.7: `parse_match` を style 統合形に書き直す** +- [ ] **Step 1.7: Rewrite `parse_match` into the style-unified form** -既存 `parse_match` メソッドを **丸ごと** 以下に置き換え: +Replace the **entire** existing `parse_match` method with the following: ```python def parse_match(self) -> A.MatchStmt: @@ -331,9 +331,9 @@ class MatchStmt: ### Sema and irgen: minimal updates to match the new AST shape -- [ ] **Step 1.8: `_resolve_type` に `Result[int, int]` を追加** +- [ ] **Step 1.8: Add `Result[int, int]` to `_resolve_type`** -`rwc/sema.py` の `_resolve_type` の `m` dict に 1 行: +Add one line to the `m` dict of `_resolve_type` in `rwc/sema.py`: ```python m = { @@ -349,9 +349,9 @@ class MatchStmt: } ``` -- [ ] **Step 1.9: 既存 Sema の `MatchStmt` 分岐を style 分岐対応に最小修正** +- [ ] **Step 1.9: Minimally update the existing Sema `MatchStmt` branch to support style branching** -`rwc/sema.py` の `_check_stmt` 内の `MatchStmt` 分岐 (現状は Option-style ハードコード) を以下に置き換え。Task 2 で `style == "result"` の本格 Sema が完成するが、ここでは「既存の Option 用ロジックを `style == "option"` のときだけ走らせる」最小ガードを入れて、新 AST フィールドの存在で既存テストが死なないようにする: +Replace the `MatchStmt` branch in `_check_stmt` of `rwc/sema.py` (currently hardcoded to Option-style) with the following. Full `style == "result"` Sema is completed in Task 2; here, add a minimal guard that "runs the existing Option logic only when `style == "option"`" so that the presence of the new AST fields does not break existing tests: ```python if isinstance(stmt, A.MatchStmt): @@ -376,9 +376,9 @@ class MatchStmt: )) ``` -- [ ] **Step 1.10: 既存 irgen の `MatchStmt` 分岐も同様に style ガード** +- [ ] **Step 1.10: Add a similar style guard to the existing irgen `MatchStmt` branch** -`rwc/irgen.py` の `_emit_stmt` 内の `MatchStmt` 分岐の最初に 1 行追加: +Add one line at the start of the `MatchStmt` branch in `_emit_stmt` of `rwc/irgen.py`: ```python if isinstance(stmt, A.MatchStmt): @@ -390,13 +390,13 @@ class MatchStmt: # ... existing Option-style lowering, unchanged ... ``` -(現状の Option-style 本体はそのまま `if stmt.style != "option": raise ...` の **下** に残す) +(Leave the current Option-style body as-is, **below** the `if stmt.style != "option": raise ...` guard.) ### Tests for Task 1 -- [ ] **Step 1.11: 型注釈 + 構文だけ通るテスト** +- [ ] **Step 1.11: Tests that pass only the type annotation + syntax** -`tests/test_sema.py` の末尾に追加: +Append to the end of `tests/test_sema.py`: ```python def test_result_int_int_type_annotation_parses(): @@ -444,17 +444,17 @@ def test_match_with_mixed_arms_is_parser_error(): assert "mixed match arms" in str(ei.value) ``` -- [ ] **Step 1.12: 既存テストを含めて pytest 全件を回し緑か確認** +- [ ] **Step 1.12: Run the full pytest suite (including existing tests) and confirm green** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 既存 115 + Task 1 新規 3 = `118 passed`。 +Expected: existing 115 + 3 new in Task 1 = `118 passed`. -既存 Option-style の `test_match_*` 系テストは parser/Sema が新 AST を出すよう -になっても挙動同一を保つ前提。もし落ちる場合は `MatchStmt` のフィールド -順序や Optional 設定を Step 1.4 と照合。 +The existing Option-style `test_match_*` tests are expected to behave +identically even after the parser/Sema emit the new AST. If they fail, +cross-check the `MatchStmt` field order and Optional settings against Step 1.4. - [ ] **Step 1.13: Commit** @@ -494,17 +494,17 @@ EOF --- -## Task 2: Sema で `OkExpr` / `ErrExpr` / Result-style `MatchStmt` を完成させる +## Task 2: Complete `OkExpr` / `ErrExpr` / Result-style `MatchStmt` in Sema -このタスクで Sema が Result の式と match を理解する。Task 1 で `"not yet implemented"` を投げていた箇所を実装で置き換える。 +In this task Sema learns to understand Result expressions and match. Replace the places that raised `"not yet implemented"` in Task 1 with real implementations. **Files:** - Modify: `rwc/sema.py` - Modify: `tests/test_sema.py` -- [ ] **Step 2.1: `_check_expr` で `OkExpr` / `ErrExpr` を扱う** +- [ ] **Step 2.1: Handle `OkExpr` / `ErrExpr` in `_check_expr`** -`rwc/sema.py` の `_check_expr` で、既存 `SomeExpr` / `NoneExpr` の **直下** に追加: +**Directly below** the existing `SomeExpr` / `NoneExpr` in `_check_expr` of `rwc/sema.py`: ```python if isinstance(expr, A.OkExpr): @@ -525,9 +525,9 @@ EOF return T.RESULT_INT_INT ``` -- [ ] **Step 2.2: `_check_stmt` の `MatchStmt` の Result-style ガードを実装に置き換え** +- [ ] **Step 2.2: Replace the Result-style guard of `MatchStmt` in `_check_stmt` with a real implementation** -Task 1 で `raise "not yet implemented"` していた箇所を以下に置き換え: +Replace the place that did `raise "not yet implemented"` in Task 1 with the following: ```python if stmt.style == "option": @@ -552,9 +552,9 @@ Task 1 で `raise "not yet implemented"` していた箇所を以下に置き換 return ok_ret and err_ret ``` -- [ ] **Step 2.3: Positive テスト** +- [ ] **Step 2.3: Positive tests** -`tests/test_sema.py` の末尾に追加: +Append to the end of `tests/test_sema.py`: ```python # ---- Result[int, int] positive cases ---- @@ -638,9 +638,9 @@ def test_match_result_terminates_via_both_arms_return(): check(src) ``` -- [ ] **Step 2.4: Negative テスト** +- [ ] **Step 2.4: Negative tests** -`tests/test_sema.py` に続けて追加: +Append further to `tests/test_sema.py`: ```python # ---- Result[int, int] negative cases ---- @@ -717,25 +717,25 @@ def test_ok_eq_some_is_type_error(): assert "same type" in e.diagnostic.message ``` -- [ ] **Step 2.5: pytest を走らせる** +- [ ] **Step 2.5: Run pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest tests/test_sema.py -q 2>&1 | tail -5 ``` -Expected: 既存 64 + Task 1 で追加した 3 + ここで positive 6 + negative 6 = `79 passed`。 +Expected: existing 64 + 3 added in Task 1 + 6 positive here + 6 negative = `79 passed`. -irgen がまだ Result-style match を扱わないので `examples/result_basic.rw` を -実行しようとすると失敗する (Step 1.10 で `"not yet implemented"` を投げる)。 -Sema レベルでは完全に通っている状態。 +Since irgen does not yet handle Result-style match, trying to run +`examples/result_basic.rw` will fail (Step 1.10 raises `"not yet implemented"`). +At the Sema level everything passes cleanly. -- [ ] **Step 2.6: 全 pytest** +- [ ] **Step 2.6: Full pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 既存 115 + Task 1 で 3 + Task 2 で 12 = `130 passed`。 +Expected: existing 115 + 3 in Task 1 + 12 in Task 2 = `130 passed`. - [ ] **Step 2.7: Commit** @@ -774,35 +774,35 @@ EOF --- -## Task 3: irgen で `Ok` / `Err` / Result-style `MatchStmt` を実装 +## Task 3: Implement `Ok` / `Err` / Result-style `MatchStmt` in irgen -このタスクで rw コードが実際に Result を扱って動く。Task 1 で立てた irgen の -ガード (`if stmt.style != "option": raise`) を外し、Option / Result の lowering -を共通 helper `_emit_arm` 経由で実装する。 +In this task rw code actually handles Result and runs. Remove the irgen +guard put in place in Task 1 (`if stmt.style != "option": raise`) and +implement the Option / Result lowering via the shared `_emit_arm` helper. **Files:** - Modify: `rwc/irgen.py` -- [ ] **Step 3.1: `RW_RESULT_INT_INT_TY` を irgen に追加** +- [ ] **Step 3.1: Add `RW_RESULT_INT_INT_TY` to irgen** -`rwc/irgen.py` 上部、`RW_OPTION_INT_TY = ...` の **直下** に: +At the top of `rwc/irgen.py`, **directly below** `RW_OPTION_INT_TY = ...`: ```python RW_RESULT_INT_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} ``` -- [ ] **Step 3.2: `llvm_type_of` に Result を追加** +- [ ] **Step 3.2: Add Result to `llvm_type_of`** -`if t is T.OPTION_INT:` の **直下** に: +**Directly below** `if t is T.OPTION_INT:`: ```python if t is T.RESULT_INT_INT: return RW_RESULT_INT_INT_TY ``` -- [ ] **Step 3.3: `_emit_expr` で `OkExpr` / `ErrExpr` を扱う** +- [ ] **Step 3.3: Handle `OkExpr` / `ErrExpr` in `_emit_expr`** -既存 `SomeExpr` / `NoneExpr` 分岐の **直下** に追加: +**Directly below** the existing `SomeExpr` / `NoneExpr` branch: ```python if isinstance(expr, A.OkExpr): @@ -817,9 +817,9 @@ RW_RESULT_INT_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} return ctx.builder.insert_value(base, v, 1) ``` -- [ ] **Step 3.4: `_emit_arm` 共通 helper を追加** +- [ ] **Step 3.4: Add the shared `_emit_arm` helper** -既存 `_emit_match` の **直上** (なければ `_emit_stmt` の前) にメソッドを追加: +Add the method **directly above** the existing `_emit_match` (or before `_emit_stmt` if there is none): ```python def _emit_arm(self, var_name, block, payload, ctx, end_bb): @@ -841,11 +841,11 @@ RW_RESULT_INT_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} b.branch(end_bb) ``` -- [ ] **Step 3.5: `_emit_stmt` の `MatchStmt` を style 別 lowering で書き直す** +- [ ] **Step 3.5: Rewrite `MatchStmt` in `_emit_stmt` with style-specific lowering** -Task 1 で立てた `if stmt.style != "option": raise ...` のガードを **削除** し、 -既存の Option-style 本体も含めて以下に置き換え (`_emit_match` というメソッド -名で切り出しても良いが、ここではインラインに): +**Remove** the `if stmt.style != "option": raise ...` guard added in Task 1 +and replace it, including the existing Option-style body, with the following +(you may factor it out into a method named `_emit_match`, but keep it inline here): ```python if isinstance(stmt, A.MatchStmt): @@ -874,7 +874,7 @@ Task 1 で立てた `if stmt.style != "option": raise ...` のガードを **削 return ``` -- [ ] **Step 3.6: smoke check** +- [ ] **Step 3.6: Smoke check** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -906,15 +906,15 @@ uv run rwc run /tmp/result_smoke.rw Expected: -IR に `switch i64` と `insertvalue` がそれぞれ含まれる (`{i64 0, i64 0}` の constant Err base と `{i64 1, i64 0}` の constant Ok base が両方出る)。 +The IR contains both `switch i64` and `insertvalue` (both the constant Err base `{i64 0, i64 0}` and the constant Ok base `{i64 1, i64 0}` appear). -実行結果: +Execution result: ``` 5 0 ``` -- [ ] **Step 3.7: 既存 Option の smoke も回帰なし** +- [ ] **Step 3.7: No regression in the existing Option smoke test** ```sh RW_WORKERS=1 uv run rwc run examples/option_basic.rw @@ -926,15 +926,15 @@ Expected: -1 ``` -`_emit_arm` 経由で Option-style もこの commit で書き直されるので回帰確認は必須。 +Since Option-style is also rewritten in this commit via `_emit_arm`, the regression check is mandatory. -- [ ] **Step 3.8: 全 pytest** +- [ ] **Step 3.8: Full pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: `130 passed` (Task 2 の数のまま、このタスクでテスト追加なし)。 +Expected: `130 passed` (same count as Task 2; no tests added in this task). - [ ] **Step 3.9: Commit** @@ -977,7 +977,7 @@ EOF - Create: `examples/result_basic.rw.expected` - Modify: `tests/test_e2e.py` -- [ ] **Step 4.1: `examples/result_basic.rw` を書く** +- [ ] **Step 4.1: Write `examples/result_basic.rw`** ```rw def div_checked(a: int, b: int) -> Result[int, int]: @@ -1008,40 +1008,40 @@ def main() -> int: 0 ``` -(末尾改行ありで保存。) +(Save with a trailing newline.) -- [ ] **Step 4.3: 手元で byte 一致** +- [ ] **Step 4.3: Confirm byte-for-byte match locally** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw diff <(RW_WORKERS=1 uv run rwc run examples/result_basic.rw 2>&1) examples/result_basic.rw.expected && echo OK ``` -Expected: `OK` だけが表示される。 +Expected: only `OK` is printed. -- [ ] **Step 4.4: `tests/test_e2e.py` の parametrize に `result_basic` を追加** +- [ ] **Step 4.4: Add `result_basic` to the parametrize in `tests/test_e2e.py`** -`tests/test_e2e.py:45` の以下の行: +The following line at `tests/test_e2e.py:45`: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic", "list_basic", "option_basic"], ``` -を以下に変更: +changes to the following: ```python ["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic", "list_basic", "option_basic", "result_basic"], ``` -- [ ] **Step 4.5: 全 pytest** +- [ ] **Step 4.5: Full pytest** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw && uv run pytest -q 2>&1 | tail -5 ``` -Expected: 130 + 1 = `131 passed`。 +Expected: 130 + 1 = `131 passed`. -- [ ] **Step 4.6: 既存 example 回帰** +- [ ] **Step 4.6: Existing example regression** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw @@ -1073,7 +1073,7 @@ hello 30 ``` -- [ ] **Step 4.7: ランタイム単体テストも緑か (ランタイム変更なしだが念のため)** +- [ ] **Step 4.7: Confirm the runtime unit tests are green too (no runtime changes, but just in case)** ```sh cd /Users/ryuichi/ghq/github.com/ryuichi1208/rw/runtime @@ -1113,37 +1113,37 @@ EOF ### Spec coverage -| Spec 要求 | カバーするタスク | +| Spec requirement | Covering tasks | |---|---| -| 新型 `Result[int, int]` (キーワード `Result`、parser が `[int, int]` 強制) | Task 1.1 / 1.2 / 1.5 / 1.8 | -| `Ok(int) -> Result[int, int]` / `Err(int) -> Result[int, int]` 式 | Task 1.3 (AST) / 1.6 (parser) / 2.1 (sema) / 3.3 (irgen) | -| match を Option / Result の 2 style に対応 | Task 1.4 (AST style field) / 1.7 (parse_match 書き直し) / 1.9 (sema 分岐) / 2.2 (Result-style sema) / 3.5 (irgen 分岐) | -| Some/None と Ok/Err の混在禁止 (parser) | Task 1.7 (`mixed match arms`) + test 1.11 | -| Result-style match の Ok/Err bound 変数が int | Task 2.2 (sema) + test 2.3 | -| return-coverage が Result の両 arm を見る | Task 2.2 (`ok_ret and err_ret`) + test 2.3 | -| 内部表現 16 バイト value 返し | Task 3.1 (`RW_RESULT_INT_INT_TY = LiteralStructType([I64, I64])`) | -| `Result[string, int]` 等は parser エラー | Task 1.5 + test 1.11 | -| match の片側欠落は parser エラー (両 style) | Task 1.7 + test 1.11 (mixed arms と片欠落は別エラー、両方が parser でカバー) | -| `print(Result[int, int])` は型エラー | `is_printable` 不変 + test 2.4 | -| `Result == Result` は型エラー | == whitelist 不変 + test 2.4 | -| `Ok(string)` / `Err(string)` は型エラー | Task 2.1 + test 2.4 | -| match ターゲットが Result[int, int] でないと型エラー | Task 2.2 + test 2.4 | -| `Future[Result[int, int]]` 禁止 | `_decl_spawn`/`_decl_await` に追加しないことで自動禁止 (Option と同じ判断) | -| 既存 115 テスト緑 | Task 1.12, 2.6, 3.8, 4.5 | -| Option-style match の挙動回帰なし | Task 1.7 の `style == "option"` パスを保つ + Task 3.5 の `_emit_arm` 共通化後の smoke (Step 3.7) + Task 4.6 | - -すべての spec 要求にタスクがある。 - -### Placeholder スキャン - -「TBD」「TODO」「(要確認)」「fill in」「Add appropriate」「Similar to Task N」は plan 内 0 件。`raise "not yet implemented"` ガード (Task 1.9 / 1.10) は **意図的な中間状態** で、Task 2 / Task 3 で削除されることが明示されている。placeholder ではない。 +| New type `Result[int, int]` (keyword `Result`, parser enforces `[int, int]`) | Task 1.1 / 1.2 / 1.5 / 1.8 | +| `Ok(int) -> Result[int, int]` / `Err(int) -> Result[int, int]` expressions | Task 1.3 (AST) / 1.6 (parser) / 2.1 (sema) / 3.3 (irgen) | +| match supports two styles, Option / Result | Task 1.4 (AST style field) / 1.7 (parse_match rewrite) / 1.9 (sema branching) / 2.2 (Result-style sema) / 3.5 (irgen branching) | +| Disallow mixing Some/None and Ok/Err (parser) | Task 1.7 (`mixed match arms`) + test 1.11 | +| Ok/Err bound variables in Result-style match are int | Task 2.2 (sema) + test 2.3 | +| return-coverage considers both arms of a Result | Task 2.2 (`ok_ret and err_ret`) + test 2.3 | +| Internal representation returned as a 16-byte value | Task 3.1 (`RW_RESULT_INT_INT_TY = LiteralStructType([I64, I64])`) | +| `Result[string, int]` and similar are parser errors | Task 1.5 + test 1.11 | +| A missing arm is a parser error (both styles) | Task 1.7 + test 1.11 (mixed arms and a missing arm are distinct errors, both covered by the parser) | +| `print(Result[int, int])` is a type error | `is_printable` unchanged + test 2.4 | +| `Result == Result` is a type error | == whitelist unchanged + test 2.4 | +| `Ok(string)` / `Err(string)` are type errors | Task 2.1 + test 2.4 | +| match target that is not Result[int, int] is a type error | Task 2.2 + test 2.4 | +| `Future[Result[int, int]]` disallowed | Automatically disallowed by not adding it to `_decl_spawn`/`_decl_await` (same decision as Option) | +| Existing 115 tests green | Task 1.12, 2.6, 3.8, 4.5 | +| No behavior regression in Option-style match | Preserve the `style == "option"` path in Task 1.7 + the post-`_emit_arm`-unification smoke test in Task 3.5 (Step 3.7) + Task 4.6 | + +Every spec requirement has a task. + +### Placeholder scan + +Zero occurrences of "TBD", "TODO", "(to be confirmed)", "fill in", "Add appropriate", or "Similar to Task N" in the plan. The `raise "not yet implemented"` guards (Task 1.9 / 1.10) are an **intentional intermediate state**, and it is explicitly stated that they are removed in Task 2 / Task 3. They are not placeholders. ### Type consistency -- `T.RESULT_INT_INT` は Task 1.1 / 1.8 / 2.1 / 2.2 / 3.2 / 3.3 で完全一致 -- LLVM 表現 `RW_RESULT_INT_INT_TY = LiteralStructType([I64, I64])` は Task 3.1 / 3.2 / 3.3 で揃っている -- 新規 AST ノード: `OkExpr` / `ErrExpr` を Task 1.3 (定義) / 1.6 (parser) / 2.1 (sema) / 3.3 (irgen) で揃って使用 -- `MatchStmt` の新フィールド: `style` / `ok_var` / `ok_block` / `err_var` / `err_block` を Task 1.4 (定義) / 1.7 (parser) / 1.9 + 2.2 (sema) / 3.5 (irgen) で揃って使用 -- Sema 型エラーメッセージ: `"Ok argument must be int"` / `"Err argument must be int"` / `"match target must be Result[int, int]"` を実装 (Task 2.1, 2.2) と negative テスト assert (Task 2.4) で完全一致 -- `_emit_arm` の引数順 `(var_name, block, payload, ctx, end_bb)` は Task 3.4 (定義) / 3.5 (呼び出し) で一致 -- 既存 Option-style の `some_var` / `some_block` / `none_block` フィールド名は Task 1.4 (AST 再定義) / 1.7 (parser) / 1.9 (sema) / 3.5 (irgen) で揃って維持 +- `T.RESULT_INT_INT` matches exactly across Task 1.1 / 1.8 / 2.1 / 2.2 / 3.2 / 3.3 +- The LLVM representation `RW_RESULT_INT_INT_TY = LiteralStructType([I64, I64])` is consistent across Task 3.1 / 3.2 / 3.3 +- New AST nodes: `OkExpr` / `ErrExpr` are used consistently in Task 1.3 (definition) / 1.6 (parser) / 2.1 (sema) / 3.3 (irgen) +- New `MatchStmt` fields: `style` / `ok_var` / `ok_block` / `err_var` / `err_block` are used consistently in Task 1.4 (definition) / 1.7 (parser) / 1.9 + 2.2 (sema) / 3.5 (irgen) +- Sema type-error messages: `"Ok argument must be int"` / `"Err argument must be int"` / `"match target must be Result[int, int]"` match exactly between the implementation (Task 2.1, 2.2) and the negative-test asserts (Task 2.4) +- The argument order of `_emit_arm`, `(var_name, block, payload, ctx, end_bb)`, matches between Task 3.4 (definition) / 3.5 (call) +- The existing Option-style field names `some_var` / `some_block` / `none_block` are consistently retained in Task 1.4 (AST redefinition) / 1.7 (parser) / 1.9 (sema) / 3.5 (irgen) diff --git a/docs/plans/2026-05-30-for-range-loop.md b/docs/plans/2026-05-30-for-range-loop.md index fcde1a7..4c2aac6 100644 --- a/docs/plans/2026-05-30-for-range-loop.md +++ b/docs/plans/2026-05-30-for-range-loop.md @@ -2,38 +2,38 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** rw に `for in range(start, stop[, step])` ループを構文糖として導入する。 +**Goal:** Introduce a `for in range(start, stop[, step])` loop into rw as syntactic sugar. -**Architecture:** parser が新しい `For` AST ノードを生成し、parser 直後・sema 前に走る独立パス `desugar.py` が `For` を既存の `VarDecl` / `While` / `Assign` ノードへ書き換える。これにより sema・irgen・runtime は無改修。`range` は `for` ヘッダ位置でのみ受理し、値としては扱わない。`step==0` は desugar したループ条件が両辺 false になる性質で 0 回ループになる。 +**Architecture:** The parser produces a new `For` AST node, and an independent pass `desugar.py` — which runs immediately after parsing and before sema — rewrites `For` into the existing `VarDecl` / `While` / `Assign` nodes. This leaves sema, irgen, and the runtime unmodified. `range` is accepted only in the `for` header position and is never treated as a value. `step==0` yields a zero-iteration loop because the desugared loop condition is false on both sides. -**Tech Stack:** Python (rwc コンパイラ: lexer/parser/sema/irgen)、llvmlite、pytest、C ランタイム (無改修)。 +**Tech Stack:** Python (rwc compiler: lexer/parser/sema/irgen), llvmlite, pytest, C runtime (unmodified). -参照 spec: `docs/specs/13-for-range-loop.md` +Reference spec: `docs/specs/13-for-range-loop.md` --- ## File Structure -- **Modify** `rwc/ast_nodes.py` — `For` dataclass を追加、`Stmt` Union に追加 -- **Modify** `rwc/parser.py` — `parse_for()` 追加、`parse_stmt` のディスパッチに `KW_FOR` 追加 -- **Create** `rwc/desugar.py` — `desugar_module(mod)` パス。`For` を `While` 等へ展開 -- **Modify** `rwc/driver.py` — `compile_source` / `emit_ir` / `emit_ast` の parse 直後に desugar を挿入 -- **Modify** `tests/test_parser.py` — for のパース結果テスト -- **Create** `tests/test_desugar.py` — desugar 展開のテスト -- **Modify** `tests/test_sema.py` — for の negative テスト (型エラー) -- **Create** `examples/for_count.rw` + `examples/for_count.rw.expected` — e2e サンプル -- **Modify** `tests/test_e2e.py` — parametrize に `for_count` 追加 +- **Modify** `rwc/ast_nodes.py` — add the `For` dataclass; add it to the `Stmt` Union +- **Modify** `rwc/parser.py` — add `parse_for()`; add `KW_FOR` to the `parse_stmt` dispatch +- **Create** `rwc/desugar.py` — the `desugar_module(mod)` pass. Expand `For` into `While` etc. +- **Modify** `rwc/driver.py` — insert desugar immediately after parse in `compile_source` / `emit_ir` / `emit_ast` +- **Modify** `tests/test_parser.py` — tests for the for parse result +- **Create** `tests/test_desugar.py` — tests for desugar expansion +- **Modify** `tests/test_sema.py` — negative tests for for (type errors) +- **Create** `examples/for_count.rw` + `examples/for_count.rw.expected` — e2e sample +- **Modify** `tests/test_e2e.py` — add `for_count` to parametrize --- -## Task 1: `For` AST ノードを追加 +## Task 1: Add the `For` AST node **Files:** - Modify: `rwc/ast_nodes.py` -- [ ] **Step 1: `For` dataclass を `While` の直後 (L193 付近) に追加** +- [ ] **Step 1: Add the `For` dataclass directly after `While` (around L193)** -`rwc/ast_nodes.py` の `While` クラス定義の直後に挿入: +Insert directly after the `While` class definition in `rwc/ast_nodes.py`: ```python @dataclass @@ -47,18 +47,18 @@ class For: col: int ``` -- [ ] **Step 2: `Stmt` Union に `For` を追加** +- [ ] **Step 2: Add `For` to the `Stmt` Union** -`rwc/ast_nodes.py` の `Stmt = Union[...]` 行 (現 L212) を変更: +Change the `Stmt = Union[...]` line (currently L212) in `rwc/ast_nodes.py`: ```python Stmt = Union[VarDecl, Assign, ExprStmt, Return, If, While, For, MatchStmt] ``` -- [ ] **Step 3: import が壊れていないか確認** +- [ ] **Step 3: Confirm the import is not broken** Run: `uv run python -c "from rwc import ast_nodes as A; A.For"` -Expected: エラーなし (何も出力されない) +Expected: no error (nothing printed) - [ ] **Step 4: Commit** @@ -69,15 +69,15 @@ git commit -m "ast: add For node for range-based loops" --- -## Task 2: parser に `for ... in range(...)` を追加 +## Task 2: Add `for ... in range(...)` to the parser **Files:** - Modify: `rwc/parser.py` - Test: `tests/test_parser.py` -- [ ] **Step 1: 失敗するテストを書く** +- [ ] **Step 1: Write a failing test** -`tests/test_parser.py` の末尾に追加: +Append to the end of `tests/test_parser.py`: ```python def test_parse_for_two_args(): @@ -129,23 +129,23 @@ def test_parse_for_four_args_is_error(): parse_src(src) ``` -- [ ] **Step 2: テストが失敗することを確認** +- [ ] **Step 2: Confirm the test fails** Run: `uv run pytest tests/test_parser.py -k for_ -v` -Expected: FAIL (`for` がパースできず ParserError、または For ノードが生成されない) +Expected: FAIL (`for` cannot be parsed, raising ParserError, or no For node is produced) -- [ ] **Step 3: `parse_stmt` のディスパッチに `KW_FOR` を追加** +- [ ] **Step 3: Add `KW_FOR` to the `parse_stmt` dispatch** -`rwc/parser.py` の `parse_stmt` (現 L239 付近)、`KW_WHILE` の分岐の直後に追加: +Add directly after the `KW_WHILE` branch in `parse_stmt` (around L239) of `rwc/parser.py`: ```python if t.kind == TokenKind.KW_FOR: return self.parse_for() ``` -- [ ] **Step 4: `parse_for()` を実装** +- [ ] **Step 4: Implement `parse_for()`** -`rwc/parser.py` の `parse_while` メソッド (現 L302) の直後に追加: +Add directly after the `parse_while` method (currently L302) in `rwc/parser.py`: ```python def parse_for(self) -> A.For: @@ -189,18 +189,20 @@ Expected: FAIL (`for` がパースできず ParserError、または For ノー return A.For(var_tok.value, start, stop, step, body, kw.line, kw.col) ``` -> 注: `range` を for 外で使うと、通常の式パスで `Call("range", ...)` が生成され -> sema が「未定義の関数 range」で弾く。parser でも for ヘッダ以外では `range` -> をビルトイン化していないため、`x = range(0,5)` は sema の段階でエラーになる。 -> ただし negative テスト `test_parse_range_outside_for_is_error` は ParserError を -> 期待しているので、Step 5 で挙動を確認し、ParserError でなく CompileError に -> なる場合はテスト側を `test_sema.py` の負ケースへ移す (Step 5 参照)。 +> Note: If `range` is used outside a for, the ordinary expression path +> produces `Call("range", ...)` and sema rejects it with "undefined function +> range". Since the parser does not treat `range` as a builtin outside the +> for header either, `x = range(0,5)` becomes an error at the sema stage. +> However, the negative test `test_parse_range_outside_for_is_error` expects a +> ParserError, so verify the behavior in Step 5; if it becomes a CompileError +> rather than a ParserError, move that test to the negative cases in +> `test_sema.py` (see Step 5). -- [ ] **Step 5: テストを実行して確認** +- [ ] **Step 5: Run the tests and confirm** Run: `uv run pytest tests/test_parser.py -k for_ -v` -Expected: positive 3 件 PASS。`test_parse_for_zero_args_is_error` / `test_parse_for_four_args_is_error` PASS。 -`test_parse_range_outside_for_is_error` は `range` が式として通ってしまう場合 FAIL する。その場合は当該テストを `tests/test_parser.py` から削除し、Task 5 の sema negative テスト (`test_for_range_outside_is_sema_error`) でカバーする。実際の挙動に合わせてどちらか一方に置く。 +Expected: the 3 positive cases PASS. `test_parse_for_zero_args_is_error` / `test_parse_for_four_args_is_error` PASS. +`test_parse_range_outside_for_is_error` FAILs if `range` passes as an expression. In that case, remove that test from `tests/test_parser.py` and cover it with the sema negative test in Task 5 (`test_for_range_outside_is_sema_error`). Place it in one location or the other according to the actual behavior. - [ ] **Step 6: Commit** @@ -211,15 +213,15 @@ git commit -m "parser: parse for-in-range loop header into For node" --- -## Task 3: desugar パスを実装 +## Task 3: Implement the desugar pass **Files:** - Create: `rwc/desugar.py` - Test: `tests/test_desugar.py` -- [ ] **Step 1: 失敗するテストを書く** +- [ ] **Step 1: Write failing tests** -`tests/test_desugar.py` を新規作成: +Create `tests/test_desugar.py`: ```python from __future__ import annotations @@ -266,14 +268,14 @@ def test_for_body_ends_with_increment(): assert isinstance(last.value, A.BinOp) and last.value.op == "+" ``` -- [ ] **Step 2: テストが失敗することを確認** +- [ ] **Step 2: Confirm the tests fail** Run: `uv run pytest tests/test_desugar.py -v` Expected: FAIL (`ModuleNotFoundError: rwc.desugar`) -- [ ] **Step 3: `rwc/desugar.py` を実装** +- [ ] **Step 3: Implement `rwc/desugar.py`** -`rwc/desugar.py` を新規作成: +Create `rwc/desugar.py`: ```python """Desugaring pass: lower syntactic-sugar AST nodes to core nodes. @@ -373,10 +375,10 @@ def desugar_module(mod: A.Module) -> A.Module: return _Desugarer().module(mod) ``` -- [ ] **Step 4: テストを実行して確認** +- [ ] **Step 4: Run the tests and confirm** Run: `uv run pytest tests/test_desugar.py -v` -Expected: 3 件すべて PASS +Expected: all 3 PASS - [ ] **Step 5: Commit** @@ -387,22 +389,22 @@ git commit -m "desugar: lower For range loops to While + assignments" --- -## Task 4: driver に desugar を組み込む +## Task 4: Wire desugar into the driver **Files:** - Modify: `rwc/driver.py` -- [ ] **Step 1: import を追加** +- [ ] **Step 1: Add the import** -`rwc/driver.py` の import 群 (現 L24 `from .parser import ...` の直後) に追加: +Add to the imports in `rwc/driver.py` (directly after L24 `from .parser import ...`): ```python from .desugar import desugar_module ``` -- [ ] **Step 2: `compile_source` の parse 直後に desugar を挿入** +- [ ] **Step 2: Insert desugar immediately after parse in `compile_source`** -`rwc/driver.py` の `compile_source` 内、`ast = parse(tokens)` の直後 (現 L82) を: +In `compile_source` of `rwc/driver.py`, directly after `ast = parse(tokens)` (currently L82): ```python tokens = tokenize(source, filename=filename) @@ -412,9 +414,9 @@ from .desugar import desugar_module llmod = irgen_generate(ast, sema) ``` -- [ ] **Step 3: `emit_ir` にも同じ挿入** +- [ ] **Step 3: Make the same insertion in `emit_ir`** -`rwc/driver.py` の `emit_ir` 内 (現 L128-131) を: +In `emit_ir` of `rwc/driver.py` (currently L128-131): ```python tokens = tokenize(source, filename=filename) @@ -424,9 +426,9 @@ from .desugar import desugar_module llmod = irgen_generate(ast, sema) ``` -- [ ] **Step 4: `emit_ast` にも同じ挿入** +- [ ] **Step 4: Make the same insertion in `emit_ast`** -`rwc/driver.py` の `emit_ast` 内 (現 L136-138) を: +In `emit_ast` of `rwc/driver.py` (currently L136-138): ```python def emit_ast(source: str, filename: str) -> ASTModule: @@ -434,7 +436,7 @@ def emit_ast(source: str, filename: str) -> ASTModule: return desugar_module(parse(tokens)) ``` -- [ ] **Step 5: パイプライン全体が通ることを確認 (一時ファイルで)** +- [ ] **Step 5: Confirm the full pipeline passes (with a temporary file)** Run: ```bash @@ -445,7 +447,7 @@ ir = emit_ir(src, filename='t.rw') print('rw_user_main' in ir) " ``` -Expected: `True` (for が desugar→sema→irgen を通って IR 生成される) +Expected: `True` (the for goes through desugar → sema → irgen and IR is generated) - [ ] **Step 6: Commit** @@ -456,18 +458,18 @@ git commit -m "driver: run desugar pass between parse and sema on all paths" --- -## Task 5: sema の negative テストを追加 +## Task 5: Add sema negative tests **Files:** - Test: `tests/test_sema.py` -> 目的: for 引数が非 int のとき型エラーになること、および `range` を for 外で -> 使うとエラーになること (parser か sema のどちらで弾かれるかは Task 2 Step 5 の -> 実挙動に従う) を固定する。 +> Purpose: pin down that a type error occurs when a for argument is non-int, +> and that using `range` outside a for is an error (whether it is rejected by +> the parser or sema follows the actual behavior from Task 2 Step 5). -- [ ] **Step 1: テストを追加** +- [ ] **Step 1: Add the tests** -`tests/test_sema.py` の末尾に追加 (`check` / `err` ヘルパは既存): +Append to the end of `tests/test_sema.py` (the `check` / `err` helpers already exist): ```python def test_for_loop_int_args_ok(): @@ -503,10 +505,10 @@ def test_for_loop_non_int_stop_is_error(): analyze(desugar_module(parse(tokenize(src))), filename="t.rw") ``` -- [ ] **Step 2: テストを実行して確認** +- [ ] **Step 2: Run the tests and confirm** Run: `uv run pytest tests/test_sema.py -k for_loop -v` -Expected: 2 件 PASS +Expected: 2 PASS - [ ] **Step 3: Commit** @@ -517,16 +519,16 @@ git commit -m "sema: tests for for-loop int typing" --- -## Task 6: e2e サンプルと期待値 +## Task 6: e2e sample and expected values **Files:** - Create: `examples/for_count.rw` - Create: `examples/for_count.rw.expected` - Modify: `tests/test_e2e.py` -- [ ] **Step 1: サンプルを作成** +- [ ] **Step 1: Create the sample** -`examples/for_count.rw` を新規作成: +Create `examples/for_count.rw`: ``` def main() -> int: @@ -549,21 +551,21 @@ def main() -> int: return 0 ``` -> total = 0+1+...+9 = 45、down = 10+9+...+1 = 55、step2 = 0+2+4+6+8 = 20、 -> empty は 0 回ループなので 0。 +> total = 0+1+...+9 = 45, down = 10+9+...+1 = 55, step2 = 0+2+4+6+8 = 20, +> empty is a zero-iteration loop so it is 0. -- [ ] **Step 2: print の出力フォーマットを確認して期待値を作る** +- [ ] **Step 2: Check print's output format and build the expected value** Run: ```bash uv run python -m rwc.cli run examples/for_count.rw ``` -Expected: 4 行の数値出力。実際の出力 (改行・整形含む) を確認する。 +Expected: four lines of numeric output. Verify the actual output (including newlines and formatting). -- [ ] **Step 3: 確認した出力で `.expected` を作る** +- [ ] **Step 3: Build `.expected` from the verified output** -Step 2 の実出力をそのまま `examples/for_count.rw.expected` に保存する。 -他の例 (`examples/while_count.rw.expected`) と同じ整形である想定。想定値: +Save the actual output from Step 2 verbatim into `examples/for_count.rw.expected`. +It is expected to use the same formatting as the other examples (`examples/while_count.rw.expected`). Expected value: ``` 45 @@ -572,11 +574,11 @@ Step 2 の実出力をそのまま `examples/for_count.rw.expected` に保存す 0 ``` -(Step 2 の実出力と差異があれば実出力を正とする) +(If there is any difference from the actual output in Step 2, treat the actual output as authoritative.) -- [ ] **Step 4: parametrize に追加** +- [ ] **Step 4: Add to parametrize** -`tests/test_e2e.py` の `@pytest.mark.parametrize` リスト (現 L52 付近) の末尾に `"for_count"` を追加: +Add `"for_count"` to the end of the `@pytest.mark.parametrize` list (around L52) in `tests/test_e2e.py`: ```python @pytest.mark.parametrize( @@ -585,7 +587,7 @@ Step 2 の実出力をそのまま `examples/for_count.rw.expected` に保存す ) ``` -- [ ] **Step 5: e2e テストを実行** +- [ ] **Step 5: Run the e2e test** Run: `uv run pytest tests/test_e2e.py -k for_count -v` Expected: PASS @@ -599,34 +601,34 @@ git commit -m "examples: add for_count exercising for-in-range loops" --- -## Task 7: 全テスト緑を確認 +## Task 7: Confirm all tests green -**Files:** なし (検証のみ) +**Files:** none (verification only) -- [ ] **Step 1: 全テストを実行** +- [ ] **Step 1: Run all tests** Run: `uv run pytest -q` -Expected: 全 PASS (既存テストの回帰なし、新規テストすべて緑) +Expected: all PASS (no regression in existing tests, all new tests green) -- [ ] **Step 2: emit-ast で desugar 後の姿を目視確認 (任意)** +- [ ] **Step 2: Visually inspect the post-desugar form with emit-ast (optional)** Run: `uv run python -m rwc.cli emit-ast examples/for_count.rw` -Expected: `For` ノードが現れず、`While` + `VarDecl` + `Assign` に展開されている +Expected: no `For` node appears; it is expanded into `While` + `VarDecl` + `Assign` --- -## Self-Review (記入済み) +## Self-Review (completed) **Spec coverage:** -- 1〜3 引数 range → Task 2 (parser でデフォルト補完) -- 任意 int 式 → Task 2 (`parse_expr` で引数を取る) + Task 5 (型チェック) -- 負 step / 半開区間 → Task 3 (条件式 `(step>0 and vstop)`) -- step==0 が 0 回ループ → Task 3 (条件両辺 false) + Task 6 (empty で検証) -- `range` を値として使わせない → Task 2 (for ヘッダ以外で range をビルトイン化しない) -- 二重評価防止 → Task 3 (`__for_stop_N` / `__for_step_N` に束縛) -- sema/irgen/runtime 無改修 → Task 3/4 (desugar が core ノードのみ生成) -- 3 経路で desugar → Task 4 (compile_source / emit_ir / emit_ast) - -**Placeholder scan:** プレースホルダなし。`.expected` の値のみ Task 6 Step 2 で実出力を正とする旨を明記 (想定値も提示)。 - -**Type consistency:** `For(var, start, stop, step, body, line, col)` は Task 1 で定義し Task 2/3 で同一シグネチャを使用。`desugar_module` は Task 3 で定義し Task 4/5 で使用。一致。 +- 1-to-3-argument range → Task 2 (parser fills in defaults) +- Arbitrary int expressions → Task 2 (arguments taken via `parse_expr`) + Task 5 (type checking) +- Negative step / half-open interval → Task 3 (condition `(step>0 and vstop)`) +- step==0 yields a zero-iteration loop → Task 3 (both sides of the condition false) + Task 6 (verified via empty) +- Do not allow `range` to be used as a value → Task 2 (range is not made a builtin outside the for header) +- Prevent double evaluation → Task 3 (bound to `__for_stop_N` / `__for_step_N`) +- No changes to sema/irgen/runtime → Task 3/4 (desugar produces only core nodes) +- Desugar on 3 paths → Task 4 (compile_source / emit_ir / emit_ast) + +**Placeholder scan:** No placeholders. Only the `.expected` value is explicitly noted as taking the real output from Task 6 Step 2 as authoritative (the anticipated value is also provided). + +**Type consistency:** `For(var, start, stop, step, body, line, col)` is defined in Task 1 and used with the same signature in Tasks 2/3. `desugar_module` is defined in Task 3 and used in Tasks 4/5. Consistent. diff --git a/docs/plans/2026-06-01-async-file-io.md b/docs/plans/2026-06-01-async-file-io.md index a46c6ea..d0982ac 100644 --- a/docs/plans/2026-06-01-async-file-io.md +++ b/docs/plans/2026-06-01-async-file-io.md @@ -543,7 +543,7 @@ Confirm output matches spec #15 (`hello file` / `second line` / blank / `23`). - **Concurrency protocol is load-bearing:** the submit→`park_current` / worker→`enqueue_ready` order must match the netpoller exactly. Do not add a publish-before-save shortcut. See `docs/specs/16-async-file-io.md` → - "並行性の正しさ" and the stackful-coroutine-scheduling skill. + "Concurrency correctness" and the stackful-coroutine-scheduling skill. - **Task struct on the stack:** `rw_aio_task t;` lives on the calling fiber's stack; a pointer is queued. The fiber is parked (alive, stack retained) until the worker wakes it, so the pointer stays valid. Do not diff --git a/docs/plans/2026-06-01-file-io.md b/docs/plans/2026-06-01-file-io.md index 60e9528..051df6a 100644 --- a/docs/plans/2026-06-01-file-io.md +++ b/docs/plans/2026-06-01-file-io.md @@ -734,11 +734,11 @@ In `docs/specs/12-netpoller-tcp.md`, find where `tcp_read` / `tcp_write` / builtin names where they appear) indicating they were superseded: ```markdown -> **更新 (#33 / 15-file-io):** `tcp_read` / `tcp_write` / `tcp_close` は -> fd 汎用の `read` / `write` / `close` に統合された。ソケットの読み書き -> 閉じはこれらを使う (実装は `runtime/io.c`)。ソケット固有の -> `tcp_listen` / `tcp_accept` はそのまま。詳細は -> [`15-file-io.md`](15-file-io.md)。 +> **Update (#33 / 15-file-io):** `tcp_read` / `tcp_write` / `tcp_close` have +> been consolidated into the fd-generic `read` / `write` / `close`. Use these +> for reading, writing, and closing sockets (implemented in `runtime/io.c`). +> The socket-specific `tcp_listen` / `tcp_accept` remain unchanged. See +> [`15-file-io.md`](15-file-io.md) for details. ``` Place it near the top of the builtins section so a reader sees it before the diff --git a/docs/specs/01-overview.md b/docs/specs/01-overview.md index 59bf06b..0f3bf67 100644 --- a/docs/specs/01-overview.md +++ b/docs/specs/01-overview.md @@ -1,42 +1,38 @@ -# rw 言語 概要 +# rw Language Overview -## 何の言語か +## What kind of language is it? -rw は **Python の書き味で書ける、非同期ファーストの静的型コンパイル言語** である。 -LLVM をバックエンドに用い、macOS arm64 / Linux x86_64 のネイティブ実行ファイルを -生成する。 +rw is a **statically typed, async-first, compiled language with the ergonomics of Python**. +It uses LLVM as its backend and produces native executables for macOS arm64 / Linux x86_64. -## 設計の柱 +## Design pillars -1. **非同期が中心**: `Future[T]` は型システムの一級市民。`spawn`/`await` は予約語。 -2. **Python 似の見た目**: インデントベース、`def`、`elif`、`and/or/not`、`true/false`。 -3. **静的型・型注釈必須**: 引数・戻り値・ローカル変数すべて型注釈を書く。MVP は - 型推論なし。 -4. **薄いランタイム + LLVM**: Cで書いた `librw.a` をリンクし、コア機能(スレッド、 - Future、print)を提供。 -5. **学習・実験フレンドリー**: コンパイラは Python 製。`rwc emit-ir` / `emit-ast` - で内部を覗ける。 +1. **Async at the core**: `Future[T]` is a first-class citizen of the type system. `spawn`/`await` are reserved words. +2. **Python-like appearance**: indentation-based, `def`, `elif`, `and/or/not`, `true/false`. +3. **Static typing with mandatory type annotations**: arguments, return values, and local variables all require type annotations. The MVP has no type inference. +4. **Thin runtime + LLVM**: a `librw.a` written in C is linked in to provide core features (threads, Future, print). +5. **Learning- and experiment-friendly**: the compiler is written in Python. `rwc emit-ir` / `emit-ast` let you inspect the internals. -## ターゲット +## Targets - macOS arm64 - Linux x86_64 -Windows と組み込みは MVP では対象外。 +Windows and embedded targets are out of scope for the MVP. -## ツールチェーン +## Toolchain -| ツール | 用途 | +| Tool | Purpose | |---|---| -| Python 3.11 | コンパイラ本体 | -| llvmlite | LLVM IR 構築 | -| clang | リンカ呼び出し + librw.a とのリンク | -| make + cc | librw.a のビルド | +| Python 3.11 | The compiler itself | +| llvmlite | Building LLVM IR | +| clang | Invoking the linker + linking against librw.a | +| make + cc | Building librw.a | -## パイプライン +## Pipeline ``` -.rw → Lexer → Parser → Sema → IRGen → Driver → 実行ファイル +.rw → Lexer → Parser → Sema → IRGen → Driver → executable ↓ librw.a (C) ``` @@ -50,17 +46,17 @@ rwc emit-ir foo.rw rwc emit-ast foo.rw ``` -## MVP のゴール +## MVP goal -`examples/` 配下の 7 本(hello, arith, fib, while_count, spawn_basic, spawn_many, -spawn_string)が **macOS arm64 と Linux x86_64 の両方で緑** になること。 +The 7 programs under `examples/` (hello, arith, fib, while_count, spawn_basic, spawn_many, +spawn_string) should be **green on both macOS arm64 and Linux x86_64**. -## やらないこと(将来拡張) +## Non-goals (future extensions) - list / dict / for / class / import -- 型推論 -- GC(現状はリーク許容、Future と malloc/free ペアのみ) -- 文字列連結・スライス -- Python 直接呼び出し(将来は `extern "c"` + プロセス分離で対応) -- 例外 / Result 型 -- 複数エラー回復(MVP は最初のエラーで停止) +- Type inference +- GC (leaks are tolerated for now; only Future and malloc/free pairs are managed) +- String concatenation / slicing +- Direct Python calls (to be supported later via `extern "c"` + process isolation) +- Exceptions / Result type +- Multi-error recovery (the MVP stops at the first error) diff --git a/docs/specs/02-syntax-and-types.md b/docs/specs/02-syntax-and-types.md index 4c30528..2b9ba06 100644 --- a/docs/specs/02-syntax-and-types.md +++ b/docs/specs/02-syntax-and-types.md @@ -1,39 +1,39 @@ -# rw 構文と型システム +# rw Syntax and Type System -## リテラルと基本型 +## Literals and primitive types -| 型 | リテラル例 | LLVM 表現 | +| Type | Literal examples | LLVM representation | |---|---|---| | `int` | `42`, `-7`, `0` | `i64` | | `float` | `3.14`, `-0.5` | `double` | -| `bool` | `true`, `false` | `i1`(関数 ABI 上は `i8`) | -| `string` | `"hello"` | `{i64 len, i8* ptr}`(不変、連結不可) | -| `Future[T]` | リテラルなし | `i8*`(不透明) | +| `bool` | `true`, `false` | `i1` (`i8` in the function ABI) | +| `string` | `"hello"` | `{i64 len, i8* ptr}` (immutable, not concatenable) | +| `Future[T]` | no literal | `i8*` (opaque) | -## 演算子 +## Operators -- 算術: `+ - * / %`(同じ型同士のみ) -- 比較: `== != < <= > >=` -- 論理: `and`, `or`, `not` -- 条件式(三項): `then if cond else els`(最も低い優先度、右結合) -- 代入: `=`(再代入可、ただし型は不変) +- Arithmetic: `+ - * / %` (only between operands of the same type) +- Comparison: `== != < <= > >=` +- Logical: `and`, `or`, `not` +- Conditional expression (ternary): `then if cond else els` (lowest precedence, right-associative) +- Assignment: `=` (reassignment is allowed, but the type is fixed) -### 条件式(三項演算子) +### Conditional expression (ternary operator) -Python 互換の `then if cond else els`。`cond` は `bool`、`then` と `els` は -同じ型で、その型が式全体の型になる(暗黙の型昇格はしない)。選ばれたブランチ -のみが評価される。 +The Python-compatible `then if cond else els`. `cond` is a `bool`; `then` and `els` must +have the same type, which becomes the type of the whole expression (there is no implicit +type promotion). Only the selected branch is evaluated. ```python larger: int = a if a > b else b label: string = "even" if a % 2 == 0 else "odd" -# 右結合: a if p else (b if q else c) +# right-associative: a if p else (b if q else c) sign: int = 1 if n > 0 else 0 if n == 0 else -1 ``` -詳細は [`14-ternary-expr.md`](14-ternary-expr.md) を参照。 +See [`14-ternary-expr.md`](14-ternary-expr.md) for details. -## 関数定義 +## Function definitions ```python def add(a: int, b: int) -> int: @@ -43,14 +43,14 @@ def greet(name: string) -> void: print(name) ``` -- 引数と戻り値の型注釈は **必須** -- 戻り値なしは `-> void` -- ローカル変数も型注釈必須: +- Type annotations on arguments and return values are **mandatory** +- No return value is written as `-> void` +- Local variables also require type annotations: ```python x: int = 1 + 2 ``` -## 制御構文 +## Control flow ```python if x > 0: @@ -64,17 +64,18 @@ while i < 10: i = i + 1 ``` -条件で値を選ぶときは、文の `if`/`else` の代わりに条件式(三項演算子)も使える: +When choosing a value based on a condition, you can use a conditional expression (ternary +operator) instead of a statement-level `if`/`else`: ```python larger: int = a if a > b else b ``` -`for ... in range(...)` のカウントループも利用できる(詳細は -[`13-for-range-loop.md`](13-for-range-loop.md))。`for in ` の -イテレータ形式は未対応。 +A `for ... in range(...)` counting loop is also available (see +[`13-for-range-loop.md`](13-for-range-loop.md)). The `for in ` iterator form +is not yet supported. -## 非同期構文 +## Async syntax ```python def fetch(n: int) -> int: @@ -85,11 +86,11 @@ result: int = await f print(result) ``` -- `spawn 式` は **関数呼び出しを別スレッドで実行** し、`Future[T]` を返す -- `await 式` は `Future[T]` を完了まで待ち `T` を取り出す -- `spawn` の対象は **関数呼び出しのみ**(任意の式ではない) +- `spawn expr` **runs a function call on a separate thread** and returns a `Future[T]` +- `await expr` waits for the `Future[T]` to complete and extracts the `T` +- The target of `spawn` must be **a function call only** (not an arbitrary expression) -## main 関数 +## The main function ```python def main() -> int: @@ -97,26 +98,26 @@ def main() -> int: return 0 ``` -`main` は必須。戻り値はプロセス終了コード(`i64` → `i32` に切り詰め)。 +`main` is required. Its return value is the process exit code (`i64` truncated to `i32`). -## コメント +## Comments ```python -# 行コメント。複数行は # を連ねる。 +# Line comment. Use consecutive # for multiple lines. ``` -## 予約語 +## Reserved words ``` def return if elif else while and or not true false void spawn await Future int float bool string -# 将来用に予約(MVP では構文エラー): +# Reserved for future use (syntax error in the MVP): extern class import for in as None ``` -## 字句構造の注意点 +## Notes on lexical structure -- インデントはスペース 4 個推奨だが、ファイル内で一貫していれば 2 でも tab でもよい -- タブとスペースの混在は **同じブロック内で禁止**(Lexer がエラー) -- 空行・コメント行はインデント計算から除外 +- 4 spaces of indentation is recommended, but 2 spaces or tabs are fine as long as they are consistent within the file +- Mixing tabs and spaces **within the same block is forbidden** (the Lexer errors out) +- Blank lines and comment-only lines are excluded from indentation computation diff --git a/docs/specs/03-runtime-and-irgen.md b/docs/specs/03-runtime-and-irgen.md index 98eee43..3b45399 100644 --- a/docs/specs/03-runtime-and-irgen.md +++ b/docs/specs/03-runtime-and-irgen.md @@ -1,8 +1,8 @@ -# rw ランタイム ABI と IR 生成方針 +# rw Runtime ABI and IR Generation Strategy -## ランタイム ABI +## Runtime ABI -ヘッダ `runtime/runtime.h`: +Header `runtime/runtime.h`: ```c #ifndef RW_RUNTIME_H @@ -18,10 +18,10 @@ void rw_print_f64(double v); void rw_print_bool(int8_t v); void rw_print_str(rw_str s); -/* 文字列ヘルパ */ +/* string helpers */ rw_str rw_str_from_cstr(const char *cstr, int64_t len); -/* spawn / await(戻り値型ごとに分離) */ +/* spawn / await (separated per return type) */ rw_future_t *rw_spawn_i64 (int64_t (*fn)(void *), void *args); rw_future_t *rw_spawn_f64 (double (*fn)(void *), void *args); rw_future_t *rw_spawn_bool(int8_t (*fn)(void *), void *args); @@ -34,35 +34,34 @@ int8_t rw_await_bool(rw_future_t *f); rw_str rw_await_str (rw_future_t *f); void rw_await_void(rw_future_t *f); -/* プロセス init / shutdown(main 冒頭・末尾で rwc が挿入) */ +/* process init / shutdown (rwc inserts these at the start/end of main) */ void rw_init(void); void rw_shutdown(void); #endif ``` -実装方針(runtime.c): -- `rw_spawn_*` は内部で `pthread_create` を呼び、`rw_future_t` に - スレッド ID と結果格納域を保持する -- `rw_await_*` は `pthread_join` 後、結果を返して `rw_future_t` を `free` する -- `rw_print_*` は単純に `printf` 系を呼ぶ -- `rw_init` / `rw_shutdown` は MVP では空でよい(将来のスレッドプール用) +Implementation strategy (runtime.c): +- `rw_spawn_*` internally calls `pthread_create` and stores the thread ID and a result slot in the `rw_future_t` +- `rw_await_*` calls `pthread_join`, then returns the result and `free`s the `rw_future_t` +- `rw_print_*` simply calls the `printf` family +- `rw_init` / `rw_shutdown` may be empty in the MVP (reserved for a future thread pool) -## `spawn f(a, b)` の IR 展開 +## IR expansion of `spawn f(a, b)` -rw コード: +rw code: ```python fut: Future[int] = spawn add(3, 4) ``` -rwc が生成するもの: +What rwc generates: -1. **クロージャ構造体** を匿名で定義: +1. **A closure struct** defined anonymously: ```llvm %closure_add_0 = type { i64, i64 } ``` -2. **トランポリン関数** を生成(呼び出しサイトごとにユニーク): +2. **A trampoline function** (unique per call site): ```llvm define i64 @rw_trampoline_add_0(i8* %args) { %p = bitcast i8* %args to %closure_add_0* @@ -76,24 +75,24 @@ rwc が生成するもの: } ``` -3. **呼び出し側**(spawn 式の展開): +3. **The call site** (expansion of the spawn expression): ```llvm %args = call i8* @malloc(i64 16) - ; %a, %b を struct にストア + ; store %a, %b into the struct %fut = call %rw_future_t* @rw_spawn_i64( i64 (i8*)* @rw_trampoline_add_0, i8* %args) ``` -## `await fut` の IR 展開 +## IR expansion of `await fut` -戻り値型は Sema で既知。対応する `rw_await_*` を直接呼ぶ: +The return type is known from Sema. The corresponding `rw_await_*` is called directly: ```llvm %v = call i64 @rw_await_i64(%rw_future_t* %fut) ``` -## main 関数 +## The main function -rwc は `def main() -> int:` を必須とし、生成 IR では: +rwc requires `def main() -> int:`, and the generated IR is: ```llvm define i32 @main() { @@ -105,42 +104,42 @@ define i32 @main() { } ``` -`@rw_user_main` はユーザー定義 `main` を改名したもの。 +`@rw_user_main` is the renamed form of the user-defined `main`. -## メモリ管理(MVP) +## Memory management (MVP) -| 対象 | 方針 | +| Subject | Strategy | |---|---| -| 文字列リテラル | `.rodata` に置く。`rw_str` は長さ+ポインタ。解放しない | -| クロージャ構造体 | `malloc` / トランポリン末尾で `free` | -| Future | `rw_await_*` 内で `free` | -| ユーザー定義型 | MVP では存在しない | +| String literals | Placed in `.rodata`. `rw_str` is length + pointer. Never freed | +| Closure structs | `malloc` / `free`d at the end of the trampoline | +| Future | `free`d inside `rw_await_*` | +| User-defined types | Do not exist in the MVP | -GC は導入しない。文字列を動的生成しない限りリークは発生しない。 +No GC is introduced. As long as strings are not dynamically generated, no leaks occur. -将来 `list[T]` や文字列連結など動的なヒープ確保を伴う機能を入れる際、 -メモリ管理方針(ARC / マーク&スイープ GC / 所有権)を選ぶ必要がある。 -**現時点では決定を保留する**。判断材料がそろうのは: +When features that entail dynamic heap allocation — such as `list[T]` or string +concatenation — are added later, a memory-management strategy (ARC / mark-and-sweep GC / +ownership) will need to be chosen. **That decision is deferred for now.** The inputs +needed to decide will only be clear once the following are settled: -- ユーザーが何を書きたいか(Web サーバー? 数値計算? Python ライブラリ連携?) -- 並行性能(fiber と GC の相性、ARC のアトミック retain/release のコスト) -- 標準ライブラリの形(文字列・配列・dict が言語仕様にどう収まるか) +- What users want to write (a web server? numerical computing? Python-library interop?) +- Concurrency performance (how well fibers and GC interact, the cost of atomic retain/release in ARC) +- The shape of the standard library (how strings, arrays, and dicts fit into the language spec) -がはっきりしてからになる。`docs/specs/06-memory-tbd.md` をいずれ作成して -論点を整理する予定。 +We plan to eventually create `docs/specs/06-memory-tbd.md` to organize these discussion points. -## エラー処理(MVP) +## Error handling (MVP) -例外なし、Result 型なし。実行時に死ぬ条件: -- 整数ゼロ除算 → LLVM `sdiv` の未定義動作に任せる -- スレッド生成失敗 → `rw_spawn_*` 内で `perror` + `exit(1)` -- malloc 失敗 → 同上 +No exceptions, no Result type. Conditions that cause the process to die at runtime: +- Integer division by zero → left to LLVM `sdiv`'s undefined behavior +- Thread creation failure → `perror` + `exit(1)` inside `rw_spawn_*` +- malloc failure → same as above -## 将来予約(MVP では未実装エラー) +## Reserved for the future (not-yet-implemented error in the MVP) ```python extern "c" def name(arg: int) -> int ``` -この構文は Lexer/Parser では受理し、Sema で「未実装」エラーを出す。 -将来 Phase 2+ で実装するときの後方互換のため。 +This syntax is accepted by the Lexer/Parser but Sema emits a "not implemented" error. +This is for backward compatibility when it is implemented in Phase 2+. diff --git a/docs/specs/04-diagnostics-and-testing.md b/docs/specs/04-diagnostics-and-testing.md index 6e78d51..6567dbc 100644 --- a/docs/specs/04-diagnostics-and-testing.md +++ b/docs/specs/04-diagnostics-and-testing.md @@ -1,8 +1,8 @@ -# rw 診断とテスト戦略 +# rw Diagnostics and Testing Strategy -## 診断フォーマット +## Diagnostic format -すべての診断は **`file:line:col` + キャレット + メッセージ** の3点セット。 +Every diagnostic is a three-part set: **`file:line:col` + caret + message**. ``` error: type mismatch @@ -12,7 +12,7 @@ error: type mismatch | ^^^^^^^ expected `int`, found `string` ``` -`diagnostics.py` は単一クラス `Diagnostic` を提供: +`diagnostics.py` provides a single class `Diagnostic`: ```python @dataclass @@ -27,68 +27,68 @@ class Diagnostic: def render(self, source: str) -> str: ... ``` -MVP は **最初のエラーで停止**。エラー回復(複数エラー収集)は将来。 +The MVP **stops at the first error**. Error recovery (collecting multiple errors) is future work. -## 診断を出す箇所 +## Where diagnostics are emitted -| ステージ | 例 | +| Stage | Examples | |---|---| | Lexer | unterminated string, inconsistent indentation | | Parser | unexpected token, expected `:` after function signature | | Sema | undefined variable, type mismatch, wrong argument count, await on non-Future | -| IRGen | 出さない(Sema で全部弾く) | +| IRGen | none (everything is rejected in Sema) | | Driver | clang not found, link error | -## テスト構成 +## Test structure -`pytest` で全部回す。4階層: +Everything runs under `pytest`. Four tiers: -### 1. 単体テスト +### 1. Unit tests -- `tests/test_lexer.py`: 入力文字列 → トークン列を検証。INDENT/DEDENT、空行、 - コメント行、インデント混在検出 -- `tests/test_parser.py`: トークン列 → AST を `repr` 比較 -- `tests/test_sema.py`: AST → 型付き AST、または期待される `Diagnostic` +- `tests/test_lexer.py`: input string → verify the token stream. INDENT/DEDENT, blank lines, + comment lines, mixed-indentation detection +- `tests/test_parser.py`: token stream → compare the AST via `repr` +- `tests/test_sema.py`: AST → typed AST, or the expected `Diagnostic` -### 2. IRGen スナップショットテスト +### 2. IRGen snapshot tests `tests/test_irgen.py` + `tests/snapshots/*.ll`: -- rw ソース → 生成 LLVM IR を文字列化 -- 既存スナップショットと差分があれば失敗 -- 意図変更は `pytest --update-snapshots` で更新 +- rw source → stringify the generated LLVM IR +- Fail if it differs from the existing snapshot +- Intentional changes are updated with `pytest --update-snapshots` -### 3. E2E テスト +### 3. E2E tests `tests/test_e2e.py`: -- `examples/*.rw` を `rwc build` でコンパイル → 実行 → 標準出力を期待値と比較 -- 期待値は `examples/*.rw.expected` に同名で配置 -- CI は macOS arm64 + Linux x86_64 の両方で実行 +- Compile `examples/*.rw` with `rwc build` → run → compare stdout against the expected value +- Expected values live alongside as `examples/*.rw.expected` +- CI runs on both macOS arm64 and Linux x86_64 -### 4. 診断テスト +### 4. Diagnostic tests `tests/test_diagnostics.py` + `tests/bad/*.rw`: -- エラーになるべき rw コードを集める -- ファイル先頭コメントに期待エラーを書く: +- Collect rw code that should error +- Write the expected error in a comment at the top of the file: ```python # ERROR: type mismatch # ERROR_LINE: 3 x: int = "hello" ``` -- ランナーがコメントを抜き出し `rwc build` の出力と照合 +- The runner extracts the comments and matches them against the output of `rwc build` -## MVP の examples(受け入れテスト相当) +## MVP examples (equivalent to acceptance tests) -| ファイル | 検証内容 | +| File | What it verifies | |---|---| -| `examples/hello.rw` | 文字列リテラルと `print` | -| `examples/arith.rw` | 整数・浮動小数・bool・比較・if/else | -| `examples/fib.rw` | 再帰関数 `fib(20)` | -| `examples/while_count.rw` | `while` ループで 1..10 | -| `examples/spawn_basic.rw` | `spawn add(3,4)` → `await` で 7 | -| `examples/spawn_many.rw` | 4 スレッド並列で合計計算 | -| `examples/spawn_string.rw` | `Future[string]` を返す関数 | +| `examples/hello.rw` | String literals and `print` | +| `examples/arith.rw` | Integers, floats, bools, comparisons, if/else | +| `examples/fib.rw` | Recursive function `fib(20)` | +| `examples/while_count.rw` | A `while` loop over 1..10 | +| `examples/spawn_basic.rw` | `spawn add(3,4)` → `await` yields 7 | +| `examples/spawn_many.rw` | Summation across 4 parallel threads | +| `examples/spawn_string.rw` | A function returning `Future[string]` | -**これら 7 本が緑になれば MVP 完成。** +**Once these 7 are green, the MVP is complete.** ## CI @@ -108,4 +108,4 @@ steps: - run: pytest -v ``` -clang はどちらのランナーにも標準で入っているので追加インストール不要。 +clang ships by default on both runners, so no extra installation is needed. diff --git a/docs/specs/05-fibers.md b/docs/specs/05-fibers.md index 2493450..894af21 100644 --- a/docs/specs/05-fibers.md +++ b/docs/specs/05-fibers.md @@ -1,38 +1,42 @@ -# rw fiber ランタイム +# rw fiber runtime -このドキュメントは、当初の `OS スレッド + Future` 方式(`docs/specs/03-runtime-and-irgen.md`)を -**緑色スレッド(stackful coroutine + 小さいスタック)** に置き換えた新ランタイムを -説明する。コンパイラ側の ABI(`rw_spawn_*` / `rw_await_*` / `rw_future_t`)は -完全に保たれているので、ユーザーが書く rw コードは何も変わらない。 +This document describes the new runtime that replaces the original +**OS thread + Future** approach (`docs/specs/03-runtime-and-irgen.md`) with +**green threads (stackful coroutines + small stacks)**. The compiler-side ABI +(`rw_spawn_*` / `rw_await_*` / `rw_future_t`) is fully preserved, so the rw code +that users write does not change at all. -## モチベーション +## Motivation -`pthread_create` ベースの旧実装は **1 万 spawn を超えると破綻**する: +The old `pthread_create`-based implementation **breaks down beyond 10,000 spawns**: -- macOS のデフォルトスレッドスタックは 512KB、Linux は 8MB -- カーネル側の `task_struct` / `kthread` も 1 本あたり数 KB -- `ulimit -u` のデフォルトが数千〜1 万のホストでは `pthread_create` が EAGAIN を返す -- コンテキストスイッチコストが OS スケジューラ経由で μs オーダー +- The default thread stack is 512KB on macOS and 8MB on Linux +- The kernel-side `task_struct` / `kthread` also costs several KB per thread +- On hosts where the default `ulimit -u` is a few thousand to ten thousand, + `pthread_create` returns EAGAIN +- Context-switch cost is on the order of microseconds because it goes through + the OS scheduler -これは「Web サーバー(C10k)を rw で書きたい」「数千ワーカーを spawn したい」 -というユースケースに合わなかった。fiber 版では: +This did not fit the use cases of "I want to write a web server (C10k) in rw" +or "I want to spawn thousands of workers." With the fiber version: -- **1 fiber あたり 64KB + ガード 2 ページ**(arm64 で 16K + 16K) -- **コンテキストスイッチはアセンブリ 1 関数**で、コール 1 回 + ロード/ストア数十命令 -- **OS リソースを使わない**(`pthread_create` を呼ばない) -- 実測: macOS arm64 で **10 万 fiber が 437ms** で完走 +- **64KB + 2 guard pages per fiber** (16K + 16K on arm64) +- **A context switch is a single assembly function**: one call plus a few dozen + load/store instructions +- **No OS resources are used** (it never calls `pthread_create`) +- Measured: **100,000 fibers finish in 437ms** on macOS arm64 -## 用語 +## Terminology -| 用語 | 意味 | +| Term | Meaning | |---|---| -| **fiber** | 軽量スレッド。1 本の OS スレッド上で多重化される実行コンテキスト | -| **fiber context** | callee-saved レジスタの保存領域(`rw_fiber_ctx`) | -| **scheduler** | READY キューを持ち、yield/完了時に次の fiber を選んで swap する小さなループ | -| **trampoline** | 新規 fiber を初回起動するときに「`x0 = arg` を準備してから `entry` にジャンプ」する小さなアセンブリ関数 | -| **ハンドル** | spawn された fiber を識別する不透明ポインタ(`rw_future_t` の正体) | +| **fiber** | A lightweight thread. An execution context multiplexed on top of a single OS thread | +| **fiber context** | The save area for callee-saved registers (`rw_fiber_ctx`) | +| **scheduler** | A small loop that holds a READY queue and, on yield/completion, picks the next fiber and swaps to it | +| **trampoline** | A small assembly function that, when starting a new fiber for the first time, "sets up `x0 = arg` and then jumps to `entry`" | +| **handle** | An opaque pointer that identifies a spawned fiber (the actual form of `rw_future_t`) | -## コンテキストスイッチ ABI +## Context-switch ABI `runtime/fiber/fiber.h`: @@ -46,30 +50,30 @@ typedef struct { void rw_fiber_swap(rw_fiber_ctx *old, rw_fiber_ctx *new); ``` -### arm64 のレジスタレイアウト +### arm64 register layout -| word index | 内容 | +| word index | contents | |---|---| -| 0..9 | x19..x28(integer callee-saved) | -| 10 | x29(FP) | -| 11 | x30(LR; 復帰先) | +| 0..9 | x19..x28 (integer callee-saved) | +| 10 | x29 (FP) | +| 11 | x30 (LR; return target) | | 12 | sp | -| 13..20 | d8..d15(FP callee-saved の下半分) | +| 13..20 | d8..d15 (lower half of the FP callee-saved registers) | -`stp` / `ldp` のペアロード/ストアを使うことで命令数を最小化している。 -詳細は `runtime/fiber/fiber_arm64.S` を参照。 +Using `stp` / `ldp` paired load/store instructions minimizes the instruction +count. See `runtime/fiber/fiber_arm64.S` for details. -### 新規 fiber の起動 +### Starting a new fiber -`rw_fiber_swap` は「callee-saved を保存・復帰」しかしない。新規 fiber の -引数 `arg` は ABI 上 `x0` に置く必要があるが、`x0` は callee-saved では -ないので swap では保存されない。 +`rw_fiber_swap` only "saves and restores callee-saved registers." The new +fiber's argument `arg` must be placed in `x0` per the ABI, but `x0` is not +callee-saved and so is not saved by swap. -解決:**`x19 = entry`, `x20 = arg`, `lr = trampoline` を仕込む**。 -`rw_fiber_swap` から最初にこの fiber に切り替わったとき、`ret` で -`trampoline` に飛び、`trampoline` が `mov x0, x20; blr x19` を実行する。 +Solution: **set up `x19 = entry`, `x20 = arg`, `lr = trampoline`**. The first +time `rw_fiber_swap` switches to this fiber, `ret` jumps to `trampoline`, and +`trampoline` executes `mov x0, x20; blr x19`. -## スケジューラ +## Scheduler `runtime/fiber/sched.h`: @@ -79,105 +83,113 @@ void rw_sched_shutdown(void); void rw_sched_yield(void); rw_fiber_handle *rw_sched_spawn_i64 (int64_t (*fn)(void *), void *arg); -/* ... f64 / bool / str / void も同様 ... */ +/* ... f64 / bool / str / void are the same ... */ int64_t rw_sched_join_i64 (rw_fiber_handle *h); -/* ... f64 / bool / str / void も同様 ... */ +/* ... f64 / bool / str / void are the same ... */ ``` -### 動作 +### Behavior -- **スレッドは 1 本のまま**(マルチコアは将来の D-6 で対応) -- ready キューは FIFO の単方向リスト -- `rw_sched_yield()` は現在の fiber をキュー末尾に積み、次の fiber に - swap する。次が無ければ呼び出し元(main または別 fiber)に戻る -- `rw_sched_join_*(h)` は対象 fiber が DONE になるまで yield を繰り返し、 - DONE になったら結果を取り出してハンドルを `free` する +- **There is still only one thread** (multicore support comes later in D-6) +- The ready queue is a FIFO singly linked list +- `rw_sched_yield()` pushes the current fiber onto the tail of the queue and + swaps to the next fiber. If there is no next one, it returns to the caller + (main or another fiber) +- `rw_sched_join_*(h)` repeatedly yields until the target fiber becomes DONE, + then extracts the result and `free`s the handle -### スタックレイアウト +### Stack layout -| ページ | 用途 | +| page | purpose | |---|---| -| `[base, base + page]` | low guard(PROT_NONE) | -| `[base + page, base + page + 64K]` | usable stack(下から上に伸びる) | -| `[base + page + 64K, base + 2page + 64K]` | high guard(PROT_NONE) | +| `[base, base + page]` | low guard (PROT_NONE) | +| `[base + page, base + page + 64K]` | usable stack (grows from low to high) | +| `[base + page + 64K, base + 2page + 64K]` | high guard (PROT_NONE) | -ガードページは `mmap` + `mprotect(PROT_NONE)` で確保。スタックオーバーフロー -は SIGSEGV になるので silent corruption しない。 +The guard pages are reserved with `mmap` + `mprotect(PROT_NONE)`. A stack +overflow becomes a SIGSEGV, so there is no silent corruption. -ページサイズは `sysconf(_SC_PAGESIZE)` で実行時取得(macOS arm64 = 16K、 -Linux x86_64 = 4K)。 +The page size is obtained at runtime with `sysconf(_SC_PAGESIZE)` +(macOS arm64 = 16K, Linux x86_64 = 4K). -## ユーザー ABI(変更なし) +## User ABI (unchanged) -`runtime/runtime.h` の以下は **シグネチャ完全互換**: +The following in `runtime/runtime.h` is **fully signature-compatible**: ```c rw_future_t *rw_spawn_i64 (int64_t (*fn)(void *), void *args); int64_t rw_await_i64 (rw_future_t *f); -/* ...他の型も同様... */ +/* ...other types are the same... */ ``` -実装は `runtime/runtime.c` で fiber スケジューラに委譲するシムになっている。 -コンパイラ(`rwc`)が吐く LLVM IR は一切変える必要がない。 +The implementation is a shim in `runtime/runtime.c` that delegates to the fiber +scheduler. The LLVM IR emitted by the compiler (`rwc`) does not need to change +at all. -## await のセマンティクス +## await semantics -旧 pthread 版では `rw_await_*` は `pthread_join` で**呼び出しスレッドを -ブロック**していた。fiber 版では: +In the old pthread version, `rw_await_*` used `pthread_join` to **block the +calling thread**. In the fiber version: -- 呼び出し fiber は **`rw_sched_yield()` を繰り返す**だけ -- その間、他の READY な fiber が走り続ける -- 対象 fiber が完了したら結果を取り出して戻る +- The calling fiber simply **repeats `rw_sched_yield()`** +- Meanwhile, other READY fibers keep running +- Once the target fiber completes, it extracts the result and returns -つまり「await はもう協調的な待ち」であり、await 中に他の spawn 済み fiber -がブロックされない。 +In other words, "await is now a cooperative wait," and other already-spawned +fibers are not blocked during an await. -ただし**現状は完全に協調**で、長時間 CPU を握る fiber は他の fiber を -飢えさせる。割り込みベースのプリエンプションは将来課題(D-6 で検討)。 +However, **the current model is fully cooperative**, so a fiber that holds the +CPU for a long time starves the other fibers. Interrupt-based preemption is a +future task (to be considered in D-6). -## ターゲット別の実装 +## Per-target implementation -| OS / arch | アセンブリファイル | 状況 | +| OS / arch | assembly file | status | |---|---|---| -| macOS arm64 | `fiber/fiber_arm64.S` | 動作確認済み | -| Linux aarch64 | `fiber/fiber_arm64.S` | 同じファイル、シンボル名のアンダースコアプレフィックスのみ条件付き | -| Linux x86_64 | `fiber/fiber_x86_64.S` | **動作確認済み(Docker linux/amd64 で全テスト緑)** | -| Windows | - | 対象外 | - -x86_64 の実装メモ(System V AMD64 ABI): - -- callee-saved 整数レジスタ: `rbx`, `rbp`, `r12`, `r13`, `r14`, `r15`, `rsp` -- 浮動小数 callee-saved は無し(XMM は全て caller-saved) -- リターンアドレスはスタック上。新規 fiber では `rw_fiber_ctx_init` が - スタックトップに `&rw_fiber_trampoline` を書き込む -- トランポリン: `r12 = entry`, `r13 = arg` から `mov %r13, %rdi; call *%r12` - -## 検証 - -- `runtime/fiber/test_pingpong.c`: 3 fiber の往復で swap 自体を検証 -- `runtime/fiber/test_sched.c`: 1000 fiber で sum of squares が正しいことを検証 -- `runtime/fiber/test_c10k.c`: 10 万 fiber で 1+2+…+N が正しいことを検証 - -加えて `tests/test_e2e.py` の `spawn_basic` / `spawn_many` / `spawn_string` -が fiber バックエンドでも緑のまま動く。 - -## 既知の制限 - -1. **スレッドは 1 本だけ**:CPU バウンドの並列実行はまだ得られない。 - work-stealing マルチコアスケジューラは別フェーズ。 -2. **プリエンプションなし**:無限ループや重い計算をする fiber は他をブロックする。 -3. **I/O 自動 yield なし**:`read`/`recv` 等の syscall が直接ブロックすると - スケジューラ全体が止まる。epoll/kqueue 連携は別フェーズ。 -4. **デバッガ表示**:lldb のスタックトレースは「現在の fiber 1 本分」しか - 見えない。複数 fiber 同時の状態取得は別 fiber を `info threads` で - 個別に検査できない。 - -## 今後 - -| ID | 内容 | 効果 | +| macOS arm64 | `fiber/fiber_arm64.S` | verified working | +| Linux aarch64 | `fiber/fiber_arm64.S` | same file; only the underscore prefix on symbol names is conditional | +| Linux x86_64 | `fiber/fiber_x86_64.S` | **verified working (all tests green on Docker linux/amd64)** | +| Windows | - | out of scope | + +Implementation notes for x86_64 (System V AMD64 ABI): + +- callee-saved integer registers: `rbx`, `rbp`, `r12`, `r13`, `r14`, `r15`, `rsp` +- there are no callee-saved floating-point registers (all XMM are caller-saved) +- the return address is on the stack. For a new fiber, `rw_fiber_ctx_init` + writes `&rw_fiber_trampoline` at the top of the stack +- trampoline: from `r12 = entry`, `r13 = arg`, run `mov %r13, %rdi; call *%r12` + +## Verification + +- `runtime/fiber/test_pingpong.c`: verifies the swap itself with a round trip + across 3 fibers +- `runtime/fiber/test_sched.c`: verifies that sum of squares is correct with + 1000 fibers +- `runtime/fiber/test_c10k.c`: verifies that 1+2+…+N is correct with + 100,000 fibers + +In addition, `spawn_basic` / `spawn_many` / `spawn_string` in +`tests/test_e2e.py` keep working green on the fiber backend as well. + +## Known limitations + +1. **Only one thread**: CPU-bound parallel execution is not yet available. + A work-stealing multicore scheduler is a separate phase. +2. **No preemption**: a fiber that runs an infinite loop or a heavy computation + blocks the others. +3. **No automatic I/O yield**: if a syscall such as `read`/`recv` blocks + directly, the entire scheduler stalls. epoll/kqueue integration is a + separate phase. +4. **Debugger display**: an lldb stack trace can only see "the single current + fiber." Inspecting the state of multiple fibers at once is not possible; + you cannot examine other fibers individually via `info threads`. + +## Future work + +| ID | contents | effect | |---|---|---| -| ~~D-4~~ | ~~Linux x86_64 アセンブリ追加~~ | **完了** | -| D-5 | I/O 多重化(epoll/kqueue 連携) | C10k Web サーバーが書ける | -| D-6 | work-stealing マルチコア | CPU バウンドでも真の並列 | -| D-7 | プリエンプション(タイマー or 安全点) | 行儀の悪い fiber を強制切替 | +| ~~D-4~~ | ~~Add Linux x86_64 assembly~~ | **done** | +| D-5 | I/O multiplexing (epoll/kqueue integration) | can write a C10k web server | +| D-6 | work-stealing multicore | true parallelism even for CPU-bound work | +| D-7 | preemption (timer or safepoints) | forcibly switch away from ill-behaved fibers | diff --git a/docs/specs/06-scheduler-mn.md b/docs/specs/06-scheduler-mn.md index cdbcecd..4702b66 100644 --- a/docs/specs/06-scheduler-mn.md +++ b/docs/specs/06-scheduler-mn.md @@ -1,28 +1,28 @@ -# rw fiber ランタイム: M:N スケジューラ +# rw fiber runtime: M:N scheduler -このドキュメントは `05-fibers.md` の単一スレッド協調スケジューラを -**M:N (複数 OS スレッドで多数の fiber を多重化)** に拡張した実装を -説明する。公開 ABI (`rw_spawn_*` / `rw_await_*` / `rw_init` / -`rw_shutdown` / `rw_str`) は完全に互換で、`rwc/irgen.py` には一切 -変更がない。 +This document describes the implementation that extends the single-threaded +cooperative scheduler of `05-fibers.md` to **M:N (multiplexing many fibers +across multiple OS threads)**. The public ABI (`rw_spawn_*` / `rw_await_*` / +`rw_init` / `rw_shutdown` / `rw_str`) is fully compatible, and there are no +changes to `rwc/irgen.py`. -## 用語 (Go の GMP と同じ) +## Terminology (same as Go's GMP) -| 記号 | 意味 | +| Symbol | Meaning | |---|---| -| **G** | fiber 1 つ。`rw_fiber_handle` で表現 | -| **M** | OS スレッド (pthread) 1 本。`rw_M` | -| **P** | 論理プロセッサ。256 スロットの有界リングを持つ。`rw_P` | -| **globq** | グローバルキュー。mutex 保護のリンクリスト。`rw_globq` | +| **G** | One fiber. Represented by `rw_fiber_handle` | +| **M** | One OS thread (pthread). `rw_M` | +| **P** | A logical processor. Holds a 256-slot bounded ring. `rw_P` | +| **globq** | The global queue. A mutex-protected linked list. `rw_globq` | -M と P は 1:1 でペアになる。M 数 = P 数 = ワーカ数で、デフォルトは -`sysconf(_SC_NPROCESSORS_ONLN)`、上限 64。環境変数 `RW_WORKERS` で -上書きできる。 +M and P are paired 1:1. Number of M = number of P = number of workers, which +defaults to `sysconf(_SC_NPROCESSORS_ONLN)`, capped at 64. It can be overridden +with the `RW_WORKERS` environment variable. -メインスレッドは **ワーカではなくオーケストレータ**。`rw_init` が -ワーカ群を spawn し、メインは `rw_user_main` を同期的に呼ぶだけ。 +The main thread is an **orchestrator, not a worker**. `rw_init` spawns the +worker group, and main just calls `rw_user_main` synchronously. -## G の状態機械 +## G state machine ``` spawn @@ -35,34 +35,34 @@ M と P は 1:1 でペアになる。M 数 = P 数 = ワーカ数で、デフォ WAITING <------ (park on wait list) | v - DONE (release-store; 結果公開済み) + DONE (release-store; result published) ``` -- `READY`: いずれかのキュー (P-local ring または globq) に乗っている -- `RUNNING`: 現在ある M で実行中 -- `WAITING`: 他 G の `wait_head` リストに park 中。どのキューにも居ない -- `DONE`: 完了。結果は trampoline が公開済み +- `READY`: sitting in one of the queues (a P-local ring or the globq) +- `RUNNING`: currently executing on some M +- `WAITING`: parked on another G's `wait_head` list. Not in any queue +- `DONE`: completed. The result has been published by the trampoline -`state` は `_Atomic int`。`RUNNING -> DONE` だけが **release/acquire** -ペアの同期エッジ。trampoline が結果書き込み後に release-store し、 -join 側が acquire-load してから結果を読む。 +`state` is `_Atomic int`. Only `RUNNING -> DONE` is a **release/acquire** +synchronization edge. The trampoline does a release-store after writing the +result, and the join side does an acquire-load before reading the result. -## 実行フロー +## Execution flow ### spawn ``` rw_sched_spawn_*(fn, arg) └─ spawn_common - ├─ handle を calloc + mmap stack + rw_fiber_ctx_init - ├─ join_mu / join_cv / wait_lock 初期化 + ├─ calloc the handle + mmap the stack + rw_fiber_ctx_init + ├─ initialize join_mu / join_cv / wait_lock └─ enqueue_ready(h) - ├─ tls_m があれば m->p の ring に push - │ + 他 M が park 中なら cond_signal で 1 つ起こす - └─ tls_m が NULL (main 由来) なら globq に push + cond_signal + ├─ if tls_m exists, push onto m->p's ring + │ + if another M is parked, cond_signal to wake one + └─ if tls_m is NULL (from main), push onto globq + cond_signal ``` -### worker のメインループ +### worker main loop ``` worker_main(m): @@ -73,13 +73,13 @@ worker_main(m): g->state = RUNNING m->current = g rw_fiber_swap(&m->sched_ctx, &g->ctx) - ──── 戻ってきた時点で g は yield/park/done のいずれか ──── + ──── on return, g is in one of yield/park/done ──── if g->state == RUNNING: - enqueue_ready(g) # yield だった → 戻す + enqueue_ready(g) # it was a yield → put it back m->current = NULL ``` -「ring に push するのは swap **後**」が重要 (後述)。 +"Pushing onto the ring happens **after** the swap" is important (see below). ### find_runnable @@ -87,11 +87,11 @@ worker_main(m): find_runnable(m): loop: if g_shutdown: return NULL - g = rw_runq_get(m->p) # 自分の P から + g = rw_runq_get(m->p) # from your own P if g: return g - g = globq から最大 CAP/2 個 refill して 1 つ取る + g = refill up to CAP/2 from globq and take one if g: return g - g = try_steal(m) # 他 P から半分 steal + g = try_steal(m) # steal half from another P if g: return g park on g_sched_cv (with shutdown re-check under lock) ``` @@ -100,141 +100,138 @@ find_runnable(m): ``` try_steal(m): - offset = xorshift64(m) % nworkers # 各 M ごとに別 PRNG + offset = xorshift64(m) % nworkers # a separate PRNG per M for i in 0..nworkers-1: idx = (offset + i) % nworkers if idx == m->id: continue n = rw_runq_grab(g_ps[idx], batch, CAP/2) if n > 0: - return batch[0] と batch[1..n-1] を自 P に push + return batch[0], and push batch[1..n-1] onto your own P return NULL ``` -`rw_runq_grab` は 1 回の CAS で victim の `head` を進めて -`ceil(n/2)` 個取り出す。 +`rw_runq_grab` advances the victim's `head` with a single CAS and takes out +`ceil(n/2)` items. -### fiber 内 await (`park_on`) +### await within a fiber (`park_on`) ``` wait_done(target): - if tls_m: # fiber が他 fiber を await + if tls_m: # a fiber awaits another fiber while target->state != DONE: park_on(target): - wait_lock 取得 - if target->state == DONE: 解放して return + acquire wait_lock + if target->state == DONE: release and return me->state = WAITING - me を target->wait_head に push - wait_lock 解放 + push me onto target->wait_head + release wait_lock rw_fiber_swap(&me->ctx, &m->sched_ctx) - ──── trampoline が起こしたら戻ってくる ──── - else: # main が fiber を await + ──── returns when the trampoline wakes it ──── + else: # main awaits a fiber pthread_mutex_lock(&target->join_mu) while target->state != DONE: pthread_cond_wait(&target->join_cv, ...) pthread_mutex_unlock(...) ``` -### trampoline 完了 (`finalize_fiber`) +### trampoline completion (`finalize_fiber`) ``` finalize_fiber(h): - atomic_store(h->state, DONE, release) # 結果を公開 + atomic_store(h->state, DONE, release) # publish the result pthread_mutex_lock(h->join_mu) - pthread_cond_broadcast(h->join_cv) # main 側 joiner を起こす + pthread_cond_broadcast(h->join_cv) # wake the main-side joiner pthread_mutex_unlock(h->join_mu) - wait_lock 取得 + acquire wait_lock waiters = h->wait_head h->wait_head = NULL - wait_lock 解放 + release wait_lock for w in waiters: - enqueue_ready(w) # WAITING -> READY に戻す + enqueue_ready(w) # move WAITING -> READY rw_fiber_swap(&h->ctx, &m->sched_ctx) ``` -## 同期ポイント +## Synchronization points -| 対象 | writer | reader | ordering | +| Object | writer | reader | ordering | |---|---|---|---| | `h->state` -> DONE | trampoline | joiner | release / acquire | -| `h->result.*` | trampoline (DONE より前) | joiner (DONE 確認後) | state の release/acquire に便乗 | -| `p->ring` / `head` / `tail` | owner (put/get), stealer (grab) | 同左 | Go 風: tail は release-store、head は CAS | +| `h->result.*` | trampoline (before DONE) | joiner (after confirming DONE) | rides on the release/acquire of state | +| `p->ring` / `head` / `tail` | owner (put/get), stealer (grab) | same | Go-style: tail is a release-store, head is a CAS | | `g_shutdown` | shutdown | worker loop | release / acquire | -| `wait_head` | parker (CAS で push), trampoline (atomic 取り出し) | 同左 | `wait_lock` (atomic_flag spinlock) で保護 | +| `wait_head` | parker (push with CAS), trampoline (atomic take-out) | same | protected by `wait_lock` (atomic_flag spinlock) | -## 落とし穴と対処 +## Pitfalls and remedies -### 1. yield 時の "ctx 公開タイミング" 競合 +### 1. "ctx publication timing" race on yield -最初は `rw_sched_yield` の中で「自分を ring に push してから swap」 -していた。これは **重大なバグ** で、ring に乗った瞬間に他 M が -`rw_runq_grab` で取り、swap-in しようとする。しかし `rw_fiber_swap` -は自分の ctx を保存している途中なので、stealer は **半分書きの -ctx を読み込んで PC=0 にジャンプ** する。 +At first, `rw_sched_yield` did "push yourself onto the ring, then swap." This +was a **serious bug**: the moment you land on the ring, another M grabs you via +`rw_runq_grab` and tries to swap you in. But `rw_fiber_swap` is in the middle of +saving your ctx, so the stealer **reads a half-written ctx and jumps to PC=0**. -修正: yield は ring に何もしないで swap だけする。「ring に戻す」 -判定は `worker_main` 内で、swap が完全に戻ってきた **後** に行う。 -これで swap による ctx 保存が完了してから stealer に渡る。 +Fix: yield does nothing to the ring and just swaps. The "put it back on the +ring" decision is made in `worker_main`, **after** the swap has fully returned. +This ensures the ctx save by swap is complete before it is handed to a stealer. -### 2. park 中の cond_wait 競合 +### 2. cond_wait race during park -joiner が「state を読む → cond_wait」の間に trampoline が DONE を -書いて broadcast を投げると、joiner は signal を取り逃がして永久に -寝てしまう。対処: `join_mu` を取った状態で state を再チェックし、 -DONE なら cond_wait しない。trampoline 側も `join_mu` を取って -broadcast するので、joiner が「mu を取る前に DONE 公開」したら -joiner はその時点で抜ける、「mu を取った後に DONE 公開」したら -broadcast を取りこぼさない。 +If the trampoline writes DONE and posts a broadcast between the joiner's +"read state → cond_wait," the joiner misses the signal and sleeps forever. +Remedy: re-check state while holding `join_mu`, and do not cond_wait if it is +DONE. The trampoline side also acquires `join_mu` to broadcast, so if the joiner +"had DONE published before acquiring mu" it exits at that point, and if +"DONE is published after acquiring mu" it does not miss the broadcast. -### 3. wait list park の競合 +### 3. wait-list park race -joiner が「state チェック → wait_head に push」の間に trampoline が -DONE 公開 + wait_head 取り出しを終えると、joiner は誰にも起こされず -寝る。対処: `wait_lock` の中で state を再チェック。 +If the trampoline finishes publishing DONE + taking items out of wait_head +between the joiner's "check state → push onto wait_head," the joiner sleeps with +no one to wake it. Remedy: re-check state inside `wait_lock`. ## RW_WORKERS -| 値 | 挙動 | +| value | behavior | |---|---| -| 設定なし | `sysconf(_SC_NPROCESSORS_ONLN)`、上限 64 | -| `1` | デターミニスティック実行。`tests/test_e2e.py` はこれを固定 | -| `2`〜`64` | 指定通り | -| 範囲外 / 非数値 | デフォルトにフォールバック | +| not set | `sysconf(_SC_NPROCESSORS_ONLN)`, capped at 64 | +| `1` | deterministic execution. `tests/test_e2e.py` pins this | +| `2`–`64` | as specified | +| out of range / non-numeric | falls back to the default | -## 観測される並列スピードアップ +## Observed parallel speedup -`runtime/fiber/test_steal.c` (200 CPU-bound fibers を 1 つの primer -fiber から spawn、つまり初期は全部 1 つの P に積まれる) を macOS -arm64 で実測: +Measured on macOS arm64 with `runtime/fiber/test_steal.c` (200 CPU-bound fibers +spawned from a single primer fiber, i.e. initially all piled onto one P): -| RW_WORKERS | elapsed | 1 スレッド比 | +| RW_WORKERS | elapsed | vs 1 thread | |---|---|---| | 1 | 505ms | 1.00x | | 2 | 259ms | 1.95x | | 4 | 139ms | 3.62x | | 8 | 82ms | 6.15x | -work-stealing なしだと N=1 と同じ速度に張り付くはずで、これが -「不均等配置でもスケールする」ことを示す。 +Without work-stealing this should stay pinned at the N=1 speed, which shows +that it "scales even with uneven placement." -## ファイル構成 +## File layout -| ファイル | 役割 | +| file | role | |---|---| -| `runtime/fiber/sched.{c,h}` | M:N スケジューラ本体 | -| `runtime/fiber/runq.{c,h}` | 256 スロット有界リング + globq | -| `runtime/fiber/park.{c,h}` | wait list 用 atomic_flag spinlock | -| `runtime/fiber/fiber.{c,h}` | `rw_fiber_ctx_init` (変更なし) | -| `runtime/fiber/fiber_{arm64,x86_64}.S` | `rw_fiber_swap` (変更なし) | +| `runtime/fiber/sched.{c,h}` | the M:N scheduler proper | +| `runtime/fiber/runq.{c,h}` | 256-slot bounded ring + globq | +| `runtime/fiber/park.{c,h}` | atomic_flag spinlock for the wait list | +| `runtime/fiber/fiber.{c,h}` | `rw_fiber_ctx_init` (unchanged) | +| `runtime/fiber/fiber_{arm64,x86_64}.S` | `rw_fiber_swap` (unchanged) | -## テスト +## Tests -| バイナリ | 検証内容 | +| binary | what it verifies | |---|---| -| `test_sched` | 1000 fiber の spawn/join、合計値の正しさ | -| `test_c10k` | 10 万 fiber、所要時間ベンチ | -| `test_pingpong` | `rw_fiber_swap` の単体動作 | -| `test_runq` | 有界リング単体: FIFO / overflow / grab / globq | -| `test_wait` | fiber→fiber await、ネスト await、fan-out | -| `test_steal` | unbalanced 配置から並列スピードアップを確認 | -| `test_shutdown` | init/shutdown を 35 サイクル、リーク・デッドロックなし | -| `tests/test_e2e.py` | コンパイラ + ランタイム結合 (RW_WORKERS=1 固定) | +| `test_sched` | spawn/join of 1000 fibers, correctness of the total | +| `test_c10k` | 100,000 fibers, elapsed-time benchmark | +| `test_pingpong` | unit behavior of `rw_fiber_swap` | +| `test_runq` | bounded ring unit: FIFO / overflow / grab / globq | +| `test_wait` | fiber→fiber await, nested await, fan-out | +| `test_steal` | confirms parallel speedup from unbalanced placement | +| `test_shutdown` | 35 cycles of init/shutdown, no leaks or deadlocks | +| `tests/test_e2e.py` | compiler + runtime integration (RW_WORKERS=1 pinned) | diff --git a/docs/specs/07-string-builtins.md b/docs/specs/07-string-builtins.md index 716219c..13ef053 100644 --- a/docs/specs/07-string-builtins.md +++ b/docs/specs/07-string-builtins.md @@ -1,60 +1,61 @@ -# rw 文字列拡張: len / == / + +# rw string extensions: len / == / + ## Context -rw は `print` と `spawn`/`await` 以外に組込み機能がほとんど無く、`string` は -**リテラルを `print` するだけ** の存在になっている。 +Apart from `print` and `spawn`/`await`, rw has almost no built-in features, and +`string` exists only to **`print` a literal**. ```rw def main() -> int: name: string = "alice" - # ここから先が書けない: - # - len(name) で長さを取りたい - # - if name == "alice": の分岐がしたい - # - print("hello, " + name) でログを組み立てたい + # You cannot write anything beyond this point: + # - len(name) to get the length + # - if name == "alice": to branch + # - print("hello, " + name) to build a log print(name) return 0 ``` -この PR は **netpoller / TCP API** を将来入れる前提で、その手前にある -「言語の最小拡張」の 1 ステップ目を入れる。echo server を rw コードで -書くのに必要なのは「読んだ文字列を書き戻す」「長さで条件分岐する」 -「ログを組み立てる」の 3 つで、これらは `len` / `==` / `+` だけで足りる。 +This PR, on the premise of adding a **netpoller / TCP API** in the future, adds +the first step of the "minimal language extension" that precedes it. What you +need to write an echo server in rw code is the three things "write back the +string you read," "branch on the length," and "build a log," and these are +covered by just `len` / `==` / `+`. -長期計画はこの spec の外で進める: +The long-term plan proceeds outside this spec: -1. **このサブプロジェクト**: string の `len` / `==` / `+` -2. Bytes 型と buffer API +1. **This sub-project**: `len` / `==` / `+` for string +2. Bytes type and buffer API 3. List[T] 4. Result[T, E] / Option[T] 5. netpoller + TCP API ## Goals -- 組込み関数 `len(s: string) -> int` を追加 -- 演算子 `==` / `!=` を string 同士で許可 (バイト一致) -- 演算子 `+` を string 同士で許可 (連結、新しい string を確保) -- 既存テスト (66 件 + C テスト) を緑のまま維持 -- 公開 ABI (`rw_spawn_*` / `rw_await_*` / `rw_str`) は変更しない +- Add the built-in function `len(s: string) -> int` +- Allow the operators `==` / `!=` between two strings (byte equality) +- Allow the operator `+` between two strings (concatenation, allocating a new string) +- Keep the existing tests (66 + C tests) green +- Do not change the public ABI (`rw_spawn_*` / `rw_await_*` / `rw_str`) ## Non-Goals -- `s.len` のドット記法 (rw に struct 経由のメンバアクセスが無いため筋が悪い) -- 辞書順比較 `<` / `<=` / `>` / `>=` (echo server に不要) -- `*` (string 反復) や `s[i]` (index) -- 文字列の mutate (`rw_str` は const ポインタ前提) -- メモリ回収 (連結結果は malloc しっぱなし、リーク許容。学習・実験用なので OK) -- 国際化、Unicode 正規化 (バイト列としての扱いのみ) +- The `s.len` dot notation (rw has no member access via structs, so it is a bad fit) +- Lexicographic comparison `<` / `<=` / `>` / `>=` (not needed for an echo server) +- `*` (string repetition) or `s[i]` (indexing) +- String mutation (`rw_str` assumes a const pointer) +- Memory reclamation (concatenation results are left malloc'd, leaks tolerated; fine because this is for learning/experimentation) +- Internationalization, Unicode normalization (treated only as byte sequences) -## 設計 +## Design -### 言語レベルから見た挙動 +### Behavior seen from the language level ```rw def main() -> int: a: string = "hello" b: string = ", world" - c: string = a + b # "hello, world" (新規 allocate) + c: string = a + b # "hello, world" (newly allocated) print(len(c)) # 12 if c == "hello, world": # true print("matched") @@ -63,9 +64,9 @@ def main() -> int: return 0 ``` -### ランタイム追加関数 (`runtime.c` / `runtime.h`) +### Runtime helper functions to add (`runtime.c` / `runtime.h`) -3 つの helper 関数を C ABI で追加する。 +Add three helper functions with the C ABI. ```c /* New helpers — internal but exposed for irgen. */ @@ -74,127 +75,132 @@ int8_t rw_str_eq (rw_str a, rw_str b); rw_str rw_str_concat(rw_str a, rw_str b); ``` -実装方針: +Implementation approach: -- `rw_str_len`: `s.len` をそのまま返すワンライナー。irgen が `extractvalue` - を直接出してもよかったが、ABI を 1 本に揃えるため C 関数で出す。後で - 「\0 終端でない場合の境界チェック」を入れる余地もできる。 -- `rw_str_eq`: `a.len != b.len` → 0 即返し。同じなら `memcmp(a.ptr, b.ptr, a.len) == 0` - を `int8_t` (0/1) で返す。`rw_print_bool` と同じ i8 表現に合わせる。 -- `rw_str_concat`: `malloc(a.len + b.len)` で確保、`memcpy(p, a.ptr, a.len)`、 - `memcpy(p + a.len, b.ptr, b.len)`、`{len = a.len+b.len, ptr = p}` を返す。 - 両方 len=0 の場合は ptr=NULL, len=0 を返す (malloc(0) を呼ばない)。 - 解放はしない (リーク許容)。 +- `rw_str_len`: a one-liner that just returns `s.len`. irgen could have emitted + `extractvalue` directly, but we emit it via a C function to keep the ABI + uniform. This also leaves room to later add a "bounds check for the case where + it is not \0-terminated." +- `rw_str_eq`: `a.len != b.len` → immediately return 0. If they are equal, + return `memcmp(a.ptr, b.ptr, a.len) == 0` as an `int8_t` (0/1), matching the + same i8 representation as `rw_print_bool`. +- `rw_str_concat`: allocate with `malloc(a.len + b.len)`, `memcpy(p, a.ptr, a.len)`, + `memcpy(p + a.len, b.ptr, b.len)`, and return `{len = a.len+b.len, ptr = p}`. + If both have len=0, return ptr=NULL, len=0 (do not call malloc(0)). + It does not free (leaks tolerated). -これらは pthread-safe (各呼び出しは局所的)、副作用なし (concat の malloc -を除く)、シングルスレッド・マルチスレッドどちらでも安全。 +These are pthread-safe (each call is local), side-effect-free (except for +concat's malloc), and safe under both single-threaded and multi-threaded +execution. -### コンパイラ変更点 +### Compiler changes #### lexer / parser -変更なし。`len(s)` は既存の `Call` ノードでパースされ、`s1 + s2` と -`s1 == s2` は既存の二項演算子ノードでパースされる。 +No change. `len(s)` parses as the existing `Call` node, and `s1 + s2` and +`s1 == s2` parse as the existing binary-operator nodes. #### sema (`rwc/sema.py`) -3 ヶ所: - -1. **組込み関数テーブル** に `len(string) -> int` を追加。現状 `print` の - 特例処理が `analyze_call` 周辺にあるので、同じ場所に `len` を追加。 - `print` は引数型が「printable な型なら何でも」だが、`len` は - 「引数 1 つ、型は string、戻り値は int」と厳密。 -2. **`==` / `!=` の型チェック** で、両辺が string なら結果型 bool を許可。 - 既存コードに `# We could allow string equality later; disallow for MVP simplicity.` - というコメントとともに弾く分岐がある。これを「string も OK」に変更。 -3. **`+` の型チェック** で、両辺が string なら結果型 string を許可。 - `int + int -> int` / `float + float -> float` の隣に同じパターンで - `string + string -> string` を足す。 - -ネガティブテストを `tests/test_sema.py` に足す: -- `"a" + 1` → 型エラー -- `"a" == 1` → 型エラー -- `len(1)` → 型エラー (引数型違い) -- `len("a", "b")` → 型エラー (引数数違い) +Three places: + +1. Add `len(string) -> int` to the **built-in function table**. The special + handling for `print` is currently around `analyze_call`, so add `len` in the + same place. `print`'s argument type is "anything as long as it's a printable + type," but `len` is strict: "one argument, of type string, returning int." +2. In the **type check for `==` / `!=`**, allow the result type bool when both + sides are string. The existing code has a branch that rejects it along with + the comment `# We could allow string equality later; disallow for MVP simplicity.`. + Change this to "string is also OK." +3. In the **type check for `+`**, allow the result type string when both sides + are string. Add `string + string -> string` with the same pattern next to + `int + int -> int` / `float + float -> float`. + +Add negative tests to `tests/test_sema.py`: +- `"a" + 1` → type error +- `"a" == 1` → type error +- `len(1)` → type error (wrong argument type) +- `len("a", "b")` → type error (wrong argument count) #### irgen (`rwc/irgen.py`) -3 ヶ所: +Three places: -1. **外部関数宣言** に `rw_str_len`, `rw_str_eq`, `rw_str_concat` を追加 - (既存の `rw_print_*` と同じ形)。 -2. **`len(s)` 呼び出し** の生成。Sema で「組込み `len` の呼び出し」と - 印が付いた `Call` を `_emit_call` で受けたら、`rw_str_len` を呼び出す - IR を出す。 -3. **二項演算子の string ケース** の生成。`==` / `!=` は `rw_str_eq` を - 呼んで結果 i8 を `icmp` で i1 に変換 (`!=` の場合は `xor` で反転)。 - `+` は `rw_str_concat` を呼んで返り値 `rw_str` をそのまま使う。 +1. Add `rw_str_len`, `rw_str_eq`, `rw_str_concat` to the **external function + declarations** (in the same form as the existing `rw_print_*`). +2. Generate the **`len(s)` call**. When `_emit_call` receives a `Call` marked by + Sema as "a call to the built-in `len`," emit IR that calls `rw_str_len`. +3. Generate the **string case for binary operators**. `==` / `!=` call + `rw_str_eq` and convert the resulting i8 to i1 with `icmp` (for `!=`, invert + with `xor`). `+` calls `rw_str_concat` and uses the returned `rw_str` as is. -### コンポーネント間の境界 +### Boundaries between components -- ランタイム側は **C ABI 関数 3 本** のみ追加。テストは C レベルでも可能。 -- Sema/irgen の変更は **既存パターンの拡張のみ**。新しい AST ノードや - IR 命令を導入しない。 -- 公開 ABI (`rw_spawn_*` 等) は無変更。`librw.a` のシンボル増加のみ。 +- The runtime side only adds **three C ABI functions**. Testing is also possible + at the C level. +- The Sema/irgen changes are **only extensions of existing patterns**. No new + AST node or IR instruction is introduced. +- The public ABI (`rw_spawn_*`, etc.) is unchanged. Only the symbols in + `librw.a` increase. -## ファイル別変更 +## Changes by file -### 変更 +### Changed -- `runtime/runtime.c` — 3 関数の実装を追加 -- `runtime/runtime.h` — 3 プロトタイプを追加 -- `rwc/sema.py` — `len` 組込み、`+`/`==`/`!=` の string 許可 -- `rwc/irgen.py` — `rw_str_*` の外部宣言と呼び出し生成 -- `tests/test_sema.py` — string オペレーションの型検査ケース追加 -- `tests/test_e2e.py` — `string_ops` を parametrize に追加 +- `runtime/runtime.c` — add the implementations of the 3 functions +- `runtime/runtime.h` — add the 3 prototypes +- `rwc/sema.py` — `len` built-in, allow `+`/`==`/`!=` for strings +- `rwc/irgen.py` — external declarations and call generation for `rw_str_*` +- `tests/test_sema.py` — add type-check cases for string operations +- `tests/test_e2e.py` — add `string_ops` to the parametrize list -### 新規 +### New -- `examples/string_ops.rw` — len / `==` / `+` を 1 つの main で使うサンプル -- `examples/string_ops.rw.expected` — 期待出力 +- `examples/string_ops.rw` — a sample that uses len / `==` / `+` in a single main +- `examples/string_ops.rw.expected` — the expected output -### 変更なし +### Unchanged - lexer, parser, driver -- fiber 関連 (`runtime/fiber/*`) -- 既存の examples / spec docs +- fiber-related (`runtime/fiber/*`) +- existing examples / spec docs -## 検証 +## Verification ```sh -# ランタイム単体 +# runtime alone make -C runtime clean && make -C runtime -# 全テスト +# all tests uv run pytest -q -# 新しい example が単独で動く +# the new example runs on its own uv run rwc run examples/string_ops.rw -# 既存 example が壊れていない +# existing examples are not broken uv run rwc run examples/hello.rw uv run rwc run examples/spawn_many.rw ``` -成功基準: 全テスト緑、`string_ops.rw` の出力が `.expected` と一致、 -新規 ネガティブ Sema テストが期待通り型エラーを返す。 +Success criteria: all tests green, the output of `string_ops.rw` matches +`.expected`, and the new negative Sema tests return type errors as expected. -## コミット構成 +## Commit structure 3 commits: -1. **runtime**: `rw_str_len` / `rw_str_eq` / `rw_str_concat` を追加。 - C レベルから直接呼んで動作確認。 -2. **rwc**: Sema で string の `len` / `==` / `+` を許可し、irgen で - ランタイム helper を呼ぶ IR を生成。Sema ネガティブテスト追加。 -3. **examples + e2e**: `string_ops.rw` を追加、`tests/test_e2e.py` の - parametrize に組み込む。 +1. **runtime**: add `rw_str_len` / `rw_str_eq` / `rw_str_concat`. Verify by + calling them directly at the C level. +2. **rwc**: allow string `len` / `==` / `+` in Sema, and generate IR in irgen + that calls the runtime helpers. Add Sema negative tests. +3. **examples + e2e**: add `string_ops.rw` and wire it into the parametrize list + of `tests/test_e2e.py`. -## リスクと対処 +## Risks and remedies -| リスク | 対処 | +| Risk | Remedy | |---|---| -| `rw_str` の `ptr` が NULL のケース (空文字列リテラル) | concat と eq は len=0 を最初に分岐して NULL deref を避ける | -| Sema で `+` の型推論が壊れる (int 計算が string 経路にハマる等) | 既存の `+` ハンドラを「両辺 int / 両辺 float / 両辺 string」の 3 分岐に明示。テストで `1 + 1` / `1.0 + 2.0` / `"a" + "b"` を網羅 | -| `len` を変数名としてユーザが使っているコード | rw リポジトリ内の `examples/*.rw` に `len` を変数として使っている箇所は無い (grep 確認済み)。Sema で「組込み名と衝突する変数定義」は別途エラーにしてもよいが今回はやらない (shadowing 許可、組込みは「呼び出される時に組込み」というだけ) | -| 連結リークが long-running echo server で問題化 | このサブプロジェクトの非ゴール。netpoller 統合より前に GC を入れる判断をするなら別 PR で対処 | +| The case where `rw_str`'s `ptr` is NULL (empty string literal) | concat and eq branch on len=0 first to avoid a NULL deref | +| Sema's type inference for `+` breaks (e.g. integer arithmetic falls into the string path) | make the existing `+` handler explicitly three branches: "both int / both float / both string." Cover `1 + 1` / `1.0 + 2.0` / `"a" + "b"` in tests | +| Code where the user uses `len` as a variable name | there is no place in the repo's `examples/*.rw` that uses `len` as a variable (confirmed by grep). Sema could separately error on "a variable definition that collides with a built-in name," but we do not do that this time (shadowing allowed; a built-in is a built-in "only when it is called") | +| Concatenation leaks become a problem in a long-running echo server | a non-goal of this sub-project. If we decide to add GC before integrating the netpoller, we address it in a separate PR | diff --git a/docs/specs/08-bytes-type.md b/docs/specs/08-bytes-type.md index 0a14398..6aeb6f8 100644 --- a/docs/specs/08-bytes-type.md +++ b/docs/specs/08-bytes-type.md @@ -1,69 +1,74 @@ -# rw Bytes 型 (immutable, echo 最小セット) +# rw Bytes type (immutable, minimal echo set) ## Context -`docs/specs/07-string-builtins.md` で `string` の `len` / `==` / `+` を入れた。 -次に必要なのは **バイナリセーフな可変長バイト列** の表現で、これは将来の -netpoller + TCP API で `read(fd, n)` (旧 `tcp_read`、#33 で fd 汎用に統合) の -戻り値型として使われる。 +In `docs/specs/07-string-builtins.md` we added `len` / `==` / `+` for `string`. +The next thing we need is a representation for **binary-safe variable-length +byte sequences**, which will be used as the return type of `read(fd, n)` +(formerly `tcp_read`, unified to be fd-generic in #33) in the future +netpoller + TCP API. -`string` だとダメな理由: -- `string` は実質 immutable な `{i64 len, i8* ptr}` で、**型システム上は「文字列」**。 - バイナリ (\0 を含む、UTF-8 でないデータ) を `string` で持つと意味論が壊れる -- 「`print(s)` できる」「`s + s` で連結できる」前提でテキストデータが想定されている +Why `string` won't do: +- `string` is effectively an immutable `{i64 len, i8* ptr}`, and **in the type + system it is a "string."** Holding binary data (containing \0, non-UTF-8 data) + in a `string` breaks the semantics +- Text data is assumed on the premise that "you can `print(s)`" and "you can + concatenate with `s + s`" -`Bytes` を **別の型** として導入し、`string` と区別する。echo server に必要な -最小操作 (`len`, `==`, string との相互変換) だけを今回入れる。 +We introduce `Bytes` as a **separate type**, distinct from `string`. This time +we add only the minimal operations an echo server needs (`len`, `==`, mutual +conversion with string). -長期計画 (再掲): +Long-term plan (restated): -1. 文字列 `len` / `==` / `+` (済) -2. **このサブプロジェクト**: Bytes 型 + 最小 API +1. String `len` / `==` / `+` (done) +2. **This sub-project**: Bytes type + minimal API 3. List[T] 4. Result[T, E] / Option[T] 5. netpoller + TCP API ## Goals -- 新しいプリミティブ型 `Bytes` を導入 (キーワード `Bytes`) -- 組込み: - - `len(b: Bytes) -> int` (既存 `len(string)` をオーバーロード) - - `b1 == b2`, `b1 != b2` (Bytes 同士のみ、`string` と Bytes の比較は禁止) +- Introduce a new primitive type `Bytes` (keyword `Bytes`) +- Built-ins: + - `len(b: Bytes) -> int` (overloads the existing `len(string)`) + - `b1 == b2`, `b1 != b2` (only between Bytes; comparing `string` and Bytes is forbidden) - `bytes_from_str(s: string) -> Bytes` - `str_from_bytes(b: Bytes) -> string` -- `Bytes` を `spawn fn() -> Bytes` の戻り値型としても使える - (= `Future[Bytes]` が動く) -- 公開 ABI 不変、既存テスト緑 +- `Bytes` can also be used as the return type of `spawn fn() -> Bytes` + (i.e. `Future[Bytes]` works) +- Public ABI unchanged, existing tests green ## Non-Goals -- `b"..."` のような Bytes リテラル構文 (lexer 拡張が必要、後で) -- `bytes_at(b, i)` / `bytes_slice(b, i, j)` / Bytes 連結 (`+`) — プロトコル - 解析用、echo server には不要、別 PR -- `print(b: Bytes)` を許可する — UTF-8 でないかもしれないデータを直接出力する - 運用は型システムで禁止。必要なら `str_from_bytes(b)` で明示変換 -- `Bytes` と `string` の暗黙変換 / `==` の混合 +- Bytes literal syntax like `b"..."` (needs a lexer extension, later) +- `bytes_at(b, i)` / `bytes_slice(b, i, j)` / Bytes concatenation (`+`) — for + protocol parsing, not needed for an echo server, a separate PR +- Allowing `print(b: Bytes)` — directly outputting data that may not be UTF-8 is + forbidden by the type system. If needed, convert explicitly with + `str_from_bytes(b)` +- Implicit conversion between `Bytes` and `string` / mixing them in `==` -## 設計 +## Design -### 内部表現 +### Internal representation -LLVM IR レベルでは `Bytes` も `string` と **同じ `{i64 len, i8* ptr}` (= `RW_STR_TY`)** -として表現する。Sema レベルで `T.STRING` と `T.BYTES` を別物として扱い、 -型混同を Sema が静的に弾く。 +At the LLVM IR level, `Bytes` is represented as the **same `{i64 len, i8* ptr}` +(= `RW_STR_TY`) as `string`**. At the Sema level, `T.STRING` and `T.BYTES` are +treated as distinct, and Sema statically rejects type confusion. -利点: -- ランタイムには新しい関数を 1 つも追加しなくて済む。`rw_str_len` / `rw_str_eq` - をそのまま流用 -- `bytes_from_str` / `str_from_bytes` は型情報だけが変わる **noop** — - irgen は引数の SSA 値をそのまま返す -- `Future[Bytes]` も `rw_spawn_str` / `rw_await_str` をそのまま使える +Benefits: +- No new runtime function needs to be added at all. `rw_str_len` / `rw_str_eq` + are reused as is +- `bytes_from_str` / `str_from_bytes` are **noops** where only the type + information changes — irgen returns the argument's SSA value as is +- `Future[Bytes]` can also use `rw_spawn_str` / `rw_await_str` as is -注意: 同じ表現を使う以上、**`Bytes` と `string` を取り違えると Sema が -失敗するだけで、不正アクセスはしない**。安全性ではなく言語のクリーンさのための -区別。 +Note: since they use the same representation, **mixing up `Bytes` and `string` +only causes Sema to fail; it does not cause an invalid access**. The distinction +is for language cleanliness, not for safety. -### 言語レベルから見た挙動 +### Behavior seen from the language level ```rw def main() -> int: @@ -76,7 +81,7 @@ def main() -> int: return 0 ``` -エラーになるケース: +Cases that become errors: ```rw def main() -> int: @@ -88,164 +93,169 @@ def main() -> int: return 0 ``` -### コンポーネント別の変更 +### Changes by component #### `rwc/types.py` +Add + ```python BYTES = _Primitive("Bytes") ``` -を追加。`is_printable` / `is_numeric` には**含めない**。 +Do **not** include it in `is_printable` / `is_numeric`. #### `rwc/lexer.py` +Add + ```python KW_BYTES = auto() ``` -を `TokenKind` に追加し、`KEYWORDS` に `"Bytes": TokenKind.KW_BYTES` を入れる。 -キーワード命名は大文字始まり (`Future` と同様、`int`/`string` 等の小文字 -プリミティブとは別グループ)。 +to `TokenKind`, and add `"Bytes": TokenKind.KW_BYTES` to `KEYWORDS`. The keyword +naming starts with an uppercase letter (like `Future`, a separate group from the +lowercase primitives such as `int`/`string`). #### `rwc/parser.py` -`parse_type` の `kind_to_name` dict に `TokenKind.KW_BYTES: "Bytes"` を追加。 -1 行。 +Add `TokenKind.KW_BYTES: "Bytes"` to the `kind_to_name` dict in `parse_type`. +One line. #### `rwc/sema.py` -3 ヶ所: +Three places: -1. `_resolve_type` の dict に `"Bytes": T.BYTES` を追加。 -2. `_check_call` の `len` ハンドラを `string` または `Bytes` に拡張: +1. Add `"Bytes": T.BYTES` to the dict in `_resolve_type`. +2. Extend the `len` handler in `_check_call` to `string` or `Bytes`: ```python if at is not T.STRING and at is not T.BYTES: raise ... f"len argument must be string or Bytes, found `{at}`" ``` -3. `_check_call` に 2 つの新組込みを追加: +3. Add two new built-ins to `_check_call`: ```python if call.callee == "bytes_from_str": # arity 1, arg is string, returns Bytes if call.callee == "str_from_bytes": # arity 1, arg is Bytes, returns string ``` - どちらも `spawn bytes_from_str(...)` / `spawn str_from_bytes(...)` を - `print` / `len` と同様に禁止する分岐を `SpawnExpr` 経路に追加。 + For both, add a branch on the `SpawnExpr` path that forbids + `spawn bytes_from_str(...)` / `spawn str_from_bytes(...)`, the same as + `print` / `len`. -二項演算子の `==` / `!=` は既に「両辺同じ型」を要求しており、`T.BYTES == -T.BYTES` も自動的に通る。irgen 側で string と同じく `rw_str_eq` ルートに乗せる -分岐を追加するだけでよい。 +The binary operators `==` / `!=` already require "both sides the same type," so +`T.BYTES == T.BYTES` passes automatically. On the irgen side, you only need to +add a branch that routes it through `rw_str_eq`, the same as string. #### `rwc/irgen.py` -- `llvm_type_of` に Bytes を追加: +- Add Bytes to `llvm_type_of`: ```python if t is T.BYTES: return RW_STR_TY ``` -- `_emit_binop` の `==`/`!=` 分岐で `is_str` の代わりに `is_strlike = lty in (T.STRING, T.BYTES)` を判定基準にし、`rw_str_eq` に渡す。 +- In the `==`/`!=` branch of `_emit_binop`, use `is_strlike = lty in (T.STRING, T.BYTES)` as the decision criterion instead of `is_str`, and pass it to `rw_str_eq`. - `_emit_call`: - - `len` の呼び出し: 引数が `T.STRING` でも `T.BYTES` でも `rw_str_len` を呼ぶ。Sema が既に型を検証しているので irgen 側は型に関係なく helper を呼ぶだけ。 - - `bytes_from_str` / `str_from_bytes`: 引数の SSA 値をそのまま返す noop: + - The `len` call: call `rw_str_len` whether the argument is `T.STRING` or `T.BYTES`. Sema has already validated the type, so the irgen side just calls the helper regardless of type. + - `bytes_from_str` / `str_from_bytes`: a noop that returns the argument's SSA value as is: ```python if call.callee in ("bytes_from_str", "str_from_bytes"): return self._emit_expr(call.args[0], ctx) ``` -- `_decl_spawn` / `_decl_await` の戻り値型分岐に `T.BYTES` を追加し、`rw_spawn_str` / `rw_await_str` を返す: +- Add `T.BYTES` to the return-type branch of `_decl_spawn` / `_decl_await`, returning `rw_spawn_str` / `rw_await_str`: ```python elif ret_ty is T.STRING or ret_ty is T.BYTES: name, ret_llvm = "rw_spawn_str", RW_STR_TY ``` -#### ランタイム +#### Runtime -変更なし。 +No change. -### テスト +### Tests #### `tests/test_sema.py` -Positive (5 件): -- `Bytes` 型注釈と `bytes_from_str` の戻り値推論 -- `len(Bytes)` の戻り値が int -- `Bytes == Bytes` が bool -- `str_from_bytes(b)` の戻り値が string -- `spawn fn() -> Bytes` が `Future[Bytes]` +Positive (5): +- `Bytes` type annotation and return-value inference of `bytes_from_str` +- `len(Bytes)` returns int +- `Bytes == Bytes` is bool +- `str_from_bytes(b)` returns string +- `spawn fn() -> Bytes` is `Future[Bytes]` -Negative (5 件): -- `print(b)` で `Bytes` が printable でないエラー -- `b + b` で `+` が Bytes を許可しないエラー -- `b == "hi"` で `==` の型不一致エラー -- `bytes_from_str(1)` で引数型エラー -- `spawn bytes_from_str("a")` で組込み禁止エラー +Negative (5): +- `print(b)` errors because `Bytes` is not printable +- `b + b` errors because `+` does not allow Bytes +- `b == "hi"` errors on `==` type mismatch +- `bytes_from_str(1)` errors on argument type +- `spawn bytes_from_str("a")` errors on forbidden built-in #### e2e -- `examples/bytes_basic.rw` (上の挙動例の通り) と `.expected` を追加 -- `tests/test_e2e.py` の parametrize に `"bytes_basic"` を追加 +- Add `examples/bytes_basic.rw` (per the behavior example above) and its `.expected` +- Add `"bytes_basic"` to the parametrize list of `tests/test_e2e.py` -ランタイム単体テスト追加は不要 (新しい C 関数なし)。 +No new runtime unit tests are needed (no new C functions). -## ファイル別変更 +## Changes by file -### 変更 +### Changed - `rwc/types.py` — `BYTES = _Primitive("Bytes")` -- `rwc/lexer.py` — `KW_BYTES` 追加、`KEYWORDS` 拡張 -- `rwc/parser.py` — `parse_type` の dict 拡張 -- `rwc/sema.py` — `_resolve_type` / `_check_call` (3 組込み拡張) / SpawnExpr 禁止リスト +- `rwc/lexer.py` — add `KW_BYTES`, extend `KEYWORDS` +- `rwc/parser.py` — extend the dict in `parse_type` +- `rwc/sema.py` — `_resolve_type` / `_check_call` (extend 3 built-ins) / SpawnExpr forbidden list - `rwc/irgen.py` — `llvm_type_of` / `_emit_binop` / `_emit_call` / `_decl_spawn` / `_decl_await` - `tests/test_sema.py` — positive 5 + negative 5 -- `tests/test_e2e.py` — parametrize に 1 行追加 +- `tests/test_e2e.py` — add 1 line to the parametrize list -### 新規 +### New - `examples/bytes_basic.rw` - `examples/bytes_basic.rw.expected` -### 変更なし +### Unchanged -- `runtime/` (一切手を入れない) +- `runtime/` (not touched at all) - `docs/specs/05-fibers.md` / `06-scheduler-mn.md` / `07-string-builtins.md` -## 検証 +## Verification ```sh # pytest uv run pytest -q -# expected: 既存 75 件 + Sema positive 5 + negative 5 + e2e 1 = 86 件 全緑 +# expected: existing 75 + Sema positive 5 + negative 5 + e2e 1 = 86, all green -# 単独実行 +# run standalone uv run rwc run examples/bytes_basic.rw -# 既存 example 回帰 +# regression of existing examples uv run rwc run examples/hello.rw uv run rwc run examples/string_ops.rw uv run rwc run examples/spawn_many.rw -# ランタイムには手を入れていないので C テストは前回のまま緑 +# runtime is untouched, so the C tests stay green as before cd runtime && make clean && make ``` -## コミット構成 +## Commit structure 3 commits: -1. **rwc: introduce Bytes type (lexer/parser/types)** — `T.BYTES` / - `KW_BYTES` / parse_type / `_resolve_type` だけ。Sema/irgen はまだなので - `Bytes` を使うコードは別所でエラーになるが、`b: Bytes = ...` の型 - 注釈だけは parse + resolve できる -2. **rwc: Bytes operations in sema + irgen** — 4 つの組込み (`len` - オーバーロード, `bytes_from_str`, `str_from_bytes`, `==`) と spawn/await - の Bytes 対応。positive/negative テスト一括追加 -3. **examples + e2e** — `bytes_basic.rw` 追加、parametrize に組み込み +1. **rwc: introduce Bytes type (lexer/parser/types)** — only `T.BYTES` / + `KW_BYTES` / parse_type / `_resolve_type`. Sema/irgen are not there yet, so + code that uses `Bytes` errors elsewhere, but the type annotation + `b: Bytes = ...` alone can be parsed + resolved +2. **rwc: Bytes operations in sema + irgen** — the 4 built-ins (`len` + overload, `bytes_from_str`, `str_from_bytes`, `==`) and spawn/await support + for Bytes. Add positive/negative tests together +3. **examples + e2e** — add `bytes_basic.rw`, wire it into the parametrize list -## リスクと対処 +## Risks and remedies -| リスク | 対処 | +| Risk | Remedy | |---|---| -| `Bytes` が変数名としてユーザコードに既出 | `grep -rE '\bBytes\b' examples/` で確認済み (該当なし)。新キーワードなので一般変数名と競合しても致命的でない (キーワードは予約語、ユーザは別名にする) | -| `==` で string vs Bytes が「同表現なのに型エラー」 | 仕様通り。Bytes と string は別型、明示変換を要求する。Sema のエラーメッセージは `"\`==\` requires same type, found \`Bytes\` and \`string\`"` のような既存形 | -| `Future[Bytes]` が `rw_spawn_str` を共有することで仮想的な型混乱が将来 | 現状の Sema は型レベルで Bytes/string を区別するので、ユーザコードから見て混乱はない。将来 Bytes の表現を変える (たとえば独自 struct 化) ことになったら `_decl_spawn` の分岐を分けるだけで対処できる | -| `print(b)` が言語サイドで弾かれない (= irgen が落ちる) リスク | `is_printable(T.BYTES) == False` を確実に Sema レベルでテスト (negative テストでカバー) | +| `Bytes` already appears as a variable name in user code | confirmed with `grep -rE '\bBytes\b' examples/` (no matches). As a new keyword, even if it collides with a general variable name it is not fatal (a keyword is reserved; the user picks another name) | +| `==` gives "a type error despite the same representation" for string vs Bytes | as specified. Bytes and string are separate types and require explicit conversion. The Sema error message is an existing form like `"\`==\` requires same type, found \`Bytes\` and \`string\`"` | +| `Future[Bytes]` sharing `rw_spawn_str` causes virtual type confusion in the future | the current Sema distinguishes Bytes/string at the type level, so there is no confusion from the user code's perspective. If we later change the representation of Bytes (e.g. make it its own struct), we can handle it by just splitting the branch in `_decl_spawn` | +| The risk that `print(b)` is not rejected on the language side (= irgen crashes) | test `is_printable(T.BYTES) == False` reliably at the Sema level (covered by a negative test) | diff --git a/docs/specs/09-list-type.md b/docs/specs/09-list-type.md index e9f2e60..b8edb3d 100644 --- a/docs/specs/09-list-type.md +++ b/docs/specs/09-list-type.md @@ -1,78 +1,83 @@ -# rw List[int] 型 (immutable, モノモーフ, echo 最小セット) +# rw List[int] type (immutable, monomorphic, minimal echo set) ## Context -本サブプロジェクトが想定する「現代の rw 言語」とは、コンパイラ・ランタイム・ -サンプルプログラム・テストが一体となって育っていく、書き手と実装者が同じ -ループの中にいる小さな言語です。 +The "modern rw language" that this sub-project assumes is a small language in +which the compiler, runtime, sample programs, and tests grow together as one, +with the language user and the implementer inside the same loop. -現実の進化はさまざまです。本来なら言語仕様を凍結してからコンパイラを書く -やり方もあれば、最小ランタイムから出発して言語機能を徐々に積み上げていく -やり方もあります。さらに、型システムを先に設計して評価器を後から付ける -ようなアカデミックなアプローチも存在します。ただし、すべての言語実装が -それらの「正しい」順序を踏んでいるわけではありません。 +Real-world evolution takes many forms. One way is to freeze the language +specification and then write the compiler; another is to start from a minimal +runtime and gradually stack up language features. There is even an academic +approach that designs the type system first and bolts on the evaluator later. +That said, not every language implementation follows one of those "correct" +orderings. -しかし方針の違いにかかわらず、新しい型を入れる、組込み関数を増やす、 -ランタイムに ABI を追加するといった作業は、現代の言語実装では必ず -組み合わさって発生し、その接続部分に固有の複雑性が生まれます。 +Regardless of the chosen approach, however, tasks such as adding a new type, +adding more built-in functions, or adding an ABI to the runtime inevitably +combine in a modern language implementation, and their connection points give +rise to complexity of their own. -本仕様では、その複雑性の源泉を次の 3 つに整理します。まず **コンテナ型を -言語にどう持ち込むかという問題そのもの**、次に **既存型 (string / Bytes / -Future) との相互運用**、最後に **「ジェネリックは本当に必要か」を毎回 -問い直す段階的拡張の判断**です。 +This specification organizes the sources of that complexity into the following +three. First, **the problem of how to bring a container type into the language +in the first place**; next, **interoperation with existing types (string / +Bytes / Future)**; and finally, **the judgment of incremental extension that +re-asks "do we really need generics?" every time**. -これまでに rw 言語に入れたプリミティブは: +The primitives put into the rw language so far are: - `string` + `len` / `==` / `+` (#91) -- `Bytes` + `len` / `==` / 相互変換 (#92) +- `Bytes` + `len` / `==` / conversions (#92) -`List[T]` は **本物のジェネリック型** だが、ロードマップの今段階で目的とする -echo server に必要なのは「クライアント fd の配列を保持する」だけ。 -そのために **`List[int]` 1 種類だけ** を入れる。 +`List[T]` is a **true generic type**, but at this stage of the roadmap the echo +server we are targeting only needs to "hold an array of client fds." For that, +we introduce **only the single type `List[int]`**. -将来の `List[string]` / `List[Bytes]` / 汎用 `List[T]` への拡張パスは残すが、 -今回は **言語にジェネリック構文を導入しない**。`List[int]` を 1 つの -プリミティブ型として扱い、`int` 以外の型パラメータは parser でエラーにする。 +We keep the path open for future extension to `List[string]` / `List[Bytes]` / +generic `List[T]`, but for now we **do not introduce generic syntax into the +language**. We treat `List[int]` as one primitive type, and any type parameter +other than `int` is turned into a parser error. -ロードマップ: +Roadmap: -1. 文字列 `len` / `==` / `+` (済) -2. Bytes 型 (済) -3. **このサブプロジェクト**: List[int] 最小 API +1. String `len` / `==` / `+` (done) +2. Bytes type (done) +3. **this sub-project**: minimal List[int] API 4. Result[T, E] / Option[T] 5. netpoller + TCP API ## Goals -- 新しいプリミティブ型 `List[int]` を導入 (`Future[T]` と同様、parser が - `List` の後の `[int]` を要求する固定形) -- 4 つの組込み: +- Introduce a new primitive type `List[int]` (a fixed form in which, like + `Future[T]`, the parser requires `[int]` after `List`) +- Four built-ins: - `list_new() -> List[int]` - - `list_push(l: List[int], v: int) -> List[int]` (新しい List を返す) + - `list_push(l: List[int], v: int) -> List[int]` (returns a new List) - `list_at(l: List[int], i: int) -> int` - - `len(l: List[int]) -> int` (既存 `len` をオーバーロード) -- 公開 ABI 既存部分は不変 -- echo server で fd 配列を扱える状態にする + - `len(l: List[int]) -> int` (overloads the existing `len`) +- The existing public ABI stays unchanged +- Get to a state where the echo server can handle an fd array ## Non-Goals -- 汎用 `List[T]` (T が int 以外): parser で「only `List[int]` is supported」 - エラー -- `Future[List[int]]` (= `spawn fn() -> List[int]`): Sema で禁止 -- mutable list (push が in-place で副作用): 値型 immutable のみ +- Generic `List[T]` (T other than int): a "only `List[int]` is supported" + error in the parser +- `Future[List[int]]` (= `spawn fn() -> List[int]`): forbidden in Sema +- mutable list (push with in-place side effects): value-type, immutable only - `list_pop` / `list_remove` / `list_slice` / `list_concat` / `list_eq` / - `list_set` (要素更新) -- `for x in l` のイテレーション構文 -- 範囲外アクセスを言語レベルで表現 (= `Result[int, IndexError]` 等は - まだ無いので、当面 `rw_list_int_at` は範囲外で `abort()`) -- `print(l)` (debug 用に便利かもしれないが、`print` の挙動定義を - オーバーロードで広げる作業を今回は避ける) + `list_set` (element update) +- `for x in l` iteration syntax +- Expressing out-of-range access at the language level (= there is still no + `Result[int, IndexError]` etc., so for now `rw_list_int_at` calls `abort()` + on out-of-range) +- `print(l)` (it might be handy for debugging, but we avoid the work of + widening the definition of `print`'s behavior via overloading this time) -## 設計 +## Design -### 内部表現 +### Internal representation -`List[int]` は 3 ワード fat struct: +`List[int]` is a 3-word fat struct: ```c typedef struct { @@ -88,34 +93,35 @@ LLVM IR: %rw_list_int = { i64 len, i64 cap, i64* data } ``` -3 ワードを SSA 値としてそのまま渡し回す (Bytes/string が `{len, ptr}` 2 ワード -fat struct なのと同じ流儀)。`Future[List[int]]` で `i8*` 経由の引き渡しを -する必要がない (今回 non-goal)。 +We pass the 3 words around directly as an SSA value (the same style as Bytes / +string being a `{len, ptr}` 2-word fat struct). There is no need to pass it via +`i8*` through `Future[List[int]]` (a non-goal this time). -### 不変性の取り扱い +### Handling immutability -`list_push` のたびに **新しい `data` 配列を malloc し、要素を全部 memcpy** し、 -末尾に新しい値を足し、新しい `{len+1, new_cap, new_data}` を返す。 -古い `data` は free しない (= 他の SSA がまだ参照しているかもしれない、 -**リーク許容**)。Push 計算量は O(n) だが、echo server スケール (fd 数 1024 -程度) では問題なし。 +On every `list_push`, we **malloc a new `data` array and memcpy all the +elements**, append the new value at the end, and return a new +`{len+1, new_cap, new_data}`. We do not free the old `data` (= another SSA might +still be referencing it, **leak allowed**). Push is O(n), but at echo-server +scale (around 1024 fds) that is no problem. -シェアリングしないので、コードの読み手は「`l2 = list_push(l1, x)` の後でも -`l1` は変わらない」と即断できる。これは Bytes/string と同じ「fat pointer 値型」 -モデルの自然な拡張。 +Because there is no sharing, a reader of the code can immediately conclude that +"after `l2 = list_push(l1, x)`, `l1` is unchanged." This is a natural extension +of the same "fat pointer value type" model used by Bytes / string. -### 容量拡張ポリシー +### Capacity growth policy -毎回新しい配列を確保するので「ポリシー」は実質「初回は 4 個分、以降 2 倍」: +Since we allocate a new array every time, the "policy" is effectively "4 +elements the first time, then double": ```c int64_t new_cap = (l.cap == 0) ? 4 : l.cap * 2; while (new_cap < l.len + 1) new_cap *= 2; ``` -`l.len + 1 <= new_cap` を保証して `malloc(new_cap * 8)` する。 +We guarantee `l.len + 1 <= new_cap` and then `malloc(new_cap * 8)`. -### 言語レベルから見た挙動 +### Behavior as seen from the language level ```rw def main() -> int: @@ -129,7 +135,7 @@ def main() -> int: return 0 ``` -エラーになるケース: +Cases that become errors: ```rw def main() -> int: @@ -144,11 +150,12 @@ def main() -> int: return 0 ``` -### コンポーネント別の変更 +### Changes by component -#### ランタイム (`runtime/runtime.h`, `runtime/runtime.c`) +#### Runtime (`runtime/runtime.h`, `runtime/runtime.c`) -公開構造体 `rw_list_int` と 4 関数を追加 (string ops と同じスタイル): +Add the public struct `rw_list_int` and 4 functions (same style as the string +ops): ```c typedef struct { @@ -163,14 +170,17 @@ int64_t rw_list_int_at (rw_list_int l, int64_t i); int64_t rw_list_int_len (rw_list_int l); ``` -実装: +Implementation: -- `_new`: `{0, 0, NULL}` を返す。 -- `_push`: 上記の容量拡張ポリシーで malloc、`memcpy(new_data, l.data, l.len * 8)`、 - `new_data[l.len] = v`、`{l.len+1, new_cap, new_data}` を返す。 -- `_at`: `i < 0 || i >= l.len` なら `fputs("rw: list_at: index out of bounds\n", stderr); abort();`。 - 範囲内なら `l.data[i]` を返す。 -- `_len`: `l.len` を返す (irgen の単純化用、`len(l)` の呼び先)。 +- `_new`: returns `{0, 0, NULL}`. +- `_push`: malloc using the capacity growth policy above, + `memcpy(new_data, l.data, l.len * 8)`, `new_data[l.len] = v`, return + `{l.len+1, new_cap, new_data}`. +- `_at`: if `i < 0 || i >= l.len`, do + `fputs("rw: list_at: index out of bounds\n", stderr); abort();`. If in range, + return `l.data[i]`. +- `_len`: returns `l.len` (for irgen simplification; the call target of + `len(l)`). #### `rwc/types.py` @@ -178,21 +188,22 @@ int64_t rw_list_int_len (rw_list_int l); LIST_INT = _Primitive("List[int]") ``` -`is_printable` / `is_numeric` には含めない。 +Do not include it in `is_printable` / `is_numeric`. #### `rwc/lexer.py` -変更なし。`List` は既存 `Future` と同じく **キーワードにせず、parser が -IDENT として処理**する手もあったが、`Future` は実際は `KW_FUTURE` キーワード -扱いになっている。同じくしたい。 +No change. We could have kept `List` **not a keyword and had the parser handle +it as an IDENT**, just like the existing `Future`, but `Future` is in fact +treated as a `KW_FUTURE` keyword. We want to do the same. -→ **`KW_LIST = auto()` を追加し、`KEYWORDS["List"] = KW_LIST`**。 -spec 提案では parser で IDENT 扱いと書いたが、`Future` との一貫性のため -キーワード化する (1 行で済む)。 +→ **Add `KW_LIST = auto()` and `KEYWORDS["List"] = KW_LIST`**. The spec proposal +said the parser would treat it as IDENT, but for consistency with `Future` we +make it a keyword (it takes just one line). #### `rwc/parser.py` -`parse_type` で `Future` を処理する分岐の **直下** に `List` 用の分岐を追加: +Add a branch for `List` **directly below** the branch in `parse_type` that +handles `Future`: ```python if t.kind == TokenKind.KW_LIST: @@ -209,93 +220,96 @@ if t.kind == TokenKind.KW_LIST: return A.TypeName("List[int]", t.line, t.col) ``` -AST には既存 `A.TypeName` をそのまま使う (名前 `"List[int]"`)。新しい AST -ノードは不要。 +Reuse the existing `A.TypeName` as-is for the AST (name `"List[int]"`). No new +AST node is needed. #### `rwc/sema.py` -3 ヶ所: +Three places: -1. `_resolve_type` の dict に `"List[int]": T.LIST_INT` を追加。 -2. `_check_call` で `len` ハンドラを `T.LIST_INT` も許可するよう拡張。さらに - 3 つの組込みを追加: +1. Add `"List[int]": T.LIST_INT` to the dict in `_resolve_type`. +2. In `_check_call`, extend the `len` handler to also allow `T.LIST_INT`. + Additionally add the 3 built-ins: - `list_new()` arity 0, returns `T.LIST_INT` - `list_push(List[int], int)` arity 2, returns `T.LIST_INT` - `list_at(List[int], int)` arity 2, returns `T.INT` -3. SpawnExpr の禁止リストに `list_new` / `list_push` / `list_at` を追加。 +3. Add `list_new` / `list_push` / `list_at` to the SpawnExpr forbidden list. #### `rwc/irgen.py` -- `RW_LIST_INT_TY` の定数を `RW_STR_TY` と同じレベルで定義: +- Define the `RW_LIST_INT_TY` constant at the same level as `RW_STR_TY`: ```python RW_LIST_INT_TY = ir.LiteralStructType([I64, I64, I64.as_pointer()]) ``` -- `llvm_type_of(T.LIST_INT) -> RW_LIST_INT_TY`。 -- `_declare_runtime` で 4 外部関数を宣言: +- `llvm_type_of(T.LIST_INT) -> RW_LIST_INT_TY`. +- Declare the 4 external functions in `_declare_runtime`: ```python self._rw_list_int_new = ir.Function(m, ir.FunctionType(RW_LIST_INT_TY, []), "rw_list_int_new") self._rw_list_int_push = ir.Function(m, ir.FunctionType(RW_LIST_INT_TY, [RW_LIST_INT_TY, I64]), "rw_list_int_push") self._rw_list_int_at = ir.Function(m, ir.FunctionType(I64, [RW_LIST_INT_TY, I64]), "rw_list_int_at") self._rw_list_int_len = ir.Function(m, ir.FunctionType(I64, [RW_LIST_INT_TY]), "rw_list_int_len") ``` -- `_emit_call` で 3 つの組込み + `len(List[int])` を扱う。`len` は Sema で - 渡された引数の型 (`self.sema.expr_types[id(call.args[0])]`) を見て - `T.LIST_INT` なら `rw_list_int_len` を、`T.STRING` / `T.BYTES` なら従来の - `rw_str_len` を呼ぶ。 -- `_decl_spawn` / `_decl_await` には `T.LIST_INT` を**追加しない** (= 渡された - 場合は `RuntimeError`、ただし Sema が既に弾いているのでここに到達しない)。 +- Handle the 3 built-ins + `len(List[int])` in `_emit_call`. For `len`, look at + the argument type passed from Sema (`self.sema.expr_types[id(call.args[0])]`); + if it is `T.LIST_INT`, call `rw_list_int_len`, and if it is `T.STRING` / + `T.BYTES`, call the existing `rw_str_len`. +- Do **not** add `T.LIST_INT` to `_decl_spawn` / `_decl_await` (= if passed, it + is a `RuntimeError`, but since Sema already rejects it, this point is never + reached). -### ランタイムでの List[int] 値の引き渡し +### Passing List[int] values in the runtime -ABI 上、`{i64, i64, i64*}` の fat struct を C コードに値渡しする。アーキ別の -calling convention: -- aarch64 (Apple/Linux ARM64): struct が 16 バイト以下なら xN レジスタで渡す。 - 3 ワード (24 バイト) は **メモリ経由** で渡される (SysV AArch64 AAPCS では - HFA 規則の外なので通常はスタックまたは hidden pointer)。 -- x86_64 SysV: struct が 16 バイト超 → メモリ経由。 +At the ABI level, we pass a `{i64, i64, i64*}` fat struct to C code by value. +Calling conventions differ by architecture: +- aarch64 (Apple/Linux ARM64): a struct of 16 bytes or less is passed in xN + registers. 3 words (24 bytes) is passed **via memory** (under the SysV + AArch64 AAPCS it falls outside the HFA rules, so it normally goes on the stack + or via a hidden pointer). +- x86_64 SysV: a struct larger than 16 bytes → passed via memory. -clang は自動的に正しい convention を選ぶので、rwc が IR で `{i64, i64, i64*}` -を引き渡すだけで OK。llvmlite は LLVM が ABI を解決する形のままで動く。 +clang picks the correct convention automatically, so rwc just needs to pass +`{i64, i64, i64*}` through in the IR and it is OK. llvmlite works as-is with +LLVM resolving the ABI. -### スレッド安全性 +### Thread safety -`rw_list_int_push` 単体は副作用 (`malloc` + `memcpy`) のみで、`l` を mutate -しない。並行に同じ `l` を push しても両方が独立した新しい List を作る (= 元 -の `l` を共有して読むだけ)。問題なし。 +`rw_list_int_push` on its own only has side effects (`malloc` + `memcpy`) and +does not mutate `l`. Even if the same `l` is pushed concurrently, both create +independent new Lists (= they only share and read the original `l`). No problem. -`l.data` の free は **しない** ので、別 fiber が古い data を read している -最中に GC で free される事故は起こりえない (リーク許容の代償)。 +Because `l.data` is **never freed**, there is no risk of it being freed by GC +while another fiber is reading the old data (the price of allowing leaks). -## ファイル別変更 +## Changes by file -### 変更 +### Changed -- `runtime/runtime.h` — `rw_list_int` struct と 4 プロトタイプ -- `runtime/runtime.c` — 4 関数実装 +- `runtime/runtime.h` — the `rw_list_int` struct and 4 prototypes +- `runtime/runtime.c` — the 4 function implementations - `rwc/types.py` — `LIST_INT = _Primitive("List[int]")` - `rwc/lexer.py` — `KW_LIST` + `KEYWORDS["List"]` -- `rwc/parser.py` — `parse_type` の Future 分岐の隣に List 分岐 -- `rwc/sema.py` — `_resolve_type` / `_check_call` (3 組込み + len) / SpawnExpr - 禁止リスト 3 件 +- `rwc/parser.py` — a List branch next to the Future branch in `parse_type` +- `rwc/sema.py` — `_resolve_type` / `_check_call` (3 built-ins + len) / 3 + entries in the SpawnExpr forbidden list - `rwc/irgen.py` — `RW_LIST_INT_TY` / `llvm_type_of` / `_declare_runtime` / `_emit_call` - `tests/test_sema.py` — positive 4 + negative 7 -- `tests/test_e2e.py` — parametrize に `list_basic` を追加 +- `tests/test_e2e.py` — add `list_basic` to the parametrize - `.gitignore` — `runtime/fiber/test_list_int` -### 新規 +### New -- `runtime/fiber/test_list_int.c` — C 単体テスト +- `runtime/fiber/test_list_int.c` — C unit test - `examples/list_basic.rw` + `.expected` -### 変更なし +### Unchanged -- fiber 関連 (`runtime/fiber/sched.c` 等)、driver、`docs/specs/05`〜`08` +- fiber-related (`runtime/fiber/sched.c` etc.), driver, `docs/specs/05`–`08` -## 検証 +## Verification ```sh -# ランタイム単体 +# runtime unit make -C runtime clean && make -C runtime cd runtime cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_list_int.c librw.a -o fiber/test_list_int @@ -304,37 +318,39 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_list_int.c librw # pytest cd .. uv run pytest -q -# 期待: 既存 87 + sema positive 4 + sema negative 7 + e2e 1 = 99 件全緑 +# expected: existing 87 + sema positive 4 + sema negative 7 + e2e 1 = 99 all green -# 単独実行 +# standalone run uv run rwc run examples/list_basic.rw -# 既存 example 回帰 +# existing example regression uv run rwc run examples/string_ops.rw uv run rwc run examples/bytes_basic.rw uv run rwc run examples/spawn_many.rw ``` -## コミット構成 +## Commit structure 4 commits: -1. **runtime**: `rw_list_int` 構造と 4 helper、`test_list_int.c` で単体テスト -2. **rwc (lexer/parser/types)**: `KW_LIST`、`parse_type` の List 分岐、 - `T.LIST_INT`、`_resolve_type` の dict 更新。型注釈だけ parse + resolve - できる状態 (Sema/irgen はまだ未対応で `list_*` 呼び出しはエラーになる) -3. **rwc (sema + irgen)**: 4 組込みの Sema 検証、`len` のオーバーロード拡張、 - irgen で IR 生成、negative テスト一括 -4. **examples + e2e**: `list_basic.rw` 追加と `tests/test_e2e.py` の - parametrize 更新 +1. **runtime**: the `rw_list_int` struct and 4 helpers, unit-tested with + `test_list_int.c` +2. **rwc (lexer/parser/types)**: `KW_LIST`, the List branch in `parse_type`, + `T.LIST_INT`, updating the `_resolve_type` dict. A state where only the type + annotation can be parsed + resolved (Sema/irgen not yet supported, so + `list_*` calls error out) +3. **rwc (sema + irgen)**: Sema validation for the 4 built-ins, the `len` + overload extension, IR generation in irgen, negative tests in one batch +4. **examples + e2e**: add `list_basic.rw` and update the parametrize in + `tests/test_e2e.py` -## リスクと対処 +## Risks and mitigations -| リスク | 対処 | +| Risk | Mitigation | |---|---| -| `List` が既存の変数名 / 関数名と競合 | `grep -rE '\bList\b' examples/ tests/` で確認済み (該当なし)。`List` を新キーワードにすると、ユーザが `List` という識別子を使えなくなるが、`Future` と同じ扱いなので問題なし | -| `list_at` の範囲外で abort するのは粗い | 本来 `Result[int, IndexError]` で返したいが Result 型未実装。spec の non-goals に明記、netpoller 後に Result を入れたら戻す | -| 同じ `l` を 2 つの fiber が push しても安全か | `_push` は `l` を読むだけで mutate しないので並行 push 安全。両者が異なる新 List を返す (= シェアできない、broadcast like の用途には別 API が要る) | -| `Future[List[int]]` を将来入れる時の互換性 | Sema が今は禁止しているので、後で `_decl_spawn` / `_decl_await` に LIST_INT を追加するだけで済む (struct を新 spawn helper で受け渡しする ABI 設計が要るが、別 PR) | -| `cap` 拡張で 2^63 オーバーフロー | echo server スケールではあり得ない。実装上 `new_cap` は `int64_t`、`l.len + 1` チェックも `int64_t` で行うが、overflow 検査は省略 (rw は学習用) | -| `int` 以外の List を書こうとしたユーザへの導線 | parser のエラーメッセージを「only `List[int]` is supported in this version of rw」と明示。誤って `List[string]` 等を書いた場合に意図が伝わる | +| `List` clashes with an existing variable / function name | Confirmed with `grep -rE '\bList\b' examples/ tests/` (no hits). Making `List` a new keyword means a user can no longer use `List` as an identifier, but since it is treated the same as `Future`, this is no problem | +| Aborting on out-of-range in `list_at` is crude | Ideally we would return `Result[int, IndexError]`, but the Result type is not implemented. Noted in the spec non-goals; revisit once Result is introduced after netpoller | +| Is it safe if two fibers push the same `l`? | `_push` only reads `l` and does not mutate it, so concurrent push is safe. Both return distinct new Lists (= they cannot be shared; a broadcast-like use needs a separate API) | +| Compatibility when introducing `Future[List[int]]` in the future | Sema forbids it for now, so later it is enough to add LIST_INT to `_decl_spawn` / `_decl_await` (an ABI design that hands off the struct via a new spawn helper is needed, but that is a separate PR) | +| 2^63 overflow in `cap` growth | Impossible at echo-server scale. In the implementation `new_cap` is `int64_t` and the `l.len + 1` check is done in `int64_t` too, but the overflow check is omitted (rw is for learning) | +| Guidance for users who try to write a List other than `int` | The parser error message makes it explicit: "only `List[int]` is supported in this version of rw". If someone mistakenly writes `List[string]` etc., the intent gets across | diff --git a/docs/specs/10-option-type.md b/docs/specs/10-option-type.md index 2314cad..44d09ff 100644 --- a/docs/specs/10-option-type.md +++ b/docs/specs/10-option-type.md @@ -1,84 +1,88 @@ -# rw Option[int] 型 + match 構文 (モノモーフ最小形) +# rw Option[int] type + match syntax (monomorphic minimal form) ## Context -ここまでに rw 言語へ入れたプリミティブ: +Primitives put into the rw language so far: -- `string` の `len` / `==` / `+` (#91) +- `string`'s `len` / `==` / `+` (#91) - `Bytes` (#92) - `List[int]` (#93) -`List[int]` で踏んだ最後の壁が、`list_at(l, i)` の **範囲外アクセスを言語で -表現できない** 問題だった。現状はランタイムで `abort()` するしかなく、これは -学習用言語としては仕方ないが、 echo server 直前にもう一段踏み込むなら -**「失敗を値として返す」表現** が要る。 +The last wall we hit with `List[int]` was that **out-of-range access in +`list_at(l, i)` cannot be expressed in the language**. Currently the only +option is to `abort()` in the runtime, which is unavoidable for a learning +language, but if we want to step one level deeper right before the echo server, +we need **a way to "return failure as a value"**. -タグ付き共用体 (sum type) を **本気で入れる** には: +To **seriously** introduce a tagged union (sum type) would involve all of: -- 真のジェネリクス (`Option[T]`) +- true generics (`Option[T]`) - `Result[T, E]` -- パターンマッチ (構文、exhaustiveness、ネスト、ガード) -- 既存組込み (`list_at` 等) のシグネチャ書き換え +- pattern matching (syntax, exhaustiveness, nesting, guards) +- rewriting the signatures of existing built-ins (`list_at` etc.) -がすべて絡む。これは間違いなく 1 PR の範囲を超える。`incremental-language- -extensions` の鉄則「ジェネリクスは 2 度目に必要になった時」「1 PR で触る -レイヤーは多くて 4 つまで」に従い、**`Option[int]` 1 種類 + 最小 match -構文** だけを本サブプロジェクトに切り出す。 +This clearly goes beyond the scope of a single PR. Following the iron rules of +`incremental-language-extensions` — "generics when you need them the second +time" and "at most 4 layers touched in one PR" — we carve out only +**`Option[int]` alone + minimal match syntax** into this sub-project. -長期計画は spec の外で進める: +The long-term plan proceeds outside the spec: -1. 文字列 `len` / `==` / `+` (済) -2. Bytes 型 (済) -3. List[int] (済) -4. **このサブプロジェクト**: Option[int] + match (4a) -5. (将来) Result[int, int] + match の 2 アーム確立 (4b) -6. (将来) 真のジェネリクス化 (4c) -7. (将来) netpoller + TCP API +1. String `len` / `==` / `+` (done) +2. Bytes type (done) +3. List[int] (done) +4. **this sub-project**: Option[int] + match (4a) +5. (future) establish the 2-arm form of Result[int, int] + match (4b) +6. (future) true generics (4c) +7. (future) netpoller + TCP API ## Goals -- 新しいプリミティブ型 `Option[int]` を導入 (parser は `[int]` 以外を拒否) -- 値構築: `Some(int) -> Option[int]`, `None: Option[int]` -- 値分解: 最小 `match` 文 (Python 3.10 風の `case` キーワード + ブロック) - - `case Some(x): ` (`x` は IDENT 1 個、bound として block 内で - `int` 型) +- Introduce a new primitive type `Option[int]` (the parser rejects anything + other than `[int]`) +- Value construction: `Some(int) -> Option[int]`, `None: Option[int]` +- Value deconstruction: a minimal `match` statement (Python 3.10-style `case` + keyword + block) + - `case Some(x): ` (`x` is a single IDENT, bound as an `int` inside the + block) - `case None: ` - - 2 アーム必須、順不同 -- `list_at_opt(l: List[int], i: int) -> Option[int]` を追加して、範囲外 - アクセスを `None` で返せるようにする (`list_at` は abort のまま残す、後方 - 互換) -- 公開 ABI 既存部分は不変、既存テスト緑 + - both arms required, order-independent +- Add `list_at_opt(l: List[int], i: int) -> Option[int]` so that out-of-range + access can be returned as `None` (leave `list_at` aborting, for backward + compatibility) +- The existing public ABI stays unchanged, existing tests green ## Non-Goals -- 汎用ジェネリック (`Option[T]` の T が int 以外): parser で「only - `Option[int]` is supported」エラー -- `Result[T, E]` (別 PR、4b) -- match を式として使う (`x = match v: ...`): match は statement のみ -- ネストパターン (`Some(Some(x))`)、ガード (`case Some(x) if x > 0`)、 - ワイルドカード (`case _`) -- match の 1 アームのみ / 3 アーム以上 (`Option[int]` は 2 値しか持たないので - ちょうど 2 アームを要求) -- `Future[Option[int]]` (spawn 経由): Sema で禁止 -- `Option` 用の組込みメソッド (`unwrap`, `is_some`, `or_else`) -- `print(opt)` (printable リストに加えない、`match` で `Some(x): print(x)` - と書かせる) -- `opt == opt` (== whitelist に加えない、`match` で分解させる) -- `list_at` を `list_at_opt` に置き換える破壊的変更 (既存 example が回帰する - ので並置) -- 範囲外を `Result[int, IndexError]` で表現 (IndexError 型がまだ無いので - None で代用) - -## 設計 - -### 内部表現 - -`Option[int]` は 2 ワード fat struct: +- Full generics (T of `Option[T]` other than int): a "only `Option[int]` is + supported" error in the parser +- `Result[T, E]` (separate PR, 4b) +- Using match as an expression (`x = match v: ...`): match is a statement only +- Nested patterns (`Some(Some(x))`), guards (`case Some(x) if x > 0`), + wildcards (`case _`) +- match with only 1 arm / 3 or more arms (`Option[int]` has only 2 values, so + it requires exactly 2 arms) +- `Future[Option[int]]` (via spawn): forbidden in Sema +- Built-in methods for `Option` (`unwrap`, `is_some`, `or_else`) +- `print(opt)` (not added to the printable list; make people write + `Some(x): print(x)` in a `match`) +- `opt == opt` (not added to the == whitelist; make people deconstruct via + `match`) +- A breaking change that replaces `list_at` with `list_at_opt` (existing + examples would regress, so they coexist) +- Expressing out-of-range with `Result[int, IndexError]` (there is still no + IndexError type, so we substitute None) + +## Design + +### Internal representation + +`Option[int]` is a 2-word fat struct: ```c typedef struct { int64_t tag; /* 0 = None, 1 = Some */ - int64_t payload; /* Some のとき int 値、None のとき未定義 */ + int64_t payload; /* the int value when Some, undefined when None */ } rw_option_int; ``` @@ -88,24 +92,24 @@ LLVM IR: %rw_option_int = { i64 tag, i64 payload } ``` -サイズ 16 バイト → arm64 / x86_64 SysV 両方で **2 レジスタで return できる**。 -`List[int]` で踏んだ pointer-out ABI 問題は **発生しない** (16 バイト以下なら -value 返し OK、`llvm-ir-c-abi` skill 参照)。 +Size 16 bytes → it **can be returned in 2 registers** on both arm64 and x86_64 +SysV. The pointer-out ABI problem we hit with `List[int]` **does not occur here** +(a value return is OK for 16 bytes or less; see the `llvm-ir-c-abi` skill). -ランタイム関数の追加は **`list_at_opt` だけ**: +The only runtime function added is **`list_at_opt`**: ```c void rw_list_int_at_opt(rw_option_int *out, const rw_list_int *l, int64_t i); ``` -`rw_option_int` も pointer-out にしたほうが対称的で安全 (将来 24 バイト -超の型に拡張するときも統一できる) なので、`list_at_opt` も pointer-out -にする。`Some` / `None` の構築自体は LLVM IR 内 (`insertvalue` / -constant struct) で完結し、ランタイムを介さない。 +Making `rw_option_int` pointer-out too is more symmetric and safer (it can be +unified when we extend to types over 24 bytes in the future), so `list_at_opt` +is also pointer-out. Construction of `Some` / `None` itself completes inside +LLVM IR (`insertvalue` / constant struct) and does not go through the runtime. -### 言語レベルの挙動 +### Language-level behavior -#### 値構築 +#### Value construction ```rw def safe_div(a: int, b: int) -> Option[int]: @@ -114,11 +118,12 @@ def safe_div(a: int, b: int) -> Option[int]: return Some(a / b) ``` -- `Some()`: 式として評価され、`{tag=1, payload=}` を返す -- `None`: リテラルとして評価され、`{tag=0, payload=0}` を返す - - `None` は新しい予約語 (lexer の `KW_NONE`)。変数名としては使えなくなる +- `Some()`: evaluated as an expression, returns `{tag=1, payload=}` +- `None`: evaluated as a literal, returns `{tag=0, payload=0}` + - `None` is a new reserved word (the lexer's `KW_NONE`). It can no longer be + used as a variable name -#### 値分解 +#### Value deconstruction ```rw def main() -> int: @@ -137,14 +142,15 @@ def main() -> int: return 0 ``` -- `match :` の式は `Option[int]` 型でなければエラー -- 続くインデント内に `case Some():` と `case None:` が **両方** 必要 - (順不同) -- 各 case の右にブロック (1 つ以上の statement) -- match 全体は **statement**。式としては使えない (現状の rw に式形 if が - 無いのと揃える) +- The expression in `match :` must be of type `Option[int]`, otherwise it + is an error +- Inside the following indent, **both** `case Some():` and `case None:` + are required (order-independent) +- To the right of each case is a block (one or more statements) +- The whole match is a **statement**. It cannot be used as an expression (in + line with the current rw having no expression-form if) -#### 範囲外を None で返す +#### Returning out-of-range as None ```rw def main() -> int: @@ -165,21 +171,21 @@ def main() -> int: return 0 ``` -### コンポーネント別の変更 +### Changes by component -#### ランタイム +#### Runtime -新規 1 関数のみ: +Only 1 new function: ```c void rw_list_int_at_opt(rw_option_int *out, const rw_list_int *l, int64_t i); ``` -実装は `rw_list_int_at` のロジックを `abort` の代わりに `out->tag = 0` を -書くように変えた版。範囲内なら `{tag=1, payload=l->data[i]}` を out に -書き込む。 +The implementation is a version of the `rw_list_int_at` logic changed to write +`out->tag = 0` instead of `abort`. If in range, it writes +`{tag=1, payload=l->data[i]}` into out. -`rw_option_int` の C 型定義も追加 (`runtime.h` のトップに): +Also add the C type definition of `rw_option_int` (at the top of `runtime.h`): ```c typedef struct { @@ -188,9 +194,9 @@ typedef struct { } rw_option_int; ``` -C 単体テスト `runtime/fiber/test_option.c` (新規): -- `rw_list_int_at_opt` が範囲内で Some、範囲外で None を返すこと -- list が空のときも None を返すこと +C unit test `runtime/fiber/test_option.c` (new): +- that `rw_list_int_at_opt` returns Some when in range and None when out of range +- that it also returns None when the list is empty #### `rwc/types.py` @@ -198,11 +204,11 @@ C 単体テスト `runtime/fiber/test_option.c` (新規): OPTION_INT = _Primitive("Option[int]") ``` -`is_printable` / `is_numeric` には含めない。 +Do not include it in `is_printable` / `is_numeric`. #### `rwc/lexer.py` -新キーワード 5 つ: +5 new keywords: ```python KW_OPTION = auto() @@ -225,9 +231,9 @@ KEYWORDS = { #### `rwc/parser.py` -##### `parse_type` で `Option[int]` を受ける +##### Accept `Option[int]` in `parse_type` -`Future` / `List` と同パターン: +Same pattern as `Future` / `List`: ```python if t.kind == TokenKind.KW_OPTION: @@ -242,31 +248,31 @@ if t.kind == TokenKind.KW_OPTION: return A.TypeName("Option[int]", t.line, t.col) ``` -##### 式パーサで `Some(e)` と `None` を受ける +##### Accept `Some(e)` and `None` in the expression parser -primary expression のレベルで: -- `KW_SOME` + `(` + 式 + `)` → `A.SomeExpr(arg)` +At the level of the primary expression: +- `KW_SOME` + `(` + expr + `)` → `A.SomeExpr(arg)` - `KW_NONE` → `A.NoneExpr()` -##### 文パーサで `match` を受ける +##### Accept `match` in the statement parser -`parse_stmt` の if/while/return と並列に: +In parallel with if/while/return in `parse_stmt`: ```python if self.cur.kind == TokenKind.KW_MATCH: return self._parse_match() ``` -`_parse_match` の仕事: -1. `match` キーワードを消費し、式をパース、`:` を要求 -2. INDENT を要求 -3. 2 つの case をパース (順不同、両方必須): +What `_parse_match` does: +1. Consume the `match` keyword, parse the expression, require `:` +2. Require INDENT +3. Parse the 2 cases (order-independent, both required): - `case Some():` + block - `case None:` + block -4. DEDENT を要求 -5. `A.MatchStmt(target, some_var, some_block, none_block)` を返す +4. Require DEDENT +5. Return `A.MatchStmt(target, some_var, some_block, none_block)` -case が 3 つ目以上ある、片側欠落、`case _:` 等は parser エラー。 +A third-or-later case, a missing side, `case _:` etc. are parser errors. #### `rwc/ast_nodes.py` @@ -291,12 +297,12 @@ class MatchStmt(Stmt): ##### `_resolve_type` -dict に `"Option[int]": T.OPTION_INT` を追加。 +Add `"Option[int]": T.OPTION_INT` to the dict. -##### 式 Sema +##### Expression Sema -- `SomeExpr`: `arg` の型が `T.INT` でなければエラー、戻り型 `T.OPTION_INT` -- `NoneExpr`: 戻り型 `T.OPTION_INT` +- `SomeExpr`: error if `arg`'s type is not `T.INT`, return type `T.OPTION_INT` +- `NoneExpr`: return type `T.OPTION_INT` ##### `MatchStmt` Sema @@ -314,31 +320,32 @@ if isinstance(stmt, A.MatchStmt): return ``` -return-coverage チェック (関数の最後の statement が return か) は match 全体 -を「両 arm 共に return しているなら全体で return している」と扱う。 -これは既存の `if/elif/else` の terminates-in-return ロジックと同じパターンで -実装する。 +The return-coverage check (whether the last statement of a function is a return) +treats the whole match as "returning overall if both arms return." This is +implemented with the same pattern as the existing `if/elif/else` +terminates-in-return logic. -##### `list_at_opt` 組込み +##### `list_at_opt` built-in -`_check_call` に `list_at_opt(List[int], int) -> Option[int]` を追加。 -SpawnExpr 禁止リストにも追加。 +Add `list_at_opt(List[int], int) -> Option[int]` to `_check_call`. Also add it +to the SpawnExpr forbidden list. -##### == ホワイトリストは触らない +##### Do not touch the == whitelist -`Option[int]` を含めないことで `opt == None` のような比較を自動的に弾く。 +By not including `Option[int]`, a comparison like `opt == None` is automatically +rejected. #### `rwc/irgen.py` -##### 型定義 +##### Type definition ```python RW_OPTION_INT_TY = ir.LiteralStructType([I64, I64]) # {tag, payload} ``` -`llvm_type_of(T.OPTION_INT) -> RW_OPTION_INT_TY`。 +`llvm_type_of(T.OPTION_INT) -> RW_OPTION_INT_TY`. -##### 値構築 +##### Value construction ```python def _emit_some(self, expr, ctx): @@ -352,7 +359,7 @@ def _emit_none(self, expr, ctx): [ir.Constant(I64, 0), ir.Constant(I64, 0)]) ``` -##### match の lowering +##### Lowering of match ```python def _emit_match(self, stmt, ctx): @@ -388,14 +395,14 @@ def _emit_match(self, stmt, ctx): b.position_at_end(end_bb) ``` -`ctx.locals` の add/remove は既存の `_emit_block` のスコープ管理に倣う。 -両 arm が return している (terminated) なら end_bb は到達不能だが -position するだけで害なし (LLVM が消す)。 +The add/remove of `ctx.locals` follows the existing scope management in +`_emit_block`. If both arms return (terminated), end_bb is unreachable, but +merely positioning there is harmless (LLVM removes it). -##### `list_at_opt` 呼び出し +##### `list_at_opt` call -`rw_list_int_at_opt` の declare を `_declare_runtime` に追加 (pointer-out -パターン: `void (out*, l*, i)`)。`_emit_call` で `list_at_opt` を: +Add the declaration of `rw_list_int_at_opt` to `_declare_runtime` (pointer-out +pattern: `void (out*, l*, i)`). In `_emit_call`, handle `list_at_opt` as: ```python if call.callee == "list_at_opt": @@ -410,32 +417,33 @@ if call.callee == "list_at_opt": ##### `_decl_spawn` / `_decl_await` -T.OPTION_INT を含めない (= `Future[Option[int]]` 不可、Sema が先に弾く)。 +Do not include T.OPTION_INT (= `Future[Option[int]]` is not allowed; Sema +rejects it first). -### テスト +### Tests #### `tests/test_sema.py` (positive 6 + negative 7) Positive: -- `Some(5)` が `Option[int]` -- `None` が `Option[int]` -- `def f() -> Option[int]: return Some(1)` が通る -- `def f() -> Option[int]: return None` が通る -- `match v:` で両 arm 揃えれば通る -- some_arm 内で bound `x` が int として使える (`print(x + 1)`) +- `Some(5)` is `Option[int]` +- `None` is `Option[int]` +- `def f() -> Option[int]: return Some(1)` passes +- `def f() -> Option[int]: return None` passes +- `match v:` passes if both arms are present +- The bound `x` inside some_arm is usable as int (`print(x + 1)`) Negative: -- `Some("hi")` → 引数型エラー -- `Option[string]` → parser エラー -- `match` の片側欠落 → Sema エラー -- `match` ターゲットが Option[int] でない → Sema エラー -- `print(Some(1))` → printable エラー -- `Some(1) == None` → 比較不可エラー -- `spawn fn() -> Option[int]` で fn を呼ぶ場合 → spawn エラー +- `Some("hi")` → argument type error +- `Option[string]` → parser error +- A missing side in `match` → Sema error +- The `match` target is not Option[int] → Sema error +- `print(Some(1))` → printable error +- `Some(1) == None` → comparison-not-allowed error +- Calling fn via `spawn fn() -> Option[int]` → spawn error #### `examples/option_basic.rw` + `.expected` -(spec 上部の例を採用) +(Adopt the example from the top of the spec) ```rw def safe_div(a: int, b: int) -> Option[int]: @@ -459,55 +467,55 @@ def main() -> int: return 0 ``` -期待出力: +Expected output: ``` 5 -1 ``` -`tests/test_e2e.py` の parametrize に `option_basic` を追加。 +Add `option_basic` to the parametrize in `tests/test_e2e.py`. -#### C 単体テスト +#### C unit test -`runtime/fiber/test_option.c`: `rw_list_int_at_opt` の Some / None / 空 list -パスをカバー。 +`runtime/fiber/test_option.c`: covers the Some / None / empty-list paths of +`rw_list_int_at_opt`. -## ファイル別変更 +## Changes by file -### 変更 +### Changed -- `runtime/runtime.h` — `rw_option_int` struct と `rw_list_int_at_opt` - プロトタイプ -- `runtime/runtime.c` — `rw_list_int_at_opt` 実装 +- `runtime/runtime.h` — the `rw_option_int` struct and the `rw_list_int_at_opt` + prototype +- `runtime/runtime.c` — the `rw_list_int_at_opt` implementation - `rwc/types.py` — `OPTION_INT` -- `rwc/lexer.py` — 5 つの新キーワード -- `rwc/parser.py` — `parse_type` Option 分岐 + `Some`/`None` 式 + - `parse_match` +- `rwc/lexer.py` — the 5 new keywords +- `rwc/parser.py` — the Option branch in `parse_type` + `Some`/`None` + expressions + `parse_match` - `rwc/ast_nodes.py` — `SomeExpr` / `NoneExpr` / `MatchStmt` -- `rwc/sema.py` — `_resolve_type` / 式 Sema / `MatchStmt` Sema / - `list_at_opt` 組込み / SpawnExpr 禁止 / return-coverage チェック更新 +- `rwc/sema.py` — `_resolve_type` / expression Sema / `MatchStmt` Sema / + `list_at_opt` built-in / SpawnExpr forbid / return-coverage check update - `rwc/irgen.py` — `RW_OPTION_INT_TY` / `llvm_type_of` / `_emit_some` / - `_emit_none` / `_emit_match` / `list_at_opt` 呼び出し / `_declare_runtime` + `_emit_none` / `_emit_match` / `list_at_opt` call / `_declare_runtime` - `tests/test_sema.py` — positive 6 + negative 7 -- `tests/test_e2e.py` — parametrize に `option_basic` +- `tests/test_e2e.py` — `option_basic` in the parametrize - `.gitignore` — `runtime/fiber/test_option` -### 新規 +### New - `runtime/fiber/test_option.c` - `examples/option_basic.rw` + `.expected` -- `docs/specs/10-option-type.md` (本ファイル) -- `docs/plans/2026-05-22-option-type.md` (writing-plans で作成) +- `docs/specs/10-option-type.md` (this file) +- `docs/plans/2026-05-22-option-type.md` (created with writing-plans) -### 変更なし +### Unchanged -- fiber スケジューラ (`runtime/fiber/sched.c` 等)、driver、既存 spec docs +- fiber scheduler (`runtime/fiber/sched.c` etc.), driver, existing spec docs -## 検証 +## Verification ```sh -# ランタイム単体 +# runtime unit make -C runtime clean && make -C runtime cd runtime cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_option.c librw.a -o fiber/test_option @@ -516,40 +524,41 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_option.c librw.a # pytest cd .. uv run pytest -q -# 期待: 既存 101 + Sema positive 6 + negative 7 + e2e 1 = 115 件全緑 +# expected: existing 101 + Sema positive 6 + negative 7 + e2e 1 = 115 all green -# 単独実行 +# standalone run uv run rwc run examples/option_basic.rw -# 期待出力: 5\n-1\n +# expected output: 5\n-1\n -# 既存 example 回帰 +# existing example regression uv run rwc run examples/list_basic.rw uv run rwc run examples/string_ops.rw uv run rwc run examples/spawn_many.rw ``` -## コミット構成 +## Commit structure 4 commits: -1. **rwc (lexer/parser/types)**: 5 つのキーワード、`Option[int]` パース、 - `Some(e)` と `None` の式、`match` 文のパース、AST ノード追加、`T.OPTION_INT`、 - `_resolve_type`。型注釈と AST 構築だけ通る (Sema/irgen は未実装) -2. **rwc (sema)**: `Some`/`None` 式の Sema、`MatchStmt` の Sema (exhaustiveness、 - bound variable、return-coverage)、negative テスト一括 -3. **runtime + rwc (irgen)**: `rw_option_int` 構造体、`rw_list_int_at_opt` - 実装と C テスト、irgen で `Some`/`None`/`match`/`list_at_opt` の IR 生成、 - smoke 検証 -4. **examples + e2e**: `option_basic.rw` 追加と `tests/test_e2e.py` の - parametrize 更新 - -## リスクと対処 - -| リスク | 対処 | +1. **rwc (lexer/parser/types)**: the 5 keywords, `Option[int]` parsing, the + `Some(e)` and `None` expressions, parsing the `match` statement, the new AST + nodes, `T.OPTION_INT`, `_resolve_type`. Only the type annotation and AST + construction pass (Sema/irgen not implemented) +2. **rwc (sema)**: Sema for the `Some`/`None` expressions, Sema for `MatchStmt` + (exhaustiveness, bound variable, return-coverage), negative tests in one batch +3. **runtime + rwc (irgen)**: the `rw_option_int` struct, the + `rw_list_int_at_opt` implementation and C test, IR generation in irgen for + `Some`/`None`/`match`/`list_at_opt`, smoke verification +4. **examples + e2e**: add `option_basic.rw` and update the parametrize in + `tests/test_e2e.py` + +## Risks and mitigations + +| Risk | Mitigation | |---|---| -| `None` を予約語化することで既存ユーザコードに衝突 | rw リポジトリ内の `examples/*.rw` に `None` を変数名として使っている箇所は無い (grep 確認)。新規予約語なので将来も意図せず衝突しない | -| match の return-coverage チェック実装が複雑 | 既存の `if/elif/else` の terminates-in-return ロジックを 1 関数に抽出して match でも再利用。両 arm が return しているかを再帰的に判定する | -| `Some(x) == ...` 比較を期待するユーザ | spec の Non-Goals に明記、エラーメッセージで「use match to inspect Option[int]」と誘導 (ただし今回エラーメッセージはシンプルに「cannot compare `Option[int]`」で済ませる) | -| 将来 `Result[T, E]` を入れたとき match を再利用したい | parser は `case Some(IDENT)` をハードコードしているのでそのままでは使えない。spec の Non-Goals に「Result 型は別 PR、その時 match parser を generalize」を明記 | -| 既存 == の whitelist 化 (#93 で入れたもの) が Option を弾けるか | `Option[int]` を whitelist に追加しないだけで自動的に「cannot compare Option[int]」エラー。テストでカバー | -| `_emit_match` の SSA / basic-block 管理が既存 `if` と整合するか | 既存 `_emit_if` を参考に同じスコープルール (alloca はエントリ BB、locals dict を block 単位で push/pop) を踏襲。実装時に既存パターンを Read で確認 | +| Making `None` a reserved word clashes with existing user code | There is no place in `examples/*.rw` in the rw repository that uses `None` as a variable name (grep confirmed). Since it is a new reserved word, it will not unintentionally clash in the future either | +| The match return-coverage check implementation is complex | Extract the existing `if/elif/else` terminates-in-return logic into one function and reuse it for match too. Recursively decide whether both arms return | +| A user who expects `Some(x) == ...` comparison | Noted in the spec Non-Goals; the error message guides with "use match to inspect Option[int]" (though this time the error message stays simple as "cannot compare `Option[int]`") | +| Wanting to reuse match when introducing `Result[T, E]` in the future | The parser hardcodes `case Some(IDENT)`, so it cannot be used as-is. The spec Non-Goals note "the Result type is a separate PR; generalize the match parser then" | +| Can the == whitelisting (added in #93) reject Option? | Simply not adding `Option[int]` to the whitelist automatically produces a "cannot compare Option[int]" error. Covered by tests | +| Do `_emit_match`'s SSA / basic-block management line up with the existing `if`? | Following the existing `_emit_if`, adopt the same scope rules (alloca in the entry BB, push/pop the locals dict per block). At implementation time, confirm the existing pattern with Read | diff --git a/docs/specs/11-result-type.md b/docs/specs/11-result-type.md index fc5a557..18c1881 100644 --- a/docs/specs/11-result-type.md +++ b/docs/specs/11-result-type.md @@ -1,70 +1,75 @@ -# rw Result[int, int] 型 + match の Ok/Err 拡張 +# rw Result[int, int] type + Ok/Err extension of match ## Context -`Option[int]` (#94) で **タグ付き共用体 + パターンマッチ** という言語機能の -土台ができた。Result はその次に置くべきもう 1 つの sum 型で、典型的な -「成功または失敗の理由付きエラー」を表現する標準形になる。 +With `Option[int]` (#94), the foundation of the language feature **tagged union ++ pattern matching** is in place. Result is the other sum type that should come +next, and it becomes the standard form for expressing the typical "success, or +failure with a reason for the error." -`incremental-language-extensions` skill の鉄則「ジェネリクスは 2 度目に -必要になった時」を厳密に適用するなら、Option (1 度目) + Result (2 度目) -で **真のジェネリクスを入れるタイミング** とも言える。しかし真のジェネリクス -化は `parse_match` を sema 経由にする大改造を含み、1 PR の範囲を超える。 +If we strictly apply the `incremental-language-extensions` skill's iron rule +"generics when you need them the second time," then Option (first time) + Result +(second time) could also be **the timing to introduce true generics**. However, +going truly generic includes a large overhaul that routes `parse_match` through +sema, which goes beyond the scope of a single PR. -そこで本サブプロジェクトは **依然モノモーフを維持**しつつ、match parser -だけは「Some/None ペア」か「Ok/Err ペア」のどちらかを 1 つ目のアームで -判別する形に拡張する。真のジェネリクス化は別 PR (4c) で行う。 +So this sub-project **still keeps things monomorphic** while extending only the +match parser to determine, from its first arm, whether it is a "Some/None pair" +or an "Ok/Err pair." Going truly generic is done in a separate PR (4c). -ロードマップ: +Roadmap: -1. 文字列 `len` / `==` / `+` (#91) -2. Bytes 型 (#92) +1. String `len` / `==` / `+` (#91) +2. Bytes type (#92) 3. List[int] (#93) 4a. Option[int] + match (#94) -4b. **このサブプロジェクト**: Result[int, int] + match Ok/Err -4c. (将来) 真のジェネリクス化 -5. (将来) netpoller + TCP API +4b. **this sub-project**: Result[int, int] + match Ok/Err +4c. (future) true generics +5. (future) netpoller + TCP API ## Goals -- 新しいプリミティブ型 `Result[int, int]` を導入 (parser は `[int, int]` 以外 - を拒否) -- 値構築: `Ok(int) -> Result[int, int]`、`Err(int) -> Result[int, int]` -- 値分解: 既存 `match` 文を「Some/None ペア」または「Ok/Err ペア」の - どちらか 2 アームに拡張 (ペア混在は parser エラー) -- `MatchStmt` AST に `style: "option" | "result"` フィールドを足し、Sema / - irgen が style 別に分岐 -- 公開 ABI 既存部分は不変、既存テスト緑 +- Introduce a new primitive type `Result[int, int]` (the parser rejects + anything other than `[int, int]`) +- Value construction: `Ok(int) -> Result[int, int]`, `Err(int) -> Result[int, int]` +- Value deconstruction: extend the existing `match` statement to a 2-arm form + that is either a "Some/None pair" or an "Ok/Err pair" (mixing the pairs is a + parser error) +- Add a `style: "option" | "result"` field to the `MatchStmt` AST, and have + Sema / irgen branch by style +- The existing public ABI stays unchanged, existing tests green ## Non-Goals -- 汎用ジェネリック (`Result[T, E]` の T/E が int 以外): parser で - 「only `Result[int, int]` is supported」エラー -- `Result[T, E]` の真のジェネリクス化 (別 PR、4c) -- match を式として使う (`x = match v: ...`): match は statement のみ -- ネストパターン (`Ok(Some(x))`)、ガード (`case Ok(x) if x > 0`)、 - ワイルドカード (`case _`) -- `?` 演算子 (early-return) -- match のアーム順序: Some/None は順不同のままにしているので Ok/Err も - 順不同 -- `Future[Result[int, int]]` (spawn 経由): Sema で禁止 -- `Result` 用の組込みメソッド (`unwrap`, `is_ok`, `or_else`) -- `Option[int]` <-> `Result[int, int]` の相互変換組込み -- `print(r: Result[int, int])` (printable リストに加えない) -- `r1 == r2` (== whitelist に加えない) -- `div_checked` 等の組込み helper (Result を返す関数は **rw 言語側でユーザ - コードとして書ける** ので、example で書いて済ます) - -## 設計 - -### 内部表現 - -`Result[int, int]` は `Option[int]` と **同じ 2 ワード fat struct** で表現: +- Full generics (T/E of `Result[T, E]` other than int): a "only + `Result[int, int]` is supported" error in the parser +- Going truly generic with `Result[T, E]` (separate PR, 4c) +- Using match as an expression (`x = match v: ...`): match is a statement only +- Nested patterns (`Ok(Some(x))`), guards (`case Ok(x) if x > 0`), wildcards + (`case _`) +- The `?` operator (early-return) +- match arm order: since Some/None remain order-independent, Ok/Err are also + order-independent +- `Future[Result[int, int]]` (via spawn): forbidden in Sema +- Built-in methods for `Result` (`unwrap`, `is_ok`, `or_else`) +- Conversion built-ins between `Option[int]` <-> `Result[int, int]` +- `print(r: Result[int, int])` (not added to the printable list) +- `r1 == r2` (not added to the == whitelist) +- Built-in helpers such as `div_checked` (a function that returns Result **can + be written as user code on the rw language side**, so we get by writing it in + an example) + +## Design + +### Internal representation + +`Result[int, int]` is represented by the **same 2-word fat struct as +`Option[int]`**: ```c typedef struct { int64_t tag; /* 0 = Err, 1 = Ok */ - int64_t payload; /* Ok のとき成功値、Err のときエラー値 */ + int64_t payload; /* the success value when Ok, the error value when Err */ } rw_result_int_int; ``` @@ -74,30 +79,31 @@ LLVM IR: %rw_result_int_int = { i64 tag, i64 payload } ``` -サイズ 16 バイト → arm64 / x86_64 SysV 両方で 2 レジスタで return できる -(pointer-out ABI 不要、`llvm-ir-c-abi` skill 参照)。 +Size 16 bytes → it can be returned in 2 registers on both arm64 and x86_64 SysV +(no pointer-out ABI needed; see the `llvm-ir-c-abi` skill). -実は **Option[int] と LLVM struct の形は完全一致**しているが、Sema レベルで -別の型 (`T.OPTION_INT` vs `T.RESULT_INT_INT`) として区別する。Bytes と -string が同じ `RW_STR_TY` を共有するのと同じパターン。 +In fact **the LLVM struct shape is exactly identical to Option[int]**, but at +the Sema level we distinguish them as different types (`T.OPTION_INT` vs +`T.RESULT_INT_INT`). This is the same pattern as Bytes and string sharing the +same `RW_STR_TY`. -タグ値の意味付け: +Meaning of the tag values: -| tag | 意味 | +| tag | meaning | |---|---| -| 0 | Err (失敗) | -| 1 | Ok (成功) | +| 0 | Err (failure) | +| 1 | Ok (success) | -Option との対応関係: +Correspondence with Option: - `Option.None` (tag=0) ↔ `Result.Err` (tag=0) - `Option.Some` (tag=1) ↔ `Result.Ok` (tag=1) -これは将来 Option → Result の cast を 0-cost にする余地のためだが、本 PR -では cast は実装しない (Non-Goal)。 +This leaves room to make a future Option → Result cast 0-cost, but this PR does +not implement the cast (Non-Goal). -### 言語レベルの挙動 +### Language-level behavior -#### 値構築と分解 +#### Value construction and deconstruction ```rw def div_checked(a: int, b: int) -> Result[int, int]: @@ -122,23 +128,23 @@ def main() -> int: return 0 ``` -#### match parser の挙動 +#### Behavior of the match parser -`parse_match` は 1 つ目のアームを見て **style を決定する**: +`parse_match` looks at the first arm and **decides the style**: -- 1 つ目が `case Some(x):` か `case None:` → Option-style -- 1 つ目が `case Ok(x):` か `case Err(e):` → Result-style +- first is `case Some(x):` or `case None:` → Option-style +- first is `case Ok(x):` or `case Err(e):` → Result-style -2 つ目のアームは同じ style のもう一方であることを要求する: -- Option-style で 1 つ目が Some → 2 つ目は None を要求 -- Option-style で 1 つ目が None → 2 つ目は Some を要求 -- Result-style で 1 つ目が Ok → 2 つ目は Err を要求 -- Result-style で 1 つ目が Err → 2 つ目は Ok を要求 +The second arm is required to be the other one of the same style: +- Option-style with first Some → second requires None +- Option-style with first None → second requires Some +- Result-style with first Ok → second requires Err +- Result-style with first Err → second requires Ok -混在 (`case Some(x): ... case Err(e):`) は parser エラー -「expected `case None` (Option-style match)」のような明示メッセージで弾く。 +Mixing (`case Some(x): ... case Err(e):`) is rejected as a parser error with an +explicit message like "expected `case None` (Option-style match)". -#### エラーになるケース +#### Cases that become errors ```rw def main() -> int: @@ -162,12 +168,13 @@ def main() -> int: return 0 ``` -### コンポーネント別の変更 +### Changes by component -#### ランタイム +#### Runtime -**変更なし**。`Result[int, int]` は LLVM IR 内 (`insertvalue` + 定数 struct) -で完結。`div_checked` は example のユーザコードとして書く、組込みではない。 +**No change**. `Result[int, int]` completes inside LLVM IR (`insertvalue` + +constant struct). `div_checked` is written as user code in an example; it is not +a built-in. #### `rwc/types.py` @@ -175,11 +182,11 @@ def main() -> int: RESULT_INT_INT = _Primitive("Result[int, int]") ``` -`is_printable` / `is_numeric` には含めない。 +Do not include it in `is_printable` / `is_numeric`. #### `rwc/lexer.py` -新キーワード 3 つ: +3 new keywords: ```python KW_RESULT = auto() @@ -198,9 +205,9 @@ KEYWORDS = { #### `rwc/parser.py` -##### `parse_type` で `Result[int, int]` を受ける +##### Accept `Result[int, int]` in `parse_type` -`Option` 分岐の隣に追加: +Add next to the `Option` branch: ```python if t.kind == TokenKind.KW_RESULT: @@ -225,9 +232,9 @@ if t.kind == TokenKind.KW_RESULT: return A.TypeName("Result[int, int]", t.line, t.col) ``` -##### 式パーサで `Ok(e)` / `Err(e)` を受ける +##### Accept `Ok(e)` / `Err(e)` in the expression parser -`parse_unary` の `KW_SOME` / `KW_NONE` の直下に追加: +Add directly below `KW_SOME` / `KW_NONE` in `parse_unary`: ```python if t.kind == TokenKind.KW_OK: @@ -244,9 +251,9 @@ if t.kind == TokenKind.KW_ERR: return A.ErrExpr(arg, t.line, t.col) ``` -##### `parse_match` の拡張 +##### Extending `parse_match` -現状の `parse_match` を以下の構造に置き換える: +Replace the current `parse_match` with the following structure: ```python def parse_match(self) -> A.MatchStmt: @@ -319,7 +326,7 @@ def parse_match(self) -> A.MatchStmt: #### `rwc/ast_nodes.py` -新規式ノード: +New expression nodes: ```python @dataclass @@ -335,9 +342,9 @@ class ErrExpr: col: int ``` -`Expr` Union に追加。 +Add to the `Expr` Union. -`MatchStmt` を style 統合形に拡張: +Extend `MatchStmt` to a style-unified form: ```python @dataclass @@ -357,14 +364,14 @@ class MatchStmt: col: int ``` -style に応じて Option-style フィールドが埋まる/Result-style フィールドが -埋まる排他関係。 +An exclusive relationship in which either the Option-style fields are filled or +the Result-style fields are filled, depending on the style. #### `rwc/sema.py` -`_resolve_type` の dict に `"Result[int, int]": T.RESULT_INT_INT` を追加。 +Add `"Result[int, int]": T.RESULT_INT_INT` to the dict in `_resolve_type`. -`_check_expr` で `OkExpr` / `ErrExpr` を扱う: +Handle `OkExpr` / `ErrExpr` in `_check_expr`: ```python if isinstance(expr, A.OkExpr): @@ -379,7 +386,7 @@ if isinstance(expr, A.ErrExpr): return T.RESULT_INT_INT ``` -`_check_stmt` の `MatchStmt` を style 別に分岐: +Branch the `MatchStmt` in `_check_stmt` by style: ```python if isinstance(stmt, A.MatchStmt): @@ -404,10 +411,11 @@ if isinstance(stmt, A.MatchStmt): return ok_ret and err_ret ``` -`==` whitelist に `T.RESULT_INT_INT` を加えない (= 比較不可、既存パターン)。 -`Spawn` 禁止リストは特に追加なし (`Ok` / `Err` は組込みではなく式ノード -なので、`spawn Ok(1)` のような構文は parser が `Ok(1)` を Call ではなく -OkExpr として解釈するため、SpawnExpr の `Call` 制約で自動的に弾かれる)。 +Do not add `T.RESULT_INT_INT` to the `==` whitelist (= comparison not allowed, +existing pattern). No addition is needed for the `Spawn` forbidden list (`Ok` / +`Err` are not built-ins but expression nodes, so a syntax like `spawn Ok(1)` is +interpreted by the parser as an OkExpr rather than a Call, and is automatically +rejected by the `Call` constraint of SpawnExpr). #### `rwc/irgen.py` @@ -415,7 +423,7 @@ OkExpr として解釈するため、SpawnExpr の `Call` 制約で自動的に RW_RESULT_INT_INT_TY = ir.LiteralStructType([I64, I64]) ``` -(`RW_OPTION_INT_TY` と同形だが、可読性のため別エイリアス。) +(Same shape as `RW_OPTION_INT_TY`, but a separate alias for readability.) `llvm_type_of(T.RESULT_INT_INT) -> RW_RESULT_INT_INT_TY` @@ -434,7 +442,7 @@ if isinstance(expr, A.ErrExpr): return ctx.builder.insert_value(base, v, 1) ``` -`_emit_stmt` の `MatchStmt`: +`MatchStmt` in `_emit_stmt`: ```python if isinstance(stmt, A.MatchStmt): @@ -462,9 +470,9 @@ if isinstance(stmt, A.MatchStmt): return ``` -`_emit_arm` は payload を bound 変数の slot に store して block を emit、 -end_bb に branch する helper (Option の None arm のような bound 変数なし -ケースは `var is None` で分岐): +`_emit_arm` is a helper that stores the payload into the bound variable's slot, +emits the block, and branches to end_bb (a case with no bound variable, like +Option's None arm, is branched on `var is None`): ```python def _emit_arm(self, var, block, payload, ctx, end_bb): @@ -485,29 +493,30 @@ def _emit_arm(self, var, block, payload, ctx, end_bb): b.branch(end_bb) ``` -これで Option / Result どちらの match も同じ lowering ロジックを共有する。 +With this, both the Option and Result match share the same lowering logic. -### テスト +### Tests #### `tests/test_sema.py` (positive 6 + negative 6) Positive: -- `Ok(5)` が `Result[int, int]` -- `Err(0)` が `Result[int, int]` -- `def f() -> Result[int, int]: return Ok(1)` が通る -- `match` の Ok/Err 両アームが揃った形が通る -- Ok arm の bound `x` が int、Err arm の bound `e` も int として使える -- Result の match が return-coverage を満たす (両 arm が return → match 全体 - が return) +- `Ok(5)` is `Result[int, int]` +- `Err(0)` is `Result[int, int]` +- `def f() -> Result[int, int]: return Ok(1)` passes +- A form with both Ok/Err arms of `match` present passes +- The bound `x` of the Ok arm is usable as int, and the bound `e` of the Err arm + is usable as int too +- The Result match satisfies return-coverage (both arms return → the whole match + returns) Negative: -- `Ok("hi")` → 引数型エラー -- `Result[string, int]` → parser エラー -- `match` で Some + Err 混在 → parser エラー -- `match` で 1 つ目 Ok / 2 つ目 None → parser エラー -- `print(Ok(1))` → printable エラー -- `Ok(1) == Err(0)` → 比較不可エラー -- `Ok(1) == Some(1)` → 同型ではないので別エラー (おまけ) +- `Ok("hi")` → argument type error +- `Result[string, int]` → parser error +- Mixing Some + Err in `match` → parser error +- First Ok / second None in `match` → parser error +- `print(Ok(1))` → printable error +- `Ok(1) == Err(0)` → comparison-not-allowed error +- `Ok(1) == Some(1)` → a different error since they are not the same type (bonus) #### `examples/result_basic.rw` + `.expected` @@ -533,85 +542,86 @@ def main() -> int: return 0 ``` -期待出力: +Expected output: ``` 5 0 ``` -## ファイル別変更 +## Changes by file -### 変更 +### Changed -- `rwc/types.py` — `RESULT_INT_INT` プリミティブ -- `rwc/lexer.py` — `KW_RESULT` / `KW_OK` / `KW_ERR` キーワード -- `rwc/parser.py` — `parse_type` の Result 分岐、`Ok(e)`/`Err(e)` 式、 - `parse_match` を style 統合形に拡張 -- `rwc/ast_nodes.py` — `OkExpr` / `ErrExpr` を Expr Union に追加、`MatchStmt` - を style 統合形に拡張 -- `rwc/sema.py` — `_resolve_type` / `OkExpr`/`ErrExpr` の Sema / - `MatchStmt` の style 別 Sema -- `rwc/irgen.py` — `RW_RESULT_INT_INT_TY` / `Ok`/`Err` の irgen / - `_emit_match` を style 別に分岐 (共通 helper `_emit_arm`) +- `rwc/types.py` — the `RESULT_INT_INT` primitive +- `rwc/lexer.py` — the `KW_RESULT` / `KW_OK` / `KW_ERR` keywords +- `rwc/parser.py` — the Result branch in `parse_type`, the `Ok(e)`/`Err(e)` + expressions, extending `parse_match` to a style-unified form +- `rwc/ast_nodes.py` — add `OkExpr` / `ErrExpr` to the Expr Union, extend + `MatchStmt` to a style-unified form +- `rwc/sema.py` — `_resolve_type` / Sema for `OkExpr`/`ErrExpr` / per-style Sema + for `MatchStmt` +- `rwc/irgen.py` — `RW_RESULT_INT_INT_TY` / irgen for `Ok`/`Err` / branch + `_emit_match` by style (shared helper `_emit_arm`) - `tests/test_sema.py` — positive 6 + negative 7 -- `tests/test_e2e.py` — parametrize に `result_basic` +- `tests/test_e2e.py` — `result_basic` in the parametrize -### 新規 +### New - `examples/result_basic.rw` + `.expected` -- `docs/specs/11-result-type.md` (本ファイル) -- `docs/plans/2026-05-23-result-type.md` (writing-plans で作成) +- `docs/specs/11-result-type.md` (this file) +- `docs/plans/2026-05-23-result-type.md` (created with writing-plans) -### 変更なし +### Unchanged -- ランタイム (`runtime/*`) -- fiber スケジューラ -- 既存 spec docs +- runtime (`runtime/*`) +- fiber scheduler +- existing spec docs -## 検証 +## Verification ```sh # pytest uv run pytest -q -# 期待: 既存 115 + sema positive 6 + negative 7 + e2e 1 = 129 件全緑 +# expected: existing 115 + sema positive 6 + negative 7 + e2e 1 = 129 all green -# 単独実行 +# standalone run uv run rwc run examples/result_basic.rw -# 期待出力: 5\n0\n +# expected output: 5\n0\n -# 既存 example 回帰 +# existing example regression uv run rwc run examples/option_basic.rw uv run rwc run examples/list_basic.rw uv run rwc run examples/string_ops.rw uv run rwc run examples/spawn_many.rw -# ランタイム単体テストはランタイム変更なしのため触らない (回帰のみ) +# runtime unit tests are untouched since there is no runtime change (regression only) make -C runtime clean && make -C runtime ``` -## コミット構成 +## Commit structure 4 commits: -1. **rwc (lexer/parser/types/ast)**: `KW_RESULT/OK/ERR`、`parse_type` の - Result 分岐、`Ok(e)`/`Err(e)` 式、`OkExpr`/`ErrExpr` AST ノード、 - `MatchStmt` を style 統合形に拡張、`parse_match` 全面書き直し、 - `T.RESULT_INT_INT`、`_resolve_type`。型注釈と AST だけ通る -2. **rwc (sema)**: `OkExpr`/`ErrExpr` の Sema、`MatchStmt` の style 別 Sema、 - 既存 Option パスが影響を受けないことを確認、negative テスト一括 -3. **rwc (irgen)**: `RW_RESULT_INT_INT_TY`、`Ok`/`Err` の irgen、 - `_emit_match` を `_emit_arm` 経由で style 別に分岐、smoke 動作確認 -4. **examples + e2e**: `result_basic.rw` 追加、`tests/test_e2e.py` の - parametrize 更新 - -## リスクと対処 - -| リスク | 対処 | +1. **rwc (lexer/parser/types/ast)**: `KW_RESULT/OK/ERR`, the Result branch in + `parse_type`, the `Ok(e)`/`Err(e)` expressions, the `OkExpr`/`ErrExpr` AST + nodes, extending `MatchStmt` to a style-unified form, a full rewrite of + `parse_match`, `T.RESULT_INT_INT`, `_resolve_type`. Only the type annotation + and AST pass +2. **rwc (sema)**: Sema for `OkExpr`/`ErrExpr`, per-style Sema for `MatchStmt`, + confirming the existing Option path is unaffected, negative tests in one batch +3. **rwc (irgen)**: `RW_RESULT_INT_INT_TY`, irgen for `Ok`/`Err`, branch + `_emit_match` per style via `_emit_arm`, smoke verification +4. **examples + e2e**: add `result_basic.rw`, update the parametrize in + `tests/test_e2e.py` + +## Risks and mitigations + +| Risk | Mitigation | |---|---| -| `MatchStmt` の AST 拡張で既存 Option-style コードが壊れる | `style` フィールドのデフォルトは要らない (parser が必ず set する)。既存 `examples/option_basic.rw` を e2e に残してあるので回帰検出可能 | -| `parse_match` の全面書き直しで既存テストが落ちる | 既存テスト (`test_match_two_arms_ok` 等) を変更せずに通るよう、Option-style の挙動を保つことを Step ごとに確認 | -| match parser 内の duplicate 検出 (同じ arm 2 回) を維持する | spec の `parse_match` スケルトンに duplicate-check の TODO を入れず、コード内で `if some_block is not None: raise duplicate error` のような既存パターンを踏襲 | -| `Ok`/`Err` を変数名としてユーザコードに使っているケース | `grep -rE '\b(Ok\|Err)\b' examples/ tests/*.rw` で確認 (該当なし)。Some/None と同じく新キーワードとして導入 | -| Option と Result の LLVM struct が同形なので irgen バグで型混同 | Sema が必ず先に弾く + 名前付き alias `RW_RESULT_INT_INT_TY` で可読性を担保。実害はないが「混同が怖いから別 alias」というのは妥当 | -| 真のジェネリクスへの道筋が見えるか | `MatchStmt` の `style` フィールドは generalize するとき「constructor name list」に置き換えやすい。Sema の style 分岐は型 dispatch table 化できる。 4c での generalize に対する障害は少ない | +| The AST extension of `MatchStmt` breaks existing Option-style code | No default is needed for the `style` field (the parser always sets it). Since the existing `examples/option_basic.rw` is kept in e2e, regressions are detectable | +| The full rewrite of `parse_match` drops existing tests | Confirm step by step that the Option-style behavior is preserved so that existing tests (`test_match_two_arms_ok` etc.) pass without changes | +| Preserving the duplicate detection inside the match parser (the same arm twice) | Do not put a duplicate-check TODO into the `parse_match` skeleton in the spec; follow the existing pattern like `if some_block is not None: raise duplicate error` in the code | +| Cases where `Ok`/`Err` are used as variable names in user code | Confirmed with `grep -rE '\b(Ok\|Err)\b' examples/ tests/*.rw` (no hits). Introduce them as new keywords, the same as Some/None | +| Since the Option and Result LLVM structs have the same shape, an irgen bug could confuse the types | Sema always rejects first + the named alias `RW_RESULT_INT_INT_TY` ensures readability. There is no actual harm, but "a separate alias because confusion is scary" is reasonable | +| Whether a path toward true generics is visible | The `style` field of `MatchStmt` is easy to replace with a "constructor name list" when generalizing. The style branch in Sema can be turned into a type dispatch table. There are few obstacles to the generalization in 4c | diff --git a/docs/specs/12-netpoller-tcp.md b/docs/specs/12-netpoller-tcp.md index 762dd6a..1f6c540 100644 --- a/docs/specs/12-netpoller-tcp.md +++ b/docs/specs/12-netpoller-tcp.md @@ -1,72 +1,71 @@ -# rw netpoller + 最小 TCP API +# rw netpoller + minimal TCP API ## Context -ロードマップの **最終目標**「rw で echo server が書ける」を達成する PR。 -これまでの言語拡張 (string / Bytes / List / Option / Result / match) と -ランタイム拡張 (M:N スケジューラ) を組み合わせ、**fiber が `recv` で -ブロックしているように見えて裏で kqueue/epoll が回る** という Go 風の -書き味を実現する。 +The PR that achieves the roadmap's **final goal**: "you can write an echo server in rw." +It combines the language extensions built so far (string / Bytes / List / Option / Result / match) +with the runtime extension (the M:N scheduler) to deliver a Go-style feel where **a fiber +appears to block on `recv` while kqueue/epoll spins underneath**. -これまでに揃った道具: +Tools assembled so far: -- M:N スケジューラ (#90): pthread worker + work-stealing + fiber 間 wait list -- 文字列 / Bytes (#91, #92): バイトデータの表現 -- List[int] (#93): クライアント fd の配列が持てる -- Option / Result (#94, #95): エラー表現の土台 (ただし今回は使わない) +- M:N scheduler (#90): pthread workers + work-stealing + inter-fiber wait list +- string / Bytes (#91, #92): representation of byte data +- List[int] (#93): can hold an array of client fds +- Option / Result (#94, #95): the foundation for error representation (not used this time) -最後に必要なのは: +What remains: -- **nonblocking I/O + fd readiness 監視**: `kqueue(2)` / `epoll(7)` -- **fiber を fd 監視に紐づけて park / wake**: netpoller スレッド -- **rw 言語からの TCP API**: `tcp_listen` / `tcp_accept` / `tcp_read` / `tcp_write` / `tcp_close` - *(注: `tcp_read` / `tcp_write` / `tcp_close` は後に `read` / `write` / `close` へ統合、後述)* +- **nonblocking I/O + fd readiness monitoring**: `kqueue(2)` / `epoll(7)` +- **park / wake a fiber tied to fd monitoring**: the netpoller thread +- **TCP API from the rw language**: `tcp_listen` / `tcp_accept` / `tcp_read` / `tcp_write` / `tcp_close` + *(Note: `tcp_read` / `tcp_write` / `tcp_close` were later consolidated into `read` / `write` / `close`, described below.)* -ロードマップ: +Roadmap: -1. 文字列 `len` / `==` / `+` (#91) -2. Bytes 型 (#92) +1. string `len` / `==` / `+` (#91) +2. Bytes type (#92) 3. List[int] (#93) 4a. Option[int] + match (#94) 4b. Result[int, int] (#95) -4c. (将来) 真のジェネリクス化 -5. **このサブプロジェクト**: netpoller + TCP API → echo server +4c. (future) true generics +5. **this sub-project**: netpoller + TCP API -> echo server ## Goals -- ランタイムに netpoller スレッドを追加 (kqueue/epoll、専用 pthread 1 つ) -- fiber を fd readiness で park / wake する内部 API (`rw_net_park_read/write`) -- rw 言語に TCP 組込み: +- Add a netpoller thread to the runtime (kqueue/epoll, one dedicated pthread) +- An internal API to park / wake a fiber on fd readiness (`rw_net_park_read/write`) +- TCP builtins in the rw language: - `tcp_listen(port: int) -> int` - `tcp_accept(listen_fd: int) -> int` - - `read(fd: int, max: int) -> Bytes` *(旧 `tcp_read`、fd 汎用に統合)* - - `write(fd: int, b: Bytes) -> int` *(旧 `tcp_write`、fd 汎用に統合)* - - `close(fd: int) -> int` *(旧 `tcp_close`、fd 汎用に統合)* -- `examples/tcp_echo.rw` が動く (1 接続 + 10 並列接続を Python から検証) -- 公開既存 ABI と既存 example はすべて回帰なし + - `read(fd: int, max: int) -> Bytes` *(formerly `tcp_read`, consolidated as generic fd)* + - `write(fd: int, b: Bytes) -> int` *(formerly `tcp_write`, consolidated as generic fd)* + - `close(fd: int) -> int` *(formerly `tcp_close`, consolidated as generic fd)* +- `examples/tcp_echo.rw` works (verified from Python with 1 connection + 10 concurrent connections) +- No regressions to the existing public ABI or any existing example ## Non-Goals - IPv6 / UDP / TLS / Unix domain socket -- `tcp_listen` のホスト指定 (`0.0.0.0` 固定 IPv4) -- 詳細エラー (errno 取得 API) -- graceful shutdown / SIGINT ハンドラ (Ctrl-C で殺す前提) -- partial write の自動 retry (`write` は実際に書いたバイト数を返すだけ、 - 全部書ききるのはユーザコードの責任) -- 同一 fd への複数 fiber 同時 park (protocol で 1 fd = 1 fiber 規約) -- Result 型でのエラー表現 (`read` は `Bytes` を返し len==0 で EOF/エラー - 両方を表現) -- 接続数の C10k ベンチ (最小 e2e のみ。1 接続成功 + 10 並列接続) -- ファイル I/O / pipe / TTY (今回 socket のみ、kqueue/epoll で監視可能な - fd でも他種類はスコープ外) -- fd 上限の自動引き上げ (シェルで `ulimit -n` を上げる前提、ランタイムが - `setrlimit` を呼ぶことはしない) -- main thread の fiber 化 (main は worker でも fiber でもない単独 thread - のまま、`tcp_accept` を main から呼ぶと blocking accept で kernel sleep) - -## 設計 - -### システム全体図 +- Host specification for `tcp_listen` (fixed to IPv4 `0.0.0.0`) +- Detailed errors (an errno-retrieval API) +- Graceful shutdown / SIGINT handler (assume the process is killed with Ctrl-C) +- Automatic retry of partial writes (`write` only returns the number of bytes actually + written; writing everything out is the user code's responsibility) +- Multiple fibers parking on the same fd simultaneously (protocol convention: 1 fd = 1 fiber) +- Error representation via the Result type (`read` returns `Bytes` and uses len==0 to represent + both EOF and error) +- A C10k connection-count benchmark (minimal e2e only: 1 successful connection + 10 concurrent connections) +- File I/O / pipe / TTY (socket only this time; other fd kinds, even those monitorable by + kqueue/epoll, are out of scope) +- Automatic raising of the fd limit (assume `ulimit -n` is raised in the shell; the runtime + does not call `setrlimit`) +- Turning the main thread into a fiber (main stays a standalone thread that is neither a worker + nor a fiber; calling `tcp_accept` from main does a blocking accept that sleeps in the kernel) + +## Design + +### System overview diagram ``` +---------------------+ @@ -87,47 +86,47 @@ +---+----+ +---+----+ +---+----+ | | | v v v - fiber 実行 (read / write / ...) + fiber execution (read / write / ...) | | EAGAIN → rw_net_park_read(fd) | ↓ - | netpoller に登録、fiber WAITING、worker M は次の fiber へ + | register with netpoller, fiber WAITING, worker M moves to next fiber | - | (netpoller が ready を検知すると上の矢印に戻る) + | (when the netpoller detects ready, control returns to the arrow above) ``` -スレッド本数: +Thread counts: -| 種類 | 本数 | 役割 | +| Kind | Count | Role | |---|---|---| -| main | 1 | `rw_user_main` 実行、`tcp_accept` で blocking sleep | -| worker M | `sysconf(_SC_NPROCESSORS_ONLN)`、上限 64 | fiber を `find_runnable` + `rw_fiber_swap` で実行 | -| netpoller | 1 | `kevent` / `epoll_wait` で fd ready を監視し、対応 fiber を `enqueue_ready` | +| main | 1 | Runs `rw_user_main`, blocking-sleeps in `tcp_accept` | +| worker M | `sysconf(_SC_NPROCESSORS_ONLN)`, capped at 64 | Runs fibers via `find_runnable` + `rw_fiber_swap` | +| netpoller | 1 | Watches fd readiness with `kevent` / `epoll_wait` and `enqueue_ready`s the corresponding fiber | -= **`nproc + 2` 本** (接続数に依存しない)。 += **`nproc + 2` threads** (independent of the connection count). -### netpoller スレッドの動き +### How the netpoller thread works ```c void *rw_netpoller_main(void *arg) { while (!atomic_load(&g_netpoller_shutdown)) { - // 1 回の syscall で最大 128 event を取得 (kernel が sleep してくれる) + // Fetch up to 128 events per syscall (the kernel sleeps for us) int n = kevent(g_kq, NULL, 0, events, 128, NULL); for (int i = 0; i < n; i++) { rw_fiber_handle *f = (rw_fiber_handle *)events[i].udata; - // 1 fd = 1 fiber (protocol 規約)、ONESHOT なので再登録なし - enqueue_ready(f); // 既存の M:N スケジューラを再利用 + // 1 fd = 1 fiber (protocol convention); ONESHOT means no re-registration + enqueue_ready(f); // Reuse the existing M:N scheduler } } return NULL; } ``` -ONESHOT モード (`EV_ONESHOT` / `EPOLLONESHOT`) を使う理由: +Why ONESHOT mode (`EV_ONESHOT` / `EPOLLONESHOT`) is used: -- 一度 ready 通知したら kernel が自動的に監視から外す -- 同じ fd を再 park したいときは fiber が改めて `rw_net_park_*` を呼んで登録 -- 「fiber が close する前に複数 fiber が park してしまう」race の窓を最小化 +- Once readiness is signaled, the kernel automatically removes the fd from monitoring +- To park the same fd again, the fiber calls `rw_net_park_*` once more to re-register +- Minimizes the race window where "multiple fibers park before the fiber closes" ### park / wake API @@ -136,7 +135,7 @@ ONESHOT モード (`EV_ONESHOT` / `EPOLLONESHOT`) を使う理由: void rw_netpoller_init(void); void rw_netpoller_shutdown(void); -int rw_set_nonblocking(int fd); // O_NONBLOCK を立てる (idempotent) +int rw_set_nonblocking(int fd); // Set O_NONBLOCK (idempotent) // Block current fiber until fd is readable / writable. // Must be called from a fiber (tls_m != NULL). @@ -144,20 +143,20 @@ void rw_net_park_read(int fd); void rw_net_park_write(int fd); ``` -`rw_net_park_read` の実装: +Implementation of `rw_net_park_read`: ```c void rw_net_park_read(int fd) { rw_M *m = tls_m; rw_fiber_handle *me = m->current; atomic_store_explicit(&me->state, RW_FIBER_WAITING, memory_order_relaxed); - register_for_read(fd, me); // kqueue / epoll に EV_ONESHOT で登録 + register_for_read(fd, me); // Register with kqueue / epoll using EV_ONESHOT rw_fiber_swap(&me->ctx, &m->sched_ctx); - // ここから戻った時点で netpoller がこの fiber を enqueue_ready している + // By the time control returns here, the netpoller has enqueue_ready'd this fiber } ``` -`register_for_read` のプラットフォーム別実装: +Platform-specific implementations of `register_for_read`: ```c // netpoller_kqueue.c @@ -173,22 +172,23 @@ static void register_for_read(int fd, rw_fiber_handle *f) { .events = EPOLLIN | EPOLLONESHOT, .data.ptr = f, }; - // 既に登録済みなら MOD、未登録なら ADD。両方試して片方を成功とする。 + // MOD if already registered, ADD if not. Try both and accept whichever succeeds. if (epoll_ctl(g_ep, EPOLL_CTL_MOD, fd, &ev) != 0) { epoll_ctl(g_ep, EPOLL_CTL_ADD, fd, &ev); } } ``` -epoll は ADD/MOD で挙動が違うので両方試行。kqueue は EV_ADD だけで十分。 +epoll behaves differently for ADD vs. MOD, so try both. For kqueue, EV_ADD alone suffices. -### tcp_* helper の実装パターン +### Implementation pattern for the tcp_* helpers -> **注 (#33):** `rw_tcp_read` / `rw_tcp_write` / `rw_tcp_close` は `runtime/io.c` の -> `rw_read` / `rw_write` / `rw_close` に統合・削除済み。以下は設計時の参考実装。 +> **Note (#33):** `rw_tcp_read` / `rw_tcp_write` / `rw_tcp_close` have been consolidated into +> `rw_read` / `rw_write` / `rw_close` in `runtime/io.c` and removed. The following is the +> reference implementation from design time. ```c -// runtime/net/tcp.c (歴史的参考実装 — 現在は runtime/io.c に統合) +// runtime/net/tcp.c (historical reference implementation — now consolidated into runtime/io.c) int64_t rw_tcp_listen(int64_t port) { int fd = socket(AF_INET, SOCK_STREAM, 0); @@ -203,22 +203,22 @@ int64_t rw_tcp_listen(int64_t port) { if (bind(fd, (void*)&addr, sizeof(addr)) < 0) { close(fd); return -1; } if (listen(fd, 128) < 0) { close(fd); return -1; } return fd; - // 注: listen fd は blocking のまま。main thread が tcp_accept で - // 待つときに kernel sleep できるよう。fiber 経由で呼ばれた最初の - // tcp_accept で nonblocking に切り替える。 + // Note: the listen fd stays blocking, so the main thread can kernel-sleep + // while it waits in tcp_accept. It is switched to nonblocking on the first + // tcp_accept invoked from a fiber. } int64_t rw_tcp_accept(int64_t listen_fd) { if (tls_m == NULL) { - // main thread: blocking accept (kernel が main を寝かす、 - // worker M / netpoller は別 thread なので影響なし) + // main thread: blocking accept (kernel puts main to sleep; worker M / + // netpoller run on separate threads, so they are unaffected) int c = accept(listen_fd, NULL, NULL); if (c < 0) return -1; rw_set_nonblocking(c); return c; } - // fiber 内: nonblocking accept + netpoller park - rw_set_nonblocking(listen_fd); // idempotent (初回のみ effect) + // in a fiber: nonblocking accept + netpoller park + rw_set_nonblocking(listen_fd); // idempotent (effect only on first call) for (;;) { int c = accept(listen_fd, NULL, NULL); if (c >= 0) { rw_set_nonblocking(c); return c; } @@ -240,8 +240,8 @@ void rw_tcp_read(rw_str *out, int64_t fd, int64_t max) { if (n == 0) { free(buf); out->len = 0; out->ptr = NULL; return; } if (errno == EAGAIN || errno == EWOULDBLOCK) { if (tls_m) { rw_net_park_read((int)fd); continue; } - // main thread (blocking でない fd を main が読む通常はないが、 - // 念のため) は EAGAIN を error 扱い + // main thread (it is unusual for main to read a nonblocking fd, but + // just in case) treats EAGAIN as an error free(buf); out->len = 0; out->ptr = NULL; return; } free(buf); out->len = 0; out->ptr = NULL; return; @@ -266,67 +266,67 @@ int64_t rw_tcp_close(int64_t fd) { } ``` -### rw 言語側の組込み +### Builtins on the rw language side -> **更新 (#33 / [15-file-io](15-file-io.md)):** `tcp_read` / `tcp_write` / -> `tcp_close` は fd 汎用の `read` / `write` / `close` に統合された。ソケットの -> 読み書き・クローズはこれらを使う (実装は `runtime/io.c`)。ソケット固有の -> `tcp_listen` / `tcp_accept` はそのまま。詳細は -> [`15-file-io.md`](15-file-io.md) を参照。 +> **Update (#33 / [15-file-io](15-file-io.md)):** `tcp_read` / `tcp_write` / +> `tcp_close` have been consolidated into the generic-fd `read` / `write` / `close`. +> Reading, writing, and closing a socket use these (implemented in `runtime/io.c`). The +> socket-specific `tcp_listen` / `tcp_accept` remain as-is. See +> [`15-file-io.md`](15-file-io.md) for details. -組込み関数を Sema / irgen に追加。シグネチャは: +Add the builtin functions to Sema / irgen. Signatures: ``` tcp_listen(int) -> int tcp_accept(int) -> int -read(int, int) -> Bytes # 旧 tcp_read -write(int, Bytes) -> int # 旧 tcp_write -close(int) -> int # 旧 tcp_close +read(int, int) -> Bytes # formerly tcp_read +write(int, Bytes) -> int # formerly tcp_write +close(int) -> int # formerly tcp_close ``` -全部 `spawn` 禁止 (組込みは spawn できない既存ルール)。 +All forbid `spawn` (existing rule: builtins cannot be spawned). -irgen は既存パターン: -- `read` は **pointer-out** (16 byte 戻り値だが alloca + load パターンに揃える) -- `write` は Bytes (16 byte) を value 渡し -- 残り 3 つはスカラ +irgen follows the existing patterns: +- `read` is **pointer-out** (a 16-byte return value, but aligned with the alloca + load pattern) +- `write` passes Bytes (16 bytes) by value +- The remaining three are scalar -### ファイル構成 +### File structure ``` -runtime/net/ (新規ディレクトリ) -├── netpoller.h (共通 API 宣言) -├── netpoller.c (init / shutdown / park / 共通ロジック) -├── netpoller_kqueue.c (macOS 固有: kevent ベース) -├── netpoller_epoll.c (Linux 固有: epoll ベース) +runtime/net/ (new directory) +├── netpoller.h (shared API declarations) +├── netpoller.c (init / shutdown / park / shared logic) +├── netpoller_kqueue.c (macOS-specific: kevent-based) +├── netpoller_epoll.c (Linux-specific: epoll-based) ├── tcp.h └── tcp.c -runtime/runtime.h (tcp_listen/tcp_accept + read/write/close/file_open + 2 つの park プロトタイプ追加) -runtime/runtime.c (rw_init / rw_shutdown に netpoller 呼び出し追加) -runtime/Makefile (net/*.o + uname 分岐) +runtime/runtime.h (add tcp_listen/tcp_accept + read/write/close/file_open + the two park prototypes) +runtime/runtime.c (add netpoller calls to rw_init / rw_shutdown) +runtime/Makefile (net/*.o + uname branching) -runtime/fiber/test_netpoller_pipe.c (新規 C テスト) -runtime/fiber/test_tcp_loopback.c (新規 C テスト) +runtime/fiber/test_netpoller_pipe.c (new C test) +runtime/fiber/test_tcp_loopback.c (new C test) -rwc/sema.py (tcp_listen/tcp_accept/read/write/close + spawn 禁止) -rwc/irgen.py (tcp_listen/tcp_accept/read/write/close emit) +rwc/sema.py (tcp_listen/tcp_accept/read/write/close + spawn prohibition) +rwc/irgen.py (emit tcp_listen/tcp_accept/read/write/close) -examples/tcp_echo.rw (echo server デモ) -examples/tcp_echo.rw.expected (今回は使わない、後述) +examples/tcp_echo.rw (echo server demo) +examples/tcp_echo.rw.expected (not used this time, described below) -tests/test_e2e_tcp.py (Python から socket で接続して echo を検証) +tests/test_e2e_tcp.py (connect via socket from Python and verify echo) -docs/specs/12-netpoller-tcp.md (本ファイル) -docs/plans/2026-05-23-netpoller-tcp.md (writing-plans で作成) +docs/specs/12-netpoller-tcp.md (this file) +docs/plans/2026-05-23-netpoller-tcp.md (created with writing-plans) ``` -### e2e テスト戦略 +### e2e test strategy -通常の `tests/test_e2e.py` のような「stdout を `.expected` と比較」は echo -server には合わない (echo server は無限ループで stdout も無い)。 +The usual "compare stdout against `.expected`" approach of `tests/test_e2e.py` does not fit an +echo server (an echo server runs an infinite loop and produces no stdout). -新規ファイル `tests/test_e2e_tcp.py`: +New file `tests/test_e2e_tcp.py`: ```python import socket, subprocess, time, signal, os @@ -339,16 +339,16 @@ def _free_port(): return port def _start_server(port): - # examples/tcp_echo.rw をビルドして起動 - # ただし port は環境変数 RW_ECHO_PORT で渡せるよう example 側も対応 - # (もしくは tcp_listen の引数を埋め込んだ tmp .rw を生成) + # Build and launch examples/tcp_echo.rw + # The example side also accepts the port via the RW_ECHO_PORT env var + # (or generate a tmp .rw with the tcp_listen argument baked in) ... def test_echo_single(): port = _free_port() proc = _start_server(port) try: - time.sleep(0.2) # サーバ起動待ち + time.sleep(0.2) # wait for the server to start s = socket.create_connection(('127.0.0.1', port)) s.sendall(b"hello\n") assert s.recv(1024) == b"hello\n" @@ -372,51 +372,51 @@ def test_echo_concurrent_10(): proc.terminate(); proc.wait() ``` -port は `socket.bind(0)` で空きを確保。example の port をハードコードできない -ので、`tcp_echo.rw` の最終形は **port を `argv` か環境変数から読む** 必要が -ある。 +The port is reserved as a free one via `socket.bind(0)`. Since the example's port cannot be +hardcoded, the final form of `tcp_echo.rw` needs to **read the port from `argv` or an +environment variable**. -ただし rw 言語に argv / env 読み取り API はまだ無い → **e2e 用に -`examples/tcp_echo.rw` を毎回テキスト置換** する形にする (port 数字だけ -書き換える簡易テンプレ)。これが最小コスト。 +However, rw does not yet have an argv / env reading API -> instead, **text-substitute +`examples/tcp_echo.rw` for each e2e run** (a simple template that only rewrites the port +number). This is the lowest-cost approach. -### コミット構成 (1 PR、6 commits) +### Commit structure (1 PR, 6 commits) -1. **runtime: netpoller スケルトン (init/shutdown のみ)** +1. **runtime: netpoller skeleton (init/shutdown only)** - `runtime/net/netpoller.{c,h}`, `netpoller_kqueue.c`, `netpoller_epoll.c` - - `rw_netpoller_init` で pthread を起動、`rw_netpoller_shutdown` で join - - Makefile に `net/*.o` 追加、`uname -s` で kqueue/epoll を分岐 - - `runtime/runtime.c` の `rw_init` / `rw_shutdown` に呼び出し追加 - - C テスト `test_netpoller_init.c` (init/shutdown を 10 回回す) - -2. **runtime: park/wake と pipe テスト** - - `rw_set_nonblocking`、`rw_net_park_read/write` の本実装 - - netpoller スレッド本体のループ - - C テスト `test_netpoller_pipe.c`: pipe 2 つを作り、reader fiber が - `rw_net_park_read` で park、writer fiber が write、reader が起きるか - -3. **runtime: tcp_* helper** + - `rw_netpoller_init` starts the pthread, `rw_netpoller_shutdown` joins it + - Add `net/*.o` to the Makefile, branch on kqueue/epoll via `uname -s` + - Add calls to `rw_init` / `rw_shutdown` in `runtime/runtime.c` + - C test `test_netpoller_init.c` (run init/shutdown 10 times) + +2. **runtime: park/wake and pipe test** + - Real implementation of `rw_set_nonblocking` and `rw_net_park_read/write` + - The netpoller thread's main loop + - C test `test_netpoller_pipe.c`: create two pipes; the reader fiber parks via + `rw_net_park_read`, the writer fiber writes, and check that the reader wakes up + +3. **runtime: tcp_* helpers** - `runtime/net/tcp.{c,h}` - - 5 関数の実装 - - C テスト `test_tcp_loopback.c`: localhost で listen → connect → recv/send + - Implementation of the 5 functions + - C test `test_tcp_loopback.c`: listen -> connect -> recv/send on localhost -4. **rwc: 組込みを sema + irgen** - - `_check_call` に分岐 + spawn 禁止 - - `_emit_call` に分岐 (`read` は pointer-out shim) - - positive / negative テスト (引数型エラー、spawn 禁止) +4. **rwc: builtins in sema + irgen** + - Branches in `_check_call` + spawn prohibition + - Branches in `_emit_call` (`read` uses the pointer-out shim) + - positive / negative tests (argument-type errors, spawn prohibition) 5. **examples + e2e** - `examples/tcp_echo.rw` - `tests/test_e2e_tcp.py` (single + concurrent 10) - - `tests/test_e2e.py` には追加しない (echo server は stdout 比較に - 合わないので別 e2e ファイル) + - Not added to `tests/test_e2e.py` (an echo server does not fit stdout comparison, so a + separate e2e file) -6. **plan ファイル commit** +6. **commit the plan file** -### 検証 +### Verification ```sh -# ランタイム単体 +# runtime alone make -C runtime clean && make -C runtime cd runtime cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_netpoller_pipe.c librw.a -o fiber/test_netpoller_pipe && ./fiber/test_netpoller_pipe @@ -425,25 +425,25 @@ cc -O2 -Wall -Wextra -std=c11 -D_GNU_SOURCE -pthread fiber/test_tcp_loopback.c l # pytest cd .. uv run pytest -q -# 期待: 既存 131 + sema 新規 10 + e2e_tcp 新規 2 = 143 件 +# Expected: existing 131 + 10 new sema + 2 new e2e_tcp = 143 total -# 手動 echo 確認 +# manual echo check RW_ECHO_PORT=18080 uv run rwc run examples/tcp_echo.rw & sleep 0.2 nc 127.0.0.1 18080 <<< 'hello' # => hello kill %1 ``` -## リスクと対処 +## Risks and mitigations -| リスク | 対処 | +| Risk | Mitigation | |---|---| -| kqueue と epoll の API 差異が漏れる | netpoller.h で共通 API、`netpoller_kqueue.c` / `netpoller_epoll.c` を `#if defined(__APPLE__)` で完全分離。Linux CI で epoll パスを検証、ローカル macOS で kqueue パス | -| fiber が park 直後 close されると ONESHOT 登録が garbage に | ONESHOT は kernel が自動で外す。close 後に kevent が ready を返してきても fiber pointer は無効でない (fiber handle は join まで生きる)。double-wake のリスクのみで実害なし | -| main thread が tcp_accept で sleep 中に Ctrl-C が来る | OS が SIGINT で main を起こす、accept が EINTR で返る → rw_tcp_accept は -1 を返してユーザコードがループから抜ける。これは spec の現状動作 | -| ephemeral port を CI で確保するときの port 衝突 | Python 側で `socket.bind(0)` で空き port を取り、それを env 経由で rw に渡す | -| 接続失敗時の `tcp_accept` リトライ無限ループ | accept が EAGAIN 以外のエラー (ECONNABORTED など) を返したら -1 を返す。ユーザコードが `if c < 0: break` で抜ける責任 | -| netpoller スレッドが kevent でずっと寝続けて shutdown シグナルに気づかない | shutdown 時に `g_netpoller_shutdown = 1` をセットし、self-pipe または eventfd で netpoller を起こす (kqueue は EVFILT_USER、epoll は eventfd) | -| 既存 e2e (`test_e2e.py`) は無限ループサーバを実行しないので問題なし | parametrize に tcp_echo は追加しない。新規 `test_e2e_tcp.py` で扱う | -| Linux で `epoll_ctl` の MOD/ADD どっち呼ぶか | ADD 失敗 (EEXIST) のときに MOD にフォールバック、または逆。両方試す形で吸収 | -| fd 上限 (`ulimit -n`) を超えた接続で accept が EMFILE | エラー扱い (`-1` 返す)。ユーザコードで対処。spec で「ulimit -n を上げる前提」明示 | +| API differences between kqueue and epoll leak through | Shared API in netpoller.h; fully separate `netpoller_kqueue.c` / `netpoller_epoll.c` with `#if defined(__APPLE__)`. Verify the epoll path in Linux CI and the kqueue path locally on macOS | +| The ONESHOT registration becomes garbage if a fiber is closed right after parking | ONESHOT is auto-removed by the kernel. Even if kevent returns ready after close, the fiber pointer is not invalid (the fiber handle lives until join). Only a double-wake risk, with no real harm | +| Ctrl-C arrives while the main thread sleeps in tcp_accept | The OS wakes main with SIGINT, accept returns with EINTR -> rw_tcp_accept returns -1 and the user code breaks out of the loop. This is the current spec behavior | +| Port collision when reserving an ephemeral port in CI | Take a free port on the Python side with `socket.bind(0)` and pass it to rw via env | +| Infinite `tcp_accept` retry loop on connection failure | If accept returns an error other than EAGAIN (such as ECONNABORTED), return -1. The user code is responsible for breaking out with `if c < 0: break` | +| The netpoller thread stays asleep in kevent and never notices the shutdown signal | On shutdown, set `g_netpoller_shutdown = 1` and wake the netpoller with a self-pipe or eventfd (EVFILT_USER for kqueue, eventfd for epoll) | +| The existing e2e (`test_e2e.py`) is fine since it does not run an infinite-loop server | Do not add tcp_echo to the parametrize list; handle it in the new `test_e2e_tcp.py` | +| Whether to call MOD or ADD for `epoll_ctl` on Linux | On ADD failure (EEXIST) fall back to MOD, or vice versa. Absorb by trying both | +| accept returns EMFILE for connections beyond the fd limit (`ulimit -n`) | Treat as an error (return `-1`). Handle in user code. The spec makes the "assume ulimit -n is raised" assumption explicit | diff --git a/docs/specs/13-for-range-loop.md b/docs/specs/13-for-range-loop.md index 70f9d62..bc63952 100644 --- a/docs/specs/13-for-range-loop.md +++ b/docs/specs/13-for-range-loop.md @@ -1,13 +1,15 @@ -# rw `for ... in range(...)` ループ (構文糖, while への desugar) +# rw `for ... in range(...)` loop (syntactic sugar, desugared to while) ## Context -rw はコンパイラ・ランタイム・サンプル・テストが一体となって育つ小さな言語で、 -これまで型 (string / Bytes / List[int] / Option / Result) と並行・I/O (fibers / -scheduler / netpoller + TCP) を 1 サブプロジェクト 1 PR の粒度で積み上げてきた。 +rw is a small language whose compiler, runtime, examples, and tests grow +together. So far it has accumulated types (string / Bytes / List[int] / Option +/ Result) and concurrency/I/O (fibers / scheduler / netpoller + TCP) at a +granularity of one sub-project per PR. -一方、制御フローは初期の `if` / `while` から拡張されておらず、最も使用頻度の -高い「カウントループ」を書くたびに以下のような定型を手で書く必要がある: +Control flow, on the other hand, has not been extended beyond the initial `if` +/ `while`, so the most frequently used "counting loop" requires writing the +following boilerplate by hand every time: ```rw i: int = 0 @@ -16,45 +18,54 @@ while i < n: i = i + 1 ``` -このサブプロジェクトは、制御フロー強化のロードマップの第一歩として -`for in range(...)` を導入する。設計の核心は **`range` を値として持つ -型を作らず、`for` 専用の構文要素として扱い、sema で `while` ループへ desugar -する** こと。これにより型システム (`types.py`) への波及をゼロに保ち、変更を -lexer / parser / sema の 3 層 + 例題に閉じ込める。 +This sub-project introduces `for in range(...)` as the first step of the +control-flow enhancement roadmap. The core of the design is to **not create a +type that holds `range` as a value; instead treat it as a `for`-specific +syntactic element and desugar it into a `while` loop in sema**. This keeps the +ripple effect on the type system (`types.py`) at zero and confines the change +to the three layers lexer / parser / sema plus the examples. -制御フローのロードマップ (このサブプロジェクトはその 1 つ目): +Control-flow roadmap (this sub-project is the first of it): -1. **このサブプロジェクト**: `for ... in range(start, stop[, step])` -2. `break` / `continue` (`for` / `while` 共通) -3. `for in ` などのイテレータ形式 -4. `if` を式として使う / 三項相当 +1. **This sub-project**: `for ... in range(start, stop[, step])` +2. `break` / `continue` (shared between `for` / `while`) +3. Iterator forms such as `for in ` +4. Using `if` as an expression / ternary equivalent ## Goals -- 新しい文 `for in range():` を導入 +- Introduce a new statement `for in range():` - `range(stop)` — start=0, step=1 - `range(start, stop)` — step=1 - `range(start, stop, step)` -- start / stop / step は **任意の `int` 式** (変数・関数呼び出しを含む) -- 半開区間 `[start, stop)`、ループ変数はループ本体スコープの `int` -- 負の `step` に対応 (`stop` を下回るまで降順に反復) -- `step == 0` は **0 回ループ** (本体を実行せず即終了)。desugar した - ループ条件が step==0 で両辺 false になる性質をそのまま利用する -- `for` ノードを `while` + 代入の AST に desugar し、sema / irgen は既存の - `while` 処理をそのまま使う (両層とも無改修) +- start / stop / step are **arbitrary `int` expressions** (including variables + and function calls) +- Half-open interval `[start, stop)`; the loop variable is an `int` scoped to + the loop body +- Support negative `step` (iterate in descending order until falling below + `stop`) +- `step == 0` means **zero iterations** (terminate immediately without + executing the body). This exploits the property that the desugared loop + condition makes both sides false when step==0 +- Desugar the `for` node into a `while` + assignment AST; sema / irgen reuse the + existing `while` handling as-is (both layers unchanged) ## Non-Goals -- **`range` を値として扱う**こと。`x = range(0, 10)` や関数引数への `range` - は構文エラー。`range` は `for ... in` の直後でのみ受理する - (parser で「`range` can only appear in a for-loop header」エラー) -- `for in ` のイテレータ形式 (別 PR) -- `break` / `continue` (別 PR、制御フロー ロードマップの 2 つ目) -- `range` を `int` 以外 (`List` など) で使うこと -- ループ変数への再代入禁止の厳密なチェック (最小限の扱い) -- `step` の定数畳み込みによる条件簡約 (LLVM の最適化に委ねる) - -## 構文 +- **Treating `range` as a value**. `x = range(0, 10)` or passing `range` as a + function argument is a syntax error. `range` is only accepted immediately + after `for ... in` (the parser raises the error "`range` can only appear in a + for-loop header") +- The iterator form `for in ` (separate PR) +- `break` / `continue` (separate PR, the second item of the control-flow + roadmap) +- Using `range` with types other than `int` (`List`, etc.) +- Strict checking that forbids reassignment of the loop variable (handled + minimally) +- Condition simplification via constant folding of `step` (delegated to LLVM's + optimizer) + +## Syntax ``` for_stmt := "for" IDENT "in" "range" "(" range_args ")" ":" NEWLINE block @@ -63,11 +74,12 @@ range_args := expr # stop | expr "," expr "," expr # start, stop, step ``` -`range` は予約語にしない (識別子 `range` をパーサが for ヘッダ位置で照合する)。 -予約語化すると変数名 `range` が使えなくなる副作用があるため、識別子マッチで -扱う。`for` / `in` は lexer に予約済み (`KW_FOR` / `KW_IN`)。 +`range` is not made a keyword (the parser matches the identifier `range` at the +for-header position). Making it a keyword would have the side effect of +prohibiting `range` as a variable name, so it is handled via identifier +matching. `for` / `in` are already reserved in the lexer (`KW_FOR` / `KW_IN`). -### 使用例 +### Usage example ```rw def main() -> int: @@ -79,78 +91,90 @@ def main() -> int: return total ``` -## 内部設計: sema による while への desugar +## Internal design: desugaring to while in sema -`for v in range(a, b, s):` を、sema が以下と等価な AST に変換する。引数の -**二重評価を防ぐため一時変数に束縛**する (start/stop/step が副作用を持つ -関数呼び出しでも 1 回だけ評価する): +sema transforms `for v in range(a, b, s):` into an AST equivalent to the +following. To **prevent double evaluation, the arguments are bound to temporary +variables** (so that start/stop/step are evaluated only once even when they are +function calls with side effects): ``` __stop = b __step = s -v = a # ループ変数 (ユーザー可視, int) +v = a # loop variable (user-visible, int) while (__step > 0 and v < __stop) or (__step < 0 and v > __stop): v = v + __step ``` -- 一時変数名はユーザー識別子と衝突しない内部名 (`__for_stop_N` 等, N は連番) -- ループ条件は step の符号で両側に分岐させる汎用形。定数畳み込みはせず、 - LLVM の最適化に委ねる (Non-Goal) -- `range(stop)` / `range(start, stop)` は不足引数を `0` / `1` のリテラル - ノードで補完してから上記に展開 -- desugar 後はすべて既存の AST ノード (`While` / `Assign` / `BinOp` / `If`) - なので、sema / irgen は無改修で動く +- The temporary variable names are internal names that do not collide with user + identifiers (`__for_stop_N`, etc., where N is a sequence number) +- The loop condition is a general form that branches on both sides based on the + sign of step. No constant folding is done; it is delegated to LLVM's optimizer + (Non-Goal) +- `range(stop)` / `range(start, stop)` are completed by filling in the missing + arguments with `0` / `1` literal nodes before expanding into the above form +- After desugaring, everything is an existing AST node (`While` / `Assign` / + `BinOp` / `If`), so sema / irgen work without modification -## step == 0 の挙動 +## Behavior when step == 0 -`step` は任意式なのでコンパイル時に 0 と判定できないケースがある。trap は -設けず、desugar したループ条件 `(step>0 and vstop)` -が step==0 で両辺 false になる性質をそのまま利用し、**0 回ループ** -(本体を実行せず即終了) とする。これにより runtime は無改修で済む。 +Because `step` is an arbitrary expression, there are cases where it cannot be +determined to be 0 at compile time. No trap is set up; instead the property that +the desugared loop condition `(step>0 and vstop)` makes +both sides false when step==0 is exploited directly, treating it as **zero +iterations** (terminate immediately without executing the body). This means the +runtime needs no modification. -## 触るレイヤー +## Layers touched -| レイヤー | ファイル | 変更 | +| Layer | File | Change | |---|---|---| -| Lexer | `rwc/lexer.py` | `KW_FOR` / `KW_IN` は予約済み (L70-71, L129-130)。追加不要 | -| AST | `rwc/ast_nodes.py` | `For` ノード追加 (var, start, stop, step, body)。`Stmt` Union に追加 | -| Parser | `rwc/parser.py` | `parse_for()` 追加、文ディスパッチに `KW_FOR` 追加。`range(...)` を for ヘッダ位置でのみ受理 | -| Desugar | `rwc/desugar.py` (新規) | parser 直後・sema 前に走る独立パス。`For` を `VarDecl`/`While`/`Assign` に書き換える。CLI のパイプラインに 1 行挿入 | -| Sema | `rwc/sema.py` | **無改修**。desugar 後は既存ノードのみなので型チェック・`local_types` 登録が自動で効く | -| irgen | `rwc/irgen.py` | **無改修**。desugar 済み while をそのまま処理 | -| Runtime | `runtime/` | **無改修**。step==0 は 0 回ループで処理 | -| Examples | `examples/for_count.rw` (+ `.expected`) | 新サンプル 1 | -| Tests | `tests/test_e2e.py` ほか | parametrize に `for_count` 追加。parser/desugar の unit test | - -実質 AST / parser / desugar(新規) + 例題で、sema・irgen・runtime は無改修。 -`incremental-language-extensions` の「1 PR 4 層まで」に収まる。 - -## 検証 - -- 例題 `examples/for_count.rw` を `rwc build` → 実行 → `.expected` と一致 -- positive テスト: - - 昇順 `range(0, 5)` が 0..4 - - `range(5)` (1 引数) が 0..4 - - 降順 `range(5, 0, -1)` が 5..1 - - step=2 `range(0, 10, 2)` が 0,2,4,6,8 - - 空ループ `range(5, 5)` / `range(0, -3)` は本体 0 回 - - step==0 `range(0, 10, 0)` は本体 0 回 (即終了) - - range 引数に変数・式を使う -- negative テスト: - - `for` 外で `range(...)` → 構文エラー - - `x = range(0, 5)` → 構文エラー - - range 引数が非 int → 型エラー (sema) - - `range()` 引数 0 個 / 4 個以上 → 構文エラー -- desugar の unit test: `For` ノードが `VarDecl`/`While`/`Assign` に展開される - -## リスクと対処 - -- **二重評価**: start/stop/step を一時変数に束縛して回避 (上記 desugar) -- **内部名の衝突**: 連番付き内部名 (`__for_stop_N`) でユーザー識別子と分離 -- **負 step の境界**: 条件を `(step>0 and vstop)` と - 両側に分け、step の符号で正しく終了 -- **step==0**: 上記条件が両辺 false になり 0 回ループで安全に終了 (trap 不要) -- **desugar の挿入漏れ**: `compile_source` / `emit_ir` / `emit_ast` の 3 経路 - すべてで parse 直後に desugar を呼ぶ (driver.py)。漏れると for が irgen に - 生のまま渡り "unknown stmt" になるため、3 経路を e2e で踏むテストで担保 +| Lexer | `rwc/lexer.py` | `KW_FOR` / `KW_IN` are already reserved (L70-71, L129-130). No addition needed | +| AST | `rwc/ast_nodes.py` | Add `For` node (var, start, stop, step, body). Add to the `Stmt` Union | +| Parser | `rwc/parser.py` | Add `parse_for()`, add `KW_FOR` to the statement dispatch. Accept `range(...)` only at the for-header position | +| Desugar | `rwc/desugar.py` (new) | Independent pass that runs right after the parser and before sema. Rewrites `For` into `VarDecl`/`While`/`Assign`. Insert one line into the CLI pipeline | +| Sema | `rwc/sema.py` | **Unchanged**. Since only existing nodes remain after desugaring, type checking and `local_types` registration work automatically | +| irgen | `rwc/irgen.py` | **Unchanged**. Processes the desugared while as-is | +| Runtime | `runtime/` | **Unchanged**. step==0 is handled as zero iterations | +| Examples | `examples/for_count.rw` (+ `.expected`) | 1 new example | +| Tests | `tests/test_e2e.py` and others | Add `for_count` to parametrize. Unit tests for parser/desugar | + +Effectively AST / parser / desugar (new) + examples, with sema / irgen / runtime +unchanged. This stays within the "up to 4 layers per PR" of +`incremental-language-extensions`. + +## Verification + +- Build the example `examples/for_count.rw` with `rwc build`, run it, and check + it matches `.expected` +- Positive tests: + - Ascending `range(0, 5)` yields 0..4 + - `range(5)` (1 argument) yields 0..4 + - Descending `range(5, 0, -1)` yields 5..1 + - step=2 `range(0, 10, 2)` yields 0,2,4,6,8 + - Empty loops `range(5, 5)` / `range(0, -3)` execute the body 0 times + - step==0 `range(0, 10, 0)` executes the body 0 times (immediate termination) + - Using variables and expressions as range arguments +- Negative tests: + - `range(...)` outside `for` → syntax error + - `x = range(0, 5)` → syntax error + - Non-int range argument → type error (sema) + - `range()` with 0 arguments / 4 or more arguments → syntax error +- Desugar unit test: the `For` node expands into `VarDecl`/`While`/`Assign` + +## Risks and mitigations + +- **Double evaluation**: avoided by binding start/stop/step to temporary + variables (the desugaring above) +- **Internal name collision**: separated from user identifiers by numbered + internal names (`__for_stop_N`) +- **Boundary with negative step**: split the condition into both sides as + `(step>0 and vstop)`, terminating correctly based on + the sign of step +- **step==0**: the above condition makes both sides false and terminates safely + as zero iterations (no trap needed) +- **Missing desugar insertion**: call desugar right after parse on all three + paths `compile_source` / `emit_ir` / `emit_ast` (driver.py). If it is missed, + a raw for is passed to irgen and becomes "unknown stmt", so this is guaranteed + by tests that exercise all three paths end-to-end diff --git a/docs/specs/14-ternary-expr.md b/docs/specs/14-ternary-expr.md index 9ba6c32..23a66e5 100644 --- a/docs/specs/14-ternary-expr.md +++ b/docs/specs/14-ternary-expr.md @@ -1,16 +1,16 @@ -# rw 条件式 / 三項演算子 (`then if cond else els`) +# rw conditional expression / ternary operator (`then if cond else els`) ## Context -rw はコンパイラ・ランタイム・サンプル・テストが一体で育つ小さな言語で、型 -(string / Bytes / List[int] / Option / Result) と並行・I/O、そして制御フロー -(`if` / `while` / `for ... in range`) を 1 サブプロジェクト 1 PR の粒度で -積み上げてきた。 +rw is a small language whose compiler, runtime, examples, and tests grow +together. It has accumulated types (string / Bytes / List[int] / Option / +Result), concurrency/I/O, and control flow (`if` / `while` / `for ... in +range`) at a granularity of one sub-project per PR. -`for ... in range` の spec ([[13-for-range-loop]]) で示した制御フロー ロード -マップの 4 番目「`if` を式として使う / 三項相当」に対応するのがこのサブ -プロジェクトである。これまで「条件で値を選ぶ」には文の `if`/`else` で一旦 -変数に代入する必要があった: +This sub-project corresponds to the fourth item of the control-flow roadmap +presented in the `for ... in range` spec ([[13-for-range-loop]]): "using `if` +as an expression / ternary equivalent". Until now, "choosing a value based on a +condition" required a statement `if`/`else` that assigns to a variable first: ```rw larger: int = 0 @@ -20,75 +20,86 @@ else: larger = b ``` -このサブプロジェクトは Python 互換の条件式 `then if cond else els` を導入し、 -上記を 1 行の式で書けるようにする: +This sub-project introduces the Python-compatible conditional expression `then +if cond else els`, so that the above can be written in a single-line expression: ```rw larger: int = a if a > b else b ``` -設計の核心は、**短絡 `and` / `or` が既に使っている cbranch + phi の IR -パターンをそのまま流用する**こと。新ノードは式 1 つ (`IfExpr`) のみで、 -desugar も lexer 変更も不要。変更は parser / sema / irgen + テスト・例題に -閉じる。 +The core of the design is to **reuse the cbranch + phi IR pattern that +short-circuit `and` / `or` already use**. The only new node is a single +expression (`IfExpr`); no desugar and no lexer change are needed. The change is +confined to parser / sema / irgen + tests and examples. ## Goals -- 新しい式 `then if cond else els` を導入 (Python の三項演算子と同一構文) -- 三項演算子は **最も低い優先度**。`x: int = 1 if c else 2` のように代入の - 右辺・関数引数・`return` 値など式が来る任意の位置で使える -- `cond` は `bool`、`then` / `els` は **同じ型**であることを要求し、その型を - 式全体の型とする -- ネストは **右結合**: `a if p else b if q else c` は - `a if p else (b if q else c)` と解釈する -- 評価は短絡的: 選ばれたブランチのみを評価する (cbranch により他方は実行 - されない) +- Introduce a new expression `then if cond else els` (identical syntax to + Python's ternary operator) +- The ternary operator has the **lowest precedence**. It can be used in any + position where an expression is allowed, such as the right-hand side of an + assignment, function arguments, or a `return` value, as in + `x: int = 1 if c else 2` +- Require `cond` to be `bool` and `then` / `els` to be **the same type**, and + use that type as the type of the whole expression +- Nesting is **right-associative**: `a if p else b if q else c` is interpreted + as `a if p else (b if q else c)` +- Evaluation is short-circuiting: only the selected branch is evaluated (cbranch + ensures the other is not executed) ## Non-Goals -- **if 式 / block 式** (式志向構文, #109 RFC)。文ブロックを式として値を返す - 仕組みは別の話で、ここでは扱わない -- **C 風の三項記号構文** `cond ? a : b`。Python 互換の `a if c else b` のみ -- **ブランチ間の暗黙の型昇格** (`int` ↔ `float` など)。両ブランチは同型必須。 - 混在は sema で型エラー -- **ネストの定数畳み込みや条件簡約**。素直に cbranch + phi で生成し、最適化は - LLVM (および将来の最適化レベル #55) に委ねる +- **if expressions / block expressions** (expression-oriented syntax, #109 RFC). + A mechanism for statement blocks to return values as expressions is a separate + matter and is not addressed here +- **C-style ternary symbol syntax** `cond ? a : b`. Only the Python-compatible + `a if c else b` +- **Implicit type promotion between branches** (`int` ↔ `float`, etc.). Both + branches must have the same type. Mixing them is a type error in sema +- **Constant folding or condition simplification of nesting**. Generate it + straightforwardly with cbranch + phi and delegate optimization to LLVM (and to + the future optimization level #55) -## 構文 +## Syntax ``` expr := ternary -ternary := or_expr [ "if" or_expr "else" ternary ] # 右結合 +ternary := or_expr [ "if" or_expr "else" ternary ] # right-associative ``` -- `if` / `else` は lexer に予約済み (`KW_IF` / `KW_ELSE`)。**lexer 変更は不要** -- `then` 側と `cond` 側は `or_expr` (三項より高い優先度) を読み、`else` 側は - 再び `ternary` を読むため右結合になる -- 三項演算子は `parse_expr` と `parse_or` の間の `parse_ternary` 層で処理する +- `if` / `else` are already reserved in the lexer (`KW_IF` / `KW_ELSE`). **No + lexer change is needed** +- The `then` side and the `cond` side read `or_expr` (higher precedence than the + ternary), while the `else` side reads `ternary` again, making it + right-associative +- The ternary operator is handled in the `parse_ternary` layer between + `parse_expr` and `parse_or` -### 文の `if` との非衝突 +### Non-collision with the statement `if` -文の `if` (`if cond:` で始まる複文) と式の `if` は次の理由で衝突しない: +The statement `if` (a compound statement beginning with `if cond:`) and the +expression `if` do not collide for the following reasons: -- 文の `if` は `parse_stmt` の **行頭ディスパッチ**でのみ分岐する -- 式の `if` は `parse_ternary` 内、つまり **式を読み始めた後**にのみ現れる +- The statement `if` branches only in the **line-head dispatch** of `parse_stmt` +- The expression `if` appears only inside `parse_ternary`, that is, **only after + expression parsing has begun** -したがって `x: int = 1 if c else 2` の `if` は式として、行頭の `if c:` は文と -して、それぞれ曖昧なく解釈される。 +Therefore the `if` in `x: int = 1 if c else 2` is interpreted as an expression, +and the line-head `if c:` as a statement, each without ambiguity. -### 使用例 +### Usage example ```rw def classify(n: int) -> int: - # ネストした条件式 (右結合) + # nested conditional expression (right-associative) return 1 if n > 0 else 0 if n == 0 else -1 def main() -> int: a: int = 10 b: int = 20 - larger: int = a if a > b else b # int ブランチ - label: string = "even" if a % 2 == 0 else "odd" # string ブランチ - ok: bool = true if larger == 20 else false # bool ブランチ + larger: int = a if a > b else b # int branch + label: string = "even" if a % 2 == 0 else "odd" # string branch + ok: bool = true if larger == 20 else false # bool branch print(larger) # 20 print(label) # even print(ok) # true @@ -96,72 +107,81 @@ def main() -> int: return 0 ``` -## 型付け (sema) +## Typing (sema) -`_infer_expr` の `IfExpr` 分岐で以下を検査する: +The `IfExpr` branch of `_infer_expr` checks the following: -1. `cond` を型推論し、`bool` でなければ型エラー +1. Infer the type of `cond`; if it is not `bool`, raise a type error (`conditional expression requires bool condition, found ...`) -2. `then` / `els` を型推論し、両者が**不一致**なら型エラー +2. Infer the types of `then` / `els`; if the two **do not match**, raise a type + error (`conditional expression branches must have the same type, found ... and ...`) -3. 一致した型を式全体の型として返す。`_check_expr` 経由で - `expr_types[id(expr)]` に登録され、irgen が phi の型決定に使う +3. Return the matched type as the type of the whole expression. Via + `_check_expr` it is registered in `expr_types[id(expr)]`, which irgen uses to + determine the phi type -## IR 生成 (irgen) +## IR generation (irgen) -`IfExpr` を、短絡 `and` / `or` と同形の **cbranch + phi** に lower する: +Lower `IfExpr` into **cbranch + phi** with the same shape as short-circuit `and` +/ `or`: ``` - cond_i1 = (cond != 0) ; bool は i8、!=0 で i1 化 + cond_i1 = (cond != 0) ; bool is i8, converted to i1 via !=0 br i1 cond_i1, label tern.then, label tern.else tern.then: - + br label tern.end tern.else: - + br label tern.end tern.end: %r = phi [ then_val, tern.then ], [ else_val, tern.else ] ``` -- phi の型は `llvm_type_of(sema.expr_types[id(expr)])` から引く。`and` / `or` - は結果が常に `i8` で固定だが、三項は int / float / bool / string いずれも - 取り得るため、sema が推論した型を使う点だけが異なる -- 選ばれたブランチのみが実行される (cbranch による短絡) +- The phi type is taken from `llvm_type_of(sema.expr_types[id(expr)])`. `and` / + `or` always produce a fixed `i8` result, but the ternary can take any of int / + float / bool / string, so the only difference is that it uses the type sema + inferred +- Only the selected branch is executed (short-circuiting via cbranch) -## 触るレイヤー +## Layers touched -| レイヤー | ファイル | 変更 | +| Layer | File | Change | |---|---|---| -| Lexer | `rwc/lexer.py` | `KW_IF` / `KW_ELSE` は予約済み。**無改修** | -| AST | `rwc/ast_nodes.py` | `IfExpr` ノード追加 (then, cond, els)。`Expr` Union に追加 | -| Parser | `rwc/parser.py` | `parse_ternary()` を `parse_expr` と `parse_or` の間に挿入。右結合 | -| Sema | `rwc/sema.py` | `_infer_expr` に `IfExpr` 分岐追加 (cond は bool、両ブランチ同型) | -| irgen | `rwc/irgen.py` | `_emit_expr` に `IfExpr` 分岐 + `_emit_if_expr` (cbranch + phi) | -| Runtime | `runtime/` | **無改修** | -| Desugar | `rwc/desugar.py` | **無改修** (desugar 不要) | -| Examples | `examples/ternary.rw` (+ `.expected`) | 新サンプル 1 | -| Tests | `tests/test_*.py` | parser / sema / irgen の unit test、e2e parametrize に `ternary` 追加 | - -`incremental-language-extensions` の「1 PR 4 層まで」(parser / sema / irgen + -例題) に収まる。 - -## 検証 - -- 例題 `examples/ternary.rw` を `rwc build` → 実行 → `.expected` と一致 -- positive (parser): `1 if c else 2` が `IfExpr` にパース、ネストが右結合 -- positive (sema): int / string / bool の同型ブランチが通る -- positive (irgen): 生成 IR に `br i1` と `phi` が出る -- negative (sema): `cond` が非 bool → 型エラー、ブランチ型不一致 → 型エラー -- negative (parser): `else` 欠落 → 構文エラー - -## リスクと対処 - -- **文 if と式 if の取り違え**: 文の `if` は行頭ディスパッチ、式の `if` は - `parse_ternary` 内でのみ現れるため衝突しない。parser テストで既存の `if` - 文が壊れていないことも担保する -- **phi の型ミスマッチ**: phi の型は固定値ではなく sema が推論した型 - (`expr_types`) から引く。両ブランチ同型は sema が保証済みなので、then 側の - 値と else 側の値は同じ LLVM 型になる -- **「ついでに」誘惑**: if 式 RFC (#109) や定数畳み込みには手を出さない - (Non-Goals に明記) +| Lexer | `rwc/lexer.py` | `KW_IF` / `KW_ELSE` are already reserved. **Unchanged** | +| AST | `rwc/ast_nodes.py` | Add `IfExpr` node (then, cond, els). Add to the `Expr` Union | +| Parser | `rwc/parser.py` | Insert `parse_ternary()` between `parse_expr` and `parse_or`. Right-associative | +| Sema | `rwc/sema.py` | Add an `IfExpr` branch to `_infer_expr` (cond is bool, both branches same type) | +| irgen | `rwc/irgen.py` | Add an `IfExpr` branch to `_emit_expr` + `_emit_if_expr` (cbranch + phi) | +| Runtime | `runtime/` | **Unchanged** | +| Desugar | `rwc/desugar.py` | **Unchanged** (no desugar needed) | +| Examples | `examples/ternary.rw` (+ `.expected`) | 1 new example | +| Tests | `tests/test_*.py` | Unit tests for parser / sema / irgen; add `ternary` to the e2e parametrize | + +This stays within the "up to 4 layers per PR" of +`incremental-language-extensions` (parser / sema / irgen + examples). + +## Verification + +- Build the example `examples/ternary.rw` with `rwc build`, run it, and check it + matches `.expected` +- positive (parser): `1 if c else 2` parses into an `IfExpr`, nesting is + right-associative +- positive (sema): same-type branches of int / string / bool pass +- positive (irgen): `br i1` and `phi` appear in the generated IR +- negative (sema): non-bool `cond` → type error, mismatched branch types → type + error +- negative (parser): missing `else` → syntax error + +## Risks and mitigations + +- **Confusing the statement if with the expression if**: the statement `if` is + line-head dispatch, and the expression `if` appears only inside + `parse_ternary`, so they do not collide. Parser tests also guarantee that the + existing `if` statement is not broken +- **phi type mismatch**: the phi type is taken not from a fixed value but from + the type sema inferred (`expr_types`). Since sema guarantees that both branches + have the same type, the then-side value and the else-side value have the same + LLVM type +- **The "while we're at it" temptation**: do not touch the if-expression RFC + (#109) or constant folding (explicitly stated in Non-Goals) diff --git a/docs/specs/15-file-io.md b/docs/specs/15-file-io.md index 1875db5..f2497f5 100644 --- a/docs/specs/15-file-io.md +++ b/docs/specs/15-file-io.md @@ -1,26 +1,27 @@ -# rw ファイル I/O と fd 汎用 `read` / `write` / `close` +# rw file I/O and fd-generic `read` / `write` / `close` ## Context -rw はコンパイラ・ランタイム・サンプル・テストが一体で育つ小さな言語で、TCP -ソケット ([[12-netpoller-tcp]]) を `tcp_listen` / `tcp_accept` / `tcp_read` / -`tcp_write` / `tcp_close` という組み込みで提供してきた。一方、issue #33 -(stdlib: ファイル I/O) のとおり、ファイルを開いて読み書きする手段がまだない。 +rw is a small language whose compiler, runtime, examples, and tests grow +together. It has provided TCP sockets ([[12-netpoller-tcp]]) via the built-ins +`tcp_listen` / `tcp_accept` / `tcp_read` / `tcp_write` / `tcp_close`. However, as +noted in issue #33 (stdlib: file I/O), there is still no way to open a file and +read/write it. -ここで TCP の組み込みを見直すと、`tcp_read` / `tcp_write` / `tcp_close` は実は -**ソケット専用ではない操作**である。Unix では `read(2)` / `write(2)` / -`close(2)` はファイル・ソケット・パイプ・標準入出力など任意の fd に使える汎用 -システムコールであり、ソケット専用なのは `tcp_listen` / `tcp_accept` -(と接続の確立) だけだ。 +Revisiting the TCP built-ins here, `tcp_read` / `tcp_write` / `tcp_close` are in +fact **operations that are not socket-specific**. On Unix, `read(2)` / `write(2)` +/ `close(2)` are generic system calls usable on any fd such as files, sockets, +pipes, and standard I/O; the only socket-specific parts are `tcp_listen` / +`tcp_accept` (and establishing the connection). -そこで本サブプロジェクトは、**`tcp_read` / `tcp_write` / `tcp_close` を廃止し、 -fd 汎用の `read` / `write` / `close` に一本化**したうえで、ファイルを開く -`file_open` を追加する。これにより書く側は「開く操作はソース別、読み書き閉じる -操作は共通」という Unix の意味論で、TCP もファイルも同じ `read` / `write` / -`close` で書けるようになる。 +Therefore this sub-project **removes `tcp_read` / `tcp_write` / `tcp_close` and +unifies them into the fd-generic `read` / `write` / `close`**, and adds +`file_open` to open a file. With this, the writer works in the Unix semantics of +"opening is source-specific, reading/writing/closing is common", so both TCP and +files can be written with the same `read` / `write` / `close`. ``` -開く(ソース別) 読み書き閉じる(共通) +open (source-specific) read/write/close (common) ───────────────── ────────────────────── tcp_listen(port) -> fd tcp_accept(lfd) -> fd ──┐ @@ -31,124 +32,134 @@ file_open(path, mode) -> fd ─┤──→ read(fd, n) -> Bytes ## Goals -- fd 汎用の組み込みを導入する: - - `read(fd: int, max: int) -> Bytes` — fd から最大 max バイト読む - - `write(fd: int, b: Bytes) -> int` — fd へ書く。書けたバイト数を返す - - `close(fd: int) -> int` — fd を閉じる。0 成功 / 負失敗 -- ファイルを開く組み込みを追加する: - - `file_open(path: string, mode: string) -> int` — `"r"` / `"w"` / `"a"` - を `open(2)` フラグに変換。失敗時は負の fd -- runtime 内部で `read(2)` / `write(2)` を使い、`EAGAIN` のとき fiber 上なら - netpoller に park する。**ソケット fd (ノンブロッキング) は park され、 - ファイル fd (EAGAIN を出さない) はそのまま同期 read になる** — 種別判定の - 分岐コードを書かずに両対応する (本設計の核心) -- `tcp_listen` / `tcp_accept` は据え置き (ソケット固有のため) -- 既存の TCP サンプル・テスト・spec を `read` / `write` / `close` に書き換える - (破壊的・完全統一) +- Introduce fd-generic built-ins: + - `read(fd: int, max: int) -> Bytes` — read at most max bytes from fd + - `write(fd: int, b: Bytes) -> int` — write to fd. Returns the number of bytes + written + - `close(fd: int) -> int` — close fd. 0 on success / negative on failure +- Add a built-in to open a file: + - `file_open(path: string, mode: string) -> int` — convert `"r"` / `"w"` / + `"a"` into `open(2)` flags. On failure, a negative fd +- Internally in the runtime, use `read(2)` / `write(2)`, and on `EAGAIN` park on + the netpoller if on a fiber. **A socket fd (non-blocking) is parked, and a file + fd (which does not emit EAGAIN) becomes a synchronous read as-is** — handling + both without writing kind-detection branch code (the core of this design) +- `tcp_listen` / `tcp_accept` are kept as-is (they are socket-specific) +- Rewrite existing TCP examples, tests, and specs to `read` / `write` / `close` + (a breaking, full unification) ## Non-Goals -- ディレクトリ操作・path 操作 (`mkdir` / `readdir` / path join 等) — #43 で別途 -- `seek` / `tell` / `truncate` / `stat` などのファイル位置・メタ操作 -- バッファリング (毎回 syscall を発行する) -- テキスト / バイナリの区別 — `read` は常に `Bytes` を返し、文字列化は既存の - `str_from_bytes` に委ねる -- ファイルパーミッションの指定 — `open` で作成するファイルは固定 `0644` -- `tcp_listen` / `tcp_accept` のリネーム (ソケット固有のまま残す) +- Directory operations / path operations (`mkdir` / `readdir` / path join, etc.) + — separately in #43 +- `seek` / `tell` / `truncate` / `stat` and other file-position/metadata + operations +- Buffering (issue a syscall every time) +- Text / binary distinction — `read` always returns `Bytes`, and stringification + is delegated to the existing `str_from_bytes` +- Specifying file permissions — files created by `open` are fixed at `0644` +- Renaming `tcp_listen` / `tcp_accept` (they remain socket-specific) -## 組み込み関数 (言語側) +## Built-in functions (language side) -| 関数 | シグネチャ | 説明 | +| Function | Signature | Description | |---|---|---| -| `file_open` | `(path: string, mode: string) -> int` | `"r"`→`O_RDONLY`, `"w"`→`O_WRONLY\|O_CREAT\|O_TRUNC`, `"a"`→`O_WRONLY\|O_CREAT\|O_APPEND`。不正 mode / open 失敗は負の fd | -| `read` | `(fd: int, max: int) -> Bytes` | 最大 max バイト読む。EOF・エラーは len=0 の Bytes | -| `write` | `(fd: int, b: Bytes) -> int` | 書けたバイト数。エラーは負 | -| `close` | `(fd: int) -> int` | 0 成功 / 負失敗 | - -`read` / `write` の Bytes ABI は旧 `tcp_read` / `tcp_write` と同一 -(`{i64 len, i8* ptr}` の sret / 値渡し) なので、irgen の emit ロジックを流用 -できる。失敗時に負値を返す慣習は既存の `tcp_*` に揃える。 - -## runtime (C) の設計 - -`runtime/runtime.c` (または新規 `runtime/io.c`) に fd 汎用ヘルパを置く: - -- `rw_read(rw_str *out, int64_t fd, int64_t max)` — `recv` ではなく - **`read(2)`**。戻り値で分岐: - - `n > 0`: `out` に len=n / ptr=buf - - `n == 0`: EOF。len=0 / ptr=NULL - - `n < 0` かつ `errno == EAGAIN/EWOULDBLOCK`: fiber 上なら - `rw_net_park_read(fd)` して継続、そうでなければ len=0 で返す - - その他のエラー: len=0 で返す -- `rw_write(int64_t fd, rw_str b)` — `send` ではなく **`write(2)`**。 - `EAGAIN` のとき fiber 上なら `rw_net_park_write(fd)` して継続。書けた - バイト数を返す +| `file_open` | `(path: string, mode: string) -> int` | `"r"`→`O_RDONLY`, `"w"`→`O_WRONLY\|O_CREAT\|O_TRUNC`, `"a"`→`O_WRONLY\|O_CREAT\|O_APPEND`. Invalid mode / open failure is a negative fd | +| `read` | `(fd: int, max: int) -> Bytes` | Read at most max bytes. EOF/error is a Bytes with len=0 | +| `write` | `(fd: int, b: Bytes) -> int` | Number of bytes written. Error is negative | +| `close` | `(fd: int) -> int` | 0 on success / negative on failure | + +The Bytes ABI of `read` / `write` is identical to the old `tcp_read` / +`tcp_write` (`{i64 len, i8* ptr}` passed by sret / by value), so the emit logic +of irgen can be reused. The convention of returning a negative value on failure +matches the existing `tcp_*`. + +## Runtime (C) design + +Place fd-generic helpers in `runtime/runtime.c` (or a new `runtime/io.c`): + +- `rw_read(rw_str *out, int64_t fd, int64_t max)` — **`read(2)`**, not `recv`. + Branch on the return value: + - `n > 0`: len=n / ptr=buf into `out` + - `n == 0`: EOF. len=0 / ptr=NULL + - `n < 0` and `errno == EAGAIN/EWOULDBLOCK`: if on a fiber, + `rw_net_park_read(fd)` and continue; otherwise return with len=0 + - Other errors: return with len=0 +- `rw_write(int64_t fd, rw_str b)` — **`write(2)`**, not `send`. On `EAGAIN`, if + on a fiber, `rw_net_park_write(fd)` and continue. Returns the number of bytes + written - `rw_close(int64_t fd)` — `close(2)` -- `rw_file_open(rw_str path, rw_str mode)` — path を NUL 終端にコピーし、 - mode 文字列を `O_*` フラグに変換して `open(path, flags, 0644)`。負の fd で - 失敗を表す +- `rw_file_open(rw_str path, rw_str mode)` — copy path into a NUL-terminated + buffer, convert the mode string into `O_*` flags, and `open(path, flags, + 0644)`. A negative fd indicates failure -### なぜ分岐なしで両対応できるか +### Why both are handled without branching -ソケット fd は `tcp_accept` 等で **ノンブロッキング**に設定される (既存の -netpoller 連携のため)。ノンブロッキングソケットは読めるデータがないと -`EAGAIN` を返すので、上記ロジックは netpoller に park して fiber を退避する。 -一方、**正規ファイルの fd は `read(2)` で `EAGAIN` を返さず**、データが -揃うまでカーネルがブロックして完了する (ディスク I/O はノンブロッキングに -ならない)。したがって同じ `rw_read` のコードが、ソケットでは park、ファイル -では同期 read として正しく振る舞う。fd 種別を `fstat` で判定する分岐は不要。 +Socket fds are set to **non-blocking** by `tcp_accept` etc. (for the existing +netpoller integration). A non-blocking socket returns `EAGAIN` when there is no +data to read, so the above logic parks on the netpoller and yields the fiber. On +the other hand, **a regular file fd does not return `EAGAIN` on `read(2)`**; the +kernel blocks until the data is ready and then completes (disk I/O cannot be +made non-blocking). Therefore the same `rw_read` code behaves correctly as a +park for sockets and as a synchronous read for files. There is no branch that +determines the fd kind via `fstat`. -既存 `tcp.c` の `rw_tcp_read` / `rw_tcp_write` / `rw_tcp_close` は削除し、 -中身を `rw_read` / `rw_write` / `rw_close` に統合・委譲する。 +Delete the existing `rw_tcp_read` / `rw_tcp_write` / `rw_tcp_close` in `tcp.c`, +and consolidate/delegate their bodies into `rw_read` / `rw_write` / `rw_close`. -## 触るレイヤー +## Layers touched -| レイヤー | ファイル | 変更 | +| Layer | File | Change | |---|---|---| -| Lexer | `rwc/lexer.py` | **無改修** (すべて通常の関数呼び出し) | -| Parser | `rwc/parser.py` | **無改修** | -| AST | `rwc/ast_nodes.py` | **無改修** (`Call` で表現) | -| Sema | `rwc/sema.py` | `file_open` / `read` / `write` / `close` を組み込みに追加。`tcp_read` / `tcp_write` / `tcp_close` の分岐を削除 (spawn 拒否リストと `_check_call` の 2 箇所)。`tcp_listen` / `tcp_accept` は据え置き | -| irgen | `rwc/irgen.py` | `rw_read` / `rw_write` / `rw_close` / `rw_file_open` を declare。`_emit_call` の `tcp_read` 等を `read` 等に置換 (Bytes ABI は流用) | -| Runtime | `runtime/runtime.c` ほか, `runtime/net/tcp.c` | `rw_read` / `rw_write` / `rw_close` / `rw_file_open` を実装。`rw_tcp_read` / `rw_tcp_write` / `rw_tcp_close` を削除・委譲 | -| Examples | `examples/file_io.rw` (+ `.expected`) 新規。`tcp_echo.rw` / `tcp_chat.rw` を書き換え | round-trip サンプル + 既存 TCP を `read`/`write`/`close` に | -| Tests | `tests/test_e2e.py` / `test_e2e_tcp.py` / `test_sema.py` / `test_irgen.py` | `file_io` を parametrize に追加、TCP テストを書き換え、sema/irgen の unit test 追加 | - -`incremental-language-extensions` の「1 PR 4 層まで」に対し、本 PR は sema / -irgen / runtime + 例題が中心。lexer / parser / AST は無改修なので層数は収まる。 -ただし TCP の破壊的書き換えを含むため commit を分けて影響範囲を明示する。 - -## 検証 +| Lexer | `rwc/lexer.py` | **Unchanged** (all are ordinary function calls) | +| Parser | `rwc/parser.py` | **Unchanged** | +| AST | `rwc/ast_nodes.py` | **Unchanged** (expressed as `Call`) | +| Sema | `rwc/sema.py` | Add `file_open` / `read` / `write` / `close` to the built-ins. Remove the `tcp_read` / `tcp_write` / `tcp_close` branches (in 2 places: the spawn rejection list and `_check_call`). `tcp_listen` / `tcp_accept` kept as-is | +| irgen | `rwc/irgen.py` | Declare `rw_read` / `rw_write` / `rw_close` / `rw_file_open`. Replace `tcp_read` etc. in `_emit_call` with `read` etc. (Bytes ABI reused) | +| Runtime | `runtime/runtime.c` and others, `runtime/net/tcp.c` | Implement `rw_read` / `rw_write` / `rw_close` / `rw_file_open`. Delete/delegate `rw_tcp_read` / `rw_tcp_write` / `rw_tcp_close` | +| Examples | `examples/file_io.rw` (+ `.expected`) new. Rewrite `tcp_echo.rw` / `tcp_chat.rw` | round-trip example + existing TCP switched to `read`/`write`/`close` | +| Tests | `tests/test_e2e.py` / `test_e2e_tcp.py` / `test_sema.py` / `test_irgen.py` | Add `file_io` to parametrize, rewrite TCP tests, add sema/irgen unit tests | + +Against the "up to 4 layers per PR" of `incremental-language-extensions`, this PR +centers on sema / irgen / runtime + examples. Since lexer / parser / AST are +unchanged, the layer count stays within bounds. However, because it includes a +breaking rewrite of TCP, the commits are split to make the scope of impact +explicit. + +## Verification ```sh make -C runtime -uv run pytest -v # 全緑 (書き換え後の TCP テスト含む) -uv run rwc run examples/file_io.rw # round-trip 出力が .expected と一致 -uv run rwc run examples/tcp_echo.rw # 書き換え後も TCP echo が動く +uv run pytest -v # all green (including rewritten TCP tests) +uv run rwc run examples/file_io.rw # round-trip output matches .expected +uv run rwc run examples/tcp_echo.rw # TCP echo works after the rewrite too ``` -- e2e (round-trip): `file_open(path, "w")` → `write` → `close` → - `file_open(path, "r")` → `read` → `print` の自己完結サンプルで書き / 読み - 両パスを踏む -- unit (sema): `file_open` の引数型・個数、`read`/`write`/`close` の型 - (`read` が Bytes を返す、`write` の第 2 引数が Bytes 等)、不正 mode は - 実行時 (負 fd) なので sema ではなく e2e/手動で確認 -- unit (irgen): 生成 IR に `rw_read` / `rw_write` / `rw_file_open` の呼び出し - が出る -- TCP リグレッション: `test_e2e_tcp.py` を `read`/`write`/`close` で通す - -## リスクと対処 - -- **TCP の破壊的変更**: `tcp_read`/`tcp_write`/`tcp_close` を使う既存サンプル・ - テスト・spec をすべて洗い出して書き換える (grep で網羅)。commit を「runtime - 統合」「sema/irgen 切替」「サンプル/テスト書き換え」「file_open 追加」に分け、 - TCP リグレッションを e2e で担保する -- **ファイル fd を netpoller に park してしまう懸念**: 正規ファイルは `EAGAIN` - を返さないため park 経路に入らない。仮に特殊 fd が EAGAIN を返しても fiber - 外なら同期フォールバックする -- **path の NUL 終端**: rw の string は `{len, ptr}` で NUL 終端保証がないため、 - `rw_file_open` で path をコピーして NUL 終端を付ける -- **mode 文字列の不正値**: `"r"`/`"w"`/`"a"` 以外は負の fd を返す (trap しない)。 - 呼び出し側が fd < 0 を判定する -- **「ついでに」誘惑**: seek / dir 操作 / バッファリングには手を出さない - (Non-Goals)。`tcp_listen`/`tcp_accept` のリネームもしない +- e2e (round-trip): a self-contained example of `file_open(path, "w")` → + `write` → `close` → `file_open(path, "r")` → `read` → `print` that exercises + both the write and read paths +- unit (sema): argument types/counts of `file_open`, types of + `read`/`write`/`close` (`read` returns Bytes, the 2nd argument of `write` is + Bytes, etc.); an invalid mode is a runtime matter (negative fd), so it is + confirmed in e2e/manually rather than in sema +- unit (irgen): calls to `rw_read` / `rw_write` / `rw_file_open` appear in the + generated IR +- TCP regression: pass `test_e2e_tcp.py` with `read`/`write`/`close` + +## Risks and mitigations + +- **Breaking change to TCP**: identify and rewrite all existing examples, tests, + and specs that use `tcp_read`/`tcp_write`/`tcp_close` (comprehensively via + grep). Split the commits into "runtime consolidation", "sema/irgen switch", + "example/test rewrite", and "add file_open", and guarantee TCP regression in + e2e +- **Concern about parking a file fd on the netpoller**: regular files do not + return `EAGAIN`, so they do not enter the park path. Even if a special fd + returns EAGAIN, it falls back to synchronous outside a fiber +- **NUL termination of path**: since rw's string is `{len, ptr}` with no NUL + termination guarantee, `rw_file_open` copies the path and appends NUL + termination +- **Invalid mode string value**: anything other than `"r"`/`"w"`/`"a"` returns a + negative fd (no trap). The caller checks for fd < 0 +- **The "while we're at it" temptation**: do not touch seek / dir operations / + buffering (Non-Goals). Do not rename `tcp_listen`/`tcp_accept` either diff --git a/docs/specs/16-async-file-io.md b/docs/specs/16-async-file-io.md index 98f1ca1..c3c8d6f 100644 --- a/docs/specs/16-async-file-io.md +++ b/docs/specs/16-async-file-io.md @@ -1,217 +1,249 @@ -# rw 非同期ファイル I/O(抽象 + thread pool バックエンド) +# rw async file I/O (abstraction + thread pool backend) ## Context -rw はファイル I/O ([[15-file-io]]) を fd 汎用の `read` / `write` / `close` で -提供している。実装は `runtime/io.c` の `rw_read` / `rw_write` で、`EAGAIN` のとき -fiber を netpoller に park する completion-ish なロジックを持つ。 +rw provides file I/O ([[15-file-io]]) via the fd-generic `read` / `write` / +`close`. The implementation is `rw_read` / `rw_write` in `runtime/io.c`, which +has completion-ish logic that parks the fiber on the netpoller on `EAGAIN`. -しかしこのロジックは **ノンブロッキングソケット**にしか効かない。正規ファイルの -fd は `read(2)` / `write(2)` で `EAGAIN` を返さず、データが揃うまでカーネルが -ブロックして完了する。その間、その `read` を呼んだ fiber が乗っている -**ワーカースレッド M が 1 本まるごとブロックされる**。fiber を 100k 走らせる -ランタイムにとって、ファイル read 1 つでワーカーが止まるのは弱点である。 +However, this logic only works for **non-blocking sockets**. A regular file fd +does not return `EAGAIN` on `read(2)` / `write(2)`; the kernel blocks until the +data is ready and then completes. During that time, the **worker thread M on +which the fiber that called that `read` is running is blocked entirely**. For a +runtime that runs 100k fibers, having a worker stop for a single file read is a +weakness. -このサブプロジェクトは、ファイル I/O を「別スレッドにオフロードして fiber を -park し、完了したら起こす」非同期モデルにする。fiber はファイル read 中も -ワーカーを明け渡し、他の fiber が同じワーカーで走り続けられる。 +This sub-project turns file I/O into an async model that "offloads to another +thread, parks the fiber, and wakes it when complete". The fiber yields the worker +even during a file read, so other fibers can keep running on the same worker. -### ロードマップ上の位置 +### Position on the roadmap -ユーザーの最終目標は「ファイル I/O で io_uring を使えるようにする」こと。 -io_uring は **completion モデル**(read 操作をカーネルに submit し、完了通知を -受ける)で、現在の readiness ベース netpoller とは根本的に異なる。そこで: +The user's ultimate goal is "to be able to use io_uring for file I/O". io_uring +is a **completion model** (submit a read operation to the kernel and receive a +completion notification), which is fundamentally different from the current +readiness-based netpoller. Therefore: -1. **このサブプロジェクト (PR 1)**: 非同期ファイル I/O の**抽象インターフェース** - を定義し、最初の実体を **thread pool バックエンド**(全 OS 共通)で実装する。 -2. **次のサブプロジェクト (PR 2)**: io_uring を **Linux でのより高速な実体**と - して抽象の下に差し替え追加する。macOS は thread pool のまま。 +1. **This sub-project (PR 1)**: Define the **abstract interface** for async file + I/O and implement the first concrete instance with a **thread pool backend** + (common to all OSes). +2. **Next sub-project (PR 2)**: Add io_uring as a **faster concrete instance on + Linux** under the abstraction, swapping it in. macOS stays on the thread pool. -抽象を先に置くことで、両 OS で「ファイル I/O が fiber をブロックしない」同じ -挙動を先に達成し、io_uring 導入時には完成済みの park/完了プロトコルへ実体を -差し込むだけにできる。 +By putting the abstraction in place first, we achieve the same "file I/O does not +block the fiber" behavior on both OSes first, and when io_uring is introduced we +only need to plug the concrete instance into the already-completed park/completion +protocol. ## Goals -- 非同期ファイル I/O の抽象 `runtime/aio.h` / `runtime/aio.c` を導入: +- Introduce the async file I/O abstraction `runtime/aio.h` / `runtime/aio.c`: - `void rw_aio_read(rw_str *out, int64_t fd, int64_t max)` - `int64_t rw_aio_write(int64_t fd, rw_str b)` -- 最初のバックエンドを **thread pool**(固定数の pthread + タスクキュー)で実装。 - worker が `read(2)` / `write(2)` を実行し、完了後に呼び出し fiber を起こす。 -- `runtime/io.c` の `rw_read` / `rw_write` で `fstat(fd)` により fd 種別を判定し: - - **正規ファイル (`S_ISREG`)** → `rw_aio_read` / `rw_aio_write`(thread pool) - - **それ以外(ソケット等)** → 従来の netpoller 経路(`EAGAIN`→park、無改修) -- fiber を park して完了で起こすのに、既存スケジューラの - `rw_sched_park_current()` / `rw_sched_enqueue_ready()` をそのまま使う。 -- `rw_init` / `rw_shutdown` で thread pool のライフサイクルを管理。 -- 呼び出し側(rw コード・sema・irgen)は**一切変更しない**。`read`/`write` が - fd 種別に応じて透過的に最適経路を選ぶ。 +- Implement the first backend with a **thread pool** (a fixed number of pthreads + + a task queue). Workers execute `read(2)` / `write(2)` and wake the calling + fiber after completion. +- In `rw_read` / `rw_write` of `runtime/io.c`, determine the fd kind via + `fstat(fd)`: + - **Regular file (`S_ISREG`)** → `rw_aio_read` / `rw_aio_write` (thread pool) + - **Otherwise (sockets, etc.)** → the traditional netpoller path + (`EAGAIN`→park, unchanged) +- To park the fiber and wake it on completion, use the existing scheduler's + `rw_sched_park_current()` / `rw_sched_enqueue_ready()` as-is. +- Manage the thread pool lifecycle in `rw_init` / `rw_shutdown`. +- The caller side (rw code, sema, irgen) is **not changed at all**. `read`/`write` + transparently choose the optimal path depending on the fd kind. ## Non-Goals -- **io_uring 本体の実装** — PR 2 で行う。本 PR は抽象 + thread pool のみ。 -- ファイル I/O 以外(ソケット)の経路変更 — netpoller はそのまま。 -- `read` 以外のファイル操作(seek/stat/truncate 等)の非同期化。 -- fixed buffer / バッチ submit / ゼロコピーなど io_uring 固有の最適化。 -- thread pool のサイズ自動調整・ワークスティーリング(固定数で十分)。 -- メインスレッド(fiber 外)での非同期化 — park できないので同期 `read(2)` - にフォールバックする。 +- **Implementing io_uring itself** — done in PR 2. This PR is abstraction + + thread pool only. +- Changing the path for anything other than file I/O (sockets) — the netpoller is + unchanged. +- Making file operations other than `read` (seek/stat/truncate, etc.) + asynchronous. +- io_uring-specific optimizations such as fixed buffers / batch submit / + zero-copy. +- Auto-tuning the thread pool size / work stealing (a fixed number is enough). +- Async on the main thread (outside a fiber) — since it cannot park, it falls + back to synchronous `read(2)`. -## アーキテクチャ +## Architecture ``` -rw_read(fd, max) ──┐ io.c で fstat(fd) 判定 +rw_read(fd, max) ──┐ fstat(fd) decision in io.c rw_write(fd, b) ──┤ - ├── S_ISREG(ファイル) ──→ rw_aio_read / rw_aio_write (aio.c) - │ │ fiber 上なら: - │ │ task を submit → rw_sched_park_current() - │ │ worker が read(2) → 結果格納 + ├── S_ISREG (file) ──→ rw_aio_read / rw_aio_write (aio.c) + │ │ if on a fiber: + │ │ submit task → rw_sched_park_current() + │ │ worker does read(2) → store result │ │ → rw_sched_enqueue_ready(handle) - │ │ fiber 外なら: 同期 read(2) - └── それ以外(socket 等) ──→ 従来の EAGAIN→rw_net_park_*(無改修) + │ │ if outside a fiber: synchronous read(2) + └── otherwise (socket, etc.) ──→ traditional EAGAIN→rw_net_park_* (unchanged) ``` -### コンポーネント +### Components -**`runtime/aio.h`** — 公開インターフェース(プロトタイプは runtime.h に集約せず -aio.h に置く。net/tcp.h と異なり aio は独立した抽象なので自前ヘッダを持つ): +**`runtime/aio.h`** — the public interface (prototypes are placed in aio.h rather +than being consolidated into runtime.h; unlike net/tcp.h, aio is an independent +abstraction, so it has its own header): - `void rw_aio_init(void);` / `void rw_aio_shutdown(void);` - `void rw_aio_read(rw_str *out, int64_t fd, int64_t max);` - `int64_t rw_aio_write(int64_t fd, rw_str b);` -**`runtime/aio.c`** — thread pool バックエンド: -- 固定数(既定 4、`RW_AIO_THREADS` 環境変数で上書き可)の worker pthread。 -- ロック付きタスクキュー(`pthread_mutex_t` + `pthread_cond_t`)。 -- タスク構造体: 操作種別(READ/WRITE)、fd、バッファ/サイズ、結果格納先、 - 待っている `rw_fiber_handle *`。 -- `rw_aio_read` / `rw_aio_write` の流れ(fiber 上): - 1. タスクを構築(結果を書き戻すスロットと自分の fiber handle を持たせる) - 2. キューに push して condvar で worker を起こす - 3. `rw_sched_park_current()` で park(WAITING になりワーカーを明け渡す) - 4. worker が `read(2)`/`write(2)` を実行 → 結果をタスクのスロットに格納 → - `rw_sched_enqueue_ready(task->handle)` で fiber を ready に戻す - 5. park から戻った fiber がタスクのスロットから結果を読み取り、呼び出し元へ -- fiber 外(`rw_sched_current_fiber() == NULL`)では park できないので、その場で - 同期 `read(2)`/`write(2)` を実行して返す(netpoller の park 同様の安全策)。 - -**`runtime/io.c`**(変更): -- `rw_read` / `rw_write` の冒頭で `fstat(fd)` し、`S_ISREG` なら `rw_aio_*` に委譲。 - それ以外は既存ロジック(ソケット向け EAGAIN+netpoller park)をそのまま実行。 -- `fstat` 失敗時は安全側で既存ロジックにフォールバック。 - -**`runtime/runtime.c`**(変更): -- `rw_init` で `rw_netpoller_init()` の隣に `rw_aio_init()`。 -- `rw_shutdown` で `rw_aio_shutdown()`(worker を止め join)。 - -### データフローと所有権 - -- `rw_aio_read` のバッファ: 既存 `rw_read` と同じく `out->ptr` に malloc した - バッファの所有権を呼び出し元へ渡す(n>0 のとき)。malloc は aio.c 側で行い、 - worker が read 後に len を設定する。EOF/エラーは len=0 / ptr=NULL。 -- タスク構造体は呼び出し fiber のスタック上に確保し、ポインタをキューに積む。 - fiber は完了まで park しているのでスタックは生存している(park 中も fiber の - スタックは保持される)。worker は完了時にスロットへ書き、handle を enqueue。 -- 結果の可視性: worker のスロット書き込みは `rw_sched_enqueue_ready` の前に行い、 - enqueue/park 復帰の happens-before(既存 netpoller と同じ acquire/release - 規約)に乗せる。 - -## 並行性の正しさ - -このプロトコルは既存の 2 つの「park して別スレッドが起こす」経路と**完全に -同型**にする。実コードを確認済み(`runtime/fiber/sched.c`, -`runtime/net/netpoller*.c`): - -- **netpoller** (`rw_net_park_read`): `rw_netpoller_register_read(fd, h)` で - kqueue/epoll に登録 → `rw_sched_park_current()`。poll スレッドがイベント時に - `rw_sched_enqueue_ready(h)`。 -- **join** (`park_on`): `wait_lock` 内で wait list に自分を入れ `state=WAITING` - → 解放 → `rw_fiber_swap`。完了側 `finalize_fiber` が `state=DONE`(release) → - `wait_lock` で list を一括 take → 各 waiter を `enqueue_ready`。 -- **aio (本 PR)**: handle を確定 → タスクを submit → `rw_sched_park_current()`。 - worker が `read(2)`/`write(2)` 完了後、結果をスロットに格納 → - `rw_sched_enqueue_ready(task->handle)`。 - -`rw_sched_enqueue_ready` は「別スレッドから安全に呼べる」と sched.h に明記され、 -`enqueue_ready` は `state=READY` にして ready キューへ push する -(`sched.c:206`)。`rw_sched_park_current` は `state=WAITING` にして -`rw_fiber_swap` でスケジューラへ戻る (`sched.c:339`)。worker_main は swap から -戻った fiber を **`state==RUNNING` のときだけ** ready へ戻し、WAITING/DONE は -戻さない (`sched.c:373`)。これが鉄則「WAITING の fiber は ready queue に居て -はならない」を担保する。 - -### ctx 公開タイミングの競合について - -stackful coroutine スケジューラには「`rw_sched_park_current` 内の -`rw_fiber_swap`(ctx 保存)が完了する前に、別スレッドが `enqueue_ready` → -別ワーカーが steal → half-written ctx を resume して PC=0 SEGV」という古典的 -競合がある([[stackful-coroutine-scheduling]])。 - -本 PR の aio はこの競合を**新規に持ち込まない**。理由: 上記のとおり netpoller / -join と**同一のプロトコル**(handle 確定 → 登録/submit → park、相手スレッドは -park 後に enqueue)を使い、独自の publish-before-save パターンを作らないから。 -worker が park より先に enqueue する可能性の有無・その安全性は、netpoller の -register→park(登録直後に fd が ready なら poll スレッドが即 enqueue しうる)と -**完全に同じ条件**であり、本 PR が既存コードの安全性水準を上下させることはない。 - -したがって本 PR の責務は「netpoller と寸分違わぬ順序でプロトコルを書く」こと。 -もし将来この同型競合が顕在化するなら、それは netpoller・join・aio に共通の -スケジューラ層の課題であり、`park.c` の `wait_lock` 相当を park_current に -組み込む別タスクとして 3 経路まとめて対処する(本 PR のスコープ外)。 - -### 検証で competition を炙り出す - -[[stackful-coroutine-scheduling]] の検証手順に従い、複数 fiber が並行で -ファイル I/O する e2e を **`RW_WORKERS=1` と `RW_WORKERS≥2`(steal が起きる)の -両方**で回し、SEGV/ハングなく結果が揃うことを確認する。これは aio 経路が -既存スケジューラと正しく協調できている証拠になる。 - -## 触るレイヤー - -| レイヤー | ファイル | 変更 | +**`runtime/aio.c`** — the thread pool backend: +- A fixed number (default 4, overridable via the `RW_AIO_THREADS` environment + variable) of worker pthreads. +- A locked task queue (`pthread_mutex_t` + `pthread_cond_t`). +- Task struct: operation kind (READ/WRITE), fd, buffer/size, result storage + slot, and the waiting `rw_fiber_handle *`. +- Flow of `rw_aio_read` / `rw_aio_write` (on a fiber): + 1. Build the task (giving it a slot to write the result back and its own fiber + handle) + 2. push to the queue and wake a worker via the condvar + 3. `rw_sched_park_current()` to park (becomes WAITING and yields the worker) + 4. The worker executes `read(2)`/`write(2)` → stores the result in the task's + slot → `rw_sched_enqueue_ready(task->handle)` to return the fiber to ready + 5. The fiber, returning from park, reads the result from the task's slot and + returns it to the caller +- Outside a fiber (`rw_sched_current_fiber() == NULL`), it cannot park, so it + executes a synchronous `read(2)`/`write(2)` on the spot and returns (the same + safety measure as the netpoller's park). + +**`runtime/io.c`** (changed): +- At the start of `rw_read` / `rw_write`, `fstat(fd)`, and if `S_ISREG` delegate + to `rw_aio_*`. Otherwise execute the existing logic (socket-oriented + EAGAIN+netpoller park) as-is. +- On `fstat` failure, fall back to the existing logic on the safe side. + +**`runtime/runtime.c`** (changed): +- `rw_aio_init()` next to `rw_netpoller_init()` in `rw_init`. +- `rw_aio_shutdown()` in `rw_shutdown` (stop and join the workers). + +### Data flow and ownership + +- Buffer of `rw_aio_read`: as with the existing `rw_read`, transfer ownership of + the malloc'd buffer to the caller via `out->ptr` (when n>0). The malloc is done + on the aio.c side, and the worker sets len after reading. EOF/error is len=0 / + ptr=NULL. +- The task struct is allocated on the calling fiber's stack, and a pointer to it + is pushed onto the queue. Since the fiber is parked until completion, the stack + is alive (the fiber's stack is retained even while parked). On completion the + worker writes to the slot and enqueues the handle. +- Result visibility: the worker's slot write is done before + `rw_sched_enqueue_ready`, riding on the happens-before of enqueue/park-return + (the same acquire/release convention as the existing netpoller). + +## Concurrency correctness + +This protocol is made **completely isomorphic** to the two existing "park and let +another thread wake" paths. The real code has been verified +(`runtime/fiber/sched.c`, `runtime/net/netpoller*.c`): + +- **netpoller** (`rw_net_park_read`): register on kqueue/epoll with + `rw_netpoller_register_read(fd, h)` → `rw_sched_park_current()`. The poll thread + does `rw_sched_enqueue_ready(h)` on the event. +- **join** (`park_on`): put itself on the wait list inside `wait_lock` and set + `state=WAITING` → release → `rw_fiber_swap`. The completing side + `finalize_fiber` sets `state=DONE` (release) → takes the list en masse under + `wait_lock` → `enqueue_ready` for each waiter. +- **aio (this PR)**: fix the handle → submit the task → `rw_sched_park_current()`. + After the worker completes `read(2)`/`write(2)`, it stores the result in the + slot → `rw_sched_enqueue_ready(task->handle)`. + +`rw_sched_enqueue_ready` is documented in sched.h as "safe to call from another +thread", and `enqueue_ready` sets `state=READY` and pushes to the ready queue +(`sched.c:206`). `rw_sched_park_current` sets `state=WAITING` and returns to the +scheduler via `rw_fiber_swap` (`sched.c:339`). worker_main returns a fiber that +returned from swap to ready **only when `state==RUNNING`**, and does not return +WAITING/DONE (`sched.c:373`). This guarantees the ironclad rule "a WAITING fiber +must not be in the ready queue". + +### On the race in ctx publication timing + +A stackful coroutine scheduler has the classic race where "before the +`rw_fiber_swap` (ctx save) inside `rw_sched_park_current` completes, another +thread does `enqueue_ready` → another worker steals → resumes a half-written ctx +→ PC=0 SEGV" ([[stackful-coroutine-scheduling]]). + +The aio in this PR does **not newly introduce** this race. Reason: as above, it +uses the **same protocol** as netpoller / join (fix handle → register/submit → +park; the other thread enqueues after park) and does not create its own +publish-before-save pattern. Whether the worker can enqueue before park, and its +safety, is under **exactly the same conditions** as the netpoller's register→park +(if the fd is ready right after registration, the poll thread can enqueue +immediately), and this PR does not raise or lower the safety level of the +existing code. + +Therefore this PR's responsibility is "to write the protocol in an order +indistinguishable from the netpoller's". Should this isomorphic race ever +materialize in the future, it is an issue of the scheduler layer common to +netpoller, join, and aio, and is addressed for all three paths together as a +separate task that builds the equivalent of `park.c`'s `wait_lock` into +park_current (out of scope for this PR). + +### Flush out races in verification + +Following the verification procedure of [[stackful-coroutine-scheduling]], run an +e2e where multiple fibers do file I/O concurrently, in **both `RW_WORKERS=1` and +`RW_WORKERS≥2` (where stealing occurs)**, and confirm that the results come out +correctly without SEGV/hang. This is evidence that the aio path cooperates +correctly with the existing scheduler. + +## Layers touched + +| Layer | File | Change | |---|---|---| -| Runtime (新規) | `runtime/aio.h` / `runtime/aio.c` | 抽象 + thread pool バックエンド | -| Runtime | `runtime/io.c` | `rw_read`/`rw_write` に `fstat` 判定を追加し、ファイルは aio に委譲 | -| Runtime | `runtime/runtime.c` | `rw_init`/`rw_shutdown` に aio init/shutdown | -| Runtime | `runtime/Makefile` | `aio.o` を OBJS とビルドルールに追加 | -| Compiler | `rwc/` | **無改修**(`read`/`write` の組み込みは不変、内部経路だけ変わる) | -| Examples | 既存 `examples/file_io.rw` で回帰確認 | 新規サンプルは任意 | -| Tests | `tests/` | 既存 e2e(file_io / tcp)が緑のまま。並行ファイル read の e2e を 1 本追加 | +| Runtime (new) | `runtime/aio.h` / `runtime/aio.c` | Abstraction + thread pool backend | +| Runtime | `runtime/io.c` | Add an `fstat` decision to `rw_read`/`rw_write`, and delegate files to aio | +| Runtime | `runtime/runtime.c` | aio init/shutdown in `rw_init`/`rw_shutdown` | +| Runtime | `runtime/Makefile` | Add `aio.o` to OBJS and the build rules | +| Compiler | `rwc/` | **Unchanged** (the `read`/`write` built-ins are unchanged; only the internal path changes) | +| Examples | Regression-check with the existing `examples/file_io.rw` | A new example is optional | +| Tests | `tests/` | The existing e2e (file_io / tcp) stays green. Add 1 e2e for concurrent file read | -`rwc/` を一切触らないのが本 PR の特徴。言語仕様は変わらず、ランタイムの -ファイル I/O 実装が同期ブロッキングから非同期オフロードに変わるだけ。 +The characteristic of this PR is that it does not touch `rwc/` at all. The +language specification does not change; only the runtime's file I/O implementation +changes from synchronous blocking to asynchronous offload. -## 検証 +## Verification ```sh make -C runtime -uv run pytest -q # 既存 169 + 新規が全緑 -uv run rwc run examples/file_io.rw # round-trip が従来どおり一致 +uv run pytest -q # existing 169 + new all green +uv run rwc run examples/file_io.rw # round-trip matches as before ``` -- 回帰: `examples/file_io.rw`(round-trip)と `tests/test_e2e_tcp.py`(ソケット - 経路が無改修で動く)が緑。 -- 非同期性の確認 (新規 e2e): 複数 fiber がそれぞれファイルを read/write する - rw サンプルを `spawn` で並行実行し、全 fiber の結果が正しく揃うこと。 - thread pool 経由で正しく park/再開できている証拠になる。 - (タイミング依存にならないよう、出力は決定的な内容にする。) -- メインスレッド経路: fiber 外から `read`/`write` を呼ぶ既存サンプル(main 直下 - での file_io)が同期フォールバックで動くこと(file_io.rw が該当)。 - -## リスクと対処 - -- **park/enqueue の競合 (ctx 公開タイミング)**: netpoller / join と**同一 - プロトコル**(handle 確定 → submit → `park_current`、worker は結果格納後 - `enqueue_ready`)を厳守し、独自の publish-before-save を作らない。本 PR は - 既存の安全性水準を変えない(上記「並行性の正しさ」参照)。実装時に - `RW_WORKERS≥2` + steal 負荷で SEGV/ハングがないことを必ず確認する。 -- **fiber スタック上のタスク生存**: park 中も fiber スタックは保持されるため、 - タスクをスタックに置きポインタを渡して安全。worker は完了書き込みのみ行い、 - タスクを free しない(呼び出し fiber が所有)。 -- **fstat のコスト**: ファイル/ソケット判定に毎回 `fstat` を 1 回呼ぶ。read 1 回 - あたり syscall 1 増だが、ブロッキング read の代替としては無視できる。PR2 の - io_uring 化でも同じ判定を流用できる。 -- **メインスレッドのブロック**: fiber 外では同期 read にフォールバックするため、 - メインスレッドは従来どおりブロックしうる(Non-Goal)。ただし netpoller 同様、 - ワーカー M と netpoller/aio スレッドは別なので並行タスクは進む。 -- **「ついでに」誘惑**: io_uring・fixed buffer・seek 等には手を出さない - (Non-Goals)。本 PR は抽象 + thread pool の最小実装に閉じる。 +- Regression: `examples/file_io.rw` (round-trip) and `tests/test_e2e_tcp.py` (the + socket path works unchanged) are green. +- Confirming asynchrony (new e2e): run an rw example where multiple fibers each + read/write a file concurrently via `spawn`, and confirm that the results of all + fibers come out correctly. This is evidence that park/resume via the thread pool + works correctly. (To avoid being timing-dependent, make the output + deterministic content.) +- Main thread path: an existing example that calls `read`/`write` from outside a + fiber (file_io directly under main) works via the synchronous fallback + (file_io.rw qualifies). + +## Risks and mitigations + +- **park/enqueue race (ctx publication timing)**: strictly follow the **same + protocol** as netpoller / join (fix handle → submit → `park_current`; the worker + does `enqueue_ready` after storing the result), and do not create your own + publish-before-save. This PR does not change the existing safety level (see + "Concurrency correctness" above). During implementation, always confirm there is + no SEGV/hang under `RW_WORKERS≥2` + steal load. +- **Task survival on the fiber stack**: since the fiber stack is retained even + while parked, it is safe to place the task on the stack and pass a pointer. The + worker only performs the completion write and does not free the task (the calling + fiber owns it). +- **Cost of fstat**: one `fstat` is called each time to decide file/socket. This is + 1 extra syscall per read, but negligible as a replacement for a blocking read. + The same decision can be reused when moving to io_uring in PR2. +- **Blocking the main thread**: outside a fiber it falls back to synchronous read, + so the main thread can block as before (Non-Goal). However, as with the + netpoller, the worker M and the netpoller/aio threads are separate, so concurrent + tasks make progress. +- **The "while we're at it" temptation**: do not touch io_uring, fixed buffers, + seek, etc. (Non-Goals). This PR is confined to the minimal implementation of + abstraction + thread pool. diff --git a/pyproject.toml b/pyproject.toml index e9d4277..3ee0c8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,5 +44,5 @@ target-version = ["py311"] python_version = "3.11" strict = false ignore_missing_imports = true -# strict は段階的に有効化する方針。 -# 個別モジュールへ override を追加して順次厳格化していく。 +# Policy: enable strict mode gradually. +# Add per-module overrides to tighten checks incrementally.