chronaxis.core.expression_v4
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""为版本4表达式编译一次性单位转换,统一处理当前单位与函数规则。23输入为已校验绑定和AST,输出包含unit_convert节点的不可变表达式树。4先解释裸常量的单位,再统一兼容量的尺度,最后推导结果单位及数值类型。5编译只使用声明与常量。clip的两个边界均无信号依赖时,标量核在此判定6其顺序;编译器不读取日志或求样本值。7温度偏移、量种和整数精度规则不能通过只改单位标签绕过。8"""910# 复用白名单语法和类型规则;所有量种关系来自同一单位注册表。11import ast12from dataclasses import replace13from .expression import _SyntaxCompiler, ExprNode, _node14from .expression_math_v4 import MATH_FUNCTIONS, check_integer_leaves, compile_math15from .lookup_compiler import compile_lookup16from .expression_integer_v4 import INTEGER_FUNCTIONS, compile_integer17from .enum_codes import compile_enum_code18from .numeric import INTEGER_BOUNDS19from .types import SignalDType as D20from . import unit_registry as units212223def numeric_coefficient(value):24 """将精确有理系数压为整数或float,供已选后端执行。2526 参数: value为Fraction。27 返回: 分母为1时返回int,否则返回float。28 异常: 不能表示的浮点范围错误由调用者/倍率模型检查。29 """30 # 保留整数路径,不能把无损倍率1000提前变成浮点。31 return value.numerator if value.denominator == 1 else float(value)323334class UnitCompiler(_SyntaxCompiler):35 """在原受限语法上接入v4单位、常量和转换规则。3637 每次编译独享状态。声明单位在裸常量适配之前保留,转换只写入树一次;38 数值执行根据节点系数转换,编译器不逐条处理数据。39 """4041 def __init__(self, bindings, derived, extensions=None):42 """建立绑定并检查本版本已经开放的函数名冲突。4344 参数: bindings为v4绑定,derived为完整派生声明tuple。45 返回: None。46 异常: ValueError为别名与本版本函数重名;其余声明错误沿原校验传播。47 """48 # 内置函数名称不能再作为信号、派生或参数别名。49 super().__init__(bindings, derived)50 self.extensions = extensions51 reserved = {'quantity', 'to_unit', 'where', 'enum_code', 'lookup'} | MATH_FUNCTIONS | INTEGER_FUNCTIONS52 if extensions is not None:53 reserved.update(extensions.functions)54 conflicts = reserved.intersection((*self.signals, *self.derived, *self.parameters))55 if conflicts:56 raise ValueError('v4名称与函数冲突:' + ', '.join(sorted(conflicts)))5758 def _known_node(self, result):59 """按v4目录判定组合结果的单位状态,不提升未知输入的可信度。6061 参数: result为已编译ExprNode。62 返回: 单位状态准确的新节点或原节点。63 异常: 非法单位文本按编译错误传播。64 """65 # name状态来自信号完整转换验证,不能用已知目标标签覆盖未验证输入。66 if result.operator == 'name':67 return result68 verified = units.resolve_unit(result.unit) is not None and all(arg.unit_status == 'verified' for arg in result.args)69 return replace(result, unit_status='verified' if verified else 'unverified')7071 def _name(self, node):72 """取得信号/派生原声明尺度,附带已有输入转换的类型提升依据。7374 参数: node为Name AST。75 返回: 已校验ExprNode。76 异常: 名称/循环/声明错误沿原编译器传播。77 """78 # 保留mV等声明尺度,使x>500里的500仍是mV。79 result = super()._name(node)80 definition = self.signals.get(node.id)81 if definition is not None and getattr(definition, 'unit_conversion_promoted', False):82 result = replace(result, conversion_promoted=True)83 return result8485 def _convert(self, child, target, node, *, explicit=False):86 """为兼容单位生成一次仿射转换,不重复应用源层的DBC或标定系数。8788 参数: child为当前数值表示;target为单位文本;node定位;explicit保留输出尺度。89 返回: 原节点或unit_convert节点,dtype在编译期确定。90 异常: ValueError为非数值转换、未知关系、量种不兼容或不可表示倍率。91 """92 # 同标签的未知单位可以继续使用,但不伪造已验证转换。93 target_unit = units.resolve_unit(target)94 label = units.output_label(target_unit, target) if target_unit is not None else target95 if child.unit == target or child.unit == label:96 return replace(child, unit=label, unit_explicit=explicit or child.unit_explicit)97 if child.dtype not in (*INTEGER_BOUNDS, D.FLOAT64):98 self._error(node, '单位转换只支持数值')99 source_unit = units.resolve_unit(child.unit)100 if source_unit is None or target_unit is None:101 self._error(node, f'未登记单位转换:{child.unit}→{target}')102 # 原子别名使用规范名;倍率为1的复合目标使用稳定名称,其他保留可解析输入。103 target = label104 try:105 scale, offset = units.conversion(source_unit, target_unit)106 factor = scale.to_float() if scale.pi_power else numeric_coefficient(scale.factor)107 # to_float还检查下溢到零,不能直接float极小Fraction。108 if type(factor) is float:109 factor = scale.to_float()110 shift = numeric_coefficient(offset)111 except (TypeError, ValueError, OverflowError) as error:112 self._error(node, str(error))113 # 整数倍且整数偏移保留原整数类型,其余必须有float容纳小数。114 promoted = child.dtype in INTEGER_BOUNDS and (type(factor) is not int or type(shift) is not int)115 dtype = D.FLOAT64 if promoted else child.dtype116 if factor == 1 and shift == 0:117 return replace(child, unit=target, unit_explicit=explicit or child.unit_explicit)118 return ExprNode('unit_convert', dtype, target, child.unit_status, (child,),119 value=(factor, shift), aliases=child.aliases, constant=child.constant,120 conversion_promoted=promoted or child.conversion_promoted,121 unit_explicit=explicit, typed_constant=child.typed_constant)122123 def _canonical(self, child, node):124 """把已知数值换到该量种的固定基准,未知标签保持。125126 参数: child为节点,node为错误位置。127 返回: 已转换节点;逻辑/枚举不作数值换算。128 异常: 单位或转换边界错误传播。129 """130 # 不是每个结果都是数值,BOOL和枚举仍沿原协议。131 if child.dtype not in (*INTEGER_BOUNDS, D.FLOAT64):132 return child133 unit = units.resolve_unit(child.unit)134 return child if unit is None else self._convert(child, units.canonical_unit(unit).symbol, node)135136 def _final_unit(self, root, unit, node):137 """在表达式输出边界同时转换目标数值和标签。138139 参数: root为表达式树;unit为显式输出单位或None;node定位。140 返回: 显式目标优先,否则用规范基准。141 异常: 不兼容目标或未知转换明确报错。142 """143 # to_unit和quantity直接作为根时保留其明确尺度,复合运算再按量种规范化。144 if unit is not None:145 return self._convert(root, unit, node, explicit=True)146 return root if root.unit_explicit else self._canonical(root, node)147148 def _context(self, literal, other, node, *, arithmetic):149 """复用定宽字面量检查,同时保持新单位的验证状态。150151 参数: literal/other为数值节点;node定位;arithmetic区分算术和比较。152 返回: 按上下文检查过的节点。153 异常: 类型、范围或枚举域错误沿原检查传播。154 """155 # 纯常量仍需保持旧精度检查,不给BOOL或ENUM增加隐式数值转换。156 if literal.typed_constant:157 # quantity已经明确单位,只借用数值字面量的定宽/精度适配,不再重标单位。158 result = super()._context(replace(literal, constant=True), other, node, arithmetic=arithmetic)159 result = replace(result, constant=False, typed_constant=True)160 else:161 result = super()._context(literal, other, node, arithmetic=arithmetic)162 return self._known_node(result) if result.args else result163164 def _units(self, operator, left, right, node, declared_unit=None):165 """先解释裸常量,再按兼容量换算,独立处理温度加减。166167 参数: operator为add/sub/compare/mul/div;左右节点及错误位置;declared_unit为未知组合标签。168 返回: 已转换左右节点与结果单位。169 异常: ValueError为不兼容量种、温度组合或未登记关系。170 """171 # 单一对方的原声明单位是裸常量锚点,quantity不是裸常量。172 if operator in ('add', 'sub', 'compare'):173 if left.constant and not right.constant and left.dtype in (*INTEGER_BOUNDS, D.FLOAT64):174 left = replace(left, unit=right.unit, unit_status=right.unit_status)175 if right.constant and not left.constant and right.dtype in (*INTEGER_BOUNDS, D.FLOAT64):176 right = replace(right, unit=left.unit, unit_status=left.unit_status)177 a, b = units.resolve_unit(left.unit), units.resolve_unit(right.unit)178 if a is None or b is None:179 # 未知组合继续要求明确标签,不能借已知输出单位伪造关系证明。180 return super()._units(operator, left, right, node, declared_unit)181 try:182 if operator in ('add', 'sub', 'compare'):183 roles = (a.temperature_role, b.temperature_role)184 if operator != 'compare' and 'absolute' in roles:185 if roles == ('absolute', 'absolute') and operator == 'sub':186 result_unit = 'delta_K'187 elif roles == ('absolute', 'delta') or (roles == ('delta', 'absolute') and operator == 'add'):188 result_unit = 'K'189 else:190 raise ValueError('绝对温度仅允许相减得到温差,或加减温差;裸常量按当前温度单位解释')191 else:192 units.conversion(a, b)193 result_unit = '1' if operator == 'compare' else units.canonical_unit(a).symbol194 return self._canonical(left, node), self._canonical(right, node), result_unit195 # 必须先检查偏移单位,不能用自动转K绕过摄氏乘除限制。196 combined = units.multiply(a, b) if operator == 'mul' else units.divide(a, b)197 return self._canonical(left, node), self._canonical(right, node), units.canonical_unit(combined).symbol198 except (TypeError, ValueError) as error:199 self._error(node, str(error))200201 def _binary(self, operator, left, right, node, declared_unit=None):202 """复用原定宽编译规则,并按新目录确定结果单位状态。203204 参数: operator、左右节点、node及可选声明标签与基类相同。205 返回: 不可变数值/比较节点。206 异常: 原类型校验和新单位校验错误直接传播。207 """208 # 单位转换产生的float提升由基类显式标志接线,不使用NumPy隐式promotion。209 return self._known_node(super()._binary(operator, left, right, node, declared_unit))210211 def _where(self, node):212 """编译只在条件已知时选取一条数值分支的节点。213214 参数: node为where调用AST;三个位置参数都必须能通过静态检查。215 返回: 包含完整依赖的节点,两条分支已统一单位和dtype。216 异常: ValueError为签名、非布尔条件、类型/量种不兼容或字面量越界。217 """218 # 即使条件为常量,未选路径同样要做名称、安全、类型和单位检查。219 if len(node.args) != 3 or node.keywords:220 self._error(node, 'where需要三个位置参数:条件、真分支、假分支')221 condition, left, right = (self.visit(arg) for arg in node.args)222 if condition.dtype is not D.BOOL:223 self._error(node.args[0], 'where条件必须是BOOL')224 if any(branch.dtype not in (*INTEGER_BOUNDS, D.FLOAT64) for branch in (left, right)):225 self._error(node, 'where的两个分支都必须是数值')226 # abs(超宽整数)等子树即使永远不被选中也不能隐藏非法输入。整数比较227 # 允许的无界阈值已编译成专用节点字段,不是需要按定宽装箱的const叶。228 check_integer_leaves(self, (condition, left, right), node)229230 # 使用比较所需的同量种关系:绝对温度可以互选,但不能与温差互选。231 # 它还会先把裸常量解释为另一侧原声明单位,之后才换到共同基准。232 left, right, _ = self._units('compare', left, right, node)233 if (all(b.constant or b.typed_constant for b in (left, right))234 and left.dtype in INTEGER_BOUNDS and right.dtype in INTEGER_BOUNDS):235 # 两侧纯整数字面量对称选择能容纳正大数的共同定宽。236 target = D.UINT64 if D.UINT64 in (left.dtype, right.dtype) else D.INT64237 anchor = ExprNode('const', target, '1', 'verified', value=0, constant=True)238 left, right = (self._context(b, anchor, node, arithmetic=True) for b in (left, right))239 else:240 # 常量适配不放宽一般整数/浮点信号或不同有符号性的类型约束。241 left = self._context(left, right, node, arithmetic=True)242 right = self._context(right, left, node, arithmetic=True)243 if left.dtype is not right.dtype:244 # 浮点字面量、消息参数或单位换算可要求提升另一侧;精度在活动行检查。245 allow_float = any(b.dtype is D.FLOAT64 and (b.constant or b.typed_constant or b.conversion_promoted246 or self.message_domain and not b.aliases) for b in (left, right))247 if not allow_float:248 self._error(node, 'where数值分支dtype不兼容')249 left, right = (b if b.dtype is D.FLOAT64 else self._known_node(_node('to_float', D.FLOAT64, b.unit, (b,)))250 for b in (left, right))251 return self._known_node(_node('where', left.dtype, left.unit, (condition, left, right)))252253 def visit(self, node, declared_unit=None):254 """解析已登记的v4单位、选择与数学函数,其余受限语法沿原访问器。255256 参数: node为AST节点;declared_unit为根目标标签。257 返回: 已检查类型、单位和依赖的表达式节点。258 异常: ValueError为签名、非字面量单位、非法数值或未知函数。259 """260 # 数学函数在编译期确定所有参数的单位和类型;属性调用仍由原白名单拒绝。261 if (self.extensions is not None and isinstance(node,ast.Call) and isinstance(node.func,ast.Name)262 and node.func.id in self.extensions.functions):263 from .extension_points import compile_host_function264 return compile_host_function(self,node,self.extensions.functions[node.func.id])265 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in MATH_FUNCTIONS:266 return compile_math(self, node)267 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'lookup':268 return compile_lookup(self, node)269 # 原码节点只接受带来源的枚举声明;普通整数函数独立检查类型及位宽。270 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'enum_code':271 return compile_enum_code(self, node)272 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in INTEGER_FUNCTIONS:273 return compile_integer(self, node)274 # where是控制求值的专门节点。275 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'where':276 return self._where(node)277 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ('quantity', 'to_unit'):278 if len(node.args) != 2 or node.keywords:279 self._error(node, f'{node.func.id}需要两个位置参数')280 unit_ast = node.args[1]281 if not isinstance(unit_ast, ast.Constant) or type(unit_ast.value) is not str:282 self._error(unit_ast, '单位必须为字符串字面量')283 target = units.resolve_unit(unit_ast.value)284 if target is None:285 self._error(unit_ast, 'quantity/to_unit需要已登记单位')286 child = self.visit(node.args[0])287 if child.dtype not in (*INTEGER_BOUNDS, D.FLOAT64):288 self._error(node, 'quantity/to_unit只支持数值')289 if node.func.id == 'quantity':290 # 带单位常量不再参与裸常量单位继承;保留原整数字面量精度。291 if child.operator != 'const' or not child.constant:292 self._error(node, 'quantity的数值必须为有限字面量')293 if child.dtype in INTEGER_BOUNDS and not INTEGER_BOUNDS[child.dtype][0] <= child.value <= INTEGER_BOUNDS[child.dtype][1]:294 self._error(node, 'quantity字面量超出64位范围')295 label = units.output_label(target, unit_ast.value)296 return replace(child, unit=label, constant=False, unit_explicit=True, typed_constant=True)297 converted = self._convert(child, unit_ast.value, node, explicit=True)298 # 显式选为无量纲的常量不能再被相邻信号解释为电压或电流。299 return replace(converted, constant=False, typed_constant=True) if child.constant else converted300 # 原访问器负责语法安全、逻辑、abs及isclose;新单位状态由同一目录确定。301 result = super().visit(node, declared_unit)302 return self._known_node(result) if result.args else result303