chronaxis.core.expression
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""把表达式文本和信号声明转换成经过检查的表达式树。23例如,已绑定的voltage和current可用于voltage * current;编译器检查4名称、类型、单位和依赖,输出结果类型、单位、实际信号依赖及绑定身份。5表达式使用受限语法(DSL):仅接受明确允许的语法树节点和注册函数,6不执行Python代码,不访问任意对象,也不读取日志或加载DBC。78compile_expression接收真实SignalBindings、关联实际解码证据的DecodedBindings,9或独立消息仓MessageBindings,输出供解释器使用的CompiledExpression;实际10证据由执行层完整核实后提供。validate_expression接收DeclarationBindings或11MessageDeclarations,只产生不可执行的StaticExpression。12两者均检查完整派生声明与循环依赖;数字常量按运算上下文检查单位和定宽。13整数比较的常量边界保留精确Python整数,不因超过定宽范围而先舍入为浮点数。1415标量与数组解释器共享已检查的树,数值计算各自独立。本模块只依赖标准库16及核心声明模型;编译状态每次新建,不修改输入,也没有跨计划的执行缓存。17"""1819# AST只用于解析与规范身份,不用动态执行。20import ast21# 冻结节点和纯常量上下文重建,没有全局执行绑定缓存。22from dataclasses import dataclass, field, replace23# SHA关联实际输入绑定和编译语义版本。24from hashlib import sha25625# 数字字面量必须有限,不允许非有限容差。26from math import isfinite27# 原始派生声明只读保存,不修改调用侧映射。28from types import MappingProxyType2930# 使用当前已校验绑定、类型及静态数值协议。31from .binding import SignalBindings, _identity32from .lookup import PreparedLookup33# 静态配置声明独立于真实输入;只由专用validate入口接收。34from .declarations import DeclarationBindings35# 已解码运行绑定不保存伪造memory行,仍与不可执行声明类型分开。36from .decoded_binding import DecodedBindings37# 当前消息的参数/运行域有独立typed入口,不伪装普通历史或TTL绑定。38from .message_binding import MessageBindings, MessageDeclarations39# 定宽/运算声明可共享,编译不调用被测数组或参考数据运算。40from .numeric import INTEGER_BOUNDS, result_dtype41# 类型与质量模型显式,不用字符串冒充dtype。42from .types import SignalDType43# 已知单位表仅检查当前显式标签,不猜物理工程含义。44from .unit_registry import resolve_unit45# 文本与标识符保留安全名称和大小写。46from .validation import require_identifier, require_text4748# 运算符映射固定,禁止幂/取模/位运算等未支持语法。49BINOPS = MappingProxyType({ast.Add: 'add', ast.Sub: 'sub', ast.Mult: 'mul', ast.Div: 'div'})50# 首版比较白名单,链式比较另展开为三值and。51CMP = MappingProxyType({ast.Eq: 'eq', ast.NotEq: 'ne', ast.Lt: 'lt', ast.LtE: 'le', ast.Gt: 'gt', ast.GtE: 'ge'})52# 常量在左侧时反向精确整数比较,不能转float。53REVERSE_CMP = MappingProxyType({'eq': 'eq', 'ne': 'ne', 'lt': 'gt', 'le': 'ge', 'gt': 'lt', 'ge': 'le'})545556@dataclass(frozen=True)57class DerivedDefinition:58 """声明一个由现有信号计算得到的新名称、表达式和结果单位。5960 name与原信号共用名称空间,需在bindings.derived_names中预先登记。61 构造本类只检查文本和名称;表达式编译时才检查单位、依赖和循环引用。62 unit省略时由表达式支持的单位关系推导;显式标签不授权猜测物理量纲,未知单位组合仍保留unverified状态,63 表示关系未验证。实例不可重新赋值,不保存信号值或运行中的状态。64 """6566 name: str # 已在bindings.derived_names登记的简单别名。67 expr: str # 白名单DSL文本,不是任意Python。68 unit: str | None = None # 缺省使用表达式已支持的单位关系。6970 def __post_init__(self) -> None:71 """验证派生声明结构,不解析或加载来源。7273 参数: 无;读取name/expr/unit。74 返回: None,不改用户声明。75 异常: TypeError/ValueError为标识符/空文本问题。76 """77 # 派生与signal名称采用同一规则。78 require_identifier(self.name, 'derived.name')79 # 不允许空表达式占位。80 require_text(self.expr, 'derived.expr')81 # 未知标签可以显式声明,但不能假称单位已验证。82 if self.unit is not None:83 require_text(self.unit, 'derived.unit')848586@dataclass(frozen=True)87class ExprNode:88 """保存表达式编译器已检查的一个内部节点。8990 operator给出注册操作,args保存不可变子节点,dtype和unit说明结果类型与单位。91 aliases记录展开后的原信号依赖,运行时只按简单别名取值,不访问属性路径。92 constant说明整棵子树只由字面量组成;编译器可据此赋予常量上下文单位和93 整数类型,不能借此改变信号本身的类型。unit_status保留单位是否已验证。94 节点不保存样本值缓存,解释器的计算结果仅在当前一次求值中保存。95 """9697 operator: str # const/name/add等固定执行分派。98 dtype: SignalDType # 静态目标类型。99 unit: str # 结果单位标签。100 unit_status: str # verified或unverified,不能借未知标签过预设。101 args: tuple = () # 子节点,isclose含四节点。102 value: int | float | bool | str | tuple[int | float, int | float] | PreparedLookup | None = None # 常量或编译参数。103 name: str | None = None # name仅简单已绑定别名。104 labels: tuple[str, ...] = () # enum逻辑域。105 aliases: tuple[str, ...] = () # 原信号依赖,不包含派生占位。106 constant: bool = False # 纯常量静态上下文标志。107 allow_float_literal: bool = False # 由本编译器证明的浮点字面量路径。108 conversion_promoted: bool = field(default=False, repr=False, compare=False) # 编译时类型提升依据,不影响最终数值节点等价。109 unit_explicit: bool = field(default=False, repr=False, compare=False) # 编译时目标尺度;运行身份由实际转换节点确定。110 typed_constant: bool = field(default=False, repr=False, compare=False) # 带单位纯常量只适配dtype,不继承别的单位。111 host_function: object = field(default=None, repr=False, compare=False) # 仅宿主逐点调用持有已验证的不可变FunctionSpec。112113114@dataclass(frozen=True)115class CompiledExpression:116 """保存可供求值器使用的表达式树及其真实输入绑定。117118 root已检查语法、类型与单位;dependencies只包含展开后的原信号别名。119 identity关联绑定、派生定义、规范语法树和编译版本。改变会话、字段映射、120 有效期或转换声明都会影响身份,解释器要求调用时bindings与编译时一致。121 实例不可重新赋值,没有全局执行缓存;创建本对象并不执行数值计算。122 """123124 text: str # 原表达式仅供定位,不动态执行。125 bindings: SignalBindings | DecodedBindings | MessageBindings # 各自真实输入域的运行身份。126 root: ExprNode # 实际白名单typed树。127 dependencies: tuple[str, ...] # 实际需要的已解码信号列。128 normalized_ast: str # 不含位置的AST规范结构。129 identity: str # 完整执行身份SHA。130131132@dataclass(frozen=True)133class StaticExpression:134 """保存只经声明校验的DSL树,不能交给数值解释器执行。135136 identity证明配置/语法/转换声明,尚未证明输入内容、字段和观测。137 与CompiledExpression类型分开,运行时必须核实来源后重新绑定。138 """139140 text: str # 原DSL,仅用于定位。141 bindings: DeclarationBindings | MessageDeclarations # 声明身份,没有实际读入证明。142 root: ExprNode # 白名单类型/单位已经校验。143 dependencies: tuple[str, ...] # 展开后的原信号依赖。144 normalized_ast: str # 规范语法,仅供静态身份。145 identity: str # 独立静态版本前缀的SHA。146147148def contains_host_call(root):149 """判定子树是否含宿主运行回调,以隔离静态常量参数检查。150151 参数: root为冻结表达式节点。返回: bool。异常: 内部不合法树传播属性错误。152 """153 seen, pending = set(), [root]154 while pending:155 node = pending.pop()156 if node.operator == 'host_function':157 return True158 if id(node) not in seen:159 seen.add(id(node))160 pending.extend(node.args)161 return False162163164def _node(operator, dtype, unit, args=(), **kwargs) -> ExprNode:165 """从已验证子节点汇总局部依赖及单位状态。166167 参数: operator/dtype/unit及args;kwargs为已验证内部字段。168 返回: 冻结ExprNode;不进行样本计算。169 异常: 内部参数冲突传播,配置由编译器先验证。170 """171 # 当前表达式所有真实依赖,不继承全局绑定。172 aliases = tuple(sorted({name for arg in args for name in arg.aliases}))173 # 任何未知关系都保留unverified,不能仅看输出单位是已知标签。174 status = 'verified' if resolve_unit(unit) is not None and all(arg.unit_status == 'verified' for arg in args) else 'unverified'175 # 所有参数均由编译器产生,执行器不接用户自由operator。176 return ExprNode(operator, dtype, unit, status, args, aliases=aliases,177 constant=all(arg.constant for arg in args),178 typed_constant=any(arg.typed_constant for arg in args) and all(arg.constant or arg.typed_constant for arg in args),179 conversion_promoted=any(arg.conversion_promoted for arg in args), **kwargs)180181182class _SyntaxCompiler:183 """在一次编译中检查名称、语法、单位与派生依赖。184185 signals和derived来自当前绑定,不读取数据来源。消息域另有显式参数表,186 参数按const执行但保constant=False,不随对方改变单位。nodes仅在本实例187 内复用已编译的派生树;visiting记录当前递归路径,用于指出循环依赖。188 根表达式的declared_unit可给未知组合提供显式标签,但不证明该量纲正确。189 状态不跨编译调用共享,不缓存信号值,也不扩大受限表达式语法。190 """191192 def __init__(self, bindings, derived):193 """建立当前声明表并校验完整派生输入。194195 参数: bindings为明确typed普通/消息运行绑定或各自静态声明;196 derived为tuple DerivedDefinition,数值执行仍由公开入口的类型门槛控制。197 返回: None,仅局部解析状态。198 异常: TypeError/ValueError为类型/缺项/重复/名称空间不一致。199 """200 # 不接无类型计划或可变派生列表。201 if not isinstance(bindings, (SignalBindings, DecodedBindings, DeclarationBindings, MessageBindings, MessageDeclarations)) or not isinstance(derived, tuple) or any(not isinstance(item, DerivedDefinition) for item in derived):202 # 结构错误先于解析/执行。203 raise TypeError('解析需要typed真实/声明绑定及tuple DerivedDefinition')204 # bindings已校验signals唯一/安全别名。205 self.bindings = bindings206 # 版本由绑定定义决定;公开入口另检查显式版本不能重解释旧绑定。207 self.config_version = getattr(bindings, 'config_version', 4)208 # 当前真实信号声明,不读取原日志。209 self.signals = {item.name: item for item in bindings.signals}210 # 只有消息域具有显式配置参数;原普通表达式不增加名称或混合dtype权限。211 self.message_domain = isinstance(bindings, (MessageBindings, MessageDeclarations))212 self.parameters = {item.name: item for item in bindings.parameters} if self.message_domain else {}213 # 重复派生声明不能靠字典覆盖隐藏。214 if len({item.name for item in derived}) != len(derived) or set(item.name for item in derived) != set(bindings.derived_names):215 # 登记名称不等于表达式已经接线。216 raise ValueError('derived声明与bindings名称不一致或重复')217 # 显式派生表自有副本,只读保存。218 self.derived = MappingProxyType({item.name: item for item in derived})219 # 本次缓存没有上次任务的节点/值。220 self.nodes = {}221 # 栈只用于完整依赖环诊断。222 self.visiting = []223224 def _error(self, node, message):225 """给出语法位置、拒绝原因与可用别名。226227 参数: node为AST节点,message为具体失败原因。228 返回: 不返回,抛ValueError。229 异常: ValueError定位行/列,不自动点号改名。230 """231 # AST位置从0列转用户可读1列;别名只来自当前计划。232 raise ValueError(f'行{getattr(node, "lineno", 1)}列{getattr(node, "col_offset", 0)+1}:{message};可用别名:{", ".join((*self.signals, *self.derived, *self.parameters))}')233234 def parse(self, text, unit=None):235 """解析一个表达式并验证根结果单位/枚举域。236237 参数: text为非空DSL;unit为可选显式结果单位。238 返回: typed ExprNode和规范AST。239 异常: ValueError为语法/白名单/类型/单位/环;不吞I/O异常。240 """241 # 文本不可空,也不接受任意对象str隐式转换。242 require_text(text, 'expression')243 # 显式单位保留原标签。244 if unit is not None:245 # unknown可以声明,但状态仍unverified。246 require_text(unit, 'expression.unit')247 # Python仅解析eval模式,赋值/多条语句自然失败。248 try:249 # 不执行解析所得对象。250 tree = ast.parse(text, mode='eval')251 except SyntaxError as error:252 # 用户语法错是执行前配置错误。253 raise ValueError(f'表达式语法错误:行{error.lineno}列{error.offset} {error.msg}') from error254 # 根声明单位只用于当前层,嵌套未知组合需独立派生声明。255 root = self.visit(tree.body, declared_unit=unit)256 # 原始字符串literal不能作为任意字符串表达式执行。257 if root.dtype is SignalDType.ENUM and not root.labels:258 # 字符串只有显式枚举比较才能获得合法域。259 self._error(tree.body, '字符串仅允许声明枚举域内的eq/ne')260 # 纯整数根也必须属于64位类型,不定义额外无界输出。261 if root.operator == 'const' and root.dtype in INTEGER_BOUNDS and not INTEGER_BOUNDS[root.dtype][0] <= root.value <= INTEGER_BOUNDS[root.dtype][1]:262 # 超大比较边界已通过特殊节点实现,正常输出不放行。263 self._error(tree.body, '整数字面量超出结果dtype')264 # 有单位结果不能靠unit字段自动做隐式转换。265 root = self._final_unit(root, unit, tree.body)266 # AST仅作为身份结构,不作为运行时eval输入。267 return root, ast.dump(tree, annotate_fields=True, include_attributes=False)268269 def _final_unit(self, root, unit, node):270 """执行当前版本的根结果单位要求。271272 参数: root为已编译节点,unit为可选目标标签,node用于定位错误。273 返回: 已核对未知单位标签的原节点。274 异常: ValueError为结果与声明单位不一致。275 """276 # 已知单位由单位编译器换算;此共享接点只核对无法推导的结果标签。277 if unit is not None and root.unit != unit:278 self._error(node, f'结果单位{root.unit}与声明{unit}不一致,须显式转换')279 return root280281 def _name(self, node):282 """解析真实别名、消息参数或本计划派生,并拒绝环/未知名称。283284 参数: node为Name AST。285 返回: 已绑定name节点或已编译派生树。286 异常: ValueError为未知别名/环/派生表达式错误。287 """288 # 普通分析只按当前显式别名查找,不读取semantic属性。289 if node.id in self.signals:290 # 原始定义含dtype/unit及固定enum域。291 definition = self.signals[node.id]292 # 多码同标签保留首次域顺序。293 labels = tuple(dict.fromkeys(value for _, value in definition.choices)) if definition.dtype is SignalDType.ENUM else ()294 # 未知标签/关系仍unverified,name不是常量。295 return ExprNode('name', definition.dtype, definition.unit, definition.unit_status, name=node.id, labels=labels, aliases=(node.id,))296 # 有单位参数按const执行,但不是裸literal;不可继承对方单位,也不是原信号依赖。297 if node.id in self.parameters:298 parameter = self.parameters[node.id]299 return ExprNode('const', parameter.dtype, parameter.unit, parameter.unit_status,300 value=parameter.value, name=node.id, constant=False)301 # 派生节点只在当前输入声明下复用。302 if node.id in self.nodes:303 # 不使用其他计划同名缓存。304 return self.nodes[node.id]305 # 访问栈包含同名证明环,须执行前拒绝。306 if node.id in self.visiting:307 # 保留完整依赖路径便于配置修正。308 self._error(node, '派生循环依赖:'+' -> '.join((*self.visiting, node.id)))309 # 未知名字不能推测原字段或语义名称。310 if node.id not in self.derived:311 # 错误提供当前可用名称。312 self._error(node, f'未知别名{node.id}')313 # 开始解析当前派生节点的依赖。314 self.visiting.append(node.id)315 # 派生表达式和明确单位必须真正接线。316 definition = self.derived[node.id]317 # 根单位校验由parse统一实施。318 result, _ = self.parse(definition.expr, definition.unit)319 # 成功后移除递归栈当前节点。320 self.visiting.pop()321 # 本次节点缓存仅存静态树,无信号值。322 self.nodes[node.id] = result323 # 派生实际依赖展开到已解码信号。324 return result325326 def _context(self, literal, other, node, *, arithmetic):327 """把纯数字/enum常量按另一侧明确类型及单位校验。328329 参数: literal为纯常量子树;other为typed对方;node定位;arithmetic330 表示算术字面量必须能在目标整数类型表示。331 返回: 合法常量子树;含浮点常量的整型运算保持float路径。332 异常: ValueError为字面量类型/定宽/枚举域冲突。333 """334 # 仅纯常量获得上下文,不能把信号强行改dtype/unit。335 if not literal.constant:336 # 输入依赖不允许隐藏转换。337 return literal338 # 枚举字符串常量严格匹配当前逻辑域。339 if literal.dtype is SignalDType.ENUM:340 # 仅裸字符串可进入指定枚举域。341 if other.dtype is not SignalDType.ENUM or literal.operator != 'const' or literal.value not in other.labels:342 # 未声明标签不自动取第一项。343 self._error(node, '枚举字面量不在声明域')344 # 字符串不作一般单位转换,只保留同域比较。345 return replace(literal, labels=other.labels, unit=other.unit, unit_status=other.unit_status)346 # 数字常量与bool不能混用,bool不是隐式0/1。347 if literal.dtype is SignalDType.BOOL or other.dtype in (SignalDType.BOOL, SignalDType.ENUM):348 # bool同型eq/ne/logic仍由静态协议允许。349 return literal350 # 整数算术字面量按对方整数dtype,递归纯常量子树同样校验。351 if other.dtype in INTEGER_BOUNDS and literal.dtype in INTEGER_BOUNDS and arithmetic:352 # 对const递归重建,以免uint64+常量子树隐藏signed promotion。353 if literal.operator == 'const':354 # 目标整数字面量必须可表示,而非算术runtime缺失。355 if not INTEGER_BOUNDS[other.dtype][0] <= literal.value <= INTEGER_BOUNDS[other.dtype][1]:356 # 包含uint信号加负整数字面量的配置拒绝。357 self._error(node, '整数字面量无法在算术dtype中表示')358 # 原整数值保持精确,不先变float。359 return replace(literal, dtype=other.dtype)360 # 纯整数子树只含定宽算术/abs/neg,逐节点重建静态目标dtype。361 args = tuple(self._context(arg, other, node, arithmetic=True) for arg in literal.args)362 # 除法已经静态为float,不会进入此分支。363 return replace(literal, dtype=other.dtype, args=args)364 # float信号与整数常量先显式转换常量,不放开任意混合信号promotion。365 if other.dtype is SignalDType.FLOAT64 and literal.dtype in INTEGER_BOUNDS:366 # 原整数constant域仍需64位,超宽只允许整数精确比较专用节点。367 if literal.operator == 'const' and not INTEGER_BOUNDS[literal.dtype][0] <= literal.value <= INTEGER_BOUNDS[literal.dtype][1]:368 # 字面量越界是执行前错误。369 self._error(node, '数值字面量超出64位原类型')370 # to_float由两个独立数值后端在runtime检查精确表示。371 return _node('to_float', SignalDType.FLOAT64, literal.unit, (literal,))372 # 显式float字面量对整数保持float,后续有精度mask。373 return literal374375 def _units(self, operator, left, right, node, declared_unit=None):376 """按首版表推断单位,裸常量加减/比较采用对方单位。377378 参数: operator及两typed子树;node定位;declared_unit为根显式未知组合标签。379 返回: 上下文单位子树及输出单位。380 异常: ValueError为未转换单位或未支持组合。381 """382 # 加减/比较包括isclose使用相同裸数字常量规则。383 if operator in ('add', 'sub', 'compare'):384 # 一边纯数值常量可以继承另一侧单位;纯常量对纯常量仍1。385 if left.constant and not right.constant and left.dtype not in (SignalDType.BOOL, SignalDType.ENUM):386 # 改静态单位不执行数值缩放。387 left = replace(left, unit=right.unit, unit_status=right.unit_status)388 # 另一侧常量同理,不能改有依赖的信号单位。389 if right.constant and not left.constant and right.dtype not in (SignalDType.BOOL, SignalDType.ENUM):390 # 当前裸常量采用对方原单位。391 right = replace(right, unit=left.unit, unit_status=left.unit_status)392 # 已知不同标签也必须额外显式转换,不根据同量纲猜倍率。393 if left.unit != right.unit:394 # 未知不同标签同样不能假设等价。395 self._error(node, f'单位{left.unit}/{right.unit}不一致,须显式转换')396 # 比较结果unit1,算术保留操作数单位。397 return left, right, '1' if operator == 'compare' else left.unit398 # 无量纲乘除不会引入新工程量纲。399 if operator == 'mul' and '1' in (left.unit, right.unit):400 # 两边都1也得到1。401 return left, right, right.unit if left.unit == '1' else left.unit402 # 相同单位相除得到1,除无量纲保留分子单位。403 if operator == 'div' and (left.unit == right.unit or right.unit == '1'):404 # 无额外值换算。405 return left, right, '1' if left.unit == right.unit else left.unit406 # 普通分析可显式声明未知组合,但不得假称已知量纲验证。407 if declared_unit is not None and (resolve_unit(left.unit) is None or resolve_unit(right.unit) is None):408 # 任意结果标签仍unverified,即使用户标成W。409 return replace(left, unit_status='unverified'), right, declared_unit410 # 已知未支持组合不能用标签覆盖当证明。411 self._error(node, f'未支持单位组合{left.unit} {operator} {right.unit},需显式规范化/派生声明')412413 def _binary(self, operator, left, right, node, declared_unit=None):414 """静态编译数值/比较,整数常量比较保留精确边界。415416 参数: operator及已编译左右节点,node定位,declared_unit根声明。417 返回: typed ExprNode;绝不运行用户代码。418 异常: ValueError为单位/类型/常量配置错误。419 """420 # 先校验单位,比较也不能隐藏mA与A混用。421 left, right, unit = self._units('compare' if operator in CMP.values() else operator, left, right, node, declared_unit)422 # 纯整数常量没有signal dtype上下文,先对称选共同定宽。423 if operator not in CMP.values() and left.constant and right.constant and left.dtype in INTEGER_BOUNDS and right.dtype in INTEGER_BOUNDS:424 # 有超signed正整数literal时选uint64;所有operand仍须可表示。425 dtype = SignalDType.UINT64 if SignalDType.UINT64 in (left.dtype, right.dtype) else SignalDType.INT64426 # 只是编译器类型上下文,不产生运行时信号或新binding。427 target = ExprNode('const', dtype, '1', 'verified', value=0, constant=True)428 # 两側独立同一目标,不以遍历顺序决定政策。429 left, right = self._context(left, target, node, arithmetic=True), self._context(right, target, node, arithmetic=True)430 # 整数信号与任意精确整数字面量比较无需把边界塞入定宽结果。431 if operator in CMP.values() and left.dtype in INTEGER_BOUNDS and right.dtype in INTEGER_BOUNDS:432 # 常量在左,统一反向比较当前正常信号值。433 if left.operator == 'const' and right.operator != 'const':434 # 特殊节点value保留完整Python整数边界。435 return _node('int_literal_'+REVERSE_CMP[operator], SignalDType.BOOL, unit, (right,), value=left.value)436 # 常量在右直接按原运算方向比较。437 if right.operator == 'const' and left.operator != 'const':438 # 超uint64边界只是精确比较阈值,不是新数值输出dtype。439 return _node('int_literal_'+operator, SignalDType.BOOL, unit, (left,), value=right.value)440 # 常量与实际另一侧的类型上下文明确处理。441 left = self._context(left, right, node, arithmetic=operator not in CMP.values())442 # 使用已经校验左側dtype,再处理右側常量。443 right = self._context(right, left, node, arithmetic=operator not in CMP.values())444 # 先保持原普通规则:只有float纯常量证明允许混合,不能放行任意信号。445 float_literal = left.dtype is not right.dtype and ((left.constant and left.dtype is SignalDType.FLOAT64) or (right.constant and right.dtype is SignalDType.FLOAT64))446 # 只有单位转换确已要求float时才补齐另一侧的精确提升;不放开普通混合信号。447 if left.dtype is not right.dtype:448 float_literal |= any(item.dtype is SignalDType.FLOAT64 and item.conversion_promoted for item in (left, right))449 # quantity的数值仍为字面量,只是单位不可继承;保留原浮点字面量精度政策。450 float_literal |= any(item.dtype is SignalDType.FLOAT64 and item.typed_constant for item in (left, right))451 # 消息域的无信号依赖配置子树是显式FLOAT64值;仍由现数值核检查整数精度。452 if self.message_domain and left.dtype is not right.dtype:453 float_literal |= ((left.dtype is SignalDType.FLOAT64 and not left.aliases)454 or (right.dtype is SignalDType.FLOAT64 and not right.aliases))455 # bool/enum及不同信号dtype在静态协议中拒绝。456 try:457 # 仅共享静态规则,没有共享数值执行。458 dtype = result_dtype(operator, left.dtype, right.dtype, float_literal)459 except (TypeError, ValueError) as error:460 # 补充用户位置而非runtime UNKNOWN。461 self._error(node, str(error))462 # enum比较必须同域,不能直接比较同编码不同标签。463 if left.dtype is SignalDType.ENUM and left.labels != right.labels:464 # 当前首版没有枚举跨域自动归一化。465 self._error(node, '枚举比较需要相同声明域')466 # 正常算术const叶仍必须在其静态类型内,即使两側都是常量。467 for operand in (left, right):468 # 巨大整数仅在前述特殊比较节点允许。469 if operand.operator == 'const' and operand.dtype in INTEGER_BOUNDS and not INTEGER_BOUNDS[operand.dtype][0] <= operand.value <= INTEGER_BOUNDS[operand.dtype][1]:470 # 不能在runtime模型构造时才失败。471 self._error(node, '整数字面量超出运算dtype')472 # 真实依赖/constant状态由统一节点模型汇总。473 return _node(operator, dtype, unit, (left, right), allow_float_literal=float_literal)474475 def visit(self, node, declared_unit=None):476 """逐AST节点执行首版白名单,不递归放行未知语法。477478 参数: node为解析节点;declared_unit仅根静态单位上下文。479 返回: 完整typed内部节点。480 异常: ValueError定位未注册语法/函数/类型/单位。481 """482 # 纯literal仅数字、bool和待枚举上下文的str。483 if isinstance(node, ast.Constant):484 # bool必须先于int分类,因为Python bool是int子类。485 value = node.value486 # 不接受None、bytes、complex或任意对象。487 if type(value) not in (bool, int, float, str) or type(value) is float and not isfinite(value):488 # 非有限literal同样是配置错误。489 self._error(node, '仅支持有限数字/bool及枚举字符串literal')490 # 字符串暂存为ENUM,只有后续明确域比较才合法。491 dtype = SignalDType.BOOL if type(value) is bool else SignalDType.ENUM if type(value) is str else SignalDType.FLOAT64 if type(value) is float else SignalDType.INT64 if value <= 2 ** 63 - 1 else SignalDType.UINT64492 # 纯literal无单位、无来源依赖。493 return ExprNode('const', dtype, '1', 'verified', value=value, constant=True)494 # Name只访问已注册别名,绝不反射对象。495 if isinstance(node, ast.Name):496 # 未注册名称也在此精确定位。497 return self._name(node)498 # 一元±常量保留完整精确整数,供算术/比较上下文校验。499 if isinstance(node, ast.UnaryOp):500 # 常量负号不先包装成uint64再运行下溢。501 if isinstance(node.op, (ast.USub, ast.UAdd)) and isinstance(node.operand, ast.Constant) and type(node.operand.value) in (int, float):502 # Python只计算编译器已识别的原生纯literal符号。503 value = -node.operand.value if isinstance(node.op, ast.USub) else node.operand.value504 # 返回仍须经过Constant合法性检查。505 return self.visit(ast.copy_location(ast.Constant(value=value), node))506 # 一元操作数先完成静态校验。507 child = self.visit(node.operand)508 # not仅bool,不使用Python truthiness。509 if isinstance(node.op, ast.Not) and child.dtype is SignalDType.BOOL:510 # 运行时not UNKNOWN仍UNKNOWN。511 return _node('not', SignalDType.BOOL, '1', (child,))512 # 数值一元±不允许bool/enum。513 if isinstance(node.op, (ast.USub, ast.UAdd)) and child.dtype in (*INTEGER_BOUNDS, SignalDType.FLOAT64):514 # unary+保留原typed树,neg由独立数值核处理。515 return child if isinstance(node.op, ast.UAdd) else _node('neg', child.dtype, child.unit, (child,))516 # 位取反等未支持形式拒绝。517 self._error(node, '未支持一元语法或类型')518 # 数值运算仅固定四种。519 if isinstance(node, ast.BinOp) and type(node.op) in BINOPS:520 # 不使用任意Python运算方法。521 return self._binary(BINOPS[type(node.op)], self.visit(node.left), self.visit(node.right), node, declared_unit)522 # 链式比较逐对生成比较节点,再用保留UNKNOWN(未知)的三值and组合。523 if isinstance(node, ast.Compare):524 # 比较类型逐项注册,is/in等均拒绝。525 if any(type(operator) not in CMP for operator in node.ops):526 # 不能让Python对象身份或容器访问进入DSL。527 self._error(node, '未支持比较运算')528 # 中间操作数静态树只编译一次,不执行求值副作用。529 operands = tuple(self.visit(item) for item in (node.left, *node.comparators))530 # 单个比较的类型/单位都先校验。531 comparisons = tuple(self._binary(CMP[type(operator)], operands[index], operands[index+1], node) for index, operator in enumerate(node.ops))532 # chain输出同一BOOL,不依赖对数组作bool。533 return comparisons[0] if len(comparisons) == 1 else _node('and', SignalDType.BOOL, '1', comparisons)534 # 逻辑and/or仅bool,不能把数值当TRUE。535 if isinstance(node, ast.BoolOp) and isinstance(node.op, (ast.And, ast.Or)):536 # 逐子树类型/名称/单位均静态检查,未知值留runtime。537 args = tuple(self.visit(item) for item in node.values)538 # 已知False能决定and但不能绕过静态配置错误。539 if any(arg.dtype is not SignalDType.BOOL for arg in args):540 # 不采用Python返回任意操作数的逻辑语义。541 self._error(node, 'and/or仅支持bool表达式')542 # TRUE、FALSE和UNKNOWN三种结果的组合规则,由两个解释器各自实现。543 return _node('and' if isinstance(node.op, ast.And) else 'or', SignalDType.BOOL, '1', args)544 # 函数调用必须裸Name且固定注册。545 if isinstance(node, ast.Call):546 # 属性调用、未注册signal/任意函数禁止。547 if not isinstance(node.func, ast.Name) or node.func.id not in ('abs', 'isclose'):548 # 不访问用户函数对象,也不隐藏注册自由插件。549 self._error(node, '函数仅允许注册abs/isclose,属性访问及任意调用禁止')550 # abs固定一个位置参数,无任意keywords/*args。551 if node.func.id == 'abs':552 # unpack等AST形式不会满足固定签名。553 if len(node.args) != 1 or node.keywords:554 # 不使用库默认自由签名。555 self._error(node, 'abs必须为abs(x)')556 # 正常数值类型才能abs。557 child = self.visit(node.args[0])558 # bool/enum不可隐式数字化。559 if child.dtype not in (*INTEGER_BOUNDS, SignalDType.FLOAT64):560 # 字符串abs拒绝在执行前。561 self._error(node, 'abs只支持数值')562 # signed最小值的runtime质量由数值核处理。563 return _node('abs', child.dtype, child.unit, (child,))564 # isclose固定两位置参数及完整两个具名容差。565 if len(node.args) != 2 or len(node.keywords) != 2 or {item.arg for item in node.keywords} != {'atol', 'rtol'}:566 # 不能默默采用NumPy默认容差。567 self._error(node, 'isclose必须显式atol、rtol及两个位置参数')568 # 输入只限数值,不让bool/enum通过float转换。569 a, b = (self.visit(item) for item in node.args)570 # isclose不能通过bool等值合法这一事实绕过数值类型要求。571 if any(item.dtype not in (*INTEGER_BOUNDS, SignalDType.FLOAT64) for item in (a, b)):572 # 逻辑/枚举不是该函数的数值实参。573 self._error(node, 'isclose只支持数值实参')574 # isclose需要真正数值常量,不能使用超宽整数比较边界。575 for item in (a, b):576 # 信号正常值已经由typed模型保证定宽。577 if item.operator == 'const' and item.dtype in INTEGER_BOUNDS and not INTEGER_BOUNDS[item.dtype][0] <= item.value <= INTEGER_BOUNDS[item.dtype][1]:578 # 不能把超宽配置literal拖到runtime失败。579 self._error(node, 'isclose实参字面量超出64位类型')580 # 裸常量首先完成比较单位上下文。581 a, b, _ = self._units('compare', a, b, node)582 # 混合真实int/float信号须先显式归一化,即使isclose也不隐藏放行。583 self._binary('eq', a, b, node)584 # 容差仍须裸literal;参数虽operator=const但constant=False,不能被偷偷重标单位。585 tolerances = {}586 # 固定两个keywords,AST表达式不动态执行。587 for keyword in node.keywords:588 # 一元±数字literal通过同一静态解析。589 tolerance = self.visit(keyword.value)590 # 大整数只能原64位literal,转换精度仍runtime核查。591 if tolerance.operator != 'const' or not tolerance.constant or tolerance.dtype not in (*INTEGER_BOUNDS, SignalDType.FLOAT64) or tolerance.value < 0 or tolerance.dtype in INTEGER_BOUNDS and not INTEGER_BOUNDS[tolerance.dtype][0] <= tolerance.value <= INTEGER_BOUNDS[tolerance.dtype][1]:592 # 表达式/不完整容差直接配置失败。593 self._error(keyword.value, 'isclose容差须有限非负数字字面量')594 # atol采用比较单位,rtol无量纲。595 tolerances[keyword.arg] = replace(tolerance, unit=a.unit if keyword.arg == 'atol' else '1')596 # 数值输入a/b和参数都通过独立float精度核计算判据。597 return _node('isclose', SignalDType.BOOL, '1', (a, b, tolerances['atol'], tolerances['rtol']))598 # 点号语法提供位置/原因/别名,而非静默替换下划线。599 if isinstance(node, ast.Attribute):600 # 禁止所有对象访问,不把semantic字符串当属性路径。601 self._error(node, '禁止ast.Attribute/点号属性访问,请使用已绑定简单别名')602 # pow/subscript/comprehension/lambda/starred等统一本白名单拒绝。603 self._error(node, f'禁止语法{type(node).__name__}')604605606def compile_expression(text: str, bindings: SignalBindings | DecodedBindings | MessageBindings, *, derived: tuple[DerivedDefinition, ...] = (), unit: str | None = None, config_version: int | None = None, extensions=None) -> CompiledExpression:607 """检查表达式及全部派生声明,关联当前真实输入绑定。608609 参数:610 text: 非空文本;bindings为真实SignalBindings、DecodedBindings或MessageBindings。611 derived: 完整且不可变的DerivedDefinition元组,需与已登记名称一致。612 unit: 可选结果单位;兼容登记单位同时转换数值。config_version省略时继承绑定。613 extensions: 可选的宿主ExtensionRegistry,省略时使用绑定已经关联的注册。614 返回:615 CompiledExpression,保存表达式树、实际依赖别名和完整绑定身份。616 异常:617 TypeError、ValueError: 输入结构、语法、名称、类型、单位、循环依赖618 或多来源同步依据不满足要求。619 流程:620 每次新建编译状态,先检查所有派生声明,再编译根表达式与同步依赖。621 此时不计算数值,也不读取日志或加载DBC;数值精度失败由求值器报告。622 """623 # 只放行明确可执行绑定;普通或消息声明均不能替代各自实际输入证据。624 if not isinstance(bindings, (SignalBindings, DecodedBindings, MessageBindings)):625 # 静态声明必须走validate_expression,不能生成可执行树。626 raise TypeError('可执行编译需要真实SignalBindings、DecodedBindings或MessageBindings')627 # 每次使用全新编译状态,隔离绑定和派生节点。628 compiler = _create_compiler(bindings, derived, config_version=config_version, extensions=extensions)629 # 即使派生未被当前根使用也检查配置循环/未知名称。630 for definition in derived:631 # Name只作为内部解析入口,位置未提供时错误默认行1列1。632 compiler._name(ast.Name(id=definition.name, ctx=ast.Load()))633 # 根和声明单位完成所有静态语义检查。634 root, normalized = compiler.parse(text, unit)635 # 多源只有当前根实际共同依赖才需要同步依据。636 bindings.require_synchronized(root.aliases)637 # 身份包含来源、显式转换、值有效期、质量政策、全部派生定义和编译版本。638 identity = expression_identity(bindings, root, normalized, derived, extensions=extensions)639 # 返回编译后的表达式及其依赖。640 return CompiledExpression(text, bindings, root, root.aliases, normalized, identity)641642643def validate_expression(text: str, bindings: DeclarationBindings | MessageDeclarations, *, derived: tuple[DerivedDefinition, ...] = (), unit: str | None = None, config_version: int | None = None, extensions=None) -> StaticExpression:644 """只根据来源声明检查表达式,不读取真实输入。645646 参数:647 text: 非空表达式文本;bindings为DeclarationBindings或MessageDeclarations。648 derived: 完整DerivedDefinition元组;unit为可选显式结果单位。649 config_version: 省略时继承绑定;extensions为可选宿主注册,不运行回调。650 返回:651 StaticExpression,说明名称、语法、类型、单位和同步声明已检查。652 该类型不能交给数值解释器执行,也不证明实际字段或数据内容已核实。653 异常:654 TypeError、ValueError: 声明结构、语法、名称、单位、循环依赖或同步缺项。655 边界:656 未被根引用的派生表达式也要检查;读取真实输入后需要重新建立运行绑定。657 """658 # 接口明确拒绝把真实绑定混为纯静态声明。659 if not isinstance(bindings, (DeclarationBindings, MessageDeclarations)):660 # 避免调用侧误把声明身份当执行缓存身份。661 raise TypeError('静态校验需要DeclarationBindings或MessageDeclarations')662 # 白名单及类型政策共用编译器,不复制另一套宽松DSL。663 compiler = _create_compiler(bindings, derived, config_version=config_version, extensions=extensions)664 # 未被根引用的派生也要检查环和单位。665 for definition in derived:666 # 只用受限内部Name入口,无动态属性访问。667 compiler._name(ast.Name(id=definition.name, ctx=ast.Load()))668 # 根解析保留错误行列与可用别名。669 root, normalized = compiler.parse(text, unit)670 # 声明联合依赖必须已有明确同步条件。671 bindings.require_synchronized(root.aliases)672 # 独立版本前缀确保不能与真实可执行身份混用。673 identity = expression_identity(bindings, root, normalized, derived, static=True, extensions=extensions)674 # 返回类型与CompiledExpression分开,解释器结构门槛仍然有效。675 return StaticExpression(text, bindings, root, root.aliases, normalized, identity)676677678def _create_compiler(bindings, derived, *, config_version=None, extensions=None):679 """构造当前编译器,拒绝用参数重解释绑定版本。680681 参数: bindings/derived为已校验声明,config_version可省略,extensions为可选v4宿主注册。682 返回: 当前任务独享的编译器实例。683 异常: TypeError/ValueError为版本类型、冲突或尚未支持的扩展对象。684 """685 # 版本是计划身份的一部分,不使用模块级可变开关。686 version = getattr(bindings, 'config_version', 4)687 from .unit_conversion import require_config_version688 require_config_version(version)689 if config_version is not None and (type(config_version) is not int or config_version != version):690 raise ValueError('表达式config_version必须与绑定版本一致')691 bound = getattr(bindings,'extensions',None)692 if extensions is not None and bound is not None and extensions is not bound:693 raise ValueError('表达式与绑定必须使用同一个宿主ExtensionRegistry')694 registry = bound if extensions is None else extensions695 if registry is not None:696 from ..extensions import ExtensionRegistry697 if type(registry) is not ExtensionRegistry or version != 4:698 raise ValueError('宿主扩展需要版本4和显式ExtensionRegistry')699 # 延迟导入仅解决类型定义依赖,不导入用户配置指定的模块。700 from .expression_v4 import UnitCompiler701 return UnitCompiler(bindings, derived, registry)702703704def expression_identity(bindings, root, normalized, derived, *, static=False, extensions=None):705 """共用公开与按需重编译的身份公式,防止版本接线遗漏。706707 参数: bindings为对应绑定,root为已编译树,normalized为AST,derived为声明;static区分静态域。708 返回: SHA256文本,包含当前单位和函数规则版本。709 异常: 内部类型错误直接传播,不读取文件。710 """711 # 计划身份显式包含当前单位与函数规则版本。712 prefix = ('static-expression-v1',) if static else ('expression-v1', 'reference+numpy-v1')713 from .unit_registry import REGISTRY_VERSION714 prefix += (4, REGISTRY_VERSION, 'functions-v4-b6')715 registry = extensions if extensions is not None else getattr(bindings,'extensions',None)716 if registry is not None:717 prefix += ('host-v1',registry.identity)718 payload = (*prefix, bindings.identity, normalized, repr(root), tuple((item.name, item.expr, item.unit) for item in derived))719 return sha256(_identity(payload).encode('utf-8')).hexdigest()720