# 表达式

> 变量和报警条件背后那一套表达式语言、它可以引用什么、每一种点位的可读面、求值器执行的那些规则、校验给出的提示，以及脚本编辑器和它的“浏览…”选择器怎么用。

Logic 页面上有两处用的是同一套表达式语言：一个[表达式变量](logic-variables)算出来的东西，以及一个[报警](logic-alarms)所盯着的那个条件。两者都是一条 JavaScript 表达式，在一个够不着这台机器的沙箱里以严格模式求值，也都在同一个脚本编辑器里编辑。动作脚本用的是同一套词汇，但它们是语句块；它们的不同之处在[动作](logic-actions)里讲。

## 什么算一条表达式 {#what-an-expression-is}

表达式恰好是一条 JavaScript 表达式：字面量、算术、比较、逻辑运算符、条件运算符 `a ? b : c`、字符串操作、模板字符串，以及对 JavaScript 内置的 `Math`、`Date` 和 `JSON` 的调用。语句（`let`、`if`、循环）属于动作：写第二条语句会被拒绝，写作“一个变量只保存一个表达式。语句属于动作。”表达式里任何位置上的赋值以及 `++`/`--` 都会被拒绝，写作“表达式是只读的。写入属于动作。”

表达式按点位带根的[地址](logic-address-space)来引用它们。每个根交给表达式的是什么：

| 根 | 什么进到表达式里 |
| --- | --- |
| `Connector` 的点位、`Variables`、`Alarms`、一个 Process 单元的 `Channels` | 一个带下面那套带类型可读面的实时对象。光写一个引用（只写 `Connector.Simulated.Pump.Flow`）求出来的是这个点位的值；光写一个报警求出来的是它的 `IsActive`。 |
| `System`、`Server`、`Assets`，以及一个 Process 单元的运行状态、产量、命令和预选 | 当前那个值本身：一个数字、一段文本、一个布尔量或者一个时刻。直接用就行；没有成员要接。接上实时点位的成员会被拒绝：`System.StartTime.Value` 得到“‘System.StartTime’是一个值：请不加 .Value 直接读取。” |

光写的实时点位是点位本身，不是它的值；只有当它是整个表达式时才代表它的值。凡是运算符把它当作值的地方，都请通过说明是哪个值的成员来读：判断真假的地方用 `.ValueToBool()`（`Variables.Start.ValueToBool() ? 1 : 0`），比较是否相等的地方用 `.Value`，比较大小或参与计算的地方用 `.ValueToReal()`（`Variables.Level.ValueToReal() > 80`），报警用 `.IsActive`。校验会拒绝写在 `?:` 的条件里、作为 `!`、`&&`、`||` 的操作数、写在比较（`==`、`!=`、`===`、`!==`、`<`、`>`、`<=`、`>=`）和算术（`+`、`-`、`*`、`/`、`%`，或前面的正负号）里的光写点位，因为在那里它要么始终被视为真，要么永远读不到它的值：`Variables.Level > 80` 得到“‘Variables.Level’是点位本身，而不是它的值：比较或计算永远读不到它的值。请用 Variables.Level.ValueToReal() 读取它的值。”传给函数的点位，以及点位的任何成员，都保持原样。同样的拒绝也适用于 `??` 的左边（点位永远不会让出它，请读 `.Value`），以及模板字符串里的占位 `${…}`（它永远不会把点位的值写进文本，请读 `.ValueToString()`）。

`System.Now` 是这次求值的时刻：一次求值里对它的每一处引用读到的是同一个时间。它和每一个时间戳一样，以 JavaScript 的 `Date` 进来，所以时刻之间自然能比较和相减：`(System.Now - Connector.Simulated.Pump.Run.SourceTimeStamp()) / 1000` 得到的是秒。

表达式变量可以引用点位、内部存储值变量、报警、Process 的点位、Assets 的点位以及 System 和 Server 的诊断值，但**绝不能引用另一个表达式变量**：编辑器会拒绝它（“‘Variables.X’是表达式变量。表达式不能引用其他表达式变量；请改为引用内部存储值变量或底层点位。”），这样结果就永远不取决于求值顺序。报警条件除了 `Alarms.*` 之外什么都可以引用（“报警条件不能引用其他报警。”），而且必须产出一个布尔量。

## 一个点位的可读面 {#the-read-surface-of-a-point}

一个实时点位提供：

| 成员 | 返回 | 含义 |
| --- | --- | --- |
| `.Value` | 带类型的值 | 工程值，走完这个点位整条[转换链](connector-value-pipeline)之后的样子 |
| `.ValueRaw` | 带类型的值 | 规整到点位声明类型的原始值，在量程换算之前 |
| `.ValueObject` | 任何东西 | 驱动产出来的、原封不动的那个值 |
| `.ValueScaled`、`.ValueCalibrated`、`.ValueLimited`、`.ValueRounded` | 数字 | 分别走完量程换算、校准、限值和小数位环节之后的值 |
| `.ValueToBool()`、`.ValueToInteger()`、`.ValueToReal()`、`.ValueToString()`、`.ValueToDateTime()` | 转换后的值 | 工程值转换成那个类别；没有值仍然是没有值 |
| `.State()` | 布尔量 | 质量：读数良好时为 true |
| `.SourceTimeStamp()` | Date | 设备产出这次读数的时刻（协议自带时间戳时，即 OPC UA）；否则是 Connector 收到它的时刻，与 `.ApplicationTimeStamp()` 相同 |
| `.ApplicationTimeStamp()` | Date | Connector 收到它的时刻 |
| `.Name`、`.EngineeringUnit` | 文本 | 这个点位的名称和单位 |

变量提供 `.Value`、五个 `.ValueTo*()` 转换、`.State()`、`.ApplicationTimeStamp()`、`.Name` 和 `.EngineeringUnit`。报警提供 `.IsActive`、`.IsAcked`、`.CurrentState`、`.State()`（它的条件在上一次扫描周期里求不求得出来）、`.ActiveSince()`、`.LastTransitionAt()` 和 `.Name`。Process 通道在绑到一个点位上时提供点位那套面，在绑到一个变量上时提供变量那套面；没有绑定的通道什么都不提供。实时对象上别的东西都够不着：这套面之外的成员读作 undefined，把它当函数调用会在校验时失败。“浏览…”选择器在每个点位下面列的就是这些成员，都不用凭记忆手打。

## 求值器执行的那些规则 {#the-rules-the-evaluator-enforces}

在编辑器里随着你打字执行一遍，在每 100 毫秒的扫描周期上再执行一遍：

- **质量关卡。** 求值之前，每一个被引用的点位都被解析并刷新。只要有任何点位缺失、不可用或者没有值，结果就直接成为一个质量不可用的空档，表达式根本不跑，所以一台离线的设备绝不可能造出一次采样，`null` 也绝不会进到引擎里变成 `0`。读数停止更新的点位（在轮询间隔再加 10 秒之内没有新的读数），在这里算作一次失败的读取：除非它在[设备没有回答](connector-value-pipeline#the-device-did-not-answer)时的策略给出替代值，否则质量就是不可用。
- **不是有限数的结果是空档。** NaN 或无穷（除以零、未定义的算术）绝不会变成一次采样。
- **失控保护。** 一次求值超过 250 毫秒、5000 条语句或者 4 MB 脚本内存就被中止，并算作一个质量不可用的空档。每一段表达式文字各留一个引擎，所以什么都不会从另一条表达式那里漏过来。
- **强制转换。** 求成功的结果会被强制转换成这个变量的[输出类型](logic-variables#output-types)；报警条件被强制转换成布尔量，而转换不了的结果让报警保持原状态。
- **错误是空档，不是异常。** 扫描周期里的一次运行时错误产出一个空档和一条诊断日志（第一次失败记一条，重复期间记汇总，恢复时记一行）；扫描周期接着往下一个变量走。

## 校验拒绝什么 {#what-validation-refuses}

编辑器每半秒校验一次草稿，并在数值或条件下面显示第一个问题：

| 提示 | 原因 |
| --- | --- |
| 需要填一条表达式。 / 需要填一个条件。 | 编辑器是空的。 |
| 表达式错误：… | 这段文字解析不了。 |
| 一个变量只保存一个表达式。语句属于动作。 | 超过一条语句，或者一条不是表达式的语句。 |
| 表达式是只读的。写入属于动作。 | 一次赋值或者 `++`/`--`。 |
| 地址必须是静态的：访问‘Root’时用了算出来的段。 | `Variables[name]` 或者任何非字面量的段。 |
| 地址‘Connector.Simulated’不完整。 | 这条链太短，在它的根下面点不到一个点位。 |
| 找不到‘X’。 | 这个地址什么都解析不出来。 |
| ‘X’是一个表达式变量。… | 引用了另一个表达式变量。 |
| ‘X’是一个值：请不加 .Value 直接读取。 | 在作为值本身进入的点位后面接了实时点位的成员（`.Value`、`.State()` 等）（`System.StartTime.Value`）。 |
| ‘X’是点位本身，而不是它的值：作为条件时它始终被视为真。请用 X.ValueToBool() 读取它的值。 | 在判断真假的地方光写的实时点位：`?:` 的条件，或 `!`、`&&`、`\|\|` 的操作数（`!Variables.Start`）。对报警，消息写的是 `.IsActive`。 |
| ‘X’是点位本身，而不是它的值：比较或计算永远读不到它的值。请用 X.ValueToReal() 读取它的值。 | 在比较或算术里光写的实时点位（`Variables.Level > 80`、`Variables.Level * 2`）。对 `==`、`!=`、`===`、`!==`，消息写的是 `.Value`；对报警，写的是 `.IsActive`。 |
| ‘X’是点位本身，而不是它的值：它永远不为空，所以 ?? 总是保留它。请用 X.Value 读取它的值。 | 在 `??` 左边光写的实时点位（`Variables.Level ?? 0`）。对报警，消息写的是 `.IsActive`。 |
| ‘X’是点位本身，而不是它的值：写进文本里也永远显示不出它的值。请用 X.ValueToString() 读取它的值。 | 在模板字符串的占位 `${…}` 里光写的实时点位。对报警，消息写的是 `.IsActive`。 |
| 报警条件不能引用其他报警。 | 条件里有一处 `Alarms.*` 引用。 |
| 条件的计算结果必须是布尔值，但它得到的是“x”。 | 条件的试算产出了一个存在、却转换不成布尔量的值。 |
| 表达式错误：实时点位不能在这里直接使用。请通过它的 .Value 成员来读（例如 Connector.Driver.Device.Tag.Value）。 | 在需要基本类型的地方用了一个实时对象（`Math.round(Variables.Level)`）。 |
| 表达式错误：`<engine message>` | 试算抛了异常：一个不认识的标识符，或者一个被挡住的成员被当作函数调用。 |

试算是拿当前的值来跑的，不管质量如何，所以一条提示反映的是这条表达式，不是一个不可用的输入；不可用的输入改在**引用的点位**卡片里显示。

## 脚本编辑器 {#the-script-editor}

页面上每一条表达式、每一个条件和每一段脚本都在同一张卡片里编辑：一行小标题（表达式、条件或脚本）、一个**浏览…**按钮、编辑器、一句提示和那条校验提示。编辑器是带 JavaScript 高亮的 CodeMirror，有撤销历史，也有地址记号的自动补全：光秃秃的一个记号补根名，然后补已经打出来的那条路径下面的子项，等这个记号点到一个点位之后，成员也在内（见[输入一个地址](logic-address-space#entering-an-address)）。这段文字去抖之后推给草稿并自动保存。表达式和条件那两个框至少 56 像素高，动作脚本那个框 160 像素。编辑器引擎起不来时，这个框降级成一个纯文本区域，仍然读得写得，只是没有高亮、没有补全、也没有历史；这时“浏览…”把挑中的地址接在末尾，而不是放到光标处。

**浏览…**打开一个对话框（“浏览：插入到表达式”），里面是整个地址空间成员那一层的树；选中一行按**插入**，或者双击它，就把它的地址放到光标处。每个编辑器各有自己的对话框，所以同一页面上的两个编辑器绝不会共用一个选中项。

## 表达式不做什么 {#what-expressions-do-not-do}

它们什么都不写，不调 Process 的运行命令，两次求值之间不留状态，不能定义在多条表达式之间共用的函数，也不会按需求值：扫描周期每 100 毫秒发布它们一次，而编辑器每秒预览草稿两次。
