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