From 7e24ee02f2a1cbf374b816f8aec37ddee564f919 Mon Sep 17 00:00:00 2001
From: HikariTish <809456206@qq.com>
Date: Tue, 4 Aug 2026 23:17:09 +0800
Subject: [PATCH 1/5] feat: implement lazy single-consumer Task
---
.idea/.gitignore | 10 +
.idea/cmp.iml | 2 +
.idea/editor.xml | 350 +++++++++++++++++++++++++++++++++++
.idea/modules.xml | 8 +
.idea/vcs.xml | 6 +
README.md | 51 +++--
README.zh.hant.md | 44 +++--
README.zh.md | 44 +++--
docs/architecture.md | 75 ++++++--
docs/architecture.zh.hant.md | 67 +++++--
docs/architecture.zh.md | 67 +++++--
examples/basic/src/main.cpp | 44 +++++
src/cmp.cppm | 259 ++++++++++++++++++++++++++
tests/cmp_test.cpp | 310 ++++++++++++++++++++++++++++++-
14 files changed, 1238 insertions(+), 99 deletions(-)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/cmp.iml
create mode 100644 .idea/editor.xml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/vcs.xml
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 @@
[](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 @@
[](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 @@
[](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
From 47918bb0c3fb41338b13160a8e97fa23fecb9627 Mon Sep 17 00:00:00 2001
From: HikariTish
Date: Wed, 12 Aug 2026 20:13:38 +0800
Subject: [PATCH 2/5] feat: implement coroutine RunLoop and Scheduler
Add caller-thread FIFO scheduling, cross-thread wakeups, root result and exception handling, boundary tests, examples, documentation, and current mcpp configuration.
---
.agents/skills/mcpp-index/SKILL.md | 21 +-
.agents/skills/mcpp-style-ref/SKILL.md | 6 +-
.agents/skills/mcpp/SKILL.md | 14 +-
.agents/skills/more-details/SKILL.md | 9 +-
.gitignore | 1 +
.idea/.gitignore | 10 -
.idea/cmp.iml | 2 -
.idea/editor.xml | 350 -----------------
.idea/modules.xml | 8 -
.idea/vcs.xml | 6 -
.xlings.json | 2 +-
README.md | 41 +-
README.zh.hant.md | 38 +-
README.zh.md | 38 +-
docs/architecture.md | 103 +++--
docs/architecture.zh.hant.md | 90 +++--
docs/architecture.zh.md | 89 +++--
.../plans/2026-08-12-cmp-run-loop.md | 41 ++
.../specs/2026-08-12-cmp-run-loop-design.md | 121 ++++++
examples/basic/src/main.cpp | 37 +-
mcpp.toml | 2 +-
src/cmp.cppm | 260 +------------
src/run_loop.cppm | 333 ++++++++++++++++
src/task.cppm | 261 +++++++++++++
tests/cmp_test.cpp | 4 +-
tests/run_loop_test.cpp | 366 ++++++++++++++++++
26 files changed, 1437 insertions(+), 816 deletions(-)
delete mode 100644 .idea/.gitignore
delete mode 100644 .idea/cmp.iml
delete mode 100644 .idea/editor.xml
delete mode 100644 .idea/modules.xml
delete mode 100644 .idea/vcs.xml
create mode 100644 docs/superpowers/plans/2026-08-12-cmp-run-loop.md
create mode 100644 docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md
create mode 100644 src/run_loop.cppm
create mode 100644 src/task.cppm
create mode 100644 tests/run_loop_test.cpp
diff --git a/.agents/skills/mcpp-index/SKILL.md b/.agents/skills/mcpp-index/SKILL.md
index 698aa78..3a9d303 100644
--- a/.agents/skills/mcpp-index/SKILL.md
+++ b/.agents/skills/mcpp-index/SKILL.md
@@ -29,13 +29,10 @@ mcpp index list|add|remove|update # manage registries
Package identity is a pair: **`namespace` is a dotted hierarchical path, `name` is a single
atomic segment** (`compat` + `zlib`, `mcpplibs.capi` + `lua` — never `mcpplibs` + `capi.lua`).
-A **bare** dependency name resolves in exactly three places, in order:
-
-1. `mcpplibs` — the default namespace
-2. `compat` — third-party C/C++ wrappers
-3. packages that declare no namespace at all
-
-Anything else must be spelled out — there is no index-wide fuzzy search by short name:
+A **bare** dependency name means the `mcpplibs` default namespace. The older fallback search
+through `compat` and packages without a namespace is deprecated in mcpp 2026.8 and removed in
+2026.9. Every other namespace must be spelled out; there is no index-wide fuzzy search by short
+name:
```toml
[dependencies]
@@ -43,12 +40,14 @@ Anything else must be spelled out — there is no index-wide fuzzy search by sho
[dependencies.chriskohlhoff] # or a namespace sub-table
asio = "1.38.1"
+
+[dependencies.compat] # third-party wrapper
+zlib = "1.3.1"
```
-When resolution fails: read the error (it lists the namespaces searched), then check the
-spelling against the online index, and run `xlings update` — a release tarball bundles an
-index snapshot frozen at build time, which is the usual reason a fresh version "does not
-exist".
+When resolution fails: read the error, check the exact namespace and spelling against the online
+index, and run `xlings update` — a release tarball bundles an index snapshot frozen at build time,
+which is the usual reason a fresh version "does not exist".
Mirrors: `mcpp self config --mirror CN` switches to the GitCode mirror; `GLOBAL` (upstream) is
the default.
diff --git a/.agents/skills/mcpp-style-ref/SKILL.md b/.agents/skills/mcpp-style-ref/SKILL.md
index 921858d..6bad24b 100644
--- a/.agents/skills/mcpp-style-ref/SKILL.md
+++ b/.agents/skills/mcpp-style-ref/SKILL.md
@@ -149,8 +149,10 @@ mcpp --version
- `.xlings.json`:声明项目工具环境
- `mcpp.toml`:声明 `[package]` 与测试依赖;简单库目标可由 mcpp 从 `src/*.cppm` 自动推断
-- `src/cmp.cppm`:库主模块接口,声明 `export module mcpplibs.cmp;`
-- `tests/cmp_test.cpp`:`mcpp test` 自动发现的 gtest 导入测试;不要定义 `main()`
+- `src/cmp.cppm`:库主模块接口,导出 `:task` 与 `:run_loop` 分区
+- `src/task.cppm`:`Task` 与 `Task` 分区
+- `src/run_loop.cppm`:`RunLoop` 与 `Scheduler` 分区
+- `tests/cmp_test.cpp`、`tests/run_loop_test.cpp`:`mcpp test` 自动发现的 gtest 测试;不要定义 `main()`
- `examples/basic/`:独立 mcpp consumer 包,通过 path 依赖引用根库
构建:
diff --git a/.agents/skills/mcpp/SKILL.md b/.agents/skills/mcpp/SKILL.md
index f26fbfa..3076702 100644
--- a/.agents/skills/mcpp/SKILL.md
+++ b/.agents/skills/mcpp/SKILL.md
@@ -9,7 +9,7 @@ The build and package tool this repository is built with. mcpp is module-first C
`import std` works out of the box, toolchains install into an isolated sandbox, and
dependencies resolve through a package index.
-Verified against **mcpp 2026.8.1.1**. mcpp is pre-1.0 and moves fast — when this skill and
+Verified against **mcpp 2026.8.11.2**. mcpp is pre-1.0 and moves fast — when this skill and
the tool disagree, the tool wins. Check with `mcpp --help`, `mcpp --help`, and the
[upstream docs](https://github.com/mcpp-community/mcpp/tree/main/docs).
@@ -81,12 +81,12 @@ defines = ["FOO=1"] # bare names; reach every TU including module scan
default-profile = "release" # project default when no --profile/--release is passed
[dependencies] # runtime deps
-cmdline = "0.0.2" # bare name: searched in mcpplibs, then compat, then no-namespace
+cmdline = "0.0.2" # bare name: mcpplibs namespace
[dependencies.mcpplibs] # namespace sub-table (preferred for several from one org)
tinyhttps = "0.2.3"
-[dev-dependencies] # test-only; `mcpp build` ignores these
+[dev-dependencies.compat] # test-only; `mcpp build` ignores these
gtest = "1.15.2"
[toolchain]
@@ -103,10 +103,10 @@ Dependency forms: `"1.2.3"` · `"^1.2"` · `"~1.2"` · `">=1.0, <2.0"` ·
`{ path = "../mylib" }` · `{ git = "...", tag = "v1.0.0" }` ·
`{ version = "0.0.3", features = ["docking"] }`.
-**Namespace rule:** a bare name resolves in exactly three places, in order — `mcpplibs`,
-`compat` (third-party C/C++ wrappers), then packages that declare no namespace. Any other
-namespace must be written out: `"chriskohlhoff.asio" = "1.38.1"` or a
-`[dependencies.chriskohlhoff]` sub-table.
+**Namespace rule:** a bare name means `mcpplibs`. Compatibility fallback to `compat` is deprecated
+in 2026.8 and removed in 2026.9, so third-party wrappers and every other namespace must be explicit:
+`[dependencies.compat]`, `"chriskohlhoff.asio" = "1.38.1"`, or the corresponding namespace
+sub-table.
## Modules
diff --git a/.agents/skills/more-details/SKILL.md b/.agents/skills/more-details/SKILL.md
index 6c62a04..da4ad14 100644
--- a/.agents/skills/more-details/SKILL.md
+++ b/.agents/skills/more-details/SKILL.md
@@ -31,8 +31,11 @@ docs, and actual command output.
- `docs/architecture.md` (`.zh.md`, `.zh.hant.md`) — current structure, mcpp conventions, runtime boundaries, CI.
- `.xlings.json` — the project tool environment (which mcpp version builds this).
- `mcpp.toml` — package metadata, dependencies, dev-dependencies.
-- `src/cmp.cppm` — the import-only CMP root module interface.
-- `tests/cmp_test.cpp` — the gtest import smoke test.
+- `src/cmp.cppm` — the CMP root module interface, re-exporting its public partitions.
+- `src/task.cppm` — lazy single-consumer Task implementation.
+- `src/run_loop.cppm` — caller-thread RunLoop, Scheduler, and root driver.
+- `tests/cmp_test.cpp` — Task contract and lifetime tests.
+- `tests/run_loop_test.cpp` — scheduling, threading, and invalid-use boundary tests.
- `examples/basic/` — a standalone path-dependency consumer.
- `.github/workflows/ci-{linux,macos,windows}.yml` — per-platform build, test, and example CI.
@@ -87,6 +90,6 @@ mcpp --version
- **Confirm an `mcpp.toml` field** → `mcpp.toml`, the [`mcpp`](../mcpp/SKILL.md) skill, then upstream `docs/05-mcpp-toml.md`.
- **Add a dependency** → the [`mcpp-index`](../mcpp-index/SKILL.md) skill, then `mcpplibs/cmdline` for a real example.
- **Add a module API** → `mcpp-style-ref`, then edit `src/*.cppm`.
-- **Add a test** → follow `tests/cmp_test.cpp`, verify with `mcpp test`.
+- **Add a test** → follow the focused file under `tests/`, verify with `mcpp test`.
- **Add an example** → follow `examples/basic/` — its own `mcpp.toml` with a path dependency.
- **Publish the library** → the [`mcpp-index`](../mcpp-index/SKILL.md) skill.
diff --git a/.gitignore b/.gitignore
index 0f41629..2367b14 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ compile_commands.json
# Local tools and editor state
.cache/
/.clice/
+/.idea/
/.rpiv/
/.vscode/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 7f050be..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# 默认忽略的文件
-/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
deleted file mode 100644
index 962e49f..0000000
--- a/.idea/cmp.iml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/.idea/editor.xml b/.idea/editor.xml
deleted file mode 100644
index 4abd7d2..0000000
--- a/.idea/editor.xml
+++ /dev/null
@@ -1,350 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index c19669f..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index c8397c9..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.xlings.json b/.xlings.json
index b08ef31..1fb0f9c 100644
--- a/.xlings.json
+++ b/.xlings.json
@@ -1,5 +1,5 @@
{
"workspace": {
- "mcpp": "2026.8.1.1"
+ "mcpp": "2026.8.11.2"
}
}
diff --git a/README.md b/README.md
index bf1e439..b791f4b 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,9 @@
[](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml)
> [!IMPORTANT]
-> 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 provides a lazy, single-consumer `Task` / `Task` and a caller-thread `RunLoop`
+> with explicit scheduling. Cancellation, timers, asynchronous I/O, and detached execution 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,27 +72,34 @@ cd examples/basic
mcpp run
```
-The example prints `Coroutine result: 42` from inside a `Task` coroutine and exits
+The example prints `Coroutine result: 42` from inside a scheduled `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.
+`mcpplibs.cmp`, compose Tasks, and drive them through the public RunLoop.
-## Current Task API
+## Current API
```cpp
import std;
import mcpplibs.cmp;
using mcpplibs::cmp::Task;
+using mcpplibs::cmp::RunLoop;
Task answer() {
co_return 42;
}
-Task print_answer() {
+Task print_answer(RunLoop::Scheduler scheduler) {
+ co_await scheduler.schedule();
auto value = co_await answer();
std::println("Coroutine result: {}", value);
co_return;
}
+
+int main() {
+ RunLoop loop {};
+ loop.run(print_answer(loop.get_scheduler()));
+}
```
`Task` is lazy: calling `answer()` creates a suspended coroutine. It starts when consumed by
@@ -104,10 +112,16 @@ A translation unit that defines a coroutine must import `std` so the compiler ca
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.
+`RunLoop::run()` consumes one root Task, executes ready coroutines on the calling thread, returns
+its value, and rethrows its exception. `Scheduler::schedule()` always suspends and queues the
+continuation. Scheduler handles are copyable, but remain tied to their originating RunLoop.
+Sequential `run()` calls are supported; nested or concurrent calls are rejected. A moved-from Task
+must not be awaited.
+
+RunLoop is not a background thread and does not make blocking code asynchronous. A Task that
+suspends without arranging a future resume can leave `run()` waiting indefinitely. CMP does not
+provide automatic thread affinity: after an external awaiter resumes on another thread, explicitly
+await the desired Scheduler to return to its RunLoop.
## Repository Layout
@@ -116,7 +130,10 @@ Task must not be awaited.
├── .xlings.json # pinned project tool environment
├── mcpp.toml # package identity and test dependency
├── src/cmp.cppm # root module interface
+├── src/task.cppm # Task module partition
+├── src/run_loop.cppm # RunLoop and Scheduler partition
├── tests/cmp_test.cpp # Task contract and lifetime tests
+├── tests/run_loop_test.cpp # scheduler, boundary, and threading tests
├── examples/basic/ # standalone path-dependency consumer
├── docs/architecture.md # current structure, boundaries, and evolution
└── .github/workflows/ # Linux, macOS, and Windows CI
@@ -140,7 +157,7 @@ The mcpp version is pinned by `.xlings.json`; contributors should not rely on an
global mcpp installation.
CMP does not track `mcpp.lock`; `.gitignore` enforces that repository policy. Runtime dependencies
-belong in `[dependencies]`; test-only dependencies belong in `[dev-dependencies]`.
+belong in `[dependencies]`; gtest is declared explicitly under `[dev-dependencies.compat]`.
## Roadmap
@@ -148,7 +165,7 @@ Runtime work is split into independently reviewable phases:
1. package identity and importable-module bootstrap — implemented;
2. coroutine task and lifetime semantics — initial `Task` implemented;
-3. a minimal single-thread scheduler;
+3. a root runner and minimal single-thread scheduler — initially implemented;
4. timers, cancellation, and structured wake-up paths;
5. multi-worker scheduling and work stealing;
6. asynchronous I/O integration and a blocking pool.
diff --git a/README.zh.hant.md b/README.zh.hant.md
index 97892c2..d400111 100644
--- a/README.zh.hant.md
+++ b/README.zh.hant.md
@@ -16,8 +16,8 @@
[](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml)
> [!IMPORTANT]
-> CMP 已提供第一個協程基礎型別:延遲啟動、單一消費者的 `Task` / `Task`。
-> 排程、取消、計時器、非同步 I/O 和公開根任務驅動器尚未實作。
+> CMP 已提供延遲啟動、單一消費者的 `Task` / `Task`,以及在呼叫執行緒運行、
+> 支援明確排程的 `RunLoop`。取消、計時器、非同步 I/O 和 detached 執行尚未實作。
CMP 計畫以標準無堆疊 C++ 協程建構現代協程執行期與函式庫。專案將以明確的 `co_await`
為主線,透過經過驗證的小步驟逐步探索排程、計時器、非同步 I/O、取消,以及阻塞工作的安全隔離。
@@ -67,26 +67,34 @@ cd examples/basic
mcpp run
```
-範例會從 `Task` 協程內部印出 `Coroutine result: 42`,然後以成功狀態結束。它負責
-證明獨立 mcpp 套件能夠解析路徑相依、匯入 `mcpplibs.cmp`、組合 Task 並執行同步協程鏈。
+範例會從經過排程的 `Task` 協程內部印出 `Coroutine result: 42`,然後以成功狀態結束。
+它負責證明獨立 mcpp 套件能夠解析路徑相依、匯入 `mcpplibs.cmp`、組合 Task 並透過公開
+RunLoop 驅動任務。
-## 目前 Task API
+## 目前 API
```cpp
import std;
import mcpplibs.cmp;
using mcpplibs::cmp::Task;
+using mcpplibs::cmp::RunLoop;
Task answer() {
co_return 42;
}
-Task print_answer() {
+Task print_answer(RunLoop::Scheduler scheduler) {
+ co_await scheduler.schedule();
auto value = co_await answer();
std::println("Coroutine result: {}", value);
co_return;
}
+
+int main() {
+ RunLoop loop {};
+ loop.run(print_answer(loop.get_scheduler()));
+}
```
`Task` 採延遲啟動:呼叫 `answer()` 只建立處於暫停狀態的協程,在被 `co_await` 消費時才
@@ -97,9 +105,14 @@ continuation 之間直接轉移,並透過 RAII 銷毀未消費的協程框架
定義協程的轉譯單元必須匯入 `std`,使編譯器能夠看到標準協程協定型別。CMP 私下匯入
`std`,不會向使用端重新匯出整個標準函式庫。
-CMP 尚未提供 `sync_wait` 或排程器,因此目前 API 是協程組合基礎,而不是完整的應用程式入口。
-獨立範例因此定義了一個很小的私有根協程,只適用於其中同步完成的協程鏈。已經被移動的
-Task 不得再次等待。
+`RunLoop::run()` 消費一個根 Task,在呼叫執行緒執行就緒協程,回傳結果並重新拋出例外。
+`Scheduler::schedule()` 始終暫停目前協程並把 continuation 放入佇列。Scheduler 可以複製,
+但始終屬於建立它的 RunLoop。支援依序多次呼叫 `run()`,巢狀或並行呼叫會被拒絕。已經被
+移動的 Task 不得再次等待。
+
+RunLoop 不是背景執行緒,也不會把阻塞程式碼自動變成非同步程式碼。如果 Task 暫停後沒有
+安排未來的恢復動作,`run()` 可能一直等待。CMP 不提供隱式執行緒親和:外部 awaiter 在其他
+執行緒恢復協程後,需要明確等待目標 Scheduler 才會返回對應 RunLoop。
## 儲存庫結構
@@ -108,7 +121,10 @@ Task 不得再次等待。
├── .xlings.json # 固定的專案工具環境
├── mcpp.toml # 套件識別與測試相依
├── src/cmp.cppm # 根模組介面
+├── src/task.cppm # Task 模組分割區
+├── src/run_loop.cppm # RunLoop 與 Scheduler 分割區
├── tests/cmp_test.cpp # Task 契約和生命週期測試
+├── tests/run_loop_test.cpp # 排程、邊界和執行緒測試
├── examples/basic/ # 獨立的路徑相依 consumer
├── docs/architecture.zh.hant.md # 目前結構、邊界與演進方向
└── .github/workflows/ # Linux、macOS 和 Windows CI
@@ -130,7 +146,7 @@ CI 在 Linux、macOS 和 Windows 上執行等價的建構、測試與獨立範
`.xlings.json` 固定,不應依賴無關的全域 mcpp 安裝。
CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定。執行期相依放在
-`[dependencies]`,測試專用相依放在 `[dev-dependencies]`。
+`[dependencies]`,gtest 明確宣告在 `[dev-dependencies.compat]` 中。
## 路線圖
@@ -138,7 +154,7 @@ CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定
1. 套件識別與可匯入模組 bootstrap——已完成;
2. 協程 task 與生命週期語意——已實作初始 `Task`;
-3. 最小單執行緒排程器;
+3. 根任務驅動器和最小單執行緒排程器——已完成初始實作;
4. 計時器、取消和結構化喚醒路徑;
5. 多 worker 排程與 work stealing;
6. 非同步 I/O 整合和 blocking pool。
diff --git a/README.zh.md b/README.zh.md
index 4b93282..71a0335 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -16,8 +16,8 @@
[](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml)
> [!IMPORTANT]
-> CMP 已经提供第一个协程基础类型:懒启动、单消费者的 `Task` / `Task`。
-> 调度、取消、定时器、异步 I/O 和公共根任务驱动器尚未实现。
+> CMP 已提供懒启动、单消费者的 `Task` / `Task`,以及在调用线程运行、支持显式
+> 调度的 `RunLoop`。取消、定时器、异步 I/O 和 detached 执行尚未实现。
CMP 计划基于标准无栈 C++ 协程构建现代协程运行时和库。项目将以显式 `co_await` 为主线,
通过经过验证的小步骤逐步探索调度、定时器、异步 I/O、取消以及阻塞工作的安全隔离。
@@ -67,26 +67,34 @@ cd examples/basic
mcpp run
```
-示例会从 `Task` 协程内部打印 `Coroutine result: 42`,然后以成功状态退出。它负责
-证明独立 mcpp 包能够解析路径依赖、导入 `mcpplibs.cmp`、组合 Task 并执行同步协程链。
+示例会从经过调度的 `Task` 协程内部打印 `Coroutine result: 42`,然后以成功状态退出。
+它负责证明独立 mcpp 包能够解析路径依赖、导入 `mcpplibs.cmp`、组合 Task 并通过公共
+RunLoop 驱动任务。
-## 当前 Task API
+## 当前 API
```cpp
import std;
import mcpplibs.cmp;
using mcpplibs::cmp::Task;
+using mcpplibs::cmp::RunLoop;
Task answer() {
co_return 42;
}
-Task print_answer() {
+Task print_answer(RunLoop::Scheduler scheduler) {
+ co_await scheduler.schedule();
auto value = co_await answer();
std::println("Coroutine result: {}", value);
co_return;
}
+
+int main() {
+ RunLoop loop {};
+ loop.run(print_answer(loop.get_scheduler()));
+}
```
`Task` 采用懒启动:调用 `answer()` 只创建处于挂起状态的协程,在被 `co_await` 消费时才
@@ -97,9 +105,14 @@ continuation 之间直接转移,并通过 RAII 销毁未消费的协程帧。
定义协程的翻译单元必须导入 `std`,使编译器能够看到标准协程协议类型。CMP 私有导入
`std`,不会向使用方重新导出整个标准库。
-CMP 尚未提供 `sync_wait` 或调度器,因此当前 API 是协程组合基础,而不是完整的应用入口。
-独立示例因此定义了一个很小的私有根协程,只适用于其中同步完成的协程链。已经被移动的
-Task 不得再次等待。
+`RunLoop::run()` 消费一个根 Task,在调用线程执行就绪协程,返回结果并重新抛出异常。
+`Scheduler::schedule()` 始终挂起当前协程并把 continuation 放入队列。Scheduler 可以复制,
+但始终属于创建它的 RunLoop。支持顺序多次调用 `run()`,嵌套或并发调用会被拒绝。已经被
+移动的 Task 不得再次等待。
+
+RunLoop 不是后台线程,也不会把阻塞代码自动变成异步代码。如果 Task 挂起后没有安排未来的
+恢复动作,`run()` 可能一直等待。CMP 不提供隐式线程亲和:外部 awaiter 在其他线程恢复协程
+后,需要显式等待目标 Scheduler 才会返回对应 RunLoop。
## 仓库结构
@@ -108,7 +121,10 @@ Task 不得再次等待。
├── .xlings.json # 固定的项目工具环境
├── mcpp.toml # 包身份和测试依赖
├── src/cmp.cppm # 根模块接口
+├── src/task.cppm # Task 模块分区
+├── src/run_loop.cppm # RunLoop 与 Scheduler 分区
├── tests/cmp_test.cpp # Task 契约和生命周期测试
+├── tests/run_loop_test.cpp # 调度、边界和线程测试
├── examples/basic/ # 独立的路径依赖 consumer
├── docs/architecture.zh.md # 当前结构、边界和演进方向
└── .github/workflows/ # Linux、macOS 和 Windows CI
@@ -130,7 +146,7 @@ CI 在 Linux、macOS 和 Windows 上执行等价的构建、测试和独立示
`.xlings.json` 固定,不应依赖无关的全局 mcpp 安装。
CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。运行时依赖放在
-`[dependencies]`,测试专用依赖放在 `[dev-dependencies]`。
+`[dependencies]`,gtest 明确声明在 `[dev-dependencies.compat]` 中。
## 路线图
@@ -138,7 +154,7 @@ CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。
1. 包身份和可导入模块 bootstrap——已完成;
2. 协程 task 与生命周期语义——已实现初始 `Task`;
-3. 最小单线程调度器;
+3. 根任务驱动器和最小单线程调度器——已完成初始实现;
4. 定时器、取消和结构化唤醒路径;
5. 多 worker 调度与 work stealing;
6. 异步 I/O 集成和 blocking pool。
diff --git a/docs/architecture.md b/docs/architecture.md
index aacc3fd..5ae155e 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -4,15 +4,16 @@
## Current status
-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.
+CMP is a C++23 module project with a small coroutine execution core. The root module exports a
+lazy, single-consumer `mcpplibs::cmp::Task`, `RunLoop`, and its copyable `Scheduler` handle.
+`RunLoop::run()` is the public root execution boundary, while `Scheduler::schedule()` explicitly
+returns a suspended coroutine to that loop.
The repository contains:
- one mcpp package manifest;
-- the root module `mcpplibs.cmp` and its Task implementation;
-- gtest contract, lifetime, exception, and symmetric-transfer tests;
+- the root module `mcpplibs.cmp` with Task and RunLoop partitions;
+- gtest contract, lifetime, exception, scheduling, and threading tests;
- one standalone path-dependency example;
- Linux, macOS, and Windows CI workflows.
@@ -38,8 +39,8 @@ There is no `src/main.cpp`, so mcpp infers a library target named `cmp`; the man
need a `[lib]` or `[targets.cmp]` override. The C++23 baseline is written explicitly even though
it is also mcpp's default.
-`gtest = "1.15.2"` is a development dependency used by the test target. CMP does not track an
-`mcpp.lock` file; it is excluded by `.gitignore`.
+`compat.gtest = "1.15.2"` is an explicitly namespaced development dependency used by the test
+targets. CMP does not track an `mcpp.lock` file; it is excluded by `.gitignore`.
## Repository layout
@@ -57,17 +58,23 @@ it is also mcpp's default.
├── examples/basic/
│ ├── mcpp.toml
│ └── src/main.cpp
-├── src/cmp.cppm
-├── tests/cmp_test.cpp
+├── src/
+│ ├── cmp.cppm
+│ ├── task.cppm
+│ └── run_loop.cppm
+├── tests/
+│ ├── cmp_test.cpp
+│ └── run_loop_test.cpp
└── mcpp.toml
```
## 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 the
-Task type contract, lazy execution, frame ownership, value and exception propagation, nested
-composition, and stack-safe symmetric transfer.
+target. `mcpp test` discovers both test files and links a gtest entry point for each. The tests
+verify Task ownership and symmetric transfer together with root execution, scheduling, exception
+propagation, cross-thread wake-up, invalid scheduler use, loop reuse, and stack-safe repeated
+scheduling.
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
@@ -102,22 +109,29 @@ import std;
import mcpplibs.cmp;
using mcpplibs::cmp::Task;
+using mcpplibs::cmp::RunLoop;
Task answer() {
co_return 42;
}
-Task print_answer() {
+Task print_answer(RunLoop::Scheduler scheduler) {
+ co_await scheduler.schedule();
auto value = co_await answer();
std::println("Coroutine result: {}", value);
co_return;
}
+
+int main() {
+ RunLoop loop {};
+ loop.run(print_answer(loop.get_scheduler()));
+}
```
-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.
+This example checks path dependency resolution, module consumption, external coroutine
+compilation, and the public root runner independently of the root test targets. The RunLoop drives
+`print_answer()` on the main thread; the explicit scheduling point is reached before the coroutine
+prints `Coroutine result: 42`.
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`
@@ -136,9 +150,31 @@ privately rather than re-exporting the entire standard library.
- 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.
+contract violation.
+
+`RunLoop` and `Scheduler` have the following contract:
+
+- RunLoop is neither copyable nor movable; its identity anchors every Scheduler it creates;
+- `run(Task)` consumes one root Task and runs ready continuations on the calling thread;
+- a root value, including a move-only value, is returned; a root exception is rethrown;
+- a RunLoop can be reused sequentially, but nested and concurrent `run()` calls throw
+ `std::logic_error`;
+- `schedule()` always suspends and appends its continuation to a thread-safe FIFO ready queue;
+- producers may enqueue from other threads, but only the thread inside `run()` consumes the queue;
+- using a Scheduler after its RunLoop is destroyed, or while its own RunLoop is not active, throws
+ `std::logic_error` from the await expression;
+- root completion with separately queued work is rejected because detached ownership is not part
+ of this phase.
+
+RunLoop does not own a worker thread and supplies no automatic thread affinity. An external
+awaiter may resume a Task on another thread; awaiting the original Scheduler explicitly returns
+the continuation to its RunLoop. A Task that suspends without arranging another thread or event
+source to resume it can leave `run()` blocked indefinitely. Blocking functions still block the
+thread on which the coroutine currently executes.
+
+There is no public free-standing `sync_wait`, detached execution, 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
@@ -153,16 +189,16 @@ 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. 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;
-7. result adapters and optional coroutine-frame allocation strategies.
+1. structured task scopes and concurrent joins;
+2. timers, wake-up paths, and cancellation;
+3. multi-worker scheduling and work stealing;
+4. asynchronous I/O integrations;
+5. a dedicated pool for unavoidable blocking work;
+6. result adapters and optional coroutine-frame allocation strategies.
-Module partitions or implementation units can be added when an implemented API needs those
-boundaries.
+Task and RunLoop now occupy separate module partitions because they are implemented public
+boundaries. Further partitions or implementation units are added only when another implemented API
+needs them.
## Verification
@@ -175,9 +211,10 @@ cd examples/basic
mcpp run
```
-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
+The expected result is a successful library build, 22 passing tests across two binaries, and an
+example that prints `Coroutine result: 42` and exits with status 0. One test performs one million
+immediate Task completions; another performs 100,000 explicit scheduling operations. These check
+that neither symmetric transfer nor queued scheduling grows 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 78aaf8d..8da5795 100644
--- a/docs/architecture.zh.hant.md
+++ b/docs/architecture.zh.hant.md
@@ -4,14 +4,15 @@
## 目前狀態
-CMP 是一個 C++23 模組專案,已實作第一個執行期基礎型別。根模組匯出延遲啟動、單一消費者的
-`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供排程器或根任務執行 API。
+CMP 是一個具備小型協程執行核心的 C++23 模組專案。根模組匯出延遲啟動、單一消費者的
+`mcpplibs::cmp::Task`、`RunLoop` 及其可複製的 `Scheduler` 控制代碼。`RunLoop::run()`
+是公開根任務執行邊界,`Scheduler::schedule()` 用於明確地把暫停協程送回對應執行迴圈。
儲存庫現有內容包括:
- 一份 mcpp 套件清單;
-- 根模組 `mcpplibs.cmp` 及其 Task 實作;
-- 涵蓋契約、生命週期、例外和對稱轉移的 gtest 測試;
+- 根模組 `mcpplibs.cmp` 及 Task、RunLoop 模組分割區;
+- 涵蓋契約、生命週期、例外、排程和執行緒行為的 gtest 測試;
- 一個透過路徑相依使用根套件的獨立範例;
- Linux、macOS 和 Windows 三套 CI 工作流程。
@@ -35,8 +36,8 @@ mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.m
`src/main.cpp`,因此 mcpp 會推斷出名為 `cmp` 的函式庫目標,不需要額外設定 `[lib]` 或
`[targets.cmp]`。雖然 C++23 也是 mcpp 的預設標準,清單中仍明確寫出這項基線。
-`gtest = "1.15.2"` 是測試使用的開發相依。CMP 目前不追蹤 `mcpp.lock`,該檔案由
-`.gitignore` 排除。
+`compat.gtest = "1.15.2"` 是測試使用的明確命名空間開發相依。CMP 目前不追蹤
+`mcpp.lock`,該檔案由 `.gitignore` 排除。
## 儲存庫結構
@@ -54,16 +55,22 @@ mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.m
├── examples/basic/
│ ├── mcpp.toml
│ └── src/main.cpp
-├── src/cmp.cppm
-├── tests/cmp_test.cpp
+├── src/
+│ ├── cmp.cppm
+│ ├── task.cppm
+│ └── run_loop.cppm
+├── tests/
+│ ├── cmp_test.cpp
+│ └── run_loop_test.cpp
└── mcpp.toml
```
## 建置與測試
`.xlings.json` 固定專案使用的 mcpp 版本。`mcpp build` 建置自動推斷的函式庫目標。
-`mcpp test` 會找到 `tests/cmp_test.cpp`,連結 gtest 進入點,並驗證 Task 型別契約、延遲執行、
-協程框架所有權、值與例外傳播、巢狀組合,以及不會增長呼叫堆疊的對稱轉移。
+`mcpp test` 會找到兩個測試檔案,並為每個檔案連結 gtest 進入點。測試同時驗證 Task 所有權
+和對稱轉移,以及根任務執行、排程、例外傳播、跨執行緒喚醒、無效 Scheduler、RunLoop
+重複使用和不會增長呼叫堆疊的重複排程。
三套 CI 工作流程都會安裝專案工具、建置函式庫、執行測試並執行 `examples/basic`。不同
作業系統的工具安裝和執行環境不同,因此分別保留工作流程檔案。
@@ -96,21 +103,28 @@ import std;
import mcpplibs.cmp;
using mcpplibs::cmp::Task;
+using mcpplibs::cmp::RunLoop;
Task answer() {
co_return 42;
}
-Task print_answer() {
+Task print_answer(RunLoop::Scheduler scheduler) {
+ co_await scheduler.schedule();
auto value = co_await answer();
std::println("Coroutine result: {}", value);
co_return;
}
+
+int main() {
+ RunLoop loop {};
+ loop.run(print_answer(loop.get_scheduler()));
+}
```
-這個範例在根測試目標之外,單獨檢查路徑相依解析、模組使用和外部協程編譯。範例私有的
-`InlineRunner` 啟動一個 eager 根協程,等待 `print_answer()` 並輸出 `Coroutine result: 42`。
-這個輔助型別只適用於目前同步完成且沒有排程器的協程鏈,不屬於 CMP 公共 API。
+這個範例在根測試目標之外,單獨檢查路徑相依解析、模組使用、外部協程編譯和公開根任務
+驅動器。RunLoop 在主執行緒驅動 `print_answer()`;協程經過明確排程點後輸出
+`Coroutine result: 42`。
任何定義協程的轉譯單元都要自行匯入 `std`,使 `std::coroutine_traits` 和標準協程協定型別
參與編譯。CMP 模組私下匯入 `std`,而不是向使用端重新匯出整個標準函式庫。
@@ -127,8 +141,27 @@ Task print_answer() {
- 未消費的 Task 銷毀自己的框架,消費它的 awaiter 銷毀已完成的框架;
- 拒絕參考和陣列結果型別。
-被移動後的 Task 為空,不得再次等待;目前實作會在違反該契約時終止程序。目前沒有公開
-`sync_wait`、detached 執行、排程器、執行緒親和保證、計時器、取消機制、非同步 I/O 後端、
+被移動後的 Task 為空,不得再次等待;目前實作會在違反該契約時終止程序。
+
+`RunLoop` 和 `Scheduler` 遵循以下契約:
+
+- RunLoop 既不能複製也不能移動;它的身分是所有關聯 Scheduler 的生命週期錨點;
+- `run(Task)` 消費一個根 Task,並在呼叫執行緒執行就緒 continuation;
+- 回傳根任務結果,包括 move-only 結果;根任務例外會重新拋出;
+- RunLoop 可以依序重複使用,但巢狀或並行呼叫 `run()` 會拋出 `std::logic_error`;
+- `schedule()` 始終暫停,並把 continuation 追加到執行緒安全的 FIFO 就緒佇列;
+- 其他執行緒可以入列,但只有正在執行 `run()` 的執行緒會消費佇列;
+- RunLoop 銷毀後繼續使用其 Scheduler,或在其所屬 RunLoop 未運行時使用 Scheduler,皆會
+ 從 await 運算式拋出 `std::logic_error`;
+- 根任務完成時若仍有單獨排隊的工作,RunLoop 會拒絕退出,因為本階段不支援 detached
+ 所有權。
+
+RunLoop 不擁有工作執行緒,也不提供自動執行緒親和。外部 awaiter 可以在其他執行緒恢復
+Task;明確等待原 Scheduler 才會把 continuation 送回對應 RunLoop。如果 Task 暫停後沒有
+安排其他執行緒或事件來源恢復它,`run()` 可能無限等待。阻塞函式仍會阻塞協程目前所在的
+執行緒。
+
+目前沒有公開自由函式 `sync_wait`、detached 執行、計時器、取消機制、非同步 I/O 後端、
自訂協程框架 allocator 或阻塞工作執行緒池,也沒有保留舊骨架模組的相容別名。
捕捉變數的協程 lambda 需要特別小心:立即呼叫一個暫時的捕捉 lambda,可能使延遲協程參考
@@ -142,15 +175,15 @@ CMP 名稱中的 `C` 與 Go 執行期中的 `G` 相呼應,但這只說明命
以下方向可以分別設計和審查,目前都不是套件的既有約定:
-1. 根任務驅動器、最小單執行緒排程器和明確的排程 awaiter;
-2. 結構化任務作用域和並行匯合;
-3. 計時器、喚醒路徑和取消;
-4. 多工作執行緒排程和工作竊取;
-5. 非同步 I/O 整合;
-6. 處理無法避免之阻塞工作的專用執行緒池;
-7. 結果適配器和可選的協程框架配置策略。
+1. 結構化任務作用域和並行匯合;
+2. 計時器、喚醒路徑和取消;
+3. 多工作執行緒排程和工作竊取;
+4. 非同步 I/O 整合;
+5. 處理無法避免之阻塞工作的專用執行緒池;
+6. 結果適配器和可選的協程框架配置策略。
-只有已實作的 API 確實需要新邊界時,才增加模組分割區或實作單元。
+Task 與 RunLoop 已形成真實的公開邊界,因此分別位於模組分割區中。只有其他已實作 API
+確實需要新邊界時,才繼續增加模組分割區或實作單元。
## 驗證
@@ -163,7 +196,8 @@ cd examples/basic
mcpp run
```
-預期結果是函式庫建置成功、八個 Task 測試通過,而且範例以狀態 0 結束。其中一個測試執行
-一百萬次立即完成的 Task,用於檢查對稱轉移不會增長原生呼叫堆疊。目前 Windows LLVM
-工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能沿用舊的 BMI
-或目的檔。完整複驗時使用 `--cache=off`。
+預期結果是函式庫建置成功、兩個二進位檔中的 22 項測試全部通過,而且範例輸出
+`Coroutine result: 42` 後以狀態 0 結束。一個測試執行一百萬次立即完成的 Task,另一個
+測試執行十萬次明確排程,用於檢查對稱轉移和佇列排程都不會增長原生呼叫堆疊。目前
+Windows LLVM 工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能
+沿用舊的 BMI 或目的檔。完整複驗時使用 `--cache=off`。
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index 1c85b21..85c3071 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -4,14 +4,15 @@
## 当前状态
-CMP 是一个 C++23 模块项目,已经实现首个运行时基础类型。根模块导出懒启动、单消费者的
-`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供调度器或根任务执行 API。
+CMP 是一个具备小型协程执行核心的 C++23 模块项目。根模块导出懒启动、单消费者的
+`mcpplibs::cmp::Task`、`RunLoop` 及其可复制的 `Scheduler` 句柄。`RunLoop::run()` 是
+公共根任务执行边界,`Scheduler::schedule()` 用于显式地把挂起协程送回对应运行循环。
仓库现有内容包括:
- 一份 mcpp 包清单;
-- 根模块 `mcpplibs.cmp` 及其 Task 实现;
-- 覆盖契约、生命周期、异常和对称转移的 gtest 测试;
+- 根模块 `mcpplibs.cmp` 及 Task、RunLoop 模块分区;
+- 覆盖契约、生命周期、异常、调度和线程行为的 gtest 测试;
- 一个通过路径依赖使用根包的独立示例;
- Linux、macOS 和 Windows 三套 CI 工作流。
@@ -35,8 +36,8 @@ mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpp
`src/main.cpp`,因此 mcpp 会推断出名为 `cmp` 的库目标,不需要额外配置 `[lib]` 或
`[targets.cmp]`。虽然 C++23 也是 mcpp 的默认标准,清单中仍然明确写出这一基线。
-`gtest = "1.15.2"` 是测试使用的开发依赖。CMP 当前不跟踪 `mcpp.lock`,该文件由
-`.gitignore` 排除。
+`compat.gtest = "1.15.2"` 是测试使用的显式命名空间开发依赖。CMP 当前不跟踪
+`mcpp.lock`,该文件由 `.gitignore` 排除。
## 仓库结构
@@ -54,16 +55,22 @@ mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpp
├── examples/basic/
│ ├── mcpp.toml
│ └── src/main.cpp
-├── src/cmp.cppm
-├── tests/cmp_test.cpp
+├── src/
+│ ├── cmp.cppm
+│ ├── task.cppm
+│ └── run_loop.cppm
+├── tests/
+│ ├── cmp_test.cpp
+│ └── run_loop_test.cpp
└── mcpp.toml
```
## 构建与测试
`.xlings.json` 固定项目使用的 mcpp 版本。`mcpp build` 构建自动推断的库目标。
-`mcpp test` 发现 `tests/cmp_test.cpp`,链接 gtest 入口,并验证 Task 类型契约、懒执行、
-协程帧所有权、值与异常传播、嵌套组合以及不会增长调用栈的对称转移。
+`mcpp test` 发现两个测试文件,并为每个文件链接 gtest 入口。测试同时验证 Task 所有权和
+对称转移,以及根任务执行、调度、异常传播、跨线程唤醒、无效 Scheduler、RunLoop 复用和
+不会增长调用栈的重复调度。
三套 CI 工作流都会安装项目工具、构建库、运行测试并执行 `examples/basic`。不同操作系统
的工具安装和运行环境不同,因此分别保留工作流文件。
@@ -96,21 +103,28 @@ import std;
import mcpplibs.cmp;
using mcpplibs::cmp::Task;
+using mcpplibs::cmp::RunLoop;
Task answer() {
co_return 42;
}
-Task print_answer() {
+Task print_answer(RunLoop::Scheduler scheduler) {
+ co_await scheduler.schedule();
auto value = co_await answer();
std::println("Coroutine result: {}", value);
co_return;
}
+
+int main() {
+ RunLoop loop {};
+ loop.run(print_answer(loop.get_scheduler()));
+}
```
-该示例在根测试目标之外,单独检查路径依赖解析、模块使用和外部协程编译。示例私有的
-`InlineRunner` 启动一个 eager 根协程,等待 `print_answer()` 并输出 `Coroutine result: 42`。
-这个辅助类型只适用于当前同步完成且没有调度器的协程链,不属于 CMP 公共 API。
+该示例在根测试目标之外,单独检查路径依赖解析、模块使用、外部协程编译和公共根任务驱动器。
+RunLoop 在主线程驱动 `print_answer()`;协程经过显式调度点后输出
+`Coroutine result: 42`。
任何定义协程的翻译单元都要自行导入 `std`,使 `std::coroutine_traits` 和标准协程协议类型
参与编译。CMP 模块私有导入 `std`,而不是向使用方重新导出整个标准库。
@@ -127,8 +141,26 @@ Task print_answer() {
- 未消费的 Task 销毁自己的帧,消费它的 awaiter 销毁已经完成的帧;
- 拒绝引用和数组结果类型。
-被移动后的 Task 为空,不得再次等待;当前实现会在违反该契约时终止进程。目前没有公共
-`sync_wait`、detached 执行、调度器、线程亲和保证、定时器、取消机制、异步 I/O 后端、
+被移动后的 Task 为空,不得再次等待;当前实现会在违反该契约时终止进程。
+
+`RunLoop` 和 `Scheduler` 遵循以下契约:
+
+- RunLoop 既不能复制也不能移动;它的身份是所有关联 Scheduler 的生命周期锚点;
+- `run(Task)` 消费一个根 Task,并在调用线程执行就绪 continuation;
+- 返回根任务结果,包括 move-only 结果;根任务异常会重新抛出;
+- RunLoop 可以顺序复用,但嵌套或并发调用 `run()` 会抛出 `std::logic_error`;
+- `schedule()` 始终挂起,并把 continuation 追加到线程安全的 FIFO 就绪队列;
+- 其他线程可以入队,但只有正在执行 `run()` 的线程会消费队列;
+- RunLoop 销毁后继续使用其 Scheduler,或在其所属 RunLoop 未运行时使用 Scheduler,都会
+ 从 await 表达式抛出 `std::logic_error`;
+- 根任务完成时如果仍存在单独排队的工作,RunLoop 会拒绝退出,因为本阶段不支持 detached
+ 所有权。
+
+RunLoop 不拥有工作线程,也不提供自动线程亲和。外部 awaiter 可以在其他线程恢复 Task;
+显式等待原 Scheduler 才会把 continuation 送回对应 RunLoop。如果 Task 挂起后没有安排
+其他线程或事件源恢复它,`run()` 可能无限等待。阻塞函数仍会阻塞协程当前所在的线程。
+
+目前没有公共自由函数 `sync_wait`、detached 执行、定时器、取消机制、异步 I/O 后端、
自定义协程帧 allocator 或阻塞任务线程池,也没有保留旧脚手架模块的兼容别名。
捕获变量的协程 lambda 需要特别小心:立即调用一个临时的捕获 lambda,可能使懒协程引用
@@ -141,15 +173,15 @@ CMP 名称中的 `C` 与 Go 运行时中的 `G` 相呼应,但这只说明命
以下方向可以分别设计和评审,目前都不是包的既有约定:
-1. 根任务驱动器、最小单线程调度器和显式调度 awaiter;
-2. 结构化任务作用域和并发汇合;
-3. 定时器、唤醒路径和取消;
-4. 多工作线程调度和工作窃取;
-5. 异步 I/O 集成;
-6. 处理不可避免的阻塞工作的专用线程池;
-7. 结果适配器和可选的协程帧分配策略。
+1. 结构化任务作用域和并发汇合;
+2. 定时器、唤醒路径和取消;
+3. 多工作线程调度和工作窃取;
+4. 异步 I/O 集成;
+5. 处理不可避免的阻塞工作的专用线程池;
+6. 结果适配器和可选的协程帧分配策略。
-只有已实现的 API 确实需要新的边界时,才增加模块分区或实现单元。
+Task 与 RunLoop 已经形成真实的公共边界,因此分别位于模块分区中。只有其他已实现 API
+确实需要新边界时,才继续增加模块分区或实现单元。
## 验证
@@ -162,7 +194,8 @@ cd examples/basic
mcpp run
```
-预期结果是库构建成功、八个 Task 测试通过,并且示例以状态 0 退出。其中一个测试执行一百万次
-立即完成的 Task,用于检查对称转移不会增长原生调用栈。当前 Windows LLVM 工具链不会生成
-GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI 或目标文件。
-完整复验时使用 `--cache=off`。
+预期结果是库构建成功、两个二进制中的 22 项测试全部通过,并且示例输出
+`Coroutine result: 42` 后以状态 0 退出。一个测试执行一百万次立即完成的 Task,另一个测试
+执行十万次显式调度,用于检查对称转移和队列调度都不会增长原生调用栈。当前 Windows LLVM
+工具链不会生成 GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI
+或目标文件。完整复验时使用 `--cache=off`。
diff --git a/docs/superpowers/plans/2026-08-12-cmp-run-loop.md b/docs/superpowers/plans/2026-08-12-cmp-run-loop.md
new file mode 100644
index 0000000..7be25bd
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-12-cmp-run-loop.md
@@ -0,0 +1,41 @@
+# CMP RunLoop v1 Implementation Plan
+
+**Goal:** Add the smallest scheduler-aware root execution API that makes Task convenient to use
+while keeping lifetime, synchronization, and failure handling inside CMP.
+
+**Toolchain:** C++23 Modules, mcpp/xlings 2026.8.11.2, LLVM 22.1.8, compat.gtest 1.15.2.
+
+**Git constraint:** Work on `feature/run-loop-v1`; do not stage, commit, or push. The user performs
+the commit.
+
+## Tasks
+
+- [x] Clean the inherited project state, ignore `.idea/`, pin mcpp 2026.8.11.2, and make the gtest
+ namespace explicit for current mcpp dependency rules.
+- [x] Move the unchanged Task implementation into `mcpplibs.cmp:task` and keep the root module as
+ the public re-export point.
+- [x] Implement `RunLoop`, `RunLoop::Scheduler`, the root coroutine bridge, FIFO ready queue,
+ condition-variable wake-up, exception propagation, and RAII run-state recovery.
+- [x] Add focused tests for values, void, move-only results, exceptions, scheduling, cross-thread
+ wake-up, direct external-thread completion, sequential reuse, nested/concurrent runs, invalid
+ Scheduler lifetime, outstanding work, and repeated scheduling.
+- [x] Replace the example-private root coroutine with the public RunLoop and print from inside the
+ scheduled Task.
+- [x] Update English, Simplified Chinese, and Traditional Chinese README/architecture documents
+ and repository-local development skills.
+- [x] Run strict clean build, all tests, release build, standalone consumer, diff validation, and
+ final repository status review.
+
+## Required Verification
+
+```bash
+mcpp --version
+mcpp build --strict --cache=off
+mcpp test --strict --cache=off
+mcpp build --release --strict --cache=off
+cd examples/basic
+mcpp run
+git diff --check
+```
+
+The RunLoop test binary is also repeated 100 times to exercise concurrency and wake-up paths.
diff --git a/docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md b/docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md
new file mode 100644
index 0000000..9dab970
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md
@@ -0,0 +1,121 @@
+# CMP RunLoop v1 Design
+
+**Date:** 2026-08-12
+**Status:** Implemented and verified, pending user commit
+**Base:** `feature/task-v1` at `7e24ee0`
+
+## Purpose
+
+Turn `Task` from a composition-only primitive into a usable application root without adding a
+thread pool, cancellation model, or detached ownership. Developers should only need to create a
+RunLoop, pass its Scheduler where an explicit scheduling point is needed, and call `run()`.
+
+## Public API
+
+```cpp
+RunLoop loop {};
+auto scheduler = loop.get_scheduler();
+auto result = loop.run(operation(scheduler));
+```
+
+The root module exports:
+
+- non-copyable, non-movable `RunLoop`;
+- copyable `RunLoop::Scheduler` handles with identity comparison;
+- `Scheduler::schedule()`, an awaitable that always suspends and enqueues the continuation;
+- `RunLoop::run(Task)`, returning `T`, supporting `void` and move-only values, and rethrowing
+ the root exception.
+
+There is no free-standing `sync_wait()` in this phase. A RunLoop member keeps the blocking root
+boundary and the Scheduler that can make progress explicitly connected.
+
+## Execution Model
+
+RunLoop owns a shared internal state with a mutex, condition variable, and FIFO deque of coroutine
+handles. Scheduler contains only a weak reference to that state. This gives invalid lifetime use a
+defined exception instead of leaving a dangling RunLoop pointer.
+
+`run()` performs these steps:
+
+1. atomically claims the RunLoop for one active root;
+2. creates an internal lazy root coroutine that awaits the supplied Task;
+3. enqueues that root and consumes ready handles on the calling thread;
+4. waits on the condition variable when no handle is ready;
+5. publishes the root value or exception under the same synchronization boundary;
+6. releases the RunLoop for sequential reuse and destroys the root frame through RAII.
+
+Task-to-Task completion continues to use the existing symmetric-transfer contract. Explicit
+`schedule()` always goes through the queue, including on the RunLoop thread, so it is a real yield
+point and does not grow the native stack.
+
+## Threading and Lifetime Boundaries
+
+- One thread at a time may execute `run()` for a RunLoop.
+- Other threads may enqueue a suspended continuation through its Scheduler.
+- Only the thread executing `run()` consumes the ready queue.
+- A root may complete on an external thread; completion publication wakes the RunLoop thread.
+- RunLoop supplies no hidden thread affinity. Code explicitly awaits a Scheduler to return to it.
+- A Scheduler is valid only while its RunLoop exists and while that RunLoop has an active root.
+- Destroying or externally resuming a coroutine after publishing its handle remains the owner's
+ responsibility; RunLoop stores scheduling references, not frame ownership.
+
+## Error Policy
+
+The root Task's own exception is rethrown by `run()`. Detectable contract violations throw
+`std::logic_error`:
+
+- nested or concurrent `run()`;
+- scheduling on a RunLoop that is not active;
+- scheduling after the RunLoop was destroyed;
+- completing the root while separately queued work remains;
+- encountering an empty, completed, or otherwise non-runnable queued handle.
+
+Queue allocation failure propagates normally. RAII resets the active-run flag and drops queued
+references before the exception leaves `run()`.
+
+A Task that suspends without arranging a future resume can keep `run()` blocked indefinitely.
+This is expected event-loop behavior; without timers, cancellation, or a work registry, CMP cannot
+distinguish a valid external wait from a deadlock.
+
+## Module Structure
+
+```text
+src/cmp.cppm root module, re-exports public partitions
+src/task.cppm existing Task implementation
+src/run_loop.cppm RunLoop, Scheduler, root bridge, and queue state
+```
+
+The implementation imports `std` privately. Consumers defining coroutine functions still import
+`std` themselves for the standard coroutine protocol.
+
+## Deliberately Excluded
+
+- detached tasks, spawn, and public queue submission;
+- `finish()`, `poll()`, `run_one()`, or early stop;
+- a dedicated background thread or thread pool;
+- timers, cancellation, I/O, and blocking-work isolation;
+- automatic Scheduler propagation or thread-local current Scheduler;
+- custom allocators and lock-free queues.
+
+These require ownership or shutdown semantics that do not exist yet.
+
+## Reference Direction
+
+The separation between a run loop and a cheap Scheduler follows the proven shape used by
+[stdexec](https://github.com/NVIDIA/stdexec/blob/main/include/stdexec/__detail/__run_loop.hpp) and
+[libunifex](https://github.com/facebookexperimental/libunifex/blob/main/include/unifex/manual_event_loop.hpp).
+The root coroutine/result bridge follows the core technique used by
+[cppcoro](https://github.com/lewissbaker/cppcoro/blob/master/include/cppcoro/detail/sync_wait_task.hpp),
+while driving the loop during the blocking boundary follows
+[Folly](https://github.com/facebook/folly/blob/main/folly/coro/BlockingWait.h). CMP keeps only the
+parts required by its current Task contract.
+
+## Acceptance Criteria
+
+- Task value, void, move-only result, and exception roots work.
+- Scheduling on and back to the calling thread works.
+- Cross-thread enqueue and external-thread root completion wake the loop safely.
+- Nested, concurrent, expired, wrong-loop, and outstanding-work cases are rejected.
+- A RunLoop remains reusable after successful runs and handled failures.
+- Repeated scheduling does not grow the stack.
+- The standalone consumer prints from inside a scheduled coroutine using only public CMP APIs.
diff --git a/examples/basic/src/main.cpp b/examples/basic/src/main.cpp
index b70cdc2..fe2ffdc 100644
--- a/examples/basic/src/main.cpp
+++ b/examples/basic/src/main.cpp
@@ -2,48 +2,21 @@ 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 {};
-}
+using mcpplibs::cmp::RunLoop;
Task answer() {
co_return 42;
}
-Task print_answer() {
+Task print_answer(RunLoop::Scheduler scheduler) {
+ co_await scheduler.schedule();
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());
+ RunLoop loop {};
+ loop.run(print_answer(loop.get_scheduler()));
return 0;
}
diff --git a/mcpp.toml b/mcpp.toml
index 47ad837..d58eccc 100644
--- a/mcpp.toml
+++ b/mcpp.toml
@@ -8,5 +8,5 @@ license = "Apache-2.0"
authors = ["mcpplibs"]
repo = "https://github.com/mcpplibs/cmp"
-[dev-dependencies]
+[dev-dependencies.compat]
gtest = "1.15.2"
diff --git a/src/cmp.cppm b/src/cmp.cppm
index 36aca08..b1f1a85 100644
--- a/src/cmp.cppm
+++ b/src/cmp.cppm
@@ -1,260 +1,4 @@
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
+export import :task;
+export import :run_loop;
diff --git a/src/run_loop.cppm b/src/run_loop.cppm
new file mode 100644
index 0000000..5dbe462
--- /dev/null
+++ b/src/run_loop.cppm
@@ -0,0 +1,333 @@
+export module mcpplibs.cmp:run_loop;
+
+import std;
+import :task;
+
+namespace mcpplibs::cmp::detail {
+
+struct RootCompletionBase {
+ bool completed_ { false };
+ std::exception_ptr exception_ {};
+};
+
+template
+struct RootCompletion final : RootCompletionBase {
+ std::optional result_ {};
+};
+
+template<>
+struct RootCompletion final : RootCompletionBase {};
+
+class RunLoopState final {
+private:
+ std::mutex mutex_ {};
+ std::condition_variable condition_ {};
+ std::deque> ready_ {};
+ bool running_ { false };
+
+public:
+ void begin_run() {
+ const std::lock_guard lock { mutex_ };
+
+ if (running_) {
+ throw std::logic_error { "run loop is already running" };
+ }
+
+ if (!ready_.empty()) {
+ throw std::logic_error { "run loop contains abandoned work" };
+ }
+
+ running_ = true;
+ }
+
+ void enqueue(std::coroutine_handle<> coroutine) {
+ if (!coroutine) {
+ throw std::invalid_argument { "cannot schedule an empty coroutine" };
+ }
+
+ {
+ const std::lock_guard lock { mutex_ };
+
+ if (!running_) {
+ throw std::logic_error { "scheduler has no active run" };
+ }
+
+ ready_.push_back(coroutine);
+ }
+
+ condition_.notify_one();
+ }
+
+ void complete(RootCompletionBase& completion) noexcept {
+ // 通过互斥量发布完成状态,以及此前写入的结果或异常
+ {
+ const std::lock_guard lock { mutex_ };
+ completion.completed_ = true;
+ }
+
+ condition_.notify_one();
+ }
+
+ void drive(RootCompletionBase& completion) {
+ while (true) {
+ std::coroutine_handle<> coroutine {};
+
+ {
+ std::unique_lock lock { mutex_ };
+ condition_.wait(lock, [&] {
+ return completion.completed_ || !ready_.empty();
+ });
+
+ if (completion.completed_) {
+ if (!ready_.empty()) {
+ throw std::logic_error {
+ "root task completed with outstanding work"
+ };
+ }
+
+ running_ = false;
+ return;
+ }
+
+ coroutine = ready_.front();
+ ready_.pop_front();
+ }
+
+ if (!coroutine || coroutine.done()) {
+ throw std::logic_error { "run loop contains a non-runnable coroutine" };
+ }
+
+ // 锁外恢复,协程再次入队时不会与 RunLoop 自锁
+ coroutine.resume();
+ }
+ }
+
+ void abort_run() noexcept {
+ const std::lock_guard lock { mutex_ };
+ ready_.clear();
+ running_ = false;
+ }
+};
+
+class RunGuard final {
+private:
+ std::shared_ptr state_ {};
+ bool active_ { true };
+
+public:
+ explicit RunGuard(std::shared_ptr state)
+ : state_ { std::move(state) } {
+ state_->begin_run();
+ }
+
+ RunGuard(const RunGuard&) = delete;
+ RunGuard& operator=(const RunGuard&) = delete;
+ RunGuard(RunGuard&&) = delete;
+ RunGuard& operator=(RunGuard&&) = delete;
+
+ ~RunGuard() {
+ if (active_) {
+ state_->abort_run();
+ }
+ }
+
+ void release() noexcept {
+ active_ = false;
+ }
+};
+
+class RootOperation final {
+public:
+ struct promise_type {
+ RunLoopState* state_ {};
+ RootCompletionBase* completion_ {};
+
+ [[nodiscard]] RootOperation get_return_object() noexcept;
+
+ [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept {
+ return {};
+ }
+
+ class FinalAwaiter final {
+ public:
+ [[nodiscard]] constexpr bool await_ready() const noexcept {
+ return false;
+ }
+
+ void await_suspend(
+ std::coroutine_handle coroutine) const noexcept {
+ auto* const state = coroutine.promise().state_;
+ auto* const completion = coroutine.promise().completion_;
+ state->complete(*completion);
+ }
+
+ constexpr void await_resume() const noexcept {}
+ };
+
+ [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept {
+ return {};
+ }
+
+ constexpr void return_void() const noexcept {}
+
+ void unhandled_exception() noexcept {
+ completion_->exception_ = std::current_exception();
+ }
+ };
+
+private:
+ using Handle = std::coroutine_handle;
+
+ Handle coroutine_ {};
+
+ explicit RootOperation(Handle coroutine) noexcept
+ : coroutine_ { coroutine } {}
+
+public:
+ RootOperation(const RootOperation&) = delete;
+ RootOperation& operator=(const RootOperation&) = delete;
+
+ RootOperation(RootOperation&& other) noexcept
+ : coroutine_ { std::exchange(other.coroutine_, {}) } {}
+
+ RootOperation& operator=(RootOperation&&) = delete;
+
+ ~RootOperation() {
+ if (coroutine_) {
+ coroutine_.destroy();
+ }
+ }
+
+ void bind(RunLoopState& state, RootCompletionBase& completion) noexcept {
+ coroutine_.promise().state_ = &state;
+ coroutine_.promise().completion_ = &completion;
+ }
+
+ [[nodiscard]] std::coroutine_handle<> handle() const noexcept {
+ return coroutine_;
+ }
+};
+
+inline RootOperation RootOperation::promise_type::get_return_object() noexcept {
+ return RootOperation {
+ std::coroutine_handle::from_promise(*this)
+ };
+}
+
+template
+RootOperation make_root_operation(Task task, RootCompletion& completion) {
+ if constexpr (std::same_as) {
+ co_await std::move(task);
+ } else {
+ completion.result_.emplace(co_await std::move(task));
+ }
+}
+
+} // namespace mcpplibs::cmp::detail
+
+export namespace mcpplibs::cmp {
+
+class RunLoop final {
+public:
+ class Scheduler final {
+ private:
+ class ScheduleAwaiter final {
+ private:
+ std::weak_ptr state_ {};
+
+ public:
+ explicit ScheduleAwaiter(
+ std::weak_ptr state) noexcept
+ : state_ { std::move(state) } {}
+
+ [[nodiscard]] constexpr bool await_ready() const noexcept {
+ return false;
+ }
+
+ void await_suspend(std::coroutine_handle<> continuation) {
+ // 入队后协程可能立即恢复,因此先把状态保存到当前线程的栈上。
+ const auto state = state_.lock();
+
+ if (!state) {
+ throw std::logic_error { "scheduler's run loop no longer exists" };
+ }
+
+ state->enqueue(continuation);
+ }
+
+ constexpr void await_resume() const noexcept {}
+ };
+
+ std::weak_ptr state_ {};
+
+ explicit Scheduler(
+ const std::shared_ptr& state) noexcept
+ : state_ { state } {}
+
+ friend class RunLoop;
+
+ public:
+ Scheduler() = delete;
+ Scheduler(const Scheduler&) = default;
+ Scheduler& operator=(const Scheduler&) = default;
+ Scheduler(Scheduler&&) noexcept = default;
+ Scheduler& operator=(Scheduler&&) noexcept = default;
+ ~Scheduler() = default;
+
+ [[nodiscard]] auto schedule() const noexcept {
+ return ScheduleAwaiter { state_ };
+ }
+
+ friend bool operator==(
+ const Scheduler& left,
+ const Scheduler& right) noexcept {
+ return !left.state_.owner_before(right.state_) &&
+ !right.state_.owner_before(left.state_);
+ }
+ };
+
+private:
+ std::shared_ptr state_ {
+ std::make_shared()
+ };
+
+public:
+ RunLoop() = default;
+ RunLoop(const RunLoop&) = delete;
+ RunLoop& operator=(const RunLoop&) = delete;
+ RunLoop(RunLoop&&) = delete;
+ RunLoop& operator=(RunLoop&&) = delete;
+ ~RunLoop() = default;
+
+ [[nodiscard]] Scheduler get_scheduler() const noexcept {
+ return Scheduler { state_ };
+ }
+
+ template
+ T run(Task task) {
+ detail::RootCompletion completion {};
+ detail::RunGuard guard { state_ };
+ auto operation = detail::make_root_operation(
+ std::move(task),
+ completion);
+
+ operation.bind(*state_, completion);
+ state_->enqueue(operation.handle());
+ state_->drive(completion);
+ guard.release();
+
+ if (completion.exception_) {
+ std::rethrow_exception(completion.exception_);
+ }
+
+ if constexpr (!std::same_as) {
+ if (!completion.result_) {
+ throw std::logic_error { "root task completed without a result" };
+ }
+
+ return std::move(*completion.result_);
+ }
+ }
+};
+
+} // namespace mcpplibs::cmp
diff --git a/src/task.cppm b/src/task.cppm
new file mode 100644
index 0000000..f2df590
--- /dev/null
+++ b/src/task.cppm
@@ -0,0 +1,261 @@
+export module mcpplibs.cmp:task;
+
+import std;
+
+export namespace mcpplibs::cmp {
+
+// 完成时对称转移到等待方,避免嵌套 Task 持续增长原生调用栈
+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 d6208d6..8a6681d 100644
--- a/tests/cmp_test.cpp
+++ b/tests/cmp_test.cpp
@@ -1,8 +1,8 @@
-#include
-
import std;
import mcpplibs.cmp;
+#include
+
namespace {
using mcpplibs::cmp::Task;
diff --git a/tests/run_loop_test.cpp b/tests/run_loop_test.cpp
new file mode 100644
index 0000000..91b6e48
--- /dev/null
+++ b/tests/run_loop_test.cpp
@@ -0,0 +1,366 @@
+import std;
+import mcpplibs.cmp;
+
+#include
+
+namespace {
+
+using mcpplibs::cmp::RunLoop;
+using mcpplibs::cmp::Task;
+
+using Scheduler = RunLoop::Scheduler;
+
+static_assert(std::default_initializable);
+static_assert(!std::copy_constructible);
+static_assert(!std::move_constructible);
+static_assert(!std::is_copy_assignable_v);
+static_assert(!std::is_move_assignable_v);
+
+static_assert(!std::default_initializable);
+static_assert(std::copy_constructible);
+static_assert(std::move_constructible);
+static_assert(std::equality_comparable);
+
+Task lazy_value(bool& started) {
+ started = true;
+ co_return 42;
+}
+
+Task increment(int& value) {
+ ++value;
+ co_return;
+}
+
+Task> make_move_only_value() {
+ co_return std::make_unique(7);
+}
+
+Task do_nothing() {
+ co_return;
+}
+
+Task schedule_once(Scheduler scheduler) {
+ co_await scheduler.schedule();
+ co_return std::this_thread::get_id();
+}
+
+Task schedule_many_times(Scheduler scheduler, int count) {
+ for (int index { 0 }; index < count; ++index) {
+ co_await scheduler.schedule();
+ }
+
+ co_return count;
+}
+
+Task fail_after_scheduling(Scheduler scheduler) {
+ co_await scheduler.schedule();
+ throw std::runtime_error { "scheduled task failed" };
+}
+
+Task run_nested(RunLoop& loop) {
+ loop.run(do_nothing());
+ co_return;
+}
+
+class ResumeOnNewThread final {
+private:
+ std::jthread* worker_ {};
+
+public:
+ explicit ResumeOnNewThread(std::jthread& worker) noexcept
+ : worker_ { &worker } {}
+
+ [[nodiscard]] constexpr bool await_ready() const noexcept {
+ return false;
+ }
+
+ void await_suspend(std::coroutine_handle<> continuation) const {
+ // 新线程可能立即恢复协程,因此启动后不再读取 awaiter 成员。
+ auto* const worker = worker_;
+ *worker = std::jthread { [continuation] {
+ continuation.resume();
+ } };
+ }
+
+ constexpr void await_resume() const noexcept {}
+};
+
+Task> leave_and_return(
+ Scheduler scheduler,
+ std::jthread& worker) {
+ co_await ResumeOnNewThread { worker };
+ const auto workerThread = std::this_thread::get_id();
+
+ co_await scheduler.schedule();
+ co_return std::pair { workerThread, std::this_thread::get_id() };
+}
+
+Task