Files
ViperC/App/lib/core/Repl.vp
T
TermiNexus cd55a7e214 refactor: rawptr[T] -> T*(R14)+ 修 REnum 前向占位未就地填充
R14:rawptr[X] -> X*(56 文件 / 834 行改动);保留 6 处 rawptr[t.CPtr](存纯裸指针的规范例外)。 修复:class X(t.REnum) 的变体字段写成自引用 X* 时,字段解析在变体扫描期经 ensure_forward_class 建了一个 opaque 前向占位;末尾 register_struct 按 Ty 指针去重会再建第二个同名 entry,而 find_struct 取首个(占位,0 字段)→ X.__sizeof__() == 0,报「已注册但大小为 0(字段链表未填充)」。改为注册前若命中同模块前向占位则就地填充(与 ensure_forward_class 文档口径一致)。TPC 无此问题,属 TPV 侧缺口。 验证:自举三重重编 SUCCESS;全量 77/77 顺序回归通过(修复前卡在 LLvmLiteTest)。
2026-10-05 17:38:28 +08:00

2108 lines
104 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 mlirlite
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 - interactive loop: wraps each fragment as an export def and JIT-compiles it to a DLL.
# Session variables/results are shared via ctx memory; errors recover via setjmp/longjmp.
# Input line buffer capacity (incl. NUL terminator).
define _REPL_LINE_CAP = 4096
# Block input buffer capacity (session-level PermPool, incl. NUL): the raw user lines
# appended while in continuation mode, joined by '\n' with no trailing newline.
define _REPL_BLOCK_CAP = 8192
# Measured jmp_buf size on mingw-w64 x86_64: _JBLEN=16 times _JBTYPE (16B) = 256 bytes.
# Confirmed with a C probe (sizeof(jmp_buf) == 256).
define _REPL_JMPBUF_SIZE = 256
# setjmp/longjmp on mingw-w64 x86_64: _setjmp takes a frame pointer, so the setjmp
# macro expands to _setjmp(buf, NULL); passing None is fine for non-SEH use.
extern def _setjmp(buf: rawptr, frame: rawptr) -> int
extern def longjmp(buf: rawptr, val: int)
# PermPool (injected by main): jmp_buf must outlive the session, so mb.reset_to must not reclaim it.
_perm_pool: memhub.MemBuddy*
# ============================================================
# M2/M3 constants: wrapped fragment source capacity covers the single-line, block and lifted paths.
define _REPL_FRAG_SRC_CAP = 26624
# Lifted module-level def source buffer capacity (incl. NUL).
define _REPL_LIFT_CAP = 12288
# Max session functions lifted per fragment (<= GenTypes.REPL_FUNC_CAP; excess is an error).
define _REPL_LIFT_MAX = 16
# Session function name length limit (ReplLiftNames slot width = limit + 1).
define _REPL_FN_NAME_MAX = 64
# Session function name table capacity (_REPL_LIFT_MAX fixed-width slots).
define _REPL_FN_NAMES_CAP = 1024
# Diagnostic display buffer (truncates non-NUL-terminated lines/names for %s).
define _REPL_DIAG_CAP = 128
# Signature rendering buffer capacity ("(int, str) -> int" is well under 96).
define _REPL_SIG_TEXT_CAP = 192
# Fragment symbol name / probe buffer capacity.
define _REPL_NAME_CAP = 64
define _REPL_NEEDLE_CAP = 128
# Temporary file path buffer capacity (directory/source paths).
define _REPL_PATH_CAP = 512
# Full module IR text capacity (matches App/main.vp IR_BUF_SIZE).
define _REPL_IR_CAP = 262144
# GetTempPathA output buffer capacity (MAX_PATH).
define _REPL_TMP_CAP = 260
# External command (clang++ -shared) line capacity. M4 grew it 1024 -> 16384 because the
# link line also carries prior fragments' import libs, else their symbols stay undefined.
define _REPL_CMD_CAP = 16384
# includes source read buffer (matches Phase1/BuildPipeline SRC_BUF_SIZE).
define _REPL_SRC_BUF_CAP = 1048576
# .vpi stub buffer (matches BuildPipeline.PYI_BUF_SIZE).
define _REPL_VPI_BUF_CAP = 262144
# ---- M4: session sha1 / fragment import lib / session baseline (CDefine + CTypedef) ----
# Max prior fragment import lib slots (a fragment takes one if its IR mentions a session symbol).
define _REPL_FRAG_IMPLIB_MAX = 128
# Fragment import lib record array capacity (slots x 8-byte pointers).
define _REPL_FRAG_IMPLIB_ARR_CAP = 1024
# Session baseline (CDefine) entry limit; replayed when each fragment rebuilds its table.
define _REPL_BASE_CDEFINE_MAX = 512
# Session baseline (CTypedef) entry limit.
define _REPL_BASE_CTYPEDEF_MAX = 512
# includes class registration (done once at session init): max modules injected via vpi.
define _REPL_INC_CLS_MOD_MAX = 512
# Convergence passes for includes class registration (field types may reference peers).
define _REPL_INC_CLS_PASSES = 3
# Struct table watermark guard for includes class registration (STRUCT_MAX = 512).
define _REPL_INC_CLS_STRUCT_GUARD = 448
# Per-module warning line limit for includes class registration; the rest are only counted.
define _REPL_INC_CLS_WARN_MAX = 3
# Startup time budget (ms) for includes class registration: multi-pass vpi injection is the
# main session-start cost, so on overrun we stop and let later fragments import on demand.
define _REPL_INC_CLS_BUDGET_MS = 20000
# Temporary LLVM module name for includes class registration (.vpi injection target).
define _REPL_INC_MOD_NAME = "repl_inc_init"
# ctx session buffer size = header 64 + 64 var slots x 16 + 32 func slots x 16 = 1600.
# Must stay in sync with the GenTypes.REPL_CTX_SIZE / REPL_* layout constants.
define _REPL_CTX_SIZE = 1600
# Template project file name (searched in cwd then exe dir when --project is absent).
define _REPL_PROJECT_FILE = "repl.vpj"
# cwd candidate suffix: GetCurrentDirectoryA omits the trailing separator, so add '/'.
define _REPL_PROJECT_SEP_FILE = "/repl.vpj"
# Longest length of the two suffixes plus NUL headroom (candidate buffer check).
define _REPL_PROJECT_PROBE_CAP = 16
# ============================================================
# ReplFloatView - float view of the ctx header (F at offset 16 reinterpreted as double).
novirtual class ReplFloatView:
"""Float view of the ctx header (result read as a double at +16)."""
Magic: int
Version: int
Kind: int
Reserved0: int
F: double
# ============================================================
# ReplFuncSlotView - ctx function-pointer slot (16B: address + reserved), same on both sides.
novirtual class ReplFuncSlotView:
"""ctx function-pointer slot (Addr = entry address in the defining fragment DLL)."""
Addr: VOIDPTR
Reserved: VOIDPTR
# ============================================================
# ReplSigOut - parsed session function signature (host reads the fragment's model ABI).
novirtual class ReplSigOut:
"""Parsed signature result (registration is rejected when Bad/EHTag is non-zero)."""
RetTag: int # GenTypes.REPL_TY_VOID / INT / FLOAT / STR
RetBits: int
ParamCount: int # excludes the leading ctx param
EHTag: int # 1=define has %__eh_msg_out__/%__eh_code_out__ (raise semantics)
Bad: int # 1=contains an unsupported type token
BadType: str # unsupported type string (points into IR text; for printing only)
# ============================================================
# Template project lookup: tries cwd/repl.vpj then exe-dir/repl.vpj, writing diagnostics.
def ReplFindProject(Out: bytes, Cap: int, Diag: bytes, DiagCap: int) -> int:
"""Locate repl.vpj in cwd then the exe directory: 1=found (writes Out), 0=miss (writes Diag)."""
if Out is None or Diag is None: return 0
if Cap < 64 or DiagCap < 64: return 0
# Separate buffers per candidate so both paths appear in the diagnostic text.
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)
# Defensive: GetCurrentDirectoryA writes are undefined on overflow, so force a 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 失败)")
# 2) exe-dir/repl.vpj (GetModuleFileNameA(NULL) returns our own full path).
n_exe: ULONG = w32file.GetModuleFileNameA(None, c2, ULONG(Cap))
if n_exe > 0:
el: int = int(n_exe)
# Defensive: on truncation the return equals Cap and the string may lack a NUL.
c2[Cap - 1] = 0
if el < Cap:
c2[el] = 0
# Locate the last separator: c2[0..dlen) is the directory prefix (with separator);
# dlen=0 means no directory part, which the OS resolves against cwd.
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 失败)")
# Not found in either place: emit diagnostics (needs --project + both searched paths).
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
# ============================================================
# Session state: permanent buffers in PermPool; per-fragment strings in mb; t.Callable slots.
ReplFragFn: callable[[rawptr], int] = None
# Fragment sequence number: module-level and monotonic, so names/files are never reused.
ReplFragNo: int = 1
# Session temp dir %TEMP%\TransPyV_repl\<pid> (keeps user projects clean; pid isolates sessions).
ReplTmpDir: bytes = None
# Fragment symbol name __repl_frag_<n>.
ReplNameBuf: bytes = None
# Wrapped fragment source (for parsing/translation).
ReplSrcBuf: bytes = None
# On-disk path of the wrapped source (real file for error echo).
ReplSrcPath: bytes = None
# Block input buffer (session PermPool, _REPL_BLOCK_CAP): user lines joined by '\n',
# allocated once rather than per line.
ReplBlockBuf: bytes = None
# Current block buffer byte count (0 = empty; NUL is always at ReplBlockBuf[ReplBlockLen]).
ReplBlockLen: int = 0
# Continuation state: 1 = caching a multi-line block ("... " prompt); 0 = normal (">>> ").
ReplInBlock: int = 0
# Full module IR text (dump_ir OUTPUT_FULL output buffer).
ReplIrBuf: bytes = None
# IR probe for the export signature "llvm.func @<name>(%__repl_ctx__: !llvm.ptr) -> i32".
ReplNeedleBuf: bytes = None
# ctx session buffer (shared by host and fragment; byte view, passed as the fragment's first arg).
ReplCtxBuf: bytes = None
# Session context object (read by Gen: tables/caps/param name/perm pool/scratch entry).
ReplCtxObj: GenTypes.ReplCtx* = None
# Session variable table (PermPool, ReplCtxObj.Table).
ReplVarTable: GenTypes.ReplVarEntry* = None
# Scratch entry (PermPool): classifies bare-expression results only; no session slot or table entry.
ReplScratch: GenTypes.ReplVarEntry* = None
# Recovery-point jmp_buf (PermPool, lives for the session): run _setjmp's into it and
# VLogger.repl_abort longjmps back from any pipeline depth; re-registered per fragment.
ReplJmpBuf: rawptr = None
# Session table watermark (ReplCtxObj.Count) at fragment start; rolled back on failure to
# drop half-registered entries (repl_ensure_entry Count+1 but repl_seal errored).
ReplSavedVarCount: int = 0
# ==== includes session-init results (once per session; for summary and recovery branches) ====
# Registered includes module count (StubMerger SHA1 table entries).
ReplIncMods: int = 0
# includes scan time (ms).
ReplIncMs: int = 0
# Newly generated .vpi stubs.
ReplIncVpiNew: int = 0
# Reused existing .vpi stubs.
ReplIncVpiReuse: int = 0
# .vpi generation failures (read/parse/write).
ReplIncVpiFail: int = 0
# ==== Host export symbols (cross-module calls) ====
# Host exe import lib path (<exe>.dll.a; PermPool, probed once); fragment link lines use it.
ReplHostImplib: bytes = None
# ==== M3 session functions (cross-fragment functions) ====
# Lifted module-level def source (session PermPool; length cleared at each fragment start).
ReplLiftBuf: bytes = None
# Lifted session function name table (PermPool, _REPL_FN_NAME_MAX x _REPL_LIFT_MAX slots).
ReplLiftNames: bytes = None
# Number of session functions lifted by this fragment.
ReplLiftNameCount: int = 0
# Signature token scratch (PermPool, GenTypes.REPL_FUNC_PARAM_MAX bytes each; reused per function).
ReplSigTags: bytes = None
ReplSigBits: bytes = None
# Signature result struct (PermPool, reused; named to avoid the ReplSigOut class).
ReplSigResult: ReplSigOut* = None
# Session function table watermark (ReplCtxObj.FuncCount) at fragment start; restored on
# failure to drop half-registered entries (same idea as ReplSavedVarCount, see ⓪).
ReplSavedFuncCount: int = 0
# Diagnostic line/name display buffer (PermPool, _REPL_DIAG_CAP; %s cannot print non-NUL lines).
ReplLineShow: bytes = None
# def-header name scratch (PermPool, _REPL_FN_NAME_MAX).
ReplMsgName: bytes = None
# Signature render buffers x2 (PermPool, _REPL_SIG_TEXT_CAP): new vs existing entry.
ReplSigText: bytes = None
ReplSigText2: bytes = None
# Fragment body (wrapper) write position (_repl_split_body writes, _repl_build_frag_src_multi reads).
ReplWrapPos: int = 0
# ==== M4: session module sha1 (the cross-fragment unique key for type names) ====
# 16-hex-char string (copy of GenTypes.REPL_SESSION_SHA1), used as every fragment's ModuleSha1.
ReplSessionSha1: bytes = None
# ==== M4: prior fragment import libs (resolution source for cross-fragment method symbols) ====
# Fragment link lines add prior fragments' import libs so their class/method symbols resolve.
ReplFragImplibs: sizet* = None
ReplFragImplibCount: int = 0
# ==== M4: session baseline (CDefine/CTypedef survive across fragments) ====
# Replayed each fragment after clear_*; only successful fragments append; failures truncate.
ReplBaseCDefineNames: sizet* = None
ReplBaseCDefineValues: sizet* = None
ReplBaseCDefineKinds: sizet* = None # 8 bytes per entry: 0=int / 1=double / 2=str
ReplBaseCDefineCount: int = 0
ReplBaseCTypedefNames: sizet* = None
ReplBaseCTypedefTypeStrs: sizet* = None
ReplBaseCTypedefCount: int = 0
# Baseline watermark at fragment start (truncated back to it on failure/rollback).
ReplSavedCDefineCount: int = 0
ReplSavedCTypedefCount: int = 0
# ==== M4: includes class registration (done once at session init) ====
# Counts of modules whose .vpi injected successfully (classes in the session StructTable) / failed / missing stub.
ReplIncClsMods: int = 0
ReplIncClsFail: int = 0
ReplIncClsMissing: int = 0
# ============================================================
# Private helpers.
# Whether path is an existing regular file (GetFileAttributesA: attributes only, no buffer).
def _is_file(path: str) -> int:
"""1=exists and is not a directory; 0=missing, a directory, or bad args."""
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
# ============================================================
# Host export symbols: locate the host exe import lib so fragments resolve includes symbols.
def ReplFindHostImplib() -> int:
"""Locate the host import lib (<exe>.dll.a): 1=found (writes ReplHostImplib), 0=not found."""
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)
# Defensive: on truncation the write may lack a NUL (same as ReplFindProject).
if el >= _REPL_PATH_CAP: el = _REPL_PATH_CAP - 1
exe_buf[el] = 0
# Split dir/file: dlen = directory prefix length (incl. separator).
dlen: int = 0
si: int = el
while si > 0:
si -= 1
ch: char = exe_buf[si]
if ch == '/' or ch == '\\':
dlen = si + 1
break
# Candidate 1: exe dir (exe_buf already has the directory prefix).
viperlib.snprintf(cand, _REPL_PATH_CAP, "%s%s.dll.a", exe_buf, exe_buf + dlen)
if _is_file(cand) != 0:
ReplHostImplib = cand
return 1
# Candidate 2: Config.OutputDir (REPL uses repl.vpj, host uses 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
# Read one line byte-by-byte: works for console (ENABLE_LINE_INPUT) and pipes.
def _read_line(h: win32base.HANDLE, buf: bytes, cap: int) -> int:
"""Read one line from h into buf (NUL-terminated); returns bytes read, -1 on EOF/error."""
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
# Do not use &(buf + total): its IR does an i32 load (4 bytes) so '\n'/'\r'
# never match; the byte index buf[total] is an i8 load.
ch: char = buf[total]
if ch == 10: # '\n': end of line
break
if ch == 13: # '\r': CRLF, drop and re-read in place
continue
total += 1
buf[total] = 0
if eof != 0 and total == 0: return -1
return total
# Count top-level statements (Module.children).
def _count_top_stmts(tree: ast.AST*) -> int:
"""Return the number of top-level statements in a Module (0 when children is empty)."""
if tree is None or tree.children is None: return 0
stmts: list[ast.AST*]* = tree.children
return int(len(stmts))
# ============================================================
# Block continuation: a line ending in ':' opens a block, ended by a blank line or EOF.
# Line test: after trimming trailing whitespace it ends with ':', or (M3) starts with '@'.
def _line_starts_block(line: str, n: int) -> int:
"""1=line opens a block buffer (ends with ':' / starts with '@'); 0=no."""
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
# Append one line to the block buffer: join lines with '\n' and write a trailing NUL.
def _block_append(line: str, n: int) -> int:
"""Append an n-byte line to the session block buffer; 0=ok, 1=out of capacity (buffer unchanged)."""
global ReplBlockLen
if ReplBlockBuf is None or line is None or n <= 0: return 1
sep: int = 0
if ReplBlockLen > 0:
sep = 1 # separator '\n' between lines
if ReplBlockLen + sep + n >= _REPL_BLOCK_CAP: # plus 1 byte for the trailing NUL
return 1 # capacity checked before writing, so no rollback
if ReplBlockLen > 0:
ReplBlockBuf[ReplBlockLen] = '\n'
ReplBlockLen += 1
string.memcpy(ReplBlockBuf + ReplBlockLen, line, sizet(n))
ReplBlockLen += n
ReplBlockBuf[ReplBlockLen] = 0
return 0
# ctx header view (int64 interpretation): host reads/writes magic/kind/result_a...
def _ctx_head() -> GenTypes.ReplResult*:
"""ctx header view; None when the session is not initialized."""
if ReplCtxBuf is None: return None
return GenTypes.ReplResult*(ReplCtxBuf)
# ctx header view (double interpretation of result_a): for echoing float results.
def _ctx_float_head() -> ReplFloatView*:
"""Float view of the ctx header; None when the session is not initialized."""
if ReplCtxBuf is None: return None
return ReplFloatView*(ReplCtxBuf)
# Result echo: read the ctx result area by result_kind and print it.
def _repl_echo_result(frag_name: str, res: GenTypes.ReplResult*) -> int:
"""Echo this fragment's result by result_kind; 0=handled (including "no result")."""
if frag_name is None or res is None: return 1
k: int = res.Kind
if k == GenTypes.REPL_RES_NONE:
# Declaration-only fragment (e.g. `x: int`): no result for this line.
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: 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 means a non-string pointer (print address); result_a == 0
# means an inline struct value (no address; type name needs the cross-fragment table, 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: session sha1 / fragment import lib / session baseline / includes class registration.
# Deep-copy a string into pool (caller supplies the pool; None on invalid input/alloc failure).
def _repl_str_dup(pool: memhub.MemBuddy*, s: str) -> str:
"""Deep-copy s into pool; None on failure."""
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
# Build the session sha1 string (PermPool; used as ModuleSha1 during fragment translation).
def ReplInitSessionSha1() -> int:
"""0=success; non-zero=permanent pool or allocation failure."""
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
# ---- Session baseline (CDefine/CTypedef): find / restore / append / truncate ----
def _repl_base_cdefine_find(name: str) -> int:
"""Find a baseline CDefine entry index by name; -1 when not found."""
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:
"""Restore the session baseline (CDefine + CTypedef) into the just-cleared tables; 0=success."""
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:
# String CDefine: GenApi has no wrapper, so call gen directly (same shape as
# the clear_cdefine_constants wrapper).
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:
"""Append baseline-missing CDefine/CTypedef entries from the current tables into the session baseline; 0=success."""
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:
"""Truncate the baseline watermarks back to the fragment-start values (failed deltas must not enter)."""
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
# ---- Fragment import lib: record / append to the link command line ----
def ReplFragImplibRecord(implib_path: str) -> int:
"""Record a fragment import lib (PermPool deep copy); 1=recorded, 0=not (limit/allocation failure)."""
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
# Append all recorded fragment import libs to the command buffer (space-separated);
# returns the new position (-1 = out of capacity, which the caller must treat as an error).
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 class registration (once per session; per-module isolation, warnings only) ----
def ReplInitIncludesClasses(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*, vpi_dir: str) -> int:
"""Inject each includes module's .vpi into a temp module so its classes enter the session StructTable; 0=done."""
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
# Per-module state (8 bytes/slot, PermPool): 0 = pending/retry, 1 = done (ok or unusable).
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)
# Injection target: temporary LLVM module + translator (annotation context only);
# struct registration uses permanent allocation so entries/fields/types outlive fragments.
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: mlirlite.LLVMModule* = mlirlite.new_module(_perm_pool, _REPL_INC_MOD_NAME)
tr_inc: 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__()
# Give the temp translator real state (func/symbol tables, name buffers, Pool): VpiReader
# reads trans._funcs and trans.Pool/imported_modules while injecting, else it derefs None.
tr_inc._init_state(_perm_pool)
tr_inc.ModuleSha1 = ReplSessionSha1
tr_inc._declare_only = 0
# ReplCtx marks the translator as in a REPL session: annotations from .vpi may reference
# unregistered classes, so TypeGenResolve builds opaque placeholders instead of failing.
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
# Startup budget (one time check per module; GetTickCount64 is cheap): on overrun
# stop, leaving the remaining modules to the on-demand import path.
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: IncludesScanner.FileEntry* = 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:
# Missing stub (host never built / includes dir lacks this module): its classes
# are unavailable, but the session continues (import degrades the same way).
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
# Set trans.ModuleSha1 to the current stub's source module sha1 (like batch), so bare
# class names and parent lookups resolve correctly during injection.
tr_inc.ModuleSha1 = ent.Sha1
# Per-module recovery point: a fatal only discards this module's injection; the struct
# table dedups by name+sha1, and only mb.reset_to(vm) is done on the fatal path.
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)
# No module succeeded this pass, so further passes add nothing; stop early.
if pass_ok == 0: break
# Summary (aggregated warnings): per-module warnings were capped, so print full counts.
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
# ============================================================
# Session init: create %TEMP%\TransPyV_repl\<pid>\ and allocate permanent buffers + ctx.
def ReplInitSession() -> int:
"""Create the temp dir plus PermPool resident buffers and ctx session memory; 0=success."""
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
global 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
# 1) %TEMP% prefix (GetTempPathA returns a trailing backslash; 0 means failure).
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
# 2) Session dir <TEMP>TransPyV_repl/<pid> (pid isolates sessions; paths use '/').
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
# 3) Permanent buffers (live for the session, not re-allocated per fragment).
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
# 3') M3 permanent buffers: lifted source / name table / sig token scratch / render buffers.
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)
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 ReplSigText is None or ReplSigText2 is None: return 1
ReplLiftBuf[0] = 0
ReplLiftNames[0] = 0
ReplLiftNameCount = 0
ReplWrapPos = 0
ReplSavedFuncCount = 0
# 4) ctx session memory (fragment's first arg): 64B header + 64x16B var slots + 32x16B func slots, zeroed.
ReplCtxBuf = _perm_pool.alloc(_REPL_CTX_SIZE)
if ReplCtxBuf is None: return 1
string.memset(ReplCtxBuf, 0, _REPL_CTX_SIZE)
# 5) Session variable table + scratch entry (PermPool; names deep-copied by Gen into the same pool).
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)
# Scratch entry name (readable in error messages: "<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
# 5') Session function table (M3; PermPool, host writes, Gen reads only) - entry array zeroed.
func_entry_size: sizet = GenTypes.ReplFuncEntry.__sizeof__()
ReplFuncTable: 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)
# 6) Session context object (the Gen-side switch: 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
# Session function table (M3): slices survive across fragments, so table/names/tokens are in PermPool.
ReplCtxObj.FuncTable = ReplFuncTable
ReplCtxObj.FuncCount = 0
ReplCtxObj.FuncCap = GenTypes.REPL_FUNC_CAP
ReplSavedFuncCount = 0
# 7) ctx fixed header: magic/version written once by the host (fragments read-only, no validation).
head: 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
# 8) M4 permanent state: session sha1 + fragment import lib table + session baseline (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 session init: scan dir -> register "module -> SHA1" -> pre-generate .vpi stubs.
def _repl_gen_includes_vpi(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*, vpi_dir: str) -> int:
"""Pre-generate .vpi stubs for the includes scan; returns the count of new stubs (-1=bad args)."""
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: IncludesScanner.FileEntry* = 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
# Existing stub -> reuse.
vf: 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
# Read source file (1MB cap matching Phase1/BuildPipeline SRC_BUF_SIZE).
f: 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
# Parse (all AST allocations stay within this file's mb window).
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
# Pass the absolute ent.Path as rel_path: vpi fatal_error reads source lines via it.
pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, ent.Path, pyi_buf, _REPL_VPI_BUF_CAP)
if pos > 0:
pf: 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:
"""Session includes init: scan, register SHA1, pre-generate .vpi, register generic templates, then per-module classes; 0=success."""
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 dir matches the fragment side: prefer the project temp dir, else the session dir.
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
# 1) Scan includes (no reachability: REPL has no SourceDir/deps graph, so scan all).
t0: uint64 = win32base.GetTickCount64()
sr: 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
# 2) Register "module -> SHA1" (same StubMerger global table batch consumers use).
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
# 3) Pre-generate .vpi stubs (count failures, do not abort the session).
_repl_gen_includes_vpi(mb, sr, vpi_dir)
stdio.printf("includes: 已注册 %d 个模块(扫描耗时 %d ms);" "存根 新生成 %d / 复用 %d / 失败 %d\n", n, ReplIncMs, ReplIncVpiNew, ReplIncVpiReuse, ReplIncVpiFail)
# 3'') Register session generic templates for real: reuse includes sources to register
# generic class templates (list/dict/Vector...) so annotations like list[...] specialize.
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")
# 3') M4.2: enable stub-layout recovery so vpi injection verifies and registers includes
# class layouts/vtable indices; also reset the generic-owner table for re-registration.
StubMergerStore.ResetGenericOwners()
VpiReader.ReplStubLayoutBegin(vpi_dir)
# 4) M4 class registration: inject each module's .vpi so its classes (with fields) enter the
# session StructTable, a prerequisite for cross-module construction/method calls.
ReplInitIncludesClasses(mb, sr, vpi_dir)
return 0
# ============================================================
# M2/M3: fragment execution path (stmt -> IR -> llc -> -shared DLL -> call -> ctx echo).
# M3 top-level def lifting: lift a top-level def into the fragment module as an export def,
# so the host can GetProcAddress it and later fragments call it through a ctx slot.
# Truncate a line (possibly non-NUL-terminated) into the session buffer for %s printing.
def _repl_show_line(p: bytes, n: int) -> str:
"""Copy a line fragment into a session buffer as a NUL-terminated string (truncated to 118 bytes)."""
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
# Line length (up to '\n' or NUL).
def _repl_line_len(p: bytes) -> int:
n: int = 0
while p[n] != '\n' and p[n] != 0: n += 1
return n
# Count leading whitespace bytes.
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
# Whether [ind, n) starts with keyword kw (kw followed by whitespace).
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
# Append a byte span to dst (keeping the trailing NUL); returns new length, -1 = out of capacity.
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
# Append a NUL-terminated string to dst; returns new length, -1 = out of capacity.
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)))
# Record a lifted function name (reserved-prefix check + same-fragment dedup + limit); 0=ok, 1=reject.
def _repl_lift_add_name(name: str) -> int:
global ReplLiftNameCount
if name is None or ReplLiftNames is None: return 1
# Reserved prefix: the fragment entry name ("__repl_frag_<n>") marks the fragment entry
# scope in Gen (session writes only work there), so users may not use it.
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
# Parse and rewrite one top-level def header into ReplLiftBuf as an export def with an
# injected ctx param; name written to name_out. Returns 0=ok/1=rejected/2=buffer full.
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 # skip "def"
while p < n and (line[p] == ' ' or line[p] == '\t'): p += 1
# Name (identifier characters).
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 generic params [T]: skip balanced brackets (translation maps T to 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
# Matching ')'.
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
# ':' must be on the same line (as must the return annotation).
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
# Whether the parameter list is empty.
empty: int = 1
q3: int = lp + 1
while q3 < rp:
if line[q3] != ' ' and line[q3] != '\t':
empty = 0
break
q3 += 1
# Reject a user-supplied __repl_ctx__ param (would duplicate the injected first param).
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
# Emit: "export " + original header [0,lp] + injection + original header [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
# Split the fragment body: lifted blocks -> ReplLiftBuf (export def, unindented); other lines
# -> ReplSrcBuf (4 spaces each); records lifted names. 0=ok/1=rejected/2=buffer full.
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 = inside a lifted block
need_header: int = 0 # saw a decorator line, no def header yet
in_body: int = 0 # past the def header (indented lines are the body; check 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:
# Blank line: stays in the lift block (blank body lines are indentation-neutral).
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
# Dedented non-blank line ends the lift block; re-evaluate this line (do not advance i).
in_lift = 0
need_header = 0
in_body = 0
continue
if blank != 0 or ind > 0:
# Into the fragment body (4 spaces per line, same as the block path).
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
# Dedented non-blank line.
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
# Assemble a multi-line block wrapper (lift + wrap): 0=ok/1=rejected/2=buffer full.
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
# Lift buffer/name table watermarks are cleared per fragment by _build_frag_src; here we only
# assemble: emit the fragment function header, then body lines each get 4 spaces.
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
# Append lifted module-level defs after the fragment entry function (module scope, order-free).
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 session function table: host owns it (writes table + ctx slots); Gen reads only.
# Find a session function table entry by name (host side; same as Gen's repl_find_func_entry).
def _repl_func_entry_find(name: str) -> GenTypes.ReplFuncEntry*:
"""1=entry found; None=not registered or table not built."""
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: GenTypes.ReplFuncEntry* = 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
# Name of the idx-th lifted function (NUL-terminated); None if out of range.
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
# Deep-copy into PermPool (session table entry names etc. that outlive a fragment).
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
# Render signature result -> human-readable "(P1, P2) -> R" (into a PermPool buffer).
def _repl_sig_to_text(tag: int, bits: int, pc: int) -> str:
"""Render ReplSigResult + ReplSigTags/Bits as "(P1, P2) -> R" (PermPool buffer)."""
if ReplSigText is None: return "(?)"
pos: int = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, 0, "(")
for i in range(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])))
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
# Pre-registration check (before llc/load): per lifted function verify signature, EH params,
# type support, ctx first param, param count, redefinition, capacity - else abort the fragment.
def _repl_precheck_session_funcs(mod: mlirlite.LLVMModule*) -> 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_model_func_sig(mod, nm, ReplSigResult)
if rc == 1:
stdio.printf("M3: 定义片段模型里找不到函数 '%s'(提升未生效?)\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: GenTypes.ReplFuncEntry* = _repl_func_entry_find(nm)
if old is None:
new_cnt += 1
continue
# Redefinition: only a same-signature overwrite is allowed (a different signature would
# make earlier fragments call new code with the old ABI -> silent UB). Same sig: update addr.
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:
for pi in range(old.ParamCount):
if int(old.ParamTags[pi]) != int(ReplSigTags[pi]) or int(old.ParamBits[pi]) != int(ReplSigBits[pi]):
sig_same = 0
break
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
# Render an existing entry's signature (for redefinition-conflict diagnostics; into ReplSigText2).
def _repl_entry_sig_text(e: GenTypes.ReplFuncEntry*) -> str:
if e is None or ReplSigText2 is None: return "(?)"
pos: int = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, 0, "(")
for i in range(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])))
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
# Register after a successful DLL load: GetProcAddress(bare name) -> write ctx slot ->
# append/update the table. 0=ok; 1=failed (caller rolls back by ReplSavedFuncCount and aborts).
def _repl_register_session_funcs(h_lib: win32base.HANDLE, frag_no: int, mod: mlirlite.LLVMModule*) -> 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
# Exported symbol is the bare name: `export def` sets has_export without a SHA1 prefix,
# and -shared without a DEF file exports all symbols automatically.
addr: VOIDPTR = win32base.GetProcAddress(h_lib, nm)
if addr is None:
stdio.printf("M3: 会话函数 '%s' 未出现在片段 DLL 的导出表" "(GetProcAddress 返回空;本片段不执行)\n", nm)
return 1
ent: GenTypes.ReplFuncEntry* = _repl_func_entry_find(nm)
if ent is None:
# New entry (signature already validated; re-parse to recover the scratch tokens).
rc: int = _repl_model_func_sig(mod, 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 = 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:
# Redefinition (same signature): only swap address and fragment number; slot offset unchanged.
ent.FragNo = frag_no
# Write the ctx slot (low 8 bytes = address, high 8 bytes reserved 0).
sv: ReplFuncSlotView* = 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 session function signature parsing: read the definition-site ABI from the model.
# token -> display name (for logs/diagnostics only).
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 "?"
# LLVMType* -> packed token (tag*1000 + bits); 0 = unsupported. Supported set matches the
# old string version: i1/i8/i16/i32/i64, float/double, Ptr(Int8) (str/bytes/rawptr).
def _repl_ty_ptr_token(ty: mlirlite.LLVMType*) -> int:
if ty is None: return 0
match ty:
case mlirlite.LLVMType.Int(bits):
if bits == 1: return GenTypes.REPL_TY_INT * 1000 + 1
if bits == 8: return GenTypes.REPL_TY_INT * 1000 + 8
if bits == 16: return GenTypes.REPL_TY_INT * 1000 + 16
if bits == 32: return GenTypes.REPL_TY_INT * 1000 + 32
if bits == 64: return GenTypes.REPL_TY_INT * 1000 + 64
return 0
case mlirlite.LLVMType.Float(bits):
if bits == 32: return GenTypes.REPL_TY_FLOAT * 1000 + 32
if bits == 64: return GenTypes.REPL_TY_FLOAT * 1000 + 64
return 0
case mlirlite.LLVMType.Ptr(pointee):
match pointee:
case mlirlite.LLVMType.Int(bits):
if bits == 8: return GenTypes.REPL_TY_STR * 1000 + 8
return 0
case _:
return 0
case _:
return 0
# Diagnostic: LLVMType* -> readable name (for unsupported-signature errors); none if unnamed.
def _repl_ty_name(ty: mlirlite.LLVMType*) -> str:
if ty is None: return "(none)"
nm: str = mlirlite.get_struct_name(ty)
if nm is not None: return nm
return "(unsupported)"
# Take a session function signature from the translation model (not by parsing IR define lines).
# Codes: 0=ok; 1=not found; 2=unsupported type; 3=EH params; 4=first param not ctx; 5=too many params.
def _repl_model_func_sig(mod: mlirlite.LLVMModule*, name: str, out: ReplSigOut*) -> int:
if name is None or out is None or mod 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
fn: mlirlite.Function* = GenApi.find_func_in_module(mod, name)
if fn is None: return 1
# 1) Return type.
rt_ty: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(fn)
is_void: int = 0
if rt_ty is None: is_void = 1
else:
match rt_ty:
case mlirlite.LLVMType.Void():
is_void = 1
case _:
is_void = 0
if is_void != 0:
out.RetTag = GenTypes.REPL_TY_VOID
out.RetBits = 0
else:
rt: int = _repl_ty_ptr_token(rt_ty)
if rt == 0:
out.Bad = 1
out.BadType = _repl_ty_name(rt_ty)
return 2
out.RetTag = rt // 1000
out.RetBits = rt % 1000
# 2) Params: the first is ctx (Ptr(Int8) named __repl_ctx__), the rest are business params.
pi: int = 0
p_cur: mlirlite.Param* = mlirlite.function_get_param_head(fn)
while p_cur is not None:
p_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(p_cur)
p_nm: str = mlirlite.param_get_name(p_cur)
# Param names may or may not carry a leading '%' (the printer adds it on demand); strip it.
bare_nm: str = p_nm
if bare_nm is not None and bare_nm[0] == '%': bare_nm = bare_nm + 1
if pi == 0:
if _repl_ty_ptr_token(p_ty) != GenTypes.REPL_TY_STR * 1000 + 8: return 4
if bare_nm is None or string.strcmp(bare_nm, GenTypes.REPL_CTX_PARAM) != 0: return 4
else:
# EH param (raise semantics).
if bare_nm is not None:
if string.strcmp(bare_nm, "__eh_msg_out__") == 0 or string.strcmp(bare_nm, "__eh_code_out__") == 0:
out.EHTag = 1
return 3
if out.ParamCount >= GenTypes.REPL_FUNC_PARAM_MAX: return 5
tk: int = _repl_ty_ptr_token(p_ty)
if tk == 0:
out.Bad = 1
out.BadType = _repl_ty_name(p_ty)
return 2
ReplSigTags[out.ParamCount] = tk // 1000
ReplSigBits[out.ParamCount] = tk % 1000
out.ParamCount = out.ParamCount + 1
pi += 1
p_cur = mlirlite.param_get_next(p_cur)
return 0
# ============================================================
# Assemble the wrapped fragment source into ReplSrcBuf (the fragment body is always a function body).
def _build_frag_src(name: str, body: str) -> int:
"""Assemble the wrapped fragment source: single line uses the template verbatim; multi-line lifts and indents by 4."""
global ReplLiftNameCount
if name is None or body is None: return 2
# Clear the previous fragment's lifting state before assembling (single-line path lifts
# nothing; a stale name count would make the precheck wrongly reject this fragment).
ReplLiftNameCount = 0
ReplLiftBuf[0] = 0
ReplLiftNames[0] = 0
if string.strchr(body, '\n') is None:
# Single line: existing template (ctx param bytes => i8*; export => no SHA1 prefix).
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
# Multi-line: lift top-level defs + assemble line by line (M3; rules in _repl_split_body).
return _repl_build_frag_src_multi(name, body)
# Execute one fragment source (single-line and block share this entry): parse the unwrapped
# source first (readable diagnostics), then hand it to ReplExecFragment to wrap/translate/run.
def _exec_repl_src(mb: memhub.MemBuddy*, src: str) -> int:
"""Parse and execute one fragment source; 0=handled (diagnostics already printed)."""
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 execution path: whole block/line runs as one fragment function body; failures only
# print the reason (or longjmp back to the recovery point), never kill the process.
rc_frag: int = ReplExecFragment(mb, src)
if rc_frag != 0:
# Non-longjmp failures already printed their reason inside the fragment; here only roll
# back the session table watermarks so the tables and ctx stay consistent.
if ReplCtxObj is not None:
ReplCtxObj.Count = ReplSavedVarCount
ReplCtxObj.FuncCount = ReplSavedFuncCount
# M4: roll back the session baseline (CDefine/CTypedef) watermarks; a no-op on the normal
# path, it keeps the "failed fragment never changes the baseline" invariant.
ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount)
return 0
# Execute one fragment (any top-level statement: expr/assign/ann-assign/aug-assign/...).
# Returns 0=ok (echoed by result_kind); non-zero=failed. Fatal errors longjmp to run's point.
def ReplExecFragment(mb: memhub.MemBuddy*, src: str) -> int:
"""Fragment -> DLL -> call -> ctx echo; 0=success."""
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
# 0) Re-arm the recovery point and snapshot session table watermarks so a failed fragment's
# half-registered entries can be rolled back; set_repl_jmpbuf is idempotent.
VLogger.set_repl_jmpbuf(ReplJmpBuf)
ReplSavedVarCount = ReplCtxObj.Count
ReplSavedFuncCount = ReplCtxObj.FuncCount
# M4: session baseline watermarks (CDefine/CTypedef); a failed fragment must not add
# increments, same idea as the var/func watermarks (only success calls ReplBaselineCapture).
ReplSavedCDefineCount = ReplBaseCDefineCount
ReplSavedCTypedefCount = ReplBaseCTypedefCount
# 1) Take the number and increment it immediately: names/files stay unique even after a failure.
n: int = ReplFragNo
ReplFragNo = n + 1
viperlib.snprintf(ReplNameBuf, _REPL_NAME_CAP, "__repl_frag_%d", n)
# 2) Strip leading whitespace (the template indents the body; stacking would error). For
# blocks only the first line is affected; step 3 indents the rest uniformly.
body: str = src
if body is not None:
while body[0] == ' ' or body[0] == '\t': body = body + 1
# 3) Wrap: ctx param (bytes => i8*); export => no SHA1 prefix. Single line uses the existing
# template; multi-line lifts top-level defs. Returns 0=ok/1=rejected/2=buffer full.
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:
# Lift rule rejection (async def / raise / duplicate / non-single-line signature / own
# ctx param...): the whole fragment is skipped (no partial side effects), session continues.
return 1
# 4) Write the wrapped source to disk (used only for error echo: fatal_error reads this file).
viperlib.snprintf(ReplSrcPath, _REPL_PATH_CAP, "%s/%s.vp", ReplTmpDir, ReplNameBuf)
sf: fileio.File* = fileio.File(ReplSrcPath, fileio.MODE.W)
if not sf.closed:
sf.write(ReplSrcBuf, string.strlen(ReplSrcBuf))
sf.close()
# 5) Reset per-fragment module-level translation state (mirrors Phase1Main/BuildPipeline);
# temp_dir is used for .vpi injection: prefer the project temp dir, else the session dir.
vpi_dir: str = Config.TempDir
if vpi_dir is None: vpi_dir = ReplTmpDir
GenApi.set_temp_dir(vpi_dir)
# CallGenBase.TempDir/TempDirLen (cross-module .vpi/.text.ll lookup, CExport checks) is only
# set by SetTempDir; set it here once per fragment, same value as 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()
# Visibility uses loose mode (strict=0, all registered structs visible): fragment types come
# from the session, so strict isolation would hide them and fail annotation resolution.
GenApi.reset_visible_structs(mb, 0)
# Struct registration uses permanent allocation (like Phase1b/Phase2 StdlibAlloc=1): table,
# entry names and field types go into PermPool, else later fragments read freed memory.
GenApi.set_stdlib_alloc(1)
# M4: replay the CDefine/CTypedef session baseline; the clear_* above emptied the tables, and
# this fragment's additions are captured on success by ReplBaselineCapture.
ReplBaselineRestore(mb)
# 6) Parse the wrapped source. Fragments containing "class " use the permanent pool (generic
# templates store the ClassDef node for later specialization); others use mb as before.
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
# 7) Translate to full module IR. ModuleSha1 = session sha1 (types are "<sha1>.<Type>");
# ReplCtx enables REPL mode and is cleared after translation.
tr: 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, mlirlite.OUTPUT_FULL)
# 7') M4: if this fragment defines session-prefixed symbols ("<sha1>."), produce and record
# its import lib so later fragments can resolve them at link time.
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
# 8) Export-symbol probe: the signature must be "(%__repl_ctx__: !llvm.ptr) -> i32"; otherwise
# the wrapper/calling convention is wrong (or raise added EH params), so abort.
viperlib.snprintf(ReplNeedleBuf, _REPL_NEEDLE_CAP, "llvm.func @%s(%%%s: !llvm.ptr) -> i32", ReplNameBuf, GenTypes.REPL_CTX_PARAM)
if string.strstr(ReplIrBuf, ReplNeedleBuf) is None:
stdio.printf("片段 %s: 未生成约定的导出函数(期望 `llvm.func @%s(%%__repl_ctx__: !llvm.ptr) -> i32`);" "含 raise 的片段会追加异常出参,M2 不支持\n", ReplNameBuf, ReplNameBuf)
return 1
# 8') Session function signature check (M3), before llc/load: any failure skips the whole
# fragment; returns 0 immediately when no functions were lifted.
if _repl_precheck_session_funcs(tr.Module) != 0: return 1
# 9) Write .ll (reusing BuildPipeline: sliced path + 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
# 10) llc -> .obj (reusing existing machinery; VLogger.error longjmps on failure).
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:
# This branch is only reached on compile_ll_to_obj's internal allocation failure (real llc
# failures longjmp), so print a note instead of silently returning to the prompt.
stdio.printf("片段 %s: llc 编译 .obj 失败(命令/路径缓冲分配失败,见上方诊断)\n", ReplNameBuf)
return 1
# 11) clang++ -shared -> .dll (command shape matches BuildPipeline.link_obj_to_exe; process
# launch reuses 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
# Cross-module symbols come from the host exe, so add the host import lib (if any) to the link
# line; M4 also adds this fragment's --out-implib and prior fragments' import libs.
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
# 11') M4: record this fragment's import lib (if it defined session-prefixed symbols) so later
# fragments resolve its class/method symbols; recording failure only warns.
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)
# 12) Dynamic load + address lookup (LoadLibraryA/GetProcAddress).
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
# 12') Session function registration (M3), before calling the entry so a failure skips this
# fragment's statements; in-fragment calls to the same name still use direct calls.
if _repl_register_session_funcs(h_lib, n, tr.Module) != 0:
if ReplCtxObj is not None: ReplCtxObj.FuncCount = ReplSavedFuncCount
return 1
# 13) Reset the result area (kind=none) and refresh var_count/last_frag; call indirectly (the
# module stays resident, so no FreeLibrary).
head: 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
# The argument type is the constructed call signature's param type (bytes => i8*).
r_rc: int = ReplFragFn(ReplCtxBuf)
# 14) Read back from the ctx result area and echo (the fragment return is only a status code).
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)
# 14') M4: on success, merge this fragment's new CDefine/CTypedef entries into the session
# baseline; failure paths skip this, so the baseline never holds a failed fragment's leftovers.
ReplBaselineCapture()
return 0
# ============================================================
# run - the M2/M3 REPL main loop.
def run(mb: memhub.MemBuddy*) -> int:
"""M2/M3 REPL: read a line/block, parse, compile to a DLL and call it (session vars via ctx slots), then echo; errors never kill the process."""
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
# Parse tables (keywords/operators) go in PermPool: they are ast module-level globals built
# once; on mb they would be freed by each line's reset_to(mark) and crash the next parse.
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
# Session-level registration: ReplJmpBuf is re-registered per fragment; the VLogger side is set
# once and cleared only on run exit (that is what covers the whole pipeline).
ReplJmpBuf = jb
ReplSavedVarCount = 0
ReplSavedFuncCount = 0
VLogger.set_repl_jmpbuf(jb)
# jit_flag is set True by main before calling run (see main.vp --repl); run resets it to False.
lg: VLogger.Logger = VLogger.logger
# Arm the recovery point before use: session init may longjmp via VLogger.error, and an
# un-armed jmp_buf would be UB; init is the only stage that must succeed, so exit on failure.
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
# Session init: temp dir + permanent buffers + ctx (all PermPool; see 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)
# === Session-level includes init (once; all failures only degrade includes classes) ===
# The recovery point must land in this frame, so a fail-fast longjmp continues into the loop.
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:
# Scan results/registry live in stdlib, so mb allocations can be reclaimed after this call,
# which also performs includes class registration (per-module _setjmp).
ReplInitIncludes(mb)
mb.reset_to(inc_mark)
# Re-arm the recovery point on run's own frame (not a helper: _setjmp's target belongs to the
# calling frame); this puts the finish step and read loop back under this frame's point.
rc_arm: int = _setjmp(jb, None)
if rc_arm != 0:
stdio.printf("REPL: includes 类布局收尾中断(诊断见上):布局注册可能不完整," "会话继续\n")
else:
# Finish: converge classes whose parents became ready in later passes + per-class warnings.
# Done after re-arming so any longjmp lands in this frame (the dead-frame issue above).
VpiReader.ReplStubLayoutFinish()
# Disabling stub-layout recovery must be unconditional: a non-zero flag would make fragment
# translation take the REPL stub path (this function only sets a flag, no failure path).
VpiReader.ReplStubLayoutEnd()
# === Host export symbols: locate the host exe import lib ===
# If found, fragment link lines include it; otherwise warn and continue (calls may fail to link).
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)
# === Read loop: single-line fragments + block continuation ===
# All init failures were absorbed, so the prompt is guaranteed to appear from here on.
stdio.printf("REPL: 初始化完成,进入读行循环\n")
stdio.fflush(None)
# Block state is zeroed after every block end/failure/EOF/overflow, so a stale block cannot
# pollute the next line (the next append overwrites from index 0).
ReplInBlock = 0
ReplBlockLen = 0
while 1:
# Prompt: "... " while in a block, otherwise ">>> " (same print point and flush timing).
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: treat as a terminating line, then exit
eof_hit = 1
n = 0
# Per line: snapshot watermarks, then _setjmp (arming this fragment's recovery point for the
# whole pipeline); any longjmp reclaims mark and continues.
mark: sizet = mb.mark()
rc: int = _setjmp(jb, None)
if rc != 0:
# longjmp return: diagnostics were already printed; clear block state, roll back session
# watermarks and reclaim this line's allocations, then print the next prompt.
ReplInBlock = 0
ReplBlockLen = 0
if ReplCtxObj is not None:
ReplCtxObj.Count = ReplSavedVarCount
ReplCtxObj.FuncCount = ReplSavedFuncCount
# M4: roll back the session baseline (CDefine/CTypedef) watermarks (same idea as above).
ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount)
mb.reset_to(mark)
if eof_hit != 0: break
continue
if eof_hit != 0:
# EOF: while in a block with buffered content, execute the cached block first (like a
# blank line); otherwise take the existing exit path (print a newline then exit).
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:
# Blank line ends continuation: run the whole block once; block errors only print.
if ReplBlockLen > 0: _exec_repl_src(mb, ReplBlockBuf)
ReplInBlock = 0
ReplBlockLen = 0
mb.reset_to(mark)
continue
# In continuation: every line is buffered; no state flips on ':' and no indent analysis.
if _block_append(line, n) != 0:
stdio.printf("REPL: 块输入超过块缓冲上界 %d 字节:本块已丢弃," "回到 >>> 继续(可减少块体积或分块执行)\n", _REPL_BLOCK_CAP)
ReplInBlock = 0
ReplBlockLen = 0
mb.reset_to(mark)
continue
# ==== Existing single-line path (unchanged when not in continuation) ====
if n == 0: # blank line: skip
mb.reset_to(mark)
continue
if _line_starts_block(line, n) != 0:
# Trailing ':' enters continuation: buffer this line without executing it. The first line
# cannot overflow (line cap < block cap), so append failure falls through below.
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)
# Clear the recovery point only at session end (never mid-session, so the pipeline stays protected).
ReplJmpBuf = None
VLogger.clear_repl_jmpbuf()
if lg is not None: lg.set_jit_flag(False)
return 0