Appearance
SWB 附录 A:预处理器与引用语法
来源:
swb_ug.pdfAppendix A(W-2024.09) 原文标题:Preprocessor and Reference Syntax说明:本页按 2024 版附录 A 的原顺序整理,重点覆盖@...@引用、树导航、#命令、split 命令与节点表达式。
附录说明
本附录讨论两类基础能力:
@...@引用与树导航语法- 预处理器命令,也就是所有以
#开头的控制语句
它和正文里的 Chapter 6、Chapter 12 关系很紧,但职责不同:
- Chapter 6 更偏“如何在项目中用预处理推进流程”
- Chapter 12 更偏“层级项目组织下路径如何变化”
- 附录 A 则更像完整语法参考表
@ 引用与树导航
手册先给出 @ 引用的 EBNF 结构。按原意改写后,可以理解成:
text
reference: simple_reference [ operator [operator] ]
simple_reference:
"node" | "previous" | file_type ["/i"] | ["/o"] |
"experiment" | "experiments" | "process_name" | "swb_parameter" |
parameter_name | variable_name
operator:
":" | "|"
后面可跟:
+number | -number | tool_label | first | last | index | all | min | max基本组成
| 名称 | 含义 |
|---|---|
file_type | 工具数据库中定义的某一种文件类型 |
/i | 指向当前工具的对应输入文件 |
/o | 指向当前工具的对应输出文件 |
parameter_name | 已声明参数之一 |
variable_name | 已知变量之一 |
tool_label | 仿真流程中某个工具实例的标签 |
experiment | 返回当前节点所属的第一个实验 |
experiments | 返回当前节点所属的全部实验 |
process_name | 返回当前节点所属的 process name |
swb_parameter | 返回当前节点的参数名 |
/i 与 /o 的语义
手册对 file_type 的扩展说明比较关键:
file_type/i指向当前工具的对应输入文件file_type/o指向当前工具的对应输出文件- 如果不写
/i或/o,SWB 会沿树向上查找“前一个匹配工具”生成的隐式输入文件
这也是为什么 @tdr@、@plot@ 这类引用在不同上下文里可能解析到不同文件。
横向流程方向
手册先解释了横向流程下的导航规则。
在这种方向里:
- 垂直方向的一个单位表示一个完整仿真阶段
- 中间节点,也就是虚拟节点或 split point,不计入这个单位
同时,横向与纵向导航运算符是可以组合使用的。
运算符方向含义
| 运算符 / 写法 | 横向流程中的含义 |
|---|---|
| ` | ` |
: | 垂直导航运算符 |
+number | 向右的相对引用,或向下的垂直相对引用 |
-number | 向左的相对引用,或向上的垂直相对引用 |
number | 向右或向下的绝对索引引用 |
all | 返回指定水平层上的全部引用 |
first | 返回该层最左侧引用 |
last | 返回该层最右侧引用 |
index | 返回该层的水平索引,而不是节点号;最左节点索引为 1 |
min | 返回该层最左节点索引,始终为 1 |
max | 返回该层最右节点索引 |
手册还特别说明:在“垂直运算符”位置,你也可以直接使用工具实例标签 tool_label 作为绝对位置指示器。
横向流程中的求值结果
一个引用最终会返回什么,取决于它引用的是哪一类对象:
- 若使用
index、min、max作为水平运算符,返回的是水平节点索引 - 若引用的是
parameter_name,返回参数值 - 若引用的是
file_type,返回文件名 - 若引用的是
tool_label,返回工具标签 - 若引用的是
node或previous,返回节点号
如果一个引用会得到多个值,SWB 返回的是一个以空格分隔的列表。
横向流程中的典型例子
| 引用 | 含义 |
|---|---|
@node@ | 当前节点号,也就是当前工具实例输出节点的节点号 |
@node:all@ | 当前树层上所有节点号列表,也就是当前工具输出节点所在层的全部节点 |
@node:2@ | 当前树层上第 2 个节点的节点号 |
@node:+2@ | 当前节点下方两个位置处的节点号,同列 |
@node:-1@ | 当前节点正上方的节点号,同列 |
@node:first@ | 当前树层最上方节点的节点号 |
| `@node | -1:all@` |
| `@node | +3@` |
@node:index@ | 当前节点索引 |
@node:min@ | 当前层第一个索引,始终为 1 |
@file_type@ | 第一个前置匹配工具输出的该类型文件 |
@file_type:all@ | 当前树层上该类型文件的全部文件名 |
@file_type:3@ | 当前树层第 3 个工具实例生成的该类型文件 |
@file_type:+1@ | 当前节点下方节点上该类型文件 |
@file_type:last@ | 当前层最右侧该类型文件 |
@file_type/i@ | 当前工具该类型输入文件,例如 n5_mesh.tdr |
@file_type/o@ | 当前工具该类型输出文件,前提是这个文件类型被声明为输出 |
| `@file_type/o | -1@` |
@node:max@ | 当前树层最后一个索引,也就是该层节点总数 |
@tool_label@ | 当前节点对应工具标签 |
| `@tool_label | all@` |
| `@tool_label | 1@` |
| `@tool_label | +1@` |
| `@tool_label | -1@` |
| `@tool_label | first@` |
| `@tool_label | last@` |
纵向流程方向
附录 A 接着给出纵向流程下的规则。
在这种方向里:
- 水平方向的一个单位表示一个完整仿真阶段
- 中间节点,也就是虚拟节点或 split point,同样不计入这个单位
手册指出,在纵向流程中,“垂直运算符”可使用的附加关键字依然包括 all、first、last、index、min、max,只是方向解释会发生变化。
运算符方向含义
| 运算符 / 写法 | 纵向流程中的含义 |
|---|---|
| ` | ` |
: | 水平导航运算符 |
+number | 向下的垂直相对引用,或向右的水平相对引用 |
-number | 向上的垂直相对引用,或向左的水平相对引用 |
number | 向下或向右的绝对索引引用 |
all | 返回指定垂直层上的全部引用 |
first | 返回该层最上方引用 |
last | 返回该层最下方引用 |
index | 返回垂直索引,而不是节点号;最上方节点索引为 1 |
min | 返回该层最上方索引,始终为 1 |
max | 返回该层最下方索引 |
手册还说明:在“水平运算符”位置,也可以使用 tool_label 作为绝对位置指示器。
纵向流程中的求值结果
纵向流程下的求值原则与横向流程相同,只是索引方向从“水平”变成“垂直”:
- 若使用
index、min、max,返回的是垂直索引 - 参数名返回参数值
- 文件类型返回文件名
- 工具标签返回工具标签
node或previous返回节点号
如果得到多个值,依然返回空格分隔列表。
纵向流程中的典型例子
| 引用 | 含义 |
|---|---|
@node@ | 当前节点号 |
@node:all@ | 当前树层全部节点号 |
@node:2@ | 当前树层第 2 个节点号 |
@node:+2@ | 当前节点右侧两个位置的节点号,同一行 |
@node:-1@ | 当前节点左侧紧邻节点号 |
@node:first@ | 当前树层最左侧节点号 |
| `@node | -1:all@` |
| `@node | +3@` |
@node:index@ | 当前节点索引 |
@node:min@ | 当前层第一个索引,始终为 1 |
@file_type@ | 第一个前置匹配工具输出的该类型文件 |
@file_type:all@ | 当前树层该类型全部文件名 |
@file_type:3@ | 当前树层第 3 个工具实例的该类型文件 |
@file_type:+1@ | 当前节点右侧节点上的该类型文件 |
@file_type:last@ | 当前层最下方该类型文件 |
@file_type/i@ | 当前工具该类型输入文件 |
@file_type/o@ | 当前工具该类型输出文件 |
| `@file_type/o | -1@` |
@node:max@ | 当前树层最后一个索引,也就是当前层节点总数 |
@tool_label@ | 当前节点工具标签 |
| `@tool_label | all@` |
| `@tool_label | 1@` |
| `@tool_label | +1@` |
| `@tool_label | -1@` |
| `@tool_label | first@` |
| `@tool_label | last@` |
返回当前目录的补充引用
手册在这一部分最后补充了两种“不带后缀”的路径引用:
| 引用 | 含义 |
|---|---|
@pwd@ | 项目的绝对路径 |
@pwd@/@file_type@ | 带绝对路径的文件引用 |
# 命令
附录 A 随后说明所有预处理器命令的共同规则:任何预处理器命令都必须以 # 作为行首第一个字符。初始 # 后面允许有空格或制表符缩进。
手册说明,spp 能识别以下命令。
预处理器命令总表
| 命令 | 含义 |
|---|---|
#<string> | 普通注释。spp 会把所有注释行从预处理结果中去掉,并用空行替换。这里的 <string> 是不属于其他预处理命令的任意字符串。 |
#define <name> <string> | 定义新宏 <name>,后续出现的 <name> 会被替换成 <string>。 |
#undef <name> | 取消之前定义的宏。 |
#setdep <list of nodes> | 显式设置这些节点的依赖关系。 |
#remdep <list of nodes> | 显式移除这些节点的依赖关系。 |
#include "<filename>" | 在当前位置包含指定文件内容。被包含文件会像当前文件的一部分那样被 spp 处理。 |
#includeext "<filename>" | 与 #include 类似,但会对文件名做更高级处理,允许 <filename> 中包含已由 #define 定义的宏,也允许在包含过程中定义新宏。 |
#if <expression> | 如果表达式求值非零,则到匹配的 #else、#elif 或 #endif 之前的后续行会被保留。表达式必须是标准 Tcl 表达式,并且在求值前会先展开其中的 @ 替换。 |
#if in <gexpr> | 与 #if 类似,但条件改成“当前节点是否属于 gexpr 返回的节点集合”。 |
#ifdef <name> | 仅当宏 <name> 之前已通过 #define 定义时,保留后续内容。 |
#ifndef <name> | 仅当宏 <name> 之前尚未定义时,保留后续内容。 |
#elif <expression> | 可在 #if 和匹配的 #else / #endif 之间出现任意多个。只有在前面 #if 与所有前置 #elif 都为零,而当前表达式非零时,才会保留它后面的内容。 |
#else | 反转当前条件段的保留逻辑。如果前面的条件已经成立,则 #else 到 #endif 被忽略;反之则保留。条件块可以嵌套。 |
#endif | 结束 #if、#ifdef 或 #ifndef 段。 |
#exit | 在这一行停止预处理,后续全部内容都被剥离。 |
#verbatim <string> | 不剥离该行,只去掉 #verbatim 前缀,后面的内容原样保留。 |
#rem <string> | 该行不会被剥离,但行内其余部分仍会进行 @ 替换。 |
#noexec | 当前节点不执行,也就是不提交到调度器。 |
#set <varname> <value> | 设置变量值,并在 SWB 的 Variable Values 视图中显示该变量。 |
#seth <varname> <value> | 设置变量值,但在 Variable Values 视图中隐藏该变量。 |
条件命令的理解
附录 A 对 #if / #elif / #else 解释得很细,核心逻辑可以概括为:
- 先看最外层
#if是否成立。 - 若不成立,按顺序继续判断每个
#elif。 - 一旦某个
#elif成立,后面的#elif和#else都不再生效。 - 如果前面都不成立,才轮到
#else。
这个规则和传统预处理器很像,但这里的表达式是 Tcl 表达式,而且会先做 @...@ 替换。
split 命令
附录 A 接着讨论 split point 相关命令。
手册先给出一个重要限制:多个 split point 只有在它们的出现顺序与仿真流程中对应参数的顺序一致时才有效。若顺序不一致,预处理器只会采用它认为“最佳匹配”的部分,而忽略某些 split point。
手册还强调:一个与 split 区段对应的“部分输入文件”,在前向引用语义上仍然被视为普通工具输入文件。常见错误是:某个参数引用出现在 split point 之前,但被引用参数在流程里其实位于 split 参数之后。这样很容易触发预处理错误。
split 相关命令
| 命令 | 含义 |
|---|---|
#header | 标记 header 区段起始。header 会在每个 partial input file 的最开头被复制到 load 命令之前。只能定义一个 header。 |
#endheader | header 区段结束行,预处理后会替换成空白行。 |
#postheader | 定义一个 postheader 区段。它会在每个 partial input file 的 load 命令之后复制进去。这个区段适合放“必须在 load 之后重新设置的仿真器默认项”。可以定义多个 postheader,按原文件中的定义顺序追加;但不允许嵌套或重叠。 |
#endpostheader | postheader 区段结束行,预处理后会替换成空白行。 |
#split @PNAME@ | 在参数 PNAME 上定义一个 split point。当前文件会被切成两段:从上一个 split point 到当前行的一段,以及从当前行到下一个 split point 或文件结束的下一段。当前行会在第一段末尾被替换成 save 命令。第二段文件则以 header、load 命令、可选 postheader,再加上真实 partial input section 的顺序开始。这里的 param_name 指 split 所作用的树层。 |
#split PNAME | 与 #split @PNAME@ 等价,是为兼容旧项目保留的旧语法。新项目里手册明确建议使用 #split @PNAME@。 |
header 与宏定义的注意事项
手册给了一个非常实用的提醒:凡是必须在所有 split 文件中都唯一且共享的 # 预处理器命令,都应该放到 #header ... #endheader 里。
例如,以下宏定义如果放在 header 中:
text
#header
...
#define macro1 string1
#define macro2 string2
#define macro3 string3
...
#endheader那么所有由 #split 生成的文件都能看到这些宏。
如果不放在 header 里,它们只会出现在第一个拆分文件中,后面生成的拆分文件就不知道这些宏,最终可能导致预处理错误。
节点表达式
附录 A 最后给出节点表达式 GEXPR 的 EBNF。
text
gexpr : gterm [operator gterm]
operator : "+" | "*" | "-" | "^"
gterm : scnr["|"level][":{" filter "}"] |
tool_label[":{" filter "}"] |
node | "(" gterm ")" | "~" gterm
node : integer
scnr : "all" | identifier
level : integer | "last" | tool
tool : identifier
filter : tcl_expr基本组成
| 名称 | 含义 |
|---|---|
scnr | 场景名 |
tool_label | 工具实例标签 |
node | 节点号 |
level | 树层级,从 0 开始 |
last | 最后一层 |
tool | 工具标识符 |
filter | Tcl 表达式 |
运算符含义
| 运算符 | 含义 |
|---|---|
+ | “或” / 并集 |
* | “与” / 交集 |
- | 差集 |
^ | 异或 |
~ | 向根扩展(extend-to-root)的一元运算符 |
理解方式
从语义上看,节点表达式就是一套“选节点集合”的语言。你可以:
- 直接选某个场景
- 选某个工具标签对应的节点
- 选具体节点号
- 对节点集合做并、交、差、异或
- 再叠加过滤条件
它也是 #if in <gexpr> 这类命令的基础。
附录 A 的核心价值,是把正文中分散出现的语法点收成一张完整参考表。以后继续细化时,这页最值得再补的两块是:
- 增加“高频引用速查表”,把
@node@、@plot@、@tdr@、@pwd@、@pwdout@单独做成短表 - 补“横向流程 / 纵向流程”对照示意,让
|和:的方向差异更直观