chronaxis.core.expression_math_v4
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""编译版本4的逐点极值、限幅、平方和开方表达式。23输入是白名单调用AST及本次编译器,输出是显式标记单位、dtype与依赖的4冻结节点。先检查完整输入,再按所有参数的原始声明尺度解释裸常量,5最后确定公共数值表示。仅对不依赖样本的clip边界执行静态判序。6"""78from dataclasses import replace9from . import unit_registry as units10from .expression import ExprNode, _node, contains_host_call11from .numeric import INTEGER_BOUNDS12from .types import SignalDType as D131415MATH_KERNELS = frozenset(('minimum', 'maximum', 'clip', 'sqrt'))16MATH_FUNCTIONS = MATH_KERNELS | {'square'} # square展开成已有的定宽乘法。171819def _common_unit(compiler, args, call):20 """将同量种的多个数值放到共同尺度,在换算前确定裸数字单位。2122 参数: compiler为v4编译器,args为数值节点,call用于错误定位。23 返回: 同单位节点tuple,已知单位取规范基准,未知同名保留原标签。24 异常: ValueError为量种不兼容、未知单位异名或裸数字有多种尺度解释。25 """26 # 不把尚无单位的裸常量本身当成单位依据;quantity及消息参数自带声明。27 anchors = [arg for arg in args if not arg.constant]28 if not anchors:29 return args30 first = anchors[0]31 origin = units.resolve_unit(first.unit)32 target = units.canonical_unit(origin).symbol if origin is not None else first.unit33 has_bare = len(anchors) != len(args)34 for arg in anchors[1:]:35 # 检查所有有单位输入,交换参数不能改变兼容与否或裸数字的物理含义。36 other = units.resolve_unit(arg.unit)37 if origin is None or other is None:38 if arg.unit != first.unit:39 compiler._error(call, '不同的未知单位不能自动比较')40 continue41 try:42 units.conversion(origin, other)43 except ValueError as error:44 compiler._error(call, str(error))45 if has_bare and (other.scale, other.offset) != (origin.scale, origin.offset):46 compiler._error(call, '裸数值存在多个声明尺度,请使用quantity指定单位')47 # 所有锚点的尺度已相同,或没有裸常量;已知量种选择的目标与参数顺序无关。48 annotated = (replace(arg, unit=first.unit, unit_status=first.unit_status) if arg.constant else arg49 for arg in args)50 return tuple(compiler._convert(arg, target, call) for arg in annotated)515253def _common_dtype(compiler, args, call):54 """对称选定可用于极值和限幅的公共dtype,明确记录必要的精确提升。5556 参数: compiler为v4编译器,args已统一单位,call用于错误定位。57 返回: 逐一适配后的节点tuple;所有节点dtype相同。58 异常: ValueError为原信号类型冲突、定宽字面量越界或不允许的提升。59 """60 # 原信号之间若已经不兼容,不能借第三个浮点字面量隐式重写其dtype。61 fixed_types = {arg.dtype for arg in args if not (arg.constant or arg.typed_constant62 or (arg.conversion_promoted and arg.dtype is D.FLOAT64)63 or (compiler.message_domain and not arg.aliases and arg.dtype is D.FLOAT64))}64 if len(fixed_types) > 1:65 compiler._error(call, '数值函数输入的信号dtype不兼容')66 if any(arg.dtype is D.FLOAT64 for arg in args):67 dtype = D.FLOAT6468 elif fixed_types:69 dtype = next(iter(fixed_types))70 else:71 dtype = D.UINT64 if any(arg.dtype is D.UINT64 for arg in args) else D.INT6472 # 先按字面量规则适配;只有浮点输出才需要对真实整数输入做精度检查。73 anchor = ExprNode('const', dtype, '1', 'verified', value=0, constant=True)74 result = []75 for arg in args:76 child = compiler._context(arg, anchor, call, arithmetic=True)77 if child.dtype is not dtype:78 if dtype is not D.FLOAT64:79 compiler._error(call, '数值函数输入无法使用共同的整数dtype')80 child = compiler._known_node(_node('to_float', D.FLOAT64, child.unit, (child,)))81 result.append(child)82 return tuple(result)838485def check_integer_leaves(compiler, args, call):86 """检查实际需要定宽存储的叶节点,包括未选分支内的配置字面量。8788 参数: compiler为v4编译器,args为要检查的表达式树,call用于定位。89 返回: None。异常: ValueError为叶节点定宽越界。90 """91 # 共享派生子树只访问一次。允许无界的纯整数比较阈值已编入专用节点字段。92 pending, seen = list(args), set()93 while pending:94 child = pending.pop()95 if id(child) in seen:96 continue97 seen.add(id(child))98 if (child.operator == 'const' and child.dtype in INTEGER_BOUNDS99 and not INTEGER_BOUNDS[child.dtype][0] <= child.value <= INTEGER_BOUNDS[child.dtype][1]):100 compiler._error(call, '数值函数输入的整数常量超出64位范围')101 pending.extend(child.args)102103104def compile_math(compiler, call):105 """为一个已注册的数学函数检查签名、单位、类型和静态边界。106107 参数: compiler为版本4编译器,call为裸函数名调用AST。108 返回: ExprNode;平方直接复用经过定宽溢出检查的乘法节点。109 异常: ValueError为非法参数、越界常量、不可推导的单位或静态倒置边界。110 """111 name = call.func.id112 count = len(call.args)113 if name not in MATH_FUNCTIONS or call.keywords:114 compiler._error(call, '数学函数仅支持已登记名称与显式位置参数')115 if ((name in ('minimum', 'maximum') and count < 2) or (name == 'clip' and count != 3)116 or (name in ('sqrt', 'square') and count != 1)):117 compiler._error(call, f'{name}位置参数数量不正确')118 args = tuple(compiler.visit(arg) for arg in call.args)119 if any(arg.dtype not in (*INTEGER_BOUNDS, D.FLOAT64) for arg in args):120 compiler._error(call, f'{name}的全部参数必须为数值')121 check_integer_leaves(compiler, args, call)122123 if name in ('square', 'sqrt'):124 # 必须先识别偏移温度,否则转成K后会绕过显式转换契约。125 child = args[0]126 unit = units.resolve_unit(child.unit)127 if unit is None or unit.offset:128 compiler._error(call, f'{name}需要已登记的无偏移单位,摄氏度须先显式转换为K')129 if name == 'square':130 return compiler._binary('mul', child, child, call)131 # mA*A先换成A²再开方,根号内倍率不必恰为有理数的平方。132 child = compiler._canonical(child, call)133 try:134 output = units.canonical_unit(units.square_root(units.canonical_unit(unit))).symbol135 except ValueError as error:136 compiler._error(call, str(error))137 return compiler._known_node(_node('sqrt', D.FLOAT64, output, (child,)))138139 args = _common_dtype(compiler, _common_unit(compiler, args, call), call)140 if name == 'clip' and all(not a.aliases and not contains_host_call(a) for a in args[1:]):141 # 参数及字面量表达式在编译时已有值。只判定两个有效边界间可证明的倒置;142 # 数值错误(如除零)仍由原节点给出具体质量,不伪造成边界顺序错误。143 from .expression_reference import _evaluate144 cache = {}145 lower, upper = (_evaluate(bound, {}, cache).value for bound in args[1:])146 if lower.valid and upper.valid and lower.value > upper.value:147 compiler._error(call, 'clip的静态下界大于上界')148 return compiler._known_node(_node(name, args[0].dtype, args[0].unit, args))149