Chronaxis用户手册English
本页目录

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
Chronaxis 用户手册 · 离线阅读