Files
ViperC/App/lib/core/Diag.vp
T
TermiNexus a23c1bda3e fix(viperc/diag): 调用栈帧与错误点同位置时不再重复打印
现象:`_start` 定义行在 Traceback 里被打印两遍 —— 栈内最内层帧与随后的“错误点帧”
落在同一 file:line(错误的 AST 节点 lineno 为 0 时,错误点位置会回退到栈最内层帧,
于是与栈帧完全重合)。

Diag.emit 增加两条去重规则(DIAG_DIR_UP / DIAG_DIR_DOWN 两个方向都生效):
① 连续两个位置完全相同的栈帧只印一次;
② 与紧随其后的“错误点帧”(emit_current 打印的那一帧)位置相同的栈帧不再重复印。
2026-10-14 15:53:05 +08:00

336 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()