From 6769cf033519e2ff094f31ca391e93d193690835 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 9 Oct 2026 18:45:06 +0800 Subject: [PATCH] =?UTF-8?q?refactor(diag):=20=E7=BC=96=E8=AF=91=E6=9C=9F?= =?UTF-8?q?=20traceback=20=E6=8A=BD=E4=B8=BA=20Diag=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=8C=E5=8D=95=E6=9D=A1=E8=AF=8A=E6=96=AD=E6=90=BA=E5=B8=A6?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E8=B0=83=E7=94=A8=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Diag.DiagEntry 与 new_entry/begin/set_current/add_frame/emit/free_entry,方向由 DIAG_DIR_* 决定 - TypeGenErr.fatal_error 改走 DiagEntry:填调用链 + 当前帧,emit(UP) 后 free + finish - 顶层函数体/嵌套函数体/方法体三处翻译入口各 push/pop 一帧(def 位置),报错可回溯所在函数 --- App/lib/core/Diag.vp | 281 +++++++++++++++++++++++ App/lib/core/Gen/Class/ClassGenVTable.vp | 7 +- App/lib/core/Gen/Stmt/StmtGenMixin.vp | 10 +- App/lib/core/Gen/TranslatorGenMixin.vp | 5 + App/lib/core/Gen/Type/TypeGenErr.vp | 145 +++--------- 5 files changed, 336 insertions(+), 112 deletions(-) create mode 100644 App/lib/core/Diag.vp diff --git a/App/lib/core/Diag.vp b/App/lib/core/Diag.vp new file mode 100644 index 0000000..fe76b2b --- /dev/null +++ b/App/lib/core/Diag.vp @@ -0,0 +1,281 @@ +import t, c +from stdint import * +import string +import stdio +import stdlib +import w32.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: + File: 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 同容量) + 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.File = None + ent.Line = 0 + ent.ColStart = 0 + ent.ColEnd = 0 + ent.Category = None + ent.Msg = None + ent.Hint = None + ent.FrameCount = 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.File = None + ent.Line = 0 + ent.ColStart = 0 + ent.ColEnd = 0 + ent.FrameCount = 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.File = dup_str(file_name) + ent.Line = lineno + ent.ColStart = col_start + ent.ColEnd = col_end + 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) → 类别: 消息 +def emit(ent: DiagEntry*, direction: int) -> int: + if ent is None: return 0 + emit_header() + 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: + emit_frame(str(ent.FrameFiles[i_d]), int(ent.FrameLines[i_d])) + i_d -= 1 + else: + i_u: int = 0 + while i_u < ent.FrameCount: + emit_frame(str(ent.FrameFiles[i_u]), int(ent.FrameLines[i_u])) + i_u += 1 + emit_current(ent.File, ent.Line, ent.ColStart, ent.ColEnd) + emit_category(ent.Category, ent.Msg) + return 0 + + +# free_entry - 释放一条诊断(含深拷贝的字符串) +def free_entry(ent: DiagEntry*) -> int: + if ent is None: return 0 + if ent.File is not None: stdlib.free(ent.File) + 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(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_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(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(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() diff --git a/App/lib/core/Gen/Class/ClassGenVTable.vp b/App/lib/core/Gen/Class/ClassGenVTable.vp index ba80a85..285814a 100644 --- a/App/lib/core/Gen/Class/ClassGenVTable.vp +++ b/App/lib/core/Gen/Class/ClassGenVTable.vp @@ -402,7 +402,12 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase): if stmt is None: continue if stmt.kind() == ast.ASTKind.FunctionDef: fd: ast.FunctionDef* = ast.FunctionDef*(stmt) - if fd is not None and fd.name is not None: self._translate_method(trans, fd, struct_ty, class_name) + if fd is not None and fd.name is not None: + # 方法体翻译 = 下潜进一个源码上下文:推一帧(def 位置),供 traceback 回溯。 + tr_mf: str = self.Gen.TypeGenResolve.get_current_file() + self.Gen.TypeGenBase.push_trace(tr_mf, fd.lineno) + self._translate_method(trans, fd, struct_ty, class_name) + self.Gen.TypeGenBase.pop_trace() # If there is a vtable, generate it before __before_init__ (which reads VTableMethodCount); # check via oop_entry directly to avoid cross-module same-name lookups. diff --git a/App/lib/core/Gen/Stmt/StmtGenMixin.vp b/App/lib/core/Gen/Stmt/StmtGenMixin.vp index 2102d0a..bcf801e 100644 --- a/App/lib/core/Gen/Stmt/StmtGenMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenMixin.vp @@ -48,7 +48,15 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin): elif k == ast.ASTKind.AnnAssign: return self.Gen.AssignGen.translate_annassign(trans, node) elif k == ast.ASTKind.FunctionDef: # nested function def: hoist to a top-level function and build a closure - return self.Gen.FuncGen.translate_nested_function_def(trans, node) + # 函数体翻译 = 下潜进一个源码上下文:推一帧(def 位置),供 traceback 回溯。 + nfd_t: ast.FunctionDef* = ast.FunctionDef*(node) + tr_nf: str = self.Gen.TypeGenResolve.get_current_file() + tr_nl: int = 0 + if nfd_t is not None: tr_nl = nfd_t.lineno + self.Gen.TypeGenBase.push_trace(tr_nf, tr_nl) + rc_nf: int = self.Gen.FuncGen.translate_nested_function_def(trans, node) + self.Gen.TypeGenBase.pop_trace() + return rc_nf elif k == ast.ASTKind.Return: return self.Gen.StmtGenCif.translate_return(trans, node) elif k == ast.ASTKind.If: return self.Gen.StmtGenCif.translate_if(trans, node) elif k == ast.ASTKind.While: diff --git a/App/lib/core/Gen/TranslatorGenMixin.vp b/App/lib/core/Gen/TranslatorGenMixin.vp index b7be654..b59742a 100644 --- a/App/lib/core/Gen/TranslatorGenMixin.vp +++ b/App/lib/core/Gen/TranslatorGenMixin.vp @@ -67,7 +67,12 @@ novirtual class TranslatorGenMixin: if fd_tc is not None and fd_tc.type_params is not None: continue # Phase 1a declare mode registers only CExport/State names; Phase 1b fully translates bodies. if trans._declare_only == 0: + # 函数体翻译 = 编译器下潜进一个源码上下文:推一帧(def 位置), + # 让函数体内部的错误在 traceback 里回溯到"正在翻译哪个函数"(调用链)。 + tr_f: str = self.Gen.TypeGenResolve.get_current_file() + self.Gen.TypeGenBase.push_trace(tr_f, fd_tc.lineno) added: int = self.Gen.FuncGenDef.translate_function_def(trans, child) + self.Gen.TypeGenBase.pop_trace() added_total += added elif trans._declare_only == 1: self._register_cexport_from_funcdef(trans, child) diff --git a/App/lib/core/Gen/Type/TypeGenErr.vp b/App/lib/core/Gen/Type/TypeGenErr.vp index 3d9eaec..c5b768e 100644 --- a/App/lib/core/Gen/Type/TypeGenErr.vp +++ b/App/lib/core/Gen/Type/TypeGenErr.vp @@ -12,6 +12,7 @@ import stdlib import sys import w32.fileio as fileio import lib.core.VLogger as VLogger +import lib.core.Diag as Diag import lib.Stub.StubMerger as StubMerger import lib.core.CompilerFactory as CompilerFactory import lib.core.Gen.Translator as HT @@ -26,90 +27,21 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): """Error reporting / source-line reading / type inference (fatal_error/infer_* etc.)""" def _read_source_line(self, file_path: str, lineno: int) -> str: - """Read line lineno from the source file""" - 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(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 + """Read line lineno from the source file (delegates to the Diag module)""" + return Diag.read_source_line(file_path, lineno) def read_source_line(self, file_path: str, lineno: int) -> str: """Public wrapper: read line lineno from the source file for traceback display""" return self._read_source_line(file_path, lineno) def dump_parse_traceback(self, file_path: str) -> None: - """On parse failure, print a Python-style traceback (like fatal_error)""" - err_line: int = ast.get_parse_err_line() - err_col: int = ast.get_parse_err_col() - err_msg: str = ast.get_parse_err_msg() - stdio.printf("Traceback (most recent call last, compile-time error):\n") - 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 = self._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(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) - # 注:返回类型必须显式给一个**真实类型**(int),不能留空、也不能用 None。 - # 本方法是"必定终止"的 fail-fast,从不返回;但留空/None 都会让跨模块 call site 解析出 - # 退化类型 i0,命中 CallGenCall.vp 的 i0 守卫 → _fail_method_ret_ty("fatal_error") 中止编译。 - # 给 int 后解析为 i32,守卫不再命中;因从不返回,该类型不会被真正使用。 + """On parse failure, print a Python-style traceback (delegates to the Diag module)""" + Diag.emit_parse(file_path, ast.get_parse_err_line(), ast.get_parse_err_col(), ast.get_parse_err_msg()) + # 注:返回类型必须显式给一个**真实类型**(int),不能留空、也不能用 None。 + # 本方法是"必定终止"的 fail-fast,从不返回;但留空/None 都会让跨模块 call site 解析出 + # 退化类型 i0,命中 CallGenCall.vp 的 i0 守卫 → _fail_method_ret_ty("fatal_error") 中止编译。 + # 给 int 后解析为 i32,守卫不再命中;因从不返回,该类型不会被真正使用。 def fatal_error(self, node: ast.AST*, msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0) -> int: - """Report an error in Python style and exit (supports multiple frames, error type, column range).""" - stdio.printf("Traceback (most recent call last, compile-time error):\n") - # Print the call stack frames (bottom to top) - for i in range(self.Gen.TypeGenBase.TraceCount): - frame: GenTypes.TraceFrame* = self.Gen.TypeGenBase._get_trace_frame(i) - if frame is not None and frame.file_name is not None: - if frame.lineno > 0: - stdio.printf(' File "%s", line %d\n', frame.file_name, frame.lineno) - src: str = self._read_source_line(frame.file_name, frame.lineno) - if src is not None: - stdio.printf(" %s\n", src) - stdlib.free(src) - else: stdio.printf(' File "%s"\n', frame.file_name) - # Print the current frame (position corresponding to node) + """Report an error in Python style and exit (single diagnostic carrying its full call chain).""" + # 当前帧(错误点)位置:优先 node,其次调用栈最内层有行号的帧,最后 current_file。 lineno: int = 0 if node is not None: lineno = node.lineno file_name: str = self.Gen.TypeGenResolve.get_current_file() @@ -125,39 +57,32 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): lineno = tfb.lineno break ti_b -= 1 - # Sanity defense: read the line first to verify node.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 = self._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(col_end): - if j >= col_start - 1 and j < col_end: stdio.printf("^") - else: stdio.printf(" ") - stdio.printf("\n") - stdlib.free(src_line) - # Output the error type + message - 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) + + # 单条诊断(自带到错误的完整调用链);打印交给 Diag。 + ent: Diag.DiagEntry* = Diag.new_entry(Diag.DIAG_MAX_FRAMES) + if ent is None: + # 极端情况(分配失败):退回单帧输出,保证错误不被静默吞掉。 + Diag.emit_header() + Diag.emit_current(file_name, lineno, col_start, col_end) + Diag.emit_category(err_type, msg) + Diag.finish() + return 0 + + Diag.begin(ent, err_type, msg) + # 调用链帧(按"自外向内"顺序)**由本类遍历**(只有它拿得到 TraceFrame),填入 entry。 + for i in range(self.Gen.TypeGenBase.TraceCount): + frame: GenTypes.TraceFrame* = self.Gen.TypeGenBase._get_trace_frame(i) + if frame is not None and frame.file_name is not None: + Diag.add_frame(ent, frame.file_name, frame.lineno) + Diag.set_current(ent, file_name, lineno, col_start, col_end) + # 默认"向上":最外层触发点在上、错误点最后(Python most recent call last)。 + Diag.emit(ent, Diag.DIAG_DIR_UP) + Diag.free_entry(ent) # Termination convergence: in REPL, longjmp back to the loop via VLogger.repl_abort (the # traceback is printed and the process survives); in batch this exit is sys.exit(1) unchanged. - VLogger.repl_abort() - # 不可达:repl_abort 要么 longjmp 回 REPL 循环、要么 sys.exit(1)。 - # 显式 return 只为满足上面声明的 -> int 返回类型(避免"缺返回路径")。 + Diag.finish() + # 不可达:finish 要么 longjmp 回 REPL 循环、要么 sys.exit(1)。 + # 显式 return 只为满足上面声明的 -> int 返回类型(避免"缺返回路径")。 return 0 # 同 fatal_error:跨模块调用需要**真实**返回类型,否则解析出 i0 会命中守卫中止编译。