Appearance
流程控制
本文档记录 KCP 流程控制语句。整体语法接近 C++,但逻辑运算使用 and / or / not,范围 for 采用当前语言自己的绑定语法。
运行时条件
if、while、do while 和 assert 的运行时条件都必须检查为 bool。当前语言不采用 C/C++ 式 truthiness:
cp
if(ok) {
run();
}
// 错误:整数、指针、enum、nullptr、str 和用户对象都不会自动当作 bool
// if(1) { run(); }
// if(pointer) { run(); }
// if(nullptr) { run(); }需要把非 bool 状态用于条件时,应显式写比较或调用返回 bool 的 API:
cp
if(count != 0) {
run();
}
if(pointer != nullptr) {
run();
}condition 位置会给表达式提供 bool 目标类型,但这只允许已有的 bool 表达式或能按普通隐式转换到 bool 的表达式通过;当前隐式转换白名单不包含整数到 bool、指针到 bool、enum 到 bool、str 到 bool 或用户自定义 truthiness。
诊断路径有一个实现差异:if、while 和 do while 使用语句条件检查,非 bool 条件报 condition_not_bool;assert(condition) 是 builtin 调用,条件参数按 bool 目标类型检查,非 bool 条件报 type_mismatch。二者的语言要求相同,都是“条件必须是 bool”,只是错误种类不同。
运行时控制流 header 必须保留圆括号。if(ok) { ... }、while(ok) { ... }、do { ... } while(ok); 和 for(let item : range) { ... } 是当前语法;if ok { ... }、while ok { ... }、do { ... } while ok;、for let item : range { ... } 都不会被 parser 当作合法控制流。条件表达式本身可以再用普通分组括号,例如 if((left < right) and ok) { ... }。
条件位置只能是表达式,不是声明或 pattern。当前不支持 C++17 风格的 init-statement,也不支持 if let / while let / if(const value = ...) 这类把名字声明放进条件头的语法。需要在条件里使用局部中间值时,先在外层 block 声明,再用普通 if(value_has_property);需要根据 variant case 分支时,用 match 表达式,而不是在 if 条件里写 pattern。
if
cp
if(condition) {
then_statement();
}cp
if(condition) {
then_statement();
} else if(other_condition) {
other_statement();
} else {
fallback_statement();
}规则:
- 条件表达式类型必须是
bool。 then分支必须是块语句。else可以接另一个if,也可以接块语句。if是语句,不是表达式。
while
cp
while(condition) {
step();
}规则:
- 条件表达式类型必须是
bool。 - 循环体必须是块语句。
do while
cp
do {
step();
} while(condition);规则:
- 条件表达式类型必须是
bool。 - 循环体至少执行一次。
- 尾部
while(condition)在do块的作用域之外检查。do块内部声明的局部名不能在条件中使用;条件只能使用外层已经可见的名字。 do while末尾必须有分号。
for
只支持范围 for:
cp
for(let value : values) {
use(value);
}cp
for(const value : values) {
use(value);
}cp
for(let ref value : values) {
value += 1;
}cp
for(const ref value : values) {
use(value);
}规则:
- 绑定语法是声明语法的子集:必须以
let或const开头,随后才可以显式写ref。for(ref value : range)和for(let const value : range)都不是当前语法。 - 范围表达式的目标语义基于 iteration.md:表达式必须是内建数组、实现
iterable,或在只读上下文中实现const_iterable。实现iterator的游标本身不能直接作为 range-for 的范围表达式。 for(let value : range)和for(const value : range)默认按值绑定,循环变量类型为read_type(iter_item)。for(let ref value : range)要求iter_item是可写引用,并把循环变量绑定为T&。for(const ref value : range)要求iter_item是引用,并把循环变量绑定为T const&。const循环变量或const ref循环变量不能被写入。- 循环体必须是块语句。
for(let value : values) use(value);不是当前语法。
不支持 C++ 三段式 for(init; condition; step)。
循环标签
范围 for 可以带标签:
cp
for: outer(let row : rows) {
for(const value : row) {
if(value < 0) {
continue outer;
}
if(value == target) {
break outer;
}
}
}规则:
- 标签写在
for后、循环条件前:for: label(...)。 - 标签只能标记范围
for。while: label(...)、do: label { ... } while(...)和template for: label(...)都不是当前语法。 - 标签名必须是单个 identifier;不支持路径、字符串、数字或表达式标签。
- 带标签的
break/continue必须解析到当前或外层范围for标签。 while和do while虽然可以作为无标签break/continue的最近运行时循环目标,但它们不能声明标签,也不能被break name;/continue name;选中。- 不要在同一嵌套区域内重复使用 loop label;重复 label 当前不作为稳定语言行为。
break 和 continue
cp
break;
continue;
break outer;
continue outer;规则:
break和continue必须出现在循环内部。- 语法只允许
break;、continue;、break label;和continue label;。标签位置只能是裸 identifier;break 1;、break value;、continue if(condition);或break outer.value;都不是当前控制流能力。若break后跟一个普通局部变量名,parser 会先把它当作 label 解析,随后语义层按找不到循环标签处理,而不是把它当作要返回给循环的值。 - 这条检查先于标签查找。也就是说,函数体顶层的
break outer;/continue outer;会先报“不在循环内”的invalid_break/invalid_continue,不会因为outer不存在而报unknown_label。 - 不带标签时作用于最近的内层运行时循环,可以是
while、do while或范围for。 - 带标签时只能作用于对应标签的当前或外层范围
for;没有标签语法的while/do while不能作为带标签跳转目标。 template for不是运行时循环,不能作为break/continue的目标。- 如果
template for展开体内声明了新的运行时循环,展开体中的break/continue可以控制这个内层循环。 break/continue不能从template for展开体穿透到外层运行时循环;带标签写法同样不能跳到展开体外部的for: label(...)。break/continue跳转前会清理从当前位置到目标循环边界之间已经注册的局部对象;析构注册和清理边界见 ownership.md。
template for
template for 是语句级编译期展开,不是运行时循环。完整泛型和参数包规则见 generic.md。
cp
template for(let value : values...) {
use(value);
}
template for(type T : Args...) {
use_type<T>();
}规则:
template for(let value : values...)和template for(const value : values...)只能遍历当前函数、成员函数或 lambda 实例中的值参数包。- 值参数包展开不会生成运行时循环变量,也不会额外复制 pack 元素。每次展开里的
value是当前函数实例中对应参数元素的局部名字;按值参数包元素本身已经是被调函数的局部副本,let ref/const ref展开则绑定为引用。 template for(let ref value : values...)/template for(const ref value : values...)使用引用绑定形态;如果参数包元素来自forward&参数包,展开出的局部绑定保留对应元素的 forward 资格。template for(type T : Args...)只能遍历当前实例中的类型参数包;T是每次展开体内的局部类型别名,不是运行时值。- header 必须写在圆括号中,绑定名和 pack 名之间必须写
:。template for let value : values... { ... }、template for(let value values...) { ... }都不是当前语法。 - 被遍历的名字后必须写
...,普通数组、tuple、range、局部变量和 concept 参数包都不能作为template for的范围。template for(let value : values)会在 parser 阶段因为缺少...被拒绝。 - 展开体必须是块语句。
template for(let value : values...) use(value);不是当前语法。 - 参数包可以为空;为空时 body 展开零次。
- 每次展开都有自己的语义上下文和局部作用域;展开体按源码顺序重复检查,不生成运行时循环。
template for不建立运行时循环目标,没有运行时计数器,也不接收循环标签。break/continue不能直接作用于template for;展开体内新建的运行时循环可以被自己的break/continue控制。- 展开体中的
return返回当前函数或 lambda,和普通语句中的return一样参与返回类型推导。
template if
template if 是语句级编译期分支,用于在具体泛型实例中选择一个语句体:
cp
template if(T == i32) {
return 1;
} else template if(T: display) {
return 2;
} else {
return 0;
}规则:
- 条件支持类型相等、concept 条件、可折叠为
bool的小型常量表达式,以及not/and/or/ 括号组合。 - 表达式条件必须先能按普通表达式规则检查为
bool,再能折叠出编译期bool常量;不能折叠时不是运行时if,而是语义错误。 - 类型相等条件比较两侧规范化后的读出类型,不是源码形状的严格文本比较。普通别名会先展开,引用会被剥掉;因此
T& == T、T const& == T和type alias = T; alias == T当前都会按同一个读出类型成立。指针、数组、tuple、struct、variant、opaque alias 等非引用形状仍按实际类型比较。 - 类型相等或 concept 条件如果在当前实例中仍然依赖未替换类型,会报错,不延迟到运行时。
and/or在条件求值中按编译期短路处理。- 每个
template if/else template if分支体都必须是块语句;最终else也只能接块语句。template if(T == i32) return 1;不是当前语法。 - 只检查第一个命中的分支;未选中的分支只要求语法正确,不解析名字、不检查类型、不贡献返回类型,也不参与循环或借用状态。
- 没有分支命中且没有
else时,等价于空语句。 - 被选中的分支按普通语句处理:其中的
return、break、continue、局部声明和块表达式规则都与运行时语句一致。
当前表达式条件的 constexpr 折叠范围:
- 可作为字面值输入的是
true/false、整数字面量、字符字面量,以及已经解析出的Enum::case。 - 括号分组会递归折叠。
- 一元
not只作用于可折叠 bool;一元-只作用于可折叠整数。 ==/!=可以比较两个同类可折叠字面值;整数、字符、bool 和 enum case 都可以参与相等比较。</<=/>/>=当前只折叠整数比较。字符、bool、浮点和字符串不参与这些关系比较。and/or只要求被实际求值的一侧能折叠为 bool;因此false and missing_name和true or missing_name不会检查右侧名字。- 不折叠算术、位运算、cast、函数调用、局部变量、字段访问、数组/tuple/struct/variant 构造、match 或 block 表达式。表达式仍会先按 bool 条件做普通语义检查;类型不是 bool 时先报
condition_not_bool,类型是 bool 但不能折叠时才报invalid_operator。 template if的普通语义检查可以把合法 enum case 条件折叠成 bool;但省略返回类型时,不要依赖 enum case 条件排除未选中分支。需要这种分支影响返回类型时,应显式写返回类型,或改用类型相等 / concept 条件。
enum case 参与折叠不绕过普通 enum 运算规则。只有普通表达式已经是合法 bool 条件时,constexpr 折叠才读取 case 的常量值。因此同一个 enum 类型的 case 可以做 == / !=,不同 enum 类型仍不能比较,enum 也不能靠这里使用 < / <= / > / >=:
cp
enum mode : u8 {
read = 1;
write = 2;
}
template if(mode::read == mode::read) {
// 合法:同类型 enum 相等比较,折叠为 true
}
template if(mode::read < mode::write) {
// 错误:enum 没有内建关系比较;需要先显式转到底层整数
}例如:
cp
template if(1 < 2 and 'a' == 'a') {
return 1;
}
template if(1 + 1 == 2) {
return 2; // 当前不合法:template if 的 constexpr 折叠不支持算术表达式
}空语句与空块
源码里单独写 ; 会被 parser 当作空语句诊断并丢弃:
cp
main()
{
let value = 1;; // warning: empty_statement
return value;
}这条诊断是 warning,不会像语法 error 一样让整次解析失败;但 ; 不会产生 AST 语句节点,也不是推荐的 no-op 语句能力。它可以出现在顶层、块语句和块表达式内部,都会被消费并从后续语义检查中消失。需要一个什么都不做的分支时,写空块 {};需要表达“没有 template if 分支命中”时,语义上等价于不生成任何语句,而不是源码层要求写一个分号。
表达式语句
普通表达式后接分号可以作为语句:
cp
counter = counter + 1;
log(counter);
panic("stop");规则:
expr;会按普通表达式做完整语义检查,包括名字查找、重载选择、赋值目标、调用参数、借用和类型转换检查。- 表达式结果会被丢弃;结果类型可以是普通值、引用、指针、内部
unit或!。当前没有[[nodiscard]]这类“结果必须使用”的语言规则。 - 分号是表达式语句的一部分。块中的最后一个表达式如果没有分号,会按块表达式尾表达式处理,而不是表达式语句;在普通函数体的块语句中不能写裸尾表达式来表示返回值。
unit表达式语句可以正常完成,例如free(ptr);、assert(condition);或调用返回void的函数。- 类型为
!的表达式语句不能正常完成,例如panic("bad");或unreachable();。这会影响后续块、函数返回检查和!返回函数的正常完成判断。 - 表达式语句不能直接声明名字;需要局部名字时使用
let/const声明。type name = Type;是局部类型别名语句,不是表达式语句。
return
cp
return;
return value;规则:
return value;的值必须能转换到当前函数返回类型。return;只允许用于unit返回。- 函数省略返回类型时,
return;会作为unit参与返回类型推导;它不能和普通带值返回混合推导成非unit类型。 - 在块表达式中写
return时,仍然从所在函数返回,不是从块表达式返回。 - 块表达式内部的
return value;会参与所在函数的返回类型推导;例如let ignored = { return 1; };会让所在函数推导为i32。 - lambda 是新的函数边界。lambda body 中的
return返回当前 lambda,不返回外层函数。 - 函数声明返回
!时,函数体不能有正常完成路径;return;不合法,return value;只有在value本身类型为!时合法。
省略返回类型时,当前实现先收集函数体中所有可见 return 语句的表达式类型,再做一个有限的统一:
- 函数体没有任何
return时,推导为unit。 return;贡献unit。它可以和其它return;统一,但不能和带值返回统一。return;和带值return value;不能统一。return;先出现、后面再出现return 1;,或先出现return 1;、后面再出现return;,当前预推导都会让函数返回类型保持未推导状态并报cannot_infer_return_type;不会把二者合并成某个更宽的类型,也不会把unit当作可忽略分支。return panic(...)、return unreachable()或其它类型为!的返回不决定普通返回类型;只要还有普通返回,推导结果由普通返回决定。所有返回都是!时,函数推导为!。- 多个整数返回可以统一成当前普通整数 common type;例如一个分支返回
i32局部、另一个分支返回i64局部时,会按整数提升规则取共同类型。rank 相同时保留先观测到的返回类型,因此return 1 as i32;后再返回u32会推导为i32,反过来先返回u32则推导为u32。 - 多个浮点返回可以统一成较高 rank 的浮点类型。
- 除整数/浮点 common type 外,其它普通返回类型必须完全相同。
i32与bool、unit与i32、不同名义struct/variant类型、数组长度不同的数组、不同 tuple 形状都不能自动统一;这些组合在省略返回类型函数中会导致cannot_infer_return_type,而不是选择第一个返回类型后再按后续return报普通return_type_mismatch。 while、do while与范围for的循环体中的return会参与省略返回类型推导;这不表示循环已被证明一定执行。 若函数体在循环后仍可能正常完成,后续正常完成检查仍会要求显式返回。- 如果推导返回类型需要调用另一个也正在推导返回类型的函数,例如直接递归
f() { return f(); }或互递归f() { return g(); } g() { return f(); },当前会报告不能推导返回类型;给其中至少一个函数写显式返回类型。 - 推导出的非
unit/ 非!返回类型仍要通过正常完成检查。f() { if(ok) { return 1; } }会先从return 1;推导出i32,然后因为函数体仍可能不返回而报告缺少返回。
当前实现按语义检查后的控制流形状判断“函数体能否正常完成”:
return、break、continue语句自身不能正常完成。- 类型为
!的表达式语句不能正常完成,例如panic("bad");。 - 声明初始化器若类型为
!,该声明不能正常完成,例如let value = panic("bad");。 初始化器是块表达式时也按块表达式最终类型判断;例如let ignored: i32 = { return 1; };中的return 1;直接从所在函数返回,初始化器类型为!,不会要求后续继续初始化ignored。assert(false)固定是unit, 不会因为实参是字面量false而让声明或后续语句发散。 - 一个块中的任意前序语句不能正常完成时,后续路径也被视为不能从该块正常到达。
- 块表达式按同样规则判断:先顺序检查块内语句;只要某条前序语句不能正常完成,块表达式整体不能正常完成,尾表达式即使存在也不可达。所有前序语句都可能正常完成时,没有尾表达式的块表达式可以正常完成;有尾表达式时,块表达式能否正常完成取决于尾表达式本身。
if没有else时始终可能正常完成;有else时,只要任一分支可能正常完成,整个if就可能正常完成。match是表达式;如果所有 arm 的值都不能正常完成,整个match表达式不能正常完成;只要存在一个能正常完成的 arm,整个match表达式仍可能正常完成。while、do while、范围for和template for当前都按可能正常完成处理,即使条件字面量写成true,循环体第一条语句就是continue,或者范围 / 参数包在当前源码形状下看起来非空。语义层不会用“循环一定至少执行一次”“没有可达 break”“参数包长度非零”这类事实证明循环整体发散;需要让函数返回!或满足非unit返回检查时,应在循环之后显式写panic(...)/unreachable()/return ...,或把发散表达成已经被语义层识别为!的表达式语句。- 被选中的
template if分支按普通语句判断;未选中的分支不参与。
因此:
cp
bad() -> i32
{
if(true) {
return 1;
}
} // 错误:没有 else,当前实现认为函数仍可能正常完成
ok() -> !
{
panic("stop");
} // 合法:panic 的类型是 !
spin() -> !
{
while(true) {
}
} // 当前仍然错误:循环语句按可能正常完成处理,不把条件字面量 true 当作发散证明