"""矩阵脚本(.mt)解析器 —— 移植自 bot4cj/src/P.cj 与仓颉版的差异(均为审计修复,语义不变): S01: submit_line 的列数校验修复为真实生效(仓颉版因 `!` 优先级恒假); S02: 移除逐字符 debug 打印,stdout 保持纯净,诊断走 logging(stderr); S04: 换行统一用 '\\n' 字符比较(Python 无 Rune/String 混淆问题); Q05: 越界访问抛 MatrixIndexError(携带块名/行/列),报错可读。 """ import logging log = logging.getLogger("pybot.parser") # ---------------- 解析异常 ---------------- class ParseError(Exception): """解析异常基类(对应仓颉版 ParseExcept)""" def __init__(self, msg, line): super().__init__(f"{msg}. we got line: {line}") self.line = line class DoesNotSync(ParseError): """行没对齐:某一行的列数与上一行不一致(修复 S01,携带期望/实际列数)""" def __init__(self, line, expected, actual): super().__init__( f"Matrix line does not sync (expected {expected} columns, got {actual})", line) self.expected = expected self.actual = actual class MatrixIndexError(Exception): """矩阵越界访问(修复 Q05,携带块名/行/列)""" def __init__(self, name, row, col): super().__init__(f"matrix @{name}: cell ({row}, {col}) out of range") self.name = name self.row = row self.col = col # ---------------- 矩阵编程语言 - 基本对象 ---------------- class MatrixObject: """一个 @块(名字 + 原始字符串矩阵)""" def __init__(self): self.name = "" # 块名(@ 后面的标识符) self.values = [] # 原始矩阵 list[list[str]](含标题行) self._tmp = [] # rune buf 字符缓冲区 self._grp = [] # one line 整行缓冲区 # -------- 是否已命名 -------- def has_name(self): return self.name != "" # -------- 行数(含标题行)/ 列数 -------- @property def size(self): return len(self.values) @property def col(self): return len(self.values[0]) if self.values else 0 # -------- 获取矩阵元素,只能取,不能修改 -------- def __getitem__(self, rc): row, col = rc try: return self.values[row][col] except IndexError: raise MatrixIndexError(self.name, row, col) from None # -------- 在某一行中寻找某一个字符串的列号 -------- def index_of_col(self, row, key): for c, item in enumerate(self.values[row]): if item == key: return c raise KeyError(f"column '{key}' not found in matrix @{self.name}") # -------- 储存对象(与仓颉版 MatrixObject.save 输出格式一致) -------- def save(self, out): out.write(f"@{self.name}\n") for vec in self.values: for item in vec: out.write(item) out.write(" ") out.write(";\n") out.write("@\n") # -------- FSM 提交接口(由 MatrixParser 驱动) -------- def add_rune(self, c): self._tmp.append(c) def submit_name(self): if not self._tmp: return self.name = "".join(self._tmp) self._tmp.clear() def submit_item(self): if not self._tmp: return self._grp.append("".join(self._tmp)) self._tmp = [] def submit_line(self, count): if not self._grp: return # 修复 S01:仓颉版 `!values.size == 0 && !grp.size == ...` 因优先级恒假 if self.values and len(self._grp) != len(self.values[-1]): raise DoesNotSync(count, len(self.values[-1]), len(self._grp)) self.values.append(self._grp) self._grp = [] # ---------------- 矩阵式解析器(FSM 逐字符,状态与仓颉版一致) ---------------- class MatrixParser: """输入文本流,逐块产出 MatrixObject(一次 parse() 解析一个 @块)""" def __init__(self, source): self.reset(source) def reset(self, source): """切换目标输入;source 为 str 或带 read() 的文本流""" self._text = source if isinstance(source, str) else source.read() self._pos = 0 self._flag = False # 结束标志 self._nl = True # new line self._cm = False # comment self._nm = False # name-line self.obj = MatrixObject() def parse(self): """解析下一个 @块;输入耗尽且无新块时返回 None""" count = 1 # 行计数器始于 1 self._flag = False # 重置结束标志 self.obj = MatrixObject() # 一次只解析一个对象 while not self._flag and self._pos < len(self._text): c = self._text[self._pos] self._pos += 1 count = self._next(c, count) if not self.obj.has_name(): # 意外终止(无新对象) return None log.debug("parsed block @%s: %d rows x %d cols", self.obj.name, self.obj.size, self.obj.col) return self.obj def _next(self, c, ct): """FSM 状态转移,与仓颉版 P.cj _next() 逐分支对齐(S02:无逐字符打印)""" count = ct # ------------ COMMENTS ------------ if not self._cm and c == '#': # 注释开始时 self._cm = True elif self._cm and c == '\n': # 注释结束时 self._nl = True self._cm = False count += 1 elif self._cm: # 注释进行时 pass # ------------ new line ------------ elif self._nl and c in ' \t\r': # 新行空字符,维持现状 pass elif self._nl and c == '\n': # 新行空行 count += 1 elif self._nl and c == '@' and not self.obj.has_name(): self._nl = False self._nm = True elif self._nl and c == '@' and self.obj.has_name(): self._flag = True # 块结束 self._nl = False self._nm = True elif self._nl: # 新行内容 self._nl = False self.obj.add_rune(c) # ------------- not nl ------------- elif self._nm and c in ' \t\n': # 添加名字 self.obj.submit_name() self._nl = True self._nm = False elif not self._nm and c in ' \t\n': # 添加命名项(S04:'\n' 统一处理) self.obj.submit_item() if c == '\n': count += 1 elif c == ';': # 普通行结束 self.obj.submit_item() self.obj.submit_line(count) self._nl = True else: # 非空格、非制表符、非分号 self.obj.add_rune(c) return count # ---------------- 矩阵式储存器 ---------------- class MatrixSaver: """接收 MatrixObject 容器与输出流,序列化回 .mt 格式""" def __init__(self, out): self._out = out def save(self, objects): for item in objects: item.save(self._out) self._out.write("\n") # 对象之间空一行 # ---------------- 占位符语义(与仓颉版 MatrixVars_Hardware.getMt/parseMt 对齐) ---------------- # # 首行 '-' 数值列解析为 0(Servo 有 max/min 钳位保护)。 # actpred/mt_parser.py 曾用 @Servo max 列填充首行 '-',2026-08 已统一 # 为本语义(填 0),见 docs/reports/bot4cj_python_migration_dev_doc.md §2.3。 def get_mt(obj, row, col): """获得项目值:原始值是占位符且非首行(row>1),递归取上一行同列值""" raw = obj[row, col] if raw == "-" and row > 1: return get_mt(obj, row - 1, col) return raw def parse_mt(obj, row, col, typ=float): """解析项目值:占位符递归向上;首行(row==1)占位符解析为 typ("0")""" raw = obj[row, col] if raw == "-" and row > 1: return parse_mt(obj, row - 1, col, typ) if raw == "-": return typ("0") return typ(raw) def to_int(s): """整数解析,支持 0x 十六进制前缀(@PCA9685 address、@MPU6050_BIND addr 列)""" s = s.strip() if s.lower().startswith("0x") or s.lower().startswith("-0x"): return int(s, 16) return int(s)