playbook/docs/tsl/syntax/05_functions_and_calls.md

23 KiB
Raw Blame History

TSL 函数与调用

文档类型:语法主线 是否可直接用于生成代码:是 是否含可直接照写示例:是 是否含不可照写反例:是 遇到不确定时:先按本页候选页继续判断;02_core_model.md(优先)、09_units_and_scope.md16_lexical_structure_and_compile_options.md18_external_calls_and_threads.md11_pitfalls.md;仍不命中时回到语法路由中心 index.md;如果问题已经超出语法层,回到 TSL 总入口 ../index.md

这一篇只负责 function / procedure 的定义、调用、参数传递和值返回。跨 unit 声明边界、外部系统交互和业务函数库只在本页保留路由或最小边界。

本篇职责

回答“如何正确声明 functionprocedure.tsl 脚本语句区如何调用后置函数声明、怎样使用参数修饰、普通函数默认参数与可变参数,以及哪些函数写法会直接编译失败”。

智能体函数/调用判断流程

  1. 先判断目标文件是 .tsl 还是 .tsf;文件形态不明确时回 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 interfaceunit const 默认值时,跳到 09_units_and_scope.md
  9. 匿名函数和 TSL 函数值只按本页明确的 call(f, ...) / ##f(...) 生成;external、原生函数指针包装、C 回调、线程和系统交互跳到 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
  • .tsf 文件模型层,顶层 function / procedure 是模块/函数扩展声明;部署到解释器 funcext 后可被脚本直接调用。
  • 函数头后默认保留分号;不要为了简写主动省略。
  • 一个函数定义体里可以同时出现主函数和子函数。
  • 函数支持参数类型注解和返回值类型注解。
  • 不带类型注解时,多个参数用逗号分隔。
  • 带类型注解时,多个参数用分号分隔。
  • 没有明确类型名证据时,不要为了“看起来更完整”而发明参数类型或返回类型。
  • constvar 形参修饰属于文档明确写法。
  • 按文档运行时边界,未修饰参数可以写回调用方;这是运行时默认行为,不是语言规范保证。
  • {$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 处理。
  • 尾部 ... 形式的可变参数属于文档明确写法。
  • 在可变参数函数体里,ParamsParamCountRealParamCount 都可用。
  • 可变参数组可以通过 ... 转发给另一个函数调用。
  • 可变参数组也可以通过 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
  • 不要在 .tsl 的函数声明区之后继续追加脚本语句。

可直接照写示例

使用这些示例时遵守:

  • 普通运行示例默认按 .tsl 脚本语句区书写;入口语句放前面,函数 / 过程 / 类型声明放在后置声明区。
  • .tsf 函数 / 过程示例只按可复用顶层声明理解,不要在 .tsf 里追加顺序执行入口语句。
  • procedure 示例只在用户明确要求 procedure / 过程时复制;普通“写函数”任务不要用。
  • external、原生函数指针包装、C 回调、线程、系统交互和二进制函数边界不在本页硬推断;命中这些任务时跳到 18_external_calls_and_threads.md 或函数库文档。

基础函数 / 过程骨架

.tsl 语句区调用后置函数声明:

代码块身份:可直接照写示例

a := 1;
test();

function test();
begin
    echo "test";
end;

代码块身份:输出片段

test

最短函数骨架:

代码块身份:可直接照写示例

function Add(a, b);
begin
    return a + b;
end;

主函数加子函数:

代码块身份:可直接照写示例

function MultiFunc();
begin
    return Twice(3);
end;

function Twice(x);
begin
    return x * 2;
end;

显式要求 procedure 时的最短骨架:

代码块身份:可直接照写示例

procedure LogDone();
begin
end;

显式要求 procedure 且需要写回参数时的最小运行样例:

代码块身份:可直接照写示例

a := 1;
Bump(a);
writeLn(a);

procedure Bump(var x);
begin
    x := x + 1;
end;

代码块身份:输出片段

2

签名增强

带参数类型和返回值类型:

代码块身份:可直接照写示例

function Demo(a: integer): integer;
begin
    return a;
end;

带类型时的多参数分隔:

代码块身份:可直接照写示例

function Demo(a: integer; b: integer);
begin
    return a + b;
end;

参数修饰:

代码块身份:可直接照写示例

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 形参赋值会编译失败

代码块身份:输出片段

11
15

参数传递方式

未修饰参数默认写回调用方:

代码块身份:可直接照写示例

x := 1;
TouchDefault(x);
writeLn(x);

function TouchDefault(a);
begin
    a := 9;
end;

结果说明:

  • TouchDefault(x) 之后,x 输出 9

代码块身份:输出片段

9

{$varByRef-}var 形参:

代码块身份:可直接照写示例

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

代码块身份:输出片段

9
1
7

in / out 调用前缀:

代码块身份:可直接照写示例

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) 后依次输出 000
  • 同样在 {$varByRef-} 下,Touch3(in a, out b, c) 后依次输出 020
  • 说明 in / out 可以在调用点逐个参数覆盖默认传递方式

代码块身份:输出片段

0
0
0
0
2
0

returnexit

代码块身份:可直接照写示例

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

代码块身份:输出片段

0
99

调用增强

命名参数调用:

代码块身份:可直接照写示例

writeLn(Pack(a: 1, b: 2));

function Pack(a, b);
begin
    return a * 10 + b;
end;

代码块身份:输出片段

12

结果说明:

  • Pack(a: 1, b: 2) 返回 12
  • Pack(b: 2, a: 1) 返回 12
  • Pack(1, b: 2) 返回 12

跳过中间参数时,未命中的参数保持 nil

代码块身份:可直接照写示例

TestFunc(1, c: 3);

function TestFunc(a, b, c);
begin
    writeLn(ifNil(b));
    return 0;
end;

结果说明:

  • 输出 1
  • 说明 TestFunc(1, c: 3) 这种调用里,中间参数 b 会保持 nil

代码块身份:输出片段

1

通过 call(...) 也支持命名参数:

代码块身份:可直接照写示例

writeLn(call("TestFunc", a: 1, c: 2, b: 3));

function TestFunc(a, b, c);
begin
    return a * 100 + b * 10 + c;
end;

结果说明:

  • 输出 132
  • 说明 call(...) 也支持按参数名传值
  • 说明命名参数传入后不再按位置解释,而是按名字绑定到形参

代码块身份:输出片段

132

默认参数

默认值参数:

代码块身份:可直接照写示例

function AddOne(a = 1);
begin
    return a + 1;
end;

带类型时也支持默认值:

代码块身份:可直接照写示例

function TypedAdd(a: integer = 1): integer;
begin
    return a + 1;
end;

多个参数时,后面的参数可以带默认值:

代码块身份:可直接照写示例

function Pack(a, b = 2);
begin
    return a * 10 + b;
end;

代码块身份:可直接照写示例

function Pack(a: integer; b: integer = 2): integer;
begin
    return a * 10 + b;
end;

默认值也可以写成表达式:

代码块身份:可直接照写示例

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 声明边界也同样可靠。

代码块身份:反例 / 不可照写

unit UnitConst;
interface

const default_value = 888;
function F(a, b = 100, c = default_value);

边界说明:

  • F(1) 输出 101F(1, 2) 输出 3
  • 同一组文件下,UnitConst.default_value 可读到 888,而 F(1, 2, 3) 输出 6
  • 因此不要把“普通函数默认参数可用”直接泛化成“unit interface 里引用 unit const 的默认参数也同样可靠”
  • 这类跨 unit 的声明边界,统一回看 09_units_and_scope.md

可变参数 ...

尾部可变参数:

代码块身份:可直接照写示例

function SumAll(...);
begin
    s := 0;
    for i, v in Params do
        s := s + v;
    return s;
end;

可变参数转发:

代码块身份:可直接照写示例

function Forward(...);
begin
    return SumAll(...);
end;

function SumAll(...);
begin
    s := 0;
    for i, v in Params do
        s := s + v;
    return s;
end;

ParamCountRealParamCount

代码块身份:可直接照写示例

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## 转发可变参数:

代码块身份:可直接照写示例

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

代码块身份:输出片段

6
6

通过 invoke 转发可变参数:

代码块身份:可直接照写示例

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

代码块身份:输出片段

222
123

匿名函数与函数指针

匿名函数变量:

代码块身份:可直接照写示例

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

代码块身份:输出片段

3
11

匿名函数也可以直接作为参数传入:

代码块身份:可直接照写示例

writeLn(Apply(function(x, y)
begin
    return x * y;
end));

function Apply(fun);
begin
    return call(fun, 2, 3);
end;

结果说明:

  • 输出 6

代码块身份:输出片段

6

函数指针变量也可以通过 findFunction(...) 拿到:

代码块身份:可直接照写示例

f := findFunction("Add");
writeLn(##f(1, 2));

function Add(a, b);
begin
    return a + b;
end;

结果说明:

  • 输出 3

代码块身份:输出片段

3

也可以通过 thisFunction(...) 从已知函数名直接拿到:

代码块身份:可直接照写示例

f := thisFunction(Add);
writeLn(Call(f, 3, 4));

function Add(a, b);
begin
    return a + b;
end;

结果说明:

  • 输出 7
  • 说明 thisFunction(Add) 可以得到稳定可调用的函数值

代码块身份:输出片段

7

直接 f(...) 不要当成可靠写法:

代码块身份:反例 / 不可照写

// 匿名函数变量直接调用
a := function(x, y)
begin
    return x + y;
end;
writeLn(a(7, 8));

结果说明:

  • 上面这种匿名函数变量直调会报 function:<name> compile error or not found
  • findFunction(...)thisFunction(...) 返回的函数值也不要写成 f(...) 直调
  • 因此本页只把 call(f, ...)##f(...) 写成稳定规则

:: 指向全局函数

当当前作用域里有同名局部函数时,可以用 ::FuncName(...) 指定去调全局/系统函数:

代码块身份:可直接照写示例

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
  • 说明 :: 可以绕过当前作用域的同名局部函数,直接指向全局/系统函数

代码块身份:输出片段

888
123

如果 ::FuncName(...) 指向的全局函数本身不存在,会在运行时报 function:<name> compile error or not found

系统交互专题

external、原生函数指针包装、makeInstance / C 回调和线程调用,统一见 18_external_calls_and_threads.md。这一篇只保留“普通函数怎样定义和调用”的主线。

默认生成模板

如果你只是要写一个能被智能体稳定续写的 .tsl 脚本,从语句区起步,需要函数时把声明区放在后面:

代码块身份:可直接照写示例

Hello();

function Hello();
begin
    echo "hello";
end;

如果你要写 .tsf 模块/函数扩展,默认从 function 骨架起步:

代码块身份:可直接照写示例

function HelloValue();
begin
    return 1;
end;

只有用户明确要求 procedure / 过程时,才用下面这个 .tsf 骨架:

代码块身份:可直接照写示例

procedure HelloProc();
begin
end;

禁止项

  • .tsl 的声明区后面继续写脚本语句。
  • 用户只说“写一个函数”时,默认改成 procedure
  • 因为任务没有返回值,就自动改成 procedure
  • 为了简写主动省略函数头后的分号;默认保留分号。
  • 以为 procedure 只是 function 的别名,不涉及参数传递语义。
  • 带类型注解时仍然用逗号分隔参数。
  • 没有类型名证据时,发明说明性参数类型或返回类型。
  • 以为默认未修饰参数天然就是按值传递。
  • a = 1 这种比较表达式误当成命名参数调用。
  • 以为默认值只能用于无类型参数。
  • const 形参上直接赋值。
  • 把普通函数的默认值规则原样套到 unit interface 里的 const 默认参数上。
  • 把匿名函数或 findFunction(...) 返回值默认写成 f(...) 直调。
  • 把命名参数直接套到二进制函数或系统函数上。
  • 在一次调用里先进入命名参数模式,后面又退回位置参数。

代码块身份:反例 / 不可照写

a := 1;
Add(1, 2);

function Add(a, b);
begin
    return a + b;
end;

echo "after function";

上面的问题不在 Add 本身,而在于 .tsl 的函数声明区后面又继续出现脚本语句。正确做法是把会执行的语句全部放在声明区之前。

代码块身份:输出片段

invalid statement

代码块身份:反例 / 不可照写

function Demo(a: integer, b: integer);
begin
    return a + b;
end;

上面这种写法会编译失败;参数一旦带类型,分隔符应改成分号。

代码块身份:反例 / 不可照写

Pack(a = 1, b = 2);

这类写法不要当成命名参数。它虽然可能编译通过,但文档结果不符合命名参数语义,不能当成可靠的命名参数语法。

代码块身份:反例 / 不可照写

function Demo(a: integer, b: integer = 2): integer;
begin
    return a + b;
end;

上面这种写法也不对;参数一旦带类型,分隔符仍然应保持分号。

代码块身份:反例 / 不可照写

function TouchDefault(a);
begin
    a := 9;
end;

不要把上面这种未修饰参数自动理解成“按值传递”。按文档边界,它会把调用方实参改掉;如果任务要求按值语义,先看 {$varByRef-}in / out 的例子。

代码块身份:反例 / 不可照写

function Bad(const x);
begin
    x := 2;
end;

上面这种写法会编译失败;const 形参不能被重新赋值。

代码块身份:反例 / 不可照写

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

代码块身份:反例 / 不可照写

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)

代码块身份:反例 / 不可照写

writeLn(intToStr(value: 200));

不要把上面这种写法直接套到二进制函数或系统函数上。它会报 named parameter mode can't support here;如果确实要命名传参,先用 TSL 函数再封一层。

代码块身份:反例 / 不可照写

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