从 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>
12 KiB
TSL 控制流
这一篇只收录流程控制与异常控制,不讨论金融语义。
本篇职责
回答“if、case、for、while、repeat、break、continue、try、raise 这些流程结构在 TSL 里到底怎么写,哪些写法可以直接生成”。
核心规则
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。
可直接照写示例
使用这些示例时遵守:
- 条件表达式、比较、布尔值和普通赋值的事实见 06_expressions_and_operators.md。
- 函数里的控制流只按控制流语法处理;函数文件模型、返回值和参数规则见 05_functions_and_calls.md。
goto、debugReturn、计时和性能分析器不在本页生成,相关事实见 14_debug_and_profiler.md。
if、while、repeat ... until
代码块身份:可直接照写示例
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);
代码块身份:输出片段
1
0
相邻两个 if 与 if ... else 的区别:
代码块身份:可直接照写示例
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排列不成立
代码块身份:输出片段
AB
A
for 的几种主干写法
最基础的递增循环:
代码块身份:可直接照写示例
sum := 0;
for i := 0 to 2 do
sum := sum + i;
writeLn(sum);
代码块身份:输出片段
3
带 step 的递增循环:
代码块身份:可直接照写示例
sum := 0;
for i := 1 to 5 step 2 do
sum := sum + i;
writeLn(sum);
输出说明:
- 输出
9
代码块身份:输出片段
9
带 step 的 downto 递减循环:
代码块身份:可直接照写示例
sum := 0;
for i := 5 downto 1 step 2 do
sum := sum + i;
writeLn(sum);
输出说明:
- 输出
9
代码块身份:输出片段
9
数组遍历:
代码块身份:可直接照写示例
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当成只读集合;需要修改时先遍历副本,或在循环结束后统一写回
代码块身份:输出片段
10
120
230
for ... in 的控制变量个数
一个控制变量拿到的是下标/键,两个才拿到行值:
代码块身份:可直接照写示例
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:只写一个控制变量时,它是行下标/键,不是行值 - 所以想累加数值却只写一个控制变量,会拿到键而不是值
代码块身份:输出片段
7
abcd
二维数组按行推进,每个 value 本身是一行数组:
代码块身份:可直接照写示例
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 - 所以二维数组不会被自动摊平成单元格序列
代码块身份:输出片段
4
10
break 与 continue
break:
代码块身份:可直接照写示例
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输出6i输出4
代码块身份:输出片段
6
4
continue:
代码块身份:可直接照写示例
i := 0;
sum := 0;
while i < 4 do
begin
i := i + 1;
if i = 2 then
continue;
sum := sum + i;
end
writeLn(sum);
输出说明:
- 输出
8
代码块身份:输出片段
8
case 语句形态
普通分支:
代码块身份:可直接照写示例
a := 2;
case a of
1:
writeLn("one");
2:
writeLn("two");
else
writeLn("other");
end
输出说明:
- 输出
two
代码块身份:输出片段
two
并列标签与区间:
代码块身份:可直接照写示例
a := 4;
case a of
1, 2:
writeLn("small");
3 to 5:
writeLn("mid");
else
writeLn("other");
end
输出说明:
- 输出
mid
代码块身份:输出片段
mid
case 表达式形态:
代码块身份:可直接照写示例
a := 3;
label_value := case a of
1, 2:
"small";
3, 4:
"mid";
else
"other";
end;
writeLn(label_value);
输出说明:
- 输出
mid
代码块身份:输出片段
mid
说明:
- 表达式形态可以放在赋值右侧。
- 表达式形态的每个分支只写单条表达式/单条语句,不写
begin ... end语句段。 - 赋值语句整体以
end;收尾。
try ... except
代码块身份:可直接照写示例
writeLn("before");
try
raise "boom";
except
writeLn("caught");
writeLn(exceptObject.errInfo);
end
writeLn("after");
输出说明:
- 先输出
before - 再输出
caught exceptObject.errInfo输出包含raise: boom的错误信息- 最后继续输出
after
代码块身份:输出片段
before
caught
raise: boom
after
exceptObject 的扩展字段:
代码块身份:可直接照写示例
try
raise "boom";
except
writeLn(exceptObject.errLine);
writeLn(exceptObject.errNo);
end
输出说明:
exceptObject.errLine输出4exceptObject.errNo输出2- 说明异常对象除了
errInfo以外,也能直接提供出错行号和错误号
代码块身份:输出片段
4
2
try ... finally
正常路径:
代码块身份:可直接照写示例
writeLn("before");
try
writeLn("body");
finally
writeLn("finally");
end
writeLn("after");
输出说明:
- 依次输出
before、body、finally、after
代码块身份:输出片段
before
body
finally
after
报错路径:
代码块身份:可直接照写示例
writeLn("before");
try
writeLn("body");
raise "boom";
finally
writeLn("finally");
end
writeLn("after");
输出说明:
- 先输出
before - 再输出
body - 然后仍会输出
finally - 随后脚本报错终止,
after不会执行
代码块身份:输出片段
before
body
finally
raise
代码块身份:可直接照写示例
writeLn("before");
raise "boom";
writeLn("after");
输出说明:
- 先输出
before - 随后脚本报错终止,
after不会执行
代码块身份:输出片段
before
默认生成模板
最短条件分支的默认骨架如下:
代码块身份:可直接照写示例
flag := 1;
if flag > 0 then
begin
value := 1;
end
else
begin
value := 0;
end
禁止项
- 在
else前面误加分号。 - 生成没有分号的裸分支赋值,例如
then value := 1 else ...;带else时用块式分支。 - 以为
try ... finally会吞掉异常。 - 在还没搞清表达式规则前,先把复杂业务函数塞进条件里。
- 把控制流问题和函数文件模型问题混在一起排查。
- 在计数
for循环体里给控制变量赋值。 - 在
for ... in遍历期间修改被遍历数组或其中元素。 - 用相邻两个
if ... then begin ... end表达互斥分支;它们是两条独立语句,需要互斥就写else。 - 在
for ... in只写一个控制变量却当成行值使用;单变量拿到的是下标/键。 - 以为二维数组会被
for ... in摊平成单元格序列;它按行推进,行本身是数组。
代码块身份:反例 / 不可照写
if flag > 0 then
value := 1;
else
value := 0;
上面这种写法会编译失败,因为 else 前面的分号会让 if 语句在上一行提前结束。生成带 else 的赋值分支时,不要改成省略分号的裸分支;使用前文的块式分支。