@telorun/cel

中文文档
0.127.0发布于 2026/11/02MITTypeScriptES Module

Telo的通用表达式语言: 读取,类型检查和评估表达式,将其保存为树,将其写回。

中文使用文档
文档目录7 节
  1. 答案是什么?
  2. 正在检查
  3. 功能目录
  4. 评估
  5. 发射JavaScript
  6. 它故意不做什么
  7. 保留字

@ telorun/cel

通用表达式语言,作为一个库:读取表达式,将其保存为树,检查它,运行它, 把它编译成JavaScript,再写回来。

import { parseExpression, qualifiedCalls, rootReferences, serializeTree } from "@telorun/cel";

const expression = parseExpression("Billing.total(cart.items)", { namespaces: ["Billing"] });

expression.diagnostics; // [] — nothing it could not read
rootReferences(expression.root); // ["cart"] — what it reads from its environment
qualifiedCalls(expression.root); // one call: Billing.total, arity 1, with its range
serializeTree(expression.root); // "Billing.total(cart.items)"

答案是什么?

  • 整个表达语法,变成一棵树,其每个节点都带有 [start, end] 张成的空间 它涵盖的来源:int64, u-后缀uint64和double文字; single-、double-和 三引号字符串,原始字符串, b"…" 和 br"…" 字节,每个转义CEL定义; true, false, null; 列表和映射文字;操作符优先级表;成员访问和索引访问 在他们的平原和可选的形式 (., .?, [], [?]),反勾选之间的成员名称,以及 一个名字一个点打开(.y); 全局调用和接收方调用。
  • 合格的电话。 Alias.fn(x) 和 obj.method(x) 是相同的语法,所以唯一的 可以区分它们的是一组表示名称空间的名称。将该集合传递为 namespaces 和 每次调用这些名字都会变成 qcall 节点。一个表达式记录了它的集合 下已解决,因此消费者可以判断它是否正在查看为自己的站点解析的树。
  • 错误恢复。 读书从不摔。畸形的或半类型的表达式返回树 对于读取的最长前缀,加上一个具有阻止它的范围的诊断-一个成员 还没有名字(request.) 是一个带有空字段的select,这是一个点后的完成 的需求。
  • 往返。 serializeTree 将读取为相等表达式的任何树作为源写回 (treesEqual),括号是从优先顺序放置的,而不是从文本中记住的。
  • 两个关于树的问题: rootReferences -每个访问链的名字,不包括 多么好的理解啊! cel.bind 绑定和排除一个命名空间;和 qualifiedCalls ——每 按源顺序对命名空间调用。
  • 输入的限制。 100000个节点,250个嵌套级别,1000个列表元素,1000个映射条目和32 调用参数,每个都报告为普通诊断而不是异常,因此不友好的输入 行为像任何其他不可读的输入。 DEFAULT_PARSE_LIMITS 是那些数字; 通过 limits 来 改变他们。

正在检查

import { CelEnvironment } from "@telorun/cel";

const environment = new CelEnvironment({ enableOptionalTypes: true })
  .registerVariable("request", { schema: requestSchema })   // typed to full depth
  .registerFunction("sha256(string): string", { hostBacked: true, throws: ["ERR_DIGEST_FAILED"] })
  .registerNamespace("Billing", ["total(int, int): int"]);

const result = environment.check("request.query.limti");
result.valid;        // false
result.diagnostics;  // [{ code: "CEL_UNKNOWN_FIELD", message: "…(declared: limit, tags)", range: [14, 19] }]
result.calls;        // which signature each call resolved to, with its metadata
environment.check("request.query.limit").typeName; // "int"
  • 注册没有特权。 CEL自己的库寄存器也是如此 registerFunction, 调度键是调用的名称,形式和参数类型-而不是其返回类型。所以 registerFunction("duration(string): Money") 替换 图书馆的 duration(string), removeFunctionsNamed("duration") 呼叫它 CEL_UNKNOWN_FUNCTION. clone() 继承 然后发散。
  • JSON模式是本机输入,完全深入 嵌套对象、元素类型、联合保存为 工会, allOf 相交,然后是参考。平面场地图 ({ fields: { columns: "map" } }) 仍然被接受,因为主机的输入必须和它要替换的东西一样浅。
  • 没有一个模式说落入 dyn 在沉默中。 一、 #/$defs/… 引用被解析为 节点所属的文档;离开文档的一个由主机自己的解析器应答, 返回已注册的类型名称 或者要在该节点的位置读取的文档。一个节点 引擎对is没有规则 报道 通过JSON指针而不是类型 dyn 静静地- environment.schemaReports(),因此知道模式在哪里编写的消费者可以锚定一个 诊断在那条线上。在下降时重新进入的引用是一个递归模式:一个深思熟虑的模式 dyn,这样宣布。
  • 命名类型不是它的基。 registerType({ name: "Money", base: "int" }) 给出一个自己的类型 运算符、比较、转换、成员和不变类型参数; 普通 int 被拒绝了 一个 Money 槽, Holder<string> 哪里 Holder<int> 被通缉是 CEL_TYPE_ARGUMENT_MISMATCH.
  • 每个判决都有一个范围,由检查员决定 - - - - - - CEL_SYNTAX_ERROR, CEL_TYPE_ERROR, CEL_UNKNOWN_IDENTIFIER, CEL_UNKNOWN_FIELD, CEL_UNKNOWN_FUNCTION, CEL_WRONG_CALL_FORM, CEL_TYPE_ARGUMENT_MISMATCH, CEL_NULLABLE_ACCESS, CEL_INVALID_ARGUMENT, FUNCTION_UNRESOLVED / FUNCTION_ARITY_MISMATCH / FUNCTION_ARGUMENT_MISMATCH 用于名称空间调用。 一个修正,当一个被提供,是整个修正的来源。
  • 名称空间调用仅根据您声明的内容进行判断。 registerNamespace("Billing", […], { open: true }) 使未声明的名称成为类型 dyn 什么也没报告-它仍然是 列出的 在 result.calls所以你要用自己的词汇和自己的判断来解决这个问题。和一个 声明写 { name: "total", returns: "double" } 而不是 { signature } 扣留 形参列表:结果是类型化的,实参的数量和类型由你来判断。都是给主人的 其分辨率或其签名语法比CEL的更丰富; 两者都不能半声明,因为 没有办法提供参数并要求忽略它们。
  • 三名警卫清除了可能为空的内容 没有其他人: ?:, &&, ||.
  • 可选库输入整个 哪里 enableOptionalTypes 是: .?, [?], [?x], {?k: v}, optional.of, none, ofNonZeroValue, hasValue, value, or, orValue, optMap, optFlatMap,平等优于期权, type() 和类型名 optional_type.
  • 名称不是标识符的成员将在反号之间读取 - - - - - - request.headers.`` 内容类型``, typed against the schema's属性exactly as a plain member is, so a typo in a dashed or dotted key is stillCEL_UNKNOWN_FIELD '。仅其中成员为 阅读: 任何其他地方反勾是一个语法错误。
  • 带点的声明是一个名称。 声明 a.b.c,或者 a.b或两者兼而有之; a.b.c 读取的变量 声明的名称和地图的条目,其中只有 a.b Is -最长的前缀胜出。
  • .y 针对环境的解决方案,永远不要反对一个名字的表达式绑定:它是唯一的 外部名称的拼写,其中一个理解变量共享它。
  • convertsToString(type) 回答该类型的值是否可以由 环境本身 string().

标准库是data - src/signatures/standard-library.json,描述于 文档/ signature-data.md. 它是 CEL's 图书馆: CEL本身的声明 不定义进位 "spec": false 并说明为什么它在这里,等价物在哪里以及如何存在 这个成员与它不同-十九个这样做。

功能目录

在CEL自己的图书馆旁边还有第二个-Telo的方言,67个功能超过86个签名,也 数据(src/signatures/function-catalog.json):

import { CelEnvironment, functionCatalog, registerFunctionCatalog } from "@telorun/cel";

const environment = new CelEnvironment({ enableOptionalTypes: true });
registerFunctionCatalog(environment, {
  handlers: { sha256: hashHex, json: writeJson, joinPath: (base, rest) => join(base, rest) },
});

environment.evaluate("formatDuration(510, 480)"); // "1d 30m"
functionCatalog();                                // the one listing surface: name, signature, category, summary, flags
  • 它通过公共表面注册 - - - - - - registerFunction,每个签名一次-所以它可以 删除、按函数替换函数或按名称删除。默认环境没有它。
  • 主机有九种功能: sha256, md5, sha1, sha512, hmac, base64Encode, base64Decode, json 和 joinPath,每个需要这个包裹可能无法到达的设施。左一 Out仍然注册,因此只检查的消费者仍然对调用进行类型检查;评估它的答案 unbound_function 命名函数。
  • 拒绝是目录自己的话, <function>: <what is wrong> -从来没有图书馆的措辞。 无效模式以RE2自己的解析错误类型之一结束,之后没有任何内容,并且 parseJson 单词自己的偏移量 (parseJson: invalid JSON at offset 3)。
  • 在检查时拒绝文字参数,不仅仅是当表达式运行时: fixed(1.0, 11) 是 CEL_INVALID_ARGUMENT 命名调用,因为签名限制类型和十进制计数 范围是一个值。十二个函数带有这样的保护,每个函数都运行评估的代码 运行。
  • 两个家庭通过申报到达接待设施 -时钟 (now, nowIso, today, nowMillis, nowSeconds)和UUID函数背后的随机源,这是什么 deterministic: false 意思是。两者都和节点一样存在于浏览器中。

评估

const program = environment.compile("request.query.limit != null ? int(request.query.limit) : 25");

program.evaluate({ request: { query: { limit: "50" } } }); // 50n
program.evaluate({ request: { query: {} } });              // 25n
  • 每个运算符和函数的行为都符合cel-spec的要求。 int 是一个 bigint 溢出是 错误而不是包装; uint 和 double 是不同的类型;除法和模数为0 两个不同的错误;字节和字符串是不同的,都是有序的;时间戳是 纳秒-精确,它的getter接受一个区域(一个IANA名称或一个固定的 HH:MM 偏移量); matches 是 RE2,所以主机自己的引擎会接受而RE2不会拒绝的模式。
  • 持续时间是其总纳秒的有符号64位范围 - - - - - - -9223372036.854775808s … 9223372036.854775807s大约是±292年,这是cell -spec自己的限制。 它在构建持续时间的任何地方进行检查: 转换,持续时间 + 和 -, timestamp - timestamp的持续时间操作数 timestamp ± duration。主机自己的持续时间格式可以是 更广泛的, google.protobuf.Duration 是 ± 10,000年-而CEL是它的一个子范围,所以持续时间a 运输所能承载的并不总是一个CEL所能建造的。
  • 八个字符串成员不是CEL的,并在各自的声明中如此表示("spec": false 与一个 原因): indexOf, lastIndexOf 和 substring 指数 UTF-16代码单元 而不是通过代码 点, lowerAscii 和 upperAscii 应用主机语言的大小写而不是ASCII 单独的信件,和 trim, split 和 join 类型中的等效字符串扩展不同 他们的理由名称。 size, startsWith, endsWith, contains, matches 和串联是 CEL自己的.
  • 错误是参与短路的一个值。 false && <missing key> 是 false 和 true || <missing key> 是 true,从任何一方; 一个错误,幸存到一个 评估成为一个抛出 CelEvaluationError 携带其中一个代码 CEL_EVALUATION_CODES (no_such_key, numeric_overflow, division_by_zero, modulo_by_zero, index_out_of_range, invalid_regular_expression, invalid_conversion, optional_value_missing, async_value_unsupported,…)以及它所涉及的文本范围。
  • 评估是同步的,而必须等待的价值则被拒绝 它来的每一扇门 通过:激活读取,注册实现的结果,以任何形式读取的成员,以及 元素进入推导体,值 cel.bind 绑定可选对象的保存值和列表 会员资格。每个答案 async_value_unsupported,从来没有一个值传递,从来没有一个答案 决定一个没有触及的值: 元素在哪里被拒绝 绑定,而那个拒绝就是 终端- - - - - - xs.all(e, false) 和 xs.exists(e, true) 在一张单子上写着承诺,而不是拒绝 回答一个可读的元素,而一个普通的 no_such_key 仍然短路为 之前。一个容器是故意不走的,所以 size(xs) 和 xs + [3] 随身携带它和每一个 读出元素的方法被拒绝。
  • 一个值通过字符串类型的键表示它是什么 下 Symbol.for("telo.cel.value"),从来没有。 构造函数,因此引擎的两个副本同意其中任何一个生成的值。 null,布尔值; 一根弦,一根 number (双) bigint (int), a Uint8Array (bytes),数组(list)和plain 对象 (字符串键映射) 不携带键; uint,时间戳,持续时间,类型值,可选, 带有键入键和错误键的映射。带有字符串键的普通物体是 数据,因为它必须。
  • 每个读取的成员都经过一次查找 在值本身的项上 a.b, a['b'], a[expr], .?, [?] 而宏的字段读起来是一样的——所以无论如何计算,没有一个键能达到a 原型,方法或函数属性。CEL地图是无原型构建的,因此 __proto__, constructor 和 prototype 作为数据的往返。
  • 否 eval,没有 new Function,没有磁盘。 compile 在运行时上构建闭包树 图书馆,这是每个操作员只住一次的地方。进程内缓存-编译的表达式 每个环境、编译模式、调用站点的解析重载都是有边界的,每个都有一个 申报能力。
  • evaluateToValue 对于携带错误的调用方,回答错误值而不是抛出 本身。使用名称空间的调用通过 namespaceFunction,每次评估提供;一个 没有什么绑定是 unbound_function.

发射JavaScript

相同的表达式编译为 JavaScript源,作为一组它们的一个模块:

const module = environment.emit(["a + b * 2", "xs.map(e, e * 2)"]);

module.key;    // 64 hexadecimal characters: what a host names its stored copy by
module.text;   // the module's source, which imports nothing
module.header; // { format, engine, environment, key, body } — the header line's own fields

// A host loads it however it loads a module, then:
const programs = programsFromEmittedModule(loaded, module, environment.emitterRuntime());
programs[0].evaluate({ a: 3n, b: 4n }); // 11n
  • 两个后端答案完全相同 -相同的值,或相同的错误代码和 相同的范围。这是布线而不是测试的属性: 每个操作员,理解, 成员读取、名称读取、bool操作数和调用站点调度是两个后端调用的一个函数,因此 两者编译的唯一不同之处在于控制如何从一个调用传递到下一个调用。
  • 运行时是工厂的参数,从来没有导入。 模块的默认导出采用 运行时支持库并回答每个表达式一个同步函数,并且文本名称为no 说明符-它从 data: URL,从装载在主机下的任意位置的缓存目录 它的解析器不是Node的。导入包的模块也会接受另一个包的运行时 版本默默地,关键看不到。
  • 关键在于环境,而不仅仅是源头。 它散列发射器的格式生成, 引擎版本、环境摘要和表达式源的有序列表。文摘结束了 环境的 解决清单 -每个幸存的函数签名,每个命名类型,每个 变量、每个名称空间、每个选项——所以两个环境是由不同的注册顺序构建的 是一个键,替换或删除标准功能的主机是另一个键。
  • 报头在运行任何内容之前进行验证,并且它是五个字段而不是三个。 format, engine 和 environment 是来源,并且它们对于每个模块一个引擎都是字节相同的 针对一个环境写入-因此 key (模块自己的标识) 和 body 的摘要 标题行之后的一切) 是什么抓住了一个回答错误查找的商店,一个文本 在其标题之后被截断,并在编写后进行编辑的文本。 body 涵盖了 integrity 导出,所以不能住在它: 标题行携带所有五个,导出携带四个a加载 对象可以持有。每一个不匹配都被存储 文本 显示的是一个重新编译,将自己命名为 refused; 一个加载 模块 声明什么都没有,声明另一个键或回答错误的数字 的函数被拒绝与 CelEngineError (emitted_module_rejected)。
  • 一个商店接缝,没有装载机。 environment.emittedModule(sources, store) 读写模块 通过主机提供的接口按键进行文本; 引擎不接触文件系统,并且命中解析 什么都没有。商店的写作应该是这样的 原子的 -写在别处,然后重命名-因为几个主机 共享一个缓存根; 这就是如何避免半写入的条目,并且 body 消化就是人的本质 如果不是,则检测到。
  • 排放是确定的: 针对相同环境的相同顺序的相同表达式是 相同的字节。
  • 它是 更快,没有不同的行为 -只有一件事: 闭包称之为成本。 两个后端都在下面支付相同的共享运行时,因此表达式的成本是一个点链 搜索和转换是一个洗,而一个调用许多小操作的收益。

它故意不做什么

  • 在读取时不展开任何内容。 has(x), xs.map(i, i) 和 cel.bind(x, 1, x) 是普通的 调用树中的节点; 检查器降低它们,因此源保持可从树写入。
  • 它不懂主人的词汇。 没有类型名,没有注释,没有清单字:一个主机寄存器 它自己的通过 registerType 还有一个模式解析器。上面的函数编目是一组 CEL自己的类型上的函数-没有签名命名主机类型-这就是方言。
  • 它在求值时从不进行类型检查。 检查员决定每一个判决和一个消费者 要求他们; 编译只拒绝根本无法编译的内容-未阅读的源代码 整体。
  • 语言没有到达主机设施。 没有文件系统,没有时钟,没有网络,没有内置的Node 在浏览器中不变地运行,编目也是如此:它的时钟和UUID函数读取工具 每个主机都有,它的九个主机支持的功能本身根本无法实现。

保留字

CEL保留了21个单词,其中没有一个可以读作名称: as break const continue else false for function if import in let loop namespace null package return true var void while。17人被拒绝 彻底的; true, false 和 null 是它们所处位置的文字,并且 in 是成员运算符, 这就是为什么一个名字位置永远无法到达他们。

保留字仍然是合法的 成员 的名字, {'let': 1}.let, a.while(), a.in, a.true -哪个 是CEL自己的规则:成员在表达式的作用域中命名一个值的条目,而不是一个名称。

主人没有任何保留: __proto__, prototype 和 constructor 是普通的名字。 将主机自己的属性保留在CEL值之外是成员读取的工作,而不是单词列表的-a 成员键可以计算(a[request.query.k]),没有名单可以判断。

cel 和 optional 是普通标识符,但永远不能注册为名称空间: 标准宏写在它们上面。

版本与发布记录

@telorun/cel 共发布 27 个版本,首次发布于 2026/10/03;过去 12 个月发布了 27 个版本。

版本发布日期距今
0.127.0latest2026/11/02今天
0.126.02026/11/011 天前
0.125.12026/10/313 天前
0.125.02026/10/304 天前
0.124.02026/10/295 天前
0.123.12026/10/268 天前

关于 @telorun/cel 的常见问题

@telorun/cel 是什么?

Telo的通用表达式语言: 读取,类型检查和评估表达式,将其保存为树,将其写回。

如何安装 @telorun/cel?

在项目目录运行 npm install @telorun/cel,或使用 pnpm add / yarn add / bun add。

@telorun/cel 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@telorun/cel 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@telorun/cel 可以商用吗?

@telorun/cel 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@telorun/cel 还在维护吗?

最新版本 0.127.0 发布于 2026/11/02(今天);至今共发布 27 个版本,其中过去 12 个月发布了 27 个;首次发布于 2026/10/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。