现象:`_start` 定义行在 Traceback 里被打印两遍 —— 栈内最内层帧与随后的“错误点帧” 落在同一 file:line(错误的 AST 节点 lineno 为 0 时,错误点位置会回退到栈最内层帧, 于是与栈帧完全重合)。 Diag.emit 增加两条去重规则(DIAG_DIR_UP / DIAG_DIR_DOWN 两个方向都生效): ① 连续两个位置完全相同的栈帧只印一次; ② 与紧随其后的“错误点帧”(emit_current 打印的那一帧)位置相同的栈帧不再重复印。
336 lines
15 KiB
Plaintext
336 lines
15 KiB
Plaintext
import t, c
|
||
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 深拷贝:编译期池会回绕,不能借用池内指针。
|
||
class 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 "<file>", 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, "<unknown>") != 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 = "<unknown>"
|
||
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()
|