playbook/docs/tsl/syntax/19_namespace_libpath_and_un...

12 KiB

TSL 命名空间、Libpath 与 unit 运行时

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

这一篇收拢 unit 的运行时行为,以及 .tsf / namespace / libpath 的查找规则。unit 的基础写法、uses 位置和重名解析,统一放在 09_units_and_scope.md

本篇职责

回答下面这些“写法已经会了,但运行起来为什么这样”的问题:

  • unit 什么时候进入 initialization
  • finalization 在什么时候触发
  • unit 里的常量、变量和 findFunction("UnitName") 到底暴露哪些成员
  • tslfilename() 返回什么
  • namespace "..."tsl.conf-LIBPATH 怎样影响 .tsf 查找
  • syssettsllibpath() / sysgettsllibpath() 怎样在运行时改查找路径

智能体命名空间/Libpath 判断流程

  1. 先判断任务是单元命名空间访问、-LIBPATH 查找,还是配置文件查找。
  2. 路径和查找顺序只写平台中立规则,不写某个开发机或容器路径。
  3. -LIBPATH 只作为运行时部署/查找规则,不写进普通语法示例。
  4. Libpath={$P}...InheritParent=1 不作为本页文档能力。
  5. 没有对应代码块时不要发明命名空间/Libpath 写法。

核心规则

  • 完整 unit 形态可以包含 interfaceimplementationinitializationfinalization,并以 end. 结束。
  • initializationunit 第一次被实际使用时触发,不是只因为顶层写了 uses 就立刻执行。
  • finalization 会在脚本结束前触发。
  • 直接写 DemoUnit.Member 时,可以读到 interfaceimplementation 里的常量、变量。
  • findFunction("DemoUnit") 拿到的是 unit 对象入口;本页只把它稳定暴露 interface 成员写成文档事实。
  • DemoUnit.var_name := value 这种限定赋值不作为可写事实;如果要改 unit 状态,应导出函数或方法来改。
  • tslfilename() 的参数规格见 ../reference/catalog/system.md;本页只保留它返回正在执行的 .tsl 主脚本完整路径这一行为事实。
  • namespace "DemoNS"; 会选择 Hello@DemoNS.tsf 这类命名空间函数文件。
  • tsl.conf[system] Namespace=... 可以设置默认命名空间;脚本里的 namespace "..." 会覆盖配置值。
  • 当全局 Hello.tsfHello@StmtNS.tsf 同时存在时,启用 StmtNS 后会优先命中命名空间版本。
  • -LIBPATH 必须跟在脚本文件名后面;目录项必须以 /\ 结尾;多路径分隔符是分号 ;
  • 当多个 -LIBPATH 目录都存在同名 .tsf 时,解释器按路径从前到后查找,先命中的目录优先。
  • tsl.conf 里的绝对 Libpath= 可以生效;Libpath={$P}... 不作为本页文档能力。
  • syssettsllibpath() / sysgettsllibpath() 可以在运行时修改和读取查找路径,路径规则与 -LIBPATH 相同。
  • InheritParent=1 不作为本页文档能力。

示例与行为

unit 生命周期

代码块身份:配置片段 / 概念骨架

// DemoUnit.tsf
unit DemoUnit;

interface

const unit_value = 7;
var unit_counter;
function Ping();

implementation

var private_counter;
function Ping();
begin
    private_counter := private_counter + 1;
    unit_counter := unit_counter + 10;
    return private_counter;
end;

initialization
    writeLn("INIT");
    unit_counter := 1;
    private_counter := 100;

finalization
    writeLn("FINAL");
end.

// main.tsl

uses DemoUnit;

writeLn("BEFORE");
writeLn(DemoUnit.unit_value);
writeLn(DemoUnit.unit_counter);
writeLn(Ping());
writeLn(DemoUnit.unit_counter);

结果说明:

  • 输出顺序是 BEFOREINIT7110111FINAL
  • 这说明顶层写了 uses DemoUnit; 之后,initialization 不是立刻执行,而是在第一次真正读 DemoUnit 成员时触发。
  • finalization 在脚本主体输出结束后触发。

代码块身份:输出片段

BEFORE
INIT
7
1
101
11
FINAL

unit 成员的读取边界

直接限定读取:

代码块身份:配置片段 / 概念骨架

// DemoUnit.tsf
unit DemoUnit;

interface

const public_const = 1;
var public_var;
function PublicFunc();

implementation

const impl_const = 2;
var impl_var;
function PublicFunc();
begin
    return 10;
end;
function ImplFunc();
begin
    return 20;
end;

initialization
    public_var := 3;
    impl_var := 4;
end.

// main.tsl

uses DemoUnit;

writeLn(DemoUnit.public_const);
writeLn(DemoUnit.public_var);
writeLn(DemoUnit.impl_const);
writeLn(DemoUnit.impl_var);
writeLn(PublicFunc());

结果说明:

  • DemoUnit.public_const 输出 1
  • DemoUnit.public_var 输出 3
  • DemoUnit.impl_const 输出 2
  • DemoUnit.impl_var 输出 4
  • PublicFunc() 输出 10
  • 本页文档边界是:实现段函数仍私有,但实现段常量和变量可以通过 DemoUnit.Member 直接读取。

实现段函数的外部调用反例:

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

uses DemoUnit;
writeLn(ImplFunc());

结果说明:

  • 上面这种写法会报 function:ImplFunc compile error or not found
  • 因此不要把“实现段常量和变量可限定读取”泛化成“实现段函数可外部调用”。

findFunction("UnitName") 拿到的 unit 对象:

代码块身份:配置片段 / 概念骨架

// DemoUnit.tsf
unit DemoUnit;

interface

const public_const = 1;
var public_var;
function Ping();

implementation

const impl_const = 2;
var impl_var;
function Ping();
begin
    return 11;
end;

initialization
    public_var := 3;
    impl_var := 4;
end.

// main.tsl

uses DemoUnit;

u := findFunction("DemoUnit");
writeLn(u.public_const);
writeLn(u.public_var);
writeLn(u.Ping());

结果说明:

  • u.public_const 输出 1
  • u.public_var 输出 3
  • u.Ping() 输出 11
  • findFunction("DemoUnit") 拿到的对象入口只稳定暴露 interface 成员。

findFunction("UnitName") 对象入口访问实现段成员反例:

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

uses DemoUnit;
u := findFunction("DemoUnit");
writeLn(u.impl_const);

结果说明:

  • 上面这种写法会报对象属性或方法不存在。
  • 因此不要把 findFunction("DemoUnit") 的对象入口等同于 DemoUnit.Member 限定访问。

限定赋值失败:

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

uses DemoUnit;
DemoUnit.unit_counter := 13;

结果说明:

  • 上面这段会编译失败,报错核心是 left side can not be assign to

tslfilename()

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

writeLn(tslfilename());

结果说明:

  • 这一行会输出正在执行的 .tsl 主脚本完整路径。
  • 本页只把“主脚本路径”写成文档事实;不要把 .tsf 被调用场景的返回值提前写成事实。

namespace 与默认命名空间

把默认命名空间、脚本覆盖、以及“全局函数与命名空间函数同名”的三组结论压成同一套最小实验:

代码块身份:配置片段 / 概念骨架

// tsl.conf
[system]
Namespace=ConfNS

// 同一目录下准备三份函数文件
// Hello.tsf
function Hello();
begin
    return 10;
end;

// Hello@ConfNS.tsf
function Hello();
begin
    return 20;
end;

// Hello@StmtNS.tsf
function Hello();
begin
    return 30;
end;

// main_conf.tsl
writeLn(Hello());

// main_stmt.tsl
namespace "StmtNS";
writeLn(Hello());

结果说明:

  • main_conf.tsl 输出 20,说明 tsl.conf 里的 Namespace=ConfNS 会默认命中 Hello@ConfNS.tsf
  • main_stmt.tsl 输出 30,说明脚本里的 namespace "StmtNS"; 会覆盖 tsl.conf 里的 Namespace=ConfNS
  • 在同一目录保留 Hello.tsf 后,不写 namespace 且不设置 Namespace 时,Hello() 输出 10
  • 如果只保留 Hello@StmtNS.tsf 而没有全局 Hello.tsf,又没有启用命名空间,Hello() 会报找不到函数。

-LIBPATH

查找顺序:

代码块身份:配置片段 / 概念骨架 代码块说明:命令行运行形态和多文件结构骨架;这些命令不可写进 .tsl 源码。

// libA/Hello.tsf
function Hello();
begin
    return 101;
end;

// libB/Hello.tsf
function Hello();
begin
    return 202;
end;

// main.tsl
writeLn(Hello());

// 命令 A
tsl .\main.tsl -LIBPATH "D:\libA\;D:\libB\"

// 命令 B
tsl .\main.tsl -LIBPATH "D:\libB\;D:\libA\"

结果说明:

  • 命令 A 输出 101
  • 命令 B 输出 202
  • 解释器按 -LIBPATH 从前到后查找,先命中的目录优先。

位置与分隔符边界:

代码块身份:配置片段 / 概念骨架 代码块说明:命令行运行形态;这些命令不可写进 .tsl 源码。

tsl -LIBPATH "D:\libA\" .\main.tsl
tsl .\main.tsl -LIBPATH "D:\libA"
tsl .\main.tsl -LIBPATH "D:\libA\,D:\libB\"

结果说明:

  • -LIBPATH 放在脚本文件名前面时,解释器不会执行脚本,而是回到 usage 提示。
  • 路径项不带结尾 /\ 时,函数找不到。
  • 多路径使用分号 ; 分隔,不使用逗号分隔。

syssettsllibpath()sysgettsllibpath()

代码块身份:配置片段 / 概念骨架

writeLn(syssettsllibpath("C:/path/to/libA/;C:/path/to/libB/"));
writeLn(sysgettsllibpath());
writeLn(call("FnA"));
writeLn(call("FnB"));

结果说明:

  • syssettsllibpath(".../libA/;.../libB/") 之后,call("FnA")call("FnB") 都能找到对应函数。
  • sysgettsllibpath() 返回设置后的完整路径串。
  • 多路径使用分号 ; 分隔;路径项保留结尾 /\

tsl.confLibpath=

代码块身份:配置片段 / 概念骨架

[system]
Libpath=D:\path\to\funcext\

结果说明:

  • tsl.conf 设为绝对 Libpath= 时,脚本可以找到该目录下的 .tsf
  • Libpath={$P}funcext/ 不作为本页文档能力。
  • InheritParent=1 不作为本页文档能力。

绝对 Libpath= 正例:

代码块身份:配置片段 / 概念骨架

// tsl.conf
[system]
Libpath=D:/abs/path/to/funcext/

// funcext/Hello.tsf
function Hello();
begin
    return 123;
end;

// main.tsl
writeLn(Hello());

结果说明:

  • 上面这种绝对 Libpath= 配置下,main.tsl 输出 123

Libpath={$P} 负例:

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

// tsl.conf
[system]
Libpath={$P}funcext/

// funcext/Hello.tsf
function Hello();
begin
    return 456;
end;

// main.tsl
writeLn(Hello());

结果说明:

  • 上面这种 Libpath={$P}funcext/ 配置下,main.tsl 仍报 function:Hello compile error or not found
  • 因此不能把 {$P} 替换写法当成文档规则。

InheritParent=1 的最小负例:

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

// parent/tsl.conf
[system]
Namespace=ParentNS
InheritParent=1

// parent/sub/Hello@ParentNS.tsf
function Hello();
begin
    return 314;
end;

// parent/sub/main.tsl

writeLn(Hello());

结果说明:

  • 这组父目录配置 + 子目录脚本的最小样例会报 function:Hello compile error or not found
  • 因此 InheritParent=1 不作为可写事实

禁止项

  • DemoUnit.var_name := value 当成可用的限定赋值。
  • 以为 implementation 里的常量和变量一定都不能从 DemoUnit.Member 读到。
  • 以为 findFunction("DemoUnit") 暴露的成员范围和 DemoUnit.Member 完全相同。
  • 把脚本内的 namespace "..." 当成和 tsl.conf 里的 Namespace=... 叠加,而不是覆盖。
  • -LIBPATH 放在脚本文件名前面。
  • 用逗号分隔 -LIBPATHsyssettsllibpath() 的多路径。
  • 忘了给 -LIBPATHsyssettsllibpath() 的目录项补结尾 /\
  • Libpath={$P}...InheritParent=1 直接当成文档规则。