- 浮点字面量在 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 缓存三处都按注解裁决宽度,保持跨模块一致
2108 lines
104 KiB
Plaintext
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
|