# TSL 函数与调用 文档类型:语法主线 是否可直接用于生成代码:是 是否含可直接照写示例:是 是否含不可照写反例:是 遇到不确定时:先按本页候选页继续判断;[02_core_model.md](02_core_model.md)(优先)、[09_units_and_scope.md](09_units_and_scope.md)、[16_lexical_structure_and_compile_options.md](16_lexical_structure_and_compile_options.md)、[18_external_calls_and_threads.md](18_external_calls_and_threads.md)、[11_pitfalls.md](11_pitfalls.md);仍不命中时回到语法路由中心 [index.md](index.md);如果问题已经超出语法层,回到 TSL 总入口 [../index.md](../index.md) 这一篇只负责 `function` / `procedure` 的定义、调用、参数传递和值返回。跨 `unit` 声明边界、外部系统交互和业务函数库只在本页保留路由或最小边界。 ## 本篇职责 回答“如何正确声明 `function` 和 `procedure`、`.tsl` 脚本语句区如何调用后置函数声明、怎样使用参数修饰、普通函数默认参数与可变参数,以及哪些函数写法会直接编译失败”。 ## 智能体函数/调用判断流程 1. 先判断目标文件是 `.tsl` 还是 `.tsf`;文件形态不明确时回 [02_core_model.md](02_core_model.md)。 2. `.tsl` 中先写语句区,再把 `function` / `procedure` 声明放在后面;不要在声明区后面追加脚本语句。 3. `.tsf` 中把顶层 `function` / `procedure` 当成模块 / 函数扩展声明;不要写成顺序执行入口。 4. 用户只说“写一个函数”且没有指定 `procedure` 时,默认用 `function`。 5. 只有用户明确要求 `procedure` / 过程时,才用 `procedure`;即使任务没有返回值,也默认用 `function`。 6. 调用普通 TSL 函数时,命名参数只写 `name: value`;不要把 `name = value` 当成命名参数。 7. 参数是否写回调用方要看 `const` / `var` / `{$varByRef-}` / `in` / `out`,不要默认按其他语言习惯推断。 8. 默认参数先按普通函数规则处理;涉及 `unit interface` 或 `unit const` 默认值时,跳到 [09_units_and_scope.md](09_units_and_scope.md)。 9. 匿名函数和 TSL 函数值只按本页明确的 `call(f, ...)` / `##f(...)` 生成;`external`、原生函数指针包装、C 回调、线程和系统交互跳到 [18_external_calls_and_threads.md](18_external_calls_and_threads.md)。 10. 没有对应代码块时不要发明函数/调用写法;尤其不要把二进制函数、系统函数、TSL 函数值、原生函数指针和匿名函数都套成同一种调用语法。 ## 核心规则 - 最稳妥的函数骨架仍然是 `function Name(...); begin ... end;`。 - 用户提示词里的”函数”默认对应 `function`,不要自动改写成 `procedure`。 - `procedure Name(...); begin ... end;` 只在用户明确要求 `procedure` / 过程时生成;不要因为没有返回值就自动改用 `procedure`。 - 在 `.tsl` 文件模型层,脚本语句后可以接函数声明;语句区在前顺序执行,声明区在后提供函数/过程定义。见 [02_core_model.md](02_core_model.md)。 - 在 `.tsf` 文件模型层,顶层 `function` / `procedure` 是模块/函数扩展声明;部署到解释器 `funcext` 后可被脚本直接调用。 - 函数头后默认保留分号;不要为了简写主动省略。 - 函数体内部的普通语句照常用分号结尾;顶层函数/过程声明的 `end` 后必须加分号(写成 `end;`)。 - 函数体内的控制流块(`if`/`while`/`for` 等)的 `end` 不加分号;控制流分号规则见 [07_control_flow.md](07_control_flow.md)。 - 一个函数定义体里可以同时出现主函数和子函数。 - 函数支持参数类型注解和返回值类型注解。 - 不带类型注解时,多个参数用逗号分隔。 - 带类型注解时,多个参数用分号分隔。 - 没有明确类型名证据时,不要为了“看起来更完整”而发明参数类型或返回类型。 - `const` 与 `var` 形参修饰属于文档明确写法。 - 按文档运行时边界,未修饰参数可以写回调用方;这是运行时默认行为,不是语言规范保证。 - `{$varByRef-}` 会把未修饰参数切换成按值传递;`var` 形参仍保持引用语义。 - 在 `{$varByRef-}` 下,调用时可以用 `in` / `out` 前缀逐个参数覆盖默认传递方式。 - 如果不确定任务是否需要写回语义,优先显式用 `const` 形参,或先切到 `{$varByRef-}`;不要依赖未修饰参数的运行时默认行为。 - `return expr;` 会直接返回当前函数结果。 - `exit;` 会立即结束当前函数;在本页最小样例里,如果此前没有写入返回结果,调用方观察到的是默认值 `0`。 - 调用时支持命名参数,写法是 `name: value`。 - 命名参数也支持 `call(...)` 这类按函数名或函数指针转调的模型。 - 一旦某次调用里开始使用命名参数,后面的参数就不能再退回位置参数。 - 对二进制函数 / 系统函数直接使用命名参数,会报 `named parameter mode can't support here`;这类函数要先用 TSL 再封一层。 - 函数参数支持默认值。 - 普通函数的默认值规则不要直接等同到 `unit interface` 声明;跨 `unit` 的默认参数边界只照本页最小反例和 [09_units_and_scope.md](09_units_and_scope.md) 处理。 - 尾部 `...` 形式的可变参数属于文档明确写法。 - 在可变参数函数体里,`Params`、`ParamCount`、`RealParamCount` 都可用。 - 可变参数组可以通过 `...` 转发给另一个函数调用。 - 可变参数组也可以通过 `call(fc, ...)`、`##fc(...)`、`invoke(obj, name, 0, ...)` 转发;这些属于调用转发边界,只在有对应文档明确样例时生成。 - `a := function(...) begin ... end;` 这种匿名函数写法属于文档明确写法。 - 匿名函数可以直接作为参数传入另一个函数。 - `thisFunction(FuncName)` 可以把已知函数绑定成函数值。 - 匿名函数和 TSL 函数值的稳定调用方式仍是 `call(f, ...)` 或 `##f(...)`。 - `f(...)` 这种“函数变量直接调用”写法不作为可写事实;无论 `f` 是匿名函数、`findFunction(...)` 还是 `thisFunction(...)` 返回的函数指针,都不要默认写成直调。 - `::FuncName(...)` 可以指向全局/系统函数,用来绕过当前作用域里的同名局部函数。 - `external`、原生函数指针包装、`makeInstance` / C 回调和线程调用统一移到 [18_external_calls_and_threads.md](18_external_calls_and_threads.md)。 - 不要在 `.tsl` 的函数声明区之后继续追加脚本语句。 ## 可直接照写示例 使用这些示例时遵守: - 普通运行示例默认按 `.tsl` 脚本语句区书写;入口语句放前面,函数 / 过程 / 类型声明放在后置声明区。 - `.tsf` 函数 / 过程示例只按可复用顶层声明理解,不要在 `.tsf` 里追加顺序执行入口语句。 - `procedure` 示例只在用户明确要求 `procedure` / 过程时复制;普通“写函数”任务不要用。 - `external`、原生函数指针包装、C 回调、线程、系统交互和二进制函数边界不在本页硬推断;命中这些任务时跳到 [18_external_calls_and_threads.md](18_external_calls_and_threads.md) 或函数库文档。 ### 基础函数 / 过程骨架 `.tsl` 语句区调用后置函数声明: 代码块身份:可直接照写示例 ```tsl a := 1; test(); function test(); begin echo "test"; end; ``` 代码块身份:输出片段 ```text test ``` 最短函数骨架: 代码块身份:可直接照写示例 ```tsl function Add(a, b); begin return a + b; end; ``` 主函数加子函数: 代码块身份:可直接照写示例 ```tsl function MultiFunc(); begin return Twice(3); end; function Twice(x); begin return x * 2; end; ``` 显式要求 `procedure` 时的最短骨架: 代码块身份:可直接照写示例 ```tsl procedure LogDone(); begin end; ``` 显式要求 `procedure` 且需要写回参数时的最小运行样例: 代码块身份:可直接照写示例 ```tsl a := 1; Bump(a); writeLn(a); procedure Bump(var x); begin x := x + 1; end; ``` 代码块身份:输出片段 ```text 2 ``` ### 签名增强 带参数类型和返回值类型: 代码块身份:可直接照写示例 ```tsl function Demo(a: integer): integer; begin return a; end; ``` 带类型时的多参数分隔: 代码块身份:可直接照写示例 ```tsl function Demo(a: integer; b: integer); begin return a + b; end; ``` 参数修饰: 代码块身份:可直接照写示例 ```tsl a := 10; writeLn(ReadConst(a)); SetVar(a); writeLn(a); function ReadConst(const x); begin return x + 1; end; function SetVar(var x); begin x := x + 5; end; ``` 结果说明: - `ReadConst(a)` 输出 `11` - `SetVar(a)` 之后,`a` 输出 `15` - 直接给 `const` 形参赋值会编译失败 代码块身份:输出片段 ```text 11 15 ``` ### 参数传递方式 未修饰参数默认写回调用方: 代码块身份:可直接照写示例 ```tsl x := 1; TouchDefault(x); writeLn(x); function TouchDefault(a); begin a := 9; end; ``` 结果说明: - `TouchDefault(x)` 之后,`x` 输出 `9` 代码块身份:输出片段 ```text 9 ``` `{$varByRef-}` 与 `var` 形参: 代码块身份:可直接照写示例 ```tsl x := 1; TouchDefault(x); writeLn(x); y := 1; TouchValue(y); writeLn(y); z := 1; TouchForcedVar(z); writeLn(z); function TouchDefault(a); begin a := 9; end; {$varByRef-} function TouchValue(a); begin a := 8; end; function TouchForcedVar(var a); begin a := 7; end; {$varByRef+} ``` 结果说明: - 默认模式下,`TouchDefault(x)` 后 `x` 输出 `9` - `{$varByRef-}` 下,未修饰参数版本 `TouchValue(y)` 之后,`y` 仍输出 `1` - `{$varByRef-}` 下,`var` 形参版本 `TouchForcedVar(z)` 之后,`z` 输出 `7` 代码块身份:输出片段 ```text 9 1 7 ``` `in` / `out` 调用前缀: 代码块身份:可直接照写示例 ```tsl a := 0; b := 0; c := 0; Touch3(a, b, c); writeLn(a); writeLn(b); writeLn(c); Touch3(in a, out b, c); writeLn(a); writeLn(b); writeLn(c); {$varByRef-} function Touch3(a, b, c); begin a := 1; b := 2; c := 3; end; ``` 结果说明: - 在 `{$varByRef-}` 下,直接调用 `Touch3(a, b, c)` 后依次输出 `0`、`0`、`0` - 同样在 `{$varByRef-}` 下,`Touch3(in a, out b, c)` 后依次输出 `0`、`2`、`0` - 说明 `in` / `out` 可以在调用点逐个参数覆盖默认传递方式 代码块身份:输出片段 ```text 0 0 0 0 2 0 ``` ### `return` 与 `exit` 代码块身份:可直接照写示例 ```tsl writeLn(Demo(1)); writeLn(Demo(0)); function Demo(x); begin if x > 0 then exit; return 99; end; ``` 结果说明: - `Demo(1)` 输出 `0` - `Demo(0)` 输出 `99` - 说明 `exit;` 会立即结束当前函数体 - 在这个最小例子里,因为 `exit;` 之前没有写入返回结果,调用方观察到的是默认值 `0` 代码块身份:输出片段 ```text 0 99 ``` ### 调用增强 命名参数调用: 代码块身份:可直接照写示例 ```tsl writeLn(Pack(a: 1, b: 2)); function Pack(a, b); begin return a * 10 + b; end; ``` 代码块身份:输出片段 ```text 12 ``` 结果说明: - `Pack(a: 1, b: 2)` 返回 `12` - `Pack(b: 2, a: 1)` 返回 `12` - `Pack(1, b: 2)` 返回 `12` 跳过中间参数时,未命中的参数保持 `nil`: 代码块身份:可直接照写示例 ```tsl TestFunc(1, c: 3); function TestFunc(a, b, c); begin writeLn(ifNil(b)); return 0; end; ``` 结果说明: - 输出 `1` - 说明 `TestFunc(1, c: 3)` 这种调用里,中间参数 `b` 会保持 `nil` 代码块身份:输出片段 ```text 1 ``` 通过 `call(...)` 也支持命名参数: 代码块身份:可直接照写示例 ```tsl writeLn(call("TestFunc", a: 1, c: 2, b: 3)); function TestFunc(a, b, c); begin return a * 100 + b * 10 + c; end; ``` 结果说明: - 输出 `132` - 说明 `call(...)` 也支持按参数名传值 - 说明命名参数传入后不再按位置解释,而是按名字绑定到形参 代码块身份:输出片段 ```text 132 ``` ### 默认参数 默认值参数: 代码块身份:可直接照写示例 ```tsl function AddOne(a = 1); begin return a + 1; end; ``` 带类型时也支持默认值: 代码块身份:可直接照写示例 ```tsl function TypedAdd(a: integer = 1): integer; begin return a + 1; end; ``` 多个参数时,后面的参数可以带默认值: 代码块身份:可直接照写示例 ```tsl function Pack(a, b = 2); begin return a * 10 + b; end; ``` 代码块身份:可直接照写示例 ```tsl function Pack(a: integer; b: integer = 2): integer; begin return a * 10 + b; end; ``` 默认值也可以写成表达式: 代码块身份:可直接照写示例 ```tsl function ExprDefault(a = 1 + 2); begin return a; end; ``` 结果说明: - `AddOne()` 返回 `2` - `AddOne(5)` 返回 `6` - `TypedAdd()` 返回 `2` - `TypedAdd(5)` 返回 `6` - `Pack(1)` 返回 `12` - `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); ``` 边界说明: - `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 function SumAll(...); begin s := 0; for i, v in Params do s := s + v; return s; end; ``` 可变参数转发: 代码块身份:可直接照写示例 ```tsl function Forward(...); begin return SumAll(...); end; function SumAll(...); begin s := 0; for i, v in Params do s := s + v; return s; end; ``` `ParamCount` 与 `RealParamCount`: 代码块身份:可直接照写示例 ```tsl function CountArgs(a, b, ...); begin return ParamCount * 10 + RealParamCount; end; ``` 结果说明: - `SumAll(1, 2, 3, 4)` 返回 `10` - `Forward(1, 2, 3, 4)` 返回 `10` - `CountArgs(1, 2, 3, 4)` 返回 `44` - `CountArgs(1, 2)` 返回 `22` 通过 `call` 与 `##` 转发可变参数: 代码块身份:可直接照写示例 ```tsl writeLn(DoFunc("Sum3", 1, 2, 3)); writeLn(DoFunc2(thisFunction(Sum3), 1, 2, 3)); function Sum3(a, b, c); begin return a + b + c; end; function DoFunc(fc, ...); begin return call(fc, ...); end; function DoFunc2(fc, ...); begin return ##fc(...); end; ``` 结果说明: - `DoFunc("Sum3", 1, 2, 3)` 输出 `6` - `DoFunc2(thisFunction(Sum3), 1, 2, 3)` 输出 `6` 代码块身份:输出片段 ```text 6 6 ``` 通过 `invoke` 转发可变参数: 代码块身份:可直接照写示例 ```tsl writeLn(DoInvoke("Dispatch", "Int", 2, 20, 200)); writeLn(DoInvoke("Dispatch", "Other", 1, 2, 3)); type TestC = class public function FuncA(a, b, c); begin return a + b + c; end; function FuncB(a, b, c); begin return a * 100 + b * 10 + c; end; function Dispatch(kind, ...); begin if kind = "Int" then begin return FuncA(...); end else begin return FuncB(...); end end; end; function DoInvoke(fc_name, ...); begin obj := new TestC(); return invoke(obj, fc_name, 0, ...); end; ``` 结果说明: - `DoInvoke("Dispatch", "Int", 2, 20, 200)` 输出 `222` - `DoInvoke("Dispatch", "Other", 1, 2, 3)` 输出 `123` 代码块身份:输出片段 ```text 222 123 ``` ### 匿名函数与函数指针 匿名函数变量: 代码块身份:可直接照写示例 ```tsl a := function(x, y) begin return x + y; end; writeLn(call(a, 1, 2)); writeLn(##a(5, 6)); ``` 结果说明: - `call(a, 1, 2)` 输出 `3` - `##a(5, 6)` 输出 `11` 代码块身份:输出片段 ```text 3 11 ``` 匿名函数也可以直接作为参数传入: 代码块身份:可直接照写示例 ```tsl writeLn(Apply(function(x, y) begin return x * y; end)); function Apply(fun); begin return call(fun, 2, 3); end; ``` 结果说明: - 输出 `6` 代码块身份:输出片段 ```text 6 ``` 函数指针变量也可以通过 `findFunction(...)` 拿到: 代码块身份:可直接照写示例 ```tsl f := findFunction("Add"); writeLn(##f(1, 2)); function Add(a, b); begin return a + b; end; ``` 结果说明: - 输出 `3` 代码块身份:输出片段 ```text 3 ``` 也可以通过 `thisFunction(...)` 从已知函数名直接拿到: 代码块身份:可直接照写示例 ```tsl f := thisFunction(Add); writeLn(Call(f, 3, 4)); function Add(a, b); begin return a + b; end; ``` 结果说明: - 输出 `7` - 说明 `thisFunction(Add)` 可以得到稳定可调用的函数值 代码块身份:输出片段 ```text 7 ``` 直接 `f(...)` 不要当成可靠写法: 代码块身份:反例 / 不可照写 ```text // 匿名函数变量直接调用 a := function(x, y) begin return x + y; end; writeLn(a(7, 8)); ``` 结果说明: - 上面这种匿名函数变量直调会报 `function: compile error or not found` - `findFunction(...)` 和 `thisFunction(...)` 返回的函数值也不要写成 `f(...)` 直调 - 因此本页只把 `call(f, ...)` 和 `##f(...)` 写成稳定规则 ### `::` 指向全局函数 当当前作用域里有同名局部函数时,可以用 `::FuncName(...)` 指定去调全局/系统函数: 代码块身份:可直接照写示例 ```tsl r := Demo(); writeLn(r[0]); writeLn(r[1]); function Demo(); begin return array(strToInt("123"), ::strToInt("123")); end; function strToInt(s); begin return 888; end; ``` 结果说明: - 第一项 `strToInt("123")` 命中局部函数,输出 `888` - 第二项 `::strToInt("123")` 命中全局系统函数,输出 `123` - 说明 `::` 可以绕过当前作用域的同名局部函数,直接指向全局/系统函数 代码块身份:输出片段 ```text 888 123 ``` 如果 `::FuncName(...)` 指向的全局函数本身不存在,会在运行时报 `function: compile error or not found` ### 系统交互专题 `external`、原生函数指针包装、`makeInstance` / C 回调和线程调用,统一见 [18_external_calls_and_threads.md](18_external_calls_and_threads.md)。这一篇只保留“普通函数怎样定义和调用”的主线。 ## 默认生成模板 如果你只是要写一个能被智能体稳定续写的 `.tsl` 脚本,从语句区起步,需要函数时把声明区放在后面: 代码块身份:可直接照写示例 ```tsl Hello(); function Hello(); begin echo "hello"; end; ``` 如果你要写 `.tsf` 模块/函数扩展,默认从 `function` 骨架起步: 代码块身份:可直接照写示例 ```tsl function HelloValue(); begin return 1; end; ``` 只有用户明确要求 `procedure` / 过程时,才用下面这个 `.tsf` 骨架: 代码块身份:可直接照写示例 ```tsl procedure HelloProc(); begin end; ``` ## 禁止项 - 在 `.tsl` 的声明区后面继续写脚本语句。 - 用户只说“写一个函数”时,默认改成 `procedure`。 - 因为任务没有返回值,就自动改成 `procedure`。 - 为了简写主动省略函数头后的分号;默认保留分号。 - 以为 `procedure` 只是 `function` 的别名,不涉及参数传递语义。 - 带类型注解时仍然用逗号分隔参数。 - 没有类型名证据时,发明说明性参数类型或返回类型。 - 以为默认未修饰参数天然就是按值传递。 - 把 `a = 1` 这种比较表达式误当成命名参数调用。 - 以为默认值只能用于无类型参数。 - 在 `const` 形参上直接赋值。 - 把普通函数的默认值规则原样套到 `unit interface` 里的 `const` 默认参数上。 - 把匿名函数或 `findFunction(...)` 返回值默认写成 `f(...)` 直调。 - 把命名参数直接套到二进制函数或系统函数上。 - 在一次调用里先进入命名参数模式,后面又退回位置参数。 代码块身份:反例 / 不可照写 ```text a := 1; Add(1, 2); function Add(a, b); begin return a + b; end; echo "after function"; ``` 上面的问题不在 `Add` 本身,而在于 `.tsl` 的函数声明区后面又继续出现脚本语句。正确做法是把会执行的语句全部放在声明区之前。 代码块身份:输出片段 ```text invalid statement ``` 代码块身份:反例 / 不可照写 ```text function Demo(a: integer, b: integer); begin return a + b; end; ``` 上面这种写法会编译失败;参数一旦带类型,分隔符应改成分号。 代码块身份:反例 / 不可照写 ```text Pack(a = 1, b = 2); ``` 这类写法不要当成命名参数。它虽然可能编译通过,但文档结果不符合命名参数语义,不能当成可靠的命名参数语法。 代码块身份:反例 / 不可照写 ```text function Demo(a: integer, b: integer = 2): integer; begin return a + b; end; ``` 上面这种写法也不对;参数一旦带类型,分隔符仍然应保持分号。 代码块身份:反例 / 不可照写 ```text function TouchDefault(a); begin a := 9; end; ``` 不要把上面这种未修饰参数自动理解成“按值传递”。按文档边界,它会把调用方实参改掉;如果任务要求按值语义,先看 `{$varByRef-}` 和 `in` / `out` 的例子。 代码块身份:反例 / 不可照写 ```text function Bad(const x); begin x := 2; end; ``` 上面这种写法会编译失败;`const` 形参不能被重新赋值。 代码块身份:反例 / 不可照写 ```text unit UnitConst; interface 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)。 代码块身份:反例 / 不可照写 ```text a := function(x, y) begin return x + y; end; writeLn(a(7, 8)); ``` 不要把上面这种匿名函数变量的直调写法直接当成可靠规则。它会报 `function:a compile error or not found`;稳定写法仍然是 `call(a, 7, 8)` 或 `##a(7, 8)`。 代码块身份:反例 / 不可照写 ```text writeLn(intToStr(value: 200)); ``` 不要把上面这种写法直接套到二进制函数或系统函数上。它会报 `named parameter mode can't support here`;如果确实要命名传参,先用 TSL 函数再封一层。 代码块身份:反例 / 不可照写 ```text function Pack(a, b, c); begin return a * 100 + b * 10 + c; end; begin writeLn(Pack(a: 1, 2, c: 3)); end. ``` 不要在同一次调用里“先进入命名参数模式,再退回位置参数”。这类写法会直接编译失败,错误信息包含 `paramname: not found`。