diff --git a/App/lib/Phase/BuildPipeline.vp b/App/lib/Phase/BuildPipeline.vp index cfc8099..851997d 100644 --- a/App/lib/Phase/BuildPipeline.vp +++ b/App/lib/Phase/BuildPipeline.vp @@ -1,2549 +1,2549 @@ -import lib.core.Gen.GenApi as GenApi -import t, c -from stdint import * -import stdio -import string -import stdlib -import memhub -import viperlib -import vthreading -import win32.fileio as fileio -import win32.win32file -import win32.win32base -import win32.win32process as win32process -import win32.win32sync as win32sync -import subprocess -import viperlib -import ast -import os as oslib -import shutil -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HandlesTranslator -import lib.core.Gen.DebugInfoGen as DebugInfoGen -import lib.Stub.StubMerger as StubMerger -import lib.Stub.StubGen.Converter as StubConverter -import lib.Projectrans.Config as Config -import lib.core.Gen.Generator as GenGenerator -import lib.core.Gen.Expr.ExprGenCls as ExprGenCls -import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect - -# BuildPipeline - compile pipeline: write .ll to temp, run llc (.ll -> .obj), -# then link with clang++ (.obj -> .exe). - -# global mbuddy pointer -_mbuddy: memhub.MemBuddy* - -# ASan mode (--asan): instrument .ll via opt -passes=asan; the ASan runtime is -# declared explicitly in project.vpj's linker.static_libs / dynamic_libs (TPV injects nothing). -_asan_mode: bool = 0 - -# Traceback mode (--traceback): force-build includes/traceback.vp, inject the startup -# Install() ctor, keep frame pointers, link dbghelp, then export .stackmap. -_traceback_mode: bool = 0 - -# --phase2command xxx.bat: do not run llc/link; write the llc (incl. ASan opt) and clang -# link commands to the given .bat path for manual execution. None = run normally (default). -_phase2_cmd_path: str = None -# whether the bat header (@echo off) has been written; set to 1 on first write -_phase2_bat_init: int = 0 - -# MLIR lowering step (fixed, no backend switch): mlirlite emits MLIR but opt/llc accept -# only LLVM IR, so run mlir-opt (validate) then mlir-translate --mlir-to-llvmir first. -MLIR_OPT_CMD: str = "mlir-opt" -MLIR_TRANSLATE_CMD: str = "mlir-translate" - - -def _ir_sibling_path(ir_path: str, suffix: str) -> str: - """Derive a sibling path from <...>.ll: strip the .ll suffix and append suffix; mbuddy-allocated.""" - if ir_path is None or suffix is None: return None - n: sizet = string.strlen(ir_path) - if n < 3: return None - suf_len: sizet = string.strlen(suffix) - buf: str = _mbuddy.alloc(n + suf_len + 1) - if buf is None: return None - string.strncpy(buf, ir_path, n - 3) - buf[n - 3] = '\0' - string.strcat(buf, suffix) - return buf - -# source buffer size (1MB) -define SRC_BUF_SIZE = 1048576 # 2^20 = 1MB - -# .vpi stub buffer size (256KB, same as Phase1). Use a compile-time define: a module-level -# global with this name would emit a second strong symbol and llvm-link reports multiply defined. -define PYI_BUF_SIZE = 262144 - -# _llc_frame_ptr_flag - llc extra flag for --traceback: " -frame-pointer=all" while -# enabled (leading space), "" otherwise so the llc command stays byte-identical. -def _llc_frame_ptr_flag() -> str: - if _traceback_mode != 0: return " -frame-pointer=all" - return "" - - -# _append_traceback_dbghelp - append -ldbghelp for --traceback (StackWalk64 etc. live in -# dbghelp); appended so libraries follow the objects, else the link has undefined symbols. -def _append_traceback_dbghelp(linker_flags: str) -> str: - if _traceback_mode == 0 or linker_flags is None: return linker_flags - need: sizet = string.strlen(linker_flags) + 16 - buf: str = _mbuddy.alloc(need) - if buf is None: - VLogger.error("--traceback: failed to allocate -ldbghelp linker argument buffer, aborting compilation", "TRACEBACK") - return linker_flags - viperlib.snprintf(buf, need, "%s -ldbghelp", linker_flags) - return buf - - -# --traceback call-site sidecar (.vpcall): the compiler accumulates F/C records per -# module; WriteCallSidecar writes and registers them, WriteStackMap reads them after link. -define TB_VPCALL_MAX = 8192 -_tb_vpcall_buf: bytes = None # TB_VPCALL_MAX * 8: one char* per slot (stdlib-allocated, one-shot per process) -_tb_vpcall_n: int = 0 - - -def _tb_register_vpcall(path: str) -> int: - """Register a written .vpcall path in the global table for WriteStackMap to read.""" - global _tb_vpcall_buf, _tb_vpcall_n - if path is None: return 0 - if _tb_vpcall_buf is None: - _tb_vpcall_buf = stdlib.malloc(sizet(TB_VPCALL_MAX) * 8) - if _tb_vpcall_buf is None: - VLogger.error("--traceback: failed to allocate .vpcall path table, aborting compilation", "TRACEBACK") - return 1 - _tb_vpcall_n = 0 - if _tb_vpcall_n >= TB_VPCALL_MAX: - VLogger.error("--traceback: .vpcall path table overflow (module count exceeds limit), aborting compilation", "TRACEBACK") - return 1 - slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(_tb_vpcall_n) * 8) - slot[0] = path - _tb_vpcall_n += 1 - return 0 - - -def WriteCallSidecar(temp_dir: str, sha1: str, text: str) -> int: - """--traceback: write this module's call-site text to //.vpcall; None text skips.""" - if temp_dir is None or sha1 is None or text is None: return 0 - vpath: str = StubMergerCollect._sliced_path(temp_dir, string.strlen(temp_dir), sha1, "vpcall") - if vpath is None: - VLogger.error(f"--traceback: failed to build .vpcall path (sha1={sha1}), aborting compilation", "TRACEBACK") - return 1 - vf: fileio.File* = fileio.File(vpath, fileio.MODE.W) - if vf.closed: - VLogger.error(f"--traceback: failed to write .vpcall (cannot open {vpath}), aborting compilation", "TRACEBACK") - stdlib.free(vpath) - return 1 - vf.write(text, string.strlen(text)) - vf.close() - return _tb_register_vpcall(vpath) - - -# _tb_basename_is_traceback - whether this source file is includes/traceback.vp -def _tb_basename_is_traceback(file_path: str) -> int: - if file_path is None: return 0 - flen: sizet = string.strlen(file_path) - if flen < 12: return 0 - if flen > 12: - prev: char = file_path[flen - 13] - if prev != '/' and prev != '\\': return 0 - return 1 if string.strcmp(file_path + (flen - 12), "traceback.vp") == 0 else 0 - -# TranslateFileGetTrans - translate one file and return a Translator (caller dumps IR). -# File -> AST -> .vpi -> LLVM IR; includes use loose scoping, user files strict (local + imports). -def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str, sha1_val: str, current_package: str = None, declare_only: int = 0, temp_dir: str = None, pre_allow_dep_inject: int = 0) -> HandlesTranslator.Translator*: - """Translate one file and return a Translator (caller dumps IR); None on failure.""" - if file_path is None: return None - f: fileio.File* = fileio.File(file_path, fileio.MODE.R) - if f.closed: return None - # read_all's capacity includes the terminator (reads cap-1 bytes max) and silently - # truncates longer files; reject by size first, else parsing gets confusing syntax errors. - if f.size() >= SRC_BUF_SIZE: - VLogger.error(f"source file exceeds {SRC_BUF_SIZE} byte limit, refusing to read: {file_path}", "fileio") - f.close() - return None - src_buf: bytes = stdlib.malloc(SRC_BUF_SIZE) - if src_buf is None: - f.close() - return None - bytes_read: LONG = f.read_all(src_buf, SRC_BUF_SIZE) - f.close() - if bytes_read <= 0: - stdlib.free(src_buf) - return None - if bytes_read < SRC_BUF_SIZE: src_buf[bytes_read] = 0 - lx: ast.Lexer* = ast.new_lexer(mb) - ast._lexer_init(lx, src_buf, mb) - tokens: ast.Token* = ast.tokenize(lx) - tree: ast.AST* = ast.parse_tokens(mb, tokens) - # AST is independent after parse; free the source buffer (avoid a 1MB/file leak) - stdlib.free(src_buf) - if tree is None: - # fail-fast location: print a Python-style parse-error traceback (like fatal_error): - # Traceback (most recent call last) / File "", line N / SyntaxError: - GenApi.dump_parse_traceback(file_path) - err_line: int = ast.get_parse_err_line() - err_col: int = ast.get_parse_err_col() - VLogger.error(f"parse failed: {file_path} (line {err_line} col {err_col})", "PARSE") - return None - # Generate the .vpi stub from the AST before tr.translate (same as Phase1); it lets - # other modules query function signatures and CTypedef definitions. - if temp_dir is not None: - pyi_buf: bytes = stdlib.malloc(PYI_BUF_SIZE) - if pyi_buf is not None: - pyi_buf[0] = 0 - # Pass file_path as rel_path: vpi-time fatal_error must blame this source file, - # and the absolute path lets _read_source_line open and print the source line. - pyi_pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, file_path, pyi_buf, PYI_BUF_SIZE) - if pyi_pos > 0: - # write {sha1}.vpi into temp_dir (sliced path) - td_len_vpi: sizet = len(temp_dir) - pyi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpi, sha1_val, "vpi") - if pyi_path is not None: - pf: fileio.File* = fileio.File(pyi_path, fileio.MODE.W) - if not pf.closed: - pf.write(pyi_buf, pyi_pos) - pf.close() - stdlib.free(pyi_path) - stdlib.free(pyi_buf) - tr: HandlesTranslator.Translator* = mb.alloc(HandlesTranslator.Translator.__sizeof__()) - tr.__before_init__() - tr.__init__() - tr.ModuleSha1 = sha1_val - tr.CurrentPackage = current_package - tr._declare_only = declare_only - # App pre-registration (Phase A-pre) switch: when 1, import handling + dependency vpi - # injection also run in the declare phase. includes pre-registration must pass 0. - tr._pre_allow_dep_inject = pre_allow_dep_inject - # P2: reuse the global gen singleton (created by App/main.vp); create it as a fallback - # when unregistered (defensive for direct-call paths). - gen: GenGenerator.Generator* = GenGenerator._g_gen - if gen is None: - gen = mb.alloc(GenGenerator.Generator.__sizeof__()) - if gen is None: - VLogger.error("Generator alloc failed", "GEN") - return None - gen.__before_init__() - gen.__init__(mb) - GenGenerator._g_gen = gen - # P2-P5: inject the Handles module-level singletons (delegated functions use local - # _g_gen to avoid package/class name ambiguity from in-body imports). - GenApi._g_gen = gen - # P6: inject singletons for statement/assign/var/import/translate-driver modules - # set the current file name (for error reporting) - GenApi.set_current_file_type(file_path) - # --traceback: mirror the switch into the Gen-layer DWARF emitter. 0 => every DI entry - # point is a no-op and the IR stays byte-identical to the pre-DWARF output. - DebugInfoGen.SetEnabled(_traceback_mode) - # also set it for HandlesImports (import SHA1 checks need it; a circular dep blocks reading HandlesType) - GenApi.set_current_file_imports(file_path) - # clear the call stack (reset before each file translation) - GenApi.clear_trace() - # module switch: clear the CDefine constant table to isolate each module's constants - GenApi.clear_cdefine_constants() - # module switch: clear the CTypedef table to isolate each module's typedefs - GenApi.clear_ctypedefs() - # namespace isolation: includes loose (all visible), user files strict (local + imports only) - strict_mode: int = 1 - if string.strstr(file_path, "includes") is not None: strict_mode = 0 - GenApi.reset_visible_structs(mb, strict_mode) - # Extract docstrings before tr.translate (pre-registration registers cross-module __doc__; - # must run before translate recycles the AST pool blocks, which crashes under ASan). - if gen is not None: gen.ExprGenCls.extract_module_docstrings(tree, sha1_val) - ret: int = tr.translate(tree) - if ret != 0: - VLogger.error(f"file translation failed: {file_path}", "Translate") - return None - # --traceback: for includes/traceback.vp, inject the startup Install() ctor into its IR. - # The injector lives in the Gen layer, so no new cross-module mlirlite dependency. - if _traceback_mode != 0 and _tb_basename_is_traceback(file_path) != 0: HandlesTranslator.InjectTracebackInstallCtor(tr) - return tr - - -class BuildResult: - Success: int - OutputPath: str - ErrorMsg: str - - def __new__(self) -> t.CPtr: - return t.CPtr(_mbuddy.alloc(BuildResult.__sizeof__())) - - def __init__(self): - self.Success = 0 - self.OutputPath = None - self.ErrorMsg = None - - -def ensure_dir(path: str) -> int: - """Recursively create a directory (like mkdir -p); an existing directory counts as success.""" - if path is None: return 1 - - path_len: sizet = len(path) - if path_len == 0: return 0 - - # Hot path (PROF: ensure_dir was 4.5%): if the final dir exists, return success early, - # saving an alloc + N+1 CreateDirectoryA calls. Semantics unchanged (exists == success). - attr: ULONG = win32.win32file.GetFileAttributesA(path) - if attr != 0xFFFFFFFF and (attr & win32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 0 - - # copy the path into a writable buffer (for stepwise truncation) - buf: bytes = _mbuddy.alloc(path_len + 1) - if buf is None: return 1 - string.strcpy(buf, path) - - # Temporarily truncate at each separator and create every directory level. - # CreateDirectoryA returns 0 when the dir exists (failure); ignore it. - plen: int = int(path_len) - for i in range(int(plen)): - ch: int = buf[i] - if ch == 47 or ch == 92: # '/' = 47, '\\' = 92 - saved: char = char(ch) - buf[i] = '\0' - win32.win32file.CreateDirectoryA(buf, None) - buf[i] = saved - - # create the final directory - win32.win32file.CreateDirectoryA(buf, None) - return 0 - - -# build_sliced_path - build a sliced path and ensure its directory exists. Fans out into -# SHA1-prefix subdirs per Config.Sha1SliceLevel; returns the full path (mbuddy) or None. -def build_sliced_path(base_dir: str, sha1: str, ext: str) -> str: - """Build a sliced path and ensure its directory exists.""" - if base_dir is None or sha1 is None or ext is None: return None - - subdir: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel) - base_len: sizet = len(base_dir) - sha1_len: sizet = len(sha1) - ext_len: sizet = len(ext) - - if subdir is not None: - sub_len: sizet = len(subdir) - # ensure_dir({base_dir}/{subdir}) - dir_path: str = _mbuddy.alloc(base_len + sub_len + 2) - if dir_path is not None: - viperlib.snprintf(dir_path, base_len + sub_len + 2, "%s/%s", base_dir, subdir) - ensure_dir(dir_path) - # file path: {base_dir}/{subdir}/{sha1}.{ext} - path_len: sizet = base_len + sub_len + sha1_len + ext_len + 4 - path: str = _mbuddy.alloc(path_len) - if path is None: return None - viperlib.snprintf(path, path_len, "%s/%s/%s.%s", base_dir, subdir, sha1, ext) - return path - else: - # no slicing: {base_dir}/{sha1}.{ext} - path_len = base_len + sha1_len + ext_len + 3 - path = _mbuddy.alloc(path_len) - if path is None: return None - viperlib.snprintf(path, path_len, "%s/%s.%s", base_dir, sha1, ext) - return path - - -def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name: str) -> int: - """Write the IR buffer to a .ll file; 0 on success, non-zero on failure.""" - if ir_buf is None or output_dir is None or module_name is None: return 1 - - # build sliced path: output_dir/{sha1 prefix}/{module_name}.ll - path: str = build_sliced_path(output_dir, module_name, "ll") - if path is None: return 1 - - # open the file for writing - f: fileio.File* = fileio.File(path, fileio.MODE.W) - if f.closed: return 1 - - written: int64 = f.write(ir_buf, ir_len) - f.close() - if written < 0: return 1 - return 0 - - -def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int: - """Compile a .ll file to .obj via llc; 0 on success, non-zero on failure.""" - if ir_path is None or cc_cmd is None or output_dir is None or module_name is None: return 1 - - # build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj - obj_path: str = build_sliced_path(output_dir, module_name, "obj") - if obj_path is None: return 1 - - # ASan mode: run opt -passes=asan before llc. -asan-globals=false disables global - # instrumentation: TPV addresses globals directly and ASan redzones would shift them. - compile_input: str = ir_path - - # ---- MLIR lowering (fixed, no switch): mlir-opt (validate) -> mlir-translate ---- - # Must run before ASan opt and llc, replacing compile_input with <...>.ll.local. - ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local") - mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt") - if ll_local_path is None or mlir_opt_out is None: return 1 - mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16 - cmd_mo: str = _mbuddy.alloc(mo_len) - if cmd_mo is None: return 1 - viperlib.snprintf(cmd_mo, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out) - mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40 - cmd_mt: str = _mbuddy.alloc(mt_len) - if cmd_mt is None: return 1 - viperlib.snprintf(cmd_mt, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path) - if _phase2_cmd_path is not None: - # write the command only (must precede the ASan/llc commands below) - _phase2_write_cmd(cmd_mo) - _phase2_write_cmd(cmd_mt) - else: - mo_rc: int = _run_cmd_wait(cmd_mo) - if mo_rc != 0: - VLogger.error(f"mlir-opt failed (module={module_name}, exit={mo_rc}, cmd={cmd_mo})", "MLIR") - return 1 - mt_rc: int = _run_cmd_wait(cmd_mt) - if mt_rc != 0: - VLogger.error(f"mlir-translate failed (module={module_name}, exit={mt_rc}, cmd={cmd_mt})", "MLIR") - return 1 - compile_input = ll_local_path - - # --phase2command mode: do not run opt/llc; write the opt (ASan) + llc commands to the bat - if _phase2_cmd_path is not None: - p2_opt_cmd: str = None - if _asan_mode != 0: - p2_asan: str = build_sliced_path(output_dir, module_name, "asan.ll") - if p2_asan is not None: - p2_olen: sizet = len(p2_asan) + len(compile_input) + 192 - p2_opt_cmd = _mbuddy.alloc(p2_olen) - if p2_opt_cmd is not None: - viperlib.snprintf(p2_opt_cmd, p2_olen, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", p2_asan, compile_input) - compile_input = p2_asan - llc_fp: str = _llc_frame_ptr_flag() - p2_clen: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp) + len(obj_path) + len(compile_input) + 64 - p2_cmd: bytes = _mbuddy.alloc(p2_clen) - if p2_cmd is not None: - viperlib.snprintf(p2_cmd, p2_clen, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp, obj_path, compile_input) - if p2_opt_cmd is not None: _phase2_write_cmd(p2_opt_cmd) - if p2_cmd is not None: _phase2_write_cmd(p2_cmd) - return 0 - - if _asan_mode != 0: - asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll") - if asan_path is None: return 1 - opt_cmd_len: sizet = len(asan_path) + len(compile_input) + 192 - opt_cmd: bytes = _mbuddy.alloc(opt_cmd_len) - if opt_cmd is None: return 1 - viperlib.snprintf(opt_cmd, opt_cmd_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", asan_path, compile_input) - opt_ret: int = stdlib.system(opt_cmd) - if opt_ret != 0: - VLogger.error(f"opt ASan instrumentation failed (module={module_name})", "ASAN") - return 1 - compile_input = asan_path - - # command: llc -filetype=obj -o {obj_path} ir_path - llc_fp2: str = _llc_frame_ptr_flag() - cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp2) + len(obj_path) + len(compile_input) + 64 - cmd: bytes = _mbuddy.alloc(cmd_len) - if cmd is None: return 1 - viperlib.snprintf(cmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp2, obj_path, compile_input) - - # Launch llc directly via CreateProcessA (avoids cmd/c hanging under asan); the child - # inherits the console. On failure, log module sha1 + exit code + full command. - sys_ret: int = _run_cmd_wait(cmd) - if sys_ret != 0: - VLogger.error(f"compilation failed (module={module_name}, exit={sys_ret}, cmd={cmd})", "LLC") - return 1 - return 0 - - -# llc parallel compile (Phase B, async threads): tasks/commands use stdlib.malloc (main pool mb -# gets reset); the worker never allocates from MemBuddy. Max threads = os.cpu_count() (16 here). -define LLC_MAX_THREADS = 16 - -# LlcTask.FailStage: which step failed (0=none, 1=mlir-opt, 2=mlir-translate, 3=ASan opt, -# 4=llc). Without this the main thread misreported mlir-opt/translate failures as llc failures. -define LLC_STAGE_NONE = 0 -define LLC_STAGE_MLIR_OPT = 1 -define LLC_STAGE_MLIR_TRANSLATE = 2 -define LLC_STAGE_ASAN_OPT = 3 -define LLC_STAGE_LLC = 4 - - -class LlcTask: - """Async llc task: command strings (stdlib.malloc), module SHA1 and result.""" - OptCmd: bytes # ASan instrumentation command (None if not ASan) - LlcCmd: bytes # llc command - ModuleSha1: bytes # module SHA1 (for error reporting) - Result: int # worker-written: 0 success, non-zero failure - # ExitCode is appended at the end so existing offsets are unchanged; all construction - # sites use __sizeof__() and memset the whole record, so offsets need no update. - ExitCode: int # worker-written: exit code of the failing command (0 on success) - # MLIR-backend lowering commands (None if not MLIR); also appended at the end. - MlirOptCmd: bytes # mlir-opt validation command - MlirTranslateCmd: bytes # mlir-translate lowering command - # failure stage (LLC_STAGE_*, see the constants above); appended at the end. - FailStage: int # worker-written: which step failed - - -def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int: - """Fill an llc task (all stdlib.malloc, kept alive for the thread's run); 0 on success.""" - if task is None or ir_path is None or output_dir is None or module_name is None: return 1 - string.memset(task, 0, LlcTask.__sizeof__()) - task.Result = -1 - obj_path: str = build_sliced_path(output_dir, module_name, "obj") - if obj_path is None: return 1 - # module SHA1 - sl: sizet = len(module_name) - task.ModuleSha1 = stdlib.malloc(sl + 1) - if task.ModuleSha1 is None: return 1 - string.strcpy(task.ModuleSha1, module_name) - # llc input choice: ASan mode must feed the instrumented .asan.ll, same as the serial - # compile_ll_to_obj path (else llc ignores the instrumentation and ASan is half-blind). - compile_input: str = ir_path - # MLIR lowering (fixed): the worker runs mlir-opt (validate) -> mlir-translate first, so - # the later ASan OptCmd / LlcCmd input must be <...>.ll.local. - ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local") - mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt") - if ll_local_path is None or mlir_opt_out is None: return 1 - mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16 - task.MlirOptCmd = stdlib.malloc(mo_len) - if task.MlirOptCmd is None: return 1 - viperlib.snprintf(task.MlirOptCmd, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out) - mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40 - task.MlirTranslateCmd = stdlib.malloc(mt_len) - if task.MlirTranslateCmd is None: return 1 - viperlib.snprintf(task.MlirTranslateCmd, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path) - compile_input = ll_local_path - # ASan mode: opt instrumentation command (the worker runs MLIR lowering/opt before llc, - # so LlcCmd's input must point at asan_path; failure sets Result). - if _asan_mode != 0: - asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll") - if asan_path is None: return 1 - opt_len: sizet = len(asan_path) + len(compile_input) + 192 - task.OptCmd = stdlib.malloc(opt_len) - if task.OptCmd is None: return 1 - viperlib.snprintf(task.OptCmd, opt_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address" " -asan-globals=false -o %s %s", asan_path, compile_input) - compile_input = asan_path - # llc command (input = compile_input: .ll normally, .asan.ll under ASan) - llc_fp3: str = _llc_frame_ptr_flag() - cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp3) + len(obj_path) + len(compile_input) + 64 - task.LlcCmd = stdlib.malloc(cmd_len) - if task.LlcCmd is None: return 1 - viperlib.snprintf(task.LlcCmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp3, obj_path, compile_input) - return 0 - - -def free_llc_task(task: LlcTask*): - """Free the task's command strings; the record itself is a slot in the task array.""" - if task is None: return - if task.OptCmd is not None: - stdlib.free(task.OptCmd) - task.OptCmd = None - if task.LlcCmd is not None: - stdlib.free(task.LlcCmd) - task.LlcCmd = None - if task.MlirOptCmd is not None: - stdlib.free(task.MlirOptCmd) - task.MlirOptCmd = None - if task.MlirTranslateCmd is not None: - stdlib.free(task.MlirTranslateCmd) - task.MlirTranslateCmd = None - if task.ModuleSha1 is not None: - stdlib.free(task.ModuleSha1) - task.ModuleSha1 = None - - -def _phase2_write_cmd(cmd: str) -> None: - """--phase2command mode: append the command to the bat file without executing it.""" - global _phase2_bat_init - if _phase2_cmd_path is None or cmd is None: return - f: fileio.File* = fileio.File(_phase2_cmd_path, fileio.MODE.A) - if f.closed: - VLogger.error("phase2command: failed to open bat", "phase2cmd") - return - if _phase2_bat_init == 0: - _phase2_bat_init = 1 - hdr1: str = "@echo off\r\n" - hdr2: str = "rem TransPyV --phase2command generated commands\r\n" - f.write(hdr1, string.strlen(hdr1)) - f.write(hdr2, string.strlen(hdr2)) - f.write(cmd, string.strlen(cmd)) - f.write("\r\n", 2) - f.close() - - -def _run_cmd_wait(cmd: str) -> int: - """Run a command synchronously via CreateProcessA and return its exit code.""" - if cmd is None: return -1 - cl_len: sizet = len(cmd) - cmd_line: bytes = stdlib.malloc(cl_len + 1) - if cmd_line is None: return -1 - string.strcpy(cmd_line, cmd) - sa: win32.win32base.SECURITY_ATTRIBUTES = win32.win32base.SECURITY_ATTRIBUTES() - si: win32process.STARTUPINFOA = win32process.STARTUPINFOA() - pi: win32process.PROCESS_INFORMATION = win32process.PROCESS_INFORMATION() - # T() only allocas, no memset: unset STARTUPINFOA fields are stack garbage, so concurrent - # threads get random dwFlags and CreateProcessA uses junk stdio handles. Clear si/sa/pi. - string.memset(&si, 0, win32process.STARTUPINFOA.__sizeof__()) - string.memset(&sa, 0, win32.win32base.SECURITY_ATTRIBUTES.__sizeof__()) - string.memset(&pi, 0, win32process.PROCESS_INFORMATION.__sizeof__()) - sa.nLength = win32.win32base.SECURITY_ATTRIBUTES.__sizeof__() - sa.lpSecurityDescriptor = t.CPtr(0) - sa.bInheritHandle = 1 - si.cb = win32process.STARTUPINFOA.__sizeof__() - # create_flags=0: the child inherits the parent console (output shown, like system); - # si.dwFlags stays 0 after memset (no STARTF_USESTDHANDLES, no std handles). - ok: int = win32process.CreateProcessA(t.CPtr(0), cmd_line, t.CPtr(0), t.CPtr(0), 1, 0, t.CPtr(0), t.CPtr(0), &si, &pi) - # CreateProcessA failure diagnostics: read GetLastError immediately (later calls overwrite - # it). Use warning, not error (error would sys.exit in a worker thread). - if ok == 0: - cp_err: uint32 = win32.win32base.GetLastError() - VLogger.warning(f"CreateProcessA failed (GetLastError={cp_err}) external command not started: cmd={cmd}", "LLC") - stdlib.free(cmd_line) - if ok == 0: return -1 - win32sync.WaitForSingleObject(pi.hProcess, win32.win32base.INFINITE) - exit_code: uint32 = 0 - win32process.GetExitCodeProcess(pi.hProcess, &exit_code) - win32.win32base.CloseHandle(pi.hProcess) - win32.win32base.CloseHandle(pi.hThread) - return int(exit_code) - - -def _llc_task_worker(arg: t.CPtr) -> t.CPtr: - """llc thread entry: only calls _run_cmd_wait, no pool allocation (thread-safe).""" - task: LlcTask* = (LlcTask*)(arg) - if task is None: return t.CPtr(0) - # MLIR backend: validate (mlir-opt) -> lower (mlir-translate), then ASan opt -> llc - if task.MlirOptCmd is not None: - mo_rc: int = _run_cmd_wait(task.MlirOptCmd) - if mo_rc != 0: - task.ExitCode = mo_rc - task.FailStage = LLC_STAGE_MLIR_OPT - task.Result = 1 - return t.CPtr(0) - if task.MlirTranslateCmd is not None: - mt_rc: int = _run_cmd_wait(task.MlirTranslateCmd) - if mt_rc != 0: - task.ExitCode = mt_rc - task.FailStage = LLC_STAGE_MLIR_TRANSLATE - task.Result = 1 - return t.CPtr(0) - if task.OptCmd is not None: - opt_rc: int = _run_cmd_wait(task.OptCmd) - if opt_rc != 0: - task.ExitCode = opt_rc - task.FailStage = LLC_STAGE_ASAN_OPT - task.Result = 1 - return t.CPtr(0) - llc_rc: int = _run_cmd_wait(task.LlcCmd) - task.ExitCode = llc_rc - if llc_rc != 0: - task.FailStage = LLC_STAGE_LLC - task.Result = 1 - else: - task.FailStage = LLC_STAGE_NONE - task.Result = 0 - return t.CPtr(0) - - -def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int: - """Run llc in parallel: batches of max_threads threads, join all then check results.""" - if tasks is None or task_count <= 0: return 0 - # --phase2command mode: do not spawn threads; write all opt (ASan) + llc commands to the bat - if _phase2_cmd_path is not None: - for pi2 in range(int(task_count)): - tsk_p: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(pi2) * LlcTask.__sizeof__()) - if tsk_p is None: continue - if tsk_p.MlirOptCmd is not None: _phase2_write_cmd(tsk_p.MlirOptCmd) - if tsk_p.MlirTranslateCmd is not None: _phase2_write_cmd(tsk_p.MlirTranslateCmd) - if tsk_p.OptCmd is not None: _phase2_write_cmd(tsk_p.OptCmd) - if tsk_p.LlcCmd is not None: _phase2_write_cmd(tsk_p.LlcCmd) - VLogger.info(f"phase2command mode: {task_count} llc commands written to {_phase2_cmd_path}", "phase2cmd") - return 0 - if max_threads <= 0: max_threads = 1 - # Thread-handle array: list[str] (str=bytes=i8*, explicit elem_size=8) stores handles; - # list[t.CPtr] append crashes reading page zero. Grab the handle right after start(). - handles: list[str]* = list[str](_mbuddy, 8) - if handles is None: - VLogger.error("run_llc_parallel: failed to allocate thread handle array", "PhaseB") - return 1 - slot_size: sizet = LlcTask.__sizeof__() - ret: int = 0 - # Batched execution: batch_start steps by max_threads (each batch spawn+join+close). - for batch_start in range(0, int(task_count), int(max_threads)): - batch_n: int = max_threads - if batch_start + batch_n > task_count: batch_n = task_count - batch_start - # spawn this batch - for j in range(int(batch_n)): - tsk: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size) - th: vthreading.Thread* = vthreading.Thread(_llc_task_worker, t.CPtr(tsk)) - th.start() - # Read the handle right after start() (the Thread object's stack slot may be - # reused; reading self.handle at join could see a cleared/overwritten value). - handles.append(t.CPtr(th.handle)) - for j in range(int(batch_n)): - h_j: t.CPtr = t.CPtr(handles.get(batch_start + j)) - wr: uint32 = win32sync.WaitForSingleObject(h_j, 600000) - if wr != 0: VLogger.error("llc thread join timeout/failure (not finished within 600 seconds)", "PhaseB") - win32.win32base.CloseHandle(h_j) - # check this batch's results - for j in range(int(batch_n)): - tsk2: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size) - if tsk2 is not None and tsk2.Result != 0: - # Failure: use FailStage to pinpoint the step and print that step's command. - # Previously everything printed the LlcCmd as "llc failed", misdirecting MLIR failures. - st: int = tsk2.FailStage - stage_name: str = "unknown stage (worker did not finish)" - stage_cmd: bytes = tsk2.LlcCmd - if st == LLC_STAGE_MLIR_OPT: - stage_name = "mlir-opt validation" - stage_cmd = tsk2.MlirOptCmd - elif st == LLC_STAGE_MLIR_TRANSLATE: - stage_name = "mlir-translate lowering" - stage_cmd = tsk2.MlirTranslateCmd - elif st == LLC_STAGE_ASAN_OPT: - stage_name = "opt ASan instrumentation" - stage_cmd = tsk2.OptCmd - elif st == LLC_STAGE_LLC: - stage_name = "llc compile" - stage_cmd = tsk2.LlcCmd - VLogger.error(f"{stage_name} failed: {(tsk2.ModuleSha1 if tsk2.ModuleSha1 is not None else "?")} (exit={tsk2.ExitCode} cmd={(stage_cmd if stage_cmd is not None else "?")})", "LLC") - ret = 1 - return ret - - -def _CollectObjFilesImpl(base_dir: str, bd_len: sizet, out_buf: bytes, out_pos: sizet, out_size: sizet) -> sizet: - """Recursively scan base_dir, collect .obj paths into out_buf, return the new out_pos.""" - find_data: win32.win32file.WIN32_FIND_DATAA* = _mbuddy.alloc(win32.win32file.WIN32_FIND_DATAA.__sizeof__()) - if find_data is None: - VLogger.warning("_CollectObjFilesImpl failed to allocate find_data, fewer .obj collected", "collect") - return out_pos - string.memset(find_data, 0, win32.win32file.WIN32_FIND_DATAA.__sizeof__()) - - pattern: bytes = _mbuddy.alloc(bd_len + 8) - if pattern is None: - VLogger.warning("_CollectObjFilesImpl failed to allocate pattern, fewer .obj collected", "collect") - return out_pos - viperlib.snprintf(pattern, bd_len + 8, "%s/*", base_dir) - - handle: win32.win32base.HANDLE = win32.win32file.FindFirstFileA(pattern, find_data) - _mbuddy.free(pattern) - if handle == win32.win32base.INVALID_HANDLE_VALUE: - VLogger.warning("_CollectObjFilesImpl cannot open directory, fewer .obj collected", "collect") - return out_pos - - while True: - fname: str = find_data.cFileName - if fname is not None: - fname_len: sizet = len(fname) - if fname_len > 0: - # skip . and .. - is_dot: int = 0 - if fname_len == 1 and fname[0] == '.': is_dot = 1 - elif fname_len == 2 and fname[0] == '.' and fname[1] == '.': is_dot = 1 - if is_dot == 0: - attrs: ULONG = find_data.dwFileAttributes - is_dir: int = 0 - if (attrs & win32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: is_dir = 1 - if is_dir != 0: - # recurse into the subdirectory - sub_dir: bytes = _mbuddy.alloc(bd_len + fname_len + 2) - if sub_dir is not None: - viperlib.snprintf(sub_dir, bd_len + fname_len + 2, "%s/%s", base_dir, fname) - sub_len: sizet = len(sub_dir) - out_pos = _CollectObjFilesImpl(sub_dir, sub_len, out_buf, out_pos, out_size) - _mbuddy.free(sub_dir) - else: - # check whether this is a .obj file - if fname_len > 4 and fname[fname_len - 4] == '.' and fname[fname_len - 3] == 'o' and fname[fname_len - 2] == 'b' and fname[fname_len - 1] == 'j': - need: sizet = bd_len + 1 + fname_len + 2 - if out_pos + need >= out_size: break - if out_pos > 0: - out_buf[out_pos] = ' ' - out_pos += 1 - viperlib.snprintf(out_buf + out_pos, need, "%s/%s", base_dir, fname) - out_pos += bd_len + 1 + fname_len - - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - - win32.win32file.FindClose(handle) - return out_pos - - -def collect_obj_files(includes_binary_dir: str, out_buf: bytes, out_size: sizet) -> sizet: - """Recursively scan includes_binary_dir, collect all .obj paths into out_buf; bytes written.""" - if includes_binary_dir is None or out_buf is None or out_size == 0: return 0 - - out_buf[0] = '\0' - dir_len: sizet = len(includes_binary_dir) - out_pos: sizet = _CollectObjFilesImpl(includes_binary_dir, dir_len, out_buf, 0, out_size) - out_buf[out_pos] = '\0' - return out_pos - - -def _append_host_export_implib(linker_flags: str, implib_path: str) -> str: - """Host-export mode: append --out-implib= when linker_flags has --export-all-symbols.""" - if linker_flags is None or implib_path is None: return linker_flags - if string.strstr(linker_flags, "--export-all-symbols") is None: return linker_flags - extra: sizet = string.strlen(" -Wl,--out-implib=") + string.strlen(implib_path) + 1 - buf: str = _mbuddy.alloc(string.strlen(linker_flags) + extra) - if buf is None: - VLogger.warning("host export: failed to allocate import lib argument buffer (fragments cannot import host symbols)", "link") - return linker_flags - viperlib.snprintf(buf, string.strlen(linker_flags) + extra, "%s -Wl,--out-implib=%s", linker_flags, implib_path) - VLogger.info(f"host export: produced import lib {implib_path}", "link") - return buf - - -def link_obj_to_exe(output_dir: str, module_name: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int: - """Link .obj files into an .exe via clang++; 0 on success, non-zero on failure.""" - if output_dir is None or linker_cmd is None or module_name is None: return 1 - - # build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj - obj_path: str = build_sliced_path(output_dir, module_name, "obj") - if obj_path is None: return 1 - - # Host-exe export (when linker.flags has --export-all-symbols) also emits an import lib - # {output_dir}/{linker_output}.dll.a; REPL fragments link it to import the host. - if linker_output is not None: - implib_len: sizet = string.strlen(output_dir) + string.strlen(linker_output) + 8 - implib_path: str = _mbuddy.alloc(implib_len) - if implib_path is not None: - viperlib.snprintf(implib_path, implib_len, "%s/%s.dll.a", output_dir, linker_output) - linker_flags = _append_host_export_implib(linker_flags, implib_path) - - # --traceback: link dbghelp (after the host-export rewrite, so both survive) - linker_flags = _append_traceback_dbghelp(linker_flags) - - # collect .obj paths from includes.binary - EXTRA_BUF_SIZE: sizet = 32768 - extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE) - if extra_objs is None: return 1 - extra_len: sizet = 0 - if includes_binary_dir is not None: - extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE) - if extra_len > 0: VLogger.info(f"appended {extra_len} bytes of includes.binary .obj files", "link") - else: VLogger.warning(f"includes.binary has no .obj files: {includes_binary_dir}", "link") - - # Command: clang++ {obj_path} extra_objs [libs] -o {...}/{linker_output} linker_flags. Objects - # and explicit libs must precede -l flags. The ASan runtime is declared in project.vpj, not hardcoded. - use_flags: str = linker_flags - static_libs: str = linker_static_libs - if static_libs is None: static_libs = "" - if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0: - # Merge static + dynamic into one space-separated string (Viper str += would emit - # invalid add i8* IR, so build it with snprintf). - merged_sl: str = _mbuddy.alloc(len(static_libs) + len(linker_dynamic_libs) + 2) - if merged_sl is not None: - if len(static_libs) > 0: - viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s %s", static_libs, linker_dynamic_libs) - else: - viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs) - static_libs = merged_sl - sl_len: sizet = len(static_libs) - cmd_len: sizet = len(linker_cmd) + len(obj_path) + len(use_flags) + len(output_dir) + len(linker_output) + extra_len + sl_len + 128 - cmd: bytes = _mbuddy.alloc(cmd_len) - if cmd is None: return 1 - if sl_len > 0 and extra_len > 0: - viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, static_libs, output_dir, linker_output, use_flags) - elif sl_len > 0: - viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, static_libs, output_dir, linker_output, use_flags) - elif extra_len > 0: - viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, output_dir, linker_output, use_flags) - else: - viperlib.snprintf(cmd, cmd_len, "%s %s -o %s/%s %s", linker_cmd, obj_path, output_dir, linker_output, use_flags) - - # --phase2command mode: do not link; write the command to the bat - if _phase2_cmd_path is not None: - _phase2_write_cmd(cmd) - return 0 - - result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) - if result is None: - VLogger.error("subprocess.run returned None", "link") - return 1 - if result.returncode != 0: - VLogger.error(f"link failed, return code: {result.returncode}", "link") - VLogger.error(f"command: {cmd}", "link") - # Show linker error output. - # Note: subprocess merges stderr into stdout on Windows, so result.stderr is always None. - if result.stdout is not None: VLogger.error(result.stdout, "link") - else: VLogger.error("no output captured", "link") - if result.stderr is not None: VLogger.error(result.stderr, "link") - return 1 - return 0 - - -def asm_to_obj(asm_path: str, sha1_val: str, output_dir: str, triple: str) -> int: - """Assemble a .s source into .obj with llvm-mc on the sliced path; 0 on success, non-zero on failure.""" - if asm_path is None or sha1_val is None or output_dir is None: return 1 - obj_path: str = build_sliced_path(output_dir, sha1_val, "obj") - if obj_path is None: return 1 - trip: str = triple - if trip is None or trip[0] == '\0': trip = "x86_64-none-elf" - cmd_len: sizet = len("llvm-mc -triple=") + len(trip) + len(" -filetype=obj -o ") + len(obj_path) + len(asm_path) + 64 - cmd: bytes = _mbuddy.alloc(cmd_len) - if cmd is None: return 1 - viperlib.snprintf(cmd, cmd_len, "llvm-mc -triple=%s -filetype=obj -o %s %s", trip, obj_path, asm_path) - - # --phase2command mode: write the command to the bat (same as llc/link) - if _phase2_cmd_path is not None: - _phase2_write_cmd(cmd) - return 0 - - result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) - if result is None: - VLogger.error("llvm-mc subprocess.run returned None", "asm") - return 1 - if result.returncode != 0: - if result.stdout is not None: stdio.printf("%s\n", result.stdout) - if result.stderr is not None: stdio.printf("%s\n", result.stderr) - VLogger.error(f"llvm-mc assembly failed, return code: {result.returncode}, command: {cmd}", "asm") - return 1 - return 0 - - -def compile_module_to_obj(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int: - """Compile IR to .obj without linking; 0 on success, non-zero on failure.""" - if ir_buf is None or temp_dir is None or output_dir is None or module_name is None: return 1 - - # Step 1: write the .ll file - ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name) - if ret != 0: - VLogger.error(f"failed to write .ll: {module_name}", "compile") - return 1 - - # Step 2: build the sliced .ll path and compile -> .obj - ir_path: str = build_sliced_path(temp_dir, module_name, "ll") - if ir_path is None: return 1 - - ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags) - if ret != 0: - VLogger.error(f"llc compilation failed: {module_name}", "compile") - return 1 - return 0 - - -def link_objs_to_exe(obj_paths: str, obj_paths_len: sizet, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int: - """Link multiple .obj files into an .exe; 0 on success, non-zero on failure.""" - if obj_paths is None or obj_paths_len == 0 or linker_cmd is None: return 1 - - # Dedup .obj paths: the same module's obj can be added by several collection stages, and - # a path appearing twice causes duplicate symbols. Keep the first occurrence, skip the rest. - if obj_paths_len > 0: - dedup: bytes = _mbuddy.alloc(obj_paths_len + 1) - if dedup is not None: - dedup[0] = '\0' - dpos: sizet = 0 - dsrc_i: sizet = 0 - while dsrc_i < obj_paths_len: - # skip leading spaces - while dsrc_i < obj_paths_len and obj_paths[dsrc_i] == ' ': dsrc_i += 1 - if dsrc_i >= obj_paths_len: break - dstart: sizet = dsrc_i - while dsrc_i < obj_paths_len and obj_paths[dsrc_i] != ' ': dsrc_i += 1 - dtlen: sizet = dsrc_i - dstart - # check whether dedup already has this token - ddup: int = 0 - dj: sizet = 0 - while dj < dpos: - while dj < dpos and dedup[dj] == ' ': dj += 1 - if dj >= dpos: break - ds2: sizet = dj - while dj < dpos and dedup[dj] != ' ': dj += 1 - dt2len: sizet = dj - ds2 - if dt2len == dtlen: - deq: int = 1 - for dk in range(int(dtlen)): - if dedup[ds2 + dk] != obj_paths[dstart + dk]: - deq = 0 - break - if deq != 0: - ddup = 1 - break - if ddup == 0: - if dpos > 0: - dedup[dpos] = ' ' - dpos += 1 - string.memcpy(dedup + dpos, obj_paths + dstart, dtlen) - dpos += dtlen - dedup[dpos] = '\0' - if dpos > 0 and dpos != obj_paths_len: - obj_paths = dedup - obj_paths_len = dpos - - # collect .obj paths from includes.binary - EXTRA_BUF_SIZE: sizet = 32768 - extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE) - if extra_objs is None: return 1 - extra_len: sizet = 0 - if includes_binary_dir is not None: extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE) - - # Command: clang++ obj_paths extra_objs -o linker_output linker_flags. The ASan runtime - # comes from project.vpj, not TPV. Host export: import lib is .dll.a. - if linker_output is not None: - implib_len2: sizet = string.strlen(linker_output) + 8 - implib_path2: str = _mbuddy.alloc(implib_len2) - if implib_path2 is not None: - viperlib.snprintf(implib_path2, implib_len2, "%s.dll.a", linker_output) - linker_flags = _append_host_export_implib(linker_flags, implib_path2) - # --traceback: link dbghelp (after the host-export rewrite) - linker_flags = _append_traceback_dbghelp(linker_flags) - use_flags: str = linker_flags - static_libs2: str = linker_static_libs - if static_libs2 is None: static_libs2 = "" - if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0: - # merge static + dynamic (Viper str += emits invalid IR; use snprintf) - merged_sl2: str = _mbuddy.alloc(len(static_libs2) + len(linker_dynamic_libs) + 2) - if merged_sl2 is not None: - if len(static_libs2) > 0: - viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s %s", static_libs2, linker_dynamic_libs) - else: - viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs) - static_libs2 = merged_sl2 - sl2_len: sizet = len(static_libs2) - cmd_len: sizet = len(linker_cmd) + obj_paths_len + len(use_flags) + len(linker_output) + extra_len + sl2_len + 128 - cmd: bytes = _mbuddy.alloc(cmd_len) - if cmd is None: return 1 - # linker.static_libs explicit static libs go after the .obj files and before -o - if sl2_len > 0 and extra_len > 0: - viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, static_libs2, linker_output, use_flags) - elif sl2_len > 0: - viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, static_libs2, linker_output, use_flags) - elif extra_len > 0: - viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, linker_output, use_flags) - else: - viperlib.snprintf(cmd, cmd_len, "%s %s -o %s %s", linker_cmd, obj_paths, linker_output, use_flags) - - # Capture clang++ stdout/stderr via subprocess.run (stdlib.system could not capture errors like - # undefined reference). --phase2command mode: do not link; write the command to the bat. - if _phase2_cmd_path is not None: - _phase2_write_cmd(cmd) - return 0 - result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) - if result is None: - VLogger.error("subprocess.run returned None", "link") - return 1 - if result.returncode != 0: - # First print clang++'s full error output (incl. undefined reference) with stdio.printf; - # it must precede VLogger.error, which calls sys.exit(1) and terminates the process. - if result.stdout is not None: stdio.printf("=== clang++ link error output ===\n%s\n", result.stdout) - if result.stderr is not None: - stdio.printf("=== clang++ stderr ===\n%s\n", result.stderr) - VLogger.error(f"link failed, return code: {result.returncode}, command: {cmd}", "link") - return 1 - return 0 - - -# --traceback: .stackmap sidecar writer (address -> source line). After a successful link, -# llvm-dwarfdump parses the PE's DWARF; addresses are normalised to RVAs and written out. - -# row record: u64 rva | u64 file_path | u32 line | u32 pad | u64 func_name (nullable). -# func_name is only carried by our .vpcall rows; DWARF rows write 0 and fall back to the subprogram table. -define TB_ROW_SIZE = 32 -define TB_ROW_OFF_FILE = 8 -define TB_ROW_OFF_LINE = 16 -define TB_ROW_OFF_FUNC = 24 -# func record: u64 low_rva | u64 high_rva | u64 name -define TB_FUNC_SIZE = 24 -# dir record: u32 idx | u32 pad | u64 name -define TB_DIR_SIZE = 16 -# file record: u32 idx | u32 dir_index | u64 name | u64 resolved_path -define TB_FILE_SIZE = 32 -# initial capacities for the (growable) row / func tables -define TB_ROW_CAP0 = 65536 -define TB_FUNC_CAP0 = 65536 -# fixed per-CU table capacities (exceeding either aborts the build) -define TB_DIR_CAP = 4096 -define TB_FILE_CAP = 65536 - - -class TbPeLayout: - Ok: int - ImageBase: uint64 - SizeOfImage: uint64 - TextRva: uint64 - - def __new__(self) -> t.CPtr: - return t.CPtr(_mbuddy.alloc(TbPeLayout.__sizeof__())) - - def __init__(self): - self.Ok = 0 - self.ImageBase = 0 - self.SizeOfImage = 0 - self.TextRva = 0 - - -class TbDump: - RowBuf: bytes - RowCap: int - RowCount: int - RawRowCount: int - FuncBuf: bytes - FuncCap: int - FuncCount: int - - def __new__(self) -> t.CPtr: - return t.CPtr(_mbuddy.alloc(TbDump.__sizeof__())) - - def __init__(self): - self.RowBuf = None - self.RowCap = 0 - self.RowCount = 0 - self.RawRowCount = 0 - self.FuncBuf = None - self.FuncCap = 0 - self.FuncCount = 0 - - -class TbPool: - Pool: bytes - PoolLen: int - Offs: bytes - OffCount: int - - def __new__(self) -> t.CPtr: - return t.CPtr(_mbuddy.alloc(TbPool.__sizeof__())) - - def __init__(self): - self.Pool = None - self.PoolLen = 0 - self.Offs = None - self.OffCount = 0 - - -# ---- little-endian byte helpers (no struct.pack in Viper) ---- -def _tb_u8(buf: bytes, off: sizet) -> uint32: - return uint32(int(buf[off]) & 0xFF) - - -def _tb_rd_u16(buf: bytes, off: sizet) -> uint32: - return _tb_u8(buf, off) | (_tb_u8(buf, off + 1) << 8) - - -def _tb_rd_u32(buf: bytes, off: sizet) -> uint32: - b0: uint32 = _tb_u8(buf, off) - b1: uint32 = _tb_u8(buf, off + 1) - b2: uint32 = _tb_u8(buf, off + 2) - b3: uint32 = _tb_u8(buf, off + 3) - return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) - - -def _tb_rd_u64(buf: bytes, off: sizet) -> uint64: - lo: uint64 = uint64(_tb_rd_u32(buf, off)) - hi: uint64 = uint64(_tb_rd_u32(buf, off + 4)) - return lo | (hi << 32) - - -def _tb_wr_u32(buf: bytes, off: sizet, v: uint64): - buf[off] = char(int(v & 0xFF)) - buf[off + 1] = char(int((v >> 8) & 0xFF)) - buf[off + 2] = char(int((v >> 16) & 0xFF)) - buf[off + 3] = char(int((v >> 24) & 0xFF)) - - -def _tb_wr_u64(buf: bytes, off: sizet, v: uint64): - _tb_wr_u32(buf, off, v & 0xFFFFFFFF) - _tb_wr_u32(buf, off + 4, (v >> 32) & 0xFFFFFFFF) - - -# ---- tiny string helpers (no re module) ---- -def _tb_cc(s: str, i: sizet) -> int: - """Byte value at s[i] as 0..255 (chars in DWARF text are ASCII).""" - if s is None: return 0 - return int(s[i]) & 0xFF - - -def _tb_contains(s: str, sub: str) -> int: - if s is None or sub is None: return 0 - return 1 if string.strstr(s, sub) is not None else 0 - - -def _tb_contains_span(s: str, ls: sizet, le: sizet, sub: str) -> int: - """Span-aware substring test: only s[ls, le) is searched. - A tail-wide strstr would both scan the whole remaining objdump text (hundreds of MB) and - match a substring belonging to a LATER line (e.g. ".text" found past a ".rdata" header).""" - if s is None or sub is None: return 0 - sl: sizet = string.strlen(sub) - if sl == 0 or le < ls or le - ls < sl: return 0 - i: sizet = ls - while i + sl <= le: - if string.strncmp(s + i, sub, sl) == 0: return 1 - i += 1 - return 0 - - -def _tb_startswith(s: str, pfx: str) -> int: - if s is None or pfx is None: return 0 - pl: sizet = string.strlen(pfx) - if string.strlen(s) < pl: return 0 - return 1 if string.strncmp(s, pfx, pl) == 0 else 0 - - -def _tb_startswith_span(s: str, ls: sizet, le: sizet, pfx: str) -> int: - """Span-aware prefix test: only s[ls, le) is considered, never the tail. - _tb_startswith on `s + ls` would run strlen over the WHOLE remaining text; llvm-objdump -d - output is hundreds of MB for a large exe, so a per-line strlen makes the whole scan O(n^2) - and the compilation appears to hang (it spins inside strlen for hours).""" - if s is None or pfx is None: return 0 - if le < ls: return 0 - pl: sizet = string.strlen(pfx) - if le - ls < pl: return 0 - return 1 if string.strncmp(s + ls, pfx, pl) == 0 else 0 - - -def _tb_ltrim(s: str) -> str: - if s is None: return None - i: sizet = 0 - while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1 - return s + i - - -def _tb_len_trimmed(s: str) -> sizet: - if s is None: return 0 - n: sizet = string.strlen(s) - while n > 0: - c: int = int(_tb_cc(s, n - 1)) - if c == 32 or c == 9 or c == 13: n -= 1 - else: break - return n - - -def _tb_dup_span(mb: memhub.MemBuddy*, src: str, n: sizet) -> str: - """Copy src[0..n) into a fresh NUL-terminated string.""" - if src is None: return None - out: str = mb.alloc(n + 1) - if out is None: return None - string.strncpy(out, src, n) - out[n] = '\0' - return out - - -def _tb_unquote(mb: memhub.MemBuddy*, v: str) -> str: - """Trim trailing spaces and strip one pair of surrounding quotes (TPC _Unquote).""" - if v is None: return None - n: sizet = _tb_len_trimmed(v) - if n >= 2 and _tb_cc(v, 0) == 34 and _tb_cc(v, n - 1) == 34: return _tb_dup_span(mb, v + 1, n - 2) - return _tb_dup_span(mb, v, n) - - -def _tb_is_abs_path(s: str) -> int: - if s is None: return 0 - c0: int = _tb_cc(s, 0) - if c0 == 0: return 0 - if c0 == 47 or c0 == 92: return 1 - if _tb_cc(s, 1) == 58: return 1 - return 0 - - -def _tb_join(mb: memhub.MemBuddy*, base: str, name: str) -> str: - """Join base and name with a separator unless base already ends in one.""" - if base is None or string.strlen(base) == 0: return name - if name is None: return base - bl: sizet = string.strlen(base) - nl: sizet = string.strlen(name) - buf: str = mb.alloc(bl + nl + 2) - if buf is None: return name - last: int = int(_tb_cc(base, bl - 1)) - if last == 47 or last == 92: viperlib.snprintf(buf, bl + nl + 2, "%s%s", base, name) - else: viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name) - return buf - - -def _tb_parse_dec(s: str) -> int: - """Parse a leading decimal integer, skipping leading blanks (dwarfdump right-aligns indices).""" - v: int = 0 - i: sizet = 0 - while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1 - while _tb_cc(s, i) >= 48 and _tb_cc(s, i) <= 57: - v = v * 10 + (_tb_cc(s, i) - 48) - i += 1 - return v - - -def _tb_bracket_idx(s: str) -> int: - """Parse the N in '[N]' (first bracket group of the line).""" - i: sizet = 0 - while _tb_cc(s, i) != 0: - if _tb_cc(s, i) == 91: return _tb_parse_dec(s + i + 1) - i += 1 - return -1 - - -def _tb_after_bracket(s: str) -> str: - i: sizet = 0 - while _tb_cc(s, i) != 0: - if _tb_cc(s, i) == 93: return s + i + 1 - i += 1 - return None - - -def _tb_value_after_sep(s: str) -> str: - """Return the text after the first '=' or ':' (left-trimmed), or None.""" - if s is None: return None - i: sizet = 0 - while _tb_cc(s, i) != 0: - c: int = _tb_cc(s, i) - if c == 61 or c == 58: return _tb_ltrim(s + i + 1) - i += 1 - return None - - -def _tb_hex_digit(ch: int) -> int: - if ch >= 48 and ch <= 57: return ch - 48 - if ch >= 97 and ch <= 102: return ch - 87 - if ch >= 65 and ch <= 70: return ch - 55 - return -1 - - -def _tb_hex_in_parens(s: str, key: str) -> uint64: - """Parse the 0x value inside the parentheses right after key; 0 when the form is absent.""" - p: str = string.strstr(s, key) - if p is None: return 0 - p = p + string.strlen(key) - while _tb_cc(p, 0) == 32 or _tb_cc(p, 0) == 9: p = p + 1 - if _tb_cc(p, 0) != 40: return 0 - p = p + 1 - while _tb_cc(p, 0) == 32: p = p + 1 - if _tb_cc(p, 0) != 48 or _tb_cc(p, 1) != 120: return 0 - q: str = p + 2 - v: uint64 = 0 - i: sizet = 0 - hd: int = _tb_hex_digit(_tb_cc(q, 0)) - while hd >= 0: - v = (v << 4) | uint64(hd) - i += 1 - hd = _tb_hex_digit(_tb_cc(q, i)) - return v - - -def _tb_quoted_after(mb: memhub.MemBuddy*, s: str, key: str) -> str: - """Extract the first double-quoted string following key (DW_AT_name ("...")).""" - p: str = string.strstr(s, key) - if p is None: return None - p = p + string.strlen(key) - q: str = string.strchr(p, 40) - if q is None: return None - q = q + 1 - while _tb_cc(q, 0) == 32: q = q + 1 - if _tb_cc(q, 0) != 34: return None - q = q + 1 - start: str = q - n: sizet = 0 - while _tb_cc(q, n) != 0: - c: int = _tb_cc(q, n) - if c == 92: - if _tb_cc(q, n + 1) == 0: break - n += 2 - continue - if c == 34: break - n += 1 - return _tb_dup_span(mb, start, n) - - -def _tb_dir_lookup(dirs: bytes, count: int, idx: int) -> str: - for i in range(int(count)): - if int(_tb_rd_u32(dirs, sizet(i) * TB_DIR_SIZE)) == idx: return rawptr(_tb_rd_u64(dirs, sizet(i) * TB_DIR_SIZE + 8)) - return None - - -def _tb_file_rec(files: bytes, count: int, idx: int) -> int: - for i in range(int(count)): - if int(_tb_rd_u32(files, sizet(i) * TB_FILE_SIZE)) == idx: return i - return -1 - - -def _tb_resolve_file(mb: memhub.MemBuddy*, files: bytes, fcount: int, dirs: bytes, dcount: int, frec: int) -> str: - """Resolve a file-table entry to a path (absolute name wins, else dir join).""" - roff: sizet = sizet(frec) * TB_FILE_SIZE - cached: uint64 = _tb_rd_u64(files, roff + 24) - if cached != 0: return rawptr(cached) - name: str = rawptr(_tb_rd_u64(files, roff + 8)) - path: str = name - if name is not None and _tb_is_abs_path(name) == 0: - didx: int = int(_tb_rd_u32(files, roff + 4)) - base: str = _tb_dir_lookup(dirs, dcount, didx) - if base is not None and string.strlen(base) > 0: - if _tb_is_abs_path(base) == 0: - root: str = _tb_dir_lookup(dirs, dcount, 0) - if root is not None and _tb_is_abs_path(root) != 0: base = _tb_join(mb, root, base) - path = _tb_join(mb, base, name) - if path is None or string.strlen(path) == 0: path = "??" - _tb_wr_u64(files, roff + 24, uint64(path)) - return path - - -def _tb_grow_rec(mb: memhub.MemBuddy*, buf: bytes, old_cap: int, rec_size: int, new_cap: int) -> bytes: - nb: bytes = mb.alloc(sizet(new_cap) * sizet(rec_size)) - if nb is None: return None - string.memcpy(nb, buf, sizet(old_cap) * sizet(rec_size)) - mb.free(buf) - return nb - - -def _tb_emit_row(mb: memhub.MemBuddy*, d: TbDump*, addr: uint64, path: str, line: int, fname: str = None) -> int: - if d is None: return 1 - if d.RowCount >= d.RowCap: - ncap: int = d.RowCap * 2 - nb: bytes = _tb_grow_rec(mb, d.RowBuf, d.RowCap, TB_ROW_SIZE, ncap) - if nb is None: - VLogger.error("--traceback: failed to grow DWARF line record table, aborting compilation", "TRACEBACK") - return 1 - d.RowBuf = nb - d.RowCap = ncap - off: sizet = sizet(d.RowCount) * TB_ROW_SIZE - _tb_wr_u64(d.RowBuf, off, addr) - _tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FILE, uint64(path)) - _tb_wr_u32(d.RowBuf, off + TB_ROW_OFF_LINE, uint64(line)) - fv: uint64 = 0 - if fname is not None: fv = uint64(fname) - _tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FUNC, fv) - d.RowCount = d.RowCount + 1 - d.RawRowCount = d.RawRowCount + 1 - return 0 - - -def _tb_emit_func(mb: memhub.MemBuddy*, d: TbDump*, low: uint64, high: uint64, name: str) -> int: - if d is None: return 1 - if d.FuncCount >= d.FuncCap: - ncap: int = d.FuncCap * 2 - nb: bytes = _tb_grow_rec(mb, d.FuncBuf, d.FuncCap, TB_FUNC_SIZE, ncap) - if nb is None: - VLogger.error("--traceback: failed to grow DWARF subprogram table, aborting compilation", "TRACEBACK") - return 1 - d.FuncBuf = nb - d.FuncCap = ncap - off: sizet = sizet(d.FuncCount) * TB_FUNC_SIZE - _tb_wr_u64(d.FuncBuf, off, low) - _tb_wr_u64(d.FuncBuf, off + 8, high) - _tb_wr_u64(d.FuncBuf, off + 16, uint64(name)) - d.FuncCount = d.FuncCount + 1 - return 0 - - -def _tb_func_name_for(fbuf: bytes, fcount: int, rva: uint64) -> str: - """bisect_right over the (sorted) func low_pc table, then range check.""" - if fbuf is None or fcount <= 0: return None - lo: int = 0 - hi: int = fcount - while lo < hi: - mid: int = lo + (hi - lo) / 2 - if _tb_rd_u64(fbuf, sizet(mid) * TB_FUNC_SIZE) <= rva: lo = mid + 1 - else: hi = mid - k: int = lo - 1 - if k < 0: return None - low: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE) - high: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 8) - if low <= rva and rva < high: return rawptr(_tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 16)) - return None - - -def _tb_intern(p: TbPool*, s: str) -> uint64: - """Intern s into the pool (linear scan; keeps offset 0 as the empty sentinel).""" - if p is None or s is None: return 0 - for k in range(int(p.OffCount)): - o: uint64 = _tb_rd_u64(p.Offs, sizet(k) * 8) - if string.strcmp(p.Pool + sizet(o), s) == 0: return o - off: uint64 = uint64(p.PoolLen) - n: sizet = string.strlen(s) - string.memcpy(p.Pool + sizet(off), s, n) - p.Pool[sizet(off) + n] = '\0' - p.PoolLen = p.PoolLen + int(n) + 1 - _tb_wr_u64(p.Offs, sizet(p.OffCount) * 8, off) - p.OffCount = p.OffCount + 1 - return off - - -def _tb_read_pe_layout(mb: memhub.MemBuddy*, exe_path: str) -> TbPeLayout*: - """Read (ImageBase, SizeOfImage, .text RVA) from the PE headers.""" - lay: TbPeLayout* = TbPeLayout() - if lay is None: return None - f: fileio.File* = fileio.File(exe_path, fileio.MODE.R) - if f.closed: return lay - HEAD: sizet = 4096 - head: bytes = mb.alloc(HEAD) - if head is None: - f.close() - return lay - got: LONG = f.read_all(head, HEAD) - f.close() - if got < 0x40: return lay - if _tb_cc(head, 0) != 77 or _tb_cc(head, 1) != 90: return lay - e_lfanew: uint64 = uint64(_tb_rd_u32(head, 0x3C)) - if e_lfanew + 24 > uint64(got): return lay - if _tb_cc(head, sizet(e_lfanew)) != 80 or _tb_cc(head, sizet(e_lfanew) + 1) != 69: return lay - coff: sizet = sizet(e_lfanew) + 4 - nsec: uint64 = uint64(_tb_rd_u16(head, coff + 2)) - optsz: uint64 = uint64(_tb_rd_u16(head, coff + 16)) - opt: sizet = coff + 20 - if uint64(opt) + 32 > uint64(got): return lay - magic: uint64 = uint64(_tb_rd_u16(head, opt)) - img_base: uint64 = 0 - if magic == 0x20B: img_base = _tb_rd_u64(head, opt + 24) - elif magic == 0x10B: img_base = uint64(_tb_rd_u32(head, opt + 28)) - else: return lay - size_img: uint64 = uint64(_tb_rd_u32(head, opt + 56)) - sec_off: sizet = opt + sizet(optsz) - text_rva: uint64 = 0x1000 - min_rva: uint64 = 0xFFFFFFFF - found_text: int = 0 - have_sec: int = 0 - for si in range(int(nsec)): - base: sizet = sec_off + sizet(si) * 40 - if uint64(base) + 40 > uint64(got): break - va: uint64 = uint64(_tb_rd_u32(head, base + 12)) - have_sec = 1 - if va < min_rva: min_rva = va - if _tb_cc(head, base) == 46 and _tb_cc(head, base + 1) == 116 and _tb_cc(head, base + 2) == 101 and _tb_cc(head, base + 3) == 120 and _tb_cc(head, base + 4) == 116: - text_rva = va - found_text = 1 - if found_text == 0 and have_sec != 0 and min_rva != 0xFFFFFFFF: text_rva = min_rva - lay.ImageBase = img_base - lay.SizeOfImage = size_img - lay.TextRva = text_rva - lay.Ok = 1 - return lay - - -def _tb_parse_dump(mb: memhub.MemBuddy*, dump: str) -> TbDump*: - """Parse llvm-dwarfdump text into a row table (addr, path, line) and a func table.""" - d: TbDump* = TbDump() - if d is None: return None - LINE_CAP: sizet = 8192 - d.RowBuf = mb.alloc(sizet(TB_ROW_CAP0) * TB_ROW_SIZE) - d.RowCap = TB_ROW_CAP0 - d.FuncBuf = mb.alloc(sizet(TB_FUNC_CAP0) * TB_FUNC_SIZE) - d.FuncCap = TB_FUNC_CAP0 - dirs: bytes = mb.alloc(sizet(TB_DIR_CAP) * TB_DIR_SIZE) - files: bytes = mb.alloc(sizet(TB_FILE_CAP) * TB_FILE_SIZE) - linebuf: bytes = mb.alloc(LINE_CAP) - if d.RowBuf is None or d.FuncBuf is None or dirs is None or files is None or linebuf is None: - VLogger.error("--traceback: failed to allocate dwarfdump parse buffer, aborting compilation", "TRACEBACK") - return None - - in_line: int = 0 - dir_count: int = 0 - file_count: int = 0 - pkind: int = 0 # 0=none, 1=file, 2=dir - pidx: int = 0 - pname: str = None - pdir: int = 0 - in_prog: int = 0 - cur_low: uint64 = 0 - cur_high: uint64 = 0 - cur_has_low: int = 0 - cur_has_high: int = 0 - cname: str = None - - dlen: sizet = string.strlen(dump) - pos: sizet = 0 - while pos < dlen: - ls: sizet = pos - while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1 - le: sizet = pos - if pos < dlen: pos += 1 - if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1 - ll: sizet = le - ls - if ll == 0: continue - if ll >= LINE_CAP: ll = LINE_CAP - 1 - string.strncpy(linebuf, dump + ls, ll) - linebuf[ll] = '\0' - st: str = _tb_ltrim(linebuf) - - # ---- line-table section gate ---- - if _tb_contains(st, "debug_line contents") != 0: - in_line = 1 - continue - if _tb_contains(st, "contents:") != 0 and _tb_contains(st, "debug_") != 0: - in_line = 0 - continue - - if in_line != 0: - if _tb_startswith(st, "debug_line[") != 0: - # new CU: commit pending file, reset both tables - if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) - file_count += 1 - pkind = 0 - pname = None - pdir = 0 - dir_count = 0 - file_count = 0 - continue - if _tb_startswith(st, "file_names[") != 0: - if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) - file_count += 1 - pname = None - pdir = 0 - pkind = 1 - pidx = _tb_bracket_idx(st) - continue - if _tb_startswith(st, "directory_names[") != 0 or _tb_startswith(st, "include_directories[") != 0: - if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) - file_count += 1 - pname = None - pdir = 0 - pidx = _tb_bracket_idx(st) - val: str = _tb_value_after_sep(_tb_after_bracket(st)) - if val is not None and _tb_len_trimmed(val) > 0: - if dir_count < TB_DIR_CAP: - _tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx)) - _tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(_tb_unquote(mb, val))) - dir_count += 1 - pkind = 0 - else: pkind = 2 - continue - if _tb_startswith(st, "name:") != 0 and pkind != 0: - val2: str = _tb_unquote(mb, _tb_ltrim(st + 5)) - if pkind == 1: pname = val2 - else: - if dir_count < TB_DIR_CAP and val2 is not None: - _tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx)) - _tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(val2)) - dir_count += 1 - pkind = 0 - continue - if _tb_startswith(st, "dir_index:") != 0 and pkind == 1: - # "dir_index:" is 10 chars with the colon; st + 9 would point at the colon and - # always parse 0 (losing dir indices). Use st + 10. - pdir = _tb_parse_dec(_tb_ltrim(st + 10)) - continue - if _tb_startswith(st, "Address") != 0 and _tb_contains(st, "Column") != 0: - if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) - file_count += 1 - pkind = 0 - pname = None - pdir = 0 - continue - if _tb_cc(linebuf, 0) == 48 and _tb_cc(linebuf, 1) == 120: - if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) - _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) - _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) - file_count += 1 - pkind = 0 - pname = None - pdir = 0 - ri: sizet = 2 - addr: uint64 = 0 - hd: int = _tb_hex_digit(_tb_cc(linebuf, ri)) - while hd >= 0: - addr = (addr << 4) | uint64(hd) - ri += 1 - hd = _tb_hex_digit(_tb_cc(linebuf, ri)) - while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1 - lno: int = 0 - while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: - lno = lno * 10 + (_tb_cc(linebuf, ri) - 48) - ri += 1 - while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1 - while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: ri += 1 - while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1 - fidx: int = 0 - while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: - fidx = fidx * 10 + (_tb_cc(linebuf, ri) - 48) - ri += 1 - if lno > 0: - frec: int = _tb_file_rec(files, file_count, fidx) - fpath: str = None - if frec >= 0: fpath = _tb_resolve_file(mb, files, file_count, dirs, dir_count, frec) - if fpath is None: fpath = "??" - if _tb_emit_row(mb, d, addr, fpath, lno) != 0: return None - continue - - # ---- DWARF debug_info subprogram parsing ---- - if _tb_contains(linebuf, "DW_TAG_") != 0: - if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0: - hi: uint64 = cur_low + 1 - if cur_has_high != 0: - hi = cur_high - if hi <= cur_low: hi = cur_low + cur_high - if hi > cur_low: - if _tb_emit_func(mb, d, cur_low, hi, cname) != 0: return None - in_prog = 0 - cur_low = 0 - cur_high = 0 - cur_has_low = 0 - cur_has_high = 0 - cname = None - if _tb_contains(linebuf, "DW_TAG_subprogram") != 0: in_prog = 1 - continue - if in_prog == 0: continue - if _tb_contains(linebuf, "DW_AT_low_pc") != 0: - cur_low = _tb_hex_in_parens(linebuf, "DW_AT_low_pc") - cur_has_low = 1 - continue - if _tb_contains(linebuf, "DW_AT_high_pc") != 0: - cur_high = _tb_hex_in_parens(linebuf, "DW_AT_high_pc") - cur_has_high = 1 - continue - if _tb_contains(linebuf, "DW_AT_name") != 0 and cname is None: - cname = _tb_quoted_after(mb, linebuf, "DW_AT_name") - continue - - # trailing subprogram - if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0: - hi2: uint64 = cur_low + 1 - if cur_has_high != 0: - hi2 = cur_high - if hi2 <= cur_low: hi2 = cur_low + cur_high - if hi2 > cur_low: - if _tb_emit_func(mb, d, cur_low, hi2, cname) != 0: return None - return d - - -# --traceback call-site normalisation: .vpcall + llvm-objdump. The MLIR backend emits no DWARF -# for our .vp functions, so each direct callq's return address is mapped back to file/line/function. - -# call record: u64 caller | u64 callee | u64 file | u32 line | u32 next (next in group, NONE=tail) -define TB_CR_SIZE = 32 -define TB_CR_OFF_CALLER = 0 -define TB_CR_OFF_CALLEE = 8 -define TB_CR_OFF_FILE = 16 -define TB_CR_OFF_LINE = 24 -define TB_CR_OFF_NEXT = 28 -# function record: u64 sym | u64 file | u32 defline | u32 pad -define TB_FR_SIZE = 24 -define TB_FR_OFF_SYM = 0 -define TB_FR_OFF_FILE = 8 -define TB_FR_OFF_DEFLINE = 16 -# group hash slots (keyed by (caller,callee); open addressing + linear probing): -# u64 caller | u64 callee | u32 used | u32 head | u32 tail | u32 cursor -define TB_GRP_SIZE = 32 -define TB_GRP_OFF_CALLER = 0 -define TB_GRP_OFF_CALLEE = 8 -define TB_GRP_OFF_USED = 16 -define TB_GRP_OFF_HEAD = 20 -define TB_GRP_OFF_TAIL = 24 -define TB_GRP_OFF_CURSOR = 28 -# invalid-index sentinel (all-ones u32) -TB_IDX_NONE: uint64 = 0xFFFFFFFF - - -class TbCallParse: - Cr: bytes # array of TB_CR_SIZE records - CrCap: int - CrN: int - Fr: bytes # array of TB_FR_SIZE records - FrCap: int - FrN: int - Grp: bytes # array of TB_GRP_SIZE slots - GrpCap: int - - def __new__(self) -> t.CPtr: - return t.CPtr(_mbuddy.alloc(TbCallParse.__sizeof__())) - - def __init__(self): - self.Cr = None - self.CrCap = 0 - self.CrN = 0 - self.Fr = None - self.FrCap = 0 - self.FrN = 0 - self.Grp = None - self.GrpCap = 0 - - -def _tb_next_pow2(n: int) -> int: - p: int = 16 - while p < n: p = p * 2 - return p - - -def _tb_strhash(s: str) -> uint64: - """FNV-1a hash (32-bit offset basis, 64-bit prime) used only for table addressing.""" - h: uint64 = 0x811C9DC5 - if s is None: return h - i: sizet = 0 - while _tb_cc(s, i) != 0: - h = (h ^ uint64(_tb_cc(s, i))) * 0x100000001B3 - i += 1 - return h - - -def _tb_grp_get(grp: bytes, cap: int, caller: str, callee: str, do_insert: int) -> int: - """Look up a slot by (caller, callee); insert on miss when do_insert=1; slot or -1.""" - if grp is None or cap <= 0 or caller is None or callee is None: return -1 - mask: uint64 = uint64(cap - 1) - hv: uint64 = (_tb_strhash(caller) * 0x100000001B3 + _tb_strhash(callee)) & mask - for i in range(int(cap)): - slot: int = int((hv + uint64(i)) & mask) - off: sizet = sizet(slot) * TB_GRP_SIZE - if uint64(_tb_rd_u32(grp, off + TB_GRP_OFF_USED)) == 0: - if do_insert == 0: return -1 - _tb_wr_u32(grp, off + TB_GRP_OFF_USED, uint64(1)) - _tb_wr_u64(grp, off + TB_GRP_OFF_CALLER, uint64(caller)) - _tb_wr_u64(grp, off + TB_GRP_OFF_CALLEE, uint64(callee)) - _tb_wr_u32(grp, off + TB_GRP_OFF_HEAD, uint64(TB_IDX_NONE)) - _tb_wr_u32(grp, off + TB_GRP_OFF_TAIL, uint64(TB_IDX_NONE)) - _tb_wr_u32(grp, off + TB_GRP_OFF_CURSOR, uint64(TB_IDX_NONE)) - return slot - sc: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLER)) - sk: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLEE)) - if sc is not None and sk is not None and string.strcmp(sc, caller) == 0 and string.strcmp(sk, callee) == 0: return slot - return -1 - - -def _tb_cp_init(mb: memhub.MemBuddy*) -> TbCallParse*: - cp: TbCallParse* = TbCallParse() - if cp is None: return None - cp.CrCap = 1024 - cp.FrCap = 1024 - cp.Cr = mb.alloc(sizet(cp.CrCap) * TB_CR_SIZE) - cp.Fr = mb.alloc(sizet(cp.FrCap) * TB_FR_SIZE) - if cp.Cr is None or cp.Fr is None: return None - cp.CrN = 0 - cp.FrN = 0 - return cp - - -def _tb_cp_add_cr(mb: memhub.MemBuddy*, cp: TbCallParse*, caller: str, callee: str, file: str, line: int) -> int: - if cp.CrN >= cp.CrCap: - ncap: int = cp.CrCap * 2 - nb: bytes = _tb_grow_rec(mb, cp.Cr, cp.CrCap, TB_CR_SIZE, ncap) - if nb is None: - VLogger.error("--traceback: failed to grow call record table, aborting compilation", "TRACEBACK") - return 1 - cp.Cr = nb - cp.CrCap = ncap - off: sizet = sizet(cp.CrN) * TB_CR_SIZE - _tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLER, uint64(caller)) - _tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLEE, uint64(callee)) - _tb_wr_u64(cp.Cr, off + TB_CR_OFF_FILE, uint64(file)) - _tb_wr_u32(cp.Cr, off + TB_CR_OFF_LINE, uint64(line)) - _tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE)) - cp.CrN = cp.CrN + 1 - return 0 - - -def _tb_cp_add_fr(mb: memhub.MemBuddy*, cp: TbCallParse*, sym: str, file: str, defline: int) -> int: - if cp.FrN >= cp.FrCap: - ncap: int = cp.FrCap * 2 - nb: bytes = _tb_grow_rec(mb, cp.Fr, cp.FrCap, TB_FR_SIZE, ncap) - if nb is None: - VLogger.error("--traceback: failed to grow function record table, aborting compilation", "TRACEBACK") - return 1 - cp.Fr = nb - cp.FrCap = ncap - off: sizet = sizet(cp.FrN) * TB_FR_SIZE - _tb_wr_u64(cp.Fr, off + TB_FR_OFF_SYM, uint64(sym)) - _tb_wr_u64(cp.Fr, off + TB_FR_OFF_FILE, uint64(file)) - _tb_wr_u32(cp.Fr, off + TB_FR_OFF_DEFLINE, uint64(defline)) - cp.FrN = cp.FrN + 1 - return 0 - - -def _tb_read_whole(mb: memhub.MemBuddy*, path: str) -> str: - """Read the whole file into a NUL-terminated string; None on failure.""" - f: fileio.File* = fileio.File(path, fileio.MODE.R) - if f.closed: return None - sz: LONGLONG = f.size() - if sz < 0: - f.close() - return None - buf: str = mb.alloc(sizet(sz) + 2) - if buf is None: - f.close() - return None - # 容量全程用 sizet(与 sz 同宽):旧写法 ULONG(sz) + ULONG(2) 会在 sz >= 4GiB 时静默回绕 - got: LONG = f.read_all(buf, sizet(sz) + 2) - f.close() - if got < 0: return None - return buf - - -def _tb_parse_f_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int: - """Parse an F record line with symbol, defline and file fields (tab-separated).""" - p: sizet = a - while p < b and _tb_cc(s, p) != 9: p += 1 - if p >= b: return 0 - p += 1 - f2: sizet = p - while f2 < b and _tb_cc(s, f2) != 9: f2 += 1 - if f2 >= b: return 0 - sym: str = _tb_dup_span(mb, s + p, f2 - p) - p = f2 + 1 - f3: sizet = p - while f3 < b and _tb_cc(s, f3) != 9: f3 += 1 - if f3 >= b: return 0 - defline: int = int(_tb_parse_dec(s + p)) - p = f3 + 1 - file: str = _tb_dup_span(mb, s + p, b - p) - if sym is None or file is None: return 0 - return _tb_cp_add_fr(mb, cp, sym, file, defline) - - -def _tb_parse_c_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int: - """Parse a C record line with caller, callee, line and file fields (tab-separated).""" - p: sizet = a - while p < b and _tb_cc(s, p) != 9: p += 1 - if p >= b: return 0 - p += 1 - e2: sizet = p - while e2 < b and _tb_cc(s, e2) != 9: e2 += 1 - if e2 >= b: return 0 - caller: str = _tb_dup_span(mb, s + p, e2 - p) - p = e2 + 1 - e3: sizet = p - while e3 < b and _tb_cc(s, e3) != 9: e3 += 1 - if e3 >= b: return 0 - callee: str = _tb_dup_span(mb, s + p, e3 - p) - p = e3 + 1 - e4: sizet = p - while e4 < b and _tb_cc(s, e4) != 9: e4 += 1 - if e4 >= b: return 0 - line: int = int(_tb_parse_dec(s + p)) - p = e4 + 1 - file: str = _tb_dup_span(mb, s + p, b - p) - if caller is None or callee is None or file is None: return 0 - return _tb_cp_add_cr(mb, cp, caller, callee, file, line) - - -def _tb_parse_vpcall_file(mb: memhub.MemBuddy*, path: str, cp: TbCallParse*) -> int: - whole: str = _tb_read_whole(mb, path) - if whole is None: - VLogger.error(f"--traceback: failed to read .vpcall: {path}, aborting compilation", "TRACEBACK") - return 1 - n: sizet = string.strlen(whole) - pos: sizet = 0 - while pos < n: - ls: sizet = pos - while pos < n and _tb_cc(whole, pos) != 10: pos += 1 - le: sizet = pos - if pos < n: pos += 1 - if le > ls and _tb_cc(whole, le - 1) == 13: le -= 1 - if le > ls: - typ: int = _tb_cc(whole, ls) - if typ == 70: - if _tb_parse_f_line(mb, whole, ls, le, cp) != 0: return 1 - elif typ == 67: - if _tb_parse_c_line(mb, whole, ls, le, cp) != 0: return 1 - return 0 - - -def _tb_load_vpcalls(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int: - if _tb_vpcall_buf is None or _tb_vpcall_n <= 0: return 0 - for k in range(int(_tb_vpcall_n)): - slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(k) * 8) - path: str = slot[0] - if path is not None: - if _tb_parse_vpcall_file(mb, path, cp) != 0: return 1 - return 0 - - -def _tb_build_groups(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int: - """Chain all C records into group lists keyed by (caller, callee) (head/tail/cursor).""" - if cp.CrN <= 0: return 0 - cp.GrpCap = _tb_next_pow2(cp.CrN * 2 + 16) - cp.Grp = mb.alloc(sizet(cp.GrpCap) * TB_GRP_SIZE) - if cp.Grp is None: - VLogger.error("--traceback: failed to allocate call-site group table, aborting compilation", "TRACEBACK") - return 1 - string.memset(cp.Grp, 0, sizet(cp.GrpCap) * TB_GRP_SIZE) - for i in range(int(cp.CrN)): - off: sizet = sizet(i) * TB_CR_SIZE - caller: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLER)) - callee: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLEE)) - slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 1) - if slot < 0: - VLogger.error("--traceback: call-site group hash table overflow, aborting compilation", "TRACEBACK") - return 1 - goff: sizet = sizet(slot) * TB_GRP_SIZE - tailv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_TAIL)) - if tailv == uint64(TB_IDX_NONE): - _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_HEAD, uint64(i)) - _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, uint64(i)) - else: - toff: sizet = sizet(int(tailv)) * TB_CR_SIZE - _tb_wr_u32(cp.Cr, toff + TB_CR_OFF_NEXT, uint64(i)) - _tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE)) - _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_TAIL, uint64(i)) - return 0 - - -def _tb_emit_call_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, caller: str, callee: str, ret_va: uint64) -> int: - """Emit a row for a return address from the next unconsumed C record of (caller, callee).""" - if caller is None or callee is None or cp.Grp is None or ret_va < img_lo: return 0 - slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 0) - if slot < 0: return 0 - goff: sizet = sizet(slot) * TB_GRP_SIZE - curv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR)) - if curv == uint64(TB_IDX_NONE): return 0 - ci: int = int(curv) - coff: sizet = sizet(ci) * TB_CR_SIZE - file: str = rawptr(_tb_rd_u64(cp.Cr, coff + TB_CR_OFF_FILE)) - line: int = int(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_LINE)) - nxt: uint64 = uint64(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_NEXT)) - _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, nxt) - return _tb_emit_row(mb, d, ret_va, file, line, caller) - - -def _tb_emit_entry_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, addr: uint64, sym: str) -> int: - """Emit a fallback function-entry row for an objdump label sym (file/line from the F record).""" - if sym is None or cp.Fr is None or cp.FrN <= 0 or addr < img_lo: return 0 - for i in range(int(cp.FrN)): - off: sizet = sizet(i) * TB_FR_SIZE - s: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_SYM)) - if s is not None and string.strcmp(s, sym) == 0: - file: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_FILE)) - dl: int = int(_tb_rd_u32(cp.Fr, off + TB_FR_OFF_DEFLINE)) - return _tb_emit_row(mb, d, addr, file, dl, sym) - return 0 - - -def _tb_hex_span(s: str, a: sizet, b: sizet) -> uint64: - v: uint64 = 0 - i: sizet = a - while i < b: - hd: int = _tb_hex_digit(_tb_cc(s, i)) - if hd < 0: break - v = (v << 4) | uint64(hd) - i += 1 - return v - - -def _tb_run_objdump(mb: memhub.MemBuddy*, exe_path: str) -> str: - cl: sizet = string.strlen(exe_path) + 64 - cmd: bytes = mb.alloc(cl) - if cmd is None: - VLogger.error("--traceback: failed to allocate objdump command buffer, aborting compilation", "TRACEBACK") - return None - viperlib.snprintf(cmd, cl, "llvm-objdump -d \"%s\"", exe_path) - res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) - if res is None or res.returncode != 0: - rc: int = -1 - if res is not None: rc = res.returncode - VLogger.error(f"--traceback: failed to run llvm-objdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK") - return None - dump: str = res.stdout - if dump is None: - VLogger.error("--traceback: llvm-objdump produced no output, aborting compilation", "TRACEBACK") - return None - return dump - - -def _tb_scan_objdump(mb: memhub.MemBuddy*, dump: str, img_lo: uint64, d: TbDump*, cp: TbCallParse*) -> int: - """Parse llvm-objdump -d text, match C/F records and write our rows into d.""" - dlen: sizet = string.strlen(dump) - pos: sizet = 0 - in_text: int = 0 - cur_func: str = None - prev_is_call: int = 0 - prev_callee: str = None - prev_func: str = None - while pos < dlen: - ls: sizet = pos - while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1 - le: sizet = pos - if pos < dlen: pos += 1 - if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1 - if le <= ls: continue - if _tb_startswith_span(dump, ls, le, "Disassembly of section") != 0: - # section change: a pending callq at the previous section's end has no valid return address - prev_is_call = 0 - if _tb_contains_span(dump, ls, le, ".text") != 0: in_text = 1 - else: in_text = 0 - continue - if in_text == 0: continue - c0: int = _tb_cc(dump, ls) - is_hex0: int = 0 - if (c0 >= 48 and c0 <= 57) or (c0 >= 97 and c0 <= 102) or (c0 >= 65 and c0 <= 70): is_hex0 = 1 - if is_hex0 == 0: continue - # first space - sp: sizet = ls - while sp < le and _tb_cc(dump, sp) != 32: sp += 1 - if sp + 1 < le and _tb_cc(dump, sp + 1) == 60: - # ---- label line `ADDR :` ---- - laddr: uint64 = _tb_hex_span(dump, ls, sp) - if prev_is_call != 0: - _tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, laddr) - prev_is_call = 0 - lb: sizet = sp + 2 - j: sizet = lb - while j < le and _tb_cc(dump, j) != 62: j += 1 - cur_func = _tb_dup_span(mb, dump + lb, j - lb) - if cur_func is not None: _tb_emit_entry_row(mb, d, cp, img_lo, laddr, cur_func) - continue - # ---- instruction line `ADDR: bytes mnemonic operands` ---- - k: sizet = ls - while k < le and _tb_cc(dump, k) != 58: k += 1 - if k >= le: continue - iaddr: uint64 = _tb_hex_span(dump, ls, k) - if prev_is_call != 0: - _tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, iaddr) - prev_is_call = 0 - # skip byte tokens (exactly 2 hex chars) and locate the mnemonic start - p: sizet = k + 1 - while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1 - ms: sizet = p - while p < le: - ts: sizet = p - while p < le and _tb_cc(dump, p) != 32 and _tb_cc(dump, p) != 9: p += 1 - tl: sizet = p - ts - ishex: int = 0 - if tl == 2 and _tb_hex_digit(_tb_cc(dump, ts)) >= 0 and _tb_hex_digit(_tb_cc(dump, ts + 1)) >= 0: ishex = 1 - ms = ts - if ishex == 0: break - while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1 - me: sizet = p - mlen: sizet = me - ms - is_call: int = 0 - if mlen == 5 and string.strncmp(dump + ms, "callq", 5) == 0: is_call = 1 - elif mlen == 4 and string.strncmp(dump + ms, "call", 4) == 0: is_call = 1 - if is_call != 0: - os: sizet = me - while os < le and (_tb_cc(dump, os) == 32 or _tb_cc(dump, os) == 9): os += 1 - if os + 1 < le and _tb_cc(dump, os) == 48 and _tb_cc(dump, os + 1) == 120: - # direct call: extract `` - ab: sizet = os - while ab < le and _tb_cc(dump, ab) != 60: ab += 1 - if ab < le: - cb: sizet = ab + 1 - ce: sizet = cb - while ce < le and _tb_cc(dump, ce) != 62: ce += 1 - xe: sizet = cb - while xe < ce and _tb_cc(dump, xe) != 43: xe += 1 - prev_callee = _tb_dup_span(mb, dump + cb, xe - cb) - prev_func = cur_func - prev_is_call = 1 - return 0 - - -def WriteStackMap(mb: memhub.MemBuddy*, exe_path: str) -> int: - """--traceback: export .stackmap after a successful link. 0 = ok.""" - if mb is None or exe_path is None: - VLogger.error("--traceback: WriteStackMap argument is null, aborting compilation", "TRACEBACK") - return 1 - - lay: TbPeLayout* = _tb_read_pe_layout(mb, exe_path) - if lay is None or lay.Ok == 0: - VLogger.error(f"--traceback: only Windows PE output is supported, but {exe_path} is not a valid PE (required by --traceback)", "TRACEBACK") - return 1 - size_of_image: uint64 = lay.SizeOfImage - if size_of_image == 0: - VLogger.error(f"--traceback: read PE.SizeOfImage is not positive ({size_of_image}), cannot bound RVA range (required by --traceback)", "TRACEBACK") - return 1 - img_lo: uint64 = lay.ImageBase - img_hi: uint64 = lay.ImageBase + size_of_image - text_rva: uint64 = lay.TextRva - - # ---- 1) run llvm-dwarfdump (bare name: CreateProcessA resolves it via PATH) ---- - cmd_len: sizet = string.strlen(exe_path) + 64 - cmd: bytes = mb.alloc(cmd_len) - if cmd is None: - VLogger.error("--traceback: failed to allocate dwarfdump command buffer, aborting compilation", "TRACEBACK") - return 1 - viperlib.snprintf(cmd, cmd_len, "llvm-dwarfdump --debug-line --debug-info \"%s\"", exe_path) - res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) - if res is None or res.returncode != 0: - rc: int = -1 - if res is not None: rc = res.returncode - VLogger.error(f"--traceback: failed to run llvm-dwarfdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK") - return 1 - dump: str = res.stdout - if dump is None: - VLogger.error("--traceback: llvm-dwarfdump produced no output, aborting compilation", "TRACEBACK") - return 1 - - d: TbDump* = _tb_parse_dump(mb, dump) - if d is None: return 1 - - # ---- 1b) our call-site rows via .vpcall + llvm-objdump ---- - # DWARF rows only cover the linked mingw CRT; .vpcall + objdump add return-address -> call-site rows. - if _tb_vpcall_n > 0: - cp: TbCallParse* = _tb_cp_init(mb) - if cp is None: - VLogger.error("--traceback: failed to allocate call-site parse state, aborting compilation", "TRACEBACK") - return 1 - if _tb_load_vpcalls(mb, cp) != 0: return 1 - if _tb_build_groups(mb, cp) != 0: return 1 - odump: str = _tb_run_objdump(mb, exe_path) - if odump is None: return 1 - our_before: int = d.RowCount - if _tb_scan_objdump(mb, odump, img_lo, d, cp) != 0: return 1 - our_added: int = d.RowCount - our_before - if our_added > 0: VLogger.success(f"--traceback: from {_tb_vpcall_n} .vpcall files generated {our_added} own call-site records (F={cp.FrN} functions, C={cp.CrN} calls)", "TRACEBACK") - else: VLogger.warning(f"--traceback: read {_tb_vpcall_n} .vpcall files (F={cp.FrN}, C={cp.CrN}) but matched no objdump call sites -- own frame line numbers still missing, falling back to DWARF", "TRACEBACK") - else: VLogger.warning("--traceback: no .vpcall registered (own modules have no call-site records), falling back to DWARF behavior", "TRACEBACK") - - if d.RawRowCount == 0: - VLogger.error(f"--traceback: could not parse any line records from {exe_path} (DWARF .debug_line is empty and there are no own .vpcall records; required by --traceback)", "TRACEBACK") - return 1 - - # ---- 2) rows: normalise to RVA, same rva keeps the largest line ---- - n: int = d.RowCount - dropped_rows: int = 0 - kept: int = 0 - i: int = 0 - while i < n: - a: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE) - if a < img_lo or a >= img_hi: - dropped_rows += 1 - i += 1 - continue - if kept != i: - _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo) - _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FILE, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)) - _tb_wr_u32(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_LINE, uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE))) - _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)) - else: _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo) - kept += 1 - i += 1 - tmp: bytes = mb.alloc(TB_ROW_SIZE) - if tmp is None: - VLogger.error("--traceback: failed to allocate line record sort buffer, aborting compilation", "TRACEBACK") - return 1 - si2: int = 1 - while si2 < kept: - string.memcpy(tmp, d.RowBuf + sizet(si2) * TB_ROW_SIZE, TB_ROW_SIZE) - sj: int = si2 - while sj > 0: - if _tb_rd_u64(d.RowBuf, sizet(sj - 1) * TB_ROW_SIZE) <= _tb_rd_u64(tmp, 0): break - string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, d.RowBuf + sizet(sj - 1) * TB_ROW_SIZE, TB_ROW_SIZE) - sj -= 1 - string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, tmp, TB_ROW_SIZE) - si2 += 1 - uniq: int = 0 - i = 0 - while i < kept: - rv: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE) - bl: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE)) - bp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE) - bf: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) - j: int = int(i + 1) - while j < kept and _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE) == rv: - lj: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_LINE)) - if lj > bl: - bl = lj - bp = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FILE) - bf = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) - j += 1 - if uniq != i: - _tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE, rv) - _tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FILE, bp) - _tb_wr_u32(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_LINE, bl) - _tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, bf) - uniq += 1 - i = j - if dropped_rows > 0: VLogger.warning(f"--traceback: dropped {dropped_rows} DWARF line records that cannot be normalised (address not in image VA range, mostly COMDAT dedup copies)", "TRACEBACK") - if uniq == 0: - VLogger.error(f"--traceback: all line records from {exe_path} cannot be normalised to runtime RVA range, aborting compilation", "TRACEBACK") - return 1 - - # ---- 3) subprograms: normalise + sort by low_rva ---- - fcount: int = int(d.FuncCount) - fkept: int = 0 - dropped_funcs: int = 0 - fi: int = 0 - while fi < fcount: - low: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE) - if low < img_lo or low >= img_hi: - dropped_funcs += 1 - fi += 1 - continue - high: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 8) - lo_rva: uint64 = low - img_lo - hi_rva: uint64 = high - img_lo - if hi_rva <= lo_rva: hi_rva = lo_rva + 1 - if hi_rva > size_of_image: hi_rva = size_of_image - _tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE, lo_rva) - _tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 8, hi_rva) - _tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 16, _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 16)) - fkept += 1 - fi += 1 - ftmp: bytes = mb.alloc(TB_FUNC_SIZE) - if ftmp is None: - VLogger.error("--traceback: failed to allocate subprogram sort buffer, aborting compilation", "TRACEBACK") - return 1 - fj: int = 1 - while fj < fkept: - string.memcpy(ftmp, d.FuncBuf + sizet(fj) * TB_FUNC_SIZE, TB_FUNC_SIZE) - fk: int = fj - while fk > 0: - if _tb_rd_u64(d.FuncBuf, sizet(fk - 1) * TB_FUNC_SIZE) <= _tb_rd_u64(ftmp, 0): break - string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, d.FuncBuf + sizet(fk - 1) * TB_FUNC_SIZE, TB_FUNC_SIZE) - fk -= 1 - string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, ftmp, TB_FUNC_SIZE) - fj += 1 - if dropped_funcs > 0: VLogger.warning(f"--traceback: dropped {dropped_funcs} DWARF subprograms that cannot be normalised (low_pc not in image VA range)", "TRACEBACK") - - # ---- 4) string pool: exact upper bound, then intern (offset 0 = sentinel) ---- - pool_need: sizet = 1 - i = 0 - while i < uniq: - pth: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)) - pool_need += string.strlen(pth) + 1 - i += 1 - # our rows carry the function name directly (string pool); DWARF rows take it from the subprogram table. - i = 0 - while i < uniq: - fp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) - if fp != 0: - fns: str = rawptr(fp) - if fns is not None: pool_need += string.strlen(fns) + 1 - i += 1 - i = 0 - while i < fkept: - nm0: str = rawptr(_tb_rd_u64(d.FuncBuf, sizet(i) * TB_FUNC_SIZE + 16)) - if nm0 is not None: pool_need += string.strlen(nm0) + 1 - i += 1 - p: TbPool* = TbPool() - if p is None: - VLogger.error("--traceback: failed to allocate string pool object, aborting compilation", "TRACEBACK") - return 1 - p.Pool = mb.alloc(pool_need) - p.Offs = mb.alloc((sizet(uniq) * 2 + sizet(fkept)) * 8 + 64) - if p.Pool is None or p.Offs is None: - VLogger.error("--traceback: failed to allocate string pool buffer, aborting compilation", "TRACEBACK") - return 1 - p.Pool[0] = '\0' - p.PoolLen = 1 - _tb_wr_u64(p.Offs, 0, uint64(0)) - p.OffCount = 1 - # file_off / func_off per unique row (u32 each) - ro: bytes = mb.alloc(sizet(uniq) * 8 + 8) - if ro is None: - VLogger.error("--traceback: failed to allocate record offset buffer, aborting compilation", "TRACEBACK") - return 1 - for i in range(int(uniq)): - pth2: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)) - foff: uint64 = _tb_intern(p, pth2) - noff: uint64 = 0 - rv2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE) - fp2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) - nm2: str = None - if fp2 != 0: nm2 = rawptr(fp2) - else: nm2 = _tb_func_name_for(d.FuncBuf, fkept, rv2) - if nm2 is not None: noff = _tb_intern(p, nm2) - _tb_wr_u32(ro, sizet(i) * 8, foff) - _tb_wr_u32(ro, sizet(i) * 8 + 4, noff) - - # ---- 5) self-checks (mirror the runtime LoadMapFile validation) ---- - prev_rva: uint64 = 0 - outside: int = 0 - plausible: int = 0 - ci: int = 0 - while ci < uniq: - cv: uint64 = _tb_rd_u64(d.RowBuf, sizet(ci) * TB_ROW_SIZE) - if ci > 0 and cv <= prev_rva: - VLogger.error(f"--traceback self-check failed: record RVAs not strictly ascending (record {ci} would break runtime binary search), aborting compilation", "TRACEBACK") - return 1 - prev_rva = cv - if cv >= size_of_image: outside += 1 - if cv >= text_rva and cv < size_of_image: plausible += 1 - ci += 1 - if outside > 0: - VLogger.error(f"--traceback self-check failed: {outside} records have RVA outside [0, SizeOfImage=0x{size_of_image:x}), aborting compilation", "TRACEBACK") - return 1 - if plausible <= 0: - VLogger.error("--traceback self-check failed: no record RVA falls in the range possible at runtime (RVA basis likely wrong), aborting compilation", "TRACEBACK") - return 1 - # file_off must be >= 1: offset 0 is the empty-string sentinel and the runtime - # rejects a whole map whose record file_off is 0. - for ci in range(int(uniq)): - fo: uint64 = uint64(_tb_rd_u32(ro, sizet(ci) * 8)) - if fo == 0: - VLogger.error("--traceback self-check failed: a record has file_off==0 (empty path; runtime would reject that sidecar file), aborting compilation", "TRACEBACK") - return 1 - - # ---- 6) serialise and write .stackmap ---- - total: sizet = 20 + sizet(uniq) * 20 + sizet(p.PoolLen) - out: bytes = mb.alloc(total) - if out is None: - VLogger.error("--traceback: failed to allocate sidecar output buffer, aborting compilation", "TRACEBACK") - return 1 - out[0] = 'T' - out[1] = 'P' - out[2] = 'C' - out[3] = 'S' - _tb_wr_u32(out, 4, uint64(1)) - _tb_wr_u32(out, 8, uint64(uniq)) - _tb_wr_u32(out, 12, uint64(p.OffCount)) - _tb_wr_u32(out, 16, uint64(p.PoolLen)) - for ri2 in range(int(uniq)): - roff2: sizet = 20 + sizet(ri2) * 20 - rv3: uint64 = _tb_rd_u64(d.RowBuf, sizet(ri2) * TB_ROW_SIZE) - ln3: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(ri2) * TB_ROW_SIZE + TB_ROW_OFF_LINE)) - _tb_wr_u64(out, roff2, rv3) - _tb_wr_u32(out, roff2 + 8, uint64(_tb_rd_u32(ro, sizet(ri2) * 8))) - _tb_wr_u32(out, roff2 + 12, ln3) - _tb_wr_u32(out, roff2 + 16, uint64(_tb_rd_u32(ro, sizet(ri2) * 8 + 4))) - string.memcpy(out + 20 + sizet(uniq) * 20, p.Pool, sizet(p.PoolLen)) - - map_len: sizet = string.strlen(exe_path) + 16 - map_path: str = mb.alloc(map_len) - if map_path is None: - VLogger.error("--traceback: failed to allocate sidecar path buffer, aborting compilation", "TRACEBACK") - return 1 - viperlib.snprintf(map_path, map_len, "%s.stackmap", exe_path) - mf: fileio.File* = fileio.File(map_path, fileio.MODE.W) - if mf.closed: - VLogger.error(f"--traceback: failed to write sidecar file (cannot open {map_path}), aborting compilation", "TRACEBACK") - return 1 - mf.write(out, total) - mf.close() - VLogger.success(f"--traceback: wrote address-to-source-line sidecar {map_path} ({uniq} records, {total} bytes)", "TRACEBACK") - return 0 - - -def run_pipeline(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> BuildResult*: - """Run the full compile pipeline; returns a BuildResult object.""" - result: BuildResult* = BuildResult() - if result is None: return None - - # Step 0: ensure temp/output dirs exist (auto-create to avoid write failures) - ensure_dir(temp_dir) - ensure_dir(output_dir) - - # Step 1: write the .ll file - ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name) - if ret != 0: - result.Success = 0 - result.ErrorMsg = "failed to write .ll file" - return result - - # Step 2: llc-compile .ll -> .obj. - # build the sliced .ll file path - ir_path: str = build_sliced_path(temp_dir, module_name, "ll") - if ir_path is None: - result.Success = 0 - result.ErrorMsg = "memory allocation failed" - return result - - ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags) - if ret != 0: - result.Success = 0 - result.ErrorMsg = "llc compilation failed" - return result - - # Step 3: clang++ link .obj -> .exe (plus includes.binary prebuilt .obj and static/dynamic libs) - ret = link_obj_to_exe(output_dir, module_name, linker_cmd, linker_flags, linker_output, includes_binary_dir, linker_static_libs, linker_dynamic_libs) - if ret != 0: - result.Success = 0 - result.ErrorMsg = "link failed" - return result - - result.Success = 1 - return result - - -# ============================================================================= -# Runtime dependency deployment (project.vpj runtime.*) -# After a successful link, copy the declared runtime deps next to the exe. -# Optional: no-op unless runtime.qt6_path / qt6_dlls / qt6_plugins are declared. -# Fail-fast: a declared source that is missing, or a failed copy/dir-create, aborts the build. -# ============================================================================= - -def _deploy_dir_of(path: str) -> str: - """Return the directory portion of path (text before the last / or backslash); None if no separator.""" - if path is None: return None - plen: sizet = string.strlen(path) - i: sizet = plen - while i > 0: - if path[i - 1] == '/' or path[i - 1] == '\\': break - i -= 1 - if i == 0: return None - buf: str = _mbuddy.alloc(i) - if buf is None: return None - string.strncpy(buf, path, i - 1) - buf[i - 1] = '\0' - return buf - - -def _deploy_join(base: str, name: str) -> str: - """Join base and name with '/'; mbuddy-allocated.""" - if base is None or name is None: return None - bl: sizet = string.strlen(base) - nl: sizet = string.strlen(name) - buf: str = _mbuddy.alloc(bl + nl + 2) - if buf is None: return None - viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name) - return buf - - -def _deploy_token(s: str, start: sizet, end: sizet) -> str: - """Copy s[start:end] into an mbuddy-allocated NUL-terminated string; None on OOM.""" - n: sizet = end - start - buf: str = _mbuddy.alloc(n + 1) - if buf is None: return None - string.strncpy(buf, s + start, n) - buf[n] = '\0' - return buf - - -def deploy_runtime_deps(exe_path: str) -> int: - """Deploy declared runtime dependencies (project.vpj runtime.*) next to the exe; 0=ok, 1=fail. - No-op when runtime.qt6_path / qt6_dlls / qt6_plugins are undeclared. Fail-fast otherwise.""" - if exe_path is None: return 0 - qt6_path: str = Config.Qt6Path - if qt6_path is None: return 0 - dlls: str = Config.Qt6Dlls - plugins: str = Config.Qt6Plugins - if dlls is None and plugins is None: return 0 - - exe_dir: str = _deploy_dir_of(exe_path) - if exe_dir is None: - VLogger.error(f"runtime deploy: cannot determine exe directory from {exe_path}", "runtime") - return 1 - bin_dir: str = _deploy_join(qt6_path, "bin") - plug_root: str = _deploy_join(qt6_path, "plugins") - - # runtime.qt6_dlls: /bin/ -> / - if dlls is not None: - total: sizet = string.strlen(dlls) - i: sizet = 0 - while i < total: - while i < total and dlls[i] == ' ': i += 1 - if i >= total: break - start: sizet = i - while i < total and dlls[i] != ' ': i += 1 - if i > start: - name: str = _deploy_token(dlls, start, i) - if name is None: - VLogger.error("runtime deploy: DLL name allocation failed (OOM)", "runtime") - return 1 - src: str = _deploy_join(bin_dir, name) - dst: str = _deploy_join(exe_dir, name) - if not oslib.isfile(src): - VLogger.error(f"runtime deploy: declared DLL not found: {src}", "runtime") - return 1 - if shutil.copyfile(src, dst) != 0: - VLogger.error(f"runtime deploy: failed to copy {src} -> {dst}", "runtime") - return 1 - VLogger.info(f"runtime deploy: {name}", "runtime") - - # runtime.qt6_plugins: /plugins/ -> / (ensure parent dir) - if plugins is not None: - total_p: sizet = string.strlen(plugins) - ip: sizet = 0 - while ip < total_p: - while ip < total_p and plugins[ip] == ' ': ip += 1 - if ip >= total_p: break - start_p: sizet = ip - while ip < total_p and plugins[ip] != ' ': ip += 1 - if ip > start_p: - rel: str = _deploy_token(plugins, start_p, ip) - if rel is None: - VLogger.error("runtime deploy: plugin path allocation failed (OOM)", "runtime") - return 1 - src_p: str = _deploy_join(plug_root, rel) - dst_p: str = _deploy_join(exe_dir, rel) - parent: str = _deploy_dir_of(dst_p) - if parent is not None: - if not oslib.isdir(parent): - if oslib.mkdir(parent) != 0: - VLogger.error(f"runtime deploy: failed to create dir {parent}", "runtime") - return 1 - if not oslib.isfile(src_p): - VLogger.error(f"runtime deploy: declared plugin not found: {src_p}", "runtime") - return 1 - if shutil.copyfile(src_p, dst_p) != 0: - VLogger.error(f"runtime deploy: failed to copy {src_p} -> {dst_p}", "runtime") - return 1 - VLogger.info(f"runtime deploy: {rel}", "runtime") - +import lib.core.Gen.GenApi as GenApi +import t, c +from stdint import * +import stdio +import string +import stdlib +import memhub +import viperlib +import vthreading +import win32.fileio as fileio +import win32.win32file +import win32.win32base +import win32.win32process as win32process +import win32.win32sync as win32sync +import subprocess +import viperlib +import ast +import os as oslib +import shutil +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HandlesTranslator +import lib.core.Gen.DebugInfoGen as DebugInfoGen +import lib.Stub.StubMerger as StubMerger +import lib.Stub.StubGen.Converter as StubConverter +import lib.Projectrans.Config as Config +import lib.core.Gen.Generator as GenGenerator +import lib.core.Gen.Expr.ExprGenCls as ExprGenCls +import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect + +# BuildPipeline - compile pipeline: write .ll to temp, run llc (.ll -> .obj), +# then link with clang++ (.obj -> .exe). + +# global mbuddy pointer +_mbuddy: memhub.MemBuddy* + +# ASan mode (--asan): instrument .ll via opt -passes=asan; the ASan runtime is +# declared explicitly in project.vpj's linker.static_libs / dynamic_libs (VPC injects nothing). +_asan_mode: bool = 0 + +# Traceback mode (--traceback): force-build includes/traceback.vp, inject the startup +# Install() ctor, keep frame pointers, link dbghelp, then export .stackmap. +_traceback_mode: bool = 0 + +# --phase2command xxx.bat: do not run llc/link; write the llc (incl. ASan opt) and clang +# link commands to the given .bat path for manual execution. None = run normally (default). +_phase2_cmd_path: str = None +# whether the bat header (@echo off) has been written; set to 1 on first write +_phase2_bat_init: int = 0 + +# MLIR lowering step (fixed, no backend switch): mlirlite emits MLIR but opt/llc accept +# only LLVM IR, so run mlir-opt (validate) then mlir-translate --mlir-to-llvmir first. +MLIR_OPT_CMD: str = "mlir-opt" +MLIR_TRANSLATE_CMD: str = "mlir-translate" + + +def _ir_sibling_path(ir_path: str, suffix: str) -> str: + """Derive a sibling path from <...>.ll: strip the .ll suffix and append suffix; mbuddy-allocated.""" + if ir_path is None or suffix is None: return None + n: sizet = string.strlen(ir_path) + if n < 3: return None + suf_len: sizet = string.strlen(suffix) + buf: str = _mbuddy.alloc(n + suf_len + 1) + if buf is None: return None + string.strncpy(buf, ir_path, n - 3) + buf[n - 3] = '\0' + string.strcat(buf, suffix) + return buf + +# source buffer size (1MB) +define SRC_BUF_SIZE = 1048576 # 2^20 = 1MB + +# .vpi stub buffer size (256KB, same as Phase1). Use a compile-time define: a module-level +# global with this name would emit a second strong symbol and llvm-link reports multiply defined. +define PYI_BUF_SIZE = 262144 + +# _llc_frame_ptr_flag - llc extra flag for --traceback: " -frame-pointer=all" while +# enabled (leading space), "" otherwise so the llc command stays byte-identical. +def _llc_frame_ptr_flag() -> str: + if _traceback_mode != 0: return " -frame-pointer=all" + return "" + + +# _append_traceback_dbghelp - append -ldbghelp for --traceback (StackWalk64 etc. live in +# dbghelp); appended so libraries follow the objects, else the link has undefined symbols. +def _append_traceback_dbghelp(linker_flags: str) -> str: + if _traceback_mode == 0 or linker_flags is None: return linker_flags + need: sizet = string.strlen(linker_flags) + 16 + buf: str = _mbuddy.alloc(need) + if buf is None: + VLogger.error("--traceback: failed to allocate -ldbghelp linker argument buffer, aborting compilation", "TRACEBACK") + return linker_flags + viperlib.snprintf(buf, need, "%s -ldbghelp", linker_flags) + return buf + + +# --traceback call-site sidecar (.vpcall): the compiler accumulates F/C records per +# module; WriteCallSidecar writes and registers them, WriteStackMap reads them after link. +define TB_VPCALL_MAX = 8192 +_tb_vpcall_buf: bytes = None # TB_VPCALL_MAX * 8: one char* per slot (stdlib-allocated, one-shot per process) +_tb_vpcall_n: int = 0 + + +def _tb_register_vpcall(path: str) -> int: + """Register a written .vpcall path in the global table for WriteStackMap to read.""" + global _tb_vpcall_buf, _tb_vpcall_n + if path is None: return 0 + if _tb_vpcall_buf is None: + _tb_vpcall_buf = stdlib.malloc(sizet(TB_VPCALL_MAX) * 8) + if _tb_vpcall_buf is None: + VLogger.error("--traceback: failed to allocate .vpcall path table, aborting compilation", "TRACEBACK") + return 1 + _tb_vpcall_n = 0 + if _tb_vpcall_n >= TB_VPCALL_MAX: + VLogger.error("--traceback: .vpcall path table overflow (module count exceeds limit), aborting compilation", "TRACEBACK") + return 1 + slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(_tb_vpcall_n) * 8) + slot[0] = path + _tb_vpcall_n += 1 + return 0 + + +def WriteCallSidecar(temp_dir: str, sha1: str, text: str) -> int: + """--traceback: write this module's call-site text to //.vpcall; None text skips.""" + if temp_dir is None or sha1 is None or text is None: return 0 + vpath: str = StubMergerCollect._sliced_path(temp_dir, string.strlen(temp_dir), sha1, "vpcall") + if vpath is None: + VLogger.error(f"--traceback: failed to build .vpcall path (sha1={sha1}), aborting compilation", "TRACEBACK") + return 1 + vf: fileio.File* = fileio.File(vpath, fileio.MODE.W) + if vf.closed: + VLogger.error(f"--traceback: failed to write .vpcall (cannot open {vpath}), aborting compilation", "TRACEBACK") + stdlib.free(vpath) + return 1 + vf.write(text, string.strlen(text)) + vf.close() + return _tb_register_vpcall(vpath) + + +# _tb_basename_is_traceback - whether this source file is includes/traceback.vp +def _tb_basename_is_traceback(file_path: str) -> int: + if file_path is None: return 0 + flen: sizet = string.strlen(file_path) + if flen < 12: return 0 + if flen > 12: + prev: char = file_path[flen - 13] + if prev != '/' and prev != '\\': return 0 + return 1 if string.strcmp(file_path + (flen - 12), "traceback.vp") == 0 else 0 + +# TranslateFileGetTrans - translate one file and return a Translator (caller dumps IR). +# File -> AST -> .vpi -> LLVM IR; includes use loose scoping, user files strict (local + imports). +def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str, sha1_val: str, current_package: str = None, declare_only: int = 0, temp_dir: str = None, pre_allow_dep_inject: int = 0) -> HandlesTranslator.Translator*: + """Translate one file and return a Translator (caller dumps IR); None on failure.""" + if file_path is None: return None + f: fileio.File* = fileio.File(file_path, fileio.MODE.R) + if f.closed: return None + # read_all's capacity includes the terminator (reads cap-1 bytes max) and silently + # truncates longer files; reject by size first, else parsing gets confusing syntax errors. + if f.size() >= SRC_BUF_SIZE: + VLogger.error(f"source file exceeds {SRC_BUF_SIZE} byte limit, refusing to read: {file_path}", "fileio") + f.close() + return None + src_buf: bytes = stdlib.malloc(SRC_BUF_SIZE) + if src_buf is None: + f.close() + return None + bytes_read: LONG = f.read_all(src_buf, SRC_BUF_SIZE) + f.close() + if bytes_read <= 0: + stdlib.free(src_buf) + return None + if bytes_read < SRC_BUF_SIZE: src_buf[bytes_read] = 0 + lx: ast.Lexer* = ast.new_lexer(mb) + ast._lexer_init(lx, src_buf, mb) + tokens: ast.Token* = ast.tokenize(lx) + tree: ast.AST* = ast.parse_tokens(mb, tokens) + # AST is independent after parse; free the source buffer (avoid a 1MB/file leak) + stdlib.free(src_buf) + if tree is None: + # fail-fast location: print a Python-style parse-error traceback (like fatal_error): + # Traceback (most recent call last) / File "", line N / SyntaxError: + GenApi.dump_parse_traceback(file_path) + err_line: int = ast.get_parse_err_line() + err_col: int = ast.get_parse_err_col() + VLogger.error(f"parse failed: {file_path} (line {err_line} col {err_col})", "PARSE") + return None + # Generate the .vpi stub from the AST before tr.translate (same as Phase1); it lets + # other modules query function signatures and CTypedef definitions. + if temp_dir is not None: + pyi_buf: bytes = stdlib.malloc(PYI_BUF_SIZE) + if pyi_buf is not None: + pyi_buf[0] = 0 + # Pass file_path as rel_path: vpi-time fatal_error must blame this source file, + # and the absolute path lets _read_source_line open and print the source line. + pyi_pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, file_path, pyi_buf, PYI_BUF_SIZE) + if pyi_pos > 0: + # write {sha1}.vpi into temp_dir (sliced path) + td_len_vpi: sizet = len(temp_dir) + pyi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpi, sha1_val, "vpi") + if pyi_path is not None: + pf: fileio.File* = fileio.File(pyi_path, fileio.MODE.W) + if not pf.closed: + pf.write(pyi_buf, pyi_pos) + pf.close() + stdlib.free(pyi_path) + stdlib.free(pyi_buf) + tr: HandlesTranslator.Translator* = mb.alloc(HandlesTranslator.Translator.__sizeof__()) + tr.__before_init__() + tr.__init__() + tr.ModuleSha1 = sha1_val + tr.CurrentPackage = current_package + tr._declare_only = declare_only + # App pre-registration (Phase A-pre) switch: when 1, import handling + dependency vpi + # injection also run in the declare phase. includes pre-registration must pass 0. + tr._pre_allow_dep_inject = pre_allow_dep_inject + # P2: reuse the global gen singleton (created by App/main.vp); create it as a fallback + # when unregistered (defensive for direct-call paths). + gen: GenGenerator.Generator* = GenGenerator._g_gen + if gen is None: + gen = mb.alloc(GenGenerator.Generator.__sizeof__()) + if gen is None: + VLogger.error("Generator alloc failed", "GEN") + return None + gen.__before_init__() + gen.__init__(mb) + GenGenerator._g_gen = gen + # P2-P5: inject the Handles module-level singletons (delegated functions use local + # _g_gen to avoid package/class name ambiguity from in-body imports). + GenApi._g_gen = gen + # P6: inject singletons for statement/assign/var/import/translate-driver modules + # set the current file name (for error reporting) + GenApi.set_current_file_type(file_path) + # --traceback: mirror the switch into the Gen-layer DWARF emitter. 0 => every DI entry + # point is a no-op and the IR stays byte-identical to the pre-DWARF output. + DebugInfoGen.SetEnabled(_traceback_mode) + # also set it for HandlesImports (import SHA1 checks need it; a circular dep blocks reading HandlesType) + GenApi.set_current_file_imports(file_path) + # clear the call stack (reset before each file translation) + GenApi.clear_trace() + # module switch: clear the CDefine constant table to isolate each module's constants + GenApi.clear_cdefine_constants() + # module switch: clear the CTypedef table to isolate each module's typedefs + GenApi.clear_ctypedefs() + # namespace isolation: includes loose (all visible), user files strict (local + imports only) + strict_mode: int = 1 + if string.strstr(file_path, "includes") is not None: strict_mode = 0 + GenApi.reset_visible_structs(mb, strict_mode) + # Extract docstrings before tr.translate (pre-registration registers cross-module __doc__; + # must run before translate recycles the AST pool blocks, which crashes under ASan). + if gen is not None: gen.ExprGenCls.extract_module_docstrings(tree, sha1_val) + ret: int = tr.translate(tree) + if ret != 0: + VLogger.error(f"file translation failed: {file_path}", "Translate") + return None + # --traceback: for includes/traceback.vp, inject the startup Install() ctor into its IR. + # The injector lives in the Gen layer, so no new cross-module mlirlite dependency. + if _traceback_mode != 0 and _tb_basename_is_traceback(file_path) != 0: HandlesTranslator.InjectTracebackInstallCtor(tr) + return tr + + +class BuildResult: + Success: int + OutputPath: str + ErrorMsg: str + + def __new__(self) -> t.CPtr: + return t.CPtr(_mbuddy.alloc(BuildResult.__sizeof__())) + + def __init__(self): + self.Success = 0 + self.OutputPath = None + self.ErrorMsg = None + + +def ensure_dir(path: str) -> int: + """Recursively create a directory (like mkdir -p); an existing directory counts as success.""" + if path is None: return 1 + + path_len: sizet = len(path) + if path_len == 0: return 0 + + # Hot path (PROF: ensure_dir was 4.5%): if the final dir exists, return success early, + # saving an alloc + N+1 CreateDirectoryA calls. Semantics unchanged (exists == success). + attr: ULONG = win32.win32file.GetFileAttributesA(path) + if attr != 0xFFFFFFFF and (attr & win32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 0 + + # copy the path into a writable buffer (for stepwise truncation) + buf: bytes = _mbuddy.alloc(path_len + 1) + if buf is None: return 1 + string.strcpy(buf, path) + + # Temporarily truncate at each separator and create every directory level. + # CreateDirectoryA returns 0 when the dir exists (failure); ignore it. + plen: int = int(path_len) + for i in range(int(plen)): + ch: int = buf[i] + if ch == 47 or ch == 92: # '/' = 47, '\\' = 92 + saved: char = char(ch) + buf[i] = '\0' + win32.win32file.CreateDirectoryA(buf, None) + buf[i] = saved + + # create the final directory + win32.win32file.CreateDirectoryA(buf, None) + return 0 + + +# build_sliced_path - build a sliced path and ensure its directory exists. Fans out into +# SHA1-prefix subdirs per Config.Sha1SliceLevel; returns the full path (mbuddy) or None. +def build_sliced_path(base_dir: str, sha1: str, ext: str) -> str: + """Build a sliced path and ensure its directory exists.""" + if base_dir is None or sha1 is None or ext is None: return None + + subdir: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel) + base_len: sizet = len(base_dir) + sha1_len: sizet = len(sha1) + ext_len: sizet = len(ext) + + if subdir is not None: + sub_len: sizet = len(subdir) + # ensure_dir({base_dir}/{subdir}) + dir_path: str = _mbuddy.alloc(base_len + sub_len + 2) + if dir_path is not None: + viperlib.snprintf(dir_path, base_len + sub_len + 2, "%s/%s", base_dir, subdir) + ensure_dir(dir_path) + # file path: {base_dir}/{subdir}/{sha1}.{ext} + path_len: sizet = base_len + sub_len + sha1_len + ext_len + 4 + path: str = _mbuddy.alloc(path_len) + if path is None: return None + viperlib.snprintf(path, path_len, "%s/%s/%s.%s", base_dir, subdir, sha1, ext) + return path + else: + # no slicing: {base_dir}/{sha1}.{ext} + path_len = base_len + sha1_len + ext_len + 3 + path = _mbuddy.alloc(path_len) + if path is None: return None + viperlib.snprintf(path, path_len, "%s/%s.%s", base_dir, sha1, ext) + return path + + +def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name: str) -> int: + """Write the IR buffer to a .ll file; 0 on success, non-zero on failure.""" + if ir_buf is None or output_dir is None or module_name is None: return 1 + + # build sliced path: output_dir/{sha1 prefix}/{module_name}.ll + path: str = build_sliced_path(output_dir, module_name, "ll") + if path is None: return 1 + + # open the file for writing + f: fileio.File* = fileio.File(path, fileio.MODE.W) + if f.closed: return 1 + + written: int64 = f.write(ir_buf, ir_len) + f.close() + if written < 0: return 1 + return 0 + + +def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int: + """Compile a .ll file to .obj via llc; 0 on success, non-zero on failure.""" + if ir_path is None or cc_cmd is None or output_dir is None or module_name is None: return 1 + + # build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj + obj_path: str = build_sliced_path(output_dir, module_name, "obj") + if obj_path is None: return 1 + + # ASan mode: run opt -passes=asan before llc. -asan-globals=false disables global + # instrumentation: VPC addresses globals directly and ASan redzones would shift them. + compile_input: str = ir_path + + # ---- MLIR lowering (fixed, no switch): mlir-opt (validate) -> mlir-translate ---- + # Must run before ASan opt and llc, replacing compile_input with <...>.ll.local. + ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local") + mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt") + if ll_local_path is None or mlir_opt_out is None: return 1 + mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16 + cmd_mo: str = _mbuddy.alloc(mo_len) + if cmd_mo is None: return 1 + viperlib.snprintf(cmd_mo, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out) + mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40 + cmd_mt: str = _mbuddy.alloc(mt_len) + if cmd_mt is None: return 1 + viperlib.snprintf(cmd_mt, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path) + if _phase2_cmd_path is not None: + # write the command only (must precede the ASan/llc commands below) + _phase2_write_cmd(cmd_mo) + _phase2_write_cmd(cmd_mt) + else: + mo_rc: int = _run_cmd_wait(cmd_mo) + if mo_rc != 0: + VLogger.error(f"mlir-opt failed (module={module_name}, exit={mo_rc}, cmd={cmd_mo})", "MLIR") + return 1 + mt_rc: int = _run_cmd_wait(cmd_mt) + if mt_rc != 0: + VLogger.error(f"mlir-translate failed (module={module_name}, exit={mt_rc}, cmd={cmd_mt})", "MLIR") + return 1 + compile_input = ll_local_path + + # --phase2command mode: do not run opt/llc; write the opt (ASan) + llc commands to the bat + if _phase2_cmd_path is not None: + p2_opt_cmd: str = None + if _asan_mode != 0: + p2_asan: str = build_sliced_path(output_dir, module_name, "asan.ll") + if p2_asan is not None: + p2_olen: sizet = len(p2_asan) + len(compile_input) + 192 + p2_opt_cmd = _mbuddy.alloc(p2_olen) + if p2_opt_cmd is not None: + viperlib.snprintf(p2_opt_cmd, p2_olen, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", p2_asan, compile_input) + compile_input = p2_asan + llc_fp: str = _llc_frame_ptr_flag() + p2_clen: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp) + len(obj_path) + len(compile_input) + 64 + p2_cmd: bytes = _mbuddy.alloc(p2_clen) + if p2_cmd is not None: + viperlib.snprintf(p2_cmd, p2_clen, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp, obj_path, compile_input) + if p2_opt_cmd is not None: _phase2_write_cmd(p2_opt_cmd) + if p2_cmd is not None: _phase2_write_cmd(p2_cmd) + return 0 + + if _asan_mode != 0: + asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll") + if asan_path is None: return 1 + opt_cmd_len: sizet = len(asan_path) + len(compile_input) + 192 + opt_cmd: bytes = _mbuddy.alloc(opt_cmd_len) + if opt_cmd is None: return 1 + viperlib.snprintf(opt_cmd, opt_cmd_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", asan_path, compile_input) + opt_ret: int = stdlib.system(opt_cmd) + if opt_ret != 0: + VLogger.error(f"opt ASan instrumentation failed (module={module_name})", "ASAN") + return 1 + compile_input = asan_path + + # command: llc -filetype=obj -o {obj_path} ir_path + llc_fp2: str = _llc_frame_ptr_flag() + cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp2) + len(obj_path) + len(compile_input) + 64 + cmd: bytes = _mbuddy.alloc(cmd_len) + if cmd is None: return 1 + viperlib.snprintf(cmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp2, obj_path, compile_input) + + # Launch llc directly via CreateProcessA (avoids cmd/c hanging under asan); the child + # inherits the console. On failure, log module sha1 + exit code + full command. + sys_ret: int = _run_cmd_wait(cmd) + if sys_ret != 0: + VLogger.error(f"compilation failed (module={module_name}, exit={sys_ret}, cmd={cmd})", "LLC") + return 1 + return 0 + + +# llc parallel compile (Phase B, async threads): tasks/commands use stdlib.malloc (main pool mb +# gets reset); the worker never allocates from MemBuddy. Max threads = os.cpu_count() (16 here). +define LLC_MAX_THREADS = 16 + +# LlcTask.FailStage: which step failed (0=none, 1=mlir-opt, 2=mlir-translate, 3=ASan opt, +# 4=llc). Without this the main thread misreported mlir-opt/translate failures as llc failures. +define LLC_STAGE_NONE = 0 +define LLC_STAGE_MLIR_OPT = 1 +define LLC_STAGE_MLIR_TRANSLATE = 2 +define LLC_STAGE_ASAN_OPT = 3 +define LLC_STAGE_LLC = 4 + + +class LlcTask: + """Async llc task: command strings (stdlib.malloc), module SHA1 and result.""" + OptCmd: bytes # ASan instrumentation command (None if not ASan) + LlcCmd: bytes # llc command + ModuleSha1: bytes # module SHA1 (for error reporting) + Result: int # worker-written: 0 success, non-zero failure + # ExitCode is appended at the end so existing offsets are unchanged; all construction + # sites use __sizeof__() and memset the whole record, so offsets need no update. + ExitCode: int # worker-written: exit code of the failing command (0 on success) + # MLIR-backend lowering commands (None if not MLIR); also appended at the end. + MlirOptCmd: bytes # mlir-opt validation command + MlirTranslateCmd: bytes # mlir-translate lowering command + # failure stage (LLC_STAGE_*, see the constants above); appended at the end. + FailStage: int # worker-written: which step failed + + +def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int: + """Fill an llc task (all stdlib.malloc, kept alive for the thread's run); 0 on success.""" + if task is None or ir_path is None or output_dir is None or module_name is None: return 1 + string.memset(task, 0, LlcTask.__sizeof__()) + task.Result = -1 + obj_path: str = build_sliced_path(output_dir, module_name, "obj") + if obj_path is None: return 1 + # module SHA1 + sl: sizet = len(module_name) + task.ModuleSha1 = stdlib.malloc(sl + 1) + if task.ModuleSha1 is None: return 1 + string.strcpy(task.ModuleSha1, module_name) + # llc input choice: ASan mode must feed the instrumented .asan.ll, same as the serial + # compile_ll_to_obj path (else llc ignores the instrumentation and ASan is half-blind). + compile_input: str = ir_path + # MLIR lowering (fixed): the worker runs mlir-opt (validate) -> mlir-translate first, so + # the later ASan OptCmd / LlcCmd input must be <...>.ll.local. + ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local") + mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt") + if ll_local_path is None or mlir_opt_out is None: return 1 + mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16 + task.MlirOptCmd = stdlib.malloc(mo_len) + if task.MlirOptCmd is None: return 1 + viperlib.snprintf(task.MlirOptCmd, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out) + mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40 + task.MlirTranslateCmd = stdlib.malloc(mt_len) + if task.MlirTranslateCmd is None: return 1 + viperlib.snprintf(task.MlirTranslateCmd, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path) + compile_input = ll_local_path + # ASan mode: opt instrumentation command (the worker runs MLIR lowering/opt before llc, + # so LlcCmd's input must point at asan_path; failure sets Result). + if _asan_mode != 0: + asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll") + if asan_path is None: return 1 + opt_len: sizet = len(asan_path) + len(compile_input) + 192 + task.OptCmd = stdlib.malloc(opt_len) + if task.OptCmd is None: return 1 + viperlib.snprintf(task.OptCmd, opt_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address" " -asan-globals=false -o %s %s", asan_path, compile_input) + compile_input = asan_path + # llc command (input = compile_input: .ll normally, .asan.ll under ASan) + llc_fp3: str = _llc_frame_ptr_flag() + cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp3) + len(obj_path) + len(compile_input) + 64 + task.LlcCmd = stdlib.malloc(cmd_len) + if task.LlcCmd is None: return 1 + viperlib.snprintf(task.LlcCmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp3, obj_path, compile_input) + return 0 + + +def free_llc_task(task: LlcTask*): + """Free the task's command strings; the record itself is a slot in the task array.""" + if task is None: return + if task.OptCmd is not None: + stdlib.free(task.OptCmd) + task.OptCmd = None + if task.LlcCmd is not None: + stdlib.free(task.LlcCmd) + task.LlcCmd = None + if task.MlirOptCmd is not None: + stdlib.free(task.MlirOptCmd) + task.MlirOptCmd = None + if task.MlirTranslateCmd is not None: + stdlib.free(task.MlirTranslateCmd) + task.MlirTranslateCmd = None + if task.ModuleSha1 is not None: + stdlib.free(task.ModuleSha1) + task.ModuleSha1 = None + + +def _phase2_write_cmd(cmd: str) -> None: + """--phase2command mode: append the command to the bat file without executing it.""" + global _phase2_bat_init + if _phase2_cmd_path is None or cmd is None: return + f: fileio.File* = fileio.File(_phase2_cmd_path, fileio.MODE.A) + if f.closed: + VLogger.error("phase2command: failed to open bat", "phase2cmd") + return + if _phase2_bat_init == 0: + _phase2_bat_init = 1 + hdr1: str = "@echo off\r\n" + hdr2: str = "rem ViperC --phase2command generated commands\r\n" + f.write(hdr1, string.strlen(hdr1)) + f.write(hdr2, string.strlen(hdr2)) + f.write(cmd, string.strlen(cmd)) + f.write("\r\n", 2) + f.close() + + +def _run_cmd_wait(cmd: str) -> int: + """Run a command synchronously via CreateProcessA and return its exit code.""" + if cmd is None: return -1 + cl_len: sizet = len(cmd) + cmd_line: bytes = stdlib.malloc(cl_len + 1) + if cmd_line is None: return -1 + string.strcpy(cmd_line, cmd) + sa: win32.win32base.SECURITY_ATTRIBUTES = win32.win32base.SECURITY_ATTRIBUTES() + si: win32process.STARTUPINFOA = win32process.STARTUPINFOA() + pi: win32process.PROCESS_INFORMATION = win32process.PROCESS_INFORMATION() + # T() only allocas, no memset: unset STARTUPINFOA fields are stack garbage, so concurrent + # threads get random dwFlags and CreateProcessA uses junk stdio handles. Clear si/sa/pi. + string.memset(&si, 0, win32process.STARTUPINFOA.__sizeof__()) + string.memset(&sa, 0, win32.win32base.SECURITY_ATTRIBUTES.__sizeof__()) + string.memset(&pi, 0, win32process.PROCESS_INFORMATION.__sizeof__()) + sa.nLength = win32.win32base.SECURITY_ATTRIBUTES.__sizeof__() + sa.lpSecurityDescriptor = t.CPtr(0) + sa.bInheritHandle = 1 + si.cb = win32process.STARTUPINFOA.__sizeof__() + # create_flags=0: the child inherits the parent console (output shown, like system); + # si.dwFlags stays 0 after memset (no STARTF_USESTDHANDLES, no std handles). + ok: int = win32process.CreateProcessA(t.CPtr(0), cmd_line, t.CPtr(0), t.CPtr(0), 1, 0, t.CPtr(0), t.CPtr(0), &si, &pi) + # CreateProcessA failure diagnostics: read GetLastError immediately (later calls overwrite + # it). Use warning, not error (error would sys.exit in a worker thread). + if ok == 0: + cp_err: uint32 = win32.win32base.GetLastError() + VLogger.warning(f"CreateProcessA failed (GetLastError={cp_err}) external command not started: cmd={cmd}", "LLC") + stdlib.free(cmd_line) + if ok == 0: return -1 + win32sync.WaitForSingleObject(pi.hProcess, win32.win32base.INFINITE) + exit_code: uint32 = 0 + win32process.GetExitCodeProcess(pi.hProcess, &exit_code) + win32.win32base.CloseHandle(pi.hProcess) + win32.win32base.CloseHandle(pi.hThread) + return int(exit_code) + + +def _llc_task_worker(arg: t.CPtr) -> t.CPtr: + """llc thread entry: only calls _run_cmd_wait, no pool allocation (thread-safe).""" + task: LlcTask* = (LlcTask*)(arg) + if task is None: return t.CPtr(0) + # MLIR backend: validate (mlir-opt) -> lower (mlir-translate), then ASan opt -> llc + if task.MlirOptCmd is not None: + mo_rc: int = _run_cmd_wait(task.MlirOptCmd) + if mo_rc != 0: + task.ExitCode = mo_rc + task.FailStage = LLC_STAGE_MLIR_OPT + task.Result = 1 + return t.CPtr(0) + if task.MlirTranslateCmd is not None: + mt_rc: int = _run_cmd_wait(task.MlirTranslateCmd) + if mt_rc != 0: + task.ExitCode = mt_rc + task.FailStage = LLC_STAGE_MLIR_TRANSLATE + task.Result = 1 + return t.CPtr(0) + if task.OptCmd is not None: + opt_rc: int = _run_cmd_wait(task.OptCmd) + if opt_rc != 0: + task.ExitCode = opt_rc + task.FailStage = LLC_STAGE_ASAN_OPT + task.Result = 1 + return t.CPtr(0) + llc_rc: int = _run_cmd_wait(task.LlcCmd) + task.ExitCode = llc_rc + if llc_rc != 0: + task.FailStage = LLC_STAGE_LLC + task.Result = 1 + else: + task.FailStage = LLC_STAGE_NONE + task.Result = 0 + return t.CPtr(0) + + +def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int: + """Run llc in parallel: batches of max_threads threads, join all then check results.""" + if tasks is None or task_count <= 0: return 0 + # --phase2command mode: do not spawn threads; write all opt (ASan) + llc commands to the bat + if _phase2_cmd_path is not None: + for pi2 in range(int(task_count)): + tsk_p: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(pi2) * LlcTask.__sizeof__()) + if tsk_p is None: continue + if tsk_p.MlirOptCmd is not None: _phase2_write_cmd(tsk_p.MlirOptCmd) + if tsk_p.MlirTranslateCmd is not None: _phase2_write_cmd(tsk_p.MlirTranslateCmd) + if tsk_p.OptCmd is not None: _phase2_write_cmd(tsk_p.OptCmd) + if tsk_p.LlcCmd is not None: _phase2_write_cmd(tsk_p.LlcCmd) + VLogger.info(f"phase2command mode: {task_count} llc commands written to {_phase2_cmd_path}", "phase2cmd") + return 0 + if max_threads <= 0: max_threads = 1 + # Thread-handle array: list[str] (str=bytes=i8*, explicit elem_size=8) stores handles; + # list[t.CPtr] append crashes reading page zero. Grab the handle right after start(). + handles: list[str]* = list[str](_mbuddy, 8) + if handles is None: + VLogger.error("run_llc_parallel: failed to allocate thread handle array", "PhaseB") + return 1 + slot_size: sizet = LlcTask.__sizeof__() + ret: int = 0 + # Batched execution: batch_start steps by max_threads (each batch spawn+join+close). + for batch_start in range(0, int(task_count), int(max_threads)): + batch_n: int = max_threads + if batch_start + batch_n > task_count: batch_n = task_count - batch_start + # spawn this batch + for j in range(int(batch_n)): + tsk: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size) + th: vthreading.Thread* = vthreading.Thread(_llc_task_worker, t.CPtr(tsk)) + th.start() + # Read the handle right after start() (the Thread object's stack slot may be + # reused; reading self.handle at join could see a cleared/overwritten value). + handles.append(t.CPtr(th.handle)) + for j in range(int(batch_n)): + h_j: t.CPtr = t.CPtr(handles.get(batch_start + j)) + wr: uint32 = win32sync.WaitForSingleObject(h_j, 600000) + if wr != 0: VLogger.error("llc thread join timeout/failure (not finished within 600 seconds)", "PhaseB") + win32.win32base.CloseHandle(h_j) + # check this batch's results + for j in range(int(batch_n)): + tsk2: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size) + if tsk2 is not None and tsk2.Result != 0: + # Failure: use FailStage to pinpoint the step and print that step's command. + # Previously everything printed the LlcCmd as "llc failed", misdirecting MLIR failures. + st: int = tsk2.FailStage + stage_name: str = "unknown stage (worker did not finish)" + stage_cmd: bytes = tsk2.LlcCmd + if st == LLC_STAGE_MLIR_OPT: + stage_name = "mlir-opt validation" + stage_cmd = tsk2.MlirOptCmd + elif st == LLC_STAGE_MLIR_TRANSLATE: + stage_name = "mlir-translate lowering" + stage_cmd = tsk2.MlirTranslateCmd + elif st == LLC_STAGE_ASAN_OPT: + stage_name = "opt ASan instrumentation" + stage_cmd = tsk2.OptCmd + elif st == LLC_STAGE_LLC: + stage_name = "llc compile" + stage_cmd = tsk2.LlcCmd + VLogger.error(f"{stage_name} failed: {(tsk2.ModuleSha1 if tsk2.ModuleSha1 is not None else "?")} (exit={tsk2.ExitCode} cmd={(stage_cmd if stage_cmd is not None else "?")})", "LLC") + ret = 1 + return ret + + +def _CollectObjFilesImpl(base_dir: str, bd_len: sizet, out_buf: bytes, out_pos: sizet, out_size: sizet) -> sizet: + """Recursively scan base_dir, collect .obj paths into out_buf, return the new out_pos.""" + find_data: win32.win32file.WIN32_FIND_DATAA* = _mbuddy.alloc(win32.win32file.WIN32_FIND_DATAA.__sizeof__()) + if find_data is None: + VLogger.warning("_CollectObjFilesImpl failed to allocate find_data, fewer .obj collected", "collect") + return out_pos + string.memset(find_data, 0, win32.win32file.WIN32_FIND_DATAA.__sizeof__()) + + pattern: bytes = _mbuddy.alloc(bd_len + 8) + if pattern is None: + VLogger.warning("_CollectObjFilesImpl failed to allocate pattern, fewer .obj collected", "collect") + return out_pos + viperlib.snprintf(pattern, bd_len + 8, "%s/*", base_dir) + + handle: win32.win32base.HANDLE = win32.win32file.FindFirstFileA(pattern, find_data) + _mbuddy.free(pattern) + if handle == win32.win32base.INVALID_HANDLE_VALUE: + VLogger.warning("_CollectObjFilesImpl cannot open directory, fewer .obj collected", "collect") + return out_pos + + while True: + fname: str = find_data.cFileName + if fname is not None: + fname_len: sizet = len(fname) + if fname_len > 0: + # skip . and .. + is_dot: int = 0 + if fname_len == 1 and fname[0] == '.': is_dot = 1 + elif fname_len == 2 and fname[0] == '.' and fname[1] == '.': is_dot = 1 + if is_dot == 0: + attrs: ULONG = find_data.dwFileAttributes + is_dir: int = 0 + if (attrs & win32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: is_dir = 1 + if is_dir != 0: + # recurse into the subdirectory + sub_dir: bytes = _mbuddy.alloc(bd_len + fname_len + 2) + if sub_dir is not None: + viperlib.snprintf(sub_dir, bd_len + fname_len + 2, "%s/%s", base_dir, fname) + sub_len: sizet = len(sub_dir) + out_pos = _CollectObjFilesImpl(sub_dir, sub_len, out_buf, out_pos, out_size) + _mbuddy.free(sub_dir) + else: + # check whether this is a .obj file + if fname_len > 4 and fname[fname_len - 4] == '.' and fname[fname_len - 3] == 'o' and fname[fname_len - 2] == 'b' and fname[fname_len - 1] == 'j': + need: sizet = bd_len + 1 + fname_len + 2 + if out_pos + need >= out_size: break + if out_pos > 0: + out_buf[out_pos] = ' ' + out_pos += 1 + viperlib.snprintf(out_buf + out_pos, need, "%s/%s", base_dir, fname) + out_pos += bd_len + 1 + fname_len + + if win32.win32file.FindNextFileA(handle, find_data) == 0: break + + win32.win32file.FindClose(handle) + return out_pos + + +def collect_obj_files(includes_binary_dir: str, out_buf: bytes, out_size: sizet) -> sizet: + """Recursively scan includes_binary_dir, collect all .obj paths into out_buf; bytes written.""" + if includes_binary_dir is None or out_buf is None or out_size == 0: return 0 + + out_buf[0] = '\0' + dir_len: sizet = len(includes_binary_dir) + out_pos: sizet = _CollectObjFilesImpl(includes_binary_dir, dir_len, out_buf, 0, out_size) + out_buf[out_pos] = '\0' + return out_pos + + +def _append_host_export_implib(linker_flags: str, implib_path: str) -> str: + """Host-export mode: append --out-implib= when linker_flags has --export-all-symbols.""" + if linker_flags is None or implib_path is None: return linker_flags + if string.strstr(linker_flags, "--export-all-symbols") is None: return linker_flags + extra: sizet = string.strlen(" -Wl,--out-implib=") + string.strlen(implib_path) + 1 + buf: str = _mbuddy.alloc(string.strlen(linker_flags) + extra) + if buf is None: + VLogger.warning("host export: failed to allocate import lib argument buffer (fragments cannot import host symbols)", "link") + return linker_flags + viperlib.snprintf(buf, string.strlen(linker_flags) + extra, "%s -Wl,--out-implib=%s", linker_flags, implib_path) + VLogger.info(f"host export: produced import lib {implib_path}", "link") + return buf + + +def link_obj_to_exe(output_dir: str, module_name: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int: + """Link .obj files into an .exe via clang++; 0 on success, non-zero on failure.""" + if output_dir is None or linker_cmd is None or module_name is None: return 1 + + # build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj + obj_path: str = build_sliced_path(output_dir, module_name, "obj") + if obj_path is None: return 1 + + # Host-exe export (when linker.flags has --export-all-symbols) also emits an import lib + # {output_dir}/{linker_output}.dll.a; REPL fragments link it to import the host. + if linker_output is not None: + implib_len: sizet = string.strlen(output_dir) + string.strlen(linker_output) + 8 + implib_path: str = _mbuddy.alloc(implib_len) + if implib_path is not None: + viperlib.snprintf(implib_path, implib_len, "%s/%s.dll.a", output_dir, linker_output) + linker_flags = _append_host_export_implib(linker_flags, implib_path) + + # --traceback: link dbghelp (after the host-export rewrite, so both survive) + linker_flags = _append_traceback_dbghelp(linker_flags) + + # collect .obj paths from includes.binary + EXTRA_BUF_SIZE: sizet = 32768 + extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE) + if extra_objs is None: return 1 + extra_len: sizet = 0 + if includes_binary_dir is not None: + extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE) + if extra_len > 0: VLogger.info(f"appended {extra_len} bytes of includes.binary .obj files", "link") + else: VLogger.warning(f"includes.binary has no .obj files: {includes_binary_dir}", "link") + + # Command: clang++ {obj_path} extra_objs [libs] -o {...}/{linker_output} linker_flags. Objects + # and explicit libs must precede -l flags. The ASan runtime is declared in project.vpj, not hardcoded. + use_flags: str = linker_flags + static_libs: str = linker_static_libs + if static_libs is None: static_libs = "" + if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0: + # Merge static + dynamic into one space-separated string (Viper str += would emit + # invalid add i8* IR, so build it with snprintf). + merged_sl: str = _mbuddy.alloc(len(static_libs) + len(linker_dynamic_libs) + 2) + if merged_sl is not None: + if len(static_libs) > 0: + viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s %s", static_libs, linker_dynamic_libs) + else: + viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs) + static_libs = merged_sl + sl_len: sizet = len(static_libs) + cmd_len: sizet = len(linker_cmd) + len(obj_path) + len(use_flags) + len(output_dir) + len(linker_output) + extra_len + sl_len + 128 + cmd: bytes = _mbuddy.alloc(cmd_len) + if cmd is None: return 1 + if sl_len > 0 and extra_len > 0: + viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, static_libs, output_dir, linker_output, use_flags) + elif sl_len > 0: + viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, static_libs, output_dir, linker_output, use_flags) + elif extra_len > 0: + viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, output_dir, linker_output, use_flags) + else: + viperlib.snprintf(cmd, cmd_len, "%s %s -o %s/%s %s", linker_cmd, obj_path, output_dir, linker_output, use_flags) + + # --phase2command mode: do not link; write the command to the bat + if _phase2_cmd_path is not None: + _phase2_write_cmd(cmd) + return 0 + + result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) + if result is None: + VLogger.error("subprocess.run returned None", "link") + return 1 + if result.returncode != 0: + VLogger.error(f"link failed, return code: {result.returncode}", "link") + VLogger.error(f"command: {cmd}", "link") + # Show linker error output. + # Note: subprocess merges stderr into stdout on Windows, so result.stderr is always None. + if result.stdout is not None: VLogger.error(result.stdout, "link") + else: VLogger.error("no output captured", "link") + if result.stderr is not None: VLogger.error(result.stderr, "link") + return 1 + return 0 + + +def asm_to_obj(asm_path: str, sha1_val: str, output_dir: str, triple: str) -> int: + """Assemble a .s source into .obj with llvm-mc on the sliced path; 0 on success, non-zero on failure.""" + if asm_path is None or sha1_val is None or output_dir is None: return 1 + obj_path: str = build_sliced_path(output_dir, sha1_val, "obj") + if obj_path is None: return 1 + trip: str = triple + if trip is None or trip[0] == '\0': trip = "x86_64-none-elf" + cmd_len: sizet = len("llvm-mc -triple=") + len(trip) + len(" -filetype=obj -o ") + len(obj_path) + len(asm_path) + 64 + cmd: bytes = _mbuddy.alloc(cmd_len) + if cmd is None: return 1 + viperlib.snprintf(cmd, cmd_len, "llvm-mc -triple=%s -filetype=obj -o %s %s", trip, obj_path, asm_path) + + # --phase2command mode: write the command to the bat (same as llc/link) + if _phase2_cmd_path is not None: + _phase2_write_cmd(cmd) + return 0 + + result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) + if result is None: + VLogger.error("llvm-mc subprocess.run returned None", "asm") + return 1 + if result.returncode != 0: + if result.stdout is not None: stdio.printf("%s\n", result.stdout) + if result.stderr is not None: stdio.printf("%s\n", result.stderr) + VLogger.error(f"llvm-mc assembly failed, return code: {result.returncode}, command: {cmd}", "asm") + return 1 + return 0 + + +def compile_module_to_obj(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int: + """Compile IR to .obj without linking; 0 on success, non-zero on failure.""" + if ir_buf is None or temp_dir is None or output_dir is None or module_name is None: return 1 + + # Step 1: write the .ll file + ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name) + if ret != 0: + VLogger.error(f"failed to write .ll: {module_name}", "compile") + return 1 + + # Step 2: build the sliced .ll path and compile -> .obj + ir_path: str = build_sliced_path(temp_dir, module_name, "ll") + if ir_path is None: return 1 + + ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags) + if ret != 0: + VLogger.error(f"llc compilation failed: {module_name}", "compile") + return 1 + return 0 + + +def link_objs_to_exe(obj_paths: str, obj_paths_len: sizet, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int: + """Link multiple .obj files into an .exe; 0 on success, non-zero on failure.""" + if obj_paths is None or obj_paths_len == 0 or linker_cmd is None: return 1 + + # Dedup .obj paths: the same module's obj can be added by several collection stages, and + # a path appearing twice causes duplicate symbols. Keep the first occurrence, skip the rest. + if obj_paths_len > 0: + dedup: bytes = _mbuddy.alloc(obj_paths_len + 1) + if dedup is not None: + dedup[0] = '\0' + dpos: sizet = 0 + dsrc_i: sizet = 0 + while dsrc_i < obj_paths_len: + # skip leading spaces + while dsrc_i < obj_paths_len and obj_paths[dsrc_i] == ' ': dsrc_i += 1 + if dsrc_i >= obj_paths_len: break + dstart: sizet = dsrc_i + while dsrc_i < obj_paths_len and obj_paths[dsrc_i] != ' ': dsrc_i += 1 + dtlen: sizet = dsrc_i - dstart + # check whether dedup already has this token + ddup: int = 0 + dj: sizet = 0 + while dj < dpos: + while dj < dpos and dedup[dj] == ' ': dj += 1 + if dj >= dpos: break + ds2: sizet = dj + while dj < dpos and dedup[dj] != ' ': dj += 1 + dt2len: sizet = dj - ds2 + if dt2len == dtlen: + deq: int = 1 + for dk in range(int(dtlen)): + if dedup[ds2 + dk] != obj_paths[dstart + dk]: + deq = 0 + break + if deq != 0: + ddup = 1 + break + if ddup == 0: + if dpos > 0: + dedup[dpos] = ' ' + dpos += 1 + string.memcpy(dedup + dpos, obj_paths + dstart, dtlen) + dpos += dtlen + dedup[dpos] = '\0' + if dpos > 0 and dpos != obj_paths_len: + obj_paths = dedup + obj_paths_len = dpos + + # collect .obj paths from includes.binary + EXTRA_BUF_SIZE: sizet = 32768 + extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE) + if extra_objs is None: return 1 + extra_len: sizet = 0 + if includes_binary_dir is not None: extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE) + + # Command: clang++ obj_paths extra_objs -o linker_output linker_flags. The ASan runtime + # comes from project.vpj, not VPC. Host export: import lib is .dll.a. + if linker_output is not None: + implib_len2: sizet = string.strlen(linker_output) + 8 + implib_path2: str = _mbuddy.alloc(implib_len2) + if implib_path2 is not None: + viperlib.snprintf(implib_path2, implib_len2, "%s.dll.a", linker_output) + linker_flags = _append_host_export_implib(linker_flags, implib_path2) + # --traceback: link dbghelp (after the host-export rewrite) + linker_flags = _append_traceback_dbghelp(linker_flags) + use_flags: str = linker_flags + static_libs2: str = linker_static_libs + if static_libs2 is None: static_libs2 = "" + if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0: + # merge static + dynamic (Viper str += emits invalid IR; use snprintf) + merged_sl2: str = _mbuddy.alloc(len(static_libs2) + len(linker_dynamic_libs) + 2) + if merged_sl2 is not None: + if len(static_libs2) > 0: + viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s %s", static_libs2, linker_dynamic_libs) + else: + viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs) + static_libs2 = merged_sl2 + sl2_len: sizet = len(static_libs2) + cmd_len: sizet = len(linker_cmd) + obj_paths_len + len(use_flags) + len(linker_output) + extra_len + sl2_len + 128 + cmd: bytes = _mbuddy.alloc(cmd_len) + if cmd is None: return 1 + # linker.static_libs explicit static libs go after the .obj files and before -o + if sl2_len > 0 and extra_len > 0: + viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, static_libs2, linker_output, use_flags) + elif sl2_len > 0: + viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, static_libs2, linker_output, use_flags) + elif extra_len > 0: + viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, linker_output, use_flags) + else: + viperlib.snprintf(cmd, cmd_len, "%s %s -o %s %s", linker_cmd, obj_paths, linker_output, use_flags) + + # Capture clang++ stdout/stderr via subprocess.run (stdlib.system could not capture errors like + # undefined reference). --phase2command mode: do not link; write the command to the bat. + if _phase2_cmd_path is not None: + _phase2_write_cmd(cmd) + return 0 + result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) + if result is None: + VLogger.error("subprocess.run returned None", "link") + return 1 + if result.returncode != 0: + # First print clang++'s full error output (incl. undefined reference) with stdio.printf; + # it must precede VLogger.error, which calls sys.exit(1) and terminates the process. + if result.stdout is not None: stdio.printf("=== clang++ link error output ===\n%s\n", result.stdout) + if result.stderr is not None: + stdio.printf("=== clang++ stderr ===\n%s\n", result.stderr) + VLogger.error(f"link failed, return code: {result.returncode}, command: {cmd}", "link") + return 1 + return 0 + + +# --traceback: .stackmap sidecar writer (address -> source line). After a successful link, +# llvm-dwarfdump parses the PE's DWARF; addresses are normalised to RVAs and written out. + +# row record: u64 rva | u64 file_path | u32 line | u32 pad | u64 func_name (nullable). +# func_name is only carried by our .vpcall rows; DWARF rows write 0 and fall back to the subprogram table. +define TB_ROW_SIZE = 32 +define TB_ROW_OFF_FILE = 8 +define TB_ROW_OFF_LINE = 16 +define TB_ROW_OFF_FUNC = 24 +# func record: u64 low_rva | u64 high_rva | u64 name +define TB_FUNC_SIZE = 24 +# dir record: u32 idx | u32 pad | u64 name +define TB_DIR_SIZE = 16 +# file record: u32 idx | u32 dir_index | u64 name | u64 resolved_path +define TB_FILE_SIZE = 32 +# initial capacities for the (growable) row / func tables +define TB_ROW_CAP0 = 65536 +define TB_FUNC_CAP0 = 65536 +# fixed per-CU table capacities (exceeding either aborts the build) +define TB_DIR_CAP = 4096 +define TB_FILE_CAP = 65536 + + +class TbPeLayout: + Ok: int + ImageBase: uint64 + SizeOfImage: uint64 + TextRva: uint64 + + def __new__(self) -> t.CPtr: + return t.CPtr(_mbuddy.alloc(TbPeLayout.__sizeof__())) + + def __init__(self): + self.Ok = 0 + self.ImageBase = 0 + self.SizeOfImage = 0 + self.TextRva = 0 + + +class TbDump: + RowBuf: bytes + RowCap: int + RowCount: int + RawRowCount: int + FuncBuf: bytes + FuncCap: int + FuncCount: int + + def __new__(self) -> t.CPtr: + return t.CPtr(_mbuddy.alloc(TbDump.__sizeof__())) + + def __init__(self): + self.RowBuf = None + self.RowCap = 0 + self.RowCount = 0 + self.RawRowCount = 0 + self.FuncBuf = None + self.FuncCap = 0 + self.FuncCount = 0 + + +class TbPool: + Pool: bytes + PoolLen: int + Offs: bytes + OffCount: int + + def __new__(self) -> t.CPtr: + return t.CPtr(_mbuddy.alloc(TbPool.__sizeof__())) + + def __init__(self): + self.Pool = None + self.PoolLen = 0 + self.Offs = None + self.OffCount = 0 + + +# ---- little-endian byte helpers (no struct.pack in Viper) ---- +def _tb_u8(buf: bytes, off: sizet) -> uint32: + return uint32(int(buf[off]) & 0xFF) + + +def _tb_rd_u16(buf: bytes, off: sizet) -> uint32: + return _tb_u8(buf, off) | (_tb_u8(buf, off + 1) << 8) + + +def _tb_rd_u32(buf: bytes, off: sizet) -> uint32: + b0: uint32 = _tb_u8(buf, off) + b1: uint32 = _tb_u8(buf, off + 1) + b2: uint32 = _tb_u8(buf, off + 2) + b3: uint32 = _tb_u8(buf, off + 3) + return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) + + +def _tb_rd_u64(buf: bytes, off: sizet) -> uint64: + lo: uint64 = uint64(_tb_rd_u32(buf, off)) + hi: uint64 = uint64(_tb_rd_u32(buf, off + 4)) + return lo | (hi << 32) + + +def _tb_wr_u32(buf: bytes, off: sizet, v: uint64): + buf[off] = char(int(v & 0xFF)) + buf[off + 1] = char(int((v >> 8) & 0xFF)) + buf[off + 2] = char(int((v >> 16) & 0xFF)) + buf[off + 3] = char(int((v >> 24) & 0xFF)) + + +def _tb_wr_u64(buf: bytes, off: sizet, v: uint64): + _tb_wr_u32(buf, off, v & 0xFFFFFFFF) + _tb_wr_u32(buf, off + 4, (v >> 32) & 0xFFFFFFFF) + + +# ---- tiny string helpers (no re module) ---- +def _tb_cc(s: str, i: sizet) -> int: + """Byte value at s[i] as 0..255 (chars in DWARF text are ASCII).""" + if s is None: return 0 + return int(s[i]) & 0xFF + + +def _tb_contains(s: str, sub: str) -> int: + if s is None or sub is None: return 0 + return 1 if string.strstr(s, sub) is not None else 0 + + +def _tb_contains_span(s: str, ls: sizet, le: sizet, sub: str) -> int: + """Span-aware substring test: only s[ls, le) is searched. + A tail-wide strstr would both scan the whole remaining objdump text (hundreds of MB) and + match a substring belonging to a LATER line (e.g. ".text" found past a ".rdata" header).""" + if s is None or sub is None: return 0 + sl: sizet = string.strlen(sub) + if sl == 0 or le < ls or le - ls < sl: return 0 + i: sizet = ls + while i + sl <= le: + if string.strncmp(s + i, sub, sl) == 0: return 1 + i += 1 + return 0 + + +def _tb_startswith(s: str, pfx: str) -> int: + if s is None or pfx is None: return 0 + pl: sizet = string.strlen(pfx) + if string.strlen(s) < pl: return 0 + return 1 if string.strncmp(s, pfx, pl) == 0 else 0 + + +def _tb_startswith_span(s: str, ls: sizet, le: sizet, pfx: str) -> int: + """Span-aware prefix test: only s[ls, le) is considered, never the tail. + _tb_startswith on `s + ls` would run strlen over the WHOLE remaining text; llvm-objdump -d + output is hundreds of MB for a large exe, so a per-line strlen makes the whole scan O(n^2) + and the compilation appears to hang (it spins inside strlen for hours).""" + if s is None or pfx is None: return 0 + if le < ls: return 0 + pl: sizet = string.strlen(pfx) + if le - ls < pl: return 0 + return 1 if string.strncmp(s + ls, pfx, pl) == 0 else 0 + + +def _tb_ltrim(s: str) -> str: + if s is None: return None + i: sizet = 0 + while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1 + return s + i + + +def _tb_len_trimmed(s: str) -> sizet: + if s is None: return 0 + n: sizet = string.strlen(s) + while n > 0: + c: int = int(_tb_cc(s, n - 1)) + if c == 32 or c == 9 or c == 13: n -= 1 + else: break + return n + + +def _tb_dup_span(mb: memhub.MemBuddy*, src: str, n: sizet) -> str: + """Copy src[0..n) into a fresh NUL-terminated string.""" + if src is None: return None + out: str = mb.alloc(n + 1) + if out is None: return None + string.strncpy(out, src, n) + out[n] = '\0' + return out + + +def _tb_unquote(mb: memhub.MemBuddy*, v: str) -> str: + """Trim trailing spaces and strip one pair of surrounding quotes (TPC _Unquote).""" + if v is None: return None + n: sizet = _tb_len_trimmed(v) + if n >= 2 and _tb_cc(v, 0) == 34 and _tb_cc(v, n - 1) == 34: return _tb_dup_span(mb, v + 1, n - 2) + return _tb_dup_span(mb, v, n) + + +def _tb_is_abs_path(s: str) -> int: + if s is None: return 0 + c0: int = _tb_cc(s, 0) + if c0 == 0: return 0 + if c0 == 47 or c0 == 92: return 1 + if _tb_cc(s, 1) == 58: return 1 + return 0 + + +def _tb_join(mb: memhub.MemBuddy*, base: str, name: str) -> str: + """Join base and name with a separator unless base already ends in one.""" + if base is None or string.strlen(base) == 0: return name + if name is None: return base + bl: sizet = string.strlen(base) + nl: sizet = string.strlen(name) + buf: str = mb.alloc(bl + nl + 2) + if buf is None: return name + last: int = int(_tb_cc(base, bl - 1)) + if last == 47 or last == 92: viperlib.snprintf(buf, bl + nl + 2, "%s%s", base, name) + else: viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name) + return buf + + +def _tb_parse_dec(s: str) -> int: + """Parse a leading decimal integer, skipping leading blanks (dwarfdump right-aligns indices).""" + v: int = 0 + i: sizet = 0 + while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1 + while _tb_cc(s, i) >= 48 and _tb_cc(s, i) <= 57: + v = v * 10 + (_tb_cc(s, i) - 48) + i += 1 + return v + + +def _tb_bracket_idx(s: str) -> int: + """Parse the N in '[N]' (first bracket group of the line).""" + i: sizet = 0 + while _tb_cc(s, i) != 0: + if _tb_cc(s, i) == 91: return _tb_parse_dec(s + i + 1) + i += 1 + return -1 + + +def _tb_after_bracket(s: str) -> str: + i: sizet = 0 + while _tb_cc(s, i) != 0: + if _tb_cc(s, i) == 93: return s + i + 1 + i += 1 + return None + + +def _tb_value_after_sep(s: str) -> str: + """Return the text after the first '=' or ':' (left-trimmed), or None.""" + if s is None: return None + i: sizet = 0 + while _tb_cc(s, i) != 0: + c: int = _tb_cc(s, i) + if c == 61 or c == 58: return _tb_ltrim(s + i + 1) + i += 1 + return None + + +def _tb_hex_digit(ch: int) -> int: + if ch >= 48 and ch <= 57: return ch - 48 + if ch >= 97 and ch <= 102: return ch - 87 + if ch >= 65 and ch <= 70: return ch - 55 + return -1 + + +def _tb_hex_in_parens(s: str, key: str) -> uint64: + """Parse the 0x value inside the parentheses right after key; 0 when the form is absent.""" + p: str = string.strstr(s, key) + if p is None: return 0 + p = p + string.strlen(key) + while _tb_cc(p, 0) == 32 or _tb_cc(p, 0) == 9: p = p + 1 + if _tb_cc(p, 0) != 40: return 0 + p = p + 1 + while _tb_cc(p, 0) == 32: p = p + 1 + if _tb_cc(p, 0) != 48 or _tb_cc(p, 1) != 120: return 0 + q: str = p + 2 + v: uint64 = 0 + i: sizet = 0 + hd: int = _tb_hex_digit(_tb_cc(q, 0)) + while hd >= 0: + v = (v << 4) | uint64(hd) + i += 1 + hd = _tb_hex_digit(_tb_cc(q, i)) + return v + + +def _tb_quoted_after(mb: memhub.MemBuddy*, s: str, key: str) -> str: + """Extract the first double-quoted string following key (DW_AT_name ("...")).""" + p: str = string.strstr(s, key) + if p is None: return None + p = p + string.strlen(key) + q: str = string.strchr(p, 40) + if q is None: return None + q = q + 1 + while _tb_cc(q, 0) == 32: q = q + 1 + if _tb_cc(q, 0) != 34: return None + q = q + 1 + start: str = q + n: sizet = 0 + while _tb_cc(q, n) != 0: + c: int = _tb_cc(q, n) + if c == 92: + if _tb_cc(q, n + 1) == 0: break + n += 2 + continue + if c == 34: break + n += 1 + return _tb_dup_span(mb, start, n) + + +def _tb_dir_lookup(dirs: bytes, count: int, idx: int) -> str: + for i in range(int(count)): + if int(_tb_rd_u32(dirs, sizet(i) * TB_DIR_SIZE)) == idx: return rawptr(_tb_rd_u64(dirs, sizet(i) * TB_DIR_SIZE + 8)) + return None + + +def _tb_file_rec(files: bytes, count: int, idx: int) -> int: + for i in range(int(count)): + if int(_tb_rd_u32(files, sizet(i) * TB_FILE_SIZE)) == idx: return i + return -1 + + +def _tb_resolve_file(mb: memhub.MemBuddy*, files: bytes, fcount: int, dirs: bytes, dcount: int, frec: int) -> str: + """Resolve a file-table entry to a path (absolute name wins, else dir join).""" + roff: sizet = sizet(frec) * TB_FILE_SIZE + cached: uint64 = _tb_rd_u64(files, roff + 24) + if cached != 0: return rawptr(cached) + name: str = rawptr(_tb_rd_u64(files, roff + 8)) + path: str = name + if name is not None and _tb_is_abs_path(name) == 0: + didx: int = int(_tb_rd_u32(files, roff + 4)) + base: str = _tb_dir_lookup(dirs, dcount, didx) + if base is not None and string.strlen(base) > 0: + if _tb_is_abs_path(base) == 0: + root: str = _tb_dir_lookup(dirs, dcount, 0) + if root is not None and _tb_is_abs_path(root) != 0: base = _tb_join(mb, root, base) + path = _tb_join(mb, base, name) + if path is None or string.strlen(path) == 0: path = "??" + _tb_wr_u64(files, roff + 24, uint64(path)) + return path + + +def _tb_grow_rec(mb: memhub.MemBuddy*, buf: bytes, old_cap: int, rec_size: int, new_cap: int) -> bytes: + nb: bytes = mb.alloc(sizet(new_cap) * sizet(rec_size)) + if nb is None: return None + string.memcpy(nb, buf, sizet(old_cap) * sizet(rec_size)) + mb.free(buf) + return nb + + +def _tb_emit_row(mb: memhub.MemBuddy*, d: TbDump*, addr: uint64, path: str, line: int, fname: str = None) -> int: + if d is None: return 1 + if d.RowCount >= d.RowCap: + ncap: int = d.RowCap * 2 + nb: bytes = _tb_grow_rec(mb, d.RowBuf, d.RowCap, TB_ROW_SIZE, ncap) + if nb is None: + VLogger.error("--traceback: failed to grow DWARF line record table, aborting compilation", "TRACEBACK") + return 1 + d.RowBuf = nb + d.RowCap = ncap + off: sizet = sizet(d.RowCount) * TB_ROW_SIZE + _tb_wr_u64(d.RowBuf, off, addr) + _tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FILE, uint64(path)) + _tb_wr_u32(d.RowBuf, off + TB_ROW_OFF_LINE, uint64(line)) + fv: uint64 = 0 + if fname is not None: fv = uint64(fname) + _tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FUNC, fv) + d.RowCount = d.RowCount + 1 + d.RawRowCount = d.RawRowCount + 1 + return 0 + + +def _tb_emit_func(mb: memhub.MemBuddy*, d: TbDump*, low: uint64, high: uint64, name: str) -> int: + if d is None: return 1 + if d.FuncCount >= d.FuncCap: + ncap: int = d.FuncCap * 2 + nb: bytes = _tb_grow_rec(mb, d.FuncBuf, d.FuncCap, TB_FUNC_SIZE, ncap) + if nb is None: + VLogger.error("--traceback: failed to grow DWARF subprogram table, aborting compilation", "TRACEBACK") + return 1 + d.FuncBuf = nb + d.FuncCap = ncap + off: sizet = sizet(d.FuncCount) * TB_FUNC_SIZE + _tb_wr_u64(d.FuncBuf, off, low) + _tb_wr_u64(d.FuncBuf, off + 8, high) + _tb_wr_u64(d.FuncBuf, off + 16, uint64(name)) + d.FuncCount = d.FuncCount + 1 + return 0 + + +def _tb_func_name_for(fbuf: bytes, fcount: int, rva: uint64) -> str: + """bisect_right over the (sorted) func low_pc table, then range check.""" + if fbuf is None or fcount <= 0: return None + lo: int = 0 + hi: int = fcount + while lo < hi: + mid: int = lo + (hi - lo) / 2 + if _tb_rd_u64(fbuf, sizet(mid) * TB_FUNC_SIZE) <= rva: lo = mid + 1 + else: hi = mid + k: int = lo - 1 + if k < 0: return None + low: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE) + high: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 8) + if low <= rva and rva < high: return rawptr(_tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 16)) + return None + + +def _tb_intern(p: TbPool*, s: str) -> uint64: + """Intern s into the pool (linear scan; keeps offset 0 as the empty sentinel).""" + if p is None or s is None: return 0 + for k in range(int(p.OffCount)): + o: uint64 = _tb_rd_u64(p.Offs, sizet(k) * 8) + if string.strcmp(p.Pool + sizet(o), s) == 0: return o + off: uint64 = uint64(p.PoolLen) + n: sizet = string.strlen(s) + string.memcpy(p.Pool + sizet(off), s, n) + p.Pool[sizet(off) + n] = '\0' + p.PoolLen = p.PoolLen + int(n) + 1 + _tb_wr_u64(p.Offs, sizet(p.OffCount) * 8, off) + p.OffCount = p.OffCount + 1 + return off + + +def _tb_read_pe_layout(mb: memhub.MemBuddy*, exe_path: str) -> TbPeLayout*: + """Read (ImageBase, SizeOfImage, .text RVA) from the PE headers.""" + lay: TbPeLayout* = TbPeLayout() + if lay is None: return None + f: fileio.File* = fileio.File(exe_path, fileio.MODE.R) + if f.closed: return lay + HEAD: sizet = 4096 + head: bytes = mb.alloc(HEAD) + if head is None: + f.close() + return lay + got: LONG = f.read_all(head, HEAD) + f.close() + if got < 0x40: return lay + if _tb_cc(head, 0) != 77 or _tb_cc(head, 1) != 90: return lay + e_lfanew: uint64 = uint64(_tb_rd_u32(head, 0x3C)) + if e_lfanew + 24 > uint64(got): return lay + if _tb_cc(head, sizet(e_lfanew)) != 80 or _tb_cc(head, sizet(e_lfanew) + 1) != 69: return lay + coff: sizet = sizet(e_lfanew) + 4 + nsec: uint64 = uint64(_tb_rd_u16(head, coff + 2)) + optsz: uint64 = uint64(_tb_rd_u16(head, coff + 16)) + opt: sizet = coff + 20 + if uint64(opt) + 32 > uint64(got): return lay + magic: uint64 = uint64(_tb_rd_u16(head, opt)) + img_base: uint64 = 0 + if magic == 0x20B: img_base = _tb_rd_u64(head, opt + 24) + elif magic == 0x10B: img_base = uint64(_tb_rd_u32(head, opt + 28)) + else: return lay + size_img: uint64 = uint64(_tb_rd_u32(head, opt + 56)) + sec_off: sizet = opt + sizet(optsz) + text_rva: uint64 = 0x1000 + min_rva: uint64 = 0xFFFFFFFF + found_text: int = 0 + have_sec: int = 0 + for si in range(int(nsec)): + base: sizet = sec_off + sizet(si) * 40 + if uint64(base) + 40 > uint64(got): break + va: uint64 = uint64(_tb_rd_u32(head, base + 12)) + have_sec = 1 + if va < min_rva: min_rva = va + if _tb_cc(head, base) == 46 and _tb_cc(head, base + 1) == 116 and _tb_cc(head, base + 2) == 101 and _tb_cc(head, base + 3) == 120 and _tb_cc(head, base + 4) == 116: + text_rva = va + found_text = 1 + if found_text == 0 and have_sec != 0 and min_rva != 0xFFFFFFFF: text_rva = min_rva + lay.ImageBase = img_base + lay.SizeOfImage = size_img + lay.TextRva = text_rva + lay.Ok = 1 + return lay + + +def _tb_parse_dump(mb: memhub.MemBuddy*, dump: str) -> TbDump*: + """Parse llvm-dwarfdump text into a row table (addr, path, line) and a func table.""" + d: TbDump* = TbDump() + if d is None: return None + LINE_CAP: sizet = 8192 + d.RowBuf = mb.alloc(sizet(TB_ROW_CAP0) * TB_ROW_SIZE) + d.RowCap = TB_ROW_CAP0 + d.FuncBuf = mb.alloc(sizet(TB_FUNC_CAP0) * TB_FUNC_SIZE) + d.FuncCap = TB_FUNC_CAP0 + dirs: bytes = mb.alloc(sizet(TB_DIR_CAP) * TB_DIR_SIZE) + files: bytes = mb.alloc(sizet(TB_FILE_CAP) * TB_FILE_SIZE) + linebuf: bytes = mb.alloc(LINE_CAP) + if d.RowBuf is None or d.FuncBuf is None or dirs is None or files is None or linebuf is None: + VLogger.error("--traceback: failed to allocate dwarfdump parse buffer, aborting compilation", "TRACEBACK") + return None + + in_line: int = 0 + dir_count: int = 0 + file_count: int = 0 + pkind: int = 0 # 0=none, 1=file, 2=dir + pidx: int = 0 + pname: str = None + pdir: int = 0 + in_prog: int = 0 + cur_low: uint64 = 0 + cur_high: uint64 = 0 + cur_has_low: int = 0 + cur_has_high: int = 0 + cname: str = None + + dlen: sizet = string.strlen(dump) + pos: sizet = 0 + while pos < dlen: + ls: sizet = pos + while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1 + le: sizet = pos + if pos < dlen: pos += 1 + if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1 + ll: sizet = le - ls + if ll == 0: continue + if ll >= LINE_CAP: ll = LINE_CAP - 1 + string.strncpy(linebuf, dump + ls, ll) + linebuf[ll] = '\0' + st: str = _tb_ltrim(linebuf) + + # ---- line-table section gate ---- + if _tb_contains(st, "debug_line contents") != 0: + in_line = 1 + continue + if _tb_contains(st, "contents:") != 0 and _tb_contains(st, "debug_") != 0: + in_line = 0 + continue + + if in_line != 0: + if _tb_startswith(st, "debug_line[") != 0: + # new CU: commit pending file, reset both tables + if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) + file_count += 1 + pkind = 0 + pname = None + pdir = 0 + dir_count = 0 + file_count = 0 + continue + if _tb_startswith(st, "file_names[") != 0: + if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) + file_count += 1 + pname = None + pdir = 0 + pkind = 1 + pidx = _tb_bracket_idx(st) + continue + if _tb_startswith(st, "directory_names[") != 0 or _tb_startswith(st, "include_directories[") != 0: + if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) + file_count += 1 + pname = None + pdir = 0 + pidx = _tb_bracket_idx(st) + val: str = _tb_value_after_sep(_tb_after_bracket(st)) + if val is not None and _tb_len_trimmed(val) > 0: + if dir_count < TB_DIR_CAP: + _tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx)) + _tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(_tb_unquote(mb, val))) + dir_count += 1 + pkind = 0 + else: pkind = 2 + continue + if _tb_startswith(st, "name:") != 0 and pkind != 0: + val2: str = _tb_unquote(mb, _tb_ltrim(st + 5)) + if pkind == 1: pname = val2 + else: + if dir_count < TB_DIR_CAP and val2 is not None: + _tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx)) + _tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(val2)) + dir_count += 1 + pkind = 0 + continue + if _tb_startswith(st, "dir_index:") != 0 and pkind == 1: + # "dir_index:" is 10 chars with the colon; st + 9 would point at the colon and + # always parse 0 (losing dir indices). Use st + 10. + pdir = _tb_parse_dec(_tb_ltrim(st + 10)) + continue + if _tb_startswith(st, "Address") != 0 and _tb_contains(st, "Column") != 0: + if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) + file_count += 1 + pkind = 0 + pname = None + pdir = 0 + continue + if _tb_cc(linebuf, 0) == 48 and _tb_cc(linebuf, 1) == 120: + if pkind == 1 and pname is not None and file_count < TB_FILE_CAP: + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx)) + _tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname)) + _tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0)) + file_count += 1 + pkind = 0 + pname = None + pdir = 0 + ri: sizet = 2 + addr: uint64 = 0 + hd: int = _tb_hex_digit(_tb_cc(linebuf, ri)) + while hd >= 0: + addr = (addr << 4) | uint64(hd) + ri += 1 + hd = _tb_hex_digit(_tb_cc(linebuf, ri)) + while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1 + lno: int = 0 + while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: + lno = lno * 10 + (_tb_cc(linebuf, ri) - 48) + ri += 1 + while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1 + while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: ri += 1 + while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1 + fidx: int = 0 + while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: + fidx = fidx * 10 + (_tb_cc(linebuf, ri) - 48) + ri += 1 + if lno > 0: + frec: int = _tb_file_rec(files, file_count, fidx) + fpath: str = None + if frec >= 0: fpath = _tb_resolve_file(mb, files, file_count, dirs, dir_count, frec) + if fpath is None: fpath = "??" + if _tb_emit_row(mb, d, addr, fpath, lno) != 0: return None + continue + + # ---- DWARF debug_info subprogram parsing ---- + if _tb_contains(linebuf, "DW_TAG_") != 0: + if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0: + hi: uint64 = cur_low + 1 + if cur_has_high != 0: + hi = cur_high + if hi <= cur_low: hi = cur_low + cur_high + if hi > cur_low: + if _tb_emit_func(mb, d, cur_low, hi, cname) != 0: return None + in_prog = 0 + cur_low = 0 + cur_high = 0 + cur_has_low = 0 + cur_has_high = 0 + cname = None + if _tb_contains(linebuf, "DW_TAG_subprogram") != 0: in_prog = 1 + continue + if in_prog == 0: continue + if _tb_contains(linebuf, "DW_AT_low_pc") != 0: + cur_low = _tb_hex_in_parens(linebuf, "DW_AT_low_pc") + cur_has_low = 1 + continue + if _tb_contains(linebuf, "DW_AT_high_pc") != 0: + cur_high = _tb_hex_in_parens(linebuf, "DW_AT_high_pc") + cur_has_high = 1 + continue + if _tb_contains(linebuf, "DW_AT_name") != 0 and cname is None: + cname = _tb_quoted_after(mb, linebuf, "DW_AT_name") + continue + + # trailing subprogram + if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0: + hi2: uint64 = cur_low + 1 + if cur_has_high != 0: + hi2 = cur_high + if hi2 <= cur_low: hi2 = cur_low + cur_high + if hi2 > cur_low: + if _tb_emit_func(mb, d, cur_low, hi2, cname) != 0: return None + return d + + +# --traceback call-site normalisation: .vpcall + llvm-objdump. The MLIR backend emits no DWARF +# for our .vp functions, so each direct callq's return address is mapped back to file/line/function. + +# call record: u64 caller | u64 callee | u64 file | u32 line | u32 next (next in group, NONE=tail) +define TB_CR_SIZE = 32 +define TB_CR_OFF_CALLER = 0 +define TB_CR_OFF_CALLEE = 8 +define TB_CR_OFF_FILE = 16 +define TB_CR_OFF_LINE = 24 +define TB_CR_OFF_NEXT = 28 +# function record: u64 sym | u64 file | u32 defline | u32 pad +define TB_FR_SIZE = 24 +define TB_FR_OFF_SYM = 0 +define TB_FR_OFF_FILE = 8 +define TB_FR_OFF_DEFLINE = 16 +# group hash slots (keyed by (caller,callee); open addressing + linear probing): +# u64 caller | u64 callee | u32 used | u32 head | u32 tail | u32 cursor +define TB_GRP_SIZE = 32 +define TB_GRP_OFF_CALLER = 0 +define TB_GRP_OFF_CALLEE = 8 +define TB_GRP_OFF_USED = 16 +define TB_GRP_OFF_HEAD = 20 +define TB_GRP_OFF_TAIL = 24 +define TB_GRP_OFF_CURSOR = 28 +# invalid-index sentinel (all-ones u32) +TB_IDX_NONE: uint64 = 0xFFFFFFFF + + +class TbCallParse: + Cr: bytes # array of TB_CR_SIZE records + CrCap: int + CrN: int + Fr: bytes # array of TB_FR_SIZE records + FrCap: int + FrN: int + Grp: bytes # array of TB_GRP_SIZE slots + GrpCap: int + + def __new__(self) -> t.CPtr: + return t.CPtr(_mbuddy.alloc(TbCallParse.__sizeof__())) + + def __init__(self): + self.Cr = None + self.CrCap = 0 + self.CrN = 0 + self.Fr = None + self.FrCap = 0 + self.FrN = 0 + self.Grp = None + self.GrpCap = 0 + + +def _tb_next_pow2(n: int) -> int: + p: int = 16 + while p < n: p = p * 2 + return p + + +def _tb_strhash(s: str) -> uint64: + """FNV-1a hash (32-bit offset basis, 64-bit prime) used only for table addressing.""" + h: uint64 = 0x811C9DC5 + if s is None: return h + i: sizet = 0 + while _tb_cc(s, i) != 0: + h = (h ^ uint64(_tb_cc(s, i))) * 0x100000001B3 + i += 1 + return h + + +def _tb_grp_get(grp: bytes, cap: int, caller: str, callee: str, do_insert: int) -> int: + """Look up a slot by (caller, callee); insert on miss when do_insert=1; slot or -1.""" + if grp is None or cap <= 0 or caller is None or callee is None: return -1 + mask: uint64 = uint64(cap - 1) + hv: uint64 = (_tb_strhash(caller) * 0x100000001B3 + _tb_strhash(callee)) & mask + for i in range(int(cap)): + slot: int = int((hv + uint64(i)) & mask) + off: sizet = sizet(slot) * TB_GRP_SIZE + if uint64(_tb_rd_u32(grp, off + TB_GRP_OFF_USED)) == 0: + if do_insert == 0: return -1 + _tb_wr_u32(grp, off + TB_GRP_OFF_USED, uint64(1)) + _tb_wr_u64(grp, off + TB_GRP_OFF_CALLER, uint64(caller)) + _tb_wr_u64(grp, off + TB_GRP_OFF_CALLEE, uint64(callee)) + _tb_wr_u32(grp, off + TB_GRP_OFF_HEAD, uint64(TB_IDX_NONE)) + _tb_wr_u32(grp, off + TB_GRP_OFF_TAIL, uint64(TB_IDX_NONE)) + _tb_wr_u32(grp, off + TB_GRP_OFF_CURSOR, uint64(TB_IDX_NONE)) + return slot + sc: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLER)) + sk: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLEE)) + if sc is not None and sk is not None and string.strcmp(sc, caller) == 0 and string.strcmp(sk, callee) == 0: return slot + return -1 + + +def _tb_cp_init(mb: memhub.MemBuddy*) -> TbCallParse*: + cp: TbCallParse* = TbCallParse() + if cp is None: return None + cp.CrCap = 1024 + cp.FrCap = 1024 + cp.Cr = mb.alloc(sizet(cp.CrCap) * TB_CR_SIZE) + cp.Fr = mb.alloc(sizet(cp.FrCap) * TB_FR_SIZE) + if cp.Cr is None or cp.Fr is None: return None + cp.CrN = 0 + cp.FrN = 0 + return cp + + +def _tb_cp_add_cr(mb: memhub.MemBuddy*, cp: TbCallParse*, caller: str, callee: str, file: str, line: int) -> int: + if cp.CrN >= cp.CrCap: + ncap: int = cp.CrCap * 2 + nb: bytes = _tb_grow_rec(mb, cp.Cr, cp.CrCap, TB_CR_SIZE, ncap) + if nb is None: + VLogger.error("--traceback: failed to grow call record table, aborting compilation", "TRACEBACK") + return 1 + cp.Cr = nb + cp.CrCap = ncap + off: sizet = sizet(cp.CrN) * TB_CR_SIZE + _tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLER, uint64(caller)) + _tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLEE, uint64(callee)) + _tb_wr_u64(cp.Cr, off + TB_CR_OFF_FILE, uint64(file)) + _tb_wr_u32(cp.Cr, off + TB_CR_OFF_LINE, uint64(line)) + _tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE)) + cp.CrN = cp.CrN + 1 + return 0 + + +def _tb_cp_add_fr(mb: memhub.MemBuddy*, cp: TbCallParse*, sym: str, file: str, defline: int) -> int: + if cp.FrN >= cp.FrCap: + ncap: int = cp.FrCap * 2 + nb: bytes = _tb_grow_rec(mb, cp.Fr, cp.FrCap, TB_FR_SIZE, ncap) + if nb is None: + VLogger.error("--traceback: failed to grow function record table, aborting compilation", "TRACEBACK") + return 1 + cp.Fr = nb + cp.FrCap = ncap + off: sizet = sizet(cp.FrN) * TB_FR_SIZE + _tb_wr_u64(cp.Fr, off + TB_FR_OFF_SYM, uint64(sym)) + _tb_wr_u64(cp.Fr, off + TB_FR_OFF_FILE, uint64(file)) + _tb_wr_u32(cp.Fr, off + TB_FR_OFF_DEFLINE, uint64(defline)) + cp.FrN = cp.FrN + 1 + return 0 + + +def _tb_read_whole(mb: memhub.MemBuddy*, path: str) -> str: + """Read the whole file into a NUL-terminated string; None on failure.""" + f: fileio.File* = fileio.File(path, fileio.MODE.R) + if f.closed: return None + sz: LONGLONG = f.size() + if sz < 0: + f.close() + return None + buf: str = mb.alloc(sizet(sz) + 2) + if buf is None: + f.close() + return None + # 容量全程用 sizet(与 sz 同宽):旧写法 ULONG(sz) + ULONG(2) 会在 sz >= 4GiB 时静默回绕 + got: LONG = f.read_all(buf, sizet(sz) + 2) + f.close() + if got < 0: return None + return buf + + +def _tb_parse_f_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int: + """Parse an F record line with symbol, defline and file fields (tab-separated).""" + p: sizet = a + while p < b and _tb_cc(s, p) != 9: p += 1 + if p >= b: return 0 + p += 1 + f2: sizet = p + while f2 < b and _tb_cc(s, f2) != 9: f2 += 1 + if f2 >= b: return 0 + sym: str = _tb_dup_span(mb, s + p, f2 - p) + p = f2 + 1 + f3: sizet = p + while f3 < b and _tb_cc(s, f3) != 9: f3 += 1 + if f3 >= b: return 0 + defline: int = int(_tb_parse_dec(s + p)) + p = f3 + 1 + file: str = _tb_dup_span(mb, s + p, b - p) + if sym is None or file is None: return 0 + return _tb_cp_add_fr(mb, cp, sym, file, defline) + + +def _tb_parse_c_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int: + """Parse a C record line with caller, callee, line and file fields (tab-separated).""" + p: sizet = a + while p < b and _tb_cc(s, p) != 9: p += 1 + if p >= b: return 0 + p += 1 + e2: sizet = p + while e2 < b and _tb_cc(s, e2) != 9: e2 += 1 + if e2 >= b: return 0 + caller: str = _tb_dup_span(mb, s + p, e2 - p) + p = e2 + 1 + e3: sizet = p + while e3 < b and _tb_cc(s, e3) != 9: e3 += 1 + if e3 >= b: return 0 + callee: str = _tb_dup_span(mb, s + p, e3 - p) + p = e3 + 1 + e4: sizet = p + while e4 < b and _tb_cc(s, e4) != 9: e4 += 1 + if e4 >= b: return 0 + line: int = int(_tb_parse_dec(s + p)) + p = e4 + 1 + file: str = _tb_dup_span(mb, s + p, b - p) + if caller is None or callee is None or file is None: return 0 + return _tb_cp_add_cr(mb, cp, caller, callee, file, line) + + +def _tb_parse_vpcall_file(mb: memhub.MemBuddy*, path: str, cp: TbCallParse*) -> int: + whole: str = _tb_read_whole(mb, path) + if whole is None: + VLogger.error(f"--traceback: failed to read .vpcall: {path}, aborting compilation", "TRACEBACK") + return 1 + n: sizet = string.strlen(whole) + pos: sizet = 0 + while pos < n: + ls: sizet = pos + while pos < n and _tb_cc(whole, pos) != 10: pos += 1 + le: sizet = pos + if pos < n: pos += 1 + if le > ls and _tb_cc(whole, le - 1) == 13: le -= 1 + if le > ls: + typ: int = _tb_cc(whole, ls) + if typ == 70: + if _tb_parse_f_line(mb, whole, ls, le, cp) != 0: return 1 + elif typ == 67: + if _tb_parse_c_line(mb, whole, ls, le, cp) != 0: return 1 + return 0 + + +def _tb_load_vpcalls(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int: + if _tb_vpcall_buf is None or _tb_vpcall_n <= 0: return 0 + for k in range(int(_tb_vpcall_n)): + slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(k) * 8) + path: str = slot[0] + if path is not None: + if _tb_parse_vpcall_file(mb, path, cp) != 0: return 1 + return 0 + + +def _tb_build_groups(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int: + """Chain all C records into group lists keyed by (caller, callee) (head/tail/cursor).""" + if cp.CrN <= 0: return 0 + cp.GrpCap = _tb_next_pow2(cp.CrN * 2 + 16) + cp.Grp = mb.alloc(sizet(cp.GrpCap) * TB_GRP_SIZE) + if cp.Grp is None: + VLogger.error("--traceback: failed to allocate call-site group table, aborting compilation", "TRACEBACK") + return 1 + string.memset(cp.Grp, 0, sizet(cp.GrpCap) * TB_GRP_SIZE) + for i in range(int(cp.CrN)): + off: sizet = sizet(i) * TB_CR_SIZE + caller: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLER)) + callee: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLEE)) + slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 1) + if slot < 0: + VLogger.error("--traceback: call-site group hash table overflow, aborting compilation", "TRACEBACK") + return 1 + goff: sizet = sizet(slot) * TB_GRP_SIZE + tailv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_TAIL)) + if tailv == uint64(TB_IDX_NONE): + _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_HEAD, uint64(i)) + _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, uint64(i)) + else: + toff: sizet = sizet(int(tailv)) * TB_CR_SIZE + _tb_wr_u32(cp.Cr, toff + TB_CR_OFF_NEXT, uint64(i)) + _tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE)) + _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_TAIL, uint64(i)) + return 0 + + +def _tb_emit_call_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, caller: str, callee: str, ret_va: uint64) -> int: + """Emit a row for a return address from the next unconsumed C record of (caller, callee).""" + if caller is None or callee is None or cp.Grp is None or ret_va < img_lo: return 0 + slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 0) + if slot < 0: return 0 + goff: sizet = sizet(slot) * TB_GRP_SIZE + curv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR)) + if curv == uint64(TB_IDX_NONE): return 0 + ci: int = int(curv) + coff: sizet = sizet(ci) * TB_CR_SIZE + file: str = rawptr(_tb_rd_u64(cp.Cr, coff + TB_CR_OFF_FILE)) + line: int = int(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_LINE)) + nxt: uint64 = uint64(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_NEXT)) + _tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, nxt) + return _tb_emit_row(mb, d, ret_va, file, line, caller) + + +def _tb_emit_entry_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, addr: uint64, sym: str) -> int: + """Emit a fallback function-entry row for an objdump label sym (file/line from the F record).""" + if sym is None or cp.Fr is None or cp.FrN <= 0 or addr < img_lo: return 0 + for i in range(int(cp.FrN)): + off: sizet = sizet(i) * TB_FR_SIZE + s: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_SYM)) + if s is not None and string.strcmp(s, sym) == 0: + file: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_FILE)) + dl: int = int(_tb_rd_u32(cp.Fr, off + TB_FR_OFF_DEFLINE)) + return _tb_emit_row(mb, d, addr, file, dl, sym) + return 0 + + +def _tb_hex_span(s: str, a: sizet, b: sizet) -> uint64: + v: uint64 = 0 + i: sizet = a + while i < b: + hd: int = _tb_hex_digit(_tb_cc(s, i)) + if hd < 0: break + v = (v << 4) | uint64(hd) + i += 1 + return v + + +def _tb_run_objdump(mb: memhub.MemBuddy*, exe_path: str) -> str: + cl: sizet = string.strlen(exe_path) + 64 + cmd: bytes = mb.alloc(cl) + if cmd is None: + VLogger.error("--traceback: failed to allocate objdump command buffer, aborting compilation", "TRACEBACK") + return None + viperlib.snprintf(cmd, cl, "llvm-objdump -d \"%s\"", exe_path) + res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) + if res is None or res.returncode != 0: + rc: int = -1 + if res is not None: rc = res.returncode + VLogger.error(f"--traceback: failed to run llvm-objdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK") + return None + dump: str = res.stdout + if dump is None: + VLogger.error("--traceback: llvm-objdump produced no output, aborting compilation", "TRACEBACK") + return None + return dump + + +def _tb_scan_objdump(mb: memhub.MemBuddy*, dump: str, img_lo: uint64, d: TbDump*, cp: TbCallParse*) -> int: + """Parse llvm-objdump -d text, match C/F records and write our rows into d.""" + dlen: sizet = string.strlen(dump) + pos: sizet = 0 + in_text: int = 0 + cur_func: str = None + prev_is_call: int = 0 + prev_callee: str = None + prev_func: str = None + while pos < dlen: + ls: sizet = pos + while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1 + le: sizet = pos + if pos < dlen: pos += 1 + if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1 + if le <= ls: continue + if _tb_startswith_span(dump, ls, le, "Disassembly of section") != 0: + # section change: a pending callq at the previous section's end has no valid return address + prev_is_call = 0 + if _tb_contains_span(dump, ls, le, ".text") != 0: in_text = 1 + else: in_text = 0 + continue + if in_text == 0: continue + c0: int = _tb_cc(dump, ls) + is_hex0: int = 0 + if (c0 >= 48 and c0 <= 57) or (c0 >= 97 and c0 <= 102) or (c0 >= 65 and c0 <= 70): is_hex0 = 1 + if is_hex0 == 0: continue + # first space + sp: sizet = ls + while sp < le and _tb_cc(dump, sp) != 32: sp += 1 + if sp + 1 < le and _tb_cc(dump, sp + 1) == 60: + # ---- label line `ADDR :` ---- + laddr: uint64 = _tb_hex_span(dump, ls, sp) + if prev_is_call != 0: + _tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, laddr) + prev_is_call = 0 + lb: sizet = sp + 2 + j: sizet = lb + while j < le and _tb_cc(dump, j) != 62: j += 1 + cur_func = _tb_dup_span(mb, dump + lb, j - lb) + if cur_func is not None: _tb_emit_entry_row(mb, d, cp, img_lo, laddr, cur_func) + continue + # ---- instruction line `ADDR: bytes mnemonic operands` ---- + k: sizet = ls + while k < le and _tb_cc(dump, k) != 58: k += 1 + if k >= le: continue + iaddr: uint64 = _tb_hex_span(dump, ls, k) + if prev_is_call != 0: + _tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, iaddr) + prev_is_call = 0 + # skip byte tokens (exactly 2 hex chars) and locate the mnemonic start + p: sizet = k + 1 + while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1 + ms: sizet = p + while p < le: + ts: sizet = p + while p < le and _tb_cc(dump, p) != 32 and _tb_cc(dump, p) != 9: p += 1 + tl: sizet = p - ts + ishex: int = 0 + if tl == 2 and _tb_hex_digit(_tb_cc(dump, ts)) >= 0 and _tb_hex_digit(_tb_cc(dump, ts + 1)) >= 0: ishex = 1 + ms = ts + if ishex == 0: break + while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1 + me: sizet = p + mlen: sizet = me - ms + is_call: int = 0 + if mlen == 5 and string.strncmp(dump + ms, "callq", 5) == 0: is_call = 1 + elif mlen == 4 and string.strncmp(dump + ms, "call", 4) == 0: is_call = 1 + if is_call != 0: + os: sizet = me + while os < le and (_tb_cc(dump, os) == 32 or _tb_cc(dump, os) == 9): os += 1 + if os + 1 < le and _tb_cc(dump, os) == 48 and _tb_cc(dump, os + 1) == 120: + # direct call: extract `` + ab: sizet = os + while ab < le and _tb_cc(dump, ab) != 60: ab += 1 + if ab < le: + cb: sizet = ab + 1 + ce: sizet = cb + while ce < le and _tb_cc(dump, ce) != 62: ce += 1 + xe: sizet = cb + while xe < ce and _tb_cc(dump, xe) != 43: xe += 1 + prev_callee = _tb_dup_span(mb, dump + cb, xe - cb) + prev_func = cur_func + prev_is_call = 1 + return 0 + + +def WriteStackMap(mb: memhub.MemBuddy*, exe_path: str) -> int: + """--traceback: export .stackmap after a successful link. 0 = ok.""" + if mb is None or exe_path is None: + VLogger.error("--traceback: WriteStackMap argument is null, aborting compilation", "TRACEBACK") + return 1 + + lay: TbPeLayout* = _tb_read_pe_layout(mb, exe_path) + if lay is None or lay.Ok == 0: + VLogger.error(f"--traceback: only Windows PE output is supported, but {exe_path} is not a valid PE (required by --traceback)", "TRACEBACK") + return 1 + size_of_image: uint64 = lay.SizeOfImage + if size_of_image == 0: + VLogger.error(f"--traceback: read PE.SizeOfImage is not positive ({size_of_image}), cannot bound RVA range (required by --traceback)", "TRACEBACK") + return 1 + img_lo: uint64 = lay.ImageBase + img_hi: uint64 = lay.ImageBase + size_of_image + text_rva: uint64 = lay.TextRva + + # ---- 1) run llvm-dwarfdump (bare name: CreateProcessA resolves it via PATH) ---- + cmd_len: sizet = string.strlen(exe_path) + 64 + cmd: bytes = mb.alloc(cmd_len) + if cmd is None: + VLogger.error("--traceback: failed to allocate dwarfdump command buffer, aborting compilation", "TRACEBACK") + return 1 + viperlib.snprintf(cmd, cmd_len, "llvm-dwarfdump --debug-line --debug-info \"%s\"", exe_path) + res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True) + if res is None or res.returncode != 0: + rc: int = -1 + if res is not None: rc = res.returncode + VLogger.error(f"--traceback: failed to run llvm-dwarfdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK") + return 1 + dump: str = res.stdout + if dump is None: + VLogger.error("--traceback: llvm-dwarfdump produced no output, aborting compilation", "TRACEBACK") + return 1 + + d: TbDump* = _tb_parse_dump(mb, dump) + if d is None: return 1 + + # ---- 1b) our call-site rows via .vpcall + llvm-objdump ---- + # DWARF rows only cover the linked mingw CRT; .vpcall + objdump add return-address -> call-site rows. + if _tb_vpcall_n > 0: + cp: TbCallParse* = _tb_cp_init(mb) + if cp is None: + VLogger.error("--traceback: failed to allocate call-site parse state, aborting compilation", "TRACEBACK") + return 1 + if _tb_load_vpcalls(mb, cp) != 0: return 1 + if _tb_build_groups(mb, cp) != 0: return 1 + odump: str = _tb_run_objdump(mb, exe_path) + if odump is None: return 1 + our_before: int = d.RowCount + if _tb_scan_objdump(mb, odump, img_lo, d, cp) != 0: return 1 + our_added: int = d.RowCount - our_before + if our_added > 0: VLogger.success(f"--traceback: from {_tb_vpcall_n} .vpcall files generated {our_added} own call-site records (F={cp.FrN} functions, C={cp.CrN} calls)", "TRACEBACK") + else: VLogger.warning(f"--traceback: read {_tb_vpcall_n} .vpcall files (F={cp.FrN}, C={cp.CrN}) but matched no objdump call sites -- own frame line numbers still missing, falling back to DWARF", "TRACEBACK") + else: VLogger.warning("--traceback: no .vpcall registered (own modules have no call-site records), falling back to DWARF behavior", "TRACEBACK") + + if d.RawRowCount == 0: + VLogger.error(f"--traceback: could not parse any line records from {exe_path} (DWARF .debug_line is empty and there are no own .vpcall records; required by --traceback)", "TRACEBACK") + return 1 + + # ---- 2) rows: normalise to RVA, same rva keeps the largest line ---- + n: int = d.RowCount + dropped_rows: int = 0 + kept: int = 0 + i: int = 0 + while i < n: + a: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE) + if a < img_lo or a >= img_hi: + dropped_rows += 1 + i += 1 + continue + if kept != i: + _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo) + _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FILE, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)) + _tb_wr_u32(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_LINE, uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE))) + _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)) + else: _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo) + kept += 1 + i += 1 + tmp: bytes = mb.alloc(TB_ROW_SIZE) + if tmp is None: + VLogger.error("--traceback: failed to allocate line record sort buffer, aborting compilation", "TRACEBACK") + return 1 + si2: int = 1 + while si2 < kept: + string.memcpy(tmp, d.RowBuf + sizet(si2) * TB_ROW_SIZE, TB_ROW_SIZE) + sj: int = si2 + while sj > 0: + if _tb_rd_u64(d.RowBuf, sizet(sj - 1) * TB_ROW_SIZE) <= _tb_rd_u64(tmp, 0): break + string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, d.RowBuf + sizet(sj - 1) * TB_ROW_SIZE, TB_ROW_SIZE) + sj -= 1 + string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, tmp, TB_ROW_SIZE) + si2 += 1 + uniq: int = 0 + i = 0 + while i < kept: + rv: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE) + bl: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE)) + bp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE) + bf: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) + j: int = int(i + 1) + while j < kept and _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE) == rv: + lj: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_LINE)) + if lj > bl: + bl = lj + bp = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FILE) + bf = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) + j += 1 + if uniq != i: + _tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE, rv) + _tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FILE, bp) + _tb_wr_u32(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_LINE, bl) + _tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, bf) + uniq += 1 + i = j + if dropped_rows > 0: VLogger.warning(f"--traceback: dropped {dropped_rows} DWARF line records that cannot be normalised (address not in image VA range, mostly COMDAT dedup copies)", "TRACEBACK") + if uniq == 0: + VLogger.error(f"--traceback: all line records from {exe_path} cannot be normalised to runtime RVA range, aborting compilation", "TRACEBACK") + return 1 + + # ---- 3) subprograms: normalise + sort by low_rva ---- + fcount: int = int(d.FuncCount) + fkept: int = 0 + dropped_funcs: int = 0 + fi: int = 0 + while fi < fcount: + low: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE) + if low < img_lo or low >= img_hi: + dropped_funcs += 1 + fi += 1 + continue + high: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 8) + lo_rva: uint64 = low - img_lo + hi_rva: uint64 = high - img_lo + if hi_rva <= lo_rva: hi_rva = lo_rva + 1 + if hi_rva > size_of_image: hi_rva = size_of_image + _tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE, lo_rva) + _tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 8, hi_rva) + _tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 16, _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 16)) + fkept += 1 + fi += 1 + ftmp: bytes = mb.alloc(TB_FUNC_SIZE) + if ftmp is None: + VLogger.error("--traceback: failed to allocate subprogram sort buffer, aborting compilation", "TRACEBACK") + return 1 + fj: int = 1 + while fj < fkept: + string.memcpy(ftmp, d.FuncBuf + sizet(fj) * TB_FUNC_SIZE, TB_FUNC_SIZE) + fk: int = fj + while fk > 0: + if _tb_rd_u64(d.FuncBuf, sizet(fk - 1) * TB_FUNC_SIZE) <= _tb_rd_u64(ftmp, 0): break + string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, d.FuncBuf + sizet(fk - 1) * TB_FUNC_SIZE, TB_FUNC_SIZE) + fk -= 1 + string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, ftmp, TB_FUNC_SIZE) + fj += 1 + if dropped_funcs > 0: VLogger.warning(f"--traceback: dropped {dropped_funcs} DWARF subprograms that cannot be normalised (low_pc not in image VA range)", "TRACEBACK") + + # ---- 4) string pool: exact upper bound, then intern (offset 0 = sentinel) ---- + pool_need: sizet = 1 + i = 0 + while i < uniq: + pth: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)) + pool_need += string.strlen(pth) + 1 + i += 1 + # our rows carry the function name directly (string pool); DWARF rows take it from the subprogram table. + i = 0 + while i < uniq: + fp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) + if fp != 0: + fns: str = rawptr(fp) + if fns is not None: pool_need += string.strlen(fns) + 1 + i += 1 + i = 0 + while i < fkept: + nm0: str = rawptr(_tb_rd_u64(d.FuncBuf, sizet(i) * TB_FUNC_SIZE + 16)) + if nm0 is not None: pool_need += string.strlen(nm0) + 1 + i += 1 + p: TbPool* = TbPool() + if p is None: + VLogger.error("--traceback: failed to allocate string pool object, aborting compilation", "TRACEBACK") + return 1 + p.Pool = mb.alloc(pool_need) + p.Offs = mb.alloc((sizet(uniq) * 2 + sizet(fkept)) * 8 + 64) + if p.Pool is None or p.Offs is None: + VLogger.error("--traceback: failed to allocate string pool buffer, aborting compilation", "TRACEBACK") + return 1 + p.Pool[0] = '\0' + p.PoolLen = 1 + _tb_wr_u64(p.Offs, 0, uint64(0)) + p.OffCount = 1 + # file_off / func_off per unique row (u32 each) + ro: bytes = mb.alloc(sizet(uniq) * 8 + 8) + if ro is None: + VLogger.error("--traceback: failed to allocate record offset buffer, aborting compilation", "TRACEBACK") + return 1 + for i in range(int(uniq)): + pth2: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)) + foff: uint64 = _tb_intern(p, pth2) + noff: uint64 = 0 + rv2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE) + fp2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC) + nm2: str = None + if fp2 != 0: nm2 = rawptr(fp2) + else: nm2 = _tb_func_name_for(d.FuncBuf, fkept, rv2) + if nm2 is not None: noff = _tb_intern(p, nm2) + _tb_wr_u32(ro, sizet(i) * 8, foff) + _tb_wr_u32(ro, sizet(i) * 8 + 4, noff) + + # ---- 5) self-checks (mirror the runtime LoadMapFile validation) ---- + prev_rva: uint64 = 0 + outside: int = 0 + plausible: int = 0 + ci: int = 0 + while ci < uniq: + cv: uint64 = _tb_rd_u64(d.RowBuf, sizet(ci) * TB_ROW_SIZE) + if ci > 0 and cv <= prev_rva: + VLogger.error(f"--traceback self-check failed: record RVAs not strictly ascending (record {ci} would break runtime binary search), aborting compilation", "TRACEBACK") + return 1 + prev_rva = cv + if cv >= size_of_image: outside += 1 + if cv >= text_rva and cv < size_of_image: plausible += 1 + ci += 1 + if outside > 0: + VLogger.error(f"--traceback self-check failed: {outside} records have RVA outside [0, SizeOfImage=0x{size_of_image:x}), aborting compilation", "TRACEBACK") + return 1 + if plausible <= 0: + VLogger.error("--traceback self-check failed: no record RVA falls in the range possible at runtime (RVA basis likely wrong), aborting compilation", "TRACEBACK") + return 1 + # file_off must be >= 1: offset 0 is the empty-string sentinel and the runtime + # rejects a whole map whose record file_off is 0. + for ci in range(int(uniq)): + fo: uint64 = uint64(_tb_rd_u32(ro, sizet(ci) * 8)) + if fo == 0: + VLogger.error("--traceback self-check failed: a record has file_off==0 (empty path; runtime would reject that sidecar file), aborting compilation", "TRACEBACK") + return 1 + + # ---- 6) serialise and write .stackmap ---- + total: sizet = 20 + sizet(uniq) * 20 + sizet(p.PoolLen) + out: bytes = mb.alloc(total) + if out is None: + VLogger.error("--traceback: failed to allocate sidecar output buffer, aborting compilation", "TRACEBACK") + return 1 + out[0] = 'T' + out[1] = 'P' + out[2] = 'C' + out[3] = 'S' + _tb_wr_u32(out, 4, uint64(1)) + _tb_wr_u32(out, 8, uint64(uniq)) + _tb_wr_u32(out, 12, uint64(p.OffCount)) + _tb_wr_u32(out, 16, uint64(p.PoolLen)) + for ri2 in range(int(uniq)): + roff2: sizet = 20 + sizet(ri2) * 20 + rv3: uint64 = _tb_rd_u64(d.RowBuf, sizet(ri2) * TB_ROW_SIZE) + ln3: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(ri2) * TB_ROW_SIZE + TB_ROW_OFF_LINE)) + _tb_wr_u64(out, roff2, rv3) + _tb_wr_u32(out, roff2 + 8, uint64(_tb_rd_u32(ro, sizet(ri2) * 8))) + _tb_wr_u32(out, roff2 + 12, ln3) + _tb_wr_u32(out, roff2 + 16, uint64(_tb_rd_u32(ro, sizet(ri2) * 8 + 4))) + string.memcpy(out + 20 + sizet(uniq) * 20, p.Pool, sizet(p.PoolLen)) + + map_len: sizet = string.strlen(exe_path) + 16 + map_path: str = mb.alloc(map_len) + if map_path is None: + VLogger.error("--traceback: failed to allocate sidecar path buffer, aborting compilation", "TRACEBACK") + return 1 + viperlib.snprintf(map_path, map_len, "%s.stackmap", exe_path) + mf: fileio.File* = fileio.File(map_path, fileio.MODE.W) + if mf.closed: + VLogger.error(f"--traceback: failed to write sidecar file (cannot open {map_path}), aborting compilation", "TRACEBACK") + return 1 + mf.write(out, total) + mf.close() + VLogger.success(f"--traceback: wrote address-to-source-line sidecar {map_path} ({uniq} records, {total} bytes)", "TRACEBACK") + return 0 + + +def run_pipeline(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> BuildResult*: + """Run the full compile pipeline; returns a BuildResult object.""" + result: BuildResult* = BuildResult() + if result is None: return None + + # Step 0: ensure temp/output dirs exist (auto-create to avoid write failures) + ensure_dir(temp_dir) + ensure_dir(output_dir) + + # Step 1: write the .ll file + ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name) + if ret != 0: + result.Success = 0 + result.ErrorMsg = "failed to write .ll file" + return result + + # Step 2: llc-compile .ll -> .obj. + # build the sliced .ll file path + ir_path: str = build_sliced_path(temp_dir, module_name, "ll") + if ir_path is None: + result.Success = 0 + result.ErrorMsg = "memory allocation failed" + return result + + ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags) + if ret != 0: + result.Success = 0 + result.ErrorMsg = "llc compilation failed" + return result + + # Step 3: clang++ link .obj -> .exe (plus includes.binary prebuilt .obj and static/dynamic libs) + ret = link_obj_to_exe(output_dir, module_name, linker_cmd, linker_flags, linker_output, includes_binary_dir, linker_static_libs, linker_dynamic_libs) + if ret != 0: + result.Success = 0 + result.ErrorMsg = "link failed" + return result + + result.Success = 1 + return result + + +# ============================================================================= +# Runtime dependency deployment (project.vpj runtime.*) +# After a successful link, copy the declared runtime deps next to the exe. +# Optional: no-op unless runtime.qt6_path / qt6_dlls / qt6_plugins are declared. +# Fail-fast: a declared source that is missing, or a failed copy/dir-create, aborts the build. +# ============================================================================= + +def _deploy_dir_of(path: str) -> str: + """Return the directory portion of path (text before the last / or backslash); None if no separator.""" + if path is None: return None + plen: sizet = string.strlen(path) + i: sizet = plen + while i > 0: + if path[i - 1] == '/' or path[i - 1] == '\\': break + i -= 1 + if i == 0: return None + buf: str = _mbuddy.alloc(i) + if buf is None: return None + string.strncpy(buf, path, i - 1) + buf[i - 1] = '\0' + return buf + + +def _deploy_join(base: str, name: str) -> str: + """Join base and name with '/'; mbuddy-allocated.""" + if base is None or name is None: return None + bl: sizet = string.strlen(base) + nl: sizet = string.strlen(name) + buf: str = _mbuddy.alloc(bl + nl + 2) + if buf is None: return None + viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name) + return buf + + +def _deploy_token(s: str, start: sizet, end: sizet) -> str: + """Copy s[start:end] into an mbuddy-allocated NUL-terminated string; None on OOM.""" + n: sizet = end - start + buf: str = _mbuddy.alloc(n + 1) + if buf is None: return None + string.strncpy(buf, s + start, n) + buf[n] = '\0' + return buf + + +def deploy_runtime_deps(exe_path: str) -> int: + """Deploy declared runtime dependencies (project.vpj runtime.*) next to the exe; 0=ok, 1=fail. + No-op when runtime.qt6_path / qt6_dlls / qt6_plugins are undeclared. Fail-fast otherwise.""" + if exe_path is None: return 0 + qt6_path: str = Config.Qt6Path + if qt6_path is None: return 0 + dlls: str = Config.Qt6Dlls + plugins: str = Config.Qt6Plugins + if dlls is None and plugins is None: return 0 + + exe_dir: str = _deploy_dir_of(exe_path) + if exe_dir is None: + VLogger.error(f"runtime deploy: cannot determine exe directory from {exe_path}", "runtime") + return 1 + bin_dir: str = _deploy_join(qt6_path, "bin") + plug_root: str = _deploy_join(qt6_path, "plugins") + + # runtime.qt6_dlls: /bin/ -> / + if dlls is not None: + total: sizet = string.strlen(dlls) + i: sizet = 0 + while i < total: + while i < total and dlls[i] == ' ': i += 1 + if i >= total: break + start: sizet = i + while i < total and dlls[i] != ' ': i += 1 + if i > start: + name: str = _deploy_token(dlls, start, i) + if name is None: + VLogger.error("runtime deploy: DLL name allocation failed (OOM)", "runtime") + return 1 + src: str = _deploy_join(bin_dir, name) + dst: str = _deploy_join(exe_dir, name) + if not oslib.isfile(src): + VLogger.error(f"runtime deploy: declared DLL not found: {src}", "runtime") + return 1 + if shutil.copyfile(src, dst) != 0: + VLogger.error(f"runtime deploy: failed to copy {src} -> {dst}", "runtime") + return 1 + VLogger.info(f"runtime deploy: {name}", "runtime") + + # runtime.qt6_plugins: /plugins/ -> / (ensure parent dir) + if plugins is not None: + total_p: sizet = string.strlen(plugins) + ip: sizet = 0 + while ip < total_p: + while ip < total_p and plugins[ip] == ' ': ip += 1 + if ip >= total_p: break + start_p: sizet = ip + while ip < total_p and plugins[ip] != ' ': ip += 1 + if ip > start_p: + rel: str = _deploy_token(plugins, start_p, ip) + if rel is None: + VLogger.error("runtime deploy: plugin path allocation failed (OOM)", "runtime") + return 1 + src_p: str = _deploy_join(plug_root, rel) + dst_p: str = _deploy_join(exe_dir, rel) + parent: str = _deploy_dir_of(dst_p) + if parent is not None: + if not oslib.isdir(parent): + if oslib.mkdir(parent) != 0: + VLogger.error(f"runtime deploy: failed to create dir {parent}", "runtime") + return 1 + if not oslib.isfile(src_p): + VLogger.error(f"runtime deploy: declared plugin not found: {src_p}", "runtime") + return 1 + if shutil.copyfile(src_p, dst_p) != 0: + VLogger.error(f"runtime deploy: failed to copy {src_p} -> {dst_p}", "runtime") + return 1 + VLogger.info(f"runtime deploy: {rel}", "runtime") + return 0 \ No newline at end of file diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index 82c4d65..8078f42 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -1,3879 +1,3879 @@ -import lib.core.Gen.GenApi as GenApi -import lib.core.Gen.GenTypes as GenTypes -import lib.core.CompilerFactory as CompilerFactory -import t, c -from stdint import * -import ast -import string -import stdio -import stdlib -import sys -import memhub -import mlirlite -import viperlib -import win32.fileio as fileio -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HT -import lib.Stub.StubMerger.StubMergerStore as StubMergerStore -import lib.Projectrans.Config as Config - - -# REPL stub layout recovery: constants and class entries (impl at file end). -# Declared first so type annotations are visible before the REPL hook uses them; REPL-only. - -define _REPL_STUB_CLS_MAX = 1024 -_REPL_STUB_FIELD_MAX: int = 128 -_REPL_STUB_VT_MAX: int = 128 -_REPL_STUB_TXT_CAP: sizet = 4194304 -_REPL_STUB_WARN_MAX: int = 10 -_REPL_STUB_FINISH_PASSES: int = 8 - -# Two sentinel values for ReplStubClass.LayoutCount (not field counts). -# Layout metadata now comes from {sha1}.meta STRUCT ... FIELDS n / OPAQUE records, not .stub.ll text. -_REPL_STUB_NOCOUNT_NOCLASS: int = -1 # no STRUCT line for this key in meta (or meta not read) -_REPL_STUB_NOCOUNT_OPAQUE: int = -2 # key is OPAQUE in meta (no body) - -# Reason codes for "layout unavailable -> explicit refuse" (_repl_stub_warn_nodata): -# distinguish missing data from untrusted data; both skip StructEntry registration. -define _REPL_SU_NOSTUB = 1 # missing data: module has no {sha1}.meta -define _REPL_SU_NOCLASS = 2 # refused: meta exists but has no STRUCT line for this class -define _REPL_SU_OPAQUE = 3 # refused: class is OPAQUE in meta (no body) -define _REPL_SU_PARENT = 4 # upstream no data: base layout unavailable/not registered (goes to _repl_stub_warn_parent) - -# ReplStubClass.Flags / MethodFlags bits -define _REPL_SF_CVTABLE = 1 -define _REPL_SF_NOVTABLE = 2 -define _REPL_SF_ANYFUNC = 4 -define _REPL_SF_PACKED = 8 - -# ReplStubClass.State -define _REPL_SS_REFUSED = 0 # layout untrusted: never register (field access fails explicitly) -define _REPL_SS_OK = 1 # full layout registered -define _REPL_SS_PENDING = 2 # base not ready yet: defer to final pass -define _REPL_SS_EMPTY = 3 # no base/no vtable/unresolvable fields: use existing vpi injection - -class ReplStubClass: - Key: str # "." (= definition-side named struct name) - ClassName: str - Sha1: str - Flags: int - OwnNames: sizet* # elements = str pointers (this class field names, AnnAssign order) - OwnTypes: sizet* # elements = LLVMType pointers - OwnCount: int - MethodNames: sizet* - MethodFlags: sizet* - MethodCount: int - Bases: sizet* - BaseCount: int - LayoutCount: int # layout field count (>=0 = meta STRUCT ... FIELDS n; negative = sentinel, not count) - # see _REPL_STUB_NOCOUNT_NOCLASS/-OPAQUE: no such class / meta unread / opaque - VtArity: int # meta VTABLE ... SLOTS n (0 = no record in meta) - Names: sizet* - Types: sizet* - Count: int - HasVt: int - IsNoVTable: int - IsOOP: int - HasInit: int - HasNew: int - VtNames: sizet* - VtCount: int - VtTrust: int # 1 = vtable slot count matches stub (VTableMethods can be registered) - OwnFail: int # 1 = a field annotation failed to resolve, so OwnCount is only partially parsed (incomplete) - State: int - - -# Method return-annotation cache (stdlib-persistent). Deep vpi AST nodes may be -# corrupted later, so return annotations are parsed from raw vpi_buf text by line. -define VPI_RET_MAX = 4096 -define VPI_KEY_LEN = 224 -define VPI_RET_LEN = 224 - -_g_vpi_ret_keys: bytes = None -_g_vpi_ret_strs: bytes = None -_g_vpi_ret_count: int = 0 -_g_vpi_ret_cap: int = 0 - -# Method parameter-annotation cache (stdlib-persistent, mirroring the return cache). -# Deep param lists may be corrupted and truncated, so params are parsed from vpi_buf text. -define VPI_PARAM_MAX = 4096 -define VPI_PARAM_KEY_LEN = 224 -define VPI_PARAM_LEN = 1024 - -# Open-addressing index for the key tables (name -> slot index). Slots store indices -# (stable across realloc); each registration appends, lookup keeps first-registered. -define VPI_RET_IDX_CAP = 8192 -define VPI_PARAM_IDX_CAP = 8192 -# Index hash constants (32-bit FNV-1a, same as StructGenMixin). -define VPI_IDX_FNV_OFFSET = 2166136261 -define VPI_IDX_FNV_PRIME = 16777619 -define VPI_IDX_FNV_MASK = 4294967295 - -# v3 跨模块逃逸摘要缓存(.vpi 行尾 `# esc= ret=`,由 StubGen.Converter._PyiFunction 写出)。 -# 键 = "{sha1}.{Class}.{method}"(方法)或 "{sha1}.{func}"(模块级函数); -# 值打包:低 32 位 = 形参逃逸掩码,高 32 位 = 返回借用位。无逃逸/无返回借用的 def 不写条目, -# 因此"查不到"即"无摘要"(调用点保守放行),与本地表口径一致。 -define VPI_ESC_MAX = 8192 -define VPI_ESC_KEY_LEN = 224 - -# P2 跨模块 willFree 标注缓存(.vpi 行尾 `# ... wf=`,由 StubGen.Converter._PyiFunction 写出)。 -# 键格式与逃逸摘要完全一致("{sha1}.{Class}.{method}" / "{sha1}.{func}");值 = 形参失效掩码 -# (第 k 位 = 第 k 个形参在本调用后失效)。无标注的 def 不写条目,"查不到" 即 "无标注"。 -define VPI_WF_MAX = 8192 -define VPI_WF_KEY_LEN = 224 -_g_vpi_wf_keys: bytes = None -_g_vpi_wf_vals: sizet* = None -_g_vpi_wf_count: int = 0 -# 跨模块常量特化(P3 收尾):同一函数可有多条 `escg=`(打包 p|k|m,见 StructGenMixin._PackEscapeClear), -# 所以是"一键多行"表:键列与 wf 表同构,值列是打包整数。 -define VPI_ESG_MAX = 8192 -define VPI_ESG_KEY_LEN = 224 -# 常量环境的形参位上限:必须与 StructGenMixin.EC_ENV_MAX 一致(越界位一律不信) -define VPI_ESG_ENV_PARAM_MAX = 64 -_g_vpi_esg_keys: bytes = None -_g_vpi_esg_vals: sizet* = None -_g_vpi_esg_count: int = 0 -# 声明式指令契约(`@life(escape(...))` / `@life(noescape(...))` / `@life(ret(...))`):与 escg 表同键 -# (sha1[.Cls].func),但每函数只一条(契约唯一);三列分别存"强制逃逸位"、"强制不逃逸位"与 -# ret_kind(A2,编码见 GenApi.RET_KIND_*)。 -_g_vpi_esd_keys: bytes = None -_g_vpi_esd_e: sizet* = None -_g_vpi_esd_n: sizet* = None -_g_vpi_esd_r: sizet* = None -_g_vpi_esd_count: int = 0 - -_g_vpi_esc_keys: bytes = None -_g_vpi_esc_vals: sizet* = None -_g_vpi_esc_count: int = 0 -# "本模块的 vpi 已加载过"备忘(每项 17 字节 sha1)。调用点每处都要查摘要, -# 若每次都走 _ensure_vpi_ret_annot_loaded 的 O(N) 前缀判定会显著拖慢编译。 -define VPI_ESC_MOD_MAX = 512 -_g_vpi_esc_mods: bytes = None -_g_vpi_esc_mod_count: int = 0 - -def _vpi_escape_module_loaded(src_sha1: str) -> int: - """该模块的 vpi 是否已由逃逸查询路径加载过;1=是 / 0=否""" - if _g_vpi_esc_mods is None or src_sha1 is None: return 0 - s0: int = src_sha1[0] - s1: int = src_sha1[1] - i: int = 0 - while i < _g_vpi_esc_mod_count: - ent_m: bytes = _g_vpi_esc_mods + sizet(i) * 17 - if ent_m[0] == s0 and ent_m[1] == s1 and string.strcmp(ent_m, src_sha1) == 0: return 1 - i += 1 - return 0 - -def _vpi_escape_mark_module(src_sha1: str) -> int: - """记录"该模块的 vpi 已加载";1=成功 / 0=失败""" - global _g_vpi_esc_mods, _g_vpi_esc_mod_count - if src_sha1 is None: return 0 - if _g_vpi_esc_mods is None: - _g_vpi_esc_mods = stdlib.malloc(VPI_ESC_MOD_MAX * 17) - if _g_vpi_esc_mods is None: return 0 - string.memset(_g_vpi_esc_mods, 0, VPI_ESC_MOD_MAX * 17) - if _vpi_escape_module_loaded(src_sha1) != 0: return 1 - if _g_vpi_esc_mod_count >= VPI_ESC_MOD_MAX: return 0 - string.strcpy(_g_vpi_esc_mods + sizet(_g_vpi_esc_mod_count) * 17, src_sha1) - _g_vpi_esc_mod_count += 1 - return 1 - -_g_vpi_param_keys: bytes = None -_g_vpi_param_strs: bytes = None -_g_vpi_param_count: int = 0 -_g_vpi_param_cap: int = 0 - -# Return-annotation key-table index (same malloc lifetime; trusted iff Built == Count) -_g_vpi_ret_idx: bytes = None -_g_vpi_ret_idx_cap: int = 0 -_g_vpi_ret_idx_built: int = 0 -# Parameter-annotation key-table index (same as above) -_g_vpi_param_idx: bytes = None -_g_vpi_param_idx_cap: int = 0 -_g_vpi_param_idx_built: int = 0 - - -# Index probe/insert/rebuild (module-level functions shared by both key tables). -def _vpi_idx_hash(key: bytes) -> uint64: - """Hash key bytes to a 64-bit FNV-1a value until NUL; empty key returns the offset basis.""" - h: uint64 = uint64(VPI_IDX_FNV_OFFSET) - if key is None: return h - i: sizet = 0 - while key[i] != '\0': - h = (h ^ uint64(int(key[i]))) * uint64(VPI_IDX_FNV_PRIME) - h = h & uint64(VPI_IDX_FNV_MASK) - i += 1 - return h - - -def _vpi_idx_slot(idx_base: bytes, si: sizet) -> sizet*: - """Return the si-th 8-byte index slot (stores slot_index+1; 0 means empty).""" - return (sizet*)(uint64(idx_base) + uint64(si) * 8) - - -def _vpi_idx_find(keys: bytes, key_stride: int, idx_base: bytes, cap: int, key: bytes) -> int: - """Probe the index for key and return the slot index (>=0); miss or unusable index returns -1.""" - if keys is None or idx_base is None or cap <= 0 or key is None: return -1 - h: uint64 = _vpi_idx_hash(key) - mask: sizet = sizet(cap - 1) - si: sizet = sizet(h) & mask - for i in range(int(cap)): - slot: sizet* = _vpi_idx_slot(idx_base, si) - sv: sizet = slot[0] - if sv == 0: return -1 - se: int = int(sv) - 1 - ek: bytes = keys + se * key_stride - if string.strcmp(ek, key) == 0: return se - si = (si + 1) & mask - return -1 - - -def _vpi_idx_insert(idx_base: bytes, cap: int, slot_index: int, key: bytes) -> int: - """Insert (key -> slot_index) into the index, one slot per registration; 0=success / 1=failure.""" - if idx_base is None or cap <= 0 or key is None: return 1 - h: uint64 = _vpi_idx_hash(key) - mask: sizet = sizet(cap - 1) - si: sizet = sizet(h) & mask - for i in range(int(cap)): - slot: sizet* = _vpi_idx_slot(idx_base, si) - if slot[0] == 0: - slot[0] = sizet(slot_index + 1) - return 0 - si = (si + 1) & mask - return 1 - - -def _vpi_rebuild_ret_idx(new_cap: int) -> int: - """Rebuild the return-annotation index for a new capacity (power of two); 0=trusted / 1=untrusted.""" - global _g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_idx_built - if new_cap <= 0: - _g_vpi_ret_idx = None - _g_vpi_ret_idx_cap = 0 - _g_vpi_ret_idx_built = -1 - return 1 - new_idx: bytes = None - if _g_vpi_ret_idx is None: new_idx = stdlib.malloc(new_cap * 8) - else: new_idx = stdlib.realloc(_g_vpi_ret_idx, new_cap * 8) - if new_idx is None: - _g_vpi_ret_idx = None - _g_vpi_ret_idx_cap = 0 - _g_vpi_ret_idx_built = -1 - return 1 - string.memset(new_idx, 0, sizet(new_cap) * 8) - _g_vpi_ret_idx = new_idx - _g_vpi_ret_idx_cap = new_cap - fail: int = 0 - ri: int - for ri in range(int(_g_vpi_ret_count)): - if _vpi_idx_insert(new_idx, new_cap, ri, _g_vpi_ret_keys + ri * VPI_KEY_LEN) != 0: - fail = 1 - break - if fail != 0: - _g_vpi_ret_idx_built = -1 - return 1 - _g_vpi_ret_idx_built = _g_vpi_ret_count - return 0 - - -def _vpi_rebuild_param_idx(new_cap: int) -> int: - """Rebuild the param-annotation index for a new capacity; 0=trusted / 1=untrusted.""" - global _g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_idx_built - if new_cap <= 0: - _g_vpi_param_idx = None - _g_vpi_param_idx_cap = 0 - _g_vpi_param_idx_built = -1 - return 1 - new_idx: bytes = None - if _g_vpi_param_idx is None: new_idx = stdlib.malloc(new_cap * 8) - else: new_idx = stdlib.realloc(_g_vpi_param_idx, new_cap * 8) - if new_idx is None: - _g_vpi_param_idx = None - _g_vpi_param_idx_cap = 0 - _g_vpi_param_idx_built = -1 - return 1 - string.memset(new_idx, 0, sizet(new_cap) * 8) - _g_vpi_param_idx = new_idx - _g_vpi_param_idx_cap = new_cap - fail: int = 0 - ri: int - for ri in range(int(_g_vpi_param_count)): - if _vpi_idx_insert(new_idx, new_cap, ri, _g_vpi_param_keys + ri * VPI_PARAM_KEY_LEN) != 0: - fail = 1 - break - if fail != 0: - _g_vpi_param_idx_built = -1 - return 1 - _g_vpi_param_idx_built = _g_vpi_param_count - return 0 - - -# _cache_vpi_return_annots_text - extract method return annotations from vpi_buf text. -# Does not rely on the AST tree. Tracks "class X:" and "def ... -> Ret:" lines. -def _cache_vpi_return_annots_text(vpi_buf: bytes, src_sha1: str) -> int: - """Extract method return-annotation strings from vpi_buf text into a persistent stdlib cache.""" - global _g_vpi_ret_keys, _g_vpi_ret_strs, _g_vpi_ret_count, _g_vpi_ret_cap - global _g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_idx_built - if vpi_buf is None or src_sha1 is None: return 0 - if _g_vpi_ret_keys is None: - _g_vpi_ret_keys = stdlib.malloc(VPI_RET_MAX * VPI_KEY_LEN) - _g_vpi_ret_strs = stdlib.malloc(VPI_RET_MAX * VPI_RET_LEN) - if _g_vpi_ret_keys is None or _g_vpi_ret_strs is None: return 0 - string.memset(_g_vpi_ret_keys, 0, VPI_RET_MAX * VPI_KEY_LEN) - string.memset(_g_vpi_ret_strs, 0, VPI_RET_MAX * VPI_RET_LEN) - _g_vpi_ret_cap = VPI_RET_MAX - # Name index: slots = key-table capacity * 2. On alloc failure stay untrusted - # (Built=-1) and fall back to linear scan (semantics unchanged). - _g_vpi_ret_idx = stdlib.malloc(VPI_RET_IDX_CAP * 8) - if _g_vpi_ret_idx is not None: - string.memset(_g_vpi_ret_idx, 0, VPI_RET_IDX_CAP * 8) - _g_vpi_ret_idx_cap = VPI_RET_IDX_CAP - _g_vpi_ret_idx_built = _g_vpi_ret_count - else: - _g_vpi_ret_idx_cap = 0 - _g_vpi_ret_idx_built = -1 - sha1_len: sizet = string.strlen(src_sha1) - buf_len: sizet = string.strlen(vpi_buf) - cur_cls: bytes = None - pos: sizet = 0 - line_no: int = 0 - while pos < buf_len: - ls: sizet = pos - le: sizet = pos - while le < buf_len and vpi_buf[le] != '\n': le += 1 - line_len: sizet = le - ls - # skip leading spaces, then check class/def - i: sizet = ls - while i < le and vpi_buf[i] == ' ': i += 1 - if i < le: - # class X: - if line_len >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: - cstart: sizet = i + 6 - cend: sizet = cstart - # Extract class name: stop at ':' / ' ' / '(' so base-class parens - # are excluded ("class MemBuddy(MemManager):" -> "MemBuddy"). - while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 - if cend > cstart: - if cur_cls is not None: stdlib.free(cur_cls) - cur_cls = stdlib.malloc(cend - cstart + 1) - if cur_cls is not None: - string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) - cur_cls[cend - cstart] = '\0' - # def name(...) -> Ret: # noqa - elif line_len >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: - # Module-level function (no indent): clear class context to avoid - # leftovers after a class ends (e.g. json null/int_val). - if i == ls and cur_cls is not None: - stdlib.free(cur_cls) - cur_cls = None - dstart: sizet = i + 4 - dend: sizet = dstart - while dend < le and vpi_buf[dend] != '(': dend += 1 - # method name = [dstart, dend) - # find "->" - rpos: sizet = dstart - arrow_pos: sizet = le - while rpos < le - 1: - if vpi_buf[rpos] == '-' and vpi_buf[rpos + 1] == '>': - arrow_pos = rpos - break - rpos += 1 - if dend > dstart: - # Return type: extract "-> Ret" annotation; unannotated non-special - # methods default to "i32" (matches HandlesType.infer_return_type). - ret_src: str = None - rtype_len: sizet = 0 - if arrow_pos < le: - # return type = from arrow_pos+2 (skip spaces) up to ':' - rstart2: sizet = arrow_pos + 2 - while rstart2 < le and vpi_buf[rstart2] == ' ': rstart2 += 1 - rend2: sizet = rstart2 - while rend2 < le and vpi_buf[rend2] != ':': rend2 += 1 - if rend2 > rstart2: - rtype_len = rend2 - rstart2 - ret_src = vpi_buf + rstart2 - else: - # no return annotation: default i32 (except special methods filtered below) - rtype_len = 3 - ret_src = "i32" - if ret_src is not None: - # build key and ret_str, store into cache - mname_len: sizet = dend - dstart - cls_len2: sizet = 0 - if cur_cls is not None: cls_len2 = string.strlen(cur_cls) - key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len - if key_len < VPI_KEY_LEN and rtype_len < VPI_RET_LEN: - # copy method name to a temp buffer (vpi_buf+dstart is not NUL-terminated) - mname_buf: bytes = stdlib.malloc(mname_len + 1) - if mname_buf is not None: - string.strncpy(mname_buf, vpi_buf + dstart, mname_len) - mname_buf[mname_len] = '\0' - # skip unannotated special methods (init/deinit): they have dedicated paths - skip_def: int = 0 - if arrow_pos >= le and (string.strcmp(mname_buf, "__init__") == 0 or string.strcmp(mname_buf, "__new__") == 0 or string.strcmp(mname_buf, "__before_init__") == 0 or string.strcmp(mname_buf, "__after_init__") == 0): - skip_def = 1 - if skip_def == 0: - key_buf: bytes = stdlib.malloc(key_len + 1) - if key_buf is not None: - if cur_cls is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf) - else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, mname_buf) - # dedup: skip if key already exists (same file may load multiple times) - dup_key: int = 0 - dki: int - # Index fast path: when trusted (Built==Count) a single - # probe detects duplicates; otherwise fall back to linear scan. - if _g_vpi_ret_idx_built == _g_vpi_ret_count and _g_vpi_ret_idx is not None and _g_vpi_ret_idx_cap > 0: - if _vpi_idx_find(_g_vpi_ret_keys, VPI_KEY_LEN, _g_vpi_ret_idx, _g_vpi_ret_idx_cap, key_buf) >= 0: - dup_key = 1 - else: - # Fast path: equal keys share the first byte, so skip - # strcmp when the candidate's first byte differs. - dup_first: int = key_buf[0] - for dki in range(int(_g_vpi_ret_count)): - ent_d: bytes = _g_vpi_ret_keys + dki * VPI_KEY_LEN - if ent_d[0] == dup_first and string.strcmp(ent_d, key_buf) == 0: - dup_key = 1 - break - grow_ok: int = 1 - if dup_key == 0 and _g_vpi_ret_count >= _g_vpi_ret_cap: grow_ok = _grow_vpi_ret_cache() - if dup_key == 0 and grow_ok != 0: - string.strcpy(_g_vpi_ret_keys + _g_vpi_ret_count * VPI_KEY_LEN, key_buf) - string.strncpy(_g_vpi_ret_strs + _g_vpi_ret_count * VPI_RET_LEN, ret_src, rtype_len) - (_g_vpi_ret_strs + _g_vpi_ret_count * VPI_RET_LEN)[rtype_len] = '\0' - _g_vpi_ret_count += 1 - # Sync index only if trusted before insert (Built==Count-1, - # incl. rebuild) and insert succeeded; else mark untrusted. - if _g_vpi_ret_idx_built == _g_vpi_ret_count - 1 and _vpi_idx_insert(_g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_count - 1, key_buf) == 0: - _g_vpi_ret_idx_built = _g_vpi_ret_count - else: _g_vpi_ret_idx_built = -1 - stdlib.free(key_buf) - stdlib.free(mname_buf) - pos = le + 1 - line_no += 1 - if cur_cls is not None: stdlib.free(cur_cls) - return 1 - - -# _cache_vpi_param_annots_text - extract function/method param annotations from vpi_buf text. -# Stored as key="sha1.ClassName.method"/"sha1.method", val='@'-separated annotation strings. -def _cache_vpi_param_annots_text(vpi_buf: bytes, src_sha1: str) -> int: - """Extract function/method param-annotation strings from vpi_buf text into a persistent stdlib cache.""" - global _g_vpi_param_keys, _g_vpi_param_strs, _g_vpi_param_count, _g_vpi_param_cap - global _g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_idx_built - if vpi_buf is None or src_sha1 is None: return 0 - if _g_vpi_param_keys is None: - _g_vpi_param_keys = stdlib.malloc(VPI_PARAM_MAX * VPI_PARAM_KEY_LEN) - _g_vpi_param_strs = stdlib.malloc(VPI_PARAM_MAX * VPI_PARAM_LEN) - if _g_vpi_param_keys is None or _g_vpi_param_strs is None: return 0 - string.memset(_g_vpi_param_keys, 0, VPI_PARAM_MAX * VPI_PARAM_KEY_LEN) - string.memset(_g_vpi_param_strs, 0, VPI_PARAM_MAX * VPI_PARAM_LEN) - _g_vpi_param_cap = VPI_PARAM_MAX - # Name index: slots = key-table capacity * 2. On alloc failure stay untrusted - # (Built=-1) and fall back to linear scan (semantics unchanged). - _g_vpi_param_idx = stdlib.malloc(VPI_PARAM_IDX_CAP * 8) - if _g_vpi_param_idx is not None: - string.memset(_g_vpi_param_idx, 0, VPI_PARAM_IDX_CAP * 8) - _g_vpi_param_idx_cap = VPI_PARAM_IDX_CAP - _g_vpi_param_idx_built = _g_vpi_param_count - else: - _g_vpi_param_idx_cap = 0 - _g_vpi_param_idx_built = -1 - sha1_len: sizet = string.strlen(src_sha1) - buf_len: sizet = string.strlen(vpi_buf) - cur_cls: bytes = None - pos: sizet = 0 - while pos < buf_len: - ls: sizet = pos - le: sizet = pos - while le < buf_len and vpi_buf[le] != '\n': le += 1 - line_len: sizet = le - ls - i: sizet = ls - while i < le and vpi_buf[i] == ' ': i += 1 - if i < le: - if line_len >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: - cstart: sizet = i + 6 - cend: sizet = cstart - while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 - if cend > cstart: - if cur_cls is not None: stdlib.free(cur_cls) - cur_cls = stdlib.malloc(cend - cstart + 1) - if cur_cls is not None: - string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) - cur_cls[cend - cstart] = '\0' - elif line_len >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: - # module-level function (no indent): clear class context - if i == ls and cur_cls is not None: - stdlib.free(cur_cls) - cur_cls = None - dstart: sizet = i + 4 - dend: sizet = dstart - while dend < le and vpi_buf[dend] != '(': dend += 1 - if dend >= le: - pos = le + 1 - continue - # find matching ')' (depth-aware: annotations may contain [ ] / { } brackets) - p: sizet = dend + 1 - pdepth: int = 0 - pend: sizet = dend - while p < le: - ch_p: char = vpi_buf[p] - if ch_p == '(' or ch_p == '[' or ch_p == '{': pdepth += 1 - elif ch_p == ')' or ch_p == ']' or ch_p == '}': - pdepth -= 1 - if pdepth < 0: - pend = p - break - p += 1 - # parse params segment by segment (top-level commas; skip *args/**kwargs) - plist: bytes = stdlib.malloc(VPI_PARAM_LEN) - if plist is None: - pos = le + 1 - continue - plist[0] = '\0' - plist_pos: sizet = 0 - seg_written: int = 0 - seg_cur: sizet = dend + 1 - while seg_cur < pend: - while seg_cur < pend and (vpi_buf[seg_cur] == ' ' or vpi_buf[seg_cur] == ','): seg_cur += 1 - if seg_cur >= pend: break - s_start: sizet = seg_cur - sd: int = 0 - while seg_cur < pend: - ch_s: char = vpi_buf[seg_cur] - if ch_s == '(' or ch_s == '[' or ch_s == '{': sd += 1 - elif ch_s == ')' or ch_s == ']' or ch_s == '}': sd -= 1 - elif ch_s == ',' and sd == 0: break - seg_cur += 1 - s_end: sizet = seg_cur - # skip * / ** (vararg/kwarg not added to args list) - st: sizet = s_start - while st < s_end and vpi_buf[st] == ' ': st += 1 - if st < s_end and vpi_buf[st] == '*': - if seg_cur < pend and vpi_buf[seg_cur] == ',': seg_cur += 1 - continue - # cut default value (top-level '=', e.g. ": str = None") - e_pos: sizet = st - ed: int = 0 - while e_pos < s_end: - ch_e: char = vpi_buf[e_pos] - if ch_e == '(' or ch_e == '[' or ch_e == '{': ed += 1 - elif ch_e == ')' or ch_e == ']' or ch_e == '}': ed -= 1 - elif ch_e == '=' and ed == 0: break - e_pos += 1 - s_end = e_pos - # extract annotation after top-level ':' (none -> empty, e.g. self) - c_pos: sizet = st - cd2: int = 0 - annot_start: sizet = s_end - while c_pos < s_end: - ch_c: char = vpi_buf[c_pos] - if ch_c == '(' or ch_c == '[' or ch_c == '{': cd2 += 1 - elif ch_c == ')' or ch_c == ']' or ch_c == '}': cd2 -= 1 - elif ch_c == ':' and cd2 == 0: - annot_start = c_pos + 1 - break - c_pos += 1 - a_start: sizet = annot_start - while a_start < s_end and vpi_buf[a_start] == ' ': a_start += 1 - a_end: sizet = s_end - while a_end > a_start and vpi_buf[a_end - 1] == ' ': a_end -= 1 - if plist_pos + (a_end - a_start) + 1 < VPI_PARAM_LEN: - if seg_written > 0: - plist[plist_pos] = '@' - plist_pos += 1 - ai2: sizet - for ai2 in range(int(a_end - a_start)): - plist[plist_pos] = vpi_buf[a_start + ai2] - plist_pos += 1 - plist[plist_pos] = '\0' - seg_written += 1 - if seg_cur < pend and vpi_buf[seg_cur] == ',': seg_cur += 1 - # store into cache (key = sha1.class.method / sha1.method, dedup) - if plist_pos > 0 and plist[0] != '\0': - mname_len: sizet = dend - dstart - cls_len2: sizet = 0 - if cur_cls is not None: cls_len2 = string.strlen(cur_cls) - key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len - if key_len < VPI_PARAM_KEY_LEN: - mname_buf: bytes = stdlib.malloc(mname_len + 1) - if mname_buf is not None: - string.strncpy(mname_buf, vpi_buf + dstart, mname_len) - mname_buf[mname_len] = '\0' - key_buf: bytes = stdlib.malloc(key_len + 1) - if key_buf is not None: - if cur_cls is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf) - else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, mname_buf) - dup_key: int = 0 - dki: int - # Index fast path: when trusted (Built==Count) a single - # probe detects duplicates; otherwise fall back to linear scan. - if _g_vpi_param_idx_built == _g_vpi_param_count and _g_vpi_param_idx is not None and _g_vpi_param_idx_cap > 0: - if _vpi_idx_find(_g_vpi_param_keys, VPI_PARAM_KEY_LEN, _g_vpi_param_idx, _g_vpi_param_idx_cap, key_buf) >= 0: - dup_key = 1 - else: - # Fast path: equal keys share the first byte, so skip - # strcmp when the candidate's first byte differs. - dup_first: int = key_buf[0] - for dki in range(int(_g_vpi_param_count)): - ent_d: bytes = _g_vpi_param_keys + dki * VPI_PARAM_KEY_LEN - if ent_d[0] == dup_first and string.strcmp(ent_d, key_buf) == 0: - dup_key = 1 - break - grow_ok: int = 1 - if dup_key == 0 and _g_vpi_param_count >= _g_vpi_param_cap: grow_ok = _grow_vpi_param_cache() - if dup_key == 0 and grow_ok != 0: - string.strcpy(_g_vpi_param_keys + _g_vpi_param_count * VPI_PARAM_KEY_LEN, key_buf) - string.strncpy(_g_vpi_param_strs + _g_vpi_param_count * VPI_PARAM_LEN, plist, plist_pos) - (_g_vpi_param_strs + _g_vpi_param_count * VPI_PARAM_LEN)[plist_pos] = '\0' - _g_vpi_param_count += 1 - # Sync index only if trusted before insert (Built==Count-1, - # incl. rebuild) and insert succeeded; else mark untrusted. - if _g_vpi_param_idx_built == _g_vpi_param_count - 1 and _vpi_idx_insert(_g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_count - 1, key_buf) == 0: - _g_vpi_param_idx_built = _g_vpi_param_count - else: _g_vpi_param_idx_built = -1 - stdlib.free(key_buf) - stdlib.free(mname_buf) - stdlib.free(plist) - pos = le + 1 - if cur_cls is not None: stdlib.free(cur_cls) - return 1 - - -# _grow_vpi_param_cache - grow the param-annotation cache (capacity doubles). -def _grow_vpi_param_cache() -> int: - """Grow the param-annotation cache to double capacity; returns 1=success / 0=failure.""" - global _g_vpi_param_keys, _g_vpi_param_strs, _g_vpi_param_cap - if _g_vpi_param_keys is None or _g_vpi_param_cap <= 0: return 0 - new_cap: int = _g_vpi_param_cap * 2 - if new_cap > 1048576: return 0 - new_keys: bytes = stdlib.realloc(_g_vpi_param_keys, new_cap * VPI_PARAM_KEY_LEN) - if new_keys is None: return 0 - new_strs: bytes = stdlib.realloc(_g_vpi_param_strs, new_cap * VPI_PARAM_LEN) - if new_strs is None: return 0 - _g_vpi_param_keys = new_keys - _g_vpi_param_strs = new_strs - _g_vpi_param_cap = new_cap - # grow the index with the table and rebuild it (slots = capacity * 2, power of 2) - _vpi_rebuild_param_idx(new_cap * 2) - return 1 - - -# _lookup_vpi_param_annots - look up the param-annotation cache. -def _lookup_vpi_param_annots(src_sha1: str, cls_name: str, method_name: str) -> str: - """Look up the param-annotation cache; returns the '@'-separated string (leading self placeholder) or None.""" - if src_sha1 is None or method_name is None or _g_vpi_param_keys is None: return None - sha1_len: sizet = string.strlen(src_sha1) - mlen: sizet = string.strlen(method_name) - clen: sizet = 0 - if cls_name is not None: clen = string.strlen(cls_name) - key_len: sizet = sha1_len + clen + mlen + 2 - if key_len >= VPI_PARAM_KEY_LEN: return None - key_buf: bytes = stdlib.malloc(key_len + 1) - if key_buf is None: return None - if cls_name is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) - else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, method_name) - r: str = None - ki: int - # Index fast path: when trusted (Built==Count) one probe returns the hit slot; - # a miss means truly absent, so no linear scan. Otherwise fall back to linear scan. - hit_p: int = -1 - if _g_vpi_param_idx_built == _g_vpi_param_count and _g_vpi_param_idx is not None and _g_vpi_param_idx_cap > 0: - hit_p = _vpi_idx_find(_g_vpi_param_keys, VPI_PARAM_KEY_LEN, _g_vpi_param_idx, _g_vpi_param_idx_cap, key_buf) - else: - # Linear-scan fallback: equal keys share the first byte, so skip strcmp when - # the candidate's first byte differs (negative check only, result unchanged). - key_first: int = key_buf[0] - for ki in range(int(_g_vpi_param_count)): - ent_p: bytes = _g_vpi_param_keys + ki * VPI_PARAM_KEY_LEN - if ent_p[0] == key_first and string.strcmp(ent_p, key_buf) == 0: - hit_p = ki - break - if hit_p >= 0: r = _g_vpi_param_strs + hit_p * VPI_PARAM_LEN - stdlib.free(key_buf) - return r - - -# _resolve_vpi_param_annot_seg_template - param-annotation segment for specialized generics. -# Locates the {sha1}.{base}[T].{method} template entry and substitutes T with the concrete arg. -def _resolve_vpi_param_annot_seg_template(pool: memhub.MemBuddy*, src_sha1: str, cls_name: str, method_name: str, seg_idx: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: - """Resolve a param-annotation segment for specialized class X[arg] by substituting T with arg; None on miss.""" - if pool is None or src_sha1 is None or cls_name is None or method_name is None or _g_vpi_param_keys is None: return None - lbkt_t: str = string.strchr(cls_name, 91) # '[' = 91 - if lbkt_t is None: return None - rbkt_t: str = string.strrchr(cls_name, 93) # ']' = 93 - if rbkt_t is None or rbkt_t <= lbkt_t: return None - base_len_t: sizet = uint64(lbkt_t) - uint64(cls_name) - arg_len_t: sizet = uint64(rbkt_t) - uint64(lbkt_t) - 1 - if base_len_t <= 0 or arg_len_t <= 0: return None - base_t: bytes = stdlib.malloc(base_len_t + 1) - if base_t is None: return None - arg_t: bytes = stdlib.malloc(arg_len_t + 1) - if arg_t is None: - stdlib.free(base_t) - return None - string.strncpy(base_t, cls_name, base_len_t) - base_t[base_len_t] = '\0' - string.strncpy(arg_t, lbkt_t + 1, arg_len_t) - arg_t[arg_len_t] = '\0' - sha1_len_t: sizet = string.strlen(src_sha1) - ty_t: mlirlite.LLVMType* = None - ki_t: int - for ki_t in range(int(_g_vpi_param_count)): - gk_t: bytes = _g_vpi_param_keys + ki_t * VPI_PARAM_KEY_LEN - if string.strncmp(gk_t, src_sha1, sha1_len_t) != 0 or gk_t[sha1_len_t] != '.': continue - if string.strncmp(gk_t + sha1_len_t + 1, base_t, base_len_t) != 0: continue - if gk_t[sha1_len_t + 1 + base_len_t] != 91: continue - gklb_t: str = string.strchr(gk_t + sha1_len_t + 1 + base_len_t, 93) - if gklb_t is None: continue - tplen_t: sizet = uint64(gklb_t) - uint64(gk_t + sha1_len_t + 1 + base_len_t) - 1 - # method name must match fully (gklb_t is followed by ']' and '.'; mangled, so unambiguous) - if tplen_t <= 0 or string.strcmp(gklb_t + 2, method_name) != 0: continue - annots_t: str = _g_vpi_param_strs + ki_t * VPI_PARAM_LEN - if annots_t is None or annots_t[0] == '\0': break - # locate segment (same scheme as _resolve_vpi_param_annot_seg: '@'-separated, seg 0 = self empty) - cur_t: sizet = 0 - cur_seg_t: int = 0 - while cur_seg_t < seg_idx and annots_t[cur_t] != '\0': - while annots_t[cur_t] != '\0' and annots_t[cur_t] != '@': cur_t += 1 - if annots_t[cur_t] == '@': cur_t += 1 - cur_seg_t += 1 - seg_start_t: sizet = cur_t - seg_len_t: sizet = 0 - while annots_t[seg_start_t + seg_len_t] != '\0' and annots_t[seg_start_t + seg_len_t] != '@': seg_len_t += 1 - if seg_len_t == 0: break - seg_buf_t: bytes = stdlib.malloc(seg_len_t + 1) - if seg_buf_t is None: break - string.strncpy(seg_buf_t, annots_t + seg_start_t, seg_len_t) - seg_buf_t[seg_len_t] = '\0' - # Replace the type-param token (e.g. T) with the concrete arg (e.g. str / AST*): - # template annotations are cached verbatim, so without this "item: T" resolves to nothing. - tparam_t: bytes = stdlib.malloc(tplen_t + 1) - if tparam_t is not None: - string.strncpy(tparam_t, gk_t + sha1_len_t + 1 + base_len_t + 1, tplen_t) - tparam_t[tplen_t] = '\0' - subst_t: str = _subst_template_arg(seg_buf_t, tparam_t, arg_t) - if subst_t is not None: - # _subst_template_arg may return its input unchanged (tplen==0 or tplen>alen), - # so only free the old buffer when subst_t is a different pointer (avoid double free). - if uint64(subst_t) != uint64(seg_buf_t): stdlib.free(seg_buf_t) - seg_buf_t = subst_t - stdlib.free(tparam_t) - ty_t = _resolve_vpi_ret_annot_str(pool, seg_buf_t, i32_ty, i8_ptr_ty) - stdlib.free(seg_buf_t) - break - stdlib.free(base_t) - stdlib.free(arg_t) - return ty_t - - -# _repair_declare_params_from_text - complete declare params from the text cache. -# When the vpi tree param list is truncated, append missing params so declare matches define. -def _repair_declare_params_from_text(pool: memhub.MemBuddy*, func: mlirlite.Function*, src_sha1: str, cls_name: str, method_name: str, param_count: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> int: - """Complete declare params from the vpi text cache; returns the completed param count.""" - if func is None or src_sha1 is None or method_name is None: return param_count - prm_annots: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name) - if prm_annots is None or prm_annots[0] == '\0': return param_count - # count text params ('@'-separated; segments = separators + 1, incl. first param) - pa_count: int = 1 - pi2: sizet = 0 - while prm_annots[pi2] != '\0': - if prm_annots[pi2] == '@': pa_count += 1 - pi2 += 1 - if pa_count <= param_count: return param_count - # append missing params: segments with index >= param_count are the missing ones - seg_idx: int = 0 - seg_start2: sizet = 0 - cur2: sizet = 0 - while prm_annots[cur2] != '\0': - while prm_annots[cur2] != '\0' and prm_annots[cur2] != '@': cur2 += 1 - seg_len2: sizet = cur2 - seg_start2 - if prm_annots[cur2] == '@': cur2 += 1 - if seg_idx >= param_count: - param_ty_r: mlirlite.LLVMType* = i8_ptr_ty - if seg_len2 > 0: - seg_buf_r: bytes = stdlib.malloc(seg_len2 + 1) - if seg_buf_r is not None: - string.strncpy(seg_buf_r, prm_annots + seg_start2, seg_len2) - seg_buf_r[seg_len2] = '\0' - rt: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_r, i32_ty, i8_ptr_ty) - if rt is not None: param_ty_r = rt - stdlib.free(seg_buf_r) - mlirlite.add_param(pool, func, param_ty_r, "arg") - seg_start2 = cur2 - seg_idx += 1 - return pa_count - - -# _resolve_vpi_param_annot_seg - resolve a vpi param-annotation segment to LLVMType. -# Parses the seg_idx-th '@'-separated segment (0 = self) via _resolve_vpi_ret_annot_str. -def _resolve_vpi_param_annot_seg(pool: memhub.MemBuddy*, src_sha1: str, cls_name: str, method_name: str, seg_idx: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: - """Resolve a vpi param-annotation segment by index to LLVMType; empty segment or failure returns None.""" - if pool is None or src_sha1 is None or method_name is None: return None - prm_annots_p: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name) - if prm_annots_p is None or prm_annots_p[0] == '\0': return None - cur_p: sizet = 0 - cur_seg_p: int = 0 - while cur_seg_p < seg_idx and prm_annots_p[cur_p] != '\0': - while prm_annots_p[cur_p] != '\0' and prm_annots_p[cur_p] != '@': cur_p += 1 - if prm_annots_p[cur_p] == '@': cur_p += 1 - cur_seg_p += 1 - seg_start_p: sizet = cur_p - seg_len_p: sizet = 0 - while prm_annots_p[seg_start_p + seg_len_p] != '\0' and prm_annots_p[seg_start_p + seg_len_p] != '@': seg_len_p += 1 - if seg_len_p == 0: return None - seg_buf_p: bytes = stdlib.malloc(seg_len_p + 1) - if seg_buf_p is None: return None - string.strncpy(seg_buf_p, prm_annots_p + seg_start_p, seg_len_p) - seg_buf_p[seg_len_p] = '\0' - seg_ty_p: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_p, i32_ty, i8_ptr_ty) - stdlib.free(seg_buf_p) - return seg_ty_p - - -# _grow_vpi_ret_cache - grow the return-annotation cache (capacity doubles). -# Doubles via stdlib.realloc (cap 1M) so entries are not silently dropped when full. -def _grow_vpi_ret_cache() -> int: - """Grow the return-annotation cache to double capacity; returns 1=success / 0=failure.""" - global _g_vpi_ret_keys, _g_vpi_ret_strs, _g_vpi_ret_cap - if _g_vpi_ret_keys is None or _g_vpi_ret_cap <= 0: return 0 - new_cap: int = _g_vpi_ret_cap * 2 - if new_cap > 1048576: return 0 - new_keys: bytes = stdlib.realloc(_g_vpi_ret_keys, new_cap * VPI_KEY_LEN) - if new_keys is None: return 0 - new_strs: bytes = stdlib.realloc(_g_vpi_ret_strs, new_cap * VPI_RET_LEN) - if new_strs is None: return 0 - _g_vpi_ret_keys = new_keys - _g_vpi_ret_strs = new_strs - _g_vpi_ret_cap = new_cap - # grow the index with the table and rebuild it (slots = capacity * 2, power of 2) - _vpi_rebuild_ret_idx(new_cap * 2) - return 1 - - -# _ensure_vpi_ret_annot_loaded - lazily load a module's vpi stub return-annotation cache. -# On cache miss reads {temp_dir}/{sha1[0:2]}/{sha1}.vpi directly; idempotent. -def _ensure_vpi_ret_annot_loaded(src_sha1: str, temp_dir: str) -> int: - """Lazily load the module's vpi stub return-annotation cache; returns 1=ready / 0=failure.""" - if src_sha1 is None or temp_dir is None: return 0 - sha1_len: sizet = string.strlen(src_sha1) - if sha1_len < 2: return 0 - # module already cached -> skip (idempotent) - if _g_vpi_ret_keys is not None and _g_vpi_ret_count > 0: - pre_i: int - # Fast path: equal prefixes share the first two bytes (the module subdir name), - # so checking them skips strncmp for most entries. - s0: int = src_sha1[0] - s1: int = src_sha1[1] - for pre_i in range(int(_g_vpi_ret_count)): - pre_key: bytes = _g_vpi_ret_keys + pre_i * VPI_KEY_LEN - if pre_key[0] == s0 and pre_key[1] == s1 and string.strncmp(pre_key, src_sha1, sha1_len) == 0 and pre_key[sha1_len] == '.': return 1 - # build path {temp_dir}/{sha1[0:2]}/{sha1}.vpi (char by char, avoiding %c formatting) - td_len: sizet = string.strlen(temp_dir) - path_len: sizet = td_len + sha1_len + 12 - vpi_path: bytes = stdlib.malloc(path_len) - if vpi_path is None: return 0 - string.strcpy(vpi_path, temp_dir) - vpi_path[td_len] = '/' - vpi_path[td_len + 1] = src_sha1[0] - vpi_path[td_len + 2] = src_sha1[1] - vpi_path[td_len + 3] = '/' - string.strcpy(vpi_path + td_len + 4, src_sha1) - string.strcpy(vpi_path + td_len + 4 + sha1_len, ".vpi") - f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R) - if f.closed: - stdlib.free(vpi_path) - return 0 - vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) - if vpi_buf is None: - f.close() - stdlib.free(vpi_path) - return 0 - bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE) - f.close() - if bytes_read > 0: - if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0 - # 单一入口:一次喂齐 ret / param / 逃逸摘要(见 _cache_vpi_annots_text 注释) - _cache_vpi_annots_text(vpi_buf, src_sha1) - stdlib.free(vpi_buf) - stdlib.free(vpi_path) - return 1 - - -# _cache_vpi_esc_clears_text - 抽取行尾**所有** `escg=` 标记(跨模块常量特化)。 -# 与 wf 表同构,但同一行可有多条(一条一个打包值),故是"一键多行"。值 = p|k|m 打包整数 -# (见 StructGenMixin._PackEscapeClear:p=守卫形参位、k=常量、m=可清掉的位掩码)。 -def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int: - """抽取 vpi 行尾的 escg= 标记进缓存;1=处理完成 / 0=缓冲区或分配失败""" - global _g_vpi_esg_keys, _g_vpi_esg_vals, _g_vpi_esg_count - global _g_vpi_esd_keys, _g_vpi_esd_e, _g_vpi_esd_n, _g_vpi_esd_r, _g_vpi_esd_count - if vpi_buf is None or src_sha1 is None: return 0 - if _g_vpi_esg_keys is None: - _g_vpi_esg_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN) - _g_vpi_esg_vals = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) - if _g_vpi_esg_keys is None or _g_vpi_esg_vals is None: return 0 - string.memset(_g_vpi_esg_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN) - string.memset(_g_vpi_esg_vals, 0, VPI_ESG_MAX * 8) - if _g_vpi_esd_keys is None: - _g_vpi_esd_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN) - _g_vpi_esd_e = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) - _g_vpi_esd_n = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) - _g_vpi_esd_r = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) - if _g_vpi_esd_keys is None or _g_vpi_esd_e is None or _g_vpi_esd_n is None or _g_vpi_esd_r is None: return 0 - string.memset(_g_vpi_esd_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN) - string.memset(_g_vpi_esd_e, 0, VPI_ESG_MAX * 8) - string.memset(_g_vpi_esd_n, 0, VPI_ESG_MAX * 8) - string.memset(_g_vpi_esd_r, 0, VPI_ESG_MAX * 8) - sha1_len: sizet = string.strlen(src_sha1) - buf_len: sizet = string.strlen(vpi_buf) - cur_cls: bytes = None - pos: sizet = 0 - while pos < buf_len: - ls: sizet = pos - le: sizet = pos - while le < buf_len and vpi_buf[le] != '\n': le += 1 - i: sizet = ls - while i < le and vpi_buf[i] == ' ': i += 1 - if i < le: - if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: - cstart: sizet = i + 6 - cend: sizet = cstart - while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 - if cend > cstart: - if cur_cls is not None: stdlib.free(cur_cls) - cur_cls = stdlib.malloc(cend - cstart + 1) - if cur_cls is not None: - string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) - cur_cls[cend - cstart] = '\0' - elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: - # 模块级函数(无缩进)清掉类上下文,避免上一类泄漏 - if i == ls and cur_cls is not None: - stdlib.free(cur_cls) - cur_cls = None - # 只在注释段('#' 之后)里找 escg=:避开参数名 - hm: sizet = le - j: sizet = i - while j < le: - if vpi_buf[j] == '#': - hm = j - break - j += 1 - dstart: sizet = i + 4 - dend: sizet = dstart - while dend < le and vpi_buf[dend] != '(': dend += 1 - if hm < le and dend > dstart: - mname_len: sizet = dend - dstart - cls_len_e: sizet = 0 - if cur_cls is not None: cls_len_e = string.strlen(cur_cls) - key_len_e: sizet = sha1_len + 1 + cls_len_e + 1 + mname_len - if key_len_e < VPI_ESG_KEY_LEN: - mname_buf_e: bytes = stdlib.malloc(mname_len + 1) - key_buf_e: bytes = stdlib.malloc(key_len_e + 1) - if mname_buf_e is not None and key_buf_e is not None: - string.strncpy(mname_buf_e, vpi_buf + dstart, mname_len) - mname_buf_e[mname_len] = '\0' - if cur_cls is not None: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_e) - else: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s", src_sha1, mname_buf_e) - j3: sizet = hm + 1 - while j3 + 5 <= le: - if vpi_buf[j3] == 'e' and vpi_buf[j3 + 1] == 's' and vpi_buf[j3 + 2] == 'c' and vpi_buf[j3 + 3] == 'g' and vpi_buf[j3 + 4] == '=': - kq: sizet = j3 + 5 - eg_v: int = 0 - while kq < le and vpi_buf[kq] >= '0' and vpi_buf[kq] <= '9': - eg_v = eg_v * 10 + (int(vpi_buf[kq]) - 48) - kq += 1 - if eg_v > 0 and _g_vpi_esg_count < VPI_ESG_MAX: - string.strcpy(_g_vpi_esg_keys + sizet(_g_vpi_esg_count) * VPI_ESG_KEY_LEN, key_buf_e) - _g_vpi_esg_vals[_g_vpi_esg_count] = sizet(eg_v) - _g_vpi_esg_count += 1 - j3 = kq - else: - j3 += 1 - # 声明式指令契约(esce= / escn= / escr=):每函数一条,只认 '#' 注释段 - de_v: int = 0 - dn_v: int = 0 - dr_v: int = 0 - j4: sizet = hm + 1 - while j4 + 5 <= le: - if vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'e' and vpi_buf[j4 + 4] == '=': - kq2: sizet = j4 + 5 - v2: int = 0 - while kq2 < le and vpi_buf[kq2] >= '0' and vpi_buf[kq2] <= '9': - v2 = v2 * 10 + (int(vpi_buf[kq2]) - 48) - kq2 += 1 - if v2 > 0: de_v = v2 - j4 = kq2 - elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'n' and vpi_buf[j4 + 4] == '=': - kq3: sizet = j4 + 5 - v3: int = 0 - while kq3 < le and vpi_buf[kq3] >= '0' and vpi_buf[kq3] <= '9': - v3 = v3 * 10 + (int(vpi_buf[kq3]) - 48) - kq3 += 1 - if v3 > 0: dn_v = v3 - j4 = kq3 - elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'r' and vpi_buf[j4 + 4] == '=': - kq5: sizet = j4 + 5 - v5: int = 0 - while kq5 < le and vpi_buf[kq5] >= '0' and vpi_buf[kq5] <= '9': - v5 = v5 * 10 + (int(vpi_buf[kq5]) - 48) - kq5 += 1 - if v5 > 0: dr_v = v5 - j4 = kq5 - else: - j4 += 1 - if (de_v != 0 or dn_v != 0 or dr_v != 0) and _g_vpi_esd_count < VPI_ESG_MAX: - string.strcpy(_g_vpi_esd_keys + sizet(_g_vpi_esd_count) * VPI_ESG_KEY_LEN, key_buf_e) - _g_vpi_esd_e[_g_vpi_esd_count] = sizet(de_v) - _g_vpi_esd_n[_g_vpi_esd_count] = sizet(dn_v) - _g_vpi_esd_r[_g_vpi_esd_count] = sizet(dr_v) - _g_vpi_esd_count += 1 - if mname_buf_e is not None: stdlib.free(mname_buf_e) - if key_buf_e is not None: stdlib.free(key_buf_e) - pos = le + 1 - if cur_cls is not None: stdlib.free(cur_cls) - return 1 - - -# _lookup_vpi_escape_clears - 跨模块常量守卫查询:按调用点常量环境算出"该清掉的位";0 = 无命中 -def _lookup_vpi_escape_clears(src_sha1: str, cls_name: str, method_name: str, env: sizet*) -> int: - """env = 常量环境(2 槽/形参:[0]=是否已知、[1]=值,下标 = 形参位);返回该清掉的位掩码 OR 值""" - if _g_vpi_esg_keys is None or _g_vpi_esg_count == 0: return 0 - if src_sha1 is None or method_name is None or env is None: return 0 - sha1_len: sizet = string.strlen(src_sha1) - cl_len: sizet = 0 - if cls_name is not None: cl_len = string.strlen(cls_name) - me_len: sizet = string.strlen(method_name) - key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len - kb: bytes = stdlib.malloc(key_len + 1) - if kb is None: return 0 - if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) - else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) - cleared: int = 0 - idx: int = 0 - while idx < _g_vpi_esg_count: - ent: bytes = _g_vpi_esg_keys + sizet(idx) * VPI_ESG_KEY_LEN - # 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp - if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: - v: int = int(_g_vpi_esg_vals[idx]) - if v > 0: - p: int = v & 255 - kv: int = (v >> 8) & 255 - if kv >= 128: kv -= 256 - m: int = v >> 16 - if p >= 0 and p < VPI_ESG_ENV_PARAM_MAX and env[p * 2] != 0: - if int64(env[p * 2 + 1]) == int64(kv): cleared = cleared | m - idx += 1 - stdlib.free(kb) - return cleared - - -# _lookup_vpi_escape_dir - 跨模块声明式指令契约查询(`@life(escape(...))` / `@life(noescape(...))` / -# `@life(ret(...))`)。which: 0=强制逃逸位 / 1=强制不逃逸位 / 2=ret_kind(A2)。 -# 0 = 无该标注(含该模块未加载过)。 -def _lookup_vpi_escape_dir(src_sha1: str, cls_name: str, method_name: str, which: int) -> int: - if _g_vpi_esd_keys is None or _g_vpi_esd_count == 0: return 0 - if src_sha1 is None or method_name is None: return 0 - sha1_len: sizet = string.strlen(src_sha1) - cl_len: sizet = 0 - if cls_name is not None: cl_len = string.strlen(cls_name) - me_len: sizet = string.strlen(method_name) - key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len - kb: bytes = stdlib.malloc(key_len + 1) - if kb is None: return 0 - if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) - else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) - r: int = 0 - i: int = 0 - while i < _g_vpi_esd_count: - ent: bytes = _g_vpi_esd_keys + sizet(i) * VPI_ESG_KEY_LEN - # 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp - if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: - if which == 2: r = int(_g_vpi_esd_r[i]) - elif which != 0: r = int(_g_vpi_esd_n[i]) - else: r = int(_g_vpi_esd_e[i]) - break - i += 1 - stdlib.free(kb) - return r - - -# _cache_vpi_annots_text - 所有"从 vpi 文本解析"的缓存**单一入口**。 -# 任何新增的文本缓存都加在这里,不要再散落到各加载点——漏喂一处就会**静默退化** -# (现场:逃逸摘要只在 _ensure_vpi_ret_annot_loaded 里喂,而 stub 注入加载点没喂, -# 前者开头又有"本模块 ret 注解已缓存即 return 1"的短路,于是跨模块逃逸查询永久查空, -# EscapeXmodTest 漏检)。 -def _cache_vpi_annots_text(vpi_buf: bytes, src_sha1: str) -> int: - """一次性把 vpi 文本派生出的所有缓存喂齐;1=完成 / 0=入参非法""" - if vpi_buf is None or src_sha1 is None: return 0 - _cache_vpi_return_annots_text(vpi_buf, src_sha1) - _cache_vpi_param_annots_text(vpi_buf, src_sha1) - _cache_vpi_escape_annots_text(vpi_buf, src_sha1) - _cache_vpi_willfree_annots_text(vpi_buf, src_sha1) - _cache_vpi_esc_clears_text(vpi_buf, src_sha1) - return 1 - - -# _cache_vpi_escape_annots_text - extract the trailing `# esc= ret=` markers. -# v3 跨模块逃逸摘要:不依赖 AST,按行扫描(与 _cache_vpi_return_annots_text 同构)。 -def _cache_vpi_escape_annots_text(vpi_buf: bytes, src_sha1: str) -> int: - """抽取 vpi 行尾逃逸摘要标记进缓存;1=处理完成 / 0=缓冲区或分配失败""" - global _g_vpi_esc_keys, _g_vpi_esc_vals, _g_vpi_esc_count - if vpi_buf is None or src_sha1 is None: return 0 - if _g_vpi_esc_keys is None: - _g_vpi_esc_keys = stdlib.malloc(VPI_ESC_MAX * VPI_ESC_KEY_LEN) - _g_vpi_esc_vals = sizet*(stdlib.malloc(VPI_ESC_MAX * 8)) - if _g_vpi_esc_keys is None or _g_vpi_esc_vals is None: return 0 - string.memset(_g_vpi_esc_keys, 0, VPI_ESC_MAX * VPI_ESC_KEY_LEN) - string.memset(_g_vpi_esc_vals, 0, VPI_ESC_MAX * 8) - sha1_len: sizet = string.strlen(src_sha1) - buf_len: sizet = string.strlen(vpi_buf) - cur_cls: bytes = None - pos: sizet = 0 - while pos < buf_len: - ls: sizet = pos - le: sizet = pos - while le < buf_len and vpi_buf[le] != '\n': le += 1 - i: sizet = ls - while i < le and vpi_buf[i] == ' ': i += 1 - if i < le: - # class X: -> 记录当前类上下文(与方法键一致) - if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: - cstart: sizet = i + 6 - cend: sizet = cstart - while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 - if cend > cstart: - if cur_cls is not None: stdlib.free(cur_cls) - cur_cls = stdlib.malloc(cend - cstart + 1) - if cur_cls is not None: - string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) - cur_cls[cend - cstart] = '\0' - # def name(...) ... # esc= ret= - elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: - # 模块级函数(无缩进)清掉类上下文,避免上一类泄漏 - if i == ls and cur_cls is not None: - stdlib.free(cur_cls) - cur_cls = None - # 找同行 "# esc=" 标记 - mark: sizet = le - j: sizet = i - while j + 6 <= le: - if vpi_buf[j] == '#' and vpi_buf[j + 1] == ' ' and string.strncmp(vpi_buf + j + 2, "esc=", 4) == 0: - mark = j - break - j += 1 - if mark < le: - kp: sizet = mark + 6 - esc_v: int = 0 - while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9': - esc_v = esc_v * 10 + (int(vpi_buf[kp]) - 48) - kp += 1 - ret_v: int = 0 - while kp < le and vpi_buf[kp] != 'e': kp += 1 - if kp + 4 <= le: - kp += 4 - while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9': - ret_v = ret_v * 10 + (int(vpi_buf[kp]) - 48) - kp += 1 - if (esc_v != 0 or ret_v != 0) and _g_vpi_esc_count < VPI_ESC_MAX: - dstart: sizet = i + 4 - dend: sizet = dstart - while dend < le and vpi_buf[dend] != '(': dend += 1 - if dend > dstart: - mname_len: sizet = dend - dstart - cls_len_e: sizet = 0 - if cur_cls is not None: cls_len_e = string.strlen(cur_cls) - key_len_e: sizet = sha1_len + 1 + cls_len_e + 1 + mname_len - if key_len_e < VPI_ESC_KEY_LEN: - mname_buf_e: bytes = stdlib.malloc(mname_len + 1) - key_buf_e: bytes = stdlib.malloc(key_len_e + 1) - if mname_buf_e is not None and key_buf_e is not None: - string.strncpy(mname_buf_e, vpi_buf + dstart, mname_len) - mname_buf_e[mname_len] = '\0' - if cur_cls is not None: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_e) - else: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s", src_sha1, mname_buf_e) - string.strcpy(_g_vpi_esc_keys + sizet(_g_vpi_esc_count) * VPI_ESC_KEY_LEN, key_buf_e) - _g_vpi_esc_vals[_g_vpi_esc_count] = sizet(esc_v) | (sizet(ret_v) << 32) - _g_vpi_esc_count += 1 - if mname_buf_e is not None: stdlib.free(mname_buf_e) - if key_buf_e is not None: stdlib.free(key_buf_e) - pos = le + 1 - if cur_cls is not None: stdlib.free(cur_cls) - return 1 - - -# _lookup_vpi_escape_packed - query the cross-module escape summary cache (v3). -def _lookup_vpi_escape_packed(src_sha1: str, cls_name: str, method_name: str) -> int: - """查跨模块逃逸摘要;返回打包值(低32位 mask | ret<<32),0 = 无摘要""" - if _g_vpi_esc_keys is None or _g_vpi_esc_count == 0: return 0 - if src_sha1 is None or method_name is None: return 0 - sha1_len: sizet = string.strlen(src_sha1) - cl_len: sizet = 0 - if cls_name is not None: cl_len = string.strlen(cls_name) - me_len: sizet = string.strlen(method_name) - key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len - kb: bytes = stdlib.malloc(key_len + 1) - if kb is None: return 0 - if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) - else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) - res: int = 0 - idx: int = 0 - while idx < _g_vpi_esc_count: - ent: bytes = _g_vpi_esc_keys + sizet(idx) * VPI_ESC_KEY_LEN - # 键以模块 sha1 打头(前 2 个十六进制字符足以筛掉绝大多数候选,避免逐项 strcmp) - if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: - res = int(_g_vpi_esc_vals[idx]) - break - idx += 1 - stdlib.free(kb) - return res - - -# _cache_vpi_willfree_annots_text - extract the trailing `wf=` marker (P2 willFree). -# 与逃逸摘要同构:不依赖 AST,按行扫描;只在有 wf 标记的 def 上建条目。 -def _cache_vpi_willfree_annots_text(vpi_buf: bytes, src_sha1: str) -> int: - """抽取 vpi 行尾 P2 willFree 标记(`# ... wf=`)进缓存;1=处理完成 / 0=缓冲区或分配失败""" - global _g_vpi_wf_keys, _g_vpi_wf_vals, _g_vpi_wf_count - if vpi_buf is None or src_sha1 is None: return 0 - if _g_vpi_wf_keys is None: - _g_vpi_wf_keys = stdlib.malloc(VPI_WF_MAX * VPI_WF_KEY_LEN) - _g_vpi_wf_vals = sizet*(stdlib.malloc(VPI_WF_MAX * 8)) - if _g_vpi_wf_keys is None or _g_vpi_wf_vals is None: return 0 - string.memset(_g_vpi_wf_keys, 0, VPI_WF_MAX * VPI_WF_KEY_LEN) - string.memset(_g_vpi_wf_vals, 0, VPI_WF_MAX * 8) - sha1_len: sizet = string.strlen(src_sha1) - buf_len: sizet = string.strlen(vpi_buf) - cur_cls: bytes = None - pos: sizet = 0 - while pos < buf_len: - ls: sizet = pos - le: sizet = pos - while le < buf_len and vpi_buf[le] != '\n': le += 1 - i: sizet = ls - while i < le and vpi_buf[i] == ' ': i += 1 - if i < le: - # class X: -> 记录当前类上下文(与方法键一致) - if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: - cstart: sizet = i + 6 - cend: sizet = cstart - while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 - if cend > cstart: - if cur_cls is not None: stdlib.free(cur_cls) - cur_cls = stdlib.malloc(cend - cstart + 1) - if cur_cls is not None: - string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) - cur_cls[cend - cstart] = '\0' - # def name(...) ... # [esc= ret=] wf= - elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: - # 模块级函数(无缩进)清掉类上下文,避免上一类泄漏 - if i == ls and cur_cls is not None: - stdlib.free(cur_cls) - cur_cls = None - # 只在注释段里找 "wf=":避开参数名(如 def f(wf: int)) - mark: sizet = le - hm: sizet = le - j: sizet = i - while j < le: - if vpi_buf[j] == '#': - hm = j - break - j += 1 - if hm < le: - j2: sizet = hm + 1 - while j2 + 3 <= le: - if vpi_buf[j2] == 'w' and vpi_buf[j2 + 1] == 'f' and vpi_buf[j2 + 2] == '=': - mark = j2 - break - j2 += 1 - if mark < le: - kp: sizet = mark + 3 - wf_v: int = 0 - while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9': - wf_v = wf_v * 10 + (int(vpi_buf[kp]) - 48) - kp += 1 - if wf_v != 0 and _g_vpi_wf_count < VPI_WF_MAX: - dstart: sizet = i + 4 - dend: sizet = dstart - while dend < le and vpi_buf[dend] != '(': dend += 1 - if dend > dstart: - mname_len: sizet = dend - dstart - cls_len_w: sizet = 0 - if cur_cls is not None: cls_len_w = string.strlen(cur_cls) - key_len_w: sizet = sha1_len + 1 + cls_len_w + 1 + mname_len - if key_len_w < VPI_WF_KEY_LEN: - mname_buf_w: bytes = stdlib.malloc(mname_len + 1) - key_buf_w: bytes = stdlib.malloc(key_len_w + 1) - if mname_buf_w is not None and key_buf_w is not None: - string.strncpy(mname_buf_w, vpi_buf + dstart, mname_len) - mname_buf_w[mname_len] = '\0' - if cur_cls is not None: viperlib.snprintf(key_buf_w, key_len_w + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_w) - else: viperlib.snprintf(key_buf_w, key_len_w + 1, "%s.%s", src_sha1, mname_buf_w) - string.strcpy(_g_vpi_wf_keys + sizet(_g_vpi_wf_count) * VPI_WF_KEY_LEN, key_buf_w) - _g_vpi_wf_vals[_g_vpi_wf_count] = sizet(wf_v) - _g_vpi_wf_count += 1 - if mname_buf_w is not None: stdlib.free(mname_buf_w) - if key_buf_w is not None: stdlib.free(key_buf_w) - pos = le + 1 - if cur_cls is not None: stdlib.free(cur_cls) - return 1 - - -# _lookup_vpi_willfree - query the cross-module P2 willFree cache (key shape = escape summary). -def _lookup_vpi_willfree(src_sha1: str, cls_name: str, method_name: str) -> int: - """查跨模块 willFree 形参掩码;0 = 无标注""" - if _g_vpi_wf_keys is None or _g_vpi_wf_count == 0: return 0 - if src_sha1 is None or method_name is None: return 0 - sha1_len: sizet = string.strlen(src_sha1) - cl_len: sizet = 0 - if cls_name is not None: cl_len = string.strlen(cls_name) - me_len: sizet = string.strlen(method_name) - key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len - kb: bytes = stdlib.malloc(key_len + 1) - if kb is None: return 0 - if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) - else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) - res: int = 0 - idx: int = 0 - while idx < _g_vpi_wf_count: - ent: bytes = _g_vpi_wf_keys + sizet(idx) * VPI_WF_KEY_LEN - # 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp - if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: - res = int(_g_vpi_wf_vals[idx]) - break - idx += 1 - stdlib.free(kb) - return res - - -# _vpi_param_annot_seg_is - equality check for a param-annotation segment (for the "str/bytes is not a generic pointer" rule). -def _vpi_param_annot_seg_is(src_sha1: str, cls_name: str, method_name: str, seg_idx: int, want: str) -> int: - """Check whether the definition-side param-annotation segment equals want exactly; 1=yes / 0=no.""" - if want is None or seg_idx < 0: return 0 - prm: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name) - if prm is None or prm[0] == '\0': return 0 - cur: sizet = 0 - # locate segment start: skip seg_idx '@'-separated segments - for seg in range(int(seg_idx)): - while prm[cur] != '\0' and prm[cur] != '@': cur += 1 - if prm[cur] == '\0': return 0 - cur += 1 - slen: sizet = 0 - while prm[cur + slen] != '\0' and prm[cur + slen] != '@': slen += 1 - wlen: sizet = string.strlen(want) - if slen != wlen: return 0 - i: sizet - for i in range(int(slen)): - if prm[cur + i] != want[i]: return 0 - return 1 - - -# _vpi_param_annot_seg_mode - value-reference mode of a param-annotation segment: 0=none / 1=ref / 2=temp. -# The definition side writes `T | t.CRef` / `T | t.CTempRef`; the call side mirrors it (auto-address). -def _vpi_param_annot_seg_mode(prm_annots: str, seg_idx: int) -> int: - """Return the value-reference mode encoded in a vpi param-annotation segment.""" - if prm_annots is None or seg_idx < 0: return 0 - cur: sizet = 0 - for seg in range(int(seg_idx)): - while prm_annots[cur] != '\0' and prm_annots[cur] != '@': cur += 1 - if prm_annots[cur] == '\0': return 0 - cur += 1 - slen: sizet = 0 - while prm_annots[cur + slen] != '\0' and prm_annots[cur + slen] != '@': slen += 1 - if slen == 0: return 0 - seg_buf: bytes = stdlib.malloc(slen + 1) - if seg_buf is None: return 0 - string.strncpy(seg_buf, prm_annots + cur, slen) - seg_buf[slen] = '\0' - mode: int = 0 - if string.strstr(seg_buf, "CTempRef") is not None: mode = 2 - elif string.strstr(seg_buf, "CRef") is not None: mode = 1 - elif seg_buf[0] == '&': - # The stub may carry the union marker (`T | t.CRef`, written by this compiler's StubGen) or - # the source syntax (`&T` / `&temp T`, written by TPC's StubGen). Accept both, else a - # cross-module value-reference call would silently lose its auto-address. - q: sizet = 0 - while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1 - if seg_buf[q] == '&': - q += 1 - while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1 - if slen - q >= 4 and string.strncmp(seg_buf + q, "temp", 4) == 0: mode = 2 - else: mode = 1 - stdlib.free(seg_buf) - return mode - - -# _ensure_vpi_param_annot_loaded - lazily load a module's vpi stub param-annotation cache. -# Mirror of the return variant; reads the .vpi file on miss, idempotent. -def _ensure_vpi_param_annot_loaded(src_sha1: str, temp_dir: str) -> int: - """Lazily load the module's vpi stub param-annotation cache; returns 1=ready / 0=failure.""" - if src_sha1 is None or temp_dir is None: return 0 - sha1_len: sizet = string.strlen(src_sha1) - if sha1_len < 2: return 0 - # module already has param cache entries -> skip (idempotent) - if _g_vpi_param_keys is not None and _g_vpi_param_count > 0: - pre_i: int - # Fast path: equal prefixes share the first two bytes (the module subdir name), - # so checking them skips strncmp for most entries. - s0: int = src_sha1[0] - s1: int = src_sha1[1] - for pre_i in range(int(_g_vpi_param_count)): - pre_key: bytes = _g_vpi_param_keys + pre_i * VPI_PARAM_KEY_LEN - if pre_key[0] == s0 and pre_key[1] == s1 and string.strncmp(pre_key, src_sha1, sha1_len) == 0 and pre_key[sha1_len] == '.': return 1 - # build path {temp_dir}/{sha1[0:2]}/{sha1}.vpi (char by char, avoiding %c formatting) - td_len: sizet = string.strlen(temp_dir) - path_len: sizet = td_len + sha1_len + 12 - vpi_path: bytes = stdlib.malloc(path_len) - if vpi_path is None: return 0 - string.strcpy(vpi_path, temp_dir) - vpi_path[td_len] = '/' - vpi_path[td_len + 1] = src_sha1[0] - vpi_path[td_len + 2] = src_sha1[1] - vpi_path[td_len + 3] = '/' - string.strcpy(vpi_path + td_len + 4, src_sha1) - string.strcpy(vpi_path + td_len + 4 + sha1_len, ".vpi") - f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R) - if f.closed: - stdlib.free(vpi_path) - return 0 - vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) - if vpi_buf is None: - f.close() - stdlib.free(vpi_path) - return 0 - bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE) - f.close() - if bytes_read > 0: - if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0 - _cache_vpi_param_annots_text(vpi_buf, src_sha1) - stdlib.free(vpi_buf) - stdlib.free(vpi_path) - return 1 - - -# _subst_template_arg - replace standalone template-parameter tokens in an annotation string. -# Replaces only boundary-delimited tokens (not inside sizet etc.); returns a new stdlib string. -def _subst_template_arg(annot: str, tparam: str, targ: str) -> str: - """Replace standalone tparam tokens in annot with targ.""" - if annot is None or tparam is None or targ is None: return None - alen: sizet = string.strlen(annot) - tplen: sizet = string.strlen(tparam) - tglen: sizet = string.strlen(targ) - if tplen == 0 or tplen > alen: return annot - # Allocate the worst-case upper bound: each replacement consumes tplen and writes tglen, - # so worst case is alen + (alen/tplen)*tglen + 1 (avoids writing past the heap). - max_sub: sizet = alen / tplen - if max_sub == 0: max_sub = 1 - out_size: sizet = alen + max_sub * tglen + 1 - out: bytes = stdlib.malloc(out_size) - if out is None: return None - opos: sizet = 0 - p: sizet = 0 - while p < alen: - # guard: fail fast (return None) if opos would exceed the allocation (never overrun) - if opos >= out_size: - stdlib.free(out) - return None - matched: int = 0 - if annot[p] == tparam[0] and p + tplen <= alen: - if string.strncmp(annot + p, tparam, tplen) == 0: - prev_ch: int = 0 - next_ch: int = 0 - if p > 0: prev_ch = annot[p - 1] - if p + tplen < alen: next_ch = annot[p + tplen] - prev_ok: int = 1 - if (prev_ch >= 'a' and prev_ch <= 'z') or (prev_ch >= 'A' and prev_ch <= 'Z') or (prev_ch >= '0' and prev_ch <= '9') or prev_ch == '_': prev_ok = 0 - next_ok: int = 1 - if (next_ch >= 'a' and next_ch <= 'z') or (next_ch >= 'A' and next_ch <= 'Z') or (next_ch >= '0' and next_ch <= '9') or next_ch == '_': next_ok = 0 - if prev_ok != 0 and next_ok != 0: - string.strncpy(out + opos, targ, tglen) - opos += tglen - p += tplen - matched = 1 - if matched == 0: - out[opos] = annot[p] - opos += 1 - p += 1 - out[opos] = '\0' - return out - - -# _lookup_vpi_ret_annot - look up a method's cached return-annotation string. -def _lookup_vpi_ret_annot(src_sha1: str, cls_name: str, method_name: str) -> str: - """Look up a method's cached return-annotation string; None if not found.""" - if _g_vpi_ret_keys is None or _g_vpi_ret_count == 0: return None - if src_sha1 is None or method_name is None: return None - sha1_len: sizet = string.strlen(src_sha1) - cl_len: sizet = 0 - if cls_name is not None: cl_len = string.strlen(cls_name) - me_len: sizet = string.strlen(method_name) - key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len - key_buf: bytes = stdlib.malloc(key_len + 1) - if key_buf is None: return None - if cls_name is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) - else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, method_name) - i: int - # Index fast path: when trusted (Built==Count) one probe returns the hit slot; - # a miss means truly absent, so skip the exact linear scan. Otherwise fall back. - hit_r: int = -1 - if _g_vpi_ret_idx_built == _g_vpi_ret_count and _g_vpi_ret_idx is not None and _g_vpi_ret_idx_cap > 0: - hit_r = _vpi_idx_find(_g_vpi_ret_keys, VPI_KEY_LEN, _g_vpi_ret_idx, _g_vpi_ret_idx_cap, key_buf) - else: - # Linear-scan fallback: equal keys share the first byte; skip candidates whose first byte differs. - key_first: int = key_buf[0] - for i in range(int(_g_vpi_ret_count)): - ent_r: bytes = _g_vpi_ret_keys + i * VPI_KEY_LEN - if ent_r[0] == key_first and string.strcmp(ent_r, key_buf) == 0: - hit_r = i - break - if hit_r >= 0: - stdlib.free(key_buf) - return _g_vpi_ret_strs + hit_r * VPI_RET_LEN - # Generic specialization fallback: list[AST*].get -> list[T].get template cache. - # Substitutes the type-param name (e.g. T) with the concrete arg; no registration needed. - if cls_name is not None: - lbkt_v: str = string.strchr(cls_name, '[') - if lbkt_v is not None: - rbkt_v: str = string.strrchr(cls_name, ']') - if rbkt_v is not None and rbkt_v > lbkt_v: - base_len_v: sizet = sizet(uint64(lbkt_v) - uint64(cls_name)) - param_len_v: sizet = sizet(uint64(rbkt_v) - uint64(lbkt_v) - 1) - if base_len_v > 0 and param_len_v > 0: - base_v: str = stdlib.malloc(base_len_v + 1) - param_v: str = stdlib.malloc(param_len_v + 1) - if base_v is not None and param_v is not None: - string.strncpy(base_v, cls_name, base_len_v) - base_v[base_len_v] = '\0' - string.strncpy(param_v, lbkt_v + 1, param_len_v) - param_v[param_len_v] = '\0' - # scan cache for {sha1}.{base}[xxx].{method} - gi: int - for gi in range(int(_g_vpi_ret_count)): - gk: bytes = _g_vpi_ret_keys + gi * VPI_KEY_LEN - if string.strncmp(gk, src_sha1, sha1_len) == 0 and gk[sha1_len] == '.' and string.strncmp(gk + sha1_len + 1, base_v, base_len_v) == 0 and gk[sha1_len + 1 + base_len_v] == '[': - gklb: str = string.strchr(gk + sha1_len + 1 + base_len_v, ']') - if gklb is not None: - tparam_len_v: sizet = sizet(uint64(gklb) - uint64(gk + sha1_len + 1 + base_len_v) - 1) - meth_start_v: str = gklb + 2 # skip ']' and '.' - if tparam_len_v > 0 and string.strcmp(meth_start_v, method_name) == 0: - tparam_v: str = stdlib.malloc(tparam_len_v + 1) - if tparam_v is not None: - string.strncpy(tparam_v, gk + sha1_len + 1 + base_len_v + 1, tparam_len_v) - tparam_v[tparam_len_v] = '\0' - new_annot_v: str = _subst_template_arg(_g_vpi_ret_strs + gi * VPI_RET_LEN, tparam_v, param_v) - stdlib.free(tparam_v) - stdlib.free(base_v) - stdlib.free(param_v) - stdlib.free(key_buf) - return new_annot_v - stdlib.free(base_v) - stdlib.free(param_v) - stdlib.free(key_buf) - return None - - -# _resolve_single_vpi_type_str - resolve a single type segment to LLVMType. -# Pure string parsing, no AST nodes. -def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.LLVMType*: - """Resolve a single type segment to LLVMType; None if unrecognized.""" - if seg is None or pool is None: return None - # Canonical pointer form is `T*` (also rawptr[T] / t.CPtr[T]); rewrite trailing `*` - # into rawptr[...] so specialized args like "{sha}.AST*" resolve instead of silently returning None. - seg_len_ps: sizet = string.strlen(seg) - star_end_ps: sizet = seg_len_ps - while star_end_ps > 0 and seg[star_end_ps - 1] == ' ': star_end_ps -= 1 - star_cnt_ps: sizet = 0 - star_base_end_ps: sizet = star_end_ps - while star_base_end_ps > 0 and seg[star_base_end_ps - 1] == '*': - star_cnt_ps += 1 - star_base_end_ps -= 1 - if star_cnt_ps > 0 and star_base_end_ps > 0: - # base segment length = star_base_end_ps; each rawptr[ + ] layer is 8 chars - star_buf_len_ps: sizet = star_base_end_ps + 8 * star_cnt_ps + 1 - star_buf_ps: bytes = stdlib.malloc(star_buf_len_ps) - if star_buf_ps is None: return None - star_buf_ps[0] = '\0' - star_k_ps: sizet = 0 - while star_k_ps < star_cnt_ps: - string.strcat(star_buf_ps, "rawptr[") - star_k_ps += 1 - star_off_ps: sizet = string.strlen(star_buf_ps) - string.strncpy(star_buf_ps + star_off_ps, seg, star_base_end_ps) - star_buf_ps[star_off_ps + star_base_end_ps] = '\0' - for star_k_ps in range(int(star_cnt_ps)): - string.strcat(star_buf_ps, "]") - star_ty_ps: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, star_buf_ps) - stdlib.free(star_buf_ps) - return star_ty_ps - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - # built-in aliases - if string.strcmp(seg, "str") == 0 or string.strcmp(seg, "bytes") == 0: return i8_ptr_ty - if string.strcmp(seg, "int") == 0: return mlirlite.Int32(pool) - if string.strcmp(seg, "bool") == 0: return mlirlite.Int1(pool) - # LLVM type strings: vpi stores "i32" for unannotated methods - # (see _cache_vpi_return_annots_text); parse it so generic template calls do not fail fast. - if string.strcmp(seg, "i32") == 0: return mlirlite.Int32(pool) - if string.strcmp(seg, "i64") == 0: return mlirlite.Int64(pool) - if string.strcmp(seg, "i16") == 0: return mlirlite.Int16(pool) - if string.strcmp(seg, "i8") == 0: return i8_ty - if string.strcmp(seg, "i1") == 0: return mlirlite.Int1(pool) - if string.strcmp(seg, "f32") == 0: return mlirlite.Float(pool) - if string.strcmp(seg, "f64") == 0: return mlirlite.Double(pool) - if string.strcmp(seg, "void") == 0: return mlirlite.Void(pool) - # vpi caches `-> None` verbatim as "None"; map None to Void so cross-module - # `-> None` methods do not fail fast with an unresolvable return type. - if string.strcmp(seg, "None") == 0: return mlirlite.Void(pool) - # rawptr[T] built-in raw pointer (equivalent to t.CPtr[T] -> Ptr(T)); vpi keeps the - # Subscript form verbatim. Same as TypeGenResolve's rawptr branch: Ptr(resolve(T)). - if string.strncmp(seg, "rawptr[", 7) == 0: - seg_len_rp: sizet = string.strlen(seg) - if seg_len_rp > 8 and seg[seg_len_rp - 1] == ']': - inner_len_rp: sizet = seg_len_rp - 8 - inner_buf_rp: bytes = stdlib.malloc(inner_len_rp + 1) - if inner_buf_rp is not None: - string.strncpy(inner_buf_rp, seg + 7, inner_len_rp) - inner_buf_rp[inner_len_rp] = '\0' - inner_ty_rp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_rp) - stdlib.free(inner_buf_rp) - if inner_ty_rp is not None: return mlirlite.Ptr(pool, inner_ty_rp) - return None - # t.CPtr[T] / c.CPtr[T] subscript pointers (nesting allowed, e.g. t.CPtr[t.CPtr] - # -> Ptr(Ptr(i8))); same as TypeGenResolve's Subscript(CPtr) branch: Ptr(resolve(T)). - cptr_sub_pfx: sizet = 0 - if string.strncmp(seg, "t.CPtr[", 7) == 0 or string.strncmp(seg, "c.CPtr[", 7) == 0: cptr_sub_pfx = 7 - if cptr_sub_pfx > 0: - seg_len_cp: sizet = string.strlen(seg) - if seg_len_cp > cptr_sub_pfx + 1 and seg[seg_len_cp - 1] == ']': - inner_len_cp: sizet = seg_len_cp - cptr_sub_pfx - 1 - inner_buf_cp: bytes = stdlib.malloc(inner_len_cp + 1) - if inner_buf_cp is not None: - string.strncpy(inner_buf_cp, seg + cptr_sub_pfx, inner_len_cp) - inner_buf_cp[inner_len_cp] = '\0' - inner_ty_cp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_cp) - stdlib.free(inner_buf_cp) - if inner_ty_cp is not None: return mlirlite.Ptr(pool, inner_ty_cp) - return None - # t.XXX / c.XXX built-in type mapping - if string.strncmp(seg, "t.", 2) == 0 or string.strncmp(seg, "c.", 2) == 0: - attr_nm: str = seg + 2 - # t.CExport is an export marker, not a real type; FuncGenDef maps it to Void, - # so normalize it here too or cross-module return types fail fast. - if string.strcmp(attr_nm, "CExport") == 0: return mlirlite.Void(pool) - # [stage 2] unified: CTypedef dynamic table first (library typedefs analyzed normally), - # on miss fall back to map_t_type (t/c library internal special-casing). - mapped_ty: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, attr_nm, None, None) - if mapped_ty is not None: return mapped_ty - return None - # module-qualified class name "memhub.MemBuddy" -> use last segment to look up struct - last_dot: str = string.strrchr(seg, '.') - if last_dot is not None: - cls_nm2: str = last_dot + 1 - mod_struct_ty: mlirlite.LLVMType* = GenApi.get_struct_type(cls_nm2) - if mod_struct_ty is not None: return mod_struct_ty - # Enum fallback: t.CEnum registers in EnumTable, not StructTable, so get_struct_type - # misses; fall back to the enum base type (usually i32) instead of returning None. - mod_enum_ty: mlirlite.LLVMType* = GenApi.get_enum_base_type(cls_nm2) - if mod_enum_ty is not None: return mod_enum_ty - return None - # bare class name -> global struct registry - bare_struct_ty: mlirlite.LLVMType* = GenApi.get_struct_type(seg) - if bare_struct_ty is not None: return bare_struct_ty - # bare enum name -> EnumTable fallback (as above: enums are not in StructTable) - bare_enum_ty: mlirlite.LLVMType* = GenApi.get_enum_base_type(seg) - if bare_enum_ty is not None: return bare_enum_ty - # bare typedef name (e.g. VOIDPTR): CTypedef dynamic table first, on miss fall back to map_t_type - mapped_bare: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, seg, None, None) - if mapped_bare is not None: return mapped_bare - return None - - -# _resolve_vpi_ret_annot_str - resolve a vpi return-annotation string to LLVMType. -# Pure string parsing (no AST nodes) to avoid corrupted kind() from cross-module AST reuse. -def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: - """Resolve a vpi return-annotation string directly to LLVMType without building AST nodes.""" - if ret_annot is None or pool is None: return None - - # tuple[T1, T2, ...] -> anonymous struct {T1, T2, ...} (multi-return packing), - # matching TypeGenMixin._resolve_generic_subscript_ty's tuple branch. - if string.strncmp(ret_annot, "tuple[", 6) == 0: - inner_len_t: sizet = 0 - while ret_annot[6 + inner_len_t] != ']' and ret_annot[6 + inner_len_t] != '\0': inner_len_t += 1 - if ret_annot[6 + inner_len_t] != ']' or inner_len_t == 0: return None - # The tuple return-type field list is mlirlite.ParamNode (new_param_node / - # paramnode_set_next / Struct all take rawptr[ParamNode]), not Function*. - tup_head_t: mlirlite.ParamNode* = None - tup_tail_t: mlirlite.ParamNode* = None - tup_count_t: int = 0 - p_t: sizet = 0 - while p_t < inner_len_t: - # skip leading spaces/commas - while p_t < inner_len_t and (ret_annot[6 + p_t] == ' ' or ret_annot[6 + p_t] == ','): p_t += 1 - seg_s_t: sizet = p_t - while p_t < inner_len_t and ret_annot[6 + p_t] != ',': p_t += 1 - seg_e_t: sizet = p_t - while seg_e_t > seg_s_t and ret_annot[6 + seg_e_t - 1] == ' ': seg_e_t -= 1 - if seg_e_t > seg_s_t: - seg_len_t: sizet = seg_e_t - seg_s_t - seg_buf_t: bytes = stdlib.malloc(seg_len_t + 1) - if seg_buf_t is None: return None - string.strncpy(seg_buf_t, ret_annot + 6 + seg_s_t, seg_len_t) - seg_buf_t[seg_len_t] = '\0' - seg_ty_t: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf_t) - stdlib.free(seg_buf_t) - if seg_ty_t is None: return None - tup_node_t: mlirlite.ParamNode* = mlirlite.new_param_node(pool, seg_ty_t) - if tup_node_t is None: return None - if tup_head_t is None: tup_head_t = tup_node_t - else: mlirlite.paramnode_set_next(tup_tail_t, tup_node_t) - tup_tail_t = tup_node_t - tup_count_t += 1 - if tup_head_t is None: return None - return mlirlite.Struct(pool, tup_head_t, tup_count_t, None) - - # Pure marker annotations (t.State / t.CExtern / t.CExport alone, optional t./c. prefix) - # -> void: decorator markers, not real return types, matching _inject_module_declare. - marker_len: sizet = string.strlen(ret_annot) - mk_s: sizet = 0 - while mk_s < marker_len and ret_annot[mk_s] == ' ': mk_s += 1 - mk_e: sizet = marker_len - while mk_e > mk_s and ret_annot[mk_e - 1] == ' ': mk_e -= 1 - mk_off_m: sizet = 0 - if mk_e - mk_s >= 2 and (ret_annot[mk_s] == 't' or ret_annot[mk_s] == 'c') and ret_annot[mk_s + 1] == '.': mk_off_m = 2 - mk_nlen: sizet = mk_e - mk_s - mk_off_m - if (mk_nlen == 5 and string.strncmp(ret_annot + mk_s + mk_off_m, "State", 5) == 0) or (mk_nlen == 7 and string.strncmp(ret_annot + mk_s + mk_off_m, "CExtern", 7) == 0) or (mk_nlen == 7 and string.strncmp(ret_annot + mk_s + mk_off_m, "CExport", 7) == 0): - return mlirlite.Void(pool) - - has_cptr: int = 0 - has_cref: int = 0 - has_cvoid: int = 0 - first_ty: mlirlite.LLVMType* = None - # Attributes of the segment that produced first_ty (for the has_cptr branch, N12): - # first_qual=qualified name; first_wl=whitelisted bare scalar; first_td=resolved bare typedef. - first_qual: int = 0 - first_wl: int = 0 - first_td: int = 0 - # Union-merge params for CPtr subscript segments (t.CPtr[T]/c.CPtr[T], nesting allowed): - # cptr_sub_lvl = outer Ptr level N, cptr_sub_ty = the segment's own type. -> Ptr^N(base). - cptr_sub_lvl: int = 0 - cptr_sub_ty: mlirlite.LLVMType* = None - - buf_len: sizet = string.strlen(ret_annot) - pos: sizet = 0 - while pos <= buf_len: - # skip leading spaces - while pos < buf_len and ret_annot[pos] == ' ': pos += 1 - seg_start: sizet = pos - # Split `|` only at the top level: `|` inside `[...]` belongs to a generic arg, - # so naive splitting would truncate `list[ast.AST*]`; depth-count on `[`/`]`. - depth_ra: int = 0 - while pos < buf_len: - ch_ra: char = ret_annot[pos] - if ch_ra == '[': depth_ra += 1 - elif ch_ra == ']': - if depth_ra > 0: depth_ra -= 1 - elif ch_ra == '|': - if depth_ra == 0: break - pos += 1 - seg_end: sizet = pos - # trim trailing spaces - while seg_end > seg_start and ret_annot[seg_end - 1] == ' ': seg_end -= 1 - if seg_end > seg_start: - seg_len2: sizet = seg_end - seg_start - seg_buf: bytes = stdlib.malloc(seg_len2 + 1) - if seg_buf is None: break - string.strncpy(seg_buf, ret_annot + seg_start, seg_len2) - seg_buf[seg_len2] = '\0' - # CPtr subscript segments must be recognized first: the definition side treats - # `X*[T]` as "CPtr side contributes N pointer levels, the other side the base". - if string.strncmp(seg_buf, "t.CPtr[", 7) == 0 or string.strncmp(seg_buf, "c.CPtr[", 7) == 0: - seg_ty_cs: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf) - if seg_ty_cs is not None: - # Count outer Ptr nesting (same as the definition side). Do not update the - # loop variable inside a case body: match sinks into a subject snapshot. - lvl_cs: int = 0 - cur_cs: mlirlite.LLVMType* = seg_ty_cs - while cur_cs is not None: - nxt_cs: mlirlite.LLVMType* = None - match cur_cs: - case mlirlite.LLVMType.Ptr(pointee_cs): - lvl_cs += 1 - nxt_cs = pointee_cs - case _: - break - cur_cs = nxt_cs - # take the max level across subscript segments (equivalent to level accumulation) - if lvl_cs > cptr_sub_lvl: - cptr_sub_lvl = lvl_cs - cptr_sub_ty = seg_ty_cs - elif string.strcmp(seg_buf, "t.CPtr") == 0 or string.strcmp(seg_buf, "CPtr") == 0: has_cptr = 1 - elif string.strcmp(seg_buf, "t.CRef") == 0 or string.strcmp(seg_buf, "CRef") == 0: has_cref = 1 - elif string.strcmp(seg_buf, "t.CTempRef") == 0 or string.strcmp(seg_buf, "CTempRef") == 0: has_cref = 1 - elif string.strcmp(seg_buf, "void") == 0 or string.strcmp(seg_buf, "CVoid") == 0 or string.strcmp(seg_buf, "void") == 0: - # Bare void is an alias of t.CVoid and must be handled the same, else the - # else branch sets first_td and produces the illegal Ptr(Void) (void* = i8*). - has_cvoid = 1 - else: - seg_ty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf) - if seg_ty is not None and first_ty is None: - first_ty = seg_ty - # Record the segment attribute (N12): the definition side keeps a bare pointee - # only for registered structs, generic params or whitelisted scalars; else i8*. - if string.strchr(seg_buf, 46) is not None: first_qual = 1 - if string.strcmp(seg_buf, "int") == 0 or string.strcmp(seg_buf, "char") == 0 or string.strcmp(seg_buf, "str") == 0 or string.strcmp(seg_buf, "bytes") == 0 or string.strcmp(seg_buf, "bool") == 0 or string.strcmp(seg_buf, "float") == 0 or string.strcmp(seg_buf, "double") == 0: - first_wl = 1 - # Preserve bare typedef aliases (N13): use GenApi.resolve_typedef_or_map (same API - # as the definition side); set only for non-pointer results (pointer aliases -> i8*). - if first_qual == 0 and first_wl == 0: - if first_ty.kind() != mlirlite.LLVMType.Ptr: - if GenApi.resolve_typedef_or_map(pool, seg_buf, None, None) is not None: first_td = 1 - stdlib.free(seg_buf) - pos += 1 # skip '|' - - if has_cptr != 0 or has_cref != 0: - if has_cvoid != 0: return i8_ptr_ty - if first_ty is not None: - # The result itself is void (e.g. an opaque typedef resolving to void): CPtr merge - # gives void* = i8*, matching has_cvoid (otherwise it would build illegal Ptr(void)). - match first_ty: - case mlirlite.LLVMType.Void(): - return i8_ptr_ty - case _: - pass - # Mirror the definition-side rules (N12/N13): qualified name / whitelisted scalar / - # registered struct / resolvable non-pointer typedef keeps pointee; else collapse to i8*. - if first_qual != 0 or first_wl != 0 or first_td != 0: return mlirlite.Ptr(pool, first_ty) - if first_ty.kind() == mlirlite.LLVMType.Struct: return mlirlite.Ptr(pool, first_ty) - if first_ty.kind() == mlirlite.LLVMType.Ptr: - # `| t.CPtr` always appends exactly one pointer level (same as the definition - # side's BinOp branch); no struct-absorption, so `T*` stays `T*` (never AST*). - return mlirlite.Ptr(pool, first_ty) - return i8_ptr_ty - return i8_ptr_ty - # CPtr subscript union merge: with another side's base type first_ty -> Ptr^N(first_ty); - # with only the subscript segment -> the segment's own type. No struct absorption here. - if cptr_sub_lvl > 0: - if first_ty is None: return cptr_sub_ty - res_ty_cs: mlirlite.LLVMType* = first_ty - for wrap_i_cs in range(int(cptr_sub_lvl)): - res_ty_cs = mlirlite.Ptr(pool, res_ty_cs) - return res_ty_cs - if has_cvoid != 0: - # t.CVoid | t.State: State is a decorator marker (ignored), t.CVoid -> void. - # Without this branch first_ty is None and cross-module return types fail fast. - return mlirlite.Void(pool) - if first_ty is not None: return first_ty - return None - - -# VpiReader - .vpi AST parser. Reads function signatures and CTypedef definitions from -# .vpi stub files and injects declares, filling in missing cross-module declarations. - -# .vpi read buffer size (256KB, matching PYI_BUF_SIZE) -VPI_READ_BUF_SIZE: sizet = 262144 - -# SHA1 hex char count (16, filename hex only, no trailing NUL). Named uniquely vs Phase1's -# SHA1_LEN (17 = 16 chars + NUL) to avoid cross-module CDefine shadowing. -SHA1_HEX_LEN: sizet = 16 - - -# _extract_sha1_from_path - extract SHA1 from a .vpi path. Takes the first 16 chars of the -# filename after the last separator; returns a malloc'd string (caller frees) or None. -def _extract_sha1_from_path(vpi_path: str) -> str: - """Extract the SHA1 from a .vpi path (first 16 chars of the filename, malloc'd).""" - if vpi_path is None: return None - # find the last path separator / or \ - total_len: sizet = string.strlen(vpi_path) - if total_len < SHA1_HEX_LEN + 4: # need at least "xxxx.vpi" length - return None - # scan backwards for the last / or \ - sep_pos: sizet = total_len - while sep_pos > 0: - sep_pos -= 1 - ch: char = vpi_path[sep_pos] - if ch == '/' or ch == '\\': - sep_pos += 1 - break - if sep_pos + SHA1_HEX_LEN > total_len: return None - # allocate buffer and copy SHA1 (16 chars + null terminator) - sha1_buf: str = stdlib.malloc(SHA1_HEX_LEN + 1) - if sha1_buf is None: return None - string.strncpy(sha1_buf, vpi_path + sep_pos, SHA1_HEX_LEN) - sha1_buf[SHA1_HEX_LEN] = '\0' - return sha1_buf - - -# Stub import-context bookkeeping goes through StubMergerStore (single source of truth). -# Unified to AppendImportedModule / AppendFromImport / NotePlainImport; pure bookkeeping. - - -# Recursive typedef pre-registration over the import closure. CTypedef tables are per-file, -# so unimported typedefs would degrade param types; this registers them (cycle/depth guarded). -define _TYPEDEF_VISIT_MAX = 1024 - -_g_vpi_typedef_visited: bytes = None -_g_vpi_typedef_visited_count: int = 0 -# Current visit-table capacity (elements). Must grow when full rather than silently dropping -# marks: dropping breaks cycle detection and re-parses the closure, risking pool OOM. -_g_vpi_typedef_visited_cap: int = 0 - - -def ResetVpiTypedefVisited() -> None: - """Clear the recursive-typedef cycle-detection marks before each module's stub injection.""" - global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count, _g_vpi_typedef_visited_cap - if _g_vpi_typedef_visited is not None: - stdlib.free(_g_vpi_typedef_visited) - _g_vpi_typedef_visited = None - _g_vpi_typedef_visited_count = 0 - _g_vpi_typedef_visited_cap = 0 - - -def _vpi_typedef_visited_check(sha1: str) -> int: - """Check whether sha1 was already recursively registered; 1=visited / 0=not visited (query only).""" - global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count - if sha1 is None: return 1 - if _g_vpi_typedef_visited is None: return 0 - vi: int - for vi in range(int(_g_vpi_typedef_visited_count)): - if string.strcmp(_g_vpi_typedef_visited + vi * 17, sha1) == 0: return 1 - return 0 - - -def _vpi_typedef_visited_mark(sha1: str) -> None: - """Mark sha1 as recursively registered (called after success, for cycle detection).""" - global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count, _g_vpi_typedef_visited_cap - if sha1 is None: return - if _g_vpi_typedef_visited is None: - _g_vpi_typedef_visited = stdlib.malloc(_TYPEDEF_VISIT_MAX * 17) - if _g_vpi_typedef_visited is None: - # fail-fast: without the cycle table the closure re-parses and exhausts the pool, so do not continue - stdio.printf("[VPI] cycle-proof visit table allocation failed (without the table the closure reparse explodes; aborting)\n") - stdio.fflush(None) - stdlib.exit(1) - string.memset(_g_vpi_typedef_visited, 0, _TYPEDEF_VISIT_MAX * 17) - _g_vpi_typedef_visited_count = 0 - _g_vpi_typedef_visited_cap = _TYPEDEF_VISIT_MAX - if _g_vpi_typedef_visited_count >= _g_vpi_typedef_visited_cap: - # Table full: grow (x2); never silently drop marks, which would break cycle detection - # and re-parse the same .vpi batch, allocating from the pool until it OOMs. - old_cap: int = _g_vpi_typedef_visited_cap - new_cap: int = old_cap * 2 - nb: bytes = stdlib.realloc(_g_vpi_typedef_visited, new_cap * 17) - if nb is None: - stdio.printf("[VPI] cycle-proof visit table growth failed cap=%d (lost marks cause a reparse explosion; aborting)\n", old_cap) - stdio.fflush(None) - stdlib.exit(1) - _g_vpi_typedef_visited = nb - string.memset(nb + old_cap * 17, 0, old_cap * 17) - _g_vpi_typedef_visited_cap = new_cap - string.strcpy(_g_vpi_typedef_visited + _g_vpi_typedef_visited_count * 17, sha1) - _g_vpi_typedef_visited_count += 1 - - -def _vpi_register_typedef_closure(pool: memhub.MemBuddy*, import_mods: str, temp_dir: str, depth: int) -> int: - """Recursively register CTypedef/CDefine from import-closure modules; always returns 0 (side-effect only).""" - if pool is None or import_mods is None or temp_dir is None: return 0 - if depth > 8: return 0 - td_len: sizet = string.strlen(temp_dir) - total_len: sizet = string.strlen(import_mods) - pos: sizet = 0 - while pos < total_len: - while pos < total_len and import_mods[pos] == ' ': pos += 1 - if pos >= total_len: break - m_start: sizet = pos - while pos < total_len and import_mods[pos] != ' ': pos += 1 - m_len: sizet = pos - m_start - if m_len == 0: continue - # t/c built-in libs are not compiled, so they have no vpi stub - if m_len == 1 and (import_mods[m_start] == 't' or import_mods[m_start] == 'c'): continue - mod_buf: bytes = stdlib.malloc(m_len + 1) - if mod_buf is None: continue - string.strncpy(mod_buf, import_mods + m_start, m_len) - mod_buf[m_len] = '\0' - sha1_r: str = StubMergerStore.LookupSha1ByModName(mod_buf) - stdlib.free(mod_buf) - if sha1_r is None: continue - if _vpi_typedef_visited_check(sha1_r) != 0: continue - # build {temp_dir}/{sha1[0:2]}/{sha1}.vpi by hand (same as _ensure_vpi_ret_annot_loaded, - # avoiding a StubMergerCollect cyclic dependency) - path_len_r: sizet = td_len + 16 + 12 - vpi_path_r: bytes = stdlib.malloc(path_len_r) - if vpi_path_r is None: continue - string.strcpy(vpi_path_r, temp_dir) - vpi_path_r[td_len] = '/' - vpi_path_r[td_len + 1] = sha1_r[0] - vpi_path_r[td_len + 2] = sha1_r[1] - vpi_path_r[td_len + 3] = '/' - string.strcpy(vpi_path_r + td_len + 4, sha1_r) - string.strcpy(vpi_path_r + td_len + 4 + 16, ".vpi") - f_r: fileio.File* = fileio.File(vpi_path_r, fileio.MODE.R) - if f_r.closed: - stdlib.free(vpi_path_r) - continue - vpi_buf_r: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) - if vpi_buf_r is None: - f_r.close() - stdlib.free(vpi_path_r) - continue - bytes_read_r: LONG = f_r.read_all(vpi_buf_r, VPI_READ_BUF_SIZE) - f_r.close() - if bytes_read_r > 0: - # mark as processed only after successfully reading the vpi (missing/read failure - # is not marked, so a later pass's stub injection can retry) - _vpi_typedef_visited_mark(sha1_r) - if bytes_read_r < VPI_READ_BUF_SIZE: vpi_buf_r[bytes_read_r] = 0 - lx_r: ast.Lexer* = ast.new_lexer(pool) - if lx_r is not None: - ast._lexer_init(lx_r, vpi_buf_r, pool) - tokens_r: ast.Token* = ast.tokenize(lx_r) - tree_r: ast.AST* = ast.parse_tokens(pool, tokens_r) - if tree_r is not None and tree_r.children is not None: - ch_r: list[ast.AST*]* = tree_r.children - cn_r: sizet = len(ch_r) - # Collect child imports (recursive closure) in two passes: first sum the required - # length, then allocate exactly and fill (no fixed cap, so nothing is dropped). - sub_need: sizet = 1 # trailing NUL - ci_c: sizet - for ci_c in range(int(cn_r)): - child_c: ast.AST* = ch_r.get(ci_c) - if child_c is None: continue - kk_c: int = child_c.kind() - if kk_c == ast.ASTKind.Import: - imp_c: ast.Import* = ast.Import*(child_c) - if imp_c.names is not None: - in_nc: sizet = len(imp_c.names) - for ii_c in range(int(in_nc)): - al_c: ast.Alias* = ast.Alias*(imp_c.names.get(ii_c)) - if al_c is not None and al_c.name is not None: sub_need += string.strlen(al_c.name) + 1 - elif kk_c == ast.ASTKind.ImportFrom: - ifr_c: ast.ImportFrom* = ast.ImportFrom*(child_c) - if ifr_c.module is not None: sub_need += string.strlen(ifr_c.module) + 1 - sub_imports: bytes = stdlib.malloc(sub_need) - if sub_imports is not None: - sub_imports[0] = '\0' - sub_len: sizet = 0 - ci_r: sizet - for ci_r in range(int(cn_r)): - child_r: ast.AST* = ch_r.get(ci_r) - if child_r is None: continue - kk_r: int = child_r.kind() - if kk_r == ast.ASTKind.Import: - imp_r: ast.Import* = ast.Import*(child_r) - if imp_r.names is not None: - in_n: sizet = len(imp_r.names) - ii_r: sizet - for ii_r in range(int(in_n)): - al_r: ast.Alias* = ast.Alias*(imp_r.names.get(ii_r)) - if al_r is not None and al_r.name is not None: - il_r: sizet = string.strlen(al_r.name) - if sub_len > 0: - sub_imports[sub_len] = ' ' - sub_len += 1 - string.strcpy(sub_imports + sub_len, al_r.name) - sub_len += il_r - sub_imports[sub_len] = '\0' - elif kk_r == ast.ASTKind.ImportFrom: - ifr_r: ast.ImportFrom* = ast.ImportFrom*(child_r) - if ifr_r.module is not None: - ml_r: sizet = string.strlen(ifr_r.module) - if sub_len > 0: - sub_imports[sub_len] = ' ' - sub_len += 1 - string.strcpy(sub_imports + sub_len, ifr_r.module) - sub_len += ml_r - sub_imports[sub_len] = '\0' - # second pass: register this module's AnnAssign CTypedef/CDefine - ci2_r: sizet - for ci2_r in range(int(cn_r)): - child2_r: ast.AST* = ch_r.get(ci2_r) - if child2_r is None: continue - if child2_r.kind() != ast.ASTKind.AnnAssign: continue - aa_r: ast.AnnAssign* = ast.AnnAssign*(child2_r) - if aa_r.annotation is None or aa_r.target is None or aa_r.value is None: continue - if GenApi._is_ctypedef_annotation(aa_r.annotation) != 0: _register_ctypedef_from_annassign(pool, aa_r) - elif GenApi.is_cdefine_annotation(aa_r.annotation) != 0: - if aa_r.target.kind() != ast.ASTKind.Name: continue - cdef_t: ast.Name* = ast.Name*(aa_r.target) - if cdef_t.id is None: continue - if aa_r.value.kind() == ast.ASTKind.Constant: - cn_v: ast.Constant* = ast.Constant*(aa_r.value) - if cn_v.const_kind == ast.CONST_FLOAT: GenApi.register_cdefine_double(pool, cdef_t.id, cn_v.float_val) - elif cn_v.const_kind == ast.CONST_INT: - cdef_val: int64 = GenApi.extract_cdefine_int_value(aa_r.value) - # 位宽与定义方同一套裁决(注解 → 右侧强转 → 值的最小可存宽度), - # 否则使用方拿 bits=0 只能兜底,跨模块位宽会与定义方不一致 - GenApi.register_cdefine_constant(pool, cdef_t.id, cdef_val, GenApi.cdefine_decl_bits(pool, aa_r.annotation, aa_r.value)) - # recurse into child imports (cycle guarded by the visited marks) - if sub_imports[0] != '\0': _vpi_register_typedef_closure(pool, sub_imports, temp_dir, depth + 1) - stdlib.free(sub_imports) - stdlib.free(vpi_buf_r) - stdlib.free(vpi_path_r) - return 0 - - -# ParseVpiIntoModule - parse a .vpi file and inject function declares into the current module. -# Walks top-level nodes, creates declares for FunctionDefs (deduped) and registers CTypedefs. -def ParseVpiIntoModule(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, vpi_path: str, trans: HT.Translator*) -> int: - """Parse a .vpi file and inject function declares into the current module.""" - if pool is None or mod is None or vpi_path is None or trans is None: return 0 - saved_cur: str = GenApi.get_current_file() - GenApi.set_current_file_type(vpi_path) - ret: int = _ParseVpiIntoModuleImpl(pool, mod, vpi_path, trans) - GenApi.set_current_file_type(saved_cur) - return ret - - -def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, vpi_path: str, trans: HT.Translator*) -> int: - """Actual implementation of ParseVpiIntoModule: parse a .vpi file and inject function declares.""" - if pool is None or mod is None or vpi_path is None or trans is None: return 0 - - # 1. read the .vpi file - f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R) - if f.closed: return 0 - vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) - if vpi_buf is None: - f.close() - return 0 - bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE) - f.close() - if bytes_read <= 0: - stdlib.free(vpi_buf) - return 0 - if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0 - - # 2. parse into AST - lx: ast.Lexer* = ast.new_lexer(pool) - if lx is None: - stdlib.free(vpi_buf) - return 0 - ast._lexer_init(lx, vpi_buf, pool) - tokens: ast.Token* = ast.tokenize(lx) - tree: ast.AST* = ast.parse_tokens(pool, tokens) - # Delay freeing vpi_buf: AST string fields may point into it (zero-copy tokens), - # so freeing early would dangle Name.id/Attribute.attr; freed at the end instead. - if tree is None: - stdlib.free(vpi_buf) - return 0 - - # extract the source module SHA1 for name mangling (after AST parse success to avoid leaks on early return) - src_sha1: str = _extract_sha1_from_path(vpi_path) - - # Cache method return annotations from vpi_buf text into stdlib (no AST tree: deep nodes - # may be corrupted later, but the raw vpi_buf text stays valid until the function ends). - # 单一入口:ret / param / 逃逸摘要一次喂齐(见 _cache_vpi_annots_text 注释)。 - # 本函数是 stub 注入路径的加载点,历史上这里漏喂逃逸摘要 → 跨模块漏检。 - _cache_vpi_annots_text(vpi_buf, src_sha1) - - # 3. walk top-level AST nodes - ch: list[ast.AST*]* = tree.children - if ch is None: - if src_sha1 is not None: stdlib.free(src_sha1) - return 1 - n: sizet = len(ch) - - # Collect the read module's own imports from the stub to build an isolated annotation - # context, replacing the current module's imports (e.g. t.CArray[str] needs 't' imported). - stub_imported_modules: str = None - stub_from_imports: str = None - # Bookkeeping goes through StubMergerStore (same as ImportsGenMixin.translate_import): - # `import X as Y -> from_imports "Y:X"`; `import a.b.c -> imported_modules "a"`. - stub_ctx: StubMergerStore.ImportCtx* = StubMergerStore.AllocImportCtx(pool, None, None) - for si in range(int(n)): - schild: ast.AST* = ch.get(si) - if schild is None: continue - sk: int = schild.kind() - if sk == ast.ASTKind.Import: - simp: ast.Import* = ast.Import*(schild) - if simp.names is not None: - sn_n: sizet = len(simp.names) - for sni in range(int(sn_n)): - sal: ast.Alias* = ast.Alias*(simp.names.get(sni)) - if sal is not None and sal.name is not None: StubMergerStore.NotePlainImport(pool, stub_ctx, sal.name, sal.asname) - elif sk == ast.ASTKind.ImportFrom: - sifr: ast.ImportFrom* = ast.ImportFrom*(schild) - sifr_mod: str = sifr.module - # Relative imports: resolve the package prefix and __init__ detection from the module - # name table. Falls back to the old rel_path route when no module-name evidence exists. - pkg_prefix_rt: str = None - if sifr.level > 0 and sifr_mod is not None and sifr_mod[0] != '\0': - _sib_sha_rt: str = StubMergerStore.LookupSha1ByModNameSuffix(sifr_mod) - if _sib_sha_rt is not None: - _sub_mod_rt: str = StubMergerStore.LookupModNameBySha1(_sib_sha_rt) - if _sub_mod_rt is not None: - _dot_rt: str = string.strrchr(_sub_mod_rt, '.') - if _dot_rt is not None: - _pfx_len_rt: sizet = sizet(uint64(_dot_rt) - uint64(_sub_mod_rt)) - if _pfx_len_rt > 0: - _my_mod_rt: str = StubMergerStore.LookupModNameBySha1(src_sha1) - # Confirm: this stub's module name must equal the prefix exactly - # (i.e. this stub is the package __init__); else defer to rel_path. - _conf_rt: int = 0 - if _my_mod_rt is not None and string.strlen(_my_mod_rt) == _pfx_len_rt: - if string.strncmp(_my_mod_rt, _sub_mod_rt, _pfx_len_rt) == 0: _conf_rt = 1 - if _conf_rt != 0: - pkg_prefix_rt = pool.alloc(_pfx_len_rt + 1) - if pkg_prefix_rt is not None: - string.strncpy(pkg_prefix_rt, _sub_mod_rt, _pfx_len_rt) - pkg_prefix_rt[_pfx_len_rt] = '\0' - sifr_mod_q: str = sifr_mod - if sifr_mod is not None and sifr_mod[0] != '\0' and sifr.level == 1: - if pkg_prefix_rt is not None: - qtotal_p: sizet = string.strlen(pkg_prefix_rt) + 1 + string.strlen(sifr_mod) + 1 - qbuf_p: str = pool.alloc(qtotal_p) - if qbuf_p is not None: - qbuf_p[0] = '\0' - string.strcat(qbuf_p, pkg_prefix_rt) - string.strcat(qbuf_p, ".") - string.strcat(qbuf_p, sifr_mod) - sifr_mod_q = qbuf_p - else: - rel_p_r: str = StubMergerStore.LookupSha1RelPath(src_sha1) - if rel_p_r is not None: - slash_r: str = string.strrchr(rel_p_r, '/') - if slash_r is not None: - pkg_len_r: sizet = string.strlen(rel_p_r) - string.strlen(slash_r) - if pkg_len_r > 0: - qtotal_r: sizet = pkg_len_r + 1 + string.strlen(sifr_mod) + 1 - qbuf_r: str = pool.alloc(qtotal_r) - if qbuf_r is not None: - string.strncpy(qbuf_r, rel_p_r, pkg_len_r) - qbuf_r[pkg_len_r] = '.' - string.strcpy(qbuf_r + pkg_len_r + 1, sifr_mod) - sifr_mod_q = qbuf_r - if sifr_mod is not None: - if stub_ctx is not None: - stub_ctx.ImportedModules = StubMergerStore.AppendImportedModule(pool, stub_ctx.ImportedModules, sifr_mod_q) - # Plan A: package __init__ re-exported classes are registered as alias entries under - # the package sha1 (only for __init__; uses pkg_prefix_rt, falling back to rel_path). - is_pkg_init_rt: int = 0 - if pkg_prefix_rt is not None: is_pkg_init_rt = 1 - if is_pkg_init_rt == 0: - rel_path_rt: str = StubMergerStore.LookupSha1RelPath(src_sha1) - if rel_path_rt is not None: - # [FIX] Use strstr(rel_path, "__init__.vp") instead of an offset-based strcmp, - # which was always false for real package paths and never triggered re-export. - if string.strstr(rel_path_rt, "__init__.vp") is not None: is_pkg_init_rt = 1 - # Resolve the from-imported module sha1: relative imports put leading dots in `level`, - # not in `module`, so prefix-qualified name lookup must be used. - sub_sha1_rt: str = None - if sifr.level > 0: - if sifr_mod is not None and sifr_mod[0] != '\0': - # Prefer the prefix-qualified exact name (sifr_mod_q, e.g. ast.base); the - # suffix match is only a fallback since it can hit another same-suffix module. - sub_sha1_rt = StubMergerStore.LookupSha1ByModName(sifr_mod_q) - if sub_sha1_rt is None: sub_sha1_rt = StubMergerStore.LookupSha1ByModNameSuffix(sifr_mod) - else: sub_sha1_rt = StubMergerStore.LookupSha1ByModName(sifr_mod) - # [D26 fail-fast] Do not silently skip when a package __init__ from-import cannot - # resolve the submodule sha1: report package and submodule names for diagnosis. - if is_pkg_init_rt != 0 and sub_sha1_rt is None and sifr_mod is not None and sifr_mod[0] != '\0': - GenApi.fatal_error(sifr, f"Package stub from-import submodule sha1 lookup failed: " f"package={rel_path_rt if rel_path_rt is not None else '?'}, " f"submodule='{sifr_mod}' (after prefix completion='{sifr_mod_q}'); " f"classes of this submodule cannot be registered, and annotations referencing them will fail to resolve later", "NameError") - if sifr.names is not None: - sifr_nn: sizet = len(sifr.names) - sifri: sizet - for sifri in range(int(sifr_nn)): - sial: ast.Alias* = ast.Alias*(sifr.names.get(sifri)) - if sial is not None and sial.name is not None: - slocal: str = sial.name - sorig: str = None - if sial.asname is not None: - slocal = sial.asname - sorig = sial.name - # `from X import *` also writes "*:mod" (same as - # ImportsGenMixin.translate_from_import_names); lookup_from_import relies on it. - if stub_ctx is not None: - stub_ctx.FromImports = StubMergerStore.AppendFromImport(pool, stub_ctx.FromImports, slocal, sifr_mod_q, sorig) - # Register re-exported classes under the package sha1 (by alias if asname); - # star has no concrete class name and is only recorded above. - if string.strcmp(sial.name, "*") != 0: - if is_pkg_init_rt != 0 and sub_sha1_rt is not None: - _inject_reexport_class_declare(pool, mod, sial.name, slocal, sub_sha1_rt, src_sha1, trans) - - # replace the current module's imports with the stub's own (correct annotation context) - if stub_ctx is not None: - stub_imported_modules = stub_ctx.ImportedModules - stub_from_imports = stub_ctx.FromImports - imported_modules: str = trans._imported_modules - from_imports: str = trans._from_imports - if stub_imported_modules is not None: imported_modules = stub_imported_modules - if stub_from_imports is not None: from_imports = stub_from_imports - # Add the source module itself to the stub annotation context: stub annotations may - # reference their own CDefines, but stub imports only list imported modules. - if src_sha1 is not None: - stub_rel_src: str = StubMergerStore.LookupSha1RelPath(src_sha1) - if stub_rel_src is not None: - sr_len_src: sizet = string.strlen(stub_rel_src) - if sr_len_src > 3 and string.strncmp(stub_rel_src + sr_len_src - 3, ".vp", 3) == 0: sr_len_src -= 3 - if sr_len_src > 0: - src_mod_buf: str = stdlib.malloc(sr_len_src + 1) - if src_mod_buf is not None: - sri_src: sizet - for sri_src in range(int(sr_len_src)): - ch_src: char = stub_rel_src[sri_src] - if ch_src == '/' or ch_src == '\\': src_mod_buf[sri_src] = '.' - else: src_mod_buf[sri_src] = ch_src - src_mod_buf[sr_len_src] = '\0' - imported_modules = StubMergerStore.AppendImportedModule(pool, imported_modules, src_mod_buf) - stdlib.free(src_mod_buf) - - # Recursively pre-register the stub import closure's CTypedef/CDefine: CTypedef tables are - # per-file, so an unimported typedef would degrade param types to i8* and misreport CastError. - if imported_modules is not None: _vpi_register_typedef_closure(pool, imported_modules, GenApi.get_temp_dir(), 0) - - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - - # function table info - funcs_ptr: GenTypes.FuncEntry* = trans._funcs - max_funcs: int = 1024 - - # REPL (ReplStubLayoutMode): restore full class layouts and vtable method indices from - # {sha1}.meta first, then run the vpi injection below. Always 0 in batch mode. - if ReplStubLayoutMode != 0: - _repl_stub_collect_module(pool, mod, ch, n, trans, src_sha1, stub_imported_modules, stub_from_imports) - - # Pass 1A: register ALL ClassDefs (fields + structs) first, without methods, so a class - # defined after a global's AnnAssign is still registered before that AnnAssign resolves. - i0: sizet - for i0 in range(int(n)): - child0: ast.AST* = ch.get(i0) - if child0 is None: continue - if child0.kind() != ast.ASTKind.ClassDef: continue - class_cd0: ast.ClassDef* = ast.ClassDef*(child0) - if class_cd0 is None or class_cd0.name is None: continue - cls_name_v0: str = class_cd0.name - # Skip generic classes (class X[T]): the stub template signature has a class-level type - # param T that cannot be resolved; specialized method defines are already emitted in Phase1. - tp_stub0: list[str]* = class_cd0.type_params - if tp_stub0 is not None and len(tp_stub0) > 0: continue - _inject_class_struct_declare(pool, mod, class_cd0, cls_name_v0, trans, src_sha1, imported_modules, from_imports) - - # App pre-registration (Phase A-pre, _pre_allow_dep_inject) skips method/function declare - # injection: its only goal is structs/enums and package re-export aliases, not transient declares. - pre_skip_fn_declares: int = 0 - if trans is not None and trans._pre_allow_dep_inject != 0: pre_skip_fn_declares = 1 - - # Pass 1B: process method declares only after all classes are registered, since method - # params/returns may reference classes defined later in the file (forward references). - for i1 in range(int(n)): - child1: ast.AST* = ch.get(i1) - if child1 is None: continue - if child1.kind() != ast.ASTKind.ClassDef: continue - class_cd1: ast.ClassDef* = ast.ClassDef*(child1) - if class_cd1 is None or class_cd1.name is None: continue - cls_name_v1: str = class_cd1.name - # skip generic classes (same criterion as pass 1A) - tp_stub1: list[str]* = class_cd1.type_params - if tp_stub1 is not None and len(tp_stub1) > 0: continue - cls_children1: list[ast.AST*]* = class_cd1.children - if cls_children1 is None: continue - cn21: sizet = len(cls_children1) - for ci21 in range(int(cn21)): - m_child1: ast.AST* = cls_children1.get(ci21) - if m_child1 is None: continue - if m_child1.kind() != ast.ASTKind.FunctionDef: continue - m_fd1: ast.FunctionDef* = ast.FunctionDef*(m_child1) - if m_fd1 is None or m_fd1.name is None: continue - # App pre-registration skips method declares (see pre_skip_fn_declares above). - if pre_skip_fn_declares == 0: - _inject_method_declare(pool, mod, m_fd1, cls_name_v1, trans, src_sha1, imported_modules, from_imports, i32_ty, i8_ptr_ty, funcs_ptr, max_funcs) - - i: sizet - for i in range(int(n)): - child: ast.AST* = ch.get(i) - if child is None: continue - kd: int = child.kind() - - # FunctionDef -> extract name/params/return type -> inject declare - if kd == ast.ASTKind.FunctionDef: - fd: ast.FunctionDef* = ast.FunctionDef*(child) - if fd is None or fd.name is None: continue - # App pre-registration skips function declares (see pre_skip_fn_declares): - # their annotations may reference classes not yet registered this pass. - if pre_skip_fn_declares != 0: continue - _inject_function_declare(pool, mod, fd, trans, src_sha1, imported_modules, from_imports, i32_ty, i8_ptr_ty, funcs_ptr, max_funcs) - continue - - # AnnAssign with t.CTypedef -> register CTypedef; - # plain module-level globals (e.g. tokens._op_head) -> external global declare - if kd == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(child) - if aa is None: continue - if aa.target is not None and aa.target.kind() == ast.ASTKind.Name: - dbg_tn: ast.Name* = ast.Name*(aa.target) - if dbg_tn.id is not None and dbg_tn.id[0] != '\0': # questionable - if string.strcmp(dbg_tn.id, "irq_handler_t") == 0 or string.strcmp(dbg_tn.id, "gdt") == 0 or string.strcmp(dbg_tn.id, "KBD_BUF_SIZE") == 0: - dbg_ct: int = GenApi._is_ctypedef_annotation(aa.annotation) - dbg_cd: int = GenApi.is_cdefine_annotation(aa.annotation) - if GenApi._is_ctypedef_annotation(aa.annotation) != 0: _register_ctypedef_from_annassign(pool, aa) - elif GenApi.is_cdefine_annotation(aa.annotation) != 0: - # Register vpi CDefine definitions (e.g. keyboard.vp KBD_BUF_SIZE) into the current - # file's CDefine table so later t.CArray counts in the same vpi can resolve them. - if aa.target is not None and aa.target.kind() == ast.ASTKind.Name and aa.value is not None: - cdef_tgt: ast.Name* = ast.Name*(aa.target) - if cdef_tgt.id is not None: - # Skip string CDefines (version numbers etc.): extract_cdefine_int_value - # fails fast on string constants, and count refs only need integer CDefines. - cdef_is_str: int = 0 - cdef_is_float: int = 0 - cdef_float_val: double = 0.0 - if aa.value.kind() == ast.ASTKind.Constant: - cn_v: ast.Constant* = ast.Constant*(aa.value) - if cn_v.const_kind == ast.CONST_STR: cdef_is_str = 1 - elif cn_v.const_kind == ast.CONST_FLOAT: - cdef_is_float = 1 - cdef_float_val = cn_v.float_val - if cdef_is_str == 0 and cdef_is_float != 0: - # Float CDefine (e.g. FRAGMENTATION_THRESHOLD: t.CDefine = 0.7): register via - # register_cdefine_double (integer extraction would fail fast). - GenApi.register_cdefine_double(pool, cdef_tgt.id, cdef_float_val) - elif cdef_is_str == 0: - cdef_val: int64 = GenApi.extract_cdefine_int_value(aa.value) - # 位宽与定义方同一套裁决(注解 → 右侧强转 → 值的最小可存宽度) - GenApi.register_cdefine_constant(pool, cdef_tgt.id, cdef_val, GenApi.cdefine_decl_bits(pool, aa.annotation, aa.value)) - else: - _inject_global_variable_declare(pool, mod, aa, trans, stub_imported_modules, stub_from_imports, i32_ty, i8_ptr_ty, src_sha1) - continue - - # ClassDef -> already registered in the first passes (fields + method declares), skip here. - # (A single pass would fail when classes are defined after globals; see pass 1A above.) - - # Import/ImportFrom -> skip (nothing to process) - - # 不要 free src_sha1:它会被后续阶段继续引用(模块/类型注册按 `.Name` 命名, - # 之后 resolve_annotation_type 仍会对它 strcmp)——在此 free 会造成 heap-use-after-free - # (现场:alloc @ _extract_sha1_from_path → free @ 本函数 → 读 @ resolve_annotation_type:2065)。 - # 跨阶段字符串按本仓惯例常驻;若将来确需回收,应改为在**存入注册表处**拷贝一份到 PermPool, - # 而不是在解析结束时释放。 - # free vpi_buf only after all tree processing (see the parsing comment) - stdlib.free(vpi_buf) - return 1 - - -# On-demand dependency vpi injection (resolves the mutual wait between package __init__ -# re-exports and submodule stubs). Re-entrancy table dedups by sha1; failures stay non-fatal. -define VPI_INJ_MAX = 1024 -_vpi_inj_sha1s: bytes = None -_vpi_inj_count: int = 0 -_vpi_inj_depth: int = 0 - - -def ResetVpiInjectVisited() -> None: - """Clear the on-demand injection reentry table before each module's stub-injection session.""" - global _vpi_inj_sha1s, _vpi_inj_count, _vpi_inj_depth - if _vpi_inj_sha1s is not None: - stdlib.free(_vpi_inj_sha1s) - _vpi_inj_sha1s = None - _vpi_inj_count = 0 - _vpi_inj_depth = 0 - - -def _ensure_dep_vpi_injected(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, dep_sha1: str, trans: HT.Translator*) -> int: - """Inject dep_sha1's .vpi into the current module on demand (reentry-guarded); 1=ready / 0=failure.""" - global _vpi_inj_sha1s, _vpi_inj_count, _vpi_inj_depth - if pool is None or mod is None or dep_sha1 is None or trans is None: return 0 - if dep_sha1[0] == '\0': return 0 - if _vpi_inj_sha1s is None: - _vpi_inj_sha1s = stdlib.malloc(VPI_INJ_MAX * 17) - if _vpi_inj_sha1s is None: return 0 - string.memset(_vpi_inj_sha1s, 0, VPI_INJ_MAX * 17) - _vpi_inj_count = 0 - # already injected / currently injecting (cycle) -> treat as ready and short-circuit - ii: int - for ii in range(int(_vpi_inj_count)): - if string.strcmp(_vpi_inj_sha1s + sizet(ii) * 17, dep_sha1) == 0: return 1 - if _vpi_inj_count >= VPI_INJ_MAX or _vpi_inj_depth >= 32: return 0 - temp_dir: str = GenApi.get_temp_dir() - if temp_dir is None: return 0 - td_len_i: sizet = string.strlen(temp_dir) - sh_len_i: sizet = string.strlen(dep_sha1) - sub_i: str = Config.slice_subdir(dep_sha1, Config.Sha1SliceLevel) - sub_len_i: sizet = 0 - if sub_i is not None: sub_len_i = string.strlen(sub_i) - path_len_i: sizet = td_len_i + sub_len_i + sh_len_i + 8 - path_i: bytes = stdlib.malloc(path_len_i) - if path_i is None: return 0 - if sub_len_i > 0: viperlib.snprintf(path_i, path_len_i, "%s/%s/%s.vpi", temp_dir, sub_i, dep_sha1) - else: viperlib.snprintf(path_i, path_len_i, "%s/%s.vpi", temp_dir, dep_sha1) - vf_i: fileio.File* = fileio.File(path_i, fileio.MODE.R) - if vf_i.closed: - stdlib.free(path_i) - return 0 - vf_i.close() - # register before injecting: recursive injection of the same sha1 short-circuits (cycle guard) - my_idx_i: int = _vpi_inj_count - string.strcpy(_vpi_inj_sha1s + sizet(my_idx_i) * 17, dep_sha1) - _vpi_inj_count += 1 - _vpi_inj_depth += 1 - ret_i: int = ParseVpiIntoModule(pool, mod, path_i, trans) - _vpi_inj_depth -= 1 - stdlib.free(path_i) - if ret_i != 1: - # Injection failed: unregister (move the last entry into this slot, keeping LIFO order), - # else later requests for the same sha1 stay short-circuited and the class never registers. - _vpi_inj_count -= 1 - if my_idx_i != _vpi_inj_count: - string.strcpy(_vpi_inj_sha1s + sizet(my_idx_i) * 17, _vpi_inj_sha1s + sizet(_vpi_inj_count) * 17) - return 0 - return 1 - - -# _inject_reexport_class_declare - register a package re-exported class as an alias entry under the -# package sha1, reusing the submodule's registered entry so ast.Attribute resolves by package. -def _inject_reexport_class_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, src_name: str, local_name: str, sub_sha1: str, pkg_sha1: str, trans: HT.Translator*) -> int: - """Register a package re-exported class as an alias entry under the package sha1.""" - if pool is None or mod is None or src_name is None or local_name is None: return 0 - if sub_sha1 is None or sub_sha1[0] == '\0' or pkg_sha1 is None or pkg_sha1[0] == '\0': return 0 - if src_name[0] == '\0' or local_name[0] == '\0': return 0 - # idempotent: skip if local_name is already registered under the package sha1 - if GenApi.find_struct_by_module(local_name, pkg_sha1) is not None: return 1 - # find the submodule source entry (only classes have entries; functions/constants are skipped) - src_entry: GenTypes.StructEntry* = GenApi.find_struct_by_module(src_name, sub_sha1) - if src_entry is None or src_entry.Ty is None: - # Source stub not injected yet (package __init__ and submodule wait on each other; see - # _ensure_dep_vpi_injected): inject sub_sha1's vpi on demand and retry once; else skip. - if _ensure_dep_vpi_injected(pool, mod, sub_sha1, trans) != 0: src_entry = GenApi.find_struct_by_module(src_name, sub_sha1) - if src_entry is None or src_entry.Ty is None: return 0 - # register the alias entry (bypassing register_struct's Ty dedup) - if GenApi.register_struct_alias(pool, local_name, pkg_sha1, src_entry) is None: return 0 - GenApi.add_visible_struct(pool, local_name) - return 1 - - -# _vpi_tattr_name / _vpi_class_has_packed - detect the vpi class packed decorator. -# The stub keeps @c.Attribute(t.attr.packed); struct injection must mirror the packed flag (B5). -def _vpi_tattr_name(node: ast.AST*, attr_name: str) -> int: - """Extract the outermost attribute name from t.attr. / t.attr.(args) and compare.""" - if node is None: return 0 - if node.kind() == ast.ASTKind.Call: - cl2: ast.Call* = ast.Call*(node) - if cl2.func is None: return 0 - return _vpi_tattr_name(cl2.func, attr_name) - if node.kind() == ast.ASTKind.Attribute: - at2: ast.Attribute* = ast.Attribute*(node) - if at2.attr is None: return 0 - return 1 if string.strcmp(at2.attr, attr_name) == 0 else 0 - return 0 - - -def _vpi_class_has_packed(decorator_list: list[ast.AST*]*) -> int: - """Check whether a vpi ClassDef decorator list contains t.attr.packed.""" - if decorator_list is None: return 0 - dn: sizet = len(decorator_list) - if dn == 0: return 0 - for di in range(int(dn)): - deco: ast.AST* = decorator_list.get(di) - if deco is None: continue - if deco.kind() == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(deco) - if cl.func is None or cl.func.kind() != ast.ASTKind.Attribute: continue - at0: ast.Attribute* = ast.Attribute*(cl.func) - if at0.attr is None or string.strcmp(at0.attr, "Attribute") != 0: continue - if cl.args is None: continue - an: sizet = len(cl.args) - for ai in range(int(an)): - ag: ast.AST* = cl.args.get(ai) - if ag is not None and _vpi_tattr_name(ag, "packed") != 0: return 1 - else: - if _vpi_tattr_name(deco, "packed") != 0: return 1 - return 0 - - -def _vpi_class_is_renum(class_cd: ast.ClassDef*) -> int: - """Check whether a vpi ClassDef is a REnum (base class contains t.REnum); 1=yes / 0=no.""" - if class_cd is None or class_cd.bases is None: return 0 - bases: list[ast.AST*]* = class_cd.bases - bn: sizet = len(bases) - for bi in range(int(bn)): - base_node: ast.AST* = bases.get(bi) - if base_node is None: continue - bk: int = base_node.kind() - # Attribute(t.REnum) form - if bk == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(base_node) - if at.attr is not None and string.strcmp(at.attr, "REnum") == 0: return 1 - # Name(REnum) form (from t import REnum) - elif bk == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(base_node) - if nm.id is not None and string.strcmp(nm.id, "REnum") == 0: return 1 - return 0 - - -# _inject_class_struct_declare - register a vpi ClassDef as a struct type (with fields) so bare -# class-name annotations in the same vpi resolve. Mirrors ClassGenDef.translate_class_def. -def _inject_class_struct_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_cd: ast.ClassDef*, cls_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, name_prefix: str = None) -> int: - """Register a vpi ClassDef as a struct type (with fields) so bare class-name annotations resolve.""" - if pool is None or mod is None or class_cd is None or cls_name is None: return 0 - # Flattened name for nested classes (`{outer}_{inner}`); register even if the base class is not - # registered (the "no registration on field failure" rule lets multi-pass convergence handle it). - real_name: str = cls_name - if name_prefix is not None and name_prefix[0] != '\0': - p_len: sizet = string.strlen(name_prefix) - c_len: sizet = string.strlen(cls_name) - flat_buf_c: str = pool.alloc(p_len + 1 + c_len + 1) - if flat_buf_c is not None: - string.strcpy(flat_buf_c, name_prefix) - flat_buf_c[p_len] = '_' - string.strcpy(flat_buf_c + p_len + 1, cls_name) - real_name = flat_buf_c - # REPL (ReplStubLayoutMode): if this class is already driven by the {sha1}.meta path, yield to it: - # mark visible if registered, defer if the base is pending, skip if the layout is untrusted. - if ReplStubLayoutMode != 0: - se: ReplStubClass* = _repl_stub_find(_repl_stub_key(pool, src_sha1, real_name)) - if se is not None and se.State != _REPL_SS_EMPTY: - if se.State == _REPL_SS_PENDING: _repl_stub_try(se, mod) - if se.State == _REPL_SS_OK: GenApi.add_visible_struct(pool, real_name) - return 1 - # 1. Already-registered check: just mark visible, except forward-decl placeholders - # (IsForwardDecl != 0) which must go through the registration path to fill the same Ty. - existing_c: GenTypes.StructEntry* = None - fill_fwd_c: int = 0 - if src_sha1 is not None and src_sha1[0] != '\0': - existing_c = GenApi.find_struct_by_module(real_name, src_sha1) - if existing_c is not None and existing_c.Ty is not None: - if existing_c.IsForwardDecl != 0: fill_fwd_c = 1 - else: - GenApi.add_visible_struct(pool, real_name) - return 1 - # Skip REnum classes (class X(t.REnum)): their layout is computed from variants by - # ClassGenEnum._translate_renum_def; registering as a plain struct would be wrong. - if _vpi_class_is_renum(class_cd) != 0: return 0 - # Rule 5.5 evidence must be set before field annotation resolution: mark_module_class - # records "{src_sha1}.{real_name}" and points the class-definition window at this class. - GenApi.mark_module_class(pool, src_sha1, real_name) - # 1.5 Nested classes/structs: vpi keeps them as indented ClassDefs; the definition side emits - # flat names (`{outer}_{inner}`), so recurse first with the same naming before field resolution. - _inject_nested_class_declares(pool, mod, class_cd, real_name, trans, src_sha1, imported_modules, from_imports) - # Restore the "defining class" window: the nested recursion's mark_module_class changes - # CurDefClassName to the nested name (A_B), which would corrupt rule 5.5 self-reference evidence. - GenApi.mark_module_class(pool, src_sha1, real_name) - # 2. collect field types (AnnAssign children, mirroring ClassGenDef.translate_class_def) - field_names_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX) - field_types_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX) - if field_names_buf is None or field_types_buf is None: return 0 - string.memset(field_names_buf, 0, 8 * GenTypes.FIELD_MAX) - string.memset(field_types_buf, 0, 8 * GenTypes.FIELD_MAX) - field_count: int = 0 - cls_children_c: list[ast.AST*]* = class_cd.children - if cls_children_c is not None: - cn_c: sizet = len(cls_children_c) - for ci_c in range(int(cn_c)): - m_child_c: ast.AST* = cls_children_c.get(ci_c) - if m_child_c is None: continue - if m_child_c.kind() != ast.ASTKind.AnnAssign: continue - m_aa_c: ast.AnnAssign* = ast.AnnAssign*(m_child_c) - if m_aa_c is None or m_aa_c.target is None: continue - if m_aa_c.target.kind() != ast.ASTKind.Name: continue - m_tgt_c: ast.Name* = ast.Name*(m_aa_c.target) - if m_tgt_c.id is None: continue - field_ty_c: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, m_aa_c.annotation, imported_modules, from_imports, trans, 0) - if field_ty_c is None: - # field type parse failed: do not register this class (leave it to the source module), not fatal - return 0 - if field_count < GenTypes.FIELD_MAX: - field_names_buf[field_count] = sizet(m_tgt_c.id) - field_types_buf[field_count] = sizet(field_ty_c) - field_count += 1 - # 3. The vpi stub has no class fields -> field_count is 0; registering an empty class would make - # the source module hit an empty struct and raise InternalCompilerError, so do not register. - if field_count == 0: return 0 - # 4. build the ParamNode list - first_node_c: mlirlite.ParamNode* = None - prev_node_c: mlirlite.ParamNode* = None - for fi_c in range(int(field_count)): - fty_c: mlirlite.LLVMType* = (mlirlite.LLVMType*)(field_types_buf[fi_c]) - pnode_c: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) - if pnode_c is None: continue - string.memset(pnode_c, 0, mlirlite.ParamNode.__sizeof__()) - pnode_c.Ty = fty_c - if first_node_c is None: first_node_c = pnode_c - if prev_node_c is not None: prev_node_c.Next = pnode_c - prev_node_c = pnode_c - # 4. Named struct type name "sha1.ClassName" (matching ClassGenDef); nested classes use the - # flat real_name, consistent with ClassGenDef._translate_nested_classdefs. - type_name_c: str = real_name - if src_sha1 is not None and src_sha1[0] != '\0': - # [D7 truncation discipline] The old fixed 64-byte buffer silently truncated class names - # over 46 chars; allocate the exact length so the struct name matches ClassGenDef. - sh_len_c: sizet = string.strlen(src_sha1) - cn_len_c: sizet = string.strlen(real_name) - name_buf_c: str = pool.alloc(sh_len_c + 1 + cn_len_c + 1) - if name_buf_c is not None: - string.strcpy(name_buf_c, src_sha1) - name_buf_c[sh_len_c] = '.' - string.strcpy(name_buf_c + sh_len_c + 1, real_name) - type_name_c = name_buf_c - struct_ty_c: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node_c, field_count, type_name_c) - if struct_ty_c is None: return 0 - # 5. Under StdlibAlloc (Phase B) deep-copy into perm_pool to survive cross-file resets - # (matching ClassGenDef.translate_class_def). - if GenApi.get_stdlib_alloc() != 0: - st_copy_c: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, struct_ty_c) - if st_copy_c is not None: struct_ty_c = st_copy_c - # 5.5 Forward-decl placeholder hit -> write in place into *existing.Ty (same as ClassGenDef's - # fill_fwd) so register_struct's Ty-pointer dedup works and the StructTable does not overflow. - if fill_fwd_c != 0 and existing_c is not None and existing_c.Ty is not None: - fwd_name_c: str = mlirlite.get_struct_name(existing_c.Ty) - fwd_fields_c: mlirlite.ParamNode* = mlirlite.get_struct_fields(struct_ty_c) - fwd_fc_c: int = mlirlite.get_struct_fcount(struct_ty_c) - *existing_c.Ty = mlirlite.LLVMType.Struct(fwd_fields_c, fwd_fc_c, fwd_name_c) - struct_ty_c = existing_c.Ty - type_name_c = fwd_name_c - existing_c.IsForwardDecl = 0 - existing_c.FieldCount = 0 - # 6. register into the current module (emit the type def) + global StructTable + visibility - mlirlite.module_add_named_type(mod, pool, struct_ty_c) - # B5: vpi keeps @c.Attribute(t.attr.packed); cross-module injected structs must be packed too - # (<{...}>), else the referencing module computes GEPs with a non-packed layout (fields misread). - if _vpi_class_has_packed(class_cd.decorator_list) != 0: mlirlite.module_set_named_type_packed(mod, pool, struct_ty_c, 1) - # Register under the flat real_name: cross-module `Outer.Inner` lookups query the flat-name - # entry (same source as the definition-side StructGen registration name). - entry_c: GenTypes.StructEntry* = GenApi.register_struct(pool, real_name, struct_ty_c, src_sha1) - if entry_c is None: return 0 - # B5: sync IsPacked on the registered entry; ensure_struct_def_in_module reads it to decide - # packing, otherwise the injected cross-module struct is non-packed (GEP layout mismatch). - if _vpi_class_has_packed(class_cd.decorator_list) != 0: entry_c.IsPacked = 1 - # Also register fields into the StructEntry field table (name + type): a set Ty with an empty - # field table makes lookup_field_by_class fail by name, which silently zeroes reads. - afi_c: int = 0 - for afi_c in range(int(field_count)): - af_name_c: str = str(field_names_buf[afi_c]) - af_ty_c: mlirlite.LLVMType* = (mlirlite.LLVMType*)(field_types_buf[afi_c]) - GenApi.add_field(pool, entry_c, af_name_c, af_ty_c) - GenApi.add_visible_struct(pool, real_name) - return 1 - - -# _inject_nested_class_declares - recursively register nested classes of a vpi class. -# Uses the flat name {outer}_{inner} (matching ClassGenDef._translate_nested_classdefs). -def _inject_nested_class_declares(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_cd: ast.ClassDef*, prefix_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str) -> None: - """Recursively register all nested ClassDefs of class_cd as flat-named types.""" - if pool is None or mod is None or class_cd is None or prefix_name is None: return - children: list[ast.AST*]* = class_cd.children - if children is None: return - cn: sizet = len(children) - for ci in range(int(cn)): - child_ast: ast.AST* = children.get(ci) - if child_ast is None: continue - if child_ast.kind() != ast.ASTKind.ClassDef: continue - child_cd: ast.ClassDef* = ast.ClassDef*(child_ast) - if child_cd is None or child_cd.name is None or child_cd.name[0] == '\0': continue - _inject_class_struct_declare(pool, mod, child_cd, child_cd.name, trans, src_sha1, imported_modules, from_imports, prefix_name) - - -# _ensure_full_value_types - register full definitions for cross-module value types. -# Opaque is not enough for llc (value structs/arrays/full def needed); pointer refs need only opaque. -def _ensure_full_value_types(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> None: - """Recursively register full definitions for value structs and array element structs.""" - if mod is None or pool is None or ty is None: return - match ty: - case mlirlite.LLVMType.Struct(_, _, _): - full_se_h: GenTypes.StructEntry* = GenApi.find_struct_by_type(ty) - if full_se_h is not None and full_se_h.Ty is not None: - mlirlite.module_add_named_type(mod, pool, full_se_h.Ty) - # B5: sync the packed flag when injecting value types (matching - # ensure_struct_def_in_module); without it the referencing module emits a non-packed {..}. - if full_se_h.IsPacked != 0: mlirlite.module_set_named_type_packed(mod, pool, full_se_h.Ty, 1) - case mlirlite.LLVMType.Array(aelem_h, _): - if aelem_h is not None: _ensure_full_value_types(mod, pool, aelem_h) - case _: - pass - - -# _AggregateParamTy - unified rule for bare-struct (aggregate) param types; must stay in sync with -# FuncGenDef's extern branch and the call side. C ABI -> by value (or i64 fallback); internal -> Ptr(struct). -def _AggregateParamTy(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*, is_c_abi: int) -> mlirlite.LLVMType*: - """Bare-struct param type: C ABI by value / internal Ptr(struct).""" - if mod is None or pool is None or ty is None: return ty - if is_c_abi != 0: - # true opaque sentinel: fcount < 0 - is_opaque_ap: int = 0 - match ty: - case mlirlite.LLVMType.Struct(_, fcount_ap, _): - if fcount_ap < 0: is_opaque_ap = 1 - case _: - pass - if is_opaque_ap == 0 and GenApi.find_struct_by_type(ty) is not None: - _ensure_full_value_types(mod, pool, ty) - return ty - return mlirlite.Int64(pool) - ptr_ty_ap: mlirlite.LLVMType* = mlirlite.Ptr(pool, ty) - mlirlite.module_ensure_opaque_for_type(mod, pool, ptr_ty_ap) - return ptr_ty_ap - - -# _inject_global_variable_declare - create a module-level global variable declare from a vpi AnnAssign. -# Needed so cross-module from-import reads of globals can find them via find_global_in_module. -def _inject_global_variable_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, aa: ast.AnnAssign*, trans: HT.Translator*, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, src_sha1: str) -> None: - """Create a module-level global variable external declare from a vpi AnnAssign.""" - if pool is None or mod is None or aa is None or trans is None: return - tgt_v: ast.AST* = aa.target - if tgt_v is None or tgt_v.kind() != ast.ASTKind.Name: return - nm_v: ast.Name* = ast.Name*(tgt_v) - if nm_v is None or nm_v.id is None: return - gname_v: str = nm_v.id - if gname_v[0] == '\0': return - # Self-injection guard (full-translation stage only): a module's own globals are emitted by - # TranslatorGenMixin.handle_module_level_var; re-injecting its own vpi would cause llc redefinition. - if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None and src_sha1 is not None: - if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return - # Mangled name: prefix with the source module SHA1 so the define side and from-import lookup - # agree; a bare short name would remain undefined. - gv_name_m: str = gname_v - # CExport globals: the definition side uses the bare name (@gp), so the stub declare must too, - # else cross-module references emit @sha1.gp and ld.lld reports an undefined symbol. - is_gv_cexport: int = 0 - if aa.annotation is not None: is_gv_cexport = GenApi.has_decorator_marker(aa.annotation, "CExport") - if src_sha1 is not None and src_sha1[0] != '\0' and is_gv_cexport == 0: - ml_v: sizet = string.strlen(src_sha1) + 1 + string.strlen(gname_v) + 1 - gv_name_m = pool.alloc(ml_v) - if gv_name_m is not None: viperlib.snprintf(gv_name_m, ml_v, "%s.%s", src_sha1, gname_v) - # dedup: walk the module's global variable list (matching the mangled name) - cur_v: mlirlite.GlobalVariable* = mod.GlobalHead - while cur_v is not None: - if cur_v.Name is not None and string.strcmp(cur_v.Name, gv_name_m) == 0: return - cur_v = cur_v.Next - # resolve the annotation type (bare class -> Ptr(struct), CArray -> element pointer) - resolved_v: mlirlite.LLVMType* = None - if aa.annotation is not None: - resolved_v = GenApi.resolve_annotation_type(pool, aa.annotation, imported_modules, from_imports, trans) - if resolved_v is None: - return # type parse failed, skip: the call side has a fail-fast location - gv_ty_v: mlirlite.LLVMType* = resolved_v - match resolved_v: - case mlirlite.LLVMType.Struct(_, _, _): - # Object classes (@t.Object/@t.CVTable: object-as-pointer) -> Ptr(struct), matching - # handle_module_level_var; plain t.CStruct value classes keep a value instance. - gv_entry_v: GenTypes.StructEntry* = GenApi.find_struct_by_type(resolved_v) - if gv_entry_v is not None and (gv_entry_v.HasNew != 0 or gv_entry_v.HasVTable != 0 or gv_entry_v.IsOOP != 0): gv_ty_v = mlirlite.Ptr(pool, resolved_v) - else: gv_ty_v = resolved_v - case mlirlite.LLVMType.Array(aelem_v, acount_v): - # Fixed-size arrays (CArray[T, N]) keep the full [N x T] type, matching - # handle_module_level_var; decaying to an element pointer would later read garbage. - if acount_v > 0: gv_ty_v = resolved_v - else: - # variable-length array (CArray[T, None]): element pointer (C array decay) - gv_ty_v = mlirlite.Ptr(pool, aelem_v) - case _: - gv_ty_v = resolved_v - # Ensure embedded structs have an opaque declaration (llc "use of undefined type"), - # matching TranslatorGenMixin; value types get full definitions via _ensure_full_value_types. - _ensure_full_value_types(mod, pool, gv_ty_v) - mlirlite.module_ensure_opaque_for_type(mod, pool, gv_ty_v) - gv_v: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, gv_name_m, gv_ty_v) - if gv_v is None: return - mlirlite.module_add_global(mod, gv_v) - gv_v.Linkage = "external" - - -# _inject_function_declare - create a declare for a single FunctionDef: detect CExport/CExtern/State, -# compute the mangled name, dedup via find_func_in_module, create the declare and register it. -def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, fd: ast.FunctionDef*, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, funcs_ptr: GenTypes.FuncEntry*, max_funcs: int) -> None: - """Create and inject a declare for a FunctionDef into the module.""" - if fd.name is None: return - - # Self-injection guard (full-translation stage only): a module's own top-level functions are - # emitted by FuncGenDef; re-injecting its own vpi would create a conflicting duplicate declare. - if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None: - if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return - - # detect CExtern/State/CExport markers (from the return-type annotation) - has_extern: int = 0 - has_state: int = 0 - has_export: int = 0 - if fd.returns is not None: - has_extern = GenApi.has_decorator_marker(fd.returns, "CExtern") - has_state = GenApi.has_decorator_marker(fd.returns, "State") - has_export = GenApi.has_decorator_marker(fd.returns, "CExport") - # Bare-struct param rule (see _AggregateParamTy): C ABI externs (t.CExtern/t.State) by value; - # the rest (incl. t.CExport) keep Ptr(struct). Same source as FuncGenDef.is_extern_decl. - agg_is_c_abi: int = 0 - if has_extern != 0 or has_state != 0: agg_is_c_abi = 1 - - # compute the mangled name: - # CExport/CExtern/State/main functions use the bare name, others get the source module SHA1 prefix - use_bare_name: int = 0 - if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: - mangled_name: str = fd.name - use_bare_name = 1 - else: mangled_name = GenApi._mangle_name_with_sha1(src_sha1, fd.name) - - # This file's own bare-name declarations take priority: extern/state/export symbols carry no SHA1 - # prefix, so an injected dependency of the same name would silently win. Only bare names matter. - if use_bare_name != 0 and GenApi.is_file_func(fd.name) != 0: return - - # dedup: skip if the function already exists - existing: mlirlite.Function* = GenApi.find_func_in_module(mod, mangled_name) - if existing is not None: return - # also check the bare name (in non-bare mode a CExport function may have been registered another way) - if use_bare_name == 0: - existing_bare: mlirlite.Function* = GenApi.find_func_in_module(mod, fd.name) - if existing_bare is not None: return - - # Infer the return type: prefer the cached return-annotation string (extracted right after tree - # parse) and resolve it directly, avoiding corrupted kind() from cross-module AST reuse. - ret_ty: mlirlite.LLVMType* = None - ret_annot_fd: str = _lookup_vpi_ret_annot(src_sha1, None, fd.name) - if ret_annot_fd is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot_fd, i32_ty, i8_ptr_ty) - if ret_ty is None and fd.returns is not None: - ret_ty = GenApi.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans) - # if the return type is a pure t.State (no real type), use Void - if ret_ty is None and fd.returns is not None: - # t.State/t.CExtern are storage-class decorator markers, not real return types. An annotation - # like "HANDLE | t.State" has a real type and must not map to Void; only pure markers do. - is_pure_marker_rt: int = 0 - rk2_rt: int = fd.returns.kind() - if rk2_rt == ast.ASTKind.Name or rk2_rt == ast.ASTKind.Constant: is_pure_marker_rt = 1 - elif rk2_rt == ast.ASTKind.Attribute: - pm_at: ast.Attribute* = ast.Attribute*(fd.returns) - if pm_at.attr is not None and (string.strcmp(pm_at.attr, "State") == 0 or string.strcmp(pm_at.attr, "CExtern") == 0 or string.strcmp(pm_at.attr, "CExport") == 0): - is_pure_marker_rt = 1 - if is_pure_marker_rt != 0: ret_ty = mlirlite.Void(pool) - elif has_state != 0 or has_extern != 0 or has_export != 0: - # Has a real type but resolution failed: fall back to the external function return-type - # whitelist (e.g. FindFirstFileA/GetStdHandle -> i8* HANDLE) instead of truncating to i32. - ret_ty = GenApi.infer_external_func_ret_ty(pool, fd.name) - # [S4 fail-fast] A return annotation that does not resolve is not silently downgraded to i32; - # no annotation (fd.returns is None) is a legitimate default and keeps i32 below. - if ret_ty is None and fd.returns is not None: - GenApi.fatal_error(fd.returns, f"extern declaration '{fd.name}' return type annotation failed to resolve", "NameError") - # default return i32 - if ret_ty is None: ret_ty = i32_ty - - # A value struct return type (e.g. Mat4) needs a full definition: declaring %Mat4 opaque makes - # llc report "use of undefined type". Register the full definition. - _ensure_full_value_types(mod, pool, ret_ty) - mlirlite.module_ensure_opaque_for_type(mod, pool, ret_ty) - - # create the declare - func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) - if func is None: return - - # extract params and add them to the declare - param_count: int = 0 - fd_defaults: t.CPtr = None - fd_default_count: int = 0 - if fd.args is not None: - args_node: ast.Arguments* = ast.Arguments*(fd.args) - if args_node.args is not None: - args_list: list[ast.AST*]* = args_node.args - param_count = int(len(args_list)) - # Param-count upper-bound sentinel (fail-fast): a corrupted vpi tree node makes __len__() - # return a huge value, so add_param runs massively and the Params chain balloons. - if param_count < 0 or param_count > 128: - stdio.printf("[VPIS] abnormal parameter count: function=%s param_count=%d (fd.args=%llu, tree may be overwritten; aborting)\n", fd.name, param_count, uint64(fd.args)) - # convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed) - VLogger.repl_abort() - pi: sizet - for pi in range(int(param_count)): - ag: ast.Arg* = ast.Arg*(args_list.get(pi)) - if ag is None: continue - pname: str = ag.arg - if pname is None: pname = "arg" - # resolve the param type - param_ty: mlirlite.LLVMType* = i8_ptr_ty - # Prefer the vpi text annotation cache (same source as return types): corrupted tree - # nodes make AST resolve return garbage types and mis-size cross-module declare params. - pseg_ty: mlirlite.LLVMType* = _resolve_vpi_param_annot_seg(pool, src_sha1, None, fd.name, int(pi), i32_ty, i8_ptr_ty) - if pseg_ty is not None: - match pseg_ty: - case mlirlite.LLVMType.Struct(_, _, _): - # Bare struct param: C ABI by value / internal Ptr(struct) - # (criterion and fallback in _AggregateParamTy; keep the three sites in sync) - param_ty = _AggregateParamTy(mod, pool, pseg_ty, agg_is_c_abi) - case mlirlite.LLVMType.Array(aelem_ty, _): - param_ty = mlirlite.Ptr(pool, aelem_ty) - case _: - param_ty = pseg_ty - elif ag.annotation is not None: - resolved_ty: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, ag.annotation, imported_modules, from_imports, trans) - if resolved_ty is not None: - # CArray param -> element pointer ty* (C array decay, matching the compiler side). - # Bare struct param -> _AggregateParamTy (same criterion). - match resolved_ty: - case mlirlite.LLVMType.Struct(_, _, _): - param_ty = _AggregateParamTy(mod, pool, resolved_ty, agg_is_c_abi) - case mlirlite.LLVMType.Array(aelem_ty, _): - param_ty = mlirlite.Ptr(pool, aelem_ty) - case _: - param_ty = resolved_ty - else: - # [S4 fail-fast] An annotation that does not resolve is not silently downgraded - # to i8* (no annotation is a legitimate default and keeps i8*). - GenApi.fatal_error(ag.annotation, f"extern declaration '{fd.name}' parameter '{pname}' type annotation failed to resolve", "NameError") - mlirlite.add_param(pool, func, param_ty, pname) - # Text-cache repair: a corrupted vpi tree truncates args, leaving too few declare params; - # complete them from the vpi text param annotations so declare matches define. - param_count = _repair_declare_params_from_text(pool, func, src_sha1, None, fd.name, param_count, i32_ty, i8_ptr_ty) - # *args varargs: set IsVarArg=1 so the call side applies C ABI promotions (i8/i16 -> i32, - # float -> double), avoiding misread %d/%x formats. - if args_node.vararg is not None: func.IsVarArg = 1 - # Default params: vpi signatures include '= '; register the defaults list so - # cross-module calls missing args fill from the table instead of reading stack garbage. - if args_node.defaults is not None: - fd_dlist: list[ast.AST*]* = args_node.defaults - fd_default_count = int(len(fd_dlist)) - if fd_default_count < 0 or fd_default_count > 128: - # A corrupted args.defaults node makes len return a huge value, so - # _register_global_defaults would over-allocate and write out of bounds; skip it. - stdio.printf("[VPIS] abnormal default parameter count: function=%s default_count=%d (tree may be overwritten; skipping default value registration)\n", fd.name, fd_default_count) - fd_defaults = None - fd_default_count = 0 - else: fd_defaults = fd_dlist - - # register into the function table (using the bare fd.name, not mangled_name) - if funcs_ptr is not None: - cur_count: int = trans._func_count - if GenApi.add_func_to_table(funcs_ptr, cur_count, fd.name, func, max_funcs, fd_defaults, fd_default_count, param_count, None, src_sha1) == 0: - trans._func_count = cur_count + 1 - # Complete the global function-table Spec (params all added, matching FuncGenDef): without it - # find_func_entry_global() sees an empty Spec and vtable generation fails to find the type. - GenApi.update_global_func_spec(fd.name, func) - - -# _inject_method_declare - create a declare for a ClassDef method. Mangled name is -# {src_sha1}.{ClassName}.{method}; param types come from vpi annotations (self defaults to i8*). -def _inject_method_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, m_fd: ast.FunctionDef*, cls_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, funcs_ptr: GenTypes.FuncEntry*, max_funcs: int) -> None: - """Create and inject a declare for a ClassDef method into the module.""" - if m_fd is None or m_fd.name is None or cls_name is None: return - if src_sha1 is None: return - - # Self-injection guard (full-translation stage only): a module's own method defines are emitted - # by ClassGenVTable; re-injecting its own vpi would create conflicting duplicate declares. - if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None: - if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return - - # build the method lookup name: ClassName.method - lookup_name_len: sizet = string.strlen(cls_name) + 1 + string.strlen(m_fd.name) + 1 - lookup_name: str = pool.alloc(lookup_name_len) - if lookup_name is None: return - viperlib.snprintf(lookup_name, lookup_name_len, "%s.%s", cls_name, m_fd.name) - - # mangled name: {src_sha1}.{ClassName}.{method} - mangled_len: sizet = string.strlen(src_sha1) + 1 + lookup_name_len - mangled_name: str = pool.alloc(mangled_len) - if mangled_name is None: return - viperlib.snprintf(mangled_name, mangled_len, "%s.%s", src_sha1, lookup_name) - - # dedup: skip if the method already exists - existing: mlirlite.Function* = GenApi.find_func_in_module(mod, mangled_name) - if existing is not None: return - - # Infer the return type: prefer the cached return-annotation string (extracted right after tree - # parse); resolving m_fd.returns directly may yield garbage from a corrupted vpi tree node. - ret_ty: mlirlite.LLVMType* = None - ret_annot: str = _lookup_vpi_ret_annot(src_sha1, cls_name, m_fd.name) - if ret_annot is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty) - if ret_ty is None and m_fd.returns is not None: - ret_ty = GenApi.resolve_annotation_type(pool, m_fd.returns, imported_modules, from_imports, trans) - # infer __new__ without annotation as a class pointer (it returns a self pointer; i32 would truncate) - if ret_ty is None and string.strcmp(m_fd.name, "__new__") == 0: - cls_ty: mlirlite.LLVMType* = GenApi.get_struct_type(cls_name) - if cls_ty is not None: ret_ty = mlirlite.Ptr(pool, cls_ty) - # [S4 fail-fast] A return annotation that does not resolve is not silently downgraded to i32; - # unannotated __new__ / normal methods keep i32 as the legitimate default below. - if ret_ty is None and m_fd.returns is not None: - GenApi.fatal_error(m_fd.returns, f"extern method '{cls_name}.{m_fd.name}' return type annotation failed to resolve", "NameError") - # Unannotated __init__ -> Void, matching the definition side (ClassGenVTable always uses Void); - # defaulting to i32 would split declare i32 / define void. - if ret_ty is None and string.strcmp(m_fd.name, "__init__") == 0: ret_ty = mlirlite.Void(pool) - if ret_ty is None: ret_ty = i32_ty - - # create the declare - func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) - if func is None: return - - # extract params and add them to the declare - param_count: int = 0 - md_defaults: t.CPtr = None - md_default_count: int = 0 - if m_fd.args is not None: - args_node: ast.Arguments* = ast.Arguments*(m_fd.args) - if args_node.args is not None: - args_list: list[ast.AST*]* = args_node.args - param_count = int(len(args_list)) - # Param-count upper-bound sentinel (fail-fast): same cause as _inject_function_declare - # (a corrupted vpi tree node makes __len__() return a huge value). - if param_count < 0 or param_count > 128: - stdio.printf("[VPIS] abnormal parameter count: method=%s.%s param_count=%d (m_fd.args=%llu, tree may be overwritten; aborting)\n", cls_name, m_fd.name, param_count, uint64(m_fd.args)) - # convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed) - VLogger.repl_abort() - pi: sizet - for pi in range(int(param_count)): - ag: ast.Arg* = ast.Arg*(args_list.get(pi)) - if ag is None: continue - pname: str = ag.arg - if pname is None: pname = "arg" - # self/cls (first param): cross-module declares use the class pointer (%"sha1.Class"*) - # instead of i8*, matching _translate_method's Ptr(StructTy) and removing ABI ambiguity. - if pi == 0 and (string.strcmp(pname, "self") == 0 or string.strcmp(pname, "cls") == 0): - cls_ty_sd: mlirlite.LLVMType* = GenApi.get_struct_type(cls_name) - if cls_ty_sd is not None: - mlirlite.add_param(pool, func, mlirlite.Ptr(pool, cls_ty_sd), pname) - continue - param_ty: mlirlite.LLVMType* = i8_ptr_ty - # Prefer the vpi text annotation cache (same source as return types): corrupted tree - # nodes mis-size declare params, causing trunc i64->i32 and a caller/callee mismatch. - pseg_ty: mlirlite.LLVMType* = _resolve_vpi_param_annot_seg(pool, src_sha1, cls_name, m_fd.name, int(pi), i32_ty, i8_ptr_ty) - if pseg_ty is not None: - match pseg_ty: - case mlirlite.LLVMType.Struct(_, _, _): - # Bare struct param: methods are always internal TPV symbols (mangled - # {sha1}.{Class}.{method}, no C ABI define), so is_c_abi=0 -> Ptr(struct). - param_ty = _AggregateParamTy(mod, pool, pseg_ty, 0) - case mlirlite.LLVMType.Array(aelem_ty, _): - param_ty = mlirlite.Ptr(pool, aelem_ty) - case _: - param_ty = pseg_ty - elif ag.annotation is not None: - resolved_ty: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, ag.annotation, imported_modules, from_imports, trans) - if resolved_ty is not None: - # CArray param -> element pointer ty* (C array decay, matching the compiler side). - # Bare struct param -> _AggregateParamTy (same criterion, is_c_abi=0). - match resolved_ty: - case mlirlite.LLVMType.Struct(_, _, _): - param_ty = _AggregateParamTy(mod, pool, resolved_ty, 0) - case mlirlite.LLVMType.Array(aelem_ty, _): - param_ty = mlirlite.Ptr(pool, aelem_ty) - case _: - param_ty = resolved_ty - else: - # [S4 fail-fast] An annotation that does not resolve is not silently downgraded - # to i8* (no annotation is a legitimate default and keeps i8*). - GenApi.fatal_error(ag.annotation, f"extern method '{cls_name}.{m_fd.name}' parameter '{pname}' type annotation failed to resolve", "NameError") - mlirlite.add_param(pool, func, param_ty, pname) - - # Text-cache repair: a corrupted vpi tree truncates args, leaving too few declare params; - # complete them from the vpi text param annotations so declare matches define. - param_count = _repair_declare_params_from_text(pool, func, src_sha1, cls_name, m_fd.name, param_count, i32_ty, i8_ptr_ty) - - # OOP iter protocol: the __next__ stub declare carries a hidden __stop_iter_flag__ (i1*), - # matching _translate_method's define; `for x in obj` calls __next__(self, flag). - if m_fd.name is not None and string.strcmp(m_fd.name, "__next__") == 0: - i1_ty_si: mlirlite.LLVMType* = mlirlite.Int1(pool) - if i1_ty_si is not None: - i1_ptr_ty_si: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_si) - if i1_ptr_ty_si is not None: mlirlite.add_param(pool, func, i1_ptr_ty_si, "__stop_iter_flag__") - # Default params: register the defaults list so cross-module calls missing args fill from the - # table instead of reading stack garbage. - if m_fd.args is not None: - md_args_node: ast.Arguments* = ast.Arguments*(m_fd.args) - if md_args_node.defaults is not None: - md_dlist: list[ast.AST*]* = md_args_node.defaults - md_default_count = int(len(md_dlist)) - if md_default_count < 0 or md_default_count > 128: - # Same as _inject_function_declare: a corrupted tree node makes defaults len huge, - # which would write out of bounds into perm_pool; skip registration. - stdio.printf("[VPIS] abnormal default parameter count: method=%s.%s default_count=%d (tree may be overwritten; skipping default value registration)\n", cls_name, m_fd.name, md_default_count) - md_defaults = None - md_default_count = 0 - else: md_defaults = md_dlist - - # Register into the function table (bare ClassName.method, matching the caller's lookup_name). - # ParamCount excludes self (matching _translate_method's len(args) - 1), so default filling aligns. - if funcs_ptr is not None: - cur_count: int = trans._func_count - param_count_no_self: int = param_count - if param_count_no_self > 0: param_count_no_self -= 1 - if GenApi.add_func_to_table(funcs_ptr, cur_count, lookup_name, func, max_funcs, md_defaults, md_default_count, param_count_no_self, None, src_sha1) == 0: - trans._func_count = cur_count + 1 - # Complete the global function-table Spec (matching ClassGenVTable): otherwise - # find_func_entry_global().Spec is empty and vtable generation fails to find the method type. - GenApi.update_global_func_spec(lookup_name, func) - - -# _register_ctypedef_from_annassign - register a CTypedef from an AnnAssign node. -# Extracts the NAME and type expression from `NAME: t.CTypedef = Type` and registers it. -def _register_ctypedef_from_annassign(pool: memhub.MemBuddy*, aa: ast.AnnAssign*) -> None: - """Extract and register a CTypedef from an AnnAssign node.""" - if aa is None or pool is None: return - # extract the target name (must be a Name node) - if aa.target is None or aa.target.kind() != ast.ASTKind.Name: return - tgt: ast.Name* = ast.Name*(aa.target) - if tgt.id is None: return - # extract the type expression string (from the value AST node) - if aa.value is None: return - type_str: str = GenApi._get_type_name_from_annotation(pool, aa.value) - if type_str is None: return - # register the CTypedef - GenApi.register_ctypedef(pool, tgt.id, type_str) - - -# REPL: restore full layouts and vtables for includes classes, driven by {sha1}.meta. -# Only active when ReplStubLayoutMode != 0; layouts are validated against meta field/slot counts. - -# session-level state (PermPool; valid during session init) -ReplStubLayoutMode: int = 0 -ReplStubPool: memhub.MemBuddy* = None -ReplStubTempDir: str = None -ReplStubTable: sizet* = None -ReplStubCount: int = 0 -ReplStubOk: int = 0 -ReplStubVtOk: int = 0 -ReplStubRefused: int = 0 -ReplStubRefusedNoStub: int = 0 # refusal cause distribution (_REPL_SU_*): missing data (module has no {sha1}.meta) -ReplStubRefusedNoClass: int = 0 # refused: meta has the module but not this class -ReplStubRefusedOpaque: int = 0 # refused: the class is OPAQUE in meta -ReplStubRefusedParent: int = 0 # upstream no data: base unavailable/not registered -ReplStubNoStub: int = 0 -ReplStubWarnLeft: int = 0 -ReplStubTmpCount: int = -1 -ReplStubTmpPacked: int = 0 -ReplStubTmpArity: int = 0 - - -def _repl_stub_dupstr(pool: memhub.MemBuddy*, src: str) -> str: - """Deep-copy a string into the given pool.""" - if src is None or pool is None: return None - ln: sizet = string.strlen(src) - dst: str = pool.alloc(ln + 1) - if dst is None: return None - string.strcpy(dst, src) - return dst - - -def _repl_stub_key(pool: memhub.MemBuddy*, sha1: str, cls: str) -> str: - """Build ".", matching the definition-side named struct exactly.""" - if pool is None or sha1 is None or cls is None: return None - if sha1[0] == '\0' or cls[0] == '\0': return None - ln: sizet = string.strlen(sha1) + 1 + string.strlen(cls) + 1 - buf: str = pool.alloc(ln) - if buf is None: return None - viperlib.snprintf(buf, ln, "%s.%s", sha1, cls) - return buf - - -def _repl_stub_path(sha1: str, ext: str) -> str: - """Build the slice path //. under the REPL temp dir (stdlib-allocated).""" - if ReplStubTempDir is None or sha1 is None: return None - td_len: sizet = string.strlen(ReplStubTempDir) - sh_len: sizet = string.strlen(sha1) - sub: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel) - sub_len: sizet = 0 - if sub is not None: sub_len = string.strlen(sub) - cap: sizet = td_len + sub_len + sh_len + 8 - buf: str = stdlib.malloc(cap) - if buf is None: return None - if sub_len > 0: viperlib.snprintf(buf, cap, "%s/%s/%s.%s", ReplStubTempDir, sub, sha1, ext) - else: viperlib.snprintf(buf, cap, "%s/%s.%s", ReplStubTempDir, sha1, ext) - return buf - - -def _repl_stub_readtext(path: str) -> str: - """Read stub text (stdlib.malloc, caller frees); None if missing or over the size cap.""" - if path is None: return None - f: fileio.File* = fileio.File(path, fileio.MODE.R) - if f is None or f.closed: return None - sz: sizet = f.size() - if sz <= 0 or sz >= _REPL_STUB_TXT_CAP: - f.close() - return None - buf: str = stdlib.malloc(sz + 1) - if buf is None: - f.close() - return None - rd: LONG = f.read_all(buf, sz + 1) - f.close() - if rd <= 0: - stdlib.free(buf) - return None - if rd < LONG(sz + 1): buf[rd] = 0 - return buf - - -# parse a decimal integer from txt[start, end) (skip leading spaces; no digits -> 0) -def _meta_atoi(txt: str, start: sizet, end: sizet) -> int: - i: sizet = start - while i < end and (txt[i] == ' ' or txt[i] == '\t'): i += 1 - neg: int = 0 - if i < end and txt[i] == '-': - neg = 1 - i += 1 - v: int = 0 - while i < end and txt[i] >= '0' and txt[i] <= '9': - v = v * 10 + (int(txt[i]) - 48) - i += 1 - if neg != 0: return -v - return v - - -# Scan {sha1}.meta text for `STRUCT FIELDS PACKED <0|1>` (or `OPAQUE`) and -# `VTABLE SLOTS `; results go to global scratch. Exact prefix match per line. -def _repl_meta_scan_layout(txt: str, key: str) -> None: - global ReplStubTmpCount, ReplStubTmpPacked, ReplStubTmpArity - ReplStubTmpCount = -1 - ReplStubTmpPacked = 0 - ReplStubTmpArity = 0 - if txt is None or key is None: return - n: sizet = string.strlen(txt) - key_len: sizet = string.strlen(key) - if key_len == 0: return - i: sizet = 0 - while i < n: - ls: sizet = i - while i < n and txt[i] != '\n': i += 1 - le: sizet = i - if i < n: i += 1 - if le > ls and txt[le - 1] == '\r': le -= 1 - if ls + 7 <= le and string.strncmp(txt + ls, "STRUCT ", 7) == 0: - ns: sizet = ls + 7 - ne: sizet = ns + key_len - if ne > le or string.strncmp(txt + ns, key, key_len) != 0: continue - if ne >= le or txt[ne] != ' ': continue - # OPAQUE - if ne + 7 <= le and string.strncmp(txt + ne + 1, "OPAQUE", 6) == 0: - ReplStubTmpCount = -2 - continue - # FIELDS PACKED <0|1> - if ne + 8 <= le and string.strncmp(txt + ne + 1, "FIELDS ", 7) == 0: - ReplStubTmpCount = _meta_atoi(txt, ne + 8, le) - ps: sizet = ne + 8 - while ps < le and txt[ps] >= '0' and txt[ps] <= '9': ps += 1 - if ps + 8 <= le and string.strncmp(txt + ps + 1, "PACKED ", 7) == 0: ReplStubTmpPacked = _meta_atoi(txt, ps + 8, le) - continue - if ls + 7 <= le and string.strncmp(txt + ls, "VTABLE ", 7) == 0: - vs: sizet = ls + 7 - ve: sizet = vs + key_len - if ve > le or string.strncmp(txt + vs, key, key_len) != 0: continue - if ve >= le or txt[ve] != ' ': continue - if ve + 7 <= le and string.strncmp(txt + ve + 1, "SLOTS ", 6) == 0: ReplStubTmpArity = _meta_atoi(txt, ve + 7, le) - continue - return - - -def ReplStubLayoutBegin(temp_dir: str) -> int: - """Enable session-level stub layout restore (called once before registering includes classes); 0=success.""" - global ReplStubLayoutMode, ReplStubPool, ReplStubTempDir, ReplStubTable - global ReplStubCount, ReplStubOk, ReplStubVtOk, ReplStubRefused, ReplStubNoStub - global ReplStubWarnLeft - global ReplStubRefusedNoStub, ReplStubRefusedNoClass, ReplStubRefusedOpaque, ReplStubRefusedParent - pool: memhub.MemBuddy* = CompilerFactory.factory.PermPool - if pool is None or temp_dir is None: return 1 - ReplStubPool = pool - ReplStubTempDir = _repl_stub_dupstr(pool, temp_dir) - if ReplStubTempDir is None: return 1 - ReplStubTable = pool.alloc(8 * _REPL_STUB_CLS_MAX) - if ReplStubTable is None: return 1 - string.memset(ReplStubTable, 0, 8 * _REPL_STUB_CLS_MAX) - ReplStubCount = 0 - ReplStubOk = 0 - ReplStubVtOk = 0 - ReplStubRefused = 0 - ReplStubRefusedNoStub = 0 - ReplStubRefusedNoClass = 0 - ReplStubRefusedOpaque = 0 - ReplStubRefusedParent = 0 - ReplStubNoStub = 0 - ReplStubWarnLeft = _REPL_STUB_WARN_MAX - ReplStubLayoutMode = 1 - return 0 - - -def _repl_stub_at(i: int) -> ReplStubClass*: - """Return the i-th class-table entry, or None.""" - if ReplStubTable is None or i < 0 or i >= ReplStubCount: return None - addr: sizet = ReplStubTable[i] - if addr == 0: return None - return ReplStubClass*(uint64(addr)) - - -def _repl_stub_find(key: str) -> ReplStubClass*: - """Look up a class-table entry by "."; None if not found.""" - if key is None: return None - i: int - for i in range(int(ReplStubCount)): - e: ReplStubClass* = _repl_stub_at(int(i)) - if e is None or e.Key is None: continue - if string.strcmp(e.Key, key) == 0: return e - return None - - -def _repl_stub_find_base(e: ReplStubClass*, bi: int) -> ReplStubClass*: - """Find a base-class entry: same-module SHA1 exact match first, then first bare-name match.""" - if e is None or e.Bases is None or bi < 0 or bi >= e.BaseCount: return None - pname: str = rawptr(e.Bases[bi]) - if pname is None: return None - i: int - for i in range(int(ReplStubCount)): - pe: ReplStubClass* = _repl_stub_at(int(i)) - if pe is None or pe.ClassName is None: continue - if string.strcmp(pe.ClassName, pname) != 0: continue - if e.Sha1 is not None and pe.Sha1 is not None: - if string.strcmp(pe.Sha1, e.Sha1) == 0: return pe - for i in range(int(ReplStubCount)): - pe2: ReplStubClass* = _repl_stub_at(int(i)) - if pe2 is None or pe2.ClassName is None: continue - if string.strcmp(pe2.ClassName, pname) == 0: return pe2 - return None - - -def _repl_stub_has_name(arr: sizet*, cnt: int, name: str) -> int: - """Check whether the name is already in the list (first-declaration wins).""" - if arr is None or name is None: return 0 - i: int - for i in range(int(cnt)): - addr: sizet = arr[i] - if addr == 0: continue - ex: str = rawptr(addr) - if ex is not None and string.strcmp(ex, name) == 0: return 1 - return 0 - - -def _repl_stub_deco_flags(deco_list: list[ast.AST*]*) -> int: - """Return class/method decorator flags (bit 0=@t.NoVTable, bit 1=@t.CVTable).""" - if deco_list is None: return 0 - flags: int = 0 - dn: sizet = len(deco_list) - di: sizet - for di in range(int(dn)): - deco: ast.AST* = deco_list.get(di) - if deco is None: continue - nm: str = None - k: int = deco.kind() - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(deco) - if at.attr is not None and at.value is not None and at.value.kind() == ast.ASTKind.Name: - vn: ast.Name* = ast.Name*(at.value) - if vn.id is not None and string.strcmp(vn.id, "t") == 0: nm = at.attr - elif k == ast.ASTKind.Name: - nn: ast.Name* = ast.Name*(deco) - nm = nn.id - if nm is None: continue - if string.strcmp(nm, "NoVTable") == 0: flags = flags | 1 - elif string.strcmp(nm, "CVTable") == 0: flags = flags | 2 - return flags - - -def _repl_stub_is_virtual(e: ReplStubClass*, mi: int, first: ReplStubClass*) -> int: - """Return whether this class's mi-th method goes into the vtable.""" - if e is None or e.MethodFlags is None or mi < 0 or mi >= e.MethodCount: return 0 - mf: int = int(e.MethodFlags[mi]) - if (mf & 1) != 0: return 0 - if (mf & 2) != 0: return 1 - if (e.Flags & _REPL_SF_CVTABLE) != 0: return 1 - if first is not None: - if first.IsNoVTable != 0: return 0 - return first.HasVt - return 0 - - -def _repl_stub_warn_refuse(e: ReplStubClass*, why: str, a: int, b: int) -> None: - """Warn on a criterion rejection: stub field count a differs from flattened+vtable inference b.""" - global ReplStubWarnLeft, ReplStubRefused - ReplStubRefused += 1 - if ReplStubWarnLeft <= 0: return - ReplStubWarnLeft -= 1 - stdio.printf("includes class layout: layout of %s.%s is untrusted => explicitly refused (reason=criterion refused: %s; " "stub field count=%d != flattened field count+vtable slots=%d). Field access on this class is explicitly refused " "(method calls are unaffected)\n", e.Sha1, e.ClassName, why, a, b) - - -def _repl_stub_warn_nodata(e: ReplStubClass*, cause: int) -> None: - """Warn on a missing-data refusal: no comparison was possible, so print the data sources instead.""" - global ReplStubWarnLeft, ReplStubRefused - global ReplStubRefusedNoStub, ReplStubRefusedNoClass - global ReplStubRefusedOpaque, ReplStubRefusedParent - ReplStubRefused += 1 - if cause == _REPL_SU_NOSTUB: ReplStubRefusedNoStub += 1 - elif cause == _REPL_SU_NOCLASS: ReplStubRefusedNoClass += 1 - elif cause == _REPL_SU_OPAQUE: ReplStubRefusedOpaque += 1 - else: ReplStubRefusedParent += 1 - if ReplStubWarnLeft <= 0: return - ReplStubWarnLeft -= 1 - if cause == _REPL_SU_NOSTUB: - # missing data: not even module layout metadata (LayoutCount = -1 is just a "not read" sentinel) - stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=data missing: " "module %s has no {sha1}.meta layout metadata; meta field count=none / inferred field count=not computed => " "no data on either side, no comparison was made, and this is not a field count mismatch). Field access on this class is " "refused (method calls are unaffected); add the module meta (run batch processing once in the same project, " "Phase A/B writes {sha1}.meta), then rerun REPL with the same --project to recover\n", e.Sha1, e.ClassName, e.Sha1) - elif cause == _REPL_SU_OPAQUE: - # refused: the key is OPAQUE in meta (no body, so no field count to compare) - stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=criterion refused: " "key %s in meta %s.meta is declared OPAQUE (no body); meta field count=unavailable / " "inferred field count=not computed => no comparison was made). Field access on this class is refused " "(method calls are unaffected)\n", e.Sha1, e.ClassName, e.Sha1, e.Key) - else: - # refused: meta exists but has no STRUCT line for this class (LayoutCount = -1 is a "not found" sentinel) - stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=criterion refused: " "meta %s.meta exists but has no `STRUCT ... FIELDS n / OPAQUE` record for key %s; " "meta field count=not found / inferred field count=not computed => no comparison was made). Field access on this class is " "refused (method calls are unaffected); if this class is indeed defined in that module, the meta is stale/" "incomplete, please rebuild\n", e.Sha1, e.ClassName, e.Sha1, e.Key) - - -def _repl_stub_warn_parent(e: ReplStubClass*, detail: str, pname: str) -> None: - """Warn on an upstream-no-data refusal: the base layout is unavailable or unregistered.""" - global ReplStubWarnLeft, ReplStubRefused, ReplStubRefusedParent - ReplStubRefused += 1 - ReplStubRefusedParent += 1 - if ReplStubWarnLeft <= 0: return - ReplStubWarnLeft -= 1 - stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=upstream no data: %s, " "parent=%s; parent fields and vtable slots cannot be flattened => no field count comparison was done for this class). Field access on this class is " "refused (method calls are unaffected)\n", e.Sha1, e.ClassName, detail, pname) - - -def _repl_stub_try(e: ReplStubClass*, mod: mlirlite.LLVMModule*) -> int: - """Validate and register the full layout from the stub: 1=registered / 0=base pending / -1=refused.""" - global ReplStubOk, ReplStubVtOk, ReplStubWarnLeft - if e is None: return -1 - if e.State == _REPL_SS_OK: return 1 - if e.State == _REPL_SS_REFUSED: return -1 - if e.State != _REPL_SS_PENDING: return -1 - pool: memhub.MemBuddy* = ReplStubPool - if pool is None: - e.State = _REPL_SS_REFUSED - return -1 - # (1) base classes must be ready (not ready -> defer; refused -> this layout is untrusted) - bi: int - for bi in range(int(e.BaseCount)): - pe: ReplStubClass* = _repl_stub_find_base(e, bi) - if pe is None: return 0 - if pe.State == _REPL_SS_PENDING: return 0 - if pe.State == _REPL_SS_REFUSED: - e.State = _REPL_SS_REFUSED - _repl_stub_warn_parent(e, "Parent layout untrusted", pe.ClassName) - return -1 - if pe.State == _REPL_SS_EMPTY and pe.OwnFail != 0: - # The base itself had field-annotation failures, so its Count is only partially parsed: - # the flattened field count is unusable, so neither compare nor allow it (M4.2: refuse). - e.State = _REPL_SS_REFUSED - _repl_stub_warn_parent(e, "Not all field types of the parent are resolved (its field count is incomplete)", pe.ClassName) - return -1 - first: ReplStubClass* = None - if e.BaseCount > 0: first = _repl_stub_find_base(e, 0) - # (2) HasVTable (transcribed from ClassGenBase._detect_vtable_status) - hv: int = 0 - if (e.Flags & _REPL_SF_CVTABLE) != 0: hv = 1 - elif (e.Flags & _REPL_SF_NOVTABLE) != 0: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0 - elif first is not None: - if first.IsNoVTable != 0: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0 - else: hv = first.HasVt - else: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0 - # (3) field flattening (base fields first + own fields, deduped by short name, first wins) - cn: int = 0 - for bi in range(int(e.BaseCount)): - pe: ReplStubClass* = _repl_stub_find_base(e, bi) - fi: int - for fi in range(int(pe.Count)): - if cn >= _REPL_STUB_FIELD_MAX: break - nm: str = rawptr(pe.Names[fi]) - if nm is None: continue - if _repl_stub_has_name(e.Names, int(cn), nm) != 0: continue - e.Names[cn] = pe.Names[fi] - e.Types[cn] = pe.Types[fi] - cn += 1 - oi: int - for oi in range(int(e.OwnCount)): - if cn >= _REPL_STUB_FIELD_MAX: break - nm2: str = rawptr(e.OwnNames[oi]) - if nm2 is None: continue - if _repl_stub_has_name(e.Names, int(cn), nm2) != 0: continue - e.Names[cn] = e.OwnNames[oi] - e.Types[cn] = e.OwnTypes[oi] - cn += 1 - e.Count = cn - e.HasVt = hv - # (4) layout check (reached only when both sides are usable): stub field count == - # __vtable__ (if any) + flattened field count. - if e.LayoutCount != cn + hv: - e.State = _REPL_SS_REFUSED - _repl_stub_warn_refuse(e, "Stub field count does not match the field name list", e.LayoutCount, cn + hv) - return -1 - # (5) virtual method short-name order (base virtuals first + own eligible methods in class order) - vn: int = 0 - if hv == 1: - for bi in range(int(e.BaseCount)): - pe: ReplStubClass* = _repl_stub_find_base(e, bi) - vi: int - for vi in range(int(pe.VtCount)): - if vn >= _REPL_STUB_VT_MAX: break - mn: str = rawptr(pe.VtNames[vi]) - if mn is None: continue - if _repl_stub_has_name(e.VtNames, vn, mn) != 0: continue - e.VtNames[vn] = pe.VtNames[vi] - vn += 1 - mi: int - for mi in range(int(e.MethodCount)): - if vn >= _REPL_STUB_VT_MAX: break - mn2: str = rawptr(e.MethodNames[mi]) - if mn2 is None: continue - if string.strcmp(mn2, "__init__") == 0: continue - if string.strcmp(mn2, "__before_init__") == 0: continue - if string.strcmp(mn2, "__new__") == 0: continue - if _repl_stub_has_name(e.VtNames, vn, mn2) != 0: continue - if _repl_stub_is_virtual(e, mi, first) == 0: continue - e.VtNames[vn] = e.MethodNames[mi] - vn += 1 - e.VtCount = vn - e.VtTrust = 0 - if hv == 1 and vn > 0: - if vn == e.VtArity: e.VtTrust = 1 - elif ReplStubWarnLeft > 0: - ReplStubWarnLeft -= 1 - stdio.printf("includes class layout: vtable slot count of %s.%s does not match the stub (%d vs %d) " "-- this class skips virtual dispatch (degrades to direct call, session continues)\n", e.Sha1, e.ClassName, vn, e.VtArity) - # (6) build the LLVM struct (named "%.", same as the definition side) - total: int = int(cn + hv) - i8_ptr: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - if i8_ptr is None: return 0 - first_node: mlirlite.ParamNode* = None - prev_node: mlirlite.ParamNode* = None - if hv == 1: - nd0: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) - if nd0 is None: return 0 - string.memset(nd0, 0, mlirlite.ParamNode.__sizeof__()) - nd0.Ty = i8_ptr - first_node = nd0 - prev_node = nd0 - fi2: int - for fi2 in range(int(cn)): - fty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(e.Types[fi2]) - if fty is None: continue - nd: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) - if nd is None: continue - string.memset(nd, 0, mlirlite.ParamNode.__sizeof__()) - nd.Ty = fty - if first_node is None: first_node = nd - if prev_node is not None: prev_node.Next = nd - prev_node = nd - struct_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node, total, e.Key) - if struct_ty is None: return 0 - # (7) register (under StdlibAlloc=1 registration goes to PermPool, surviving across fragments) - entry: GenTypes.StructEntry* = GenApi.register_struct(pool, e.ClassName, struct_ty, e.Sha1) - if entry is None: - e.State = _REPL_SS_REFUSED - return -1 - if mod is not None: mlirlite.module_add_named_type(mod, pool, struct_ty) - if (e.Flags & _REPL_SF_PACKED) != 0: - entry.IsPacked = 1 - if mod is not None: mlirlite.module_set_named_type_packed(mod, pool, struct_ty, 1) - if hv == 1: - entry.HasVTable = 1 - GenApi.add_field(pool, entry, "__vtable__", i8_ptr) - fi3: int - for fi3 in range(int(cn)): - fname: str = rawptr(e.Names[fi3]) - fty3: mlirlite.LLVMType* = (mlirlite.LLVMType*)(e.Types[fi3]) - if fname is None or fty3 is None: continue - GenApi.add_field(pool, entry, fname, fty3) - if e.IsNoVTable != 0: entry.IsNoVTable = 1 - entry.IsOOP = e.IsOOP - entry.HasInit = e.HasInit - entry.HasNew = e.HasNew - # parent names (multiple-inheritance array; declaration order, for vtable/MRO/field-inheritance queries) - if e.BaseCount > 0: - arr: sizet* = pool.alloc(8 * e.BaseCount) - if arr is not None: - string.memset(arr, 0, 8 * e.BaseCount) - bi3: int - for bi3 in range(int(e.BaseCount)): - bnm: str = rawptr(e.Bases[bi3]) - bdup: str = _repl_stub_dupstr(pool, bnm) - if bdup is not None: arr[bi3] = sizet(bdup) - entry.ParentNames = arr - entry.ParentCount = e.BaseCount - pn: str = rawptr(e.Bases[0]) - entry.ParentName = _repl_stub_dupstr(pool, pn) - if hv == 1 and e.VtTrust == 1 and e.VtCount > 0: - entry.VTableMethods = e.VtNames - entry.VTableMethodCount = e.VtCount - ReplStubVtOk += 1 - e.State = _REPL_SS_OK - ReplStubOk += 1 - return 1 - - -def _repl_stub_add_class(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cd: ast.ClassDef*, sha1: str, txt: str, have_stub: int, trans: HT.Translator*, imported_modules: str, from_imports: str) -> int: - """Collect layout elements of one vpi ClassDef and try to register it; 1=registered/deferred, 0=skipped.""" - global ReplStubCount - if pool is None or cd is None or cd.name is None or ReplStubTable is None: return 0 - if cd.name[0] == '\0' or ReplStubCount >= _REPL_STUB_CLS_MAX: return 0 - key: str = _repl_stub_key(pool, sha1, cd.name) - if key is None: return 0 - if _repl_stub_find(key) is not None: return 0 - e: ReplStubClass* = pool.alloc(ReplStubClass.__sizeof__()) - if e is None: return 0 - string.memset(e, 0, ReplStubClass.__sizeof__()) - e.Key = key - e.ClassName = _repl_stub_dupstr(pool, cd.name) - e.Sha1 = _repl_stub_dupstr(pool, sha1) - e.OwnNames = pool.alloc(8 * _REPL_STUB_FIELD_MAX) - e.OwnTypes = pool.alloc(8 * _REPL_STUB_FIELD_MAX) - e.MethodNames = pool.alloc(8 * _REPL_STUB_FIELD_MAX) - e.MethodFlags = pool.alloc(8 * _REPL_STUB_FIELD_MAX) - e.Bases = pool.alloc(8 * _REPL_STUB_FIELD_MAX) - e.Names = pool.alloc(8 * _REPL_STUB_FIELD_MAX) - e.Types = pool.alloc(8 * _REPL_STUB_FIELD_MAX) - e.VtNames = pool.alloc(8 * _REPL_STUB_VT_MAX) - if e.OwnNames is None or e.OwnTypes is None or e.MethodNames is None or e.MethodFlags is None or e.Bases is None or e.Names is None or e.Types is None or e.VtNames is None: return 0 - string.memset(e.OwnNames, 0, 8 * _REPL_STUB_FIELD_MAX) - string.memset(e.OwnTypes, 0, 8 * _REPL_STUB_FIELD_MAX) - string.memset(e.MethodNames, 0, 8 * _REPL_STUB_FIELD_MAX) - string.memset(e.MethodFlags, 0, 8 * _REPL_STUB_FIELD_MAX) - string.memset(e.Bases, 0, 8 * _REPL_STUB_FIELD_MAX) - string.memset(e.Names, 0, 8 * _REPL_STUB_FIELD_MAX) - string.memset(e.Types, 0, 8 * _REPL_STUB_FIELD_MAX) - string.memset(e.VtNames, 0, 8 * _REPL_STUB_VT_MAX) - # class decorators - cf: int = _repl_stub_deco_flags(cd.decorator_list) - if (cf & 2) != 0: e.Flags = e.Flags | _REPL_SF_CVTABLE - if (cf & 1) != 0: - e.Flags = e.Flags | _REPL_SF_NOVTABLE - e.IsNoVTable = 1 - if _vpi_class_has_packed(cd.decorator_list) != 0: e.Flags = e.Flags | _REPL_SF_PACKED - # real bases (reuse ClassGenBase._collect_parents: alias resolution + marker removal) - pb: sizet* = pool.alloc(8 * GenTypes.PARENT_MAX) - bcnt: int = 0 - if pb is not None and GenApi._g_gen is not None: - string.memset(pb, 0, 8 * GenTypes.PARENT_MAX) - bcnt = GenApi._g_gen.ClassGenBase._collect_parents(pool, cd, pb, GenTypes.PARENT_MAX, trans) - if bcnt > 0: - if bcnt > _REPL_STUB_FIELD_MAX: bcnt = _REPL_STUB_FIELD_MAX - bi: int - for bi in range(int(bcnt)): - bnm: str = rawptr(pb[bi]) - bdup: str = _repl_stub_dupstr(pool, bnm) - if bdup is not None: e.Bases[bi] = sizet(bdup) - e.BaseCount = bcnt - # scan the class body: fields (AnnAssign) + methods (FunctionDef) - children: list[ast.AST*]* = cd.children - own_fail: int = 0 - if children is not None: - cn: sizet = len(children) - ci: sizet - for ci in range(int(cn)): - ch2: ast.AST* = children.get(ci) - if ch2 is None: continue - k2: int = ch2.kind() - if k2 == ast.ASTKind.AnnAssign: - if e.OwnCount >= _REPL_STUB_FIELD_MAX: continue - aa2: ast.AnnAssign* = ast.AnnAssign*(ch2) - if aa2 is None or aa2.target is None or aa2.annotation is None: continue - if aa2.target.kind() != ast.ASTKind.Name: continue - tg: ast.Name* = ast.Name*(aa2.target) - if tg.id is None or tg.id[0] == '\0': continue - fty2: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, aa2.annotation, imported_modules, from_imports, trans, 0) - if fty2 is None: - own_fail = 1 - continue - fdup: str = _repl_stub_dupstr(pool, tg.id) - if fdup is None: - own_fail = 1 - continue - e.OwnNames[e.OwnCount] = sizet(fdup) - e.OwnTypes[e.OwnCount] = sizet(fty2) - e.OwnCount += 1 - continue - if k2 == ast.ASTKind.FunctionDef: - fd2: ast.FunctionDef* = ast.FunctionDef*(ch2) - if fd2 is None or fd2.name is None or fd2.name[0] == '\0': continue - mf2: int = _repl_stub_deco_flags(fd2.decorator_list) - if (mf2 & 2) != 0: e.Flags = e.Flags | _REPL_SF_ANYFUNC - if string.strcmp(fd2.name, "__init__") == 0: e.HasInit = 1 - elif string.strcmp(fd2.name, "__new__") == 0: e.HasNew = 1 - e.IsOOP = 1 - if e.MethodCount < _REPL_STUB_FIELD_MAX: - e.MethodNames[e.MethodCount] = sizet(_repl_stub_dupstr(pool, fd2.name)) - e.MethodFlags[e.MethodCount] = sizet(mf2) - e.MethodCount += 1 - continue - # stub metadata (LayoutCount stays at the "not found" sentinel when missing; have_stub distinguishes causes) - e.LayoutCount = _REPL_STUB_NOCOUNT_NOCLASS - if txt is not None: - _repl_meta_scan_layout(txt, key) - e.LayoutCount = ReplStubTmpCount - e.VtArity = ReplStubTmpArity - if ReplStubTmpPacked != 0: e.Flags = e.Flags | _REPL_SF_PACKED - # state decision - if own_fail != 0: - # Field-type resolution failed: defer to the existing vpi injection (which skips the class); - # OwnFail records that OwnCount is only partially parsed and unusable for subclass flattening. - e.OwnFail = 1 - e.State = _REPL_SS_EMPTY - e.Count = e.OwnCount - e.Names = e.OwnNames - e.Types = e.OwnTypes - elif e.LayoutCount >= 0: e.State = _REPL_SS_PENDING - elif e.BaseCount == 0 and (e.Flags & (_REPL_SF_CVTABLE | _REPL_SF_ANYFUNC)) == 0: - # no base, no vtable -> this class's own fields are the full layout (equivalent to the existing rule) - e.State = _REPL_SS_EMPTY - e.Count = e.OwnCount - e.Names = e.OwnNames - e.Types = e.OwnTypes - else: - # No layout data -> explicitly refuse (no StructEntry). Report the causes separately: - # missing module meta (recoverable) vs OPAQUE/no STRUCT record (untrusted). - e.State = _REPL_SS_REFUSED - if have_stub == 0: _repl_stub_warn_nodata(e, _REPL_SU_NOSTUB) - elif e.LayoutCount == _REPL_STUB_NOCOUNT_OPAQUE: _repl_stub_warn_nodata(e, _REPL_SU_OPAQUE) - else: _repl_stub_warn_nodata(e, _REPL_SU_NOCLASS) - ReplStubTable[ReplStubCount] = sizet(e) - ReplStubCount += 1 - if e.State == _REPL_SS_PENDING: _repl_stub_try(e, mod) - return 1 - - -def _repl_stub_collect_module(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, ch: list[ast.AST*]*, n: sizet, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str) -> int: - """(REPL) Collect layout elements for the current module's classes and try to register them.""" - global ReplStubNoStub, ReplStubWarnLeft - if ReplStubLayoutMode == 0 or pool is None or ch is None: return 0 - if src_sha1 is None or src_sha1[0] == '\0' or ReplStubTable is None: return 0 - # layout metadata ({sha1}.meta; if missing, classes with bases/vtables are refused as "missing data") - txt: str = None - sp: str = _repl_stub_path(src_sha1, "meta") - if sp is not None: - txt = _repl_stub_readtext(sp) - stdlib.free(sp) - have_stub: int = 0 - if txt is not None: have_stub = 1 - else: - ReplStubNoStub += 1 - if ReplStubWarnLeft > 0: - ReplStubWarnLeft -= 1 - stdio.printf("includes class layout: module %s has no .meta stub (data missing) -- this module " "classes with parent/vtable will each be explicitly refused as data missing (layout metadata cannot be read => no " "field count to compare, not a field count mismatch). First run batch processing once in the same project to generate stubs, " "then reuse them with `--project .\\project.vpj --repl` (in the TransPyV directory)\n", src_sha1) - cnt: int = 0 - i: sizet - for i in range(int(n)): - child: ast.AST* = ch.get(i) - if child is None: continue - if child.kind() != ast.ASTKind.ClassDef: continue - cd: ast.ClassDef* = ast.ClassDef*(child) - if cd is None or cd.name is None: continue - # Generic template classes are skipped (same as _inject_class_struct_declare / pass 1A), - # but the owning module must be registered so generic subscripts can build type placeholders. - tp: list[str]* = cd.type_params - if tp is not None and len(tp) > 0: - StubMergerStore.RegisterGenericOwner(cd.name, src_sha1) - continue - # REnum classes: layout {i32 tag, payload} is computed by ClassGenEnum, so skip them here - if _vpi_class_is_renum(cd) != 0: continue - if _repl_stub_add_class(pool, mod, cd, src_sha1, txt, have_stub, trans, imported_modules, from_imports) != 0: - cnt += 1 - if txt is not None: stdlib.free(txt) - return cnt - - -def ReplStubLayoutFinish() -> int: - """Finish: converge remaining class registrations, print a summary, and return the registered count.""" - global ReplStubRefused, ReplStubWarnLeft - if ReplStubLayoutMode == 0: return 0 - rounds: int - for rounds in range(int(_REPL_STUB_FINISH_PASSES)): - prog: int = 0 - i: int - for i in range(int(ReplStubCount)): - e: ReplStubClass* = _repl_stub_at(int(i)) - if e is None or e.State != _REPL_SS_PENDING: continue - if _repl_stub_try(e, None) != 0: prog += 1 - if prog == 0: break - # still not ready -> the base was never registered (missing stub/refused): explicitly refuse - i2: int - for i2 in range(int(ReplStubCount)): - e2: ReplStubClass* = _repl_stub_at(i2) - if e2 is None or e2.State != _REPL_SS_PENDING: continue - e2.State = _REPL_SS_REFUSED - # find the still-unready base class name (to identify the upstream) - pbad: str = "(not located)" - bi2: int - for bi2 in range(int(e2.BaseCount)): - peb: ReplStubClass* = _repl_stub_find_base(e2, bi2) - if peb is not None and peb.State == _REPL_SS_OK: continue - if peb is not None and peb.ClassName is not None: pbad = peb.ClassName - break - _repl_stub_warn_parent(e2, "Parent not registered in the session or its layout was refused", pbad) - stdio.printf("includes class layout: %d complete layouts (%d with a usable vtable index) / " "%d refused / %d modules missing layout metadata\n", ReplStubOk, ReplStubVtOk, ReplStubRefused, ReplStubNoStub) - stdio.printf(" refusal reason distribution (self-witnessing data source): data missing-module has no {sha1}.meta %d classes / " "criterion refused-meta has no such class %d / criterion refused-meta is opaque %d / " "upstream no data-parent unavailable %d\n", ReplStubRefusedNoStub, ReplStubRefusedNoClass, ReplStubRefusedOpaque, ReplStubRefusedParent) - if ReplStubRefused > 0 or ReplStubNoStub > 0: - stdio.printf(" Note: field access on refused classes reports class not registered/attribute access failed " "(it does not silently access at a wrong offset); method calls are unaffected.\n") - if ReplStubRefusedNoStub > 0: - stdio.printf(" Of these, the %d classes with data missing simply failed to read the module layout metadata (the session temp directory %s " "is missing these {sha1}.meta): only batch processing writes meta to disk (the REPL branch runs before Phase1 " "and returns, so --repl itself does not generate stubs). Fix: run batch processing once in the same project " "(TransPyV directory: .\\tpv_build\\output\\TransPyV.exe --project " ".\\project.vpj; add --rebuild-includes when includes changed), then " "reuse its temp directory with --project .\\project.vpj --repl\n", ReplStubRefusedNoStub, ReplStubTempDir) - if ReplStubRefusedNoClass + ReplStubRefusedOpaque > 0: - stdio.printf(" Of these, the %d classes with criterion refused: data exists but is untrusted (the stub has no field count for this class, " "or it is declared opaque); per M4.2 none are allowed through -- please confirm whether this class should have been registered as a struct " "in the first place: if it should be registered as a struct but is not found in the stub => the stub is stale/incomplete (rebuild includes " "stubs)\n", ReplStubRefusedNoClass + ReplStubRefusedOpaque) - return ReplStubOk - - -def ReplStubLayoutEnd() -> None: - """Disable stub layout restore (the batch path is unaffected afterwards).""" - global ReplStubLayoutMode - ReplStubLayoutMode = 0 +import lib.core.Gen.GenApi as GenApi +import lib.core.Gen.GenTypes as GenTypes +import lib.core.CompilerFactory as CompilerFactory +import t, c +from stdint import * +import ast +import string +import stdio +import stdlib +import sys +import memhub +import mlirlite +import viperlib +import win32.fileio as fileio +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HT +import lib.Stub.StubMerger.StubMergerStore as StubMergerStore +import lib.Projectrans.Config as Config + + +# REPL stub layout recovery: constants and class entries (impl at file end). +# Declared first so type annotations are visible before the REPL hook uses them; REPL-only. + +define _REPL_STUB_CLS_MAX = 1024 +_REPL_STUB_FIELD_MAX: int = 128 +_REPL_STUB_VT_MAX: int = 128 +_REPL_STUB_TXT_CAP: sizet = 4194304 +_REPL_STUB_WARN_MAX: int = 10 +_REPL_STUB_FINISH_PASSES: int = 8 + +# Two sentinel values for ReplStubClass.LayoutCount (not field counts). +# Layout metadata now comes from {sha1}.meta STRUCT ... FIELDS n / OPAQUE records, not .stub.ll text. +_REPL_STUB_NOCOUNT_NOCLASS: int = -1 # no STRUCT line for this key in meta (or meta not read) +_REPL_STUB_NOCOUNT_OPAQUE: int = -2 # key is OPAQUE in meta (no body) + +# Reason codes for "layout unavailable -> explicit refuse" (_repl_stub_warn_nodata): +# distinguish missing data from untrusted data; both skip StructEntry registration. +define _REPL_SU_NOSTUB = 1 # missing data: module has no {sha1}.meta +define _REPL_SU_NOCLASS = 2 # refused: meta exists but has no STRUCT line for this class +define _REPL_SU_OPAQUE = 3 # refused: class is OPAQUE in meta (no body) +define _REPL_SU_PARENT = 4 # upstream no data: base layout unavailable/not registered (goes to _repl_stub_warn_parent) + +# ReplStubClass.Flags / MethodFlags bits +define _REPL_SF_CVTABLE = 1 +define _REPL_SF_NOVTABLE = 2 +define _REPL_SF_ANYFUNC = 4 +define _REPL_SF_PACKED = 8 + +# ReplStubClass.State +define _REPL_SS_REFUSED = 0 # layout untrusted: never register (field access fails explicitly) +define _REPL_SS_OK = 1 # full layout registered +define _REPL_SS_PENDING = 2 # base not ready yet: defer to final pass +define _REPL_SS_EMPTY = 3 # no base/no vtable/unresolvable fields: use existing vpi injection + +class ReplStubClass: + Key: str # "." (= definition-side named struct name) + ClassName: str + Sha1: str + Flags: int + OwnNames: sizet* # elements = str pointers (this class field names, AnnAssign order) + OwnTypes: sizet* # elements = LLVMType pointers + OwnCount: int + MethodNames: sizet* + MethodFlags: sizet* + MethodCount: int + Bases: sizet* + BaseCount: int + LayoutCount: int # layout field count (>=0 = meta STRUCT ... FIELDS n; negative = sentinel, not count) + # see _REPL_STUB_NOCOUNT_NOCLASS/-OPAQUE: no such class / meta unread / opaque + VtArity: int # meta VTABLE ... SLOTS n (0 = no record in meta) + Names: sizet* + Types: sizet* + Count: int + HasVt: int + IsNoVTable: int + IsOOP: int + HasInit: int + HasNew: int + VtNames: sizet* + VtCount: int + VtTrust: int # 1 = vtable slot count matches stub (VTableMethods can be registered) + OwnFail: int # 1 = a field annotation failed to resolve, so OwnCount is only partially parsed (incomplete) + State: int + + +# Method return-annotation cache (stdlib-persistent). Deep vpi AST nodes may be +# corrupted later, so return annotations are parsed from raw vpi_buf text by line. +define VPI_RET_MAX = 4096 +define VPI_KEY_LEN = 224 +define VPI_RET_LEN = 224 + +_g_vpi_ret_keys: bytes = None +_g_vpi_ret_strs: bytes = None +_g_vpi_ret_count: int = 0 +_g_vpi_ret_cap: int = 0 + +# Method parameter-annotation cache (stdlib-persistent, mirroring the return cache). +# Deep param lists may be corrupted and truncated, so params are parsed from vpi_buf text. +define VPI_PARAM_MAX = 4096 +define VPI_PARAM_KEY_LEN = 224 +define VPI_PARAM_LEN = 1024 + +# Open-addressing index for the key tables (name -> slot index). Slots store indices +# (stable across realloc); each registration appends, lookup keeps first-registered. +define VPI_RET_IDX_CAP = 8192 +define VPI_PARAM_IDX_CAP = 8192 +# Index hash constants (32-bit FNV-1a, same as StructGenMixin). +define VPI_IDX_FNV_OFFSET = 2166136261 +define VPI_IDX_FNV_PRIME = 16777619 +define VPI_IDX_FNV_MASK = 4294967295 + +# v3 跨模块逃逸摘要缓存(.vpi 行尾 `# esc= ret=`,由 StubGen.Converter._PyiFunction 写出)。 +# 键 = "{sha1}.{Class}.{method}"(方法)或 "{sha1}.{func}"(模块级函数); +# 值打包:低 32 位 = 形参逃逸掩码,高 32 位 = 返回借用位。无逃逸/无返回借用的 def 不写条目, +# 因此"查不到"即"无摘要"(调用点保守放行),与本地表口径一致。 +define VPI_ESC_MAX = 8192 +define VPI_ESC_KEY_LEN = 224 + +# P2 跨模块 willFree 标注缓存(.vpi 行尾 `# ... wf=`,由 StubGen.Converter._PyiFunction 写出)。 +# 键格式与逃逸摘要完全一致("{sha1}.{Class}.{method}" / "{sha1}.{func}");值 = 形参失效掩码 +# (第 k 位 = 第 k 个形参在本调用后失效)。无标注的 def 不写条目,"查不到" 即 "无标注"。 +define VPI_WF_MAX = 8192 +define VPI_WF_KEY_LEN = 224 +_g_vpi_wf_keys: bytes = None +_g_vpi_wf_vals: sizet* = None +_g_vpi_wf_count: int = 0 +# 跨模块常量特化(P3 收尾):同一函数可有多条 `escg=`(打包 p|k|m,见 StructGenMixin._PackEscapeClear), +# 所以是"一键多行"表:键列与 wf 表同构,值列是打包整数。 +define VPI_ESG_MAX = 8192 +define VPI_ESG_KEY_LEN = 224 +# 常量环境的形参位上限:必须与 StructGenMixin.EC_ENV_MAX 一致(越界位一律不信) +define VPI_ESG_ENV_PARAM_MAX = 64 +_g_vpi_esg_keys: bytes = None +_g_vpi_esg_vals: sizet* = None +_g_vpi_esg_count: int = 0 +# 声明式指令契约(`@life(escape(...))` / `@life(noescape(...))` / `@life(ret(...))`):与 escg 表同键 +# (sha1[.Cls].func),但每函数只一条(契约唯一);三列分别存"强制逃逸位"、"强制不逃逸位"与 +# ret_kind(A2,编码见 GenApi.RET_KIND_*)。 +_g_vpi_esd_keys: bytes = None +_g_vpi_esd_e: sizet* = None +_g_vpi_esd_n: sizet* = None +_g_vpi_esd_r: sizet* = None +_g_vpi_esd_count: int = 0 + +_g_vpi_esc_keys: bytes = None +_g_vpi_esc_vals: sizet* = None +_g_vpi_esc_count: int = 0 +# "本模块的 vpi 已加载过"备忘(每项 17 字节 sha1)。调用点每处都要查摘要, +# 若每次都走 _ensure_vpi_ret_annot_loaded 的 O(N) 前缀判定会显著拖慢编译。 +define VPI_ESC_MOD_MAX = 512 +_g_vpi_esc_mods: bytes = None +_g_vpi_esc_mod_count: int = 0 + +def _vpi_escape_module_loaded(src_sha1: str) -> int: + """该模块的 vpi 是否已由逃逸查询路径加载过;1=是 / 0=否""" + if _g_vpi_esc_mods is None or src_sha1 is None: return 0 + s0: int = src_sha1[0] + s1: int = src_sha1[1] + i: int = 0 + while i < _g_vpi_esc_mod_count: + ent_m: bytes = _g_vpi_esc_mods + sizet(i) * 17 + if ent_m[0] == s0 and ent_m[1] == s1 and string.strcmp(ent_m, src_sha1) == 0: return 1 + i += 1 + return 0 + +def _vpi_escape_mark_module(src_sha1: str) -> int: + """记录"该模块的 vpi 已加载";1=成功 / 0=失败""" + global _g_vpi_esc_mods, _g_vpi_esc_mod_count + if src_sha1 is None: return 0 + if _g_vpi_esc_mods is None: + _g_vpi_esc_mods = stdlib.malloc(VPI_ESC_MOD_MAX * 17) + if _g_vpi_esc_mods is None: return 0 + string.memset(_g_vpi_esc_mods, 0, VPI_ESC_MOD_MAX * 17) + if _vpi_escape_module_loaded(src_sha1) != 0: return 1 + if _g_vpi_esc_mod_count >= VPI_ESC_MOD_MAX: return 0 + string.strcpy(_g_vpi_esc_mods + sizet(_g_vpi_esc_mod_count) * 17, src_sha1) + _g_vpi_esc_mod_count += 1 + return 1 + +_g_vpi_param_keys: bytes = None +_g_vpi_param_strs: bytes = None +_g_vpi_param_count: int = 0 +_g_vpi_param_cap: int = 0 + +# Return-annotation key-table index (same malloc lifetime; trusted iff Built == Count) +_g_vpi_ret_idx: bytes = None +_g_vpi_ret_idx_cap: int = 0 +_g_vpi_ret_idx_built: int = 0 +# Parameter-annotation key-table index (same as above) +_g_vpi_param_idx: bytes = None +_g_vpi_param_idx_cap: int = 0 +_g_vpi_param_idx_built: int = 0 + + +# Index probe/insert/rebuild (module-level functions shared by both key tables). +def _vpi_idx_hash(key: bytes) -> uint64: + """Hash key bytes to a 64-bit FNV-1a value until NUL; empty key returns the offset basis.""" + h: uint64 = uint64(VPI_IDX_FNV_OFFSET) + if key is None: return h + i: sizet = 0 + while key[i] != '\0': + h = (h ^ uint64(int(key[i]))) * uint64(VPI_IDX_FNV_PRIME) + h = h & uint64(VPI_IDX_FNV_MASK) + i += 1 + return h + + +def _vpi_idx_slot(idx_base: bytes, si: sizet) -> sizet*: + """Return the si-th 8-byte index slot (stores slot_index+1; 0 means empty).""" + return (sizet*)(uint64(idx_base) + uint64(si) * 8) + + +def _vpi_idx_find(keys: bytes, key_stride: int, idx_base: bytes, cap: int, key: bytes) -> int: + """Probe the index for key and return the slot index (>=0); miss or unusable index returns -1.""" + if keys is None or idx_base is None or cap <= 0 or key is None: return -1 + h: uint64 = _vpi_idx_hash(key) + mask: sizet = sizet(cap - 1) + si: sizet = sizet(h) & mask + for i in range(int(cap)): + slot: sizet* = _vpi_idx_slot(idx_base, si) + sv: sizet = slot[0] + if sv == 0: return -1 + se: int = int(sv) - 1 + ek: bytes = keys + se * key_stride + if string.strcmp(ek, key) == 0: return se + si = (si + 1) & mask + return -1 + + +def _vpi_idx_insert(idx_base: bytes, cap: int, slot_index: int, key: bytes) -> int: + """Insert (key -> slot_index) into the index, one slot per registration; 0=success / 1=failure.""" + if idx_base is None or cap <= 0 or key is None: return 1 + h: uint64 = _vpi_idx_hash(key) + mask: sizet = sizet(cap - 1) + si: sizet = sizet(h) & mask + for i in range(int(cap)): + slot: sizet* = _vpi_idx_slot(idx_base, si) + if slot[0] == 0: + slot[0] = sizet(slot_index + 1) + return 0 + si = (si + 1) & mask + return 1 + + +def _vpi_rebuild_ret_idx(new_cap: int) -> int: + """Rebuild the return-annotation index for a new capacity (power of two); 0=trusted / 1=untrusted.""" + global _g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_idx_built + if new_cap <= 0: + _g_vpi_ret_idx = None + _g_vpi_ret_idx_cap = 0 + _g_vpi_ret_idx_built = -1 + return 1 + new_idx: bytes = None + if _g_vpi_ret_idx is None: new_idx = stdlib.malloc(new_cap * 8) + else: new_idx = stdlib.realloc(_g_vpi_ret_idx, new_cap * 8) + if new_idx is None: + _g_vpi_ret_idx = None + _g_vpi_ret_idx_cap = 0 + _g_vpi_ret_idx_built = -1 + return 1 + string.memset(new_idx, 0, sizet(new_cap) * 8) + _g_vpi_ret_idx = new_idx + _g_vpi_ret_idx_cap = new_cap + fail: int = 0 + ri: int + for ri in range(int(_g_vpi_ret_count)): + if _vpi_idx_insert(new_idx, new_cap, ri, _g_vpi_ret_keys + ri * VPI_KEY_LEN) != 0: + fail = 1 + break + if fail != 0: + _g_vpi_ret_idx_built = -1 + return 1 + _g_vpi_ret_idx_built = _g_vpi_ret_count + return 0 + + +def _vpi_rebuild_param_idx(new_cap: int) -> int: + """Rebuild the param-annotation index for a new capacity; 0=trusted / 1=untrusted.""" + global _g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_idx_built + if new_cap <= 0: + _g_vpi_param_idx = None + _g_vpi_param_idx_cap = 0 + _g_vpi_param_idx_built = -1 + return 1 + new_idx: bytes = None + if _g_vpi_param_idx is None: new_idx = stdlib.malloc(new_cap * 8) + else: new_idx = stdlib.realloc(_g_vpi_param_idx, new_cap * 8) + if new_idx is None: + _g_vpi_param_idx = None + _g_vpi_param_idx_cap = 0 + _g_vpi_param_idx_built = -1 + return 1 + string.memset(new_idx, 0, sizet(new_cap) * 8) + _g_vpi_param_idx = new_idx + _g_vpi_param_idx_cap = new_cap + fail: int = 0 + ri: int + for ri in range(int(_g_vpi_param_count)): + if _vpi_idx_insert(new_idx, new_cap, ri, _g_vpi_param_keys + ri * VPI_PARAM_KEY_LEN) != 0: + fail = 1 + break + if fail != 0: + _g_vpi_param_idx_built = -1 + return 1 + _g_vpi_param_idx_built = _g_vpi_param_count + return 0 + + +# _cache_vpi_return_annots_text - extract method return annotations from vpi_buf text. +# Does not rely on the AST tree. Tracks "class X:" and "def ... -> Ret:" lines. +def _cache_vpi_return_annots_text(vpi_buf: bytes, src_sha1: str) -> int: + """Extract method return-annotation strings from vpi_buf text into a persistent stdlib cache.""" + global _g_vpi_ret_keys, _g_vpi_ret_strs, _g_vpi_ret_count, _g_vpi_ret_cap + global _g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_idx_built + if vpi_buf is None or src_sha1 is None: return 0 + if _g_vpi_ret_keys is None: + _g_vpi_ret_keys = stdlib.malloc(VPI_RET_MAX * VPI_KEY_LEN) + _g_vpi_ret_strs = stdlib.malloc(VPI_RET_MAX * VPI_RET_LEN) + if _g_vpi_ret_keys is None or _g_vpi_ret_strs is None: return 0 + string.memset(_g_vpi_ret_keys, 0, VPI_RET_MAX * VPI_KEY_LEN) + string.memset(_g_vpi_ret_strs, 0, VPI_RET_MAX * VPI_RET_LEN) + _g_vpi_ret_cap = VPI_RET_MAX + # Name index: slots = key-table capacity * 2. On alloc failure stay untrusted + # (Built=-1) and fall back to linear scan (semantics unchanged). + _g_vpi_ret_idx = stdlib.malloc(VPI_RET_IDX_CAP * 8) + if _g_vpi_ret_idx is not None: + string.memset(_g_vpi_ret_idx, 0, VPI_RET_IDX_CAP * 8) + _g_vpi_ret_idx_cap = VPI_RET_IDX_CAP + _g_vpi_ret_idx_built = _g_vpi_ret_count + else: + _g_vpi_ret_idx_cap = 0 + _g_vpi_ret_idx_built = -1 + sha1_len: sizet = string.strlen(src_sha1) + buf_len: sizet = string.strlen(vpi_buf) + cur_cls: bytes = None + pos: sizet = 0 + line_no: int = 0 + while pos < buf_len: + ls: sizet = pos + le: sizet = pos + while le < buf_len and vpi_buf[le] != '\n': le += 1 + line_len: sizet = le - ls + # skip leading spaces, then check class/def + i: sizet = ls + while i < le and vpi_buf[i] == ' ': i += 1 + if i < le: + # class X: + if line_len >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: + cstart: sizet = i + 6 + cend: sizet = cstart + # Extract class name: stop at ':' / ' ' / '(' so base-class parens + # are excluded ("class MemBuddy(MemManager):" -> "MemBuddy"). + while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 + if cend > cstart: + if cur_cls is not None: stdlib.free(cur_cls) + cur_cls = stdlib.malloc(cend - cstart + 1) + if cur_cls is not None: + string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) + cur_cls[cend - cstart] = '\0' + # def name(...) -> Ret: # noqa + elif line_len >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: + # Module-level function (no indent): clear class context to avoid + # leftovers after a class ends (e.g. json null/int_val). + if i == ls and cur_cls is not None: + stdlib.free(cur_cls) + cur_cls = None + dstart: sizet = i + 4 + dend: sizet = dstart + while dend < le and vpi_buf[dend] != '(': dend += 1 + # method name = [dstart, dend) + # find "->" + rpos: sizet = dstart + arrow_pos: sizet = le + while rpos < le - 1: + if vpi_buf[rpos] == '-' and vpi_buf[rpos + 1] == '>': + arrow_pos = rpos + break + rpos += 1 + if dend > dstart: + # Return type: extract "-> Ret" annotation; unannotated non-special + # methods default to "i32" (matches HandlesType.infer_return_type). + ret_src: str = None + rtype_len: sizet = 0 + if arrow_pos < le: + # return type = from arrow_pos+2 (skip spaces) up to ':' + rstart2: sizet = arrow_pos + 2 + while rstart2 < le and vpi_buf[rstart2] == ' ': rstart2 += 1 + rend2: sizet = rstart2 + while rend2 < le and vpi_buf[rend2] != ':': rend2 += 1 + if rend2 > rstart2: + rtype_len = rend2 - rstart2 + ret_src = vpi_buf + rstart2 + else: + # no return annotation: default i32 (except special methods filtered below) + rtype_len = 3 + ret_src = "i32" + if ret_src is not None: + # build key and ret_str, store into cache + mname_len: sizet = dend - dstart + cls_len2: sizet = 0 + if cur_cls is not None: cls_len2 = string.strlen(cur_cls) + key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len + if key_len < VPI_KEY_LEN and rtype_len < VPI_RET_LEN: + # copy method name to a temp buffer (vpi_buf+dstart is not NUL-terminated) + mname_buf: bytes = stdlib.malloc(mname_len + 1) + if mname_buf is not None: + string.strncpy(mname_buf, vpi_buf + dstart, mname_len) + mname_buf[mname_len] = '\0' + # skip unannotated special methods (init/deinit): they have dedicated paths + skip_def: int = 0 + if arrow_pos >= le and (string.strcmp(mname_buf, "__init__") == 0 or string.strcmp(mname_buf, "__new__") == 0 or string.strcmp(mname_buf, "__before_init__") == 0 or string.strcmp(mname_buf, "__after_init__") == 0): + skip_def = 1 + if skip_def == 0: + key_buf: bytes = stdlib.malloc(key_len + 1) + if key_buf is not None: + if cur_cls is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf) + else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, mname_buf) + # dedup: skip if key already exists (same file may load multiple times) + dup_key: int = 0 + dki: int + # Index fast path: when trusted (Built==Count) a single + # probe detects duplicates; otherwise fall back to linear scan. + if _g_vpi_ret_idx_built == _g_vpi_ret_count and _g_vpi_ret_idx is not None and _g_vpi_ret_idx_cap > 0: + if _vpi_idx_find(_g_vpi_ret_keys, VPI_KEY_LEN, _g_vpi_ret_idx, _g_vpi_ret_idx_cap, key_buf) >= 0: + dup_key = 1 + else: + # Fast path: equal keys share the first byte, so skip + # strcmp when the candidate's first byte differs. + dup_first: int = key_buf[0] + for dki in range(int(_g_vpi_ret_count)): + ent_d: bytes = _g_vpi_ret_keys + dki * VPI_KEY_LEN + if ent_d[0] == dup_first and string.strcmp(ent_d, key_buf) == 0: + dup_key = 1 + break + grow_ok: int = 1 + if dup_key == 0 and _g_vpi_ret_count >= _g_vpi_ret_cap: grow_ok = _grow_vpi_ret_cache() + if dup_key == 0 and grow_ok != 0: + string.strcpy(_g_vpi_ret_keys + _g_vpi_ret_count * VPI_KEY_LEN, key_buf) + string.strncpy(_g_vpi_ret_strs + _g_vpi_ret_count * VPI_RET_LEN, ret_src, rtype_len) + (_g_vpi_ret_strs + _g_vpi_ret_count * VPI_RET_LEN)[rtype_len] = '\0' + _g_vpi_ret_count += 1 + # Sync index only if trusted before insert (Built==Count-1, + # incl. rebuild) and insert succeeded; else mark untrusted. + if _g_vpi_ret_idx_built == _g_vpi_ret_count - 1 and _vpi_idx_insert(_g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_count - 1, key_buf) == 0: + _g_vpi_ret_idx_built = _g_vpi_ret_count + else: _g_vpi_ret_idx_built = -1 + stdlib.free(key_buf) + stdlib.free(mname_buf) + pos = le + 1 + line_no += 1 + if cur_cls is not None: stdlib.free(cur_cls) + return 1 + + +# _cache_vpi_param_annots_text - extract function/method param annotations from vpi_buf text. +# Stored as key="sha1.ClassName.method"/"sha1.method", val='@'-separated annotation strings. +def _cache_vpi_param_annots_text(vpi_buf: bytes, src_sha1: str) -> int: + """Extract function/method param-annotation strings from vpi_buf text into a persistent stdlib cache.""" + global _g_vpi_param_keys, _g_vpi_param_strs, _g_vpi_param_count, _g_vpi_param_cap + global _g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_idx_built + if vpi_buf is None or src_sha1 is None: return 0 + if _g_vpi_param_keys is None: + _g_vpi_param_keys = stdlib.malloc(VPI_PARAM_MAX * VPI_PARAM_KEY_LEN) + _g_vpi_param_strs = stdlib.malloc(VPI_PARAM_MAX * VPI_PARAM_LEN) + if _g_vpi_param_keys is None or _g_vpi_param_strs is None: return 0 + string.memset(_g_vpi_param_keys, 0, VPI_PARAM_MAX * VPI_PARAM_KEY_LEN) + string.memset(_g_vpi_param_strs, 0, VPI_PARAM_MAX * VPI_PARAM_LEN) + _g_vpi_param_cap = VPI_PARAM_MAX + # Name index: slots = key-table capacity * 2. On alloc failure stay untrusted + # (Built=-1) and fall back to linear scan (semantics unchanged). + _g_vpi_param_idx = stdlib.malloc(VPI_PARAM_IDX_CAP * 8) + if _g_vpi_param_idx is not None: + string.memset(_g_vpi_param_idx, 0, VPI_PARAM_IDX_CAP * 8) + _g_vpi_param_idx_cap = VPI_PARAM_IDX_CAP + _g_vpi_param_idx_built = _g_vpi_param_count + else: + _g_vpi_param_idx_cap = 0 + _g_vpi_param_idx_built = -1 + sha1_len: sizet = string.strlen(src_sha1) + buf_len: sizet = string.strlen(vpi_buf) + cur_cls: bytes = None + pos: sizet = 0 + while pos < buf_len: + ls: sizet = pos + le: sizet = pos + while le < buf_len and vpi_buf[le] != '\n': le += 1 + line_len: sizet = le - ls + i: sizet = ls + while i < le and vpi_buf[i] == ' ': i += 1 + if i < le: + if line_len >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: + cstart: sizet = i + 6 + cend: sizet = cstart + while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 + if cend > cstart: + if cur_cls is not None: stdlib.free(cur_cls) + cur_cls = stdlib.malloc(cend - cstart + 1) + if cur_cls is not None: + string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) + cur_cls[cend - cstart] = '\0' + elif line_len >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: + # module-level function (no indent): clear class context + if i == ls and cur_cls is not None: + stdlib.free(cur_cls) + cur_cls = None + dstart: sizet = i + 4 + dend: sizet = dstart + while dend < le and vpi_buf[dend] != '(': dend += 1 + if dend >= le: + pos = le + 1 + continue + # find matching ')' (depth-aware: annotations may contain [ ] / { } brackets) + p: sizet = dend + 1 + pdepth: int = 0 + pend: sizet = dend + while p < le: + ch_p: char = vpi_buf[p] + if ch_p == '(' or ch_p == '[' or ch_p == '{': pdepth += 1 + elif ch_p == ')' or ch_p == ']' or ch_p == '}': + pdepth -= 1 + if pdepth < 0: + pend = p + break + p += 1 + # parse params segment by segment (top-level commas; skip *args/**kwargs) + plist: bytes = stdlib.malloc(VPI_PARAM_LEN) + if plist is None: + pos = le + 1 + continue + plist[0] = '\0' + plist_pos: sizet = 0 + seg_written: int = 0 + seg_cur: sizet = dend + 1 + while seg_cur < pend: + while seg_cur < pend and (vpi_buf[seg_cur] == ' ' or vpi_buf[seg_cur] == ','): seg_cur += 1 + if seg_cur >= pend: break + s_start: sizet = seg_cur + sd: int = 0 + while seg_cur < pend: + ch_s: char = vpi_buf[seg_cur] + if ch_s == '(' or ch_s == '[' or ch_s == '{': sd += 1 + elif ch_s == ')' or ch_s == ']' or ch_s == '}': sd -= 1 + elif ch_s == ',' and sd == 0: break + seg_cur += 1 + s_end: sizet = seg_cur + # skip * / ** (vararg/kwarg not added to args list) + st: sizet = s_start + while st < s_end and vpi_buf[st] == ' ': st += 1 + if st < s_end and vpi_buf[st] == '*': + if seg_cur < pend and vpi_buf[seg_cur] == ',': seg_cur += 1 + continue + # cut default value (top-level '=', e.g. ": str = None") + e_pos: sizet = st + ed: int = 0 + while e_pos < s_end: + ch_e: char = vpi_buf[e_pos] + if ch_e == '(' or ch_e == '[' or ch_e == '{': ed += 1 + elif ch_e == ')' or ch_e == ']' or ch_e == '}': ed -= 1 + elif ch_e == '=' and ed == 0: break + e_pos += 1 + s_end = e_pos + # extract annotation after top-level ':' (none -> empty, e.g. self) + c_pos: sizet = st + cd2: int = 0 + annot_start: sizet = s_end + while c_pos < s_end: + ch_c: char = vpi_buf[c_pos] + if ch_c == '(' or ch_c == '[' or ch_c == '{': cd2 += 1 + elif ch_c == ')' or ch_c == ']' or ch_c == '}': cd2 -= 1 + elif ch_c == ':' and cd2 == 0: + annot_start = c_pos + 1 + break + c_pos += 1 + a_start: sizet = annot_start + while a_start < s_end and vpi_buf[a_start] == ' ': a_start += 1 + a_end: sizet = s_end + while a_end > a_start and vpi_buf[a_end - 1] == ' ': a_end -= 1 + if plist_pos + (a_end - a_start) + 1 < VPI_PARAM_LEN: + if seg_written > 0: + plist[plist_pos] = '@' + plist_pos += 1 + ai2: sizet + for ai2 in range(int(a_end - a_start)): + plist[plist_pos] = vpi_buf[a_start + ai2] + plist_pos += 1 + plist[plist_pos] = '\0' + seg_written += 1 + if seg_cur < pend and vpi_buf[seg_cur] == ',': seg_cur += 1 + # store into cache (key = sha1.class.method / sha1.method, dedup) + if plist_pos > 0 and plist[0] != '\0': + mname_len: sizet = dend - dstart + cls_len2: sizet = 0 + if cur_cls is not None: cls_len2 = string.strlen(cur_cls) + key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len + if key_len < VPI_PARAM_KEY_LEN: + mname_buf: bytes = stdlib.malloc(mname_len + 1) + if mname_buf is not None: + string.strncpy(mname_buf, vpi_buf + dstart, mname_len) + mname_buf[mname_len] = '\0' + key_buf: bytes = stdlib.malloc(key_len + 1) + if key_buf is not None: + if cur_cls is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf) + else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, mname_buf) + dup_key: int = 0 + dki: int + # Index fast path: when trusted (Built==Count) a single + # probe detects duplicates; otherwise fall back to linear scan. + if _g_vpi_param_idx_built == _g_vpi_param_count and _g_vpi_param_idx is not None and _g_vpi_param_idx_cap > 0: + if _vpi_idx_find(_g_vpi_param_keys, VPI_PARAM_KEY_LEN, _g_vpi_param_idx, _g_vpi_param_idx_cap, key_buf) >= 0: + dup_key = 1 + else: + # Fast path: equal keys share the first byte, so skip + # strcmp when the candidate's first byte differs. + dup_first: int = key_buf[0] + for dki in range(int(_g_vpi_param_count)): + ent_d: bytes = _g_vpi_param_keys + dki * VPI_PARAM_KEY_LEN + if ent_d[0] == dup_first and string.strcmp(ent_d, key_buf) == 0: + dup_key = 1 + break + grow_ok: int = 1 + if dup_key == 0 and _g_vpi_param_count >= _g_vpi_param_cap: grow_ok = _grow_vpi_param_cache() + if dup_key == 0 and grow_ok != 0: + string.strcpy(_g_vpi_param_keys + _g_vpi_param_count * VPI_PARAM_KEY_LEN, key_buf) + string.strncpy(_g_vpi_param_strs + _g_vpi_param_count * VPI_PARAM_LEN, plist, plist_pos) + (_g_vpi_param_strs + _g_vpi_param_count * VPI_PARAM_LEN)[plist_pos] = '\0' + _g_vpi_param_count += 1 + # Sync index only if trusted before insert (Built==Count-1, + # incl. rebuild) and insert succeeded; else mark untrusted. + if _g_vpi_param_idx_built == _g_vpi_param_count - 1 and _vpi_idx_insert(_g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_count - 1, key_buf) == 0: + _g_vpi_param_idx_built = _g_vpi_param_count + else: _g_vpi_param_idx_built = -1 + stdlib.free(key_buf) + stdlib.free(mname_buf) + stdlib.free(plist) + pos = le + 1 + if cur_cls is not None: stdlib.free(cur_cls) + return 1 + + +# _grow_vpi_param_cache - grow the param-annotation cache (capacity doubles). +def _grow_vpi_param_cache() -> int: + """Grow the param-annotation cache to double capacity; returns 1=success / 0=failure.""" + global _g_vpi_param_keys, _g_vpi_param_strs, _g_vpi_param_cap + if _g_vpi_param_keys is None or _g_vpi_param_cap <= 0: return 0 + new_cap: int = _g_vpi_param_cap * 2 + if new_cap > 1048576: return 0 + new_keys: bytes = stdlib.realloc(_g_vpi_param_keys, new_cap * VPI_PARAM_KEY_LEN) + if new_keys is None: return 0 + new_strs: bytes = stdlib.realloc(_g_vpi_param_strs, new_cap * VPI_PARAM_LEN) + if new_strs is None: return 0 + _g_vpi_param_keys = new_keys + _g_vpi_param_strs = new_strs + _g_vpi_param_cap = new_cap + # grow the index with the table and rebuild it (slots = capacity * 2, power of 2) + _vpi_rebuild_param_idx(new_cap * 2) + return 1 + + +# _lookup_vpi_param_annots - look up the param-annotation cache. +def _lookup_vpi_param_annots(src_sha1: str, cls_name: str, method_name: str) -> str: + """Look up the param-annotation cache; returns the '@'-separated string (leading self placeholder) or None.""" + if src_sha1 is None or method_name is None or _g_vpi_param_keys is None: return None + sha1_len: sizet = string.strlen(src_sha1) + mlen: sizet = string.strlen(method_name) + clen: sizet = 0 + if cls_name is not None: clen = string.strlen(cls_name) + key_len: sizet = sha1_len + clen + mlen + 2 + if key_len >= VPI_PARAM_KEY_LEN: return None + key_buf: bytes = stdlib.malloc(key_len + 1) + if key_buf is None: return None + if cls_name is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) + else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, method_name) + r: str = None + ki: int + # Index fast path: when trusted (Built==Count) one probe returns the hit slot; + # a miss means truly absent, so no linear scan. Otherwise fall back to linear scan. + hit_p: int = -1 + if _g_vpi_param_idx_built == _g_vpi_param_count and _g_vpi_param_idx is not None and _g_vpi_param_idx_cap > 0: + hit_p = _vpi_idx_find(_g_vpi_param_keys, VPI_PARAM_KEY_LEN, _g_vpi_param_idx, _g_vpi_param_idx_cap, key_buf) + else: + # Linear-scan fallback: equal keys share the first byte, so skip strcmp when + # the candidate's first byte differs (negative check only, result unchanged). + key_first: int = key_buf[0] + for ki in range(int(_g_vpi_param_count)): + ent_p: bytes = _g_vpi_param_keys + ki * VPI_PARAM_KEY_LEN + if ent_p[0] == key_first and string.strcmp(ent_p, key_buf) == 0: + hit_p = ki + break + if hit_p >= 0: r = _g_vpi_param_strs + hit_p * VPI_PARAM_LEN + stdlib.free(key_buf) + return r + + +# _resolve_vpi_param_annot_seg_template - param-annotation segment for specialized generics. +# Locates the {sha1}.{base}[T].{method} template entry and substitutes T with the concrete arg. +def _resolve_vpi_param_annot_seg_template(pool: memhub.MemBuddy*, src_sha1: str, cls_name: str, method_name: str, seg_idx: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: + """Resolve a param-annotation segment for specialized class X[arg] by substituting T with arg; None on miss.""" + if pool is None or src_sha1 is None or cls_name is None or method_name is None or _g_vpi_param_keys is None: return None + lbkt_t: str = string.strchr(cls_name, 91) # '[' = 91 + if lbkt_t is None: return None + rbkt_t: str = string.strrchr(cls_name, 93) # ']' = 93 + if rbkt_t is None or rbkt_t <= lbkt_t: return None + base_len_t: sizet = uint64(lbkt_t) - uint64(cls_name) + arg_len_t: sizet = uint64(rbkt_t) - uint64(lbkt_t) - 1 + if base_len_t <= 0 or arg_len_t <= 0: return None + base_t: bytes = stdlib.malloc(base_len_t + 1) + if base_t is None: return None + arg_t: bytes = stdlib.malloc(arg_len_t + 1) + if arg_t is None: + stdlib.free(base_t) + return None + string.strncpy(base_t, cls_name, base_len_t) + base_t[base_len_t] = '\0' + string.strncpy(arg_t, lbkt_t + 1, arg_len_t) + arg_t[arg_len_t] = '\0' + sha1_len_t: sizet = string.strlen(src_sha1) + ty_t: mlirlite.LLVMType* = None + ki_t: int + for ki_t in range(int(_g_vpi_param_count)): + gk_t: bytes = _g_vpi_param_keys + ki_t * VPI_PARAM_KEY_LEN + if string.strncmp(gk_t, src_sha1, sha1_len_t) != 0 or gk_t[sha1_len_t] != '.': continue + if string.strncmp(gk_t + sha1_len_t + 1, base_t, base_len_t) != 0: continue + if gk_t[sha1_len_t + 1 + base_len_t] != 91: continue + gklb_t: str = string.strchr(gk_t + sha1_len_t + 1 + base_len_t, 93) + if gklb_t is None: continue + tplen_t: sizet = uint64(gklb_t) - uint64(gk_t + sha1_len_t + 1 + base_len_t) - 1 + # method name must match fully (gklb_t is followed by ']' and '.'; mangled, so unambiguous) + if tplen_t <= 0 or string.strcmp(gklb_t + 2, method_name) != 0: continue + annots_t: str = _g_vpi_param_strs + ki_t * VPI_PARAM_LEN + if annots_t is None or annots_t[0] == '\0': break + # locate segment (same scheme as _resolve_vpi_param_annot_seg: '@'-separated, seg 0 = self empty) + cur_t: sizet = 0 + cur_seg_t: int = 0 + while cur_seg_t < seg_idx and annots_t[cur_t] != '\0': + while annots_t[cur_t] != '\0' and annots_t[cur_t] != '@': cur_t += 1 + if annots_t[cur_t] == '@': cur_t += 1 + cur_seg_t += 1 + seg_start_t: sizet = cur_t + seg_len_t: sizet = 0 + while annots_t[seg_start_t + seg_len_t] != '\0' and annots_t[seg_start_t + seg_len_t] != '@': seg_len_t += 1 + if seg_len_t == 0: break + seg_buf_t: bytes = stdlib.malloc(seg_len_t + 1) + if seg_buf_t is None: break + string.strncpy(seg_buf_t, annots_t + seg_start_t, seg_len_t) + seg_buf_t[seg_len_t] = '\0' + # Replace the type-param token (e.g. T) with the concrete arg (e.g. str / AST*): + # template annotations are cached verbatim, so without this "item: T" resolves to nothing. + tparam_t: bytes = stdlib.malloc(tplen_t + 1) + if tparam_t is not None: + string.strncpy(tparam_t, gk_t + sha1_len_t + 1 + base_len_t + 1, tplen_t) + tparam_t[tplen_t] = '\0' + subst_t: str = _subst_template_arg(seg_buf_t, tparam_t, arg_t) + if subst_t is not None: + # _subst_template_arg may return its input unchanged (tplen==0 or tplen>alen), + # so only free the old buffer when subst_t is a different pointer (avoid double free). + if uint64(subst_t) != uint64(seg_buf_t): stdlib.free(seg_buf_t) + seg_buf_t = subst_t + stdlib.free(tparam_t) + ty_t = _resolve_vpi_ret_annot_str(pool, seg_buf_t, i32_ty, i8_ptr_ty) + stdlib.free(seg_buf_t) + break + stdlib.free(base_t) + stdlib.free(arg_t) + return ty_t + + +# _repair_declare_params_from_text - complete declare params from the text cache. +# When the vpi tree param list is truncated, append missing params so declare matches define. +def _repair_declare_params_from_text(pool: memhub.MemBuddy*, func: mlirlite.Function*, src_sha1: str, cls_name: str, method_name: str, param_count: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> int: + """Complete declare params from the vpi text cache; returns the completed param count.""" + if func is None or src_sha1 is None or method_name is None: return param_count + prm_annots: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name) + if prm_annots is None or prm_annots[0] == '\0': return param_count + # count text params ('@'-separated; segments = separators + 1, incl. first param) + pa_count: int = 1 + pi2: sizet = 0 + while prm_annots[pi2] != '\0': + if prm_annots[pi2] == '@': pa_count += 1 + pi2 += 1 + if pa_count <= param_count: return param_count + # append missing params: segments with index >= param_count are the missing ones + seg_idx: int = 0 + seg_start2: sizet = 0 + cur2: sizet = 0 + while prm_annots[cur2] != '\0': + while prm_annots[cur2] != '\0' and prm_annots[cur2] != '@': cur2 += 1 + seg_len2: sizet = cur2 - seg_start2 + if prm_annots[cur2] == '@': cur2 += 1 + if seg_idx >= param_count: + param_ty_r: mlirlite.LLVMType* = i8_ptr_ty + if seg_len2 > 0: + seg_buf_r: bytes = stdlib.malloc(seg_len2 + 1) + if seg_buf_r is not None: + string.strncpy(seg_buf_r, prm_annots + seg_start2, seg_len2) + seg_buf_r[seg_len2] = '\0' + rt: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_r, i32_ty, i8_ptr_ty) + if rt is not None: param_ty_r = rt + stdlib.free(seg_buf_r) + mlirlite.add_param(pool, func, param_ty_r, "arg") + seg_start2 = cur2 + seg_idx += 1 + return pa_count + + +# _resolve_vpi_param_annot_seg - resolve a vpi param-annotation segment to LLVMType. +# Parses the seg_idx-th '@'-separated segment (0 = self) via _resolve_vpi_ret_annot_str. +def _resolve_vpi_param_annot_seg(pool: memhub.MemBuddy*, src_sha1: str, cls_name: str, method_name: str, seg_idx: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: + """Resolve a vpi param-annotation segment by index to LLVMType; empty segment or failure returns None.""" + if pool is None or src_sha1 is None or method_name is None: return None + prm_annots_p: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name) + if prm_annots_p is None or prm_annots_p[0] == '\0': return None + cur_p: sizet = 0 + cur_seg_p: int = 0 + while cur_seg_p < seg_idx and prm_annots_p[cur_p] != '\0': + while prm_annots_p[cur_p] != '\0' and prm_annots_p[cur_p] != '@': cur_p += 1 + if prm_annots_p[cur_p] == '@': cur_p += 1 + cur_seg_p += 1 + seg_start_p: sizet = cur_p + seg_len_p: sizet = 0 + while prm_annots_p[seg_start_p + seg_len_p] != '\0' and prm_annots_p[seg_start_p + seg_len_p] != '@': seg_len_p += 1 + if seg_len_p == 0: return None + seg_buf_p: bytes = stdlib.malloc(seg_len_p + 1) + if seg_buf_p is None: return None + string.strncpy(seg_buf_p, prm_annots_p + seg_start_p, seg_len_p) + seg_buf_p[seg_len_p] = '\0' + seg_ty_p: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_p, i32_ty, i8_ptr_ty) + stdlib.free(seg_buf_p) + return seg_ty_p + + +# _grow_vpi_ret_cache - grow the return-annotation cache (capacity doubles). +# Doubles via stdlib.realloc (cap 1M) so entries are not silently dropped when full. +def _grow_vpi_ret_cache() -> int: + """Grow the return-annotation cache to double capacity; returns 1=success / 0=failure.""" + global _g_vpi_ret_keys, _g_vpi_ret_strs, _g_vpi_ret_cap + if _g_vpi_ret_keys is None or _g_vpi_ret_cap <= 0: return 0 + new_cap: int = _g_vpi_ret_cap * 2 + if new_cap > 1048576: return 0 + new_keys: bytes = stdlib.realloc(_g_vpi_ret_keys, new_cap * VPI_KEY_LEN) + if new_keys is None: return 0 + new_strs: bytes = stdlib.realloc(_g_vpi_ret_strs, new_cap * VPI_RET_LEN) + if new_strs is None: return 0 + _g_vpi_ret_keys = new_keys + _g_vpi_ret_strs = new_strs + _g_vpi_ret_cap = new_cap + # grow the index with the table and rebuild it (slots = capacity * 2, power of 2) + _vpi_rebuild_ret_idx(new_cap * 2) + return 1 + + +# _ensure_vpi_ret_annot_loaded - lazily load a module's vpi stub return-annotation cache. +# On cache miss reads {temp_dir}/{sha1[0:2]}/{sha1}.vpi directly; idempotent. +def _ensure_vpi_ret_annot_loaded(src_sha1: str, temp_dir: str) -> int: + """Lazily load the module's vpi stub return-annotation cache; returns 1=ready / 0=failure.""" + if src_sha1 is None or temp_dir is None: return 0 + sha1_len: sizet = string.strlen(src_sha1) + if sha1_len < 2: return 0 + # module already cached -> skip (idempotent) + if _g_vpi_ret_keys is not None and _g_vpi_ret_count > 0: + pre_i: int + # Fast path: equal prefixes share the first two bytes (the module subdir name), + # so checking them skips strncmp for most entries. + s0: int = src_sha1[0] + s1: int = src_sha1[1] + for pre_i in range(int(_g_vpi_ret_count)): + pre_key: bytes = _g_vpi_ret_keys + pre_i * VPI_KEY_LEN + if pre_key[0] == s0 and pre_key[1] == s1 and string.strncmp(pre_key, src_sha1, sha1_len) == 0 and pre_key[sha1_len] == '.': return 1 + # build path {temp_dir}/{sha1[0:2]}/{sha1}.vpi (char by char, avoiding %c formatting) + td_len: sizet = string.strlen(temp_dir) + path_len: sizet = td_len + sha1_len + 12 + vpi_path: bytes = stdlib.malloc(path_len) + if vpi_path is None: return 0 + string.strcpy(vpi_path, temp_dir) + vpi_path[td_len] = '/' + vpi_path[td_len + 1] = src_sha1[0] + vpi_path[td_len + 2] = src_sha1[1] + vpi_path[td_len + 3] = '/' + string.strcpy(vpi_path + td_len + 4, src_sha1) + string.strcpy(vpi_path + td_len + 4 + sha1_len, ".vpi") + f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R) + if f.closed: + stdlib.free(vpi_path) + return 0 + vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) + if vpi_buf is None: + f.close() + stdlib.free(vpi_path) + return 0 + bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE) + f.close() + if bytes_read > 0: + if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0 + # 单一入口:一次喂齐 ret / param / 逃逸摘要(见 _cache_vpi_annots_text 注释) + _cache_vpi_annots_text(vpi_buf, src_sha1) + stdlib.free(vpi_buf) + stdlib.free(vpi_path) + return 1 + + +# _cache_vpi_esc_clears_text - 抽取行尾**所有** `escg=` 标记(跨模块常量特化)。 +# 与 wf 表同构,但同一行可有多条(一条一个打包值),故是"一键多行"。值 = p|k|m 打包整数 +# (见 StructGenMixin._PackEscapeClear:p=守卫形参位、k=常量、m=可清掉的位掩码)。 +def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int: + """抽取 vpi 行尾的 escg= 标记进缓存;1=处理完成 / 0=缓冲区或分配失败""" + global _g_vpi_esg_keys, _g_vpi_esg_vals, _g_vpi_esg_count + global _g_vpi_esd_keys, _g_vpi_esd_e, _g_vpi_esd_n, _g_vpi_esd_r, _g_vpi_esd_count + if vpi_buf is None or src_sha1 is None: return 0 + if _g_vpi_esg_keys is None: + _g_vpi_esg_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN) + _g_vpi_esg_vals = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) + if _g_vpi_esg_keys is None or _g_vpi_esg_vals is None: return 0 + string.memset(_g_vpi_esg_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN) + string.memset(_g_vpi_esg_vals, 0, VPI_ESG_MAX * 8) + if _g_vpi_esd_keys is None: + _g_vpi_esd_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN) + _g_vpi_esd_e = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) + _g_vpi_esd_n = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) + _g_vpi_esd_r = sizet*(stdlib.malloc(VPI_ESG_MAX * 8)) + if _g_vpi_esd_keys is None or _g_vpi_esd_e is None or _g_vpi_esd_n is None or _g_vpi_esd_r is None: return 0 + string.memset(_g_vpi_esd_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN) + string.memset(_g_vpi_esd_e, 0, VPI_ESG_MAX * 8) + string.memset(_g_vpi_esd_n, 0, VPI_ESG_MAX * 8) + string.memset(_g_vpi_esd_r, 0, VPI_ESG_MAX * 8) + sha1_len: sizet = string.strlen(src_sha1) + buf_len: sizet = string.strlen(vpi_buf) + cur_cls: bytes = None + pos: sizet = 0 + while pos < buf_len: + ls: sizet = pos + le: sizet = pos + while le < buf_len and vpi_buf[le] != '\n': le += 1 + i: sizet = ls + while i < le and vpi_buf[i] == ' ': i += 1 + if i < le: + if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: + cstart: sizet = i + 6 + cend: sizet = cstart + while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 + if cend > cstart: + if cur_cls is not None: stdlib.free(cur_cls) + cur_cls = stdlib.malloc(cend - cstart + 1) + if cur_cls is not None: + string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) + cur_cls[cend - cstart] = '\0' + elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: + # 模块级函数(无缩进)清掉类上下文,避免上一类泄漏 + if i == ls and cur_cls is not None: + stdlib.free(cur_cls) + cur_cls = None + # 只在注释段('#' 之后)里找 escg=:避开参数名 + hm: sizet = le + j: sizet = i + while j < le: + if vpi_buf[j] == '#': + hm = j + break + j += 1 + dstart: sizet = i + 4 + dend: sizet = dstart + while dend < le and vpi_buf[dend] != '(': dend += 1 + if hm < le and dend > dstart: + mname_len: sizet = dend - dstart + cls_len_e: sizet = 0 + if cur_cls is not None: cls_len_e = string.strlen(cur_cls) + key_len_e: sizet = sha1_len + 1 + cls_len_e + 1 + mname_len + if key_len_e < VPI_ESG_KEY_LEN: + mname_buf_e: bytes = stdlib.malloc(mname_len + 1) + key_buf_e: bytes = stdlib.malloc(key_len_e + 1) + if mname_buf_e is not None and key_buf_e is not None: + string.strncpy(mname_buf_e, vpi_buf + dstart, mname_len) + mname_buf_e[mname_len] = '\0' + if cur_cls is not None: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_e) + else: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s", src_sha1, mname_buf_e) + j3: sizet = hm + 1 + while j3 + 5 <= le: + if vpi_buf[j3] == 'e' and vpi_buf[j3 + 1] == 's' and vpi_buf[j3 + 2] == 'c' and vpi_buf[j3 + 3] == 'g' and vpi_buf[j3 + 4] == '=': + kq: sizet = j3 + 5 + eg_v: int = 0 + while kq < le and vpi_buf[kq] >= '0' and vpi_buf[kq] <= '9': + eg_v = eg_v * 10 + (int(vpi_buf[kq]) - 48) + kq += 1 + if eg_v > 0 and _g_vpi_esg_count < VPI_ESG_MAX: + string.strcpy(_g_vpi_esg_keys + sizet(_g_vpi_esg_count) * VPI_ESG_KEY_LEN, key_buf_e) + _g_vpi_esg_vals[_g_vpi_esg_count] = sizet(eg_v) + _g_vpi_esg_count += 1 + j3 = kq + else: + j3 += 1 + # 声明式指令契约(esce= / escn= / escr=):每函数一条,只认 '#' 注释段 + de_v: int = 0 + dn_v: int = 0 + dr_v: int = 0 + j4: sizet = hm + 1 + while j4 + 5 <= le: + if vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'e' and vpi_buf[j4 + 4] == '=': + kq2: sizet = j4 + 5 + v2: int = 0 + while kq2 < le and vpi_buf[kq2] >= '0' and vpi_buf[kq2] <= '9': + v2 = v2 * 10 + (int(vpi_buf[kq2]) - 48) + kq2 += 1 + if v2 > 0: de_v = v2 + j4 = kq2 + elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'n' and vpi_buf[j4 + 4] == '=': + kq3: sizet = j4 + 5 + v3: int = 0 + while kq3 < le and vpi_buf[kq3] >= '0' and vpi_buf[kq3] <= '9': + v3 = v3 * 10 + (int(vpi_buf[kq3]) - 48) + kq3 += 1 + if v3 > 0: dn_v = v3 + j4 = kq3 + elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'r' and vpi_buf[j4 + 4] == '=': + kq5: sizet = j4 + 5 + v5: int = 0 + while kq5 < le and vpi_buf[kq5] >= '0' and vpi_buf[kq5] <= '9': + v5 = v5 * 10 + (int(vpi_buf[kq5]) - 48) + kq5 += 1 + if v5 > 0: dr_v = v5 + j4 = kq5 + else: + j4 += 1 + if (de_v != 0 or dn_v != 0 or dr_v != 0) and _g_vpi_esd_count < VPI_ESG_MAX: + string.strcpy(_g_vpi_esd_keys + sizet(_g_vpi_esd_count) * VPI_ESG_KEY_LEN, key_buf_e) + _g_vpi_esd_e[_g_vpi_esd_count] = sizet(de_v) + _g_vpi_esd_n[_g_vpi_esd_count] = sizet(dn_v) + _g_vpi_esd_r[_g_vpi_esd_count] = sizet(dr_v) + _g_vpi_esd_count += 1 + if mname_buf_e is not None: stdlib.free(mname_buf_e) + if key_buf_e is not None: stdlib.free(key_buf_e) + pos = le + 1 + if cur_cls is not None: stdlib.free(cur_cls) + return 1 + + +# _lookup_vpi_escape_clears - 跨模块常量守卫查询:按调用点常量环境算出"该清掉的位";0 = 无命中 +def _lookup_vpi_escape_clears(src_sha1: str, cls_name: str, method_name: str, env: sizet*) -> int: + """env = 常量环境(2 槽/形参:[0]=是否已知、[1]=值,下标 = 形参位);返回该清掉的位掩码 OR 值""" + if _g_vpi_esg_keys is None or _g_vpi_esg_count == 0: return 0 + if src_sha1 is None or method_name is None or env is None: return 0 + sha1_len: sizet = string.strlen(src_sha1) + cl_len: sizet = 0 + if cls_name is not None: cl_len = string.strlen(cls_name) + me_len: sizet = string.strlen(method_name) + key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len + kb: bytes = stdlib.malloc(key_len + 1) + if kb is None: return 0 + if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) + else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) + cleared: int = 0 + idx: int = 0 + while idx < _g_vpi_esg_count: + ent: bytes = _g_vpi_esg_keys + sizet(idx) * VPI_ESG_KEY_LEN + # 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp + if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: + v: int = int(_g_vpi_esg_vals[idx]) + if v > 0: + p: int = v & 255 + kv: int = (v >> 8) & 255 + if kv >= 128: kv -= 256 + m: int = v >> 16 + if p >= 0 and p < VPI_ESG_ENV_PARAM_MAX and env[p * 2] != 0: + if int64(env[p * 2 + 1]) == int64(kv): cleared = cleared | m + idx += 1 + stdlib.free(kb) + return cleared + + +# _lookup_vpi_escape_dir - 跨模块声明式指令契约查询(`@life(escape(...))` / `@life(noescape(...))` / +# `@life(ret(...))`)。which: 0=强制逃逸位 / 1=强制不逃逸位 / 2=ret_kind(A2)。 +# 0 = 无该标注(含该模块未加载过)。 +def _lookup_vpi_escape_dir(src_sha1: str, cls_name: str, method_name: str, which: int) -> int: + if _g_vpi_esd_keys is None or _g_vpi_esd_count == 0: return 0 + if src_sha1 is None or method_name is None: return 0 + sha1_len: sizet = string.strlen(src_sha1) + cl_len: sizet = 0 + if cls_name is not None: cl_len = string.strlen(cls_name) + me_len: sizet = string.strlen(method_name) + key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len + kb: bytes = stdlib.malloc(key_len + 1) + if kb is None: return 0 + if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) + else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) + r: int = 0 + i: int = 0 + while i < _g_vpi_esd_count: + ent: bytes = _g_vpi_esd_keys + sizet(i) * VPI_ESG_KEY_LEN + # 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp + if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: + if which == 2: r = int(_g_vpi_esd_r[i]) + elif which != 0: r = int(_g_vpi_esd_n[i]) + else: r = int(_g_vpi_esd_e[i]) + break + i += 1 + stdlib.free(kb) + return r + + +# _cache_vpi_annots_text - 所有"从 vpi 文本解析"的缓存**单一入口**。 +# 任何新增的文本缓存都加在这里,不要再散落到各加载点——漏喂一处就会**静默退化** +# (现场:逃逸摘要只在 _ensure_vpi_ret_annot_loaded 里喂,而 stub 注入加载点没喂, +# 前者开头又有"本模块 ret 注解已缓存即 return 1"的短路,于是跨模块逃逸查询永久查空, +# EscapeXmodTest 漏检)。 +def _cache_vpi_annots_text(vpi_buf: bytes, src_sha1: str) -> int: + """一次性把 vpi 文本派生出的所有缓存喂齐;1=完成 / 0=入参非法""" + if vpi_buf is None or src_sha1 is None: return 0 + _cache_vpi_return_annots_text(vpi_buf, src_sha1) + _cache_vpi_param_annots_text(vpi_buf, src_sha1) + _cache_vpi_escape_annots_text(vpi_buf, src_sha1) + _cache_vpi_willfree_annots_text(vpi_buf, src_sha1) + _cache_vpi_esc_clears_text(vpi_buf, src_sha1) + return 1 + + +# _cache_vpi_escape_annots_text - extract the trailing `# esc= ret=` markers. +# v3 跨模块逃逸摘要:不依赖 AST,按行扫描(与 _cache_vpi_return_annots_text 同构)。 +def _cache_vpi_escape_annots_text(vpi_buf: bytes, src_sha1: str) -> int: + """抽取 vpi 行尾逃逸摘要标记进缓存;1=处理完成 / 0=缓冲区或分配失败""" + global _g_vpi_esc_keys, _g_vpi_esc_vals, _g_vpi_esc_count + if vpi_buf is None or src_sha1 is None: return 0 + if _g_vpi_esc_keys is None: + _g_vpi_esc_keys = stdlib.malloc(VPI_ESC_MAX * VPI_ESC_KEY_LEN) + _g_vpi_esc_vals = sizet*(stdlib.malloc(VPI_ESC_MAX * 8)) + if _g_vpi_esc_keys is None or _g_vpi_esc_vals is None: return 0 + string.memset(_g_vpi_esc_keys, 0, VPI_ESC_MAX * VPI_ESC_KEY_LEN) + string.memset(_g_vpi_esc_vals, 0, VPI_ESC_MAX * 8) + sha1_len: sizet = string.strlen(src_sha1) + buf_len: sizet = string.strlen(vpi_buf) + cur_cls: bytes = None + pos: sizet = 0 + while pos < buf_len: + ls: sizet = pos + le: sizet = pos + while le < buf_len and vpi_buf[le] != '\n': le += 1 + i: sizet = ls + while i < le and vpi_buf[i] == ' ': i += 1 + if i < le: + # class X: -> 记录当前类上下文(与方法键一致) + if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: + cstart: sizet = i + 6 + cend: sizet = cstart + while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 + if cend > cstart: + if cur_cls is not None: stdlib.free(cur_cls) + cur_cls = stdlib.malloc(cend - cstart + 1) + if cur_cls is not None: + string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) + cur_cls[cend - cstart] = '\0' + # def name(...) ... # esc= ret= + elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: + # 模块级函数(无缩进)清掉类上下文,避免上一类泄漏 + if i == ls and cur_cls is not None: + stdlib.free(cur_cls) + cur_cls = None + # 找同行 "# esc=" 标记 + mark: sizet = le + j: sizet = i + while j + 6 <= le: + if vpi_buf[j] == '#' and vpi_buf[j + 1] == ' ' and string.strncmp(vpi_buf + j + 2, "esc=", 4) == 0: + mark = j + break + j += 1 + if mark < le: + kp: sizet = mark + 6 + esc_v: int = 0 + while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9': + esc_v = esc_v * 10 + (int(vpi_buf[kp]) - 48) + kp += 1 + ret_v: int = 0 + while kp < le and vpi_buf[kp] != 'e': kp += 1 + if kp + 4 <= le: + kp += 4 + while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9': + ret_v = ret_v * 10 + (int(vpi_buf[kp]) - 48) + kp += 1 + if (esc_v != 0 or ret_v != 0) and _g_vpi_esc_count < VPI_ESC_MAX: + dstart: sizet = i + 4 + dend: sizet = dstart + while dend < le and vpi_buf[dend] != '(': dend += 1 + if dend > dstart: + mname_len: sizet = dend - dstart + cls_len_e: sizet = 0 + if cur_cls is not None: cls_len_e = string.strlen(cur_cls) + key_len_e: sizet = sha1_len + 1 + cls_len_e + 1 + mname_len + if key_len_e < VPI_ESC_KEY_LEN: + mname_buf_e: bytes = stdlib.malloc(mname_len + 1) + key_buf_e: bytes = stdlib.malloc(key_len_e + 1) + if mname_buf_e is not None and key_buf_e is not None: + string.strncpy(mname_buf_e, vpi_buf + dstart, mname_len) + mname_buf_e[mname_len] = '\0' + if cur_cls is not None: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_e) + else: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s", src_sha1, mname_buf_e) + string.strcpy(_g_vpi_esc_keys + sizet(_g_vpi_esc_count) * VPI_ESC_KEY_LEN, key_buf_e) + _g_vpi_esc_vals[_g_vpi_esc_count] = sizet(esc_v) | (sizet(ret_v) << 32) + _g_vpi_esc_count += 1 + if mname_buf_e is not None: stdlib.free(mname_buf_e) + if key_buf_e is not None: stdlib.free(key_buf_e) + pos = le + 1 + if cur_cls is not None: stdlib.free(cur_cls) + return 1 + + +# _lookup_vpi_escape_packed - query the cross-module escape summary cache (v3). +def _lookup_vpi_escape_packed(src_sha1: str, cls_name: str, method_name: str) -> int: + """查跨模块逃逸摘要;返回打包值(低32位 mask | ret<<32),0 = 无摘要""" + if _g_vpi_esc_keys is None or _g_vpi_esc_count == 0: return 0 + if src_sha1 is None or method_name is None: return 0 + sha1_len: sizet = string.strlen(src_sha1) + cl_len: sizet = 0 + if cls_name is not None: cl_len = string.strlen(cls_name) + me_len: sizet = string.strlen(method_name) + key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len + kb: bytes = stdlib.malloc(key_len + 1) + if kb is None: return 0 + if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) + else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) + res: int = 0 + idx: int = 0 + while idx < _g_vpi_esc_count: + ent: bytes = _g_vpi_esc_keys + sizet(idx) * VPI_ESC_KEY_LEN + # 键以模块 sha1 打头(前 2 个十六进制字符足以筛掉绝大多数候选,避免逐项 strcmp) + if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: + res = int(_g_vpi_esc_vals[idx]) + break + idx += 1 + stdlib.free(kb) + return res + + +# _cache_vpi_willfree_annots_text - extract the trailing `wf=` marker (P2 willFree). +# 与逃逸摘要同构:不依赖 AST,按行扫描;只在有 wf 标记的 def 上建条目。 +def _cache_vpi_willfree_annots_text(vpi_buf: bytes, src_sha1: str) -> int: + """抽取 vpi 行尾 P2 willFree 标记(`# ... wf=`)进缓存;1=处理完成 / 0=缓冲区或分配失败""" + global _g_vpi_wf_keys, _g_vpi_wf_vals, _g_vpi_wf_count + if vpi_buf is None or src_sha1 is None: return 0 + if _g_vpi_wf_keys is None: + _g_vpi_wf_keys = stdlib.malloc(VPI_WF_MAX * VPI_WF_KEY_LEN) + _g_vpi_wf_vals = sizet*(stdlib.malloc(VPI_WF_MAX * 8)) + if _g_vpi_wf_keys is None or _g_vpi_wf_vals is None: return 0 + string.memset(_g_vpi_wf_keys, 0, VPI_WF_MAX * VPI_WF_KEY_LEN) + string.memset(_g_vpi_wf_vals, 0, VPI_WF_MAX * 8) + sha1_len: sizet = string.strlen(src_sha1) + buf_len: sizet = string.strlen(vpi_buf) + cur_cls: bytes = None + pos: sizet = 0 + while pos < buf_len: + ls: sizet = pos + le: sizet = pos + while le < buf_len and vpi_buf[le] != '\n': le += 1 + i: sizet = ls + while i < le and vpi_buf[i] == ' ': i += 1 + if i < le: + # class X: -> 记录当前类上下文(与方法键一致) + if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0: + cstart: sizet = i + 6 + cend: sizet = cstart + while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1 + if cend > cstart: + if cur_cls is not None: stdlib.free(cur_cls) + cur_cls = stdlib.malloc(cend - cstart + 1) + if cur_cls is not None: + string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart) + cur_cls[cend - cstart] = '\0' + # def name(...) ... # [esc= ret=] wf= + elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0: + # 模块级函数(无缩进)清掉类上下文,避免上一类泄漏 + if i == ls and cur_cls is not None: + stdlib.free(cur_cls) + cur_cls = None + # 只在注释段里找 "wf=":避开参数名(如 def f(wf: int)) + mark: sizet = le + hm: sizet = le + j: sizet = i + while j < le: + if vpi_buf[j] == '#': + hm = j + break + j += 1 + if hm < le: + j2: sizet = hm + 1 + while j2 + 3 <= le: + if vpi_buf[j2] == 'w' and vpi_buf[j2 + 1] == 'f' and vpi_buf[j2 + 2] == '=': + mark = j2 + break + j2 += 1 + if mark < le: + kp: sizet = mark + 3 + wf_v: int = 0 + while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9': + wf_v = wf_v * 10 + (int(vpi_buf[kp]) - 48) + kp += 1 + if wf_v != 0 and _g_vpi_wf_count < VPI_WF_MAX: + dstart: sizet = i + 4 + dend: sizet = dstart + while dend < le and vpi_buf[dend] != '(': dend += 1 + if dend > dstart: + mname_len: sizet = dend - dstart + cls_len_w: sizet = 0 + if cur_cls is not None: cls_len_w = string.strlen(cur_cls) + key_len_w: sizet = sha1_len + 1 + cls_len_w + 1 + mname_len + if key_len_w < VPI_WF_KEY_LEN: + mname_buf_w: bytes = stdlib.malloc(mname_len + 1) + key_buf_w: bytes = stdlib.malloc(key_len_w + 1) + if mname_buf_w is not None and key_buf_w is not None: + string.strncpy(mname_buf_w, vpi_buf + dstart, mname_len) + mname_buf_w[mname_len] = '\0' + if cur_cls is not None: viperlib.snprintf(key_buf_w, key_len_w + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_w) + else: viperlib.snprintf(key_buf_w, key_len_w + 1, "%s.%s", src_sha1, mname_buf_w) + string.strcpy(_g_vpi_wf_keys + sizet(_g_vpi_wf_count) * VPI_WF_KEY_LEN, key_buf_w) + _g_vpi_wf_vals[_g_vpi_wf_count] = sizet(wf_v) + _g_vpi_wf_count += 1 + if mname_buf_w is not None: stdlib.free(mname_buf_w) + if key_buf_w is not None: stdlib.free(key_buf_w) + pos = le + 1 + if cur_cls is not None: stdlib.free(cur_cls) + return 1 + + +# _lookup_vpi_willfree - query the cross-module P2 willFree cache (key shape = escape summary). +def _lookup_vpi_willfree(src_sha1: str, cls_name: str, method_name: str) -> int: + """查跨模块 willFree 形参掩码;0 = 无标注""" + if _g_vpi_wf_keys is None or _g_vpi_wf_count == 0: return 0 + if src_sha1 is None or method_name is None: return 0 + sha1_len: sizet = string.strlen(src_sha1) + cl_len: sizet = 0 + if cls_name is not None: cl_len = string.strlen(cls_name) + me_len: sizet = string.strlen(method_name) + key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len + kb: bytes = stdlib.malloc(key_len + 1) + if kb is None: return 0 + if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) + else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name) + res: int = 0 + idx: int = 0 + while idx < _g_vpi_wf_count: + ent: bytes = _g_vpi_wf_keys + sizet(idx) * VPI_WF_KEY_LEN + # 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp + if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0: + res = int(_g_vpi_wf_vals[idx]) + break + idx += 1 + stdlib.free(kb) + return res + + +# _vpi_param_annot_seg_is - equality check for a param-annotation segment (for the "str/bytes is not a generic pointer" rule). +def _vpi_param_annot_seg_is(src_sha1: str, cls_name: str, method_name: str, seg_idx: int, want: str) -> int: + """Check whether the definition-side param-annotation segment equals want exactly; 1=yes / 0=no.""" + if want is None or seg_idx < 0: return 0 + prm: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name) + if prm is None or prm[0] == '\0': return 0 + cur: sizet = 0 + # locate segment start: skip seg_idx '@'-separated segments + for seg in range(int(seg_idx)): + while prm[cur] != '\0' and prm[cur] != '@': cur += 1 + if prm[cur] == '\0': return 0 + cur += 1 + slen: sizet = 0 + while prm[cur + slen] != '\0' and prm[cur + slen] != '@': slen += 1 + wlen: sizet = string.strlen(want) + if slen != wlen: return 0 + i: sizet + for i in range(int(slen)): + if prm[cur + i] != want[i]: return 0 + return 1 + + +# _vpi_param_annot_seg_mode - value-reference mode of a param-annotation segment: 0=none / 1=ref / 2=temp. +# The definition side writes `T | t.CRef` / `T | t.CTempRef`; the call side mirrors it (auto-address). +def _vpi_param_annot_seg_mode(prm_annots: str, seg_idx: int) -> int: + """Return the value-reference mode encoded in a vpi param-annotation segment.""" + if prm_annots is None or seg_idx < 0: return 0 + cur: sizet = 0 + for seg in range(int(seg_idx)): + while prm_annots[cur] != '\0' and prm_annots[cur] != '@': cur += 1 + if prm_annots[cur] == '\0': return 0 + cur += 1 + slen: sizet = 0 + while prm_annots[cur + slen] != '\0' and prm_annots[cur + slen] != '@': slen += 1 + if slen == 0: return 0 + seg_buf: bytes = stdlib.malloc(slen + 1) + if seg_buf is None: return 0 + string.strncpy(seg_buf, prm_annots + cur, slen) + seg_buf[slen] = '\0' + mode: int = 0 + if string.strstr(seg_buf, "CTempRef") is not None: mode = 2 + elif string.strstr(seg_buf, "CRef") is not None: mode = 1 + elif seg_buf[0] == '&': + # The stub may carry the union marker (`T | t.CRef`, written by this compiler's StubGen) or + # the source syntax (`&T` / `&temp T`, written by TPC's StubGen). Accept both, else a + # cross-module value-reference call would silently lose its auto-address. + q: sizet = 0 + while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1 + if seg_buf[q] == '&': + q += 1 + while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1 + if slen - q >= 4 and string.strncmp(seg_buf + q, "temp", 4) == 0: mode = 2 + else: mode = 1 + stdlib.free(seg_buf) + return mode + + +# _ensure_vpi_param_annot_loaded - lazily load a module's vpi stub param-annotation cache. +# Mirror of the return variant; reads the .vpi file on miss, idempotent. +def _ensure_vpi_param_annot_loaded(src_sha1: str, temp_dir: str) -> int: + """Lazily load the module's vpi stub param-annotation cache; returns 1=ready / 0=failure.""" + if src_sha1 is None or temp_dir is None: return 0 + sha1_len: sizet = string.strlen(src_sha1) + if sha1_len < 2: return 0 + # module already has param cache entries -> skip (idempotent) + if _g_vpi_param_keys is not None and _g_vpi_param_count > 0: + pre_i: int + # Fast path: equal prefixes share the first two bytes (the module subdir name), + # so checking them skips strncmp for most entries. + s0: int = src_sha1[0] + s1: int = src_sha1[1] + for pre_i in range(int(_g_vpi_param_count)): + pre_key: bytes = _g_vpi_param_keys + pre_i * VPI_PARAM_KEY_LEN + if pre_key[0] == s0 and pre_key[1] == s1 and string.strncmp(pre_key, src_sha1, sha1_len) == 0 and pre_key[sha1_len] == '.': return 1 + # build path {temp_dir}/{sha1[0:2]}/{sha1}.vpi (char by char, avoiding %c formatting) + td_len: sizet = string.strlen(temp_dir) + path_len: sizet = td_len + sha1_len + 12 + vpi_path: bytes = stdlib.malloc(path_len) + if vpi_path is None: return 0 + string.strcpy(vpi_path, temp_dir) + vpi_path[td_len] = '/' + vpi_path[td_len + 1] = src_sha1[0] + vpi_path[td_len + 2] = src_sha1[1] + vpi_path[td_len + 3] = '/' + string.strcpy(vpi_path + td_len + 4, src_sha1) + string.strcpy(vpi_path + td_len + 4 + sha1_len, ".vpi") + f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R) + if f.closed: + stdlib.free(vpi_path) + return 0 + vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) + if vpi_buf is None: + f.close() + stdlib.free(vpi_path) + return 0 + bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE) + f.close() + if bytes_read > 0: + if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0 + _cache_vpi_param_annots_text(vpi_buf, src_sha1) + stdlib.free(vpi_buf) + stdlib.free(vpi_path) + return 1 + + +# _subst_template_arg - replace standalone template-parameter tokens in an annotation string. +# Replaces only boundary-delimited tokens (not inside sizet etc.); returns a new stdlib string. +def _subst_template_arg(annot: str, tparam: str, targ: str) -> str: + """Replace standalone tparam tokens in annot with targ.""" + if annot is None or tparam is None or targ is None: return None + alen: sizet = string.strlen(annot) + tplen: sizet = string.strlen(tparam) + tglen: sizet = string.strlen(targ) + if tplen == 0 or tplen > alen: return annot + # Allocate the worst-case upper bound: each replacement consumes tplen and writes tglen, + # so worst case is alen + (alen/tplen)*tglen + 1 (avoids writing past the heap). + max_sub: sizet = alen / tplen + if max_sub == 0: max_sub = 1 + out_size: sizet = alen + max_sub * tglen + 1 + out: bytes = stdlib.malloc(out_size) + if out is None: return None + opos: sizet = 0 + p: sizet = 0 + while p < alen: + # guard: fail fast (return None) if opos would exceed the allocation (never overrun) + if opos >= out_size: + stdlib.free(out) + return None + matched: int = 0 + if annot[p] == tparam[0] and p + tplen <= alen: + if string.strncmp(annot + p, tparam, tplen) == 0: + prev_ch: int = 0 + next_ch: int = 0 + if p > 0: prev_ch = annot[p - 1] + if p + tplen < alen: next_ch = annot[p + tplen] + prev_ok: int = 1 + if (prev_ch >= 'a' and prev_ch <= 'z') or (prev_ch >= 'A' and prev_ch <= 'Z') or (prev_ch >= '0' and prev_ch <= '9') or prev_ch == '_': prev_ok = 0 + next_ok: int = 1 + if (next_ch >= 'a' and next_ch <= 'z') or (next_ch >= 'A' and next_ch <= 'Z') or (next_ch >= '0' and next_ch <= '9') or next_ch == '_': next_ok = 0 + if prev_ok != 0 and next_ok != 0: + string.strncpy(out + opos, targ, tglen) + opos += tglen + p += tplen + matched = 1 + if matched == 0: + out[opos] = annot[p] + opos += 1 + p += 1 + out[opos] = '\0' + return out + + +# _lookup_vpi_ret_annot - look up a method's cached return-annotation string. +def _lookup_vpi_ret_annot(src_sha1: str, cls_name: str, method_name: str) -> str: + """Look up a method's cached return-annotation string; None if not found.""" + if _g_vpi_ret_keys is None or _g_vpi_ret_count == 0: return None + if src_sha1 is None or method_name is None: return None + sha1_len: sizet = string.strlen(src_sha1) + cl_len: sizet = 0 + if cls_name is not None: cl_len = string.strlen(cls_name) + me_len: sizet = string.strlen(method_name) + key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len + key_buf: bytes = stdlib.malloc(key_len + 1) + if key_buf is None: return None + if cls_name is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name) + else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, method_name) + i: int + # Index fast path: when trusted (Built==Count) one probe returns the hit slot; + # a miss means truly absent, so skip the exact linear scan. Otherwise fall back. + hit_r: int = -1 + if _g_vpi_ret_idx_built == _g_vpi_ret_count and _g_vpi_ret_idx is not None and _g_vpi_ret_idx_cap > 0: + hit_r = _vpi_idx_find(_g_vpi_ret_keys, VPI_KEY_LEN, _g_vpi_ret_idx, _g_vpi_ret_idx_cap, key_buf) + else: + # Linear-scan fallback: equal keys share the first byte; skip candidates whose first byte differs. + key_first: int = key_buf[0] + for i in range(int(_g_vpi_ret_count)): + ent_r: bytes = _g_vpi_ret_keys + i * VPI_KEY_LEN + if ent_r[0] == key_first and string.strcmp(ent_r, key_buf) == 0: + hit_r = i + break + if hit_r >= 0: + stdlib.free(key_buf) + return _g_vpi_ret_strs + hit_r * VPI_RET_LEN + # Generic specialization fallback: list[AST*].get -> list[T].get template cache. + # Substitutes the type-param name (e.g. T) with the concrete arg; no registration needed. + if cls_name is not None: + lbkt_v: str = string.strchr(cls_name, '[') + if lbkt_v is not None: + rbkt_v: str = string.strrchr(cls_name, ']') + if rbkt_v is not None and rbkt_v > lbkt_v: + base_len_v: sizet = sizet(uint64(lbkt_v) - uint64(cls_name)) + param_len_v: sizet = sizet(uint64(rbkt_v) - uint64(lbkt_v) - 1) + if base_len_v > 0 and param_len_v > 0: + base_v: str = stdlib.malloc(base_len_v + 1) + param_v: str = stdlib.malloc(param_len_v + 1) + if base_v is not None and param_v is not None: + string.strncpy(base_v, cls_name, base_len_v) + base_v[base_len_v] = '\0' + string.strncpy(param_v, lbkt_v + 1, param_len_v) + param_v[param_len_v] = '\0' + # scan cache for {sha1}.{base}[xxx].{method} + gi: int + for gi in range(int(_g_vpi_ret_count)): + gk: bytes = _g_vpi_ret_keys + gi * VPI_KEY_LEN + if string.strncmp(gk, src_sha1, sha1_len) == 0 and gk[sha1_len] == '.' and string.strncmp(gk + sha1_len + 1, base_v, base_len_v) == 0 and gk[sha1_len + 1 + base_len_v] == '[': + gklb: str = string.strchr(gk + sha1_len + 1 + base_len_v, ']') + if gklb is not None: + tparam_len_v: sizet = sizet(uint64(gklb) - uint64(gk + sha1_len + 1 + base_len_v) - 1) + meth_start_v: str = gklb + 2 # skip ']' and '.' + if tparam_len_v > 0 and string.strcmp(meth_start_v, method_name) == 0: + tparam_v: str = stdlib.malloc(tparam_len_v + 1) + if tparam_v is not None: + string.strncpy(tparam_v, gk + sha1_len + 1 + base_len_v + 1, tparam_len_v) + tparam_v[tparam_len_v] = '\0' + new_annot_v: str = _subst_template_arg(_g_vpi_ret_strs + gi * VPI_RET_LEN, tparam_v, param_v) + stdlib.free(tparam_v) + stdlib.free(base_v) + stdlib.free(param_v) + stdlib.free(key_buf) + return new_annot_v + stdlib.free(base_v) + stdlib.free(param_v) + stdlib.free(key_buf) + return None + + +# _resolve_single_vpi_type_str - resolve a single type segment to LLVMType. +# Pure string parsing, no AST nodes. +def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.LLVMType*: + """Resolve a single type segment to LLVMType; None if unrecognized.""" + if seg is None or pool is None: return None + # Canonical pointer form is `T*` (also rawptr[T] / t.CPtr[T]); rewrite trailing `*` + # into rawptr[...] so specialized args like "{sha}.AST*" resolve instead of silently returning None. + seg_len_ps: sizet = string.strlen(seg) + star_end_ps: sizet = seg_len_ps + while star_end_ps > 0 and seg[star_end_ps - 1] == ' ': star_end_ps -= 1 + star_cnt_ps: sizet = 0 + star_base_end_ps: sizet = star_end_ps + while star_base_end_ps > 0 and seg[star_base_end_ps - 1] == '*': + star_cnt_ps += 1 + star_base_end_ps -= 1 + if star_cnt_ps > 0 and star_base_end_ps > 0: + # base segment length = star_base_end_ps; each rawptr[ + ] layer is 8 chars + star_buf_len_ps: sizet = star_base_end_ps + 8 * star_cnt_ps + 1 + star_buf_ps: bytes = stdlib.malloc(star_buf_len_ps) + if star_buf_ps is None: return None + star_buf_ps[0] = '\0' + star_k_ps: sizet = 0 + while star_k_ps < star_cnt_ps: + string.strcat(star_buf_ps, "rawptr[") + star_k_ps += 1 + star_off_ps: sizet = string.strlen(star_buf_ps) + string.strncpy(star_buf_ps + star_off_ps, seg, star_base_end_ps) + star_buf_ps[star_off_ps + star_base_end_ps] = '\0' + for star_k_ps in range(int(star_cnt_ps)): + string.strcat(star_buf_ps, "]") + star_ty_ps: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, star_buf_ps) + stdlib.free(star_buf_ps) + return star_ty_ps + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + # built-in aliases + if string.strcmp(seg, "str") == 0 or string.strcmp(seg, "bytes") == 0: return i8_ptr_ty + if string.strcmp(seg, "int") == 0: return mlirlite.Int32(pool) + if string.strcmp(seg, "bool") == 0: return mlirlite.Int1(pool) + # LLVM type strings: vpi stores "i32" for unannotated methods + # (see _cache_vpi_return_annots_text); parse it so generic template calls do not fail fast. + if string.strcmp(seg, "i32") == 0: return mlirlite.Int32(pool) + if string.strcmp(seg, "i64") == 0: return mlirlite.Int64(pool) + if string.strcmp(seg, "i16") == 0: return mlirlite.Int16(pool) + if string.strcmp(seg, "i8") == 0: return i8_ty + if string.strcmp(seg, "i1") == 0: return mlirlite.Int1(pool) + if string.strcmp(seg, "f32") == 0: return mlirlite.Float(pool) + if string.strcmp(seg, "f64") == 0: return mlirlite.Double(pool) + if string.strcmp(seg, "void") == 0: return mlirlite.Void(pool) + # vpi caches `-> None` verbatim as "None"; map None to Void so cross-module + # `-> None` methods do not fail fast with an unresolvable return type. + if string.strcmp(seg, "None") == 0: return mlirlite.Void(pool) + # rawptr[T] built-in raw pointer (equivalent to t.CPtr[T] -> Ptr(T)); vpi keeps the + # Subscript form verbatim. Same as TypeGenResolve's rawptr branch: Ptr(resolve(T)). + if string.strncmp(seg, "rawptr[", 7) == 0: + seg_len_rp: sizet = string.strlen(seg) + if seg_len_rp > 8 and seg[seg_len_rp - 1] == ']': + inner_len_rp: sizet = seg_len_rp - 8 + inner_buf_rp: bytes = stdlib.malloc(inner_len_rp + 1) + if inner_buf_rp is not None: + string.strncpy(inner_buf_rp, seg + 7, inner_len_rp) + inner_buf_rp[inner_len_rp] = '\0' + inner_ty_rp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_rp) + stdlib.free(inner_buf_rp) + if inner_ty_rp is not None: return mlirlite.Ptr(pool, inner_ty_rp) + return None + # t.CPtr[T] / c.CPtr[T] subscript pointers (nesting allowed, e.g. t.CPtr[t.CPtr] + # -> Ptr(Ptr(i8))); same as TypeGenResolve's Subscript(CPtr) branch: Ptr(resolve(T)). + cptr_sub_pfx: sizet = 0 + if string.strncmp(seg, "t.CPtr[", 7) == 0 or string.strncmp(seg, "c.CPtr[", 7) == 0: cptr_sub_pfx = 7 + if cptr_sub_pfx > 0: + seg_len_cp: sizet = string.strlen(seg) + if seg_len_cp > cptr_sub_pfx + 1 and seg[seg_len_cp - 1] == ']': + inner_len_cp: sizet = seg_len_cp - cptr_sub_pfx - 1 + inner_buf_cp: bytes = stdlib.malloc(inner_len_cp + 1) + if inner_buf_cp is not None: + string.strncpy(inner_buf_cp, seg + cptr_sub_pfx, inner_len_cp) + inner_buf_cp[inner_len_cp] = '\0' + inner_ty_cp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_cp) + stdlib.free(inner_buf_cp) + if inner_ty_cp is not None: return mlirlite.Ptr(pool, inner_ty_cp) + return None + # t.XXX / c.XXX built-in type mapping + if string.strncmp(seg, "t.", 2) == 0 or string.strncmp(seg, "c.", 2) == 0: + attr_nm: str = seg + 2 + # t.CExport is an export marker, not a real type; FuncGenDef maps it to Void, + # so normalize it here too or cross-module return types fail fast. + if string.strcmp(attr_nm, "CExport") == 0: return mlirlite.Void(pool) + # [stage 2] unified: CTypedef dynamic table first (library typedefs analyzed normally), + # on miss fall back to map_t_type (t/c library internal special-casing). + mapped_ty: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, attr_nm, None, None) + if mapped_ty is not None: return mapped_ty + return None + # module-qualified class name "memhub.MemBuddy" -> use last segment to look up struct + last_dot: str = string.strrchr(seg, '.') + if last_dot is not None: + cls_nm2: str = last_dot + 1 + mod_struct_ty: mlirlite.LLVMType* = GenApi.get_struct_type(cls_nm2) + if mod_struct_ty is not None: return mod_struct_ty + # Enum fallback: t.CEnum registers in EnumTable, not StructTable, so get_struct_type + # misses; fall back to the enum base type (usually i32) instead of returning None. + mod_enum_ty: mlirlite.LLVMType* = GenApi.get_enum_base_type(cls_nm2) + if mod_enum_ty is not None: return mod_enum_ty + return None + # bare class name -> global struct registry + bare_struct_ty: mlirlite.LLVMType* = GenApi.get_struct_type(seg) + if bare_struct_ty is not None: return bare_struct_ty + # bare enum name -> EnumTable fallback (as above: enums are not in StructTable) + bare_enum_ty: mlirlite.LLVMType* = GenApi.get_enum_base_type(seg) + if bare_enum_ty is not None: return bare_enum_ty + # bare typedef name (e.g. VOIDPTR): CTypedef dynamic table first, on miss fall back to map_t_type + mapped_bare: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, seg, None, None) + if mapped_bare is not None: return mapped_bare + return None + + +# _resolve_vpi_ret_annot_str - resolve a vpi return-annotation string to LLVMType. +# Pure string parsing (no AST nodes) to avoid corrupted kind() from cross-module AST reuse. +def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: + """Resolve a vpi return-annotation string directly to LLVMType without building AST nodes.""" + if ret_annot is None or pool is None: return None + + # tuple[T1, T2, ...] -> anonymous struct {T1, T2, ...} (multi-return packing), + # matching TypeGenMixin._resolve_generic_subscript_ty's tuple branch. + if string.strncmp(ret_annot, "tuple[", 6) == 0: + inner_len_t: sizet = 0 + while ret_annot[6 + inner_len_t] != ']' and ret_annot[6 + inner_len_t] != '\0': inner_len_t += 1 + if ret_annot[6 + inner_len_t] != ']' or inner_len_t == 0: return None + # The tuple return-type field list is mlirlite.ParamNode (new_param_node / + # paramnode_set_next / Struct all take rawptr[ParamNode]), not Function*. + tup_head_t: mlirlite.ParamNode* = None + tup_tail_t: mlirlite.ParamNode* = None + tup_count_t: int = 0 + p_t: sizet = 0 + while p_t < inner_len_t: + # skip leading spaces/commas + while p_t < inner_len_t and (ret_annot[6 + p_t] == ' ' or ret_annot[6 + p_t] == ','): p_t += 1 + seg_s_t: sizet = p_t + while p_t < inner_len_t and ret_annot[6 + p_t] != ',': p_t += 1 + seg_e_t: sizet = p_t + while seg_e_t > seg_s_t and ret_annot[6 + seg_e_t - 1] == ' ': seg_e_t -= 1 + if seg_e_t > seg_s_t: + seg_len_t: sizet = seg_e_t - seg_s_t + seg_buf_t: bytes = stdlib.malloc(seg_len_t + 1) + if seg_buf_t is None: return None + string.strncpy(seg_buf_t, ret_annot + 6 + seg_s_t, seg_len_t) + seg_buf_t[seg_len_t] = '\0' + seg_ty_t: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf_t) + stdlib.free(seg_buf_t) + if seg_ty_t is None: return None + tup_node_t: mlirlite.ParamNode* = mlirlite.new_param_node(pool, seg_ty_t) + if tup_node_t is None: return None + if tup_head_t is None: tup_head_t = tup_node_t + else: mlirlite.paramnode_set_next(tup_tail_t, tup_node_t) + tup_tail_t = tup_node_t + tup_count_t += 1 + if tup_head_t is None: return None + return mlirlite.Struct(pool, tup_head_t, tup_count_t, None) + + # Pure marker annotations (t.State / t.CExtern / t.CExport alone, optional t./c. prefix) + # -> void: decorator markers, not real return types, matching _inject_module_declare. + marker_len: sizet = string.strlen(ret_annot) + mk_s: sizet = 0 + while mk_s < marker_len and ret_annot[mk_s] == ' ': mk_s += 1 + mk_e: sizet = marker_len + while mk_e > mk_s and ret_annot[mk_e - 1] == ' ': mk_e -= 1 + mk_off_m: sizet = 0 + if mk_e - mk_s >= 2 and (ret_annot[mk_s] == 't' or ret_annot[mk_s] == 'c') and ret_annot[mk_s + 1] == '.': mk_off_m = 2 + mk_nlen: sizet = mk_e - mk_s - mk_off_m + if (mk_nlen == 5 and string.strncmp(ret_annot + mk_s + mk_off_m, "State", 5) == 0) or (mk_nlen == 7 and string.strncmp(ret_annot + mk_s + mk_off_m, "CExtern", 7) == 0) or (mk_nlen == 7 and string.strncmp(ret_annot + mk_s + mk_off_m, "CExport", 7) == 0): + return mlirlite.Void(pool) + + has_cptr: int = 0 + has_cref: int = 0 + has_cvoid: int = 0 + first_ty: mlirlite.LLVMType* = None + # Attributes of the segment that produced first_ty (for the has_cptr branch, N12): + # first_qual=qualified name; first_wl=whitelisted bare scalar; first_td=resolved bare typedef. + first_qual: int = 0 + first_wl: int = 0 + first_td: int = 0 + # Union-merge params for CPtr subscript segments (t.CPtr[T]/c.CPtr[T], nesting allowed): + # cptr_sub_lvl = outer Ptr level N, cptr_sub_ty = the segment's own type. -> Ptr^N(base). + cptr_sub_lvl: int = 0 + cptr_sub_ty: mlirlite.LLVMType* = None + + buf_len: sizet = string.strlen(ret_annot) + pos: sizet = 0 + while pos <= buf_len: + # skip leading spaces + while pos < buf_len and ret_annot[pos] == ' ': pos += 1 + seg_start: sizet = pos + # Split `|` only at the top level: `|` inside `[...]` belongs to a generic arg, + # so naive splitting would truncate `list[ast.AST*]`; depth-count on `[`/`]`. + depth_ra: int = 0 + while pos < buf_len: + ch_ra: char = ret_annot[pos] + if ch_ra == '[': depth_ra += 1 + elif ch_ra == ']': + if depth_ra > 0: depth_ra -= 1 + elif ch_ra == '|': + if depth_ra == 0: break + pos += 1 + seg_end: sizet = pos + # trim trailing spaces + while seg_end > seg_start and ret_annot[seg_end - 1] == ' ': seg_end -= 1 + if seg_end > seg_start: + seg_len2: sizet = seg_end - seg_start + seg_buf: bytes = stdlib.malloc(seg_len2 + 1) + if seg_buf is None: break + string.strncpy(seg_buf, ret_annot + seg_start, seg_len2) + seg_buf[seg_len2] = '\0' + # CPtr subscript segments must be recognized first: the definition side treats + # `X*[T]` as "CPtr side contributes N pointer levels, the other side the base". + if string.strncmp(seg_buf, "t.CPtr[", 7) == 0 or string.strncmp(seg_buf, "c.CPtr[", 7) == 0: + seg_ty_cs: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf) + if seg_ty_cs is not None: + # Count outer Ptr nesting (same as the definition side). Do not update the + # loop variable inside a case body: match sinks into a subject snapshot. + lvl_cs: int = 0 + cur_cs: mlirlite.LLVMType* = seg_ty_cs + while cur_cs is not None: + nxt_cs: mlirlite.LLVMType* = None + match cur_cs: + case mlirlite.LLVMType.Ptr(pointee_cs): + lvl_cs += 1 + nxt_cs = pointee_cs + case _: + break + cur_cs = nxt_cs + # take the max level across subscript segments (equivalent to level accumulation) + if lvl_cs > cptr_sub_lvl: + cptr_sub_lvl = lvl_cs + cptr_sub_ty = seg_ty_cs + elif string.strcmp(seg_buf, "t.CPtr") == 0 or string.strcmp(seg_buf, "CPtr") == 0: has_cptr = 1 + elif string.strcmp(seg_buf, "t.CRef") == 0 or string.strcmp(seg_buf, "CRef") == 0: has_cref = 1 + elif string.strcmp(seg_buf, "t.CTempRef") == 0 or string.strcmp(seg_buf, "CTempRef") == 0: has_cref = 1 + elif string.strcmp(seg_buf, "void") == 0 or string.strcmp(seg_buf, "CVoid") == 0 or string.strcmp(seg_buf, "void") == 0: + # Bare void is an alias of t.CVoid and must be handled the same, else the + # else branch sets first_td and produces the illegal Ptr(Void) (void* = i8*). + has_cvoid = 1 + else: + seg_ty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf) + if seg_ty is not None and first_ty is None: + first_ty = seg_ty + # Record the segment attribute (N12): the definition side keeps a bare pointee + # only for registered structs, generic params or whitelisted scalars; else i8*. + if string.strchr(seg_buf, 46) is not None: first_qual = 1 + if string.strcmp(seg_buf, "int") == 0 or string.strcmp(seg_buf, "char") == 0 or string.strcmp(seg_buf, "str") == 0 or string.strcmp(seg_buf, "bytes") == 0 or string.strcmp(seg_buf, "bool") == 0 or string.strcmp(seg_buf, "float") == 0 or string.strcmp(seg_buf, "double") == 0: + first_wl = 1 + # Preserve bare typedef aliases (N13): use GenApi.resolve_typedef_or_map (same API + # as the definition side); set only for non-pointer results (pointer aliases -> i8*). + if first_qual == 0 and first_wl == 0: + if first_ty.kind() != mlirlite.LLVMType.Ptr: + if GenApi.resolve_typedef_or_map(pool, seg_buf, None, None) is not None: first_td = 1 + stdlib.free(seg_buf) + pos += 1 # skip '|' + + if has_cptr != 0 or has_cref != 0: + if has_cvoid != 0: return i8_ptr_ty + if first_ty is not None: + # The result itself is void (e.g. an opaque typedef resolving to void): CPtr merge + # gives void* = i8*, matching has_cvoid (otherwise it would build illegal Ptr(void)). + match first_ty: + case mlirlite.LLVMType.Void(): + return i8_ptr_ty + case _: + pass + # Mirror the definition-side rules (N12/N13): qualified name / whitelisted scalar / + # registered struct / resolvable non-pointer typedef keeps pointee; else collapse to i8*. + if first_qual != 0 or first_wl != 0 or first_td != 0: return mlirlite.Ptr(pool, first_ty) + if first_ty.kind() == mlirlite.LLVMType.Struct: return mlirlite.Ptr(pool, first_ty) + if first_ty.kind() == mlirlite.LLVMType.Ptr: + # `| t.CPtr` always appends exactly one pointer level (same as the definition + # side's BinOp branch); no struct-absorption, so `T*` stays `T*` (never AST*). + return mlirlite.Ptr(pool, first_ty) + return i8_ptr_ty + return i8_ptr_ty + # CPtr subscript union merge: with another side's base type first_ty -> Ptr^N(first_ty); + # with only the subscript segment -> the segment's own type. No struct absorption here. + if cptr_sub_lvl > 0: + if first_ty is None: return cptr_sub_ty + res_ty_cs: mlirlite.LLVMType* = first_ty + for wrap_i_cs in range(int(cptr_sub_lvl)): + res_ty_cs = mlirlite.Ptr(pool, res_ty_cs) + return res_ty_cs + if has_cvoid != 0: + # t.CVoid | t.State: State is a decorator marker (ignored), t.CVoid -> void. + # Without this branch first_ty is None and cross-module return types fail fast. + return mlirlite.Void(pool) + if first_ty is not None: return first_ty + return None + + +# VpiReader - .vpi AST parser. Reads function signatures and CTypedef definitions from +# .vpi stub files and injects declares, filling in missing cross-module declarations. + +# .vpi read buffer size (256KB, matching PYI_BUF_SIZE) +VPI_READ_BUF_SIZE: sizet = 262144 + +# SHA1 hex char count (16, filename hex only, no trailing NUL). Named uniquely vs Phase1's +# SHA1_LEN (17 = 16 chars + NUL) to avoid cross-module CDefine shadowing. +SHA1_HEX_LEN: sizet = 16 + + +# _extract_sha1_from_path - extract SHA1 from a .vpi path. Takes the first 16 chars of the +# filename after the last separator; returns a malloc'd string (caller frees) or None. +def _extract_sha1_from_path(vpi_path: str) -> str: + """Extract the SHA1 from a .vpi path (first 16 chars of the filename, malloc'd).""" + if vpi_path is None: return None + # find the last path separator / or \ + total_len: sizet = string.strlen(vpi_path) + if total_len < SHA1_HEX_LEN + 4: # need at least "xxxx.vpi" length + return None + # scan backwards for the last / or \ + sep_pos: sizet = total_len + while sep_pos > 0: + sep_pos -= 1 + ch: char = vpi_path[sep_pos] + if ch == '/' or ch == '\\': + sep_pos += 1 + break + if sep_pos + SHA1_HEX_LEN > total_len: return None + # allocate buffer and copy SHA1 (16 chars + null terminator) + sha1_buf: str = stdlib.malloc(SHA1_HEX_LEN + 1) + if sha1_buf is None: return None + string.strncpy(sha1_buf, vpi_path + sep_pos, SHA1_HEX_LEN) + sha1_buf[SHA1_HEX_LEN] = '\0' + return sha1_buf + + +# Stub import-context bookkeeping goes through StubMergerStore (single source of truth). +# Unified to AppendImportedModule / AppendFromImport / NotePlainImport; pure bookkeeping. + + +# Recursive typedef pre-registration over the import closure. CTypedef tables are per-file, +# so unimported typedefs would degrade param types; this registers them (cycle/depth guarded). +define _TYPEDEF_VISIT_MAX = 1024 + +_g_vpi_typedef_visited: bytes = None +_g_vpi_typedef_visited_count: int = 0 +# Current visit-table capacity (elements). Must grow when full rather than silently dropping +# marks: dropping breaks cycle detection and re-parses the closure, risking pool OOM. +_g_vpi_typedef_visited_cap: int = 0 + + +def ResetVpiTypedefVisited() -> None: + """Clear the recursive-typedef cycle-detection marks before each module's stub injection.""" + global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count, _g_vpi_typedef_visited_cap + if _g_vpi_typedef_visited is not None: + stdlib.free(_g_vpi_typedef_visited) + _g_vpi_typedef_visited = None + _g_vpi_typedef_visited_count = 0 + _g_vpi_typedef_visited_cap = 0 + + +def _vpi_typedef_visited_check(sha1: str) -> int: + """Check whether sha1 was already recursively registered; 1=visited / 0=not visited (query only).""" + global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count + if sha1 is None: return 1 + if _g_vpi_typedef_visited is None: return 0 + vi: int + for vi in range(int(_g_vpi_typedef_visited_count)): + if string.strcmp(_g_vpi_typedef_visited + vi * 17, sha1) == 0: return 1 + return 0 + + +def _vpi_typedef_visited_mark(sha1: str) -> None: + """Mark sha1 as recursively registered (called after success, for cycle detection).""" + global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count, _g_vpi_typedef_visited_cap + if sha1 is None: return + if _g_vpi_typedef_visited is None: + _g_vpi_typedef_visited = stdlib.malloc(_TYPEDEF_VISIT_MAX * 17) + if _g_vpi_typedef_visited is None: + # fail-fast: without the cycle table the closure re-parses and exhausts the pool, so do not continue + stdio.printf("[VPI] cycle-proof visit table allocation failed (without the table the closure reparse explodes; aborting)\n") + stdio.fflush(None) + stdlib.exit(1) + string.memset(_g_vpi_typedef_visited, 0, _TYPEDEF_VISIT_MAX * 17) + _g_vpi_typedef_visited_count = 0 + _g_vpi_typedef_visited_cap = _TYPEDEF_VISIT_MAX + if _g_vpi_typedef_visited_count >= _g_vpi_typedef_visited_cap: + # Table full: grow (x2); never silently drop marks, which would break cycle detection + # and re-parse the same .vpi batch, allocating from the pool until it OOMs. + old_cap: int = _g_vpi_typedef_visited_cap + new_cap: int = old_cap * 2 + nb: bytes = stdlib.realloc(_g_vpi_typedef_visited, new_cap * 17) + if nb is None: + stdio.printf("[VPI] cycle-proof visit table growth failed cap=%d (lost marks cause a reparse explosion; aborting)\n", old_cap) + stdio.fflush(None) + stdlib.exit(1) + _g_vpi_typedef_visited = nb + string.memset(nb + old_cap * 17, 0, old_cap * 17) + _g_vpi_typedef_visited_cap = new_cap + string.strcpy(_g_vpi_typedef_visited + _g_vpi_typedef_visited_count * 17, sha1) + _g_vpi_typedef_visited_count += 1 + + +def _vpi_register_typedef_closure(pool: memhub.MemBuddy*, import_mods: str, temp_dir: str, depth: int) -> int: + """Recursively register CTypedef/CDefine from import-closure modules; always returns 0 (side-effect only).""" + if pool is None or import_mods is None or temp_dir is None: return 0 + if depth > 8: return 0 + td_len: sizet = string.strlen(temp_dir) + total_len: sizet = string.strlen(import_mods) + pos: sizet = 0 + while pos < total_len: + while pos < total_len and import_mods[pos] == ' ': pos += 1 + if pos >= total_len: break + m_start: sizet = pos + while pos < total_len and import_mods[pos] != ' ': pos += 1 + m_len: sizet = pos - m_start + if m_len == 0: continue + # t/c built-in libs are not compiled, so they have no vpi stub + if m_len == 1 and (import_mods[m_start] == 't' or import_mods[m_start] == 'c'): continue + mod_buf: bytes = stdlib.malloc(m_len + 1) + if mod_buf is None: continue + string.strncpy(mod_buf, import_mods + m_start, m_len) + mod_buf[m_len] = '\0' + sha1_r: str = StubMergerStore.LookupSha1ByModName(mod_buf) + stdlib.free(mod_buf) + if sha1_r is None: continue + if _vpi_typedef_visited_check(sha1_r) != 0: continue + # build {temp_dir}/{sha1[0:2]}/{sha1}.vpi by hand (same as _ensure_vpi_ret_annot_loaded, + # avoiding a StubMergerCollect cyclic dependency) + path_len_r: sizet = td_len + 16 + 12 + vpi_path_r: bytes = stdlib.malloc(path_len_r) + if vpi_path_r is None: continue + string.strcpy(vpi_path_r, temp_dir) + vpi_path_r[td_len] = '/' + vpi_path_r[td_len + 1] = sha1_r[0] + vpi_path_r[td_len + 2] = sha1_r[1] + vpi_path_r[td_len + 3] = '/' + string.strcpy(vpi_path_r + td_len + 4, sha1_r) + string.strcpy(vpi_path_r + td_len + 4 + 16, ".vpi") + f_r: fileio.File* = fileio.File(vpi_path_r, fileio.MODE.R) + if f_r.closed: + stdlib.free(vpi_path_r) + continue + vpi_buf_r: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) + if vpi_buf_r is None: + f_r.close() + stdlib.free(vpi_path_r) + continue + bytes_read_r: LONG = f_r.read_all(vpi_buf_r, VPI_READ_BUF_SIZE) + f_r.close() + if bytes_read_r > 0: + # mark as processed only after successfully reading the vpi (missing/read failure + # is not marked, so a later pass's stub injection can retry) + _vpi_typedef_visited_mark(sha1_r) + if bytes_read_r < VPI_READ_BUF_SIZE: vpi_buf_r[bytes_read_r] = 0 + lx_r: ast.Lexer* = ast.new_lexer(pool) + if lx_r is not None: + ast._lexer_init(lx_r, vpi_buf_r, pool) + tokens_r: ast.Token* = ast.tokenize(lx_r) + tree_r: ast.AST* = ast.parse_tokens(pool, tokens_r) + if tree_r is not None and tree_r.children is not None: + ch_r: list[ast.AST*]* = tree_r.children + cn_r: sizet = len(ch_r) + # Collect child imports (recursive closure) in two passes: first sum the required + # length, then allocate exactly and fill (no fixed cap, so nothing is dropped). + sub_need: sizet = 1 # trailing NUL + ci_c: sizet + for ci_c in range(int(cn_r)): + child_c: ast.AST* = ch_r.get(ci_c) + if child_c is None: continue + kk_c: int = child_c.kind() + if kk_c == ast.ASTKind.Import: + imp_c: ast.Import* = ast.Import*(child_c) + if imp_c.names is not None: + in_nc: sizet = len(imp_c.names) + for ii_c in range(int(in_nc)): + al_c: ast.Alias* = ast.Alias*(imp_c.names.get(ii_c)) + if al_c is not None and al_c.name is not None: sub_need += string.strlen(al_c.name) + 1 + elif kk_c == ast.ASTKind.ImportFrom: + ifr_c: ast.ImportFrom* = ast.ImportFrom*(child_c) + if ifr_c.module is not None: sub_need += string.strlen(ifr_c.module) + 1 + sub_imports: bytes = stdlib.malloc(sub_need) + if sub_imports is not None: + sub_imports[0] = '\0' + sub_len: sizet = 0 + ci_r: sizet + for ci_r in range(int(cn_r)): + child_r: ast.AST* = ch_r.get(ci_r) + if child_r is None: continue + kk_r: int = child_r.kind() + if kk_r == ast.ASTKind.Import: + imp_r: ast.Import* = ast.Import*(child_r) + if imp_r.names is not None: + in_n: sizet = len(imp_r.names) + ii_r: sizet + for ii_r in range(int(in_n)): + al_r: ast.Alias* = ast.Alias*(imp_r.names.get(ii_r)) + if al_r is not None and al_r.name is not None: + il_r: sizet = string.strlen(al_r.name) + if sub_len > 0: + sub_imports[sub_len] = ' ' + sub_len += 1 + string.strcpy(sub_imports + sub_len, al_r.name) + sub_len += il_r + sub_imports[sub_len] = '\0' + elif kk_r == ast.ASTKind.ImportFrom: + ifr_r: ast.ImportFrom* = ast.ImportFrom*(child_r) + if ifr_r.module is not None: + ml_r: sizet = string.strlen(ifr_r.module) + if sub_len > 0: + sub_imports[sub_len] = ' ' + sub_len += 1 + string.strcpy(sub_imports + sub_len, ifr_r.module) + sub_len += ml_r + sub_imports[sub_len] = '\0' + # second pass: register this module's AnnAssign CTypedef/CDefine + ci2_r: sizet + for ci2_r in range(int(cn_r)): + child2_r: ast.AST* = ch_r.get(ci2_r) + if child2_r is None: continue + if child2_r.kind() != ast.ASTKind.AnnAssign: continue + aa_r: ast.AnnAssign* = ast.AnnAssign*(child2_r) + if aa_r.annotation is None or aa_r.target is None or aa_r.value is None: continue + if GenApi._is_ctypedef_annotation(aa_r.annotation) != 0: _register_ctypedef_from_annassign(pool, aa_r) + elif GenApi.is_cdefine_annotation(aa_r.annotation) != 0: + if aa_r.target.kind() != ast.ASTKind.Name: continue + cdef_t: ast.Name* = ast.Name*(aa_r.target) + if cdef_t.id is None: continue + if aa_r.value.kind() == ast.ASTKind.Constant: + cn_v: ast.Constant* = ast.Constant*(aa_r.value) + if cn_v.const_kind == ast.CONST_FLOAT: GenApi.register_cdefine_double(pool, cdef_t.id, cn_v.float_val) + elif cn_v.const_kind == ast.CONST_INT: + cdef_val: int64 = GenApi.extract_cdefine_int_value(aa_r.value) + # 位宽与定义方同一套裁决(注解 → 右侧强转 → 值的最小可存宽度), + # 否则使用方拿 bits=0 只能兜底,跨模块位宽会与定义方不一致 + GenApi.register_cdefine_constant(pool, cdef_t.id, cdef_val, GenApi.cdefine_decl_bits(pool, aa_r.annotation, aa_r.value)) + # recurse into child imports (cycle guarded by the visited marks) + if sub_imports[0] != '\0': _vpi_register_typedef_closure(pool, sub_imports, temp_dir, depth + 1) + stdlib.free(sub_imports) + stdlib.free(vpi_buf_r) + stdlib.free(vpi_path_r) + return 0 + + +# ParseVpiIntoModule - parse a .vpi file and inject function declares into the current module. +# Walks top-level nodes, creates declares for FunctionDefs (deduped) and registers CTypedefs. +def ParseVpiIntoModule(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, vpi_path: str, trans: HT.Translator*) -> int: + """Parse a .vpi file and inject function declares into the current module.""" + if pool is None or mod is None or vpi_path is None or trans is None: return 0 + saved_cur: str = GenApi.get_current_file() + GenApi.set_current_file_type(vpi_path) + ret: int = _ParseVpiIntoModuleImpl(pool, mod, vpi_path, trans) + GenApi.set_current_file_type(saved_cur) + return ret + + +def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, vpi_path: str, trans: HT.Translator*) -> int: + """Actual implementation of ParseVpiIntoModule: parse a .vpi file and inject function declares.""" + if pool is None or mod is None or vpi_path is None or trans is None: return 0 + + # 1. read the .vpi file + f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R) + if f.closed: return 0 + vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE) + if vpi_buf is None: + f.close() + return 0 + bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE) + f.close() + if bytes_read <= 0: + stdlib.free(vpi_buf) + return 0 + if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0 + + # 2. parse into AST + lx: ast.Lexer* = ast.new_lexer(pool) + if lx is None: + stdlib.free(vpi_buf) + return 0 + ast._lexer_init(lx, vpi_buf, pool) + tokens: ast.Token* = ast.tokenize(lx) + tree: ast.AST* = ast.parse_tokens(pool, tokens) + # Delay freeing vpi_buf: AST string fields may point into it (zero-copy tokens), + # so freeing early would dangle Name.id/Attribute.attr; freed at the end instead. + if tree is None: + stdlib.free(vpi_buf) + return 0 + + # extract the source module SHA1 for name mangling (after AST parse success to avoid leaks on early return) + src_sha1: str = _extract_sha1_from_path(vpi_path) + + # Cache method return annotations from vpi_buf text into stdlib (no AST tree: deep nodes + # may be corrupted later, but the raw vpi_buf text stays valid until the function ends). + # 单一入口:ret / param / 逃逸摘要一次喂齐(见 _cache_vpi_annots_text 注释)。 + # 本函数是 stub 注入路径的加载点,历史上这里漏喂逃逸摘要 → 跨模块漏检。 + _cache_vpi_annots_text(vpi_buf, src_sha1) + + # 3. walk top-level AST nodes + ch: list[ast.AST*]* = tree.children + if ch is None: + if src_sha1 is not None: stdlib.free(src_sha1) + return 1 + n: sizet = len(ch) + + # Collect the read module's own imports from the stub to build an isolated annotation + # context, replacing the current module's imports (e.g. t.CArray[str] needs 't' imported). + stub_imported_modules: str = None + stub_from_imports: str = None + # Bookkeeping goes through StubMergerStore (same as ImportsGenMixin.translate_import): + # `import X as Y -> from_imports "Y:X"`; `import a.b.c -> imported_modules "a"`. + stub_ctx: StubMergerStore.ImportCtx* = StubMergerStore.AllocImportCtx(pool, None, None) + for si in range(int(n)): + schild: ast.AST* = ch.get(si) + if schild is None: continue + sk: int = schild.kind() + if sk == ast.ASTKind.Import: + simp: ast.Import* = ast.Import*(schild) + if simp.names is not None: + sn_n: sizet = len(simp.names) + for sni in range(int(sn_n)): + sal: ast.Alias* = ast.Alias*(simp.names.get(sni)) + if sal is not None and sal.name is not None: StubMergerStore.NotePlainImport(pool, stub_ctx, sal.name, sal.asname) + elif sk == ast.ASTKind.ImportFrom: + sifr: ast.ImportFrom* = ast.ImportFrom*(schild) + sifr_mod: str = sifr.module + # Relative imports: resolve the package prefix and __init__ detection from the module + # name table. Falls back to the old rel_path route when no module-name evidence exists. + pkg_prefix_rt: str = None + if sifr.level > 0 and sifr_mod is not None and sifr_mod[0] != '\0': + _sib_sha_rt: str = StubMergerStore.LookupSha1ByModNameSuffix(sifr_mod) + if _sib_sha_rt is not None: + _sub_mod_rt: str = StubMergerStore.LookupModNameBySha1(_sib_sha_rt) + if _sub_mod_rt is not None: + _dot_rt: str = string.strrchr(_sub_mod_rt, '.') + if _dot_rt is not None: + _pfx_len_rt: sizet = sizet(uint64(_dot_rt) - uint64(_sub_mod_rt)) + if _pfx_len_rt > 0: + _my_mod_rt: str = StubMergerStore.LookupModNameBySha1(src_sha1) + # Confirm: this stub's module name must equal the prefix exactly + # (i.e. this stub is the package __init__); else defer to rel_path. + _conf_rt: int = 0 + if _my_mod_rt is not None and string.strlen(_my_mod_rt) == _pfx_len_rt: + if string.strncmp(_my_mod_rt, _sub_mod_rt, _pfx_len_rt) == 0: _conf_rt = 1 + if _conf_rt != 0: + pkg_prefix_rt = pool.alloc(_pfx_len_rt + 1) + if pkg_prefix_rt is not None: + string.strncpy(pkg_prefix_rt, _sub_mod_rt, _pfx_len_rt) + pkg_prefix_rt[_pfx_len_rt] = '\0' + sifr_mod_q: str = sifr_mod + if sifr_mod is not None and sifr_mod[0] != '\0' and sifr.level == 1: + if pkg_prefix_rt is not None: + qtotal_p: sizet = string.strlen(pkg_prefix_rt) + 1 + string.strlen(sifr_mod) + 1 + qbuf_p: str = pool.alloc(qtotal_p) + if qbuf_p is not None: + qbuf_p[0] = '\0' + string.strcat(qbuf_p, pkg_prefix_rt) + string.strcat(qbuf_p, ".") + string.strcat(qbuf_p, sifr_mod) + sifr_mod_q = qbuf_p + else: + rel_p_r: str = StubMergerStore.LookupSha1RelPath(src_sha1) + if rel_p_r is not None: + slash_r: str = string.strrchr(rel_p_r, '/') + if slash_r is not None: + pkg_len_r: sizet = string.strlen(rel_p_r) - string.strlen(slash_r) + if pkg_len_r > 0: + qtotal_r: sizet = pkg_len_r + 1 + string.strlen(sifr_mod) + 1 + qbuf_r: str = pool.alloc(qtotal_r) + if qbuf_r is not None: + string.strncpy(qbuf_r, rel_p_r, pkg_len_r) + qbuf_r[pkg_len_r] = '.' + string.strcpy(qbuf_r + pkg_len_r + 1, sifr_mod) + sifr_mod_q = qbuf_r + if sifr_mod is not None: + if stub_ctx is not None: + stub_ctx.ImportedModules = StubMergerStore.AppendImportedModule(pool, stub_ctx.ImportedModules, sifr_mod_q) + # Plan A: package __init__ re-exported classes are registered as alias entries under + # the package sha1 (only for __init__; uses pkg_prefix_rt, falling back to rel_path). + is_pkg_init_rt: int = 0 + if pkg_prefix_rt is not None: is_pkg_init_rt = 1 + if is_pkg_init_rt == 0: + rel_path_rt: str = StubMergerStore.LookupSha1RelPath(src_sha1) + if rel_path_rt is not None: + # [FIX] Use strstr(rel_path, "__init__.vp") instead of an offset-based strcmp, + # which was always false for real package paths and never triggered re-export. + if string.strstr(rel_path_rt, "__init__.vp") is not None: is_pkg_init_rt = 1 + # Resolve the from-imported module sha1: relative imports put leading dots in `level`, + # not in `module`, so prefix-qualified name lookup must be used. + sub_sha1_rt: str = None + if sifr.level > 0: + if sifr_mod is not None and sifr_mod[0] != '\0': + # Prefer the prefix-qualified exact name (sifr_mod_q, e.g. ast.base); the + # suffix match is only a fallback since it can hit another same-suffix module. + sub_sha1_rt = StubMergerStore.LookupSha1ByModName(sifr_mod_q) + if sub_sha1_rt is None: sub_sha1_rt = StubMergerStore.LookupSha1ByModNameSuffix(sifr_mod) + else: sub_sha1_rt = StubMergerStore.LookupSha1ByModName(sifr_mod) + # [D26 fail-fast] Do not silently skip when a package __init__ from-import cannot + # resolve the submodule sha1: report package and submodule names for diagnosis. + if is_pkg_init_rt != 0 and sub_sha1_rt is None and sifr_mod is not None and sifr_mod[0] != '\0': + GenApi.fatal_error(sifr, f"Package stub from-import submodule sha1 lookup failed: " f"package={rel_path_rt if rel_path_rt is not None else '?'}, " f"submodule='{sifr_mod}' (after prefix completion='{sifr_mod_q}'); " f"classes of this submodule cannot be registered, and annotations referencing them will fail to resolve later", "NameError") + if sifr.names is not None: + sifr_nn: sizet = len(sifr.names) + sifri: sizet + for sifri in range(int(sifr_nn)): + sial: ast.Alias* = ast.Alias*(sifr.names.get(sifri)) + if sial is not None and sial.name is not None: + slocal: str = sial.name + sorig: str = None + if sial.asname is not None: + slocal = sial.asname + sorig = sial.name + # `from X import *` also writes "*:mod" (same as + # ImportsGenMixin.translate_from_import_names); lookup_from_import relies on it. + if stub_ctx is not None: + stub_ctx.FromImports = StubMergerStore.AppendFromImport(pool, stub_ctx.FromImports, slocal, sifr_mod_q, sorig) + # Register re-exported classes under the package sha1 (by alias if asname); + # star has no concrete class name and is only recorded above. + if string.strcmp(sial.name, "*") != 0: + if is_pkg_init_rt != 0 and sub_sha1_rt is not None: + _inject_reexport_class_declare(pool, mod, sial.name, slocal, sub_sha1_rt, src_sha1, trans) + + # replace the current module's imports with the stub's own (correct annotation context) + if stub_ctx is not None: + stub_imported_modules = stub_ctx.ImportedModules + stub_from_imports = stub_ctx.FromImports + imported_modules: str = trans._imported_modules + from_imports: str = trans._from_imports + if stub_imported_modules is not None: imported_modules = stub_imported_modules + if stub_from_imports is not None: from_imports = stub_from_imports + # Add the source module itself to the stub annotation context: stub annotations may + # reference their own CDefines, but stub imports only list imported modules. + if src_sha1 is not None: + stub_rel_src: str = StubMergerStore.LookupSha1RelPath(src_sha1) + if stub_rel_src is not None: + sr_len_src: sizet = string.strlen(stub_rel_src) + if sr_len_src > 3 and string.strncmp(stub_rel_src + sr_len_src - 3, ".vp", 3) == 0: sr_len_src -= 3 + if sr_len_src > 0: + src_mod_buf: str = stdlib.malloc(sr_len_src + 1) + if src_mod_buf is not None: + sri_src: sizet + for sri_src in range(int(sr_len_src)): + ch_src: char = stub_rel_src[sri_src] + if ch_src == '/' or ch_src == '\\': src_mod_buf[sri_src] = '.' + else: src_mod_buf[sri_src] = ch_src + src_mod_buf[sr_len_src] = '\0' + imported_modules = StubMergerStore.AppendImportedModule(pool, imported_modules, src_mod_buf) + stdlib.free(src_mod_buf) + + # Recursively pre-register the stub import closure's CTypedef/CDefine: CTypedef tables are + # per-file, so an unimported typedef would degrade param types to i8* and misreport CastError. + if imported_modules is not None: _vpi_register_typedef_closure(pool, imported_modules, GenApi.get_temp_dir(), 0) + + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + + # function table info + funcs_ptr: GenTypes.FuncEntry* = trans._funcs + max_funcs: int = 1024 + + # REPL (ReplStubLayoutMode): restore full class layouts and vtable method indices from + # {sha1}.meta first, then run the vpi injection below. Always 0 in batch mode. + if ReplStubLayoutMode != 0: + _repl_stub_collect_module(pool, mod, ch, n, trans, src_sha1, stub_imported_modules, stub_from_imports) + + # Pass 1A: register ALL ClassDefs (fields + structs) first, without methods, so a class + # defined after a global's AnnAssign is still registered before that AnnAssign resolves. + i0: sizet + for i0 in range(int(n)): + child0: ast.AST* = ch.get(i0) + if child0 is None: continue + if child0.kind() != ast.ASTKind.ClassDef: continue + class_cd0: ast.ClassDef* = ast.ClassDef*(child0) + if class_cd0 is None or class_cd0.name is None: continue + cls_name_v0: str = class_cd0.name + # Skip generic classes (class X[T]): the stub template signature has a class-level type + # param T that cannot be resolved; specialized method defines are already emitted in Phase1. + tp_stub0: list[str]* = class_cd0.type_params + if tp_stub0 is not None and len(tp_stub0) > 0: continue + _inject_class_struct_declare(pool, mod, class_cd0, cls_name_v0, trans, src_sha1, imported_modules, from_imports) + + # App pre-registration (Phase A-pre, _pre_allow_dep_inject) skips method/function declare + # injection: its only goal is structs/enums and package re-export aliases, not transient declares. + pre_skip_fn_declares: int = 0 + if trans is not None and trans._pre_allow_dep_inject != 0: pre_skip_fn_declares = 1 + + # Pass 1B: process method declares only after all classes are registered, since method + # params/returns may reference classes defined later in the file (forward references). + for i1 in range(int(n)): + child1: ast.AST* = ch.get(i1) + if child1 is None: continue + if child1.kind() != ast.ASTKind.ClassDef: continue + class_cd1: ast.ClassDef* = ast.ClassDef*(child1) + if class_cd1 is None or class_cd1.name is None: continue + cls_name_v1: str = class_cd1.name + # skip generic classes (same criterion as pass 1A) + tp_stub1: list[str]* = class_cd1.type_params + if tp_stub1 is not None and len(tp_stub1) > 0: continue + cls_children1: list[ast.AST*]* = class_cd1.children + if cls_children1 is None: continue + cn21: sizet = len(cls_children1) + for ci21 in range(int(cn21)): + m_child1: ast.AST* = cls_children1.get(ci21) + if m_child1 is None: continue + if m_child1.kind() != ast.ASTKind.FunctionDef: continue + m_fd1: ast.FunctionDef* = ast.FunctionDef*(m_child1) + if m_fd1 is None or m_fd1.name is None: continue + # App pre-registration skips method declares (see pre_skip_fn_declares above). + if pre_skip_fn_declares == 0: + _inject_method_declare(pool, mod, m_fd1, cls_name_v1, trans, src_sha1, imported_modules, from_imports, i32_ty, i8_ptr_ty, funcs_ptr, max_funcs) + + i: sizet + for i in range(int(n)): + child: ast.AST* = ch.get(i) + if child is None: continue + kd: int = child.kind() + + # FunctionDef -> extract name/params/return type -> inject declare + if kd == ast.ASTKind.FunctionDef: + fd: ast.FunctionDef* = ast.FunctionDef*(child) + if fd is None or fd.name is None: continue + # App pre-registration skips function declares (see pre_skip_fn_declares): + # their annotations may reference classes not yet registered this pass. + if pre_skip_fn_declares != 0: continue + _inject_function_declare(pool, mod, fd, trans, src_sha1, imported_modules, from_imports, i32_ty, i8_ptr_ty, funcs_ptr, max_funcs) + continue + + # AnnAssign with t.CTypedef -> register CTypedef; + # plain module-level globals (e.g. tokens._op_head) -> external global declare + if kd == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(child) + if aa is None: continue + if aa.target is not None and aa.target.kind() == ast.ASTKind.Name: + dbg_tn: ast.Name* = ast.Name*(aa.target) + if dbg_tn.id is not None and dbg_tn.id[0] != '\0': # questionable + if string.strcmp(dbg_tn.id, "irq_handler_t") == 0 or string.strcmp(dbg_tn.id, "gdt") == 0 or string.strcmp(dbg_tn.id, "KBD_BUF_SIZE") == 0: + dbg_ct: int = GenApi._is_ctypedef_annotation(aa.annotation) + dbg_cd: int = GenApi.is_cdefine_annotation(aa.annotation) + if GenApi._is_ctypedef_annotation(aa.annotation) != 0: _register_ctypedef_from_annassign(pool, aa) + elif GenApi.is_cdefine_annotation(aa.annotation) != 0: + # Register vpi CDefine definitions (e.g. keyboard.vp KBD_BUF_SIZE) into the current + # file's CDefine table so later t.CArray counts in the same vpi can resolve them. + if aa.target is not None and aa.target.kind() == ast.ASTKind.Name and aa.value is not None: + cdef_tgt: ast.Name* = ast.Name*(aa.target) + if cdef_tgt.id is not None: + # Skip string CDefines (version numbers etc.): extract_cdefine_int_value + # fails fast on string constants, and count refs only need integer CDefines. + cdef_is_str: int = 0 + cdef_is_float: int = 0 + cdef_float_val: double = 0.0 + if aa.value.kind() == ast.ASTKind.Constant: + cn_v: ast.Constant* = ast.Constant*(aa.value) + if cn_v.const_kind == ast.CONST_STR: cdef_is_str = 1 + elif cn_v.const_kind == ast.CONST_FLOAT: + cdef_is_float = 1 + cdef_float_val = cn_v.float_val + if cdef_is_str == 0 and cdef_is_float != 0: + # Float CDefine (e.g. FRAGMENTATION_THRESHOLD: t.CDefine = 0.7): register via + # register_cdefine_double (integer extraction would fail fast). + GenApi.register_cdefine_double(pool, cdef_tgt.id, cdef_float_val) + elif cdef_is_str == 0: + cdef_val: int64 = GenApi.extract_cdefine_int_value(aa.value) + # 位宽与定义方同一套裁决(注解 → 右侧强转 → 值的最小可存宽度) + GenApi.register_cdefine_constant(pool, cdef_tgt.id, cdef_val, GenApi.cdefine_decl_bits(pool, aa.annotation, aa.value)) + else: + _inject_global_variable_declare(pool, mod, aa, trans, stub_imported_modules, stub_from_imports, i32_ty, i8_ptr_ty, src_sha1) + continue + + # ClassDef -> already registered in the first passes (fields + method declares), skip here. + # (A single pass would fail when classes are defined after globals; see pass 1A above.) + + # Import/ImportFrom -> skip (nothing to process) + + # 不要 free src_sha1:它会被后续阶段继续引用(模块/类型注册按 `.Name` 命名, + # 之后 resolve_annotation_type 仍会对它 strcmp)——在此 free 会造成 heap-use-after-free + # (现场:alloc @ _extract_sha1_from_path → free @ 本函数 → 读 @ resolve_annotation_type:2065)。 + # 跨阶段字符串按本仓惯例常驻;若将来确需回收,应改为在**存入注册表处**拷贝一份到 PermPool, + # 而不是在解析结束时释放。 + # free vpi_buf only after all tree processing (see the parsing comment) + stdlib.free(vpi_buf) + return 1 + + +# On-demand dependency vpi injection (resolves the mutual wait between package __init__ +# re-exports and submodule stubs). Re-entrancy table dedups by sha1; failures stay non-fatal. +define VPI_INJ_MAX = 1024 +_vpi_inj_sha1s: bytes = None +_vpi_inj_count: int = 0 +_vpi_inj_depth: int = 0 + + +def ResetVpiInjectVisited() -> None: + """Clear the on-demand injection reentry table before each module's stub-injection session.""" + global _vpi_inj_sha1s, _vpi_inj_count, _vpi_inj_depth + if _vpi_inj_sha1s is not None: + stdlib.free(_vpi_inj_sha1s) + _vpi_inj_sha1s = None + _vpi_inj_count = 0 + _vpi_inj_depth = 0 + + +def _ensure_dep_vpi_injected(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, dep_sha1: str, trans: HT.Translator*) -> int: + """Inject dep_sha1's .vpi into the current module on demand (reentry-guarded); 1=ready / 0=failure.""" + global _vpi_inj_sha1s, _vpi_inj_count, _vpi_inj_depth + if pool is None or mod is None or dep_sha1 is None or trans is None: return 0 + if dep_sha1[0] == '\0': return 0 + if _vpi_inj_sha1s is None: + _vpi_inj_sha1s = stdlib.malloc(VPI_INJ_MAX * 17) + if _vpi_inj_sha1s is None: return 0 + string.memset(_vpi_inj_sha1s, 0, VPI_INJ_MAX * 17) + _vpi_inj_count = 0 + # already injected / currently injecting (cycle) -> treat as ready and short-circuit + ii: int + for ii in range(int(_vpi_inj_count)): + if string.strcmp(_vpi_inj_sha1s + sizet(ii) * 17, dep_sha1) == 0: return 1 + if _vpi_inj_count >= VPI_INJ_MAX or _vpi_inj_depth >= 32: return 0 + temp_dir: str = GenApi.get_temp_dir() + if temp_dir is None: return 0 + td_len_i: sizet = string.strlen(temp_dir) + sh_len_i: sizet = string.strlen(dep_sha1) + sub_i: str = Config.slice_subdir(dep_sha1, Config.Sha1SliceLevel) + sub_len_i: sizet = 0 + if sub_i is not None: sub_len_i = string.strlen(sub_i) + path_len_i: sizet = td_len_i + sub_len_i + sh_len_i + 8 + path_i: bytes = stdlib.malloc(path_len_i) + if path_i is None: return 0 + if sub_len_i > 0: viperlib.snprintf(path_i, path_len_i, "%s/%s/%s.vpi", temp_dir, sub_i, dep_sha1) + else: viperlib.snprintf(path_i, path_len_i, "%s/%s.vpi", temp_dir, dep_sha1) + vf_i: fileio.File* = fileio.File(path_i, fileio.MODE.R) + if vf_i.closed: + stdlib.free(path_i) + return 0 + vf_i.close() + # register before injecting: recursive injection of the same sha1 short-circuits (cycle guard) + my_idx_i: int = _vpi_inj_count + string.strcpy(_vpi_inj_sha1s + sizet(my_idx_i) * 17, dep_sha1) + _vpi_inj_count += 1 + _vpi_inj_depth += 1 + ret_i: int = ParseVpiIntoModule(pool, mod, path_i, trans) + _vpi_inj_depth -= 1 + stdlib.free(path_i) + if ret_i != 1: + # Injection failed: unregister (move the last entry into this slot, keeping LIFO order), + # else later requests for the same sha1 stay short-circuited and the class never registers. + _vpi_inj_count -= 1 + if my_idx_i != _vpi_inj_count: + string.strcpy(_vpi_inj_sha1s + sizet(my_idx_i) * 17, _vpi_inj_sha1s + sizet(_vpi_inj_count) * 17) + return 0 + return 1 + + +# _inject_reexport_class_declare - register a package re-exported class as an alias entry under the +# package sha1, reusing the submodule's registered entry so ast.Attribute resolves by package. +def _inject_reexport_class_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, src_name: str, local_name: str, sub_sha1: str, pkg_sha1: str, trans: HT.Translator*) -> int: + """Register a package re-exported class as an alias entry under the package sha1.""" + if pool is None or mod is None or src_name is None or local_name is None: return 0 + if sub_sha1 is None or sub_sha1[0] == '\0' or pkg_sha1 is None or pkg_sha1[0] == '\0': return 0 + if src_name[0] == '\0' or local_name[0] == '\0': return 0 + # idempotent: skip if local_name is already registered under the package sha1 + if GenApi.find_struct_by_module(local_name, pkg_sha1) is not None: return 1 + # find the submodule source entry (only classes have entries; functions/constants are skipped) + src_entry: GenTypes.StructEntry* = GenApi.find_struct_by_module(src_name, sub_sha1) + if src_entry is None or src_entry.Ty is None: + # Source stub not injected yet (package __init__ and submodule wait on each other; see + # _ensure_dep_vpi_injected): inject sub_sha1's vpi on demand and retry once; else skip. + if _ensure_dep_vpi_injected(pool, mod, sub_sha1, trans) != 0: src_entry = GenApi.find_struct_by_module(src_name, sub_sha1) + if src_entry is None or src_entry.Ty is None: return 0 + # register the alias entry (bypassing register_struct's Ty dedup) + if GenApi.register_struct_alias(pool, local_name, pkg_sha1, src_entry) is None: return 0 + GenApi.add_visible_struct(pool, local_name) + return 1 + + +# _vpi_tattr_name / _vpi_class_has_packed - detect the vpi class packed decorator. +# The stub keeps @c.Attribute(t.attr.packed); struct injection must mirror the packed flag (B5). +def _vpi_tattr_name(node: ast.AST*, attr_name: str) -> int: + """Extract the outermost attribute name from t.attr. / t.attr.(args) and compare.""" + if node is None: return 0 + if node.kind() == ast.ASTKind.Call: + cl2: ast.Call* = ast.Call*(node) + if cl2.func is None: return 0 + return _vpi_tattr_name(cl2.func, attr_name) + if node.kind() == ast.ASTKind.Attribute: + at2: ast.Attribute* = ast.Attribute*(node) + if at2.attr is None: return 0 + return 1 if string.strcmp(at2.attr, attr_name) == 0 else 0 + return 0 + + +def _vpi_class_has_packed(decorator_list: list[ast.AST*]*) -> int: + """Check whether a vpi ClassDef decorator list contains t.attr.packed.""" + if decorator_list is None: return 0 + dn: sizet = len(decorator_list) + if dn == 0: return 0 + for di in range(int(dn)): + deco: ast.AST* = decorator_list.get(di) + if deco is None: continue + if deco.kind() == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(deco) + if cl.func is None or cl.func.kind() != ast.ASTKind.Attribute: continue + at0: ast.Attribute* = ast.Attribute*(cl.func) + if at0.attr is None or string.strcmp(at0.attr, "Attribute") != 0: continue + if cl.args is None: continue + an: sizet = len(cl.args) + for ai in range(int(an)): + ag: ast.AST* = cl.args.get(ai) + if ag is not None and _vpi_tattr_name(ag, "packed") != 0: return 1 + else: + if _vpi_tattr_name(deco, "packed") != 0: return 1 + return 0 + + +def _vpi_class_is_renum(class_cd: ast.ClassDef*) -> int: + """Check whether a vpi ClassDef is a REnum (base class contains t.REnum); 1=yes / 0=no.""" + if class_cd is None or class_cd.bases is None: return 0 + bases: list[ast.AST*]* = class_cd.bases + bn: sizet = len(bases) + for bi in range(int(bn)): + base_node: ast.AST* = bases.get(bi) + if base_node is None: continue + bk: int = base_node.kind() + # Attribute(t.REnum) form + if bk == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(base_node) + if at.attr is not None and string.strcmp(at.attr, "REnum") == 0: return 1 + # Name(REnum) form (from t import REnum) + elif bk == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(base_node) + if nm.id is not None and string.strcmp(nm.id, "REnum") == 0: return 1 + return 0 + + +# _inject_class_struct_declare - register a vpi ClassDef as a struct type (with fields) so bare +# class-name annotations in the same vpi resolve. Mirrors ClassGenDef.translate_class_def. +def _inject_class_struct_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_cd: ast.ClassDef*, cls_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, name_prefix: str = None) -> int: + """Register a vpi ClassDef as a struct type (with fields) so bare class-name annotations resolve.""" + if pool is None or mod is None or class_cd is None or cls_name is None: return 0 + # Flattened name for nested classes (`{outer}_{inner}`); register even if the base class is not + # registered (the "no registration on field failure" rule lets multi-pass convergence handle it). + real_name: str = cls_name + if name_prefix is not None and name_prefix[0] != '\0': + p_len: sizet = string.strlen(name_prefix) + c_len: sizet = string.strlen(cls_name) + flat_buf_c: str = pool.alloc(p_len + 1 + c_len + 1) + if flat_buf_c is not None: + string.strcpy(flat_buf_c, name_prefix) + flat_buf_c[p_len] = '_' + string.strcpy(flat_buf_c + p_len + 1, cls_name) + real_name = flat_buf_c + # REPL (ReplStubLayoutMode): if this class is already driven by the {sha1}.meta path, yield to it: + # mark visible if registered, defer if the base is pending, skip if the layout is untrusted. + if ReplStubLayoutMode != 0: + se: ReplStubClass* = _repl_stub_find(_repl_stub_key(pool, src_sha1, real_name)) + if se is not None and se.State != _REPL_SS_EMPTY: + if se.State == _REPL_SS_PENDING: _repl_stub_try(se, mod) + if se.State == _REPL_SS_OK: GenApi.add_visible_struct(pool, real_name) + return 1 + # 1. Already-registered check: just mark visible, except forward-decl placeholders + # (IsForwardDecl != 0) which must go through the registration path to fill the same Ty. + existing_c: GenTypes.StructEntry* = None + fill_fwd_c: int = 0 + if src_sha1 is not None and src_sha1[0] != '\0': + existing_c = GenApi.find_struct_by_module(real_name, src_sha1) + if existing_c is not None and existing_c.Ty is not None: + if existing_c.IsForwardDecl != 0: fill_fwd_c = 1 + else: + GenApi.add_visible_struct(pool, real_name) + return 1 + # Skip REnum classes (class X(t.REnum)): their layout is computed from variants by + # ClassGenEnum._translate_renum_def; registering as a plain struct would be wrong. + if _vpi_class_is_renum(class_cd) != 0: return 0 + # Rule 5.5 evidence must be set before field annotation resolution: mark_module_class + # records "{src_sha1}.{real_name}" and points the class-definition window at this class. + GenApi.mark_module_class(pool, src_sha1, real_name) + # 1.5 Nested classes/structs: vpi keeps them as indented ClassDefs; the definition side emits + # flat names (`{outer}_{inner}`), so recurse first with the same naming before field resolution. + _inject_nested_class_declares(pool, mod, class_cd, real_name, trans, src_sha1, imported_modules, from_imports) + # Restore the "defining class" window: the nested recursion's mark_module_class changes + # CurDefClassName to the nested name (A_B), which would corrupt rule 5.5 self-reference evidence. + GenApi.mark_module_class(pool, src_sha1, real_name) + # 2. collect field types (AnnAssign children, mirroring ClassGenDef.translate_class_def) + field_names_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX) + field_types_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX) + if field_names_buf is None or field_types_buf is None: return 0 + string.memset(field_names_buf, 0, 8 * GenTypes.FIELD_MAX) + string.memset(field_types_buf, 0, 8 * GenTypes.FIELD_MAX) + field_count: int = 0 + cls_children_c: list[ast.AST*]* = class_cd.children + if cls_children_c is not None: + cn_c: sizet = len(cls_children_c) + for ci_c in range(int(cn_c)): + m_child_c: ast.AST* = cls_children_c.get(ci_c) + if m_child_c is None: continue + if m_child_c.kind() != ast.ASTKind.AnnAssign: continue + m_aa_c: ast.AnnAssign* = ast.AnnAssign*(m_child_c) + if m_aa_c is None or m_aa_c.target is None: continue + if m_aa_c.target.kind() != ast.ASTKind.Name: continue + m_tgt_c: ast.Name* = ast.Name*(m_aa_c.target) + if m_tgt_c.id is None: continue + field_ty_c: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, m_aa_c.annotation, imported_modules, from_imports, trans, 0) + if field_ty_c is None: + # field type parse failed: do not register this class (leave it to the source module), not fatal + return 0 + if field_count < GenTypes.FIELD_MAX: + field_names_buf[field_count] = sizet(m_tgt_c.id) + field_types_buf[field_count] = sizet(field_ty_c) + field_count += 1 + # 3. The vpi stub has no class fields -> field_count is 0; registering an empty class would make + # the source module hit an empty struct and raise InternalCompilerError, so do not register. + if field_count == 0: return 0 + # 4. build the ParamNode list + first_node_c: mlirlite.ParamNode* = None + prev_node_c: mlirlite.ParamNode* = None + for fi_c in range(int(field_count)): + fty_c: mlirlite.LLVMType* = (mlirlite.LLVMType*)(field_types_buf[fi_c]) + pnode_c: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) + if pnode_c is None: continue + string.memset(pnode_c, 0, mlirlite.ParamNode.__sizeof__()) + pnode_c.Ty = fty_c + if first_node_c is None: first_node_c = pnode_c + if prev_node_c is not None: prev_node_c.Next = pnode_c + prev_node_c = pnode_c + # 4. Named struct type name "sha1.ClassName" (matching ClassGenDef); nested classes use the + # flat real_name, consistent with ClassGenDef._translate_nested_classdefs. + type_name_c: str = real_name + if src_sha1 is not None and src_sha1[0] != '\0': + # [D7 truncation discipline] The old fixed 64-byte buffer silently truncated class names + # over 46 chars; allocate the exact length so the struct name matches ClassGenDef. + sh_len_c: sizet = string.strlen(src_sha1) + cn_len_c: sizet = string.strlen(real_name) + name_buf_c: str = pool.alloc(sh_len_c + 1 + cn_len_c + 1) + if name_buf_c is not None: + string.strcpy(name_buf_c, src_sha1) + name_buf_c[sh_len_c] = '.' + string.strcpy(name_buf_c + sh_len_c + 1, real_name) + type_name_c = name_buf_c + struct_ty_c: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node_c, field_count, type_name_c) + if struct_ty_c is None: return 0 + # 5. Under StdlibAlloc (Phase B) deep-copy into perm_pool to survive cross-file resets + # (matching ClassGenDef.translate_class_def). + if GenApi.get_stdlib_alloc() != 0: + st_copy_c: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, struct_ty_c) + if st_copy_c is not None: struct_ty_c = st_copy_c + # 5.5 Forward-decl placeholder hit -> write in place into *existing.Ty (same as ClassGenDef's + # fill_fwd) so register_struct's Ty-pointer dedup works and the StructTable does not overflow. + if fill_fwd_c != 0 and existing_c is not None and existing_c.Ty is not None: + fwd_name_c: str = mlirlite.get_struct_name(existing_c.Ty) + fwd_fields_c: mlirlite.ParamNode* = mlirlite.get_struct_fields(struct_ty_c) + fwd_fc_c: int = mlirlite.get_struct_fcount(struct_ty_c) + *existing_c.Ty = mlirlite.LLVMType.Struct(fwd_fields_c, fwd_fc_c, fwd_name_c) + struct_ty_c = existing_c.Ty + type_name_c = fwd_name_c + existing_c.IsForwardDecl = 0 + existing_c.FieldCount = 0 + # 6. register into the current module (emit the type def) + global StructTable + visibility + mlirlite.module_add_named_type(mod, pool, struct_ty_c) + # B5: vpi keeps @c.Attribute(t.attr.packed); cross-module injected structs must be packed too + # (<{...}>), else the referencing module computes GEPs with a non-packed layout (fields misread). + if _vpi_class_has_packed(class_cd.decorator_list) != 0: mlirlite.module_set_named_type_packed(mod, pool, struct_ty_c, 1) + # Register under the flat real_name: cross-module `Outer.Inner` lookups query the flat-name + # entry (same source as the definition-side StructGen registration name). + entry_c: GenTypes.StructEntry* = GenApi.register_struct(pool, real_name, struct_ty_c, src_sha1) + if entry_c is None: return 0 + # B5: sync IsPacked on the registered entry; ensure_struct_def_in_module reads it to decide + # packing, otherwise the injected cross-module struct is non-packed (GEP layout mismatch). + if _vpi_class_has_packed(class_cd.decorator_list) != 0: entry_c.IsPacked = 1 + # Also register fields into the StructEntry field table (name + type): a set Ty with an empty + # field table makes lookup_field_by_class fail by name, which silently zeroes reads. + afi_c: int = 0 + for afi_c in range(int(field_count)): + af_name_c: str = str(field_names_buf[afi_c]) + af_ty_c: mlirlite.LLVMType* = (mlirlite.LLVMType*)(field_types_buf[afi_c]) + GenApi.add_field(pool, entry_c, af_name_c, af_ty_c) + GenApi.add_visible_struct(pool, real_name) + return 1 + + +# _inject_nested_class_declares - recursively register nested classes of a vpi class. +# Uses the flat name {outer}_{inner} (matching ClassGenDef._translate_nested_classdefs). +def _inject_nested_class_declares(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_cd: ast.ClassDef*, prefix_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str) -> None: + """Recursively register all nested ClassDefs of class_cd as flat-named types.""" + if pool is None or mod is None or class_cd is None or prefix_name is None: return + children: list[ast.AST*]* = class_cd.children + if children is None: return + cn: sizet = len(children) + for ci in range(int(cn)): + child_ast: ast.AST* = children.get(ci) + if child_ast is None: continue + if child_ast.kind() != ast.ASTKind.ClassDef: continue + child_cd: ast.ClassDef* = ast.ClassDef*(child_ast) + if child_cd is None or child_cd.name is None or child_cd.name[0] == '\0': continue + _inject_class_struct_declare(pool, mod, child_cd, child_cd.name, trans, src_sha1, imported_modules, from_imports, prefix_name) + + +# _ensure_full_value_types - register full definitions for cross-module value types. +# Opaque is not enough for llc (value structs/arrays/full def needed); pointer refs need only opaque. +def _ensure_full_value_types(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> None: + """Recursively register full definitions for value structs and array element structs.""" + if mod is None or pool is None or ty is None: return + match ty: + case mlirlite.LLVMType.Struct(_, _, _): + full_se_h: GenTypes.StructEntry* = GenApi.find_struct_by_type(ty) + if full_se_h is not None and full_se_h.Ty is not None: + mlirlite.module_add_named_type(mod, pool, full_se_h.Ty) + # B5: sync the packed flag when injecting value types (matching + # ensure_struct_def_in_module); without it the referencing module emits a non-packed {..}. + if full_se_h.IsPacked != 0: mlirlite.module_set_named_type_packed(mod, pool, full_se_h.Ty, 1) + case mlirlite.LLVMType.Array(aelem_h, _): + if aelem_h is not None: _ensure_full_value_types(mod, pool, aelem_h) + case _: + pass + + +# _AggregateParamTy - unified rule for bare-struct (aggregate) param types; must stay in sync with +# FuncGenDef's extern branch and the call side. C ABI -> by value (or i64 fallback); internal -> Ptr(struct). +def _AggregateParamTy(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*, is_c_abi: int) -> mlirlite.LLVMType*: + """Bare-struct param type: C ABI by value / internal Ptr(struct).""" + if mod is None or pool is None or ty is None: return ty + if is_c_abi != 0: + # true opaque sentinel: fcount < 0 + is_opaque_ap: int = 0 + match ty: + case mlirlite.LLVMType.Struct(_, fcount_ap, _): + if fcount_ap < 0: is_opaque_ap = 1 + case _: + pass + if is_opaque_ap == 0 and GenApi.find_struct_by_type(ty) is not None: + _ensure_full_value_types(mod, pool, ty) + return ty + return mlirlite.Int64(pool) + ptr_ty_ap: mlirlite.LLVMType* = mlirlite.Ptr(pool, ty) + mlirlite.module_ensure_opaque_for_type(mod, pool, ptr_ty_ap) + return ptr_ty_ap + + +# _inject_global_variable_declare - create a module-level global variable declare from a vpi AnnAssign. +# Needed so cross-module from-import reads of globals can find them via find_global_in_module. +def _inject_global_variable_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, aa: ast.AnnAssign*, trans: HT.Translator*, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, src_sha1: str) -> None: + """Create a module-level global variable external declare from a vpi AnnAssign.""" + if pool is None or mod is None or aa is None or trans is None: return + tgt_v: ast.AST* = aa.target + if tgt_v is None or tgt_v.kind() != ast.ASTKind.Name: return + nm_v: ast.Name* = ast.Name*(tgt_v) + if nm_v is None or nm_v.id is None: return + gname_v: str = nm_v.id + if gname_v[0] == '\0': return + # Self-injection guard (full-translation stage only): a module's own globals are emitted by + # TranslatorGenMixin.handle_module_level_var; re-injecting its own vpi would cause llc redefinition. + if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None and src_sha1 is not None: + if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return + # Mangled name: prefix with the source module SHA1 so the define side and from-import lookup + # agree; a bare short name would remain undefined. + gv_name_m: str = gname_v + # CExport globals: the definition side uses the bare name (@gp), so the stub declare must too, + # else cross-module references emit @sha1.gp and ld.lld reports an undefined symbol. + is_gv_cexport: int = 0 + if aa.annotation is not None: is_gv_cexport = GenApi.has_decorator_marker(aa.annotation, "CExport") + if src_sha1 is not None and src_sha1[0] != '\0' and is_gv_cexport == 0: + ml_v: sizet = string.strlen(src_sha1) + 1 + string.strlen(gname_v) + 1 + gv_name_m = pool.alloc(ml_v) + if gv_name_m is not None: viperlib.snprintf(gv_name_m, ml_v, "%s.%s", src_sha1, gname_v) + # dedup: walk the module's global variable list (matching the mangled name) + cur_v: mlirlite.GlobalVariable* = mod.GlobalHead + while cur_v is not None: + if cur_v.Name is not None and string.strcmp(cur_v.Name, gv_name_m) == 0: return + cur_v = cur_v.Next + # resolve the annotation type (bare class -> Ptr(struct), CArray -> element pointer) + resolved_v: mlirlite.LLVMType* = None + if aa.annotation is not None: + resolved_v = GenApi.resolve_annotation_type(pool, aa.annotation, imported_modules, from_imports, trans) + if resolved_v is None: + return # type parse failed, skip: the call side has a fail-fast location + gv_ty_v: mlirlite.LLVMType* = resolved_v + match resolved_v: + case mlirlite.LLVMType.Struct(_, _, _): + # Object classes (@t.Object/@t.CVTable: object-as-pointer) -> Ptr(struct), matching + # handle_module_level_var; plain t.CStruct value classes keep a value instance. + gv_entry_v: GenTypes.StructEntry* = GenApi.find_struct_by_type(resolved_v) + if gv_entry_v is not None and (gv_entry_v.HasNew != 0 or gv_entry_v.HasVTable != 0 or gv_entry_v.IsOOP != 0): gv_ty_v = mlirlite.Ptr(pool, resolved_v) + else: gv_ty_v = resolved_v + case mlirlite.LLVMType.Array(aelem_v, acount_v): + # Fixed-size arrays (CArray[T, N]) keep the full [N x T] type, matching + # handle_module_level_var; decaying to an element pointer would later read garbage. + if acount_v > 0: gv_ty_v = resolved_v + else: + # variable-length array (CArray[T, None]): element pointer (C array decay) + gv_ty_v = mlirlite.Ptr(pool, aelem_v) + case _: + gv_ty_v = resolved_v + # Ensure embedded structs have an opaque declaration (llc "use of undefined type"), + # matching TranslatorGenMixin; value types get full definitions via _ensure_full_value_types. + _ensure_full_value_types(mod, pool, gv_ty_v) + mlirlite.module_ensure_opaque_for_type(mod, pool, gv_ty_v) + gv_v: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, gv_name_m, gv_ty_v) + if gv_v is None: return + mlirlite.module_add_global(mod, gv_v) + gv_v.Linkage = "external" + + +# _inject_function_declare - create a declare for a single FunctionDef: detect CExport/CExtern/State, +# compute the mangled name, dedup via find_func_in_module, create the declare and register it. +def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, fd: ast.FunctionDef*, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, funcs_ptr: GenTypes.FuncEntry*, max_funcs: int) -> None: + """Create and inject a declare for a FunctionDef into the module.""" + if fd.name is None: return + + # Self-injection guard (full-translation stage only): a module's own top-level functions are + # emitted by FuncGenDef; re-injecting its own vpi would create a conflicting duplicate declare. + if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None: + if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return + + # detect CExtern/State/CExport markers (from the return-type annotation) + has_extern: int = 0 + has_state: int = 0 + has_export: int = 0 + if fd.returns is not None: + has_extern = GenApi.has_decorator_marker(fd.returns, "CExtern") + has_state = GenApi.has_decorator_marker(fd.returns, "State") + has_export = GenApi.has_decorator_marker(fd.returns, "CExport") + # Bare-struct param rule (see _AggregateParamTy): C ABI externs (t.CExtern/t.State) by value; + # the rest (incl. t.CExport) keep Ptr(struct). Same source as FuncGenDef.is_extern_decl. + agg_is_c_abi: int = 0 + if has_extern != 0 or has_state != 0: agg_is_c_abi = 1 + + # compute the mangled name: + # CExport/CExtern/State/main functions use the bare name, others get the source module SHA1 prefix + use_bare_name: int = 0 + if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: + mangled_name: str = fd.name + use_bare_name = 1 + else: mangled_name = GenApi._mangle_name_with_sha1(src_sha1, fd.name) + + # This file's own bare-name declarations take priority: extern/state/export symbols carry no SHA1 + # prefix, so an injected dependency of the same name would silently win. Only bare names matter. + if use_bare_name != 0 and GenApi.is_file_func(fd.name) != 0: return + + # dedup: skip if the function already exists + existing: mlirlite.Function* = GenApi.find_func_in_module(mod, mangled_name) + if existing is not None: return + # also check the bare name (in non-bare mode a CExport function may have been registered another way) + if use_bare_name == 0: + existing_bare: mlirlite.Function* = GenApi.find_func_in_module(mod, fd.name) + if existing_bare is not None: return + + # Infer the return type: prefer the cached return-annotation string (extracted right after tree + # parse) and resolve it directly, avoiding corrupted kind() from cross-module AST reuse. + ret_ty: mlirlite.LLVMType* = None + ret_annot_fd: str = _lookup_vpi_ret_annot(src_sha1, None, fd.name) + if ret_annot_fd is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot_fd, i32_ty, i8_ptr_ty) + if ret_ty is None and fd.returns is not None: + ret_ty = GenApi.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans) + # if the return type is a pure t.State (no real type), use Void + if ret_ty is None and fd.returns is not None: + # t.State/t.CExtern are storage-class decorator markers, not real return types. An annotation + # like "HANDLE | t.State" has a real type and must not map to Void; only pure markers do. + is_pure_marker_rt: int = 0 + rk2_rt: int = fd.returns.kind() + if rk2_rt == ast.ASTKind.Name or rk2_rt == ast.ASTKind.Constant: is_pure_marker_rt = 1 + elif rk2_rt == ast.ASTKind.Attribute: + pm_at: ast.Attribute* = ast.Attribute*(fd.returns) + if pm_at.attr is not None and (string.strcmp(pm_at.attr, "State") == 0 or string.strcmp(pm_at.attr, "CExtern") == 0 or string.strcmp(pm_at.attr, "CExport") == 0): + is_pure_marker_rt = 1 + if is_pure_marker_rt != 0: ret_ty = mlirlite.Void(pool) + elif has_state != 0 or has_extern != 0 or has_export != 0: + # Has a real type but resolution failed: fall back to the external function return-type + # whitelist (e.g. FindFirstFileA/GetStdHandle -> i8* HANDLE) instead of truncating to i32. + ret_ty = GenApi.infer_external_func_ret_ty(pool, fd.name) + # [S4 fail-fast] A return annotation that does not resolve is not silently downgraded to i32; + # no annotation (fd.returns is None) is a legitimate default and keeps i32 below. + if ret_ty is None and fd.returns is not None: + GenApi.fatal_error(fd.returns, f"extern declaration '{fd.name}' return type annotation failed to resolve", "NameError") + # default return i32 + if ret_ty is None: ret_ty = i32_ty + + # A value struct return type (e.g. Mat4) needs a full definition: declaring %Mat4 opaque makes + # llc report "use of undefined type". Register the full definition. + _ensure_full_value_types(mod, pool, ret_ty) + mlirlite.module_ensure_opaque_for_type(mod, pool, ret_ty) + + # create the declare + func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) + if func is None: return + + # extract params and add them to the declare + param_count: int = 0 + fd_defaults: t.CPtr = None + fd_default_count: int = 0 + if fd.args is not None: + args_node: ast.Arguments* = ast.Arguments*(fd.args) + if args_node.args is not None: + args_list: list[ast.AST*]* = args_node.args + param_count = int(len(args_list)) + # Param-count upper-bound sentinel (fail-fast): a corrupted vpi tree node makes __len__() + # return a huge value, so add_param runs massively and the Params chain balloons. + if param_count < 0 or param_count > 128: + stdio.printf("[VPIS] abnormal parameter count: function=%s param_count=%d (fd.args=%llu, tree may be overwritten; aborting)\n", fd.name, param_count, uint64(fd.args)) + # convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed) + VLogger.repl_abort() + pi: sizet + for pi in range(int(param_count)): + ag: ast.Arg* = ast.Arg*(args_list.get(pi)) + if ag is None: continue + pname: str = ag.arg + if pname is None: pname = "arg" + # resolve the param type + param_ty: mlirlite.LLVMType* = i8_ptr_ty + # Prefer the vpi text annotation cache (same source as return types): corrupted tree + # nodes make AST resolve return garbage types and mis-size cross-module declare params. + pseg_ty: mlirlite.LLVMType* = _resolve_vpi_param_annot_seg(pool, src_sha1, None, fd.name, int(pi), i32_ty, i8_ptr_ty) + if pseg_ty is not None: + match pseg_ty: + case mlirlite.LLVMType.Struct(_, _, _): + # Bare struct param: C ABI by value / internal Ptr(struct) + # (criterion and fallback in _AggregateParamTy; keep the three sites in sync) + param_ty = _AggregateParamTy(mod, pool, pseg_ty, agg_is_c_abi) + case mlirlite.LLVMType.Array(aelem_ty, _): + param_ty = mlirlite.Ptr(pool, aelem_ty) + case _: + param_ty = pseg_ty + elif ag.annotation is not None: + resolved_ty: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, ag.annotation, imported_modules, from_imports, trans) + if resolved_ty is not None: + # CArray param -> element pointer ty* (C array decay, matching the compiler side). + # Bare struct param -> _AggregateParamTy (same criterion). + match resolved_ty: + case mlirlite.LLVMType.Struct(_, _, _): + param_ty = _AggregateParamTy(mod, pool, resolved_ty, agg_is_c_abi) + case mlirlite.LLVMType.Array(aelem_ty, _): + param_ty = mlirlite.Ptr(pool, aelem_ty) + case _: + param_ty = resolved_ty + else: + # [S4 fail-fast] An annotation that does not resolve is not silently downgraded + # to i8* (no annotation is a legitimate default and keeps i8*). + GenApi.fatal_error(ag.annotation, f"extern declaration '{fd.name}' parameter '{pname}' type annotation failed to resolve", "NameError") + mlirlite.add_param(pool, func, param_ty, pname) + # Text-cache repair: a corrupted vpi tree truncates args, leaving too few declare params; + # complete them from the vpi text param annotations so declare matches define. + param_count = _repair_declare_params_from_text(pool, func, src_sha1, None, fd.name, param_count, i32_ty, i8_ptr_ty) + # *args varargs: set IsVarArg=1 so the call side applies C ABI promotions (i8/i16 -> i32, + # float -> double), avoiding misread %d/%x formats. + if args_node.vararg is not None: func.IsVarArg = 1 + # Default params: vpi signatures include '= '; register the defaults list so + # cross-module calls missing args fill from the table instead of reading stack garbage. + if args_node.defaults is not None: + fd_dlist: list[ast.AST*]* = args_node.defaults + fd_default_count = int(len(fd_dlist)) + if fd_default_count < 0 or fd_default_count > 128: + # A corrupted args.defaults node makes len return a huge value, so + # _register_global_defaults would over-allocate and write out of bounds; skip it. + stdio.printf("[VPIS] abnormal default parameter count: function=%s default_count=%d (tree may be overwritten; skipping default value registration)\n", fd.name, fd_default_count) + fd_defaults = None + fd_default_count = 0 + else: fd_defaults = fd_dlist + + # register into the function table (using the bare fd.name, not mangled_name) + if funcs_ptr is not None: + cur_count: int = trans._func_count + if GenApi.add_func_to_table(funcs_ptr, cur_count, fd.name, func, max_funcs, fd_defaults, fd_default_count, param_count, None, src_sha1) == 0: + trans._func_count = cur_count + 1 + # Complete the global function-table Spec (params all added, matching FuncGenDef): without it + # find_func_entry_global() sees an empty Spec and vtable generation fails to find the type. + GenApi.update_global_func_spec(fd.name, func) + + +# _inject_method_declare - create a declare for a ClassDef method. Mangled name is +# {src_sha1}.{ClassName}.{method}; param types come from vpi annotations (self defaults to i8*). +def _inject_method_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, m_fd: ast.FunctionDef*, cls_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, funcs_ptr: GenTypes.FuncEntry*, max_funcs: int) -> None: + """Create and inject a declare for a ClassDef method into the module.""" + if m_fd is None or m_fd.name is None or cls_name is None: return + if src_sha1 is None: return + + # Self-injection guard (full-translation stage only): a module's own method defines are emitted + # by ClassGenVTable; re-injecting its own vpi would create conflicting duplicate declares. + if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None: + if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return + + # build the method lookup name: ClassName.method + lookup_name_len: sizet = string.strlen(cls_name) + 1 + string.strlen(m_fd.name) + 1 + lookup_name: str = pool.alloc(lookup_name_len) + if lookup_name is None: return + viperlib.snprintf(lookup_name, lookup_name_len, "%s.%s", cls_name, m_fd.name) + + # mangled name: {src_sha1}.{ClassName}.{method} + mangled_len: sizet = string.strlen(src_sha1) + 1 + lookup_name_len + mangled_name: str = pool.alloc(mangled_len) + if mangled_name is None: return + viperlib.snprintf(mangled_name, mangled_len, "%s.%s", src_sha1, lookup_name) + + # dedup: skip if the method already exists + existing: mlirlite.Function* = GenApi.find_func_in_module(mod, mangled_name) + if existing is not None: return + + # Infer the return type: prefer the cached return-annotation string (extracted right after tree + # parse); resolving m_fd.returns directly may yield garbage from a corrupted vpi tree node. + ret_ty: mlirlite.LLVMType* = None + ret_annot: str = _lookup_vpi_ret_annot(src_sha1, cls_name, m_fd.name) + if ret_annot is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty) + if ret_ty is None and m_fd.returns is not None: + ret_ty = GenApi.resolve_annotation_type(pool, m_fd.returns, imported_modules, from_imports, trans) + # infer __new__ without annotation as a class pointer (it returns a self pointer; i32 would truncate) + if ret_ty is None and string.strcmp(m_fd.name, "__new__") == 0: + cls_ty: mlirlite.LLVMType* = GenApi.get_struct_type(cls_name) + if cls_ty is not None: ret_ty = mlirlite.Ptr(pool, cls_ty) + # [S4 fail-fast] A return annotation that does not resolve is not silently downgraded to i32; + # unannotated __new__ / normal methods keep i32 as the legitimate default below. + if ret_ty is None and m_fd.returns is not None: + GenApi.fatal_error(m_fd.returns, f"extern method '{cls_name}.{m_fd.name}' return type annotation failed to resolve", "NameError") + # Unannotated __init__ -> Void, matching the definition side (ClassGenVTable always uses Void); + # defaulting to i32 would split declare i32 / define void. + if ret_ty is None and string.strcmp(m_fd.name, "__init__") == 0: ret_ty = mlirlite.Void(pool) + if ret_ty is None: ret_ty = i32_ty + + # create the declare + func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) + if func is None: return + + # extract params and add them to the declare + param_count: int = 0 + md_defaults: t.CPtr = None + md_default_count: int = 0 + if m_fd.args is not None: + args_node: ast.Arguments* = ast.Arguments*(m_fd.args) + if args_node.args is not None: + args_list: list[ast.AST*]* = args_node.args + param_count = int(len(args_list)) + # Param-count upper-bound sentinel (fail-fast): same cause as _inject_function_declare + # (a corrupted vpi tree node makes __len__() return a huge value). + if param_count < 0 or param_count > 128: + stdio.printf("[VPIS] abnormal parameter count: method=%s.%s param_count=%d (m_fd.args=%llu, tree may be overwritten; aborting)\n", cls_name, m_fd.name, param_count, uint64(m_fd.args)) + # convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed) + VLogger.repl_abort() + pi: sizet + for pi in range(int(param_count)): + ag: ast.Arg* = ast.Arg*(args_list.get(pi)) + if ag is None: continue + pname: str = ag.arg + if pname is None: pname = "arg" + # self/cls (first param): cross-module declares use the class pointer (%"sha1.Class"*) + # instead of i8*, matching _translate_method's Ptr(StructTy) and removing ABI ambiguity. + if pi == 0 and (string.strcmp(pname, "self") == 0 or string.strcmp(pname, "cls") == 0): + cls_ty_sd: mlirlite.LLVMType* = GenApi.get_struct_type(cls_name) + if cls_ty_sd is not None: + mlirlite.add_param(pool, func, mlirlite.Ptr(pool, cls_ty_sd), pname) + continue + param_ty: mlirlite.LLVMType* = i8_ptr_ty + # Prefer the vpi text annotation cache (same source as return types): corrupted tree + # nodes mis-size declare params, causing trunc i64->i32 and a caller/callee mismatch. + pseg_ty: mlirlite.LLVMType* = _resolve_vpi_param_annot_seg(pool, src_sha1, cls_name, m_fd.name, int(pi), i32_ty, i8_ptr_ty) + if pseg_ty is not None: + match pseg_ty: + case mlirlite.LLVMType.Struct(_, _, _): + # Bare struct param: methods are always internal VPC symbols (mangled + # {sha1}.{Class}.{method}, no C ABI define), so is_c_abi=0 -> Ptr(struct). + param_ty = _AggregateParamTy(mod, pool, pseg_ty, 0) + case mlirlite.LLVMType.Array(aelem_ty, _): + param_ty = mlirlite.Ptr(pool, aelem_ty) + case _: + param_ty = pseg_ty + elif ag.annotation is not None: + resolved_ty: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, ag.annotation, imported_modules, from_imports, trans) + if resolved_ty is not None: + # CArray param -> element pointer ty* (C array decay, matching the compiler side). + # Bare struct param -> _AggregateParamTy (same criterion, is_c_abi=0). + match resolved_ty: + case mlirlite.LLVMType.Struct(_, _, _): + param_ty = _AggregateParamTy(mod, pool, resolved_ty, 0) + case mlirlite.LLVMType.Array(aelem_ty, _): + param_ty = mlirlite.Ptr(pool, aelem_ty) + case _: + param_ty = resolved_ty + else: + # [S4 fail-fast] An annotation that does not resolve is not silently downgraded + # to i8* (no annotation is a legitimate default and keeps i8*). + GenApi.fatal_error(ag.annotation, f"extern method '{cls_name}.{m_fd.name}' parameter '{pname}' type annotation failed to resolve", "NameError") + mlirlite.add_param(pool, func, param_ty, pname) + + # Text-cache repair: a corrupted vpi tree truncates args, leaving too few declare params; + # complete them from the vpi text param annotations so declare matches define. + param_count = _repair_declare_params_from_text(pool, func, src_sha1, cls_name, m_fd.name, param_count, i32_ty, i8_ptr_ty) + + # OOP iter protocol: the __next__ stub declare carries a hidden __stop_iter_flag__ (i1*), + # matching _translate_method's define; `for x in obj` calls __next__(self, flag). + if m_fd.name is not None and string.strcmp(m_fd.name, "__next__") == 0: + i1_ty_si: mlirlite.LLVMType* = mlirlite.Int1(pool) + if i1_ty_si is not None: + i1_ptr_ty_si: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_si) + if i1_ptr_ty_si is not None: mlirlite.add_param(pool, func, i1_ptr_ty_si, "__stop_iter_flag__") + # Default params: register the defaults list so cross-module calls missing args fill from the + # table instead of reading stack garbage. + if m_fd.args is not None: + md_args_node: ast.Arguments* = ast.Arguments*(m_fd.args) + if md_args_node.defaults is not None: + md_dlist: list[ast.AST*]* = md_args_node.defaults + md_default_count = int(len(md_dlist)) + if md_default_count < 0 or md_default_count > 128: + # Same as _inject_function_declare: a corrupted tree node makes defaults len huge, + # which would write out of bounds into perm_pool; skip registration. + stdio.printf("[VPIS] abnormal default parameter count: method=%s.%s default_count=%d (tree may be overwritten; skipping default value registration)\n", cls_name, m_fd.name, md_default_count) + md_defaults = None + md_default_count = 0 + else: md_defaults = md_dlist + + # Register into the function table (bare ClassName.method, matching the caller's lookup_name). + # ParamCount excludes self (matching _translate_method's len(args) - 1), so default filling aligns. + if funcs_ptr is not None: + cur_count: int = trans._func_count + param_count_no_self: int = param_count + if param_count_no_self > 0: param_count_no_self -= 1 + if GenApi.add_func_to_table(funcs_ptr, cur_count, lookup_name, func, max_funcs, md_defaults, md_default_count, param_count_no_self, None, src_sha1) == 0: + trans._func_count = cur_count + 1 + # Complete the global function-table Spec (matching ClassGenVTable): otherwise + # find_func_entry_global().Spec is empty and vtable generation fails to find the method type. + GenApi.update_global_func_spec(lookup_name, func) + + +# _register_ctypedef_from_annassign - register a CTypedef from an AnnAssign node. +# Extracts the NAME and type expression from `NAME: t.CTypedef = Type` and registers it. +def _register_ctypedef_from_annassign(pool: memhub.MemBuddy*, aa: ast.AnnAssign*) -> None: + """Extract and register a CTypedef from an AnnAssign node.""" + if aa is None or pool is None: return + # extract the target name (must be a Name node) + if aa.target is None or aa.target.kind() != ast.ASTKind.Name: return + tgt: ast.Name* = ast.Name*(aa.target) + if tgt.id is None: return + # extract the type expression string (from the value AST node) + if aa.value is None: return + type_str: str = GenApi._get_type_name_from_annotation(pool, aa.value) + if type_str is None: return + # register the CTypedef + GenApi.register_ctypedef(pool, tgt.id, type_str) + + +# REPL: restore full layouts and vtables for includes classes, driven by {sha1}.meta. +# Only active when ReplStubLayoutMode != 0; layouts are validated against meta field/slot counts. + +# session-level state (PermPool; valid during session init) +ReplStubLayoutMode: int = 0 +ReplStubPool: memhub.MemBuddy* = None +ReplStubTempDir: str = None +ReplStubTable: sizet* = None +ReplStubCount: int = 0 +ReplStubOk: int = 0 +ReplStubVtOk: int = 0 +ReplStubRefused: int = 0 +ReplStubRefusedNoStub: int = 0 # refusal cause distribution (_REPL_SU_*): missing data (module has no {sha1}.meta) +ReplStubRefusedNoClass: int = 0 # refused: meta has the module but not this class +ReplStubRefusedOpaque: int = 0 # refused: the class is OPAQUE in meta +ReplStubRefusedParent: int = 0 # upstream no data: base unavailable/not registered +ReplStubNoStub: int = 0 +ReplStubWarnLeft: int = 0 +ReplStubTmpCount: int = -1 +ReplStubTmpPacked: int = 0 +ReplStubTmpArity: int = 0 + + +def _repl_stub_dupstr(pool: memhub.MemBuddy*, src: str) -> str: + """Deep-copy a string into the given pool.""" + if src is None or pool is None: return None + ln: sizet = string.strlen(src) + dst: str = pool.alloc(ln + 1) + if dst is None: return None + string.strcpy(dst, src) + return dst + + +def _repl_stub_key(pool: memhub.MemBuddy*, sha1: str, cls: str) -> str: + """Build ".", matching the definition-side named struct exactly.""" + if pool is None or sha1 is None or cls is None: return None + if sha1[0] == '\0' or cls[0] == '\0': return None + ln: sizet = string.strlen(sha1) + 1 + string.strlen(cls) + 1 + buf: str = pool.alloc(ln) + if buf is None: return None + viperlib.snprintf(buf, ln, "%s.%s", sha1, cls) + return buf + + +def _repl_stub_path(sha1: str, ext: str) -> str: + """Build the slice path //. under the REPL temp dir (stdlib-allocated).""" + if ReplStubTempDir is None or sha1 is None: return None + td_len: sizet = string.strlen(ReplStubTempDir) + sh_len: sizet = string.strlen(sha1) + sub: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel) + sub_len: sizet = 0 + if sub is not None: sub_len = string.strlen(sub) + cap: sizet = td_len + sub_len + sh_len + 8 + buf: str = stdlib.malloc(cap) + if buf is None: return None + if sub_len > 0: viperlib.snprintf(buf, cap, "%s/%s/%s.%s", ReplStubTempDir, sub, sha1, ext) + else: viperlib.snprintf(buf, cap, "%s/%s.%s", ReplStubTempDir, sha1, ext) + return buf + + +def _repl_stub_readtext(path: str) -> str: + """Read stub text (stdlib.malloc, caller frees); None if missing or over the size cap.""" + if path is None: return None + f: fileio.File* = fileio.File(path, fileio.MODE.R) + if f is None or f.closed: return None + sz: sizet = f.size() + if sz <= 0 or sz >= _REPL_STUB_TXT_CAP: + f.close() + return None + buf: str = stdlib.malloc(sz + 1) + if buf is None: + f.close() + return None + rd: LONG = f.read_all(buf, sz + 1) + f.close() + if rd <= 0: + stdlib.free(buf) + return None + if rd < LONG(sz + 1): buf[rd] = 0 + return buf + + +# parse a decimal integer from txt[start, end) (skip leading spaces; no digits -> 0) +def _meta_atoi(txt: str, start: sizet, end: sizet) -> int: + i: sizet = start + while i < end and (txt[i] == ' ' or txt[i] == '\t'): i += 1 + neg: int = 0 + if i < end and txt[i] == '-': + neg = 1 + i += 1 + v: int = 0 + while i < end and txt[i] >= '0' and txt[i] <= '9': + v = v * 10 + (int(txt[i]) - 48) + i += 1 + if neg != 0: return -v + return v + + +# Scan {sha1}.meta text for `STRUCT FIELDS PACKED <0|1>` (or `OPAQUE`) and +# `VTABLE SLOTS `; results go to global scratch. Exact prefix match per line. +def _repl_meta_scan_layout(txt: str, key: str) -> None: + global ReplStubTmpCount, ReplStubTmpPacked, ReplStubTmpArity + ReplStubTmpCount = -1 + ReplStubTmpPacked = 0 + ReplStubTmpArity = 0 + if txt is None or key is None: return + n: sizet = string.strlen(txt) + key_len: sizet = string.strlen(key) + if key_len == 0: return + i: sizet = 0 + while i < n: + ls: sizet = i + while i < n and txt[i] != '\n': i += 1 + le: sizet = i + if i < n: i += 1 + if le > ls and txt[le - 1] == '\r': le -= 1 + if ls + 7 <= le and string.strncmp(txt + ls, "STRUCT ", 7) == 0: + ns: sizet = ls + 7 + ne: sizet = ns + key_len + if ne > le or string.strncmp(txt + ns, key, key_len) != 0: continue + if ne >= le or txt[ne] != ' ': continue + # OPAQUE + if ne + 7 <= le and string.strncmp(txt + ne + 1, "OPAQUE", 6) == 0: + ReplStubTmpCount = -2 + continue + # FIELDS PACKED <0|1> + if ne + 8 <= le and string.strncmp(txt + ne + 1, "FIELDS ", 7) == 0: + ReplStubTmpCount = _meta_atoi(txt, ne + 8, le) + ps: sizet = ne + 8 + while ps < le and txt[ps] >= '0' and txt[ps] <= '9': ps += 1 + if ps + 8 <= le and string.strncmp(txt + ps + 1, "PACKED ", 7) == 0: ReplStubTmpPacked = _meta_atoi(txt, ps + 8, le) + continue + if ls + 7 <= le and string.strncmp(txt + ls, "VTABLE ", 7) == 0: + vs: sizet = ls + 7 + ve: sizet = vs + key_len + if ve > le or string.strncmp(txt + vs, key, key_len) != 0: continue + if ve >= le or txt[ve] != ' ': continue + if ve + 7 <= le and string.strncmp(txt + ve + 1, "SLOTS ", 6) == 0: ReplStubTmpArity = _meta_atoi(txt, ve + 7, le) + continue + return + + +def ReplStubLayoutBegin(temp_dir: str) -> int: + """Enable session-level stub layout restore (called once before registering includes classes); 0=success.""" + global ReplStubLayoutMode, ReplStubPool, ReplStubTempDir, ReplStubTable + global ReplStubCount, ReplStubOk, ReplStubVtOk, ReplStubRefused, ReplStubNoStub + global ReplStubWarnLeft + global ReplStubRefusedNoStub, ReplStubRefusedNoClass, ReplStubRefusedOpaque, ReplStubRefusedParent + pool: memhub.MemBuddy* = CompilerFactory.factory.PermPool + if pool is None or temp_dir is None: return 1 + ReplStubPool = pool + ReplStubTempDir = _repl_stub_dupstr(pool, temp_dir) + if ReplStubTempDir is None: return 1 + ReplStubTable = pool.alloc(8 * _REPL_STUB_CLS_MAX) + if ReplStubTable is None: return 1 + string.memset(ReplStubTable, 0, 8 * _REPL_STUB_CLS_MAX) + ReplStubCount = 0 + ReplStubOk = 0 + ReplStubVtOk = 0 + ReplStubRefused = 0 + ReplStubRefusedNoStub = 0 + ReplStubRefusedNoClass = 0 + ReplStubRefusedOpaque = 0 + ReplStubRefusedParent = 0 + ReplStubNoStub = 0 + ReplStubWarnLeft = _REPL_STUB_WARN_MAX + ReplStubLayoutMode = 1 + return 0 + + +def _repl_stub_at(i: int) -> ReplStubClass*: + """Return the i-th class-table entry, or None.""" + if ReplStubTable is None or i < 0 or i >= ReplStubCount: return None + addr: sizet = ReplStubTable[i] + if addr == 0: return None + return ReplStubClass*(uint64(addr)) + + +def _repl_stub_find(key: str) -> ReplStubClass*: + """Look up a class-table entry by "."; None if not found.""" + if key is None: return None + i: int + for i in range(int(ReplStubCount)): + e: ReplStubClass* = _repl_stub_at(int(i)) + if e is None or e.Key is None: continue + if string.strcmp(e.Key, key) == 0: return e + return None + + +def _repl_stub_find_base(e: ReplStubClass*, bi: int) -> ReplStubClass*: + """Find a base-class entry: same-module SHA1 exact match first, then first bare-name match.""" + if e is None or e.Bases is None or bi < 0 or bi >= e.BaseCount: return None + pname: str = rawptr(e.Bases[bi]) + if pname is None: return None + i: int + for i in range(int(ReplStubCount)): + pe: ReplStubClass* = _repl_stub_at(int(i)) + if pe is None or pe.ClassName is None: continue + if string.strcmp(pe.ClassName, pname) != 0: continue + if e.Sha1 is not None and pe.Sha1 is not None: + if string.strcmp(pe.Sha1, e.Sha1) == 0: return pe + for i in range(int(ReplStubCount)): + pe2: ReplStubClass* = _repl_stub_at(int(i)) + if pe2 is None or pe2.ClassName is None: continue + if string.strcmp(pe2.ClassName, pname) == 0: return pe2 + return None + + +def _repl_stub_has_name(arr: sizet*, cnt: int, name: str) -> int: + """Check whether the name is already in the list (first-declaration wins).""" + if arr is None or name is None: return 0 + i: int + for i in range(int(cnt)): + addr: sizet = arr[i] + if addr == 0: continue + ex: str = rawptr(addr) + if ex is not None and string.strcmp(ex, name) == 0: return 1 + return 0 + + +def _repl_stub_deco_flags(deco_list: list[ast.AST*]*) -> int: + """Return class/method decorator flags (bit 0=@t.NoVTable, bit 1=@t.CVTable).""" + if deco_list is None: return 0 + flags: int = 0 + dn: sizet = len(deco_list) + di: sizet + for di in range(int(dn)): + deco: ast.AST* = deco_list.get(di) + if deco is None: continue + nm: str = None + k: int = deco.kind() + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(deco) + if at.attr is not None and at.value is not None and at.value.kind() == ast.ASTKind.Name: + vn: ast.Name* = ast.Name*(at.value) + if vn.id is not None and string.strcmp(vn.id, "t") == 0: nm = at.attr + elif k == ast.ASTKind.Name: + nn: ast.Name* = ast.Name*(deco) + nm = nn.id + if nm is None: continue + if string.strcmp(nm, "NoVTable") == 0: flags = flags | 1 + elif string.strcmp(nm, "CVTable") == 0: flags = flags | 2 + return flags + + +def _repl_stub_is_virtual(e: ReplStubClass*, mi: int, first: ReplStubClass*) -> int: + """Return whether this class's mi-th method goes into the vtable.""" + if e is None or e.MethodFlags is None or mi < 0 or mi >= e.MethodCount: return 0 + mf: int = int(e.MethodFlags[mi]) + if (mf & 1) != 0: return 0 + if (mf & 2) != 0: return 1 + if (e.Flags & _REPL_SF_CVTABLE) != 0: return 1 + if first is not None: + if first.IsNoVTable != 0: return 0 + return first.HasVt + return 0 + + +def _repl_stub_warn_refuse(e: ReplStubClass*, why: str, a: int, b: int) -> None: + """Warn on a criterion rejection: stub field count a differs from flattened+vtable inference b.""" + global ReplStubWarnLeft, ReplStubRefused + ReplStubRefused += 1 + if ReplStubWarnLeft <= 0: return + ReplStubWarnLeft -= 1 + stdio.printf("includes class layout: layout of %s.%s is untrusted => explicitly refused (reason=criterion refused: %s; " "stub field count=%d != flattened field count+vtable slots=%d). Field access on this class is explicitly refused " "(method calls are unaffected)\n", e.Sha1, e.ClassName, why, a, b) + + +def _repl_stub_warn_nodata(e: ReplStubClass*, cause: int) -> None: + """Warn on a missing-data refusal: no comparison was possible, so print the data sources instead.""" + global ReplStubWarnLeft, ReplStubRefused + global ReplStubRefusedNoStub, ReplStubRefusedNoClass + global ReplStubRefusedOpaque, ReplStubRefusedParent + ReplStubRefused += 1 + if cause == _REPL_SU_NOSTUB: ReplStubRefusedNoStub += 1 + elif cause == _REPL_SU_NOCLASS: ReplStubRefusedNoClass += 1 + elif cause == _REPL_SU_OPAQUE: ReplStubRefusedOpaque += 1 + else: ReplStubRefusedParent += 1 + if ReplStubWarnLeft <= 0: return + ReplStubWarnLeft -= 1 + if cause == _REPL_SU_NOSTUB: + # missing data: not even module layout metadata (LayoutCount = -1 is just a "not read" sentinel) + stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=data missing: " "module %s has no {sha1}.meta layout metadata; meta field count=none / inferred field count=not computed => " "no data on either side, no comparison was made, and this is not a field count mismatch). Field access on this class is " "refused (method calls are unaffected); add the module meta (run batch processing once in the same project, " "Phase A/B writes {sha1}.meta), then rerun REPL with the same --project to recover\n", e.Sha1, e.ClassName, e.Sha1) + elif cause == _REPL_SU_OPAQUE: + # refused: the key is OPAQUE in meta (no body, so no field count to compare) + stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=criterion refused: " "key %s in meta %s.meta is declared OPAQUE (no body); meta field count=unavailable / " "inferred field count=not computed => no comparison was made). Field access on this class is refused " "(method calls are unaffected)\n", e.Sha1, e.ClassName, e.Sha1, e.Key) + else: + # refused: meta exists but has no STRUCT line for this class (LayoutCount = -1 is a "not found" sentinel) + stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=criterion refused: " "meta %s.meta exists but has no `STRUCT ... FIELDS n / OPAQUE` record for key %s; " "meta field count=not found / inferred field count=not computed => no comparison was made). Field access on this class is " "refused (method calls are unaffected); if this class is indeed defined in that module, the meta is stale/" "incomplete, please rebuild\n", e.Sha1, e.ClassName, e.Sha1, e.Key) + + +def _repl_stub_warn_parent(e: ReplStubClass*, detail: str, pname: str) -> None: + """Warn on an upstream-no-data refusal: the base layout is unavailable or unregistered.""" + global ReplStubWarnLeft, ReplStubRefused, ReplStubRefusedParent + ReplStubRefused += 1 + ReplStubRefusedParent += 1 + if ReplStubWarnLeft <= 0: return + ReplStubWarnLeft -= 1 + stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=upstream no data: %s, " "parent=%s; parent fields and vtable slots cannot be flattened => no field count comparison was done for this class). Field access on this class is " "refused (method calls are unaffected)\n", e.Sha1, e.ClassName, detail, pname) + + +def _repl_stub_try(e: ReplStubClass*, mod: mlirlite.LLVMModule*) -> int: + """Validate and register the full layout from the stub: 1=registered / 0=base pending / -1=refused.""" + global ReplStubOk, ReplStubVtOk, ReplStubWarnLeft + if e is None: return -1 + if e.State == _REPL_SS_OK: return 1 + if e.State == _REPL_SS_REFUSED: return -1 + if e.State != _REPL_SS_PENDING: return -1 + pool: memhub.MemBuddy* = ReplStubPool + if pool is None: + e.State = _REPL_SS_REFUSED + return -1 + # (1) base classes must be ready (not ready -> defer; refused -> this layout is untrusted) + bi: int + for bi in range(int(e.BaseCount)): + pe: ReplStubClass* = _repl_stub_find_base(e, bi) + if pe is None: return 0 + if pe.State == _REPL_SS_PENDING: return 0 + if pe.State == _REPL_SS_REFUSED: + e.State = _REPL_SS_REFUSED + _repl_stub_warn_parent(e, "Parent layout untrusted", pe.ClassName) + return -1 + if pe.State == _REPL_SS_EMPTY and pe.OwnFail != 0: + # The base itself had field-annotation failures, so its Count is only partially parsed: + # the flattened field count is unusable, so neither compare nor allow it (M4.2: refuse). + e.State = _REPL_SS_REFUSED + _repl_stub_warn_parent(e, "Not all field types of the parent are resolved (its field count is incomplete)", pe.ClassName) + return -1 + first: ReplStubClass* = None + if e.BaseCount > 0: first = _repl_stub_find_base(e, 0) + # (2) HasVTable (transcribed from ClassGenBase._detect_vtable_status) + hv: int = 0 + if (e.Flags & _REPL_SF_CVTABLE) != 0: hv = 1 + elif (e.Flags & _REPL_SF_NOVTABLE) != 0: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0 + elif first is not None: + if first.IsNoVTable != 0: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0 + else: hv = first.HasVt + else: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0 + # (3) field flattening (base fields first + own fields, deduped by short name, first wins) + cn: int = 0 + for bi in range(int(e.BaseCount)): + pe: ReplStubClass* = _repl_stub_find_base(e, bi) + fi: int + for fi in range(int(pe.Count)): + if cn >= _REPL_STUB_FIELD_MAX: break + nm: str = rawptr(pe.Names[fi]) + if nm is None: continue + if _repl_stub_has_name(e.Names, int(cn), nm) != 0: continue + e.Names[cn] = pe.Names[fi] + e.Types[cn] = pe.Types[fi] + cn += 1 + oi: int + for oi in range(int(e.OwnCount)): + if cn >= _REPL_STUB_FIELD_MAX: break + nm2: str = rawptr(e.OwnNames[oi]) + if nm2 is None: continue + if _repl_stub_has_name(e.Names, int(cn), nm2) != 0: continue + e.Names[cn] = e.OwnNames[oi] + e.Types[cn] = e.OwnTypes[oi] + cn += 1 + e.Count = cn + e.HasVt = hv + # (4) layout check (reached only when both sides are usable): stub field count == + # __vtable__ (if any) + flattened field count. + if e.LayoutCount != cn + hv: + e.State = _REPL_SS_REFUSED + _repl_stub_warn_refuse(e, "Stub field count does not match the field name list", e.LayoutCount, cn + hv) + return -1 + # (5) virtual method short-name order (base virtuals first + own eligible methods in class order) + vn: int = 0 + if hv == 1: + for bi in range(int(e.BaseCount)): + pe: ReplStubClass* = _repl_stub_find_base(e, bi) + vi: int + for vi in range(int(pe.VtCount)): + if vn >= _REPL_STUB_VT_MAX: break + mn: str = rawptr(pe.VtNames[vi]) + if mn is None: continue + if _repl_stub_has_name(e.VtNames, vn, mn) != 0: continue + e.VtNames[vn] = pe.VtNames[vi] + vn += 1 + mi: int + for mi in range(int(e.MethodCount)): + if vn >= _REPL_STUB_VT_MAX: break + mn2: str = rawptr(e.MethodNames[mi]) + if mn2 is None: continue + if string.strcmp(mn2, "__init__") == 0: continue + if string.strcmp(mn2, "__before_init__") == 0: continue + if string.strcmp(mn2, "__new__") == 0: continue + if _repl_stub_has_name(e.VtNames, vn, mn2) != 0: continue + if _repl_stub_is_virtual(e, mi, first) == 0: continue + e.VtNames[vn] = e.MethodNames[mi] + vn += 1 + e.VtCount = vn + e.VtTrust = 0 + if hv == 1 and vn > 0: + if vn == e.VtArity: e.VtTrust = 1 + elif ReplStubWarnLeft > 0: + ReplStubWarnLeft -= 1 + stdio.printf("includes class layout: vtable slot count of %s.%s does not match the stub (%d vs %d) " "-- this class skips virtual dispatch (degrades to direct call, session continues)\n", e.Sha1, e.ClassName, vn, e.VtArity) + # (6) build the LLVM struct (named "%.", same as the definition side) + total: int = int(cn + hv) + i8_ptr: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + if i8_ptr is None: return 0 + first_node: mlirlite.ParamNode* = None + prev_node: mlirlite.ParamNode* = None + if hv == 1: + nd0: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) + if nd0 is None: return 0 + string.memset(nd0, 0, mlirlite.ParamNode.__sizeof__()) + nd0.Ty = i8_ptr + first_node = nd0 + prev_node = nd0 + fi2: int + for fi2 in range(int(cn)): + fty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(e.Types[fi2]) + if fty is None: continue + nd: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) + if nd is None: continue + string.memset(nd, 0, mlirlite.ParamNode.__sizeof__()) + nd.Ty = fty + if first_node is None: first_node = nd + if prev_node is not None: prev_node.Next = nd + prev_node = nd + struct_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node, total, e.Key) + if struct_ty is None: return 0 + # (7) register (under StdlibAlloc=1 registration goes to PermPool, surviving across fragments) + entry: GenTypes.StructEntry* = GenApi.register_struct(pool, e.ClassName, struct_ty, e.Sha1) + if entry is None: + e.State = _REPL_SS_REFUSED + return -1 + if mod is not None: mlirlite.module_add_named_type(mod, pool, struct_ty) + if (e.Flags & _REPL_SF_PACKED) != 0: + entry.IsPacked = 1 + if mod is not None: mlirlite.module_set_named_type_packed(mod, pool, struct_ty, 1) + if hv == 1: + entry.HasVTable = 1 + GenApi.add_field(pool, entry, "__vtable__", i8_ptr) + fi3: int + for fi3 in range(int(cn)): + fname: str = rawptr(e.Names[fi3]) + fty3: mlirlite.LLVMType* = (mlirlite.LLVMType*)(e.Types[fi3]) + if fname is None or fty3 is None: continue + GenApi.add_field(pool, entry, fname, fty3) + if e.IsNoVTable != 0: entry.IsNoVTable = 1 + entry.IsOOP = e.IsOOP + entry.HasInit = e.HasInit + entry.HasNew = e.HasNew + # parent names (multiple-inheritance array; declaration order, for vtable/MRO/field-inheritance queries) + if e.BaseCount > 0: + arr: sizet* = pool.alloc(8 * e.BaseCount) + if arr is not None: + string.memset(arr, 0, 8 * e.BaseCount) + bi3: int + for bi3 in range(int(e.BaseCount)): + bnm: str = rawptr(e.Bases[bi3]) + bdup: str = _repl_stub_dupstr(pool, bnm) + if bdup is not None: arr[bi3] = sizet(bdup) + entry.ParentNames = arr + entry.ParentCount = e.BaseCount + pn: str = rawptr(e.Bases[0]) + entry.ParentName = _repl_stub_dupstr(pool, pn) + if hv == 1 and e.VtTrust == 1 and e.VtCount > 0: + entry.VTableMethods = e.VtNames + entry.VTableMethodCount = e.VtCount + ReplStubVtOk += 1 + e.State = _REPL_SS_OK + ReplStubOk += 1 + return 1 + + +def _repl_stub_add_class(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cd: ast.ClassDef*, sha1: str, txt: str, have_stub: int, trans: HT.Translator*, imported_modules: str, from_imports: str) -> int: + """Collect layout elements of one vpi ClassDef and try to register it; 1=registered/deferred, 0=skipped.""" + global ReplStubCount + if pool is None or cd is None or cd.name is None or ReplStubTable is None: return 0 + if cd.name[0] == '\0' or ReplStubCount >= _REPL_STUB_CLS_MAX: return 0 + key: str = _repl_stub_key(pool, sha1, cd.name) + if key is None: return 0 + if _repl_stub_find(key) is not None: return 0 + e: ReplStubClass* = pool.alloc(ReplStubClass.__sizeof__()) + if e is None: return 0 + string.memset(e, 0, ReplStubClass.__sizeof__()) + e.Key = key + e.ClassName = _repl_stub_dupstr(pool, cd.name) + e.Sha1 = _repl_stub_dupstr(pool, sha1) + e.OwnNames = pool.alloc(8 * _REPL_STUB_FIELD_MAX) + e.OwnTypes = pool.alloc(8 * _REPL_STUB_FIELD_MAX) + e.MethodNames = pool.alloc(8 * _REPL_STUB_FIELD_MAX) + e.MethodFlags = pool.alloc(8 * _REPL_STUB_FIELD_MAX) + e.Bases = pool.alloc(8 * _REPL_STUB_FIELD_MAX) + e.Names = pool.alloc(8 * _REPL_STUB_FIELD_MAX) + e.Types = pool.alloc(8 * _REPL_STUB_FIELD_MAX) + e.VtNames = pool.alloc(8 * _REPL_STUB_VT_MAX) + if e.OwnNames is None or e.OwnTypes is None or e.MethodNames is None or e.MethodFlags is None or e.Bases is None or e.Names is None or e.Types is None or e.VtNames is None: return 0 + string.memset(e.OwnNames, 0, 8 * _REPL_STUB_FIELD_MAX) + string.memset(e.OwnTypes, 0, 8 * _REPL_STUB_FIELD_MAX) + string.memset(e.MethodNames, 0, 8 * _REPL_STUB_FIELD_MAX) + string.memset(e.MethodFlags, 0, 8 * _REPL_STUB_FIELD_MAX) + string.memset(e.Bases, 0, 8 * _REPL_STUB_FIELD_MAX) + string.memset(e.Names, 0, 8 * _REPL_STUB_FIELD_MAX) + string.memset(e.Types, 0, 8 * _REPL_STUB_FIELD_MAX) + string.memset(e.VtNames, 0, 8 * _REPL_STUB_VT_MAX) + # class decorators + cf: int = _repl_stub_deco_flags(cd.decorator_list) + if (cf & 2) != 0: e.Flags = e.Flags | _REPL_SF_CVTABLE + if (cf & 1) != 0: + e.Flags = e.Flags | _REPL_SF_NOVTABLE + e.IsNoVTable = 1 + if _vpi_class_has_packed(cd.decorator_list) != 0: e.Flags = e.Flags | _REPL_SF_PACKED + # real bases (reuse ClassGenBase._collect_parents: alias resolution + marker removal) + pb: sizet* = pool.alloc(8 * GenTypes.PARENT_MAX) + bcnt: int = 0 + if pb is not None and GenApi._g_gen is not None: + string.memset(pb, 0, 8 * GenTypes.PARENT_MAX) + bcnt = GenApi._g_gen.ClassGenBase._collect_parents(pool, cd, pb, GenTypes.PARENT_MAX, trans) + if bcnt > 0: + if bcnt > _REPL_STUB_FIELD_MAX: bcnt = _REPL_STUB_FIELD_MAX + bi: int + for bi in range(int(bcnt)): + bnm: str = rawptr(pb[bi]) + bdup: str = _repl_stub_dupstr(pool, bnm) + if bdup is not None: e.Bases[bi] = sizet(bdup) + e.BaseCount = bcnt + # scan the class body: fields (AnnAssign) + methods (FunctionDef) + children: list[ast.AST*]* = cd.children + own_fail: int = 0 + if children is not None: + cn: sizet = len(children) + ci: sizet + for ci in range(int(cn)): + ch2: ast.AST* = children.get(ci) + if ch2 is None: continue + k2: int = ch2.kind() + if k2 == ast.ASTKind.AnnAssign: + if e.OwnCount >= _REPL_STUB_FIELD_MAX: continue + aa2: ast.AnnAssign* = ast.AnnAssign*(ch2) + if aa2 is None or aa2.target is None or aa2.annotation is None: continue + if aa2.target.kind() != ast.ASTKind.Name: continue + tg: ast.Name* = ast.Name*(aa2.target) + if tg.id is None or tg.id[0] == '\0': continue + fty2: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, aa2.annotation, imported_modules, from_imports, trans, 0) + if fty2 is None: + own_fail = 1 + continue + fdup: str = _repl_stub_dupstr(pool, tg.id) + if fdup is None: + own_fail = 1 + continue + e.OwnNames[e.OwnCount] = sizet(fdup) + e.OwnTypes[e.OwnCount] = sizet(fty2) + e.OwnCount += 1 + continue + if k2 == ast.ASTKind.FunctionDef: + fd2: ast.FunctionDef* = ast.FunctionDef*(ch2) + if fd2 is None or fd2.name is None or fd2.name[0] == '\0': continue + mf2: int = _repl_stub_deco_flags(fd2.decorator_list) + if (mf2 & 2) != 0: e.Flags = e.Flags | _REPL_SF_ANYFUNC + if string.strcmp(fd2.name, "__init__") == 0: e.HasInit = 1 + elif string.strcmp(fd2.name, "__new__") == 0: e.HasNew = 1 + e.IsOOP = 1 + if e.MethodCount < _REPL_STUB_FIELD_MAX: + e.MethodNames[e.MethodCount] = sizet(_repl_stub_dupstr(pool, fd2.name)) + e.MethodFlags[e.MethodCount] = sizet(mf2) + e.MethodCount += 1 + continue + # stub metadata (LayoutCount stays at the "not found" sentinel when missing; have_stub distinguishes causes) + e.LayoutCount = _REPL_STUB_NOCOUNT_NOCLASS + if txt is not None: + _repl_meta_scan_layout(txt, key) + e.LayoutCount = ReplStubTmpCount + e.VtArity = ReplStubTmpArity + if ReplStubTmpPacked != 0: e.Flags = e.Flags | _REPL_SF_PACKED + # state decision + if own_fail != 0: + # Field-type resolution failed: defer to the existing vpi injection (which skips the class); + # OwnFail records that OwnCount is only partially parsed and unusable for subclass flattening. + e.OwnFail = 1 + e.State = _REPL_SS_EMPTY + e.Count = e.OwnCount + e.Names = e.OwnNames + e.Types = e.OwnTypes + elif e.LayoutCount >= 0: e.State = _REPL_SS_PENDING + elif e.BaseCount == 0 and (e.Flags & (_REPL_SF_CVTABLE | _REPL_SF_ANYFUNC)) == 0: + # no base, no vtable -> this class's own fields are the full layout (equivalent to the existing rule) + e.State = _REPL_SS_EMPTY + e.Count = e.OwnCount + e.Names = e.OwnNames + e.Types = e.OwnTypes + else: + # No layout data -> explicitly refuse (no StructEntry). Report the causes separately: + # missing module meta (recoverable) vs OPAQUE/no STRUCT record (untrusted). + e.State = _REPL_SS_REFUSED + if have_stub == 0: _repl_stub_warn_nodata(e, _REPL_SU_NOSTUB) + elif e.LayoutCount == _REPL_STUB_NOCOUNT_OPAQUE: _repl_stub_warn_nodata(e, _REPL_SU_OPAQUE) + else: _repl_stub_warn_nodata(e, _REPL_SU_NOCLASS) + ReplStubTable[ReplStubCount] = sizet(e) + ReplStubCount += 1 + if e.State == _REPL_SS_PENDING: _repl_stub_try(e, mod) + return 1 + + +def _repl_stub_collect_module(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, ch: list[ast.AST*]*, n: sizet, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str) -> int: + """(REPL) Collect layout elements for the current module's classes and try to register them.""" + global ReplStubNoStub, ReplStubWarnLeft + if ReplStubLayoutMode == 0 or pool is None or ch is None: return 0 + if src_sha1 is None or src_sha1[0] == '\0' or ReplStubTable is None: return 0 + # layout metadata ({sha1}.meta; if missing, classes with bases/vtables are refused as "missing data") + txt: str = None + sp: str = _repl_stub_path(src_sha1, "meta") + if sp is not None: + txt = _repl_stub_readtext(sp) + stdlib.free(sp) + have_stub: int = 0 + if txt is not None: have_stub = 1 + else: + ReplStubNoStub += 1 + if ReplStubWarnLeft > 0: + ReplStubWarnLeft -= 1 + stdio.printf("includes class layout: module %s has no .meta stub (data missing) -- this module " "classes with parent/vtable will each be explicitly refused as data missing (layout metadata cannot be read => no " "field count to compare, not a field count mismatch). First run batch processing once in the same project to generate stubs, " "then reuse them with `--project .\\project.vpj --repl` (in the ViperC directory)\n", src_sha1) + cnt: int = 0 + i: sizet + for i in range(int(n)): + child: ast.AST* = ch.get(i) + if child is None: continue + if child.kind() != ast.ASTKind.ClassDef: continue + cd: ast.ClassDef* = ast.ClassDef*(child) + if cd is None or cd.name is None: continue + # Generic template classes are skipped (same as _inject_class_struct_declare / pass 1A), + # but the owning module must be registered so generic subscripts can build type placeholders. + tp: list[str]* = cd.type_params + if tp is not None and len(tp) > 0: + StubMergerStore.RegisterGenericOwner(cd.name, src_sha1) + continue + # REnum classes: layout {i32 tag, payload} is computed by ClassGenEnum, so skip them here + if _vpi_class_is_renum(cd) != 0: continue + if _repl_stub_add_class(pool, mod, cd, src_sha1, txt, have_stub, trans, imported_modules, from_imports) != 0: + cnt += 1 + if txt is not None: stdlib.free(txt) + return cnt + + +def ReplStubLayoutFinish() -> int: + """Finish: converge remaining class registrations, print a summary, and return the registered count.""" + global ReplStubRefused, ReplStubWarnLeft + if ReplStubLayoutMode == 0: return 0 + rounds: int + for rounds in range(int(_REPL_STUB_FINISH_PASSES)): + prog: int = 0 + i: int + for i in range(int(ReplStubCount)): + e: ReplStubClass* = _repl_stub_at(int(i)) + if e is None or e.State != _REPL_SS_PENDING: continue + if _repl_stub_try(e, None) != 0: prog += 1 + if prog == 0: break + # still not ready -> the base was never registered (missing stub/refused): explicitly refuse + i2: int + for i2 in range(int(ReplStubCount)): + e2: ReplStubClass* = _repl_stub_at(i2) + if e2 is None or e2.State != _REPL_SS_PENDING: continue + e2.State = _REPL_SS_REFUSED + # find the still-unready base class name (to identify the upstream) + pbad: str = "(not located)" + bi2: int + for bi2 in range(int(e2.BaseCount)): + peb: ReplStubClass* = _repl_stub_find_base(e2, bi2) + if peb is not None and peb.State == _REPL_SS_OK: continue + if peb is not None and peb.ClassName is not None: pbad = peb.ClassName + break + _repl_stub_warn_parent(e2, "Parent not registered in the session or its layout was refused", pbad) + stdio.printf("includes class layout: %d complete layouts (%d with a usable vtable index) / " "%d refused / %d modules missing layout metadata\n", ReplStubOk, ReplStubVtOk, ReplStubRefused, ReplStubNoStub) + stdio.printf(" refusal reason distribution (self-witnessing data source): data missing-module has no {sha1}.meta %d classes / " "criterion refused-meta has no such class %d / criterion refused-meta is opaque %d / " "upstream no data-parent unavailable %d\n", ReplStubRefusedNoStub, ReplStubRefusedNoClass, ReplStubRefusedOpaque, ReplStubRefusedParent) + if ReplStubRefused > 0 or ReplStubNoStub > 0: + stdio.printf(" Note: field access on refused classes reports class not registered/attribute access failed " "(it does not silently access at a wrong offset); method calls are unaffected.\n") + if ReplStubRefusedNoStub > 0: + stdio.printf(" Of these, the %d classes with data missing simply failed to read the module layout metadata (the session temp directory %s " "is missing these {sha1}.meta): only batch processing writes meta to disk (the REPL branch runs before Phase1 " "and returns, so --repl itself does not generate stubs). Fix: run batch processing once in the same project " "(ViperC directory: .\\tpv_build\\output\\viperc.exe --project " ".\\project.vpj; add --rebuild-includes when includes changed), then " "reuse its temp directory with --project .\\project.vpj --repl\n", ReplStubRefusedNoStub, ReplStubTempDir) + if ReplStubRefusedNoClass + ReplStubRefusedOpaque > 0: + stdio.printf(" Of these, the %d classes with criterion refused: data exists but is untrusted (the stub has no field count for this class, " "or it is declared opaque); per M4.2 none are allowed through -- please confirm whether this class should have been registered as a struct " "in the first place: if it should be registered as a struct but is not found in the stub => the stub is stale/incomplete (rebuild includes " "stubs)\n", ReplStubRefusedNoClass + ReplStubRefusedOpaque) + return ReplStubOk + + +def ReplStubLayoutEnd() -> None: + """Disable stub layout restore (the batch path is unaffected afterwards).""" + global ReplStubLayoutMode + ReplStubLayoutMode = 0 diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index a692b02..56f1276 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1,176 +1,176 @@ -import t, c -from stdint import * -import ast -import mlirlite -import memhub -import string -import stdio -import stdlib -import viperlib -import win32.fileio as fileio -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HT -import lib.Stub.StubMerger.StubMergerStore as StubMergerStore -import lib.core.Gen.GenTypes as GenTypes -import lib.core.Gen.Generator as Generator -import lib.core.CompilerFactory as CompilerFactory -import lib.Stub.StubGen.VpiReader as VpiReader -import lib.core.Gen.GenApi as GenApi - -# AssignGenMixin - assignment translation mixin -novirtual class AssignGenMixin: - Gen: Generator.Generator* - """Assignment translation mixin.""" - - RecoverFound: int - - # Check whether an annotation is t.CDefine (module-level function) - def is_cdefine_annotation(self, annot: ast.AST*) -> int: - """Return 1 if the annotation is t.CDefine, else 0.""" - if annot is None: return 0 - k: int = annot.kind() - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(annot) - if at.attr is not None and string.strcmp(at.attr, "CDefine") == 0: return 1 - return 0 - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(annot) - if self.is_cdefine_annotation(bop.left) != 0: return 1 - if self.is_cdefine_annotation(bop.right) != 0: return 1 - return 0 - - # extract_cdefine_int_value - extract an integer constant from AnnAssign.value - # ---- 统一编译期常量表达式求值(值 + 位宽) ---- - # 所有 define/常量折叠入口都走这里,避免各写一套(此前 extract_cdefine_int_value / - # _eval_const_int_expr / _cif_eval_node 三套互相独立,导致位宽信息在 define 上丢失)。 - def extract_cdefine_int_value(self, val_node: ast.AST*) -> int64: - """仅取值(丢弃位宽)的薄封装;需要位宽请用 extract_cdefine_int_value_bits。""" - b_legacy: int = 0 - return self.extract_cdefine_int_value_bits(val_node, None, &b_legacy) - - def extract_cdefine_int_value_bits(self, val_node: ast.AST*, pool: memhub.MemBuddy*, bits_out: int*) -> int64: - """编译期常量表达式求值:返回值,并把**位宽**写入 bits_out[0](0=未知)。 - - 位宽规则(供 define 物化用): - - 显式强转(Call 的 callee 是整型/typedef,如 WORD / int / t.CUInt16T)→ 该位宽; - - Name 引用其它 define → 沿用其位宽; - - BinOp:两侧位宽**相等** → 保持该位宽(**不提升**);不等 → 取较宽;任一未知 → 未知; - - 字面量/一元 → 未知(物化时按值推断)。 - pool 为 None 时无法解析 typedef(仅旧调用方走此路),强转按"取实参值、位宽未知"处理。 - """ - if bits_out is not None: bits_out[0] = 0 - if val_node is None: return 0 - k: int = val_node.kind() - - # Case 1: Constant(INT) - e.g. 0x0002 - if k == ast.ASTKind.Constant: - cn: ast.Constant* = ast.Constant*(val_node) - # True/False boolean constant -> 1/0 (bool treated as int in CDefine context) - if cn.const_kind == ast.CONST_BOOL: return 1 if cn.int_val != 0 else 0 - if cn.const_kind != ast.CONST_INT: - # Non-INT constant: fail fast - self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") - return cn.int_val - - # Case 2: BinOp - e.g. FOREGROUND_RED | FOREGROUND_GREEN, (1 << N) - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(val_node) - lb: int = 0 - rb: int = 0 - left_val: int64 = self.extract_cdefine_int_value_bits(bop.left, pool, &lb) - right_val: int64 = self.extract_cdefine_int_value_bits(bop.right, pool, &rb) - if bits_out is not None: - if lb != 0 and rb != 0: bits_out[0] = lb if lb > rb else rb - elif lb != 0: bits_out[0] = lb - elif rb != 0: bits_out[0] = rb - if bop.op == ast.OpKind.BitOr: return left_val | right_val - if bop.op == ast.OpKind.BitAnd: return left_val & right_val - if bop.op == ast.OpKind.BitXor: return left_val ^ right_val - # 移位:求值器内部按 64 位算,计数为负或 >= 64 时结果无意义(宿主侧是 poison/未定义), - # 与 require_shift_count_in_range 同一口径直接 fail-fast,不产出垃圾常量。 - if bop.op == ast.OpKind.LShift or bop.op == ast.OpKind.RShift: - if right_val < 0 or right_val >= 64: - self.Gen.TypeGenErr.fatal_error(val_node, f"constant expression shift count out of range: the shift count evaluates to {right_val} at compile time, " "but the evaluator works on 64-bit integers (valid range 0..63); the result would be undefined. " "Fix: use a count in 0..63, or restructure the expression.", "ValueError") - if bop.op == ast.OpKind.LShift: return left_val << right_val - return left_val >> right_val - if bop.op == ast.OpKind.Add: return left_val + right_val - if bop.op == ast.OpKind.Sub: return left_val - right_val - if bop.op == ast.OpKind.Mult: return left_val * right_val - # 编译期除零:绝不把除法交给宿主(会直接把编译器打挂),fail-fast 给出可读诊断。 - if bop.op == ast.OpKind.Div or bop.op == ast.OpKind.FloorDiv or bop.op == ast.OpKind.Mod: - if right_val == 0: - self.Gen.TypeGenErr.fatal_error(val_node, "constant expression division by zero: the right operand of '/', '//' or '%' evaluates to 0 at compile time.", "ValueError") - if bop.op == ast.OpKind.Mod: return left_val % right_val - return left_val // right_val - # Unknown BinOp operator: fail fast - self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") - - # Case 3: Name - a registered CDefine constant or True/False literal - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(val_node) - if nm.id is not None: - # True/False literal -> 1/0 (bool treated as int in CDefine context) - if string.strcmp(nm.id, "True") == 0: return 1 - if string.strcmp(nm.id, "False") == 0: return 0 - looked_up: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(nm.id) - if self.Gen.TypeGenCDef.is_cdefine_found() != 0: - # 沿用被引用 define 的位宽(宽度随宏传播,不再丢) - if bits_out is not None: bits_out[0] = self.Gen.TypeGenCDef.lookup_cdefine_bits(nm.id) - return looked_up - # Unregistered reference: fail fast - self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") - - # Case 4: Call - 整型/typedef 强转(如 WORD(0x0080) / t.CUnsignedLong(-11)): - # 取实参值,并按**目标类型的位宽**规范化(这赋予常量"已知宽度")。 - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(val_node) - t_bits: int = 0 - if pool is not None: t_bits = self._annot_int_bits(pool, cl.func) - # Bind the property to a list[...]* local before calling len() (else GEP base breaks) - cl_args_list: list[ast.AST*]* = cl.args - if cl_args_list is not None and len(cl_args_list) > 0: - first_arg: ast.AST* = cl_args_list.get(0) - ib: int = 0 - arg_val: int64 = self.extract_cdefine_int_value_bits(first_arg, pool, &ib) - if t_bits != 0: - if bits_out is not None: bits_out[0] = t_bits - return self._normalize_cdefine_bits(arg_val, t_bits) - if bits_out is not None: bits_out[0] = ib - return arg_val - # Call with no args: fail fast - self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") - - # Case 5: UnaryOp - e.g. -11 - if k == ast.ASTKind.UnaryOp: - uop: ast.UnaryOp* = ast.UnaryOp*(val_node) - ob: int = 0 - operand_val: int64 = self.extract_cdefine_int_value_bits(uop.operand, pool, &ob) - if bits_out is not None: bits_out[0] = ob - if uop.op == ast.OpKind.USub: return -operand_val - if uop.op == ast.OpKind.UAdd: return operand_val - if uop.op == ast.OpKind.Invert: return ~operand_val - # Unknown UnaryOp operator: fail fast - self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") - - # Unknown node kind: fail fast - self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") - # After fatal_error add an explicit return so the block is properly terminated - return 0 - - def _normalize_cdefine_bits(self, value: int64, bits: int) -> int64: - """把值按位宽**截断**后再重解释为该位宽的有符号表示(与 ExprGenCdef._cdefine_bits_value 同口径)。 - - 「左侧只会看到截断了的数据」:超宽的值先按 bits 取低位(如 WORD(0x80000000) → 0), - 不是直接把原值搬过来、也不是只对"刚好越界一点"的值做重解释。 - 实现用左移后算术右移,而不是 `value & 0xFFFFFFFF` 这类大掩码字面量: - 掩码字面量(如 4294967295)超出 int32 范围,其加宽方向在 TPC/TPV 上可能判成 sext, - 会得到 0xFFFFFFFFFFFFFFFF 而不是 0xFFFFFFFF,导致截断失效。 - """ - if bits == 8: return (value << 56) >> 56 - if bits == 16: return (value << 48) >> 48 - if bits == 32: return (value << 32) >> 32 - return value - +import t, c +from stdint import * +import ast +import mlirlite +import memhub +import string +import stdio +import stdlib +import viperlib +import win32.fileio as fileio +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HT +import lib.Stub.StubMerger.StubMergerStore as StubMergerStore +import lib.core.Gen.GenTypes as GenTypes +import lib.core.Gen.Generator as Generator +import lib.core.CompilerFactory as CompilerFactory +import lib.Stub.StubGen.VpiReader as VpiReader +import lib.core.Gen.GenApi as GenApi + +# AssignGenMixin - assignment translation mixin +novirtual class AssignGenMixin: + Gen: Generator.Generator* + """Assignment translation mixin.""" + + RecoverFound: int + + # Check whether an annotation is t.CDefine (module-level function) + def is_cdefine_annotation(self, annot: ast.AST*) -> int: + """Return 1 if the annotation is t.CDefine, else 0.""" + if annot is None: return 0 + k: int = annot.kind() + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(annot) + if at.attr is not None and string.strcmp(at.attr, "CDefine") == 0: return 1 + return 0 + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(annot) + if self.is_cdefine_annotation(bop.left) != 0: return 1 + if self.is_cdefine_annotation(bop.right) != 0: return 1 + return 0 + + # extract_cdefine_int_value - extract an integer constant from AnnAssign.value + # ---- 统一编译期常量表达式求值(值 + 位宽) ---- + # 所有 define/常量折叠入口都走这里,避免各写一套(此前 extract_cdefine_int_value / + # _eval_const_int_expr / _cif_eval_node 三套互相独立,导致位宽信息在 define 上丢失)。 + def extract_cdefine_int_value(self, val_node: ast.AST*) -> int64: + """仅取值(丢弃位宽)的薄封装;需要位宽请用 extract_cdefine_int_value_bits。""" + b_legacy: int = 0 + return self.extract_cdefine_int_value_bits(val_node, None, &b_legacy) + + def extract_cdefine_int_value_bits(self, val_node: ast.AST*, pool: memhub.MemBuddy*, bits_out: int*) -> int64: + """编译期常量表达式求值:返回值,并把**位宽**写入 bits_out[0](0=未知)。 + + 位宽规则(供 define 物化用): + - 显式强转(Call 的 callee 是整型/typedef,如 WORD / int / t.CUInt16T)→ 该位宽; + - Name 引用其它 define → 沿用其位宽; + - BinOp:两侧位宽**相等** → 保持该位宽(**不提升**);不等 → 取较宽;任一未知 → 未知; + - 字面量/一元 → 未知(物化时按值推断)。 + pool 为 None 时无法解析 typedef(仅旧调用方走此路),强转按"取实参值、位宽未知"处理。 + """ + if bits_out is not None: bits_out[0] = 0 + if val_node is None: return 0 + k: int = val_node.kind() + + # Case 1: Constant(INT) - e.g. 0x0002 + if k == ast.ASTKind.Constant: + cn: ast.Constant* = ast.Constant*(val_node) + # True/False boolean constant -> 1/0 (bool treated as int in CDefine context) + if cn.const_kind == ast.CONST_BOOL: return 1 if cn.int_val != 0 else 0 + if cn.const_kind != ast.CONST_INT: + # Non-INT constant: fail fast + self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") + return cn.int_val + + # Case 2: BinOp - e.g. FOREGROUND_RED | FOREGROUND_GREEN, (1 << N) + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(val_node) + lb: int = 0 + rb: int = 0 + left_val: int64 = self.extract_cdefine_int_value_bits(bop.left, pool, &lb) + right_val: int64 = self.extract_cdefine_int_value_bits(bop.right, pool, &rb) + if bits_out is not None: + if lb != 0 and rb != 0: bits_out[0] = lb if lb > rb else rb + elif lb != 0: bits_out[0] = lb + elif rb != 0: bits_out[0] = rb + if bop.op == ast.OpKind.BitOr: return left_val | right_val + if bop.op == ast.OpKind.BitAnd: return left_val & right_val + if bop.op == ast.OpKind.BitXor: return left_val ^ right_val + # 移位:求值器内部按 64 位算,计数为负或 >= 64 时结果无意义(宿主侧是 poison/未定义), + # 与 require_shift_count_in_range 同一口径直接 fail-fast,不产出垃圾常量。 + if bop.op == ast.OpKind.LShift or bop.op == ast.OpKind.RShift: + if right_val < 0 or right_val >= 64: + self.Gen.TypeGenErr.fatal_error(val_node, f"constant expression shift count out of range: the shift count evaluates to {right_val} at compile time, " "but the evaluator works on 64-bit integers (valid range 0..63); the result would be undefined. " "Fix: use a count in 0..63, or restructure the expression.", "ValueError") + if bop.op == ast.OpKind.LShift: return left_val << right_val + return left_val >> right_val + if bop.op == ast.OpKind.Add: return left_val + right_val + if bop.op == ast.OpKind.Sub: return left_val - right_val + if bop.op == ast.OpKind.Mult: return left_val * right_val + # 编译期除零:绝不把除法交给宿主(会直接把编译器打挂),fail-fast 给出可读诊断。 + if bop.op == ast.OpKind.Div or bop.op == ast.OpKind.FloorDiv or bop.op == ast.OpKind.Mod: + if right_val == 0: + self.Gen.TypeGenErr.fatal_error(val_node, "constant expression division by zero: the right operand of '/', '//' or '%' evaluates to 0 at compile time.", "ValueError") + if bop.op == ast.OpKind.Mod: return left_val % right_val + return left_val // right_val + # Unknown BinOp operator: fail fast + self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") + + # Case 3: Name - a registered CDefine constant or True/False literal + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(val_node) + if nm.id is not None: + # True/False literal -> 1/0 (bool treated as int in CDefine context) + if string.strcmp(nm.id, "True") == 0: return 1 + if string.strcmp(nm.id, "False") == 0: return 0 + looked_up: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(nm.id) + if self.Gen.TypeGenCDef.is_cdefine_found() != 0: + # 沿用被引用 define 的位宽(宽度随宏传播,不再丢) + if bits_out is not None: bits_out[0] = self.Gen.TypeGenCDef.lookup_cdefine_bits(nm.id) + return looked_up + # Unregistered reference: fail fast + self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") + + # Case 4: Call - 整型/typedef 强转(如 WORD(0x0080) / t.CUnsignedLong(-11)): + # 取实参值,并按**目标类型的位宽**规范化(这赋予常量"已知宽度")。 + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(val_node) + t_bits: int = 0 + if pool is not None: t_bits = self._annot_int_bits(pool, cl.func) + # Bind the property to a list[...]* local before calling len() (else GEP base breaks) + cl_args_list: list[ast.AST*]* = cl.args + if cl_args_list is not None and len(cl_args_list) > 0: + first_arg: ast.AST* = cl_args_list.get(0) + ib: int = 0 + arg_val: int64 = self.extract_cdefine_int_value_bits(first_arg, pool, &ib) + if t_bits != 0: + if bits_out is not None: bits_out[0] = t_bits + return self._normalize_cdefine_bits(arg_val, t_bits) + if bits_out is not None: bits_out[0] = ib + return arg_val + # Call with no args: fail fast + self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") + + # Case 5: UnaryOp - e.g. -11 + if k == ast.ASTKind.UnaryOp: + uop: ast.UnaryOp* = ast.UnaryOp*(val_node) + ob: int = 0 + operand_val: int64 = self.extract_cdefine_int_value_bits(uop.operand, pool, &ob) + if bits_out is not None: bits_out[0] = ob + if uop.op == ast.OpKind.USub: return -operand_val + if uop.op == ast.OpKind.UAdd: return operand_val + if uop.op == ast.OpKind.Invert: return ~operand_val + # Unknown UnaryOp operator: fail fast + self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") + + # Unknown node kind: fail fast + self.Gen.TypeGenErr.fatal_error(val_node, "CDefine value extraction failed: cannot parse a compile-time constant from the expression", "TypeError") + # After fatal_error add an explicit return so the block is properly terminated + return 0 + + def _normalize_cdefine_bits(self, value: int64, bits: int) -> int64: + """把值按位宽**截断**后再重解释为该位宽的有符号表示(与 ExprGenCdef._cdefine_bits_value 同口径)。 + + 「左侧只会看到截断了的数据」:超宽的值先按 bits 取低位(如 WORD(0x80000000) → 0), + 不是直接把原值搬过来、也不是只对"刚好越界一点"的值做重解释。 + 实现用左移后算术右移,而不是 `value & 0xFFFFFFFF` 这类大掩码字面量: + 掩码字面量(如 4294967295)超出 int32 范围,其加宽方向在 TPC/VPC 上可能判成 sext, + 会得到 0xFFFFFFFFFFFFFFFF 而不是 0xFFFFFFFF,导致截断失效。 + """ + if bits == 8: return (value << 56) >> 56 + if bits == 16: return (value << 48) >> 48 + if bits == 32: return (value << 32) >> 32 + return value + def cdefine_decl_bits(self, pool: memhub.MemBuddy*, annot: ast.AST*, value_node: ast.AST*) -> int: """define 声明的位宽裁决(定义方与跨模块使用方**共用同一套**,避免两边位宽不一致)。 @@ -188,27 +188,27 @@ novirtual class AssignGenMixin: if rhs_bits != 0: return rhs_bits return self.define_min_width(val) - # define / t.CDefine integer constant storable and width checks - # Auto-derived => 64-bit pattern; explicit type T => T's width range. - def define_min_width(self, value: int64) -> int: - """Return the minimal storable width (8/16/32/64) for an integer value.""" - if value < 0: - if value >= -128: return 8 - if value >= -32768: return 16 - # -2^31: use an arithmetic equivalent to avoid an i32-wrapping literal - if value >= -2147483647 - 1: return 32 - return 64 - if value <= 255: return 8 - if value <= 65535: return 16 - if value <= 2147483647: return 32 - # 2^31..2^32-1:无符号 32 位位型即可容纳 → 32 位。 - # 用 (value >> 32) == 0 判断,**不要**写成 uint64(uint32(value)) == value:uint32(...) 之后的 - # 加宽方向(zext/sext)由 should_zext_widen 按节点无符号性决定,TPC 与 TPV 自举的判定可能不一致; - # 一旦判成 sext,uint64(0x80000000) 会变成 0xFFFFFFFF80000000 ≠ value → 误判 64 位 → - # `GENERIC_READ | GENERIC_WRITE` 被放大到 i64 → 赋给 32 位目标报隐式窄化。 - if (value >> 32) == 0: return 32 - return 64 - + # define / t.CDefine integer constant storable and width checks + # Auto-derived => 64-bit pattern; explicit type T => T's width range. + def define_min_width(self, value: int64) -> int: + """Return the minimal storable width (8/16/32/64) for an integer value.""" + if value < 0: + if value >= -128: return 8 + if value >= -32768: return 16 + # -2^31: use an arithmetic equivalent to avoid an i32-wrapping literal + if value >= -2147483647 - 1: return 32 + return 64 + if value <= 255: return 8 + if value <= 65535: return 16 + if value <= 2147483647: return 32 + # 2^31..2^32-1:无符号 32 位位型即可容纳 → 32 位。 + # 用 (value >> 32) == 0 判断,**不要**写成 uint64(uint32(value)) == value:uint32(...) 之后的 + # 加宽方向(zext/sext)由 should_zext_widen 按节点无符号性决定,TPC 与 VPC 自举的判定可能不一致; + # 一旦判成 sext,uint64(0x80000000) 会变成 0xFFFFFFFF80000000 ≠ value → 误判 64 位 → + # `GENERIC_READ | GENERIC_WRITE` 被放大到 i64 → 赋给 32 位目标报隐式窄化。 + if (value >> 32) == 0: return 32 + return 64 + # _is_static_int_expr - 该节点是否属于通用求值器一定能折叠的整型表达式(只判定,不求值) def _binop_is_foldable(self, op: int) -> int: """1=通用求值器支持的整型二元运算(与 extract_cdefine_int_value_bits 的 BinOp 分支一致)""" @@ -280,2917 +280,2917 @@ novirtual class AssignGenMixin: v: int64 = 0 if self._static_int_of(count_node, &v) == 0: return if v < 0: - self.Gen.TypeGenErr.fatal_error(node, f"[TPV] shift count is negative ({v}): shifting by a negative amount is undefined. " "Fix: use a non-negative count.", "ValueError") + self.Gen.TypeGenErr.fatal_error(node, f"[VPC] shift count is negative ({v}): shifting by a negative amount is undefined. " "Fix: use a non-negative count.", "ValueError") if uint64(v) >= uint64(lhs_bits): - self.Gen.TypeGenErr.fatal_error(node, f"[TPV] shift count out of range: the count {v} is >= the left operand width {lhs_bits} bits; " f"the result would be 0 or undefined (not a value the compiler may guess). " f"Fix: use a count < {lhs_bits}, or widen the left operand.", "ValueError") + self.Gen.TypeGenErr.fatal_error(node, f"[VPC] shift count out of range: the count {v} is >= the left operand width {lhs_bits} bits; " f"the result would be 0 or undefined (not a value the compiler may guess). " f"Fix: use a count < {lhs_bits}, or widen the left operand.", "ValueError") - def _is_cdefine_marker_node(self, node: ast.AST*) -> int: - """Return 1 if the node is a t.CDefine marker node.""" - if node is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(node) - if at.attr is not None and string.strcmp(at.attr, "CDefine") == 0: return 1 - return 0 - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm.id is not None and string.strcmp(nm.id, "CDefine") == 0: return 1 - return 0 - - def _define_explicit_annot(self, annot: ast.AST*) -> ast.AST*: - """Return the explicit type T of a `T | t.CDefine` annotation, or None.""" - if annot is None: return None - if annot.kind() != ast.ASTKind.BinOp: return None - bop: ast.BinOp* = ast.BinOp*(annot) - if bop.left is None: return None - if self._is_cdefine_marker_node(bop.left) != 0: return None - return bop.left - - def _annot_int_bits(self, pool: memhub.MemBuddy*, annot: ast.AST*) -> int: - """Return the bit width of an integer annotation, or 0 if unknown.""" - if annot is None: return 0 - k: int = annot.kind() - nm: str = None - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(annot) - nm = at.attr - elif k == ast.ASTKind.Name: - n2: ast.Name* = ast.Name*(annot) - nm = n2.id - if nm is None: return 0 - return self._int_bits_of_type_name(pool, nm, 0) - - def _int_bits_of_type_name(self, pool: memhub.MemBuddy*, nm: str, depth: int) -> int: - """类型名 → 整数位宽(0=未知/非整数)。depth 防 typedef 环。 - - 覆盖:内建名(int/bool/int8../uint64/sizet)、`t.CXxx`(map_t_type)、 - 以及**用户 typedef**(如 stdint 的 `WORD: t.CTypedef = uint16` —— 经 lookup_ctypedef - 取其底层类型文本再递归定宽)。此前用户 typedef 一律返回 0,导致 - `define X: WORD = ...` 的声明宽度不参与校验/物化。 - """ - if nm is None or depth > 10: return 0 - # 去掉模块/类限定前缀:`t.CUInt16T` → `CUInt16T`;`stdint.uint16` → `uint16` - dot: str = string.strrchr(nm, 46) - if dot is not None and dot[1] != '\0': - return self._int_bits_of_type_name(pool, dot + 1, depth + 1) - if string.strcmp(nm, "int") == 0: return 32 - if string.strcmp(nm, "bool") == 0: return 8 - if string.strcmp(nm, "int8") == 0 or string.strcmp(nm, "uint8") == 0: return 8 - if string.strcmp(nm, "int16") == 0 or string.strcmp(nm, "uint16") == 0: return 16 - if string.strcmp(nm, "int32") == 0 or string.strcmp(nm, "uint32") == 0: return 32 - if string.strcmp(nm, "int64") == 0 or string.strcmp(nm, "uint64") == 0: return 64 - if string.strcmp(nm, "sizet") == 0 or string.strcmp(nm, "size_t") == 0: return 64 - ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.map_t_type(pool, nm) - if ty is not None: - match ty: - case mlirlite.LLVMType.Int(bits): - if bits == 1: return 8 - if bits == 8 or bits == 16 or bits == 32 or bits == 64: return bits - return 0 - case _: - return 0 - # 用户/include typedef(如 stdint.vp 的 `WORD: t.CTypedef = uint16`): - # 表内值可能是模块限定 ref(stdint.uint16),先用 _expand_typedef_ref 展开; - # 本地表未命中再走 builtins 源扫描兜底。 - underlying: str = self.Gen.TypeGenCType.lookup_ctypedef(nm) - if underlying is None: - underlying = self.Gen.TypeGenResolve._lookup_builtins_typedef(pool, nm) - if underlying is not None: - expanded: str = self.Gen.TypeGenResolve._expand_typedef_ref(pool, underlying) - if expanded is not None: underlying = expanded - if underlying is None: return 0 - if string.strcmp(underlying, nm) == 0: return 0 - return self._int_bits_of_type_name(pool, underlying, depth + 1) - - def _define_annot_bits(self, pool: memhub.MemBuddy*, annot: ast.AST*) -> int: - """Return the bit width of a CDefine declaration's explicit type T.""" - return self._annot_int_bits(pool, self._define_explicit_annot(annot)) - - def _cdefine_int_literal_text(self, name: str) -> str: - """Return the raw value text of a CDefine line, or None if not found.""" - if name is None: return None - fp: str = self.Gen.TypeGenResolve.get_current_file() - if fp is None: return None - f: fileio.File* = fileio.File(fp, fileio.MODE.R) - if f.closed: return None - BUF_SZ: sizet = 1048576 - buf: bytes = stdlib.malloc(BUF_SZ) - if buf is None: - f.close() - return None - nrd: LONG = f.read_all(buf, BUF_SZ) - f.close() - if nrd <= 0: - stdlib.free(buf) - return None - if nrd < BUF_SZ: buf[nrd] = 0 - name_len: sizet = string.strlen(name) - total_len: sizet = string.strlen(buf) - pos: sizet = 0 - result: str = None - while pos < total_len: - line_start: sizet = pos - while pos < total_len and buf[pos] != '\n': pos += 1 - line_len: sizet = pos - line_start - if pos < total_len: pos += 1 - if line_len < name_len + 1: continue - ok: int = 1 - ki: sizet - for ki in range(int(name_len)): - if buf[line_start + ki] != name[ki]: - ok = 0 - break - if ok == 0: continue - sep: sizet = line_start + name_len - while sep < line_start + line_len and buf[sep] == ' ': sep += 1 - if sep >= line_start + line_len: continue - if buf[sep] != ':' and buf[sep] != '=': continue - eq: sizet = line_start - while eq < line_start + line_len and buf[eq] != '=': eq += 1 - if eq >= line_start + line_len: continue - eq += 1 - while eq < line_start + line_len and buf[eq] == ' ': eq += 1 - vstart: sizet = eq - while eq < line_start + line_len and buf[eq] != ' ' and buf[eq] != '\t' and buf[eq] != '#' and buf[eq] != '\r': eq += 1 - if eq == vstart: continue - txt: str = stdlib.malloc(eq - vstart + 1) - if txt is None: continue - vi: sizet - for vi in range(int(eq - vstart)): txt[vi] = buf[vstart + vi] - txt[eq - vstart] = 0 - result = txt - break - stdlib.free(buf) - return result - - def _int_text_exceeds_64(self, text: str) -> int: - """Return 1 if the literal text exceeds the 64-bit range, else 0.""" - if text is None: return 0 - tl: sizet = string.strlen(text) - if tl == 0: return 0 - neg: int = 0 - i: sizet = 0 - if text[0] == '-': - neg = 1 - i = 1 - elif text[0] == '+': i = 1 - if i >= tl: return 0 - if i + 1 < tl and text[i] == '0' and (text[i + 1] == 'x' or text[i + 1] == 'X'): - # Hex: significant digits after removing '_' and leading zeros (>16 => overflow) - sig: sizet = 0 - j2: sizet = i + 2 - leading: int = 1 - while j2 < tl: - c2: char = text[j2] - j2 += 1 - if c2 == '_': continue - if not ((c2 >= '0' and c2 <= '9') or (c2 >= 'a' and c2 <= 'f') or (c2 >= 'A' and c2 <= 'F')): - return 0 - if leading != 0 and c2 == '0': continue - leading = 0 - sig += 1 - if sig > 16: return 1 - return 0 - # Decimal: locate the first significant digit - s: sizet = i - while s < tl and (text[s] == '0' or text[s] == '_'): s += 1 - digits2: int = 0 - j3: sizet = s - while j3 < tl: - c3: char = text[j3] - j3 += 1 - if c3 == '_': continue - if c3 < '0' or c3 > '9': return 0 - digits2 += 1 - if digits2 == 0: return 0 - max_text: str = "18446744073709551615" - mlen: sizet = 20 - if neg != 0: - max_text = "9223372036854775808" - mlen = 19 - if digits2 > mlen: return 1 - if digits2 < mlen: return 0 - # Equal length: lexicographic digit compare (same as numeric order) - d: sizet = 0 - k2: sizet = s - while k2 < tl and d < mlen: - c4: char = text[k2] - k2 += 1 - if c4 == '_': continue - mc: char = max_text[d] - if c4 > mc: return 1 - if c4 < mc: return 0 - d += 1 - return 0 - - def require_cdefine_storable(self, pool: memhub.MemBuddy*, node: ast.AST*, name: str, value: int64, annot: ast.AST*, rhs_bits: int = 0) -> None: - """Fail fast if a CDefine value is not storable in its declared type. - - rhs_bits: 右侧表达式自身的位宽(0=未定宽)。右侧带显式强转(如 WORD(...))且不宽于声明注解时, - 截断是**显式**的(左侧只看到截断后的数据),此时不再按"存不下"报错——这正是 - "注解存不下且没有强转截断的需要报错"里被豁免的那一半。 - """ - text: str = self._cdefine_int_literal_text(name) - bits: int = self._define_annot_bits(pool, annot) - wmin: int = self.define_min_width(value) - if self._int_text_exceeds_64(text) != 0: - self.Gen.TypeGenErr.fatal_error(node, f"[TPV] define constant value not storable: the initial value {text} of constant '{name}' exceeds 64 bits " f"(the widest integer type in this language is 64 bits; the storable range under both signed and unsigned interpretations is " f"-9223372036854775808..18446744073709551615; no wider integer type exists => widening is required). " f"Silent truncation/downgrade is forbidden; compilation aborted. Available integer types: int8/uint8, " f"int16/uint16, int32/uint32, int64/uint64 " f"(or int8/uint8/.../uint64, char, sizet). " f"Fix: move the value into the 64-bit range, or introduce a larger integer type for this number (widen).", "TypeError") - return - if bits == 0: return - # Explicit truncation exemption: the RHS narrowed itself to rhs_bits (a cast such as WORD(...)), - # so the value the declaration stores is already the truncated one. - if rhs_bits != 0 and rhs_bits <= bits: return - # Build the 32-bit bounds from overflow-safe literals (see define_min_width) - m31: int64 = 2147483647 - lo: int64 = -m31 - 1 - hi: int64 = m31 * 2 + 1 - if bits == 8: - lo = -128 - hi = 255 - elif bits == 16: - lo = -32768 - hi = 65535 - elif bits == 64: return - if value < lo or value > hi: - self.Gen.TypeGenErr.fatal_error(node, f"[TPV] define constant value not storable: the initial value {value} of constant '{name}' is not storable. " f"Minimal storable width W_min: {wmin} bits; declared type width {bits} bits, " f"storable range of this declaration: {lo}..{hi}. " f"Silent truncation/downgrade is forbidden; compilation aborted. " f"Fix: widen the explicit type to a wider type (such as int32 / int64 / uint64), " f"or correct the literal.", "TypeError") - - def require_cdefine_use_width(self, pool: memhub.MemBuddy*, trans: HT.Translator*, target_annot: ast.AST*, value_node: ast.AST*) -> None: - """Fail fast when a define constant is wider than its use-site type. - - 比较基准是 define 的**声明位宽**(注解 → 右侧强转 → 值的最小宽度,见 cdefine_decl_bits), - 不是值本身的最小宽度:define 声明成 64 位,写进 uint8 位置就是隐式窄化,必须报错。 - """ - if value_node is None or target_annot is None: return - if value_node.kind() != ast.ASTKind.Name: return - rn: ast.Name* = ast.Name*(value_node) - if rn.id is None: return - if trans is not None: - if self.Gen.VarGen.lookup_var(trans.SymTab, rn.id) is not None: return - cdef_val: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(rn.id) - if self.Gen.TypeGenCDef.is_cdefine_found() == 0: return - bits: int = self._annot_int_bits(pool, target_annot) - if bits == 0: return - # 声明位宽优先;未登记位宽(0)时才回退到推断位宽(值的最小可存宽度) - dw: int = self.Gen.TypeGenCDef.lookup_cdefine_bits(rn.id) - if dw == 0: dw = self.define_min_width(cdef_val) - if bits >= dw: return - self.Gen.TypeGenErr.fatal_error(value_node, f"[TPV] define constant use-site width insufficient: constant '{rn.id}' is {dw} bits wide " f"(declared width; actual value {cdef_val}), but the use site (variable declaration annotation) provides only {bits} bits. " f"Silent truncation/downgrade is forbidden; compilation aborted. " f"Fix: widen the variable declaration annotation to at least {dw} bits (such as int{dw}T / CUInt{dw}T), " f"or narrow the constant at its define with an explicit cast.", "TypeError") - - # _register_cdefine_from_value - register a CDefine constant (float or int) - def _register_cdefine_from_value(self, pool: memhub.MemBuddy*, name: str, val_node: ast.AST*, annot: ast.AST* = None) -> None: - """Register a CDefine constant (double for floats, else integer).""" - if pool is None or name is None: return - # Call-wrapped constant (e.g. float(3.14)): use its first argument as the value - if val_node is not None and val_node.kind() == ast.ASTKind.Call: - cl_cd: ast.Call* = ast.Call*(val_node) - if cl_cd is not None and cl_cd.args is not None: - cl_args_cd: list[ast.AST*]* = cl_cd.args - if len(cl_args_cd) > 0: - first_arg_cd: ast.AST* = cl_args_cd.get(0) - if first_arg_cd is not None and first_arg_cd.kind() == ast.ASTKind.Constant: val_node = first_arg_cd - # Negative float literal: unwrap UnaryOp(USub, Constant(float)); value recovered from source - if val_node is not None and val_node.kind() == ast.ASTKind.UnaryOp: - uu_cd: ast.UnaryOp* = ast.UnaryOp*(val_node) - if uu_cd is not None and uu_cd.op == ast.OpKind.USub and uu_cd.operand is not None and uu_cd.operand.kind() == ast.ASTKind.Constant: - ic_cd: ast.Constant* = ast.Constant*(uu_cd.operand) - if ic_cd is not None and ic_cd.const_kind == ast.CONST_FLOAT: val_node = uu_cd.operand - if val_node is not None and val_node.kind() == ast.ASTKind.Constant: - cn_fp: ast.Constant* = ast.Constant*(val_node) - if cn_fp.const_kind == ast.CONST_STR: - # String CDefine: register as a string constant (reader returns a global string) - self.Gen.TypeGenCDef.register_cdefine_string(pool, name, cn_fp.str_val) - return - if cn_fp.const_kind == ast.CONST_FLOAT: - # Recover the float value from the source file (AST float_val may be overwritten) - use_val: double = cn_fp.float_val - rec_d: double = 0.0 - if self._recover_cdefine_float(name, &rec_d) != 0: use_val = rec_d - else: - # Source recovery failed: fail fast to avoid registering a corrupted float - self.Gen.TypeGenErr.fatal_error(val_node, "CDefine float value source-file recovery failed", "InternalCompilerError") - return - self.Gen.TypeGenCDef.register_cdefine_double(pool, name, use_val) - return - if val_node is None: - # CDefine declaration without a value node: fail fast - self.Gen.TypeGenErr.fatal_error(None, f"CDefine value extraction failed: '{name}' is missing an initial value", "TypeError") - return - # 位宽裁决:与跨模块使用方共用 cdefine_decl_bits(注解 → 右侧显式强转 → 值的最小可存宽度)。 - # 有注解而存不下时:右侧带显式强转截断则不报错(左侧只看到截断值),否则由 - # require_cdefine_storable 报"存不下"。 - rhs_bits: int = 0 - val_fp: int64 = self.extract_cdefine_int_value_bits(val_node, pool, &rhs_bits) - vb: int = self.cdefine_decl_bits(pool, annot, val_node) - # Check storability before registering (source text >64-bit or explicit T too narrow => fail) - self.require_cdefine_storable(pool, val_node, name, val_fp, annot, rhs_bits) - self.Gen.TypeGenCDef.register_cdefine_constant(pool, name, val_fp, vb) - - # Recover a CDefine float value by re-reading its source line with atof - - def _recover_found(self) -> int: - """Return whether the last _recover_cdefine_float succeeded.""" - return self.RecoverFound - - def _recover_float_literal(self, name: str, out: double*, require_cdefine: int) -> int: - """Recover a float literal from the module source into out[0]; 1 on success.""" - self.RecoverFound = 0 - fp: str = self.Gen.TypeGenResolve.get_current_file() - if fp is None or name is None or out is None: return 0 - f: fileio.File* = fileio.File(fp, fileio.MODE.R) - if f.closed: return 0 - BUF_SZ: sizet = 1048576 - buf: bytes = stdlib.malloc(BUF_SZ) - if buf is None: - f.close() - return 0 - nrd: LONG = f.read_all(buf, BUF_SZ) - f.close() - if nrd <= 0: - stdlib.free(buf) - return 0 - if nrd < BUF_SZ: buf[nrd] = 0 - - name_len: sizet = string.strlen(name) - total_len: sizet = string.strlen(buf) - pos: sizet = 0 - while pos < total_len: - line_start: sizet = pos - while pos < total_len and buf[pos] != '\n': pos += 1 - line_len: sizet = pos - line_start - if pos < total_len: pos += 1 - - # define NAME: T = V form: the name is preceded by the define keyword - ns: sizet = line_start - is_define_line: int = 0 - if line_len >= 8: - if buf[ns] == 'd' and buf[ns + 1] == 'e' and buf[ns + 2] == 'f' and buf[ns + 3] == 'i' and buf[ns + 4] == 'n' and buf[ns + 5] == 'e': - nb: sizet = ns + 6 - while nb < line_start + line_len and (buf[nb] == ' ' or buf[nb] == '\t'): nb += 1 - # Keyword only when followed by whitespace (avoid matching defineX) - if nb > ns + 6: - is_define_line = 1 - ns = nb - # Match the name at line start (or after define); it must be followed by ':' or '=' - if ns + name_len + 1 > line_start + line_len: continue - match_ok: int = 1 - ki: sizet - for ki in range(int(name_len)): - if buf[ns + ki] != name[ki]: - match_ok = 0 - break - if match_ok == 0: continue - sep: sizet = ns + name_len - while sep < line_start + line_len and buf[sep] == ' ': sep += 1 - if sep >= line_start + line_len: continue - if buf[sep] != ':' and buf[sep] != '=': continue - - # CDefine path requires t.CDefine in the line, or the define keyword form - if require_cdefine != 0 and is_define_line == 0: - has_cd: int = 0 - ci: sizet = line_start + name_len + 1 - while ci + 8 <= line_start + line_len: - if (buf[ci] == 't' and buf[ci + 1] == '.' and buf[ci + 2] == 'C' and buf[ci + 3] == 'D' and buf[ci + 4] == 'e' and buf[ci + 5] == 'f' and buf[ci + 6] == 'i' and buf[ci + 7] == 'n' and buf[ci + 8] == 'e'): - has_cd = 1 - break - ci += 1 - if has_cd == 0: continue - - # Find '=' - eq: sizet = line_start - while eq < line_start + line_len and buf[eq] != '=': eq += 1 - if eq >= line_start + line_len: continue - eq += 1 - while eq < line_start + line_len and buf[eq] == ' ': eq += 1 - # Call-wrapped float constant: skip the float( prefix and start at the first numeric char - if eq < line_start + line_len: - c0_rf: char = buf[eq] - if not ((c0_rf >= '0' and c0_rf <= '9') or c0_rf == '-' or c0_rf == '+' or c0_rf == '.'): - pfx_rf: sizet = eq - while pfx_rf < line_start + line_len: - cp_rf: char = buf[pfx_rf] - if (cp_rf >= '0' and cp_rf <= '9') or cp_rf == '.' or cp_rf == '-' or cp_rf == '+': - eq = pfx_rf - break - pfx_rf += 1 - val_start: sizet = eq - while eq < line_start + line_len and buf[eq] != ' ' and buf[eq] != '\t' and buf[eq] != '#' and buf[eq] != '\r' and buf[eq] != '\n': eq += 1 - if eq == val_start: continue - - val_txt: str = stdlib.malloc(eq - val_start + 1) - if val_txt is None: - stdlib.free(buf) - return 0 - vi: sizet - for vi in range(int(eq - val_start)): val_txt[vi] = buf[val_start + vi] - val_txt[eq - val_start] = 0 - stdlib.free(buf) - # Use atof_out (an out param) to avoid the cross-module double return bug - if string.atof_out(val_txt, out) != 0: - self.RecoverFound = 1 - return 1 - return 0 - - stdlib.free(buf) - return 0 - - def _recover_cdefine_float(self, name: str, out: double*) -> int: - """Recover a CDefine float value (requiring a t.CDefine line).""" - return self._recover_float_literal(name, out, 1) - - # _annotation_contains_name - recursively check whether an annotation contains a name - def _annotation_contains_name(self, annot: ast.AST*, name: str) -> int: - """Return 1 if the annotation recursively contains the given name.""" - if annot is None or name is None: return 0 - k: int = annot.kind() - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(annot) - if at.attr is not None and string.strcmp(at.attr, name) == 0: return 1 - return 0 - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(annot) - if nm.id is not None and string.strcmp(nm.id, name) == 0: return 1 - return 0 - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(annot) - if self._annotation_contains_name(bop.left, name) != 0: return 1 - if self._annotation_contains_name(bop.right, name) != 0: return 1 - return 0 - - def _annotation_has_cptr_marker(self, annot: ast.AST*) -> int: - """Return 1 if the annotation union carries a CPtr marker (the star form ` | t.CPtr`).""" - if annot is None: return 0 - k: int = annot.kind() - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(annot) - if at.attr is not None and string.strcmp(at.attr, "CPtr") == 0: return 1 - return 0 - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(annot) - if nm.id is not None and string.strcmp(nm.id, "CPtr") == 0: return 1 - return 0 - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(annot) - if self._annotation_has_cptr_marker(bop.left) != 0: return 1 - if self._annotation_has_cptr_marker(bop.right) != 0: return 1 - return 0 - - # _is_ptr_element_annotation - check whether an annotation is bytes* or str* - # bytes*/str* => 8-byte element stride (i8**); char*/int8* => 1-byte stride. - def _is_ptr_element_annotation(self, annot: ast.AST*) -> int: - """Return 1 if the annotation is a bytes* or str* form.""" - if annot is None: return 0 - k: int = annot.kind() - if k != ast.ASTKind.BinOp: return 0 - bop: ast.BinOp* = ast.BinOp*(annot) - if bop.op != ast.OpKind.BitOr: return 0 - # str/bytes are already i8*; only the union WITH a pointer marker (str*/bytes*) is i8** and - # needs 8-byte element slots. A union that merely carries them (e.g. `const str`, which is - # t.CConst | str and stays i8*) must keep the 1-byte stride. - if self._annotation_has_cptr_marker(annot) != 0: - if self._annotation_contains_name(annot, "str") != 0: return 1 - if self._annotation_contains_name(annot, "bytes") != 0: return 1 - # Generic type param / nested union | t.CPtr: scalars use their own size, pointers use 8-byte slots - if self._is_generic_tp_or_nested(bop.left) != 0 or self._is_generic_tp_or_nested(bop.right) != 0: - gp_nm: str = None - if bop.left is not None and bop.left.kind() == ast.ASTKind.Name: - gp_left_nm: ast.Name* = ast.Name*(bop.left) - gp_nm = gp_left_nm.id - elif bop.right is not None and bop.right.kind() == ast.ASTKind.Name: - gp_right_nm: ast.Name* = ast.Name*(bop.right) - gp_nm = gp_right_nm.id - if gp_nm is not None: - # Resolve the generic param via the global gen (direct field access breaks static binding) - gp_resolved: str = self.Gen.TypeGenBase.resolve_generic_type_name(gp_nm) - if gp_resolved is not None and string.strcmp(gp_resolved, gp_nm) != 0: - # Generic param resolved to a scalar => use scalar size, not an 8-byte pointer slot - if self._is_scalar_ty_name(gp_resolved) != 0: return 0 - return 1 - # 64-bit scalar | CPtr: element is 8 bytes, so use an 8-byte stride (else GEP truncation) - if self._is_64bit_scalar_ty(bop.left) != 0 or self._is_64bit_scalar_ty(bop.right) != 0: return 1 - return 0 - - def _stg_ty_equal(self, pool: memhub.MemBuddy*, ta: mlirlite.LLVMType*, tb: mlirlite.LLVMType*) -> int: - """Return 1 if the two types are equal by printed content.""" - if ta is None or tb is None: return 0 - # Fast path: identical instance is equal - if uint64(ta) == uint64(tb): return 1 - buf_a: str = pool.alloc_tagged(4096, memhub.DETSITE_ASSIGNGEN_578) - buf_b: str = pool.alloc_tagged(4096, memhub.DETSITE_ASSIGNGEN_579) - if buf_a is None or buf_b is None: return 0 - mlirlite.TypePrint(buf_a, 4096, ta, pool) - mlirlite.TypePrint(buf_b, 4096, tb, pool) - if buf_a[0] == 0 or buf_b[0] == 0: return 0 - return 1 if string.strcmp(buf_a, buf_b) == 0 else 0 - - # ============================================================ - # 生命期(LifeTag)判定 / 传播 / 汇点检查(架构见 Debugger/ownership-and-new.md §11) - # 标签常量见 GenTypes define LIFE_*;有序,越小越"短命",汇合取 min。 - # ============================================================ - - # ── A2(§14)`@life(ret(...))`:调用点解析被调者的返回值生命期契约 ── - # _RecvClassName - 从方法的接收者表达式取它静态类型指向的类名(去 sha1 前缀);判不定返回 None。 - # 只认「名字变量」接收者:变量表里查不到(如 `m.f()` 里的模块名 `m`)返回 None —— 调用方据此 - # 把"模块限定函数调用"与"方法调用"分开(二者在 AST 里都是 Attribute)。 - def _RecvClassName(self, trans: HT.Translator*, recv: ast.AST*) -> str: - """接收者静态类型 → 类名(无 sha1 前缀);判不定返回 None""" - if trans is None or recv is None: return None - if recv.kind() != ast.ASTKind.Name: return None - rn: ast.Name* = ast.Name*(recv) - if rn.id is None: return None - v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn.id) - if v is None: return None - ty: mlirlite.LLVMType* = v.Ty # 变量槽类型 = <变量类型>* - if ty is None or self.Gen.ExprGenBase.is_ptr_type(ty) == 0: return None - p1: mlirlite.LLVMType* = ty.Pointee # 变量类型本身 - if p1 is None: return None - # 类变量通常是指针(`S*`);值语义的类变量(`s: S`,罕见)直接取本类型。 - elem: mlirlite.LLVMType* = p1.Pointee - if self.Gen.ExprGenBase.is_ptr_type(p1) == 0: elem = p1 - sname: str = mlirlite.get_struct_name(elem) - if sname is None: return None - pl: int = GenApi.sha1_prefix_len(sname) - if pl > 0: sname = sname + pl - return sname - - # _CallArgNodeForParam - `@life(ret(borrows=<形参 pi>))` 在调用点对应的实参节点。 - # **方法**调用的形参表含 self(pi=0 即接收者),故 is_method=1 时偏移一格; - # **模块限定函数**(`m.f(...)`:callee 也是 Attribute,但不是方法)形参 0 就是第一个实参, - # 不能偏移 —— 否则 pi=0 会被解析成"模块名"这个非实参节点(现场:本函数把 - # `retdep2.IdentBorrow(l)` 的形参 0 取成了 `retdep2` → life=STATIC → 跨模块 ret 契约静默失效)。 - def _CallArgNodeForParam(self, cl: ast.Call*, pi: int, is_method: int) -> ast.AST*: - """第 pi 号形参对应的实参表达式;越界/判不定返回 None""" - if cl is None or pi < 0: return None - cargs: list[ast.AST*]* = cl.args - if is_method != 0: - if pi == 0: - at: ast.Attribute* = ast.Attribute*(cl.func) - return at.value - if cargs is None or sizet(pi - 1) >= len(cargs): return None - return cargs.get(sizet(pi - 1)) - if cargs is None or sizet(pi) >= len(cargs): return None - return cargs.get(sizet(pi)) - - # _CallIsMethod - 该调用是否"接收者是对象变量"的方法调用(决定形参表是否含 self)。 - def _CallIsMethod(self, trans: HT.Translator*, cl: ast.Call*) -> int: - """1 = 方法调用(形参表含 self);0 = 模块限定函数 / 裸函数""" - if cl is None or cl.func is None: return 0 - if cl.func.kind() != ast.ASTKind.Attribute: return 0 - at: ast.Attribute* = ast.Attribute*(cl.func) - if self._RecvClassName(trans, at.value) is None: return 0 - return 1 - - # _CallRetKind - 解析被调者的 `@life(ret(...))` 契约(本文件表 → 跨模块 .vpi);-1 = 无声明/判不定。 - def _CallRetKind(self, trans: HT.Translator*, cl: ast.Call*) -> int: - """被调者 ret_kind 编码(GenApi.RET_KIND_*);-1 = 无声明/判不定""" - if cl is None or cl.func is None: return -1 - fnm: str = self.Gen.ExprGenBase.get_func_name(cl.func) - if fnm is None: return -1 - pool: memhub.MemBuddy* = None - if trans is not None: pool = trans.Pool - if pool is None: return -1 - # 方法调用:接收者是本帧变量 → 取它静态类型指向的类名;接收者是模块名 → 不当方法。 - if cl.func.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(cl.func) - cls: str = self._RecvClassName(trans, at.value) - if cls is not None: - rk_m: int = GenApi.get_ret_kind_method(pool, cls, fnm) - if rk_m != 0: return rk_m - sha_m: str = self.Gen.StructGen.get_struct_sha1(cls) - if sha_m is not None: - rk_m = self.Gen.CallGenBase.lookup_ret_kind_via_vpi(sha_m, cls, fnm) - if rk_m != 0: return rk_m - return -1 - # 模块级函数(裸名,或模块限定 `m.f()`):本文件表优先,再跨模块 .vpi 回退。 - rk: int = GenApi.get_ret_kind(fnm) - if rk != 0: return rk - mod_sha: str = None - if cl.func.kind() == ast.ASTKind.Attribute: - at2: ast.Attribute* = ast.Attribute*(cl.func) - # 只认裸模块名接收者(`m.f()`);`self.h.f()` 之类链式接收者判不定 → 不猜。 - if at2.value is None or at2.value.kind() != ast.ASTKind.Name: return -1 - mn2: ast.Name* = ast.Name*(at2.value) - mod_p: str = None - if mn2.id is not None: mod_p = mn2.id - if mod_p is not None: - fi2: str = None - if trans is not None: fi2 = trans._from_imports - mod_sha = self.Gen.StructGen._resolve_module_sha1(mod_p, fi2) - # 接收者既不是模块(解析不出 sha1)也不是变量(否则上面早按方法走了)→ 判不定。 - if mod_sha is None: return -1 - if mod_sha is None and trans is not None and trans._from_imports is not None: - mod_sha = self.Gen.CallGenBase._lookup_mod_sha1(pool, trans._from_imports, fnm, 0) - if mod_sha is not None: - rk = self.Gen.CallGenBase.lookup_ret_kind_via_vpi(mod_sha, None, fnm) - if rk != 0: return rk - return -1 - - # life_of_expr - 求表达式当前值的生命期标签。 - # target_ty 参与判定:裸本帧数组名在**指针**目标位置 = 数组退让(交出本帧栈地址), - # 在同型聚合目标位置 = 按值拷贝(LIFE_VALUE)。 - def life_of_expr(self, trans: HT.Translator*, node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: - """表达式的 LifeTag;无法判定时 LIFE_UNKNOWN(保守放行)""" - if node is None or trans is None: return GenTypes.LIFE_VALUE - k: int = node.kind() - if k == ast.ASTKind.Constant: - cn: ast.Constant* = ast.Constant*(node) - if cn is None: return GenTypes.LIFE_VALUE - if cn.const_kind == ast.CONST_STR: return GenTypes.LIFE_STATIC - if cn.const_kind == ast.CONST_NONE: return GenTypes.LIFE_UNKNOWN - return GenTypes.LIFE_VALUE - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm.id is None: return GenTypes.LIFE_UNKNOWN - if GenApi.is_global_name(trans, nm.id) != 0: return GenTypes.LIFE_STATIC - if GenApi.is_nonlocal_name(trans, nm.id) != 0: return GenTypes.LIFE_UNKNOWN - v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) - if v is None: return GenTypes.LIFE_STATIC - if v.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(v.Ty) != 0: - inner: mlirlite.LLVMType* = v.Ty.Pointee - if inner is not None and inner.kind() == mlirlite.LLVMType.Array: - # 本帧局部数组名:退让到指针目标交出栈地址;同型聚合目标是值拷贝 - if target_ty is not None and self.Gen.ExprGenBase.is_ptr_type(target_ty) != 0: return GenTypes.LIFE_FRAME - return GenTypes.LIFE_VALUE - # 指针/标量变量:读登记的生命期(默认 UNKNOWN) - return self.Gen.VarGen.get_var_life(trans.SymTab, nm.id) - if k == ast.ASTKind.UnaryOp: - uo: ast.UnaryOp* = ast.UnaryOp*(node) - if uo.op == ast.OpKind.AddrOf and uo.operand is not None and uo.operand.kind() == ast.ASTKind.Name: - onm: ast.Name* = ast.Name*(uo.operand) - if onm.id is not None: - if GenApi.is_global_name(trans, onm.id) != 0: return GenTypes.LIFE_STATIC - if GenApi.is_nonlocal_name(trans, onm.id) != 0: return GenTypes.LIFE_UNKNOWN - if self.Gen.VarGen.lookup_var(trans.SymTab, onm.id) is not None: return GenTypes.LIFE_FRAME - return GenTypes.LIFE_STATIC - return GenTypes.LIFE_UNKNOWN - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - # §14 A2:`@life(ret(...))` 声明**优先于任何推断**(无条件)。 - rk: int = self._CallRetKind(trans, cl) - if rk >= 0: - if rk == GenApi.RET_KIND_FRAME: return GenTypes.LIFE_FRAME - if rk == GenApi.RET_KIND_HEAP: return GenTypes.LIFE_HEAP - if rk == GenApi.RET_KIND_STATIC: return GenTypes.LIFE_STATIC - if rk == GenApi.RET_KIND_NONFRAME: return GenTypes.LIFE_UNKNOWN - if rk >= GenApi.RET_KIND_BORROW_BASE: - pi: int = rk - GenApi.RET_KIND_BORROW_BASE - # 形参位 → 实参位:**方法**调用的形参表含 self(pi=0 = 接收者); - # 模块限定函数(`m.f(...)`)不含,pi 直接就是实参下标。 - bn: ast.AST* = self._CallArgNodeForParam(cl, pi, self._CallIsMethod(trans, cl)) - if bn is None: return GenTypes.LIFE_UNKNOWN - return self.life_of_expr(trans, bn, None) - # 分配类调用 → 堆;其余调用 → 未知(函数摘要落地前保守放行) - if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(cl.func) - if at.attr is not None: - if (string.strcmp(at.attr, "alloc") == 0 or string.strcmp(at.attr, "alloc_buf") == 0 or string.strcmp(at.attr, "calloc") == 0 or string.strcmp(at.attr, "realloc") == 0): return GenTypes.LIFE_HEAP - if cl.func is not None and cl.func.kind() == ast.ASTKind.Name: - fnm: ast.Name* = ast.Name*(cl.func) - if fnm.id is not None and string.strcmp(fnm.id, "malloc") == 0: return GenTypes.LIFE_HEAP - # 裸 `T(...)` 构造:**没走 `new`、且该类没有 `__new__`** 时,存储就是入口块的 alloca - # (CallGenCtor._translate_struct_ctor:`tmp = _alloca_at_entry(...)`,只有 __new__ 才会 - # 改写存储指针)→ 帧生命期。这是"写进本帧对象字段不算逃逸"(调用点同命判定)成立的前提。 - # 有 `__new__` 的类仍返回未知(存储由 __new__ 决定,可能是池/堆)。 - ctor_nm: str = self.Gen.ExprGenBase.get_func_name(cl.func) - if ctor_nm is not None: - ct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type(ctor_nm) - if ct_ty is not None: - ct_en: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(ct_ty) - if ct_en is None or ct_en.HasNew == 0: return GenTypes.LIFE_FRAME - return GenTypes.LIFE_UNKNOWN - if k == ast.ASTKind.New: - # M2.4 `new`:生命期由描述符决定(§2.3 组合矩阵) - # stack → slife:帧生命期(等价既有 `&T(...)`) - # heap/memgr slife → 同样按帧生命期:作用域结束即释放(RAII),逃逸出去必然悬垂 - # hlife(含 heap/memgr 的缺省)→ 堆/池生命期,可自由传递 - nn: ast.New* = ast.New*(node) - if nn is None: return GenTypes.LIFE_UNKNOWN - if nn.Life == ast.NEW_LIFE_SLIFE: return GenTypes.LIFE_FRAME - if nn.Life == ast.NEW_LIFE_DEFAULT and nn.Source == ast.NEW_SRC_STACK: return GenTypes.LIFE_FRAME - return GenTypes.LIFE_HEAP - return GenTypes.LIFE_UNKNOWN - - # life_of_lvalue - 求赋值目标(左值)所在存储的生命期。 - # P3 用它做"左值比右值长命"的判定:Name 目标看变量/全局;Attribute/Subscript 递归其基对象 - # (`obj.f` / `arr[i]` 的存储生命期 = obj / arr 的生命期)。 - def life_of_lvalue(self, trans: HT.Translator*, target_node: ast.AST*) -> int: - """赋值目标的 LifeTag;无法判定时 LIFE_UNKNOWN(保守放行)""" - if trans is None or target_node is None: return GenTypes.LIFE_UNKNOWN - k: int = target_node.kind() - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(target_node) - if nm.id is None: return GenTypes.LIFE_UNKNOWN - if GenApi.is_global_name(trans, nm.id) != 0: return GenTypes.LIFE_STATIC - if GenApi.is_nonlocal_name(trans, nm.id) != 0: return GenTypes.LIFE_UNKNOWN - v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) - # 局部变量槽本身的生命期就是本帧(LIFE_FRAME)。注意**不能**用 get_var_life: - # 那是"变量当前值所指向的存储"的生命期(供传播/借用判定用),语义不同—— - # 混用会把 `p = &local`(p 是本帧局部指针)误判成长命左值。 - if v is not None: return GenTypes.LIFE_FRAME - return GenTypes.LIFE_STATIC - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(target_node) - if at.value is None: return GenTypes.LIFE_UNKNOWN - return self.life_of_expr(trans, at.value, None) - if k == ast.ASTKind.Subscript: - sb: ast.Subscript* = ast.Subscript*(target_node) - if sb.value is None: return GenTypes.LIFE_UNKNOWN - return self.life_of_expr(trans, sb.value, None) - return GenTypes.LIFE_UNKNOWN - - # _fail_if_lvalue_outlives_rhs - P3 的一般规则:左值存储比右值长命时,右值不得交出更短的存储。 - # 只在"明确长命"(HEAP/STATIC)的左值上触发;RHS 判定复用 fail_if_life_escapes(FRAME → 报)。 - # 例:`obj.f = &local`(obj 指向堆对象,堆活过本帧)→ LifetimeError; - # `local_struct.f = &local2`(左值也在本帧,FRAME)→ 放行(两者同命)。 - def _fail_if_lvalue_outlives_rhs(self, trans: HT.Translator*, target_node: ast.AST*, rhs_node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: - if trans is None or target_node is None or rhs_node is None: return 0 - lv: int = self.life_of_lvalue(trans, target_node) - if lv != GenTypes.LIFE_HEAP and lv != GenTypes.LIFE_STATIC: return 0 - GenApi.fail_if_stack_addr_escape(rhs_node, "a longer-lived field/element", trans, target_ty) - return 0 - - # propagate_life - 把 RHS 的生命期写到赋值目标(仅 Name 目标;全局目标走汇点检查)。 - def propagate_life(self, trans: HT.Translator*, target_node: ast.AST*, rhs_node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: - """Propagate the RHS life tag onto a Name target; 0=done / 1=skipped""" - if trans is None or target_node is None: return 1 - if target_node.kind() != ast.ASTKind.Name: - # P3:非 Name 目标(属性/下标)——左值存储比右值长命时,右值不得是本帧栈地址 - self._fail_if_lvalue_outlives_rhs(trans, target_node, rhs_node, target_ty) - return 1 - tnm: ast.Name* = ast.Name*(target_node) - if tnm.id is None: return 1 - if GenApi.is_global_name(trans, tnm.id) != 0: return 1 - if GenApi.is_nonlocal_name(trans, tnm.id) != 0: return 1 - life: int = self.life_of_expr(trans, rhs_node, target_ty) - # §13 NLL:默认生命期的 `new heap T()` 若证明在本函数内封闭(不外泄)→ 按帧生命期处理。 - # 与 `new heap slife` 同口径(生命期 FRAME + 作用域出口释放,见 _slife_note_assign)。 - if life == GenTypes.LIFE_HEAP: - if self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, tnm.id) != 0: - life = GenTypes.LIFE_FRAME - self.Gen.VarGen.set_var_life(trans.SymTab, tnm.id, life) - return 0 - - # _CurFuncIsEntryMain - 当前正在翻译的函数是否为**入口 main**(1=是 / 0=否)。 - # 嵌套函数不算:它们的帧不是进程生命期,不能享受豁免。 - def _CurFuncIsEntryMain(self, trans: HT.Translator*) -> int: - """当前函数是否为入口 main(按 llirlite 函数名判,与 FuncGenDef 对 main 不改名同口径)""" - if trans is None or trans._cur_func is None: return 0 - nm: str = mlirlite.function_get_name(trans._cur_func) - if nm is None: return 0 - return 1 if string.strcmp(nm, "main") == 0 else 0 - - # fail_if_life_escapes - 汇点检查:被写出表达式的生命期为 LIFE_FRAME → LifetimeError。 - # 这是 GenApi.fail_if_stack_addr_escape 的唯一实现(语法判据已并入 life_of_expr)。 - # 例外(入口 main 全豁免):main 的帧生命期 = 进程生命期,其栈对象在本帧返回(= 进程退出) - # 后不可能再被使用,故 main 内不做栈地址逃逸检查(如 `argparse._mbuddy = mb`:mb 是帧内 - # 结构体)。**嵌套函数不豁免**(它们的帧不是进程生命期)。 - def fail_if_life_escapes(self, node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType*, leak_func: str = None, leak_param: int = -1) -> int: - """Fail fast (LifetimeError) when the written-out value is frame-lifetime storage""" - if node is None or trans is None: return 0 - if self._CurFuncIsEntryMain(trans) != 0: return 0 - if self.life_of_expr(trans, node, target_ty) != GenTypes.LIFE_FRAME: return 0 - # 报文里给出名字(&x / x),便于定位;前缀单独存放,避免 str 相加(str 是 i8*,无 '+' 语义) - nm_txt: str = "expression" - nm_pre: str = "" - if node.kind() == ast.ASTKind.Name: - n2: ast.Name* = ast.Name*(node) - if n2.id is not None: nm_txt = n2.id - elif node.kind() == ast.ASTKind.UnaryOp: - u2: ast.UnaryOp* = ast.UnaryOp*(node) - if u2.operand is not None and u2.operand.kind() == ast.ASTKind.Name: - n3: ast.Name* = ast.Name*(u2.operand) - if n3.id is not None: - nm_txt = n3.id - nm_pre = "&" - # 泄漏点(可选,只有"确实报错"才去算):被调者那边**造成逃逸的写入点**在同一源文件里, - # 追加一帧 "File lifetime leak ..."。"先过 life 判定再采集"是刻意的:escape_leak_line - # 会重扫被调者函数体并动 EscConstEnv/EscIndCount,不能对"其实不报错"的实参白白跑一遍。 - leak_f: str = None - leak_l: int = 0 - if leak_func is not None and leak_param >= 0: - leak_l = self.Gen.StructGen.escape_leak_line(leak_func, leak_param) - if leak_l > 0: leak_f = self.Gen.TypeGenResolve.get_current_file() - self.Gen.TypeGenErr.fatal_error(node, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local; using it at {where} hands out a dangling pointer once this frame returns. Copy the value, or allocate on the heap (`new heap hlife ...`).", "LifetimeError", 0, 0, leak_f, leak_l) - return 0 - - # check_return_frame_decl - **定义端** `return <本帧存储>` 的**声明感知**检查(A2 / §14.3 第 6 条)。 - # A2 移除了"一律拦"的旧判据:定义端分不清"交出本帧"与"借出调用方的存储"(后者是 - # `return <借用形参>`,一律拦会误报)。但也不能一律放 —— 无声明的函数返回本帧存储会让调用点 - # 永远只拿到"未知"(悬垂指针一路静默)。判据收敛到被调者自己的 `@life(ret(...))`: - # · `ret(frame)` → 放行(去向由调用点负责); - # · `ret(borrows=<该形参>)` → 放行(借出的正是那个形参); - # · 无声明 / `ret(heap|static|borrows=none)` → `LifetimeError`(提示补声明 / 改声明)。 - # 拿不到当前函数定义(非三条函数体翻译路径)时**不判**:宁可少报,也不误报。 - def check_return_frame_decl(self, trans: HT.Translator*, value: ast.AST*) -> int: - """返回值是本帧存储且当前函数的 ret 声明不允许 → LifetimeError;0=放行""" - if trans is None or value is None: return 0 - fd: ast.AST* = self.Gen.StructGen.NllCurFunc - if fd is None: return 0 - rv_ty: mlirlite.LLVMType* = None - if trans._cur_func is not None: rv_ty = mlirlite.function_get_ret_ty(trans._cur_func) - # target_ty 必须传返回类型:裸本帧数组名在**指针**返回类型下是数组退让(交出栈地址), - # 在同型聚合返回类型下是按值拷贝(LIFE_VALUE,不在此列)。 - if self.life_of_expr(trans, value, rv_ty) != GenTypes.LIFE_FRAME: return 0 - rk: int = GenApi.get_ret_kind_by_func(fd) - if rk == GenApi.RET_KIND_FRAME: return 0 - is_borrow_kind: int = 0 - pi: int = -1 - if rk >= GenApi.RET_KIND_BORROW_BASE: - is_borrow_kind = 1 - pi = rk - GenApi.RET_KIND_BORROW_BASE - # 只有"返回的正是被声明的那个形参"才算借出;返回别的本帧存储仍是悬垂 - if self.Gen.StructGen._ParamIndex(ast.FunctionDef*(fd), value) == pi: return 0 - # 报文里给出名字(&x / x),便于定位 - nm_txt: str = "expression" - nm_pre: str = "" - if value.kind() == ast.ASTKind.Name: - n2: ast.Name* = ast.Name*(value) - if n2.id is not None: nm_txt = n2.id - elif value.kind() == ast.ASTKind.UnaryOp: - u2: ast.UnaryOp* = ast.UnaryOp*(value) - if u2.operand is not None and u2.operand.kind() == ast.ASTKind.Name: - n3: ast.Name* = ast.Name*(u2.operand) - if n3.id is not None: - nm_txt = n3.id - nm_pre = "&" - if rk == 0: - self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local and this function has no `@life(ret(...))` declaration, so callers cannot know the result's lifetime. Declare `@life(ret(frame))` (the result may point into this frame; callers must keep it in-frame) or `@life(ret(borrows=))` (the result borrows a parameter), or return a value copy / a heap allocation (`new heap hlife ...`).", "LifetimeError") - return 0 - if is_borrow_kind != 0: - self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local but this function declares `@life(ret(borrows=))` (parameter #{pi}), which only permits returning that parameter. Return that parameter, or declare `@life(ret(frame))`.", "LifetimeError") - return 0 - self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local but this function is declared as non-frame (`@life(ret(heap))` / `ret(static)` / `ret(borrows=none)`); the declaration contradicts the returned value. Fix the declaration, or return a non-frame value.", "LifetimeError") - return 0 - - # _SlifeDropOwned - 该槽已被登记为 slife 绑定时交还所有权:释放它当前拥有的对象、影子清 0。 - # 用于所有"槽里不再是编译器拥有的对象"的赋值(覆盖赋值 / 别名拷贝 / 外来指针 / None)。 - # 未登记 → no-op(该槽从未被 `new heap slife` 绑定过,编译器不负责它)。 - def _SlifeDropOwned(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*) -> int: - """释放该槽当前拥有的对象并置"不拥有"(先按 §3.6 调度 `__del__`);1=处理过 / 0=未登记""" - if target_slot is None: return 0 - sh: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(target_slot) - if sh is None: return 0 - self.Gen.VarGen._SlifeEmitGuardedFree(pool, builder, mod, trans, target_slot, sh) - self.Gen.VarGen._SlifeNoteStore(pool, builder, sh, 0) - return 1 - - # _slife_note_assign - M3 赋值处的所有权记账(§4.2 RAII / §4.3 编译器独占释放)。 - # 只在**本地 Name 目标**的赋值落地处调用;目标槽 target_slot 即该变量的 alloca。 - # ① RHS = `new heap slife T(...)`:登记该槽(首次建影子标志)→ 覆盖释放旧拥有对象 → 影子置 1; - # ② 其它 RHS(`new stack` / `new … hlife` / 裸名别名 `q = p` / 外来指针 / None): - # 槽已登记时交还所有权(释放旧拥有对象 + 影子置 0)——绝不把外来指针当自己的对象释放。 - # **必须在值写入之前调用**:覆盖释放要读到槽里的旧值,晚于写入就变成"释放刚写进去的对象"。 - def _slife_note_assign(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*, rhs_node: ast.AST*, target_name: str = None) -> int: - """M3:赋值处的 slife 所有权记账;返回 1=该槽此刻是编译器拥有的 slife 绑定""" - if target_slot is None or rhs_node is None or trans is None: return 0 - if rhs_node.kind() == ast.ASTKind.New: - nn: ast.New* = ast.New*(rhs_node) - owned: int = self.Gen.CallGenCtor._new_is_owned_heap(nn) - if owned == 0: - # §13 NLL:判据成立时该 `new heap` 也按"编译器托管的堆对象"处理 —— - # 生命期已是 FRAME(propagate_life 同一谓词),这里补上**出口释放**这一半。 - owned = self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, target_name) - if owned != 0: - sh: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot) - if sh is None: return 0 - self._SlifeDropOwned(pool, builder, mod, trans, target_slot) - self.Gen.VarGen._SlifeNoteStore(pool, builder, sh, 1) - return 1 - # 自赋值(p = p):保持原状;否则会"先释放再放弃所有权",把对象漏掉 - if rhs_node.kind() == ast.ASTKind.Name: - rn: ast.Name* = ast.Name*(rhs_node) - if rn.id is not None: - src_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn.id) - if src_slot is not None and uint64(src_slot) == uint64(target_slot): return 0 - # P4 所有权转移:`q = p` 后 **q 接管生命周期** —— 影子标志从 p 移到 q - # (q 置 1、p 置 0)。此后对 p 重新赋值不会释放对象(p 已不拥有该对象), - # 出口只释放 q 一次。不转移的话,`p = None` 会把 q 指向的对象提前释放 → q 悬垂。 - if src_slot is not None: - if self.Gen.VarGen._SlifeOwned(src_slot) != 0: - tsh: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot) - if tsh is not None: - # 覆盖释放 target 原有对象:若它与源是**同一个对象**(自别名 `q = p; q = p`) - # 则跳过,否则会把两个槽共持的对象提前释放。 - self.Gen.VarGen._SlifeDropOwnedUnlessSame(pool, builder, mod, trans, target_slot, src_slot) - self.Gen.VarGen._SlifeNoteStore(pool, builder, tsh, 1) - ssh: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(src_slot) - if ssh is not None: self.Gen.VarGen._SlifeNoteStore(pool, builder, ssh, 0) - return 1 - self._SlifeDropOwned(pool, builder, mod, trans, target_slot) - return 0 - - def _is_addrlike_call(self, node: ast.AST*) -> int: - """Return 1 if the rhs is an explicit pointer expression (c.Addr/c.Deref/c.Cast/c.Ptr call, or new-syntax `&x` / `*p`).""" - if node is None: return 0 - # New syntax (R3/R4): `&x` and `*p` parse as UnaryOp(AddrOf) / UnaryOp(Deref) and replace the - # c.Addr / c.Deref call forms. A pointer-valued RHS must not take the module-global - # "object with static storage -> copy the object" branch, so both forms count as addrlike. - if node.kind() == ast.ASTKind.UnaryOp: - uo_al: ast.UnaryOp* = ast.UnaryOp*(node) - if uo_al.op == ast.OpKind.AddrOf: return 1 - if uo_al.op == ast.OpKind.Deref: return 1 - return 0 - if node.kind() != ast.ASTKind.Call: return 0 - cl: ast.Call* = ast.Call*(node) - if cl.func is None or cl.func.kind() != ast.ASTKind.Attribute: return 0 - at: ast.Attribute* = ast.Attribute*(cl.func) - if at.value is None or at.value.kind() != ast.ASTKind.Name: return 0 - vn: ast.Name* = ast.Name*(at.value) - if vn.id is None or string.strcmp(vn.id, "c") != 0: return 0 - if at.attr is None: return 0 - if string.strcmp(at.attr, "Addr") == 0: return 1 - if string.strcmp(at.attr, "Deref") == 0: return 1 - if string.strcmp(at.attr, "Cast") == 0: return 1 - if string.strcmp(at.attr, "Ptr") == 0: return 1 - return 0 - - def _is_64bit_scalar_ty(self, node: ast.AST*) -> int: - """Return 1 for a 64-bit scalar type (CUInt64T/CInt64T/CSizeT).""" - if node is None: return 0 - ty_nm: str = None - nk: int = node.kind() - if nk == ast.ASTKind.Name: - nm64: ast.Name* = ast.Name*(node) - ty_nm = nm64.id - elif nk == ast.ASTKind.Attribute: - at64: ast.Attribute* = ast.Attribute*(node) - ty_nm = at64.attr - if ty_nm is None: return 0 - if string.strcmp(ty_nm, "CUInt64T") == 0: return 1 - if string.strcmp(ty_nm, "CInt64T") == 0: return 1 - if string.strcmp(ty_nm, "CSizeT") == 0: return 1 - return 0 - - def _is_scalar_ty_name(self, ty_name: str) -> int: - """Return 1 if the type name is a known pure scalar (whitelist).""" - if ty_name is None: return 0 - # Strip the t./c. namespace prefix so int and bare aliases are judged the same - if ty_name[0] == 't' and ty_name[1] == '.': ty_name += 2 - elif ty_name[0] == 'c' and ty_name[1] == '.': ty_name += 2 - if string.strchr(ty_name, '|') is not None: return 0 - if string.strchr(ty_name, '*') is not None: return 0 - if string.strcmp(ty_name, "str") == 0: return 0 - if string.strcmp(ty_name, "bytes") == 0: return 0 - if string.strcmp(ty_name, "CPtr") == 0: return 0 - if string.strstr(ty_name, "Ptr") is not None: return 0 - # Whitelist: known pure scalars (base names + stdint uppercase aliases) - if string.strcmp(ty_name, "int") == 0: return 1 - if string.strcmp(ty_name, "char") == 0: return 1 - # Python builtin scalar names (float/double/bool) pass through as scalar types - if string.strcmp(ty_name, "float") == 0: return 1 - if string.strcmp(ty_name, "double") == 0: return 1 - if string.strcmp(ty_name, "bool") == 0: return 1 - if string.strcmp(ty_name, "CInt") == 0: return 1 - if string.strcmp(ty_name, "CUnsignedInt") == 0: return 1 - if string.strcmp(ty_name, "CUnsigned") == 0: return 1 - if string.strcmp(ty_name, "CChar") == 0: return 1 - if string.strcmp(ty_name, "CUnsignedChar") == 0: return 1 - if string.strcmp(ty_name, "CSignedChar") == 0: return 1 - if string.strcmp(ty_name, "CShort") == 0: return 1 - if string.strcmp(ty_name, "CUnsignedShort") == 0: return 1 - if string.strcmp(ty_name, "CLong") == 0: return 1 - if string.strcmp(ty_name, "CUnsignedLong") == 0: return 1 - if string.strcmp(ty_name, "CLongLong") == 0: return 1 - if string.strcmp(ty_name, "CUnsignedLongLong") == 0: return 1 - if string.strcmp(ty_name, "CSizeT") == 0: return 1 - if string.strcmp(ty_name, "CBool") == 0: return 1 - # Character width family: widths 8/16/32 are not 8-byte pointer slots - if string.strcmp(ty_name, "CWCharT") == 0: return 1 - if string.strcmp(ty_name, "CChar8T") == 0: return 1 - if string.strcmp(ty_name, "CChar16T") == 0: return 1 - if string.strcmp(ty_name, "CChar32T") == 0: return 1 - if string.strcmp(ty_name, "BOOL") == 0: return 1 - if string.strcmp(ty_name, "BYTE") == 0: return 1 - if string.strcmp(ty_name, "UCHAR") == 0: return 1 - if string.strcmp(ty_name, "CHAR") == 0: return 1 - if string.strcmp(ty_name, "SHORT") == 0: return 1 - if string.strcmp(ty_name, "USHORT") == 0: return 1 - if string.strcmp(ty_name, "LONG") == 0: return 1 - if string.strcmp(ty_name, "ULONG") == 0: return 1 - if string.strcmp(ty_name, "LONGLONG") == 0: return 1 - if string.strcmp(ty_name, "ULONGLONG") == 0: return 1 - if string.strcmp(ty_name, "INT") == 0: return 1 - if string.strcmp(ty_name, "UINT") == 0: return 1 - if string.strcmp(ty_name, "WORD") == 0: return 1 - if string.strcmp(ty_name, "DWORD") == 0: return 1 - if string.strcmp(ty_name, "QWORD") == 0: return 1 - # Lowercase bare C type aliases from builtins (same table) - if string.strcmp(ty_name, "uchar") == 0: return 1 - if string.strcmp(ty_name, "short") == 0: return 1 - if string.strcmp(ty_name, "ushort") == 0: return 1 - if string.strcmp(ty_name, "uint") == 0: return 1 - if string.strcmp(ty_name, "long") == 0: return 1 - if string.strcmp(ty_name, "ulong") == 0: return 1 - if string.strcmp(ty_name, "longlong") == 0: return 1 - if string.strcmp(ty_name, "ulonglong") == 0: return 1 - # Fixed-width int/float (C*T forms and bare aliases, prefix-matched) - if string.strstr(ty_name, "Int8") is not None: return 1 - if string.strstr(ty_name, "Int16") is not None: return 1 - if string.strstr(ty_name, "Int32") is not None: return 1 - if string.strstr(ty_name, "Int64") is not None: return 1 - if string.strstr(ty_name, "UInt8") is not None: return 1 - if string.strstr(ty_name, "UInt16") is not None: return 1 - if string.strstr(ty_name, "UInt32") is not None: return 1 - if string.strstr(ty_name, "UInt64") is not None: return 1 - if string.strstr(ty_name, "Float") is not None: return 1 - if string.strstr(ty_name, "FLOAT") is not None: return 1 - if string.strstr(ty_name, "Double") is not None: return 1 - if string.strstr(ty_name, "DOUBLE") is not None: return 1 - # Unknown class name => not a scalar, use an 8-byte pointer slot - return 0 - - def _is_generic_tp_or_nested(self, node: ast.AST*) -> int: - """Return 1 for a generic type-param name or nested union (BinOp).""" - if node is None: return 0 - nk: int = node.kind() - if nk == ast.ASTKind.BinOp: return 1 - if nk == ast.ASTKind.Name: - nm_tp: ast.Name* = ast.Name*(node) - if nm_tp.id is None: return 0 - tp_len: sizet = string.strlen(nm_tp.id) - if tp_len == 1: - if nm_tp.id[0] >= 65 and nm_tp.id[0] <= 90: return 1 - return 0 - - # _init_global_array_from_list - initialize a global array from a list literal - def _init_global_array_from_list(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, array_alloca: mlirlite.Value*, list_node: ast.AST*, trans: HT.Translator*, owner_ctx: str = None) -> int: - """Initialize a global array from a list literal via per-element GEP+store.""" - if builder is None or array_alloca is None or list_node is None: return 1 - - # Get the array type (pointee of [N x elem_ty]*) - arr_ty: mlirlite.LLVMType* = None - if array_alloca.Ty is not None: arr_ty = array_alloca.Ty.Pointee - if arr_ty is None: return 1 - - # Match the array type to get the element type and declared length - elem_ty: mlirlite.LLVMType* = None - arr_count: int = 0 - match arr_ty: - case mlirlite.LLVMType.Array(et, cnt): - elem_ty = et - arr_count = cnt - if elem_ty is None: return 1 - - # Get the list elements - lst: ast.List* = ast.List*(list_node) - if lst is None or lst.elts is None: return 1 - elts: list[ast.AST*]* = lst.elts - elts_count: sizet = len(elts) - - # Special case: a single string literal initializes a char array byte by byte - if self.Gen.ExprGenBase.get_llvm_type_bits(elem_ty) == 8 and elts_count == 1 and arr_count >= 1: - e0_s: ast.AST* = elts.get(0) - if e0_s is not None and e0_s.kind() == ast.ASTKind.Constant: - c0_s: ast.Constant* = ast.Constant*(e0_s) - if c0_s.const_kind == ast.CONST_STR: - sv_s: str = ast.constant_get_str_val(c0_s) - if sv_s is not None: - n_s: sizet = string.strlen(sv_s) - if sizet(arr_count) < n_s: - self.Gen.TypeGenErr.fatal_error(list_node, f"string literal length {n_s} exceeds the declared char array length {arr_count}; out-of-bounds writes are forbidden", "OverflowError") - for bi_s in range(int(n_s)): - bidx_s: mlirlite.Value* = mlirlite.const_int32(pool, bi_s) - bp_s: mlirlite.Value* = mlirlite.build_gep_array(builder, arr_ty, elem_ty, array_alloca, bidx_s) - if bp_s is None: break - bvv: mlirlite.Value* = mlirlite.const_int32(pool, sv_s[bi_s]) - bsv: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, bvv, elem_ty, e0_s) - mlirlite.build_store(builder, bsv, bp_s) - return 0 - - # Element count exceeds declared length: fail fast (avoid out-of-range GEP writes) - if arr_count > 0 and sizet(arr_count) < elts_count: - self.Gen.TypeGenErr.fatal_error(list_node, f"array initializer element count {elts_count} exceeds the declared array length {arr_count}; out-of-bounds writes are forbidden", "OverflowError") - - # Emit GEP + store for each element - for i in range(int(elts_count)): - elem_node: ast.AST* = elts.get(i) - # Explicit null check (repo convention: is None); an empty slot means an element count mismatch. - # When the slot is null, fail fast with class/field/index context. - if elem_node is None: - ctx_dbg: str = "" - if owner_ctx is not None: ctx_dbg = owner_ctx - src_line: int = 0 - src_col: int = 0 - if list_node is not None: - src_line = list_node.lineno - src_col = list_node.col_offset - self.Gen.TypeGenErr.fatal_error(list_node, f"global array initialization hit a null AST element: {ctx_dbg}element {i} is a bare null pointer (0), " f"but the list element count is {elts_count} (element and count mismatch). " f"somewhere upstream produced a null AST element (slot {i} of list-literal node {src_line}:{src_col} was not filled), " f"silently skipping/padding with zero is refused; compilation aborted.", "ValueError") - return 1 - # Object validity check (nonzero vptr): a zero vptr makes any virtual call read address 0 - elem_vptr: uint64 = *((uint64*)(elem_node)) - if elem_vptr == 0: - ctx_vt: str = "" - if owner_ctx is not None: ctx_vt = owner_ctx - vt_line: int = 0 - vt_col: int = 0 - if list_node is not None: - vt_line = list_node.lineno - vt_col = list_node.col_offset - self.Gen.TypeGenErr.fatal_error(list_node, f"global array initialization hit an invalid-object AST element (vptr is 0): {ctx_vt}" f"element {i} has a nonzero pointer (address {uint64(elem_node)}) but its object header (vtable pointer) is 0, " f"and any virtual call on it (kind()/dump()/...) reads address 0 at `load [0+slot]`. " f"list-literal node position {vt_line}:{vt_col}, element count {elts_count}. " f"somewhere upstream produced an AST node with an uninitialized vptr (typical source: a deep/block copy that moved only fields, not the vptr), " f"refusing to continue; compilation aborted.", "ValueError", vt_col) - return 1 - idx_val: mlirlite.Value* = mlirlite.const_int32(pool, i) - elem_ptr: mlirlite.Value* = mlirlite.build_gep_array(builder, arr_ty, elem_ty, array_alloca, idx_val) - if elem_ptr is None: continue - # Nested array: element type is an array and the node is a list literal => recurse - inner_arr: int = 0 - if self.Gen.ExprGenBase.is_array_type(elem_ty) != 0: - # Null check for nested element: use is not None (not integer 0) - if elem_node is not None and elem_node.kind() == ast.ASTKind.List: inner_arr = 1 - if inner_arr != 0: - self._init_global_array_from_list(builder, pool, mod, elem_ptr, elem_node, trans, owner_ctx) - continue - elem_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, elem_node, None, 0, trans) - if elem_val is None: - # Element translation failed: fail fast to expose the upstream cause - self.Gen.TypeGenErr.fatal_error(elem_node, "global array initializer element translation failed", "TypeError") - # Type coercion (e.g. i32 -> i8); Pointee is statically i64 so compare with != 0 - store_val: mlirlite.Value* = elem_val - if elem_ptr.Ty is not None and elem_ptr.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, elem_val, elem_ptr.Ty.Pointee, elem_node) - mlirlite.build_store(builder, store_val, elem_ptr) - - return 0 - - # _apply_aug_op - apply the binary op of an augmented assignment (float, pointer, integer) - def _apply_aug_op(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, op: int, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, trans: HT.Translator* = None, lhs_node: ast.AST* = None) -> mlirlite.Value*: - """Apply the binary op of an augmented assignment (float/pointer/int).""" - # Power aug-assign: base types use llvm.pow via ExprGenOps.emit_pow - if op == ast.OpKind.Pow: - if trans is None: return None - return self.Gen.ExprGenOps.emit_pow(pool, builder, trans.Module, lhs, rhs, node, trans) - lhs_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(lhs.Ty) - rhs_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(rhs.Ty) - lhs_fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(lhs.Ty) - rhs_fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(rhs.Ty) - - # Float op when either operand is float (check before pointer arithmetic) - if lhs_fbits != 0 or rhs_fbits != 0: - # Pick the target float type (the wider one) - target_fbits: int = lhs_fbits - if rhs_fbits > target_fbits: target_fbits = rhs_fbits - target_float_ty: mlirlite.LLVMType* = mlirlite.Double(pool) - if target_fbits == 32: target_float_ty = mlirlite.Float(pool) - # Coerce both operands to the target float type - lhs = self.Gen.ExprGenBase.coerce_to_type(builder, lhs, target_float_ty, node) - rhs = self.Gen.ExprGenBase.coerce_to_type(builder, rhs, target_float_ty, node) - if op == ast.OpKind.Add: return mlirlite.build_fadd(builder, lhs, rhs) - if op == ast.OpKind.Sub: return mlirlite.build_fsub(builder, lhs, rhs) - if op == ast.OpKind.Mult: return mlirlite.build_fmul(builder, lhs, rhs) - if op == ast.OpKind.Div: return mlirlite.build_fdiv(builder, lhs, rhs) - if op == ast.OpKind.Mod: return mlirlite.build_frem(builder, lhs, rhs) - return None - - # Pointer arithmetic: ptr += int / ptr -= int -> getelementptr (scaled by element size) - if lhs_bits == 0 and rhs_bits != 0: - if op == ast.OpKind.Add or op == ast.OpKind.Sub: return self.Gen.ExprGenBase.translate_ptr_arith(pool, builder, lhs, rhs, op) - return None - - # Double-pointer arithmetic: delegate the check to ExprGenOps.fail_if_double_ptr_binop - self.Gen.ExprGenOps.fail_if_double_ptr_binop(pool, op, lhs, rhs, node, 1) - - # x op= y 的操作数约束(fail-fast,先于运算与提升): - # ① y 的有效宽度必须 <= x 的宽度。变量按其 LLVM 类型宽度;常量按其值的**最小可存宽度** - # (所以 `u8 += 1` 合法、`u8 += 300` 报错)。y 更宽 = 隐式收窄,必须先显式收窄再写。 - # ② 不允许 y 显式有符号而 x 无符号:负值经符号扩展会变成巨大的无符号值,属静默语义改变。 - # 移位(<<= / >>=)**豁免** ①②:计数不是写回目标的值,改用静态范围检查(见下方)。 - y_node_aa: ast.AST* = None - if node is not None and node.kind() == ast.ASTKind.AugAssign: y_node_aa = ast.AugAssign*(node).value - if lhs_bits != 0 and rhs_bits != 0 and op != ast.OpKind.LShift and op != ast.OpKind.RShift: - y_bits_eff: int = rhs_bits - if y_node_aa is not None and y_node_aa.kind() == ast.ASTKind.Constant: - cn_aa: ast.Constant* = ast.Constant*(y_node_aa) - if cn_aa.const_kind == ast.CONST_INT: y_bits_eff = self.define_min_width(cn_aa.int_val) - if y_bits_eff > lhs_bits: - self.Gen.TypeGenErr.fatal_error(node, f"[TPV] augmented assignment operand too wide: the right-hand side needs {y_bits_eff} bits, " f"but the target provides only {lhs_bits} bits; an implicit narrowing is forbidden. " f"Fix: narrow the right-hand side explicitly (e.g. uint{lhs_bits}(...) / int{lhs_bits}(...)) before the operation.", "CastError") - x_uns_aa: int = 0 - if lhs_node is not None: x_uns_aa = self.Gen.ExprGenOps._node_type_unsigned(pool, lhs_node, trans) - y_sgn_aa: int = 0 - if y_node_aa is not None: y_sgn_aa = self.Gen.ExprGenOps._node_type_signed(pool, y_node_aa, trans) - if x_uns_aa != 0 and y_sgn_aa != 0: - self.Gen.TypeGenErr.fatal_error(node, "[TPV] augmented assignment operand signedness conflict: the right-hand side is explicitly signed " "while the target is unsigned; sign extension would turn negative values into huge unsigned values. " "Fix: use an unsigned right-hand side (e.g. uint32(...)), or make the target signed.", "CastError") - - # Integer op: type promotion - if lhs_bits > rhs_bits and rhs_bits > 0: rhs = self.Gen.ExprGenBase.coerce_to_type(builder, rhs, lhs.Ty, node, trans) - elif rhs_bits > lhs_bits and lhs_bits > 0: lhs = self.Gen.ExprGenBase.coerce_to_type(builder, lhs, rhs.Ty, node, trans) - - # 移位:计数本身豁免宽度/符号规则,改按**提升后**的左操作数位宽做静态范围检查 - if op == ast.OpKind.LShift or op == ast.OpKind.RShift: - self.require_shift_count_in_range(pool, trans, node, y_node_aa, self.Gen.ExprGenBase.get_llvm_type_bits(lhs.Ty)) - - # Unsigned operands use udiv/urem; signed use sdiv/srem - aug_unsigned: int = 0 - if lhs_node is not None: - if self.Gen.ExprGenOps._node_type_unsigned(pool, lhs_node, trans) != 0: aug_unsigned = 1 - - if op == ast.OpKind.Add: return mlirlite.build_add(builder, lhs, rhs) - elif op == ast.OpKind.Sub: return mlirlite.build_sub(builder, lhs, rhs) - elif op == ast.OpKind.Mult: return mlirlite.build_mul(builder, lhs, rhs) - elif op == ast.OpKind.Div: - if aug_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) - return mlirlite.build_sdiv(builder, lhs, rhs) - elif op == ast.OpKind.FloorDiv: - if aug_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) - return mlirlite.build_sdiv(builder, lhs, rhs) - elif op == ast.OpKind.Mod: - if aug_unsigned != 0: return mlirlite.build_urem(builder, lhs, rhs) - return mlirlite.build_srem(builder, lhs, rhs) - elif op == ast.OpKind.BitAnd: return mlirlite.build_and(builder, lhs, rhs) - elif op == ast.OpKind.BitOr: return mlirlite.build_or(builder, lhs, rhs) - elif op == ast.OpKind.BitXor: return mlirlite.build_xor(builder, lhs, rhs) - elif op == ast.OpKind.LShift: return mlirlite.build_shl(builder, lhs, rhs) - elif op == ast.OpKind.RShift: - # Unsigned >> uses lshr, signed >> uses ashr (i8/i16 default unsigned) - if lhs_bits == 8 or lhs_bits == 16: return mlirlite.build_lshr(builder, lhs, rhs) - if lhs_node is not None and trans is not None: - if self.Gen.ExprGenOps._node_type_unsigned(pool, lhs_node, trans) != 0: return mlirlite.build_lshr(builder, lhs, rhs) - return mlirlite.build_ashr(builder, lhs, rhs) - return None - - # translate_augassign - translate augmented assignment statements - def const_assign_msg(self, kind: str, name: str, src_name: str) -> str: - """Build the const-assignment error message used by const_assign_fail.""" - decl: str = "(annotation contains t.CConst; the source name at the declaration was not recorded)" - if src_name is not None: decl = f"t.CConst | {src_name}" - return GenApi.escape_fstring_copy(f"cannot assign to const variable '{name}' ({kind}); " f"declaration type (including const): {decl}" f"; fix: remove the const descriptor / t.CConst from the declaration, or assign once at the declaration " f"and afterwards use only non-const copies/temporaries; a const object forbids assignment, " f"augmented assignment and element assignment within its scope, and this never degrades to a warning") - - def const_assign_fail(self, node: ast.AST*, kind: str, name: str, src_name: str) -> int: - """Fail fast when a const target is written.""" - self.Gen.TypeGenErr.fatal_error(node, self.const_assign_msg(kind, name, src_name), "TypeError") - return 0 - - def translate_augassign(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Translate an augmented assignment statement (x += 1).""" - if node is None: return 0 - - aug: ast.AugAssign* = ast.AugAssign*(node) - if aug is None: return 0 - - target: ast.AST* = aug.target - if target is None: return 0 - - tk: int = target.kind() - - # Attribute target: self.field += 1 (get_attribute_ptr -> load -> binop -> store) - if tk == ast.ASTKind.Attribute: - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - mod: mlirlite.LLVMModule* = trans.Module - - # const field: fail fast on the field write - at_ca: ast.Attribute* = ast.Attribute*(target) - if at_ca is not None and at_ca.attr is not None: - if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, target) != 0: - self.const_assign_fail(target, "field assignment", at_ca.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, target)) - - # 1. Get the field pointer - field_ptr: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, target, trans) - if field_ptr is None: - # fail-fast: attribute pointer resolution failed - self.Gen.TypeGenErr.fatal_error(target, "attribute ptr is None", "TypeError") - - # 2. Determine the field type and load the current value - target_ty: mlirlite.LLVMType* = None - if field_ptr.Ty is not None and field_ptr.Ty.Pointee != 0: target_ty = field_ptr.Ty.Pointee - if target_ty is None: - # fail-fast: field type missing, no i32 fallback - self.Gen.TypeGenErr.fatal_error(target, "field type information missing", "TypeError") - # t.CVolatile field: both load and store carry the volatile qualifier - vol_fa: int = self.Gen.ExprGenAttr.attr_field_is_volatile(pool, trans, target) - # rawptr[t.CVolatile|T] base: field load/store also carry volatile - at_aug: ast.Attribute* = ast.Attribute*(target) - if at_aug is not None: - if self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, at_aug.value) != 0: vol_fa = 1 - cur_val: mlirlite.Value* = mlirlite.build_load(builder, target_ty, field_ptr, vol_fa) - if cur_val is None: - # fail-fast: attribute current value load failed - self.Gen.TypeGenErr.fatal_error(target, "cannot load the current value of the attribute", "TypeError") - - # 3. Translate the RHS value - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) - if rhs_val is None: - # fail-fast: RHS translation failed - self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") - - # 4. Apply the binary op - result: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, cur_val, rhs_val, node, trans, target) - if result is None: - # fail-fast: attribute binary op failed - self.Gen.TypeGenErr.fatal_error(target, f"attribute binary op failed op={aug.op}", "TypeError") - - # 5. Coerce the type and store - result = self.Gen.ExprGenBase.coerce_to_type(builder, result, target_ty, node) - if result is None: - # fail-fast: type coercion failed - self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") - mlirlite.build_store(builder, result, field_ptr, vol_fa) - return 0 - - # Subscript target: self.state[i] += x (get_subscript_ptr -> load -> binop -> store) - if tk == ast.ASTKind.Subscript: - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - mod: mlirlite.LLVMModule* = trans.Module - - # const element target: fail fast when the base variable or field is const - sub_ca: ast.Subscript* = ast.Subscript*(target) - if sub_ca is not None and sub_ca.value is not None: - if sub_ca.value.kind() == ast.ASTKind.Name: - nm_ca: ast.Name* = ast.Name*(sub_ca.value) - if nm_ca.id is not None: - ve_ca: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ca.id) - if self.Gen.VarGen.var_is_const(ve_ca) != 0: - self.const_assign_fail(target, "element assignment", nm_ca.id, self.Gen.VarGen.var_annot_class_name(ve_ca)) - elif sub_ca.value.kind() == ast.ASTKind.Attribute: - at_ca2: ast.Attribute* = ast.Attribute*(sub_ca.value) - if at_ca2.attr is not None: - if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, sub_ca.value) != 0: - self.const_assign_fail(target, "element assignment", at_ca2.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, sub_ca.value)) - - # 1. Get the element pointer - elem_ptr: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, target, trans) - if elem_ptr is None: - # fail-fast: subscript pointer resolution failed - self.Gen.TypeGenErr.fatal_error(target, "subscript ptr is None", "TypeError") - - # 2. Determine the element type and load the current value - target_ty: mlirlite.LLVMType* = None - if elem_ptr.Ty is not None and elem_ptr.Ty.Pointee != 0: target_ty = elem_ptr.Ty.Pointee - if target_ty is None: - # fail-fast: element type missing, no i32 fallback - self.Gen.TypeGenErr.fatal_error(target, "element type information missing", "TypeError") - # rawptr[t.CVolatile|T] base: p[i] load/store carry volatile when the base is resolvable - vol_sa: int = 0 - sub_aug: ast.Subscript* = ast.Subscript*(target) - if sub_aug is not None: vol_sa = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, sub_aug.value) - cur_val: mlirlite.Value* = mlirlite.build_load(builder, target_ty, elem_ptr, vol_sa) - if cur_val is None: - # fail-fast: element current value load failed - self.Gen.TypeGenErr.fatal_error(target, "cannot load the current value of the subscript element", "TypeError") - - # 3. Translate the RHS value - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) - if rhs_val is None: - # fail-fast: RHS translation failed - self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") - - # 4. Apply the binary op - result: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, cur_val, rhs_val, node, trans, target) - if result is None: - # fail-fast: subscript binary op failed - self.Gen.TypeGenErr.fatal_error(target, f"subscript binary op failed op={aug.op}", "TypeError") - - # 5. Coerce the type and store - result = self.Gen.ExprGenBase.coerce_to_type(builder, result, target_ty, node) - if result is None: - # fail-fast: type coercion failed - self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") - mlirlite.build_store(builder, result, elem_ptr, vol_sa) - return 0 - - # UnaryOp target: *p += v (deref augmented assignment: load, op, store back) - if tk == ast.ASTKind.UnaryOp: - uo_ag: ast.UnaryOp* = ast.UnaryOp*(target) - if uo_ag is None or uo_ag.operand is None: self.Gen.TypeGenErr.fatal_error(target, "augmented-assignment target unary op is missing an operand", "TypeError") - if uo_ag.op != ast.OpKind.Deref: - self.Gen.TypeGenErr.fatal_error(target, "augmented-assignment target supports only the unary '*' dereference (&x / -x / ~x are not valid lvalues)", "SyntaxError") - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - mod: mlirlite.LLVMModule* = trans.Module - ptr_ag: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo_ag.operand, None, 0, trans) - if ptr_ag is None or ptr_ag.Ty is None or self.Gen.ExprGenBase.is_ptr_type(ptr_ag.Ty) == 0: - self.Gen.TypeGenErr.fatal_error(target, "the operand of augmented-assignment '*' must be a pointer type (type inference degraded)", "TypeError") - ty_ag: mlirlite.LLVMType* = ptr_ag.Ty.Pointee - if ty_ag is None: - self.Gen.TypeGenErr.fatal_error(target, "deref augmented-assignment target type missing (pointee is empty)", "TypeError") - old_ag: mlirlite.Value* = mlirlite.build_load(builder, ty_ag, ptr_ag, 0) - if old_ag is None: self.Gen.TypeGenErr.fatal_error(target, "cannot load the current dereferenced value", "TypeError") - rhs_ag: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) - if rhs_ag is None: self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") - res_ag: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, old_ag, rhs_ag, node, trans, target) - if res_ag is None: self.Gen.TypeGenErr.fatal_error(target, f"deref binary op failed op={aug.op}", "TypeError") - res_ag = self.Gen.ExprGenBase.coerce_to_type(builder, res_ag, ty_ag, node, trans) - if res_ag is None: self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") - mlirlite.build_store(builder, res_ag, ptr_ag, 0) - return 0 - - if tk != ast.ASTKind.Name: - # fail-fast: unsupported AugAssign target kind - self.Gen.TypeGenErr.fatal_error(target, "AugAssign target type unsupported; only Name/Attribute/Subscript/*dereference are supported", "TypeError") - - nm: ast.Name* = ast.Name*(target) - if nm is None or nm.id is None: return 0 - - # const variable: fail fast immediately - ve_aug: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) - if self.Gen.VarGen.var_is_const(ve_aug) != 0: - self.const_assign_fail(target, "augmented assignment", nm.id, self.Gen.VarGen.var_annot_class_name(ve_aug)) - - # REPL session variable aug-assign: read slot, apply op, write slot and result area - if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: - if self.Gen.VarGen.repl_try_augassign(trans, nm.id, aug.op, aug.value, node) != 0: return 0 - - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - mod: mlirlite.LLVMModule* = trans.Module - - # 1. Determine scope: 0=local, 1=global, 2=nonlocal - scope_type: int = 0 - if GenApi.is_global_name(trans, nm.id) != 0: scope_type = 1 - elif GenApi.is_nonlocal_name(trans, nm.id) != 0: scope_type = 2 - - # t.CVolatile variable: both load and store carry the volatile qualifier - vol_aa: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) - - # 2. Load the current value - cur_val: mlirlite.Value* = None - target_alloca: mlirlite.Value* = None - target_ty: mlirlite.LLVMType* = None - - if scope_type == 1: - # global variable - target_alloca = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) - if target_alloca is not None: - if target_alloca.Ty is None: - # fail-fast: variable type missing, no i32 fallback - self.Gen.TypeGenErr.fatal_error(target, "variable type information missing", "TypeError") - target_ty = target_alloca.Ty.Pointee - cur_val = mlirlite.build_load(builder, target_ty, target_alloca, vol_aa) - elif scope_type == 2: - # nonlocal variable (via closure env) - cur_val = self.Gen.StmtGenDecl.load_nonlocal_var(trans, nm.id) - if cur_val is not None: target_ty = cur_val.Ty - else: - # Ordinary local variable - target_alloca = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) - if target_alloca is not None: - if target_alloca.Ty is None: - # fail-fast: variable type missing, no i32 fallback - self.Gen.TypeGenErr.fatal_error(target, "variable type information missing", "TypeError") - target_ty = target_alloca.Ty.Pointee - cur_val = mlirlite.build_load(builder, target_ty, target_alloca, vol_aa) - - if cur_val is None: - # fail-fast: target variable current value load failed - self.Gen.TypeGenErr.fatal_error(target, f"cannot load the current value of target variable '{nm.id}'", "TypeError") - - # 3. Translate the RHS value - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) - if rhs_val is None: - # fail-fast: RHS translation failed - self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") - - # 3b. Operator overload: object += -> __iadd__, fall back to __add__ (locals only) - if scope_type == 0 and target_alloca is not None: - aug_dup: str = self._aug_op_to_dunder(aug.op) - if aug_dup is not None: - ovl_self_aug: mlirlite.Value* = target_alloca - # Pointer storage slot (Ptr(Ptr(Struct))): load the object pointer as self - if target_alloca.Ty is not None and target_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(target_alloca.Ty.Pointee) != 0: - load_self_aug: mlirlite.Value* = mlirlite.build_load(builder, target_alloca.Ty.Pointee, target_alloca) - if load_self_aug is not None: ovl_self_aug = load_self_aug - # Try the aug dunder only when the target is a struct (class instance) - if self.Gen.StructGen.get_struct_type_from_value(ovl_self_aug) is not None: - aug_extra: sizet* = pool.alloc(8) - if aug_extra is not None: - aug_extra[0] = sizet(rhs_val) - aug_ovl: mlirlite.Value* = self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, ovl_self_aug, aug_dup, trans, aug_extra, 1) - if aug_ovl is None: - aug_fb: str = self._op_to_dunder(aug.op) - if aug_fb is not None: aug_ovl = self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, ovl_self_aug, aug_fb, trans, aug_extra, 1) - if aug_ovl is not None: - aug_res: mlirlite.Value* = aug_ovl - if target_ty is not None and aug_res.Ty is not None and self.Gen.ExprGenBase._same_agg_ty(pool, aug_res.Ty, target_ty) == 0: - # Target is a struct value but the result is a pointer -> load the struct value - if self.Gen.ExprGenBase.is_ptr_type(target_ty) == 0 and self.Gen.ExprGenBase.is_ptr_type(aug_res.Ty) != 0 and aug_res.Ty.Pointee != 0: - aug_res = mlirlite.build_load(builder, aug_res.Ty.Pointee, aug_res) - else: - coerced_aug: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, aug_res, target_ty, node, trans) - if coerced_aug is not None: aug_res = coerced_aug - if aug_res is None or target_ty is None or self.Gen.ExprGenBase._same_agg_ty(pool, aug_res.Ty, target_ty) == 0: - # fail-fast: overload return type mismatch, do not corrupt memory silently - self.Gen.TypeGenErr.fatal_error(node, "augmented-assignment operator overload return type does not match the target variable", "TypeError") - mlirlite.build_store(builder, aug_res, target_alloca, vol_aa) - return 0 - - # 4. Coerce the type and apply the binary op - result: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, cur_val, rhs_val, node, trans, target) - if result is None: - # fail-fast: binary op failed - self.Gen.TypeGenErr.fatal_error(target, f"applying binary op to variable '{nm.id}' failed op={aug.op}", "TypeError") - - # 5. Store the result - result = self.Gen.ExprGenBase.coerce_to_type(builder, result, target_ty, node) - if result is None: - # fail-fast: type coercion failed - self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") - - if scope_type == 1: - # global variable - if target_alloca is not None: mlirlite.build_store(builder, result, target_alloca, vol_aa) - elif scope_type == 2: - # nonlocal variable - nl_ptr: mlirlite.Value* = self.Gen.StmtGenDecl.get_nonlocal_var_ptr(trans, nm.id) - if nl_ptr is not None: mlirlite.build_store(builder, result, nl_ptr) - else: - # Ordinary local variable - if target_alloca is not None: mlirlite.build_store(builder, result, target_alloca, vol_aa) - - return 0 - - # is_ctypedef_annotation - check whether an annotation is t.CTypedef - def is_ctypedef_annotation(self, annot: ast.AST*) -> int: - """Return 1 if the annotation is t.CTypedef, else 0.""" - return self.Gen.TypeGenCType._is_ctypedef_annotation(annot) - - # _is_single_param_carray_annot - check for single-param t.CArray[elem_ty] - def _is_single_param_carray_annot(self, annot: ast.AST*) -> int: - """Return 1 for a single-param or None-length t.CArray annotation.""" - if annot is None or annot.kind() != ast.ASTKind.Subscript: return 0 - sub: ast.Subscript* = ast.Subscript*(annot) - if sub.value is None or sub.slice is None: return 0 - if sub.value.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(sub.value) - if at.attr is None or string.strcmp(at.attr, "CArray") != 0: return 0 - if at.value is None or at.value.kind() != ast.ASTKind.Name: return 0 - mv: ast.Name* = ast.Name*(at.value) - if mv.id is None or string.strcmp(mv.id, "t") != 0: return 0 - elif sub.value.kind() == ast.ASTKind.Name: - # Bare CArray / builtin rawarray / builtin array are all equivalent to t.CArray - bn: ast.Name* = ast.Name*(sub.value) - if bn.id is None or (string.strcmp(bn.id, "CArray") != 0 and string.strcmp(bn.id, "rawarray") != 0 and string.strcmp(bn.id, "array") != 0): return 0 - else: return 0 - # Single-param form: slice is not a Tuple (e.g. t.CArray[u64]) - if sub.slice.kind() != ast.ASTKind.Tuple: return 1 - # Two-param form with count=None is equivalent to single-param - tup: ast.Tuple* = ast.Tuple*(sub.slice) - if tup.elts is None or len(tup.elts) < 2: return 0 - cnt_node: ast.AST* = tup.elts.get(1) - if cnt_node is not None and cnt_node.kind() == ast.ASTKind.Constant: - cn0: ast.Constant* = ast.Constant*(cnt_node) - if cn0.const_kind == ast.CONST_NONE: return 1 - return 0 - - # _infer_static_array_ty_from_literal - single-param CArray + list literal -> [len x elem_ty] - def _infer_static_array_ty_from_literal(self, pool: memhub.MemBuddy*, annot: ast.AST*, value: ast.AST*, resolved_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: - """Return Array(elem_ty, len) for a single-param CArray with a list literal.""" - if value is not None and value.kind() == ast.ASTKind.List and resolved_ty is not None: - if self._is_single_param_carray_annot(annot) != 0: - elem_ty_arr: mlirlite.LLVMType* = resolved_ty.Pointee - lst_arr: ast.List* = ast.List*(value) - cnt_arr: int = 0 - if lst_arr.elts is not None: cnt_arr = int(len(lst_arr.elts)) - # Special case: a single string literal means length strlen+1 (include the NUL) - if elem_ty_arr is not None and cnt_arr == 1 and lst_arr.elts is not None: - e0_arr: ast.AST* = lst_arr.elts.get(0) - if e0_arr is not None and e0_arr.kind() == ast.ASTKind.Constant: - c0_arr: ast.Constant* = ast.Constant*(e0_arr) - if c0_arr.const_kind == ast.CONST_STR: - sv0_arr: str = ast.constant_get_str_val(c0_arr) - if sv0_arr is not None and self.Gen.ExprGenBase.get_llvm_type_bits(elem_ty_arr) == 8: return mlirlite.Array(pool, elem_ty_arr, int(string.strlen(sv0_arr)) + 1) - if elem_ty_arr is not None and cnt_arr > 0: return mlirlite.Array(pool, elem_ty_arr, cnt_arr) - return resolved_ty - - # _build_ann_fail_msg - build a detailed annotation-resolution-failed error - def _build_ann_fail_msg(self, pool: memhub.MemBuddy*, annot: ast.AST*, nm_id: str) -> str: - """Build a detailed annotation-resolution-failure message (heap buffer).""" - ann_txt_x: str = None - if annot is not None: ann_txt_x = self.Gen.TypeGenResolve._get_type_name_from_annotation(pool, annot) - if ann_txt_x is None: ann_txt_x = "?" - btd_nm_x: str = self.Gen.TypeGenCType.LastBuiltinsTypedefName - btd_rs_x: str = self.Gen.TypeGenCType.LastBuiltinsTypedefResult - msg_len: sizet = 640 - msg_buf: str = stdlib.malloc(msg_len) - if msg_buf is None: return ann_txt_x - if btd_nm_x is not None and btd_rs_x is not None: viperlib.snprintf(msg_buf, msg_len, "variable type annotation resolution failed, variable name: %s, annotation: %s. last builtins match: %s expanded to %s, still unresolvable", nm_id, ann_txt_x, btd_nm_x, btd_rs_x) - elif btd_nm_x is not None: viperlib.snprintf(msg_buf, msg_len, "variable type annotation resolution failed, variable name: %s, annotation: %s. last builtins match: %s missed (builtins.vp has no such typedef, or the scan did not run)", nm_id, ann_txt_x, btd_nm_x) - else: viperlib.snprintf(msg_buf, msg_len, "variable type annotation resolution failed, variable name: %s, annotation: %s. last builtins match: not triggered", nm_id, ann_txt_x) - return msg_buf - - # translate_annassign_prescan - pre-create alloca for annotated vars - def translate_annassign_prescan(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Pre-create allocas for AnnAssign variables at the entry block.""" - if node is None: return 0 - k: int = node.kind() - if k != ast.ASTKind.AnnAssign: return 0 - - aa: ast.AnnAssign* = ast.AnnAssign*(node) - if aa is None or aa.target is None: return 0 - target: ast.AST* = aa.target - if target.kind() != ast.ASTKind.Name: return 0 - - nm: ast.Name* = ast.Name*(target) - if nm.id is None: return 0 - - # CDefine annotation: register the constant instead of creating an alloca - if self.is_cdefine_annotation(aa.annotation) != 0: - pool_ps: memhub.MemBuddy* = trans.Pool - self._register_cdefine_from_value(pool_ps, nm.id, aa.value) - return 0 - - # CTypedef annotation: register the alias only, no storage (avoids duplicate symbols) - if self.is_ctypedef_annotation(aa.annotation) != 0: return 0 - - # REPL fragment entry: AnnAssign goes to the session slot; skip pre-creating a local alloca - if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: return 0 - - # global/nonlocal variables need no local alloca - if GenApi.is_global_name(trans, nm.id) != 0: return 0 - if GenApi.is_nonlocal_name(trans, nm.id) != 0: return 0 - - # Module-level globals are already registered; only skip the local alloca at module scope - if self.Gen.VarGen.is_module_scope(trans.SymTab) != 0: - if self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) is not None: return 0 - - # Check whether it already exists - existing: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) - if existing is not None: return 0 - - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - - # Infer the type from the annotation (no hard-coded i32) - var_ty: mlirlite.LLVMType* = None - annot: ast.AST* = aa.annotation - if annot is not None: - # Value-reference markers are parameter-only: a local variable `x: &T` fails fast. - self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "a local variable declaration") - # Specialize generics before resolving so list[X] does not degrade to opaque list* - self._prespecialize_generic_annotation(trans, pool, annot) - resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0) - if resolved is not None: - var_ty = resolved - # Single-param CArray + list literal -> static array; keep PreScan and Handle types in sync - var_ty = self._infer_static_array_ty_from_literal(pool, annot, aa.value, var_ty) - # [D31 fail-fast] generic annotation degraded to a bare base class - self._fail_on_degenerated_generic_annotation(aa, nm.id, annot, var_ty) - else: - # fail-fast: annotation resolution failed (no silent i32 fallback) - self.Gen.TypeGenErr.fatal_error(aa, self._build_ann_fail_msg(pool, annot, nm.id), "TypeError") - else: - # fail-fast: AnnAssign must have an annotation - self.Gen.TypeGenErr.fatal_error(aa, f"AnnAssign is missing a type annotation, variable name: {nm.id}", "SyntaxError") - - # Ensure the full cross-module struct definition is available (for alloca) - self.Gen.StructGen.ensure_struct_def_in_module(pool, trans.Module, var_ty) - alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, var_ty) - if alloca is not None: - if self.Gen.VarGen.define_var(trans.SymTab, nm.id, alloca) == 0: - # Store the annotation's class name; pass pool so generic Subscript yields the mangled name - if annot is not None: - cls_nm_ps: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(annot, trans._imported_modules, pool, trans._from_imports) - if cls_nm_ps is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, nm.id, cls_nm_ps) - # Check for bytes* or str* (i8** semantics, 8-byte stride) - if self._is_ptr_element_annotation(annot) != 0: self.Gen.VarGen.set_var_ptr_element(trans.SymTab, nm.id) - # const object (annotation has t.CConst and the type is not a pointer): no writes in scope - if self.Gen.TypeGenErr.is_const_object_marker(annot, var_ty) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, nm.id) - # t.CVolatile: mark the variable at declaration; its load/store carry volatile - if self.Gen.TypeGenErr.has_decorator_marker(annot, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) - # rawptr[t.CVolatile | T]: dereference load/store carry volatile - if self.Gen.TypeGenErr.has_pointee_volatile_marker(annot) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, nm.id) - return 0 - return 0 - - # _prespecialize_generic_annotation - register the generic specialization before resolving - def _prespecialize_generic_annotation(self, trans: HT.Translator*, pool: memhub.MemBuddy*, annot: ast.AST*) -> int: - """Pre-specialize a generic annotation; return 1 if attempted.""" - if annot is None or trans is None: return 0 - # Take the generic Subscript side: the Subscript itself or the Subscript of A* - spec_annot: ast.Subscript* = None - if annot.kind() == ast.ASTKind.Subscript: spec_annot = ast.Subscript*(annot) - elif annot.kind() == ast.ASTKind.BinOp: - bop_ps: ast.BinOp* = ast.BinOp*(annot) - if bop_ps.op == ast.OpKind.BitOr: - if bop_ps.left is not None and bop_ps.left.kind() == ast.ASTKind.Subscript: spec_annot = ast.Subscript*(bop_ps.left) - elif bop_ps.right is not None and bop_ps.right.kind() == ast.ASTKind.Subscript: spec_annot = ast.Subscript*(bop_ps.right) - if spec_annot is None: return 0 - sv_ps: ast.AST* = spec_annot.value - if sv_ps is None or sv_ps.kind() != ast.ASTKind.Name: return 0 - sub_nm: ast.Name* = ast.Name*(sv_ps) - if sub_nm.id is None: return 0 - template_cd: ast.ClassDef* = self.Gen.ClassGenGeneric._find_generic_template(sub_nm.id) - if template_cd is None: return 0 - type_args_ps: list[str]* = self.Gen.CallGenCtor._extract_type_args_from_slice(pool, spec_annot.slice, trans._from_imports) - if type_args_ps is None or len(type_args_ps) == 0: return 0 - self.Gen.ClassGenGeneric._specialize_generic_class(trans, sub_nm.id, type_args_ps) - return 1 - - # translate_annassign - translate an AnnAssign statement; returns new variable count - def _fail_on_degenerated_generic_annotation(self, aa: ast.AnnAssign*, var_name: str, annot: ast.AST*, var_ty: mlirlite.LLVMType*) -> int: - """Fail fast when a generic annotation degrades to a bare base pointer.""" - if aa is None or annot is None or var_ty is None: return 0 - # Take the generic Subscript side: the Subscript itself or the Subscript of A* - ann_sub: ast.Subscript* = None - if annot.kind() == ast.ASTKind.Subscript: ann_sub = ast.Subscript*(annot) - elif annot.kind() == ast.ASTKind.BinOp: - bop_ff: ast.BinOp* = ast.BinOp*(annot) - if bop_ff.op == ast.OpKind.BitOr: - if bop_ff.left is not None and bop_ff.left.kind() == ast.ASTKind.Subscript: ann_sub = ast.Subscript*(bop_ff.left) - elif bop_ff.right is not None and bop_ff.right.kind() == ast.ASTKind.Subscript: ann_sub = ast.Subscript*(bop_ff.right) - if ann_sub is None or ann_sub.value is None or ann_sub.value.kind() != ast.ASTKind.Name: return 0 - base_nm_ff: ast.Name* = ast.Name*(ann_sub.value) - if base_nm_ff.id is None: return 0 - # Only treat it as degraded when the base is a real generic template - if self.Gen.ClassGenGeneric._find_generic_template(base_nm_ff.id) is None: return 0 - if var_ty.Pointee == 0: return 0 - vt_nm_ff: str = mlirlite.get_struct_name(var_ty.Pointee) - if vt_nm_ff is None: return 0 - vt_pf_ff: int = GenApi.sha1_prefix_len(vt_nm_ff) - vt_short_ff: str = vt_nm_ff + vt_pf_ff if vt_pf_ff != 0 else vt_nm_ff - if string.strchr(vt_short_ff, '[') is not None: - return 0 # With type args: not degraded - self.Gen.TypeGenErr.fatal_error(aa, f"the generic annotation '{base_nm_ff.id}[...]' of variable '{var_name}' resolved to a bare base class " f"'{vt_nm_ff}*' (type arguments lost); subsequent .get()/.set() will specialize by the wrong element " f"type (such as list[int].get returning i32) -> inttoptr truncates the pointer and crashes. " f"Silent continuation is forbidden -- check the degradation point of this annotation in resolve_annotation_type.", "TypeError") - return 0 - - def translate_annassign(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Translate an AnnAssign statement; return the new variable count.""" - aa: ast.AnnAssign* = ast.AnnAssign*(node) - if aa is None: return 0 - - target: ast.AST* = aa.target - if target is None: return 0 - # 属性/下标目标(`obj.f: T = v` / `arr[i]: T = v`):走独立路径——不建局部变量,而是按注解 - # 类型对既有存储做一次"带 P3 长命左值检查"的写出。原先这里直接 return 0,整条语句被**静默丢弃**。 - if target.kind() != ast.ASTKind.Name: return self._translate_lvalue_annassign(trans, aa, target) - - nm: ast.Name* = ast.Name*(target) - if nm.id is None: return 0 - - # CDefine annotation: compile-time constant, no runtime code; register it (idempotent) - if self.is_cdefine_annotation(aa.annotation) != 0: - pool_cd: memhub.MemBuddy* = trans.Pool - self._register_cdefine_from_value(pool_cd, nm.id, aa.value, aa.annotation) - return 0 - - # CTypedef annotation: register the alias on demand; no storage (avoids duplicate symbols) - if self.is_ctypedef_annotation(aa.annotation) != 0: - pool_ct: memhub.MemBuddy* = trans.Pool - ctd_str: str = self.Gen.TypeGenResolve._get_type_name_from_annotation(pool_ct, aa.value) - if ctd_str is not None: self.Gen.TypeGenCType.register_ctypedef(pool_ct, nm.id, ctd_str) - return 0 - - # Use-site width check for define/CDefine constants (a: T = C): T must be >= W_min - self.require_cdefine_use_width(trans.Pool, trans, aa.annotation, aa.value) - - # REPL session variable: annotated declaration at fragment entry goes to the session slot - if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: - rp_e: GenTypes.ReplVarEntry* = self.Gen.VarGen.repl_ensure_entry(trans, nm.id, aa) - if rp_e is not None: - self.Gen.VarGen.repl_try_annassign(trans, nm.id, aa.annotation, aa.value, aa) - return 0 - - # Duplicate-declaration check (fail fast): re-annotating a module-level name inside a function. - # Legal alternatives: a global declaration plus unannotated assignment, or a differently named local. - has_mod_decl_add: int = 0 - if nm.id is not None and trans is not None and self.Gen.VarGen.is_module_scope(trans.SymTab) == 0: - if self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) is not None: has_mod_decl_add = 1 - # 双保险:本文件顶层变量名集合(SymTab.Root.Vars 可能在部分时序看不到) - if has_mod_decl_add == 0 and GenApi.is_file_var(nm.id) != 0: has_mod_decl_add = 1 - if has_mod_decl_add != 0: - dup_decl: int = 0 - if GenApi.is_global_name(trans, nm.id) != 0 or GenApi.is_nonlocal_name(trans, nm.id) != 0: dup_decl = 1 - if dup_decl != 0: - self.Gen.TypeGenErr.fatal_error(aa, f"duplicate declaration: '{nm.id}' is already a module-level variable; after declaring global/nonlocal here " f"it is re-declared with a type annotation. Remove the type annotation on this line; a plain assignment modifies the global", "TypeError") - else: - self.Gen.TypeGenErr.fatal_error(aa, f"duplicate declaration: '{nm.id}' is already a module-level variable; without a global declaration in a function, a type annotation must not re-declare " f"the same name (it would shadow the global with a local). For an independent local, use another name; " f"to modify the global, add a global declaration in the function and remove this annotation", "TypeError") - - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - mod: mlirlite.LLVMModule* = trans.Module - - # Determine the type (inferred from the annotation) - var_ty: mlirlite.LLVMType* = None - annot: ast.AST* = aa.annotation - if annot is not None: - # Value-reference markers are parameter-only: a local variable `x: &T` fails fast. - self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "a local variable declaration") - # Same as PreScan: specialize generics first so list[X] does not degrade to bare list* - self._prespecialize_generic_annotation(trans, pool, annot) - resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0) - if resolved is not None: var_ty = resolved - else: - # fail-fast: annotation resolution failed (no silent i32 fallback) - self.Gen.TypeGenErr.fatal_error(aa, self._build_ann_fail_msg(pool, annot, nm.id), "TypeError") - else: - # fail-fast: AnnAssign must have an annotation - self.Gen.TypeGenErr.fatal_error(aa, f"AnnAssign is missing a type annotation, variable name: {nm.id}", "SyntaxError") - - # void cannot be a variable type (only void* and opaque-struct typedef) - if var_ty is not None and self.Gen.StmtGenDecl._is_void_type(var_ty) != 0: - self.Gen.TypeGenErr.fatal_error(aa, f"cannot declare a variable of void type '{nm.id}' (void may only be used for void* and typedef aliases)", "TypeError") - - # t.Callable function pointer: register the return-type signature - callable_ret: mlirlite.LLVMType* = self.Gen.TypeGenErr.try_extract_callable_ret(pool, annot, trans._imported_modules, trans._from_imports, trans) - if callable_ret is not None: - # Nonzero return already reported inside register_func_ptr_sig; this branch is defensive - if self.Gen.CallGenLeafKit.register_func_ptr_sig(pool, nm.id, callable_ret, aa) != 0: return 0 - - # Value-position volatile call prefix: mark the result slot volatile - if aa.value is not None and aa.value.kind() == ast.ASTKind.Call and (ast.Call*(aa.value)).IsVolatile != 0 and nm.id is not None: - if self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) is not None: self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) - - # global variable: write to the module-scope global - if GenApi.is_global_name(trans, nm.id) != 0: - mod_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) - # 栈地址逃逸:`global G; G: T = &local`,或 `G: T* = arr`(数组退让)——与普通 - # 赋值同口径,禁止把本帧栈地址写出;目标为同型聚合时按值拷贝,不在此列。 - if aa.value is not None: - tgt_aa: mlirlite.LLVMType* = None - if mod_alloca is not None and mod_alloca.Ty is not None: tgt_aa = mod_alloca.Ty.Pointee - GenApi.fail_if_stack_addr_escape(aa.value, "a module-level global", trans, tgt_aa) - if mod_alloca is not None and aa.value is not None: - # P1-A context width propagation: pass the target type so integer literals materialize at target width - aa_glob_ty: mlirlite.LLVMType* = None - if mod_alloca.Ty is not None: aa_glob_ty = mod_alloca.Ty.Pointee - # Set CurAssignAnnot/CurAssignNode before translating the RHS (kept in sync across all four branches) - trans.CurAssignAnnot = aa.annotation - trans.CurAssignNode = aa.value - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, aa_glob_ty, "module-level global") - if rhs_val is not None: - # Object global with static storage: bind storage and copy the object - stg_ref_aa: mlirlite.Value* = None - if nm.id is not None: - stg_key_aa: str = pool.alloc(sizet(string.strlen(nm.id)) + 8) - if stg_key_aa is not None: - viperlib.snprintf(stg_key_aa, sizet(string.strlen(nm.id)) + 8, "%s.__stg", nm.id) - stg_ref_aa = self.Gen.VarGen.lookup_module_var(trans.SymTab, stg_key_aa) - if self._is_addrlike_call(aa.value) == 0 and stg_ref_aa is not None and rhs_val.Ty is not None and stg_ref_aa.Ty is not None and stg_ref_aa.Ty.Pointee != 0: - stg_ty_aa: mlirlite.LLVMType* = stg_ref_aa.Ty.Pointee - rhs_inner_aa: mlirlite.LLVMType* = None - match rhs_val.Ty: - case mlirlite.LLVMType.Ptr(pointee_aa): - rhs_inner_aa = pointee_aa - case _: - pass - if rhs_inner_aa is not None and rhs_inner_aa.kind() == mlirlite.LLVMType.Struct and self._stg_ty_equal(pool, stg_ty_aa, rhs_inner_aa) != 0: - mlirlite.build_store(builder, stg_ref_aa, mod_alloca) - stg_obj_val_aa: mlirlite.Value* = mlirlite.build_load(builder, stg_ty_aa, rhs_val) - if stg_obj_val_aa is not None: mlirlite.build_store(builder, stg_obj_val_aa, stg_ref_aa) - # Clear the assignment context before returning (avoid leaking into later statements) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - return 0 - target_ty: mlirlite.LLVMType* = None - if mod_alloca.Ty is not None: target_ty = mod_alloca.Ty.Pointee - if target_ty is not None: - # str/bytes is not a generic pointer: bare str/bytes annotation with an aggregate-pointer RHS - self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val, aa.annotation, node) - rhs_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, node, trans) - # t.CVolatile global: the initializing store carries the volatile qualifier - vol_gaa: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) - mlirlite.build_store(builder, rhs_val, mod_alloca, vol_gaa) - # Clear the assignment context (all four branches must stay in sync) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - return 0 - - # nonlocal variable: write through the closure env - if GenApi.is_nonlocal_name(trans, nm.id) != 0: - if aa.value is not None: - # P1-A: fetch the nonlocal slot pointer first so its type can be the target type - nl_ptr: mlirlite.Value* = self.Gen.StmtGenDecl.get_nonlocal_var_ptr(trans, nm.id) - # Infer the nonlocal type from nl_ptr instead of forcing i32 - nl_target_ty: mlirlite.LLVMType* = None - if nl_ptr is not None and nl_ptr.Ty is not None: nl_target_ty = nl_ptr.Ty.Pointee - # Set CurAssignAnnot/CurAssignNode before translating the RHS (sync across all four branches) - trans.CurAssignAnnot = aa.annotation - trans.CurAssignNode = aa.value - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, nl_target_ty, "closure variable (nonlocal)") - if rhs_val is not None: - if nl_ptr is not None: - if nl_target_ty is None: - # fail-fast: nl_ptr type lost, cannot store safely - VLogger.error(f"nonlocal variable type information lost, cannot write: {nm.id}", "HandlesAnnAssign") - # Clear the assignment context before returning (avoid leaking into later statements) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - return 0 - self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val, aa.annotation, node) - rhs_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, nl_target_ty, node) - mlirlite.build_store(builder, rhs_coerced, nl_ptr) - # Clear the assignment context (all four branches must stay in sync) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - return 0 - - # Module-level global: only module code modifies it; in a function without global, create a local - if self.Gen.VarGen.is_module_scope(trans.SymTab) != 0: mod_glob: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) - else: mod_glob = None - if mod_glob is not None: - if aa.value is not None: - # List literal: initialize the array with per-element GEP + store - if aa.value.kind() == ast.ASTKind.List: self._init_global_array_from_list(builder, pool, mod, mod_glob, aa.value, trans) - else: - # P1-A: use the module-level global type as the target type - mg_val_ty: mlirlite.LLVMType* = None - if mod_glob.Ty is not None: mg_val_ty = mod_glob.Ty.Pointee - # Set CurAssignAnnot/CurAssignNode before translating the RHS (sync across all four branches) - trans.CurAssignAnnot = aa.annotation - trans.CurAssignNode = aa.value - rhs_val_mg: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, mg_val_ty, "module-level global") - if rhs_val_mg is not None: - # Object global with static storage: bind storage and copy the object - stg_ref_mg: mlirlite.Value* = None - if nm.id is not None: - stg_key_mg: str = pool.alloc(sizet(string.strlen(nm.id)) + 8) - if stg_key_mg is not None: - viperlib.snprintf(stg_key_mg, sizet(string.strlen(nm.id)) + 8, "%s.__stg", nm.id) - stg_ref_mg = self.Gen.VarGen.lookup_module_var(trans.SymTab, stg_key_mg) - if self._is_addrlike_call(aa.value) == 0 and stg_ref_mg is not None and rhs_val_mg.Ty is not None and stg_ref_mg.Ty is not None and stg_ref_mg.Ty.Pointee != 0: - stg_ty_mg: mlirlite.LLVMType* = stg_ref_mg.Ty.Pointee - rhs_inner_mg: mlirlite.LLVMType* = None - match rhs_val_mg.Ty: - case mlirlite.LLVMType.Ptr(pointee_mg): - rhs_inner_mg = pointee_mg - case _: - pass - if rhs_inner_mg is not None and rhs_inner_mg.kind() == mlirlite.LLVMType.Struct and self._stg_ty_equal(pool, stg_ty_mg, rhs_inner_mg) != 0: - mlirlite.build_store(builder, stg_ref_mg, mod_glob) - stg_obj_val_mg: mlirlite.Value* = mlirlite.build_load(builder, stg_ty_mg, rhs_val_mg) - if stg_obj_val_mg is not None: mlirlite.build_store(builder, stg_obj_val_mg, stg_ref_mg) - # Clear the assignment context before returning (avoid leaking into later statements) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - return 0 - target_ty_mg: mlirlite.LLVMType* = None - if mod_glob.Ty is not None: target_ty_mg = mod_glob.Ty.Pointee - if target_ty_mg is not None: - # str/bytes is not a generic pointer: bare str/bytes annotation with an aggregate-pointer RHS - self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val_mg, aa.annotation, node) - rhs_val_mg = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val_mg, target_ty_mg, node) - # t.CVolatile module-level global: the initializing store carries volatile - vol_mga: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) - mlirlite.build_store(builder, rhs_val_mg, mod_glob, vol_mga) - # Clear the assignment context (all four branches must stay in sync) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - return 0 - - # Single-param CArray + list literal -> static array; match PreScan so the alloca is reused - var_ty = self._infer_static_array_ty_from_literal(pool, annot, aa.value, var_ty) - # [D31 fail-fast] generic annotation degraded to a bare base class - self._fail_on_degenerated_generic_annotation(aa, nm.id, annot, var_ty) - - # Ordinary local variable: create the alloca - alloca: mlirlite.Value* = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, nm.id, var_ty) - if alloca is None: - # fail-fast: storage creation failed - self.Gen.TypeGenErr.fatal_error(aa, f"cannot create storage for variable {nm.id}", "TypeError") - # A reused alloca type must match the resolved annotation type (compare struct names) - if alloca is not None and alloca.Ty is not None and var_ty is not None: - av_pt_d: mlirlite.LLVMType* = alloca.Ty.Pointee - if av_pt_d is not None and self.Gen.ExprGenBase.is_ptr_type(av_pt_d) != 0 and self.Gen.ExprGenBase.is_ptr_type(var_ty) != 0: - av_in: mlirlite.LLVMType* = av_pt_d.Pointee - vt_in: mlirlite.LLVMType* = var_ty.Pointee - if av_in is not None and vt_in is not None: - av_nm_d: str = mlirlite.get_struct_name(av_in) - vt_nm_d: str = mlirlite.get_struct_name(vt_in) - if av_nm_d is not None and vt_nm_d is not None and string.strcmp(av_nm_d, vt_nm_d) != 0: - # Fail fast only when the reused pointee is a bare generic base (lost type args) - av_pf: int = GenApi.sha1_prefix_len(av_nm_d) - av_short: str = av_nm_d + av_pf if av_pf != 0 else av_nm_d - if string.strchr(av_short, '[') is None: - av_tpl: ast.ClassDef* = self.Gen.ClassGenGeneric._find_generic_template(av_short) - if av_tpl is not None: - self.Gen.TypeGenErr.fatal_error(aa, f"the reused storage type of variable '{nm.id}' is the bare generic base class '{av_nm_d}*'" f" (type arguments lost), inconsistent with the annotation-resolved type '{vt_nm_d}*'; " f"silent mixing is forbidden -- method calls would specialize by the wrong element type and truncate the pointer", "TypeError") - - # FakeDuck: a str variable gets a shadow struct {var}__meta__ for per-instance fields - fd_str_ty: mlirlite.LLVMType* = None - fd_meta_alloca: mlirlite.Value* = None - fd_meta_name: str = None - if annot is not None and annot.kind() == ast.ASTKind.Name: - fd_annot: ast.Name* = ast.Name*(annot) - if fd_annot is not None and fd_annot.id is not None: - if string.strcmp(fd_annot.id, "str") == 0: fd_str_ty = self.Gen.StructGen.get_struct_type("_str") - if fd_str_ty is not None: - # Ensure the full _str definition is available (opaque types cannot be alloca'd) - self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, fd_str_ty) - # Shadow variable name must use a pool buffer (f-string stack buffers get overwritten) - fd_meta_name = pool.alloc(256) - if fd_meta_name is not None: viperlib.snprintf(fd_meta_name, 256, "%s__meta__", nm.id) - fd_meta_alloca = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, fd_meta_name, fd_str_ty) - if fd_meta_alloca is None: - # fail-fast: shadow storage creation failed - self.Gen.TypeGenErr.fatal_error(aa, f"cannot create storage for the str shadow variable {fd_meta_name}", "TypeError") - # Store the annotation's class name; pass pool so generic Subscript yields the mangled name - if annot is not None: - cls_nm_hd: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(annot, trans._imported_modules, pool, trans._from_imports) - if cls_nm_hd is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, nm.id, cls_nm_hd) - # Check for bytes* or str* (i8** semantics, 8-byte stride) - if self._is_ptr_element_annotation(annot) != 0: self.Gen.VarGen.set_var_ptr_element(trans.SymTab, nm.id) - - new_vars: int = 0 - existing: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) - if existing is None: new_vars = 1 - - # If there is an initial value, store it - if aa.value is not None: - # Coroutine: record the variable name assigned from an async call (for frame-type lookup) - if aa.value.kind() == ast.ASTKind.Call: - val_cl: ast.Call* = ast.Call*(aa.value) - if val_cl.func is not None and val_cl.func.kind() == ast.ASTKind.Name: - val_nm: ast.Name* = ast.Name*(val_cl.func) - if val_nm.id is not None: - if self.Gen.AsyncGen.is_async_func(trans, val_nm.id) != 0: self.Gen.AsyncGen.register_coro_var(trans, nm.id, val_nm.id) - # c.Addr(async_call): interpret the tt: Coroutine = c.Addr(task()) form in place - elif val_cl.func is not None and val_cl.func.kind() == ast.ASTKind.Attribute: - val_at: ast.Attribute* = ast.Attribute*(val_cl.func) - if val_at.attr is not None and string.strcmp(val_at.attr, "Addr") == 0 and val_cl.args is not None and len(val_cl.args) >= 1: - icall: ast.AST* = val_cl.args.get(0) - if icall is not None and icall.kind() == ast.ASTKind.Call: - icl2: ast.Call* = ast.Call*(icall) - if icl2.func is not None and icl2.func.kind() == ast.ASTKind.Name: - inm2: ast.Name* = ast.Name*(icl2.func) - if inm2.id is not None: - if self.Gen.AsyncGen.is_async_func(trans, inm2.id) != 0: self.Gen.AsyncGen.register_coro_var(trans, nm.id, inm2.id) - # Local array from a list literal: zero it first, then per-element GEP + store (no list[T]) - local_arr_ty: mlirlite.LLVMType* = None - if aa.value.kind() == ast.ASTKind.List and alloca.Ty is not None: - pt_l: mlirlite.LLVMType* = alloca.Ty.Pointee - if self.Gen.ExprGenBase.is_array_type(pt_l) != 0: local_arr_ty = pt_l - if local_arr_ty is not None: - zero_arr: mlirlite.Value* = mlirlite.ConstZero(pool, local_arr_ty) - if zero_arr is not None: mlirlite.build_store(builder, zero_arr, alloca) - self._init_global_array_from_list(builder, pool, mod, alloca, aa.value, trans) - else: - # Set CurAssignAnnot/CurAssignNode for bare generic construction (sync across all four branches) - trans.CurAssignAnnot = aa.annotation - trans.CurAssignNode = aa.value - # P1-A: use the local alloca pointee as the target type so integer literals materialize at target width - lv_target_ty: mlirlite.LLVMType* = None - if alloca.Ty is not None: lv_target_ty = alloca.Ty.Pointee - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, lv_target_ty, "local variable annotation") - if rhs_val is not None: - # Fix degraded annotation: rebuild the alloca with the precise RHS struct type - precise_rhs_ty: mlirlite.LLVMType* = None - if rhs_val.Ty is not None: - match rhs_val.Ty: - case mlirlite.LLVMType.Ptr(inner_struct): - match inner_struct: - case mlirlite.LLVMType.Struct(_, _, _): - precise_rhs_ty = rhs_val.Ty - case _: - pass - case _: - pass - if precise_rhs_ty is not None and alloca.Ty is not None: - alloca_pt: mlirlite.LLVMType* = alloca.Ty.Pointee - # Forbid downgrade rebuild: do not rebuild when the RHS pointee is a bare generic base - ann_gen_base: str = None - if annot is not None: - ag_side: ast.AST* = annot - if ag_side.kind() == ast.ASTKind.BinOp: - ag_bop: ast.BinOp* = ast.BinOp*(ag_side) - if ag_bop.op == ast.OpKind.BitOr: - if ag_bop.left is not None and ag_bop.left.kind() == ast.ASTKind.Subscript: ag_side = ag_bop.left - elif ag_bop.right is not None and ag_bop.right.kind() == ast.ASTKind.Subscript: ag_side = ag_bop.right - if ag_side.kind() == ast.ASTKind.Subscript: - ag_sub: ast.Subscript* = ast.Subscript*(ag_side) - if ag_sub.value is not None and ag_sub.value.kind() == ast.ASTKind.Name: - ag_nm: ast.Name* = ast.Name*(ag_sub.value) - if ag_nm.id is not None: ann_gen_base = ag_nm.id - rb_is_bare_generic: int = 0 - if alloca_pt is not None and uint64(alloca_pt) != uint64(precise_rhs_ty): - rhs_pt_rb: mlirlite.LLVMType* = precise_rhs_ty.Pointee - if rhs_pt_rb is not None: - rb_nm: str = mlirlite.get_struct_name(rhs_pt_rb) - if rb_nm is not None: - rb_pf: int = GenApi.sha1_prefix_len(rb_nm) - rb_short: str = rb_nm + rb_pf if rb_pf != 0 else rb_nm - if string.strchr(rb_short, '[') is None: - # (1) The bare name hits the generic template registry (list/GSList/...) - if self.Gen.ClassGenGeneric._find_generic_template(rb_short) is not None: rb_is_bare_generic = 1 - # (2) Same base name as the annotation => rebuild would downgrade; forbid it - elif ann_gen_base is not None and string.strcmp(rb_short, ann_gen_base) == 0: rb_is_bare_generic = 1 - # Forbid narrowing an upcast: annotated base pointer with an RHS derived pointer must not be narrowed - rb_is_upcast: int = 0 - if alloca_pt is not None and uint64(alloca_pt) != uint64(precise_rhs_ty) and self.Gen.ExprGenBase.is_ptr_type(alloca_pt) != 0: - ann_pt_rb: mlirlite.LLVMType* = alloca_pt.Pointee - rhs_pt_up: mlirlite.LLVMType* = precise_rhs_ty.Pointee - if ann_pt_rb is not None and rhs_pt_up is not None and uint64(ann_pt_rb) != uint64(rhs_pt_up): - if self.Gen.ExprGenBase._is_inheritance_upcast(pool, rhs_pt_up, ann_pt_rb) != 0: rb_is_upcast = 1 - if alloca_pt is not None and uint64(alloca_pt) != uint64(precise_rhs_ty) and rb_is_bare_generic == 0 and rb_is_upcast == 0: - new_alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, precise_rhs_ty) - if new_alloca is not None: - v_entry_rb: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) - if v_entry_rb is not None: - v_entry_rb.Alloca = new_alloca - alloca = new_alloca - # Type coercion: convert rhs_val to the alloca pointee (integer literals may need trunc/sext) - target_ty: mlirlite.LLVMType* = None - if alloca.Ty is not None: target_ty = alloca.Ty.Pointee - if target_ty is not None: - # str/bytes is not a generic pointer: bare str/bytes annotation with an aggregate-pointer RHS - self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val, aa.annotation, node) - rhs_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, node, trans) - # t.CVolatile local variable: mark the VarEntry and store with volatile - vol_laa: int = 0 - if aa.annotation is not None and nm.id is not None: - # const object local variable: mark VarEntry.IsConst (idempotent with prescan) - if self.Gen.TypeGenErr.is_const_object_marker(aa.annotation, target_ty) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, nm.id) - if self.Gen.TypeGenErr.has_decorator_marker(aa.annotation, "CVolatile", 0) != 0: - self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) - vol_laa = 1 - # rawptr[t.CVolatile | T]: mark PointeeVolatile (this store writes the pointer itself) - if self.Gen.TypeGenErr.has_pointee_volatile_marker(aa.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, nm.id) - # Value-position volatile call prefix: the result slot is accessed with volatile - if aa.value is not None and aa.value.kind() == ast.ASTKind.Call and (ast.Call*(aa.value)).IsVolatile != 0: - self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) - vol_laa = 1 - # M3 所有权记账:必须**先**记账再写值——覆盖释放要读到槽里的旧值 - # (`p: T* = new heap slife …` → 释放旧对象 + 影子置 1;`q: T* = p` → 交还所有权) - self._slife_note_assign(pool, builder, mod, trans, alloca, aa.value, nm.id) - mlirlite.build_store(builder, rhs_val, alloca, vol_laa) - # P2 willFree:带注解声明/赋值后清除失效态(新变量本就为 0;复用槽兜底) - self.Gen.VarGen.clear_var_freed(trans.SymTab, nm.id) - # FakeDuck: initialize the shadow struct __data__ field with the initial string pointer - if fd_str_ty is not None and fd_meta_alloca is not None: - fd_data_fe: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(fd_str_ty, "__data__") - if fd_data_fe is not None: - fd_data_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, fd_str_ty, fd_data_fe.Ty, fd_meta_alloca, fd_data_fe.Index) - if fd_data_ptr is not None: mlirlite.build_store(builder, rhs_val, fd_data_ptr) - # Clear the assignment context after the RHS translation (sync across all four branches) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - - # FakeDuck: initialize the __mbuddy__ field to NULL - if fd_str_ty is not None and fd_meta_alloca is not None: - fd_buddy_fe: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(fd_str_ty, "__mbuddy__") - if fd_buddy_fe is not None and fd_buddy_fe.Ty is not None: - fd_null: mlirlite.Value* = mlirlite.ConstNull(pool, fd_buddy_fe.Ty, "null") - fd_buddy_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, fd_str_ty, fd_buddy_fe.Ty, fd_meta_alloca, fd_buddy_fe.Index) - if fd_buddy_ptr is not None and fd_null is not None: mlirlite.build_store(builder, fd_null, fd_buddy_ptr) - # FakeDuck: auto-inject requires fields (e.g. __mbuddy__) for the with context - self.Gen.CallGenFd._inject_requires_fields(trans, pool, builder, mod, fd_meta_alloca, "_str") - - # 生命期传播(本地变量):`s: str = l` → Life(s) = Life(l)(数组退让即借用) - if aa.value is not None and nm.id is not None and alloca is not None: - tgt_life_ty: mlirlite.LLVMType* = None - if alloca.Ty is not None: tgt_life_ty = alloca.Ty.Pointee - self.propagate_life(trans, target, aa.value, tgt_life_ty) - - return 0 - - # _translate_lvalue_annassign - 注解赋值的**属性/下标目标**:`obj.f: T = v` / `arr[i]: T = v`。 - # 与 Name 目标同口径:注解是源码级权威类型(RHS 以它为靶型求值);并且必须过 P3 - # "长命左值不得接收本帧栈地址"的检查(= propagate_life 的非 Name 分支)。 - # 存储目标复用既有左值指针求解:Attribute → get_attribute_ptr,Subscript → get_subscript_ptr。 - def _translate_lvalue_annassign(self, trans: HT.Translator*, aa: ast.AnnAssign*, target: ast.AST*) -> int: - """注解赋值的属性/下标目标;0=完成""" - if trans is None or aa is None or target is None: return 0 - tk: int = target.kind() - if tk != ast.ASTKind.Attribute and tk != ast.ASTKind.Subscript: - self.Gen.TypeGenErr.fatal_error(target, "the target of an annotated assignment must be a variable, an attribute or a subscript", "SyntaxError") - return 0 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - mod: mlirlite.LLVMModule* = trans.Module - annot: ast.AST* = aa.annotation - if annot is None: - self.Gen.TypeGenErr.fatal_error(aa, "AnnAssign is missing a type annotation", "SyntaxError") - return 0 - # 与 Name 路径同口径解析注解:先特化泛型(避免 list[X] 退化成裸 list*),解析失败即 fail-fast。 - self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "an annotated assignment to an attribute/subscript") - self._prespecialize_generic_annotation(trans, pool, annot) - ann_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0) - if ann_ty is None: - self.Gen.TypeGenErr.fatal_error(aa, "cannot resolve the annotation type of an annotated assignment to an attribute/subscript", "TypeError") - return 0 - if self.Gen.StmtGenDecl._is_void_type(ann_ty) != 0: - self.Gen.TypeGenErr.fatal_error(aa, "cannot assign to an attribute/subscript of void type (void may only be used for void* and typedef aliases)", "TypeError") - return 0 - # 左值存储指针:属性/下标各走既有求解器。失败一律 fail-fast——绝不静默丢掉整条语句。 - is_attr: int = 0 - ptr: mlirlite.Value* = None - if tk == ast.ASTKind.Attribute: - is_attr = 1 - at_lv: ast.Attribute* = ast.Attribute*(target) - if at_lv.attr is not None and self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, target) != 0: - self.const_assign_fail(target, "field assignment", at_lv.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, target)) - ptr = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, target, trans) - else: - ptr = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, target, trans) - if ptr is None or ptr.Ty is None or ptr.Ty.Pointee == 0: - self.Gen.TypeGenErr.fatal_error(target, "cannot resolve the storage of the annotated assignment target (the attribute/subscript pointer or its element type is missing)", "TypeError") - return 0 - target_ty: mlirlite.LLVMType* = ptr.Ty.Pointee - # P3:左值存储比右值长命时,右值不得交出本帧栈地址(如 `h.p: T* = &local`)。 - self._fail_if_lvalue_outlives_rhs(trans, target, aa.value, target_ty) - # 与普通 Assign 同口径:f-string 结果是函数内临时量,写出即悬垂。 - if aa.value is not None: - if is_attr != 0: GenApi.fail_if_fstring_escape(aa.value, "field assignment") - else: GenApi.fail_if_fstring_escape(aa.value, "element assignment") - # RHS 以注解靶型求值(CurAssignAnnot/CurAssignNode 供裸泛型构造推断;求值完即复位,不跨语句泄漏)。 - trans.CurAssignAnnot = annot - trans.CurAssignNode = aa.value - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, ann_ty, "annotated assignment to an attribute/subscript") - trans.CurAssignAnnot = None - trans.CurAssignNode = None - if rhs_val is None: - self.Gen.TypeGenErr.fatal_error(aa, "the right-hand side of the annotated assignment to an attribute/subscript could not be translated", "TypeError") - return 0 - store_val: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, aa, trans) - if store_val is None: - self.Gen.TypeGenErr.fatal_error(aa, "type coercion failed for the annotated assignment to an attribute/subscript", "TypeError") - return 0 - # t.CVolatile 字段 / rawptr[t.CVolatile|T] 基址:写出同样带 volatile。 - vol_lv: int = 0 - if is_attr != 0: - vol_lv = self.Gen.ExprGenAttr.attr_field_is_volatile(pool, trans, target) - else: - sub_lv: ast.Subscript* = ast.Subscript*(target) - if sub_lv.value is not None: vol_lv = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, sub_lv.value) - mlirlite.build_store(builder, store_val, ptr, vol_lv) - return 0 - - # translate_assign - handle an Assign statement; returns the number of new variables - def translate_assign(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Translate an Assign statement; return the number of new variables.""" - - asgn: ast.Assign* = ast.Assign*(node) - if asgn is None: - VLogger.error("cast failed", "ASGN") - return 0 - - targets: list[ast.AST*]* = asgn.targets - if targets is None: - VLogger.error("targets is None", "ASGN") - return 0 - - # Read shared state from trans (replaces 11 parameters) - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - mod: mlirlite.LLVMModule* = trans.Module - - # const target: fail fast before assignment (Name/Attribute/Subscript) - for ti_c in range(int(len(targets))): - tgt_c: ast.AST* = targets.get(ti_c) - if tgt_c is None: continue - if tgt_c.kind() == ast.ASTKind.Name: - nm_c: ast.Name* = ast.Name*(tgt_c) - if nm_c.id is None: continue - ve_c: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_c.id) - if self.Gen.VarGen.var_is_const(ve_c) != 0: - self.const_assign_fail(tgt_c, "assignment", nm_c.id, self.Gen.VarGen.var_annot_class_name(ve_c)) - elif tgt_c.kind() == ast.ASTKind.Attribute: - at_c: ast.Attribute* = ast.Attribute*(tgt_c) - if at_c.attr is None: continue - if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, tgt_c) != 0: - self.const_assign_fail(tgt_c, "field assignment", at_c.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, tgt_c)) - elif tgt_c.kind() == ast.ASTKind.Subscript: - sub_c: ast.Subscript* = ast.Subscript*(tgt_c) - if sub_c.value is None: continue - if sub_c.value.kind() == ast.ASTKind.Name: - nm_c2: ast.Name* = ast.Name*(sub_c.value) - if nm_c2.id is None: continue - ve_c2: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_c2.id) - if self.Gen.VarGen.var_is_const(ve_c2) != 0: - self.const_assign_fail(tgt_c, "element assignment", nm_c2.id, self.Gen.VarGen.var_annot_class_name(ve_c2)) - elif sub_c.value.kind() == ast.ASTKind.Attribute: - at_c2: ast.Attribute* = ast.Attribute*(sub_c.value) - if at_c2.attr is None: continue - if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, sub_c.value) != 0: - self.const_assign_fail(tgt_c, "element assignment", at_c2.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, sub_c.value)) - - # f-string escape: results are function-local temporaries; writing them out is dangling - for ti_f in range(int(len(targets))): - tgt_f: ast.AST* = targets.get(ti_f) - if tgt_f is None: continue - tk_f: int = tgt_f.kind() - if tk_f == ast.ASTKind.Attribute: GenApi.fail_if_fstring_escape(asgn.value, "field assignment") - elif tk_f == ast.ASTKind.Subscript: GenApi.fail_if_fstring_escape(asgn.value, "element assignment") - elif tk_f == ast.ASTKind.Name: - nm_f: ast.Name* = ast.Name*(tgt_f) - if nm_f.id is not None and GenApi.is_global_name(trans, nm_f.id) != 0: - GenApi.fail_if_fstring_escape(asgn.value, "global variable assignment") - # 栈地址逃逸:`G = &local` 与 `G = arr`(G 为指针时的数组退让)都把本帧栈地址 - # 写进模块级全局,函数返回后悬垂;`G = arr` 且 G 为同型聚合时是值拷贝,不在此列。 - gv_sa: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm_f.id) - tgt_sa: mlirlite.LLVMType* = None - if gv_sa is not None and gv_sa.Ty is not None: tgt_sa = gv_sa.Ty.Pointee - GenApi.fail_if_stack_addr_escape(asgn.value, "a module-level global", trans, tgt_sa) - - # Translate the RHS; clear CurAssignAnnot/CurAssignNode (no target type to infer here) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - # P1-A: for a single Name target with a known storage type, pass it as the target type - asg_target_ty: mlirlite.LLVMType* = None - if len(targets) == 1: - asg_t0: ast.AST* = targets.get(0) - if asg_t0 is not None and asg_t0.kind() == ast.ASTKind.Name: - asg_nm0: ast.Name* = ast.Name*(asg_t0) - if asg_nm0.id is not None: - asg_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, asg_nm0.id) - if asg_slot is None: asg_slot = self.Gen.VarGen.lookup_module_var(trans.SymTab, asg_nm0.id) - if asg_slot is not None and asg_slot.Ty is not None: asg_target_ty = asg_slot.Ty.Pointee - elif trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: - # REPL: a typed session variable also uses its slot type as the target type - asg_re: GenTypes.ReplVarEntry* = self.Gen.VarGen.repl_find_entry(trans.ReplCtx, asg_nm0.id) - if asg_re is not None and asg_re.Kind != GenTypes.REPL_KIND_UNTYPED: asg_target_ty = self.Gen.VarGen.repl_slot_ty(pool, asg_re) - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, asgn.value, None, 0, trans, asg_target_ty) - if rhs_val is None: - # RHS is None usually means allocation failure; dump both pool statistics - mb_diag: memhub.MemBuddy* = CompilerFactory.factory.Pool - pp_diag: memhub.MemBuddy* = CompilerFactory.factory.PermPool - if mb_diag is not None: mb_diag.dump_stats("Main Pool(Pool)") - if pp_diag is not None: pp_diag.dump_stats("Permanent Pool(PermPool)") - stdio.fflush(None) - # Enhanced error message: file name + lineno + AST node details - if asgn.value is not None: - sha1: str = trans.ModuleSha1 - val_kind: int = asgn.value.kind() - val_line: int = asgn.value.lineno - # Look up the file name from sha1 (human readable) - rel_path: str = None - if sha1 is not None: rel_path = StubMergerStore.LookupSha1RelPath(sha1) - # Extract details by AST node kind - if val_kind == ast.ASTKind.Attribute: - at_nv: ast.Attribute* = ast.Attribute*(asgn.value) - attr_nm: str = at_nv.attr - target_nm: str = "?" - if at_nv.value is not None: - if at_nv.value.kind() == ast.ASTKind.Name: - tn_nm: ast.Name* = ast.Name*(at_nv.value) - target_nm = tn_nm.id - if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} attribute access of '{target_nm}.{attr_nm}' failed", "ASGN") - else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} attribute access of '{target_nm}.{attr_nm}' failed", "ASGN") - elif val_kind == ast.ASTKind.Name: - nm_nv: ast.Name* = ast.Name*(asgn.value) - if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} variable '{nm_nv.id}' not found", "ASGN") - else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} variable '{nm_nv.id}' not found", "ASGN") - elif val_kind == ast.ASTKind.Call: - cl_nv: ast.Call* = ast.Call*(asgn.value) - func_nm: str = "?" - if cl_nv.func is not None: - if cl_nv.func.kind() == ast.ASTKind.Name: - fn_nm: ast.Name* = ast.Name*(cl_nv.func) - func_nm = fn_nm.id - if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} call '{func_nm}(...)' returned None", "ASGN") - else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} call '{func_nm}(...)' returned None", "ASGN") - else: - # 必须与相邻分支一致显式传 category:跨模块模块级函数调用目前不会补默认值, - # 漏传会生成操作数不足的 llvm.call(MLIR 校验直接失败)。 - if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} [kind={val_kind}]", "ASGN") - else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} [kind={val_kind}]", "ASGN") - else: VLogger.error("assignment rhs is None (asgn.value is empty)", "ASGN") - return 0 - - # 生命期传播(单 Name 目标):`s = l` → Life(s) = Life(l) - if len(targets) == 1: - t0_sl: ast.AST* = targets.get(0) - self.propagate_life(trans, t0_sl, asgn.value, asg_target_ty) - - new_vars: int = 0 - tn: sizet = len(targets) - for ti in range(int(tn)): - target: ast.AST* = targets.get(ti) - if target is None: continue - - tk: int = target.kind() - - # Tuple/List assignment: unpack a struct return value into Name targets - if tk == ast.ASTKind.Tuple or tk == ast.ASTKind.List: - tgt_tup: ast.Tuple* = ast.Tuple*(target) - tgt_elts: list[ast.AST*]* = tgt_tup.elts - # Guard: skip this target when rhs is not a struct (tuple) - if tgt_elts is None or rhs_val is None or rhs_val.Ty is None: continue - match rhs_val.Ty: - case mlirlite.LLVMType.Struct(_, _, _): - tgt_n: sizet = len(tgt_elts) - tmp_s: mlirlite.Value* = mlirlite.build_alloca(builder, rhs_val.Ty) - if tmp_s is not None: - mlirlite.build_store(builder, rhs_val, tmp_s) - fchain: mlirlite.ParamNode* = mlirlite.get_struct_fields(rhs_val.Ty) - fcur: mlirlite.ParamNode* = fchain - fti: sizet - for fti in range(int(tgt_n)): - tgt_elt: ast.AST* = tgt_elts.get(fti) - if tgt_elt is None or fcur is None: break - fty: mlirlite.LLVMType* = mlirlite.paramnode_get_ty(fcur) - fcur = mlirlite.paramnode_get_next(fcur) - if fty is None: - # fail-fast: field type missing - self.Gen.TypeGenErr.fatal_error(target, "tuple-unpack field type missing", "TypeError") - # Guard: skip non-Name elements (cannot unpack) - if tgt_elt.kind() != ast.ASTKind.Name: continue - tgt_nm2: ast.Name* = ast.Name*(tgt_elt) - if tgt_nm2.id is None: continue - fp2: mlirlite.Value* = mlirlite.build_gep_struct(builder, rhs_val.Ty, fty, tmp_s, fti) - if fp2 is None: continue - fv2: mlirlite.Value* = mlirlite.build_load(builder, fty, fp2) - tgt_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, tgt_nm2.id) - if tgt_alloca is None: tgt_alloca = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, tgt_nm2.id, fty) - if tgt_alloca is None: continue - sval: mlirlite.Value* = fv2 - if tgt_alloca.Ty is not None and tgt_alloca.Ty.Pointee != 0: sval = self.Gen.ExprGenBase.coerce_to_type(builder, fv2, tgt_alloca.Ty.Pointee, node, trans) - mlirlite.build_store(builder, sval, tgt_alloca) - case _: - pass - continue - - # Subscript assignment: arr[i] = val / ptr[i] = val / list[i] = val - if tk == ast.ASTKind.Subscript: - # Check for a list[T] Subscript; list subscript assignment uses the inline __setitem__ path - list_obj: mlirlite.Value* = self.Gen.ExprGenName.is_list_subscript(target, trans) - if list_obj is not None: - # list[T]: generate the __setitem__ logic inline - sub_node: ast.Subscript* = ast.Subscript*(target) - if sub_node is not None and sub_node.slice is not None: - list_idx_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_node.slice, None, 0, trans) - if list_idx_val is None: - # fail-fast: list index translation failed - self.Gen.TypeGenErr.fatal_error(target, "list index translation failed", "TypeError") - self.Gen.ExprGenName.list_setitem_inline(builder, pool, list_obj, list_idx_val, rhs_val, target) - continue - # Ordinary Subscript assignment - elem_ptr: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, target, trans) - if elem_ptr is not None: - store_val: mlirlite.Value* = rhs_val - # Fail fast when a rawptr[T] element is an object but RHS is a T pointer (whole-object copy) - self._reject_whole_object_copy_into_ptr_slot(pool, trans, target, elem_ptr, rhs_val) - # Guard: coerce only when the element type is complete - if elem_ptr.Ty is not None and elem_ptr.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, elem_ptr.Ty.Pointee, node, trans) - # rawptr[t.CVolatile|T] base: p[i] = v stores with volatile when the base is resolvable - vol_sw: int = 0 - sub_wr: ast.Subscript* = ast.Subscript*(target) - if sub_wr is not None: vol_sw = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, sub_wr.value) - mlirlite.build_store(builder, store_val, elem_ptr, vol_sw) - continue - # get_subscript_ptr returned None: try the __setitem__ overload for custom classes - sub_asgn: ast.Subscript* = ast.Subscript*(target) - setitem_done: int = 0 - if sub_asgn is not None and sub_asgn.value is not None: - if sub_asgn.value.kind() == ast.ASTKind.Name: - sub_nm: ast.Name* = ast.Name*(sub_asgn.value) - if sub_nm.id is not None: - sub_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, sub_nm.id) - if sub_alloca is not None and sub_alloca.Ty is not None and sub_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(sub_alloca.Ty) != 0: - sub_pointee: mlirlite.LLVMType* = sub_alloca.Ty.Pointee - cls_nm_set: str = self.Gen.StructGen.get_class_name_by_type(pool, sub_pointee) - obj_val_set: mlirlite.Value* = mlirlite.build_load(builder, sub_pointee, sub_alloca) - # Pointer variable (X*): sub_pointee is Ptr(Struct); load it to inspect the inner struct - if cls_nm_set is None and obj_val_set is not None and self.Gen.ExprGenBase.is_ptr_type(sub_pointee) != 0 and sub_pointee.Pointee != 0: - cls_nm_set = self.Gen.StructGen.get_class_name_by_type(pool, sub_pointee.Pointee) - if cls_nm_set is not None and obj_val_set is not None: - key_val_set: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.slice, None, 0, trans) - if key_val_set is not None: - arg_vals_set: sizet* = pool.alloc(16) - if arg_vals_set is not None: - arg_vals_set[0] = sizet(key_val_set) - arg_vals_set[1] = sizet(rhs_val) - self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, cls_nm_set, "__setitem__", obj_val_set, arg_vals_set, 2, trans) - setitem_done = 1 - elif sub_asgn.value.kind() == ast.ASTKind.Attribute: - # self._ht[key] = val -> get the field pointer then call __setitem__ - field_ptr_set: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, sub_asgn.value, trans) - if field_ptr_set is not None and field_ptr_set.Ty is not None and field_ptr_set.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(field_ptr_set.Ty) != 0: - field_pointee_set: mlirlite.LLVMType* = field_ptr_set.Ty.Pointee - cls_nm_attr: str = self.Gen.StructGen.get_class_name_by_type(pool, field_pointee_set) - obj_val_attr: mlirlite.Value* = field_ptr_set - # field_ptr is Ptr(Ptr(Struct)) for an X* field: load to get Ptr(Struct) - if cls_nm_attr is None and self.Gen.ExprGenBase.is_ptr_type(field_pointee_set) != 0 and field_pointee_set.Pointee != 0: - cls_nm_attr = self.Gen.StructGen.get_class_name_by_type(pool, field_pointee_set.Pointee) - if cls_nm_attr is not None: obj_val_attr = mlirlite.build_load(builder, field_pointee_set, field_ptr_set) - if cls_nm_attr is not None and obj_val_attr is not None: - key_val_attr: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.slice, None, 0, trans) - if key_val_attr is not None: - arg_vals_attr: sizet* = pool.alloc(16) - if arg_vals_attr is not None: - arg_vals_attr[0] = sizet(key_val_attr) - arg_vals_attr[1] = sizet(rhs_val) - self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, cls_nm_attr, "__setitem__", obj_val_attr, arg_vals_attr, 2, trans) - setitem_done = 1 - if setitem_done == 0: - # Generic fallback: translate sub.value and try __setitem__ or a direct GEP+store - if sub_asgn is not None and sub_asgn.value is not None: - obj_val_gen: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.value, None, 0, trans) - key_val_gen: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.slice, None, 0, trans) - if obj_val_gen is not None and key_val_gen is not None and obj_val_gen.Ty is not None: - # Inline the _get_custom_struct_cls_nm logic (avoid a new dependency) - cls_nm_gen: str = self.Gen.StructGen.get_class_name_by_type(pool, obj_val_gen.Ty) - if cls_nm_gen is None and self.Gen.ExprGenBase.is_ptr_type(obj_val_gen.Ty) != 0 and obj_val_gen.Ty.Pointee != 0: - cls_nm_gen = self.Gen.StructGen.get_class_name_by_type(pool, obj_val_gen.Ty.Pointee) - if cls_nm_gen is not None: - # Custom struct: call __setitem__ - arg_vals_gen: sizet* = pool.alloc(16) - if arg_vals_gen is not None: - arg_vals_gen[0] = sizet(key_val_gen) - arg_vals_gen[1] = sizet(rhs_val) - self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, cls_nm_gen, "__setitem__", obj_val_gen, arg_vals_gen, 2, trans) - setitem_done = 1 - elif self.Gen.ExprGenBase.is_ptr_type(obj_val_gen.Ty) != 0 and obj_val_gen.Ty.Pointee != 0: - # Non-custom struct: direct GEP + store (handles the None from get_subscript_ptr) - elem_gen: mlirlite.LLVMType* = obj_val_gen.Ty.Pointee - store_val_gen: mlirlite.Value* = rhs_val - elem_ptr_gen: mlirlite.Value* = None - match elem_gen: - case mlirlite.LLVMType.Array(arr_elem_gen, _): - elem_ptr_gen = mlirlite.build_gep_array(builder, elem_gen, arr_elem_gen, obj_val_gen, key_val_gen) - case _: - elem_ptr_gen = mlirlite.build_gep(builder, elem_gen, obj_val_gen, key_val_gen) - if elem_ptr_gen is not None and elem_ptr_gen.Ty is not None: - if elem_ptr_gen.Ty.Pointee != 0: store_val_gen = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, elem_ptr_gen.Ty.Pointee, node) - mlirlite.build_store(builder, store_val_gen, elem_ptr_gen) - setitem_done = 1 - if setitem_done == 0: self.Gen.TypeGenErr.fatal_error(target, "subscript ptr is None") - continue - - # UnaryOp target: *p = val (deref write); only Deref is a valid lvalue, others fail fast - if tk == ast.ASTKind.UnaryOp: - uo_w: ast.UnaryOp* = ast.UnaryOp*(target) - if uo_w is None or uo_w.operand is None: self.Gen.TypeGenErr.fatal_error(target, "assignment target unary op is missing an operand", "TypeError") - if uo_w.op != ast.OpKind.Deref: - self.Gen.TypeGenErr.fatal_error(target, "assignment target supports only the unary '*' dereference (&x / -x / ~x are not valid lvalues)", "SyntaxError") - ptr_w: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo_w.operand, None, 0, trans) - if ptr_w is None or ptr_w.Ty is None or self.Gen.ExprGenBase.is_ptr_type(ptr_w.Ty) == 0: - self.Gen.TypeGenErr.fatal_error(target, "the operand of unary '*' assignment must be a pointer type (type inference degraded)", "TypeError") - pointee_w: mlirlite.LLVMType* = ptr_w.Ty.Pointee - if pointee_w is None: - self.Gen.TypeGenErr.fatal_error(target, "deref write target type missing (pointee is empty)", "TypeError") - sv_w: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, pointee_w, node, trans) - mlirlite.build_store(builder, sv_w, ptr_w) - continue - - # Attribute assignment: obj.field = val - if tk == ast.ASTKind.Attribute: - # Narrow target to ast.Attribute* explicitly (required under the strict pointer rules) - at_tg: ast.Attribute* = ast.Attribute*(target) - # property setter: when attr is a setter, emit the setter call and skip the field store - # (asgn.value gives the setter value param's AST for value-reference auto-address). - if self.Gen.CallGenProp._try_property_setter(pool, builder, mod, at_tg, rhs_val, trans, asgn.value) == 1: continue - # FakeDuck: write per-instance str fields through the {var}__meta__ shadow struct - fd_an: ast.Attribute* = at_tg - if fd_an is not None and fd_an.value is not None and fd_an.attr is not None: - if fd_an.value.kind() == ast.ASTKind.Name: - fd_obj_nm: ast.Name* = ast.Name*(fd_an.value) - if fd_obj_nm.id is not None: - # Pool buffer (f-string stack buffers die when the function returns) - fd_meta_w: str = pool.alloc(256) - if fd_meta_w is not None: viperlib.snprintf(fd_meta_w, 256, "%s__meta__", fd_obj_nm.id) - fd_meta_vw: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, fd_meta_w) - if fd_meta_vw is not None: - fd_sty_w: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type("_str") - if fd_sty_w is not None: - fd_fe_w: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(fd_sty_w, fd_an.attr) - if fd_fe_w is not None and fd_fe_w.Ty is not None: - fd_fp_w: mlirlite.Value* = mlirlite.build_gep_struct(builder, fd_sty_w, fd_fe_w.Ty, fd_meta_vw, fd_fe_w.Index) - if fd_fp_w is not None: - sv_fd: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, fd_fe_w.Ty, node) - mlirlite.build_store(builder, sv_fd, fd_fp_w) - continue - # VTABLE write: obj.method = func writes the vtable slot instead of a field - if self.Gen.CallGenVt._write_vtable_attr(builder, pool, mod, target, rhs_val, trans) == 1: continue - field_ptr: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, target, trans) - if field_ptr is not None: - # Get the field type and coerce rhs_val (e.g. i32 -> i64) - store_val: mlirlite.Value* = rhs_val - if field_ptr.Ty is not None and field_ptr.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, field_ptr.Ty.Pointee, node, trans) - # t.CVolatile field: store with the volatile qualifier - vol_fw: int = self.Gen.ExprGenAttr.attr_field_is_volatile(pool, trans, target) - # rawptr[t.CVolatile|T] base: p.field = v also stores with volatile - at_wr: ast.Attribute* = ast.Attribute*(target) - if at_wr is not None: - if self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, at_wr.value) != 0: vol_fw = 1 - mlirlite.build_store(builder, store_val, field_ptr, vol_fw) - continue - # Guard: field_ptr is None -> build a detailed error message - attr_node: ast.Attribute* = ast.Attribute*(target) - attr_name: str = "(unknown)" - obj_name: str = "(unknown)" - if attr_node is not None: - attr_name = attr_node.attr - if attr_node.value is not None and attr_node.value.kind() == ast.ASTKind.Name: - obj_nm: ast.Name* = ast.Name*(attr_node.value) - obj_name = obj_nm.id - err_buf: str = pool.alloc(256) - if err_buf is not None: - viperlib.snprintf(err_buf, 256, "attribute ptr is None: %s.%s", obj_name, attr_name) - self.Gen.TypeGenErr.fatal_error(target, err_buf) - else: self.Gen.TypeGenErr.fatal_error(target, "attribute ptr is None") - continue - - # Name assignment: var = val - if tk == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(target) - # Guard: skip when the target name is missing - if nm.id is None: continue - # global variable: write to the module-scope global - if GenApi.is_global_name(trans, nm.id) != 0: - mod_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) - if mod_alloca is None: - # fail-fast: global variable not registered - self.Gen.TypeGenErr.fatal_error(target, f"global variable '{nm.id}' is not registered; cannot assign", "NameError") - # Object global with static storage: bind to storage and copy the object (else dangling/NULL) - stg_ref_as: mlirlite.Value* = None - if nm.id is not None: - stg_key_as: str = pool.alloc(sizet(string.strlen(nm.id)) + 8) - if stg_key_as is not None: - viperlib.snprintf(stg_key_as, sizet(string.strlen(nm.id)) + 8, "%s.__stg", nm.id) - stg_ref_as = self.Gen.VarGen.lookup_module_var(trans.SymTab, stg_key_as) - if self._is_addrlike_call(asgn.value) == 0 and stg_ref_as is not None and rhs_val is not None and rhs_val.Ty is not None and stg_ref_as.Ty is not None and stg_ref_as.Ty.Pointee != 0: - stg_ty_as: mlirlite.LLVMType* = stg_ref_as.Ty.Pointee - rhs_inner_as: mlirlite.LLVMType* = None - match rhs_val.Ty: - case mlirlite.LLVMType.Ptr(pointee_as): - rhs_inner_as = pointee_as - case _: - pass - if rhs_inner_as is not None and rhs_inner_as.kind() == mlirlite.LLVMType.Struct and self._stg_ty_equal(pool, stg_ty_as, rhs_inner_as) != 0: - # 1) Bind the global reference to storage (drop the temporary alloca) - mlirlite.build_store(builder, stg_ref_as, mod_alloca) - # 2) Copy the constructed temporary into storage - stg_obj_val_as: mlirlite.Value* = mlirlite.build_load(builder, stg_ty_as, rhs_val) - if stg_obj_val_as is not None: mlirlite.build_store(builder, stg_obj_val_as, stg_ref_as) - continue - target_ty: mlirlite.LLVMType* = None - if mod_alloca.Ty is not None: target_ty = mod_alloca.Ty.Pointee - if target_ty is not None: rhs_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, node, trans) - # t.CVolatile global: store with the volatile qualifier - vol_gas: int = 0 - if nm.id is not None: vol_gas = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) - mlirlite.build_store(builder, rhs_val, mod_alloca, vol_gas) - continue - - # nonlocal variable: write through the closure env - if GenApi.is_nonlocal_name(trans, nm.id) != 0: - nl_ptr: mlirlite.Value* = self.Gen.StmtGenDecl.get_nonlocal_var_ptr(trans, nm.id) - if nl_ptr is None: - # fail-fast: nonlocal variable pointer resolution failed - self.Gen.TypeGenErr.fatal_error(target, f"nonlocal variable '{nm.id}' pointer resolution failed", "NameError") - # Infer the nonlocal type from nl_ptr instead of forcing i32 - nl_target_ty: mlirlite.LLVMType* = None - if nl_ptr.Ty is not None: nl_target_ty = nl_ptr.Ty.Pointee - if nl_target_ty is None: - # fail-fast: nl_ptr type lost, cannot store safely - self.Gen.TypeGenErr.fatal_error(target, f"nonlocal variable '{nm.id}' type information lost; cannot write", "TypeError") - rhs_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, nl_target_ty, node, trans) - mlirlite.build_store(builder, rhs_coerced, nl_ptr) - continue - - # REPL session variable: a Name target at fragment entry defines/assigns the session slot - if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: - rp_entry: GenTypes.ReplVarEntry* = self.Gen.VarGen.repl_ensure_entry(trans, nm.id, target) - if rp_entry is not None: - self.Gen.VarGen.repl_try_assign(trans, nm.id, rhs_val, asgn.value, node) - continue - - # Block assignment to a module-level global inside a function without a global declaration - if self.Gen.VarGen.is_module_scope(trans.SymTab) == 0: - mod_gv: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) - mod_decl_g: int = 0 - if mod_gv is not None: mod_decl_g = 1 - # 双保险:SymTab.Root.Vars 在部分时序/阶段可能看不到本文件已声明的模块全局 - # (现场:某函数对模块全局赋值漏写 global → 静默建局部影子 → 全局仍 NULL → - # 下游 ASan 写 0x208 崩)。按规则此处必须 fail-fast,不得静默建局部。 - if mod_decl_g == 0 and GenApi.is_file_var(nm.id) != 0: mod_decl_g = 1 - if mod_decl_g != 0: self.Gen.TypeGenErr.fatal_error(target, f"variable '{nm.id}' is a module-level global; assignment inside a function requires a global declaration (otherwise a local variable shadows the global, leaving it null and crashing)", "SyntaxError") - - # Ordinary local variable - alloca: mlirlite.Value* = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, nm.id, rhs_val.Ty) - if alloca is not None: - # Unannotated assignment: infer the class name from the RHS expression - cls_inf: str = self._infer_rhs_annot(pool, mod, asgn.value, trans, rhs_val) - if cls_inf is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, nm.id, cls_inf) - # Coerce the value to the alloca type (e.g. double -> float) - store_val: mlirlite.Value* = rhs_val - if alloca.Ty is not None and alloca.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, alloca.Ty.Pointee, node, trans) - # t.CVolatile local variable: store with the volatile qualifier - vol_las: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) - # M3 所有权记账:必须先记账再写值(覆盖释放要读到旧值); - # `p = new heap slife …` → 释放旧对象 + 影子置 1;`q = p` / 外来指针 → 交还所有权 - self._slife_note_assign(pool, builder, mod, trans, alloca, asgn.value, nm.id) - mlirlite.build_store(builder, store_val, alloca, vol_las) - # P2 willFree:对同名变量重新赋值即重新获得有效值,清除失效态 - # (新变量由 add_var 置 0;已存在变量走这里,见 clear_var_freed 注释) - self.Gen.VarGen.clear_var_freed(trans.SymTab, nm.id) - # A lambda assigned to a variable must be registered as a closure name - if asgn.value is not None and asgn.value.kind() == ast.ASTKind.Lambda: self.Gen.CallGenLeafKit.register_closure_name(nm.id) - existing: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) - if existing is None: new_vars += 1 - else: - # fail-fast: variable storage creation failed - self.Gen.TypeGenErr.fatal_error(target, f"variable '{nm.id}' storage creation failed", "TypeError") - - return new_vars - - # _reject_whole_object_copy_into_ptr_slot - fail fast when a rawptr[T] subscript gets a T pointer RHS - # (the whole object would be copied into an 8-byte pointer slot) - def _reject_whole_object_copy_into_ptr_slot(self, pool: memhub.MemBuddy*, trans: HT.Translator*, target: ast.AST*, elem_ptr: mlirlite.Value*, rhs_val: mlirlite.Value*) -> None: - """Fail fast when a rawptr[T] subscript gets a T pointer RHS.""" - if elem_ptr is None or elem_ptr.Ty is None: return - if rhs_val is None or rhs_val.Ty is None: return - elem_ty: mlirlite.LLVMType* = None - match elem_ptr.Ty: - case mlirlite.LLVMType.Ptr(pointee_wo): - elem_ty = pointee_wo - case _: - return - if elem_ty is None: return - # Only object (struct) elements are checked; scalar and pointer elements return early - if elem_ty.kind() != mlirlite.LLVMType.Struct: return - rhs_inner_wo: mlirlite.LLVMType* = None - match rhs_val.Ty: - case mlirlite.LLVMType.Ptr(pointee_rhs_wo): - rhs_inner_wo = pointee_rhs_wo - case _: - return - if rhs_inner_wo is None: return - if rhs_inner_wo.kind() != mlirlite.LLVMType.Struct: return - # The pointee type must match the element type - if self._stg_ty_equal(pool, elem_ty, rhs_inner_wo) == 0: return - # Array slots (t.CArray[T,N]) are legal; only block when the base is a pointer store - if self.Gen.ExprGenName.subscript_base_store_kind(pool, trans, target) != 0: return - elem_nm_wo: str = mlirlite.get_struct_name(elem_ty) - if elem_nm_wo is None: elem_nm_wo = "" - rhs_nm_wo: str = mlirlite.get_struct_name(rhs_inner_wo) - if rhs_nm_wo is None: rhs_nm_wo = elem_nm_wo - elem_sz_wo: int = self.Gen.ClassGenEnum._get_type_size(elem_ty) - ptr_sz_wo: int = 8 - over_wo: int = elem_sz_wo - ptr_sz_wo - if over_wo < 0: over_wo = 0 - col_s_wo: int = 0 - col_e_wo: int = 0 - if target is not None: - col_s_wo = target.col_offset + 1 - col_e_wo = target.end_col_offset + 1 - self.Gen.TypeGenErr.fatal_error(target, f"the lvalue element of the subscript assignment is object '{elem_nm_wo}' ({elem_sz_wo} bytes), " f"but the rhs is a pointer to '{rhs_nm_wo}': the compiler would silently emit a whole-object copy " f"(load {elem_nm_wo} + store {elem_nm_wo}, writing {elem_sz_wo} bytes), " f"instead of writing {ptr_sz_wo} bytes as a pointer. If that address actually stores pointers in 8-byte slots " f"(such as a rawptr[T] buffer/free list), every element written overflows by {over_wo} bytes, " f"writing through adjacent blocks. Fix: declare the base type of the pointer slot in pointer-element form " f"(`rawptr[T]*` or `rawptr[rawptr[T]]`) before assigning; " f"or explicitly cast the base to t.CPtr (untyped pointer) and write with 8-byte pointer semantics.", "TypeError", col_s_wo, col_e_wo) - - # _infer_rhs_annot - infer a variable's annotation class name from the RHS expression - def _infer_rhs_annot(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator*, rhs_val: mlirlite.Value* = None) -> str: - """Infer a variable's annotation class name from the RHS, or None.""" - if node is None or trans is None: return None - k: int = node.kind() - # Call: class instantiation or method call - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - if cl is None or cl.func is None: return None - if cl.func.kind() == ast.ASTKind.Name: - # Class instantiation: only accept a registered struct name - cand: str = (ast.Name*(cl.func)).id - if cand is None: return None - se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(cand) - if se is not None: return cand - return None - if cl.func.kind() == ast.ASTKind.Attribute: - # Method call: resolve the exact return class from the vpi annotation cache - ca: ast.Attribute* = ast.Attribute*(cl.func) - if ca is None or ca.attr is None or ca.value is None: return None - obj_cls: str = self._infer_rhs_annot(pool, mod, ca.value, trans) - if obj_cls is None: - # Module-level function call: resolve the return class from the module's vpi cache - if ca.value.kind() == ast.ASTKind.Name: - mod_nm_rhs: ast.Name* = ast.Name*(ca.value) - if mod_nm_rhs.id is not None: - mod_sha1_rhs: str = self.Gen.CallGenBase._lookup_module_sha1(mod_nm_rhs.id) - if mod_sha1_rhs is not None: - temp_dir_rhs: str = None - if self.Gen.CallGenBase.TempDir is not None: temp_dir_rhs = self.Gen.CallGenBase.TempDir - if temp_dir_rhs is not None: VpiReader._ensure_vpi_ret_annot_loaded(mod_sha1_rhs, temp_dir_rhs) - annot_rhs: str = VpiReader._lookup_vpi_ret_annot(mod_sha1_rhs, None, ca.attr) - if annot_rhs is not None: - ret_cls_rhs: str = self._extract_annot_class_name(pool, annot_rhs) - if ret_cls_rhs is not None: return ret_cls_rhs - return None - if self._cls_has_dunder(pool, mod, obj_cls, ca.attr) == 0: return None - # SHA1 of the module defining the method (struct entry, else current module) - cls_sha1: str = self.Gen.StructGen.get_struct_sha1(obj_cls) - if cls_sha1 is None and trans.ModuleSha1 is not None: cls_sha1 = trans.ModuleSha1 - ret_cls: str = None - if cls_sha1 is not None: - temp_dir: str = None - if self.Gen.CallGenBase.TempDir is not None: temp_dir = self.Gen.CallGenBase.TempDir - if temp_dir is not None: VpiReader._ensure_vpi_ret_annot_loaded(cls_sha1, temp_dir) - annot_ret: str = VpiReader._lookup_vpi_ret_annot(cls_sha1, obj_cls, ca.attr) - if annot_ret is not None: ret_cls = self._extract_annot_class_name(pool, annot_ret) - return ret_cls - return None - # Name: the source variable annotation - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm is None or nm.id is None: return None - ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) - if ve is None: return None - return self.Gen.VarGen.var_annot_class_name(ve) - # BinOp/UnaryOp: operator overload returns an object of the same class - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(node) - if bop is None or bop.left is None: return None - dunder: str = self._op_to_dunder(bop.op) - if dunder is None: return None - lhs_cls: str = self._infer_rhs_annot(pool, mod, bop.left, trans) - if lhs_cls is None or self._cls_has_dunder(pool, mod, lhs_cls, dunder) == 0: return None - return lhs_cls - if k == ast.ASTKind.UnaryOp: - uop: ast.UnaryOp* = ast.UnaryOp*(node) - if uop is None or uop.operand is None: return None - dunder: str = self._op_to_dunder(uop.op) - if dunder is None: return None - opr_cls: str = self._infer_rhs_annot(pool, mod, uop.operand, trans) - if opr_cls is None or self._cls_has_dunder(pool, mod, opr_cls, dunder) == 0: return None - return opr_cls - # Attribute: field annotation class name (recursive attribute chain) - if k == ast.ASTKind.Attribute: return self.Gen.ExprGenAttr._resolve_attr_field_class(pool, trans, node) - return None - - # _extract_annot_class_name - extract a registered struct class name from a return annotation text - def _extract_annot_class_name(self, pool: memhub.MemBuddy*, annot: str) -> str: - """Extract a registered struct class name from a return annotation.""" - if pool is None or annot is None: return None - alen: sizet = string.strlen(annot) - if alen == 0: return None - seg: str = pool.alloc(256) - if seg is None: return None - start: sizet = 0 - i: sizet - for i in range(int(alen)): - if annot[i] == '|': - seglen: sizet = i - start - # Segment [start, i): trim spaces/quotes then look up the registry - s: sizet = start - e: sizet = i - while s < e and (annot[s] == ' ' or annot[s] == '\'' or annot[s] == '"'): s += 1 - # Also strip a trailing '*'; otherwise find_struct misses and chained calls degrade - while e > s and (annot[e - 1] == ' ' or annot[e - 1] == '\'' or annot[e - 1] == '"' or annot[e - 1] == '*'): e -= 1 - seglen = e - s - if seglen > 0 and seglen < 256: - string.strncpy(seg, annot + s, seglen) - seg[seglen] = '\0' - se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(seg) - if se is not None: return seg - start = i + 1 - # Last segment (no '|' suffix) - s: sizet = start - e: sizet = alen - while s < e and (annot[s] == ' ' or annot[s] == '\'' or annot[s] == '"'): s += 1 - # Also strip a trailing '*'; otherwise find_struct misses and chained calls degrade - while e > s and (annot[e - 1] == ' ' or annot[e - 1] == '\'' or annot[e - 1] == '"' or annot[e - 1] == '*'): e -= 1 - seglen: sizet = e - s - if seglen > 0 and seglen < 256: - string.strncpy(seg, annot + s, seglen) - seg[seglen] = '\0' - se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(seg) - if se is not None: return seg - return None - - # _op_to_dunder - operator -> dunder method name (local copy to avoid cross-mixin calls) - def _op_to_dunder(self, op: int) -> str: - """Map a BinOp/UnaryOp operator to its dunder name, or None.""" - if op == ast.OpKind.Add: return "__add__" - if op == ast.OpKind.Sub: return "__sub__" - if op == ast.OpKind.Mult: return "__mul__" - if op == ast.OpKind.Div: return "__truediv__" - if op == ast.OpKind.FloorDiv: return "__floordiv__" - if op == ast.OpKind.Mod: return "__mod__" - if op == ast.OpKind.Pow: return "__pow__" - if op == ast.OpKind.BitAnd: return "__and__" - if op == ast.OpKind.BitOr: return "__or__" - if op == ast.OpKind.BitXor: return "__xor__" - if op == ast.OpKind.LShift: return "__lshift__" - if op == ast.OpKind.RShift: return "__rshift__" - if op == ast.OpKind.USub: return "__neg__" - if op == ast.OpKind.UAdd: return "__pos__" - if op == ast.OpKind.Invert: return "__invert__" - return None - - # _aug_op_to_dunder - augmented operator -> aug dunder (__iadd__ etc.); None falls back to _op_to_dunder - def _aug_op_to_dunder(self, op: int) -> str: - """Map an AugAssign operator to its in-place dunder name, or None.""" - if op == ast.OpKind.Add: return "__iadd__" - if op == ast.OpKind.Sub: return "__isub__" - if op == ast.OpKind.Mult: return "__imul__" - if op == ast.OpKind.Div: return "__itruediv__" - if op == ast.OpKind.FloorDiv: return "__ifloordiv__" - if op == ast.OpKind.Mod: return "__imod__" - if op == ast.OpKind.Pow: return "__ipow__" - if op == ast.OpKind.BitAnd: return "__iand__" - if op == ast.OpKind.BitOr: return "__ior__" - if op == ast.OpKind.BitXor: return "__ixor__" - if op == ast.OpKind.LShift: return "__ilshift__" - if op == ast.OpKind.RShift: return "__irshift__" - return None - - # _cls_has_dunder - check whether a class (including its MRO) has a dunder method - def _cls_has_dunder(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_name: str, dunder: str) -> int: - """Return 1 if the class or its MRO chain has the dunder method.""" - if class_name is None or dunder is None or mod is None: return 0 - lookup_name: str = pool.alloc(128) - if lookup_name is None: return 0 - viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder) - if self.Gen.CallGenBase.find_func_in_module(mod, lookup_name) is not None: return 1 - # Search the inheritance chain ancestors - mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name) - mi: int = 1 - while mro_ptr is not None and mro_ptr[mi] != 0: - anc: str = rawptr(mro_ptr[mi]) - if anc is not None: - anc_lookup: str = pool.alloc(128) - if anc_lookup is not None: - viperlib.snprintf(anc_lookup, 128, "%s.%s", anc, dunder) - if self.Gen.CallGenBase.find_func_in_module(mod, anc_lookup) is not None: return 1 - mi += 1 - # Cross-module: a class with a SHA1 (method declared in the stub) counts as having it - if self.Gen.StructGen.get_struct_sha1(class_name) is not None: return 1 - return 0 - - def InitAssignGen(self) -> int: - """Initialize AssignGen fields (called by Generator at construction).""" - if self is None: return 1 - self.RecoverFound = 0 - return 0 + def _is_cdefine_marker_node(self, node: ast.AST*) -> int: + """Return 1 if the node is a t.CDefine marker node.""" + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + if at.attr is not None and string.strcmp(at.attr, "CDefine") == 0: return 1 + return 0 + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is not None and string.strcmp(nm.id, "CDefine") == 0: return 1 + return 0 + + def _define_explicit_annot(self, annot: ast.AST*) -> ast.AST*: + """Return the explicit type T of a `T | t.CDefine` annotation, or None.""" + if annot is None: return None + if annot.kind() != ast.ASTKind.BinOp: return None + bop: ast.BinOp* = ast.BinOp*(annot) + if bop.left is None: return None + if self._is_cdefine_marker_node(bop.left) != 0: return None + return bop.left + + def _annot_int_bits(self, pool: memhub.MemBuddy*, annot: ast.AST*) -> int: + """Return the bit width of an integer annotation, or 0 if unknown.""" + if annot is None: return 0 + k: int = annot.kind() + nm: str = None + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(annot) + nm = at.attr + elif k == ast.ASTKind.Name: + n2: ast.Name* = ast.Name*(annot) + nm = n2.id + if nm is None: return 0 + return self._int_bits_of_type_name(pool, nm, 0) + + def _int_bits_of_type_name(self, pool: memhub.MemBuddy*, nm: str, depth: int) -> int: + """类型名 → 整数位宽(0=未知/非整数)。depth 防 typedef 环。 + + 覆盖:内建名(int/bool/int8../uint64/sizet)、`t.CXxx`(map_t_type)、 + 以及**用户 typedef**(如 stdint 的 `WORD: t.CTypedef = uint16` —— 经 lookup_ctypedef + 取其底层类型文本再递归定宽)。此前用户 typedef 一律返回 0,导致 + `define X: WORD = ...` 的声明宽度不参与校验/物化。 + """ + if nm is None or depth > 10: return 0 + # 去掉模块/类限定前缀:`t.CUInt16T` → `CUInt16T`;`stdint.uint16` → `uint16` + dot: str = string.strrchr(nm, 46) + if dot is not None and dot[1] != '\0': + return self._int_bits_of_type_name(pool, dot + 1, depth + 1) + if string.strcmp(nm, "int") == 0: return 32 + if string.strcmp(nm, "bool") == 0: return 8 + if string.strcmp(nm, "int8") == 0 or string.strcmp(nm, "uint8") == 0: return 8 + if string.strcmp(nm, "int16") == 0 or string.strcmp(nm, "uint16") == 0: return 16 + if string.strcmp(nm, "int32") == 0 or string.strcmp(nm, "uint32") == 0: return 32 + if string.strcmp(nm, "int64") == 0 or string.strcmp(nm, "uint64") == 0: return 64 + if string.strcmp(nm, "sizet") == 0 or string.strcmp(nm, "size_t") == 0: return 64 + ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.map_t_type(pool, nm) + if ty is not None: + match ty: + case mlirlite.LLVMType.Int(bits): + if bits == 1: return 8 + if bits == 8 or bits == 16 or bits == 32 or bits == 64: return bits + return 0 + case _: + return 0 + # 用户/include typedef(如 stdint.vp 的 `WORD: t.CTypedef = uint16`): + # 表内值可能是模块限定 ref(stdint.uint16),先用 _expand_typedef_ref 展开; + # 本地表未命中再走 builtins 源扫描兜底。 + underlying: str = self.Gen.TypeGenCType.lookup_ctypedef(nm) + if underlying is None: + underlying = self.Gen.TypeGenResolve._lookup_builtins_typedef(pool, nm) + if underlying is not None: + expanded: str = self.Gen.TypeGenResolve._expand_typedef_ref(pool, underlying) + if expanded is not None: underlying = expanded + if underlying is None: return 0 + if string.strcmp(underlying, nm) == 0: return 0 + return self._int_bits_of_type_name(pool, underlying, depth + 1) + + def _define_annot_bits(self, pool: memhub.MemBuddy*, annot: ast.AST*) -> int: + """Return the bit width of a CDefine declaration's explicit type T.""" + return self._annot_int_bits(pool, self._define_explicit_annot(annot)) + + def _cdefine_int_literal_text(self, name: str) -> str: + """Return the raw value text of a CDefine line, or None if not found.""" + if name is None: return None + fp: str = self.Gen.TypeGenResolve.get_current_file() + if fp is None: return None + f: fileio.File* = fileio.File(fp, fileio.MODE.R) + if f.closed: return None + BUF_SZ: sizet = 1048576 + buf: bytes = stdlib.malloc(BUF_SZ) + if buf is None: + f.close() + return None + nrd: LONG = f.read_all(buf, BUF_SZ) + f.close() + if nrd <= 0: + stdlib.free(buf) + return None + if nrd < BUF_SZ: buf[nrd] = 0 + name_len: sizet = string.strlen(name) + total_len: sizet = string.strlen(buf) + pos: sizet = 0 + result: str = None + while pos < total_len: + line_start: sizet = pos + while pos < total_len and buf[pos] != '\n': pos += 1 + line_len: sizet = pos - line_start + if pos < total_len: pos += 1 + if line_len < name_len + 1: continue + ok: int = 1 + ki: sizet + for ki in range(int(name_len)): + if buf[line_start + ki] != name[ki]: + ok = 0 + break + if ok == 0: continue + sep: sizet = line_start + name_len + while sep < line_start + line_len and buf[sep] == ' ': sep += 1 + if sep >= line_start + line_len: continue + if buf[sep] != ':' and buf[sep] != '=': continue + eq: sizet = line_start + while eq < line_start + line_len and buf[eq] != '=': eq += 1 + if eq >= line_start + line_len: continue + eq += 1 + while eq < line_start + line_len and buf[eq] == ' ': eq += 1 + vstart: sizet = eq + while eq < line_start + line_len and buf[eq] != ' ' and buf[eq] != '\t' and buf[eq] != '#' and buf[eq] != '\r': eq += 1 + if eq == vstart: continue + txt: str = stdlib.malloc(eq - vstart + 1) + if txt is None: continue + vi: sizet + for vi in range(int(eq - vstart)): txt[vi] = buf[vstart + vi] + txt[eq - vstart] = 0 + result = txt + break + stdlib.free(buf) + return result + + def _int_text_exceeds_64(self, text: str) -> int: + """Return 1 if the literal text exceeds the 64-bit range, else 0.""" + if text is None: return 0 + tl: sizet = string.strlen(text) + if tl == 0: return 0 + neg: int = 0 + i: sizet = 0 + if text[0] == '-': + neg = 1 + i = 1 + elif text[0] == '+': i = 1 + if i >= tl: return 0 + if i + 1 < tl and text[i] == '0' and (text[i + 1] == 'x' or text[i + 1] == 'X'): + # Hex: significant digits after removing '_' and leading zeros (>16 => overflow) + sig: sizet = 0 + j2: sizet = i + 2 + leading: int = 1 + while j2 < tl: + c2: char = text[j2] + j2 += 1 + if c2 == '_': continue + if not ((c2 >= '0' and c2 <= '9') or (c2 >= 'a' and c2 <= 'f') or (c2 >= 'A' and c2 <= 'F')): + return 0 + if leading != 0 and c2 == '0': continue + leading = 0 + sig += 1 + if sig > 16: return 1 + return 0 + # Decimal: locate the first significant digit + s: sizet = i + while s < tl and (text[s] == '0' or text[s] == '_'): s += 1 + digits2: int = 0 + j3: sizet = s + while j3 < tl: + c3: char = text[j3] + j3 += 1 + if c3 == '_': continue + if c3 < '0' or c3 > '9': return 0 + digits2 += 1 + if digits2 == 0: return 0 + max_text: str = "18446744073709551615" + mlen: sizet = 20 + if neg != 0: + max_text = "9223372036854775808" + mlen = 19 + if digits2 > mlen: return 1 + if digits2 < mlen: return 0 + # Equal length: lexicographic digit compare (same as numeric order) + d: sizet = 0 + k2: sizet = s + while k2 < tl and d < mlen: + c4: char = text[k2] + k2 += 1 + if c4 == '_': continue + mc: char = max_text[d] + if c4 > mc: return 1 + if c4 < mc: return 0 + d += 1 + return 0 + + def require_cdefine_storable(self, pool: memhub.MemBuddy*, node: ast.AST*, name: str, value: int64, annot: ast.AST*, rhs_bits: int = 0) -> None: + """Fail fast if a CDefine value is not storable in its declared type. + + rhs_bits: 右侧表达式自身的位宽(0=未定宽)。右侧带显式强转(如 WORD(...))且不宽于声明注解时, + 截断是**显式**的(左侧只看到截断后的数据),此时不再按"存不下"报错——这正是 + "注解存不下且没有强转截断的需要报错"里被豁免的那一半。 + """ + text: str = self._cdefine_int_literal_text(name) + bits: int = self._define_annot_bits(pool, annot) + wmin: int = self.define_min_width(value) + if self._int_text_exceeds_64(text) != 0: + self.Gen.TypeGenErr.fatal_error(node, f"[VPC] define constant value not storable: the initial value {text} of constant '{name}' exceeds 64 bits " f"(the widest integer type in this language is 64 bits; the storable range under both signed and unsigned interpretations is " f"-9223372036854775808..18446744073709551615; no wider integer type exists => widening is required). " f"Silent truncation/downgrade is forbidden; compilation aborted. Available integer types: int8/uint8, " f"int16/uint16, int32/uint32, int64/uint64 " f"(or int8/uint8/.../uint64, char, sizet). " f"Fix: move the value into the 64-bit range, or introduce a larger integer type for this number (widen).", "TypeError") + return + if bits == 0: return + # Explicit truncation exemption: the RHS narrowed itself to rhs_bits (a cast such as WORD(...)), + # so the value the declaration stores is already the truncated one. + if rhs_bits != 0 and rhs_bits <= bits: return + # Build the 32-bit bounds from overflow-safe literals (see define_min_width) + m31: int64 = 2147483647 + lo: int64 = -m31 - 1 + hi: int64 = m31 * 2 + 1 + if bits == 8: + lo = -128 + hi = 255 + elif bits == 16: + lo = -32768 + hi = 65535 + elif bits == 64: return + if value < lo or value > hi: + self.Gen.TypeGenErr.fatal_error(node, f"[VPC] define constant value not storable: the initial value {value} of constant '{name}' is not storable. " f"Minimal storable width W_min: {wmin} bits; declared type width {bits} bits, " f"storable range of this declaration: {lo}..{hi}. " f"Silent truncation/downgrade is forbidden; compilation aborted. " f"Fix: widen the explicit type to a wider type (such as int32 / int64 / uint64), " f"or correct the literal.", "TypeError") + + def require_cdefine_use_width(self, pool: memhub.MemBuddy*, trans: HT.Translator*, target_annot: ast.AST*, value_node: ast.AST*) -> None: + """Fail fast when a define constant is wider than its use-site type. + + 比较基准是 define 的**声明位宽**(注解 → 右侧强转 → 值的最小宽度,见 cdefine_decl_bits), + 不是值本身的最小宽度:define 声明成 64 位,写进 uint8 位置就是隐式窄化,必须报错。 + """ + if value_node is None or target_annot is None: return + if value_node.kind() != ast.ASTKind.Name: return + rn: ast.Name* = ast.Name*(value_node) + if rn.id is None: return + if trans is not None: + if self.Gen.VarGen.lookup_var(trans.SymTab, rn.id) is not None: return + cdef_val: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(rn.id) + if self.Gen.TypeGenCDef.is_cdefine_found() == 0: return + bits: int = self._annot_int_bits(pool, target_annot) + if bits == 0: return + # 声明位宽优先;未登记位宽(0)时才回退到推断位宽(值的最小可存宽度) + dw: int = self.Gen.TypeGenCDef.lookup_cdefine_bits(rn.id) + if dw == 0: dw = self.define_min_width(cdef_val) + if bits >= dw: return + self.Gen.TypeGenErr.fatal_error(value_node, f"[VPC] define constant use-site width insufficient: constant '{rn.id}' is {dw} bits wide " f"(declared width; actual value {cdef_val}), but the use site (variable declaration annotation) provides only {bits} bits. " f"Silent truncation/downgrade is forbidden; compilation aborted. " f"Fix: widen the variable declaration annotation to at least {dw} bits (such as int{dw}T / CUInt{dw}T), " f"or narrow the constant at its define with an explicit cast.", "TypeError") + + # _register_cdefine_from_value - register a CDefine constant (float or int) + def _register_cdefine_from_value(self, pool: memhub.MemBuddy*, name: str, val_node: ast.AST*, annot: ast.AST* = None) -> None: + """Register a CDefine constant (double for floats, else integer).""" + if pool is None or name is None: return + # Call-wrapped constant (e.g. float(3.14)): use its first argument as the value + if val_node is not None and val_node.kind() == ast.ASTKind.Call: + cl_cd: ast.Call* = ast.Call*(val_node) + if cl_cd is not None and cl_cd.args is not None: + cl_args_cd: list[ast.AST*]* = cl_cd.args + if len(cl_args_cd) > 0: + first_arg_cd: ast.AST* = cl_args_cd.get(0) + if first_arg_cd is not None and first_arg_cd.kind() == ast.ASTKind.Constant: val_node = first_arg_cd + # Negative float literal: unwrap UnaryOp(USub, Constant(float)); value recovered from source + if val_node is not None and val_node.kind() == ast.ASTKind.UnaryOp: + uu_cd: ast.UnaryOp* = ast.UnaryOp*(val_node) + if uu_cd is not None and uu_cd.op == ast.OpKind.USub and uu_cd.operand is not None and uu_cd.operand.kind() == ast.ASTKind.Constant: + ic_cd: ast.Constant* = ast.Constant*(uu_cd.operand) + if ic_cd is not None and ic_cd.const_kind == ast.CONST_FLOAT: val_node = uu_cd.operand + if val_node is not None and val_node.kind() == ast.ASTKind.Constant: + cn_fp: ast.Constant* = ast.Constant*(val_node) + if cn_fp.const_kind == ast.CONST_STR: + # String CDefine: register as a string constant (reader returns a global string) + self.Gen.TypeGenCDef.register_cdefine_string(pool, name, cn_fp.str_val) + return + if cn_fp.const_kind == ast.CONST_FLOAT: + # Recover the float value from the source file (AST float_val may be overwritten) + use_val: double = cn_fp.float_val + rec_d: double = 0.0 + if self._recover_cdefine_float(name, &rec_d) != 0: use_val = rec_d + else: + # Source recovery failed: fail fast to avoid registering a corrupted float + self.Gen.TypeGenErr.fatal_error(val_node, "CDefine float value source-file recovery failed", "InternalCompilerError") + return + self.Gen.TypeGenCDef.register_cdefine_double(pool, name, use_val) + return + if val_node is None: + # CDefine declaration without a value node: fail fast + self.Gen.TypeGenErr.fatal_error(None, f"CDefine value extraction failed: '{name}' is missing an initial value", "TypeError") + return + # 位宽裁决:与跨模块使用方共用 cdefine_decl_bits(注解 → 右侧显式强转 → 值的最小可存宽度)。 + # 有注解而存不下时:右侧带显式强转截断则不报错(左侧只看到截断值),否则由 + # require_cdefine_storable 报"存不下"。 + rhs_bits: int = 0 + val_fp: int64 = self.extract_cdefine_int_value_bits(val_node, pool, &rhs_bits) + vb: int = self.cdefine_decl_bits(pool, annot, val_node) + # Check storability before registering (source text >64-bit or explicit T too narrow => fail) + self.require_cdefine_storable(pool, val_node, name, val_fp, annot, rhs_bits) + self.Gen.TypeGenCDef.register_cdefine_constant(pool, name, val_fp, vb) + + # Recover a CDefine float value by re-reading its source line with atof + + def _recover_found(self) -> int: + """Return whether the last _recover_cdefine_float succeeded.""" + return self.RecoverFound + + def _recover_float_literal(self, name: str, out: double*, require_cdefine: int) -> int: + """Recover a float literal from the module source into out[0]; 1 on success.""" + self.RecoverFound = 0 + fp: str = self.Gen.TypeGenResolve.get_current_file() + if fp is None or name is None or out is None: return 0 + f: fileio.File* = fileio.File(fp, fileio.MODE.R) + if f.closed: return 0 + BUF_SZ: sizet = 1048576 + buf: bytes = stdlib.malloc(BUF_SZ) + if buf is None: + f.close() + return 0 + nrd: LONG = f.read_all(buf, BUF_SZ) + f.close() + if nrd <= 0: + stdlib.free(buf) + return 0 + if nrd < BUF_SZ: buf[nrd] = 0 + + name_len: sizet = string.strlen(name) + total_len: sizet = string.strlen(buf) + pos: sizet = 0 + while pos < total_len: + line_start: sizet = pos + while pos < total_len and buf[pos] != '\n': pos += 1 + line_len: sizet = pos - line_start + if pos < total_len: pos += 1 + + # define NAME: T = V form: the name is preceded by the define keyword + ns: sizet = line_start + is_define_line: int = 0 + if line_len >= 8: + if buf[ns] == 'd' and buf[ns + 1] == 'e' and buf[ns + 2] == 'f' and buf[ns + 3] == 'i' and buf[ns + 4] == 'n' and buf[ns + 5] == 'e': + nb: sizet = ns + 6 + while nb < line_start + line_len and (buf[nb] == ' ' or buf[nb] == '\t'): nb += 1 + # Keyword only when followed by whitespace (avoid matching defineX) + if nb > ns + 6: + is_define_line = 1 + ns = nb + # Match the name at line start (or after define); it must be followed by ':' or '=' + if ns + name_len + 1 > line_start + line_len: continue + match_ok: int = 1 + ki: sizet + for ki in range(int(name_len)): + if buf[ns + ki] != name[ki]: + match_ok = 0 + break + if match_ok == 0: continue + sep: sizet = ns + name_len + while sep < line_start + line_len and buf[sep] == ' ': sep += 1 + if sep >= line_start + line_len: continue + if buf[sep] != ':' and buf[sep] != '=': continue + + # CDefine path requires t.CDefine in the line, or the define keyword form + if require_cdefine != 0 and is_define_line == 0: + has_cd: int = 0 + ci: sizet = line_start + name_len + 1 + while ci + 8 <= line_start + line_len: + if (buf[ci] == 't' and buf[ci + 1] == '.' and buf[ci + 2] == 'C' and buf[ci + 3] == 'D' and buf[ci + 4] == 'e' and buf[ci + 5] == 'f' and buf[ci + 6] == 'i' and buf[ci + 7] == 'n' and buf[ci + 8] == 'e'): + has_cd = 1 + break + ci += 1 + if has_cd == 0: continue + + # Find '=' + eq: sizet = line_start + while eq < line_start + line_len and buf[eq] != '=': eq += 1 + if eq >= line_start + line_len: continue + eq += 1 + while eq < line_start + line_len and buf[eq] == ' ': eq += 1 + # Call-wrapped float constant: skip the float( prefix and start at the first numeric char + if eq < line_start + line_len: + c0_rf: char = buf[eq] + if not ((c0_rf >= '0' and c0_rf <= '9') or c0_rf == '-' or c0_rf == '+' or c0_rf == '.'): + pfx_rf: sizet = eq + while pfx_rf < line_start + line_len: + cp_rf: char = buf[pfx_rf] + if (cp_rf >= '0' and cp_rf <= '9') or cp_rf == '.' or cp_rf == '-' or cp_rf == '+': + eq = pfx_rf + break + pfx_rf += 1 + val_start: sizet = eq + while eq < line_start + line_len and buf[eq] != ' ' and buf[eq] != '\t' and buf[eq] != '#' and buf[eq] != '\r' and buf[eq] != '\n': eq += 1 + if eq == val_start: continue + + val_txt: str = stdlib.malloc(eq - val_start + 1) + if val_txt is None: + stdlib.free(buf) + return 0 + vi: sizet + for vi in range(int(eq - val_start)): val_txt[vi] = buf[val_start + vi] + val_txt[eq - val_start] = 0 + stdlib.free(buf) + # Use atof_out (an out param) to avoid the cross-module double return bug + if string.atof_out(val_txt, out) != 0: + self.RecoverFound = 1 + return 1 + return 0 + + stdlib.free(buf) + return 0 + + def _recover_cdefine_float(self, name: str, out: double*) -> int: + """Recover a CDefine float value (requiring a t.CDefine line).""" + return self._recover_float_literal(name, out, 1) + + # _annotation_contains_name - recursively check whether an annotation contains a name + def _annotation_contains_name(self, annot: ast.AST*, name: str) -> int: + """Return 1 if the annotation recursively contains the given name.""" + if annot is None or name is None: return 0 + k: int = annot.kind() + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(annot) + if at.attr is not None and string.strcmp(at.attr, name) == 0: return 1 + return 0 + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(annot) + if nm.id is not None and string.strcmp(nm.id, name) == 0: return 1 + return 0 + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(annot) + if self._annotation_contains_name(bop.left, name) != 0: return 1 + if self._annotation_contains_name(bop.right, name) != 0: return 1 + return 0 + + def _annotation_has_cptr_marker(self, annot: ast.AST*) -> int: + """Return 1 if the annotation union carries a CPtr marker (the star form ` | t.CPtr`).""" + if annot is None: return 0 + k: int = annot.kind() + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(annot) + if at.attr is not None and string.strcmp(at.attr, "CPtr") == 0: return 1 + return 0 + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(annot) + if nm.id is not None and string.strcmp(nm.id, "CPtr") == 0: return 1 + return 0 + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(annot) + if self._annotation_has_cptr_marker(bop.left) != 0: return 1 + if self._annotation_has_cptr_marker(bop.right) != 0: return 1 + return 0 + + # _is_ptr_element_annotation - check whether an annotation is bytes* or str* + # bytes*/str* => 8-byte element stride (i8**); char*/int8* => 1-byte stride. + def _is_ptr_element_annotation(self, annot: ast.AST*) -> int: + """Return 1 if the annotation is a bytes* or str* form.""" + if annot is None: return 0 + k: int = annot.kind() + if k != ast.ASTKind.BinOp: return 0 + bop: ast.BinOp* = ast.BinOp*(annot) + if bop.op != ast.OpKind.BitOr: return 0 + # str/bytes are already i8*; only the union WITH a pointer marker (str*/bytes*) is i8** and + # needs 8-byte element slots. A union that merely carries them (e.g. `const str`, which is + # t.CConst | str and stays i8*) must keep the 1-byte stride. + if self._annotation_has_cptr_marker(annot) != 0: + if self._annotation_contains_name(annot, "str") != 0: return 1 + if self._annotation_contains_name(annot, "bytes") != 0: return 1 + # Generic type param / nested union | t.CPtr: scalars use their own size, pointers use 8-byte slots + if self._is_generic_tp_or_nested(bop.left) != 0 or self._is_generic_tp_or_nested(bop.right) != 0: + gp_nm: str = None + if bop.left is not None and bop.left.kind() == ast.ASTKind.Name: + gp_left_nm: ast.Name* = ast.Name*(bop.left) + gp_nm = gp_left_nm.id + elif bop.right is not None and bop.right.kind() == ast.ASTKind.Name: + gp_right_nm: ast.Name* = ast.Name*(bop.right) + gp_nm = gp_right_nm.id + if gp_nm is not None: + # Resolve the generic param via the global gen (direct field access breaks static binding) + gp_resolved: str = self.Gen.TypeGenBase.resolve_generic_type_name(gp_nm) + if gp_resolved is not None and string.strcmp(gp_resolved, gp_nm) != 0: + # Generic param resolved to a scalar => use scalar size, not an 8-byte pointer slot + if self._is_scalar_ty_name(gp_resolved) != 0: return 0 + return 1 + # 64-bit scalar | CPtr: element is 8 bytes, so use an 8-byte stride (else GEP truncation) + if self._is_64bit_scalar_ty(bop.left) != 0 or self._is_64bit_scalar_ty(bop.right) != 0: return 1 + return 0 + + def _stg_ty_equal(self, pool: memhub.MemBuddy*, ta: mlirlite.LLVMType*, tb: mlirlite.LLVMType*) -> int: + """Return 1 if the two types are equal by printed content.""" + if ta is None or tb is None: return 0 + # Fast path: identical instance is equal + if uint64(ta) == uint64(tb): return 1 + buf_a: str = pool.alloc_tagged(4096, memhub.DETSITE_ASSIGNGEN_578) + buf_b: str = pool.alloc_tagged(4096, memhub.DETSITE_ASSIGNGEN_579) + if buf_a is None or buf_b is None: return 0 + mlirlite.TypePrint(buf_a, 4096, ta, pool) + mlirlite.TypePrint(buf_b, 4096, tb, pool) + if buf_a[0] == 0 or buf_b[0] == 0: return 0 + return 1 if string.strcmp(buf_a, buf_b) == 0 else 0 + + # ============================================================ + # 生命期(LifeTag)判定 / 传播 / 汇点检查(架构见 Debugger/ownership-and-new.md §11) + # 标签常量见 GenTypes define LIFE_*;有序,越小越"短命",汇合取 min。 + # ============================================================ + + # ── A2(§14)`@life(ret(...))`:调用点解析被调者的返回值生命期契约 ── + # _RecvClassName - 从方法的接收者表达式取它静态类型指向的类名(去 sha1 前缀);判不定返回 None。 + # 只认「名字变量」接收者:变量表里查不到(如 `m.f()` 里的模块名 `m`)返回 None —— 调用方据此 + # 把"模块限定函数调用"与"方法调用"分开(二者在 AST 里都是 Attribute)。 + def _RecvClassName(self, trans: HT.Translator*, recv: ast.AST*) -> str: + """接收者静态类型 → 类名(无 sha1 前缀);判不定返回 None""" + if trans is None or recv is None: return None + if recv.kind() != ast.ASTKind.Name: return None + rn: ast.Name* = ast.Name*(recv) + if rn.id is None: return None + v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn.id) + if v is None: return None + ty: mlirlite.LLVMType* = v.Ty # 变量槽类型 = <变量类型>* + if ty is None or self.Gen.ExprGenBase.is_ptr_type(ty) == 0: return None + p1: mlirlite.LLVMType* = ty.Pointee # 变量类型本身 + if p1 is None: return None + # 类变量通常是指针(`S*`);值语义的类变量(`s: S`,罕见)直接取本类型。 + elem: mlirlite.LLVMType* = p1.Pointee + if self.Gen.ExprGenBase.is_ptr_type(p1) == 0: elem = p1 + sname: str = mlirlite.get_struct_name(elem) + if sname is None: return None + pl: int = GenApi.sha1_prefix_len(sname) + if pl > 0: sname = sname + pl + return sname + + # _CallArgNodeForParam - `@life(ret(borrows=<形参 pi>))` 在调用点对应的实参节点。 + # **方法**调用的形参表含 self(pi=0 即接收者),故 is_method=1 时偏移一格; + # **模块限定函数**(`m.f(...)`:callee 也是 Attribute,但不是方法)形参 0 就是第一个实参, + # 不能偏移 —— 否则 pi=0 会被解析成"模块名"这个非实参节点(现场:本函数把 + # `retdep2.IdentBorrow(l)` 的形参 0 取成了 `retdep2` → life=STATIC → 跨模块 ret 契约静默失效)。 + def _CallArgNodeForParam(self, cl: ast.Call*, pi: int, is_method: int) -> ast.AST*: + """第 pi 号形参对应的实参表达式;越界/判不定返回 None""" + if cl is None or pi < 0: return None + cargs: list[ast.AST*]* = cl.args + if is_method != 0: + if pi == 0: + at: ast.Attribute* = ast.Attribute*(cl.func) + return at.value + if cargs is None or sizet(pi - 1) >= len(cargs): return None + return cargs.get(sizet(pi - 1)) + if cargs is None or sizet(pi) >= len(cargs): return None + return cargs.get(sizet(pi)) + + # _CallIsMethod - 该调用是否"接收者是对象变量"的方法调用(决定形参表是否含 self)。 + def _CallIsMethod(self, trans: HT.Translator*, cl: ast.Call*) -> int: + """1 = 方法调用(形参表含 self);0 = 模块限定函数 / 裸函数""" + if cl is None or cl.func is None: return 0 + if cl.func.kind() != ast.ASTKind.Attribute: return 0 + at: ast.Attribute* = ast.Attribute*(cl.func) + if self._RecvClassName(trans, at.value) is None: return 0 + return 1 + + # _CallRetKind - 解析被调者的 `@life(ret(...))` 契约(本文件表 → 跨模块 .vpi);-1 = 无声明/判不定。 + def _CallRetKind(self, trans: HT.Translator*, cl: ast.Call*) -> int: + """被调者 ret_kind 编码(GenApi.RET_KIND_*);-1 = 无声明/判不定""" + if cl is None or cl.func is None: return -1 + fnm: str = self.Gen.ExprGenBase.get_func_name(cl.func) + if fnm is None: return -1 + pool: memhub.MemBuddy* = None + if trans is not None: pool = trans.Pool + if pool is None: return -1 + # 方法调用:接收者是本帧变量 → 取它静态类型指向的类名;接收者是模块名 → 不当方法。 + if cl.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + cls: str = self._RecvClassName(trans, at.value) + if cls is not None: + rk_m: int = GenApi.get_ret_kind_method(pool, cls, fnm) + if rk_m != 0: return rk_m + sha_m: str = self.Gen.StructGen.get_struct_sha1(cls) + if sha_m is not None: + rk_m = self.Gen.CallGenBase.lookup_ret_kind_via_vpi(sha_m, cls, fnm) + if rk_m != 0: return rk_m + return -1 + # 模块级函数(裸名,或模块限定 `m.f()`):本文件表优先,再跨模块 .vpi 回退。 + rk: int = GenApi.get_ret_kind(fnm) + if rk != 0: return rk + mod_sha: str = None + if cl.func.kind() == ast.ASTKind.Attribute: + at2: ast.Attribute* = ast.Attribute*(cl.func) + # 只认裸模块名接收者(`m.f()`);`self.h.f()` 之类链式接收者判不定 → 不猜。 + if at2.value is None or at2.value.kind() != ast.ASTKind.Name: return -1 + mn2: ast.Name* = ast.Name*(at2.value) + mod_p: str = None + if mn2.id is not None: mod_p = mn2.id + if mod_p is not None: + fi2: str = None + if trans is not None: fi2 = trans._from_imports + mod_sha = self.Gen.StructGen._resolve_module_sha1(mod_p, fi2) + # 接收者既不是模块(解析不出 sha1)也不是变量(否则上面早按方法走了)→ 判不定。 + if mod_sha is None: return -1 + if mod_sha is None and trans is not None and trans._from_imports is not None: + mod_sha = self.Gen.CallGenBase._lookup_mod_sha1(pool, trans._from_imports, fnm, 0) + if mod_sha is not None: + rk = self.Gen.CallGenBase.lookup_ret_kind_via_vpi(mod_sha, None, fnm) + if rk != 0: return rk + return -1 + + # life_of_expr - 求表达式当前值的生命期标签。 + # target_ty 参与判定:裸本帧数组名在**指针**目标位置 = 数组退让(交出本帧栈地址), + # 在同型聚合目标位置 = 按值拷贝(LIFE_VALUE)。 + def life_of_expr(self, trans: HT.Translator*, node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: + """表达式的 LifeTag;无法判定时 LIFE_UNKNOWN(保守放行)""" + if node is None or trans is None: return GenTypes.LIFE_VALUE + k: int = node.kind() + if k == ast.ASTKind.Constant: + cn: ast.Constant* = ast.Constant*(node) + if cn is None: return GenTypes.LIFE_VALUE + if cn.const_kind == ast.CONST_STR: return GenTypes.LIFE_STATIC + if cn.const_kind == ast.CONST_NONE: return GenTypes.LIFE_UNKNOWN + return GenTypes.LIFE_VALUE + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is None: return GenTypes.LIFE_UNKNOWN + if GenApi.is_global_name(trans, nm.id) != 0: return GenTypes.LIFE_STATIC + if GenApi.is_nonlocal_name(trans, nm.id) != 0: return GenTypes.LIFE_UNKNOWN + v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) + if v is None: return GenTypes.LIFE_STATIC + if v.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(v.Ty) != 0: + inner: mlirlite.LLVMType* = v.Ty.Pointee + if inner is not None and inner.kind() == mlirlite.LLVMType.Array: + # 本帧局部数组名:退让到指针目标交出栈地址;同型聚合目标是值拷贝 + if target_ty is not None and self.Gen.ExprGenBase.is_ptr_type(target_ty) != 0: return GenTypes.LIFE_FRAME + return GenTypes.LIFE_VALUE + # 指针/标量变量:读登记的生命期(默认 UNKNOWN) + return self.Gen.VarGen.get_var_life(trans.SymTab, nm.id) + if k == ast.ASTKind.UnaryOp: + uo: ast.UnaryOp* = ast.UnaryOp*(node) + if uo.op == ast.OpKind.AddrOf and uo.operand is not None and uo.operand.kind() == ast.ASTKind.Name: + onm: ast.Name* = ast.Name*(uo.operand) + if onm.id is not None: + if GenApi.is_global_name(trans, onm.id) != 0: return GenTypes.LIFE_STATIC + if GenApi.is_nonlocal_name(trans, onm.id) != 0: return GenTypes.LIFE_UNKNOWN + if self.Gen.VarGen.lookup_var(trans.SymTab, onm.id) is not None: return GenTypes.LIFE_FRAME + return GenTypes.LIFE_STATIC + return GenTypes.LIFE_UNKNOWN + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + # §14 A2:`@life(ret(...))` 声明**优先于任何推断**(无条件)。 + rk: int = self._CallRetKind(trans, cl) + if rk >= 0: + if rk == GenApi.RET_KIND_FRAME: return GenTypes.LIFE_FRAME + if rk == GenApi.RET_KIND_HEAP: return GenTypes.LIFE_HEAP + if rk == GenApi.RET_KIND_STATIC: return GenTypes.LIFE_STATIC + if rk == GenApi.RET_KIND_NONFRAME: return GenTypes.LIFE_UNKNOWN + if rk >= GenApi.RET_KIND_BORROW_BASE: + pi: int = rk - GenApi.RET_KIND_BORROW_BASE + # 形参位 → 实参位:**方法**调用的形参表含 self(pi=0 = 接收者); + # 模块限定函数(`m.f(...)`)不含,pi 直接就是实参下标。 + bn: ast.AST* = self._CallArgNodeForParam(cl, pi, self._CallIsMethod(trans, cl)) + if bn is None: return GenTypes.LIFE_UNKNOWN + return self.life_of_expr(trans, bn, None) + # 分配类调用 → 堆;其余调用 → 未知(函数摘要落地前保守放行) + if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.attr is not None: + if (string.strcmp(at.attr, "alloc") == 0 or string.strcmp(at.attr, "alloc_buf") == 0 or string.strcmp(at.attr, "calloc") == 0 or string.strcmp(at.attr, "realloc") == 0): return GenTypes.LIFE_HEAP + if cl.func is not None and cl.func.kind() == ast.ASTKind.Name: + fnm: ast.Name* = ast.Name*(cl.func) + if fnm.id is not None and string.strcmp(fnm.id, "malloc") == 0: return GenTypes.LIFE_HEAP + # 裸 `T(...)` 构造:**没走 `new`、且该类没有 `__new__`** 时,存储就是入口块的 alloca + # (CallGenCtor._translate_struct_ctor:`tmp = _alloca_at_entry(...)`,只有 __new__ 才会 + # 改写存储指针)→ 帧生命期。这是"写进本帧对象字段不算逃逸"(调用点同命判定)成立的前提。 + # 有 `__new__` 的类仍返回未知(存储由 __new__ 决定,可能是池/堆)。 + ctor_nm: str = self.Gen.ExprGenBase.get_func_name(cl.func) + if ctor_nm is not None: + ct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type(ctor_nm) + if ct_ty is not None: + ct_en: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(ct_ty) + if ct_en is None or ct_en.HasNew == 0: return GenTypes.LIFE_FRAME + return GenTypes.LIFE_UNKNOWN + if k == ast.ASTKind.New: + # M2.4 `new`:生命期由描述符决定(§2.3 组合矩阵) + # stack → slife:帧生命期(等价既有 `&T(...)`) + # heap/memgr slife → 同样按帧生命期:作用域结束即释放(RAII),逃逸出去必然悬垂 + # hlife(含 heap/memgr 的缺省)→ 堆/池生命期,可自由传递 + nn: ast.New* = ast.New*(node) + if nn is None: return GenTypes.LIFE_UNKNOWN + if nn.Life == ast.NEW_LIFE_SLIFE: return GenTypes.LIFE_FRAME + if nn.Life == ast.NEW_LIFE_DEFAULT and nn.Source == ast.NEW_SRC_STACK: return GenTypes.LIFE_FRAME + return GenTypes.LIFE_HEAP + return GenTypes.LIFE_UNKNOWN + + # life_of_lvalue - 求赋值目标(左值)所在存储的生命期。 + # P3 用它做"左值比右值长命"的判定:Name 目标看变量/全局;Attribute/Subscript 递归其基对象 + # (`obj.f` / `arr[i]` 的存储生命期 = obj / arr 的生命期)。 + def life_of_lvalue(self, trans: HT.Translator*, target_node: ast.AST*) -> int: + """赋值目标的 LifeTag;无法判定时 LIFE_UNKNOWN(保守放行)""" + if trans is None or target_node is None: return GenTypes.LIFE_UNKNOWN + k: int = target_node.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(target_node) + if nm.id is None: return GenTypes.LIFE_UNKNOWN + if GenApi.is_global_name(trans, nm.id) != 0: return GenTypes.LIFE_STATIC + if GenApi.is_nonlocal_name(trans, nm.id) != 0: return GenTypes.LIFE_UNKNOWN + v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) + # 局部变量槽本身的生命期就是本帧(LIFE_FRAME)。注意**不能**用 get_var_life: + # 那是"变量当前值所指向的存储"的生命期(供传播/借用判定用),语义不同—— + # 混用会把 `p = &local`(p 是本帧局部指针)误判成长命左值。 + if v is not None: return GenTypes.LIFE_FRAME + return GenTypes.LIFE_STATIC + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(target_node) + if at.value is None: return GenTypes.LIFE_UNKNOWN + return self.life_of_expr(trans, at.value, None) + if k == ast.ASTKind.Subscript: + sb: ast.Subscript* = ast.Subscript*(target_node) + if sb.value is None: return GenTypes.LIFE_UNKNOWN + return self.life_of_expr(trans, sb.value, None) + return GenTypes.LIFE_UNKNOWN + + # _fail_if_lvalue_outlives_rhs - P3 的一般规则:左值存储比右值长命时,右值不得交出更短的存储。 + # 只在"明确长命"(HEAP/STATIC)的左值上触发;RHS 判定复用 fail_if_life_escapes(FRAME → 报)。 + # 例:`obj.f = &local`(obj 指向堆对象,堆活过本帧)→ LifetimeError; + # `local_struct.f = &local2`(左值也在本帧,FRAME)→ 放行(两者同命)。 + def _fail_if_lvalue_outlives_rhs(self, trans: HT.Translator*, target_node: ast.AST*, rhs_node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: + if trans is None or target_node is None or rhs_node is None: return 0 + lv: int = self.life_of_lvalue(trans, target_node) + if lv != GenTypes.LIFE_HEAP and lv != GenTypes.LIFE_STATIC: return 0 + GenApi.fail_if_stack_addr_escape(rhs_node, "a longer-lived field/element", trans, target_ty) + return 0 + + # propagate_life - 把 RHS 的生命期写到赋值目标(仅 Name 目标;全局目标走汇点检查)。 + def propagate_life(self, trans: HT.Translator*, target_node: ast.AST*, rhs_node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: + """Propagate the RHS life tag onto a Name target; 0=done / 1=skipped""" + if trans is None or target_node is None: return 1 + if target_node.kind() != ast.ASTKind.Name: + # P3:非 Name 目标(属性/下标)——左值存储比右值长命时,右值不得是本帧栈地址 + self._fail_if_lvalue_outlives_rhs(trans, target_node, rhs_node, target_ty) + return 1 + tnm: ast.Name* = ast.Name*(target_node) + if tnm.id is None: return 1 + if GenApi.is_global_name(trans, tnm.id) != 0: return 1 + if GenApi.is_nonlocal_name(trans, tnm.id) != 0: return 1 + life: int = self.life_of_expr(trans, rhs_node, target_ty) + # §13 NLL:默认生命期的 `new heap T()` 若证明在本函数内封闭(不外泄)→ 按帧生命期处理。 + # 与 `new heap slife` 同口径(生命期 FRAME + 作用域出口释放,见 _slife_note_assign)。 + if life == GenTypes.LIFE_HEAP: + if self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, tnm.id) != 0: + life = GenTypes.LIFE_FRAME + self.Gen.VarGen.set_var_life(trans.SymTab, tnm.id, life) + return 0 + + # _CurFuncIsEntryMain - 当前正在翻译的函数是否为**入口 main**(1=是 / 0=否)。 + # 嵌套函数不算:它们的帧不是进程生命期,不能享受豁免。 + def _CurFuncIsEntryMain(self, trans: HT.Translator*) -> int: + """当前函数是否为入口 main(按 llirlite 函数名判,与 FuncGenDef 对 main 不改名同口径)""" + if trans is None or trans._cur_func is None: return 0 + nm: str = mlirlite.function_get_name(trans._cur_func) + if nm is None: return 0 + return 1 if string.strcmp(nm, "main") == 0 else 0 + + # fail_if_life_escapes - 汇点检查:被写出表达式的生命期为 LIFE_FRAME → LifetimeError。 + # 这是 GenApi.fail_if_stack_addr_escape 的唯一实现(语法判据已并入 life_of_expr)。 + # 例外(入口 main 全豁免):main 的帧生命期 = 进程生命期,其栈对象在本帧返回(= 进程退出) + # 后不可能再被使用,故 main 内不做栈地址逃逸检查(如 `argparse._mbuddy = mb`:mb 是帧内 + # 结构体)。**嵌套函数不豁免**(它们的帧不是进程生命期)。 + def fail_if_life_escapes(self, node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType*, leak_func: str = None, leak_param: int = -1) -> int: + """Fail fast (LifetimeError) when the written-out value is frame-lifetime storage""" + if node is None or trans is None: return 0 + if self._CurFuncIsEntryMain(trans) != 0: return 0 + if self.life_of_expr(trans, node, target_ty) != GenTypes.LIFE_FRAME: return 0 + # 报文里给出名字(&x / x),便于定位;前缀单独存放,避免 str 相加(str 是 i8*,无 '+' 语义) + nm_txt: str = "expression" + nm_pre: str = "" + if node.kind() == ast.ASTKind.Name: + n2: ast.Name* = ast.Name*(node) + if n2.id is not None: nm_txt = n2.id + elif node.kind() == ast.ASTKind.UnaryOp: + u2: ast.UnaryOp* = ast.UnaryOp*(node) + if u2.operand is not None and u2.operand.kind() == ast.ASTKind.Name: + n3: ast.Name* = ast.Name*(u2.operand) + if n3.id is not None: + nm_txt = n3.id + nm_pre = "&" + # 泄漏点(可选,只有"确实报错"才去算):被调者那边**造成逃逸的写入点**在同一源文件里, + # 追加一帧 "File lifetime leak ..."。"先过 life 判定再采集"是刻意的:escape_leak_line + # 会重扫被调者函数体并动 EscConstEnv/EscIndCount,不能对"其实不报错"的实参白白跑一遍。 + leak_f: str = None + leak_l: int = 0 + if leak_func is not None and leak_param >= 0: + leak_l = self.Gen.StructGen.escape_leak_line(leak_func, leak_param) + if leak_l > 0: leak_f = self.Gen.TypeGenResolve.get_current_file() + self.Gen.TypeGenErr.fatal_error(node, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local; using it at {where} hands out a dangling pointer once this frame returns. Copy the value, or allocate on the heap (`new heap hlife ...`).", "LifetimeError", 0, 0, leak_f, leak_l) + return 0 + + # check_return_frame_decl - **定义端** `return <本帧存储>` 的**声明感知**检查(A2 / §14.3 第 6 条)。 + # A2 移除了"一律拦"的旧判据:定义端分不清"交出本帧"与"借出调用方的存储"(后者是 + # `return <借用形参>`,一律拦会误报)。但也不能一律放 —— 无声明的函数返回本帧存储会让调用点 + # 永远只拿到"未知"(悬垂指针一路静默)。判据收敛到被调者自己的 `@life(ret(...))`: + # · `ret(frame)` → 放行(去向由调用点负责); + # · `ret(borrows=<该形参>)` → 放行(借出的正是那个形参); + # · 无声明 / `ret(heap|static|borrows=none)` → `LifetimeError`(提示补声明 / 改声明)。 + # 拿不到当前函数定义(非三条函数体翻译路径)时**不判**:宁可少报,也不误报。 + def check_return_frame_decl(self, trans: HT.Translator*, value: ast.AST*) -> int: + """返回值是本帧存储且当前函数的 ret 声明不允许 → LifetimeError;0=放行""" + if trans is None or value is None: return 0 + fd: ast.AST* = self.Gen.StructGen.NllCurFunc + if fd is None: return 0 + rv_ty: mlirlite.LLVMType* = None + if trans._cur_func is not None: rv_ty = mlirlite.function_get_ret_ty(trans._cur_func) + # target_ty 必须传返回类型:裸本帧数组名在**指针**返回类型下是数组退让(交出栈地址), + # 在同型聚合返回类型下是按值拷贝(LIFE_VALUE,不在此列)。 + if self.life_of_expr(trans, value, rv_ty) != GenTypes.LIFE_FRAME: return 0 + rk: int = GenApi.get_ret_kind_by_func(fd) + if rk == GenApi.RET_KIND_FRAME: return 0 + is_borrow_kind: int = 0 + pi: int = -1 + if rk >= GenApi.RET_KIND_BORROW_BASE: + is_borrow_kind = 1 + pi = rk - GenApi.RET_KIND_BORROW_BASE + # 只有"返回的正是被声明的那个形参"才算借出;返回别的本帧存储仍是悬垂 + if self.Gen.StructGen._ParamIndex(ast.FunctionDef*(fd), value) == pi: return 0 + # 报文里给出名字(&x / x),便于定位 + nm_txt: str = "expression" + nm_pre: str = "" + if value.kind() == ast.ASTKind.Name: + n2: ast.Name* = ast.Name*(value) + if n2.id is not None: nm_txt = n2.id + elif value.kind() == ast.ASTKind.UnaryOp: + u2: ast.UnaryOp* = ast.UnaryOp*(value) + if u2.operand is not None and u2.operand.kind() == ast.ASTKind.Name: + n3: ast.Name* = ast.Name*(u2.operand) + if n3.id is not None: + nm_txt = n3.id + nm_pre = "&" + if rk == 0: + self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local and this function has no `@life(ret(...))` declaration, so callers cannot know the result's lifetime. Declare `@life(ret(frame))` (the result may point into this frame; callers must keep it in-frame) or `@life(ret(borrows=))` (the result borrows a parameter), or return a value copy / a heap allocation (`new heap hlife ...`).", "LifetimeError") + return 0 + if is_borrow_kind != 0: + self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local but this function declares `@life(ret(borrows=))` (parameter #{pi}), which only permits returning that parameter. Return that parameter, or declare `@life(ret(frame))`.", "LifetimeError") + return 0 + self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local but this function is declared as non-frame (`@life(ret(heap))` / `ret(static)` / `ret(borrows=none)`); the declaration contradicts the returned value. Fix the declaration, or return a non-frame value.", "LifetimeError") + return 0 + + # _SlifeDropOwned - 该槽已被登记为 slife 绑定时交还所有权:释放它当前拥有的对象、影子清 0。 + # 用于所有"槽里不再是编译器拥有的对象"的赋值(覆盖赋值 / 别名拷贝 / 外来指针 / None)。 + # 未登记 → no-op(该槽从未被 `new heap slife` 绑定过,编译器不负责它)。 + def _SlifeDropOwned(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*) -> int: + """释放该槽当前拥有的对象并置"不拥有"(先按 §3.6 调度 `__del__`);1=处理过 / 0=未登记""" + if target_slot is None: return 0 + sh: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(target_slot) + if sh is None: return 0 + self.Gen.VarGen._SlifeEmitGuardedFree(pool, builder, mod, trans, target_slot, sh) + self.Gen.VarGen._SlifeNoteStore(pool, builder, sh, 0) + return 1 + + # _slife_note_assign - M3 赋值处的所有权记账(§4.2 RAII / §4.3 编译器独占释放)。 + # 只在**本地 Name 目标**的赋值落地处调用;目标槽 target_slot 即该变量的 alloca。 + # ① RHS = `new heap slife T(...)`:登记该槽(首次建影子标志)→ 覆盖释放旧拥有对象 → 影子置 1; + # ② 其它 RHS(`new stack` / `new … hlife` / 裸名别名 `q = p` / 外来指针 / None): + # 槽已登记时交还所有权(释放旧拥有对象 + 影子置 0)——绝不把外来指针当自己的对象释放。 + # **必须在值写入之前调用**:覆盖释放要读到槽里的旧值,晚于写入就变成"释放刚写进去的对象"。 + def _slife_note_assign(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*, rhs_node: ast.AST*, target_name: str = None) -> int: + """M3:赋值处的 slife 所有权记账;返回 1=该槽此刻是编译器拥有的 slife 绑定""" + if target_slot is None or rhs_node is None or trans is None: return 0 + if rhs_node.kind() == ast.ASTKind.New: + nn: ast.New* = ast.New*(rhs_node) + owned: int = self.Gen.CallGenCtor._new_is_owned_heap(nn) + if owned == 0: + # §13 NLL:判据成立时该 `new heap` 也按"编译器托管的堆对象"处理 —— + # 生命期已是 FRAME(propagate_life 同一谓词),这里补上**出口释放**这一半。 + owned = self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, target_name) + if owned != 0: + sh: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot) + if sh is None: return 0 + self._SlifeDropOwned(pool, builder, mod, trans, target_slot) + self.Gen.VarGen._SlifeNoteStore(pool, builder, sh, 1) + return 1 + # 自赋值(p = p):保持原状;否则会"先释放再放弃所有权",把对象漏掉 + if rhs_node.kind() == ast.ASTKind.Name: + rn: ast.Name* = ast.Name*(rhs_node) + if rn.id is not None: + src_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn.id) + if src_slot is not None and uint64(src_slot) == uint64(target_slot): return 0 + # P4 所有权转移:`q = p` 后 **q 接管生命周期** —— 影子标志从 p 移到 q + # (q 置 1、p 置 0)。此后对 p 重新赋值不会释放对象(p 已不拥有该对象), + # 出口只释放 q 一次。不转移的话,`p = None` 会把 q 指向的对象提前释放 → q 悬垂。 + if src_slot is not None: + if self.Gen.VarGen._SlifeOwned(src_slot) != 0: + tsh: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot) + if tsh is not None: + # 覆盖释放 target 原有对象:若它与源是**同一个对象**(自别名 `q = p; q = p`) + # 则跳过,否则会把两个槽共持的对象提前释放。 + self.Gen.VarGen._SlifeDropOwnedUnlessSame(pool, builder, mod, trans, target_slot, src_slot) + self.Gen.VarGen._SlifeNoteStore(pool, builder, tsh, 1) + ssh: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(src_slot) + if ssh is not None: self.Gen.VarGen._SlifeNoteStore(pool, builder, ssh, 0) + return 1 + self._SlifeDropOwned(pool, builder, mod, trans, target_slot) + return 0 + + def _is_addrlike_call(self, node: ast.AST*) -> int: + """Return 1 if the rhs is an explicit pointer expression (c.Addr/c.Deref/c.Cast/c.Ptr call, or new-syntax `&x` / `*p`).""" + if node is None: return 0 + # New syntax (R3/R4): `&x` and `*p` parse as UnaryOp(AddrOf) / UnaryOp(Deref) and replace the + # c.Addr / c.Deref call forms. A pointer-valued RHS must not take the module-global + # "object with static storage -> copy the object" branch, so both forms count as addrlike. + if node.kind() == ast.ASTKind.UnaryOp: + uo_al: ast.UnaryOp* = ast.UnaryOp*(node) + if uo_al.op == ast.OpKind.AddrOf: return 1 + if uo_al.op == ast.OpKind.Deref: return 1 + return 0 + if node.kind() != ast.ASTKind.Call: return 0 + cl: ast.Call* = ast.Call*(node) + if cl.func is None or cl.func.kind() != ast.ASTKind.Attribute: return 0 + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.value is None or at.value.kind() != ast.ASTKind.Name: return 0 + vn: ast.Name* = ast.Name*(at.value) + if vn.id is None or string.strcmp(vn.id, "c") != 0: return 0 + if at.attr is None: return 0 + if string.strcmp(at.attr, "Addr") == 0: return 1 + if string.strcmp(at.attr, "Deref") == 0: return 1 + if string.strcmp(at.attr, "Cast") == 0: return 1 + if string.strcmp(at.attr, "Ptr") == 0: return 1 + return 0 + + def _is_64bit_scalar_ty(self, node: ast.AST*) -> int: + """Return 1 for a 64-bit scalar type (CUInt64T/CInt64T/CSizeT).""" + if node is None: return 0 + ty_nm: str = None + nk: int = node.kind() + if nk == ast.ASTKind.Name: + nm64: ast.Name* = ast.Name*(node) + ty_nm = nm64.id + elif nk == ast.ASTKind.Attribute: + at64: ast.Attribute* = ast.Attribute*(node) + ty_nm = at64.attr + if ty_nm is None: return 0 + if string.strcmp(ty_nm, "CUInt64T") == 0: return 1 + if string.strcmp(ty_nm, "CInt64T") == 0: return 1 + if string.strcmp(ty_nm, "CSizeT") == 0: return 1 + return 0 + + def _is_scalar_ty_name(self, ty_name: str) -> int: + """Return 1 if the type name is a known pure scalar (whitelist).""" + if ty_name is None: return 0 + # Strip the t./c. namespace prefix so int and bare aliases are judged the same + if ty_name[0] == 't' and ty_name[1] == '.': ty_name += 2 + elif ty_name[0] == 'c' and ty_name[1] == '.': ty_name += 2 + if string.strchr(ty_name, '|') is not None: return 0 + if string.strchr(ty_name, '*') is not None: return 0 + if string.strcmp(ty_name, "str") == 0: return 0 + if string.strcmp(ty_name, "bytes") == 0: return 0 + if string.strcmp(ty_name, "CPtr") == 0: return 0 + if string.strstr(ty_name, "Ptr") is not None: return 0 + # Whitelist: known pure scalars (base names + stdint uppercase aliases) + if string.strcmp(ty_name, "int") == 0: return 1 + if string.strcmp(ty_name, "char") == 0: return 1 + # Python builtin scalar names (float/double/bool) pass through as scalar types + if string.strcmp(ty_name, "float") == 0: return 1 + if string.strcmp(ty_name, "double") == 0: return 1 + if string.strcmp(ty_name, "bool") == 0: return 1 + if string.strcmp(ty_name, "CInt") == 0: return 1 + if string.strcmp(ty_name, "CUnsignedInt") == 0: return 1 + if string.strcmp(ty_name, "CUnsigned") == 0: return 1 + if string.strcmp(ty_name, "CChar") == 0: return 1 + if string.strcmp(ty_name, "CUnsignedChar") == 0: return 1 + if string.strcmp(ty_name, "CSignedChar") == 0: return 1 + if string.strcmp(ty_name, "CShort") == 0: return 1 + if string.strcmp(ty_name, "CUnsignedShort") == 0: return 1 + if string.strcmp(ty_name, "CLong") == 0: return 1 + if string.strcmp(ty_name, "CUnsignedLong") == 0: return 1 + if string.strcmp(ty_name, "CLongLong") == 0: return 1 + if string.strcmp(ty_name, "CUnsignedLongLong") == 0: return 1 + if string.strcmp(ty_name, "CSizeT") == 0: return 1 + if string.strcmp(ty_name, "CBool") == 0: return 1 + # Character width family: widths 8/16/32 are not 8-byte pointer slots + if string.strcmp(ty_name, "CWCharT") == 0: return 1 + if string.strcmp(ty_name, "CChar8T") == 0: return 1 + if string.strcmp(ty_name, "CChar16T") == 0: return 1 + if string.strcmp(ty_name, "CChar32T") == 0: return 1 + if string.strcmp(ty_name, "BOOL") == 0: return 1 + if string.strcmp(ty_name, "BYTE") == 0: return 1 + if string.strcmp(ty_name, "UCHAR") == 0: return 1 + if string.strcmp(ty_name, "CHAR") == 0: return 1 + if string.strcmp(ty_name, "SHORT") == 0: return 1 + if string.strcmp(ty_name, "USHORT") == 0: return 1 + if string.strcmp(ty_name, "LONG") == 0: return 1 + if string.strcmp(ty_name, "ULONG") == 0: return 1 + if string.strcmp(ty_name, "LONGLONG") == 0: return 1 + if string.strcmp(ty_name, "ULONGLONG") == 0: return 1 + if string.strcmp(ty_name, "INT") == 0: return 1 + if string.strcmp(ty_name, "UINT") == 0: return 1 + if string.strcmp(ty_name, "WORD") == 0: return 1 + if string.strcmp(ty_name, "DWORD") == 0: return 1 + if string.strcmp(ty_name, "QWORD") == 0: return 1 + # Lowercase bare C type aliases from builtins (same table) + if string.strcmp(ty_name, "uchar") == 0: return 1 + if string.strcmp(ty_name, "short") == 0: return 1 + if string.strcmp(ty_name, "ushort") == 0: return 1 + if string.strcmp(ty_name, "uint") == 0: return 1 + if string.strcmp(ty_name, "long") == 0: return 1 + if string.strcmp(ty_name, "ulong") == 0: return 1 + if string.strcmp(ty_name, "longlong") == 0: return 1 + if string.strcmp(ty_name, "ulonglong") == 0: return 1 + # Fixed-width int/float (C*T forms and bare aliases, prefix-matched) + if string.strstr(ty_name, "Int8") is not None: return 1 + if string.strstr(ty_name, "Int16") is not None: return 1 + if string.strstr(ty_name, "Int32") is not None: return 1 + if string.strstr(ty_name, "Int64") is not None: return 1 + if string.strstr(ty_name, "UInt8") is not None: return 1 + if string.strstr(ty_name, "UInt16") is not None: return 1 + if string.strstr(ty_name, "UInt32") is not None: return 1 + if string.strstr(ty_name, "UInt64") is not None: return 1 + if string.strstr(ty_name, "Float") is not None: return 1 + if string.strstr(ty_name, "FLOAT") is not None: return 1 + if string.strstr(ty_name, "Double") is not None: return 1 + if string.strstr(ty_name, "DOUBLE") is not None: return 1 + # Unknown class name => not a scalar, use an 8-byte pointer slot + return 0 + + def _is_generic_tp_or_nested(self, node: ast.AST*) -> int: + """Return 1 for a generic type-param name or nested union (BinOp).""" + if node is None: return 0 + nk: int = node.kind() + if nk == ast.ASTKind.BinOp: return 1 + if nk == ast.ASTKind.Name: + nm_tp: ast.Name* = ast.Name*(node) + if nm_tp.id is None: return 0 + tp_len: sizet = string.strlen(nm_tp.id) + if tp_len == 1: + if nm_tp.id[0] >= 65 and nm_tp.id[0] <= 90: return 1 + return 0 + + # _init_global_array_from_list - initialize a global array from a list literal + def _init_global_array_from_list(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, array_alloca: mlirlite.Value*, list_node: ast.AST*, trans: HT.Translator*, owner_ctx: str = None) -> int: + """Initialize a global array from a list literal via per-element GEP+store.""" + if builder is None or array_alloca is None or list_node is None: return 1 + + # Get the array type (pointee of [N x elem_ty]*) + arr_ty: mlirlite.LLVMType* = None + if array_alloca.Ty is not None: arr_ty = array_alloca.Ty.Pointee + if arr_ty is None: return 1 + + # Match the array type to get the element type and declared length + elem_ty: mlirlite.LLVMType* = None + arr_count: int = 0 + match arr_ty: + case mlirlite.LLVMType.Array(et, cnt): + elem_ty = et + arr_count = cnt + if elem_ty is None: return 1 + + # Get the list elements + lst: ast.List* = ast.List*(list_node) + if lst is None or lst.elts is None: return 1 + elts: list[ast.AST*]* = lst.elts + elts_count: sizet = len(elts) + + # Special case: a single string literal initializes a char array byte by byte + if self.Gen.ExprGenBase.get_llvm_type_bits(elem_ty) == 8 and elts_count == 1 and arr_count >= 1: + e0_s: ast.AST* = elts.get(0) + if e0_s is not None and e0_s.kind() == ast.ASTKind.Constant: + c0_s: ast.Constant* = ast.Constant*(e0_s) + if c0_s.const_kind == ast.CONST_STR: + sv_s: str = ast.constant_get_str_val(c0_s) + if sv_s is not None: + n_s: sizet = string.strlen(sv_s) + if sizet(arr_count) < n_s: + self.Gen.TypeGenErr.fatal_error(list_node, f"string literal length {n_s} exceeds the declared char array length {arr_count}; out-of-bounds writes are forbidden", "OverflowError") + for bi_s in range(int(n_s)): + bidx_s: mlirlite.Value* = mlirlite.const_int32(pool, bi_s) + bp_s: mlirlite.Value* = mlirlite.build_gep_array(builder, arr_ty, elem_ty, array_alloca, bidx_s) + if bp_s is None: break + bvv: mlirlite.Value* = mlirlite.const_int32(pool, sv_s[bi_s]) + bsv: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, bvv, elem_ty, e0_s) + mlirlite.build_store(builder, bsv, bp_s) + return 0 + + # Element count exceeds declared length: fail fast (avoid out-of-range GEP writes) + if arr_count > 0 and sizet(arr_count) < elts_count: + self.Gen.TypeGenErr.fatal_error(list_node, f"array initializer element count {elts_count} exceeds the declared array length {arr_count}; out-of-bounds writes are forbidden", "OverflowError") + + # Emit GEP + store for each element + for i in range(int(elts_count)): + elem_node: ast.AST* = elts.get(i) + # Explicit null check (repo convention: is None); an empty slot means an element count mismatch. + # When the slot is null, fail fast with class/field/index context. + if elem_node is None: + ctx_dbg: str = "" + if owner_ctx is not None: ctx_dbg = owner_ctx + src_line: int = 0 + src_col: int = 0 + if list_node is not None: + src_line = list_node.lineno + src_col = list_node.col_offset + self.Gen.TypeGenErr.fatal_error(list_node, f"global array initialization hit a null AST element: {ctx_dbg}element {i} is a bare null pointer (0), " f"but the list element count is {elts_count} (element and count mismatch). " f"somewhere upstream produced a null AST element (slot {i} of list-literal node {src_line}:{src_col} was not filled), " f"silently skipping/padding with zero is refused; compilation aborted.", "ValueError") + return 1 + # Object validity check (nonzero vptr): a zero vptr makes any virtual call read address 0 + elem_vptr: uint64 = *((uint64*)(elem_node)) + if elem_vptr == 0: + ctx_vt: str = "" + if owner_ctx is not None: ctx_vt = owner_ctx + vt_line: int = 0 + vt_col: int = 0 + if list_node is not None: + vt_line = list_node.lineno + vt_col = list_node.col_offset + self.Gen.TypeGenErr.fatal_error(list_node, f"global array initialization hit an invalid-object AST element (vptr is 0): {ctx_vt}" f"element {i} has a nonzero pointer (address {uint64(elem_node)}) but its object header (vtable pointer) is 0, " f"and any virtual call on it (kind()/dump()/...) reads address 0 at `load [0+slot]`. " f"list-literal node position {vt_line}:{vt_col}, element count {elts_count}. " f"somewhere upstream produced an AST node with an uninitialized vptr (typical source: a deep/block copy that moved only fields, not the vptr), " f"refusing to continue; compilation aborted.", "ValueError", vt_col) + return 1 + idx_val: mlirlite.Value* = mlirlite.const_int32(pool, i) + elem_ptr: mlirlite.Value* = mlirlite.build_gep_array(builder, arr_ty, elem_ty, array_alloca, idx_val) + if elem_ptr is None: continue + # Nested array: element type is an array and the node is a list literal => recurse + inner_arr: int = 0 + if self.Gen.ExprGenBase.is_array_type(elem_ty) != 0: + # Null check for nested element: use is not None (not integer 0) + if elem_node is not None and elem_node.kind() == ast.ASTKind.List: inner_arr = 1 + if inner_arr != 0: + self._init_global_array_from_list(builder, pool, mod, elem_ptr, elem_node, trans, owner_ctx) + continue + elem_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, elem_node, None, 0, trans) + if elem_val is None: + # Element translation failed: fail fast to expose the upstream cause + self.Gen.TypeGenErr.fatal_error(elem_node, "global array initializer element translation failed", "TypeError") + # Type coercion (e.g. i32 -> i8); Pointee is statically i64 so compare with != 0 + store_val: mlirlite.Value* = elem_val + if elem_ptr.Ty is not None and elem_ptr.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, elem_val, elem_ptr.Ty.Pointee, elem_node) + mlirlite.build_store(builder, store_val, elem_ptr) + + return 0 + + # _apply_aug_op - apply the binary op of an augmented assignment (float, pointer, integer) + def _apply_aug_op(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, op: int, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, trans: HT.Translator* = None, lhs_node: ast.AST* = None) -> mlirlite.Value*: + """Apply the binary op of an augmented assignment (float/pointer/int).""" + # Power aug-assign: base types use llvm.pow via ExprGenOps.emit_pow + if op == ast.OpKind.Pow: + if trans is None: return None + return self.Gen.ExprGenOps.emit_pow(pool, builder, trans.Module, lhs, rhs, node, trans) + lhs_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(lhs.Ty) + rhs_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(rhs.Ty) + lhs_fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(lhs.Ty) + rhs_fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(rhs.Ty) + + # Float op when either operand is float (check before pointer arithmetic) + if lhs_fbits != 0 or rhs_fbits != 0: + # Pick the target float type (the wider one) + target_fbits: int = lhs_fbits + if rhs_fbits > target_fbits: target_fbits = rhs_fbits + target_float_ty: mlirlite.LLVMType* = mlirlite.Double(pool) + if target_fbits == 32: target_float_ty = mlirlite.Float(pool) + # Coerce both operands to the target float type + lhs = self.Gen.ExprGenBase.coerce_to_type(builder, lhs, target_float_ty, node) + rhs = self.Gen.ExprGenBase.coerce_to_type(builder, rhs, target_float_ty, node) + if op == ast.OpKind.Add: return mlirlite.build_fadd(builder, lhs, rhs) + if op == ast.OpKind.Sub: return mlirlite.build_fsub(builder, lhs, rhs) + if op == ast.OpKind.Mult: return mlirlite.build_fmul(builder, lhs, rhs) + if op == ast.OpKind.Div: return mlirlite.build_fdiv(builder, lhs, rhs) + if op == ast.OpKind.Mod: return mlirlite.build_frem(builder, lhs, rhs) + return None + + # Pointer arithmetic: ptr += int / ptr -= int -> getelementptr (scaled by element size) + if lhs_bits == 0 and rhs_bits != 0: + if op == ast.OpKind.Add or op == ast.OpKind.Sub: return self.Gen.ExprGenBase.translate_ptr_arith(pool, builder, lhs, rhs, op) + return None + + # Double-pointer arithmetic: delegate the check to ExprGenOps.fail_if_double_ptr_binop + self.Gen.ExprGenOps.fail_if_double_ptr_binop(pool, op, lhs, rhs, node, 1) + + # x op= y 的操作数约束(fail-fast,先于运算与提升): + # ① y 的有效宽度必须 <= x 的宽度。变量按其 LLVM 类型宽度;常量按其值的**最小可存宽度** + # (所以 `u8 += 1` 合法、`u8 += 300` 报错)。y 更宽 = 隐式收窄,必须先显式收窄再写。 + # ② 不允许 y 显式有符号而 x 无符号:负值经符号扩展会变成巨大的无符号值,属静默语义改变。 + # 移位(<<= / >>=)**豁免** ①②:计数不是写回目标的值,改用静态范围检查(见下方)。 + y_node_aa: ast.AST* = None + if node is not None and node.kind() == ast.ASTKind.AugAssign: y_node_aa = ast.AugAssign*(node).value + if lhs_bits != 0 and rhs_bits != 0 and op != ast.OpKind.LShift and op != ast.OpKind.RShift: + y_bits_eff: int = rhs_bits + if y_node_aa is not None and y_node_aa.kind() == ast.ASTKind.Constant: + cn_aa: ast.Constant* = ast.Constant*(y_node_aa) + if cn_aa.const_kind == ast.CONST_INT: y_bits_eff = self.define_min_width(cn_aa.int_val) + if y_bits_eff > lhs_bits: + self.Gen.TypeGenErr.fatal_error(node, f"[VPC] augmented assignment operand too wide: the right-hand side needs {y_bits_eff} bits, " f"but the target provides only {lhs_bits} bits; an implicit narrowing is forbidden. " f"Fix: narrow the right-hand side explicitly (e.g. uint{lhs_bits}(...) / int{lhs_bits}(...)) before the operation.", "CastError") + x_uns_aa: int = 0 + if lhs_node is not None: x_uns_aa = self.Gen.ExprGenOps._node_type_unsigned(pool, lhs_node, trans) + y_sgn_aa: int = 0 + if y_node_aa is not None: y_sgn_aa = self.Gen.ExprGenOps._node_type_signed(pool, y_node_aa, trans) + if x_uns_aa != 0 and y_sgn_aa != 0: + self.Gen.TypeGenErr.fatal_error(node, "[VPC] augmented assignment operand signedness conflict: the right-hand side is explicitly signed " "while the target is unsigned; sign extension would turn negative values into huge unsigned values. " "Fix: use an unsigned right-hand side (e.g. uint32(...)), or make the target signed.", "CastError") + + # Integer op: type promotion + if lhs_bits > rhs_bits and rhs_bits > 0: rhs = self.Gen.ExprGenBase.coerce_to_type(builder, rhs, lhs.Ty, node, trans) + elif rhs_bits > lhs_bits and lhs_bits > 0: lhs = self.Gen.ExprGenBase.coerce_to_type(builder, lhs, rhs.Ty, node, trans) + + # 移位:计数本身豁免宽度/符号规则,改按**提升后**的左操作数位宽做静态范围检查 + if op == ast.OpKind.LShift or op == ast.OpKind.RShift: + self.require_shift_count_in_range(pool, trans, node, y_node_aa, self.Gen.ExprGenBase.get_llvm_type_bits(lhs.Ty)) + + # Unsigned operands use udiv/urem; signed use sdiv/srem + aug_unsigned: int = 0 + if lhs_node is not None: + if self.Gen.ExprGenOps._node_type_unsigned(pool, lhs_node, trans) != 0: aug_unsigned = 1 + + if op == ast.OpKind.Add: return mlirlite.build_add(builder, lhs, rhs) + elif op == ast.OpKind.Sub: return mlirlite.build_sub(builder, lhs, rhs) + elif op == ast.OpKind.Mult: return mlirlite.build_mul(builder, lhs, rhs) + elif op == ast.OpKind.Div: + if aug_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) + return mlirlite.build_sdiv(builder, lhs, rhs) + elif op == ast.OpKind.FloorDiv: + if aug_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) + return mlirlite.build_sdiv(builder, lhs, rhs) + elif op == ast.OpKind.Mod: + if aug_unsigned != 0: return mlirlite.build_urem(builder, lhs, rhs) + return mlirlite.build_srem(builder, lhs, rhs) + elif op == ast.OpKind.BitAnd: return mlirlite.build_and(builder, lhs, rhs) + elif op == ast.OpKind.BitOr: return mlirlite.build_or(builder, lhs, rhs) + elif op == ast.OpKind.BitXor: return mlirlite.build_xor(builder, lhs, rhs) + elif op == ast.OpKind.LShift: return mlirlite.build_shl(builder, lhs, rhs) + elif op == ast.OpKind.RShift: + # Unsigned >> uses lshr, signed >> uses ashr (i8/i16 default unsigned) + if lhs_bits == 8 or lhs_bits == 16: return mlirlite.build_lshr(builder, lhs, rhs) + if lhs_node is not None and trans is not None: + if self.Gen.ExprGenOps._node_type_unsigned(pool, lhs_node, trans) != 0: return mlirlite.build_lshr(builder, lhs, rhs) + return mlirlite.build_ashr(builder, lhs, rhs) + return None + + # translate_augassign - translate augmented assignment statements + def const_assign_msg(self, kind: str, name: str, src_name: str) -> str: + """Build the const-assignment error message used by const_assign_fail.""" + decl: str = "(annotation contains t.CConst; the source name at the declaration was not recorded)" + if src_name is not None: decl = f"t.CConst | {src_name}" + return GenApi.escape_fstring_copy(f"cannot assign to const variable '{name}' ({kind}); " f"declaration type (including const): {decl}" f"; fix: remove the const descriptor / t.CConst from the declaration, or assign once at the declaration " f"and afterwards use only non-const copies/temporaries; a const object forbids assignment, " f"augmented assignment and element assignment within its scope, and this never degrades to a warning") + + def const_assign_fail(self, node: ast.AST*, kind: str, name: str, src_name: str) -> int: + """Fail fast when a const target is written.""" + self.Gen.TypeGenErr.fatal_error(node, self.const_assign_msg(kind, name, src_name), "TypeError") + return 0 + + def translate_augassign(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Translate an augmented assignment statement (x += 1).""" + if node is None: return 0 + + aug: ast.AugAssign* = ast.AugAssign*(node) + if aug is None: return 0 + + target: ast.AST* = aug.target + if target is None: return 0 + + tk: int = target.kind() + + # Attribute target: self.field += 1 (get_attribute_ptr -> load -> binop -> store) + if tk == ast.ASTKind.Attribute: + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + mod: mlirlite.LLVMModule* = trans.Module + + # const field: fail fast on the field write + at_ca: ast.Attribute* = ast.Attribute*(target) + if at_ca is not None and at_ca.attr is not None: + if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, target) != 0: + self.const_assign_fail(target, "field assignment", at_ca.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, target)) + + # 1. Get the field pointer + field_ptr: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, target, trans) + if field_ptr is None: + # fail-fast: attribute pointer resolution failed + self.Gen.TypeGenErr.fatal_error(target, "attribute ptr is None", "TypeError") + + # 2. Determine the field type and load the current value + target_ty: mlirlite.LLVMType* = None + if field_ptr.Ty is not None and field_ptr.Ty.Pointee != 0: target_ty = field_ptr.Ty.Pointee + if target_ty is None: + # fail-fast: field type missing, no i32 fallback + self.Gen.TypeGenErr.fatal_error(target, "field type information missing", "TypeError") + # t.CVolatile field: both load and store carry the volatile qualifier + vol_fa: int = self.Gen.ExprGenAttr.attr_field_is_volatile(pool, trans, target) + # rawptr[t.CVolatile|T] base: field load/store also carry volatile + at_aug: ast.Attribute* = ast.Attribute*(target) + if at_aug is not None: + if self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, at_aug.value) != 0: vol_fa = 1 + cur_val: mlirlite.Value* = mlirlite.build_load(builder, target_ty, field_ptr, vol_fa) + if cur_val is None: + # fail-fast: attribute current value load failed + self.Gen.TypeGenErr.fatal_error(target, "cannot load the current value of the attribute", "TypeError") + + # 3. Translate the RHS value + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) + if rhs_val is None: + # fail-fast: RHS translation failed + self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") + + # 4. Apply the binary op + result: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, cur_val, rhs_val, node, trans, target) + if result is None: + # fail-fast: attribute binary op failed + self.Gen.TypeGenErr.fatal_error(target, f"attribute binary op failed op={aug.op}", "TypeError") + + # 5. Coerce the type and store + result = self.Gen.ExprGenBase.coerce_to_type(builder, result, target_ty, node) + if result is None: + # fail-fast: type coercion failed + self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") + mlirlite.build_store(builder, result, field_ptr, vol_fa) + return 0 + + # Subscript target: self.state[i] += x (get_subscript_ptr -> load -> binop -> store) + if tk == ast.ASTKind.Subscript: + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + mod: mlirlite.LLVMModule* = trans.Module + + # const element target: fail fast when the base variable or field is const + sub_ca: ast.Subscript* = ast.Subscript*(target) + if sub_ca is not None and sub_ca.value is not None: + if sub_ca.value.kind() == ast.ASTKind.Name: + nm_ca: ast.Name* = ast.Name*(sub_ca.value) + if nm_ca.id is not None: + ve_ca: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ca.id) + if self.Gen.VarGen.var_is_const(ve_ca) != 0: + self.const_assign_fail(target, "element assignment", nm_ca.id, self.Gen.VarGen.var_annot_class_name(ve_ca)) + elif sub_ca.value.kind() == ast.ASTKind.Attribute: + at_ca2: ast.Attribute* = ast.Attribute*(sub_ca.value) + if at_ca2.attr is not None: + if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, sub_ca.value) != 0: + self.const_assign_fail(target, "element assignment", at_ca2.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, sub_ca.value)) + + # 1. Get the element pointer + elem_ptr: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, target, trans) + if elem_ptr is None: + # fail-fast: subscript pointer resolution failed + self.Gen.TypeGenErr.fatal_error(target, "subscript ptr is None", "TypeError") + + # 2. Determine the element type and load the current value + target_ty: mlirlite.LLVMType* = None + if elem_ptr.Ty is not None and elem_ptr.Ty.Pointee != 0: target_ty = elem_ptr.Ty.Pointee + if target_ty is None: + # fail-fast: element type missing, no i32 fallback + self.Gen.TypeGenErr.fatal_error(target, "element type information missing", "TypeError") + # rawptr[t.CVolatile|T] base: p[i] load/store carry volatile when the base is resolvable + vol_sa: int = 0 + sub_aug: ast.Subscript* = ast.Subscript*(target) + if sub_aug is not None: vol_sa = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, sub_aug.value) + cur_val: mlirlite.Value* = mlirlite.build_load(builder, target_ty, elem_ptr, vol_sa) + if cur_val is None: + # fail-fast: element current value load failed + self.Gen.TypeGenErr.fatal_error(target, "cannot load the current value of the subscript element", "TypeError") + + # 3. Translate the RHS value + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) + if rhs_val is None: + # fail-fast: RHS translation failed + self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") + + # 4. Apply the binary op + result: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, cur_val, rhs_val, node, trans, target) + if result is None: + # fail-fast: subscript binary op failed + self.Gen.TypeGenErr.fatal_error(target, f"subscript binary op failed op={aug.op}", "TypeError") + + # 5. Coerce the type and store + result = self.Gen.ExprGenBase.coerce_to_type(builder, result, target_ty, node) + if result is None: + # fail-fast: type coercion failed + self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") + mlirlite.build_store(builder, result, elem_ptr, vol_sa) + return 0 + + # UnaryOp target: *p += v (deref augmented assignment: load, op, store back) + if tk == ast.ASTKind.UnaryOp: + uo_ag: ast.UnaryOp* = ast.UnaryOp*(target) + if uo_ag is None or uo_ag.operand is None: self.Gen.TypeGenErr.fatal_error(target, "augmented-assignment target unary op is missing an operand", "TypeError") + if uo_ag.op != ast.OpKind.Deref: + self.Gen.TypeGenErr.fatal_error(target, "augmented-assignment target supports only the unary '*' dereference (&x / -x / ~x are not valid lvalues)", "SyntaxError") + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + mod: mlirlite.LLVMModule* = trans.Module + ptr_ag: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo_ag.operand, None, 0, trans) + if ptr_ag is None or ptr_ag.Ty is None or self.Gen.ExprGenBase.is_ptr_type(ptr_ag.Ty) == 0: + self.Gen.TypeGenErr.fatal_error(target, "the operand of augmented-assignment '*' must be a pointer type (type inference degraded)", "TypeError") + ty_ag: mlirlite.LLVMType* = ptr_ag.Ty.Pointee + if ty_ag is None: + self.Gen.TypeGenErr.fatal_error(target, "deref augmented-assignment target type missing (pointee is empty)", "TypeError") + old_ag: mlirlite.Value* = mlirlite.build_load(builder, ty_ag, ptr_ag, 0) + if old_ag is None: self.Gen.TypeGenErr.fatal_error(target, "cannot load the current dereferenced value", "TypeError") + rhs_ag: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) + if rhs_ag is None: self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") + res_ag: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, old_ag, rhs_ag, node, trans, target) + if res_ag is None: self.Gen.TypeGenErr.fatal_error(target, f"deref binary op failed op={aug.op}", "TypeError") + res_ag = self.Gen.ExprGenBase.coerce_to_type(builder, res_ag, ty_ag, node, trans) + if res_ag is None: self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") + mlirlite.build_store(builder, res_ag, ptr_ag, 0) + return 0 + + if tk != ast.ASTKind.Name: + # fail-fast: unsupported AugAssign target kind + self.Gen.TypeGenErr.fatal_error(target, "AugAssign target type unsupported; only Name/Attribute/Subscript/*dereference are supported", "TypeError") + + nm: ast.Name* = ast.Name*(target) + if nm is None or nm.id is None: return 0 + + # const variable: fail fast immediately + ve_aug: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) + if self.Gen.VarGen.var_is_const(ve_aug) != 0: + self.const_assign_fail(target, "augmented assignment", nm.id, self.Gen.VarGen.var_annot_class_name(ve_aug)) + + # REPL session variable aug-assign: read slot, apply op, write slot and result area + if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: + if self.Gen.VarGen.repl_try_augassign(trans, nm.id, aug.op, aug.value, node) != 0: return 0 + + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + mod: mlirlite.LLVMModule* = trans.Module + + # 1. Determine scope: 0=local, 1=global, 2=nonlocal + scope_type: int = 0 + if GenApi.is_global_name(trans, nm.id) != 0: scope_type = 1 + elif GenApi.is_nonlocal_name(trans, nm.id) != 0: scope_type = 2 + + # t.CVolatile variable: both load and store carry the volatile qualifier + vol_aa: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) + + # 2. Load the current value + cur_val: mlirlite.Value* = None + target_alloca: mlirlite.Value* = None + target_ty: mlirlite.LLVMType* = None + + if scope_type == 1: + # global variable + target_alloca = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + if target_alloca is not None: + if target_alloca.Ty is None: + # fail-fast: variable type missing, no i32 fallback + self.Gen.TypeGenErr.fatal_error(target, "variable type information missing", "TypeError") + target_ty = target_alloca.Ty.Pointee + cur_val = mlirlite.build_load(builder, target_ty, target_alloca, vol_aa) + elif scope_type == 2: + # nonlocal variable (via closure env) + cur_val = self.Gen.StmtGenDecl.load_nonlocal_var(trans, nm.id) + if cur_val is not None: target_ty = cur_val.Ty + else: + # Ordinary local variable + target_alloca = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) + if target_alloca is not None: + if target_alloca.Ty is None: + # fail-fast: variable type missing, no i32 fallback + self.Gen.TypeGenErr.fatal_error(target, "variable type information missing", "TypeError") + target_ty = target_alloca.Ty.Pointee + cur_val = mlirlite.build_load(builder, target_ty, target_alloca, vol_aa) + + if cur_val is None: + # fail-fast: target variable current value load failed + self.Gen.TypeGenErr.fatal_error(target, f"cannot load the current value of target variable '{nm.id}'", "TypeError") + + # 3. Translate the RHS value + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aug.value, None, 0, trans) + if rhs_val is None: + # fail-fast: RHS translation failed + self.Gen.TypeGenErr.fatal_error(target, "rhs translation failed", "TypeError") + + # 3b. Operator overload: object += -> __iadd__, fall back to __add__ (locals only) + if scope_type == 0 and target_alloca is not None: + aug_dup: str = self._aug_op_to_dunder(aug.op) + if aug_dup is not None: + ovl_self_aug: mlirlite.Value* = target_alloca + # Pointer storage slot (Ptr(Ptr(Struct))): load the object pointer as self + if target_alloca.Ty is not None and target_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(target_alloca.Ty.Pointee) != 0: + load_self_aug: mlirlite.Value* = mlirlite.build_load(builder, target_alloca.Ty.Pointee, target_alloca) + if load_self_aug is not None: ovl_self_aug = load_self_aug + # Try the aug dunder only when the target is a struct (class instance) + if self.Gen.StructGen.get_struct_type_from_value(ovl_self_aug) is not None: + aug_extra: sizet* = pool.alloc(8) + if aug_extra is not None: + aug_extra[0] = sizet(rhs_val) + aug_ovl: mlirlite.Value* = self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, ovl_self_aug, aug_dup, trans, aug_extra, 1) + if aug_ovl is None: + aug_fb: str = self._op_to_dunder(aug.op) + if aug_fb is not None: aug_ovl = self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, ovl_self_aug, aug_fb, trans, aug_extra, 1) + if aug_ovl is not None: + aug_res: mlirlite.Value* = aug_ovl + if target_ty is not None and aug_res.Ty is not None and self.Gen.ExprGenBase._same_agg_ty(pool, aug_res.Ty, target_ty) == 0: + # Target is a struct value but the result is a pointer -> load the struct value + if self.Gen.ExprGenBase.is_ptr_type(target_ty) == 0 and self.Gen.ExprGenBase.is_ptr_type(aug_res.Ty) != 0 and aug_res.Ty.Pointee != 0: + aug_res = mlirlite.build_load(builder, aug_res.Ty.Pointee, aug_res) + else: + coerced_aug: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, aug_res, target_ty, node, trans) + if coerced_aug is not None: aug_res = coerced_aug + if aug_res is None or target_ty is None or self.Gen.ExprGenBase._same_agg_ty(pool, aug_res.Ty, target_ty) == 0: + # fail-fast: overload return type mismatch, do not corrupt memory silently + self.Gen.TypeGenErr.fatal_error(node, "augmented-assignment operator overload return type does not match the target variable", "TypeError") + mlirlite.build_store(builder, aug_res, target_alloca, vol_aa) + return 0 + + # 4. Coerce the type and apply the binary op + result: mlirlite.Value* = self._apply_aug_op(pool, builder, aug.op, cur_val, rhs_val, node, trans, target) + if result is None: + # fail-fast: binary op failed + self.Gen.TypeGenErr.fatal_error(target, f"applying binary op to variable '{nm.id}' failed op={aug.op}", "TypeError") + + # 5. Store the result + result = self.Gen.ExprGenBase.coerce_to_type(builder, result, target_ty, node) + if result is None: + # fail-fast: type coercion failed + self.Gen.TypeGenErr.fatal_error(target, "type coercion failed", "TypeError") + + if scope_type == 1: + # global variable + if target_alloca is not None: mlirlite.build_store(builder, result, target_alloca, vol_aa) + elif scope_type == 2: + # nonlocal variable + nl_ptr: mlirlite.Value* = self.Gen.StmtGenDecl.get_nonlocal_var_ptr(trans, nm.id) + if nl_ptr is not None: mlirlite.build_store(builder, result, nl_ptr) + else: + # Ordinary local variable + if target_alloca is not None: mlirlite.build_store(builder, result, target_alloca, vol_aa) + + return 0 + + # is_ctypedef_annotation - check whether an annotation is t.CTypedef + def is_ctypedef_annotation(self, annot: ast.AST*) -> int: + """Return 1 if the annotation is t.CTypedef, else 0.""" + return self.Gen.TypeGenCType._is_ctypedef_annotation(annot) + + # _is_single_param_carray_annot - check for single-param t.CArray[elem_ty] + def _is_single_param_carray_annot(self, annot: ast.AST*) -> int: + """Return 1 for a single-param or None-length t.CArray annotation.""" + if annot is None or annot.kind() != ast.ASTKind.Subscript: return 0 + sub: ast.Subscript* = ast.Subscript*(annot) + if sub.value is None or sub.slice is None: return 0 + if sub.value.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(sub.value) + if at.attr is None or string.strcmp(at.attr, "CArray") != 0: return 0 + if at.value is None or at.value.kind() != ast.ASTKind.Name: return 0 + mv: ast.Name* = ast.Name*(at.value) + if mv.id is None or string.strcmp(mv.id, "t") != 0: return 0 + elif sub.value.kind() == ast.ASTKind.Name: + # Bare CArray / builtin rawarray / builtin array are all equivalent to t.CArray + bn: ast.Name* = ast.Name*(sub.value) + if bn.id is None or (string.strcmp(bn.id, "CArray") != 0 and string.strcmp(bn.id, "rawarray") != 0 and string.strcmp(bn.id, "array") != 0): return 0 + else: return 0 + # Single-param form: slice is not a Tuple (e.g. t.CArray[u64]) + if sub.slice.kind() != ast.ASTKind.Tuple: return 1 + # Two-param form with count=None is equivalent to single-param + tup: ast.Tuple* = ast.Tuple*(sub.slice) + if tup.elts is None or len(tup.elts) < 2: return 0 + cnt_node: ast.AST* = tup.elts.get(1) + if cnt_node is not None and cnt_node.kind() == ast.ASTKind.Constant: + cn0: ast.Constant* = ast.Constant*(cnt_node) + if cn0.const_kind == ast.CONST_NONE: return 1 + return 0 + + # _infer_static_array_ty_from_literal - single-param CArray + list literal -> [len x elem_ty] + def _infer_static_array_ty_from_literal(self, pool: memhub.MemBuddy*, annot: ast.AST*, value: ast.AST*, resolved_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: + """Return Array(elem_ty, len) for a single-param CArray with a list literal.""" + if value is not None and value.kind() == ast.ASTKind.List and resolved_ty is not None: + if self._is_single_param_carray_annot(annot) != 0: + elem_ty_arr: mlirlite.LLVMType* = resolved_ty.Pointee + lst_arr: ast.List* = ast.List*(value) + cnt_arr: int = 0 + if lst_arr.elts is not None: cnt_arr = int(len(lst_arr.elts)) + # Special case: a single string literal means length strlen+1 (include the NUL) + if elem_ty_arr is not None and cnt_arr == 1 and lst_arr.elts is not None: + e0_arr: ast.AST* = lst_arr.elts.get(0) + if e0_arr is not None and e0_arr.kind() == ast.ASTKind.Constant: + c0_arr: ast.Constant* = ast.Constant*(e0_arr) + if c0_arr.const_kind == ast.CONST_STR: + sv0_arr: str = ast.constant_get_str_val(c0_arr) + if sv0_arr is not None and self.Gen.ExprGenBase.get_llvm_type_bits(elem_ty_arr) == 8: return mlirlite.Array(pool, elem_ty_arr, int(string.strlen(sv0_arr)) + 1) + if elem_ty_arr is not None and cnt_arr > 0: return mlirlite.Array(pool, elem_ty_arr, cnt_arr) + return resolved_ty + + # _build_ann_fail_msg - build a detailed annotation-resolution-failed error + def _build_ann_fail_msg(self, pool: memhub.MemBuddy*, annot: ast.AST*, nm_id: str) -> str: + """Build a detailed annotation-resolution-failure message (heap buffer).""" + ann_txt_x: str = None + if annot is not None: ann_txt_x = self.Gen.TypeGenResolve._get_type_name_from_annotation(pool, annot) + if ann_txt_x is None: ann_txt_x = "?" + btd_nm_x: str = self.Gen.TypeGenCType.LastBuiltinsTypedefName + btd_rs_x: str = self.Gen.TypeGenCType.LastBuiltinsTypedefResult + msg_len: sizet = 640 + msg_buf: str = stdlib.malloc(msg_len) + if msg_buf is None: return ann_txt_x + if btd_nm_x is not None and btd_rs_x is not None: viperlib.snprintf(msg_buf, msg_len, "variable type annotation resolution failed, variable name: %s, annotation: %s. last builtins match: %s expanded to %s, still unresolvable", nm_id, ann_txt_x, btd_nm_x, btd_rs_x) + elif btd_nm_x is not None: viperlib.snprintf(msg_buf, msg_len, "variable type annotation resolution failed, variable name: %s, annotation: %s. last builtins match: %s missed (builtins.vp has no such typedef, or the scan did not run)", nm_id, ann_txt_x, btd_nm_x) + else: viperlib.snprintf(msg_buf, msg_len, "variable type annotation resolution failed, variable name: %s, annotation: %s. last builtins match: not triggered", nm_id, ann_txt_x) + return msg_buf + + # translate_annassign_prescan - pre-create alloca for annotated vars + def translate_annassign_prescan(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Pre-create allocas for AnnAssign variables at the entry block.""" + if node is None: return 0 + k: int = node.kind() + if k != ast.ASTKind.AnnAssign: return 0 + + aa: ast.AnnAssign* = ast.AnnAssign*(node) + if aa is None or aa.target is None: return 0 + target: ast.AST* = aa.target + if target.kind() != ast.ASTKind.Name: return 0 + + nm: ast.Name* = ast.Name*(target) + if nm.id is None: return 0 + + # CDefine annotation: register the constant instead of creating an alloca + if self.is_cdefine_annotation(aa.annotation) != 0: + pool_ps: memhub.MemBuddy* = trans.Pool + self._register_cdefine_from_value(pool_ps, nm.id, aa.value) + return 0 + + # CTypedef annotation: register the alias only, no storage (avoids duplicate symbols) + if self.is_ctypedef_annotation(aa.annotation) != 0: return 0 + + # REPL fragment entry: AnnAssign goes to the session slot; skip pre-creating a local alloca + if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: return 0 + + # global/nonlocal variables need no local alloca + if GenApi.is_global_name(trans, nm.id) != 0: return 0 + if GenApi.is_nonlocal_name(trans, nm.id) != 0: return 0 + + # Module-level globals are already registered; only skip the local alloca at module scope + if self.Gen.VarGen.is_module_scope(trans.SymTab) != 0: + if self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) is not None: return 0 + + # Check whether it already exists + existing: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) + if existing is not None: return 0 + + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + + # Infer the type from the annotation (no hard-coded i32) + var_ty: mlirlite.LLVMType* = None + annot: ast.AST* = aa.annotation + if annot is not None: + # Value-reference markers are parameter-only: a local variable `x: &T` fails fast. + self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "a local variable declaration") + # Specialize generics before resolving so list[X] does not degrade to opaque list* + self._prespecialize_generic_annotation(trans, pool, annot) + resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0) + if resolved is not None: + var_ty = resolved + # Single-param CArray + list literal -> static array; keep PreScan and Handle types in sync + var_ty = self._infer_static_array_ty_from_literal(pool, annot, aa.value, var_ty) + # [D31 fail-fast] generic annotation degraded to a bare base class + self._fail_on_degenerated_generic_annotation(aa, nm.id, annot, var_ty) + else: + # fail-fast: annotation resolution failed (no silent i32 fallback) + self.Gen.TypeGenErr.fatal_error(aa, self._build_ann_fail_msg(pool, annot, nm.id), "TypeError") + else: + # fail-fast: AnnAssign must have an annotation + self.Gen.TypeGenErr.fatal_error(aa, f"AnnAssign is missing a type annotation, variable name: {nm.id}", "SyntaxError") + + # Ensure the full cross-module struct definition is available (for alloca) + self.Gen.StructGen.ensure_struct_def_in_module(pool, trans.Module, var_ty) + alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, var_ty) + if alloca is not None: + if self.Gen.VarGen.define_var(trans.SymTab, nm.id, alloca) == 0: + # Store the annotation's class name; pass pool so generic Subscript yields the mangled name + if annot is not None: + cls_nm_ps: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(annot, trans._imported_modules, pool, trans._from_imports) + if cls_nm_ps is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, nm.id, cls_nm_ps) + # Check for bytes* or str* (i8** semantics, 8-byte stride) + if self._is_ptr_element_annotation(annot) != 0: self.Gen.VarGen.set_var_ptr_element(trans.SymTab, nm.id) + # const object (annotation has t.CConst and the type is not a pointer): no writes in scope + if self.Gen.TypeGenErr.is_const_object_marker(annot, var_ty) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, nm.id) + # t.CVolatile: mark the variable at declaration; its load/store carry volatile + if self.Gen.TypeGenErr.has_decorator_marker(annot, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) + # rawptr[t.CVolatile | T]: dereference load/store carry volatile + if self.Gen.TypeGenErr.has_pointee_volatile_marker(annot) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, nm.id) + return 0 + return 0 + + # _prespecialize_generic_annotation - register the generic specialization before resolving + def _prespecialize_generic_annotation(self, trans: HT.Translator*, pool: memhub.MemBuddy*, annot: ast.AST*) -> int: + """Pre-specialize a generic annotation; return 1 if attempted.""" + if annot is None or trans is None: return 0 + # Take the generic Subscript side: the Subscript itself or the Subscript of A* + spec_annot: ast.Subscript* = None + if annot.kind() == ast.ASTKind.Subscript: spec_annot = ast.Subscript*(annot) + elif annot.kind() == ast.ASTKind.BinOp: + bop_ps: ast.BinOp* = ast.BinOp*(annot) + if bop_ps.op == ast.OpKind.BitOr: + if bop_ps.left is not None and bop_ps.left.kind() == ast.ASTKind.Subscript: spec_annot = ast.Subscript*(bop_ps.left) + elif bop_ps.right is not None and bop_ps.right.kind() == ast.ASTKind.Subscript: spec_annot = ast.Subscript*(bop_ps.right) + if spec_annot is None: return 0 + sv_ps: ast.AST* = spec_annot.value + if sv_ps is None or sv_ps.kind() != ast.ASTKind.Name: return 0 + sub_nm: ast.Name* = ast.Name*(sv_ps) + if sub_nm.id is None: return 0 + template_cd: ast.ClassDef* = self.Gen.ClassGenGeneric._find_generic_template(sub_nm.id) + if template_cd is None: return 0 + type_args_ps: list[str]* = self.Gen.CallGenCtor._extract_type_args_from_slice(pool, spec_annot.slice, trans._from_imports) + if type_args_ps is None or len(type_args_ps) == 0: return 0 + self.Gen.ClassGenGeneric._specialize_generic_class(trans, sub_nm.id, type_args_ps) + return 1 + + # translate_annassign - translate an AnnAssign statement; returns new variable count + def _fail_on_degenerated_generic_annotation(self, aa: ast.AnnAssign*, var_name: str, annot: ast.AST*, var_ty: mlirlite.LLVMType*) -> int: + """Fail fast when a generic annotation degrades to a bare base pointer.""" + if aa is None or annot is None or var_ty is None: return 0 + # Take the generic Subscript side: the Subscript itself or the Subscript of A* + ann_sub: ast.Subscript* = None + if annot.kind() == ast.ASTKind.Subscript: ann_sub = ast.Subscript*(annot) + elif annot.kind() == ast.ASTKind.BinOp: + bop_ff: ast.BinOp* = ast.BinOp*(annot) + if bop_ff.op == ast.OpKind.BitOr: + if bop_ff.left is not None and bop_ff.left.kind() == ast.ASTKind.Subscript: ann_sub = ast.Subscript*(bop_ff.left) + elif bop_ff.right is not None and bop_ff.right.kind() == ast.ASTKind.Subscript: ann_sub = ast.Subscript*(bop_ff.right) + if ann_sub is None or ann_sub.value is None or ann_sub.value.kind() != ast.ASTKind.Name: return 0 + base_nm_ff: ast.Name* = ast.Name*(ann_sub.value) + if base_nm_ff.id is None: return 0 + # Only treat it as degraded when the base is a real generic template + if self.Gen.ClassGenGeneric._find_generic_template(base_nm_ff.id) is None: return 0 + if var_ty.Pointee == 0: return 0 + vt_nm_ff: str = mlirlite.get_struct_name(var_ty.Pointee) + if vt_nm_ff is None: return 0 + vt_pf_ff: int = GenApi.sha1_prefix_len(vt_nm_ff) + vt_short_ff: str = vt_nm_ff + vt_pf_ff if vt_pf_ff != 0 else vt_nm_ff + if string.strchr(vt_short_ff, '[') is not None: + return 0 # With type args: not degraded + self.Gen.TypeGenErr.fatal_error(aa, f"the generic annotation '{base_nm_ff.id}[...]' of variable '{var_name}' resolved to a bare base class " f"'{vt_nm_ff}*' (type arguments lost); subsequent .get()/.set() will specialize by the wrong element " f"type (such as list[int].get returning i32) -> inttoptr truncates the pointer and crashes. " f"Silent continuation is forbidden -- check the degradation point of this annotation in resolve_annotation_type.", "TypeError") + return 0 + + def translate_annassign(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Translate an AnnAssign statement; return the new variable count.""" + aa: ast.AnnAssign* = ast.AnnAssign*(node) + if aa is None: return 0 + + target: ast.AST* = aa.target + if target is None: return 0 + # 属性/下标目标(`obj.f: T = v` / `arr[i]: T = v`):走独立路径——不建局部变量,而是按注解 + # 类型对既有存储做一次"带 P3 长命左值检查"的写出。原先这里直接 return 0,整条语句被**静默丢弃**。 + if target.kind() != ast.ASTKind.Name: return self._translate_lvalue_annassign(trans, aa, target) + + nm: ast.Name* = ast.Name*(target) + if nm.id is None: return 0 + + # CDefine annotation: compile-time constant, no runtime code; register it (idempotent) + if self.is_cdefine_annotation(aa.annotation) != 0: + pool_cd: memhub.MemBuddy* = trans.Pool + self._register_cdefine_from_value(pool_cd, nm.id, aa.value, aa.annotation) + return 0 + + # CTypedef annotation: register the alias on demand; no storage (avoids duplicate symbols) + if self.is_ctypedef_annotation(aa.annotation) != 0: + pool_ct: memhub.MemBuddy* = trans.Pool + ctd_str: str = self.Gen.TypeGenResolve._get_type_name_from_annotation(pool_ct, aa.value) + if ctd_str is not None: self.Gen.TypeGenCType.register_ctypedef(pool_ct, nm.id, ctd_str) + return 0 + + # Use-site width check for define/CDefine constants (a: T = C): T must be >= W_min + self.require_cdefine_use_width(trans.Pool, trans, aa.annotation, aa.value) + + # REPL session variable: annotated declaration at fragment entry goes to the session slot + if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: + rp_e: GenTypes.ReplVarEntry* = self.Gen.VarGen.repl_ensure_entry(trans, nm.id, aa) + if rp_e is not None: + self.Gen.VarGen.repl_try_annassign(trans, nm.id, aa.annotation, aa.value, aa) + return 0 + + # Duplicate-declaration check (fail fast): re-annotating a module-level name inside a function. + # Legal alternatives: a global declaration plus unannotated assignment, or a differently named local. + has_mod_decl_add: int = 0 + if nm.id is not None and trans is not None and self.Gen.VarGen.is_module_scope(trans.SymTab) == 0: + if self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) is not None: has_mod_decl_add = 1 + # 双保险:本文件顶层变量名集合(SymTab.Root.Vars 可能在部分时序看不到) + if has_mod_decl_add == 0 and GenApi.is_file_var(nm.id) != 0: has_mod_decl_add = 1 + if has_mod_decl_add != 0: + dup_decl: int = 0 + if GenApi.is_global_name(trans, nm.id) != 0 or GenApi.is_nonlocal_name(trans, nm.id) != 0: dup_decl = 1 + if dup_decl != 0: + self.Gen.TypeGenErr.fatal_error(aa, f"duplicate declaration: '{nm.id}' is already a module-level variable; after declaring global/nonlocal here " f"it is re-declared with a type annotation. Remove the type annotation on this line; a plain assignment modifies the global", "TypeError") + else: + self.Gen.TypeGenErr.fatal_error(aa, f"duplicate declaration: '{nm.id}' is already a module-level variable; without a global declaration in a function, a type annotation must not re-declare " f"the same name (it would shadow the global with a local). For an independent local, use another name; " f"to modify the global, add a global declaration in the function and remove this annotation", "TypeError") + + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + mod: mlirlite.LLVMModule* = trans.Module + + # Determine the type (inferred from the annotation) + var_ty: mlirlite.LLVMType* = None + annot: ast.AST* = aa.annotation + if annot is not None: + # Value-reference markers are parameter-only: a local variable `x: &T` fails fast. + self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "a local variable declaration") + # Same as PreScan: specialize generics first so list[X] does not degrade to bare list* + self._prespecialize_generic_annotation(trans, pool, annot) + resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0) + if resolved is not None: var_ty = resolved + else: + # fail-fast: annotation resolution failed (no silent i32 fallback) + self.Gen.TypeGenErr.fatal_error(aa, self._build_ann_fail_msg(pool, annot, nm.id), "TypeError") + else: + # fail-fast: AnnAssign must have an annotation + self.Gen.TypeGenErr.fatal_error(aa, f"AnnAssign is missing a type annotation, variable name: {nm.id}", "SyntaxError") + + # void cannot be a variable type (only void* and opaque-struct typedef) + if var_ty is not None and self.Gen.StmtGenDecl._is_void_type(var_ty) != 0: + self.Gen.TypeGenErr.fatal_error(aa, f"cannot declare a variable of void type '{nm.id}' (void may only be used for void* and typedef aliases)", "TypeError") + + # t.Callable function pointer: register the return-type signature + callable_ret: mlirlite.LLVMType* = self.Gen.TypeGenErr.try_extract_callable_ret(pool, annot, trans._imported_modules, trans._from_imports, trans) + if callable_ret is not None: + # Nonzero return already reported inside register_func_ptr_sig; this branch is defensive + if self.Gen.CallGenLeafKit.register_func_ptr_sig(pool, nm.id, callable_ret, aa) != 0: return 0 + + # Value-position volatile call prefix: mark the result slot volatile + if aa.value is not None and aa.value.kind() == ast.ASTKind.Call and (ast.Call*(aa.value)).IsVolatile != 0 and nm.id is not None: + if self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) is not None: self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) + + # global variable: write to the module-scope global + if GenApi.is_global_name(trans, nm.id) != 0: + mod_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + # 栈地址逃逸:`global G; G: T = &local`,或 `G: T* = arr`(数组退让)——与普通 + # 赋值同口径,禁止把本帧栈地址写出;目标为同型聚合时按值拷贝,不在此列。 + if aa.value is not None: + tgt_aa: mlirlite.LLVMType* = None + if mod_alloca is not None and mod_alloca.Ty is not None: tgt_aa = mod_alloca.Ty.Pointee + GenApi.fail_if_stack_addr_escape(aa.value, "a module-level global", trans, tgt_aa) + if mod_alloca is not None and aa.value is not None: + # P1-A context width propagation: pass the target type so integer literals materialize at target width + aa_glob_ty: mlirlite.LLVMType* = None + if mod_alloca.Ty is not None: aa_glob_ty = mod_alloca.Ty.Pointee + # Set CurAssignAnnot/CurAssignNode before translating the RHS (kept in sync across all four branches) + trans.CurAssignAnnot = aa.annotation + trans.CurAssignNode = aa.value + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, aa_glob_ty, "module-level global") + if rhs_val is not None: + # Object global with static storage: bind storage and copy the object + stg_ref_aa: mlirlite.Value* = None + if nm.id is not None: + stg_key_aa: str = pool.alloc(sizet(string.strlen(nm.id)) + 8) + if stg_key_aa is not None: + viperlib.snprintf(stg_key_aa, sizet(string.strlen(nm.id)) + 8, "%s.__stg", nm.id) + stg_ref_aa = self.Gen.VarGen.lookup_module_var(trans.SymTab, stg_key_aa) + if self._is_addrlike_call(aa.value) == 0 and stg_ref_aa is not None and rhs_val.Ty is not None and stg_ref_aa.Ty is not None and stg_ref_aa.Ty.Pointee != 0: + stg_ty_aa: mlirlite.LLVMType* = stg_ref_aa.Ty.Pointee + rhs_inner_aa: mlirlite.LLVMType* = None + match rhs_val.Ty: + case mlirlite.LLVMType.Ptr(pointee_aa): + rhs_inner_aa = pointee_aa + case _: + pass + if rhs_inner_aa is not None and rhs_inner_aa.kind() == mlirlite.LLVMType.Struct and self._stg_ty_equal(pool, stg_ty_aa, rhs_inner_aa) != 0: + mlirlite.build_store(builder, stg_ref_aa, mod_alloca) + stg_obj_val_aa: mlirlite.Value* = mlirlite.build_load(builder, stg_ty_aa, rhs_val) + if stg_obj_val_aa is not None: mlirlite.build_store(builder, stg_obj_val_aa, stg_ref_aa) + # Clear the assignment context before returning (avoid leaking into later statements) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + return 0 + target_ty: mlirlite.LLVMType* = None + if mod_alloca.Ty is not None: target_ty = mod_alloca.Ty.Pointee + if target_ty is not None: + # str/bytes is not a generic pointer: bare str/bytes annotation with an aggregate-pointer RHS + self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val, aa.annotation, node) + rhs_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, node, trans) + # t.CVolatile global: the initializing store carries the volatile qualifier + vol_gaa: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) + mlirlite.build_store(builder, rhs_val, mod_alloca, vol_gaa) + # Clear the assignment context (all four branches must stay in sync) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + return 0 + + # nonlocal variable: write through the closure env + if GenApi.is_nonlocal_name(trans, nm.id) != 0: + if aa.value is not None: + # P1-A: fetch the nonlocal slot pointer first so its type can be the target type + nl_ptr: mlirlite.Value* = self.Gen.StmtGenDecl.get_nonlocal_var_ptr(trans, nm.id) + # Infer the nonlocal type from nl_ptr instead of forcing i32 + nl_target_ty: mlirlite.LLVMType* = None + if nl_ptr is not None and nl_ptr.Ty is not None: nl_target_ty = nl_ptr.Ty.Pointee + # Set CurAssignAnnot/CurAssignNode before translating the RHS (sync across all four branches) + trans.CurAssignAnnot = aa.annotation + trans.CurAssignNode = aa.value + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, nl_target_ty, "closure variable (nonlocal)") + if rhs_val is not None: + if nl_ptr is not None: + if nl_target_ty is None: + # fail-fast: nl_ptr type lost, cannot store safely + VLogger.error(f"nonlocal variable type information lost, cannot write: {nm.id}", "HandlesAnnAssign") + # Clear the assignment context before returning (avoid leaking into later statements) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + return 0 + self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val, aa.annotation, node) + rhs_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, nl_target_ty, node) + mlirlite.build_store(builder, rhs_coerced, nl_ptr) + # Clear the assignment context (all four branches must stay in sync) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + return 0 + + # Module-level global: only module code modifies it; in a function without global, create a local + if self.Gen.VarGen.is_module_scope(trans.SymTab) != 0: mod_glob: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + else: mod_glob = None + if mod_glob is not None: + if aa.value is not None: + # List literal: initialize the array with per-element GEP + store + if aa.value.kind() == ast.ASTKind.List: self._init_global_array_from_list(builder, pool, mod, mod_glob, aa.value, trans) + else: + # P1-A: use the module-level global type as the target type + mg_val_ty: mlirlite.LLVMType* = None + if mod_glob.Ty is not None: mg_val_ty = mod_glob.Ty.Pointee + # Set CurAssignAnnot/CurAssignNode before translating the RHS (sync across all four branches) + trans.CurAssignAnnot = aa.annotation + trans.CurAssignNode = aa.value + rhs_val_mg: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, mg_val_ty, "module-level global") + if rhs_val_mg is not None: + # Object global with static storage: bind storage and copy the object + stg_ref_mg: mlirlite.Value* = None + if nm.id is not None: + stg_key_mg: str = pool.alloc(sizet(string.strlen(nm.id)) + 8) + if stg_key_mg is not None: + viperlib.snprintf(stg_key_mg, sizet(string.strlen(nm.id)) + 8, "%s.__stg", nm.id) + stg_ref_mg = self.Gen.VarGen.lookup_module_var(trans.SymTab, stg_key_mg) + if self._is_addrlike_call(aa.value) == 0 and stg_ref_mg is not None and rhs_val_mg.Ty is not None and stg_ref_mg.Ty is not None and stg_ref_mg.Ty.Pointee != 0: + stg_ty_mg: mlirlite.LLVMType* = stg_ref_mg.Ty.Pointee + rhs_inner_mg: mlirlite.LLVMType* = None + match rhs_val_mg.Ty: + case mlirlite.LLVMType.Ptr(pointee_mg): + rhs_inner_mg = pointee_mg + case _: + pass + if rhs_inner_mg is not None and rhs_inner_mg.kind() == mlirlite.LLVMType.Struct and self._stg_ty_equal(pool, stg_ty_mg, rhs_inner_mg) != 0: + mlirlite.build_store(builder, stg_ref_mg, mod_glob) + stg_obj_val_mg: mlirlite.Value* = mlirlite.build_load(builder, stg_ty_mg, rhs_val_mg) + if stg_obj_val_mg is not None: mlirlite.build_store(builder, stg_obj_val_mg, stg_ref_mg) + # Clear the assignment context before returning (avoid leaking into later statements) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + return 0 + target_ty_mg: mlirlite.LLVMType* = None + if mod_glob.Ty is not None: target_ty_mg = mod_glob.Ty.Pointee + if target_ty_mg is not None: + # str/bytes is not a generic pointer: bare str/bytes annotation with an aggregate-pointer RHS + self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val_mg, aa.annotation, node) + rhs_val_mg = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val_mg, target_ty_mg, node) + # t.CVolatile module-level global: the initializing store carries volatile + vol_mga: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) + mlirlite.build_store(builder, rhs_val_mg, mod_glob, vol_mga) + # Clear the assignment context (all four branches must stay in sync) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + return 0 + + # Single-param CArray + list literal -> static array; match PreScan so the alloca is reused + var_ty = self._infer_static_array_ty_from_literal(pool, annot, aa.value, var_ty) + # [D31 fail-fast] generic annotation degraded to a bare base class + self._fail_on_degenerated_generic_annotation(aa, nm.id, annot, var_ty) + + # Ordinary local variable: create the alloca + alloca: mlirlite.Value* = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, nm.id, var_ty) + if alloca is None: + # fail-fast: storage creation failed + self.Gen.TypeGenErr.fatal_error(aa, f"cannot create storage for variable {nm.id}", "TypeError") + # A reused alloca type must match the resolved annotation type (compare struct names) + if alloca is not None and alloca.Ty is not None and var_ty is not None: + av_pt_d: mlirlite.LLVMType* = alloca.Ty.Pointee + if av_pt_d is not None and self.Gen.ExprGenBase.is_ptr_type(av_pt_d) != 0 and self.Gen.ExprGenBase.is_ptr_type(var_ty) != 0: + av_in: mlirlite.LLVMType* = av_pt_d.Pointee + vt_in: mlirlite.LLVMType* = var_ty.Pointee + if av_in is not None and vt_in is not None: + av_nm_d: str = mlirlite.get_struct_name(av_in) + vt_nm_d: str = mlirlite.get_struct_name(vt_in) + if av_nm_d is not None and vt_nm_d is not None and string.strcmp(av_nm_d, vt_nm_d) != 0: + # Fail fast only when the reused pointee is a bare generic base (lost type args) + av_pf: int = GenApi.sha1_prefix_len(av_nm_d) + av_short: str = av_nm_d + av_pf if av_pf != 0 else av_nm_d + if string.strchr(av_short, '[') is None: + av_tpl: ast.ClassDef* = self.Gen.ClassGenGeneric._find_generic_template(av_short) + if av_tpl is not None: + self.Gen.TypeGenErr.fatal_error(aa, f"the reused storage type of variable '{nm.id}' is the bare generic base class '{av_nm_d}*'" f" (type arguments lost), inconsistent with the annotation-resolved type '{vt_nm_d}*'; " f"silent mixing is forbidden -- method calls would specialize by the wrong element type and truncate the pointer", "TypeError") + + # FakeDuck: a str variable gets a shadow struct {var}__meta__ for per-instance fields + fd_str_ty: mlirlite.LLVMType* = None + fd_meta_alloca: mlirlite.Value* = None + fd_meta_name: str = None + if annot is not None and annot.kind() == ast.ASTKind.Name: + fd_annot: ast.Name* = ast.Name*(annot) + if fd_annot is not None and fd_annot.id is not None: + if string.strcmp(fd_annot.id, "str") == 0: fd_str_ty = self.Gen.StructGen.get_struct_type("_str") + if fd_str_ty is not None: + # Ensure the full _str definition is available (opaque types cannot be alloca'd) + self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, fd_str_ty) + # Shadow variable name must use a pool buffer (f-string stack buffers get overwritten) + fd_meta_name = pool.alloc(256) + if fd_meta_name is not None: viperlib.snprintf(fd_meta_name, 256, "%s__meta__", nm.id) + fd_meta_alloca = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, fd_meta_name, fd_str_ty) + if fd_meta_alloca is None: + # fail-fast: shadow storage creation failed + self.Gen.TypeGenErr.fatal_error(aa, f"cannot create storage for the str shadow variable {fd_meta_name}", "TypeError") + # Store the annotation's class name; pass pool so generic Subscript yields the mangled name + if annot is not None: + cls_nm_hd: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(annot, trans._imported_modules, pool, trans._from_imports) + if cls_nm_hd is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, nm.id, cls_nm_hd) + # Check for bytes* or str* (i8** semantics, 8-byte stride) + if self._is_ptr_element_annotation(annot) != 0: self.Gen.VarGen.set_var_ptr_element(trans.SymTab, nm.id) + + new_vars: int = 0 + existing: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) + if existing is None: new_vars = 1 + + # If there is an initial value, store it + if aa.value is not None: + # Coroutine: record the variable name assigned from an async call (for frame-type lookup) + if aa.value.kind() == ast.ASTKind.Call: + val_cl: ast.Call* = ast.Call*(aa.value) + if val_cl.func is not None and val_cl.func.kind() == ast.ASTKind.Name: + val_nm: ast.Name* = ast.Name*(val_cl.func) + if val_nm.id is not None: + if self.Gen.AsyncGen.is_async_func(trans, val_nm.id) != 0: self.Gen.AsyncGen.register_coro_var(trans, nm.id, val_nm.id) + # c.Addr(async_call): interpret the tt: Coroutine = c.Addr(task()) form in place + elif val_cl.func is not None and val_cl.func.kind() == ast.ASTKind.Attribute: + val_at: ast.Attribute* = ast.Attribute*(val_cl.func) + if val_at.attr is not None and string.strcmp(val_at.attr, "Addr") == 0 and val_cl.args is not None and len(val_cl.args) >= 1: + icall: ast.AST* = val_cl.args.get(0) + if icall is not None and icall.kind() == ast.ASTKind.Call: + icl2: ast.Call* = ast.Call*(icall) + if icl2.func is not None and icl2.func.kind() == ast.ASTKind.Name: + inm2: ast.Name* = ast.Name*(icl2.func) + if inm2.id is not None: + if self.Gen.AsyncGen.is_async_func(trans, inm2.id) != 0: self.Gen.AsyncGen.register_coro_var(trans, nm.id, inm2.id) + # Local array from a list literal: zero it first, then per-element GEP + store (no list[T]) + local_arr_ty: mlirlite.LLVMType* = None + if aa.value.kind() == ast.ASTKind.List and alloca.Ty is not None: + pt_l: mlirlite.LLVMType* = alloca.Ty.Pointee + if self.Gen.ExprGenBase.is_array_type(pt_l) != 0: local_arr_ty = pt_l + if local_arr_ty is not None: + zero_arr: mlirlite.Value* = mlirlite.ConstZero(pool, local_arr_ty) + if zero_arr is not None: mlirlite.build_store(builder, zero_arr, alloca) + self._init_global_array_from_list(builder, pool, mod, alloca, aa.value, trans) + else: + # Set CurAssignAnnot/CurAssignNode for bare generic construction (sync across all four branches) + trans.CurAssignAnnot = aa.annotation + trans.CurAssignNode = aa.value + # P1-A: use the local alloca pointee as the target type so integer literals materialize at target width + lv_target_ty: mlirlite.LLVMType* = None + if alloca.Ty is not None: lv_target_ty = alloca.Ty.Pointee + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, lv_target_ty, "local variable annotation") + if rhs_val is not None: + # Fix degraded annotation: rebuild the alloca with the precise RHS struct type + precise_rhs_ty: mlirlite.LLVMType* = None + if rhs_val.Ty is not None: + match rhs_val.Ty: + case mlirlite.LLVMType.Ptr(inner_struct): + match inner_struct: + case mlirlite.LLVMType.Struct(_, _, _): + precise_rhs_ty = rhs_val.Ty + case _: + pass + case _: + pass + if precise_rhs_ty is not None and alloca.Ty is not None: + alloca_pt: mlirlite.LLVMType* = alloca.Ty.Pointee + # Forbid downgrade rebuild: do not rebuild when the RHS pointee is a bare generic base + ann_gen_base: str = None + if annot is not None: + ag_side: ast.AST* = annot + if ag_side.kind() == ast.ASTKind.BinOp: + ag_bop: ast.BinOp* = ast.BinOp*(ag_side) + if ag_bop.op == ast.OpKind.BitOr: + if ag_bop.left is not None and ag_bop.left.kind() == ast.ASTKind.Subscript: ag_side = ag_bop.left + elif ag_bop.right is not None and ag_bop.right.kind() == ast.ASTKind.Subscript: ag_side = ag_bop.right + if ag_side.kind() == ast.ASTKind.Subscript: + ag_sub: ast.Subscript* = ast.Subscript*(ag_side) + if ag_sub.value is not None and ag_sub.value.kind() == ast.ASTKind.Name: + ag_nm: ast.Name* = ast.Name*(ag_sub.value) + if ag_nm.id is not None: ann_gen_base = ag_nm.id + rb_is_bare_generic: int = 0 + if alloca_pt is not None and uint64(alloca_pt) != uint64(precise_rhs_ty): + rhs_pt_rb: mlirlite.LLVMType* = precise_rhs_ty.Pointee + if rhs_pt_rb is not None: + rb_nm: str = mlirlite.get_struct_name(rhs_pt_rb) + if rb_nm is not None: + rb_pf: int = GenApi.sha1_prefix_len(rb_nm) + rb_short: str = rb_nm + rb_pf if rb_pf != 0 else rb_nm + if string.strchr(rb_short, '[') is None: + # (1) The bare name hits the generic template registry (list/GSList/...) + if self.Gen.ClassGenGeneric._find_generic_template(rb_short) is not None: rb_is_bare_generic = 1 + # (2) Same base name as the annotation => rebuild would downgrade; forbid it + elif ann_gen_base is not None and string.strcmp(rb_short, ann_gen_base) == 0: rb_is_bare_generic = 1 + # Forbid narrowing an upcast: annotated base pointer with an RHS derived pointer must not be narrowed + rb_is_upcast: int = 0 + if alloca_pt is not None and uint64(alloca_pt) != uint64(precise_rhs_ty) and self.Gen.ExprGenBase.is_ptr_type(alloca_pt) != 0: + ann_pt_rb: mlirlite.LLVMType* = alloca_pt.Pointee + rhs_pt_up: mlirlite.LLVMType* = precise_rhs_ty.Pointee + if ann_pt_rb is not None and rhs_pt_up is not None and uint64(ann_pt_rb) != uint64(rhs_pt_up): + if self.Gen.ExprGenBase._is_inheritance_upcast(pool, rhs_pt_up, ann_pt_rb) != 0: rb_is_upcast = 1 + if alloca_pt is not None and uint64(alloca_pt) != uint64(precise_rhs_ty) and rb_is_bare_generic == 0 and rb_is_upcast == 0: + new_alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, precise_rhs_ty) + if new_alloca is not None: + v_entry_rb: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) + if v_entry_rb is not None: + v_entry_rb.Alloca = new_alloca + alloca = new_alloca + # Type coercion: convert rhs_val to the alloca pointee (integer literals may need trunc/sext) + target_ty: mlirlite.LLVMType* = None + if alloca.Ty is not None: target_ty = alloca.Ty.Pointee + if target_ty is not None: + # str/bytes is not a generic pointer: bare str/bytes annotation with an aggregate-pointer RHS + self.Gen.ExprGenBase.fail_if_str_annot_ast_agg_ptr(rhs_val, aa.annotation, node) + rhs_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, node, trans) + # t.CVolatile local variable: mark the VarEntry and store with volatile + vol_laa: int = 0 + if aa.annotation is not None and nm.id is not None: + # const object local variable: mark VarEntry.IsConst (idempotent with prescan) + if self.Gen.TypeGenErr.is_const_object_marker(aa.annotation, target_ty) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, nm.id) + if self.Gen.TypeGenErr.has_decorator_marker(aa.annotation, "CVolatile", 0) != 0: + self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) + vol_laa = 1 + # rawptr[t.CVolatile | T]: mark PointeeVolatile (this store writes the pointer itself) + if self.Gen.TypeGenErr.has_pointee_volatile_marker(aa.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, nm.id) + # Value-position volatile call prefix: the result slot is accessed with volatile + if aa.value is not None and aa.value.kind() == ast.ASTKind.Call and (ast.Call*(aa.value)).IsVolatile != 0: + self.Gen.VarGen.set_var_volatile(trans.SymTab, nm.id) + vol_laa = 1 + # M3 所有权记账:必须**先**记账再写值——覆盖释放要读到槽里的旧值 + # (`p: T* = new heap slife …` → 释放旧对象 + 影子置 1;`q: T* = p` → 交还所有权) + self._slife_note_assign(pool, builder, mod, trans, alloca, aa.value, nm.id) + mlirlite.build_store(builder, rhs_val, alloca, vol_laa) + # P2 willFree:带注解声明/赋值后清除失效态(新变量本就为 0;复用槽兜底) + self.Gen.VarGen.clear_var_freed(trans.SymTab, nm.id) + # FakeDuck: initialize the shadow struct __data__ field with the initial string pointer + if fd_str_ty is not None and fd_meta_alloca is not None: + fd_data_fe: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(fd_str_ty, "__data__") + if fd_data_fe is not None: + fd_data_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, fd_str_ty, fd_data_fe.Ty, fd_meta_alloca, fd_data_fe.Index) + if fd_data_ptr is not None: mlirlite.build_store(builder, rhs_val, fd_data_ptr) + # Clear the assignment context after the RHS translation (sync across all four branches) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + + # FakeDuck: initialize the __mbuddy__ field to NULL + if fd_str_ty is not None and fd_meta_alloca is not None: + fd_buddy_fe: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(fd_str_ty, "__mbuddy__") + if fd_buddy_fe is not None and fd_buddy_fe.Ty is not None: + fd_null: mlirlite.Value* = mlirlite.ConstNull(pool, fd_buddy_fe.Ty, "null") + fd_buddy_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, fd_str_ty, fd_buddy_fe.Ty, fd_meta_alloca, fd_buddy_fe.Index) + if fd_buddy_ptr is not None and fd_null is not None: mlirlite.build_store(builder, fd_null, fd_buddy_ptr) + # FakeDuck: auto-inject requires fields (e.g. __mbuddy__) for the with context + self.Gen.CallGenFd._inject_requires_fields(trans, pool, builder, mod, fd_meta_alloca, "_str") + + # 生命期传播(本地变量):`s: str = l` → Life(s) = Life(l)(数组退让即借用) + if aa.value is not None and nm.id is not None and alloca is not None: + tgt_life_ty: mlirlite.LLVMType* = None + if alloca.Ty is not None: tgt_life_ty = alloca.Ty.Pointee + self.propagate_life(trans, target, aa.value, tgt_life_ty) + + return 0 + + # _translate_lvalue_annassign - 注解赋值的**属性/下标目标**:`obj.f: T = v` / `arr[i]: T = v`。 + # 与 Name 目标同口径:注解是源码级权威类型(RHS 以它为靶型求值);并且必须过 P3 + # "长命左值不得接收本帧栈地址"的检查(= propagate_life 的非 Name 分支)。 + # 存储目标复用既有左值指针求解:Attribute → get_attribute_ptr,Subscript → get_subscript_ptr。 + def _translate_lvalue_annassign(self, trans: HT.Translator*, aa: ast.AnnAssign*, target: ast.AST*) -> int: + """注解赋值的属性/下标目标;0=完成""" + if trans is None or aa is None or target is None: return 0 + tk: int = target.kind() + if tk != ast.ASTKind.Attribute and tk != ast.ASTKind.Subscript: + self.Gen.TypeGenErr.fatal_error(target, "the target of an annotated assignment must be a variable, an attribute or a subscript", "SyntaxError") + return 0 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + mod: mlirlite.LLVMModule* = trans.Module + annot: ast.AST* = aa.annotation + if annot is None: + self.Gen.TypeGenErr.fatal_error(aa, "AnnAssign is missing a type annotation", "SyntaxError") + return 0 + # 与 Name 路径同口径解析注解:先特化泛型(避免 list[X] 退化成裸 list*),解析失败即 fail-fast。 + self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "an annotated assignment to an attribute/subscript") + self._prespecialize_generic_annotation(trans, pool, annot) + ann_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0) + if ann_ty is None: + self.Gen.TypeGenErr.fatal_error(aa, "cannot resolve the annotation type of an annotated assignment to an attribute/subscript", "TypeError") + return 0 + if self.Gen.StmtGenDecl._is_void_type(ann_ty) != 0: + self.Gen.TypeGenErr.fatal_error(aa, "cannot assign to an attribute/subscript of void type (void may only be used for void* and typedef aliases)", "TypeError") + return 0 + # 左值存储指针:属性/下标各走既有求解器。失败一律 fail-fast——绝不静默丢掉整条语句。 + is_attr: int = 0 + ptr: mlirlite.Value* = None + if tk == ast.ASTKind.Attribute: + is_attr = 1 + at_lv: ast.Attribute* = ast.Attribute*(target) + if at_lv.attr is not None and self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, target) != 0: + self.const_assign_fail(target, "field assignment", at_lv.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, target)) + ptr = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, target, trans) + else: + ptr = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, target, trans) + if ptr is None or ptr.Ty is None or ptr.Ty.Pointee == 0: + self.Gen.TypeGenErr.fatal_error(target, "cannot resolve the storage of the annotated assignment target (the attribute/subscript pointer or its element type is missing)", "TypeError") + return 0 + target_ty: mlirlite.LLVMType* = ptr.Ty.Pointee + # P3:左值存储比右值长命时,右值不得交出本帧栈地址(如 `h.p: T* = &local`)。 + self._fail_if_lvalue_outlives_rhs(trans, target, aa.value, target_ty) + # 与普通 Assign 同口径:f-string 结果是函数内临时量,写出即悬垂。 + if aa.value is not None: + if is_attr != 0: GenApi.fail_if_fstring_escape(aa.value, "field assignment") + else: GenApi.fail_if_fstring_escape(aa.value, "element assignment") + # RHS 以注解靶型求值(CurAssignAnnot/CurAssignNode 供裸泛型构造推断;求值完即复位,不跨语句泄漏)。 + trans.CurAssignAnnot = annot + trans.CurAssignNode = aa.value + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, aa.value, None, 0, trans, ann_ty, "annotated assignment to an attribute/subscript") + trans.CurAssignAnnot = None + trans.CurAssignNode = None + if rhs_val is None: + self.Gen.TypeGenErr.fatal_error(aa, "the right-hand side of the annotated assignment to an attribute/subscript could not be translated", "TypeError") + return 0 + store_val: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, aa, trans) + if store_val is None: + self.Gen.TypeGenErr.fatal_error(aa, "type coercion failed for the annotated assignment to an attribute/subscript", "TypeError") + return 0 + # t.CVolatile 字段 / rawptr[t.CVolatile|T] 基址:写出同样带 volatile。 + vol_lv: int = 0 + if is_attr != 0: + vol_lv = self.Gen.ExprGenAttr.attr_field_is_volatile(pool, trans, target) + else: + sub_lv: ast.Subscript* = ast.Subscript*(target) + if sub_lv.value is not None: vol_lv = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, sub_lv.value) + mlirlite.build_store(builder, store_val, ptr, vol_lv) + return 0 + + # translate_assign - handle an Assign statement; returns the number of new variables + def translate_assign(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Translate an Assign statement; return the number of new variables.""" + + asgn: ast.Assign* = ast.Assign*(node) + if asgn is None: + VLogger.error("cast failed", "ASGN") + return 0 + + targets: list[ast.AST*]* = asgn.targets + if targets is None: + VLogger.error("targets is None", "ASGN") + return 0 + + # Read shared state from trans (replaces 11 parameters) + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + mod: mlirlite.LLVMModule* = trans.Module + + # const target: fail fast before assignment (Name/Attribute/Subscript) + for ti_c in range(int(len(targets))): + tgt_c: ast.AST* = targets.get(ti_c) + if tgt_c is None: continue + if tgt_c.kind() == ast.ASTKind.Name: + nm_c: ast.Name* = ast.Name*(tgt_c) + if nm_c.id is None: continue + ve_c: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_c.id) + if self.Gen.VarGen.var_is_const(ve_c) != 0: + self.const_assign_fail(tgt_c, "assignment", nm_c.id, self.Gen.VarGen.var_annot_class_name(ve_c)) + elif tgt_c.kind() == ast.ASTKind.Attribute: + at_c: ast.Attribute* = ast.Attribute*(tgt_c) + if at_c.attr is None: continue + if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, tgt_c) != 0: + self.const_assign_fail(tgt_c, "field assignment", at_c.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, tgt_c)) + elif tgt_c.kind() == ast.ASTKind.Subscript: + sub_c: ast.Subscript* = ast.Subscript*(tgt_c) + if sub_c.value is None: continue + if sub_c.value.kind() == ast.ASTKind.Name: + nm_c2: ast.Name* = ast.Name*(sub_c.value) + if nm_c2.id is None: continue + ve_c2: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_c2.id) + if self.Gen.VarGen.var_is_const(ve_c2) != 0: + self.const_assign_fail(tgt_c, "element assignment", nm_c2.id, self.Gen.VarGen.var_annot_class_name(ve_c2)) + elif sub_c.value.kind() == ast.ASTKind.Attribute: + at_c2: ast.Attribute* = ast.Attribute*(sub_c.value) + if at_c2.attr is None: continue + if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, sub_c.value) != 0: + self.const_assign_fail(tgt_c, "element assignment", at_c2.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, sub_c.value)) + + # f-string escape: results are function-local temporaries; writing them out is dangling + for ti_f in range(int(len(targets))): + tgt_f: ast.AST* = targets.get(ti_f) + if tgt_f is None: continue + tk_f: int = tgt_f.kind() + if tk_f == ast.ASTKind.Attribute: GenApi.fail_if_fstring_escape(asgn.value, "field assignment") + elif tk_f == ast.ASTKind.Subscript: GenApi.fail_if_fstring_escape(asgn.value, "element assignment") + elif tk_f == ast.ASTKind.Name: + nm_f: ast.Name* = ast.Name*(tgt_f) + if nm_f.id is not None and GenApi.is_global_name(trans, nm_f.id) != 0: + GenApi.fail_if_fstring_escape(asgn.value, "global variable assignment") + # 栈地址逃逸:`G = &local` 与 `G = arr`(G 为指针时的数组退让)都把本帧栈地址 + # 写进模块级全局,函数返回后悬垂;`G = arr` 且 G 为同型聚合时是值拷贝,不在此列。 + gv_sa: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm_f.id) + tgt_sa: mlirlite.LLVMType* = None + if gv_sa is not None and gv_sa.Ty is not None: tgt_sa = gv_sa.Ty.Pointee + GenApi.fail_if_stack_addr_escape(asgn.value, "a module-level global", trans, tgt_sa) + + # Translate the RHS; clear CurAssignAnnot/CurAssignNode (no target type to infer here) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + # P1-A: for a single Name target with a known storage type, pass it as the target type + asg_target_ty: mlirlite.LLVMType* = None + if len(targets) == 1: + asg_t0: ast.AST* = targets.get(0) + if asg_t0 is not None and asg_t0.kind() == ast.ASTKind.Name: + asg_nm0: ast.Name* = ast.Name*(asg_t0) + if asg_nm0.id is not None: + asg_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, asg_nm0.id) + if asg_slot is None: asg_slot = self.Gen.VarGen.lookup_module_var(trans.SymTab, asg_nm0.id) + if asg_slot is not None and asg_slot.Ty is not None: asg_target_ty = asg_slot.Ty.Pointee + elif trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: + # REPL: a typed session variable also uses its slot type as the target type + asg_re: GenTypes.ReplVarEntry* = self.Gen.VarGen.repl_find_entry(trans.ReplCtx, asg_nm0.id) + if asg_re is not None and asg_re.Kind != GenTypes.REPL_KIND_UNTYPED: asg_target_ty = self.Gen.VarGen.repl_slot_ty(pool, asg_re) + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, asgn.value, None, 0, trans, asg_target_ty) + if rhs_val is None: + # RHS is None usually means allocation failure; dump both pool statistics + mb_diag: memhub.MemBuddy* = CompilerFactory.factory.Pool + pp_diag: memhub.MemBuddy* = CompilerFactory.factory.PermPool + if mb_diag is not None: mb_diag.dump_stats("Main Pool(Pool)") + if pp_diag is not None: pp_diag.dump_stats("Permanent Pool(PermPool)") + stdio.fflush(None) + # Enhanced error message: file name + lineno + AST node details + if asgn.value is not None: + sha1: str = trans.ModuleSha1 + val_kind: int = asgn.value.kind() + val_line: int = asgn.value.lineno + # Look up the file name from sha1 (human readable) + rel_path: str = None + if sha1 is not None: rel_path = StubMergerStore.LookupSha1RelPath(sha1) + # Extract details by AST node kind + if val_kind == ast.ASTKind.Attribute: + at_nv: ast.Attribute* = ast.Attribute*(asgn.value) + attr_nm: str = at_nv.attr + target_nm: str = "?" + if at_nv.value is not None: + if at_nv.value.kind() == ast.ASTKind.Name: + tn_nm: ast.Name* = ast.Name*(at_nv.value) + target_nm = tn_nm.id + if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} attribute access of '{target_nm}.{attr_nm}' failed", "ASGN") + else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} attribute access of '{target_nm}.{attr_nm}' failed", "ASGN") + elif val_kind == ast.ASTKind.Name: + nm_nv: ast.Name* = ast.Name*(asgn.value) + if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} variable '{nm_nv.id}' not found", "ASGN") + else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} variable '{nm_nv.id}' not found", "ASGN") + elif val_kind == ast.ASTKind.Call: + cl_nv: ast.Call* = ast.Call*(asgn.value) + func_nm: str = "?" + if cl_nv.func is not None: + if cl_nv.func.kind() == ast.ASTKind.Name: + fn_nm: ast.Name* = ast.Name*(cl_nv.func) + func_nm = fn_nm.id + if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} call '{func_nm}(...)' returned None", "ASGN") + else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} call '{func_nm}(...)' returned None", "ASGN") + else: + # 必须与相邻分支一致显式传 category:跨模块模块级函数调用目前不会补默认值, + # 漏传会生成操作数不足的 llvm.call(MLIR 校验直接失败)。 + if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} [kind={val_kind}]", "ASGN") + else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} [kind={val_kind}]", "ASGN") + else: VLogger.error("assignment rhs is None (asgn.value is empty)", "ASGN") + return 0 + + # 生命期传播(单 Name 目标):`s = l` → Life(s) = Life(l) + if len(targets) == 1: + t0_sl: ast.AST* = targets.get(0) + self.propagate_life(trans, t0_sl, asgn.value, asg_target_ty) + + new_vars: int = 0 + tn: sizet = len(targets) + for ti in range(int(tn)): + target: ast.AST* = targets.get(ti) + if target is None: continue + + tk: int = target.kind() + + # Tuple/List assignment: unpack a struct return value into Name targets + if tk == ast.ASTKind.Tuple or tk == ast.ASTKind.List: + tgt_tup: ast.Tuple* = ast.Tuple*(target) + tgt_elts: list[ast.AST*]* = tgt_tup.elts + # Guard: skip this target when rhs is not a struct (tuple) + if tgt_elts is None or rhs_val is None or rhs_val.Ty is None: continue + match rhs_val.Ty: + case mlirlite.LLVMType.Struct(_, _, _): + tgt_n: sizet = len(tgt_elts) + tmp_s: mlirlite.Value* = mlirlite.build_alloca(builder, rhs_val.Ty) + if tmp_s is not None: + mlirlite.build_store(builder, rhs_val, tmp_s) + fchain: mlirlite.ParamNode* = mlirlite.get_struct_fields(rhs_val.Ty) + fcur: mlirlite.ParamNode* = fchain + fti: sizet + for fti in range(int(tgt_n)): + tgt_elt: ast.AST* = tgt_elts.get(fti) + if tgt_elt is None or fcur is None: break + fty: mlirlite.LLVMType* = mlirlite.paramnode_get_ty(fcur) + fcur = mlirlite.paramnode_get_next(fcur) + if fty is None: + # fail-fast: field type missing + self.Gen.TypeGenErr.fatal_error(target, "tuple-unpack field type missing", "TypeError") + # Guard: skip non-Name elements (cannot unpack) + if tgt_elt.kind() != ast.ASTKind.Name: continue + tgt_nm2: ast.Name* = ast.Name*(tgt_elt) + if tgt_nm2.id is None: continue + fp2: mlirlite.Value* = mlirlite.build_gep_struct(builder, rhs_val.Ty, fty, tmp_s, fti) + if fp2 is None: continue + fv2: mlirlite.Value* = mlirlite.build_load(builder, fty, fp2) + tgt_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, tgt_nm2.id) + if tgt_alloca is None: tgt_alloca = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, tgt_nm2.id, fty) + if tgt_alloca is None: continue + sval: mlirlite.Value* = fv2 + if tgt_alloca.Ty is not None and tgt_alloca.Ty.Pointee != 0: sval = self.Gen.ExprGenBase.coerce_to_type(builder, fv2, tgt_alloca.Ty.Pointee, node, trans) + mlirlite.build_store(builder, sval, tgt_alloca) + case _: + pass + continue + + # Subscript assignment: arr[i] = val / ptr[i] = val / list[i] = val + if tk == ast.ASTKind.Subscript: + # Check for a list[T] Subscript; list subscript assignment uses the inline __setitem__ path + list_obj: mlirlite.Value* = self.Gen.ExprGenName.is_list_subscript(target, trans) + if list_obj is not None: + # list[T]: generate the __setitem__ logic inline + sub_node: ast.Subscript* = ast.Subscript*(target) + if sub_node is not None and sub_node.slice is not None: + list_idx_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_node.slice, None, 0, trans) + if list_idx_val is None: + # fail-fast: list index translation failed + self.Gen.TypeGenErr.fatal_error(target, "list index translation failed", "TypeError") + self.Gen.ExprGenName.list_setitem_inline(builder, pool, list_obj, list_idx_val, rhs_val, target) + continue + # Ordinary Subscript assignment + elem_ptr: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, target, trans) + if elem_ptr is not None: + store_val: mlirlite.Value* = rhs_val + # Fail fast when a rawptr[T] element is an object but RHS is a T pointer (whole-object copy) + self._reject_whole_object_copy_into_ptr_slot(pool, trans, target, elem_ptr, rhs_val) + # Guard: coerce only when the element type is complete + if elem_ptr.Ty is not None and elem_ptr.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, elem_ptr.Ty.Pointee, node, trans) + # rawptr[t.CVolatile|T] base: p[i] = v stores with volatile when the base is resolvable + vol_sw: int = 0 + sub_wr: ast.Subscript* = ast.Subscript*(target) + if sub_wr is not None: vol_sw = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, sub_wr.value) + mlirlite.build_store(builder, store_val, elem_ptr, vol_sw) + continue + # get_subscript_ptr returned None: try the __setitem__ overload for custom classes + sub_asgn: ast.Subscript* = ast.Subscript*(target) + setitem_done: int = 0 + if sub_asgn is not None and sub_asgn.value is not None: + if sub_asgn.value.kind() == ast.ASTKind.Name: + sub_nm: ast.Name* = ast.Name*(sub_asgn.value) + if sub_nm.id is not None: + sub_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, sub_nm.id) + if sub_alloca is not None and sub_alloca.Ty is not None and sub_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(sub_alloca.Ty) != 0: + sub_pointee: mlirlite.LLVMType* = sub_alloca.Ty.Pointee + cls_nm_set: str = self.Gen.StructGen.get_class_name_by_type(pool, sub_pointee) + obj_val_set: mlirlite.Value* = mlirlite.build_load(builder, sub_pointee, sub_alloca) + # Pointer variable (X*): sub_pointee is Ptr(Struct); load it to inspect the inner struct + if cls_nm_set is None and obj_val_set is not None and self.Gen.ExprGenBase.is_ptr_type(sub_pointee) != 0 and sub_pointee.Pointee != 0: + cls_nm_set = self.Gen.StructGen.get_class_name_by_type(pool, sub_pointee.Pointee) + if cls_nm_set is not None and obj_val_set is not None: + key_val_set: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.slice, None, 0, trans) + if key_val_set is not None: + arg_vals_set: sizet* = pool.alloc(16) + if arg_vals_set is not None: + arg_vals_set[0] = sizet(key_val_set) + arg_vals_set[1] = sizet(rhs_val) + self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, cls_nm_set, "__setitem__", obj_val_set, arg_vals_set, 2, trans) + setitem_done = 1 + elif sub_asgn.value.kind() == ast.ASTKind.Attribute: + # self._ht[key] = val -> get the field pointer then call __setitem__ + field_ptr_set: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, sub_asgn.value, trans) + if field_ptr_set is not None and field_ptr_set.Ty is not None and field_ptr_set.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(field_ptr_set.Ty) != 0: + field_pointee_set: mlirlite.LLVMType* = field_ptr_set.Ty.Pointee + cls_nm_attr: str = self.Gen.StructGen.get_class_name_by_type(pool, field_pointee_set) + obj_val_attr: mlirlite.Value* = field_ptr_set + # field_ptr is Ptr(Ptr(Struct)) for an X* field: load to get Ptr(Struct) + if cls_nm_attr is None and self.Gen.ExprGenBase.is_ptr_type(field_pointee_set) != 0 and field_pointee_set.Pointee != 0: + cls_nm_attr = self.Gen.StructGen.get_class_name_by_type(pool, field_pointee_set.Pointee) + if cls_nm_attr is not None: obj_val_attr = mlirlite.build_load(builder, field_pointee_set, field_ptr_set) + if cls_nm_attr is not None and obj_val_attr is not None: + key_val_attr: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.slice, None, 0, trans) + if key_val_attr is not None: + arg_vals_attr: sizet* = pool.alloc(16) + if arg_vals_attr is not None: + arg_vals_attr[0] = sizet(key_val_attr) + arg_vals_attr[1] = sizet(rhs_val) + self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, cls_nm_attr, "__setitem__", obj_val_attr, arg_vals_attr, 2, trans) + setitem_done = 1 + if setitem_done == 0: + # Generic fallback: translate sub.value and try __setitem__ or a direct GEP+store + if sub_asgn is not None and sub_asgn.value is not None: + obj_val_gen: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.value, None, 0, trans) + key_val_gen: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_asgn.slice, None, 0, trans) + if obj_val_gen is not None and key_val_gen is not None and obj_val_gen.Ty is not None: + # Inline the _get_custom_struct_cls_nm logic (avoid a new dependency) + cls_nm_gen: str = self.Gen.StructGen.get_class_name_by_type(pool, obj_val_gen.Ty) + if cls_nm_gen is None and self.Gen.ExprGenBase.is_ptr_type(obj_val_gen.Ty) != 0 and obj_val_gen.Ty.Pointee != 0: + cls_nm_gen = self.Gen.StructGen.get_class_name_by_type(pool, obj_val_gen.Ty.Pointee) + if cls_nm_gen is not None: + # Custom struct: call __setitem__ + arg_vals_gen: sizet* = pool.alloc(16) + if arg_vals_gen is not None: + arg_vals_gen[0] = sizet(key_val_gen) + arg_vals_gen[1] = sizet(rhs_val) + self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, cls_nm_gen, "__setitem__", obj_val_gen, arg_vals_gen, 2, trans) + setitem_done = 1 + elif self.Gen.ExprGenBase.is_ptr_type(obj_val_gen.Ty) != 0 and obj_val_gen.Ty.Pointee != 0: + # Non-custom struct: direct GEP + store (handles the None from get_subscript_ptr) + elem_gen: mlirlite.LLVMType* = obj_val_gen.Ty.Pointee + store_val_gen: mlirlite.Value* = rhs_val + elem_ptr_gen: mlirlite.Value* = None + match elem_gen: + case mlirlite.LLVMType.Array(arr_elem_gen, _): + elem_ptr_gen = mlirlite.build_gep_array(builder, elem_gen, arr_elem_gen, obj_val_gen, key_val_gen) + case _: + elem_ptr_gen = mlirlite.build_gep(builder, elem_gen, obj_val_gen, key_val_gen) + if elem_ptr_gen is not None and elem_ptr_gen.Ty is not None: + if elem_ptr_gen.Ty.Pointee != 0: store_val_gen = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, elem_ptr_gen.Ty.Pointee, node) + mlirlite.build_store(builder, store_val_gen, elem_ptr_gen) + setitem_done = 1 + if setitem_done == 0: self.Gen.TypeGenErr.fatal_error(target, "subscript ptr is None") + continue + + # UnaryOp target: *p = val (deref write); only Deref is a valid lvalue, others fail fast + if tk == ast.ASTKind.UnaryOp: + uo_w: ast.UnaryOp* = ast.UnaryOp*(target) + if uo_w is None or uo_w.operand is None: self.Gen.TypeGenErr.fatal_error(target, "assignment target unary op is missing an operand", "TypeError") + if uo_w.op != ast.OpKind.Deref: + self.Gen.TypeGenErr.fatal_error(target, "assignment target supports only the unary '*' dereference (&x / -x / ~x are not valid lvalues)", "SyntaxError") + ptr_w: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo_w.operand, None, 0, trans) + if ptr_w is None or ptr_w.Ty is None or self.Gen.ExprGenBase.is_ptr_type(ptr_w.Ty) == 0: + self.Gen.TypeGenErr.fatal_error(target, "the operand of unary '*' assignment must be a pointer type (type inference degraded)", "TypeError") + pointee_w: mlirlite.LLVMType* = ptr_w.Ty.Pointee + if pointee_w is None: + self.Gen.TypeGenErr.fatal_error(target, "deref write target type missing (pointee is empty)", "TypeError") + sv_w: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, pointee_w, node, trans) + mlirlite.build_store(builder, sv_w, ptr_w) + continue + + # Attribute assignment: obj.field = val + if tk == ast.ASTKind.Attribute: + # Narrow target to ast.Attribute* explicitly (required under the strict pointer rules) + at_tg: ast.Attribute* = ast.Attribute*(target) + # property setter: when attr is a setter, emit the setter call and skip the field store + # (asgn.value gives the setter value param's AST for value-reference auto-address). + if self.Gen.CallGenProp._try_property_setter(pool, builder, mod, at_tg, rhs_val, trans, asgn.value) == 1: continue + # FakeDuck: write per-instance str fields through the {var}__meta__ shadow struct + fd_an: ast.Attribute* = at_tg + if fd_an is not None and fd_an.value is not None and fd_an.attr is not None: + if fd_an.value.kind() == ast.ASTKind.Name: + fd_obj_nm: ast.Name* = ast.Name*(fd_an.value) + if fd_obj_nm.id is not None: + # Pool buffer (f-string stack buffers die when the function returns) + fd_meta_w: str = pool.alloc(256) + if fd_meta_w is not None: viperlib.snprintf(fd_meta_w, 256, "%s__meta__", fd_obj_nm.id) + fd_meta_vw: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, fd_meta_w) + if fd_meta_vw is not None: + fd_sty_w: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type("_str") + if fd_sty_w is not None: + fd_fe_w: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(fd_sty_w, fd_an.attr) + if fd_fe_w is not None and fd_fe_w.Ty is not None: + fd_fp_w: mlirlite.Value* = mlirlite.build_gep_struct(builder, fd_sty_w, fd_fe_w.Ty, fd_meta_vw, fd_fe_w.Index) + if fd_fp_w is not None: + sv_fd: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, fd_fe_w.Ty, node) + mlirlite.build_store(builder, sv_fd, fd_fp_w) + continue + # VTABLE write: obj.method = func writes the vtable slot instead of a field + if self.Gen.CallGenVt._write_vtable_attr(builder, pool, mod, target, rhs_val, trans) == 1: continue + field_ptr: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, target, trans) + if field_ptr is not None: + # Get the field type and coerce rhs_val (e.g. i32 -> i64) + store_val: mlirlite.Value* = rhs_val + if field_ptr.Ty is not None and field_ptr.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, field_ptr.Ty.Pointee, node, trans) + # t.CVolatile field: store with the volatile qualifier + vol_fw: int = self.Gen.ExprGenAttr.attr_field_is_volatile(pool, trans, target) + # rawptr[t.CVolatile|T] base: p.field = v also stores with volatile + at_wr: ast.Attribute* = ast.Attribute*(target) + if at_wr is not None: + if self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, at_wr.value) != 0: vol_fw = 1 + mlirlite.build_store(builder, store_val, field_ptr, vol_fw) + continue + # Guard: field_ptr is None -> build a detailed error message + attr_node: ast.Attribute* = ast.Attribute*(target) + attr_name: str = "(unknown)" + obj_name: str = "(unknown)" + if attr_node is not None: + attr_name = attr_node.attr + if attr_node.value is not None and attr_node.value.kind() == ast.ASTKind.Name: + obj_nm: ast.Name* = ast.Name*(attr_node.value) + obj_name = obj_nm.id + err_buf: str = pool.alloc(256) + if err_buf is not None: + viperlib.snprintf(err_buf, 256, "attribute ptr is None: %s.%s", obj_name, attr_name) + self.Gen.TypeGenErr.fatal_error(target, err_buf) + else: self.Gen.TypeGenErr.fatal_error(target, "attribute ptr is None") + continue + + # Name assignment: var = val + if tk == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(target) + # Guard: skip when the target name is missing + if nm.id is None: continue + # global variable: write to the module-scope global + if GenApi.is_global_name(trans, nm.id) != 0: + mod_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + if mod_alloca is None: + # fail-fast: global variable not registered + self.Gen.TypeGenErr.fatal_error(target, f"global variable '{nm.id}' is not registered; cannot assign", "NameError") + # Object global with static storage: bind to storage and copy the object (else dangling/NULL) + stg_ref_as: mlirlite.Value* = None + if nm.id is not None: + stg_key_as: str = pool.alloc(sizet(string.strlen(nm.id)) + 8) + if stg_key_as is not None: + viperlib.snprintf(stg_key_as, sizet(string.strlen(nm.id)) + 8, "%s.__stg", nm.id) + stg_ref_as = self.Gen.VarGen.lookup_module_var(trans.SymTab, stg_key_as) + if self._is_addrlike_call(asgn.value) == 0 and stg_ref_as is not None and rhs_val is not None and rhs_val.Ty is not None and stg_ref_as.Ty is not None and stg_ref_as.Ty.Pointee != 0: + stg_ty_as: mlirlite.LLVMType* = stg_ref_as.Ty.Pointee + rhs_inner_as: mlirlite.LLVMType* = None + match rhs_val.Ty: + case mlirlite.LLVMType.Ptr(pointee_as): + rhs_inner_as = pointee_as + case _: + pass + if rhs_inner_as is not None and rhs_inner_as.kind() == mlirlite.LLVMType.Struct and self._stg_ty_equal(pool, stg_ty_as, rhs_inner_as) != 0: + # 1) Bind the global reference to storage (drop the temporary alloca) + mlirlite.build_store(builder, stg_ref_as, mod_alloca) + # 2) Copy the constructed temporary into storage + stg_obj_val_as: mlirlite.Value* = mlirlite.build_load(builder, stg_ty_as, rhs_val) + if stg_obj_val_as is not None: mlirlite.build_store(builder, stg_obj_val_as, stg_ref_as) + continue + target_ty: mlirlite.LLVMType* = None + if mod_alloca.Ty is not None: target_ty = mod_alloca.Ty.Pointee + if target_ty is not None: rhs_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, target_ty, node, trans) + # t.CVolatile global: store with the volatile qualifier + vol_gas: int = 0 + if nm.id is not None: vol_gas = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) + mlirlite.build_store(builder, rhs_val, mod_alloca, vol_gas) + continue + + # nonlocal variable: write through the closure env + if GenApi.is_nonlocal_name(trans, nm.id) != 0: + nl_ptr: mlirlite.Value* = self.Gen.StmtGenDecl.get_nonlocal_var_ptr(trans, nm.id) + if nl_ptr is None: + # fail-fast: nonlocal variable pointer resolution failed + self.Gen.TypeGenErr.fatal_error(target, f"nonlocal variable '{nm.id}' pointer resolution failed", "NameError") + # Infer the nonlocal type from nl_ptr instead of forcing i32 + nl_target_ty: mlirlite.LLVMType* = None + if nl_ptr.Ty is not None: nl_target_ty = nl_ptr.Ty.Pointee + if nl_target_ty is None: + # fail-fast: nl_ptr type lost, cannot store safely + self.Gen.TypeGenErr.fatal_error(target, f"nonlocal variable '{nm.id}' type information lost; cannot write", "TypeError") + rhs_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, nl_target_ty, node, trans) + mlirlite.build_store(builder, rhs_coerced, nl_ptr) + continue + + # REPL session variable: a Name target at fragment entry defines/assigns the session slot + if trans.ReplCtx is not None and self.Gen.VarGen.repl_in_frag_entry_scope(trans) != 0: + rp_entry: GenTypes.ReplVarEntry* = self.Gen.VarGen.repl_ensure_entry(trans, nm.id, target) + if rp_entry is not None: + self.Gen.VarGen.repl_try_assign(trans, nm.id, rhs_val, asgn.value, node) + continue + + # Block assignment to a module-level global inside a function without a global declaration + if self.Gen.VarGen.is_module_scope(trans.SymTab) == 0: + mod_gv: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + mod_decl_g: int = 0 + if mod_gv is not None: mod_decl_g = 1 + # 双保险:SymTab.Root.Vars 在部分时序/阶段可能看不到本文件已声明的模块全局 + # (现场:某函数对模块全局赋值漏写 global → 静默建局部影子 → 全局仍 NULL → + # 下游 ASan 写 0x208 崩)。按规则此处必须 fail-fast,不得静默建局部。 + if mod_decl_g == 0 and GenApi.is_file_var(nm.id) != 0: mod_decl_g = 1 + if mod_decl_g != 0: self.Gen.TypeGenErr.fatal_error(target, f"variable '{nm.id}' is a module-level global; assignment inside a function requires a global declaration (otherwise a local variable shadows the global, leaving it null and crashing)", "SyntaxError") + + # Ordinary local variable + alloca: mlirlite.Value* = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, nm.id, rhs_val.Ty) + if alloca is not None: + # Unannotated assignment: infer the class name from the RHS expression + cls_inf: str = self._infer_rhs_annot(pool, mod, asgn.value, trans, rhs_val) + if cls_inf is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, nm.id, cls_inf) + # Coerce the value to the alloca type (e.g. double -> float) + store_val: mlirlite.Value* = rhs_val + if alloca.Ty is not None and alloca.Ty.Pointee != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, alloca.Ty.Pointee, node, trans) + # t.CVolatile local variable: store with the volatile qualifier + vol_las: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)) + # M3 所有权记账:必须先记账再写值(覆盖释放要读到旧值); + # `p = new heap slife …` → 释放旧对象 + 影子置 1;`q = p` / 外来指针 → 交还所有权 + self._slife_note_assign(pool, builder, mod, trans, alloca, asgn.value, nm.id) + mlirlite.build_store(builder, store_val, alloca, vol_las) + # P2 willFree:对同名变量重新赋值即重新获得有效值,清除失效态 + # (新变量由 add_var 置 0;已存在变量走这里,见 clear_var_freed 注释) + self.Gen.VarGen.clear_var_freed(trans.SymTab, nm.id) + # A lambda assigned to a variable must be registered as a closure name + if asgn.value is not None and asgn.value.kind() == ast.ASTKind.Lambda: self.Gen.CallGenLeafKit.register_closure_name(nm.id) + existing: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) + if existing is None: new_vars += 1 + else: + # fail-fast: variable storage creation failed + self.Gen.TypeGenErr.fatal_error(target, f"variable '{nm.id}' storage creation failed", "TypeError") + + return new_vars + + # _reject_whole_object_copy_into_ptr_slot - fail fast when a rawptr[T] subscript gets a T pointer RHS + # (the whole object would be copied into an 8-byte pointer slot) + def _reject_whole_object_copy_into_ptr_slot(self, pool: memhub.MemBuddy*, trans: HT.Translator*, target: ast.AST*, elem_ptr: mlirlite.Value*, rhs_val: mlirlite.Value*) -> None: + """Fail fast when a rawptr[T] subscript gets a T pointer RHS.""" + if elem_ptr is None or elem_ptr.Ty is None: return + if rhs_val is None or rhs_val.Ty is None: return + elem_ty: mlirlite.LLVMType* = None + match elem_ptr.Ty: + case mlirlite.LLVMType.Ptr(pointee_wo): + elem_ty = pointee_wo + case _: + return + if elem_ty is None: return + # Only object (struct) elements are checked; scalar and pointer elements return early + if elem_ty.kind() != mlirlite.LLVMType.Struct: return + rhs_inner_wo: mlirlite.LLVMType* = None + match rhs_val.Ty: + case mlirlite.LLVMType.Ptr(pointee_rhs_wo): + rhs_inner_wo = pointee_rhs_wo + case _: + return + if rhs_inner_wo is None: return + if rhs_inner_wo.kind() != mlirlite.LLVMType.Struct: return + # The pointee type must match the element type + if self._stg_ty_equal(pool, elem_ty, rhs_inner_wo) == 0: return + # Array slots (t.CArray[T,N]) are legal; only block when the base is a pointer store + if self.Gen.ExprGenName.subscript_base_store_kind(pool, trans, target) != 0: return + elem_nm_wo: str = mlirlite.get_struct_name(elem_ty) + if elem_nm_wo is None: elem_nm_wo = "" + rhs_nm_wo: str = mlirlite.get_struct_name(rhs_inner_wo) + if rhs_nm_wo is None: rhs_nm_wo = elem_nm_wo + elem_sz_wo: int = self.Gen.ClassGenEnum._get_type_size(elem_ty) + ptr_sz_wo: int = 8 + over_wo: int = elem_sz_wo - ptr_sz_wo + if over_wo < 0: over_wo = 0 + col_s_wo: int = 0 + col_e_wo: int = 0 + if target is not None: + col_s_wo = target.col_offset + 1 + col_e_wo = target.end_col_offset + 1 + self.Gen.TypeGenErr.fatal_error(target, f"the lvalue element of the subscript assignment is object '{elem_nm_wo}' ({elem_sz_wo} bytes), " f"but the rhs is a pointer to '{rhs_nm_wo}': the compiler would silently emit a whole-object copy " f"(load {elem_nm_wo} + store {elem_nm_wo}, writing {elem_sz_wo} bytes), " f"instead of writing {ptr_sz_wo} bytes as a pointer. If that address actually stores pointers in 8-byte slots " f"(such as a rawptr[T] buffer/free list), every element written overflows by {over_wo} bytes, " f"writing through adjacent blocks. Fix: declare the base type of the pointer slot in pointer-element form " f"(`rawptr[T]*` or `rawptr[rawptr[T]]`) before assigning; " f"or explicitly cast the base to t.CPtr (untyped pointer) and write with 8-byte pointer semantics.", "TypeError", col_s_wo, col_e_wo) + + # _infer_rhs_annot - infer a variable's annotation class name from the RHS expression + def _infer_rhs_annot(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator*, rhs_val: mlirlite.Value* = None) -> str: + """Infer a variable's annotation class name from the RHS, or None.""" + if node is None or trans is None: return None + k: int = node.kind() + # Call: class instantiation or method call + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + if cl is None or cl.func is None: return None + if cl.func.kind() == ast.ASTKind.Name: + # Class instantiation: only accept a registered struct name + cand: str = (ast.Name*(cl.func)).id + if cand is None: return None + se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(cand) + if se is not None: return cand + return None + if cl.func.kind() == ast.ASTKind.Attribute: + # Method call: resolve the exact return class from the vpi annotation cache + ca: ast.Attribute* = ast.Attribute*(cl.func) + if ca is None or ca.attr is None or ca.value is None: return None + obj_cls: str = self._infer_rhs_annot(pool, mod, ca.value, trans) + if obj_cls is None: + # Module-level function call: resolve the return class from the module's vpi cache + if ca.value.kind() == ast.ASTKind.Name: + mod_nm_rhs: ast.Name* = ast.Name*(ca.value) + if mod_nm_rhs.id is not None: + mod_sha1_rhs: str = self.Gen.CallGenBase._lookup_module_sha1(mod_nm_rhs.id) + if mod_sha1_rhs is not None: + temp_dir_rhs: str = None + if self.Gen.CallGenBase.TempDir is not None: temp_dir_rhs = self.Gen.CallGenBase.TempDir + if temp_dir_rhs is not None: VpiReader._ensure_vpi_ret_annot_loaded(mod_sha1_rhs, temp_dir_rhs) + annot_rhs: str = VpiReader._lookup_vpi_ret_annot(mod_sha1_rhs, None, ca.attr) + if annot_rhs is not None: + ret_cls_rhs: str = self._extract_annot_class_name(pool, annot_rhs) + if ret_cls_rhs is not None: return ret_cls_rhs + return None + if self._cls_has_dunder(pool, mod, obj_cls, ca.attr) == 0: return None + # SHA1 of the module defining the method (struct entry, else current module) + cls_sha1: str = self.Gen.StructGen.get_struct_sha1(obj_cls) + if cls_sha1 is None and trans.ModuleSha1 is not None: cls_sha1 = trans.ModuleSha1 + ret_cls: str = None + if cls_sha1 is not None: + temp_dir: str = None + if self.Gen.CallGenBase.TempDir is not None: temp_dir = self.Gen.CallGenBase.TempDir + if temp_dir is not None: VpiReader._ensure_vpi_ret_annot_loaded(cls_sha1, temp_dir) + annot_ret: str = VpiReader._lookup_vpi_ret_annot(cls_sha1, obj_cls, ca.attr) + if annot_ret is not None: ret_cls = self._extract_annot_class_name(pool, annot_ret) + return ret_cls + return None + # Name: the source variable annotation + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm is None or nm.id is None: return None + ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) + if ve is None: return None + return self.Gen.VarGen.var_annot_class_name(ve) + # BinOp/UnaryOp: operator overload returns an object of the same class + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(node) + if bop is None or bop.left is None: return None + dunder: str = self._op_to_dunder(bop.op) + if dunder is None: return None + lhs_cls: str = self._infer_rhs_annot(pool, mod, bop.left, trans) + if lhs_cls is None or self._cls_has_dunder(pool, mod, lhs_cls, dunder) == 0: return None + return lhs_cls + if k == ast.ASTKind.UnaryOp: + uop: ast.UnaryOp* = ast.UnaryOp*(node) + if uop is None or uop.operand is None: return None + dunder: str = self._op_to_dunder(uop.op) + if dunder is None: return None + opr_cls: str = self._infer_rhs_annot(pool, mod, uop.operand, trans) + if opr_cls is None or self._cls_has_dunder(pool, mod, opr_cls, dunder) == 0: return None + return opr_cls + # Attribute: field annotation class name (recursive attribute chain) + if k == ast.ASTKind.Attribute: return self.Gen.ExprGenAttr._resolve_attr_field_class(pool, trans, node) + return None + + # _extract_annot_class_name - extract a registered struct class name from a return annotation text + def _extract_annot_class_name(self, pool: memhub.MemBuddy*, annot: str) -> str: + """Extract a registered struct class name from a return annotation.""" + if pool is None or annot is None: return None + alen: sizet = string.strlen(annot) + if alen == 0: return None + seg: str = pool.alloc(256) + if seg is None: return None + start: sizet = 0 + i: sizet + for i in range(int(alen)): + if annot[i] == '|': + seglen: sizet = i - start + # Segment [start, i): trim spaces/quotes then look up the registry + s: sizet = start + e: sizet = i + while s < e and (annot[s] == ' ' or annot[s] == '\'' or annot[s] == '"'): s += 1 + # Also strip a trailing '*'; otherwise find_struct misses and chained calls degrade + while e > s and (annot[e - 1] == ' ' or annot[e - 1] == '\'' or annot[e - 1] == '"' or annot[e - 1] == '*'): e -= 1 + seglen = e - s + if seglen > 0 and seglen < 256: + string.strncpy(seg, annot + s, seglen) + seg[seglen] = '\0' + se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(seg) + if se is not None: return seg + start = i + 1 + # Last segment (no '|' suffix) + s: sizet = start + e: sizet = alen + while s < e and (annot[s] == ' ' or annot[s] == '\'' or annot[s] == '"'): s += 1 + # Also strip a trailing '*'; otherwise find_struct misses and chained calls degrade + while e > s and (annot[e - 1] == ' ' or annot[e - 1] == '\'' or annot[e - 1] == '"' or annot[e - 1] == '*'): e -= 1 + seglen: sizet = e - s + if seglen > 0 and seglen < 256: + string.strncpy(seg, annot + s, seglen) + seg[seglen] = '\0' + se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(seg) + if se is not None: return seg + return None + + # _op_to_dunder - operator -> dunder method name (local copy to avoid cross-mixin calls) + def _op_to_dunder(self, op: int) -> str: + """Map a BinOp/UnaryOp operator to its dunder name, or None.""" + if op == ast.OpKind.Add: return "__add__" + if op == ast.OpKind.Sub: return "__sub__" + if op == ast.OpKind.Mult: return "__mul__" + if op == ast.OpKind.Div: return "__truediv__" + if op == ast.OpKind.FloorDiv: return "__floordiv__" + if op == ast.OpKind.Mod: return "__mod__" + if op == ast.OpKind.Pow: return "__pow__" + if op == ast.OpKind.BitAnd: return "__and__" + if op == ast.OpKind.BitOr: return "__or__" + if op == ast.OpKind.BitXor: return "__xor__" + if op == ast.OpKind.LShift: return "__lshift__" + if op == ast.OpKind.RShift: return "__rshift__" + if op == ast.OpKind.USub: return "__neg__" + if op == ast.OpKind.UAdd: return "__pos__" + if op == ast.OpKind.Invert: return "__invert__" + return None + + # _aug_op_to_dunder - augmented operator -> aug dunder (__iadd__ etc.); None falls back to _op_to_dunder + def _aug_op_to_dunder(self, op: int) -> str: + """Map an AugAssign operator to its in-place dunder name, or None.""" + if op == ast.OpKind.Add: return "__iadd__" + if op == ast.OpKind.Sub: return "__isub__" + if op == ast.OpKind.Mult: return "__imul__" + if op == ast.OpKind.Div: return "__itruediv__" + if op == ast.OpKind.FloorDiv: return "__ifloordiv__" + if op == ast.OpKind.Mod: return "__imod__" + if op == ast.OpKind.Pow: return "__ipow__" + if op == ast.OpKind.BitAnd: return "__iand__" + if op == ast.OpKind.BitOr: return "__ior__" + if op == ast.OpKind.BitXor: return "__ixor__" + if op == ast.OpKind.LShift: return "__ilshift__" + if op == ast.OpKind.RShift: return "__irshift__" + return None + + # _cls_has_dunder - check whether a class (including its MRO) has a dunder method + def _cls_has_dunder(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_name: str, dunder: str) -> int: + """Return 1 if the class or its MRO chain has the dunder method.""" + if class_name is None or dunder is None or mod is None: return 0 + lookup_name: str = pool.alloc(128) + if lookup_name is None: return 0 + viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder) + if self.Gen.CallGenBase.find_func_in_module(mod, lookup_name) is not None: return 1 + # Search the inheritance chain ancestors + mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name) + mi: int = 1 + while mro_ptr is not None and mro_ptr[mi] != 0: + anc: str = rawptr(mro_ptr[mi]) + if anc is not None: + anc_lookup: str = pool.alloc(128) + if anc_lookup is not None: + viperlib.snprintf(anc_lookup, 128, "%s.%s", anc, dunder) + if self.Gen.CallGenBase.find_func_in_module(mod, anc_lookup) is not None: return 1 + mi += 1 + # Cross-module: a class with a SHA1 (method declared in the stub) counts as having it + if self.Gen.StructGen.get_struct_sha1(class_name) is not None: return 1 + return 0 + + def InitAssignGen(self) -> int: + """Initialize AssignGen fields (called by Generator at construction).""" + if self is None: return 1 + self.RecoverFound = 0 + return 0 diff --git a/App/lib/core/Gen/AsyncGenMixin.vp b/App/lib/core/Gen/AsyncGenMixin.vp index 030f947..84debe4 100644 --- a/App/lib/core/Gen/AsyncGenMixin.vp +++ b/App/lib/core/Gen/AsyncGenMixin.vp @@ -1,2098 +1,2098 @@ -import lib.core.Gen.GenTypes as GenTypes -import t, c -from stdint import * -import ast -import mlirlite -from mlirlite import LLVMType -import memhub -import string -import viperlib -import stdio -import stdlib -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HT -import lib.core.Gen.Call.CallGenBase as CallGenBase - - -# ============================================================ -# AsyncGenMixin - TPV-M1 coroutine translation (async def / await / coro.coro_run) -define SCOPE_FUNCTION = 1 -novirtual class AsyncGenMixin(CallGenBase.CallGenBase): - """Coroutine translation mixin (TPV-M1 state machine)""" - - # Entry: async function definition. Called by FuncGenDef.translate_function_def after - # detecting fd.flags & FLAG_IS_ASYNC; early-returns so it never enters the normal path. - def translate_async_function(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Translate an async function definition; returns 0 on success""" - fd: ast.FunctionDef* = ast.FunctionDef*(node) - if fd is None or fd.name is None or trans is None: return 0 - name: str = fd.name - pool: memhub.MemBuddy* = trans.Pool - mod: mlirlite.LLVMModule* = trans.Module - if pool is None or mod is None: return 0 - - # avoid duplicate generation: create_ already exists (multi-pass / dup def) - cname_buf: str = pool.alloc(64) - if cname_buf is not None: viperlib.snprintf(cname_buf, 64, "create_%s", name) - create_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, cname_buf, 0) - if self.Gen.CallGenBase.find_func_in_module(mod, create_mangled) is not None: return 0 - - # register the async bare name (await target check / coro_run reverse lookup) - self._register_async_func(trans, name) - - # M2: collect params -> trans._coro_params (char* array, 8 bytes per item) - trans._coro_param_count = 0 - nparams: int = self._collect_param_names(trans, fd) - - # collect locals (top-level AnnAssign/Assign Name targets -> frame slots) - trans._coro_local_count = 0 - nlocals: int = self._collect_local_names(trans, fd) - - # M3-M7: pre-scan extra slots (wake/for/await type tables + position arrays) + state ids. - # _coro_await_count = resume-block count; nseg = 1 entry + await_count. - trans._coro_extra_count = 0 - trans._coro_hoist_count = 0 - trans._coro_await_count = 0 - trans._coro_uses_sleep = 0 - trans._coro_scan_for_count = 0 - trans._coro_for_counter = 0 - self._scan_sleep_usage(trans, fd) - self._pre_scan_awaits(trans, fd) - nextra: int = trans._coro_extra_count - trans._coro_nseg = 1 + trans._coro_await_count - - # frame struct %Coro_ - frame_ty: mlirlite.LLVMType* = self._build_frame_ty(pool, mod, trans, name, nparams, nlocals, nextra) - if frame_ty is None: return 0 - self._register_coro_frame(trans, name, frame_ty) - # M3: register the wake-slot index (coro_run polling; wake is extra slot 0) - if trans._coro_uses_sleep != 0: self._register_coro_wake(trans, name, trans._coro_extra_base + 0) - - # enter function scope so resume slot registration stays isolated from module scope - self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_FUNCTION) - - self._emit_create(pool, mod, trans, name, frame_ty, nparams) - self._emit_resume(pool, mod, trans, name, fd, frame_ty, nparams, nlocals, nextra) - - self.Gen.VarGen.exit_scope(trans.SymTab) - return 0 - - # ============================================================ - # M2: parameter collection (into frame param slots) - def _collect_param_names(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: - """Collect async parameter names into trans._coro_params (8-byte slots); slot index = 3 + i""" - if fd is None or fd.args is None or trans is None: return 0 - if trans._coro_params is None: return 0 - ags: ast.Arguments* = ast.Arguments*(fd.args) - cnt: int = 0 - if ags.args is None: return 0 - bn: sizet = len(ags.args) - for i in range(int(bn)): - a0: ast.AST* = ags.args.get(i) - if a0 is None: continue - aname: str = None - if a0.kind() == ast.ASTKind.Arg: - ar: ast.Arg* = ast.Arg*(a0) - aname = ar.arg - elif a0.kind() == ast.ASTKind.Name: - anm: ast.Name* = ast.Name*(a0) - aname = anm.id - if aname is None: continue - if cnt >= HT.MAX_CORO_VARS: break - addr: uint64 = uint64(trans._coro_params) + cnt * 8 - slot: str* = (str*)(addr) - slot[0] = aname - cnt = cnt + 1 - trans._coro_param_count = cnt - return cnt - - # M3-M7: pre-scan extra slots + state ids in the same DFS order as translation. - # Extra-slot table _coro_extra_types (1=i32, 2=i64); position arrays map slot -> relative index. - def _pre_scan_awaits(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: - """Pre-scan the coroutine body to fill the extra-slot table and return its count""" - if fd is None or fd.children is None or trans is None: return 0 - trans._coro_extra_count = 0 - trans._coro_hoist_count = 0 - trans._coro_await_count = 0 - trans._coro_scan_for_count = 0 - if trans._coro_uses_sleep != 0: - trans._coro_extra_types[0] = 2 # __coro_wake_us (i64) relative index 0 - trans._coro_extra_count = 1 - self._scan_stmts(trans, fd.children) - return trans._coro_extra_count - - # ---------- pass 1: coro_sleep detection (decides the wake slot) ---------- - def _scan_sleep_usage(self, trans: HT.Translator*, fd: ast.FunctionDef*): - if fd is None or fd.children is None or trans is None: return - self._scan_sleep_stmts(trans, fd.children) - - def _scan_sleep_stmts(self, trans: HT.Translator*, stmts: list[ast.AST*]*): - if stmts is None or trans._coro_uses_sleep != 0: return - bn: sizet = len(stmts) - i: sizet = 0 - for i in range(int(bn)): self._scan_sleep_stmt(trans, stmts.get(i)) - - def _scan_sleep_stmt(self, trans: HT.Translator*, stmt: ast.AST*): - if stmt is None or trans._coro_uses_sleep != 0: return - sk: int = stmt.kind() - if sk == ast.ASTKind.If: - ifn: ast.If* = ast.If*(stmt) - self._scan_sleep_expr(trans, ifn.test) - self._scan_sleep_stmts(trans, stmt.children) - self._scan_sleep_stmts(trans, ifn.orelse) - return - if sk == ast.ASTKind.While: - whn: ast.While* = ast.While*(stmt) - self._scan_sleep_expr(trans, whn.test) - self._scan_sleep_stmts(trans, stmt.children) - self._scan_sleep_stmts(trans, whn.orelse) - return - if sk == ast.ASTKind.For: - fn2: ast.For* = ast.For*(stmt) - self._scan_sleep_expr(trans, fn2.iter) - self._scan_sleep_stmts(trans, stmt.children) - self._scan_sleep_stmts(trans, fn2.orelse) - return - if sk == ast.ASTKind.Try: - trn: ast.Try* = ast.Try*(stmt) - self._scan_sleep_stmts(trans, stmt.children) - if trn.handlers is not None: - nh: sizet = len(trn.handlers) - hh: sizet = 0 - for hh in range(int(nh)): - h0: ast.AST* = trn.handlers.get(hh) - if h0 is not None: self._scan_sleep_stmts(trans, h0.children) - self._scan_sleep_stmts(trans, trn.orelse) - self._scan_sleep_stmts(trans, trn.finalbody) - return - if sk == ast.ASTKind.Return: - rv: ast.Return* = ast.Return*(stmt) - self._scan_sleep_expr(trans, rv.value) - return - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - self._scan_sleep_expr(trans, aa.value) - return - if sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - self._scan_sleep_expr(trans, as_.value) - return - if sk == ast.ASTKind.Expr: - e: ast.Expr* = ast.Expr*(stmt) - self._scan_sleep_expr(trans, e.value) - return - - def _scan_sleep_expr(self, trans: HT.Translator*, node: ast.AST*): - if node is None or trans._coro_uses_sleep != 0: return - k: int = node.kind() - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - if cl.func is not None: - if cl.func.kind() == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(cl.func) - if nm.id is not None and string.strcmp(nm.id, "coro_sleep") == 0: - trans._coro_uses_sleep = 1 - return - elif cl.func.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(cl.func) - if at.attr is not None and string.strcmp(at.attr, "coro_sleep") == 0: - trans._coro_uses_sleep = 1 - return - self._scan_sleep_expr(trans, cl.func) - if cl.args is not None: - nb: sizet = len(cl.args) - j: sizet = 0 - for j in range(int(nb)): self._scan_sleep_expr(trans, cl.args.get(j)) - return - if k == ast.ASTKind.BinOp: - b: ast.BinOp* = ast.BinOp*(node) - self._scan_sleep_expr(trans, b.left) - self._scan_sleep_expr(trans, b.right) - return - if k == ast.ASTKind.UnaryOp: - u: ast.UnaryOp* = ast.UnaryOp*(node) - self._scan_sleep_expr(trans, u.operand) - return - if k == ast.ASTKind.Compare: - cm: ast.Compare* = ast.Compare*(node) - self._scan_sleep_expr(trans, cm.left) - if cm.comparators is not None: - nb2: sizet = len(cm.comparators) - j2: sizet = 0 - for j2 in range(int(nb2)): self._scan_sleep_expr(trans, cm.comparators.get(j2)) - return - if k == ast.ASTKind.BoolOp: - bo: ast.BoolOp* = ast.BoolOp*(node) - if bo.values is not None: - nb3: sizet = len(bo.values) - j3: sizet = 0 - for j3 in range(int(nb3)): self._scan_sleep_expr(trans, bo.values.get(j3)) - return - if k == ast.ASTKind.IfExp: - ie: ast.IfExp* = ast.IfExp*(node) - self._scan_sleep_expr(trans, ie.test) - self._scan_sleep_expr(trans, ie.body) - self._scan_sleep_expr(trans, ie.orelse) - return - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(node) - self._scan_sleep_expr(trans, at.value) - return - if k == ast.ASTKind.Subscript: - sb: ast.Subscript* = ast.Subscript*(node) - self._scan_sleep_expr(trans, sb.value) - self._scan_sleep_expr(trans, sb.slice) - return - if k == ast.ASTKind.Await: - aw: ast.Await* = ast.Await*(node) - self._scan_sleep_expr(trans, aw.value) - return - - # ---------- pass 2: fill the extra-slot table + count state ids ---------- - def _scan_stmts(self, trans: HT.Translator*, stmts: list[ast.AST*]*): - if stmts is None: return - bn: sizet = len(stmts) - i: sizet = 0 - for i in range(int(bn)): self._scan_stmt(trans, stmts.get(i)) - - def _scan_stmt(self, trans: HT.Translator*, stmt: ast.AST*): - if stmt is None: return - sk: int = stmt.kind() - if sk == ast.ASTKind.If: - ifn: ast.If* = ast.If*(stmt) - if ifn.test is not None: self._scan_expr(trans, ifn.test) - self._scan_stmts(trans, stmt.children) - self._scan_stmts(trans, ifn.orelse) - return - if sk == ast.ASTKind.While: - whn: ast.While* = ast.While*(stmt) - if whn.test is not None: self._scan_expr(trans, whn.test) - self._scan_stmts(trans, stmt.children) - self._scan_stmts(trans, whn.orelse) - return - if sk == ast.ASTKind.For: - fn_: ast.For* = ast.For*(stmt) - if self._for_deep_await(stmt) != 0: - # contains await: must be a range-For (M3) - if self._is_range_for(stmt) == 0: - err_b2: str = trans.Pool.alloc(256) - if err_b2 is not None: - viperlib.snprintf(err_b2, 256, "[coro-M3] For inside a coroutine supports only range iteration (including await)") - self.Gen.TypeGenErr.fatal_error(stmt, err_b2, "SyntaxError") - return - # fill __f_idx_N / __f_len_N (i32) - if trans._coro_scan_for_count < HT.MAX_CORO_VARS: trans._coro_for_slots[trans._coro_scan_for_count] = trans._coro_extra_count - trans._coro_extra_types[trans._coro_extra_count] = 1 - trans._coro_extra_count = trans._coro_extra_count + 1 - trans._coro_extra_types[trans._coro_extra_count] = 1 - trans._coro_extra_count = trans._coro_extra_count + 1 - trans._coro_scan_for_count = trans._coro_scan_for_count + 1 - self._scan_stmts(trans, stmt.children) - self._scan_stmts(trans, fn_.orelse) - return - # no await: delegate to standard For; recurse (nested For/Try may contain await) - self._scan_stmts(trans, stmt.children) - self._scan_stmts(trans, fn_.orelse) - return - if sk == ast.ASTKind.Try: - tr2: ast.Try* = ast.Try*(stmt) - self._scan_stmts(trans, stmt.children) - if tr2.handlers is not None: - nh2: sizet = len(tr2.handlers) - hh2: sizet = 0 - for hh2 in range(int(nh2)): - h2: ast.AST* = tr2.handlers.get(hh2) - if h2 is not None: self._scan_stmts(trans, h2.children) - self._scan_stmts(trans, tr2.orelse) - self._scan_stmts(trans, tr2.finalbody) - return - if sk == ast.ASTKind.Raise: - rz: ast.Raise* = ast.Raise*(stmt) - if rz.exc is not None: self._scan_expr(trans, rz.exc) - return - if sk == ast.ASTKind.Return: - rv: ast.Return* = ast.Return*(stmt) - if rv.value is not None: self._scan_expr(trans, rv.value) - return - if sk == ast.ASTKind.Break or sk == ast.ASTKind.Continue: return - # top-level await (AnnAssign/Assign/Expr value is Await): count without hoisting - if sk == ast.ASTKind.AnnAssign or sk == ast.ASTKind.Assign or sk == ast.ASTKind.Expr: - if self._is_await_tail(stmt) != 0: - trans._coro_await_count = trans._coro_await_count + 1 - return - # nested await inside a statement: DFS the value expression - self._scan_stmt_exprs(trans, stmt) - - def _scan_stmt_exprs(self, trans: HT.Translator*, stmt: ast.AST*): - """Scan statement value expressions for nested awaits""" - if stmt is None: return - sk: int = stmt.kind() - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - if aa.value is not None: self._scan_expr(trans, aa.value) - return - if sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - if as_.value is not None: self._scan_expr(trans, as_.value) - return - if sk == ast.ASTKind.Expr: - e: ast.Expr* = ast.Expr*(stmt) - if e.value is not None: self._scan_expr(trans, e.value) - return - # other statement types: fail fast at translation - - def _scan_expr(self, trans: HT.Translator*, node: ast.AST*): - """DFS the expression for Await: allocate a __await_N slot and count the state number""" - if node is None: return - k: int = node.kind() - if k == ast.ASTKind.Await: - # M3: await coro_sleep suspends via the wake slot, not __await_N (state id only) - aw_s: ast.Await* = ast.Await*(node) - if self._expr_await_is_sleep(aw_s) != 0: - trans._coro_await_count = trans._coro_await_count + 1 - return - if trans._coro_hoist_count < HT.MAX_CORO_VARS: trans._coro_await_slots[trans._coro_hoist_count] = trans._coro_extra_count - trans._coro_extra_types[trans._coro_extra_count] = 1 - trans._coro_extra_count = trans._coro_extra_count + 1 - trans._coro_hoist_count = trans._coro_hoist_count + 1 - trans._coro_await_count = trans._coro_await_count + 1 - return - if k == ast.ASTKind.BinOp: - b: ast.BinOp* = ast.BinOp*(node) - self._scan_expr(trans, b.left) - self._scan_expr(trans, b.right) - return - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - if cl.func is not None: self._scan_expr(trans, cl.func) - if cl.args is not None: - nb: sizet = len(cl.args) - j: sizet = 0 - for j in range(int(nb)): self._scan_expr(trans, cl.args.get(j)) - return - if k == ast.ASTKind.UnaryOp: - u: ast.UnaryOp* = ast.UnaryOp*(node) - if u.operand is not None: self._scan_expr(trans, u.operand) - return - if k == ast.ASTKind.Compare: - cm: ast.Compare* = ast.Compare*(node) - if cm.left is not None: self._scan_expr(trans, cm.left) - if cm.comparators is not None: - nb2: sizet = len(cm.comparators) - j2: sizet = 0 - for j2 in range(int(nb2)): self._scan_expr(trans, cm.comparators.get(j2)) - return - if k == ast.ASTKind.BoolOp: - bo: ast.BoolOp* = ast.BoolOp*(node) - if bo.values is not None: - nb3: sizet = len(bo.values) - j3: sizet = 0 - for j3 in range(int(nb3)): self._scan_expr(trans, bo.values.get(j3)) - return - if k == ast.ASTKind.IfExp: - ie: ast.IfExp* = ast.IfExp*(node) - self._scan_expr(trans, ie.test) - self._scan_expr(trans, ie.body) - self._scan_expr(trans, ie.orelse) - return - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(node) - if at.value is not None: self._scan_expr(trans, at.value) - return - if k == ast.ASTKind.Subscript: - sb: ast.Subscript* = ast.Subscript*(node) - if sb.value is not None: self._scan_expr(trans, sb.value) - if sb.slice is not None: self._scan_expr(trans, sb.slice) - return - # leaves like Name / Constant: no nested await - - # ---------- For helpers (M3) ---------- - def _for_deep_await(self, stmt: ast.AST*) -> int: - """Return whether a For statement tree contains await""" - if stmt is None: return 0 - fn3: ast.For* = ast.For*(stmt) - if fn3.iter is not None and self._find_expr_await(fn3.iter) is not None: return 1 - if stmt.children is not None: - bn4: sizet = len(stmt.children) - i4: sizet = 0 - for i4 in range(int(bn4)): - if self._stmt_deep_await(stmt.children.get(i4)) != 0: return 1 - if fn3.orelse is not None: - bn5: sizet = len(fn3.orelse) - i5: sizet = 0 - for i5 in range(int(bn5)): - if self._stmt_deep_await(fn3.orelse.get(i5)) != 0: return 1 - return 0 - - def _stmt_deep_await(self, stmt: ast.AST*) -> int: - """Return whether a statement tree contains await""" - if stmt is None: return 0 - sk2: int = stmt.kind() - if sk2 == ast.ASTKind.If: - ifn2: ast.If* = ast.If*(stmt) - if ifn2.test is not None and self._find_expr_await(ifn2.test) is not None: return 1 - if self._stmts_deep_await(stmt.children) != 0: return 1 - return self._stmts_deep_await(ifn2.orelse) - if sk2 == ast.ASTKind.While: - whn2: ast.While* = ast.While*(stmt) - if whn2.test is not None and self._find_expr_await(whn2.test) is not None: return 1 - if self._stmts_deep_await(stmt.children) != 0: return 1 - return self._stmts_deep_await(whn2.orelse) - if sk2 == ast.ASTKind.For: return self._for_deep_await(stmt) - if sk2 == ast.ASTKind.Try: - tr3: ast.Try* = ast.Try*(stmt) - if self._stmts_deep_await(stmt.children) != 0: return 1 - if tr3.handlers is not None: - nh3: sizet = len(tr3.handlers) - hh3: sizet = 0 - for hh3 in range(int(nh3)): - h3: ast.AST* = tr3.handlers.get(hh3) - if h3 is not None and self._stmts_deep_await(h3.children) != 0: return 1 - if self._stmts_deep_await(tr3.orelse) != 0: return 1 - return self._stmts_deep_await(tr3.finalbody) - return self._stmt_has_await(stmt) - - def _stmts_deep_await(self, stmts: list[ast.AST*]*) -> int: - if stmts is None: return 0 - bn6: sizet = len(stmts) - i6: sizet = 0 - for i6 in range(int(bn6)): - if self._stmt_deep_await(stmts.get(i6)) != 0: return 1 - return 0 - - def _is_range_for(self, stmt: ast.AST*) -> int: - """Return whether a for-loop is a range-for with a plain Name target and no await in its args""" - if stmt is None: return 0 - fn4: ast.For* = ast.For*(stmt) - if fn4.target is None or fn4.target.kind() != ast.ASTKind.Name: return 0 - if fn4.iter is None or fn4.iter.kind() != ast.ASTKind.Call: return 0 - cl4: ast.Call* = ast.Call*(fn4.iter) - if cl4.func is None or cl4.func.kind() != ast.ASTKind.Name: return 0 - nm4: ast.Name* = ast.Name*(cl4.func) - if nm4.id is None or string.strcmp(nm4.id, "range") != 0: return 0 - if cl4.args is None: return 0 - nb7: sizet = len(cl4.args) - if nb7 != 1 and nb7 != 2: return 0 - j7: sizet = 0 - for j7 in range(int(nb7)): - if self._find_expr_await(cl4.args.get(j7)) is not None: return 0 - return 1 - - # ============================================================ - # Local-variable collection (top-level AnnAssign / Assign Name targets) - def _collect_local_names(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: - """Collect frame local variable names into trans._coro_locals (8-byte slots)""" - if fd is None or fd.children is None or trans is None: return 0 - if trans._coro_locals is None: return 0 - trans._coro_local_count = 0 - self._collect_locals_stmts(trans, fd.children) - return trans._coro_local_count - - def _collect_locals_stmts(self, trans: HT.Translator*, stmts: list[ast.AST*]*): - if stmts is None: return - bn: sizet = len(stmts) - i: sizet = 0 - for i in range(int(bn)): self._collect_locals_stmt(trans, stmts.get(i)) - - def _collect_locals_stmt(self, trans: HT.Translator*, stmt: ast.AST*): - if stmt is None: return - sk: int = stmt.kind() - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - if aa.target is not None and aa.target.kind() == ast.ASTKind.Name: - tn: ast.Name* = ast.Name*(aa.target) - self._add_local(trans, tn.id) - return - if sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - if as_.targets is not None and len(as_.targets) == 1: - t0: ast.AST* = as_.targets.get(0) - if t0 is not None and t0.kind() == ast.ASTKind.Name: - tn2: ast.Name* = ast.Name*(t0) - self._add_local(trans, tn2.id) - return - if sk == ast.ASTKind.If: - self._collect_locals_stmts(trans, stmt.children) - ifn: ast.If* = ast.If*(stmt) - self._collect_locals_stmts(trans, ifn.orelse) - return - if sk == ast.ASTKind.While: - self._collect_locals_stmts(trans, stmt.children) - whn: ast.While* = ast.While*(stmt) - self._collect_locals_stmts(trans, whn.orelse) - return - if sk == ast.ASTKind.For: - fn: ast.For* = ast.For*(stmt) - if self._for_deep_await(stmt) != 0: - if fn.target is not None and fn.target.kind() == ast.ASTKind.Name: - tn3: ast.Name* = ast.Name*(fn.target) - self._add_local(trans, tn3.id) - self._collect_locals_stmts(trans, stmt.children) - self._collect_locals_stmts(trans, fn.orelse) - return - if sk == ast.ASTKind.Try: - tr: ast.Try* = ast.Try*(stmt) - self._collect_locals_stmts(trans, stmt.children) - if tr.handlers is not None: - nh: sizet = len(tr.handlers) - hh: sizet = 0 - for hh in range(int(nh)): - h0: ast.AST* = tr.handlers.get(hh) - if h0 is None: continue - hd: ast.ExceptHandler* = ast.ExceptHandler*(h0) - self._add_local(trans, hd.name) - self._collect_locals_stmts(trans, h0.children) - self._collect_locals_stmts(trans, tr.orelse) - self._collect_locals_stmts(trans, tr.finalbody) - return - - def _add_local(self, trans: HT.Translator*, name: str): - if name is None: return - cnt: int = trans._coro_local_count - if cnt >= HT.MAX_CORO_VARS: return - addr: uint64 = uint64(trans._coro_locals) + cnt * 8 - slot: str* = (str*)(addr) - slot[0] = name - trans._coro_local_count = cnt + 1 - - # Frame struct %Coro_ = { i32 state, i32 done, i32 ret, ... }; - # slot index: 0=state 1=done 2=ret 3=exc 4=msg, then params / locals / extra slots. - def _build_frame_ty(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str, nparams: int, nlocals: int, nextra: int) -> mlirlite.LLVMType*: - """Build the frame struct, register it on the module and struct table, and return its LLVMType""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - # slot base (used by resume/await to compute slot indexes) - trans._coro_local_base = 5 + nparams - trans._coro_extra_base = trans._coro_local_base + nlocals - total: int = 5 + nparams + nlocals + nextra - head: mlirlite.ParamNode* = None - prev: mlirlite.ParamNode* = None - fi: int = 0 - for fi in range(int(total)): - fty: mlirlite.LLVMType* = i32_ty - if fi == 4: fty = i8_ptr_ty - elif fi >= trans._coro_extra_base: - tidx: int = fi - trans._coro_extra_base - if tidx < nextra and trans._coro_extra_types is not None: - if trans._coro_extra_types[tidx] == 2: fty = i64_ty - pnode: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) - if pnode is None: continue - string.memset(pnode, 0, mlirlite.ParamNode.__sizeof__()) - pnode.Ty = fty - if head is None: head = pnode - if prev is not None: prev.Next = pnode - prev = pnode - ty_name: str = pool.alloc(128) - if ty_name is not None: viperlib.snprintf(ty_name, 128, "Coro_%s", name) - frame_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, head, total, ty_name) - if frame_ty is None: return None - mlirlite.module_add_named_type(mod, pool, frame_ty) - self.Gen.StructGen.register_struct(pool, ty_name, frame_ty, trans.ModuleSha1) - return frame_ty - - # ============================================================ - # create_() -> %Coro_* - def _emit_create(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str, frame_ty: mlirlite.LLVMType*, nparams: int) -> int: - """Emit create_(args...): pop or malloc a frame, zero the header, store args, return it""" - if frame_ty is None: return 0 - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - i64_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i64_ty) - frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) - - cname_buf2: str = pool.alloc(64) - if cname_buf2 is not None: viperlib.snprintf(cname_buf2, 64, "create_%s", name) - mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, cname_buf2, 0) - if self.Gen.CallGenBase.find_func_in_module(mod, mangled) is not None: return 0 - func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled, frame_ptr_ty) - if func is None: return 0 - # params (i32, frame slot 5+i). TPV has no Python "%" formatting ("%" is modulo), - # so names are built with snprintf ("%%arg" -> "%arg", matching resume's "%frame"). - pi: int = 0 - argn_buf: str = pool.alloc(16) - for pi in range(int(nparams)): - if argn_buf is not None: viperlib.snprintf(argn_buf, 16, "%%arg%d", pi) - mlirlite.add_param(pool, func, i32_ty, argn_buf) - # register in the function table (bare name create_) - funcs_ptr: GenTypes.FuncEntry* = trans._funcs - cur_count: int = trans._func_count - if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, cur_count, cname_buf2, func, 1024, None, 0, 0, trans.Pool) == 0: trans._func_count = cur_count + 1 - - entry: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") - if entry is None: return 0 - builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) - if builder is None: return 0 - mlirlite.position_at_end(builder, entry) - - frame_size: int = self.Gen.ClassGenEnum._get_type_size(frame_ty) - size_val: mlirlite.Value* = mlirlite.const_int64(pool, frame_size) - if size_val is None: return 0 - size_val.Next = None - - # M7 frame pool: i64 head (0=empty); first 8 bytes hold next (state/done zeroed on reuse) - pool_gv: mlirlite.GlobalVariable* = self._get_coro_pool(pool, mod, trans, name) - # GlobalVariable is not a Value: build a Value view (Ty=Ptr(i64), Name="@coro_pool_") - pool_ref_name: str = pool.alloc(96) - if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, 96, "@coro_pool_%s", name) - pool_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i64_ptr_ty, pool_ref_name) - pool_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, pool_ref) - if pool_head is None: return 0 - zero64: mlirlite.Value* = mlirlite.const_int64(pool, 0) - empty_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, pool_head, zero64) - if empty_cond is None: return 0 - hit_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "pool.hit") - miss_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "pool.miss") - join_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "pool.join") - if hit_bb is None or miss_bb is None or join_bb is None: return 0 - mlirlite.build_cond_br(builder, empty_cond, miss_bb, hit_bb) - # hit: pop from pool (first 8 bytes are next) - mlirlite.position_at_end(builder, hit_bb) - node_next_ptr: mlirlite.Value* = mlirlite.build_inttoptr(builder, pool_head, i64_ptr_ty) - next_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, node_next_ptr) - if next_head is None: return 0 - mlirlite.build_store(builder, next_head, pool_ref) - hit_frame: mlirlite.Value* = mlirlite.build_inttoptr(builder, pool_head, frame_ptr_ty) - if hit_frame is None: return 0 - mlirlite.build_br(builder, join_bb) - # miss: malloc + zero the first 8 bytes (clear stale next from pooled frames) - mlirlite.position_at_end(builder, miss_bb) - raw: mlirlite.Value* = mlirlite.build_call(builder, "malloc", size_val, 1, i8_ptr_ty, 0) - if raw is None: return 0 - miss_frame: mlirlite.Value* = mlirlite.build_bitcast(builder, raw, frame_ptr_ty) - if miss_frame is None: return 0 - miss_head_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, miss_frame, i64_ptr_ty) - if miss_head_ptr is not None: mlirlite.build_store(builder, zero64, miss_head_ptr) - mlirlite.build_br(builder, join_bb) - # join: phi-select the frame + zero the 5 header fields + store params into slots - mlirlite.position_at_end(builder, join_bb) - inc1: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, hit_frame, hit_bb) - inc2: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, miss_frame, miss_bb) - if inc1 is None or inc2 is None: return 0 - inc1.Next = inc2 - frame_ref: mlirlite.Value* = mlirlite.build_phi(builder, frame_ptr_ty, inc1, 2) - if frame_ref is None: return 0 - zero: mlirlite.Value* = mlirlite.const_int32(pool, 0) - null_p: mlirlite.Value* = mlirlite.ConstNull(pool, i8_ptr_ty, "null") - si: int = 0 - for si in range(5): - if si == 4: - mslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i8_ptr_ty, frame_ref, 4) - if mslot is not None: mlirlite.build_store(builder, null_p, mslot) - else: - slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ref, si) - if slot is not None: mlirlite.build_store(builder, zero, slot) - # store params into frame slots (index 5+i) - pj: int = 0 - for pj in range(int(nparams)): - pslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ref, 5 + pj) - if pslot is not None: - arnb: str = pool.alloc(16) - if arnb is not None: viperlib.snprintf(arnb, 16, "%%arg%d", pj) - arg_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i32_ty, arnb) - if arg_ref is not None: mlirlite.build_store(builder, arg_ref, pslot) - mlirlite.build_ret(builder, frame_ref) - return 0 - - # ============================================================ - # M7: per-frame-type global free-list pool head (i64, 0 = empty) - def _get_coro_pool(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str) -> mlirlite.GlobalVariable*: - """Get or create the coro_pool_ global (i64 pool head)""" - pn_buf: str = pool.alloc(128) - if pn_buf is not None: viperlib.snprintf(pn_buf, 128, "coro_pool_%s", name) - # idempotent: create/destroy both call this, so scan the module global list (no find_global API) - g: mlirlite.GlobalVariable* = mod.GlobalHead - while g is not None: - if g.Name is not None and string.strcmp(g.Name, pn_buf) == 0: return g - g = g.Next - return mlirlite.create_global_int(pool, mod, pn_buf, 64, 0) - - def _register_coro_wake(self, trans: HT.Translator*, name: str, wake_idx: int): - """Record the wake-slot index for an async name""" - if trans is None or name is None or trans._coro_wake_names is None: return - cnt: int = trans._coro_wake_count - if cnt >= HT.MAX_CORO_FUNCS: return - addr: uint64 = uint64(trans._coro_wake_names) + cnt * 8 - slot: str* = (str*)(addr) - slot[0] = name - trans._coro_wake_idxs[cnt] = wake_idx - trans._coro_wake_count = cnt + 1 - - def _lookup_coro_wake(self, trans: HT.Translator*, name: str) -> int: - """Return the wake-slot index for an async name, or -1 if none""" - if trans is None or name is None or trans._coro_wake_names is None: return -1 - i: int = 0 - for i in range(int(trans._coro_wake_count)): - addr: uint64 = uint64(trans._coro_wake_names) + i * 8 - slot: str* = (str*)(addr) - if slot[0] is not None and string.strcmp(slot[0], name) == 0: return trans._coro_wake_idxs[i] - return -1 - - # ============================================================ - # resume_(%Coro_*) -> i32 state machine (static segment splitting) - def _emit_resume(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str, fd: ast.FunctionDef*, frame_ty: mlirlite.LLVMType*, nparams: int, nlocals: int, nextra: int) -> int: - """Emit resume_(frame) -> i32 state machine returning done (0=SUSPEND, 1=DONE)""" - if frame_ty is None: return 0 - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) - - rname_buf: str = pool.alloc(64) - if rname_buf is not None: viperlib.snprintf(rname_buf, 64, "resume_%s", name) - mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rname_buf, 0) - func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled, i32_ty) - if func is None: return 0 - mlirlite.add_param(pool, func, frame_ptr_ty, "%frame") - funcs_ptr: GenTypes.FuncEntry* = trans._funcs - cur_count: int = trans._func_count - if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, cur_count, rname_buf, func, 1024, None, 0, 1, trans.Pool) == 0: - trans._func_count = cur_count + 1 - - entry: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") - if entry is None: return 0 - builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) - if builder is None: return 0 - mlirlite.position_at_end(builder, entry) - - frame_arg: mlirlite.Value* = mlirlite.SSAValue(pool, frame_ptr_ty, "%frame") - if frame_arg is None: return 0 - - # GEP the 5 header slots (M4 exc / M7 msg) - i8_ptr_ty_r: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - i64_ty_r: mlirlite.LLVMType* = mlirlite.Int64(pool) - state_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 0) - done_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 1) - ret_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 2) - exc_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 3) - msg_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i8_ptr_ty_r, frame_arg, 4) - - # GEP param slots -> register in the symbol table (index 5+i) - pj: int = 0 - for pj in range(int(nparams)): - pname: str = self._read_param_name(trans, pj) - if pname is None: continue - pslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 5 + pj) - if pslot is not None: self.Gen.VarGen.define_var(trans.SymTab, pname, pslot) - - # GEP local slots -> register in the symbol table - li: int = 0 - for li in range(int(nlocals)): - lname: str = self._read_local_name(trans, li) - if lname is None: continue - slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, trans._coro_local_base + li) - if slot is not None: self.Gen.VarGen.define_var(trans.SymTab, lname, slot) - - # M3-M7: register extra slots (wake i64 / __f_idx/__f_len/__await i32). - # Name reverse lookup: wake=uses_sleep && ei==0; for=_coro_for_slots; else __await_M. - ei: int = 0 - for ei in range(int(nextra)): - ety: int = 1 - if trans._coro_extra_types is not None: ety = trans._coro_extra_types[ei] - ety_llvm: mlirlite.LLVMType* = i32_ty - if ety == 2: ety_llvm = i64_ty_r - eslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, ety_llvm, frame_arg, trans._coro_extra_base + ei) - if eslot is None: continue - # wake slot: store the pointer only, no SymTab entry (used by coro_run polling) - if trans._coro_uses_sleep != 0 and ei == 0: - trans._coro_wake_ptr = eslot - continue - ename: str = pool.alloc(16) - # for slot: _coro_for_slots[N] is __f_idx_N, +1 is __f_len_N - is_for: int = 0 - is_len: int = 0 - kf: int = 0 - for kf in range(int(trans._coro_scan_for_count)): - if trans._coro_for_slots[kf] == ei: - is_for = 1 - break - if trans._coro_for_slots[kf] + 1 == ei: - is_for = 1 - is_len = 1 - break - if is_for != 0: - if ename is not None: - if is_len != 0: viperlib.snprintf(ename, 16, "__f_len_%d", kf) - else: viperlib.snprintf(ename, 16, "__f_idx_%d", kf) - else: - # __await_M: reverse lookup the position array - found: int = 0 - km: int = 0 - for km in range(int(nextra)): - if trans._coro_await_slots[km] == ei: - found = 1 - break - if found == 0: continue - if ename is not None: viperlib.snprintf(ename, 16, "__await_%d", km) - if ename is not None: self.Gen.VarGen.define_var(trans.SymTab, ename, eslot) - - # state value + exit block - state_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, state_ptr) - exit_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.exit") - if exit_bb is None: return 0 - - # save resume context (shared by await suspension / resume blocks) - trans._coro_state_ptr = state_ptr - trans._coro_done_ptr = done_ptr - trans._coro_ret_ptr = ret_ptr - trans._coro_exc_ptr = exc_ptr - trans._coro_msg_ptr = msg_ptr - trans._coro_wake_ptr = None - trans._coro_exit_bb = exit_bb - trans._coro_frame = frame_arg - trans._coro_await_slot = None - # state0 is the entry; await states start at 1 (same DFS order as pre-scan) - trans._coro_next_state = 1 - # translation-time indices (same order as pre-scan) - trans._coro_hoist_count = 0 - trans._coro_for_counter = 0 - trans._coro_try_stack_count = 0 - trans._coro_break_bb = None - trans._coro_continue_bb = None - - nseg: int = trans._coro_nseg - if nseg <= 0: nseg = 1 - # pre-build state blocks + switch cases (one-shot; resume blocks are positioned later) - state_bbs: sizet* = pool.alloc(nseg * 8) - if state_bbs is None: return 0 - trans._coro_state_bbs = state_bbs - sw_head: mlirlite.SwitchCase* = None - sw_tail: mlirlite.SwitchCase* = None - sw_count: int = 0 - bb_name: str = pool.alloc(32) - si: int = 0 - for si in range(int(nseg)): - if bb_name is not None: viperlib.snprintf(bb_name, 32, "state%d", si) - sbb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, bb_name) - if sbb is None: continue - state_bbs[si] = sizet(sbb) - sc: mlirlite.SwitchCase* = mlirlite.new_switch_case(pool, mlirlite.const_int32(pool, si), sbb) - if sc is not None: - if sw_head is None: - sw_head = sc - sw_tail = sc - else: - sw_tail.Next = sc - sw_tail = sc - sw_count = sw_count + 1 - mlirlite.build_switch(builder, state_val, exit_bb, sw_head, sw_count) - - # ---- state0 entry: recursively translate the whole body (If/While awaits resume dynamically). - # Set trans._cur_builder/_cur_func here; later position_at_end reuse the same builder object. - trans._cur_func = func - trans._cur_builder = builder - # P2 willFree 失效表:协程体是独立帧 → 入口同样复位 - self.Gen.VarGen.freed_clear() - seg_bb: mlirlite.BasicBlock* = mlirlite.BasicBlock*(state_bbs[0]) - mlirlite.position_at_end(builder, seg_bb) - # M3 硬约束锚点:登记"正在翻译其 body 的协程函数",CallGenCtor._translate_new_expr 据此对 - # 协程体内的 `new heap slife` fail-fast(协程体被状态机拆分,释放点无法确定)。 - # 保存/恢复旧值:协程里再定义协程时不会把外层的标记冲掉。 - old_coro_body: mlirlite.Function* = trans._coro_body_func - trans._coro_body_func = func - self._handle_segment(trans, builder, pool, mod, fd.children) - trans._coro_body_func = old_coro_body - # body fell through -> implicit return (ret=0 + done=1 + br exit) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: - zret: mlirlite.Value* = mlirlite.const_int32(pool, 0) - done_one: mlirlite.Value* = mlirlite.const_int32(pool, 1) - mlirlite.build_store(builder, zret, ret_ptr) - mlirlite.build_store(builder, done_one, done_ptr) - mlirlite.build_br(builder, exit_bb) - - # exit block: load done and return (0=SUSPEND 1=DONE) - mlirlite.position_at_end(builder, exit_bb) - done_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, done_ptr) - mlirlite.build_ret(builder, done_val) - return 0 - - # ============================================================ - # M2: recursive coroutine-body translation (Return/top-level await/If/While/nested await/plain) - def _handle_segment(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmts: list[ast.AST*]*): - """Recursively translate coroutine body statements into the current builder block""" - if stmts is None: return - bn: sizet = len(stmts) - i: sizet = 0 - for i in range(int(bn)): - stmt: ast.AST* = stmts.get(i) - if stmt is None: continue - # current block is terminated (return/suspend) -> stop this segment - if mlirlite.builder_cur_block_is_terminated(builder) != 0: return - sk: int = stmt.kind() - if sk == ast.ASTKind.Return: - self._emit_coro_return(builder, pool, mod, trans, stmt) - continue - if sk == ast.ASTKind.If: - self._handle_coro_if(trans, builder, pool, mod, stmt) - continue - if sk == ast.ASTKind.While: - self._handle_coro_while(trans, builder, pool, mod, stmt) - continue - if sk == ast.ASTKind.Raise: - self._handle_coro_raise(builder, pool, mod, trans, stmt) - continue - if sk == ast.ASTKind.Try: - tr_: ast.Try* = ast.Try*(stmt) - has_fin: int = 0 - has_exc: int = 0 - if tr_.finalbody is not None and len(tr_.finalbody) > 0: has_fin = 1 - if tr_.handlers is not None and len(tr_.handlers) > 0: has_exc = 1 - if has_fin != 0 and has_exc == 0: self._handle_coro_try_finally(trans, builder, pool, mod, stmt) - else: self._handle_coro_try(trans, builder, pool, mod, stmt) - continue - if sk == ast.ASTKind.For: - if self._for_deep_await(stmt) != 0: self._handle_coro_for(trans, builder, pool, mod, stmt) - else: - # no await: delegate to the standard For - self.Gen.StmtGen.translate_stmt(trans, stmt) - continue - if sk == ast.ASTKind.Break: - if trans._coro_break_bb is not None: mlirlite.build_br(builder, trans._coro_break_bb) - continue - if sk == ast.ASTKind.Continue: - if trans._coro_continue_bb is not None: mlirlite.build_br(builder, trans._coro_continue_bb) - continue - if sk == ast.ASTKind.Pass: continue - # top-level await: AnnAssign/Assign/Expr value is Await -> suspend - if sk == ast.ASTKind.AnnAssign or sk == ast.ASTKind.Assign or sk == ast.ASTKind.Expr: - if self._is_await_tail(stmt) != 0: - target_name: str = self._get_assign_target_name(stmt) - if target_name is not None: trans._coro_await_slot = self.Gen.VarGen.lookup_var(trans.SymTab, target_name) - else: - # Expr(await ...): no target slot (discard return), clear stale slot - trans._coro_await_slot = None - aw_node: ast.Await* = self._get_await_value(stmt) - call_name: str = None - if aw_node is not None: call_name = self._extract_await_call_name(aw_node) - if call_name is not None and self.is_async_func(trans, call_name) != 0: self._emit_await(builder, pool, mod, trans, aw_node, call_name) - else: - if call_name is None: call_name = "(unknown)" - err_buf: str = pool.alloc(256) - if err_buf is not None: - viperlib.snprintf(err_buf, 256, "[coro-M2] await target '%s' is not an async function", call_name) - self.Gen.TypeGenErr.fatal_error(stmt, err_buf, "SyntaxError") - continue - # nested await in the statement -> hoist to __await_N - if self._stmt_has_await(stmt) != 0: - self._handle_nested_await_stmt(trans, builder, pool, mod, stmt) - continue - # other statements -> standard translation - self.Gen.StmtGen.translate_stmt(trans, stmt) - - # ============================================================ - # M2: coroutine If (awaits inside branches suspend/resume in place) - def _handle_coro_if(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): - ifn: ast.If* = ast.If*(stmt) - if ifn is None or ifn.test is None: return - # cif/celif (internal c.@CIf marker): compile-time prune, then translate only the taken branch - # so awaits inside it still suspend in place. The marker must never be lowered as a runtime value. - if self.Gen.StmtGenCif._is_cif_call(ifn.test) == 1: - if self.Gen.StmtGenCif._prune_cif_test(pool, trans, ifn.test) != 0: - self._handle_segment(trans, builder, pool, mod, ifn.children) - else: - self._handle_segment(trans, builder, pool, mod, ifn.orelse) - return - if self._find_expr_await(ifn.test) is not None: - err_b: str = pool.alloc(256) - if err_b is not None: - viperlib.snprintf(err_b, 256, "[coro-M2] await is not supported in an if condition") - self.Gen.TypeGenErr.fatal_error(stmt, err_b, "SyntaxError") - return - cond: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ifn.test, None, 0, trans) - if cond is None: return - cond1: mlirlite.Value* = self._normalize_cond(builder, pool, mod, cond, trans) - func: mlirlite.Function* = trans._cur_func - then_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.then") - merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.end") - has_else: int = 0 - if ifn.orelse is not None and len(ifn.orelse) > 0: has_else = 1 - else_bb: mlirlite.BasicBlock* = None - if has_else != 0: else_bb = mlirlite.create_block(pool, func, "coro.else") - if cond1 is None or then_bb is None or merge_bb is None: return - mlirlite.build_cond_br(builder, cond1, then_bb, else_bb if else_bb is not None else merge_bb) - # then branch - mlirlite.position_at_end(builder, then_bb) - self._handle_segment(trans, builder, pool, mod, stmt.children) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) - # else branch - if else_bb is not None: - mlirlite.position_at_end(builder, else_bb) - self._handle_segment(trans, builder, pool, mod, ifn.orelse) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) - mlirlite.position_at_end(builder, merge_bb) - - # ============================================================ - # M2: coroutine While (condition block + body; awaits in the body suspend/resume in place) - def _handle_coro_while(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): - whn: ast.While* = ast.While*(stmt) - if whn is None: return - if whn.test is not None and self._find_expr_await(whn.test) is not None: - err_c: str = pool.alloc(256) - if err_c is not None: - viperlib.snprintf(err_c, 256, "[coro-M2] await is not supported in a while condition") - self.Gen.TypeGenErr.fatal_error(stmt, err_c, "SyntaxError") - return - func: mlirlite.Function* = trans._cur_func - cond_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wcond") - body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wbody") - after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wend") - if cond_bb is None or body_bb is None or after_bb is None: return - # break/continue targets (M2, single level) - prev_break: mlirlite.BasicBlock* = trans._coro_break_bb - prev_cont: mlirlite.BasicBlock* = trans._coro_continue_bb - trans._coro_break_bb = after_bb - trans._coro_continue_bb = cond_bb - mlirlite.build_br(builder, cond_bb) - # condition block - mlirlite.position_at_end(builder, cond_bb) - cond: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, whn.test, None, 0, trans) - if cond is None: cond = mlirlite.const_int32(pool, 0) - cond1: mlirlite.Value* = self._normalize_cond(builder, pool, mod, cond, trans) - mlirlite.build_cond_br(builder, cond1, body_bb, after_bb) - # loop body - mlirlite.position_at_end(builder, body_bb) - self._handle_segment(trans, builder, pool, mod, stmt.children) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, cond_bb) - trans._coro_break_bb = prev_break - trans._coro_continue_bb = prev_cont - # loop exit block - mlirlite.position_at_end(builder, after_bb) - - # ============================================================ - # M2: nested await hoisting: a = b + await f() -> __await_N = await f(); a = b + __await_N - def _handle_nested_await_stmt(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): - """Hoist all nested awaits in a statement to top-level temporaries in DFS order""" - guard: int = 0 - while True: - aw_node: ast.Await* = self._extract_first_await(stmt) - if aw_node is None: break - if guard >= HT.MAX_CORO_VARS: break - guard = guard + 1 - temp_name: str = pool.alloc(32) - if temp_name is not None: viperlib.snprintf(temp_name, 32, "__await_%d", trans._coro_hoist_count) - # target slot = __await_N extra slot (pre-scan positions, wake/for slots precede) - slot_idx: int = trans._coro_extra_base + trans._coro_await_slots[trans._coro_hoist_count] - trans._coro_hoist_count = trans._coro_hoist_count + 1 - slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, self._lookup_coro_frame_by_trans(trans), mlirlite.Int32(pool), trans._coro_frame, slot_idx) - trans._coro_await_slot = slot - call_name: str = self._extract_await_call_name(aw_node) - if call_name is None: break - self._emit_await(builder, pool, mod, trans, aw_node, call_name) - self._replace_await_with_name(pool, stmt, temp_name) - # hoisting done -> standard translation (resume block continues the rest of the expression) - self.Gen.StmtGen.translate_stmt(trans, stmt) - - # ============================================================ - # Nested-await detection / extraction / replacement (DFS order matches pre-scan) - def _stmt_has_await(self, stmt: ast.AST*) -> int: - aw: ast.Await* = self._extract_first_await(stmt) - if aw is not None: return 1 - return 0 - - def _extract_first_await(self, stmt: ast.AST*) -> ast.Await*: - """Return the first Await node in a statement (DFS field order), or None""" - if stmt is None: return None - sk: int = stmt.kind() - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - return self._find_expr_await(aa.value) - if sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - return self._find_expr_await(as_.value) - if sk == ast.ASTKind.Expr: - e: ast.Expr* = ast.Expr*(stmt) - return self._find_expr_await(e.value) - if sk == ast.ASTKind.Return: - rv: ast.Return* = ast.Return*(stmt) - return self._find_expr_await(rv.value) - return None - - def _find_expr_await(self, node: ast.AST*) -> ast.Await*: - """DFS the expression tree for the first Await""" - if node is None: return None - k: int = node.kind() - if k == ast.ASTKind.Await: return ast.Await*(node) - if k == ast.ASTKind.BinOp: - b: ast.BinOp* = ast.BinOp*(node) - r: ast.Await* = self._find_expr_await(b.left) - if r is not None: return r - return self._find_expr_await(b.right) - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - if cl.func is not None: - r2: ast.Await* = self._find_expr_await(cl.func) - if r2 is not None: return r2 - if cl.args is not None: - nb: sizet = len(cl.args) - j: sizet = 0 - for j in range(int(nb)): - r3: ast.Await* = self._find_expr_await(cl.args.get(j)) - if r3 is not None: return r3 - return None - if k == ast.ASTKind.UnaryOp: - u: ast.UnaryOp* = ast.UnaryOp*(node) - return self._find_expr_await(u.operand) - if k == ast.ASTKind.Compare: - cm: ast.Compare* = ast.Compare*(node) - r4: ast.Await* = self._find_expr_await(cm.left) - if r4 is not None: return r4 - if cm.comparators is not None: - nb2: sizet = len(cm.comparators) - j2: sizet = 0 - for j2 in range(int(nb2)): - r5: ast.Await* = self._find_expr_await(cm.comparators.get(j2)) - if r5 is not None: return r5 - return None - if k == ast.ASTKind.BoolOp: - bo: ast.BoolOp* = ast.BoolOp*(node) - if bo.values is not None: - nb3: sizet = len(bo.values) - j3: sizet = 0 - for j3 in range(int(nb3)): - r6: ast.Await* = self._find_expr_await(bo.values.get(j3)) - if r6 is not None: return r6 - return None - if k == ast.ASTKind.IfExp: - ie: ast.IfExp* = ast.IfExp*(node) - r7: ast.Await* = self._find_expr_await(ie.test) - if r7 is not None: return r7 - r8: ast.Await* = self._find_expr_await(ie.body) - if r8 is not None: return r8 - return self._find_expr_await(ie.orelse) - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(node) - return self._find_expr_await(at.value) - if k == ast.ASTKind.Subscript: - sb: ast.Subscript* = ast.Subscript*(node) - r9: ast.Await* = self._find_expr_await(sb.value) - if r9 is not None: return r9 - return self._find_expr_await(sb.slice) - return None - - def _replace_await_with_name(self, pool: memhub.MemBuddy*, stmt: ast.AST*, temp_name: str) -> int: - """Replace the first Await in a statement tree with Name(temp_name) in DFS order""" - if stmt is None or temp_name is None: return 0 - sk: int = stmt.kind() - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - if aa.value is not None: return self._replace_expr_await(pool, aa.value, temp_name) - return 0 - if sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - if as_.value is not None: return self._replace_expr_await(pool, as_.value, temp_name) - return 0 - if sk == ast.ASTKind.Expr: - e: ast.Expr* = ast.Expr*(stmt) - if e.value is not None: return self._replace_expr_await(pool, e.value, temp_name) - return 0 - if sk == ast.ASTKind.Return: - rv: ast.Return* = ast.Return*(stmt) - if rv.value is not None: return self._replace_expr_await(pool, rv.value, temp_name) - return 0 - return 0 - - def _replace_expr_await(self, pool: memhub.MemBuddy*, node: ast.AST*, temp_name: str) -> int: - """Replace the first Await in an expression with Name(temp_name); returns 1/0/2""" - if node is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Await: return 2 - if k == ast.ASTKind.BinOp: - b: ast.BinOp* = ast.BinOp*(node) - r: int = self._replace_expr_await(pool, b.left, temp_name) - if r == 1: return 1 - if r == 2: - b.left = ast.Name(pool, temp_name, 0) - return 1 - r2: int = self._replace_expr_await(pool, b.right, temp_name) - if r2 == 1: return 1 - if r2 == 2: - b.right = ast.Name(pool, temp_name, 0) - return 1 - return 0 - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - if cl.func is not None: - r3: int = self._replace_expr_await(pool, cl.func, temp_name) - if r3 == 1: return 1 - if r3 == 2: - cl.func = ast.Name(pool, temp_name, 0) - return 1 - if cl.args is not None: - nb: sizet = len(cl.args) - j: sizet = 0 - for j in range(int(nb)): - r4: int = self._replace_expr_await(pool, cl.args.get(j), temp_name) - if r4 == 1: return 1 - if r4 == 2: - cl.args.set(j, ast.Name(pool, temp_name, 0)) - return 1 - return 0 - if k == ast.ASTKind.UnaryOp: - u: ast.UnaryOp* = ast.UnaryOp*(node) - r5: int = self._replace_expr_await(pool, u.operand, temp_name) - if r5 == 1: return 1 - if r5 == 2: - u.operand = ast.Name(pool, temp_name, 0) - return 1 - return 0 - if k == ast.ASTKind.Compare: - cm: ast.Compare* = ast.Compare*(node) - r6: int = self._replace_expr_await(pool, cm.left, temp_name) - if r6 == 1: return 1 - if r6 == 2: - cm.left = ast.Name(pool, temp_name, 0) - return 1 - if cm.comparators is not None: - nb2: sizet = len(cm.comparators) - j2: sizet = 0 - for j2 in range(int(nb2)): - r7: int = self._replace_expr_await(pool, cm.comparators.get(j2), temp_name) - if r7 == 1: return 1 - if r7 == 2: - cm.comparators.set(j2, ast.Name(pool, temp_name, 0)) - return 1 - return 0 - if k == ast.ASTKind.BoolOp: - bo: ast.BoolOp* = ast.BoolOp*(node) - if bo.values is not None: - nb3: sizet = len(bo.values) - j3: sizet = 0 - for j3 in range(int(nb3)): - r8: int = self._replace_expr_await(pool, bo.values.get(j3), temp_name) - if r8 == 1: return 1 - if r8 == 2: - bo.values.set(j3, ast.Name(pool, temp_name, 0)) - return 1 - return 0 - if k == ast.ASTKind.IfExp: - ie: ast.IfExp* = ast.IfExp*(node) - r9: int = self._replace_expr_await(pool, ie.test, temp_name) - if r9 == 1: return 1 - if r9 == 2: - ie.test = ast.Name(pool, temp_name, 0) - return 1 - r10: int = self._replace_expr_await(pool, ie.body, temp_name) - if r10 == 1: return 1 - if r10 == 2: - ie.body = ast.Name(pool, temp_name, 0) - return 1 - r11: int = self._replace_expr_await(pool, ie.orelse, temp_name) - if r11 == 1: return 1 - if r11 == 2: - ie.orelse = ast.Name(pool, temp_name, 0) - return 1 - return 0 - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(node) - r12: int = self._replace_expr_await(pool, at.value, temp_name) - if r12 == 1: return 1 - if r12 == 2: - at.value = ast.Name(pool, temp_name, 0) - return 1 - return 0 - if k == ast.ASTKind.Subscript: - sb: ast.Subscript* = ast.Subscript*(node) - r13: int = self._replace_expr_await(pool, sb.value, temp_name) - if r13 == 1: return 1 - if r13 == 2: - sb.value = ast.Name(pool, temp_name, 0) - return 1 - r14: int = self._replace_expr_await(pool, sb.slice, temp_name) - if r14 == 1: return 1 - if r14 == 2: - sb.slice = ast.Name(pool, temp_name, 0) - return 1 - return 0 - return 0 - - # ============================================================ - # Condition normalization (i1 passes through; otherwise icmp ne 0) - def _normalize_cond(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cond: mlirlite.Value*, trans: HT.Translator*) -> mlirlite.Value*: - """Normalize a coroutine condition to i1, preferring __bool__ dispatch""" - return self.Gen.ExprGenOps.coerce_cond_to_bool(pool, builder, mod, cond, trans) - - # ============================================================ - # Parameter-name read (from the _coro_params table) - def _read_param_name(self, trans: HT.Translator*, idx: int) -> str: - if trans is None or trans._coro_params is None: return None - addr: uint64 = uint64(trans._coro_params) + idx * 8 - slot: str* = (str*)(addr) - return slot[0] - - # ============================================================ - # Resume-block lookup (state{N} from the pre-built array) - def _get_state_bb(self, trans: HT.Translator*, state_no: int) -> mlirlite.BasicBlock*: - if trans is None or trans._coro_state_bbs is None: return None - if state_no < 0 or state_no >= trans._coro_nseg: return None - # state_bbs holds block pointers (sizet): index it, never take an element address, - # or position lands on an invalid block and the IR chain corrupts (RenumberSSA crash). - return mlirlite.BasicBlock*(trans._coro_state_bbs[state_no]) - - def _lookup_coro_frame_by_trans(self, trans: HT.Translator*) -> mlirlite.LLVMType*: - """Return the frame type of the current coroutine, looked up from the registered frames""" - # resume GEP needs the frame type: take the Pointee of _coro_frame.Ty (Ptr(frame)) - if trans is None or trans._coro_frame is None: return None - ft: mlirlite.LLVMType* = trans._coro_frame.Ty - if ft is None: return None - match ft: - case LLVMType.Ptr(): - return ft.Pointee - case _: - return None - - # ============================================================ - # M3: await coro_sleep(ms) - suspend via the wake slot (coro_run polls to resume) - def _emit_await_sleep(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, node: ast.AST*) -> mlirlite.Value*: - """Emit await coro_sleep(ms): store the wake deadline in the frame and suspend the parent""" - i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - if node is None or trans is None or trans._coro_wake_ptr is None: return mlirlite.const_int32(pool, 0) - ms_val: mlirlite.Value* = mlirlite.const_int64(pool, 0) - aw_s: ast.Await* = ast.Await*(node) - if aw_s.value is not None and aw_s.value.kind() == ast.ASTKind.Call: - cl_s: ast.Call* = ast.Call*(aw_s.value) - if cl_s.args is not None and len(cl_s.args) > 0: - ms_v: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl_s.args.get(0), None, 0, trans) - if ms_v is not None: ms_val = ms_v - # GetTickCount64() + ms(i64) - now_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, "GetTickCount64") - if now_fn is None: now_fn = mlirlite.create_declare(pool, mod, "GetTickCount64", i64_ty) - if now_fn is None: return mlirlite.const_int32(pool, 0) - now_val: mlirlite.Value* = mlirlite.build_call(builder, "GetTickCount64", None, 0, i64_ty, 0) - if now_val is None: return mlirlite.const_int32(pool, 0) - wake_val: mlirlite.Value* = mlirlite.build_add(builder, now_val, ms_val) - if wake_val is None: return mlirlite.const_int32(pool, 0) - mlirlite.build_store(builder, wake_val, trans._coro_wake_ptr) - # parent suspend: new state id + br exit + position the resume block - new_state_s: int = trans._coro_next_state - trans._coro_next_state = new_state_s + 1 - st_v_s: mlirlite.Value* = mlirlite.const_int32(pool, new_state_s) - mlirlite.build_store(builder, st_v_s, trans._coro_state_ptr) - mlirlite.build_br(builder, trans._coro_exit_bb) - resume_bb_s: mlirlite.BasicBlock* = self._get_state_bb(trans, new_state_s) - if resume_bb_s is not None: mlirlite.position_at_end(builder, resume_bb_s) - return mlirlite.const_int32(pool, 0) - - # ============================================================ - # M4-M6: raise inside a coroutine (exception type/value -> frame exc slot) - def _handle_coro_raise(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, stmt: ast.AST*): - """Handle raise inside a coroutine: store the exception code/message, then jump to the handler or exit""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - exc_code: mlirlite.Value* = mlirlite.const_int32(pool, 1) - rz: ast.Raise* = ast.Raise*(stmt) - if rz is not None and rz.exc is not None: - code: int = self._resolve_raise_code(pool, rz.exc) - if code != 0: - exc_code = mlirlite.const_int32(pool, code) - # M7: exception message (first arg of Exception("msg")) - msg_v: mlirlite.Value* = self._resolve_raise_msg(builder, pool, mod, trans, rz.exc) - if msg_v is not None and trans._coro_msg_ptr is not None: mlirlite.build_store(builder, msg_v, trans._coro_msg_ptr) - else: - v: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rz.exc, None, 0, trans) - if v is not None: exc_code = v - if trans._coro_exc_ptr is not None: mlirlite.build_store(builder, exc_code, trans._coro_exc_ptr) - # jump to try/except or terminate - if trans._coro_try_stack_count > 0: - tbb_r: mlirlite.BasicBlock* = mlirlite.BasicBlock*(trans._coro_try_stack[trans._coro_try_stack_count - 1]) - mlirlite.build_br(builder, tbb_r) - else: - done_r: mlirlite.Value* = mlirlite.const_int32(pool, 1) - mlirlite.build_store(builder, done_r, trans._coro_done_ptr) - mlirlite.build_br(builder, trans._coro_exit_bb) - - def _resolve_raise_code(self, pool: memhub.MemBuddy*, exc_node: ast.AST*) -> int: - """Resolve the exception code of a raise target; return 0 for non-exception classes""" - if exc_node is None: return 0 - name: str = None - if exc_node.kind() == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(exc_node) - name = nm.id - elif exc_node.kind() == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(exc_node) - if cl.func is not None and cl.func.kind() == ast.ASTKind.Name: - nm2: ast.Name* = ast.Name*(cl.func) - name = nm2.id - elif exc_node.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(exc_node) - name = at.attr - if name is None: return 0 - return self.Gen.ClassGenBase.lookup_exception_code(name) - - def _resolve_raise_msg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, exc_node: ast.AST*) -> mlirlite.Value*: - """Return the exception message expression (first arg of Exception), or None""" - if exc_node is None or exc_node.kind() != ast.ASTKind.Call: return None - cl: ast.Call* = ast.Call*(exc_node) - if cl.args is None or len(cl.args) < 1: return None - return self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) - - # ============================================================ - # M5-M6: coroutine try/except (chained multi-except match + type codes + `as e` binding) - def _handle_coro_try(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): - """Handle try/except inside a coroutine: match handlers in order and propagate unmatched exceptions""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - tr: ast.Try* = ast.Try*(stmt) - if tr is None: return - nhandlers: sizet = 0 - if tr.handlers is not None: nhandlers = len(tr.handlers) - if nhandlers == 0: - err_t: str = pool.alloc(256) - if err_t is not None: - viperlib.snprintf(err_t, 256, "[coro-M6] try inside a coroutine requires an except clause") - self.Gen.TypeGenErr.fatal_error(stmt, err_t, "SyntaxError") - return - func: mlirlite.Function* = trans._cur_func - except_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.try_except") - merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.try_merge") - if except_bb is None or merge_bb is None: return - # push the try stack - if trans._coro_try_stack_count < HT.MAX_CORO_TRY: - trans._coro_try_stack[trans._coro_try_stack_count] = sizet(except_bb) - trans._coro_try_stack_count = trans._coro_try_stack_count + 1 - # try body - self._handle_segment(trans, builder, pool, mod, stmt.children) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) - if trans._coro_try_stack_count > 0: trans._coro_try_stack_count = trans._coro_try_stack_count - 1 - # ---- dispatch: match the exc code against each except in order ---- - mlirlite.position_at_end(builder, except_bb) - exc_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, trans._coro_exc_ptr) - # pre-build check/handler/unmatched blocks - check_bbs: sizet* = pool.alloc((nhandlers + 1) * 8) - handler_bbs: sizet* = pool.alloc(nhandlers * 8) - if check_bbs is None or handler_bbs is None: return - check_bbs[0] = sizet(except_bb) - hi: int = 0 - for hi in range(int(nhandlers)): - # [FIX 2026-08-31] build block names with snprintf: "%" is not string formatting here, - # so the old "%d" form produced i32 garbage and triggered a false int->ptr cast error. - chk_name: str = pool.alloc(64) - exc_name: str = pool.alloc(64) - if chk_name is None or exc_name is None: return - viperlib.snprintf(chk_name, 64, "coro.exc_check_%d", hi) - viperlib.snprintf(exc_name, 64, "coro.except_%d", hi) - check_bbs[hi + 1] = sizet(mlirlite.create_block(pool, func, chk_name)) - handler_bbs[hi] = sizet(mlirlite.create_block(pool, func, exc_name)) - unmatched_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.try_reraise") - codes_buf: int* = pool.alloc(256) - # per handler: the check chain - for hi in range(int(nhandlers)): - h_node: ast.AST* = tr.handlers.get(hi) - check_bb: mlirlite.BasicBlock* = (mlirlite.BasicBlock*)(check_bbs[hi]) - mlirlite.position_at_end(builder, check_bb) - catch_all: int = 0 - ncodes: int = 0 - if h_node is not None: - hd: ast.ExceptHandler* = ast.ExceptHandler*(h_node) - if hd.type is None: catch_all = 1 - else: - ename: str = self._exception_type_name(pool, hd.type) - if ename is not None and string.strcmp(ename, "Exception") == 0: catch_all = 1 - else: ncodes = self.Gen.ClassGenBase.collect_descendant_codes(ename, codes_buf, 64) - next_check: mlirlite.BasicBlock* = None - if hi + 1 < nhandlers: next_check = mlirlite.BasicBlock*(check_bbs[hi + 1]) - else: next_check = unmatched_bb - handler_bb: mlirlite.BasicBlock* = (mlirlite.BasicBlock*)(handler_bbs[hi]) - if catch_all != 0: mlirlite.build_br(builder, handler_bb) - else: - match_cond: mlirlite.Value* = None - ci: int = 0 - for ci in range(int(ncodes)): - eq_v: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, exc_val, mlirlite.const_int32(pool, codes_buf[ci])) - if match_cond is None: match_cond = eq_v - else: match_cond = mlirlite.build_or(builder, match_cond, eq_v) - if match_cond is None: mlirlite.build_br(builder, next_check) - else: mlirlite.build_cond_br(builder, match_cond, handler_bb, next_check) - # no match: keep propagating (terminate the coroutine again) - mlirlite.position_at_end(builder, unmatched_bb) - done_u: mlirlite.Value* = mlirlite.const_int32(pool, 1) - mlirlite.build_store(builder, done_u, trans._coro_done_ptr) - mlirlite.build_br(builder, trans._coro_exit_bb) - # ---- each except handler ---- - for hi in range(int(nhandlers)): - h_node2: ast.AST* = tr.handlers.get(hi) - handler_bb2: mlirlite.BasicBlock* = (mlirlite.BasicBlock*)(handler_bbs[hi]) - mlirlite.position_at_end(builder, handler_bb2) - if h_node2 is not None: - hd2: ast.ExceptHandler* = ast.ExceptHandler*(h_node2) - # `as e`: bind the exception code to e's slot (a local collected by _collect_local_names) - if hd2.name is not None and trans._coro_exc_ptr is not None: - e_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, hd2.name) - if e_slot is not None: - hv: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, trans._coro_exc_ptr) - mlirlite.build_store(builder, hv, e_slot) - # clear the frame exc after catching - z_clr: mlirlite.Value* = mlirlite.const_int32(pool, 0) - mlirlite.build_store(builder, z_clr, trans._coro_exc_ptr) - self._handle_segment(trans, builder, pool, mod, h_node2.children) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) - mlirlite.position_at_end(builder, merge_bb) - - def _exception_type_name(self, pool: memhub.MemBuddy*, type_node: ast.AST*) -> str: - """Return the except type name (Name/Attribute), or None if unavailable""" - if type_node is None: return None - if type_node.kind() == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(type_node) - return nm.id - if type_node.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(type_node) - return at.attr - return None - - # ============================================================ - # M7: coroutine try/finally (no except) - def _handle_coro_try_finally(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): - """Handle try/finally inside a coroutine: always run fbody, then re-propagate any pending exception""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - tr: ast.Try* = ast.Try*(stmt) - if tr is None: return - if tr.finalbody is not None and self._stmts_deep_await(tr.finalbody) != 0: - err_f: str = pool.alloc(256) - if err_f is not None: - viperlib.snprintf(err_f, 256, "[coro-M7] await is not yet supported inside finally") - self.Gen.TypeGenErr.fatal_error(stmt, err_f, "SyntaxError") - return - func: mlirlite.Function* = trans._cur_func - fin_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fin") - fin_exc_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fin_exc") - merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fin_merge") - if fin_bb is None or fin_exc_bb is None or merge_bb is None: return - # push the try stack (exceptions jump to finally) - if trans._coro_try_stack_count < HT.MAX_CORO_TRY: - trans._coro_try_stack[trans._coro_try_stack_count] = sizet(fin_bb) - trans._coro_try_stack_count = trans._coro_try_stack_count + 1 - self._handle_segment(trans, builder, pool, mod, stmt.children) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, fin_bb) - if trans._coro_try_stack_count > 0: trans._coro_try_stack_count = trans._coro_try_stack_count - 1 - # fin_bb: record the exception flag + run fbody - mlirlite.position_at_end(builder, fin_bb) - was_exc: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, mlirlite.build_load(builder, i32_ty, trans._coro_exc_ptr), mlirlite.const_int32(pool, 0)) - self._handle_segment(trans, builder, pool, mod, tr.finalbody) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_cond_br(builder, was_exc, fin_exc_bb, merge_bb) - # fin_exc: keep propagating the exception after finally - mlirlite.position_at_end(builder, fin_exc_bb) - done_f: mlirlite.Value* = mlirlite.const_int32(pool, 1) - mlirlite.build_store(builder, done_f, trans._coro_done_ptr) - mlirlite.build_br(builder, trans._coro_exit_bb) - mlirlite.position_at_end(builder, merge_bb) - - # ============================================================ - # M3: coroutine range-For (with await) turned into While (__f_idx/__f_len frame slots) - def _handle_coro_for(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): - """Lower a range-for loop with await into a while loop over frame index/length slots""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - if self._is_range_for(stmt) == 0: - err_g: str = pool.alloc(256) - if err_g is not None: - viperlib.snprintf(err_g, 256, "[coro-M3] For inside a coroutine supports only range iteration (including await)") - self.Gen.TypeGenErr.fatal_error(stmt, err_g, "SyntaxError") - return - fn: ast.For* = ast.For*(stmt) - k: int = trans._coro_for_counter - trans._coro_for_counter = k + 1 - idx_idx: int = trans._coro_extra_base + trans._coro_for_slots[k] - len_idx: int = idx_idx + 1 - idx_slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, self._lookup_coro_frame_by_trans(trans), i32_ty, trans._coro_frame, idx_idx) - len_slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, self._lookup_coro_frame_by_trans(trans), i32_ty, trans._coro_frame, len_idx) - if idx_slot is None or len_slot is None: return - # start/end (range 1 or 2 args) - cl: ast.Call* = ast.Call*(fn.iter) - start_val: mlirlite.Value* = None - end_val: mlirlite.Value* = None - nargs: int = int(len(cl.args)) - if nargs == 1: - start_val = mlirlite.const_int32(pool, 0) - end_val = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) - else: - start_val = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) - end_val = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(1), None, 0, trans) - if start_val is None: start_val = mlirlite.const_int32(pool, 0) - if end_val is None: end_val = mlirlite.const_int32(pool, 0) - mlirlite.build_store(builder, start_val, idx_slot) - mlirlite.build_store(builder, end_val, len_slot) - # loop structure - func: mlirlite.Function* = trans._cur_func - cond_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fcond") - body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fbody") - after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fend") - if cond_bb is None or body_bb is None or after_bb is None: return - prev_break: mlirlite.BasicBlock* = trans._coro_break_bb - prev_cont: mlirlite.BasicBlock* = trans._coro_continue_bb - trans._coro_break_bb = after_bb - trans._coro_continue_bb = cond_bb - mlirlite.build_br(builder, cond_bb) - # condition block - mlirlite.position_at_end(builder, cond_bb) - idx_v: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, idx_slot) - len_v: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, len_slot) - cmp_v: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_SLT, idx_v, len_v) - mlirlite.build_cond_br(builder, cmp_v, body_bb, after_bb) - # loop body: target = idx; ; idx += 1 - mlirlite.position_at_end(builder, body_bb) - if fn.target is not None and fn.target.kind() == ast.ASTKind.Name: - tn: ast.Name* = ast.Name*(fn.target) - tgt_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, tn.id) - if tgt_slot is not None: - idx_v2: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, idx_slot) - mlirlite.build_store(builder, idx_v2, tgt_slot) - self._handle_segment(trans, builder, pool, mod, stmt.children) - if mlirlite.builder_cur_block_is_terminated(builder) == 0: - idx_v3: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, idx_slot) - one_v: mlirlite.Value* = mlirlite.const_int32(pool, 1) - nxt_v: mlirlite.Value* = mlirlite.build_add(builder, idx_v3, one_v) - mlirlite.build_store(builder, nxt_v, idx_slot) - mlirlite.build_br(builder, cond_bb) - trans._coro_break_bb = prev_break - trans._coro_continue_bb = prev_cont - mlirlite.position_at_end(builder, after_bb) - - # ============================================================ - # M3/M4/M7: coro.coro_destroy / coro_exception / coro_exception_msg / coro_gather - def translate_coro_destroy(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: - """coro.coro_destroy(frame): return the frame to the free-list pool""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) - if cl is None or trans is None or cl.args is None or len(cl.args) < 1: return mlirlite.const_int32(pool, 0) - async_name: str = self._var_to_async(trans, cl.args.get(0)) - if async_name is None: return mlirlite.const_int32(pool, 0) - frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) - if frame_ty is None: return mlirlite.const_int32(pool, 0) - frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) - frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) - if frame_val is None: return mlirlite.const_int32(pool, 0) - frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) - if frame_ptr is None: return mlirlite.const_int32(pool, 0) - # return to pool: frame's first 8 bytes hold the old head; head = this frame (i64) - pool_gv: mlirlite.GlobalVariable* = self._get_coro_pool(pool, mod, trans, async_name) - # GlobalVariable is not a Value: build a Value view (Ty=Ptr(i64), Name="@coro_pool_") - i64_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i64_ty) - pool_ref_name: str = pool.alloc(96) - if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, 96, "@coro_pool_%s", async_name) - pool_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i64_ptr_ty, pool_ref_name) - pool_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, pool_ref) - node_next: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_ptr, i64_ptr_ty) - mlirlite.build_store(builder, pool_head, node_next) - frame_i64: mlirlite.Value* = mlirlite.build_ptrtoint(builder, frame_ptr, i64_ty) - mlirlite.build_store(builder, frame_i64, pool_ref) - return mlirlite.const_int32(pool, 0) - - def translate_coro_exception(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: - """coro.coro_exception(frame): return the frame exception code (0 = none)""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - if cl is None or trans is None or cl.args is None or len(cl.args) < 1: return mlirlite.const_int32(pool, 0) - async_name: str = self._var_to_async(trans, cl.args.get(0)) - if async_name is None: return mlirlite.const_int32(pool, 0) - frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) - if frame_ty is None: return mlirlite.const_int32(pool, 0) - frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) - frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) - if frame_val is None: return mlirlite.const_int32(pool, 0) - frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) - if frame_ptr is None: return mlirlite.const_int32(pool, 0) - exc_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ptr, 3) - return mlirlite.build_load(builder, i32_ty, exc_ptr) - - def translate_coro_exception_msg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: - """coro.coro_exception_msg(frame): return the frame exception message pointer, or null""" - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - if cl is None or trans is None or cl.args is None or len(cl.args) < 1: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") - async_name: str = self._var_to_async(trans, cl.args.get(0)) - if async_name is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") - frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) - if frame_ty is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") - frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) - frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) - if frame_val is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") - frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) - if frame_ptr is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") - msg_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i8_ptr_ty, frame_ptr, 4) - return mlirlite.build_load(builder, i8_ptr_ty, msg_ptr) - - def translate_coro_gather(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: - """coro.coro_gather(frames...): resume all coroutines to completion and return the finished count""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - if cl is None or trans is None or cl.args is None: return mlirlite.const_int32(pool, 0) - nb: sizet = len(cl.args) - j: sizet = 0 - for j in range(int(nb)): - arg0: ast.AST* = cl.args.get(j) - async_name: str = self._var_to_async(trans, arg0) - if async_name is None: continue - frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) - if frame_ty is None: continue - frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) - frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg0, None, 0, trans) - if frame_val is None: continue - frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) - if frame_ptr is None: continue - rn_buf: str = pool.alloc(64) - if rn_buf is not None: viperlib.snprintf(rn_buf, 64, "resume_%s", async_name) - resume_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rn_buf, 0) - if self.Gen.CallGenBase.find_func_in_module(mod, resume_mangled) is None: continue - func: mlirlite.Function* = trans._cur_func - loop_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "gather.loop") - after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "gather.after") - if loop_bb is None or after_bb is None: continue - mlirlite.build_br(builder, loop_bb) - mlirlite.position_at_end(builder, loop_bb) - frame_ptr.Next = None - status: mlirlite.Value* = mlirlite.build_call(builder, resume_mangled, frame_ptr, 1, i32_ty, 0) - done_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, status, mlirlite.const_int32(pool, 0)) - mlirlite.build_cond_br(builder, done_cond, after_bb, loop_bb) - mlirlite.position_at_end(builder, after_bb) - return mlirlite.const_int32(pool, 0) - - def _var_to_async(self, trans: HT.Translator*, arg0: ast.AST*) -> str: - """Map a variable node to its async function name, or None if not found""" - if trans is None or arg0 is None or arg0.kind() != ast.ASTKind.Name: return None - nm: ast.Name* = ast.Name*(arg0) - if nm.id is None: return None - return self._lookup_coro_var(trans, nm.id) - - # ============================================================ - # await expression translation (shared by segment tail / translate_value Await branch) - def _emit_await(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, node: ast.AST*, call_name: str) -> mlirlite.Value*: - """Inline-translate await async_fn(args...): create the child frame, resume it to DONE, store its result""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i8_ptr_ty_a: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - if node is None or call_name is None or trans is None: return mlirlite.const_int32(pool, 0) - # M3: await coro_sleep(ms) - suspend until the wake time (frame wake slot) - if string.strcmp(call_name, "coro_sleep") == 0: return self._emit_await_sleep(builder, pool, mod, trans, node) - child_frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, call_name) - if child_frame_ty is None: return mlirlite.const_int32(pool, 0) - child_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, child_frame_ty) - - cc_buf: str = pool.alloc(64) - if cc_buf is not None: viperlib.snprintf(cc_buf, 64, "create_%s", call_name) - create_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, cc_buf, 0) - rc_buf: str = pool.alloc(64) - if rc_buf is not None: viperlib.snprintf(rc_buf, 64, "resume_%s", call_name) - resume_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rc_buf, 0) - if self.Gen.CallGenBase.find_func_in_module(mod, create_mangled) is None or self.Gen.CallGenBase.find_func_in_module(mod, resume_mangled) is None: - # async functions must be defined first (no forward refs); silently return 0 - return mlirlite.const_int32(pool, 0) - - # M2: child-call args (await fn(a, b) -> create_fn(a, b)) - args_head: mlirlite.Value* = None - args_tail: mlirlite.Value* = None - args_count: int = 0 - aw_n: ast.Await* = ast.Await*(node) - if aw_n.value is not None and aw_n.value.kind() == ast.ASTKind.Call: - cl_a: ast.Call* = ast.Call*(aw_n.value) - if cl_a.args is not None: - nb: sizet = len(cl_a.args) - j: sizet = 0 - for j in range(int(nb)): - av: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl_a.args.get(j), None, 0, trans) - if av is None: av = mlirlite.const_int32(pool, 0) - av.Next = None - if args_head is None: - args_head = av - args_tail = av - else: - args_tail.Next = av - args_tail = av - args_count = args_count + 1 - - # child create (M2 with args) + resume - child: mlirlite.Value* = mlirlite.build_call(builder, create_mangled, args_head, args_count, child_ptr_ty, 0) - if child is None: return mlirlite.const_int32(pool, 0) - child.Next = None - mlirlite.build_call(builder, resume_mangled, child, 1, i32_ty, 0) - # GEP the child ret slot [0,2] and load - child_ret_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, child_frame_ty, i32_ty, child, 2) - child_ret: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, child_ret_ptr) - # M4: child exception propagation - child exc != 0 -> parent exc/msg + jump try or terminate - child_exc_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, child_frame_ty, i32_ty, child, 3) - child_exc: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, child_exc_ptr) - zero32_x: mlirlite.Value* = mlirlite.const_int32(pool, 0) - exc_ok_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, trans._cur_func, "coro.ok") - exc_bad_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, trans._cur_func, "coro.exc") - if exc_ok_bb is None or exc_bad_bb is None: return mlirlite.const_int32(pool, 0) - exc_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, child_exc, zero32_x) - mlirlite.build_cond_br(builder, exc_cond, exc_ok_bb, exc_bad_bb) - # exc path: parent exc/msg slots + jump to try or terminate - mlirlite.position_at_end(builder, exc_bad_bb) - if trans._coro_exc_ptr is not None: mlirlite.build_store(builder, child_exc, trans._coro_exc_ptr) - if trans._coro_msg_ptr is not None: - child_msg_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, child_frame_ty, i8_ptr_ty_a, child, 4) - child_msg: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty_a, child_msg_ptr) - mlirlite.build_store(builder, child_msg, trans._coro_msg_ptr) - if trans._coro_try_stack_count > 0: - tbb: mlirlite.BasicBlock* = mlirlite.BasicBlock*(trans._coro_try_stack[trans._coro_try_stack_count - 1]) - mlirlite.build_br(builder, tbb) - else: - done_x: mlirlite.Value* = mlirlite.const_int32(pool, 1) - mlirlite.build_store(builder, done_x, trans._coro_done_ptr) - mlirlite.build_br(builder, trans._coro_exit_bb) - # normal path: store into the parent await slot (if any) + suspend - mlirlite.position_at_end(builder, exc_ok_bb) - slot: mlirlite.Value* = trans._coro_await_slot - if slot is not None: mlirlite.build_store(builder, child_ret, slot) - # parent suspend: new state id (matching the pre-scan DFS order) - new_state: int = trans._coro_next_state - trans._coro_next_state = new_state + 1 - st_v: mlirlite.Value* = mlirlite.const_int32(pool, new_state) - mlirlite.build_store(builder, st_v, trans._coro_state_ptr) - mlirlite.build_br(builder, trans._coro_exit_bb) - # resume block state{new_state}: position immediately (pre-built case; translation continues) - resume_bb: mlirlite.BasicBlock* = self._get_state_bb(trans, new_state) - if resume_bb is not None: mlirlite.position_at_end(builder, resume_bb) - return child_ret - - # ============================================================ - # coro.coro_run(frame) inline translation - def translate_coro_run(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: - """Inline-translate coro.coro_run(frame): resolve the async name, resume to DONE, and return the result""" - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - if cl is None or trans is None: return mlirlite.const_int32(pool, 0) - if cl.args is None or len(cl.args) < 1: return mlirlite.const_int32(pool, 0) - arg0: ast.AST* = cl.args.get(0) - if arg0 is None or arg0.kind() != ast.ASTKind.Name: return mlirlite.const_int32(pool, 0) - nm: ast.Name* = ast.Name*(arg0) - if nm.id is None: return mlirlite.const_int32(pool, 0) - async_name: str = self._lookup_coro_var(trans, nm.id) - if async_name is None: return mlirlite.const_int32(pool, 0) - frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) - if frame_ty is None: return mlirlite.const_int32(pool, 0) - frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) - - # frame variable value (i8* / Ptr(frame)) -> bitcast to Ptr(frame) - frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg0, None, 0, trans) - if frame_val is None: return mlirlite.const_int32(pool, 0) - frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) - if frame_ptr is None: return mlirlite.const_int32(pool, 0) - - func: mlirlite.Function* = trans._cur_func - if func is None: return mlirlite.const_int32(pool, 0) - rn_buf: str = pool.alloc(64) - if rn_buf is not None: viperlib.snprintf(rn_buf, 64, "resume_%s", async_name) - resume_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rn_buf, 0) - if self.Gen.CallGenBase.find_func_in_module(mod, resume_mangled) is None: return mlirlite.const_int32(pool, 0) - - loop_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.loop") - check_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.check") - after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.after") - if loop_bb is None or check_bb is None or after_bb is None: return mlirlite.const_int32(pool, 0) - mlirlite.build_br(builder, loop_bb) - mlirlite.position_at_end(builder, loop_bb) - frame_ptr.Next = None - status: mlirlite.Value* = mlirlite.build_call(builder, resume_mangled, frame_ptr, 1, i32_ty, 0) - zero_cmp: mlirlite.Value* = mlirlite.const_int32(pool, 0) - done_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, status, zero_cmp) - mlirlite.build_cond_br(builder, done_cond, after_bb, check_bb) - # M3: SUSPEND -> wake polling (continue if GetTickCount64 >= wake or wake==0, else Sleep(1)) - mlirlite.position_at_end(builder, check_bb) - wake_idx_r: int = self._lookup_coro_wake(trans, async_name) - if wake_idx_r < 0: mlirlite.build_br(builder, loop_bb) - else: - i64_ty_r2: mlirlite.LLVMType* = mlirlite.Int64(pool) - wake_ptr_r: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i64_ty_r2, frame_ptr, wake_idx_r) - wake_v_r: mlirlite.Value* = mlirlite.build_load(builder, i64_ty_r2, wake_ptr_r) - now_v_r: mlirlite.Value* = mlirlite.build_call(builder, "GetTickCount64", None, 0, i64_ty_r2, 0) - not_wake_r: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, wake_v_r, mlirlite.const_int64(pool, 0)) - due_r: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_UGE, now_v_r, wake_v_r) - cont_r: mlirlite.Value* = mlirlite.build_or(builder, not_wake_r, due_r) - wsleep_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wsleep") - wdue_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wdue") - mlirlite.build_cond_br(builder, cont_r, wdue_bb, wsleep_bb) - mlirlite.position_at_end(builder, wsleep_bb) - sleep_one: mlirlite.Value* = mlirlite.const_int32(pool, 1) - mlirlite.build_call(builder, "Sleep", sleep_one, 1, mlirlite.Void(pool), 0) - mlirlite.build_br(builder, loop_bb) - mlirlite.position_at_end(builder, wdue_bb) - mlirlite.build_br(builder, loop_bb) - mlirlite.position_at_end(builder, after_bb) - ret_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ptr, 2) - ret_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, ret_ptr) - return ret_val - - # ============================================================ - # Public queries / registration - def is_async_func(self, trans: HT.Translator*, name: str) -> int: - """Return whether name is registered as an async function (1 = yes, 0 = no)""" - if trans is None or name is None or trans._async_funcs is None: return 0 - cnt: int = trans._async_func_count - i: int = 0 - for i in range(int(cnt)): - addr: uint64 = uint64(trans._async_funcs) + i * 8 - slot: str* = (str*)(addr) - if slot[0] is not None and string.strcmp(slot[0], name) == 0: return 1 - return 0 - - def register_coro_var(self, trans: HT.Translator*, var_name: str, func_name: str) -> int: - """Record the variable-name to async-function-name mapping used by coro.coro_run""" - if trans is None or var_name is None or func_name is None: return 1 - if trans._coro_var_async is None: return 1 - cnt: int = trans._coro_var_async_count - i: int = 0 - for i in range(int(cnt)): - addr: uint64 = uint64(trans._coro_var_async) + i * 16 - pair: str* = (str*)(addr) - if pair[0] is not None and string.strcmp(pair[0], var_name) == 0: return 0 - if cnt >= HT.MAX_CORO_VARS: return 1 - pair2_addr: uint64 = uint64(trans._coro_var_async) + cnt * 16 - pair2: str* = (str*)(pair2_addr) - pair2[0] = var_name - pair2[1] = func_name - trans._coro_var_async_count = cnt + 1 - return 0 - - def _register_async_func(self, trans: HT.Translator*, name: str) -> int: - """Register an async function bare name in the _async_funcs table""" - if trans is None or name is None or trans._async_funcs is None: return 1 - cnt: int = trans._async_func_count - i: int = 0 - for i in range(int(cnt)): - addr: uint64 = uint64(trans._async_funcs) + i * 8 - slot: str* = (str*)(addr) - if slot[0] is not None and string.strcmp(slot[0], name) == 0: return 0 - if cnt >= HT.MAX_CORO_FUNCS: return 1 - addr2: uint64 = uint64(trans._async_funcs) + cnt * 8 - slot2: str* = (str*)(addr2) - slot2[0] = name - trans._async_func_count = cnt + 1 - return 0 - - def _register_coro_frame(self, trans: HT.Translator*, name: str, frame_ty: mlirlite.LLVMType*) -> int: - """Register an async name to frame type in the _coro_frames table""" - if trans is None or name is None or frame_ty is None: return 1 - if trans._coro_frames is None: return 1 - cnt: int = trans._coro_frame_count - i: int = 0 - for i in range(int(cnt)): - addr: uint64 = uint64(trans._coro_frames) + i * 16 - pair: str* = (str*)(addr) - if pair[0] is not None and string.strcmp(pair[0], name) == 0: - pair[1] = str(frame_ty) - return 0 - if cnt >= HT.MAX_CORO_FUNCS: return 1 - pair2_addr: uint64 = uint64(trans._coro_frames) + cnt * 16 - pair2: str* = (str*)(pair2_addr) - pair2[0] = name - pair2[1] = str(frame_ty) - trans._coro_frame_count = cnt + 1 - return 0 - - def _lookup_coro_frame(self, trans: HT.Translator*, name: str) -> mlirlite.LLVMType*: - """Look up the frame type by async name, or None if not found""" - if trans is None or name is None or trans._coro_frames is None: return None - cnt: int = trans._coro_frame_count - i: int = 0 - for i in range(int(cnt)): - addr: uint64 = uint64(trans._coro_frames) + i * 16 - pair: str* = (str*)(addr) - if pair[0] is not None and string.strcmp(pair[0], name) == 0: - if pair[1] is not None: - ty_addr: sizet = sizet(pair[1]) - return (mlirlite.LLVMType*)(ty_addr) - return None - return None - - def _lookup_coro_var(self, trans: HT.Translator*, var_name: str) -> str: - """Reverse-look-up the async function name by variable name, or None if not found""" - if trans is None or var_name is None or trans._coro_var_async is None: return None - cnt: int = trans._coro_var_async_count - i: int = 0 - for i in range(int(cnt)): - addr: uint64 = uint64(trans._coro_var_async) + i * 16 - pair: str* = (str*)(addr) - if pair[0] is not None and string.strcmp(pair[0], var_name) == 0: return pair[1] - return None - - # ============================================================ - # Segment-translation helpers - def _emit_coro_return(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, stmt: ast.AST*) -> None: - """Special-case Return inside a coroutine: store the return slot, set done, and branch to exit""" - rv_node: ast.Return* = ast.Return*(stmt) - ret_val: mlirlite.Value* = None - if rv_node is not None and rv_node.value is not None: - if rv_node.value.kind() != ast.ASTKind.Await: ret_val = self.Gen.ExprGen.translate_value(builder, pool, mod, rv_node.value, None, 0, trans) - if ret_val is None: ret_val = mlirlite.const_int32(pool, 0) - mlirlite.build_store(builder, ret_val, trans._coro_ret_ptr) - done_one: mlirlite.Value* = mlirlite.const_int32(pool, 1) - mlirlite.build_store(builder, done_one, trans._coro_done_ptr) - mlirlite.build_br(builder, trans._coro_exit_bb) - - def _expr_await_is_sleep(self, aw: ast.Await*) -> int: - """Return whether an Await target is coro_sleep (Name or coro.coro_sleep)""" - if aw is None or aw.value is None: return 0 - if aw.value.kind() != ast.ASTKind.Call: return 0 - cl: ast.Call* = ast.Call*(aw.value) - if cl.func is None: return 0 - if cl.func.kind() == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(cl.func) - if nm.id is not None and string.strcmp(nm.id, "coro_sleep") == 0: return 1 - elif cl.func.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(cl.func) - if at.attr is not None and string.strcmp(at.attr, "coro_sleep") == 0: return 1 - return 0 - - def _is_await_tail(self, stmt: ast.AST*) -> int: - """Return whether a statement is a segment tail, i.e. its value is directly an Await""" - if stmt is None: return 0 - sk: int = stmt.kind() - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - if aa.value is not None and aa.value.kind() == ast.ASTKind.Await: return 1 - elif sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - if as_.value is not None and as_.value.kind() == ast.ASTKind.Await: - if as_.targets is not None and len(as_.targets) == 1: return 1 - elif sk == ast.ASTKind.Expr: - e: ast.Expr* = ast.Expr*(stmt) - if e.value is not None and e.value.kind() == ast.ASTKind.Await: return 1 - return 0 - - def _get_assign_target_name(self, stmt: ast.AST*) -> str: - """Return the target name of an AnnAssign/Assign to a single Name, or None""" - if stmt is None: return None - sk: int = stmt.kind() - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - if aa.target is not None and aa.target.kind() == ast.ASTKind.Name: - tn: ast.Name* = ast.Name*(aa.target) - return tn.id - elif sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - if as_.targets is not None and len(as_.targets) == 1: - t0: ast.AST* = as_.targets.get(0) - if t0 is not None and t0.kind() == ast.ASTKind.Name: - tn2: ast.Name* = ast.Name*(t0) - return tn2.id - return None - - def _get_await_value(self, stmt: ast.AST*) -> ast.Await*: - """Return the Await value node of a segment-tail statement, or None""" - if stmt is None: return None - sk: int = stmt.kind() - if sk == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(stmt) - if aa.value is not None and aa.value.kind() == ast.ASTKind.Await: return ast.Await*(aa.value) - elif sk == ast.ASTKind.Assign: - as_: ast.Assign* = ast.Assign*(stmt) - if as_.value is not None and as_.value.kind() == ast.ASTKind.Await: return ast.Await*(as_.value) - elif sk == ast.ASTKind.Expr: - ex: ast.Expr* = ast.Expr*(stmt) - if ex.value is not None and ex.value.kind() == ast.ASTKind.Await: return ast.Await*(ex.value) - return None - - def _extract_await_call_name(self, aw_node: ast.Await*) -> str: - """Extract the async function name or coro_sleep from an Await call, or None""" - if aw_node is None or aw_node.value is None: return None - if aw_node.value.kind() == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(aw_node.value) - if cl.func is not None: - if cl.func.kind() == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(cl.func) - return nm.id - if cl.func.kind() == ast.ASTKind.Attribute: - # coro.coro_sleep(ms): M3 sleep suspension - at: ast.Attribute* = ast.Attribute*(cl.func) - if at.attr is not None and string.strcmp(at.attr, "coro_sleep") == 0: return "coro_sleep" - return None - - def _read_local_name(self, trans: HT.Translator*, idx: int) -> str: - """Return the idx-th local variable name from trans._coro_locals, or None if out of range""" - if trans is None or trans._coro_locals is None: return None - addr: uint64 = uint64(trans._coro_locals) + idx * 8 - slot: str* = (str*)(addr) - return slot[0] +import lib.core.Gen.GenTypes as GenTypes +import t, c +from stdint import * +import ast +import mlirlite +from mlirlite import LLVMType +import memhub +import string +import viperlib +import stdio +import stdlib +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HT +import lib.core.Gen.Call.CallGenBase as CallGenBase + + +# ============================================================ +# AsyncGenMixin - VPC-M1 coroutine translation (async def / await / coro.coro_run) +define SCOPE_FUNCTION = 1 +novirtual class AsyncGenMixin(CallGenBase.CallGenBase): + """Coroutine translation mixin (VPC-M1 state machine)""" + + # Entry: async function definition. Called by FuncGenDef.translate_function_def after + # detecting fd.flags & FLAG_IS_ASYNC; early-returns so it never enters the normal path. + def translate_async_function(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Translate an async function definition; returns 0 on success""" + fd: ast.FunctionDef* = ast.FunctionDef*(node) + if fd is None or fd.name is None or trans is None: return 0 + name: str = fd.name + pool: memhub.MemBuddy* = trans.Pool + mod: mlirlite.LLVMModule* = trans.Module + if pool is None or mod is None: return 0 + + # avoid duplicate generation: create_ already exists (multi-pass / dup def) + cname_buf: str = pool.alloc(64) + if cname_buf is not None: viperlib.snprintf(cname_buf, 64, "create_%s", name) + create_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, cname_buf, 0) + if self.Gen.CallGenBase.find_func_in_module(mod, create_mangled) is not None: return 0 + + # register the async bare name (await target check / coro_run reverse lookup) + self._register_async_func(trans, name) + + # M2: collect params -> trans._coro_params (char* array, 8 bytes per item) + trans._coro_param_count = 0 + nparams: int = self._collect_param_names(trans, fd) + + # collect locals (top-level AnnAssign/Assign Name targets -> frame slots) + trans._coro_local_count = 0 + nlocals: int = self._collect_local_names(trans, fd) + + # M3-M7: pre-scan extra slots (wake/for/await type tables + position arrays) + state ids. + # _coro_await_count = resume-block count; nseg = 1 entry + await_count. + trans._coro_extra_count = 0 + trans._coro_hoist_count = 0 + trans._coro_await_count = 0 + trans._coro_uses_sleep = 0 + trans._coro_scan_for_count = 0 + trans._coro_for_counter = 0 + self._scan_sleep_usage(trans, fd) + self._pre_scan_awaits(trans, fd) + nextra: int = trans._coro_extra_count + trans._coro_nseg = 1 + trans._coro_await_count + + # frame struct %Coro_ + frame_ty: mlirlite.LLVMType* = self._build_frame_ty(pool, mod, trans, name, nparams, nlocals, nextra) + if frame_ty is None: return 0 + self._register_coro_frame(trans, name, frame_ty) + # M3: register the wake-slot index (coro_run polling; wake is extra slot 0) + if trans._coro_uses_sleep != 0: self._register_coro_wake(trans, name, trans._coro_extra_base + 0) + + # enter function scope so resume slot registration stays isolated from module scope + self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_FUNCTION) + + self._emit_create(pool, mod, trans, name, frame_ty, nparams) + self._emit_resume(pool, mod, trans, name, fd, frame_ty, nparams, nlocals, nextra) + + self.Gen.VarGen.exit_scope(trans.SymTab) + return 0 + + # ============================================================ + # M2: parameter collection (into frame param slots) + def _collect_param_names(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: + """Collect async parameter names into trans._coro_params (8-byte slots); slot index = 3 + i""" + if fd is None or fd.args is None or trans is None: return 0 + if trans._coro_params is None: return 0 + ags: ast.Arguments* = ast.Arguments*(fd.args) + cnt: int = 0 + if ags.args is None: return 0 + bn: sizet = len(ags.args) + for i in range(int(bn)): + a0: ast.AST* = ags.args.get(i) + if a0 is None: continue + aname: str = None + if a0.kind() == ast.ASTKind.Arg: + ar: ast.Arg* = ast.Arg*(a0) + aname = ar.arg + elif a0.kind() == ast.ASTKind.Name: + anm: ast.Name* = ast.Name*(a0) + aname = anm.id + if aname is None: continue + if cnt >= HT.MAX_CORO_VARS: break + addr: uint64 = uint64(trans._coro_params) + cnt * 8 + slot: str* = (str*)(addr) + slot[0] = aname + cnt = cnt + 1 + trans._coro_param_count = cnt + return cnt + + # M3-M7: pre-scan extra slots + state ids in the same DFS order as translation. + # Extra-slot table _coro_extra_types (1=i32, 2=i64); position arrays map slot -> relative index. + def _pre_scan_awaits(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: + """Pre-scan the coroutine body to fill the extra-slot table and return its count""" + if fd is None or fd.children is None or trans is None: return 0 + trans._coro_extra_count = 0 + trans._coro_hoist_count = 0 + trans._coro_await_count = 0 + trans._coro_scan_for_count = 0 + if trans._coro_uses_sleep != 0: + trans._coro_extra_types[0] = 2 # __coro_wake_us (i64) relative index 0 + trans._coro_extra_count = 1 + self._scan_stmts(trans, fd.children) + return trans._coro_extra_count + + # ---------- pass 1: coro_sleep detection (decides the wake slot) ---------- + def _scan_sleep_usage(self, trans: HT.Translator*, fd: ast.FunctionDef*): + if fd is None or fd.children is None or trans is None: return + self._scan_sleep_stmts(trans, fd.children) + + def _scan_sleep_stmts(self, trans: HT.Translator*, stmts: list[ast.AST*]*): + if stmts is None or trans._coro_uses_sleep != 0: return + bn: sizet = len(stmts) + i: sizet = 0 + for i in range(int(bn)): self._scan_sleep_stmt(trans, stmts.get(i)) + + def _scan_sleep_stmt(self, trans: HT.Translator*, stmt: ast.AST*): + if stmt is None or trans._coro_uses_sleep != 0: return + sk: int = stmt.kind() + if sk == ast.ASTKind.If: + ifn: ast.If* = ast.If*(stmt) + self._scan_sleep_expr(trans, ifn.test) + self._scan_sleep_stmts(trans, stmt.children) + self._scan_sleep_stmts(trans, ifn.orelse) + return + if sk == ast.ASTKind.While: + whn: ast.While* = ast.While*(stmt) + self._scan_sleep_expr(trans, whn.test) + self._scan_sleep_stmts(trans, stmt.children) + self._scan_sleep_stmts(trans, whn.orelse) + return + if sk == ast.ASTKind.For: + fn2: ast.For* = ast.For*(stmt) + self._scan_sleep_expr(trans, fn2.iter) + self._scan_sleep_stmts(trans, stmt.children) + self._scan_sleep_stmts(trans, fn2.orelse) + return + if sk == ast.ASTKind.Try: + trn: ast.Try* = ast.Try*(stmt) + self._scan_sleep_stmts(trans, stmt.children) + if trn.handlers is not None: + nh: sizet = len(trn.handlers) + hh: sizet = 0 + for hh in range(int(nh)): + h0: ast.AST* = trn.handlers.get(hh) + if h0 is not None: self._scan_sleep_stmts(trans, h0.children) + self._scan_sleep_stmts(trans, trn.orelse) + self._scan_sleep_stmts(trans, trn.finalbody) + return + if sk == ast.ASTKind.Return: + rv: ast.Return* = ast.Return*(stmt) + self._scan_sleep_expr(trans, rv.value) + return + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + self._scan_sleep_expr(trans, aa.value) + return + if sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + self._scan_sleep_expr(trans, as_.value) + return + if sk == ast.ASTKind.Expr: + e: ast.Expr* = ast.Expr*(stmt) + self._scan_sleep_expr(trans, e.value) + return + + def _scan_sleep_expr(self, trans: HT.Translator*, node: ast.AST*): + if node is None or trans._coro_uses_sleep != 0: return + k: int = node.kind() + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + if cl.func is not None: + if cl.func.kind() == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(cl.func) + if nm.id is not None and string.strcmp(nm.id, "coro_sleep") == 0: + trans._coro_uses_sleep = 1 + return + elif cl.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.attr is not None and string.strcmp(at.attr, "coro_sleep") == 0: + trans._coro_uses_sleep = 1 + return + self._scan_sleep_expr(trans, cl.func) + if cl.args is not None: + nb: sizet = len(cl.args) + j: sizet = 0 + for j in range(int(nb)): self._scan_sleep_expr(trans, cl.args.get(j)) + return + if k == ast.ASTKind.BinOp: + b: ast.BinOp* = ast.BinOp*(node) + self._scan_sleep_expr(trans, b.left) + self._scan_sleep_expr(trans, b.right) + return + if k == ast.ASTKind.UnaryOp: + u: ast.UnaryOp* = ast.UnaryOp*(node) + self._scan_sleep_expr(trans, u.operand) + return + if k == ast.ASTKind.Compare: + cm: ast.Compare* = ast.Compare*(node) + self._scan_sleep_expr(trans, cm.left) + if cm.comparators is not None: + nb2: sizet = len(cm.comparators) + j2: sizet = 0 + for j2 in range(int(nb2)): self._scan_sleep_expr(trans, cm.comparators.get(j2)) + return + if k == ast.ASTKind.BoolOp: + bo: ast.BoolOp* = ast.BoolOp*(node) + if bo.values is not None: + nb3: sizet = len(bo.values) + j3: sizet = 0 + for j3 in range(int(nb3)): self._scan_sleep_expr(trans, bo.values.get(j3)) + return + if k == ast.ASTKind.IfExp: + ie: ast.IfExp* = ast.IfExp*(node) + self._scan_sleep_expr(trans, ie.test) + self._scan_sleep_expr(trans, ie.body) + self._scan_sleep_expr(trans, ie.orelse) + return + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + self._scan_sleep_expr(trans, at.value) + return + if k == ast.ASTKind.Subscript: + sb: ast.Subscript* = ast.Subscript*(node) + self._scan_sleep_expr(trans, sb.value) + self._scan_sleep_expr(trans, sb.slice) + return + if k == ast.ASTKind.Await: + aw: ast.Await* = ast.Await*(node) + self._scan_sleep_expr(trans, aw.value) + return + + # ---------- pass 2: fill the extra-slot table + count state ids ---------- + def _scan_stmts(self, trans: HT.Translator*, stmts: list[ast.AST*]*): + if stmts is None: return + bn: sizet = len(stmts) + i: sizet = 0 + for i in range(int(bn)): self._scan_stmt(trans, stmts.get(i)) + + def _scan_stmt(self, trans: HT.Translator*, stmt: ast.AST*): + if stmt is None: return + sk: int = stmt.kind() + if sk == ast.ASTKind.If: + ifn: ast.If* = ast.If*(stmt) + if ifn.test is not None: self._scan_expr(trans, ifn.test) + self._scan_stmts(trans, stmt.children) + self._scan_stmts(trans, ifn.orelse) + return + if sk == ast.ASTKind.While: + whn: ast.While* = ast.While*(stmt) + if whn.test is not None: self._scan_expr(trans, whn.test) + self._scan_stmts(trans, stmt.children) + self._scan_stmts(trans, whn.orelse) + return + if sk == ast.ASTKind.For: + fn_: ast.For* = ast.For*(stmt) + if self._for_deep_await(stmt) != 0: + # contains await: must be a range-For (M3) + if self._is_range_for(stmt) == 0: + err_b2: str = trans.Pool.alloc(256) + if err_b2 is not None: + viperlib.snprintf(err_b2, 256, "[coro-M3] For inside a coroutine supports only range iteration (including await)") + self.Gen.TypeGenErr.fatal_error(stmt, err_b2, "SyntaxError") + return + # fill __f_idx_N / __f_len_N (i32) + if trans._coro_scan_for_count < HT.MAX_CORO_VARS: trans._coro_for_slots[trans._coro_scan_for_count] = trans._coro_extra_count + trans._coro_extra_types[trans._coro_extra_count] = 1 + trans._coro_extra_count = trans._coro_extra_count + 1 + trans._coro_extra_types[trans._coro_extra_count] = 1 + trans._coro_extra_count = trans._coro_extra_count + 1 + trans._coro_scan_for_count = trans._coro_scan_for_count + 1 + self._scan_stmts(trans, stmt.children) + self._scan_stmts(trans, fn_.orelse) + return + # no await: delegate to standard For; recurse (nested For/Try may contain await) + self._scan_stmts(trans, stmt.children) + self._scan_stmts(trans, fn_.orelse) + return + if sk == ast.ASTKind.Try: + tr2: ast.Try* = ast.Try*(stmt) + self._scan_stmts(trans, stmt.children) + if tr2.handlers is not None: + nh2: sizet = len(tr2.handlers) + hh2: sizet = 0 + for hh2 in range(int(nh2)): + h2: ast.AST* = tr2.handlers.get(hh2) + if h2 is not None: self._scan_stmts(trans, h2.children) + self._scan_stmts(trans, tr2.orelse) + self._scan_stmts(trans, tr2.finalbody) + return + if sk == ast.ASTKind.Raise: + rz: ast.Raise* = ast.Raise*(stmt) + if rz.exc is not None: self._scan_expr(trans, rz.exc) + return + if sk == ast.ASTKind.Return: + rv: ast.Return* = ast.Return*(stmt) + if rv.value is not None: self._scan_expr(trans, rv.value) + return + if sk == ast.ASTKind.Break or sk == ast.ASTKind.Continue: return + # top-level await (AnnAssign/Assign/Expr value is Await): count without hoisting + if sk == ast.ASTKind.AnnAssign or sk == ast.ASTKind.Assign or sk == ast.ASTKind.Expr: + if self._is_await_tail(stmt) != 0: + trans._coro_await_count = trans._coro_await_count + 1 + return + # nested await inside a statement: DFS the value expression + self._scan_stmt_exprs(trans, stmt) + + def _scan_stmt_exprs(self, trans: HT.Translator*, stmt: ast.AST*): + """Scan statement value expressions for nested awaits""" + if stmt is None: return + sk: int = stmt.kind() + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + if aa.value is not None: self._scan_expr(trans, aa.value) + return + if sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + if as_.value is not None: self._scan_expr(trans, as_.value) + return + if sk == ast.ASTKind.Expr: + e: ast.Expr* = ast.Expr*(stmt) + if e.value is not None: self._scan_expr(trans, e.value) + return + # other statement types: fail fast at translation + + def _scan_expr(self, trans: HT.Translator*, node: ast.AST*): + """DFS the expression for Await: allocate a __await_N slot and count the state number""" + if node is None: return + k: int = node.kind() + if k == ast.ASTKind.Await: + # M3: await coro_sleep suspends via the wake slot, not __await_N (state id only) + aw_s: ast.Await* = ast.Await*(node) + if self._expr_await_is_sleep(aw_s) != 0: + trans._coro_await_count = trans._coro_await_count + 1 + return + if trans._coro_hoist_count < HT.MAX_CORO_VARS: trans._coro_await_slots[trans._coro_hoist_count] = trans._coro_extra_count + trans._coro_extra_types[trans._coro_extra_count] = 1 + trans._coro_extra_count = trans._coro_extra_count + 1 + trans._coro_hoist_count = trans._coro_hoist_count + 1 + trans._coro_await_count = trans._coro_await_count + 1 + return + if k == ast.ASTKind.BinOp: + b: ast.BinOp* = ast.BinOp*(node) + self._scan_expr(trans, b.left) + self._scan_expr(trans, b.right) + return + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + if cl.func is not None: self._scan_expr(trans, cl.func) + if cl.args is not None: + nb: sizet = len(cl.args) + j: sizet = 0 + for j in range(int(nb)): self._scan_expr(trans, cl.args.get(j)) + return + if k == ast.ASTKind.UnaryOp: + u: ast.UnaryOp* = ast.UnaryOp*(node) + if u.operand is not None: self._scan_expr(trans, u.operand) + return + if k == ast.ASTKind.Compare: + cm: ast.Compare* = ast.Compare*(node) + if cm.left is not None: self._scan_expr(trans, cm.left) + if cm.comparators is not None: + nb2: sizet = len(cm.comparators) + j2: sizet = 0 + for j2 in range(int(nb2)): self._scan_expr(trans, cm.comparators.get(j2)) + return + if k == ast.ASTKind.BoolOp: + bo: ast.BoolOp* = ast.BoolOp*(node) + if bo.values is not None: + nb3: sizet = len(bo.values) + j3: sizet = 0 + for j3 in range(int(nb3)): self._scan_expr(trans, bo.values.get(j3)) + return + if k == ast.ASTKind.IfExp: + ie: ast.IfExp* = ast.IfExp*(node) + self._scan_expr(trans, ie.test) + self._scan_expr(trans, ie.body) + self._scan_expr(trans, ie.orelse) + return + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + if at.value is not None: self._scan_expr(trans, at.value) + return + if k == ast.ASTKind.Subscript: + sb: ast.Subscript* = ast.Subscript*(node) + if sb.value is not None: self._scan_expr(trans, sb.value) + if sb.slice is not None: self._scan_expr(trans, sb.slice) + return + # leaves like Name / Constant: no nested await + + # ---------- For helpers (M3) ---------- + def _for_deep_await(self, stmt: ast.AST*) -> int: + """Return whether a For statement tree contains await""" + if stmt is None: return 0 + fn3: ast.For* = ast.For*(stmt) + if fn3.iter is not None and self._find_expr_await(fn3.iter) is not None: return 1 + if stmt.children is not None: + bn4: sizet = len(stmt.children) + i4: sizet = 0 + for i4 in range(int(bn4)): + if self._stmt_deep_await(stmt.children.get(i4)) != 0: return 1 + if fn3.orelse is not None: + bn5: sizet = len(fn3.orelse) + i5: sizet = 0 + for i5 in range(int(bn5)): + if self._stmt_deep_await(fn3.orelse.get(i5)) != 0: return 1 + return 0 + + def _stmt_deep_await(self, stmt: ast.AST*) -> int: + """Return whether a statement tree contains await""" + if stmt is None: return 0 + sk2: int = stmt.kind() + if sk2 == ast.ASTKind.If: + ifn2: ast.If* = ast.If*(stmt) + if ifn2.test is not None and self._find_expr_await(ifn2.test) is not None: return 1 + if self._stmts_deep_await(stmt.children) != 0: return 1 + return self._stmts_deep_await(ifn2.orelse) + if sk2 == ast.ASTKind.While: + whn2: ast.While* = ast.While*(stmt) + if whn2.test is not None and self._find_expr_await(whn2.test) is not None: return 1 + if self._stmts_deep_await(stmt.children) != 0: return 1 + return self._stmts_deep_await(whn2.orelse) + if sk2 == ast.ASTKind.For: return self._for_deep_await(stmt) + if sk2 == ast.ASTKind.Try: + tr3: ast.Try* = ast.Try*(stmt) + if self._stmts_deep_await(stmt.children) != 0: return 1 + if tr3.handlers is not None: + nh3: sizet = len(tr3.handlers) + hh3: sizet = 0 + for hh3 in range(int(nh3)): + h3: ast.AST* = tr3.handlers.get(hh3) + if h3 is not None and self._stmts_deep_await(h3.children) != 0: return 1 + if self._stmts_deep_await(tr3.orelse) != 0: return 1 + return self._stmts_deep_await(tr3.finalbody) + return self._stmt_has_await(stmt) + + def _stmts_deep_await(self, stmts: list[ast.AST*]*) -> int: + if stmts is None: return 0 + bn6: sizet = len(stmts) + i6: sizet = 0 + for i6 in range(int(bn6)): + if self._stmt_deep_await(stmts.get(i6)) != 0: return 1 + return 0 + + def _is_range_for(self, stmt: ast.AST*) -> int: + """Return whether a for-loop is a range-for with a plain Name target and no await in its args""" + if stmt is None: return 0 + fn4: ast.For* = ast.For*(stmt) + if fn4.target is None or fn4.target.kind() != ast.ASTKind.Name: return 0 + if fn4.iter is None or fn4.iter.kind() != ast.ASTKind.Call: return 0 + cl4: ast.Call* = ast.Call*(fn4.iter) + if cl4.func is None or cl4.func.kind() != ast.ASTKind.Name: return 0 + nm4: ast.Name* = ast.Name*(cl4.func) + if nm4.id is None or string.strcmp(nm4.id, "range") != 0: return 0 + if cl4.args is None: return 0 + nb7: sizet = len(cl4.args) + if nb7 != 1 and nb7 != 2: return 0 + j7: sizet = 0 + for j7 in range(int(nb7)): + if self._find_expr_await(cl4.args.get(j7)) is not None: return 0 + return 1 + + # ============================================================ + # Local-variable collection (top-level AnnAssign / Assign Name targets) + def _collect_local_names(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: + """Collect frame local variable names into trans._coro_locals (8-byte slots)""" + if fd is None or fd.children is None or trans is None: return 0 + if trans._coro_locals is None: return 0 + trans._coro_local_count = 0 + self._collect_locals_stmts(trans, fd.children) + return trans._coro_local_count + + def _collect_locals_stmts(self, trans: HT.Translator*, stmts: list[ast.AST*]*): + if stmts is None: return + bn: sizet = len(stmts) + i: sizet = 0 + for i in range(int(bn)): self._collect_locals_stmt(trans, stmts.get(i)) + + def _collect_locals_stmt(self, trans: HT.Translator*, stmt: ast.AST*): + if stmt is None: return + sk: int = stmt.kind() + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + if aa.target is not None and aa.target.kind() == ast.ASTKind.Name: + tn: ast.Name* = ast.Name*(aa.target) + self._add_local(trans, tn.id) + return + if sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + if as_.targets is not None and len(as_.targets) == 1: + t0: ast.AST* = as_.targets.get(0) + if t0 is not None and t0.kind() == ast.ASTKind.Name: + tn2: ast.Name* = ast.Name*(t0) + self._add_local(trans, tn2.id) + return + if sk == ast.ASTKind.If: + self._collect_locals_stmts(trans, stmt.children) + ifn: ast.If* = ast.If*(stmt) + self._collect_locals_stmts(trans, ifn.orelse) + return + if sk == ast.ASTKind.While: + self._collect_locals_stmts(trans, stmt.children) + whn: ast.While* = ast.While*(stmt) + self._collect_locals_stmts(trans, whn.orelse) + return + if sk == ast.ASTKind.For: + fn: ast.For* = ast.For*(stmt) + if self._for_deep_await(stmt) != 0: + if fn.target is not None and fn.target.kind() == ast.ASTKind.Name: + tn3: ast.Name* = ast.Name*(fn.target) + self._add_local(trans, tn3.id) + self._collect_locals_stmts(trans, stmt.children) + self._collect_locals_stmts(trans, fn.orelse) + return + if sk == ast.ASTKind.Try: + tr: ast.Try* = ast.Try*(stmt) + self._collect_locals_stmts(trans, stmt.children) + if tr.handlers is not None: + nh: sizet = len(tr.handlers) + hh: sizet = 0 + for hh in range(int(nh)): + h0: ast.AST* = tr.handlers.get(hh) + if h0 is None: continue + hd: ast.ExceptHandler* = ast.ExceptHandler*(h0) + self._add_local(trans, hd.name) + self._collect_locals_stmts(trans, h0.children) + self._collect_locals_stmts(trans, tr.orelse) + self._collect_locals_stmts(trans, tr.finalbody) + return + + def _add_local(self, trans: HT.Translator*, name: str): + if name is None: return + cnt: int = trans._coro_local_count + if cnt >= HT.MAX_CORO_VARS: return + addr: uint64 = uint64(trans._coro_locals) + cnt * 8 + slot: str* = (str*)(addr) + slot[0] = name + trans._coro_local_count = cnt + 1 + + # Frame struct %Coro_ = { i32 state, i32 done, i32 ret, ... }; + # slot index: 0=state 1=done 2=ret 3=exc 4=msg, then params / locals / extra slots. + def _build_frame_ty(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str, nparams: int, nlocals: int, nextra: int) -> mlirlite.LLVMType*: + """Build the frame struct, register it on the module and struct table, and return its LLVMType""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + # slot base (used by resume/await to compute slot indexes) + trans._coro_local_base = 5 + nparams + trans._coro_extra_base = trans._coro_local_base + nlocals + total: int = 5 + nparams + nlocals + nextra + head: mlirlite.ParamNode* = None + prev: mlirlite.ParamNode* = None + fi: int = 0 + for fi in range(int(total)): + fty: mlirlite.LLVMType* = i32_ty + if fi == 4: fty = i8_ptr_ty + elif fi >= trans._coro_extra_base: + tidx: int = fi - trans._coro_extra_base + if tidx < nextra and trans._coro_extra_types is not None: + if trans._coro_extra_types[tidx] == 2: fty = i64_ty + pnode: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__()) + if pnode is None: continue + string.memset(pnode, 0, mlirlite.ParamNode.__sizeof__()) + pnode.Ty = fty + if head is None: head = pnode + if prev is not None: prev.Next = pnode + prev = pnode + ty_name: str = pool.alloc(128) + if ty_name is not None: viperlib.snprintf(ty_name, 128, "Coro_%s", name) + frame_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, head, total, ty_name) + if frame_ty is None: return None + mlirlite.module_add_named_type(mod, pool, frame_ty) + self.Gen.StructGen.register_struct(pool, ty_name, frame_ty, trans.ModuleSha1) + return frame_ty + + # ============================================================ + # create_() -> %Coro_* + def _emit_create(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str, frame_ty: mlirlite.LLVMType*, nparams: int) -> int: + """Emit create_(args...): pop or malloc a frame, zero the header, store args, return it""" + if frame_ty is None: return 0 + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + i64_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i64_ty) + frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) + + cname_buf2: str = pool.alloc(64) + if cname_buf2 is not None: viperlib.snprintf(cname_buf2, 64, "create_%s", name) + mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, cname_buf2, 0) + if self.Gen.CallGenBase.find_func_in_module(mod, mangled) is not None: return 0 + func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled, frame_ptr_ty) + if func is None: return 0 + # params (i32, frame slot 5+i). VPC has no Python "%" formatting ("%" is modulo), + # so names are built with snprintf ("%%arg" -> "%arg", matching resume's "%frame"). + pi: int = 0 + argn_buf: str = pool.alloc(16) + for pi in range(int(nparams)): + if argn_buf is not None: viperlib.snprintf(argn_buf, 16, "%%arg%d", pi) + mlirlite.add_param(pool, func, i32_ty, argn_buf) + # register in the function table (bare name create_) + funcs_ptr: GenTypes.FuncEntry* = trans._funcs + cur_count: int = trans._func_count + if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, cur_count, cname_buf2, func, 1024, None, 0, 0, trans.Pool) == 0: trans._func_count = cur_count + 1 + + entry: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") + if entry is None: return 0 + builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) + if builder is None: return 0 + mlirlite.position_at_end(builder, entry) + + frame_size: int = self.Gen.ClassGenEnum._get_type_size(frame_ty) + size_val: mlirlite.Value* = mlirlite.const_int64(pool, frame_size) + if size_val is None: return 0 + size_val.Next = None + + # M7 frame pool: i64 head (0=empty); first 8 bytes hold next (state/done zeroed on reuse) + pool_gv: mlirlite.GlobalVariable* = self._get_coro_pool(pool, mod, trans, name) + # GlobalVariable is not a Value: build a Value view (Ty=Ptr(i64), Name="@coro_pool_") + pool_ref_name: str = pool.alloc(96) + if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, 96, "@coro_pool_%s", name) + pool_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i64_ptr_ty, pool_ref_name) + pool_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, pool_ref) + if pool_head is None: return 0 + zero64: mlirlite.Value* = mlirlite.const_int64(pool, 0) + empty_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, pool_head, zero64) + if empty_cond is None: return 0 + hit_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "pool.hit") + miss_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "pool.miss") + join_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "pool.join") + if hit_bb is None or miss_bb is None or join_bb is None: return 0 + mlirlite.build_cond_br(builder, empty_cond, miss_bb, hit_bb) + # hit: pop from pool (first 8 bytes are next) + mlirlite.position_at_end(builder, hit_bb) + node_next_ptr: mlirlite.Value* = mlirlite.build_inttoptr(builder, pool_head, i64_ptr_ty) + next_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, node_next_ptr) + if next_head is None: return 0 + mlirlite.build_store(builder, next_head, pool_ref) + hit_frame: mlirlite.Value* = mlirlite.build_inttoptr(builder, pool_head, frame_ptr_ty) + if hit_frame is None: return 0 + mlirlite.build_br(builder, join_bb) + # miss: malloc + zero the first 8 bytes (clear stale next from pooled frames) + mlirlite.position_at_end(builder, miss_bb) + raw: mlirlite.Value* = mlirlite.build_call(builder, "malloc", size_val, 1, i8_ptr_ty, 0) + if raw is None: return 0 + miss_frame: mlirlite.Value* = mlirlite.build_bitcast(builder, raw, frame_ptr_ty) + if miss_frame is None: return 0 + miss_head_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, miss_frame, i64_ptr_ty) + if miss_head_ptr is not None: mlirlite.build_store(builder, zero64, miss_head_ptr) + mlirlite.build_br(builder, join_bb) + # join: phi-select the frame + zero the 5 header fields + store params into slots + mlirlite.position_at_end(builder, join_bb) + inc1: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, hit_frame, hit_bb) + inc2: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, miss_frame, miss_bb) + if inc1 is None or inc2 is None: return 0 + inc1.Next = inc2 + frame_ref: mlirlite.Value* = mlirlite.build_phi(builder, frame_ptr_ty, inc1, 2) + if frame_ref is None: return 0 + zero: mlirlite.Value* = mlirlite.const_int32(pool, 0) + null_p: mlirlite.Value* = mlirlite.ConstNull(pool, i8_ptr_ty, "null") + si: int = 0 + for si in range(5): + if si == 4: + mslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i8_ptr_ty, frame_ref, 4) + if mslot is not None: mlirlite.build_store(builder, null_p, mslot) + else: + slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ref, si) + if slot is not None: mlirlite.build_store(builder, zero, slot) + # store params into frame slots (index 5+i) + pj: int = 0 + for pj in range(int(nparams)): + pslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ref, 5 + pj) + if pslot is not None: + arnb: str = pool.alloc(16) + if arnb is not None: viperlib.snprintf(arnb, 16, "%%arg%d", pj) + arg_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i32_ty, arnb) + if arg_ref is not None: mlirlite.build_store(builder, arg_ref, pslot) + mlirlite.build_ret(builder, frame_ref) + return 0 + + # ============================================================ + # M7: per-frame-type global free-list pool head (i64, 0 = empty) + def _get_coro_pool(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str) -> mlirlite.GlobalVariable*: + """Get or create the coro_pool_ global (i64 pool head)""" + pn_buf: str = pool.alloc(128) + if pn_buf is not None: viperlib.snprintf(pn_buf, 128, "coro_pool_%s", name) + # idempotent: create/destroy both call this, so scan the module global list (no find_global API) + g: mlirlite.GlobalVariable* = mod.GlobalHead + while g is not None: + if g.Name is not None and string.strcmp(g.Name, pn_buf) == 0: return g + g = g.Next + return mlirlite.create_global_int(pool, mod, pn_buf, 64, 0) + + def _register_coro_wake(self, trans: HT.Translator*, name: str, wake_idx: int): + """Record the wake-slot index for an async name""" + if trans is None or name is None or trans._coro_wake_names is None: return + cnt: int = trans._coro_wake_count + if cnt >= HT.MAX_CORO_FUNCS: return + addr: uint64 = uint64(trans._coro_wake_names) + cnt * 8 + slot: str* = (str*)(addr) + slot[0] = name + trans._coro_wake_idxs[cnt] = wake_idx + trans._coro_wake_count = cnt + 1 + + def _lookup_coro_wake(self, trans: HT.Translator*, name: str) -> int: + """Return the wake-slot index for an async name, or -1 if none""" + if trans is None or name is None or trans._coro_wake_names is None: return -1 + i: int = 0 + for i in range(int(trans._coro_wake_count)): + addr: uint64 = uint64(trans._coro_wake_names) + i * 8 + slot: str* = (str*)(addr) + if slot[0] is not None and string.strcmp(slot[0], name) == 0: return trans._coro_wake_idxs[i] + return -1 + + # ============================================================ + # resume_(%Coro_*) -> i32 state machine (static segment splitting) + def _emit_resume(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str, fd: ast.FunctionDef*, frame_ty: mlirlite.LLVMType*, nparams: int, nlocals: int, nextra: int) -> int: + """Emit resume_(frame) -> i32 state machine returning done (0=SUSPEND, 1=DONE)""" + if frame_ty is None: return 0 + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) + + rname_buf: str = pool.alloc(64) + if rname_buf is not None: viperlib.snprintf(rname_buf, 64, "resume_%s", name) + mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rname_buf, 0) + func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled, i32_ty) + if func is None: return 0 + mlirlite.add_param(pool, func, frame_ptr_ty, "%frame") + funcs_ptr: GenTypes.FuncEntry* = trans._funcs + cur_count: int = trans._func_count + if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, cur_count, rname_buf, func, 1024, None, 0, 1, trans.Pool) == 0: + trans._func_count = cur_count + 1 + + entry: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") + if entry is None: return 0 + builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) + if builder is None: return 0 + mlirlite.position_at_end(builder, entry) + + frame_arg: mlirlite.Value* = mlirlite.SSAValue(pool, frame_ptr_ty, "%frame") + if frame_arg is None: return 0 + + # GEP the 5 header slots (M4 exc / M7 msg) + i8_ptr_ty_r: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + i64_ty_r: mlirlite.LLVMType* = mlirlite.Int64(pool) + state_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 0) + done_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 1) + ret_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 2) + exc_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 3) + msg_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i8_ptr_ty_r, frame_arg, 4) + + # GEP param slots -> register in the symbol table (index 5+i) + pj: int = 0 + for pj in range(int(nparams)): + pname: str = self._read_param_name(trans, pj) + if pname is None: continue + pslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, 5 + pj) + if pslot is not None: self.Gen.VarGen.define_var(trans.SymTab, pname, pslot) + + # GEP local slots -> register in the symbol table + li: int = 0 + for li in range(int(nlocals)): + lname: str = self._read_local_name(trans, li) + if lname is None: continue + slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_arg, trans._coro_local_base + li) + if slot is not None: self.Gen.VarGen.define_var(trans.SymTab, lname, slot) + + # M3-M7: register extra slots (wake i64 / __f_idx/__f_len/__await i32). + # Name reverse lookup: wake=uses_sleep && ei==0; for=_coro_for_slots; else __await_M. + ei: int = 0 + for ei in range(int(nextra)): + ety: int = 1 + if trans._coro_extra_types is not None: ety = trans._coro_extra_types[ei] + ety_llvm: mlirlite.LLVMType* = i32_ty + if ety == 2: ety_llvm = i64_ty_r + eslot: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, ety_llvm, frame_arg, trans._coro_extra_base + ei) + if eslot is None: continue + # wake slot: store the pointer only, no SymTab entry (used by coro_run polling) + if trans._coro_uses_sleep != 0 and ei == 0: + trans._coro_wake_ptr = eslot + continue + ename: str = pool.alloc(16) + # for slot: _coro_for_slots[N] is __f_idx_N, +1 is __f_len_N + is_for: int = 0 + is_len: int = 0 + kf: int = 0 + for kf in range(int(trans._coro_scan_for_count)): + if trans._coro_for_slots[kf] == ei: + is_for = 1 + break + if trans._coro_for_slots[kf] + 1 == ei: + is_for = 1 + is_len = 1 + break + if is_for != 0: + if ename is not None: + if is_len != 0: viperlib.snprintf(ename, 16, "__f_len_%d", kf) + else: viperlib.snprintf(ename, 16, "__f_idx_%d", kf) + else: + # __await_M: reverse lookup the position array + found: int = 0 + km: int = 0 + for km in range(int(nextra)): + if trans._coro_await_slots[km] == ei: + found = 1 + break + if found == 0: continue + if ename is not None: viperlib.snprintf(ename, 16, "__await_%d", km) + if ename is not None: self.Gen.VarGen.define_var(trans.SymTab, ename, eslot) + + # state value + exit block + state_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, state_ptr) + exit_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.exit") + if exit_bb is None: return 0 + + # save resume context (shared by await suspension / resume blocks) + trans._coro_state_ptr = state_ptr + trans._coro_done_ptr = done_ptr + trans._coro_ret_ptr = ret_ptr + trans._coro_exc_ptr = exc_ptr + trans._coro_msg_ptr = msg_ptr + trans._coro_wake_ptr = None + trans._coro_exit_bb = exit_bb + trans._coro_frame = frame_arg + trans._coro_await_slot = None + # state0 is the entry; await states start at 1 (same DFS order as pre-scan) + trans._coro_next_state = 1 + # translation-time indices (same order as pre-scan) + trans._coro_hoist_count = 0 + trans._coro_for_counter = 0 + trans._coro_try_stack_count = 0 + trans._coro_break_bb = None + trans._coro_continue_bb = None + + nseg: int = trans._coro_nseg + if nseg <= 0: nseg = 1 + # pre-build state blocks + switch cases (one-shot; resume blocks are positioned later) + state_bbs: sizet* = pool.alloc(nseg * 8) + if state_bbs is None: return 0 + trans._coro_state_bbs = state_bbs + sw_head: mlirlite.SwitchCase* = None + sw_tail: mlirlite.SwitchCase* = None + sw_count: int = 0 + bb_name: str = pool.alloc(32) + si: int = 0 + for si in range(int(nseg)): + if bb_name is not None: viperlib.snprintf(bb_name, 32, "state%d", si) + sbb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, bb_name) + if sbb is None: continue + state_bbs[si] = sizet(sbb) + sc: mlirlite.SwitchCase* = mlirlite.new_switch_case(pool, mlirlite.const_int32(pool, si), sbb) + if sc is not None: + if sw_head is None: + sw_head = sc + sw_tail = sc + else: + sw_tail.Next = sc + sw_tail = sc + sw_count = sw_count + 1 + mlirlite.build_switch(builder, state_val, exit_bb, sw_head, sw_count) + + # ---- state0 entry: recursively translate the whole body (If/While awaits resume dynamically). + # Set trans._cur_builder/_cur_func here; later position_at_end reuse the same builder object. + trans._cur_func = func + trans._cur_builder = builder + # P2 willFree 失效表:协程体是独立帧 → 入口同样复位 + self.Gen.VarGen.freed_clear() + seg_bb: mlirlite.BasicBlock* = mlirlite.BasicBlock*(state_bbs[0]) + mlirlite.position_at_end(builder, seg_bb) + # M3 硬约束锚点:登记"正在翻译其 body 的协程函数",CallGenCtor._translate_new_expr 据此对 + # 协程体内的 `new heap slife` fail-fast(协程体被状态机拆分,释放点无法确定)。 + # 保存/恢复旧值:协程里再定义协程时不会把外层的标记冲掉。 + old_coro_body: mlirlite.Function* = trans._coro_body_func + trans._coro_body_func = func + self._handle_segment(trans, builder, pool, mod, fd.children) + trans._coro_body_func = old_coro_body + # body fell through -> implicit return (ret=0 + done=1 + br exit) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: + zret: mlirlite.Value* = mlirlite.const_int32(pool, 0) + done_one: mlirlite.Value* = mlirlite.const_int32(pool, 1) + mlirlite.build_store(builder, zret, ret_ptr) + mlirlite.build_store(builder, done_one, done_ptr) + mlirlite.build_br(builder, exit_bb) + + # exit block: load done and return (0=SUSPEND 1=DONE) + mlirlite.position_at_end(builder, exit_bb) + done_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, done_ptr) + mlirlite.build_ret(builder, done_val) + return 0 + + # ============================================================ + # M2: recursive coroutine-body translation (Return/top-level await/If/While/nested await/plain) + def _handle_segment(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmts: list[ast.AST*]*): + """Recursively translate coroutine body statements into the current builder block""" + if stmts is None: return + bn: sizet = len(stmts) + i: sizet = 0 + for i in range(int(bn)): + stmt: ast.AST* = stmts.get(i) + if stmt is None: continue + # current block is terminated (return/suspend) -> stop this segment + if mlirlite.builder_cur_block_is_terminated(builder) != 0: return + sk: int = stmt.kind() + if sk == ast.ASTKind.Return: + self._emit_coro_return(builder, pool, mod, trans, stmt) + continue + if sk == ast.ASTKind.If: + self._handle_coro_if(trans, builder, pool, mod, stmt) + continue + if sk == ast.ASTKind.While: + self._handle_coro_while(trans, builder, pool, mod, stmt) + continue + if sk == ast.ASTKind.Raise: + self._handle_coro_raise(builder, pool, mod, trans, stmt) + continue + if sk == ast.ASTKind.Try: + tr_: ast.Try* = ast.Try*(stmt) + has_fin: int = 0 + has_exc: int = 0 + if tr_.finalbody is not None and len(tr_.finalbody) > 0: has_fin = 1 + if tr_.handlers is not None and len(tr_.handlers) > 0: has_exc = 1 + if has_fin != 0 and has_exc == 0: self._handle_coro_try_finally(trans, builder, pool, mod, stmt) + else: self._handle_coro_try(trans, builder, pool, mod, stmt) + continue + if sk == ast.ASTKind.For: + if self._for_deep_await(stmt) != 0: self._handle_coro_for(trans, builder, pool, mod, stmt) + else: + # no await: delegate to the standard For + self.Gen.StmtGen.translate_stmt(trans, stmt) + continue + if sk == ast.ASTKind.Break: + if trans._coro_break_bb is not None: mlirlite.build_br(builder, trans._coro_break_bb) + continue + if sk == ast.ASTKind.Continue: + if trans._coro_continue_bb is not None: mlirlite.build_br(builder, trans._coro_continue_bb) + continue + if sk == ast.ASTKind.Pass: continue + # top-level await: AnnAssign/Assign/Expr value is Await -> suspend + if sk == ast.ASTKind.AnnAssign or sk == ast.ASTKind.Assign or sk == ast.ASTKind.Expr: + if self._is_await_tail(stmt) != 0: + target_name: str = self._get_assign_target_name(stmt) + if target_name is not None: trans._coro_await_slot = self.Gen.VarGen.lookup_var(trans.SymTab, target_name) + else: + # Expr(await ...): no target slot (discard return), clear stale slot + trans._coro_await_slot = None + aw_node: ast.Await* = self._get_await_value(stmt) + call_name: str = None + if aw_node is not None: call_name = self._extract_await_call_name(aw_node) + if call_name is not None and self.is_async_func(trans, call_name) != 0: self._emit_await(builder, pool, mod, trans, aw_node, call_name) + else: + if call_name is None: call_name = "(unknown)" + err_buf: str = pool.alloc(256) + if err_buf is not None: + viperlib.snprintf(err_buf, 256, "[coro-M2] await target '%s' is not an async function", call_name) + self.Gen.TypeGenErr.fatal_error(stmt, err_buf, "SyntaxError") + continue + # nested await in the statement -> hoist to __await_N + if self._stmt_has_await(stmt) != 0: + self._handle_nested_await_stmt(trans, builder, pool, mod, stmt) + continue + # other statements -> standard translation + self.Gen.StmtGen.translate_stmt(trans, stmt) + + # ============================================================ + # M2: coroutine If (awaits inside branches suspend/resume in place) + def _handle_coro_if(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): + ifn: ast.If* = ast.If*(stmt) + if ifn is None or ifn.test is None: return + # cif/celif (internal c.@CIf marker): compile-time prune, then translate only the taken branch + # so awaits inside it still suspend in place. The marker must never be lowered as a runtime value. + if self.Gen.StmtGenCif._is_cif_call(ifn.test) == 1: + if self.Gen.StmtGenCif._prune_cif_test(pool, trans, ifn.test) != 0: + self._handle_segment(trans, builder, pool, mod, ifn.children) + else: + self._handle_segment(trans, builder, pool, mod, ifn.orelse) + return + if self._find_expr_await(ifn.test) is not None: + err_b: str = pool.alloc(256) + if err_b is not None: + viperlib.snprintf(err_b, 256, "[coro-M2] await is not supported in an if condition") + self.Gen.TypeGenErr.fatal_error(stmt, err_b, "SyntaxError") + return + cond: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ifn.test, None, 0, trans) + if cond is None: return + cond1: mlirlite.Value* = self._normalize_cond(builder, pool, mod, cond, trans) + func: mlirlite.Function* = trans._cur_func + then_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.then") + merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.end") + has_else: int = 0 + if ifn.orelse is not None and len(ifn.orelse) > 0: has_else = 1 + else_bb: mlirlite.BasicBlock* = None + if has_else != 0: else_bb = mlirlite.create_block(pool, func, "coro.else") + if cond1 is None or then_bb is None or merge_bb is None: return + mlirlite.build_cond_br(builder, cond1, then_bb, else_bb if else_bb is not None else merge_bb) + # then branch + mlirlite.position_at_end(builder, then_bb) + self._handle_segment(trans, builder, pool, mod, stmt.children) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) + # else branch + if else_bb is not None: + mlirlite.position_at_end(builder, else_bb) + self._handle_segment(trans, builder, pool, mod, ifn.orelse) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) + mlirlite.position_at_end(builder, merge_bb) + + # ============================================================ + # M2: coroutine While (condition block + body; awaits in the body suspend/resume in place) + def _handle_coro_while(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): + whn: ast.While* = ast.While*(stmt) + if whn is None: return + if whn.test is not None and self._find_expr_await(whn.test) is not None: + err_c: str = pool.alloc(256) + if err_c is not None: + viperlib.snprintf(err_c, 256, "[coro-M2] await is not supported in a while condition") + self.Gen.TypeGenErr.fatal_error(stmt, err_c, "SyntaxError") + return + func: mlirlite.Function* = trans._cur_func + cond_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wcond") + body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wbody") + after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wend") + if cond_bb is None or body_bb is None or after_bb is None: return + # break/continue targets (M2, single level) + prev_break: mlirlite.BasicBlock* = trans._coro_break_bb + prev_cont: mlirlite.BasicBlock* = trans._coro_continue_bb + trans._coro_break_bb = after_bb + trans._coro_continue_bb = cond_bb + mlirlite.build_br(builder, cond_bb) + # condition block + mlirlite.position_at_end(builder, cond_bb) + cond: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, whn.test, None, 0, trans) + if cond is None: cond = mlirlite.const_int32(pool, 0) + cond1: mlirlite.Value* = self._normalize_cond(builder, pool, mod, cond, trans) + mlirlite.build_cond_br(builder, cond1, body_bb, after_bb) + # loop body + mlirlite.position_at_end(builder, body_bb) + self._handle_segment(trans, builder, pool, mod, stmt.children) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, cond_bb) + trans._coro_break_bb = prev_break + trans._coro_continue_bb = prev_cont + # loop exit block + mlirlite.position_at_end(builder, after_bb) + + # ============================================================ + # M2: nested await hoisting: a = b + await f() -> __await_N = await f(); a = b + __await_N + def _handle_nested_await_stmt(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): + """Hoist all nested awaits in a statement to top-level temporaries in DFS order""" + guard: int = 0 + while True: + aw_node: ast.Await* = self._extract_first_await(stmt) + if aw_node is None: break + if guard >= HT.MAX_CORO_VARS: break + guard = guard + 1 + temp_name: str = pool.alloc(32) + if temp_name is not None: viperlib.snprintf(temp_name, 32, "__await_%d", trans._coro_hoist_count) + # target slot = __await_N extra slot (pre-scan positions, wake/for slots precede) + slot_idx: int = trans._coro_extra_base + trans._coro_await_slots[trans._coro_hoist_count] + trans._coro_hoist_count = trans._coro_hoist_count + 1 + slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, self._lookup_coro_frame_by_trans(trans), mlirlite.Int32(pool), trans._coro_frame, slot_idx) + trans._coro_await_slot = slot + call_name: str = self._extract_await_call_name(aw_node) + if call_name is None: break + self._emit_await(builder, pool, mod, trans, aw_node, call_name) + self._replace_await_with_name(pool, stmt, temp_name) + # hoisting done -> standard translation (resume block continues the rest of the expression) + self.Gen.StmtGen.translate_stmt(trans, stmt) + + # ============================================================ + # Nested-await detection / extraction / replacement (DFS order matches pre-scan) + def _stmt_has_await(self, stmt: ast.AST*) -> int: + aw: ast.Await* = self._extract_first_await(stmt) + if aw is not None: return 1 + return 0 + + def _extract_first_await(self, stmt: ast.AST*) -> ast.Await*: + """Return the first Await node in a statement (DFS field order), or None""" + if stmt is None: return None + sk: int = stmt.kind() + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + return self._find_expr_await(aa.value) + if sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + return self._find_expr_await(as_.value) + if sk == ast.ASTKind.Expr: + e: ast.Expr* = ast.Expr*(stmt) + return self._find_expr_await(e.value) + if sk == ast.ASTKind.Return: + rv: ast.Return* = ast.Return*(stmt) + return self._find_expr_await(rv.value) + return None + + def _find_expr_await(self, node: ast.AST*) -> ast.Await*: + """DFS the expression tree for the first Await""" + if node is None: return None + k: int = node.kind() + if k == ast.ASTKind.Await: return ast.Await*(node) + if k == ast.ASTKind.BinOp: + b: ast.BinOp* = ast.BinOp*(node) + r: ast.Await* = self._find_expr_await(b.left) + if r is not None: return r + return self._find_expr_await(b.right) + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + if cl.func is not None: + r2: ast.Await* = self._find_expr_await(cl.func) + if r2 is not None: return r2 + if cl.args is not None: + nb: sizet = len(cl.args) + j: sizet = 0 + for j in range(int(nb)): + r3: ast.Await* = self._find_expr_await(cl.args.get(j)) + if r3 is not None: return r3 + return None + if k == ast.ASTKind.UnaryOp: + u: ast.UnaryOp* = ast.UnaryOp*(node) + return self._find_expr_await(u.operand) + if k == ast.ASTKind.Compare: + cm: ast.Compare* = ast.Compare*(node) + r4: ast.Await* = self._find_expr_await(cm.left) + if r4 is not None: return r4 + if cm.comparators is not None: + nb2: sizet = len(cm.comparators) + j2: sizet = 0 + for j2 in range(int(nb2)): + r5: ast.Await* = self._find_expr_await(cm.comparators.get(j2)) + if r5 is not None: return r5 + return None + if k == ast.ASTKind.BoolOp: + bo: ast.BoolOp* = ast.BoolOp*(node) + if bo.values is not None: + nb3: sizet = len(bo.values) + j3: sizet = 0 + for j3 in range(int(nb3)): + r6: ast.Await* = self._find_expr_await(bo.values.get(j3)) + if r6 is not None: return r6 + return None + if k == ast.ASTKind.IfExp: + ie: ast.IfExp* = ast.IfExp*(node) + r7: ast.Await* = self._find_expr_await(ie.test) + if r7 is not None: return r7 + r8: ast.Await* = self._find_expr_await(ie.body) + if r8 is not None: return r8 + return self._find_expr_await(ie.orelse) + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + return self._find_expr_await(at.value) + if k == ast.ASTKind.Subscript: + sb: ast.Subscript* = ast.Subscript*(node) + r9: ast.Await* = self._find_expr_await(sb.value) + if r9 is not None: return r9 + return self._find_expr_await(sb.slice) + return None + + def _replace_await_with_name(self, pool: memhub.MemBuddy*, stmt: ast.AST*, temp_name: str) -> int: + """Replace the first Await in a statement tree with Name(temp_name) in DFS order""" + if stmt is None or temp_name is None: return 0 + sk: int = stmt.kind() + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + if aa.value is not None: return self._replace_expr_await(pool, aa.value, temp_name) + return 0 + if sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + if as_.value is not None: return self._replace_expr_await(pool, as_.value, temp_name) + return 0 + if sk == ast.ASTKind.Expr: + e: ast.Expr* = ast.Expr*(stmt) + if e.value is not None: return self._replace_expr_await(pool, e.value, temp_name) + return 0 + if sk == ast.ASTKind.Return: + rv: ast.Return* = ast.Return*(stmt) + if rv.value is not None: return self._replace_expr_await(pool, rv.value, temp_name) + return 0 + return 0 + + def _replace_expr_await(self, pool: memhub.MemBuddy*, node: ast.AST*, temp_name: str) -> int: + """Replace the first Await in an expression with Name(temp_name); returns 1/0/2""" + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Await: return 2 + if k == ast.ASTKind.BinOp: + b: ast.BinOp* = ast.BinOp*(node) + r: int = self._replace_expr_await(pool, b.left, temp_name) + if r == 1: return 1 + if r == 2: + b.left = ast.Name(pool, temp_name, 0) + return 1 + r2: int = self._replace_expr_await(pool, b.right, temp_name) + if r2 == 1: return 1 + if r2 == 2: + b.right = ast.Name(pool, temp_name, 0) + return 1 + return 0 + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + if cl.func is not None: + r3: int = self._replace_expr_await(pool, cl.func, temp_name) + if r3 == 1: return 1 + if r3 == 2: + cl.func = ast.Name(pool, temp_name, 0) + return 1 + if cl.args is not None: + nb: sizet = len(cl.args) + j: sizet = 0 + for j in range(int(nb)): + r4: int = self._replace_expr_await(pool, cl.args.get(j), temp_name) + if r4 == 1: return 1 + if r4 == 2: + cl.args.set(j, ast.Name(pool, temp_name, 0)) + return 1 + return 0 + if k == ast.ASTKind.UnaryOp: + u: ast.UnaryOp* = ast.UnaryOp*(node) + r5: int = self._replace_expr_await(pool, u.operand, temp_name) + if r5 == 1: return 1 + if r5 == 2: + u.operand = ast.Name(pool, temp_name, 0) + return 1 + return 0 + if k == ast.ASTKind.Compare: + cm: ast.Compare* = ast.Compare*(node) + r6: int = self._replace_expr_await(pool, cm.left, temp_name) + if r6 == 1: return 1 + if r6 == 2: + cm.left = ast.Name(pool, temp_name, 0) + return 1 + if cm.comparators is not None: + nb2: sizet = len(cm.comparators) + j2: sizet = 0 + for j2 in range(int(nb2)): + r7: int = self._replace_expr_await(pool, cm.comparators.get(j2), temp_name) + if r7 == 1: return 1 + if r7 == 2: + cm.comparators.set(j2, ast.Name(pool, temp_name, 0)) + return 1 + return 0 + if k == ast.ASTKind.BoolOp: + bo: ast.BoolOp* = ast.BoolOp*(node) + if bo.values is not None: + nb3: sizet = len(bo.values) + j3: sizet = 0 + for j3 in range(int(nb3)): + r8: int = self._replace_expr_await(pool, bo.values.get(j3), temp_name) + if r8 == 1: return 1 + if r8 == 2: + bo.values.set(j3, ast.Name(pool, temp_name, 0)) + return 1 + return 0 + if k == ast.ASTKind.IfExp: + ie: ast.IfExp* = ast.IfExp*(node) + r9: int = self._replace_expr_await(pool, ie.test, temp_name) + if r9 == 1: return 1 + if r9 == 2: + ie.test = ast.Name(pool, temp_name, 0) + return 1 + r10: int = self._replace_expr_await(pool, ie.body, temp_name) + if r10 == 1: return 1 + if r10 == 2: + ie.body = ast.Name(pool, temp_name, 0) + return 1 + r11: int = self._replace_expr_await(pool, ie.orelse, temp_name) + if r11 == 1: return 1 + if r11 == 2: + ie.orelse = ast.Name(pool, temp_name, 0) + return 1 + return 0 + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + r12: int = self._replace_expr_await(pool, at.value, temp_name) + if r12 == 1: return 1 + if r12 == 2: + at.value = ast.Name(pool, temp_name, 0) + return 1 + return 0 + if k == ast.ASTKind.Subscript: + sb: ast.Subscript* = ast.Subscript*(node) + r13: int = self._replace_expr_await(pool, sb.value, temp_name) + if r13 == 1: return 1 + if r13 == 2: + sb.value = ast.Name(pool, temp_name, 0) + return 1 + r14: int = self._replace_expr_await(pool, sb.slice, temp_name) + if r14 == 1: return 1 + if r14 == 2: + sb.slice = ast.Name(pool, temp_name, 0) + return 1 + return 0 + return 0 + + # ============================================================ + # Condition normalization (i1 passes through; otherwise icmp ne 0) + def _normalize_cond(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cond: mlirlite.Value*, trans: HT.Translator*) -> mlirlite.Value*: + """Normalize a coroutine condition to i1, preferring __bool__ dispatch""" + return self.Gen.ExprGenOps.coerce_cond_to_bool(pool, builder, mod, cond, trans) + + # ============================================================ + # Parameter-name read (from the _coro_params table) + def _read_param_name(self, trans: HT.Translator*, idx: int) -> str: + if trans is None or trans._coro_params is None: return None + addr: uint64 = uint64(trans._coro_params) + idx * 8 + slot: str* = (str*)(addr) + return slot[0] + + # ============================================================ + # Resume-block lookup (state{N} from the pre-built array) + def _get_state_bb(self, trans: HT.Translator*, state_no: int) -> mlirlite.BasicBlock*: + if trans is None or trans._coro_state_bbs is None: return None + if state_no < 0 or state_no >= trans._coro_nseg: return None + # state_bbs holds block pointers (sizet): index it, never take an element address, + # or position lands on an invalid block and the IR chain corrupts (RenumberSSA crash). + return mlirlite.BasicBlock*(trans._coro_state_bbs[state_no]) + + def _lookup_coro_frame_by_trans(self, trans: HT.Translator*) -> mlirlite.LLVMType*: + """Return the frame type of the current coroutine, looked up from the registered frames""" + # resume GEP needs the frame type: take the Pointee of _coro_frame.Ty (Ptr(frame)) + if trans is None or trans._coro_frame is None: return None + ft: mlirlite.LLVMType* = trans._coro_frame.Ty + if ft is None: return None + match ft: + case LLVMType.Ptr(): + return ft.Pointee + case _: + return None + + # ============================================================ + # M3: await coro_sleep(ms) - suspend via the wake slot (coro_run polls to resume) + def _emit_await_sleep(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, node: ast.AST*) -> mlirlite.Value*: + """Emit await coro_sleep(ms): store the wake deadline in the frame and suspend the parent""" + i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + if node is None or trans is None or trans._coro_wake_ptr is None: return mlirlite.const_int32(pool, 0) + ms_val: mlirlite.Value* = mlirlite.const_int64(pool, 0) + aw_s: ast.Await* = ast.Await*(node) + if aw_s.value is not None and aw_s.value.kind() == ast.ASTKind.Call: + cl_s: ast.Call* = ast.Call*(aw_s.value) + if cl_s.args is not None and len(cl_s.args) > 0: + ms_v: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl_s.args.get(0), None, 0, trans) + if ms_v is not None: ms_val = ms_v + # GetTickCount64() + ms(i64) + now_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, "GetTickCount64") + if now_fn is None: now_fn = mlirlite.create_declare(pool, mod, "GetTickCount64", i64_ty) + if now_fn is None: return mlirlite.const_int32(pool, 0) + now_val: mlirlite.Value* = mlirlite.build_call(builder, "GetTickCount64", None, 0, i64_ty, 0) + if now_val is None: return mlirlite.const_int32(pool, 0) + wake_val: mlirlite.Value* = mlirlite.build_add(builder, now_val, ms_val) + if wake_val is None: return mlirlite.const_int32(pool, 0) + mlirlite.build_store(builder, wake_val, trans._coro_wake_ptr) + # parent suspend: new state id + br exit + position the resume block + new_state_s: int = trans._coro_next_state + trans._coro_next_state = new_state_s + 1 + st_v_s: mlirlite.Value* = mlirlite.const_int32(pool, new_state_s) + mlirlite.build_store(builder, st_v_s, trans._coro_state_ptr) + mlirlite.build_br(builder, trans._coro_exit_bb) + resume_bb_s: mlirlite.BasicBlock* = self._get_state_bb(trans, new_state_s) + if resume_bb_s is not None: mlirlite.position_at_end(builder, resume_bb_s) + return mlirlite.const_int32(pool, 0) + + # ============================================================ + # M4-M6: raise inside a coroutine (exception type/value -> frame exc slot) + def _handle_coro_raise(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, stmt: ast.AST*): + """Handle raise inside a coroutine: store the exception code/message, then jump to the handler or exit""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + exc_code: mlirlite.Value* = mlirlite.const_int32(pool, 1) + rz: ast.Raise* = ast.Raise*(stmt) + if rz is not None and rz.exc is not None: + code: int = self._resolve_raise_code(pool, rz.exc) + if code != 0: + exc_code = mlirlite.const_int32(pool, code) + # M7: exception message (first arg of Exception("msg")) + msg_v: mlirlite.Value* = self._resolve_raise_msg(builder, pool, mod, trans, rz.exc) + if msg_v is not None and trans._coro_msg_ptr is not None: mlirlite.build_store(builder, msg_v, trans._coro_msg_ptr) + else: + v: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rz.exc, None, 0, trans) + if v is not None: exc_code = v + if trans._coro_exc_ptr is not None: mlirlite.build_store(builder, exc_code, trans._coro_exc_ptr) + # jump to try/except or terminate + if trans._coro_try_stack_count > 0: + tbb_r: mlirlite.BasicBlock* = mlirlite.BasicBlock*(trans._coro_try_stack[trans._coro_try_stack_count - 1]) + mlirlite.build_br(builder, tbb_r) + else: + done_r: mlirlite.Value* = mlirlite.const_int32(pool, 1) + mlirlite.build_store(builder, done_r, trans._coro_done_ptr) + mlirlite.build_br(builder, trans._coro_exit_bb) + + def _resolve_raise_code(self, pool: memhub.MemBuddy*, exc_node: ast.AST*) -> int: + """Resolve the exception code of a raise target; return 0 for non-exception classes""" + if exc_node is None: return 0 + name: str = None + if exc_node.kind() == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(exc_node) + name = nm.id + elif exc_node.kind() == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(exc_node) + if cl.func is not None and cl.func.kind() == ast.ASTKind.Name: + nm2: ast.Name* = ast.Name*(cl.func) + name = nm2.id + elif exc_node.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(exc_node) + name = at.attr + if name is None: return 0 + return self.Gen.ClassGenBase.lookup_exception_code(name) + + def _resolve_raise_msg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, exc_node: ast.AST*) -> mlirlite.Value*: + """Return the exception message expression (first arg of Exception), or None""" + if exc_node is None or exc_node.kind() != ast.ASTKind.Call: return None + cl: ast.Call* = ast.Call*(exc_node) + if cl.args is None or len(cl.args) < 1: return None + return self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) + + # ============================================================ + # M5-M6: coroutine try/except (chained multi-except match + type codes + `as e` binding) + def _handle_coro_try(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): + """Handle try/except inside a coroutine: match handlers in order and propagate unmatched exceptions""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + tr: ast.Try* = ast.Try*(stmt) + if tr is None: return + nhandlers: sizet = 0 + if tr.handlers is not None: nhandlers = len(tr.handlers) + if nhandlers == 0: + err_t: str = pool.alloc(256) + if err_t is not None: + viperlib.snprintf(err_t, 256, "[coro-M6] try inside a coroutine requires an except clause") + self.Gen.TypeGenErr.fatal_error(stmt, err_t, "SyntaxError") + return + func: mlirlite.Function* = trans._cur_func + except_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.try_except") + merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.try_merge") + if except_bb is None or merge_bb is None: return + # push the try stack + if trans._coro_try_stack_count < HT.MAX_CORO_TRY: + trans._coro_try_stack[trans._coro_try_stack_count] = sizet(except_bb) + trans._coro_try_stack_count = trans._coro_try_stack_count + 1 + # try body + self._handle_segment(trans, builder, pool, mod, stmt.children) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) + if trans._coro_try_stack_count > 0: trans._coro_try_stack_count = trans._coro_try_stack_count - 1 + # ---- dispatch: match the exc code against each except in order ---- + mlirlite.position_at_end(builder, except_bb) + exc_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, trans._coro_exc_ptr) + # pre-build check/handler/unmatched blocks + check_bbs: sizet* = pool.alloc((nhandlers + 1) * 8) + handler_bbs: sizet* = pool.alloc(nhandlers * 8) + if check_bbs is None or handler_bbs is None: return + check_bbs[0] = sizet(except_bb) + hi: int = 0 + for hi in range(int(nhandlers)): + # [FIX 2026-08-31] build block names with snprintf: "%" is not string formatting here, + # so the old "%d" form produced i32 garbage and triggered a false int->ptr cast error. + chk_name: str = pool.alloc(64) + exc_name: str = pool.alloc(64) + if chk_name is None or exc_name is None: return + viperlib.snprintf(chk_name, 64, "coro.exc_check_%d", hi) + viperlib.snprintf(exc_name, 64, "coro.except_%d", hi) + check_bbs[hi + 1] = sizet(mlirlite.create_block(pool, func, chk_name)) + handler_bbs[hi] = sizet(mlirlite.create_block(pool, func, exc_name)) + unmatched_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.try_reraise") + codes_buf: int* = pool.alloc(256) + # per handler: the check chain + for hi in range(int(nhandlers)): + h_node: ast.AST* = tr.handlers.get(hi) + check_bb: mlirlite.BasicBlock* = (mlirlite.BasicBlock*)(check_bbs[hi]) + mlirlite.position_at_end(builder, check_bb) + catch_all: int = 0 + ncodes: int = 0 + if h_node is not None: + hd: ast.ExceptHandler* = ast.ExceptHandler*(h_node) + if hd.type is None: catch_all = 1 + else: + ename: str = self._exception_type_name(pool, hd.type) + if ename is not None and string.strcmp(ename, "Exception") == 0: catch_all = 1 + else: ncodes = self.Gen.ClassGenBase.collect_descendant_codes(ename, codes_buf, 64) + next_check: mlirlite.BasicBlock* = None + if hi + 1 < nhandlers: next_check = mlirlite.BasicBlock*(check_bbs[hi + 1]) + else: next_check = unmatched_bb + handler_bb: mlirlite.BasicBlock* = (mlirlite.BasicBlock*)(handler_bbs[hi]) + if catch_all != 0: mlirlite.build_br(builder, handler_bb) + else: + match_cond: mlirlite.Value* = None + ci: int = 0 + for ci in range(int(ncodes)): + eq_v: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, exc_val, mlirlite.const_int32(pool, codes_buf[ci])) + if match_cond is None: match_cond = eq_v + else: match_cond = mlirlite.build_or(builder, match_cond, eq_v) + if match_cond is None: mlirlite.build_br(builder, next_check) + else: mlirlite.build_cond_br(builder, match_cond, handler_bb, next_check) + # no match: keep propagating (terminate the coroutine again) + mlirlite.position_at_end(builder, unmatched_bb) + done_u: mlirlite.Value* = mlirlite.const_int32(pool, 1) + mlirlite.build_store(builder, done_u, trans._coro_done_ptr) + mlirlite.build_br(builder, trans._coro_exit_bb) + # ---- each except handler ---- + for hi in range(int(nhandlers)): + h_node2: ast.AST* = tr.handlers.get(hi) + handler_bb2: mlirlite.BasicBlock* = (mlirlite.BasicBlock*)(handler_bbs[hi]) + mlirlite.position_at_end(builder, handler_bb2) + if h_node2 is not None: + hd2: ast.ExceptHandler* = ast.ExceptHandler*(h_node2) + # `as e`: bind the exception code to e's slot (a local collected by _collect_local_names) + if hd2.name is not None and trans._coro_exc_ptr is not None: + e_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, hd2.name) + if e_slot is not None: + hv: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, trans._coro_exc_ptr) + mlirlite.build_store(builder, hv, e_slot) + # clear the frame exc after catching + z_clr: mlirlite.Value* = mlirlite.const_int32(pool, 0) + mlirlite.build_store(builder, z_clr, trans._coro_exc_ptr) + self._handle_segment(trans, builder, pool, mod, h_node2.children) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, merge_bb) + mlirlite.position_at_end(builder, merge_bb) + + def _exception_type_name(self, pool: memhub.MemBuddy*, type_node: ast.AST*) -> str: + """Return the except type name (Name/Attribute), or None if unavailable""" + if type_node is None: return None + if type_node.kind() == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(type_node) + return nm.id + if type_node.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(type_node) + return at.attr + return None + + # ============================================================ + # M7: coroutine try/finally (no except) + def _handle_coro_try_finally(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): + """Handle try/finally inside a coroutine: always run fbody, then re-propagate any pending exception""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + tr: ast.Try* = ast.Try*(stmt) + if tr is None: return + if tr.finalbody is not None and self._stmts_deep_await(tr.finalbody) != 0: + err_f: str = pool.alloc(256) + if err_f is not None: + viperlib.snprintf(err_f, 256, "[coro-M7] await is not yet supported inside finally") + self.Gen.TypeGenErr.fatal_error(stmt, err_f, "SyntaxError") + return + func: mlirlite.Function* = trans._cur_func + fin_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fin") + fin_exc_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fin_exc") + merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fin_merge") + if fin_bb is None or fin_exc_bb is None or merge_bb is None: return + # push the try stack (exceptions jump to finally) + if trans._coro_try_stack_count < HT.MAX_CORO_TRY: + trans._coro_try_stack[trans._coro_try_stack_count] = sizet(fin_bb) + trans._coro_try_stack_count = trans._coro_try_stack_count + 1 + self._handle_segment(trans, builder, pool, mod, stmt.children) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, fin_bb) + if trans._coro_try_stack_count > 0: trans._coro_try_stack_count = trans._coro_try_stack_count - 1 + # fin_bb: record the exception flag + run fbody + mlirlite.position_at_end(builder, fin_bb) + was_exc: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, mlirlite.build_load(builder, i32_ty, trans._coro_exc_ptr), mlirlite.const_int32(pool, 0)) + self._handle_segment(trans, builder, pool, mod, tr.finalbody) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_cond_br(builder, was_exc, fin_exc_bb, merge_bb) + # fin_exc: keep propagating the exception after finally + mlirlite.position_at_end(builder, fin_exc_bb) + done_f: mlirlite.Value* = mlirlite.const_int32(pool, 1) + mlirlite.build_store(builder, done_f, trans._coro_done_ptr) + mlirlite.build_br(builder, trans._coro_exit_bb) + mlirlite.position_at_end(builder, merge_bb) + + # ============================================================ + # M3: coroutine range-For (with await) turned into While (__f_idx/__f_len frame slots) + def _handle_coro_for(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*): + """Lower a range-for loop with await into a while loop over frame index/length slots""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + if self._is_range_for(stmt) == 0: + err_g: str = pool.alloc(256) + if err_g is not None: + viperlib.snprintf(err_g, 256, "[coro-M3] For inside a coroutine supports only range iteration (including await)") + self.Gen.TypeGenErr.fatal_error(stmt, err_g, "SyntaxError") + return + fn: ast.For* = ast.For*(stmt) + k: int = trans._coro_for_counter + trans._coro_for_counter = k + 1 + idx_idx: int = trans._coro_extra_base + trans._coro_for_slots[k] + len_idx: int = idx_idx + 1 + idx_slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, self._lookup_coro_frame_by_trans(trans), i32_ty, trans._coro_frame, idx_idx) + len_slot: mlirlite.Value* = mlirlite.build_gep_struct(builder, self._lookup_coro_frame_by_trans(trans), i32_ty, trans._coro_frame, len_idx) + if idx_slot is None or len_slot is None: return + # start/end (range 1 or 2 args) + cl: ast.Call* = ast.Call*(fn.iter) + start_val: mlirlite.Value* = None + end_val: mlirlite.Value* = None + nargs: int = int(len(cl.args)) + if nargs == 1: + start_val = mlirlite.const_int32(pool, 0) + end_val = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) + else: + start_val = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) + end_val = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(1), None, 0, trans) + if start_val is None: start_val = mlirlite.const_int32(pool, 0) + if end_val is None: end_val = mlirlite.const_int32(pool, 0) + mlirlite.build_store(builder, start_val, idx_slot) + mlirlite.build_store(builder, end_val, len_slot) + # loop structure + func: mlirlite.Function* = trans._cur_func + cond_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fcond") + body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fbody") + after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.fend") + if cond_bb is None or body_bb is None or after_bb is None: return + prev_break: mlirlite.BasicBlock* = trans._coro_break_bb + prev_cont: mlirlite.BasicBlock* = trans._coro_continue_bb + trans._coro_break_bb = after_bb + trans._coro_continue_bb = cond_bb + mlirlite.build_br(builder, cond_bb) + # condition block + mlirlite.position_at_end(builder, cond_bb) + idx_v: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, idx_slot) + len_v: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, len_slot) + cmp_v: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_SLT, idx_v, len_v) + mlirlite.build_cond_br(builder, cmp_v, body_bb, after_bb) + # loop body: target = idx; ; idx += 1 + mlirlite.position_at_end(builder, body_bb) + if fn.target is not None and fn.target.kind() == ast.ASTKind.Name: + tn: ast.Name* = ast.Name*(fn.target) + tgt_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, tn.id) + if tgt_slot is not None: + idx_v2: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, idx_slot) + mlirlite.build_store(builder, idx_v2, tgt_slot) + self._handle_segment(trans, builder, pool, mod, stmt.children) + if mlirlite.builder_cur_block_is_terminated(builder) == 0: + idx_v3: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, idx_slot) + one_v: mlirlite.Value* = mlirlite.const_int32(pool, 1) + nxt_v: mlirlite.Value* = mlirlite.build_add(builder, idx_v3, one_v) + mlirlite.build_store(builder, nxt_v, idx_slot) + mlirlite.build_br(builder, cond_bb) + trans._coro_break_bb = prev_break + trans._coro_continue_bb = prev_cont + mlirlite.position_at_end(builder, after_bb) + + # ============================================================ + # M3/M4/M7: coro.coro_destroy / coro_exception / coro_exception_msg / coro_gather + def translate_coro_destroy(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: + """coro.coro_destroy(frame): return the frame to the free-list pool""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) + if cl is None or trans is None or cl.args is None or len(cl.args) < 1: return mlirlite.const_int32(pool, 0) + async_name: str = self._var_to_async(trans, cl.args.get(0)) + if async_name is None: return mlirlite.const_int32(pool, 0) + frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) + if frame_ty is None: return mlirlite.const_int32(pool, 0) + frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) + frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) + if frame_val is None: return mlirlite.const_int32(pool, 0) + frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) + if frame_ptr is None: return mlirlite.const_int32(pool, 0) + # return to pool: frame's first 8 bytes hold the old head; head = this frame (i64) + pool_gv: mlirlite.GlobalVariable* = self._get_coro_pool(pool, mod, trans, async_name) + # GlobalVariable is not a Value: build a Value view (Ty=Ptr(i64), Name="@coro_pool_") + i64_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i64_ty) + pool_ref_name: str = pool.alloc(96) + if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, 96, "@coro_pool_%s", async_name) + pool_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i64_ptr_ty, pool_ref_name) + pool_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, pool_ref) + node_next: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_ptr, i64_ptr_ty) + mlirlite.build_store(builder, pool_head, node_next) + frame_i64: mlirlite.Value* = mlirlite.build_ptrtoint(builder, frame_ptr, i64_ty) + mlirlite.build_store(builder, frame_i64, pool_ref) + return mlirlite.const_int32(pool, 0) + + def translate_coro_exception(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: + """coro.coro_exception(frame): return the frame exception code (0 = none)""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + if cl is None or trans is None or cl.args is None or len(cl.args) < 1: return mlirlite.const_int32(pool, 0) + async_name: str = self._var_to_async(trans, cl.args.get(0)) + if async_name is None: return mlirlite.const_int32(pool, 0) + frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) + if frame_ty is None: return mlirlite.const_int32(pool, 0) + frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) + frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) + if frame_val is None: return mlirlite.const_int32(pool, 0) + frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) + if frame_ptr is None: return mlirlite.const_int32(pool, 0) + exc_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ptr, 3) + return mlirlite.build_load(builder, i32_ty, exc_ptr) + + def translate_coro_exception_msg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: + """coro.coro_exception_msg(frame): return the frame exception message pointer, or null""" + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + if cl is None or trans is None or cl.args is None or len(cl.args) < 1: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") + async_name: str = self._var_to_async(trans, cl.args.get(0)) + if async_name is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") + frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) + if frame_ty is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") + frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) + frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), None, 0, trans) + if frame_val is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") + frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) + if frame_ptr is None: return mlirlite.ConstNull(pool, i8_ptr_ty, "null") + msg_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i8_ptr_ty, frame_ptr, 4) + return mlirlite.build_load(builder, i8_ptr_ty, msg_ptr) + + def translate_coro_gather(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: + """coro.coro_gather(frames...): resume all coroutines to completion and return the finished count""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + if cl is None or trans is None or cl.args is None: return mlirlite.const_int32(pool, 0) + nb: sizet = len(cl.args) + j: sizet = 0 + for j in range(int(nb)): + arg0: ast.AST* = cl.args.get(j) + async_name: str = self._var_to_async(trans, arg0) + if async_name is None: continue + frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) + if frame_ty is None: continue + frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) + frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg0, None, 0, trans) + if frame_val is None: continue + frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) + if frame_ptr is None: continue + rn_buf: str = pool.alloc(64) + if rn_buf is not None: viperlib.snprintf(rn_buf, 64, "resume_%s", async_name) + resume_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rn_buf, 0) + if self.Gen.CallGenBase.find_func_in_module(mod, resume_mangled) is None: continue + func: mlirlite.Function* = trans._cur_func + loop_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "gather.loop") + after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "gather.after") + if loop_bb is None or after_bb is None: continue + mlirlite.build_br(builder, loop_bb) + mlirlite.position_at_end(builder, loop_bb) + frame_ptr.Next = None + status: mlirlite.Value* = mlirlite.build_call(builder, resume_mangled, frame_ptr, 1, i32_ty, 0) + done_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, status, mlirlite.const_int32(pool, 0)) + mlirlite.build_cond_br(builder, done_cond, after_bb, loop_bb) + mlirlite.position_at_end(builder, after_bb) + return mlirlite.const_int32(pool, 0) + + def _var_to_async(self, trans: HT.Translator*, arg0: ast.AST*) -> str: + """Map a variable node to its async function name, or None if not found""" + if trans is None or arg0 is None or arg0.kind() != ast.ASTKind.Name: return None + nm: ast.Name* = ast.Name*(arg0) + if nm.id is None: return None + return self._lookup_coro_var(trans, nm.id) + + # ============================================================ + # await expression translation (shared by segment tail / translate_value Await branch) + def _emit_await(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, node: ast.AST*, call_name: str) -> mlirlite.Value*: + """Inline-translate await async_fn(args...): create the child frame, resume it to DONE, store its result""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i8_ptr_ty_a: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + if node is None or call_name is None or trans is None: return mlirlite.const_int32(pool, 0) + # M3: await coro_sleep(ms) - suspend until the wake time (frame wake slot) + if string.strcmp(call_name, "coro_sleep") == 0: return self._emit_await_sleep(builder, pool, mod, trans, node) + child_frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, call_name) + if child_frame_ty is None: return mlirlite.const_int32(pool, 0) + child_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, child_frame_ty) + + cc_buf: str = pool.alloc(64) + if cc_buf is not None: viperlib.snprintf(cc_buf, 64, "create_%s", call_name) + create_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, cc_buf, 0) + rc_buf: str = pool.alloc(64) + if rc_buf is not None: viperlib.snprintf(rc_buf, 64, "resume_%s", call_name) + resume_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rc_buf, 0) + if self.Gen.CallGenBase.find_func_in_module(mod, create_mangled) is None or self.Gen.CallGenBase.find_func_in_module(mod, resume_mangled) is None: + # async functions must be defined first (no forward refs); silently return 0 + return mlirlite.const_int32(pool, 0) + + # M2: child-call args (await fn(a, b) -> create_fn(a, b)) + args_head: mlirlite.Value* = None + args_tail: mlirlite.Value* = None + args_count: int = 0 + aw_n: ast.Await* = ast.Await*(node) + if aw_n.value is not None and aw_n.value.kind() == ast.ASTKind.Call: + cl_a: ast.Call* = ast.Call*(aw_n.value) + if cl_a.args is not None: + nb: sizet = len(cl_a.args) + j: sizet = 0 + for j in range(int(nb)): + av: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl_a.args.get(j), None, 0, trans) + if av is None: av = mlirlite.const_int32(pool, 0) + av.Next = None + if args_head is None: + args_head = av + args_tail = av + else: + args_tail.Next = av + args_tail = av + args_count = args_count + 1 + + # child create (M2 with args) + resume + child: mlirlite.Value* = mlirlite.build_call(builder, create_mangled, args_head, args_count, child_ptr_ty, 0) + if child is None: return mlirlite.const_int32(pool, 0) + child.Next = None + mlirlite.build_call(builder, resume_mangled, child, 1, i32_ty, 0) + # GEP the child ret slot [0,2] and load + child_ret_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, child_frame_ty, i32_ty, child, 2) + child_ret: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, child_ret_ptr) + # M4: child exception propagation - child exc != 0 -> parent exc/msg + jump try or terminate + child_exc_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, child_frame_ty, i32_ty, child, 3) + child_exc: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, child_exc_ptr) + zero32_x: mlirlite.Value* = mlirlite.const_int32(pool, 0) + exc_ok_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, trans._cur_func, "coro.ok") + exc_bad_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, trans._cur_func, "coro.exc") + if exc_ok_bb is None or exc_bad_bb is None: return mlirlite.const_int32(pool, 0) + exc_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, child_exc, zero32_x) + mlirlite.build_cond_br(builder, exc_cond, exc_ok_bb, exc_bad_bb) + # exc path: parent exc/msg slots + jump to try or terminate + mlirlite.position_at_end(builder, exc_bad_bb) + if trans._coro_exc_ptr is not None: mlirlite.build_store(builder, child_exc, trans._coro_exc_ptr) + if trans._coro_msg_ptr is not None: + child_msg_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, child_frame_ty, i8_ptr_ty_a, child, 4) + child_msg: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty_a, child_msg_ptr) + mlirlite.build_store(builder, child_msg, trans._coro_msg_ptr) + if trans._coro_try_stack_count > 0: + tbb: mlirlite.BasicBlock* = mlirlite.BasicBlock*(trans._coro_try_stack[trans._coro_try_stack_count - 1]) + mlirlite.build_br(builder, tbb) + else: + done_x: mlirlite.Value* = mlirlite.const_int32(pool, 1) + mlirlite.build_store(builder, done_x, trans._coro_done_ptr) + mlirlite.build_br(builder, trans._coro_exit_bb) + # normal path: store into the parent await slot (if any) + suspend + mlirlite.position_at_end(builder, exc_ok_bb) + slot: mlirlite.Value* = trans._coro_await_slot + if slot is not None: mlirlite.build_store(builder, child_ret, slot) + # parent suspend: new state id (matching the pre-scan DFS order) + new_state: int = trans._coro_next_state + trans._coro_next_state = new_state + 1 + st_v: mlirlite.Value* = mlirlite.const_int32(pool, new_state) + mlirlite.build_store(builder, st_v, trans._coro_state_ptr) + mlirlite.build_br(builder, trans._coro_exit_bb) + # resume block state{new_state}: position immediately (pre-built case; translation continues) + resume_bb: mlirlite.BasicBlock* = self._get_state_bb(trans, new_state) + if resume_bb is not None: mlirlite.position_at_end(builder, resume_bb) + return child_ret + + # ============================================================ + # coro.coro_run(frame) inline translation + def translate_coro_run(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*: + """Inline-translate coro.coro_run(frame): resolve the async name, resume to DONE, and return the result""" + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + if cl is None or trans is None: return mlirlite.const_int32(pool, 0) + if cl.args is None or len(cl.args) < 1: return mlirlite.const_int32(pool, 0) + arg0: ast.AST* = cl.args.get(0) + if arg0 is None or arg0.kind() != ast.ASTKind.Name: return mlirlite.const_int32(pool, 0) + nm: ast.Name* = ast.Name*(arg0) + if nm.id is None: return mlirlite.const_int32(pool, 0) + async_name: str = self._lookup_coro_var(trans, nm.id) + if async_name is None: return mlirlite.const_int32(pool, 0) + frame_ty: mlirlite.LLVMType* = self._lookup_coro_frame(trans, async_name) + if frame_ty is None: return mlirlite.const_int32(pool, 0) + frame_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty) + + # frame variable value (i8* / Ptr(frame)) -> bitcast to Ptr(frame) + frame_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg0, None, 0, trans) + if frame_val is None: return mlirlite.const_int32(pool, 0) + frame_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_val, frame_ptr_ty) + if frame_ptr is None: return mlirlite.const_int32(pool, 0) + + func: mlirlite.Function* = trans._cur_func + if func is None: return mlirlite.const_int32(pool, 0) + rn_buf: str = pool.alloc(64) + if rn_buf is not None: viperlib.snprintf(rn_buf, 64, "resume_%s", async_name) + resume_mangled: str = self.Gen.FuncGenBase._mangle_func_name(trans, rn_buf, 0) + if self.Gen.CallGenBase.find_func_in_module(mod, resume_mangled) is None: return mlirlite.const_int32(pool, 0) + + loop_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.loop") + check_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.check") + after_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.after") + if loop_bb is None or check_bb is None or after_bb is None: return mlirlite.const_int32(pool, 0) + mlirlite.build_br(builder, loop_bb) + mlirlite.position_at_end(builder, loop_bb) + frame_ptr.Next = None + status: mlirlite.Value* = mlirlite.build_call(builder, resume_mangled, frame_ptr, 1, i32_ty, 0) + zero_cmp: mlirlite.Value* = mlirlite.const_int32(pool, 0) + done_cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, status, zero_cmp) + mlirlite.build_cond_br(builder, done_cond, after_bb, check_bb) + # M3: SUSPEND -> wake polling (continue if GetTickCount64 >= wake or wake==0, else Sleep(1)) + mlirlite.position_at_end(builder, check_bb) + wake_idx_r: int = self._lookup_coro_wake(trans, async_name) + if wake_idx_r < 0: mlirlite.build_br(builder, loop_bb) + else: + i64_ty_r2: mlirlite.LLVMType* = mlirlite.Int64(pool) + wake_ptr_r: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i64_ty_r2, frame_ptr, wake_idx_r) + wake_v_r: mlirlite.Value* = mlirlite.build_load(builder, i64_ty_r2, wake_ptr_r) + now_v_r: mlirlite.Value* = mlirlite.build_call(builder, "GetTickCount64", None, 0, i64_ty_r2, 0) + not_wake_r: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, wake_v_r, mlirlite.const_int64(pool, 0)) + due_r: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_UGE, now_v_r, wake_v_r) + cont_r: mlirlite.Value* = mlirlite.build_or(builder, not_wake_r, due_r) + wsleep_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wsleep") + wdue_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "coro.wdue") + mlirlite.build_cond_br(builder, cont_r, wdue_bb, wsleep_bb) + mlirlite.position_at_end(builder, wsleep_bb) + sleep_one: mlirlite.Value* = mlirlite.const_int32(pool, 1) + mlirlite.build_call(builder, "Sleep", sleep_one, 1, mlirlite.Void(pool), 0) + mlirlite.build_br(builder, loop_bb) + mlirlite.position_at_end(builder, wdue_bb) + mlirlite.build_br(builder, loop_bb) + mlirlite.position_at_end(builder, after_bb) + ret_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, frame_ty, i32_ty, frame_ptr, 2) + ret_val: mlirlite.Value* = mlirlite.build_load(builder, i32_ty, ret_ptr) + return ret_val + + # ============================================================ + # Public queries / registration + def is_async_func(self, trans: HT.Translator*, name: str) -> int: + """Return whether name is registered as an async function (1 = yes, 0 = no)""" + if trans is None or name is None or trans._async_funcs is None: return 0 + cnt: int = trans._async_func_count + i: int = 0 + for i in range(int(cnt)): + addr: uint64 = uint64(trans._async_funcs) + i * 8 + slot: str* = (str*)(addr) + if slot[0] is not None and string.strcmp(slot[0], name) == 0: return 1 + return 0 + + def register_coro_var(self, trans: HT.Translator*, var_name: str, func_name: str) -> int: + """Record the variable-name to async-function-name mapping used by coro.coro_run""" + if trans is None or var_name is None or func_name is None: return 1 + if trans._coro_var_async is None: return 1 + cnt: int = trans._coro_var_async_count + i: int = 0 + for i in range(int(cnt)): + addr: uint64 = uint64(trans._coro_var_async) + i * 16 + pair: str* = (str*)(addr) + if pair[0] is not None and string.strcmp(pair[0], var_name) == 0: return 0 + if cnt >= HT.MAX_CORO_VARS: return 1 + pair2_addr: uint64 = uint64(trans._coro_var_async) + cnt * 16 + pair2: str* = (str*)(pair2_addr) + pair2[0] = var_name + pair2[1] = func_name + trans._coro_var_async_count = cnt + 1 + return 0 + + def _register_async_func(self, trans: HT.Translator*, name: str) -> int: + """Register an async function bare name in the _async_funcs table""" + if trans is None or name is None or trans._async_funcs is None: return 1 + cnt: int = trans._async_func_count + i: int = 0 + for i in range(int(cnt)): + addr: uint64 = uint64(trans._async_funcs) + i * 8 + slot: str* = (str*)(addr) + if slot[0] is not None and string.strcmp(slot[0], name) == 0: return 0 + if cnt >= HT.MAX_CORO_FUNCS: return 1 + addr2: uint64 = uint64(trans._async_funcs) + cnt * 8 + slot2: str* = (str*)(addr2) + slot2[0] = name + trans._async_func_count = cnt + 1 + return 0 + + def _register_coro_frame(self, trans: HT.Translator*, name: str, frame_ty: mlirlite.LLVMType*) -> int: + """Register an async name to frame type in the _coro_frames table""" + if trans is None or name is None or frame_ty is None: return 1 + if trans._coro_frames is None: return 1 + cnt: int = trans._coro_frame_count + i: int = 0 + for i in range(int(cnt)): + addr: uint64 = uint64(trans._coro_frames) + i * 16 + pair: str* = (str*)(addr) + if pair[0] is not None and string.strcmp(pair[0], name) == 0: + pair[1] = str(frame_ty) + return 0 + if cnt >= HT.MAX_CORO_FUNCS: return 1 + pair2_addr: uint64 = uint64(trans._coro_frames) + cnt * 16 + pair2: str* = (str*)(pair2_addr) + pair2[0] = name + pair2[1] = str(frame_ty) + trans._coro_frame_count = cnt + 1 + return 0 + + def _lookup_coro_frame(self, trans: HT.Translator*, name: str) -> mlirlite.LLVMType*: + """Look up the frame type by async name, or None if not found""" + if trans is None or name is None or trans._coro_frames is None: return None + cnt: int = trans._coro_frame_count + i: int = 0 + for i in range(int(cnt)): + addr: uint64 = uint64(trans._coro_frames) + i * 16 + pair: str* = (str*)(addr) + if pair[0] is not None and string.strcmp(pair[0], name) == 0: + if pair[1] is not None: + ty_addr: sizet = sizet(pair[1]) + return (mlirlite.LLVMType*)(ty_addr) + return None + return None + + def _lookup_coro_var(self, trans: HT.Translator*, var_name: str) -> str: + """Reverse-look-up the async function name by variable name, or None if not found""" + if trans is None or var_name is None or trans._coro_var_async is None: return None + cnt: int = trans._coro_var_async_count + i: int = 0 + for i in range(int(cnt)): + addr: uint64 = uint64(trans._coro_var_async) + i * 16 + pair: str* = (str*)(addr) + if pair[0] is not None and string.strcmp(pair[0], var_name) == 0: return pair[1] + return None + + # ============================================================ + # Segment-translation helpers + def _emit_coro_return(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, stmt: ast.AST*) -> None: + """Special-case Return inside a coroutine: store the return slot, set done, and branch to exit""" + rv_node: ast.Return* = ast.Return*(stmt) + ret_val: mlirlite.Value* = None + if rv_node is not None and rv_node.value is not None: + if rv_node.value.kind() != ast.ASTKind.Await: ret_val = self.Gen.ExprGen.translate_value(builder, pool, mod, rv_node.value, None, 0, trans) + if ret_val is None: ret_val = mlirlite.const_int32(pool, 0) + mlirlite.build_store(builder, ret_val, trans._coro_ret_ptr) + done_one: mlirlite.Value* = mlirlite.const_int32(pool, 1) + mlirlite.build_store(builder, done_one, trans._coro_done_ptr) + mlirlite.build_br(builder, trans._coro_exit_bb) + + def _expr_await_is_sleep(self, aw: ast.Await*) -> int: + """Return whether an Await target is coro_sleep (Name or coro.coro_sleep)""" + if aw is None or aw.value is None: return 0 + if aw.value.kind() != ast.ASTKind.Call: return 0 + cl: ast.Call* = ast.Call*(aw.value) + if cl.func is None: return 0 + if cl.func.kind() == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(cl.func) + if nm.id is not None and string.strcmp(nm.id, "coro_sleep") == 0: return 1 + elif cl.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.attr is not None and string.strcmp(at.attr, "coro_sleep") == 0: return 1 + return 0 + + def _is_await_tail(self, stmt: ast.AST*) -> int: + """Return whether a statement is a segment tail, i.e. its value is directly an Await""" + if stmt is None: return 0 + sk: int = stmt.kind() + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + if aa.value is not None and aa.value.kind() == ast.ASTKind.Await: return 1 + elif sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + if as_.value is not None and as_.value.kind() == ast.ASTKind.Await: + if as_.targets is not None and len(as_.targets) == 1: return 1 + elif sk == ast.ASTKind.Expr: + e: ast.Expr* = ast.Expr*(stmt) + if e.value is not None and e.value.kind() == ast.ASTKind.Await: return 1 + return 0 + + def _get_assign_target_name(self, stmt: ast.AST*) -> str: + """Return the target name of an AnnAssign/Assign to a single Name, or None""" + if stmt is None: return None + sk: int = stmt.kind() + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + if aa.target is not None and aa.target.kind() == ast.ASTKind.Name: + tn: ast.Name* = ast.Name*(aa.target) + return tn.id + elif sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + if as_.targets is not None and len(as_.targets) == 1: + t0: ast.AST* = as_.targets.get(0) + if t0 is not None and t0.kind() == ast.ASTKind.Name: + tn2: ast.Name* = ast.Name*(t0) + return tn2.id + return None + + def _get_await_value(self, stmt: ast.AST*) -> ast.Await*: + """Return the Await value node of a segment-tail statement, or None""" + if stmt is None: return None + sk: int = stmt.kind() + if sk == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(stmt) + if aa.value is not None and aa.value.kind() == ast.ASTKind.Await: return ast.Await*(aa.value) + elif sk == ast.ASTKind.Assign: + as_: ast.Assign* = ast.Assign*(stmt) + if as_.value is not None and as_.value.kind() == ast.ASTKind.Await: return ast.Await*(as_.value) + elif sk == ast.ASTKind.Expr: + ex: ast.Expr* = ast.Expr*(stmt) + if ex.value is not None and ex.value.kind() == ast.ASTKind.Await: return ast.Await*(ex.value) + return None + + def _extract_await_call_name(self, aw_node: ast.Await*) -> str: + """Extract the async function name or coro_sleep from an Await call, or None""" + if aw_node is None or aw_node.value is None: return None + if aw_node.value.kind() == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(aw_node.value) + if cl.func is not None: + if cl.func.kind() == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(cl.func) + return nm.id + if cl.func.kind() == ast.ASTKind.Attribute: + # coro.coro_sleep(ms): M3 sleep suspension + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.attr is not None and string.strcmp(at.attr, "coro_sleep") == 0: return "coro_sleep" + return None + + def _read_local_name(self, trans: HT.Translator*, idx: int) -> str: + """Return the idx-th local variable name from trans._coro_locals, or None if out of range""" + if trans is None or trans._coro_locals is None: return None + addr: uint64 = uint64(trans._coro_locals) + idx * 8 + slot: str* = (str*)(addr) + return slot[0] diff --git a/App/lib/core/Gen/Expr/ExprGenCdef.vp b/App/lib/core/Gen/Expr/ExprGenCdef.vp index 1436402..5dbc2f1 100644 --- a/App/lib/core/Gen/Expr/ExprGenCdef.vp +++ b/App/lib/core/Gen/Expr/ExprGenCdef.vp @@ -1,882 +1,882 @@ -import t, c -from stdint import * -import ast -import mlirlite -import memhub -import string -import stdio -import stdlib -import viperlib -import win32.fileio as fileio -import lib.Stub.StubMerger as StubMerger -import lib.core.VLogger as VLogger -import lib.Projectrans.Config as Config - -import lib.core.Gen.Expr.ExprGenBase as ExprGenBase -import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect -import lib.Stub.StubMerger.StubMergerStore as StubMergerStore - - -# ============================================================ -# ExprGenCdef - ExprGenMixin split (stage D): cross-module CDefine lookup -novirtual class ExprGenCdef(ExprGenBase.ExprGenBase): - """Cross-module CDefine lookup""" - - # _build_source_path_from_sha1: build a source .vp/.py path from SHA1 via the StubMerger - # global store (.vpi missing -> source .py); "lib/..." -> SourceDir, else IncludesDir. - def _build_source_path_from_sha1(self, sha1: str) -> str: - """Build a source .py path from a SHA1 via the global store""" - if sha1 is None: return None - if Config.IncludesDir is None: return None - # get the rel-path array from the StubMerger global store - rel_arr_ptr: bytes = StubMergerStore.GetSha1StoreRelArrPtr() - count: int = StubMergerStore.GetSha1StoreCount() - if rel_arr_ptr is None or count <= 0: return None - sha1_arr_ptr: bytes = StubMergerStore.GetSha1StoreArrPtr() - if sha1_arr_ptr is None: return None - # iterate to find the matching SHA1 - idx_s: int = -1 - for i in range(int(count)): - sidx: sizet = sizet(i) * 17 - cur_sha1: str = sha1_arr_ptr + sidx - if cur_sha1[0] == '\0': continue - if string.strcmp(cur_sha1, sha1) == 0: - idx_s = i - break - if idx_s < 0: return None - # get the rel path (MAX_REL_PATH_LEN=256, avoids cross-module CDefine issues) - MAX_RPL: sizet = 256 - ridx: sizet = sizet(idx_s) * MAX_RPL - rel_path: str = rel_arr_ptr + ridx - if rel_path[0] == '\0': return None - # choose base dir: 1) stored includes root IncDir (absolute, supports multiple dirs); - # 2) prefix heuristic: "lib/" -> SourceDir, "includes/" -> IncludesDir, else SourceDir then IncludesDir. - base_dir: str = None - rel_used: str = rel_path - inc_arr_ptr: bytes = StubMergerStore.GetSha1StoreIncdirArrPtr() - if inc_arr_ptr is not None: - iidx: sizet = sizet(idx_s) * StubMerger.MAX_INC_DIR_LEN - inc_dir: str = inc_arr_ptr + iidx - if inc_dir[0] != '\0': base_dir = inc_dir - if base_dir is None and string.strlen(rel_path) >= 4: - if (rel_path[0] == 'l' and rel_path[1] == 'i' and rel_path[2] == 'b' and rel_path[3] == '/'): - base_dir = Config.SourceDir if Config.SourceDir is not None else Config.IncludesDir - if base_dir is None and string.strlen(rel_path) >= 9: - if (rel_path[0] == 'i' and rel_path[1] == 'n' and rel_path[2] == 'c' and rel_path[3] == 'l' and rel_path[4] == 'u' and rel_path[5] == 'd' and rel_path[6] == 'e' and rel_path[7] == 's' and rel_path[8] == '/'): - rel_used = rel_path + 9 - base_dir = Config.IncludesDir - if base_dir is None: base_dir = Config.SourceDir if Config.SourceDir is not None else Config.IncludesDir - # build full path: {base_dir}/{rel_used} - base_len: sizet = string.strlen(base_dir) - rel_len: sizet = string.strlen(rel_used) - path_len: sizet = base_len + rel_len + 2 - buf: str = stdlib.malloc(path_len) - if buf is None: return None - viperlib.snprintf(buf, path_len, "%s/%s", base_dir, rel_used) - # existence check: if missing under SourceDir and IncludesDir differs, retry IncludesDir - pf_chk: fileio.File* = fileio.File(buf, fileio.MODE.R) - if pf_chk.closed: - pf_chk.close() - if Config.IncludesDir is not None and base_dir != Config.IncludesDir: - stdlib.free(buf) - base_len2: sizet = string.strlen(Config.IncludesDir) - path_len2: sizet = base_len2 + rel_len + 2 - buf = stdlib.malloc(path_len2) - if buf is None: return None - viperlib.snprintf(buf, path_len2, "%s/%s", Config.IncludesDir, rel_used) - pf_chk2: fileio.File* = fileio.File(buf, fileio.MODE.R) - if pf_chk2.closed: - pf_chk2.close() - stdlib.free(buf) - return None - pf_chk2.close() - else: - stdlib.free(buf) - return None - else: pf_chk.close() - return buf - # _lookup_cross_module_cdefine - cross-module CDefine lookup: resolve the source module - # from from_imports, parse its pyi "NAME: t.CDefine = value", then cache in the current module. - def _lookup_cross_module_cdefine(self, pool: memhub.MemBuddy*, name: str, from_imports: str, imported_modules: str = None, depth: int = 0, node: ast.AST* = None) -> int64: - """Look up a CDefine constant across modules, returning its 64-bit value or -1""" - # recursion-depth guard: re-export chains + imported_modules walk can loop and OOM; - # cap at 32 to fail fast on cyclic imports (error carries node line + module chain). - if depth > 32: - err_buf: str = pool.alloc(256) - if err_buf is not None: - viperlib.snprintf(err_buf, 256, "cross-module CDefine lookup recursion too deep (suspected module import/re-export cycle): %s (depth=%d fi=%s)", name, depth, from_imports if from_imports is not None else "(nil)") - self.Gen.TypeGenErr.fatal_error(node, err_buf, "InternalCompilerError") - self.Gen.TypeGenErr.fatal_error(node, "cross-module CDefine lookup recursion too deep (suspected module import/re-export cycle)", "InternalCompilerError") - # clear the found flag at entry so every early return reports "not found", - # avoiding a stale success flag left over from a previous recursive call. - self.Gen.TypeGenCDef.set_cdefine_found(0) - if name is None: return -1 - if from_imports is None: from_imports = "" - - # 1. resolve the module name for `name` from from_imports; try exact match first - # (allow_star_fallback=0) so a star import cannot mislead CDefine constants. - mod_name_raw: str = self.Gen.ImportsGen.lookup_from_import(from_imports, name, 0) - if mod_name_raw is None: - # exact match failed: try the star import fallback - mod_name_raw = self.Gen.ImportsGen.lookup_from_import(from_imports, name, 1) - - # truncate module name at NUL: lookup_from_import returns a pointer into from_imports - # (not NUL-terminated); match _lookup_sha1_from_modname_raw (space/colon/NUL delimited). - base_mod: str = None - if mod_name_raw is not None: - mbl: sizet = 0 - while mod_name_raw[mbl] != ' ' and mod_name_raw[mbl] != '\0' and mod_name_raw[mbl] != ':': mbl += 1 - mb_buf: str = pool.alloc(mbl + 1) - if mb_buf is not None: - mi2: sizet - for mi2 in range(int(mbl)): mb_buf[mi2] = mod_name_raw[mi2] - mb_buf[mbl] = '\0' - base_mod = mb_buf - while base_mod is not None and base_mod[0] == '.': base_mod = base_mod + 1 - - # cross-module CDefine cache (perm_pool, reused across files): key = "name@srcModule" - # keeps modules isolated so same-name constants from different modules do not cross. - xkey_buf: str = None - if base_mod is not None: - xk_len: sizet = string.strlen(name) + string.strlen(base_mod) + 2 - xkey_buf = pool.alloc(xk_len) - if xkey_buf is not None: - viperlib.snprintf(xkey_buf, xk_len, "%s@%s", name, base_mod) - xval: str = self.Gen.TypeGenCDef.lookup_xmod_cdefine(xkey_buf) - if self.Gen.TypeGenCDef.is_cdefine_found() != 0: - if self.Gen.TypeGenCDef.is_cdefine_str() != 0: self.Gen.TypeGenCDef.register_cdefine_string(pool, name, xval) - elif self.Gen.TypeGenCDef.is_cdefine_double() != 0: - xd_v: double = string.atof(xval) - self.Gen.TypeGenCDef.register_cdefine_double(pool, name, xd_v) - else: - xv_i: int64 = string.atoll(xval) - self.Gen.TypeGenCDef.register_cdefine_constant(pool, name, xv_i) - return string.atoll(xval) - - # 2. from_imports failed: fall back to the imported_modules walk (step 9.6); - # a star fallback may name a wrong module, so keep scanning imported_modules. - if mod_name_raw is None: - # from_imports matched nothing (exact and star both failed): - # go straight to the imported_modules walk (step 9.6). - if imported_modules is None: return -1 - # skip reading the from_imports module; go directly to the step 9.6 walk. - # result_found=0 skips steps 3-9 and jumps to 9.6. - self.Gen.TypeGenCDef.set_cdefine_found(0) - # iterate imported_modules directly - im_len: sizet = string.strlen(imported_modules) - im_ci: sizet = 0 - while im_ci < im_len: - while im_ci < im_len and imported_modules[im_ci] == ' ': im_ci += 1 - if im_ci >= im_len: break - im_start: sizet = im_ci - while im_ci < im_len and imported_modules[im_ci] != ' ': im_ci += 1 - im_end: sizet = im_ci - im_nlen: sizet = im_end - im_start - if im_nlen > 0 and im_nlen < 256: - im_buf: str = pool.alloc(im_nlen + 1) - if im_buf is not None: - for mk in range(int(im_nlen)): - im_buf[mk] = imported_modules[im_start + mk] - im_buf[im_nlen] = '\0' - sub_fi: str = pool.alloc(im_nlen + 3) - if sub_fi is not None: - sub_fi[0] = '*' - sub_fi[1] = ':' - for mk2 in range(int(im_nlen)): - sub_fi[2 + mk2] = im_buf[mk2] - sub_fi[2 + im_nlen] = '\0' - sub_val: int64 = self._lookup_cross_module_cdefine(pool, name, sub_fi, None, depth + 1, node) - if self.Gen.TypeGenCDef.is_cdefine_found() != 0: return sub_val - return -1 - - # truncate module name, strip leading dots, find SHA1 via exact+suffix (shared logic) - sha1: str = self.Gen.CallGenBase._lookup_sha1_from_modname_raw(pool, mod_name_raw) - if sha1 is None: return -1 - - # 5. get temp_dir - temp_dir: str = self.Gen.TypeGenBase.get_temp_dir() - if temp_dir is None: return -1 - - # 6. build the vpi file path - td_len: sizet = string.strlen(temp_dir) - pyi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1, "vpi") - if pyi_path is None: return -1 - - # 7. read vpi; if missing or too short (empty stub), fall back to the source .vp/.py - PYI_READ_BUF_SIZE: sizet = 65536 - pyi_buf: bytes = stdlib.malloc(PYI_READ_BUF_SIZE) - if pyi_buf is None: - stdlib.free(pyi_path) - return -1 - pf: fileio.File* = fileio.File(pyi_path, fileio.MODE.R) - need_py_fallback: int = 0 - bytes_read: LONG = 0 - if pf.closed: - # .vpi missing: fall back to the source .vp/.py - need_py_fallback = 1 - else: - bytes_read = pf.read_all(pyi_buf, PYI_READ_BUF_SIZE) - pf.close() - # vpi too short (< 10 bytes): treat as empty stub, fall back to .py - if bytes_read < 10: need_py_fallback = 1 - else: - if bytes_read < PYI_READ_BUF_SIZE: pyi_buf[bytes_read] = 0 - # vpi stubs may declare a CDefine name without a value; the real value lives - # in the source .vp/.py, so trust vpi only when the target name's line has a value. - cd_sig: int = 0 - name_has_val: int = 0 - cs_name_len: sizet = string.strlen(name) - cs_total: sizet = string.strlen(pyi_buf) - cs_i: sizet = 0 - while cs_i + 9 < cs_total: - if (pyi_buf[cs_i] == 't' and pyi_buf[cs_i + 1] == '.' and pyi_buf[cs_i + 2] == 'C' and pyi_buf[cs_i + 3] == 'D' and pyi_buf[cs_i + 4] == 'e' and pyi_buf[cs_i + 5] == 'f' and pyi_buf[cs_i + 6] == 'i' and pyi_buf[cs_i + 7] == 'n' and pyi_buf[cs_i + 8] == 'e'): - # check whether the rest of the line has '=' (only valued decls match in vpi) - cd_eq: int = 0 - cs_j: sizet = cs_i + 9 - while cs_j < cs_total and pyi_buf[cs_j] != '\n': - if pyi_buf[cs_j] == '=': - cd_eq = 1 - break - cs_j += 1 - if cd_eq != 0: - cd_sig = 1 - # locate line start; check it begins with "name:" (target has a value) - cs_line_start: sizet = cs_i - while cs_line_start > 0 and pyi_buf[cs_line_start - 1] != '\n': cs_line_start -= 1 - if cs_i - cs_line_start >= cs_name_len + 1: - nm_ok: int = 1 - for ki2 in range(int(cs_name_len)): - if pyi_buf[cs_line_start + ki2] != name[ki2]: - nm_ok = 0 - break - if nm_ok == 1 and pyi_buf[cs_line_start + cs_name_len] == ':': name_has_val = 1 - cs_i += 1 - if cd_sig == 0 or name_has_val == 0: need_py_fallback = 1 - stdlib.free(pyi_path) - - if need_py_fallback != 0: - # look up the rel path from the SHA1 store; build {base_dir}/{rel_path} - src_path: str = self._build_source_path_from_sha1(sha1) - if src_path is None: - # SHA1 absent from the store = compiler bug (Phase1/Phase2 not populated): - # report and abort instead of silently returning -1 (avoids bad @UNDEFINED refs). - err_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENCDEF_263) - if err_buf is not None: - viperlib.snprintf(err_buf, 512, "cross-module CDefine lookup failed: SHA1 %s is not in the store (base_mod=%s name=%s). " "Phase1/Phase2 did not correctly populate PopulateSha1MapStore/AppendToSha1MapStore", sha1, base_mod, name) - VLogger.error(err_buf, "XMOD-CD") - stdlib.free(pyi_buf) - return -1 - pf = fileio.File(src_path, fileio.MODE.R) - stdlib.free(src_path) - if pf.closed: - stdlib.free(pyi_buf) - return -1 - bytes_read = pf.read_all(pyi_buf, PYI_READ_BUF_SIZE) - pf.close() - if bytes_read <= 0: - stdlib.free(pyi_buf) - return -1 - if bytes_read < PYI_READ_BUF_SIZE: pyi_buf[bytes_read] = 0 - - # 8. search pyi_buf for the "name: t.CDefine = value" line - # format: NAME: t.CDefine = value - name_len: sizet = string.strlen(name) - total_len: sizet = string.strlen(pyi_buf) - pos: sizet = 0 - result_val: int64 = 0 - result_found: int = 0 - res_is_double: int = 0 - res_is_str: int = 0 - while pos < total_len: - # find line start - line_start: sizet = pos - # find line end - while pos < total_len and pyi_buf[pos] != '\n': pos += 1 - line_len: sizet = pos - line_start - if pos < total_len: - pos += 1 # skip '\n' - - # check whether the line starts with "name:" - if line_len < name_len + 1: continue - match_ok: int = 1 - for ki in range(int(name_len)): - if pyi_buf[line_start + ki] != name[ki]: - match_ok = 0 - break - if match_ok == 0: continue - if pyi_buf[line_start + name_len] != ':': continue - - # check the line contains "t.CDefine" - # scan the line for "t.CDefine" - has_cdefine: int = 0 - ci: sizet = line_start + name_len + 1 - while ci + 8 <= line_start + line_len: - if (pyi_buf[ci] == 't' and pyi_buf[ci + 1] == '.' and pyi_buf[ci + 2] == 'C' and pyi_buf[ci + 3] == 'D' and pyi_buf[ci + 4] == 'e' and pyi_buf[ci + 5] == 'f' and pyi_buf[ci + 6] == 'i' and pyi_buf[ci + 7] == 'n' and pyi_buf[ci + 8] == 'e'): - has_cdefine = 1 - break - ci += 1 - if has_cdefine == 0: continue - - # find the value after "=" - eq_pos: sizet = line_start + name_len + 1 - while eq_pos < line_start + line_len and pyi_buf[eq_pos] != '=': eq_pos += 1 - if eq_pos >= line_start + line_len: continue - eq_pos += 1 # skip '=' - - # skip spaces - while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 - - # check for type-ctor forms like t.CUnsignedLong(...) / t.CLong(...) / int(...); - # if so, skip the "t.CXxx(" prefix, parse inside the parens, ignore trailing ')'. - is_type_ctor: int = 0 - if eq_pos + 2 < line_start + line_len: - if pyi_buf[eq_pos] == 't' and pyi_buf[eq_pos + 1] == '.': - # find the '(' position - paren_pos: sizet = eq_pos + 2 - while paren_pos < line_start + line_len and pyi_buf[paren_pos] != '(': paren_pos += 1 - if paren_pos < line_start + line_len and pyi_buf[paren_pos] == '(': - is_type_ctor = 1 - eq_pos = paren_pos + 1 # skip '(' - # skip possible leading spaces inside the parens - while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 - - # paren-wrapped flag: (1 << 23) / (512) / (A | B) - is_paren_expr: int = 0 - if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '(': - is_paren_expr = 1 - eq_pos += 1 - while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 - - # === parse left operand token (number / hex / sign / name) === - # sign flag - is_neg: int = 0 - if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '-': - is_neg = 1 - eq_pos += 1 - val_str_start: sizet = eq_pos - val_str_len: sizet = 0 - is_hex: int = 0 - is_float_val: int = 0 - is_name_val: int = 0 - is_str_val: int = 0 - # string value (e.g. ZLIB_VERSION = "1.3.2"): read inside the quotes; without this, - # cross-module read-back skipped it and reported "CDefine not found". - if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '"': - is_str_val = 1 - eq_pos += 1 # skip opening quote - val_str_start = eq_pos - while eq_pos < line_start + line_len and pyi_buf[eq_pos] != '"': - val_str_len += 1 - eq_pos += 1 - if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '"': - eq_pos += 1 # skip closing quote - if eq_pos + 1 < line_start + line_len: - if pyi_buf[eq_pos] == '0' and (pyi_buf[eq_pos + 1] == 'x' or pyi_buf[eq_pos + 1] == 'X'): - is_hex = 1 - val_str_len = 2 - eq_pos += 2 - elif (pyi_buf[eq_pos] >= 'A' and pyi_buf[eq_pos] <= 'Z') or (pyi_buf[eq_pos] >= 'a' and pyi_buf[eq_pos] <= 'z') or pyi_buf[eq_pos] == '_': - is_name_val = 1 - while eq_pos < line_start + line_len: - c_tk: char = pyi_buf[eq_pos] - if (c_tk >= 'A' and c_tk <= 'Z') or (c_tk >= 'a' and c_tk <= 'z') or (c_tk >= '0' and c_tk <= '9') or c_tk == '_': - val_str_len += 1 - eq_pos += 1 - else: break - # continue reading digit/hex chars (string values are read whole, skipped) - while eq_pos < line_start + line_len and is_name_val == 0 and is_str_val == 0: - ch: char = pyi_buf[eq_pos] - if is_hex == 1: - if ((ch >= '0' and ch <= '9') or (ch >= 'a' and ch <= 'f') or (ch >= 'A' and ch <= 'F')): - val_str_len += 1 - eq_pos += 1 - else: break - else: - if ch >= '0' and ch <= '9': - val_str_len += 1 - eq_pos += 1 - elif ch == '.' and is_float_val == 0: - # float value: '3.14159' ('.' allowed only once) - is_float_val = 1 - val_str_len += 1 - eq_pos += 1 - else: break - - if val_str_len == 0 and is_str_val == 0: continue - - # copy the left token into a buffer - val_buf: str = pool.alloc(val_str_len + 1) - if val_buf is None: continue - for vi in range(int(val_str_len)): val_buf[vi] = pyi_buf[val_str_start + vi] - val_buf[val_str_len] = '\0' - - # evaluate the left token - lval: int64 = 0 - l_is_double: int = 0 - ldbl: double = 0.0 - if is_str_val != 0: - # string CDefine (e.g. ZLIB_VERSION = "1.3.2"): register the string value - # (including ""), mark result_found + res_is_str so the tail skips integers. - self.Gen.TypeGenCDef.register_cdefine_string(pool, name, val_buf) - result_val = 0 - result_found = 1 - res_is_str = 1 - elif is_name_val != 0: - # True/False literal -> 1/0 (TPC treats bool as integer in CDefine context) - if string.strcmp(val_buf, "True") == 0: lval = 1 - elif string.strcmp(val_buf, "False") == 0: lval = 0 - else: - # name reference: look up the current CDefine table (e.g. FOREGROUND_COMBO) - lval = self.Gen.TypeGenCDef.lookup_cdefine_constant(val_buf) - if self.Gen.TypeGenCDef.is_cdefine_found() == 0: - # name not registered: cannot evaluate, skip the line - continue - elif is_hex == 1: - # parse hex manually (skip the 0x prefix) with 64-bit accumulation so - # 0xFFFFFFFF=4294967295 keeps full precision (constant side truncates to i32 -1). - hex_result: int64 = 0 - hex_buf: str = val_buf + 2 # skip "0x" - for hi in range(int(val_str_len - 2)): - hc: char = hex_buf[hi] - hd: int = 0 - if hc >= '0' and hc <= '9': hd = hc - '0' - elif hc >= 'a' and hc <= 'f': hd = hc - 'a' + 10 - elif hc >= 'A' and hc <= 'F': hd = hc - 'A' + 10 - else: break - hex_result = hex_result * 16 + hd - lval = hex_result - elif is_float_val != 0: - l_is_double = 1 - ldbl = string.atof(val_buf) - lval = string.atoll(val_buf) - else: lval = string.atoll(val_buf) - if is_neg != 0: - lval = -lval - if l_is_double != 0: ldbl = -ldbl - - # skip spaces; check for a binary operator - while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 - - # parse operator (<< >> | & ^ + - * /) - cd_op: int = 0 - if eq_pos + 1 < line_start + line_len: - if pyi_buf[eq_pos] == '<' and pyi_buf[eq_pos + 1] == '<': - cd_op = 1 # LShift - eq_pos += 2 - elif pyi_buf[eq_pos] == '>' and pyi_buf[eq_pos + 1] == '>': - cd_op = 2 # RShift - eq_pos += 2 - if cd_op == 0 and eq_pos < line_start + line_len: - if pyi_buf[eq_pos] == '|': - cd_op = 3 # BitOr - eq_pos += 1 - elif pyi_buf[eq_pos] == '&': - cd_op = 4 # BitAnd - eq_pos += 1 - elif pyi_buf[eq_pos] == '^': - cd_op = 5 # BitXor - eq_pos += 1 - elif pyi_buf[eq_pos] == '+': - cd_op = 6 # Add - eq_pos += 1 - elif pyi_buf[eq_pos] == '-': - cd_op = 7 # Sub - eq_pos += 1 - elif pyi_buf[eq_pos] == '*': - cd_op = 8 # Mult - eq_pos += 1 - elif pyi_buf[eq_pos] == '/': - cd_op = 9 # Div - eq_pos += 1 - - if cd_op != 0: - # === parse right operand token === - while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 - r_neg: int = 0 - if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '-': - r_neg = 1 - eq_pos += 1 - r_start: sizet = eq_pos - r_len: sizet = 0 - r_hex: int = 0 - r_name: int = 0 - if eq_pos + 1 < line_start + line_len: - if pyi_buf[eq_pos] == '0' and (pyi_buf[eq_pos + 1] == 'x' or pyi_buf[eq_pos + 1] == 'X'): - r_hex = 1 - r_len = 2 - eq_pos += 2 - elif (pyi_buf[eq_pos] >= 'A' and pyi_buf[eq_pos] <= 'Z') or (pyi_buf[eq_pos] >= 'a' and pyi_buf[eq_pos] <= 'z') or pyi_buf[eq_pos] == '_': - r_name = 1 - while eq_pos < line_start + line_len: - c_r: char = pyi_buf[eq_pos] - if (c_r >= 'A' and c_r <= 'Z') or (c_r >= 'a' and c_r <= 'z') or (c_r >= '0' and c_r <= '9') or c_r == '_': - r_len += 1 - eq_pos += 1 - else: break - while eq_pos < line_start + line_len and r_name == 0: - c_r2: char = pyi_buf[eq_pos] - if r_hex == 1: - if ((c_r2 >= '0' and c_r2 <= '9') or (c_r2 >= 'a' and c_r2 <= 'f') or (c_r2 >= 'A' and c_r2 <= 'F')): - r_len += 1 - eq_pos += 1 - else: break - else: - if c_r2 >= '0' and c_r2 <= '9': - r_len += 1 - eq_pos += 1 - else: break - if r_len == 0: - # no right operand after operator: cannot evaluate, skip the line - continue - r_buf: str = pool.alloc(r_len + 1) - if r_buf is None: continue - for r_i in range(int(r_len)): r_buf[r_i] = pyi_buf[r_start + r_i] - r_buf[r_len] = '\0' - rval: int64 = 0 - if r_name != 0: - # True/False literal -> 1/0 - if string.strcmp(r_buf, "True") == 0: rval = 1 - elif string.strcmp(r_buf, "False") == 0: rval = 0 - else: - rval = self.Gen.TypeGenCDef.lookup_cdefine_constant(r_buf) - if self.Gen.TypeGenCDef.is_cdefine_found() == 0: - # name not registered: cannot evaluate, skip the line - continue - elif r_hex != 0: - rhex_v: int64 = 0 - rh_buf: str = r_buf + 2 - for rh_i in range(int(r_len - 2)): - rc: char = rh_buf[rh_i] - rd: int = 0 - if rc >= '0' and rc <= '9': rd = rc - '0' - elif rc >= 'a' and rc <= 'f': rd = rc - 'a' + 10 - elif rc >= 'A' and rc <= 'F': rd = rc - 'A' + 10 - else: break - rhex_v = rhex_v * 16 + rd - rval = rhex_v - else: rval = string.atoll(r_buf) - if r_neg != 0: rval = -rval - # compute the binary expression result (aligned with TPC ConstEvaluator) - if cd_op == 1: result_val = lval << rval - elif cd_op == 2: result_val = lval >> rval - elif cd_op == 3: result_val = lval | rval - elif cd_op == 4: result_val = lval & rval - elif cd_op == 5: result_val = lval ^ rval - elif cd_op == 6: result_val = lval + rval - elif cd_op == 7: result_val = lval - rval - elif cd_op == 8: result_val = lval * rval - else: - if rval == 0: continue - result_val = lval // rval - result_found = 1 - break - else: - # single value (no operator): parenthesized values like (512) / (-11) also land here - if is_paren_expr != 0: - while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 - if eq_pos < line_start + line_len and pyi_buf[eq_pos] == ')': eq_pos += 1 - if is_float_val != 0: - # float CDefine (e.g. U_M_PI = 3.14159...): cache as double for double - # constant generation; int part serves the int path (is_cdefine_double flag). - self.Gen.TypeGenCDef.register_cdefine_double(pool, name, ldbl) - result_val = lval - result_found = 1 - res_is_double = 1 - else: - result_val = lval - result_found = 1 - break - - # 9.5 if not found in this module, recurse into submodules for `from .X import NAME` / - # `from .X import *` (e.g. ast.__init__ re-exports CONST_INT from .base). - if result_found == 0: - # base_mod was computed at function entry (mod_name_raw with leading dots removed); - # the submodule recursion uses it to build the full "base_mod.X" name. - name_len_rc: sizet = string.strlen(name) - rc_pos: sizet = 0 - while rc_pos < total_len and result_found == 0: - # find line start - rc_line_start: sizet = rc_pos - # find line end - while rc_pos < total_len and pyi_buf[rc_pos] != '\n': rc_pos += 1 - rc_line_len: sizet = rc_pos - rc_line_start - if rc_pos < total_len: rc_pos += 1 - - # check whether the line starts with "from " (module import) - if rc_line_len < 12: continue - if pyi_buf[rc_line_start] != 'f' or pyi_buf[rc_line_start+1] != 'r' or pyi_buf[rc_line_start+2] != 'o' or pyi_buf[rc_line_start+3] != 'm' or pyi_buf[rc_line_start+4] != ' ': continue - - # extract the module token (skip leading dots); support both relative - # `from .base import X` (join base_mod.base) and absolute `from vqt6._types import *`. - rc_is_rel: int = 0 - rc_sub_start: sizet = rc_line_start + 5 - while rc_sub_start < rc_line_start + rc_line_len and pyi_buf[rc_sub_start] == '.': - rc_is_rel = 1 - rc_sub_start += 1 - rc_sub_end: sizet = rc_sub_start - while rc_sub_end < rc_line_start + rc_line_len and pyi_buf[rc_sub_end] != ' ' and pyi_buf[rc_sub_end] != '\t': rc_sub_end += 1 - rc_sub_len: sizet = rc_sub_end - rc_sub_start - if rc_sub_len == 0: continue - - # check whether the line contains "import" - rc_has_import: int = 0 - rc_ipos: sizet = rc_sub_end - while rc_ipos + 6 <= rc_line_start + rc_line_len: - if pyi_buf[rc_ipos] == 'i' and pyi_buf[rc_ipos+1] == 'm' and pyi_buf[rc_ipos+2] == 'p' and pyi_buf[rc_ipos+3] == 'o' and pyi_buf[rc_ipos+4] == 'r' and pyi_buf[rc_ipos+5] == 't': - rc_has_import = 1 - break - rc_ipos += 1 - if rc_has_import == 0: continue - - # check NAME is in the import list, or the import is *; handle multi-line - # imports: scan from "import" up to ")" when a "(" is present. - rc_imp_start: sizet = rc_ipos + 6 - rc_star: int = 0 - rc_name_match: int = 0 - - # determine the scan range: single line or multi-line (inside parens) - rc_scan_end: sizet = rc_line_start + rc_line_len - rc_has_paren: int = 0 - rc_pp: sizet = rc_imp_start - while rc_pp < rc_line_start + rc_line_len: - if pyi_buf[rc_pp] == '(': - rc_has_paren = 1 - break - if pyi_buf[rc_pp] == '*': rc_star = 1 - rc_pp += 1 - if rc_has_paren != 0: - # multi-line import: scan from after "(" to ")" - rc_scan_end = rc_pp + 1 - rc_paren_closed: int = 0 - rc_sp: sizet = rc_pp + 1 - while rc_sp < total_len and rc_paren_closed == 0: - if pyi_buf[rc_sp] == ')': - rc_paren_closed = 1 - rc_scan_end = rc_sp - break - rc_sp += 1 - if rc_paren_closed == 0: rc_scan_end = total_len - - # scan from rc_imp_start to rc_scan_end for NAME and * - rc_cpos: sizet = rc_imp_start - while rc_cpos < rc_scan_end: - rc_ch: char = pyi_buf[rc_cpos] - if rc_ch == '*': rc_star = 1 - # check NAME matches as a whole word - if rc_cpos + name_len_rc <= rc_scan_end: - rc_match: int = 1 - rc_ki: sizet - for rc_ki in range(int(name_len_rc)): - if pyi_buf[rc_cpos + rc_ki] != name[rc_ki]: - rc_match = 0 - break - if rc_match == 1: - # check the preceding char is a non-identifier char - rc_before_ok: int = 0 - if rc_cpos == rc_imp_start: rc_before_ok = 1 - else: - rc_before_ch: char = pyi_buf[rc_cpos - 1] - if rc_before_ch == ' ' or rc_before_ch == '\t' or rc_before_ch == '(' or rc_before_ch == ',' or rc_before_ch == '\n' or rc_before_ch == '\r': rc_before_ok = 1 - # check the following char is a non-identifier char - rc_after_ok: int = 0 - rc_after_pos: sizet = rc_cpos + name_len_rc - if rc_after_pos >= rc_scan_end: rc_after_ok = 1 - else: - rc_after_ch: char = pyi_buf[rc_after_pos] - if rc_after_ch == ' ' or rc_after_ch == '\t' or rc_after_ch == ')' or rc_after_ch == ',' or rc_after_ch == '\r' or rc_after_ch == '\n': rc_after_ok = 1 - if rc_before_ok == 1 and rc_after_ok == 1: rc_name_match = 1 - rc_cpos += 1 - - if rc_star == 0 and rc_name_match == 0: continue - - # copy the submodule name into a new buffer (pyi_buf + rc_sub_start is not NUL-terminated) - rc_sub_buf: str = pool.alloc(rc_sub_len + 1) - if rc_sub_buf is None: continue - rc_si: sizet - for rc_si in range(int(rc_sub_len)): rc_sub_buf[rc_si] = pyi_buf[rc_sub_start + rc_si] - rc_sub_buf[rc_sub_len] = '\0' - - # build the full module name: relative import -> package + "." + token (the base - # must be the package, not the module name); absolute import -> token as-is. - rc_full_mod: str = None - if rc_is_rel != 0: - rc_base_mod: str = base_mod - rc_pkg: str = self.Gen.CallGenBase._module_package_from_sha1(pool, sha1) - if rc_pkg is not None: rc_base_mod = rc_pkg - rc_full_len: sizet = string.strlen(rc_base_mod) + 1 + rc_sub_len + 1 - rc_full_buf: str = pool.alloc(rc_full_len) - if rc_full_buf is None: continue - viperlib.snprintf(rc_full_buf, rc_full_len, "%s.%s", rc_base_mod, rc_sub_buf) - rc_full_mod = rc_full_buf - else: - if string.strcmp(rc_sub_buf, base_mod) == 0: continue - rc_full_mod = rc_sub_buf - - # loop guard: if base_mod already ends with "."+rc_sub_buf (ast.base + .base), - # the recursion is re-appending the same segment -> skip (infinite-growth root cause). - rc_blen: sizet = string.strlen(base_mod) - if rc_blen > rc_sub_len + 1: - rc_tail: str = base_mod + (rc_blen - rc_sub_len) - if rc_tail[0] == '.' and string.strncmp(rc_tail + 1, rc_sub_buf, rc_sub_len) == 0: continue - - # build a temporary from_imports "name:full_mod" - rc_fi_len: sizet = name_len_rc + 1 + string.strlen(rc_full_mod) + 1 - rc_fi: str = pool.alloc(rc_fi_len) - if rc_fi is None: continue - viperlib.snprintf(rc_fi, rc_fi_len, "%s:%s", name, rc_full_mod) - - # recursive call to search the submodule - rc_sub_val: int64 = self._lookup_cross_module_cdefine(pool, name, rc_fi, None, depth + 1, node) - if self.Gen.TypeGenCDef.is_cdefine_found() != 0: - result_val = rc_sub_val - result_found = 1 - - stdlib.free(pyi_buf) - - # 9.6 star-fallback module had no match: fall back to walking imported_modules, - # e.g. `from stdint import *` names stdint but SHARE_READ lives in `import win32.fileio`. - if result_found == 0 and imported_modules is not None: - im2_len: sizet = string.strlen(imported_modules) - im2_ci: sizet = 0 - while im2_ci < im2_len and result_found == 0: - while im2_ci < im2_len and imported_modules[im2_ci] == ' ': im2_ci += 1 - if im2_ci >= im2_len: break - im2_start: sizet = im2_ci - while im2_ci < im2_len and imported_modules[im2_ci] != ' ': im2_ci += 1 - im2_end: sizet = im2_ci - im2_nlen: sizet = im2_end - im2_start - if im2_nlen > 0 and im2_nlen < 256: - im2_buf: str = pool.alloc(im2_nlen + 1) - if im2_buf is not None: - for mk3 in range(int(im2_nlen)): - im2_buf[mk3] = imported_modules[im2_start + mk3] - im2_buf[im2_nlen] = '\0' - sub_fi2: str = pool.alloc(im2_nlen + 3) - if sub_fi2 is not None: - sub_fi2[0] = '*' - sub_fi2[1] = ':' - for mk4 in range(int(im2_nlen)): - sub_fi2[2 + mk4] = im2_buf[mk4] - sub_fi2[2 + im2_nlen] = '\0' - sub_val2: int64 = self._lookup_cross_module_cdefine(pool, name, sub_fi2, None, depth + 1, node) - if self.Gen.TypeGenCDef.is_cdefine_found() != 0: - result_val = sub_val2 - result_found = 1 - - # 10. cache into the current module's CDefine table and the cross-module perm_pool; - # float CDefines are already cached as double, so only integers are written here. - if result_found != 0: - if res_is_str != 0: - # string CDefine: the value was already registered in the match loop (val_buf is - # the hit line's content, valid across the function); only write the cross cache. - if base_mod is not None: - ck_len_s: sizet = string.strlen(name) + string.strlen(base_mod) + 2 - ckey_s: str = pool.alloc(ck_len_s) - if ckey_s is not None: - viperlib.snprintf(ckey_s, ck_len_s, "%s@%s", name, base_mod) - self.Gen.TypeGenCDef.register_xmod_cdefine(ckey_s, val_buf, 0, 1) - elif res_is_double == 0: - self.Gen.TypeGenCDef.register_cdefine_constant(pool, name, result_val) - # cross-module cache (perm_pool, reused across files), integers: key matches the - # entry query "name@srcModule" (truncated base_mod, leading dots removed). - if base_mod is not None: - ck_len: sizet = string.strlen(name) + string.strlen(base_mod) + 2 - ckey_buf: str = pool.alloc(ck_len) - if ckey_buf is not None: - viperlib.snprintf(ckey_buf, ck_len, "%s@%s", name, base_mod) - cbuf_i: str = pool.alloc(32) - if cbuf_i is not None: - viperlib.snprintf(cbuf_i, 32, "%lld", result_val) - self.Gen.TypeGenCDef.register_xmod_cdefine(ckey_buf, cbuf_i, 0, 0) - else: - # float: fetch %.17g text from the local table and write the cross cache (lossless) - if base_mod is not None: - ck_len2: sizet = string.strlen(name) + string.strlen(base_mod) + 2 - ckey2: str = pool.alloc(ck_len2) - if ckey2 is not None: - viperlib.snprintf(ckey2, ck_len2, "%s@%s", name, base_mod) - dval_cc: double = self.Gen.TypeGenCDef.lookup_cdefine_double(name) - if self.Gen.TypeGenCDef.is_cdefine_double() != 0: - cbuf_d: str = pool.alloc(64) - if cbuf_d is not None: - stdio.snprintf(cbuf_d, 64, "%.17g", dval_cc) - self.Gen.TypeGenCDef.register_xmod_cdefine(ckey2, cbuf_d, 1, 0) - self.Gen.TypeGenCDef.set_cdefine_found(1) - else: self.Gen.TypeGenCDef.set_cdefine_found(0) - - return result_val - # _is_cdefine_name_pattern - CDefine names follow ALL_CAPS (A-Z, 0-9, _ with >=1 uppercase); - # lowercase keywords/variables won't match, avoiding fail-fast false positives. 1=match, 0=no. - def _is_cdefine_name_pattern(self, name: str) -> int: - """Return 1 if name follows the CDefine convention (uppercase letters, digits, underscore)""" - if name is None: return 0 - has_upper: int = 0 - i: sizet = 0 - while name[i] != '\0': - c: char = name[i] - if c >= 'a' and c <= 'z': return 0 - if c >= 'A' and c <= 'Z': has_upper = 1 - i += 1 - return has_upper - # ============================================================ - # Translate a variable reference (Name node) -> load - def _const_int_for_cdefine(self, pool: memhub.MemBuddy*, name: str, value: int64) -> mlirlite.Value*: - """Materialize a CDefine integer constant as an LLVM constant (TPC TypeConvert aligned) - - 位宽优先用 define 的**已知位宽**(右侧显式强转或声明注解,见 register_cdefine_constant 的 bits); - 没有位宽记录时按**推断位宽**物化(值的最小可存宽度;0x80000000 → 32)。 - 这样 `define X: WORD = WORD(...)` 的 X 保持 u16、`X | Y` 里两个同宽 u16 不会被提升成 - i32;无注解的 `define A = 0x80000000` 也不会退化成 i64 把 `A | B` 放大到 64 位, - 而小值 define 说成 8/16 也在算术提升下不会溢出。 - """ - dbits: int = 0 - if name is not None: dbits = self.Gen.TypeGenCDef.lookup_cdefine_bits(name) - if dbits == 8 or dbits == 16 or dbits == 32 or dbits == 64: - return mlirlite.const_int(pool, dbits, self._cdefine_bits_value(value, dbits)) - # 无位宽记录(例如只从 .vpi 跨模块引入、未登记 bits)→ 按**推断位宽**物化: - # 值的最小可存宽度(2^31..2^32-1 取 32 位无符号位型,不退化成 i64; - # 小值取 8/16,参与算术时由整数提升先升到 i32,不会溢出)。 - wmin: int = self.Gen.AssignGen.define_min_width(value) - return mlirlite.const_int(pool, wmin, self._cdefine_bits_value(value, wmin)) - - # ============================================================ - # cdefine_use_width_value - materialize CDefine constants at the use-site width (TPC-aligned) - def _cdefine_bits_value(self, value: int64, bits: int) -> int64: - """把值按位宽**截断**后再重解释为该位宽的有符号表示(与 AssignGenMixin._normalize_cdefine_bits 同口径)。 - - 「左侧只会看到截断了的数据」:超宽的值先按 bits 取低位(WORD(0x80000000) → 0), - 而不是只对"刚好越界一点"的值做重解释。 - 实现用左移后算术右移,而不是 `value & 0xFFFFFFFF` 这类大掩码字面量: - 掩码字面量(如 4294967295)超出 int32 范围,其加宽方向在 TPC/TPV 上可能判成 sext, - 会得到 0xFFFFFFFFFFFFFFFF 而不是 0xFFFFFFFF,导致截断失效。 - """ - if bits == 8: return (value << 56) >> 56 - if bits == 16: return (value << 48) >> 48 - if bits == 32: return (value << 32) >> 32 - return value - - def cdefine_use_width_value(self, pool: memhub.MemBuddy*, node: ast.AST*, name: str, value: int64, target_ty: mlirlite.LLVMType*, use_ctx: str = None) -> mlirlite.Value*: - """Materialize a CDefine integer constant at a known use-site width, or None on fallback""" - bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(target_ty) - if bits != 8 and bits != 16 and bits != 32 and bits != 64: return None - wmin: int = self.Gen.AssignGen.define_min_width(value) - if bits < wmin: - # "representable range at the use site" uses the same rule as the check = TPC - # DefineWidthLoHi = [-(2^(bits-1)), 2^bits - 1]; literals built from safe 2147483647. - m31: int64 = 2147483647 - lo: int64 = -m31 - 1 - hi: int64 = m31 * 2 + 1 - if bits == 8: - lo = -128 - hi = 255 - elif bits == 16: - lo = -32768 - hi = 65535 - ctx: str = "" - if use_ctx is not None: - # this language's str is i8* with no `str + str`: `+` degrades to pointer add. - # Build strings with f-string interpolation / string.strcat / viperlib.snprintf. - ctx = f"({use_ctx})" - self.Gen.TypeGenErr.fatal_error(node, f"[TPV] define constant use site width insufficient: constant '{name}' actual value {value} " f"needs at least {wmin} bits (minimum representable width W_min), " f"but the use site{ctx} provides only {bits} bits " f"(the acceptable range for this bit width under both signed/unsigned interpretation: {lo}..{hi}). " f"silent truncation/downgrade is forbidden, compilation aborted. " f"fix: widen the type of the use site{ctx} to at least {wmin} bits " f"(e.g. int{wmin}T / CUInt{wmin}T or wider types), or use a smaller constant value.", "TypeError") - return None +import t, c +from stdint import * +import ast +import mlirlite +import memhub +import string +import stdio +import stdlib +import viperlib +import win32.fileio as fileio +import lib.Stub.StubMerger as StubMerger +import lib.core.VLogger as VLogger +import lib.Projectrans.Config as Config + +import lib.core.Gen.Expr.ExprGenBase as ExprGenBase +import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect +import lib.Stub.StubMerger.StubMergerStore as StubMergerStore + + +# ============================================================ +# ExprGenCdef - ExprGenMixin split (stage D): cross-module CDefine lookup +novirtual class ExprGenCdef(ExprGenBase.ExprGenBase): + """Cross-module CDefine lookup""" + + # _build_source_path_from_sha1: build a source .vp/.py path from SHA1 via the StubMerger + # global store (.vpi missing -> source .py); "lib/..." -> SourceDir, else IncludesDir. + def _build_source_path_from_sha1(self, sha1: str) -> str: + """Build a source .py path from a SHA1 via the global store""" + if sha1 is None: return None + if Config.IncludesDir is None: return None + # get the rel-path array from the StubMerger global store + rel_arr_ptr: bytes = StubMergerStore.GetSha1StoreRelArrPtr() + count: int = StubMergerStore.GetSha1StoreCount() + if rel_arr_ptr is None or count <= 0: return None + sha1_arr_ptr: bytes = StubMergerStore.GetSha1StoreArrPtr() + if sha1_arr_ptr is None: return None + # iterate to find the matching SHA1 + idx_s: int = -1 + for i in range(int(count)): + sidx: sizet = sizet(i) * 17 + cur_sha1: str = sha1_arr_ptr + sidx + if cur_sha1[0] == '\0': continue + if string.strcmp(cur_sha1, sha1) == 0: + idx_s = i + break + if idx_s < 0: return None + # get the rel path (MAX_REL_PATH_LEN=256, avoids cross-module CDefine issues) + MAX_RPL: sizet = 256 + ridx: sizet = sizet(idx_s) * MAX_RPL + rel_path: str = rel_arr_ptr + ridx + if rel_path[0] == '\0': return None + # choose base dir: 1) stored includes root IncDir (absolute, supports multiple dirs); + # 2) prefix heuristic: "lib/" -> SourceDir, "includes/" -> IncludesDir, else SourceDir then IncludesDir. + base_dir: str = None + rel_used: str = rel_path + inc_arr_ptr: bytes = StubMergerStore.GetSha1StoreIncdirArrPtr() + if inc_arr_ptr is not None: + iidx: sizet = sizet(idx_s) * StubMerger.MAX_INC_DIR_LEN + inc_dir: str = inc_arr_ptr + iidx + if inc_dir[0] != '\0': base_dir = inc_dir + if base_dir is None and string.strlen(rel_path) >= 4: + if (rel_path[0] == 'l' and rel_path[1] == 'i' and rel_path[2] == 'b' and rel_path[3] == '/'): + base_dir = Config.SourceDir if Config.SourceDir is not None else Config.IncludesDir + if base_dir is None and string.strlen(rel_path) >= 9: + if (rel_path[0] == 'i' and rel_path[1] == 'n' and rel_path[2] == 'c' and rel_path[3] == 'l' and rel_path[4] == 'u' and rel_path[5] == 'd' and rel_path[6] == 'e' and rel_path[7] == 's' and rel_path[8] == '/'): + rel_used = rel_path + 9 + base_dir = Config.IncludesDir + if base_dir is None: base_dir = Config.SourceDir if Config.SourceDir is not None else Config.IncludesDir + # build full path: {base_dir}/{rel_used} + base_len: sizet = string.strlen(base_dir) + rel_len: sizet = string.strlen(rel_used) + path_len: sizet = base_len + rel_len + 2 + buf: str = stdlib.malloc(path_len) + if buf is None: return None + viperlib.snprintf(buf, path_len, "%s/%s", base_dir, rel_used) + # existence check: if missing under SourceDir and IncludesDir differs, retry IncludesDir + pf_chk: fileio.File* = fileio.File(buf, fileio.MODE.R) + if pf_chk.closed: + pf_chk.close() + if Config.IncludesDir is not None and base_dir != Config.IncludesDir: + stdlib.free(buf) + base_len2: sizet = string.strlen(Config.IncludesDir) + path_len2: sizet = base_len2 + rel_len + 2 + buf = stdlib.malloc(path_len2) + if buf is None: return None + viperlib.snprintf(buf, path_len2, "%s/%s", Config.IncludesDir, rel_used) + pf_chk2: fileio.File* = fileio.File(buf, fileio.MODE.R) + if pf_chk2.closed: + pf_chk2.close() + stdlib.free(buf) + return None + pf_chk2.close() + else: + stdlib.free(buf) + return None + else: pf_chk.close() + return buf + # _lookup_cross_module_cdefine - cross-module CDefine lookup: resolve the source module + # from from_imports, parse its pyi "NAME: t.CDefine = value", then cache in the current module. + def _lookup_cross_module_cdefine(self, pool: memhub.MemBuddy*, name: str, from_imports: str, imported_modules: str = None, depth: int = 0, node: ast.AST* = None) -> int64: + """Look up a CDefine constant across modules, returning its 64-bit value or -1""" + # recursion-depth guard: re-export chains + imported_modules walk can loop and OOM; + # cap at 32 to fail fast on cyclic imports (error carries node line + module chain). + if depth > 32: + err_buf: str = pool.alloc(256) + if err_buf is not None: + viperlib.snprintf(err_buf, 256, "cross-module CDefine lookup recursion too deep (suspected module import/re-export cycle): %s (depth=%d fi=%s)", name, depth, from_imports if from_imports is not None else "(nil)") + self.Gen.TypeGenErr.fatal_error(node, err_buf, "InternalCompilerError") + self.Gen.TypeGenErr.fatal_error(node, "cross-module CDefine lookup recursion too deep (suspected module import/re-export cycle)", "InternalCompilerError") + # clear the found flag at entry so every early return reports "not found", + # avoiding a stale success flag left over from a previous recursive call. + self.Gen.TypeGenCDef.set_cdefine_found(0) + if name is None: return -1 + if from_imports is None: from_imports = "" + + # 1. resolve the module name for `name` from from_imports; try exact match first + # (allow_star_fallback=0) so a star import cannot mislead CDefine constants. + mod_name_raw: str = self.Gen.ImportsGen.lookup_from_import(from_imports, name, 0) + if mod_name_raw is None: + # exact match failed: try the star import fallback + mod_name_raw = self.Gen.ImportsGen.lookup_from_import(from_imports, name, 1) + + # truncate module name at NUL: lookup_from_import returns a pointer into from_imports + # (not NUL-terminated); match _lookup_sha1_from_modname_raw (space/colon/NUL delimited). + base_mod: str = None + if mod_name_raw is not None: + mbl: sizet = 0 + while mod_name_raw[mbl] != ' ' and mod_name_raw[mbl] != '\0' and mod_name_raw[mbl] != ':': mbl += 1 + mb_buf: str = pool.alloc(mbl + 1) + if mb_buf is not None: + mi2: sizet + for mi2 in range(int(mbl)): mb_buf[mi2] = mod_name_raw[mi2] + mb_buf[mbl] = '\0' + base_mod = mb_buf + while base_mod is not None and base_mod[0] == '.': base_mod = base_mod + 1 + + # cross-module CDefine cache (perm_pool, reused across files): key = "name@srcModule" + # keeps modules isolated so same-name constants from different modules do not cross. + xkey_buf: str = None + if base_mod is not None: + xk_len: sizet = string.strlen(name) + string.strlen(base_mod) + 2 + xkey_buf = pool.alloc(xk_len) + if xkey_buf is not None: + viperlib.snprintf(xkey_buf, xk_len, "%s@%s", name, base_mod) + xval: str = self.Gen.TypeGenCDef.lookup_xmod_cdefine(xkey_buf) + if self.Gen.TypeGenCDef.is_cdefine_found() != 0: + if self.Gen.TypeGenCDef.is_cdefine_str() != 0: self.Gen.TypeGenCDef.register_cdefine_string(pool, name, xval) + elif self.Gen.TypeGenCDef.is_cdefine_double() != 0: + xd_v: double = string.atof(xval) + self.Gen.TypeGenCDef.register_cdefine_double(pool, name, xd_v) + else: + xv_i: int64 = string.atoll(xval) + self.Gen.TypeGenCDef.register_cdefine_constant(pool, name, xv_i) + return string.atoll(xval) + + # 2. from_imports failed: fall back to the imported_modules walk (step 9.6); + # a star fallback may name a wrong module, so keep scanning imported_modules. + if mod_name_raw is None: + # from_imports matched nothing (exact and star both failed): + # go straight to the imported_modules walk (step 9.6). + if imported_modules is None: return -1 + # skip reading the from_imports module; go directly to the step 9.6 walk. + # result_found=0 skips steps 3-9 and jumps to 9.6. + self.Gen.TypeGenCDef.set_cdefine_found(0) + # iterate imported_modules directly + im_len: sizet = string.strlen(imported_modules) + im_ci: sizet = 0 + while im_ci < im_len: + while im_ci < im_len and imported_modules[im_ci] == ' ': im_ci += 1 + if im_ci >= im_len: break + im_start: sizet = im_ci + while im_ci < im_len and imported_modules[im_ci] != ' ': im_ci += 1 + im_end: sizet = im_ci + im_nlen: sizet = im_end - im_start + if im_nlen > 0 and im_nlen < 256: + im_buf: str = pool.alloc(im_nlen + 1) + if im_buf is not None: + for mk in range(int(im_nlen)): + im_buf[mk] = imported_modules[im_start + mk] + im_buf[im_nlen] = '\0' + sub_fi: str = pool.alloc(im_nlen + 3) + if sub_fi is not None: + sub_fi[0] = '*' + sub_fi[1] = ':' + for mk2 in range(int(im_nlen)): + sub_fi[2 + mk2] = im_buf[mk2] + sub_fi[2 + im_nlen] = '\0' + sub_val: int64 = self._lookup_cross_module_cdefine(pool, name, sub_fi, None, depth + 1, node) + if self.Gen.TypeGenCDef.is_cdefine_found() != 0: return sub_val + return -1 + + # truncate module name, strip leading dots, find SHA1 via exact+suffix (shared logic) + sha1: str = self.Gen.CallGenBase._lookup_sha1_from_modname_raw(pool, mod_name_raw) + if sha1 is None: return -1 + + # 5. get temp_dir + temp_dir: str = self.Gen.TypeGenBase.get_temp_dir() + if temp_dir is None: return -1 + + # 6. build the vpi file path + td_len: sizet = string.strlen(temp_dir) + pyi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1, "vpi") + if pyi_path is None: return -1 + + # 7. read vpi; if missing or too short (empty stub), fall back to the source .vp/.py + PYI_READ_BUF_SIZE: sizet = 65536 + pyi_buf: bytes = stdlib.malloc(PYI_READ_BUF_SIZE) + if pyi_buf is None: + stdlib.free(pyi_path) + return -1 + pf: fileio.File* = fileio.File(pyi_path, fileio.MODE.R) + need_py_fallback: int = 0 + bytes_read: LONG = 0 + if pf.closed: + # .vpi missing: fall back to the source .vp/.py + need_py_fallback = 1 + else: + bytes_read = pf.read_all(pyi_buf, PYI_READ_BUF_SIZE) + pf.close() + # vpi too short (< 10 bytes): treat as empty stub, fall back to .py + if bytes_read < 10: need_py_fallback = 1 + else: + if bytes_read < PYI_READ_BUF_SIZE: pyi_buf[bytes_read] = 0 + # vpi stubs may declare a CDefine name without a value; the real value lives + # in the source .vp/.py, so trust vpi only when the target name's line has a value. + cd_sig: int = 0 + name_has_val: int = 0 + cs_name_len: sizet = string.strlen(name) + cs_total: sizet = string.strlen(pyi_buf) + cs_i: sizet = 0 + while cs_i + 9 < cs_total: + if (pyi_buf[cs_i] == 't' and pyi_buf[cs_i + 1] == '.' and pyi_buf[cs_i + 2] == 'C' and pyi_buf[cs_i + 3] == 'D' and pyi_buf[cs_i + 4] == 'e' and pyi_buf[cs_i + 5] == 'f' and pyi_buf[cs_i + 6] == 'i' and pyi_buf[cs_i + 7] == 'n' and pyi_buf[cs_i + 8] == 'e'): + # check whether the rest of the line has '=' (only valued decls match in vpi) + cd_eq: int = 0 + cs_j: sizet = cs_i + 9 + while cs_j < cs_total and pyi_buf[cs_j] != '\n': + if pyi_buf[cs_j] == '=': + cd_eq = 1 + break + cs_j += 1 + if cd_eq != 0: + cd_sig = 1 + # locate line start; check it begins with "name:" (target has a value) + cs_line_start: sizet = cs_i + while cs_line_start > 0 and pyi_buf[cs_line_start - 1] != '\n': cs_line_start -= 1 + if cs_i - cs_line_start >= cs_name_len + 1: + nm_ok: int = 1 + for ki2 in range(int(cs_name_len)): + if pyi_buf[cs_line_start + ki2] != name[ki2]: + nm_ok = 0 + break + if nm_ok == 1 and pyi_buf[cs_line_start + cs_name_len] == ':': name_has_val = 1 + cs_i += 1 + if cd_sig == 0 or name_has_val == 0: need_py_fallback = 1 + stdlib.free(pyi_path) + + if need_py_fallback != 0: + # look up the rel path from the SHA1 store; build {base_dir}/{rel_path} + src_path: str = self._build_source_path_from_sha1(sha1) + if src_path is None: + # SHA1 absent from the store = compiler bug (Phase1/Phase2 not populated): + # report and abort instead of silently returning -1 (avoids bad @UNDEFINED refs). + err_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENCDEF_263) + if err_buf is not None: + viperlib.snprintf(err_buf, 512, "cross-module CDefine lookup failed: SHA1 %s is not in the store (base_mod=%s name=%s). " "Phase1/Phase2 did not correctly populate PopulateSha1MapStore/AppendToSha1MapStore", sha1, base_mod, name) + VLogger.error(err_buf, "XMOD-CD") + stdlib.free(pyi_buf) + return -1 + pf = fileio.File(src_path, fileio.MODE.R) + stdlib.free(src_path) + if pf.closed: + stdlib.free(pyi_buf) + return -1 + bytes_read = pf.read_all(pyi_buf, PYI_READ_BUF_SIZE) + pf.close() + if bytes_read <= 0: + stdlib.free(pyi_buf) + return -1 + if bytes_read < PYI_READ_BUF_SIZE: pyi_buf[bytes_read] = 0 + + # 8. search pyi_buf for the "name: t.CDefine = value" line + # format: NAME: t.CDefine = value + name_len: sizet = string.strlen(name) + total_len: sizet = string.strlen(pyi_buf) + pos: sizet = 0 + result_val: int64 = 0 + result_found: int = 0 + res_is_double: int = 0 + res_is_str: int = 0 + while pos < total_len: + # find line start + line_start: sizet = pos + # find line end + while pos < total_len and pyi_buf[pos] != '\n': pos += 1 + line_len: sizet = pos - line_start + if pos < total_len: + pos += 1 # skip '\n' + + # check whether the line starts with "name:" + if line_len < name_len + 1: continue + match_ok: int = 1 + for ki in range(int(name_len)): + if pyi_buf[line_start + ki] != name[ki]: + match_ok = 0 + break + if match_ok == 0: continue + if pyi_buf[line_start + name_len] != ':': continue + + # check the line contains "t.CDefine" + # scan the line for "t.CDefine" + has_cdefine: int = 0 + ci: sizet = line_start + name_len + 1 + while ci + 8 <= line_start + line_len: + if (pyi_buf[ci] == 't' and pyi_buf[ci + 1] == '.' and pyi_buf[ci + 2] == 'C' and pyi_buf[ci + 3] == 'D' and pyi_buf[ci + 4] == 'e' and pyi_buf[ci + 5] == 'f' and pyi_buf[ci + 6] == 'i' and pyi_buf[ci + 7] == 'n' and pyi_buf[ci + 8] == 'e'): + has_cdefine = 1 + break + ci += 1 + if has_cdefine == 0: continue + + # find the value after "=" + eq_pos: sizet = line_start + name_len + 1 + while eq_pos < line_start + line_len and pyi_buf[eq_pos] != '=': eq_pos += 1 + if eq_pos >= line_start + line_len: continue + eq_pos += 1 # skip '=' + + # skip spaces + while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 + + # check for type-ctor forms like t.CUnsignedLong(...) / t.CLong(...) / int(...); + # if so, skip the "t.CXxx(" prefix, parse inside the parens, ignore trailing ')'. + is_type_ctor: int = 0 + if eq_pos + 2 < line_start + line_len: + if pyi_buf[eq_pos] == 't' and pyi_buf[eq_pos + 1] == '.': + # find the '(' position + paren_pos: sizet = eq_pos + 2 + while paren_pos < line_start + line_len and pyi_buf[paren_pos] != '(': paren_pos += 1 + if paren_pos < line_start + line_len and pyi_buf[paren_pos] == '(': + is_type_ctor = 1 + eq_pos = paren_pos + 1 # skip '(' + # skip possible leading spaces inside the parens + while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 + + # paren-wrapped flag: (1 << 23) / (512) / (A | B) + is_paren_expr: int = 0 + if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '(': + is_paren_expr = 1 + eq_pos += 1 + while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 + + # === parse left operand token (number / hex / sign / name) === + # sign flag + is_neg: int = 0 + if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '-': + is_neg = 1 + eq_pos += 1 + val_str_start: sizet = eq_pos + val_str_len: sizet = 0 + is_hex: int = 0 + is_float_val: int = 0 + is_name_val: int = 0 + is_str_val: int = 0 + # string value (e.g. ZLIB_VERSION = "1.3.2"): read inside the quotes; without this, + # cross-module read-back skipped it and reported "CDefine not found". + if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '"': + is_str_val = 1 + eq_pos += 1 # skip opening quote + val_str_start = eq_pos + while eq_pos < line_start + line_len and pyi_buf[eq_pos] != '"': + val_str_len += 1 + eq_pos += 1 + if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '"': + eq_pos += 1 # skip closing quote + if eq_pos + 1 < line_start + line_len: + if pyi_buf[eq_pos] == '0' and (pyi_buf[eq_pos + 1] == 'x' or pyi_buf[eq_pos + 1] == 'X'): + is_hex = 1 + val_str_len = 2 + eq_pos += 2 + elif (pyi_buf[eq_pos] >= 'A' and pyi_buf[eq_pos] <= 'Z') or (pyi_buf[eq_pos] >= 'a' and pyi_buf[eq_pos] <= 'z') or pyi_buf[eq_pos] == '_': + is_name_val = 1 + while eq_pos < line_start + line_len: + c_tk: char = pyi_buf[eq_pos] + if (c_tk >= 'A' and c_tk <= 'Z') or (c_tk >= 'a' and c_tk <= 'z') or (c_tk >= '0' and c_tk <= '9') or c_tk == '_': + val_str_len += 1 + eq_pos += 1 + else: break + # continue reading digit/hex chars (string values are read whole, skipped) + while eq_pos < line_start + line_len and is_name_val == 0 and is_str_val == 0: + ch: char = pyi_buf[eq_pos] + if is_hex == 1: + if ((ch >= '0' and ch <= '9') or (ch >= 'a' and ch <= 'f') or (ch >= 'A' and ch <= 'F')): + val_str_len += 1 + eq_pos += 1 + else: break + else: + if ch >= '0' and ch <= '9': + val_str_len += 1 + eq_pos += 1 + elif ch == '.' and is_float_val == 0: + # float value: '3.14159' ('.' allowed only once) + is_float_val = 1 + val_str_len += 1 + eq_pos += 1 + else: break + + if val_str_len == 0 and is_str_val == 0: continue + + # copy the left token into a buffer + val_buf: str = pool.alloc(val_str_len + 1) + if val_buf is None: continue + for vi in range(int(val_str_len)): val_buf[vi] = pyi_buf[val_str_start + vi] + val_buf[val_str_len] = '\0' + + # evaluate the left token + lval: int64 = 0 + l_is_double: int = 0 + ldbl: double = 0.0 + if is_str_val != 0: + # string CDefine (e.g. ZLIB_VERSION = "1.3.2"): register the string value + # (including ""), mark result_found + res_is_str so the tail skips integers. + self.Gen.TypeGenCDef.register_cdefine_string(pool, name, val_buf) + result_val = 0 + result_found = 1 + res_is_str = 1 + elif is_name_val != 0: + # True/False literal -> 1/0 (TPC treats bool as integer in CDefine context) + if string.strcmp(val_buf, "True") == 0: lval = 1 + elif string.strcmp(val_buf, "False") == 0: lval = 0 + else: + # name reference: look up the current CDefine table (e.g. FOREGROUND_COMBO) + lval = self.Gen.TypeGenCDef.lookup_cdefine_constant(val_buf) + if self.Gen.TypeGenCDef.is_cdefine_found() == 0: + # name not registered: cannot evaluate, skip the line + continue + elif is_hex == 1: + # parse hex manually (skip the 0x prefix) with 64-bit accumulation so + # 0xFFFFFFFF=4294967295 keeps full precision (constant side truncates to i32 -1). + hex_result: int64 = 0 + hex_buf: str = val_buf + 2 # skip "0x" + for hi in range(int(val_str_len - 2)): + hc: char = hex_buf[hi] + hd: int = 0 + if hc >= '0' and hc <= '9': hd = hc - '0' + elif hc >= 'a' and hc <= 'f': hd = hc - 'a' + 10 + elif hc >= 'A' and hc <= 'F': hd = hc - 'A' + 10 + else: break + hex_result = hex_result * 16 + hd + lval = hex_result + elif is_float_val != 0: + l_is_double = 1 + ldbl = string.atof(val_buf) + lval = string.atoll(val_buf) + else: lval = string.atoll(val_buf) + if is_neg != 0: + lval = -lval + if l_is_double != 0: ldbl = -ldbl + + # skip spaces; check for a binary operator + while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 + + # parse operator (<< >> | & ^ + - * /) + cd_op: int = 0 + if eq_pos + 1 < line_start + line_len: + if pyi_buf[eq_pos] == '<' and pyi_buf[eq_pos + 1] == '<': + cd_op = 1 # LShift + eq_pos += 2 + elif pyi_buf[eq_pos] == '>' and pyi_buf[eq_pos + 1] == '>': + cd_op = 2 # RShift + eq_pos += 2 + if cd_op == 0 and eq_pos < line_start + line_len: + if pyi_buf[eq_pos] == '|': + cd_op = 3 # BitOr + eq_pos += 1 + elif pyi_buf[eq_pos] == '&': + cd_op = 4 # BitAnd + eq_pos += 1 + elif pyi_buf[eq_pos] == '^': + cd_op = 5 # BitXor + eq_pos += 1 + elif pyi_buf[eq_pos] == '+': + cd_op = 6 # Add + eq_pos += 1 + elif pyi_buf[eq_pos] == '-': + cd_op = 7 # Sub + eq_pos += 1 + elif pyi_buf[eq_pos] == '*': + cd_op = 8 # Mult + eq_pos += 1 + elif pyi_buf[eq_pos] == '/': + cd_op = 9 # Div + eq_pos += 1 + + if cd_op != 0: + # === parse right operand token === + while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 + r_neg: int = 0 + if eq_pos < line_start + line_len and pyi_buf[eq_pos] == '-': + r_neg = 1 + eq_pos += 1 + r_start: sizet = eq_pos + r_len: sizet = 0 + r_hex: int = 0 + r_name: int = 0 + if eq_pos + 1 < line_start + line_len: + if pyi_buf[eq_pos] == '0' and (pyi_buf[eq_pos + 1] == 'x' or pyi_buf[eq_pos + 1] == 'X'): + r_hex = 1 + r_len = 2 + eq_pos += 2 + elif (pyi_buf[eq_pos] >= 'A' and pyi_buf[eq_pos] <= 'Z') or (pyi_buf[eq_pos] >= 'a' and pyi_buf[eq_pos] <= 'z') or pyi_buf[eq_pos] == '_': + r_name = 1 + while eq_pos < line_start + line_len: + c_r: char = pyi_buf[eq_pos] + if (c_r >= 'A' and c_r <= 'Z') or (c_r >= 'a' and c_r <= 'z') or (c_r >= '0' and c_r <= '9') or c_r == '_': + r_len += 1 + eq_pos += 1 + else: break + while eq_pos < line_start + line_len and r_name == 0: + c_r2: char = pyi_buf[eq_pos] + if r_hex == 1: + if ((c_r2 >= '0' and c_r2 <= '9') or (c_r2 >= 'a' and c_r2 <= 'f') or (c_r2 >= 'A' and c_r2 <= 'F')): + r_len += 1 + eq_pos += 1 + else: break + else: + if c_r2 >= '0' and c_r2 <= '9': + r_len += 1 + eq_pos += 1 + else: break + if r_len == 0: + # no right operand after operator: cannot evaluate, skip the line + continue + r_buf: str = pool.alloc(r_len + 1) + if r_buf is None: continue + for r_i in range(int(r_len)): r_buf[r_i] = pyi_buf[r_start + r_i] + r_buf[r_len] = '\0' + rval: int64 = 0 + if r_name != 0: + # True/False literal -> 1/0 + if string.strcmp(r_buf, "True") == 0: rval = 1 + elif string.strcmp(r_buf, "False") == 0: rval = 0 + else: + rval = self.Gen.TypeGenCDef.lookup_cdefine_constant(r_buf) + if self.Gen.TypeGenCDef.is_cdefine_found() == 0: + # name not registered: cannot evaluate, skip the line + continue + elif r_hex != 0: + rhex_v: int64 = 0 + rh_buf: str = r_buf + 2 + for rh_i in range(int(r_len - 2)): + rc: char = rh_buf[rh_i] + rd: int = 0 + if rc >= '0' and rc <= '9': rd = rc - '0' + elif rc >= 'a' and rc <= 'f': rd = rc - 'a' + 10 + elif rc >= 'A' and rc <= 'F': rd = rc - 'A' + 10 + else: break + rhex_v = rhex_v * 16 + rd + rval = rhex_v + else: rval = string.atoll(r_buf) + if r_neg != 0: rval = -rval + # compute the binary expression result (aligned with TPC ConstEvaluator) + if cd_op == 1: result_val = lval << rval + elif cd_op == 2: result_val = lval >> rval + elif cd_op == 3: result_val = lval | rval + elif cd_op == 4: result_val = lval & rval + elif cd_op == 5: result_val = lval ^ rval + elif cd_op == 6: result_val = lval + rval + elif cd_op == 7: result_val = lval - rval + elif cd_op == 8: result_val = lval * rval + else: + if rval == 0: continue + result_val = lval // rval + result_found = 1 + break + else: + # single value (no operator): parenthesized values like (512) / (-11) also land here + if is_paren_expr != 0: + while eq_pos < line_start + line_len and pyi_buf[eq_pos] == ' ': eq_pos += 1 + if eq_pos < line_start + line_len and pyi_buf[eq_pos] == ')': eq_pos += 1 + if is_float_val != 0: + # float CDefine (e.g. U_M_PI = 3.14159...): cache as double for double + # constant generation; int part serves the int path (is_cdefine_double flag). + self.Gen.TypeGenCDef.register_cdefine_double(pool, name, ldbl) + result_val = lval + result_found = 1 + res_is_double = 1 + else: + result_val = lval + result_found = 1 + break + + # 9.5 if not found in this module, recurse into submodules for `from .X import NAME` / + # `from .X import *` (e.g. ast.__init__ re-exports CONST_INT from .base). + if result_found == 0: + # base_mod was computed at function entry (mod_name_raw with leading dots removed); + # the submodule recursion uses it to build the full "base_mod.X" name. + name_len_rc: sizet = string.strlen(name) + rc_pos: sizet = 0 + while rc_pos < total_len and result_found == 0: + # find line start + rc_line_start: sizet = rc_pos + # find line end + while rc_pos < total_len and pyi_buf[rc_pos] != '\n': rc_pos += 1 + rc_line_len: sizet = rc_pos - rc_line_start + if rc_pos < total_len: rc_pos += 1 + + # check whether the line starts with "from " (module import) + if rc_line_len < 12: continue + if pyi_buf[rc_line_start] != 'f' or pyi_buf[rc_line_start+1] != 'r' or pyi_buf[rc_line_start+2] != 'o' or pyi_buf[rc_line_start+3] != 'm' or pyi_buf[rc_line_start+4] != ' ': continue + + # extract the module token (skip leading dots); support both relative + # `from .base import X` (join base_mod.base) and absolute `from vqt6._types import *`. + rc_is_rel: int = 0 + rc_sub_start: sizet = rc_line_start + 5 + while rc_sub_start < rc_line_start + rc_line_len and pyi_buf[rc_sub_start] == '.': + rc_is_rel = 1 + rc_sub_start += 1 + rc_sub_end: sizet = rc_sub_start + while rc_sub_end < rc_line_start + rc_line_len and pyi_buf[rc_sub_end] != ' ' and pyi_buf[rc_sub_end] != '\t': rc_sub_end += 1 + rc_sub_len: sizet = rc_sub_end - rc_sub_start + if rc_sub_len == 0: continue + + # check whether the line contains "import" + rc_has_import: int = 0 + rc_ipos: sizet = rc_sub_end + while rc_ipos + 6 <= rc_line_start + rc_line_len: + if pyi_buf[rc_ipos] == 'i' and pyi_buf[rc_ipos+1] == 'm' and pyi_buf[rc_ipos+2] == 'p' and pyi_buf[rc_ipos+3] == 'o' and pyi_buf[rc_ipos+4] == 'r' and pyi_buf[rc_ipos+5] == 't': + rc_has_import = 1 + break + rc_ipos += 1 + if rc_has_import == 0: continue + + # check NAME is in the import list, or the import is *; handle multi-line + # imports: scan from "import" up to ")" when a "(" is present. + rc_imp_start: sizet = rc_ipos + 6 + rc_star: int = 0 + rc_name_match: int = 0 + + # determine the scan range: single line or multi-line (inside parens) + rc_scan_end: sizet = rc_line_start + rc_line_len + rc_has_paren: int = 0 + rc_pp: sizet = rc_imp_start + while rc_pp < rc_line_start + rc_line_len: + if pyi_buf[rc_pp] == '(': + rc_has_paren = 1 + break + if pyi_buf[rc_pp] == '*': rc_star = 1 + rc_pp += 1 + if rc_has_paren != 0: + # multi-line import: scan from after "(" to ")" + rc_scan_end = rc_pp + 1 + rc_paren_closed: int = 0 + rc_sp: sizet = rc_pp + 1 + while rc_sp < total_len and rc_paren_closed == 0: + if pyi_buf[rc_sp] == ')': + rc_paren_closed = 1 + rc_scan_end = rc_sp + break + rc_sp += 1 + if rc_paren_closed == 0: rc_scan_end = total_len + + # scan from rc_imp_start to rc_scan_end for NAME and * + rc_cpos: sizet = rc_imp_start + while rc_cpos < rc_scan_end: + rc_ch: char = pyi_buf[rc_cpos] + if rc_ch == '*': rc_star = 1 + # check NAME matches as a whole word + if rc_cpos + name_len_rc <= rc_scan_end: + rc_match: int = 1 + rc_ki: sizet + for rc_ki in range(int(name_len_rc)): + if pyi_buf[rc_cpos + rc_ki] != name[rc_ki]: + rc_match = 0 + break + if rc_match == 1: + # check the preceding char is a non-identifier char + rc_before_ok: int = 0 + if rc_cpos == rc_imp_start: rc_before_ok = 1 + else: + rc_before_ch: char = pyi_buf[rc_cpos - 1] + if rc_before_ch == ' ' or rc_before_ch == '\t' or rc_before_ch == '(' or rc_before_ch == ',' or rc_before_ch == '\n' or rc_before_ch == '\r': rc_before_ok = 1 + # check the following char is a non-identifier char + rc_after_ok: int = 0 + rc_after_pos: sizet = rc_cpos + name_len_rc + if rc_after_pos >= rc_scan_end: rc_after_ok = 1 + else: + rc_after_ch: char = pyi_buf[rc_after_pos] + if rc_after_ch == ' ' or rc_after_ch == '\t' or rc_after_ch == ')' or rc_after_ch == ',' or rc_after_ch == '\r' or rc_after_ch == '\n': rc_after_ok = 1 + if rc_before_ok == 1 and rc_after_ok == 1: rc_name_match = 1 + rc_cpos += 1 + + if rc_star == 0 and rc_name_match == 0: continue + + # copy the submodule name into a new buffer (pyi_buf + rc_sub_start is not NUL-terminated) + rc_sub_buf: str = pool.alloc(rc_sub_len + 1) + if rc_sub_buf is None: continue + rc_si: sizet + for rc_si in range(int(rc_sub_len)): rc_sub_buf[rc_si] = pyi_buf[rc_sub_start + rc_si] + rc_sub_buf[rc_sub_len] = '\0' + + # build the full module name: relative import -> package + "." + token (the base + # must be the package, not the module name); absolute import -> token as-is. + rc_full_mod: str = None + if rc_is_rel != 0: + rc_base_mod: str = base_mod + rc_pkg: str = self.Gen.CallGenBase._module_package_from_sha1(pool, sha1) + if rc_pkg is not None: rc_base_mod = rc_pkg + rc_full_len: sizet = string.strlen(rc_base_mod) + 1 + rc_sub_len + 1 + rc_full_buf: str = pool.alloc(rc_full_len) + if rc_full_buf is None: continue + viperlib.snprintf(rc_full_buf, rc_full_len, "%s.%s", rc_base_mod, rc_sub_buf) + rc_full_mod = rc_full_buf + else: + if string.strcmp(rc_sub_buf, base_mod) == 0: continue + rc_full_mod = rc_sub_buf + + # loop guard: if base_mod already ends with "."+rc_sub_buf (ast.base + .base), + # the recursion is re-appending the same segment -> skip (infinite-growth root cause). + rc_blen: sizet = string.strlen(base_mod) + if rc_blen > rc_sub_len + 1: + rc_tail: str = base_mod + (rc_blen - rc_sub_len) + if rc_tail[0] == '.' and string.strncmp(rc_tail + 1, rc_sub_buf, rc_sub_len) == 0: continue + + # build a temporary from_imports "name:full_mod" + rc_fi_len: sizet = name_len_rc + 1 + string.strlen(rc_full_mod) + 1 + rc_fi: str = pool.alloc(rc_fi_len) + if rc_fi is None: continue + viperlib.snprintf(rc_fi, rc_fi_len, "%s:%s", name, rc_full_mod) + + # recursive call to search the submodule + rc_sub_val: int64 = self._lookup_cross_module_cdefine(pool, name, rc_fi, None, depth + 1, node) + if self.Gen.TypeGenCDef.is_cdefine_found() != 0: + result_val = rc_sub_val + result_found = 1 + + stdlib.free(pyi_buf) + + # 9.6 star-fallback module had no match: fall back to walking imported_modules, + # e.g. `from stdint import *` names stdint but SHARE_READ lives in `import win32.fileio`. + if result_found == 0 and imported_modules is not None: + im2_len: sizet = string.strlen(imported_modules) + im2_ci: sizet = 0 + while im2_ci < im2_len and result_found == 0: + while im2_ci < im2_len and imported_modules[im2_ci] == ' ': im2_ci += 1 + if im2_ci >= im2_len: break + im2_start: sizet = im2_ci + while im2_ci < im2_len and imported_modules[im2_ci] != ' ': im2_ci += 1 + im2_end: sizet = im2_ci + im2_nlen: sizet = im2_end - im2_start + if im2_nlen > 0 and im2_nlen < 256: + im2_buf: str = pool.alloc(im2_nlen + 1) + if im2_buf is not None: + for mk3 in range(int(im2_nlen)): + im2_buf[mk3] = imported_modules[im2_start + mk3] + im2_buf[im2_nlen] = '\0' + sub_fi2: str = pool.alloc(im2_nlen + 3) + if sub_fi2 is not None: + sub_fi2[0] = '*' + sub_fi2[1] = ':' + for mk4 in range(int(im2_nlen)): + sub_fi2[2 + mk4] = im2_buf[mk4] + sub_fi2[2 + im2_nlen] = '\0' + sub_val2: int64 = self._lookup_cross_module_cdefine(pool, name, sub_fi2, None, depth + 1, node) + if self.Gen.TypeGenCDef.is_cdefine_found() != 0: + result_val = sub_val2 + result_found = 1 + + # 10. cache into the current module's CDefine table and the cross-module perm_pool; + # float CDefines are already cached as double, so only integers are written here. + if result_found != 0: + if res_is_str != 0: + # string CDefine: the value was already registered in the match loop (val_buf is + # the hit line's content, valid across the function); only write the cross cache. + if base_mod is not None: + ck_len_s: sizet = string.strlen(name) + string.strlen(base_mod) + 2 + ckey_s: str = pool.alloc(ck_len_s) + if ckey_s is not None: + viperlib.snprintf(ckey_s, ck_len_s, "%s@%s", name, base_mod) + self.Gen.TypeGenCDef.register_xmod_cdefine(ckey_s, val_buf, 0, 1) + elif res_is_double == 0: + self.Gen.TypeGenCDef.register_cdefine_constant(pool, name, result_val) + # cross-module cache (perm_pool, reused across files), integers: key matches the + # entry query "name@srcModule" (truncated base_mod, leading dots removed). + if base_mod is not None: + ck_len: sizet = string.strlen(name) + string.strlen(base_mod) + 2 + ckey_buf: str = pool.alloc(ck_len) + if ckey_buf is not None: + viperlib.snprintf(ckey_buf, ck_len, "%s@%s", name, base_mod) + cbuf_i: str = pool.alloc(32) + if cbuf_i is not None: + viperlib.snprintf(cbuf_i, 32, "%lld", result_val) + self.Gen.TypeGenCDef.register_xmod_cdefine(ckey_buf, cbuf_i, 0, 0) + else: + # float: fetch %.17g text from the local table and write the cross cache (lossless) + if base_mod is not None: + ck_len2: sizet = string.strlen(name) + string.strlen(base_mod) + 2 + ckey2: str = pool.alloc(ck_len2) + if ckey2 is not None: + viperlib.snprintf(ckey2, ck_len2, "%s@%s", name, base_mod) + dval_cc: double = self.Gen.TypeGenCDef.lookup_cdefine_double(name) + if self.Gen.TypeGenCDef.is_cdefine_double() != 0: + cbuf_d: str = pool.alloc(64) + if cbuf_d is not None: + stdio.snprintf(cbuf_d, 64, "%.17g", dval_cc) + self.Gen.TypeGenCDef.register_xmod_cdefine(ckey2, cbuf_d, 1, 0) + self.Gen.TypeGenCDef.set_cdefine_found(1) + else: self.Gen.TypeGenCDef.set_cdefine_found(0) + + return result_val + # _is_cdefine_name_pattern - CDefine names follow ALL_CAPS (A-Z, 0-9, _ with >=1 uppercase); + # lowercase keywords/variables won't match, avoiding fail-fast false positives. 1=match, 0=no. + def _is_cdefine_name_pattern(self, name: str) -> int: + """Return 1 if name follows the CDefine convention (uppercase letters, digits, underscore)""" + if name is None: return 0 + has_upper: int = 0 + i: sizet = 0 + while name[i] != '\0': + c: char = name[i] + if c >= 'a' and c <= 'z': return 0 + if c >= 'A' and c <= 'Z': has_upper = 1 + i += 1 + return has_upper + # ============================================================ + # Translate a variable reference (Name node) -> load + def _const_int_for_cdefine(self, pool: memhub.MemBuddy*, name: str, value: int64) -> mlirlite.Value*: + """Materialize a CDefine integer constant as an LLVM constant (TPC TypeConvert aligned) + + 位宽优先用 define 的**已知位宽**(右侧显式强转或声明注解,见 register_cdefine_constant 的 bits); + 没有位宽记录时按**推断位宽**物化(值的最小可存宽度;0x80000000 → 32)。 + 这样 `define X: WORD = WORD(...)` 的 X 保持 u16、`X | Y` 里两个同宽 u16 不会被提升成 + i32;无注解的 `define A = 0x80000000` 也不会退化成 i64 把 `A | B` 放大到 64 位, + 而小值 define 说成 8/16 也在算术提升下不会溢出。 + """ + dbits: int = 0 + if name is not None: dbits = self.Gen.TypeGenCDef.lookup_cdefine_bits(name) + if dbits == 8 or dbits == 16 or dbits == 32 or dbits == 64: + return mlirlite.const_int(pool, dbits, self._cdefine_bits_value(value, dbits)) + # 无位宽记录(例如只从 .vpi 跨模块引入、未登记 bits)→ 按**推断位宽**物化: + # 值的最小可存宽度(2^31..2^32-1 取 32 位无符号位型,不退化成 i64; + # 小值取 8/16,参与算术时由整数提升先升到 i32,不会溢出)。 + wmin: int = self.Gen.AssignGen.define_min_width(value) + return mlirlite.const_int(pool, wmin, self._cdefine_bits_value(value, wmin)) + + # ============================================================ + # cdefine_use_width_value - materialize CDefine constants at the use-site width (TPC-aligned) + def _cdefine_bits_value(self, value: int64, bits: int) -> int64: + """把值按位宽**截断**后再重解释为该位宽的有符号表示(与 AssignGenMixin._normalize_cdefine_bits 同口径)。 + + 「左侧只会看到截断了的数据」:超宽的值先按 bits 取低位(WORD(0x80000000) → 0), + 而不是只对"刚好越界一点"的值做重解释。 + 实现用左移后算术右移,而不是 `value & 0xFFFFFFFF` 这类大掩码字面量: + 掩码字面量(如 4294967295)超出 int32 范围,其加宽方向在 TPC/VPC 上可能判成 sext, + 会得到 0xFFFFFFFFFFFFFFFF 而不是 0xFFFFFFFF,导致截断失效。 + """ + if bits == 8: return (value << 56) >> 56 + if bits == 16: return (value << 48) >> 48 + if bits == 32: return (value << 32) >> 32 + return value + + def cdefine_use_width_value(self, pool: memhub.MemBuddy*, node: ast.AST*, name: str, value: int64, target_ty: mlirlite.LLVMType*, use_ctx: str = None) -> mlirlite.Value*: + """Materialize a CDefine integer constant at a known use-site width, or None on fallback""" + bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(target_ty) + if bits != 8 and bits != 16 and bits != 32 and bits != 64: return None + wmin: int = self.Gen.AssignGen.define_min_width(value) + if bits < wmin: + # "representable range at the use site" uses the same rule as the check = TPC + # DefineWidthLoHi = [-(2^(bits-1)), 2^bits - 1]; literals built from safe 2147483647. + m31: int64 = 2147483647 + lo: int64 = -m31 - 1 + hi: int64 = m31 * 2 + 1 + if bits == 8: + lo = -128 + hi = 255 + elif bits == 16: + lo = -32768 + hi = 65535 + ctx: str = "" + if use_ctx is not None: + # this language's str is i8* with no `str + str`: `+` degrades to pointer add. + # Build strings with f-string interpolation / string.strcat / viperlib.snprintf. + ctx = f"({use_ctx})" + self.Gen.TypeGenErr.fatal_error(node, f"[VPC] define constant use site width insufficient: constant '{name}' actual value {value} " f"needs at least {wmin} bits (minimum representable width W_min), " f"but the use site{ctx} provides only {bits} bits " f"(the acceptable range for this bit width under both signed/unsigned interpretation: {lo}..{hi}). " f"silent truncation/downgrade is forbidden, compilation aborted. " f"fix: widen the type of the use site{ctx} to at least {wmin} bits " f"(e.g. int{wmin}T / CUInt{wmin}T or wider types), or use a smaller constant value.", "TypeError") + return None return mlirlite.const_int(pool, bits, self._cdefine_bits_value(value, bits)) \ No newline at end of file diff --git a/App/lib/core/Gen/Expr/ExprGenMixin.vp b/App/lib/core/Gen/Expr/ExprGenMixin.vp index 67487cb..96363b8 100644 --- a/App/lib/core/Gen/Expr/ExprGenMixin.vp +++ b/App/lib/core/Gen/Expr/ExprGenMixin.vp @@ -1,82 +1,82 @@ -import t, c -from stdint import * -import ast -import memhub -import mlirlite -import lib.core.Gen.Translator as HT -import lib.core.Gen.Expr.ExprGenBase as ExprGenBase - - -# docstring registry capacities (__doc__ magic attribute support). -# DOC_MAX: max entries; DOC_KEY_SZ: key "{module_sha1}.{symbol}" width; DOC_VAL_SZ: content width. -define DOC_MAX = 512 -define DOC_KEY_SZ = 192 -define DOC_VAL_SZ = 4096 - -# f-string translation constants (aligned with TPC HandlesExprFormat). -# FSTR_BUF_SIZE: runtime stack buffer; FSTR_SPEC_MAX: format_spec parse width; FSTR_FMT_MAX: printf format width. -define FSTR_BUF_SIZE = 1024 -define FSTR_SPEC_MAX = 64 -define FSTR_FMT_MAX = 64 - - -# ExprGenMixin - expression handling mixin (objectified P3). - - -# ExprGenMixin - expressions / constants / subscripts / attributes / operators. -novirtual class ExprGenMixin(ExprGenBase.ExprGenBase): - """Expression-handling mixin (pure methods, no state fields).""" - - # Translate a value expression (RHS dispatch) - module-level version. - def translate_value(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr = None, func_count: int = 0, trans: HT.Translator* = None, target_ty: mlirlite.LLVMType* = None, use_ctx: str = None) -> mlirlite.Value*: - """Translate a value expression to an LLVM Value (depth-guard wrapper; see _translate_value_impl). - target_ty = assignment/parameter type context; use_ctx = real call site for CDefine width checks.""" - if trans is not None: - if trans.TranslateDepth >= HT.TRANSLATE_MAX_DEPTH: self.Gen.TypeGenErr.fatal_error(node, "expression translation nested too deep (suspected AST self-reference cycle or translation bug)", "RecursionError") - trans.TranslateDepth += 1 - rv: mlirlite.Value* = self._translate_value_impl(builder, pool, mod, node, funcs_ptr, func_count, trans, target_ty, use_ctx) - if trans is not None: trans.TranslateDepth -= 1 - return rv - - def _translate_value_impl(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr = None, func_count: int = 0, trans: HT.Translator* = None, target_ty: mlirlite.LLVMType* = None, use_ctx: str = None) -> mlirlite.Value*: - """Translate a value expression to an LLVM Value.""" - if node is None: return None - k: int = node.kind() - if k == ast.ASTKind.Constant: - rv: mlirlite.Value* = self.Gen.ExprGenConst.translate_constant(builder, pool, mod, node, trans, target_ty) - return rv - elif k == ast.ASTKind.Name: return self.Gen.ExprGenName.translate_name_value(builder, pool, node, trans, target_ty, use_ctx) - elif k == ast.ASTKind.BinOp: return self.Gen.ExprGenOps.translate_binop(pool, builder, mod, node, trans) +import t, c +from stdint import * +import ast +import memhub +import mlirlite +import lib.core.Gen.Translator as HT +import lib.core.Gen.Expr.ExprGenBase as ExprGenBase + + +# docstring registry capacities (__doc__ magic attribute support). +# DOC_MAX: max entries; DOC_KEY_SZ: key "{module_sha1}.{symbol}" width; DOC_VAL_SZ: content width. +define DOC_MAX = 512 +define DOC_KEY_SZ = 192 +define DOC_VAL_SZ = 4096 + +# f-string translation constants (aligned with TPC HandlesExprFormat). +# FSTR_BUF_SIZE: runtime stack buffer; FSTR_SPEC_MAX: format_spec parse width; FSTR_FMT_MAX: printf format width. +define FSTR_BUF_SIZE = 1024 +define FSTR_SPEC_MAX = 64 +define FSTR_FMT_MAX = 64 + + +# ExprGenMixin - expression handling mixin (objectified P3). + + +# ExprGenMixin - expressions / constants / subscripts / attributes / operators. +novirtual class ExprGenMixin(ExprGenBase.ExprGenBase): + """Expression-handling mixin (pure methods, no state fields).""" + + # Translate a value expression (RHS dispatch) - module-level version. + def translate_value(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr = None, func_count: int = 0, trans: HT.Translator* = None, target_ty: mlirlite.LLVMType* = None, use_ctx: str = None) -> mlirlite.Value*: + """Translate a value expression to an LLVM Value (depth-guard wrapper; see _translate_value_impl). + target_ty = assignment/parameter type context; use_ctx = real call site for CDefine width checks.""" + if trans is not None: + if trans.TranslateDepth >= HT.TRANSLATE_MAX_DEPTH: self.Gen.TypeGenErr.fatal_error(node, "expression translation nested too deep (suspected AST self-reference cycle or translation bug)", "RecursionError") + trans.TranslateDepth += 1 + rv: mlirlite.Value* = self._translate_value_impl(builder, pool, mod, node, funcs_ptr, func_count, trans, target_ty, use_ctx) + if trans is not None: trans.TranslateDepth -= 1 + return rv + + def _translate_value_impl(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr = None, func_count: int = 0, trans: HT.Translator* = None, target_ty: mlirlite.LLVMType* = None, use_ctx: str = None) -> mlirlite.Value*: + """Translate a value expression to an LLVM Value.""" + if node is None: return None + k: int = node.kind() + if k == ast.ASTKind.Constant: + rv: mlirlite.Value* = self.Gen.ExprGenConst.translate_constant(builder, pool, mod, node, trans, target_ty) + return rv + elif k == ast.ASTKind.Name: return self.Gen.ExprGenName.translate_name_value(builder, pool, node, trans, target_ty, use_ctx) + elif k == ast.ASTKind.BinOp: return self.Gen.ExprGenOps.translate_binop(pool, builder, mod, node, trans) elif k == ast.ASTKind.Call: return self.Gen.CallGen.translate_call(pool, builder, mod, node, funcs_ptr, func_count, trans) # M2.4 `new stack|heap|memgr(x) [slife|hlife] T(args)`:只换"取存储",其余复用构造序列 - elif k == ast.ASTKind.New: return self.Gen.CallGenCtor._translate_new_expr(pool, builder, mod, ast.New*(node), trans) - elif k == ast.ASTKind.Compare: return self.Gen.ExprGenOps.translate_compare(builder, pool, mod, node, trans) - elif k == ast.ASTKind.UnaryOp: return self.Gen.ExprGenOps.translate_unaryop(builder, pool, mod, node, trans) - elif k == ast.ASTKind.BoolOp: return self.Gen.ExprGenOps.translate_boolop(builder, pool, mod, node, funcs_ptr, func_count, trans) - elif k == ast.ASTKind.Subscript: return self.Gen.ExprGenName.translate_subscript(builder, pool, mod, node, trans) - elif k == ast.ASTKind.Attribute: return self.Gen.ExprGenAttr.translate_attribute(builder, pool, mod, node, trans) - elif k == ast.ASTKind.List: return self.Gen.ExprGenConst.translate_list_literal(builder, pool, mod, node, None, trans) - elif k == ast.ASTKind.IfExp: return self.Gen.ExprGenOps.translate_ifexp(builder, pool, mod, node, trans) - elif k == ast.ASTKind.Lambda: return self.Gen.FuncGen.translate_lambda(builder, pool, mod, node, trans) - elif k == ast.ASTKind.JoinedStr: return self.Gen.ExprGenFstr.translate_joined_str(builder, pool, mod, node, trans) - elif k == ast.ASTKind.FormattedValue: return self.Gen.ExprGenFstr.translate_formatted_value(builder, pool, mod, node, trans) - elif k == ast.ASTKind.Await: - # Coroutine await (TPV-M1): inline-translate when the await target is an async call. - # Returns the child coroutine's i32 ret; top-level await is handled by AsyncGen. - aw: ast.Await* = ast.Await*(node) - if aw is not None and aw.value is not None and aw.value.kind() == ast.ASTKind.Call: - aw_cl: ast.Call* = ast.Call*(aw.value) - if aw_cl.func is not None and aw_cl.func.kind() == ast.ASTKind.Name: - aw_nm: ast.Name* = ast.Name*(aw_cl.func) - if aw_nm.id is not None and trans is not None: - if self.Gen.AsyncGen.is_async_func(trans, aw_nm.id) != 0: return self.Gen.AsyncGen._emit_await(builder, pool, mod, trans, aw, aw_nm.id) - return None - elif k == ast.ASTKind.Starred: - # *x in a value context: the parser tags '*' as Starred, but Viper has no unpacking. - # Treat it as C unary deref (*ptr -> load pointee) by building a Deref UnaryOp. - st: ast.Starred* = ast.Starred*(node) - if st is None or st.value is None: return None - deref_node: ast.AST* = ast.UnaryOp(pool, ast.OpKind.Deref, st.value) - if deref_node is None: return None - return self.Gen.ExprGenOps.translate_unaryop(builder, pool, mod, deref_node, trans) - return None + elif k == ast.ASTKind.New: return self.Gen.CallGenCtor._translate_new_expr(pool, builder, mod, ast.New*(node), trans) + elif k == ast.ASTKind.Compare: return self.Gen.ExprGenOps.translate_compare(builder, pool, mod, node, trans) + elif k == ast.ASTKind.UnaryOp: return self.Gen.ExprGenOps.translate_unaryop(builder, pool, mod, node, trans) + elif k == ast.ASTKind.BoolOp: return self.Gen.ExprGenOps.translate_boolop(builder, pool, mod, node, funcs_ptr, func_count, trans) + elif k == ast.ASTKind.Subscript: return self.Gen.ExprGenName.translate_subscript(builder, pool, mod, node, trans) + elif k == ast.ASTKind.Attribute: return self.Gen.ExprGenAttr.translate_attribute(builder, pool, mod, node, trans) + elif k == ast.ASTKind.List: return self.Gen.ExprGenConst.translate_list_literal(builder, pool, mod, node, None, trans) + elif k == ast.ASTKind.IfExp: return self.Gen.ExprGenOps.translate_ifexp(builder, pool, mod, node, trans) + elif k == ast.ASTKind.Lambda: return self.Gen.FuncGen.translate_lambda(builder, pool, mod, node, trans) + elif k == ast.ASTKind.JoinedStr: return self.Gen.ExprGenFstr.translate_joined_str(builder, pool, mod, node, trans) + elif k == ast.ASTKind.FormattedValue: return self.Gen.ExprGenFstr.translate_formatted_value(builder, pool, mod, node, trans) + elif k == ast.ASTKind.Await: + # Coroutine await (VPC-M1): inline-translate when the await target is an async call. + # Returns the child coroutine's i32 ret; top-level await is handled by AsyncGen. + aw: ast.Await* = ast.Await*(node) + if aw is not None and aw.value is not None and aw.value.kind() == ast.ASTKind.Call: + aw_cl: ast.Call* = ast.Call*(aw.value) + if aw_cl.func is not None and aw_cl.func.kind() == ast.ASTKind.Name: + aw_nm: ast.Name* = ast.Name*(aw_cl.func) + if aw_nm.id is not None and trans is not None: + if self.Gen.AsyncGen.is_async_func(trans, aw_nm.id) != 0: return self.Gen.AsyncGen._emit_await(builder, pool, mod, trans, aw, aw_nm.id) + return None + elif k == ast.ASTKind.Starred: + # *x in a value context: the parser tags '*' as Starred, but Viper has no unpacking. + # Treat it as C unary deref (*ptr -> load pointee) by building a Deref UnaryOp. + st: ast.Starred* = ast.Starred*(node) + if st is None or st.value is None: return None + deref_node: ast.AST* = ast.UnaryOp(pool, ast.OpKind.Deref, st.value) + if deref_node is None: return None + return self.Gen.ExprGenOps.translate_unaryop(builder, pool, mod, deref_node, trans) + return None diff --git a/App/lib/core/Gen/Expr/ExprGenOps.vp b/App/lib/core/Gen/Expr/ExprGenOps.vp index 032950c..9e73006 100644 --- a/App/lib/core/Gen/Expr/ExprGenOps.vp +++ b/App/lib/core/Gen/Expr/ExprGenOps.vp @@ -1,1207 +1,1207 @@ -import lib.core.Gen.GenTypes as GenTypes -import t, c -from stdint import * -import ast -import mlirlite -import memhub -import string -import viperlib -import lib.core.Gen.Translator as HT -import lib.core.Gen.Type.TypeSpec as TSpec - - -import lib.core.Gen.Expr.ExprGenBase as ExprGenBase - -# ExprGenOps - ExprGenMixin split (stage D): compare / unary / bool / binop operations -# ============================================================ -novirtual class ExprGenOps(ExprGenBase.ExprGenBase): - """Comparison/unary/boolean/binop operations""" - - # translate ternary IfExp(test, body, orelse): emit `cond ? body : orelse` with then/else blocks - # branching to a merge block whose phi is [body_val, then_blk], [orelse_val, else_blk]. - def translate_ifexp(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: - """Translate a ternary (body if test else orelse) into a phi Value.""" - if node is None or builder is None or trans is None: return None - ie: ast.IfExp* = ast.IfExp*(node) - if ie is None: return None - - func: mlirlite.Function* = trans._cur_func - if func is None: return None - - # 1. Evaluate the condition - cond_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.test, None, 0, trans) - if cond_val is None: return None - # Convert to i1 (prefer dispatching __bool__) - cond_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, cond_val, trans) - - # 2. Create the three basic blocks - cnt: int = trans._label_counter - trans._label_counter = cnt + 1 - name_buf: str = pool.alloc(32) - if name_buf is None: return None - viperlib.snprintf(name_buf, 32, "ifexp.then.%d", cnt) - then_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) - viperlib.snprintf(name_buf, 32, "ifexp.else.%d", cnt) - else_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) - viperlib.snprintf(name_buf, 32, "ifexp.merge.%d", cnt) - merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) - - if then_bb is None or else_bb is None or merge_bb is None: return None - - # 3. Conditional branch - mlirlite.build_cond_br(builder, cond_i1, then_bb, else_bb) - - # 4. then branch - mlirlite.position_at_end(builder, then_bb) - body_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.body, None, 0, trans) - then_blk_for_phi: mlirlite.BasicBlock* = trans._cur_builder.CurBlock - if body_val is None: - # fail-fast: ternary then-branch translation failed; abort compilation (expose the upstream root cause) - self.Gen.TypeGenErr.fatal_error(ie.body, "ternary expression branch translation failed", "TypeError") - # Do not br yet; br after type alignment - - # 5. else branch. When the then-branch is a pointer, pass its type as the else target-type context: a single-quoted - # 1-char literal (CONST_CHAR, e.g. '?') materializes as a 1-char string, otherwise as i32 -> Coerce CastError. - orelse_target_ty: mlirlite.LLVMType* = None - if self.is_ptr_type(body_val.Ty) != 0: orelse_target_ty = body_val.Ty - mlirlite.position_at_end(builder, else_bb) - orelse_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.orelse, None, 0, trans, orelse_target_ty) - else_blk_for_phi: mlirlite.BasicBlock* = trans._cur_builder.CurBlock - if orelse_val is None: - # fail-fast: ternary else-branch translation failed; abort compilation (expose the upstream root cause) - self.Gen.TypeGenErr.fatal_error(ie.orelse, "ternary expression branch translation failed", "TypeError") - # Do not br yet; br after type alignment - - # Type alignment (the two branches may differ): coerce_to_type must run inside each branch so the merge block's - # PHI is its first instruction (otherwise sext/zext/ptrtoint/inttoptr land before the PHI, violating LLVM rules). - body_bits: int = self.get_llvm_type_bits(body_val.Ty) - orelse_bits: int = self.get_llvm_type_bits(orelse_val.Ty) - body_is_ptr: int = self.is_ptr_type(body_val.Ty) - orelse_is_ptr: int = self.is_ptr_type(orelse_val.Ty) - # Case 0: one side is a pointer and the other an integer (e.g. i64 vs i8* null); must be handled first since - # Ptr.bits == 0 skips the next three branches. body_val.Ty becomes phi_ty and orelse_val is coerced to it. - if body_is_ptr != orelse_is_ptr: - mlirlite.position_at_end(builder, else_blk_for_phi) - orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node) - elif body_bits > orelse_bits and orelse_bits > 0: - # Coerce orelse_val in the else block - mlirlite.position_at_end(builder, else_blk_for_phi) - orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node) - elif orelse_bits > body_bits and body_bits > 0: - # Back in the then block, coerce body_val - mlirlite.position_at_end(builder, then_blk_for_phi) - body_val = self.coerce_to_type(builder, body_val, orelse_val.Ty, node) - elif body_bits == orelse_bits and body_bits > 0: - # Same bits but different types (e.g. i64 vs Ptr): null cannot be used in an i64 PHI. - # This branch should not trigger (case 0 covers it); kept as a defensive guard. - if self.is_ptr_type(body_val.Ty) != self.is_ptr_type(orelse_val.Ty): - mlirlite.position_at_end(builder, else_blk_for_phi) - orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node) - - # Now br to merge from the then/else blocks (after type coercion) - mlirlite.position_at_end(builder, then_blk_for_phi) - mlirlite.build_br(builder, merge_bb) - mlirlite.position_at_end(builder, else_blk_for_phi) - mlirlite.build_br(builder, merge_bb) - - # 6. merge block: phi (the PHI is the merge block's first instruction) - mlirlite.position_at_end(builder, merge_bb) - - phi_ty: mlirlite.LLVMType* = body_val.Ty - if phi_ty is None: - # fail-fast: ternary branch type missing; abort compilation (expose the upstream root cause) - self.Gen.TypeGenErr.fatal_error(node, "ternary expression branch type missing", "TypeError") - - inc1: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, body_val, then_blk_for_phi) - inc2: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, orelse_val, else_blk_for_phi) - if inc1 is None or inc2 is None: return None - # List order: inc1 (head) -> inc2 (tail). Writing inc2.Next = inc1 by mistake left inc1.Next as None, emitting a - # single phi entry and triggering LLVM verification: "PHINode should have one entry for each predecessor". - inc1.Next = inc2 - return mlirlite.build_phi(builder, phi_ty, inc1, 2) - # translate_compare - Compare(left, ops, comparators) - # ============================================================ - def should_zext_widen(self, pool: memhub.MemBuddy*, narrow_val: mlirlite.Value*, wide_val: mlirlite.Value*, narrow_node: ast.AST* = None, trans: HT.Translator* = None) -> int: - """Decide integer widening: 1 = zext (unsigned semantics), 0 = sext.""" - if narrow_val is None or narrow_val.Ty is None: return 0 - narrow_bits: int = self.get_llvm_type_bits(narrow_val.Ty) - if narrow_bits == 1: return 1 - if narrow_bits == 8: - # i8 defaults to unsigned-byte handling (systems convention), but an explicitly signed source annotation - # must sext to preserve sign: `int32(v)` with v: int8 = -50 must stay -50, not become 206. - if self._node_type_signed(pool, narrow_node, trans) != 0: return 0 - return 1 - # Wide-side constant heuristic - if wide_val is not None and wide_val.IsConst == 1: - wide_bits: int = self.get_llvm_type_bits(wide_val.Ty) - if wide_bits > narrow_bits and narrow_bits < 64: - signed_max: int64 = (int64(1) << (narrow_bits - 1)) - 1 - if wide_val.IntVal > signed_max: return 1 - # Narrow side resolves to an unsigned type (e.g. t.CUIntXXT literal / cast). trans must be threaded down: for - # Name/Subscript, unsignedness comes from trans.SymTab; without trans it returns 0 and sext is wrongly used. - if narrow_node is not None: - if self._node_type_unsigned(pool, narrow_node, trans) != 0: return 1 - # A CDefine constant with the high bit set (0x80000000..0xFFFFFFFF) -> zext. CDefine is a compile-time value, - # so _node_type_unsigned finds no annotation; the negative-signed range means an unsigned sentinel (e.g. 0xFFFFFFFF). - if narrow_node is not None and narrow_node.kind() == ast.ASTKind.Name: - cdef_nm_zw: ast.Name* = ast.Name*(narrow_node) - if cdef_nm_zw.id is not None: - cdv_zw: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(cdef_nm_zw.id) - if self.Gen.TypeGenCDef.is_cdefine_found() != 0: - if cdv_zw > 0x7FFFFFFF: return 1 - return 0 - def _node_type_unsigned(self, pool: memhub.MemBuddy*, node: ast.AST*, trans: HT.Translator* = None) -> int: - """Return 1 if the AST node resolves to an unsigned integer type, else 0.""" - if node is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(cl.func) - if at.value is not None and at.value.kind() == ast.ASTKind.Name: - mod_nm: ast.Name* = ast.Name*(at.value) - if mod_nm.id is not None and string.strcmp(mod_nm.id, "t") == 0 and at.attr is not None: - if self._type_name_unsigned(at.attr) != 0: return 1 - return 0 - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm.id is not None: - # 1) The name itself is an unsigned type name (e.g. the UInt32T alias) - if self._type_name_unsigned(nm.id) != 0: return 1 - # 2) The variable's annotation type is unsigned (query SymTab when trans is available) - if trans is not None: - ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) - if ve is not None: - # [S4 read] An explicit unsigned Spec (TS_UINT) takes priority over the string check - if self._entry_spec_unsigned(ve.Spec) != 0: return 1 - vcn_u: str = self.Gen.VarGen.var_annot_class_name(ve) - if vcn_u is not None: - if self._type_name_unsigned(vcn_u) != 0: return 1 - return 0 - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(node) - if self._node_type_unsigned(pool, bop.left, trans) != 0: return 1 - if self._node_type_unsigned(pool, bop.right, trans) != 0: return 1 - return 0 - if k == ast.ASTKind.UnaryOp: - uop: ast.UnaryOp* = ast.UnaryOp*(node) - if self._node_type_unsigned(pool, uop.operand, trans) != 0: return 1 - return 0 - if k == ast.ASTKind.Subscript: - # Array/pointer subscript: md5_buf[i] (t.CArray[uint8, N]) or buf[i] (param uint8*) has an unsigned element -> zext. - sub_su: ast.Subscript* = ast.Subscript*(node) - if sub_su.value is not None and sub_su.value.kind() == ast.ASTKind.Name: - snm_su: ast.Name* = ast.Name*(sub_su.value) - if snm_su.id is not None and trans is not None: - ve_su: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, snm_su.id) - if ve_su is not None: - # [S4 read] An explicit unsigned Spec wins; for union annotations the Spec is still Ptr/Array -> returns 0 and falls back to the string path. - if self._entry_spec_unsigned(ve_su.Spec) != 0: return 1 - scn_u: str = self.Gen.VarGen.var_annot_class_name(ve_su) - if scn_u is not None: - if self._annot_str_unsigned(scn_u) != 0: return 1 - return 0 - if k == ast.ASTKind.Attribute: - # Struct field (e.g. viperio.Buf's self.capacity/length: sizet): get the class name from the receiver's AnnotClassName, then look up the field annotation to decide unsignedness. Read-only; returns 0 on failure, so find_struct + manual field walk is used instead of lookup_field_by_class. - at_u: ast.Attribute* = ast.Attribute*(node) - if at_u.value is not None and at_u.attr is not None and trans is not None: - cls_nm_u: str = None - if at_u.value.kind() == ast.ASTKind.Name: - vn_u: ast.Name* = ast.Name*(at_u.value) - if vn_u.id is not None: - ve_u: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, vn_u.id) - if ve_u is not None: cls_nm_u = self.Gen.VarGen.var_annot_class_name(ve_u) - if cls_nm_u is not None: - # Take the bare class name (before the dot): look up by bare name only, avoiding module-qualified resolution - bare_u: str = cls_nm_u - dot_u: str = self.Gen.StructGen._last_dot_outside_brackets(cls_nm_u) - if dot_u is not None: bare_u = dot_u + 1 - if bare_u is not None: - st_u: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(bare_u) - if st_u is not None: - for fi_u in range(int(st_u.FieldCount)): - fe_u: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(st_u, fi_u) - if fe_u is not None and fe_u.Name is not None and string.strcmp(fe_u.Name, at_u.attr) == 0: - # [S4 read] An explicit unsigned field Spec wins - if self._entry_spec_unsigned(fe_u.Spec) != 0: return 1 - fcn_u: str = self.Gen.StructGen.get_field_annot_class_name(fe_u) - if fcn_u is not None: - if self._annot_str_unsigned(fcn_u) != 0: return 1 - break - return 0 - return 0 - def _node_type_signed(self, pool: memhub.MemBuddy*, node: ast.AST*, trans: HT.Translator* = None) -> int: - """Return 1 if the source node is explicitly a signed integer, else 0.""" - if node is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Call: - cl: ast.Call* = ast.Call*(node) - if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(cl.func) - if at.value is not None and at.value.kind() == ast.ASTKind.Name: - mod_nm: ast.Name* = ast.Name*(at.value) - if mod_nm.id is not None and string.strcmp(mod_nm.id, "t") == 0 and at.attr is not None: - if self._spec_sign_from_annot(at.attr) == TSpec.TS_SIGN_SIGNED: return 1 - return 0 - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm.id is not None: - if self._spec_sign_from_annot(nm.id) == TSpec.TS_SIGN_SIGNED: return 1 - if trans is not None: - ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) - if ve is not None: - ncn_s: str = self.Gen.VarGen.var_annot_class_name(ve) - if ncn_s is not None: - if self._spec_sign_from_annot(ncn_s) == TSpec.TS_SIGN_SIGNED: return 1 - return 0 - if k == ast.ASTKind.Subscript: - sub_s: ast.Subscript* = ast.Subscript*(node) - if sub_s.value is not None and sub_s.value.kind() == ast.ASTKind.Name: - snm: ast.Name* = ast.Name*(sub_s.value) - if snm.id is not None and trans is not None: - ve_s: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, snm.id) - if ve_s is not None: - scn_s: str = self.Gen.VarGen.var_annot_class_name(ve_s) - if scn_s is not None: - if self._spec_sign_from_annot(scn_s) == TSpec.TS_SIGN_SIGNED: return 1 - return 0 - return 0 - # S4 de-stringification (unsigned detection): the single source of truth is TypeGenCType.spec_sign_of_annot; the old - # strstr-substring fallbacks were deleted (they caused the A4/A5 false positives). UNKNOWN means no opinion -> return 0. - def _spec_sign_from_annot(self, annot: str) -> int: - """Map an annotation to TS_SIGN_*; UNKNOWN takes no position.""" - return self.Gen.TypeGenCType.spec_sign_of_annot(annot, 0) - # _entry_spec_unsigned - whether a variable/field entry's Spec is an explicitly unsigned integer. `Kind == TS_UINT` - # is a strong signal and takes priority; `Kind == TS_INT` is ambiguous, so only the unsigned direction is switched. - def _entry_spec_unsigned(self, sp: TSpec.TypeSpec*) -> int: - """Return 1 if the entry Spec is explicitly unsigned (TS_UINT), else 0.""" - if sp is None: return 0 - if sp.Kind == TSpec.TS_UINT: return 1 - return 0 - def _type_name_unsigned(self, type_name: str) -> int: - """Return 1 if the type name is unsigned; UNKNOWN means 0.""" - if type_name is not None: - if self._spec_sign_from_annot(type_name) == TSpec.TS_SIGN_UNSIGNED: return 1 - return 0 - def _annot_str_unsigned(self, annot: str) -> int: - """Return 1 if the annotation string is unsigned; UNKNOWN means 0.""" - if annot is not None: - if self._spec_sign_from_annot(annot) == TSpec.TS_SIGN_UNSIGNED: return 1 - return 0 - def translate_compare(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: - """Translate a comparison expression and return an i1 result.""" - cmp: ast.Compare* = ast.Compare*(node) - if cmp is None: return None - - ops_list: list[int]* = (list[int]*)(cmp.ops) - if ops_list is None or len(ops_list) == 0: return None - op: int = ops_list.get(0) - - comparators: list[ast.AST*]* = cmp.comparators - if comparators is None or len(comparators) == 0: return None - rhs_node: ast.AST* = comparators.get(0) - rhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rhs_node, None, 0, trans) - if rhs is None: return None - - # === None comparison optimization: x == None / x != None use identity comparison (icmp eq/ne null) === - # Avoids emitting invalid __eq__/__ne__ calls for struct pointers; Python treats == None as identity comparison. - lhs_node: ast.AST* = cmp.left - cmp_with_none: int = 0 - if op == ast.OpKind.Eq or op == ast.OpKind.Ne or op == ast.OpKind.Is or op == ast.OpKind.IsNot: - if rhs_node is not None and rhs_node.kind() == ast.ASTKind.Constant: - rhs_cn: ast.Constant* = ast.Constant*(rhs_node) - if rhs_cn is not None and rhs_cn.const_kind == ast.CONST_NONE: cmp_with_none = 1 - if cmp_with_none == 0 and lhs_node is not None and lhs_node.kind() == ast.ASTKind.Constant: - lhs_cn: ast.Constant* = ast.Constant*(lhs_node) - if lhs_cn is not None and lhs_cn.const_kind == ast.CONST_NONE: cmp_with_none = 1 - - # === Pointer null-check pattern: ptr != 0 / ptr == 0. Skip operator overload and use identity comparison - # (icmp null); otherwise cross-module classes without __eq__/__ne__ get ghost calls -> undefined reference. === - cmp_zero: int = 0 - if op == ast.OpKind.Eq or op == ast.OpKind.Ne: - if rhs_node is not None and self._is_zero_int_literal(rhs_node) != 0: cmp_zero = 1 - if cmp_zero == 0 and lhs_node is not None and self._is_zero_int_literal(lhs_node) != 0: cmp_zero = 1 - - # === Compare operator overload path 1: lhs is a Name bound to a struct variable === - # For value-type variables (e.g. cnt: Counter) try the overload with the alloca pointer; None comparison is skipped. - if cmp_with_none == 0 and lhs_node is not None and lhs_node.kind() == ast.ASTKind.Name and trans is not None: - nm: ast.Name* = ast.Name*(lhs_node) - if nm is not None and nm.id is not None: - lhs_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) - if lhs_alloca is not None: - if cmp_zero == 0 or self.get_ptr_level(lhs_alloca.Ty) == 0: - ovl_self1: mlirlite.Value* = lhs_alloca - # Ptr(Ptr(Struct)) object storage slot: load the object pointer as self (same as translate_binop path 1) to avoid using the slot address as the object address. - if lhs_alloca.Ty is not None and lhs_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(lhs_alloca.Ty.Pointee) != 0: - load_self1: mlirlite.Value* = mlirlite.build_load(builder, lhs_alloca.Ty.Pointee, lhs_alloca) - if load_self1 is not None: ovl_self1 = load_self1 - ovl_result: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, ovl_self1, rhs, op, trans, 1) - if ovl_result is not None: return ovl_result - - # Translate lhs normally - lhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cmp.left, None, 0, trans) - if lhs is None: return None - - # === in / not in: the container rhs calls __contains__ (aligned with TPC HandlesExprOps.py L261-281). - # x in y -> y.__contains__(x); x not in y -> !y.__contains__(x). With no __contains__, return i1 0 (always False). - if op == ast.OpKind.In or op == ast.OpKind.NotIn: - cn_st: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(rhs) - if cn_st is not None: - cn_cls: str = self.Gen.StructGen.get_class_name_by_type(pool, cn_st) - if cn_cls is not None: - cn_lookup: str = pool.alloc(128) - if cn_lookup is not None: - viperlib.snprintf(cn_lookup, 128, "%s.__contains__", cn_cls) - cn_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, cn_lookup) - if cn_fn is not None: - cn_ret: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(cn_fn) - cn_fn_name: str = mlirlite.function_get_name(cn_fn) - if cn_ret is not None and cn_fn_name is not None: - # Align the self argument (container) to __contains__'s first param type - cn_self: mlirlite.Value* = rhs - cn_first: mlirlite.Param* = mlirlite.function_get_param_head(cn_fn) - if cn_first is not None: - cn_self_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(cn_first) - if cn_self_ty is not None: cn_self = mlirlite.build_bitcast(builder, rhs, cn_self_ty) - if cn_self is not None: - # Param list: self -> key (separately null-terminated) - cn_self.Next = lhs - lhs.Next = None - cn_res: mlirlite.Value* = mlirlite.build_call(builder, cn_fn_name, cn_self, 2, cn_ret, 0) - if cn_res is not None: - # Normalize to i1 (__contains__ may return int/i32 etc.) - cn_i1: mlirlite.Value* = cn_res - match cn_ret: - case mlirlite.LLVMType.Int(cn_bits): - if cn_bits > 1: - cn_zero: mlirlite.Value* = mlirlite.ConstInt(pool, cn_ret, 0, "0") - if cn_zero is not None: cn_i1 = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cn_res, cn_zero) - case _: - pass - if op == ast.OpKind.NotIn: - i1_ty_ni: mlirlite.LLVMType* = mlirlite.Int1(pool) - if i1_ty_ni is not None and cn_i1 is not None: - one_i1_ni: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_ni, 1, "1") - if one_i1_ni is not None: cn_i1 = mlirlite.build_xor(builder, cn_i1, one_i1_ni) - if cn_i1 is not None: return cn_i1 - # No __contains__: TPC semantics are always False (i1 0) - i1_ty_in: mlirlite.LLVMType* = mlirlite.Int1(pool) - if i1_ty_in is not None: return mlirlite.ConstInt(pool, i1_ty_in, 0, "0") - return None - - # === Compare operator overload path 2: lhs is Ptr(Struct) === - # Is/IsNot are unmapped so they return None and fall back to native comparison; None and pointer null-checks skip it. - if cmp_with_none == 0 and (cmp_zero == 0 or self.get_ptr_level(lhs.Ty) == 0): - ovl_result2: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, lhs, rhs, op, trans, 1) - if ovl_result2 is not None: return ovl_result2 - - # === Unsigned comparison (A5): unsigned int operands (CUInt64T/CUInt32T etc.) use ULT/ULE/UGT/UGE. - # A fixed signed predicate made uint64 address comparisons wrong across 2^63 (A5 root cause); reuse _node_type_unsigned. - unsigned_l: int = 0 - unsigned_r: int = 0 - if lhs_node is not None: unsigned_l = self._node_type_unsigned(pool, lhs_node, trans) - if rhs_node is not None: unsigned_r = self._node_type_unsigned(pool, rhs_node, trans) - cmp_unsigned: int = 1 if (unsigned_l != 0 or unsigned_r != 0) else 0 - - predicate: int = mlirlite.ICMP_SLT - if op == ast.OpKind.Lt: predicate = mlirlite.ICMP_ULT if cmp_unsigned != 0 else mlirlite.ICMP_SLT - elif op == ast.OpKind.Le: predicate = mlirlite.ICMP_ULE if cmp_unsigned != 0 else mlirlite.ICMP_SLE - elif op == ast.OpKind.Gt: predicate = mlirlite.ICMP_UGT if cmp_unsigned != 0 else mlirlite.ICMP_SGT - elif op == ast.OpKind.Ge: predicate = mlirlite.ICMP_UGE if cmp_unsigned != 0 else mlirlite.ICMP_SGE - elif op == ast.OpKind.Eq: predicate = mlirlite.ICMP_EQ - elif op == ast.OpKind.Ne: predicate = mlirlite.ICMP_NE - elif op == ast.OpKind.Is: - # `x is y` -> pointer/value equality comparison - predicate = mlirlite.ICMP_EQ - elif op == ast.OpKind.IsNot: - # `x is not y` -> pointer/value inequality comparison - predicate = mlirlite.ICMP_NE - - # Type safety: number vs number allowed (sext-aligned by bit width); one-level pointer vs one-level pointer allowed (address compare); number vs pointer(1+) forbidden (fail-fast, except None compare); different-level pointers forbidden. None compare skips this check. - if cmp_with_none == 0: - lhs_ptr_level: int = self.get_ptr_level(lhs.Ty) - rhs_ptr_level: int = self.get_ptr_level(rhs.Ty) - if lhs_ptr_level != rhs_ptr_level: - # Pointer vs integer literal 0 (null-check pattern): 0 is treated as NULL and converted to a same-type - # null pointer (icmp needs matching types), not a type mismatch. - zero_is_rhs: int = 0 - zero_is_lhs: int = 0 - if rhs_ptr_level == 0 and self._is_zero_int_literal(rhs_node) != 0: zero_is_rhs = 1 - if lhs_ptr_level == 0 and self._is_zero_int_literal(lhs_node) != 0: zero_is_lhs = 1 - if zero_is_rhs != 0 or zero_is_lhs != 0: - if zero_is_rhs != 0: rhs = mlirlite.ConstNull(pool, lhs.Ty, "null") - else: lhs = mlirlite.ConstNull(pool, rhs.Ty, "null") - else: - # Pointer vs non-zero integer (C sentinel pattern, e.g. INVALID_HANDLE_VALUE = -1): align with TPC - # HandlesExprOps.py L388-398; ptrtoint the pointer to compare as an integer address. - if lhs_ptr_level != 0 and rhs_ptr_level == 0: lhs = mlirlite.build_ptrtoint(builder, lhs, rhs.Ty) - elif rhs_ptr_level != 0 and lhs_ptr_level == 0: rhs = mlirlite.build_ptrtoint(builder, rhs, lhs.Ty) - else: - # Different-level pointers (1 vs 2 etc.) -> forbidden (fail-fast) - self.Gen.TypeGenErr.fatal_error(cmp, f"comparison type mismatch: pointers of different levels compared (lhs level={lhs_ptr_level}, rhs level={rhs_ptr_level})", "TypeError") - else: - # None comparison: the None side is implicitly 0/null, its form decided by the other side. - # pointer -> same-type null pointer (icmp eq/ne); integer -> 0 (x == 0); struct value -> never None (== false, != true). - lhs_is_none_lit: int = 0 - if lhs_node is not None and lhs_node.kind() == ast.ASTKind.Constant: - lhs_none_cn: ast.Constant* = ast.Constant*(lhs_node) - if lhs_none_cn is not None and lhs_none_cn.const_kind == ast.CONST_NONE: lhs_is_none_lit = 1 - lhs_ptr_level_none: int = self.get_ptr_level(lhs.Ty) - rhs_ptr_level_none: int = self.get_ptr_level(rhs.Ty) - if lhs_is_none_lit != 0: - # None on the left (`None is x` / `None == x`): the rhs type decides None's form - if rhs_ptr_level_none == 0: - rhs_is_struct_none: int = 0 - match rhs.Ty: - case mlirlite.LLVMType.Struct(_, _, _): - rhs_is_struct_none = 1 - case _: - rhs_is_struct_none = 0 - if rhs_is_struct_none != 0: - if op == ast.OpKind.Eq or op == ast.OpKind.Is: return mlirlite.const_int(pool, 1, 0) - return mlirlite.const_int(pool, 1, 1) - # rhs is numeric -> convert the None side to integer 0 (rhs unchanged) - lhs = mlirlite.const_int32(pool, 0) - else: - # None on the right (`x is None` / `x == None`): the lhs type decides None's form - if lhs_ptr_level_none == 0: - # Check whether lhs is a struct value type (a non-pointer Struct) - lhs_is_struct_none: int = 0 - match lhs.Ty: - case mlirlite.LLVMType.Struct(_, _, _): - lhs_is_struct_none = 1 - case _: - lhs_is_struct_none = 0 - if lhs_is_struct_none != 0: - # A struct value is never None: ==/is return false, !=/is not return true - if op == ast.OpKind.Eq or op == ast.OpKind.Is: return mlirlite.const_int(pool, 1, 0) - else: # Ne, IsNot - return mlirlite.const_int(pool, 1, 1) - # lhs is numeric and rhs is i8* null -> convert rhs to numeric 0 (lhs unchanged) - rhs = mlirlite.const_int32(pool, 0) - - # Type alignment: icmp requires matching types on both sides. Only numeric types are aligned (same-level pointers icmp directly); i1 uses zext (1 stays 1). - lhs_bits: int = self.get_llvm_type_bits(lhs.Ty) - rhs_bits: int = self.get_llvm_type_bits(rhs.Ty) - # C 语义整数提升(与算术同一口径):两侧都窄于 32 位时先提升到 i32 再比较,避免 8/16 位 - # 直接比较在符号/边界组合下语义模糊(如 signed i8 与 unsigned u8 直接比)。 - # 提升后把 bits 记为 32,下面的等宽化不再重复处理。 - if lhs_bits != 0 and rhs_bits != 0 and lhs_bits < 32 and rhs_bits < 32: - cmp_i32_pr: mlirlite.LLVMType* = mlirlite.Int32(pool) - cmp_probe: mlirlite.Value* = mlirlite.const_int32(pool, 0) - if self.should_zext_widen(pool, lhs, cmp_probe, cmp.left, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, cmp_i32_pr) - else: lhs = mlirlite.build_sext(builder, lhs, cmp_i32_pr) - if self.should_zext_widen(pool, rhs, cmp_probe, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, cmp_i32_pr) - else: rhs = mlirlite.build_sext(builder, rhs, cmp_i32_pr) - lhs_bits = 32 - rhs_bits = 32 - if lhs_bits != 0 and rhs_bits != 0 and lhs_bits != rhs_bits: - if lhs_bits < rhs_bits: - if self.should_zext_widen(pool, lhs, rhs, cmp.left, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, rhs.Ty) - else: lhs = mlirlite.build_sext(builder, lhs, rhs.Ty) - else: - if self.should_zext_widen(pool, rhs, lhs, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, lhs.Ty) - else: rhs = mlirlite.build_sext(builder, rhs, lhs.Ty) - - # Pointer type alignment: same-level pointers with different pointee types are unified by bitcast (e.g. i8* vs Struct*). - if lhs_bits == 0 and rhs_bits == 0: - lhs_is_ptr_now: int = self.is_ptr_type(lhs.Ty) - rhs_is_ptr_now: int = self.is_ptr_type(rhs.Ty) - if lhs_is_ptr_now != 0 and rhs_is_ptr_now != 0: - # Both are pointers; compare type strings via TypePrint - lhs_ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_500) - rhs_ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_501) - if lhs_ty_buf is not None and rhs_ty_buf is not None: - mlirlite.TypePrint(lhs_ty_buf, 4096, lhs.Ty, pool) - mlirlite.TypePrint(rhs_ty_buf, 4096, rhs.Ty, pool) - if string.strcmp(lhs_ty_buf, rhs_ty_buf) != 0: rhs = mlirlite.build_bitcast(builder, rhs, lhs.Ty) - - # Float comparison: use fcmp instead of icmp when either operand is floating point - lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty) - rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty) - if lhs_fbits != 0 or rhs_fbits != 0: - # Float type alignment (fpext/fptrunc) - if lhs_fbits != 0 and rhs_fbits != 0 and lhs_fbits != rhs_fbits: - if lhs_fbits < rhs_fbits: lhs = mlirlite.build_fpext(builder, lhs, rhs.Ty) - else: rhs = mlirlite.build_fptrunc(builder, rhs, lhs.Ty) - elif lhs_fbits != 0 and rhs_fbits == 0: - # int → float - rhs = mlirlite.build_si2fp(builder, rhs, lhs.Ty) - elif rhs_fbits != 0 and lhs_fbits == 0: lhs = mlirlite.build_si2fp(builder, lhs, rhs.Ty) - # Map the comparison predicate to fcmp - fpredicate: int = mlirlite.FCMP_OEQ - if op == ast.OpKind.Eq or op == ast.OpKind.Is: fpredicate = mlirlite.FCMP_OEQ - elif op == ast.OpKind.Ne or op == ast.OpKind.IsNot: fpredicate = mlirlite.FCMP_ONE - elif op == ast.OpKind.Lt: fpredicate = mlirlite.FCMP_OLT - elif op == ast.OpKind.Le: fpredicate = mlirlite.FCMP_OLE - elif op == ast.OpKind.Gt: fpredicate = mlirlite.FCMP_OGT - elif op == ast.OpKind.Ge: fpredicate = mlirlite.FCMP_OGE - return mlirlite.build_fcmp(builder, fpredicate, lhs, rhs) - - return mlirlite.build_icmp(builder, predicate, lhs, rhs) - # translate_unaryop - UnaryOp(op, operand) - # ============================================================ - def translate_unaryop(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: - """Translate unary operations (-x, +x, not x, ~x).""" - uo: ast.UnaryOp* = ast.UnaryOp*(node) - if uo is None: return None - - operand: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo.operand, None, 0, trans) - if operand is None: return None - - # === Unary operator overload: -obj/+obj/~obj call __neg__/__pos__/__invert__ (only USub/UAdd/Invert have overloads). - if uo.op == ast.OpKind.USub or uo.op == ast.OpKind.UAdd or uo.op == ast.OpKind.Invert: - ovl_u: mlirlite.Value* = self.try_unary_operator_overload(pool, builder, mod, operand, uo.op, trans) - if ovl_u is not None: return ovl_u - - # C 语义整数提升(与算术/比较同一口径):一元 +/-/~ 先把窄于 32 位的整数操作数提升到 i32, - # 这样 `0 - x` / `x ^ -1` 的常量与操作数同宽(否则是 i32 常量对 i8/i16 操作数)。 - # `not x` 不需要:结果为 i1,操作数宽度不影响语义。 - if uo.op == ast.OpKind.USub or uo.op == ast.OpKind.UAdd or uo.op == ast.OpKind.Invert: - u_bits: int = self.get_llvm_type_bits(operand.Ty) - if u_bits != 0 and u_bits < 32: - u_i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - u_probe: mlirlite.Value* = mlirlite.const_int32(pool, 0) - if self.should_zext_widen(pool, operand, u_probe, uo.operand, trans) != 0: operand = mlirlite.build_zext(builder, operand, u_i32_ty) - else: operand = mlirlite.build_sext(builder, operand, u_i32_ty) - - if uo.op == ast.OpKind.USub: - # -x - # Float type uses fneg (emit fneg directly to avoid 0.0 - x type mismatch) - operand_fbits: int = self.get_llvm_float_bits(operand.Ty) - if operand_fbits > 0: return mlirlite.build_fneg(builder, operand) - # Integer: -x = 0 - x, choosing the zero constant by bit width (avoid i32/i64 mismatch) - operand_bits: int = self.get_llvm_type_bits(operand.Ty) - zero: mlirlite.Value* = mlirlite.const_int32(pool, 0) - if operand_bits == 64: zero = mlirlite.const_int64(pool, 0) - elif operand_bits > 0 and operand_bits != 32: - # Other widths (i8/i16 etc.): use i32 zero; operand will be coerced - zero = mlirlite.const_int32(pool, 0) - return mlirlite.build_sub(builder, zero, operand) - elif uo.op == ast.OpKind.UAdd: - # +x = x - return operand - elif uo.op == ast.OpKind.Not: - # not x = (bool(x) == 0), returns i1; prefer dispatching __bool__ - not_cond: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, operand, trans) - if not_cond is None: return None - false_val: mlirlite.Value* = mlirlite.const_int(pool, 1, 0) - return mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, not_cond, false_val) - elif uo.op == ast.OpKind.Invert: - # ~x = x ^ -1 - operand_bits_inv: int = self.get_llvm_type_bits(operand.Ty) - neg1: mlirlite.Value* = mlirlite.const_int32(pool, -1) - if operand_bits_inv == 64: neg1 = mlirlite.const_int64(pool, -1) - return mlirlite.build_xor(builder, operand, neg1) - elif uo.op == ast.OpKind.Deref: - # *p -> load by the pointee type (aligned with translate_c_deref) - if self.Gen.ExprGenBase.is_ptr_type(operand.Ty) != 0: - d_pointee: mlirlite.LLVMType* = operand.Ty.Pointee - if d_pointee is not None: return mlirlite.build_load(builder, d_pointee, operand) - # Non-pointer means type inference degraded: fail-fast, never fall back to reading as i8 - self.Gen.TypeGenErr.fatal_error(node, "the operand of unary '*' dereference must be a pointer type (type inference degraded)", "TypeError") - return None - elif uo.op == ast.OpKind.AddrOf: - # &x -> lvalue address (no load): Name alloca / module global / module function / Attribute field GEP / Subscript element address. - if trans is None: return None - if uo.operand.kind() == ast.ASTKind.Name: - nm_ao: ast.Name* = ast.Name*(uo.operand) - if nm_ao.id is not None: - # P2 willFree:对已失效变量取地址 → fail-fast(与读取路径同一口径) - if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id)) != 0: - self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': already released by a @life(willFree(...)) call", "LifetimeError") - # self/cls object reference: return the object pointer itself (Ptr(Struct)), not the pointer variable's address (Ptr(Ptr(Struct))). - if string.strcmp(nm_ao.id, "self") == 0 or string.strcmp(nm_ao.id, "cls") == 0: - se_ao: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ao.id) - if se_ao is not None and se_ao.Alloca is not None and se_ao.Alloca.Ty is not None: - match se_ao.Alloca.Ty: - case mlirlite.LLVMType.Ptr(sa1): - match sa1: - case mlirlite.LLVMType.Ptr(sa2): - obj_ty_sa: mlirlite.LLVMType* = mlirlite.Ptr(pool, sa2) - return mlirlite.build_load(builder, obj_ty_sa, se_ao.Alloca) - case _: - pass - case _: - pass - v_ao: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id) - if v_ao is None and mod is not None: - # Module global variable: convert via make_global_ref into a quoted Ptr(Ty) reference - gv_ao: mlirlite.GlobalVariable* = self.Gen.ExprGenAttr.find_global_in_module(mod, nm_ao.id) - if gv_ao is not None and gv_ao.Name is not None and gv_ao.Ty is not None: v_ao = self.Gen.ExprGenAttr.make_global_ref(pool, gv_ao.Name, gv_ao.Ty) - if v_ao is not None and v_ao.Ty is not None: - # Object-pointer variable (alloca is Ptr(Ptr(Struct))): the variable is the object, return the object pointer (load once), aligned with translate_c_addr. - match v_ao.Ty: - case mlirlite.LLVMType.Ptr(ao1): - match ao1: - case mlirlite.LLVMType.Ptr(ao2): - match ao2: - case mlirlite.LLVMType.Struct(_, _, _): - obj_ty_ao: mlirlite.LLVMType* = mlirlite.Ptr(pool, ao2) - return mlirlite.build_load(builder, obj_ty_ao, v_ao) - case _: - pass - case _: - pass - case _: - pass - return v_ao - # Module function address: the function symbol itself - if v_ao is None and mod is not None: - f_ao: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, nm_ao.id) - if f_ao is not None: - fname_ao: str = mlirlite.function_get_name(f_ao) - if fname_ao is not None: - # The param chain is a Param chain but mlirlite.Func's type builder uses ParamNode -> rebuild via _FuncTyFromFunction (same as CallGenCBuiltin). - fn_ty_ao: mlirlite.LLVMType* = self.Gen.CallGenBase._FuncTyFromFunction(pool, f_ao) - # Quote the function reference name when needed (sha1-prefixed names starting with a digit or containing '.') - fnlen_ao: sizet = string.strlen(fname_ao) - needs_q_ao: int = self.Gen.ClassGenVTable._vtable_name_needs_quote(fname_ao) - fbuf_ao: str = pool.alloc(fnlen_ao + 4) - if fbuf_ao is not None and fn_ty_ao is not None: - if needs_q_ao != 0: viperlib.snprintf(fbuf_ao, fnlen_ao + 4, "@\"%s\"", fname_ao) - else: viperlib.snprintf(fbuf_ao, fnlen_ao + 4, "@%s", fname_ao) - return mlirlite.SSAValue(pool, mlirlite.Ptr(pool, fn_ty_ao), fbuf_ao) - elif uo.operand.kind() == ast.ASTKind.Attribute: - # Field address (GEP, no load) - fptr_ao: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, uo.operand, trans) - if fptr_ao is not None: return fptr_ao - elif uo.operand.kind() == ast.ASTKind.Subscript: - # Element address (GEP, no load) - sptr_ao: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, uo.operand, trans) - if sptr_ao is not None: return sptr_ao - elif uo.operand.kind() == ast.ASTKind.Call: - # &() -> take the address of an rvalue. Reuse the already-evaluated operand (never re-evaluate: - # a ctor call would re-run __before_init__/__init__). See _addrof_of_call. - ac_ao: mlirlite.Value* = self._addrof_of_call(builder, pool, operand) - if ac_ao is not None: return ac_ao - self.Gen.TypeGenErr.fatal_error(node, "the operand of unary '&' address-of must be an lvalue (Name/Attribute/Subscript) or a constructor/function call result", "TypeError") - return None - return operand - - def _addrof_of_call(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, call_val: mlirlite.Value*) -> mlirlite.Value*: - """Return the address of a call result (&()), reusing the evaluated value.""" - if call_val is None or call_val.Ty is None: return None - if self.Gen.ExprGenBase.is_ptr_type(call_val.Ty) != 0 and call_val.Ty.Pointee is not None: - pointee_is_struct: int = 0 - match call_val.Ty.Pointee: - case mlirlite.LLVMType.Struct(_, _, _): - pointee_is_struct = 1 - case _: - pointee_is_struct = 0 - if pointee_is_struct != 0: return call_val - tmp_slot_ao: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, call_val.Ty) - if tmp_slot_ao is None: return None - mlirlite.build_store(builder, call_val, tmp_slot_ao) - return tmp_slot_ao - # translate_boolop - BoolOp(op, values): short-circuit evaluation returning i1. - # and: a false short-circuits to merge (returns a's i1); or: a true short-circuits; the last value is evaluated then branches to merge; merge phis all i1 edges. - def translate_boolop(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr = None, func_count: int = 0, trans: HT.Translator* = None) -> mlirlite.Value*: - """Translate boolean and/or with short-circuit evaluation, returning i1.""" - bo: ast.BoolOp* = ast.BoolOp*(node) - if bo is None: return None - - op: int = bo.op - values: list[ast.AST*]* = bo.values - if values is None: return None - count: sizet = len(values) - if count == 0: return None - if count == 1: - return self.Gen.ExprGen.translate_value(builder, pool, mod, values.get(0), funcs_ptr, func_count, trans) - - func: mlirlite.Function* = builder.Func - if func is None: return None - - # Create the merge BB (unique label from builder.Counter; position_at_end's move_to_end keeps BB text order matching control-flow order). - cnt: int = builder.Counter - builder.Counter = cnt + 1 - name_buf: str = pool.alloc(32) - if name_buf is None: return None - viperlib.snprintf(name_buf, 32, "bool.merge.%d", cnt) - merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) - - # phi incoming list head/tail - phi_head: mlirlite.PhiIncoming* = None - phi_tail: mlirlite.PhiIncoming* = None - phi_count: int = int(count - 1) # loop iteration count - - # Evaluate and short-circuit each value except the last - for i in range(int(count - 1)): - val_node: ast.AST* = values.get(i) - val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, val_node, funcs_ptr, func_count, trans) - if val is None: return None - - # Convert to i1 (use directly if already i1; else coerce_cond_to_bool, preferring __bool__) - val_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, val, trans) - - # Create the next BB (evaluate the next value) - cnt = builder.Counter - builder.Counter = cnt + 1 - viperlib.snprintf(name_buf, 32, "bool.next.%d", cnt) - next_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) - - # and: false -> merge, true -> next; or: true -> merge, false -> next - if op == ast.OpKind.And: mlirlite.build_cond_br(builder, val_i1, next_bb, merge_bb) - else: mlirlite.build_cond_br(builder, val_i1, merge_bb, next_bb) - - # Add the phi incoming edge: (val_i1, the BB holding cond_br) - cur_bb: mlirlite.BasicBlock* = builder.CurBlock - inc: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, val_i1, cur_bb) - if phi_head is None: - phi_head = inc - phi_tail = inc - else: - phi_tail.Next = inc - phi_tail = inc - - # Move to the next BB and evaluate the next value - mlirlite.position_at_end(builder, next_bb) - - # Last value: evaluate then branch straight to merge - last_node: ast.AST* = values.get(count - 1) - last_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, last_node, funcs_ptr, func_count, trans) - if last_val is None: return None - last_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, last_val, trans) - - mlirlite.build_br(builder, merge_bb) - - cur_bb = builder.CurBlock - inc = mlirlite.new_phi_incoming(pool, last_i1, cur_bb) - if phi_head is None: phi_head = inc - else: phi_tail.Next = inc - phi_count += 1 - - # Move to merge and create the phi merging all incoming edges - mlirlite.position_at_end(builder, merge_bb) - i1_ty: mlirlite.LLVMType* = mlirlite.Int1(pool) - return mlirlite.build_phi(builder, i1_ty, phi_head, phi_count) - # emit_pow - `**` exponentiation for basic types (aligned with TPC ExprGen.emit_binary_op's Pow branch). - # Float -> llvm.pow; int -> compute in double and fptosi back when the exponent is a non-negative constant. Custom types go through __pow__. - def emit_pow(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, trans: HT.Translator* = None) -> mlirlite.Value*: - """Emit `**` exponentiation (basic types go through llvm.pow).""" - if lhs is None or rhs is None: return None - lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty) - rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty) - f64_ty: mlirlite.LLVMType* = mlirlite.Double(pool) - # Float path: either operand floating -> float result - if lhs_fbits != 0 or rhs_fbits != 0: - target_f: mlirlite.LLVMType* = f64_ty - if lhs_fbits == 32 and rhs_fbits == 32: target_f = mlirlite.Float(pool) - lf: mlirlite.Value* = self.coerce_to_type(builder, lhs, target_f, node, trans) - rf: mlirlite.Value* = self.coerce_to_type(builder, rhs, target_f, node, trans) - return self._call_llvm_pow(pool, builder, mod, lf, rf) - # Integer path: promote to double and compute - li: mlirlite.Value* = self.coerce_to_type(builder, lhs, f64_ty, node, trans) - ri: mlirlite.Value* = self.coerce_to_type(builder, rhs, f64_ty, node, trans) - res: mlirlite.Value* = self._call_llvm_pow(pool, builder, mod, li, ri) - if res is None: return None - # Non-negative integer exponent constant -> fptosi back to lhs type (Python int ** int = int) - rhs_bits: int = self.get_llvm_type_bits(rhs.Ty) - if rhs_bits != 0 and rhs.IsConst == 1 and rhs.IntVal >= 0: return self.coerce_to_type(builder, res, lhs.Ty, node, trans) - return res - - def _call_llvm_pow(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lf: mlirlite.Value*, rf: mlirlite.Value*) -> mlirlite.Value*: - """Call the llvm.pow.f64 intrinsic and truncate an f32 result back to float.""" - if lf is None or rf is None: return None - f64_ty: mlirlite.LLVMType* = mlirlite.Double(pool) - pow_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, "llvm.pow.f64") - if pow_fn is None: - pow_fn = mlirlite.create_declare(pool, mod, "llvm.pow.f64", f64_ty) - if pow_fn is not None: - mlirlite.add_param(pool, pow_fn, f64_ty, "x") - mlirlite.add_param(pool, pow_fn, f64_ty, "y") - lf_d: mlirlite.Value* = lf - if self.get_llvm_float_bits(lf.Ty) != 64: lf_d = mlirlite.build_fpext(builder, lf, f64_ty) - rf_d: mlirlite.Value* = rf - if self.get_llvm_float_bits(rf.Ty) != 64: rf_d = mlirlite.build_fpext(builder, rf, f64_ty) - if lf_d is None or rf_d is None: return None - lf_d.Next = rf_d - rf_d.Next = None - res: mlirlite.Value* = mlirlite.build_call(builder, "llvm.pow.f64", lf_d, 2, f64_ty, 0) - if res is None: return None - # f32 target: llvm.pow is always f64, truncate the result back to float - if self.get_llvm_float_bits(lf.Ty) == 32 and self.get_llvm_float_bits(rf.Ty) == 32: res = mlirlite.build_fptrunc(builder, res, mlirlite.Float(pool)) - return res - - # _binop_to_dunder - BinOp op -> dunder method name - # ============================================================ - def _binop_to_dunder(self, op: int) -> str: - """Map a BinOp operator to its dunder method name, or None if unmapped.""" - if op == ast.OpKind.Add: return "__add__" - if op == ast.OpKind.Sub: return "__sub__" - if op == ast.OpKind.Mult: return "__mul__" - if op == ast.OpKind.Div: return "__truediv__" - if op == ast.OpKind.Mod: return "__mod__" - if op == ast.OpKind.Pow: return "__pow__" - if op == ast.OpKind.BitAnd: return "__and__" - if op == ast.OpKind.BitOr: return "__or__" - if op == ast.OpKind.BitXor: return "__xor__" - if op == ast.OpKind.LShift: return "__lshift__" - if op == ast.OpKind.RShift: return "__rshift__" - if op == ast.OpKind.FloorDiv: return "__floordiv__" - return None - # _rbinop_to_dunder - BinOp op -> reverse dunder name (__radd__ etc.): dispatch rhs's reverse dunder - # when lhs lacks the normal dunder (a + b -> b.__radd__(a) if a.__add__ is missing). - def _rbinop_to_dunder(self, op: int) -> str: - """Map a BinOp operator to its reverse dunder method name, or None.""" - if op == ast.OpKind.Add: return "__radd__" - if op == ast.OpKind.Sub: return "__rsub__" - if op == ast.OpKind.Mult: return "__rmul__" - if op == ast.OpKind.Div: return "__rtruediv__" - if op == ast.OpKind.FloorDiv: return "__rfloordiv__" - if op == ast.OpKind.Mod: return "__rmod__" - if op == ast.OpKind.Pow: return "__rpow__" - if op == ast.OpKind.BitAnd: return "__rand__" - if op == ast.OpKind.BitOr: return "__ror__" - if op == ast.OpKind.BitXor: return "__rxor__" - if op == ast.OpKind.LShift: return "__rlshift__" - if op == ast.OpKind.RShift: return "__rrshift__" - return None - # _cmpop_to_dunder - Compare op -> dunder method name - # ============================================================ - def _cmpop_to_dunder(self, op: int) -> str: - """Map a Compare operator to its dunder method name, or None if unmapped.""" - if op == ast.OpKind.Eq: return "__eq__" - if op == ast.OpKind.Ne: return "__ne__" - if op == ast.OpKind.Lt: return "__lt__" - if op == ast.OpKind.Le: return "__le__" - if op == ast.OpKind.Gt: return "__gt__" - if op == ast.OpKind.Ge: return "__ge__" - return None - # _unaryop_to_dunder - UnaryOp op -> dunder method name - # ============================================================ - def _unaryop_to_dunder(self, op: int) -> str: - """Map a UnaryOp operator to its dunder method name, or None if unmapped.""" - if op == ast.OpKind.USub: return "__neg__" - if op == ast.OpKind.UAdd: return "__pos__" - if op == ast.OpKind.Invert: return "__invert__" - return None - # try_operator_overload - attempt operator overloading. If lhs is a struct pointer, look up the dunder method - # in its class (including the inheritance chain) and emit lhs.__dunder__(rhs); otherwise return None to fall back. - def try_operator_overload(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lhs: mlirlite.Value*, rhs: mlirlite.Value*, op: int, trans: HT.Translator*, is_compare: int) -> mlirlite.Value*: - """Call lhs's dunder method when lhs is a struct pointer; else return None.""" - if lhs is None or rhs is None: return None - # Map the operator to a dunder method name - dunder: str = None - if is_compare != 0: dunder = self._cmpop_to_dunder(op) - else: dunder = self._binop_to_dunder(op) - if dunder is None: return None - - # Check whether lhs is a struct pointer - struct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(lhs) - if struct_ty is None: return None - - # Get the class name - class_name: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty) - if class_name is None: return None - - # Look up the dunder method in the inheritance chain - lookup_name: str = pool.alloc(128) - if lookup_name is None: return None - viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder) - found_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, lookup_name) - - search_class: str = class_name - if found_func is None: - # Multiple inheritance: walk the MRO ancestor chain (earlier-declared parent first) - mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name) - mi: int = 1 - while mro_ptr is not None and mro_ptr[mi] != 0: - anc: str = rawptr(mro_ptr[mi]) - if anc is not None: - parent_lookup: str = pool.alloc(128) - if parent_lookup is not None: - viperlib.snprintf(parent_lookup, 128, "%s.%s", anc, dunder) - parent_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, parent_lookup) - if parent_func is not None: - found_func = parent_func - search_class = anc - break - mi += 1 - - # When the method is not in the current module, check whether the class (or a parent) has a SHA1 (stub may be - # uninjected). Prefer locating the entry by type pointer to get the SHA1. - cls_sha1: str = None - op_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty) - if op_entry is not None: cls_sha1 = op_entry.ModuleSha1 - if cls_sha1 is None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(search_class) - if cls_sha1 is None: - mro_p: sizet* = self.Gen.StructGen.get_mro_chain(pool, search_class) - mj: int = 1 - while mro_p is not None and mro_p[mj] != 0 and cls_sha1 is None: - anc_p: str = rawptr(mro_p[mj]) - if anc_p is not None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(anc_p) - mj += 1 - - # Neither a function definition nor a SHA1 -> the class has no such dunder; return None to fall back to native. - if found_func is None and cls_sha1 is None: return None - - # The method really does not exist in its defining module (same-module class without the dunder): return None - # and never emit a ghost call, which would leave a side-effecting `call void @...__div__` and double-emit sdiv. - if found_func is None and trans is not None and trans.ModuleSha1 is not None: - if cls_sha1 is not None and string.strcmp(cls_sha1, trans.ModuleSha1) == 0: return None - # Cross-module class: the stub may be uninjected, so method existence relies on the dependency module's vpi. - # A vpi miss falls back to native comparison (icmp); a ghost call would link as undefined (SysLibTest root cause). - vpi_dup: mlirlite.LLVMType* = self.Gen.CallGenBase._lookup_method_ret_ty_via_vpi(pool, cls_sha1, search_class, dunder) - if vpi_dup is None: return None - - # Build the extra_args array (just the one rhs argument) - extra_args: sizet* = pool.alloc(8) - if extra_args is None: return None - extra_args[0] = sizet(rhs) - - # Call the method: lhs.__dunder__(rhs) - return self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, search_class, dunder, lhs, extra_args, 1, trans) - # try_unary_operator_overload - attempt unary operator overloading. If operand is a struct pointer, look up - # __neg__/__pos__/__invert__ in its class (with inheritance) and emit operand.__dunder__(); else return None. - def try_unary_operator_overload(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, operand: mlirlite.Value*, op: int, trans: HT.Translator*) -> mlirlite.Value*: - """Call operand's dunder method when operand is a struct pointer; else None.""" - if operand is None: return None - dunder: str = self._unaryop_to_dunder(op) - if dunder is None: return None - return self._try_named_dunder(pool, builder, mod, operand, dunder, trans) - - def _resolve_dunder_by_class(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_name: str, dunder: str, trans: HT.Translator*, struct_ty: mlirlite.LLVMType* = None) -> str: - """Resolve the class (including MRO ancestors) that defines the dunder, or None.""" - if class_name is None or dunder is None: return None - - # Look up the dunder method in the inheritance chain - lookup_name: str = pool.alloc(128) - if lookup_name is None: return None - viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder) - found_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, lookup_name) - - search_class: str = class_name - if found_func is None: - # Multiple inheritance: walk the MRO ancestor chain (earlier-declared parent first) - mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name) - mi: int = 1 - while mro_ptr is not None and mro_ptr[mi] != 0: - anc: str = rawptr(mro_ptr[mi]) - if anc is not None: - parent_lookup: str = pool.alloc(128) - if parent_lookup is not None: - viperlib.snprintf(parent_lookup, 128, "%s.%s", anc, dunder) - parent_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, parent_lookup) - if parent_func is not None: - found_func = parent_func - search_class = anc - break - mi += 1 - - # When the method is not in the current module, check whether the class (or a parent) has a SHA1 (stub may be uninjected) - cls_sha1: str = None - if struct_ty is not None: - uop_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty) - if uop_entry is not None: cls_sha1 = uop_entry.ModuleSha1 - if cls_sha1 is None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(search_class) - if cls_sha1 is None: - mro_p: sizet* = self.Gen.StructGen.get_mro_chain(pool, search_class) - mj: int = 1 - while mro_p is not None and mro_p[mj] != 0 and cls_sha1 is None: - anc_p: str = rawptr(mro_p[mj]) - if anc_p is not None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(anc_p) - mj += 1 - - # Neither a function definition nor a SHA1 -> the class has no such dunder method - if found_func is None and cls_sha1 is None: return None - - # Same-module class without the dunder: return None and fall back (consistent with try_operator_overload) to avoid a ghost call. - if found_func is None and trans is not None and trans.ModuleSha1 is not None: - if cls_sha1 is not None and string.strcmp(cls_sha1, trans.ModuleSha1) == 0: return None - # Cross-module class: existence relies on the dependency module's vpi; a vpi miss falls back (no ghost call, avoiding undefined). - if cls_sha1 is not None: - vpi_dup: mlirlite.LLVMType* = self.Gen.CallGenBase._lookup_method_ret_ty_via_vpi(pool, cls_sha1, search_class, dunder) - if vpi_dup is None: return None - return search_class - - def _try_named_dunder(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, operand: mlirlite.Value*, dunder: str, trans: HT.Translator*, extra_args: sizet* = None, extra_count: int = 0) -> mlirlite.Value*: - """Look up and call a dunder method by name on operand's class (including MRO).""" - if operand is None or dunder is None: return None - - # Check whether operand is a struct pointer - struct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(operand) - if struct_ty is None: return None - - # Get the class name - class_name: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty) - if class_name is None: return None - - search_class: str = self._resolve_dunder_by_class(pool, mod, class_name, dunder, trans, struct_ty) - if search_class is None: return None - - # Call the method: operand.__dunder__(extra_args...) - return self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, search_class, dunder, operand, extra_args, extra_count, trans) - - # coerce_cond_to_bool - normalize a condition value to i1 (the single implementation for if/while/and/or/ternary). - # Already-i1 returns as-is; __bool__ is called if defined; pointer != null; otherwise != 0. - def coerce_cond_to_bool(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, val: mlirlite.Value*, trans: HT.Translator*) -> mlirlite.Value*: - """Normalize a condition to i1, preferring __bool__ and else comparing with 0/null.""" - if val is None: return None - bits: int = self.get_llvm_type_bits(val.Ty) - if bits == 1: return val - # Class instance: prefer calling __bool__ (pointer to a struct whose class defines it) - bres: mlirlite.Value* = self._try_named_dunder(pool, builder, mod, val, "__bool__", trans) - if bres is not None: - b_bits: int = self.get_llvm_type_bits(bres.Ty) - if b_bits == 1: return bres - if self.is_ptr_type(bres.Ty) != 0: - null_b: mlirlite.Value* = mlirlite.ConstNull(pool, bres.Ty, "null") - return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, bres, null_b) - zero_b: mlirlite.Value* = mlirlite.const_int32(pool, 0) - return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, bres, zero_b) - if self.is_ptr_type(val.Ty) != 0: - # Pointer type: compare with null (not integer 0, which llc rejects as a type mismatch) - null_val: mlirlite.Value* = mlirlite.ConstNull(pool, val.Ty, "null") - return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, val, null_val) - zero: mlirlite.Value* = mlirlite.const_int32(pool, 0) - return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, val, zero) - # fail_if_double_ptr_binop - double-pointer arithmetic fail-fast (shared by BinOp and AugAssign). Fires when the op - # is arithmetic/bitwise/shift AND both operands are pointers; such IR (`add i8*, i8*`) is invalid, so fail before emitting. - def fail_if_double_ptr_binop(self, pool: memhub.MemBuddy*, op: int, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, is_aug: int = 0) -> int: - """Fail fast on double-pointer arithmetic; returns 0 when not triggered.""" - if lhs is None or rhs is None: return 0 - if self.is_ptr_type(lhs.Ty) == 0 or self.is_ptr_type(rhs.Ty) == 0: return 0 - op_ch: str = None - if op == ast.OpKind.Add: op_ch = "+" - elif op == ast.OpKind.Sub: op_ch = "-" - elif op == ast.OpKind.Mult: op_ch = "*" - elif op == ast.OpKind.Div: op_ch = "/" - elif op == ast.OpKind.FloorDiv: op_ch = "//" - elif op == ast.OpKind.Mod: op_ch = "%" - elif op == ast.OpKind.Pow: op_ch = "**" - elif op == ast.OpKind.BitAnd: op_ch = "&" - elif op == ast.OpKind.BitOr: op_ch = "|" - elif op == ast.OpKind.BitXor: op_ch = "^" - elif op == ast.OpKind.LShift: op_ch = "<<" - elif op == ast.OpKind.RShift: op_ch = ">>" - else: return 0 - op_lbl: str = op_ch - aug_note: str = "" - if is_aug != 0: - # Augmented assignment: build the "+=" form with an f-string (this language's str has no `+` concatenation). - op_lbl = f"{op_ch}=" - aug_note = "(augmented assignment)" - lty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_1059) - rty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_1060) - lty_lbl: str = "?" - rty_lbl: str = "?" - if lty_buf is not None: - mlirlite.TypePrint(lty_buf, 4096, lhs.Ty, pool) - lty_lbl = lty_buf - if string.strcmp(lty_buf, "i8*") == 0: lty_lbl = "str(i8*)" - if rty_buf is not None: - mlirlite.TypePrint(rty_buf, 4096, rhs.Ty, pool) - rty_lbl = rty_buf - if string.strcmp(rty_buf, "i8*") == 0: rty_lbl = "str(i8*)" - col_s: int = 0 - if node is not None: col_s = node.col_offset + 1 - self.Gen.TypeGenErr.fatal_error(node, f"[TPV] illegal double-pointer arithmetic type: op = '{op_lbl}'{aug_note}, " f"left operand type = {lty_lbl}, right operand type = {rty_lbl}. " f"in this language str is i8*, with no `+` concatenation semantics (`str + str` and pointer +/- pointer " f"both degrade to raw pointer add/sub => illegal IR: opt reports " f"invalid operand type for instruction). " f"silent degradation is forbidden, compilation aborted. " f"fix: build strings with f-string interpolation, string.strcat, or viperlib.snprintf.", "TypeError", col_s, col_s) - return 1 - # binop_needs_int_promotion - whether this op takes C-style integer promotion (to >= 32 bits) - def binop_needs_int_promotion(self, op: int) -> int: - """C 语义整数提升的适用范围:**算术与移位**为 1(窄于 32 位先提升再算,避免 8/16 位回绕), - **位运算(& | ^)为 0**——按既有约定"两侧同宽就不提升"(WORD | WORD 保持 u16)。""" - if op == ast.OpKind.Add or op == ast.OpKind.Sub or op == ast.OpKind.Mult: return 1 - if op == ast.OpKind.Div or op == ast.OpKind.FloorDiv or op == ast.OpKind.Mod: return 1 - if op == ast.OpKind.LShift or op == ast.OpKind.RShift: return 1 - return 0 - - # translate_binop - binary operations with automatic type promotion - # ============================================================ - def translate_binop(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: - """Translate a binary operation with automatic type promotion.""" - binop: ast.BinOp* = ast.BinOp*(node) - if binop is None: return None - - lhs_node: ast.AST* = binop.left - rhs_node: ast.AST* = binop.right - op: int = binop.op - - # Translate rhs first (only once, to avoid repeating side effects) - rhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rhs_node, None, 0, trans) - if rhs is None: return None - - # === Operator overload path 1: lhs is a Name bound to a struct variable === - # For value-type variables (e.g. cnt: Counter) translate_value loads a Struct value, but dunder needs Ptr(Struct) as self; try the overload with the alloca pointer. - if lhs_node is not None and lhs_node.kind() == ast.ASTKind.Name and trans is not None: - nm: ast.Name* = ast.Name*(lhs_node) - if nm is not None and nm.id is not None: - lhs_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) - if lhs_alloca is not None: - ovl_self1: mlirlite.Value* = lhs_alloca - # When the alloca is Ptr(Ptr(Struct)) (an object storage slot), load the object pointer as self; using the - # slot address as self reads the object pointer as a field -> garbage (MultiInheritTest root cause). - if lhs_alloca.Ty is not None and lhs_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(lhs_alloca.Ty.Pointee) != 0: - load_self1: mlirlite.Value* = mlirlite.build_load(builder, lhs_alloca.Ty.Pointee, lhs_alloca) - if load_self1 is not None: ovl_self1 = load_self1 - ovl_result: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, ovl_self1, rhs, op, trans, 0) - if ovl_result is not None: return ovl_result - - # Translate lhs normally - lhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, lhs_node, None, 0, trans) - if lhs is None: return None - - # === Operator overload path 2: lhs is Ptr(Struct) (e.g. after loading a Counter* variable) === - ovl_result2: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, lhs, rhs, op, trans, 0) - if ovl_result2 is not None: return ovl_result2 - - # === Operator overload path 3: reverse operator - dispatch rhs.__rX__(lhs) when lhs has no dunder === - # (Python semantics: a + b -> a.__add__(b), else b.__radd__(a)). Skip early if rhs is not a struct. - r_dunder: str = self._rbinop_to_dunder(op) - if r_dunder is not None: - if self.Gen.StructGen.get_struct_type_from_value(rhs) is not None: - rev_extra: sizet* = pool.alloc(8) - if rev_extra is not None: - rev_extra[0] = sizet(lhs) - r_ovl: mlirlite.Value* = self._try_named_dunder(pool, builder, mod, rhs, r_dunder, trans, rev_extra, 1) - if r_ovl is not None: return r_ovl - - # `**` exponentiation: basic types -> llvm.pow (int result written back per the exponent constant). Handled before the float/int split and after overloads. - if op == ast.OpKind.Pow: return self.emit_pow(pool, builder, mod, lhs, rhs, node, trans) - - # Fetch operand type information - lhs_bits: int = self.get_llvm_type_bits(lhs.Ty) - rhs_bits: int = self.get_llvm_type_bits(rhs.Ty) - lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty) - rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty) - - # Float operation: use float instructions when either operand is float (must precede pointer arithmetic, since float has 0 int bits and would be mistaken for a pointer). - if lhs_fbits != 0 or rhs_fbits != 0: - # Determine the target float type (use the larger bit width) - target_fbits: int = lhs_fbits - if rhs_fbits > target_fbits: target_fbits = rhs_fbits - target_float_ty: mlirlite.LLVMType* = mlirlite.Double(pool) - if target_fbits == 32: target_float_ty = mlirlite.Float(pool) - # Convert both operands to the target float type (int->float via si2fp, float->float via fpext/fptrunc) - lhs = self.coerce_to_type(builder, lhs, target_float_ty, node) - rhs = self.coerce_to_type(builder, rhs, target_float_ty, node) - if op == ast.OpKind.Add: return mlirlite.build_fadd(builder, lhs, rhs) - if op == ast.OpKind.Sub: return mlirlite.build_fsub(builder, lhs, rhs) - if op == ast.OpKind.Mult: return mlirlite.build_fmul(builder, lhs, rhs) - if op == ast.OpKind.Div: return mlirlite.build_fdiv(builder, lhs, rhs) - if op == ast.OpKind.Mod: return mlirlite.build_frem(builder, lhs, rhs) - return None - - # Pointer arithmetic: ptr + int / ptr - int / int + ptr -> getelementptr (scaled by element size). int - ptr has no GEP form (illegal in C). - if (lhs_bits == 0 and rhs_bits != 0) or (lhs_bits != 0 and rhs_bits == 0): - if op == ast.OpKind.Add: - if lhs_bits == 0: return self.translate_ptr_arith(pool, builder, lhs, rhs, op) - # int + ptr (swap operands) - return self.translate_ptr_arith(pool, builder, rhs, lhs, op) - if op == ast.OpKind.Sub and lhs_bits == 0: return self.translate_ptr_arith(pool, builder, lhs, rhs, op) - - # Double-pointer arithmetic (including AugAssign) fail-fast: the single implementation is fail_if_double_ptr_binop. - # Pointer-int (GEP) is handled by translate_ptr_arith above; double-pointer Add/Sub/... is illegal here (str is i8*). - self.fail_if_double_ptr_binop(pool, op, lhs, rhs, node, 0) - - # Widening direction (zext/sext) and the div/mod predicates both need "is the operand unsigned"; evaluate once here and thread trans down. - binop_unsigned: int = 0 - if lhs_node is not None: - if self._node_type_unsigned(pool, lhs_node, trans) != 0: binop_unsigned = 1 - if binop_unsigned == 0 and rhs_node is not None: - if self._node_type_unsigned(pool, rhs_node, trans) != 0: binop_unsigned = 1 - +import lib.core.Gen.GenTypes as GenTypes +import t, c +from stdint import * +import ast +import mlirlite +import memhub +import string +import viperlib +import lib.core.Gen.Translator as HT +import lib.core.Gen.Type.TypeSpec as TSpec + + +import lib.core.Gen.Expr.ExprGenBase as ExprGenBase + +# ExprGenOps - ExprGenMixin split (stage D): compare / unary / bool / binop operations +# ============================================================ +novirtual class ExprGenOps(ExprGenBase.ExprGenBase): + """Comparison/unary/boolean/binop operations""" + + # translate ternary IfExp(test, body, orelse): emit `cond ? body : orelse` with then/else blocks + # branching to a merge block whose phi is [body_val, then_blk], [orelse_val, else_blk]. + def translate_ifexp(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: + """Translate a ternary (body if test else orelse) into a phi Value.""" + if node is None or builder is None or trans is None: return None + ie: ast.IfExp* = ast.IfExp*(node) + if ie is None: return None + + func: mlirlite.Function* = trans._cur_func + if func is None: return None + + # 1. Evaluate the condition + cond_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.test, None, 0, trans) + if cond_val is None: return None + # Convert to i1 (prefer dispatching __bool__) + cond_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, cond_val, trans) + + # 2. Create the three basic blocks + cnt: int = trans._label_counter + trans._label_counter = cnt + 1 + name_buf: str = pool.alloc(32) + if name_buf is None: return None + viperlib.snprintf(name_buf, 32, "ifexp.then.%d", cnt) + then_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) + viperlib.snprintf(name_buf, 32, "ifexp.else.%d", cnt) + else_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) + viperlib.snprintf(name_buf, 32, "ifexp.merge.%d", cnt) + merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) + + if then_bb is None or else_bb is None or merge_bb is None: return None + + # 3. Conditional branch + mlirlite.build_cond_br(builder, cond_i1, then_bb, else_bb) + + # 4. then branch + mlirlite.position_at_end(builder, then_bb) + body_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.body, None, 0, trans) + then_blk_for_phi: mlirlite.BasicBlock* = trans._cur_builder.CurBlock + if body_val is None: + # fail-fast: ternary then-branch translation failed; abort compilation (expose the upstream root cause) + self.Gen.TypeGenErr.fatal_error(ie.body, "ternary expression branch translation failed", "TypeError") + # Do not br yet; br after type alignment + + # 5. else branch. When the then-branch is a pointer, pass its type as the else target-type context: a single-quoted + # 1-char literal (CONST_CHAR, e.g. '?') materializes as a 1-char string, otherwise as i32 -> Coerce CastError. + orelse_target_ty: mlirlite.LLVMType* = None + if self.is_ptr_type(body_val.Ty) != 0: orelse_target_ty = body_val.Ty + mlirlite.position_at_end(builder, else_bb) + orelse_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.orelse, None, 0, trans, orelse_target_ty) + else_blk_for_phi: mlirlite.BasicBlock* = trans._cur_builder.CurBlock + if orelse_val is None: + # fail-fast: ternary else-branch translation failed; abort compilation (expose the upstream root cause) + self.Gen.TypeGenErr.fatal_error(ie.orelse, "ternary expression branch translation failed", "TypeError") + # Do not br yet; br after type alignment + + # Type alignment (the two branches may differ): coerce_to_type must run inside each branch so the merge block's + # PHI is its first instruction (otherwise sext/zext/ptrtoint/inttoptr land before the PHI, violating LLVM rules). + body_bits: int = self.get_llvm_type_bits(body_val.Ty) + orelse_bits: int = self.get_llvm_type_bits(orelse_val.Ty) + body_is_ptr: int = self.is_ptr_type(body_val.Ty) + orelse_is_ptr: int = self.is_ptr_type(orelse_val.Ty) + # Case 0: one side is a pointer and the other an integer (e.g. i64 vs i8* null); must be handled first since + # Ptr.bits == 0 skips the next three branches. body_val.Ty becomes phi_ty and orelse_val is coerced to it. + if body_is_ptr != orelse_is_ptr: + mlirlite.position_at_end(builder, else_blk_for_phi) + orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node) + elif body_bits > orelse_bits and orelse_bits > 0: + # Coerce orelse_val in the else block + mlirlite.position_at_end(builder, else_blk_for_phi) + orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node) + elif orelse_bits > body_bits and body_bits > 0: + # Back in the then block, coerce body_val + mlirlite.position_at_end(builder, then_blk_for_phi) + body_val = self.coerce_to_type(builder, body_val, orelse_val.Ty, node) + elif body_bits == orelse_bits and body_bits > 0: + # Same bits but different types (e.g. i64 vs Ptr): null cannot be used in an i64 PHI. + # This branch should not trigger (case 0 covers it); kept as a defensive guard. + if self.is_ptr_type(body_val.Ty) != self.is_ptr_type(orelse_val.Ty): + mlirlite.position_at_end(builder, else_blk_for_phi) + orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node) + + # Now br to merge from the then/else blocks (after type coercion) + mlirlite.position_at_end(builder, then_blk_for_phi) + mlirlite.build_br(builder, merge_bb) + mlirlite.position_at_end(builder, else_blk_for_phi) + mlirlite.build_br(builder, merge_bb) + + # 6. merge block: phi (the PHI is the merge block's first instruction) + mlirlite.position_at_end(builder, merge_bb) + + phi_ty: mlirlite.LLVMType* = body_val.Ty + if phi_ty is None: + # fail-fast: ternary branch type missing; abort compilation (expose the upstream root cause) + self.Gen.TypeGenErr.fatal_error(node, "ternary expression branch type missing", "TypeError") + + inc1: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, body_val, then_blk_for_phi) + inc2: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, orelse_val, else_blk_for_phi) + if inc1 is None or inc2 is None: return None + # List order: inc1 (head) -> inc2 (tail). Writing inc2.Next = inc1 by mistake left inc1.Next as None, emitting a + # single phi entry and triggering LLVM verification: "PHINode should have one entry for each predecessor". + inc1.Next = inc2 + return mlirlite.build_phi(builder, phi_ty, inc1, 2) + # translate_compare - Compare(left, ops, comparators) + # ============================================================ + def should_zext_widen(self, pool: memhub.MemBuddy*, narrow_val: mlirlite.Value*, wide_val: mlirlite.Value*, narrow_node: ast.AST* = None, trans: HT.Translator* = None) -> int: + """Decide integer widening: 1 = zext (unsigned semantics), 0 = sext.""" + if narrow_val is None or narrow_val.Ty is None: return 0 + narrow_bits: int = self.get_llvm_type_bits(narrow_val.Ty) + if narrow_bits == 1: return 1 + if narrow_bits == 8: + # i8 defaults to unsigned-byte handling (systems convention), but an explicitly signed source annotation + # must sext to preserve sign: `int32(v)` with v: int8 = -50 must stay -50, not become 206. + if self._node_type_signed(pool, narrow_node, trans) != 0: return 0 + return 1 + # Wide-side constant heuristic + if wide_val is not None and wide_val.IsConst == 1: + wide_bits: int = self.get_llvm_type_bits(wide_val.Ty) + if wide_bits > narrow_bits and narrow_bits < 64: + signed_max: int64 = (int64(1) << (narrow_bits - 1)) - 1 + if wide_val.IntVal > signed_max: return 1 + # Narrow side resolves to an unsigned type (e.g. t.CUIntXXT literal / cast). trans must be threaded down: for + # Name/Subscript, unsignedness comes from trans.SymTab; without trans it returns 0 and sext is wrongly used. + if narrow_node is not None: + if self._node_type_unsigned(pool, narrow_node, trans) != 0: return 1 + # A CDefine constant with the high bit set (0x80000000..0xFFFFFFFF) -> zext. CDefine is a compile-time value, + # so _node_type_unsigned finds no annotation; the negative-signed range means an unsigned sentinel (e.g. 0xFFFFFFFF). + if narrow_node is not None and narrow_node.kind() == ast.ASTKind.Name: + cdef_nm_zw: ast.Name* = ast.Name*(narrow_node) + if cdef_nm_zw.id is not None: + cdv_zw: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(cdef_nm_zw.id) + if self.Gen.TypeGenCDef.is_cdefine_found() != 0: + if cdv_zw > 0x7FFFFFFF: return 1 + return 0 + def _node_type_unsigned(self, pool: memhub.MemBuddy*, node: ast.AST*, trans: HT.Translator* = None) -> int: + """Return 1 if the AST node resolves to an unsigned integer type, else 0.""" + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.value is not None and at.value.kind() == ast.ASTKind.Name: + mod_nm: ast.Name* = ast.Name*(at.value) + if mod_nm.id is not None and string.strcmp(mod_nm.id, "t") == 0 and at.attr is not None: + if self._type_name_unsigned(at.attr) != 0: return 1 + return 0 + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is not None: + # 1) The name itself is an unsigned type name (e.g. the UInt32T alias) + if self._type_name_unsigned(nm.id) != 0: return 1 + # 2) The variable's annotation type is unsigned (query SymTab when trans is available) + if trans is not None: + ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) + if ve is not None: + # [S4 read] An explicit unsigned Spec (TS_UINT) takes priority over the string check + if self._entry_spec_unsigned(ve.Spec) != 0: return 1 + vcn_u: str = self.Gen.VarGen.var_annot_class_name(ve) + if vcn_u is not None: + if self._type_name_unsigned(vcn_u) != 0: return 1 + return 0 + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(node) + if self._node_type_unsigned(pool, bop.left, trans) != 0: return 1 + if self._node_type_unsigned(pool, bop.right, trans) != 0: return 1 + return 0 + if k == ast.ASTKind.UnaryOp: + uop: ast.UnaryOp* = ast.UnaryOp*(node) + if self._node_type_unsigned(pool, uop.operand, trans) != 0: return 1 + return 0 + if k == ast.ASTKind.Subscript: + # Array/pointer subscript: md5_buf[i] (t.CArray[uint8, N]) or buf[i] (param uint8*) has an unsigned element -> zext. + sub_su: ast.Subscript* = ast.Subscript*(node) + if sub_su.value is not None and sub_su.value.kind() == ast.ASTKind.Name: + snm_su: ast.Name* = ast.Name*(sub_su.value) + if snm_su.id is not None and trans is not None: + ve_su: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, snm_su.id) + if ve_su is not None: + # [S4 read] An explicit unsigned Spec wins; for union annotations the Spec is still Ptr/Array -> returns 0 and falls back to the string path. + if self._entry_spec_unsigned(ve_su.Spec) != 0: return 1 + scn_u: str = self.Gen.VarGen.var_annot_class_name(ve_su) + if scn_u is not None: + if self._annot_str_unsigned(scn_u) != 0: return 1 + return 0 + if k == ast.ASTKind.Attribute: + # Struct field (e.g. viperio.Buf's self.capacity/length: sizet): get the class name from the receiver's AnnotClassName, then look up the field annotation to decide unsignedness. Read-only; returns 0 on failure, so find_struct + manual field walk is used instead of lookup_field_by_class. + at_u: ast.Attribute* = ast.Attribute*(node) + if at_u.value is not None and at_u.attr is not None and trans is not None: + cls_nm_u: str = None + if at_u.value.kind() == ast.ASTKind.Name: + vn_u: ast.Name* = ast.Name*(at_u.value) + if vn_u.id is not None: + ve_u: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, vn_u.id) + if ve_u is not None: cls_nm_u = self.Gen.VarGen.var_annot_class_name(ve_u) + if cls_nm_u is not None: + # Take the bare class name (before the dot): look up by bare name only, avoiding module-qualified resolution + bare_u: str = cls_nm_u + dot_u: str = self.Gen.StructGen._last_dot_outside_brackets(cls_nm_u) + if dot_u is not None: bare_u = dot_u + 1 + if bare_u is not None: + st_u: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(bare_u) + if st_u is not None: + for fi_u in range(int(st_u.FieldCount)): + fe_u: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(st_u, fi_u) + if fe_u is not None and fe_u.Name is not None and string.strcmp(fe_u.Name, at_u.attr) == 0: + # [S4 read] An explicit unsigned field Spec wins + if self._entry_spec_unsigned(fe_u.Spec) != 0: return 1 + fcn_u: str = self.Gen.StructGen.get_field_annot_class_name(fe_u) + if fcn_u is not None: + if self._annot_str_unsigned(fcn_u) != 0: return 1 + break + return 0 + return 0 + def _node_type_signed(self, pool: memhub.MemBuddy*, node: ast.AST*, trans: HT.Translator* = None) -> int: + """Return 1 if the source node is explicitly a signed integer, else 0.""" + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.value is not None and at.value.kind() == ast.ASTKind.Name: + mod_nm: ast.Name* = ast.Name*(at.value) + if mod_nm.id is not None and string.strcmp(mod_nm.id, "t") == 0 and at.attr is not None: + if self._spec_sign_from_annot(at.attr) == TSpec.TS_SIGN_SIGNED: return 1 + return 0 + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is not None: + if self._spec_sign_from_annot(nm.id) == TSpec.TS_SIGN_SIGNED: return 1 + if trans is not None: + ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) + if ve is not None: + ncn_s: str = self.Gen.VarGen.var_annot_class_name(ve) + if ncn_s is not None: + if self._spec_sign_from_annot(ncn_s) == TSpec.TS_SIGN_SIGNED: return 1 + return 0 + if k == ast.ASTKind.Subscript: + sub_s: ast.Subscript* = ast.Subscript*(node) + if sub_s.value is not None and sub_s.value.kind() == ast.ASTKind.Name: + snm: ast.Name* = ast.Name*(sub_s.value) + if snm.id is not None and trans is not None: + ve_s: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, snm.id) + if ve_s is not None: + scn_s: str = self.Gen.VarGen.var_annot_class_name(ve_s) + if scn_s is not None: + if self._spec_sign_from_annot(scn_s) == TSpec.TS_SIGN_SIGNED: return 1 + return 0 + return 0 + # S4 de-stringification (unsigned detection): the single source of truth is TypeGenCType.spec_sign_of_annot; the old + # strstr-substring fallbacks were deleted (they caused the A4/A5 false positives). UNKNOWN means no opinion -> return 0. + def _spec_sign_from_annot(self, annot: str) -> int: + """Map an annotation to TS_SIGN_*; UNKNOWN takes no position.""" + return self.Gen.TypeGenCType.spec_sign_of_annot(annot, 0) + # _entry_spec_unsigned - whether a variable/field entry's Spec is an explicitly unsigned integer. `Kind == TS_UINT` + # is a strong signal and takes priority; `Kind == TS_INT` is ambiguous, so only the unsigned direction is switched. + def _entry_spec_unsigned(self, sp: TSpec.TypeSpec*) -> int: + """Return 1 if the entry Spec is explicitly unsigned (TS_UINT), else 0.""" + if sp is None: return 0 + if sp.Kind == TSpec.TS_UINT: return 1 + return 0 + def _type_name_unsigned(self, type_name: str) -> int: + """Return 1 if the type name is unsigned; UNKNOWN means 0.""" + if type_name is not None: + if self._spec_sign_from_annot(type_name) == TSpec.TS_SIGN_UNSIGNED: return 1 + return 0 + def _annot_str_unsigned(self, annot: str) -> int: + """Return 1 if the annotation string is unsigned; UNKNOWN means 0.""" + if annot is not None: + if self._spec_sign_from_annot(annot) == TSpec.TS_SIGN_UNSIGNED: return 1 + return 0 + def translate_compare(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: + """Translate a comparison expression and return an i1 result.""" + cmp: ast.Compare* = ast.Compare*(node) + if cmp is None: return None + + ops_list: list[int]* = (list[int]*)(cmp.ops) + if ops_list is None or len(ops_list) == 0: return None + op: int = ops_list.get(0) + + comparators: list[ast.AST*]* = cmp.comparators + if comparators is None or len(comparators) == 0: return None + rhs_node: ast.AST* = comparators.get(0) + rhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rhs_node, None, 0, trans) + if rhs is None: return None + + # === None comparison optimization: x == None / x != None use identity comparison (icmp eq/ne null) === + # Avoids emitting invalid __eq__/__ne__ calls for struct pointers; Python treats == None as identity comparison. + lhs_node: ast.AST* = cmp.left + cmp_with_none: int = 0 + if op == ast.OpKind.Eq or op == ast.OpKind.Ne or op == ast.OpKind.Is or op == ast.OpKind.IsNot: + if rhs_node is not None and rhs_node.kind() == ast.ASTKind.Constant: + rhs_cn: ast.Constant* = ast.Constant*(rhs_node) + if rhs_cn is not None and rhs_cn.const_kind == ast.CONST_NONE: cmp_with_none = 1 + if cmp_with_none == 0 and lhs_node is not None and lhs_node.kind() == ast.ASTKind.Constant: + lhs_cn: ast.Constant* = ast.Constant*(lhs_node) + if lhs_cn is not None and lhs_cn.const_kind == ast.CONST_NONE: cmp_with_none = 1 + + # === Pointer null-check pattern: ptr != 0 / ptr == 0. Skip operator overload and use identity comparison + # (icmp null); otherwise cross-module classes without __eq__/__ne__ get ghost calls -> undefined reference. === + cmp_zero: int = 0 + if op == ast.OpKind.Eq or op == ast.OpKind.Ne: + if rhs_node is not None and self._is_zero_int_literal(rhs_node) != 0: cmp_zero = 1 + if cmp_zero == 0 and lhs_node is not None and self._is_zero_int_literal(lhs_node) != 0: cmp_zero = 1 + + # === Compare operator overload path 1: lhs is a Name bound to a struct variable === + # For value-type variables (e.g. cnt: Counter) try the overload with the alloca pointer; None comparison is skipped. + if cmp_with_none == 0 and lhs_node is not None and lhs_node.kind() == ast.ASTKind.Name and trans is not None: + nm: ast.Name* = ast.Name*(lhs_node) + if nm is not None and nm.id is not None: + lhs_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) + if lhs_alloca is not None: + if cmp_zero == 0 or self.get_ptr_level(lhs_alloca.Ty) == 0: + ovl_self1: mlirlite.Value* = lhs_alloca + # Ptr(Ptr(Struct)) object storage slot: load the object pointer as self (same as translate_binop path 1) to avoid using the slot address as the object address. + if lhs_alloca.Ty is not None and lhs_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(lhs_alloca.Ty.Pointee) != 0: + load_self1: mlirlite.Value* = mlirlite.build_load(builder, lhs_alloca.Ty.Pointee, lhs_alloca) + if load_self1 is not None: ovl_self1 = load_self1 + ovl_result: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, ovl_self1, rhs, op, trans, 1) + if ovl_result is not None: return ovl_result + + # Translate lhs normally + lhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cmp.left, None, 0, trans) + if lhs is None: return None + + # === in / not in: the container rhs calls __contains__ (aligned with TPC HandlesExprOps.py L261-281). + # x in y -> y.__contains__(x); x not in y -> !y.__contains__(x). With no __contains__, return i1 0 (always False). + if op == ast.OpKind.In or op == ast.OpKind.NotIn: + cn_st: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(rhs) + if cn_st is not None: + cn_cls: str = self.Gen.StructGen.get_class_name_by_type(pool, cn_st) + if cn_cls is not None: + cn_lookup: str = pool.alloc(128) + if cn_lookup is not None: + viperlib.snprintf(cn_lookup, 128, "%s.__contains__", cn_cls) + cn_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, cn_lookup) + if cn_fn is not None: + cn_ret: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(cn_fn) + cn_fn_name: str = mlirlite.function_get_name(cn_fn) + if cn_ret is not None and cn_fn_name is not None: + # Align the self argument (container) to __contains__'s first param type + cn_self: mlirlite.Value* = rhs + cn_first: mlirlite.Param* = mlirlite.function_get_param_head(cn_fn) + if cn_first is not None: + cn_self_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(cn_first) + if cn_self_ty is not None: cn_self = mlirlite.build_bitcast(builder, rhs, cn_self_ty) + if cn_self is not None: + # Param list: self -> key (separately null-terminated) + cn_self.Next = lhs + lhs.Next = None + cn_res: mlirlite.Value* = mlirlite.build_call(builder, cn_fn_name, cn_self, 2, cn_ret, 0) + if cn_res is not None: + # Normalize to i1 (__contains__ may return int/i32 etc.) + cn_i1: mlirlite.Value* = cn_res + match cn_ret: + case mlirlite.LLVMType.Int(cn_bits): + if cn_bits > 1: + cn_zero: mlirlite.Value* = mlirlite.ConstInt(pool, cn_ret, 0, "0") + if cn_zero is not None: cn_i1 = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cn_res, cn_zero) + case _: + pass + if op == ast.OpKind.NotIn: + i1_ty_ni: mlirlite.LLVMType* = mlirlite.Int1(pool) + if i1_ty_ni is not None and cn_i1 is not None: + one_i1_ni: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_ni, 1, "1") + if one_i1_ni is not None: cn_i1 = mlirlite.build_xor(builder, cn_i1, one_i1_ni) + if cn_i1 is not None: return cn_i1 + # No __contains__: TPC semantics are always False (i1 0) + i1_ty_in: mlirlite.LLVMType* = mlirlite.Int1(pool) + if i1_ty_in is not None: return mlirlite.ConstInt(pool, i1_ty_in, 0, "0") + return None + + # === Compare operator overload path 2: lhs is Ptr(Struct) === + # Is/IsNot are unmapped so they return None and fall back to native comparison; None and pointer null-checks skip it. + if cmp_with_none == 0 and (cmp_zero == 0 or self.get_ptr_level(lhs.Ty) == 0): + ovl_result2: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, lhs, rhs, op, trans, 1) + if ovl_result2 is not None: return ovl_result2 + + # === Unsigned comparison (A5): unsigned int operands (CUInt64T/CUInt32T etc.) use ULT/ULE/UGT/UGE. + # A fixed signed predicate made uint64 address comparisons wrong across 2^63 (A5 root cause); reuse _node_type_unsigned. + unsigned_l: int = 0 + unsigned_r: int = 0 + if lhs_node is not None: unsigned_l = self._node_type_unsigned(pool, lhs_node, trans) + if rhs_node is not None: unsigned_r = self._node_type_unsigned(pool, rhs_node, trans) + cmp_unsigned: int = 1 if (unsigned_l != 0 or unsigned_r != 0) else 0 + + predicate: int = mlirlite.ICMP_SLT + if op == ast.OpKind.Lt: predicate = mlirlite.ICMP_ULT if cmp_unsigned != 0 else mlirlite.ICMP_SLT + elif op == ast.OpKind.Le: predicate = mlirlite.ICMP_ULE if cmp_unsigned != 0 else mlirlite.ICMP_SLE + elif op == ast.OpKind.Gt: predicate = mlirlite.ICMP_UGT if cmp_unsigned != 0 else mlirlite.ICMP_SGT + elif op == ast.OpKind.Ge: predicate = mlirlite.ICMP_UGE if cmp_unsigned != 0 else mlirlite.ICMP_SGE + elif op == ast.OpKind.Eq: predicate = mlirlite.ICMP_EQ + elif op == ast.OpKind.Ne: predicate = mlirlite.ICMP_NE + elif op == ast.OpKind.Is: + # `x is y` -> pointer/value equality comparison + predicate = mlirlite.ICMP_EQ + elif op == ast.OpKind.IsNot: + # `x is not y` -> pointer/value inequality comparison + predicate = mlirlite.ICMP_NE + + # Type safety: number vs number allowed (sext-aligned by bit width); one-level pointer vs one-level pointer allowed (address compare); number vs pointer(1+) forbidden (fail-fast, except None compare); different-level pointers forbidden. None compare skips this check. + if cmp_with_none == 0: + lhs_ptr_level: int = self.get_ptr_level(lhs.Ty) + rhs_ptr_level: int = self.get_ptr_level(rhs.Ty) + if lhs_ptr_level != rhs_ptr_level: + # Pointer vs integer literal 0 (null-check pattern): 0 is treated as NULL and converted to a same-type + # null pointer (icmp needs matching types), not a type mismatch. + zero_is_rhs: int = 0 + zero_is_lhs: int = 0 + if rhs_ptr_level == 0 and self._is_zero_int_literal(rhs_node) != 0: zero_is_rhs = 1 + if lhs_ptr_level == 0 and self._is_zero_int_literal(lhs_node) != 0: zero_is_lhs = 1 + if zero_is_rhs != 0 or zero_is_lhs != 0: + if zero_is_rhs != 0: rhs = mlirlite.ConstNull(pool, lhs.Ty, "null") + else: lhs = mlirlite.ConstNull(pool, rhs.Ty, "null") + else: + # Pointer vs non-zero integer (C sentinel pattern, e.g. INVALID_HANDLE_VALUE = -1): align with TPC + # HandlesExprOps.py L388-398; ptrtoint the pointer to compare as an integer address. + if lhs_ptr_level != 0 and rhs_ptr_level == 0: lhs = mlirlite.build_ptrtoint(builder, lhs, rhs.Ty) + elif rhs_ptr_level != 0 and lhs_ptr_level == 0: rhs = mlirlite.build_ptrtoint(builder, rhs, lhs.Ty) + else: + # Different-level pointers (1 vs 2 etc.) -> forbidden (fail-fast) + self.Gen.TypeGenErr.fatal_error(cmp, f"comparison type mismatch: pointers of different levels compared (lhs level={lhs_ptr_level}, rhs level={rhs_ptr_level})", "TypeError") + else: + # None comparison: the None side is implicitly 0/null, its form decided by the other side. + # pointer -> same-type null pointer (icmp eq/ne); integer -> 0 (x == 0); struct value -> never None (== false, != true). + lhs_is_none_lit: int = 0 + if lhs_node is not None and lhs_node.kind() == ast.ASTKind.Constant: + lhs_none_cn: ast.Constant* = ast.Constant*(lhs_node) + if lhs_none_cn is not None and lhs_none_cn.const_kind == ast.CONST_NONE: lhs_is_none_lit = 1 + lhs_ptr_level_none: int = self.get_ptr_level(lhs.Ty) + rhs_ptr_level_none: int = self.get_ptr_level(rhs.Ty) + if lhs_is_none_lit != 0: + # None on the left (`None is x` / `None == x`): the rhs type decides None's form + if rhs_ptr_level_none == 0: + rhs_is_struct_none: int = 0 + match rhs.Ty: + case mlirlite.LLVMType.Struct(_, _, _): + rhs_is_struct_none = 1 + case _: + rhs_is_struct_none = 0 + if rhs_is_struct_none != 0: + if op == ast.OpKind.Eq or op == ast.OpKind.Is: return mlirlite.const_int(pool, 1, 0) + return mlirlite.const_int(pool, 1, 1) + # rhs is numeric -> convert the None side to integer 0 (rhs unchanged) + lhs = mlirlite.const_int32(pool, 0) + else: + # None on the right (`x is None` / `x == None`): the lhs type decides None's form + if lhs_ptr_level_none == 0: + # Check whether lhs is a struct value type (a non-pointer Struct) + lhs_is_struct_none: int = 0 + match lhs.Ty: + case mlirlite.LLVMType.Struct(_, _, _): + lhs_is_struct_none = 1 + case _: + lhs_is_struct_none = 0 + if lhs_is_struct_none != 0: + # A struct value is never None: ==/is return false, !=/is not return true + if op == ast.OpKind.Eq or op == ast.OpKind.Is: return mlirlite.const_int(pool, 1, 0) + else: # Ne, IsNot + return mlirlite.const_int(pool, 1, 1) + # lhs is numeric and rhs is i8* null -> convert rhs to numeric 0 (lhs unchanged) + rhs = mlirlite.const_int32(pool, 0) + + # Type alignment: icmp requires matching types on both sides. Only numeric types are aligned (same-level pointers icmp directly); i1 uses zext (1 stays 1). + lhs_bits: int = self.get_llvm_type_bits(lhs.Ty) + rhs_bits: int = self.get_llvm_type_bits(rhs.Ty) + # C 语义整数提升(与算术同一口径):两侧都窄于 32 位时先提升到 i32 再比较,避免 8/16 位 + # 直接比较在符号/边界组合下语义模糊(如 signed i8 与 unsigned u8 直接比)。 + # 提升后把 bits 记为 32,下面的等宽化不再重复处理。 + if lhs_bits != 0 and rhs_bits != 0 and lhs_bits < 32 and rhs_bits < 32: + cmp_i32_pr: mlirlite.LLVMType* = mlirlite.Int32(pool) + cmp_probe: mlirlite.Value* = mlirlite.const_int32(pool, 0) + if self.should_zext_widen(pool, lhs, cmp_probe, cmp.left, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, cmp_i32_pr) + else: lhs = mlirlite.build_sext(builder, lhs, cmp_i32_pr) + if self.should_zext_widen(pool, rhs, cmp_probe, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, cmp_i32_pr) + else: rhs = mlirlite.build_sext(builder, rhs, cmp_i32_pr) + lhs_bits = 32 + rhs_bits = 32 + if lhs_bits != 0 and rhs_bits != 0 and lhs_bits != rhs_bits: + if lhs_bits < rhs_bits: + if self.should_zext_widen(pool, lhs, rhs, cmp.left, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, rhs.Ty) + else: lhs = mlirlite.build_sext(builder, lhs, rhs.Ty) + else: + if self.should_zext_widen(pool, rhs, lhs, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, lhs.Ty) + else: rhs = mlirlite.build_sext(builder, rhs, lhs.Ty) + + # Pointer type alignment: same-level pointers with different pointee types are unified by bitcast (e.g. i8* vs Struct*). + if lhs_bits == 0 and rhs_bits == 0: + lhs_is_ptr_now: int = self.is_ptr_type(lhs.Ty) + rhs_is_ptr_now: int = self.is_ptr_type(rhs.Ty) + if lhs_is_ptr_now != 0 and rhs_is_ptr_now != 0: + # Both are pointers; compare type strings via TypePrint + lhs_ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_500) + rhs_ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_501) + if lhs_ty_buf is not None and rhs_ty_buf is not None: + mlirlite.TypePrint(lhs_ty_buf, 4096, lhs.Ty, pool) + mlirlite.TypePrint(rhs_ty_buf, 4096, rhs.Ty, pool) + if string.strcmp(lhs_ty_buf, rhs_ty_buf) != 0: rhs = mlirlite.build_bitcast(builder, rhs, lhs.Ty) + + # Float comparison: use fcmp instead of icmp when either operand is floating point + lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty) + rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty) + if lhs_fbits != 0 or rhs_fbits != 0: + # Float type alignment (fpext/fptrunc) + if lhs_fbits != 0 and rhs_fbits != 0 and lhs_fbits != rhs_fbits: + if lhs_fbits < rhs_fbits: lhs = mlirlite.build_fpext(builder, lhs, rhs.Ty) + else: rhs = mlirlite.build_fptrunc(builder, rhs, lhs.Ty) + elif lhs_fbits != 0 and rhs_fbits == 0: + # int → float + rhs = mlirlite.build_si2fp(builder, rhs, lhs.Ty) + elif rhs_fbits != 0 and lhs_fbits == 0: lhs = mlirlite.build_si2fp(builder, lhs, rhs.Ty) + # Map the comparison predicate to fcmp + fpredicate: int = mlirlite.FCMP_OEQ + if op == ast.OpKind.Eq or op == ast.OpKind.Is: fpredicate = mlirlite.FCMP_OEQ + elif op == ast.OpKind.Ne or op == ast.OpKind.IsNot: fpredicate = mlirlite.FCMP_ONE + elif op == ast.OpKind.Lt: fpredicate = mlirlite.FCMP_OLT + elif op == ast.OpKind.Le: fpredicate = mlirlite.FCMP_OLE + elif op == ast.OpKind.Gt: fpredicate = mlirlite.FCMP_OGT + elif op == ast.OpKind.Ge: fpredicate = mlirlite.FCMP_OGE + return mlirlite.build_fcmp(builder, fpredicate, lhs, rhs) + + return mlirlite.build_icmp(builder, predicate, lhs, rhs) + # translate_unaryop - UnaryOp(op, operand) + # ============================================================ + def translate_unaryop(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: + """Translate unary operations (-x, +x, not x, ~x).""" + uo: ast.UnaryOp* = ast.UnaryOp*(node) + if uo is None: return None + + operand: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo.operand, None, 0, trans) + if operand is None: return None + + # === Unary operator overload: -obj/+obj/~obj call __neg__/__pos__/__invert__ (only USub/UAdd/Invert have overloads). + if uo.op == ast.OpKind.USub or uo.op == ast.OpKind.UAdd or uo.op == ast.OpKind.Invert: + ovl_u: mlirlite.Value* = self.try_unary_operator_overload(pool, builder, mod, operand, uo.op, trans) + if ovl_u is not None: return ovl_u + + # C 语义整数提升(与算术/比较同一口径):一元 +/-/~ 先把窄于 32 位的整数操作数提升到 i32, + # 这样 `0 - x` / `x ^ -1` 的常量与操作数同宽(否则是 i32 常量对 i8/i16 操作数)。 + # `not x` 不需要:结果为 i1,操作数宽度不影响语义。 + if uo.op == ast.OpKind.USub or uo.op == ast.OpKind.UAdd or uo.op == ast.OpKind.Invert: + u_bits: int = self.get_llvm_type_bits(operand.Ty) + if u_bits != 0 and u_bits < 32: + u_i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + u_probe: mlirlite.Value* = mlirlite.const_int32(pool, 0) + if self.should_zext_widen(pool, operand, u_probe, uo.operand, trans) != 0: operand = mlirlite.build_zext(builder, operand, u_i32_ty) + else: operand = mlirlite.build_sext(builder, operand, u_i32_ty) + + if uo.op == ast.OpKind.USub: + # -x + # Float type uses fneg (emit fneg directly to avoid 0.0 - x type mismatch) + operand_fbits: int = self.get_llvm_float_bits(operand.Ty) + if operand_fbits > 0: return mlirlite.build_fneg(builder, operand) + # Integer: -x = 0 - x, choosing the zero constant by bit width (avoid i32/i64 mismatch) + operand_bits: int = self.get_llvm_type_bits(operand.Ty) + zero: mlirlite.Value* = mlirlite.const_int32(pool, 0) + if operand_bits == 64: zero = mlirlite.const_int64(pool, 0) + elif operand_bits > 0 and operand_bits != 32: + # Other widths (i8/i16 etc.): use i32 zero; operand will be coerced + zero = mlirlite.const_int32(pool, 0) + return mlirlite.build_sub(builder, zero, operand) + elif uo.op == ast.OpKind.UAdd: + # +x = x + return operand + elif uo.op == ast.OpKind.Not: + # not x = (bool(x) == 0), returns i1; prefer dispatching __bool__ + not_cond: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, operand, trans) + if not_cond is None: return None + false_val: mlirlite.Value* = mlirlite.const_int(pool, 1, 0) + return mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, not_cond, false_val) + elif uo.op == ast.OpKind.Invert: + # ~x = x ^ -1 + operand_bits_inv: int = self.get_llvm_type_bits(operand.Ty) + neg1: mlirlite.Value* = mlirlite.const_int32(pool, -1) + if operand_bits_inv == 64: neg1 = mlirlite.const_int64(pool, -1) + return mlirlite.build_xor(builder, operand, neg1) + elif uo.op == ast.OpKind.Deref: + # *p -> load by the pointee type (aligned with translate_c_deref) + if self.Gen.ExprGenBase.is_ptr_type(operand.Ty) != 0: + d_pointee: mlirlite.LLVMType* = operand.Ty.Pointee + if d_pointee is not None: return mlirlite.build_load(builder, d_pointee, operand) + # Non-pointer means type inference degraded: fail-fast, never fall back to reading as i8 + self.Gen.TypeGenErr.fatal_error(node, "the operand of unary '*' dereference must be a pointer type (type inference degraded)", "TypeError") + return None + elif uo.op == ast.OpKind.AddrOf: + # &x -> lvalue address (no load): Name alloca / module global / module function / Attribute field GEP / Subscript element address. + if trans is None: return None + if uo.operand.kind() == ast.ASTKind.Name: + nm_ao: ast.Name* = ast.Name*(uo.operand) + if nm_ao.id is not None: + # P2 willFree:对已失效变量取地址 → fail-fast(与读取路径同一口径) + if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id)) != 0: + self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': already released by a @life(willFree(...)) call", "LifetimeError") + # self/cls object reference: return the object pointer itself (Ptr(Struct)), not the pointer variable's address (Ptr(Ptr(Struct))). + if string.strcmp(nm_ao.id, "self") == 0 or string.strcmp(nm_ao.id, "cls") == 0: + se_ao: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ao.id) + if se_ao is not None and se_ao.Alloca is not None and se_ao.Alloca.Ty is not None: + match se_ao.Alloca.Ty: + case mlirlite.LLVMType.Ptr(sa1): + match sa1: + case mlirlite.LLVMType.Ptr(sa2): + obj_ty_sa: mlirlite.LLVMType* = mlirlite.Ptr(pool, sa2) + return mlirlite.build_load(builder, obj_ty_sa, se_ao.Alloca) + case _: + pass + case _: + pass + v_ao: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id) + if v_ao is None and mod is not None: + # Module global variable: convert via make_global_ref into a quoted Ptr(Ty) reference + gv_ao: mlirlite.GlobalVariable* = self.Gen.ExprGenAttr.find_global_in_module(mod, nm_ao.id) + if gv_ao is not None and gv_ao.Name is not None and gv_ao.Ty is not None: v_ao = self.Gen.ExprGenAttr.make_global_ref(pool, gv_ao.Name, gv_ao.Ty) + if v_ao is not None and v_ao.Ty is not None: + # Object-pointer variable (alloca is Ptr(Ptr(Struct))): the variable is the object, return the object pointer (load once), aligned with translate_c_addr. + match v_ao.Ty: + case mlirlite.LLVMType.Ptr(ao1): + match ao1: + case mlirlite.LLVMType.Ptr(ao2): + match ao2: + case mlirlite.LLVMType.Struct(_, _, _): + obj_ty_ao: mlirlite.LLVMType* = mlirlite.Ptr(pool, ao2) + return mlirlite.build_load(builder, obj_ty_ao, v_ao) + case _: + pass + case _: + pass + case _: + pass + return v_ao + # Module function address: the function symbol itself + if v_ao is None and mod is not None: + f_ao: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, nm_ao.id) + if f_ao is not None: + fname_ao: str = mlirlite.function_get_name(f_ao) + if fname_ao is not None: + # The param chain is a Param chain but mlirlite.Func's type builder uses ParamNode -> rebuild via _FuncTyFromFunction (same as CallGenCBuiltin). + fn_ty_ao: mlirlite.LLVMType* = self.Gen.CallGenBase._FuncTyFromFunction(pool, f_ao) + # Quote the function reference name when needed (sha1-prefixed names starting with a digit or containing '.') + fnlen_ao: sizet = string.strlen(fname_ao) + needs_q_ao: int = self.Gen.ClassGenVTable._vtable_name_needs_quote(fname_ao) + fbuf_ao: str = pool.alloc(fnlen_ao + 4) + if fbuf_ao is not None and fn_ty_ao is not None: + if needs_q_ao != 0: viperlib.snprintf(fbuf_ao, fnlen_ao + 4, "@\"%s\"", fname_ao) + else: viperlib.snprintf(fbuf_ao, fnlen_ao + 4, "@%s", fname_ao) + return mlirlite.SSAValue(pool, mlirlite.Ptr(pool, fn_ty_ao), fbuf_ao) + elif uo.operand.kind() == ast.ASTKind.Attribute: + # Field address (GEP, no load) + fptr_ao: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, uo.operand, trans) + if fptr_ao is not None: return fptr_ao + elif uo.operand.kind() == ast.ASTKind.Subscript: + # Element address (GEP, no load) + sptr_ao: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, uo.operand, trans) + if sptr_ao is not None: return sptr_ao + elif uo.operand.kind() == ast.ASTKind.Call: + # &() -> take the address of an rvalue. Reuse the already-evaluated operand (never re-evaluate: + # a ctor call would re-run __before_init__/__init__). See _addrof_of_call. + ac_ao: mlirlite.Value* = self._addrof_of_call(builder, pool, operand) + if ac_ao is not None: return ac_ao + self.Gen.TypeGenErr.fatal_error(node, "the operand of unary '&' address-of must be an lvalue (Name/Attribute/Subscript) or a constructor/function call result", "TypeError") + return None + return operand + + def _addrof_of_call(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, call_val: mlirlite.Value*) -> mlirlite.Value*: + """Return the address of a call result (&()), reusing the evaluated value.""" + if call_val is None or call_val.Ty is None: return None + if self.Gen.ExprGenBase.is_ptr_type(call_val.Ty) != 0 and call_val.Ty.Pointee is not None: + pointee_is_struct: int = 0 + match call_val.Ty.Pointee: + case mlirlite.LLVMType.Struct(_, _, _): + pointee_is_struct = 1 + case _: + pointee_is_struct = 0 + if pointee_is_struct != 0: return call_val + tmp_slot_ao: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, call_val.Ty) + if tmp_slot_ao is None: return None + mlirlite.build_store(builder, call_val, tmp_slot_ao) + return tmp_slot_ao + # translate_boolop - BoolOp(op, values): short-circuit evaluation returning i1. + # and: a false short-circuits to merge (returns a's i1); or: a true short-circuits; the last value is evaluated then branches to merge; merge phis all i1 edges. + def translate_boolop(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr = None, func_count: int = 0, trans: HT.Translator* = None) -> mlirlite.Value*: + """Translate boolean and/or with short-circuit evaluation, returning i1.""" + bo: ast.BoolOp* = ast.BoolOp*(node) + if bo is None: return None + + op: int = bo.op + values: list[ast.AST*]* = bo.values + if values is None: return None + count: sizet = len(values) + if count == 0: return None + if count == 1: + return self.Gen.ExprGen.translate_value(builder, pool, mod, values.get(0), funcs_ptr, func_count, trans) + + func: mlirlite.Function* = builder.Func + if func is None: return None + + # Create the merge BB (unique label from builder.Counter; position_at_end's move_to_end keeps BB text order matching control-flow order). + cnt: int = builder.Counter + builder.Counter = cnt + 1 + name_buf: str = pool.alloc(32) + if name_buf is None: return None + viperlib.snprintf(name_buf, 32, "bool.merge.%d", cnt) + merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) + + # phi incoming list head/tail + phi_head: mlirlite.PhiIncoming* = None + phi_tail: mlirlite.PhiIncoming* = None + phi_count: int = int(count - 1) # loop iteration count + + # Evaluate and short-circuit each value except the last + for i in range(int(count - 1)): + val_node: ast.AST* = values.get(i) + val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, val_node, funcs_ptr, func_count, trans) + if val is None: return None + + # Convert to i1 (use directly if already i1; else coerce_cond_to_bool, preferring __bool__) + val_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, val, trans) + + # Create the next BB (evaluate the next value) + cnt = builder.Counter + builder.Counter = cnt + 1 + viperlib.snprintf(name_buf, 32, "bool.next.%d", cnt) + next_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) + + # and: false -> merge, true -> next; or: true -> merge, false -> next + if op == ast.OpKind.And: mlirlite.build_cond_br(builder, val_i1, next_bb, merge_bb) + else: mlirlite.build_cond_br(builder, val_i1, merge_bb, next_bb) + + # Add the phi incoming edge: (val_i1, the BB holding cond_br) + cur_bb: mlirlite.BasicBlock* = builder.CurBlock + inc: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, val_i1, cur_bb) + if phi_head is None: + phi_head = inc + phi_tail = inc + else: + phi_tail.Next = inc + phi_tail = inc + + # Move to the next BB and evaluate the next value + mlirlite.position_at_end(builder, next_bb) + + # Last value: evaluate then branch straight to merge + last_node: ast.AST* = values.get(count - 1) + last_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, last_node, funcs_ptr, func_count, trans) + if last_val is None: return None + last_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, last_val, trans) + + mlirlite.build_br(builder, merge_bb) + + cur_bb = builder.CurBlock + inc = mlirlite.new_phi_incoming(pool, last_i1, cur_bb) + if phi_head is None: phi_head = inc + else: phi_tail.Next = inc + phi_count += 1 + + # Move to merge and create the phi merging all incoming edges + mlirlite.position_at_end(builder, merge_bb) + i1_ty: mlirlite.LLVMType* = mlirlite.Int1(pool) + return mlirlite.build_phi(builder, i1_ty, phi_head, phi_count) + # emit_pow - `**` exponentiation for basic types (aligned with TPC ExprGen.emit_binary_op's Pow branch). + # Float -> llvm.pow; int -> compute in double and fptosi back when the exponent is a non-negative constant. Custom types go through __pow__. + def emit_pow(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, trans: HT.Translator* = None) -> mlirlite.Value*: + """Emit `**` exponentiation (basic types go through llvm.pow).""" + if lhs is None or rhs is None: return None + lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty) + rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty) + f64_ty: mlirlite.LLVMType* = mlirlite.Double(pool) + # Float path: either operand floating -> float result + if lhs_fbits != 0 or rhs_fbits != 0: + target_f: mlirlite.LLVMType* = f64_ty + if lhs_fbits == 32 and rhs_fbits == 32: target_f = mlirlite.Float(pool) + lf: mlirlite.Value* = self.coerce_to_type(builder, lhs, target_f, node, trans) + rf: mlirlite.Value* = self.coerce_to_type(builder, rhs, target_f, node, trans) + return self._call_llvm_pow(pool, builder, mod, lf, rf) + # Integer path: promote to double and compute + li: mlirlite.Value* = self.coerce_to_type(builder, lhs, f64_ty, node, trans) + ri: mlirlite.Value* = self.coerce_to_type(builder, rhs, f64_ty, node, trans) + res: mlirlite.Value* = self._call_llvm_pow(pool, builder, mod, li, ri) + if res is None: return None + # Non-negative integer exponent constant -> fptosi back to lhs type (Python int ** int = int) + rhs_bits: int = self.get_llvm_type_bits(rhs.Ty) + if rhs_bits != 0 and rhs.IsConst == 1 and rhs.IntVal >= 0: return self.coerce_to_type(builder, res, lhs.Ty, node, trans) + return res + + def _call_llvm_pow(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lf: mlirlite.Value*, rf: mlirlite.Value*) -> mlirlite.Value*: + """Call the llvm.pow.f64 intrinsic and truncate an f32 result back to float.""" + if lf is None or rf is None: return None + f64_ty: mlirlite.LLVMType* = mlirlite.Double(pool) + pow_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, "llvm.pow.f64") + if pow_fn is None: + pow_fn = mlirlite.create_declare(pool, mod, "llvm.pow.f64", f64_ty) + if pow_fn is not None: + mlirlite.add_param(pool, pow_fn, f64_ty, "x") + mlirlite.add_param(pool, pow_fn, f64_ty, "y") + lf_d: mlirlite.Value* = lf + if self.get_llvm_float_bits(lf.Ty) != 64: lf_d = mlirlite.build_fpext(builder, lf, f64_ty) + rf_d: mlirlite.Value* = rf + if self.get_llvm_float_bits(rf.Ty) != 64: rf_d = mlirlite.build_fpext(builder, rf, f64_ty) + if lf_d is None or rf_d is None: return None + lf_d.Next = rf_d + rf_d.Next = None + res: mlirlite.Value* = mlirlite.build_call(builder, "llvm.pow.f64", lf_d, 2, f64_ty, 0) + if res is None: return None + # f32 target: llvm.pow is always f64, truncate the result back to float + if self.get_llvm_float_bits(lf.Ty) == 32 and self.get_llvm_float_bits(rf.Ty) == 32: res = mlirlite.build_fptrunc(builder, res, mlirlite.Float(pool)) + return res + + # _binop_to_dunder - BinOp op -> dunder method name + # ============================================================ + def _binop_to_dunder(self, op: int) -> str: + """Map a BinOp operator to its dunder method name, or None if unmapped.""" + if op == ast.OpKind.Add: return "__add__" + if op == ast.OpKind.Sub: return "__sub__" + if op == ast.OpKind.Mult: return "__mul__" + if op == ast.OpKind.Div: return "__truediv__" + if op == ast.OpKind.Mod: return "__mod__" + if op == ast.OpKind.Pow: return "__pow__" + if op == ast.OpKind.BitAnd: return "__and__" + if op == ast.OpKind.BitOr: return "__or__" + if op == ast.OpKind.BitXor: return "__xor__" + if op == ast.OpKind.LShift: return "__lshift__" + if op == ast.OpKind.RShift: return "__rshift__" + if op == ast.OpKind.FloorDiv: return "__floordiv__" + return None + # _rbinop_to_dunder - BinOp op -> reverse dunder name (__radd__ etc.): dispatch rhs's reverse dunder + # when lhs lacks the normal dunder (a + b -> b.__radd__(a) if a.__add__ is missing). + def _rbinop_to_dunder(self, op: int) -> str: + """Map a BinOp operator to its reverse dunder method name, or None.""" + if op == ast.OpKind.Add: return "__radd__" + if op == ast.OpKind.Sub: return "__rsub__" + if op == ast.OpKind.Mult: return "__rmul__" + if op == ast.OpKind.Div: return "__rtruediv__" + if op == ast.OpKind.FloorDiv: return "__rfloordiv__" + if op == ast.OpKind.Mod: return "__rmod__" + if op == ast.OpKind.Pow: return "__rpow__" + if op == ast.OpKind.BitAnd: return "__rand__" + if op == ast.OpKind.BitOr: return "__ror__" + if op == ast.OpKind.BitXor: return "__rxor__" + if op == ast.OpKind.LShift: return "__rlshift__" + if op == ast.OpKind.RShift: return "__rrshift__" + return None + # _cmpop_to_dunder - Compare op -> dunder method name + # ============================================================ + def _cmpop_to_dunder(self, op: int) -> str: + """Map a Compare operator to its dunder method name, or None if unmapped.""" + if op == ast.OpKind.Eq: return "__eq__" + if op == ast.OpKind.Ne: return "__ne__" + if op == ast.OpKind.Lt: return "__lt__" + if op == ast.OpKind.Le: return "__le__" + if op == ast.OpKind.Gt: return "__gt__" + if op == ast.OpKind.Ge: return "__ge__" + return None + # _unaryop_to_dunder - UnaryOp op -> dunder method name + # ============================================================ + def _unaryop_to_dunder(self, op: int) -> str: + """Map a UnaryOp operator to its dunder method name, or None if unmapped.""" + if op == ast.OpKind.USub: return "__neg__" + if op == ast.OpKind.UAdd: return "__pos__" + if op == ast.OpKind.Invert: return "__invert__" + return None + # try_operator_overload - attempt operator overloading. If lhs is a struct pointer, look up the dunder method + # in its class (including the inheritance chain) and emit lhs.__dunder__(rhs); otherwise return None to fall back. + def try_operator_overload(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lhs: mlirlite.Value*, rhs: mlirlite.Value*, op: int, trans: HT.Translator*, is_compare: int) -> mlirlite.Value*: + """Call lhs's dunder method when lhs is a struct pointer; else return None.""" + if lhs is None or rhs is None: return None + # Map the operator to a dunder method name + dunder: str = None + if is_compare != 0: dunder = self._cmpop_to_dunder(op) + else: dunder = self._binop_to_dunder(op) + if dunder is None: return None + + # Check whether lhs is a struct pointer + struct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(lhs) + if struct_ty is None: return None + + # Get the class name + class_name: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty) + if class_name is None: return None + + # Look up the dunder method in the inheritance chain + lookup_name: str = pool.alloc(128) + if lookup_name is None: return None + viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder) + found_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, lookup_name) + + search_class: str = class_name + if found_func is None: + # Multiple inheritance: walk the MRO ancestor chain (earlier-declared parent first) + mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name) + mi: int = 1 + while mro_ptr is not None and mro_ptr[mi] != 0: + anc: str = rawptr(mro_ptr[mi]) + if anc is not None: + parent_lookup: str = pool.alloc(128) + if parent_lookup is not None: + viperlib.snprintf(parent_lookup, 128, "%s.%s", anc, dunder) + parent_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, parent_lookup) + if parent_func is not None: + found_func = parent_func + search_class = anc + break + mi += 1 + + # When the method is not in the current module, check whether the class (or a parent) has a SHA1 (stub may be + # uninjected). Prefer locating the entry by type pointer to get the SHA1. + cls_sha1: str = None + op_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty) + if op_entry is not None: cls_sha1 = op_entry.ModuleSha1 + if cls_sha1 is None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(search_class) + if cls_sha1 is None: + mro_p: sizet* = self.Gen.StructGen.get_mro_chain(pool, search_class) + mj: int = 1 + while mro_p is not None and mro_p[mj] != 0 and cls_sha1 is None: + anc_p: str = rawptr(mro_p[mj]) + if anc_p is not None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(anc_p) + mj += 1 + + # Neither a function definition nor a SHA1 -> the class has no such dunder; return None to fall back to native. + if found_func is None and cls_sha1 is None: return None + + # The method really does not exist in its defining module (same-module class without the dunder): return None + # and never emit a ghost call, which would leave a side-effecting `call void @...__div__` and double-emit sdiv. + if found_func is None and trans is not None and trans.ModuleSha1 is not None: + if cls_sha1 is not None and string.strcmp(cls_sha1, trans.ModuleSha1) == 0: return None + # Cross-module class: the stub may be uninjected, so method existence relies on the dependency module's vpi. + # A vpi miss falls back to native comparison (icmp); a ghost call would link as undefined (SysLibTest root cause). + vpi_dup: mlirlite.LLVMType* = self.Gen.CallGenBase._lookup_method_ret_ty_via_vpi(pool, cls_sha1, search_class, dunder) + if vpi_dup is None: return None + + # Build the extra_args array (just the one rhs argument) + extra_args: sizet* = pool.alloc(8) + if extra_args is None: return None + extra_args[0] = sizet(rhs) + + # Call the method: lhs.__dunder__(rhs) + return self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, search_class, dunder, lhs, extra_args, 1, trans) + # try_unary_operator_overload - attempt unary operator overloading. If operand is a struct pointer, look up + # __neg__/__pos__/__invert__ in its class (with inheritance) and emit operand.__dunder__(); else return None. + def try_unary_operator_overload(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, operand: mlirlite.Value*, op: int, trans: HT.Translator*) -> mlirlite.Value*: + """Call operand's dunder method when operand is a struct pointer; else None.""" + if operand is None: return None + dunder: str = self._unaryop_to_dunder(op) + if dunder is None: return None + return self._try_named_dunder(pool, builder, mod, operand, dunder, trans) + + def _resolve_dunder_by_class(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_name: str, dunder: str, trans: HT.Translator*, struct_ty: mlirlite.LLVMType* = None) -> str: + """Resolve the class (including MRO ancestors) that defines the dunder, or None.""" + if class_name is None or dunder is None: return None + + # Look up the dunder method in the inheritance chain + lookup_name: str = pool.alloc(128) + if lookup_name is None: return None + viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder) + found_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, lookup_name) + + search_class: str = class_name + if found_func is None: + # Multiple inheritance: walk the MRO ancestor chain (earlier-declared parent first) + mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name) + mi: int = 1 + while mro_ptr is not None and mro_ptr[mi] != 0: + anc: str = rawptr(mro_ptr[mi]) + if anc is not None: + parent_lookup: str = pool.alloc(128) + if parent_lookup is not None: + viperlib.snprintf(parent_lookup, 128, "%s.%s", anc, dunder) + parent_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, parent_lookup) + if parent_func is not None: + found_func = parent_func + search_class = anc + break + mi += 1 + + # When the method is not in the current module, check whether the class (or a parent) has a SHA1 (stub may be uninjected) + cls_sha1: str = None + if struct_ty is not None: + uop_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty) + if uop_entry is not None: cls_sha1 = uop_entry.ModuleSha1 + if cls_sha1 is None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(search_class) + if cls_sha1 is None: + mro_p: sizet* = self.Gen.StructGen.get_mro_chain(pool, search_class) + mj: int = 1 + while mro_p is not None and mro_p[mj] != 0 and cls_sha1 is None: + anc_p: str = rawptr(mro_p[mj]) + if anc_p is not None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(anc_p) + mj += 1 + + # Neither a function definition nor a SHA1 -> the class has no such dunder method + if found_func is None and cls_sha1 is None: return None + + # Same-module class without the dunder: return None and fall back (consistent with try_operator_overload) to avoid a ghost call. + if found_func is None and trans is not None and trans.ModuleSha1 is not None: + if cls_sha1 is not None and string.strcmp(cls_sha1, trans.ModuleSha1) == 0: return None + # Cross-module class: existence relies on the dependency module's vpi; a vpi miss falls back (no ghost call, avoiding undefined). + if cls_sha1 is not None: + vpi_dup: mlirlite.LLVMType* = self.Gen.CallGenBase._lookup_method_ret_ty_via_vpi(pool, cls_sha1, search_class, dunder) + if vpi_dup is None: return None + return search_class + + def _try_named_dunder(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, operand: mlirlite.Value*, dunder: str, trans: HT.Translator*, extra_args: sizet* = None, extra_count: int = 0) -> mlirlite.Value*: + """Look up and call a dunder method by name on operand's class (including MRO).""" + if operand is None or dunder is None: return None + + # Check whether operand is a struct pointer + struct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(operand) + if struct_ty is None: return None + + # Get the class name + class_name: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty) + if class_name is None: return None + + search_class: str = self._resolve_dunder_by_class(pool, mod, class_name, dunder, trans, struct_ty) + if search_class is None: return None + + # Call the method: operand.__dunder__(extra_args...) + return self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, search_class, dunder, operand, extra_args, extra_count, trans) + + # coerce_cond_to_bool - normalize a condition value to i1 (the single implementation for if/while/and/or/ternary). + # Already-i1 returns as-is; __bool__ is called if defined; pointer != null; otherwise != 0. + def coerce_cond_to_bool(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, val: mlirlite.Value*, trans: HT.Translator*) -> mlirlite.Value*: + """Normalize a condition to i1, preferring __bool__ and else comparing with 0/null.""" + if val is None: return None + bits: int = self.get_llvm_type_bits(val.Ty) + if bits == 1: return val + # Class instance: prefer calling __bool__ (pointer to a struct whose class defines it) + bres: mlirlite.Value* = self._try_named_dunder(pool, builder, mod, val, "__bool__", trans) + if bres is not None: + b_bits: int = self.get_llvm_type_bits(bres.Ty) + if b_bits == 1: return bres + if self.is_ptr_type(bres.Ty) != 0: + null_b: mlirlite.Value* = mlirlite.ConstNull(pool, bres.Ty, "null") + return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, bres, null_b) + zero_b: mlirlite.Value* = mlirlite.const_int32(pool, 0) + return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, bres, zero_b) + if self.is_ptr_type(val.Ty) != 0: + # Pointer type: compare with null (not integer 0, which llc rejects as a type mismatch) + null_val: mlirlite.Value* = mlirlite.ConstNull(pool, val.Ty, "null") + return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, val, null_val) + zero: mlirlite.Value* = mlirlite.const_int32(pool, 0) + return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, val, zero) + # fail_if_double_ptr_binop - double-pointer arithmetic fail-fast (shared by BinOp and AugAssign). Fires when the op + # is arithmetic/bitwise/shift AND both operands are pointers; such IR (`add i8*, i8*`) is invalid, so fail before emitting. + def fail_if_double_ptr_binop(self, pool: memhub.MemBuddy*, op: int, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, is_aug: int = 0) -> int: + """Fail fast on double-pointer arithmetic; returns 0 when not triggered.""" + if lhs is None or rhs is None: return 0 + if self.is_ptr_type(lhs.Ty) == 0 or self.is_ptr_type(rhs.Ty) == 0: return 0 + op_ch: str = None + if op == ast.OpKind.Add: op_ch = "+" + elif op == ast.OpKind.Sub: op_ch = "-" + elif op == ast.OpKind.Mult: op_ch = "*" + elif op == ast.OpKind.Div: op_ch = "/" + elif op == ast.OpKind.FloorDiv: op_ch = "//" + elif op == ast.OpKind.Mod: op_ch = "%" + elif op == ast.OpKind.Pow: op_ch = "**" + elif op == ast.OpKind.BitAnd: op_ch = "&" + elif op == ast.OpKind.BitOr: op_ch = "|" + elif op == ast.OpKind.BitXor: op_ch = "^" + elif op == ast.OpKind.LShift: op_ch = "<<" + elif op == ast.OpKind.RShift: op_ch = ">>" + else: return 0 + op_lbl: str = op_ch + aug_note: str = "" + if is_aug != 0: + # Augmented assignment: build the "+=" form with an f-string (this language's str has no `+` concatenation). + op_lbl = f"{op_ch}=" + aug_note = "(augmented assignment)" + lty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_1059) + rty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_1060) + lty_lbl: str = "?" + rty_lbl: str = "?" + if lty_buf is not None: + mlirlite.TypePrint(lty_buf, 4096, lhs.Ty, pool) + lty_lbl = lty_buf + if string.strcmp(lty_buf, "i8*") == 0: lty_lbl = "str(i8*)" + if rty_buf is not None: + mlirlite.TypePrint(rty_buf, 4096, rhs.Ty, pool) + rty_lbl = rty_buf + if string.strcmp(rty_buf, "i8*") == 0: rty_lbl = "str(i8*)" + col_s: int = 0 + if node is not None: col_s = node.col_offset + 1 + self.Gen.TypeGenErr.fatal_error(node, f"[VPC] illegal double-pointer arithmetic type: op = '{op_lbl}'{aug_note}, " f"left operand type = {lty_lbl}, right operand type = {rty_lbl}. " f"in this language str is i8*, with no `+` concatenation semantics (`str + str` and pointer +/- pointer " f"both degrade to raw pointer add/sub => illegal IR: opt reports " f"invalid operand type for instruction). " f"silent degradation is forbidden, compilation aborted. " f"fix: build strings with f-string interpolation, string.strcat, or viperlib.snprintf.", "TypeError", col_s, col_s) + return 1 + # binop_needs_int_promotion - whether this op takes C-style integer promotion (to >= 32 bits) + def binop_needs_int_promotion(self, op: int) -> int: + """C 语义整数提升的适用范围:**算术与移位**为 1(窄于 32 位先提升再算,避免 8/16 位回绕), + **位运算(& | ^)为 0**——按既有约定"两侧同宽就不提升"(WORD | WORD 保持 u16)。""" + if op == ast.OpKind.Add or op == ast.OpKind.Sub or op == ast.OpKind.Mult: return 1 + if op == ast.OpKind.Div or op == ast.OpKind.FloorDiv or op == ast.OpKind.Mod: return 1 + if op == ast.OpKind.LShift or op == ast.OpKind.RShift: return 1 + return 0 + + # translate_binop - binary operations with automatic type promotion + # ============================================================ + def translate_binop(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: + """Translate a binary operation with automatic type promotion.""" + binop: ast.BinOp* = ast.BinOp*(node) + if binop is None: return None + + lhs_node: ast.AST* = binop.left + rhs_node: ast.AST* = binop.right + op: int = binop.op + + # Translate rhs first (only once, to avoid repeating side effects) + rhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rhs_node, None, 0, trans) + if rhs is None: return None + + # === Operator overload path 1: lhs is a Name bound to a struct variable === + # For value-type variables (e.g. cnt: Counter) translate_value loads a Struct value, but dunder needs Ptr(Struct) as self; try the overload with the alloca pointer. + if lhs_node is not None and lhs_node.kind() == ast.ASTKind.Name and trans is not None: + nm: ast.Name* = ast.Name*(lhs_node) + if nm is not None and nm.id is not None: + lhs_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) + if lhs_alloca is not None: + ovl_self1: mlirlite.Value* = lhs_alloca + # When the alloca is Ptr(Ptr(Struct)) (an object storage slot), load the object pointer as self; using the + # slot address as self reads the object pointer as a field -> garbage (MultiInheritTest root cause). + if lhs_alloca.Ty is not None and lhs_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(lhs_alloca.Ty.Pointee) != 0: + load_self1: mlirlite.Value* = mlirlite.build_load(builder, lhs_alloca.Ty.Pointee, lhs_alloca) + if load_self1 is not None: ovl_self1 = load_self1 + ovl_result: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, ovl_self1, rhs, op, trans, 0) + if ovl_result is not None: return ovl_result + + # Translate lhs normally + lhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, lhs_node, None, 0, trans) + if lhs is None: return None + + # === Operator overload path 2: lhs is Ptr(Struct) (e.g. after loading a Counter* variable) === + ovl_result2: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, lhs, rhs, op, trans, 0) + if ovl_result2 is not None: return ovl_result2 + + # === Operator overload path 3: reverse operator - dispatch rhs.__rX__(lhs) when lhs has no dunder === + # (Python semantics: a + b -> a.__add__(b), else b.__radd__(a)). Skip early if rhs is not a struct. + r_dunder: str = self._rbinop_to_dunder(op) + if r_dunder is not None: + if self.Gen.StructGen.get_struct_type_from_value(rhs) is not None: + rev_extra: sizet* = pool.alloc(8) + if rev_extra is not None: + rev_extra[0] = sizet(lhs) + r_ovl: mlirlite.Value* = self._try_named_dunder(pool, builder, mod, rhs, r_dunder, trans, rev_extra, 1) + if r_ovl is not None: return r_ovl + + # `**` exponentiation: basic types -> llvm.pow (int result written back per the exponent constant). Handled before the float/int split and after overloads. + if op == ast.OpKind.Pow: return self.emit_pow(pool, builder, mod, lhs, rhs, node, trans) + + # Fetch operand type information + lhs_bits: int = self.get_llvm_type_bits(lhs.Ty) + rhs_bits: int = self.get_llvm_type_bits(rhs.Ty) + lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty) + rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty) + + # Float operation: use float instructions when either operand is float (must precede pointer arithmetic, since float has 0 int bits and would be mistaken for a pointer). + if lhs_fbits != 0 or rhs_fbits != 0: + # Determine the target float type (use the larger bit width) + target_fbits: int = lhs_fbits + if rhs_fbits > target_fbits: target_fbits = rhs_fbits + target_float_ty: mlirlite.LLVMType* = mlirlite.Double(pool) + if target_fbits == 32: target_float_ty = mlirlite.Float(pool) + # Convert both operands to the target float type (int->float via si2fp, float->float via fpext/fptrunc) + lhs = self.coerce_to_type(builder, lhs, target_float_ty, node) + rhs = self.coerce_to_type(builder, rhs, target_float_ty, node) + if op == ast.OpKind.Add: return mlirlite.build_fadd(builder, lhs, rhs) + if op == ast.OpKind.Sub: return mlirlite.build_fsub(builder, lhs, rhs) + if op == ast.OpKind.Mult: return mlirlite.build_fmul(builder, lhs, rhs) + if op == ast.OpKind.Div: return mlirlite.build_fdiv(builder, lhs, rhs) + if op == ast.OpKind.Mod: return mlirlite.build_frem(builder, lhs, rhs) + return None + + # Pointer arithmetic: ptr + int / ptr - int / int + ptr -> getelementptr (scaled by element size). int - ptr has no GEP form (illegal in C). + if (lhs_bits == 0 and rhs_bits != 0) or (lhs_bits != 0 and rhs_bits == 0): + if op == ast.OpKind.Add: + if lhs_bits == 0: return self.translate_ptr_arith(pool, builder, lhs, rhs, op) + # int + ptr (swap operands) + return self.translate_ptr_arith(pool, builder, rhs, lhs, op) + if op == ast.OpKind.Sub and lhs_bits == 0: return self.translate_ptr_arith(pool, builder, lhs, rhs, op) + + # Double-pointer arithmetic (including AugAssign) fail-fast: the single implementation is fail_if_double_ptr_binop. + # Pointer-int (GEP) is handled by translate_ptr_arith above; double-pointer Add/Sub/... is illegal here (str is i8*). + self.fail_if_double_ptr_binop(pool, op, lhs, rhs, node, 0) + + # Widening direction (zext/sext) and the div/mod predicates both need "is the operand unsigned"; evaluate once here and thread trans down. + binop_unsigned: int = 0 + if lhs_node is not None: + if self._node_type_unsigned(pool, lhs_node, trans) != 0: binop_unsigned = 1 + if binop_unsigned == 0 and rhs_node is not None: + if self._node_type_unsigned(pool, rhs_node, trans) != 0: binop_unsigned = 1 + # C 语义整数提升(仅算术/移位;位运算 & | ^ 保持"同宽不提升"): # 两侧都窄于 32 位时先提升到 32 位再算,避免 8/16 位回绕(如两个 16 位 define 的 # `MAX_INCLUDES * MAX_REL_PATH_LEN` 会算成 0)。提升后把 lhs_bits/rhs_bits 记为 32, @@ -1219,46 +1219,46 @@ novirtual class ExprGenOps(ExprGenBase.ExprGenBase): lhs_bits = 32 rhs_bits = 32 - if lhs_bits > rhs_bits: - if self.should_zext_widen(pool, rhs, lhs, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, lhs.Ty) - else: rhs = self.coerce_to_type(builder, rhs, lhs.Ty, node, trans) - elif rhs_bits > lhs_bits: - if self.should_zext_widen(pool, lhs, rhs, lhs_node, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, rhs.Ty) - else: lhs = self.coerce_to_type(builder, lhs, rhs.Ty, node, trans) - - # Bitwise type safety: when both sides are pointers (lhs_bits==0 and rhs_bits==0), bitwise ops are illegal (LLVM - # requires integer operands). Common trigger: a failed CDefine lookup returns pointer null, then `|` with a pointer. - if lhs_bits == 0 and rhs_bits == 0: - if op == ast.OpKind.BitOr or op == ast.OpKind.BitAnd or op == ast.OpKind.BitXor or op == ast.OpKind.LShift or op == ast.OpKind.RShift: - # fail-fast: illegal bitwise operand types; abort compilation (expose the upstream root cause) - self.Gen.TypeGenErr.fatal_error(node, "illegal bitwise operand types: both lhs/rhs are non-integer types (pointer or struct), cannot perform bitwise operations", "TypeError") - + if lhs_bits > rhs_bits: + if self.should_zext_widen(pool, rhs, lhs, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, lhs.Ty) + else: rhs = self.coerce_to_type(builder, rhs, lhs.Ty, node, trans) + elif rhs_bits > lhs_bits: + if self.should_zext_widen(pool, lhs, rhs, lhs_node, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, rhs.Ty) + else: lhs = self.coerce_to_type(builder, lhs, rhs.Ty, node, trans) + + # Bitwise type safety: when both sides are pointers (lhs_bits==0 and rhs_bits==0), bitwise ops are illegal (LLVM + # requires integer operands). Common trigger: a failed CDefine lookup returns pointer null, then `|` with a pointer. + if lhs_bits == 0 and rhs_bits == 0: + if op == ast.OpKind.BitOr or op == ast.OpKind.BitAnd or op == ast.OpKind.BitXor or op == ast.OpKind.LShift or op == ast.OpKind.RShift: + # fail-fast: illegal bitwise operand types; abort compilation (expose the upstream root cause) + self.Gen.TypeGenErr.fatal_error(node, "illegal bitwise operand types: both lhs/rhs are non-integer types (pointer or struct), cannot perform bitwise operations", "TypeError") + # 移位:计数静态可确定时做范围检查(与 AugAssign 的 <<= / >>= 共用); # 计数宽度本身豁免,按提升/等宽化后的左操作数有效位宽比较(C 语义下 u8 << 8 合法)。 if op == ast.OpKind.LShift or op == ast.OpKind.RShift: self.Gen.AssignGen.require_shift_count_in_range(pool, trans, node, rhs_node, self.get_llvm_type_bits(lhs.Ty)) - if op == ast.OpKind.Add: return mlirlite.build_add(builder, lhs, rhs) - elif op == ast.OpKind.Sub: return mlirlite.build_sub(builder, lhs, rhs) - elif op == ast.OpKind.Mult: return mlirlite.build_mul(builder, lhs, rhs) - elif op == ast.OpKind.Div: - # Unsigned operands -> udiv (binop_unsigned from _node_type_unsigned, covering sizet/uint64 and annotated - # variable/array elements). An unconditional sdiv gave wrong results for unsigned values >= 2^63. - if binop_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) - return mlirlite.build_sdiv(builder, lhs, rhs) - elif op == ast.OpKind.FloorDiv: - if binop_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) - return mlirlite.build_sdiv(builder, lhs, rhs) - elif op == ast.OpKind.Mod: - if binop_unsigned != 0: return mlirlite.build_urem(builder, lhs, rhs) - return mlirlite.build_srem(builder, lhs, rhs) - elif op == ast.OpKind.BitAnd: return mlirlite.build_and(builder, lhs, rhs) - elif op == ast.OpKind.BitOr: return mlirlite.build_or(builder, lhs, rhs) - elif op == ast.OpKind.BitXor: return mlirlite.build_xor(builder, lhs, rhs) - elif op == ast.OpKind.LShift: return mlirlite.build_shl(builder, lhs, rhs) - elif op == ast.OpKind.RShift: - # Unsigned >> -> lshr, signed >> -> ashr (aligned with TPC ExprGen.emit_binary_op: i8/i16 default unsigned, others check operand signedness). - if lhs_bits == 8 or lhs_bits == 16: return mlirlite.build_lshr(builder, lhs, rhs) - if self._node_type_unsigned(pool, lhs_node, trans) != 0: return mlirlite.build_lshr(builder, lhs, rhs) - return mlirlite.build_ashr(builder, lhs, rhs) + if op == ast.OpKind.Add: return mlirlite.build_add(builder, lhs, rhs) + elif op == ast.OpKind.Sub: return mlirlite.build_sub(builder, lhs, rhs) + elif op == ast.OpKind.Mult: return mlirlite.build_mul(builder, lhs, rhs) + elif op == ast.OpKind.Div: + # Unsigned operands -> udiv (binop_unsigned from _node_type_unsigned, covering sizet/uint64 and annotated + # variable/array elements). An unconditional sdiv gave wrong results for unsigned values >= 2^63. + if binop_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) + return mlirlite.build_sdiv(builder, lhs, rhs) + elif op == ast.OpKind.FloorDiv: + if binop_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs) + return mlirlite.build_sdiv(builder, lhs, rhs) + elif op == ast.OpKind.Mod: + if binop_unsigned != 0: return mlirlite.build_urem(builder, lhs, rhs) + return mlirlite.build_srem(builder, lhs, rhs) + elif op == ast.OpKind.BitAnd: return mlirlite.build_and(builder, lhs, rhs) + elif op == ast.OpKind.BitOr: return mlirlite.build_or(builder, lhs, rhs) + elif op == ast.OpKind.BitXor: return mlirlite.build_xor(builder, lhs, rhs) + elif op == ast.OpKind.LShift: return mlirlite.build_shl(builder, lhs, rhs) + elif op == ast.OpKind.RShift: + # Unsigned >> -> lshr, signed >> -> ashr (aligned with TPC ExprGen.emit_binary_op: i8/i16 default unsigned, others check operand signedness). + if lhs_bits == 8 or lhs_bits == 16: return mlirlite.build_lshr(builder, lhs, rhs) + if self._node_type_unsigned(pool, lhs_node, trans) != 0: return mlirlite.build_lshr(builder, lhs, rhs) + return mlirlite.build_ashr(builder, lhs, rhs) return None \ No newline at end of file diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index 113cf33..0a7b230 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -1,1162 +1,1162 @@ -import lib.core.Gen.GenTypes as GenTypes -import lib.core.Gen.Func.FuncGenMixin as FuncGenMixin -import t, c -from stdint import * -import ast -import mlirlite -import memhub -import string -import viperlib -import stdio -import stdlib -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HT -import lib.core.Gen.Call.CallGenBase as CallGenBase -import lib.core.Gen.GenApi as GenApi -import lib.core.Gen.DebugInfoGen as DebugInfoGen - - -# FuncGenDef - function declaration / translate_function_def / decorator wrapper -# ============================================================ -novirtual class FuncGenDef(CallGenBase.CallGenBase): - """Function declarations, translate_function_def, and decorator wrappers.""" - - # _register_func_auto_addr_modes - record which parameters of fd are value references (&T / &temp T). - # Registered under the emitted symbol name so call sites can auto-address the argument. - def _register_func_auto_addr_modes(self, trans: HT.Translator*, name: str, fd: ast.FunctionDef*) -> int: - """Register per-parameter value-reference modes for a compiled function definition.""" - if trans is None or name is None or fd is None or fd.args is None: return 0 - ags: ast.Arguments* = ast.Arguments*(fd.args) - if ags.args is None: return 0 - alist: list[ast.AST*]* = ags.args - an: sizet = len(alist) - buf: bytes = stdlib.malloc(an + 1) - if buf is None: return 0 - cnt: int = 0 - has_ref: int = 0 - imp: str = trans._imported_modules - for i in range(int(an)): - arg: ast.Arg* = ast.Arg*(alist.get(i)) - if arg is None or arg.arg is None: continue - # CVoid params are not emitted into the signature, so they must not occupy a mode slot. - if arg.annotation is not None and self.Gen.TypeGenResolve._is_t_attr(arg.annotation, "CVoid", imp) != 0: continue - m: int = 0 - if arg.annotation is not None: m = self.Gen.TypeGenResolve.ref_param_mode(arg.annotation, imp) - if m == 1: buf[cnt] = char(49) - elif m == 2: buf[cnt] = char(50) - else: buf[cnt] = char(48) - if m != 0: has_ref = 1 - cnt += 1 - buf[cnt] = char(0) - if has_ref != 0: self.Gen.CallGenBase._register_auto_addr_modes(name, buf, cnt) - stdlib.free(buf) - return 1 - - # forward_declare_functions - pre-scan all top-level FunctionDefs and emit forward declares. - # Solves same-module forward references; translate_function_def later reuses the declare via find_func_in_module. - def forward_declare_functions(self, trans: HT.Translator*, tree: ast.AST*) -> int: - """Pre-scan all top-level FunctionDefs and create forward declarations.""" - if trans is None or tree is None: return 0 - pool: memhub.MemBuddy* = trans.Pool - mod: mlirlite.LLVMModule* = trans.Module - if pool is None or mod is None: return 0 - - imported_modules: str = trans._imported_modules - from_imports: str = trans._from_imports - funcs_ptr: GenTypes.FuncEntry* = trans._funcs - - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - - ch: list[ast.AST*]* = tree.children - if ch is None: return 0 - - # Temporarily switch to declare-only mode while parsing signatures: generic class annotations trigger - # specialization, and translating method bodies now would forward-reference not-yet-declared functions. - saved_declare_only: int = trans._declare_only - trans._declare_only = 1 - cn_count: sizet = len(ch) - - # Pre-register this module's ClassDefs in declare-only mode (struct+fields+method declares, no bodies): - # stage 1a cannot resolve dependent CTypedef (vpi not yet injected); re-registering here keeps annotations resolvable. - pre_i: sizet - for pre_i in range(int(cn_count)): - pre_child: ast.AST* = ch.get(pre_i) - if pre_child is None: continue - if pre_child.kind() != ast.ASTKind.ClassDef: continue - pre_cd: ast.ClassDef* = ast.ClassDef*(pre_child) - if pre_cd is None or pre_cd.name is None: continue - pre_ex: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_module(pre_cd.name, trans.ModuleSha1) - if pre_ex is not None and pre_ex.FieldCount > 0: continue - self.Gen.ClassGenDef.translate_class_def(trans, pre_child) - - for ci in range(int(cn_count)): - child: ast.AST* = ch.get(ci) - if child is None: continue - if child.kind() != ast.ASTKind.FunctionDef: continue - fd: ast.FunctionDef* = ast.FunctionDef*(child) - if fd is None or fd.name is None: continue - - # PEP 695 generic functions emit no forward declare; they register as templates and are cloned by - # _specialize_generic_function at call sites. Old behavior (mapping T to int, compiling as ordinary) is gone. - if fd.type_params is not None: - self.Gen.ClassGenGeneric._register_generic_func_template(pool, fd, trans.ModuleSha1) - continue - tp_names_fd: list[str]* = None - - # Infer the return type - ret_ty: mlirlite.LLVMType* = None - if fd.returns is not None: - # Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return). - self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type") - ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0) - # A lone t.CExport return annotation is an export modifier with no real type -> Void - # (CExport fell back to i32, forcing the tail check to return i32: mm.vp free root cause). - if fd.returns.kind() == ast.ASTKind.Attribute: - ce_at: ast.Attribute* = ast.Attribute*(fd.returns) - if ce_at.attr is not None and string.strcmp(ce_at.attr, "CExport") == 0: - if ce_at.value is not None and ce_at.value.kind() == ast.ASTKind.Name: - ce_nm: ast.Name* = ast.Name*(ce_at.value) - if ce_nm.id is not None and string.strcmp(ce_nm.id, "t") == 0: ret_ty = mlirlite.Void(pool) - if ret_ty is None and fd.returns is not None: - if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0: ret_ty = mlirlite.Void(pool) - # @c.CReturn decorator removed: encountering it is a compile-time error (use -> tuple[...] for multiple returns) - if ret_ty is None: self.Gen.FuncGenBase.RejectCReturnDecorator(fd, fd.decorator_list) - if ret_ty is None and fd.returns is not None: - # Return annotation present but unresolvable: compile-time error, no silent i32 fallback - # (that forced return i32 and misaligned cross-module return types). No annotation keeps i32. - self.Gen.TypeGenErr.fatal_error(fd, "function return type resolution failed: annotation is not resolvable", "TypeError") - return 0 - if ret_ty is None: ret_ty = i32_ty - - # Detect CExtern/State/CExport - has_extern: int = 0 - has_state: int = 0 - has_export: int = 0 - if fd.returns is not None: - has_extern = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExtern", 0) - has_state = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) - has_export = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExport", 0) - # Decorator-form storage markers (@t.CExport/@t.CExtern/@t.State/@t.CStatic), aligned with TPC - # HandlesFunctions._extract_decorator_storage_flags. Use ClassGenBase._has_decorator, not self. (NoVTable). - if fd.decorator_list is not None: - if has_extern == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExtern") == 1: has_extern = 1 - if has_state == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "State") == 1: has_state = 1 - if has_export == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExport") == 1: has_export = 1 - is_extern_decl: int = 0 - if has_extern != 0 or has_state != 0: is_extern_decl = 1 - - # static (no cross-module use): recognize both the annotation form and the decorator form and register; - # logic is kept in _RegisterStaticFunc (inlining a new branch would disturb this function's type inference). - self._RegisterStaticFunc(trans, fd) - - # Compute the mangled name - if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: mangled_name: str = fd.name - else: mangled_name = self.Gen.FuncGenBase._mangle_func_name(trans, fd.name, 0) - - # Record value-reference (&T / &temp T) parameter modes for call-site auto-address. - self._register_func_auto_addr_modes(trans, mangled_name, fd) - - # Skip if the function already exists (e.g. duplicate definition) - existing: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name) - if existing is not None: - if tp_names_fd is not None: self.Gen.TypeGenBase.clear_generic_context() - continue - - # Create the declare - func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) - if func is None: - if tp_names_fd is not None: self.Gen.TypeGenBase.clear_generic_context() - continue - - # Extract default-argument info. Note: assign the attribute to an explicitly typed list[...]* local before - # calling __len__(), otherwise the list type is not recognized and the GEP base becomes i32 0 (llc error). - fd_args_node: ast.Arguments* = fd.args - fd_defaults: t.CPtr = None - fd_default_count: int = 0 - fd_param_count: int = 0 - if fd_args_node is not None: - fd_ags: ast.Arguments* = ast.Arguments*(fd_args_node) - if fd_ags.args is not None: - fd_alist: list[ast.AST*]* = fd_ags.args - fd_param_count = int(len(fd_alist)) - if fd_ags.defaults is not None: - fd_dlist: list[ast.AST*]* = fd_ags.defaults - fd_defaults = fd_dlist - fd_default_count = int(len(fd_dlist)) - - # Register in the function table (using bare fd.name, not mangled_name) - max_funcs: int = 1024 - cur_count: int = trans._func_count - if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, cur_count, fd.name, func, max_funcs, fd_defaults, fd_default_count, fd_param_count, trans.Pool) == 0: - trans._func_count = cur_count + 1 - - # Register the CExport function in the global table - if (has_export != 0 or has_state != 0) and trans.ModuleSha1 is not None: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name) - - # Add parameters - args_node: ast.Arguments* = fd.args - if args_node is not None: - ags: ast.Arguments* = ast.Arguments*(args_node) - if ags.args is not None: - alist: list[ast.AST*]* = ags.args - an: sizet = len(alist) - for ai in range(int(an)): - arg: ast.Arg* = ast.Arg*(alist.get(ai)) - if arg is None or arg.arg is None: continue - # CVoid means empty param: skip - if arg.annotation is not None: - if self.Gen.TypeGenResolve._is_t_attr(arg.annotation, "CVoid", imported_modules) != 0: continue - param_ty: mlirlite.LLVMType* = i32_ty - if arg.annotation is not None: - resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, imported_modules, from_imports, trans, 0) - if resolved is None: - # [S4 fail-fast] Annotation present but unresolvable: no silent i32 - # (declare/define signature mismatch -> ABI misalignment, runtime reads garbage from the stack) - self.Gen.TypeGenErr.fatal_error(arg, f"forward declaration param '{arg.arg}' type annotation failed to resolve", "NameError") - return 0 - # Bare class param -> Ptr(struct) (object is a pointer); CArray param -> element pointer ty* (C array decay). - # Exception (keep in sync in 3 places): C ABI extern bare struct params pass by value %Struct with a full definition. - match resolved: - case mlirlite.LLVMType.Void(): - # void is not a legal param type (void is only for void* and typedef of an opaque struct); - # fail-fast at compile time instead of emitting invalid IR. `void*` is already i8* upstream. - self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") - return 0 - case mlirlite.LLVMType.Struct(_, _, _): - if is_extern_decl != 0 and self.Gen.StructGen.find_struct_by_type(resolved) is not None: - self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved) - param_ty = resolved - else: param_ty = mlirlite.Ptr(pool, resolved) - case mlirlite.LLVMType.Array(aelem_ty, _): - param_ty = mlirlite.Ptr(pool, aelem_ty) - case _: - param_ty = resolved - pname: str = pool.alloc(32) - if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg) - mlirlite.add_param(pool, func, param_ty, pname) - - # *args variadic: set IsVarArg=1 to emit the ... suffix - if ags.vararg is not None: func.IsVarArg = 1 - - # Params added; fill the global func-table Spec (lets vtable derive type text after func is reset) - self.Gen.CallGenLeafKit.update_global_func_spec(fd.name, func) - - # Clear the generic-func context once used (prevents leaking into later functions' annotation resolution) - if tp_names_fd is not None: self.Gen.TypeGenBase.clear_generic_context() - - # Restore the original declare mode - trans._declare_only = saved_declare_only - return 0 - # translate_function_def - translate a function definition FunctionDef(name, args, body, ...). - # Single-trans arg mode: all shared state comes from trans; local scope via enter_scope/exit_scope. +import lib.core.Gen.GenTypes as GenTypes +import lib.core.Gen.Func.FuncGenMixin as FuncGenMixin +import t, c +from stdint import * +import ast +import mlirlite +import memhub +import string +import viperlib +import stdio +import stdlib +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HT +import lib.core.Gen.Call.CallGenBase as CallGenBase +import lib.core.Gen.GenApi as GenApi +import lib.core.Gen.DebugInfoGen as DebugInfoGen + + +# FuncGenDef - function declaration / translate_function_def / decorator wrapper +# ============================================================ +novirtual class FuncGenDef(CallGenBase.CallGenBase): + """Function declarations, translate_function_def, and decorator wrappers.""" + + # _register_func_auto_addr_modes - record which parameters of fd are value references (&T / &temp T). + # Registered under the emitted symbol name so call sites can auto-address the argument. + def _register_func_auto_addr_modes(self, trans: HT.Translator*, name: str, fd: ast.FunctionDef*) -> int: + """Register per-parameter value-reference modes for a compiled function definition.""" + if trans is None or name is None or fd is None or fd.args is None: return 0 + ags: ast.Arguments* = ast.Arguments*(fd.args) + if ags.args is None: return 0 + alist: list[ast.AST*]* = ags.args + an: sizet = len(alist) + buf: bytes = stdlib.malloc(an + 1) + if buf is None: return 0 + cnt: int = 0 + has_ref: int = 0 + imp: str = trans._imported_modules + for i in range(int(an)): + arg: ast.Arg* = ast.Arg*(alist.get(i)) + if arg is None or arg.arg is None: continue + # CVoid params are not emitted into the signature, so they must not occupy a mode slot. + if arg.annotation is not None and self.Gen.TypeGenResolve._is_t_attr(arg.annotation, "CVoid", imp) != 0: continue + m: int = 0 + if arg.annotation is not None: m = self.Gen.TypeGenResolve.ref_param_mode(arg.annotation, imp) + if m == 1: buf[cnt] = char(49) + elif m == 2: buf[cnt] = char(50) + else: buf[cnt] = char(48) + if m != 0: has_ref = 1 + cnt += 1 + buf[cnt] = char(0) + if has_ref != 0: self.Gen.CallGenBase._register_auto_addr_modes(name, buf, cnt) + stdlib.free(buf) + return 1 + + # forward_declare_functions - pre-scan all top-level FunctionDefs and emit forward declares. + # Solves same-module forward references; translate_function_def later reuses the declare via find_func_in_module. + def forward_declare_functions(self, trans: HT.Translator*, tree: ast.AST*) -> int: + """Pre-scan all top-level FunctionDefs and create forward declarations.""" + if trans is None or tree is None: return 0 + pool: memhub.MemBuddy* = trans.Pool + mod: mlirlite.LLVMModule* = trans.Module + if pool is None or mod is None: return 0 + + imported_modules: str = trans._imported_modules + from_imports: str = trans._from_imports + funcs_ptr: GenTypes.FuncEntry* = trans._funcs + + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + + ch: list[ast.AST*]* = tree.children + if ch is None: return 0 + + # Temporarily switch to declare-only mode while parsing signatures: generic class annotations trigger + # specialization, and translating method bodies now would forward-reference not-yet-declared functions. + saved_declare_only: int = trans._declare_only + trans._declare_only = 1 + cn_count: sizet = len(ch) + + # Pre-register this module's ClassDefs in declare-only mode (struct+fields+method declares, no bodies): + # stage 1a cannot resolve dependent CTypedef (vpi not yet injected); re-registering here keeps annotations resolvable. + pre_i: sizet + for pre_i in range(int(cn_count)): + pre_child: ast.AST* = ch.get(pre_i) + if pre_child is None: continue + if pre_child.kind() != ast.ASTKind.ClassDef: continue + pre_cd: ast.ClassDef* = ast.ClassDef*(pre_child) + if pre_cd is None or pre_cd.name is None: continue + pre_ex: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_module(pre_cd.name, trans.ModuleSha1) + if pre_ex is not None and pre_ex.FieldCount > 0: continue + self.Gen.ClassGenDef.translate_class_def(trans, pre_child) + + for ci in range(int(cn_count)): + child: ast.AST* = ch.get(ci) + if child is None: continue + if child.kind() != ast.ASTKind.FunctionDef: continue + fd: ast.FunctionDef* = ast.FunctionDef*(child) + if fd is None or fd.name is None: continue + + # PEP 695 generic functions emit no forward declare; they register as templates and are cloned by + # _specialize_generic_function at call sites. Old behavior (mapping T to int, compiling as ordinary) is gone. + if fd.type_params is not None: + self.Gen.ClassGenGeneric._register_generic_func_template(pool, fd, trans.ModuleSha1) + continue + tp_names_fd: list[str]* = None + + # Infer the return type + ret_ty: mlirlite.LLVMType* = None + if fd.returns is not None: + # Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return). + self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type") + ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0) + # A lone t.CExport return annotation is an export modifier with no real type -> Void + # (CExport fell back to i32, forcing the tail check to return i32: mm.vp free root cause). + if fd.returns.kind() == ast.ASTKind.Attribute: + ce_at: ast.Attribute* = ast.Attribute*(fd.returns) + if ce_at.attr is not None and string.strcmp(ce_at.attr, "CExport") == 0: + if ce_at.value is not None and ce_at.value.kind() == ast.ASTKind.Name: + ce_nm: ast.Name* = ast.Name*(ce_at.value) + if ce_nm.id is not None and string.strcmp(ce_nm.id, "t") == 0: ret_ty = mlirlite.Void(pool) + if ret_ty is None and fd.returns is not None: + if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0: ret_ty = mlirlite.Void(pool) + # @c.CReturn decorator removed: encountering it is a compile-time error (use -> tuple[...] for multiple returns) + if ret_ty is None: self.Gen.FuncGenBase.RejectCReturnDecorator(fd, fd.decorator_list) + if ret_ty is None and fd.returns is not None: + # Return annotation present but unresolvable: compile-time error, no silent i32 fallback + # (that forced return i32 and misaligned cross-module return types). No annotation keeps i32. + self.Gen.TypeGenErr.fatal_error(fd, "function return type resolution failed: annotation is not resolvable", "TypeError") + return 0 + if ret_ty is None: ret_ty = i32_ty + + # Detect CExtern/State/CExport + has_extern: int = 0 + has_state: int = 0 + has_export: int = 0 + if fd.returns is not None: + has_extern = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExtern", 0) + has_state = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) + has_export = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExport", 0) + # Decorator-form storage markers (@t.CExport/@t.CExtern/@t.State/@t.CStatic), aligned with TPC + # HandlesFunctions._extract_decorator_storage_flags. Use ClassGenBase._has_decorator, not self. (NoVTable). + if fd.decorator_list is not None: + if has_extern == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExtern") == 1: has_extern = 1 + if has_state == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "State") == 1: has_state = 1 + if has_export == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExport") == 1: has_export = 1 + is_extern_decl: int = 0 + if has_extern != 0 or has_state != 0: is_extern_decl = 1 + + # static (no cross-module use): recognize both the annotation form and the decorator form and register; + # logic is kept in _RegisterStaticFunc (inlining a new branch would disturb this function's type inference). + self._RegisterStaticFunc(trans, fd) + + # Compute the mangled name + if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: mangled_name: str = fd.name + else: mangled_name = self.Gen.FuncGenBase._mangle_func_name(trans, fd.name, 0) + + # Record value-reference (&T / &temp T) parameter modes for call-site auto-address. + self._register_func_auto_addr_modes(trans, mangled_name, fd) + + # Skip if the function already exists (e.g. duplicate definition) + existing: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name) + if existing is not None: + if tp_names_fd is not None: self.Gen.TypeGenBase.clear_generic_context() + continue + + # Create the declare + func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) + if func is None: + if tp_names_fd is not None: self.Gen.TypeGenBase.clear_generic_context() + continue + + # Extract default-argument info. Note: assign the attribute to an explicitly typed list[...]* local before + # calling __len__(), otherwise the list type is not recognized and the GEP base becomes i32 0 (llc error). + fd_args_node: ast.Arguments* = fd.args + fd_defaults: t.CPtr = None + fd_default_count: int = 0 + fd_param_count: int = 0 + if fd_args_node is not None: + fd_ags: ast.Arguments* = ast.Arguments*(fd_args_node) + if fd_ags.args is not None: + fd_alist: list[ast.AST*]* = fd_ags.args + fd_param_count = int(len(fd_alist)) + if fd_ags.defaults is not None: + fd_dlist: list[ast.AST*]* = fd_ags.defaults + fd_defaults = fd_dlist + fd_default_count = int(len(fd_dlist)) + + # Register in the function table (using bare fd.name, not mangled_name) + max_funcs: int = 1024 + cur_count: int = trans._func_count + if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, cur_count, fd.name, func, max_funcs, fd_defaults, fd_default_count, fd_param_count, trans.Pool) == 0: + trans._func_count = cur_count + 1 + + # Register the CExport function in the global table + if (has_export != 0 or has_state != 0) and trans.ModuleSha1 is not None: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name) + + # Add parameters + args_node: ast.Arguments* = fd.args + if args_node is not None: + ags: ast.Arguments* = ast.Arguments*(args_node) + if ags.args is not None: + alist: list[ast.AST*]* = ags.args + an: sizet = len(alist) + for ai in range(int(an)): + arg: ast.Arg* = ast.Arg*(alist.get(ai)) + if arg is None or arg.arg is None: continue + # CVoid means empty param: skip + if arg.annotation is not None: + if self.Gen.TypeGenResolve._is_t_attr(arg.annotation, "CVoid", imported_modules) != 0: continue + param_ty: mlirlite.LLVMType* = i32_ty + if arg.annotation is not None: + resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, imported_modules, from_imports, trans, 0) + if resolved is None: + # [S4 fail-fast] Annotation present but unresolvable: no silent i32 + # (declare/define signature mismatch -> ABI misalignment, runtime reads garbage from the stack) + self.Gen.TypeGenErr.fatal_error(arg, f"forward declaration param '{arg.arg}' type annotation failed to resolve", "NameError") + return 0 + # Bare class param -> Ptr(struct) (object is a pointer); CArray param -> element pointer ty* (C array decay). + # Exception (keep in sync in 3 places): C ABI extern bare struct params pass by value %Struct with a full definition. + match resolved: + case mlirlite.LLVMType.Void(): + # void is not a legal param type (void is only for void* and typedef of an opaque struct); + # fail-fast at compile time instead of emitting invalid IR. `void*` is already i8* upstream. + self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") + return 0 + case mlirlite.LLVMType.Struct(_, _, _): + if is_extern_decl != 0 and self.Gen.StructGen.find_struct_by_type(resolved) is not None: + self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved) + param_ty = resolved + else: param_ty = mlirlite.Ptr(pool, resolved) + case mlirlite.LLVMType.Array(aelem_ty, _): + param_ty = mlirlite.Ptr(pool, aelem_ty) + case _: + param_ty = resolved + pname: str = pool.alloc(32) + if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg) + mlirlite.add_param(pool, func, param_ty, pname) + + # *args variadic: set IsVarArg=1 to emit the ... suffix + if ags.vararg is not None: func.IsVarArg = 1 + + # Params added; fill the global func-table Spec (lets vtable derive type text after func is reset) + self.Gen.CallGenLeafKit.update_global_func_spec(fd.name, func) + + # Clear the generic-func context once used (prevents leaking into later functions' annotation resolution) + if tp_names_fd is not None: self.Gen.TypeGenBase.clear_generic_context() + + # Restore the original declare mode + trans._declare_only = saved_declare_only + return 0 + # translate_function_def - translate a function definition FunctionDef(name, args, body, ...). + # Single-trans arg mode: all shared state comes from trans; local scope via enter_scope/exit_scope. def translate_function_def(self, trans: HT.Translator*, node: ast.AST*) -> int: """Translate a function definition; returns the number of added variables (usually 0).""" fd: ast.FunctionDef* = ast.FunctionDef*(node) - if fd is None or fd.name is None: return 0 - - # PEP 695 generic function templates are not emitted directly (same rule as forward_declare_functions): - # clones are emitted at call sites by _specialize_generic_function; here we only ensure the template is registered. - if fd.type_params is not None: - self.Gen.ClassGenGeneric._register_generic_func_template(trans.Pool, fd, trans.ModuleSha1) - return 0 - - # async def (TPV-M1 coroutine): FLAG_IS_ASYNC=1 (parser stores async def as FunctionDef with flags & 1) - # -> AsyncGenMixin state machine (create_/resume_); early-return so the plain path cannot suspend/resume. - if fd.flags is not None and (fd.flags & 1) != 0: return self.Gen.AsyncGen.translate_async_function(trans, node) - - pool: memhub.MemBuddy* = trans.Pool - mod: mlirlite.LLVMModule* = trans.Module - imported_modules: str = trans._imported_modules - from_imports: str = trans._from_imports - funcs_ptr: GenTypes.FuncEntry* = trans._funcs - func_count: int = trans._func_count - - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - - # Infer the return type: prefer the return annotation - ret_ty: mlirlite.LLVMType* = None - if fd.returns is not None: - # Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return). - self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type") - ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0) - # A lone t.CExport return annotation is an export modifier with no real type -> Void - # (CExport fell back to i32, forcing return i32: mm.vp free root cause). Mixed types keep the resolved result. - if fd.returns.kind() == ast.ASTKind.Attribute: - ce_at2: ast.Attribute* = ast.Attribute*(fd.returns) - if ce_at2.attr is not None and string.strcmp(ce_at2.attr, "CExport") == 0: - if ce_at2.value is not None and ce_at2.value.kind() == ast.ASTKind.Name: - ce_nm2: ast.Name* = ast.Name*(ce_at2.value) - if ce_nm2.id is not None and string.strcmp(ce_nm2.id, "t") == 0: ret_ty = mlirlite.Void(pool) - - # If the return annotation is purely a decorator marker (e.g. t.State, no real type), use void. - # Must be checked before infer_return_type, which would return at least i32. - if ret_ty is None and fd.returns is not None: - if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0: ret_ty = mlirlite.Void(pool) - - # @c.CReturn decorator removed: encountering it is a compile-time error (use -> tuple[...] for multiple returns) - if ret_ty is None: self.Gen.FuncGenBase.RejectCReturnDecorator(fd, fd.decorator_list) - - # If still None, infer by scanning return statements (at least i32) - if ret_ty is None: - # fail-fast: no return annotation (fd.returns is None) but the body returns a non-None value -> error, no silent - # inference. Returning a value requires an explicit -> annotation; unannotated functions allow only procedure form. - if fd.returns is None: - if self.Gen.TypeGenErr.body_has_value_return(pool, fd.children) != 0: - rtn_err: str = pool.alloc(256) - if rtn_err is not None: - viperlib.snprintf(rtn_err, 256, "function '%s' has no return type annotation but returns a non-None value, add an explicit return type annotation (e.g. -> int)", fd.name) - self.Gen.TypeGenErr.fatal_error(fd, rtn_err, "TypeError") - return 0 - self.Gen.TypeGenErr.fatal_error(fd, "function has no return type annotation but returns a non-None value", "TypeError") - return 0 - param_types_str: str = self.Gen.TypeGenErr.build_param_types_str(pool, fd.args) - ret_ty = self.Gen.TypeGenErr.infer_return_type(pool, fd.children, param_types_str) - # Detect an extern declaration (t.CExtern or t.State): t.CExtern ignores the body and emits only a declare; - # t.State = t.CExtern + t.CExport. t.CExtern/t.State/t.CExport get no SHA1 prefix and map to C library names. - is_extern_decl: int = 0 - has_export: int = 0 - has_extern: int = 0 - has_state: int = 0 - if fd.returns is not None: - has_extern = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExtern", 0) - has_state = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) - has_export = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExport", 0) - # t.CExtern/t.State ignore the body and only declare (whether the body is pass or not) - if has_extern != 0 or has_state != 0: is_extern_decl = 1 - # Decorator-form storage markers (@t.CExport/@t.CExtern/@t.State/@t.CStatic), aligned with TPC - # HandlesFunctions._extract_decorator_storage_flags (same as forward_declare_functions). Use ClassGenBase._has_decorator. - if fd.decorator_list is not None: - if has_extern == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExtern") == 1: has_extern = 1 - if has_state == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "State") == 1: has_state = 1 - if has_export == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExport") == 1: has_export = 1 - if (has_extern != 0 or has_state != 0) and is_extern_decl == 0: is_extern_decl = 1 - # @t.TLS one-time-init marker (aligned with TPC HandlesFunctions.py L1223-1231) - has_tls: int = 0 - if fd.decorator_list is not None: has_tls = self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "TLS") - - # Custom behavior decorator detection (@log/@skip/@repeat(3) etc.): Name/Name(args) decorators that are neither - # t storage markers nor builtins. Aligned with TPC HandlesFunctions.py L1161-1189 + DecoratorPass.py; first one wins. - has_custom_decor: int = 0 - decor_handler: str = None - decor_arg0: int64 = 0 - if fd.decorator_list is not None: - ddn: sizet = len(fd.decorator_list) - for ddi in range(int(ddn)): - deco: ast.AST* = fd.decorator_list.get(ddi) - if deco is None: continue - dk: int = deco.kind() - dname: str = None - if dk == ast.ASTKind.Name: - dnm: ast.Name* = ast.Name*(deco) - dname = dnm.id - elif dk == ast.ASTKind.Call: - dcl: ast.Call* = ast.Call*(deco) - if dcl.func is not None and dcl.func.kind() == ast.ASTKind.Name: - dnm2: ast.Name* = ast.Name*(dcl.func) - dname = dnm2.id - # @repeat(3): take the first constant int argument (aligned with TPC L1182-1184) - if dcl.args is not None and len(dcl.args) >= 1: - da0: ast.AST* = dcl.args.get(0) - if da0 is not None and da0.kind() == ast.ASTKind.Constant: - dc0: ast.Constant* = ast.Constant*(da0) - if dc0.const_kind == ast.CONST_INT: decor_arg0 = dc0.int_val - if dname is None: continue - # Skip builtin decorators - if string.strcmp(dname, "staticmethod") == 0 or string.strcmp(dname, "property") == 0 or string.strcmp(dname, "classmethod") == 0: continue - # Skip t-module storage markers (@t.CExport/@t.CExtern/@t.State/@t.TLS/@t.CStatic/@t.CInline/@t.CVTable/ - # @t.NoVTable/@t.Object/@t.CEnum/@t.CUnion/@t.REnum/@t.CDefine). Attribute-form @t.xxx is not handled here. - # 另跳过**编译期生命期标注**(P2:规范形式 @life(willFree(...)) 与兼容叠加形式 @willFree(...)): - # 它们是纯标注、没有运行时装饰器函数。漏在这里会让 TPV 生成 __decor_wrap_ wrapper, - # 去调用并不存在的 @life/@willFree,并把被装饰的 extern def(如 stdlib.free)变成 internal 声明 - # (现场:llc 报 "invalid linkage for function declaration")。 - if string.strcmp(dname, "CExport") == 0 or string.strcmp(dname, "CExtern") == 0 or string.strcmp(dname, "State") == 0 or string.strcmp(dname, "TLS") == 0 or string.strcmp(dname, "CStatic") == 0 or string.strcmp(dname, "CInline") == 0 or string.strcmp(dname, "CVTable") == 0 or string.strcmp(dname, "NoVTable") == 0 or string.strcmp(dname, "Object") == 0 or string.strcmp(dname, "CEnum") == 0 or string.strcmp(dname, "CUnion") == 0 or string.strcmp(dname, "REnum") == 0 or string.strcmp(dname, "CDefine") == 0 or string.strcmp(dname, "life") == 0 or string.strcmp(dname, "willFree") == 0: - continue - has_custom_decor = 1 - decor_handler = dname - break - - # SHA1 namespace: t.CExtern/t.State/t.CExport use bare names (bare maps to C library symbols; a sha1 prefix - # would cause undefined reference). main also gets no prefix (program entry point). - if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: mangled_name: str = fd.name - else: mangled_name: str = self.Gen.FuncGenBase._mangle_func_name(trans, fd.name, 0) - - # Record value-reference (&T / &temp T) parameter modes for call-site auto-address. - self._register_func_auto_addr_modes(trans, mangled_name, fd) - - # Register the CExport function in the global table (for cross-module calls). t.CExport definitions use bare - # names (@strlen); cross-module calls consult the table to use the bare name instead of @{sha1}.func. - if (has_export != 0 or has_state != 0) and trans.ModuleSha1 is not None: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name) - - if is_extern_decl != 0: - # Extern declaration function: emit a declare (declare only, no definition). First check whether - # forward_declare_functions already created it to avoid a duplicate declare. - existing_extern: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name) - if existing_extern is not None: return 0 - - func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) - if func is None: return 0 - - # Register in the function table - max_funcs_extern: int = 256 - if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, func_count, fd.name, func, max_funcs_extern, None, 0, 0, pool) == 0: trans._func_count = func_count + 1 - - # Add parameters (with type annotation support) - args_node_extern: ast.Arguments* = fd.args - if args_node_extern is not None: - ags_e: ast.Arguments* = ast.Arguments*(args_node_extern) - if ags_e.args is not None: - alist_e: list[ast.AST*]* = ags_e.args - an_e: sizet = len(alist_e) - for ai_e in range(int(an_e)): - arg_e: ast.Arg* = ast.Arg*(alist_e.get(ai_e)) - if arg_e is not None and arg_e.arg is not None: - # CVoid means empty param: skip, do not add to the function signature - if arg_e.annotation is not None and self.Gen.TypeGenResolve._is_t_attr(arg_e.annotation, "CVoid", imported_modules) != 0: continue - param_ty_e: mlirlite.LLVMType* = i32_ty - if arg_e.annotation is not None: - resolved_e: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg_e.annotation, imported_modules, from_imports, trans, 0) - if resolved_e is None: - # fail-fast: extern function param has an annotation but is unresolvable; no silent i32 - # (a wrong declare signature shows up at the ABI layer) - self.Gen.TypeGenErr.fatal_error(arg_e, f"argument '{arg_e.arg}' failed to resolve type: {self.Gen.TypeGenResolve.annotation_to_str(arg_e.annotation)}", "NameError") - resolved_e = i32_ty # unreachable (fatal_error already exited) - if resolved_e is not None: - # Bare class param -> Ptr(struct); CArray param -> element pointer ty* (C decay). This branch is - # always a C ABI extern declare: bare struct params pass by value %Struct (keep in sync with 2 others). - match resolved_e: - case mlirlite.LLVMType.Void(): - # void is not a legal param type (see the same rule above) - self.Gen.TypeGenErr.fatal_error(arg_e, f"param '{arg_e.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") - case mlirlite.LLVMType.Struct(_, _, _): - if self.Gen.StructGen.find_struct_by_type(resolved_e) is not None: - self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved_e) - param_ty_e = resolved_e - else: param_ty_e = mlirlite.Ptr(pool, resolved_e) - case mlirlite.LLVMType.Array(aelem_ty_e, _): - param_ty_e = mlirlite.Ptr(pool, aelem_ty_e) - case _: - param_ty_e = resolved_e - pname_e: str = pool.alloc(32) - if pname_e is not None: viperlib.snprintf(pname_e, 32, "%%%s", arg_e.arg) - mlirlite.add_param(pool, func, param_ty_e, pname_e) - - # Extract @c.Attribute decorator attributes and set them on the function - func_attrs_e: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) - if func_attrs_e is not None: mlirlite.function_set_attrs(func, func_attrs_e) - # B5: section string attribute goes through attribute group #N - func_section_e: str = self.Gen.FuncGenBase.extract_func_section(pool, fd.decorator_list, imported_modules) - if func_section_e is not None: mlirlite.function_set_attr_group(func, func_section_e) - - # Params added; fill the global func-table Spec - self.Gen.CallGenLeafKit.update_global_func_spec(fd.name, func) - - return 0 - - # args_node is used later for alloca creation; assign it early - args_node: ast.Arguments* = fd.args - - # Check for an existing forward declare (created by forward_declare_functions) - func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name) - if func is not None and mlirlite.function_is_declared(func) != 0: - # Reuse the forward declare: clear IsDeclared and turn it into a define - func.IsDeclared = 0 - # Set decorator attributes (not set when the forward declare was created) - func_attrs_reuse: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) - if func_attrs_reuse is not None: mlirlite.function_set_attrs(func, func_attrs_reuse) - # B5: section string attribute goes through attribute group #N - func_section_reuse: str = self.Gen.FuncGenBase.extract_func_section(pool, fd.decorator_list, imported_modules) - if func_section_reuse is not None: mlirlite.function_set_attr_group(func, func_section_reuse) - else: - # Create a new LLVM function (using the SHA1-mangled name) - func = mlirlite.create_function(pool, mod, mangled_name, ret_ty) - if func is None: - VLogger.error(f"create_function {fd.name} failed", "FUNC") - return 0 - - # Extract @c.Attribute decorator attributes and set them on the function - func_attrs: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) - if func_attrs is not None: mlirlite.function_set_attrs(func, func_attrs) - # B5: section string attribute goes through attribute group #N (LLVM rejects inline section="...") - func_section: str = self.Gen.FuncGenBase.extract_func_section(pool, fd.decorator_list, imported_modules) - if func_section is not None: mlirlite.function_set_attr_group(func, func_section) - - # Extract default-argument info - tfd_defaults: t.CPtr = None - tfd_default_count: int = 0 - tfd_param_count: int = 0 - if args_node is not None: - tfd_ags: ast.Arguments* = ast.Arguments*(args_node) - if tfd_ags.args is not None: - tfd_alist: list[ast.AST*]* = tfd_ags.args - tfd_param_count = int(len(tfd_alist)) - if tfd_ags.defaults is not None: - tfd_dlist: list[ast.AST*]* = tfd_ags.defaults - tfd_defaults = tfd_dlist - tfd_default_count = int(len(tfd_dlist)) - - # Register in the function table - max_funcs: int = 1024 - if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, func_count, fd.name, func, max_funcs, tfd_defaults, tfd_default_count, tfd_param_count, trans.Pool) == 0: - trans._func_count = func_count + 1 - - # Add parameters (with type annotation support) - if args_node is not None: - ags: ast.Arguments* = ast.Arguments*(args_node) - if ags.args is not None: - alist: list[ast.AST*]* = ags.args - an: sizet = len(alist) - for ai in range(int(an)): - arg: ast.Arg* = ast.Arg*(alist.get(ai)) - if arg is not None and arg.arg is not None: - # CVoid means empty param: skip, do not add to the function signature - if arg.annotation is not None and self.Gen.TypeGenResolve._is_t_attr(arg.annotation, "CVoid", imported_modules) != 0: continue - param_ty: mlirlite.LLVMType* = i32_ty - if arg.annotation is not None: - resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, imported_modules, from_imports, trans, 0) - if resolved is None: - # [S4 fail-fast] Annotation present but unresolvable: no silent i32 - # (wrong param type -> ABI misalignment / wrong field access) - self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' type annotation failed to resolve", "NameError") - return 0 - # Bare class param -> Ptr(struct); CArray param -> element pointer ty* (C array decay). Passing - # Ptr(array) would make out[i]'s GEP stride by the whole array (garbage summary root cause). - match resolved: - case mlirlite.LLVMType.Void(): - # void is not a legal param type (see the same rule above) - self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") - return 0 - case mlirlite.LLVMType.Struct(_, _, _): - param_ty = mlirlite.Ptr(pool, resolved) - case mlirlite.LLVMType.Array(aelem_ty, _): - param_ty = mlirlite.Ptr(pool, aelem_ty) - case _: - param_ty = resolved - pname: str = pool.alloc(32) - if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg) - mlirlite.add_param(pool, func, param_ty, pname) - - # *args variadic: set IsVarArg=1 to emit the ... suffix - if ags.vararg is not None: func.IsVarArg = 1 - - # try-except: a body containing raise gets extra __eh_msg_out__ (i8**) / __eh_code_out__ (i32*) output params - # (TPC HandlesFunctions.py L706-711; except main). Must be outside the args_node block so no-arg functions are covered. - if string.strcmp(fd.name, "main") != 0: - if self.Gen.FuncGenBase._body_has_raise(fd) != 0: - i8_ty_eh: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh) - i8_ptr_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh) - i32_ty_eh: mlirlite.LLVMType* = mlirlite.Int32(pool) - i32_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh) - if i8_ptr_ptr_ty_eh is not None and i32_ptr_ty_eh is not None: - mlirlite.add_param(pool, func, i8_ptr_ptr_ty_eh, "%__eh_msg_out__") - mlirlite.add_param(pool, func, i32_ptr_ty_eh, "%__eh_code_out__") - - # Params added; fill the global func-table Spec - self.Gen.CallGenLeafKit.update_global_func_spec(fd.name, func) - - # Create the entry block - entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") - if entry_blk is None: return 0 - - # Create the function-specific builder - func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) - if func_builder is None: return 0 - mlirlite.position_at_end(func_builder, entry_blk) - - # --traceback: attach a DISubprogram to this define and set every instruction in the body to the entry DILocation - # (refined per statement); no-op when DI is disabled. - dbg_pushed: int = DebugInfoGen.PushFunction(pool, func_builder, func, fd.lineno, fd.col_offset + 1) - - # @t.TLS one-time-init guard (aligned with TPC HandlesFunctions.py): the first call runs the body and sets - # __tls_init_done_{name}=1; later calls return the default. Without it every call re-runs TlsAlloc and loses state. - if has_tls != 0: - tls_flag_name: str = pool.alloc(128) - if tls_flag_name is not None: - viperlib.snprintf(tls_flag_name, 128, "__tls_init_done_%s", fd.name) - tls_flag_gv: mlirlite.GlobalVariable* = mlirlite.create_global_int(pool, mod, tls_flag_name, 1, 0) - if tls_flag_gv is not None: - i1_ty_t: mlirlite.LLVMType* = mlirlite.Int1(pool) - if i1_ty_t is not None: - # GlobalVariable is not a Value subclass (different field layout), so build_load/store reading - # ptr.Ty/ptr.Name as a Value misaligns. As in create_global_string, build a Value view (Ty=Ptr(i1), Name="@name"). - tls_gv_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_t) - tls_ref_name: str = pool.alloc(64) - if tls_ref_name is not None: - viperlib.snprintf(tls_ref_name, 64, "@%s", tls_flag_name) - tls_flag_ref: mlirlite.Value* = mlirlite.SSAValue(pool, tls_gv_ptr_ty, tls_ref_name) - if tls_flag_ref is not None: - tls_flag_ld: mlirlite.Value* = mlirlite.build_load(func_builder, i1_ty_t, tls_flag_ref) - # Unique block name (create_block does not auto-number; same-label conflict within a function) - lc_t: int = trans._label_counter - trans._label_counter = lc_t + 1 - tls_bb_buf: str = pool.alloc(64) - if tls_bb_buf is not None: - viperlib.snprintf(tls_bb_buf, 64, "tls.skip.%d", lc_t) - tls_skip_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, tls_bb_buf) - viperlib.snprintf(tls_bb_buf, 64, "tls.cont.%d", lc_t) - tls_cont_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, tls_bb_buf) - if tls_skip_bb is not None and tls_cont_bb is not None: - mlirlite.build_cond_br(func_builder, tls_flag_ld, tls_skip_bb, tls_cont_bb) - # skip block: return a default value matching the return type - mlirlite.position_at_end(func_builder, tls_skip_bb) - match ret_ty: - case mlirlite.LLVMType.Void(): - mlirlite.build_ret_void(func_builder) - case mlirlite.LLVMType.Int(_): - tlz0: mlirlite.Value* = mlirlite.ConstInt(pool, ret_ty, 0, "0") - if tlz0 is not None: mlirlite.build_ret(func_builder, tlz0) - case mlirlite.LLVMType.Float(_): - tlf0: mlirlite.Value* = mlirlite.ConstFloat(pool, ret_ty, 0.0) - if tlf0 is not None: mlirlite.build_ret(func_builder, tlf0) - case _: - tln0: mlirlite.Value* = mlirlite.ConstNull(pool, ret_ty, "null") - if tln0 is not None: mlirlite.build_ret(func_builder, tln0) - # cont block: set the flag=1 and continue the body - mlirlite.position_at_end(func_builder, tls_cont_bb) - tlone: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_t, 1, "1") - if tlone is not None: mlirlite.build_store(func_builder, tlone, tls_flag_ref) - - # Enter the function scope (nested symbol table) - self.Gen.VarGen.enter_scope(trans.SymTab, FuncGenMixin.SCOPE_FUNCTION) - - # Generic function templates already returned early (see the top of this function); here fd.type_params is always - # None (specialization clones clear it). The generic context is set by _specialize_generic_function before the call. - - # arg()/va_list scope reset: save the ENCLOSING function's va_list state and start this body with - # none of it. A non-variadic function must not inherit the previous function's buffer: that Value - # belongs to another function while the printer emits it by NAME, so the name re-binds to - # whatever local happens to carry it here (scene: viperlib vsnprintf emitted `va_arg ptr %9` - # where %9 is `count_mode`'s i32 alloca -> va_arg loaded a garbage va_list pointer -> - # access-violation inside vsnprintf). The *args setup below re-sets both for a variadic callee, - # and old_va_list_ptr/old_vararg_name are restored after the body so that nested functions still - # see the enclosing variadic function's buffer. - old_va_list_ptr: mlirlite.Value* = trans._va_list_ptr - old_vararg_name: str = trans._vararg_name - trans._va_list_ptr = None - trans._vararg_name = None - - # Create allocas for the parameters - if args_node is not None: - ags2: ast.Arguments* = ast.Arguments*(args_node) - if ags2.args is not None: - alist2: list[ast.AST*]* = ags2.args - an2: sizet = len(alist2) - for ai2 in range(int(an2)): - arg2: ast.Arg* = ast.Arg*(alist2.get(ai2)) - if arg2 is not None and arg2.arg is not None: - # CVoid means empty param: skip, do not create an alloca - if arg2.annotation is not None: - if self.Gen.TypeGenResolve._is_t_attr(arg2.annotation, "CVoid", imported_modules) != 0: continue - param_ty2: mlirlite.LLVMType* = i32_ty - if arg2.annotation is not None: - resolved2: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg2.annotation, imported_modules, from_imports, trans, 0) - if resolved2 is None: - # fail-fast: param has an annotation but is unresolvable; no silent i32 - self.Gen.TypeGenErr.fatal_error(arg2, f"argument '{arg2.arg}' failed to resolve type: {self.Gen.TypeGenResolve.annotation_to_str(arg2.annotation)}", "NameError") - resolved2 = i32_ty # unreachable (fatal_error already exited) - if resolved2 is not None: - # Consistent with add_param: bare class param -> Ptr(struct); CArray param -> element pointer ty* - # (array decay; the alloca type must match the param or storing ty* into [N x ty]* mismatches). - match resolved2: - case mlirlite.LLVMType.Void(): - # void is not a legal param type (see the same rule above) - self.Gen.TypeGenErr.fatal_error(arg2, f"param '{arg2.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") - return 0 - case mlirlite.LLVMType.Struct(_, _, _): - param_ty2 = mlirlite.Ptr(pool, resolved2) - case mlirlite.LLVMType.Array(aelem_ty2, _): - param_ty2 = mlirlite.Ptr(pool, aelem_ty2) - case _: - param_ty2 = resolved2 - alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, param_ty2) - if alloca is not None: - self.Gen.VarGen.define_var(trans.SymTab, arg2.arg, alloca) - # Store the original annotation's class name (method-call detection falls back from Ptr(i8) to the class). - # Pass pool: a generic Subscript returns a mangled name (aligned with the HandlesAnnAssign fix). - cls_nm: str = None - if arg2.annotation is not None: cls_nm = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg2.annotation, imported_modules, pool, trans._from_imports) - if cls_nm is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg2.arg, cls_nm) - # t.CVolatile param: marked at declaration time so loads/stores of this name carry volatile - # (a volatile param object's read-modify-write must not be hoisted/merged, e.g. a spin-wait arg). - if arg2.annotation is not None: - # const param: reuse TPC's historical param criterion (only whether the annotation contains - # t.CConst, regardless of pointer depth); TPC HandlesFunctions IsParam=True uses the same condition. - if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "CConst", 0) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, arg2.arg) - if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, arg2.arg) - # rawptr[t.CVolatile | T] param (volatile pointee): dereferences in the body - # (p[i]/p.field/c.Deref(p)) carry volatile. - if self.Gen.TypeGenErr.has_pointee_volatile_marker(arg2.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, arg2.arg) - pname2: str = pool.alloc(32) - if pname2 is not None: viperlib.snprintf(pname2, 32, "%%%s", arg2.arg) - param_val: mlirlite.Value* = mlirlite.SSAValue(pool, param_ty2, pname2) - vol_p2: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, arg2.arg)) - mlirlite.build_store(func_builder, param_val, alloca, vol_p2) - - # *args variadic: allocate a 24-byte va_list buffer, bitcast to i8*, call @llvm.va_start, register in the symbol table. - # Must be inside the param alloca section but outside the EH block, or args is unregistered (NameError: 'args'). - if ags2.vararg is not None: - vararg_node: ast.Arg* = ast.Arg*(ags2.vararg) - if vararg_node is not None and vararg_node.arg is not None: - vararg_name_str: str = vararg_node.arg - # va_list buffer is 24 bytes (Win64 __va_list_tag = {i32,i32,i8*,i8*}) with 16-byte alignment - # (aligned with TPC HandlesFunctions.py:1739-1741). - i8_ty_va: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty_va: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_va) - va_buf_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty_va, 24) - va_list_alloca: mlirlite.Value* = mlirlite.build_alloca_aligned(func_builder, va_buf_ty, 16) - if va_list_alloca is not None: - # i8* address (pointing at the 24-byte va_list buffer) - the value semantics of the vararg name - va_list_addr: mlirlite.Value* = mlirlite.build_bitcast(func_builder, va_list_alloca, i8_ptr_ty_va) - if va_list_addr is not None: - # Register in the symbol table (translate_name_value special-cases vararg names to return the address) - self.Gen.VarGen.define_var(trans.SymTab, vararg_name_str, va_list_alloca) - # Call @llvm.va_start(i8* %va_list_addr) - void_ty_va: mlirlite.LLVMType* = mlirlite.Void(pool) - mlirlite.build_call(func_builder, "llvm.va_start", va_list_addr, 1, void_ty_va, 0) - # Save on trans for the arg(type) sugar and vararg-name translation - trans._va_list_ptr = va_list_addr - trans._vararg_name = vararg_name_str - - # try-except: a new function scope starts with zero EH state. Save the enclosing function's EH output params - # before and restore them after; otherwise nested functions corrupt the EH state chain. - old_eh_msg_out: mlirlite.Value* = trans._eh_msg_out_param - old_eh_code_out: mlirlite.Value* = trans._eh_code_out_param - old_cur_func_has_eh: int = trans._cur_func_has_eh - trans._eh_msg_out_param = None - trans._eh_code_out_param = None - trans._cur_func_has_eh = 0 - - # try-except: save this function's EH output params (__eh_msg_out__/__eh_code_out__) for translate_raise / - # UnhandledBB to write and propagate. Functions without EH params are set to None. - if string.strcmp(fd.name, "main") != 0 and self.Gen.FuncGenBase._body_has_raise(fd) != 0: - i8_ty_eh2: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh2) - i8_ptr_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh2) - i32_ty_eh2: mlirlite.LLVMType* = mlirlite.Int32(pool) - i32_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh2) - if i8_ptr_ptr_ty_eh2 is not None and i32_ptr_ty_eh2 is not None: - msg_out_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ptr_ty_eh2, "%__eh_msg_out__") - code_out_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i32_ptr_ty_eh2, "%__eh_code_out__") - trans._eh_msg_out_param = msg_out_ref - trans._eh_code_out_param = code_out_ref - trans._cur_func_has_eh = 1 - - # Save module-level scope state (non-variable-table related only) - old_func: mlirlite.Function* = trans._cur_func - old_builder: mlirlite.IRBuilder* = trans._cur_builder - old_global_count: int = trans._global_name_count - old_nonlocal_count: int = trans._nonlocal_name_count - old_env_count: int = trans._closure_env_count - # old_va_list_ptr / old_vararg_name were saved above (before the *args setup) so that this body - # could start from a clean va_list state; the restore below hands the enclosing value back. - # Return-annotation context ([C①], one source of type-argument fallback for bare generic construction): - # record this function's return annotation before entering the body and restore the outer value on exit. - old_ret_annot: ast.AST* = trans.CurFuncRetAnnot - # break/continue targets and the match-case flag must be cleared for the new function scope (see assignments below), - # otherwise break/continue in a nested function inherits the outer (even cross-function) br targets. - old_break_bb: mlirlite.BasicBlock* = trans._break_bb - old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb - old_insw: int = trans._in_switch_case - - trans._cur_func = func - trans._cur_builder = func_builder - trans.CurFuncRetAnnot = fd.returns - # P2 willFree 失效表:**函数作用域复位**(表按栈槽地址记录,槽地址会被后续函数复用) - self.Gen.VarGen.freed_clear() - trans._break_bb = None - trans._continue_bb = None - trans._in_switch_case = 0 - # Clear the global/nonlocal name sets (new function scope) - GenApi.clear_scope_names(trans) - # Reset the in-chain nonlocal heap prebuild flag (built once per function; multiple nested function definitions - # share one heap, preventing repeated malloc + double VarEntry redirection). - trans._nl_chain_done = 0 - - # Pre-scan the body: 1) Global/Nonlocal declarations first (Python semantics: they apply to the whole function - # scope regardless of position, so register before any assignment); 2) AnnAssign to create local allocas early. - body: list[ast.AST*]* = fd.children - if body is not None: - bn: sizet = len(body) - for bi in range(int(bn)): - stmt: ast.AST* = body.get(bi) - if stmt is not None: self.Gen.StmtGenDecl.pre_scan_scope_decls(trans, stmt) - for bi in range(int(bn)): - stmt: ast.AST* = body.get(bi) - if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt) - - # M3 owned slife(§4.2 函数级 scope guard):进入函数体前把登记区间起点抬到当前栈顶。 - # 表是函数间共享的一段栈式区间,嵌套函数各自独立;退出时由 _SlifeRestore 弹回外层起点。 - # 放在这里(而非更早)是因为上方存在多条提前返回分支(泛型模板/async/extern), - # 只有从这里开始才保证"有 reset 必有 restore"。 - old_slife_base: int = self.Gen.VarGen._SlifeReset(pool) - - # Translate the function body - if body is not None: - bn2: sizet = len(body) - for bi2 in range(int(bn2)): - stmt2: ast.AST* = body.get(bi2) - if stmt2 is not None: - # Before a nested function definition: pre-scan its in-chain nonlocals and build the heap - # (for cases where different levels capture different variables; see _prepare_chain_nonlocals). - if stmt2.kind() == ast.ASTKind.FunctionDef: - self.Gen.FuncGen._prepare_chain_nonlocals(trans, stmt2, pool, func_builder, mod, mlirlite.Int8(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool))) - self.Gen.StmtGen.translate_stmt(trans, stmt2) - - # If the last body statement is not Return, add an implicit ret - last_is_return: int = 0 - # bn3 is initialized early so the fail-fast branch below does not reference an undefined bn3 when body is None. - bn3: sizet = 0 - if body is not None: - bn3 = len(body) - if bn3 > 0: - last_stmt: ast.AST* = body.get(bn3 - 1) - if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1 - if last_is_return == 0: - if mlirlite.builder_cur_block_is_terminated(func_builder) == 0: - # The compiler never invents a return value: falling off the end without an explicit return is fail-fast. - # Legal endings: void -> ret void; unreachable merge (all branches terminated) -> unreachable; unannotated -> zero return. - last_is_ctrl: int = 0 - if bn3 > 0: - last_stmt3: ast.AST* = body.get(bn3 - 1) - if last_stmt3 is not None: - k3: int = last_stmt3.kind() - if k3 == ast.ASTKind.If or k3 == ast.ASTKind.While or k3 == ast.ASTKind.For or k3 == ast.ASTKind.Try or k3 == ast.ASTKind.With or k3 == ast.ASTKind.Match: last_is_ctrl = 1 - _is_void: int = 0 - if ret_ty is not None: - match ret_ty: - case mlirlite.LLVMType.Void(): - _is_void = 1 - case _: - pass - # Annotation-level "no return value" criterion (a second check alongside the ret_ty type check): if the - # REnum class-pattern match degrades, _is_void stays 0 and a void function is misreported as missing a return. - if _is_void == 0 and fd.returns is not None: - if self.Gen.TypeGenResolve._is_cvoid_marker(fd.returns, imported_modules) != 0: _is_void = 1 - elif fd.returns.kind() == ast.ASTKind.Constant: - ret_none_cn: ast.Constant* = (ast.Constant*)(fd.returns) - if ret_none_cn.const_kind == ast.CONST_NONE: _is_void = 1 - if _is_void != 0: - # M3:隐式收尾也是一个函数出口,先释放本函数登记的 slife 再返回 - self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans) - mlirlite.build_ret_void(func_builder) - elif last_is_ctrl != 0 and trans._ctrl_merge_reachable == 0: - # A non-void function whose last statement is control flow with a structurally unreachable merge (all - # branches return, no br to merge) -> LLVM unreachable. Reachability uses only `_ctrl_merge_reachable`. - mlirlite.build_unreachable(func_builder) - elif fd.returns is None: - # No return annotation -> a no-value procedure function, return a zero default - if ret_ty is not None: - zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty) - if zero_val is not None: - # M3:隐式收尾出口,先释放 slife 再返回 - self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans) - mlirlite.build_ret(func_builder, zero_val) - else: self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") - else: self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") - else: - # fail-fast: has a -> annotation but the flow falls off the end (pointing at the function definition - # node; fatal_error prints a traceback-style error). - if ret_ty is not None: - # mlirlite has no type_get_name API; serialize the type with the real interface - # TypePrint(buf, size, ty, pool). - rt_name: str = pool.alloc_tagged(4096, memhub.DETSITE_FUNCGENDEF_792) - if rt_name is not None: - mlirlite.TypePrint(rt_name, 4096, ret_ty, pool) - if rt_name[0] == '\0': rt_name = "?" - err_fn: str = pool.alloc(256) - if err_fn is not None: - viperlib.snprintf(err_fn, 256, "function '%s' flow falls off the end of the function without an explicit return (return type %s), please add an explicit return (e.g. return 0 / return null)", fd.name, rt_name) - self.Gen.TypeGenErr.fatal_error(node, err_fn, "TypeError") - self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") - else: self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") - - # Restore module-level scope (exit the function scope) - self.Gen.VarGen.exit_scope(trans.SymTab) - # M3 owned slife:丢弃本函数的登记区间、弹回外层起点(释放已在各出口用 _SlifeEmitFrees 完成) - self.Gen.VarGen._SlifeRestore(old_slife_base) - trans._cur_func = old_func - trans._cur_builder = old_builder - trans._break_bb = old_break_bb - trans._continue_bb = old_continue_bb - trans._in_switch_case = old_insw - trans._global_name_count = old_global_count - trans._nonlocal_name_count = old_nonlocal_count - trans._closure_env_count = old_env_count - trans._va_list_ptr = old_va_list_ptr - trans._vararg_name = old_vararg_name - # Hand back the outer function's return annotation (see old_ret_annot above) - trans.CurFuncRetAnnot = old_ret_annot - # Restore the outer (caller) function's EH output params: this function's raise propagation uses its own params, - # so hand them back on exit or nested/chained calls leave stale EH state and write to the wrong out pointer. - trans._eh_msg_out_param = old_eh_msg_out - trans._eh_code_out_param = old_eh_code_out - trans._cur_func_has_eh = old_cur_func_has_eh - - # Custom decorator: register and generate a wrapper (aligned with TPC DecoratorPass.run). Module-level functions - # only; wrapper name is __decor_wrap_{bare name}; call sites are rewritten via lookup_decor_wrapper. - if has_custom_decor != 0: - wb_name: str = pool.alloc(128) - if wb_name is not None: - viperlib.snprintf(wb_name, 128, "__decor_wrap_%s", fd.name) - self.Gen.ClassGenBase.register_decorator(pool, fd.name, wb_name, decor_arg0) - self._gen_decor_wrapper(trans, pool, mod, func, fd.name, wb_name, decor_handler, decor_arg0) - - if dbg_pushed != 0: DebugInfoGen.PopFunction(func_builder) - - return 0 - - # _RegisterStaticFunc - register a static function (no cross-module use). Two equivalent forms (normalized by the - # parser): annotation form or decorator form; on hit, register (name, defining module SHA1) in the static symbol table. - def _RegisterStaticFunc(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: - """Register a static function when its annotation or @t.CStatic matches; 1=registered, 0=miss.""" - if trans is None or fd is None or fd.name is None: return 0 - if trans.ModuleSha1 is None: return 0 - has: int = 0 - if fd.returns is not None: has = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CStatic", 0) - if has == 0 and fd.decorator_list is not None: - if self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CStatic") == 1: has = 1 - if has == 0: return 0 - return self.Gen.CallGenBase.RegisterStaticSymbol(trans.ModuleSha1, fd.name) - - # _gen_decor_wrapper - generate a decorator wrapper (aligned with TPC DecoratorPass.py _generate_single_wrapper v4). - # Blocks: entry -> recursive_call/decorated_entry -> loop.header/loop.body -> post; handler signature i32 decor(i8*, i8*, i32, i8*, i8*, i8*). - def _gen_decor_wrapper(self, trans: HT.Translator*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, func: mlirlite.Function*, func_name: str, wrapper_name: str, handler_name: str, decor_arg0: int64) -> mlirlite.Function*: - """Generate a decorator wrapper function; returns the wrapper or None.""" - if trans is None or pool is None or mod is None or func is None: return None - if func_name is None or wrapper_name is None or handler_name is None: return None - ret_ty: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(func) - if ret_ty is None: return None - orig_call_name: str = mlirlite.function_get_name(func) - if orig_call_name is None: return None - - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - - # ---- Collect the original function's param types (by position, up to 16) ---- - n_params: int = 0 - param_tys: sizet* = pool.alloc(16 * 8) - if param_tys is None: return None - string.memset(param_tys, 0, 16 * 8) - pp: mlirlite.Param* = mlirlite.function_get_param_head(func) - while pp is not None: - pty: mlirlite.LLVMType* = mlirlite.param_get_ty(pp) - if pty is not None and n_params < 16: - param_tys[n_params] = sizet(pty) - n_params += 1 - pp = mlirlite.param_get_next(pp) - - # ---- Create the wrapper function and add params (named %a0..%aN) ---- - wrapper: mlirlite.Function* = mlirlite.create_function(pool, mod, wrapper_name, ret_ty) - if wrapper is None: return None - pi: int = 0 - for pi in range(int(n_params)): - pname_w: str = pool.alloc(16) - if pname_w is not None: - viperlib.snprintf(pname_w, 16, "%%a%d", pi) - mlirlite.add_param(pool, wrapper, (mlirlite.LLVMType*)(param_tys[pi]), pname_w) - - # ---- Create blocks ---- - entry_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "entry") - loop_h_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "loop.header") - loop_b_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "loop.body") - post_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "post") - if entry_bb is None or loop_h_bb is None or loop_b_bb is None or post_bb is None: return None - - # Recursion guard flag (outermost): @__decor_rec_{wrapper} = global i8 0 - rec_flag_ref: mlirlite.Value* = None - rec_flag_name: str = pool.alloc(128) - if rec_flag_name is not None: - viperlib.snprintf(rec_flag_name, 128, "__decor_rec_%s", wrapper_name) - mlirlite.create_global_int(pool, mod, rec_flag_name, 8, 0) - rec_ref_nm: str = pool.alloc(96) - if rec_ref_nm is not None: - viperlib.snprintf(rec_ref_nm, 96, "@%s", rec_flag_name) - rec_flag_ref = mlirlite.SSAValue(pool, mlirlite.Ptr(pool, i8_ty), rec_ref_nm) - - wb: mlirlite.IRBuilder* = mlirlite.new_builder(pool, wrapper) - if wb is None: return None - - # Classify the return type - ret_is_void: int = 0 - match ret_ty: - case mlirlite.LLVMType.Void(): - ret_is_void = 1 - case _: - pass - - # ---- entry: recursion guard check (outermost) ---- - mlirlite.position_at_end(wb, entry_bb) - loop_pred_bb: mlirlite.BasicBlock* = entry_bb - if rec_flag_ref is not None: - rec_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "recursive_call") - dec_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "decorated_entry") - if rec_bb is None or dec_bb is None: return None - rec_val: mlirlite.Value* = mlirlite.build_load(wb, i8_ty, rec_flag_ref) - rec_zero: mlirlite.Value* = mlirlite.ConstInt(pool, i8_ty, 0, "0") - rec_is: mlirlite.Value* = mlirlite.build_icmp(wb, mlirlite.ICMP_NE, rec_val, rec_zero) - mlirlite.build_cond_br(wb, rec_is, rec_bb, dec_bb) - # recursive_call: call the original function directly (skipping all decoration logic) - mlirlite.position_at_end(wb, rec_bb) - rec_args: mlirlite.Value* = None - ri: int = 0 - for ri in range(int(n_params)): - an_r: str = pool.alloc(16) - if an_r is None: continue - viperlib.snprintf(an_r, 16, "%%a%d", ri) - av_r: mlirlite.Value* = mlirlite.SSAValue(pool, (mlirlite.LLVMType*)(param_tys[ri]), an_r) - if av_r is not None: - mlirlite.value_set_next(av_r, None) - if rec_args is None: rec_args = av_r - else: - # Append to the tail - rt: mlirlite.Value* = rec_args - while rt.Next is not None: rt = rt.Next - mlirlite.value_set_next(rt, av_r) - if ret_is_void != 0: - mlirlite.build_call(wb, orig_call_name, rec_args, n_params, ret_ty, 0) - mlirlite.build_ret_void(wb) - else: - rc_res: mlirlite.Value* = mlirlite.build_call(wb, orig_call_name, rec_args, n_params, ret_ty, 0) - if rc_res is not None: mlirlite.build_ret(wb, rc_res) - else: mlirlite.build_ret_void(wb) - # decorated_entry: set the recursion flag and continue the decoration path - mlirlite.position_at_end(wb, dec_bb) - rec_one: mlirlite.Value* = mlirlite.ConstInt(pool, i8_ty, 1, "1") - if rec_one is not None: mlirlite.build_store(wb, rec_one, rec_flag_ref) - loop_pred_bb = dec_bb - - # ---- Common decoration path (entry or decorated_entry) ---- - # 1. ctx = alloca [32 x i8]; ctx_ptr = bitcast i8* - ctx_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty, 32) - ctx_alloca: mlirlite.Value* = mlirlite.build_alloca(wb, ctx_ty) - ctx_ptr: mlirlite.Value* = mlirlite.build_bitcast(wb, ctx_alloca, i8_ptr_ty) - - # 2. Pack function params into a struct {param_types} (the decorator can read/write args_ptr) - args_alloca: mlirlite.Value* = None - args_ptr: mlirlite.Value* = None - if n_params > 0: - fields_head: mlirlite.ParamNode* = None - fk: int = 0 - for fk in range(int(n_params - 1), -1, -1): - pn_n: mlirlite.ParamNode* = mlirlite.new_param_node(pool, (mlirlite.LLVMType*)(param_tys[fk])) - if pn_n is not None: - pn_n.Next = fields_head - fields_head = pn_n - args_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, fields_head, n_params, None) - if args_ty is not None: - args_alloca = mlirlite.build_alloca(wb, args_ty) - fi2: int = 0 - for fi2 in range(int(n_params)): - fld_ty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(param_tys[fi2]) - fld_ptr: mlirlite.Value* = mlirlite.build_gep_struct(wb, args_ty, fld_ty, args_alloca, fi2) - if fld_ptr is not None: - fld_nm: str = pool.alloc(16) - if fld_nm is not None: - viperlib.snprintf(fld_nm, 16, "%%a%d", fi2) - fld_v: mlirlite.Value* = mlirlite.SSAValue(pool, fld_ty, fld_nm) - if fld_v is not None: mlirlite.build_store(wb, fld_v, fld_ptr) - args_ptr = mlirlite.build_bitcast(wb, args_alloca, i8_ptr_ty) - else: args_ptr = mlirlite.ConstNull(pool, i8_ptr_ty, "null") - - # 3. Return value storage (zero-initialized; a safe default is returned when skipping the original) - ret_alloca: mlirlite.Value* = None - if ret_is_void == 0: - ret_alloca = mlirlite.build_alloca(wb, ret_ty) - ret_zero: mlirlite.Value* = None - match ret_ty: - case mlirlite.LLVMType.Int(_): - ret_zero = mlirlite.ConstInt(pool, ret_ty, 0, "0") - case mlirlite.LLVMType.Float(_): - ret_zero = mlirlite.ConstFloat(pool, ret_ty, 0.0) - case _: - ret_zero = mlirlite.ConstNull(pool, ret_ty, "null") - if ret_zero is not None and ret_alloca is not None: mlirlite.build_store(wb, ret_zero, ret_alloca) - - # 4. Function-name string constant - fn_str: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(wb, pool, mod, func_name, trans) - if fn_str is None: fn_str = mlirlite.ConstNull(pool, i8_ptr_ty, "null") - - # 5. handler declare (declared when untranslated; fixed signature i32 (i8*,i8*,i32,i8*,i8*,i8*)) - if self.Gen.CallGenBase.find_func_in_module(mod, handler_name) is None: - handler_d: mlirlite.Function* = mlirlite.create_declare(pool, mod, handler_name, i32_ty) - if handler_d is not None: - mlirlite.add_param(pool, handler_d, i8_ptr_ty, "ctx") - mlirlite.add_param(pool, handler_d, i8_ptr_ty, "func_name") - mlirlite.add_param(pool, handler_d, i32_ty, "phase") - mlirlite.add_param(pool, handler_d, i8_ptr_ty, "args_ptr") - mlirlite.add_param(pool, handler_d, i8_ptr_ty, "ret_ptr") - mlirlite.add_param(pool, handler_d, i8_ptr_ty, "decor_args_ptr") - - # 6. Pre-phase: handler returns the call count n_calls - ph0: mlirlite.Value* = mlirlite.ConstInt(pool, i32_ty, 0, "0") - null_ret: mlirlite.Value* = mlirlite.ConstNull(pool, i8_ptr_ty, "null") - null_dec: mlirlite.Value* = mlirlite.ConstNull(pool, i8_ptr_ty, "null") - # Arg linked list (6 separate values): ctx_ptr -> fn_str -> 0 -> args_ptr -> null -> null. - # Note: no self-link (null.Next = null would make build_call loop forever). - if ctx_ptr is not None: mlirlite.value_set_next(ctx_ptr, fn_str) - if fn_str is not None: mlirlite.value_set_next(fn_str, ph0) - if ph0 is not None: mlirlite.value_set_next(ph0, args_ptr) - if args_ptr is not None: mlirlite.value_set_next(args_ptr, null_ret) - if null_ret is not None: mlirlite.value_set_next(null_ret, null_dec) - n_calls: mlirlite.Value* = mlirlite.build_call(wb, handler_name, ctx_ptr, 6, i32_ty, 0) - - # 7. Counter alloca (TPV uses an alloca counter instead of phi) - cnt_alloca: mlirlite.Value* = mlirlite.build_alloca(wb, i32_ty) - cnt_zero: mlirlite.Value* = mlirlite.ConstInt(pool, i32_ty, 0, "0") - if cnt_alloca is not None and cnt_zero is not None: mlirlite.build_store(wb, cnt_zero, cnt_alloca) - mlirlite.build_br(wb, loop_h_bb) - - # 8. loop.header:cur = load cnt; cur < n_calls → body / post - mlirlite.position_at_end(wb, loop_h_bb) - cur_i: mlirlite.Value* = mlirlite.build_load(wb, i32_ty, cnt_alloca) - loop_cond: mlirlite.Value* = mlirlite.build_icmp(wb, mlirlite.ICMP_SLT, cur_i, n_calls) - mlirlite.build_cond_br(wb, loop_cond, loop_b_bb, post_bb) - - # 9. loop.body: read back params (the decorator may have modified them) -> call the original -> store ret - mlirlite.position_at_end(wb, loop_b_bb) - call_args: mlirlite.Value* = None - if n_params > 0: - k: int = 0 - for k in range(int(n_params)): - fld_ty_k: mlirlite.LLVMType* = (mlirlite.LLVMType*)(param_tys[k]) - fld_pk: mlirlite.Value* = mlirlite.build_gep_struct(wb, args_ty, fld_ty_k, args_alloca, k) - if fld_pk is None: continue - fld_vk: mlirlite.Value* = mlirlite.build_load(wb, fld_ty_k, fld_pk) - if fld_vk is not None: - mlirlite.value_set_next(fld_vk, None) - if call_args is None: call_args = fld_vk - else: - ct: mlirlite.Value* = call_args - while ct.Next is not None: ct = ct.Next - mlirlite.value_set_next(ct, fld_vk) - res_v: mlirlite.Value* = mlirlite.build_call(wb, orig_call_name, call_args, n_params, ret_ty, 0) - if ret_alloca is not None and res_v is not None: mlirlite.build_store(wb, res_v, ret_alloca) - nxt_i: mlirlite.Value* = mlirlite.build_add(wb, cur_i, mlirlite.ConstInt(pool, i32_ty, 1, "1")) - if nxt_i is not None and cnt_alloca is not None: mlirlite.build_store(wb, nxt_i, cnt_alloca) - mlirlite.build_br(wb, loop_h_bb) - - # 10. post: post-phase handler -> clear the recursion flag -> return the result - mlirlite.position_at_end(wb, post_bb) - ret_ptr: mlirlite.Value* = null_ret - if ret_alloca is not None: ret_ptr = mlirlite.build_bitcast(wb, ret_alloca, i8_ptr_ty) - ph1: mlirlite.Value* = mlirlite.ConstInt(pool, i32_ty, 1, "1") - # Post-phase arg chain: ctx_ptr -> fn_str -> 1 -> args_ptr -> ret_ptr -> null - if ctx_ptr is not None: mlirlite.value_set_next(ctx_ptr, fn_str) - if fn_str is not None: mlirlite.value_set_next(fn_str, ph1) - if ph1 is not None: mlirlite.value_set_next(ph1, args_ptr) - if args_ptr is not None: mlirlite.value_set_next(args_ptr, ret_ptr) - if ret_ptr is not None: mlirlite.value_set_next(ret_ptr, null_dec) - mlirlite.build_call(wb, handler_name, ctx_ptr, 6, i32_ty, 0) - if rec_flag_ref is not None: - rec_zero2: mlirlite.Value* = mlirlite.ConstInt(pool, i8_ty, 0, "0") - if rec_zero2 is not None: mlirlite.build_store(wb, rec_zero2, rec_flag_ref) - if ret_is_void != 0: mlirlite.build_ret_void(wb) - else: - final_v: mlirlite.Value* = mlirlite.build_load(wb, ret_ty, ret_alloca) - if final_v is not None: mlirlite.build_ret(wb, final_v) - else: mlirlite.build_ret_void(wb) - + if fd is None or fd.name is None: return 0 + + # PEP 695 generic function templates are not emitted directly (same rule as forward_declare_functions): + # clones are emitted at call sites by _specialize_generic_function; here we only ensure the template is registered. + if fd.type_params is not None: + self.Gen.ClassGenGeneric._register_generic_func_template(trans.Pool, fd, trans.ModuleSha1) + return 0 + + # async def (VPC-M1 coroutine): FLAG_IS_ASYNC=1 (parser stores async def as FunctionDef with flags & 1) + # -> AsyncGenMixin state machine (create_/resume_); early-return so the plain path cannot suspend/resume. + if fd.flags is not None and (fd.flags & 1) != 0: return self.Gen.AsyncGen.translate_async_function(trans, node) + + pool: memhub.MemBuddy* = trans.Pool + mod: mlirlite.LLVMModule* = trans.Module + imported_modules: str = trans._imported_modules + from_imports: str = trans._from_imports + funcs_ptr: GenTypes.FuncEntry* = trans._funcs + func_count: int = trans._func_count + + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + + # Infer the return type: prefer the return annotation + ret_ty: mlirlite.LLVMType* = None + if fd.returns is not None: + # Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return). + self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type") + ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0) + # A lone t.CExport return annotation is an export modifier with no real type -> Void + # (CExport fell back to i32, forcing return i32: mm.vp free root cause). Mixed types keep the resolved result. + if fd.returns.kind() == ast.ASTKind.Attribute: + ce_at2: ast.Attribute* = ast.Attribute*(fd.returns) + if ce_at2.attr is not None and string.strcmp(ce_at2.attr, "CExport") == 0: + if ce_at2.value is not None and ce_at2.value.kind() == ast.ASTKind.Name: + ce_nm2: ast.Name* = ast.Name*(ce_at2.value) + if ce_nm2.id is not None and string.strcmp(ce_nm2.id, "t") == 0: ret_ty = mlirlite.Void(pool) + + # If the return annotation is purely a decorator marker (e.g. t.State, no real type), use void. + # Must be checked before infer_return_type, which would return at least i32. + if ret_ty is None and fd.returns is not None: + if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0: ret_ty = mlirlite.Void(pool) + + # @c.CReturn decorator removed: encountering it is a compile-time error (use -> tuple[...] for multiple returns) + if ret_ty is None: self.Gen.FuncGenBase.RejectCReturnDecorator(fd, fd.decorator_list) + + # If still None, infer by scanning return statements (at least i32) + if ret_ty is None: + # fail-fast: no return annotation (fd.returns is None) but the body returns a non-None value -> error, no silent + # inference. Returning a value requires an explicit -> annotation; unannotated functions allow only procedure form. + if fd.returns is None: + if self.Gen.TypeGenErr.body_has_value_return(pool, fd.children) != 0: + rtn_err: str = pool.alloc(256) + if rtn_err is not None: + viperlib.snprintf(rtn_err, 256, "function '%s' has no return type annotation but returns a non-None value, add an explicit return type annotation (e.g. -> int)", fd.name) + self.Gen.TypeGenErr.fatal_error(fd, rtn_err, "TypeError") + return 0 + self.Gen.TypeGenErr.fatal_error(fd, "function has no return type annotation but returns a non-None value", "TypeError") + return 0 + param_types_str: str = self.Gen.TypeGenErr.build_param_types_str(pool, fd.args) + ret_ty = self.Gen.TypeGenErr.infer_return_type(pool, fd.children, param_types_str) + # Detect an extern declaration (t.CExtern or t.State): t.CExtern ignores the body and emits only a declare; + # t.State = t.CExtern + t.CExport. t.CExtern/t.State/t.CExport get no SHA1 prefix and map to C library names. + is_extern_decl: int = 0 + has_export: int = 0 + has_extern: int = 0 + has_state: int = 0 + if fd.returns is not None: + has_extern = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExtern", 0) + has_state = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) + has_export = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExport", 0) + # t.CExtern/t.State ignore the body and only declare (whether the body is pass or not) + if has_extern != 0 or has_state != 0: is_extern_decl = 1 + # Decorator-form storage markers (@t.CExport/@t.CExtern/@t.State/@t.CStatic), aligned with TPC + # HandlesFunctions._extract_decorator_storage_flags (same as forward_declare_functions). Use ClassGenBase._has_decorator. + if fd.decorator_list is not None: + if has_extern == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExtern") == 1: has_extern = 1 + if has_state == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "State") == 1: has_state = 1 + if has_export == 0 and self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CExport") == 1: has_export = 1 + if (has_extern != 0 or has_state != 0) and is_extern_decl == 0: is_extern_decl = 1 + # @t.TLS one-time-init marker (aligned with TPC HandlesFunctions.py L1223-1231) + has_tls: int = 0 + if fd.decorator_list is not None: has_tls = self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "TLS") + + # Custom behavior decorator detection (@log/@skip/@repeat(3) etc.): Name/Name(args) decorators that are neither + # t storage markers nor builtins. Aligned with TPC HandlesFunctions.py L1161-1189 + DecoratorPass.py; first one wins. + has_custom_decor: int = 0 + decor_handler: str = None + decor_arg0: int64 = 0 + if fd.decorator_list is not None: + ddn: sizet = len(fd.decorator_list) + for ddi in range(int(ddn)): + deco: ast.AST* = fd.decorator_list.get(ddi) + if deco is None: continue + dk: int = deco.kind() + dname: str = None + if dk == ast.ASTKind.Name: + dnm: ast.Name* = ast.Name*(deco) + dname = dnm.id + elif dk == ast.ASTKind.Call: + dcl: ast.Call* = ast.Call*(deco) + if dcl.func is not None and dcl.func.kind() == ast.ASTKind.Name: + dnm2: ast.Name* = ast.Name*(dcl.func) + dname = dnm2.id + # @repeat(3): take the first constant int argument (aligned with TPC L1182-1184) + if dcl.args is not None and len(dcl.args) >= 1: + da0: ast.AST* = dcl.args.get(0) + if da0 is not None and da0.kind() == ast.ASTKind.Constant: + dc0: ast.Constant* = ast.Constant*(da0) + if dc0.const_kind == ast.CONST_INT: decor_arg0 = dc0.int_val + if dname is None: continue + # Skip builtin decorators + if string.strcmp(dname, "staticmethod") == 0 or string.strcmp(dname, "property") == 0 or string.strcmp(dname, "classmethod") == 0: continue + # Skip t-module storage markers (@t.CExport/@t.CExtern/@t.State/@t.TLS/@t.CStatic/@t.CInline/@t.CVTable/ + # @t.NoVTable/@t.Object/@t.CEnum/@t.CUnion/@t.REnum/@t.CDefine). Attribute-form @t.xxx is not handled here. + # 另跳过**编译期生命期标注**(P2:规范形式 @life(willFree(...)) 与兼容叠加形式 @willFree(...)): + # 它们是纯标注、没有运行时装饰器函数。漏在这里会让 VPC 生成 __decor_wrap_ wrapper, + # 去调用并不存在的 @life/@willFree,并把被装饰的 extern def(如 stdlib.free)变成 internal 声明 + # (现场:llc 报 "invalid linkage for function declaration")。 + if string.strcmp(dname, "CExport") == 0 or string.strcmp(dname, "CExtern") == 0 or string.strcmp(dname, "State") == 0 or string.strcmp(dname, "TLS") == 0 or string.strcmp(dname, "CStatic") == 0 or string.strcmp(dname, "CInline") == 0 or string.strcmp(dname, "CVTable") == 0 or string.strcmp(dname, "NoVTable") == 0 or string.strcmp(dname, "Object") == 0 or string.strcmp(dname, "CEnum") == 0 or string.strcmp(dname, "CUnion") == 0 or string.strcmp(dname, "REnum") == 0 or string.strcmp(dname, "CDefine") == 0 or string.strcmp(dname, "life") == 0 or string.strcmp(dname, "willFree") == 0: + continue + has_custom_decor = 1 + decor_handler = dname + break + + # SHA1 namespace: t.CExtern/t.State/t.CExport use bare names (bare maps to C library symbols; a sha1 prefix + # would cause undefined reference). main also gets no prefix (program entry point). + if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: mangled_name: str = fd.name + else: mangled_name: str = self.Gen.FuncGenBase._mangle_func_name(trans, fd.name, 0) + + # Record value-reference (&T / &temp T) parameter modes for call-site auto-address. + self._register_func_auto_addr_modes(trans, mangled_name, fd) + + # Register the CExport function in the global table (for cross-module calls). t.CExport definitions use bare + # names (@strlen); cross-module calls consult the table to use the bare name instead of @{sha1}.func. + if (has_export != 0 or has_state != 0) and trans.ModuleSha1 is not None: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name) + + if is_extern_decl != 0: + # Extern declaration function: emit a declare (declare only, no definition). First check whether + # forward_declare_functions already created it to avoid a duplicate declare. + existing_extern: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name) + if existing_extern is not None: return 0 + + func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty) + if func is None: return 0 + + # Register in the function table + max_funcs_extern: int = 256 + if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, func_count, fd.name, func, max_funcs_extern, None, 0, 0, pool) == 0: trans._func_count = func_count + 1 + + # Add parameters (with type annotation support) + args_node_extern: ast.Arguments* = fd.args + if args_node_extern is not None: + ags_e: ast.Arguments* = ast.Arguments*(args_node_extern) + if ags_e.args is not None: + alist_e: list[ast.AST*]* = ags_e.args + an_e: sizet = len(alist_e) + for ai_e in range(int(an_e)): + arg_e: ast.Arg* = ast.Arg*(alist_e.get(ai_e)) + if arg_e is not None and arg_e.arg is not None: + # CVoid means empty param: skip, do not add to the function signature + if arg_e.annotation is not None and self.Gen.TypeGenResolve._is_t_attr(arg_e.annotation, "CVoid", imported_modules) != 0: continue + param_ty_e: mlirlite.LLVMType* = i32_ty + if arg_e.annotation is not None: + resolved_e: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg_e.annotation, imported_modules, from_imports, trans, 0) + if resolved_e is None: + # fail-fast: extern function param has an annotation but is unresolvable; no silent i32 + # (a wrong declare signature shows up at the ABI layer) + self.Gen.TypeGenErr.fatal_error(arg_e, f"argument '{arg_e.arg}' failed to resolve type: {self.Gen.TypeGenResolve.annotation_to_str(arg_e.annotation)}", "NameError") + resolved_e = i32_ty # unreachable (fatal_error already exited) + if resolved_e is not None: + # Bare class param -> Ptr(struct); CArray param -> element pointer ty* (C decay). This branch is + # always a C ABI extern declare: bare struct params pass by value %Struct (keep in sync with 2 others). + match resolved_e: + case mlirlite.LLVMType.Void(): + # void is not a legal param type (see the same rule above) + self.Gen.TypeGenErr.fatal_error(arg_e, f"param '{arg_e.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") + case mlirlite.LLVMType.Struct(_, _, _): + if self.Gen.StructGen.find_struct_by_type(resolved_e) is not None: + self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved_e) + param_ty_e = resolved_e + else: param_ty_e = mlirlite.Ptr(pool, resolved_e) + case mlirlite.LLVMType.Array(aelem_ty_e, _): + param_ty_e = mlirlite.Ptr(pool, aelem_ty_e) + case _: + param_ty_e = resolved_e + pname_e: str = pool.alloc(32) + if pname_e is not None: viperlib.snprintf(pname_e, 32, "%%%s", arg_e.arg) + mlirlite.add_param(pool, func, param_ty_e, pname_e) + + # Extract @c.Attribute decorator attributes and set them on the function + func_attrs_e: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) + if func_attrs_e is not None: mlirlite.function_set_attrs(func, func_attrs_e) + # B5: section string attribute goes through attribute group #N + func_section_e: str = self.Gen.FuncGenBase.extract_func_section(pool, fd.decorator_list, imported_modules) + if func_section_e is not None: mlirlite.function_set_attr_group(func, func_section_e) + + # Params added; fill the global func-table Spec + self.Gen.CallGenLeafKit.update_global_func_spec(fd.name, func) + + return 0 + + # args_node is used later for alloca creation; assign it early + args_node: ast.Arguments* = fd.args + + # Check for an existing forward declare (created by forward_declare_functions) + func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name) + if func is not None and mlirlite.function_is_declared(func) != 0: + # Reuse the forward declare: clear IsDeclared and turn it into a define + func.IsDeclared = 0 + # Set decorator attributes (not set when the forward declare was created) + func_attrs_reuse: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) + if func_attrs_reuse is not None: mlirlite.function_set_attrs(func, func_attrs_reuse) + # B5: section string attribute goes through attribute group #N + func_section_reuse: str = self.Gen.FuncGenBase.extract_func_section(pool, fd.decorator_list, imported_modules) + if func_section_reuse is not None: mlirlite.function_set_attr_group(func, func_section_reuse) + else: + # Create a new LLVM function (using the SHA1-mangled name) + func = mlirlite.create_function(pool, mod, mangled_name, ret_ty) + if func is None: + VLogger.error(f"create_function {fd.name} failed", "FUNC") + return 0 + + # Extract @c.Attribute decorator attributes and set them on the function + func_attrs: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) + if func_attrs is not None: mlirlite.function_set_attrs(func, func_attrs) + # B5: section string attribute goes through attribute group #N (LLVM rejects inline section="...") + func_section: str = self.Gen.FuncGenBase.extract_func_section(pool, fd.decorator_list, imported_modules) + if func_section is not None: mlirlite.function_set_attr_group(func, func_section) + + # Extract default-argument info + tfd_defaults: t.CPtr = None + tfd_default_count: int = 0 + tfd_param_count: int = 0 + if args_node is not None: + tfd_ags: ast.Arguments* = ast.Arguments*(args_node) + if tfd_ags.args is not None: + tfd_alist: list[ast.AST*]* = tfd_ags.args + tfd_param_count = int(len(tfd_alist)) + if tfd_ags.defaults is not None: + tfd_dlist: list[ast.AST*]* = tfd_ags.defaults + tfd_defaults = tfd_dlist + tfd_default_count = int(len(tfd_dlist)) + + # Register in the function table + max_funcs: int = 1024 + if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, func_count, fd.name, func, max_funcs, tfd_defaults, tfd_default_count, tfd_param_count, trans.Pool) == 0: + trans._func_count = func_count + 1 + + # Add parameters (with type annotation support) + if args_node is not None: + ags: ast.Arguments* = ast.Arguments*(args_node) + if ags.args is not None: + alist: list[ast.AST*]* = ags.args + an: sizet = len(alist) + for ai in range(int(an)): + arg: ast.Arg* = ast.Arg*(alist.get(ai)) + if arg is not None and arg.arg is not None: + # CVoid means empty param: skip, do not add to the function signature + if arg.annotation is not None and self.Gen.TypeGenResolve._is_t_attr(arg.annotation, "CVoid", imported_modules) != 0: continue + param_ty: mlirlite.LLVMType* = i32_ty + if arg.annotation is not None: + resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, imported_modules, from_imports, trans, 0) + if resolved is None: + # [S4 fail-fast] Annotation present but unresolvable: no silent i32 + # (wrong param type -> ABI misalignment / wrong field access) + self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' type annotation failed to resolve", "NameError") + return 0 + # Bare class param -> Ptr(struct); CArray param -> element pointer ty* (C array decay). Passing + # Ptr(array) would make out[i]'s GEP stride by the whole array (garbage summary root cause). + match resolved: + case mlirlite.LLVMType.Void(): + # void is not a legal param type (see the same rule above) + self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") + return 0 + case mlirlite.LLVMType.Struct(_, _, _): + param_ty = mlirlite.Ptr(pool, resolved) + case mlirlite.LLVMType.Array(aelem_ty, _): + param_ty = mlirlite.Ptr(pool, aelem_ty) + case _: + param_ty = resolved + pname: str = pool.alloc(32) + if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg) + mlirlite.add_param(pool, func, param_ty, pname) + + # *args variadic: set IsVarArg=1 to emit the ... suffix + if ags.vararg is not None: func.IsVarArg = 1 + + # try-except: a body containing raise gets extra __eh_msg_out__ (i8**) / __eh_code_out__ (i32*) output params + # (TPC HandlesFunctions.py L706-711; except main). Must be outside the args_node block so no-arg functions are covered. + if string.strcmp(fd.name, "main") != 0: + if self.Gen.FuncGenBase._body_has_raise(fd) != 0: + i8_ty_eh: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh) + i8_ptr_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh) + i32_ty_eh: mlirlite.LLVMType* = mlirlite.Int32(pool) + i32_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh) + if i8_ptr_ptr_ty_eh is not None and i32_ptr_ty_eh is not None: + mlirlite.add_param(pool, func, i8_ptr_ptr_ty_eh, "%__eh_msg_out__") + mlirlite.add_param(pool, func, i32_ptr_ty_eh, "%__eh_code_out__") + + # Params added; fill the global func-table Spec + self.Gen.CallGenLeafKit.update_global_func_spec(fd.name, func) + + # Create the entry block + entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") + if entry_blk is None: return 0 + + # Create the function-specific builder + func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) + if func_builder is None: return 0 + mlirlite.position_at_end(func_builder, entry_blk) + + # --traceback: attach a DISubprogram to this define and set every instruction in the body to the entry DILocation + # (refined per statement); no-op when DI is disabled. + dbg_pushed: int = DebugInfoGen.PushFunction(pool, func_builder, func, fd.lineno, fd.col_offset + 1) + + # @t.TLS one-time-init guard (aligned with TPC HandlesFunctions.py): the first call runs the body and sets + # __tls_init_done_{name}=1; later calls return the default. Without it every call re-runs TlsAlloc and loses state. + if has_tls != 0: + tls_flag_name: str = pool.alloc(128) + if tls_flag_name is not None: + viperlib.snprintf(tls_flag_name, 128, "__tls_init_done_%s", fd.name) + tls_flag_gv: mlirlite.GlobalVariable* = mlirlite.create_global_int(pool, mod, tls_flag_name, 1, 0) + if tls_flag_gv is not None: + i1_ty_t: mlirlite.LLVMType* = mlirlite.Int1(pool) + if i1_ty_t is not None: + # GlobalVariable is not a Value subclass (different field layout), so build_load/store reading + # ptr.Ty/ptr.Name as a Value misaligns. As in create_global_string, build a Value view (Ty=Ptr(i1), Name="@name"). + tls_gv_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_t) + tls_ref_name: str = pool.alloc(64) + if tls_ref_name is not None: + viperlib.snprintf(tls_ref_name, 64, "@%s", tls_flag_name) + tls_flag_ref: mlirlite.Value* = mlirlite.SSAValue(pool, tls_gv_ptr_ty, tls_ref_name) + if tls_flag_ref is not None: + tls_flag_ld: mlirlite.Value* = mlirlite.build_load(func_builder, i1_ty_t, tls_flag_ref) + # Unique block name (create_block does not auto-number; same-label conflict within a function) + lc_t: int = trans._label_counter + trans._label_counter = lc_t + 1 + tls_bb_buf: str = pool.alloc(64) + if tls_bb_buf is not None: + viperlib.snprintf(tls_bb_buf, 64, "tls.skip.%d", lc_t) + tls_skip_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, tls_bb_buf) + viperlib.snprintf(tls_bb_buf, 64, "tls.cont.%d", lc_t) + tls_cont_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, tls_bb_buf) + if tls_skip_bb is not None and tls_cont_bb is not None: + mlirlite.build_cond_br(func_builder, tls_flag_ld, tls_skip_bb, tls_cont_bb) + # skip block: return a default value matching the return type + mlirlite.position_at_end(func_builder, tls_skip_bb) + match ret_ty: + case mlirlite.LLVMType.Void(): + mlirlite.build_ret_void(func_builder) + case mlirlite.LLVMType.Int(_): + tlz0: mlirlite.Value* = mlirlite.ConstInt(pool, ret_ty, 0, "0") + if tlz0 is not None: mlirlite.build_ret(func_builder, tlz0) + case mlirlite.LLVMType.Float(_): + tlf0: mlirlite.Value* = mlirlite.ConstFloat(pool, ret_ty, 0.0) + if tlf0 is not None: mlirlite.build_ret(func_builder, tlf0) + case _: + tln0: mlirlite.Value* = mlirlite.ConstNull(pool, ret_ty, "null") + if tln0 is not None: mlirlite.build_ret(func_builder, tln0) + # cont block: set the flag=1 and continue the body + mlirlite.position_at_end(func_builder, tls_cont_bb) + tlone: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_t, 1, "1") + if tlone is not None: mlirlite.build_store(func_builder, tlone, tls_flag_ref) + + # Enter the function scope (nested symbol table) + self.Gen.VarGen.enter_scope(trans.SymTab, FuncGenMixin.SCOPE_FUNCTION) + + # Generic function templates already returned early (see the top of this function); here fd.type_params is always + # None (specialization clones clear it). The generic context is set by _specialize_generic_function before the call. + + # arg()/va_list scope reset: save the ENCLOSING function's va_list state and start this body with + # none of it. A non-variadic function must not inherit the previous function's buffer: that Value + # belongs to another function while the printer emits it by NAME, so the name re-binds to + # whatever local happens to carry it here (scene: viperlib vsnprintf emitted `va_arg ptr %9` + # where %9 is `count_mode`'s i32 alloca -> va_arg loaded a garbage va_list pointer -> + # access-violation inside vsnprintf). The *args setup below re-sets both for a variadic callee, + # and old_va_list_ptr/old_vararg_name are restored after the body so that nested functions still + # see the enclosing variadic function's buffer. + old_va_list_ptr: mlirlite.Value* = trans._va_list_ptr + old_vararg_name: str = trans._vararg_name + trans._va_list_ptr = None + trans._vararg_name = None + + # Create allocas for the parameters + if args_node is not None: + ags2: ast.Arguments* = ast.Arguments*(args_node) + if ags2.args is not None: + alist2: list[ast.AST*]* = ags2.args + an2: sizet = len(alist2) + for ai2 in range(int(an2)): + arg2: ast.Arg* = ast.Arg*(alist2.get(ai2)) + if arg2 is not None and arg2.arg is not None: + # CVoid means empty param: skip, do not create an alloca + if arg2.annotation is not None: + if self.Gen.TypeGenResolve._is_t_attr(arg2.annotation, "CVoid", imported_modules) != 0: continue + param_ty2: mlirlite.LLVMType* = i32_ty + if arg2.annotation is not None: + resolved2: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg2.annotation, imported_modules, from_imports, trans, 0) + if resolved2 is None: + # fail-fast: param has an annotation but is unresolvable; no silent i32 + self.Gen.TypeGenErr.fatal_error(arg2, f"argument '{arg2.arg}' failed to resolve type: {self.Gen.TypeGenResolve.annotation_to_str(arg2.annotation)}", "NameError") + resolved2 = i32_ty # unreachable (fatal_error already exited) + if resolved2 is not None: + # Consistent with add_param: bare class param -> Ptr(struct); CArray param -> element pointer ty* + # (array decay; the alloca type must match the param or storing ty* into [N x ty]* mismatches). + match resolved2: + case mlirlite.LLVMType.Void(): + # void is not a legal param type (see the same rule above) + self.Gen.TypeGenErr.fatal_error(arg2, f"param '{arg2.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError") + return 0 + case mlirlite.LLVMType.Struct(_, _, _): + param_ty2 = mlirlite.Ptr(pool, resolved2) + case mlirlite.LLVMType.Array(aelem_ty2, _): + param_ty2 = mlirlite.Ptr(pool, aelem_ty2) + case _: + param_ty2 = resolved2 + alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, param_ty2) + if alloca is not None: + self.Gen.VarGen.define_var(trans.SymTab, arg2.arg, alloca) + # Store the original annotation's class name (method-call detection falls back from Ptr(i8) to the class). + # Pass pool: a generic Subscript returns a mangled name (aligned with the HandlesAnnAssign fix). + cls_nm: str = None + if arg2.annotation is not None: cls_nm = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg2.annotation, imported_modules, pool, trans._from_imports) + if cls_nm is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg2.arg, cls_nm) + # t.CVolatile param: marked at declaration time so loads/stores of this name carry volatile + # (a volatile param object's read-modify-write must not be hoisted/merged, e.g. a spin-wait arg). + if arg2.annotation is not None: + # const param: reuse TPC's historical param criterion (only whether the annotation contains + # t.CConst, regardless of pointer depth); TPC HandlesFunctions IsParam=True uses the same condition. + if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "CConst", 0) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, arg2.arg) + if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, arg2.arg) + # rawptr[t.CVolatile | T] param (volatile pointee): dereferences in the body + # (p[i]/p.field/c.Deref(p)) carry volatile. + if self.Gen.TypeGenErr.has_pointee_volatile_marker(arg2.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, arg2.arg) + pname2: str = pool.alloc(32) + if pname2 is not None: viperlib.snprintf(pname2, 32, "%%%s", arg2.arg) + param_val: mlirlite.Value* = mlirlite.SSAValue(pool, param_ty2, pname2) + vol_p2: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, arg2.arg)) + mlirlite.build_store(func_builder, param_val, alloca, vol_p2) + + # *args variadic: allocate a 24-byte va_list buffer, bitcast to i8*, call @llvm.va_start, register in the symbol table. + # Must be inside the param alloca section but outside the EH block, or args is unregistered (NameError: 'args'). + if ags2.vararg is not None: + vararg_node: ast.Arg* = ast.Arg*(ags2.vararg) + if vararg_node is not None and vararg_node.arg is not None: + vararg_name_str: str = vararg_node.arg + # va_list buffer is 24 bytes (Win64 __va_list_tag = {i32,i32,i8*,i8*}) with 16-byte alignment + # (aligned with TPC HandlesFunctions.py:1739-1741). + i8_ty_va: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty_va: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_va) + va_buf_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty_va, 24) + va_list_alloca: mlirlite.Value* = mlirlite.build_alloca_aligned(func_builder, va_buf_ty, 16) + if va_list_alloca is not None: + # i8* address (pointing at the 24-byte va_list buffer) - the value semantics of the vararg name + va_list_addr: mlirlite.Value* = mlirlite.build_bitcast(func_builder, va_list_alloca, i8_ptr_ty_va) + if va_list_addr is not None: + # Register in the symbol table (translate_name_value special-cases vararg names to return the address) + self.Gen.VarGen.define_var(trans.SymTab, vararg_name_str, va_list_alloca) + # Call @llvm.va_start(i8* %va_list_addr) + void_ty_va: mlirlite.LLVMType* = mlirlite.Void(pool) + mlirlite.build_call(func_builder, "llvm.va_start", va_list_addr, 1, void_ty_va, 0) + # Save on trans for the arg(type) sugar and vararg-name translation + trans._va_list_ptr = va_list_addr + trans._vararg_name = vararg_name_str + + # try-except: a new function scope starts with zero EH state. Save the enclosing function's EH output params + # before and restore them after; otherwise nested functions corrupt the EH state chain. + old_eh_msg_out: mlirlite.Value* = trans._eh_msg_out_param + old_eh_code_out: mlirlite.Value* = trans._eh_code_out_param + old_cur_func_has_eh: int = trans._cur_func_has_eh + trans._eh_msg_out_param = None + trans._eh_code_out_param = None + trans._cur_func_has_eh = 0 + + # try-except: save this function's EH output params (__eh_msg_out__/__eh_code_out__) for translate_raise / + # UnhandledBB to write and propagate. Functions without EH params are set to None. + if string.strcmp(fd.name, "main") != 0 and self.Gen.FuncGenBase._body_has_raise(fd) != 0: + i8_ty_eh2: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh2) + i8_ptr_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh2) + i32_ty_eh2: mlirlite.LLVMType* = mlirlite.Int32(pool) + i32_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh2) + if i8_ptr_ptr_ty_eh2 is not None and i32_ptr_ty_eh2 is not None: + msg_out_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ptr_ty_eh2, "%__eh_msg_out__") + code_out_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i32_ptr_ty_eh2, "%__eh_code_out__") + trans._eh_msg_out_param = msg_out_ref + trans._eh_code_out_param = code_out_ref + trans._cur_func_has_eh = 1 + + # Save module-level scope state (non-variable-table related only) + old_func: mlirlite.Function* = trans._cur_func + old_builder: mlirlite.IRBuilder* = trans._cur_builder + old_global_count: int = trans._global_name_count + old_nonlocal_count: int = trans._nonlocal_name_count + old_env_count: int = trans._closure_env_count + # old_va_list_ptr / old_vararg_name were saved above (before the *args setup) so that this body + # could start from a clean va_list state; the restore below hands the enclosing value back. + # Return-annotation context ([C①], one source of type-argument fallback for bare generic construction): + # record this function's return annotation before entering the body and restore the outer value on exit. + old_ret_annot: ast.AST* = trans.CurFuncRetAnnot + # break/continue targets and the match-case flag must be cleared for the new function scope (see assignments below), + # otherwise break/continue in a nested function inherits the outer (even cross-function) br targets. + old_break_bb: mlirlite.BasicBlock* = trans._break_bb + old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb + old_insw: int = trans._in_switch_case + + trans._cur_func = func + trans._cur_builder = func_builder + trans.CurFuncRetAnnot = fd.returns + # P2 willFree 失效表:**函数作用域复位**(表按栈槽地址记录,槽地址会被后续函数复用) + self.Gen.VarGen.freed_clear() + trans._break_bb = None + trans._continue_bb = None + trans._in_switch_case = 0 + # Clear the global/nonlocal name sets (new function scope) + GenApi.clear_scope_names(trans) + # Reset the in-chain nonlocal heap prebuild flag (built once per function; multiple nested function definitions + # share one heap, preventing repeated malloc + double VarEntry redirection). + trans._nl_chain_done = 0 + + # Pre-scan the body: 1) Global/Nonlocal declarations first (Python semantics: they apply to the whole function + # scope regardless of position, so register before any assignment); 2) AnnAssign to create local allocas early. + body: list[ast.AST*]* = fd.children + if body is not None: + bn: sizet = len(body) + for bi in range(int(bn)): + stmt: ast.AST* = body.get(bi) + if stmt is not None: self.Gen.StmtGenDecl.pre_scan_scope_decls(trans, stmt) + for bi in range(int(bn)): + stmt: ast.AST* = body.get(bi) + if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt) + + # M3 owned slife(§4.2 函数级 scope guard):进入函数体前把登记区间起点抬到当前栈顶。 + # 表是函数间共享的一段栈式区间,嵌套函数各自独立;退出时由 _SlifeRestore 弹回外层起点。 + # 放在这里(而非更早)是因为上方存在多条提前返回分支(泛型模板/async/extern), + # 只有从这里开始才保证"有 reset 必有 restore"。 + old_slife_base: int = self.Gen.VarGen._SlifeReset(pool) + + # Translate the function body + if body is not None: + bn2: sizet = len(body) + for bi2 in range(int(bn2)): + stmt2: ast.AST* = body.get(bi2) + if stmt2 is not None: + # Before a nested function definition: pre-scan its in-chain nonlocals and build the heap + # (for cases where different levels capture different variables; see _prepare_chain_nonlocals). + if stmt2.kind() == ast.ASTKind.FunctionDef: + self.Gen.FuncGen._prepare_chain_nonlocals(trans, stmt2, pool, func_builder, mod, mlirlite.Int8(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool))) + self.Gen.StmtGen.translate_stmt(trans, stmt2) + + # If the last body statement is not Return, add an implicit ret + last_is_return: int = 0 + # bn3 is initialized early so the fail-fast branch below does not reference an undefined bn3 when body is None. + bn3: sizet = 0 + if body is not None: + bn3 = len(body) + if bn3 > 0: + last_stmt: ast.AST* = body.get(bn3 - 1) + if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1 + if last_is_return == 0: + if mlirlite.builder_cur_block_is_terminated(func_builder) == 0: + # The compiler never invents a return value: falling off the end without an explicit return is fail-fast. + # Legal endings: void -> ret void; unreachable merge (all branches terminated) -> unreachable; unannotated -> zero return. + last_is_ctrl: int = 0 + if bn3 > 0: + last_stmt3: ast.AST* = body.get(bn3 - 1) + if last_stmt3 is not None: + k3: int = last_stmt3.kind() + if k3 == ast.ASTKind.If or k3 == ast.ASTKind.While or k3 == ast.ASTKind.For or k3 == ast.ASTKind.Try or k3 == ast.ASTKind.With or k3 == ast.ASTKind.Match: last_is_ctrl = 1 + _is_void: int = 0 + if ret_ty is not None: + match ret_ty: + case mlirlite.LLVMType.Void(): + _is_void = 1 + case _: + pass + # Annotation-level "no return value" criterion (a second check alongside the ret_ty type check): if the + # REnum class-pattern match degrades, _is_void stays 0 and a void function is misreported as missing a return. + if _is_void == 0 and fd.returns is not None: + if self.Gen.TypeGenResolve._is_cvoid_marker(fd.returns, imported_modules) != 0: _is_void = 1 + elif fd.returns.kind() == ast.ASTKind.Constant: + ret_none_cn: ast.Constant* = (ast.Constant*)(fd.returns) + if ret_none_cn.const_kind == ast.CONST_NONE: _is_void = 1 + if _is_void != 0: + # M3:隐式收尾也是一个函数出口,先释放本函数登记的 slife 再返回 + self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans) + mlirlite.build_ret_void(func_builder) + elif last_is_ctrl != 0 and trans._ctrl_merge_reachable == 0: + # A non-void function whose last statement is control flow with a structurally unreachable merge (all + # branches return, no br to merge) -> LLVM unreachable. Reachability uses only `_ctrl_merge_reachable`. + mlirlite.build_unreachable(func_builder) + elif fd.returns is None: + # No return annotation -> a no-value procedure function, return a zero default + if ret_ty is not None: + zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty) + if zero_val is not None: + # M3:隐式收尾出口,先释放 slife 再返回 + self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans) + mlirlite.build_ret(func_builder, zero_val) + else: self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") + else: self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") + else: + # fail-fast: has a -> annotation but the flow falls off the end (pointing at the function definition + # node; fatal_error prints a traceback-style error). + if ret_ty is not None: + # mlirlite has no type_get_name API; serialize the type with the real interface + # TypePrint(buf, size, ty, pool). + rt_name: str = pool.alloc_tagged(4096, memhub.DETSITE_FUNCGENDEF_792) + if rt_name is not None: + mlirlite.TypePrint(rt_name, 4096, ret_ty, pool) + if rt_name[0] == '\0': rt_name = "?" + err_fn: str = pool.alloc(256) + if err_fn is not None: + viperlib.snprintf(err_fn, 256, "function '%s' flow falls off the end of the function without an explicit return (return type %s), please add an explicit return (e.g. return 0 / return null)", fd.name, rt_name) + self.Gen.TypeGenErr.fatal_error(node, err_fn, "TypeError") + self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") + else: self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError") + + # Restore module-level scope (exit the function scope) + self.Gen.VarGen.exit_scope(trans.SymTab) + # M3 owned slife:丢弃本函数的登记区间、弹回外层起点(释放已在各出口用 _SlifeEmitFrees 完成) + self.Gen.VarGen._SlifeRestore(old_slife_base) + trans._cur_func = old_func + trans._cur_builder = old_builder + trans._break_bb = old_break_bb + trans._continue_bb = old_continue_bb + trans._in_switch_case = old_insw + trans._global_name_count = old_global_count + trans._nonlocal_name_count = old_nonlocal_count + trans._closure_env_count = old_env_count + trans._va_list_ptr = old_va_list_ptr + trans._vararg_name = old_vararg_name + # Hand back the outer function's return annotation (see old_ret_annot above) + trans.CurFuncRetAnnot = old_ret_annot + # Restore the outer (caller) function's EH output params: this function's raise propagation uses its own params, + # so hand them back on exit or nested/chained calls leave stale EH state and write to the wrong out pointer. + trans._eh_msg_out_param = old_eh_msg_out + trans._eh_code_out_param = old_eh_code_out + trans._cur_func_has_eh = old_cur_func_has_eh + + # Custom decorator: register and generate a wrapper (aligned with TPC DecoratorPass.run). Module-level functions + # only; wrapper name is __decor_wrap_{bare name}; call sites are rewritten via lookup_decor_wrapper. + if has_custom_decor != 0: + wb_name: str = pool.alloc(128) + if wb_name is not None: + viperlib.snprintf(wb_name, 128, "__decor_wrap_%s", fd.name) + self.Gen.ClassGenBase.register_decorator(pool, fd.name, wb_name, decor_arg0) + self._gen_decor_wrapper(trans, pool, mod, func, fd.name, wb_name, decor_handler, decor_arg0) + + if dbg_pushed != 0: DebugInfoGen.PopFunction(func_builder) + + return 0 + + # _RegisterStaticFunc - register a static function (no cross-module use). Two equivalent forms (normalized by the + # parser): annotation form or decorator form; on hit, register (name, defining module SHA1) in the static symbol table. + def _RegisterStaticFunc(self, trans: HT.Translator*, fd: ast.FunctionDef*) -> int: + """Register a static function when its annotation or @t.CStatic matches; 1=registered, 0=miss.""" + if trans is None or fd is None or fd.name is None: return 0 + if trans.ModuleSha1 is None: return 0 + has: int = 0 + if fd.returns is not None: has = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CStatic", 0) + if has == 0 and fd.decorator_list is not None: + if self.Gen.ClassGenBase._has_decorator(fd.decorator_list, "CStatic") == 1: has = 1 + if has == 0: return 0 + return self.Gen.CallGenBase.RegisterStaticSymbol(trans.ModuleSha1, fd.name) + + # _gen_decor_wrapper - generate a decorator wrapper (aligned with TPC DecoratorPass.py _generate_single_wrapper v4). + # Blocks: entry -> recursive_call/decorated_entry -> loop.header/loop.body -> post; handler signature i32 decor(i8*, i8*, i32, i8*, i8*, i8*). + def _gen_decor_wrapper(self, trans: HT.Translator*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, func: mlirlite.Function*, func_name: str, wrapper_name: str, handler_name: str, decor_arg0: int64) -> mlirlite.Function*: + """Generate a decorator wrapper function; returns the wrapper or None.""" + if trans is None or pool is None or mod is None or func is None: return None + if func_name is None or wrapper_name is None or handler_name is None: return None + ret_ty: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(func) + if ret_ty is None: return None + orig_call_name: str = mlirlite.function_get_name(func) + if orig_call_name is None: return None + + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + + # ---- Collect the original function's param types (by position, up to 16) ---- + n_params: int = 0 + param_tys: sizet* = pool.alloc(16 * 8) + if param_tys is None: return None + string.memset(param_tys, 0, 16 * 8) + pp: mlirlite.Param* = mlirlite.function_get_param_head(func) + while pp is not None: + pty: mlirlite.LLVMType* = mlirlite.param_get_ty(pp) + if pty is not None and n_params < 16: + param_tys[n_params] = sizet(pty) + n_params += 1 + pp = mlirlite.param_get_next(pp) + + # ---- Create the wrapper function and add params (named %a0..%aN) ---- + wrapper: mlirlite.Function* = mlirlite.create_function(pool, mod, wrapper_name, ret_ty) + if wrapper is None: return None + pi: int = 0 + for pi in range(int(n_params)): + pname_w: str = pool.alloc(16) + if pname_w is not None: + viperlib.snprintf(pname_w, 16, "%%a%d", pi) + mlirlite.add_param(pool, wrapper, (mlirlite.LLVMType*)(param_tys[pi]), pname_w) + + # ---- Create blocks ---- + entry_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "entry") + loop_h_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "loop.header") + loop_b_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "loop.body") + post_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "post") + if entry_bb is None or loop_h_bb is None or loop_b_bb is None or post_bb is None: return None + + # Recursion guard flag (outermost): @__decor_rec_{wrapper} = global i8 0 + rec_flag_ref: mlirlite.Value* = None + rec_flag_name: str = pool.alloc(128) + if rec_flag_name is not None: + viperlib.snprintf(rec_flag_name, 128, "__decor_rec_%s", wrapper_name) + mlirlite.create_global_int(pool, mod, rec_flag_name, 8, 0) + rec_ref_nm: str = pool.alloc(96) + if rec_ref_nm is not None: + viperlib.snprintf(rec_ref_nm, 96, "@%s", rec_flag_name) + rec_flag_ref = mlirlite.SSAValue(pool, mlirlite.Ptr(pool, i8_ty), rec_ref_nm) + + wb: mlirlite.IRBuilder* = mlirlite.new_builder(pool, wrapper) + if wb is None: return None + + # Classify the return type + ret_is_void: int = 0 + match ret_ty: + case mlirlite.LLVMType.Void(): + ret_is_void = 1 + case _: + pass + + # ---- entry: recursion guard check (outermost) ---- + mlirlite.position_at_end(wb, entry_bb) + loop_pred_bb: mlirlite.BasicBlock* = entry_bb + if rec_flag_ref is not None: + rec_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "recursive_call") + dec_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, wrapper, "decorated_entry") + if rec_bb is None or dec_bb is None: return None + rec_val: mlirlite.Value* = mlirlite.build_load(wb, i8_ty, rec_flag_ref) + rec_zero: mlirlite.Value* = mlirlite.ConstInt(pool, i8_ty, 0, "0") + rec_is: mlirlite.Value* = mlirlite.build_icmp(wb, mlirlite.ICMP_NE, rec_val, rec_zero) + mlirlite.build_cond_br(wb, rec_is, rec_bb, dec_bb) + # recursive_call: call the original function directly (skipping all decoration logic) + mlirlite.position_at_end(wb, rec_bb) + rec_args: mlirlite.Value* = None + ri: int = 0 + for ri in range(int(n_params)): + an_r: str = pool.alloc(16) + if an_r is None: continue + viperlib.snprintf(an_r, 16, "%%a%d", ri) + av_r: mlirlite.Value* = mlirlite.SSAValue(pool, (mlirlite.LLVMType*)(param_tys[ri]), an_r) + if av_r is not None: + mlirlite.value_set_next(av_r, None) + if rec_args is None: rec_args = av_r + else: + # Append to the tail + rt: mlirlite.Value* = rec_args + while rt.Next is not None: rt = rt.Next + mlirlite.value_set_next(rt, av_r) + if ret_is_void != 0: + mlirlite.build_call(wb, orig_call_name, rec_args, n_params, ret_ty, 0) + mlirlite.build_ret_void(wb) + else: + rc_res: mlirlite.Value* = mlirlite.build_call(wb, orig_call_name, rec_args, n_params, ret_ty, 0) + if rc_res is not None: mlirlite.build_ret(wb, rc_res) + else: mlirlite.build_ret_void(wb) + # decorated_entry: set the recursion flag and continue the decoration path + mlirlite.position_at_end(wb, dec_bb) + rec_one: mlirlite.Value* = mlirlite.ConstInt(pool, i8_ty, 1, "1") + if rec_one is not None: mlirlite.build_store(wb, rec_one, rec_flag_ref) + loop_pred_bb = dec_bb + + # ---- Common decoration path (entry or decorated_entry) ---- + # 1. ctx = alloca [32 x i8]; ctx_ptr = bitcast i8* + ctx_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty, 32) + ctx_alloca: mlirlite.Value* = mlirlite.build_alloca(wb, ctx_ty) + ctx_ptr: mlirlite.Value* = mlirlite.build_bitcast(wb, ctx_alloca, i8_ptr_ty) + + # 2. Pack function params into a struct {param_types} (the decorator can read/write args_ptr) + args_alloca: mlirlite.Value* = None + args_ptr: mlirlite.Value* = None + if n_params > 0: + fields_head: mlirlite.ParamNode* = None + fk: int = 0 + for fk in range(int(n_params - 1), -1, -1): + pn_n: mlirlite.ParamNode* = mlirlite.new_param_node(pool, (mlirlite.LLVMType*)(param_tys[fk])) + if pn_n is not None: + pn_n.Next = fields_head + fields_head = pn_n + args_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, fields_head, n_params, None) + if args_ty is not None: + args_alloca = mlirlite.build_alloca(wb, args_ty) + fi2: int = 0 + for fi2 in range(int(n_params)): + fld_ty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(param_tys[fi2]) + fld_ptr: mlirlite.Value* = mlirlite.build_gep_struct(wb, args_ty, fld_ty, args_alloca, fi2) + if fld_ptr is not None: + fld_nm: str = pool.alloc(16) + if fld_nm is not None: + viperlib.snprintf(fld_nm, 16, "%%a%d", fi2) + fld_v: mlirlite.Value* = mlirlite.SSAValue(pool, fld_ty, fld_nm) + if fld_v is not None: mlirlite.build_store(wb, fld_v, fld_ptr) + args_ptr = mlirlite.build_bitcast(wb, args_alloca, i8_ptr_ty) + else: args_ptr = mlirlite.ConstNull(pool, i8_ptr_ty, "null") + + # 3. Return value storage (zero-initialized; a safe default is returned when skipping the original) + ret_alloca: mlirlite.Value* = None + if ret_is_void == 0: + ret_alloca = mlirlite.build_alloca(wb, ret_ty) + ret_zero: mlirlite.Value* = None + match ret_ty: + case mlirlite.LLVMType.Int(_): + ret_zero = mlirlite.ConstInt(pool, ret_ty, 0, "0") + case mlirlite.LLVMType.Float(_): + ret_zero = mlirlite.ConstFloat(pool, ret_ty, 0.0) + case _: + ret_zero = mlirlite.ConstNull(pool, ret_ty, "null") + if ret_zero is not None and ret_alloca is not None: mlirlite.build_store(wb, ret_zero, ret_alloca) + + # 4. Function-name string constant + fn_str: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(wb, pool, mod, func_name, trans) + if fn_str is None: fn_str = mlirlite.ConstNull(pool, i8_ptr_ty, "null") + + # 5. handler declare (declared when untranslated; fixed signature i32 (i8*,i8*,i32,i8*,i8*,i8*)) + if self.Gen.CallGenBase.find_func_in_module(mod, handler_name) is None: + handler_d: mlirlite.Function* = mlirlite.create_declare(pool, mod, handler_name, i32_ty) + if handler_d is not None: + mlirlite.add_param(pool, handler_d, i8_ptr_ty, "ctx") + mlirlite.add_param(pool, handler_d, i8_ptr_ty, "func_name") + mlirlite.add_param(pool, handler_d, i32_ty, "phase") + mlirlite.add_param(pool, handler_d, i8_ptr_ty, "args_ptr") + mlirlite.add_param(pool, handler_d, i8_ptr_ty, "ret_ptr") + mlirlite.add_param(pool, handler_d, i8_ptr_ty, "decor_args_ptr") + + # 6. Pre-phase: handler returns the call count n_calls + ph0: mlirlite.Value* = mlirlite.ConstInt(pool, i32_ty, 0, "0") + null_ret: mlirlite.Value* = mlirlite.ConstNull(pool, i8_ptr_ty, "null") + null_dec: mlirlite.Value* = mlirlite.ConstNull(pool, i8_ptr_ty, "null") + # Arg linked list (6 separate values): ctx_ptr -> fn_str -> 0 -> args_ptr -> null -> null. + # Note: no self-link (null.Next = null would make build_call loop forever). + if ctx_ptr is not None: mlirlite.value_set_next(ctx_ptr, fn_str) + if fn_str is not None: mlirlite.value_set_next(fn_str, ph0) + if ph0 is not None: mlirlite.value_set_next(ph0, args_ptr) + if args_ptr is not None: mlirlite.value_set_next(args_ptr, null_ret) + if null_ret is not None: mlirlite.value_set_next(null_ret, null_dec) + n_calls: mlirlite.Value* = mlirlite.build_call(wb, handler_name, ctx_ptr, 6, i32_ty, 0) + + # 7. Counter alloca (VPC uses an alloca counter instead of phi) + cnt_alloca: mlirlite.Value* = mlirlite.build_alloca(wb, i32_ty) + cnt_zero: mlirlite.Value* = mlirlite.ConstInt(pool, i32_ty, 0, "0") + if cnt_alloca is not None and cnt_zero is not None: mlirlite.build_store(wb, cnt_zero, cnt_alloca) + mlirlite.build_br(wb, loop_h_bb) + + # 8. loop.header:cur = load cnt; cur < n_calls → body / post + mlirlite.position_at_end(wb, loop_h_bb) + cur_i: mlirlite.Value* = mlirlite.build_load(wb, i32_ty, cnt_alloca) + loop_cond: mlirlite.Value* = mlirlite.build_icmp(wb, mlirlite.ICMP_SLT, cur_i, n_calls) + mlirlite.build_cond_br(wb, loop_cond, loop_b_bb, post_bb) + + # 9. loop.body: read back params (the decorator may have modified them) -> call the original -> store ret + mlirlite.position_at_end(wb, loop_b_bb) + call_args: mlirlite.Value* = None + if n_params > 0: + k: int = 0 + for k in range(int(n_params)): + fld_ty_k: mlirlite.LLVMType* = (mlirlite.LLVMType*)(param_tys[k]) + fld_pk: mlirlite.Value* = mlirlite.build_gep_struct(wb, args_ty, fld_ty_k, args_alloca, k) + if fld_pk is None: continue + fld_vk: mlirlite.Value* = mlirlite.build_load(wb, fld_ty_k, fld_pk) + if fld_vk is not None: + mlirlite.value_set_next(fld_vk, None) + if call_args is None: call_args = fld_vk + else: + ct: mlirlite.Value* = call_args + while ct.Next is not None: ct = ct.Next + mlirlite.value_set_next(ct, fld_vk) + res_v: mlirlite.Value* = mlirlite.build_call(wb, orig_call_name, call_args, n_params, ret_ty, 0) + if ret_alloca is not None and res_v is not None: mlirlite.build_store(wb, res_v, ret_alloca) + nxt_i: mlirlite.Value* = mlirlite.build_add(wb, cur_i, mlirlite.ConstInt(pool, i32_ty, 1, "1")) + if nxt_i is not None and cnt_alloca is not None: mlirlite.build_store(wb, nxt_i, cnt_alloca) + mlirlite.build_br(wb, loop_h_bb) + + # 10. post: post-phase handler -> clear the recursion flag -> return the result + mlirlite.position_at_end(wb, post_bb) + ret_ptr: mlirlite.Value* = null_ret + if ret_alloca is not None: ret_ptr = mlirlite.build_bitcast(wb, ret_alloca, i8_ptr_ty) + ph1: mlirlite.Value* = mlirlite.ConstInt(pool, i32_ty, 1, "1") + # Post-phase arg chain: ctx_ptr -> fn_str -> 1 -> args_ptr -> ret_ptr -> null + if ctx_ptr is not None: mlirlite.value_set_next(ctx_ptr, fn_str) + if fn_str is not None: mlirlite.value_set_next(fn_str, ph1) + if ph1 is not None: mlirlite.value_set_next(ph1, args_ptr) + if args_ptr is not None: mlirlite.value_set_next(args_ptr, ret_ptr) + if ret_ptr is not None: mlirlite.value_set_next(ret_ptr, null_dec) + mlirlite.build_call(wb, handler_name, ctx_ptr, 6, i32_ty, 0) + if rec_flag_ref is not None: + rec_zero2: mlirlite.Value* = mlirlite.ConstInt(pool, i8_ty, 0, "0") + if rec_zero2 is not None: mlirlite.build_store(wb, rec_zero2, rec_flag_ref) + if ret_is_void != 0: mlirlite.build_ret_void(wb) + else: + final_v: mlirlite.Value* = mlirlite.build_load(wb, ret_ty, ret_alloca) + if final_v is not None: mlirlite.build_ret(wb, final_v) + else: mlirlite.build_ret_void(wb) + return wrapper \ No newline at end of file diff --git a/App/lib/core/Gen/Func/FuncGenMixin.vp b/App/lib/core/Gen/Func/FuncGenMixin.vp index 8e5e15f..3d4f73e 100644 --- a/App/lib/core/Gen/Func/FuncGenMixin.vp +++ b/App/lib/core/Gen/Func/FuncGenMixin.vp @@ -1,1245 +1,1245 @@ -import lib.core.Gen.GenTypes as GenTypes -import t, c -from stdint import * -import ast -import mlirlite -import memhub -import string -import viperlib -import stdio -import stdlib -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HT -import lib.core.Gen.Call.CallGenBase as CallGenBase -import lib.core.Gen.GenApi as GenApi -import lib.core.Gen.DebugInfoGen as DebugInfoGen - - -# ============================================================ -# FuncGenMixin - function/method declaration and translation Mixin - -# Scope type constant (uncertain) -define SCOPE_FUNCTION = 1 - - -# ============================================================ -# FuncGenMixin - function attribute/naming/declaration/translation -novirtual class FuncGenMixin(CallGenBase.CallGenBase): - """Function/method translation Mixin (stateless)""" - - # translate_nested_function_def - hoist a nested function to a top-level - # @__closure_{name}(i8* %env) and build its closure {fn_ptr, env_ptr} in the parent. - def translate_nested_function_def(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Translate a nested function definition: hoist to a top-level function and build a closure""" - fd: ast.FunctionDef* = ast.FunctionDef*(node) - if fd is None or fd.name is None: return 0 - - pool: memhub.MemBuddy* = trans.Pool - mod: mlirlite.LLVMModule* = trans.Module - func_name: str = fd.name - - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - - # 1. Build the promoted name __closure_{name} (SHA1 prefix avoids cross-module clashes) - promoted_name: str = pool.alloc(64) - if promoted_name is not None: viperlib.snprintf(promoted_name, 64, "__closure_%s", func_name) - mangled_promoted: str = self.Gen.FuncGenBase._mangle_name(trans, promoted_name) - - # 2. Create the promoted function: define @__closure_{name}(i8* %env, args...) - # 返回类型**跟随 `-> T` 注解**。原实现写死 i32:于是 `def Inner(p: char*) -> char*: return p` - # 生成 `define i32 @__closure_Inner(i8* %env, i8* %p)`,而 `return p` 要把 char* 隐式转 i32 - # → 撞 coerce_to_type 的 CastError(现场:EscapeMethodTest 的 (C) 用例)。那是"嵌套 def - # 只支持 int 返回"这个预先存在的缺口,此前被同文件更早的用例报错掩盖。 - # 约定(最小改动的边界): - # · `fd.returns is None` 或注解是 `None` / `t.CVoid` → **保持 i32**(不改既有 ABI, - # 避免打断现有"无返回值/无注解"的嵌套 def;这是已知缺口,不在本次范围内); - # · 其余注解采用 resolve_annotation_type 的结果;解析不出 → fail-fast,不静默兜底。 - ret_ty_n: mlirlite.LLVMType* = i32_ty - if fd.returns is not None: - keep_i32_n: int = 0 - if fd.returns.kind() == ast.ASTKind.Constant: - rc_n: ast.Constant* = ast.Constant*(fd.returns) - if rc_n is not None and rc_n.const_kind == ast.CONST_NONE: keep_i32_n = 1 - if self.Gen.TypeGenResolve._is_cvoid_marker(fd.returns, trans._imported_modules) != 0: keep_i32_n = 1 - if keep_i32_n == 0: - rt_res_n: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, trans._imported_modules, trans._from_imports, trans, 0) - if rt_res_n is None: - self.Gen.TypeGenErr.fatal_error(fd, f"nested function '{fd.name}': cannot resolve the return type annotation for the promoted closure", "TypeError") - else: - rt_is_void_n: int = 0 - match rt_res_n: - case mlirlite.LLVMType.Void(): - rt_is_void_n = 1 - case _: - pass - if rt_is_void_n == 0: ret_ty_n = rt_res_n - func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled_promoted, ret_ty_n) - if func is None: return 0 - mlirlite.add_param(pool, func, i8_ptr_ty, "%env") - - # 2a. Nested function params join the promoted signature (i8* %env, args...); - # types go into the param_tys buffer (CVoid skipped, aligning with alloca indices). - imported_modules: str = trans._imported_modules - from_imports: str = trans._from_imports - param_tys: t.CPtr = pool.alloc(8 * 64) - if param_tys is not None: string.memset(param_tys, 0, 8 * 64) - param_count: int = 0 - if fd.args is not None: - ags_n: ast.Arguments* = ast.Arguments*(fd.args) - if ags_n.args is not None: - alist_n: list[ast.AST*]* = ags_n.args - an_n: sizet = len(alist_n) - for ai_n in range(int(an_n)): - arg_n: ast.Arg* = ast.Arg*(alist_n.get(ai_n)) - if arg_n is None or arg_n.arg is None: continue - if arg_n.annotation is not None: - if self.Gen.TypeGenResolve._is_t_attr(arg_n.annotation, "CVoid", imported_modules) != 0: continue - pty_n: mlirlite.LLVMType* = i32_ty - if arg_n.annotation is not None: - resolved_n: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg_n.annotation, imported_modules, from_imports, trans, 0) - if resolved_n is not None: - match resolved_n: - case mlirlite.LLVMType.Struct(_, _, _): - pty_n = mlirlite.Ptr(pool, resolved_n) - case mlirlite.LLVMType.Array(aelem_ty_n, _): - pty_n = mlirlite.Ptr(pool, aelem_ty_n) - case _: - pty_n = resolved_n - if param_count < 64 and param_tys is not None: - pty_slot_n: str* = (str*)(uint64(param_tys) + param_count * 8) - pty_slot_n[0] = pty_n - pname_n: str = pool.alloc(32) - if pname_n is not None: - viperlib.snprintf(pname_n, 32, "%%%s", arg_n.arg) - mlirlite.add_param(pool, func, pty_n, pname_n) - param_count += 1 - - # 3. Create the entry block and builder - entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") - if entry_blk is None: return 0 - func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) - if func_builder is None: return 0 - mlirlite.position_at_end(func_builder, entry_blk) - - # --traceback: promoted nested closure gets its own DISubprogram. - dbg_pushed_n: int = DebugInfoGen.PushFunction(pool, func_builder, func, node.lineno, node.col_offset + 1) - - # 4. Enter the nested function scope (nested symbol table) - self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_FUNCTION) - - # 5. Create the _env_ptr alloca and store the %env parameter - env_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, i8_ptr_ty) - if env_alloca is not None: - self.Gen.VarGen.define_var(trans.SymTab, "_env_ptr", env_alloca) - env_param_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "%env") - mlirlite.build_store(func_builder, env_param_val, env_alloca) - - # 5a. Create alloca for nested function params so the body can reference them - # (e.g. for i in range(n) inside accumulate(n), result = val inside set_result(val)). - if fd.args is not None: - ags_p: ast.Arguments* = ast.Arguments*(fd.args) - if ags_p.args is not None: - alist_p: list[ast.AST*]* = ags_p.args - idx_p: int = 0 - apn: sizet = len(alist_p) - for api in range(int(apn)): - arg_p: ast.Arg* = ast.Arg*(alist_p.get(api)) - if arg_p is None or arg_p.arg is None: continue - if arg_p.annotation is not None: - if self.Gen.TypeGenResolve._is_t_attr(arg_p.annotation, "CVoid", imported_modules) != 0: continue - if idx_p >= param_count: break - pty2_n: mlirlite.LLVMType* = i32_ty - if param_tys is not None: - ty_addr_n: uint64 = uint64(param_tys) + idx_p * 8 - ty_slot_n: str* = (str*)(ty_addr_n) - pty2_n = (mlirlite.LLVMType*)(ty_slot_n[0]) - if pty2_n is None: pty2_n = i32_ty - p_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, pty2_n) - if p_alloca is not None: - self.Gen.VarGen.define_var(trans.SymTab, arg_p.arg, p_alloca) - if arg_p.annotation is not None: - cls_nm_p: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg_p.annotation, imported_modules, pool, trans._from_imports) - if cls_nm_p is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg_p.arg, cls_nm_p) - # const param: keep TPC's historical test (only whether the annotation contains - # t.CConst, not the pointer depth); both sides align per type-descriptors.md. - if self.Gen.TypeGenErr.has_decorator_marker(arg_p.annotation, "CConst", 0) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, arg_p.arg) - # t.CVolatile param: mark at declaration (same as the FuncGenDef main path) so - # load/store of this param name in the body carry volatile. - if self.Gen.TypeGenErr.has_decorator_marker(arg_p.annotation, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, arg_p.arg) - # rawptr[t.CVolatile | T] param (volatile pointee): dereferences in the body carry - # volatile (same as the FuncGenDef main path). - if self.Gen.TypeGenErr.has_pointee_volatile_marker(arg_p.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, arg_p.arg) - pname2_n: str = pool.alloc(32) - if pname2_n is not None: viperlib.snprintf(pname2_n, 32, "%%%s", arg_p.arg) - p_val: mlirlite.Value* = mlirlite.SSAValue(pool, pty2_n, pname2_n) - vol_pn: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, arg_p.arg)) - mlirlite.build_store(func_builder, p_val, p_alloca, vol_pn) - idx_p += 1 - - # 6. Save the parent function scope state (variable-table independent) - old_func: mlirlite.Function* = trans._cur_func - old_builder: mlirlite.IRBuilder* = trans._cur_builder - old_func_name: str = trans._cur_func_name - old_global_count: int = trans._global_name_count - old_nonlocal_count: int = trans._nonlocal_name_count - old_env_count: int = trans._closure_env_count - # break/continue targets and match-case flag must be cleared for the new scope (see - # assignments below); otherwise a nested break/continue inherits the outer target. - old_break_bb: mlirlite.BasicBlock* = trans._break_bb - old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb - old_insw: int = trans._in_switch_case - # Separate nonlocal buffer: sharing [0] would overwrite the parent's set (ClosureTest2 - # Test 7: level2's b clobbers level1's a). Restored after translation. - saved_nl_names: t.CPtr = trans._nonlocal_names - saved_nl_types: t.CPtr = trans._nonlocal_types - - # 7. Switch to the nested function scope and clear scope names - trans._cur_func = func - trans._cur_builder = func_builder - trans._cur_func_name = func_name - # P2 willFree 失效表:函数作用域复位(栈槽地址会被后续函数复用,跨函数保留会误报) - self.Gen.VarGen.freed_clear() - GenApi.clear_scope_names(trans) - trans._break_bb = None - trans._continue_bb = None - trans._in_switch_case = 0 - # Independent nonlocal buffers for the nested function (avoid clobbering the parent set) - trans._nonlocal_names = stdlib.malloc(HT.MAX_GLOBAL_NAMES * 8) - trans._nonlocal_types = stdlib.malloc(HT.MAX_GLOBAL_NAMES * 8) - if trans._nonlocal_names is not None: string.memset(trans._nonlocal_names, 0, HT.MAX_GLOBAL_NAMES * 8) - if trans._nonlocal_types is not None: string.memset(trans._nonlocal_types, 0, HT.MAX_GLOBAL_NAMES * 8) - - # 8. Pre-scan the body: (a) Global/Nonlocal declarations (Python: whole scope), - # (b) AnnAssign locals to create their allocas early. - body: list[ast.AST*]* = fd.children - if body is not None: - bn: sizet = len(body) - for bi in range(int(bn)): - stmt: ast.AST* = body.get(bi) - if stmt is not None: self.Gen.StmtGenDecl.pre_scan_scope_decls(trans, stmt) - for bi in range(int(bn)): - stmt: ast.AST* = body.get(bi) - if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt) - - # 8c. Implicit capture scan: outer locals read by the nested function become nonlocal - # (Python semantics). Branch AnnAssign locals are already pre-registered and excluded. - if body is not None: - bnc: sizet = len(body) - for bic in range(int(bnc)): - stmtc: ast.AST* = body.get(bic) - if stmtc is not None: self._closure_scan_captures(trans, stmtc) - - # M3 owned slife:嵌套函数体走的是这条**独立**的转译路径(不经过 FuncGenDef.translate_function_def), - # 所以登记区间必须在这里自己抬升/弹回。否则内层登记会落进外层的区间 → 内层出口把外层对象 - # 一起 free(提前释放),外层出口再 free 一次 → **double free / 堆损坏**(现场:NewLifetimeTest - # 的 CaseNestedDef,STATUS_HEAP_CORRUPTION 0xC0000374)。 - old_slife_base: int = self.Gen.VarGen._SlifeReset(pool) - - # 9. Translate the body (Nonlocal statements fill _nonlocal_names) - if body is not None: - bn2: sizet = len(body) - for bi2 in range(int(bn2)): - stmt2: ast.AST* = body.get(bi2) - if stmt2 is not None: self.Gen.StmtGen.translate_stmt(trans, stmt2) - - # 10. Implicit return - last_is_return: int = 0 - if body is not None: - bn3: sizet = len(body) - if bn3 > 0: - last_stmt: ast.AST* = body.get(bn3 - 1) - if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1 - if last_is_return == 0: - # M3:隐式收尾也是函数出口,先释放本嵌套函数登记的 slife - self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans) - # 零值必须与**本函数的返回类型**一致(原实现写死 const_int32(0):一旦返回类型改为 - # `char*` 之类,`ret i32 0` 与签名不符,llc 会直接拒绝)。ret_ty_n 见第 2 步。 - zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty_n) - if zero_val is None: - self.Gen.TypeGenErr.fatal_error(fd, f"nested function '{fd.name}': flow falls off the end without an implicit return and the return type has no zero value", "TypeError") - else: - mlirlite.build_ret(func_builder, zero_val) - - # M3:弹回外层登记区间(释放已在本嵌套函数各出口完成) - self.Gen.VarGen._SlifeRestore(old_slife_base) - - # 11. Save the nested function's nonlocal info - nested_nonlocal_count: int = trans._nonlocal_name_count - - # 12. Leave the nested function scope, back to the parent (keep _nonlocal_names for env) - self.Gen.VarGen.exit_scope(trans.SymTab) - trans._cur_func = old_func - trans._cur_builder = old_builder - trans._break_bb = old_break_bb - trans._continue_bb = old_continue_bb - trans._in_switch_case = old_insw - if dbg_pushed_n != 0: DebugInfoGen.PopFunction(func_builder) - # NOTE: _nonlocal_names and _nonlocal_name_count still hold the nested function's values - - # 13. Create the closure object in the parent function - parent_builder: mlirlite.IRBuilder* = old_builder - if parent_builder is None: - # No builder (module-level nested function?) - cannot create a closure, only restore state - trans._cur_func_name = old_func_name - trans._global_name_count = old_global_count - trans._nonlocal_name_count = old_nonlocal_count - trans._closure_env_count = old_env_count - trans._nonlocal_names = saved_nl_names - trans._nonlocal_types = saved_nl_types - return 0 - - # 13a. Create env: malloc(8 * nested_nonlocal_count) - an array of pointers, one - # 8-byte i8* slot per nonlocal variable pointing at the parent's alloca (shared). - env_ptr: mlirlite.Value* = None - if nested_nonlocal_count > 0: - env_size: int64 = nested_nonlocal_count * 8 - env_size_val: mlirlite.Value* = mlirlite.const_int64(pool, env_size) - if env_size_val is not None: - env_size_val.Next = None - env_ptr = mlirlite.build_call(parent_builder, "malloc", env_size_val, 1, i8_ptr_ty, 0) - - # Build heap storage per nonlocal and register it into env (runtime env chaining, no - # compile-time map): parent local -> new heap + redirect; parent nonlocal -> reuse heap. - if env_ptr is not None: - i8_ptr_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty) - for ni in range(int(nested_nonlocal_count)): - # Read the name from the _nonlocal_names buffer - name_addr: uint64 = uint64(trans._nonlocal_names) + ni * 8 - nl_slot: str* = (str*)(name_addr) - nl_name: str = nl_slot[0] - if nl_name is None: continue - heap_store: mlirlite.Value* = None - # 1. Prefer the global heap-address map (_prepare_chain_nonlocals already built heaps - # for chain nonlocals and stored @nlheap.*; shared across layers, e.g. Test 7). - gname_ck: str = self._nl_heap_global_name(pool, trans, nl_name) - if gname_ck is not None and self._nl_heap_global_exists(mod, gname_ck) != 0: - gv_ref0: mlirlite.Value* = self._nl_heap_global_ref(pool, trans, nl_name) - if gv_ref0 is not None: heap_store = mlirlite.build_load(parent_builder, i8_ptr_ty, gv_ref0) - if heap_store is None: - local_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nl_name) - if local_alloca is not None and local_alloca.Ty is not None: - # Parent local: new heap storage + copy current value + redirect - nl_load_ty: mlirlite.LLVMType* = local_alloca.Ty.Pointee - if nl_load_ty is not None: - cur_val_nl: mlirlite.Value* = mlirlite.build_load(parent_builder, nl_load_ty, local_alloca) - # Heap size uses the type's byte size: arrays (e.g. [4 x i32]=16 bytes) must not be - # fixed at 8 (out-of-bounds write); pointers/scalars are >= 8 bytes. - st_size: int = self._type_byte_size(pool, nl_load_ty) - st_size_val: mlirlite.Value* = mlirlite.const_int64(pool, st_size) - if st_size_val is not None: st_size_val.Next = None - var_storage: mlirlite.Value* = mlirlite.build_call(parent_builder, "malloc", st_size_val, 1, i8_ptr_ty, 0) - if var_storage is not None: - # Copy the current value to heap storage - st_slot_nl: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, var_storage, mlirlite.Ptr(pool, nl_load_ty)) - if st_slot_nl is not None and cur_val_nl is not None: mlirlite.build_store(parent_builder, cur_val_nl, st_slot_nl) - # Redirect the parent's VarEntry.Alloca to the heap (parent reads/writes share it) - ve_nl: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nl_name) - heap_typed: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, var_storage, mlirlite.Ptr(pool, nl_load_ty)) - if ve_nl is not None and heap_typed is not None: ve_nl.Alloca = heap_typed - # Record into the global map (visible to later layers/parent) - gv_ref_new: mlirlite.Value* = None - if gname_ck is not None and self._nl_heap_global_exists(mod, gname_ck) == 0: - gv_new: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, gname_ck, i8_ptr_ty) - if gv_new is not None: - # Clear external linkage + zero-init (prevents undefined reference) - gv_new.Linkage = None - gv_new.Initializer = "null" - mlirlite.module_add_global(mod, gv_new) - gv_ref_new = self._nl_heap_global_ref(pool, trans, nl_name) - if gv_ref_new is not None: mlirlite.build_store(parent_builder, var_storage, gv_ref_new) - heap_store = var_storage - else: - # In the parent this variable is nonlocal (multi-level nesting): read the heap - # address from the parent env (chained sharing), do not allocate. - heap_store = self.Gen.StmtGenDecl._load_raw_var_ptr(trans, nl_name) - if heap_store is None: continue - # env[ni] = heap storage address (shared) - offset_val: mlirlite.Value* = mlirlite.const_int32(pool, ni * 8) - slot: mlirlite.Value* = mlirlite.build_gep(parent_builder, i8_ty, env_ptr, offset_val) - if slot is not None: - slot_typed: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, slot, i8_ptr_ptr_ty) - if slot_typed is not None: mlirlite.build_store(parent_builder, heap_store, slot_typed) - - # 13b. Create the closure struct: malloc(16) - closure_size_val: mlirlite.Value* = mlirlite.const_int64(pool, 16) - if closure_size_val is not None: closure_size_val.Next = None - closure_ptr: mlirlite.Value* = mlirlite.build_call(parent_builder, "malloc", closure_size_val, 1, i8_ptr_ty, 0) - if closure_ptr is None: - # malloc failed, restore state - trans._cur_func_name = old_func_name - trans._global_name_count = old_global_count - trans._nonlocal_name_count = old_nonlocal_count - trans._closure_env_count = old_env_count - trans._nonlocal_names = saved_nl_names - trans._nonlocal_types = saved_nl_types - return 0 - - # 13c. Store fn_ptr at offset 0; create the function pointer type i32(i8*)*. - fn_param_node: mlirlite.ParamNode* = mlirlite.new_param_node(pool, i8_ptr_ty) - fn_func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, i32_ty, fn_param_node, 1) - fn_func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, fn_func_ty) - - # Create the function reference Value (type i32(i8*)*, using the SHA1 mangled name) - fn_ptr_name: str = pool.alloc(64) - if fn_ptr_name is not None: - # SHA1-prefixed names contain '.' so they need quoting (e.g. @"sha1.__closure_func") - if string.strchr(mangled_promoted, '.') is not None: viperlib.snprintf(fn_ptr_name, 64, "@\"%s\"", mangled_promoted) - else: viperlib.snprintf(fn_ptr_name, 64, "@%s", mangled_promoted) - fn_ptr_val: mlirlite.Value* = mlirlite.SSAValue(pool, fn_func_ptr_ty, fn_ptr_name) - - # bitcast the function pointer to i8* (the closure stores i8*) - fn_as_i8ptr: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, fn_ptr_val, i8_ptr_ty) - fn_slot: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, closure_ptr, mlirlite.Ptr(pool, i8_ptr_ty)) - if fn_slot is not None and fn_as_i8ptr is not None: mlirlite.build_store(parent_builder, fn_as_i8ptr, fn_slot) - - # 13d. Store env_ptr at offset 8 - eight_val: mlirlite.Value* = mlirlite.const_int32(pool, 8) - env_slot_addr: mlirlite.Value* = mlirlite.build_gep(parent_builder, i8_ty, closure_ptr, eight_val) - if env_slot_addr is not None: - env_slot: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, env_slot_addr, mlirlite.Ptr(pool, i8_ptr_ty)) - if env_slot is not None: - if env_ptr is not None: mlirlite.build_store(parent_builder, env_ptr, env_slot) - else: - # No nonlocal variables, store null - null_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "null") - mlirlite.build_store(parent_builder, null_val, env_slot) - - # 13e. Store the closure pointer into the parent's local variable {func_name} - closure_alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(parent_builder, i8_ptr_ty) - if closure_alloca is not None: - mlirlite.build_store(parent_builder, closure_ptr, closure_alloca) - self.Gen.VarGen.define_var(trans.SymTab, func_name, closure_alloca) - # Register the closure variable name so calls can tell a closure from a plain function - # pointer variable (both allocas are i8**, which _is_func_ptr_alloca cannot distinguish). - self.Gen.CallGenLeafKit.register_closure_name(func_name) - - # 14. Restore scope names (including the nonlocal set pointers) - trans._cur_func_name = old_func_name - trans._global_name_count = old_global_count - trans._nonlocal_name_count = old_nonlocal_count - trans._closure_env_count = old_env_count - trans._nonlocal_names = saved_nl_names - trans._nonlocal_types = saved_nl_types - - return 1 - - # _closure_scan_captures - recursively register implicit captures of the nested body. - # Order must match step 13's env build; runs before body translation, so it is stable. - def _closure_scan_captures(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Recursively register implicit captures in a nested body as nonlocal""" - if node is None or trans is None: return 1 - k: int = node.kind() - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm.id is None: return 1 - # Defined in the current scope (param / AnnAssign local, incl. branches) - not a capture - if self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) is not None: return 1 - # Already a registered nonlocal (explicit statement / already captured) - skip - if GenApi.is_nonlocal_name(trans, nm.id) != 0: return 1 - ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) - if ve is None or ve.Alloca is None or ve.Alloca.Ty is None: return 1 - nl_ty: mlirlite.LLVMType* = ve.Alloca.Ty.Pointee - if nl_ty is None: return 1 - # Module-level globals resolve directly, no env capture needed - mod_gv: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) - if mod_gv is not None: - if uint64(mod_gv) == uint64(ve.Alloca): return 1 - # Outer-function local -> implicit capture (register as nonlocal) - GenApi.add_nonlocal_name(trans, nm.id, nl_ty) - return 1 - # ---- Statements ---- - if k == ast.ASTKind.Assign: - asn: ast.Assign* = ast.Assign*(node) - if asn.value is not None: self._closure_scan_captures(trans, asn.value) - return 1 - if k == ast.ASTKind.AnnAssign: - aan: ast.AnnAssign* = ast.AnnAssign*(node) - if aan.value is not None: self._closure_scan_captures(trans, aan.value) - return 1 - if k == ast.ASTKind.AugAssign: - aun: ast.AugAssign* = ast.AugAssign*(node) - # The target is also read (reads the old value), so it participates in capture - if aun.target is not None: self._closure_scan_captures(trans, aun.target) - if aun.value is not None: self._closure_scan_captures(trans, aun.value) - return 1 - if k == ast.ASTKind.If: - ifn: ast.If* = ast.If*(node) - if ifn.test is not None: self._closure_scan_captures(trans, ifn.test) - if ifn.children is not None: - bn_if: sizet = len(ifn.children) - for bi_if in range(int(bn_if)): self._closure_scan_captures(trans, ifn.children.get(bi_if)) - if ifn.orelse is not None: - bn_el: sizet = len(ifn.orelse) - for bi_el in range(int(bn_el)): self._closure_scan_captures(trans, ifn.orelse.get(bi_el)) - return 1 - if k == ast.ASTKind.For: - for_n: ast.For* = ast.For*(node) - # The loop target is a new local, not captured - if for_n.iter is not None: self._closure_scan_captures(trans, for_n.iter) - if for_n.children is not None: - bn_f: sizet = len(for_n.children) - for bi_f in range(int(bn_f)): self._closure_scan_captures(trans, for_n.children.get(bi_f)) - if for_n.orelse is not None: - bn_fe: sizet = len(for_n.orelse) - for bi_fe in range(int(bn_fe)): self._closure_scan_captures(trans, for_n.orelse.get(bi_fe)) - return 1 - if k == ast.ASTKind.While: - wh_n: ast.While* = ast.While*(node) - if wh_n.test is not None: self._closure_scan_captures(trans, wh_n.test) - if wh_n.children is not None: - bn_w: sizet = len(wh_n.children) - for bi_w in range(int(bn_w)): self._closure_scan_captures(trans, wh_n.children.get(bi_w)) - if wh_n.orelse is not None: - bn_we: sizet = len(wh_n.orelse) - for bi_we in range(int(bn_we)): self._closure_scan_captures(trans, wh_n.orelse.get(bi_we)) - return 1 - if k == ast.ASTKind.Return: - rt_n: ast.Return* = ast.Return*(node) - if rt_n.value is not None: self._closure_scan_captures(trans, rt_n.value) - return 1 - if k == ast.ASTKind.Expr: - ex_n: ast.Expr* = ast.Expr*(node) - if ex_n.value is not None: self._closure_scan_captures(trans, ex_n.value) - return 1 - if k == ast.ASTKind.With: - wth: ast.With* = ast.With*(node) - if wth.items is not None: - bn_wi: sizet = len(wth.items) - for bi_wi in range(int(bn_wi)): - wi_n: ast.AST* = wth.items.get(bi_wi) - if wi_n is not None and wi_n.kind() == ast.ASTKind.WithItem: - wi_c: ast.WithItem* = ast.WithItem*(wi_n) - if wi_c.context_expr is not None: self._closure_scan_captures(trans, wi_c.context_expr) - if wth.children is not None: - bn_wb: sizet = len(wth.children) - for bi_wb in range(int(bn_wb)): self._closure_scan_captures(trans, wth.children.get(bi_wb)) - return 1 - if k == ast.ASTKind.Raise: - rs_n: ast.Raise* = ast.Raise*(node) - if rs_n.exc is not None: self._closure_scan_captures(trans, rs_n.exc) - if rs_n.cause is not None: self._closure_scan_captures(trans, rs_n.cause) - return 1 - if k == ast.ASTKind.Assert: - as_n: ast.Assert* = ast.Assert*(node) - if as_n.test is not None: self._closure_scan_captures(trans, as_n.test) - if as_n.msg is not None: self._closure_scan_captures(trans, as_n.msg) - return 1 - if k == ast.ASTKind.Try: - tr_n: ast.Try* = ast.Try*(node) - if tr_n.children is not None: - bn_t: sizet = len(tr_n.children) - for bi_t in range(int(bn_t)): self._closure_scan_captures(trans, tr_n.children.get(bi_t)) - if tr_n.handlers is not None: - bn_th: sizet = len(tr_n.handlers) - for bi_th in range(int(bn_th)): - eh_n: ast.AST* = tr_n.handlers.get(bi_th) - if eh_n is not None and eh_n.kind() == ast.ASTKind.ExceptHandler: - eh_c: ast.ExceptHandler* = ast.ExceptHandler*(eh_n) - if eh_c.children is not None: - bn_ehb: sizet = len(eh_c.children) - for bi_ehb in range(int(bn_ehb)): self._closure_scan_captures(trans, eh_c.children.get(bi_ehb)) - if tr_n.orelse is not None: - bn_to: sizet = len(tr_n.orelse) - for bi_to in range(int(bn_to)): self._closure_scan_captures(trans, tr_n.orelse.get(bi_to)) - if tr_n.finalbody is not None: - bn_tf: sizet = len(tr_n.finalbody) - for bi_tf in range(int(bn_tf)): self._closure_scan_captures(trans, tr_n.finalbody.get(bi_tf)) - return 1 - if k == ast.ASTKind.Match: - mt_n: ast.Match* = ast.Match*(node) - if mt_n.subject is not None: self._closure_scan_captures(trans, mt_n.subject) - if mt_n.cases is not None: - bn_mc: sizet = len(mt_n.cases) - for bi_mc in range(int(bn_mc)): - mc_n: ast.AST* = mt_n.cases.get(bi_mc) - if mc_n is not None and mc_n.kind() == ast.ASTKind.MatchCase: - mc_c: ast.MatchCase* = ast.MatchCase*(mc_n) - if mc_c.guard is not None: self._closure_scan_captures(trans, mc_c.guard) - if mc_c.children is not None: - bn_mcb: sizet = len(mc_c.children) - for bi_mcb in range(int(bn_mcb)): self._closure_scan_captures(trans, mc_c.children.get(bi_mcb)) - return 1 - # ---- Expressions ---- - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(node) - if bop.left is not None: self._closure_scan_captures(trans, bop.left) - if bop.right is not None: self._closure_scan_captures(trans, bop.right) - return 1 - if k == ast.ASTKind.UnaryOp: - uop: ast.UnaryOp* = ast.UnaryOp*(node) - if uop.operand is not None: self._closure_scan_captures(trans, uop.operand) - return 1 - if k == ast.ASTKind.BoolOp: - bop2: ast.BoolOp* = ast.BoolOp*(node) - if bop2.values is not None: - bn_bo: sizet = len(bop2.values) - for bi_bo in range(int(bn_bo)): self._closure_scan_captures(trans, bop2.values.get(bi_bo)) - return 1 - if k == ast.ASTKind.Compare: - cmp: ast.Compare* = ast.Compare*(node) - if cmp.left is not None: self._closure_scan_captures(trans, cmp.left) - if cmp.comparators is not None: - bn_cmp: sizet = len(cmp.comparators) - for bi_cmp in range(int(bn_cmp)): self._closure_scan_captures(trans, cmp.comparators.get(bi_cmp)) - return 1 - if k == ast.ASTKind.Call: - cal: ast.Call* = ast.Call*(node) - if cal.func is not None: self._closure_scan_captures(trans, cal.func) - if cal.args is not None: - bn_ca: sizet = len(cal.args) - for bi_ca in range(int(bn_ca)): self._closure_scan_captures(trans, cal.args.get(bi_ca)) - if cal.keywords is not None: - bn_ck: sizet = len(cal.keywords) - for bi_ck in range(int(bn_ck)): - kw_n: ast.AST* = cal.keywords.get(bi_ck) - if kw_n is not None and kw_n.kind() == ast.ASTKind.Keyword: - kw_c: ast.Keyword* = ast.Keyword*(kw_n) - if kw_c.value is not None: self._closure_scan_captures(trans, kw_c.value) - return 1 - if k == ast.ASTKind.Subscript: - sub: ast.Subscript* = ast.Subscript*(node) - if sub.value is not None: self._closure_scan_captures(trans, sub.value) - if sub.slice is not None: self._closure_scan_captures(trans, sub.slice) - return 1 - if k == ast.ASTKind.Attribute: - at_n: ast.Attribute* = ast.Attribute*(node) - if at_n.value is not None: self._closure_scan_captures(trans, at_n.value) - return 1 - if k == ast.ASTKind.IfExp: - ie: ast.IfExp* = ast.IfExp*(node) - if ie.test is not None: self._closure_scan_captures(trans, ie.test) - if ie.body is not None: self._closure_scan_captures(trans, ie.body) - if ie.orelse is not None: self._closure_scan_captures(trans, ie.orelse) - return 1 - if k == ast.ASTKind.Dict: - dc: ast.Dict* = ast.Dict*(node) - if dc.keys is not None: - bn_dk: sizet = len(dc.keys) - for bi_dk in range(int(bn_dk)): self._closure_scan_captures(trans, dc.keys.get(bi_dk)) - if dc.values is not None: - bn_dv: sizet = len(dc.values) - for bi_dv in range(int(bn_dv)): self._closure_scan_captures(trans, dc.values.get(bi_dv)) - return 1 - if k == ast.ASTKind.Set: - st_n: ast.Set* = ast.Set*(node) - if st_n.elts is not None: - bn_set: sizet = len(st_n.elts) - for bi_set in range(int(bn_set)): self._closure_scan_captures(trans, st_n.elts.get(bi_set)) - return 1 - if k == ast.ASTKind.List: - ls_n: ast.List* = ast.List*(node) - if ls_n.elts is not None: - bn_ls: sizet = len(ls_n.elts) - for bi_ls in range(int(bn_ls)): self._closure_scan_captures(trans, ls_n.elts.get(bi_ls)) - return 1 - if k == ast.ASTKind.Tuple: - tp_n: ast.Tuple* = ast.Tuple*(node) - if tp_n.elts is not None: - bn_tp: sizet = len(tp_n.elts) - for bi_tp in range(int(bn_tp)): self._closure_scan_captures(trans, tp_n.elts.get(bi_tp)) - return 1 - if k == ast.ASTKind.Starred: - sr_n: ast.Starred* = ast.Starred*(node) - if sr_n.value is not None: self._closure_scan_captures(trans, sr_n.value) - return 1 - if k == ast.ASTKind.NamedExpr: - ne_n: ast.NamedExpr* = ast.NamedExpr*(node) - if ne_n.value is not None: self._closure_scan_captures(trans, ne_n.value) - return 1 - if k == ast.ASTKind.Slice: - sl_n: ast.Slice* = ast.Slice*(node) - if sl_n.lower is not None: self._closure_scan_captures(trans, sl_n.lower) - if sl_n.upper is not None: self._closure_scan_captures(trans, sl_n.upper) - if sl_n.step is not None: self._closure_scan_captures(trans, sl_n.step) - return 1 - if k == ast.ASTKind.JoinedStr: - js_n: ast.JoinedStr* = ast.JoinedStr*(node) - if js_n.values is not None: - bn_js: sizet = len(js_n.values) - for bi_js in range(int(bn_js)): self._closure_scan_captures(trans, js_n.values.get(bi_js)) - return 1 - if k == ast.ASTKind.FormattedValue: - fv_n: ast.FormattedValue* = ast.FormattedValue*(node) - if fv_n.value is not None: self._closure_scan_captures(trans, fv_n.value) - if fv_n.format_spec is not None: self._closure_scan_captures(trans, fv_n.format_spec) - return 1 - if k == ast.ASTKind.Await: - aw_n: ast.Await* = ast.Await*(node) - if aw_n.value is not None: self._closure_scan_captures(trans, aw_n.value) - return 1 - if k == ast.ASTKind.Yield: - yd_n: ast.Yield* = ast.Yield*(node) - if yd_n.value is not None: self._closure_scan_captures(trans, yd_n.value) - return 1 - if k == ast.ASTKind.YieldFrom: - yf_n: ast.YieldFrom* = ast.YieldFrom*(node) - if yf_n.value is not None: self._closure_scan_captures(trans, yf_n.value) - return 1 - is_comp: int = 0 - if k == ast.ASTKind.ListComp: is_comp = 1 - elif k == ast.ASTKind.SetComp: is_comp = 1 - elif k == ast.ASTKind.GeneratorExp: is_comp = 1 - if is_comp != 0: - lc_n: ast.ListComp* = ast.ListComp*(node) - if lc_n.elt is not None: self._closure_scan_captures(trans, lc_n.elt) - if lc_n.generators is not None: - bn_gen: sizet = len(lc_n.generators) - for bi_gen in range(int(bn_gen)): - cp_n: ast.AST* = lc_n.generators.get(bi_gen) - if cp_n is not None and cp_n.kind() == ast.ASTKind.Comprehension: - cp_c: ast.Comprehension* = ast.Comprehension*(cp_n) - if cp_c.iter is not None: self._closure_scan_captures(trans, cp_c.iter) - if cp_c.ifs is not None: - bn_cpi: sizet = len(cp_c.ifs) - for bi_cpi in range(int(bn_cpi)): self._closure_scan_captures(trans, cp_c.ifs.get(bi_cpi)) - return 1 - if k == ast.ASTKind.DictComp: - dcn: ast.DictComp* = ast.DictComp*(node) - if dcn.key is not None: self._closure_scan_captures(trans, dcn.key) - if dcn.value is not None: self._closure_scan_captures(trans, dcn.value) - if dcn.generators is not None: - bn_dgen: sizet = len(dcn.generators) - for bi_dgen in range(int(bn_dgen)): - cp_dn: ast.AST* = dcn.generators.get(bi_dgen) - if cp_dn is not None and cp_dn.kind() == ast.ASTKind.Comprehension: - cp_dc: ast.Comprehension* = ast.Comprehension*(cp_dn) - if cp_dc.iter is not None: self._closure_scan_captures(trans, cp_dc.iter) - if cp_dc.ifs is not None: - bn_dcpi: sizet = len(cp_dc.ifs) - for bi_dcpi in range(int(bn_dcpi)): self._closure_scan_captures(trans, cp_dc.ifs.get(bi_dcpi)) - return 1 - return 1 - - - # _type_byte_size - compute an LLVM type's byte size recursively (for nonlocal shared - # heap: arrays must not be fixed at 8). `case _` returns 8 because size is unknown. - def _type_byte_size(self, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> int: - """Compute an LLVM type's byte size recursively""" - if ty is None: - # [S4 fail-fast] Missing type -> no longer assume 8 bytes (under-allocation overruns neighbors) - self.Gen.TypeGenErr.fatal_error(None, "nonlocal shared storage: type missing, cannot compute size", "TypeError") - return 0 - match ty: - case mlirlite.LLVMType.Int(bits): - sz_i: int = (bits + 7) // 8 - if sz_i < 8: sz_i = 8 - return sz_i - case mlirlite.LLVMType.Ptr(_pe): - return 8 - case mlirlite.LLVMType.Float(fb): - if fb == 64: return 8 - if fb >= 128: return 16 - return 4 - case mlirlite.LLVMType.Array(et, cnt): - esz: int = self._type_byte_size(pool, et) - return esz * cnt - case _: - return 8 - - # ============================================================ - # _nl_heap_global_name - global map name nlheap_{sha1}.{name} for a nonlocal heap address - def _nl_heap_global_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*, name: str) -> str: - """Build the nonlocal heap-address global map name nlheap_{sha1}.{name}""" - if pool is None or trans is None or name is None: return None - mangled: str = self.Gen.FuncGenBase._mangle_name(trans, name) - buf: str = pool.alloc(160) - if buf is not None: viperlib.snprintf(buf, 160, "nlheap_%s", mangled) - return buf - - # ============================================================ - # _nl_heap_global_ref - reference Value (i8**) for the global map variable - def _nl_heap_global_ref(self, pool: memhub.MemBuddy*, trans: HT.Translator*, name: str) -> mlirlite.Value*: - """Get a reference Value for the nonlocal heap-address global map variable""" - if pool is None or trans is None or name is None: return None - gname: str = self._nl_heap_global_name(pool, trans, name) - if gname is None: return None - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - ref_buf: str = pool.alloc(176) - if ref_buf is None: return None - viperlib.snprintf(ref_buf, 176, "@\"%s\"", gname) - return mlirlite.SSAValue(pool, mlirlite.Ptr(pool, i8_ptr_ty), ref_buf) - - # ============================================================ - # _nl_heap_global_exists - check whether the global map variable already exists - def _nl_heap_global_exists(self, mod: mlirlite.LLVMModule*, gname: str) -> int: - """Return 1 if the global map variable gname already exists in the module""" - if mod is None or gname is None: return 0 - gcur: mlirlite.GlobalVariable* = mod.GlobalHead - while gcur is not None: - if gcur.Name is not None and string.strcmp(gcur.Name, gname) == 0: return 1 - gcur = gcur.Next - return 0 - - # ============================================================ - # _collect_chain_nonlocals - recursively gather nonlocal names in the nested chain (deduped) - def _collect_chain_nonlocals(self, trans: HT.Translator*, node: ast.AST*, names: t.CPtr, count: int) -> int: - """Recursively collect all nonlocal declaration names in a nested FunctionDef chain (deduped)""" - if node is None or names is None: return count - fd: ast.FunctionDef* = ast.FunctionDef*(node) - if fd is None or fd.children is None: return count - ch: list[ast.AST*]* = fd.children - cn: sizet = len(ch) - for ci in range(int(cn)): - st: ast.AST* = ch.get(ci) - if st is None: continue - k: int = st.kind() - if k == ast.ASTKind.FunctionDef: count = self._collect_chain_nonlocals(trans, st, names, count) - elif k == ast.ASTKind.Nonlocal: - nl_node: ast.Nonlocal* = ast.Nonlocal*(st) - if nl_node is not None and nl_node.names is not None: - nl_list: list[ast.AST*]* = nl_node.names - nn: sizet = len(nl_list) - for ni in range(int(nn)): - nm_node: ast.AST* = nl_list.get(ni) - if nm_node is None: continue - nm_x: ast.Name* = ast.Name*(nm_node) - if nm_x is None or nm_x.id is None: continue - dup: int = 0 - for di in range(int(count)): - if string.strcmp((str)(uint64(names) + di * 8), nm_x.id) == 0: - dup = 1 - break - if dup == 0 and count < 64: - nm_slot: str* = str*(uint64(names) + count * 8) - nm_slot[0] = nm_x.id - count += 1 - return count - - # _prepare_chain_nonlocals - pre-scan the nested chain's nonlocals and build heaps. - # Called before translate_nested_function_def so inner closures can load from @nlheap.*. - def _prepare_chain_nonlocals(self, trans: HT.Translator*, node: ast.AST*, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, i8_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> int: - """Pre-collect nonlocals in a nested function chain and build their heaps; returns 1 on success""" - if node is None or trans is None or builder is None or mod is None: return 0 - # Build only once per function so sibling nested defs share one heap (avoids double - # malloc and double VarEntry.Alloca redirect desyncing counters). - if trans._nl_chain_done != 0: return 1 - if trans._nl_chain_names is None: - trans._nl_chain_names = pool.alloc(8 * 64) - if trans._nl_chain_names is not None: string.memset(trans._nl_chain_names, 0, 8 * 64) - if trans._nl_chain_names is None: return 0 - trans._nl_chain_count = self._collect_chain_nonlocals(trans, node, trans._nl_chain_names, 0) - trans._nl_chain_done = 1 - for ci in range(int(trans._nl_chain_count)): - nm_slot: str* = str*(uint64(trans._nl_chain_names) + ci * 8) - nl_name: str = nm_slot[0] - if nl_name is None: continue - # Build the heap only in the defining function (current top scope has an alloca) - local_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nl_name) - if local_alloca is None or local_alloca.Ty is None: continue - nl_ty: mlirlite.LLVMType* = local_alloca.Ty.Pointee - if nl_ty is None: continue - gname: str = self._nl_heap_global_name(pool, trans, nl_name) - if gname is None: continue - # Build the heap and copy the current value each time this function runs, then store - # into the module map; do not skip just because the map variable exists. - st_size: int = self._type_byte_size(pool, nl_ty) - st_size_val: mlirlite.Value* = mlirlite.const_int64(pool, st_size) - if st_size_val is not None: st_size_val.Next = None - var_storage: mlirlite.Value* = mlirlite.build_call(builder, "malloc", st_size_val, 1, i8_ptr_ty, 0) - if var_storage is None: continue - cur_val: mlirlite.Value* = mlirlite.build_load(builder, nl_ty, local_alloca) - st_slot: mlirlite.Value* = mlirlite.build_bitcast(builder, var_storage, mlirlite.Ptr(pool, nl_ty)) - if st_slot is not None and cur_val is not None: mlirlite.build_store(builder, cur_val, st_slot) - # Redirect VarEntry so the parent reads/writes the shared heap - ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nl_name) - if ve is not None: - heap_typed: mlirlite.Value* = mlirlite.build_bitcast(builder, var_storage, mlirlite.Ptr(pool, nl_ty)) - if heap_typed is not None: ve.Alloca = heap_typed - # Create/reuse the global map variable and store the heap address (cross-function visible) - gv_found: mlirlite.GlobalVariable* = None - gcur: mlirlite.GlobalVariable* = mod.GlobalHead - while gcur is not None: - if gcur.Name is not None and string.strcmp(gcur.Name, gname) == 0: - gv_found = gcur - break - gcur = gcur.Next - if gv_found is None: - gv_found = mlirlite.new_global_variable(pool, gname, i8_ptr_ty) - if gv_found is not None: - # Clear external linkage and zero-init, otherwise it prints as an external - # declaration with no definition -> link-time undefined reference. - gv_found.Linkage = None - gv_found.Initializer = "null" - mlirlite.module_add_global(mod, gv_found) - if gv_found is not None: - gv_ref: mlirlite.Value* = self._nl_heap_global_ref(pool, trans, nl_name) - if gv_ref is not None: mlirlite.build_store(builder, var_storage, gv_ref) - return 1 - - # translate_lambda - translate a lambda into a closure, same architecture as - # translate_nested_function_def: hoisted __lambda_{id}, captured heap slots, {fn,env}. - def translate_lambda(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: - """Translate a lambda expression and return the closure pointer (i8*)""" - if node is None or trans is None or builder is None: return None - lam: ast.Lambda* = ast.Lambda*(node) - if lam is None: return None - - i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - i8_ptr_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty) - - # 1. Lambda id + promoted function name. The counter lives on trans (_lambda_count): - # a new self field would crash TPC's Mixin class_members lookup (FuncGen.py L437). - lambda_id: int = trans._lambda_count - trans._lambda_count += 1 - pname_buf: str = pool.alloc(64) - if pname_buf is None: return None - viperlib.snprintf(pname_buf, 64, "__lambda_%d", lambda_id) - mangled_promoted: str = self.Gen.FuncGenBase._mangle_name(trans, pname_buf) - - # 2. Resolve params (name + type; default i32, resolved from the annotation) - param_names: str = pool.alloc(32 * 64) # 64 params x 32-byte names - if param_names is None: return None - string.memset(param_names, 0, 32 * 64) - param_tys: t.CPtr = pool.alloc(8 * 64) # LLVMType pointer slots - if param_tys is None: return None - string.memset(param_tys, 0, 8 * 64) - param_count: int = 0 - if lam.args is not None: - ags: ast.Arguments* = ast.Arguments*(lam.args) - if ags.args is not None: - alist: list[ast.AST*]* = ags.args - pn: sizet = len(alist) - for pi in range(int(pn)): - if param_count >= 64: break - arg_n: ast.Arg* = ast.Arg*(alist.get(pi)) - if arg_n is None or arg_n.arg is None: continue - string.strncpy(param_names + param_count * 32, arg_n.arg, 31) - p_ty: mlirlite.LLVMType* = i32_ty - if arg_n.annotation is not None: - resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg_n.annotation, trans._imported_modules, trans._from_imports, trans, 0) - if resolved is None: - # fail-fast: annotated lambda param failed to resolve; do not silently keep i32 - self.Gen.TypeGenErr.fatal_error(arg_n, f"argument '{arg_n.arg}' failed to resolve type: {self.Gen.TypeGenResolve.annotation_to_str(arg_n.annotation)}", "NameError") - resolved = i32_ty # unreachable (fatal_error already exited) - if resolved is not None: - match resolved: - case mlirlite.LLVMType.Struct(_, _, _): - p_ty = mlirlite.Ptr(pool, resolved) - case mlirlite.LLVMType.Array(aelem_ty_l, _): - p_ty = mlirlite.Ptr(pool, aelem_ty_l) - case _: - p_ty = resolved - ty_slot_p: str* = str*(uint64(param_tys) + param_count * 8) - ty_slot_p[0] = p_ty - param_count += 1 - - # 3. Collect captured variables (outer Name references in the body) - cap_names: str = pool.alloc(64 * 64) # 64 captures x 64-byte names - if cap_names is None: return None - string.memset(cap_names, 0, 64 * 64) - cap_allocas: t.CPtr = pool.alloc(8 * 64) # Parent alloca pointer slots - if cap_allocas is None: return None - string.memset(cap_allocas, 0, 8 * 64) - cap_tys: t.CPtr = pool.alloc(8 * 64) # Capture variable type slots - if cap_tys is None: return None - string.memset(cap_tys, 0, 8 * 64) - cap_count: int = self._lambda_scan_body(trans, lam.body, param_names, param_count, cap_names, cap_allocas, cap_tys, 0) - - # 4. Create the promoted function __lambda_{id}(i8* %env, [args...]) - func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled_promoted, i32_ty) - if func is None: return None - mlirlite.add_param(pool, func, i8_ptr_ty, "%env") - for pi2 in range(int(param_count)): - ty_addr2: uint64 = uint64(param_tys) + pi2 * 8 - ty_slot2: str* = str*(ty_addr2) - p_ty2: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot2[0]) - if p_ty2 is None: p_ty2 = i32_ty - pname_l: str = pool.alloc(32) - if pname_l is not None: - viperlib.snprintf(pname_l, 32, "%%%s", param_names + pi2 * 32) - mlirlite.add_param(pool, func, p_ty2, pname_l) - - # 5. Entry block + builder - entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") - if entry_blk is None: return None - func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) - if func_builder is None: return None - mlirlite.position_at_end(func_builder, entry_blk) - - # --traceback: promoted lambda gets its own DISubprogram. - dbg_pushed_l: int = DebugInfoGen.PushFunction(pool, func_builder, func, node.lineno, node.col_offset + 1) - - # 6. Enter the lambda scope - self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_FUNCTION) - - # 7. _env_ptr alloca (stores the %env parameter) - env_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, i8_ptr_ty) - if env_alloca is not None: - self.Gen.VarGen.define_var(trans.SymTab, "_env_ptr", env_alloca) - env_param_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "%env") - mlirlite.build_store(func_builder, env_param_val, env_alloca) - - # 8. Save parent state and switch to the promoted function - old_func: mlirlite.Function* = trans._cur_func - old_builder: mlirlite.IRBuilder* = trans._cur_builder - old_func_name: str = trans._cur_func_name - old_global_count: int = trans._global_name_count - old_nonlocal_count: int = trans._nonlocal_name_count - old_env_count: int = trans._closure_env_count - # break/continue targets and match-case flag must be cleared for the new scope (see - # assignments below); otherwise a lambda break/continue inherits the outer target. - old_break_bb: mlirlite.BasicBlock* = trans._break_bb - old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb - old_insw: int = trans._in_switch_case - trans._cur_func = func - trans._cur_builder = func_builder - trans._cur_func_name = mangled_promoted - # P2 willFree 失效表:函数作用域复位(栈槽地址会被后续函数复用,跨函数保留会误报) - self.Gen.VarGen.freed_clear() - GenApi.clear_scope_names(trans) - trans._break_bb = None - trans._continue_bb = None - trans._in_switch_case = 0 - - # 9. Captured variables: load values from env heap slots into local allocas - for ci in range(int(cap_count)): - # Take the capture name via str pointer offset (like param_names + pi*32). Do not use - # (str)(cap_addr): TPV compiles that to itoa, corrupting define_var keys. - cap_slot: str = cap_names + ci * 64 - if cap_slot[0] == '\0': continue - ty_addr3: uint64 = uint64(cap_tys) + ci * 8 - ty_slot3: str* = (str*)(ty_addr3) - cap_ty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot3[0]) - if cap_ty is None: continue - off_val: mlirlite.Value* = mlirlite.const_int32(pool, ci * 8) - slot_addr: mlirlite.Value* = mlirlite.build_gep(func_builder, i8_ty, env_param_val, off_val) - if slot_addr is None: continue - storage_addr: mlirlite.Value* = mlirlite.build_load(func_builder, i8_ptr_ty, slot_addr) - if storage_addr is None: continue - st_typed: mlirlite.Value* = mlirlite.build_bitcast(func_builder, storage_addr, mlirlite.Ptr(pool, cap_ty)) - if st_typed is None: continue - cap_val: mlirlite.Value* = mlirlite.build_load(func_builder, cap_ty, st_typed) - if cap_val is None: continue - var_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, cap_ty) - if var_alloca is not None: - mlirlite.build_store(func_builder, cap_val, var_alloca) - self.Gen.VarGen.define_var(trans.SymTab, cap_slot, var_alloca) - - # 10. Parameter allocas - for pi3 in range(int(param_count)): - ty_addr4: uint64 = uint64(param_tys) + pi3 * 8 - ty_slot4: str* = (str*)(ty_addr4) - p_ty3: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot4[0]) - if p_ty3 is None: - # fail-fast: lambda param type slot missing (inconsistent state); do not keep i32 - self.Gen.TypeGenErr.fatal_error(lam, f"lambda param '{param_names + pi3 * 32}' type info missing", "InternalCompilerError") - p_ty3 = i32_ty # unreachable (fatal_error already exited) - pname_ref: str = pool.alloc(32) - if pname_ref is not None: - viperlib.snprintf(pname_ref, 32, "%%%s", param_names + pi3 * 32) - param_ref: mlirlite.Value* = mlirlite.SSAValue(pool, p_ty3, pname_ref) - arg_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, p_ty3) - if arg_alloca is not None: - mlirlite.build_store(func_builder, param_ref, arg_alloca) - self.Gen.VarGen.define_var(trans.SymTab, param_names + pi3 * 32, arg_alloca) - - # 11. Translate the body expression and return it. - # 返回类型**完全跟随 body 的实际类型**:提升函数创建时(第 4 步)只能先写 i32——那时 body - # 还没翻译、拿不到类型。这里按 body 类型修正 `func.RetTy`(mlirlite 公开字段;本步之后才发 - # ret,期间没有任何人读 RetTy;调用点取签名时读到的已是修正后的类型),然后**原样返回、 - # 不再强转**。修正了原实现(一律 coerce 到 i32)的三类错法: - # · 指针 → coerce_to_type(ptr → i32) 直接 CastError(pointer→integer 禁止); - # · 浮点 → fptosi 成 i32(静默有损); - # · 更宽整数等 → trunc 成 i32(静默有损,如 i64 实参)。 - # void body(如 `lambda: `)→ 提升函数返回 void 并发 ret void - # (原来会因 "a void value cannot take part in any operation" 直接报错)。 - body_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(func_builder, pool, mod, lam.body, None, 0, trans) - if body_val is None or body_val.Ty is None: - # fail-fast: lambda body translation failed; do not silently return 0 - self.Gen.TypeGenErr.fatal_error(lam.body, "lambda body expression translation failed", "TypeError") - elif self.Gen.StmtGenDecl._is_void_type(body_val.Ty) != 0: - func.RetTy = body_val.Ty - mlirlite.build_ret_void(func_builder) - else: - func.RetTy = body_val.Ty - mlirlite.build_ret(func_builder, body_val) - - # 12. Leave the lambda scope and restore the parent state - self.Gen.VarGen.exit_scope(trans.SymTab) - trans._cur_func = old_func - trans._cur_builder = old_builder - trans._break_bb = old_break_bb - trans._continue_bb = old_continue_bb - trans._in_switch_case = old_insw - trans._cur_func_name = old_func_name - trans._global_name_count = old_global_count - trans._nonlocal_name_count = old_nonlocal_count - trans._closure_env_count = old_env_count - if dbg_pushed_l != 0: DebugInfoGen.PopFunction(func_builder) - - # 13. Create env in the parent (8-byte heap per capture, env[i] = heap address) - env_ptr: mlirlite.Value* = None - if cap_count > 0: - env_size: int64 = cap_count * 8 - env_size_val: mlirlite.Value* = mlirlite.const_int64(pool, env_size) - if env_size_val is not None: env_size_val.Next = None - env_ptr = mlirlite.build_call(builder, "malloc", env_size_val, 1, i8_ptr_ty, 0) - if env_ptr is not None: - for ci3 in range(int(cap_count)): - # Same as step 9: null-check via str pointer offset, avoiding itoa semantics - cap_slot3: str = cap_names + ci3 * 64 - if cap_slot3[0] == '\0': continue - al_addr: uint64 = uint64(cap_allocas) + ci3 * 8 - al_slot: str* = (str*)(al_addr) - var_alloca2: mlirlite.Value* = (mlirlite.Value*)(al_slot[0]) - if var_alloca2 is None: continue - ty_addr5: uint64 = uint64(cap_tys) + ci3 * 8 - ty_slot5: str* = (str*)(ty_addr5) - cap_ty2: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot5[0]) - if cap_ty2 is None: continue - # Heap size: bits/8, at least 8 bytes (fits pointers/scalars) - ty_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(cap_ty2) - st_size: int = 8 - if ty_bits > 64: st_size = ty_bits // 8 - st_size_val: mlirlite.Value* = mlirlite.const_int64(pool, st_size) - if st_size_val is not None: st_size_val.Next = None - var_storage: mlirlite.Value* = mlirlite.build_call(builder, "malloc", st_size_val, 1, i8_ptr_ty, 0) - if var_storage is not None: - cur_val: mlirlite.Value* = mlirlite.build_load(builder, cap_ty2, var_alloca2) - if cur_val is not None: - st_slot5: mlirlite.Value* = mlirlite.build_bitcast(builder, var_storage, mlirlite.Ptr(pool, cap_ty2)) - if st_slot5 is not None: mlirlite.build_store(builder, cur_val, st_slot5) - off_val2: mlirlite.Value* = mlirlite.const_int32(pool, ci3 * 8) - slot2: mlirlite.Value* = mlirlite.build_gep(builder, i8_ty, env_ptr, off_val2) - if slot2 is not None: - slot_typed2: mlirlite.Value* = mlirlite.build_bitcast(builder, slot2, i8_ptr_ptr_ty) - if slot_typed2 is not None: mlirlite.build_store(builder, var_storage, slot_typed2) - - # 14. Create the closure in the parent: malloc(16) - closure_size_val: mlirlite.Value* = mlirlite.const_int64(pool, 16) - if closure_size_val is not None: closure_size_val.Next = None - closure_ptr: mlirlite.Value* = mlirlite.build_call(builder, "malloc", closure_size_val, 1, i8_ptr_ty, 0) - if closure_ptr is None: return None - - # 15. [0] = fn_ptr (function reference bitcast to i8*) - fn_param_node: mlirlite.ParamNode* = mlirlite.new_param_node(pool, i8_ptr_ty) - fn_func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, i32_ty, fn_param_node, 1) - fn_func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, fn_func_ty) - fn_ptr_name: str = pool.alloc(64) - if fn_ptr_name is not None: - if string.strchr(mangled_promoted, '.') is not None: viperlib.snprintf(fn_ptr_name, 64, "@\"%s\"", mangled_promoted) - else: viperlib.snprintf(fn_ptr_name, 64, "@%s", mangled_promoted) - fn_ptr_val: mlirlite.Value* = mlirlite.SSAValue(pool, fn_func_ptr_ty, fn_ptr_name) - fn_as_i8ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, fn_ptr_val, i8_ptr_ty) - fn_slot: mlirlite.Value* = mlirlite.build_bitcast(builder, closure_ptr, mlirlite.Ptr(pool, i8_ptr_ty)) - if fn_slot is not None and fn_as_i8ptr is not None: mlirlite.build_store(builder, fn_as_i8ptr, fn_slot) - - # 16. [8] = env_ptr (store null when there are no captures) - eight_val: mlirlite.Value* = mlirlite.const_int32(pool, 8) - env_slot_addr: mlirlite.Value* = mlirlite.build_gep(builder, i8_ty, closure_ptr, eight_val) - if env_slot_addr is not None: - env_slot: mlirlite.Value* = mlirlite.build_bitcast(builder, env_slot_addr, mlirlite.Ptr(pool, i8_ptr_ty)) - if env_slot is not None: - if env_ptr is not None: mlirlite.build_store(builder, env_ptr, env_slot) - else: - null_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "null") - mlirlite.build_store(builder, null_val, env_slot) - - # 17. Return the closure pointer (i8*) - return closure_ptr - - # ============================================================ - # _lambda_scan_body - recursively collect outer Name refs into cap_names/cap_allocas/cap_tys - def _lambda_scan_body(self, trans: HT.Translator*, node: ast.AST*, param_names: str, param_count: int, cap_names: str, cap_allocas: t.CPtr, cap_tys: t.CPtr, cap_count: int) -> int: - """Recursively scan lambda body Name references and collect captured variables""" - if node is None or trans is None: return cap_count - k: int = node.kind() - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm.id is None: return cap_count - # Skip lambda parameters - for pi in range(int(param_count)): - if string.strcmp(param_names + pi * 32, nm.id) == 0: return cap_count - # Skip already-captured names - for ci in range(int(cap_count)): - if string.strcmp(cap_names + ci * 64, nm.id) == 0: return cap_count - # Look up the current scope chain - alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) - if alloca is None or alloca.Ty is None: return cap_count - pointee: mlirlite.LLVMType* = alloca.Ty.Pointee - if pointee is None: return cap_count - # Skip module-level globals (the body resolves them directly) - mod_gv: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) - if mod_gv is not None: - if uint64(mod_gv) == uint64(alloca): return cap_count - if cap_count >= 64: return cap_count - string.strncpy(cap_names + cap_count * 64, nm.id, 63) - al_slot: str* = (str*)(uint64(cap_allocas) + cap_count * 8) - al_slot[0] = alloca - ty_slot: str* = (str*)(uint64(cap_tys) + cap_count * 8) - ty_slot[0] = pointee - return cap_count + 1 - # Recurse into common expression children - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(node) - cc1: int = self._lambda_scan_body(trans, bop.left, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - return self._lambda_scan_body(trans, bop.right, param_names, param_count, cap_names, cap_allocas, cap_tys, cc1) - if k == ast.ASTKind.UnaryOp: - uop: ast.UnaryOp* = ast.UnaryOp*(node) - return self._lambda_scan_body(trans, uop.operand, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - if k == ast.ASTKind.BoolOp: - bop2: ast.BoolOp* = ast.BoolOp*(node) - cc2: int = cap_count - if bop2.values is not None: - vs: list[ast.AST*]* = bop2.values - vn: sizet = len(vs) - for vi in range(int(vn)): - cc2 = self._lambda_scan_body(trans, vs.get(vi), param_names, param_count, cap_names, cap_allocas, cap_tys, cc2) - return cc2 - if k == ast.ASTKind.Compare: - cmp: ast.Compare* = ast.Compare*(node) - cc3: int = self._lambda_scan_body(trans, cmp.left, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - if cmp.comparators is not None: - cl: list[ast.AST*]* = cmp.comparators - cn: sizet = len(cl) - for cvi in range(int(cn)): - cc3 = self._lambda_scan_body(trans, cl.get(cvi), param_names, param_count, cap_names, cap_allocas, cap_tys, cc3) - return cc3 - if k == ast.ASTKind.Call: - cal: ast.Call* = ast.Call*(node) - cc4: int = self._lambda_scan_body(trans, cal.func, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - if cal.args is not None: - als: list[ast.AST*]* = cal.args - an: sizet = len(als) - for ai in range(int(an)): - cc4 = self._lambda_scan_body(trans, als.get(ai), param_names, param_count, cap_names, cap_allocas, cap_tys, cc4) - if cal.keywords is not None: - kws: list[ast.AST*]* = cal.keywords - kn: sizet = len(kws) - for ki in range(int(kn)): - kw: ast.Keyword* = ast.Keyword*(kws.get(ki)) - if kw is not None and kw.value is not None: - cc4 = self._lambda_scan_body(trans, kw.value, param_names, param_count, cap_names, cap_allocas, cap_tys, cc4) - return cc4 - if k == ast.ASTKind.Subscript: - sub: ast.Subscript* = ast.Subscript*(node) - cc5: int = self._lambda_scan_body(trans, sub.value, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - if sub.slice is not None: - cc5 = self._lambda_scan_body(trans, sub.slice, param_names, param_count, cap_names, cap_allocas, cap_tys, cc5) - return cc5 - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(node) - return self._lambda_scan_body(trans, at.value, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - if k == ast.ASTKind.IfExp: - ie: ast.IfExp* = ast.IfExp*(node) - cc6: int = self._lambda_scan_body(trans, ie.test, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - cc6 = self._lambda_scan_body(trans, ie.body, param_names, param_count, cap_names, cap_allocas, cap_tys, cc6) - return self._lambda_scan_body(trans, ie.orelse, param_names, param_count, cap_names, cap_allocas, cap_tys, cc6) - if k == ast.ASTKind.List: - ls: ast.List* = ast.List*(node) - cc7: int = cap_count - if ls.elts is not None: - es: list[ast.AST*]* = ls.elts - en: sizet = len(es) - for ei in range(int(en)): - cc7 = self._lambda_scan_body(trans, es.get(ei), param_names, param_count, cap_names, cap_allocas, cap_tys, cc7) - return cc7 - if k == ast.ASTKind.Lambda: - lam2: ast.Lambda* = ast.Lambda*(node) - return self._lambda_scan_body(trans, lam2.body, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) - return cap_count +import lib.core.Gen.GenTypes as GenTypes +import t, c +from stdint import * +import ast +import mlirlite +import memhub +import string +import viperlib +import stdio +import stdlib +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HT +import lib.core.Gen.Call.CallGenBase as CallGenBase +import lib.core.Gen.GenApi as GenApi +import lib.core.Gen.DebugInfoGen as DebugInfoGen + + +# ============================================================ +# FuncGenMixin - function/method declaration and translation Mixin + +# Scope type constant (uncertain) +define SCOPE_FUNCTION = 1 + + +# ============================================================ +# FuncGenMixin - function attribute/naming/declaration/translation +novirtual class FuncGenMixin(CallGenBase.CallGenBase): + """Function/method translation Mixin (stateless)""" + + # translate_nested_function_def - hoist a nested function to a top-level + # @__closure_{name}(i8* %env) and build its closure {fn_ptr, env_ptr} in the parent. + def translate_nested_function_def(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Translate a nested function definition: hoist to a top-level function and build a closure""" + fd: ast.FunctionDef* = ast.FunctionDef*(node) + if fd is None or fd.name is None: return 0 + + pool: memhub.MemBuddy* = trans.Pool + mod: mlirlite.LLVMModule* = trans.Module + func_name: str = fd.name + + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + + # 1. Build the promoted name __closure_{name} (SHA1 prefix avoids cross-module clashes) + promoted_name: str = pool.alloc(64) + if promoted_name is not None: viperlib.snprintf(promoted_name, 64, "__closure_%s", func_name) + mangled_promoted: str = self.Gen.FuncGenBase._mangle_name(trans, promoted_name) + + # 2. Create the promoted function: define @__closure_{name}(i8* %env, args...) + # 返回类型**跟随 `-> T` 注解**。原实现写死 i32:于是 `def Inner(p: char*) -> char*: return p` + # 生成 `define i32 @__closure_Inner(i8* %env, i8* %p)`,而 `return p` 要把 char* 隐式转 i32 + # → 撞 coerce_to_type 的 CastError(现场:EscapeMethodTest 的 (C) 用例)。那是"嵌套 def + # 只支持 int 返回"这个预先存在的缺口,此前被同文件更早的用例报错掩盖。 + # 约定(最小改动的边界): + # · `fd.returns is None` 或注解是 `None` / `t.CVoid` → **保持 i32**(不改既有 ABI, + # 避免打断现有"无返回值/无注解"的嵌套 def;这是已知缺口,不在本次范围内); + # · 其余注解采用 resolve_annotation_type 的结果;解析不出 → fail-fast,不静默兜底。 + ret_ty_n: mlirlite.LLVMType* = i32_ty + if fd.returns is not None: + keep_i32_n: int = 0 + if fd.returns.kind() == ast.ASTKind.Constant: + rc_n: ast.Constant* = ast.Constant*(fd.returns) + if rc_n is not None and rc_n.const_kind == ast.CONST_NONE: keep_i32_n = 1 + if self.Gen.TypeGenResolve._is_cvoid_marker(fd.returns, trans._imported_modules) != 0: keep_i32_n = 1 + if keep_i32_n == 0: + rt_res_n: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, trans._imported_modules, trans._from_imports, trans, 0) + if rt_res_n is None: + self.Gen.TypeGenErr.fatal_error(fd, f"nested function '{fd.name}': cannot resolve the return type annotation for the promoted closure", "TypeError") + else: + rt_is_void_n: int = 0 + match rt_res_n: + case mlirlite.LLVMType.Void(): + rt_is_void_n = 1 + case _: + pass + if rt_is_void_n == 0: ret_ty_n = rt_res_n + func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled_promoted, ret_ty_n) + if func is None: return 0 + mlirlite.add_param(pool, func, i8_ptr_ty, "%env") + + # 2a. Nested function params join the promoted signature (i8* %env, args...); + # types go into the param_tys buffer (CVoid skipped, aligning with alloca indices). + imported_modules: str = trans._imported_modules + from_imports: str = trans._from_imports + param_tys: t.CPtr = pool.alloc(8 * 64) + if param_tys is not None: string.memset(param_tys, 0, 8 * 64) + param_count: int = 0 + if fd.args is not None: + ags_n: ast.Arguments* = ast.Arguments*(fd.args) + if ags_n.args is not None: + alist_n: list[ast.AST*]* = ags_n.args + an_n: sizet = len(alist_n) + for ai_n in range(int(an_n)): + arg_n: ast.Arg* = ast.Arg*(alist_n.get(ai_n)) + if arg_n is None or arg_n.arg is None: continue + if arg_n.annotation is not None: + if self.Gen.TypeGenResolve._is_t_attr(arg_n.annotation, "CVoid", imported_modules) != 0: continue + pty_n: mlirlite.LLVMType* = i32_ty + if arg_n.annotation is not None: + resolved_n: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg_n.annotation, imported_modules, from_imports, trans, 0) + if resolved_n is not None: + match resolved_n: + case mlirlite.LLVMType.Struct(_, _, _): + pty_n = mlirlite.Ptr(pool, resolved_n) + case mlirlite.LLVMType.Array(aelem_ty_n, _): + pty_n = mlirlite.Ptr(pool, aelem_ty_n) + case _: + pty_n = resolved_n + if param_count < 64 and param_tys is not None: + pty_slot_n: str* = (str*)(uint64(param_tys) + param_count * 8) + pty_slot_n[0] = pty_n + pname_n: str = pool.alloc(32) + if pname_n is not None: + viperlib.snprintf(pname_n, 32, "%%%s", arg_n.arg) + mlirlite.add_param(pool, func, pty_n, pname_n) + param_count += 1 + + # 3. Create the entry block and builder + entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") + if entry_blk is None: return 0 + func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) + if func_builder is None: return 0 + mlirlite.position_at_end(func_builder, entry_blk) + + # --traceback: promoted nested closure gets its own DISubprogram. + dbg_pushed_n: int = DebugInfoGen.PushFunction(pool, func_builder, func, node.lineno, node.col_offset + 1) + + # 4. Enter the nested function scope (nested symbol table) + self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_FUNCTION) + + # 5. Create the _env_ptr alloca and store the %env parameter + env_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, i8_ptr_ty) + if env_alloca is not None: + self.Gen.VarGen.define_var(trans.SymTab, "_env_ptr", env_alloca) + env_param_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "%env") + mlirlite.build_store(func_builder, env_param_val, env_alloca) + + # 5a. Create alloca for nested function params so the body can reference them + # (e.g. for i in range(n) inside accumulate(n), result = val inside set_result(val)). + if fd.args is not None: + ags_p: ast.Arguments* = ast.Arguments*(fd.args) + if ags_p.args is not None: + alist_p: list[ast.AST*]* = ags_p.args + idx_p: int = 0 + apn: sizet = len(alist_p) + for api in range(int(apn)): + arg_p: ast.Arg* = ast.Arg*(alist_p.get(api)) + if arg_p is None or arg_p.arg is None: continue + if arg_p.annotation is not None: + if self.Gen.TypeGenResolve._is_t_attr(arg_p.annotation, "CVoid", imported_modules) != 0: continue + if idx_p >= param_count: break + pty2_n: mlirlite.LLVMType* = i32_ty + if param_tys is not None: + ty_addr_n: uint64 = uint64(param_tys) + idx_p * 8 + ty_slot_n: str* = (str*)(ty_addr_n) + pty2_n = (mlirlite.LLVMType*)(ty_slot_n[0]) + if pty2_n is None: pty2_n = i32_ty + p_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, pty2_n) + if p_alloca is not None: + self.Gen.VarGen.define_var(trans.SymTab, arg_p.arg, p_alloca) + if arg_p.annotation is not None: + cls_nm_p: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg_p.annotation, imported_modules, pool, trans._from_imports) + if cls_nm_p is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg_p.arg, cls_nm_p) + # const param: keep TPC's historical test (only whether the annotation contains + # t.CConst, not the pointer depth); both sides align per type-descriptors.md. + if self.Gen.TypeGenErr.has_decorator_marker(arg_p.annotation, "CConst", 0) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, arg_p.arg) + # t.CVolatile param: mark at declaration (same as the FuncGenDef main path) so + # load/store of this param name in the body carry volatile. + if self.Gen.TypeGenErr.has_decorator_marker(arg_p.annotation, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, arg_p.arg) + # rawptr[t.CVolatile | T] param (volatile pointee): dereferences in the body carry + # volatile (same as the FuncGenDef main path). + if self.Gen.TypeGenErr.has_pointee_volatile_marker(arg_p.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, arg_p.arg) + pname2_n: str = pool.alloc(32) + if pname2_n is not None: viperlib.snprintf(pname2_n, 32, "%%%s", arg_p.arg) + p_val: mlirlite.Value* = mlirlite.SSAValue(pool, pty2_n, pname2_n) + vol_pn: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, arg_p.arg)) + mlirlite.build_store(func_builder, p_val, p_alloca, vol_pn) + idx_p += 1 + + # 6. Save the parent function scope state (variable-table independent) + old_func: mlirlite.Function* = trans._cur_func + old_builder: mlirlite.IRBuilder* = trans._cur_builder + old_func_name: str = trans._cur_func_name + old_global_count: int = trans._global_name_count + old_nonlocal_count: int = trans._nonlocal_name_count + old_env_count: int = trans._closure_env_count + # break/continue targets and match-case flag must be cleared for the new scope (see + # assignments below); otherwise a nested break/continue inherits the outer target. + old_break_bb: mlirlite.BasicBlock* = trans._break_bb + old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb + old_insw: int = trans._in_switch_case + # Separate nonlocal buffer: sharing [0] would overwrite the parent's set (ClosureTest2 + # Test 7: level2's b clobbers level1's a). Restored after translation. + saved_nl_names: t.CPtr = trans._nonlocal_names + saved_nl_types: t.CPtr = trans._nonlocal_types + + # 7. Switch to the nested function scope and clear scope names + trans._cur_func = func + trans._cur_builder = func_builder + trans._cur_func_name = func_name + # P2 willFree 失效表:函数作用域复位(栈槽地址会被后续函数复用,跨函数保留会误报) + self.Gen.VarGen.freed_clear() + GenApi.clear_scope_names(trans) + trans._break_bb = None + trans._continue_bb = None + trans._in_switch_case = 0 + # Independent nonlocal buffers for the nested function (avoid clobbering the parent set) + trans._nonlocal_names = stdlib.malloc(HT.MAX_GLOBAL_NAMES * 8) + trans._nonlocal_types = stdlib.malloc(HT.MAX_GLOBAL_NAMES * 8) + if trans._nonlocal_names is not None: string.memset(trans._nonlocal_names, 0, HT.MAX_GLOBAL_NAMES * 8) + if trans._nonlocal_types is not None: string.memset(trans._nonlocal_types, 0, HT.MAX_GLOBAL_NAMES * 8) + + # 8. Pre-scan the body: (a) Global/Nonlocal declarations (Python: whole scope), + # (b) AnnAssign locals to create their allocas early. + body: list[ast.AST*]* = fd.children + if body is not None: + bn: sizet = len(body) + for bi in range(int(bn)): + stmt: ast.AST* = body.get(bi) + if stmt is not None: self.Gen.StmtGenDecl.pre_scan_scope_decls(trans, stmt) + for bi in range(int(bn)): + stmt: ast.AST* = body.get(bi) + if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt) + + # 8c. Implicit capture scan: outer locals read by the nested function become nonlocal + # (Python semantics). Branch AnnAssign locals are already pre-registered and excluded. + if body is not None: + bnc: sizet = len(body) + for bic in range(int(bnc)): + stmtc: ast.AST* = body.get(bic) + if stmtc is not None: self._closure_scan_captures(trans, stmtc) + + # M3 owned slife:嵌套函数体走的是这条**独立**的转译路径(不经过 FuncGenDef.translate_function_def), + # 所以登记区间必须在这里自己抬升/弹回。否则内层登记会落进外层的区间 → 内层出口把外层对象 + # 一起 free(提前释放),外层出口再 free 一次 → **double free / 堆损坏**(现场:NewLifetimeTest + # 的 CaseNestedDef,STATUS_HEAP_CORRUPTION 0xC0000374)。 + old_slife_base: int = self.Gen.VarGen._SlifeReset(pool) + + # 9. Translate the body (Nonlocal statements fill _nonlocal_names) + if body is not None: + bn2: sizet = len(body) + for bi2 in range(int(bn2)): + stmt2: ast.AST* = body.get(bi2) + if stmt2 is not None: self.Gen.StmtGen.translate_stmt(trans, stmt2) + + # 10. Implicit return + last_is_return: int = 0 + if body is not None: + bn3: sizet = len(body) + if bn3 > 0: + last_stmt: ast.AST* = body.get(bn3 - 1) + if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1 + if last_is_return == 0: + # M3:隐式收尾也是函数出口,先释放本嵌套函数登记的 slife + self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans) + # 零值必须与**本函数的返回类型**一致(原实现写死 const_int32(0):一旦返回类型改为 + # `char*` 之类,`ret i32 0` 与签名不符,llc 会直接拒绝)。ret_ty_n 见第 2 步。 + zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty_n) + if zero_val is None: + self.Gen.TypeGenErr.fatal_error(fd, f"nested function '{fd.name}': flow falls off the end without an implicit return and the return type has no zero value", "TypeError") + else: + mlirlite.build_ret(func_builder, zero_val) + + # M3:弹回外层登记区间(释放已在本嵌套函数各出口完成) + self.Gen.VarGen._SlifeRestore(old_slife_base) + + # 11. Save the nested function's nonlocal info + nested_nonlocal_count: int = trans._nonlocal_name_count + + # 12. Leave the nested function scope, back to the parent (keep _nonlocal_names for env) + self.Gen.VarGen.exit_scope(trans.SymTab) + trans._cur_func = old_func + trans._cur_builder = old_builder + trans._break_bb = old_break_bb + trans._continue_bb = old_continue_bb + trans._in_switch_case = old_insw + if dbg_pushed_n != 0: DebugInfoGen.PopFunction(func_builder) + # NOTE: _nonlocal_names and _nonlocal_name_count still hold the nested function's values + + # 13. Create the closure object in the parent function + parent_builder: mlirlite.IRBuilder* = old_builder + if parent_builder is None: + # No builder (module-level nested function?) - cannot create a closure, only restore state + trans._cur_func_name = old_func_name + trans._global_name_count = old_global_count + trans._nonlocal_name_count = old_nonlocal_count + trans._closure_env_count = old_env_count + trans._nonlocal_names = saved_nl_names + trans._nonlocal_types = saved_nl_types + return 0 + + # 13a. Create env: malloc(8 * nested_nonlocal_count) - an array of pointers, one + # 8-byte i8* slot per nonlocal variable pointing at the parent's alloca (shared). + env_ptr: mlirlite.Value* = None + if nested_nonlocal_count > 0: + env_size: int64 = nested_nonlocal_count * 8 + env_size_val: mlirlite.Value* = mlirlite.const_int64(pool, env_size) + if env_size_val is not None: + env_size_val.Next = None + env_ptr = mlirlite.build_call(parent_builder, "malloc", env_size_val, 1, i8_ptr_ty, 0) + + # Build heap storage per nonlocal and register it into env (runtime env chaining, no + # compile-time map): parent local -> new heap + redirect; parent nonlocal -> reuse heap. + if env_ptr is not None: + i8_ptr_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty) + for ni in range(int(nested_nonlocal_count)): + # Read the name from the _nonlocal_names buffer + name_addr: uint64 = uint64(trans._nonlocal_names) + ni * 8 + nl_slot: str* = (str*)(name_addr) + nl_name: str = nl_slot[0] + if nl_name is None: continue + heap_store: mlirlite.Value* = None + # 1. Prefer the global heap-address map (_prepare_chain_nonlocals already built heaps + # for chain nonlocals and stored @nlheap.*; shared across layers, e.g. Test 7). + gname_ck: str = self._nl_heap_global_name(pool, trans, nl_name) + if gname_ck is not None and self._nl_heap_global_exists(mod, gname_ck) != 0: + gv_ref0: mlirlite.Value* = self._nl_heap_global_ref(pool, trans, nl_name) + if gv_ref0 is not None: heap_store = mlirlite.build_load(parent_builder, i8_ptr_ty, gv_ref0) + if heap_store is None: + local_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nl_name) + if local_alloca is not None and local_alloca.Ty is not None: + # Parent local: new heap storage + copy current value + redirect + nl_load_ty: mlirlite.LLVMType* = local_alloca.Ty.Pointee + if nl_load_ty is not None: + cur_val_nl: mlirlite.Value* = mlirlite.build_load(parent_builder, nl_load_ty, local_alloca) + # Heap size uses the type's byte size: arrays (e.g. [4 x i32]=16 bytes) must not be + # fixed at 8 (out-of-bounds write); pointers/scalars are >= 8 bytes. + st_size: int = self._type_byte_size(pool, nl_load_ty) + st_size_val: mlirlite.Value* = mlirlite.const_int64(pool, st_size) + if st_size_val is not None: st_size_val.Next = None + var_storage: mlirlite.Value* = mlirlite.build_call(parent_builder, "malloc", st_size_val, 1, i8_ptr_ty, 0) + if var_storage is not None: + # Copy the current value to heap storage + st_slot_nl: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, var_storage, mlirlite.Ptr(pool, nl_load_ty)) + if st_slot_nl is not None and cur_val_nl is not None: mlirlite.build_store(parent_builder, cur_val_nl, st_slot_nl) + # Redirect the parent's VarEntry.Alloca to the heap (parent reads/writes share it) + ve_nl: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nl_name) + heap_typed: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, var_storage, mlirlite.Ptr(pool, nl_load_ty)) + if ve_nl is not None and heap_typed is not None: ve_nl.Alloca = heap_typed + # Record into the global map (visible to later layers/parent) + gv_ref_new: mlirlite.Value* = None + if gname_ck is not None and self._nl_heap_global_exists(mod, gname_ck) == 0: + gv_new: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, gname_ck, i8_ptr_ty) + if gv_new is not None: + # Clear external linkage + zero-init (prevents undefined reference) + gv_new.Linkage = None + gv_new.Initializer = "null" + mlirlite.module_add_global(mod, gv_new) + gv_ref_new = self._nl_heap_global_ref(pool, trans, nl_name) + if gv_ref_new is not None: mlirlite.build_store(parent_builder, var_storage, gv_ref_new) + heap_store = var_storage + else: + # In the parent this variable is nonlocal (multi-level nesting): read the heap + # address from the parent env (chained sharing), do not allocate. + heap_store = self.Gen.StmtGenDecl._load_raw_var_ptr(trans, nl_name) + if heap_store is None: continue + # env[ni] = heap storage address (shared) + offset_val: mlirlite.Value* = mlirlite.const_int32(pool, ni * 8) + slot: mlirlite.Value* = mlirlite.build_gep(parent_builder, i8_ty, env_ptr, offset_val) + if slot is not None: + slot_typed: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, slot, i8_ptr_ptr_ty) + if slot_typed is not None: mlirlite.build_store(parent_builder, heap_store, slot_typed) + + # 13b. Create the closure struct: malloc(16) + closure_size_val: mlirlite.Value* = mlirlite.const_int64(pool, 16) + if closure_size_val is not None: closure_size_val.Next = None + closure_ptr: mlirlite.Value* = mlirlite.build_call(parent_builder, "malloc", closure_size_val, 1, i8_ptr_ty, 0) + if closure_ptr is None: + # malloc failed, restore state + trans._cur_func_name = old_func_name + trans._global_name_count = old_global_count + trans._nonlocal_name_count = old_nonlocal_count + trans._closure_env_count = old_env_count + trans._nonlocal_names = saved_nl_names + trans._nonlocal_types = saved_nl_types + return 0 + + # 13c. Store fn_ptr at offset 0; create the function pointer type i32(i8*)*. + fn_param_node: mlirlite.ParamNode* = mlirlite.new_param_node(pool, i8_ptr_ty) + fn_func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, i32_ty, fn_param_node, 1) + fn_func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, fn_func_ty) + + # Create the function reference Value (type i32(i8*)*, using the SHA1 mangled name) + fn_ptr_name: str = pool.alloc(64) + if fn_ptr_name is not None: + # SHA1-prefixed names contain '.' so they need quoting (e.g. @"sha1.__closure_func") + if string.strchr(mangled_promoted, '.') is not None: viperlib.snprintf(fn_ptr_name, 64, "@\"%s\"", mangled_promoted) + else: viperlib.snprintf(fn_ptr_name, 64, "@%s", mangled_promoted) + fn_ptr_val: mlirlite.Value* = mlirlite.SSAValue(pool, fn_func_ptr_ty, fn_ptr_name) + + # bitcast the function pointer to i8* (the closure stores i8*) + fn_as_i8ptr: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, fn_ptr_val, i8_ptr_ty) + fn_slot: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, closure_ptr, mlirlite.Ptr(pool, i8_ptr_ty)) + if fn_slot is not None and fn_as_i8ptr is not None: mlirlite.build_store(parent_builder, fn_as_i8ptr, fn_slot) + + # 13d. Store env_ptr at offset 8 + eight_val: mlirlite.Value* = mlirlite.const_int32(pool, 8) + env_slot_addr: mlirlite.Value* = mlirlite.build_gep(parent_builder, i8_ty, closure_ptr, eight_val) + if env_slot_addr is not None: + env_slot: mlirlite.Value* = mlirlite.build_bitcast(parent_builder, env_slot_addr, mlirlite.Ptr(pool, i8_ptr_ty)) + if env_slot is not None: + if env_ptr is not None: mlirlite.build_store(parent_builder, env_ptr, env_slot) + else: + # No nonlocal variables, store null + null_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "null") + mlirlite.build_store(parent_builder, null_val, env_slot) + + # 13e. Store the closure pointer into the parent's local variable {func_name} + closure_alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(parent_builder, i8_ptr_ty) + if closure_alloca is not None: + mlirlite.build_store(parent_builder, closure_ptr, closure_alloca) + self.Gen.VarGen.define_var(trans.SymTab, func_name, closure_alloca) + # Register the closure variable name so calls can tell a closure from a plain function + # pointer variable (both allocas are i8**, which _is_func_ptr_alloca cannot distinguish). + self.Gen.CallGenLeafKit.register_closure_name(func_name) + + # 14. Restore scope names (including the nonlocal set pointers) + trans._cur_func_name = old_func_name + trans._global_name_count = old_global_count + trans._nonlocal_name_count = old_nonlocal_count + trans._closure_env_count = old_env_count + trans._nonlocal_names = saved_nl_names + trans._nonlocal_types = saved_nl_types + + return 1 + + # _closure_scan_captures - recursively register implicit captures of the nested body. + # Order must match step 13's env build; runs before body translation, so it is stable. + def _closure_scan_captures(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Recursively register implicit captures in a nested body as nonlocal""" + if node is None or trans is None: return 1 + k: int = node.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is None: return 1 + # Defined in the current scope (param / AnnAssign local, incl. branches) - not a capture + if self.Gen.VarGen.lookup_current(trans.SymTab, nm.id) is not None: return 1 + # Already a registered nonlocal (explicit statement / already captured) - skip + if GenApi.is_nonlocal_name(trans, nm.id) != 0: return 1 + ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) + if ve is None or ve.Alloca is None or ve.Alloca.Ty is None: return 1 + nl_ty: mlirlite.LLVMType* = ve.Alloca.Ty.Pointee + if nl_ty is None: return 1 + # Module-level globals resolve directly, no env capture needed + mod_gv: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + if mod_gv is not None: + if uint64(mod_gv) == uint64(ve.Alloca): return 1 + # Outer-function local -> implicit capture (register as nonlocal) + GenApi.add_nonlocal_name(trans, nm.id, nl_ty) + return 1 + # ---- Statements ---- + if k == ast.ASTKind.Assign: + asn: ast.Assign* = ast.Assign*(node) + if asn.value is not None: self._closure_scan_captures(trans, asn.value) + return 1 + if k == ast.ASTKind.AnnAssign: + aan: ast.AnnAssign* = ast.AnnAssign*(node) + if aan.value is not None: self._closure_scan_captures(trans, aan.value) + return 1 + if k == ast.ASTKind.AugAssign: + aun: ast.AugAssign* = ast.AugAssign*(node) + # The target is also read (reads the old value), so it participates in capture + if aun.target is not None: self._closure_scan_captures(trans, aun.target) + if aun.value is not None: self._closure_scan_captures(trans, aun.value) + return 1 + if k == ast.ASTKind.If: + ifn: ast.If* = ast.If*(node) + if ifn.test is not None: self._closure_scan_captures(trans, ifn.test) + if ifn.children is not None: + bn_if: sizet = len(ifn.children) + for bi_if in range(int(bn_if)): self._closure_scan_captures(trans, ifn.children.get(bi_if)) + if ifn.orelse is not None: + bn_el: sizet = len(ifn.orelse) + for bi_el in range(int(bn_el)): self._closure_scan_captures(trans, ifn.orelse.get(bi_el)) + return 1 + if k == ast.ASTKind.For: + for_n: ast.For* = ast.For*(node) + # The loop target is a new local, not captured + if for_n.iter is not None: self._closure_scan_captures(trans, for_n.iter) + if for_n.children is not None: + bn_f: sizet = len(for_n.children) + for bi_f in range(int(bn_f)): self._closure_scan_captures(trans, for_n.children.get(bi_f)) + if for_n.orelse is not None: + bn_fe: sizet = len(for_n.orelse) + for bi_fe in range(int(bn_fe)): self._closure_scan_captures(trans, for_n.orelse.get(bi_fe)) + return 1 + if k == ast.ASTKind.While: + wh_n: ast.While* = ast.While*(node) + if wh_n.test is not None: self._closure_scan_captures(trans, wh_n.test) + if wh_n.children is not None: + bn_w: sizet = len(wh_n.children) + for bi_w in range(int(bn_w)): self._closure_scan_captures(trans, wh_n.children.get(bi_w)) + if wh_n.orelse is not None: + bn_we: sizet = len(wh_n.orelse) + for bi_we in range(int(bn_we)): self._closure_scan_captures(trans, wh_n.orelse.get(bi_we)) + return 1 + if k == ast.ASTKind.Return: + rt_n: ast.Return* = ast.Return*(node) + if rt_n.value is not None: self._closure_scan_captures(trans, rt_n.value) + return 1 + if k == ast.ASTKind.Expr: + ex_n: ast.Expr* = ast.Expr*(node) + if ex_n.value is not None: self._closure_scan_captures(trans, ex_n.value) + return 1 + if k == ast.ASTKind.With: + wth: ast.With* = ast.With*(node) + if wth.items is not None: + bn_wi: sizet = len(wth.items) + for bi_wi in range(int(bn_wi)): + wi_n: ast.AST* = wth.items.get(bi_wi) + if wi_n is not None and wi_n.kind() == ast.ASTKind.WithItem: + wi_c: ast.WithItem* = ast.WithItem*(wi_n) + if wi_c.context_expr is not None: self._closure_scan_captures(trans, wi_c.context_expr) + if wth.children is not None: + bn_wb: sizet = len(wth.children) + for bi_wb in range(int(bn_wb)): self._closure_scan_captures(trans, wth.children.get(bi_wb)) + return 1 + if k == ast.ASTKind.Raise: + rs_n: ast.Raise* = ast.Raise*(node) + if rs_n.exc is not None: self._closure_scan_captures(trans, rs_n.exc) + if rs_n.cause is not None: self._closure_scan_captures(trans, rs_n.cause) + return 1 + if k == ast.ASTKind.Assert: + as_n: ast.Assert* = ast.Assert*(node) + if as_n.test is not None: self._closure_scan_captures(trans, as_n.test) + if as_n.msg is not None: self._closure_scan_captures(trans, as_n.msg) + return 1 + if k == ast.ASTKind.Try: + tr_n: ast.Try* = ast.Try*(node) + if tr_n.children is not None: + bn_t: sizet = len(tr_n.children) + for bi_t in range(int(bn_t)): self._closure_scan_captures(trans, tr_n.children.get(bi_t)) + if tr_n.handlers is not None: + bn_th: sizet = len(tr_n.handlers) + for bi_th in range(int(bn_th)): + eh_n: ast.AST* = tr_n.handlers.get(bi_th) + if eh_n is not None and eh_n.kind() == ast.ASTKind.ExceptHandler: + eh_c: ast.ExceptHandler* = ast.ExceptHandler*(eh_n) + if eh_c.children is not None: + bn_ehb: sizet = len(eh_c.children) + for bi_ehb in range(int(bn_ehb)): self._closure_scan_captures(trans, eh_c.children.get(bi_ehb)) + if tr_n.orelse is not None: + bn_to: sizet = len(tr_n.orelse) + for bi_to in range(int(bn_to)): self._closure_scan_captures(trans, tr_n.orelse.get(bi_to)) + if tr_n.finalbody is not None: + bn_tf: sizet = len(tr_n.finalbody) + for bi_tf in range(int(bn_tf)): self._closure_scan_captures(trans, tr_n.finalbody.get(bi_tf)) + return 1 + if k == ast.ASTKind.Match: + mt_n: ast.Match* = ast.Match*(node) + if mt_n.subject is not None: self._closure_scan_captures(trans, mt_n.subject) + if mt_n.cases is not None: + bn_mc: sizet = len(mt_n.cases) + for bi_mc in range(int(bn_mc)): + mc_n: ast.AST* = mt_n.cases.get(bi_mc) + if mc_n is not None and mc_n.kind() == ast.ASTKind.MatchCase: + mc_c: ast.MatchCase* = ast.MatchCase*(mc_n) + if mc_c.guard is not None: self._closure_scan_captures(trans, mc_c.guard) + if mc_c.children is not None: + bn_mcb: sizet = len(mc_c.children) + for bi_mcb in range(int(bn_mcb)): self._closure_scan_captures(trans, mc_c.children.get(bi_mcb)) + return 1 + # ---- Expressions ---- + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(node) + if bop.left is not None: self._closure_scan_captures(trans, bop.left) + if bop.right is not None: self._closure_scan_captures(trans, bop.right) + return 1 + if k == ast.ASTKind.UnaryOp: + uop: ast.UnaryOp* = ast.UnaryOp*(node) + if uop.operand is not None: self._closure_scan_captures(trans, uop.operand) + return 1 + if k == ast.ASTKind.BoolOp: + bop2: ast.BoolOp* = ast.BoolOp*(node) + if bop2.values is not None: + bn_bo: sizet = len(bop2.values) + for bi_bo in range(int(bn_bo)): self._closure_scan_captures(trans, bop2.values.get(bi_bo)) + return 1 + if k == ast.ASTKind.Compare: + cmp: ast.Compare* = ast.Compare*(node) + if cmp.left is not None: self._closure_scan_captures(trans, cmp.left) + if cmp.comparators is not None: + bn_cmp: sizet = len(cmp.comparators) + for bi_cmp in range(int(bn_cmp)): self._closure_scan_captures(trans, cmp.comparators.get(bi_cmp)) + return 1 + if k == ast.ASTKind.Call: + cal: ast.Call* = ast.Call*(node) + if cal.func is not None: self._closure_scan_captures(trans, cal.func) + if cal.args is not None: + bn_ca: sizet = len(cal.args) + for bi_ca in range(int(bn_ca)): self._closure_scan_captures(trans, cal.args.get(bi_ca)) + if cal.keywords is not None: + bn_ck: sizet = len(cal.keywords) + for bi_ck in range(int(bn_ck)): + kw_n: ast.AST* = cal.keywords.get(bi_ck) + if kw_n is not None and kw_n.kind() == ast.ASTKind.Keyword: + kw_c: ast.Keyword* = ast.Keyword*(kw_n) + if kw_c.value is not None: self._closure_scan_captures(trans, kw_c.value) + return 1 + if k == ast.ASTKind.Subscript: + sub: ast.Subscript* = ast.Subscript*(node) + if sub.value is not None: self._closure_scan_captures(trans, sub.value) + if sub.slice is not None: self._closure_scan_captures(trans, sub.slice) + return 1 + if k == ast.ASTKind.Attribute: + at_n: ast.Attribute* = ast.Attribute*(node) + if at_n.value is not None: self._closure_scan_captures(trans, at_n.value) + return 1 + if k == ast.ASTKind.IfExp: + ie: ast.IfExp* = ast.IfExp*(node) + if ie.test is not None: self._closure_scan_captures(trans, ie.test) + if ie.body is not None: self._closure_scan_captures(trans, ie.body) + if ie.orelse is not None: self._closure_scan_captures(trans, ie.orelse) + return 1 + if k == ast.ASTKind.Dict: + dc: ast.Dict* = ast.Dict*(node) + if dc.keys is not None: + bn_dk: sizet = len(dc.keys) + for bi_dk in range(int(bn_dk)): self._closure_scan_captures(trans, dc.keys.get(bi_dk)) + if dc.values is not None: + bn_dv: sizet = len(dc.values) + for bi_dv in range(int(bn_dv)): self._closure_scan_captures(trans, dc.values.get(bi_dv)) + return 1 + if k == ast.ASTKind.Set: + st_n: ast.Set* = ast.Set*(node) + if st_n.elts is not None: + bn_set: sizet = len(st_n.elts) + for bi_set in range(int(bn_set)): self._closure_scan_captures(trans, st_n.elts.get(bi_set)) + return 1 + if k == ast.ASTKind.List: + ls_n: ast.List* = ast.List*(node) + if ls_n.elts is not None: + bn_ls: sizet = len(ls_n.elts) + for bi_ls in range(int(bn_ls)): self._closure_scan_captures(trans, ls_n.elts.get(bi_ls)) + return 1 + if k == ast.ASTKind.Tuple: + tp_n: ast.Tuple* = ast.Tuple*(node) + if tp_n.elts is not None: + bn_tp: sizet = len(tp_n.elts) + for bi_tp in range(int(bn_tp)): self._closure_scan_captures(trans, tp_n.elts.get(bi_tp)) + return 1 + if k == ast.ASTKind.Starred: + sr_n: ast.Starred* = ast.Starred*(node) + if sr_n.value is not None: self._closure_scan_captures(trans, sr_n.value) + return 1 + if k == ast.ASTKind.NamedExpr: + ne_n: ast.NamedExpr* = ast.NamedExpr*(node) + if ne_n.value is not None: self._closure_scan_captures(trans, ne_n.value) + return 1 + if k == ast.ASTKind.Slice: + sl_n: ast.Slice* = ast.Slice*(node) + if sl_n.lower is not None: self._closure_scan_captures(trans, sl_n.lower) + if sl_n.upper is not None: self._closure_scan_captures(trans, sl_n.upper) + if sl_n.step is not None: self._closure_scan_captures(trans, sl_n.step) + return 1 + if k == ast.ASTKind.JoinedStr: + js_n: ast.JoinedStr* = ast.JoinedStr*(node) + if js_n.values is not None: + bn_js: sizet = len(js_n.values) + for bi_js in range(int(bn_js)): self._closure_scan_captures(trans, js_n.values.get(bi_js)) + return 1 + if k == ast.ASTKind.FormattedValue: + fv_n: ast.FormattedValue* = ast.FormattedValue*(node) + if fv_n.value is not None: self._closure_scan_captures(trans, fv_n.value) + if fv_n.format_spec is not None: self._closure_scan_captures(trans, fv_n.format_spec) + return 1 + if k == ast.ASTKind.Await: + aw_n: ast.Await* = ast.Await*(node) + if aw_n.value is not None: self._closure_scan_captures(trans, aw_n.value) + return 1 + if k == ast.ASTKind.Yield: + yd_n: ast.Yield* = ast.Yield*(node) + if yd_n.value is not None: self._closure_scan_captures(trans, yd_n.value) + return 1 + if k == ast.ASTKind.YieldFrom: + yf_n: ast.YieldFrom* = ast.YieldFrom*(node) + if yf_n.value is not None: self._closure_scan_captures(trans, yf_n.value) + return 1 + is_comp: int = 0 + if k == ast.ASTKind.ListComp: is_comp = 1 + elif k == ast.ASTKind.SetComp: is_comp = 1 + elif k == ast.ASTKind.GeneratorExp: is_comp = 1 + if is_comp != 0: + lc_n: ast.ListComp* = ast.ListComp*(node) + if lc_n.elt is not None: self._closure_scan_captures(trans, lc_n.elt) + if lc_n.generators is not None: + bn_gen: sizet = len(lc_n.generators) + for bi_gen in range(int(bn_gen)): + cp_n: ast.AST* = lc_n.generators.get(bi_gen) + if cp_n is not None and cp_n.kind() == ast.ASTKind.Comprehension: + cp_c: ast.Comprehension* = ast.Comprehension*(cp_n) + if cp_c.iter is not None: self._closure_scan_captures(trans, cp_c.iter) + if cp_c.ifs is not None: + bn_cpi: sizet = len(cp_c.ifs) + for bi_cpi in range(int(bn_cpi)): self._closure_scan_captures(trans, cp_c.ifs.get(bi_cpi)) + return 1 + if k == ast.ASTKind.DictComp: + dcn: ast.DictComp* = ast.DictComp*(node) + if dcn.key is not None: self._closure_scan_captures(trans, dcn.key) + if dcn.value is not None: self._closure_scan_captures(trans, dcn.value) + if dcn.generators is not None: + bn_dgen: sizet = len(dcn.generators) + for bi_dgen in range(int(bn_dgen)): + cp_dn: ast.AST* = dcn.generators.get(bi_dgen) + if cp_dn is not None and cp_dn.kind() == ast.ASTKind.Comprehension: + cp_dc: ast.Comprehension* = ast.Comprehension*(cp_dn) + if cp_dc.iter is not None: self._closure_scan_captures(trans, cp_dc.iter) + if cp_dc.ifs is not None: + bn_dcpi: sizet = len(cp_dc.ifs) + for bi_dcpi in range(int(bn_dcpi)): self._closure_scan_captures(trans, cp_dc.ifs.get(bi_dcpi)) + return 1 + return 1 + + + # _type_byte_size - compute an LLVM type's byte size recursively (for nonlocal shared + # heap: arrays must not be fixed at 8). `case _` returns 8 because size is unknown. + def _type_byte_size(self, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> int: + """Compute an LLVM type's byte size recursively""" + if ty is None: + # [S4 fail-fast] Missing type -> no longer assume 8 bytes (under-allocation overruns neighbors) + self.Gen.TypeGenErr.fatal_error(None, "nonlocal shared storage: type missing, cannot compute size", "TypeError") + return 0 + match ty: + case mlirlite.LLVMType.Int(bits): + sz_i: int = (bits + 7) // 8 + if sz_i < 8: sz_i = 8 + return sz_i + case mlirlite.LLVMType.Ptr(_pe): + return 8 + case mlirlite.LLVMType.Float(fb): + if fb == 64: return 8 + if fb >= 128: return 16 + return 4 + case mlirlite.LLVMType.Array(et, cnt): + esz: int = self._type_byte_size(pool, et) + return esz * cnt + case _: + return 8 + + # ============================================================ + # _nl_heap_global_name - global map name nlheap_{sha1}.{name} for a nonlocal heap address + def _nl_heap_global_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*, name: str) -> str: + """Build the nonlocal heap-address global map name nlheap_{sha1}.{name}""" + if pool is None or trans is None or name is None: return None + mangled: str = self.Gen.FuncGenBase._mangle_name(trans, name) + buf: str = pool.alloc(160) + if buf is not None: viperlib.snprintf(buf, 160, "nlheap_%s", mangled) + return buf + + # ============================================================ + # _nl_heap_global_ref - reference Value (i8**) for the global map variable + def _nl_heap_global_ref(self, pool: memhub.MemBuddy*, trans: HT.Translator*, name: str) -> mlirlite.Value*: + """Get a reference Value for the nonlocal heap-address global map variable""" + if pool is None or trans is None or name is None: return None + gname: str = self._nl_heap_global_name(pool, trans, name) + if gname is None: return None + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + ref_buf: str = pool.alloc(176) + if ref_buf is None: return None + viperlib.snprintf(ref_buf, 176, "@\"%s\"", gname) + return mlirlite.SSAValue(pool, mlirlite.Ptr(pool, i8_ptr_ty), ref_buf) + + # ============================================================ + # _nl_heap_global_exists - check whether the global map variable already exists + def _nl_heap_global_exists(self, mod: mlirlite.LLVMModule*, gname: str) -> int: + """Return 1 if the global map variable gname already exists in the module""" + if mod is None or gname is None: return 0 + gcur: mlirlite.GlobalVariable* = mod.GlobalHead + while gcur is not None: + if gcur.Name is not None and string.strcmp(gcur.Name, gname) == 0: return 1 + gcur = gcur.Next + return 0 + + # ============================================================ + # _collect_chain_nonlocals - recursively gather nonlocal names in the nested chain (deduped) + def _collect_chain_nonlocals(self, trans: HT.Translator*, node: ast.AST*, names: t.CPtr, count: int) -> int: + """Recursively collect all nonlocal declaration names in a nested FunctionDef chain (deduped)""" + if node is None or names is None: return count + fd: ast.FunctionDef* = ast.FunctionDef*(node) + if fd is None or fd.children is None: return count + ch: list[ast.AST*]* = fd.children + cn: sizet = len(ch) + for ci in range(int(cn)): + st: ast.AST* = ch.get(ci) + if st is None: continue + k: int = st.kind() + if k == ast.ASTKind.FunctionDef: count = self._collect_chain_nonlocals(trans, st, names, count) + elif k == ast.ASTKind.Nonlocal: + nl_node: ast.Nonlocal* = ast.Nonlocal*(st) + if nl_node is not None and nl_node.names is not None: + nl_list: list[ast.AST*]* = nl_node.names + nn: sizet = len(nl_list) + for ni in range(int(nn)): + nm_node: ast.AST* = nl_list.get(ni) + if nm_node is None: continue + nm_x: ast.Name* = ast.Name*(nm_node) + if nm_x is None or nm_x.id is None: continue + dup: int = 0 + for di in range(int(count)): + if string.strcmp((str)(uint64(names) + di * 8), nm_x.id) == 0: + dup = 1 + break + if dup == 0 and count < 64: + nm_slot: str* = str*(uint64(names) + count * 8) + nm_slot[0] = nm_x.id + count += 1 + return count + + # _prepare_chain_nonlocals - pre-scan the nested chain's nonlocals and build heaps. + # Called before translate_nested_function_def so inner closures can load from @nlheap.*. + def _prepare_chain_nonlocals(self, trans: HT.Translator*, node: ast.AST*, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, i8_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> int: + """Pre-collect nonlocals in a nested function chain and build their heaps; returns 1 on success""" + if node is None or trans is None or builder is None or mod is None: return 0 + # Build only once per function so sibling nested defs share one heap (avoids double + # malloc and double VarEntry.Alloca redirect desyncing counters). + if trans._nl_chain_done != 0: return 1 + if trans._nl_chain_names is None: + trans._nl_chain_names = pool.alloc(8 * 64) + if trans._nl_chain_names is not None: string.memset(trans._nl_chain_names, 0, 8 * 64) + if trans._nl_chain_names is None: return 0 + trans._nl_chain_count = self._collect_chain_nonlocals(trans, node, trans._nl_chain_names, 0) + trans._nl_chain_done = 1 + for ci in range(int(trans._nl_chain_count)): + nm_slot: str* = str*(uint64(trans._nl_chain_names) + ci * 8) + nl_name: str = nm_slot[0] + if nl_name is None: continue + # Build the heap only in the defining function (current top scope has an alloca) + local_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, nl_name) + if local_alloca is None or local_alloca.Ty is None: continue + nl_ty: mlirlite.LLVMType* = local_alloca.Ty.Pointee + if nl_ty is None: continue + gname: str = self._nl_heap_global_name(pool, trans, nl_name) + if gname is None: continue + # Build the heap and copy the current value each time this function runs, then store + # into the module map; do not skip just because the map variable exists. + st_size: int = self._type_byte_size(pool, nl_ty) + st_size_val: mlirlite.Value* = mlirlite.const_int64(pool, st_size) + if st_size_val is not None: st_size_val.Next = None + var_storage: mlirlite.Value* = mlirlite.build_call(builder, "malloc", st_size_val, 1, i8_ptr_ty, 0) + if var_storage is None: continue + cur_val: mlirlite.Value* = mlirlite.build_load(builder, nl_ty, local_alloca) + st_slot: mlirlite.Value* = mlirlite.build_bitcast(builder, var_storage, mlirlite.Ptr(pool, nl_ty)) + if st_slot is not None and cur_val is not None: mlirlite.build_store(builder, cur_val, st_slot) + # Redirect VarEntry so the parent reads/writes the shared heap + ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nl_name) + if ve is not None: + heap_typed: mlirlite.Value* = mlirlite.build_bitcast(builder, var_storage, mlirlite.Ptr(pool, nl_ty)) + if heap_typed is not None: ve.Alloca = heap_typed + # Create/reuse the global map variable and store the heap address (cross-function visible) + gv_found: mlirlite.GlobalVariable* = None + gcur: mlirlite.GlobalVariable* = mod.GlobalHead + while gcur is not None: + if gcur.Name is not None and string.strcmp(gcur.Name, gname) == 0: + gv_found = gcur + break + gcur = gcur.Next + if gv_found is None: + gv_found = mlirlite.new_global_variable(pool, gname, i8_ptr_ty) + if gv_found is not None: + # Clear external linkage and zero-init, otherwise it prints as an external + # declaration with no definition -> link-time undefined reference. + gv_found.Linkage = None + gv_found.Initializer = "null" + mlirlite.module_add_global(mod, gv_found) + if gv_found is not None: + gv_ref: mlirlite.Value* = self._nl_heap_global_ref(pool, trans, nl_name) + if gv_ref is not None: mlirlite.build_store(builder, var_storage, gv_ref) + return 1 + + # translate_lambda - translate a lambda into a closure, same architecture as + # translate_nested_function_def: hoisted __lambda_{id}, captured heap slots, {fn,env}. + def translate_lambda(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*: + """Translate a lambda expression and return the closure pointer (i8*)""" + if node is None or trans is None or builder is None: return None + lam: ast.Lambda* = ast.Lambda*(node) + if lam is None: return None + + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + i8_ptr_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty) + + # 1. Lambda id + promoted function name. The counter lives on trans (_lambda_count): + # a new self field would crash TPC's Mixin class_members lookup (FuncGen.py L437). + lambda_id: int = trans._lambda_count + trans._lambda_count += 1 + pname_buf: str = pool.alloc(64) + if pname_buf is None: return None + viperlib.snprintf(pname_buf, 64, "__lambda_%d", lambda_id) + mangled_promoted: str = self.Gen.FuncGenBase._mangle_name(trans, pname_buf) + + # 2. Resolve params (name + type; default i32, resolved from the annotation) + param_names: str = pool.alloc(32 * 64) # 64 params x 32-byte names + if param_names is None: return None + string.memset(param_names, 0, 32 * 64) + param_tys: t.CPtr = pool.alloc(8 * 64) # LLVMType pointer slots + if param_tys is None: return None + string.memset(param_tys, 0, 8 * 64) + param_count: int = 0 + if lam.args is not None: + ags: ast.Arguments* = ast.Arguments*(lam.args) + if ags.args is not None: + alist: list[ast.AST*]* = ags.args + pn: sizet = len(alist) + for pi in range(int(pn)): + if param_count >= 64: break + arg_n: ast.Arg* = ast.Arg*(alist.get(pi)) + if arg_n is None or arg_n.arg is None: continue + string.strncpy(param_names + param_count * 32, arg_n.arg, 31) + p_ty: mlirlite.LLVMType* = i32_ty + if arg_n.annotation is not None: + resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg_n.annotation, trans._imported_modules, trans._from_imports, trans, 0) + if resolved is None: + # fail-fast: annotated lambda param failed to resolve; do not silently keep i32 + self.Gen.TypeGenErr.fatal_error(arg_n, f"argument '{arg_n.arg}' failed to resolve type: {self.Gen.TypeGenResolve.annotation_to_str(arg_n.annotation)}", "NameError") + resolved = i32_ty # unreachable (fatal_error already exited) + if resolved is not None: + match resolved: + case mlirlite.LLVMType.Struct(_, _, _): + p_ty = mlirlite.Ptr(pool, resolved) + case mlirlite.LLVMType.Array(aelem_ty_l, _): + p_ty = mlirlite.Ptr(pool, aelem_ty_l) + case _: + p_ty = resolved + ty_slot_p: str* = str*(uint64(param_tys) + param_count * 8) + ty_slot_p[0] = p_ty + param_count += 1 + + # 3. Collect captured variables (outer Name references in the body) + cap_names: str = pool.alloc(64 * 64) # 64 captures x 64-byte names + if cap_names is None: return None + string.memset(cap_names, 0, 64 * 64) + cap_allocas: t.CPtr = pool.alloc(8 * 64) # Parent alloca pointer slots + if cap_allocas is None: return None + string.memset(cap_allocas, 0, 8 * 64) + cap_tys: t.CPtr = pool.alloc(8 * 64) # Capture variable type slots + if cap_tys is None: return None + string.memset(cap_tys, 0, 8 * 64) + cap_count: int = self._lambda_scan_body(trans, lam.body, param_names, param_count, cap_names, cap_allocas, cap_tys, 0) + + # 4. Create the promoted function __lambda_{id}(i8* %env, [args...]) + func: mlirlite.Function* = mlirlite.create_function(pool, mod, mangled_promoted, i32_ty) + if func is None: return None + mlirlite.add_param(pool, func, i8_ptr_ty, "%env") + for pi2 in range(int(param_count)): + ty_addr2: uint64 = uint64(param_tys) + pi2 * 8 + ty_slot2: str* = str*(ty_addr2) + p_ty2: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot2[0]) + if p_ty2 is None: p_ty2 = i32_ty + pname_l: str = pool.alloc(32) + if pname_l is not None: + viperlib.snprintf(pname_l, 32, "%%%s", param_names + pi2 * 32) + mlirlite.add_param(pool, func, p_ty2, pname_l) + + # 5. Entry block + builder + entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry") + if entry_blk is None: return None + func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func) + if func_builder is None: return None + mlirlite.position_at_end(func_builder, entry_blk) + + # --traceback: promoted lambda gets its own DISubprogram. + dbg_pushed_l: int = DebugInfoGen.PushFunction(pool, func_builder, func, node.lineno, node.col_offset + 1) + + # 6. Enter the lambda scope + self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_FUNCTION) + + # 7. _env_ptr alloca (stores the %env parameter) + env_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, i8_ptr_ty) + if env_alloca is not None: + self.Gen.VarGen.define_var(trans.SymTab, "_env_ptr", env_alloca) + env_param_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "%env") + mlirlite.build_store(func_builder, env_param_val, env_alloca) + + # 8. Save parent state and switch to the promoted function + old_func: mlirlite.Function* = trans._cur_func + old_builder: mlirlite.IRBuilder* = trans._cur_builder + old_func_name: str = trans._cur_func_name + old_global_count: int = trans._global_name_count + old_nonlocal_count: int = trans._nonlocal_name_count + old_env_count: int = trans._closure_env_count + # break/continue targets and match-case flag must be cleared for the new scope (see + # assignments below); otherwise a lambda break/continue inherits the outer target. + old_break_bb: mlirlite.BasicBlock* = trans._break_bb + old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb + old_insw: int = trans._in_switch_case + trans._cur_func = func + trans._cur_builder = func_builder + trans._cur_func_name = mangled_promoted + # P2 willFree 失效表:函数作用域复位(栈槽地址会被后续函数复用,跨函数保留会误报) + self.Gen.VarGen.freed_clear() + GenApi.clear_scope_names(trans) + trans._break_bb = None + trans._continue_bb = None + trans._in_switch_case = 0 + + # 9. Captured variables: load values from env heap slots into local allocas + for ci in range(int(cap_count)): + # Take the capture name via str pointer offset (like param_names + pi*32). Do not use + # (str)(cap_addr): VPC compiles that to itoa, corrupting define_var keys. + cap_slot: str = cap_names + ci * 64 + if cap_slot[0] == '\0': continue + ty_addr3: uint64 = uint64(cap_tys) + ci * 8 + ty_slot3: str* = (str*)(ty_addr3) + cap_ty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot3[0]) + if cap_ty is None: continue + off_val: mlirlite.Value* = mlirlite.const_int32(pool, ci * 8) + slot_addr: mlirlite.Value* = mlirlite.build_gep(func_builder, i8_ty, env_param_val, off_val) + if slot_addr is None: continue + storage_addr: mlirlite.Value* = mlirlite.build_load(func_builder, i8_ptr_ty, slot_addr) + if storage_addr is None: continue + st_typed: mlirlite.Value* = mlirlite.build_bitcast(func_builder, storage_addr, mlirlite.Ptr(pool, cap_ty)) + if st_typed is None: continue + cap_val: mlirlite.Value* = mlirlite.build_load(func_builder, cap_ty, st_typed) + if cap_val is None: continue + var_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, cap_ty) + if var_alloca is not None: + mlirlite.build_store(func_builder, cap_val, var_alloca) + self.Gen.VarGen.define_var(trans.SymTab, cap_slot, var_alloca) + + # 10. Parameter allocas + for pi3 in range(int(param_count)): + ty_addr4: uint64 = uint64(param_tys) + pi3 * 8 + ty_slot4: str* = (str*)(ty_addr4) + p_ty3: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot4[0]) + if p_ty3 is None: + # fail-fast: lambda param type slot missing (inconsistent state); do not keep i32 + self.Gen.TypeGenErr.fatal_error(lam, f"lambda param '{param_names + pi3 * 32}' type info missing", "InternalCompilerError") + p_ty3 = i32_ty # unreachable (fatal_error already exited) + pname_ref: str = pool.alloc(32) + if pname_ref is not None: + viperlib.snprintf(pname_ref, 32, "%%%s", param_names + pi3 * 32) + param_ref: mlirlite.Value* = mlirlite.SSAValue(pool, p_ty3, pname_ref) + arg_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, p_ty3) + if arg_alloca is not None: + mlirlite.build_store(func_builder, param_ref, arg_alloca) + self.Gen.VarGen.define_var(trans.SymTab, param_names + pi3 * 32, arg_alloca) + + # 11. Translate the body expression and return it. + # 返回类型**完全跟随 body 的实际类型**:提升函数创建时(第 4 步)只能先写 i32——那时 body + # 还没翻译、拿不到类型。这里按 body 类型修正 `func.RetTy`(mlirlite 公开字段;本步之后才发 + # ret,期间没有任何人读 RetTy;调用点取签名时读到的已是修正后的类型),然后**原样返回、 + # 不再强转**。修正了原实现(一律 coerce 到 i32)的三类错法: + # · 指针 → coerce_to_type(ptr → i32) 直接 CastError(pointer→integer 禁止); + # · 浮点 → fptosi 成 i32(静默有损); + # · 更宽整数等 → trunc 成 i32(静默有损,如 i64 实参)。 + # void body(如 `lambda: `)→ 提升函数返回 void 并发 ret void + # (原来会因 "a void value cannot take part in any operation" 直接报错)。 + body_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(func_builder, pool, mod, lam.body, None, 0, trans) + if body_val is None or body_val.Ty is None: + # fail-fast: lambda body translation failed; do not silently return 0 + self.Gen.TypeGenErr.fatal_error(lam.body, "lambda body expression translation failed", "TypeError") + elif self.Gen.StmtGenDecl._is_void_type(body_val.Ty) != 0: + func.RetTy = body_val.Ty + mlirlite.build_ret_void(func_builder) + else: + func.RetTy = body_val.Ty + mlirlite.build_ret(func_builder, body_val) + + # 12. Leave the lambda scope and restore the parent state + self.Gen.VarGen.exit_scope(trans.SymTab) + trans._cur_func = old_func + trans._cur_builder = old_builder + trans._break_bb = old_break_bb + trans._continue_bb = old_continue_bb + trans._in_switch_case = old_insw + trans._cur_func_name = old_func_name + trans._global_name_count = old_global_count + trans._nonlocal_name_count = old_nonlocal_count + trans._closure_env_count = old_env_count + if dbg_pushed_l != 0: DebugInfoGen.PopFunction(func_builder) + + # 13. Create env in the parent (8-byte heap per capture, env[i] = heap address) + env_ptr: mlirlite.Value* = None + if cap_count > 0: + env_size: int64 = cap_count * 8 + env_size_val: mlirlite.Value* = mlirlite.const_int64(pool, env_size) + if env_size_val is not None: env_size_val.Next = None + env_ptr = mlirlite.build_call(builder, "malloc", env_size_val, 1, i8_ptr_ty, 0) + if env_ptr is not None: + for ci3 in range(int(cap_count)): + # Same as step 9: null-check via str pointer offset, avoiding itoa semantics + cap_slot3: str = cap_names + ci3 * 64 + if cap_slot3[0] == '\0': continue + al_addr: uint64 = uint64(cap_allocas) + ci3 * 8 + al_slot: str* = (str*)(al_addr) + var_alloca2: mlirlite.Value* = (mlirlite.Value*)(al_slot[0]) + if var_alloca2 is None: continue + ty_addr5: uint64 = uint64(cap_tys) + ci3 * 8 + ty_slot5: str* = (str*)(ty_addr5) + cap_ty2: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_slot5[0]) + if cap_ty2 is None: continue + # Heap size: bits/8, at least 8 bytes (fits pointers/scalars) + ty_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(cap_ty2) + st_size: int = 8 + if ty_bits > 64: st_size = ty_bits // 8 + st_size_val: mlirlite.Value* = mlirlite.const_int64(pool, st_size) + if st_size_val is not None: st_size_val.Next = None + var_storage: mlirlite.Value* = mlirlite.build_call(builder, "malloc", st_size_val, 1, i8_ptr_ty, 0) + if var_storage is not None: + cur_val: mlirlite.Value* = mlirlite.build_load(builder, cap_ty2, var_alloca2) + if cur_val is not None: + st_slot5: mlirlite.Value* = mlirlite.build_bitcast(builder, var_storage, mlirlite.Ptr(pool, cap_ty2)) + if st_slot5 is not None: mlirlite.build_store(builder, cur_val, st_slot5) + off_val2: mlirlite.Value* = mlirlite.const_int32(pool, ci3 * 8) + slot2: mlirlite.Value* = mlirlite.build_gep(builder, i8_ty, env_ptr, off_val2) + if slot2 is not None: + slot_typed2: mlirlite.Value* = mlirlite.build_bitcast(builder, slot2, i8_ptr_ptr_ty) + if slot_typed2 is not None: mlirlite.build_store(builder, var_storage, slot_typed2) + + # 14. Create the closure in the parent: malloc(16) + closure_size_val: mlirlite.Value* = mlirlite.const_int64(pool, 16) + if closure_size_val is not None: closure_size_val.Next = None + closure_ptr: mlirlite.Value* = mlirlite.build_call(builder, "malloc", closure_size_val, 1, i8_ptr_ty, 0) + if closure_ptr is None: return None + + # 15. [0] = fn_ptr (function reference bitcast to i8*) + fn_param_node: mlirlite.ParamNode* = mlirlite.new_param_node(pool, i8_ptr_ty) + fn_func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, i32_ty, fn_param_node, 1) + fn_func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, fn_func_ty) + fn_ptr_name: str = pool.alloc(64) + if fn_ptr_name is not None: + if string.strchr(mangled_promoted, '.') is not None: viperlib.snprintf(fn_ptr_name, 64, "@\"%s\"", mangled_promoted) + else: viperlib.snprintf(fn_ptr_name, 64, "@%s", mangled_promoted) + fn_ptr_val: mlirlite.Value* = mlirlite.SSAValue(pool, fn_func_ptr_ty, fn_ptr_name) + fn_as_i8ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, fn_ptr_val, i8_ptr_ty) + fn_slot: mlirlite.Value* = mlirlite.build_bitcast(builder, closure_ptr, mlirlite.Ptr(pool, i8_ptr_ty)) + if fn_slot is not None and fn_as_i8ptr is not None: mlirlite.build_store(builder, fn_as_i8ptr, fn_slot) + + # 16. [8] = env_ptr (store null when there are no captures) + eight_val: mlirlite.Value* = mlirlite.const_int32(pool, 8) + env_slot_addr: mlirlite.Value* = mlirlite.build_gep(builder, i8_ty, closure_ptr, eight_val) + if env_slot_addr is not None: + env_slot: mlirlite.Value* = mlirlite.build_bitcast(builder, env_slot_addr, mlirlite.Ptr(pool, i8_ptr_ty)) + if env_slot is not None: + if env_ptr is not None: mlirlite.build_store(builder, env_ptr, env_slot) + else: + null_val: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ty, "null") + mlirlite.build_store(builder, null_val, env_slot) + + # 17. Return the closure pointer (i8*) + return closure_ptr + + # ============================================================ + # _lambda_scan_body - recursively collect outer Name refs into cap_names/cap_allocas/cap_tys + def _lambda_scan_body(self, trans: HT.Translator*, node: ast.AST*, param_names: str, param_count: int, cap_names: str, cap_allocas: t.CPtr, cap_tys: t.CPtr, cap_count: int) -> int: + """Recursively scan lambda body Name references and collect captured variables""" + if node is None or trans is None: return cap_count + k: int = node.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is None: return cap_count + # Skip lambda parameters + for pi in range(int(param_count)): + if string.strcmp(param_names + pi * 32, nm.id) == 0: return cap_count + # Skip already-captured names + for ci in range(int(cap_count)): + if string.strcmp(cap_names + ci * 64, nm.id) == 0: return cap_count + # Look up the current scope chain + alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) + if alloca is None or alloca.Ty is None: return cap_count + pointee: mlirlite.LLVMType* = alloca.Ty.Pointee + if pointee is None: return cap_count + # Skip module-level globals (the body resolves them directly) + mod_gv: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + if mod_gv is not None: + if uint64(mod_gv) == uint64(alloca): return cap_count + if cap_count >= 64: return cap_count + string.strncpy(cap_names + cap_count * 64, nm.id, 63) + al_slot: str* = (str*)(uint64(cap_allocas) + cap_count * 8) + al_slot[0] = alloca + ty_slot: str* = (str*)(uint64(cap_tys) + cap_count * 8) + ty_slot[0] = pointee + return cap_count + 1 + # Recurse into common expression children + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(node) + cc1: int = self._lambda_scan_body(trans, bop.left, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + return self._lambda_scan_body(trans, bop.right, param_names, param_count, cap_names, cap_allocas, cap_tys, cc1) + if k == ast.ASTKind.UnaryOp: + uop: ast.UnaryOp* = ast.UnaryOp*(node) + return self._lambda_scan_body(trans, uop.operand, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + if k == ast.ASTKind.BoolOp: + bop2: ast.BoolOp* = ast.BoolOp*(node) + cc2: int = cap_count + if bop2.values is not None: + vs: list[ast.AST*]* = bop2.values + vn: sizet = len(vs) + for vi in range(int(vn)): + cc2 = self._lambda_scan_body(trans, vs.get(vi), param_names, param_count, cap_names, cap_allocas, cap_tys, cc2) + return cc2 + if k == ast.ASTKind.Compare: + cmp: ast.Compare* = ast.Compare*(node) + cc3: int = self._lambda_scan_body(trans, cmp.left, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + if cmp.comparators is not None: + cl: list[ast.AST*]* = cmp.comparators + cn: sizet = len(cl) + for cvi in range(int(cn)): + cc3 = self._lambda_scan_body(trans, cl.get(cvi), param_names, param_count, cap_names, cap_allocas, cap_tys, cc3) + return cc3 + if k == ast.ASTKind.Call: + cal: ast.Call* = ast.Call*(node) + cc4: int = self._lambda_scan_body(trans, cal.func, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + if cal.args is not None: + als: list[ast.AST*]* = cal.args + an: sizet = len(als) + for ai in range(int(an)): + cc4 = self._lambda_scan_body(trans, als.get(ai), param_names, param_count, cap_names, cap_allocas, cap_tys, cc4) + if cal.keywords is not None: + kws: list[ast.AST*]* = cal.keywords + kn: sizet = len(kws) + for ki in range(int(kn)): + kw: ast.Keyword* = ast.Keyword*(kws.get(ki)) + if kw is not None and kw.value is not None: + cc4 = self._lambda_scan_body(trans, kw.value, param_names, param_count, cap_names, cap_allocas, cap_tys, cc4) + return cc4 + if k == ast.ASTKind.Subscript: + sub: ast.Subscript* = ast.Subscript*(node) + cc5: int = self._lambda_scan_body(trans, sub.value, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + if sub.slice is not None: + cc5 = self._lambda_scan_body(trans, sub.slice, param_names, param_count, cap_names, cap_allocas, cap_tys, cc5) + return cc5 + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + return self._lambda_scan_body(trans, at.value, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + if k == ast.ASTKind.IfExp: + ie: ast.IfExp* = ast.IfExp*(node) + cc6: int = self._lambda_scan_body(trans, ie.test, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + cc6 = self._lambda_scan_body(trans, ie.body, param_names, param_count, cap_names, cap_allocas, cap_tys, cc6) + return self._lambda_scan_body(trans, ie.orelse, param_names, param_count, cap_names, cap_allocas, cap_tys, cc6) + if k == ast.ASTKind.List: + ls: ast.List* = ast.List*(node) + cc7: int = cap_count + if ls.elts is not None: + es: list[ast.AST*]* = ls.elts + en: sizet = len(es) + for ei in range(int(en)): + cc7 = self._lambda_scan_body(trans, es.get(ei), param_names, param_count, cap_names, cap_allocas, cap_tys, cc7) + return cc7 + if k == ast.ASTKind.Lambda: + lam2: ast.Lambda* = ast.Lambda*(node) + return self._lambda_scan_body(trans, lam2.body, param_names, param_count, cap_names, cap_allocas, cap_tys, cap_count) + return cap_count diff --git a/App/lib/core/Gen/TranslatorGenMixin.vp b/App/lib/core/Gen/TranslatorGenMixin.vp index bba612f..0992234 100644 --- a/App/lib/core/Gen/TranslatorGenMixin.vp +++ b/App/lib/core/Gen/TranslatorGenMixin.vp @@ -1,1091 +1,1091 @@ -import lib.core.Gen.Generator as Generator -import t, c -from stdint import * -import ast -import mlirlite -import memhub -import string -import stdio -import viperlib -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HT -import lib.core.Gen.GenTypes as GenTypes -import lib.core.Gen.GenApi as GenApi - - -# TranslatorGenMixin - translation driver Mixin: module-level translation entry points -# (translate_children/create_wrapper_main/handle_module_level_var) and scope name management. - - -# TranslatorGenMixin - translation driver (module-level entry + scope name management) -novirtual class TranslatorGenMixin: - """Translation driver Mixin.""" - - # _register_cexport_from_funcdef - Phase 1a: pre-register every CExport/State function - # name in the global table so Phase 1b's alphabetical order cannot miss bare-name calls. - Gen: Generator.Generator* - def _register_cexport_from_funcdef(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Pre-register CExport/State function names into the global table (no IR).""" - fd: ast.FunctionDef* = ast.FunctionDef*(node) - if fd is None or fd.name is None: return 0 - if trans is None or trans.ModuleSha1 is None: return 0 - - # Check whether the return type carries a CExport or State marker - has_export: int = 0 - has_state: int = 0 - if fd.returns is not None: - has_export = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExport", 0) - has_state = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) - - if has_export != 0 or has_state != 0: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name) - - return 0 - - # translate_children - walk AST children and dispatch translation; shared state is - # read from trans, while imported_modules/from_imports are written back to trans. - def translate_children(self, trans: HT.Translator*, tree: ast.AST*) -> int: - """Translate every child of tree.children; return the number of variables added.""" - ch: list[ast.AST*]* = tree.children - if ch is None: return 0 - cn_count: sizet = len(ch) - added_total: int = 0 - for ci in range(int(cn_count)): - child: ast.AST* = ch.get(ci) - if child is None: continue - kd: int = child.kind() - # When _declare_only == 0, _translate_module_level already handled imports; - # skip here to avoid duplicating from_imports entries. - if trans._declare_only == 0 and (kd == ast.ASTKind.Import or kd == ast.ASTKind.ImportFrom): continue - if kd == ast.ASTKind.Import: self.Gen.ImportsGen.translate_import(trans, child) - elif kd == ast.ASTKind.ImportFrom: - self.Gen.ImportsGen.translate_import_from_module(trans, child) - self.Gen.ImportsGen.translate_import_from_names(trans, child) - elif kd == ast.ASTKind.FunctionDef: - # PEP 695 generic functions (def f[T]): skip at the definition site (the - # template is registered by Phase1Scan/Phase2) and monomorphize at call sites. - fd_tc: ast.FunctionDef* = ast.FunctionDef*(child) - if fd_tc is not None and fd_tc.type_params is not None: continue - # Phase 1a declare mode registers only CExport/State names; Phase 1b fully translates bodies. - if trans._declare_only == 0: - # 函数体翻译 = 编译器下潜进一个源码上下文:推一帧(def 位置), - # 让函数体内部的错误在 traceback 里回溯到"正在翻译哪个函数"(调用链)。 - tr_f: str = self.Gen.TypeGenResolve.get_current_file() - # §13 NLL:记录当前函数体(谓词 NllFrameConfined 要遍历它) - prev_nf: ast.AST* = self.Gen.StructGen.NllCurFunc - self.Gen.StructGen.NllCurFunc = child - self.Gen.TypeGenBase.push_trace(tr_f, fd_tc.lineno) - added: int = self.Gen.FuncGenDef.translate_function_def(trans, child) - self.Gen.TypeGenBase.pop_trace() - self.Gen.StructGen.NllCurFunc = prev_nf - added_total += added - elif trans._declare_only == 1: - self._register_cexport_from_funcdef(trans, child) - # Prescan signature types to trigger generic specializations before mark1, - # so their Ty/Fields are not recycled by Phase 1b mark/reset (avoids UAF). - self._prescan_funcdef_signatures(trans, child) - elif kd == ast.ASTKind.ClassDef: - # ClassDef is handled directly at module level (no builder needed); skip in - # _declare_only=2 (import-scan) mode, which only handles import dependencies. - if trans._declare_only != 2: self.Gen.ClassGenDef.translate_class_def(trans, child) - elif trans._declare_only == 0 and trans._cur_builder is not None: - # Has a builder -> delegate statement dispatch to HandlesBody - added = self.Gen.StmtGen.translate_stmt(trans, child) - added_total += added - elif kd == ast.ASTKind.AnnAssign and trans._declare_only != 2: - # Module-level AnnAssign: skip in import-scan mode, handle only CDefine in - # struct-registration mode, and handle all in full-translation mode. - added = self.handle_module_level_var(trans, child) - added_total += added - elif trans._declare_only == 0 and kd == ast.ASTKind.Assign: - # Module-level Assign without a builder -> create a global (full-translation mode only) - added = self.handle_module_level_var(trans, child) - added_total += added - - return added_total - - # _prescan_funcdef_signatures - Phase 1a: resolve module-level function signatures to - # trigger generic specializations before mark1 (avoids dangling structs / UAF in Phase 2). - def _prescan_funcdef_signatures(self, trans: HT.Translator*, node: ast.AST*) -> None: - """Pre-resolve module-level function signature types to trigger generic specializations.""" - if trans is None or node is None: return - # The entry takes the base ast.AST* (matching sibling dispatchers) and narrows it here - # explicitly to ast.FunctionDef*: implicit downcasting is disallowed under strict rules. - fd: ast.FunctionDef* = ast.FunctionDef*(node) - if fd is None: return - pool: memhub.MemBuddy* = trans.Pool - if pool is None: return - imp: str = trans._imported_modules - fi: str = trans._from_imports - if fd.returns is not None: self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imp, fi, trans, 0) - if fd.args is not None: - ags: ast.Arguments* = ast.Arguments*(fd.args) - if ags.args is not None: - al: list[ast.AST*]* = ags.args - an: sizet = len(al) - ai: sizet - for ai in range(int(an)): - ag: ast.Arg* = ast.Arg*(al.get(ai)) - if ag is not None and ag.annotation is not None: self.Gen.TypeGenResolve.resolve_annotation_type(pool, ag.annotation, imp, fi, trans, 0) - - # _build_array_initializer_text - build an LLVM IR array initializer from an AST List, - # formatted as [N x elem_ty] [elem_ty val0, ...], for module-level t.CArray literals. - def _build_array_initializer_text(self, pool: memhub.MemBuddy*, var_ty: mlirlite.LLVMType*, list_node: ast.List*) -> str: - """Build LLVM IR array initializer text from an AST List node; None on failure.""" - if var_ty is None or list_node is None: return None - - # Re-cast so the TPV compiler recognizes ast.List, avoiding type inference degrading - # to t.CPtr when init_list_node's initial value is None. - ln: ast.List* = ast.List*(list_node) - if ln is None: return None - - # Extract array element type and count from var_ty - elem_ty: mlirlite.LLVMType* = None - arr_count: int = 0 - match var_ty: - case mlirlite.LLVMType.Array(et, cnt): - elem_ty = et - arr_count = cnt - - if elem_ty is None or arr_count <= 0: return None - - # Determine the IR representation of the element type - elem_ir_ty: str = "i8" - is_int_elem: int = 1 - match elem_ty: - case mlirlite.LLVMType.Int(bits): - is_int_elem = 1 - if bits == 8: elem_ir_ty = "i8" - elif bits == 16: elem_ir_ty = "i16" - elif bits == 32: elem_ir_ty = "i32" - elif bits == 64: elem_ir_ty = "i64" - case _: - is_int_elem = 0 - - # Get list elements via ln.elts (not list_node.elts) to ensure correct type recognition - elts: list[ast.AST*]* = ln.elts - if elts is None: return None - elts_count: sizet = len(elts) - - # More elements than the declared length -> fail-fast (never silently truncate or drop). - if sizet(arr_count) < elts_count: - self.Gen.TypeGenErr.fatal_error(list_node, f"array initializer has {elts_count} elements, exceeding the declared array length {arr_count}; silent truncation is not allowed", "OverflowError") - return None - - # Non-integer element types (aggregates): only an all-zero list yields the whole - # "zeroinitializer"; any non-zero constant is not expressible as text -> None. - if is_int_elem == 0: - # Nested array element type: build sub-array initializer text recursively with the - # inner type omitted (inferred from the outer global type), which llc accepts. - is_nested_arr: int = 0 - match elem_ty: - case mlirlite.LLVMType.Array(_, _): - is_nested_arr = 1 - case _: - pass - if is_nested_arr != 0: - cnt_total_n: sizet = sizet(arr_count) - buf_size_n: sizet = 128 + cnt_total_n * 768 - buf_n: str = pool.alloc(buf_size_n) - if buf_n is None: return None - written_n: int = viperlib.snprintf(buf_n, buf_size_n, "[") - pos_n: sizet = sizet(written_n) - # Sub-array type prefix (e.g. "[16 x i8]"): nested initializers must spell out the - # inner type, so serialize elem_ty via TypePrint for the [[16 x i8][...]] form. - elem_ty_str_n: str = pool.alloc_tagged(4096, memhub.DETSITE_TRANSLATORGEN_185) - if elem_ty_str_n is None: return None - mlirlite.TypePrint(elem_ty_str_n, 4096, elem_ty, pool) - for i_n in range(int(cnt_total_n)): - sub_n: str = None - if i_n < elts_count: - en_n: ast.AST* = elts.get(i_n) - if en_n is not None: - ek_n: int = en_n.kind() - if ek_n == ast.ASTKind.List: sub_n = self._build_array_initializer_text(pool, elem_ty, ast.List*(en_n)) - elif ek_n == ast.ASTKind.Set: - # Brace literals (e.g. font tables {0x00, ...}): Set.elts is - # isomorphic to List, so wrap it in a temp List and recurse. - se_n: ast.Set* = ast.Set*(en_n) - if se_n is not None and se_n.elts is not None: sub_n = self._build_array_initializer_text(pool, elem_ty, ast.List(pool, se_n.elts, ast.ASTCtx.Load)) - if sub_n is None: - # Missing/unevaluable element: zero-initialize the whole sub-array - sub_n = "zeroinitializer" - if i_n > 0: - written_n = viperlib.snprintf(buf_n + pos_n, buf_size_n - pos_n, ", ") - pos_n += sizet(written_n) - written_n = viperlib.snprintf(buf_n + pos_n, buf_size_n - pos_n, "%s%s", elem_ty_str_n, sub_n) - pos_n += sizet(written_n) - viperlib.snprintf(buf_n + pos_n, buf_size_n - pos_n, "]") - return buf_n - for zi in range(int(elts_count)): - ze: ast.AST* = elts.get(zi) - if ze is None: continue - zk: int = ze.kind() - if zk == ast.ASTKind.Constant: - zc: ast.Constant* = ast.Constant*(ze) - if zc is None: continue - # Treat None (CONST_NONE) as zero: function-pointer arrays use [None] to mean - # all-empty, so None must normalize to zero fill rather than fail-fast. - if zc.const_kind == ast.CONST_NONE: continue - if zc.const_kind != ast.CONST_INT or zc.int_val != 0: return None - elif zk == ast.ASTKind.UnaryOp: - if self.Gen.AssignGen.extract_cdefine_int_value(ze) != 0: return None - else: return None - return "zeroinitializer" - - # Size the buffer from the declared length arr_count (missing elements are zero-filled, - # so it may exceed elts_count); each element needs about 26 chars. - cnt_total: sizet = sizet(arr_count) - buf_size: sizet = 64 + cnt_total * 32 - buf: str = pool.alloc(buf_size) - if buf is None: return None - - # Write the "[" prefix (the type prefix is emitted by _print_global; only the element list here) - written: int = viperlib.snprintf(buf, buf_size, "[") - pos: sizet = sizet(written) - - # Write elements up to the declared length arr_count, zero-padding short lists so the - # initializer element count always matches the type llc expects. - for i in range(int(cnt_total)): - elem_val: int64 = 0 - if i < elts_count: - elem_node: ast.AST* = elts.get(i) - # Read int_val directly from the Constant node (avoids int truncation in extract_cdefine_int_value) - if elem_node is not None: - ek: int = elem_node.kind() - if ek == ast.ASTKind.Constant: - ec: ast.Constant* = ast.Constant*(elem_node) - # CONST_CHAR literals (e.g. `_ps2_set1 = ['\x1b', '1', ...]`): int_val is - # the character code (unrecognized chars would all be compiled as 0). - if ec is not None and (ec.const_kind == ast.CONST_INT or ec.const_kind == ast.CONST_CHAR): elem_val = ec.int_val - # Single-char CONST_STR (double quotes like "'" / '"'): the parser treats - # them as CONST_STR; take the first char code when strlen == 1, else 0. - elif ec is not None and ec.const_kind == ast.CONST_STR: - if ec.str_val is not None and string.strlen(ec.str_val) == 1: elem_val = int64(ec.str_val[0]) - elif ek == ast.ASTKind.UnaryOp: - # Negative: UnaryOp(USub, Constant) - elem_val = self.Gen.AssignGen.extract_cdefine_int_value(elem_node) - if i > 0: - written = viperlib.snprintf(buf + pos, buf_size - pos, ", ") - pos += sizet(written) - written = viperlib.snprintf(buf + pos, buf_size - pos, "%s %lld", elem_ir_ty, elem_val) - pos += sizet(written) - - # Write the closing "]" suffix - viperlib.snprintf(buf + pos, buf_size - pos, "]") - return buf - - # _build_string_array_initializer_text - initialize a char array global from a string literal: - # t.CArray[char, N] = "..." -> [i8 b0, ..., i8 0, ...] zero-padded; empty -> zeroinitializer. - def _build_string_array_initializer_text(self, pool: memhub.MemBuddy*, var_ty: mlirlite.LLVMType*, str_val: str) -> str: - """Build initializer text for an array global from a string literal; None on failure.""" - if var_ty is None or str_val is None: return None - arr_count: int = 0 - match var_ty: - case mlirlite.LLVMType.Array(_, cnt): - arr_count = cnt - if arr_count <= 0: return None - slen: sizet = string.strlen(str_val) - if slen == 0: return "zeroinitializer" - buf_size: sizet = 64 + sizet(arr_count) * 16 - buf: str = pool.alloc(buf_size) - if buf is None: return None - written: int = viperlib.snprintf(buf, buf_size, "[") - pos: sizet = sizet(written) - for i in range(int(sizet(arr_count))): - if i > 0: - written = viperlib.snprintf(buf + pos, buf_size - pos, ", ") - pos += sizet(written) - bv: int64 = 0 - if i < slen: bv = int64(str_val[i]) - written = viperlib.snprintf(buf + pos, buf_size - pos, "i8 %lld", bv) - pos += sizet(written) - viperlib.snprintf(buf + pos, buf_size - pos, "]") - return buf - - # _build_string_ptr_initializer - initialize a pointer-typed global from a string literal by - # creating an internal constant `@.str.{var_name}` and returning a GEP initializer text. - def _build_string_ptr_initializer(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, var_name: str, str_val: str) -> str: - """Create an internal constant global for a string literal; return GEP init text.""" - if str_val is None or var_name is None: return None - - # Build the internal string constant name: .str.{var_name} - str_name_buf: str = pool.alloc(64) - if str_name_buf is None: return None - viperlib.snprintf(str_name_buf, 64, ".str.%s", var_name) - - # Create the string constant global via mlirlite.create_global_string - str_gv: mlirlite.GlobalVariable* = mlirlite.create_global_string(pool, mod, str_name_buf, str_val) - if str_gv is None: return None - - # Generate the GEP initializer text - str_len: sizet = string.strlen(str_val) - init_buf: str = pool.alloc(128) - if init_buf is None: return None - viperlib.snprintf(init_buf, 128, "getelementptr inbounds ([%d x i8], [%d x i8]* @%s, i32 0, i32 0)", str_len + 1, str_len + 1, str_name_buf) - return init_buf - - def _stg_field_zero_text(self, pool: memhub.MemBuddy*, fty: mlirlite.LLVMType*, ty_str: str) -> str: - """Build the zero initializer text for a storage struct field by LLVM type.""" - if fty is None or ty_str is None: return None - # Size by the actual type text: TypePrint inlines full struct bodies (up to 4096 bytes), and - # the historic fixed 160-byte buffer was written with a 576-byte snprintf limit, so a long - # type text overflowed the pool block and corrupted the neighbouring free block. - cap: sizet = string.strlen(ty_str) + 24 - buf: str = pool.alloc(cap) - if buf is None: return None - match fty: - case mlirlite.LLVMType.Ptr(_): - viperlib.snprintf(buf, cap, "%s null", ty_str) - case mlirlite.LLVMType.Int(_): - viperlib.snprintf(buf, cap, "%s 0", ty_str) - case mlirlite.LLVMType.Float(_): - viperlib.snprintf(buf, cap, "%s 0.0", ty_str) - case _: - # Aggregates such as Array / Struct -> zeroinitializer - viperlib.snprintf(buf, cap, "%s zeroinitializer", ty_str) - return buf - - # _default_val_const_text - field default value -> LLVM constant element text; only foldable - # integer defaults (CONST_INT/CONST_CHAR, negated literals) are handled; others return None. - def _default_val_const_text(self, pool: memhub.MemBuddy*, fty: mlirlite.LLVMType*, ty_str: str, dval: ast.AST*) -> str: - """Render a field default as "ty value" text; None when not foldable.""" - if dval is None or fty is None or ty_str is None: return None - is_int: int = 0 - dv_int: int64 = 0 - if dval.kind() == ast.ASTKind.Constant: - dc: ast.Constant* = ast.Constant*(dval) - if dc is not None: - if dc.const_kind == ast.CONST_INT or dc.const_kind == ast.CONST_CHAR: - dv_int = dc.int_val - is_int = 1 - elif dval.kind() == ast.ASTKind.UnaryOp: - # Negative / bitwise-invert literals (e.g. -1 / 0xFFFFFFFFFFFFFFFF) - dv_int = self.Gen.AssignGen.extract_cdefine_int_value(dval) - is_int = 1 - if is_int == 0: return None - # Only integer fields accept an integer constant; float/pointer/aggregate fall back to zero - match fty: - case mlirlite.LLVMType.Int(_): - pass - case _: - return None - buf: str = pool.alloc(576) - if buf is None: return None - viperlib.snprintf(buf, 576, "%s %lld", ty_str, dv_int) - return buf - - # _build_stg_struct_body_text - storage struct field element text (no braces), used by - # _build_stg_struct_init_text and vtable storage; None when all field defaults are zero. - def _build_stg_struct_body_text(self, pool: memhub.MemBuddy*, se: GenTypes.StructEntry*, start_idx: int) -> str: - """Render "e0, e1, ..." element text from field defaults; None when all zero.""" - if se is None or se.Fields is None: return None - fc: int = se.FieldCount - if fc <= start_idx: return None - # Each field element = type prefix (up to _TY_TEXT_CAP bytes in MLIR form) + value text; - # the old 160-byte estimate truncated longer types. - buf_size: sizet = 256 + sizet(fc) * 576 - buf: str = pool.alloc(buf_size) - if buf is None: return None - pos: sizet = 0 - first: int = 1 - has_nonzero: int = 0 - i: int = start_idx - while i < fc: - fe: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(se, i) - i += 1 - if fe is None or fe.Ty is None: continue - ty_str: str = pool.alloc_tagged(4096, memhub.DETSITE_TRANSLATORGEN_384) - if ty_str is None: continue - mlirlite.TypePrint(ty_str, 4096, fe.Ty, pool) - elem: str = self._default_val_const_text(pool, fe.Ty, ty_str, fe.DefaultVal) - is_zero: int = 0 - if elem is None: - elem = self._stg_field_zero_text(pool, fe.Ty, ty_str) - is_zero = 1 - if elem is None: continue - written: int = 0 - if first == 0: - written = viperlib.snprintf(buf + pos, buf_size - pos, ", ") - pos += sizet(written) - written = viperlib.snprintf(buf + pos, buf_size - pos, "%s", elem) - pos += sizet(written) - first = 0 - if is_zero == 0: has_nonzero = 1 - if has_nonzero == 0: return None - return buf - - # _build_stg_struct_init_text - full constant initializer for a storage struct (all fields, - # with defaults) for module-level `X: T = T()`; None when no non-zero default exists. - def _build_stg_struct_init_text(self, pool: memhub.MemBuddy*, se: GenTypes.StructEntry*, start_idx: int) -> str: - """Render a full struct initializer from field defaults; None when all zero.""" - body: str = self._build_stg_struct_body_text(pool, se, start_idx) - if body is None: return None - body_len: sizet = sizet(string.strlen(body)) - buf: str = pool.alloc(body_len + 8) - if buf is None: return None - viperlib.snprintf(buf, body_len + 8, "{ %s }", body) - return buf - - # Module-level global t.attr.* extraction: attributes live in the annotation union - # (`T | t.attr.section(".x")`) and are written directly to gv.Section/Align/Linkage fields. - def _scan_global_tattr(self, node: ast.AST*, want_name: str) -> ast.AST*: - """Find the t.attr. node in a global type annotation; None when absent.""" - if node is None: return None - if node.kind() == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(node) - if bop.op == ast.OpKind.BitOr: - hit_l: ast.AST* = self._scan_global_tattr(bop.left, want_name) - if hit_l is not None: return hit_l - return self._scan_global_tattr(bop.right, want_name) - return None - if self.Gen.ClassGenBase._tattr_name(node, want_name) != 0: return node - return None - def _extract_global_attr_section(self, node: ast.AST*, var_name: str) -> str: - """Extract the section name from `T | t.attr.section("xxx")`; fail-fast when empty.""" - hit: ast.AST* = self._scan_global_tattr(node, "section") - if hit is None: return None - sec_val: str = self.Gen.FuncGenBase._get_attr_call_arg_str(hit) - if sec_val is None or sec_val[0] == 0: - self.Gen.TypeGenErr.fatal_error(node, f"section name of t.attr.section(...) for module-level variable '{var_name}' is missing or empty", "ValueError") - return None - return sec_val - def _extract_global_attr_aligned(self, node: ast.AST*, var_name: str) -> int: - """Extract alignment bytes from `T | t.attr.aligned(N)`; 0 when absent, fail-fast when invalid.""" - hit: ast.AST* = self._scan_global_tattr(node, "aligned") - if hit is None: return 0 - al_val: int = self.Gen.ClassGenBase._tattr_aligned_int(hit) - if al_val <= 0: - self.Gen.TypeGenErr.fatal_error(node, f"t.attr.aligned(N) for module-level variable '{var_name}' requires a positive integer literal N", "ValueError") - return 0 - return al_val - def _extract_global_attr_weak(self, node: ast.AST*) -> int: - """Detect `T | t.attr.weak` / `t.attr.weak()` (1 present, 0 absent).""" - if self._scan_global_tattr(node, "weak") is not None: return 1 - return 0 - - # handle_module_level_var - module-level var declaration -> LLVM global (int/list/string init). - # _RegisterStaticModuleVar - register (name, def module SHA1) of t.CStatic vars for cross-module access checks. - def _RegisterStaticModuleVar(self, trans: HT.Translator*, annot_node: ast.AST*, var_name: str) -> int: - if trans is None or annot_node is None or var_name is None: return 0 - if trans.ModuleSha1 is None: return 0 - if self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CStatic", 0) == 0: return 0 - self.Gen.CallGenBase.RegisterStaticSymbol(trans.ModuleSha1, var_name) - return 0 - - - def handle_module_level_var(self, trans: HT.Translator*, node: ast.AST*) -> int: - """Handle a module-level variable declaration by creating its global.""" - pool: memhub.MemBuddy* = trans.Pool - mod: mlirlite.LLVMModule* = trans.Module - - # CDefine annotation: a compile-time constant, no global is created; register it in the - # global CDefine table so t.CArray[elem_ty, NAME] can resolve it. - k: int = node.kind() - if k == ast.ASTKind.AnnAssign: - aa_cd: ast.AnnAssign* = ast.AnnAssign*(node) - if aa_cd is not None and aa_cd.target is not None: - if aa_cd.target.kind() == ast.ASTKind.Name: - nm_cd: ast.Name* = ast.Name*(aa_cd.target) - if nm_cd.id is not None: - if self.Gen.AssignGen.is_cdefine_annotation(aa_cd.annotation) != 0: - # Auto-detect and register float/int CDefine values (e.g. vipermath U_M_PI) - self.Gen.AssignGen._register_cdefine_from_value(pool, nm_cd.id, aa_cd.value, aa_cd.annotation) - return 0 - # CTypedef annotation: no global is created (would duplicate the symbol); - # register it in the CTypedef table instead, or annotation lookups fail. - if self.Gen.TypeGenCType._is_ctypedef_annotation(aa_cd.annotation) != 0: - ctd_str: str = self.Gen.TypeGenResolve._get_type_name_from_annotation(pool, aa_cd.value) - if ctd_str is not None: self.Gen.TypeGenCType.register_ctypedef(pool, nm_cd.id, ctd_str) - return 0 - - # In _declare_only=1 (struct-registration) mode only CDefine is handled; reaching here - # means it is not a CDefine, so skip. - if trans._declare_only == 1: return 0 - - var_name: str = None - var_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) - resolved_stg_ty: mlirlite.LLVMType* = None # Object-class global: static storage type (BSS instance) - resolved_stg_entry: GenTypes.StructEntry* = None # Object-class global: matching StructEntry (for vtable init) - init_val: int64 = 0 - init_fval: double = 0.0 # Value when init_kind==4 (float literal) +import lib.core.Gen.Generator as Generator +import t, c +from stdint import * +import ast +import mlirlite +import memhub +import string +import stdio +import viperlib +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HT +import lib.core.Gen.GenTypes as GenTypes +import lib.core.Gen.GenApi as GenApi + + +# TranslatorGenMixin - translation driver Mixin: module-level translation entry points +# (translate_children/create_wrapper_main/handle_module_level_var) and scope name management. + + +# TranslatorGenMixin - translation driver (module-level entry + scope name management) +novirtual class TranslatorGenMixin: + """Translation driver Mixin.""" + + # _register_cexport_from_funcdef - Phase 1a: pre-register every CExport/State function + # name in the global table so Phase 1b's alphabetical order cannot miss bare-name calls. + Gen: Generator.Generator* + def _register_cexport_from_funcdef(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Pre-register CExport/State function names into the global table (no IR).""" + fd: ast.FunctionDef* = ast.FunctionDef*(node) + if fd is None or fd.name is None: return 0 + if trans is None or trans.ModuleSha1 is None: return 0 + + # Check whether the return type carries a CExport or State marker + has_export: int = 0 + has_state: int = 0 + if fd.returns is not None: + has_export = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "CExport", 0) + has_state = self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) + + if has_export != 0 or has_state != 0: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name) + + return 0 + + # translate_children - walk AST children and dispatch translation; shared state is + # read from trans, while imported_modules/from_imports are written back to trans. + def translate_children(self, trans: HT.Translator*, tree: ast.AST*) -> int: + """Translate every child of tree.children; return the number of variables added.""" + ch: list[ast.AST*]* = tree.children + if ch is None: return 0 + cn_count: sizet = len(ch) + added_total: int = 0 + for ci in range(int(cn_count)): + child: ast.AST* = ch.get(ci) + if child is None: continue + kd: int = child.kind() + # When _declare_only == 0, _translate_module_level already handled imports; + # skip here to avoid duplicating from_imports entries. + if trans._declare_only == 0 and (kd == ast.ASTKind.Import or kd == ast.ASTKind.ImportFrom): continue + if kd == ast.ASTKind.Import: self.Gen.ImportsGen.translate_import(trans, child) + elif kd == ast.ASTKind.ImportFrom: + self.Gen.ImportsGen.translate_import_from_module(trans, child) + self.Gen.ImportsGen.translate_import_from_names(trans, child) + elif kd == ast.ASTKind.FunctionDef: + # PEP 695 generic functions (def f[T]): skip at the definition site (the + # template is registered by Phase1Scan/Phase2) and monomorphize at call sites. + fd_tc: ast.FunctionDef* = ast.FunctionDef*(child) + if fd_tc is not None and fd_tc.type_params is not None: continue + # Phase 1a declare mode registers only CExport/State names; Phase 1b fully translates bodies. + if trans._declare_only == 0: + # 函数体翻译 = 编译器下潜进一个源码上下文:推一帧(def 位置), + # 让函数体内部的错误在 traceback 里回溯到"正在翻译哪个函数"(调用链)。 + tr_f: str = self.Gen.TypeGenResolve.get_current_file() + # §13 NLL:记录当前函数体(谓词 NllFrameConfined 要遍历它) + prev_nf: ast.AST* = self.Gen.StructGen.NllCurFunc + self.Gen.StructGen.NllCurFunc = child + self.Gen.TypeGenBase.push_trace(tr_f, fd_tc.lineno) + added: int = self.Gen.FuncGenDef.translate_function_def(trans, child) + self.Gen.TypeGenBase.pop_trace() + self.Gen.StructGen.NllCurFunc = prev_nf + added_total += added + elif trans._declare_only == 1: + self._register_cexport_from_funcdef(trans, child) + # Prescan signature types to trigger generic specializations before mark1, + # so their Ty/Fields are not recycled by Phase 1b mark/reset (avoids UAF). + self._prescan_funcdef_signatures(trans, child) + elif kd == ast.ASTKind.ClassDef: + # ClassDef is handled directly at module level (no builder needed); skip in + # _declare_only=2 (import-scan) mode, which only handles import dependencies. + if trans._declare_only != 2: self.Gen.ClassGenDef.translate_class_def(trans, child) + elif trans._declare_only == 0 and trans._cur_builder is not None: + # Has a builder -> delegate statement dispatch to HandlesBody + added = self.Gen.StmtGen.translate_stmt(trans, child) + added_total += added + elif kd == ast.ASTKind.AnnAssign and trans._declare_only != 2: + # Module-level AnnAssign: skip in import-scan mode, handle only CDefine in + # struct-registration mode, and handle all in full-translation mode. + added = self.handle_module_level_var(trans, child) + added_total += added + elif trans._declare_only == 0 and kd == ast.ASTKind.Assign: + # Module-level Assign without a builder -> create a global (full-translation mode only) + added = self.handle_module_level_var(trans, child) + added_total += added + + return added_total + + # _prescan_funcdef_signatures - Phase 1a: resolve module-level function signatures to + # trigger generic specializations before mark1 (avoids dangling structs / UAF in Phase 2). + def _prescan_funcdef_signatures(self, trans: HT.Translator*, node: ast.AST*) -> None: + """Pre-resolve module-level function signature types to trigger generic specializations.""" + if trans is None or node is None: return + # The entry takes the base ast.AST* (matching sibling dispatchers) and narrows it here + # explicitly to ast.FunctionDef*: implicit downcasting is disallowed under strict rules. + fd: ast.FunctionDef* = ast.FunctionDef*(node) + if fd is None: return + pool: memhub.MemBuddy* = trans.Pool + if pool is None: return + imp: str = trans._imported_modules + fi: str = trans._from_imports + if fd.returns is not None: self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imp, fi, trans, 0) + if fd.args is not None: + ags: ast.Arguments* = ast.Arguments*(fd.args) + if ags.args is not None: + al: list[ast.AST*]* = ags.args + an: sizet = len(al) + ai: sizet + for ai in range(int(an)): + ag: ast.Arg* = ast.Arg*(al.get(ai)) + if ag is not None and ag.annotation is not None: self.Gen.TypeGenResolve.resolve_annotation_type(pool, ag.annotation, imp, fi, trans, 0) + + # _build_array_initializer_text - build an LLVM IR array initializer from an AST List, + # formatted as [N x elem_ty] [elem_ty val0, ...], for module-level t.CArray literals. + def _build_array_initializer_text(self, pool: memhub.MemBuddy*, var_ty: mlirlite.LLVMType*, list_node: ast.List*) -> str: + """Build LLVM IR array initializer text from an AST List node; None on failure.""" + if var_ty is None or list_node is None: return None + + # Re-cast so the VPC compiler recognizes ast.List, avoiding type inference degrading + # to t.CPtr when init_list_node's initial value is None. + ln: ast.List* = ast.List*(list_node) + if ln is None: return None + + # Extract array element type and count from var_ty + elem_ty: mlirlite.LLVMType* = None + arr_count: int = 0 + match var_ty: + case mlirlite.LLVMType.Array(et, cnt): + elem_ty = et + arr_count = cnt + + if elem_ty is None or arr_count <= 0: return None + + # Determine the IR representation of the element type + elem_ir_ty: str = "i8" + is_int_elem: int = 1 + match elem_ty: + case mlirlite.LLVMType.Int(bits): + is_int_elem = 1 + if bits == 8: elem_ir_ty = "i8" + elif bits == 16: elem_ir_ty = "i16" + elif bits == 32: elem_ir_ty = "i32" + elif bits == 64: elem_ir_ty = "i64" + case _: + is_int_elem = 0 + + # Get list elements via ln.elts (not list_node.elts) to ensure correct type recognition + elts: list[ast.AST*]* = ln.elts + if elts is None: return None + elts_count: sizet = len(elts) + + # More elements than the declared length -> fail-fast (never silently truncate or drop). + if sizet(arr_count) < elts_count: + self.Gen.TypeGenErr.fatal_error(list_node, f"array initializer has {elts_count} elements, exceeding the declared array length {arr_count}; silent truncation is not allowed", "OverflowError") + return None + + # Non-integer element types (aggregates): only an all-zero list yields the whole + # "zeroinitializer"; any non-zero constant is not expressible as text -> None. + if is_int_elem == 0: + # Nested array element type: build sub-array initializer text recursively with the + # inner type omitted (inferred from the outer global type), which llc accepts. + is_nested_arr: int = 0 + match elem_ty: + case mlirlite.LLVMType.Array(_, _): + is_nested_arr = 1 + case _: + pass + if is_nested_arr != 0: + cnt_total_n: sizet = sizet(arr_count) + buf_size_n: sizet = 128 + cnt_total_n * 768 + buf_n: str = pool.alloc(buf_size_n) + if buf_n is None: return None + written_n: int = viperlib.snprintf(buf_n, buf_size_n, "[") + pos_n: sizet = sizet(written_n) + # Sub-array type prefix (e.g. "[16 x i8]"): nested initializers must spell out the + # inner type, so serialize elem_ty via TypePrint for the [[16 x i8][...]] form. + elem_ty_str_n: str = pool.alloc_tagged(4096, memhub.DETSITE_TRANSLATORGEN_185) + if elem_ty_str_n is None: return None + mlirlite.TypePrint(elem_ty_str_n, 4096, elem_ty, pool) + for i_n in range(int(cnt_total_n)): + sub_n: str = None + if i_n < elts_count: + en_n: ast.AST* = elts.get(i_n) + if en_n is not None: + ek_n: int = en_n.kind() + if ek_n == ast.ASTKind.List: sub_n = self._build_array_initializer_text(pool, elem_ty, ast.List*(en_n)) + elif ek_n == ast.ASTKind.Set: + # Brace literals (e.g. font tables {0x00, ...}): Set.elts is + # isomorphic to List, so wrap it in a temp List and recurse. + se_n: ast.Set* = ast.Set*(en_n) + if se_n is not None and se_n.elts is not None: sub_n = self._build_array_initializer_text(pool, elem_ty, ast.List(pool, se_n.elts, ast.ASTCtx.Load)) + if sub_n is None: + # Missing/unevaluable element: zero-initialize the whole sub-array + sub_n = "zeroinitializer" + if i_n > 0: + written_n = viperlib.snprintf(buf_n + pos_n, buf_size_n - pos_n, ", ") + pos_n += sizet(written_n) + written_n = viperlib.snprintf(buf_n + pos_n, buf_size_n - pos_n, "%s%s", elem_ty_str_n, sub_n) + pos_n += sizet(written_n) + viperlib.snprintf(buf_n + pos_n, buf_size_n - pos_n, "]") + return buf_n + for zi in range(int(elts_count)): + ze: ast.AST* = elts.get(zi) + if ze is None: continue + zk: int = ze.kind() + if zk == ast.ASTKind.Constant: + zc: ast.Constant* = ast.Constant*(ze) + if zc is None: continue + # Treat None (CONST_NONE) as zero: function-pointer arrays use [None] to mean + # all-empty, so None must normalize to zero fill rather than fail-fast. + if zc.const_kind == ast.CONST_NONE: continue + if zc.const_kind != ast.CONST_INT or zc.int_val != 0: return None + elif zk == ast.ASTKind.UnaryOp: + if self.Gen.AssignGen.extract_cdefine_int_value(ze) != 0: return None + else: return None + return "zeroinitializer" + + # Size the buffer from the declared length arr_count (missing elements are zero-filled, + # so it may exceed elts_count); each element needs about 26 chars. + cnt_total: sizet = sizet(arr_count) + buf_size: sizet = 64 + cnt_total * 32 + buf: str = pool.alloc(buf_size) + if buf is None: return None + + # Write the "[" prefix (the type prefix is emitted by _print_global; only the element list here) + written: int = viperlib.snprintf(buf, buf_size, "[") + pos: sizet = sizet(written) + + # Write elements up to the declared length arr_count, zero-padding short lists so the + # initializer element count always matches the type llc expects. + for i in range(int(cnt_total)): + elem_val: int64 = 0 + if i < elts_count: + elem_node: ast.AST* = elts.get(i) + # Read int_val directly from the Constant node (avoids int truncation in extract_cdefine_int_value) + if elem_node is not None: + ek: int = elem_node.kind() + if ek == ast.ASTKind.Constant: + ec: ast.Constant* = ast.Constant*(elem_node) + # CONST_CHAR literals (e.g. `_ps2_set1 = ['\x1b', '1', ...]`): int_val is + # the character code (unrecognized chars would all be compiled as 0). + if ec is not None and (ec.const_kind == ast.CONST_INT or ec.const_kind == ast.CONST_CHAR): elem_val = ec.int_val + # Single-char CONST_STR (double quotes like "'" / '"'): the parser treats + # them as CONST_STR; take the first char code when strlen == 1, else 0. + elif ec is not None and ec.const_kind == ast.CONST_STR: + if ec.str_val is not None and string.strlen(ec.str_val) == 1: elem_val = int64(ec.str_val[0]) + elif ek == ast.ASTKind.UnaryOp: + # Negative: UnaryOp(USub, Constant) + elem_val = self.Gen.AssignGen.extract_cdefine_int_value(elem_node) + if i > 0: + written = viperlib.snprintf(buf + pos, buf_size - pos, ", ") + pos += sizet(written) + written = viperlib.snprintf(buf + pos, buf_size - pos, "%s %lld", elem_ir_ty, elem_val) + pos += sizet(written) + + # Write the closing "]" suffix + viperlib.snprintf(buf + pos, buf_size - pos, "]") + return buf + + # _build_string_array_initializer_text - initialize a char array global from a string literal: + # t.CArray[char, N] = "..." -> [i8 b0, ..., i8 0, ...] zero-padded; empty -> zeroinitializer. + def _build_string_array_initializer_text(self, pool: memhub.MemBuddy*, var_ty: mlirlite.LLVMType*, str_val: str) -> str: + """Build initializer text for an array global from a string literal; None on failure.""" + if var_ty is None or str_val is None: return None + arr_count: int = 0 + match var_ty: + case mlirlite.LLVMType.Array(_, cnt): + arr_count = cnt + if arr_count <= 0: return None + slen: sizet = string.strlen(str_val) + if slen == 0: return "zeroinitializer" + buf_size: sizet = 64 + sizet(arr_count) * 16 + buf: str = pool.alloc(buf_size) + if buf is None: return None + written: int = viperlib.snprintf(buf, buf_size, "[") + pos: sizet = sizet(written) + for i in range(int(sizet(arr_count))): + if i > 0: + written = viperlib.snprintf(buf + pos, buf_size - pos, ", ") + pos += sizet(written) + bv: int64 = 0 + if i < slen: bv = int64(str_val[i]) + written = viperlib.snprintf(buf + pos, buf_size - pos, "i8 %lld", bv) + pos += sizet(written) + viperlib.snprintf(buf + pos, buf_size - pos, "]") + return buf + + # _build_string_ptr_initializer - initialize a pointer-typed global from a string literal by + # creating an internal constant `@.str.{var_name}` and returning a GEP initializer text. + def _build_string_ptr_initializer(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, var_name: str, str_val: str) -> str: + """Create an internal constant global for a string literal; return GEP init text.""" + if str_val is None or var_name is None: return None + + # Build the internal string constant name: .str.{var_name} + str_name_buf: str = pool.alloc(64) + if str_name_buf is None: return None + viperlib.snprintf(str_name_buf, 64, ".str.%s", var_name) + + # Create the string constant global via mlirlite.create_global_string + str_gv: mlirlite.GlobalVariable* = mlirlite.create_global_string(pool, mod, str_name_buf, str_val) + if str_gv is None: return None + + # Generate the GEP initializer text + str_len: sizet = string.strlen(str_val) + init_buf: str = pool.alloc(128) + if init_buf is None: return None + viperlib.snprintf(init_buf, 128, "getelementptr inbounds ([%d x i8], [%d x i8]* @%s, i32 0, i32 0)", str_len + 1, str_len + 1, str_name_buf) + return init_buf + + def _stg_field_zero_text(self, pool: memhub.MemBuddy*, fty: mlirlite.LLVMType*, ty_str: str) -> str: + """Build the zero initializer text for a storage struct field by LLVM type.""" + if fty is None or ty_str is None: return None + # Size by the actual type text: TypePrint inlines full struct bodies (up to 4096 bytes), and + # the historic fixed 160-byte buffer was written with a 576-byte snprintf limit, so a long + # type text overflowed the pool block and corrupted the neighbouring free block. + cap: sizet = string.strlen(ty_str) + 24 + buf: str = pool.alloc(cap) + if buf is None: return None + match fty: + case mlirlite.LLVMType.Ptr(_): + viperlib.snprintf(buf, cap, "%s null", ty_str) + case mlirlite.LLVMType.Int(_): + viperlib.snprintf(buf, cap, "%s 0", ty_str) + case mlirlite.LLVMType.Float(_): + viperlib.snprintf(buf, cap, "%s 0.0", ty_str) + case _: + # Aggregates such as Array / Struct -> zeroinitializer + viperlib.snprintf(buf, cap, "%s zeroinitializer", ty_str) + return buf + + # _default_val_const_text - field default value -> LLVM constant element text; only foldable + # integer defaults (CONST_INT/CONST_CHAR, negated literals) are handled; others return None. + def _default_val_const_text(self, pool: memhub.MemBuddy*, fty: mlirlite.LLVMType*, ty_str: str, dval: ast.AST*) -> str: + """Render a field default as "ty value" text; None when not foldable.""" + if dval is None or fty is None or ty_str is None: return None + is_int: int = 0 + dv_int: int64 = 0 + if dval.kind() == ast.ASTKind.Constant: + dc: ast.Constant* = ast.Constant*(dval) + if dc is not None: + if dc.const_kind == ast.CONST_INT or dc.const_kind == ast.CONST_CHAR: + dv_int = dc.int_val + is_int = 1 + elif dval.kind() == ast.ASTKind.UnaryOp: + # Negative / bitwise-invert literals (e.g. -1 / 0xFFFFFFFFFFFFFFFF) + dv_int = self.Gen.AssignGen.extract_cdefine_int_value(dval) + is_int = 1 + if is_int == 0: return None + # Only integer fields accept an integer constant; float/pointer/aggregate fall back to zero + match fty: + case mlirlite.LLVMType.Int(_): + pass + case _: + return None + buf: str = pool.alloc(576) + if buf is None: return None + viperlib.snprintf(buf, 576, "%s %lld", ty_str, dv_int) + return buf + + # _build_stg_struct_body_text - storage struct field element text (no braces), used by + # _build_stg_struct_init_text and vtable storage; None when all field defaults are zero. + def _build_stg_struct_body_text(self, pool: memhub.MemBuddy*, se: GenTypes.StructEntry*, start_idx: int) -> str: + """Render "e0, e1, ..." element text from field defaults; None when all zero.""" + if se is None or se.Fields is None: return None + fc: int = se.FieldCount + if fc <= start_idx: return None + # Each field element = type prefix (up to _TY_TEXT_CAP bytes in MLIR form) + value text; + # the old 160-byte estimate truncated longer types. + buf_size: sizet = 256 + sizet(fc) * 576 + buf: str = pool.alloc(buf_size) + if buf is None: return None + pos: sizet = 0 + first: int = 1 + has_nonzero: int = 0 + i: int = start_idx + while i < fc: + fe: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(se, i) + i += 1 + if fe is None or fe.Ty is None: continue + ty_str: str = pool.alloc_tagged(4096, memhub.DETSITE_TRANSLATORGEN_384) + if ty_str is None: continue + mlirlite.TypePrint(ty_str, 4096, fe.Ty, pool) + elem: str = self._default_val_const_text(pool, fe.Ty, ty_str, fe.DefaultVal) + is_zero: int = 0 + if elem is None: + elem = self._stg_field_zero_text(pool, fe.Ty, ty_str) + is_zero = 1 + if elem is None: continue + written: int = 0 + if first == 0: + written = viperlib.snprintf(buf + pos, buf_size - pos, ", ") + pos += sizet(written) + written = viperlib.snprintf(buf + pos, buf_size - pos, "%s", elem) + pos += sizet(written) + first = 0 + if is_zero == 0: has_nonzero = 1 + if has_nonzero == 0: return None + return buf + + # _build_stg_struct_init_text - full constant initializer for a storage struct (all fields, + # with defaults) for module-level `X: T = T()`; None when no non-zero default exists. + def _build_stg_struct_init_text(self, pool: memhub.MemBuddy*, se: GenTypes.StructEntry*, start_idx: int) -> str: + """Render a full struct initializer from field defaults; None when all zero.""" + body: str = self._build_stg_struct_body_text(pool, se, start_idx) + if body is None: return None + body_len: sizet = sizet(string.strlen(body)) + buf: str = pool.alloc(body_len + 8) + if buf is None: return None + viperlib.snprintf(buf, body_len + 8, "{ %s }", body) + return buf + + # Module-level global t.attr.* extraction: attributes live in the annotation union + # (`T | t.attr.section(".x")`) and are written directly to gv.Section/Align/Linkage fields. + def _scan_global_tattr(self, node: ast.AST*, want_name: str) -> ast.AST*: + """Find the t.attr. node in a global type annotation; None when absent.""" + if node is None: return None + if node.kind() == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(node) + if bop.op == ast.OpKind.BitOr: + hit_l: ast.AST* = self._scan_global_tattr(bop.left, want_name) + if hit_l is not None: return hit_l + return self._scan_global_tattr(bop.right, want_name) + return None + if self.Gen.ClassGenBase._tattr_name(node, want_name) != 0: return node + return None + def _extract_global_attr_section(self, node: ast.AST*, var_name: str) -> str: + """Extract the section name from `T | t.attr.section("xxx")`; fail-fast when empty.""" + hit: ast.AST* = self._scan_global_tattr(node, "section") + if hit is None: return None + sec_val: str = self.Gen.FuncGenBase._get_attr_call_arg_str(hit) + if sec_val is None or sec_val[0] == 0: + self.Gen.TypeGenErr.fatal_error(node, f"section name of t.attr.section(...) for module-level variable '{var_name}' is missing or empty", "ValueError") + return None + return sec_val + def _extract_global_attr_aligned(self, node: ast.AST*, var_name: str) -> int: + """Extract alignment bytes from `T | t.attr.aligned(N)`; 0 when absent, fail-fast when invalid.""" + hit: ast.AST* = self._scan_global_tattr(node, "aligned") + if hit is None: return 0 + al_val: int = self.Gen.ClassGenBase._tattr_aligned_int(hit) + if al_val <= 0: + self.Gen.TypeGenErr.fatal_error(node, f"t.attr.aligned(N) for module-level variable '{var_name}' requires a positive integer literal N", "ValueError") + return 0 + return al_val + def _extract_global_attr_weak(self, node: ast.AST*) -> int: + """Detect `T | t.attr.weak` / `t.attr.weak()` (1 present, 0 absent).""" + if self._scan_global_tattr(node, "weak") is not None: return 1 + return 0 + + # handle_module_level_var - module-level var declaration -> LLVM global (int/list/string init). + # _RegisterStaticModuleVar - register (name, def module SHA1) of t.CStatic vars for cross-module access checks. + def _RegisterStaticModuleVar(self, trans: HT.Translator*, annot_node: ast.AST*, var_name: str) -> int: + if trans is None or annot_node is None or var_name is None: return 0 + if trans.ModuleSha1 is None: return 0 + if self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CStatic", 0) == 0: return 0 + self.Gen.CallGenBase.RegisterStaticSymbol(trans.ModuleSha1, var_name) + return 0 + + + def handle_module_level_var(self, trans: HT.Translator*, node: ast.AST*) -> int: + """Handle a module-level variable declaration by creating its global.""" + pool: memhub.MemBuddy* = trans.Pool + mod: mlirlite.LLVMModule* = trans.Module + + # CDefine annotation: a compile-time constant, no global is created; register it in the + # global CDefine table so t.CArray[elem_ty, NAME] can resolve it. + k: int = node.kind() + if k == ast.ASTKind.AnnAssign: + aa_cd: ast.AnnAssign* = ast.AnnAssign*(node) + if aa_cd is not None and aa_cd.target is not None: + if aa_cd.target.kind() == ast.ASTKind.Name: + nm_cd: ast.Name* = ast.Name*(aa_cd.target) + if nm_cd.id is not None: + if self.Gen.AssignGen.is_cdefine_annotation(aa_cd.annotation) != 0: + # Auto-detect and register float/int CDefine values (e.g. vipermath U_M_PI) + self.Gen.AssignGen._register_cdefine_from_value(pool, nm_cd.id, aa_cd.value, aa_cd.annotation) + return 0 + # CTypedef annotation: no global is created (would duplicate the symbol); + # register it in the CTypedef table instead, or annotation lookups fail. + if self.Gen.TypeGenCType._is_ctypedef_annotation(aa_cd.annotation) != 0: + ctd_str: str = self.Gen.TypeGenResolve._get_type_name_from_annotation(pool, aa_cd.value) + if ctd_str is not None: self.Gen.TypeGenCType.register_ctypedef(pool, nm_cd.id, ctd_str) + return 0 + + # In _declare_only=1 (struct-registration) mode only CDefine is handled; reaching here + # means it is not a CDefine, so skip. + if trans._declare_only == 1: return 0 + + var_name: str = None + var_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) + resolved_stg_ty: mlirlite.LLVMType* = None # Object-class global: static storage type (BSS instance) + resolved_stg_entry: GenTypes.StructEntry* = None # Object-class global: matching StructEntry (for vtable init) + init_val: int64 = 0 + init_fval: double = 0.0 # Value when init_kind==4 (float literal) init_kind: int = 0 # 0=none, 1=int, 2=list, 3=str, 4=float, 5=symbol address - init_sym_node: ast.AST* = None # Operand of `= &global` (init_kind==5) - init_str_val: str = None - init_list_node: ast.List* = None - # Explicit `= None` init keeps lazy-initialization pointer semantics: the global must stay - # NULL, so no BSS storage is created (storage would break the `!= None` guard). - is_none_init: int = 0 - annot_node: ast.AST* = None # Annotation node (used to detect t.CExport markers) - - if k == ast.ASTKind.AnnAssign: - aa: ast.AnnAssign* = ast.AnnAssign*(node) - if aa is None or aa.target is None: return 0 - if aa.target.kind() != ast.ASTKind.Name: return 0 - nm: ast.Name* = ast.Name*(aa.target) - var_name = nm.id - annot_node = aa.annotation - # f-string escape: a module-level initial value is global, but the f-string result - # points into the current function's stack buffer -> fail-fast (use escape_fstring_copy). - GenApi.fail_if_fstring_escape(aa.value, "global variable initialization") - # t.CStatic vars (no cross-module use) are registered as (name, def module SHA1); - # the logic stays inside _RegisterStaticModuleVar to avoid disturbing type inference. - self._RegisterStaticModuleVar(trans, annot_node, var_name) - # Module-level t.Callable function pointers register (name -> return type) here so - # indirect calls resolve; done before any body translation, same as in-function AnnAssign. - if annot_node is not None: - ml_callable_ret: mlirlite.LLVMType* = self.Gen.TypeGenErr.try_extract_callable_ret(pool, annot_node, trans._imported_modules,trans._from_imports, trans) - if ml_callable_ret is not None: - # 0 = success/idempotent; non-zero already fatal-errored inside register_func_ptr_sig, - # so this branch is only defensive. - if self.Gen.CallGenLeafKit.register_func_ptr_sig(pool, var_name, ml_callable_ret, aa) != 0: return 0 - # Resolve the type - if aa.annotation is not None: - # Value-reference markers are parameter-only: a module variable `x: &T` fails fast. - self.Gen.TypeGenResolve.reject_ref_marker_position(aa.annotation, trans._imported_modules, "a module-level variable declaration") - resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, aa.annotation, trans._imported_modules, trans._from_imports, trans, 0) - if resolved is not None: - # Object-class module vars (@t.Object/@t.CVTable: object-as-pointer) wrap the - # resolved struct value in Ptr; pure t.CStruct keeps a value instance. - if resolved.kind() == mlirlite.LLVMType.Struct: - gv_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(resolved) - if gv_entry is not None and (gv_entry.HasNew != 0 or gv_entry.HasVTable != 0 or gv_entry.IsOOP != 0): - # Object-class global (object-as-pointer): remember the Struct value type - # so a BSS storage global can hold the instance; otherwise @g is NULL. - resolved_stg_ty = resolved - resolved_stg_entry = gv_entry - resolved = mlirlite.Ptr(pool, resolved) - elif resolved.kind() == mlirlite.LLVMType.Ptr: - # Object-class union annotation (T* -> T*): also create storage, else the - # virtual call loads NULL @g and faults; var_ty stays Ptr. - pointee_uv: mlirlite.LLVMType* = resolved.Pointee - if pointee_uv is not None and pointee_uv.kind() == mlirlite.LLVMType.Struct: - gv_entry_u: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(pointee_uv) - if gv_entry_u is not None and (gv_entry_u.HasNew != 0 or gv_entry_u.HasVTable != 0 or gv_entry_u.IsOOP != 0): - resolved_stg_ty = pointee_uv - resolved_stg_entry = gv_entry_u - var_ty = resolved - else: - # [S4 fail-fast] An annotated module var that fails to resolve errors out instead - # of silently using i32 (a wrong type corrupts storage size/field access/ABI). - self.Gen.TypeGenErr.fatal_error(aa.annotation, f"failed to resolve the type annotation of module-level variable '{var_name}'", "NameError") - return 0 - # Parse the initial value (integer, negative literal, list literal, string literal) - if aa.value is not None: - val_kind: int = aa.value.kind() - if val_kind == ast.ASTKind.Constant: - cn: ast.Constant* = ast.Constant*(aa.value) - if cn.const_kind == ast.CONST_NONE: is_none_init = 1 - elif cn.const_kind == ast.CONST_INT: - init_val = cn.int_val - init_kind = 1 - elif cn.const_kind == ast.CONST_STR: - init_str_val = cn.str_val - init_kind = 3 - elif cn.const_kind == ast.CONST_FLOAT: - # Float literal: init_kind must be set or the global degrades to - # zeroinitializer; the value is re-read from source at init_kind==4 below. - init_kind = 4 - elif val_kind == ast.ASTKind.UnaryOp: - # Negative/bitwise-invert literal: the AST is UnaryOp(USub, Constant); unhandled - # it would compile to zeroinitializer and lose the initial value. `&global` - # (UnaryOp(AddrOf, Name)) is a symbol-address initializer instead and must not be - # fed to the integer constant extractor. - uop_g: ast.UnaryOp* = ast.UnaryOp*(aa.value) - if uop_g is not None and uop_g.op == ast.OpKind.AddrOf: - init_sym_node = uop_g.operand - init_kind = 5 - else: - init_val = self.Gen.AssignGen.extract_cdefine_int_value(aa.value) - init_kind = 1 - elif val_kind == ast.ASTKind.List: - init_list_node = ast.List*(aa.value) - init_kind = 2 - elif k == ast.ASTKind.Assign: - asgn: ast.Assign* = ast.Assign*(node) - if asgn is None or asgn.targets is None: return 0 - targets: list[ast.AST*]* = asgn.targets - if len(targets) < 1: return 0 - t0: ast.AST* = targets.get(0) - if t0 is None or t0.kind() != ast.ASTKind.Name: return 0 - nm2: ast.Name* = ast.Name*(t0) - var_name = nm2.id - if asgn.value is not None: - val_kind2: int = asgn.value.kind() - if val_kind2 == ast.ASTKind.Constant: - cn2: ast.Constant* = ast.Constant*(asgn.value) - if cn2.const_kind == ast.CONST_NONE: is_none_init = 1 - elif cn2.const_kind == ast.CONST_INT: - init_val = cn2.int_val - init_kind = 1 - elif cn2.const_kind == ast.CONST_STR: - init_str_val = cn2.str_val - init_kind = 3 - elif cn2.const_kind == ast.CONST_FLOAT: - # Unannotated module-level float assignment: the literal itself implies the - # type, so promote to double (default var_ty is i32). - init_kind = 4 - var_ty = mlirlite.Double(pool) - elif val_kind2 == ast.ASTKind.UnaryOp: - # Negative literal (same as the AnnAssign branch above): UnaryOp(USub, Constant). - # `= &global` is a symbol-address initializer instead (see the AnnAssign branch). - uop_g2: ast.UnaryOp* = ast.UnaryOp*(asgn.value) - if uop_g2 is not None and uop_g2.op == ast.OpKind.AddrOf: - init_sym_node = uop_g2.operand - init_kind = 5 - else: - init_val = self.Gen.AssignGen.extract_cdefine_int_value(asgn.value) - init_kind = 1 - elif val_kind2 == ast.ASTKind.List: - init_list_node = ast.List*(asgn.value) - init_kind = 2 - elif val_kind2 == ast.ASTKind.Attribute: - # Module-level re-export alias (e.g. `_read32 = types._read32`): the RHS is an - # entity reference with no value semantics, so emit no LLVM global (same as __all__). - return 0 - - if var_name is None: return 0 - - # __all__ is Python module metadata with no runtime semantics, so skip global creation; - # otherwise the unannotated list-of-strings global would falsely fail initializer building. - if string.strcmp(var_name, "__all__") == 0: return 0 - - # Float literal init (init_kind==4): re-read the value from source by name instead of the - # AST float_val (which pool ops may overwrite); also validate that the annotation is float. - if init_kind == 4: - is_fty_g: int = 0 - match var_ty: - case mlirlite.LLVMType.Float(_): - is_fty_g = 1 - case _: - pass - if is_fty_g == 0: - self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with a float literal, but its annotation is not a float type", "TypeError") - return 0 - if self.Gen.AssignGen._recover_float_literal(var_name, &init_fval, 0) != 1: - self.Gen.TypeGenErr.fatal_error(node, f"failed to recover the initializer value of module-level float variable '{var_name}' from the source file", "InternalCompilerError") - return 0 - - # Check whether it is already registered - existing: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, var_name) - if existing is not None: return 0 - - # Create the global @var_name, first ensuring opaque declarations for any cross-module - # struct type in var_ty, or llc reports "use of undefined type named 'sha1.ClassName'". - mlirlite.module_ensure_opaque_for_type(mod, pool, var_ty) - # Module-level globals are SHA1-prefixed (like function mangling) to avoid duplicate - # symbols; SymTab keys stay bare, and t.CExport globals keep bare names for asm references. - is_global_cexport: int = 0 - if annot_node is not None: is_global_cexport = self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CExport", 0) - # extern globals (annotation has t.State/t.CExtern) are declared only: `@bare = external - # global T` with external linkage, no initializer and no storage; export instead defines them. - is_global_extern: int = 0 - if annot_node is not None: - is_global_extern = self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CExtern", 0) - if is_global_extern == 0: is_global_extern = self.Gen.TypeGenErr.has_decorator_marker(annot_node, "State", 0) - # Global-side t.attr.* attributes (`T | t.attr.xxx` union): section->gv.Section, - # aligned->gv.Align, weak->gv.Linkage; only present for AnnAssign (annot_node not None). - gv_attr_section: str = None - gv_attr_align: int = 0 - gv_attr_weak: int = 0 - if annot_node is not None: - gv_attr_section = self._extract_global_attr_section(annot_node, var_name) - gv_attr_align = self._extract_global_attr_aligned(annot_node, var_name) - gv_attr_weak = self._extract_global_attr_weak(annot_node) - mangled_name: str = var_name - if is_global_cexport == 0 and is_global_extern == 0: mangled_name = self.Gen.FuncGenBase._mangle_name(trans, var_name) - gv: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, mangled_name, var_ty) - if gv is None: return 0 - mlirlite.module_add_global(mod, gv) - - # Section name: write the field directly (_print_global adds the quotes for `, section "xxx"`). - if gv_attr_section is not None: mlirlite.global_set_section(gv, gv_attr_section) - - # B5: apply alignment to global instances of @c.Attribute(t.attr.aligned(N)) classes. - # An explicit variable-level `T | t.attr.aligned(N)` wins over the class-level struct Align. - al_ty: mlirlite.LLVMType* = var_ty - match var_ty: - case mlirlite.LLVMType.Ptr(pointee_al): - al_ty = pointee_al - case _: - pass - al_entry: GenTypes.StructEntry* = None - if al_ty is not None and al_ty.kind() == mlirlite.LLVMType.Struct: al_entry = self.Gen.StructGen.find_struct_by_type(al_ty) - if gv_attr_align > 0: gv.Align = gv_attr_align - elif al_entry is not None and al_entry.Align > 0: gv.Align = al_entry.Align - - # Set the initializer (clearing external linkage, since LLVM 22+ forbids external globals - # with initializers); extern globals are the exception and stay external. - if is_global_extern != 0: gv.Linkage = "external" - else: - gv.Linkage = None - # t.attr.weak -> weak linkage (must be set after Linkage is cleared). - if gv_attr_weak != 0: mlirlite.global_set_linkage(gv, "weak") - if init_kind == 1: - # Integer initializer - init_buf: str = pool.alloc(48) - if init_buf is not None: - viperlib.snprintf(init_buf, 48, "%lld", init_val) - gv.Initializer = init_buf - elif init_kind == 2: - # List literal -> array initializer [N x elem_ty] [elem_ty val0, ...] - arr_init: str = self._build_array_initializer_text(pool, var_ty, init_list_node) - if arr_init is not None: gv.Initializer = arr_init - else: - # Initializer build failed: error out (fail-fast) instead of falling back to zeroinitializer - self.Gen.TypeGenErr.fatal_error(node, f"failed to build the initializer of module-level variable '{var_name}'", "TypeError") - elif init_kind == 3: - # String literal initializer: for array types (t.CArray[char, N]) use a byte-array - # initializer (content + zero padding); a string-pointer GEP initializer would mismatch. - is_arr_g: int = 0 - match var_ty: - case mlirlite.LLVMType.Array(_, _): - is_arr_g = 1 - case _: - pass - if is_arr_g != 0: - arr_str_init: str = self._build_string_array_initializer_text(pool, var_ty, init_str_val) - if arr_str_init is not None: gv.Initializer = arr_str_init - else: self.Gen.TypeGenErr.fatal_error(node, f"failed to build the string initializer of module-level array variable '{var_name}'", "TypeError") - else: - # Pointer type (e.g. b64_tab: t.CArray[char, None] = "ABC..."): create an internal - # string constant global and reference it via a GEP initializer. - str_init: str = self._build_string_ptr_initializer(pool, mod, var_name, init_str_val) - if str_init is not None: gv.Initializer = str_init - else: self.Gen.TypeGenErr.fatal_error(node, f"failed to build the initializer of module-level variable '{var_name}'", "TypeError") - elif init_kind == 4: - # Float initializer: LLVM float constants must be written as bit-pattern text - # (0x%016llx / 0x%08x); decimal text has ulp errors, and omitting this zeroes the global. - fc_g: mlirlite.Value* = mlirlite.ConstFloat(pool, var_ty, init_fval) - if fc_g is not None and fc_g.Name is not None: gv.Initializer = fc_g.Name - else: self.Gen.TypeGenErr.fatal_error(node, f"failed to build the float initializer of module-level variable '{var_name}'", "TypeError") - elif init_kind == 5: - # Symbol-address initializer (`= &global`): the referenced global's mangled symbol is - # emitted as `@name` (LLVM-quoted when required); the MLIR layer renders it as - # llvm.mlir.addressof. Only a pointer-typed global can hold an address. - is_ptr_g: int = 0 - match var_ty: - case mlirlite.LLVMType.Ptr(_): - is_ptr_g = 1 - case _: - pass - if is_ptr_g == 0: - self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with an address-of expression, but its type is not a pointer (add a pointer annotation or drop the '&')", "TypeError") - return 0 - if init_sym_node is None or init_sym_node.kind() != ast.ASTKind.Name: - self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with an address-of expression whose operand is not a variable name", "TypeError") - return 0 - sym_nm: ast.Name* = ast.Name*(init_sym_node) - sym_val: mlirlite.Value* = None - if sym_nm.id is not None: sym_val = self.Gen.VarGen.lookup_module_var(trans.SymTab, sym_nm.id) - if sym_val is None or sym_val.Name is None: - self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with &{sym_nm.id}, but '{sym_nm.id}' is not a module-level variable declared before this line", "NameError") - return 0 - sym_buf: str = pool.alloc(256) - if sym_buf is None: return 0 - # The module-scope entry already holds the LLVM reference text of that global - # (`@mangled` or `@"mangled"`), so it is used verbatim; only a bare name (no '@') needs - # the reference prefix added here. - if sym_val.Name[0] == '@': viperlib.snprintf(sym_buf, 256, "%s", sym_val.Name) - elif self.Gen.ClassGenVTable._vtable_name_needs_quote(sym_val.Name) != 0: viperlib.snprintf(sym_buf, 256, "@\"%s\"", sym_val.Name) - else: viperlib.snprintf(sym_buf, 256, "@%s", sym_val.Name) - gv.Initializer = sym_buf - else: - # Use zeroinitializer (valid for pointer/integer/aggregate types). Object-class globals - # with storage get &@storage below; `= None` lazy globals still need it to stay complete. - if (resolved_stg_ty is None or is_none_init != 0) and is_global_extern == 0: gv.Initializer = "zeroinitializer" - - # Object-class globals: create a BSS storage global holding the instance (@g references it, - # so it is never NULL); `= None` lazy globals and extern globals skip storage instead. - if resolved_stg_ty is not None and is_none_init == 0 and is_global_extern == 0: - # Value-type storage needs a full struct definition, since cross-module classes are - # opaque by default and a zeroinitializer global would report "use of undefined type". - self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved_stg_ty) - # @t.CVTable object classes initialize storage field 0 (vtable pointer) to the vtable - # address; otherwise the entry virtual call jumps to 0x0. @t.Object stays zeroinitializer. - stg_init_text: str = "zeroinitializer" - if resolved_stg_entry is not None and resolved_stg_entry.Name is not None and resolved_stg_entry.HasVTable != 0: - vt_name_buf: str = pool.alloc(128) - if vt_name_buf is not None: - viperlib.snprintf(vt_name_buf, 128, "%s.vtable", resolved_stg_entry.Name) - # The vtable symbol must use the defining module's sha1: mangling with the current - # module would reference a nonexistent symbol (llc "use of undefined value"). - vt_sha1: str = None - if resolved_stg_entry.ModuleSha1 is not None: vt_sha1 = resolved_stg_entry.ModuleSha1 - if vt_sha1 is None: vt_sha1 = trans.ModuleSha1 - vt_mangled: str = self.Gen.FuncGenBase._mangle_name_with_sha1(vt_sha1, vt_name_buf) - if vt_mangled is not None: - # A cross-module vtable is referenced only by this initializer text, so emit an - # external i8* declaration here (same-module vtables are already defined). - vt_cross: int = 0 - if resolved_stg_entry.ModuleSha1 is not None and trans.ModuleSha1 is not None: - if string.strncmp(resolved_stg_entry.ModuleSha1, trans.ModuleSha1, 16) != 0: vt_cross = 1 - if vt_cross != 0 and self.Gen.ExprGenAttr.find_global_in_module(mod, vt_mangled) is None: - vt_decl_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - vt_gv: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, vt_mangled, vt_decl_ty) - if vt_gv is not None: mlirlite.module_add_global(mod, vt_gv) - # Full struct initializer (LLVM rejects partial): field 0 = __vtable__ (i8* @vtable), - # remaining user fields zeroed by type. - init_full_len: sizet = 16384 - init_full: str = pool.alloc(init_full_len) - if init_full is not None: - written_v: int = viperlib.snprintf(init_full, init_full_len, "{ ") - pos_v: sizet = sizet(written_v) - if self.Gen.ClassGenVTable._vtable_name_needs_quote(vt_mangled) != 0: written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, "i8* @\"%s\"", vt_mangled) - else: written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, "i8* @%s", vt_mangled) - pos_v += sizet(written_v) - # Fields 1..N prefer folded default values (e.g. `_mbuddy: MemBuddy`), falling - # back to per-field zeros when all defaults are zero; both list every field. - rest_v: str = self._build_stg_struct_body_text(pool, resolved_stg_entry, 1) - if rest_v is not None: - written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, ", %s", rest_v) - pos_v += sizet(written_v) - else: - fc_v: int = resolved_stg_entry.FieldCount - for fi_v in range(1, int(fc_v)): - fe_v: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(resolved_stg_entry, fi_v) - if fe_v is None or fe_v.Ty is None: continue - ty_buf_v: str = pool.alloc_tagged(4096, memhub.DETSITE_TRANSLATORGEN_832) - if ty_buf_v is None: continue - mlirlite.TypePrint(ty_buf_v, 4096, fe_v.Ty, pool) - zero_v: str = self._stg_field_zero_text(pool, fe_v.Ty, ty_buf_v) - if zero_v is None: continue - written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, ", %s", zero_v) - pos_v += sizet(written_v) - viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, " }") - stg_init_text = init_full - elif resolved_stg_entry is not None: - # Object globals without a vtable (@t.Object / plain classes): build the initializer - # from field defaults, since the module-level X: T = T() constructor call emits nothing. - stg_default_text: str = self._build_stg_struct_init_text(pool, resolved_stg_entry, 0) - if stg_default_text is not None: stg_init_text = stg_default_text - stg_name_len: sizet = sizet(string.strlen(mangled_name)) + 8 - stg_name_buf: str = pool.alloc(stg_name_len) - if stg_name_buf is not None: - viperlib.snprintf(stg_name_buf, stg_name_len, "%s.stg", mangled_name) - stg_gv: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, stg_name_buf, resolved_stg_ty) - if stg_gv is not None: - mlirlite.module_add_global(mod, stg_gv) - stg_gv.Linkage = None - stg_gv.Initializer = stg_init_text - # B5: the storage (BSS object instance) also gets the aligned alignment - if resolved_stg_entry is not None and resolved_stg_entry.Align > 0: stg_gv.Align = resolved_stg_entry.Align - # @g initializer = &@storage (the object reference points to the BSS instance) - stg_init_len: sizet = sizet(string.strlen(mangled_name)) + 8 - stg_init_buf: str = pool.alloc(stg_init_len) - if stg_init_buf is not None: - if self.Gen.ClassGenVTable._vtable_name_needs_quote(stg_name_buf) != 0: viperlib.snprintf(stg_init_buf, stg_init_len, "@\"%s\"", stg_name_buf) - else: viperlib.snprintf(stg_init_buf, stg_init_len, "@%s", stg_name_buf) - gv.Initializer = stg_init_buf - # Register the storage reference (key: var_name.__stg) so AssignGenMixin's global - # assignment branch can detect object-class globals and copy objects into storage. - stg_ref: mlirlite.Value* = self.Gen.ExprGenAttr.make_global_ref(pool, stg_name_buf, resolved_stg_ty) - if stg_ref is not None: - stg_key_len: sizet = sizet(string.strlen(var_name)) + 8 - stg_key: str = pool.alloc(stg_key_len) - if stg_key is not None: - viperlib.snprintf(stg_key, stg_key_len, "%s.__stg", var_name) - self.Gen.VarGen.define_module_var(trans.SymTab, stg_key, stg_ref) - - # Create the Value reference (@mangled_name, type var_ty*) - var_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, var_ty) - ref_name: str = pool.alloc(96) - if ref_name is not None: - # The mangled name has a SHA1 prefix (leading digit), so the reference must be quoted. - if self.Gen.ClassGenVTable._vtable_name_needs_quote(mangled_name) != 0: viperlib.snprintf(ref_name, 96, "@\"%s\"", mangled_name) - else: viperlib.snprintf(ref_name, 96, "@%s", mangled_name) - gv_ref: mlirlite.Value* = mlirlite.SSAValue(pool, var_ptr_ty, ref_name) - - # Register in the module scope - if self.Gen.VarGen.define_module_var(trans.SymTab, var_name, gv_ref) == 0: - # t.CVolatile storage marker (union annotation): mark the global volatile so all - # loads/stores carry LLVM volatile and busy-wait loops are not optimized away. - if annot_node is not None: - # const object global: a non-pointer annotation with t.CConst registers IsConst, - # making later assignment/aug-assign/element-assign to the name fail-fast. - if self.Gen.TypeGenErr.is_const_object_marker(annot_node, var_ty) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, var_name) - if self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, var_name) - # rawptr[t.CVolatile | T] (volatile pointee): loads/stores through p[i]/p.field/ - # c.Deref(p) carry volatile; distinct from the variable's own volatile qualifier. - if self.Gen.TypeGenErr.has_pointee_volatile_marker(annot_node) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, var_name) - # Set AnnotClassName so attribute access can fall back to the class name when a union - # type like Logger* is simplified to Ptr(i8). - if k == ast.ASTKind.AnnAssign: - aa_ml: ast.AnnAssign* = ast.AnnAssign*(node) - if aa_ml is not None and aa_ml.annotation is not None: - cls_nm_ml: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(aa_ml.annotation, trans._imported_modules, pool, trans._from_imports) - if cls_nm_ml is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, var_name, cls_nm_ml) - # Module-level str*/bytes* globals: mark IsPtrElement so var[i] steps by 8-byte - # pointers; otherwise it degrades to a 1-byte GEP + load i8 that truncates pointers. - if aa_ml.annotation.kind() == ast.ASTKind.BinOp: - bop_ml: ast.BinOp* = ast.BinOp*(aa_ml.annotation) - if bop_ml.op == ast.OpKind.BitOr: - if self.Gen.AssignGen._annotation_has_cptr_marker(aa_ml.annotation) != 0: - if self.Gen.AssignGen._annotation_contains_name(aa_ml.annotation, "str") != 0 or self.Gen.AssignGen._annotation_contains_name(aa_ml.annotation, "bytes") != 0: - self.Gen.VarGen.set_var_ptr_element(trans.SymTab, var_name) - return 1 - return 0 - - # create_wrapper_main - create a wrapper main() -> i32 when the user defines none; sets the - # current func/builder, prescans allocas, translates children and returns 0. - def create_wrapper_main(self, trans: HT.Translator*, tree: ast.AST*, i32_ty: mlirlite.LLVMType*) -> int: - """Create a wrapper main function and translate all top-level statements.""" - pool: memhub.MemBuddy* = trans.Pool - mod: mlirlite.LLVMModule* = trans.Module - - main_func: mlirlite.Function* = mlirlite.create_function(pool, mod, "main", i32_ty) - if main_func is None: - VLogger.error("CreateFunction main returned NULL", "TR") - return 0 - - entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, main_func, "entry") - if entry_blk is None: - VLogger.error("CreateBlock returned NULL", "TR") - return 0 - - builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, main_func) - if builder is None: - VLogger.error("NewBuilder returned NULL", "TR") - return 0 - mlirlite.position_at_end(builder, entry_blk) - - # Set the current translation context (Handles access it via self.Trans._cur_builder) - trans._cur_func = main_func - trans._cur_builder = builder - # P2 willFree 失效表:进入 main 体前复位(避免上一翻译单元残留的栈槽失效态泄漏进来) - self.Gen.VarGen.freed_clear() - - # Pre-process module-level variables (create LLVM globals) before pre_scan_allocas, or - # PreScan would create local allocas that other functions reference by SSA number. - ch: list[ast.AST*]* = tree.children - if ch is not None: - cn_count: sizet = len(ch) - for ci in range(int(cn_count)): - child: ast.AST* = ch.get(ci) - if child is not None: - kd: int = child.kind() - if kd == ast.ASTKind.AnnAssign or kd == ast.ASTKind.Assign: self.handle_module_level_var(trans, child) - - # Prescan top-level statements to pre-create allocas for in-function AnnAssign; module-level - # variables are already registered so PreScan's lookup_current skips them. - if ch is not None: - for ci in range(int(cn_count)): - child: ast.AST* = ch.get(ci) - if child is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, child) - - # Translate children - added_total: int = self.translate_children(trans, tree) - - # Return 0 - zero_val: mlirlite.Value* = mlirlite.const_int32(pool, 0) - mlirlite.build_ret(builder, zero_val) - - return added_total - - # global/nonlocal name management (migrated from HandlesTranslator.vp module-level functions) - def add_global_name(self, trans: HT.Translator*, name: str): - """Add a global variable name.""" - if trans is None or name is None: return - if trans._global_names is None: return - # Check whether it already exists - if self.is_global_name(trans, name) != 0: return - if trans._global_name_count >= HT.MAX_GLOBAL_NAMES: - pool_err: memhub.MemBuddy* = trans.Pool - err_buf: str = None - if pool_err is not None: err_buf = pool_err.alloc(256) - if err_buf is not None: - viperlib.snprintf(err_buf, 256, "the number of global variables exceeds the limit MAX_GLOBAL_NAMES=%d; reduce global declarations or increase MAX_GLOBAL_NAMES", HT.MAX_GLOBAL_NAMES) - VLogger.error(err_buf, "ScopeDecl") - else: VLogger.error("the number of global variables exceeds the limit; reduce global declarations or increase MAX_GLOBAL_NAMES", "ScopeDecl") - return - entry_addr: uint64 = uint64(trans._global_names) + trans._global_name_count * 8 - slot_ptr: str* = (str*)(entry_addr) - slot_ptr[0] = name - trans._global_name_count += 1 - - def is_global_name(self, trans: HT.Translator*, name: str) -> int: - """Return 1 when name is in the current function's global set.""" - if trans is None or name is None or trans._global_names is None: return 0 - for i in range(int(trans._global_name_count)): - entry_addr: uint64 = uint64(trans._global_names) + i * 8 - slot_ptr: str* = (str*)(entry_addr) - entry: str = slot_ptr[0] - if entry is not None: - if string.strcmp(entry, name) == 0: return 1 - return 0 - - def add_nonlocal_name(self, trans: HT.Translator*, name: str, var_ty: mlirlite.LLVMType* = None): - """Add a nonlocal variable name together with its LLVM type.""" - if trans is None or name is None: return - if trans._nonlocal_names is None: return - if self.is_nonlocal_name(trans, name) != 0: - # Already present: refresh the type (possibly unknown at prescan time). _nonlocal_types - # stores one LLVMType* per 8-byte slot, accessed via i8** to copy just the pointer. - if var_ty is not None and trans._nonlocal_types is not None: - idx: int = self.get_nonlocal_index(trans, name) - if idx >= 0: - ty_addr: uint64 = uint64(trans._nonlocal_types) + idx * 8 - ty_slot: str* = (str*)(ty_addr) - ty_slot[0] = var_ty - return - if trans._nonlocal_name_count >= HT.MAX_GLOBAL_NAMES: - pool_err_nl: memhub.MemBuddy* = trans.Pool - err_buf_nl: str = None - if pool_err_nl is not None: err_buf_nl = pool_err_nl.alloc(256) - if err_buf_nl is not None: - viperlib.snprintf(err_buf_nl, 256, "the number of nonlocal variables exceeds the limit MAX_GLOBAL_NAMES=%d; reduce nonlocal declarations or increase MAX_GLOBAL_NAMES", HT.MAX_GLOBAL_NAMES) - VLogger.error(err_buf_nl, "ScopeDecl") - else: VLogger.error("the number of nonlocal variables exceeds the limit; reduce nonlocal declarations or increase MAX_GLOBAL_NAMES", "ScopeDecl") - return - entry_addr: uint64 = uint64(trans._nonlocal_names) + trans._nonlocal_name_count * 8 - slot_ptr: str* = (str*)(entry_addr) - slot_ptr[0] = name - # Store type info (str* = i8**, same pattern as _nonlocal_names) - if trans._nonlocal_types is not None: - ty_addr2: uint64 = uint64(trans._nonlocal_types) + trans._nonlocal_name_count * 8 - ty_slot2: str* = (str*)(ty_addr2) - # A None var_ty errors out (fail-fast) instead of defaulting to i32 - stored_ty: mlirlite.LLVMType* = var_ty - if stored_ty is None: - self.Gen.TypeGenErr.fatal_error(None, f"nonlocal variable '{name}' has no type, cannot register", "TypeError") - return - ty_slot2[0] = stored_ty - trans._nonlocal_name_count += 1 - trans._closure_env_count += 1 - - def get_nonlocal_type(self, trans: HT.Translator*, name: str) -> mlirlite.LLVMType*: - """Return the LLVM type of a nonlocal variable; None when not found.""" - if trans is None or name is None or trans._nonlocal_types is None: return None - idx: int = self.get_nonlocal_index(trans, name) - if idx < 0: return None - ty_addr: uint64 = uint64(trans._nonlocal_types) + idx * 8 - ty_slot: str* = (str*)(ty_addr) - return (mlirlite.LLVMType*)(ty_slot[0]) - - def is_nonlocal_name(self, trans: HT.Translator*, name: str) -> int: - """Return 1 when name is in the current function's nonlocal set.""" - if trans is None or name is None or trans._nonlocal_names is None: return 0 - for i in range(int(trans._nonlocal_name_count)): - entry_addr: uint64 = uint64(trans._nonlocal_names) + i * 8 - slot_ptr: str* = (str*)(entry_addr) - entry: str = slot_ptr[0] - if entry is not None: - if string.strcmp(entry, name) == 0: return 1 - return 0 - - def get_nonlocal_index(self, trans: HT.Translator*, name: str) -> int: - """Return the 0-based env index of a nonlocal variable; -1 when not found.""" - if trans is None or name is None or trans._nonlocal_names is None: return -1 - for i in range(int(trans._nonlocal_name_count)): - entry_addr: uint64 = uint64(trans._nonlocal_names) + i * 8 - slot_ptr: str* = (str*)(entry_addr) - entry: str = slot_ptr[0] - if entry is not None: - if string.strcmp(entry, name) == 0: return i - return -1 - - def clear_scope_names(self, trans: HT.Translator*): - """Clear the current function's global/nonlocal name sets on entering a new function.""" - if trans is None: return - trans._global_name_count = 0 - trans._nonlocal_name_count = 0 - trans._closure_env_count = 0 + init_sym_node: ast.AST* = None # Operand of `= &global` (init_kind==5) + init_str_val: str = None + init_list_node: ast.List* = None + # Explicit `= None` init keeps lazy-initialization pointer semantics: the global must stay + # NULL, so no BSS storage is created (storage would break the `!= None` guard). + is_none_init: int = 0 + annot_node: ast.AST* = None # Annotation node (used to detect t.CExport markers) + + if k == ast.ASTKind.AnnAssign: + aa: ast.AnnAssign* = ast.AnnAssign*(node) + if aa is None or aa.target is None: return 0 + if aa.target.kind() != ast.ASTKind.Name: return 0 + nm: ast.Name* = ast.Name*(aa.target) + var_name = nm.id + annot_node = aa.annotation + # f-string escape: a module-level initial value is global, but the f-string result + # points into the current function's stack buffer -> fail-fast (use escape_fstring_copy). + GenApi.fail_if_fstring_escape(aa.value, "global variable initialization") + # t.CStatic vars (no cross-module use) are registered as (name, def module SHA1); + # the logic stays inside _RegisterStaticModuleVar to avoid disturbing type inference. + self._RegisterStaticModuleVar(trans, annot_node, var_name) + # Module-level t.Callable function pointers register (name -> return type) here so + # indirect calls resolve; done before any body translation, same as in-function AnnAssign. + if annot_node is not None: + ml_callable_ret: mlirlite.LLVMType* = self.Gen.TypeGenErr.try_extract_callable_ret(pool, annot_node, trans._imported_modules,trans._from_imports, trans) + if ml_callable_ret is not None: + # 0 = success/idempotent; non-zero already fatal-errored inside register_func_ptr_sig, + # so this branch is only defensive. + if self.Gen.CallGenLeafKit.register_func_ptr_sig(pool, var_name, ml_callable_ret, aa) != 0: return 0 + # Resolve the type + if aa.annotation is not None: + # Value-reference markers are parameter-only: a module variable `x: &T` fails fast. + self.Gen.TypeGenResolve.reject_ref_marker_position(aa.annotation, trans._imported_modules, "a module-level variable declaration") + resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, aa.annotation, trans._imported_modules, trans._from_imports, trans, 0) + if resolved is not None: + # Object-class module vars (@t.Object/@t.CVTable: object-as-pointer) wrap the + # resolved struct value in Ptr; pure t.CStruct keeps a value instance. + if resolved.kind() == mlirlite.LLVMType.Struct: + gv_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(resolved) + if gv_entry is not None and (gv_entry.HasNew != 0 or gv_entry.HasVTable != 0 or gv_entry.IsOOP != 0): + # Object-class global (object-as-pointer): remember the Struct value type + # so a BSS storage global can hold the instance; otherwise @g is NULL. + resolved_stg_ty = resolved + resolved_stg_entry = gv_entry + resolved = mlirlite.Ptr(pool, resolved) + elif resolved.kind() == mlirlite.LLVMType.Ptr: + # Object-class union annotation (T* -> T*): also create storage, else the + # virtual call loads NULL @g and faults; var_ty stays Ptr. + pointee_uv: mlirlite.LLVMType* = resolved.Pointee + if pointee_uv is not None and pointee_uv.kind() == mlirlite.LLVMType.Struct: + gv_entry_u: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(pointee_uv) + if gv_entry_u is not None and (gv_entry_u.HasNew != 0 or gv_entry_u.HasVTable != 0 or gv_entry_u.IsOOP != 0): + resolved_stg_ty = pointee_uv + resolved_stg_entry = gv_entry_u + var_ty = resolved + else: + # [S4 fail-fast] An annotated module var that fails to resolve errors out instead + # of silently using i32 (a wrong type corrupts storage size/field access/ABI). + self.Gen.TypeGenErr.fatal_error(aa.annotation, f"failed to resolve the type annotation of module-level variable '{var_name}'", "NameError") + return 0 + # Parse the initial value (integer, negative literal, list literal, string literal) + if aa.value is not None: + val_kind: int = aa.value.kind() + if val_kind == ast.ASTKind.Constant: + cn: ast.Constant* = ast.Constant*(aa.value) + if cn.const_kind == ast.CONST_NONE: is_none_init = 1 + elif cn.const_kind == ast.CONST_INT: + init_val = cn.int_val + init_kind = 1 + elif cn.const_kind == ast.CONST_STR: + init_str_val = cn.str_val + init_kind = 3 + elif cn.const_kind == ast.CONST_FLOAT: + # Float literal: init_kind must be set or the global degrades to + # zeroinitializer; the value is re-read from source at init_kind==4 below. + init_kind = 4 + elif val_kind == ast.ASTKind.UnaryOp: + # Negative/bitwise-invert literal: the AST is UnaryOp(USub, Constant); unhandled + # it would compile to zeroinitializer and lose the initial value. `&global` + # (UnaryOp(AddrOf, Name)) is a symbol-address initializer instead and must not be + # fed to the integer constant extractor. + uop_g: ast.UnaryOp* = ast.UnaryOp*(aa.value) + if uop_g is not None and uop_g.op == ast.OpKind.AddrOf: + init_sym_node = uop_g.operand + init_kind = 5 + else: + init_val = self.Gen.AssignGen.extract_cdefine_int_value(aa.value) + init_kind = 1 + elif val_kind == ast.ASTKind.List: + init_list_node = ast.List*(aa.value) + init_kind = 2 + elif k == ast.ASTKind.Assign: + asgn: ast.Assign* = ast.Assign*(node) + if asgn is None or asgn.targets is None: return 0 + targets: list[ast.AST*]* = asgn.targets + if len(targets) < 1: return 0 + t0: ast.AST* = targets.get(0) + if t0 is None or t0.kind() != ast.ASTKind.Name: return 0 + nm2: ast.Name* = ast.Name*(t0) + var_name = nm2.id + if asgn.value is not None: + val_kind2: int = asgn.value.kind() + if val_kind2 == ast.ASTKind.Constant: + cn2: ast.Constant* = ast.Constant*(asgn.value) + if cn2.const_kind == ast.CONST_NONE: is_none_init = 1 + elif cn2.const_kind == ast.CONST_INT: + init_val = cn2.int_val + init_kind = 1 + elif cn2.const_kind == ast.CONST_STR: + init_str_val = cn2.str_val + init_kind = 3 + elif cn2.const_kind == ast.CONST_FLOAT: + # Unannotated module-level float assignment: the literal itself implies the + # type, so promote to double (default var_ty is i32). + init_kind = 4 + var_ty = mlirlite.Double(pool) + elif val_kind2 == ast.ASTKind.UnaryOp: + # Negative literal (same as the AnnAssign branch above): UnaryOp(USub, Constant). + # `= &global` is a symbol-address initializer instead (see the AnnAssign branch). + uop_g2: ast.UnaryOp* = ast.UnaryOp*(asgn.value) + if uop_g2 is not None and uop_g2.op == ast.OpKind.AddrOf: + init_sym_node = uop_g2.operand + init_kind = 5 + else: + init_val = self.Gen.AssignGen.extract_cdefine_int_value(asgn.value) + init_kind = 1 + elif val_kind2 == ast.ASTKind.List: + init_list_node = ast.List*(asgn.value) + init_kind = 2 + elif val_kind2 == ast.ASTKind.Attribute: + # Module-level re-export alias (e.g. `_read32 = types._read32`): the RHS is an + # entity reference with no value semantics, so emit no LLVM global (same as __all__). + return 0 + + if var_name is None: return 0 + + # __all__ is Python module metadata with no runtime semantics, so skip global creation; + # otherwise the unannotated list-of-strings global would falsely fail initializer building. + if string.strcmp(var_name, "__all__") == 0: return 0 + + # Float literal init (init_kind==4): re-read the value from source by name instead of the + # AST float_val (which pool ops may overwrite); also validate that the annotation is float. + if init_kind == 4: + is_fty_g: int = 0 + match var_ty: + case mlirlite.LLVMType.Float(_): + is_fty_g = 1 + case _: + pass + if is_fty_g == 0: + self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with a float literal, but its annotation is not a float type", "TypeError") + return 0 + if self.Gen.AssignGen._recover_float_literal(var_name, &init_fval, 0) != 1: + self.Gen.TypeGenErr.fatal_error(node, f"failed to recover the initializer value of module-level float variable '{var_name}' from the source file", "InternalCompilerError") + return 0 + + # Check whether it is already registered + existing: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, var_name) + if existing is not None: return 0 + + # Create the global @var_name, first ensuring opaque declarations for any cross-module + # struct type in var_ty, or llc reports "use of undefined type named 'sha1.ClassName'". + mlirlite.module_ensure_opaque_for_type(mod, pool, var_ty) + # Module-level globals are SHA1-prefixed (like function mangling) to avoid duplicate + # symbols; SymTab keys stay bare, and t.CExport globals keep bare names for asm references. + is_global_cexport: int = 0 + if annot_node is not None: is_global_cexport = self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CExport", 0) + # extern globals (annotation has t.State/t.CExtern) are declared only: `@bare = external + # global T` with external linkage, no initializer and no storage; export instead defines them. + is_global_extern: int = 0 + if annot_node is not None: + is_global_extern = self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CExtern", 0) + if is_global_extern == 0: is_global_extern = self.Gen.TypeGenErr.has_decorator_marker(annot_node, "State", 0) + # Global-side t.attr.* attributes (`T | t.attr.xxx` union): section->gv.Section, + # aligned->gv.Align, weak->gv.Linkage; only present for AnnAssign (annot_node not None). + gv_attr_section: str = None + gv_attr_align: int = 0 + gv_attr_weak: int = 0 + if annot_node is not None: + gv_attr_section = self._extract_global_attr_section(annot_node, var_name) + gv_attr_align = self._extract_global_attr_aligned(annot_node, var_name) + gv_attr_weak = self._extract_global_attr_weak(annot_node) + mangled_name: str = var_name + if is_global_cexport == 0 and is_global_extern == 0: mangled_name = self.Gen.FuncGenBase._mangle_name(trans, var_name) + gv: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, mangled_name, var_ty) + if gv is None: return 0 + mlirlite.module_add_global(mod, gv) + + # Section name: write the field directly (_print_global adds the quotes for `, section "xxx"`). + if gv_attr_section is not None: mlirlite.global_set_section(gv, gv_attr_section) + + # B5: apply alignment to global instances of @c.Attribute(t.attr.aligned(N)) classes. + # An explicit variable-level `T | t.attr.aligned(N)` wins over the class-level struct Align. + al_ty: mlirlite.LLVMType* = var_ty + match var_ty: + case mlirlite.LLVMType.Ptr(pointee_al): + al_ty = pointee_al + case _: + pass + al_entry: GenTypes.StructEntry* = None + if al_ty is not None and al_ty.kind() == mlirlite.LLVMType.Struct: al_entry = self.Gen.StructGen.find_struct_by_type(al_ty) + if gv_attr_align > 0: gv.Align = gv_attr_align + elif al_entry is not None and al_entry.Align > 0: gv.Align = al_entry.Align + + # Set the initializer (clearing external linkage, since LLVM 22+ forbids external globals + # with initializers); extern globals are the exception and stay external. + if is_global_extern != 0: gv.Linkage = "external" + else: + gv.Linkage = None + # t.attr.weak -> weak linkage (must be set after Linkage is cleared). + if gv_attr_weak != 0: mlirlite.global_set_linkage(gv, "weak") + if init_kind == 1: + # Integer initializer + init_buf: str = pool.alloc(48) + if init_buf is not None: + viperlib.snprintf(init_buf, 48, "%lld", init_val) + gv.Initializer = init_buf + elif init_kind == 2: + # List literal -> array initializer [N x elem_ty] [elem_ty val0, ...] + arr_init: str = self._build_array_initializer_text(pool, var_ty, init_list_node) + if arr_init is not None: gv.Initializer = arr_init + else: + # Initializer build failed: error out (fail-fast) instead of falling back to zeroinitializer + self.Gen.TypeGenErr.fatal_error(node, f"failed to build the initializer of module-level variable '{var_name}'", "TypeError") + elif init_kind == 3: + # String literal initializer: for array types (t.CArray[char, N]) use a byte-array + # initializer (content + zero padding); a string-pointer GEP initializer would mismatch. + is_arr_g: int = 0 + match var_ty: + case mlirlite.LLVMType.Array(_, _): + is_arr_g = 1 + case _: + pass + if is_arr_g != 0: + arr_str_init: str = self._build_string_array_initializer_text(pool, var_ty, init_str_val) + if arr_str_init is not None: gv.Initializer = arr_str_init + else: self.Gen.TypeGenErr.fatal_error(node, f"failed to build the string initializer of module-level array variable '{var_name}'", "TypeError") + else: + # Pointer type (e.g. b64_tab: t.CArray[char, None] = "ABC..."): create an internal + # string constant global and reference it via a GEP initializer. + str_init: str = self._build_string_ptr_initializer(pool, mod, var_name, init_str_val) + if str_init is not None: gv.Initializer = str_init + else: self.Gen.TypeGenErr.fatal_error(node, f"failed to build the initializer of module-level variable '{var_name}'", "TypeError") + elif init_kind == 4: + # Float initializer: LLVM float constants must be written as bit-pattern text + # (0x%016llx / 0x%08x); decimal text has ulp errors, and omitting this zeroes the global. + fc_g: mlirlite.Value* = mlirlite.ConstFloat(pool, var_ty, init_fval) + if fc_g is not None and fc_g.Name is not None: gv.Initializer = fc_g.Name + else: self.Gen.TypeGenErr.fatal_error(node, f"failed to build the float initializer of module-level variable '{var_name}'", "TypeError") + elif init_kind == 5: + # Symbol-address initializer (`= &global`): the referenced global's mangled symbol is + # emitted as `@name` (LLVM-quoted when required); the MLIR layer renders it as + # llvm.mlir.addressof. Only a pointer-typed global can hold an address. + is_ptr_g: int = 0 + match var_ty: + case mlirlite.LLVMType.Ptr(_): + is_ptr_g = 1 + case _: + pass + if is_ptr_g == 0: + self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with an address-of expression, but its type is not a pointer (add a pointer annotation or drop the '&')", "TypeError") + return 0 + if init_sym_node is None or init_sym_node.kind() != ast.ASTKind.Name: + self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with an address-of expression whose operand is not a variable name", "TypeError") + return 0 + sym_nm: ast.Name* = ast.Name*(init_sym_node) + sym_val: mlirlite.Value* = None + if sym_nm.id is not None: sym_val = self.Gen.VarGen.lookup_module_var(trans.SymTab, sym_nm.id) + if sym_val is None or sym_val.Name is None: + self.Gen.TypeGenErr.fatal_error(node, f"module-level variable '{var_name}' is initialized with &{sym_nm.id}, but '{sym_nm.id}' is not a module-level variable declared before this line", "NameError") + return 0 + sym_buf: str = pool.alloc(256) + if sym_buf is None: return 0 + # The module-scope entry already holds the LLVM reference text of that global + # (`@mangled` or `@"mangled"`), so it is used verbatim; only a bare name (no '@') needs + # the reference prefix added here. + if sym_val.Name[0] == '@': viperlib.snprintf(sym_buf, 256, "%s", sym_val.Name) + elif self.Gen.ClassGenVTable._vtable_name_needs_quote(sym_val.Name) != 0: viperlib.snprintf(sym_buf, 256, "@\"%s\"", sym_val.Name) + else: viperlib.snprintf(sym_buf, 256, "@%s", sym_val.Name) + gv.Initializer = sym_buf + else: + # Use zeroinitializer (valid for pointer/integer/aggregate types). Object-class globals + # with storage get &@storage below; `= None` lazy globals still need it to stay complete. + if (resolved_stg_ty is None or is_none_init != 0) and is_global_extern == 0: gv.Initializer = "zeroinitializer" + + # Object-class globals: create a BSS storage global holding the instance (@g references it, + # so it is never NULL); `= None` lazy globals and extern globals skip storage instead. + if resolved_stg_ty is not None and is_none_init == 0 and is_global_extern == 0: + # Value-type storage needs a full struct definition, since cross-module classes are + # opaque by default and a zeroinitializer global would report "use of undefined type". + self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved_stg_ty) + # @t.CVTable object classes initialize storage field 0 (vtable pointer) to the vtable + # address; otherwise the entry virtual call jumps to 0x0. @t.Object stays zeroinitializer. + stg_init_text: str = "zeroinitializer" + if resolved_stg_entry is not None and resolved_stg_entry.Name is not None and resolved_stg_entry.HasVTable != 0: + vt_name_buf: str = pool.alloc(128) + if vt_name_buf is not None: + viperlib.snprintf(vt_name_buf, 128, "%s.vtable", resolved_stg_entry.Name) + # The vtable symbol must use the defining module's sha1: mangling with the current + # module would reference a nonexistent symbol (llc "use of undefined value"). + vt_sha1: str = None + if resolved_stg_entry.ModuleSha1 is not None: vt_sha1 = resolved_stg_entry.ModuleSha1 + if vt_sha1 is None: vt_sha1 = trans.ModuleSha1 + vt_mangled: str = self.Gen.FuncGenBase._mangle_name_with_sha1(vt_sha1, vt_name_buf) + if vt_mangled is not None: + # A cross-module vtable is referenced only by this initializer text, so emit an + # external i8* declaration here (same-module vtables are already defined). + vt_cross: int = 0 + if resolved_stg_entry.ModuleSha1 is not None and trans.ModuleSha1 is not None: + if string.strncmp(resolved_stg_entry.ModuleSha1, trans.ModuleSha1, 16) != 0: vt_cross = 1 + if vt_cross != 0 and self.Gen.ExprGenAttr.find_global_in_module(mod, vt_mangled) is None: + vt_decl_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + vt_gv: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, vt_mangled, vt_decl_ty) + if vt_gv is not None: mlirlite.module_add_global(mod, vt_gv) + # Full struct initializer (LLVM rejects partial): field 0 = __vtable__ (i8* @vtable), + # remaining user fields zeroed by type. + init_full_len: sizet = 16384 + init_full: str = pool.alloc(init_full_len) + if init_full is not None: + written_v: int = viperlib.snprintf(init_full, init_full_len, "{ ") + pos_v: sizet = sizet(written_v) + if self.Gen.ClassGenVTable._vtable_name_needs_quote(vt_mangled) != 0: written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, "i8* @\"%s\"", vt_mangled) + else: written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, "i8* @%s", vt_mangled) + pos_v += sizet(written_v) + # Fields 1..N prefer folded default values (e.g. `_mbuddy: MemBuddy`), falling + # back to per-field zeros when all defaults are zero; both list every field. + rest_v: str = self._build_stg_struct_body_text(pool, resolved_stg_entry, 1) + if rest_v is not None: + written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, ", %s", rest_v) + pos_v += sizet(written_v) + else: + fc_v: int = resolved_stg_entry.FieldCount + for fi_v in range(1, int(fc_v)): + fe_v: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(resolved_stg_entry, fi_v) + if fe_v is None or fe_v.Ty is None: continue + ty_buf_v: str = pool.alloc_tagged(4096, memhub.DETSITE_TRANSLATORGEN_832) + if ty_buf_v is None: continue + mlirlite.TypePrint(ty_buf_v, 4096, fe_v.Ty, pool) + zero_v: str = self._stg_field_zero_text(pool, fe_v.Ty, ty_buf_v) + if zero_v is None: continue + written_v = viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, ", %s", zero_v) + pos_v += sizet(written_v) + viperlib.snprintf(init_full + pos_v, init_full_len - pos_v, " }") + stg_init_text = init_full + elif resolved_stg_entry is not None: + # Object globals without a vtable (@t.Object / plain classes): build the initializer + # from field defaults, since the module-level X: T = T() constructor call emits nothing. + stg_default_text: str = self._build_stg_struct_init_text(pool, resolved_stg_entry, 0) + if stg_default_text is not None: stg_init_text = stg_default_text + stg_name_len: sizet = sizet(string.strlen(mangled_name)) + 8 + stg_name_buf: str = pool.alloc(stg_name_len) + if stg_name_buf is not None: + viperlib.snprintf(stg_name_buf, stg_name_len, "%s.stg", mangled_name) + stg_gv: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, stg_name_buf, resolved_stg_ty) + if stg_gv is not None: + mlirlite.module_add_global(mod, stg_gv) + stg_gv.Linkage = None + stg_gv.Initializer = stg_init_text + # B5: the storage (BSS object instance) also gets the aligned alignment + if resolved_stg_entry is not None and resolved_stg_entry.Align > 0: stg_gv.Align = resolved_stg_entry.Align + # @g initializer = &@storage (the object reference points to the BSS instance) + stg_init_len: sizet = sizet(string.strlen(mangled_name)) + 8 + stg_init_buf: str = pool.alloc(stg_init_len) + if stg_init_buf is not None: + if self.Gen.ClassGenVTable._vtable_name_needs_quote(stg_name_buf) != 0: viperlib.snprintf(stg_init_buf, stg_init_len, "@\"%s\"", stg_name_buf) + else: viperlib.snprintf(stg_init_buf, stg_init_len, "@%s", stg_name_buf) + gv.Initializer = stg_init_buf + # Register the storage reference (key: var_name.__stg) so AssignGenMixin's global + # assignment branch can detect object-class globals and copy objects into storage. + stg_ref: mlirlite.Value* = self.Gen.ExprGenAttr.make_global_ref(pool, stg_name_buf, resolved_stg_ty) + if stg_ref is not None: + stg_key_len: sizet = sizet(string.strlen(var_name)) + 8 + stg_key: str = pool.alloc(stg_key_len) + if stg_key is not None: + viperlib.snprintf(stg_key, stg_key_len, "%s.__stg", var_name) + self.Gen.VarGen.define_module_var(trans.SymTab, stg_key, stg_ref) + + # Create the Value reference (@mangled_name, type var_ty*) + var_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, var_ty) + ref_name: str = pool.alloc(96) + if ref_name is not None: + # The mangled name has a SHA1 prefix (leading digit), so the reference must be quoted. + if self.Gen.ClassGenVTable._vtable_name_needs_quote(mangled_name) != 0: viperlib.snprintf(ref_name, 96, "@\"%s\"", mangled_name) + else: viperlib.snprintf(ref_name, 96, "@%s", mangled_name) + gv_ref: mlirlite.Value* = mlirlite.SSAValue(pool, var_ptr_ty, ref_name) + + # Register in the module scope + if self.Gen.VarGen.define_module_var(trans.SymTab, var_name, gv_ref) == 0: + # t.CVolatile storage marker (union annotation): mark the global volatile so all + # loads/stores carry LLVM volatile and busy-wait loops are not optimized away. + if annot_node is not None: + # const object global: a non-pointer annotation with t.CConst registers IsConst, + # making later assignment/aug-assign/element-assign to the name fail-fast. + if self.Gen.TypeGenErr.is_const_object_marker(annot_node, var_ty) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, var_name) + if self.Gen.TypeGenErr.has_decorator_marker(annot_node, "CVolatile", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, var_name) + # rawptr[t.CVolatile | T] (volatile pointee): loads/stores through p[i]/p.field/ + # c.Deref(p) carry volatile; distinct from the variable's own volatile qualifier. + if self.Gen.TypeGenErr.has_pointee_volatile_marker(annot_node) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, var_name) + # Set AnnotClassName so attribute access can fall back to the class name when a union + # type like Logger* is simplified to Ptr(i8). + if k == ast.ASTKind.AnnAssign: + aa_ml: ast.AnnAssign* = ast.AnnAssign*(node) + if aa_ml is not None and aa_ml.annotation is not None: + cls_nm_ml: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(aa_ml.annotation, trans._imported_modules, pool, trans._from_imports) + if cls_nm_ml is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, var_name, cls_nm_ml) + # Module-level str*/bytes* globals: mark IsPtrElement so var[i] steps by 8-byte + # pointers; otherwise it degrades to a 1-byte GEP + load i8 that truncates pointers. + if aa_ml.annotation.kind() == ast.ASTKind.BinOp: + bop_ml: ast.BinOp* = ast.BinOp*(aa_ml.annotation) + if bop_ml.op == ast.OpKind.BitOr: + if self.Gen.AssignGen._annotation_has_cptr_marker(aa_ml.annotation) != 0: + if self.Gen.AssignGen._annotation_contains_name(aa_ml.annotation, "str") != 0 or self.Gen.AssignGen._annotation_contains_name(aa_ml.annotation, "bytes") != 0: + self.Gen.VarGen.set_var_ptr_element(trans.SymTab, var_name) + return 1 + return 0 + + # create_wrapper_main - create a wrapper main() -> i32 when the user defines none; sets the + # current func/builder, prescans allocas, translates children and returns 0. + def create_wrapper_main(self, trans: HT.Translator*, tree: ast.AST*, i32_ty: mlirlite.LLVMType*) -> int: + """Create a wrapper main function and translate all top-level statements.""" + pool: memhub.MemBuddy* = trans.Pool + mod: mlirlite.LLVMModule* = trans.Module + + main_func: mlirlite.Function* = mlirlite.create_function(pool, mod, "main", i32_ty) + if main_func is None: + VLogger.error("CreateFunction main returned NULL", "TR") + return 0 + + entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, main_func, "entry") + if entry_blk is None: + VLogger.error("CreateBlock returned NULL", "TR") + return 0 + + builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, main_func) + if builder is None: + VLogger.error("NewBuilder returned NULL", "TR") + return 0 + mlirlite.position_at_end(builder, entry_blk) + + # Set the current translation context (Handles access it via self.Trans._cur_builder) + trans._cur_func = main_func + trans._cur_builder = builder + # P2 willFree 失效表:进入 main 体前复位(避免上一翻译单元残留的栈槽失效态泄漏进来) + self.Gen.VarGen.freed_clear() + + # Pre-process module-level variables (create LLVM globals) before pre_scan_allocas, or + # PreScan would create local allocas that other functions reference by SSA number. + ch: list[ast.AST*]* = tree.children + if ch is not None: + cn_count: sizet = len(ch) + for ci in range(int(cn_count)): + child: ast.AST* = ch.get(ci) + if child is not None: + kd: int = child.kind() + if kd == ast.ASTKind.AnnAssign or kd == ast.ASTKind.Assign: self.handle_module_level_var(trans, child) + + # Prescan top-level statements to pre-create allocas for in-function AnnAssign; module-level + # variables are already registered so PreScan's lookup_current skips them. + if ch is not None: + for ci in range(int(cn_count)): + child: ast.AST* = ch.get(ci) + if child is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, child) + + # Translate children + added_total: int = self.translate_children(trans, tree) + + # Return 0 + zero_val: mlirlite.Value* = mlirlite.const_int32(pool, 0) + mlirlite.build_ret(builder, zero_val) + + return added_total + + # global/nonlocal name management (migrated from HandlesTranslator.vp module-level functions) + def add_global_name(self, trans: HT.Translator*, name: str): + """Add a global variable name.""" + if trans is None or name is None: return + if trans._global_names is None: return + # Check whether it already exists + if self.is_global_name(trans, name) != 0: return + if trans._global_name_count >= HT.MAX_GLOBAL_NAMES: + pool_err: memhub.MemBuddy* = trans.Pool + err_buf: str = None + if pool_err is not None: err_buf = pool_err.alloc(256) + if err_buf is not None: + viperlib.snprintf(err_buf, 256, "the number of global variables exceeds the limit MAX_GLOBAL_NAMES=%d; reduce global declarations or increase MAX_GLOBAL_NAMES", HT.MAX_GLOBAL_NAMES) + VLogger.error(err_buf, "ScopeDecl") + else: VLogger.error("the number of global variables exceeds the limit; reduce global declarations or increase MAX_GLOBAL_NAMES", "ScopeDecl") + return + entry_addr: uint64 = uint64(trans._global_names) + trans._global_name_count * 8 + slot_ptr: str* = (str*)(entry_addr) + slot_ptr[0] = name + trans._global_name_count += 1 + + def is_global_name(self, trans: HT.Translator*, name: str) -> int: + """Return 1 when name is in the current function's global set.""" + if trans is None or name is None or trans._global_names is None: return 0 + for i in range(int(trans._global_name_count)): + entry_addr: uint64 = uint64(trans._global_names) + i * 8 + slot_ptr: str* = (str*)(entry_addr) + entry: str = slot_ptr[0] + if entry is not None: + if string.strcmp(entry, name) == 0: return 1 + return 0 + + def add_nonlocal_name(self, trans: HT.Translator*, name: str, var_ty: mlirlite.LLVMType* = None): + """Add a nonlocal variable name together with its LLVM type.""" + if trans is None or name is None: return + if trans._nonlocal_names is None: return + if self.is_nonlocal_name(trans, name) != 0: + # Already present: refresh the type (possibly unknown at prescan time). _nonlocal_types + # stores one LLVMType* per 8-byte slot, accessed via i8** to copy just the pointer. + if var_ty is not None and trans._nonlocal_types is not None: + idx: int = self.get_nonlocal_index(trans, name) + if idx >= 0: + ty_addr: uint64 = uint64(trans._nonlocal_types) + idx * 8 + ty_slot: str* = (str*)(ty_addr) + ty_slot[0] = var_ty + return + if trans._nonlocal_name_count >= HT.MAX_GLOBAL_NAMES: + pool_err_nl: memhub.MemBuddy* = trans.Pool + err_buf_nl: str = None + if pool_err_nl is not None: err_buf_nl = pool_err_nl.alloc(256) + if err_buf_nl is not None: + viperlib.snprintf(err_buf_nl, 256, "the number of nonlocal variables exceeds the limit MAX_GLOBAL_NAMES=%d; reduce nonlocal declarations or increase MAX_GLOBAL_NAMES", HT.MAX_GLOBAL_NAMES) + VLogger.error(err_buf_nl, "ScopeDecl") + else: VLogger.error("the number of nonlocal variables exceeds the limit; reduce nonlocal declarations or increase MAX_GLOBAL_NAMES", "ScopeDecl") + return + entry_addr: uint64 = uint64(trans._nonlocal_names) + trans._nonlocal_name_count * 8 + slot_ptr: str* = (str*)(entry_addr) + slot_ptr[0] = name + # Store type info (str* = i8**, same pattern as _nonlocal_names) + if trans._nonlocal_types is not None: + ty_addr2: uint64 = uint64(trans._nonlocal_types) + trans._nonlocal_name_count * 8 + ty_slot2: str* = (str*)(ty_addr2) + # A None var_ty errors out (fail-fast) instead of defaulting to i32 + stored_ty: mlirlite.LLVMType* = var_ty + if stored_ty is None: + self.Gen.TypeGenErr.fatal_error(None, f"nonlocal variable '{name}' has no type, cannot register", "TypeError") + return + ty_slot2[0] = stored_ty + trans._nonlocal_name_count += 1 + trans._closure_env_count += 1 + + def get_nonlocal_type(self, trans: HT.Translator*, name: str) -> mlirlite.LLVMType*: + """Return the LLVM type of a nonlocal variable; None when not found.""" + if trans is None or name is None or trans._nonlocal_types is None: return None + idx: int = self.get_nonlocal_index(trans, name) + if idx < 0: return None + ty_addr: uint64 = uint64(trans._nonlocal_types) + idx * 8 + ty_slot: str* = (str*)(ty_addr) + return (mlirlite.LLVMType*)(ty_slot[0]) + + def is_nonlocal_name(self, trans: HT.Translator*, name: str) -> int: + """Return 1 when name is in the current function's nonlocal set.""" + if trans is None or name is None or trans._nonlocal_names is None: return 0 + for i in range(int(trans._nonlocal_name_count)): + entry_addr: uint64 = uint64(trans._nonlocal_names) + i * 8 + slot_ptr: str* = (str*)(entry_addr) + entry: str = slot_ptr[0] + if entry is not None: + if string.strcmp(entry, name) == 0: return 1 + return 0 + + def get_nonlocal_index(self, trans: HT.Translator*, name: str) -> int: + """Return the 0-based env index of a nonlocal variable; -1 when not found.""" + if trans is None or name is None or trans._nonlocal_names is None: return -1 + for i in range(int(trans._nonlocal_name_count)): + entry_addr: uint64 = uint64(trans._nonlocal_names) + i * 8 + slot_ptr: str* = (str*)(entry_addr) + entry: str = slot_ptr[0] + if entry is not None: + if string.strcmp(entry, name) == 0: return i + return -1 + + def clear_scope_names(self, trans: HT.Translator*): + """Clear the current function's global/nonlocal name sets on entering a new function.""" + if trans is None: return + trans._global_name_count = 0 + trans._nonlocal_name_count = 0 + trans._closure_env_count = 0 diff --git a/App/lib/core/Gen/VarGenMixin.vp b/App/lib/core/Gen/VarGenMixin.vp index 796c1e1..7858291 100644 --- a/App/lib/core/Gen/VarGenMixin.vp +++ b/App/lib/core/Gen/VarGenMixin.vp @@ -1,30 +1,30 @@ -import lib.core.Gen.GenTypes as GenTypes -import lib.core.Gen.Generator as Generator -import lib.core.Gen.Type.TypeSpec as TSpec -from MatchGenMixin import SCOPE_MATCH -import lib.core.Gen.Translator as HT -import t, c -from stdint import * -import ast -import mlirlite -import memhub -import string -import stdlib -import viperlib - - -# VarGenMixin - variable/scope symbol table Mixin -# ============================================================ - - -# Constants (scope kinds; capacity constants are referenced via HT.). -# Scope kinds: module / function / block. -define SCOPE_MODULE = 0 -define SCOPE_FUNCTION = 1 -define SCOPE_BLOCK = 2 -# Max variables per scope: the single source of truth is Translator.vp; this file always uses HT.MAX_VARS - - +import lib.core.Gen.GenTypes as GenTypes +import lib.core.Gen.Generator as Generator +import lib.core.Gen.Type.TypeSpec as TSpec +from MatchGenMixin import SCOPE_MATCH +import lib.core.Gen.Translator as HT +import t, c +from stdint import * +import ast +import mlirlite +import memhub +import string +import stdlib +import viperlib + + +# VarGenMixin - variable/scope symbol table Mixin +# ============================================================ + + +# Constants (scope kinds; capacity constants are referenced via HT.). +# Scope kinds: module / function / block. +define SCOPE_MODULE = 0 +define SCOPE_FUNCTION = 1 +define SCOPE_BLOCK = 2 +# Max variables per scope: the single source of truth is Translator.vp; this file always uses HT.MAX_VARS + + # VarGenMixin - variable/scope symbol table Mixin # ============================================================ # ============================================================ @@ -44,7 +44,7 @@ define SCOPE_BLOCK = 2 # 保存的旧值还原)——否则内层函数会在 0 号槽覆写外层登记项,且内层出口会把外层的对象一起 free 掉。 # · 表的存储必须**跨文件存活**:trans.Pool 是每文件池,Phase2 每翻完一个文件就 reset_to(mark2) # (见 Phase2/__init__.vp)。若表落在这个池里,第二个文件起表体已是被回收的内存,后续 -# 写入就是往活跃数据上砸(现场:TransPyV.exe 编译任何工程 access-violation on 0x0, +# 写入就是往活跃数据上砸(现场:viperc.exe 编译任何工程 access-violation on 0x0, # 崩点每次都不同)。故与 _g_freed_slots 同口径,用 stdlib.malloc。 # 登记表容量(P5:128 → 256)。超限时 **fail-fast**(不再静默退化为"不登记 = 不释放"的泄漏)。 define _SLIFE_MAX = 256 @@ -64,670 +64,670 @@ _g_slife_save: sizet* = None _g_slife_depth: int = 0 -novirtual class VarGenMixin: - """Variable/scope mixin""" - # init_vars - allocate and zero the VarEntry array - # ============================================================ - Gen: Generator.Generator* - def init_vars(self, pool: memhub.MemBuddy*) -> GenTypes.VarEntry*: - """Allocate and zero the variable table array""" - size: sizet = HT.MAX_VARS * GenTypes.VarEntry.__sizeof__() - vars_ptr: GenTypes.VarEntry* = pool.alloc(size) - if vars_ptr is not None: string.memset(vars_ptr, 0, size) - return vars_ptr - - # find_var - look up a name in the variable table - # ============================================================ - def find_var(self, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str) -> mlirlite.Value*: - """Look up by name in the variable table""" - if name is None or vars_ptr is None: return None - entry_size: sizet = GenTypes.VarEntry.__sizeof__() - for i in range(int(var_count)): - entry_addr: uint64 = uint64(vars_ptr) + i * entry_size - entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr) - if entry.Name is not None and entry.Used != 0: - # Fast path: strcmp equality implies equal first chars, so skip strcmp when they differ - if entry.Name[0] == name[0] and string.strcmp(entry.Name, name) == 0: return entry.Alloca - return None - - # find_var_entry - look up a name in the variable table, returning the VarEntry (with AnnotTy) - # ============================================================ - def find_var_entry(self, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str) -> GenTypes.VarEntry*: - """Look up by name in the variable table, return the VarEntry or None""" - if name is None or vars_ptr is None: return None - entry_size: sizet = GenTypes.VarEntry.__sizeof__() - for i in range(int(var_count)): - entry_addr: uint64 = uint64(vars_ptr) + i * entry_size - entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr) - if entry.Name is not None and entry.Used != 0: - if string.strcmp(entry.Name, name) == 0: return entry - return None - - # lookup_var_entry - walk from the current scope upward, returning the VarEntry - # ============================================================ - def lookup_var_entry(self, symtab: GenTypes.SymbolTable*, name: str) -> GenTypes.VarEntry*: - """Walk up from the current scope to find a variable, return the VarEntry or None""" - if symtab is None or name is None: return None - scope: GenTypes.Scope* = symtab.Current - while scope is not None: - result: GenTypes.VarEntry* = self.find_var_entry(scope.Vars, scope.VarCount, name) - if result is not None: return result - scope = scope.Parent - return None - - # set_var_annot_class_name - set a variable's original type-annotation class name. - # Stores it in Spec.SourceName so method dispatch can recover the struct type when alloca is Ptr(i8). - def set_var_annot_class_name(self, symtab: GenTypes.SymbolTable*, name: str, class_name: str) -> int: - """Set a variable's original type-annotation class name into Spec.SourceName, return 0=success / 1=failure""" - if symtab is None or name is None: return 1 - entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) - if entry is None: return 1 - if class_name is None: - # No annotation name: do not write (VarEntry slots are not reused, so the original value is always None). - return 0 - arena_sn: TSpec.TypeSpecArena* = self.Gen.TypeGenBase.ensure_spec_arena() - if arena_sn is None: return 1 - if entry.Spec is None: - entry.Spec = TSpec.NewTypeSpec(arena_sn, TSpec.TS_UNKNOWN, 0, TSpec.TS_SIGN_UNKNOWN) - if entry.Spec is None: return 1 - sign: int = self.Gen.TypeGenCType.spec_sign_of_annot(class_name) - if sign != TSpec.TS_SIGN_UNKNOWN: entry.Spec = TSpec.SpecWithSign(arena_sn, entry.Spec, sign) - entry.Spec = TSpec.SpecWithSource(entry.Spec, class_name) - return 0 - - # var_annot_class_name - unified read entry for a variable entry's annotation class name (S4). - # The annotation side writes Spec.SourceName via SpecWithSource; all read points go through here. - def var_annot_class_name(self, entry: GenTypes.VarEntry*) -> str: - """The variable entry's annotation class name (Spec.SourceName); None if unset""" - if entry is None: return None - return TSpec.SpecSourceName(entry.Spec) - - # set_var_ptr_element - mark a variable with "pointer element" semantics. - # bytes*/str*/T*/64-bit scalar|CPtr degrade to i8*, but indexing must use 8-byte slot strides (ElemBits=64). - def set_var_ptr_element(self, symtab: GenTypes.SymbolTable*, name: str) -> int: - """Mark a variable as ptr-element (Spec.ElemBits=64), return 0=success / 1=failure""" - if symtab is None or name is None: return 1 - entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) - if entry is None: return 1 - # A ptr-element variable's alloca is Ptr(Ptr(Int8)), so its value type implies Spec PtrLevel>=1. - # Violation means an upstream type error; fail-fast, since a silent pass would mis-index by 1 byte. - if entry.Spec is None or entry.Spec.PtrLevel < 1: - self.Gen.TypeGenErr.fatal_error(None, f"variable '{name}' is marked as ptr-element, but its Spec cannot carry pointer semantics", "InternalCompilerError") - return 1 - entry.Spec = TSpec.SpecWithElemBits(entry.Spec, 64) - return 0 - - # var_is_ptr_element - whether a variable has "pointer element" semantics (Spec.ElemBits). - # True when PtrLevel>=1 and Elem.Bits==8 and ElemBits==64; i64*/str/bytes/char* do not match. - def var_is_ptr_element(self, entry: GenTypes.VarEntry*) -> int: - """Whether the variable has ptr-element semantics (1=yes / 0=no)""" - if entry is None or entry.Spec is None: return 0 - sp: TSpec.TypeSpec* = entry.Spec - if sp.PtrLevel < 1 or sp.Elem is None: return 0 - if sp.Elem.Bits != 8: return 0 - if sp.ElemBits == 64: return 1 - return 0 - - # set_var_volatile - mark a variable volatile (annotation union contains t.CVolatile). - # Every load/store of it carries LLVM volatile, preventing hoisting/removal of busy-wait loads. - def set_var_volatile(self, symtab: GenTypes.SymbolTable*, name: str) -> int: - """Mark a variable as volatile (VarEntry.IsVolatile=1), return 0=success / 1=failure""" - if symtab is None or name is None: return 1 - entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) - if entry is None: return 1 - entry.IsVolatile = 1 - return 0 - - # var_is_volatile - whether a variable is volatile (unified read/write entry) - def var_is_volatile(self, entry: GenTypes.VarEntry*) -> int: - """Whether the variable entry is volatile (1=yes / 0=no; 0 when entry is None)""" - if entry is None: return 0 - return entry.IsVolatile - - # set_var_pointee_volatile - mark the pointed-to object volatile (rawptr[t.CVolatile | T]). - # Dereferences (p[i] / p.field / c.Deref(p)) then use LLVM volatile load/store on the pointee. - def set_var_pointee_volatile(self, symtab: GenTypes.SymbolTable*, name: str) -> int: - """Mark the variable's pointee volatile (VarEntry.PointeeVolatile=1), return 0=success / 1=failure""" - if symtab is None or name is None: return 1 - entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) - if entry is None: return 1 - entry.PointeeVolatile = 1 - return 0 - - # var_pointee_is_volatile - whether the pointed-to object is volatile (unified dereference entry) - def var_pointee_is_volatile(self, entry: GenTypes.VarEntry*) -> int: - """Whether the variable entry's pointee is volatile (1=yes / 0=no; 0 when entry is None)""" - if entry is None: return 0 - return entry.PointeeVolatile - - # set_var_const - mark a variable as a const object (annotation has t.CConst and the type is not a pointer). - # Assignment / augmented assignment / element assignment to it are then rejected (same rule as TPC). - def set_var_const(self, symtab: GenTypes.SymbolTable*, name: str) -> int: - """Mark a variable as a const object (VarEntry.IsConst=1), return 0=success / 1=failure""" - if symtab is None or name is None: return 1 - entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) - if entry is None: return 1 - entry.IsConst = 1 - return 0 - - # var_is_const - whether a variable is a const object (unified intercept entry) - def var_is_const(self, entry: GenTypes.VarEntry*) -> int: - """Whether the variable entry is a const object (1=yes / 0=no; 0 when entry is None)""" - if entry is None: return 0 - return entry.IsConst - - # ============================================================ - # 生命期标签读写(LifeTag,见 GenTypes define LIFE_*;架构见 ownership-and-new.md §11) - # ============================================================ - - # set_var_life - write a variable's current-value life tag. - def set_var_life(self, symtab: GenTypes.SymbolTable*, name: str, life: int) -> int: - """Set a variable's LifeTag (VarEntry.Life), return 0=success / 1=failure""" - if symtab is None or name is None: return 1 - entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) - if entry is None: return 1 - entry.Life = life - return 0 - - # get_var_life - read a variable's life tag; LIFE_UNKNOWN when unregistered (conservative). - def get_var_life(self, symtab: GenTypes.SymbolTable*, name: str) -> int: - """A variable's LifeTag; LIFE_UNKNOWN when the variable is unknown""" - if symtab is None or name is None: return GenTypes.LIFE_UNKNOWN - entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) - if entry is None: return GenTypes.LIFE_UNKNOWN - return entry.Life - - # life_meet - control-flow merge: the shorter life wins (smaller tag = shorter). - def life_meet(self, a: int, b: int) -> int: - """Meet of two life tags (min); used where branches join""" - return a if a < b else b - - # life_capture - 快照当前作用域链上所有变量的 LifeTag(供控制流汇合 Meet)。 - # 返回池上数组:buf[0]=count,其后每项 (entry_ptr, life) 各占一个 sizet 槽;失败返回 None。 - def life_capture(self, trans: HT.Translator*) -> sizet*: - """Snapshot every in-scope variable's LifeTag (control-flow merge support)""" - if trans is None or trans.SymTab is None: return None - pool: memhub.MemBuddy* = trans.Pool - if pool is None: return None - cnt: sizet = 0 - sc_c: GenTypes.Scope* = trans.SymTab.Current - while sc_c is not None: - cnt += sizet(sc_c.VarCount) - sc_c = sc_c.Parent - buf: sizet* = sizet*(pool.alloc(8 * (2 * cnt + 2))) - if buf is None: return None - buf[0] = cnt - idx: sizet = 1 - esz: uint64 = uint64(GenTypes.VarEntry.__sizeof__()) - sc: GenTypes.Scope* = trans.SymTab.Current - while sc is not None: - if sc.Vars is not None and sc.VarCount > 0: - j: int = 0 - while j < sc.VarCount: - ent: GenTypes.VarEntry* = GenTypes.VarEntry*(uint64(sc.Vars) + uint64(j) * esz) - buf[idx * 2] = sizet(uint64(ent)) - buf[idx * 2 + 1] = sizet(ent.Life) - idx += 1 - j += 1 - sc = sc.Parent - return buf - - # life_restore - 把快照写回各变量(让 else 分支从 if 之前的状态开始)。 - def life_restore(self, buf: sizet*) -> int: - """Restore LifeTag values from a snapshot; 0=ok / 1=no snapshot""" - if buf is None: return 1 - cnt: sizet = buf[0] - i: sizet = 1 - while i <= cnt: - ent: GenTypes.VarEntry* = GenTypes.VarEntry*(uint64(buf[i * 2])) - if ent is not None: ent.Life = int(buf[i * 2 + 1]) - i += 1 - return 0 - - # life_join - 当前值与快照逐项 Meet(汇合:任一路径为短命即取短命)。 - def life_join(self, buf: sizet*) -> int: - """Meet the current LifeTags with a snapshot; 0=ok / 1=no snapshot""" - if buf is None: return 1 - cnt: sizet = buf[0] - i: sizet = 1 - while i <= cnt: - ent: GenTypes.VarEntry* = GenTypes.VarEntry*(uint64(buf[i * 2])) - if ent is not None: - ent.Life = self.life_meet(ent.Life, int(buf[i * 2 + 1])) - i += 1 - return 0 - - # add_var - add a variable to the variable table - # ============================================================ - def add_var(self, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str, alloca: mlirlite.Value*) -> int: - """Add a variable to the variable table""" - if name is None or alloca is None or vars_ptr is None: return 1 - if var_count >= HT.MAX_VARS: return 1 - entry_size: sizet = GenTypes.VarEntry.__sizeof__() - entry_addr: uint64 = uint64(vars_ptr) + var_count * entry_size - entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr) - # Deep-copy the variable name into stdlib (survives pool reset): storing the AST Name.id pointer lets - # later allocations overwrite it, so find_var's strcmp silently misses and rebuilds a degenerate alloca. - name_len: sizet = string.strlen(name) - name_copy: bytes = stdlib.malloc(name_len + 1) - if name_copy is None: return 1 - string.strcpy(name_copy, name) - entry.Name = name_copy - entry.Alloca = alloca - entry.Used = 1 - # Explicitly zero the const flag (do not rely on the entry's zero initialization): set_var_const - # sets it to 1 only for real const objects, so zeroing avoids false positives. - entry.IsConst = 0 - # 生命期标签显式初始化:未登记 = LIFE_UNKNOWN(保守放行)。 - entry.Life = GenTypes.LIFE_UNKNOWN - # [S3 dual write] Value-type Spec (alloca.Ty is Ptr(value type), so take its pointee) - entry.Spec = self._spec_of_var_alloca(alloca) - return 0 - - # ---- P2 willFree 失效槽表 API(不改 VarEntry 布局) ---- - def _FreedMark(self, slot: mlirlite.Value*) -> int: - """把变量槽标为"已失效"(被 @life(willFree(...)) 标注的调用消费过);1=已标记 / 0=未处理""" - global _g_freed_slots, _g_freed_count - if slot is None: return 0 - if self._FreedCheck(slot) != 0: return 1 - if _g_freed_slots is None: - _g_freed_slots = sizet*(stdlib.malloc(_FREED_MAX * 8)) - if _g_freed_slots is None: return 0 - if _g_freed_count >= _FREED_MAX: - self.Gen.TypeGenErr.fatal_error(None, "released-variable table full (capacity 4096): too many released variables in one translation", "LifetimeError") - return 0 - _g_freed_slots[_g_freed_count] = sizet(slot) - _g_freed_count += 1 - return 1 - - def _FreedCheck(self, slot: mlirlite.Value*) -> int: - """该变量槽是否已被标为失效;1=已失效(其后再读/取址/传参即 LifetimeError)/ 0=有效""" - if slot is None or _g_freed_slots is None: return 0 - i: int = 0 - while i < _g_freed_count: - if uint64(_g_freed_slots[i]) == uint64(slot): return 1 - i += 1 - return 0 - - def _FreedClearSlot(self, slot: mlirlite.Value*) -> int: - """清除某槽的失效态(对同名变量重新赋值后调用);1=清除过 / 0=本来不在表里""" - global _g_freed_count - if slot is None or _g_freed_slots is None: return 0 - i: int = 0 - while i < _g_freed_count: - if uint64(_g_freed_slots[i]) == uint64(slot): - _g_freed_count -= 1 - _g_freed_slots[i] = _g_freed_slots[_g_freed_count] - return 1 - i += 1 - return 0 - - def clear_var_freed(self, symtab: GenTypes.SymbolTable*, name: str) -> int: - """P2 willFree:对同名变量重新赋值后清除失效态;1=清除过 / 0=无该项""" - if symtab is None or name is None: return 0 - return self._FreedClearSlot(self.lookup_var(symtab, name)) - - # ---- P2 willFree:失效槽表的控制流汇合(与 life_capture/life_restore/life_join 同点位) ---- - # 背景:失效表是"平坦、单调"的全局表(_FreedMark 只插入),原本无分支合流语义, - # 于是"分支体内 free(x)"会把失效态泄漏到合流之后 → `if c: free(p); return` 之后的 - # `return p` 误报。这里给 if/while/for 补上快照/复位/求交,口径 = "只有所有到达合流的 - # 路径都失效,才算失效"(求交),与 life_* 的 Meet 同点位、同生命周期(pool 快照)。 - def freed_capture(self, trans: HT.Translator*) -> sizet*: - """快照当前失效槽表:buf[0]=count,其后每项一个失效槽指针;失败返回 None""" - global _g_freed_slots, _g_freed_count - if trans is None or trans.Pool is None: return None - pool: memhub.MemBuddy* = trans.Pool - n: sizet = sizet(_g_freed_count) - buf: sizet* = sizet*(pool.alloc((n + 1) * 8)) - if buf is None: return None - buf[0] = n - i: sizet = 0 - while i < n: - buf[1 + i] = _g_freed_slots[i] - i += 1 - return buf - - def freed_restore(self, buf: sizet*) -> int: - """把失效槽表复位到快照(分支出口 / 单侧到达合流时用);0=完成 / 1=无快照""" - global _g_freed_slots, _g_freed_count - if buf is None: return 1 - n: sizet = buf[0] - if n > 0: - if _g_freed_slots is None: - _g_freed_slots = sizet*(stdlib.malloc(_FREED_MAX * 8)) - if _g_freed_slots is None: return 1 - i: sizet = 0 - while i < n: - _g_freed_slots[i] = buf[1 + i] - i += 1 - _g_freed_count = int(n) - return 0 - - def freed_clear(self) -> None: - """进入新函数体:清空失效槽表(函数作用域复位)。 - - 表按 **alloca 槽地址** 记录(_FreedCheck 比较槽指针),而栈槽地址会被后续函数复用 - (前帧已销毁)⇒ 若跨函数保留,上一函数的失效态会误加到新函数的同名/同址变量上 - (现场:ExprGenCls._resolve_doc_symbol_attr 首行读形参 out_sym 即报 "already released", - 而该函数体内根本没有 willFree 调用——是上一个函数的栈槽地址被复用)。 - 故每个函数体入口清零;嵌套函数体结束后不恢复外层失效态(仅为少报,绝非误报)。 - """ - global _g_freed_count - _g_freed_count = 0 - - def freed_join(self, buf: sizet*) -> int: - """合流:当前失效槽表与快照**求交**(只有两路都失效才算失效);0=完成 / 1=无快照""" - global _g_freed_slots, _g_freed_count - if buf is None: return 1 - n: sizet = buf[0] - out: int = 0 - i: sizet = 0 - while i < sizet(_g_freed_count): - if _g_freed_slots is None: break - s: sizet = _g_freed_slots[i] - j: sizet = 0 - keep: int = 0 - while j < n: - if buf[1 + j] == s: keep = 1 - if keep != 0: break - j += 1 - if keep != 0: - _g_freed_slots[out] = s - out += 1 - i += 1 - _g_freed_count = out - return 0 - - # _spec_of_var_alloca - build a TypeSpec from the alloca's value type (S3 dual write). - # alloca.Ty is Ptr(var_ty), so the pointee is the value type; None means failure or an unsupported type. - def _spec_of_var_alloca(self, alloca: mlirlite.Value*) -> TSpec.TypeSpec*: - """Build a TypeSpec from an alloca's value type; None if not applicable or failed""" - if alloca is None or alloca.Ty is None: return None - ty: mlirlite.LLVMType* = alloca.Ty - if self.Gen.ExprGenBase.is_ptr_type(ty) != 0: ty = ty.Pointee - arena: TSpec.TypeSpecArena* = self.Gen.TypeGenBase.ensure_spec_arena() - if arena is None: return None - return TSpec.SpecFromLLVMType(arena, ty) - - # _alloca_at_entry - emit an alloca in the function entry block (ensures dominance). - # LLVM IR requires every instruction to dominate its uses, so all allocas go in the entry block. - def _alloca_at_entry(self, builder: mlirlite.IRBuilder*, ty: mlirlite.LLVMType*) -> mlirlite.Value*: - """Emit an alloca in the function entry block before the terminator, return the alloca value.""" - if builder is None or ty is None: return None - return mlirlite.build_alloca_at_entry(builder, ty) - - # get_or_create_var - find or create a variable alloca (legacy-compatible) - def get_or_create_var(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str, ty: mlirlite.LLVMType*) -> mlirlite.Value*: - """Find or create a variable alloca""" - existing: mlirlite.Value* = self.find_var(vars_ptr, var_count, name) - if existing is not None: return existing - alloca: mlirlite.Value* = self._alloca_at_entry(builder, ty) - if alloca is None: return None - if self.add_var(vars_ptr, var_count, name, alloca) != 0: return None - return alloca - - # _create_scope - create a new scope node (internal helper) - # ============================================================ - def _create_scope(self, pool: memhub.MemBuddy*, parent: GenTypes.Scope*, kind: int) -> GenTypes.Scope*: - """Create and initialize a scope node""" - scope: GenTypes.Scope* = pool.alloc(GenTypes.Scope.__sizeof__()) - if scope is None: return None - string.memset(scope, 0, GenTypes.Scope.__sizeof__()) - scope.Parent = parent - scope.Vars = self.init_vars(pool) - scope.VarCount = 0 - scope.Kind = kind - if scope.Vars is None: return None - return scope - - # init_symbol_table - create the symbol table (with the module-level root scope) - # ============================================================ - def init_symbol_table(self, pool: memhub.MemBuddy*) -> GenTypes.SymbolTable*: - """Create and initialize the symbol table with a module-level root scope""" - if pool is None: return None - symtab: GenTypes.SymbolTable* = pool.alloc(GenTypes.SymbolTable.__sizeof__()) - if symtab is None: return None - string.memset(symtab, 0, GenTypes.SymbolTable.__sizeof__()) - symtab.Pool = pool - - # Create the root scope (module level) - root: GenTypes.Scope* = self._create_scope(pool, None, SCOPE_MODULE) - if root is None: return None - - symtab.Root = root - symtab.Current = root - return symtab - - # enter_scope - enter a new scope - # ============================================================ - def enter_scope(self, symtab: GenTypes.SymbolTable*, kind: int) -> GenTypes.Scope*: - """Enter a new scope and return it""" - if symtab is None: return None - pool: memhub.MemBuddy* = symtab.Pool - scope: GenTypes.Scope* = self._create_scope(pool, symtab.Current, kind) - if scope is None: return None - symtab.Current = scope - return scope - - # exit_scope - exit the current scope - # ============================================================ - def exit_scope(self, symtab: GenTypes.SymbolTable*): - """Exit the current scope and restore its parent""" - if symtab is not None and symtab.Current is not None: symtab.Current = symtab.Current.Parent - - # define_var - define a variable in the current scope - # ============================================================ - def define_var(self, symtab: GenTypes.SymbolTable*, name: str, alloca: mlirlite.Value*) -> int: - """Define a variable in the current scope, return 0 on success""" - if symtab is None or name is None or alloca is None: return 1 - scope: GenTypes.Scope* = symtab.Current - if scope is None: return 1 - ret: int = self.add_var(scope.Vars, scope.VarCount, name, alloca) - if ret == 0: scope.VarCount = scope.VarCount + 1 - return ret - - # define_module_var - define a variable in the module scope - # ============================================================ - def define_module_var(self, symtab: GenTypes.SymbolTable*, name: str, alloca: mlirlite.Value*) -> int: - """Define a variable in the module scope, return 0 on success""" - if symtab is None or name is None or alloca is None: return 1 - scope: GenTypes.Scope* = symtab.Root - if scope is None: return 1 - ret: int = self.add_var(scope.Vars, scope.VarCount, name, alloca) - if ret == 0: scope.VarCount = scope.VarCount + 1 - return ret - - # lookup_var - walk from the current scope upward looking for a variable - # ============================================================ - def lookup_var(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: - """Walk up from the current scope to find a variable, return its alloca or None""" - if symtab is None or name is None: return None - scope: GenTypes.Scope* = symtab.Current - while scope is not None: - result: mlirlite.Value* = self.find_var(scope.Vars, scope.VarCount, name) - if result is not None: return result - scope = scope.Parent - return None - - # lookup_current - look up a variable only in the current scope - # ============================================================ - def lookup_current(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: - """Look up a variable in the current scope only""" - if symtab is None or name is None: return None - scope: GenTypes.Scope* = symtab.Current - if scope is None: return None - return self.find_var(scope.Vars, scope.VarCount, name) - - # ============================================================ - # M3 owned slife 登记 API(§4.2 函数级 scope guard / §4.3 不许手动 free) - # ============================================================ - def _SlifeReset(self, pool: memhub.MemBuddy*) -> int: - """函数入口:保存外层区间两个边界、把本层起点抬到当前栈顶;返回令牌(-1=不可用,须原样交还 _SlifeRestore)""" - global _g_slife_owned, _g_slife_save, _g_slife_count, _g_slife_top, _g_slife_depth - if _g_slife_owned is None: - # 表体跨文件存活:必须用 stdlib.malloc,**不能**用 pool(trans.Pool 是每文件池, - # Phase2 每文件结束 reset_to 后表体即悬垂,后续写入会砸活跃数据)。 - _g_slife_owned = sizet*(stdlib.malloc(_SLIFE_MAX * 16)) - if _g_slife_owned is None: return -1 - if _g_slife_save is None: - _g_slife_save = sizet*(stdlib.malloc(_SLIFE_DEPTH_MAX * 16)) - if _g_slife_save is None: return -1 - if _g_slife_depth >= _SLIFE_DEPTH_MAX: - self.Gen.TypeGenErr.fatal_error(None, "slife ownership save stack full (function nesting > 64)", "LifetimeError") - return -1 - _g_slife_save[_g_slife_depth * 2] = sizet(_g_slife_count) - _g_slife_save[_g_slife_depth * 2 + 1] = sizet(_g_slife_top) - tok: int = _g_slife_depth - _g_slife_depth = _g_slife_depth + 1 - _g_slife_count = _g_slife_top - return tok - - def _SlifeRestore(self, tok: int) -> int: - """函数出口:**两个边界一起弹回**进入前的值(不释放;释放由 _SlifeEmitFrees 先做)。 - - 关键:必须把 _g_slife_top 还原成进入前的值。只还原 count 会让 top 随函数数量单调增长, - 上一文件/上一函数的陈旧条目就一直留在 [_g_slife_count, _g_slife_top) 被扫描—— - 而每文件池 reset_to 会复用同样的地址,于是"陈旧条目"能与新 alloca 地址相同, - _SlifeShadow 返回悬垂的影子槽,_SlifeNoteStore 直接写坏内存。""" - global _g_slife_count, _g_slife_top, _g_slife_depth - if tok < 0 or _g_slife_save is None: return 0 - if tok >= _g_slife_depth: return 0 - _g_slife_count = int(_g_slife_save[tok * 2]) - _g_slife_top = int(_g_slife_save[tok * 2 + 1]) - _g_slife_depth = tok - return 1 - - def _SlifeShadow(self, alloca: mlirlite.Value*) -> mlirlite.Value*: - """查已登记槽的所有权影子标志槽(未登记 → None)""" - if alloca is None or _g_slife_owned is None: return None - i: int = 0 - while i < _g_slife_top: - if uint64(_g_slife_owned[i * 2]) == uint64(alloca): - if _g_slife_owned[i * 2 + 1] == 0: return None - return mlirlite.Value*(_g_slife_owned[i * 2 + 1]) - i += 1 - return None - - def _SlifeOwned(self, alloca: mlirlite.Value*) -> int: - """该槽是否登记为 slife 绑定(= 由编译器负责释放,§4.3 判据5);1=是 / 0=否""" - if self._SlifeShadow(alloca) is not None: return 1 - return 0 - - def _SlifeEnsureOwned(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, alloca: mlirlite.Value*) -> mlirlite.Value*: - """登记该槽(首次:在入口块建 i8 影子并置 0),返回影子标志槽;不可用 → None""" - global _g_slife_top - if alloca is None or _g_slife_owned is None: return None - exist: mlirlite.Value* = self._SlifeShadow(alloca) - if exist is not None: return exist +novirtual class VarGenMixin: + """Variable/scope mixin""" + # init_vars - allocate and zero the VarEntry array + # ============================================================ + Gen: Generator.Generator* + def init_vars(self, pool: memhub.MemBuddy*) -> GenTypes.VarEntry*: + """Allocate and zero the variable table array""" + size: sizet = HT.MAX_VARS * GenTypes.VarEntry.__sizeof__() + vars_ptr: GenTypes.VarEntry* = pool.alloc(size) + if vars_ptr is not None: string.memset(vars_ptr, 0, size) + return vars_ptr + + # find_var - look up a name in the variable table + # ============================================================ + def find_var(self, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str) -> mlirlite.Value*: + """Look up by name in the variable table""" + if name is None or vars_ptr is None: return None + entry_size: sizet = GenTypes.VarEntry.__sizeof__() + for i in range(int(var_count)): + entry_addr: uint64 = uint64(vars_ptr) + i * entry_size + entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr) + if entry.Name is not None and entry.Used != 0: + # Fast path: strcmp equality implies equal first chars, so skip strcmp when they differ + if entry.Name[0] == name[0] and string.strcmp(entry.Name, name) == 0: return entry.Alloca + return None + + # find_var_entry - look up a name in the variable table, returning the VarEntry (with AnnotTy) + # ============================================================ + def find_var_entry(self, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str) -> GenTypes.VarEntry*: + """Look up by name in the variable table, return the VarEntry or None""" + if name is None or vars_ptr is None: return None + entry_size: sizet = GenTypes.VarEntry.__sizeof__() + for i in range(int(var_count)): + entry_addr: uint64 = uint64(vars_ptr) + i * entry_size + entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr) + if entry.Name is not None and entry.Used != 0: + if string.strcmp(entry.Name, name) == 0: return entry + return None + + # lookup_var_entry - walk from the current scope upward, returning the VarEntry + # ============================================================ + def lookup_var_entry(self, symtab: GenTypes.SymbolTable*, name: str) -> GenTypes.VarEntry*: + """Walk up from the current scope to find a variable, return the VarEntry or None""" + if symtab is None or name is None: return None + scope: GenTypes.Scope* = symtab.Current + while scope is not None: + result: GenTypes.VarEntry* = self.find_var_entry(scope.Vars, scope.VarCount, name) + if result is not None: return result + scope = scope.Parent + return None + + # set_var_annot_class_name - set a variable's original type-annotation class name. + # Stores it in Spec.SourceName so method dispatch can recover the struct type when alloca is Ptr(i8). + def set_var_annot_class_name(self, symtab: GenTypes.SymbolTable*, name: str, class_name: str) -> int: + """Set a variable's original type-annotation class name into Spec.SourceName, return 0=success / 1=failure""" + if symtab is None or name is None: return 1 + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return 1 + if class_name is None: + # No annotation name: do not write (VarEntry slots are not reused, so the original value is always None). + return 0 + arena_sn: TSpec.TypeSpecArena* = self.Gen.TypeGenBase.ensure_spec_arena() + if arena_sn is None: return 1 + if entry.Spec is None: + entry.Spec = TSpec.NewTypeSpec(arena_sn, TSpec.TS_UNKNOWN, 0, TSpec.TS_SIGN_UNKNOWN) + if entry.Spec is None: return 1 + sign: int = self.Gen.TypeGenCType.spec_sign_of_annot(class_name) + if sign != TSpec.TS_SIGN_UNKNOWN: entry.Spec = TSpec.SpecWithSign(arena_sn, entry.Spec, sign) + entry.Spec = TSpec.SpecWithSource(entry.Spec, class_name) + return 0 + + # var_annot_class_name - unified read entry for a variable entry's annotation class name (S4). + # The annotation side writes Spec.SourceName via SpecWithSource; all read points go through here. + def var_annot_class_name(self, entry: GenTypes.VarEntry*) -> str: + """The variable entry's annotation class name (Spec.SourceName); None if unset""" + if entry is None: return None + return TSpec.SpecSourceName(entry.Spec) + + # set_var_ptr_element - mark a variable with "pointer element" semantics. + # bytes*/str*/T*/64-bit scalar|CPtr degrade to i8*, but indexing must use 8-byte slot strides (ElemBits=64). + def set_var_ptr_element(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """Mark a variable as ptr-element (Spec.ElemBits=64), return 0=success / 1=failure""" + if symtab is None or name is None: return 1 + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return 1 + # A ptr-element variable's alloca is Ptr(Ptr(Int8)), so its value type implies Spec PtrLevel>=1. + # Violation means an upstream type error; fail-fast, since a silent pass would mis-index by 1 byte. + if entry.Spec is None or entry.Spec.PtrLevel < 1: + self.Gen.TypeGenErr.fatal_error(None, f"variable '{name}' is marked as ptr-element, but its Spec cannot carry pointer semantics", "InternalCompilerError") + return 1 + entry.Spec = TSpec.SpecWithElemBits(entry.Spec, 64) + return 0 + + # var_is_ptr_element - whether a variable has "pointer element" semantics (Spec.ElemBits). + # True when PtrLevel>=1 and Elem.Bits==8 and ElemBits==64; i64*/str/bytes/char* do not match. + def var_is_ptr_element(self, entry: GenTypes.VarEntry*) -> int: + """Whether the variable has ptr-element semantics (1=yes / 0=no)""" + if entry is None or entry.Spec is None: return 0 + sp: TSpec.TypeSpec* = entry.Spec + if sp.PtrLevel < 1 or sp.Elem is None: return 0 + if sp.Elem.Bits != 8: return 0 + if sp.ElemBits == 64: return 1 + return 0 + + # set_var_volatile - mark a variable volatile (annotation union contains t.CVolatile). + # Every load/store of it carries LLVM volatile, preventing hoisting/removal of busy-wait loads. + def set_var_volatile(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """Mark a variable as volatile (VarEntry.IsVolatile=1), return 0=success / 1=failure""" + if symtab is None or name is None: return 1 + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return 1 + entry.IsVolatile = 1 + return 0 + + # var_is_volatile - whether a variable is volatile (unified read/write entry) + def var_is_volatile(self, entry: GenTypes.VarEntry*) -> int: + """Whether the variable entry is volatile (1=yes / 0=no; 0 when entry is None)""" + if entry is None: return 0 + return entry.IsVolatile + + # set_var_pointee_volatile - mark the pointed-to object volatile (rawptr[t.CVolatile | T]). + # Dereferences (p[i] / p.field / c.Deref(p)) then use LLVM volatile load/store on the pointee. + def set_var_pointee_volatile(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """Mark the variable's pointee volatile (VarEntry.PointeeVolatile=1), return 0=success / 1=failure""" + if symtab is None or name is None: return 1 + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return 1 + entry.PointeeVolatile = 1 + return 0 + + # var_pointee_is_volatile - whether the pointed-to object is volatile (unified dereference entry) + def var_pointee_is_volatile(self, entry: GenTypes.VarEntry*) -> int: + """Whether the variable entry's pointee is volatile (1=yes / 0=no; 0 when entry is None)""" + if entry is None: return 0 + return entry.PointeeVolatile + + # set_var_const - mark a variable as a const object (annotation has t.CConst and the type is not a pointer). + # Assignment / augmented assignment / element assignment to it are then rejected (same rule as TPC). + def set_var_const(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """Mark a variable as a const object (VarEntry.IsConst=1), return 0=success / 1=failure""" + if symtab is None or name is None: return 1 + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return 1 + entry.IsConst = 1 + return 0 + + # var_is_const - whether a variable is a const object (unified intercept entry) + def var_is_const(self, entry: GenTypes.VarEntry*) -> int: + """Whether the variable entry is a const object (1=yes / 0=no; 0 when entry is None)""" + if entry is None: return 0 + return entry.IsConst + + # ============================================================ + # 生命期标签读写(LifeTag,见 GenTypes define LIFE_*;架构见 ownership-and-new.md §11) + # ============================================================ + + # set_var_life - write a variable's current-value life tag. + def set_var_life(self, symtab: GenTypes.SymbolTable*, name: str, life: int) -> int: + """Set a variable's LifeTag (VarEntry.Life), return 0=success / 1=failure""" + if symtab is None or name is None: return 1 + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return 1 + entry.Life = life + return 0 + + # get_var_life - read a variable's life tag; LIFE_UNKNOWN when unregistered (conservative). + def get_var_life(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """A variable's LifeTag; LIFE_UNKNOWN when the variable is unknown""" + if symtab is None or name is None: return GenTypes.LIFE_UNKNOWN + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return GenTypes.LIFE_UNKNOWN + return entry.Life + + # life_meet - control-flow merge: the shorter life wins (smaller tag = shorter). + def life_meet(self, a: int, b: int) -> int: + """Meet of two life tags (min); used where branches join""" + return a if a < b else b + + # life_capture - 快照当前作用域链上所有变量的 LifeTag(供控制流汇合 Meet)。 + # 返回池上数组:buf[0]=count,其后每项 (entry_ptr, life) 各占一个 sizet 槽;失败返回 None。 + def life_capture(self, trans: HT.Translator*) -> sizet*: + """Snapshot every in-scope variable's LifeTag (control-flow merge support)""" + if trans is None or trans.SymTab is None: return None + pool: memhub.MemBuddy* = trans.Pool + if pool is None: return None + cnt: sizet = 0 + sc_c: GenTypes.Scope* = trans.SymTab.Current + while sc_c is not None: + cnt += sizet(sc_c.VarCount) + sc_c = sc_c.Parent + buf: sizet* = sizet*(pool.alloc(8 * (2 * cnt + 2))) + if buf is None: return None + buf[0] = cnt + idx: sizet = 1 + esz: uint64 = uint64(GenTypes.VarEntry.__sizeof__()) + sc: GenTypes.Scope* = trans.SymTab.Current + while sc is not None: + if sc.Vars is not None and sc.VarCount > 0: + j: int = 0 + while j < sc.VarCount: + ent: GenTypes.VarEntry* = GenTypes.VarEntry*(uint64(sc.Vars) + uint64(j) * esz) + buf[idx * 2] = sizet(uint64(ent)) + buf[idx * 2 + 1] = sizet(ent.Life) + idx += 1 + j += 1 + sc = sc.Parent + return buf + + # life_restore - 把快照写回各变量(让 else 分支从 if 之前的状态开始)。 + def life_restore(self, buf: sizet*) -> int: + """Restore LifeTag values from a snapshot; 0=ok / 1=no snapshot""" + if buf is None: return 1 + cnt: sizet = buf[0] + i: sizet = 1 + while i <= cnt: + ent: GenTypes.VarEntry* = GenTypes.VarEntry*(uint64(buf[i * 2])) + if ent is not None: ent.Life = int(buf[i * 2 + 1]) + i += 1 + return 0 + + # life_join - 当前值与快照逐项 Meet(汇合:任一路径为短命即取短命)。 + def life_join(self, buf: sizet*) -> int: + """Meet the current LifeTags with a snapshot; 0=ok / 1=no snapshot""" + if buf is None: return 1 + cnt: sizet = buf[0] + i: sizet = 1 + while i <= cnt: + ent: GenTypes.VarEntry* = GenTypes.VarEntry*(uint64(buf[i * 2])) + if ent is not None: + ent.Life = self.life_meet(ent.Life, int(buf[i * 2 + 1])) + i += 1 + return 0 + + # add_var - add a variable to the variable table + # ============================================================ + def add_var(self, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str, alloca: mlirlite.Value*) -> int: + """Add a variable to the variable table""" + if name is None or alloca is None or vars_ptr is None: return 1 + if var_count >= HT.MAX_VARS: return 1 + entry_size: sizet = GenTypes.VarEntry.__sizeof__() + entry_addr: uint64 = uint64(vars_ptr) + var_count * entry_size + entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr) + # Deep-copy the variable name into stdlib (survives pool reset): storing the AST Name.id pointer lets + # later allocations overwrite it, so find_var's strcmp silently misses and rebuilds a degenerate alloca. + name_len: sizet = string.strlen(name) + name_copy: bytes = stdlib.malloc(name_len + 1) + if name_copy is None: return 1 + string.strcpy(name_copy, name) + entry.Name = name_copy + entry.Alloca = alloca + entry.Used = 1 + # Explicitly zero the const flag (do not rely on the entry's zero initialization): set_var_const + # sets it to 1 only for real const objects, so zeroing avoids false positives. + entry.IsConst = 0 + # 生命期标签显式初始化:未登记 = LIFE_UNKNOWN(保守放行)。 + entry.Life = GenTypes.LIFE_UNKNOWN + # [S3 dual write] Value-type Spec (alloca.Ty is Ptr(value type), so take its pointee) + entry.Spec = self._spec_of_var_alloca(alloca) + return 0 + + # ---- P2 willFree 失效槽表 API(不改 VarEntry 布局) ---- + def _FreedMark(self, slot: mlirlite.Value*) -> int: + """把变量槽标为"已失效"(被 @life(willFree(...)) 标注的调用消费过);1=已标记 / 0=未处理""" + global _g_freed_slots, _g_freed_count + if slot is None: return 0 + if self._FreedCheck(slot) != 0: return 1 + if _g_freed_slots is None: + _g_freed_slots = sizet*(stdlib.malloc(_FREED_MAX * 8)) + if _g_freed_slots is None: return 0 + if _g_freed_count >= _FREED_MAX: + self.Gen.TypeGenErr.fatal_error(None, "released-variable table full (capacity 4096): too many released variables in one translation", "LifetimeError") + return 0 + _g_freed_slots[_g_freed_count] = sizet(slot) + _g_freed_count += 1 + return 1 + + def _FreedCheck(self, slot: mlirlite.Value*) -> int: + """该变量槽是否已被标为失效;1=已失效(其后再读/取址/传参即 LifetimeError)/ 0=有效""" + if slot is None or _g_freed_slots is None: return 0 + i: int = 0 + while i < _g_freed_count: + if uint64(_g_freed_slots[i]) == uint64(slot): return 1 + i += 1 + return 0 + + def _FreedClearSlot(self, slot: mlirlite.Value*) -> int: + """清除某槽的失效态(对同名变量重新赋值后调用);1=清除过 / 0=本来不在表里""" + global _g_freed_count + if slot is None or _g_freed_slots is None: return 0 + i: int = 0 + while i < _g_freed_count: + if uint64(_g_freed_slots[i]) == uint64(slot): + _g_freed_count -= 1 + _g_freed_slots[i] = _g_freed_slots[_g_freed_count] + return 1 + i += 1 + return 0 + + def clear_var_freed(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """P2 willFree:对同名变量重新赋值后清除失效态;1=清除过 / 0=无该项""" + if symtab is None or name is None: return 0 + return self._FreedClearSlot(self.lookup_var(symtab, name)) + + # ---- P2 willFree:失效槽表的控制流汇合(与 life_capture/life_restore/life_join 同点位) ---- + # 背景:失效表是"平坦、单调"的全局表(_FreedMark 只插入),原本无分支合流语义, + # 于是"分支体内 free(x)"会把失效态泄漏到合流之后 → `if c: free(p); return` 之后的 + # `return p` 误报。这里给 if/while/for 补上快照/复位/求交,口径 = "只有所有到达合流的 + # 路径都失效,才算失效"(求交),与 life_* 的 Meet 同点位、同生命周期(pool 快照)。 + def freed_capture(self, trans: HT.Translator*) -> sizet*: + """快照当前失效槽表:buf[0]=count,其后每项一个失效槽指针;失败返回 None""" + global _g_freed_slots, _g_freed_count + if trans is None or trans.Pool is None: return None + pool: memhub.MemBuddy* = trans.Pool + n: sizet = sizet(_g_freed_count) + buf: sizet* = sizet*(pool.alloc((n + 1) * 8)) + if buf is None: return None + buf[0] = n + i: sizet = 0 + while i < n: + buf[1 + i] = _g_freed_slots[i] + i += 1 + return buf + + def freed_restore(self, buf: sizet*) -> int: + """把失效槽表复位到快照(分支出口 / 单侧到达合流时用);0=完成 / 1=无快照""" + global _g_freed_slots, _g_freed_count + if buf is None: return 1 + n: sizet = buf[0] + if n > 0: + if _g_freed_slots is None: + _g_freed_slots = sizet*(stdlib.malloc(_FREED_MAX * 8)) + if _g_freed_slots is None: return 1 + i: sizet = 0 + while i < n: + _g_freed_slots[i] = buf[1 + i] + i += 1 + _g_freed_count = int(n) + return 0 + + def freed_clear(self) -> None: + """进入新函数体:清空失效槽表(函数作用域复位)。 + + 表按 **alloca 槽地址** 记录(_FreedCheck 比较槽指针),而栈槽地址会被后续函数复用 + (前帧已销毁)⇒ 若跨函数保留,上一函数的失效态会误加到新函数的同名/同址变量上 + (现场:ExprGenCls._resolve_doc_symbol_attr 首行读形参 out_sym 即报 "already released", + 而该函数体内根本没有 willFree 调用——是上一个函数的栈槽地址被复用)。 + 故每个函数体入口清零;嵌套函数体结束后不恢复外层失效态(仅为少报,绝非误报)。 + """ + global _g_freed_count + _g_freed_count = 0 + + def freed_join(self, buf: sizet*) -> int: + """合流:当前失效槽表与快照**求交**(只有两路都失效才算失效);0=完成 / 1=无快照""" + global _g_freed_slots, _g_freed_count + if buf is None: return 1 + n: sizet = buf[0] + out: int = 0 + i: sizet = 0 + while i < sizet(_g_freed_count): + if _g_freed_slots is None: break + s: sizet = _g_freed_slots[i] + j: sizet = 0 + keep: int = 0 + while j < n: + if buf[1 + j] == s: keep = 1 + if keep != 0: break + j += 1 + if keep != 0: + _g_freed_slots[out] = s + out += 1 + i += 1 + _g_freed_count = out + return 0 + + # _spec_of_var_alloca - build a TypeSpec from the alloca's value type (S3 dual write). + # alloca.Ty is Ptr(var_ty), so the pointee is the value type; None means failure or an unsupported type. + def _spec_of_var_alloca(self, alloca: mlirlite.Value*) -> TSpec.TypeSpec*: + """Build a TypeSpec from an alloca's value type; None if not applicable or failed""" + if alloca is None or alloca.Ty is None: return None + ty: mlirlite.LLVMType* = alloca.Ty + if self.Gen.ExprGenBase.is_ptr_type(ty) != 0: ty = ty.Pointee + arena: TSpec.TypeSpecArena* = self.Gen.TypeGenBase.ensure_spec_arena() + if arena is None: return None + return TSpec.SpecFromLLVMType(arena, ty) + + # _alloca_at_entry - emit an alloca in the function entry block (ensures dominance). + # LLVM IR requires every instruction to dominate its uses, so all allocas go in the entry block. + def _alloca_at_entry(self, builder: mlirlite.IRBuilder*, ty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Emit an alloca in the function entry block before the terminator, return the alloca value.""" + if builder is None or ty is None: return None + return mlirlite.build_alloca_at_entry(builder, ty) + + # get_or_create_var - find or create a variable alloca (legacy-compatible) + def get_or_create_var(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str, ty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Find or create a variable alloca""" + existing: mlirlite.Value* = self.find_var(vars_ptr, var_count, name) + if existing is not None: return existing + alloca: mlirlite.Value* = self._alloca_at_entry(builder, ty) + if alloca is None: return None + if self.add_var(vars_ptr, var_count, name, alloca) != 0: return None + return alloca + + # _create_scope - create a new scope node (internal helper) + # ============================================================ + def _create_scope(self, pool: memhub.MemBuddy*, parent: GenTypes.Scope*, kind: int) -> GenTypes.Scope*: + """Create and initialize a scope node""" + scope: GenTypes.Scope* = pool.alloc(GenTypes.Scope.__sizeof__()) + if scope is None: return None + string.memset(scope, 0, GenTypes.Scope.__sizeof__()) + scope.Parent = parent + scope.Vars = self.init_vars(pool) + scope.VarCount = 0 + scope.Kind = kind + if scope.Vars is None: return None + return scope + + # init_symbol_table - create the symbol table (with the module-level root scope) + # ============================================================ + def init_symbol_table(self, pool: memhub.MemBuddy*) -> GenTypes.SymbolTable*: + """Create and initialize the symbol table with a module-level root scope""" + if pool is None: return None + symtab: GenTypes.SymbolTable* = pool.alloc(GenTypes.SymbolTable.__sizeof__()) + if symtab is None: return None + string.memset(symtab, 0, GenTypes.SymbolTable.__sizeof__()) + symtab.Pool = pool + + # Create the root scope (module level) + root: GenTypes.Scope* = self._create_scope(pool, None, SCOPE_MODULE) + if root is None: return None + + symtab.Root = root + symtab.Current = root + return symtab + + # enter_scope - enter a new scope + # ============================================================ + def enter_scope(self, symtab: GenTypes.SymbolTable*, kind: int) -> GenTypes.Scope*: + """Enter a new scope and return it""" + if symtab is None: return None + pool: memhub.MemBuddy* = symtab.Pool + scope: GenTypes.Scope* = self._create_scope(pool, symtab.Current, kind) + if scope is None: return None + symtab.Current = scope + return scope + + # exit_scope - exit the current scope + # ============================================================ + def exit_scope(self, symtab: GenTypes.SymbolTable*): + """Exit the current scope and restore its parent""" + if symtab is not None and symtab.Current is not None: symtab.Current = symtab.Current.Parent + + # define_var - define a variable in the current scope + # ============================================================ + def define_var(self, symtab: GenTypes.SymbolTable*, name: str, alloca: mlirlite.Value*) -> int: + """Define a variable in the current scope, return 0 on success""" + if symtab is None or name is None or alloca is None: return 1 + scope: GenTypes.Scope* = symtab.Current + if scope is None: return 1 + ret: int = self.add_var(scope.Vars, scope.VarCount, name, alloca) + if ret == 0: scope.VarCount = scope.VarCount + 1 + return ret + + # define_module_var - define a variable in the module scope + # ============================================================ + def define_module_var(self, symtab: GenTypes.SymbolTable*, name: str, alloca: mlirlite.Value*) -> int: + """Define a variable in the module scope, return 0 on success""" + if symtab is None or name is None or alloca is None: return 1 + scope: GenTypes.Scope* = symtab.Root + if scope is None: return 1 + ret: int = self.add_var(scope.Vars, scope.VarCount, name, alloca) + if ret == 0: scope.VarCount = scope.VarCount + 1 + return ret + + # lookup_var - walk from the current scope upward looking for a variable + # ============================================================ + def lookup_var(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: + """Walk up from the current scope to find a variable, return its alloca or None""" + if symtab is None or name is None: return None + scope: GenTypes.Scope* = symtab.Current + while scope is not None: + result: mlirlite.Value* = self.find_var(scope.Vars, scope.VarCount, name) + if result is not None: return result + scope = scope.Parent + return None + + # lookup_current - look up a variable only in the current scope + # ============================================================ + def lookup_current(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: + """Look up a variable in the current scope only""" + if symtab is None or name is None: return None + scope: GenTypes.Scope* = symtab.Current + if scope is None: return None + return self.find_var(scope.Vars, scope.VarCount, name) + + # ============================================================ + # M3 owned slife 登记 API(§4.2 函数级 scope guard / §4.3 不许手动 free) + # ============================================================ + def _SlifeReset(self, pool: memhub.MemBuddy*) -> int: + """函数入口:保存外层区间两个边界、把本层起点抬到当前栈顶;返回令牌(-1=不可用,须原样交还 _SlifeRestore)""" + global _g_slife_owned, _g_slife_save, _g_slife_count, _g_slife_top, _g_slife_depth + if _g_slife_owned is None: + # 表体跨文件存活:必须用 stdlib.malloc,**不能**用 pool(trans.Pool 是每文件池, + # Phase2 每文件结束 reset_to 后表体即悬垂,后续写入会砸活跃数据)。 + _g_slife_owned = sizet*(stdlib.malloc(_SLIFE_MAX * 16)) + if _g_slife_owned is None: return -1 + if _g_slife_save is None: + _g_slife_save = sizet*(stdlib.malloc(_SLIFE_DEPTH_MAX * 16)) + if _g_slife_save is None: return -1 + if _g_slife_depth >= _SLIFE_DEPTH_MAX: + self.Gen.TypeGenErr.fatal_error(None, "slife ownership save stack full (function nesting > 64)", "LifetimeError") + return -1 + _g_slife_save[_g_slife_depth * 2] = sizet(_g_slife_count) + _g_slife_save[_g_slife_depth * 2 + 1] = sizet(_g_slife_top) + tok: int = _g_slife_depth + _g_slife_depth = _g_slife_depth + 1 + _g_slife_count = _g_slife_top + return tok + + def _SlifeRestore(self, tok: int) -> int: + """函数出口:**两个边界一起弹回**进入前的值(不释放;释放由 _SlifeEmitFrees 先做)。 + + 关键:必须把 _g_slife_top 还原成进入前的值。只还原 count 会让 top 随函数数量单调增长, + 上一文件/上一函数的陈旧条目就一直留在 [_g_slife_count, _g_slife_top) 被扫描—— + 而每文件池 reset_to 会复用同样的地址,于是"陈旧条目"能与新 alloca 地址相同, + _SlifeShadow 返回悬垂的影子槽,_SlifeNoteStore 直接写坏内存。""" + global _g_slife_count, _g_slife_top, _g_slife_depth + if tok < 0 or _g_slife_save is None: return 0 + if tok >= _g_slife_depth: return 0 + _g_slife_count = int(_g_slife_save[tok * 2]) + _g_slife_top = int(_g_slife_save[tok * 2 + 1]) + _g_slife_depth = tok + return 1 + + def _SlifeShadow(self, alloca: mlirlite.Value*) -> mlirlite.Value*: + """查已登记槽的所有权影子标志槽(未登记 → None)""" + if alloca is None or _g_slife_owned is None: return None + i: int = 0 + while i < _g_slife_top: + if uint64(_g_slife_owned[i * 2]) == uint64(alloca): + if _g_slife_owned[i * 2 + 1] == 0: return None + return mlirlite.Value*(_g_slife_owned[i * 2 + 1]) + i += 1 + return None + + def _SlifeOwned(self, alloca: mlirlite.Value*) -> int: + """该槽是否登记为 slife 绑定(= 由编译器负责释放,§4.3 判据5);1=是 / 0=否""" + if self._SlifeShadow(alloca) is not None: return 1 + return 0 + + def _SlifeEnsureOwned(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, alloca: mlirlite.Value*) -> mlirlite.Value*: + """登记该槽(首次:在入口块建 i8 影子并置 0),返回影子标志槽;不可用 → None""" + global _g_slife_top + if alloca is None or _g_slife_owned is None: return None + exist: mlirlite.Value* = self._SlifeShadow(alloca) + if exist is not None: return exist if _g_slife_top >= _SLIFE_MAX: # P5:超限 **fail-fast**(容量见文件头的 _SLIFE_MAX,此处文案里的数字与之同步)。 # 静默返回 None 会让该对象永不登记 → 永不释放(纯泄漏),与"编译器自动管理"的承诺矛盾; # 下面的 return None 只是流程完整(fatal_error 已终止进程)。 self.Gen.TypeGenErr.fatal_error(None, "slife ownership table full (capacity 256): too many live compiler-owned slife objects in one function", "LifetimeError") - return None - if pool is None or builder is None: return None - sh: mlirlite.Value* = self._alloca_at_entry(builder, mlirlite.Int8(pool)) - if sh is None: return None - zero8: mlirlite.Value* = mlirlite.const_int(pool, 8, 0) - if zero8 is None: return None - # 入口块置 0(显式传第 4 参,不依赖 include 默认值补全):赋值语句可能在运行期没执行, - # 那种路径上影子必须保持"不拥有",否则会 free 栈垃圾。 - mlirlite.build_store_at_entry(builder, zero8, sh, 0) - _g_slife_owned[_g_slife_top * 2] = sizet(alloca) - _g_slife_owned[_g_slife_top * 2 + 1] = sizet(sh) - _g_slife_top += 1 - return sh - - def _SlifeNoteStore(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, sh: mlirlite.Value*, owned: int) -> int: - """把影子标志写成 owned(1=该槽此刻持有编译器分配的对象 / 0=不持有)""" - if pool is None or builder is None or sh is None: return 0 - v: mlirlite.Value* = mlirlite.const_int(pool, 8, owned) - if v is None: return 0 - mlirlite.build_store(builder, v, sh) - return 1 - - def _SlifeEmitDtorBranch(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, alloca: mlirlite.Value*, sh: mlirlite.Value*) -> int: - """§3.6 析构调度:拥有该对象时先调 `__del__`(对齐 TPC MemoryOps._emit_local_heap_frees:先析构、再 free)。 - - 用**条件分支**而不是 select:未拥有的路径上必须完全不进析构体(析构体里会解引用 self)。 - 类(含 MRO / 跨模块 stub)没定义 `__del__` 时**不加任何分支**,直接返回——常见路径零开销。 - 析构名的选择:与 TPC 的**释放**路径一致用 `__del__`(`del obj` 那条语句才是 `__delete__`)。""" - if pool is None or builder is None or mod is None or trans is None: return 0 - if alloca is None or sh is None: return 0 - if alloca.Ty is None or alloca.Ty.Pointee == 0: return 0 - elem: mlirlite.LLVMType* = alloca.Ty.Pointee # 变量类型(T*) - if elem is None or self.Gen.ExprGenBase.is_ptr_type(elem) == 0: return 0 - struct_ty: mlirlite.LLVMType* = elem.Pointee # T - if struct_ty is None or struct_ty.kind() != mlirlite.LLVMType.Struct: return 0 - cls_nm: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty) - if cls_nm is None: return 0 - if self.Gen.ExprGenOps._resolve_dunder_by_class(pool, mod, cls_nm, "__del__", trans, struct_ty) is None: return 0 - func: mlirlite.Function* = builder.Func - if func is None: return 0 - zero8: mlirlite.Value* = mlirlite.const_int(pool, 8, 0) - if zero8 is None: return 0 - shv: mlirlite.Value* = mlirlite.build_load(builder, mlirlite.Int8(pool), sh) - if shv is None: return 0 - cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, shv, zero8) - if cond is None: return 0 - cur: mlirlite.Value* = mlirlite.build_load(builder, alloca.Ty.Pointee, alloca) - if cur is None: return 0 - # 唯一标签:用 trans._label_counter(与 translate_if 同源,避免同函数内重名标签) - cnt: int = trans._label_counter - trans._label_counter = cnt + 1 - name_buf: str = pool.alloc(32) - if name_buf is None: return 0 - viperlib.snprintf(name_buf, 32, "slife.dtor.%d", cnt) - dtor_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) - viperlib.snprintf(name_buf, 32, "slife.dtor.end.%d", cnt) - merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) - if dtor_bb is None or merge_bb is None: return 0 - mlirlite.build_cond_br(builder, cond, dtor_bb, merge_bb) - mlirlite.position_at_end(builder, dtor_bb) - # `%cur` 定义在分支块之前,支配 dtor_bb 与 merge_bb 两边 ✓ - self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, cur, "__del__", trans, None, 0) - mlirlite.build_br(builder, merge_bb) - mlirlite.position_at_end(builder, merge_bb) - return 1 - - def _SlifeEmitGuardedFree(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, alloca: mlirlite.Value*, sh: mlirlite.Value*) -> int: - """释放该槽**确实拥有**的对象:先按 §3.6 调度析构 `__del__`(若类定义了),再 `free`。 - - 释放本体用 icmp + select 而不是分支块:释放点可能插在任意位置(每个 return、覆盖赋值处), - select 无分支、无合流,安全得多;sh=0 时取 null,等价 free(NULL),槽里的外来指针/未初始化 - 值绝不参与释放。析构那一步另有条件分支(见 _SlifeEmitDtorBranch)。""" - if pool is None or builder is None or alloca is None or sh is None: return 0 - if alloca.Ty is None or alloca.Ty.Pointee == 0: return 0 - # §3.6:先析构,再 free - self._SlifeEmitDtorBranch(pool, builder, mod, trans, alloca, sh) - zero8: mlirlite.Value* = mlirlite.const_int(pool, 8, 0) - if zero8 is None: return 0 - shv: mlirlite.Value* = mlirlite.build_load(builder, mlirlite.Int8(pool), sh) - if shv is None: return 0 - cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, shv, zero8) - if cond is None: return 0 - cur: mlirlite.Value* = mlirlite.build_load(builder, alloca.Ty.Pointee, alloca) - if cur is None: return 0 - # sh=0 时 select 取 null,槽里的值(可能是未初始化的垃圾或外来指针)**不会**被释放 - nulp: mlirlite.Value* = mlirlite.ConstNull(pool, alloca.Ty.Pointee, "null") - if nulp is None: return 0 - sel: mlirlite.Value* = mlirlite.build_select(builder, cond, cur, nulp) - if sel is None: return 0 - i8p: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - c: mlirlite.Value* = sel - if sel.Ty is not None and uint64(sel.Ty) != uint64(i8p): - c = mlirlite.build_bitcast(builder, sel, i8p) - if c is None: return 0 - if mod is not None: - # `free` 由模块前言(Translator.vp,与 malloc 同处)统一声明,这里只查表 + fail-fast: - # 绝不吐出指向**未声明符号**的 llvm.call——mlir-opt 只会报 - # "does not reference a symbol in the current scope",离根因很远(现场见 Translator.vp 的 free 注释)。 - if self.Gen.CallGenBase.find_func_in_module(mod, "free") is None: - self.Gen.TypeGenErr.fatal_error(None, "cannot release a `slife` object: the module has no declaration of the C library function 'free'", "TypeError") - r: mlirlite.Value* = mlirlite.build_call(builder, "free", c, 1, mlirlite.Void(pool), 0) - if r is None: return 0 - return 1 - + return None + if pool is None or builder is None: return None + sh: mlirlite.Value* = self._alloca_at_entry(builder, mlirlite.Int8(pool)) + if sh is None: return None + zero8: mlirlite.Value* = mlirlite.const_int(pool, 8, 0) + if zero8 is None: return None + # 入口块置 0(显式传第 4 参,不依赖 include 默认值补全):赋值语句可能在运行期没执行, + # 那种路径上影子必须保持"不拥有",否则会 free 栈垃圾。 + mlirlite.build_store_at_entry(builder, zero8, sh, 0) + _g_slife_owned[_g_slife_top * 2] = sizet(alloca) + _g_slife_owned[_g_slife_top * 2 + 1] = sizet(sh) + _g_slife_top += 1 + return sh + + def _SlifeNoteStore(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, sh: mlirlite.Value*, owned: int) -> int: + """把影子标志写成 owned(1=该槽此刻持有编译器分配的对象 / 0=不持有)""" + if pool is None or builder is None or sh is None: return 0 + v: mlirlite.Value* = mlirlite.const_int(pool, 8, owned) + if v is None: return 0 + mlirlite.build_store(builder, v, sh) + return 1 + + def _SlifeEmitDtorBranch(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, alloca: mlirlite.Value*, sh: mlirlite.Value*) -> int: + """§3.6 析构调度:拥有该对象时先调 `__del__`(对齐 TPC MemoryOps._emit_local_heap_frees:先析构、再 free)。 + + 用**条件分支**而不是 select:未拥有的路径上必须完全不进析构体(析构体里会解引用 self)。 + 类(含 MRO / 跨模块 stub)没定义 `__del__` 时**不加任何分支**,直接返回——常见路径零开销。 + 析构名的选择:与 TPC 的**释放**路径一致用 `__del__`(`del obj` 那条语句才是 `__delete__`)。""" + if pool is None or builder is None or mod is None or trans is None: return 0 + if alloca is None or sh is None: return 0 + if alloca.Ty is None or alloca.Ty.Pointee == 0: return 0 + elem: mlirlite.LLVMType* = alloca.Ty.Pointee # 变量类型(T*) + if elem is None or self.Gen.ExprGenBase.is_ptr_type(elem) == 0: return 0 + struct_ty: mlirlite.LLVMType* = elem.Pointee # T + if struct_ty is None or struct_ty.kind() != mlirlite.LLVMType.Struct: return 0 + cls_nm: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty) + if cls_nm is None: return 0 + if self.Gen.ExprGenOps._resolve_dunder_by_class(pool, mod, cls_nm, "__del__", trans, struct_ty) is None: return 0 + func: mlirlite.Function* = builder.Func + if func is None: return 0 + zero8: mlirlite.Value* = mlirlite.const_int(pool, 8, 0) + if zero8 is None: return 0 + shv: mlirlite.Value* = mlirlite.build_load(builder, mlirlite.Int8(pool), sh) + if shv is None: return 0 + cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, shv, zero8) + if cond is None: return 0 + cur: mlirlite.Value* = mlirlite.build_load(builder, alloca.Ty.Pointee, alloca) + if cur is None: return 0 + # 唯一标签:用 trans._label_counter(与 translate_if 同源,避免同函数内重名标签) + cnt: int = trans._label_counter + trans._label_counter = cnt + 1 + name_buf: str = pool.alloc(32) + if name_buf is None: return 0 + viperlib.snprintf(name_buf, 32, "slife.dtor.%d", cnt) + dtor_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) + viperlib.snprintf(name_buf, 32, "slife.dtor.end.%d", cnt) + merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf) + if dtor_bb is None or merge_bb is None: return 0 + mlirlite.build_cond_br(builder, cond, dtor_bb, merge_bb) + mlirlite.position_at_end(builder, dtor_bb) + # `%cur` 定义在分支块之前,支配 dtor_bb 与 merge_bb 两边 ✓ + self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, cur, "__del__", trans, None, 0) + mlirlite.build_br(builder, merge_bb) + mlirlite.position_at_end(builder, merge_bb) + return 1 + + def _SlifeEmitGuardedFree(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, alloca: mlirlite.Value*, sh: mlirlite.Value*) -> int: + """释放该槽**确实拥有**的对象:先按 §3.6 调度析构 `__del__`(若类定义了),再 `free`。 + + 释放本体用 icmp + select 而不是分支块:释放点可能插在任意位置(每个 return、覆盖赋值处), + select 无分支、无合流,安全得多;sh=0 时取 null,等价 free(NULL),槽里的外来指针/未初始化 + 值绝不参与释放。析构那一步另有条件分支(见 _SlifeEmitDtorBranch)。""" + if pool is None or builder is None or alloca is None or sh is None: return 0 + if alloca.Ty is None or alloca.Ty.Pointee == 0: return 0 + # §3.6:先析构,再 free + self._SlifeEmitDtorBranch(pool, builder, mod, trans, alloca, sh) + zero8: mlirlite.Value* = mlirlite.const_int(pool, 8, 0) + if zero8 is None: return 0 + shv: mlirlite.Value* = mlirlite.build_load(builder, mlirlite.Int8(pool), sh) + if shv is None: return 0 + cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, shv, zero8) + if cond is None: return 0 + cur: mlirlite.Value* = mlirlite.build_load(builder, alloca.Ty.Pointee, alloca) + if cur is None: return 0 + # sh=0 时 select 取 null,槽里的值(可能是未初始化的垃圾或外来指针)**不会**被释放 + nulp: mlirlite.Value* = mlirlite.ConstNull(pool, alloca.Ty.Pointee, "null") + if nulp is None: return 0 + sel: mlirlite.Value* = mlirlite.build_select(builder, cond, cur, nulp) + if sel is None: return 0 + i8p: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + c: mlirlite.Value* = sel + if sel.Ty is not None and uint64(sel.Ty) != uint64(i8p): + c = mlirlite.build_bitcast(builder, sel, i8p) + if c is None: return 0 + if mod is not None: + # `free` 由模块前言(Translator.vp,与 malloc 同处)统一声明,这里只查表 + fail-fast: + # 绝不吐出指向**未声明符号**的 llvm.call——mlir-opt 只会报 + # "does not reference a symbol in the current scope",离根因很远(现场见 Translator.vp 的 free 注释)。 + if self.Gen.CallGenBase.find_func_in_module(mod, "free") is None: + self.Gen.TypeGenErr.fatal_error(None, "cannot release a `slife` object: the module has no declaration of the C library function 'free'", "TypeError") + r: mlirlite.Value* = mlirlite.build_call(builder, "free", c, 1, mlirlite.Void(pool), 0) + if r is None: return 0 + return 1 + def _SlifeDropOwnedUnlessSame(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*, src_slot: mlirlite.Value*) -> int: """P4:覆盖释放 target 的当前对象;若它与 src 的当前值相同(自别名 `q = p; q = p`)则整体跳过。 @@ -764,790 +764,790 @@ novirtual class VarGenMixin: return 0 def _SlifeSlotFirstSeen(self, idx: int, slot: uint64) -> int: - """槽 slot 在 (idx, _g_slife_top) 里是否已经出现过;1=本次首次出现(该释放)/ 0=重复(跳过)""" - j: int = int(idx + 1) - while j < _g_slife_top: - if uint64(_g_slife_owned[j * 2]) == slot: return 0 - j += 1 - return 1 - - def _SlifeEmitFrees(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*) -> int: - """函数出口(所有 return / 隐式收尾):逆序释放**本函数**登记且仍拥有的 slife 对象。 - - 防御性去重:同一变量槽若在表里出现多次(登记路径异常 / 旧条目残留),只发一次 free—— - 对同一对象 free 两次是堆损坏(0xC0000374),绝不能出。""" - if pool is None or builder is None or _g_slife_owned is None: return 0 - i: int = _g_slife_top - 1 - while i >= _g_slife_count: - slot: uint64 = uint64(_g_slife_owned[i * 2]) - sh: mlirlite.Value* = mlirlite.Value*(_g_slife_owned[i * 2 + 1]) - if sh is not None and self._SlifeSlotFirstSeen(int(i), slot) != 0: - self._SlifeEmitGuardedFree(pool, builder, mod, trans, mlirlite.Value*(_g_slife_owned[i * 2]), sh) - i -= 1 - return 0 - - # lookup_module_var - look up a variable in the module scope - # ============================================================ - def lookup_module_var(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: - """Look up a variable in the module scope""" - if symtab is None or name is None: return None - scope: GenTypes.Scope* = symtab.Root - if scope is None: return None - return self.find_var(scope.Vars, scope.VarCount, name) - - # is_module_scope - whether the current scope is module level. - # Module-level AnnAssign modifies a module variable; inside a function (without global) it creates a local. - def is_module_scope(self, symtab: GenTypes.SymbolTable*) -> int: - """Whether the current scope is module level (Current == Root), 1=module / 0=function scope""" - if symtab is None: return 0 - if symtab.Root is None or symtab.Current is None: return 0 - # Compare pointers: module level when Current equals Root - if uint64(symtab.Current) == uint64(symtab.Root): return 1 - return 0 - - # _lookup_through_match_scope - look up a variable, walking through match-case scopes - def _lookup_through_match_scope(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: - """Walk up from the current scope through SCOPE_MATCH scopes only to find a variable alloca""" - if symtab is None or name is None: return None - scope: GenTypes.Scope* = symtab.Current - while scope is not None: - result: mlirlite.Value* = self.find_var(scope.Vars, scope.VarCount, name) - if result is not None: return result - if scope.Kind == SCOPE_MATCH: - scope = scope.Parent - continue - return None - return None - - # get_or_create_sym - find or create a variable (in the current scope) - # ============================================================ - def get_or_create_sym(self, symtab: GenTypes.SymbolTable*, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, name: str, ty: mlirlite.LLVMType*) -> mlirlite.Value*: - """Find or create a variable alloca in the current scope""" - existing: mlirlite.Value* = self.lookup_current(symtab, name) - if existing is not None: return existing - # The SCOPE_MATCH isolation scope must be transparent to assignment: - existing = self._lookup_through_match_scope(symtab, name) - if existing is not None: return existing - alloca: mlirlite.Value* = self._alloca_at_entry(builder, ty) - if alloca is None: return None - if self.define_var(symtab, name, alloca) != 0: self.Gen.TypeGenErr.fatal_error(None, f"failed to register variable '{name}' (scope variable count exceeds the limit MAX_VARS)", "InternalCompilerError") - return alloca - - # REPL mode (M2) - session variable slot read/write; entered only when trans.ReplCtx != None (slots hold the value at ctx + x.Offset). - # ---- repl_find_entry - look up a session table entry by name ---- - def repl_find_entry(self, ctx: GenTypes.ReplCtx*, name: str) -> GenTypes.ReplVarEntry*: - """Look up an entry by name in the session table; None if ctx is invalid or unregistered""" - if ctx is None or name is None: return None - # Magic check: avoid treating a non-ReplCtx pointer as a context (better not to intercept than to over-intercept) - if ctx.Marker != GenTypes.REPL_CTX_MARKER: return None - if ctx.Table is None or ctx.Count <= 0: return None - entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__() - for i in range(int(ctx.Count)): - entry_addr: uint64 = uint64(ctx.Table) + i * entry_size - entry: GenTypes.ReplVarEntry* = GenTypes.ReplVarEntry*(entry_addr) - if entry.Name is not None and string.strcmp(entry.Name, name) == 0: return entry - return None - - # ---- repl_in_frag_scope - whether the nearest function scope is the fragment function itself ---- - def repl_in_frag_scope(self, symtab: GenTypes.SymbolTable*) -> int: - """1=the nearest function scope is the fragment body itself (its parent is the module root); 0=otherwise.""" - if symtab is None or symtab.Current is None or symtab.Root is None: return 0 - scope: GenTypes.Scope* = symtab.Current - while scope is not None: - if scope.Kind == SCOPE_FUNCTION: - if scope.Parent is not None and uint64(scope.Parent) == uint64(symtab.Root): return 1 - return 0 - scope = scope.Parent - return 0 - - # ---- repl_ctx_ptr - load the fragment function's ctx parameter value (i8*) ---- - def repl_ctx_ptr(self, trans: HT.Translator*, ctx: GenTypes.ReplCtx*, node: ast.AST*) -> mlirlite.Value*: - """Load the snippet function's ctx parameter (alloca built by FuncGenDef); fail fast on error""" - if trans is None or ctx is None or ctx.CtxParam is None: return None - symtab: GenTypes.SymbolTable* = trans.SymTab - param_alloca: mlirlite.Value* = self.lookup_var(symtab, ctx.CtxParam) - if param_alloca is None or param_alloca.Ty is None or param_alloca.Ty.Pointee == 0: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: snippet function is missing the ctx parameter '{ctx.CtxParam}' (wrapper template and M2 calling convention disagree)", "InternalCompilerError") - return None - return mlirlite.build_load(trans._cur_builder, param_alloca.Ty.Pointee, param_alloca) - - # ---- repl_in_frag_entry_scope - whether the nearest function scope is the fragment entry function ---- - # Differs from repl_in_frag_scope (M3): promoted session functions also parent to module root; write paths use this. - def repl_in_frag_entry_scope(self, trans: HT.Translator*) -> int: - """1=currently translating a snippet entry function body; 0=otherwise (including promoted session bodies)""" - if trans is None or trans._cur_func is None: return 0 - if self.repl_in_frag_scope(trans.SymTab) == 0: return 0 - cur_nm: str = mlirlite.function_get_name(trans._cur_func) - if cur_nm is None: return 0 - pfx: str = GenTypes.REPL_ENTRY_PREFIX - pl: sizet = string.strlen(pfx) - if string.strncmp(cur_nm, pfx, pl) != 0: return 0 - return 1 - - # ---- repl_find_func_entry - look up a session function table entry by name (M3) ---- - def repl_find_func_entry(self, ctx: GenTypes.ReplCtx*, name: str) -> GenTypes.ReplFuncEntry*: - """Look up by name in the session function table; None if ctx is invalid or unregistered (read-only)""" - if ctx is None or name is None: return None - if ctx.Marker != GenTypes.REPL_CTX_MARKER: return None - if ctx.FuncTable is None or ctx.FuncCount <= 0: return None - entry_size: sizet = GenTypes.ReplFuncEntry.__sizeof__() - for i in range(int(ctx.FuncCount)): - entry_addr: uint64 = uint64(ctx.FuncTable) + i * entry_size - entry: GenTypes.ReplFuncEntry* = GenTypes.ReplFuncEntry*(entry_addr) - if entry.Name is not None and string.strcmp(entry.Name, name) == 0: return entry - return None - - # ---- repl_func_ty - signature token -> LLVM type (M3) ---- - # Tokens come from the host parsing the define line; only iN/float/double/i8* are supported (others -> error). - def repl_func_ty(self, pool: memhub.MemBuddy*, tag: int, bits: int) -> mlirlite.LLVMType*: - """Map a signature token to an LLVM type; None for unsupported tokens""" - if pool is None: return None - if tag == GenTypes.REPL_TY_INT: return self.repl_scalar_ty(pool, bits, 0) - if tag == GenTypes.REPL_TY_FLOAT: return self.repl_scalar_ty(pool, bits, 1) - if tag == GenTypes.REPL_TY_STR: return mlirlite.Ptr(pool, mlirlite.Int8(pool)) - return None - - # ---- repl_func_sig_label - signature token -> human-readable type name (for error messages) ---- - def repl_func_sig_label(self, tag: int, bits: int) -> str: - """Map a signature token to a display name; "?" if unknown (VOID displays as void)""" - if tag == GenTypes.REPL_TY_VOID: return "void" - return self.repl_tag_label(tag, bits) - - # ---- repl_try_session_func_call - indirect call to a session function (M3 core) ---- - # On a hit, load the address from the ctx slot, prepend ctx, coerce args to the table signature, and call_indirect (hit=1). - def repl_try_session_func_call(self, trans: HT.Translator*, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, func_name: str, cargs: list[ast.AST*]*, can: int, kw_count: int, node: ast.AST*, hit: t.CPtr) -> mlirlite.Value*: - """Session function call: return the call result on hit (void returns None but hit is set)""" - if trans is None or trans.ReplCtx is None or pool is None or builder is None: return None - if func_name is None or hit is None: return None - ctx: GenTypes.ReplCtx* = trans.ReplCtx - entry: GenTypes.ReplFuncEntry* = self.repl_find_func_entry(ctx, func_name) - if entry is None: return None - # Same rule as M2's read path: the fragment entry function or a promoted session function may call - # other session functions; nested closures (no ctx param) are rejected, letting the existing path report. - if self.repl_in_frag_scope(trans.SymTab) == 0: return None - hit[0] = 1 - if kw_count != 0: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: session function '{func_name}' does not support keyword arguments (M3 supports positional arguments only)", "TypeError") - return None - if can != entry.ParamCount: - self.Gen.TypeGenErr.fatal_error( node, f"REPL: session function '{func_name}' expects {entry.ParamCount} positional arguments, but the call site passed {can} (M3 does not support default arguments or default completion)", "TypeError") - return None - ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, ctx, node) - if ctx_val is None: return None - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) - # (1) Load the function address from the slot: the slot is i8** (Ptr(i8*)); load yields i8* - slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, entry.SlotOff, i8_ptr_ty) - if slot is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to generate the session function pointer slot address", "InternalCompilerError") - return None - fn_i8: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty, slot) - if fn_i8 is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to read the address of session function '{func_name}'", "InternalCompilerError") - return None - # (2) Resolve param types before building the signature (token -> LLVM type; unsupported -> error) - ptypes: t.CPtr = pool.alloc(8 * GenTypes.REPL_FUNC_PARAM_MAX) - if ptypes is None: return None - string.memset(ptypes, 0, 8 * GenTypes.REPL_FUNC_PARAM_MAX) - ai_chk: int - for ai_chk in range(int(entry.ParamCount)): - pty_chk: mlirlite.LLVMType* = self.repl_func_ty(pool, int(entry.ParamTags[ai_chk]), int(entry.ParamBits[ai_chk])) - if pty_chk is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: type token of parameter {ai_chk + 1} of session function '{func_name}' is not supported (M3 supports iN/float/double/i8*)", "TypeError") - return None - pty_slot: str* = (str*)(uint64(ptypes) + ai_chk * 8) - pty_slot[0] = pty_chk - # (3) Args: the first is this fragment's ctx; the rest are translated and coerced to the signature types - head: mlirlite.Value* = ctx_val - head.Next = None - tail: mlirlite.Value* = head - arg_count: int = 1 - ai: int - for ai in range(int(entry.ParamCount)): - arg: ast.AST* = cargs.get(ai) - if arg is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: argument {ai + 1} of session function '{func_name}' is missing", "SyntaxError") - return None - pty_slot2: str* = (str*)(uint64(ptypes) + ai * 8) - pty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(pty_slot2[0]) - arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans, pty) - if arg_val is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to translate argument {ai + 1} of session function '{func_name}'", "TypeError") - return None - coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, pty, arg, trans) - if coerced is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: argument {ai + 1} of session function '{func_name}' is incompatible with the signature type", "TypeError") - return None - coerced.Next = None - tail.Next = coerced - tail = coerced - arg_count += 1 - # (4) Function type: ret + (i8*, P1..Pn) (void uses mlirlite.Void; build_call_indirect returns None, - # so the host caller treats it as a void call). - ret_ty: mlirlite.LLVMType* = mlirlite.Void(pool) - if entry.RetTag != GenTypes.REPL_TY_VOID: - ret_ty = self.repl_func_ty(pool, entry.RetTag, entry.RetBits) - if ret_ty is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: return type token of session function '{func_name}' is not supported (M3 supports void/iN/float/double/i8*)", "TypeError") - return None - # Param list nodes are mlirlite.ParamNode (new_param_node returns rawptr[ParamNode], and - # paramnode_set_next / Func params are rawptr[ParamNode] too) - previously mistyped as Function*. - fp_first: mlirlite.ParamNode* = None - fp_prev: mlirlite.ParamNode* = None - pi: int - for pi in range(int(arg_count)): - cur_ty: mlirlite.LLVMType* = i8_ptr_ty - if pi > 0: - pty_slot3: str* = (str*)(uint64(ptypes) + (pi - 1) * 8) - cur_ty = (mlirlite.LLVMType*)(pty_slot3[0]) - pn: mlirlite.ParamNode* = mlirlite.new_param_node(pool, cur_ty) - if pn is not None: - if fp_first is None: fp_first = pn - if fp_prev is not None: mlirlite.paramnode_set_next(fp_prev, pn) - fp_prev = pn - func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, ret_ty, fp_first, arg_count) - if func_ty is None: return None - func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, func_ty) - fn_typed: mlirlite.Value* = mlirlite.build_bitcast(builder, fn_i8, func_ptr_ty) - if fn_typed is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to convert the address of session function '{func_name}'", "InternalCompilerError") - return None - return mlirlite.build_call_indirect(builder, fn_typed, head, arg_count, ret_ty) - - # ---- repl_slot_ptr - slot address (byte GEP from ctx + off, then bitcast to Ptr(val_ty)) ---- - def repl_slot_ptr(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, ctx_val: mlirlite.Value*, off: int, val_ty: mlirlite.LLVMType*) -> mlirlite.Value*: - """Return the slot pointer (Ptr(val_ty)) via a byte-offset i8 GEP then bitcast; None if args incomplete.""" - if pool is None or builder is None or ctx_val is None or val_ty is None: return None - i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) - target_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, val_ty) - off_val: mlirlite.Value* = mlirlite.const_int64(pool, int64(off)) - if off_val is None: return None - base: mlirlite.Value* = mlirlite.build_gep(builder, i8_ty, ctx_val, off_val) - if base is None: return None - # Skip bitcast when types are equal: mlirlite types are not canonical, and LLVM rejects - # `bitcast i8* %x to i8*`. When val_ty is i8 the target is exactly i8*, so return the GEP result. - if base.Ty is not None and self.Gen.AssignGen._stg_ty_equal(pool, base.Ty, target_ptr_ty) != 0: return base - return mlirlite.build_bitcast(builder, base, target_ptr_ty) - - # ---- repl_scalar_ty - bit width -> LLVM scalar type ---- - def repl_scalar_ty(self, pool: memhub.MemBuddy*, bits: int, is_float: int) -> mlirlite.LLVMType*: - """Restore an LLVM scalar type from a bit width; None for unsupported widths""" - if is_float != 0: - if bits == 32: return mlirlite.Float(pool) - if bits == 64: return mlirlite.Double(pool) - return None - if bits == 1: return mlirlite.Int1(pool) - if bits == 8: return mlirlite.Int8(pool) - if bits == 16: return mlirlite.Int16(pool) - if bits == 32: return mlirlite.Int32(pool) - if bits == 64: return mlirlite.Int64(pool) - return None - - # ---- repl_llvm_ty - entry -> LLVM value type (structs re-resolved by name) ---- - def repl_llvm_ty(self, pool: memhub.MemBuddy*, entry: GenTypes.ReplVarEntry*) -> mlirlite.LLVMType*: - """Infer the entry's LLVM value type; None when untyped or a struct type cannot be re-resolved in this fragment.""" - if entry is None or pool is None: return None - if entry.Kind == GenTypes.REPL_KIND_UNTYPED: return None - if entry.TyTag == GenTypes.REPL_TY_INT: return self.repl_scalar_ty(pool, entry.Bits, 0) - if entry.TyTag == GenTypes.REPL_TY_FLOAT: return self.repl_scalar_ty(pool, entry.Bits, 1) - if entry.TyTag == GenTypes.REPL_TY_STR: return mlirlite.Ptr(pool, mlirlite.Int8(pool)) - if entry.TyTag == GenTypes.REPL_TY_PTR: - if entry.TyName is not None: - st: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type(entry.TyName) - if st is None: return None - return mlirlite.Ptr(pool, st) - ptee: mlirlite.LLVMType* = self.repl_scalar_ty(pool, entry.Bits, 0) - if ptee is None: ptee = mlirlite.Int8(pool) - return mlirlite.Ptr(pool, ptee) - if entry.TyTag == GenTypes.REPL_TY_STRUCT: - if entry.TyName is None: return None - return self.Gen.StructGen.get_struct_type(entry.TyName) - return None - - # ---- repl_slot_ty - the value type stored in the slot ---- - def repl_slot_ty(self, pool: memhub.MemBuddy*, entry: GenTypes.ReplVarEntry*) -> mlirlite.LLVMType*: - """The value type stored in the slot equals the entry's value type (scalars/pointers/structs inline); None if it overflows the slot.""" - if entry is None or pool is None: return None - vt: mlirlite.LLVMType* = self.repl_llvm_ty(pool, entry) - if vt is None: return None - if entry.TyTag == GenTypes.REPL_TY_STRUCT: - sz: int = self.repl_type_size(vt) - al: int = self.repl_type_align(vt) - if sz <= 0 or al <= 0 or sz > GenTypes.REPL_SLOT_SIZE or al > GenTypes.REPL_SLOT_SIZE: return None - return vt - - # ---- repl_type_align / repl_type_size - struct value size/alignment (LLVM standard layout) ---- - def repl_type_align(self, ty: mlirlite.LLVMType*, depth: int = 0) -> int: - """Natural alignment bytes of a type; 0 if unknown (deep guards recursion depth)""" - if ty is None or depth > 64: return 0 - ib: int = self.Gen.ExprGenBase.get_llvm_type_bits(ty) - if ib != 0: - if ib <= 8: return 1 - if ib <= 16: return 2 - if ib <= 32: return 4 - return 8 - fb: int = self.Gen.ExprGenBase.get_llvm_float_bits(ty) - if fb != 0: - if fb <= 16: return 2 - if fb <= 32: return 4 - return 8 - if self.Gen.ExprGenBase.is_ptr_type(ty) != 0: return 8 - elem_ty: mlirlite.LLVMType* = None - match ty: - case mlirlite.LLVMType.Array(elem_a, _cnt_a): - elem_ty = elem_a - case _: - pass - if elem_ty is not None: return self.repl_type_align(elem_ty, depth + 1) - fields: mlirlite.ParamNode* = mlirlite.get_struct_fields(ty) - if fields is None: return 0 - cur: mlirlite.ParamNode* = fields - best: int = 1 - while cur is not None: - fty: mlirlite.LLVMType* = mlirlite.paramnode_get_ty(cur) - fa: int = self.repl_type_align(fty, depth + 1) - if fa <= 0: return 0 - if fa > best: best = fa - cur = mlirlite.paramnode_get_next(cur) - return best - - def repl_type_size(self, ty: mlirlite.LLVMType*, depth: int = 0) -> int: - """Type size in bytes under LLVM standard layout (fields aligned with tail padding); 0 if unknown.""" - if ty is None or depth > 64: return 0 - ib: int = self.Gen.ExprGenBase.get_llvm_type_bits(ty) - if ib != 0: return (ib + 7) // 8 - fb: int = self.Gen.ExprGenBase.get_llvm_float_bits(ty) - if fb != 0: return fb // 8 - if self.Gen.ExprGenBase.is_ptr_type(ty) != 0: return 8 - elem_ty: mlirlite.LLVMType* = None - cnt: int = 0 - match ty: - case mlirlite.LLVMType.Array(elem_s, cnt_s): - elem_ty = elem_s - cnt = cnt_s - case _: - pass - if elem_ty is not None: - es: int = self.repl_type_size(elem_ty, depth + 1) - if es <= 0 or cnt <= 0: return 0 - return es * cnt - fields: mlirlite.ParamNode* = mlirlite.get_struct_fields(ty) - if fields is None: return 0 - align_struct: int = self.repl_type_align(ty, depth + 1) - if align_struct <= 0: return 0 - cur: mlirlite.ParamNode* = fields - off: int = 0 - counted: int = 0 - while cur is not None: - fty2: mlirlite.LLVMType* = mlirlite.paramnode_get_ty(cur) - fsz: int = self.repl_type_size(fty2, depth + 1) - fa2: int = self.repl_type_align(fty2, depth + 1) - if fsz <= 0 or fa2 <= 0: return 0 - rem: int = off % fa2 - if rem != 0: off += fa2 - rem - off += fsz - counted += 1 - cur = mlirlite.paramnode_get_next(cur) - if counted <= 0: return 0 - rem2: int = off % align_struct - if rem2 != 0: off += align_struct - rem2 - return off - - # ---- repl_seal - first-time typing (if already typed, verify the type tag matches) ---- - def repl_seal(self, trans: HT.Translator*, entry: GenTypes.ReplVarEntry*, val_ty: mlirlite.LLVMType*, node: ast.AST*) -> int: - """Type the entry by val_ty; return 0=success (idempotent hits included) / 1=unsupported or conflicting (fail-fast already raised).""" - if trans is None or entry is None or val_ty is None: return 1 - perm: memhub.MemBuddy* = self.repl_perm_pool(trans) - if perm is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: session permanent pool is unavailable (not injected by the host)", "InternalCompilerError") - return 1 - tag: int = 0 - bits: int = 0 - kind: int = GenTypes.REPL_KIND_SCALAR - tname: str = None - ib: int = self.Gen.ExprGenBase.get_llvm_type_bits(val_ty) - fb: int = self.Gen.ExprGenBase.get_llvm_float_bits(val_ty) - if ib != 0: - tag = GenTypes.REPL_TY_INT - bits = ib - elif fb != 0: - tag = GenTypes.REPL_TY_FLOAT - bits = fb - elif self.Gen.ExprGenBase.is_ptr_type(val_ty) != 0: - ptee: mlirlite.LLVMType* = val_ty.Pointee - pname: str = None - if ptee is not None: pname = mlirlite.get_struct_name(ptee) - if pname is not None: - tag = GenTypes.REPL_TY_PTR - bits = 64 - tname = pname - else: - pb: int = 0 - pf: int = 0 - if ptee is not None: - pb = self.Gen.ExprGenBase.get_llvm_type_bits(ptee) - pf = self.Gen.ExprGenBase.get_llvm_float_bits(ptee) - if pb == 8: - tag = GenTypes.REPL_TY_STR - bits = 8 - else: - tag = GenTypes.REPL_TY_PTR - if pb != 0: bits = pb - elif pf != 0: bits = pf - else: bits = 0 - else: - sname: str = mlirlite.get_struct_name(val_ty) - if sname is not None: - # Struct value: inlined in the slot by declared type (no boxing). The slot capacity/alignment - # limit is REPL_SLOT_SIZE (16 bytes); exceeding it is a hard error, never silently truncated. - sz: int = self.repl_type_size(val_ty) - al: int = self.repl_type_align(val_ty) - if sz <= 0 or al <= 0: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: size/alignment of the struct value '{sname}' of session variable '{entry.Name}' is unknown" f" (cannot determine whether it fits in a {GenTypes.REPL_SLOT_SIZE} byte slot)", "TypeError") - return 1 - if sz > GenTypes.REPL_SLOT_SIZE or al > GenTypes.REPL_SLOT_SIZE: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: struct value '{sname}' of session variable '{entry.Name}' is {sz} bytes and " f"aligned to {al} bytes, so it cannot fit in a {GenTypes.REPL_SLOT_SIZE} byte slot" f" (M2 struct values must be inlined into the slot; use an object pointer or heap allocation for large objects)", "TypeError") - return 1 - tag = GenTypes.REPL_TY_STRUCT - bits = sz * 8 - kind = GenTypes.REPL_KIND_SCALAR - tname = sname - else: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: the type of session variable '{entry.Name}' is not supported yet" f" (M2 supports int/bool/float/str/bytes/pointer/struct value)", "TypeError") - return 1 - if entry.Kind == GenTypes.REPL_KIND_UNTYPED: - entry.Kind = kind - entry.TyTag = tag - entry.Bits = bits - entry.TyName = None - if tname is not None: entry.TyName = self.repl_perm_strdup(perm, tname) - return 0 - if entry.Kind != kind or entry.TyTag != tag: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{entry.Name}' is already typed as {self.repl_tag_label(entry.TyTag, entry.Bits)} and " f"cannot be assigned {self.repl_tag_label(tag, bits)} (restart the session to change its type)", "TypeError") - return 1 - if tname is not None and entry.TyName is None: entry.TyName = self.repl_perm_strdup(perm, tname) - return 0 - - # ---- repl_perm_strdup - deep copy into PermPool (survives across fragments) ---- - def repl_perm_strdup(self, pool: memhub.MemBuddy*, s: str) -> str: - """Deep-copy s into pool (caller passes PermPool) for long-lived session storage; None on failure""" - if s is None or pool is None: return None - n: sizet = string.strlen(s) - buf: str = pool.alloc(n + 1) - if buf is None: return None - string.strcpy(buf, s) - return buf - - # ---- repl_tag_label - type tag -> display name (same convention as the host echo) ---- - def repl_tag_label(self, tag: int, bits: int) -> str: - """Map TyTag/Bits to a human-readable type name (for error messages only)""" - 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" - if tag == GenTypes.REPL_TY_PTR: return "ptr" - if tag == GenTypes.REPL_TY_STRUCT: return "struct" - return "?" - - # ---- repl_ensure_entry - allocate a slot when an untyped session variable name is first seen ---- - # Allocation happens during fragment translation on a local-table miss; a full table fails fast (no silent drop). - def repl_ensure_entry(self, trans: HT.Translator*, name: str, node: ast.AST*) -> GenTypes.ReplVarEntry*: - """Get an entry by name, allocating and registering a slot if absent; None on failure (fail-fast already raised).""" - if trans is None or name is None: return None - ctx: GenTypes.ReplCtx* = trans.ReplCtx - if ctx is None or ctx.Marker != GenTypes.REPL_CTX_MARKER: return None - hit: GenTypes.ReplVarEntry* = self.repl_find_entry(ctx, name) - if hit is not None: return hit - if ctx.Table is None or ctx.Cap <= 0: - self.Gen.TypeGenErr.fatal_error(node, "REPL: session variable table is not initialized (host did not create ctx)", "InternalCompilerError") - return None - if ctx.Count >= ctx.Cap: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variables have reached the limit of {ctx.Cap} (ctx slots exhausted); " f"cannot allocate another slot for '{name}'; restart the session", "ResourceError") - return None - perm: memhub.MemBuddy* = self.repl_perm_pool(trans) - if perm is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: session permanent pool is unavailable (not injected by the host)", "InternalCompilerError") - return None - name_copy: str = self.repl_perm_strdup(perm, name) - if name_copy is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to deep-copy the name of session variable '{name}'", "InternalCompilerError") - return None - entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__() - entry: GenTypes.ReplVarEntry* = GenTypes.ReplVarEntry*(uint64(ctx.Table) + uint64(ctx.Count) * uint64(entry_size)) - if entry is None: return None - entry.Name = name_copy - entry.TyName = None - entry.Offset = GenTypes.REPL_SLOT_BASE + ctx.Count * GenTypes.REPL_SLOT_SIZE - entry.Kind = GenTypes.REPL_KIND_UNTYPED - entry.TyTag = 0 - entry.Bits = 0 - ctx.Count = ctx.Count + 1 - return entry - - # ---- repl_perm_pool - the permanent pool for session data (survives across fragments) ---- - def repl_perm_pool(self, trans: HT.Translator*) -> memhub.MemBuddy*: - """The host-injected PermPool (ctx.Perm); None if there is no ctx or it was not injected""" - if trans is None or trans.ReplCtx is None: return None - return trans.ReplCtx.Perm - - # ---- repl_emit_result - write this fragment's result into the ctx result area ---- - def repl_emit_result(self, trans: HT.Translator*, ctx_val: mlirlite.Value*, val: mlirlite.Value*, tag: int, bits: int, node: ast.AST* = None) -> int: - """Write val into the ctx result area per tag (see GenTypes result_kind); 0=success. Host reads only (kind, result_a, result_b).""" - if trans is None or ctx_val is None or val is None: return 1 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - if pool is None or builder is None: return 1 - i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) - zero64: mlirlite.Value* = mlirlite.const_int64(pool, 0) - if tag == GenTypes.REPL_TY_INT: - iv: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, val, i64_ty, node, trans) - if iv is None: return 1 - if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, GenTypes.REPL_RES_INT) != 0: - return 1 - if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, iv) != 0: return 1 - return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) - if tag == GenTypes.REPL_TY_FLOAT: - dv: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, val, mlirlite.Double(pool), node, trans) - if dv is None: return 1 - # double -> i64 bit pattern (bitcast, no numeric conversion): the host reinterprets the bits back to double - dbits: mlirlite.Value* = mlirlite.build_bitcast(builder, dv, i64_ty) - if dbits is None: return 1 - if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, GenTypes.REPL_RES_FLOAT) != 0: - return 1 - if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, dbits) != 0: return 1 - return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) - if tag == GenTypes.REPL_TY_STR or tag == GenTypes.REPL_TY_PTR: - i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) - pv: mlirlite.Value* = val - if val.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(val.Ty) != 0 and self.Gen.AssignGen._stg_ty_equal(pool, val.Ty, i8_ptr_ty) == 0: - # Skip when already an i8 pointer (str/bytes/t.CPtr) - LLVM rejects same-type bitcast - pv = mlirlite.build_bitcast(builder, val, i8_ptr_ty) - if pv is None: return 1 - # Pointer -> i64 (ptrtoint): the host reads it as an integer then inttoptrs it back to an address - pbits: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, pv, i64_ty, node, trans) - if pbits is None: return 1 - kind: int = GenTypes.REPL_RES_OBJ - if tag == GenTypes.REPL_TY_STR: kind = GenTypes.REPL_RES_STR - if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, kind) != 0: - return 1 - if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, pbits) != 0: return 1 - return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) - if tag == GenTypes.REPL_TY_STRUCT: - # Struct value: inlined in the session slot so the value itself has no address; the result area - # only reports the category (kind=4 and result_a=0), and the host prints . - if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, GenTypes.REPL_RES_OBJ) != 0: - return 1 - if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, zero64) != 0: return 1 - return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) - return 1 - - # ---- _repl_store_res_i64 / _repl_store_res_i32 - write a fixed-width constant/value at a result offset ---- - def _repl_store_res_i64(self, trans: HT.Translator*, ctx_val: mlirlite.Value*, off: int, val: mlirlite.Value*) -> int: - """Write the i64 value to the ctx result area at offset off (0=success)""" - if trans is None or ctx_val is None or val is None: return 1 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, off, mlirlite.Int64(pool)) - if slot is None: return 1 - mlirlite.build_store(builder, val, slot) - return 0 - - def _repl_store_res_i32(self, trans: HT.Translator*, ctx_val: mlirlite.Value*, off: int, val: int) -> int: - """Write the i32 constant to the ctx result area at offset off (0=success)""" - if trans is None or ctx_val is None: return 1 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - if pool is None or builder is None: return 1 - slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, off, mlirlite.Int32(pool)) - if slot is None: return 1 - cv: mlirlite.Value* = mlirlite.const_int32(pool, val) - if cv is None: return 1 - mlirlite.build_store(builder, cv, slot) - return 0 - - # ---- repl_try_read - session variable read (returns the loaded value on a hit, else None) ---- - def repl_try_read(self, trans: HT.Translator*, name: str, node: ast.AST*) -> mlirlite.Value*: - """REPL session variable read: return the slot value on a hit (a box address for structs), else None.""" - if trans is None or trans.ReplCtx is None or name is None: return None - entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) - if entry is None: return None - if self.repl_in_frag_scope(trans.SymTab) == 0: return None - if entry.Kind == GenTypes.REPL_KIND_UNTYPED: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' has not been assigned yet (untyped; first run a 'x = ...' or 'x: int = ...')", "NameError") - return None - pool: memhub.MemBuddy* = trans.Pool - slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) - if slot_ty is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: the type " f"{self.repl_tag_label(entry.TyTag, entry.Bits)} of session variable '{name}' is not available in the current snippet" f" (cross-snippet type visibility belongs to M4)", "NameError") - return None - ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) - if ctx_val is None: return None - slot: mlirlite.Value* = self.repl_slot_ptr(pool, trans._cur_builder, ctx_val, entry.Offset, slot_ty) - if slot is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to generate the session slot address", "InternalCompilerError") - return None - return mlirlite.build_load(trans._cur_builder, slot_ty, slot) - - # ---- repl_try_addr - session variable address-of (c.Addr(x) -> ctx slot address) ---- - def repl_try_addr(self, trans: HT.Translator*, name: str, node: ast.AST*) -> mlirlite.Value*: - """Session variable address-of: return the slot address (Ptr(value type)); None when unregistered, untyped, or outside the fragment body.""" - if trans is None or trans.ReplCtx is None or name is None: return None - entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) - if entry is None: return None - if self.repl_in_frag_scope(trans.SymTab) == 0: return None - if entry.Kind == GenTypes.REPL_KIND_UNTYPED: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' has not been assigned yet (untyped), cannot take its address (c.Addr)", "NameError") - return None - pool: memhub.MemBuddy* = trans.Pool - slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) - if slot_ty is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: the type of session variable '{name}' is not available in the current snippet (cross-snippet type visibility belongs to M4)", "NameError") - return None - ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) - if ctx_val is None: return None - slot: mlirlite.Value* = self.repl_slot_ptr(pool, trans._cur_builder, ctx_val, entry.Offset, slot_ty) - if slot is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to generate the session slot address", "InternalCompilerError") - return None - # The value is inlined in the slot (no boxing indirection), so the slot address is the value address - return slot - - # ---- repl_store_slot - write a typed value into the slot (and into the result area) ---- - def repl_store_slot(self, trans: HT.Translator*, entry: GenTypes.ReplVarEntry*, ctx_val: mlirlite.Value*, rhs_val: mlirlite.Value*, node: ast.AST*) -> int: - """Coerce to the slot type, store into the slot, then write the result area; 0=success (fail-fast already raised on failure).""" - if trans is None or entry is None or ctx_val is None or rhs_val is None: return 1 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) - if slot_ty is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: the slot type of session variable '{entry.Name}' is not available" f" (the type is unresolvable in the current snippet, or the struct value exceeds the slot capacity)", "TypeError") - return 1 - store_val: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, slot_ty, node, trans) - if store_val is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: cannot convert the right-hand value of session variable '{entry.Name}' to" f" {self.repl_tag_label(entry.TyTag, entry.Bits)}", "CastError") - return 1 - if self.Gen.ExprGenBase.is_ptr_type(slot_ty) != 0 and store_val.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(store_val.Ty) != 0 and self.Gen.AssignGen._stg_ty_equal(pool, store_val.Ty, slot_ty) == 0: - # Pointer same-typing (the slot type and the RHS pointer may be different type instances but the same width). - # Skip when types are equal (including both i8*) - LLVM rejects same-type bitcast. - store_val = mlirlite.build_bitcast(builder, store_val, slot_ty) - if store_val is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to unify the type of the session slot pointer", "CastError") - return 1 - slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, entry.Offset, slot_ty) - if slot is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to write the session slot", "InternalCompilerError") - return 1 - mlirlite.build_store(builder, store_val, slot) - self.repl_emit_result(trans, ctx_val, store_val, entry.TyTag, entry.Bits, node) - return 0 - - # ---- repl_try_assign - session variable assignment (returns 1 on a hit) ---- - def repl_try_assign(self, trans: HT.Translator*, name: str, rhs_val: mlirlite.Value*, rhs_node: ast.AST*, node: ast.AST*) -> int: - """REPL session variable assignment: return 1 on a hit (registered and inside the fragment entry function), else 0.""" - if trans is None or trans.ReplCtx is None or name is None or rhs_val is None: return 0 - entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) - if entry is None: return 0 - if self.repl_in_frag_entry_scope(trans) == 0: return 0 - if rhs_val.Ty is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: the right-hand type of the assignment to session variable '{name}' is unknown", "TypeError") - return 1 - if self.repl_seal(trans, entry, rhs_val.Ty, rhs_node) != 0: return 1 - ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) - if ctx_val is None: return 1 - self.repl_store_slot(trans, entry, ctx_val, rhs_val, node) - return 1 - - # ---- repl_try_annassign - session variable annotated declaration/assignment (returns 1 on a hit) ---- - def repl_try_annassign(self, trans: HT.Translator*, name: str, annot_node: ast.AST*, value_node: ast.AST*, node: ast.AST*) -> int: - """REPL session variable annotated path (x: int = 1 / x: int, typing only): resolve the annotation, then type the entry and use it as the RHS target type.""" - if trans is None or trans.ReplCtx is None or name is None: return 0 - entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) - if entry is None: return 0 - if self.repl_in_frag_entry_scope(trans) == 0: return 0 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - if annot_node is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' is missing an annotation", "SyntaxError") - return 1 - # Same rule as ordinary AnnAssign: pre-register the generic specialization first so list[X] does not - # degrade to a bare base class (a bare list* makes later .get() misdispatch and truncate pointers). - self.Gen.AssignGen._prespecialize_generic_annotation(trans, pool, annot_node) - annot_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot_node, trans._imported_modules, trans._from_imports, trans, 0) - if annot_ty is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to resolve the type annotation of session variable '{name}'" f" (the snippet has no import; only built-in names such as int/str/bytes/bool/float/double/char can be resolved)", "NameError") - return 1 - if self.repl_seal(trans, entry, annot_ty, node) != 0: return 1 - if value_node is None: - # Typing only (x: int): no value produced; the host zeroes this line's result area, so no result - return 1 - slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) - # Same rule as ordinary AnnAssign: set the context before translating the RHS (for bare generic construction inference) - trans.CurAssignAnnot = annot_node - trans.CurAssignNode = value_node - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, trans.Module, value_node, None, 0, trans, slot_ty) - trans.CurAssignAnnot = None - trans.CurAssignNode = None - if rhs_val is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to translate the initial value of session variable '{name}'", "TypeError") - return 1 - ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) - if ctx_val is None: return 1 - self.repl_store_slot(trans, entry, ctx_val, rhs_val, node) - return 1 - - # ---- repl_try_augassign - session variable augmented assignment (returns 1 on a hit) ---- - def repl_try_augassign(self, trans: HT.Translator*, name: str, op: int, rhs_node: ast.AST*, node: ast.AST*) -> int: - """REPL session variable x += v: read slot, apply the binary op, write the slot and result area (return 1 on a hit).""" - if trans is None or trans.ReplCtx is None or name is None: return 0 - entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) - if entry is None: return 0 - if self.repl_in_frag_entry_scope(trans) == 0: return 0 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - if entry.TyTag == GenTypes.REPL_TY_STRUCT: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' is a struct value, augmented assignment is not supported", "TypeError") - return 1 - cur_val: mlirlite.Value* = self.repl_try_read(trans, name, node) - if cur_val is None: return 1 - rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, trans.Module, rhs_node, None, 0, trans) - if rhs_val is None: - self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to translate the right-hand side of the augmented assignment", "TypeError") - return 1 - result: mlirlite.Value* = self.Gen.AssignGen._apply_aug_op(pool, builder, op, cur_val, rhs_val, node, trans, node) - if result is None: - self.Gen.TypeGenErr.fatal_error(node, f"REPL: augmented assignment on session variable '{name}' failed op={op}", "TypeError") - return 1 - # Write back to the slot (reuses the assignment path: typing already exists, so only convert and store) - return self.repl_try_assign(trans, name, result, node, node) - - # ---- repl_expr_result - result pass-through for a bare expression statement in a fragment ---- - def repl_expr_result(self, trans: HT.Translator*, value_node: ast.AST*) -> int: - """In REPL mode, evaluate a bare expression statement (e.g. x + 1) and write the result area; 0=success.""" - if trans is None or trans.ReplCtx is None or value_node is None: return 1 - pool: memhub.MemBuddy* = trans.Pool - builder: mlirlite.IRBuilder* = trans._cur_builder - val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, trans.Module, value_node, None, 0, trans) - if val is None or val.Ty is None: - self.Gen.TypeGenErr.fatal_error(value_node, "REPL: failed to evaluate the expression snippet", "TypeError") - return 1 - ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, value_node) - if ctx_val is None: return 1 - # Type classification uses the host's scratch entry (occupies no session slot, not in the table); reset to untyped each time - sh: GenTypes.ReplVarEntry* = trans.ReplCtx.Scratch - if sh is None: - self.Gen.TypeGenErr.fatal_error(value_node, "REPL: session is missing the shadow entry (host not initialized)", "InternalCompilerError") - return 1 - sh.Kind = GenTypes.REPL_KIND_UNTYPED - sh.TyTag = 0 - sh.Bits = 0 - sh.TyName = None - if self.repl_seal(trans, sh, val.Ty, value_node) != 0: return 1 - return self.repl_emit_result(trans, ctx_val, val, sh.TyTag, sh.Bits, value_node) + """槽 slot 在 (idx, _g_slife_top) 里是否已经出现过;1=本次首次出现(该释放)/ 0=重复(跳过)""" + j: int = int(idx + 1) + while j < _g_slife_top: + if uint64(_g_slife_owned[j * 2]) == slot: return 0 + j += 1 + return 1 + + def _SlifeEmitFrees(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*) -> int: + """函数出口(所有 return / 隐式收尾):逆序释放**本函数**登记且仍拥有的 slife 对象。 + + 防御性去重:同一变量槽若在表里出现多次(登记路径异常 / 旧条目残留),只发一次 free—— + 对同一对象 free 两次是堆损坏(0xC0000374),绝不能出。""" + if pool is None or builder is None or _g_slife_owned is None: return 0 + i: int = _g_slife_top - 1 + while i >= _g_slife_count: + slot: uint64 = uint64(_g_slife_owned[i * 2]) + sh: mlirlite.Value* = mlirlite.Value*(_g_slife_owned[i * 2 + 1]) + if sh is not None and self._SlifeSlotFirstSeen(int(i), slot) != 0: + self._SlifeEmitGuardedFree(pool, builder, mod, trans, mlirlite.Value*(_g_slife_owned[i * 2]), sh) + i -= 1 + return 0 + + # lookup_module_var - look up a variable in the module scope + # ============================================================ + def lookup_module_var(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: + """Look up a variable in the module scope""" + if symtab is None or name is None: return None + scope: GenTypes.Scope* = symtab.Root + if scope is None: return None + return self.find_var(scope.Vars, scope.VarCount, name) + + # is_module_scope - whether the current scope is module level. + # Module-level AnnAssign modifies a module variable; inside a function (without global) it creates a local. + def is_module_scope(self, symtab: GenTypes.SymbolTable*) -> int: + """Whether the current scope is module level (Current == Root), 1=module / 0=function scope""" + if symtab is None: return 0 + if symtab.Root is None or symtab.Current is None: return 0 + # Compare pointers: module level when Current equals Root + if uint64(symtab.Current) == uint64(symtab.Root): return 1 + return 0 + + # _lookup_through_match_scope - look up a variable, walking through match-case scopes + def _lookup_through_match_scope(self, symtab: GenTypes.SymbolTable*, name: str) -> mlirlite.Value*: + """Walk up from the current scope through SCOPE_MATCH scopes only to find a variable alloca""" + if symtab is None or name is None: return None + scope: GenTypes.Scope* = symtab.Current + while scope is not None: + result: mlirlite.Value* = self.find_var(scope.Vars, scope.VarCount, name) + if result is not None: return result + if scope.Kind == SCOPE_MATCH: + scope = scope.Parent + continue + return None + return None + + # get_or_create_sym - find or create a variable (in the current scope) + # ============================================================ + def get_or_create_sym(self, symtab: GenTypes.SymbolTable*, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, name: str, ty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Find or create a variable alloca in the current scope""" + existing: mlirlite.Value* = self.lookup_current(symtab, name) + if existing is not None: return existing + # The SCOPE_MATCH isolation scope must be transparent to assignment: + existing = self._lookup_through_match_scope(symtab, name) + if existing is not None: return existing + alloca: mlirlite.Value* = self._alloca_at_entry(builder, ty) + if alloca is None: return None + if self.define_var(symtab, name, alloca) != 0: self.Gen.TypeGenErr.fatal_error(None, f"failed to register variable '{name}' (scope variable count exceeds the limit MAX_VARS)", "InternalCompilerError") + return alloca + + # REPL mode (M2) - session variable slot read/write; entered only when trans.ReplCtx != None (slots hold the value at ctx + x.Offset). + # ---- repl_find_entry - look up a session table entry by name ---- + def repl_find_entry(self, ctx: GenTypes.ReplCtx*, name: str) -> GenTypes.ReplVarEntry*: + """Look up an entry by name in the session table; None if ctx is invalid or unregistered""" + if ctx is None or name is None: return None + # Magic check: avoid treating a non-ReplCtx pointer as a context (better not to intercept than to over-intercept) + if ctx.Marker != GenTypes.REPL_CTX_MARKER: return None + if ctx.Table is None or ctx.Count <= 0: return None + entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__() + for i in range(int(ctx.Count)): + entry_addr: uint64 = uint64(ctx.Table) + i * entry_size + entry: GenTypes.ReplVarEntry* = GenTypes.ReplVarEntry*(entry_addr) + if entry.Name is not None and string.strcmp(entry.Name, name) == 0: return entry + return None + + # ---- repl_in_frag_scope - whether the nearest function scope is the fragment function itself ---- + def repl_in_frag_scope(self, symtab: GenTypes.SymbolTable*) -> int: + """1=the nearest function scope is the fragment body itself (its parent is the module root); 0=otherwise.""" + if symtab is None or symtab.Current is None or symtab.Root is None: return 0 + scope: GenTypes.Scope* = symtab.Current + while scope is not None: + if scope.Kind == SCOPE_FUNCTION: + if scope.Parent is not None and uint64(scope.Parent) == uint64(symtab.Root): return 1 + return 0 + scope = scope.Parent + return 0 + + # ---- repl_ctx_ptr - load the fragment function's ctx parameter value (i8*) ---- + def repl_ctx_ptr(self, trans: HT.Translator*, ctx: GenTypes.ReplCtx*, node: ast.AST*) -> mlirlite.Value*: + """Load the snippet function's ctx parameter (alloca built by FuncGenDef); fail fast on error""" + if trans is None or ctx is None or ctx.CtxParam is None: return None + symtab: GenTypes.SymbolTable* = trans.SymTab + param_alloca: mlirlite.Value* = self.lookup_var(symtab, ctx.CtxParam) + if param_alloca is None or param_alloca.Ty is None or param_alloca.Ty.Pointee == 0: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: snippet function is missing the ctx parameter '{ctx.CtxParam}' (wrapper template and M2 calling convention disagree)", "InternalCompilerError") + return None + return mlirlite.build_load(trans._cur_builder, param_alloca.Ty.Pointee, param_alloca) + + # ---- repl_in_frag_entry_scope - whether the nearest function scope is the fragment entry function ---- + # Differs from repl_in_frag_scope (M3): promoted session functions also parent to module root; write paths use this. + def repl_in_frag_entry_scope(self, trans: HT.Translator*) -> int: + """1=currently translating a snippet entry function body; 0=otherwise (including promoted session bodies)""" + if trans is None or trans._cur_func is None: return 0 + if self.repl_in_frag_scope(trans.SymTab) == 0: return 0 + cur_nm: str = mlirlite.function_get_name(trans._cur_func) + if cur_nm is None: return 0 + pfx: str = GenTypes.REPL_ENTRY_PREFIX + pl: sizet = string.strlen(pfx) + if string.strncmp(cur_nm, pfx, pl) != 0: return 0 + return 1 + + # ---- repl_find_func_entry - look up a session function table entry by name (M3) ---- + def repl_find_func_entry(self, ctx: GenTypes.ReplCtx*, name: str) -> GenTypes.ReplFuncEntry*: + """Look up by name in the session function table; None if ctx is invalid or unregistered (read-only)""" + if ctx is None or name is None: return None + if ctx.Marker != GenTypes.REPL_CTX_MARKER: return None + if ctx.FuncTable is None or ctx.FuncCount <= 0: return None + entry_size: sizet = GenTypes.ReplFuncEntry.__sizeof__() + for i in range(int(ctx.FuncCount)): + entry_addr: uint64 = uint64(ctx.FuncTable) + i * entry_size + entry: GenTypes.ReplFuncEntry* = GenTypes.ReplFuncEntry*(entry_addr) + if entry.Name is not None and string.strcmp(entry.Name, name) == 0: return entry + return None + + # ---- repl_func_ty - signature token -> LLVM type (M3) ---- + # Tokens come from the host parsing the define line; only iN/float/double/i8* are supported (others -> error). + def repl_func_ty(self, pool: memhub.MemBuddy*, tag: int, bits: int) -> mlirlite.LLVMType*: + """Map a signature token to an LLVM type; None for unsupported tokens""" + if pool is None: return None + if tag == GenTypes.REPL_TY_INT: return self.repl_scalar_ty(pool, bits, 0) + if tag == GenTypes.REPL_TY_FLOAT: return self.repl_scalar_ty(pool, bits, 1) + if tag == GenTypes.REPL_TY_STR: return mlirlite.Ptr(pool, mlirlite.Int8(pool)) + return None + + # ---- repl_func_sig_label - signature token -> human-readable type name (for error messages) ---- + def repl_func_sig_label(self, tag: int, bits: int) -> str: + """Map a signature token to a display name; "?" if unknown (VOID displays as void)""" + if tag == GenTypes.REPL_TY_VOID: return "void" + return self.repl_tag_label(tag, bits) + + # ---- repl_try_session_func_call - indirect call to a session function (M3 core) ---- + # On a hit, load the address from the ctx slot, prepend ctx, coerce args to the table signature, and call_indirect (hit=1). + def repl_try_session_func_call(self, trans: HT.Translator*, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, func_name: str, cargs: list[ast.AST*]*, can: int, kw_count: int, node: ast.AST*, hit: t.CPtr) -> mlirlite.Value*: + """Session function call: return the call result on hit (void returns None but hit is set)""" + if trans is None or trans.ReplCtx is None or pool is None or builder is None: return None + if func_name is None or hit is None: return None + ctx: GenTypes.ReplCtx* = trans.ReplCtx + entry: GenTypes.ReplFuncEntry* = self.repl_find_func_entry(ctx, func_name) + if entry is None: return None + # Same rule as M2's read path: the fragment entry function or a promoted session function may call + # other session functions; nested closures (no ctx param) are rejected, letting the existing path report. + if self.repl_in_frag_scope(trans.SymTab) == 0: return None + hit[0] = 1 + if kw_count != 0: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: session function '{func_name}' does not support keyword arguments (M3 supports positional arguments only)", "TypeError") + return None + if can != entry.ParamCount: + self.Gen.TypeGenErr.fatal_error( node, f"REPL: session function '{func_name}' expects {entry.ParamCount} positional arguments, but the call site passed {can} (M3 does not support default arguments or default completion)", "TypeError") + return None + ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, ctx, node) + if ctx_val is None: return None + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + # (1) Load the function address from the slot: the slot is i8** (Ptr(i8*)); load yields i8* + slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, entry.SlotOff, i8_ptr_ty) + if slot is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to generate the session function pointer slot address", "InternalCompilerError") + return None + fn_i8: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty, slot) + if fn_i8 is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to read the address of session function '{func_name}'", "InternalCompilerError") + return None + # (2) Resolve param types before building the signature (token -> LLVM type; unsupported -> error) + ptypes: t.CPtr = pool.alloc(8 * GenTypes.REPL_FUNC_PARAM_MAX) + if ptypes is None: return None + string.memset(ptypes, 0, 8 * GenTypes.REPL_FUNC_PARAM_MAX) + ai_chk: int + for ai_chk in range(int(entry.ParamCount)): + pty_chk: mlirlite.LLVMType* = self.repl_func_ty(pool, int(entry.ParamTags[ai_chk]), int(entry.ParamBits[ai_chk])) + if pty_chk is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: type token of parameter {ai_chk + 1} of session function '{func_name}' is not supported (M3 supports iN/float/double/i8*)", "TypeError") + return None + pty_slot: str* = (str*)(uint64(ptypes) + ai_chk * 8) + pty_slot[0] = pty_chk + # (3) Args: the first is this fragment's ctx; the rest are translated and coerced to the signature types + head: mlirlite.Value* = ctx_val + head.Next = None + tail: mlirlite.Value* = head + arg_count: int = 1 + ai: int + for ai in range(int(entry.ParamCount)): + arg: ast.AST* = cargs.get(ai) + if arg is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: argument {ai + 1} of session function '{func_name}' is missing", "SyntaxError") + return None + pty_slot2: str* = (str*)(uint64(ptypes) + ai * 8) + pty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(pty_slot2[0]) + arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans, pty) + if arg_val is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to translate argument {ai + 1} of session function '{func_name}'", "TypeError") + return None + coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, pty, arg, trans) + if coerced is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: argument {ai + 1} of session function '{func_name}' is incompatible with the signature type", "TypeError") + return None + coerced.Next = None + tail.Next = coerced + tail = coerced + arg_count += 1 + # (4) Function type: ret + (i8*, P1..Pn) (void uses mlirlite.Void; build_call_indirect returns None, + # so the host caller treats it as a void call). + ret_ty: mlirlite.LLVMType* = mlirlite.Void(pool) + if entry.RetTag != GenTypes.REPL_TY_VOID: + ret_ty = self.repl_func_ty(pool, entry.RetTag, entry.RetBits) + if ret_ty is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: return type token of session function '{func_name}' is not supported (M3 supports void/iN/float/double/i8*)", "TypeError") + return None + # Param list nodes are mlirlite.ParamNode (new_param_node returns rawptr[ParamNode], and + # paramnode_set_next / Func params are rawptr[ParamNode] too) - previously mistyped as Function*. + fp_first: mlirlite.ParamNode* = None + fp_prev: mlirlite.ParamNode* = None + pi: int + for pi in range(int(arg_count)): + cur_ty: mlirlite.LLVMType* = i8_ptr_ty + if pi > 0: + pty_slot3: str* = (str*)(uint64(ptypes) + (pi - 1) * 8) + cur_ty = (mlirlite.LLVMType*)(pty_slot3[0]) + pn: mlirlite.ParamNode* = mlirlite.new_param_node(pool, cur_ty) + if pn is not None: + if fp_first is None: fp_first = pn + if fp_prev is not None: mlirlite.paramnode_set_next(fp_prev, pn) + fp_prev = pn + func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, ret_ty, fp_first, arg_count) + if func_ty is None: return None + func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, func_ty) + fn_typed: mlirlite.Value* = mlirlite.build_bitcast(builder, fn_i8, func_ptr_ty) + if fn_typed is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to convert the address of session function '{func_name}'", "InternalCompilerError") + return None + return mlirlite.build_call_indirect(builder, fn_typed, head, arg_count, ret_ty) + + # ---- repl_slot_ptr - slot address (byte GEP from ctx + off, then bitcast to Ptr(val_ty)) ---- + def repl_slot_ptr(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, ctx_val: mlirlite.Value*, off: int, val_ty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Return the slot pointer (Ptr(val_ty)) via a byte-offset i8 GEP then bitcast; None if args incomplete.""" + if pool is None or builder is None or ctx_val is None or val_ty is None: return None + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + target_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, val_ty) + off_val: mlirlite.Value* = mlirlite.const_int64(pool, int64(off)) + if off_val is None: return None + base: mlirlite.Value* = mlirlite.build_gep(builder, i8_ty, ctx_val, off_val) + if base is None: return None + # Skip bitcast when types are equal: mlirlite types are not canonical, and LLVM rejects + # `bitcast i8* %x to i8*`. When val_ty is i8 the target is exactly i8*, so return the GEP result. + if base.Ty is not None and self.Gen.AssignGen._stg_ty_equal(pool, base.Ty, target_ptr_ty) != 0: return base + return mlirlite.build_bitcast(builder, base, target_ptr_ty) + + # ---- repl_scalar_ty - bit width -> LLVM scalar type ---- + def repl_scalar_ty(self, pool: memhub.MemBuddy*, bits: int, is_float: int) -> mlirlite.LLVMType*: + """Restore an LLVM scalar type from a bit width; None for unsupported widths""" + if is_float != 0: + if bits == 32: return mlirlite.Float(pool) + if bits == 64: return mlirlite.Double(pool) + return None + if bits == 1: return mlirlite.Int1(pool) + if bits == 8: return mlirlite.Int8(pool) + if bits == 16: return mlirlite.Int16(pool) + if bits == 32: return mlirlite.Int32(pool) + if bits == 64: return mlirlite.Int64(pool) + return None + + # ---- repl_llvm_ty - entry -> LLVM value type (structs re-resolved by name) ---- + def repl_llvm_ty(self, pool: memhub.MemBuddy*, entry: GenTypes.ReplVarEntry*) -> mlirlite.LLVMType*: + """Infer the entry's LLVM value type; None when untyped or a struct type cannot be re-resolved in this fragment.""" + if entry is None or pool is None: return None + if entry.Kind == GenTypes.REPL_KIND_UNTYPED: return None + if entry.TyTag == GenTypes.REPL_TY_INT: return self.repl_scalar_ty(pool, entry.Bits, 0) + if entry.TyTag == GenTypes.REPL_TY_FLOAT: return self.repl_scalar_ty(pool, entry.Bits, 1) + if entry.TyTag == GenTypes.REPL_TY_STR: return mlirlite.Ptr(pool, mlirlite.Int8(pool)) + if entry.TyTag == GenTypes.REPL_TY_PTR: + if entry.TyName is not None: + st: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type(entry.TyName) + if st is None: return None + return mlirlite.Ptr(pool, st) + ptee: mlirlite.LLVMType* = self.repl_scalar_ty(pool, entry.Bits, 0) + if ptee is None: ptee = mlirlite.Int8(pool) + return mlirlite.Ptr(pool, ptee) + if entry.TyTag == GenTypes.REPL_TY_STRUCT: + if entry.TyName is None: return None + return self.Gen.StructGen.get_struct_type(entry.TyName) + return None + + # ---- repl_slot_ty - the value type stored in the slot ---- + def repl_slot_ty(self, pool: memhub.MemBuddy*, entry: GenTypes.ReplVarEntry*) -> mlirlite.LLVMType*: + """The value type stored in the slot equals the entry's value type (scalars/pointers/structs inline); None if it overflows the slot.""" + if entry is None or pool is None: return None + vt: mlirlite.LLVMType* = self.repl_llvm_ty(pool, entry) + if vt is None: return None + if entry.TyTag == GenTypes.REPL_TY_STRUCT: + sz: int = self.repl_type_size(vt) + al: int = self.repl_type_align(vt) + if sz <= 0 or al <= 0 or sz > GenTypes.REPL_SLOT_SIZE or al > GenTypes.REPL_SLOT_SIZE: return None + return vt + + # ---- repl_type_align / repl_type_size - struct value size/alignment (LLVM standard layout) ---- + def repl_type_align(self, ty: mlirlite.LLVMType*, depth: int = 0) -> int: + """Natural alignment bytes of a type; 0 if unknown (deep guards recursion depth)""" + if ty is None or depth > 64: return 0 + ib: int = self.Gen.ExprGenBase.get_llvm_type_bits(ty) + if ib != 0: + if ib <= 8: return 1 + if ib <= 16: return 2 + if ib <= 32: return 4 + return 8 + fb: int = self.Gen.ExprGenBase.get_llvm_float_bits(ty) + if fb != 0: + if fb <= 16: return 2 + if fb <= 32: return 4 + return 8 + if self.Gen.ExprGenBase.is_ptr_type(ty) != 0: return 8 + elem_ty: mlirlite.LLVMType* = None + match ty: + case mlirlite.LLVMType.Array(elem_a, _cnt_a): + elem_ty = elem_a + case _: + pass + if elem_ty is not None: return self.repl_type_align(elem_ty, depth + 1) + fields: mlirlite.ParamNode* = mlirlite.get_struct_fields(ty) + if fields is None: return 0 + cur: mlirlite.ParamNode* = fields + best: int = 1 + while cur is not None: + fty: mlirlite.LLVMType* = mlirlite.paramnode_get_ty(cur) + fa: int = self.repl_type_align(fty, depth + 1) + if fa <= 0: return 0 + if fa > best: best = fa + cur = mlirlite.paramnode_get_next(cur) + return best + + def repl_type_size(self, ty: mlirlite.LLVMType*, depth: int = 0) -> int: + """Type size in bytes under LLVM standard layout (fields aligned with tail padding); 0 if unknown.""" + if ty is None or depth > 64: return 0 + ib: int = self.Gen.ExprGenBase.get_llvm_type_bits(ty) + if ib != 0: return (ib + 7) // 8 + fb: int = self.Gen.ExprGenBase.get_llvm_float_bits(ty) + if fb != 0: return fb // 8 + if self.Gen.ExprGenBase.is_ptr_type(ty) != 0: return 8 + elem_ty: mlirlite.LLVMType* = None + cnt: int = 0 + match ty: + case mlirlite.LLVMType.Array(elem_s, cnt_s): + elem_ty = elem_s + cnt = cnt_s + case _: + pass + if elem_ty is not None: + es: int = self.repl_type_size(elem_ty, depth + 1) + if es <= 0 or cnt <= 0: return 0 + return es * cnt + fields: mlirlite.ParamNode* = mlirlite.get_struct_fields(ty) + if fields is None: return 0 + align_struct: int = self.repl_type_align(ty, depth + 1) + if align_struct <= 0: return 0 + cur: mlirlite.ParamNode* = fields + off: int = 0 + counted: int = 0 + while cur is not None: + fty2: mlirlite.LLVMType* = mlirlite.paramnode_get_ty(cur) + fsz: int = self.repl_type_size(fty2, depth + 1) + fa2: int = self.repl_type_align(fty2, depth + 1) + if fsz <= 0 or fa2 <= 0: return 0 + rem: int = off % fa2 + if rem != 0: off += fa2 - rem + off += fsz + counted += 1 + cur = mlirlite.paramnode_get_next(cur) + if counted <= 0: return 0 + rem2: int = off % align_struct + if rem2 != 0: off += align_struct - rem2 + return off + + # ---- repl_seal - first-time typing (if already typed, verify the type tag matches) ---- + def repl_seal(self, trans: HT.Translator*, entry: GenTypes.ReplVarEntry*, val_ty: mlirlite.LLVMType*, node: ast.AST*) -> int: + """Type the entry by val_ty; return 0=success (idempotent hits included) / 1=unsupported or conflicting (fail-fast already raised).""" + if trans is None or entry is None or val_ty is None: return 1 + perm: memhub.MemBuddy* = self.repl_perm_pool(trans) + if perm is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: session permanent pool is unavailable (not injected by the host)", "InternalCompilerError") + return 1 + tag: int = 0 + bits: int = 0 + kind: int = GenTypes.REPL_KIND_SCALAR + tname: str = None + ib: int = self.Gen.ExprGenBase.get_llvm_type_bits(val_ty) + fb: int = self.Gen.ExprGenBase.get_llvm_float_bits(val_ty) + if ib != 0: + tag = GenTypes.REPL_TY_INT + bits = ib + elif fb != 0: + tag = GenTypes.REPL_TY_FLOAT + bits = fb + elif self.Gen.ExprGenBase.is_ptr_type(val_ty) != 0: + ptee: mlirlite.LLVMType* = val_ty.Pointee + pname: str = None + if ptee is not None: pname = mlirlite.get_struct_name(ptee) + if pname is not None: + tag = GenTypes.REPL_TY_PTR + bits = 64 + tname = pname + else: + pb: int = 0 + pf: int = 0 + if ptee is not None: + pb = self.Gen.ExprGenBase.get_llvm_type_bits(ptee) + pf = self.Gen.ExprGenBase.get_llvm_float_bits(ptee) + if pb == 8: + tag = GenTypes.REPL_TY_STR + bits = 8 + else: + tag = GenTypes.REPL_TY_PTR + if pb != 0: bits = pb + elif pf != 0: bits = pf + else: bits = 0 + else: + sname: str = mlirlite.get_struct_name(val_ty) + if sname is not None: + # Struct value: inlined in the slot by declared type (no boxing). The slot capacity/alignment + # limit is REPL_SLOT_SIZE (16 bytes); exceeding it is a hard error, never silently truncated. + sz: int = self.repl_type_size(val_ty) + al: int = self.repl_type_align(val_ty) + if sz <= 0 or al <= 0: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: size/alignment of the struct value '{sname}' of session variable '{entry.Name}' is unknown" f" (cannot determine whether it fits in a {GenTypes.REPL_SLOT_SIZE} byte slot)", "TypeError") + return 1 + if sz > GenTypes.REPL_SLOT_SIZE or al > GenTypes.REPL_SLOT_SIZE: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: struct value '{sname}' of session variable '{entry.Name}' is {sz} bytes and " f"aligned to {al} bytes, so it cannot fit in a {GenTypes.REPL_SLOT_SIZE} byte slot" f" (M2 struct values must be inlined into the slot; use an object pointer or heap allocation for large objects)", "TypeError") + return 1 + tag = GenTypes.REPL_TY_STRUCT + bits = sz * 8 + kind = GenTypes.REPL_KIND_SCALAR + tname = sname + else: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: the type of session variable '{entry.Name}' is not supported yet" f" (M2 supports int/bool/float/str/bytes/pointer/struct value)", "TypeError") + return 1 + if entry.Kind == GenTypes.REPL_KIND_UNTYPED: + entry.Kind = kind + entry.TyTag = tag + entry.Bits = bits + entry.TyName = None + if tname is not None: entry.TyName = self.repl_perm_strdup(perm, tname) + return 0 + if entry.Kind != kind or entry.TyTag != tag: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{entry.Name}' is already typed as {self.repl_tag_label(entry.TyTag, entry.Bits)} and " f"cannot be assigned {self.repl_tag_label(tag, bits)} (restart the session to change its type)", "TypeError") + return 1 + if tname is not None and entry.TyName is None: entry.TyName = self.repl_perm_strdup(perm, tname) + return 0 + + # ---- repl_perm_strdup - deep copy into PermPool (survives across fragments) ---- + def repl_perm_strdup(self, pool: memhub.MemBuddy*, s: str) -> str: + """Deep-copy s into pool (caller passes PermPool) for long-lived session storage; None on failure""" + if s is None or pool is None: return None + n: sizet = string.strlen(s) + buf: str = pool.alloc(n + 1) + if buf is None: return None + string.strcpy(buf, s) + return buf + + # ---- repl_tag_label - type tag -> display name (same convention as the host echo) ---- + def repl_tag_label(self, tag: int, bits: int) -> str: + """Map TyTag/Bits to a human-readable type name (for error messages only)""" + 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" + if tag == GenTypes.REPL_TY_PTR: return "ptr" + if tag == GenTypes.REPL_TY_STRUCT: return "struct" + return "?" + + # ---- repl_ensure_entry - allocate a slot when an untyped session variable name is first seen ---- + # Allocation happens during fragment translation on a local-table miss; a full table fails fast (no silent drop). + def repl_ensure_entry(self, trans: HT.Translator*, name: str, node: ast.AST*) -> GenTypes.ReplVarEntry*: + """Get an entry by name, allocating and registering a slot if absent; None on failure (fail-fast already raised).""" + if trans is None or name is None: return None + ctx: GenTypes.ReplCtx* = trans.ReplCtx + if ctx is None or ctx.Marker != GenTypes.REPL_CTX_MARKER: return None + hit: GenTypes.ReplVarEntry* = self.repl_find_entry(ctx, name) + if hit is not None: return hit + if ctx.Table is None or ctx.Cap <= 0: + self.Gen.TypeGenErr.fatal_error(node, "REPL: session variable table is not initialized (host did not create ctx)", "InternalCompilerError") + return None + if ctx.Count >= ctx.Cap: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variables have reached the limit of {ctx.Cap} (ctx slots exhausted); " f"cannot allocate another slot for '{name}'; restart the session", "ResourceError") + return None + perm: memhub.MemBuddy* = self.repl_perm_pool(trans) + if perm is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: session permanent pool is unavailable (not injected by the host)", "InternalCompilerError") + return None + name_copy: str = self.repl_perm_strdup(perm, name) + if name_copy is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to deep-copy the name of session variable '{name}'", "InternalCompilerError") + return None + entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__() + entry: GenTypes.ReplVarEntry* = GenTypes.ReplVarEntry*(uint64(ctx.Table) + uint64(ctx.Count) * uint64(entry_size)) + if entry is None: return None + entry.Name = name_copy + entry.TyName = None + entry.Offset = GenTypes.REPL_SLOT_BASE + ctx.Count * GenTypes.REPL_SLOT_SIZE + entry.Kind = GenTypes.REPL_KIND_UNTYPED + entry.TyTag = 0 + entry.Bits = 0 + ctx.Count = ctx.Count + 1 + return entry + + # ---- repl_perm_pool - the permanent pool for session data (survives across fragments) ---- + def repl_perm_pool(self, trans: HT.Translator*) -> memhub.MemBuddy*: + """The host-injected PermPool (ctx.Perm); None if there is no ctx or it was not injected""" + if trans is None or trans.ReplCtx is None: return None + return trans.ReplCtx.Perm + + # ---- repl_emit_result - write this fragment's result into the ctx result area ---- + def repl_emit_result(self, trans: HT.Translator*, ctx_val: mlirlite.Value*, val: mlirlite.Value*, tag: int, bits: int, node: ast.AST* = None) -> int: + """Write val into the ctx result area per tag (see GenTypes result_kind); 0=success. Host reads only (kind, result_a, result_b).""" + if trans is None or ctx_val is None or val is None: return 1 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + if pool is None or builder is None: return 1 + i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) + zero64: mlirlite.Value* = mlirlite.const_int64(pool, 0) + if tag == GenTypes.REPL_TY_INT: + iv: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, val, i64_ty, node, trans) + if iv is None: return 1 + if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, GenTypes.REPL_RES_INT) != 0: + return 1 + if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, iv) != 0: return 1 + return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) + if tag == GenTypes.REPL_TY_FLOAT: + dv: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, val, mlirlite.Double(pool), node, trans) + if dv is None: return 1 + # double -> i64 bit pattern (bitcast, no numeric conversion): the host reinterprets the bits back to double + dbits: mlirlite.Value* = mlirlite.build_bitcast(builder, dv, i64_ty) + if dbits is None: return 1 + if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, GenTypes.REPL_RES_FLOAT) != 0: + return 1 + if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, dbits) != 0: return 1 + return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) + if tag == GenTypes.REPL_TY_STR or tag == GenTypes.REPL_TY_PTR: + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) + pv: mlirlite.Value* = val + if val.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(val.Ty) != 0 and self.Gen.AssignGen._stg_ty_equal(pool, val.Ty, i8_ptr_ty) == 0: + # Skip when already an i8 pointer (str/bytes/t.CPtr) - LLVM rejects same-type bitcast + pv = mlirlite.build_bitcast(builder, val, i8_ptr_ty) + if pv is None: return 1 + # Pointer -> i64 (ptrtoint): the host reads it as an integer then inttoptrs it back to an address + pbits: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, pv, i64_ty, node, trans) + if pbits is None: return 1 + kind: int = GenTypes.REPL_RES_OBJ + if tag == GenTypes.REPL_TY_STR: kind = GenTypes.REPL_RES_STR + if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, kind) != 0: + return 1 + if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, pbits) != 0: return 1 + return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) + if tag == GenTypes.REPL_TY_STRUCT: + # Struct value: inlined in the session slot so the value itself has no address; the result area + # only reports the category (kind=4 and result_a=0), and the host prints . + if self._repl_store_res_i32(trans, ctx_val, GenTypes.REPL_RES_KIND_OFF, GenTypes.REPL_RES_OBJ) != 0: + return 1 + if self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_A_OFF, zero64) != 0: return 1 + return self._repl_store_res_i64(trans, ctx_val, GenTypes.REPL_RES_B_OFF, zero64) + return 1 + + # ---- _repl_store_res_i64 / _repl_store_res_i32 - write a fixed-width constant/value at a result offset ---- + def _repl_store_res_i64(self, trans: HT.Translator*, ctx_val: mlirlite.Value*, off: int, val: mlirlite.Value*) -> int: + """Write the i64 value to the ctx result area at offset off (0=success)""" + if trans is None or ctx_val is None or val is None: return 1 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, off, mlirlite.Int64(pool)) + if slot is None: return 1 + mlirlite.build_store(builder, val, slot) + return 0 + + def _repl_store_res_i32(self, trans: HT.Translator*, ctx_val: mlirlite.Value*, off: int, val: int) -> int: + """Write the i32 constant to the ctx result area at offset off (0=success)""" + if trans is None or ctx_val is None: return 1 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + if pool is None or builder is None: return 1 + slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, off, mlirlite.Int32(pool)) + if slot is None: return 1 + cv: mlirlite.Value* = mlirlite.const_int32(pool, val) + if cv is None: return 1 + mlirlite.build_store(builder, cv, slot) + return 0 + + # ---- repl_try_read - session variable read (returns the loaded value on a hit, else None) ---- + def repl_try_read(self, trans: HT.Translator*, name: str, node: ast.AST*) -> mlirlite.Value*: + """REPL session variable read: return the slot value on a hit (a box address for structs), else None.""" + if trans is None or trans.ReplCtx is None or name is None: return None + entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) + if entry is None: return None + if self.repl_in_frag_scope(trans.SymTab) == 0: return None + if entry.Kind == GenTypes.REPL_KIND_UNTYPED: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' has not been assigned yet (untyped; first run a 'x = ...' or 'x: int = ...')", "NameError") + return None + pool: memhub.MemBuddy* = trans.Pool + slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) + if slot_ty is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: the type " f"{self.repl_tag_label(entry.TyTag, entry.Bits)} of session variable '{name}' is not available in the current snippet" f" (cross-snippet type visibility belongs to M4)", "NameError") + return None + ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) + if ctx_val is None: return None + slot: mlirlite.Value* = self.repl_slot_ptr(pool, trans._cur_builder, ctx_val, entry.Offset, slot_ty) + if slot is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to generate the session slot address", "InternalCompilerError") + return None + return mlirlite.build_load(trans._cur_builder, slot_ty, slot) + + # ---- repl_try_addr - session variable address-of (c.Addr(x) -> ctx slot address) ---- + def repl_try_addr(self, trans: HT.Translator*, name: str, node: ast.AST*) -> mlirlite.Value*: + """Session variable address-of: return the slot address (Ptr(value type)); None when unregistered, untyped, or outside the fragment body.""" + if trans is None or trans.ReplCtx is None or name is None: return None + entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) + if entry is None: return None + if self.repl_in_frag_scope(trans.SymTab) == 0: return None + if entry.Kind == GenTypes.REPL_KIND_UNTYPED: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' has not been assigned yet (untyped), cannot take its address (c.Addr)", "NameError") + return None + pool: memhub.MemBuddy* = trans.Pool + slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) + if slot_ty is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: the type of session variable '{name}' is not available in the current snippet (cross-snippet type visibility belongs to M4)", "NameError") + return None + ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) + if ctx_val is None: return None + slot: mlirlite.Value* = self.repl_slot_ptr(pool, trans._cur_builder, ctx_val, entry.Offset, slot_ty) + if slot is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to generate the session slot address", "InternalCompilerError") + return None + # The value is inlined in the slot (no boxing indirection), so the slot address is the value address + return slot + + # ---- repl_store_slot - write a typed value into the slot (and into the result area) ---- + def repl_store_slot(self, trans: HT.Translator*, entry: GenTypes.ReplVarEntry*, ctx_val: mlirlite.Value*, rhs_val: mlirlite.Value*, node: ast.AST*) -> int: + """Coerce to the slot type, store into the slot, then write the result area; 0=success (fail-fast already raised on failure).""" + if trans is None or entry is None or ctx_val is None or rhs_val is None: return 1 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) + if slot_ty is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: the slot type of session variable '{entry.Name}' is not available" f" (the type is unresolvable in the current snippet, or the struct value exceeds the slot capacity)", "TypeError") + return 1 + store_val: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, slot_ty, node, trans) + if store_val is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: cannot convert the right-hand value of session variable '{entry.Name}' to" f" {self.repl_tag_label(entry.TyTag, entry.Bits)}", "CastError") + return 1 + if self.Gen.ExprGenBase.is_ptr_type(slot_ty) != 0 and store_val.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(store_val.Ty) != 0 and self.Gen.AssignGen._stg_ty_equal(pool, store_val.Ty, slot_ty) == 0: + # Pointer same-typing (the slot type and the RHS pointer may be different type instances but the same width). + # Skip when types are equal (including both i8*) - LLVM rejects same-type bitcast. + store_val = mlirlite.build_bitcast(builder, store_val, slot_ty) + if store_val is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to unify the type of the session slot pointer", "CastError") + return 1 + slot: mlirlite.Value* = self.repl_slot_ptr(pool, builder, ctx_val, entry.Offset, slot_ty) + if slot is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to write the session slot", "InternalCompilerError") + return 1 + mlirlite.build_store(builder, store_val, slot) + self.repl_emit_result(trans, ctx_val, store_val, entry.TyTag, entry.Bits, node) + return 0 + + # ---- repl_try_assign - session variable assignment (returns 1 on a hit) ---- + def repl_try_assign(self, trans: HT.Translator*, name: str, rhs_val: mlirlite.Value*, rhs_node: ast.AST*, node: ast.AST*) -> int: + """REPL session variable assignment: return 1 on a hit (registered and inside the fragment entry function), else 0.""" + if trans is None or trans.ReplCtx is None or name is None or rhs_val is None: return 0 + entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) + if entry is None: return 0 + if self.repl_in_frag_entry_scope(trans) == 0: return 0 + if rhs_val.Ty is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: the right-hand type of the assignment to session variable '{name}' is unknown", "TypeError") + return 1 + if self.repl_seal(trans, entry, rhs_val.Ty, rhs_node) != 0: return 1 + ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) + if ctx_val is None: return 1 + self.repl_store_slot(trans, entry, ctx_val, rhs_val, node) + return 1 + + # ---- repl_try_annassign - session variable annotated declaration/assignment (returns 1 on a hit) ---- + def repl_try_annassign(self, trans: HT.Translator*, name: str, annot_node: ast.AST*, value_node: ast.AST*, node: ast.AST*) -> int: + """REPL session variable annotated path (x: int = 1 / x: int, typing only): resolve the annotation, then type the entry and use it as the RHS target type.""" + if trans is None or trans.ReplCtx is None or name is None: return 0 + entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) + if entry is None: return 0 + if self.repl_in_frag_entry_scope(trans) == 0: return 0 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + if annot_node is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' is missing an annotation", "SyntaxError") + return 1 + # Same rule as ordinary AnnAssign: pre-register the generic specialization first so list[X] does not + # degrade to a bare base class (a bare list* makes later .get() misdispatch and truncate pointers). + self.Gen.AssignGen._prespecialize_generic_annotation(trans, pool, annot_node) + annot_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot_node, trans._imported_modules, trans._from_imports, trans, 0) + if annot_ty is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to resolve the type annotation of session variable '{name}'" f" (the snippet has no import; only built-in names such as int/str/bytes/bool/float/double/char can be resolved)", "NameError") + return 1 + if self.repl_seal(trans, entry, annot_ty, node) != 0: return 1 + if value_node is None: + # Typing only (x: int): no value produced; the host zeroes this line's result area, so no result + return 1 + slot_ty: mlirlite.LLVMType* = self.repl_slot_ty(pool, entry) + # Same rule as ordinary AnnAssign: set the context before translating the RHS (for bare generic construction inference) + trans.CurAssignAnnot = annot_node + trans.CurAssignNode = value_node + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, trans.Module, value_node, None, 0, trans, slot_ty) + trans.CurAssignAnnot = None + trans.CurAssignNode = None + if rhs_val is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: failed to translate the initial value of session variable '{name}'", "TypeError") + return 1 + ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, node) + if ctx_val is None: return 1 + self.repl_store_slot(trans, entry, ctx_val, rhs_val, node) + return 1 + + # ---- repl_try_augassign - session variable augmented assignment (returns 1 on a hit) ---- + def repl_try_augassign(self, trans: HT.Translator*, name: str, op: int, rhs_node: ast.AST*, node: ast.AST*) -> int: + """REPL session variable x += v: read slot, apply the binary op, write the slot and result area (return 1 on a hit).""" + if trans is None or trans.ReplCtx is None or name is None: return 0 + entry: GenTypes.ReplVarEntry* = self.repl_find_entry(trans.ReplCtx, name) + if entry is None: return 0 + if self.repl_in_frag_entry_scope(trans) == 0: return 0 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + if entry.TyTag == GenTypes.REPL_TY_STRUCT: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: session variable '{name}' is a struct value, augmented assignment is not supported", "TypeError") + return 1 + cur_val: mlirlite.Value* = self.repl_try_read(trans, name, node) + if cur_val is None: return 1 + rhs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, trans.Module, rhs_node, None, 0, trans) + if rhs_val is None: + self.Gen.TypeGenErr.fatal_error(node, "REPL: failed to translate the right-hand side of the augmented assignment", "TypeError") + return 1 + result: mlirlite.Value* = self.Gen.AssignGen._apply_aug_op(pool, builder, op, cur_val, rhs_val, node, trans, node) + if result is None: + self.Gen.TypeGenErr.fatal_error(node, f"REPL: augmented assignment on session variable '{name}' failed op={op}", "TypeError") + return 1 + # Write back to the slot (reuses the assignment path: typing already exists, so only convert and store) + return self.repl_try_assign(trans, name, result, node, node) + + # ---- repl_expr_result - result pass-through for a bare expression statement in a fragment ---- + def repl_expr_result(self, trans: HT.Translator*, value_node: ast.AST*) -> int: + """In REPL mode, evaluate a bare expression statement (e.g. x + 1) and write the result area; 0=success.""" + if trans is None or trans.ReplCtx is None or value_node is None: return 1 + pool: memhub.MemBuddy* = trans.Pool + builder: mlirlite.IRBuilder* = trans._cur_builder + val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, trans.Module, value_node, None, 0, trans) + if val is None or val.Ty is None: + self.Gen.TypeGenErr.fatal_error(value_node, "REPL: failed to evaluate the expression snippet", "TypeError") + return 1 + ctx_val: mlirlite.Value* = self.repl_ctx_ptr(trans, trans.ReplCtx, value_node) + if ctx_val is None: return 1 + # Type classification uses the host's scratch entry (occupies no session slot, not in the table); reset to untyped each time + sh: GenTypes.ReplVarEntry* = trans.ReplCtx.Scratch + if sh is None: + self.Gen.TypeGenErr.fatal_error(value_node, "REPL: session is missing the shadow entry (host not initialized)", "InternalCompilerError") + return 1 + sh.Kind = GenTypes.REPL_KIND_UNTYPED + sh.TyTag = 0 + sh.Bits = 0 + sh.TyName = None + if self.repl_seal(trans, sh, val.Ty, value_node) != 0: return 1 + return self.repl_emit_result(trans, ctx_val, val, sh.TyTag, sh.Bits, value_node) diff --git a/App/lib/core/Repl.vp b/App/lib/core/Repl.vp index 092d389..442930c 100644 --- a/App/lib/core/Repl.vp +++ b/App/lib/core/Repl.vp @@ -1,2106 +1,2106 @@ -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 \n" " Template location: TransPyV/repl.vpj in the repo; copy it next to TransPyV.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%\TransPyV_repl\ (keeps user projects clean; pid isolates sessions). -ReplTmpDir: bytes = None -# Fragment symbol name __repl_frag_. -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 @(%__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 (.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 (.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 => \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 => \n", frag_name) - return 0 - stdio.printf("%s => \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 - elif cdef_dbl is not None and int(cdef_dbl.get(i)) == 1: kind = 1 - nc: str = _repl_str_dup(_perm_pool, nm) - vc: str = _repl_str_dup(_perm_pool, val) - if nc is None or vc is None: continue - j: int = ReplBaseCDefineCount - ReplBaseCDefineNames[j] = sizet(nc) - ReplBaseCDefineValues[j] = sizet(vc) - ReplBaseCDefineKinds[j] = sizet(kind) - ReplBaseCDefineCount = j + 1 - ct_names: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefNames - if ct_names is not None: - ct_vals: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefTypeStrs - cn2: sizet = len(ct_names) - i2: sizet - for i2 in range(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%\TransPyV_repl\\ 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 TransPyV_repl/ (pid isolates sessions; paths use '/'). - ReplTmpDir = _perm_pool.alloc(_REPL_PATH_CAP) - if ReplTmpDir is None: return 1 - viperlib.snprintf(ReplTmpDir, _REPL_PATH_CAP, "%sTransPyV_repl/%u", tmp_buf, win32process.GetCurrentProcessId()) - if BuildPipeline.ensure_dir(ReplTmpDir) != 0: return 1 - # 3) Permanent buffers (live for the session, not re-allocated per fragment). - ReplNameBuf = _perm_pool.alloc(_REPL_NAME_CAP) - ReplSrcBuf = _perm_pool.alloc(_REPL_FRAG_SRC_CAP) - ReplSrcPath = _perm_pool.alloc(_REPL_PATH_CAP) - ReplIrBuf = _perm_pool.alloc(_REPL_IR_CAP) - ReplNeedleBuf = _perm_pool.alloc(_REPL_NEEDLE_CAP) - ReplBlockBuf = _perm_pool.alloc(_REPL_BLOCK_CAP) - ReplBlockLen = 0 - ReplInBlock = 0 - if ReplNameBuf is None or ReplSrcBuf is None or ReplSrcPath is None: return 1 - if ReplIrBuf is None or ReplNeedleBuf is None: return 1 - if ReplBlockBuf is None: return 1 - ReplBlockBuf[0] = 0 - # 3') M3 permanent buffers: lifted source / name table / sig token scratch / render buffers. - ReplLiftBuf = _perm_pool.alloc(_REPL_LIFT_CAP) - ReplLiftNames = _perm_pool.alloc(_REPL_FN_NAMES_CAP) - ReplSigTags = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) - ReplSigBits = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) - ReplSigResult = _perm_pool.alloc(ReplSigOut.__sizeof__()) - ReplLineShow = _perm_pool.alloc(_REPL_DIAG_CAP) - ReplMsgName = _perm_pool.alloc(_REPL_FN_NAME_MAX) - ReplSigText = _perm_pool.alloc(_REPL_SIG_TEXT_CAP) - ReplSigText2 = _perm_pool.alloc(_REPL_SIG_TEXT_CAP) - if ReplLiftBuf is None or ReplLiftNames is None or ReplSigTags is None or ReplSigBits is None or ReplSigResult is None: return 1 - if ReplLineShow is None or ReplMsgName is None or ReplSigText is None or ReplSigText2 is None: return 1 - ReplLiftBuf[0] = 0 - ReplLiftNames[0] = 0 - ReplLiftNameCount = 0 - ReplWrapPos = 0 - ReplSavedFuncCount = 0 - # 4) ctx session memory (fragment's first arg): 64B header + 64x16B var slots + 32x16B func slots, zeroed. - ReplCtxBuf = _perm_pool.alloc(_REPL_CTX_SIZE) - if ReplCtxBuf is None: return 1 - string.memset(ReplCtxBuf, 0, _REPL_CTX_SIZE) - # 5) Session variable table + scratch entry (PermPool; names deep-copied by Gen into the same pool). - entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__() - ReplVarTable = _perm_pool.alloc(entry_size * GenTypes.REPL_VAR_CAP) - if ReplVarTable is None: return 1 - string.memset(ReplVarTable, 0, entry_size * GenTypes.REPL_VAR_CAP) - ReplScratch = _perm_pool.alloc(entry_size) - if ReplScratch is None: return 1 - string.memset(ReplScratch, 0, entry_size) - # Scratch entry name (readable in error messages: ""). - repl_scratch_name: str = _perm_pool.alloc(8) - if repl_scratch_name is None: return 1 - viperlib.snprintf(repl_scratch_name, 8, "%s", "") - 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_") 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 "."); - # 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 ("."), 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 (.dll.a) not found -- " "cross-module symbols (of the form <16-hex>.) cannot be resolved; " "build the host with TransPyV/project.vpj (including -Wl,--export-all-symbols), " "and keep .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>., " "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("TransPyV 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.),\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: .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 " "TransPyV/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 +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 \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\ (keeps user projects clean; pid isolates sessions). +ReplTmpDir: bytes = None +# Fragment symbol name __repl_frag_. +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 @(%__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 (.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 (.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 => \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 => \n", frag_name) + return 0 + stdio.printf("%s => \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 + elif cdef_dbl is not None and int(cdef_dbl.get(i)) == 1: kind = 1 + nc: str = _repl_str_dup(_perm_pool, nm) + vc: str = _repl_str_dup(_perm_pool, val) + if nc is None or vc is None: continue + j: int = ReplBaseCDefineCount + ReplBaseCDefineNames[j] = sizet(nc) + ReplBaseCDefineValues[j] = sizet(vc) + ReplBaseCDefineKinds[j] = sizet(kind) + ReplBaseCDefineCount = j + 1 + ct_names: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefNames + if ct_names is not None: + ct_vals: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefTypeStrs + cn2: sizet = len(ct_names) + i2: sizet + for i2 in range(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\\ 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 ViperC_repl/ (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: ""). + repl_scratch_name: str = _perm_pool.alloc(8) + if repl_scratch_name is None: return 1 + viperlib.snprintf(repl_scratch_name, 8, "%s", "") + 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_") 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 "."); + # 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 ("."), 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 (.dll.a) not found -- " "cross-module symbols (of the form <16-hex>.) cannot be resolved; " "build the host with ViperC/project.vpj (including -Wl,--export-all-symbols), " "and keep .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>., " "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.),\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: .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