🐛 fix(tsl-syntax-reference): close verified syntax gaps

This commit is contained in:
csh
2026-08-13 14:49:43 +08:00
parent ede9287d5f
commit 8315f1060a
17 changed files with 619 additions and 96 deletions
+2 -2
View File
@@ -58,9 +58,9 @@ python <this-skill-dir>/scripts/lookup.py --section "syntax-05-004" "syntax-02-0
- 中文口语词自动扩展到 TSL 术语(如"打印"→输出/writeLn"列表"→数组,
"程序慢"→性能分析)。保留提取词的原写法即可。
- `tsl``tsf``tinysoft``program``debug``please` 这几个词不参与逐词匹配,
- `tsl``tsf``tinysoft``debug``please` 这几个词不参与逐词匹配,
但可能整体把查询导向某个专题页。无结果时靠加这类词补救没有用,改为换更具体的
语法要素名。
语法要素名。`program` 是真实 TSL 关键字,会参与精确匹配。
## 弱命中视同无匹配
+37 -9
View File
@@ -31,23 +31,42 @@
"函数指针释放": ["DeleteInstance", "MakeInstance"],
"Linux 动态加载": ["dlopen", "dlsym", ".so"],
"远程调用客户端": ["RDo", "RDo2", "客户端远程调用"],
"本地弹窗": ["RDo2", "InputQuery", "客户端远程调用"]
"本地弹窗": ["RDo2", "InputQuery", "客户端远程调用"],
"全局变量": ["global", "跨函数共享变量"],
"静态计算": ["static", "表达式缓存"],
"只计算一次": ["static", "表达式缓存"],
"指定系统函数": ["system", "系统函数限定"],
"内存上限": ["_maxMem_", "最大内存"],
"每列聚集": ["avgof", "多字段聚集"],
"按行广播": ["二维矩阵", "一维数组", "行广播"]
},
"page_intent_aliases": {
"01_quickstart.md": ["最简单能跑", "最简单的脚本", "天软脚本", "tinysoft"],
"02_core_model.md": [
"脚本和可复用",
"可复用函数文件",
"声明函数后面写代码"
"声明函数后面写代码",
"完整 program 入口",
"echo 输出多个值"
],
"03_values_and_literals.md": [
"字符串和数组下标",
"下标从几开始",
"下标起点"
],
"04_variables_and_constants.md": ["常量怎么声明", "变量能不能直接赋值"],
"05_functions_and_calls.md": ["默认参数", "函数怎么带"],
"06_expressions_and_operators.md": ["赋值和相等比较"],
"04_variables_and_constants.md": [
"常量怎么声明",
"变量能不能直接赋值",
"全局变量",
"运行时常量"
],
"05_functions_and_calls.md": ["默认参数", "函数怎么带", "指定系统函数"],
"06_expressions_and_operators.md": [
"赋值和相等比较",
"运算符优先级",
"静态计算",
"只计算一次"
],
"07_control_flow.md": [
"跳出去",
"循环里满足条件",
@@ -59,7 +78,12 @@
"08_objects_and_classes.md": ["定义类", "创建对象"],
"09_units_and_scope.md": ["多个文件", "复用一组函数"],
"10_runtime_context_and_with.md": ["临时切换系统参数"],
"11_matrix_and_collections.md": ["某行存在", "二维数组怎么判断"],
"11_matrix_and_collections.md": [
"某行存在",
"二维数组怎么判断",
"按行广播",
"二维矩阵乘一维数组"
],
"12_resultset_and_filters.md": ["保留匹配行", "按某一列"],
"13_ts_sql.md": [
"左连接",
@@ -67,7 +91,9 @@
"左联接",
"数据库",
"分组排序",
"聚合排序"
"聚合排序",
"JOIN 第二张表当前行",
"每列聚集"
],
"14_debug_and_profiler.md": [
"程序慢",
@@ -77,11 +103,13 @@
"执行了多久",
"执行时间",
"耗时",
"debug"
"debug",
"内存上限"
],
"15_lexical_structure_and_compile_options.md": [
"变量名区分大小写",
"注释怎么写"
"注释怎么写",
"嵌套注释"
],
"16_types_and_conversions.md": ["字符串转整数", "类型转换"],
"17_external_calls_and_threads.md": [
@@ -6,7 +6,7 @@
<!-- section-id: syntax-02-001 -->
回答“目标文件到底是 `.tsl` 脚本还是 `.tsf` 可复用声明文件,以及 `.tsl` 里的哪些内容会顺序执行、哪些内容只是后置声明”。
回答“目标文件到底是 `.tsl` 脚本还是 `.tsf` 可复用声明文件,`.tsl` 里的哪些内容会顺序执行、哪些内容只是后置声明,以及何时使用完整 `program ... begin ... end.` 入口形态”。
本页是文件模型的唯一事实源:后缀判断、语句区 / 声明区顺序、`.tsf` 顶层声明形态和文件名约束都在这里收口。函数体、类体、`unit` 内部的语法外形由各自专题页拥有。
@@ -33,6 +33,7 @@
- `uses` 可以出现在顶层,但这里只把它当成辅助语句,不把它当成主体声明;函数体和类定义体里的位置限制见 [09_units_and_scope.md](09_units_and_scope.md)。
-`class Name` 不作为类定义写法使用。
-`.tsl` 中,不要在声明区之后继续追加脚本语句。
- `program Name; ... begin ... end.` 是完整程序入口形态;它可以在主 `begin` 前声明子函数,但不是普通 `.tsl` 脚本的默认起手式。
- `unit` 默认先按完整形态理解;它也可以省略 `interface` / `implementation` 写成简写形态,见 [09_units_and_scope.md](09_units_and_scope.md)。
- 不要把 `.tsl` 写成只有顶层函数的模块;如果用户要通用可复用函数,优先写 `.tsf`
- 不要把 `.tsf` 写成会直接执行脚本语句的入口;如果用户要顺序执行入口,优先写 `.tsl`
@@ -184,6 +185,64 @@ end.
1
```
### 完整 `program` 入口形态
<!-- section-id: syntax-02-009 -->
<!-- tags: PROGRAM 入口, 完整程序, 独立脚本入口, CGI 入口, 主 begin 块 -->
普通 `.tsl` 默认仍使用前面的松散语句区模型。只有用户明确要求完整程序入口、独立脚本 / CGI 兼容形态,或现有代码已经使用 `program` 时,才写成下面的结构:
代码块身份:可直接照写示例
```tsl
program DemoProgram;
function DoubleValue(value);
begin
return value * 2;
end;
begin
echo "result:", DoubleValue(3);
end.
```
代码块身份:输出片段
```text
result:6
```
说明:
- `program Name;` 位于文件开头
- 子函数声明位于主 `begin ... end.` 之前
- 完整程序以 `end.` 结束,而普通函数声明仍以 `end;` 结束
- 不要把这套结构与“松散语句区在前、声明区在后”的默认 `.tsl` 模型混写
### `echo` 输出语句
<!-- section-id: syntax-02-010 -->
<!-- tags: echo 输出, 打印表达式, 输出多个值, 逗号输出, 控制台输出 -->
`echo expr1, expr2, ...;` 会按从左到右的顺序输出表达式列表。简单值直接输出;数组、对象等复杂值可能只显示类型摘要。具体输出目标由宿主环境决定,可能是控制台、Web 响应或客户端输出窗口。
代码块身份:可直接照写示例
```tsl
echo "A=", 1, ",B=", 2;
```
代码块身份:输出片段
```text
A=1,B=2
```
不要把 `echo` 的宿主输出位置或复杂对象的展示格式当成跨环境固定结果。
### 文件模型反例
<!-- section-id: syntax-02-006 -->
@@ -262,5 +321,6 @@ function:__main__:line 9: invalid statement
-`.tsl` 当成 `.tsf` 来写,只给一个顶层函数,不写任何会执行的脚本语句。
-`.tsf` 当成 `.tsl` 来写,在模块文件里直接堆顺序执行的脚本语句。
-`program ... begin ... end.` 与松散 `.tsl` 语句区 / 后置声明区混成一套文件结构。
-`uses` 当成主体声明,而不是辅助组织语句。
- `.tsf` 文件名与顶层声明不一致:`UserAccount.tsf` 中写 `function GetUser``type Customer = class``unit CustomerModule` 会导致加载失败或检索混乱。
@@ -24,6 +24,7 @@
<!-- quickstart-rule: index-origins -->
- `array(...)` 既可以写顺序数组,也可以写字符串键表;顺序数组和 `binary(...)` 二进制缓冲区下标从 `0` 开始,字符串下标从 `1` 开始。
- 数组元素可以是混合类型;给不存在的下标赋值会扩张数组。显式整数键可写成 `array(0: value0, 1: value1)`
- `s[0]` 在运行时会越界,不要把字符串当成 0 基下标。
- 字符串取子串用 `s[start:end]`,并且 `end` 是包含在结果里的。
- 字符串替换子串用 `s[start:end] := "..."`
@@ -36,6 +37,8 @@
- `U""` 不是宽串;需要在 UTF8、宽串、普通串之间显式转换时,继续用 `utf8ToUnicode(...)``utf8ToAnsi(...)``ansiToUtf8(...)``unicodeToUtf8(...)``string(...)``wideString(...)`
- `#number` 可以直接把字符码拼进字符串。
- `\0``#0` 都能把 ASCII `0` 放进字符串,并且不会把字符串截断。
- 字符串 `like` 的右侧按正则表达式解释,不是 SQL `%` / `_` 通配,也不要把正则里的 `*` 当成独立的 glob 通配符;默认匹配不区分大小写。
- 数值 `like` 表示近似相等;`likeEps``likeEpsRate` 分别控制绝对误差与相对误差阈值,默认值都是 `1e-6`
## 可直接照写示例
@@ -170,6 +173,33 @@ C
代码块说明:顺序数组 `items``0` 开始,字符串键表 `row` 用字符串键访问,字符串 `s``1` 开始。
数组按写入下标扩张:
代码块身份:可直接照写示例
```tsl
items := array(1);
items[3] := 4;
keyed := array(0: 2, 1: 3);
writeLn(length(items));
writeLn(items[1] = nil);
writeLn(items[3]);
writeLn(keyed[0]);
writeLn(keyed[1]);
```
代码块身份:输出片段
```text
2
1
4
2
3
```
说明:`length(items)` 统计已有项数,不等于“最大整数键 + 1”;下标 `1` 没有写入,因此读取为 `nil`
### 字符串边界规则
<!-- section-id: syntax-03-004 -->
@@ -416,20 +446,20 @@ A=1 B=2.34 C=AAA
- 纯小数可能走科学计数法:`"_" $ 0.0000005 $ ","` 返回 `"_5E-7,"`
- 带整数部分的小数保留精度:`"_" $ 1.0000005 $ ","` 返回 `"_1.0000005,"`
### 字符串 `like` 模式匹配
### 字符串与数值 `like`
<!-- section-id: syntax-03-007 -->
<!-- tags: 通配符匹配, 正则匹配, 正则表达式, 模糊匹配, 判断是否符合模式 -->
<!-- tags: 正则匹配, 正则表达式, 模糊匹配, 判断是否符合模式, 默认不区分大小写, 数值近似相等, like 精度 -->
`like` 用于判断字符串是否符合指定模式(支持通配符和正则表达式)
字符串 `like` 的右侧是正则表达式;默认匹配不区分大小写
代码块身份:可直接照写示例
```tsl
result1 := "hello" like "he*";
result2 := "HELLO" like "he*";
result3 := "test@example.com" like "*@*";
result1 := "a" like "A";
result2 := "HELLO" like "hello";
result3 := "2009-1-1" like "\\d+-\\d+-\\d+";
writeLn("result1:", result1);
writeLn("result2:", result2);
writeLn("result3:", result3);
@@ -439,22 +469,49 @@ writeLn("result3:", result3);
```text
result1: 1
result2: 0
result2: 1
result3: 1
```
说明:
- `like` 大小写敏感,`"hello" like "he*"` 返回 `1`(真),`"HELLO" like "he*"` 返回 `0`
- `*` 是通配符,匹配任意字符序列
- `like` 也支持正则表达式模式(如 `"\\d{4}-\\d{2}-\\d{2}"` 匹配日期格式)
- 默认情况下,`"a" like "A"` `"HELLO" like "hello"` 返回 `1`
- `"\\d+-\\d+-\\d+"` 是正则表达式,匹配示例中的日期形态
- 正则量词 `*` 只修饰它前面的原子;不要把 `"he*"` 解释成“`he` 后面任意字符”的 glob 模式,需要任意字符序列时写成 `.*`
- 匹配控制标志可以改变大小写规则;控制标志的精确 API、参数与目标 scope 使用 `tsl-api-reference` skill 核对
数值 `like` 用于近似相等判断:
代码块身份:可直接照写示例
```tsl
old_abs_eps := likeEps;
old_rate_eps := likeEpsRate;
likeEps := 0.1;
likeEpsRate := 0;
writeLn(1.05 like 1);
likeEps := old_abs_eps;
likeEpsRate := old_rate_eps;
```
代码块身份:输出片段
```text
1
```
说明:
- `likeEps``likeEpsRate` 都可读写,默认值均为 `1e-6`
- 两数绝对差小于 `likeEps`,或“绝对差 / 两数绝对值的平均值”小于 `likeEpsRate` 时,数值 `like` 返回真
- 临时修改阈值后要恢复旧值,避免影响同一运行时中的后续比较
`not like` 是取反形式(TSL 2025/8 版本起支持):
代码块身份:可直接照写示例
```tsl
result := "abc" not like "xyz*";
result := "abc" not like "xyz.*";
writeLn(result);
```
@@ -547,6 +604,10 @@ items := array(1, 2, 3);
- 把普通字符串里的 `\uXXXX` 直接当成宽串单字符。
-`U""` 当成宽串;需要宽串时按本页转换链处理。
-`#0` / `\0` 当成 C 风格字符串终止符。
- 把字符串 `like` 误写成默认区分大小写;默认行为是不区分大小写。
-`like` 只理解成字符串模式匹配,遗漏数值近似相等语义。
- 修改 `likeEps` / `likeEpsRate` 后不恢复,导致后续代码继承意外的比较阈值。
- 以为给较大整数下标赋值后,中间所有位置都会自动生成实际元素;未写入的位置仍为 `nil`
代码块身份:反例 / 不可照写
@@ -6,7 +6,7 @@
<!-- section-id: syntax-04-001 -->
回答“普通变量怎样直接使用、`var` 在什么位置出现、常量必须怎样初始化、哪些名字一旦绑定就不能再赋值”。
回答“普通变量怎样直接使用、`var` 在什么位置出现、常量必须怎样初始化、`global` 怎样跨函数共享,以及运行时常量怎样冻结一次调用中的表达式结果”。
## 核心规则
@@ -27,6 +27,8 @@
- 右侧数组元素也可以是数组;拆出来的变量会直接得到对应子数组。
- 多参数赋值也可以出现在函数调用参数里。
- `{$explicit+}` 开启后,后续变量必须先用 `var` 声明;未声明变量会报 `variable not defined`
- `global x, y;` 声明当前任务中的全局变量;每个需要访问它的函数都要再次写 `global` 声明。
- `const name := expression;` 是运行时常量:进入所属函数时计算,当前调用内不可再次赋值;不要与编译时常量 `const name = expression;` 混写。
## 可直接照写示例
@@ -178,6 +180,71 @@ const kMaxRetries = 3 + 4;
value := kMaxRetries;
```
### 全局变量 `global`
<!-- section-id: syntax-04-009 -->
<!-- tags: 全局变量, 跨函数共享变量, 不同函数访问同一变量, global 声明 -->
`global` 让同一运行任务中的不同函数访问同一变量。每个需要读写该变量的函数都要声明它;漏写时,同名标识符会按局部变量处理。
代码块身份:可直接照写示例
```tsl
global shared_value;
shared_value := 37;
writeLn(ReadShared());
function ReadShared();
begin
global shared_value;
return shared_value;
end;
```
代码块身份:输出片段
```text
37
```
本节只拥有“当前任务内跨函数共享”和“每个引用函数都要声明”的规则;不要从中推断跨任务持久化、线程隔离或进程级生命周期。
### 运行时常量 `const name := expression`
<!-- section-id: syntax-04-010 -->
<!-- tags: 运行时常量, 表达式常量, 每次调用重新计算, 初始化后不能修改 -->
运行时常量使用 `:=` 初始化,可以依赖参数、变量或函数调用。它在每次进入所属函数时重新计算一次,随后在本次调用内不可修改。
代码块身份:可直接照写示例
```tsl
writeLn(FreezeValue(2));
writeLn(FreezeValue(5));
function FreezeValue(value);
begin
const fixed := value + 1;
return fixed;
end;
```
代码块身份:输出片段
```text
3
6
```
边界:
- 编译时常量写 `const name = expression;`;运行时常量写 `const name := expression;`
- 运行时常量声明放在函数 `begin ... end` 语句体中,不放在函数头后的 `const` 声明段
- 运行时常量不作为类成员写法
- 初始化后再次赋值会破坏常量约束,不要生成
### 多参数赋值
<!-- section-id: syntax-04-006 -->
@@ -331,6 +398,9 @@ items := array(1, 2, 3);
- 以为 `const` 可以只声明名字,不写初始化表达式。
- 以为单变量拆包可以写成 `[a] := array(...)`
- 以为 `{$explicit+}` 开启后仍然可以继续直接写未声明变量。
- 只在脚本顶层声明一次 `global`,却忘记在读取它的函数里再次声明。
- 把运行时常量 `const name := expression;` 写进函数头后的编译时 `const` 声明段。
- 把运行时常量当成可重新赋值的普通变量或类成员。
代码块身份:反例 / 不可照写
@@ -50,7 +50,8 @@
- 一旦某次调用里开始使用命名参数,后面的参数就不能再退回位置参数。
- 对二进制函数 / 系统函数直接使用命名参数,会报 `named parameter mode can't support here`;这类函数要先用 TSL 再封一层。
- 函数参数支持默认值。
- 普通函数的默认值规则不要直接等同到 `unit interface` 声明;跨 `unit` 的默认参数边界只照本页最小反例和 [09_units_and_scope.md](09_units_and_scope.md) 处理
- `unit interface` 中的默认参数可以引用该接口中可访问的 `const`;实现函数头不要重复声明默认值
- 默认参数表达式属于新一代 TSL 能力;表达式里引用变量时,该变量按 `0` 求值。面向旧运行时或版本不明时,默认只生成字面量 / 可访问常量默认值。
- 尾部 `...` 形式的可变参数属于文档明确写法。
- 在可变参数函数体里,`Params``ParamCount``RealParamCount` 都可用。
- 可变参数组可以通过 `...` 转发给另一个函数调用。
@@ -61,6 +62,7 @@
- 匿名函数和 TSL 函数值的稳定调用方式仍是 `call(f, ...)``##f(...)`
- `f(...)` 这种“函数变量直接调用”写法不作为可写事实;无论 `f` 是匿名函数、`findFunction(...)` 还是 `thisFunction(...)` 返回的函数指针,都不要默认写成直调。
- `::FuncName(...)` 可以指向全局/系统函数,用来绕过当前作用域里的同名局部函数。
- `system.FuncName(...)` 专门强制指定系统函数;它与 `::FuncName(...)` 的全局限定语义不要互相替代。
- `external`、原生函数指针包装、`makeInstance` / C 回调和线程调用的事实见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。
- 不要在 `.tsl` 的函数声明区之后继续追加脚本语句。
@@ -502,7 +504,7 @@ begin
end;
```
默认值也可以写成表达式:
新一代 TSL 的默认值也可以写成表达式:
代码块身份:可直接照写示例
@@ -523,24 +525,56 @@ end;
- `Pack(a: 1)` 返回 `12`
- `ExprDefault()` 返回 `3`
`unit interface` 声明下的默认参数要单独看。普通函数默认参数可用,不等于跨 `unit` 声明边界也同样可靠。
表达式默认值的版本边界:
代码块身份:反例 / 不可照写
代码块身份:可直接照写示例
```text
unit UnitConst;
interface
const default_value = 888;
function F(a, b = 100, c = default_value);
```tsl
function RefDefault(a, b = a + 1);
begin
return b;
end;
```
边界说明:
- `F(1)` 输出 `101``F(1, 2)` 输出 `3`
- 同一组文件下,`UnitConst.default_value` 可读到 `888`,而 `F(1, 2, 3)` 输出 `6`
- 因此不要把“普通函数默认参数可用”直接泛化成“`unit interface` 里引用 `unit const` 的默认参数也同样可靠”
- 这类跨 `unit` 的声明边界事实见 [09_units_and_scope.md](09_units_and_scope.md)
- 在支持默认参数表达式的新一代 TSL 中,`RefDefault(5)` 返回 `1`因为默认表达式里的变量 `a` `0` 求值
- 这项能力自 2025-08-27 后的 NG 客户端 / 新一代 TSL 服务端提供;目标版本不明时不要生成变量参与的默认表达式
`unit interface` 可以使用接口中可访问的常量作为默认值:
代码块身份:配置片段 / 概念骨架
```text
// UnitDefaults.tsf
unit UnitDefaults;
interface
const default_value = 888;
function F(a, b = 100, c = default_value);
implementation
function F(a, b, c);
begin
return a + b + c;
end;
end.
// main.tsl
uses UnitDefaults;
echo F(1), ",", F(1, 2);
```
代码块身份:输出片段
```text
989,891
```
说明:默认值只在 `interface` 声明处写一次;实现函数头使用同一组形参,但不重复 `= ...`
### 可变参数 `...`
@@ -856,7 +890,26 @@ end;
<!-- section-id: syntax-05-013 -->
`external`、原生函数指针包装、`makeInstance` / C 回调和线程调用,统一见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。这一篇只保留“普通函数怎样定义和调用”的主线。
<!-- tags: system 前缀, 指定系统函数, 绕过同名用户函数, 系统函数限定 -->
系统函数可能与用户定义函数同名。需要明确指定系统实现时,使用 `system.FuncName(...)`
代码块身份:可直接照写示例
```tsl
value := system.strToInt("123");
writeLn(value);
```
代码块身份:输出片段
```text
123
```
本节只拥有 `system.` 这一调用限定语法。示例中的真实函数名、签名和目标 scope 必须由 `tsl-api-reference` skill 核对;不要从本节推断任意系统 API。
`external`、原生函数指针包装、`makeInstance` / C 回调和线程调用,统一见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。
## 默认生成模板
@@ -911,7 +964,10 @@ end;
-`a = 1` 这种比较表达式误当成命名参数调用。
- 以为默认值只能用于无类型参数。
-`const` 形参上直接赋值。
- 把普通函数的默认值规则原样套到 `unit interface` 里的 `const` 默认参数
- 在旧运行时或版本不明时生成默认参数表达式
- 以为默认参数表达式里的形参会取本次调用实参;其中变量按 `0` 求值。
-`unit interface``implementation` 的函数头上重复写默认值。
-`::FuncName(...)``system.FuncName(...)` 互相替代,而不区分全局限定与系统函数限定。
- 把匿名函数或 `findFunction(...)` 返回值默认写成 `f(...)` 直调。
- 把命名参数直接套到二进制函数或系统函数上。
- 在一次调用里先进入命名参数模式,后面又退回位置参数。
@@ -1000,7 +1056,7 @@ const default_value = 888;
function F(a, b = 100, c = default_value);
```
不要把上面这种 `unit interface` 声明直接当成已经等价于普通函数默认参数规则。按文档结果,对应的 `F(1)` 输出是 `101`,不是按 `default_value = 888` 补成的结果;具体边界见 [09_units_and_scope.md](09_units_and_scope.md)
上面的片段缺少 `implementation``end.`,因此不能作为完整 `unit` 文件直接照写;它不是“接口常量不能作为默认值”的反例。完整可写结构见本页默认参数章节
代码块身份:反例 / 不可照写
@@ -101,6 +101,7 @@
- 连续标量比较才用 `:>``:<``:<>``:==``:>=``:<=`
- 数组逐元素链式比较才用 `::>``::<``::<>``::==``::>=``::<=`
- 混合两类以上运算符时,优先用括号明确分组,不依赖跨语言记忆里的优先级。
- TSL 的主要优先级从高到低是:成员/下标/调用,`not`、前置自增减与倒数/逆等一元运算,字符串 `$`,幂,乘除移位,加减(包括一元正负号)与集合/位运算,比较与 `in`/`like`/`is``and`/`or`,冒号,赋值,表达式前导 `@`;同级通常从左到右。
边界规则:
@@ -566,6 +567,113 @@ writeLn(if 2 > 1 then 2 else 1);
`if condition then true_value else false_value` 必须带 `else`,否则不是本页可照写的表达式形态。
### 运算符优先级
<!-- section-id: syntax-06-015 -->
<!-- tags: 运算符优先级, 谁先计算, 先乘除后加减, and or 优先级, 表达式加括号 -->
下表是面向当前正式文档已收录运算符的保守分组,数字越小优先级越高:
<!-- prettier-ignore-start -->
| 级别 | 主要形态 | 说明 |
| --- | --- | --- |
| 0 | `()``[]``.``?.`、函数调用 | 分组、访问、下标与调用最先结合。 |
| 1 | `not`、前置 `++` / `--``!``.!``.!!` | 逻辑非、前置自增减与倒数/逆等一元运算。 |
| 2 | `$` | 字符串连接。 |
| 3 | `^``~``:^` | 幂、开方与对应矩阵形态。 |
| 4 | `*``/``\``%``div``mod``shl``shr``rol``ror` | 乘除、取余与移位。 |
| 5 | `+``-`、一元正号 / 负号、集合运算、点前缀位/逻辑运算 | 加减、一元正负号及同组语言运算;因此幂先于一元正负号结合,例如 `-2 ^ 2` 等于 `-(2 ^ 2)`。 |
| 6 | 比较、`is``in``sqlin``like`、链式比较 | 关系判断。 |
| 7 | `and``or` | 低于比较,因此 `a > 1 and b < 2` 按两个比较再逻辑与理解。 |
| 8 | `:` | 冒号相关表达式形态。 |
| 9 | `:=` 与各类复合赋值 | 赋值接近最低优先级。 |
| 10 | `@` | 表达式对象前导最低。 |
<!-- prettier-ignore-end -->
同级运算通常从左到右求值。为了避免不同语言之间的优先级记忆混淆,混合两类以上运算符时仍推荐显式加括号:
代码块身份:可直接照写示例
```tsl
value := 1 + 2 * 3;
signedPower := -2 ^ 2;
flag := (2 > 1) and (1 < 2);
writeLn(value);
writeLn(signedPower);
writeLn(flag);
```
代码块身份:输出片段
```text
7
-4
1
```
稀有矩阵运算符的详细优先级以其专题页为准;不要用本表外推尚未写入正式文档的符号。
### 静态计算表达式 `static`
<!-- section-id: syntax-06-016 -->
<!-- tags: 静态计算, 只计算一次, 表达式缓存, static name, 按键缓存 -->
表达式前的 `static` 会缓存第一次计算结果。带 `name` 时,名称表达式是缓存键:同一键复用第一次结果,不同键分别计算。
代码块身份:可直接照写示例
```tsl
echo StaticValue(), ",", StaticValue();
function StaticValue();
begin
return static NextValue();
end;
function NextValue();
begin
global static_calls;
static_calls += 1;
return static_calls;
end;
```
代码块身份:输出片段
```text
1,1
```
按键分别缓存:
代码块身份:可直接照写示例
```tsl
echo NamedValue("A"), ",", NamedValue("A"), ",", NamedValue("B");
function NamedValue(key);
begin
return static NextValue() name key;
end;
function NextValue();
begin
global named_static_calls;
named_static_calls += 1;
return named_static_calls;
end;
```
代码块身份:输出片段
```text
1,1,2
```
这不是类成员的 `static` 字段;类静态成员见 [08_objects_and_classes.md](08_objects_and_classes.md)。缓存结果具有运行时状态,不要用它保存每次调用都必须重新计算的值。
### 表达式对象
<!-- section-id: syntax-06-007 -->
@@ -847,6 +955,8 @@ writeLn(value);
-`if` 表达式写成没有 `else` 的半句。
- 把本页明确的 `c?.a?.[1]` 外推成所有深链式空安全访问都可靠。
- 从其他语言推断 TSL 运算符能力。
- 把类成员 `static field;` 与表达式前导 `static Expression [name Key]` 当成同一种语法。
- 在复杂混合表达式里依赖其他语言的优先级记忆而省略括号。
代码块身份:反例 / 不可照写
@@ -16,6 +16,8 @@
- 块式分支内部的普通语句必须用分号结尾。
- 控制流块的 `begin ... end` 后可以加分号也可以不加(语法都允许)。
- `for` 支持 `to``downto`、可选 `step`,以及 `for i, v in array` 遍历。
- 计数 `for` 的初值、终值和步长确定后,循环次数随之固定;循环体内不要给控制变量赋值。
- `for i, v in array` 遍历期间,不要修改被遍历数组或其中元素。
- `while``repeat ... until` 都可直接使用;`repeat` 至少会先执行一轮再判断结束条件。
- `break` 会跳出当前最近一层循环,`continue` 会跳过当前轮剩余语句。
- `case ... of ... else ... end` 可作为语句形态生成;`end` 后可以加分号也可以不加。
@@ -154,6 +156,7 @@ for i, value in numbers do
- 依次输出 `10``120``230`
- 这说明 `for i, value in numbers` 里的 `i``0` 开始
- 遍历期间把 `numbers` 当成只读集合;需要修改时先遍历副本,或在循环结束后统一写回
代码块身份:输出片段
@@ -495,6 +498,8 @@ end
- 以为 `try ... finally` 会吞掉异常。
- 在还没搞清表达式规则前,先把复杂业务函数塞进条件里。
- 把控制流问题和函数文件模型问题混在一起排查。
- 在计数 `for` 循环体里给控制变量赋值。
- 在 `for ... in` 遍历期间修改被遍历数组或其中元素。
代码块身份:反例 / 不可照写
@@ -18,7 +18,7 @@
- `setSysParam(key, value)``getSysParam(key)` 可以直接用字符串键。
- `sysParams[key]` 可以直接读写这些运行时参数。
- 块环境语句可写成 `with *, sys_param_values do begin ... end``with **, sys_param_values do begin ... end`
- `with *` 会把提供的系统参数合并进当前运行时上下文;不要依赖它在块结束后自动恢复外层值
- `with *` 会把提供的系统参数合并进当前运行时上下文。普通自定义键可能在块后保留新值;特殊系统环境变量会在块结束后恢复,因此不要把两类键的恢复行为混为一谈
- `with **` 会用提供的系统参数建立隔离块环境;块结束后恢复外层系统参数。
- 后缀 `with` 形式写在函数文件调用后面:`#Func() with array(...)`
- `with array(...)` 只在该次调用里临时覆盖对应键,调用结束后会恢复外部原值。
@@ -110,7 +110,8 @@ writeLn(getSysParam("b"));
- 块内输出 `2``3`
- 块后输出 `2``3`
- 说明 `with *` 会把传入键合并进当前系统参数上下文;不要把它当成自动恢复外层值的隔离块
- 这组实验只证明普通自定义键 `"a"` / `"b"` 会合并并在块后保留新值
- 特殊系统环境变量在块结束后会恢复;不要用普通字符串键的结果外推股票、日期等特殊环境
`with *, SysParamArray do` 使用当前所有系统参数:
@@ -443,7 +444,7 @@ end;
- 把系统参数页直接写成金融函数页。
- 把 `#Func() with array(...)` 误判成也能直接套在本地函数 `Demo()` 后面。
- `with *` 误判成会自动恢复外层系统参数
- 以为 `with *` 对所有键都统一“不恢复”或统一“恢复”;普通自定义键与特殊系统环境变量的边界不同
- 以为 `with array(...)` 改的是全局永久值,不会恢复外层原环境。
- 把网格句柄直接当最终值用,而不做 `dupvalue(...)`
- 以为从全局缓存取出的值,本地写入后仍然保持缓存身份。
@@ -31,7 +31,7 @@
<!-- section-id: syntax-11-004 -->
<!-- tags: 建数组, 数组下标, 数组索引, array 下标, 字典, 键值对, 二维数组, 嵌套数组, 按名字取值 -->
<!-- tags: 建数组, 字典, 键值对, 二维数组, 嵌套数组, 按名字取值 -->
顺序数组与字符串键表:
@@ -299,7 +299,7 @@ writeLn("子集 (1,0):", subset[1][0]);
<!-- section-id: syntax-11-011 -->
<!-- tags: 长度不一致, 缺位补零, 标量广播, 数组和数字运算 -->
<!-- tags: 长度不一致, 缺位补零, 标量广播, 数组和数字运算, 按行广播, 二维矩阵乘一维数组 -->
基础算符作用于非完全矩阵(行长度不一致或字符串键不对齐的数组)时,对应位置不存在或为 `nil` 时**默认当 0 处理**
@@ -356,6 +356,32 @@ writeLn("(1,1):", result[1][1]);
- `matrix_value + 10` 每个元素都加 10
- 这些是逐元素运算(element-wise),区别于矩阵乘法 `:*`,见 [21_matrix_deep_dive.md](21_matrix_deep_dive.md)
二维矩阵与一维数组做基础算术时,一维数组按“行”广播;它的长度必须等于矩阵行数:
代码块身份:可直接照写示例
```tsl
matrix_value := array((1, 2, 3), (4, 5, 6));
row_factors := array(10, 100);
left_result := matrix_value * row_factors;
right_result := row_factors * matrix_value;
writeLn(left_result[0][0]);
writeLn(left_result[1][2]);
writeLn(right_result[0][1]);
writeLn(right_result[1][0]);
```
代码块身份:输出片段
```text
10
600
20
400
```
说明:第一行使用 `10`,第二行使用 `100`;左右操作数交换后仍按行广播。不要把这条规则误写成按列广播。
## 本页不生成的范围
<!-- section-id: syntax-11-012 -->
@@ -383,3 +409,4 @@ writeLn("(1,1):", result[1][1]);
- 不要把普通 `array(...)` 自动升级成 `FMArray``FMArray` 专属事实见 [22_fmarray.md](22_fmarray.md)。
- 不要把点前缀比较 `.>` 和矩阵链式比较 `::>` 混用;`.>` 返回逻辑数组,`::>` 是链式比较。
- 不要以为非完全矩阵缺位会报错;默认当 `0` 处理。
- 二维矩阵与一维数组运算时,不要把一维数组当成按列因子;它按行广播且长度要匹配行数。
@@ -19,8 +19,9 @@
- 在一维数组上做 TS-SQL 时,优先使用 `thisRow``thisRowIndex`
- `select` 返回二维结果,`sselect` 返回一维结果,`vselect` 返回单值,`mselect` 返回 `Matrix`
- `where``group by``order by` 可以直接接在 `from` 后面继续使用;`order by` 支持 `asc`/`desc` 与多列逗号分隔。
- 分组后按聚集条件筛选用 `having``where` 不能用聚集);`having``countof([字段])``countof(1)`,不要用 `countof(*)`
- 分组后按聚集条件筛选用 `having``where` 不能用聚集);计数可`countof([字段])``countof(1)``countof()`,也可用带空格的 `countof( * )`。无空格的 `countof(*)` 会与块注释起始符 `(*` 冲突
- 多表 `join` 时,字段访问应写成 `[表序号].["字段名"]``on` 可用 `and` 写多条件;`[表序号].*` 取整表列。
- 多表联接中,`thisRow(表序号)``thisRowIndex(表序号)` 分别取得指定来源表的当前整行与原始下标。
- 联接类型:`left join` 保留左表、`right join` 保留右表、`full join` 保留双方、`cross join` 笛卡尔积、逗号联接等价于 `cross join`;不匹配处用 `nil` 填充。
- `select` 列表支持 `distinct` 去重、`as 别名``as nil`(参与计算但不返回)、`起始列 to 结束列` 字段区间、`selectopt(位选项)``drange(区间/M of N)`
- 聚集函数统一形态 `Func(Expr[, Cond[, N[, MovingFirst[, CacheId]]]])`:条件聚集、移动聚集、多字段聚集、`refof(Expr, N)` 引用相对行;`aggof('名', Expr)` 调用自定义聚集回调。
@@ -233,6 +234,23 @@ writeLn(join_result[0]["V2"]);
100
```
联接上下文里的指定来源当前行:
代码块身份:可直接照写示例
```tsl
left_rows := array(("id": 1, "v": 10), ("id": 2, "v": 20));
right_rows := array(("id": 2, "v": 20), ("id": 3, "v": 30));
join_rows := select thisRow(1) as "LeftRow",
thisRow(2) as "RightRow",
thisRowIndex(1) as "LeftIndex",
thisRowIndex(2) as "RightIndex"
from left_rows join right_rows on [1].["id"] = [2].["id"]
end;
```
结果说明:唯一匹配行中,`LeftRow["v"] = 20``RightRow["v"] = 20``LeftIndex = 1``RightIndex = 0`
### `thisGroup`
<!-- section-id: syntax-13-011 -->
@@ -773,6 +791,29 @@ writeLn(ref_prev[1]["Expr1"]);
- 移动聚集:`avgof(表达式, 条件, N, MovingFirst)` 取当前行往前 N 条的滑动统计
- `refof(表达式, N)` 引用前 N 行的值(`N` 为负则往后);首行无前值时返回 `0`
`*` 作为聚集输入时必须和左括号留空格,避免 `(*` 被词法层识别为块注释:
代码块身份:可直接照写示例
```tsl
a := array(("x": 1, "y": 10), ("x": 3, "y": 20));
row_count := vselect countof( * ) from a end;
column_avg := select avgof( * ) from a end;
writeLn(row_count);
writeLn(column_avg[0]["Expr1"]);
writeLn(column_avg[0]["Expr2"]);
```
代码块身份:输出片段
```text
2
2.0
15.0
```
说明:`avgof( * )` 对每列分别聚集;这里两列平均值依次为 `2``15`
### `group by ... having`
<!-- section-id: syntax-13-024 -->
@@ -800,7 +841,7 @@ A
说明:
- `having 聚集条件` 在分组后筛选(上例只保留成员数大于 1 的 `A` 组)
- `having` 里的计数用 `countof([字段])``countof(1)`
- `having` 里的计数优先`countof([字段])``countof(1)``countof()`;确需星号时写成 `countof( * )`
代码块身份:反例 / 不可照写
@@ -808,7 +849,7 @@ A
having_rows := select ["cls"] from a group by ["cls"] having countof(*) > 1 end;
```
`countof(*)` 这种带 `*` 的写法不成立,会`CountOf ( not found`计数改用 `countof([字段])``countof(1)`
无空格的 `countof(*)` 会把 `(*` 词法组合解释成块注释开头,随后`CountOf ( not found`。改用 `countof( * )``countof()``countof([字段])``countof(1)`
### `thisOrder` 与多列 `order by`
@@ -967,7 +1008,7 @@ query_result := select * from source_rows end;
- 在 `left join` 时省略 `on` 子句或不用 `[表序号].["字段"]` 形式。
- 在 `insert` 时漏掉 `insertfields` 或字段数与值数不匹配。
- 期望 `update`/`delete` 返回新数组;它们直接修改原数组。
- `countof(*)` 数行数;`*` 星号形式不被支持,改用 `countof([字段])``countof(1)`
- 写无空格的 `countof(*)`,使 `(*` 与块注释起始符冲突;改用 `countof( * )``countof()``countof([字段])``countof(1)`
代码块身份:反例 / 不可照写
@@ -975,7 +1016,7 @@ query_result := select * from source_rows end;
n := vselect countof(*) from source_rows end;
```
`countof(*)` 会报 `CountOf ( not found`。数行数改用 `countof([字段])` `countof(1)`
`countof(*)` 会报 `CountOf ( not found`,原因是 `(*` 与块注释起始符冲突。星号写法加空格为 `countof( * )`,或改用 `countof()` / 明确表达式
代码块身份:反例 / 不可照写
@@ -6,7 +6,7 @@
<!-- section-id: syntax-14-001 -->
回答“`goto``debugReturn``debugRunEnv``mtic` / `mtoc``setProfiler``__line__``__stack_frame` 怎样写、会怎样表现”。
回答“`goto``debugReturn``debugRunEnv``mtic` / `mtoc``setProfiler`内存伪变量、`__line__``__stack_frame` 怎样写、会怎样表现”。
## 核心规则
@@ -24,6 +24,7 @@
- `setProfiler(7)` 配合 `getProfilerInfo(1)`,可以在不弹窗的情况下拿到性能分析器信息。
- `__line__` 会返回所在代码行号。
- `__stack_frame` 会返回调用栈帧数组;最小 `toStn(...)` 观察结果里,每一项是 `(line, "function")` 这一类二元组。
- `_myMem_` 表示应用当前已使用内存,`_maxMem_` 表示应用允许使用的最大内存;两者是只读数值伪变量。单位和 `_maxMem_` 的具体取值由宿主环境决定。
## 可直接照写示例
@@ -217,6 +218,32 @@ writeLn(length(info) > 0);
- 说明 `setProfiler(7)` 可以开启性能分析器统计
- 说明 `getProfilerInfo(1)` 会直接返回性能分析器信息,而且结果是非空数组
### 内存伪变量 `_myMem_` / `_maxMem_`
<!-- section-id: syntax-14-011 -->
<!-- tags: 已用内存, 内存上限, 最大内存, 内存伪变量, _myMem_, _maxMem_ -->
代码块身份:可直接照写示例
```tsl
writeLn(ifNumber(_myMem_));
writeLn(ifNumber(_maxMem_));
```
代码块身份:输出片段
```text
1
1
```
说明:
- `_myMem_` 是应用已使用内存的数值
- `_maxMem_` 是宿主允许应用使用的最大内存数值;某些环境可能返回 `0` 表示未给出可比较的上限
- 不要假设 `_maxMem_ >= _myMem_`,也不要在没有宿主文档时写死单位
### `__line__``__stack_frame`
<!-- section-id: syntax-14-009 -->
@@ -276,3 +303,4 @@ array(
- 不要假设 `goto` 可以跨函数、跨脚本体或跳到单独成行的 `label`
- 不要给计时或性能分析器调用补未写入文档参数。
- 不要把调试客户端副作用写成普通输出事实。
- 不要假定 `_maxMem_` 总是非零、总是大于 `_myMem_`,或擅自指定内存单位。
@@ -13,7 +13,7 @@
<!-- section-id: syntax-15-002 -->
- 标识符大小写无关;下划线可出现在标识符中。
- `//` 是行注释;首行 `#!` 可作为 CGI 风格注释;`{ ... }``(* ... *)` 是块注释。
- `//` 是行注释;首行 `#!` 可作为 CGI 风格注释;`{ ... }``(* ... *)` 是块注释。两种块注释可以交错嵌套,同类块注释不能嵌套。
- 条件编译指令使用 `{$define}``{$undef}``{$ifdef}``{$ifndef}``{$else}``{$endif}`
- 条件编译只编译命中的分支;未命中的分支不参与脚本编译。
- `{$explicit+}` 开启后,后续变量必须先用 `var` 声明;`{$explicit-}` 可以在同一源文件里重新关闭这个要求。
@@ -58,7 +58,7 @@ TSL 关键字大小写无关;本表统一按文档推荐写法展示。生成
<!-- section-id: syntax-15-005 -->
<!-- tags: 注释怎么写, 大小写敏感吗, 变量命名, 下划线 -->
<!-- tags: 注释怎么写, 大小写敏感吗, 变量命名, 下划线, 嵌套注释, 块注释 -->
大小写无关与下划线标识符:
@@ -112,6 +112,8 @@ writeLn(40);
- 依次输出 `1``10``30`
- 说明首行 `#!``//``{ ... }``(* ... *)` 都属于文档明确注释形态
- 说明外层 `{ ... }` 可以包含 `(* ... *)`;反向交错也可用
- 同类 `{ { ... } }``(* (* ... *) *)` 不构成嵌套注释,会产生语法错误
- 说明 `define` / `undef` / `ifdef` / `ifndef` / `else` / `endif` 这一组条件编译指令可以正常生效
### 显式变量声明开关
@@ -184,27 +186,13 @@ writeLn(1);
<!-- tags: 编译开关, 编译器选项, 改默认行为 -->
`{$CompileOption}` 用于设置编译期开关,改变编译器的默认行为
TSL 的编译选项使用 `{$Option+}` / `{$Option-}` 一类指令改变后续源码的编译方式。本页只拥有已经分别验证并有专题规则的选项
代码块身份:可直接照写示例
- `{$explicit+}` / `{$explicit-}`:切换变量是否必须预先声明,见上一节
- `{$varByRef+}` / `{$varByRef-}`:切换未修饰形参的默认传递方式,见下一节
- `{$ifdef ...}` 等条件编译指令:控制分支是否参与编译
```tsl
{$CompileOption optimize=1}
echo 1 + 1;
```
代码块身份:输出片段
```text
2
```
说明:
- `{$CompileOption optimize=1}` 开启优化
- 编译选项从出现位置开始生效,直到源文件结束或被其他选项覆盖
- 常见选项包括 `optimize``buffermode``DebugInfo`
- 编译选项细节以项目工具链和实际编译命令为准。
`optimize``buffermode``DebugInfo` 等没有在本 skill 中形成可验证语义,不作为正式可生成选项。不要用“脚本仍能输出结果”来证明某个未知编译选项确实生效。
### 参数默认传递开关
@@ -263,6 +251,8 @@ end;
<!-- section-id: syntax-15-010 -->
- 不要在 `{$explicit+}` 后继续直接使用未声明变量。
- 不要同类嵌套 `{ ... }``(* ... *)` 块注释;需要嵌套时交错使用两种定界符。
- 不要生成未在本页形成可验证规则的 `{$CompileOption optimize=...}``buffermode``DebugInfo`
- 不要把 `{$i ...}` / `{$include ...}` 包含文件写法当成可用能力。
- 不要把 `反例 / 不可照写` 代码块复制进正向示例。
@@ -22,7 +22,7 @@
- 完整 `unit` 形态可以包含 `interface``implementation``initialization``finalization`,并以 `end.` 结束。
- `initialization``unit` 第一次被实际使用时触发,不是只因为顶层写了 `uses` 就立刻执行。
- `finalization` 会在脚本结束前触发。
- 直接写 `DemoUnit.Member` 时,可以读到 `interface` `implementation` 里的常量、变量
- 跨版本安全边界只保证 `interface` 中声明的常量、变量和函数可由引用者访问;只在 `implementation` 中声明的成员按私有内容处理
- `findFunction("DemoUnit")` 拿到的是 `unit` 对象入口;本页只把它稳定暴露 `interface` 成员写成文档事实。
- `DemoUnit.var_name := value` 这种限定赋值不作为可写事实;如果要改 `unit` 状态,应导出函数或方法来改。
- `tslfilename()` 的参数规格使用 `tsl-api-reference` skill 按名查询;本页只保留它返回正在执行的 `.tsl` 主脚本完整路径这一行为事实。
@@ -111,7 +111,7 @@ FINAL
<!-- tags: 读模块常量, 访问 unit 成员, 限定名读取 -->
直接限定读取:
跨版本安全的限定读取:
代码块身份:配置片段 / 概念骨架
@@ -149,8 +149,6 @@ uses DemoUnit;
writeLn(DemoUnit.public_const);
writeLn(DemoUnit.public_var);
writeLn(DemoUnit.impl_const);
writeLn(DemoUnit.impl_var);
writeLn(PublicFunc());
```
@@ -158,10 +156,10 @@ writeLn(PublicFunc());
- `DemoUnit.public_const` 输出 `1`
- `DemoUnit.public_var` 输出 `3`
- `DemoUnit.impl_const` 输出 `2`
- `DemoUnit.impl_var` 输出 `4`
- `PublicFunc()` 输出 `10`
- 本页文档边界是:实现段函数仍私有,但实现段常量和变量可以通过 `DemoUnit.Member` 直接读取
- 本页的跨版本文档边界是:外部只依赖 `interface` 引出的成员;实现段里的常量、变量和函数都视为私有
部分新一代解释器允许用 `DemoUnit.impl_const` / `DemoUnit.impl_var` 限定读取实现段数据,但这与经典 `unit` 可见性规则冲突,不作为跨环境默认生成能力。若项目已经依赖该行为,必须先按目标解释器实测并记录版本。
实现段函数的外部调用反例:
@@ -491,7 +489,7 @@ writeLn(Hello());
<!-- section-id: syntax-18-011 -->
- 把 `DemoUnit.var_name := value` 当成可用的限定赋值。
- 以为 `implementation` 里的常量和变量一定都不能从 `DemoUnit.Member` 读到
- 默认从外部读取只在 `implementation` 中声明的成员;跨版本安全代码应通过 `interface` 导出
- 以为 `findFunction("DemoUnit")` 暴露的成员范围和 `DemoUnit.Member` 完全相同。
- 把脚本内的 `namespace "..."` 当成和 `tsl.conf` 里的 `Namespace=...` 叠加,而不是覆盖。
- 把 `-LIBPATH` 放在脚本文件名前面。
@@ -36,7 +36,7 @@
<!-- section-id: syntax-22-004 -->
<!-- tags: 建 FMArray, 高性能数组, 字面量写法 -->
<!-- tags: 建 FMArray, 高性能数组, 高性能矩阵, fmarray, 字面量写法 -->
代码块身份:可直接照写示例
+13 -12
View File
@@ -41,6 +41,7 @@ STRUCTURAL_METADATA_RE = re.compile(
r"<!--\s*(?:section-id|quickstart-rule)\s*:.*?-->",
re.DOTALL | re.IGNORECASE,
)
HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
IDENTITY_PREFIX = "代码块身份:"
BLOCK_DESCRIPTION_PREFIX = "代码块说明:"
ALLOWED_IDENTITIES = {
@@ -156,7 +157,6 @@ CHINESE_QUERY_PARTICLES = ("的", "是", "吗", "呢", "吧")
ASCII_FILTER_STOP_TOKENS = {
"debug",
"please",
"program",
"tinysoft",
"tsl",
"tsf",
@@ -480,7 +480,7 @@ def load_sections(references_dir: Path = DEFAULT_REFERENCES_DIR) -> list[Section
raise ReferenceStructureError(f"重复 section ID{base_id}")
seen_ids.add(base_id)
tags = _section_tags(local_body)
searchable_body = STRUCTURAL_METADATA_RE.sub(" ", local_body)
searchable_body = HTML_COMMENT_RE.sub(" ", local_body)
searchable_text = normalize(
"\n".join((page.stem, page_title, *heading_path, *tags, searchable_body))
)
@@ -1122,9 +1122,9 @@ def _text_contains_exact_query(text: str, query: str) -> bool:
def _intent_score(section: Section, query: str) -> int:
aliases = PAGE_INTENT_ALIASES.get(section.page.name, ())
return PAGE_INTENT_SCORE * sum(
return PAGE_INTENT_SCORE if any(
_query_contains_phrase(query, alias) for alias in aliases
)
) else 0
def _has_chinese_context(section: Section, query: str) -> bool:
@@ -1161,7 +1161,9 @@ def _tag_matched_tokens(tags: tuple[str, ...], query_token_set: set[str]) -> int
def _code_text(body: str) -> str:
inline = INLINE_CODE_RE.findall(body)
fenced = FENCED_CODE_RE.findall(body)
return normalize("\n".join((*inline, *fenced)))
# 标识符信号只来自 ASCII 代码术语。中文散文会走标题、tag 和正文得分;
# 若把围栏里的“下标数组”等输出标签也当标识符,中文查询会被样例值劫持。
return normalize("\n".join(ASCII_TOKEN_RE.findall("\n".join((*inline, *fenced)))))
def _score_section(section: Section, query: str, mode: str) -> ScoreBreakdown:
@@ -1169,8 +1171,7 @@ def _score_section(section: Section, query: str, mode: str) -> ScoreBreakdown:
tokens = query_tokens(query)
heading_text = normalize("\n".join(section.heading_path))
page_title_text = normalize(section.page_title)
body_without_metadata = STRUCTURAL_METADATA_RE.sub(" ", section.local_body)
body_text = normalize(SECTION_TAG_RE.sub(" ", body_without_metadata))
body_text = normalize(HTML_COMMENT_RE.sub(" ", section.local_body))
tag_text = normalize("\n".join(section.tags))
term_text = _code_text(section.local_body)
expanded_only_tokens = _synonym_tokens(query) - _base_query_tokens(query)
@@ -1356,6 +1357,7 @@ def query_sections(
)
ranked.sort(
key=lambda match: (
match.weak,
-match.score,
*(-value for value in match.priority),
match.section.page.as_posix(),
@@ -1400,10 +1402,9 @@ def _safe_json_string(value: str) -> str:
def _plain_text_summary(body: str, limit: int = 180) -> str:
# 标签是检索元数据,不是事实正文;不能泄进候选摘要。
without_metadata = STRUCTURAL_METADATA_RE.sub(" ", body)
without_tags = SECTION_TAG_RE.sub(" ", without_metadata)
without_fences = FENCED_CODE_RE.sub(" ", without_tags)
# HTML 注释都是维护元数据,不是事实正文;不能泄进候选摘要。
without_comments = HTML_COMMENT_RE.sub(" ", body)
without_fences = FENCED_CODE_RE.sub(" ", without_comments)
without_links = re.sub(
r"!?\[([^\]]*)\]\([^)]+\)", lambda match: match.group(1), without_fences
)
@@ -1464,7 +1465,7 @@ def render_candidates(result: QueryResult) -> str:
def render_section(section: Section) -> str:
body = STRUCTURAL_METADATA_RE.sub("", section.body)
body = HTML_COMMENT_RE.sub("", section.body)
body = re.sub(r"\n{3,}", "\n\n", body).rstrip()
lines = [
"# TSL Syntax Section",
+49 -2
View File
@@ -298,8 +298,8 @@ class TslSyntaxReferenceTests(unittest.TestCase):
for query in ("数组下标", "请帮我解释数组的下标"):
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertEqual("11_matrix_and_collections.md", result.matches[0].section.page.name)
self.assertEqual("基础数组与键表", result.matches[0].section.heading_path[-1])
self.assertEqual("03_values_and_literals.md", result.matches[0].section.page.name)
self.assertEqual("核心规则", result.matches[0].section.heading_path[-1])
def test_documented_write_and_diagnose_queries_rank_expected_sections(self):
write_result = lookup.query_sections("命名参数", "write", limit=1)
@@ -349,6 +349,53 @@ class TslSyntaxReferenceTests(unittest.TestCase):
self.assertEqual(2, result.returncode)
self.assertIn("only weak candidates", result.stderr)
def test_strong_candidates_are_ranked_before_weak_diagnose_boosts(self):
result = lookup.query_sections("变量赋值", "diagnose", limit=1)
self.assertTrue(result.matches)
self.assertFalse(result.matches[0].weak)
self.assertNotEqual("syntax-04-008", result.matches[0].section.id)
def test_page_intent_aliases_score_once_per_page(self):
result = lookup.query_sections("高性能矩阵 fmarray", "explain", limit=3)
self.assertTrue(result.matches)
self.assertEqual("syntax-22-004", result.matches[0].section.id)
for match in result.matches:
self.assertNotIn("intent=160", match.reasons)
self.assertIn("intent=80", result.matches[0].reasons)
def test_program_is_a_searchable_language_keyword(self):
result = lookup.query_sections("PROGRAM", "explain", limit=1)
self.assertTrue(result.matches)
self.assertEqual("syntax-02-009", result.matches[0].section.id)
def test_maintenance_html_comments_do_not_leak_into_lookup_output(self):
result = run_lookup("--query", "PROGRAM", "--mode", "explain")
self.assertEqual(0, result.returncode)
self.assertNotIn("prettier-ignore", result.stdout)
def test_new_language_gaps_have_stable_retrieval_entries(self):
cases = {
"全局变量": "syntax-04-009",
"运行时常量": "syntax-04-010",
"静态计算": "syntax-06-016",
"只计算一次": "syntax-06-016",
"指定系统函数": "syntax-05-013",
"运算符优先级": "syntax-06-015",
"内存上限": "syntax-14-011",
"嵌套注释": "syntax-15-005",
"JOIN 第二张表当前行": "syntax-13-010",
"按行广播": "syntax-11-011",
}
for query, expected_id in cases.items():
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertTrue(result.matches, query)
self.assertEqual(expected_id, result.matches[0].section.id)
def test_batch_section_retrieval_is_atomic(self):
ids = [section.id for section in lookup.load_sections()[:2]]
success = run_lookup("--section", *ids)