Files
TermiNexus 508428bbc9 fix(viperc): 浮点常量按目标类型/声明宽度直接出 f32,不再先出 double
- 浮点字面量在 f32 目标(float(1.5) / x: float = 1.5 / f32 形参)下直接物化 f32 常量,
  负号折叠(-c)同口径;去掉旧的「f64 常量 + fptrunc」
- CDefine 浮点条目记录宽度种类(1=f64 / 2=f32):define PI: float = ... 的使用处
  直接出 f32 常量,与 f32 字面量混算不再被提升成 double(现场 33 处 cvtss2sd)
- 本模块注册 / pyi·vpi 跨模块重解析 / xmod 缓存三处都按注解裁决宽度,保持跨模块一致
2026-10-15 18:35:48 +08:00

2108 lines
104 KiB
Plaintext

from stdint import *
import stdio
import stdlib
import string
import viperlib
import mlirlite
import memhub
import ast
import win32.fileio as fileio
import win32.win32file as win32file
import win32.win32base as win32base
import win32.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 = win32file.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 failed)")
# 2) exe-dir/repl.vpj (GetModuleFileNameA(NULL) returns our own full path).
n_exe: ULONG = win32file.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 failed)")
# Not found in either place: emit diagnostics (needs --project + both searched paths).
viperlib.snprintf(Diag, DiagCap, " Searched(1): %s\n" " Searched(2): %s\n" " --project needed: without repl.vpj the REPL cannot get the project config; " "specify it with --project <path/to/repl.vpj>\n" " Template location: ViperC/repl.vpj in the repo; copy it next to viperc.exe" "or into the current working directory to be found automatically\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%\ViperC_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 = win32file.GetFileAttributesA(path)
if attr == 0xFFFFFFFF: # INVALID_FILE_ATTRIBUTES
return 0
if (attr & win32file.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 = win32file.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 = win32file.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 = 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 value>\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(int(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(int(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(int(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(int(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
# 浮点条目:1=f64 / 2=f32(见 TypeGenCDef 的 CDEFINE_FLOAT_KIND_*),非 0 即浮点
elif cdef_dbl is not None and int(cdef_dbl.get(i)) != 0: 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(int(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(int(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(int(ReplFragImplibCount)):
p: str = rawptr(ReplFragImplibs[i])
if p is None: continue
pos = _repl_cat_str(cmd, cap, int(pos), " ")
if pos < 0: return -1
pos = _repl_cat_str(cmd, cap, int(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(int(_REPL_INC_CLS_PASSES)):
pass_ok: int = 0
i: int
for i in range(int(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 class registration: struct table reached %d entries (near the limit); " "stopping registration; unregistered classes are unavailable in the session (importing their module " "can still register on demand)\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 class registration: module %s has no .vpi stub -- its classes are " "unavailable in the session (other modules unaffected; build the host once with project.vpj " "to generate all stubs)\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 class registration: injection of module %d's .vpi interrupted " "(its classes may be incomplete; session continues)\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 class registration: injection of module %s's .vpi failed " "(that module's classes are incomplete/unavailable; session continues)\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(int(n_mod)):
if int(st[si]) == 0: skip_cnt += 1
stdio.printf("includes classes: registered vpi classes for %d modules (failed %d, missing stub %d, " "not attempted/to retry %d, warnings dropped %d)\n", ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing, skip_cnt, warn_dropped)
if budget_hit != 0:
stdio.printf("includes classes: startup budget of %d ms exhausted, stopped startup registration (classes of the remaining %d " "modules are unavailable in the session; a fragment that `import`s such a module still registers on demand)\n", _REPL_INC_CLS_BUDGET_MS, skip_cnt)
stdio.fflush(None)
return 0
# ============================================================
# Session init: create %TEMP%\ViperC_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 = win32file.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>ViperC_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, "%sViperC_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(int(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: project has no includes dir configured (project.vpj's includes); " "skipping includes init; importing includes modules will report an unregistered 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: no usable .vpi dir (both Config.TempDir and the session dir are empty); " "skipping includes init\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 scan failed (dir: %s); " "importing includes modules will report an unregistered 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 scan registered no modules (dir: %s): " "check the includes path and the .vp files\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: registered %d modules (scan took %d ms); " "stubs new %d / reused %d / failed %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 generic templates: 0 -- list[...]/dict[...] in annotations can only be parsed as " "\"type-only\" placeholders (element layout and methods unavailable)\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 = 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, int(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: session function name cannot start with the reserved prefix '%s': %s\n", pfx, name)
return 1
i: int
for i in range(int(ReplLiftNameCount)):
if string.strcmp(ReplLiftNames + i * _REPL_FN_NAME_MAX, name) == 0:
stdio.printf("M3: session function '%s' defined twice in the same fragment (define it only once)\n", name)
return 1
if ReplLiftNameCount >= _REPL_LIFT_MAX:
stdio.printf("M3: a fragment can lift at most %d session functions (this fragment lifted %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 header is missing the function name: %s\n", _repl_show_line(line, int(n)))
return 1
if nlen >= name_cap:
stdio.printf("M3: session function name too long (limit %d bytes): %s\n", name_cap - 1, _repl_show_line(line, int(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: unclosed generic-parameter bracket in def header: %s\n", _repl_show_line(line, int(n)))
return 1
if p >= n or line[p] != '(':
stdio.printf("M3: def header is missing '(' after the function name: %s\n", _repl_show_line(line, int(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 header is missing ')' (the signature must be on one line): %s\n", _repl_show_line(line, int(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 header ':' is not on the same line (the signature must be on one line): %s\n", _repl_show_line(line, int(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: session function '%s' cannot declare the %s parameter " "(it is auto-injected as the first parameter when lifting)\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, int(pos), "export ")
pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), line, lp + 1)
if empty != 0: pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "__repl_ctx__: bytes")
else: pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "__repl_ctx__: bytes, ")
pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), line + lp + 1, n - lp - 1)
pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "\n")
if pos < 0:
stdio.printf("M3: lift source buffer too small (%d bytes)\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 = int(_repl_line_len(body + i))
ind: int = int(_repl_indent_len(body + i, int(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, int(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: a def header must immediately follow the decorator: %s\n", _repl_show_line(body + i, int(n)))
return 1
if in_body != 0 and _repl_line_has_kw(body + i, int(n), ind, "raise") != 0:
stdio.printf("M3: session function '%s' has raise in its body: %s\n" " M3 does not support cross-fragment exception propagation (raise appends the " "__eh_msg_out__/__eh_code_out__ out params to the function, " "but indirect calls through the function pointer do not pass them) -- the whole fragment is skipped\n", name_buf, _repl_show_line(body + i, int(n)))
return 1
lp1: int = int(string.strlen(ReplLiftBuf))
lp1 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp1, body + i, int(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, int(pos), " ")
pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), body + i, int(n))
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(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, int(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, int(n), 0, "def") != 0:
rc_h: int = int(_repl_lift_def_header(body + i, int(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, int(n), 0, "async") != 0 and _repl_line_has_kw(body + i, int(n), 6, "def") != 0:
stdio.printf("M3: async def session functions are not supported (coroutine ABI is incompatible with " "the M3 signature token; rewrite as a plain def): %s\n", _repl_show_line(body + i, int(n)))
return 1
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), " ")
pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), body + i, int(n))
pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), "\n")
if pos < 0: return 2
i += n + 1
if need_header != 0:
stdio.printf("M3: missing def header after decorator (end of fragment)\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, int(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, int(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(int(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(int(pc)):
if i > 0: pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), ", ")
pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(int(ReplSigTags[i]), int(ReplSigBits[i])))
pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), ") -> ")
pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(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: session function table/signature buffer not initialized (session init failed)\n")
return 1
new_cnt: int = 0
idx: int
for idx in range(int(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: function '%s' not found in the defining fragment's model (lift not effective?)\n", nm)
return 1
if rc == 2:
stdio.printf("M3: session function '%s' signature has an unsupported type '%s'\n" " M3 supports void / i1 / i8 / i16 / i32 / i64 / float / double / " "i8* (str, bytes, rawptr); for struct values/arrays/multi-level pointers use pointer params " "or wait for M4\n", nm, ReplSigResult.BadType)
return 1
if rc == 3:
stdio.printf("M3: session function '%s' signature has EH out params (%%__eh_msg_out__/" "%%__eh_code_out__) -- its body contains raise\n" " M3 does not support cross-fragment exception propagation: indirect calls through the function pointer do not pass these two args " "(arity mismatch); the whole fragment is skipped\n", nm)
return 1
if rc == 4:
stdio.printf("M3: session function '%s' define's first param is not %s (lift template and calling convention " "disagree)\n", nm, GenTypes.REPL_CTX_PARAM)
return 1
if rc == 5:
stdio.printf("M3: session function '%s' has more than %d params\n", nm, GenTypes.REPL_FUNC_PARAM_MAX)
return 1
if rc != 0:
stdio.printf("M3: failed to parse session function '%s' signature (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(int(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: redefinition of session function '%s' has a different signature (old %s, new %s)\n" " M3 allows a same-signature overwrite (address only); a different signature would make call " "sites already compiled in earlier fragments call new code with the old ABI -- the whole fragment is skipped; use another name\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: session function limit of %d reached (ctx function pointer slots exhausted); %d added this time; " "restart the session or use fewer session functions\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(int(e.ParamCount)):
if i > 0: pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), ", ")
pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(int(e.ParamTags[i]), int(e.ParamBits[i])))
pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), ") -> ")
pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(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: session function table/ctx not initialized\n")
return 1
idx: int
for idx in range(int(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: session function '%s' is absent from the fragment DLL's export table " "(GetProcAddress returned null; this fragment is skipped)\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: failed to re-parse session function '%s' signature (rc=%d)\n", nm, rc)
return 1
j: int = 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: failed to allocate the session function entry ('%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("session function %s: export symbol %s -> ctx slot +%d (defining fragment %d, signature %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] = char(tk // 1000)
ReplSigBits[out.ParamCount] = char(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 = "(unknown)"
stdio.printf("ParseError: line %d column %d: %s\n", el, ec, em)
return 0
if _count_top_stmts(tree) <= 0:
stdio.printf("Parsed: 0 top-level statements (skipping)\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: session buffer not initialized\n")
return 1
if ReplSrcPath is None or ReplIrBuf is None or ReplNeedleBuf is None:
stdio.printf("REPL: session buffer not initialized\n")
return 1
if ReplCtxBuf is None or ReplCtxObj is None or ReplVarTable is None:
stdio.printf("REPL: ctx session memory not initialized\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("fragment %s: failed to assemble the wrapped source (buffer too small, %d bytes)\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 = "(unknown)"
stdio.printf("fragment %s: failed to parse the wrapped source (line %d column %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("fragment %s: Translator allocation failed\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("fragment %s: translation failed\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("fragment %s: expected export function was not generated (expected `llvm.func @%s(%%__repl_ctx__: !llvm.ptr) -> i32`); " "fragments with raise append EH out params, which M2 does not support\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("fragment %s: failed to write .ll (dir %s)\n", ReplNameBuf, ReplTmpDir)
return 1
ir_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "ll")
if ir_path is None:
stdio.printf("fragment %s: failed to build the .ll path\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("fragment %s: llc failed to compile .obj (command/path buffer allocation failed; see diagnostics above)\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("fragment %s: failed to build the .obj/.dll path\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("fragment %s: command-line buffer allocation failed\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("fragment %s: import lib path buffer allocation failed\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("fragment %s: link command line exceeds %d bytes (%d registered fragment import libs " "in total) -- this fragment is skipped; restart the session to reduce the fragment count\n", ReplNameBuf, _REPL_CMD_CAP, ReplFragImplibCount)
return 1
link_rc: int = BuildPipeline._run_cmd_wait(cmd)
if link_rc != 0:
stdio.printf("fragment %s: failed to generate .dll (exit=%d)\n command: %s\n", ReplNameBuf, link_rc, cmd)
if ReplHostImplib is None:
stdio.printf(" Hint: host import lib (<exe name>.dll.a) not found -- " "cross-module symbols (of the form <16-hex>.<func name>) cannot be resolved; " "build the host with ViperC/project.vpj (including -Wl,--export-all-symbols), " "and keep <exe name>.dll.a in the same dir as the exe\n")
else:
stdio.printf(" Hint: if the output contains undefined symbol of the form <16-hex>.<name>, " "that symbol is not defined/exported by the host (the corresponding includes module was not linked when building the host); " "if the function is `export def` in source (CExport, bare-name definition) yet still carries a prefix, " "the include stub .vpi is probably missing (see the includes scan line at session start)\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("fragment %s: registered fragment import lib (later fragments can resolve types/method " "symbols defined by this fragment): %s\n", ReplNameBuf, implib_path)
else:
stdio.printf("fragment %s: fragment import lib not registered (%d registered, limit %d; or the file " "is missing) -- later fragments may fail to resolve types/method symbols defined here\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("fragment %s: LoadLibraryA failed: %s\n", ReplNameBuf, dll_path)
if ReplHostImplib is not None:
stdio.printf(" Hint: the fragment DLL import table points to the host exe (%s); " "if GetLastError is 127 (procedure not found) / loader 0xC0000139, " "the host and this import lib are usually from different builds, or the host lacks that export; " "it also fails when the process image name (%s) differs from the recorded import lib name\n", ReplHostImplib, dll_path)
return 1
fp_addr: VOIDPTR = win32base.GetProcAddress(h_lib, ReplNameBuf)
if fp_addr is None:
stdio.printf("fragment %s: GetProcAddress returned null (symbol absent from the export table): %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, int(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("fragment %s: failed to read the result area\n", ReplNameBuf)
return 1
if r_rc != 0: stdio.printf("fragment %s: fragment returned non-zero status %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: need the main pool and PermPool (main injects _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 allocation failed\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: session init interrupted (see diagnostics above) -- no resident buffers/ctx, REPL cannot start\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: session init failed (temp dir/resident buffers/ctx)\n")
ReplJmpBuf = None
VLogger.clear_repl_jmpbuf()
if lg is not None: lg.set_jit_flag(False)
return 1
h_in: win32base.HANDLE = win32file.GetStdHandle(win32file.STD_INPUT_HANDLE)
line: bytes = stdlib.malloc(_REPL_LINE_CAP)
if line is None:
stdio.printf("REPL: line buffer allocation failed\n")
ReplJmpBuf = None
VLogger.clear_repl_jmpbuf()
if lg is not None: lg.set_jit_flag(False)
return 1
stdio.printf("ViperC REPL (M2/M3/M4) - session variables + session functions (cross-fragment) + ctx result echo;\n")
stdio.printf(" Supports expressions (1+2), definitions (x = 1 / x: int = 5), references (x + 1),\n")
stdio.printf(" augmented assignment (x += 2), address-of (p = &x), etc.; blank lines are skipped, Ctrl+Z or EOF exits\n")
stdio.printf(" Multi-line block: a trailing ':' enters continuation (prompt ...), a blank line ends it and runs the block once\n")
stdio.printf(" Cross-fragment functions: a top-level def in a block is lifted to the fragment module's export def (bare-name export),\n")
stdio.printf(" later fragments can call it directly (e.g. def f(x: int) -> int: newline + 4 spaces return x + 1);\n")
stdio.printf(" Restrictions: signature must be one line, async def is unsupported, raise in the body is unsupported, partial functions are unsupported\n")
stdio.printf(" Cross-fragment types (M4): a class in a block is translated into the session (type name %s.<class name>),\n", ReplSessionSha1)
stdio.printf(" later fragments can instantiate it, read/write fields and call methods; imported includes modules\n")
stdio.printf(" have their classes registered into the session too (when the .vpi stub is missing that module's classes are unavailable; the session continues)\n")
stdio.printf(" Fragment temp dir: %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 init interrupted (%d modules registered, %d stubs): " "importing includes modules may fail; other fragments are unaffected\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 class layout finish interrupted (see diagnostics above): layout registration may be incomplete; " "session continues\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("Host symbol import lib: %s (fragments can call includes functions/globals across modules)\n", ReplHostImplib)
else:
stdio.printf("Host symbol import lib: <exe name>.dll.a not found; fragment calls to includes module-level " "functions will report undefined symbol at link time (the session does not terminate). Build the host with " "ViperC/project.vpj (which declares -Wl,--export-all-symbols), " "and keep the exe and .dll.a in the same dir\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: init complete, entering the read loop\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, int(n)) != 0:
stdio.printf("REPL: block input exceeds the block buffer cap of %d bytes: this block was discarded, " "returning to >>> (reduce the block size or run it in chunks)\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, int(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, int(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