Files
ViperC/App/lib/core/Repl.vp
T
TermiNexus 1ad7abf33d refactor(TransPyV): 统一自举写法(novirtual / define / rawptr→* / 去续行反斜杠)
- @t.NoVTable class X → novirtual class X
- t.CDefine = N → define N
- rawptr[X]、X | t.CPtr → X*
- t.CPtr → rawptr(裸指针标记)
- t.Callable[...] → callable[...]
- 去掉 if/elif 条件的行续反斜杠(改单行写法)
- w32.win32file.X → win32file.X(已 import as)
- core/__init__.vp 去掉未使用的 import t, c / from stdint import *

涉及:CompilerFactory、IncludesScanner、Repl、core/__init__、
Gen/Call/{CallGenBase,CallGenCBuiltin,CallGenCall,CallGenDispatch,CallGenLeafKit}、
Gen/Class/ClassGenGeneric。
2026-10-02 02:16:37 +08:00

2971 lines
151 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
from stdint import *
import stdio
import stdlib
import string
import viperlib
import llvmlite
import memhub
import ast
import w32.fileio as fileio
import w32.win32file as w32file
import w32.win32base as win32base
import w32.win32process as win32process
import lib.core.VLogger as VLogger
import lib.core.Gen.GenApi as GenApi
import lib.core.Gen.GenTypes as GenTypes
import lib.core.Gen.Translator as HandlesTranslator
import lib.core.IncludesScanner as IncludesScanner
import lib.Phase.Phase1.Phase1Scan as Phase1Scan
import lib.Phase.BuildPipeline as BuildPipeline
import lib.Projectrans.Config as Config
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect
import lib.Stub.StubGen.Converter as StubConverter
import lib.Stub.StubGen.VpiReader as VpiReader
# ============================================================
# Repl - TransPyV 交互式循环(M2:会话变量 + ctx 回传结果)
#
# 输入:单行片段(既有路径)与**块级续行**——整行去行尾空白后以 ':' 结尾 ⇒ 进入续行
# (提示符 "... ",本行不执行),空行或 EOF 结束并把整块作为**一个**片段执行一次;
# 块文本存会话级 PermPool 缓冲(_REPL_BLOCK_CAP),失败/溢出后清空(残块不污染下一行)。
#
# 片段通路:行 → 包裹为 `export def __repl_frag_<n>(__repl_ctx__: bytes) -> int`
# → 翻译为完整模块 IR(会话变量按 ctx 槽寻址)→ .ll → llc → clang++ -shared
# → LoadLibraryA/GetProcAddress → t.Callable 调用(传 ctx 基址)→ 从 ctx 结果区
# 读回并回显。
#
# ctx(会话内存,宿主 PermPool 一次性分配;布局见 GenTypes 顶部注释):
# 偏移 0..63 固定头(magic/version/result_kind/result_a/result_b/var_count/last_frag)
# 偏移 64 起为变量槽(每槽 16 字节)。宿主与片段在**同一进程**内共享该内存:
# 会话变量只存一份 ⇒ &x 得到的就是槽地址(地址同一性)。
# 值的类型按首次赋值推导(`x = 1` 与 `x: int = 1` 等价,均定型并分配槽)。
#
# 错误可恢复依赖 VLogger 的 jit_flag + setjmp/longjmp:
# - Repl 用 PermPool 分配 jmp_buf 并注册给 VLogger(PermPool 不受 mb.reset_to 影响);
# - 每轮 mb.mark() 后 _setjmp,解析/翻译全用 mb 分配;无论成败都 reset_to(mark) 回收。
#
# 复用既有机制(不新造进程拉起步子):
# - BuildPipeline.write_ir_to_file / build_sliced_path / compile_ll_to_obj(llc);
# - BuildPipeline._run_cmd_wait(CreateProcessA + WaitForSingleObject,见 :488);
# - Config 的 compiler/linker/target 通道(project.vpj 原样透传);
# - includes 会话初始化:IncludesScanner.scan_includes + StubMergerStore.PopulateSha1MapStore
# + StubConverter._GeneratePyiFromAst + StubMergerCollect._sliced_path(与 Phase1 同款,
# 见 ReplInitIncludes;否则片段里的 `import <includes 模块>` 会因 SHA1 表为空 fail-fast)。
# ============================================================
# 输入行缓冲容量(含 NUL 终止符)
define _REPL_LINE_CAP = 4096
# 块输入缓冲容量(会话级 PermPool,含 NUL 终止符):续行状态下逐行追加的用户原文,
# 行间以一个 '\n' 连接、无行尾换行。
# 取 8 KiB 的理由:常见多行块(10 行 × 80 列 ≈ 0.8 KiB)绰绰有余;整段粘贴
# 100 行 × 80 列 ≈ 8 KiB 也刚好容纳;再大属异常输入,按"明确报错 + 丢弃该块"
# 处理(绝不截断后执行)。上界同时约束包裹源码缓冲的下界(见 _REPL_FRAG_SRC_CAP)。
define _REPL_BLOCK_CAP = 8192
# jmp_buf 实测大小:mingw-w64 x86_64 setjmp.h 中 _JBLEN=16、_JBTYPE=SETJMP_FLOAT128(16B)
# ⇒ 16 * 16 = 256 字节(已用 C 探针 sizeof(jmp_buf) 实测 = 256)
define _REPL_JMPBUF_SIZE = 256
# setjmp/longjmp:mingw-w64 x86_64 的 _setjmp 第二参数为 frame 指针,
# setjmp 宏即展开为 _setjmp(buf, NULL)(非 SEH 场景传 None 即可)
extern def _setjmp(buf: rawptr, frame: rawptr) -> int
extern def longjmp(buf: rawptr, val: int)
# PermPool(由 main 注入):jmp_buf 生命周期须覆盖整个会话,不能被 mb.reset_to 回收
_perm_pool: memhub.MemBuddy*
# ============================================================
# M2/M3 常量
# ============================================================
# 片段包裹源码容量(含 NUL)。三条路径的上界取大者:
# 单行路径 = 行缓冲 4096 + 包裹开销 128(原口径,未变);
# 块路径 = 块缓冲 8192 + 逐行 4 空格缩进(块内每行至少 1 字符 + 一个行间 '\n'
# ⇒ 行数 ≤ 4096 ⇒ 缩进 ≤ 4096*4 = 16384)≤ 24576;
# 提升路径(M3)= 上面 24576 + 每个被提升 def 的头部注入("export " 7 +
# "__repl_ctx__: bytes, " 21 ⇒ 按 64 字节/个上 slack,def 数 ≤ 16
# ⇒ ≤ 1024)+ 包裹头尾开销 512 ⇒ ≤ 26112。
# 缩进是"块路径"的唯一放大项,故上界必须按"行数上界"而非"行数×行长"估;
# 提升行按原文进入模块区(不缩进),其代价低于按 4 空格放大的上界,故不再附加项。
_REPL_FRAG_SRC_CAP: t.CDefine = 26624
# 提升出的模块级 def 源码缓冲容量(含 NUL):提升行原文 ≤ 8192 + 注入 ≤ 1024
_REPL_LIFT_CAP: t.CDefine = 12288
# 单片段最多提升的会话函数数(≤ GenTypes.REPL_FUNC_CAP;超出 ⇒ 明确报错)
_REPL_LIFT_MAX: t.CDefine = 16
# 会话函数名长度上限(写入 ReplLiftNames 的槽宽 = 上限 + 1)
_REPL_FN_NAME_MAX: t.CDefine = 64
# 会话函数名表容量(_REPL_LIFT_MAX 个定长槽)
_REPL_FN_NAMES_CAP: t.CDefine = 1024
# define 行里单个类型串的拷贝缓冲("i32"/"i8*"/"%struct.x*" 截断到这里再比较)
_REPL_FN_TY_CAP: t.CDefine = 64
# 诊断显示缓冲(把非 NUL 结尾的行/名字截断后供 %s 打印)
_REPL_DIAG_CAP: t.CDefine = 128
# 签名渲染缓冲容量("(int, str) -> int" 最长 ≈ 8×(5+2)+8+8 < 96)
_REPL_SIG_TEXT_CAP: t.CDefine = 192
# 片段符号名/探针缓冲容量("__repl_frag_<n>" / "define i32 @<name>(i8* %__repl_ctx__")
_REPL_NAME_CAP: t.CDefine = 64
_REPL_NEEDLE_CAP: t.CDefine = 128
# 临时文件路径缓冲容量(目录/源文件路径)
_REPL_PATH_CAP: t.CDefine = 512
# 完整模块 IR 文本容量(与 App/main.vp IR_BUF_SIZE 一致)
_REPL_IR_CAP: t.CDefine = 262144
# GetTempPathA 输出缓冲容量(MAX_PATH)
_REPL_TMP_CAP: t.CDefine = 260
# 外部命令(clang++ -shared)命令行容量。
# M4 扩容 1024 → 16384:片段链接命令行现在除了宿主 import lib,还要带上**先前
# 片段**的 import lib(每个 ≈ 80~110 字节路径),否则"片段 N 定义的类/方法"在
# 后续片段里是 undefined symbol(见 ReplFragImplibs 段)。
_REPL_CMD_CAP: t.CDefine = 16384
# includes 源文件读取缓冲(与 Phase1.SRC_BUF_SIZE / BuildPipeline.SRC_BUF_SIZE 一致)
_REPL_SRC_BUF_CAP: t.CDefine = 1048576
# .vpi 存根缓冲(与 BuildPipeline.PYI_BUF_SIZE 一致)
_REPL_VPI_BUF_CAP: t.CDefine = 262144
# ---- M4:会话 sha1 / 片段 import lib / 会话基线(CDefine + CTypedef)----
# 先前片段 import lib 的槽数上限(每个"IR 里出现会话前缀符号"的片段最多一条;
# 判据是"出现过"而非"定义过" ⇒ 纯调用片段也会占一个槽,故留足余量)
_REPL_FRAG_IMPLIB_MAX: t.CDefine = 128
# 片段 import lib 记录数组容量(槽数 × 8 字节指针)
_REPL_FRAG_IMPLIB_ARR_CAP: t.CDefine = 1024
# 会话基线(CDefine)条目上限:每片段重建表时按基线回灌
_REPL_BASE_CDEFINE_MAX: t.CDefine = 512
# 会话基线(CTypedef)条目上限
_REPL_BASE_CTYPEDEF_MAX: t.CDefine = 512
# includes 类注册(会话初始化做一次):参与 vpi 注入的模块数上限
_REPL_INC_CLS_MOD_MAX: t.CDefine = 512
# includes 类注册的收敛遍数(字段类型依赖同批其它类 ⇒ 多遍收敛)
_REPL_INC_CLS_PASSES: t.CDefine = 3
# includes 类注册的结构体表水位守卫(STRUCT_MAX = 512;预留余量)
_REPL_INC_CLS_STRUCT_GUARD: t.CDefine = 448
# includes 类注册的逐模块告警行上限(超出只累计计数,由汇总行给出总数)
_REPL_INC_CLS_WARN_MAX: t.CDefine = 3
# includes 类注册(会话初始化)的启动时间预算(ms):多遍 vpi 注入 + 逐类布局校验是
# 会话启动的主要耗时,且失败模块会在后续遍重试 ⇒ 模块数 × 遍数的上界虽有限,但
# 上界本身可能很长(缺存根时每模块仍要读一遍 .vpi + 解析 + 注入了 declare)。
# 超预算即停止"启动期注册",剩余模块留待片段 `import` 时按既有 on-demand 路径注册。
# 取舍:宁可让部分 includes 类在启动后仍不可用(且明确告知),也不能让启动"看起来
# 死了"——提示符必须先出现(REPL 可用性 > 启动期注册完整度)。
_REPL_INC_CLS_BUDGET_MS: t.CDefine = 20000
# includes 类注册用的临时 LLVM 模块名(.vpi 注入的目标模块,会话初始化时一次)
_REPL_INC_MOD_NAME: t.CDefine = "repl_inc_init"
# ctx 会话缓冲字节数 = 固定头 64 + 槽数上限 64 × 槽步长 16
#(M3 追加函数指针槽区:+ 函数上限 32 × 槽步长 16 ⇒ 64 + 1024 + 512 = 1600)
# 必须与 GenTypes.REPL_CTX_SIZE / REPL_SLOT_BASE / REPL_VAR_CAP / REPL_SLOT_SIZE /
# REPL_FUNC_BASE / REPL_FUNC_CAP / REPL_FUNC_SLOT_SIZE 一致
_REPL_CTX_SIZE: t.CDefine = 1600
# 模板工程文件名(--repl 未给 --project 时在 cwd → exe 同目录查找)
_REPL_PROJECT_FILE: t.CDefine = "repl.vpj"
# cwd 候选后缀:GetCurrentDirectoryA 返回值不含结尾分隔符,故须补 '/'
_REPL_PROJECT_SEP_FILE: t.CDefine = "/repl.vpj"
# 上述两种后缀的最长长度 + NUL 余量(候选缓冲余量检查用)
_REPL_PROJECT_PROBE_CAP: t.CDefine = 16
# ============================================================
# ReplFloatView - ctx 头的浮点视图(宿主回显浮点结果用)
#
# 与 GenTypes.ReplResult 同一块内存:F 位于偏移 16,正是 ReplResult.A 的 8 字节。
# 片段把 double 的 IEEE754 位模式写进 result_a;宿主按本视图读回即得 double
# (位模式重解释,不做数值转换)。
# ============================================================
novirtual class ReplFloatView:
"""ctx 头(+16 处按 double 读)"""
Magic: int
Version: int
Kind: int
Reserved0: int
F: t.CDouble
# ============================================================
# ReplFuncSlotView - ctx 函数指针槽视图(M3;宿主写入片段函数的入口地址)
#
# 与 GenTypes.REPL_FUNC_BASE/REPL_FUNC_SLOT_SIZE 同一布局约定:每个会话函数占
# 16 字节(低 8 字节 = 地址,高 8 字节保留)。片段侧按同一偏移(i8** 视图)
# load 该地址后间接调用;宿主用本视图写入 ⇒ 两侧对"槽内是什么"只有一个约定。
# ============================================================
novirtual class ReplFuncSlotView:
"""ctx 函数指针槽(Addr = 定义片段 DLL 里该函数的入口地址)"""
Addr: VOIDPTR
Reserved: VOIDPTR
# ============================================================
# ReplSigOut - 会话函数签名解析结果(M3;宿主从片段 IR 的 define 行解析)
#
# 出处:定义片段 translate 后 dump 的 IR 里
# `define <ret> @f(i8* %__repl_ctx__, <P1> %p1, ...)`
# 是定义端 ABI 的唯一真相(注解 → LLVM 类型由 codegen 解析)。宿主据此填
# ReplFuncEntry 的签名 token;调用片段再用 token 重建同型签名 ⇒ 两侧一致。
# 形参/返回的 token 数组写进会话 scratch(ReplSigTags/ReplSigBits)。
# ============================================================
novirtual class ReplSigOut:
"""签名解析结果(Bad/EHTag 非 0 时拒绝注册)"""
RetTag: int # GenTypes.REPL_TY_VOID / INT / FLOAT / STR
RetBits: int
ParamCount: int # 不含 ctx 首形参
EHTag: int # 1=define 含 %__eh_msg_out__/%__eh_code_out__(raise 语义)
Bad: int # 1=含不受支持的类型 token
BadType: str # 不受支持的类型串(指向 IR 文本内,仅用于打印;否则 None)
# ============================================================
# 模板工程查找 - --repl 未给 --project 时的启动便利
#
# 顺序:① 当前工作目录 repl.vpj → ② TransPyV.exe 同目录 repl.vpj。
# 命中路径由 main 按 --project 等价路径喂给既有的 Config.Load_project_config
# 加载流程(不另写加载器);两处都没有时由调用方按未命中报错。
# Out/Diag 由调用方(main,stdlib.malloc)提供:本函数在会话初始化之前调用,
# 故只用入参缓冲 + 一次性 stdlib.malloc,不碰 Repl 会话缓冲与 PermPool。
# ============================================================
def ReplFindProject(Out: bytes, Cap: int, Diag: bytes, DiagCap: int) -> int:
"""cwd → exe 同目录 查找 repl.vpj:1=命中(写 Out),0=未命中(写 Diag)"""
if Out is None or Diag is None:
return 0
if Cap < 64 or DiagCap < 64:
return 0
# 两个候选各用独立缓冲:未命中时两条路径都要进诊断文本
c1: bytes = stdlib.malloc(Cap)
c2: bytes = stdlib.malloc(Cap)
if c1 is None or c2 is None:
if c1 is not None:
stdlib.free(c1)
if c2 is not None:
stdlib.free(c2)
return 0
c1[0] = 0
c2[0] = 0
# ① cwd/repl.vpj
n_cwd: ULONG = w32file.GetCurrentDirectoryA(ULONG(Cap), c1)
if n_cwd > 0:
cl: int = int(n_cwd)
# 防御:缓冲区不足时 GetCurrentDirectoryA 的写入内容未定义,强制补 NUL
c1[Cap - 1] = 0
if cl + _REPL_PROJECT_PROBE_CAP <= Cap:
viperlib.snprintf(c1 + cl, Cap - cl, "%s", _REPL_PROJECT_SEP_FILE)
if _is_file(c1) != 0:
string.strcpy(Out, c1)
stdlib.free(c1)
stdlib.free(c2)
return 1
else:
string.strcpy(c1, "(GetCurrentDirectoryA 失败)")
# ② exe 同目录/repl.vpj(GetModuleFileNameA(NULL) 取自身全路径)
n_exe: ULONG = w32file.GetModuleFileNameA(None, c2, ULONG(Cap))
if n_exe > 0:
el: int = int(n_exe)
# 防御:截断时返回值 == Cap 且串可能未 NUL 结尾
c2[Cap - 1] = 0
if el < Cap:
c2[el] = 0
# 定位最后一个分隔符:c2[0..dlen) 为目录前缀(含分隔符);
# dlen=0 表示 exe 路径无目录部分(等价于 cwd,交由 OS 解析)
dlen: int = 0
si: int = el
while si > 0:
si -= 1
ch: char = c2[si]
if ch == '/' or ch == '\\':
dlen = si + 1
break
if dlen + _REPL_PROJECT_PROBE_CAP <= Cap:
viperlib.snprintf(c2 + dlen, Cap - dlen, "%s", _REPL_PROJECT_FILE)
if _is_file(c2) != 0:
string.strcpy(Out, c2)
stdlib.free(c1)
stdlib.free(c2)
return 1
else:
string.strcpy(c2, "(GetModuleFileNameA 失败)")
# 两处都没有:诊断文本(需要 --project + 两个已查位置 + 模板位置)
viperlib.snprintf(Diag, DiagCap,
" 已查找(1): %s\n"
" 已查找(2): %s\n"
" 需要 --project: 未找到 repl.vpj 时 REPL 拿不到工程配置,"
"请用 --project <path/to/repl.vpj> 指定\n"
" 模板位置: 仓库 TransPyV/repl.vpj;拷到 TransPyV.exe 同目录"
"或当前工作目录即可被自动找到\n",
c1, c2)
stdlib.free(c1)
stdlib.free(c2)
return 0
# ============================================================
# 会话状态
#
# 常驻缓冲一律 PermPool 分配(ReplInitSession 一次性建,进程退出随池释放);
# 按片段产生的短命串(.ll/.obj/.dll 路径、命令行)来自 mb,行末 reset_to(mark) 回收。
# ============================================================
# 片段函数指针槽:t.Callable 注解 → i8** 槽(VET/App/lib/VGuard.vp:358 同款范式)。
# 模块级 AnnAssign 的返回类型签名由 TranslatorGenMixin.vp:721-742 登记 →
# 调用点经 CallGenMixin.vp:960-984 走"按实参构造签名 + 间接 call"路径。
# M2 约定:唯一实参是 ctx 基址(i8*),返回状态码 int(结果经 ctx 回传)。
ReplFragFn: callable[[rawptr], int] = None
# 片段序号:模块级(longjmp 恢复后仍单调递增)→ 符号名/DLL 文件名在会话内不复用
ReplFragNo: int = 1
# 会话临时目录 %TEMP%\TransPyV_repl\<pid>(不污染用户工程目录;pid 隔离并发会话)
ReplTmpDir: bytes = None
# 片段符号名 __repl_frag_<n>
ReplNameBuf: bytes = None
# 包裹后的片段源码(供解析/翻译)
ReplSrcBuf: bytes = None
# 包裹源码落盘路径(错误回显用真实源文件)
ReplSrcPath: bytes = None
# 块输入缓冲(会话级 PermPool,容量 _REPL_BLOCK_CAP):续行状态下逐行追加的用户原文,
# 行间以 '\n' 连接、无行尾换行。一次性分配,不按行 malloc。
ReplBlockBuf: bytes = None
# 块缓冲当前字节数(0 = 无缓存内容;NUL 恒写在 ReplBlockBuf[ReplBlockLen])
ReplBlockLen: int = 0
# 续行状态:1 = 正在缓存多行块(提示符 "... ");0 = 正常(提示符 ">>> ")
ReplInBlock: int = 0
# 完整模块 IR 文本(dump_ir OUTPUT_FULL 的输出缓冲)
ReplIrBuf: bytes = None
# IR 探针 "define i32 @<name>(i8* %__repl_ctx__"(确认导出函数签名与调用约定一致)
ReplNeedleBuf: bytes = None
# ctx 会话缓冲(宿主与片段共享;字节视图,作为片段首实参传递)
ReplCtxBuf: bytes = None
# 会话上下文对象(Gen 侧读:表/容量/形参名/永久池/影子条目)
ReplCtxObj: rawptr[GenTypes.ReplCtx] = None
# 会话变量表(PermPool,ReplCtxObj.Table)
ReplVarTable: rawptr[GenTypes.ReplVarEntry] = None
# 影子条目(PermPool):仅供"裸表达式结果"做类型分类,不占会话槽、不进表
ReplScratch: rawptr[GenTypes.ReplVarEntry] = None
# 恢复点 jmp_buf(PermPool,会话期存活):run 里 _setjmp 到它,
# VLogger.repl_abort 从流水线任意深度 longjmp 回它。每片段入口重注册(幂等)。
ReplJmpBuf: rawptr = None
# 片段开始时的会话表水位(ReplCtxObj.Count):恢复时回滚到它,
# 丢弃失败片段的半成品登记(repl_ensure_entry 已 Count+1 但 repl_seal 报错的条目)。
ReplSavedVarCount: int = 0
# ==== includes 会话初始化结果(会话级一次;供摘要行与恢复分支报告进度)====
# 已注册的 includes 模块数(StubMerger SHA1 表条目数)
ReplIncMods: int = 0
# includes 扫描耗时(ms)
ReplIncMs: int = 0
# 新生成的 .vpi 存根数
ReplIncVpiNew: int = 0
# 复用的既有 .vpi 存根数
ReplIncVpiReuse: int = 0
# .vpi 生成失败数(读/解析/写盘失败)
ReplIncVpiFail: int = 0
# ==== 宿主导出符号(跨模块调用)====
# 宿主 exe 的 import lib 路径(<exe 名>.dll.a;PermPool,会话级一次探测)。
# 非 None ⇒ 片段链接命令把它加进去 ⇒ 片段 DLL 从宿主 exe 导入 includes 符号,
# 与宿主共用同一份代码与数据(无状态重复)。None ⇒ 宿主未按导出模式构建。
ReplHostImplib: bytes = None
# ==== M3 会话函数(跨片段函数)====
# 提升出的模块级 def 源码(会话级 PermPool;每片段开头清零长度)
ReplLiftBuf: bytes = None
# 提升出的会话函数名表(PermPool,_REPL_FN_NAME_MAX 定长槽 × _REPL_LIFT_MAX 个)
ReplLiftNames: bytes = None
# 本次片段提升出的会话函数个数
ReplLiftNameCount: int = 0
# 签名 token scratch(PermPool,各 GenTypes.REPL_FUNC_PARAM_MAX 字节;逐函数复用)
ReplSigTags: bytes = None
ReplSigBits: bytes = None
# 签名解析结果体(PermPool,会话级复用;名字避开类名 ReplSigOut)
ReplSigResult: rawptr[ReplSigOut] = None
# 会话函数表水位(ReplCtxObj.FuncCount)在片段开始时的快照:失败/回滚时恢复到它,
# 丢弃失败片段的半成品登记(与 ReplSavedVarCount 同思路,见 ReplExecFragment ⓪)
ReplSavedFuncCount: int = 0
# 诊断用行/名字显示缓冲(PermPool,_REPL_DIAG_CAP;%s 不能直接打非 NUL 结尾的行)
ReplLineShow: bytes = None
# def 头名字 scratch(PermPool,_REPL_FN_NAME_MAX)
ReplMsgName: bytes = None
# 签名解析的类型串 scratch(PermPool,_REPL_FN_TY_CAP)
ReplTySpan: bytes = None
# 签名解析的"%+形参名"合成 scratch(PermPool,_REPL_FN_TY_CAP)
ReplTySpan3: bytes = None
# IR 里最近命中的 define 行起点偏移(_repl_ir_find_define_at 写、_repl_ir_func_sig 读)
ReplIrDefOff: int = -1
# 签名渲染缓冲 ×2(PermPool,_REPL_SIG_TEXT_CAP):新签名 / 已有条目签名对比展示
ReplSigText: bytes = None
ReplSigText2: bytes = None
# 片段体(包裹)当前写入位置(_repl_split_body 写、_repl_build_frag_src_multi 读)
ReplWrapPos: int = 0
# ==== M4:会话模块 sha1(类型名的跨片段唯一键)====
# PermPool 上的 16 位 hex 串(GenTypes.REPL_SESSION_SHA1 的副本)。
# 每个片段翻译器都取它作 ModuleSha1 ⇒ 片段内定义的类名恒定形如
# "<session sha1>.<Class>",后续片段据同一前缀查到同一结构体条目。
ReplSessionSha1: bytes = None
# ==== M4:先前片段的 import lib(跨片段方法/构造符号的解析来源)====
# 片段 DLL 只把自己 obj 与"已记录 import lib"链在一起:片段 N 里的
# `p = Point(1, 2)`(Point 定义在片段 M<N)会生成对 <session sha1>.Point.__init__
# 的引用,该符号只存在于片段 M 的 DLL 里 ⇒ 必须把 M 的 import lib 加进 N 的链接
# 命令行。数组槽内是 PermPool 里的路径串指针(8 字节/槽)。
ReplFragImplibs: sizet* = None
ReplFragImplibCount: int = 0
# ==== M4:会话基线(CDefine / CTypedef 的跨片段存活)====
# 每片段开头的 clear_cdefine_constants/clear_ctypedefs 会把表指针置 None
# (表内容随片段池一起回收),而表的语义是"整个会话一份"(includes 注入的常量、
# include 类型别名、片段里 `N: t.CDefine = 3` 等)⇒ 用会话基线 + 片段增量:
# - 基线:PermPool 里的 (名字, 值/类型串, 种类) 三元组数组,只在**成功**片段
# 结束时追加该片段新增的条目(失败片段不追加 ⇒ 无污染,水位回滚天然成立);
# - 每片段清空表后按基线回灌(条目分配在本片段池,随片段回收);
# - 失败片段用 ReplSavedCDefineCount/ReplSavedCTypedefCount 截断(防御性,
# 与 ReplSavedVarCount/ReplSavedFuncCount 同思路)。
ReplBaseCDefineNames: sizet* = None
ReplBaseCDefineValues: sizet* = None
ReplBaseCDefineKinds: sizet* = None # 每条目 8 字节:0=int / 1=double / 2=str
ReplBaseCDefineCount: int = 0
ReplBaseCTypedefNames: sizet* = None
ReplBaseCTypedefTypeStrs: sizet* = None
ReplBaseCTypedefCount: int = 0
# 片段开始时的基线水位(失败/回滚时截断回它)
ReplSavedCDefineCount: int = 0
ReplSavedCTypedefCount: int = 0
# ==== M4:includes 类注册(会话初始化做一次)====
# 已成功注入 .vpi(⇒ 其类进入会话 StructTable)的模块数 / 失败数 / 缺存根数
ReplIncClsMods: int = 0
ReplIncClsFail: int = 0
ReplIncClsMissing: int = 0
# ============================================================
# 私有 helper
# ============================================================
# 路径是否为"存在的普通文件"(GetFileAttributesA:只返回属性值,无输出缓冲、无越界风险)
def _is_file(path: str) -> int:
"""1=存在且非目录;0=不存在/是目录/参数错误"""
if path is None:
return 0
attr: ULONG = w32file.GetFileAttributesA(path)
if attr == 0xFFFFFFFF: # INVALID_FILE_ATTRIBUTES
return 0
if (attr & w32file.FILE_ATTRIBUTE_DIRECTORY) != 0:
return 0
return 1
# ============================================================
# 宿主导出符号:定位宿主 exe 的 import lib(跨模块调用)
#
# 为什么需要:片段 IR 对 includes 的引用是 `<sha1>.<函数名>`(.text.ll 实证:
# `define i32 @"<sha1>.utf8_is_valid"`;模块级全局同理 `<sha1>.<变量名>`,如
# `@"<sha1>._memdbg_enable" = global i32 zeroinitializer`),这些符号由**宿主 exe**
# 定义(exe 链接时已把 includes.binary 的 obj 全部链入,实测 output/TransPyV.exe
# 的 COFF 符号表含 118 个 `<16位hex>.` 前缀)。片段链接命令若只含片段自身 obj,
# 跨模块符号会以 undefined symbol 停在链接期。
#
# 机制(方案 C):宿主以 `-Wl,--export-all-symbols` 构建 ⇒ exe 带导出表;
# BuildPipeline 同时产出 `{OutputDir}/{exe 名}.dll.a`(`-Wl,--out-implib=`)。
# 片段链接时把该 import lib 加进命令行 ⇒ 片段 DLL 的导入表指向宿主 exe,
# 加载时由同一进程内已加载的宿主满足 ⇒ 宿主与片段**共用同一份** includes 代码与
# 数据(状态不重复;片段 DLL 内不存在任何 `<sha1>.` 定义)。
#
# 查找顺序:① 本 exe 同目录(默认构建把 exe 与 .dll.a 一起写在 OutputDir)
# ② Config.OutputDir(--project 指向真正宿主的 vpj 时)
# 两处都没有 ⇒ 不报错,按旧口径只用片段自身 obj 链接(失败时给出明确提示)。
# ============================================================
def ReplFindHostImplib() -> int:
"""定位宿主 import lib(<exe 名>.dll.a):1=命中(写 ReplHostImplib),0=未找到"""
global ReplHostImplib
if _perm_pool is None:
return 0
ReplHostImplib = None
exe_buf: bytes = _perm_pool.alloc(_REPL_PATH_CAP)
cand: bytes = _perm_pool.alloc(_REPL_PATH_CAP)
if exe_buf is None or cand is None:
return 0
exe_buf[0] = 0
cand[0] = 0
n_exe: ULONG = w32file.GetModuleFileNameA(None, exe_buf, ULONG(_REPL_PATH_CAP))
if n_exe <= 0:
return 0
el: int = int(n_exe)
# 防御:截断时写入内容可能未 NUL 结尾(与 ReplFindProject 同口径)
if el >= _REPL_PATH_CAP:
el = _REPL_PATH_CAP - 1
exe_buf[el] = 0
# 拆目录/文件名:dlen = 目录前缀长度(含分隔符)
dlen: int = 0
si: int = el
while si > 0:
si -= 1
ch: char = exe_buf[si]
if ch == '/' or ch == '\\':
dlen = si + 1
break
# 候选 ①:exe 同目录(exe_buf 已含目录前缀)
viperlib.snprintf(cand, _REPL_PATH_CAP, "%s%s.dll.a", exe_buf, exe_buf + dlen)
if _is_file(cand) != 0:
ReplHostImplib = cand
return 1
# 候选 ②:Config.OutputDir(REPL 用 repl.vpj 起、宿主用 project.vpj 建时)
if Config.OutputDir is not None:
viperlib.snprintf(cand, _REPL_PATH_CAP, "%s/%s.dll.a",
Config.OutputDir, exe_buf + dlen)
if _is_file(cand) != 0:
ReplHostImplib = cand
return 1
return 0
# 逐字节 ReadFile 读取一行:控制台(ENABLE_LINE_INPUT,回车后返回整行)与管道均适用
def _read_line(h: win32base.HANDLE, buf: bytes, cap: int) -> int:
"""从 h 读取一行写入 buf(NUL 结尾),返回字节数(不含 NUL);EOF/错误返回 -1。
剥离 CR(兼容 CRLF 输入),遇 LF 结束;缓冲区满时停止(余量留作下一行)。
"""
if buf is None or cap < 2:
return -1
total: int = 0
bytes_read: ULONG = 0
ok: BOOL = 0
eof: int = 0
while total < cap - 1:
bytes_read = 0
ok = w32file.ReadFile(h, buf + total, 1, &bytes_read, None)
if ok == 0 or bytes_read == 0:
eof = 1
break
# 注意:不能用 &(buf + total)——其 IR 生成 i32 load(读 4 字节),
# 比较 '\n'/'\r' 永不命中;字节索引 buf[total] 才是 i8 load
ch: char = buf[total]
if ch == 10: # '\n':行结束
break
if ch == 13: # '\r':CRLF,丢弃后原位覆盖读取
continue
total += 1
buf[total] = 0
if eof != 0 and total == 0:
return -1
return total
# 统计顶层语句数(Module.children)
def _count_top_stmts(tree: ast.AST*) -> int:
"""返回 Module 的顶层语句数(children 为空时为 0)。"""
if tree is None or tree.children is None:
return 0
stmts: list[ast.AST*]* = tree.children
return int(len(stmts))
# ============================================================
# 块级续行(多行块输入)
#
# 判据(与用户拍板口径一致,刻意保持"最简单可预测"):
# 进入:非续行状态下,整行去掉行尾空白后以 ':' 结尾 ⇒ 进入续行,该行进缓冲、不执行;
# 结束:续行中读到空行(长度 0)或 EOF ⇒ 整块作为**一个**片段执行一次;
# 续行中不做任何缩进/关键字分析,行尾 ':' 也不反复切换状态。
# 为什么只看行尾 ':':Viper 的块头(def/class/for/if/while/with/try/match/case…)
# 一律以 ':' 收尾,而完整单行语句几乎不会以 ':' 结尾 ⇒ 判据零误报面;
# 唯一代价是"行尾带 ':' 的非法/罕见单行"会多缓存一行,空行即执行,可预期。
# 为什么空行结束:与 Python REPL 一致(空行 = 块输入完毕),且无需缩进分析;
# 批处理源码里空行不影响语法(lexer 的 _handle_indent 跳过空行/纯空白行)。
# ============================================================
# 行尾判据:去掉行尾空白后以 ':' 结尾;或(M3)首个非空白字符是 '@'(装饰器行)
def _line_starts_block(line: str, n: int) -> int:
"""1=该行进入块缓冲(':' 结尾 / '@' 开头);0=否
'@' 开头的行单独成句在 Viper/Python 里都是语法错误(装饰器必须后接 def/class),
故把它纳入"进入续行"判据零误报面;这是 M3 会话函数能用 @decorator 的前提
(装饰器行不以 ':' 结尾,否则用户无法把装饰器连同 def 一起送进一个块)。
"""
if line is None or n <= 0:
return 0
s: int = 0
while s < n and (line[s] == ' ' or line[s] == '\t'):
s += 1
if s >= n:
return 0
if line[s] == '@':
return 1
i: int = n
while i > 0:
ch: char = line[i - 1]
if ch != ' ' and ch != '\t':
break
i -= 1
if i <= 0:
return 0
if line[i - 1] == ':':
return 1
return 0
# 追加一行到块缓冲:行间补一个 '\n',末尾写 NUL
def _block_append(line: str, n: int) -> int:
"""把 n 字节行追加到会话块缓冲;0=成功,1=容量不足(此时缓冲内容未改)"""
global ReplBlockLen
if ReplBlockBuf is None or line is None or n <= 0:
return 1
sep: int = 0
if ReplBlockLen > 0:
sep = 1 # 行间 '\n'
if ReplBlockLen + sep + n >= _REPL_BLOCK_CAP: # 末尾还需 1 字节 NUL
return 1 # 先判容量后写 ⇒ 失败时无需回滚
if ReplBlockLen > 0:
ReplBlockBuf[ReplBlockLen] = '\n'
ReplBlockLen += 1
string.memcpy(ReplBlockBuf + ReplBlockLen, line, sizet(n))
ReplBlockLen += n
ReplBlockBuf[ReplBlockLen] = 0
return 0
# ctx 头视图(int64 解释):宿主读写 magic/kind/result_a...
def _ctx_head() -> rawptr[GenTypes.ReplResult]:
"""ctx 头视图;会话未初始化返回 None"""
if ReplCtxBuf is None:
return None
return rawptr[GenTypes.ReplResult](ReplCtxBuf)
# ctx 头视图(double 解释 result_a):回显浮点结果用
def _ctx_float_head() -> rawptr[ReplFloatView]:
"""ctx 头浮点视图;会话未初始化返回 None"""
if ReplCtxBuf is None:
return None
return rawptr[ReplFloatView](ReplCtxBuf)
# 结果回显:按 result_kind 读 ctx 结果区并打印
def _repl_echo_result(frag_name: str, res: rawptr[GenTypes.ReplResult]) -> int:
"""按 result_kind 回显本片段结果;0=已处理(含"无结果")"""
if frag_name is None or res is None:
return 1
k: int = res.Kind
if k == GenTypes.REPL_RES_NONE:
# 声明类片段(如 `x: int`):本行无结果
return 0
if k == GenTypes.REPL_RES_INT:
stdio.printf("%s => %lld\n", frag_name, res.A)
return 0
if k == GenTypes.REPL_RES_FLOAT:
fv: rawptr[ReplFloatView] = _ctx_float_head()
if fv is None:
return 1
stdio.printf("%s => %f\n", frag_name, fv.F)
return 0
if k == GenTypes.REPL_RES_STR:
sp: rawptr = rawptr(res.A)
if sp is None:
stdio.printf("%s => <null str>\n", frag_name)
return 0
stdio.printf("%s => %s\n", frag_name, sp)
return 0
# REPL_RES_OBJ:result_a != 0 → 非字符串指针(报地址);result_a == 0 →
# 结构体值(内联在槽里,本身没有地址,类型名需跨片段类型注册表,属 M4)
if res.A == 0:
stdio.printf("%s => <struct 值>\n", frag_name)
return 0
stdio.printf("%s => <obj @ 0x%llX>\n", frag_name, res.A)
return 0
# ============================================================
# M4 - 会话 sha1 / 片段 import lib / 会话基线(CDefine + CTypedef)/ includes 类注册
#
# 三段机制各自解决一个"跨片段"问题(设计论证见 repl-design.md §M4):
# ① 会话 sha1:片段翻译器的 ModuleSha1 恒定取 GenTypes.REPL_SESSION_SHA1
# ⇒ 片段内定义的类型名恒定 "<sha1>.<Class>",会话 StructTable 的注册与后续
# 片段查找用同一个键(这是类型跨片段可见的唯一基础)。
# ② 片段 import lib:片段链接时加 --out-implib;若该片段 IR 里**定义**了会话前缀
# 符号(类方法/构造/类全局),登记其 .dll.a,后续片段链接时全部带上 ⇒ 那些
# 符号在链接期解析到**定义片段**的 DLL(代码与数据都只有一份)。
# ③ 会话基线:CDefine/CTypedef 表每片段被 clear_* 置空 ⇒ 用 PermPool 基线回灌 +
# 片段成功后追加增量,失败只按水位截断(表语义 = 整会话一份)。
# ============================================================
# 串深拷贝进 pool(调用方给池;池/串无效或分配失败返回 None)
def _repl_str_dup(pool: memhub.MemBuddy*, s: str) -> str:
"""深拷贝 s 到 pool;失败返回 None"""
if pool is None or s is None:
return None
n: int = int(string.strlen(s))
b: str = pool.alloc(n + 1)
if b is None:
return None
string.memcpy(b, s, sizet(n))
b[n] = 0
return b
# 建会话 sha1 串(PermPool;片段翻译期作为 ModuleSha1 使用)
def ReplInitSessionSha1() -> int:
"""0=成功;非 0=永久池/分配失败"""
global ReplSessionSha1
if _perm_pool is None:
return 1
ReplSessionSha1 = _repl_str_dup(_perm_pool, GenTypes.REPL_SESSION_SHA1)
if ReplSessionSha1 is None:
return 1
return 0
# ---- 会话基线(CDefine/CTypedef):查找 / 回灌 / 追加 / 截断 ----
def _repl_base_cdefine_find(name: str) -> int:
"""基线里按名查找 CDefine 条目下标;未命中返回 -1"""
if name is None or ReplBaseCDefineNames is None:
return -1
i: int
for i in range(ReplBaseCDefineCount):
nm: str = rawptr(ReplBaseCDefineNames[i])
if nm is not None and string.strcmp(nm, name) == 0:
return i
return -1
def ReplBaselineRestore(pool: memhub.MemBuddy*) -> int:
"""把会话基线(CDefine + CTypedef)回灌进刚被 clear_* 清空的表;0=成功
条目分配在 pool(= 本片段池):表每片段被清空,条目只需活到本片段结束。
回灌在 clear_* 之后、翻译之前调用 ⇒ 片段内的名字解析能同时看到
"会话基线(先前片段/先前 import 带进来的)∪ 本片段新增"。
"""
if pool is None or ReplBaseCDefineNames is None:
return 1
if GenApi._g_gen is None:
return 1
i: int
for i in range(ReplBaseCDefineCount):
nm: str = rawptr(ReplBaseCDefineNames[i])
val: str = rawptr(ReplBaseCDefineValues[i])
kind: int = int(ReplBaseCDefineKinds[i])
if nm is None or val is None:
continue
if kind == 0:
GenApi.register_cdefine_constant(pool, nm, int64(string.atoll(val)))
elif kind == 1:
GenApi.register_cdefine_double(pool, nm, string.atof(val))
else:
# 字符串 CDefine:GenApi 无同名壳,经 gen 单例直达(与 clear_cdefine_constants
# 的壳同款调用形态)
GenApi._g_gen.TypeGenCDef.register_cdefine_string(pool, nm, val)
for i in range(ReplBaseCTypedefCount):
tn: str = rawptr(ReplBaseCTypedefNames[i])
tv: str = rawptr(ReplBaseCTypedefTypeStrs[i])
if tn is None or tv is None:
continue
GenApi.register_ctypedef(pool, tn, tv)
return 0
def ReplBaselineCapture() -> int:
"""把当前表里"基线里还没有"的 CDefine/CTypedef 条目追加进会话基线;0=成功
只在片段**成功执行**后调用(失败片段不调用 ⇒ 基线天然不含失败片段的半成品,
无需回滚内容;截断只是防御性的一致性保障)。新条目深拷贝进 PermPool。
"""
global ReplBaseCDefineCount, ReplBaseCTypedefCount
if _perm_pool is None or GenApi._g_gen is None:
return 1
cdef_names: list[str]* = GenApi._g_gen.TypeGenCDef.CDefineNames
if cdef_names is not None:
cdef_vals: list[str]* = GenApi._g_gen.TypeGenCDef.CDefineValues
cdef_dbl: list[int]* = GenApi._g_gen.TypeGenCDef.CDefineDoubleFlags
cdef_str: list[int]* = GenApi._g_gen.TypeGenCDef.CDefineStrFlags
n: sizet = len(cdef_names)
i: sizet
for i in range(n):
nm: str = cdef_names.get(i)
if nm is None:
continue
if _repl_base_cdefine_find(nm) >= 0:
continue
if ReplBaseCDefineCount >= _REPL_BASE_CDEFINE_MAX:
break
val: str = None
if cdef_vals is not None:
val = cdef_vals.get(i)
if val is None:
continue
kind: int = 0
if cdef_str is not None and int(cdef_str.get(i)) == 1:
kind = 2
elif cdef_dbl is not None and int(cdef_dbl.get(i)) == 1:
kind = 1
nc: str = _repl_str_dup(_perm_pool, nm)
vc: str = _repl_str_dup(_perm_pool, val)
if nc is None or vc is None:
continue
j: int = ReplBaseCDefineCount
ReplBaseCDefineNames[j] = sizet(nc)
ReplBaseCDefineValues[j] = sizet(vc)
ReplBaseCDefineKinds[j] = sizet(kind)
ReplBaseCDefineCount = j + 1
ct_names: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefNames
if ct_names is not None:
ct_vals: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefTypeStrs
cn2: sizet = len(ct_names)
i2: sizet
for i2 in range(cn2):
tn: str = ct_names.get(i2)
if tn is None:
continue
if ReplBaseCTypedefCount >= _REPL_BASE_CTYPEDEF_MAX:
break
dup_hit: int = 0
k: int
for k in range(ReplBaseCTypedefCount):
ex: str = rawptr(ReplBaseCTypedefNames[k])
if ex is not None and string.strcmp(ex, tn) == 0:
dup_hit = 1
break
if dup_hit != 0:
continue
tv: str = None
if ct_vals is not None:
tv = ct_vals.get(i2)
if tv is None:
continue
tnc: str = _repl_str_dup(_perm_pool, tn)
tvc: str = _repl_str_dup(_perm_pool, tv)
if tnc is None or tvc is None:
continue
j2: int = ReplBaseCTypedefCount
ReplBaseCTypedefNames[j2] = sizet(tnc)
ReplBaseCTypedefTypeStrs[j2] = sizet(tvc)
ReplBaseCTypedefCount = j2 + 1
return 0
def ReplBaselineTruncate(cd_cnt: int, ct_cnt: int) -> None:
"""把基线水位截断回片段开始时的值(失败片段的增量不得进基线)"""
global ReplBaseCDefineCount, ReplBaseCTypedefCount
if cd_cnt >= 0 and cd_cnt <= ReplBaseCDefineCount:
ReplBaseCDefineCount = cd_cnt
if ct_cnt >= 0 and ct_cnt <= ReplBaseCTypedefCount:
ReplBaseCTypedefCount = ct_cnt
# ---- 片段 import lib:登记 / 追加到链接命令行 ----
def ReplFragImplibRecord(implib_path: str) -> int:
"""登记一个片段 import lib(PermPool 深拷贝);1=已登记,0=未登记(上限/分配失败)"""
global ReplFragImplibCount
if implib_path is None or ReplFragImplibs is None or _perm_pool is None:
return 0
if ReplFragImplibCount >= _REPL_FRAG_IMPLIB_MAX:
return 0
dup: str = _repl_str_dup(_perm_pool, implib_path)
if dup is None:
return 0
ReplFragImplibs[ReplFragImplibCount] = sizet(dup)
ReplFragImplibCount += 1
return 1
# 把已登记的全部片段 import lib 追加到命令缓冲(空格分隔);
# 返回新的位置(-1 = 容量不足,调用方按"不带片段 import lib"重试也不可——直接报错)
def _repl_cmd_append_implibs(cmd: bytes, cap: int, pos: int) -> int:
if cmd is None or ReplFragImplibs is None:
return pos
i: int
for i in range(ReplFragImplibCount):
p: str = rawptr(ReplFragImplibs[i])
if p is None:
continue
pos = _repl_cat_str(cmd, cap, pos, " ")
if pos < 0:
return -1
pos = _repl_cat_str(cmd, cap, pos, p)
if pos < 0:
return -1
return pos
# ---- includes 类注册(会话初始化一次;逐模块隔离,失败只告警)----
def ReplInitIncludesClasses(mb: memhub.MemBuddy*,
sr: rawptr[IncludesScanner.ScanResult],
vpi_dir: str) -> int:
"""把各 includes 模块的 .vpi 注入临时模块 ⇒ 其类进入会话 StructTable;0=完成
为什么需要:REPL 从不翻译 includes 源码 ⇒ 会话 StructTable(类型解析/字段查找/
方法归属的唯一入口)里没有任何 includes 类。`.vpi` 存根含类名 + 字段
(Converter._PyiClass 序列化类体 AnnAssign 与 __init__ 里的 `self.x: T`)+ 方法
签名;VpiReader.ParseVpiIntoModule(即批处理 LoadDepVpiIntoModule 的最后一步)
会把"字段类型全部可解析"的类注册为 StructEntry(含字段表)。
多遍收敛:字段类型可能引用同批的另一个类 ⇒ 单遍必然注册不全;本函数第 1 遍试
全部模块,第 2..N 遍只重试上一遍失败的模块(成功者不重复注入 ⇒ 代价受控)。
启动上界:整段注册有 `_REPL_INC_CLS_BUDGET_MS` 时间预算,超预算即停止尝试剩余
模块(计数进汇总行;这些模块的类在会话里不可用,片段 `import` 时仍按需注册)。
逐模块隔离:每模块一次 _setjmp(复用会话 jmp_buf)⇒ 单模块的 fatal/解析失败只
影响该模块(逐模块告警 + 该模块的类不可用),**不终止会话**、不影响其它模块。
告警聚合:逐模块告警最多打印 `_REPL_INC_CLS_WARN_MAX` 条,其余只累计计数,
末尾一条汇总行给出"成功/失败/缺存根/未尝试/被省略告警"的数量。
⚠ 退出约定:本函数内部改写了 jmp_buf 的跳转目标(指向本帧)⇒ 返回后调用方必须
在**自己的帧上**重新武装恢复点(见 Repl.run 的 includes 段),否则后续 longjmp
会跳到已返回的帧(未定义行为)。
收尾(ReplStubLayoutFinish/ReplStubLayoutEnd)**不在**本函数内做:它们只能在
调用方重新武装恢复点之后调用(见 Repl.run),否则"表满 → VLogger.error →
longjmp 到已返回帧"会在提示符出现前把进程带走。
"""
global ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing
if mb is None or sr is None or vpi_dir is None or sr.Entries is None:
return 1
if ReplJmpBuf is None or _perm_pool is None:
return 1
n_mod: int = int(sr.Count)
if n_mod <= 0:
return 1
if n_mod > _REPL_INC_CLS_MOD_MAX:
n_mod = _REPL_INC_CLS_MOD_MAX
# 每模块状态(8 字节/槽,PermPool):0=未尝试/待重试、1=已完成(成功或确认不可用)
st: sizet* = _perm_pool.alloc(_REPL_INC_CLS_MOD_MAX * 8)
if st is None:
return 1
string.memset(st, 0, _REPL_INC_CLS_MOD_MAX * 8)
# 注入目标:临时 LLVM 模块 + 临时翻译器(只用其注解解析上下文;模块本身丢弃)
# 结构体注册走永久分配(StdlibAlloc=1)⇒ 条目/字段/类型跨片段存活。
GenApi.set_stdlib_alloc(1)
GenApi.reset_visible_structs(mb, 0)
GenApi.set_temp_dir(vpi_dir)
GenApi.SetTempDir(vpi_dir)
GenApi.set_current_file_type(vpi_dir)
tmp_mod: llvmlite.LLVMModule* = llvmlite.new_module(_perm_pool, _REPL_INC_MOD_NAME)
tr_inc: rawptr[HandlesTranslator.Translator] = _perm_pool.alloc(HandlesTranslator.Translator.__sizeof__())
if tmp_mod is None or tr_inc is None:
return 1
tr_inc.__before_init__()
tr_inc.__init__()
# 给临时翻译器一套真状态(函数表/符号表/名集合缓冲区 + Pool):VpiReader 的
# 注入会读 trans._funcs(把 inject 出的 declare 登记进函数表,VpiReader:2130)
# 与 trans.Pool/imported_modules 等;缺它们会在注入中途解引用 None。
tr_inc._init_state(_perm_pool)
tr_inc.ModuleSha1 = ReplSessionSha1
tr_inc._declare_only = 0
# ReplCtx 置位 = 声明本翻译器处于 REPL 会话(Gen 侧唯一开关,见
# Translator.vp:256-257)。注入各模块 `.vpi` 时,其注解可能引用"会话里
# **未登记**(M4.2 布局不可信被拒 / 字段类型不可解析被跳过)的类"——如
# ast 包的 `rawptr[memhub.MemManager]`。此时由 TypeGenResolve 的 REPL 分支
# (`trans.ReplCtx != None` 守卫)按定义侧命名造 opaque 具名类型占位,使注解
# 可解析,避免 S4 fail-fast 中断整个模块的注入;StructTable 仍不登记该类
# (字段访问照旧明确报错)。批处理 ReplCtx 恒 None ⇒ 该分支不经过。
# 片段翻译另有自己的 Translator(ReplExecFragment 内同样置 ReplCtx)。
tr_inc.ReplCtx = ReplCtxObj
es: sizet = IncludesScanner.FileEntry.__sizeof__()
dl: sizet = string.strlen(vpi_dir)
warn_left: int = _REPL_INC_CLS_WARN_MAX
warn_dropped: int = 0
budget_t0: uint64 = win32base.GetTickCount64()
budget_hit: int = 0
p_i: int
for p_i in range(_REPL_INC_CLS_PASSES):
pass_ok: int = 0
i: int
for i in range(n_mod):
if int(st[i]) != 0:
continue
# 启动预算(每个模块一次取时;GetTickCount64 无系统调用开销):
# 超预算即停手,剩余待注册模块留给片段 `import` 的按需路径。
if int(win32base.GetTickCount64() - budget_t0) > _REPL_INC_CLS_BUDGET_MS:
budget_hit = 1
break
if GenApi.get_struct_count() >= _REPL_INC_CLS_STRUCT_GUARD:
stdio.printf("includes 类注册: 结构体表已达 %d 条(上限附近),"
"停止注册;未注册的类在会话里不可用(import 其模块"
"仍可按需注册)\n", GenApi.get_struct_count())
stdio.fflush(None)
return 0
ent: rawptr[IncludesScanner.FileEntry] = rawptr[IncludesScanner.FileEntry](uint64(sr.Entries) + uint64(i) * uint64(es))
if ent is None or ent.Sha1 is None:
st[i] = sizet(1)
ReplIncClsFail += 1
continue
vm: sizet = mb.mark()
vpi_path: str = StubMergerCollect._sliced_path(vpi_dir, dl, ent.Sha1, "vpi")
if vpi_path is None:
st[i] = sizet(1)
ReplIncClsFail += 1
mb.reset_to(vm)
continue
if _is_file(vpi_path) == 0:
# 缺存根(未构建过宿主 / includes 目录不含该模块):该模块的类
# 不可用,会话继续(import 该模块时同样按缺存根退化)
ReplIncClsMissing += 1
if warn_left > 0:
stdio.printf("includes 类注册: 模块 %s 无 .vpi 存根 —— 其类在"
"会话中不可用(不影响其它模块;先按 project.vpj "
"构建一次宿主可生成全部存根)\n", ent.Sha1)
warn_left -= 1
else:
warn_dropped += 1
stdlib.free(vpi_path)
mb.reset_to(vm)
st[i] = sizet(1)
continue
# 注入期把 trans.ModuleSha1 置为**当前 stub 的源模块 sha1**(对齐批处理
# "trans.ModuleSha1 = 正在翻译的文件"):注解里的"本模块裸类名"走
# find_struct_by_module(name, trans.ModuleSha1),父类判定
# (_collect_parents → find_struct_by_module(parent_name, ModuleSha1))也读它;
# 同时 REPL 的"未登记类类型占位"(TypeGenResolve 的 ReplCtx 分支)按它拼
# "{sha1}.{类名}"。沿用会话 sha1 会让本模块类查找与占位命名都错位。
tr_inc.ModuleSha1 = ent.Sha1
# 每模块一个恢复点:单模块 fatal 只丢弃该模块**本次**注入(已注册的类
# 由结构体表去重语义兜住:同名同 sha1 再注入时 find_struct_by_module
# 命中即只 add_visible_struct,不新增条目)。注意:longjmp 回来后
# vpi_path/vm 之外的本帧局部(在 _setjmp 之后赋值的)值不确定,故恢复
# 分支只做 mb.reset_to(vm)——vpi_path 是 stdlib 串,此处不释放(每模块
# 泄漏 ≤512B,仅发生在 fatal 路径)。
rc_m: int = _setjmp(ReplJmpBuf, None)
if rc_m != 0:
ReplIncClsFail += 1
if warn_left > 0:
stdio.printf("includes 类注册: 模块 %d 的 .vpi 注入中断"
"(其类可能不完整;会话继续)\n", i)
warn_left -= 1
else:
warn_dropped += 1
mb.reset_to(vm)
continue
ok_m: int = VpiReader.ParseVpiIntoModule(_perm_pool, tmp_mod, vpi_path, tr_inc)
if ok_m != 0:
st[i] = sizet(1)
ReplIncClsMods += 1
pass_ok += 1
else:
ReplIncClsFail += 1
if warn_left > 0:
stdio.printf("includes 类注册: 模块 %s 的 .vpi 注入失败"
"(该模块的类不完整/不可用;会话继续)\n", ent.Sha1)
warn_left -= 1
else:
warn_dropped += 1
stdlib.free(vpi_path)
mb.reset_to(vm)
# 本遍没有任何模块成功 ⇒ 后续遍不会带来新信息(失败的模块留在待重试集里,
# 但重试同样会失败;省下全部重试的解析开销)
if pass_ok == 0:
break
# 汇总(聚合告警):逐模块告警已按上限截断,这里给出全部计数与未尝试模块数。
skip_cnt: int = 0
si: int
for si in range(n_mod):
if int(st[si]) == 0:
skip_cnt += 1
stdio.printf("includes 类: 已注册 %d 个模块的 vpi 类(失败 %d,缺存根 %d,"
"未尝试/待重试 %d,省略告警 %d)\n",
ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing,
skip_cnt, warn_dropped)
if budget_hit != 0:
stdio.printf("includes 类: 启动预算 %d ms 用尽,已停止启动期注册(剩余 %d 个"
"模块的类在会话里不可用;片段 `import` 该模块时仍按需注册)\n",
_REPL_INC_CLS_BUDGET_MS, skip_cnt)
stdio.fflush(None)
return 0
# ============================================================
# 会话初始化:建 %TEMP%\TransPyV_repl\<pid>\ + 一次性分配常驻缓冲 + ctx
# 返回 0 成功;非 0 失败(原因由调用方打印)。
# ============================================================
def ReplInitSession() -> int:
"""建临时目录 + PermPool 常驻缓冲 + ctx 会话内存;0=成功"""
global ReplTmpDir, ReplNameBuf, ReplSrcBuf, ReplSrcPath, ReplIrBuf, ReplNeedleBuf
global ReplCtxBuf, ReplCtxObj, ReplVarTable, ReplScratch
global ReplBlockBuf, ReplBlockLen, ReplInBlock
global ReplLiftBuf, ReplLiftNames, ReplLiftNameCount, ReplSigTags, ReplSigBits
global ReplSigResult, ReplLineShow, ReplMsgName, ReplTySpan, ReplTySpan3
global ReplIrDefOff, ReplSigText, ReplSigText2, ReplWrapPos, ReplSavedFuncCount
global ReplSessionSha1, ReplFragImplibs, ReplFragImplibCount
global ReplBaseCDefineNames, ReplBaseCDefineValues, ReplBaseCDefineKinds
global ReplBaseCDefineCount, ReplBaseCTypedefNames, ReplBaseCTypedefTypeStrs
global ReplBaseCTypedefCount, ReplSavedCDefineCount, ReplSavedCTypedefCount
global ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing
if _perm_pool is None:
return 1
# ① %TEMP% 前缀(GetTempPathA 返回值含结尾反斜杠;0 表示失败)
tmp_buf: bytes = _perm_pool.alloc(_REPL_TMP_CAP)
if tmp_buf is None:
return 1
tmp_len: ULONG = w32file.GetTempPathA(_REPL_TMP_CAP, tmp_buf)
if tmp_len == 0:
return 1
if int(tmp_len) >= _REPL_TMP_CAP:
return 1
# ② 会话目录:<TEMP>TransPyV_repl/<pid>(pid 隔离并发会话;路径统一用 '/')
ReplTmpDir = _perm_pool.alloc(_REPL_PATH_CAP)
if ReplTmpDir is None:
return 1
viperlib.snprintf(ReplTmpDir, _REPL_PATH_CAP, "%sTransPyV_repl/%u",
tmp_buf, win32process.GetCurrentProcessId())
if BuildPipeline.ensure_dir(ReplTmpDir) != 0:
return 1
# ③ 常驻缓冲(会话期存活,不按片段反复分配)
ReplNameBuf = _perm_pool.alloc(_REPL_NAME_CAP)
ReplSrcBuf = _perm_pool.alloc(_REPL_FRAG_SRC_CAP)
ReplSrcPath = _perm_pool.alloc(_REPL_PATH_CAP)
ReplIrBuf = _perm_pool.alloc(_REPL_IR_CAP)
ReplNeedleBuf = _perm_pool.alloc(_REPL_NEEDLE_CAP)
ReplBlockBuf = _perm_pool.alloc(_REPL_BLOCK_CAP)
ReplBlockLen = 0
ReplInBlock = 0
if ReplNameBuf is None or ReplSrcBuf is None or ReplSrcPath is None:
return 1
if ReplIrBuf is None or ReplNeedleBuf is None:
return 1
if ReplBlockBuf is None:
return 1
ReplBlockBuf[0] = 0
# ③' M3 常驻缓冲:提升源码 / 会话函数名表 / 签名 token scratch / 诊断与渲染缓冲
ReplLiftBuf = _perm_pool.alloc(_REPL_LIFT_CAP)
ReplLiftNames = _perm_pool.alloc(_REPL_FN_NAMES_CAP)
ReplSigTags = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX)
ReplSigBits = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX)
ReplSigResult = _perm_pool.alloc(ReplSigOut.__sizeof__())
ReplLineShow = _perm_pool.alloc(_REPL_DIAG_CAP)
ReplMsgName = _perm_pool.alloc(_REPL_FN_NAME_MAX)
ReplTySpan = _perm_pool.alloc(_REPL_FN_TY_CAP)
ReplTySpan3 = _perm_pool.alloc(_REPL_FN_TY_CAP)
ReplSigText = _perm_pool.alloc(_REPL_SIG_TEXT_CAP)
ReplSigText2 = _perm_pool.alloc(_REPL_SIG_TEXT_CAP)
if ReplLiftBuf is None or ReplLiftNames is None or ReplSigTags is None \
or ReplSigBits is None or ReplSigResult is None:
return 1
if ReplLineShow is None or ReplMsgName is None or ReplTySpan is None \
or ReplTySpan3 is None or ReplSigText is None or ReplSigText2 is None:
return 1
ReplLiftBuf[0] = 0
ReplLiftNames[0] = 0
ReplLiftNameCount = 0
ReplIrDefOff = -1
ReplWrapPos = 0
ReplSavedFuncCount = 0
# ④ ctx 会话内存(片段首形参指向它):头 64B + 变量槽 64×16B + 函数指针槽 32×16B,全部清零
ReplCtxBuf = _perm_pool.alloc(_REPL_CTX_SIZE)
if ReplCtxBuf is None:
return 1
string.memset(ReplCtxBuf, 0, _REPL_CTX_SIZE)
# ⑤ 会话变量表 + 影子条目(PermPool;条目名/结构体名由 Gen 侧深拷贝进同一池)
entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__()
ReplVarTable = _perm_pool.alloc(entry_size * GenTypes.REPL_VAR_CAP)
if ReplVarTable is None:
return 1
string.memset(ReplVarTable, 0, entry_size * GenTypes.REPL_VAR_CAP)
ReplScratch = _perm_pool.alloc(entry_size)
if ReplScratch is None:
return 1
string.memset(ReplScratch, 0, entry_size)
# 影子条目的名字(错误消息可读:"<expr>")
repl_scratch_name: str = _perm_pool.alloc(8)
if repl_scratch_name is None:
return 1
viperlib.snprintf(repl_scratch_name, 8, "%s", "<expr>")
ReplScratch.Name = repl_scratch_name
ReplScratch.TyName = None
ReplScratch.Offset = 0
ReplScratch.Kind = GenTypes.REPL_KIND_UNTYPED
ReplScratch.TyTag = 0
ReplScratch.Bits = 0
# ⑤' 会话函数表(M3;PermPool,宿主写、Gen 只读)——条目数组清零
func_entry_size: sizet = GenTypes.ReplFuncEntry.__sizeof__()
ReplFuncTable: rawptr[GenTypes.ReplFuncEntry] = _perm_pool.alloc(func_entry_size * GenTypes.REPL_FUNC_CAP)
if ReplFuncTable is None:
return 1
string.memset(ReplFuncTable, 0, func_entry_size * GenTypes.REPL_FUNC_CAP)
# ⑥ 会话上下文对象(Gen 侧唯一开关:trans.ReplCtx)
ReplCtxObj = _perm_pool.alloc(GenTypes.ReplCtx.__sizeof__())
if ReplCtxObj is None:
return 1
string.memset(ReplCtxObj, 0, GenTypes.ReplCtx.__sizeof__())
ctx_param: str = _perm_pool.alloc(_REPL_NAME_CAP)
if ctx_param is None:
return 1
viperlib.snprintf(ctx_param, _REPL_NAME_CAP, "%s", GenTypes.REPL_CTX_PARAM)
ReplCtxObj.Marker = GenTypes.REPL_CTX_MARKER
ReplCtxObj.Table = ReplVarTable
ReplCtxObj.Count = 0
ReplCtxObj.Cap = GenTypes.REPL_VAR_CAP
ReplCtxObj.CtxParam = ctx_param
ReplCtxObj.SlotSize = GenTypes.REPL_SLOT_SIZE
ReplCtxObj.Scratch = ReplScratch
ReplCtxObj.Perm = _perm_pool
# 会话函数表(M3):切片跨片段存活 ⇒ 表体/条目名/token 数组都在 PermPool
ReplCtxObj.FuncTable = ReplFuncTable
ReplCtxObj.FuncCount = 0
ReplCtxObj.FuncCap = GenTypes.REPL_FUNC_CAP
ReplSavedFuncCount = 0
# ⑦ ctx 固定头:magic/version 由宿主写一次(片段只读约定,不校验)
head: rawptr[GenTypes.ReplResult] = _ctx_head()
if head is None:
return 1
head.Magic = GenTypes.REPL_CTX_MAGIC
head.Version = GenTypes.REPL_CTX_VERSION
head.Kind = GenTypes.REPL_RES_NONE
head.Reserved0 = 0
head.A = 0
head.B = 0
head.VarCount = 0
head.LastFrag = 0
# ⑧ M4 常驻状态:会话 sha1 + 片段 import lib 表 + 会话基线(CDefine/CTypedef)
if ReplInitSessionSha1() != 0:
return 1
ReplFragImplibs = _perm_pool.alloc(_REPL_FRAG_IMPLIB_ARR_CAP)
ReplBaseCDefineNames = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX)
ReplBaseCDefineValues = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX)
ReplBaseCDefineKinds = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX)
ReplBaseCTypedefNames = _perm_pool.alloc(8 * _REPL_BASE_CTYPEDEF_MAX)
ReplBaseCTypedefTypeStrs = _perm_pool.alloc(8 * _REPL_BASE_CTYPEDEF_MAX)
if ReplFragImplibs is None or ReplBaseCDefineNames is None \
or ReplBaseCDefineValues is None or ReplBaseCDefineKinds is None:
return 1
if ReplBaseCTypedefNames is None or ReplBaseCTypedefTypeStrs is None:
return 1
string.memset(ReplFragImplibs, 0, _REPL_FRAG_IMPLIB_ARR_CAP)
string.memset(ReplBaseCDefineNames, 0, 8 * _REPL_BASE_CDEFINE_MAX)
string.memset(ReplBaseCDefineValues, 0, 8 * _REPL_BASE_CDEFINE_MAX)
string.memset(ReplBaseCDefineKinds, 0, 8 * _REPL_BASE_CDEFINE_MAX)
string.memset(ReplBaseCTypedefNames, 0, 8 * _REPL_BASE_CTYPEDEF_MAX)
string.memset(ReplBaseCTypedefTypeStrs, 0, 8 * _REPL_BASE_CTYPEDEF_MAX)
ReplFragImplibCount = 0
ReplBaseCDefineCount = 0
ReplBaseCTypedefCount = 0
ReplSavedCDefineCount = 0
ReplSavedCTypedefCount = 0
ReplIncClsMods = 0
ReplIncClsFail = 0
ReplIncClsMissing = 0
return 0
# ============================================================
# includes 会话级初始化:扫描 includes 目录 → 注册"模块名 → SHA1" → 预生成 .vpi 存根
#
# 为什么必须做:片段按**全量翻译**走(tr._declare_only = 0),片段体里的 import 会
# 经 ImportsGenMixin._check_import_sha1(ImportsGenMixin.vp:781-812)校验"模块名 →
# SHA1"是否已注册,未注册即 fail-fast("模块 'X' 未注册 SHA1(includes 扫描未找到
# 该模块…)")。该判据查的是 StubMerger 的全局存储器
# (StubMerger/__init__.vp:46-50 的 _sha1_store_arr/_mod/_rel/_incdir + _count,
# 由 StubMergerStore.vp 读写);批处理路径上这张表由 Phase1 依赖图扫描填充
# (Phase1Main.RunPhase1 → StubMergerBuild._BuildReachableSha1Set →
# AddIncludeToSha1MapStore,StubMergerBuild.vp:1049),而 REPL 从不跑 Phase1
# ⇒ 表为空 ⇒ 片段里任何 `import <includes 模块>` 都报未注册 SHA1。
#
# .vpi 存根同理:跨模块的返回类型/CDefine/CExport 判定都要读
# {GenApi temp_dir}/{sha1[0:2]}/{sha1}.vpi(CallGenMixin.vp:1364-1370、
# CallGenBase._LookupMethodRetTyFromTextLL:1092、ExprGenCdef.vp:282),批处理由
# Phase1 1b-pre 预生成(Phase1Main.vp:716-798),REPL 不跑 Phase1 ⇒ 目录里没有
# ⇒ 跨模块解析退化为默认推断(如返回类型默认 i32)。
#
# 复用既有能力(不另写扫描):
# - 扫描:IncludesScanner.scan_includes(与 Phase1Main.vp:526 同款调用);
# - 注册:StubMergerStore.PopulateSha1MapStore(全量 includes → 同一张表,
# 写入字段与 AddIncludeToSha1MapStore 一致)。批处理现入口
# _BuildReachableSha1Set 与 Phase1 强耦合(需 Config.SourceDir、deps 图、
# worklist、写 deps.txt),repl.vpj 刻意不含 source_dir,故取同族的"全量扫描
# + 注册"入口;差别是该表在批处理只含**可达** includes,REPL 含全部 includes
# (超集,注册内容/消费者完全一致:LookupSha1ByModName/Suffix);
# - 存根:StubConverter._GeneratePyiFromAst + StubMergerCollect._sliced_path
# (与 Phase1Main.vp:787-798、BuildPipeline.TranslateFileGetTrans:121-131 同款)。
#
# 一次性 vs 每次:放会话开始时做一次。includes 源文件在一个会话内视为静态
# (片段新增的是片段源码,不落 includes 目录),重扫会给每个片段增加 O(文件数)
# 的读盘 + SHA1/parse 成本;改动 includes 后重启会话即可。
#
# 失败口径(宽容,与 M0 的恢复点一致):includes 目录缺失/扫描为 0/存根生成失败
# 都只告警,**不终止会话**——"不带 import 的片段"仍照常可用。
# ============================================================
def _repl_gen_includes_vpi(mb: memhub.MemBuddy*,
sr: rawptr[IncludesScanner.ScanResult],
vpi_dir: str) -> int:
"""为 includes 扫描结果预生成 .vpi 存根;返回新生成个数(-1=参数错误)
已存在同名 .vpi 直接复用(.vpi 按内容 SHA1 命名 ⇒ 同名必然同内容,与
Phase1 1b-pre 的"vpi 已存在则跳过"同判据)。每文件取一次 mb.mark() 并在末尾
reset_to:读文件/parse/generate 的瞬时分配不跨文件累积(对齐 Phase1 1b-pre)。
"""
global ReplIncVpiNew, ReplIncVpiReuse, ReplIncVpiFail
if mb is None or sr is None or vpi_dir is None:
return -1
if sr.Entries is None:
return -1
es: sizet = IncludesScanner.FileEntry.__sizeof__()
dl: sizet = string.strlen(vpi_dir)
n: int
for n in range(sr.Count):
ent: rawptr[IncludesScanner.FileEntry] = rawptr[IncludesScanner.FileEntry](uint64(sr.Entries) + uint64(n) * es)
if ent is None or ent.Sha1 is None or ent.Path is None:
ReplIncVpiFail += 1
continue
fm: sizet = mb.mark()
vpi_path: str = StubMergerCollect._sliced_path(vpi_dir, dl, ent.Sha1, "vpi")
if vpi_path is None:
ReplIncVpiFail += 1
mb.reset_to(fm)
continue
# 已有存根 → 复用
vf: rawptr[fileio.File] = fileio.File(vpi_path, fileio.MODE.R)
if not vf.closed:
vf.close()
stdlib.free(vpi_path)
ReplIncVpiReuse += 1
mb.reset_to(fm)
continue
# 读源文件(1MB 上限与 Phase1/BuildPipeline 的 SRC_BUF_SIZE 一致)
f: rawptr[fileio.File] = fileio.File(ent.Path, fileio.MODE.R)
if f.closed:
stdlib.free(vpi_path)
ReplIncVpiFail += 1
mb.reset_to(fm)
continue
if f.size() >= _REPL_SRC_BUF_CAP:
f.close()
stdlib.free(vpi_path)
ReplIncVpiFail += 1
mb.reset_to(fm)
continue
src_buf: bytes = stdlib.malloc(_REPL_SRC_BUF_CAP)
if src_buf is None:
f.close()
stdlib.free(vpi_path)
ReplIncVpiFail += 1
mb.reset_to(fm)
continue
bytes_read: LONG = f.read_all(src_buf, _REPL_SRC_BUF_CAP)
f.close()
if bytes_read <= 0:
stdlib.free(src_buf)
stdlib.free(vpi_path)
ReplIncVpiFail += 1
mb.reset_to(fm)
continue
if bytes_read < _REPL_SRC_BUF_CAP:
src_buf[bytes_read] = 0
# 解析(AST 全部分配在本文件的 mb 窗口内)
lx: ast.Lexer* = ast.new_lexer(mb)
tree: ast.AST* = None
if lx is not None:
ast._lexer_init(lx, src_buf, mb)
tokens: ast.Token* = ast.tokenize(lx)
tree = ast.parse_tokens(mb, tokens)
if tree is None:
stdlib.free(src_buf)
stdlib.free(vpi_path)
ReplIncVpiFail += 1
mb.reset_to(fm)
continue
pyi_buf: bytes = stdlib.malloc(_REPL_VPI_BUF_CAP)
if pyi_buf is None:
stdlib.free(src_buf)
stdlib.free(vpi_path)
ReplIncVpiFail += 1
mb.reset_to(fm)
continue
# rel_path 传绝对路径 ent.Path:vpi 生成期的 fatal_error 需按该路径读源码行
pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, ent.Path,
pyi_buf, _REPL_VPI_BUF_CAP)
if pos > 0:
pf: rawptr[fileio.File] = fileio.File(vpi_path, fileio.MODE.W)
if not pf.closed:
pf.write(pyi_buf, pos)
pf.close()
ReplIncVpiNew += 1
else:
ReplIncVpiFail += 1
else:
ReplIncVpiFail += 1
stdlib.free(pyi_buf)
stdlib.free(src_buf)
stdlib.free(vpi_path)
mb.reset_to(fm)
return ReplIncVpiNew
def ReplInitIncludes(mb: memhub.MemBuddy*) -> int:
"""会话级 includes 初始化:扫描 → 注册 SHA1 → 预生成 .vpi → 注册泛型模板
→ 逐模块类注册;0=成功,非 0=降级跳过
降级(返回非 0)时调用方只告警、继续进入读行循环(不带 import 的片段不受影响)。
⚠ 本函数内部会经 `ReplInitIncludesClasses` 使用**嵌套 _setjmp**(逐模块隔离),
返回后 jmp_buf 的跳转目标指向那些已返回的帧 ⇒ 调用方必须在**自己的帧上**重新
武装恢复点,再做 includes 收尾(ReplStubLayoutFinish/ReplStubLayoutEnd,见 run)。
故收尾不放在本函数内。
"""
global ReplIncMods, ReplIncMs
if mb is None:
return 1
inc_dirs: str = Config.IncludesDirs
if inc_dirs is None or string.strlen(inc_dirs) == 0:
stdio.printf("REPL: 工程未配置 includes 目录(project.vpj 的 includes),"
"跳过 includes 初始化;import includes 模块会报未注册 SHA1\n")
return 1
# .vpi 目录与片段侧一致:优先工程临时目录,无工程配置时退回会话目录
vpi_dir: str = Config.TempDir
if vpi_dir is None:
vpi_dir = ReplTmpDir
if vpi_dir is None:
stdio.printf("REPL: 无可用 .vpi 目录(Config.TempDir 与会话目录均为空),"
"跳过 includes 初始化\n")
return 1
# ① 扫描 includes(可达性不判定:REPL 无 SourceDir/deps 图,取全量超集)
t0: uint64 = win32base.GetTickCount64()
sr: rawptr[IncludesScanner.ScanResult] = IncludesScanner.scan_includes(mb, inc_dirs)
if sr is None:
stdio.printf("REPL: includes 扫描失败(目录: %s);"
"import includes 模块会报未注册 SHA1\n", inc_dirs)
return 1
# ② 注册"模块名 → SHA1"(同一张 StubMerger 全局表,批处理消费者直接可用)
n: int = StubMergerStore.PopulateSha1MapStore(sr)
ReplIncMs = int(win32base.GetTickCount64() - t0)
if n <= 0:
stdio.printf("REPL: includes 扫描未注册任何模块(目录: %s):"
"请检查 includes 路径与 .vp 文件\n", inc_dirs)
return 1
ReplIncMods = n
# ③ 预生成 .vpi 存根(失败逐个计数,不终止会话)
_repl_gen_includes_vpi(mb, sr, vpi_dir)
stdio.printf("includes: 已注册 %d 个模块(扫描耗时 %d ms);"
"存根 新生成 %d / 复用 %d / 失败 %d\n",
n, ReplIncMs, ReplIncVpiNew, ReplIncVpiReuse, ReplIncVpiFail)
# ③'' 会话泛型模板注册(**真注册**,不再只造 opaque 类型占位):按批处理同一
# 机制把 includes 的泛型类模板(list/dict/Vector/…)注册进会话泛型模板表——
# Phase1Scan._PreRegisterAllGenericTemplates 复用 includes 源码,先按
# "class X[" 文本标记预筛(不 parse 全部文件),仅对命中文件用**永久池**
# 解析并调 GenApi._register_generic_template ⇒ 模板节点与 4 张表
# (GenericClassNames/Nodes/Sha1s/Files + GenericClassTpParams)都落在
# PermPool(M4 已定的会话级常驻池;不随片段 reset 回收),与批处理建表
# 内容/顺序逐字一致。
# 为什么必须:REPL 从不跑 Phase1/Phase2、也不翻译 includes 源码 ⇒ 会话里
# 模板表为空 ⇒ 注解里的 `list[...]`/`dict[...]` 泛型下标恒 miss(日志
# "[GENERIC-DBG] 模板未注册: list"),只能退到"仅类型"opaque 占位;而
# opaque 只让类型"有名"(可过注入校验、指针签名与宿主兼容),**没有元素
# 类型/布局/方法**(.get() 无从按正确元素类型分派,字段与方法都不可用)。
# 真注册后 _resolve_generic_subscript_ty 走既有特化路径:`list[rawptr[AST]]`
# → 真实特化 struct(含 __data__/__count__/__capacity__ 布局)+ 方法 declare。
# 失败隔离:本调用在 run 的恢复点保护下(其内部唯一终止出口是"源文件超
# 上限"的 VLogger.error → longjmp 回 run 的 includes 恢复点,会话继续、
# 提示符照常出现);函数自身对每个文件 continue,不抛异常。
# 影响面:只写会话泛型模板表;批处理不调用本函数(其表由 Phase1/Phase2 的
# 两遍预注册建),表内容与插入顺序不受影响 ⇒ 批处理逐字不变。
cur_file_saved: str = GenApi.get_current_file()
n_gen_tpl: int = Phase1Scan._PreRegisterAllGenericTemplates(mb, sr)
if cur_file_saved is not None:
GenApi.set_current_file_type(cur_file_saved)
if n_gen_tpl <= 0:
stdio.printf("includes 泛型模板: 0 个 —— 注解里的 list[...]/dict[...] 只能按"
"\"仅类型\"占位解析(元素布局与方法不可用)\n")
# ③' M4.2:开启"存根布局恢复"——随后的 vpi 注入会按 .stub.ll 校验并登记
# includes 类的**完整布局**(含父类展平字段与 __vtable__)与虚表方法索引;
# 布局不可信 ⇒ 该类的 StructEntry 不登记(字段访问明确报错,不静默错位)。
# 同时清空"泛型类归属表"(跟随后的 vpi 注入逐 .vpi 重新登记;会话级单例,
# 见 StubMergerStore.RegisterGenericOwner):注解里的泛型下标要靠它取
# "模板模块 sha1" 拼定义侧特化名造类型占位(M4.3)。
StubMergerStore.ResetGenericOwners()
VpiReader.ReplStubLayoutBegin(vpi_dir)
# ④ M4 类注册:把各模块 .vpi 注入临时模块 ⇒ 其类(含字段表)进入**会话**
# StructTable(这是后续片段里 `memhub.NewMemPool(...)` 之类跨模块构造/
# 方法调用的前提:类型解析与字段查找都只认 StructEntry)。
# 逐模块隔离 + 多遍收敛 + 启动时间预算,失败只告警(见 ReplInitIncludesClasses)。
ReplInitIncludesClasses(mb, sr, vpi_dir)
return 0
# ============================================================
# M2 - 片段执行通路(语句 → IR → llc → clang++ -shared → DLL → 调用 → ctx 回显)
# M3 - 顶层 def 提升 + 会话函数表 + 定义端签名解析(跨片段函数)
#
# 本段(到 _repl_ir_func_sig 结束)是 M3 的全部宿主侧机制:装配期提升顶层 def、
# 从片段 IR 解析定义端 ABI、注册前校验、DLL 加载后取址写 ctx 槽 + 建表。
# 片段执行通路本体见 _build_frag_src / _exec_repl_src / ReplExecFragment(其后)。
#
# ---- M3 顶层 def 提升 ----
# 目标:片段里顶层(顶格)的 `def` 块被**提升**为片段模块里的顶层 `export def`
# (裸名导出),片段体只留其余语句 ⇒ 该函数进 DLL 导出表,宿主
# GetProcAddress(裸名) 取到地址、写入 ctx 的函数指针槽,后续片段即可经槽间接
# 调用(函数代码只在定义片段里一份,调用点不复制、不重编译)。
#
# 为什么选**源码行分割**而不是 AST 拆分 + 重新打印:
# ① 本仓库没有"AST → 源码"的打印器(Gen 侧只有 IR 打印);自写打印器要覆盖
# 全部节点类型/字面量转义/注解还原/缩进规则,代码量与走样风险远超收益;
# ② 行分割只做两件可预测的事——把 def 块**原样搬**到模块区、在 def 头上注入
# ctx 形参并加 `export `;其余字符(docstring/注释/多行表达式/字符串)逐字保留,
# 不存在"重新打印后语义走样"这一整类风险。
# 误判面(已知,写进文档):判断只看"是否顶格 def(async 拒绝)"+"签名是否单行",
# 不做缩进语义分析;签名行内的字符串字面量若含括号会干扰 '(' ')' 配对。
#
# 精确规则:
# ① 提升块 = [可选若干顶格 '@' 装饰器行] + [顶格 def 头行] + 其后所有
# "缩进 > 0 的行或纯空白行";遇到下一个**顶格非空行**即结束(该行重新判定)。
# ② def 头行形如 `def <名>[(<泛型参数>)](…)…:`,且 '(' 到行尾的 ':' 必须在
# **同一行**(多行签名 ⇒ 明确报错,不猜);注入规则:名字后的 '(' 之后插入
# "__repl_ctx__: bytes, "(形参表为空时插入不带逗号的版本)。
# ③ 提升行按原文写出(不缩进);其余行进片段体,每行统一加 4 空格(块路径口径)。
# ④ 拒绝(明确报错 + **整片段不执行**):
# - 顶格 async def(协程 ABI 与 M3 的签名 token 不相容)
# - 装饰器行后不是 def 头行 / def 头缺 '(' / ')' 或 ':' 不在同一行
# - 形参表里已出现 __repl_ctx__(用户自带 ctx 形参)
# - 函数体(含嵌套)任一行以 raise 开头(EH 出参 ⇒ 间接调用 arity 不匹配)
# - 同一片段内同名 def 出现两次 / 名字超长 / 数量超 _REPL_LIFT_MAX
# ⑤ 不提升:缩进 > 0 的 def(走原嵌套函数/闭包路径);`fn`/`func`/`function`/
# `define` 形式不在本次范围(同样走原路径)。
# ============================================================
# 把一行(可能非 NUL 结尾)截断拷进会话缓冲,供 %s 打印
def _repl_show_line(p: bytes, n: int) -> str:
"""行片段 → 会话缓冲里的 NUL 结尾串(截断到 118 字节)"""
if ReplLineShow is None or p is None:
return "(?)"
m: int = n
if m > 118:
m = 118
if m < 0:
m = 0
string.memcpy(ReplLineShow, p, sizet(m))
ReplLineShow[m] = 0
return ReplLineShow
# 行长度(到 '\n' 或 NUL 为止)
def _repl_line_len(p: bytes) -> int:
n: int = 0
while p[n] != '\n' and p[n] != 0:
n += 1
return n
# 前导空白字节数
def _repl_indent_len(p: bytes, n: int) -> int:
i: int = 0
while i < n and (p[i] == ' ' or p[i] == '\t'):
i += 1
return i
# 在 [ind, n) 处是否以关键词 kw 开头(且 kw 后是空白)
def _repl_line_has_kw(p: bytes, n: int, ind: int, kw: str) -> int:
k: int = int(string.strlen(kw))
if ind + k + 1 > n:
return 0
if string.strncmp(p + ind, kw, sizet(k)) != 0:
return 0
c: char = p[ind + k]
if c != ' ' and c != '\t':
return 0
return 1
# 追加字节段到 dst(维护末尾 NUL);返回新长度,-1=容量不足
def _repl_cat(dst: bytes, cap: int, pos: int, src: bytes, n: int) -> int:
if dst is None or src is None or pos < 0 or n < 0:
return -1
if pos + n + 1 > cap:
return -1
string.memcpy(dst + pos, src, sizet(n))
pos += n
dst[pos] = 0
return pos
# 追加 NUL 结尾串到 dst;返回新长度,-1=容量不足
def _repl_cat_str(dst: bytes, cap: int, pos: int, s: str) -> int:
if s is None:
return pos
return _repl_cat(dst, cap, pos, s, int(string.strlen(s)))
# 提升出的函数名入表(保留前缀检查 + 同片段查重 + 上限);0=成功,1=拒绝(已打印)
def _repl_lift_add_name(name: str) -> int:
global ReplLiftNameCount
if name is None or ReplLiftNames is None:
return 1
# 保留前缀:片段入口函数名("__repl_frag_<n>")是 Gen 侧判定"片段入口作用域"的
# 依据(会话变量的写路径只在该作用域生效),用户不得占用该前缀
pfx: str = GenTypes.REPL_ENTRY_PREFIX
pl: int = int(string.strlen(pfx))
if string.strncmp(name, pfx, sizet(pl)) == 0:
stdio.printf("M3: 会话函数名不能以保留前缀 '%s' 开头:%s\n", pfx, name)
return 1
i: int
for i in range(ReplLiftNameCount):
if string.strcmp(ReplLiftNames + i * _REPL_FN_NAME_MAX, name) == 0:
stdio.printf("M3: 同一片段内重复定义会话函数 '%s'(请只定义一次)\n", name)
return 1
if ReplLiftNameCount >= _REPL_LIFT_MAX:
stdio.printf("M3: 单个片段最多提升 %d 个会话函数(本片段已提升 %d 个)\n",
_REPL_LIFT_MAX, ReplLiftNameCount)
return 1
string.strcpy(ReplLiftNames + ReplLiftNameCount * _REPL_FN_NAME_MAX, name)
ReplLiftNameCount += 1
return 0
# 解析并改写一行顶格 def 头 → 追加到 ReplLiftBuf
#("export def <名>(__repl_ctx__: bytes, <原形参表>) -> <原返回注解>:",其余逐字保留)
# 名字写入 name_out(NUL 结尾)。返回 0=成功;1=规则拒绝(已打印);2=缓冲不足
def _repl_lift_def_header(line: bytes, n: int, name_out: bytes, name_cap: int) -> int:
if n < 5 or name_out is None or ReplLiftBuf is None:
return 1
p: int = 3 # 跳过 "def"
while p < n and (line[p] == ' ' or line[p] == '\t'):
p += 1
# 名字(标识符字符)
ns: int = p
while p < n:
c: char = line[p]
is_id: int = 0
if (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') \
or c == '_':
is_id = 1
if is_id == 0:
break
p += 1
nlen: int = p - ns
if nlen <= 0:
stdio.printf("M3: def 头缺少函数名:%s\n", _repl_show_line(line, n))
return 1
if nlen >= name_cap:
stdio.printf("M3: 会话函数名过长(上限 %d 字节):%s\n",
name_cap - 1, _repl_show_line(line, n))
return 1
string.memcpy(name_out, line + ns, sizet(nlen))
name_out[nlen] = 0
# PEP 695 泛型参数 [T]:跳过平衡的方括号(翻译侧把 T 映射为 int)
if p < n and line[p] == '[':
d: int = 0
while p < n:
if line[p] == '[':
d += 1
elif line[p] == ']':
d -= 1
if d == 0:
p += 1
break
p += 1
if d != 0:
stdio.printf("M3: def 头的泛型参数方括号不闭合:%s\n", _repl_show_line(line, n))
return 1
if p >= n or line[p] != '(':
stdio.printf("M3: def 头在函数名之后缺少 '(':%s\n", _repl_show_line(line, n))
return 1
lp: int = p
# 匹配的 ')'
d2: int = 0
rp: int = -1
q: int = lp
while q < n:
c2: char = line[q]
if c2 == '(':
d2 += 1
elif c2 == ')':
d2 -= 1
if d2 == 0:
rp = q
break
q += 1
if rp < 0:
stdio.printf("M3: def 头的 ')' 缺失(签名必须写在一行内):%s\n", _repl_show_line(line, n))
return 1
# ':' 必须在同一行(返回注解也必须在同一行)
has_colon: int = 0
q2: int = rp + 1
while q2 < n:
if line[q2] == ':':
has_colon = 1
break
q2 += 1
if has_colon == 0:
stdio.printf("M3: def 头的 ':' 不在同一行(签名必须写在一行内):%s\n",
_repl_show_line(line, n))
return 1
# 形参表是否为空
empty: int = 1
q3: int = lp + 1
while q3 < rp:
if line[q3] != ' ' and line[q3] != '\t':
empty = 0
break
q3 += 1
# 用户自带 __repl_ctx__ 形参 ⇒ 拒绝(避免与注入的首形参重复)
if empty == 0:
seg: int = lp + 1
while seg + 12 <= rp:
if line[seg] == '_' and string.strncmp(line + seg, GenTypes.REPL_CTX_PARAM,
sizet(12)) == 0:
stdio.printf("M3: 会话函数 '%s' 不能自定义 %s 形参"
"(提升时自动注入为第一个形参)\n", name_out, GenTypes.REPL_CTX_PARAM)
return 1
seg += 1
# 写出:"export " + 原头 [0,lp] + 注入 + 原头 [lp+1,n) + '\n'
pos: int = int(string.strlen(ReplLiftBuf))
pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, pos, "export ")
pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, pos, line, lp + 1)
if empty != 0:
pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, pos, "__repl_ctx__: bytes")
else:
pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, pos, "__repl_ctx__: bytes, ")
pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, pos, line + lp + 1, n - lp - 1)
pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, pos, "\n")
if pos < 0:
stdio.printf("M3: 提升源码缓冲(%d 字节)不足\n", _REPL_LIFT_CAP)
return 2
return 0
# 拆分片段体:提升块 → ReplLiftBuf(模块级 export def,原文不缩进);
# 其余行 → ReplSrcBuf(自 ReplWrapPos 起,每行 4 空格);并登记被提升的函数名。
# 返回 0=成功;1=规则拒绝(已打印);2=缓冲不足。成功时 ReplWrapPos 更新为片段体末尾位置。
def _repl_split_body(body: bytes) -> int:
global ReplWrapPos
if body is None or ReplLiftBuf is None or ReplSrcBuf is None:
return 2
total: int = int(string.strlen(body))
i: int = 0
pos: int = ReplWrapPos
in_lift: int = 0 # 1=正在提升块内
need_header: int = 0 # 提升块内已见装饰器行、尚未见 def 头
in_body: int = 0 # 已越过 def 头(后续缩进行是函数体,逐行查 raise)
name_buf: bytes = ReplMsgName
while i < total:
n: int = _repl_line_len(body + i)
ind: int = _repl_indent_len(body + i, n)
blank: int = 0
if ind >= n:
blank = 1
if in_lift != 0:
if blank != 0:
# 纯空白行:留在提升块内(函数体里的空行,对缩进无语义)
lp0: int = int(string.strlen(ReplLiftBuf))
lp0 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp0, body + i, n)
lp0 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp0, "\n")
if lp0 < 0:
return 2
i += n + 1
continue
if ind > 0:
if need_header != 0:
stdio.printf("M3: 装饰器之后必须紧跟 def 头行:%s\n",
_repl_show_line(body + i, n))
return 1
if in_body != 0 and _repl_line_has_kw(body + i, n, ind, "raise") != 0:
stdio.printf("M3: 会话函数 '%s' 的函数体含 raise:%s\n"
" M3 不支持跨片段异常传播(raise 会给函数追加 "
"__eh_msg_out__/__eh_code_out__ 出参,"
"而经函数指针的间接调用不会补这些实参)——该片段整体不执行\n",
name_buf, _repl_show_line(body + i, n))
return 1
lp1: int = int(string.strlen(ReplLiftBuf))
lp1 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp1, body + i, n)
lp1 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp1, "\n")
if lp1 < 0:
return 2
i += n + 1
continue
# 顶格非空行 ⇒ 提升块结束,本行按"未进入提升"重新判定(不推进 i)
in_lift = 0
need_header = 0
in_body = 0
continue
if blank != 0 or ind > 0:
# 进片段体(每行 4 空格;与块路径逐字同口径)
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, " ")
pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, body + i, n)
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, "\n")
if pos < 0:
return 2
i += n + 1
continue
# 顶格非空行
if body[i] == '@':
in_lift = 1
need_header = 1
in_body = 0
lp2: int = int(string.strlen(ReplLiftBuf))
lp2 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp2, body + i, n)
lp2 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp2, "\n")
if lp2 < 0:
return 2
i += n + 1
continue
if _repl_line_has_kw(body + i, n, 0, "def") != 0:
rc_h: int = _repl_lift_def_header(body + i, n, name_buf, _REPL_FN_NAME_MAX)
if rc_h != 0:
return rc_h
if _repl_lift_add_name(name_buf) != 0:
return 1
in_lift = 1
need_header = 0
in_body = 1
i += n + 1
continue
if _repl_line_has_kw(body + i, n, 0, "async") != 0 \
and _repl_line_has_kw(body + i, n, 6, "def") != 0:
stdio.printf("M3: 不支持 async def 会话函数(协程 ABI 与 M3 签名 token "
"不相容,请改写成普通 def):%s\n", _repl_show_line(body + i, n))
return 1
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, " ")
pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, body + i, n)
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, "\n")
if pos < 0:
return 2
i += n + 1
if need_header != 0:
stdio.printf("M3: 装饰器之后缺少 def 头行(片段末尾)\n")
return 1
ReplWrapPos = pos
return 0
# 多行块的包裹装配(提升 + 包裹):返回 0=成功;1=规则拒绝(已打印);2=缓冲不足
def _repl_build_frag_src_multi(name: str, body: bytes) -> int:
global ReplWrapPos
if name is None or body is None:
return 2
if ReplLiftBuf is None or ReplLiftNames is None or ReplSrcBuf is None:
return 2
# 提升缓冲与名表水位已由调用方 _build_frag_src 每片段清零(单行路径也必须清零,
# 故清零点在调用方而非此处);这里只负责装配
# 片段函数头(与单行模板同一形态);片段体随后逐行 4 空格
viperlib.snprintf(ReplSrcBuf, _REPL_FRAG_SRC_CAP,
"export def %s(%s: bytes) -> int:\n",
name, GenTypes.REPL_CTX_PARAM)
ReplWrapPos = int(string.strlen(ReplSrcBuf))
rc: int = _repl_split_body(body)
if rc != 0:
return rc
pos: int = ReplWrapPos
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, " return 0\n")
if pos < 0:
return 2
# 提升出的模块级 def 追加在片段入口函数之后(同为模块级,顺序无关)
lift_len: int = int(string.strlen(ReplLiftBuf))
if lift_len > 0:
pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, pos, ReplLiftBuf, lift_len)
if pos < 0:
return 2
ReplWrapPos = pos
return 0
# ============================================================
# M3 - 会话函数表:宿主持有(写表 + 写 ctx 指针槽),Gen 只读
#
# 时序(见 ReplExecFragment):
# ① 定义片段:提升 → 翻译 → **注册前校验**(本节的 _repl_precheck_session_funcs,
# 在 llc/加载之前 ⇒ 失败则整片段不执行)→ 加载 DLL → GetProcAddress(裸名)
# → 写 ctx 槽 → 追加/更新表条目;
# ② 调用片段:翻译期命中表 → 生成"从槽 load 地址 + 按表内签名间接调用"。
# 一致性:表条目与 ctx 槽只在"定义片段 DLL 加载成功且符号取址成功"后一起写;
# 失败路径由 ReplSavedFuncCount 水位回滚(与 ReplSavedVarCount 同思路)。
# ============================================================
# 会话函数表按名查找(宿主侧;与 Gen 侧 repl_find_func_entry 同口径)
def _repl_func_entry_find(name: str) -> rawptr[GenTypes.ReplFuncEntry]:
"""1=命中条目;None=未登记/表未建"""
if ReplCtxObj is None or ReplCtxObj.FuncTable is None or name is None:
return None
es: sizet = GenTypes.ReplFuncEntry.__sizeof__()
i: int
for i in range(ReplCtxObj.FuncCount):
e: rawptr[GenTypes.ReplFuncEntry] = rawptr[GenTypes.ReplFuncEntry](uint64(ReplCtxObj.FuncTable) + uint64(i) * uint64(es))
if e.Name is not None and string.strcmp(e.Name, name) == 0:
return e
return None
# 提升出的第 idx 个函数名(NUL 结尾);越界返回 None
def _repl_lift_name(idx: int) -> str:
if ReplLiftNames is None or idx < 0 or idx >= ReplLiftNameCount:
return None
return ReplLiftNames + idx * _REPL_FN_NAME_MAX
# 深拷贝到 PermPool(会话表条目名等跨片段存活的数据)
def _repl_perm_dup(s: str) -> str:
if s is None or _perm_pool is None:
return None
n: sizet = string.strlen(s)
b: str = _perm_pool.alloc(n + 1)
if b is None:
return None
string.strcpy(b, s)
return b
# 签名解析结果 → 人类可读串("((int, str) -> int)"式;写进调用方缓冲)
def _repl_sig_to_text(tag: int, bits: int, pc: int) -> str:
"""把 ReplSigResult + ReplSigTags/Bits 渲染成 "(P1, P2) -> R"(PermPool 缓冲)"""
if ReplSigText is None:
return "(?)"
pos: int = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, 0, "(")
i: int = 0
while i < pc:
if i > 0:
pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, pos, ", ")
pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, pos,
_repl_tag_text(int(ReplSigTags[i]), int(ReplSigBits[i])))
i += 1
pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, pos, ") -> ")
pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, pos, _repl_tag_text(tag, bits))
if pos < 0:
return "(?)"
return ReplSigText
# 注册前校验(在 llc/加载之前):逐个提升函数解析 IR 签名,判
# ① EH 出参 ② 类型不受支持 ③ 首形参不是 ctx ④ 形参过多 ⑤ 未找到 define
# ⑥ 重定义签名不一致 ⑦ 表容量不足。任一项失败 ⇒ 打印原因并返回 1(整片段不执行)。
def _repl_precheck_session_funcs() -> int:
if ReplLiftNameCount <= 0:
return 0
if ReplCtxObj is None or ReplCtxObj.FuncTable is None or ReplSigResult is None:
stdio.printf("M3: 会话函数表/签名缓冲未初始化(会话初始化失败)\n")
return 1
new_cnt: int = 0
idx: int
for idx in range(ReplLiftNameCount):
nm: str = _repl_lift_name(idx)
if nm is None:
return 1
rc: int = _repl_ir_func_sig(nm, ReplSigResult)
if rc == 1:
stdio.printf("M3: 片段 IR 里找不到 '%s' 的 define 行(提升未生效?)\n", nm)
return 1
if rc == 2:
stdio.printf("M3: 会话函数 '%s' 的签名含不受支持的类型 '%s'\n"
" M3 支持 void / i1 / i8 / i16 / i32 / i64 / float / double / "
"i8*(str、bytes、rawptr);结构体值/数组/多级指针请改用指针参数"
"或等 M4\n",
nm, ReplSigResult.BadType)
return 1
if rc == 3:
stdio.printf("M3: 会话函数 '%s' 的签名含异常出参(%%__eh_msg_out__/"
"%%__eh_code_out__)——其函数体含 raise\n"
" M3 不支持跨片段异常传播:经函数指针的间接调用不会补这两个实参"
"(arity 不匹配);该片段整体不执行\n", nm)
return 1
if rc == 4:
stdio.printf("M3: 会话函数 '%s' 的 define 首形参不是 %s(提升模板与调用约定"
"不一致)\n", nm, GenTypes.REPL_CTX_PARAM)
return 1
if rc == 5:
stdio.printf("M3: 会话函数 '%s' 的形参超过 %d 个\n",
nm, GenTypes.REPL_FUNC_PARAM_MAX)
return 1
if rc != 0:
stdio.printf("M3: 会话函数 '%s' 的签名解析失败(rc=%d)\n", nm, rc)
return 1
old: rawptr[GenTypes.ReplFuncEntry] = _repl_func_entry_find(nm)
if old is None:
new_cnt += 1
continue
# 重定义:只允许**同签名**覆盖(换签名会让先前片段里已编好的调用点按旧 ABI
# 调新代码 ⇒ 静默 UB)。签名一致时只更新槽内地址(段号随之刷新)。
sig_same: int = 1
if old.RetTag != ReplSigResult.RetTag or old.RetBits != ReplSigResult.RetBits \
or old.ParamCount != ReplSigResult.ParamCount:
sig_same = 0
if sig_same != 0:
pi: int = 0
while pi < old.ParamCount:
if int(old.ParamTags[pi]) != int(ReplSigTags[pi]) \
or int(old.ParamBits[pi]) != int(ReplSigBits[pi]):
sig_same = 0
break
pi += 1
if sig_same == 0:
stdio.printf("M3: 会话函数 '%s' 重定义签名不一致(旧 %s,新 %s)\n"
" M3 允许同签名覆盖(只换地址);换签名会让先前片段里已编好的"
"调用点按旧 ABI 调新代码 —— 该片段整体不执行,请换名字\n",
nm,
_repl_entry_sig_text(old),
_repl_sig_to_text(ReplSigResult.RetTag, ReplSigResult.RetBits,
ReplSigResult.ParamCount))
return 1
if ReplCtxObj.FuncCount + new_cnt > ReplCtxObj.FuncCap:
stdio.printf("M3: 会话函数已达上限 %d 个(ctx 函数指针槽用尽),本次新增 %d 个;"
"请重启会话或减少会话函数\n",
ReplCtxObj.FuncCap, new_cnt)
return 1
return 0
# 已有条目的签名渲染(重定义冲突诊断用;写进 ReplSigText2)
def _repl_entry_sig_text(e: rawptr[GenTypes.ReplFuncEntry]) -> str:
if e is None or ReplSigText2 is None:
return "(?)"
pos: int = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, 0, "(")
i: int = 0
while i < e.ParamCount:
if i > 0:
pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, pos, ", ")
pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, pos,
_repl_tag_text(int(e.ParamTags[i]), int(e.ParamBits[i])))
i += 1
pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, pos, ") -> ")
pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, pos,
_repl_tag_text(e.RetTag, e.RetBits))
if pos < 0:
return "(?)"
return ReplSigText2
# 注册(DLL 加载成功后调用):GetProcAddress(裸名) → 写 ctx 函数指针槽 → 追加/更新表
# 返回 0=成功;1=失败(已打印;调用方按 ReplSavedFuncCount 回滚表水位后放弃本片段)
def _repl_register_session_funcs(h_lib: win32base.HANDLE, frag_no: int) -> int:
if ReplLiftNameCount <= 0:
return 0
if ReplCtxObj is None or ReplCtxObj.FuncTable is None or ReplCtxBuf is None:
stdio.printf("M3: 会话函数表/ctx 未初始化\n")
return 1
idx: int
for idx in range(ReplLiftNameCount):
nm: str = _repl_lift_name(idx)
if nm is None:
return 1
# 导出符号名 = 裸名:定义端 `export def` ⇒ has_export=1 不加 SHA1 前缀
# (FuncGenDef.vp:482),且 REPL 片段 ModuleSha1=None 使 _mangle_name 原样返回
# (FuncGenBase.vp:264);-shared 未用 DEF 文件 ⇒ 全部符号自动进导出表(M1 实测)
addr: VOIDPTR = win32base.GetProcAddress(h_lib, nm)
if addr is None:
stdio.printf("M3: 会话函数 '%s' 未出现在片段 DLL 的导出表"
"(GetProcAddress 返回空;本片段不执行)\n", nm)
return 1
ent: rawptr[GenTypes.ReplFuncEntry] = _repl_func_entry_find(nm)
if ent is None:
# 新建条目(签名已在校验阶段通过;此处重新解析以取回 scratch 里的 token)
rc: int = _repl_ir_func_sig(nm, ReplSigResult)
if rc != 0:
stdio.printf("M3: 会话函数 '%s' 的签名重新解析失败(rc=%d)\n", nm, rc)
return 1
j: int = ReplCtxObj.FuncCount
es: sizet = GenTypes.ReplFuncEntry.__sizeof__()
ent = rawptr[GenTypes.ReplFuncEntry](uint64(ReplCtxObj.FuncTable) + uint64(j) * uint64(es))
nm_c: str = _repl_perm_dup(nm)
nm_s: str = _repl_perm_dup(nm)
tags_b: bytes = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX)
bits_b: bytes = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX)
if nm_c is None or nm_s is None or tags_b is None or bits_b is None:
stdio.printf("M3: 会话函数条目内存分配失败('%s')\n", nm)
return 1
string.memset(tags_b, 0, GenTypes.REPL_FUNC_PARAM_MAX)
string.memset(bits_b, 0, GenTypes.REPL_FUNC_PARAM_MAX)
string.memcpy(tags_b, ReplSigTags, sizet(ReplSigResult.ParamCount))
string.memcpy(bits_b, ReplSigBits, sizet(ReplSigResult.ParamCount))
ent.Name = nm_c
ent.SymName = nm_s
ent.FragNo = frag_no
ent.SlotOff = GenTypes.REPL_FUNC_BASE + j * GenTypes.REPL_FUNC_SLOT_SIZE
ent.RetTag = ReplSigResult.RetTag
ent.RetBits = ReplSigResult.RetBits
ent.ParamCount = ReplSigResult.ParamCount
ent.ParamTags = tags_b
ent.ParamBits = bits_b
ReplCtxObj.FuncCount = j + 1
else:
# 重定义(同签名):只换地址与段号,槽偏移不变
ent.FragNo = frag_no
# 写 ctx 槽(低 8 字节=地址,高 8 字节保留 0)
sv: rawptr[ReplFuncSlotView] = rawptr[ReplFuncSlotView](uint64(ReplCtxBuf) + uint64(ent.SlotOff))
sv.Addr = addr
sv.Reserved = None
stdio.printf("会话函数 %s: 导出符号 %s → ctx 槽 +%d(定义片段 %d,签名 %s)\n",
nm, nm, ent.SlotOff, frag_no,
_repl_entry_sig_text(ent))
return 0
# ============================================================
# M3 - 会话函数签名解析(从片段 IR 的 define 行取定义端 ABI)
#
# 出处:定义片段 translate 后 dump 的 IR 里
# `define <ret> @<名>(i8* %__repl_ctx__[, <ty> %<p>]*)`
# 是定义端 ABI 的唯一真相(注解 → LLVM 类型由 codegen 解析)。宿主据此填
# ReplFuncEntry 的签名 token;调用片段用同一 token 重建同型签名 ⇒ 两侧一致。
# 只接受可往返重建的类型:i1/i8/i16/i32/i64、float、double、i8*(+ 返回 void)。
# ============================================================
# 把 [src, src+n) 截断拷进会话缓冲并 NUL 结尾(供 strcmp/%s):缓冲区 1
def _repl_span_str(src: bytes, n: int) -> str:
if ReplTySpan is None or src is None:
return None
m: int = n
if m > _REPL_FN_TY_CAP - 1:
m = _REPL_FN_TY_CAP - 1
if m < 0:
m = 0
string.memcpy(ReplTySpan, src, sizet(m))
ReplTySpan[m] = 0
return ReplTySpan
# LLVM 类型串 → 打包 token(tag*1000 + bits);0=不支持
def _repl_llvm_ty_token(ty: str) -> int:
if ty is None:
return 0
if string.strcmp(ty, "i1") == 0:
return GenTypes.REPL_TY_INT * 1000 + 1
if string.strcmp(ty, "i8") == 0:
return GenTypes.REPL_TY_INT * 1000 + 8
if string.strcmp(ty, "i16") == 0:
return GenTypes.REPL_TY_INT * 1000 + 16
if string.strcmp(ty, "i32") == 0:
return GenTypes.REPL_TY_INT * 1000 + 32
if string.strcmp(ty, "i64") == 0:
return GenTypes.REPL_TY_INT * 1000 + 64
if string.strcmp(ty, "float") == 0:
return GenTypes.REPL_TY_FLOAT * 1000 + 32
if string.strcmp(ty, "double") == 0:
return GenTypes.REPL_TY_FLOAT * 1000 + 64
if string.strcmp(ty, "i8*") == 0:
return GenTypes.REPL_TY_STR * 1000 + 8
return 0
# token → 显示名(仅用于日志/诊断)
def _repl_tag_text(tag: int, bits: int) -> str:
if tag == GenTypes.REPL_TY_VOID:
return "void"
if tag == GenTypes.REPL_TY_INT:
if bits == 1:
return "bool"
if bits == 8:
return "char"
if bits == 16:
return "int16"
if bits == 32:
return "int"
if bits == 64:
return "int64"
return "int?"
if tag == GenTypes.REPL_TY_FLOAT:
if bits == 32:
return "float"
if bits == 64:
return "double"
return "float?"
if tag == GenTypes.REPL_TY_STR:
return "str"
return "?"
# 在 IR 文本里定位符号 name 的 define 行:命中返回 '(' 的**字节偏移**
#(并把 "define " 的偏移写进 ReplIrDefOff),未命中返回 -1。
# 全程用整数偏移 + 逐字节扫描(不借 strchr/strstr 的返回指针,避免 str/bytes
# 指针类型分叉带来的编译期不确定性);只做正向扫描。
def _repl_ir_find_define_at(ir: bytes, name: str) -> int:
global ReplIrDefOff
ReplIrDefOff = -1
if ir is None or name is None:
return -1
kw: str = "define "
nl: int = int(string.strlen(name))
if nl <= 0:
return -1
i: int = 0
while ir[i] != 0:
hit: int = 1
k: int = 0
while k < 7:
if ir[i + k] == 0 or ir[i + k] != kw[k]:
hit = 0
break
k += 1
if hit == 0:
i += 1
continue
# i+7 起是返回类型,直到 '@';要求 '@' 与 '(' 在同一行内(同 define 行)
j: int = i + 7
while ir[j] != 0 and ir[j] != '@' and ir[j] != '\n':
j += 1
if ir[j] != '@':
i += 1
continue
n0: int = j + 1
n1: int = n0
while ir[n1] != 0 and ir[n1] != '(' and ir[n1] != '\n':
n1 += 1
if ir[n1] != '(' or n1 - n0 != nl:
i += 1
continue
if string.strncmp(ir + n0, name, nl) == 0:
ReplIrDefOff = i
return n1
i += 1
return -1
# 解析会话函数的 define 行签名 → ReplSigResult + ReplSigTags/ReplSigBits
# 返回 0=成功;1=未找到 define;2=类型不受支持;3=含 EH 出参;4=首形参不是 ctx;
# 5=形参过多(> REPL_FUNC_PARAM_MAX)
def _repl_ir_func_sig(name: str, out: rawptr[ReplSigOut]) -> int:
if name is None or out is None or ReplIrBuf is None:
return 1
out.RetTag = GenTypes.REPL_TY_VOID
out.RetBits = 0
out.ParamCount = 0
out.EHTag = 0
out.Bad = 0
out.BadType = None
ir: bytes = ReplIrBuf
lp: int = _repl_ir_find_define_at(ir, name)
if lp < 0 or ReplIrDefOff < 0:
return 1
# ① 返回类型:'define ' 之后到 '@'(去首尾空白)
at: int = ReplIrDefOff + 7
while ir[at] != 0 and ir[at] != '@':
at += 1
if ir[at] != '@' or at >= lp:
return 1
ret_f: int = ReplIrDefOff + 7
ret_e: int = at
while ret_f < ret_e and (ir[ret_f] == ' ' or ir[ret_f] == '\t'):
ret_f += 1
while ret_e > ret_f and (ir[ret_e - 1] == ' ' or ir[ret_e - 1] == '\t'):
ret_e -= 1
ret_str: str = _repl_span_str(ir + ret_f, ret_e - ret_f)
if ret_str is None:
return 2
if string.strcmp(ret_str, "void") == 0:
out.RetTag = GenTypes.REPL_TY_VOID
out.RetBits = 0
else:
rt: int = _repl_llvm_ty_token(ret_str)
if rt == 0:
out.Bad = 1
out.BadType = ret_str
return 2
out.RetTag = rt // 1000
out.RetBits = rt % 1000
# ② 形参表:'(' 到匹配的 ')'(按括号深度配平;IR 类型里可能有 {..} / [..])
d: int = 0
rp: int = lp
while ir[rp] != 0:
if ir[rp] == '(':
d += 1
elif ir[rp] == ')':
d -= 1
if d == 0:
break
rp += 1
if ir[rp] != ')':
return 1
# ③ 逐个形参段(按 ',' 切;段内形态 "类型 [%名字]")
# 类型 span = [ps+ty_f, ps+ty_n);名字 span = [ps+nm_f, ps+nm_e)(相对段起点)
ps: int = lp + 1
cur: int = ps
pi: int = 0
while 1:
is_sep: int = 0
if cur >= rp:
is_sep = 1
elif ir[cur] == ',':
is_sep = 1
if is_sep == 0:
cur += 1
continue
seg_n: int = cur - ps
pc: int = -1
k: int = 0
while k < seg_n:
if ir[ps + k] == '%':
pc = k
k += 1
ty_n: int = seg_n
if pc >= 0:
ty_n = pc
ty_f: int = 0
while ty_f < ty_n and (ir[ps + ty_f] == ' ' or ir[ps + ty_f] == '\t'):
ty_f += 1
while ty_n > ty_f and (ir[ps + ty_n - 1] == ' ' or ir[ps + ty_n - 1] == '\t'):
ty_n -= 1
nm_f: int = -1
nm_e: int = -1
if pc >= 0:
nm_f = pc + 1
nm_e = seg_n
while nm_f < nm_e and (ir[ps + nm_f] == ' ' or ir[ps + nm_f] == '\t'):
nm_f += 1
while nm_e > nm_f and (ir[ps + nm_e - 1] == ' ' or ir[ps + nm_e - 1] == '\t'):
nm_e -= 1
if nm_e <= nm_f:
nm_f = -1
if pi == 0:
# 首形参固定为 ctx:类型 i8*、名字逐字等于 __repl_ctx__
# 名字 span [nm_f, nm_e) 是**去掉前导 '%'** 的(nm_f = pc + 1,见上"名字 span"
# 口径),故期望串就是 ctx 形参名本身。此处**不能**再拼 '%':拼出来的
# "%__repl_ctx__" 长 13,而 span 长 12 ⇒ 长度判据恒不等,任何合法提升函数
# 都被误判 rc=4(`会话函数 'f' 的 define 首形参不是 __repl_ctx__`),
# M3 定义片段必在注册前校验处失败(提升装配其实是对的)。
ty_str0: str = _repl_span_str(ir + ps + ty_f, ty_n - ty_f)
if ty_str0 is None or string.strcmp(ty_str0, "i8*") != 0 or nm_f < 0:
return 4
c0: int = _repl_cat_str(ReplTySpan3, _REPL_FN_TY_CAP, 0, GenTypes.REPL_CTX_PARAM)
if c0 < 0:
return 4
if nm_e - nm_f != int(string.strlen(ReplTySpan3)) \
or string.strncmp(ir + ps + nm_f, ReplTySpan3, nm_e - nm_f) != 0:
return 4
else:
# EH 出参(raise 语义):define 形参名 %__eh_msg_out__/%__eh_code_out__
if nm_f >= 0:
c1: int = _repl_cat_str(ReplTySpan3, _REPL_FN_TY_CAP, 0, "%")
c1 = _repl_cat(ReplTySpan3, _REPL_FN_TY_CAP, c1, ir + ps + nm_f, nm_e - nm_f)
if c1 >= 0:
if string.strcmp(ReplTySpan3, "%__eh_msg_out__") == 0 \
or string.strcmp(ReplTySpan3, "%__eh_code_out__") == 0:
out.EHTag = 1
return 3
ty_str: str = _repl_span_str(ir + ps + ty_f, ty_n - ty_f)
if ty_str is None:
return 2
if out.ParamCount >= GenTypes.REPL_FUNC_PARAM_MAX:
return 5
tk: int = _repl_llvm_ty_token(ty_str)
if tk == 0:
out.Bad = 1
out.BadType = _repl_span_str(ir + ps + ty_f, ty_n - ty_f)
return 2
ReplSigTags[out.ParamCount] = tk // 1000
ReplSigBits[out.ParamCount] = tk % 1000
out.ParamCount = out.ParamCount + 1
pi += 1
if cur >= rp:
break
ps = cur + 1
cur = ps
return 0
# ============================================================
# 装配片段包裹源码到 ReplSrcBuf(片段体恒为**函数体**)。
# 单行(体不含 '\n'):走既有 snprintf 模板,**逐字不变**;
# 多行(块):M3 起走 _repl_build_frag_src_multi —— 顶层 def 提升为模块级
# export def(原文不缩进),其余行逐行加 4 空格进片段体,随后片段入口函数,
# 最后追加提升出的模块级 def。
# 为什么逐行处理:片段体缩进由模板给出,原来单行是"整行前加 4 空格";多行若只给
# 第一行加 4 空格,第 2 行起顶格 ⇒ IndentationError。故每行独立加 4 空格,
# 而每行**原有的相对缩进原样保留**(块内嵌套层级因此不变)。
# 为什么单行/多行分两条:单行路径要求与改动前逐字一致(同一 snprintf、同一模板、
# 同一输出),多行路径才需要逐行装配与提升;两者互不干扰。
# 返回 0=已写入 ReplSrcBuf;1=提升规则拒绝(原因已打印);2=缓冲不足。
def _build_frag_src(name: str, body: str) -> int:
"""装配包裹源码:单行=既有模板(逐字不变);多行=提升 + 逐行 4 空格"""
global ReplLiftNameCount
if name is None or body is None:
return 2
# 每片段装配前清零上一片段留下的提升状态(名表水位 + 提升缓冲):
# **单行路径不提升任何函数**,若不清零,上一片段提升出的名字会被本片段的
# 注册前校验(_repl_precheck_session_funcs 按 ReplLiftNameCount 逐名遍历)当成
# "本片段提升的函数"去本片段 IR 里找 define ⇒ 必然找不到 ⇒ 该片段被误拒。
# 典型症状:定义片段成功后的**第一个调用片段**(如 `f(41)`)报
# `M3: 片段 IR 里找不到 'f' 的 define 行(提升未生效?)`。
# 多行路径随后按需追加提升内容(_repl_split_body 写、_repl_build_frag_src_multi 读)。
ReplLiftNameCount = 0
ReplLiftBuf[0] = 0
ReplLiftNames[0] = 0
if string.strchr(body, '\n') is None:
# 单行:既有模板(ctx 形参 bytes ⇒ i8*;export ⇒ 不加 SHA1 前缀)
viperlib.snprintf(ReplSrcBuf, _REPL_FRAG_SRC_CAP,
"export def %s(%s: bytes) -> int:\n %s\n return 0\n",
name, GenTypes.REPL_CTX_PARAM, body)
return 0
# 多行:提升顶层 def + 逐行装配(M3;见 _repl_split_body 的规则)
return _repl_build_frag_src_multi(name, body)
# 执行一份片段源码(单行与整块同一入口):先按**未包裹**源码解析一次(诊断行号
# 对用户可读、且与单行路径同款消息),再交给 ReplExecFragment 包裹/翻译/执行。
# 行为与改动前 run 里的单行逻辑逐语句一致(moved,非新增)。
def _exec_repl_src(mb: memhub.MemBuddy*, src: str) -> int:
"""解析 + 执行一份片段源码;0=已处理(含各类诊断已打印)"""
if src is None:
return 1
tree: ast.AST* = ast.parse(src, mb)
if tree is None:
el: int = ast.get_parse_err_line()
ec: int = ast.get_parse_err_col()
em: str = ast.get_parse_err_msg()
if em is None:
em = "(未知)"
stdio.printf("ParseError: 第 %d 行第 %d 列: %s\n", el, ec, em)
return 0
if _count_top_stmts(tree) <= 0:
stdio.printf("已解析: 0 个顶层语句(跳过)\n")
return 0
# M2/M3 执行通路:整块/整行作为**一个**片段函数体翻译执行;失败只打印原因
# (或经 longjmp 回到恢复点),不终止进程
rc_frag: int = ReplExecFragment(mb, src)
if rc_frag != 0:
# 非 longjmp 的失败(提升规则拒绝、签名校验失败、llc/链接失败、
# LoadLibraryA/GetProcAddress 为空、导出签名不符等):片段内部已逐条打印
# 原因(ReplExecFragment 的各失败分支都有 printf),此处只回滚会话表水位
# (变量表 Count + 函数表 FuncCount),保证"失败后表与 ctx 槽一致"。
if ReplCtxObj is not None:
ReplCtxObj.Count = ReplSavedVarCount
ReplCtxObj.FuncCount = ReplSavedFuncCount
# M4:会话基线(CDefine/CTypedef)水位回滚。基线只在片段成功时追加增量
# (ReplBaselineCapture),故此处截断对正常路径是恒等操作;它保证"失败片段
# 绝不改变基线"这一不变式在将来捕获点位置变化时仍然成立。
ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount)
return 0
# 执行一个片段(任意顶层语句:表达式/赋值/注解赋值/增强赋值/...)。
# 返回 0=成功(已按 result_kind 回显);非 0=失败(已打印原因,未崩)。
# 注意:翻译期 fatal_error / llc 失败会经 VLogger.error → longjmp 回到 run 的恢复点,
# 此时本函数不返回;失败片段占用的符号序号已提前递增(名字/文件名不复用)。
def ReplExecFragment(mb: memhub.MemBuddy*, src: str) -> int:
"""片段 → DLL → 调用 → ctx 回显;0=成功"""
global ReplFragFn, ReplFragNo, ReplSavedVarCount, ReplSavedFuncCount
global ReplSavedCDefineCount, ReplSavedCTypedefCount
if mb is None or src is None:
return 1
if ReplTmpDir is None or ReplNameBuf is None or ReplSrcBuf is None:
stdio.printf("REPL: 会话缓冲未初始化\n")
return 1
if ReplSrcPath is None or ReplIrBuf is None or ReplNeedleBuf is None:
stdio.printf("REPL: 会话缓冲未初始化\n")
return 1
if ReplCtxBuf is None or ReplCtxObj is None or ReplVarTable is None:
stdio.printf("REPL: ctx 会话内存未初始化\n")
return 1
# ⓪ 每片段重新武装恢复点 + 会话表水位快照
# - set_repl_jmpbuf 幂等重注册:jmp_buf 生命周期 = 整个会话(PermPool,只在
# run 退出时 clear_repl_jmpbuf),中途不清 ⇒ 覆盖整条流水线(翻译 → 写 .ll
# → llc → clang++ → LoadLibrary → GetProcAddress → 调用 → 回显);
# - 真正的"武装"是 run 在本函数调用前的 _setjmp(jb, None)(恢复点存于同一
# 存活栈帧);此处重注册只是保证注册值不因任何路径丢失。
# - 水位快照供恢复分支回滚:失败片段可能已 repl_ensure_entry(Count+1)后才
# 在 repl_seal/后续阶段 fail-fast,若不回滚会把半成品条目留给下一片段。
# - M3 同时快照会话函数表水位(FuncCount):定义片段的登记是"逐函数"的,
# 中途失败(如第 2 个符号取址失败)必须整体回滚,否则表里留下半批登记。
VLogger.set_repl_jmpbuf(ReplJmpBuf)
ReplSavedVarCount = ReplCtxObj.Count
ReplSavedFuncCount = ReplCtxObj.FuncCount
# M4:会话基线水位(CDefine/CTypedef)——失败片段不得把增量带进基线,
# 与变量表/函数表水位同思路(成功才由 ReplBaselineCapture 追加增量)。
ReplSavedCDefineCount = ReplBaseCDefineCount
ReplSavedCTypedefCount = ReplBaseCTypedefCount
# ① 取号并立即递增:即使后续失败(含 longjmp)也不复用号,保证会话内名字/文件唯一
n: int = ReplFragNo
ReplFragNo = n + 1
viperlib.snprintf(ReplNameBuf, _REPL_NAME_CAP, "__repl_frag_%d", n)
# ② 剥行首空白:片段整体作为函数体,缩进由模板统一给出(否则与模板缩进叠加
# 会产生 IndentationError)。多行块时该步只作用于**第一行**的行首空白
# (块内其余行由 ③ 的逐行装配统一加 4 空格、原相对缩进保留)。
body: str = src
if body is not None:
while body[0] == ' ' or body[0] == '\t':
body = body + 1
# ③ 包裹模板:ctx 形参(bytes ⇒ i8*,名与 GenTypes.REPL_CTX_PARAM 同源)
# export → 不加 SHA1 前缀(parser 改写返回注解为 <T>|t.CExport;
# FuncGenDef 命中 has_export → 用裸名 define)
# 单行 = 既有模板逐字不变;多行(块)= M3 提升顶层 def + 逐行缩进 4 空格
# (见 _repl_split_body / _repl_build_frag_src_multi;只缩进第一行会让
# 第 2 行起顶格 ⇒ IndentationError)
# 返回:0=成功;1=提升规则拒绝(原因已由提升函数打印);2=缓冲不足
rc_build: int = _build_frag_src(ReplNameBuf, body)
if rc_build == 2:
stdio.printf("片段 %s: 包裹源码装配失败(缓冲 %d 字节不足)\n",
ReplNameBuf, _REPL_FRAG_SRC_CAP)
return 1
if rc_build != 0:
# 提升规则拒绝(async def / raise / 重名 / 签名非单行 / 自带 ctx 形参…):
# 整片段不执行(无任何部分副作用),会话继续
return 1
# ④ 包裹源码落盘(仅用于报错回显源行:fatal_error 经 _read_source_line 读该文件)
viperlib.snprintf(ReplSrcPath, _REPL_PATH_CAP, "%s/%s.vp", ReplTmpDir, ReplNameBuf)
sf: rawptr[fileio.File] = fileio.File(ReplSrcPath, fileio.MODE.W)
if not sf.closed:
sf.write(ReplSrcBuf, string.strlen(ReplSrcBuf))
sf.close()
# ⑤ 每片段重置模块级翻译状态(对齐 Phase1Main.vp:1027-1032 / BuildPipeline.vp:155-168)
# temp_dir 供依赖 .vpi 注入使用:优先用工程临时目录(含 includes 存根),
# 无工程配置时退回本会话目录
vpi_dir: str = Config.TempDir
if vpi_dir is None:
vpi_dir = ReplTmpDir
GenApi.set_temp_dir(vpi_dir)
# CallGenBase.TempDir/TempDirLen(跨模块返回类型 .vpi/.text.ll 查找、CExport 判定
# 的目录)只由 SetTempDir 设置(CallGenBase.vp:824-828);批处理在 RunPhase1 开头
# 设置(Phase1Main.vp:332),REPL 此前漏设 ⇒ 跨模块解析读不到 .vpi。与上面的
# set_temp_dir 同值、幂等,每片段设置一次。
GenApi.SetTempDir(vpi_dir)
GenApi.set_current_file_type(ReplSrcPath)
GenApi.set_current_file_imports(ReplSrcPath)
GenApi.clear_trace()
GenApi.clear_cdefine_constants()
GenApi.clear_ctypedefs()
# 可见性用**宽松模式(strict=0,全部已注册结构体可见)**:片段的类型来自
# 会话(先前片段定义/注册的类)而非"本文件 import",严格模式(批处理的每文件
# 隔离口径)会把它们全判为不可见 → 裸类名注解解析失败。宽松模式使
# `p: Point = ...` 在定义过 Point 的后续片段里可解析。
GenApi.reset_visible_structs(mb, 0)
# 结构体注册走"永久分配"路径(对齐 Phase 1b/Phase2 的 StdlibAlloc=1):
# 结构体表体/条目名/字段类型进 PermPool,否则它们随本片段 mb.reset_to 回收,
# 后续片段的 find_struct/get_struct_type 读悬空内存(M1 无结构体,故未设)。
GenApi.set_stdlib_alloc(1)
# M4:CDefine/CTypedef 的"会话基线"回灌。上面两个 clear_* 把表指针置 None
# (表语义是"整个会话一份",见 M4 设计)⇒ 此处把基线条目重新登记进本片段池:
# 片段内看到的是"会话基线(先前片段 / 先前 import 带进来的常量与类型别名)
# ∪ 本片段新增";本片段新增由成功后 ReplBaselineCapture 追加进基线。
ReplBaselineRestore(mb)
# ⑥ 解析包裹源码。池选择(M4):片段源码含 "class " 时用**永久池**解析——
# 片段里的 class 定义会被翻译进会话(结构体 + 方法 + 泛型模板表),其中
# **泛型模板**会把 ClassDef **节点本身**存进永久表(GenericClassNodes),
# 供后续片段特化时重读模板;若节点在本片段池,后续片段特化即读已回收内存。
# 永久池解析让"模板节点"与"模板表"同寿命(代价:含 class 的片段 AST 常驻,
# 量级 KB/片段)。不含 class 的片段仍用 mb,行为与 M2/M3 逐字一致。
parse_pool: memhub.MemBuddy* = mb
if _perm_pool is not None:
if string.strstr(ReplSrcBuf, "class ") is not None:
parse_pool = _perm_pool
tree: ast.AST* = ast.parse(ReplSrcBuf, parse_pool)
if tree is None:
el: int = ast.get_parse_err_line()
ec: int = ast.get_parse_err_col()
em: str = ast.get_parse_err_msg()
if em is None:
em = "(未知)"
stdio.printf("片段 %s: 包裹源码解析失败(第 %d 行第 %d 列: %s)\n",
ReplNameBuf, el, ec, em)
return 1
# ⑦ 翻译为完整模块 IR(Translator 本体在 mb)
# ModuleSha1 = 会话 sha1(M4):片段内定义的类型名恒定 "<sha1>.<Type>",
# 与后续片段的查找用同一键(类型跨片段可见的唯一基础);
# `export def`(片段入口 / 会话函数)走 has_export 分支取**裸名**
# (FuncGenDef.vp:482-485,经 _mangle_func_name 亦不前缀),故 M1-M3 的
# 裸名约定不受影响(片段入口名/会话函数名/GetProcAddress/探针全部照旧)。
# ReplCtx 置位 = 打开 REPL 模式(会话变量按 ctx 槽寻址);翻译后清空。
# 每个片段用新建的 Translator ⇒ longjmp 中断也不会把 ReplCtx 残留给下一片段。
tr: rawptr[HandlesTranslator.Translator] = mb.alloc(HandlesTranslator.Translator.__sizeof__())
if tr is None:
stdio.printf("片段 %s: Translator 分配失败\n", ReplNameBuf)
return 1
tr.__before_init__()
tr.__init__()
tr.ModuleSha1 = ReplSessionSha1
tr._declare_only = 0
tr.ReplCtx = ReplCtxObj
rc_tr: int = tr.translate(tree)
tr.ReplCtx = None
if rc_tr != 0:
stdio.printf("片段 %s: 翻译失败\n", ReplNameBuf)
return 1
tr.dump_ir(ReplIrBuf, _REPL_IR_CAP, llvmlite.OUTPUT_FULL)
# ⑦' M4:本片段是否**定义**了会话前缀符号(`@"<session sha1>.…"` = 类方法/
# 构造/类全局)?是 ⇒ 其 import lib 必须产出并登记,供后续片段链接期解析
# 这些符号(片段 DLL 之间没有隐式链接)。用 "@\"" + sha1 + "." 作探针
# (def/global 的命名起点;类型定义行是 %"…" 形态,不会误命中)。
needs_implib: int = 0
if ReplSessionSha1 is not None and ReplNeedleBuf is not None:
viperlib.snprintf(ReplNeedleBuf, _REPL_NEEDLE_CAP, "@\"%s.", ReplSessionSha1)
if string.strstr(ReplIrBuf, ReplNeedleBuf) is not None:
needs_implib = 1
# ⑧ 导出符号探针:签名必须是 (i8* %__repl_ctx__) 且返回 i32。
# 不含该形参 ⇒ 包裹模板/调用约定不一致,或片段含 raise 使函数被追加
# __eh_msg_out__/__eh_code_out__ 两个出参——此时宿主按单参调用是未定义行为,
# 故直接明确报错(不加载、不调用)。
viperlib.snprintf(ReplNeedleBuf, _REPL_NEEDLE_CAP, "define i32 @%s(i8* %%%s",
ReplNameBuf, GenTypes.REPL_CTX_PARAM)
if string.strstr(ReplIrBuf, ReplNeedleBuf) is None:
stdio.printf("片段 %s: 未生成约定的导出函数(期望 `define i32 @%s(i8* %%__repl_ctx__)`);"
"含 raise 的片段会追加异常出参,M2 不支持\n",
ReplNameBuf, ReplNameBuf)
return 1
# ⑧' 会话函数签名校验(M3):在 llc/加载**之前**判完(EH 出参 / 类型不受支持 /
# 首形参非 ctx / 形参过多 / 找不到 define / 重定义签名不一致 / 表容量不足)
# ⇒ 任一不通过就整片段不执行(无部分副作用;ctx 槽与表都不动)。
# ReplLiftNameCount 为 0(本片段没有提升任何会话函数)时立即返回 0。
if _repl_precheck_session_funcs() != 0:
return 1
# ⑨ 写 .ll(复用 BuildPipeline:切片路径 + ensure_dir)
ir_len: sizet = string.strlen(ReplIrBuf)
if BuildPipeline.write_ir_to_file(ReplIrBuf, ir_len, ReplTmpDir, ReplNameBuf) != 0:
stdio.printf("片段 %s: 写 .ll 失败(目录 %s)\n", ReplNameBuf, ReplTmpDir)
return 1
ir_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "ll")
if ir_path is None:
stdio.printf("片段 %s: .ll 路径构造失败\n", ReplNameBuf)
return 1
# ⑩ llc → .obj(复用既有机制;失败路径内部 VLogger.error 报 module/exit/cmd 后 longjmp)
cc_cmd: str = Config.CompilerCmd
if cc_cmd is None:
cc_cmd = "llc"
cc_flags: str = Config.CompilerFlags
if cc_flags is None:
cc_flags = "-filetype=obj -relocation-model=pic"
if BuildPipeline.compile_ll_to_obj(ir_path, ReplTmpDir, ReplNameBuf, cc_cmd, cc_flags) != 0:
# 走不到这里的 llc 失败:compile_ll_to_obj 内部 VLogger.error 已打印 module/exit/cmd
# 并 longjmp(BuildPipeline.vp:373-375)。能返回非 0 的只有其内部"分配失败"分支
# (obj_path/cmd 缓冲为 NULL,BuildPipeline.vp:315/367)⇒ 补一行明示,避免静默回提示符。
stdio.printf("片段 %s: llc 编译 .obj 失败(命令/路径缓冲分配失败,见上方诊断)\n",
ReplNameBuf)
return 1
# ⑪ clang++ -shared → .dll(命令拼法对齐 BuildPipeline.link_obj_to_exe 的
# "cmd ... -o 输出 输入";拉起步子复用 BuildPipeline._run_cmd_wait)
obj_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "obj")
dll_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "dll")
if obj_path is None or dll_path is None:
stdio.printf("片段 %s: .obj/.dll 路径构造失败\n", ReplNameBuf)
return 1
linker_cmd: str = Config.LinkerCmd
if linker_cmd is None:
linker_cmd = "clang++"
cmd: bytes = mb.alloc(_REPL_CMD_CAP)
if cmd is None:
stdio.printf("片段 %s: 命令行缓冲分配失败\n", ReplNameBuf)
return 1
# 跨模块符号(`<sha1>.<函数>` / `<sha1>.<全局>`)由宿主 exe 定义:把宿主导出的
# import lib 加进命令行 ⇒ 片段 DLL 的导入表指向宿主 exe,符号在链接期解析、
# 加载期由同一进程内已加载的宿主满足(同一份 includes 代码与数据,无状态重复)。
# ReplHostImplib 为 None(宿主未按导出模式构建)时保持旧口径:只链片段自身 obj,
# 此时跨模块调用会在链接期报 undefined symbol(不终止会话,见下方提示)。
# M4 追加:① 本片段产出 import lib(--out-implib)供后续片段链接;
# ② 把先前片段登记的 import lib 全部带上(本片段引用其定义的类/方法)。
implib_path: str = mb.alloc(_REPL_PATH_CAP)
if implib_path is None:
stdio.printf("片段 %s: import lib 路径缓冲分配失败\n", ReplNameBuf)
return 1
viperlib.snprintf(implib_path, _REPL_PATH_CAP, "%s.dll.a", dll_path)
pos_c: int = _repl_cat_str(cmd, _REPL_CMD_CAP, 0, linker_cmd)
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " -shared -o ")
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, dll_path)
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " ")
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, obj_path)
if ReplHostImplib is not None:
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " ")
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, ReplHostImplib)
pos_c = _repl_cmd_append_implibs(cmd, _REPL_CMD_CAP, pos_c)
if pos_c >= 0:
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " -Wl,--out-implib=")
pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, implib_path)
if pos_c < 0:
stdio.printf("片段 %s: 链接命令行超过 %d 字节(已登记的片段 import lib 共 %d "
"个)——本片段不执行;可重启会话减少片段数\n",
ReplNameBuf, _REPL_CMD_CAP, ReplFragImplibCount)
return 1
link_rc: int = BuildPipeline._run_cmd_wait(cmd)
if link_rc != 0:
stdio.printf("片段 %s: 生成 .dll 失败 (exit=%d)\n 命令: %s\n",
ReplNameBuf, link_rc, cmd)
if ReplHostImplib is None:
stdio.printf(" 原因提示: 未找到宿主 import lib(<exe 名>.dll.a)——"
"跨模块符号(形如 <16位hex>.<函数名>)无法解析;"
"宿主需按 TransPyV/project.vpj 构建(含 -Wl,--export-all-symbols),"
"且 <exe 名>.dll.a 与 exe 同目录\n")
else:
stdio.printf(" 原因提示: 若输出含 undefined symbol 且形如 <16位hex>.<名>,"
"则该符号未被宿主定义/导出(宿主构建时未链入对应 includes 模块);"
"若该函数在源码里是 `export def`(CExport,裸名定义)却仍带前缀,"
"多为 include 存根 .vpi 缺失(见会话开头的 includes 扫描行)\n")
return 1
# ⑪' M4:若本片段 IR 里定义了会话前缀符号 ⇒ 登记其 import lib
# (链接已产出 <dll>.dll.a)。后续片段链接时自动带上它 ⇒ 片段 N 里
# `p = Point(1,2)` / `p.x` 之外的**方法调用**(`p.get()`)能解析到片段 1
# 的 `<session sha1>.Point.get` 定义(构造/类全局同理)。
# 登记失败(上限/文件缺失)只告警:更晚的片段对这些符号会在链接期
# undefined(会话继续,见 ReplFragImplibCount 的上限说明)。
if needs_implib != 0:
if _is_file(implib_path) != 0 and ReplFragImplibRecord(implib_path) != 0:
stdio.printf("片段 %s: 已登记片段 import lib(后续片段可解析本片段定义的"
"类型/方法符号): %s\n", ReplNameBuf, implib_path)
else:
stdio.printf("片段 %s: 片段 import lib 未登记(已登记 %d 个,上限 %d;或文件"
"缺失)——更晚的片段可能解析不到本片段定义的类型/方法符号\n",
ReplNameBuf, ReplFragImplibCount, _REPL_FRAG_IMPLIB_MAX)
# ⑫ 动态加载 + 取地址(LoadLibraryA/GetProcAddress 见 includes/w32/win32base.vp:65/68)
h_lib: win32base.HANDLE = win32base.LoadLibraryA(dll_path)
if h_lib is None:
stdio.printf("片段 %s: LoadLibraryA 失败: %s\n", ReplNameBuf, dll_path)
if ReplHostImplib is not None:
stdio.printf(" 原因提示: 片段 DLL 的导入表指向宿主 exe(%s);"
"若 GetLastError 是 127(找不到指定过程)/ 加载器 0xC0000139,"
"多为宿主与该 import lib 非同一次构建产物、或宿主缺该导出;"
"进程镜像名(%s)与 import lib 记录名不一致时同样会失败\n",
ReplHostImplib, dll_path)
return 1
fp_addr: VOIDPTR = win32base.GetProcAddress(h_lib, ReplNameBuf)
if fp_addr is None:
stdio.printf("片段 %s: GetProcAddress 返回空(导出表中无该符号): %s\n",
ReplNameBuf, dll_path)
return 1
# ⑫' 会话函数注册(M3):DLL 已加载 ⇒ GetProcAddress(裸名) 取地址 → 写 ctx
# 函数指针槽 → 追加/更新会话函数表。放在**调用片段入口之前**:失败则本片段
# 的语句一律不执行(只留下已加载但未被引用的 DLL),且由水位回滚丢弃半批登记。
# 本片段内对同名函数的调用仍走直接调用(模块内有 define)⇒ 注册只服务后续片段。
if _repl_register_session_funcs(h_lib, n) != 0:
if ReplCtxObj is not None:
ReplCtxObj.FuncCount = ReplSavedFuncCount
return 1
# ⑬ 调用前复位结果区(kind=none),并刷新 var_count/last_frag(供检视/调试);
# 存入 t.Callable 槽并间接调用(函数地址必须存活到调用之后,故不 FreeLibrary:
# 会话模型下模块常驻,供后续片段复用)
head: rawptr[GenTypes.ReplResult] = _ctx_head()
if head is None:
return 1
head.Kind = GenTypes.REPL_RES_NONE
head.VarCount = ReplCtxObj.Count
head.LastFrag = n
ReplFragFn = fp_addr
# 实参类型即构造出的调用签名形参类型(bytes ⇒ i8*),与片段定义一致
r_rc: int = ReplFragFn(ReplCtxBuf)
# ⑭ 从 ctx 结果区读回并回显(片段返回值只作状态码)
if _repl_echo_result(ReplNameBuf, head) != 0:
stdio.printf("片段 %s: 结果区读取失败\n", ReplNameBuf)
return 1
if r_rc != 0:
stdio.printf("片段 %s: 片段返回非零状态码 %d\n", ReplNameBuf, r_rc)
# ⑭' M4:片段**成功** ⇒ 把本片段新增的 CDefine/CTypedef 条目并入会话基线。
# 失败路径(早返回 / longjmp)不执行 ⇒ 基线天然不含失败片段的半成品;
# 截断(ReplBaselineTruncate)只是水位一致性的防御性保障。
ReplBaselineCapture()
return 0
# ============================================================
# run - M2 REPL 主循环
# ============================================================
def run(mb: memhub.MemBuddy*) -> int:
"""M2/M3 REPL:读一行/一整块 → 解析 → 编译为 DLL 并调用(会话变量经 ctx 槽)→ 回显;
行尾 ':' 进入块级续行(提示符 "... "),空行或 EOF 结束并整块执行一次;错误不终止进程。"""
global ReplJmpBuf, ReplSavedVarCount, ReplSavedFuncCount, ReplInBlock, ReplBlockLen
if mb is None or _perm_pool is None:
stdio.printf("REPL: 需要主池与 PermPool(由 main 注入 _perm_pool)\n")
return 1
# 解析表(关键字/运算符)建在 PermPool:它们是 ast 的模块级全局、仅首次建表;
# 若建在 mb 上会被每行 reset_to(mark) 释放 → 下一行解析 use-after-free 崩溃。
ast._init_tables(_perm_pool)
jb: rawptr = _perm_pool.alloc(_REPL_JMPBUF_SIZE)
if jb is None:
stdio.printf("REPL: jmp_buf 分配失败\n")
return 1
# 会话级注册:ReplJmpBuf 供每片段入口幂等重注册;VLogger 侧登记一次即可长期有效,
# 只在 run 退出时 clear(中途不清 —— 这是"覆盖整条流水线"的前提)。
ReplJmpBuf = jb
ReplSavedVarCount = 0
ReplSavedFuncCount = 0
VLogger.set_repl_jmpbuf(jb)
# jit_flag 由 main 在调用 run 前置 True(见 main.vp --repl 分支),run 退出时复位 False
lg: VLogger.Logger = VLogger.logger
# 恢复点必须"**先武装再使用**":ReplInitSession 及其调用(GetTempPathA / 建目录 /
# 一次性常驻分配)内部若走 VLogger.error(→ repl_abort → longjmp),jmp_buf 必须
# 已被 _setjmp 初始化过;否则是对**未初始化 jmp_buf** 的 longjmp(未定义行为,
# 表现为"启动期进程消失、提示符永不出现")。会话初始化是**唯一**必须成功的阶段
# (没有常驻缓冲/ctx 就没有片段通路),故这里恢复后按失败退出,不做"带伤继续"。
# (注:JmpBuf 尚未武装的窗口只有上面的 ast._init_tables —— 那时 _repl_jmpbuf 也
# 尚未注册,repl_abort 走 sys.exit(1),与批处理同口径。)
rc_init: int = _setjmp(jb, None)
if rc_init != 0:
stdio.printf("REPL: 会话初始化中断(诊断见上)—— 无常驻缓冲/ctx,REPL 无法启动\n")
stdio.fflush(None)
ReplJmpBuf = None
VLogger.clear_repl_jmpbuf()
if lg is not None:
lg.set_jit_flag(False)
return 1
# 会话初始化:临时目录 + 常驻缓冲 + ctx(均 PermPool,见 ReplInitSession)
if ReplInitSession() != 0:
stdio.printf("REPL: 会话初始化失败(临时目录/常驻缓冲/ctx)\n")
ReplJmpBuf = None
VLogger.clear_repl_jmpbuf()
if lg is not None:
lg.set_jit_flag(False)
return 1
h_in: win32base.HANDLE = w32file.GetStdHandle(w32file.STD_INPUT_HANDLE)
line: bytes = stdlib.malloc(_REPL_LINE_CAP)
if line is None:
stdio.printf("REPL: 行缓冲分配失败\n")
ReplJmpBuf = None
VLogger.clear_repl_jmpbuf()
if lg is not None:
lg.set_jit_flag(False)
return 1
stdio.printf("TransPyV REPL (M2/M3/M4) - 会话变量 + 会话函数(跨片段)+ ctx 回传结果;\n")
stdio.printf(" 支持表达式(1+2)、定义(x = 1 / x: int = 5)、引用(x + 1)、\n")
stdio.printf(" 增强赋值(x += 2)、取址(p = &x)等;空行跳过,Ctrl+Z 或 EOF 退出\n")
stdio.printf(" 多行块:行尾 ':' 进入续行(提示符 ...),空行结束并整块执行一次\n")
stdio.printf(" 跨片段函数:块里顶格的 def 会被提升为片段模块的 export def(裸名导出),\n")
stdio.printf(" 后续片段可直接调用(如 def f(x: int) -> int: 换行 +4 空格 return x + 1);\n")
stdio.printf(" 限制:签名须单行、不支持 async def、函数体内不支持 raise、不支持偏函数\n")
stdio.printf(" 跨片段类型(M4):块里的 class 会被翻译进会话(类型名 %s.<类名>),\n",
ReplSessionSha1)
stdio.printf(" 后续片段可实例化、读写字段、调用方法;import 的 includes 模块\n")
stdio.printf(" 其类同样注册进会话(缺 .vpi 存根时该模块的类不可用,会话继续)\n")
stdio.printf(" 片段临时目录: %s\n", ReplTmpDir)
stdio.fflush(None)
# === 会话级 includes 初始化(只做一次):扫描 + 注册 SHA1 + 预生成 .vpi
# + 泛型模板注册 + 逐模块类注册 ===
# 会话开始即武装一次恢复点:includes 扫描/存根生成/模板注册内部若 fail-fast
# (VLogger.error → repl_abort → longjmp),必须落到本帧,否则跳到未武装的
# jmp_buf 是未定义行为。恢复后按宽容口径继续进入读行循环。
# 【硬保证】提示符必现:includes 的任何失败(含"类注册整体失败")都只影响
# includes 类的可用性,绝不阻止进入读行循环 —— 本段所有出口都不 return。
inc_mark: sizet = mb.mark()
rc_inc: int = _setjmp(jb, None)
if rc_inc != 0:
stdio.printf("REPL: includes 初始化中断(已注册 %d 个模块、存根 %d 个):"
"import includes 模块可能失败,其余片段不受影响\n",
ReplIncMods, ReplIncVpiNew)
mb.reset_to(inc_mark)
else:
# 扫描结果/注册表都在 stdlib(create_scan_result 用 stdlib.malloc),
# 扫描期的 mb 分配(sha1 十六进制串等)在注册后即可回收。
# M4:本调用内部还会做"includes 类注册"(逐模块 _setjmp,见
# ReplInitIncludesClasses 的退出约定)。
ReplInitIncludes(mb)
mb.reset_to(inc_mark)
# ⚠ 必须在 run 的帧上重新武装恢复点(注意:**不能**把这几行包成辅助函数——
# _setjmp 留下的跳转目标属于"调用它的那个帧",包成函数后该帧立刻返回,
# 目标又变成死帧)。ReplInitIncludes 内部的逐模块 _setjmp
# (ReplInitIncludesClasses)已把目标指向那些**已返回**的帧;此处重设之后,
# 下面的收尾与整个读行循环都在本帧的恢复点保护下 —— 这正是"includes 类注册
# 之后提示符消失"(longjmp 到已返回的帧 → 未定义行为)的修复点。
rc_arm: int = _setjmp(jb, None)
if rc_arm != 0:
stdio.printf("REPL: includes 类布局收尾中断(诊断见上):布局注册可能不完整,"
"会话继续\n")
else:
# 收尾:收敛登记"父类在后续遍才就绪"的类 + 逐类告警拒绝 + 汇总。放在
# **重武装之后**:其内部可能经 GenApi.register_struct → VLogger.error 触发
# longjmp,此刻必须落在本帧(否则就是上面那个死帧问题)。
VpiReader.ReplStubLayoutFinish()
# 关闭"存根布局恢复"必须无条件执行:ReplStubLayoutMode 不归零会让片段翻译
# 误走 REPL 存根路径(本函数只置标志,无失败路径)。
VpiReader.ReplStubLayoutEnd()
# === 宿主导出符号(跨模块调用):定位宿主 exe 的 import lib(只读探测) ===
# 命中 ⇒ 片段链接时把它加进命令行(片段 DLL 从宿主导入 includes 符号,共用同一份
# 代码与数据);未命中 ⇒ 只告警,不带 import lib 链接(跨模块调用会在链接期失败,
# 由 ReplExecFragment 打印明确原因,会话继续)。
if ReplFindHostImplib() != 0:
stdio.printf("宿主符号导入库: %s(片段可跨模块调用 includes 函数/全局)\n",
ReplHostImplib)
else:
stdio.printf("宿主符号导入库: 未找到 <exe 名>.dll.a;片段调用 includes 模块级"
"函数会在链接期报 undefined symbol(会话不终止)。请用 "
"TransPyV/project.vpj(已声明 -Wl,--export-all-symbols)构建宿主,"
"并让 exe 与 .dll.a 同目录\n")
stdio.fflush(None)
# === 读行循环:单行片段 + 块级续行 ===
# 走到这里 = 初始化阶段的**全部**失败路径都已消化(includes 类注册失败/超预算
# 只降级功能,不影响可用性)⇒ 提示符必然出现。日志里这一行是"REPL 起来了"的
# 硬判据(紧随其后即 ">>> ")。
stdio.printf("REPL: 初始化完成,进入读行循环\n")
stdio.fflush(None)
# 块状态(ReplInBlock/ReplBlockLen)在每次"块结束/失败/EOF/溢出"后一律归零:
# ① 空行/EOF 结束续行 → 执行整块后归零;② 执行期 longjmp → 恢复分支归零;
# ③ 块缓冲溢出 → 丢弃整块并归零。
# 归零是"残块不污染下一行"的依据:下一次 _block_append 从索引 0 覆盖写并重写 NUL,
# 旧字节不可达(且新块解析只经 NUL 结尾串,不读旧长度之后的内容)。
ReplInBlock = 0
ReplBlockLen = 0
while 1:
# 提示符:续行 "... "、正常 ">>> "(与既有 ">>> " 同一打印点、同一刷新时机)
if ReplInBlock != 0:
stdio.printf("... ")
else:
stdio.printf(">>> ")
stdio.fflush(None)
n: int = _read_line(h_in, line, _REPL_LINE_CAP)
eof_hit: int = 0
if n < 0: # EOF:先按"终止行"处理,再退出
eof_hit = 1
n = 0
# 每行独立:先取水位,再 _setjmp(武装本片段/本块的恢复点,覆盖整条流水线);
# 解析/翻译/llc/加载/调用期的任何 longjmp 都回收到 mark 并继续。
mark: sizet = mb.mark()
rc: int = _setjmp(jb, None)
if rc != 0:
# longjmp 返回:诊断已由 VLogger.error / fatal_error 等打印(均以换行
# 结尾),此处只清块状态(残块不得留给下一行输入)+ 回滚会话表水位
# (丢弃失败片段的半成品登记)+ 回收本行分配,随后直接打印下一个提示符
# ——不再额外打印"已从错误恢复"。块状态归零对单行路径是恒等写入。
ReplInBlock = 0
ReplBlockLen = 0
if ReplCtxObj is not None:
ReplCtxObj.Count = ReplSavedVarCount
ReplCtxObj.FuncCount = ReplSavedFuncCount
# M4:会话基线(CDefine/CTypedef)水位回滚(与上面的表水位同思路)
ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount)
mb.reset_to(mark)
if eof_hit != 0:
break
continue
if eof_hit != 0:
# EOF 结束:续行中且缓冲非空 ⇒ 先把已缓存块执行掉(等价空行终止);
# 否则走既有退出路径(打印换行后退出)
if ReplInBlock != 0 and ReplBlockLen > 0:
_exec_repl_src(mb, ReplBlockBuf)
ReplInBlock = 0
ReplBlockLen = 0
mb.reset_to(mark)
stdio.printf("\n")
break
if ReplInBlock != 0:
if n == 0:
# 空行结束续行:整块作为**一个**片段执行一次;块内错误只打印、会话存活
if ReplBlockLen > 0:
_exec_repl_src(mb, ReplBlockBuf)
ReplInBlock = 0
ReplBlockLen = 0
mb.reset_to(mark)
continue
# 续行中:每行都入缓冲;不因行尾 ':' 反复切换,也不做缩进分析
if _block_append(line, n) != 0:
stdio.printf("REPL: 块输入超过块缓冲上界 %d 字节:本块已丢弃,"
"回到 >>> 继续(可减少块体积或分块执行)\n",
_REPL_BLOCK_CAP)
ReplInBlock = 0
ReplBlockLen = 0
mb.reset_to(mark)
continue
# ==== 以下为既有单行路径(未进入续行时逐语句不变)====
if n == 0: # 空行:跳过
mb.reset_to(mark)
continue
if _line_starts_block(line, n) != 0:
# 行尾 ':' ⇒ 进入续行:该行进缓冲、本行不执行,提示符变 "... "。
# 首行不可能溢出(行缓冲上界 4095 < 块缓冲上界 8191):append 失败即
# 按下方单行路径处理,不进入续行状态。
if _block_append(line, n) == 0:
ReplInBlock = 1
mb.reset_to(mark)
continue
_exec_repl_src(mb, line)
mb.reset_to(mark)
stdlib.free(line)
# 会话结束才清空恢复点:中途从不清(保证流水线全程可 longjmp)
ReplJmpBuf = None
VLogger.clear_repl_jmpbuf()
if lg is not None:
lg.set_jit_flag(False)
return 0