从 FAQ 语料(2018 篇镜像)与参考页做完 169 条逐条差集,补入 6 组此前未收录的语言 事实。每条先用 TSL.exe 实跑观察行为,再收敛成 39 条断言的回归门,在 TSLGen2、 AnalyseNG.NET、TSL 三个解释器上均通过后才写入。 - 16 页:0/0 得 nan、2/0 得 inf;nan 的 = > < >= <= 恒假而 <> 为真;nan 沿算术 传播;nil 在乘除减里按 0 参与 - 04 页:整体赋值免声明,但对未建立的变量写单元格赋值会报「变量类型不是Array」 - 07 页:相邻两个 if 是两条独立语句都会执行;for..in 单控制变量拿到的是键不是值; 二维数组按行推进,行本身是数组(新增 syntax-07-013) - 21 页:子矩阵赋值语句求值为 0;t[:,:] 按位置复制不按列名对齐(新增 syntax-21-019);| 列并时同名字符串列键由右操作数覆盖(新增 syntax-21-018) - 13 页:select 输出列表同名列后者覆盖前者,须用 as 改名(新增 syntax-13-032) 三条 FAQ 说法实跑不复现,故未写入:12574 的括号消歧、19857 的 -2147483648 字面量限制、31474 的 :. bugfix 记录。15305/11045 的用户/公用/共享函数同名解析 优先级需要服务端多账号环境,本地无法验证,仍为已知缺口。 lexicon.json 为 16 页补两个页级 alias,使 nan/nil 语义能被自然语言查询命中。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
628 lines
12 KiB
Markdown
628 lines
12 KiB
Markdown
# TSL 控制流
|
||
|
||
这一篇只收录流程控制与异常控制,不讨论金融语义。
|
||
|
||
## 本篇职责
|
||
|
||
<!-- section-id: syntax-07-001 -->
|
||
|
||
回答“`if`、`case`、`for`、`while`、`repeat`、`break`、`continue`、`try`、`raise` 这些流程结构在 TSL 里到底怎么写,哪些写法可以直接生成”。
|
||
|
||
## 核心规则
|
||
|
||
<!-- section-id: syntax-07-002 -->
|
||
|
||
- `if ... then ... else ...` 默认写成块式分支:`then begin ... end else begin ... end`。
|
||
- 两个相邻的 `if ... then begin ... end` 是两条**独立**语句,两个条件都会求值、都可能执行;只有写了 `else` 才是互斥分支。
|
||
- 块式分支内部的普通语句必须用分号结尾。
|
||
- 控制流块的 `begin ... end` 后可以加分号也可以不加(语法都允许)。
|
||
- `for` 支持 `to`、`downto`、可选 `step`,以及 `for i, v in array` 遍历。
|
||
- `for ... in` 也可以只写一个控制变量:`for i in array do` 里的 `i` 是行下标/键,不是行值;要拿到行值就写两个控制变量。
|
||
- 二维数组按 `for i, v in array` 遍历时按行推进,`v` 本身是整行数组;要落到单元格需要再嵌套一层 `for ... in`。
|
||
- 计数 `for` 的初值、终值和步长确定后,循环次数随之固定;循环体内不要给控制变量赋值。
|
||
- `for i, v in array` 遍历期间,不要修改被遍历数组或其中元素。
|
||
- `while` 和 `repeat ... until` 都可直接使用;`repeat` 至少会先执行一轮再判断结束条件。
|
||
- `break` 会跳出当前最近一层循环,`continue` 会跳过当前轮剩余语句。
|
||
- `case ... of ... else ... end` 可作为语句形态生成;`end` 后可以加分号也可以不加。
|
||
- `value := case ... of ... else ... end;` 可作为表达式形态生成;表达式形态赋值语句本身要用分号结尾。
|
||
- `case` 分支标签支持逗号并列和 `to` 区间。
|
||
- `try ... except ... end` 可以捕获 `raise` 产生的错误,并继续执行后续语句。
|
||
- `exceptObject.errInfo` 在 `except` 块中可读,能拿到当前错误信息。
|
||
- `exceptObject.errLine` 和 `exceptObject.errNo` 在 `except` 块中也可读。
|
||
- `try ... finally ... end` 无论是否报错,都会先执行 `finally`;如果没有 `except` 吞掉错误,脚本仍会在 `finally` 之后报错终止。
|
||
- `raise "message"` 是最小抛错写法。
|
||
- `goto`、`debugReturn`、`debugRunEnv`、计时和性能分析器这类“控制流补充工具”统一放到 [14_debug_and_profiler.md](14_debug_and_profiler.md)。
|
||
|
||
## 可直接照写示例
|
||
|
||
<!-- section-id: syntax-07-003 -->
|
||
|
||
使用这些示例时遵守:
|
||
|
||
- 条件表达式、比较、布尔值和普通赋值的事实见 [06_expressions_and_operators.md](06_expressions_and_operators.md)。
|
||
- 函数里的控制流只按控制流语法处理;函数文件模型、返回值和参数规则见 [05_functions_and_calls.md](05_functions_and_calls.md)。
|
||
- `goto`、`debugReturn`、计时和性能分析器不在本页生成,相关事实见 [14_debug_and_profiler.md](14_debug_and_profiler.md)。
|
||
|
||
### `if`、`while`、`repeat ... until`
|
||
|
||
<!-- section-id: syntax-07-004 -->
|
||
|
||
<!-- tags: 条件判断, 循环, 判断语句, 直到满足, 反复执行 -->
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
flag := 1;
|
||
if flag > 0 then
|
||
begin
|
||
value := 1;
|
||
end
|
||
else
|
||
begin
|
||
value := 0;
|
||
end
|
||
|
||
counter := 0;
|
||
while counter < 3 do
|
||
counter := counter + 1;
|
||
|
||
repeat
|
||
counter := counter - 1;
|
||
until counter = 0;
|
||
writeLn(value);
|
||
writeLn(counter);
|
||
```
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
1
|
||
0
|
||
```
|
||
|
||
相邻两个 `if` 与 `if ... else` 的区别:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
trace := "";
|
||
if 1 then
|
||
begin
|
||
trace := trace + "A";
|
||
end
|
||
if 1 then
|
||
begin
|
||
trace := trace + "B";
|
||
end
|
||
writeLn(trace);
|
||
|
||
trace2 := "";
|
||
if 1 then
|
||
begin
|
||
trace2 := trace2 + "A";
|
||
end
|
||
else
|
||
begin
|
||
trace2 := trace2 + "B";
|
||
end
|
||
writeLn(trace2);
|
||
```
|
||
|
||
结果说明:
|
||
|
||
- 第一段输出 `AB`:两个 `if` 是两条独立语句,条件都为真所以都执行
|
||
- 第二段输出 `A`:`if ... else` 是一条语句,两支互斥
|
||
- 所以“多个条件里只能命中一个”必须写 `else`,靠相邻 `if` 排列不成立
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
AB
|
||
A
|
||
```
|
||
|
||
### `for` 的几种主干写法
|
||
|
||
<!-- section-id: syntax-07-005 -->
|
||
|
||
<!-- tags: 循环写法, 遍历数组, 计数循环, 倒着循环, 遍历键值 -->
|
||
|
||
最基础的递增循环:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
sum := 0;
|
||
for i := 0 to 2 do
|
||
sum := sum + i;
|
||
writeLn(sum);
|
||
```
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
3
|
||
```
|
||
|
||
带 `step` 的递增循环:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
sum := 0;
|
||
for i := 1 to 5 step 2 do
|
||
sum := sum + i;
|
||
writeLn(sum);
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 输出 `9`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
9
|
||
```
|
||
|
||
带 `step` 的 `downto` 递减循环:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
sum := 0;
|
||
for i := 5 downto 1 step 2 do
|
||
sum := sum + i;
|
||
writeLn(sum);
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 输出 `9`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
9
|
||
```
|
||
|
||
数组遍历:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
numbers := array(10, 20, 30);
|
||
for i, value in numbers do
|
||
writeLn(i * 100 + value);
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 依次输出 `10`、`120`、`230`
|
||
- 这说明 `for i, value in numbers` 里的 `i` 从 `0` 开始
|
||
- 遍历期间把 `numbers` 当成只读集合;需要修改时先遍历副本,或在循环结束后统一写回
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
10
|
||
120
|
||
230
|
||
```
|
||
|
||
### `for ... in` 的控制变量个数
|
||
|
||
<!-- section-id: syntax-07-013 -->
|
||
|
||
<!-- tags: 遍历只写一个变量, 键还是值, 遍历二维数组, 每行是数组, 嵌套遍历 -->
|
||
|
||
一个控制变量拿到的是下标/键,两个才拿到行值:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
data := array("a": 1, "b": 5, "c": 3, "d": -2);
|
||
total := 0;
|
||
for i, value in data do
|
||
total := total + value;
|
||
writeLn(total);
|
||
|
||
keys := "";
|
||
for i in data do
|
||
keys := keys + i;
|
||
writeLn(keys);
|
||
```
|
||
|
||
结果说明:
|
||
|
||
- 先输出 `7`:两个控制变量时,`value` 是行值
|
||
- 再输出 `abcd`:只写一个控制变量时,它是行下标/键,不是行值
|
||
- 所以想累加数值却只写一个控制变量,会拿到键而不是值
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
7
|
||
abcd
|
||
```
|
||
|
||
二维数组按行推进,每个 `value` 本身是一行数组:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
rows := array(("A": 1, "B": 2), ("A": 3, "B": 4));
|
||
colA := 0;
|
||
for i, value in rows do
|
||
colA := colA + value["A"];
|
||
writeLn(colA);
|
||
|
||
all := 0;
|
||
for i, value in rows do
|
||
for j, cell in value do
|
||
all := all + cell;
|
||
writeLn(all);
|
||
```
|
||
|
||
结果说明:
|
||
|
||
- 先输出 `4`:`value` 是整行数组,可以直接按列键取 `value["A"]`
|
||
- 再输出 `10`:要落到每个单元格,需要再嵌套一层 `for ... in`
|
||
- 所以二维数组不会被自动摊平成单元格序列
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
4
|
||
10
|
||
```
|
||
|
||
### `break` 与 `continue`
|
||
|
||
<!-- section-id: syntax-07-006 -->
|
||
|
||
<!-- tags: 跳出循环, 提前退出循环, 跳过本次, 结束循环 -->
|
||
|
||
`break`:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
i := 0;
|
||
sum := 0;
|
||
while true do
|
||
begin
|
||
i := i + 1;
|
||
if i > 3 then
|
||
break;
|
||
sum := sum + i;
|
||
end
|
||
writeLn(sum);
|
||
writeLn(i);
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- `sum` 输出 `6`
|
||
- `i` 输出 `4`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
6
|
||
4
|
||
```
|
||
|
||
`continue`:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
i := 0;
|
||
sum := 0;
|
||
while i < 4 do
|
||
begin
|
||
i := i + 1;
|
||
if i = 2 then
|
||
continue;
|
||
sum := sum + i;
|
||
end
|
||
writeLn(sum);
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 输出 `8`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
8
|
||
```
|
||
|
||
### `case` 语句形态
|
||
|
||
<!-- section-id: syntax-07-007 -->
|
||
|
||
<!-- tags: 多分支, 分支判断, switch 怎么写, 多条件选择 -->
|
||
|
||
普通分支:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
a := 2;
|
||
case a of
|
||
1:
|
||
writeLn("one");
|
||
2:
|
||
writeLn("two");
|
||
else
|
||
writeLn("other");
|
||
end
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 输出 `two`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
two
|
||
```
|
||
|
||
并列标签与区间:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
a := 4;
|
||
case a of
|
||
1, 2:
|
||
writeLn("small");
|
||
3 to 5:
|
||
writeLn("mid");
|
||
else
|
||
writeLn("other");
|
||
end
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 输出 `mid`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
mid
|
||
```
|
||
|
||
`case` 表达式形态:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
a := 3;
|
||
label_value := case a of
|
||
1, 2:
|
||
"small";
|
||
3, 4:
|
||
"mid";
|
||
else
|
||
"other";
|
||
end;
|
||
writeLn(label_value);
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 输出 `mid`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
mid
|
||
```
|
||
|
||
说明:
|
||
|
||
- 表达式形态可以放在赋值右侧。
|
||
- 表达式形态的每个分支只写单条表达式/单条语句,不写 `begin ... end` 语句段。
|
||
- 赋值语句整体以 `end;` 收尾。
|
||
|
||
### `try ... except`
|
||
|
||
<!-- section-id: syntax-07-008 -->
|
||
|
||
<!-- tags: 捕获异常, 异常处理, 出错处理, 抓错误, 异常不中断 -->
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
writeLn("before");
|
||
try
|
||
raise "boom";
|
||
except
|
||
writeLn("caught");
|
||
writeLn(exceptObject.errInfo);
|
||
end
|
||
writeLn("after");
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 先输出 `before`
|
||
- 再输出 `caught`
|
||
- `exceptObject.errInfo` 输出包含 `raise: boom` 的错误信息
|
||
- 最后继续输出 `after`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
before
|
||
caught
|
||
raise: boom
|
||
after
|
||
```
|
||
|
||
`exceptObject` 的扩展字段:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
try
|
||
raise "boom";
|
||
except
|
||
writeLn(exceptObject.errLine);
|
||
writeLn(exceptObject.errNo);
|
||
end
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- `exceptObject.errLine` 输出 `4`
|
||
- `exceptObject.errNo` 输出 `2`
|
||
- 说明异常对象除了 `errInfo` 以外,也能直接提供出错行号和错误号
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
4
|
||
2
|
||
```
|
||
|
||
### `try ... finally`
|
||
|
||
<!-- section-id: syntax-07-009 -->
|
||
|
||
<!-- tags: 收尾处理, 无论如何都执行, 释放资源, 清理动作 -->
|
||
|
||
正常路径:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
writeLn("before");
|
||
try
|
||
writeLn("body");
|
||
finally
|
||
writeLn("finally");
|
||
end
|
||
writeLn("after");
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 依次输出 `before`、`body`、`finally`、`after`
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
before
|
||
body
|
||
finally
|
||
after
|
||
```
|
||
|
||
报错路径:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
writeLn("before");
|
||
try
|
||
writeLn("body");
|
||
raise "boom";
|
||
finally
|
||
writeLn("finally");
|
||
end
|
||
writeLn("after");
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 先输出 `before`
|
||
- 再输出 `body`
|
||
- 然后仍会输出 `finally`
|
||
- 随后脚本报错终止,`after` 不会执行
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
before
|
||
body
|
||
finally
|
||
```
|
||
|
||
### `raise`
|
||
|
||
<!-- section-id: syntax-07-010 -->
|
||
|
||
<!-- tags: 抛异常, 主动报错, 中断执行, 扔错误 -->
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
writeLn("before");
|
||
raise "boom";
|
||
writeLn("after");
|
||
```
|
||
|
||
输出说明:
|
||
|
||
- 先输出 `before`
|
||
- 随后脚本报错终止,`after` 不会执行
|
||
|
||
代码块身份:输出片段
|
||
|
||
```text
|
||
before
|
||
```
|
||
|
||
## 默认生成模板
|
||
|
||
<!-- section-id: syntax-07-011 -->
|
||
|
||
最短条件分支的默认骨架如下:
|
||
|
||
代码块身份:可直接照写示例
|
||
|
||
```tsl
|
||
flag := 1;
|
||
if flag > 0 then
|
||
begin
|
||
value := 1;
|
||
end
|
||
else
|
||
begin
|
||
value := 0;
|
||
end
|
||
```
|
||
|
||
## 禁止项
|
||
|
||
<!-- section-id: syntax-07-012 -->
|
||
|
||
- 在 `else` 前面误加分号。
|
||
- 生成没有分号的裸分支赋值,例如 `then value := 1 else ...`;带 `else` 时用块式分支。
|
||
- 以为 `try ... finally` 会吞掉异常。
|
||
- 在还没搞清表达式规则前,先把复杂业务函数塞进条件里。
|
||
- 把控制流问题和函数文件模型问题混在一起排查。
|
||
- 在计数 `for` 循环体里给控制变量赋值。
|
||
- 在 `for ... in` 遍历期间修改被遍历数组或其中元素。
|
||
- 用相邻两个 `if ... then begin ... end` 表达互斥分支;它们是两条独立语句,需要互斥就写 `else`。
|
||
- 在 `for ... in` 只写一个控制变量却当成行值使用;单变量拿到的是下标/键。
|
||
- 以为二维数组会被 `for ... in` 摊平成单元格序列;它按行推进,行本身是数组。
|
||
|
||
代码块身份:反例 / 不可照写
|
||
|
||
```text
|
||
if flag > 0 then
|
||
value := 1;
|
||
else
|
||
value := 0;
|
||
```
|
||
|
||
上面这种写法会编译失败,因为 `else` 前面的分号会让 `if` 语句在上一行提前结束。生成带 `else` 的赋值分支时,不要改成省略分号的裸分支;使用前文的块式分支。
|