diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..7f050be --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cmp.iml b/.idea/cmp.iml new file mode 100644 index 0000000..962e49f --- /dev/null +++ b/.idea/cmp.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..4abd7d2 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,350 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c19669f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c8397c9 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 8a7467d..bf1e439 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP is currently in its **bootstrap stage**. The package exports the root module -> `mcpplibs.cmp`, but it does not provide coroutine runtime APIs yet. +> CMP now provides its first coroutine primitive: a lazy, single-consumer `Task` / `Task`. +> Scheduling, cancellation, timers, asynchronous I/O, and a public root runner are not implemented. CMP is being built as a modern coroutine runtime and library on standard stackless C++ coroutines. The intended direction is an explicit `co_await` model that can grow, in small @@ -71,21 +71,43 @@ cd examples/basic mcpp run ``` -The example exits successfully without output. Its purpose is to prove that an independent mcpp -package can resolve the path dependency and import `mcpplibs.cmp`. +The example prints `Coroutine result: 42` from inside a `Task` coroutine and exits +successfully. It proves that an independent mcpp package can resolve the path dependency, import +`mcpplibs.cmp`, compose Tasks, and execute the synchronous chain. -## Current Module +## Current Task API ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -The module deliberately has no public declarations during bootstrap. Future public APIs will use -the namespace `mcpplibs::cmp`. +`Task` is lazy: calling `answer()` creates a suspended coroutine. It starts when consumed by +`co_await`. A Task is move-only, has one consumer, and can only be awaited as an rvalue. It stores +either a value or an exception, transfers directly between child and continuation, and destroys +an unconsumed frame through RAII. `Task`, copying, move assignment, and detached execution are +deliberately unsupported. + +A translation unit that defines a coroutine must import `std` so the compiler can see the standard +coroutine protocol types. CMP imports `std` privately and does not re-export the whole standard +library. + +CMP does not yet provide `sync_wait` or a scheduler, so the current API is a composition primitive +rather than a complete application entry point. The standalone example therefore defines a small, +private root coroutine that is suitable only for its synchronously completing chain. A moved-from +Task must not be awaited. ## Repository Layout @@ -94,7 +116,7 @@ the namespace `mcpplibs::cmp`. ├── .xlings.json # pinned project tool environment ├── mcpp.toml # package identity and test dependency ├── src/cmp.cppm # root module interface -├── tests/cmp_test.cpp # import smoke test +├── tests/cmp_test.cpp # Task contract and lifetime tests ├── examples/basic/ # standalone path-dependency consumer ├── docs/architecture.md # current structure, boundaries, and evolution └── .github/workflows/ # Linux, macOS, and Windows CI @@ -122,17 +144,16 @@ belong in `[dependencies]`; test-only dependencies belong in `[dev-dependencies] ## Roadmap -Runtime work will be split into independently reviewable phases: +Runtime work is split into independently reviewable phases: -1. package identity and importable-module bootstrap; -2. coroutine task and lifetime semantics; +1. package identity and importable-module bootstrap — implemented; +2. coroutine task and lifetime semantics — initial `Task` implemented; 3. a minimal single-thread scheduler; 4. timers, cancellation, and structured wake-up paths; 5. multi-worker scheduling and work stealing; 6. asynchronous I/O integration and a blocking pool. -The order after the bootstrap is directional, not a promise that any listed feature is already -implemented. +The remaining order is directional, not a promise that a listed feature is already implemented. ## Contributing diff --git a/README.zh.hant.md b/README.zh.hant.md index d7438b4..97892c2 100644 --- a/README.zh.hant.md +++ b/README.zh.hant.md @@ -16,7 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP 目前處於 **bootstrap 階段**。套件已經匯出根模組 `mcpplibs.cmp`,但尚未提供協程執行期 API。 +> CMP 已提供第一個協程基礎型別:延遲啟動、單一消費者的 `Task` / `Task`。 +> 排程、取消、計時器、非同步 I/O 和公開根任務驅動器尚未實作。 CMP 計畫以標準無堆疊 C++ 協程建構現代協程執行期與函式庫。專案將以明確的 `co_await` 為主線,透過經過驗證的小步驟逐步探索排程、計時器、非同步 I/O、取消,以及阻塞工作的安全隔離。 @@ -66,20 +67,39 @@ cd examples/basic mcpp run ``` -範例會以成功狀態結束且不產生輸出。它只負責證明獨立 mcpp 套件能夠解析路徑相依並匯入 -`mcpplibs.cmp`。 +範例會從 `Task` 協程內部印出 `Coroutine result: 42`,然後以成功狀態結束。它負責 +證明獨立 mcpp 套件能夠解析路徑相依、匯入 `mcpplibs.cmp`、組合 Task 並執行同步協程鏈。 -## 目前模組 +## 目前 Task API ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -bootstrap 階段的模組刻意不包含公開宣告。未來公共 API 將使用 `mcpplibs::cmp` 命名空間。 +`Task` 採延遲啟動:呼叫 `answer()` 只建立處於暫停狀態的協程,在被 `co_await` 消費時才 +開始執行。Task 只能移動、只有一個消費者且只能作為右值等待;它保存值或例外,在子協程與 +continuation 之間直接轉移,並透過 RAII 銷毀未消費的協程框架。目前刻意不支援 `Task`、 +複製、移動賦值和 detached 執行。 + +定義協程的轉譯單元必須匯入 `std`,使編譯器能夠看到標準協程協定型別。CMP 私下匯入 +`std`,不會向使用端重新匯出整個標準函式庫。 + +CMP 尚未提供 `sync_wait` 或排程器,因此目前 API 是協程組合基礎,而不是完整的應用程式入口。 +獨立範例因此定義了一個很小的私有根協程,只適用於其中同步完成的協程鏈。已經被移動的 +Task 不得再次等待。 ## 儲存庫結構 @@ -88,7 +108,7 @@ bootstrap 階段的模組刻意不包含公開宣告。未來公共 API 將使 ├── .xlings.json # 固定的專案工具環境 ├── mcpp.toml # 套件識別與測試相依 ├── src/cmp.cppm # 根模組介面 -├── tests/cmp_test.cpp # 匯入 smoke 測試 +├── tests/cmp_test.cpp # Task 契約和生命週期測試 ├── examples/basic/ # 獨立的路徑相依 consumer ├── docs/architecture.zh.hant.md # 目前結構、邊界與演進方向 └── .github/workflows/ # Linux、macOS 和 Windows CI @@ -114,16 +134,16 @@ CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定 ## 路線圖 -執行期工作將拆分為可以獨立審查的階段: +執行期工作拆分為可以獨立審查的階段: -1. 套件識別與可匯入模組 bootstrap; -2. 協程 task 與生命週期語意; +1. 套件識別與可匯入模組 bootstrap——已完成; +2. 協程 task 與生命週期語意——已實作初始 `Task`; 3. 最小單執行緒排程器; 4. 計時器、取消和結構化喚醒路徑; 5. 多 worker 排程與 work stealing; 6. 非同步 I/O 整合和 blocking pool。 -bootstrap 之後的順序只是方向,不代表這些能力已經實作。 +剩餘順序只是方向,不代表列出的能力已經實作。 ## 參與貢獻 diff --git a/README.zh.md b/README.zh.md index b57de0a..4b93282 100644 --- a/README.zh.md +++ b/README.zh.md @@ -16,7 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP 当前处于 **bootstrap 阶段**。包已经导出根模块 `mcpplibs.cmp`,但尚未提供协程运行时 API。 +> CMP 已经提供第一个协程基础类型:懒启动、单消费者的 `Task` / `Task`。 +> 调度、取消、定时器、异步 I/O 和公共根任务驱动器尚未实现。 CMP 计划基于标准无栈 C++ 协程构建现代协程运行时和库。项目将以显式 `co_await` 为主线, 通过经过验证的小步骤逐步探索调度、定时器、异步 I/O、取消以及阻塞工作的安全隔离。 @@ -66,20 +67,39 @@ cd examples/basic mcpp run ``` -示例会以成功状态退出且不产生输出。它只负责证明独立 mcpp 包能够解析路径依赖并导入 -`mcpplibs.cmp`。 +示例会从 `Task` 协程内部打印 `Coroutine result: 42`,然后以成功状态退出。它负责 +证明独立 mcpp 包能够解析路径依赖、导入 `mcpplibs.cmp`、组合 Task 并执行同步协程链。 -## 当前模块 +## 当前 Task API ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -bootstrap 阶段的模块刻意不包含公开声明。未来公共 API 将使用 `mcpplibs::cmp` 命名空间。 +`Task` 采用懒启动:调用 `answer()` 只创建处于挂起状态的协程,在被 `co_await` 消费时才 +开始执行。Task 只能移动、只有一个消费者且只能作为右值等待;它保存值或异常,在子协程与 +continuation 之间直接转移,并通过 RAII 销毁未消费的协程帧。当前刻意不支持 `Task`、 +复制、移动赋值和 detached 执行。 + +定义协程的翻译单元必须导入 `std`,使编译器能够看到标准协程协议类型。CMP 私有导入 +`std`,不会向使用方重新导出整个标准库。 + +CMP 尚未提供 `sync_wait` 或调度器,因此当前 API 是协程组合基础,而不是完整的应用入口。 +独立示例因此定义了一个很小的私有根协程,只适用于其中同步完成的协程链。已经被移动的 +Task 不得再次等待。 ## 仓库结构 @@ -88,7 +108,7 @@ bootstrap 阶段的模块刻意不包含公开声明。未来公共 API 将使 ├── .xlings.json # 固定的项目工具环境 ├── mcpp.toml # 包身份和测试依赖 ├── src/cmp.cppm # 根模块接口 -├── tests/cmp_test.cpp # 导入 smoke 测试 +├── tests/cmp_test.cpp # Task 契约和生命周期测试 ├── examples/basic/ # 独立的路径依赖 consumer ├── docs/architecture.zh.md # 当前结构、边界和演进方向 └── .github/workflows/ # Linux、macOS 和 Windows CI @@ -114,16 +134,16 @@ CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。 ## 路线图 -运行时工作将拆分为可以独立审查的阶段: +运行时工作拆分为可以独立审查的阶段: -1. 包身份和可导入模块 bootstrap; -2. 协程 task 与生命周期语义; +1. 包身份和可导入模块 bootstrap——已完成; +2. 协程 task 与生命周期语义——已实现初始 `Task`; 3. 最小单线程调度器; 4. 定时器、取消和结构化唤醒路径; 5. 多 worker 调度与 work stealing; 6. 异步 I/O 集成和 blocking pool。 -bootstrap 之后的顺序只是方向,不代表这些能力已经实现。 +剩余顺序只是方向,不代表列出的能力已经实现。 ## 参与贡献 diff --git a/docs/architecture.md b/docs/architecture.md index 1e20967..aacc3fd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,14 +4,15 @@ ## Current status -CMP currently consists of an initial C++23 module project. The package can be built and imported, -but the root module has no public declarations and no coroutine runtime has been implemented. +CMP is a C++23 module project whose first runtime primitive is implemented. The root module exports +a lazy, single-consumer `mcpplibs::cmp::Task` with a `Task` specialization. It does not yet +provide a scheduler or a root execution API. The repository contains: - one mcpp package manifest; -- the import-only root module `mcpplibs.cmp`; -- one gtest import test; +- the root module `mcpplibs.cmp` and its Task implementation; +- gtest contract, lifetime, exception, and symmetric-transfer tests; - one standalone path-dependency example; - Linux, macOS, and Windows CI workflows. @@ -29,8 +30,8 @@ repo = "https://github.com/mcpplibs/cmp" ``` The mcpp package identity is the pair `mcpplibs` and `cmp`. A consumer declares `cmp` under -`[dependencies.mcpplibs]` and imports the C++ module `mcpplibs.cmp`. The namespace -`mcpplibs::cmp` is reserved for future public C++ declarations. +`[dependencies.mcpplibs]` and imports the C++ module `mcpplibs.cmp`. Public C++ declarations use +the namespace `mcpplibs::cmp`. The root interface is `src/cmp.cppm`, which matches mcpp's default library-root naming rule. There is no `src/main.cpp`, so mcpp infers a library target named `cmp`; the manifest does not @@ -64,8 +65,9 @@ it is also mcpp's default. ## Build and tests `.xlings.json` pins the mcpp version used by the project. `mcpp build` builds the inferred library -target. `mcpp test` discovers `tests/cmp_test.cpp`, links the gtest entry point, and verifies that a -separate translation unit can import `mcpplibs.cmp`. +target. `mcpp test` discovers `tests/cmp_test.cpp`, links the gtest entry point, and verifies the +Task type contract, lazy execution, frame ownership, value and exception propagation, nested +composition, and stack-safe symmetric transfer. Each CI workflow installs the project tools, builds the library, runs the test suite, and runs `examples/basic`. The workflows are separate because tool installation and runner details differ @@ -96,21 +98,51 @@ cmp = { path = "../.." } Its program uses the same import path as an external package: ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -This example checks path dependency resolution and module consumption independently of the root -test target. +This example checks path dependency resolution, module consumption, and external coroutine +compilation independently of the root test target. Its example-private `InlineRunner` starts an +eager root coroutine, which awaits `print_answer()` and produces `Coroutine result: 42`. The helper +is limited to this synchronously completing, scheduler-free chain and is not a CMP public API. + +Any translation unit that defines a coroutine imports `std` itself so `std::coroutine_traits` and +the standard coroutine protocol types participate in compilation. The CMP module imports `std` +privately rather than re-exporting the entire standard library. ## Current constraints -The module currently provides no `task`, promise type, scheduler, timer, cancellation mechanism, -asynchronous I/O backend, or blocking-work pool. It also provides no compatibility alias for the -old scaffold module. +`Task` and `Task` have the following contract: + +- construction is lazy; the coroutine body starts when the Task is awaited; +- ownership is unique: Task is movable but not copyable or move-assignable; +- `operator co_await()` is rvalue-only and consumes the coroutine handle; +- one value or `std::exception_ptr` is stored in the coroutine frame; +- child completion transfers directly to its continuation, avoiding recursive `resume()` chains; +- an unconsumed Task destroys its frame, and the consuming awaiter destroys a completed frame; +- reference and array result types are rejected. + +A moved-from Task is empty and must not be awaited. The current implementation terminates on that +contract violation. There is no public `sync_wait`, detached execution, scheduler, thread-affinity +guarantee, timer, cancellation mechanism, asynchronous I/O backend, custom frame allocator, or +blocking-work pool. The module also provides no compatibility alias for the old scaffold module. + +Capturing coroutine lambdas require particular care: invoking a temporary capturing lambda can +leave the lazy coroutine referring to a destroyed closure. CMP does not yet provide a helper that +extends that closure's lifetime. The `C` in CMP echoes the naming role of Go runtime's `G`; it does not imply equivalent semantics. Standard C++ coroutines provide suspension and resumption mechanics, but they do not supply a @@ -121,12 +153,13 @@ scheduler and do not make a blocking operation asynchronous. The following areas may be considered in separate designs. They are not part of the current package contract: -1. coroutine task ownership, completion, and lifetime rules; -2. a single-thread scheduler and explicit scheduling awaiters; +1. a root runner and minimal single-thread scheduler with explicit scheduling awaiters; +2. structured task scopes and concurrent joins; 3. timers, wake-up paths, and cancellation; 4. multi-worker scheduling and work stealing; 5. asynchronous I/O integrations; -6. a dedicated pool for unavoidable blocking work. +6. a dedicated pool for unavoidable blocking work; +7. result adapters and optional coroutine-frame allocation strategies. Module partitions or implementation units can be added when an implemented API needs those boundaries. @@ -142,7 +175,9 @@ cd examples/basic mcpp run ``` -The expected result is a successful library build, one passing import test, and an example that -exits with status 0. The current Windows LLVM toolchain does not emit GNU depfiles. If a file +The expected result is a successful library build, eight passing Task tests, and an example that +exits with status 0. One test performs one million immediate Task completions to check that +symmetric transfer does not grow the native call stack. The current Windows LLVM toolchain does +not emit GNU depfiles. If a file included by a module interface changes, an incremental build can reuse an older BMI or object; `--cache=off` is used for a full local verification. diff --git a/docs/architecture.zh.hant.md b/docs/architecture.zh.hant.md index b8b9a25..78aaf8d 100644 --- a/docs/architecture.zh.hant.md +++ b/docs/architecture.zh.hant.md @@ -4,14 +4,14 @@ ## 目前狀態 -CMP 目前是一個初始的 C++23 模組專案。套件可以建置和匯入,但根模組還沒有公開宣告, -協程執行期也尚未實作。 +CMP 是一個 C++23 模組專案,已實作第一個執行期基礎型別。根模組匯出延遲啟動、單一消費者的 +`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供排程器或根任務執行 API。 儲存庫現有內容包括: - 一份 mcpp 套件清單; -- 只包含模組宣告的根模組 `mcpplibs.cmp`; -- 一個 gtest 匯入測試; +- 根模組 `mcpplibs.cmp` 及其 Task 實作; +- 涵蓋契約、生命週期、例外和對稱轉移的 gtest 測試; - 一個透過路徑相依使用根套件的獨立範例; - Linux、macOS 和 Windows 三套 CI 工作流程。 @@ -29,8 +29,7 @@ repo = "https://github.com/mcpplibs/cmp" ``` mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.mcpplibs]` 中宣告 -`cmp`,在 C++ 原始碼中匯入 `mcpplibs.cmp`。`mcpplibs::cmp` 保留給日後的公開 C++ -宣告使用。 +`cmp`,在 C++ 原始碼中匯入 `mcpplibs.cmp`。公開 C++ 宣告使用 `mcpplibs::cmp` 命名空間。 根模組介面位於 `src/cmp.cppm`,符合 mcpp 預設的函式庫根模組命名規則。儲存庫中沒有 `src/main.cpp`,因此 mcpp 會推斷出名為 `cmp` 的函式庫目標,不需要額外設定 `[lib]` 或 @@ -63,8 +62,8 @@ mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.m ## 建置與測試 `.xlings.json` 固定專案使用的 mcpp 版本。`mcpp build` 建置自動推斷的函式庫目標。 -`mcpp test` 會找到 `tests/cmp_test.cpp`,連結 gtest 進入點,並驗證另一個轉譯單元可以 -匯入 `mcpplibs.cmp`。 +`mcpp test` 會找到 `tests/cmp_test.cpp`,連結 gtest 進入點,並驗證 Task 型別契約、延遲執行、 +協程框架所有權、值與例外傳播、巢狀組合,以及不會增長呼叫堆疊的對稱轉移。 三套 CI 工作流程都會安裝專案工具、建置函式庫、執行測試並執行 `examples/basic`。不同 作業系統的工具安裝和執行環境不同,因此分別保留工作流程檔案。 @@ -93,19 +92,47 @@ cmp = { path = "../.." } 範例程式使用與外部專案相同的匯入路徑: ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -這個範例在根測試目標之外,單獨檢查路徑相依解析和模組使用。 +這個範例在根測試目標之外,單獨檢查路徑相依解析、模組使用和外部協程編譯。範例私有的 +`InlineRunner` 啟動一個 eager 根協程,等待 `print_answer()` 並輸出 `Coroutine result: 42`。 +這個輔助型別只適用於目前同步完成且沒有排程器的協程鏈,不屬於 CMP 公共 API。 + +任何定義協程的轉譯單元都要自行匯入 `std`,使 `std::coroutine_traits` 和標準協程協定型別 +參與編譯。CMP 模組私下匯入 `std`,而不是向使用端重新匯出整個標準函式庫。 ## 目前邊界 -根模組目前沒有提供 `task`、promise 型別、排程器、計時器、取消機制、非同步 I/O 後端或 -阻塞工作執行緒池,也沒有保留舊骨架模組的相容別名。 +`Task` 和 `Task` 遵循以下契約: + +- 建構時延遲啟動,協程本體在 Task 被等待時開始執行; +- 所有權唯一:Task 可以移動建構,但不能複製或移動賦值; +- `operator co_await()` 僅用於右值,並在等待時消費協程控制代碼; +- 協程框架保存一個值或 `std::exception_ptr`; +- 子協程完成後直接轉移到 continuation,避免遞迴呼叫 `resume()`; +- 未消費的 Task 銷毀自己的框架,消費它的 awaiter 銷毀已完成的框架; +- 拒絕參考和陣列結果型別。 + +被移動後的 Task 為空,不得再次等待;目前實作會在違反該契約時終止程序。目前沒有公開 +`sync_wait`、detached 執行、排程器、執行緒親和保證、計時器、取消機制、非同步 I/O 後端、 +自訂協程框架 allocator 或阻塞工作執行緒池,也沒有保留舊骨架模組的相容別名。 + +捕捉變數的協程 lambda 需要特別小心:立即呼叫一個暫時的捕捉 lambda,可能使延遲協程參考 +已經銷毀的閉包。CMP 尚未提供延長該閉包生命週期的輔助函式。 CMP 名稱中的 `C` 與 Go 執行期中的 `G` 相呼應,但這只說明命名來源,不表示兩者語意 等價。標準 C++ 協程提供暫停和恢復機制,本身不包含排程器,也不會把阻塞操作自動變成 @@ -115,12 +142,13 @@ CMP 名稱中的 `C` 與 Go 執行期中的 `G` 相呼應,但這只說明命 以下方向可以分別設計和審查,目前都不是套件的既有約定: -1. 協程工作的所有權、完成和生命週期規則; -2. 單執行緒排程器和明確的排程 awaiter; +1. 根任務驅動器、最小單執行緒排程器和明確的排程 awaiter; +2. 結構化任務作用域和並行匯合; 3. 計時器、喚醒路徑和取消; 4. 多工作執行緒排程和工作竊取; 5. 非同步 I/O 整合; -6. 處理無法避免之阻塞工作的專用執行緒池。 +6. 處理無法避免之阻塞工作的專用執行緒池; +7. 結果適配器和可選的協程框架配置策略。 只有已實作的 API 確實需要新邊界時,才增加模組分割區或實作單元。 @@ -135,6 +163,7 @@ cd examples/basic mcpp run ``` -預期結果是函式庫建置成功、一個匯入測試通過,而且範例以狀態 0 結束。目前 Windows -LLVM 工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能沿用舊的 -BMI 或目的檔。完整複驗時使用 `--cache=off`。 +預期結果是函式庫建置成功、八個 Task 測試通過,而且範例以狀態 0 結束。其中一個測試執行 +一百萬次立即完成的 Task,用於檢查對稱轉移不會增長原生呼叫堆疊。目前 Windows LLVM +工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能沿用舊的 BMI +或目的檔。完整複驗時使用 `--cache=off`。 diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 78b4bef..1c85b21 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -4,14 +4,14 @@ ## 当前状态 -CMP 目前是一个初始的 C++23 模块项目。包可以构建和导入,但根模块还没有公共声明, -协程运行时也尚未实现。 +CMP 是一个 C++23 模块项目,已经实现首个运行时基础类型。根模块导出懒启动、单消费者的 +`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供调度器或根任务执行 API。 仓库现有内容包括: - 一份 mcpp 包清单; -- 仅包含模块声明的根模块 `mcpplibs.cmp`; -- 一个 gtest 导入测试; +- 根模块 `mcpplibs.cmp` 及其 Task 实现; +- 覆盖契约、生命周期、异常和对称转移的 gtest 测试; - 一个通过路径依赖使用根包的独立示例; - Linux、macOS 和 Windows 三套 CI 工作流。 @@ -29,8 +29,7 @@ repo = "https://github.com/mcpplibs/cmp" ``` mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpplibs]` 中声明 -`cmp`,在 C++ 源码中导入 `mcpplibs.cmp`。`mcpplibs::cmp` 留作以后公共 C++ 声明 -使用。 +`cmp`,在 C++ 源码中导入 `mcpplibs.cmp`。公共 C++ 声明使用 `mcpplibs::cmp` 命名空间。 根模块接口位于 `src/cmp.cppm`,符合 mcpp 默认的库根模块命名规则。仓库中没有 `src/main.cpp`,因此 mcpp 会推断出名为 `cmp` 的库目标,不需要额外配置 `[lib]` 或 @@ -63,8 +62,8 @@ mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpp ## 构建与测试 `.xlings.json` 固定项目使用的 mcpp 版本。`mcpp build` 构建自动推断的库目标。 -`mcpp test` 发现 `tests/cmp_test.cpp`,链接 gtest 入口,并验证另一个翻译单元可以导入 -`mcpplibs.cmp`。 +`mcpp test` 发现 `tests/cmp_test.cpp`,链接 gtest 入口,并验证 Task 类型契约、懒执行、 +协程帧所有权、值与异常传播、嵌套组合以及不会增长调用栈的对称转移。 三套 CI 工作流都会安装项目工具、构建库、运行测试并执行 `examples/basic`。不同操作系统 的工具安装和运行环境不同,因此分别保留工作流文件。 @@ -93,19 +92,47 @@ cmp = { path = "../.." } 示例程序使用与外部项目相同的导入路径: ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -该示例在根测试目标之外,单独检查路径依赖解析和模块使用。 +该示例在根测试目标之外,单独检查路径依赖解析、模块使用和外部协程编译。示例私有的 +`InlineRunner` 启动一个 eager 根协程,等待 `print_answer()` 并输出 `Coroutine result: 42`。 +这个辅助类型只适用于当前同步完成且没有调度器的协程链,不属于 CMP 公共 API。 + +任何定义协程的翻译单元都要自行导入 `std`,使 `std::coroutine_traits` 和标准协程协议类型 +参与编译。CMP 模块私有导入 `std`,而不是向使用方重新导出整个标准库。 ## 当前边界 -根模块目前没有提供 `task`、promise 类型、调度器、定时器、取消机制、异步 I/O 后端或 -阻塞任务线程池,也没有保留旧脚手架模块的兼容别名。 +`Task` 和 `Task` 遵循以下契约: + +- 构造时懒启动,协程体在 Task 被等待时开始执行; +- 所有权唯一:Task 可以移动构造,但不能复制或移动赋值; +- `operator co_await()` 仅用于右值,并在等待时消费协程句柄; +- 协程帧保存一个值或 `std::exception_ptr`; +- 子协程完成后直接转移到 continuation,避免递归调用 `resume()`; +- 未消费的 Task 销毁自己的帧,消费它的 awaiter 销毁已经完成的帧; +- 拒绝引用和数组结果类型。 + +被移动后的 Task 为空,不得再次等待;当前实现会在违反该契约时终止进程。目前没有公共 +`sync_wait`、detached 执行、调度器、线程亲和保证、定时器、取消机制、异步 I/O 后端、 +自定义协程帧 allocator 或阻塞任务线程池,也没有保留旧脚手架模块的兼容别名。 + +捕获变量的协程 lambda 需要特别小心:立即调用一个临时的捕获 lambda,可能使懒协程引用 +已经销毁的闭包。CMP 尚未提供延长该闭包生命周期的辅助函数。 CMP 名称中的 `C` 与 Go 运行时中的 `G` 相呼应,但这只说明命名来源,不表示两者语义等价。 标准 C++ 协程提供挂起和恢复机制,本身不包含调度器,也不会把阻塞操作自动变成异步操作。 @@ -114,12 +141,13 @@ CMP 名称中的 `C` 与 Go 运行时中的 `G` 相呼应,但这只说明命 以下方向可以分别设计和评审,目前都不是包的既有约定: -1. 协程任务的所有权、完成和生命周期规则; -2. 单线程调度器和显式调度 awaiter; +1. 根任务驱动器、最小单线程调度器和显式调度 awaiter; +2. 结构化任务作用域和并发汇合; 3. 定时器、唤醒路径和取消; 4. 多工作线程调度和工作窃取; 5. 异步 I/O 集成; -6. 处理不可避免的阻塞工作的专用线程池。 +6. 处理不可避免的阻塞工作的专用线程池; +7. 结果适配器和可选的协程帧分配策略。 只有已实现的 API 确实需要新的边界时,才增加模块分区或实现单元。 @@ -134,6 +162,7 @@ cd examples/basic mcpp run ``` -预期结果是库构建成功、一个导入测试通过,并且示例以状态 0 退出。当前 Windows LLVM -工具链不会生成 GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI -或目标文件。完整复验时使用 `--cache=off`。 +预期结果是库构建成功、八个 Task 测试通过,并且示例以状态 0 退出。其中一个测试执行一百万次 +立即完成的 Task,用于检查对称转移不会增长原生调用栈。当前 Windows LLVM 工具链不会生成 +GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI 或目标文件。 +完整复验时使用 `--cache=off`。 diff --git a/examples/basic/src/main.cpp b/examples/basic/src/main.cpp index 191aa3c..b70cdc2 100644 --- a/examples/basic/src/main.cpp +++ b/examples/basic/src/main.cpp @@ -1,5 +1,49 @@ +import std; import mcpplibs.cmp; +using mcpplibs::cmp::Task; + +class InlineRunner { +public: + struct promise_type { + [[nodiscard]] InlineRunner get_return_object() const noexcept; + + [[nodiscard]] constexpr std::suspend_never initial_suspend() const noexcept { + return {}; + } + + [[nodiscard]] constexpr std::suspend_never final_suspend() const noexcept { + return {}; + } + + constexpr void return_void() const noexcept {} + + [[noreturn]] void unhandled_exception() const noexcept { + std::terminate(); + } + }; +}; + +InlineRunner InlineRunner::promise_type::get_return_object() const noexcept { + return {}; +} + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; +} + +InlineRunner run_inline(Task task) { + co_await std::move(task); + co_return; +} + int main() { + run_inline(print_answer()); return 0; } diff --git a/src/cmp.cppm b/src/cmp.cppm index c6912de..36aca08 100644 --- a/src/cmp.cppm +++ b/src/cmp.cppm @@ -1 +1,260 @@ export module mcpplibs.cmp; + +import std; + +export namespace mcpplibs::cmp { + +template +requires ( + std::same_as || + (std::is_object_v && !std::is_array_v) +) +class [[nodiscard]] Task { +public: + struct promise_type { + std::optional result_ {}; + std::exception_ptr exception_ {}; + std::coroutine_handle<> continuation_ { std::noop_coroutine() }; + + [[nodiscard]] Task get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + class FinalAwaiter { + public: + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle coroutine) const noexcept { + return coroutine.promise().continuation_; + } + + constexpr void await_resume() const noexcept {} + }; + + [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { + return {}; + } + + template + requires std::constructible_from + void return_value(U&& value) + noexcept(std::is_nothrow_constructible_v) { + result_.emplace(std::forward(value)); + } + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + class Awaiter { + private: + Handle coroutine_ {}; + + public: + explicit Awaiter(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + + Awaiter(const Awaiter&) = delete; + Awaiter& operator=(const Awaiter&) = delete; + + Awaiter(Awaiter&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Awaiter& operator=(Awaiter&&) = delete; + + ~Awaiter() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle<> continuation) noexcept { + coroutine_.promise().continuation_ = continuation; + return coroutine_; + } + + T await_resume() { + auto& promise = coroutine_.promise(); + + if (promise.exception_) { + std::rethrow_exception(promise.exception_); + } + + return std::move(*promise.result_); + } + }; + + Handle coroutine_ {}; + + explicit Task(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + Task() = delete; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task(Task&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Task& operator=(Task&&) = delete; + + ~Task() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] auto operator co_await() && noexcept { + if (!coroutine_) { + std::terminate(); + } + + return Awaiter { std::exchange(coroutine_, {}) }; + } +}; + +template +requires ( + std::same_as || + (std::is_object_v && !std::is_array_v) +) +Task Task::promise_type::get_return_object() noexcept { + return Task { + std::coroutine_handle::from_promise(*this) + }; +} + +template<> +class [[nodiscard]] Task { +public: + struct promise_type { + std::exception_ptr exception_ {}; + std::coroutine_handle<> continuation_ { std::noop_coroutine() }; + + [[nodiscard]] Task get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + class FinalAwaiter { + public: + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle coroutine) const noexcept { + return coroutine.promise().continuation_; + } + + constexpr void await_resume() const noexcept {} + }; + + [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { + return {}; + } + + constexpr void return_void() const noexcept {} + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + class Awaiter { + private: + Handle coroutine_ {}; + + public: + explicit Awaiter(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + + Awaiter(const Awaiter&) = delete; + Awaiter& operator=(const Awaiter&) = delete; + + Awaiter(Awaiter&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Awaiter& operator=(Awaiter&&) = delete; + + ~Awaiter() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle<> continuation) noexcept { + coroutine_.promise().continuation_ = continuation; + return coroutine_; + } + + void await_resume() { + auto& promise = coroutine_.promise(); + + if (promise.exception_) { + std::rethrow_exception(promise.exception_); + } + } + }; + + Handle coroutine_ {}; + + explicit Task(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + Task() = delete; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task(Task&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Task& operator=(Task&&) = delete; + + ~Task() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] auto operator co_await() && noexcept { + if (!coroutine_) { + std::terminate(); + } + + return Awaiter { std::exchange(coroutine_, {}) }; + } +}; + +inline Task Task::promise_type::get_return_object() noexcept { + return Task { + std::coroutine_handle::from_promise(*this) + }; +} + +} // namespace mcpplibs::cmp diff --git a/tests/cmp_test.cpp b/tests/cmp_test.cpp index 4354c83..d6208d6 100644 --- a/tests/cmp_test.cpp +++ b/tests/cmp_test.cpp @@ -1,7 +1,313 @@ #include +import std; import mcpplibs.cmp; -TEST(CmpModuleTest, Imports) { - SUCCEED(); +namespace { + +using mcpplibs::cmp::Task; + +template +concept SupportsTask = requires { + typename Task; +}; + +template +concept HasLvalueCoAwait = requires(T& task) { + task.operator co_await(); +}; + +template +concept HasRvalueCoAwait = requires(T&& task) { + std::move(task).operator co_await(); +}; + +static_assert(SupportsTask); +static_assert(SupportsTask); +static_assert(!SupportsTask); +static_assert(!SupportsTask); +static_assert(!SupportsTask); +static_assert(!SupportsTask); + +static_assert(!std::default_initializable>); +static_assert(!std::copy_constructible>); +static_assert(std::move_constructible>); +static_assert(!std::is_move_assignable_v>); +static_assert(!HasLvalueCoAwait>); +static_assert(HasRvalueCoAwait>); + +class TestOperation { +public: + struct promise_type { + std::exception_ptr exception_ {}; + + [[nodiscard]] TestOperation get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + [[nodiscard]] constexpr std::suspend_always final_suspend() const noexcept { + return {}; + } + + void return_void() const noexcept {} + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + Handle coroutine_ {}; + + explicit TestOperation(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + TestOperation(const TestOperation&) = delete; + TestOperation& operator=(const TestOperation&) = delete; + + TestOperation(TestOperation&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + TestOperation& operator=(TestOperation&&) = delete; + + ~TestOperation() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + void run(); +}; + +TestOperation TestOperation::promise_type::get_return_object() noexcept { + return TestOperation { TestOperation::Handle::from_promise(*this) }; +} + +void TestOperation::run() { + if (!coroutine_ || coroutine_.done()) { + throw std::logic_error { "test operation is not runnable" }; + } + + coroutine_.resume(); + + if (!coroutine_.done()) { + throw std::logic_error { "task unexpectedly suspended" }; + } + + if (coroutine_.promise().exception_) { + std::rethrow_exception(coroutine_.promise().exception_); + } +} + +template +TestOperation store_result(Task task, std::optional& result) { + result.emplace(co_await std::move(task)); +} + +TestOperation await_task(Task task) { + co_await std::move(task); +} + +Task make_value(bool& started) { + started = true; + co_return 42; +} + +Task increment(int& value) { + ++value; + co_return; +} + +Task make_nested_value() { + bool started { false }; + auto value = co_await make_value(started); + co_return value + 1; +} + +class LifetimeToken { +private: + int* liveCount_ {}; + +public: + explicit LifetimeToken(int& liveCount) noexcept + : liveCount_ { &liveCount } { + ++*liveCount_; + } + + LifetimeToken(const LifetimeToken&) = delete; + LifetimeToken& operator=(const LifetimeToken&) = delete; + + LifetimeToken(LifetimeToken&& other) noexcept + : liveCount_ { std::exchange(other.liveCount_, nullptr) } {} + + LifetimeToken& operator=(LifetimeToken&&) = delete; + + ~LifetimeToken() { + if (liveCount_) { + --*liveCount_; + } + } +}; + +Task hold_token(LifetimeToken token) { + static_cast(token); + co_return; +} + +Task throw_error(LifetimeToken token) { + static_cast(token); + throw std::runtime_error { "task failed" }; + co_return 0; +} + +class MoveOnlyValue { +private: + int* liveCount_ {}; + +public: + int value {}; + + MoveOnlyValue(int value, int& liveCount) noexcept + : liveCount_ { &liveCount }, value { value } { + ++*liveCount_; + } + + MoveOnlyValue(const MoveOnlyValue&) = delete; + MoveOnlyValue& operator=(const MoveOnlyValue&) = delete; + + MoveOnlyValue(MoveOnlyValue&& other) noexcept + : liveCount_ { std::exchange(other.liveCount_, nullptr) }, + value { other.value } {} + + MoveOnlyValue& operator=(MoveOnlyValue&&) = delete; + + ~MoveOnlyValue() { + if (liveCount_) { + --*liveCount_; + } + } +}; + +Task make_move_only_value(int& liveCount) { + co_return MoveOnlyValue { 7, liveCount }; +} + +Task complete_immediately() { + co_return; +} + +Task complete_many_times(int count) { + for (int index { 0 }; index < count; ++index) { + co_await complete_immediately(); + } + + co_return count; +} + +TEST(CmpTaskTest, IsLazyAndReturnsValue) { + bool started { false }; + auto task = make_value(started); + std::optional result {}; + + EXPECT_FALSE(started); + + auto operation = store_result(std::move(task), result); + EXPECT_FALSE(started); + + operation.run(); + + EXPECT_TRUE(started); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 42); +} + +TEST(CmpTaskTest, SupportsVoidResults) { + int value { 0 }; + auto operation = await_task(increment(value)); + + EXPECT_EQ(value, 0); + operation.run(); + EXPECT_EQ(value, 1); } + +TEST(CmpTaskTest, ComposesNestedTasks) { + std::optional result {}; + auto operation = store_result(make_nested_value(), result); + + operation.run(); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 43); +} + +TEST(CmpTaskTest, DestroysAnUnawaitedFrame) { + int liveCount { 0 }; + + { + auto task = hold_token(LifetimeToken { liveCount }); + EXPECT_EQ(liveCount, 1); + } + + EXPECT_EQ(liveCount, 0); +} + +TEST(CmpTaskTest, MovingTransfersFrameOwnershipOnce) { + int liveCount { 0 }; + + { + auto first = hold_token(LifetimeToken { liveCount }); + auto second = std::move(first); + static_cast(second); + EXPECT_EQ(liveCount, 1); + } + + EXPECT_EQ(liveCount, 0); +} + +TEST(CmpTaskTest, MovesResultBeforeDestroyingFrame) { + int liveCount { 0 }; + std::optional result {}; + auto operation = store_result(make_move_only_value(liveCount), result); + + operation.run(); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->value, 7); + EXPECT_EQ(liveCount, 1); + + result.reset(); + EXPECT_EQ(liveCount, 0); +} + +TEST(CmpTaskTest, PropagatesExceptionAndDestroysFrame) { + int liveCount { 0 }; + std::optional result {}; + auto operation = store_result( + throw_error(LifetimeToken { liveCount }), + result); + + EXPECT_EQ(liveCount, 1); + EXPECT_THROW(operation.run(), std::runtime_error); + EXPECT_EQ(liveCount, 0); + EXPECT_FALSE(result.has_value()); +} + +TEST(CmpTaskTest, SymmetricTransferDoesNotGrowTheStack) { + constexpr int COMPLETION_COUNT { 1'000'000 }; + std::optional result {}; + auto operation = store_result( + complete_many_times(COMPLETION_COUNT), + result); + + operation.run(); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, COMPLETION_COUNT); +} + +} // namespace