feat(playbook): add syntax book and codex skills tooling

This commit is contained in:
csh
2025-12-22 13:39:49 +08:00
parent 5b97ed5322
commit 283d311d4f
41 changed files with 247368 additions and 646 deletions
+158 -13
View File
@@ -2,12 +2,42 @@
本章节规定 TSL 代码的结构与格式约定。
相关文档:
- 命名规范:`docs/tsl/naming.md`
- 工具链与验证命令(模板):`docs/tsl/toolchain.md`
## 1. 文件与组织
### 1.1 单一职责
- 一个文件只做一件事;职责明确。
- 文件名使用 `PascalCase`,并与文件内唯一的顶层声明同名(语法要求)。扩展名按类型使用 `.tsl`/`.tsf`(两者都属于 TSL 源文件,风格规则一致)
- 避免循环依赖;公共能力下沉到可复用模块
- `.tsl` 建议作为“入口/编排层”:聚合参数/配置、串起流程;可复用逻辑下沉到 `.tsf``unit`/`class`/`function`)中
- 当一个顶层声明同时承担“协议适配 + 业务计算 + I/O/环境依赖 + 临时代码”时,优先拆分边界:核心纯逻辑 → 工具函数 → 边界适配(I/O)
### 1.2 文件名与顶层声明(硬约束)
- TSL 语法要求:每个文件只能有一个顶层声明,且文件基名必须与顶层声明同名。
- 推荐文件名使用 `PascalCase` 以提升检索与协作一致性;扩展名按类型使用 `.tsl`/`.tsf`(两者都属于 TSL 源文件,风格规则一致)。
- 详细约束与命名细则见 `docs/tsl/naming.md`
### 1.3 依赖与分层
- 避免循环依赖;依赖方向应从“入口/业务层”指向“通用/底层模块”。
- 公共能力(类型/常量/纯函数)下沉到可复用模块;不要在多个文件里复制粘贴同一段工具逻辑。
- 发现环依赖时的默认处理顺序:
1. 提取共享部分到更底层的 `*Common`/`*Shared` `unit`
2. 通过参数/回调注入反转依赖(让底层不再直接引用上层)。
3. 必要时引入更明确的边界文件(例如 adapter 层),把依赖集中在边界处。
### 1.4 推荐布局(读者视角)
- 同类代码按“对外 API → 核心实现 → 辅助工具 → 测试/示例”的顺序组织。
- 对外 API:尽量靠前,读者先看到“怎么用”;实现细节与 helper 放到后面(例如 `unit``interface``class``public`)。
- 对外声明尽量“收口”:
- `unit``interface` 只放对外 `const/type/function` 声明与必要注释;实现细节与 helper 放在后部。
- `class` 的对外 API 放在 `public`;内部状态与实现细节放在 `private`
- 测试/示例:优先独立文件/目录,避免夹在核心实现中间(减少无关 diff 干扰 review)。
## 2. 格式(Formatting
@@ -26,7 +56,7 @@
### 2.3 begin/end 与代码块
- 代码块使用统一的块结构(示例为伪代码,按 TSL 语法调整):
- 代码块使用统一的块结构(示例按常见 TSL 写法;若项目语法/约定有差异,以项目现有代码为准):
```tsl
if cond then
@@ -41,6 +71,12 @@ end
- 多语句分支使用 `begin/end` 包裹:在 `then/else` 后换行写 `begin``end` 单独成行。
- `else/elseif` 等分支关键字另起一行,与上一块的 `end` 对齐。
- 单语句分支可省略 `begin/end`(保持清晰优先;一旦分支变复杂就回退到块结构):
```tsl
if cond then DoSomething()
else DoOther()
```
### 2.4 运算符与分隔符
@@ -64,6 +100,15 @@ if !ok then return err
注释用于解释**为什么**以及必要的背景,而不是重复代码。
### 3.0 注释形式与语言
- 支持的注释形式:
- 行注释:`// ...`(默认优先使用)
- 块注释:`{ ... }`
- 块注释:`/* ... */`
- 注释语言:跟随文件,可中英混写;同一文件内尽量保持一致的表达风格。
- 注释中不要写入明文密钥/Token/密码等敏感信息;示例使用占位符(如 `<TOKEN>`)。
### 3.1 文件级注释
- 文件开头说明用途、主要职责、关键依赖/约束。
@@ -71,16 +116,31 @@ if !ok then return err
### 3.2 函数/接口注释
- “对外可见”的定义:
- `unit``interface` 区域中的声明(对外 API
- `class``public` 区域的方法/property(对外 API
- 顶层 `function`:该函数本身(对外入口)
- 对外可见的函数必须写注释,包含:
- 做什么(行为)
- 入参/返回值含义(必要时含单位、范围)
- 关键副作用与异常情况
- 注释使用完整句子,末尾带标点。
- 推荐模板(按需裁剪;语言可中英混写):
```tsl
// Summary: 一句话说明做什么(以及关键约束/边界)。
// Args:
// - foo: 含义(单位/范围/约束)。
// Returns: 返回值语义(以及错误/空值含义,如适用)。
// Side effects: 关键副作用(I/O/全局状态/缓存/日志等)。
// Errors: 失败条件与处理方式(返回/抛出/降级)。
```
### 3.3 行内注释
- 用于解释复杂逻辑、非直观边界条件、性能/安全考量。
- 避免“显而易见注释”:
- 尾随注释(写在代码行末)只用于非常短的补充;超过一行时改为写在语句上方,或重构代码提醒意图。
```tsl
count = count + 1 // bad: obvious
@@ -90,34 +150,119 @@ count = count + 1 // bad: obvious
- 统一格式:`TODO(name): ...` / `FIXME(name): ...`
- 写清原因和期望修复方向,而非“留个坑”。
- `name` 使用 Git 用户名;不强制附 issue/ticket(如有可追加在描述中)。
## 4. 代码实践(Best Practices
> 本节偏“实践建议”(should),用于提升可读性/可测试性;若目标项目有更严格的约束与检查命令,以项目落地的工具链为准(参考 `docs/tsl/toolchain.md`)。如需给自动化/AI 代理配置强约束,可参考 `.agents/tsl/code_quality.md` 与 `.agents/tsl/testing.md`。
### 4.1 变量与常量
- 默认使用不可变/只读(如语法支持 `const` 或等价机制)
- 默认使用不可变/只读:能用 `const` 就用 `const`;可变状态尽量压到最小作用域,并让“更新点”集中且明显
- 对外 API 优先只读:对外暴露用只读 property(只有 `read`,不写 `write`),内部用私有成员保存。
```tsl
type User = class
public
property UserId read user_id_; // readonly
private
user_id_;
end;
```
- 变量声明与第一次使用尽量靠近。
- 避免隐式类型转换与隐式全局
- 避免隐式类型转换:TSL 为动态类型,但运行时仍有类型与单位;外部输入(参数/配置/文件/接口)应在边界处显式解析与校验,再进入核心逻辑
- 避免隐式全局:函数尽量只依赖显式入参;若必须使用顶层全局/静态可变变量,必须在声明处写注释说明:它是什么、用于什么、以及(如不明显)为什么需要是全局/静态。
### 4.2 函数设计
- 函数参数建议显式写类型注解,提升可读性与工具检查能力
- 签名尽量自解释:对外 API 的参数/返回值建议显式写类型注解;并用注释写清契约(可复用 3.2 的模板)
- 无返回值函数显式标注返回类型为 `void`
```tsl
function Func(a: string; b: ClassName): void;
```
- 单一职责;函数过长说明拆分点已出现(建议 ≤ 40–60 行)
- 参数顺序:输入参数在前,输出/回调在后
- 尽量避免超过 5 个参数;必要时封装为对象(class/unit)。
- 参数默认可读写(引用语义);输入参数如果不应被修改,优先使用 `const` 修饰符让意图与约束更明确
- 单一职责:函数过长说明拆分点已出现(建议 ≤ 40–60 行);把“纯计算”与“I/O/环境依赖(文件/网络/数据库/全局状态)”分离,降低耦合、便于测试
- 参数组织与顺序:
- 输入参数在前;可选配置/选项(如 `*Options`/`*Config`)居中;输出/回调在后。
- 避免堆叠多个布尔开关参数;优先收敛到 `*Options`/`*Config`(按需在 `class``unit` 中定义)。
- 示例:避免多个布尔开关参数(调用点难以理解 `true/false` 的含义),改为 `*Options`/`*Config`
```tsl
// 注:参数类型名按项目实际替换(此处 bool/Any 仅为示例占位)。
// bad: 多个 bool 参数在调用点难读、易传错
function ExportReport(
path: string;
data: Any;
include_header: bool;
compress: bool;
dry_run: bool
): void;
// good: 将可选开关收敛到 Options(调用点更自解释、后续扩展更稳定)
type ExportOptions = class
public
property IncludeHeader read include_header_ write include_header_;
property Compress read compress_ write compress_;
property DryRun read dry_run_ write dry_run_;
private
include_header_;
compress_;
dry_run_;
end;
function ExportReport(path: string; data: Any; options: ExportOptions): void;
```
- 尽量避免超过 5 个参数;必要时封装为对象(`class`/`unit`)。
### 4.3 错误处理
- 错误必须显式处理:返回错误、抛出异常或记录并降级(按项目约定)
- 不要吞掉异常/错误;必须加注释说明原因
- 错误必须显式处理:返回失败/错误、抛出异常或记录并降级(best-effort)。禁止“看起来成功了但其实失败了”的隐式路径
- **不设默认策略**:按场景选择返回/抛出/降级,并在对外注释里写清契约(参考 3.2 模板的 `Errors`
- **返回失败/错误**:调用方有能力恢复/重试/改参数时(参数不合法、外部输入解析失败、依赖不可用等)。
- **抛出异常**:不应发生的内部错误/不变量被破坏,继续执行风险更大时。
- **记录并降级**:功能可选、失败不影响主流程时(例如缓存读取失败 → 当作 cache miss),必须在代码旁注释说明“为什么允许”。
- 不要吞掉异常/错误:`try/except` 之后如果继续执行,必须有明确替代行为(返回/重试/降级)以及理由;否则应将错误继续向上抛出或返回。
- 错误信息与日志(允许在库里打日志,但要克制):
- 错误/日志至少包含:**做什么失败** + **关键上下文(脱敏)**,便于定位;避免只有“failed”。
- 禁止把 Token/密码/个人数据等敏感信息写入日志、注释或错误信息(参考 `.agents/tsl/auth.md`)。
- 避免重复记录:同一个错误链路尽量只在**边界层**记录一次(库里记录后,上层通常不再重复打一遍同等级日志)。
- 示例:`try/except/end` + 降级(best-effort):
- 注:示例中的 `Any`/`nil`/`LogWarn`/`ReadCacheFromFile` 为占位,按项目实际类型与函数替换。
```tsl
// 读取可选缓存:失败允许降级为 cache miss(必须可观测,并说明原因)。
function ReadOptionalCache(path: string): Any;
begin
try
return ReadCacheFromFile(path)
except
// best-effort: cache 仅用于提速,失败不应影响主流程
LogWarn("ReadOptionalCache failed; fallback to miss. path=" + path)
return nil
end
end;
```
### 4.4 性能与可测试性
- 避免过早优化先写清晰正确的代码,再用数据驱动优化
- 复杂逻辑要可测试:拆成纯函数或可注入依赖的模块
- 避免过早优化先写清晰正确的代码,再用数据profile/trace/log/基准)定位瓶颈并做最小化改动(参考 `.agents/tsl/performance.md`
- 复杂逻辑要可测试:把“纯计算/解析/规则”与“I/O/环境依赖(文件/网络/DB/全局状态)”分离;I/O 层做薄封装,核心逻辑保持可单测(参考 `.agents/tsl/testing.md`
- 避免在热路径里做隐式昂贵操作:循环内重复 I/O、重复解析/格式化、无界缓存、隐式复制等;缓存如必须引入,明确生命周期与上限(大小/TTL/清理点)。
- 示例:薄 I/O + 厚纯逻辑(便于测试与复用):
- 注:示例中的 `Any`/`ReadAllText` 为占位,按项目实际类型与函数替换。
```tsl
// pure: 只做解析/校验,不做 I/O,便于单元测试
function ParseConfig(text: string): Any;
// I/O: 只负责读文件与兜底处理,把逻辑交给 ParseConfig
function LoadConfig(path: string): Any;
begin
text = ReadAllText(path)
return ParseConfig(text)
end;
```
+77 -27
View File
@@ -5,41 +5,50 @@
## 1. 选名原则
- **可读一致**:名字清晰可读,并随可见范围调整具体程度。
- 可见范围越大(越对外),名字越应具体、少省略。
- 本指南中“对外可见”指:`unit interface``class public`、顶层 `function`
- **标识符语言**:标识符(类型/函数/property/变量/参数等)统一使用英文;禁止中文与拼音(注释可中英混合,见 `docs/tsl/code_style.md`)。
- **少用生僻缩写**:能写全称就写全称。
- 允许使用团队已约定、大家都懂的常见缩写;若缩写不够通用,优先写全称或在评审/文档中先达成约定。
- **驼峰/帕斯卡中的缩写规则**:缩写(首字母缩写/词组缩写)在 `PascalCase`/`camelCase` 中**按一个单词处理**,写成“首字母大写其余小写”,不要写一串全大写。
- 示例:`UserId`(不是 `UserID`)、`UrlTable`(不是 `URLTable`)、
`StartRpcServer`(不是 `StartRPCServer`)、`HttpClient`(不是 `HTTPClient`)。
- **避免无意义词**:如 `data``info``tmp``handle` 等。
- 可以作为限定词的一部分(例如 `user_data`),但不要单独用作名字(例如仅叫 `data`)。
## 2. 命名风格总览
对于以下规则,“单词”指英文中不带空格的词。
- `snake_case`:全小写,下划线分隔单词,用于普通变量/参数等;私有类成员变量在此基础上末尾加下划线
- `PascalCase``UpperCamelCase`):每个单词首字母大写,无下划线,用于类型、顶层函数/方法、property,以及(少量)公有成员字段。
- `snake_case`:全小写,下划线分隔单词,用于普通变量/参数等;私有类成员变量使用 `snake_case_`(见 5.2
- `PascalCase``UpperCamelCase`):每个单词首字母大写,无下划线,用于类型、顶层函数/“动作型”方法、property,以及(少量)公有成员字段(访问器/设置器方法见 7 的例外约定)
- 自定义标识符只使用本指南约定的 `PascalCase`/`snake_case``lowerCamelCase` 仅用于沿用内置/标准库/第三方 API 的既有命名。
**大小写与关键字约定**
- TSL 语言大小写无关,但本指南仍要求按约定使用大小写以提升可读性;不要用仅大小写不同的名字区分不同实体。
- TSL 语言大小写无关,但本指南仍要求按约定使用大小写以提升可读性;不要用仅大小写不同的名字区分不同实体;同一标识符在仓库中应保持一致写法
- 所有语法关键字统一使用全小写书写,例如 `if``for``class``function``unit``return` 等。
- 调用内置/标准库方法时,推荐保持官方大小写形式(`aaBBCC`/lowerCamelCase),例如 `getSysParams("xxx")`
- 调用内置/标准库/第三方 API 时,推荐保持对方官方大小写形式(`aaBBCC`/lowerCamelCase),例如 `getSysParams("xxx")`;自定义 wrapper 仍按本指南使用 `PascalCase`
## 3. 类型命名(Type Names
TSL 的顶层声明只有三种:`class``unit``function`
因此文件基名必须与顶层声明同名(见“4. 文件命名与顶层声明”)。
- **类(class)与单元(unit**使用 `PascalCase`,不带下划线。
- **顶层函数(function**使用 `PascalCase`,详见函数命名章节
- 示例:`UserAccount``OrderUnit``LoadMarketData()`
- **类(class)与单元(unit**使用 `PascalCase`,不带下划线;名称应为名词/名词短语(通常单数),避免动词开头
- 不推荐 `*Unit` 作为 `unit` 的后缀(`unit` 本身已表达语义);需要表达用途时,可使用 `*Shared`/`*Common`/`*Enums` 等更具体后缀(按团队约定)
- **顶层函数(function**使用 `PascalCase`;名称优先动词/动词短语(例如 `Load`/`Parse`/`Build`),详见函数命名章节
- 示例:`UserAccount``OrderShared``LoadMarketData()`
## 4. 文件命名与顶层声明(File Names)
TSL 的语法要求:每个文件只能有一个顶层声明,且**文件基名必须与该顶层声明名字一致**。
- 顶层声明可能是 `class``unit``function`(见类型命名)。
- `.tsl` 脚本文件:顶层声明只能是 `function`因此文件基名 = 顶层函数名。
- `.tsf` 代码文件:顶层声明可为 `class`/`unit`/`function`,文件基名需与之同名
- `.tsf` 代码文件:用于库/模块等“顶层声明”的承载文件;顶层声明可为 `class`/`unit`/`function`,文件基名需与之同名。
- `.tsl` 脚本文件:用于入口/编排层;顶层声明只能是 `function`因此文件基名 = 顶层函数名;可复用逻辑应下沉到 `.tsf`(见 `docs/tsl/code_style.md`
- 注:`.tsf` 也是 TSL 源文件,命名/风格与 `.tsl` 遵循同一套规则。
- **硬规则**:重命名顶层声明时必须同步重命名文件基名,否则语法/加载规则无法识别;批量重命名可参考 `$bulk-refactor-workflow`
命名建议:
@@ -48,6 +57,7 @@ TSL 的语法要求:每个文件只能有一个顶层声明,且**文件基
- `LoadMarketData.tsl` 中定义 `function LoadMarketData(...)`.
- `UserAccount.tsf` 中定义 `type UserAccount = class ... end;`.
- `DocxEnumerations.tsf` 中定义 `unit DocxEnumerations; ... end.`
- `ParseConfig.tsf` 中定义 `function ParseConfig(...)`.
注:TSL 大小写无关,实际编译时按大小写比较不会出错,但仍应保持文件名与声明名的推荐写法一致以便检索与协作。
@@ -56,9 +66,10 @@ TSL 的语法要求:每个文件只能有一个顶层声明,且**文件基
### 5.1 普通变量与参数
- **局部变量、函数参数、非成员变量**使用 `snake_case`
- 若参数名与 TSL 关键字冲突导致编译失败,使用前导下划线的 `snake_case` 作为例外,例如 `_type``_unit`
- 若参数名与 TSL 关键字冲突导致编译失败,使用前导下划线的 `snake_case` 作为例外,例如 `_type``type` 是常见冲突关键字)
- 前导下划线 `_` **仅用于上述关键字冲突的参数场景**,不要用于其他局部变量、成员变量、函数/类型/单元名称或全局变量。
- 示例:`table_name``max_retry_count``user_id`
- 建议把单位写进名字(尤其时间/金额/比例):例如 `timeout_ms``spread_bp``ratio_pct`
- 示例:`table_name``max_retry_count``user_id`(短名例外见 5.5)。
### 5.2 类成员(Class Data Members
@@ -68,44 +79,69 @@ TSL 的语法要求:每个文件只能有一个顶层声明,且**文件基
- 对外暴露的成员优先使用 **property**
- property 名称使用 `PascalCase`(视为对外 API)。
- property 的 `read/write` 指向真实成员(通常为私有 `snake_case_`)。
- 布尔 property 使用 `Is`/`Has`/`Can`/`Should` 等前缀的 `PascalCase`(例如 `IsReady`),对应私有成员可用 `is_ready_` 等。
- 示例:
```tsl
type User = class
public
property UserId read user_id_ write user_id_;
property IsReady read is_ready_; // bool property example
private
user_id_;
is_ready_;
end;
```
### 5.3 全局/静态变量
- 不推荐使用顶层全局/静态可变变量;优先封装到 `unit`/`class` 中,通过函数或 property 访问。
- 若必须声明顶层全局/静态变量,使用 `g_snake_case` 前缀显式标识其全局性质,例如 `g_user_cache``g_market_state`
- 全局/静态常量仍按常量规则使用 `kPascalCase`
- 合理例外(仍需集中管理,避免到处读写):
- 进程级只读配置缓存(启动后不再变)
- 有上限/可清理的缓存(明确容量/TTL/清理点)。
- 指标/计数器(只增不减,或集中在少数写入点更新)。
- 若必须声明顶层全局/静态可变变量:
- 命名仍使用 `snake_case`(不使用 `g_` 前缀)。
- 必须在声明处写注释说明:它是什么、用于什么、以及(如不明显)为什么需要是全局/静态。
- 建议补充写入点与生命周期:谁会写、何时写、何时清理/重置;如涉及并发,写明并发假设/保护方式。
- 不要在注释/日志中写入任何敏感信息(参考 `.agents/tsl/auth.md`)。
- 示例(注释模板,按需裁剪):
```tsl
// <var_name>: <what it is>
// Used for: <what it is used for>
// Global because: <why it needs to be global (if unclear)>
```
- 全局/静态常量仍按常量规则使用 `kPascalCase`(建议同样写一句用途注释,便于检索与维护)。
### 5.4 布尔变量
- 使用 `is_ / has_ / can_ / should_` 等前缀表达语义
- 示例:`is_ready``has_error``can_retry`
- 布尔变量建议区分两类语义
- **状态/谓词(predicate)**:描述“是否满足某条件/是否处于某状态”,使用 `is_ / has_ / can_ / should_` 等前缀表达语义
- 示例:`is_ready``has_error``can_retry`
- **选项/开关(flag/option)**:描述“是否启用某行为/模式”,允许使用不带 `is_``snake_case` 短语(更贴近配置项语义)。
- 示例:`dry_run``include_header``enable_cache``use_cache`
- 尽量使用正向命名,避免双重否定:`is_valid` 优于 `is_not_valid``disable_cache` 这类命名需谨慎(容易在调用点读错)。
### 5.5 短名例外
- 在极小作用域内可用习惯短名:`i``j``n``t`
- 作用域一旦扩大,必须改为有含义的名字。
- 在极小作用域内可用习惯短名:仅限 `for/while` 的索引变量或约 5–10 行内的临时值
- 允许短名清单(建议严格执行):`i/j/k`(索引)、`n`(计数);其他一律使用有含义的名字。
- 作用域一旦扩大(跨多个分支/循环、跨函数、跨文件),必须改为有含义的名字。
### 5.6 集合与复数命名(Collections
- **数组/列表/可迭代集合**使用复数名词的 `snake_case``users``order_items`
- 若复数形式不直观或为不可数名词,使用后缀明确类型:`news_list``price_items`
- **映射/字典(key→value**使用 `snake_case` 并加后缀 `_map`必要时可用 `_by_<key>` 表达键语义:`user_map``price_by_symbol`
- **映射/字典(key→value**使用 `snake_case` 并加后缀 `_map`必要时可用 `_by_<key>` 表达键语义(仍需保留 `_map``user_map``price_by_symbol_map`
- **集合/去重集合**使用后缀 `_set``user_id_set``symbol_set`
## 6. 常量命名(Constant Names
- **编译期/全局固定常量**使用 `kPascalCase`,以 `k` 开头。
- **模块级/全局固定常量**(写死、与入参无关、加载后不变)使用 `kPascalCase`,以 `k` 开头。
- 示例:`kDaysInAWeek``kAndroid8_0_0`
- 常量名建议带单位(尤其时间/金额/比例):例如 `kTimeoutMs``kSpreadBp``kRatioPct`
- 对于**局部 const 但值来自参数/运行时**的变量:
- 可用普通变量名 `snake_case`
- 不要用 `k` 前缀误导读者认为其全局固定。
@@ -114,11 +150,22 @@ end;
TSL 没有内置 `enum`,推荐使用 `unit` + `const``interface` 区域模拟枚举集合。
- `unit` 名称使用 `PascalCase`,建议以 `Enumerations`/`Enums` 结尾表达用途。
- 枚举值使用 `const` 定义并放在 `interface`;命名优先沿用外部/业务域既有前缀与风格(属于例外场景)。
- `unit` 名称使用 `PascalCase`,建议以 `Enumerations` 结尾表达用途(例如 `DocxEnumerations`
- 枚举值使用 `const` 定义并放在 `interface`
- 项目自定义枚举值:默认使用 `kPascalCase`(属于模块级固定常量)。
- 外部/互操作枚举值:允许沿用对方既有前缀与命名(例外场景)。
示例:
```tsl
unit AlertEnumerations;
interface
const kAlertLevelAll = -1;
const kAlertLevelNone = 0;
end.
```
```tsl
unit DocxEnumerations;
interface
@@ -130,22 +177,25 @@ end.
## 7. 函数与方法命名(Function Names
- 所有**普通**函数/方法(包含 `public`/`private`)均使用 `PascalCase`
- 顶层函数与“动作型/业务型”方法使用 `PascalCase`
- 访问器/设置器方法(仅当 property 无法表达语义时才使用)允许使用 `snake_case` 的小写形式,以贴近“字段/状态”的语义(按团队约定的例外)。
- **特殊函数/运算符重载为语法固定名,必须使用全小写**:
- 构造/初始化函数:`create`
- 析构/释放函数:`destroy`
- 运算符重载:`operator+()` 等,按语法使用小写 `operator<op>()` 形式。
- 示例:`AddTableEntry()``DeleteUrl()``OpenFileOrDie()`
- **推荐使用 property 语法**对外暴露访问器:property 名 `PascalCase``read/write` 绑定成员变量(见类成员章节)。
- 不推荐新增显式 getter/setter;仅当 property 无法表达语义时,才使用 getter/setter,命名可与字段同形的 `snake_case`(如 `count()``set_count(x)`)。
- 不推荐新增显式 getter/setter;仅当 property 无法表达语义时,才使用 getter/setter
- getter:与字段同形的 `snake_case`(如 `count()``is_ready()`)。
- setter:使用 `set_` 前缀的 `snake_case`(如 `set_count(x)``set_ready(x)`)。
## 8. 宏与编译期开关(Macro Names
- 能不用宏就不用。
- 必须使用,命名为全大写加下划线,并带项目/业务前缀:
- `TSL_ROUND(x)``TSL_ENABLE_FOO`
- 若项目不支持宏/预处理,本节可忽略;若支持且必须使用,命名为全大写加下划线,并带项目/业务前缀:
- `<PROJECT>_ENABLE_FOO``<PROJECT>_USE_BAR``<PROJECT>_ROUND(x)`
## 9. 例外(Exceptions
- 当命名需要与外部既有 API/协议保持一致时,可沿用对方风格。
- 例如对接 C/C++ 库、历史接口、跨语言互操作代码等
- 当命名需要与外部既有 API/协议保持一致时,可沿用对方风格(例如对接 C/C++ 库、历史接口、跨语言互操作代码等)
- 不要为了“命名隔离”引入不必要的 wrapper/嵌套;优先保证语义清晰、可检索,并在必要处用注释说明“该命名来自外部约束/协议”
File diff suppressed because it is too large Load Diff
+830
View File
@@ -0,0 +1,830 @@
# 02 控制流与异常
本章汇总流程控制、错误控制与调试相关语句。
## 目录
- [02 控制流与异常](#02-控制流与异常)
- [目录](#目录)
- [流程控制语句](#流程控制语句)
- [内容](#内容)
- [条件语句](#条件语句)
- [内容](#内容-1)
- [IF](#if)
- [IF 表达式](#if-表达式)
- [CASE](#case)
- [循环语句](#循环语句)
- [内容](#内容-2)
- [WHILE](#while)
- [REPEAT](#repeat)
- [FOR](#for)
- [BREAK](#break)
- [CONTINUE](#continue)
- [GOTO](#goto)
- [错误控制,以及调试语句](#错误控制以及调试语句)
- [内容](#内容-3)
- [异常处理 Try Except/Finally](#异常处理-try-exceptfinally)
- [ExceptObject 异常对象](#exceptobject-异常对象)
- [RAISE](#raise)
- [DEBUGRETURN](#debugreturn)
- [DebugRunEnv 与 DebugRunEnvDo](#debugrunenv-与-debugrunenvdo)
- [MTIC,MTOC 计算运算时间](#mticmtoc-计算运算时间)
- [SetProfiler,GetProfilerInfo 优化信息](#setprofilergetprofilerinfo-优化信息)
- [调用信息与代码行号](#调用信息与代码行号)
- [函数的返回和退出](#函数的返回和退出)
## 流程控制语句
### 内容
- 条件语句
- 循环语句
- GOTO
- 错误控制,以及调试语句
- 函数的返回和退出
### 条件语句
#### 内容
- IF
- IF 表达式
- CASE
#### IF
IF 语句是由一个布尔表达式和两个供选择的操作序列组成。运行时根据布尔表达式求值结果,选取其中之一的操作序列执行。有两种形式的 IF 语句:
```text
If <布尔表达式> then <语句>;
```
```text
If <布尔表达式> then <语句1>
else <语句2>;
```
当布尔表达式的值为真,执行 then 后面的语句;当值为假时则有两种情况:要么什么也不做,要么执行 else 后面的语句。
注意:
else 前面没有分号,因为分号是两个语句之间的分隔符,而 else 并非语句。如果在该处添了分号,则远程服务器在编译的时候就会认为 if 语句到此结束,而把 else 当作另一句的开头,这样就会输出出错信息。
语句可以是一条语句或是一组语句,如果是一组语句时,这组语句必须使用 Begin … End 标识符来限定,写成复合语句。在用 if 语句连续嵌套时,如果你插入适量的复合语句,有利于程序的阅读和理解。
例 2:求 y=f(x),当 x>0 时,y=1,当 x=0 时,y=0,当 x<0 时,y=-1。
```text
Function IfExample();
Begin
if x>0 then y:=1
else if x=0 then y:=0
else y:=-1;
return y;
End;
```
例 3:当 x>0 时候,计算 x*x,并且输出 x*x,否则输出 0。
```text
FunctionIfExample2(x);
begin
if x>=0 then
begin
x1:=x*x;
return x1;
end
else
return 0;
end;
```
注意:当 if 语句嵌套时,TSL 约定 else 总是和最近的一个 if 配对。
#### IF 表达式
if 表达式是一种条件表达式,它根据条件的真假来返回不同的值。它与 if 语句不同:
if 语句:是一种控制流语句,用于决定是否执行某段代码块,本身不返回值。
if 表达式:会计算一个结果,这个结果可以赋值给变量、作为函数参数或在其他表达式中使用。功能类似三元运算符,但 if 表达式更通用,可读性更高
其基本形式通常如下:
```text
if 条件 then 值1 else 值2
```
例如 if a>1 then 2 else 1,如果 a 大于 1,整个表达式的结果就是 2,否则是 1。
if 表达式必须存在 else 部分,主要是为了确保表达式始终有确定的返回值。如果没有 else,当条件为假时,表达式的返回值将是不确定的。
注:仅 2025-08-27 以后的语言版本支持此功能
示例:
```text
ret:=if x>0 then x*x else 0;
return ret;
```
当 x>0,返回 x\*xx<=0 时,返回 0。
//多个分支
```text
ret:=if x>0 then x*x else if x<0 then -(x*x) else 0;
return ret;
```
当 x>0,返回 x*xx<0 时,返回-x*xx=0,返回 0。
#### CASE
多分支条件语句,Case of
语法一:普通语法。
CASE <Expression> OF
<情况标号表 1>: 语句 1;
<情况标号表 2>: 语句 2;
...
<情况标号表 N>: 语句 N;
[Else 例外语句;]
End;
情况标号表的语法为:
CASE 区间 1[,CASE 区间 2..CASE 区间 N]
CASE 区间的语法为:
区间开始值[TO 区间结束值]
如果没有 TO 语句,则结束值和开始值相同。
例:
```text
Function CaseExample(Age);
Begin
Case Age Of
0: Writeln("婴儿");
1 ,2: Writeln("婴幼儿");
3 TO 6: Writeln("幼儿");
7 TO 14: Writeln("少年");
15 TO 17: Writeln("青少年");
Else
Writeln("成年");
End;
End;
```
语法二:支持 Case 表达式,在该种情况下,分支语句不支持语句段,只能是单语句表达式。
B:= CASE <Expression> OF
<情况标号表 1>: 表达式 1;
<情况标号表 2>: 表达式 2;
…(其它的与普通用法一致)
范例:
范例一:
```text
a:=3;
b:=case a of
1,2:"1/2";
3,4:"3/4";
else
"OTHER";
end;
return b;
```
//结果:3/4
范例二:
```text
a:=3;
b:=case a of
1,2:echo "1/2";
3,4:echo "3/4";
else
"OTHER";
end;
return b;
```
//结果:0。打印窗口:3/4
范例三:表达式的用法
```text
b:=@case a of
1,2:"1/2";
3,4:"3/4";
else
"OTHER";
end;
a:=2;
return eval(b);
```
//结果:1/2
### 循环语句
当需要重复执行一条或是一组语句时,可以使用循环控制语句。TSL 中的循环控制语句有 While 语句和 For 语句。
#### 内容
- WHILE
- REPEAT
- FOR
- BREAK
- CONTINUE
#### WHILE
while 语句用于"当满足某一条件时重复执行语句"的情况。while 语句的语法格式:
while 布尔表达式 do 语句;
循环结束条件在进入循环体之前测试,若最初的测试值为 false,则根本不进入循环体。为了能使 while 重复能终止,循环体中一定要有影响布尔表达式的操作,否则该循就是一个死循环。
说明:
语句可以是一条语句或是一组语句,如果是一组语句时,这组语句必须使用 Begin … End 标识符来限定,写成复合语句。
例 4:计算从 0 到某个数之间的和。
```text
Function sums(limit);
begin
sum:=0;
num:=0;
while num<=limit do
begin
sum:=sum+num;
num++;
end;
return sum;
end;
```
#### REPEAT
repeat 语句用于”重复执行语句直到满足某一条件”的情况。repeat 语句的语法格式:
```text
repeat
语句段;
until 布尔表达式;
```
说明:
repeat 与 while 不同之处有几点:
1,repeat 先做后判断是否结束,while 先判断后做,也就是说 repeat 至少会做一次;
2,repeat 的判断条件是结束条件,而 while 的判定条件是开始做的条件;
3,repeat 和 util 之间可以有语句段,不需要 begin end 来限定,而 while 由于没有结束的特殊标识符,因此当使用语句段的时候必须用 Begin end 来约束。
例 5:求第一个阶乘超过指定值的值
```text
Function MinMultiValue(limit);
begin
multi:=1;
value:=1;
repeat
multi:=multi*value;
value++;
until multi>limit;
return value;
end;
```
#### FOR
for 语句用来描述已知重复次数的循环结构。for 语句有三种形式:
(1) for 控制变量:=初值 to 终值 [step 步长] do 语句;
(2) for 控制变量:=初值 downto 终值 [step 步长] do 语句;
(3) for 控制变量 1,控制变量 2 IN 数组 Do 语句;
第一种形式的 for 语句是递增循环。
首先将初值赋给控制变量,接着判断控制变量的值是否小于或等于终值,若是,则执行循环体,在执行了循环体之后,自动将控制变量的值该为它的后继值,并重新判断是否小于或等于终值。当控制变量的值大于终值时,退出 for 循环,执行 for 语句之后的语句。
可通过 step N 方式指定递增步长,可省,默认为 1。
第二种形式的 for 语句是递减循环。
首先将初值赋给控制变量,接着判断控制变量的值是否大于或等于终值,若是,则执行循环体,在执行了循环体之后,自动将控制变量的值该为它的前趋值,并重新判断是否大于或等于终值。当控制变量的值小于终值时,退出 for 循环,执行 for 语句之后的语句。
可通过 step N 方式指定递减步长,可省,默认为 1。
注意:for 语句中,当初值、终值、步长确定后,重复的次数就确定不变了,并且控制变量在重复语句内不能施加任何赋值操作。
例如:计算 1+2+3+……+99+100 的值
```text
Function PlusFor();
begin
sum:=0;
for i:=1 to 100 do //缺省步长,默认步长为1
sum:=sum+i;
return sum;
end;
```
例如:计算 1+3+5+……+99 的值
```text
Function PlusFor2();
begin
sum:=0;
for i:=1 to 100 step 2 do
sum:=sum+i;
return sum;
end;
```
第三种形式的 for 语句是直接对数组进行遍历
对数组中的每一行(第一维)进行遍历,当前行的下标存放在第一个控制变量中,该行对应的值存放在第二个控制变量中。从第一行开始,将行标与当前行的值分别赋值给控制变量 1 与控制变量 2 后,执行循环体,在执行了循环体之后,自动将 2 个控制变量的值赋值为下一行的下标及该行值,当遍历完最后一行之后,退出 for 循环,执行 for 语句之后的语句。
For … IN 遍历的用法说明
语法:For i,v IN TArray DO 语句;说明:对数据的遍历。
其中,i:控制变量 1,获取当前循环中数组第一维的下标值 v:控制变量 2,对应当前循环中第一维度的值
TArray:需要被遍历的数组。
注 1:二维及多维数组可当作一维处理,此时的控制变量 2 的值则可能是一个数组。
注 2:在此过程中,不可更改一维数组的值,也不可对该数组中的任何元素进行赋值操作,对在循环过程中不可对循环数组 TArray 进行变更操作。
适应场景:对于非数字下标的数组,处理比较方便,且效率高
范例一:一维数组的应用
```text
data:=array('a':1,'b':5,'c':3,'d':-2);
s:=0;
for i,v in data do
s+=v;
return s;
//返回实数7
```
范例二:二维数组的应用
```text
data:=rand(array('a','b','c'),array('AA','BB','CC','DD'));
s:=0;
t:=1;
for i,v in data do //data是二维数组,所以第一维中,v的值是一个一维数组,即当前行。
for j,v1 in v do
begin
s+=v1;
t*=v1;
end
return array(s,t);
```
返回:array(12,1)
#### BREAK
在执行 WHILE 和 FOR 以及 REPEAT UNTIL 循环语句时,可以用 BREAK 语句随时从当前循环的语句段中跳出来,并继续执行循环语句后面的语句。
注意:Break 语句只是从当前的语句循环中跳出来,如果要从多个嵌套的循环语句中跳出,则需要通过多个对应的 Break 语句来完成。
例 7:我们用 While 语句和 Break 语句重新来例 5 中的 1+2+3+……+99+100 值
```text
Function PlusWhile();
begin
sum:=0;
i:=0;
while True do
begin
i++;
if i>100 then
break;
sum:=sum+i;
end;
return sum; //BREAK后执行的第一行语句。
end;
```
#### CONTINUE
CONTINUE 语句和 BREAK 语句一样,都可以改变 WHILE 循环语句和 FOR 循环语句以及 REPEAT UNTIL 的执行顺序。
BREAK 是强制地从一个循环语句中跳出来,提前结束循环,而 CONTINUE 语句则强制地结束当前循环开始进入下一次循环。
如:
```text
While true do
Begin
i++;
if i=100 then continue;//跳过100
if i>=1000 then break; //到1000结束
End;
```
### GOTO
几乎所有的分支流程控制语句都指令跳转有关,只是绝大多数情况下是有条件跳转,GOTO 是无条件跳转语句,其规则是使用 label 定义标号,使用 goto 可以跳转到指定的标号。
一个 GOTO 的案例:
```text
for i := 0 to length(data) -1 do
begin
for j := 0 to length(data[i])-1 do
begin
if data[i][j] = target then
begin
goto finded;
end;
end;
end;
label finded;
//在一个二维数组中查找只要查找到则结束
```
GOTO 有一个特性,就是只能从内层往外层跳转(且不能跨越函数)
### 错误控制,以及调试语句
#### 内容
- 异常处理 Try Except/Finally
- ExceptObject 异常对象
- RAISE
- DEBUGRETURN
- DebugRunEnv 与 DebugRunEnvDo
- MTIC,MTOC 计算运算时间
- SetProfiler,GetProfilerInfo 优化信息
- 调用信息与代码行号
#### 异常处理 Try Except/Finally
某些函数在执行的过程中可能会自动抛出异常,或者被手动 Raise 抛出异常,这个时候如果没有异常处理运行就会终止。
使用异常处理则可以保护程序继续执行,并可以对异常进行相应的处理。异常的信息可以由 ExceptObject 对象获得,异常处理使用如下模式:
```text
Try
被保护的程序执行段
Except
异常处理程序段
End;
```
例如:
```text
Try
I:=StrToInt(S); //当S不能转换为整数的时候会产生异常。
Except
I:=0; //当发生异常的时候设置I为0;
Writeln(ExceptObject.ErrInfo);
End;
```
对于某个程序段可能出现中途返回或者退出,或者中途被异常中断,而某些代码必需要在其后执行的,则采用如下模式:
```text
Try
被保护的程序执行段
Finally
保证执行的处理程序段,即便Try Finally之间的语句有返回或者异常产生。
End;
```
注:try...Except...end 可以使程序在报错时继续向下运行,即主程序不终止。
try...Finally...end 则是该报错时就会报错,即发生错误时程序会报错且终止,只是在中断前会执行完 Finally 中的命令行。
#### ExceptObject 异常对象
在 Except 块中,可以用 ExceptObject 获得当前的异常信息。
ExceptObject 是一个异常对象,包括以下几个成员:
ExceptObject. ErrInfo 获得错误的信息串
ExceptObject. ErrLine 错误的行号
ExceptObject. ErrNo 错误号
例如:
```text
a:=100;
try
a:=1+'a';
except
echo ExceptObject.ErrInfo;
end;
return a;
```
打印结果:
function:NoName501:line 11:instruction:+: Addition instruction error,operand type error
#### RAISE
主动抛出运行时异常,会引发程序出错并终止运行,RAISE 后跟随一个字符串,该字符串为出错返回的错误信息。
如:
```text
A:=-1;
If a<0 then raise “a不能小于0”;
```
运行时报错如:
#### DEBUGRETURN
调试返回,后面跟返回值,可在任何地方直接将结果返回,而不是象 RETURN 一样返回到上一级别,这有助于用户调试使用。
如下面示例,返回为 3 而不是 4
```text
A:=abcd(3);
Return A+1;
Function abcd(bb);
Begin
debugreturn
bb;
End;
```
#### DebugRunEnv 与 DebugRunEnvDo
DebugRunEnv(0)与 DebugRunEnv(1)
DebugRunEnv(0)可以将所有的变量内容递交到客户端的调试窗口
DebugRunEnv(1)可以将变量以及系统参数的内容递交到客户端的调试窗口
DebugRunEnvDo FunctionXX(….)
运行完指定的函数以后返回该函数最后的变量结果
#### MTIC,MTOC 计算运算时间
可以通过 MTIC 与 MTOC 记录一段程序运行的时间。
一般使用:以上代码可以计算所耗费的秒数
```text
MTIC
;
A:=0;
For i:=0 to 99999 do
&#61607; A++;
Return
MTOC
;
```
扩展使用:同时统计多段程序的运行时间
默认 MTOC 和上次 MTIC 匹配,但是也可以指定某个 MTIC 的返回来计算时间
```text
T1:=
MTIC
;
For i:=0 to 9999 do
A++;
TE1:=
MTOC(T1)
;
MTIC;
For j:=0 to 9999 do
A++;
TE2:=
MTOC(T1)
;
Return array(TE1,TE2,
MTOC
);
```
返回结果中:TE1 为第一段循环运行的时间,TE2 为两段循环运行的时间,第三个值为第二段循环运行的时间。
#### SetProfiler,GetProfilerInfo 优化信息
在程序中可通过指定 SetProfiler 指定运行时计算用户函数,系统函数,以及运算指令的耗费时间,最后通过 GetProfilerInfo()获得这些信息,如果不用 GetProfilerInfo,返回时会自动新建 Profiler 窗口来显示这些信息,客户端可以在运行时指定优化信息系统参数。
函数具体用法及优化信息结构可参考:SetProfiler、GetProfilerInfo
#### 调用信息与代码行号
通过关键字\_\_stack_frame 获得调用的堆栈的函数名以及行号
通过关键字**line**获得当前所在的行号
### 函数的返回和退出
参见函数返回以及退出
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,673 @@
# 07 运行时与性能工具
本章收录网格计算与全局缓存等运行时与性能相关机制。
## 目录
- [#网格计算操作符](#网格计算操作符)
- [内容](#内容)
- [#网格计算操作符简介](#网格计算操作符简介)
- [网格计算案例](#网格计算案例)
- [网格计算代入系统参数的案例](#网格计算代入系统参数的案例)
- [网格计算设置任务超时时间](#网格计算设置任务超时时间)
- [TSL 全局缓存的应用说明](#tsl全局缓存的应用说明)
- [内容](#内容-1)
- [全局缓存管理](#全局缓存管理)
- [内容](#内容-2)
- [全局缓存管理的函数](#全局缓存管理的函数)
- [全局缓存的使用](#全局缓存的使用)
- [全局缓存的过期与回收策略](#全局缓存的过期与回收策略)
- [全局缓存管理的初始化和监控](#全局缓存管理的初始化和监控)
- [未升级的系统对新代码的使用](#未升级的系统对新代码的使用)
- [初始化 TSL 和监控管理的 TSL](#初始化tsl和监控管理的tsl)
- [内容](#内容-3)
- [功能设计](#功能设计)
## #网格计算操作符
### 内容
- #网格计算操作符简介
- 网格计算案例
- 网格计算代入系统参数的案例
- 网格计算设置任务超时时间
### #网格计算操作符简介
什么是网格计算?
随着计算技术的发展,计算机已经得到了大规模普及,计算资源随处可见。另一方面,随着 CPU 制程技术提高的门槛越来越高,制造工艺成为了单一 CPU 内核计算性能提升的瓶颈。CPU 的主流发展从主频的提高逐步向多核迈进,而在应用层面上,以往单服务器计算逐渐被集群式计算所代替。在处理数据规模越来越大,计算模型越来越复杂的趋势下,传统单任务串行计算越来越成为了技术瓶颈,对于多 CPU 多内核以及多服务器群而言,成为了计算资源的巨大浪费,这个时候,对并行计算的研究成为了未来发展的主流,而网格计算则成为了并行计算研究中的热点。
并行计算在超算中心的大型主机应用中被广泛应用,依靠硬件技术,成千上万台主机被联接成为一个大型主机,这样使得在其上的开发对于用户而言,后台成千上万的主机只是一台拥有成千上万个计算核心的计算,用户不用关注数据的同步,用户也不用关注计算将派发到哪里。但是超算中心的建设和使用成本都非常巨大,而且还受到超算中心的数据管理麻烦的制约。
普通 PC 服务器的 CPU 核心数都非常有限,单机并行计算无法充分利用起计算机资源,这个时候,网格计算就孕育而生了。
网格使用格式:
R[i]:=#函数名(参数…) with array(系统参数列表…) timeout N;
其中,with 语句可指定网格运算子程中的系统参数,此输入可省
timeout N 为指定网格运算子程序运行的超时时间,若运行超过设定时间,则程序报错,可省,默认为一直等待,单位为:毫秒。
### 网格计算案例
如果用户拥有网格计算的权限,就可以进行网格计算了。
在 TSL 中使用网格计算非常简单,仅仅只需要在网格调用的函数前加上#即可
例如:
```text
A:=Array();
B:=Array('SZ000001','SZ000002','SH600000');
for i:=0 to length(B)-1 do
begin
A[I]:=
###
CalcStock(B[I]);
end;
//--对结果进行访问,用来等待所有网格运行完成,并得到运行结果
r:=array();
for j:=0 to length(A)-1 do
r[j]:=dupvalue(A[j]);//复制网格子程序结果-》即等待网格运行完成
Return r;
```
//---需要被网格调用的函数 CalcStock
```text
Function CalcStock(StockId);
Begin
//…………………
End;
```
### 网格计算代入系统参数的案例
在有些函数调用的时候,需要设置系统参数,由于网格计算是一个重新开始的计算,并不会主动将系统参数带给新的网格计算函数,这个时候可以使用 with 后缀。
例如:
```text
A[i]:=#Close()
with Array(pn_Stock():StockId,pn_Date():ToDay()-1)
```
就可以获得指定的股票昨天的收盘价,\*实际中我们不会用网格计算去调用收盘价函数,因为网格存在开销那样的效率会更低,这只是一个例子。
### 网格计算设置任务超时时间
网格调用时可通过设置 timeout N 对该子进程进行设置超时时间,若网格运行的程序运行时间超过该设置时间(单位:毫秒),则程序进行报错。
如有网格运行目标程序:
```text
Function testdo();
begin
sleep(10*1000);
return getsysparam(pn_stock());
end;
```
在网格中设置超时间为 3 秒,调用如下:
```text
r:=# testDo() timeout 3000;
t:=dupvalue(r);
return t;
```
在网格中通过 with 传入系统参数的同时设置超时间为 3 秒,调用如下:
```text
r:=# testDo() with array(pn_stock():"SZ000002") timeout 3000;
t:=dupvalue(r);
return t;
```
超时报错 Grid timeout,示例如下:
## TSL 全局缓存的应用说明
### 内容
- 全局缓存管理
- 初始化 TSL 和监控管理的 TSL
### 全局缓存管理
TSL 设计的全局缓存是一套极为高效的内存缓存机制,其以 COPYONWRITE 的模式实现了设置和写入完全分离,允许存在多份使用中的版本,使得更新数据和读数据无冲突。
TSL 的全局缓存主要是为了对公共类的数据进行全局优化,尤其是对那些准备效率低下的数据,例如存贮在数据库内的数据,又或者是需要经过大的计算的数据。这些数据的全局缓存化可以使得应用不再关注于数据准备的开销上。
TSL 的全局缓存对于用户而言是一种新的数据类型,但这种数据结构和 TSL 原生数据结构完全相同,是在 TSL 的原生数据类型上扩展而成的,我们的开发使其不仅仅支持 TSL 的标准算符,例如四则运算,同时也支持矩阵计算,还支持子矩阵等算符,不仅仅如此,全局缓存还支持 SELECT,绝大多数 TSL 的函数对于全局缓存也是透明的,在计算的使用上,用户完全不需要理会一个数据到底是全局缓存还是其他的类型,除非真的需要(例如缓存是否过期等)。
TSL 的全局缓存对于用户的透明还有一个特性,我们对全局缓存的数据类型进行更改的时候(不是设置),系统会自动将用户使用的全局数据的引用实例化,也就是会将全局数据的相关内容复制到用户的运行环境中,然后进行修改的操作。
TSL 的全局缓存主要应用于数组和矩阵两种类型,也支持其他简单类型,但仅仅对数组和矩阵两种类型采用引用的方式,而其余数据类型取出的时候就进行了实例化。
由于我们对 TSL 全局缓存的透明处理,因为普通函数无法分辨全局缓存还是标准数据类型,除非采用特殊的函数。
#### 内容
- 全局缓存管理的函数
- 全局缓存的使用
- 全局缓存的过期与回收策略
- 全局缓存管理的初始化和监控
- 未升级的系统对新代码的使用
#### 全局缓存管理的函数
##### 内容
- SetGlobalCache
- GetGlobalCache
- CheckGlobalCacheExpired
- GetGlobalCacheInfo
- ListGlobalCache
- ListGlobalCacheRemoved
- IfCache
##### SetGlobalCache
范例
```text
a:=rand(1000,100);
return SetGlobalCache("LLL",a,now()+1); //设置一个名为”LLL”的全局缓存,生存周期为1天
```
##### GetGlobalCache
范例
```text
mtic;
for i:=0 to 999999 do
getglobalCache("LLL",V);
return array(V,mtoc);
```
这个例子告诉我们,对于全局缓存数据,无论全局缓存数据本身多大,例如这个是 1000\*1000 的矩阵,获取 100 万次花费的时间也是微乎其微的。这样我们的数据准备工作所耗费的时间几乎就不存在了。
##### CheckGlobalCacheExpired
范例
```text
Setglobalcache("VVV",rand(1000,100));
Getglobalcache("VVV",V);
expired1:=CheckGlobalCacheExpired(V);
setglobalcache("VVV",rand(1000,100));//重置,V过期
return array("重置前":expired1,"重置后":CheckGlobalCacheExpired(V));
```
##### GetGlobalCacheInfo
范例
```text
if GetGlobalCache("LLL",V) then
return GetGlobalCacheInfo(V);
```
##### ListGlobalCache
范例
```text
getglobalcache("LLL",v);
return listglobalcache();
```
Owners 列里是缓存使用中的用户列表
##### ListGlobalCacheRemoved
范例
范例 1
```text
SetGlobalCache("CCC",array(1,2,3));
Getglobalcache("CCC",V); //v指向全局缓存
setglobalcache("CCC",array(1,2,3,4));//v的版本已经过期
return listglobalcacheremoved();
```
存在一份 CCC 的过期版本。
范例 2
```text
SetGlobalCache("CCC",array(1,2,3));
Getglobalcache("CCC",V); //v指向全局缓存
setglobalcache("CCC",array(1,2,3,4));//V已经过期
//return listglobalcacheremoved();
V:=nil;//V释放了没有过期版本
return listglobalcacheremoved();
```
返回结果:空数组。
##### IfCache
#### 全局缓存的使用
##### 内容
- 全局缓存的基础函数和子矩阵的支持
- 全局缓存的算符支持
- 支持 SELECT
- 写入实例化
##### 全局缓存的基础函数和子矩阵的支持
绝大多数的函数已经可以完全支持全局缓存,当成和原始数据类型对待,而子矩阵这类的操作也毫无问题。
```text
a:=array();
for i:=1 to 9 do
for j:=1 to 9 do
a[i-1,j-1]:=i*j;
setglobalcache("99MT",a);
Getglobalcache("99MT",V);
return array(sum(sum(V)),length(V),mcols(V),ifarray(V),V[8,8],V[0:3,0:3]);
```
##### 全局缓存的算符支持
对于四则运算等算符,以及矩阵运算符,还有集合运算符号等等,全局缓存和原始类型一致。
```text
a:=array();
for i:=1 to 9 do
for j:=1 to 9 do
a[i-1,j-1]:=i*j;
setglobalcache("99MT",a);
Getglobalcache("99MT",V);
return V+100;
```
##### 支持 SELECT
对于 SELECT 而言,全局缓存的表现和其原始数据没有任何差异。
```text
a:=array();
for i:=1 to 9 do
for j:=1 to 9 do
a[i-1,j-1]:=i*j;
setglobalcache("99MT",a);
Getglobalcache("99MT",V);
return select * from V order by [0] desc end;
```
##### 写入实例化
当对全局缓存进行各式写入操作时,无论是数据设置,还是 UPDATE,INSERT 等 SQL 操作,我们均会将全局缓存实例化,在用户使用的时候和传统数据复制后进行写入操作毫无差异,这样最大化地保证了易用性。
```text
a:=array();
for i:=1 to 9 do
for j:=1 to 9 do
a[i-1,j-1]:=i*j;
setglobalcache("99MT",a);
Getglobalcache("99MT",V);
b:=ifcache(V);//是缓存 ,b为真
V[0,0]:=100; //设置完成后,ifcache就为假了
return array(b,ifcache(V),V);
```
使用 SQL 的 Update 更新全局缓存也引发数据的实例化
```text
a:=array();
for i:=1 to 9 do
for j:=1 to 9 do
a[i-1,j-1]:=i*j;
setglobalcache("99MT",a);
Getglobalcache("99MT",V);
b:=ifcache(V);//是缓存 ,b为真
update v set [0]=1 end; //update后V也不再是globalcache
return array(b,ifcache(V),V);
```
#### 全局缓存的过期与回收策略
用户一旦使用过期的全局缓存,会导致内存的占用,因而系统必需建立回收过期缓存的机制,否则可能会危害到系统的正常运行的安全。
一旦系统进行过期的内存的回收,会导致使用这些过期的全局缓存的模型被终止,并产生不可恢复的错误,这又会对一些特殊的应用造成,因而内存的回收是必要又是需要谨慎的。
TSL 为全局缓存建立了一套回收规则,结合了系统的剩余内存比例,剩余内存的物理大小,以及占用的过期内存总和大小以及占用的时长等等,基于极为审慎的原则对违反规则的模型进行终止,保障其他正常模型的运行。
一旦遇到这类的问题,用户应该检查模型使用这些全局缓存是否存在问题。由于全局缓存的获取效率以及使用效率均极高,用户不应该将全局缓存放在系统变量,TSL 的 GLOBAL 存贮等等中,用户应尽量直接使用缓存,并审慎长期占用缓存的模式。
##### 内容
- 配置
##### 配置
配置在 plugin\FileMgr.ini 中
一个典型的设置如下:
[Global Cache]
MemoryLoadLimit=90 //在物理内存使用达到 90%的时候才检查
MemoryAvailLimit=26214400 //在内存剩余大小不到 25G 时才检查,单位 KB
ExpiredSecondsCheck=900 //允许过期后使用的秒数
ExpiredLoadLimit=5 //允许过期的全局缓存占用的物理内存百分比
ExpiredAvailLimit=16777216 //允许过期的全局缓存占用的物理内存大小,单位 KB
###### 内容
- MemoryLoadLimit
- MemoryAvailLimit
- ExpiredSecondsCheck
- ExpiredLoadLimit
- ExpiredAvailLimit
###### MemoryLoadLimit
单位:百分数
描述进行全局缓存回收的物理内存占用比例阈值,不超过该阈值不回收
###### MemoryAvailLimit
单位:KB
描述在内存剩余大小低于的阈值才回收,剩余内存超过不回收。
###### ExpiredSecondsCheck
单位:秒
描述安全使用的过期后的时长
###### ExpiredLoadLimit
单位:百分数
描述允许超过安全使用时的全局内存占用的物理内存比例
###### ExpiredAvailLimit
单位:KB
描述允许超过安全使用时的全局内存占用的物理内存大小。
#### 全局缓存管理的初始化和监控
##### 内容
- 有瑕疵的全局缓存管理方式
- 期望的方式
- 不推荐的模式
- 全局缓存的初始化
- 全局缓存的更新监控
##### 有瑕疵的全局缓存管理方式
全局缓存的生成,一种模式是在由应用模型内来设置:
例如: if not GetGlobalCache(CacheName,V) then
Begin
V:=CalcDataCall();
SetGlobalCache(Cache,V);
End;
但这样存在几个问题:
一是全局缓存的设置是需要权限的,一旦采用这样的模式,代码只能运行在高权限下。
二是用户模型的性能是不稳定的,当第一次运行的时候会很缓慢,这样有时候会造成不可靠的用户体验。
三是我们很难知道何时适合于进行缓存的准备工作以及缓存的更新工作,因为缓存总有失效的时候,如果要解决失效问题,我们还得将代码变成如下:
if not GetGlobalCache(CacheName,V) or IsDataNeedReCalc(V) then
Begin
V:=CalcDataCall();
SetGlobalCache(Cache,V);
End
我们需要在 IsDataNeedReCalc 里来检查诸如外部数据的版本是否发生了变更等工作,往往这种检查对事件的耗费远远大于全局缓存的获取,这样又会影响到用户模型的效率。
##### 期望的方式
如果全局缓存的生成和更新,交由系统,那么我们期望的应用开发是如下模式:
if not GetGlobalCache(CacheName,V) then
V:=CalcDataCall();
由于全局缓存系统管了生成,所以我们只需要取即可,如果系统未生成,我们直接进入计算模式。
##### 不推荐的模式
GetGlobalCache(CacheName,V);
有的开发者会在升级应用后,将取数程序变成了最简单的模式,假设缓存的获得会成功,这样在项目实施中并无不可,假设数据的初始化和数据变更确定性由系统其他部分完成了,这样也带来了数据底层来源和上层应用分离的优势。
但在产品开发中,我们并不推荐如此模式,因为这样会带来了新的问题。
一是采用缓存本身是对旧有模式的升级,一旦采用了这样的方式,缓存就成为了必需而非选项,这样对程序的兼容产生不利影响。
二是可能产生内存依赖,缓存本身是依赖数据在内存里的常驻来达到性能的飞跃,如果一些系统的内存本身就不足够,实施缓存模式本身就不现实。
三是不利于灵活化实施,我们可能会根据内存的大小进行灵活化实施,例如某些数据进入到缓存管理里,某些数据则采用旧有模式,在这种情况下,保留缓存的检查的模式是最佳的选择。
##### 全局缓存的初始化
采用【新开发的初始化 TSL 和监控管理的 TSL】功能,将所需的全局缓存的初始化工作交由 InitRun.TSL 来完成。如果开发采用了强依赖模式,那么,我们在初始化中必需保障全部用到的全局缓存的准备工作。
系统将在这个初始化 TSL 运行完成后才会接收应用,无论是 WEB 还是平台。
##### 全局缓存的更新监控
采用【新开发的初始化 TSL 和监控管理的 TSL】功能,将所需的全局缓存的更新监控工作交由 AutoMon.TSL 来完成。这个 TSL 会在独立的线程里运行,和用户模型的运行平行独立。而 AutoMon.TSL 的工作是监视数据的变动,当数据发生了变动就立刻重置全局缓存。
如果开发采用了对全局缓存的弱依赖模式,例如初始化全部全局缓存的时间代价太高,而全局缓存的击中概率也不高,那我们也可以将一些本身应由初始化完成的全局缓存准备工作交由监控来完成。这样可以逐步将全局缓存准备出来,而不会因为初始化速度很低影响到用户的使用。
#### 未升级的系统对新代码的使用
由于 TSL 的设计特性,底层系统函数的优先级高于公共和用户函数,我们建议在未升级全局缓存管理功能的用户处,按照规范新增两个函数 SetGlobalCache 以及 GetGlobalCache,返回的结果为假即可。
这样,老的 TSL 版本也可以正确地运行使用了新特性的模型。
### 初始化 TSL 和监控管理的 TSL
某些特殊情况下,平台或者 WEB 都需要一些初始化工作,例如数据初始化或者缓存准备等工作,这时候平台管理者会希望在启动的时候运行一个 TSL 代码。当没有这种支撑的时候,往往管理者会采用调度一个 TSL 代码来执行的模式。
在另外一些情况下,平台或者 WEB 可能需要一些非用户任务,不需要调度而是不断在后台监控运行。例如,用于数据变动检查,进行一些资源回收等等,这些工作有时候被开发者放入了一些用户模型中,这样既不及时,也会影响用户模型的效率,而且还存在一些权限性问题。
为了这些应用的需求,因而我们在天软的平台以及 WEB 模块里设计了初始化 TSL 以及监控 TSL 的功能。
初始化是指平台或者 WEB 在启动的时候先允许执行一个初始化的 TSL。
而监控 TSL,则是允许在后台启动数个线程(一般只需要 1 个),这个线程可以运行监控的 TSL,用于从事缓存更新以及其他所需要的工作。
注:初始化 TSL 只运行一次,而监控的线程不会退出,当 TSL 运行完毕后会重新调用运行。
#### 内容
- 功能设计
#### 功能设计
##### 内容
- WEB 模块的初始化和监控
- 平台的初始化和监控
##### WEB 模块的初始化和监控
Apache 的模块,设置在 TSL.INI 中。
[WebApp]
automonthreads=1 //监控线程的个数
initrun=1 //是否运行初始化 TSL
###### 内容
- 初始化
- 监控及管理线程
###### 初始化
初始化 InitRun.TSL 位于进程或者模块所在目录,进程所在目录优先。
当 initrun=1 时候,apache 的模块将会在启动后运行 InitRun.TSL,运行完成后才接收请求,否则返回 406 错误,HTTP 406 错误指无法接受 (Not acceptable)错误
###### 监控及管理线程
Automonthreads 设置的是启动的监控管理线程的数量。
这些线程执行 WEB 服务器进程或者模块所在目录的文件(进程目录优先):
文件名的规则为:
第一个线程执行 AutoMon0.TSL,第二个线程执行 AutoMon1.TSL,依此类推。
如果每个线程没有特殊的 TSL,则执行缺省的 AutoMon.TSL
执行 TSL 可以使用 AutoMonIndex 系统参数来获得自己是第几个线程,并可以 AutoFileName 系统参数获得运行的文件名
##### 平台的初始化和监控
初始化是在执行接收分发任务前,设计上初始化无法也不允许调用网格计算等功能。
监控进程目前并未支持调用网格计算。
###### 内容
- 初始化
- 监控及管理线程
- 平台的初始化和监控 TSL 的管理
- 平台的初始化和监控的权限管控
###### 初始化
初始化的启动是通过参数-i 来设定的,例如 Exec64.exe -i 则表明需要启动初始化。
初始化程序为 InitRun.TSL 位于执行进程所在目录。
当初始化参数指定的时候,执行将在数据同步准备完成后启动 InitRun.TSL,运行完成后才接收请求。
###### 监控及管理线程
监控线程的启动是通过参数-M 来设置,例如 Exec64.exe -M 1 则表明采用了一个监控管理线程。
这些线程执行进程所在目录的文件:
文件名的规则为:
第一个线程执行 AutoMon0.TSL,第二个线程执行 AutoMon1.TSL,一次类推。
如果每个线程没有特殊的 TSL,则执行缺省的 AutoMon.TSL
执行 TSL 可以使用 AutoMonIndex 系统参数来获得自己是第几个线程,并可以 AutoFileName 系统参数获得运行的文件名
###### 平台的初始化和监控 TSL 的管理
应用执行服务启动的时候,会尝试同步下载 InitRun.TSL 以及 AutoMon.TSL 和相应的 AutoMon0….TSL 等 TSL 文件。这些文件平台管理员可以按照更新 ini 配置的方式通过事件服务器进行统一管理。
###### 平台的初始化和监控的权限管控
如果初始化和监控 TSL 需要调用内外部的 TSL 函数来进行数据准备,设计者推荐用户采用 data:=sudo("modeluser",getcalcdata())的模式来进行数据的一些准备工作,因为 GetCalcData()这类的函数往往不需要任何特殊权限,这样可以最大限度地防止非授权代码的运行。
如果我们仅仅只是利用初始化和监控进行一些系统性操作,设计者强烈建议不需要使用一些中间函数,将除了二进制函数外的实现直接在.TSL 里完成,这样做可以让这些代码可以独立运行。如果无法保障这一点,强烈建议将无需权限运行的内容以 sudo 模式来运行。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+20 -10
View File
@@ -2,6 +2,12 @@
本文件提供一份**通用占位模板**,用于在不同 TSL 项目中快速补齐“工具链与如何验证”的关键上下文。
最小必填清单(落地到具体项目时必须补齐,否则文档不可用):
- 工具名称、可执行命令、安装方式(版本要求可选)
- 至少一个可运行的“最小冒烟”命令(或脚本入口)
- 失败处理约定(含:无法执行时的替代验证方式)
使用方式:
- 在具体项目中复制本模板并把占位符替换为真实信息;
@@ -12,10 +18,11 @@
### 1.1 解释器/编译器(必填)
- 工具名称:`<tsl/tslcli/内部工具名>`
- 可执行命令:
- macOS/Linux`<command>`(例:`tsl` / `tslcli` / `sh scripts/tsl.sh`
- Windows`<command>`(例:`tsl.exe` / `tslcli.exe` / `powershell -File scripts/tsl.ps1`
- 版本要求:`<固定版本或范围,例如:= 3.2.1 / >=3.2,<4.0>`
- 可执行命令(统一用 `<tsl>` 表示 TSL 可执行入口)
- macOS/Linux`<tsl>`(例:`tsl` / `sh scripts/tsl.sh`
- Windows`<tsl>`(例:`tsl.exe` / `powershell -File scripts/tsl.ps1`
- 基本执行方式:`<tsl> <path/to/script.tsl> <args...>`TSL 通常直接执行脚本文件)
- 版本要求(可选):`<固定版本或范围,例如:= 3.2.1 / >=3.2,<4.0>`(未知可留空或写 `N/A`
- 安装方式:`<内部安装包/路径/IDE 自带/CI 镜像等>`
- 推荐统一入口脚本:`scripts/tsl.{sh,ps1}`(封装参数与环境变量,避免每个任务重复猜测)
@@ -26,6 +33,7 @@
- 运行约束:
- 是否允许联网:`<yes/no>`
- 是否需要许可证/凭证:`<说明如何在本地与 CI 提供;禁止写入仓库>`
- 约定:凭证/许可证等敏感信息通过环境变量或 CI secrets 注入;文档只写变量名/获取方式,不写明文值。
## 2. 验证命令
@@ -33,30 +41,32 @@
### 2.1 最小冒烟(必须能跑)
- macOS/Linux`<tsl> run <path/to/SmokeTest.tsl> -- <args>`
- Windows`<tsl.exe> run <path\\to\\SmokeTest.tsl> -- <args>`
- macOS/Linux`<tsl> <path/to/SmokeTest.tsl> <args...>`
- Windows`<tsl> <path\\to\\SmokeTest.tsl> <args...>`
- 或统一入口:
- `sh scripts/smoke.sh`
- `powershell -File scripts/smoke.ps1`
- Success signal(建议写清):退出码为 0;并给出“成功时的关键输出/产物路径”(例如输出包含某行、或生成某文件)。
### 2.2 单元测试(如有)
- `sh scripts/test.sh`
- 或:`<tsl> test <tests/>`
- 或:`<tsl> run <path/to/TestRunner.tsf>`
- 或:`<tsl> <path/to/TestRunner.tsl> <args...>`
- Success signal:退出码为 0;失败时能定位到具体用例/输入。
### 2.3 静态检查/格式化(如有)
- `sh scripts/lint.sh`
- `sh scripts/format.sh`
- 或:`<tsl> check <src/>` / `<tsl> fmt <src/>`
- Success signal:退出码为 0formatter 二次运行无新增 diff(若项目提供 formatter)。
### 2.4 构建/打包(如有)
- `sh scripts/build.sh`
- 或:`<tsl> build <project-file>`
- Success signal:退出码为 0;产物路径明确且可复现(例如输出目录/包名)。
### 2.5 失败处理约定(必填)
- 只修复与本次改动直接相关的失败;无关失败在输出中说明并隔离。
- 若某验证步骤无法执行(缺环境/缺凭证),必须明确写出原因与替代验证手段(例如最小复现脚本/手动检查清单)。
- 建议在输出中记录:执行的命令、退出码、以及关键日志片段(便于 review 与复现)。