from stdint import * import string import stdio import stdlib import win32.fileio as fileio import lib.core.VLogger as VLogger # ============================================================ # Diag - 编译期诊断(traceback 系统):源码行读取 + 统一打印 + 终止策略 # ============================================================ # 抽出来的原因:格式化 / 源码行读取 / 终止策略原先都散在 TypeGenErr 里(连同它的 # TraceCount 调用栈遍历),导致"多错误栈"(错误收集 + 每单元去重 + 上限)无处安放。 # 现在 TypeGenErr / GenApi 的 `fatal_error` / `dump_parse_traceback` 全部**委托**到这里; # 保留在 TypeGenErr 里的只有两件"它才知道"的东西: # ① 调用栈帧的遍历(`TypeGenBase._get_trace_frame`); # ② 当前文件名(`TypeGenResolve.get_current_file`)。 # # **行为与抽离前逐字一致**:报文顺序、缩进、caret 规则、fflush、repl_abort 全不变。 # 诊断模型:**单条 + 立即终止**(不收集多条错误)—— 但单条自带**整条调用链**(DiagEntry), # 由调用方(TypeGenErr)填好 entry 后 emit,方向由 DIAG_DIR_* 决定。所有打印都拆成独立的 # emit_*,便于在不改调用点的前提下调整格式。 # ============================================================ # ---- traceback 打印方向 ---- define DIAG_DIR_UP = 0 # 外→内:最外层触发点在上,错误点最后(Python "most recent call last") define DIAG_DIR_DOWN = 1 # 内→外:错误点最先,越往外越下 define DIAG_MAX_FRAMES = 32 # 调用链上限(与 TypeGenMixin.TRACE_MAX 对齐) # ============================================================ # DiagEntry - 单条编译期诊断(= 一次 traceback 要打印的全部内容) # ============================================================ # **不是**错误收集容器:任何时刻只处理"出错的那一条"(打印完即终止), # 但这一条自带**整条调用链** —— 编译器逐层下潜进各源码上下文 # (import 触发依赖加载 / 泛型特化 / 函数体翻译)时的触发点, # 用于"从错误发生点向外展示触发过程"。 # 所有字符串都用 stdlib 深拷贝:编译期池会回绕,不能借用池内指针。 struct DiagEntry: # 字段名不要与"本模块 import 的类名"重名:TPC 的类型/符号表按**非限定名**索引 # (Gen.structs / SymbolTable / _mangle_func_name),曾把本字段的 `File` 与 # `win32.fileio.File` 撞在一起,导致同模块 `fileio.File(...)` 的构造调用把 # `File.__before_init__` 的 self 形参解析成 `i8**`(llvmlite: "Type of #1 arg # mismatch: i8** != %"...fileio.File"*")。故此处用 FileName 而非 File。 FileName: str # 当前帧(错误点)文件 Line: int # 当前帧行号 ColStart: int # caret 起始列(1-based;0 = 不画) ColEnd: int # caret 结束列 Category: str # LifetimeError / SyntaxError / TypeError ... Msg: str # 消息 Hint: str # 可选提示(可 None) FrameCount: int # 调用链帧数 FrameFiles: sizet* # 调用链每帧文件名(深拷贝) FrameLines: sizet* # 调用链每帧行号(与 FrameFiles 同容量) # 泄漏点(可选):被调者那边**造成逃逸的写入点**。只有逃逸类 LifetimeError 会有值; # 打印成独立一帧 `File lifetime leak "", line N`,与调用栈帧(`File "..."`)区分。 LeakFileName: str LeakLine: int Cap: int # 内部:FrameFiles/FrameLines 容量(不参与打印) # dup_str - 深拷贝到 stdlib 堆(诊断需跨池回绕点存活) def dup_str(s: str) -> str: if s is None: return None n: sizet = string.strlen(s) b: str = str(stdlib.malloc(n + 1)) if b is None: return None string.strncpy(b, s, n) b[n] = '\0' return b # new_entry - 分配一条诊断(调用方持有;打印完用 free_entry 释放) def new_entry(max_frames: int) -> DiagEntry*: ent: DiagEntry* = (DiagEntry*)(stdlib.malloc(DiagEntry.__sizeof__())) if ent is None: return None if max_frames <= 0: max_frames = 1 ent.FileName = None ent.Line = 0 ent.ColStart = 0 ent.ColEnd = 0 ent.Category = None ent.Msg = None ent.Hint = None ent.FrameCount = 0 ent.LeakFileName = None ent.LeakLine = 0 ent.Cap = max_frames ent.FrameFiles = (sizet*)(stdlib.malloc(sizet(max_frames) * 8)) ent.FrameLines = (sizet*)(stdlib.malloc(sizet(max_frames) * 8)) return ent # begin - 初始化一条诊断(随后用 add_frame/set_current 填调用链与当前位置) def begin(ent: DiagEntry*, category: str, msg: str, hint: str = None) -> int: if ent is None: return 0 ent.Category = dup_str(category) ent.Msg = dup_str(msg) ent.Hint = dup_str(hint) ent.FileName = None ent.Line = 0 ent.ColStart = 0 ent.ColEnd = 0 ent.FrameCount = 0 ent.LeakFileName = None ent.LeakLine = 0 return 0 # set_current - 设置当前帧(错误点)位置 def set_current(ent: DiagEntry*, file_name: str, lineno: int, col_start: int, col_end: int) -> int: if ent is None: return 0 ent.FileName = dup_str(file_name) ent.Line = lineno ent.ColStart = col_start ent.ColEnd = col_end return 0 # set_leak - 设置"泄漏点"帧(File lifetime leak);仅逃逸类 LifetimeError 使用 def set_leak(ent: DiagEntry*, file_name: str, lineno: int) -> int: if ent is None or lineno <= 0: return 0 ent.LeakFileName = dup_str(file_name) ent.LeakLine = lineno return 0 # add_frame - 追加调用链一帧(按"自外向内"顺序追加) def add_frame(ent: DiagEntry*, file_name: str, lineno: int) -> int: if ent is None or ent.FrameFiles is None or ent.FrameLines is None: return 0 if ent.FrameCount >= ent.Cap: return 0 ent.FrameFiles[ent.FrameCount] = sizet(dup_str(file_name)) ent.FrameLines[ent.FrameCount] = sizet(lineno) ent.FrameCount += 1 return 0 # emit - 打印一条诊断:头 → 调用链(按 direction) → 当前帧(caret) → 类别: 消息 # 去重:① 连续两个位置完全相同的栈帧只印一次;② 与“错误点帧”位置相同的栈帧不再重复印 #(此前常见 `_start` 定义行被打印两遍:栈内最内层帧与错误点帧落在同一行)。 def emit(ent: DiagEntry*, direction: int) -> int: if ent is None: return 0 emit_header() last_file: str = "" last_line: int = -1 if ent.FrameFiles is not None and ent.FrameLines is not None: if direction == DIAG_DIR_DOWN: i_d: int = ent.FrameCount - 1 while i_d >= 0: ff: str = str(ent.FrameFiles[i_d]) ll: int = int(ent.FrameLines[i_d]) dup: int = 1 if last_file != "" and string.strcmp(ff, last_file) == 0 and ll == last_line else 0 if dup == 0: emit_frame(ff, ll) last_file = ff last_line = ll i_d -= 1 else: i_u: int = 0 while i_u < ent.FrameCount: ff2: str = str(ent.FrameFiles[i_u]) ll2: int = int(ent.FrameLines[i_u]) dup2: int = 1 if last_file != "" and string.strcmp(ff2, last_file) == 0 and ll2 == last_line else 0 cur_same: int = 1 if ent.FileName is not None and string.strcmp(ff2, ent.FileName) == 0 and ll2 == ent.Line else 0 if dup2 == 0 and cur_same == 0: emit_frame(ff2, ll2) last_file = ff2 last_line = ll2 i_u += 1 emit_current(ent.FileName, ent.Line, ent.ColStart, ent.ColEnd) # 泄漏点帧紧跟在"错误点"之后(往下再一行),再印类别: 消息 if ent.LeakLine > 0: emit_leak_frame(ent.LeakFileName, ent.LeakLine) emit_category(ent.Category, ent.Msg) return 0 # free_entry - 释放一条诊断(含深拷贝的字符串) def free_entry(ent: DiagEntry*) -> int: if ent is None: return 0 if ent.FileName is not None: stdlib.free(ent.FileName) if ent.LeakFileName is not None: stdlib.free(ent.LeakFileName) if ent.Category is not None: stdlib.free(ent.Category) if ent.Msg is not None: stdlib.free(ent.Msg) if ent.Hint is not None: stdlib.free(ent.Hint) if ent.FrameFiles is not None: i_f: int = 0 while i_f < ent.FrameCount: if ent.FrameFiles[i_f] != 0: stdlib.free(str(ent.FrameFiles[i_f])) i_f += 1 stdlib.free(ent.FrameFiles) if ent.FrameLines is not None: stdlib.free(ent.FrameLines) stdlib.free(ent) return 0 # read_source_line - 读源文件第 lineno 行,供 traceback 展示 def read_source_line(file_path: str, lineno: int) -> str: """读第 lineno 行;返回 stdlib 分配的缓冲(调用方 free),读不到返回 None""" if file_path is None or lineno <= 0: return None f: fileio.File* = fileio.File(file_path, fileio.MODE.R) if f.closed: return None # Allocate by actual file size: a fixed 256KB buffer truncated large files so late line # numbers were unreadable. The 64MB cap guards against reading abnormally huge files. fsize: LONGLONG = f.size() if fsize <= 0 or fsize > 67108864: f.close() return None src_size: sizet = sizet(fsize) buf: bytes = stdlib.malloc(src_size + 1) if buf is None: f.close() return None bytes_read: LONG = f.read_all(buf, ULONG(src_size + 1)) f.close() if bytes_read <= 0: stdlib.free(buf) return None # Scan line by line for the target lineno cur_line: int = 1 line_start: sizet = 0 for i in range(int(bytes_read)): if buf[i] == '\n': if cur_line == lineno: line_len: sizet = i - line_start if line_len > 0 and buf[line_start + line_len - 1] == '\r': line_len -= 1 line_buf: bytes = stdlib.malloc(line_len + 1) if line_buf is not None: string.strncpy(line_buf, buf + line_start, line_len) line_buf[line_len] = '\0' stdlib.free(buf) return line_buf stdlib.free(buf) return None cur_line += 1 line_start = i + 1 stdlib.free(buf) return None # emit_header - traceback 头(解析失败与语义错误共用同一行文案) def emit_header() -> int: stdio.printf("Traceback (most recent call last, compile-time error):\n") return 0 # emit_frame - 调用栈里的一帧(只印位置与源码行,不印 caret) def emit_frame(file_name: str, lineno: int) -> int: if file_name is None: return 0 if lineno > 0: stdio.printf(' File "%s", line %d\n', file_name, lineno) src: str = read_source_line(file_name, lineno) if src is not None: stdio.printf(" %s\n", src) stdlib.free(src) else: stdio.printf(' File "%s"\n', file_name) return 0 # emit_leak_frame - "泄漏点"帧:标签用 `File lifetime leak`,与调用栈帧(`File "..."`)区分 def emit_leak_frame(file_name: str, lineno: int) -> int: if file_name is None: return 0 if lineno > 0: stdio.printf(' File lifetime leak "%s", line %d\n', file_name, lineno) src: str = read_source_line(file_name, lineno) if src is not None: stdio.printf(" %s\n", src) stdlib.free(src) else: stdio.printf(' File lifetime leak "%s"\n', file_name) return 0 # emit_current - 当前帧(带 caret 列范围;行读不出时印警告) def emit_current(file_name: str, lineno: int, col_start: int, col_end: int) -> int: # Sanity defense: read the line first to verify lineno belongs to the current file. # If unreadable the file/line is untrustworthy (e.g. reused AST); print a warning instead. src_line: str = None line_ok: int = 1 if lineno > 0 and string.strcmp(file_name, "") != 0: src_line = read_source_line(file_name, lineno) if src_line is None: line_ok = 0 if line_ok == 0: stdio.printf(' File "%s", line %d (!! cannot read that source line: file does not exist or line number out of range; the error may come from a reused AST tree, rely on the error message)\n', file_name, lineno) else: stdio.printf(' File "%s", line %d\n', file_name, lineno) if src_line is not None: stdio.printf(" %s\n", src_line) # Column-range marker: use ^ under the source line to mark the error range if col_start > 0 and col_end >= col_start: stdio.printf(" ") j: int for j in range(int(col_end)): if j >= col_start - 1 and j < col_end: stdio.printf("^") else: stdio.printf(" ") stdio.printf("\n") stdlib.free(src_line) return 0 # emit_category - `类别: 消息`,随后立即 fflush def emit_category(err_type: str, msg: str) -> int: if err_type is None or err_type[0] == 0: err_type = "SyntaxError" stdio.printf("%s: %s\n", err_type, msg) # Force flush before exiting: stdout redirected to a file is fully buffered, so terminating # directly drops the whole traceback (observed: 2.log kept only one VLogger line). stdio.fflush(None) return 0 # emit_parse - 解析失败的单帧 traceback(Python 风格,单个 ^ 指向 token 起始列) def emit_parse(file_path: str, err_line: int, err_col: int, err_msg: str) -> int: emit_header() if file_path is None or file_path[0] == 0: file_path = "" if err_line > 0: stdio.printf(' File "%s", line %d\n', file_path, err_line) src: str = read_source_line(file_path, err_line) if src is not None: stdio.printf(" %s\n", src) # Column marker: put ^ under the error token's start column (parser col_offset is # 0-based, so 4-space indent + err_col spaces lands on the token's first char) if err_col > 0: stdio.printf(" ") for _ in range(int(err_col)): stdio.printf(" ") stdio.printf("^\n") stdlib.free(src) else: stdio.printf(' File "%s"\n', file_path) if err_msg is None or err_msg[0] == 0: err_msg = "syntax error" stdio.printf("SyntaxError: %s\n", err_msg) return 0 # finish - 终止策略:REPL 里 longjmp 回循环(报文已印、进程存活),批处理里 sys.exit(1) def finish() -> None: VLogger.repl_abort()