Skip to content

Commit e25429c

Browse files
committed
docs(openkal): capabilities are ADL-probeable — the caps struct and its config file are gone
The design's §4 rested on one measurement: `requires { mcpp::runner("x") }` is a hard error when the name is absent. The measurement was right; the quantifier in the conclusion was not. It is QUALIFIED names that cannot be probed — unqualified lookup through ADL is dependent inside a template and evaluates to false, exactly as wanted. Verified on llvm 22.1.8 against real C++20 modules, not headers, with all three behaviours holding at once: * backend present → the concept is true and the call resolves to it * backend absent → the concept is FALSE, so `if constexpr` degrades * backend absent, called anyway → a compile error carrying the spec's own wording, which is what 'build the diagnostic in' was asking for ⚠️ One trap worth the record: if the fallback overload returns the same type as the real one, the concept is true even with no backend — a requires-expression does not instantiate the body, so the static_assert never fires. The fallback must return a distinct type. Measured, after writing it the other way first. Consequences: the caps struct, the generated caps module and capabilities.toml are all deleted. A backend's module interface IS its capability declaration, so claim and implementation become the same artifact by construction and the whole 'caps can lie' problem shrinks from structural+behavioural to behavioural only. Nothing leaves mcpp.toml — backend selection stays a conditional dependency.
1 parent 910ec2b commit e25429c

1 file changed

Lines changed: 131 additions & 131 deletions

File tree

.agents/docs/2026-08-20-openkal-design.md

Lines changed: 131 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -175,52 +175,90 @@ _Noreturn void kal_exit (int32_t code);
175175
176176
---
177177
178-
## 4. 能力组件化:三种机制,不能混用
178+
## 4. 能力组件化:ADL 探测 + 兜底重载
179179
180-
⚠️ **先说一条会砍掉最直觉做法的实测**(mcpp 2026.8.19.4 上验的):
180+
⚠️ **草案在这里错了一整节。** 它说:
181+
182+
> `requires` 作用在不存在的限定名上是硬错误 ⇒ 「能力 = 符号在不在」这条路 C++ 内测不出来
183+
> ⇒ 声明必须永远齐全,能力要放进一个 `caps` 结构体。
184+
185+
**第一句对,后面全错。** 硬错误只发生在**限定名**上;**非限定名 + ADL** 在模板里是
186+
dependent 的,`requires` 会老实求值为 `false`。
187+
188+
ⓘ **实测(llvm 22.1.8,真 C++20 模块,不是头文件)**:
181189
182190
```cpp
183-
if constexpr (requires { kal::seek(s, 0); }) // ⚠️ 名字不存在 ⇒ 硬错误,不是 false
191+
// ⛔ 限定名:名字不存在 ⇒ 硬错误
192+
if constexpr (requires(S s) { kal::seek(s, 0); })
193+
194+
// ✅ 非限定 + ADL:名字不存在 ⇒ false
195+
template <class S> concept Seekable = requires(S s, long o) { seek(s, o); };
184196
```
185197

186-
`requires` 作用在**不存在的限定名**上是 ill-formed。
187-
**「能力 = 符号在不在」这条路,C++ 语言内测不出来。**
188-
**声明必须永远齐全,能力必须是另一个可命名的东西。**
198+
**能力不需要另一个可命名的东西。后端的模块接口本身就是能力声明。**
189199

190-
| | 例子 | 机制 | 失败时机 |
191-
|---|---|---|---|
192-
| **① 接口在不在** | 有没有 `openkal.task` | **模块导入** | 编译期,点名模块 |
193-
| **② 接口内的操作在不在** |`write``seek` | **constexpr caps** | 编译期,`static_assert` 文案 |
194-
| **③ 语义能力** | 抢占式 vs 协作式调度 | **`cfg()`** | 依赖解析期 |
200+
### 4.1 三件事同时成立(ⓘ 全部实测)
195201

196-
⚠️ **③ 绝不能做成 concept**:ⓘ K1/K2 实测 `RiscvSv39``NoMmu` **同时满足**同一个
197-
`AddressSpace` concept,通用代码在 NoMmu 上**静默失败****concept 检查语法,不检查语义。**
202+
```cpp
203+
export module openkal.stream;
204+
export namespace kal {
198205

199-
**但 openkal core 里一条 ③ 都没有** —— 见 §4.3。K1/K2 那个例子是 **openarch**
200-
`AddressSpace`,不是 openkal 的。
206+
struct stream { unsigned long h; };
207+
struct io_result { unsigned long n; int e; };
201208

202-
### 4.1 ①:让模块解析本身成为能力检查
209+
// ⭐ 兜底重载的返回类型与真实现不同 —— 这是让「探测」与「兜底」共存的关键
210+
struct unsupported_t {};
211+
template <class> inline constexpr bool always_false = false;
203212

213+
template <class S>
214+
unsupported_t seek(S, long) {
215+
static_assert(always_false<S>,
216+
"this openkal backend provides no seekable streams. "
217+
"openkal.fs hands out descriptors that do.");
218+
return {};
219+
}
220+
221+
// 探测:要求返回真类型,兜底自动落选
222+
template <class S> concept HasSeek =
223+
requires(S s, long o) { requires __is_same(decltype(seek(s, o)), io_result); };
224+
225+
}
204226
```
205-
openkal.stream ← 接口:extern "C" 声明 + concepts + 类型化封装
206-
openkal.stream.caps ← 由「后端」提供
207-
```
227+
228+
后端只需要**声明并定义真实现**,不需要任何配置:
208229
209230
```cpp
210-
export module openkal.stream;
211-
import openkal.stream.caps; // 没有后端 ⇒ 编译期找不到模块,点名它
231+
export module openkal.uart;
232+
export import openkal.stream;
233+
export namespace kal { io_result seek(stream, long) { /* … */ } }
212234
```
213235

214-
**「没有实现者」不是链接器吐未定义符号,而是编译器说模块不存在。**
236+
| 场景 | 结果 ||
237+
|---|---|---|
238+
| 有后端,调用 `seek(s, 0)` | ✅ 编过,真实现赢重载 | 实测 |
239+
| 有后端,`HasSeek<stream>` |**** | 实测 |
240+
| 无后端,`HasSeek<stream>` |****(不是硬错误)⇒ `if constexpr` 可优雅降级 | 实测 |
241+
| 无后端,**强行调用** |**编译期报错,文案是规范自己写的那句** | 实测 |
242+
243+
⚠️ **这里有一个必须踩过才知道的坑**:如果兜底重载的返回类型**和真实现一样**,
244+
`HasSeek` 在没有后端时也是**** —— 因为 ADL 找到了兜底,而 `requires` 不实例化函数体,
245+
`static_assert` 不会触发。**两个机制互相干扰,靠返回类型区分才能共存。**
246+
ⓘ 我第一版就是这么写的,测出来是真才发现。
215247

216-
⚠️ **接法要注意方向。** 草案曾写「接口包 `reexport` 后端的 provisions」——
217-
**错的**:ⓘ `reexport`**向下游**传播(`grpc` 把 protoc 透给它的用户),
218-
而这里需要的是接口拿到**消费者所选后端**提供的东西,方向相反,`reexport` 表达不了。
248+
### 4.2 ⇒ 草案里三样东西被删掉了
249+
250+
| 删掉的 | 为什么不再需要 |
251+
|---|---|
252+
| `caps` 结构体 | 后端的模块接口就是声明 |
253+
| `openkal.stream.caps` 模块 | 同上 |
254+
| `capabilities.toml` |**生态整洁性**:mcpp 的一切都在 `mcpp.toml` 里,不该为这个引入第二份配置 |
219255

220-
正确接法是**反过来**,而且正好是 `reexport` 的本意:
256+
**后端选择仍然只是条件依赖**(§4.3),整条链路**零新增配置、零新增引擎轴**
257+
258+
### 4.3 后端怎么被选中:只用 `mcpp.toml`
221259

222260
```toml
223-
# 后端包 openkal-uart 的 manifest
261+
# 后端包 openkal-uart
224262
[dependencies]
225263
openkal-stream = { version = "0.1", reexport = true } # 把接口透给我的消费者
226264
```
@@ -234,120 +272,61 @@ openkal-uart = "0.1"
234272
```
235273

236274
源码 `import openkal.stream;` 两个 target 一字不改。
237-
**零新增引擎轴,且用的是已有机制的本意。**
238-
239-
⚠️ 两个后端同时进图会造成 `openkal.stream.caps` 模块重复定义 —— 基数为 1(§12)
240-
使这成为用户错误,mcpp 会报模块冲突。
241-
242-
### 4.2 ②:能力是****
243275

244-
```cpp
245-
export module openkal.stream.caps;
246-
export namespace kal::stream_caps {
247-
struct caps {
248-
static constexpr bool sequential = true;
249-
static constexpr bool seek = false; // 这个后端没有
250-
static constexpr bool vectored = true;
251-
static constexpr bool nonblock = false;
252-
};
253-
}
254-
```
276+
⚠️ ⓘ 方向要注意:`reexport`**向下游**传播(`grpc` 把 protoc 透给用户),
277+
所以是**后端 reexport 接口**,不是接口 reexport 后端 —— 草案写反过。
255278

256-
```cpp
257-
if constexpr (kal::stream_caps::caps::seek) { kal::seek(s, off); }
258-
259-
static_assert(kal::stream_caps::caps::seek,
260-
"this backend has no seekable streams; openkal.fs hands out "
261-
"descriptors that do");
262-
```
279+
### 4.4 接口层面的缺失(①)
263280

264-
组合 = **concept over caps**,不是 concept over 符号:
281+
一整个 interface 不存在时,`import openkal.task;` **编译期就找不到模块**
282+
这一条不变,而且和 §4.1 是同一套失败语义:**编译期,点名,不是运行期返回值。**
265283

266-
```cpp
267-
template <class C> concept Sequential = C::sequential;
268-
template <class C> concept Seekable = Sequential<C> && C::seek;
269-
```
270-
271-
### 4.3 ③:⚠️ openkal core 用不到它 —— 一条被撤回的引擎改动
272-
273-
草案曾要求扩 `cfg()` 文法以支持 `cfg(mmu)` 这类能力谓词。**重估后撤回。**
274-
275-
那条结论的出处是 K1/K2,而 K1/K2 测的是 **openarch 的 `AddressSpace`** —— 草案把它
276-
搬进了 openkal。逐个接口检查 openkal 有没有「存在但语义不同」的能力:
277-
278-
| interface | 有 ③ 类语义轴吗 |
279-
|---|---|
280-
| `abort` ||
281-
| `stream` | seek / nonblock / vectored 都是**操作**(②类) |
282-
| `memory` | 静态 arena vs 按需分页 = **容量**不是能力;分配失败到处都有定义 |
283-
| `time` | monotonic vs wall 是**两种资源**,不是一个资源的两种语义 |
284-
| `task` | ⚠️ 抢占 vs 协作**确实是** —— 但那是 D1 以后的事 |
284+
---
285285

286-
**core(abort + stream + memory)一条语义轴都不需要,①② 足够。**
286+
## 5. ⭐「声称 ≠ 事实」问题:新机制消掉了大半
287287

288-
而且 **triple 本身已经承载了大部分**:`riscv64-none-elf``riscv64-linux-gnu`
289-
区别里就包含了 MMU 用不用。今天已有的文法足以选后端。
288+
草案担心的是:后端写 `caps::seek = true``seek()` 永远失败。
289+
**§4 换成 ADL 之后,结构性的谎话已经说不出来了:**
290290

291-
**撤回后,本方案变成零引擎改动。**
291+
> **你不能「声称有 seek」而不真的声明一个返回 `io_result` 的 `seek`。**
292+
> 而声明了不定义,是链接错误。
292293

293-
## 5. ⭐ caps 撒谎问题:四层防御,按强度排
294+
⇒ **声称与实现是同一个制品**,不再需要「同源生成」那套纪律,也不需要 `capabilities.toml`。
294295

295-
**问题**:后端可以写 `caps::seek = true` 然后 `seek()` 永远失败。
296-
这是 K1/K2 的问题换了一层出现 —— caps 检查的是**有没有这个字段**,不是**它说的是不是真的**
296+
### 5.1 ① 让「不支持」在类型系统里**不可表达**(仍然最强)
297297

298-
### 5.1 ① 让「不支持」在类型系统里**不可表达**(最强)
298+
剩下的问题是**分类错误**,不是撒谎。判据不变:
299299

300-
如果一个操作可以「存在但永远失败」,**通常说明它被错误地合并了**
300+
> **一个操作若能「存在但永远失败」,通常说明它被错误地合并了。**
301301

302302
MMU 就是这个的实例:`NoMmu::map()` 对非恒等映射返回 `false` —— 那**不是撒谎,
303303
是这个抽象本来就不该把两族东西装进一个 concept**。当时的结论是把它挪到 `cfg` 轴。
304304

305-
推广到 `seek`:socket 不能 seek,但那**不是后端撒谎,是名词错了**
306-
307-
```cpp
308-
// 不是:caps::seek = false
309-
// 而是:句柄类型里根本没有那个能力位
310-
using console = kal::stream_of<kal::cap::write>; // seek 不在
311-
using file = kal::stream_of<kal::cap::read, kal::cap::write,
312-
kal::cap::seek>;
313-
kal::seek(c, 0); // ⚠️ 编译错误:重载要求 seek 位
314-
```
315-
316-
⇒ **能撒的谎少了一整类**,因为「`caps::x = true` 而 `x()` 无意义」在类型层面构造不出来。
317-
318-
### 5.2 ② caps 与实现**同源生成**
319-
320-
不要让后端手写 caps。**一张表同时产出 caps 模块和源码选择**:
305+
推广:socket 不能 seek,但那**不是后端撒谎,是名词错了** ——
306+
所以 `fs` 的 descriptor 与 `net` 的 socket 各持自己的句柄类型(§2.3),
307+
`kal::seek(socket, 0)` 因为**没有那个重载**而编译失败,不是因为返回错误。
321308

322-
```
323-
backend/capabilities.toml ← 唯一来源
324-
├─→ mcpp:generated= → openkal.stream.caps
325-
└─→ 源码选择 → seek.cpp 编不编进去
326-
```
327-
328-
⇒ **撒谎要改表,而改表就把实现一起删了。**
329-
`mcpp:generated=` 与源码选择**都是今天已有的指令**,不需要新机制。
330-
331-
### 5.3 ③ 双向 conformance:`false` 也要验
309+
### 5.2 ② 双向 conformance:`false` 也要验
332310

333-
> `caps::seek == false` 的后端,**必须不导出 seek 符号** —— `nm` 可查。
311+
> 后端若不提供 `seek`,**必须真的不导出这个符号** —— `nm` 可查。
334312

335313
⭐ 价值在于**它不是行为测试,是对制品的静态检查**:又快又不可能漏测。
336-
两侧都钉,caps 才从「声称」变成「事实」。
314+
在新机制下这条更强了:导出了符号 ⇒ ADL 就会找到 ⇒ 探测为真 ⇒ 与声称自动一致。
315+
**这条变成了「验证机制本身没被绕过」,而不是「验证后端没撒谎」。**
337316

338-
### 5.4 ④ 过程兜底
317+
### 5.3 过程兜底
339318

340319
conformance 结果进索引元数据:没过 seek 那组的后端,描述符里不允许声称。
341320
这是唯一能约束「实现者根本不跑 conformance」的东西。
342321

343-
### 5.5 ⚠️ 诚实的残余风险
322+
### 5.4 ⚠️ 诚实的残余风险
344323

345-
**这四条都不证明行为。** 后端可以导出符号、通过 happy path、在某个输入上错。
324+
**以上都不证明行为。** 后端可以导出 `seek`、通过 happy path、在某个输入上错。
346325
这是**每一份规范都有的残余风险**(POSIX 也一样),答案只能是 conformance 的覆盖度,
347326
不存在语言层的解法。
348327

349-
**顺序很重要**:先靠 ① 让错误分类不可表达,再靠 ② 让撒谎自毁,③④ 只是兜底。
350-
一上来就写更严的 conformance,是在给一个**本可以消除的问题**加检查
328+
但和草案相比,**残余从「结构 + 行为」缩小到只剩「行为」** —— 这正是把机制从
329+
「声明一个 bool」换成「声明一个函数」买到的东西
351330

352331
---
353332

@@ -358,8 +337,8 @@ conformance 结果进索引元数据:没过 seek 那组的后端,描述符里不
358337
| | |
359338
|---|---|
360339
| 一组 `extern "C"` 定义 | §3 的清单,按你实现的 interface |
361-
| 一个 `<interface>.caps` 模块 | ⚠️ **生成的**,不是手写的(§5.2) |
362-
| conformance 通过记录 | 双向:声称有的能用,声称没有的**符号不存在** |
340+
| 一个导出真实现的模块 | **它就是能力声明**,不需要额外的 caps(§4) |
341+
| conformance 通过记录 | 双向:声称有的能用,没提供的**符号不存在** |
363342

364343
### 6.2 最小实现:一个裸机 UART 后端
365344

@@ -380,13 +359,9 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, uintptr_t n) {
380359
}
381360
```
382361
383-
```toml
384-
# capabilities.toml —— caps 与源码选择的唯一来源
385-
[stream]
386-
sequential = true
387-
seek = false # ⇒ seek.cpp 不编进去,符号也不存在
388-
vectored = false
389-
```
362+
⚠️ **没有第二份配置。** 这个后端不提供 `seek`,做法就是**不声明它** ——
363+
`mcpp.toml` 里只有普通的包信息,`seek.cpp` 不存在,符号也不存在,
364+
而消费者侧 `kal::HasSeek<kal::stream>` 因此为假(§4.1)。
390365
391366
### 6.3 后端怎么被选中
392367
@@ -415,14 +390,25 @@ openkal-linux = "0.1"
415390
### 7.1 应用直接用
416391

417392
```cpp
418-
import openkal.stream;
393+
import openkal.uart; // 后端;接口随 reexport 而来
419394

420395
int main() {
421396
kal::write(kal::stdout(), "hello\n");
422-
if constexpr (kal::stream_caps::caps::vectored) { /* 用 writev 形状 */ }
397+
398+
// 有就用,没有就走别的路 —— 探测是 ADL,不是查表
399+
if constexpr (kal::HasVectored<kal::stream>) { /* writev 形状 */ }
400+
else { /* 逐段写 */ }
401+
402+
// 要求必须有:文案是规范自己写的
403+
static_assert(kal::HasSeek<kal::stream>,
404+
"this program needs seekable streams");
423405
}
424406
```
425407

408+
**而且不写 `if constexpr` 也不会错**:直接调 `kal::seek(s, 0)` 在没有后端支持时
409+
就是**编译期报错并给出规范的原话**(§4.1 实测)—— 这是 §7 里问「能不能内置」的答案:
410+
**能,而且是默认行为,不需要消费者做任何事。**
411+
426412
**KA2 实测:换后端不重编应用** —— 同一个 `app.o` 换掉后端目标文件重链即成。
427413

428414
### 7.2 类型化封装(C ABI 之上的零成本层)
@@ -609,7 +595,8 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零
609595
| 特性 | 解决什么 | ⚠️ 限制 |
610596
|---|---|---|
611597
| **modules** | 接口/caps 分离;**模块找不到 = 能力检查**,把链接期错误提前到编译期 | 需要 mcpp 的 `reexport`/provisions 承载 |
612-
| **`constexpr` caps 描述符** | 能力是**值**不是符号 —— 绕开「`requires` 测不了缺失名字」 | — |
598+
| ⭐ **ADL + `requires`** | ⓘ **非限定名**在模板里是 dependent 的 ⇒ 缺失时求值为 `false` 而不是硬错误。**能力探测不需要额外的数据结构,后端的模块接口就是声明** | ⚠️ **限定名**(`kal::seek`)仍是硬错误 —— 两者的差别是这套设计成立的全部基础 |
599+
| ⭐ **兜底重载 + `static_assert`** | 没有后端时直接调用 ⇒ **编译期报错,文案是规范写的** | ⚠️ 兜底的**返回类型必须与真实现不同**,否则探测恒为真(ⓘ 实测踩过) |
613600
| **`if constexpr`** | 消费者按能力降级,未选中的分支**不实例化** | — |
614601
| **concepts** | 组合 caps;openhal 的多提供者共存 | ⚠️ ⓘ **只检查语法不检查语义**(K1/K2),③ 类能力不能用它 |
615602
| **非类型模板参数(能力位)** | 让「不支持」**在类型系统里不可表达**(§5.1) | — |
@@ -620,7 +607,8 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零
620607
| **`_Noreturn` / `[[noreturn]]`** | `kal_abort` 的控制流事实进类型 | — |
621608
| ~~virtual / vtable~~ | — | ⛔ 把结构体布局写进 ABI,「只增不改」保护不了 |
622609
| ~~exceptions / RTTI~~ | — | ⛔ 裸机整图关闭(mcpp 2026.8.19.4 起) |
623-
| ~~`requires` 探测符号~~ | — | ⛔ ⓘ **实测:硬错误,不是 `false`** |
610+
| ~~`requires` 探测**限定名**~~ | — | ⛔ ⓘ 实测:硬错误。**改用非限定 + ADL** |
611+
| ~~`caps` 结构体 / `capabilities.toml`~~ | — | ⛔ 被 ADL 机制整个取代;且第二份配置文件违背生态整洁性 |
624612
625613
---
626614
@@ -688,6 +676,7 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零
688676
|---|---|---|
689677
| **`openkal.namespace`**(取代 fs/net) | 「文件 / socket / UART 给你的都是 stream,只是命名方式不同」 | ① 触犯本文自己的 §5.1 规矩(caps 成为不相干能力并集);② URI 解析器**就是**模拟层,违反对下判据;③ 引用的 WASIp2 先例是**误读**(它分开资源种类,只共享 stream 类型) |
690678
| **扩 `cfg()` 文法支持 `cfg(mmu)`** | K1/K2 说「MMU 是能力轴不是契约」 | 那条结论是关于 **openarch** 的;openkal core 逐个接口查下来**一条 ③ 类语义轴都没有**,而且 triple 本身已承载大部分 |
679+
| **`caps` 结构体 + `capabilities.toml`** | 以为「`requires` 测不了缺失的名字」⇒ 能力必须放进另一个可命名的东西 | ⓘ **只对限定名成立**。非限定 + ADL 在模板里是 dependent 的,缺失时求值为 `false`。⇒ 后端的模块接口本身就是能力声明,**两样东西整个删掉**,也不需要第二份配置 |
691680
| **「接口包 `reexport` 后端」** | 以为 mcpp 的承载件现成 | ⓘ `reexport` 是**向下游**传播,方向相反。正确接法是**后端 reexport 接口**(§4.1),恰好是该机制的本意 |
692681
693682
⭐ 三条的共同形状:**都是「我有一个漂亮的统一」,而漂亮的统一把两件本来不同的事合并了。**
@@ -769,14 +758,25 @@ POSIX 至少规定了 `PIPE_BUF` 以内的原子性。**草案一个字没提。
769758
770759
三条撤回(§15)+ 六条开放问题(§16)里,有一个共同形状值得单独记:
771760
772-
⭐ **草案的错误全部是「一个漂亮的统一,把两件本来不同的事合并了」**:
761+
草案的错误分两族。
762+
763+
**族一:一个漂亮的统一,把两件本来不同的事合并了。**
773764
774765
- `openkal.namespace` 合并了「命名」与「资源种类」
775766
- `cfg(mmu)` 把 openarch 的结论搬进 openkal
776-
- 「两个堆」是没有合并该合并的(分配器)
767+
- 「两个堆」是**没有**合并该合并的(分配器)
777768
778769
而 §5.1 给出的判据 —— **「一个操作若能『存在但永远失败』,说明它被错误地合并了」** ——
779-
本来就能抓住前两条。
770+
本来就能抓住前两条。⚠️ **我只用它去检查别人的后端,没有用它检查自己的分解。**
771+
772+
**族二:测了一种写法,把结论推广到了全部。**
773+
774+
⚠️ `caps` 结构体 + `capabilities.toml` 那整套,建立在**一次实测**上:
775+
「`requires { mcpp::runner("x") }` 是硬错误」。那次实测本身没错,
776+
错在**结论的量词** —— 我写的是「C++ 语言内测不出来」,而真实情况是
777+
**「限定名测不出来」**。非限定 + ADL 一直是可以的。
778+
779+
⇒ 代价是**一整节设计 + 一份多余的配置文件格式**,而验证它只需要三行代码。
780780
781-
⚠️ **我只用它去检查别人的后端,没有用它检查自己的分解。**
782-
⇒ 判据要对**自己的设计**先跑一遍,再拿去当准入门槛
781+
**两族合起来的规律**:一条实测能否定一个做法,**但否定不了一整类做法** ——
782+
写下「X 做不到」之前,要先问「我测的是 X,还是 X 的某一种写法」

0 commit comments

Comments
 (0)