07-logic.yaml作为领域语言-DSL设计
07 · logic.yaml 作为领域语言:expr 语法、setter 语义与编译策略前 6 篇把平台是什么、“数据怎么流”、工具链怎么护讲完了。这一篇换一个角度——把logic.yaml当一门领域语言(DSL)看:它的 expr 语法长什么样、7 个 setter 的签名怎么设计、ExprTk 编译踩了哪些坑、preprocessQuotes为什么必须存在。看完你会明白:这门 DSL 不是配置文件里塞表达式,而是标定工程师跟平台 c 之间的一份契约。一条 rule 长什么样config/logic.yaml里的一条规则:-id:bat_overvolt_alarmdesc:电池过压 420V → 触发 high 报警 灯闪烁can_signals:[bat_volt]expr:|if (isovertime(bat_volt) 0 and bat_volt 420) { setwarnon(bat_overvolt); setlightshine(bat_warn_light); } else { setwarnoff(bat_overvolt); }四个字段,职责分明:字段谁写干什么id标定工程师规则唯一标识,日志/调试用desc标定工程师自然语言描述,不参与执行can_signals标定工程师声明这条 rule 依赖哪些 CAN 信号expr标定工程师业务逻辑本体注意can_signals不是从哪读变量——expr里可以引用所有can_ids.yaml声明的信号。can_signals的作用是声明依赖,让引擎知道这条 rule 跟哪些信号相关(日志、超时判定、未来可能的增量 tick 优化都靠它)。expr 的语法:ExprTk 子集expr不是自由 C,是 ExprTk 表达式引擎支持的子集。支持的东西:变量 bat_volt, vehicle_speed, motor_temp ... ← can_ids.yaml 字段名 字面量 420, 120, 0, 1.0 ← 数字 字符串 bat_overvolt, -- ← 单引号(预处理后) 算术 , -, *, / 比较 , , , , , ! 逻辑 and, or, not 控制流 if / else if / else 函数调用 setwarnon(...), isovertime(...) 等 7 个 setter不支持的东西(标定工程师常踩的坑):没有for/while循环——仪表逻辑不需要,也不该有没有数组 / 对象——信号是标量,显示值是标量没有自定义函数——setter 是平台预注册的,业务不能自己加没有switch——用if / else if链代替这些限制是有意的。DSL 的价值不在于什么都能写,而在于写不出错的东西。一个不能写循环、不能定义变量的语言,标定工程师不可能写出死循环或内存泄漏。7 个 setter:DSL 的全部副作用logic.yaml的 expr 是纯读 副作用模型:读信号变量,通过 setter 写状态。7 个 setter 覆盖了仪表 HMI 的全部输出:显示值setdisplayvalue(name, type, value) name: 显示 id 字符串,如 bat_volt_disp type: int / float / string value: 数字或字符串(union 签名 SST | SSS)这是唯一有多态参数的 setter。type告诉 QML 端怎么格式化,value是实际值。当typestring时 value 是字符串字面量(如--表示超时占位);当typefloat时 value 是信号变量。超时检测isovertime(name) → 1.0 | 0.0 name: CAN 信号名 返回 1.0 超时(5 × cycle_ms 没更新), 0.0 正常这不是 setter,是查询函数。它读m_lastUpdateMs[name]跟当前时间比较,超时阈值 cycle_ms × 5(从can_ids.yaml的period_ms来)。为什么阈值是5 × cycle_ms而不是固定值?因为不同信号周期差 10 倍(VCPU 50ms, HYBRID_EV_RANGE 1000ms),固定阈值要么误报(短周期信号偶尔丢帧)、要么漏报(长周期信号超时了还没发现)。5 ×是经验值——5 个周期没到基本可以确定总线断了。报警setwarnon(name) ← 触发报警 setwarnoff(name) ← 消除报警 name: 报警 id,必须在 warn.yaml 里声明过指示灯setlighton(name) ← 常亮 setlightshine(name) ← 闪烁 setlightoff(name) ← 灭 name: 灯 id,必须在 lights.yaml 里声明过7 个 setter 的命名刻意对称:on/off一对,light多一个shine(闪烁是仪表高频需求)。签名设计:SSS|SST union 的来由setdisplayvalue是 7 个 setter 里唯一需要多态的——第三个参数可能是字符串也可能是数字。ExprTk 的igeneric_function用签名串声明参数类型:S stringT scalar(double)V vector单签名直接写SSS或SST。但setdisplayvalue要同时支持两种——用SSS|SSTunion 签名。union 签名的代价:ExprTk 走multimode_genfunction_node路径,调的是2 参 overloadoperator()(const std::size_t, parameter_list_t),而不是单参operator()(parameter_list_t)。两个 overload 必须同时 override,否则 2 参版本走基类empty_body返回 NaN。这是 ExprTk 的隐藏坑,写在SetterBundle的注释里:// 注意: union 签名 (|) 让 ExprTk 走 multimode_genfunction_node,// 调用的是 operator()(const std::size_t, parameter_list_t) (2 参版本),// 不是单参 operator()(parameter_list_t).// 所以必须同时 override 两个 overload, 否则 2 参版本走基类 default empty_body, 返回 NaN.其他 6 个 setter 都是纯S签名,只 override 单参版本就行。preprocessQuotes:为什么必须存在yaml 里写字符串自然用双引号:expr:|if (isovertime(bat_volt) 0 and bat_volt 420) { setwarnon(bat_overvolt); }但 ExprTk不认双引号——它用单引号...当字符串字面量。如果直接把 yaml 的 expr 喂给 ExprTk,bat_volt会被当成未知标识符,编译报错。preprocessQuotes做的事很简单:把 expr 里所有...替换成...。staticstd::stringpreprocessQuotes(conststd::stringsrc){std::string out;boolin_strfalse;for(size_t i0;isrc.size();i){charcsrc[i];if(!in_str){if(c){in_strtrue;out\;}else{outc;}}else{if(c){in_strfalse;out\;}else{outc;}}}returnout;}为什么不让标定工程师直接写单引号?因为 yaml 的|块里双引号是最自然的写法,强制写单引号会在 yaml 高亮、编辑器补全、复制粘贴时处处别扭。DSL 的用户体验也是设计——让写的人舒服,让机器去适配。编译策略:两步法compileRule用了一个不太常见的两步编译:// 第 1 步: 1 参 overload 探测错误exprtk::parserdoubleparser;exprtk::expressiondoubleprobe;probe.register_symbol_table(*rule.syms);if(!parser.compile(rule.expr_source,probe)){std::fprintf(stderr,[LogicEngine] compile FAIL in %s: %s\n,rule.id.c_str(),parser.error().c_str());returnfalse;}// 第 2 步: 2 参 overload 拿 expressionautocompiledparser.compile(rule.expr_source,*rule.syms);rule.exprstd::make_sharedexprtk::expressiondouble(std::move(compiled));为什么不走一步?因为 ExprTk 的 2 参 overloadcompile(string, symbol_table)在某些错误路径上parser.error()返回No Error——错误诊断不可靠。但 1 参 overloadcompile(string, expression)的错误报告是准的。两步法的代价是同一 expr 编译两次。但 rule 数量少(几十条),且只在启动时编译一次,运行时只调expr-value()——编译开销可以忽略。注释里还有一条关键约束:rule.syms必须在compileRule之前就绑到rule上(emplace_back后地址稳定),因为add_variable/add_function注册的指针会被编译产物引用。如果 vector 扩容导致rule搬移,symbol_table 里的指针就悬空了。变量绑定:从 can_ids.yaml 到 expr 槽compileRule里有一段看似冗余的变量绑定:autobind[](conststd::stringname){if(std::find(rule.var_names.begin(),rule.var_names.end(),name)!rule.var_names.end())return;rule.var_names.push_back(name);};for(constautos:rule.can_signals)bind(s);// 先绑声明的依赖for(constauto[n,_]:m_signals)bind(n);// 再绑全部信号为什么不只绑can_signals里声明的?因为expr里可能引用未声明的信号——can_signals是我声明我依赖这些,但 expr 里写bat_curr也是合法的(只是日志/超时判定不跟踪它)。全量绑定保证 expr 里写任何信号名都能编译通过。var_slots是vectordouble,每个 slot 对应一个信号变量。tick时:for(constauto[name,val]:ctx){for(autorule:m_rules){autoitstd::find(rule.var_names.begin(),rule.var_names.end(),name);if(itrule.var_names.end())continue;rule.var_slots[it-rule.var_names.begin()]val;}}ctx 里的每个信号值被写进对应 rule 的 slot——ExprTk 编译时拿的是 slot 的地址,之后每次expr-value()读的是 slot 的当前值。这就是为什么var_slots必须在emplace_back之后稳定:地址不能变。DSL 的边界:什么该写、什么不该写回到开篇说的契约。logic.yaml作为 DSL,边界很清晰:该写的:信号阈值判断(bat_volt 420)报警触发/消除(setwarnon/setwarnoff)显示值格式化(setdisplayvalue)指示灯控制(setlighton/setlightshine/setlightoff)超时降级(isovertime→ 显示--)不该写的:字节解析(那是 DBC /can_ids.yaml的活)UI 布局 / 颜色 / 字号(那是warn.yaml/lights.yaml/ QML 的活)优先级排序(那是 QtBinder / QML 的活)跨帧关联计算(如果复杂到需要状态机,应该升级 c setter,不是塞 expr)最后一条尤其重要:如果标定工程师发现自己在 expr 里写if ... else if ... else if ...超过 5 层,那说明这个逻辑不该用 DSL 表达——应该让平台 c 加一个新 setter,把复杂度封装掉。DSL 的价值是让 90% 的变更留在 yaml 里,但剩下 10% 的复杂度应该回流到平台,而不是在 DSL 里硬撑。一句话总结logic.yaml是一门受限的 DSL:ExprTk 子集语法 7 个 setter 覆盖全部副作用 preprocessQuotes适配 yaml 双引号 两步编译拿可靠错误诊断。它的价值不是什么都能写,而是标定工程师写不出错的东西。接下来08 · ExprTk 实战坑:string_view 生命周期与签名声明——igeneric_function的type_store::string_view为什么不能reinterpret_caststd::string*,union 签名的 multimode 分派细节09 · 测试哲学:怎么用测试护住平台化承诺——test_decode_table/test_exprtk_string/test_dbc_to_yaml三层测试各护什么

相关新闻

FSV9520|13.56MHz 国产 NFC 读写芯片,Pin 对 Pin 替代方案

FSV9520|13.56MHz 国产 NFC 读写芯片,Pin 对 Pin 替代方案

FSV9520 是福芯微(Forsinve)推出的高集成度 13.56MHz 非接触读写卡芯片,面向 ISO/IEC 14443 TypeA、MIFARE 协议应用,可直接对标替代同类进口读卡芯片,采用QFN-32(55mm)小尺寸封装。芯片集成完整…

2026/9/21 18:10:06 阅读更多 →
国内智能驾驶供应商怎么选?2026年国内算法自研型Tier 1全景扫描

国内智能驾驶供应商怎么选?2026年国内算法自研型Tier 1全景扫描

第一部分:宏观引言——高阶智驾量产,从“能不能做”到“能不能做好”2026年的智能驾驶行业,正在经历一场从“技术竞赛”到“量产竞赛”的深刻切换。过去两年,行业热议的是“谁能跑通端到端”、“谁的城市NOA场景更多”&#xff1b…

2026/9/23 14:48:58 阅读更多 →
Claude Code 升级 Rust 版 Bun:10% 启动速度提升的实践指南

Claude Code 升级 Rust 版 Bun:10% 启动速度提升的实践指南

这类开发工具升级最值得关注的不是版本号变化,而是实际落地时启动速度、资源占用和稳定性到底有没有提升。Claude Code 从原有方案切换到 Rust 版 Bun 后,官方称启动速度提升 10%,这个数字看起来不大,但对需要频繁重启或批量调用 …

2026/9/23 8:22:54 阅读更多 →

最新新闻

Edge隐藏多线程下载开关:开启后大文件下载速度翻倍

Edge隐藏多线程下载开关:开启后大文件下载速度翻倍

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:50:43 阅读更多 →
STM32中断系统实战:NVIC优先级、EXTI与HAL库回调全解析

STM32中断系统实战:NVIC优先级、EXTI与HAL库回调全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:50:43 阅读更多 →
C语言开根号全解析:从sqrt到pow的实战指南

C语言开根号全解析:从sqrt到pow的实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:50:42 阅读更多 →
Arduino WS2812B彩灯DIY:从接线到彩虹灯环完整实战

Arduino WS2812B彩灯DIY:从接线到彩虹灯环完整实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:50:42 阅读更多 →
pygame俄罗斯方块实战:逻辑与渲染分离的完整开发指南

pygame俄罗斯方块实战:逻辑与渲染分离的完整开发指南

简介:基于Python pygame的经典俄罗斯方块小游戏开发资源包,面向计算机相关专业高校学生与Python游戏开发初学者,可完成课程实训、课程设计或期末作业,也可作为毕设或项目立项演示。资源共6个文件,包含tetris.py主程序、…

2026/9/25 4:50:42 阅读更多 →
OpenClaw 部署安全指南:从会话锁到提示注入的完整防护

OpenClaw 部署安全指南:从会话锁到提示注入的完整防护

本来我想先夸一夸 OpenClaw 这玩意儿本地跑起来有多爽,但这篇文章还是直接从风险讲起吧。你把它部署好、接到 Teams 或飞书上、再配上千问之类的模型,它就从一个普通程序变成了一个常驻代理:能读文件、能发消息、能调 API、还能替你执行命令。…

2026/9/25 4:49:42 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →